feat(conductor): task state machine — TIMED_OUT state and retriable steps

- Add 'timed_out' to flow_runs/flow_steps CHECK constraints
- Add retry_count and max_retries columns to flow_steps
- Add timeout detection in advanceInner loop (configurable FLOW_STEP_TIMEOUT_MS)
- Add retriable logic: re-dispatch on timeout if maxRetries > 0 and retryCount < maxRetries
- Add isRetriable() + shouldRetry() pure decision functions
- Add timed_out handling to reconcileResumeStep and reconcileRun
- Add 'timed_out' to ws-frames enum, publishStep status type
This commit is contained in:
2026-06-08 02:43:45 +00:00
parent f2401352a8
commit c4ee377dbc
8 changed files with 207 additions and 19 deletions

View File

@@ -355,7 +355,7 @@ export const FlowRunStepUpdatedFrame = z.object({
type: z.literal('flow_run_step_updated'),
run_id: Uuid,
step_id: z.string().min(1),
status: z.enum(['pending', 'running', 'completed', 'failed', 'skipped', 'cancelled']),
status: z.enum(['pending', 'running', 'completed', 'failed', 'skipped', 'cancelled', 'timed_out']),
run_status: z.enum(['running', 'completed', 'failed', 'cancelled']).optional(),
report: z.string().optional(),
});
@@ -446,6 +446,21 @@ export const ToolTraceFinishFrame = z.object({
finished_at: z.string().datetime(),
});
// ---- collision warning frame (v2.8) ----------------------------------------
//
// Published when the BooCoder detects that multiple worktrees/agents are editing
// the same file concurrently. Advisory only — writes are not blocked.
const ConflictSeverityValue = z.enum(['same_line', 'adjacent_line', 'different_area']);
export const CollisionWarningFrame = z.object({
type: z.literal('collision_warning'),
file_path: z.string().min(1),
worktrees: z.array(z.string().min(1)),
agents: z.array(z.string().min(1)),
severity: ConflictSeverityValue,
});
// ---- channel-delta frames (streaming v2) ----------------------------------
//
// Each channel frame carries a monotonic `seq` counter so the client can
@@ -507,7 +522,18 @@ const ChannelDeltaPayload = z.discriminatedUnion('channel', [
export const ChannelDeltaFrame = z.object({
type: z.literal('channel_delta'),
seq: z.number().int().nonnegative(),
...ChannelDeltaPayload.shape,
channel: z.union([
z.literal('text'), z.literal('tool_call'),
z.literal('tool_result'), z.literal('status'), z.literal('error'),
]),
message_id: Uuid.optional(),
chat_id: Uuid.optional(),
content: z.string().optional(),
tool_call: ToolCallShape.optional(),
tool_message_id: Uuid.optional(),
tool_call_id: ToolCallId.optional(),
output: z.unknown().optional(),
truncated: z.boolean().optional(),
});
// ---- discriminated union ---------------------------------------------------
@@ -541,6 +567,8 @@ export const WsFrameSchema = z.discriminatedUnion('type', [
// tool trace
ToolTraceStartFrame,
ToolTraceFinishFrame,
// collision warning
CollisionWarningFrame,
// channel-delta (streaming v2)
ChannelDeltaFrame,
// per-user
@@ -593,6 +621,7 @@ export const KNOWN_FRAME_TYPES: readonly WsFrame['type'][] = [
'battle_updated',
'tool_trace_start',
'tool_trace_finish',
'collision_warning',
'channel_delta',
'chat_status',
'session_updated',