@coreplane/switchboard 1.249.1 → 1.251.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 (57) hide show
  1. package/dist/assets/config/config.example.yaml +46 -4
  2. package/dist/assets/deploy/cloudflare-memory/worker.ts +260 -1
  3. package/dist/assets/deploy/cloudflare-resident/Dockerfile +12 -3
  4. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +7 -0
  5. package/dist/assets/package-lock.json +3 -3
  6. package/dist/assets/package.json +3 -2
  7. package/dist/assets/project.json +4 -0
  8. package/dist/assets/source.json +3 -3
  9. package/dist/assets/src/agents/registry.ts +1 -1
  10. package/dist/assets/src/core/authz/policy.ts +10 -0
  11. package/dist/assets/src/core/authz/resource.ts +6 -2
  12. package/dist/assets/src/core/authz/types.ts +2 -0
  13. package/dist/assets/src/core/budgets.ts +13 -0
  14. package/dist/assets/src/core/coordinator/contract.ts +120 -0
  15. package/dist/assets/src/core/coordinator/driver.ts +17 -3
  16. package/dist/assets/src/core/modelCard.ts +19 -3
  17. package/dist/assets/src/core/prDescriptionTypes.ts +35 -0
  18. package/dist/assets/src/core/provider.ts +57 -3
  19. package/dist/assets/src/core/refusal.ts +14 -0
  20. package/dist/assets/src/core/runEvents.ts +49 -2
  21. package/dist/assets/src/core/runFriction.ts +2 -0
  22. package/dist/assets/src/core/runLedger/decisions.ts +9 -1
  23. package/dist/assets/src/core/runLedger/sessionLog.ts +10 -0
  24. package/dist/assets/src/core/runLedger/transcript.ts +13 -3
  25. package/dist/assets/src/core/runLedger/types.ts +72 -6
  26. package/dist/assets/src/core/runRecord.ts +34 -0
  27. package/dist/assets/src/core/ship/contract.ts +17 -4
  28. package/dist/assets/src/core/ship/coordinator.ts +70 -4
  29. package/dist/assets/src/core/trace/attrs.ts +13 -2
  30. package/dist/assets/src/core/trace/workerTrace.ts +3 -2
  31. package/dist/assets/src/core/types.ts +3 -0
  32. package/dist/assets/web/dist/.vite/manifest.json +58 -52
  33. package/dist/assets/web/dist/assets/DeliveryPage-DUXd-Sl-.js +1 -0
  34. package/dist/assets/web/dist/assets/HomePage-mSiqEEcN.js +2 -0
  35. package/dist/assets/web/dist/assets/{PendingTurnRow-BuRre8it.js → PendingTurnRow-DDhMhrI7.js} +1 -1
  36. package/dist/assets/web/dist/assets/{ResidentDetailPage-BIUXyz6K.js → ResidentDetailPage-BnEoOnGQ.js} +1 -1
  37. package/dist/assets/web/dist/assets/{ResidentsIndexPage-BZymgSAb.js → ResidentsIndexPage-Dxpgf-l-.js} +1 -1
  38. package/dist/assets/web/dist/assets/RunFoldRow-CSo4-vld.js +1 -0
  39. package/dist/assets/web/dist/assets/{RunRoutePage-bgkjkA0p.js → RunRoutePage-9klVWhSF.js} +6 -6
  40. package/dist/assets/web/dist/assets/{RunsIndexPage-8S944AzB.js → RunsIndexPage-BplMIgaw.js} +1 -1
  41. package/dist/assets/web/dist/assets/{ScheduledPage-8bBtG9y3.js → ScheduledPage-B_GgeJrb.js} +1 -1
  42. package/dist/assets/web/dist/assets/{SettingsPage-DQeNvfaV.js → SettingsPage-BXX4R113.js} +1 -1
  43. package/dist/assets/web/dist/assets/{StatusDot-BPE5syBa.js → StatusDot-BOaw8le9.js} +1 -1
  44. package/dist/assets/web/dist/assets/{Tooltip-DkoeZfTs.js → Tooltip-DYZZ4l4V.js} +1 -1
  45. package/dist/assets/web/dist/assets/{UnitRoutePage-BUzw--Ii.js → UnitRoutePage-BaSW5Odq.js} +1 -1
  46. package/dist/assets/web/dist/assets/budgets-BvWYKPsY.js +1 -0
  47. package/dist/assets/web/dist/assets/{dist-D11y9ZJ4.js → dist-BCVXeBJ9.js} +1 -1
  48. package/dist/assets/web/dist/assets/indexRow-Bde9OZxG.js +1 -0
  49. package/dist/assets/web/dist/assets/{main-B6LcgNM6.js → main-Dkcbtu3u.js} +2 -2
  50. package/dist/assets/web/dist/assets/sseReplay-DPwdsaok.js +9 -0
  51. package/dist/cli.js +4594 -2743
  52. package/package.json +1 -1
  53. package/dist/assets/web/dist/assets/DeliveryPage-DF4aQypG.js +0 -1
  54. package/dist/assets/web/dist/assets/HomePage-BpQRky8B.js +0 -2
  55. package/dist/assets/web/dist/assets/RunFoldRow-3m4CPRI4.js +0 -1
  56. package/dist/assets/web/dist/assets/indexRow-BmK74Vp1.js +0 -1
  57. package/dist/assets/web/dist/assets/sseReplay-DXC7kGbN.js +0 -9
@@ -59,6 +59,14 @@ 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
+ /** A ship pipeline's parent (record 0060): claimed under the host key
63
+ * (`hostKey.ts`) while `threadKey` here names the thread itself, so every
64
+ * record, notice and rebuilt handle files by the metadata's thread and the
65
+ * occupancy readers that key on the ledger's key column never see it. */
66
+ hosted?: true;
67
+ /** The run's index label (the registry sets it at create): on the row so a
68
+ * hosted run listed from the ledger reads as its registry view does. */
69
+ label?: string;
62
70
  ref?: string;
63
71
  headSha?: string;
64
72
  pr?: number;
@@ -96,10 +104,10 @@ export interface LiveRunMeta {
96
104
  request?: Record<string, unknown>;
97
105
  /** The router's decision when it chose the run's preset (routing-and-config
98
106
  * item 21) — the same fields the record's `route` event carries. On the row
99
- * so a resume repaints the card as it was (` · routed: <reason>`, the
100
- * parts, the override footer on the close) and a reclaim closing the run
101
- * knows it was routed without reading the events. Absent for a preset a
102
- * person, a scope or the default chose. */
107
+ * so a resume repaints the card as it was (the `route reason:` note at
108
+ * debug, the parts) and a record built from the row knows it was routed
109
+ * without reading the events. Absent for a preset a person, a scope or the
110
+ * default chose. */
103
111
  route?: {
104
112
  preset: string;
105
113
  reason: string;
@@ -250,7 +258,65 @@ export interface CompactionEntry {
250
258
 
251
259
  /** The rows a step write carries, each at its log index: the previous step's
252
260
  * results and this step's assistant turn as messages, and pi's compaction
253
- * entry as a row of its own between them. */
254
- export type TranscriptTurn = { idx: number; message: ChatMessage } | { idx: number; compaction: CompactionEntry };
261
+ * entry as a row of its own between them. The optional `actor` on a message
262
+ * turn is the platform-namespaced id of the person who authored it (record
263
+ * 0057); absent for machine turns, compaction rows and pre-0057 rows. */
264
+ export type TranscriptTurn =
265
+ { idx: number; message: ChatMessage; actor?: string } | { idx: number; compaction: CompactionEntry };
255
266
 
256
267
  export type AppendableEvent = RunEvent & { seq: number };
268
+
269
+ /** One intake verdict as the ledger stores it (docs/reference/specs/run-history.md item
270
+ * 59; docs/decisions/0058): keyed by the message (`<channel>:<ts>`), written
271
+ * first-writer-wins so one unmentioned thread reply is decided once across
272
+ * processes and the reconnect catch-up reads the verdict instead of deciding
273
+ * again. The shape is the intake seam's (`src/core/intake.ts` re-exports it);
274
+ * spelled out here because this file is shared with the state Worker. */
275
+ export interface IntakeReceipt {
276
+ verdict: "addressed" | "silent";
277
+ reason: string;
278
+ source: "model" | "mode" | "error" | "timeout";
279
+ mode: "mention" | "classify";
280
+ /** The `<provider>/<model>` ref the verdict ran on. */
281
+ model: string;
282
+ /** The deciding process's generation counter, for telling a retried write's
283
+ * own landed row from another writer's. */
284
+ gen: number;
285
+ threadKey: string;
286
+ /** Epoch ms. */
287
+ decidedAt: number;
288
+ }
289
+
290
+ /** What an intake write answers: whether THIS write landed, and the row that
291
+ * stands — the first writer's, whoever that was. */
292
+ export interface IntakeWriteResult {
293
+ inserted: boolean;
294
+ stored: IntakeReceipt;
295
+ }
296
+
297
+ /** `listIntake`'s filters: a thread's rows, rows since an instant, or both. */
298
+ export interface IntakeQuery {
299
+ threadKey?: string;
300
+ since?: number;
301
+ }
302
+
303
+ /** The receipt as the Worker route validates it: every field present and of
304
+ * its type, the enums closed — a malformed receipt is 400, never stored. */
305
+ export function isIntakeReceipt(v: unknown): v is IntakeReceipt {
306
+ if (typeof v !== "object" || v === null) return false;
307
+ const r = v as Record<string, unknown>;
308
+ return (
309
+ (r.verdict === "addressed" || r.verdict === "silent") &&
310
+ typeof r.reason === "string" &&
311
+ (r.source === "model" || r.source === "mode" || r.source === "error" || r.source === "timeout") &&
312
+ (r.mode === "mention" || r.mode === "classify") &&
313
+ typeof r.model === "string" &&
314
+ typeof r.gen === "number" &&
315
+ Number.isFinite(r.gen) &&
316
+ typeof r.threadKey === "string" &&
317
+ r.threadKey.length > 0 &&
318
+ r.threadKey.length <= 256 &&
319
+ typeof r.decidedAt === "number" &&
320
+ Number.isFinite(r.decidedAt)
321
+ );
322
+ }
@@ -39,6 +39,12 @@ import {
39
39
  // reached `finish`.
40
40
  export type RunStatus = "completed" | "stopped_soft" | "stopped_hard" | "failed" | "interrupted";
41
41
 
42
+ /** How every store-only reader renders a record still in its provisional
43
+ * window (run-history.md item 27's third state): not `interrupted` (a real
44
+ * terminal state) and not `running` (the store cannot know). One string for
45
+ * the CLI's `runs list`/`runs get` and the web's index row and run page. */
46
+ export const PROVISIONAL_LABEL = "unfinished — no finish recorded";
47
+
42
48
  const RUN_STATUSES: readonly RunStatus[] = ["completed", "stopped_soft", "stopped_hard", "failed", "interrupted"];
43
49
 
44
50
  /** Every `runs.*` id: checked before any store call. */
@@ -115,6 +121,13 @@ export interface RunRecord {
115
121
  stepCount?: number;
116
122
  schema?: number;
117
123
  status: RunStatus;
124
+ /** Present on a tombstone record still in its provisional window — the
125
+ * start-of-run `interrupted` or the drain-deadline upgrade — before the
126
+ * run's final write lands. Absent on every final (finished) record. A
127
+ * store-only reader must render a provisional record as "unfinished — no
128
+ * finish recorded" rather than as `interrupted`, because the run may still
129
+ * be live in a registry the reader cannot see (run-history.md item 27). */
130
+ provisional?: true;
118
131
  /** The failure by name, when a `failed` run has one (item 57):
119
132
  * `policy_refusal`, the provider refused the run's model call under its
120
133
  * usage policy. Absent on a run that did not fail, on one that failed for
@@ -190,6 +203,11 @@ export interface RunRecord {
190
203
  * item 48), stored at the claim so a retried spawn finds its run. Present
191
204
  * exactly when `parentInstanceId` is — both or neither, never one alone. */
192
205
  idempotencyKey?: string;
206
+ /** The plan runner instance this run's hand-off created (record 0051 R2;
207
+ * item 2): the last `ship_handoff` event, folded at the assembly like the
208
+ * coordinator tag. Present only on a ship run whose hand-off succeeded;
209
+ * a record written before the event has none. */
210
+ instanceId?: string;
193
211
  /** Where the run's conversation started (item 52): `channel` — its own
194
212
  * thread's history, as for every run a person, a schedule or a coordinator
195
213
  * started — or `parent` — a spawned child seeded from its parent's text
@@ -266,6 +284,16 @@ export function leaseOfEvents(events: readonly RunEvent[]): RunLease | undefined
266
284
  return undefined;
267
285
  }
268
286
 
287
+ /** The plan runner instance a ship run's events say its hand-off created —
288
+ * the last `ship_handoff` wins — or nothing (record 0051 R2): projected onto
289
+ * `RunRecord.instanceId` the way `coordinator_tag` rides the record, so the
290
+ * thread's owner rule finds the instance from the page's ship run. */
291
+ export function instanceIdOfEvents(events: readonly RunEvent[]): string | undefined {
292
+ let id: string | undefined;
293
+ for (const e of events) if (e.type === "ship_handoff") id = e.instanceId;
294
+ return id;
295
+ }
296
+
269
297
  /** The pull request a run's events say it opened or edited — the last
270
298
  * `pr_opened` wins, as an edit after an open names the same PR — or nothing. */
271
299
  export function prOfEvents(events: readonly RunEvent[]): RunPullRequest | undefined {
@@ -908,6 +936,9 @@ export function isRunRecord(v: unknown): v is RunRecord {
908
936
  (typeof r.idempotencyKey !== "string" || !IDEMPOTENCY_KEY_PATTERN.test(r.idempotencyKey))
909
937
  )
910
938
  return false;
939
+ // The instance a ship run's hand-off created (record 0051 R2; item 2).
940
+ if (r.instanceId !== undefined && (typeof r.instanceId !== "string" || !INSTANCE_ID_PATTERN.test(r.instanceId)))
941
+ return false;
911
942
  if (typeof r.channelId !== "string" || typeof r.userId !== "string" || typeof r.threadKey !== "string") return false;
912
943
  if (r.relayedBy !== undefined && typeof r.relayedBy !== "string") return false;
913
944
  if (r.authenticatedAs !== undefined && typeof r.authenticatedAs !== "string") return false;
@@ -926,6 +957,9 @@ export function isRunRecord(v: unknown): v is RunRecord {
926
957
  return false;
927
958
  }
928
959
  if (!RUN_STATUSES.includes(r.status as RunStatus)) return false;
960
+ // A provisional tombstone carries `provisional: true`; any other value (false,
961
+ // a string, etc.) is a malformed record — only the presence of the flag matters.
962
+ if (r.provisional !== undefined && r.provisional !== true) return false;
929
963
  if (!isFiniteNumber(r.eventCount) || !isFiniteNumber(r.storedEventCount)) return false;
930
964
  if (typeof r.truncated !== "boolean") return false;
931
965
  if (!Array.isArray(r.events)) return false;
@@ -439,17 +439,30 @@ export const TIMEOUT_ON_LONG_COMMANDS =
439
439
  "State a timeout on any command you expect to run longer than a minute: a timeout that reaches past the loop's " +
440
440
  "end is refused before the command runs, never cut midway.";
441
441
 
442
+ /** The fast gates a plan child runs before every push, named one by one
443
+ * (agent-ship item 13; agent-coding item 9). "Its cheapest proving checks"
444
+ * left the choice to the child, and children chose wrong: prettier was
445
+ * reported clean while `format:check` was red, and hygiene imprints reached
446
+ * CI that `hygiene:check` would have caught locally. Naming the commands
447
+ * makes each gate a receipt — the exit line goes into the handoff's verified
448
+ * list, and a gate the child could not run is unproven, never claimed clean. */
449
+ export const FAST_GATES_BEFORE_PUSH =
450
+ "The fast gates, before every push: `npx prettier --check` on the changed files, `npm run hygiene:check`, " +
451
+ "`npm run specs:check`, and `tsc --noEmit` on the touched project under `NODE_OPTIONS=--max-old-space-size=6144`. " +
452
+ "Paste each command's exit line into the handoff's verified list; a gate you could not run goes under unproven " +
453
+ "and is never claimed clean.";
454
+
442
455
  function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
443
456
  const branch = rebase.branch ? `\`${rebase.branch}\`` : "the unit's branch";
444
457
  const onto = rebase.onto ? `\`${rebase.onto}\`` : "the merged parent";
445
458
  return (
446
459
  `Rebase ${branch} onto ${onto} before any other work — the parent unit has merged and the base has moved; ` +
447
460
  `the only writes are your own on that branch. A conflict ends the unit: report it as the handoff and stop. ` +
448
- `Push the branch as soon as the change exists and its cheapest proving checks pass — before the project's ` +
461
+ `Push the branch as soon as the change exists and the fast gates pass — before the project's ` +
449
462
  `full verification, which runs after that push with any fix as a further commit; an unpushed tree does not ` +
450
- `survive the run's end. Right before each push, fetch ${onto} again and rebase once more if it moved while ` +
451
- `you worked, so the pull request is not born conflicting. At the wind-down note, commit and push what compiles, ` +
452
- `say what does not, then answer. ${TIMEOUT_ON_LONG_COMMANDS}`
463
+ `survive the run's end. ${FAST_GATES_BEFORE_PUSH} Right before each push, fetch ${onto} again and rebase ` +
464
+ `once more if it moved while you worked, so the pull request is not born conflicting. At the wind-down note, ` +
465
+ `commit and push what compiles, say what does not, then answer. ${TIMEOUT_ON_LONG_COMMANDS}`
453
466
  );
454
467
  }
455
468
 
@@ -453,6 +453,13 @@ export type CoordinatorAction =
453
453
  * submitted description when the record holds one) instead of answering
454
454
  * `none` over stranded work. */
455
455
  recover?: { runId: string };
456
+ /** The pull request the machine has adopted (`state.pr`), when it holds
457
+ * one: the child may have worked that pull request's own head branch,
458
+ * not the unit's (issue 1799), so when nothing heads the unit's branch
459
+ * the bot follows this number and answers the pull request's LIVE state
460
+ * — open at a fresh head, merged, or closed (`prClosed`) — never `none`
461
+ * over a record fact written minutes earlier. */
462
+ pr?: number;
456
463
  }
457
464
  | { type: "merge"; step: string; prNumber: number; headSha: string }
458
465
  /** Wait for the intake's checks-settled event at the approved head, bounded as the fallback. */
@@ -509,6 +516,10 @@ export type PrCheck =
509
516
  * naming where the scope landed is the `already_landed` ending
510
517
  * (agent-ship item 12); absent, the fact is unknown and never claimed. */
511
518
  aheadOfBase?: number;
519
+ /** The action's followed pull request (`pr`) was verified CLOSED
520
+ * unmerged: the machine must not brief a review round on it. Absent when
521
+ * nothing was followed or the follow could not read the pull request. */
522
+ prClosed?: boolean;
512
523
  }
513
524
  | {
514
525
  state: "open";
@@ -704,6 +715,10 @@ export interface UnitPipelineInput {
704
715
  * pre-check finds the open pull request still at exactly this head, there is
705
716
  * nothing to code and the attempt starts at the review round. */
706
717
  lastPush?: string;
718
+ /** The runs page base (`<PUBLIC_BASE_URL>/runs`), the plan route's answer:
719
+ * the report's pointer at a child's write-up links its run page with it and
720
+ * names the run id without it — the machine never reads an environment. */
721
+ runPageBase?: string;
707
722
  }
708
723
 
709
724
  type Phase =
@@ -935,6 +950,9 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
935
950
  type: "pr-check",
936
951
  step: `${roundStep(s, p.round)}/pr-check`,
937
952
  ...(p.dead !== undefined ? { recover: { runId: p.runId } } : {}),
953
+ // The adopted pull request rides the check so the bot can follow it
954
+ // when nothing heads the unit's branch (issue 1799).
955
+ ...(s.pr !== undefined ? { pr: s.pr.number } : {}),
938
956
  };
939
957
  case "merge":
940
958
  return { type: "merge", step: `${unit}/merge/${p.n}`, prNumber: p.pr.number, headSha: p.headSha };
@@ -1286,6 +1304,22 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
1286
1304
  [roundNote(round, "aborted")],
1287
1305
  );
1288
1306
  }
1307
+ // Nothing heads the unit's branch, but the machine holds the round's pull
1308
+ // request (`pr_opened` off the child's record, or an earlier round's
1309
+ // adoption): the child worked on that pull request's own head branch, not
1310
+ // the unit's — a re-issued task in the thread of an existing pull request
1311
+ // (issue 1799). The round HAS its pull request — at round 0 and after a
1312
+ // findings step alike, since the same child keeps repushing that branch
1313
+ // through every later round — so it carries on to the (re-)review at the
1314
+ // head the child pushed through the shared open settle (a findings step
1315
+ // that repushed nothing keeps its abort, item 7), instead of the unit
1316
+ // ending "no pull request" or "closed out from under" over work that
1317
+ // stands. Never over a followed answer that VERIFIED the pull request
1318
+ // closed (`prClosed`): a review briefed on a closed pull request reviews
1319
+ // nothing, so the endings below stand — and are then truthful. Dead
1320
+ // children never reach here: their endings are above.
1321
+ if (s.pr !== undefined && pr.prClosed !== true)
1322
+ return roundOnOpenPr(s, phase, { prNumber: s.pr.number, url: s.pr.url });
1289
1323
  if (round.index === 0) {
1290
1324
  // The scope already landed (agent-ship item 12): the child's handoff
1291
1325
  // names where, and the branch carries no commits over the base — two
@@ -1370,6 +1404,18 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
1370
1404
  [roundNote(round, "aborted")],
1371
1405
  );
1372
1406
  }
1407
+ return roundOnOpenPr(s, phase, pr);
1408
+ }
1409
+
1410
+ /** The round carried on its open pull request: the pr-check's `open` answer,
1411
+ * or — when nothing heads the unit's branch — the pull request the machine
1412
+ * already holds (issue 1799), at the head the child pushed. */
1413
+ function roundOnOpenPr(
1414
+ s: UnitPipelineState,
1415
+ phase: Extract<Phase, { at: "pr-check" }>,
1416
+ pr: { prNumber: number; url: string; headSha?: string },
1417
+ ): Transition {
1418
+ const { round } = phase;
1373
1419
  const head = pr.headSha ?? phase.childHead;
1374
1420
  const next: UnitPipelineState = { ...s, pr: { number: pr.prNumber, url: pr.url } };
1375
1421
  if (round.kind === "findings") {
@@ -1647,6 +1693,18 @@ function dispositionFor(s: UnitPipelineState, finding: Finding, round: number):
1647
1693
  return undefined;
1648
1694
  }
1649
1695
 
1696
+ /** Where a child's write-up lives, for the report's pointer (agent-ship item
1697
+ * 12): the child's own message in the thread is the single copy of the detail
1698
+ * — posted moments before the unit-end — so the report points at it instead
1699
+ * of repeating it: the run page when the plan named the base, the run id
1700
+ * otherwise. Undefined when no run is known to point at. */
1701
+ function writeUpPointer(s: UnitPipelineState, kind: RoundKind, runId: string | undefined): string | undefined {
1702
+ if (runId === undefined) return undefined;
1703
+ const base = s.input.runPageBase;
1704
+ const at = base !== undefined ? `${base.replace(/\/+$/, "")}/${encodeURIComponent(runId)}` : `run ${runId}`;
1705
+ return `The ${presetOf(kind)} child's write-up is its own message above in this thread — the single copy of the detail (${at}).`;
1706
+ }
1707
+
1650
1708
  /** How the budget went, in the card's words: coding, review, waiting minutes. */
1651
1709
  function budgetSplitLine(spent: ShipBudgetSpent, maxMinutes: number): string {
1652
1710
  const min = (ms: number) => Math.round(ms / MIN);
@@ -1815,7 +1873,11 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
1815
1873
  case "stopped":
1816
1874
  return join([
1817
1875
  `${e.mode === "hard" ? "⛔" : "⏹"} Ship stopped by operator (${e.mode} stop) after ${rounds}.${prLine}`,
1818
- e.finalReply,
1876
+ writeUpPointer(
1877
+ s,
1878
+ e.round.kind,
1879
+ e.round.kind === "review" ? s.reviewRunByRound[e.round.index] : s.lastCodingRunId,
1880
+ ),
1819
1881
  e.postedReview
1820
1882
  ? "ℹ️ A changes-requested review was posted this round before the stop — its findings stand on the PR."
1821
1883
  : undefined,
@@ -1823,22 +1885,26 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
1823
1885
  ]);
1824
1886
  case "aborted":
1825
1887
  return join([
1826
- e.finalReply,
1827
1888
  e.reason,
1889
+ writeUpPointer(
1890
+ s,
1891
+ e.round?.kind ?? "coding",
1892
+ e.round?.kind === "review" ? s.reviewRunByRound[e.round.index] : s.lastCodingRunId,
1893
+ ),
1828
1894
  e.renewal !== undefined ? `🔁 Not renewed: ${e.renewal.line}.` : undefined,
1829
1895
  `⚠️ Ship aborted after ${rounds}.`,
1830
1896
  reissue,
1831
1897
  ]);
1832
1898
  case "continued":
1833
1899
  return join([
1834
- e.finalReply,
1900
+ writeUpPointer(s, e.round.kind, e.runId),
1835
1901
  `🔁 Segment ${e.segment - 1} ended at its lease with the unit unfinished — ${e.line}. Segment ${e.segment} opens in this thread${e.from !== undefined ? ` from \`${e.from.slice(0, 7)}\`` : ""} under a fresh ${s.input.caps.maxMinutes}-minute lease, with this segment's write-up as its request; ${e.renewalsLeft} renewal${e.renewalsLeft === 1 ? "" : "s"} remain${e.spendUsd !== null ? `, $${e.spendUsd.toFixed(2)} spent so far` : ""}.`,
1836
1902
  budgetSplitLine(e.spent, s.input.caps.maxMinutes),
1837
1903
  ]);
1838
1904
  case "no_verdict":
1839
1905
  return join([
1840
1906
  `⚠️ Review round ${e.round.index} ended without a submitted verdict (budget, refusal, or stop) — ship never converts that into a request for changes, so no findings step ran.`,
1841
- e.finalReply ? `Review round's final message:\n\n${e.finalReply}` : undefined,
1907
+ writeUpPointer(s, e.round.kind, s.reviewRunByRound[e.round.index]),
1842
1908
  `⚠️ Ship aborted after ${rounds}.`,
1843
1909
  reissue,
1844
1910
  ]);
@@ -22,8 +22,16 @@ export interface AttrDomain {
22
22
  caughtUp: boolean;
23
23
  files: number;
24
24
  dedupe: "fresh" | "duplicate";
25
- /** How the requester was found (slack-channel.md item 13): the sender, the relay footer's thread, the thread's parent, or the app itself. */
26
- requester: "message" | "relay-footer" | "thread-parent" | "bot";
25
+ /** How the requester was found (slack-channel.md item 13): the sender, the configured relay app's footer, or the app itself. */
26
+ requester: "message" | "relay-footer" | "bot";
27
+ /** The intake verdict on an unmentioned thread reply (routing-and-config.md
28
+ * item 27, record 0058); the reason is free text and lives on the receipt
29
+ * row and the log line, never on a span. */
30
+ intake: "addressed" | "silent";
31
+ /** How the verdict was reached (`IntakeSource`). */
32
+ intakeSource: "model" | "mode" | "error" | "timeout";
33
+ /** What became of the receipt (`IntakeReceiptOutcome`). */
34
+ intakeReceipt: "inserted" | "existing" | "failed" | "absent";
27
35
  // dispatch.* / run.* / post.*
28
36
  outcome: string;
29
37
  /** The refusal's code (src/core/refusal.ts) — on the `dispatch.refuse` span
@@ -169,6 +177,9 @@ const ATTR_TYPE: Record<SpanAttrKey, "string" | "number" | "boolean"> = {
169
177
  files: "number",
170
178
  dedupe: "string",
171
179
  requester: "string",
180
+ intake: "string",
181
+ intakeSource: "string",
182
+ intakeReceipt: "string",
172
183
  outcome: "string",
173
184
  refusal: "string",
174
185
  cause: "string",
@@ -54,9 +54,10 @@ export function shimRoute(pathname: string): string | undefined {
54
54
  if (pathname.startsWith("/admin/")) return "admin";
55
55
  // The artifact copy (deploy/cloudflare/artifactsCopy.ts): the shim's own route, a 1 GB stream.
56
56
  if (pathname === "/artifacts/copy") return "artifacts";
57
- // The model proxy's two routes (docs/reference/specs/model-proxy.md): a bounded
57
+ // The model proxy's three routes (docs/reference/specs/model-proxy.md): a bounded
58
58
  // request per model call, forwarded to the container like everything else.
59
- if (pathname === "/v1/messages" || pathname === "/v1/chat/completions") return "model-proxy";
59
+ if (pathname === "/v1/messages" || pathname === "/v1/chat/completions" || pathname === "/v1/responses")
60
+ return "model-proxy";
60
61
  // The pi harness's four routes (docs/reference/specs/harness-pi.md item 7): a
61
62
  // run's extension asking for its tools, a verdict, a relayed tool's result,
62
63
  // how a compaction is written.
@@ -146,6 +146,9 @@ export interface HistoryItem {
146
146
  images?: ImageAttachment[];
147
147
  /** Non-image files attached to this turn, if any (user turns only in practice). */
148
148
  documents?: DocumentAttachment[];
149
+ /** The platform-namespaced id of the author (e.g. `slack:U…`); absent for
150
+ * bot turns and channels that do not stamp user ids (record 0057). */
151
+ user?: string;
149
152
  }
150
153
 
151
154
  /** What the run is doing right now, typed so each channel draws it in its