@sema-agent/server 7.109.0 → 7.111.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 (107) hide show
  1. package/MIGRATION.md +25 -0
  2. package/README.md +8 -0
  3. package/README.zh-CN.md +5 -0
  4. package/USAGE.md +2 -2
  5. package/dist/approval-ask-audit-store.d.ts +38 -25
  6. package/dist/approval-ask-audit-store.js +26 -59
  7. package/dist/approval-card.d.ts +6 -5
  8. package/dist/approval-card.js +8 -11
  9. package/dist/bench/s1/run-firm.d.ts +11 -0
  10. package/dist/bench/s1/run-firm.js +47 -32
  11. package/dist/boot/coordinators.js +1 -1
  12. package/dist/boot/reapers.d.ts +3 -0
  13. package/dist/boot/reapers.js +3 -2
  14. package/dist/boot/resolve-spec.js +3 -4
  15. package/dist/boot/session-faces.d.ts +4 -0
  16. package/dist/boot/session-faces.js +3 -1
  17. package/dist/boot/stage-07-capability-layer.d.ts +2 -0
  18. package/dist/boot/stage-07-capability-layer.js +18 -3
  19. package/dist/boot/stage-08-reapers.d.ts +2 -0
  20. package/dist/boot/stage-08-reapers.js +2 -2
  21. package/dist/boot/stage-09-leader.d.ts +2 -0
  22. package/dist/boot/stage-09-leader.js +2 -1
  23. package/dist/boot/stage-10-http-server.d.ts +2 -0
  24. package/dist/boot/stage-10-http-server.js +2 -1
  25. package/dist/boot/workflow-orchestration.js +1 -1
  26. package/dist/config-catalog.js +1 -1
  27. package/dist/config-types.d.ts +3 -2
  28. package/dist/config.js +19 -7
  29. package/dist/deployment-governance.d.ts +8 -2
  30. package/dist/deployment-governance.js +9 -8
  31. package/dist/hooks/hook-runner.d.ts +2 -2
  32. package/dist/hooks/hook-runner.js +40 -26
  33. package/dist/http/durable-run-leg.js +1 -1
  34. package/dist/http/resume-legs.js +12 -4
  35. package/dist/http/routes/runs.d.ts +0 -1
  36. package/dist/http/routes/runs.js +36 -1
  37. package/dist/http/routes/sessions.js +12 -2
  38. package/dist/http/routes/tasks.js +13 -7
  39. package/dist/http/server.d.ts +5 -0
  40. package/dist/index.d.ts +2 -0
  41. package/dist/index.js +1 -0
  42. package/dist/instruction-selection.d.ts +34 -0
  43. package/dist/instruction-selection.js +23 -0
  44. package/dist/instruction-source-notice.d.ts +3 -3
  45. package/dist/memory-layer-legacy-lock.d.ts +3 -3
  46. package/dist/model-entry-headers.d.ts +5 -2
  47. package/dist/model-entry-headers.js +4 -4
  48. package/dist/model-unreachable-notice.d.ts +2 -2
  49. package/dist/observability/corrupt-read-seat.d.ts +24 -0
  50. package/dist/observability/corrupt-read-seat.js +24 -0
  51. package/dist/observability/fail-open.d.ts +25 -1
  52. package/dist/observability/fail-open.js +25 -1
  53. package/dist/observability/metrics.js +2 -1
  54. package/dist/observability/run-terminal.js +10 -0
  55. package/dist/operator-ask.d.ts +9 -8
  56. package/dist/operator-ask.js +9 -1
  57. package/dist/orchestration/workflow-agent-session-index.d.ts +12 -23
  58. package/dist/orchestration/workflow-agent-session-index.js +11 -50
  59. package/dist/orchestration/workflow-notify-journal.d.ts +11 -8
  60. package/dist/orchestration/workflow-notify-journal.js +20 -36
  61. package/dist/plugins/file-outcome-sink.d.ts +4 -1
  62. package/dist/plugins/file-outcome-sink.js +9 -3
  63. package/dist/plugins/file-run-store.js +4 -4
  64. package/dist/plugins/memory-run-store.d.ts +1 -1
  65. package/dist/plugins/memory-run-store.js +2 -1
  66. package/dist/plugins/pg-pool.js +15 -0
  67. package/dist/plugins/retention-store-sql.d.ts +1 -1
  68. package/dist/plugins/retention-store-sql.js +2 -0
  69. package/dist/plugins/run-store-sql.d.ts +1 -1
  70. package/dist/plugins/run-store-sql.js +3 -2
  71. package/dist/plugins/schema-indexes.js +2 -0
  72. package/dist/plugins/store-backend.d.ts +4 -0
  73. package/dist/plugins/store-backend.js +9 -0
  74. package/dist/plugins/store-contracts.d.ts +24 -0
  75. package/dist/plugins/store-contracts.js +10 -0
  76. package/dist/plugins/tidb-pool.js +17 -0
  77. package/dist/plugins/user-input-ledger-file.d.ts +35 -0
  78. package/dist/plugins/user-input-ledger-file.js +108 -0
  79. package/dist/plugins/user-input-ledger-sql.d.ts +54 -0
  80. package/dist/plugins/user-input-ledger-sql.js +129 -0
  81. package/dist/project-memory.d.ts +10 -2
  82. package/dist/project-memory.js +5 -2
  83. package/dist/run-local.d.ts +18 -5
  84. package/dist/run-local.js +24 -9
  85. package/dist/runs.d.ts +8 -1
  86. package/dist/runs.js +11 -5
  87. package/dist/runtime-governance.d.ts +20 -8
  88. package/dist/runtime-governance.js +7 -12
  89. package/dist/session-records-skipped-notice.d.ts +4 -3
  90. package/dist/task-settings.d.ts +0 -6
  91. package/dist/task-settings.js +0 -1
  92. package/dist/tool-approval.d.ts +8 -1
  93. package/dist/tool-approval.js +4 -1
  94. package/dist/trace/core-keyset-guard.d.ts +16 -1
  95. package/dist/trace/engine-notice-wire.d.ts +30 -35
  96. package/dist/trace/engine-notice-wire.js +23 -23
  97. package/dist/trace/ledger-sink.d.ts +4 -0
  98. package/dist/trace/ledger-sink.js +1 -1
  99. package/dist/trace/project.d.ts +11 -3
  100. package/dist/trace/project.js +6 -5
  101. package/dist/trace/self-minted-wire.d.ts +40 -0
  102. package/dist/trace/self-minted-wire.js +15 -0
  103. package/dist/trace/wire-projection-faces.d.ts +3 -3
  104. package/dist/trace/wire-projection-faces.js +4 -2
  105. package/dist/user-input-ledger.d.ts +349 -0
  106. package/dist/user-input-ledger.js +354 -0
  107. package/package.json +3 -3
@@ -20,6 +20,7 @@ import { buildCheckpointAnchorHash } from "../approval-ask-audit-store.js";
20
20
  import { TERMINAL_PARK_GATE_KINDS } from "../plugins/checkpoint-store-sql.js";
21
21
  import { withPrincipal } from "../observability/principal-context.js";
22
22
  import { withModelReachScope } from "../model-reach-scope.js";
23
+ import { createInstructionSelectionLeg } from "../instruction-selection.js";
23
24
  import { modelTextEventData, turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, toolRosterDeltaEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "../trace/project.js";
24
25
  import { redactSecrets } from "../trace/redact.js";
25
26
  import { createLedgerSink, createSerialLedgerAppend } from "../trace/ledger-sink.js";
@@ -503,7 +504,8 @@ export function createResumeLegs(ctx) {
503
504
  await args.onResumeCommitted();
504
505
  let unregisterEngineNotice;
505
506
  try {
506
- result = await withModelReachScope(sessionId, () => withPrincipal(principal, async () => {
507
+ const instructionLeg = createInstructionSelectionLeg();
508
+ result = await instructionLeg.run(sessionId, () => withModelReachScope(sessionId, () => withPrincipal(principal, async () => {
507
509
  const resumeNotifiedKeys = new NotifiedKeys();
508
510
  if (deps.runStore && taskId) {
509
511
  const rs0 = deps.runStore;
@@ -526,6 +528,7 @@ export function createResumeLegs(ctx) {
526
528
  persistThinking: deps.config.traceThinking,
527
529
  notifiedKeys: resumeNotifiedKeys,
528
530
  ...(taskId ? { onActivity: () => recordTurnActivity(taskId) } : {}),
531
+ instructionsSegment: instructionLeg.segment,
529
532
  });
530
533
  const vAppend = (type, data) => verifySink.append(type, data);
531
534
  const verifyTailContentMode = resumeTaskConfig.forwardSubagentEvents === true ? "on" : "progress_only";
@@ -622,11 +625,14 @@ export function createResumeLegs(ctx) {
622
625
  unregisterEngineNotice = registerEngineNoticeLeg({
623
626
  sessionId,
624
627
  ...(args.engineNotices !== undefined && args.engineNotices.length > 0 ? { pending: [...args.engineNotices] } : {}),
625
- durable: (row) => void append("engine_notice", row).catch((err) => {
628
+ durable: (row) => append("engine_notice", row).then(() => true, (err) => {
626
629
  recordFailOpen("server.engine-notice.durable-append-failed", `leg=resume code=${row.code} task=${taskId ?? ""}`);
627
630
  deps.logger?.warn?.("engine_notice_append_failed", { taskId, noticeCode: row.code, sessionId: row.sessionId, err: err instanceof Error ? err.message : String(err) });
631
+ return false;
628
632
  }),
629
633
  });
634
+ if (deps.userInputLedger && taskId)
635
+ await deps.userInputLedger.announceUndrained({ sessionId, taskId });
630
636
  const anchorOwner = principal ?? null;
631
637
  const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
632
638
  const captureTurnAnchor = deps.resumeAnchorStore && getLeafId
@@ -768,7 +774,7 @@ export function createResumeLegs(ctx) {
768
774
  break;
769
775
  case "wiring_manifest":
770
776
  await flush();
771
- await append("wiring_manifest", wiringManifestEventData(ev));
777
+ await append("wiring_manifest", wiringManifestEventData(ev, instructionLeg.segment()));
772
778
  break;
773
779
  case "tool_roster_delta": {
774
780
  await flush();
@@ -872,7 +878,7 @@ export function createResumeLegs(ctx) {
872
878
  return await (deps.sendUserFile && taskId
873
879
  ? deps.sendUserFile.runWithContext({ taskId, emit: async (frame) => { const { type, ...rest } = frame; await append(type, rest); } }, withElicitation)
874
880
  : withElicitation());
875
- }));
881
+ })));
876
882
  }
877
883
  catch (e) {
878
884
  if (e instanceof CheckpointError) {
@@ -964,6 +970,8 @@ export function createResumeLegs(ctx) {
964
970
  finally {
965
971
  unregisterEngineNotice?.();
966
972
  if (taskId) {
973
+ if (resumeStreamRef !== undefined)
974
+ await deps.userInputLedger?.settleLeg(resumeStreamRef);
967
975
  if (inflightRuns.get(taskId) === cancelCtrl)
968
976
  inflightRuns.delete(taskId);
969
977
  if (preemptableRuns.get(taskId) === preemptCtrl)
@@ -6,7 +6,6 @@ import type { RouteCtx, PreparedTaskSubmission, RouteMatch, RouteIdsOf } from ".
6
6
  * —— 导出的是**行为**而不是 schema 对象:schema 的推断类型会牵出 registry-core 内嵌的 zod 路径
7
7
  * (TS2742,不可移植),而这道门要钉的本来就是行为。非 wire 面。 */
8
8
  export declare function projectDecisionActor(raw: unknown): ActorAssertionWire | undefined;
9
- /** 子代理 / 任务句柄 / HITL 应答段——`handle()` 里它在 SVC-5 workflow steer **之后**,故是独立的域入口。 */
10
9
  /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
11
10
  * = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
12
11
  export declare function handleRuns(req: IncomingMessage, res: ServerResponse, match: RouteMatch<RunsRouteId>, ctx: RouteCtx): Promise<void>;
@@ -134,6 +134,18 @@ async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
134
134
  staleFrame: () => ({ event: "failed", data: { type: "failed", errorMessage: "run stalled (instance lost?)" } }),
135
135
  }, taskId, staleMs);
136
136
  }
137
+ async function acceptLiveUserInput(deps, res, at) {
138
+ if (!deps.userInputLedger)
139
+ return undefined;
140
+ try {
141
+ return await deps.userInputLedger.accept({ input: "steer", messageId: at.messageId, sessionId: at.sessionId, taskId: at.taskId, leg: at.leg });
142
+ }
143
+ catch (e) {
144
+ deps.logger?.warn?.("user_input_ledger_accept_failed", { taskId: at.taskId, sessionId: at.sessionId, err: e instanceof Error ? e.message : String(e) });
145
+ sendError(res, 503, "steering.ledger_unavailable", "this steer was NOT delivered: the server could not record it in its user-input ledger (an acceptance receipt is only given once the acceptance is durable, so a crash before the run consumes it can be announced) — nothing reached the engine; retry later");
146
+ return "refused";
147
+ }
148
+ }
137
149
  export async function handleRuns(req, res, match, ctx) {
138
150
  const { deps } = ctx;
139
151
  const { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints } = ctx.registry;
@@ -468,12 +480,17 @@ export async function handleRuns(req, res, match, ctx) {
468
480
  const sendNotRunning = (error) => sendError(res, 409, "steering.not_running", error);
469
481
  const live = steerableRuns.get(taskId);
470
482
  if (live) {
483
+ const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId, leg: live });
484
+ if (acceptance === "refused")
485
+ return;
471
486
  try {
472
487
  await live.steer(text, { trusted, ...(steerInputId !== undefined ? { inputId: steerInputId } : {}) });
473
488
  sendJson(res, 200, { taskId, status: "running", delivery: "applied", messageId, ...(priority ? { priority } : {}) });
474
489
  return;
475
490
  }
476
491
  catch (e) {
492
+ if (acceptance !== undefined)
493
+ await deps.userInputLedger.withdraw(acceptance);
477
494
  const code = e.code;
478
495
  if (code === "steering.invalid_content") {
479
496
  sendError(res, 422, "steering.invalid_content", e instanceof Error ? e.message : "invalid steering content");
@@ -776,10 +793,15 @@ export async function handleRuns(req, res, match, ctx) {
776
793
  });
777
794
  return;
778
795
  }
796
+ const acceptance = await acceptLiveUserInput(deps, res, { messageId: interruptMessageId, sessionId: run.sessionId, taskId, leg: liveStream });
797
+ if (acceptance === "refused")
798
+ return;
779
799
  try {
780
800
  await liveStream.steer(text, { trusted, priority: "now", ...(interruptInputId !== undefined ? { inputId: interruptInputId } : {}) });
781
801
  }
782
802
  catch (e) {
803
+ if (acceptance !== undefined)
804
+ await deps.userInputLedger.withdraw(acceptance);
783
805
  const code = e.code;
784
806
  if (code === "steering.invalid_content") {
785
807
  sendError(res, 422, "steering.invalid_content", e instanceof Error ? e.message : "invalid steering content");
@@ -1133,6 +1155,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1133
1155
  }
1134
1156
  const handle = resolution?.handle;
1135
1157
  if (handle) {
1158
+ let acceptance;
1136
1159
  try {
1137
1160
  if (verb === "resume") {
1138
1161
  if (!handle.resume) {
@@ -1148,11 +1171,23 @@ export async function handleRunVerbs(req, res, match, ctx) {
1148
1171
  sendJson(res, 200, { taskId: runId, target, status: "running", delivery: "applied", marker, note: `Message queued for delivery to ${handle.agentName ?? target}; it will continue in the background.` });
1149
1172
  return;
1150
1173
  }
1174
+ const messageId = uuidv7();
1175
+ const accepted = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId: runId, leg: handle });
1176
+ if (accepted === "refused")
1177
+ return;
1178
+ acceptance = accepted;
1151
1179
  const marker = await handle.steer(redacted);
1152
- sendJson(res, 200, { taskId: runId, target, status: "running", delivery: "applied", marker, note: `Message queued for delivery to ${handle.agentName ?? target} at its next tool round.` });
1180
+ if (acceptance !== undefined) {
1181
+ const ledger = deps.userInputLedger;
1182
+ const settle = () => ledger.settleLeg(handle);
1183
+ void handle.settled.then(settle, settle);
1184
+ }
1185
+ sendJson(res, 200, { taskId: runId, target, status: "running", delivery: "applied", messageId, marker, note: `Message queued for delivery to ${handle.agentName ?? target} at its next tool round.` });
1153
1186
  return;
1154
1187
  }
1155
1188
  catch (e) {
1189
+ if (acceptance !== undefined)
1190
+ await deps.userInputLedger.withdraw(acceptance);
1156
1191
  const code = e.code;
1157
1192
  if (code === "steering.not_running") {
1158
1193
  send409(code, "sub-agent just finished — no longer accepting steers");
@@ -618,8 +618,18 @@ export async function handleSessions(req, res, match, ctx) {
618
618
  const tail = qi("tail");
619
619
  const before = qi("before");
620
620
  const blobCap = qi("blobCap");
621
+ let undrained;
622
+ if (deps.userInputLedger) {
623
+ try {
624
+ undrained = await deps.userInputLedger.readSession(match.params.sessionId);
625
+ }
626
+ catch (e) {
627
+ recordFailOpen("server.user-input-ledger.read-failed", `op=session-view ${e instanceof Error ? e.message : String(e)}`);
628
+ }
629
+ }
630
+ const undrainedKey = undrained !== undefined ? { userInputUndrained: undrained } : {};
621
631
  if (tail === undefined && before === undefined && blobCap === undefined) {
622
- sendJson(res, 200, audit);
632
+ sendJson(res, 200, { ...audit, ...undrainedKey });
623
633
  return;
624
634
  }
625
635
  let messages = audit.messages;
@@ -631,7 +641,7 @@ export async function handleSessions(req, res, match, ctx) {
631
641
  }
632
642
  if (blobCap !== undefined && blobCap > 0)
633
643
  messages = truncateMessageBlobs(messages, blobCap);
634
- sendJson(res, 200, { ...audit, messages, ...(window ? { window } : {}) });
644
+ sendJson(res, 200, { ...audit, messages, ...(window ? { window } : {}), ...undrainedKey });
635
645
  return;
636
646
  }
637
647
  }
@@ -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;
@@ -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) {
@@ -261,7 +263,7 @@ export async function handleTasks(req, res, match, ctx) {
261
263
  if (durableTaskId && deps.runStore) {
262
264
  const rs = deps.runStore;
263
265
  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) });
266
+ 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
267
  usageKey = tid;
266
268
  deps.modelUsage?.register(tid);
267
269
  const owner = principal ?? null;
@@ -333,10 +335,12 @@ export async function handleTasks(req, res, match, ctx) {
333
335
  },
334
336
  ...(noticeLedger
335
337
  ? {
336
- durable: (row) => void noticeLedger.append("engine_notice", row).catch(() => recordFailOpen("server.engine-notice.durable-append-failed", `leg=sync code=${row.code} task=${durableTaskId ?? ""}`)),
338
+ durable: (row) => noticeLedger.append("engine_notice", row).then(() => true, () => (recordFailOpen("server.engine-notice.durable-append-failed", `leg=sync code=${row.code} task=${durableTaskId ?? ""}`), false)),
337
339
  }
338
340
  : {}),
339
341
  });
342
+ if (deps.userInputLedger && durableTaskId && prepared.spec.sessionId)
343
+ await deps.userInputLedger.announceUndrained({ sessionId: prepared.spec.sessionId, taskId: durableTaskId });
340
344
  try {
341
345
  const streamBody = async () => {
342
346
  await withPrincipal(principal, async () => {
@@ -502,7 +506,7 @@ export async function handleTasks(req, res, match, ctx) {
502
506
  sseData(res, { type: "compaction_outcome", ...compactionOutcomeEventData(ev) });
503
507
  }
504
508
  else if (ev.type === "wiring_manifest") {
505
- const wiringFace = wiringManifestFaceEventData(ev, wiringOperatorFace);
509
+ const wiringFace = wiringManifestFaceEventData(ev, wiringOperatorFace, instructionLeg.segment());
506
510
  wiringReceipt ??= wiringFace;
507
511
  sseData(res, { type: "wiring_manifest", ...wiringFace });
508
512
  }
@@ -645,7 +649,7 @@ export async function handleTasks(req, res, match, ctx) {
645
649
  });
646
650
  }
647
651
  const withApproval = () => approvalCtx && deps.toolApproval ? deps.toolApproval.runWithContext(approvalCtx, withQuestion) : withQuestion();
648
- await withModelReachScope(prepared.spec.sessionId, () => deps.sendUserFile
652
+ await instructionLeg.run(prepared.spec.sessionId, () => withModelReachScope(prepared.spec.sessionId, () => deps.sendUserFile
649
653
  ? deps.sendUserFile.runWithContext({
650
654
  taskId: askTaskId,
651
655
  emit: async (f) => {
@@ -656,7 +660,7 @@ export async function handleTasks(req, res, match, ctx) {
656
660
  }
657
661
  },
658
662
  }, withApproval)
659
- : withApproval());
663
+ : withApproval()));
660
664
  if (durableTaskId && deps.runStore) {
661
665
  const finalPlane = finalResult ? outwardPlane(finalResult.terminal) : undefined;
662
666
  if (finalResult && finalPlane?.status === "suspended" && deps.checkpointStore)
@@ -700,6 +704,8 @@ export async function handleTasks(req, res, match, ctx) {
700
704
  clearInterval(durableHeartbeat);
701
705
  if (durableTaskId && steerableRuns.get(durableTaskId) === liveStreamRef)
702
706
  steerableRuns.delete(durableTaskId);
707
+ if (liveStreamRef !== undefined)
708
+ await deps.userInputLedger?.settleLeg(liveStreamRef);
703
709
  if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
704
710
  inflightRuns.delete(durableTaskId);
705
711
  cancelledViaVerb.delete(durableTaskId);
@@ -832,7 +838,7 @@ export async function handleTasks(req, res, match, ctx) {
832
838
  const observeSyncLegEvent = (ev) => {
833
839
  if (ev.type !== "wiring_manifest" || wiringReceipt !== undefined)
834
840
  return;
835
- wiringReceipt = wiringManifestFaceEventData(ev, wiringOperatorFace);
841
+ wiringReceipt = wiringManifestFaceEventData(ev, wiringOperatorFace, instructionLeg.segment());
836
842
  };
837
843
  let result;
838
844
  let cancelVerbLabel = false;
@@ -846,7 +852,7 @@ export async function handleTasks(req, res, match, ctx) {
846
852
  observeSyncLegEvent(ev);
847
853
  return stream.result();
848
854
  };
849
- result = await withPrincipal(principal, () => prepared.verify ? verifyLeg(prepared.verify) : prepared.cascade ? cascadeLeg() : plainLeg());
855
+ result = await instructionLeg.run(prepared.spec.sessionId, () => withPrincipal(principal, () => prepared.verify ? verifyLeg(prepared.verify) : prepared.cascade ? cascadeLeg() : plainLeg()));
850
856
  }
851
857
  finally {
852
858
  counters.uncountedBillableInflight--;
@@ -217,6 +217,11 @@ export interface ServiceStoreDeps {
217
217
  /** SendUserFile 账本(多租户治理面):scope↔object 映射的读面 GET /v1/sendfile-links。present ⇔ 签发面
218
218
  * 已配 AND 有 store backend(main.ts 建;写点在两条 send lane 的 withLedgerRecording)。 */
219
219
  sendFileLedger?: import("../plugins/send-file-ledger.js").SendFileLedger;
220
+ /** S-928 用户输入受理账本(「200 applied」受理的影子;单写者 = `src/user-input-ledger.ts` 的活对象,boot 装配在 stage-07 并跑一次收敛)。
221
+ * 在场 ⇔ 有 store backend(生产恒与 `runStore` 同源于 backend)。消费点:steer 的 live 腿 / interrupt{text} / 同步子代的 steer(S-966)受理前写 `accepted`(写不进 ⇒ 503
222
+ * `steering.ledger_unavailable`)、三条 run 腿开通告口那一拍铸 `crashed` 通告并在收场时 settle、`GET /v1/sessions/:id` 的
223
+ * `userInputUndrained` 读面。缺席只出现在不接 backend 的 stub-harness 上(那时没有这一面,行为与修前逐字相同)。 */
224
+ userInputLedger?: import("../user-input-ledger.js").UserInputLedger;
220
225
  /** 2c session-sync (P1d) — the WHOLE durable backend, so the `/v1/sessions/:id/sync/*` routes can fan out across
221
226
  * ALL four session subsystems (session entries + fileSnapshot blobs/manifests + sessionPolicy + resumeAnchor)
222
227
  * in the §8 fail-closed atomic order.
package/dist/index.d.ts CHANGED
@@ -28,4 +28,6 @@ export { createSideQueryLane, createPerModelKeyBrain, type KeyResolver, type Sid
28
28
  export { createKeyResolver, resolveModelApiKey, type ModelKeyRef } from "./key-resolver.js";
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
+ export { CATALOG_ENTRY_HEADER_LANES, CATALOG_ENTRY_HEADER_NAMES, type CatalogEntryHeaderName } from "./model-entry-headers.js";
32
+ export type { UserInputLedgerStore, UserInputAcceptance, NewUserInputAcceptance, OpenScanCursor, OpenScanPage, SettleHow, WriterGoneEvidence } from "./user-input-ledger.js";
31
33
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -25,4 +25,5 @@ export { createSideQueryLane, createPerModelKeyBrain } from "./boot/side-query-l
25
25
  export { createKeyResolver, resolveModelApiKey } from "./key-resolver.js";
26
26
  export { isModelKeyPoison, ModelKeyPoisonedError } from "./key-resolver.js";
27
27
  export { RETIRED_ENVS } from "./config.js";
28
+ export { CATALOG_ENTRY_HEADER_LANES, CATALOG_ENTRY_HEADER_NAMES } from "./model-entry-headers.js";
28
29
  //# sourceMappingURL=index.js.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
@@ -28,9 +28,9 @@
28
28
  * sessionId / phase)—— 与 `mcp.injection_dropped`「装配期铸、无 runId」同形,run 归属由投递腿承载(这条 run 的 SSE /
29
29
  * 这条 run 的账本)。向 core 要 ctx 加 runId 已登记为未闭环项。
30
30
  */
31
- import type { EngineNotice } from "@sema-agent/core";
32
- import type { ServerNoticeCode } from "./trace/engine-notice-wire.js";
33
- export declare const INSTRUCTION_SOURCE_CHANGED: ServerNoticeCode;
31
+ import type { EngineNotice, EngineNoticeCode } from "@sema-agent/core";
32
+ /** 码常量型 = core 码册(#1240 入册:受众 user、铸者 `NOTICE_MINTER` = `"server"`)。 */
33
+ export declare const INSTRUCTION_SOURCE_CHANGED: EngineNoticeCode;
34
34
  /** 基线读数。`recorded: false` = 这个会话还没有任何 run 记过基线(首 run ⇒ 只写不发);`source: null` = 记过,且那一次
35
35
  * 没有项目指令文件被选中。两态必须可判别:把「没记过」读成「记过没有」会让新会话首 run 发一帧 `null → CLAUDE.md`。 */
36
36
  export type InstructionSourceBaseline = {
@@ -1,6 +1,6 @@
1
- import type { EngineNotice } from "@sema-agent/core";
2
- import type { ServerNoticeCode } from "./trace/engine-notice-wire.js";
3
- export declare const MEMORY_LAYER_LOCKED_LEGACY: ServerNoticeCode;
1
+ import type { EngineNotice, EngineNoticeCode } from "@sema-agent/core";
2
+ /** 码常量型 = core 码册(#1240 入册:受众 operator、铸者 `NOTICE_MINTER` = `"server"`;不上会话流,只落转发体的运维日志)。 */
3
+ export declare const MEMORY_LAYER_LOCKED_LEGACY: EngineNoticeCode;
4
4
  /** 探测读数:锁住的层名(排序)+ 看不全的目录条数。挂载面不存在(新数据根)⇒ 两者皆空。 */
5
5
  export interface LockedLegacyLayerProbe {
6
6
  readonly layers: readonly string[];
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * 白名单 = 名字 → 读它的线路(闭集;**加一个名字 = 加一行并写明哪条线路读它、为什么是部署的事**)。
3
3
  * `anthropic-beta`:Anthropic Messages 的 beta 开关(逗号分隔的 beta 名;core anthropic 腿会把自己的 beta 并进同一个头)。
4
+ *
5
+ * S-963:本表与 {@link CATALOG_ENTRY_HEADER_NAMES} / {@link CatalogEntryHeaderName} 从包根 `src/index.ts` 原对象导出(web-admin 目录编辑页构建期取白名单,
6
+ * 不手抄;`RETIRED_ENVS` 同族)—— 它是**本进程准入判据的活表**,所以三只(表 / 每行线路数组 / 名字数组)在这里一次冻结:导出面上的嵌入方改不了它。
4
7
  */
5
- export declare const CATALOG_ENTRY_HEADER_LANES: {
8
+ export declare const CATALOG_ENTRY_HEADER_LANES: Readonly<{
6
9
  readonly "anthropic-beta": readonly ["anthropic-messages"];
7
- };
10
+ }>;
8
11
  export type CatalogEntryHeaderName = keyof typeof CATALOG_ENTRY_HEADER_LANES;
9
12
  export declare const CATALOG_ENTRY_HEADER_NAMES: readonly CatalogEntryHeaderName[];
10
13
  /** 判过的袋(只含白名单里的名字,值逐字)。 */
@@ -1,8 +1,8 @@
1
1
  import { redactSecrets } from "./trace/redact.js";
2
- export const CATALOG_ENTRY_HEADER_LANES = {
3
- "anthropic-beta": ["anthropic-messages"],
4
- };
5
- export const CATALOG_ENTRY_HEADER_NAMES = Object.keys(CATALOG_ENTRY_HEADER_LANES);
2
+ export const CATALOG_ENTRY_HEADER_LANES = Object.freeze({
3
+ "anthropic-beta": Object.freeze(["anthropic-messages"]),
4
+ });
5
+ export const CATALOG_ENTRY_HEADER_NAMES = Object.freeze(Object.keys(CATALOG_ENTRY_HEADER_LANES));
6
6
  export const MAX_HEADER_VALUE_CHARS = 1024;
7
7
  const HEADER_VALUE_SHAPE = /^[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?$/;
8
8
  const HEADER_NAME_TOKEN = /^[A-Za-z0-9!#$%&'*+.^_`|~-]{1,64}$/;
@@ -5,8 +5,8 @@
5
5
  * ## 病
6
6
  * 运营用户看不到日志 / 诊断面:VPN 中途切换之后模型网关建连超时,用户只拿到一句 undici 原句(`fetch failed (connect …)`),
7
7
  * 分不清是网络断了还是起服时读进的代理已经失效,也没有「查 VPN / 代理、代理变过就重启」这条出路。只落运维日志不算闭环
8
- * (test 立案原话)。core 没有现成的「模型不可达」用户通告码(`ENGINE_NOTICE_CODES` 亲查零命中)⇒ 本仓自铸,走既有的
9
- * `SERVER_NOTICE_AUDIENCE` 族(S-690 形:白名单 / 归属 / 消毒 / 隔离四件判据一个不绕),不开第二条通道。
8
+ * (test 立案原话)。core 没有现成的「模型不可达」用户通告码 ⇒ 本仓铸,走既有的本仓铸码族(S-690 形:白名单 / 归属 / 消毒 / 隔离四件判据
9
+ * 一个不绕),不开第二条通道。core 7.38.0(#1240)起本码在 core 码册里:受众 `NOTICE_AUDIENCE` = user、铸者 `NOTICE_MINTER` = `"server"`。
10
10
  *
11
11
  * ## 判据(一句):**这一次模型调用的终局是 `stopReason: "error"`,且它发出的最后一条请求在建连时就失败了**
12
12
  * 「最后一条请求落在哪」由出网观察者记在调用作用域上(`model-reach-scope.ts`,undici 诊断通道,见 `outbound-dispatcher.ts`)——
@@ -150,6 +150,30 @@ 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
+ };
171
+ readonly "user-input-ledger": {
172
+ readonly direction: "fail-open";
173
+ readonly event: "user_input_ledger_corrupt_read";
174
+ readonly tag: "server.user-input-ledger.corrupt-record-skipped";
175
+ readonly note: "F: a skipped user-input ledger record can only bend the crash-loss ANNOUNCEMENT (a lost settle announces one steer that was handled; a lost accept leaves a real loss unannounced; a lost announce repeats it once) — it gates nothing and changes no run.";
176
+ };
153
177
  };
154
178
  /** 登记名(闭集)。 */
155
179
  export type JsonlReplaySeat = keyof typeof JSONL_REPLAY_SEATS;
@@ -93,6 +93,30 @@ 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
+ },
114
+ "user-input-ledger": {
115
+ direction: "fail-open",
116
+ event: "user_input_ledger_corrupt_read",
117
+ tag: "server.user-input-ledger.corrupt-record-skipped",
118
+ note: "F: a skipped user-input ledger record can only bend the crash-loss ANNOUNCEMENT (a lost settle announces one steer that was handled; a lost accept leaves a real loss unannounced; a lost announce repeats it once) — it gates nothing and changes no run.",
119
+ },
96
120
  };
97
121
  function corruptRefusalSentence(seat, path, reason) {
98
122
  const entry = JSONL_REPLAY_SEATS[seat];
@@ -291,7 +291,7 @@ export declare const FAIL_OPEN_TAGS: {
291
291
  };
292
292
  readonly "server.approval-card.closed-word-out-of-set": {
293
293
  readonly cls: "F";
294
- readonly note: "S-249(`approval-card.ts` 的边界窄读族;今天三个成员:`readRuleOffersAbsence`、S-934 ② 起的 `readAskClassPair`(core 7.37.0 #1214 的类对 `askClass` / `classes`,活卡帧 / 呈卡帧的卡 / 运维队列行同一只,成员筛 = core `isAskClass`,两键同在同缺,半对 / 首员不符同样算读不出;detail 前缀 `askClass=` / `classes=`),与 S-721 起的 `readClosedWordSeat` —— 一座一只 core 成员筛,座是参数:`mandate`(core 7.32.0 #1092,活卡帧 / 活挂起行 / 运维队列行同一只,闭集筛 = core `isPersistedRuleMandate`)与 S-755 起的 `origin`(运维队列行读 park 行孪生座,闭集筛 = core `isAskOrigin`,与 inbox 行 core 投影处同一只筛);detail 前缀 `ruleOffersAbsence=` / `mandate=` / `origin=` 判别是哪一座):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。";
294
+ readonly note: "S-249(`approval-card.ts` 的边界窄读族;今天三个成员:`readRuleOffersAbsence`、S-934 ② 起的 `readAskClassPair`(core 7.37.0 #1214 的类对 `askClass` / `classes`,活卡帧 / 呈卡帧的卡 / 运维队列行同一只,判法 = core 屏 `screenAskClassMembers`(S-955 ⑥ / core 7.38.0 #1249,本仓零自写对判),两键同在同缺,半对 / 集外 / 首员不符 / 重复员 = 屏拒 = 读不出;detail 前缀 `askClass=` / `classes=`),与 S-721 起的 `readClosedWordSeat` —— 一座一只 core 成员筛,座是参数:`mandate`(core 7.32.0 #1092,活卡帧 / 活挂起行 / 运维队列行同一只,闭集筛 = core `isPersistedRuleMandate`)与 S-755 起的 `origin`(运维队列行读 park 行孪生座,闭集筛 = core `isAskOrigin`,与 inbox 行 core 投影处同一只筛);detail 前缀 `ruleOffersAbsence=` / `mandate=` / `origin=` 判别是哪一座):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。";
295
295
  };
296
296
  readonly "server.pg-pool.idle-client-error": {
297
297
  readonly cls: "F";
@@ -317,6 +317,30 @@ export declare const FAIL_OPEN_TAGS: {
317
317
  readonly cls: "F";
318
318
  readonly note: "S-582(`plugins/send-file-ledger.ts`,local 车道的发文件账本):一条账行读不出形(`42` / 缺 id·scope·objectKey 三元组 / 中间半截 JSON)⇒ 跳过它继续。最坏后果 = 那条已签发的链接在它租户的列表里**看不见**(对象本身没动;账本本来就不存 URL)。治理面少一行是可见性损失,不是放行 ⇒ F;但它恰是治理面要看的那一类事实,所以必须留痕 —— 修前零披露。";
319
319
  };
320
+ readonly "server.approval-ask-audit.corrupt-record-skipped": {
321
+ readonly cls: "F";
322
+ 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**。";
323
+ };
324
+ readonly "server.workflow-notify-journal.corrupt-record-skipped": {
325
+ readonly cls: "F";
326
+ 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\"`)。";
327
+ };
328
+ readonly "server.workflow-agent-session-index.corrupt-record-skipped": {
329
+ readonly cls: "F";
330
+ 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`。";
331
+ };
332
+ readonly "server.user-input-ledger.append-failed": {
333
+ readonly cls: "F";
334
+ 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。";
335
+ };
336
+ readonly "server.user-input-ledger.read-failed": {
337
+ readonly cls: "F";
338
+ 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)是拿一个告知面的抖动去打掉主路径,方向更坏。";
339
+ };
340
+ readonly "server.user-input-ledger.corrupt-record-skipped": {
341
+ readonly cls: "F";
342
+ 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 新立)。";
343
+ };
320
344
  readonly "server.outbound.connect-observer-threw": {
321
345
  readonly cls: "F";
322
346
  readonly note: "S-925(`outbound-dispatcher.ts` 的 undici 诊断通道订阅者:`undici:request:create` / `:headers` / `:error`):一只订阅回调抛了。通道在发布方的调用栈里同步回调订阅者,抛出去在 Node 上是未捕获异常(整进程倒),所以每只回调整体 try、落本 tag。最坏后果 = 这一条请求的建连失败没被记下:少一行 `outbound_connect_failed`、诊断段 `outbound.lastConnectFailure` 不更新、那次模型调用不铸 `server.model_unreachable` 用户通告 —— 请求本身、它的错误、run 的终态一个字节不受影响(观察只读)。计数恒零是证据;非零 = undici 通道载荷的形变了,去看 detail 的 site。";