@coreplane/switchboard 1.260.4 → 1.260.6

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.
Files changed (51) hide show
  1. package/dist/assets/deploy/cloudflare/package.json +2 -2
  2. package/dist/assets/deploy/cloudflare-docs/package.json +1 -1
  3. package/dist/assets/deploy/cloudflare-memory/package.json +2 -2
  4. package/dist/assets/deploy/cloudflare-memory/worker.ts +222 -11
  5. package/dist/assets/deploy/cloudflare-resident/package.json +2 -2
  6. package/dist/assets/deploy/cloudflare-resident/worker.ts +124 -40
  7. package/dist/assets/deploy/cloudflare-sandbox/package.json +2 -2
  8. package/dist/assets/package-lock.json +712 -97
  9. package/dist/assets/package.json +4 -4
  10. package/dist/assets/source.json +3 -3
  11. package/dist/assets/src/core/budgets.ts +10 -0
  12. package/dist/assets/src/core/coordinator/contract.ts +5 -0
  13. package/dist/assets/src/core/coordinator/driver.ts +155 -29
  14. package/dist/assets/src/core/pipelineStanding.ts +2 -0
  15. package/dist/assets/src/core/plane/decide.ts +19 -2
  16. package/dist/assets/src/core/provider.ts +380 -0
  17. package/dist/assets/src/core/refusal.ts +2 -1
  18. package/dist/assets/src/core/runEvents.ts +8 -0
  19. package/dist/assets/src/core/runLedger/decisions.ts +10 -1
  20. package/dist/assets/src/core/runLedger/types.ts +13 -1
  21. package/dist/assets/src/core/runRecord.ts +31 -0
  22. package/dist/assets/src/core/ship/contract.ts +2 -0
  23. package/dist/assets/src/core/ship/coordinator.ts +515 -30
  24. package/dist/assets/src/core/types.ts +6 -0
  25. package/dist/assets/src/execution/residentRebind.ts +15 -0
  26. package/dist/assets/src/execution/residentReuse.ts +23 -2
  27. package/dist/assets/web/dist/.vite/manifest.json +67 -67
  28. package/dist/assets/web/dist/assets/{DeliveryPage-BaemWKW8.js → DeliveryPage-BUJckSuW.js} +1 -1
  29. package/dist/assets/web/dist/assets/{HomePage-fgKQHLaO.js → HomePage-CBQpcnaW.js} +1 -1
  30. package/dist/assets/web/dist/assets/{PendingTurnRow-vuWiDzvz.js → PendingTurnRow-DIC86uWA.js} +1 -1
  31. package/dist/assets/web/dist/assets/{PlanePage-DXMJf8FB.js → PlanePage-CfrbMh8t.js} +1 -1
  32. package/dist/assets/web/dist/assets/{ResidentDetailPage-DdC8yU_S.js → ResidentDetailPage-C-SgOUTl.js} +1 -1
  33. package/dist/assets/web/dist/assets/{ResidentsIndexPage-CSM-KODv.js → ResidentsIndexPage-CiDhvXok.js} +1 -1
  34. package/dist/assets/web/dist/assets/RunFoldRow-DZ8huwUU.js +1 -0
  35. package/dist/assets/web/dist/assets/{RunRoutePage-jo6NZxsP.js → RunRoutePage-Cp6wicYc.js} +3 -3
  36. package/dist/assets/web/dist/assets/{RunsIndexPage-Dx1Y4sSq.js → RunsIndexPage-BPXtfwwx.js} +1 -1
  37. package/dist/assets/web/dist/assets/{ScheduledPage-BuB_NZyR.js → ScheduledPage-DbvP48HI.js} +1 -1
  38. package/dist/assets/web/dist/assets/{SettingsPage-Cn96jB6Q.js → SettingsPage-B87lZaiN.js} +1 -1
  39. package/dist/assets/web/dist/assets/{SilentTurn-D2cJfpEq.js → SilentTurn-CPED_XCV.js} +1 -1
  40. package/dist/assets/web/dist/assets/{StatusDot-DDHUCPVJ.js → StatusDot-CFLn-DZ2.js} +1 -1
  41. package/dist/assets/web/dist/assets/{Tooltip-B4VIHkAI.js → Tooltip-Dx9YBVEx.js} +1 -1
  42. package/dist/assets/web/dist/assets/{UnitRoutePage-C-2DfjLI.js → UnitRoutePage-d4A2X_o7.js} +1 -1
  43. package/dist/assets/web/dist/assets/budgets-BQgcCyvb.js +1 -0
  44. package/dist/assets/web/dist/assets/{dist-Cf1CmCSK.js → dist-Cn51vWpw.js} +11 -11
  45. package/dist/assets/web/dist/assets/{indexRow-Dxg7C8s9.js → indexRow-BsWcHHVh.js} +1 -1
  46. package/dist/assets/web/dist/assets/{main-DxPqaFu9.js → main-DYwRHh0F.js} +2 -2
  47. package/dist/assets/web/dist/assets/{sseReplay-BzoeVxZB.js → sseReplay-DGRDRLua.js} +1 -1
  48. package/dist/cli.js +2550 -924
  49. package/package.json +4 -4
  50. package/dist/assets/web/dist/assets/RunFoldRow-Csik3wD6.js +0 -1
  51. package/dist/assets/web/dist/assets/budgets-KNNkT4PZ.js +0 -1
@@ -98,6 +98,386 @@ export interface Provider {
98
98
  complete(req: CompletionRequest): Promise<CompletionResult>;
99
99
  }
100
100
 
101
+ /** Every model-call failure crosses this one seam. The cause is deliberately
102
+ * small and wire-neutral: adapters classify status plus structured body once;
103
+ * callers decide from `cause` and render only `renderProviderFailure(cause)`.
104
+ * `operatorUrl` is retained for an admin surface and never appears in the
105
+ * error's message or the user-facing renderer. */
106
+ export const PROVIDER_FAILURE_CAUSES = [
107
+ "transient",
108
+ "rate-limited",
109
+ "credit-or-quota-exhausted",
110
+ "key-absent",
111
+ "key-invalid",
112
+ "model-unknown",
113
+ "request-rejected",
114
+ "permanent",
115
+ ] as const;
116
+ export type ProviderFailureCause = (typeof PROVIDER_FAILURE_CAUSES)[number];
117
+
118
+ export interface ProviderFailureAnswer {
119
+ status?: number;
120
+ body?: unknown;
121
+ error?: unknown;
122
+ provider?: string;
123
+ model?: string;
124
+ /** Only adapters that verified the model proxy's response-specific HMAC
125
+ * marker may set this. Provider response bodies are never trusted to name
126
+ * their own disposition. */
127
+ trustedEnvelope?: boolean;
128
+ }
129
+
130
+ export interface ProviderSchemaRejection {
131
+ tool: string;
132
+ keyword: string;
133
+ }
134
+
135
+ export interface ProviderFailureDetails extends Pick<ProviderFailureAnswer, "status" | "provider" | "model"> {
136
+ operatorUrl?: string;
137
+ /** The configured variable involved in a key failure. It is operator-safe
138
+ * diagnostic context, never part of the requester-facing renderer. */
139
+ keyVariable?: string;
140
+ /** Vouched evidence minted at the provider boundary when its response names
141
+ * one offered tool and one schema keyword. Callers may repair only on
142
+ * this pair, never on a generic 400 or provider-controlled prose. */
143
+ schemaRejection?: ProviderSchemaRejection;
144
+ }
145
+
146
+ function providerFailureDiagnostic(cause: ProviderFailureCause, details: ProviderFailureDetails): string {
147
+ if (details.provider !== undefined && details.keyVariable !== undefined) {
148
+ if (cause === "key-absent") return `Provider "${details.provider}": ${details.keyVariable} is not set`;
149
+ if (cause === "key-invalid") return `Provider "${details.provider}": ${details.keyVariable} was refused`;
150
+ }
151
+ return renderProviderFailure(cause);
152
+ }
153
+
154
+ export class ProviderFailure extends Error {
155
+ override readonly name = "ProviderFailure";
156
+ override readonly cause: ProviderFailureCause;
157
+ readonly status: number | undefined;
158
+ readonly provider: string | undefined;
159
+ readonly model: string | undefined;
160
+ readonly operatorUrl: string | undefined;
161
+ readonly keyVariable: string | undefined;
162
+ readonly schemaRejection: ProviderSchemaRejection | undefined;
163
+
164
+ constructor(cause: ProviderFailureCause, details: ProviderFailureDetails = {}) {
165
+ super(providerFailureDiagnostic(cause, details));
166
+ this.cause = cause;
167
+ this.status = details.status;
168
+ this.provider = details.provider;
169
+ this.model = details.model;
170
+ this.operatorUrl = details.operatorUrl;
171
+ this.keyVariable = details.keyVariable;
172
+ this.schemaRejection = details.schemaRejection;
173
+ }
174
+ }
175
+
176
+ export type ProviderFailureSurface = "parked" | "ended";
177
+
178
+ /** One cause, one sentence for the surface that owns the disposition. Only a
179
+ * live leased turn may promise continuation; every pre-run door defaults to
180
+ * an ending-safe sentence. No wire payload, provider URL or recovery
181
+ * instruction is accepted as an input. */
182
+ export function renderProviderFailure(cause: ProviderFailureCause, surface: ProviderFailureSurface = "ended"): string {
183
+ switch (cause) {
184
+ case "transient":
185
+ return surface === "parked"
186
+ ? "The model provider is temporarily unavailable; your work is kept and will continue when service recovers."
187
+ : "The model provider is temporarily unavailable; this request did not start.";
188
+ case "rate-limited":
189
+ return surface === "parked"
190
+ ? "The model provider is rate-limited; your work is kept and will continue when capacity returns."
191
+ : "The model provider is rate-limited; this request did not start.";
192
+ case "credit-or-quota-exhausted":
193
+ return surface === "parked"
194
+ ? "The model provider's credit or quota is exhausted; your work is kept and will continue when service recovers."
195
+ : "The model provider's credit or quota is exhausted; this request did not start.";
196
+ case "key-absent":
197
+ return "The model provider key is not configured; this request cannot start until the service is restored.";
198
+ case "key-invalid":
199
+ return "The model provider key was refused; this request cannot start until the service is restored.";
200
+ case "model-unknown":
201
+ return "The configured model is unavailable from its provider; this request cannot start until the service is restored.";
202
+ case "request-rejected":
203
+ return "The model provider rejected the request shape; no work was started.";
204
+ case "permanent":
205
+ return "The model provider refused the call; the request ended without exposing the provider's response.";
206
+ }
207
+ }
208
+
209
+ const providerFailureRecord = (value: unknown): Record<string, unknown> | undefined =>
210
+ typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
211
+
212
+ function providerSchemaRejectionRecord(value: unknown): ProviderSchemaRejection | undefined {
213
+ const row = providerFailureRecord(value);
214
+ return typeof row?.tool === "string" &&
215
+ row.tool.length > 0 &&
216
+ typeof row.keyword === "string" &&
217
+ row.keyword.length > 0
218
+ ? { tool: row.tool, keyword: row.keyword }
219
+ : undefined;
220
+ }
221
+
222
+ function providerFailureBody(body: unknown): unknown {
223
+ if (typeof body !== "string") return body;
224
+ const text = body.trim();
225
+ try {
226
+ return JSON.parse(text) as unknown;
227
+ } catch {
228
+ const at = text.indexOf("{");
229
+ if (at < 0) return undefined;
230
+ try {
231
+ return JSON.parse(text.slice(at)) as unknown;
232
+ } catch {
233
+ return undefined;
234
+ }
235
+ }
236
+ }
237
+
238
+ function providerFailureSignals(value: unknown, into: string[] = []): string[] {
239
+ if (Array.isArray(value)) {
240
+ for (const item of value) providerFailureSignals(item, into);
241
+ return into;
242
+ }
243
+ const row = providerFailureRecord(value);
244
+ if (!row) return into;
245
+ for (const [key, item] of Object.entries(row)) {
246
+ const normalizedKey = key.toLowerCase();
247
+ if (typeof item === "string" && ["code", "type", "error_type", "reason", "limit_source"].includes(normalizedKey))
248
+ into.push(item.toLowerCase());
249
+ else if (typeof item === "number" && normalizedKey === "code") into.push(String(item));
250
+ providerFailureSignals(item, into);
251
+ }
252
+ return into;
253
+ }
254
+
255
+ function providerFailureStatus(input: ProviderFailureAnswer, text: string): number | undefined {
256
+ if (input.status !== undefined) return input.status;
257
+ const match = /(?:^|\b(?:http|status(?: code)?|error|api error)[^0-9]{0,8})([1-5]\d\d)\b/i.exec(text);
258
+ return match ? Number(match[1]) : undefined;
259
+ }
260
+
261
+ function providerFailureText(input: ProviderFailureAnswer): string {
262
+ const error = input.error;
263
+ if (error instanceof Error) return error.message;
264
+ if (typeof error === "string") return error;
265
+ if (typeof input.body === "string") return input.body;
266
+ try {
267
+ return input.body === undefined ? "" : JSON.stringify(input.body);
268
+ } catch {
269
+ return "";
270
+ }
271
+ }
272
+
273
+ function quotedWord(text: string, word: string): boolean {
274
+ return [`'${word}'`, `"${word}"`, `\`${word}\``].some((candidate) => text.includes(candidate));
275
+ }
276
+
277
+ function toolSchemaKeywords(value: unknown, into = new Set<string>()): Set<string> {
278
+ if (Array.isArray(value)) {
279
+ for (const item of value) toolSchemaKeywords(item, into);
280
+ return into;
281
+ }
282
+ const schema = providerFailureRecord(value);
283
+ if (!schema) return into;
284
+ for (const [keyword, item] of Object.entries(schema)) {
285
+ into.add(keyword);
286
+ if ((keyword === "properties" || keyword === "$defs" || keyword === "definitions") && providerFailureRecord(item)) {
287
+ for (const child of Object.values(providerFailureRecord(item)!)) toolSchemaKeywords(child, into);
288
+ } else {
289
+ toolSchemaKeywords(item, into);
290
+ }
291
+ }
292
+ return into;
293
+ }
294
+
295
+ /** The provider adapter's evidence for one repairable schema rejection. The
296
+ * response must prove schema validation failed and name exactly one offered
297
+ * tool plus exactly one keyword present in that tool's sent schema. */
298
+ export function providerSchemaRejectionOf(
299
+ error: unknown,
300
+ tools: readonly ToolDef[] | undefined,
301
+ ): ProviderSchemaRejection | undefined {
302
+ if (!tools || tools.length === 0) return undefined;
303
+ const text = providerFailureText({ error });
304
+ const parsed = providerFailureBody(providerFailureRecord(error) ?? text);
305
+ const prose = providerFailureProse(parsed ?? text);
306
+ if (
307
+ !/(?:invalid|unsupported|refused|rejected)[^.!?]{0,80}(?:json )?schema|(?:json )?schema[^.!?]{0,80}(?:invalid|unsupported|not (?:permitted|supported))/i.test(
308
+ prose,
309
+ )
310
+ )
311
+ return undefined;
312
+ const namedTools = tools.filter((tool) => quotedWord(prose, tool.name));
313
+ if (namedTools.length !== 1) return undefined;
314
+ const [tool] = namedTools;
315
+ const namedKeywords = [...toolSchemaKeywords(tool.inputSchema)].filter((keyword) => quotedWord(prose, keyword));
316
+ if (namedKeywords.length !== 1) return undefined;
317
+ return { tool: tool.name, keyword: namedKeywords[0]! };
318
+ }
319
+
320
+ function providerFailureProse(value: unknown, into: string[] = []): string {
321
+ if (typeof value === "string") {
322
+ into.push(value);
323
+ return into.join(" ");
324
+ }
325
+ if (Array.isArray(value)) {
326
+ for (const item of value) providerFailureProse(item, into);
327
+ return into.join(" ");
328
+ }
329
+ const row = providerFailureRecord(value);
330
+ if (!row) return into.join(" ");
331
+ for (const [key, item] of Object.entries(row)) {
332
+ if (key.toLowerCase() !== "cause") providerFailureProse(item, into);
333
+ }
334
+ return into.join(" ");
335
+ }
336
+
337
+ function signalIncludes(signals: readonly string[], words: readonly string[]): boolean {
338
+ return signals.some((signal) => words.some((word) => signal === word || signal.includes(word)));
339
+ }
340
+
341
+ function isProviderFailureCause(value: unknown): value is ProviderFailureCause {
342
+ return typeof value === "string" && (PROVIDER_FAILURE_CAUSES as readonly string[]).includes(value);
343
+ }
344
+
345
+ function trustedProviderFailureEnvelope(
346
+ value: unknown,
347
+ ): { cause: ProviderFailureCause; schemaRejection?: ProviderSchemaRejection } | undefined {
348
+ if (Array.isArray(value)) {
349
+ for (const item of value) {
350
+ const envelope = trustedProviderFailureEnvelope(item);
351
+ if (envelope !== undefined) return envelope;
352
+ }
353
+ return undefined;
354
+ }
355
+ const row = providerFailureRecord(value);
356
+ if (!row) return undefined;
357
+ if (row.type === "provider_failure" && isProviderFailureCause(row.cause)) {
358
+ const schemaRejection = providerSchemaRejectionRecord(row.schemaRejection);
359
+ return { cause: row.cause, ...(schemaRejection !== undefined ? { schemaRejection } : {}) };
360
+ }
361
+ for (const item of Object.values(row)) {
362
+ const envelope = trustedProviderFailureEnvelope(item);
363
+ if (envelope !== undefined) return envelope;
364
+ }
365
+ return undefined;
366
+ }
367
+
368
+ /** Status plus structured body → one typed failure. Text is consulted only at
369
+ * this adapter boundary for transports that expose no structured error; no
370
+ * consumer owns a word list. Mandatory provider statuses and transport facts
371
+ * precede every untrusted body signal. Unknown answers fail closed as
372
+ * `permanent`. */
373
+ export function classifyProviderFailure(input: ProviderFailureAnswer): ProviderFailure {
374
+ if (input.error instanceof ProviderFailure) return input.error;
375
+ const text = providerFailureText(input);
376
+ const parsed = providerFailureBody(input.body ?? providerFailureRecord(input.error) ?? text);
377
+ const signals = providerFailureSignals(parsed);
378
+ const trusted = input.trustedEnvelope === true ? trustedProviderFailureEnvelope(parsed) : undefined;
379
+ const status = providerFailureStatus(input, text);
380
+ // A parsed provider object contributes its structured signals and prose,
381
+ // but never its untrusted `cause` value. That preserves message-only
382
+ // adapters without re-reading `cause: rate-limited` through the regex.
383
+ const unstructuredText =
384
+ input.body !== undefined && (providerFailureRecord(parsed) !== undefined || Array.isArray(parsed))
385
+ ? providerFailureProse(parsed)
386
+ : text;
387
+ const transientTransport =
388
+ status === 408 ||
389
+ status === 425 ||
390
+ (input.error instanceof Error && (input.error.name === "AbortError" || input.error.name === "TimeoutError")) ||
391
+ (status !== undefined && status >= 500) ||
392
+ /stream ended before message_stop|stream ended without finish_reason|ended before completion/i.test(
393
+ unstructuredText,
394
+ ) ||
395
+ /^(?:(?:AbortError:\s*)?(?:This|The) operation was aborted|Request aborted)\.?$/i.test(unstructuredText.trim()) ||
396
+ /ECONNRESET|ETIMEDOUT|EPIPE|socket hang up|fetch failed|other side closed|network (?:error|failure)|(?:connection|stream) (?:reset|closed|terminated)|timed? ?out/i.test(
397
+ unstructuredText,
398
+ ) ||
399
+ /^terminated$/i.test(unstructuredText.trim()) ||
400
+ /\boverloaded\b/i.test(unstructuredText) ||
401
+ /<html[\s>][\s\S]{0,4000}\b(?:bad gateway|service unavailable|gateway timeout)\b/i.test(unstructuredText);
402
+ let cause: ProviderFailureCause;
403
+ if (trusted !== undefined) cause = trusted.cause;
404
+ else if (status === 402) cause = "credit-or-quota-exhausted";
405
+ else if (status === 429) cause = "rate-limited";
406
+ else if (transientTransport) cause = "transient";
407
+ else if (
408
+ signalIncludes(signals, [
409
+ "limit_source",
410
+ "insufficient_quota",
411
+ "quota_exceeded",
412
+ "credit_limit",
413
+ "billing_hard_limit",
414
+ "payment_required",
415
+ ]) ||
416
+ (providerFailureRecord(parsed)?.metadata !== undefined &&
417
+ providerFailureRecord(providerFailureRecord(parsed)?.metadata)?.limit_source !== undefined)
418
+ )
419
+ cause = "credit-or-quota-exhausted";
420
+ else if (
421
+ signalIncludes(signals, ["rate_limit", "too_many_requests"]) ||
422
+ /\brate[- ]limit(?:ed)?\b/i.test(unstructuredText)
423
+ )
424
+ cause = "rate-limited";
425
+ else if (signalIncludes(signals, ["provider_key_missing", "key_absent", "missing_api_key"])) cause = "key-absent";
426
+ else if (
427
+ status === 401 ||
428
+ signalIncludes(signals, ["authentication_error", "invalid_api_key", "invalid_key", "unauthorized"])
429
+ )
430
+ cause = "key-invalid";
431
+ else if (signalIncludes(signals, ["model_not_found", "unknown_model", "model_unknown"])) cause = "model-unknown";
432
+ else if (
433
+ status === 400 ||
434
+ status === 422 ||
435
+ signalIncludes(signals, ["invalid_request_error", "invalid_json_schema", "bad_request", "unprocessable_entity"])
436
+ )
437
+ cause = "request-rejected";
438
+ else cause = "permanent";
439
+ const operatorUrl = /https?:\/\/[^\s"'<>]+/.exec(text)?.[0];
440
+ return new ProviderFailure(cause, {
441
+ ...(status !== undefined ? { status } : {}),
442
+ ...(input.provider !== undefined ? { provider: input.provider } : {}),
443
+ ...(input.model !== undefined ? { model: input.model } : {}),
444
+ ...(operatorUrl !== undefined ? { operatorUrl } : {}),
445
+ ...(trusted?.schemaRejection !== undefined ? { schemaRejection: trusted.schemaRejection } : {}),
446
+ });
447
+ }
448
+
449
+ function typedProviderFailureOf(error: unknown, seen = new Set<object>()): ProviderFailure | undefined {
450
+ if (error instanceof ProviderFailure) return error;
451
+ const row = providerFailureRecord(error);
452
+ if (!row || seen.has(row)) return undefined;
453
+ seen.add(row);
454
+ if (row.name === "ProviderFailure" && isProviderFailureCause(row.cause)) {
455
+ return new ProviderFailure(row.cause, {
456
+ ...(typeof row.status === "number" ? { status: row.status } : {}),
457
+ ...(typeof row.provider === "string" ? { provider: row.provider } : {}),
458
+ ...(typeof row.model === "string" ? { model: row.model } : {}),
459
+ ...(typeof row.operatorUrl === "string" ? { operatorUrl: row.operatorUrl } : {}),
460
+ ...(typeof row.keyVariable === "string" ? { keyVariable: row.keyVariable } : {}),
461
+ ...(providerSchemaRejectionRecord(row.schemaRejection) !== undefined
462
+ ? { schemaRejection: providerSchemaRejectionRecord(row.schemaRejection)! }
463
+ : {}),
464
+ });
465
+ }
466
+ // StructuredAskError and other boundary wrappers preserve the original
467
+ // throw as `cause`. Follow typed causes through those wrappers before the
468
+ // outer error's copied message can be classified without its status/body.
469
+ return typedProviderFailureOf(row.cause, seen);
470
+ }
471
+
472
+ export function providerFailureOf(error: unknown): ProviderFailure {
473
+ return typedProviderFailureOf(error) ?? classifyProviderFailure({ error });
474
+ }
475
+
476
+ /** Causes that make the provider unavailable rather than ending one call. */
477
+ export function providerFailureParks(cause: ProviderFailureCause): boolean {
478
+ return cause === "transient" || cause === "rate-limited" || cause === "credit-or-quota-exhausted";
479
+ }
480
+
101
481
  /** The three wire shapes a provider block may declare (record 0052):
102
482
  * Anthropic's Messages API, OpenAI's Chat Completions and OpenAI's Responses
103
483
  * API. Each is a proxy route of its own (`PROXY_PATHS`): an `openai-responses`
@@ -47,7 +47,7 @@ const CAUSE_OF = {
47
47
  repo_not_onboarded: "request",
48
48
  repo_access: "policy",
49
49
  pr_head_unknown: "system",
50
- branch_moved: "system",
50
+ workspace_head_mismatch: "system",
51
51
  coordinator_thread_live: "system",
52
52
  live_agent_allowlist: "policy",
53
53
  follow_up_refused: "request",
@@ -103,6 +103,7 @@ const CAUSE_OF = {
103
103
  plan_runner_state_unread: "system",
104
104
  plan_units_merged: "request",
105
105
  plan_history_unavailable: "system",
106
+ decision_record_store_unavailable: "system",
106
107
  plan_runner_conflict: "system",
107
108
  plan_instance_orphaned: "system",
108
109
  plan_start_failed: "system",
@@ -4,6 +4,7 @@
4
4
  import type { DescriptionIssue, PrDescription, RecordedJson, RenderedPointer } from "./prDescriptionTypes.js";
5
5
  import type { HarnessScope } from "./harness/scope.js";
6
6
  import type { ModelCard } from "./modelCard.js";
7
+ import type { ProviderFailureCause } from "./provider.js";
7
8
 
8
9
  /** The `pr_description` review artifact minus the event envelope
9
10
  * (docs/reference/specs/reading-diff.md item 7). */
@@ -752,6 +753,10 @@ export type RunEvent =
752
753
  agentSource?: AgentSource;
753
754
  /** Absent on a command run, which resolves no model. */
754
755
  model?: string;
756
+ /** Decision-record reservation carried in a coding child's brief. */
757
+ record?: string;
758
+ /** Stable direct-task key used to recover that reservation on a re-issue. */
759
+ recordTaskKey?: string;
755
760
  /** The request's trace id (docs/reference/specs/tracing.md), once the root exists. */
756
761
  traceId?: string;
757
762
  /** The harness the run is driven by (`Harness.name`; docs/reference/specs/harness.md
@@ -1118,6 +1123,9 @@ export type RunEvent =
1118
1123
  request?: string;
1119
1124
  refusalCause?: string;
1120
1125
  refusalText?: string;
1126
+ /** The typed cause when the operator model call failed at the provider
1127
+ * boundary. Its refusal text is always the cause-owned renderer. */
1128
+ providerFailure?: ProviderFailureCause;
1121
1129
  attempts?: ReadonlyArray<{ outcome: "accepted" | "violation"; violation?: string }>;
1122
1130
  intake?: { verdict: string; reason: string };
1123
1131
  latencyMs?: number;
@@ -2,7 +2,7 @@
2
2
  // Durable Object applies them inside one transaction; the in-memory ledger
3
3
  // applies them in tests; both agree because this is the only copy.
4
4
 
5
- import type { ClaimResult, FenceResult, IntakeReceipt, IntakeWriteResult, LivePhase } from "./types.js";
5
+ import type { ClaimResult, FenceResult, IntakeReceipt, IntakeWriteResult, LivePhase, StepRecord } from "./types.js";
6
6
 
7
7
  /** One live run per thread. The existing row, if any, is what `live_runs` holds
8
8
  * for the thread; the same run re-claimed by its owner is idempotent (a retry
@@ -80,6 +80,15 @@ export function selectReclaim<T extends { leaseUntil: number; phase: LivePhase;
80
80
  return rows.filter((r) => r.ownerGen !== gen && (r.phase === "handoff" || r.leaseUntil <= now));
81
81
  }
82
82
 
83
+ /** Which inbox rows a reclaim hands the next generation (run-history item 40):
84
+ * every row past the last record's cursor, and the rows at or below it the
85
+ * record names deferred — handed to the run, never read by its model. */
86
+ export function unreadInbox(lastStep: Pick<StepRecord, "inboxConsumedSeq" | "inboxDeferredSeqs"> | null) {
87
+ const consumed = lastStep?.inboxConsumedSeq ?? 0;
88
+ const deferred = new Set(lastStep?.inboxDeferredSeqs ?? []);
89
+ return (item: { seq: number }): boolean => item.seq > consumed || deferred.has(item.seq);
90
+ }
91
+
83
92
  /** The compare-and-swap table for a run's phase. `attaching → live` (the
84
93
  * prompt landed) or `→ finishing` (the dispatch failed before it — never
85
94
  * handoff: an attaching run has nothing to resume from, so the drain waits
@@ -4,7 +4,7 @@
4
4
  // bot and by `deploy/cloudflare-memory/worker.ts` alike, the way runRecord.ts is.
5
5
 
6
6
  import type { ChatMessage } from "../chatMessage.js";
7
- import type { ToolDef } from "../provider.js";
7
+ import { PROVIDER_FAILURE_CAUSES, type ProviderFailureCause, type ToolDef } from "../provider.js";
8
8
  import type { ChannelVisibility } from "../authz/types.js";
9
9
  import type { RunProfile } from "../../config/profile.js";
10
10
  import type { RunEvent } from "../runEvents.js";
@@ -59,6 +59,10 @@ export interface LiveRunMeta {
59
59
  /** The app that relayed the request for the person (authorization.md item 14): a resume or restart keeps app ∩ person at the gates. */
60
60
  postedBy?: string;
61
61
  effort?: string;
62
+ /** Decision-record reservation assigned before the attach, so a restart of
63
+ * the admitted task keeps the same brief and process environment. */
64
+ record?: string;
65
+ recordTaskKey?: string;
62
66
  /** A ship pipeline's parent (record 0060): claimed under the host key
63
67
  * (`hostKey.ts`) while `threadKey` here names the thread itself, so every
64
68
  * record, notice and rebuilt handle files by the metadata's thread and the
@@ -175,6 +179,9 @@ export interface StepRecord {
175
179
  inFlight: InFlightCall[];
176
180
  /** The inbox `seq` the run has consumed up to (0 = none). */
177
181
  inboxConsumedSeq: number;
182
+ /** Seqs at or below `inboxConsumedSeq` the run has not consumed yet: a
183
+ * reclaim offers them again with every row past the cursor. */
184
+ inboxDeferredSeqs?: number[];
178
185
  remainingMs: number;
179
186
  turn: number;
180
187
  iteration: number;
@@ -296,6 +303,8 @@ export interface IntakeReceipt {
296
303
  verdict: "addressed" | "silent";
297
304
  reason: string;
298
305
  source: "model" | "mode" | "question" | "error" | "timeout";
306
+ /** Present only when `source: error` is a failed provider call. */
307
+ providerFailure?: ProviderFailureCause;
299
308
  /** The structured seam's attempts (docs/decisions/0067): what each answer
300
309
  * violated, or that it was accepted; absent when no model was asked. */
301
310
  attempts?: ReadonlyArray<{ outcome: "accepted" | "violation"; violation?: string }>;
@@ -336,6 +345,9 @@ export function isIntakeReceipt(v: unknown): v is IntakeReceipt {
336
345
  r.source === "question" ||
337
346
  r.source === "error" ||
338
347
  r.source === "timeout") &&
348
+ (r.providerFailure === undefined ||
349
+ (typeof r.providerFailure === "string" &&
350
+ (PROVIDER_FAILURE_CAUSES as readonly string[]).includes(r.providerFailure))) &&
339
351
  (r.mode === "mention" || r.mode === "classify") &&
340
352
  typeof r.model === "string" &&
341
353
  typeof r.gen === "number" &&
@@ -1,6 +1,7 @@
1
1
  import type { ChannelVisibility, Predicate } from "./authz/types.js";
2
2
  import type { BoundaryScope, Identity, MachineClass, RunProfile } from "../config/profile.js";
3
3
  import type { RunEvent } from "./runEvents.js";
4
+ import type { ProviderFailureCause } from "./provider.js";
4
5
  import { isHeadMaterial, isSpanRecord } from "./runEvents.js";
5
6
  import { isRunUsage, type RunUsage } from "./runUsage.js";
6
7
  import type { PushedBranch } from "../execution/residentRebind.js";
@@ -153,6 +154,16 @@ export interface RunRecord {
153
154
  /** The thread that started the run (`IncomingMessage.sourceUrl`), for the
154
155
  * index's hover link. Optional as above. */
155
156
  sourceUrl?: string;
157
+ /** The decision-record number the runner reserved and passed in this coding
158
+ * run's brief. Absent when the task writes no record. */
159
+ record?: string;
160
+ /** Stable text-free key for a direct task's re-issue. */
161
+ recordTaskKey?: string;
162
+ /** The final workspace head the run loop independently observed (7 to 40
163
+ * lowercase hex), after every tail step and mechanical salvage. Present
164
+ * only when the workspace had a readable Git HEAD; absent on older records
165
+ * and runs without a Git workspace. */
166
+ headSha?: string;
156
167
  /** The typed handoff a coding child submitted (docs/reference/specs/agent-ship.md
157
168
  * item 14): its deviations from the plan unit, its follow-ups and the
158
169
  * criteria it could not prove — redacted like every stored string. Present
@@ -327,6 +338,20 @@ export function instanceIdOfEvents(events: readonly RunEvent[]): string | undefi
327
338
  return id;
328
339
  }
329
340
 
341
+ /** The reservation carried by the last run_meta that names one. */
342
+ export function decisionRecordOfEvents(
343
+ events: readonly RunEvent[],
344
+ ): { record: string; recordTaskKey?: string } | undefined {
345
+ let reservation: { record: string; recordTaskKey?: string } | undefined;
346
+ for (const event of events)
347
+ if (event.type === "run_meta" && event.record !== undefined)
348
+ reservation = {
349
+ record: event.record,
350
+ ...(event.recordTaskKey !== undefined ? { recordTaskKey: event.recordTaskKey } : {}),
351
+ };
352
+ return reservation;
353
+ }
354
+
330
355
  /** The pull request a run's events say it opened or edited — the last
331
356
  * `pr_opened` wins, as an edit after an open names the same PR — or nothing. */
332
357
  export function prOfEvents(events: readonly RunEvent[]): RunPullRequest | undefined {
@@ -453,6 +478,7 @@ export interface RunOperatorDecision {
453
478
  request?: string;
454
479
  refusalCause?: string;
455
480
  refusalText?: string;
481
+ providerFailure?: ProviderFailureCause;
456
482
  /** The structured seam's attempts (record 0067): what each answer violated,
457
483
  * or that it was accepted. */
458
484
  attempts?: { outcome: "accepted" | "violation"; violation?: string }[];
@@ -488,6 +514,7 @@ export function operatorOfEvents(events: readonly RunEvent[]): RunOperatorDecisi
488
514
  ...(e.request !== undefined ? { request: e.request } : {}),
489
515
  ...(e.refusalCause !== undefined ? { refusalCause: e.refusalCause } : {}),
490
516
  ...(e.refusalText !== undefined ? { refusalText: e.refusalText } : {}),
517
+ ...(e.providerFailure !== undefined ? { providerFailure: e.providerFailure } : {}),
491
518
  ...(e.attempts
492
519
  ? {
493
520
  attempts: e.attempts.map((a) => ({
@@ -1011,6 +1038,10 @@ export function isRunRecord(v: unknown): v is RunRecord {
1011
1038
  )
1012
1039
  return false;
1013
1040
  if (!isOptionalString(r.activity) || !isOptionalString(r.sourceUrl) || !isOptionalString(r.userName)) return false;
1041
+ if (r.record !== undefined && (typeof r.record !== "string" || !/^\d{4}$/.test(r.record))) return false;
1042
+ if (r.recordTaskKey !== undefined && (typeof r.recordTaskKey !== "string" || !/^[0-9a-f]{16}$/.test(r.recordTaskKey)))
1043
+ return false;
1044
+ if (r.headSha !== undefined && (typeof r.headSha !== "string" || !REVIEW_HEAD_PATTERN.test(r.headSha))) return false;
1014
1045
  // The handoff is checked for shape, not bounds (docs/reference/specs/agent-ship.md
1015
1046
  // item 14): redaction may lengthen a stored string past the tool's limit.
1016
1047
  if (r.handoff !== undefined && !isHandoffShape(r.handoff)) return false;
@@ -65,6 +65,8 @@ export interface Guard {
65
65
 
66
66
  export interface ChildContract {
67
67
  unit: ContractUnit;
68
+ /** The decision-record number reserved for this unit at admission. */
69
+ record?: string;
68
70
  specRows: ContractSpecRow[];
69
71
  /** Absent when the repository has neither rules file. */
70
72
  agentRules: AgentRules | undefined;