@coreplane/switchboard 1.254.0 → 1.255.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.
Files changed (63) hide show
  1. package/dist/assets/.dockerignore +3 -0
  2. package/dist/assets/Dockerfile +12 -1
  3. package/dist/assets/config/config.example.yaml +6 -1
  4. package/dist/assets/deploy/cloudflare/worker.ts +39 -23
  5. package/dist/assets/deploy/cloudflare-memory/worker.ts +552 -5
  6. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +5 -0
  7. package/dist/assets/deploy/cloudflare-resident/Dockerfile +13 -1
  8. package/dist/assets/deploy/cloudflare-resident/levels.ts +84 -0
  9. package/dist/assets/deploy/cloudflare-resident/prepare-commit-msg +17 -0
  10. package/dist/assets/deploy/cloudflare-resident/worker.ts +202 -31
  11. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +11 -1
  12. package/dist/assets/deploy/cloudflare-sandbox/prepare-commit-msg +17 -0
  13. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +21 -4
  14. package/dist/assets/deploy/hooks/prepare-commit-msg +17 -0
  15. package/dist/assets/deploy/secrets.manifest.json +12 -0
  16. package/dist/assets/package-lock.json +3 -3
  17. package/dist/assets/package.json +1 -1
  18. package/dist/assets/source.json +3 -3
  19. package/dist/assets/src/agents/registry.ts +21 -0
  20. package/dist/assets/src/core/budgets.ts +35 -2
  21. package/dist/assets/src/core/coordinator/contract.ts +6 -0
  22. package/dist/assets/src/core/coordinator/driver.ts +49 -6
  23. package/dist/assets/src/core/costs.ts +39 -16
  24. package/dist/assets/src/core/pipelineStanding.ts +5 -0
  25. package/dist/assets/src/core/plane/decide.ts +389 -22
  26. package/dist/assets/src/core/refusal.ts +3 -0
  27. package/dist/assets/src/core/reviewVerdict.ts +15 -2
  28. package/dist/assets/src/core/runEvents.ts +32 -13
  29. package/dist/assets/src/core/runLedger/sessionLog.ts +128 -0
  30. package/dist/assets/src/core/runLedger/types.ts +3 -0
  31. package/dist/assets/src/core/runRecord.ts +24 -7
  32. package/dist/assets/src/core/ship/coordinator.ts +327 -61
  33. package/dist/assets/src/core/trace/workerTrace.ts +3 -0
  34. package/dist/assets/src/execution/sandboxErrors.ts +77 -6
  35. package/dist/assets/web/dist/.vite/manifest.json +59 -58
  36. package/dist/assets/web/dist/assets/CostsPage-BaeWnm-o.js +1 -0
  37. package/dist/assets/web/dist/assets/{DeliveryPage-ngPsO2to.js → DeliveryPage-BBAyLwPq.js} +1 -1
  38. package/dist/assets/web/dist/assets/{HomePage-DvxTHzPx.js → HomePage-Be7jLLnU.js} +1 -1
  39. package/dist/assets/web/dist/assets/PendingTurnRow-DGINv9XT.js +1 -0
  40. package/dist/assets/web/dist/assets/{PlanePage-DpWfiX4C.js → PlanePage-JEj-lqgz.js} +1 -1
  41. package/dist/assets/web/dist/assets/{ResidentDetailPage-DG86v39Y.js → ResidentDetailPage-D_RD6wLo.js} +1 -1
  42. package/dist/assets/web/dist/assets/{ResidentsIndexPage-x6p689VH.js → ResidentsIndexPage-BLkSuCxo.js} +1 -1
  43. package/dist/assets/web/dist/assets/RunFoldRow-CfuZqf_O.js +1 -0
  44. package/dist/assets/web/dist/assets/{RunRoutePage-ysJBY8xQ.js → RunRoutePage-CmWYGR36.js} +4 -4
  45. package/dist/assets/web/dist/assets/{RunsIndexPage-CuzFchcn.js → RunsIndexPage-DW-HHuZa.js} +1 -1
  46. package/dist/assets/web/dist/assets/{ScheduledPage-BuLmfcbG.js → ScheduledPage-3aYsDf-q.js} +1 -1
  47. package/dist/assets/web/dist/assets/{SettingsPage-BujWkdU_.js → SettingsPage-DG-p5Xy1.js} +1 -1
  48. package/dist/assets/web/dist/assets/{StatusDot-C8Bc0pTX.js → StatusDot-CEnGlyAL.js} +1 -1
  49. package/dist/assets/web/dist/assets/{Tooltip-BWwJx27K.js → Tooltip-CiunVowT.js} +1 -1
  50. package/dist/assets/web/dist/assets/UnitRoutePage-iOYnmTcQ.js +1 -0
  51. package/dist/assets/web/dist/assets/budgets-c1eumrqD.js +1 -0
  52. package/dist/assets/web/dist/assets/{dist-CpnyQGOb.js → dist-luhv3YSo.js} +1 -1
  53. package/dist/assets/web/dist/assets/indexRow-DborJPFp.js +1 -0
  54. package/dist/assets/web/dist/assets/{main-Comxmwi4.js → main-mAKx_zo9.js} +2 -2
  55. package/dist/assets/web/dist/assets/{sseReplay-IzTdD4-3.js → sseReplay-DE6wv1Ua.js} +6 -6
  56. package/dist/cli.js +4154 -3061
  57. package/package.json +1 -1
  58. package/dist/assets/web/dist/assets/CostsPage-BuKjw3nv.js +0 -1
  59. package/dist/assets/web/dist/assets/PendingTurnRow-ZYIRCCZ2.js +0 -1
  60. package/dist/assets/web/dist/assets/RunFoldRow-DG29LOTs.js +0 -1
  61. package/dist/assets/web/dist/assets/UnitRoutePage-B9kjA1AT.js +0 -1
  62. package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +0 -1
  63. package/dist/assets/web/dist/assets/indexRow-DABQtONT.js +0 -1
@@ -35,6 +35,134 @@ export function sessionKey(threadKey: string, agent: string | undefined): string
35
35
  return `${threadKey}:${agent ?? "-"}`;
36
36
  }
37
37
 
38
+ /** The thread session's key (record 0057; the one-door plan's memory unit,
39
+ * item 13): one log per thread, read and written by the operator — every
40
+ * connector event the intake gate admits, every child's report, every
41
+ * question and answer is a turn in it. The `@thread` half can never collide
42
+ * with `sessionKey`'s agent half: no agent id starts with `@`. */
43
+ export function threadSessionKey(threadKey: string): string {
44
+ return `${threadKey}:@thread`;
45
+ }
46
+
47
+ /** A working session's lane: a unit's coding rounds continue one log, its
48
+ * review rounds another (item 13). */
49
+ export type WorkingLane = "coding" | "review";
50
+
51
+ /** A working session's key `<instance>:<unit>:<lane>` (item 13). A re-issue of the
52
+ * same plan carries an attempt suffix on its instance id (`planInstanceId`:
53
+ * `plan-<id>-<attempt>`, attempt ≥ 2); the key strips it, so a re-issue
54
+ * continues the prior instance's lanes rather than starting cold. */
55
+ export function workingSessionKey(instance: { id: string; attempt?: number }, unit: string, lane: WorkingLane): string {
56
+ const suffix = instance.attempt !== undefined ? `-${instance.attempt}` : "";
57
+ const base =
58
+ suffix !== "" && instance.id.endsWith(suffix)
59
+ ? instance.id.slice(0, instance.id.length - suffix.length)
60
+ : instance.id;
61
+ return `${base}:${unit}:${lane}`;
62
+ }
63
+
64
+ /** A connector turn's row id (item 13): the message id, with its edit timestamp
65
+ * when the message was edited — an edited message appends a second row (the
66
+ * first said what the person first said), a re-delivered unedited one
67
+ * appends nothing. */
68
+ export function connectorRowId(messageId: string, editedAt?: string | number): string {
69
+ return editedAt === undefined ? messageId : `${messageId}:edit-${editedAt}`;
70
+ }
71
+
72
+ /** The fold's row id for a hosted parent's `ship_unit` event (item 13): the
73
+ * event's own identity — its unit, its state and the registry seq it was
74
+ * published under — so the same event read twice folds one row. The seq is
75
+ * required (the registry stamps every published event with one): without it,
76
+ * a unit reopened at a second segment would repeat a state — two `started`
77
+ * events — and the second fold would be dropped as a duplicate. */
78
+ export function shipUnitRowId(event: { unit: string; state: string; seq: number }): string {
79
+ return `ship-unit:${event.unit}:${event.state}:${event.seq}`;
80
+ }
81
+
82
+ /** One thread-session row as its JSON is stored (item 13): a text turn with
83
+ * its author when a person wrote it, `silent` when the intake gate withheld
84
+ * the reply (the person's words are context all the same), `folded` when the
85
+ * row is a child's report folded whole — what `operatorTail` keeps ahead of
86
+ * older turns. */
87
+ export function storedTurnRow(turn: {
88
+ role: "user" | "assistant";
89
+ text: string;
90
+ actor?: string;
91
+ silent?: boolean;
92
+ folded?: boolean;
93
+ }): string {
94
+ return JSON.stringify({
95
+ role: turn.role,
96
+ part: { type: "text", text: turn.text },
97
+ ...(turn.actor !== undefined ? { actor: turn.actor } : {}),
98
+ ...(turn.silent === true ? { silent: true } : {}),
99
+ ...(turn.folded === true ? { folded: true } : {}),
100
+ });
101
+ }
102
+
103
+ /** Whether a stored row is a reply the intake gate withheld as silent (item 13). */
104
+ export function silentOfStoredRow(json: string): boolean {
105
+ const stored = parseStored(json);
106
+ return stored !== undefined && !("compaction" in stored) && (stored as { silent?: unknown }).silent === true;
107
+ }
108
+
109
+ /** Whether a stored row is a folded report (item 13) — kept whole by the
110
+ * operator's cap ahead of older turns. */
111
+ export function foldedOfStoredRow(json: string): boolean {
112
+ const stored = parseStored(json);
113
+ return stored !== undefined && !("compaction" in stored) && (stored as { folded?: unknown }).folded === true;
114
+ }
115
+
116
+ /** A run of an old `<thread>:<agent>` log, as the cutover migration reads it
117
+ * (item 13): its log's key, when it started and the row range its record closed. */
118
+ export interface MigrationRun {
119
+ key: string;
120
+ startedAt: number;
121
+ range: { from: number; to?: number };
122
+ }
123
+
124
+ /** The order the cutover migration reads an old thread's per-agent logs into
125
+ * the thread session (item 13): the runs by their start times, each run's rows in
126
+ * its session range in index order, and rows outside any run's range after
127
+ * the runs that precede them in their own log (before every run of that log
128
+ * when none does). The read is once: each row lands under the row id
129
+ * `migrationRowId(key, idx)`, so a replay appends nothing twice, and the old
130
+ * keys stay read-only for recall. */
131
+ export function migrationOrder(
132
+ runs: readonly MigrationRun[],
133
+ logs: readonly { key: string; rows: readonly number[] }[],
134
+ ): Array<{ key: string; idx: number }> {
135
+ const byStart = [...runs].sort((a, b) => a.startedAt - b.startedAt);
136
+ // A row's place: inside a run's range it rides at that run's start (phase 0);
137
+ // past a run's closed range it rides after that run's rows (phase 1); before
138
+ // every run of its log it comes first of all (start -Infinity).
139
+ const placed = logs.flatMap((log, logOrder) =>
140
+ log.rows.map((idx) => {
141
+ let at = Number.NEGATIVE_INFINITY;
142
+ let phase = 1;
143
+ for (const r of byStart) {
144
+ if (r.key !== log.key) continue;
145
+ const to = r.range.to;
146
+ if (idx >= r.range.from && (to === undefined || idx <= to)) {
147
+ at = r.startedAt;
148
+ phase = 0;
149
+ break;
150
+ }
151
+ if (to !== undefined && to < idx && r.startedAt > at) at = r.startedAt;
152
+ }
153
+ return { key: log.key, idx, at, phase, logOrder };
154
+ }),
155
+ );
156
+ placed.sort((a, b) => a.at - b.at || a.phase - b.phase || a.logOrder - b.logOrder || a.idx - b.idx);
157
+ return placed.map(({ key, idx }) => ({ key, idx }));
158
+ }
159
+
160
+ /** The row id a migrated row lands under (item 13): the old log's key and the
161
+ * row's index there — stable, so the read-once migration is idempotent. */
162
+ export function migrationRowId(key: string, idx: number): string {
163
+ return `migrated:${key}#${idx}`;
164
+ }
165
+
38
166
  /** The request is the seed's last user turn (`splitSeed` reads the seed the
39
167
  * same way); a seed with no user turn — or no turns — puts its first row there. */
40
168
  export function requestIndex(seed: readonly ChatMessage[]): number {
@@ -287,6 +287,9 @@ export interface IntakeReceipt {
287
287
  verdict: "addressed" | "silent";
288
288
  reason: string;
289
289
  source: "model" | "mode" | "error" | "timeout";
290
+ /** The structured seam's attempts (docs/decisions/0067): what each answer
291
+ * violated, or that it was accepted; absent when no model was asked. */
292
+ attempts?: ReadonlyArray<{ outcome: "accepted" | "violation"; violation?: string }>;
290
293
  mode: "mention" | "classify";
291
294
  /** The `<provider>/<model>` ref the verdict ran on. */
292
295
  model: string;
@@ -392,9 +392,10 @@ export function callsInFlight(events: readonly RunEvent[], status: RunStatus): C
392
392
  return [...calls.values()].filter((c) => c.state === "cut" || (c.state === "open" && !orderly)).map((c) => c.call);
393
393
  }
394
394
 
395
- /** The router's decision as a record carries it — the same fields the
396
- * `route` event and the ledger row's `meta.route` carry (routing-and-config
397
- * item 21). */
395
+ /** The router's decision as a record carries it — the fields the `route`
396
+ * event and the ledger row's `meta.route` carry (routing-and-config item 21),
397
+ * minus the event's `attempts` (record 0067): the record keeps the decision,
398
+ * the re-asks stay on the event. */
398
399
  export interface RunRouteDecision {
399
400
  preset: string;
400
401
  reason: string;
@@ -425,7 +426,7 @@ export function routeOfEvents(events: readonly RunEvent[]): RunRouteDecision | u
425
426
  * fields less the stream bookkeeping (record 0057; run-history item 60). */
426
427
  export interface RunOperatorDecision {
427
428
  mode: "shadow" | "on";
428
- outcome: "binds" | "question" | "refusal";
429
+ outcome: "binds" | "question" | "refusal" | "non_decision";
429
430
  reason: string;
430
431
  binds?: { line: string; reason: string }[];
431
432
  question?: string;
@@ -433,6 +434,9 @@ export interface RunOperatorDecision {
433
434
  proposal?: string;
434
435
  refusalCause?: string;
435
436
  refusalText?: string;
437
+ /** The structured seam's attempts (record 0067): what each answer violated,
438
+ * or that it was accepted. */
439
+ attempts?: { outcome: "accepted" | "violation"; violation?: string }[];
436
440
  intake?: { verdict: string; reason: string };
437
441
  latencyMs?: number;
438
442
  outputTokens?: number;
@@ -454,6 +458,14 @@ export function operatorOfEvents(events: readonly RunEvent[]): RunOperatorDecisi
454
458
  ...(e.proposal !== undefined ? { proposal: e.proposal } : {}),
455
459
  ...(e.refusalCause !== undefined ? { refusalCause: e.refusalCause } : {}),
456
460
  ...(e.refusalText !== undefined ? { refusalText: e.refusalText } : {}),
461
+ ...(e.attempts
462
+ ? {
463
+ attempts: e.attempts.map((a) => ({
464
+ outcome: a.outcome,
465
+ ...(a.violation !== undefined ? { violation: a.violation } : {}),
466
+ })),
467
+ }
468
+ : {}),
457
469
  ...(e.intake ? { intake: { verdict: e.intake.verdict, reason: e.intake.reason } } : {}),
458
470
  ...(e.latencyMs !== undefined ? { latencyMs: e.latencyMs } : {}),
459
471
  ...(e.outputTokens !== undefined ? { outputTokens: e.outputTokens } : {}),
@@ -497,9 +509,14 @@ function isRunPullRequestShape(v: unknown): v is RunPullRequest {
497
509
  * (item 57). `policy_refusal`: the model provider refused the run's call
498
510
  * under its usage policy — the stop reason its wire names, never the
499
511
  * explanation's words — so the session's next seed leaves the refused
500
- * request out of its tail (docs/reference/specs/session-log.md item 9). A
501
- * failure without a name here leaves the record without the field. */
502
- export const RUN_FAILURE_KINDS = ["policy_refusal"] as const;
512
+ * request out of its tail (docs/reference/specs/session-log.md item 9).
513
+ * `provider_transient`: the run's model call failed on a provider transient
514
+ * (a gateway 5xx, a cut stream, a gateway timeout) with the harness's retry
515
+ * ladder spent — the ship runner reads it off the child's record to re-run a
516
+ * round-0 child that pushed nothing instead of aborting the unit
517
+ * (docs/reference/specs/agent-ship.md item 9, issue 1932). A failure without
518
+ * a name here leaves the record without the field. */
519
+ export const RUN_FAILURE_KINDS = ["policy_refusal", "provider_transient"] as const;
503
520
  export type RunFailureKind = (typeof RUN_FAILURE_KINDS)[number];
504
521
  export interface RunFailure {
505
522
  kind: RunFailureKind;