@sema-agent/server 7.109.0 → 7.110.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 (80) hide show
  1. package/MIGRATION.md +17 -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-card.d.ts +6 -5
  6. package/dist/approval-card.js +8 -11
  7. package/dist/boot/reapers.d.ts +3 -0
  8. package/dist/boot/reapers.js +3 -2
  9. package/dist/boot/resolve-spec.js +3 -4
  10. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  11. package/dist/boot/stage-07-capability-layer.js +17 -3
  12. package/dist/boot/stage-08-reapers.d.ts +1 -0
  13. package/dist/boot/stage-08-reapers.js +2 -2
  14. package/dist/boot/stage-09-leader.d.ts +1 -0
  15. package/dist/boot/stage-10-http-server.d.ts +1 -0
  16. package/dist/boot/stage-10-http-server.js +2 -1
  17. package/dist/config-catalog.js +1 -1
  18. package/dist/config-types.d.ts +3 -2
  19. package/dist/config.js +19 -7
  20. package/dist/deployment-governance.d.ts +8 -2
  21. package/dist/deployment-governance.js +9 -8
  22. package/dist/hooks/hook-runner.d.ts +2 -2
  23. package/dist/hooks/hook-runner.js +40 -26
  24. package/dist/http/durable-run-leg.js +1 -1
  25. package/dist/http/resume-legs.js +6 -1
  26. package/dist/http/routes/runs.d.ts +0 -1
  27. package/dist/http/routes/runs.js +22 -0
  28. package/dist/http/routes/sessions.js +12 -2
  29. package/dist/http/routes/tasks.js +5 -1
  30. package/dist/http/server.d.ts +5 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +1 -0
  33. package/dist/instruction-source-notice.d.ts +3 -3
  34. package/dist/memory-layer-legacy-lock.d.ts +3 -3
  35. package/dist/model-entry-headers.d.ts +5 -2
  36. package/dist/model-entry-headers.js +4 -4
  37. package/dist/model-unreachable-notice.d.ts +2 -2
  38. package/dist/observability/corrupt-read-seat.d.ts +6 -0
  39. package/dist/observability/corrupt-read-seat.js +6 -0
  40. package/dist/observability/fail-open.d.ts +13 -1
  41. package/dist/observability/fail-open.js +13 -1
  42. package/dist/observability/metrics.js +2 -1
  43. package/dist/observability/run-terminal.js +10 -0
  44. package/dist/operator-ask.d.ts +9 -8
  45. package/dist/operator-ask.js +9 -1
  46. package/dist/plugins/file-run-store.js +2 -2
  47. package/dist/plugins/memory-run-store.d.ts +1 -1
  48. package/dist/plugins/memory-run-store.js +2 -1
  49. package/dist/plugins/pg-pool.js +15 -0
  50. package/dist/plugins/run-store-sql.d.ts +1 -1
  51. package/dist/plugins/run-store-sql.js +3 -2
  52. package/dist/plugins/schema-indexes.js +2 -0
  53. package/dist/plugins/store-backend.d.ts +4 -0
  54. package/dist/plugins/store-backend.js +8 -0
  55. package/dist/plugins/store-contracts.d.ts +7 -0
  56. package/dist/plugins/store-contracts.js +1 -0
  57. package/dist/plugins/tidb-pool.js +17 -0
  58. package/dist/plugins/user-input-ledger-file.d.ts +33 -0
  59. package/dist/plugins/user-input-ledger-file.js +98 -0
  60. package/dist/plugins/user-input-ledger-sql.d.ts +44 -0
  61. package/dist/plugins/user-input-ledger-sql.js +101 -0
  62. package/dist/run-local.d.ts +18 -5
  63. package/dist/run-local.js +24 -9
  64. package/dist/runs.d.ts +8 -1
  65. package/dist/runs.js +6 -2
  66. package/dist/runtime-governance.d.ts +20 -8
  67. package/dist/runtime-governance.js +7 -12
  68. package/dist/session-records-skipped-notice.d.ts +4 -3
  69. package/dist/task-settings.d.ts +0 -6
  70. package/dist/task-settings.js +0 -1
  71. package/dist/tool-approval.d.ts +8 -1
  72. package/dist/tool-approval.js +4 -1
  73. package/dist/trace/core-keyset-guard.d.ts +16 -1
  74. package/dist/trace/engine-notice-wire.d.ts +30 -35
  75. package/dist/trace/engine-notice-wire.js +23 -23
  76. package/dist/trace/self-minted-wire.d.ts +14 -0
  77. package/dist/trace/self-minted-wire.js +6 -0
  78. package/dist/user-input-ledger.d.ts +296 -0
  79. package/dist/user-input-ledger.js +337 -0
  80. package/package.json +3 -3
@@ -648,12 +648,12 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
648
648
  }
649
649
  return results;
650
650
  }
651
- function basePayload(ctx) {
651
+ function basePayload(ctx, permissionMode) {
652
652
  return {
653
653
  session_id: ctx.sessionId,
654
654
  transcript_path: "",
655
655
  cwd: ctx.cwd,
656
- ...(ctx.permissionMode ? { permission_mode: ctx.permissionMode } : {}),
656
+ ...(permissionMode !== undefined ? { permission_mode: permissionMode } : {}),
657
657
  };
658
658
  }
659
659
  function ignoreMatchers(groups) {
@@ -732,7 +732,7 @@ export function createTaskHooks(config, ctx) {
732
732
  if (!m)
733
733
  continue;
734
734
  const payload = {
735
- ...basePayload(ctx),
735
+ ...basePayload(ctx, tctx.permissionMode),
736
736
  hook_event_name: "PreToolUse",
737
737
  tool_name: toolName,
738
738
  tool_input: currentInput,
@@ -827,7 +827,7 @@ export function createTaskHooks(config, ctx) {
827
827
  .map((c) => (c && typeof c === "object" && typeof c.text === "string" ? c.text : ""))
828
828
  .join("");
829
829
  const payload = {
830
- ...basePayload(ctx),
830
+ ...basePayload(ctx, tctx.permissionMode),
831
831
  hook_event_name: "PostToolUse",
832
832
  tool_name: toolName,
833
833
  tool_input: input,
@@ -876,7 +876,7 @@ export function createTaskHooks(config, ctx) {
876
876
  const postToolUseFailureHook = async (toolName, input, f, tctx) => {
877
877
  const contexts = [];
878
878
  const payload = {
879
- ...basePayload(ctx),
879
+ ...basePayload(ctx, tctx.permissionMode),
880
880
  hook_event_name: "PostToolUseFailure",
881
881
  tool_name: toolName,
882
882
  tool_input: input,
@@ -919,10 +919,10 @@ export function createTaskHooks(config, ctx) {
919
919
  hooks.postToolUseFailure = postToolUseFailureHook;
920
920
  }
921
921
  if (postBatch) {
922
- const postToolBatchHook = async (calls) => {
922
+ const postToolBatchHook = async (calls, _meta, bctx) => {
923
923
  const contexts = [];
924
924
  const payload = {
925
- ...basePayload(ctx),
925
+ ...basePayload(ctx, bctx?.permissionMode),
926
926
  hook_event_name: "PostToolBatch",
927
927
  tool_calls: calls.map((c) => ({
928
928
  tool_name: c.toolName,
@@ -962,9 +962,9 @@ export function createTaskHooks(config, ctx) {
962
962
  hooks.postToolBatch = postToolBatchHook;
963
963
  }
964
964
  if (promptSubmit) {
965
- const userPromptSubmitHook = async (prompt) => {
965
+ const userPromptSubmitHook = async (prompt, uctx) => {
966
966
  const contexts = [];
967
- const payload = { ...basePayload(ctx), hook_event_name: "UserPromptSubmit", prompt: clip(prompt, MAX_TOOL_INPUT_CHARS) };
967
+ const payload = { ...basePayload(ctx, uctx?.permissionMode), hook_event_name: "UserPromptSubmit", prompt: clip(prompt, MAX_TOOL_INPUT_CHARS) };
968
968
  const singles = await runMatchingEntries("UserPromptSubmit", ignoreMatchers(promptSubmit), "", payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
969
969
  for (const { entry, run } of singles) {
970
970
  if (run.code === 2) {
@@ -1003,7 +1003,7 @@ export function createTaskHooks(config, ctx) {
1003
1003
  }
1004
1004
  if (stopEntries) {
1005
1005
  const stopHook = async (sctx) => {
1006
- const payload = { ...basePayload(ctx), hook_event_name: "Stop", stop_hook_active: sctx.stopHookActive };
1006
+ const payload = { ...basePayload(ctx, sctx.permissionMode), hook_event_name: "Stop", stop_hook_active: sctx.stopHookActive };
1007
1007
  let llmExtra;
1008
1008
  const wantsPrompt = stopEntries.some((g) => g.hooks.some((h) => h.type === "prompt"));
1009
1009
  if (wantsPrompt) {
@@ -1097,7 +1097,7 @@ export function createTaskHooks(config, ctx) {
1097
1097
  const stopFailureHook = async (fctx) => {
1098
1098
  const turnError = toAssistantTurnError(fctx.errorKind);
1099
1099
  const payload = {
1100
- ...basePayload(ctx),
1100
+ ...basePayload(ctx, undefined),
1101
1101
  hook_event_name: "StopFailure",
1102
1102
  error: turnError,
1103
1103
  ...(fctx.error ? { error_details: clip(redactSecrets(fctx.error), MAX_HOOK_FEEDBACK_CHARS) } : {}),
@@ -1109,7 +1109,7 @@ export function createTaskHooks(config, ctx) {
1109
1109
  if (preCompactEntries) {
1110
1110
  const preCompactHook = async (pctx) => {
1111
1111
  const payload = {
1112
- ...basePayload(ctx),
1112
+ ...basePayload(ctx, undefined),
1113
1113
  hook_event_name: "PreCompact",
1114
1114
  trigger: pctx.trigger,
1115
1115
  custom_instructions: pctx.customInstructions ?? null,
@@ -1143,7 +1143,7 @@ export function createTaskHooks(config, ctx) {
1143
1143
  if (postCompactEntries) {
1144
1144
  const postCompactHook = async (cctx) => {
1145
1145
  const payload = {
1146
- ...basePayload(ctx),
1146
+ ...basePayload(ctx, undefined),
1147
1147
  hook_event_name: "PostCompact",
1148
1148
  trigger: cctx.trigger,
1149
1149
  compact_summary: clip(cctx.summary, MAX_TOOL_RESPONSE_CHARS),
@@ -1191,16 +1191,30 @@ export function composeHooks(deployment, task) {
1191
1191
  if (!slot)
1192
1192
  continue;
1193
1193
  const r = await slot(toolName, current, ctx);
1194
- if (!r)
1194
+ if (r === undefined || Object.is(r, null) || Object.is(r, false))
1195
1195
  continue;
1196
+ if (typeof r !== "object")
1197
+ return r;
1196
1198
  if (r.additionalContext)
1197
1199
  contexts.push(r.additionalContext);
1198
- if (r.action === "deny")
1199
- return { ...r, ...buildHookContextField(contexts) };
1200
- if (r.action === "ask")
1201
- asked = r;
1202
- if (r.action === "allow" && r.updatedInput !== undefined) {
1203
- current = r.updatedInput;
1200
+ const action = r.action;
1201
+ const updatedInput = r.updatedInput;
1202
+ switch (action) {
1203
+ case "deny":
1204
+ return { ...r, ...buildHookContextField(contexts) };
1205
+ case "ask":
1206
+ asked = r;
1207
+ break;
1208
+ case "allow":
1209
+ case undefined:
1210
+ if (updatedInput !== undefined)
1211
+ current = updatedInput;
1212
+ break;
1213
+ default: {
1214
+ const unread = action;
1215
+ void unread;
1216
+ return r;
1217
+ }
1204
1218
  }
1205
1219
  }
1206
1220
  if (asked) {
@@ -1245,20 +1259,20 @@ export function composeHooks(deployment, task) {
1245
1259
  };
1246
1260
  }
1247
1261
  if (d.postToolBatch || t.postToolBatch) {
1248
- out.postToolBatch = async (calls) => {
1249
- const rd = d.postToolBatch ? await d.postToolBatch(calls) : undefined;
1250
- const rt = t.postToolBatch ? await t.postToolBatch(calls) : undefined;
1262
+ out.postToolBatch = async (calls, meta, bctx) => {
1263
+ const rd = d.postToolBatch ? await d.postToolBatch(calls, meta, bctx) : undefined;
1264
+ const rt = t.postToolBatch ? await t.postToolBatch(calls, meta, bctx) : undefined;
1251
1265
  const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1252
1266
  const additionalContext = buildHookContext(contexts);
1253
1267
  return additionalContext === undefined ? undefined : { additionalContext };
1254
1268
  };
1255
1269
  }
1256
1270
  if (d.userPromptSubmit || t.userPromptSubmit) {
1257
- out.userPromptSubmit = async (prompt) => {
1258
- const rd = d.userPromptSubmit ? await d.userPromptSubmit(prompt) : undefined;
1271
+ out.userPromptSubmit = async (prompt, uctx) => {
1272
+ const rd = d.userPromptSubmit ? await d.userPromptSubmit(prompt, uctx) : undefined;
1259
1273
  if (rd?.block)
1260
1274
  return { ...rd, ...buildHookContextField(rd.additionalContext ? [rd.additionalContext] : []) };
1261
- const rt = t.userPromptSubmit ? await t.userPromptSubmit(prompt) : undefined;
1275
+ const rt = t.userPromptSubmit ? await t.userPromptSubmit(prompt, uctx) : undefined;
1262
1276
  if (!rd && !rt)
1263
1277
  return undefined;
1264
1278
  const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
@@ -63,7 +63,7 @@ export async function mintAndDriveDurableRun(ctx, runStore, args) {
63
63
  windowMs: deps.config.streamApproval?.windowMs,
64
64
  windowMarginMs: deps.config.streamAskWindowMarginMs,
65
65
  }
66
- : undefined, deps.checkpointStore, args.prepared.engineNotices, args.trustedInternals);
66
+ : undefined, deps.checkpointStore, args.prepared.engineNotices, args.trustedInternals, deps.userInputLedger);
67
67
  return { status: 202, body: { taskId, sessionId, status: "running" } };
68
68
  }
69
69
  //# sourceMappingURL=durable-run-leg.js.map
@@ -622,11 +622,14 @@ export function createResumeLegs(ctx) {
622
622
  unregisterEngineNotice = registerEngineNoticeLeg({
623
623
  sessionId,
624
624
  ...(args.engineNotices !== undefined && args.engineNotices.length > 0 ? { pending: [...args.engineNotices] } : {}),
625
- durable: (row) => void append("engine_notice", row).catch((err) => {
625
+ durable: (row) => append("engine_notice", row).then(() => true, (err) => {
626
626
  recordFailOpen("server.engine-notice.durable-append-failed", `leg=resume code=${row.code} task=${taskId ?? ""}`);
627
627
  deps.logger?.warn?.("engine_notice_append_failed", { taskId, noticeCode: row.code, sessionId: row.sessionId, err: err instanceof Error ? err.message : String(err) });
628
+ return false;
628
629
  }),
629
630
  });
631
+ if (deps.userInputLedger && taskId)
632
+ await deps.userInputLedger.announceUndrained({ sessionId, taskId });
630
633
  const anchorOwner = principal ?? null;
631
634
  const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
632
635
  const captureTurnAnchor = deps.resumeAnchorStore && getLeafId
@@ -964,6 +967,8 @@ export function createResumeLegs(ctx) {
964
967
  finally {
965
968
  unregisterEngineNotice?.();
966
969
  if (taskId) {
970
+ if (resumeStreamRef !== undefined)
971
+ await deps.userInputLedger?.settleLeg(resumeStreamRef);
967
972
  if (inflightRuns.get(taskId) === cancelCtrl)
968
973
  inflightRuns.delete(taskId);
969
974
  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");
@@ -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
  }
@@ -333,10 +333,12 @@ export async function handleTasks(req, res, match, ctx) {
333
333
  },
334
334
  ...(noticeLedger
335
335
  ? {
336
- durable: (row) => void noticeLedger.append("engine_notice", row).catch(() => recordFailOpen("server.engine-notice.durable-append-failed", `leg=sync code=${row.code} task=${durableTaskId ?? ""}`)),
336
+ 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
337
  }
338
338
  : {}),
339
339
  });
340
+ if (deps.userInputLedger && durableTaskId && prepared.spec.sessionId)
341
+ await deps.userInputLedger.announceUndrained({ sessionId: prepared.spec.sessionId, taskId: durableTaskId });
340
342
  try {
341
343
  const streamBody = async () => {
342
344
  await withPrincipal(principal, async () => {
@@ -700,6 +702,8 @@ export async function handleTasks(req, res, match, ctx) {
700
702
  clearInterval(durableHeartbeat);
701
703
  if (durableTaskId && steerableRuns.get(durableTaskId) === liveStreamRef)
702
704
  steerableRuns.delete(durableTaskId);
705
+ if (liveStreamRef !== undefined)
706
+ await deps.userInputLedger?.settleLeg(liveStreamRef);
703
707
  if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
704
708
  inflightRuns.delete(durableTaskId);
705
709
  cancelledViaVerb.delete(durableTaskId);
@@ -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 腿受理前写 `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, 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
@@ -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,12 @@ 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 "user-input-ledger": {
154
+ readonly direction: "fail-open";
155
+ readonly event: "user_input_ledger_corrupt_read";
156
+ readonly tag: "server.user-input-ledger.corrupt-record-skipped";
157
+ 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.";
158
+ };
153
159
  };
154
160
  /** 登记名(闭集)。 */
155
161
  export type JsonlReplaySeat = keyof typeof JSONL_REPLAY_SEATS;
@@ -93,6 +93,12 @@ 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
+ "user-input-ledger": {
97
+ direction: "fail-open",
98
+ event: "user_input_ledger_corrupt_read",
99
+ tag: "server.user-input-ledger.corrupt-record-skipped",
100
+ 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.",
101
+ },
96
102
  };
97
103
  function corruptRefusalSentence(seat, path, reason) {
98
104
  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,18 @@ 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.user-input-ledger.append-failed": {
321
+ 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。";
323
+ };
324
+ readonly "server.user-input-ledger.read-failed": {
325
+ 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)是拿一个告知面的抖动去打掉主路径,方向更坏。";
327
+ };
328
+ readonly "server.user-input-ledger.corrupt-record-skipped": {
329
+ 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 新立)。";
331
+ };
320
332
  readonly "server.outbound.connect-observer-threw": {
321
333
  readonly cls: "F";
322
334
  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。";
@@ -264,7 +264,7 @@ export const FAIL_OPEN_TAGS = {
264
264
  "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
265
  "server.approval-card.closed-word-out-of-set": {
266
266
  cls: "F",
267
- 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}` 曾让诊断本身抛出、打掉整只队列)。",
267
+ 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}` 曾让诊断本身抛出、打掉整只队列)。",
268
268
  },
269
269
  "server.pg-pool.idle-client-error": {
270
270
  cls: "F",
@@ -290,6 +290,18 @@ export const FAIL_OPEN_TAGS = {
290
290
  cls: "F",
291
291
  note: "S-582(`plugins/send-file-ledger.ts`,local 车道的发文件账本):一条账行读不出形(`42` / 缺 id·scope·objectKey 三元组 / 中间半截 JSON)⇒ 跳过它继续。最坏后果 = 那条已签发的链接在它租户的列表里**看不见**(对象本身没动;账本本来就不存 URL)。治理面少一行是可见性损失,不是放行 ⇒ F;但它恰是治理面要看的那一类事实,所以必须留痕 —— 修前零披露。",
292
292
  },
293
+ "server.user-input-ledger.append-failed": {
294
+ 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。",
296
+ },
297
+ "server.user-input-ledger.read-failed": {
298
+ 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)是拿一个告知面的抖动去打掉主路径,方向更坏。",
300
+ },
301
+ "server.user-input-ledger.corrupt-record-skipped": {
302
+ 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 新立)。",
304
+ },
293
305
  "server.outbound.connect-observer-threw": {
294
306
  cls: "F",
295
307
  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。",
@@ -215,7 +215,7 @@ export function createMetrics() {
215
215
  m.counter("http_requests_total", "HTTP requests by route/method/status");
216
216
  m.histogram("http_request_duration_seconds", "HTTP request duration");
217
217
  m.counter("rate_limited_total", "Requests rejected by the rate limiter");
218
- m.counter("brain_echo_dropped_total", "Provider echo fields (response request-id header / reported model name) the engine's brain dropped at write time for failing the echo shape (visible ASCII, 1-128 chars), by field (requestId|responseModel); non-zero = the endpoint or a gateway in front of it echoes out-of-shape bytes");
218
+ m.counter("brain_echo_dropped_total", "Provider echo fields (response request-id header / reported model name) the engine's brain dropped at write time for failing the echo shape (visible ASCII, 1-128 chars), by field (requestId|responseModel|apiErrorCode — the last is the provider's own refusal word on a 4xx, core 7.38.0); non-zero = the endpoint or a gateway in front of it echoes out-of-shape bytes");
219
219
  m.counter("mcp_probe_total", `No-run MCP status face outcomes by outcome (${MCP_PROBE_OUTCOMES.join("/")} — see src/capabilities/mcp-probe.ts MCP_PROBE_OUTCOMES; rate_limited has TWO causes: the per-identity fixed window AND the replica-wide in-flight cap, which can refuse caller B because of caller A)`);
220
220
  m.counter("tasks_total", "Finished tasks by terminal status");
221
221
  m.counter("task_tokens_total", "Total model tokens consumed by tasks");
@@ -265,6 +265,7 @@ export function createMetrics() {
265
265
  m.counter("images_omitted_total", "Tasks whose images will be degraded to text placeholders (no-vision model), by model (S6)");
266
266
  m.counter("runs_reaped_total", "Run rows flipped by the background reapers (S7), by kind (stale/suspended/expired_checkpoint)");
267
267
  m.counter("checkpoints_reaped_total", "Durable checkpoints expired by the backstop reaper (S7)");
268
+ m.counter("user_input_converged_total", "User-input acceptances (S-928 ledger) converged to crashed by the maintenance tick (the boot pass reports its count on the user_input_ledger_crash_converged warn line instead): steers accepted whose writer process ended before the run settled");
268
269
  m.counter("bakes_reaped_total", "Image bakes failed by the stale-lease reaper (S7)");
269
270
  m.counter("counter_flush_failed_total", "Write-behind counter flush/refresh failures (S9), by table and kind");
270
271
  m.gauge("counter_flush_fail_streak", "Current consecutive write-behind flush/refresh failures (S9), by table and kind");
@@ -176,6 +176,11 @@ const STRANDED_ANSWER_TREATMENT = {
176
176
  deliveryId: "verbatim",
177
177
  toolCallId: "provider-join-key",
178
178
  };
179
+ const TERMINAL_FAILURE_DETAIL_TREATMENT = {
180
+ kind: "verbatim",
181
+ status: "verbatim",
182
+ code: "provider-echo",
183
+ };
179
184
  const PROVIDER_ECHO_SHAPERS = {
180
185
  responseModel: (v) => (admitProviderEcho(v, "responseModel") ? v : undefined),
181
186
  apiFailure: (v) => shapeEchoMembers(v, API_FAILURE_TREATMENT, "apiFailure"),
@@ -238,6 +243,11 @@ export function redactTerminalResult(result) {
238
243
  const g = gateOf(np);
239
244
  if (g?.changed === true)
240
245
  nt = { ...base, nestedPause: { ...np, gate: g.gate } };
246
+ const cur = nt ?? base;
247
+ const detail = cur.detail;
248
+ const shaped = shapeEchoMembers(detail, TERMINAL_FAILURE_DETAIL_TREATMENT, "terminal.detail");
249
+ if (shaped !== detail)
250
+ nt = { ...cur, detail: shaped };
241
251
  }
242
252
  if (nt !== undefined) {
243
253
  out.terminal = nt;
@@ -33,7 +33,7 @@
33
33
  * (`dist/core/gate-fold.js`),那是 `allowLayerMayClear` 的另一条独立豁免;本仓再盖一次是第二个写者。
34
34
  * · **问答工具**(`deployment-governance.ts` 的 `AskUserQuestion`)—— core 的谓词把保留问答工具单列豁免。
35
35
  */
36
- import type { PermissionResult } from "@sema-agent/core";
36
+ import { type PermissionResult, type ToolPolicy } from "@sema-agent/core";
37
37
  /** 本文件两只铸点的返回形 = core `PermissionResult` 的 ask 臂(窄型:`authoredBy` 只长在这一臂上)。 */
38
38
  export type OperatorAskResult = Extract<PermissionResult, {
39
39
  action: "ask";
@@ -63,12 +63,13 @@ export declare function operatorAsk(rule: string, message: string): OperatorAskR
63
63
  */
64
64
  export declare function operatorAlwaysHumanAsk(rule: string, message: string): OperatorAskResult;
65
65
  /**
66
- * core 部署出身位 `authoredBy:"deployment"` 的**唯一**盖位函数(S-897 起收成一处):给一只**部署自己的声明**产的 ask 盖上出身 ⇒
67
- * core 类读 `operator_forced`、`mandated`、不铸 `mandate` 词(表外的门;缺词 = 另有门在)⇒ bypassPermissions 放不掉、站着的答案
68
- * 退不掉、壳也不会按单个强制词替人答。三个调用者:{@link operatorAlwaysHumanAsk}(`APPROVAL_NEVER_AUTO` / 预算耗尽)、
69
- * 治理层包装(`runtime-governance.ts` 的 `createGovernanceAskMarkingPolicy`)、SUP 路由姿态(`router/route-orchestration.ts`,
70
- * 经 {@link operatorAlwaysHumanAsk})。修前前两处各写一份 `{ ...d, authoredBy: "deployment" }`(两个写者),S-897 若再抄就是第三份。
71
- * 只作用于 ask 臂(类型上就只收 ask);其余键逐字透传。
66
+ * 🔴 **S-958 —— 治理层的叶策略包装**:`leaf` 产的每一只 ask 盖部署出身(经 {@link withDeploymentAuthorship}),allow / deny 原物交回;
67
+ * 成员(`projection` / `pathDenyRules` / `writeTargetGuard` / `nameSets`)经 core `wrapToolPolicy` 随行(契约「包装至少拒内层所拒」:
68
+ * 只加一位、从不放宽)。同步叶保持同步返回;叶抛照抛(fail-closed 的属主仍是叶)。
69
+ *
70
+ * **只收叶**:治理层(`runtime-governance.ts` `applyRuntimeGovernance`)对每一片叶各盖一次,再交 core `combinePolicies` 折**一次**
71
+ * (折叠是最外层,其输出原样交回 —— core 把落选 ask 的类(含本位带出的 `operator_forced`)写进 `foldedAskClasses` 并担保那只对象)。
72
+ * 把一只折叠交给本函数 = 在折叠的输出上展开 —— 那正是 S-958 修掉的病(7.39.0 起按名拒,响亮、fail-closed,不是静默)。
72
73
  */
73
- export declare function withDeploymentAuthorship<A extends OperatorAskResult>(ask: A): A;
74
+ export declare function createDeploymentAuthoredPolicy(leaf: ToolPolicy): ToolPolicy;
74
75
  //# sourceMappingURL=operator-ask.d.ts.map