@sema-agent/server 7.110.0 → 7.112.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 (67) hide show
  1. package/MIGRATION.md +16 -0
  2. package/dist/approval-ask-audit-store.d.ts +38 -25
  3. package/dist/approval-ask-audit-store.js +26 -59
  4. package/dist/bench/s1/run-firm.d.ts +11 -0
  5. package/dist/bench/s1/run-firm.js +47 -32
  6. package/dist/boot/coordinators.js +1 -1
  7. package/dist/boot/session-faces.d.ts +4 -0
  8. package/dist/boot/session-faces.js +3 -1
  9. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  10. package/dist/boot/stage-07-capability-layer.js +4 -3
  11. package/dist/boot/stage-08-reapers.d.ts +1 -0
  12. package/dist/boot/stage-09-leader.d.ts +1 -0
  13. package/dist/boot/stage-09-leader.js +2 -1
  14. package/dist/boot/stage-10-http-server.d.ts +1 -0
  15. package/dist/boot/workflow-orchestration.js +1 -1
  16. package/dist/hooks/hook-runner.d.ts +5 -0
  17. package/dist/hooks/hook-runner.js +45 -1
  18. package/dist/http/admission.js +6 -1
  19. package/dist/http/resume-legs.d.ts +6 -15
  20. package/dist/http/resume-legs.js +33 -14
  21. package/dist/http/route-ctx.d.ts +28 -11
  22. package/dist/http/routes/approvals-assistant.js +1 -1
  23. package/dist/http/routes/runs.js +17 -7
  24. package/dist/http/routes/tasks.d.ts +3 -2
  25. package/dist/http/routes/tasks.js +45 -51
  26. package/dist/http/server.d.ts +1 -1
  27. package/dist/http/server.js +1 -2
  28. package/dist/index.d.ts +1 -1
  29. package/dist/instruction-selection.d.ts +34 -0
  30. package/dist/instruction-selection.js +23 -0
  31. package/dist/observability/corrupt-read-seat.d.ts +18 -0
  32. package/dist/observability/corrupt-read-seat.js +18 -0
  33. package/dist/observability/fail-open.d.ts +20 -4
  34. package/dist/observability/fail-open.js +17 -4
  35. package/dist/orchestration/workflow-agent-session-index.d.ts +12 -23
  36. package/dist/orchestration/workflow-agent-session-index.js +11 -50
  37. package/dist/orchestration/workflow-notify-journal.d.ts +11 -8
  38. package/dist/orchestration/workflow-notify-journal.js +20 -36
  39. package/dist/plugins/file-outcome-sink.d.ts +4 -1
  40. package/dist/plugins/file-outcome-sink.js +9 -3
  41. package/dist/plugins/file-run-store.js +3 -3
  42. package/dist/plugins/retention-store-sql.d.ts +1 -1
  43. package/dist/plugins/retention-store-sql.js +2 -0
  44. package/dist/plugins/store-backend.js +1 -0
  45. package/dist/plugins/store-contracts.d.ts +59 -9
  46. package/dist/plugins/store-contracts.js +10 -0
  47. package/dist/plugins/user-input-ledger-file.d.ts +4 -2
  48. package/dist/plugins/user-input-ledger-file.js +11 -1
  49. package/dist/plugins/user-input-ledger-sql.d.ts +14 -4
  50. package/dist/plugins/user-input-ledger-sql.js +46 -18
  51. package/dist/project-memory.d.ts +10 -2
  52. package/dist/project-memory.js +5 -2
  53. package/dist/run-cancel-context.d.ts +59 -7
  54. package/dist/run-cancel-context.js +26 -0
  55. package/dist/runs.d.ts +7 -5
  56. package/dist/runs.js +22 -16
  57. package/dist/trace/ledger-sink.d.ts +4 -0
  58. package/dist/trace/ledger-sink.js +1 -1
  59. package/dist/trace/project.d.ts +11 -3
  60. package/dist/trace/project.js +6 -5
  61. package/dist/trace/self-minted-wire.d.ts +48 -0
  62. package/dist/trace/self-minted-wire.js +17 -0
  63. package/dist/trace/wire-projection-faces.d.ts +3 -3
  64. package/dist/trace/wire-projection-faces.js +4 -2
  65. package/dist/user-input-ledger.d.ts +101 -48
  66. package/dist/user-input-ledger.js +52 -35
  67. package/package.json +3 -3
@@ -10,7 +10,7 @@ import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotific
10
10
  import { createLedgerSink } from "../../trace/ledger-sink.js";
11
11
  import { registerEngineNoticeLeg } from "../../trace/engine-notice-wire.js";
12
12
  import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../../turn-activity.js";
13
- import { mintCancelledResult } from "../../run-cancel-context.js";
13
+ import { buildCancelledLedgerRow, createRunAbort, mintCancelledResult } from "../../run-cancel-context.js";
14
14
  import { recordResourceWindow } from "../../resource-window.js";
15
15
  import { redactSecrets } from "../../trace/redact.js";
16
16
  import { contextUsageEventData, turnEndEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestFaceEventData, toolRosterDeltaEventData, toolProgressEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage, textEndEventData, liveIdentityFields } from "../../trace/project.js";
@@ -28,6 +28,7 @@ import { failOpenTagForDroppedFrame, recordFailOpen } from "../../observability/
28
28
  import { buildSyncSubmitBody } from "../sync-submit-body.js";
29
29
  import { assertNever } from "../route-ctx.js";
30
30
  import { withModelReachScope } from "../../model-reach-scope.js";
31
+ import { createInstructionSelectionLeg } from "../../instruction-selection.js";
31
32
  const DISCONNECT_PARK_GRACE_MS = 10_000;
32
33
  function sseData(res, payload) {
33
34
  const anchor = payload.eventId;
@@ -56,7 +57,7 @@ function sendSubmitResult(res, resp) {
56
57
  const MULTI_ATTEMPT_NO_EVENT_CHANNEL_NOTICE = "verify / cascade ran with their normal multi-attempt semantics, but the synchronous /v1/tasks endpoint has no event channel — no status/progress frames exist to replay on this shape. For status / progress / delegated-child frames and the terminal done, submit via POST /v1/runs and read GET /v1/runs/:id/events; note that verify / cascade legs carry no per-turn stream there either (no wiring_manifest / text / turn_end frames — the engine runs those legs as result-returning attempts).";
57
58
  export async function handleTasks(req, res, match, ctx) {
58
59
  const { deps } = ctx;
59
- const { idemCache, inflightRuns, cancelledViaVerb, steerableRuns, counters } = ctx.registry;
60
+ const { idemCache, inflightRuns, steerableRuns, counters } = ctx.registry;
60
61
  const { rateLimited, quotaExceeded, leaseDenied, usageWindowDenied } = ctx.helpers;
61
62
  const { prepareSpec, finalizeTaskResult } = ctx.legs;
62
63
  const reqState = ctx.req;
@@ -114,6 +115,7 @@ export async function handleTasks(req, res, match, ctx) {
114
115
  const principal = prepared.auth?.principal;
115
116
  const wiringOperatorFace = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals);
116
117
  let wiringReceipt;
118
+ const instructionLeg = createInstructionSelectionLeg();
117
119
  const withRunReceipt = (out) => wiringReceipt !== undefined ? { ...out, wiringManifest: wiringReceipt } : out;
118
120
  if (liveStream) {
119
121
  if (prepared.verify || prepared.cascade) {
@@ -134,7 +136,7 @@ export async function handleTasks(req, res, match, ctx) {
134
136
  if (earlyDurableTid) {
135
137
  res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", taskId: earlyDurableTid, ...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}) })}\n\n`);
136
138
  }
137
- const ac = new AbortController();
139
+ const ac = createRunAbort();
138
140
  let closed = false;
139
141
  let detachLogged = false;
140
142
  let disconnectParkGrace;
@@ -156,14 +158,14 @@ export async function handleTasks(req, res, match, ctx) {
156
158
  deps.logger?.info("stream_disconnect_ask_parked", { taskId: earlyDurableTid, sessionId: prepared.spec.sessionId, asks: parked, graceMs: DISCONNECT_PARK_GRACE_MS });
157
159
  disconnectParkGrace = setTimeout(() => {
158
160
  closed = true;
159
- ac.abort();
161
+ ac.abort("ask_park_grace_exhausted");
160
162
  }, DISCONNECT_PARK_GRACE_MS);
161
163
  disconnectParkGrace.unref?.();
162
164
  return;
163
165
  }
164
166
  }
165
167
  closed = true;
166
- ac.abort();
168
+ ac.abort("client_disconnected");
167
169
  };
168
170
  req.on("close", onDisconnect);
169
171
  res.on("close", () => {
@@ -211,13 +213,7 @@ export async function handleTasks(req, res, match, ctx) {
211
213
  let runRowSettled = false;
212
214
  let durableTaskId;
213
215
  let durableHeartbeat;
214
- const cancelledResult = (errorMessage, engineResult) => mintCancelledResult({
215
- taskId: durableTaskId ?? "",
216
- sessionId: prepared.spec.sessionId ?? "",
217
- startedAtMs: streamLegStartedAt,
218
- message: errorMessage,
219
- ...(engineResult !== undefined ? { engineResult } : {}),
220
- });
216
+ const cancelledResult = (errorMessage, cause) => mintCancelledResult({ taskId: durableTaskId ?? "", sessionId: prepared.spec.sessionId ?? "", startedAtMs: streamLegStartedAt, message: errorMessage, cause });
221
217
  let userMsgEntryId;
222
218
  let anchorPut = false;
223
219
  const putRewindAnchor = () => {
@@ -261,14 +257,11 @@ export async function handleTasks(req, res, match, ctx) {
261
257
  if (durableTaskId && deps.runStore) {
262
258
  const rs = deps.runStore;
263
259
  const tid = durableTaskId;
264
- ledgerSink = createLedgerSink({ appendEvent: (seq, type, data) => rs.appendEvent(tid, seq, type, data), persistThinking: deps.config.traceThinking, onActivity: (kind, statusData) => recordTurnActivity(tid, kind, statusData) });
260
+ ledgerSink = createLedgerSink({ appendEvent: (seq, type, data) => rs.appendEvent(tid, seq, type, data), persistThinking: deps.config.traceThinking, onActivity: (kind, statusData) => recordTurnActivity(tid, kind, statusData), instructionsSegment: instructionLeg.segment });
265
261
  usageKey = tid;
266
262
  deps.modelUsage?.register(tid);
267
263
  const owner = principal ?? null;
268
- const abortFromVerb = () => {
269
- cancelledViaVerb.add(tid);
270
- ac.abort();
271
- };
264
+ const abortFromVerb = () => ac.abort("cancel_verb");
272
265
  durableHeartbeat = setInterval(() => {
273
266
  void rs.heartbeat(tid, owner).catch(() => undefined);
274
267
  if (!ac.signal.aborted) {
@@ -290,10 +283,8 @@ export async function handleTasks(req, res, match, ctx) {
290
283
  catch (e) {
291
284
  if (durableHeartbeat)
292
285
  clearInterval(durableHeartbeat);
293
- if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
286
+ if (durableTaskId && inflightRuns.get(durableTaskId) === ac)
294
287
  inflightRuns.delete(durableTaskId);
295
- cancelledViaVerb.delete(durableTaskId);
296
- }
297
288
  throw e;
298
289
  }
299
290
  let fleetSettled = false;
@@ -313,10 +304,8 @@ export async function handleTasks(req, res, match, ctx) {
313
304
  catch (e) {
314
305
  if (durableHeartbeat)
315
306
  clearInterval(durableHeartbeat);
316
- if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
307
+ if (durableTaskId && inflightRuns.get(durableTaskId) === ac)
317
308
  inflightRuns.delete(durableTaskId);
318
- cancelledViaVerb.delete(durableTaskId);
319
- }
320
309
  settleFleet("failed");
321
310
  throw e;
322
311
  }
@@ -428,13 +417,13 @@ export async function handleTasks(req, res, match, ctx) {
428
417
  sseData(res, end);
429
418
  if (ev.type === "message_committed" && ev.role === "user")
430
419
  userMsgEntryId = ev.entryId;
431
- const cancelVerbAtDone = ev.type === "done" && durableTaskId !== undefined && cancelledViaVerb.has(durableTaskId);
420
+ const causeAtDone = ev.type === "done" ? ac.cause() : undefined;
432
421
  if (ev.type === "done") {
433
422
  finalResult = await mintSubmitExitResult(ev.result, deps.checkpointStore);
434
423
  if (durableTaskId && finalResult)
435
424
  finalResult = { ...finalResult, taskId: durableTaskId };
436
- if (cancelVerbAtDone && finalResult !== undefined) {
437
- finalResult = cancelledResult("cancelled by user", finalResult);
425
+ if (causeAtDone !== undefined && finalResult !== undefined) {
426
+ finalResult = mintCancelledResult({ taskId: durableTaskId ?? "", sessionId: prepared.spec.sessionId ?? "", startedAtMs: streamLegStartedAt, message: "cancelled by user", cause: causeAtDone, engineResult: finalResult });
438
427
  }
439
428
  finalizeTaskResult(ev.result, principal, prepared.spec.objective, prepared.spec.sessionId);
440
429
  noteRunOutcome(finalResult);
@@ -504,7 +493,7 @@ export async function handleTasks(req, res, match, ctx) {
504
493
  sseData(res, { type: "compaction_outcome", ...compactionOutcomeEventData(ev) });
505
494
  }
506
495
  else if (ev.type === "wiring_manifest") {
507
- const wiringFace = wiringManifestFaceEventData(ev, wiringOperatorFace);
496
+ const wiringFace = wiringManifestFaceEventData(ev, wiringOperatorFace, instructionLeg.segment());
508
497
  wiringReceipt ??= wiringFace;
509
498
  sseData(res, { type: "wiring_manifest", ...wiringFace });
510
499
  }
@@ -647,7 +636,7 @@ export async function handleTasks(req, res, match, ctx) {
647
636
  });
648
637
  }
649
638
  const withApproval = () => approvalCtx && deps.toolApproval ? deps.toolApproval.runWithContext(approvalCtx, withQuestion) : withQuestion();
650
- await withModelReachScope(prepared.spec.sessionId, () => deps.sendUserFile
639
+ await instructionLeg.run(prepared.spec.sessionId, () => withModelReachScope(prepared.spec.sessionId, () => deps.sendUserFile
651
640
  ? deps.sendUserFile.runWithContext({
652
641
  taskId: askTaskId,
653
642
  emit: async (f) => {
@@ -658,7 +647,7 @@ export async function handleTasks(req, res, match, ctx) {
658
647
  }
659
648
  },
660
649
  }, withApproval)
661
- : withApproval());
650
+ : withApproval()));
662
651
  if (durableTaskId && deps.runStore) {
663
652
  const finalPlane = finalResult ? outwardPlane(finalResult.terminal) : undefined;
664
653
  if (finalResult && finalPlane?.status === "suspended" && deps.checkpointStore)
@@ -670,6 +659,7 @@ export async function handleTasks(req, res, match, ctx) {
670
659
  else {
671
660
  putRewindAnchor();
672
661
  const pending = await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId);
662
+ const dcCause = ac.cause();
673
663
  if (pending) {
674
664
  if (ledgerSink && !ledgerTerminal) {
675
665
  ledgerTerminal = true;
@@ -679,18 +669,28 @@ export async function handleTasks(req, res, match, ctx) {
679
669
  await deps.runStore.setSuspended(durableTaskId, null);
680
670
  settleFleet("suspended");
681
671
  }
682
- else {
672
+ else if (dcCause !== undefined) {
683
673
  const dcErr = "stream client disconnected before completion (run aborted)";
684
- const dc = cancelledResult(dcErr);
674
+ const dc = cancelledResult(dcErr, dcCause);
685
675
  if (ledgerSink && !ledgerTerminal) {
686
676
  ledgerTerminal = true;
687
677
  await ledgerSink.flush().catch(() => undefined);
688
- await ledgerSink.append("failed", { errorMessage: dcErr, errorCode: "cancelled" }).catch(() => undefined);
678
+ await ledgerSink.append("failed", buildCancelledLedgerRow(dc)).catch(() => undefined);
689
679
  }
690
680
  await deps.runStore.setTerminal(durableTaskId, "failed", dc, dcErr);
691
681
  noteRunOutcome(dc);
692
682
  settleFleet("failed");
693
683
  }
684
+ else {
685
+ const err = "stream ended before a terminal event";
686
+ if (ledgerSink && !ledgerTerminal) {
687
+ ledgerTerminal = true;
688
+ await ledgerSink.flush().catch(() => undefined);
689
+ await ledgerSink.append("failed", { errorMessage: err }).catch(() => undefined);
690
+ }
691
+ await deps.runStore.setTerminal(durableTaskId, "failed", null, err);
692
+ settleFleet("failed");
693
+ }
694
694
  }
695
695
  runRowSettled = true;
696
696
  }
@@ -704,10 +704,8 @@ export async function handleTasks(req, res, match, ctx) {
704
704
  steerableRuns.delete(durableTaskId);
705
705
  if (liveStreamRef !== undefined)
706
706
  await deps.userInputLedger?.settleLeg(liveStreamRef);
707
- if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
707
+ if (durableTaskId && inflightRuns.get(durableTaskId) === ac)
708
708
  inflightRuns.delete(durableTaskId);
709
- cancelledViaVerb.delete(durableTaskId);
710
- }
711
709
  for (const evict of subagentHandleEvictions)
712
710
  evict();
713
711
  if (!runRowSettled && durableTaskId && deps.runStore) {
@@ -715,6 +713,7 @@ export async function handleTasks(req, res, match, ctx) {
715
713
  const row = await deps.runStore.getRun(durableTaskId);
716
714
  if (row?.status === "running") {
717
715
  const pending = prepared.spec.sessionId ? await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId) : undefined;
716
+ const backstopCause = ac.cause();
718
717
  if (pending) {
719
718
  if (ledgerSink && !ledgerTerminal) {
720
719
  ledgerTerminal = true;
@@ -733,13 +732,13 @@ export async function handleTasks(req, res, match, ctx) {
733
732
  await deps.runStore.setTerminal(durableTaskId, retryPlane.status, finalResult, retryPlane.errorMessage ?? null);
734
733
  settleFleet(retryPlane.status, fleetRunResiduals(finalResult));
735
734
  }
736
- else if (ac.signal.aborted) {
735
+ else if (backstopCause !== undefined) {
737
736
  const err = "stream aborted before completion (run cancelled)";
738
- const c = cancelledResult(err);
737
+ const c = cancelledResult(err, backstopCause);
739
738
  if (ledgerSink && !ledgerTerminal) {
740
739
  ledgerTerminal = true;
741
740
  await ledgerSink.flush().catch(() => undefined);
742
- await ledgerSink.append("failed", { errorMessage: err, errorCode: "cancelled" }).catch(() => undefined);
741
+ await ledgerSink.append("failed", buildCancelledLedgerRow(c)).catch(() => undefined);
743
742
  }
744
743
  await deps.runStore.setTerminal(durableTaskId, "failed", c, err);
745
744
  }
@@ -806,17 +805,14 @@ export async function handleTasks(req, res, match, ctx) {
806
805
  if (deps.checkpointStore && prepared.spec.sessionId) {
807
806
  await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
808
807
  }
809
- const syncCancelCtrl = new AbortController();
808
+ const syncCancelCtrl = createRunAbort();
810
809
  if (prepared.spec.sessionId)
811
810
  markChildrenStoppedByUserOnAbort(syncCancelCtrl.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
812
811
  if (durableTaskId && deps.runStore) {
813
812
  const rs = deps.runStore;
814
813
  const tid = durableTaskId;
815
814
  const owner = principal ?? null;
816
- const abortFromVerb = () => {
817
- cancelledViaVerb.add(tid);
818
- syncCancelCtrl.abort();
819
- };
815
+ const abortFromVerb = () => syncCancelCtrl.abort("cancel_verb");
820
816
  durableHeartbeat = setInterval(() => {
821
817
  void rs.heartbeat(tid, owner).catch(() => undefined);
822
818
  if (!syncCancelCtrl.signal.aborted) {
@@ -836,10 +832,9 @@ export async function handleTasks(req, res, match, ctx) {
836
832
  const observeSyncLegEvent = (ev) => {
837
833
  if (ev.type !== "wiring_manifest" || wiringReceipt !== undefined)
838
834
  return;
839
- wiringReceipt = wiringManifestFaceEventData(ev, wiringOperatorFace);
835
+ wiringReceipt = wiringManifestFaceEventData(ev, wiringOperatorFace, instructionLeg.segment());
840
836
  };
841
837
  let result;
842
- let cancelVerbLabel = false;
843
838
  counters.uncountedBillableInflight++;
844
839
  try {
845
840
  const verifyLeg = (v) => runWithVerification(prepared.runner, specWithSignal, v);
@@ -850,24 +845,23 @@ export async function handleTasks(req, res, match, ctx) {
850
845
  observeSyncLegEvent(ev);
851
846
  return stream.result();
852
847
  };
853
- result = await withPrincipal(principal, () => prepared.verify ? verifyLeg(prepared.verify) : prepared.cascade ? cascadeLeg() : plainLeg());
848
+ result = await instructionLeg.run(prepared.spec.sessionId, () => withPrincipal(principal, () => prepared.verify ? verifyLeg(prepared.verify) : prepared.cascade ? cascadeLeg() : plainLeg()));
854
849
  }
855
850
  finally {
856
851
  counters.uncountedBillableInflight--;
857
852
  if (durableHeartbeat)
858
853
  clearInterval(durableHeartbeat);
859
- if (durableTaskId && inflightRuns.get(durableTaskId) === syncCancelCtrl) {
854
+ if (durableTaskId && inflightRuns.get(durableTaskId) === syncCancelCtrl)
860
855
  inflightRuns.delete(durableTaskId);
861
- cancelVerbLabel = cancelledViaVerb.has(durableTaskId);
862
- cancelledViaVerb.delete(durableTaskId);
863
- }
864
856
  }
865
- if (cancelVerbLabel) {
857
+ const syncAbortCause = syncCancelCtrl.cause();
858
+ if (syncAbortCause !== undefined) {
866
859
  result = mintCancelledResult({
867
860
  taskId: durableTaskId ?? "",
868
861
  sessionId: prepared.spec.sessionId ?? "",
869
862
  startedAtMs: syncLegStartedAt,
870
863
  message: "cancelled by user",
864
+ cause: syncAbortCause,
871
865
  engineResult: result,
872
866
  });
873
867
  }
@@ -218,7 +218,7 @@ export interface ServiceStoreDeps {
218
218
  * 已配 AND 有 store backend(main.ts 建;写点在两条 send lane 的 withLedgerRecording)。 */
219
219
  sendFileLedger?: import("../plugins/send-file-ledger.js").SendFileLedger;
220
220
  /** S-928 用户输入受理账本(「200 applied」受理的影子;单写者 = `src/user-input-ledger.ts` 的活对象,boot 装配在 stage-07 并跑一次收敛)。
221
- * 在场 ⇔ 有 store backend(生产恒与 `runStore` 同源于 backend)。消费点:steer 的 live 腿受理前写 `accepted`(写不进 ⇒ 503
221
+ * 在场 ⇔ 有 store backend(生产恒与 `runStore` 同源于 backend)。消费点:steer 的 live 腿 / interrupt{text} / 同步子代的 steer(S-966)受理前写 `accepted`(写不进 ⇒ 503
222
222
  * `steering.ledger_unavailable`)、三条 run 腿开通告口那一拍铸 `crashed` 通告并在收场时 settle、`GET /v1/sessions/:id` 的
223
223
  * `userInputUndrained` 读面。缺席只出现在不接 backend 的 stub-harness 上(那时没有这一面,行为与修前逐字相同)。 */
224
224
  userInputLedger?: import("../user-input-ledger.js").UserInputLedger;
@@ -43,7 +43,6 @@ export function createHttpServer(rawDeps) {
43
43
  }
44
44
  const idemCache = new IdempotencyCache();
45
45
  const inflightRuns = new Map();
46
- const cancelledViaVerb = new Set();
47
46
  const preemptableRuns = new Map();
48
47
  const steerableRuns = new Map();
49
48
  const wakeParkMints = new Map();
@@ -97,7 +96,7 @@ export function createHttpServer(rawDeps) {
97
96
  }
98
97
  const { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, runSessionAcceptOk, readJson, readRawBody } = createRequestGuards({ deps });
99
98
  const { prepareSpec } = createAdmission({ deps, readJson, runnerFor });
100
- const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake } = createResumeLegs({ deps, runnerFor, idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor });
99
+ const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake } = createResumeLegs({ deps, runnerFor, idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor });
101
100
  const { denyExpiredApprovals, redeemParkedAsk } = createApprovalSweeps({ deps, resumeCheckpoint, mayDiscloseRowError });
102
101
  const { handle } = createDispatch({ deps, noteActivity, routeCtxBase, counters });
103
102
  const server = http.createServer((req, res) => {
package/dist/index.d.ts CHANGED
@@ -29,5 +29,5 @@ export { createKeyResolver, resolveModelApiKey, type ModelKeyRef } from "./key-r
29
29
  export { isModelKeyPoison, ModelKeyPoisonedError, type ModelKeyPoison, type ModelKeyPoisonReason } from "./key-resolver.js";
30
30
  export { RETIRED_ENVS, type RetiredEnv, type RetiredEnvKind, type RetiredEnvWire } from "./config.js";
31
31
  export { CATALOG_ENTRY_HEADER_LANES, CATALOG_ENTRY_HEADER_NAMES, type CatalogEntryHeaderName } from "./model-entry-headers.js";
32
- export type { UserInputLedgerStore, UserInputAcceptance, NewUserInputAcceptance, OpenScanCursor, SettleHow, WriterGoneEvidence } from "./user-input-ledger.js";
32
+ export type { UserInputLedgerStore, UserInputAcceptance, NewUserInputAcceptance, OpenScanCursor, OpenScanPage, SettleHow, WriterGoneEvidence } from "./user-input-ledger.js";
33
33
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,34 @@
1
+ import type { InstructionChainMember } from "./project-memory.js";
2
+ /** wire 段 `wiring_manifest.instructions` 的形。`selected` 的成员拼写 = `INSTRUCTION_FILE_CHAIN` 的成员(相对 cwd)。 */
3
+ export interface InstructionsSegment {
4
+ readonly selected: readonly InstructionChainMember[];
5
+ }
6
+ /**
7
+ * 一次 loader 调用对**腿**说了什么:
8
+ * · `stated: false` —— 没有陈述(走链之前的门拦下了:无手 / cwd 不是本机绝对目录;或这是委派子代的 prepare);
9
+ * · `stated: true` —— 走了链,`file` = 选中的链成员,`null` = 一个都没有。
10
+ * 判别式显式写出来(不拿 `undefined` / `null` 两个空值各代表一件事 —— O6 那条教训:「不适用」不是「来源 = 无」)。
11
+ */
12
+ export type LegInstructionStatement = {
13
+ readonly stated: false;
14
+ } | {
15
+ readonly stated: true;
16
+ readonly file: InstructionChainMember | null;
17
+ };
18
+ /** 一条腿的指令文件选择:开作用域 + 读段。每条投 `wiring_manifest` 的腿(流式 / 非流式 / bg / 续跑)各建一只。 */
19
+ export interface InstructionSelectionLeg {
20
+ /** 在这条腿的作用域里跑它的模型驱动(引擎的 prepare 在里面调 loader)。`sessionId` = 这条腿的会话 —— 作用域里只有**同会话**的 loader 调用
21
+ * 才算这条腿自己的 prepare([ref] opus 腿 F1:wake 续跑在腿自己的 prepare 之前同步跑部署 `userPromptSubmit` 的 agent 型 hook,hook agent 是另一只
22
+ * 会话、却与本进程共用同一只 loader ⇒ 只按「第一次调用」会被它抢先封槽)。嵌套时内层覆盖外层 —— 每条腿说自己的。 */
23
+ run<T>(sessionId: string | undefined, fn: () => T): T;
24
+ /** 这条腿的 `instructions` 段;`undefined` ⇒ 段缺席。在投帧那一刻读:core 在推帧之前已经 await 过 loader。 */
25
+ segment(): InstructionsSegment | undefined;
26
+ }
27
+ export declare function createInstructionSelectionLeg(): InstructionSelectionLeg;
28
+ /**
29
+ * loader 侧唯一写口:当前作用域里、**同会话的**第一次调用封槽(`sessionId` = loader ctx 带的会话;不等于腿的会话 —— hook agent / 子代 /
30
+ * 别的嵌套 run 的 prepare —— 一律不算,也不封槽,等这条腿自己的那一次);作用域外(run-local、leader、council 子任务 …)无效。
31
+ * 「身份 = 作用域 ∧ 同会话」两条都要:只靠作用域会被同进程共用 loader 的别的 Runner 抢先(F1);只靠会话会让同会话的并发腿 / 快速续跑串值。
32
+ */
33
+ export declare function settleLegInstructionStatement(statement: LegInstructionStatement, sessionId: string | undefined): void;
34
+ //# sourceMappingURL=instruction-selection.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const legScope = new AsyncLocalStorage();
3
+ export function createInstructionSelectionLeg() {
4
+ let slot;
5
+ return {
6
+ run: (sessionId, fn) => {
7
+ slot = { sessionId, settled: false };
8
+ return legScope.run(slot, fn);
9
+ },
10
+ segment: () => slot?.segment,
11
+ };
12
+ }
13
+ export function settleLegInstructionStatement(statement, sessionId) {
14
+ const slot = legScope.getStore();
15
+ if (slot === undefined || slot.settled)
16
+ return;
17
+ if (slot.sessionId !== undefined && sessionId !== slot.sessionId)
18
+ return;
19
+ slot.settled = true;
20
+ if (statement.stated)
21
+ slot.segment = { selected: statement.file === null ? [] : [statement.file] };
22
+ }
23
+ //# sourceMappingURL=instruction-selection.js.map
@@ -150,6 +150,24 @@ export declare const JSONL_REPLAY_SEATS: {
150
150
  readonly tag: "server.send-file-ledger.corrupt-record-skipped";
151
151
  readonly note: "F: a skipped ledger row means that issued link is missing from its tenant's listing (the stored object itself is untouched).";
152
152
  };
153
+ readonly "approval-ask-audit": {
154
+ readonly direction: "fail-open";
155
+ readonly event: "approval_ask_audit_corrupt_read";
156
+ readonly tag: "server.approval-ask-audit.corrupt-record-skipped";
157
+ readonly note: "F: a skipped ask-audit record is one audit row missing from the boot crash-convergence read (an ask / settle / leg-close fact gone ⇒ that orphan is listed on a thinner basis or not at all, never a gate — the audit store gates nothing and the approval path never reads it back).";
158
+ };
159
+ readonly "workflow-notify-journal": {
160
+ readonly direction: "fail-open";
161
+ readonly event: "workflow_notify_journal_corrupt_read";
162
+ readonly tag: "server.workflow-notify-journal.corrupt-record-skipped";
163
+ readonly note: "F: a skipped notify-journal record is one run's pending entry (or its ack) missing from the boot replay. The recovery sweep walks the journal, so a lost pending line means that run's completion notify is not re-derived after a crash (the live in-process notify and the WorkflowStatus poll floor are untouched); a lost ack line means one extra idempotent re-delivery (the receiver dedups on runId). It gates no permission and changes no run.";
164
+ };
165
+ readonly "workflow-agent-session-index": {
166
+ readonly direction: "fail-open";
167
+ readonly event: "workflow_agent_session_ledger_line_rejected";
168
+ readonly tag: "server.workflow-agent-session-index.corrupt-record-skipped";
169
+ readonly note: "F: a skipped index line leaves that run on its previous known-good snapshot; a /decide for a park the line should have indexed finds no row and falls back to the legacy lane (an honest miss, never a mis-route — the redeem key is compared verbatim on the run record, not read from this index).";
170
+ };
153
171
  readonly "user-input-ledger": {
154
172
  readonly direction: "fail-open";
155
173
  readonly event: "user_input_ledger_corrupt_read";
@@ -93,6 +93,24 @@ export const JSONL_REPLAY_SEATS = {
93
93
  tag: "server.send-file-ledger.corrupt-record-skipped",
94
94
  note: "F: a skipped ledger row means that issued link is missing from its tenant's listing (the stored object itself is untouched).",
95
95
  },
96
+ "approval-ask-audit": {
97
+ direction: "fail-open",
98
+ event: "approval_ask_audit_corrupt_read",
99
+ tag: "server.approval-ask-audit.corrupt-record-skipped",
100
+ note: "F: a skipped ask-audit record is one audit row missing from the boot crash-convergence read (an ask / settle / leg-close fact gone ⇒ that orphan is listed on a thinner basis or not at all, never a gate — the audit store gates nothing and the approval path never reads it back).",
101
+ },
102
+ "workflow-notify-journal": {
103
+ direction: "fail-open",
104
+ event: "workflow_notify_journal_corrupt_read",
105
+ tag: "server.workflow-notify-journal.corrupt-record-skipped",
106
+ note: "F: a skipped notify-journal record is one run's pending entry (or its ack) missing from the boot replay. The recovery sweep walks the journal, so a lost pending line means that run's completion notify is not re-derived after a crash (the live in-process notify and the WorkflowStatus poll floor are untouched); a lost ack line means one extra idempotent re-delivery (the receiver dedups on runId). It gates no permission and changes no run.",
107
+ },
108
+ "workflow-agent-session-index": {
109
+ direction: "fail-open",
110
+ event: "workflow_agent_session_ledger_line_rejected",
111
+ tag: "server.workflow-agent-session-index.corrupt-record-skipped",
112
+ note: "F: a skipped index line leaves that run on its previous known-good snapshot; a /decide for a park the line should have indexed finds no row and falls back to the legacy lane (an honest miss, never a mis-route — the redeem key is compared verbatim on the run record, not read from this index).",
113
+ },
96
114
  "user-input-ledger": {
97
115
  direction: "fail-open",
98
116
  event: "user_input_ledger_corrupt_read",
@@ -281,9 +281,13 @@ export declare const FAIL_OPEN_TAGS: {
281
281
  readonly cls: "F";
282
282
  readonly note: "S-107:持久 error 脱后 UTF-8 字节 > 65,000(TiDB TEXT 65,535 字节)⇒ 脱后切并带标记(切在占位里不是泄漏);计数=多少条终态文案被字节封顶截断";
283
283
  };
284
+ readonly "server.runs.abort-cause-missing": {
285
+ readonly cls: "F";
286
+ readonly note: "S-947 候裁 ⑤(主会话裁:默认值保留,miss 臂必须响):续跑腿 `http/resume-legs.ts` 的 `checkpoint.resume_aborted` 臂 —— core 只在本腿递进去的信号**已中止**时铸该码,而那只信号唯一的写者是取消动词(快路 / 耐久旗轮询,句柄登记 `cancel_verb`),所以**只有 core 契约外的形**(桩 / 非 core Runner 直接抛该码,本腿句柄从未中止)才到得了「句柄上读不到成因」这一臂。取消终局照铸,成因取本腿唯一的中止词 `cancel_verb`(取消终局恒带成因由铸点签名保证,不让它缺);detail `task=<taskId>`;计数 = 多少次取消成因是默认值而不是句柄登记的。生产上恒零是证据,不归零 = 有一条不走 core 契约的 Runner 在跑续跑腿。";
287
+ };
284
288
  readonly "server.runs.terminal-ledger-write-failed": {
285
289
  readonly cls: "F";
286
- readonly note: "S-107(全量门 silent-degradation 棘轮抓出的五处静默 `catch {}`):run 终局链里**账本腿**(flush 缓冲 text/reasoning、append `failed` 行)与终态翻转(setTerminal)已不共命运——账本 INSERT 抛不再让 run 卡 running 到 reaper;但那条抛掉的账本行之前是**静默**吞的。detail `arm=` 四臂(flush-clean-cancel / append-clean-cancel / flush-catch / append-thrown-cancel / append-failed):缺的是 run_events 尾巴(缓冲文本或 failed 事件),run 行本身仍是 durable truth,读面/reaper 容忍缺失,故 F 类;计数=多少次终局账本写失败。";
290
+ readonly note: "S-107(全量门 silent-degradation 棘轮抓出的五处静默 `catch {}`):run 终局链里**账本腿**(flush 缓冲 text/reasoning、append `failed` 行)与终态翻转(setTerminal)已不共命运——账本 INSERT 抛不再让 run 卡 running 到 reaper;但那条抛掉的账本行之前是**静默**吞的。detail `arm=` 四臂(flush-clean-cancel / append-clean-cancel / flush-catch / append-thrown-cancel / append-failed);S-947 起续跑腿(`http/resume-legs.ts`)两座同 tag:resume-append-resume-aborted / resume-append-thrown-cancel(取消抛出两臂的 `failed` 行,修前那两臂根本不落账本终局行):缺的是 run_events 尾巴(缓冲文本或 failed 事件),run 行本身仍是 durable truth,读面/reaper 容忍缺失,故 F 类;计数=多少次终局账本写失败。";
287
291
  };
288
292
  readonly "server.redact.ppk-eat-to-end": {
289
293
  readonly cls: "F";
@@ -317,17 +321,29 @@ export declare const FAIL_OPEN_TAGS: {
317
321
  readonly cls: "F";
318
322
  readonly note: "S-582(`plugins/send-file-ledger.ts`,local 车道的发文件账本):一条账行读不出形(`42` / 缺 id·scope·objectKey 三元组 / 中间半截 JSON)⇒ 跳过它继续。最坏后果 = 那条已签发的链接在它租户的列表里**看不见**(对象本身没动;账本本来就不存 URL)。治理面少一行是可见性损失,不是放行 ⇒ F;但它恰是治理面要看的那一类事实,所以必须留痕 —— 修前零披露。";
319
323
  };
324
+ readonly "server.approval-ask-audit.corrupt-record-skipped": {
325
+ readonly cls: "F";
326
+ readonly note: "S-967(`approval-ask-audit-store.ts`,local 车道 File ask 审计店的 boot 重放;登记名 `approval-ask-audit`):账本**中间**一行读不出形(`42` / 数组 / 缺键 / 词表外 `kind`·`outcome`·`orphanState` / 半截 JSON;撕尾不走本 tag)⇒ 跳过它继续。最坏后果按行种:丢 `ask` ⇒ 那只孤儿在崩溃收敛读面缺席(无身份可列);丢 `settle` ⇒ 该组按 pending 臂收(审批门在执行之前 ⇒ 若这条 settle 其实是 `allowed` 且工具已执行,`resumeSafe:true` 是错的 —— 与头注「双故障窗」同族);丢 `leg_closed` ⇒ decided 臂多收一行(保守方向);丢 `crash_converged` ⇒ 下一次 boot 重收(幂等);丢 `interrupted_by_shutdown` / `late_settle` ⇒ 成因注退回 `crashed_before_park` / 少一行取证。审计店不开门、不改任何审批结算 ⇒ F。修前:重放坏行只计入 `convergeCrashedAsks().corruptLines`(boot 日志一个数),**无逐条披露、无 tag**。";
327
+ };
328
+ readonly "server.workflow-notify-journal.corrupt-record-skipped": {
329
+ readonly cls: "F";
330
+ readonly note: "S-977(`orchestration/workflow-notify-journal.ts`,local 车道 File notify journal 的 boot 重放;登记名 `workflow-notify-journal`):账本**中间**一行读不出形(`42` / 数组 / 缺 runId·scope / `acked` 非布尔 / 半截 JSON;撕尾不走本 tag)⇒ 跳过它继续。最坏后果按行种:丢 `pending` 行(`record` 写下的那条)⇒ 那条 run 不在恢复积压里,崩溃丢掉的终态通告 boot 恢复扫描不再重导(live 进程内通告与 `WorkflowStatus` 轮询地板不受影响);丢 `acked` 行 ⇒ 该 run 读回 pending(或前一份已知良好的条目),多一次幂等重投(接收方按 runId 去重)。账本不开门、不改任何 run 的执行或授权 ⇒ F。修前:坏行 / 缺键行**静默**(解析失败 `continue`、只判 `typeof runId === \"string\"`)。";
331
+ };
332
+ readonly "server.workflow-agent-session-index.corrupt-record-skipped": {
333
+ readonly cls: "F";
334
+ readonly note: "S-977(`orchestration/workflow-agent-session-index.ts`,local 车道 workflow 出身 park 的 join 索引账本的 boot 重放;登记名 `workflow-agent-session-index`):账本**中间**一行读不出形(`42` / 数组 / 缺 `runId`·`rows` / 任一行成员缺身份位 / 半截 JSON;撕尾不走本 tag)⇒ **整行**跳过,那条 run 保留上一份已知良好的快照(不做「丢坏成员、留好成员」—— 那会铸出比真相少几条 park 的行集)。最坏后果 = 该 run 的 park 在索引里缺行 ⇒ `/decide` 找不到它、退回 legacy 腿(409 `conflict.resume_context_unavailable`):诚实 miss,不是错配 —— 赎回键由记录上的 `parks[]` 逐字比对,不读本索引。索引不开门 ⇒ F。修前:形不对的整行响亮(`workflow_agent_session_ledger_line_rejected`,同事件名保留)但**无 tag**,带换行的半截 JSON 静默 `continue`。";
335
+ };
320
336
  readonly "server.user-input-ledger.append-failed": {
321
337
  readonly cls: "F";
322
- readonly note: "S-928(`src/user-input-ledger.ts`):200 承诺**之后**的一次账本写失败 —— detail 的 `op=` 点名是哪一种:`settle`(腿在本进程收场)/ `withdraw`(引擎没收下这次 steer)/ `converge`(收敛器判孤儿)/ `announce`(通告已投出,记「告知过」)。放行的最坏后果逐个说清:settle / withdraw 漏写 ⇒ 这次受理在账上仍是 open —— 进程之后**若再崩**(local 车道下一次 boot),它会被当成崩溃丢失告知一次(一句多余的「你的 steer 丢了」,而它其实被正常收场了;SQL 车道的收敛判据只认 run 店已落盘的 reap 判决 —— `failed` + `RUN_STALLED_ERROR` ⇒ crashed、仍 `running` ⇒ live、正常翻面 ⇒ leg_over 补结清 —— 正常收场的 run 不会被误收);converge 漏写 ⇒ 留在 open,下一次扫描再判(延迟,不丢);announce 漏写 ⇒ 下一条 run 再说一遍(至多一次退化成至少一次)。都是**告知面**的偏差,不开任何门、不改任何 run 的执行 ⇒ F。计数非零 = 这台机器的账本写不动了(local:数据根盘满 / 只读;SQL:库不可达),steer 此刻多半也在回 503。";
338
+ readonly note: "S-928(`src/user-input-ledger.ts`):200 承诺**之后**的一次账本写失败 —— detail 的 `op=` 点名是哪一种:`settle`(腿在本进程收场)/ `withdraw`(引擎没收下这次 steer)/ `converge`(收敛器判孤儿)/ `announce`(通告已投出,记「告知过」)。放行的最坏后果逐个说清:settle / withdraw 漏写 ⇒ 这次受理在账上仍是 open —— 本进程下一次收敛扫描补结清(本进程写的、已不在活腿上 ⇒ leg_over);进程在那之前就崩了 ⇒ 下一次 boot 的收敛读 run 店判(S-969 L8 起两车道同一只判词:承载它的 run 正常翻面 / 已 park ⇒ leg_over 补结清、不告知;落款死亡判决或写者已死仍 running ⇒ crashed)—— 仍会多说一句的只剩两形:withdraw 漏写后那条腿崩了(一次没变成 200 的受理被告知一次);settle 漏写后本进程续跑了同一条 run、续跑腿崩了(run 行说的是同一 taskId 上**最新**那条腿)。方向都是多说,不漏;converge 漏写 ⇒ 留在 open,下一次扫描再判(延迟,不丢);announce 漏写 ⇒ 下一条 run 再说一遍(至多一次退化成至少一次)。都是**告知面**的偏差,不开任何门、不改任何 run 的执行 ⇒ F。计数非零 = 这台机器的账本写不动了(local:数据根盘满 / 只读;SQL:库不可达),steer 此刻多半也在回 503。";
323
339
  };
324
340
  readonly "server.user-input-ledger.read-failed": {
325
341
  readonly cls: "F";
326
- readonly note: "S-928(`src/user-input-ledger.ts` / `http/routes/sessions.ts`):一次账本**读**失败 —— `op=converge`(收敛扫描读不出 open 受理 ⇒ 本轮不收敛,下一个 tick 再来)/ `op=judge`(SQL 臂判词要读 run 店,run 店读失败 ⇒ 该行留在 open、下一次扫描再判)/ `op=announce`(新 run 首拍读不出本会话的待告知受理 ⇒ 这一拍不告知,run 照跑,下一条 run 再试)/ `op=session-view`(`GET /v1/sessions/:id` 的 additive 键 `userInputUndrained` 读不出 ⇒ 键缺席、会话视图照常 200)。放行的最坏后果 = 一次「你的输入因崩溃丢了」的告知被推迟,或会话视图上这一格读作零条;不阻断任何 run、不改任何回执。反方向(读失败就拒跑 run / 整只会话视图 500)是拿一个告知面的抖动去打掉主路径,方向更坏。";
342
+ readonly note: "S-928(`src/user-input-ledger.ts` / `http/routes/sessions.ts`):一次账本**读**失败 —— `op=converge`(收敛扫描读不出 open 受理 ⇒ 本轮不收敛,下一个 tick 再来)/ `op=judge`(判词要读 run 店 —— S-969 L8 起两车道都读;一趟内同一 taskId 只读一次,读失败 ⇒ 同 run 的每一行各记一笔、留在 open、下一次扫描再判)/ `op=announce`(新 run 首拍读不出本会话的待告知受理 ⇒ 这一拍不告知,run 照跑,下一条 run 再试)/ `op=session-view`(`GET /v1/sessions/:id` 的 additive 键 `userInputUndrained` 读不出 ⇒ 键缺席、会话视图照常 200)。放行的最坏后果 = 一次「你的输入因崩溃丢了」的告知被推迟,或会话视图上这一格读作零条;不阻断任何 run、不改任何回执。反方向(读失败就拒跑 run / 整只会话视图 500)是拿一个告知面的抖动去打掉主路径,方向更坏。";
327
343
  };
328
344
  readonly "server.user-input-ledger.corrupt-record-skipped": {
329
345
  readonly cls: "F";
330
- readonly note: "S-928(`plugins/user-input-ledger-file.ts`,local 车道 File 孪生的 boot 重放;登记名 `user-input-ledger`):账本中间一行读不出形(`42` / 缺键 / 词表外的 `kind`·`input`·`how`·`cause` / 半截 JSON;撕尾不走本 tag)⇒ 跳过它继续。最坏后果按行种:丢 `accepted` ⇒ 那次受理若真随崩溃丢了,不会被告知(= 本账之前的状态);丢 `settled` ⇒ 一次正常收场的受理被当成崩溃丢失告知一次;丢 `converged` ⇒ 下一次 boot 重收一次(幂等);丢 `announced` ⇒ 再告知一次。全在告知面,不开门 ⇒ F。修前不存在(本账本 S-928 新立)。";
346
+ readonly note: "S-928(`plugins/user-input-ledger-file.ts`,local 车道 File 孪生的 boot 重放;登记名 `user-input-ledger`):账本中间一行读不出形(`42` / 缺键 / 词表外的 `kind`·`input`·`how`·`cause` / 半截 JSON;撕尾不走本 tag)⇒ 跳过它继续。S-969 L6 起 SQL 孪生(`plugins/user-input-ledger-sql.ts` `readRows`)同一只 tag:三只读口取到的行上读不出的列(`input_kind` 词表外 —— 新版本写的输入族 / 手改;收敛行缺 `state_at_ms`)⇒ 逐行跳过、其余照交,`listOpen` 的续翻游标按原始行推进(修前整页抛:收敛断点不前进、同会话通告与读面一并失效);坏行留在表里 ⇒ 每读一次计一次,非零 = 表里有本版本读不懂的行。最坏后果按行种:丢 `accepted` ⇒ 那次受理若真随崩溃丢了,不会被告知(= 本账之前的状态);丢 `settled` ⇒ 该受理在账上仍 open,下一趟收敛按 run 行判(S-969 L8,两车道同一只判词):本进程写的 / run 行已正常收场、已 park、易手 ⇒ 补结清、不告知;只有 run 行仍 `running` 且持有者是写者(local)、带死亡落款且持有者是写者、或行不在(local:File 店藏了读不出的行)⇒ 当成崩溃丢失告知一次;丢 `converged` ⇒ 下一次 boot 重收一次(幂等);丢 `announced` ⇒ 再告知一次;丢 `erased`(S-965,7.111.0 起的会话级墓碑;缺 `sessionId` 的那一行**不**读成「擦全部」)⇒ 被删会话的受理行在本进程的折叠里复现 —— 只有身份 + 时刻(无用户文本),读面 / 通告按 sessionId 取,同一个 id 被重新登记时新住客可见上一位的 `crashed` 告知(= 7.110.x 无级联时的状态);撕尾的 `erased` 不在此列:墓碑写在会话本体删除**之前**,撕掉 ⇒ DELETE 没有成功、会话还在,幂等重试重写。全在告知面,不开门 ⇒ F。修前不存在(本账本 S-928 新立)。";
331
347
  };
332
348
  readonly "server.outbound.connect-observer-threw": {
333
349
  readonly cls: "F";
@@ -260,7 +260,8 @@ export const FAIL_OPEN_TAGS = {
260
260
  "server.run-terminal.artifact-capped": { cls: "F", note: "S-107:leader_run 行上的工件字节(salvaged[].patch / candidatePatch.patch / merge.candidatePatch / merge.rej)长度 > ARTIFACT_INPUT_CAP(256KB)⇒ 整份换 «redacted:oversized-artifact»(fail-closed 取证丢弃;原字节仍在 worker 分支/merge 咽喉);计数=多少份工件被整份丢弃" },
261
261
  "server.run-terminal.diag-capped": { cls: "F", note: "S-107:诊断文本座(merge details/measure/grader trace、leader 日志座、planner 样本、journal 预览、workflow_run error cut)长度 > DIAG_INPUT_CAP(256KB)⇒ 整段换 «redacted:oversized-diagnostic» [len=N](fail-closed 取证丢弃;固定窗不 fail-closed 故不开窗);计数=多少段诊断文本被整段丢弃" },
262
262
  "server.run-terminal.output-truncated": { cls: "F", note: "S-107:持久 error 脱后 UTF-8 字节 > 65,000(TiDB TEXT 65,535 字节)⇒ 脱后切并带标记(切在占位里不是泄漏);计数=多少条终态文案被字节封顶截断" },
263
- "server.runs.terminal-ledger-write-failed": { cls: "F", note: "S-107(全量门 silent-degradation 棘轮抓出的五处静默 `catch {}`):run 终局链里**账本腿**(flush 缓冲 text/reasoning、append `failed` 行)与终态翻转(setTerminal)已不共命运——账本 INSERT 抛不再让 run 卡 running 到 reaper;但那条抛掉的账本行之前是**静默**吞的。detail `arm=` 四臂(flush-clean-cancel / append-clean-cancel / flush-catch / append-thrown-cancel / append-failed):缺的是 run_events 尾巴(缓冲文本或 failed 事件),run 行本身仍是 durable truth,读面/reaper 容忍缺失,故 F 类;计数=多少次终局账本写失败。" },
263
+ "server.runs.abort-cause-missing": { cls: "F", note: "S-947 候裁 ⑤(主会话裁:默认值保留,miss 臂必须响):续跑腿 `http/resume-legs.ts` 的 `checkpoint.resume_aborted` 臂 —— core 只在本腿递进去的信号**已中止**时铸该码,而那只信号唯一的写者是取消动词(快路 / 耐久旗轮询,句柄登记 `cancel_verb`),所以**只有 core 契约外的形**(桩 / 非 core Runner 直接抛该码,本腿句柄从未中止)才到得了「句柄上读不到成因」这一臂。取消终局照铸,成因取本腿唯一的中止词 `cancel_verb`(取消终局恒带成因由铸点签名保证,不让它缺);detail `task=<taskId>`;计数 = 多少次取消成因是默认值而不是句柄登记的。生产上恒零是证据,不归零 = 有一条不走 core 契约的 Runner 在跑续跑腿。" },
264
+ "server.runs.terminal-ledger-write-failed": { cls: "F", note: "S-107(全量门 silent-degradation 棘轮抓出的五处静默 `catch {}`):run 终局链里**账本腿**(flush 缓冲 text/reasoning、append `failed` 行)与终态翻转(setTerminal)已不共命运——账本 INSERT 抛不再让 run 卡 running 到 reaper;但那条抛掉的账本行之前是**静默**吞的。detail `arm=` 四臂(flush-clean-cancel / append-clean-cancel / flush-catch / append-thrown-cancel / append-failed);S-947 起续跑腿(`http/resume-legs.ts`)两座同 tag:resume-append-resume-aborted / resume-append-thrown-cancel(取消抛出两臂的 `failed` 行,修前那两臂根本不落账本终局行):缺的是 run_events 尾巴(缓冲文本或 failed 事件),run 行本身仍是 durable truth,读面/reaper 容忍缺失,故 F 类;计数=多少次终局账本写失败。" },
264
265
  "server.redact.ppk-eat-to-end": { cls: "F", note: "S-107 第三十二轮重扫:PuTTY .ppk 容器扫描器(redact.ts redactPpkContainers)在「无合格终止 / 收口后同容器范围再见私钥字段 / 收口后到下一版本头之间夹非空白」三形下从头**吃到输入末尾**(fail-closed 取证丢弃)——终态 errorMessage 座上=整条错误文案变成 «redacted:private-key»,与 redactions_applied_total{private-key} 的诚实替换同形不可分,故要有自己的计数/探针;影子扫不计;计数=多少次整段吃尾。" },
265
266
  "server.approval-card.closed-word-out-of-set": {
266
267
  cls: "F",
@@ -290,17 +291,29 @@ export const FAIL_OPEN_TAGS = {
290
291
  cls: "F",
291
292
  note: "S-582(`plugins/send-file-ledger.ts`,local 车道的发文件账本):一条账行读不出形(`42` / 缺 id·scope·objectKey 三元组 / 中间半截 JSON)⇒ 跳过它继续。最坏后果 = 那条已签发的链接在它租户的列表里**看不见**(对象本身没动;账本本来就不存 URL)。治理面少一行是可见性损失,不是放行 ⇒ F;但它恰是治理面要看的那一类事实,所以必须留痕 —— 修前零披露。",
292
293
  },
294
+ "server.approval-ask-audit.corrupt-record-skipped": {
295
+ cls: "F",
296
+ note: "S-967(`approval-ask-audit-store.ts`,local 车道 File ask 审计店的 boot 重放;登记名 `approval-ask-audit`):账本**中间**一行读不出形(`42` / 数组 / 缺键 / 词表外 `kind`·`outcome`·`orphanState` / 半截 JSON;撕尾不走本 tag)⇒ 跳过它继续。最坏后果按行种:丢 `ask` ⇒ 那只孤儿在崩溃收敛读面缺席(无身份可列);丢 `settle` ⇒ 该组按 pending 臂收(审批门在执行之前 ⇒ 若这条 settle 其实是 `allowed` 且工具已执行,`resumeSafe:true` 是错的 —— 与头注「双故障窗」同族);丢 `leg_closed` ⇒ decided 臂多收一行(保守方向);丢 `crash_converged` ⇒ 下一次 boot 重收(幂等);丢 `interrupted_by_shutdown` / `late_settle` ⇒ 成因注退回 `crashed_before_park` / 少一行取证。审计店不开门、不改任何审批结算 ⇒ F。修前:重放坏行只计入 `convergeCrashedAsks().corruptLines`(boot 日志一个数),**无逐条披露、无 tag**。",
297
+ },
298
+ "server.workflow-notify-journal.corrupt-record-skipped": {
299
+ cls: "F",
300
+ note: "S-977(`orchestration/workflow-notify-journal.ts`,local 车道 File notify journal 的 boot 重放;登记名 `workflow-notify-journal`):账本**中间**一行读不出形(`42` / 数组 / 缺 runId·scope / `acked` 非布尔 / 半截 JSON;撕尾不走本 tag)⇒ 跳过它继续。最坏后果按行种:丢 `pending` 行(`record` 写下的那条)⇒ 那条 run 不在恢复积压里,崩溃丢掉的终态通告 boot 恢复扫描不再重导(live 进程内通告与 `WorkflowStatus` 轮询地板不受影响);丢 `acked` 行 ⇒ 该 run 读回 pending(或前一份已知良好的条目),多一次幂等重投(接收方按 runId 去重)。账本不开门、不改任何 run 的执行或授权 ⇒ F。修前:坏行 / 缺键行**静默**(解析失败 `continue`、只判 `typeof runId === \"string\"`)。",
301
+ },
302
+ "server.workflow-agent-session-index.corrupt-record-skipped": {
303
+ cls: "F",
304
+ note: "S-977(`orchestration/workflow-agent-session-index.ts`,local 车道 workflow 出身 park 的 join 索引账本的 boot 重放;登记名 `workflow-agent-session-index`):账本**中间**一行读不出形(`42` / 数组 / 缺 `runId`·`rows` / 任一行成员缺身份位 / 半截 JSON;撕尾不走本 tag)⇒ **整行**跳过,那条 run 保留上一份已知良好的快照(不做「丢坏成员、留好成员」—— 那会铸出比真相少几条 park 的行集)。最坏后果 = 该 run 的 park 在索引里缺行 ⇒ `/decide` 找不到它、退回 legacy 腿(409 `conflict.resume_context_unavailable`):诚实 miss,不是错配 —— 赎回键由记录上的 `parks[]` 逐字比对,不读本索引。索引不开门 ⇒ F。修前:形不对的整行响亮(`workflow_agent_session_ledger_line_rejected`,同事件名保留)但**无 tag**,带换行的半截 JSON 静默 `continue`。",
305
+ },
293
306
  "server.user-input-ledger.append-failed": {
294
307
  cls: "F",
295
- note: "S-928(`src/user-input-ledger.ts`):200 承诺**之后**的一次账本写失败 —— detail 的 `op=` 点名是哪一种:`settle`(腿在本进程收场)/ `withdraw`(引擎没收下这次 steer)/ `converge`(收敛器判孤儿)/ `announce`(通告已投出,记「告知过」)。放行的最坏后果逐个说清:settle / withdraw 漏写 ⇒ 这次受理在账上仍是 open —— 进程之后**若再崩**(local 车道下一次 boot),它会被当成崩溃丢失告知一次(一句多余的「你的 steer 丢了」,而它其实被正常收场了;SQL 车道的收敛判据只认 run 店已落盘的 reap 判决 —— `failed` + `RUN_STALLED_ERROR` ⇒ crashed、仍 `running` ⇒ live、正常翻面 ⇒ leg_over 补结清 —— 正常收场的 run 不会被误收);converge 漏写 ⇒ 留在 open,下一次扫描再判(延迟,不丢);announce 漏写 ⇒ 下一条 run 再说一遍(至多一次退化成至少一次)。都是**告知面**的偏差,不开任何门、不改任何 run 的执行 ⇒ F。计数非零 = 这台机器的账本写不动了(local:数据根盘满 / 只读;SQL:库不可达),steer 此刻多半也在回 503。",
308
+ note: "S-928(`src/user-input-ledger.ts`):200 承诺**之后**的一次账本写失败 —— detail 的 `op=` 点名是哪一种:`settle`(腿在本进程收场)/ `withdraw`(引擎没收下这次 steer)/ `converge`(收敛器判孤儿)/ `announce`(通告已投出,记「告知过」)。放行的最坏后果逐个说清:settle / withdraw 漏写 ⇒ 这次受理在账上仍是 open —— 本进程下一次收敛扫描补结清(本进程写的、已不在活腿上 ⇒ leg_over);进程在那之前就崩了 ⇒ 下一次 boot 的收敛读 run 店判(S-969 L8 起两车道同一只判词:承载它的 run 正常翻面 / 已 park ⇒ leg_over 补结清、不告知;落款死亡判决或写者已死仍 running ⇒ crashed)—— 仍会多说一句的只剩两形:withdraw 漏写后那条腿崩了(一次没变成 200 的受理被告知一次);settle 漏写后本进程续跑了同一条 run、续跑腿崩了(run 行说的是同一 taskId 上**最新**那条腿)。方向都是多说,不漏;converge 漏写 ⇒ 留在 open,下一次扫描再判(延迟,不丢);announce 漏写 ⇒ 下一条 run 再说一遍(至多一次退化成至少一次)。都是**告知面**的偏差,不开任何门、不改任何 run 的执行 ⇒ F。计数非零 = 这台机器的账本写不动了(local:数据根盘满 / 只读;SQL:库不可达),steer 此刻多半也在回 503。",
296
309
  },
297
310
  "server.user-input-ledger.read-failed": {
298
311
  cls: "F",
299
- note: "S-928(`src/user-input-ledger.ts` / `http/routes/sessions.ts`):一次账本**读**失败 —— `op=converge`(收敛扫描读不出 open 受理 ⇒ 本轮不收敛,下一个 tick 再来)/ `op=judge`(SQL 臂判词要读 run 店,run 店读失败 ⇒ 该行留在 open、下一次扫描再判)/ `op=announce`(新 run 首拍读不出本会话的待告知受理 ⇒ 这一拍不告知,run 照跑,下一条 run 再试)/ `op=session-view`(`GET /v1/sessions/:id` 的 additive 键 `userInputUndrained` 读不出 ⇒ 键缺席、会话视图照常 200)。放行的最坏后果 = 一次「你的输入因崩溃丢了」的告知被推迟,或会话视图上这一格读作零条;不阻断任何 run、不改任何回执。反方向(读失败就拒跑 run / 整只会话视图 500)是拿一个告知面的抖动去打掉主路径,方向更坏。",
312
+ note: "S-928(`src/user-input-ledger.ts` / `http/routes/sessions.ts`):一次账本**读**失败 —— `op=converge`(收敛扫描读不出 open 受理 ⇒ 本轮不收敛,下一个 tick 再来)/ `op=judge`(判词要读 run 店 —— S-969 L8 起两车道都读;一趟内同一 taskId 只读一次,读失败 ⇒ 同 run 的每一行各记一笔、留在 open、下一次扫描再判)/ `op=announce`(新 run 首拍读不出本会话的待告知受理 ⇒ 这一拍不告知,run 照跑,下一条 run 再试)/ `op=session-view`(`GET /v1/sessions/:id` 的 additive 键 `userInputUndrained` 读不出 ⇒ 键缺席、会话视图照常 200)。放行的最坏后果 = 一次「你的输入因崩溃丢了」的告知被推迟,或会话视图上这一格读作零条;不阻断任何 run、不改任何回执。反方向(读失败就拒跑 run / 整只会话视图 500)是拿一个告知面的抖动去打掉主路径,方向更坏。",
300
313
  },
301
314
  "server.user-input-ledger.corrupt-record-skipped": {
302
315
  cls: "F",
303
- note: "S-928(`plugins/user-input-ledger-file.ts`,local 车道 File 孪生的 boot 重放;登记名 `user-input-ledger`):账本中间一行读不出形(`42` / 缺键 / 词表外的 `kind`·`input`·`how`·`cause` / 半截 JSON;撕尾不走本 tag)⇒ 跳过它继续。最坏后果按行种:丢 `accepted` ⇒ 那次受理若真随崩溃丢了,不会被告知(= 本账之前的状态);丢 `settled` ⇒ 一次正常收场的受理被当成崩溃丢失告知一次;丢 `converged` ⇒ 下一次 boot 重收一次(幂等);丢 `announced` ⇒ 再告知一次。全在告知面,不开门 ⇒ F。修前不存在(本账本 S-928 新立)。",
316
+ note: "S-928(`plugins/user-input-ledger-file.ts`,local 车道 File 孪生的 boot 重放;登记名 `user-input-ledger`):账本中间一行读不出形(`42` / 缺键 / 词表外的 `kind`·`input`·`how`·`cause` / 半截 JSON;撕尾不走本 tag)⇒ 跳过它继续。S-969 L6 起 SQL 孪生(`plugins/user-input-ledger-sql.ts` `readRows`)同一只 tag:三只读口取到的行上读不出的列(`input_kind` 词表外 —— 新版本写的输入族 / 手改;收敛行缺 `state_at_ms`)⇒ 逐行跳过、其余照交,`listOpen` 的续翻游标按原始行推进(修前整页抛:收敛断点不前进、同会话通告与读面一并失效);坏行留在表里 ⇒ 每读一次计一次,非零 = 表里有本版本读不懂的行。最坏后果按行种:丢 `accepted` ⇒ 那次受理若真随崩溃丢了,不会被告知(= 本账之前的状态);丢 `settled` ⇒ 该受理在账上仍 open,下一趟收敛按 run 行判(S-969 L8,两车道同一只判词):本进程写的 / run 行已正常收场、已 park、易手 ⇒ 补结清、不告知;只有 run 行仍 `running` 且持有者是写者(local)、带死亡落款且持有者是写者、或行不在(local:File 店藏了读不出的行)⇒ 当成崩溃丢失告知一次;丢 `converged` ⇒ 下一次 boot 重收一次(幂等);丢 `announced` ⇒ 再告知一次;丢 `erased`(S-965,7.111.0 起的会话级墓碑;缺 `sessionId` 的那一行**不**读成「擦全部」)⇒ 被删会话的受理行在本进程的折叠里复现 —— 只有身份 + 时刻(无用户文本),读面 / 通告按 sessionId 取,同一个 id 被重新登记时新住客可见上一位的 `crashed` 告知(= 7.110.x 无级联时的状态);撕尾的 `erased` 不在此列:墓碑写在会话本体删除**之前**,撕掉 ⇒ DELETE 没有成功、会话还在,幂等重试重写。全在告知面,不开门 ⇒ F。修前不存在(本账本 S-928 新立)。",
304
317
  },
305
318
  "server.outbound.connect-observer-threw": {
306
319
  cls: "F",