mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Fix mobile agent terminal state handling
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -7,7 +7,9 @@ import 'observer_subscription.dart';
|
||||
|
||||
const _defaultLivenessTimeout = Duration(seconds: 30);
|
||||
const _activeTurnClockInterval = Duration(seconds: 5);
|
||||
const _maximumLivenessInterval = Duration(hours: 24);
|
||||
// Matches buzz-acp's MAX_TURN_DURATION_CEILING_SECS. A disabled or unusually
|
||||
// sparse liveness cadence must not expire before any legal turn can finish.
|
||||
const _maximumTurnDuration = Duration(days: 7);
|
||||
const _livenessTimeoutSlack = Duration(seconds: 30);
|
||||
|
||||
/// Lifecycle state reconstructed from owner-scoped observer frames.
|
||||
@@ -114,6 +116,12 @@ List<AgentTurnState> reduceAgentTurnStates(
|
||||
if (turnId != null) {
|
||||
terminalOrderById[turnId] = frameOrderAt;
|
||||
final existing = turnsById[turnId];
|
||||
// The harness's generic completion guard can run after its result
|
||||
// handler emits the specific failure outcome.
|
||||
if (frame.kind == 'turn_completed' &&
|
||||
existing?.phase == AgentTurnPhase.error) {
|
||||
continue;
|
||||
}
|
||||
final channelId = existing?.channelId ?? frame.channelId;
|
||||
if (channelId == null) continue;
|
||||
turnsById[turnId] =
|
||||
@@ -293,11 +301,11 @@ Duration _livenessTimeout(dynamic payload) {
|
||||
|
||||
final intervalSeconds = rawInterval.toInt();
|
||||
if (intervalSeconds <= 0) {
|
||||
return _maximumLivenessInterval + _livenessTimeoutSlack;
|
||||
return _maximumTurnDuration + _livenessTimeoutSlack;
|
||||
}
|
||||
final boundedInterval = intervalSeconds.clamp(
|
||||
5,
|
||||
_maximumLivenessInterval.inSeconds,
|
||||
_maximumTurnDuration.inSeconds,
|
||||
);
|
||||
final timeoutSeconds = boundedInterval + _livenessTimeoutSlack.inSeconds;
|
||||
return Duration(
|
||||
|
||||
@@ -50,6 +50,26 @@ void main() {
|
||||
expect(turns[1].errorMessage, 'Tool permission denied');
|
||||
});
|
||||
|
||||
test('keeps an error terminal when generic completion arrives later', () {
|
||||
final turns = reduceAgentTurnStates({
|
||||
'agent-a': [
|
||||
_frame(seq: 1, second: 1, kind: 'turn_started'),
|
||||
_frame(
|
||||
seq: 2,
|
||||
second: 2,
|
||||
kind: 'turn_error',
|
||||
payload: {'error': 'Agent timed out'},
|
||||
),
|
||||
_frame(seq: 3, second: 3, kind: 'turn_completed'),
|
||||
],
|
||||
}, now: DateTime.utc(2026, 8, 16, 12, 1));
|
||||
|
||||
expect(turns, hasLength(1));
|
||||
expect(turns.single.phase, AgentTurnPhase.error);
|
||||
expect(turns.single.errorMessage, 'Agent timed out');
|
||||
expect(turns.single.terminalAt, DateTime.utc(2026, 8, 16, 12, 0, 2));
|
||||
});
|
||||
|
||||
test('expires silence without claiming the turn finished', () {
|
||||
final turns = reduceAgentTurnStates({
|
||||
'agent-a': [_frame(seq: 1, second: 1, kind: 'turn_started')],
|
||||
@@ -84,6 +104,35 @@ void main() {
|
||||
expect(afterTimeout, isEmpty);
|
||||
});
|
||||
|
||||
test('honors advertised liveness intervals longer than one day', () {
|
||||
final frames = {
|
||||
'agent-a': [
|
||||
_frame(
|
||||
seq: 1,
|
||||
second: 1,
|
||||
kind: 'turn_started',
|
||||
payload: {'livenessIntervalSecs': 48 * 60 * 60},
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
final beforeTimeout = reduceAgentTurnStates(
|
||||
frames,
|
||||
now: DateTime.utc(2026, 8, 18, 12, 0, 30),
|
||||
);
|
||||
final afterTimeout = reduceAgentTurnStates(
|
||||
frames,
|
||||
now: DateTime.utc(2026, 8, 18, 12, 0, 32),
|
||||
);
|
||||
|
||||
expect(beforeTimeout, hasLength(1));
|
||||
expect(
|
||||
beforeTimeout.single.livenessTimeout,
|
||||
const Duration(hours: 48, seconds: 30),
|
||||
);
|
||||
expect(afterTimeout, isEmpty);
|
||||
});
|
||||
|
||||
test('keeps liveness-disabled turns until the bounded crash backstop', () {
|
||||
final frames = {
|
||||
'agent-a': [
|
||||
@@ -98,17 +147,17 @@ void main() {
|
||||
|
||||
final longRunning = reduceAgentTurnStates(
|
||||
frames,
|
||||
now: DateTime.utc(2026, 8, 17, 12),
|
||||
now: DateTime.utc(2026, 8, 23, 12),
|
||||
);
|
||||
final pastBackstop = reduceAgentTurnStates(
|
||||
frames,
|
||||
now: DateTime.utc(2026, 8, 17, 12, 0, 32),
|
||||
now: DateTime.utc(2026, 8, 23, 12, 0, 32),
|
||||
);
|
||||
|
||||
expect(longRunning, hasLength(1));
|
||||
expect(
|
||||
longRunning.single.livenessTimeout,
|
||||
const Duration(hours: 24, seconds: 30),
|
||||
const Duration(days: 7, seconds: 30),
|
||||
);
|
||||
expect(pastBackstop, isEmpty);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user