From 6eca0dad9695b1d2818ad29828b839f9b9aef6cc Mon Sep 17 00:00:00 2001 From: John Fawcett Date: Thu, 19 Mar 2026 03:42:59 +0000 Subject: [PATCH 1/4] feat: add FailureReason type and extend updateBeadStatus() to persist failure reasons - Add FailureReason type to cloudflare-gastown/src/types.ts - Extend updateBeadStatus() with optional failureReason param (5th arg) - When status is 'failed' and failureReason is provided, logBeadEvent stores it in event metadata as { failure_reason: { code, message, details?, source } } - All existing callers continue to work unchanged (param is optional) --- cloudflare-gastown/src/dos/town/beads.ts | 5 ++++- cloudflare-gastown/src/types.ts | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/cloudflare-gastown/src/dos/town/beads.ts b/cloudflare-gastown/src/dos/town/beads.ts index 3a641db71d..648fc56d3e 100644 --- a/cloudflare-gastown/src/dos/town/beads.ts +++ b/cloudflare-gastown/src/dos/town/beads.ts @@ -40,6 +40,7 @@ import type { BeadStatus, BeadPriority, BeadType, + FailureReason, } from '../../types'; import type { BeadEventType } from '../../db/tables/bead-events.table'; @@ -251,7 +252,8 @@ export function updateBeadStatus( sql: SqlStorage, beadId: string, status: string, - agentId: string | null + agentId: string | null, + failureReason?: FailureReason ): Bead { const bead = getBead(sql, beadId); if (!bead) throw new Error(`Bead ${beadId} not found`); @@ -281,6 +283,7 @@ export function updateBeadStatus( eventType: 'status_changed', oldValue: oldStatus, newValue: status, + metadata: status === 'failed' && failureReason ? { failure_reason: failureReason } : {}, }); // If the bead reached a terminal status and is tracked by a convoy, diff --git a/cloudflare-gastown/src/types.ts b/cloudflare-gastown/src/types.ts index d24b683ba9..43d89e2621 100644 --- a/cloudflare-gastown/src/types.ts +++ b/cloudflare-gastown/src/types.ts @@ -180,6 +180,19 @@ export type PatrolResult = { orphaned_beads: string[]; }; +// -- Failure Reasons -- + +export type FailureReason = { + /** Machine-readable failure code */ + code: string; + /** Human-readable summary */ + message: string; + /** Optional detail: stack trace, error output, container logs */ + details?: string; + /** What triggered the failure: 'scheduler' | 'patrol' | 'refinery' | 'triage' | 'admin' | 'container' */ + source: string; +}; + // -- Merge Strategy -- export const MergeStrategy = z.enum(['direct', 'pr']); From 1df9a61143820268c40c8da991320c777b9a96b9 Mon Sep 17 00:00:00 2001 From: John Fawcett Date: Thu, 19 Mar 2026 04:07:20 +0000 Subject: [PATCH 2/4] feat(beads): add FailureReason type and extend updateBeadStatus() to persist failure reasons --- cloudflare-gastown/src/dos/town/beads.ts | 1 + cloudflare-gastown/src/dos/town/types.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 cloudflare-gastown/src/dos/town/types.ts diff --git a/cloudflare-gastown/src/dos/town/beads.ts b/cloudflare-gastown/src/dos/town/beads.ts index 648fc56d3e..f38aacb679 100644 --- a/cloudflare-gastown/src/dos/town/beads.ts +++ b/cloudflare-gastown/src/dos/town/beads.ts @@ -4,6 +4,7 @@ */ import { z } from 'zod'; +import type { FailureReason } from './types'; import { beads, BeadRecord, createTableBeads, getIndexesBeads } from '../../db/tables/beads.table'; import { bead_events, diff --git a/cloudflare-gastown/src/dos/town/types.ts b/cloudflare-gastown/src/dos/town/types.ts new file mode 100644 index 0000000000..f8fdde62fd --- /dev/null +++ b/cloudflare-gastown/src/dos/town/types.ts @@ -0,0 +1,10 @@ +/** + * Shared types for the Town DO. + */ + +export type FailureReason = { + code: string; // machine-readable: 'dispatch_exhausted', 'agent_crashed', 'timeout', 'orphaned_work', 'missing_rig_id', 'missing_rig_config', 'container_start_failed', 'admin_force_fail' + message: string; // human-readable summary + details?: string; // optional: stack trace, error output, container logs + source: string; // what triggered it: 'scheduler', 'patrol', 'refinery', 'triage', 'admin', 'container' +}; From 170640945a8f4029447c1318efd7cb7de8c2e364 Mon Sep 17 00:00:00 2001 From: John Fawcett Date: Thu, 19 Mar 2026 04:52:18 +0000 Subject: [PATCH 3/4] fix(beads): resolve unsafe-assignment lint errors in metadata parsing and deduplicate FailureReason import --- cloudflare-gastown/src/dos/town/beads.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/cloudflare-gastown/src/dos/town/beads.ts b/cloudflare-gastown/src/dos/town/beads.ts index f38aacb679..ec87d3d0b6 100644 --- a/cloudflare-gastown/src/dos/town/beads.ts +++ b/cloudflare-gastown/src/dos/town/beads.ts @@ -4,7 +4,6 @@ */ import { z } from 'zod'; -import type { FailureReason } from './types'; import { beads, BeadRecord, createTableBeads, getIndexesBeads } from '../../db/tables/beads.table'; import { bead_events, @@ -1005,8 +1004,10 @@ export function addBeadToConvoy(sql: SqlStorage, beadId: string, convoyId: strin const metadataPatch: Record = { convoy_id: convoyId }; if (featureBranch) metadataPatch.feature_branch = featureBranch; - const existingMetadata: Record = - typeof bead.metadata === 'string' ? JSON.parse(bead.metadata) : (bead.metadata ?? {}); + const existingMetadata = z + .record(z.string(), z.unknown()) + .catch({}) + .parse(typeof bead.metadata === 'string' ? JSON.parse(bead.metadata) : (bead.metadata ?? {})); const merged = { ...existingMetadata, ...metadataPatch }; query( @@ -1077,8 +1078,10 @@ export function removeBeadFromConvoy(sql: SqlStorage, beadId: string): string | // Strip convoy_id + feature_branch from metadata const bead = getBead(sql, beadId); if (bead) { - const existingMetadata: Record = - typeof bead.metadata === 'string' ? JSON.parse(bead.metadata) : (bead.metadata ?? {}); + const existingMetadata = z + .record(z.string(), z.unknown()) + .catch({}) + .parse(typeof bead.metadata === 'string' ? JSON.parse(bead.metadata) : (bead.metadata ?? {})); delete existingMetadata.convoy_id; delete existingMetadata.feature_branch; const timestamp = now(); From 8d8858214ebd2cf49ee66b7c45a53872e6aaf54b Mon Sep 17 00:00:00 2001 From: John Fawcett Date: Thu, 19 Mar 2026 14:06:33 +0000 Subject: [PATCH 4/4] chore(beads): remove unused town/types.ts (FailureReason is in src/types.ts) --- cloudflare-gastown/src/dos/town/types.ts | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 cloudflare-gastown/src/dos/town/types.ts diff --git a/cloudflare-gastown/src/dos/town/types.ts b/cloudflare-gastown/src/dos/town/types.ts deleted file mode 100644 index f8fdde62fd..0000000000 --- a/cloudflare-gastown/src/dos/town/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Shared types for the Town DO. - */ - -export type FailureReason = { - code: string; // machine-readable: 'dispatch_exhausted', 'agent_crashed', 'timeout', 'orphaned_work', 'missing_rig_id', 'missing_rig_config', 'container_start_failed', 'admin_force_fail' - message: string; // human-readable summary - details?: string; // optional: stack trace, error output, container logs - source: string; // what triggered it: 'scheduler', 'patrol', 'refinery', 'triage', 'admin', 'container' -};