@cat-factory/worker 0.171.2 → 0.173.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,38 +1,55 @@
1
1
  import { parseNotificationWebhookTypes, parsePlatformAlertEvents, parseRunLifecycleEvents, } from '@cat-factory/server';
2
2
  /**
3
- * A workspace's outbound webhook, one row per workspace (migration 0061; `run_events` added in
4
- * 0072, `alert_events` in 0080). All three JSON filter columns are decoded through the SHARED
5
- * parsers the Drizzle repo uses, so the columns can't drift between runtimes.
3
+ * A workspace's outbound webhooks, keyed by (workspace, endpoint id) (migration 0061; `run_events`
4
+ * added in 0072, `alert_events` in 0080, the named-collection key in 0085). All three JSON filter
5
+ * columns are decoded through the SHARED parsers the Drizzle repo uses, so the columns can't drift
6
+ * between runtimes.
6
7
  */
7
8
  export class D1NotificationWebhookRepository {
8
9
  db;
9
10
  constructor({ db }) {
10
11
  this.db = db;
11
12
  }
12
- async get(workspaceId) {
13
+ async get(workspaceId, id) {
13
14
  const row = await this.db
14
- .prepare(`SELECT * FROM notification_webhooks WHERE workspace_id = ?`)
15
- .bind(workspaceId)
15
+ .prepare(`SELECT * FROM notification_webhooks WHERE workspace_id = ? AND id = ?`)
16
+ .bind(workspaceId, id)
16
17
  .first();
17
- if (!row)
18
- return null;
19
- return {
20
- workspaceId: row.workspace_id,
21
- url: row.url,
22
- types: parseNotificationWebhookTypes(row.types),
23
- runEvents: parseRunLifecycleEvents(row.run_events),
24
- alertEvents: parsePlatformAlertEvents(row.alert_events),
25
- enabled: row.enabled === 1,
26
- secretSealed: row.secret_sealed,
27
- updatedAt: row.updated_at,
28
- };
18
+ return row ? toRecord(row) : null;
29
19
  }
30
- async put(record) {
31
- await this.db
20
+ async list(workspaceId) {
21
+ // `COLLATE BINARY` is SQLite's default for a text column and is spelled out only to name the
22
+ // order this port promises: the Drizzle mirror has to ask Postgres for `COLLATE "C"` to match,
23
+ // because its database-locale default would sort `web-hook` after `webhook` by ignoring the
24
+ // punctuation. Byte order on both, so the two runtimes return one sequence.
25
+ const { results } = await this.db
26
+ .prepare(`SELECT * FROM notification_webhooks WHERE workspace_id = ? ORDER BY id COLLATE BINARY`)
27
+ .bind(workspaceId)
28
+ .all();
29
+ return (results ?? []).map(toRecord);
30
+ }
31
+ async put(record, limit) {
32
+ // ONE statement, so the cap and the write cannot be separated by another writer. The row
33
+ // source is a `SELECT ... WHERE` rather than `VALUES` precisely so the admission test rides
34
+ // inside the insert: SQLite serializes statements, so a concurrent enrolment either sees this
35
+ // row or is seen by it, and can never read the same free slot twice.
36
+ //
37
+ // The predicate admits an existing endpoint unconditionally (`EXISTS`) before it consults the
38
+ // count, because the count includes that row: without the first half, a workspace sitting
39
+ // exactly at the limit could no longer edit, disable or re-point what it had already
40
+ // registered, which are the only ways back under it.
41
+ //
42
+ // The `WHERE` also disambiguates the parse. SQLite cannot otherwise tell an upsert's `ON
43
+ // CONFLICT` from a join's `ON` in an `INSERT ... SELECT`, and its documented fix is exactly a
44
+ // `WHERE` clause on the SELECT, which this needs anyway.
45
+ const result = await this.db
32
46
  .prepare(`INSERT INTO notification_webhooks
33
- (workspace_id, url, types, run_events, alert_events, enabled, secret_sealed, updated_at)
34
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
35
- ON CONFLICT (workspace_id) DO UPDATE SET
47
+ (workspace_id, id, name, url, types, run_events, alert_events, enabled, secret_sealed, updated_at)
48
+ SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
49
+ WHERE EXISTS (SELECT 1 FROM notification_webhooks WHERE workspace_id = ? AND id = ?)
50
+ OR (SELECT COUNT(*) FROM notification_webhooks WHERE workspace_id = ?) < ?
51
+ ON CONFLICT (workspace_id, id) DO UPDATE SET
52
+ name = excluded.name,
36
53
  url = excluded.url,
37
54
  types = excluded.types,
38
55
  run_events = excluded.run_events,
@@ -40,14 +57,31 @@ export class D1NotificationWebhookRepository {
40
57
  enabled = excluded.enabled,
41
58
  secret_sealed = excluded.secret_sealed,
42
59
  updated_at = excluded.updated_at`)
43
- .bind(record.workspaceId, record.url, JSON.stringify(record.types), JSON.stringify(record.runEvents), JSON.stringify(record.alertEvents), record.enabled ? 1 : 0, record.secretSealed, record.updatedAt)
60
+ .bind(record.workspaceId, record.id, record.name, record.url, JSON.stringify(record.types), JSON.stringify(record.runEvents), JSON.stringify(record.alertEvents), record.enabled ? 1 : 0, record.secretSealed, record.updatedAt, record.workspaceId, record.id, record.workspaceId, limit)
44
61
  .run();
62
+ // A filtered-out row writes nothing, which is the refusal. An admitted one always reports a
63
+ // change, whether it inserted or took the conflict branch.
64
+ return (result.meta?.changes ?? 0) > 0 ? 'stored' : 'limit_reached';
45
65
  }
46
- async delete(workspaceId) {
66
+ async delete(workspaceId, id) {
47
67
  await this.db
48
- .prepare(`DELETE FROM notification_webhooks WHERE workspace_id = ?`)
49
- .bind(workspaceId)
68
+ .prepare(`DELETE FROM notification_webhooks WHERE workspace_id = ? AND id = ?`)
69
+ .bind(workspaceId, id)
50
70
  .run();
51
71
  }
52
72
  }
73
+ function toRecord(row) {
74
+ return {
75
+ workspaceId: row.workspace_id,
76
+ id: row.id,
77
+ name: row.name,
78
+ url: row.url,
79
+ types: parseNotificationWebhookTypes(row.types),
80
+ runEvents: parseRunLifecycleEvents(row.run_events),
81
+ alertEvents: parsePlatformAlertEvents(row.alert_events),
82
+ enabled: row.enabled === 1,
83
+ secretSealed: row.secret_sealed,
84
+ updatedAt: row.updated_at,
85
+ };
86
+ }
53
87
  //# sourceMappingURL=D1NotificationWebhookRepository.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"D1NotificationWebhookRepository.js","sourceRoot":"","sources":["../../../src/infrastructure/repositories/D1NotificationWebhookRepository.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,6BAA6B,EAC7B,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,qBAAqB,CAAA;AAa5B;;;;GAIG;AACH,MAAM,OAAO,+BAA+B;IACzB,EAAE,CAAY;IAE/B,YAAY,EAAE,EAAE,EAAsB;QACpC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;IACd,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,WAAmB;QAC3B,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,EAAE;aACtB,OAAO,CAAC,4DAA4D,CAAC;aACrE,IAAI,CAAC,WAAW,CAAC;aACjB,KAAK,EAA0B,CAAA;QAClC,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAA;QACrB,OAAO;YACL,WAAW,EAAE,GAAG,CAAC,YAAY;YAC7B,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,KAAK,EAAE,6BAA6B,CAAC,GAAG,CAAC,KAAK,CAAC;YAC/C,SAAS,EAAE,uBAAuB,CAAC,GAAG,CAAC,UAAU,CAAC;YAClD,WAAW,EAAE,wBAAwB,CAAC,GAAG,CAAC,YAAY,CAAC;YACvD,OAAO,EAAE,GAAG,CAAC,OAAO,KAAK,CAAC;YAC1B,YAAY,EAAE,GAAG,CAAC,aAAa;YAC/B,SAAS,EAAE,GAAG,CAAC,UAAU;SAC1B,CAAA;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,MAAiC;QACzC,MAAM,IAAI,CAAC,EAAE;aACV,OAAO,CACN;;;;;;;;;;4CAUoC,CACrC;aACA,IAAI,CACH,MAAM,CAAC,WAAW,EAClB,MAAM,CAAC,GAAG,EACV,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,EAC5B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,EAChC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,EAClC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EACtB,MAAM,CAAC,YAAY,EACnB,MAAM,CAAC,SAAS,CACjB;aACA,GAAG,EAAE,CAAA;IACV,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,WAAmB;QAC9B,MAAM,IAAI,CAAC,EAAE;aACV,OAAO,CAAC,0DAA0D,CAAC;aACnE,IAAI,CAAC,WAAW,CAAC;aACjB,GAAG,EAAE,CAAA;IACV,CAAC;CACF"}
1
+ {"version":3,"file":"D1NotificationWebhookRepository.js","sourceRoot":"","sources":["../../../src/infrastructure/repositories/D1NotificationWebhookRepository.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,6BAA6B,EAC7B,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,qBAAqB,CAAA;AAe5B;;;;;GAKG;AACH,MAAM,OAAO,+BAA+B;IACzB,EAAE,CAAY;IAE/B,YAAY,EAAE,EAAE,EAAsB;QACpC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;IACd,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,WAAmB,EAAE,EAAU;QACvC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,EAAE;aACtB,OAAO,CAAC,uEAAuE,CAAC;aAChF,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;aACrB,KAAK,EAA0B,CAAA;QAClC,OAAO,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IACnC,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,WAAmB;QAC5B,6FAA6F;QAC7F,+FAA+F;QAC/F,4FAA4F;QAC5F,4EAA4E;QAC5E,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,EAAE;aAC9B,OAAO,CACN,uFAAuF,CACxF;aACA,IAAI,CAAC,WAAW,CAAC;aACjB,GAAG,EAA0B,CAAA;QAChC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACtC,CAAC;IAED,KAAK,CAAC,GAAG,CACP,MAAiC,EACjC,KAAa;QAEb,yFAAyF;QACzF,4FAA4F;QAC5F,8FAA8F;QAC9F,qEAAqE;QACrE,EAAE;QACF,8FAA8F;QAC9F,0FAA0F;QAC1F,qFAAqF;QACrF,qDAAqD;QACrD,EAAE;QACF,yFAAyF;QACzF,8FAA8F;QAC9F,yDAAyD;QACzD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,EAAE;aACzB,OAAO,CACN;;;;;;;;;;;;;4CAaoC,CACrC;aACA,IAAI,CACH,MAAM,CAAC,WAAW,EAClB,MAAM,CAAC,EAAE,EACT,MAAM,CAAC,IAAI,EACX,MAAM,CAAC,GAAG,EACV,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,EAC5B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,EAChC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,EAClC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EACtB,MAAM,CAAC,YAAY,EACnB,MAAM,CAAC,SAAS,EAChB,MAAM,CAAC,WAAW,EAClB,MAAM,CAAC,EAAE,EACT,MAAM,CAAC,WAAW,EAClB,KAAK,CACN;aACA,GAAG,EAAE,CAAA;QACR,4FAA4F;QAC5F,2DAA2D;QAC3D,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAA;IACrE,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,WAAmB,EAAE,EAAU;QAC1C,MAAM,IAAI,CAAC,EAAE;aACV,OAAO,CAAC,qEAAqE,CAAC;aAC9E,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;aACrB,GAAG,EAAE,CAAA;IACV,CAAC;CACF;AAED,SAAS,QAAQ,CAAC,GAA2B;IAC3C,OAAO;QACL,WAAW,EAAE,GAAG,CAAC,YAAY;QAC7B,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,GAAG,EAAE,GAAG,CAAC,GAAG;QACZ,KAAK,EAAE,6BAA6B,CAAC,GAAG,CAAC,KAAK,CAAC;QAC/C,SAAS,EAAE,uBAAuB,CAAC,GAAG,CAAC,UAAU,CAAC;QAClD,WAAW,EAAE,wBAAwB,CAAC,GAAG,CAAC,YAAY,CAAC;QACvD,OAAO,EAAE,GAAG,CAAC,OAAO,KAAK,CAAC;QAC1B,YAAY,EAAE,GAAG,CAAC,aAAa;QAC/B,SAAS,EAAE,GAAG,CAAC,UAAU;KAC1B,CAAA;AACH,CAAC"}
@@ -2,35 +2,66 @@ import type { AgentRunRef, AgentRunRepository, Clock, EnvironmentTestRunRecord,
2
2
  import type { Workflow } from '@cloudflare/workers-types';
3
3
  /**
4
4
  * A run's durable instance, as the sweeper needs to classify it:
5
- * - `alive` running/queued/waiting/paused; leave it.
6
- * - `terminal` the instance exists but completed/errored/terminated. It can NOT
5
+ * - `alive` : running/queued/waiting/paused/waitingForPause; leave it.
6
+ * - `terminal` : the instance exists but completed/errored/terminated. It can NOT
7
7
  * be recreated (instance ids are unique), so a re-drive via
8
- * `create` is a silent no-op the run must be FINALIZED instead.
9
- * - `missing` no instance for this id; safe to (re-)create via `redrive`.
8
+ * `create` is a silent no-op and the run must be FINALIZED instead.
9
+ * - `missing` : no instance for this id; safe to (re-)create via `redrive`.
10
+ * - `unknown` : the probe could not classify the instance. The Workflows API refused the
11
+ * lookup, the binding for this run's kind is unconfigured, or Workflows
12
+ * itself answered `unknown`. Every disposition the sweeper has is DESTRUCTIVE
13
+ * against a live run (a re-drive of a run that is fine is at best wasted, a
14
+ * finalize kills it outright), so this state buys the run one more tick
15
+ * instead of guessing. It exists because the two swallows this replaced both
16
+ * answered `missing`, which made a Workflows outage read as every stale run
17
+ * having lost its instance at once, and re-drove the whole fleet with no log
18
+ * line to say why.
10
19
  */
11
- export type InstanceState = 'alive' | 'terminal' | 'missing';
20
+ export type InstanceState = 'alive' | 'terminal' | 'missing' | 'unknown';
21
+ /** What the probe learned about a run's durable instance. */
22
+ export interface InstanceProbe {
23
+ state: InstanceState;
24
+ /**
25
+ * What the probe learned beyond the state, already scrubbed and capped for a surface a
26
+ * person reads:
27
+ * - on `terminal`, the instance's OWN error (`InstanceStatus.error`) when Workflows
28
+ * reported one. It is the only account of why the driver died that survives the
29
+ * instance, and the run is about to be stopped with a fixed sentence that says nothing.
30
+ * - on `unknown`, why the lookup could not classify.
31
+ *
32
+ * Absent when there is nothing to say, which is not the same as an empty string: a terminal
33
+ * instance that Workflows reports no error for ended on purpose.
34
+ */
35
+ detail?: string;
36
+ }
12
37
  /** Tells the sweeper the state of a run's durable instance. */
13
38
  interface WorkflowLookup {
14
- instanceState(runId: string): Promise<InstanceState>;
39
+ instanceState(runId: string): Promise<InstanceProbe>;
15
40
  }
16
41
  /** WorkflowLookup over a Cloudflare Workflows binding. */
17
42
  export declare class WorkflowsLookup implements WorkflowLookup {
18
43
  private readonly workflow;
19
- constructor(workflow: Workflow);
20
- instanceState(runId: string): Promise<InstanceState>;
44
+ private readonly log;
45
+ constructor(workflow: Workflow, log?: Logger);
46
+ instanceState(runId: string): Promise<InstanceProbe>;
21
47
  }
22
48
  export interface SweepDeps {
23
49
  agentRunRepository: AgentRunRepository;
24
50
  /** State of the durable instance backing this run (by kind). */
25
- instanceState(ref: AgentRunRef): Promise<InstanceState>;
51
+ instanceState(ref: AgentRunRef): Promise<InstanceProbe>;
26
52
  /** (Re-)create the durable driver for a run whose instance is `missing`. */
27
53
  redrive(ref: AgentRunRef): Promise<void>;
28
54
  /**
29
55
  * Finalize a run whose instance is `terminal` (so it can't be re-created): mark it
30
56
  * stopped/failed and reclaim any leftover container, routed by kind. Without this,
31
57
  * such a run would show as `running` forever (the re-drive is a silent no-op).
58
+ *
59
+ * `cause` is the dead instance's own account of itself ({@link InstanceProbe.detail}), when
60
+ * Workflows kept one. It is the only thing that distinguishes the runs this branch settles,
61
+ * all of which otherwise carry the same fixed sentence, so it belongs in the stop reason
62
+ * rather than only in a log line the operator has to go and find.
32
63
  */
33
- finalizeOrphan(ref: AgentRunRef): Promise<void>;
64
+ finalizeOrphan(ref: AgentRunRef, cause?: string): Promise<void>;
34
65
  /**
35
66
  * Fail an execution run whose instance stayed `missing` past the hard-stall deadline —
36
67
  * re-driving isn't resurrecting it, so flag it `stalled` (loud banner + retry) instead of
@@ -72,6 +103,12 @@ export interface SweepResult {
72
103
  finalized: number;
73
104
  /** Runs failed `stalled` (instance missing past the hard-stall deadline). */
74
105
  stalled: number;
106
+ /**
107
+ * Runs the probe could not classify, so the sweep left them alone. Reported apart from the
108
+ * three dispositions because it is not one: a rising number here means the sweeper is
109
+ * BLIND, which reads in every other number as a fleet that suddenly went quiet.
110
+ */
111
+ unknown: number;
75
112
  }
76
113
  /**
77
114
  * Backstop for runs that are still `running` in storage but whose Workflows
@@ -82,6 +119,10 @@ export interface SweepResult {
82
119
  * recreated under the same id, so re-driving is a no-op — without
83
120
  * this the run would be stuck `running` forever).
84
121
  * - `alive` → leave it.
122
+ * - `unknown` → leave it, count it, and forget its orphan clock. The sweep acts on what it
123
+ * KNOWS; an unclassifiable instance costs the run one tick of recovery
124
+ * latency, where guessing costs a live run its container or an orphan its
125
+ * place in the queue for as long as the outage lasts.
85
126
  * Pure orchestration over its ports so it is unit-testable with fakes.
86
127
  */
87
128
  export declare function sweepStuckRuns({ agentRunRepository, instanceState, redrive, finalizeOrphan, failStalled, clock, leaseMs, hardStallMs, orphanedSince, metrics, logger, }: SweepDeps): Promise<SweepResult>;
@@ -90,14 +131,15 @@ export interface EnvTestSweepDeps {
90
131
  listStale(cutoffMs: number): Promise<EnvironmentTestRunRecord[]>;
91
132
  };
92
133
  /** State of the run's EnvironmentTestWorkflow instance. */
93
- instanceState(runId: string): Promise<InstanceState>;
134
+ instanceState(runId: string): Promise<InstanceProbe>;
94
135
  /** (Re-)create the durable driver for a run whose instance is `missing`. */
95
136
  redrive(workspaceId: string, runId: string): Promise<void>;
96
137
  /**
97
138
  * Finalize a run whose instance is `terminal` (it can't be re-created under the same
98
139
  * id): best-effort cleanup + mark it failed, via `EnvironmentTestService.expire`.
140
+ * `cause` is the dead instance's own error when Workflows kept one (see {@link SweepDeps}).
99
141
  */
100
- finalizeOrphan(workspaceId: string, runId: string): Promise<void>;
142
+ finalizeOrphan(workspaceId: string, runId: string, cause?: string): Promise<void>;
101
143
  clock: Clock;
102
144
  /** A run is considered stuck if its record hasn't been touched in this many ms. */
103
145
  leaseMs: number;
@@ -115,6 +157,7 @@ export interface EnvTestSweepDeps {
115
157
  export declare function sweepStuckEnvTests({ repository, instanceState, redrive, finalizeOrphan, clock, leaseMs, }: EnvTestSweepDeps): Promise<{
116
158
  redriven: number;
117
159
  finalized: number;
160
+ unknown: number;
118
161
  }>;
119
162
  export {};
120
163
  //# sourceMappingURL=sweeper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sweeper.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/workflows/sweeper.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sweeper.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/workflows/sweeper.ts"],"names":[],"mappings":"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"}
@@ -1,32 +1,156 @@
1
- import { noopLogger, noopOperationalMetrics, runBestEffort } from '@cat-factory/kernel';
1
+ import { describeError, noopLogger, noopOperationalMetrics, redactSecrets, runBestEffort, } from '@cat-factory/kernel';
2
+ /**
3
+ * How much of an instance's own error text is carried onto the run's stop reason. It is
4
+ * rendered in the run's details, so it is a sentence or two of context, not a transcript.
5
+ */
6
+ const INSTANCE_DETAIL_CAP = 400;
7
+ /**
8
+ * Workflows' code for "no instance with this id". It rides the error MESSAGE
9
+ * (`(instance.not_found) Instance does not exist` from the API, a bare `instance.not_found`
10
+ * from the local binding); the binding's own `WorkflowError.code` is a NUMBER, so this string
11
+ * is the only spelling stable across both.
12
+ */
13
+ const INSTANCE_NOT_FOUND_CODE = 'instance.not_found';
14
+ /** How far down a `cause` chain the classification below looks before giving up. */
15
+ const CAUSE_DEPTH = 4;
16
+ /**
17
+ * The message a thrown value carries, whatever shape it arrived in.
18
+ *
19
+ * Reading `.message` only off an `Error` is the trap: `Workflow.get` is declared to reject with
20
+ * a plain `WorkflowError` (`{ code?: number; message: string }`), which is not an `Error`, so an
21
+ * `instanceof` test falls through to `String(error)` and renders it `[object Object]`. That
22
+ * silently makes `missing` unreachable, and with it the ENTIRE stale-run backstop: every probe
23
+ * answers `unknown`, which by design takes no action at all. Empty when the value carries no
24
+ * message, so a caller can tell "nothing said" from "said nothing useful".
25
+ */
26
+ function messageOf(error) {
27
+ if (typeof error === 'string')
28
+ return error;
29
+ if (error instanceof Error)
30
+ return error.message;
31
+ if (typeof error === 'object' && error !== null) {
32
+ const { message } = error;
33
+ if (typeof message === 'string')
34
+ return message;
35
+ }
36
+ return '';
37
+ }
38
+ /**
39
+ * Whether a lookup throw means "no instance with this id" (the state the sweeper acts on)
40
+ * rather than "the lookup failed" (which says nothing about the run).
41
+ *
42
+ * Matching the CODE rather than the prose keeps it stable across Workflows' two spellings, and
43
+ * walking the `cause` chain keeps it stable across a binding shim that re-throws wrapping the
44
+ * original. Anything else (a quota rejection, an unbound namespace, an API outage) is a lookup
45
+ * failure and must NOT be reported as a lost instance.
46
+ *
47
+ * A misclassification here is invisible in the sweep's dispositions, which is what
48
+ * `sweep.run_state_unknown` is counted for: a sweeper that can no longer recognise the code
49
+ * reports every stale run as unclassifiable rather than re-driving none of them quietly.
50
+ */
51
+ function isInstanceNotFound(error) {
52
+ let current = error;
53
+ for (let depth = 0; depth < CAUSE_DEPTH && current != null; depth++) {
54
+ if (messageOf(current).includes(INSTANCE_NOT_FOUND_CODE))
55
+ return true;
56
+ current = current.cause;
57
+ }
58
+ return false;
59
+ }
60
+ /**
61
+ * Prepare free text for {@link InstanceProbe.detail}: scrub, trim, cap. Workflows echoes the
62
+ * step's own throw, which routinely carries a URL or a provider response, and this lands on a
63
+ * run surface a person reads. Empty in ⇒ empty out, so the caller can tell "nothing to say"
64
+ * from "said nothing useful".
65
+ */
66
+ function probeDetail(text) {
67
+ const scrubbed = (redactSecrets(text.trim()) ?? '').trim();
68
+ return scrubbed.length > INSTANCE_DETAIL_CAP
69
+ ? `${scrubbed.slice(0, INSTANCE_DETAIL_CAP)}…`
70
+ : scrubbed;
71
+ }
72
+ /** The instance's own error, scrubbed and capped; empty when Workflows reported none. */
73
+ function describeInstanceError(error) {
74
+ if (!error)
75
+ return '';
76
+ return probeDetail([error.name, error.message].filter(Boolean).join(': '));
77
+ }
78
+ /**
79
+ * Why a lookup could not answer, scrubbed and capped. Reads the message off whatever shape the
80
+ * throw arrived in (see {@link messageOf}) rather than stringifying it, since the plain
81
+ * `WorkflowError` the binding is declared to reject with renders as `[object Object]` and would
82
+ * put a run's only account of an outage as literally nothing.
83
+ */
84
+ function describeLookupFailure(error) {
85
+ const message = messageOf(error);
86
+ if (!message)
87
+ return probeDetail(safeStringify(error));
88
+ const name = error instanceof Error ? error.name : '';
89
+ return probeDetail(name ? `${name}: ${message}` : message);
90
+ }
91
+ /** A last-resort rendering of a throw that carries no message. Never throws itself. */
92
+ function safeStringify(error) {
93
+ try {
94
+ return typeof error === 'object' && error !== null ? JSON.stringify(error) : String(error);
95
+ }
96
+ catch {
97
+ // silent-catch-ok: the value is unserialisable (a cycle, a throwing getter); its type is
98
+ // the only honest thing left to say about it, and this is already the fallback path.
99
+ return Object.prototype.toString.call(error);
100
+ }
101
+ }
2
102
  /** WorkflowLookup over a Cloudflare Workflows binding. */
3
103
  export class WorkflowsLookup {
4
104
  workflow;
5
- constructor(workflow) {
105
+ log;
106
+ constructor(workflow, log = noopLogger) {
6
107
  this.workflow = workflow;
108
+ this.log = log;
7
109
  }
8
110
  async instanceState(runId) {
9
111
  let instance;
10
112
  try {
11
113
  instance = await this.workflow.get(runId);
12
114
  }
13
- catch {
14
- // No instance with this id was ever created → safe to create one.
15
- return 'missing';
115
+ catch (error) {
116
+ if (isInstanceNotFound(error))
117
+ return { state: 'missing' };
118
+ // The lookup itself failed. Reporting `missing` here is how one Workflows outage became
119
+ // a fleet-wide re-drive; report that we do not know instead, and name the cause.
120
+ this.log.warn('workflow instance lookup failed', { runId, ...describeError(error) });
121
+ return { state: 'unknown', detail: describeLookupFailure(error) };
16
122
  }
17
123
  try {
18
- const { status } = await instance.status();
19
- return status === 'running' ||
124
+ const { status, error } = await instance.status();
125
+ if (status === 'running' ||
20
126
  status === 'queued' ||
21
127
  status === 'waiting' ||
22
- status === 'paused'
23
- ? 'alive'
24
- : 'terminal';
128
+ status === 'paused' ||
129
+ // Finishing the current work before parking: still driving the run, so leaving it
130
+ // alone is right. Absent from the alive set it fell through to `terminal`, and the
131
+ // sweeper force-stopped a run that was pausing exactly as it was asked to.
132
+ status === 'waitingForPause') {
133
+ return { state: 'alive' };
134
+ }
135
+ // Workflows' OWN "I cannot tell you" answer. It shares the fall-through with the genuine
136
+ // terminal states, so it used to finalize the run, the most destructive disposition
137
+ // available, taken on the one answer that carries no information.
138
+ if (status === 'unknown') {
139
+ this.log.warn('workflow instance status is unknown', { runId });
140
+ return { state: 'unknown', detail: 'Workflows reported the instance status as unknown.' };
141
+ }
142
+ const detail = describeInstanceError(error);
143
+ return { state: 'terminal', ...(detail ? { detail } : {}) };
25
144
  }
26
- catch {
27
- // The instance handle resolved but status is unreadable treat as missing so
28
- // the sweeper tries to (re-)create rather than wrongly finalizing a live run.
29
- return 'missing';
145
+ catch (error) {
146
+ // The handle `get` hands back is LAZY: it resolves nothing, so "no instance with this id"
147
+ // routinely surfaces here rather than from `get`. It is the same fact and takes the same
148
+ // classification, or a genuinely lost instance reads as an unreadable one and is never
149
+ // re-created.
150
+ if (isInstanceNotFound(error))
151
+ return { state: 'missing' };
152
+ this.log.warn('workflow instance status unreadable', { runId, ...describeError(error) });
153
+ return { state: 'unknown', detail: describeLookupFailure(error) };
30
154
  }
31
155
  }
32
156
  }
@@ -39,6 +163,10 @@ export class WorkflowsLookup {
39
163
  * recreated under the same id, so re-driving is a no-op — without
40
164
  * this the run would be stuck `running` forever).
41
165
  * - `alive` → leave it.
166
+ * - `unknown` → leave it, count it, and forget its orphan clock. The sweep acts on what it
167
+ * KNOWS; an unclassifiable instance costs the run one tick of recovery
168
+ * latency, where guessing costs a live run its container or an orphan its
169
+ * place in the queue for as long as the outage lasts.
42
170
  * Pure orchestration over its ports so it is unit-testable with fakes.
43
171
  */
44
172
  export async function sweepStuckRuns({ agentRunRepository, instanceState, redrive, finalizeOrphan, failStalled, clock, leaseMs, hardStallMs, orphanedSince = new Map(), metrics = noopOperationalMetrics, logger = noopLogger, }) {
@@ -47,19 +175,31 @@ export async function sweepStuckRuns({ agentRunRepository, instanceState, redriv
47
175
  let redriven = 0;
48
176
  let finalized = 0;
49
177
  let stalled = 0;
178
+ let unknown = 0;
50
179
  // Which runs were observed still-orphaned (`missing`) THIS tick — used to prune the
51
180
  // per-process clock of any run that recovered or went terminal so its deadline restarts
52
181
  // if it ever stalls again.
53
182
  const stillOrphaned = new Set();
54
183
  for (const ref of stale) {
55
- const state = await instanceState(ref);
184
+ const { state, detail } = await instanceState(ref);
56
185
  if (state === 'alive') {
57
186
  orphanedSince.delete(ref.id);
58
187
  continue;
59
188
  }
189
+ if (state === 'unknown') {
190
+ // Forget the orphan clock rather than carrying it: the run was NOT observed orphaned
191
+ // this tick, and letting an outage age a deadline nobody could measure is how a
192
+ // Workflows incident would come out the far side as a batch of `stalled` runs. The
193
+ // deadline restarts from the next observation that actually saw something, which also
194
+ // guarantees the run is re-driven at least once before it can be given up on.
195
+ orphanedSince.delete(ref.id);
196
+ unknown++;
197
+ metrics.increment('sweep.run_state_unknown', { kind: ref.kind });
198
+ continue;
199
+ }
60
200
  if (state === 'terminal') {
61
201
  orphanedSince.delete(ref.id);
62
- await finalizeOrphan(ref);
202
+ await finalizeOrphan(ref, detail);
63
203
  finalized++;
64
204
  // The run KIND is a bounded enum and the split that matters: bootstrap runs and
65
205
  // execution runs are lost for different reasons and fixed in different places.
@@ -99,7 +239,7 @@ export async function sweepStuckRuns({ agentRunRepository, instanceState, redriv
99
239
  if (!stillOrphaned.has(id))
100
240
  orphanedSince.delete(id);
101
241
  }
102
- return { redriven, finalized, stalled };
242
+ return { redriven, finalized, stalled, unknown };
103
243
  }
104
244
  /**
105
245
  * The env-test sibling of {@link sweepStuckRuns}: self-test runs live in their own
@@ -115,18 +255,26 @@ export async function sweepStuckEnvTests({ repository, instanceState, redrive, f
115
255
  const stale = await repository.listStale(clock.now() - leaseMs);
116
256
  let redriven = 0;
117
257
  let finalized = 0;
258
+ let unknown = 0;
118
259
  for (const run of stale) {
119
- const state = await instanceState(run.id);
260
+ const { state, detail } = await instanceState(run.id);
120
261
  if (state === 'alive')
121
262
  continue;
263
+ // Unclassifiable: leave it for the next tick, exactly like the run sweep. Re-driving a
264
+ // test whose instance is alive would be harmless, but expiring one that is provisioning
265
+ // real infrastructure is not, and neither disposition is safe to take blind.
266
+ if (state === 'unknown') {
267
+ unknown++;
268
+ continue;
269
+ }
122
270
  if (state === 'terminal') {
123
- await finalizeOrphan(run.workspaceId, run.id);
271
+ await finalizeOrphan(run.workspaceId, run.id, detail);
124
272
  finalized++;
125
273
  continue;
126
274
  }
127
275
  await redrive(run.workspaceId, run.id);
128
276
  redriven++;
129
277
  }
130
- return { redriven, finalized };
278
+ return { redriven, finalized, unknown };
131
279
  }
132
280
  //# sourceMappingURL=sweeper.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sweeper.js","sourceRoot":"","sources":["../../../src/infrastructure/workflows/sweeper.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sweeper.js","sourceRoot":"","sources":["../../../src/infrastructure/workflows/sweeper.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ -- Several NAMED outbound webhooks per workspace, replacing the one-row-per-workspace shape from
2
+ -- migration 0061. One endpoint per workspace made a second integration's enrolment a hostile act:
3
+ -- registering it overwrote whatever was already there, and the only symptom was that the previous
4
+ -- receiver went quiet. Each row now carries a caller-chosen `id` (the key, alongside the workspace)
5
+ -- and an operator-facing `name`.
6
+ --
7
+ -- SQLite cannot re-key a table in place, so this is the standard rebuild. Existing rows migrate to
8
+ -- the id `default`, which is exactly what the singular `/api/v1/notification-webhook` routes now
9
+ -- address: an already-registered endpoint keeps delivering, and the caller that registered it keeps
10
+ -- addressing it through the same route.
11
+
12
+ CREATE TABLE notification_webhooks_new (
13
+ workspace_id TEXT NOT NULL,
14
+ id TEXT NOT NULL,
15
+ name TEXT NOT NULL,
16
+ url TEXT NOT NULL,
17
+ types TEXT NOT NULL DEFAULT '[]',
18
+ run_events TEXT NOT NULL DEFAULT '[]',
19
+ alert_events TEXT NOT NULL DEFAULT '[]',
20
+ enabled INTEGER NOT NULL DEFAULT 1,
21
+ secret_sealed TEXT,
22
+ updated_at INTEGER NOT NULL,
23
+ PRIMARY KEY (workspace_id, id)
24
+ );
25
+
26
+ INSERT INTO notification_webhooks_new
27
+ (workspace_id, id, name, url, types, run_events, alert_events, enabled, secret_sealed, updated_at)
28
+ SELECT
29
+ workspace_id, 'default', 'Default', url, types, run_events, alert_events, enabled, secret_sealed, updated_at
30
+ FROM notification_webhooks;
31
+
32
+ DROP TABLE notification_webhooks;
33
+
34
+ ALTER TABLE notification_webhooks_new RENAME TO notification_webhooks;
35
+
36
+ -- No extra index on `workspace_id`: it is the leading column of the composite primary key, so the
37
+ -- per-workspace list every delivery reads is already served by that index.