@sema-agent/server 7.141.0 → 7.142.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 (171) hide show
  1. package/MIGRATION.md +215 -0
  2. package/USAGE.md +67 -12
  3. package/dist/approval-ask-audit-store.d.ts +54 -38
  4. package/dist/approval-ask-audit-store.js +58 -39
  5. package/dist/approval-card.d.ts +8 -8
  6. package/dist/approval-crash-convergence.d.ts +17 -7
  7. package/dist/approval-crash-convergence.js +94 -22
  8. package/dist/approval-deny-reasons.d.ts +34 -30
  9. package/dist/approval-deny-reasons.js +21 -6
  10. package/dist/approval-policy-settlement.d.ts +1 -1
  11. package/dist/approval-reconciler.d.ts +33 -1
  12. package/dist/approval-reconciler.js +43 -40
  13. package/dist/auth-bridge.d.ts +9 -6
  14. package/dist/auth-bridge.js +9 -3
  15. package/dist/boot/adoption.js +2 -1
  16. package/dist/boot/budget-tracing.d.ts +16 -0
  17. package/dist/boot/budget-tracing.js +10 -6
  18. package/dist/boot/device-lane.d.ts +1 -1
  19. package/dist/boot/execution-env.d.ts +24 -4
  20. package/dist/boot/execution-env.js +26 -23
  21. package/dist/boot/execution-lane.js +2 -1
  22. package/dist/boot/exit-boundary.js +5 -4
  23. package/dist/boot/memory-consolidation.js +2 -1
  24. package/dist/boot/parked-revive-gate.js +2 -1
  25. package/dist/boot/permission-rules-audit.d.ts +0 -28
  26. package/dist/boot/permission-rules-audit.js +2 -1
  27. package/dist/boot/read-deny-anchor-audit.js +2 -1
  28. package/dist/boot/reapers.js +4 -3
  29. package/dist/boot/resolve-spec.js +11 -2
  30. package/dist/boot/retention-lane.js +5 -3
  31. package/dist/boot/runner-deps.js +16 -13
  32. package/dist/boot/runtime-caps.js +3 -2
  33. package/dist/boot/session-faces.js +4 -3
  34. package/dist/boot/shutdown.js +3 -2
  35. package/dist/boot/stage-04-stores.js +3 -2
  36. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  37. package/dist/boot/stage-05-execution-env.js +8 -6
  38. package/dist/boot/stage-06-runners.d.ts +1 -1
  39. package/dist/boot/stage-06-runners.js +2 -1
  40. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  41. package/dist/boot/stage-07-capability-layer.js +2 -1
  42. package/dist/boot/stage-08-reapers.d.ts +1 -1
  43. package/dist/boot/stage-08-reapers.js +3 -2
  44. package/dist/boot/stage-09-leader.d.ts +1 -1
  45. package/dist/boot/stage-10-http-server.d.ts +1 -1
  46. package/dist/boot/stage-10-http-server.js +5 -4
  47. package/dist/boot/stores.js +3 -2
  48. package/dist/boot/web-admin.js +17 -17
  49. package/dist/boot/webfetch-summarize-lane.js +2 -1
  50. package/dist/boot/workflow-orchestration.js +3 -2
  51. package/dist/capabilities/builtin-tools.d.ts +2 -1
  52. package/dist/capabilities/builtin-tools.js +2 -4
  53. package/dist/capabilities/composite-members.d.ts +2 -1
  54. package/dist/capabilities/composite-members.js +2 -1
  55. package/dist/capability-facts.d.ts +24 -0
  56. package/dist/capability-facts.js +23 -0
  57. package/dist/config-catalog.js +1 -0
  58. package/dist/config-types.d.ts +15 -4
  59. package/dist/config.d.ts +6 -0
  60. package/dist/config.js +17 -3
  61. package/dist/execution-lane-caps.d.ts +2 -1
  62. package/dist/fleet/fleet-bus.d.ts +6 -3
  63. package/dist/hooks/hook-runner.d.ts +6 -0
  64. package/dist/hooks/hook-runner.js +30 -1
  65. package/dist/http/dispatch.js +4 -2
  66. package/dist/http/execution-context.d.ts +2 -2
  67. package/dist/http/idempotency.d.ts +1 -1
  68. package/dist/http/principal-gate.d.ts +3 -1
  69. package/dist/http/principal-gate.js +17 -8
  70. package/dist/http/resume-legs.js +5 -9
  71. package/dist/http/route-table.js +2 -0
  72. package/dist/http/routes/attachments.js +18 -0
  73. package/dist/http/routes/capabilities.js +5 -2
  74. package/dist/http/routes/memory-policy.js +26 -6
  75. package/dist/http/routes/runs.js +3 -3
  76. package/dist/http/routes/session-memory-sync.d.ts +63 -0
  77. package/dist/http/routes/session-memory-sync.js +157 -0
  78. package/dist/http/routes/sessions-list.d.ts +2 -1
  79. package/dist/http/routes/tasks.js +7 -5
  80. package/dist/http/run-read-face.d.ts +7 -0
  81. package/dist/http/run-read-face.js +9 -3
  82. package/dist/http/send.d.ts +5 -0
  83. package/dist/http/send.js +9 -2
  84. package/dist/http/server.d.ts +14 -8
  85. package/dist/http/server.js +2 -4
  86. package/dist/http/submit-replay.d.ts +4 -3
  87. package/dist/http/submit-replay.js +2 -2
  88. package/dist/http/sync-submit-body.d.ts +21 -4
  89. package/dist/http/sync-submit-body.js +13 -0
  90. package/dist/identity-source.d.ts +21 -2
  91. package/dist/identity-source.js +6 -0
  92. package/dist/memory-incident-sink.d.ts +19 -6
  93. package/dist/memory-incident-sink.js +8 -4
  94. package/dist/memory-sync-client.d.ts +21 -21
  95. package/dist/memory-sync-client.js +50 -42
  96. package/dist/memory-sync-identity.d.ts +24 -0
  97. package/dist/memory-sync-identity.js +57 -0
  98. package/dist/memory-sync.d.ts +7 -1
  99. package/dist/memory-sync.js +6 -3
  100. package/dist/observability/fail-open.d.ts +27 -10
  101. package/dist/observability/fail-open.js +20 -7
  102. package/dist/observability/run-terminal.d.ts +92 -34
  103. package/dist/observability/run-terminal.js +167 -162
  104. package/dist/plugins/approval-ask-store-file.d.ts +14 -4
  105. package/dist/plugins/approval-ask-store-file.js +48 -7
  106. package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
  107. package/dist/plugins/approval-ask-store-memory.js +9 -8
  108. package/dist/plugins/approval-ask-store-sql.d.ts +42 -10
  109. package/dist/plugins/approval-ask-store-sql.js +49 -19
  110. package/dist/plugins/file-run-store.d.ts +6 -1
  111. package/dist/plugins/file-run-store.js +11 -1
  112. package/dist/plugins/local-session-store.d.ts +36 -7
  113. package/dist/plugins/local-session-store.js +76 -32
  114. package/dist/plugins/local-task-attachment-store.d.ts +12 -2
  115. package/dist/plugins/local-task-attachment-store.js +67 -43
  116. package/dist/plugins/memory-key-guards.d.ts +3 -3
  117. package/dist/plugins/memory-run-store.d.ts +6 -1
  118. package/dist/plugins/memory-run-store.js +11 -1
  119. package/dist/plugins/one-time-migrations.d.ts +27 -1
  120. package/dist/plugins/one-time-migrations.js +57 -3
  121. package/dist/plugins/remote-env-e2b.d.ts +23 -4
  122. package/dist/plugins/remote-env-e2b.js +111 -28
  123. package/dist/plugins/remote-env-k8s.d.ts +31 -5
  124. package/dist/plugins/remote-env-k8s.js +86 -19
  125. package/dist/plugins/remote-env-local-docker.d.ts +15 -3
  126. package/dist/plugins/remote-env-local-docker.js +40 -12
  127. package/dist/plugins/run-store-sql.d.ts +9 -1
  128. package/dist/plugins/run-store-sql.js +9 -1
  129. package/dist/plugins/store-backend.d.ts +10 -7
  130. package/dist/plugins/store-backend.js +1 -0
  131. package/dist/plugins/store-contracts.d.ts +26 -1
  132. package/dist/plugins/store-contracts.js +3 -0
  133. package/dist/plugins/task-attachment-store.d.ts +112 -14
  134. package/dist/plugins/task-attachment-store.js +221 -28
  135. package/dist/plugins/workspace-fill.d.ts +60 -0
  136. package/dist/plugins/workspace-fill.js +106 -0
  137. package/dist/project-bundle.d.ts +38 -0
  138. package/dist/project-bundle.js +609 -0
  139. package/dist/run-local.js +2 -8
  140. package/dist/runs.js +1 -2
  141. package/dist/sandbox-egress-policy.d.ts +4 -1
  142. package/dist/sandbox-egress-policy.js +3 -1
  143. package/dist/sandbox-git-plane.d.ts +31 -12
  144. package/dist/sandbox-git-plane.js +24 -4
  145. package/dist/security.d.ts +9 -0
  146. package/dist/security.js +8 -0
  147. package/dist/session-error-wire.d.ts +24 -0
  148. package/dist/session-error-wire.js +7 -0
  149. package/dist/task-settings.d.ts +10 -2
  150. package/dist/task-settings.js +15 -2
  151. package/dist/terminal.d.ts +39 -19
  152. package/dist/terminal.js +65 -22
  153. package/dist/tool-approval.d.ts +4 -3
  154. package/dist/tool-approval.js +12 -14
  155. package/dist/trace/core-keyset-guard.d.ts +123 -6
  156. package/dist/trace/engine-notice-wire.d.ts +8 -2
  157. package/dist/trace/engine-notice-wire.js +4 -2
  158. package/dist/trace/exit-seat.d.ts +2 -1
  159. package/dist/trace/ledger-sink.d.ts +8 -4
  160. package/dist/trace/ledger-sink.js +2 -2
  161. package/dist/trace/project.d.ts +18 -1
  162. package/dist/trace/project.js +16 -0
  163. package/dist/trace/self-minted-wire.d.ts +251 -2
  164. package/dist/trace/self-minted-wire.js +82 -0
  165. package/dist/trace/wire-projection-faces.d.ts +1 -1
  166. package/dist/trace/wire-projection-faces.js +1 -1
  167. package/dist/web-admin-client/apply-effective.js +21 -1
  168. package/dist/web-admin-client/effective-keys.js +2 -0
  169. package/dist/web-admin-client/types.d.ts +4 -0
  170. package/package.json +1 -1
  171. package/wire-changes/server@7.142.0.json +639 -0
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { APPROVAL_ASK_AUDIT_DIR, APPROVAL_ASK_AUDIT_LEDGER, FileApprovalAskAuditStore } from "./approval-ask-audit-store.js";
4
4
  import { readStoredCardEnvelope } from "./approval-card.js";
5
5
  import { APPROVAL_ENDING_CAUSES } from "./approval-deny-reasons.js";
6
+ import { bindParkingAskAtGate } from "./approval-reconciler.js";
6
7
  import { writerGoneEvidenceFor } from "./user-input-ledger.js";
7
8
  import { recordFailOpen } from "./observability/fail-open.js";
8
9
  const CONVERGE_PAGE = 500;
@@ -17,57 +18,128 @@ export function openAuditEvidenceForConvergence(live, localDataRoot) {
17
18
  const opened = new FileApprovalAskAuditStore(localDataRoot);
18
19
  return { audit: opened, release: () => opened.close() };
19
20
  }
21
+ async function forEachRowInState(store, state, visit) {
22
+ const seen = new Set();
23
+ let stayed = 0;
24
+ for (;;) {
25
+ const page = (await store.listByState(state, stayed + CONVERGE_PAGE)).filter((r) => !seen.has(r.askId));
26
+ if (page.length === 0)
27
+ return;
28
+ for (const row of page) {
29
+ seen.add(row.askId);
30
+ if (!(await visit(row)))
31
+ stayed += 1;
32
+ }
33
+ }
34
+ }
35
+ function refusingToStart(state, row, err) {
36
+ return new Error(`approval crash convergence: could not close the previous process's ${state === "STREAM_PENDING" ? "pending" : "parking"} approval ask ${row.askId} (task ${row.taskId}): ` +
37
+ `${err instanceof Error ? err.message : String(err)} — refusing to start, because serving now would leave a card that nothing waits for ` +
38
+ "decidable with a 200. Fix the data root (disk full / read-only?) and start again.");
39
+ }
20
40
  export async function convergeCrashedAsksAtBoot(input) {
21
41
  const now = input.now ?? Date.now;
22
- const audited = input.audit?.convergeCrashedAsks() ?? { pending: 0, decided: 0, inconsistent: 0, corruptLines: 0 };
42
+ const audit = input.audit;
43
+ const audited = audit?.convergeCrashedAsks() ?? { pending: 0, decided: 0, inconsistent: 0, corruptLines: 0 };
23
44
  const asksVoided = { [APPROVAL_ENDING_CAUSES.CRASHED_BEFORE_PARK]: 0, [APPROVAL_ENDING_CAUSES.SHUTDOWN_BEFORE_PARK]: 0 };
24
45
  let causeUnreadable = 0;
46
+ let parkingBound = 0;
25
47
  const store = input.askStore;
26
48
  if (store !== undefined) {
27
- const seen = new Set();
28
- for (;;) {
29
- const page = (await store.listByState("STREAM_PENDING", CONVERGE_PAGE)).filter((r) => !seen.has(r.askId));
30
- if (page.length === 0)
31
- break;
32
- for (const row of page) {
33
- seen.add(row.askId);
49
+ await forEachRowInState(store, "STREAM_PENDING", async (row) => {
50
+ const envelope = readStoredCardEnvelope(row.cardJson);
51
+ if (envelope === undefined) {
52
+ causeUnreadable += 1;
53
+ recordFailOpen("server.approval-crash-convergence.cause-unreadable", `ask ${row.askId} (task ${row.taskId}): card envelope unreadable — cause defaulted to ${APPROVAL_ENDING_CAUSES.CRASHED_BEFORE_PARK}`);
54
+ }
55
+ const reason = envelope !== undefined && audit !== undefined ? audit.holderGoneVerdict(envelope.approvalId).cause : APPROVAL_ENDING_CAUSES.CRASHED_BEFORE_PARK;
56
+ let won;
57
+ try {
58
+ won = await store.transitionAsk(row.askId, "STREAM_PENDING", "VOID", { endingCause: reason, updatedAtMs: now() });
59
+ }
60
+ catch (err) {
61
+ throw refusingToStart("STREAM_PENDING", row, err);
62
+ }
63
+ if (won)
64
+ asksVoided[reason] += 1;
65
+ return true;
66
+ });
67
+ if (audit !== undefined) {
68
+ const orphanOf = (row) => {
34
69
  const envelope = readStoredCardEnvelope(row.cardJson);
35
- if (envelope === undefined) {
36
- causeUnreadable += 1;
37
- recordFailOpen("server.approval-crash-convergence.cause-unreadable", `ask ${row.askId} (task ${row.taskId}): card envelope unreadable — cause defaulted to ${APPROVAL_ENDING_CAUSES.CRASHED_BEFORE_PARK}`);
70
+ if (envelope === undefined)
71
+ return undefined;
72
+ const verdict = audit.holderGoneVerdict(envelope.approvalId);
73
+ return verdict.orphan ? { approvalId: envelope.approvalId, cause: verdict.cause } : undefined;
74
+ };
75
+ let voidedSiblings = [];
76
+ const gate = {
77
+ askStore: store,
78
+ checkpoints: input.checkpoints,
79
+ warn: (event, fields) => input.logger.warn(event, fields),
80
+ revoke: (_row, askIds) => {
81
+ voidedSiblings = askIds;
82
+ },
83
+ };
84
+ await forEachRowInState(store, "PARKING", async (row) => {
85
+ const orphan = orphanOf(row);
86
+ if (orphan === undefined)
87
+ return false;
88
+ voidedSiblings = [];
89
+ let bound;
90
+ try {
91
+ bound = (await bindParkingAskAtGate(gate, row, now())).bound;
38
92
  }
39
- const reason = envelope !== undefined && input.audit !== undefined ? input.audit.causeFor(envelope.approvalId) : APPROVAL_ENDING_CAUSES.CRASHED_BEFORE_PARK;
93
+ catch (err) {
94
+ throw refusingToStart("PARKING", row, err);
95
+ }
96
+ if (!bound)
97
+ return false;
98
+ parkingBound += 1;
99
+ input.logger.warn("approval_crash_convergence_park_committed", {
100
+ askId: row.askId,
101
+ taskId: row.taskId,
102
+ approvalId: orphan.approvalId,
103
+ voidedSiblings,
104
+ note: "the audit ledger saw no settlement for this approval ask (an orphan of the previous process), but its park row had committed — bound PARKED, decidable through the durable approval endpoint; the commit landed in the window between the park commit and its observation. Same-batch siblings still parking were voided as superseded by this park (batch_bound_elsewhere)",
105
+ });
106
+ return true;
107
+ });
108
+ await forEachRowInState(store, "PARKING", async (row) => {
109
+ const orphan = orphanOf(row);
110
+ if (orphan === undefined)
111
+ return false;
40
112
  let won;
41
113
  try {
42
- won = await store.transitionAsk(row.askId, "STREAM_PENDING", "VOID", { deniedReason: reason, updatedAtMs: now() });
114
+ won = await store.transitionAsk(row.askId, "PARKING", "VOID", { endingCause: orphan.cause, updatedAtMs: now() });
43
115
  }
44
116
  catch (err) {
45
- throw new Error(`approval crash convergence: could not close the previous process's pending approval ask ${row.askId} (task ${row.taskId}): ` +
46
- `${err instanceof Error ? err.message : String(err)} — refusing to start, because serving now would leave a card that nothing waits for ` +
47
- "decidable with a 200. Fix the data root (disk full / read-only?) and start again.");
117
+ throw refusingToStart("PARKING", row, err);
48
118
  }
49
119
  if (won)
50
- asksVoided[reason] += 1;
51
- }
120
+ asksVoided[orphan.cause] += 1;
121
+ return true;
122
+ });
52
123
  }
53
124
  }
54
- return { ...audited, asksVoided, causeUnreadable };
125
+ return { ...audited, asksVoided, causeUnreadable, parkingBound };
55
126
  }
56
127
  export async function runBootCrashConvergence(ctx) {
57
128
  const { backend, runStore, logger } = ctx;
58
129
  if (backend === undefined || runStore === undefined || writerGoneEvidenceFor(backend.kind, runStore).kind !== "exclusive_data_root")
59
130
  return undefined;
60
131
  const evidence = openAuditEvidenceForConvergence(ctx.liveAudit, ctx.localDataRoot);
61
- const converged = await convergeCrashedAsksAtBoot({ askStore: backend.approvalAsk(), audit: evidence.audit }).finally(evidence.release);
62
- if (converged.pending > 0 || converged.decided > 0 || converged.inconsistent > 0 || converged.corruptLines > 0 || totalAsksVoided(converged) > 0) {
132
+ const converged = await convergeCrashedAsksAtBoot({ askStore: backend.approvalAsk(), audit: evidence.audit, checkpoints: backend.checkpointReadView(), logger }).finally(evidence.release);
133
+ if (converged.pending > 0 || converged.decided > 0 || converged.inconsistent > 0 || converged.corruptLines > 0 || totalAsksVoided(converged) > 0 || converged.parkingBound > 0) {
63
134
  logger.warn("approval_ask_audit_crash_converged", {
64
135
  pending: converged.pending,
65
136
  decided: converged.decided,
66
137
  inconsistent: converged.inconsistent,
67
138
  corruptLines: converged.corruptLines,
68
139
  asksVoided: converged.asksVoided,
140
+ parkingBound: converged.parkingBound,
69
141
  causeUnreadable: converged.causeUnreadable,
70
- note: "approval asks left pending by the previous process were converged: each such ask is now VOID in the ask ledger (the decision endpoint answers 410 gone.ask with this cause) and is listed under GET /v1/approvals crashConverged when the audit ledger saw it (cause crashed_before_park = the process crashed; cause shutdown_before_park = an orderly shutdown cut the leg off before it closed or before its park committed)",
142
+ note: "approval asks left by the previous process were converged: each pending ask, and each parking ask the audit ledger judged an orphan, is now VOID in the ask ledger (the decision endpoint answers 410 gone.ask with this cause) and is listed under GET /v1/approvals crashConverged when the audit ledger saw it (cause crashed_before_park = the process crashed; cause shutdown_before_park = the previous process completed its orderly shutdown wind-down while the ask was still unsettled); a parking ask whose park row had committed is bound PARKED instead",
71
143
  });
72
144
  }
73
145
  return converged;
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * ## 它治的病(同一件事、四张词表、一词多义)
5
5
  * 一张卡没被人答就结束了,今天有四个读面各说各话:审计 settle 行(`expired` 一词盖 TTL 到期 / run 取消 / run 退出清理 / 广播全灭,批内兄弟被撤卡也是同一个词)、
6
- * 问账 VOID/DENIED 行(`denied_reason` 列)、晚答 404 的判别位、决断口 409 的码(多种成因借 `approval_binding_mismatch` 一个名)。壳读账写不出「已取消」,
6
+ * 问账 VOID/DENIED 行(成因列)、晚答 404 的判别位、决断口 409 的码(多种成因借 `approval_binding_mismatch` 一个名)。壳读账写不出「已取消」,
7
7
  * 也分不出「重取坐标再决」与「改答案再决」。收成**一张表、一个词、各面同读**:
8
8
  * · 审计 settle 行的 `cause`(非人结局必填,人答缺席;`settle(verdict)` 判别联合逼每个非人调用点点名一个)与活卡结局帧 `tool_approval_complete` 的 additive `cause`(同一个词,同一条律);
9
- * · 问账 VOID/DENIED 行的 `denied_reason` 列 → 决断口 410 `gone.ask` 体的 `cause` 与晚答 404 体的 `cause`(读同一列,经唯一读口 {@link causeOfAskRow});
9
+ * · 问账 VOID/DENIED 行的 `ending_cause` 列 → 决断口 410 `gone.ask` 体的 `cause` 与晚答 404 体的 `cause`(读同一列,经唯一读口 {@link askRowEndingCause});
10
10
  * · `GET /v1/approvals` `crashConverged[].cause`(本表的 {@link HolderGoneCause} 两词子集);
11
11
  * · 决断口 409 的码:`binding_changed` / `answer_invalid` / `outcome_cell_invalid` 三词各归一个码。`retriable` 不由成因说 —— 三词都出自 core 的
12
12
  * `checkpoint.invalid_outcome`,由 `http/resume-legs.ts` 的 core 码 → 处置一张表派生(一个意思:卡仍 pending、行重新 park)。
@@ -16,8 +16,9 @@
16
16
  * · 成因**只从事实读**:run 行 `errorCode:"cancelled"`、钟到、emit 零接收、停车行 / 问账行的列、`detail.field`;不读拒句散文。
17
17
  * · 「停驻行的格式比本引擎新」**不是**本表的词:它只有 core 一个来源(`checkpoint.unsupported_version` + `detail.reason:"version_newer"`,原码镜像);服务层不从续跑上下文的写者版本推断成因。
18
18
  * · **人答的结局(`allowed` / `denied`)没有 cause**:`cause` 在场 ⇔ 非人结局。
19
- * · 落列面:问账行的成因写进 `approval_ask.denied_reason`(VARCHAR(64)),一列按 `state` 承载两种意思 —— VOID / DENIED 行上是**终态成因**,PARKING / PARKED 行上是
20
- * **转 park 的触发成因**;读它只经 {@link causeOfAskRow}(按 state 穷举分派,两张读词表只被它读)。列名将在下一个 DDL 窗按意义改名。
19
+ * · 落列面:问账行的成因落**两列,一列一义**(各 VARCHAR(64)):`approval_ask.ending_cause` = **终态成因**(这一行为什么没被人答就结束了;到 `VOID` / `DENIED` 的转移带着它写),
20
+ * `approval_ask.park_trigger` = **转 park 的触发成因**(这一行为什么被转进投递面;只由那一次 `claimTerminal(expire, { cause })` 写,此后不再被写 —— 终态不覆盖它)。
21
+ * 两只读口各读一列:{@link askRowEndingCause} / {@link askRowParkTrigger}(两张读词表各只被一只读)。
21
22
  *
22
23
  * ## 为什么 `routing_failure_fail_closed` 也在表里(归因诚实性)
23
24
  * 审计面唯一能证明「超时永不产生 denial」的东西是**终态 + 成因**的组合:人拒恒为 `DECIDED(decision=deny)`,路由失败恒为 `DENIED(routing_failure_fail_closed)`,
@@ -71,11 +72,15 @@ export declare const APPROVAL_ENDING_CAUSES: {
71
72
  */
72
73
  readonly ROUTING_FAILURE: "routing_failure_fail_closed";
73
74
  /**
74
- * 启动收敛(`approval-crash-convergence.ts`,local 车道):**承载这一问的进程没了** —— 前世进程崩溃 / 被杀,留下的 `STREAM_PENDING` 行在下一次启动、任何卡能开始之前收成 VOID。
75
- * 证据是数据根 `LOCK`(前世写者全死),不是「等够久了」。值与 `GET /v1/approvals` `crashConverged[].cause` 是**同一个词**。
75
+ * 启动收敛(`approval-crash-convergence.ts`,local 车道):**承载这一问的进程没了** —— 前世进程崩溃 / 被杀,留下的 `STREAM_PENDING` 行与审计判为孤儿的 `PARKING` 行
76
+ * (停车行没提交)在下一次启动、任何卡能开始之前收成 VOID。证据是数据根 `LOCK`(前世写者全死)+ 审计账的孤儿判定,不是「等够久了」。
77
+ * 值与 `GET /v1/approvals` `crashConverged[].cause` 是**同一个词**(一处判:审计店的 `holderGoneVerdict`)。
76
78
  */
77
79
  readonly CRASHED_BEFORE_PARK: "crashed_before_park";
78
- /** 同上,但承载它的腿是被**有序关停**截断的(审计账本上有本腿的关停记号;关停收尾没能在宽限窗内把这张卡结清),或有序关停期间这张卡被改路停驻(settle 行的成因)。 */
80
+ /**
81
+ * 同上,但前世走完了**有序关停**的审批收尾(审计账本上那一世的进程级关停记录落在这张卡的末事件之后 —— 收尾没能在宽限窗内把它结清;是进程的事实,
82
+ * 不看承载它的腿在不在追踪视野里),或有序关停期间这张卡被改路停驻(settle 行 / 问账行 PARKING 的触发成因)。
83
+ */
79
84
  readonly SHUTDOWN_BEFORE_PARK: "shutdown_before_park";
80
85
  /** 判据 5:任何在 `STREAM_APPROVAL_ORPHAN_TTL_MS` 内仍未收敛的 `PARKING` 行(量 immutable 的 `createdAtMs`,§9 C5)—— 遗孤最终可判的最后兜底,带一条 warn。 */
81
86
  readonly ORPHAN_TTL_EXCEEDED: "orphan_ttl_exceeded";
@@ -117,7 +122,7 @@ export declare const SETTLE_CAUSE_WORDS: {
117
122
  };
118
123
  /** 一个值是不是非人结局的成因词(审计账读盘用这一只判词)。 */
119
124
  export declare function isSettleCause(v: unknown): v is SettleCause;
120
- /** 「承载这一问的进程没了」的两个成因词(启动收敛写进问账行 `denied_reason`,审计收敛行 `cause` 读同一型)。 */
125
+ /** 「承载这一问的进程没了」的两个成因词(启动收敛写进问账行 `ending_cause`(前世的 `STREAM_PENDING` 与审计判孤儿的 `PARKING`),审计收敛行 `cause` 读同一型)。 */
121
126
  export type HolderGoneCause = typeof APPROVAL_ENDING_CAUSES.CRASHED_BEFORE_PARK | typeof APPROVAL_ENDING_CAUSES.SHUTDOWN_BEFORE_PARK;
122
127
  /** 对账器 `DENIED` 终态的成因(单员:全仓 DENIED 只有一个写点)。 */
123
128
  export type ReconcileDenyCause = typeof APPROVAL_ENDING_CAUSES.ROUTING_FAILURE;
@@ -128,7 +133,7 @@ export type ReconcileVoidCause = typeof APPROVAL_ENDING_CAUSES.RUN_CANCELLED | t
128
133
  * (取消赢下 VOID ⇒ `run_cancelled` / `run_exited`,兄弟被撤卡 ⇒ `batch_sibling_withdrawn`)。窗到期 / 广播全灭 / 决断类的词只落审计 settle 行或 409 码,不落这一列。
129
134
  */
130
135
  export type AskRowEndingCause = ReconcileDenyCause | ReconcileVoidCause | HolderGoneCause | typeof APPROVAL_ENDING_CAUSES.RUN_EXITED | typeof APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN;
131
- /** 问账行 `denied_reason` 列在 VOID / DENIED 行上的读词表({@link AskRowEndingCause} 的穷举,`satisfies` ⇒ 子集加词而本表没跟 = 编译红)。唯一读者 = {@link causeOfAskRow}。 */
136
+ /** 问账行 `ending_cause` 列的读词表({@link AskRowEndingCause} 的穷举,`satisfies` ⇒ 子集加词而本表没跟 = 编译红)。唯一读者 = {@link askRowEndingCause}。 */
132
137
  export declare const ASK_ROW_ENDING_CAUSE_WORDS: {
133
138
  readonly routing_failure_fail_closed: true;
134
139
  readonly run_cancelled: true;
@@ -142,12 +147,12 @@ export declare const ASK_ROW_ENDING_CAUSE_WORDS: {
142
147
  readonly shutdown_before_park: true;
143
148
  };
144
149
  /**
145
- * `expire` 终局 claim(窗到期 / 广播全灭 / 有序关停把一张卡转投递面 = `PARKING`)赢下时写进这一行 `denied_reason` 的**触发成因**:这一行此后若走到 `DENIED` / `VOID`,终态成因会覆盖它
146
- * ({@link AskRowEndingCause});`PARKED` 行的这一格就是「它为什么被转 park」。跨副本的读者(另一进程的悬挂腿轮询、CAS 败方读数)读到 `PARKING` / `PARKED` 时靠它归因,
147
- * 不替没写的行猜一个词。
150
+ * `expire` 终局 claim(窗到期 / 广播全灭 / 断连 / 呈不出 / 有序关停把一张卡转投递面 = `PARKING`)赢下时写进这一行 `park_trigger` 的**触发成因**:此后不再被写 ——
151
+ * 这一行走到 `PARKED` / `DENIED` / `VOID` 之后它仍是「这一行为什么被转 park」(终态成因在另一列,{@link AskRowEndingCause})。跨副本的读者(另一进程的悬挂腿轮询、
152
+ * CAS 败方读数)读到 `PARKING` / `PARKED` 时靠它归因,不替没写的行猜一个词。
148
153
  */
149
154
  export type ParkTriggerCause = typeof APPROVAL_ENDING_CAUSES.TTL_EXPIRED | typeof APPROVAL_ENDING_CAUSES.BROADCAST_LOST | typeof APPROVAL_ENDING_CAUSES.STREAM_DISCONNECTED | typeof APPROVAL_ENDING_CAUSES.UNDELIVERABLE | typeof APPROVAL_ENDING_CAUSES.SHUTDOWN_BEFORE_PARK;
150
- /** 问账行 `denied_reason` 在 `PARKING` / `PARKED` 行上的读词表({@link ParkTriggerCause} 的穷举)。唯一读者 = {@link causeOfAskRow}。 */
155
+ /** 问账行 `park_trigger` 列的读词表({@link ParkTriggerCause} 的穷举)。唯一读者 = {@link askRowParkTrigger}。 */
151
156
  export declare const PARK_TRIGGER_CAUSE_WORDS: {
152
157
  readonly ttl_expired: true;
153
158
  readonly broadcast_lost: true;
@@ -155,25 +160,24 @@ export declare const PARK_TRIGGER_CAUSE_WORDS: {
155
160
  readonly undeliverable: true;
156
161
  readonly shutdown_before_park: true;
157
162
  };
158
- /** 问账行成因的读数:`kind` 说这一个词在这一行上是哪种意思(终态成因 / 转 park 的触发成因)。 */
159
- export type AskRowCause = {
160
- readonly kind: "ending";
161
- readonly cause: AskRowEndingCause;
162
- } | {
163
- readonly kind: "park_trigger";
164
- readonly cause: ParkTriggerCause;
165
- };
166
163
  /**
167
- * 🔴 **问账行成因的唯一读口**:`denied_reason` 一列按 `state` 承载两种意思,本函数按 `state` 穷举分派(加状态 ⇒ 这里编译红),读者不自己先判 state 再查表:
168
- * · `VOID` / `DENIED` ⇒ 终态成因({@link AskRowEndingCause};决断口 410、晚答 404、CAS 败方读数、悬挂腿的轮询读这一种);
169
- * · `PARKING` / `PARKED` ⇒ 转 park 的触发成因({@link ParkTriggerCause};跨副本的悬挂腿与 CAS 败方读这一种);
170
- * · `DECIDED` / `STREAM_PENDING` / `absent`(败方读数的「行不在」)⇒ 无成因。
171
- * 列值 `null` / 键缺席(没写归因的存量行)或与 state 不配的词(手改 / 将来版本写的词)⇒ `undefined`(读面缺席即缺席,不替没写的行编一个成因)。返回纯数据。
164
+ * 🔴 **终态成因的唯一读口**(决断口 410、晚答 404、`GET` 读面、CAS 败方读数、悬挂腿的轮询读这一只):只读 `ending_cause` 一列,不看别的列。
165
+ * 行不变式由本口守:终态成因只在 `VOID` / `DENIED` 上作数(写者只在到这两态的转移上写它;别的态上读到 ⇒ 不是这一行的事实)—— 按 `state` 穷举(加状态 ⇒ 这里编译红)。
166
+ * 列值 `null` / 键缺席(没写归因的行)、词表外的词(手改 / 将来版本写的词)或与 state 不配 ⇒ `undefined`(读面缺席即缺席,不替没写的行编一个成因)。返回纯数据。
167
+ */
168
+ export declare function askRowEndingCause(row: {
169
+ readonly state: AskState;
170
+ readonly endingCause?: string | null;
171
+ }): AskRowEndingCause | undefined;
172
+ /**
173
+ * 🔴 **转 park 触发成因的唯一读口**(跨副本的悬挂腿、CAS 败方读数读这一只):只读 `park_trigger` 一列,不看别的列。
174
+ * 行不变式由本口守:触发成因只在**走过 `PARKING`** 的行上作数 —— `PARKING` / `PARKED`,以及从 `PARKING` 收成的 `DENIED` / `VOID`(终态不覆盖它);
175
+ * `STREAM_PENDING` / `DECIDED`(状态机上没有经 `PARKING` 到它们的边)上读到 ⇒ `undefined`。其余与 {@link askRowEndingCause} 同律。
172
176
  */
173
- export declare function causeOfAskRow(row: {
174
- readonly state: AskState | "absent";
175
- readonly deniedReason?: string | null;
176
- }): AskRowCause | undefined;
177
+ export declare function askRowParkTrigger(row: {
178
+ readonly state: AskState;
179
+ readonly parkTrigger?: string | null;
180
+ }): ParkTriggerCause | undefined;
177
181
  /** 非人**结局**的成因(审计 settle 行 `cause`、活卡结局帧 `tool_approval_complete.cause`):闭集去掉决断口 409 的三个词({@link DecisionRefusalCause})—— 那三个词不结束一张卡,只拒一次决议。 */
178
182
  export type SettleCause = Exclude<ApprovalEndingCause, DecisionRefusalCause>;
179
183
  /** 决断口 409 的成因子集(core 的 `detail.field` → 码;每词一个码,见 `http/resume-legs.ts`)。 */
@@ -63,22 +63,37 @@ export const PARK_TRIGGER_CAUSE_WORDS = {
63
63
  function isParkTriggerCause(v) {
64
64
  return Object.hasOwn(PARK_TRIGGER_CAUSE_WORDS, v);
65
65
  }
66
- export function causeOfAskRow(row) {
67
- const stored = row.deniedReason;
66
+ export function askRowEndingCause(row) {
67
+ const stored = row.endingCause;
68
68
  switch (row.state) {
69
69
  case "VOID":
70
70
  case "DENIED":
71
- return stored !== null && stored !== undefined && isAskRowEndingCause(stored) ? { kind: "ending", cause: stored } : undefined;
71
+ return stored !== null && stored !== undefined && isAskRowEndingCause(stored) ? stored : undefined;
72
72
  case "PARKING":
73
73
  case "PARKED":
74
- return stored !== null && stored !== undefined && isParkTriggerCause(stored) ? { kind: "park_trigger", cause: stored } : undefined;
75
74
  case "DECIDED":
76
75
  case "STREAM_PENDING":
77
- case "absent":
78
76
  return undefined;
79
77
  default: {
80
78
  const unhandled = row.state;
81
- throw new Error(`causeOfAskRow: unhandled ask row state ${String(unhandled)}`);
79
+ throw new Error(`askRowEndingCause: unhandled ask row state ${String(unhandled)}`);
80
+ }
81
+ }
82
+ }
83
+ export function askRowParkTrigger(row) {
84
+ const stored = row.parkTrigger;
85
+ switch (row.state) {
86
+ case "PARKING":
87
+ case "PARKED":
88
+ case "DENIED":
89
+ case "VOID":
90
+ return stored !== null && stored !== undefined && isParkTriggerCause(stored) ? stored : undefined;
91
+ case "DECIDED":
92
+ case "STREAM_PENDING":
93
+ return undefined;
94
+ default: {
95
+ const unhandled = row.state;
96
+ throw new Error(`askRowParkTrigger: unhandled ask row state ${String(unhandled)}`);
82
97
  }
83
98
  }
84
99
  }
@@ -29,6 +29,6 @@ import type { ParkTriggerCause } from "./approval-deny-reasons.js";
29
29
  * `batchId` 必须是**行上**的批(幂等命中的存量行可能是别的政策 / 旧版本铸的整腿批,错 scope 的 claim 如实 `absent`)。
30
30
  */
31
31
  export declare function settleByPolicy(store: Pick<ApprovalAskStore, "claimTerminal">, row: Pick<AskRow, "askId" | "batchId">, policy: UnattendedApprovalPolicy,
32
- /** 转 park 的触发成因(窗到期 / 广播全灭 / 有序关停):`park` 政策的 `expire` 赢下 PARKING 时随同一步写进行(`denied_reason`),跨副本的读者据它归因;`deny` 政策落的是一条决议,不带。 */
32
+ /** 转 park 的触发成因(窗到期 / 广播全灭 / 有序关停):`park` 政策的 `expire` 赢下 PARKING 时随同一步写进行(`park_trigger`),跨副本的读者据它归因;`deny` 政策落的是一条决议,不带。 */
33
33
  cause: ParkTriggerCause): Promise<AskTerminalClaimOutcome>;
34
34
  //# sourceMappingURL=approval-policy-settlement.d.ts.map
@@ -123,6 +123,17 @@ export declare function classifyGateMatch(ask: AskRow, candidates: readonly Chec
123
123
  export declare function selectGateCandidate(ask: AskRow, candidates: readonly CheckpointAskCandidate[]): CheckpointAskCandidate | undefined;
124
124
  /** 判据表 v2 的判定(纯函数;顺序 = 设计稿 §9 尾的五臂汇总,注见文件头)。 */
125
125
  export declare function decideReconcileAction(input: ReconcileInput): ReconcileAction;
126
+ /** 判据 1 执行体({@link bindParkingAskAtGate})一次尝试的结果。没绑上 ⇒ 交回批的真实态,周期扫据它续判 ②③④⑤。 */
127
+ export type GateBindAttempt = {
128
+ bound: true;
129
+ match: GateMatchOutcome;
130
+ } | {
131
+ bound: false;
132
+ match?: GateMatchOutcome;
133
+ batchState: BatchState | "MISSING" | null;
134
+ batchBoundAskId: string | null;
135
+ irreversibleBatch: boolean;
136
+ };
126
137
  /** 收敛器一次 tick 的产出(可观测 + 测试断言面)。 */
127
138
  export interface ReconcileStats {
128
139
  /** 段一扫到的 `PARKING` 行数。 */
@@ -202,7 +213,7 @@ export type ApprovalRevokeEmitter = (frame: ApprovalRevokeFrame, target: {
202
213
  sessionId: string;
203
214
  taskId: string;
204
215
  }) => void;
205
- /** 判据 1 的读口(窄接口,不绑具体 checkpoint 店实现——local 车道的店没有这个面,装配点传 undefined)。 */
216
+ /** 判据 1 的读口(窄接口,不绑具体 checkpoint 店实现;三个后端的 checkpoint 店都有这一面,后端的只读口 `checkpointReadView()` 也交它 —— 起服收敛读那一口)。 */
206
217
  export interface ReconcileCheckpointPort {
207
218
  findCheckpointCandidatesForAsk(scope: string, sessionId: string, toolCallId: string, sinceMs: number): Promise<CheckpointAskCandidate[]>;
208
219
  }
@@ -245,5 +256,26 @@ export interface ApprovalReconciler {
245
256
  * 段级错误由调用方(reaper 腿的 throttled catch)兜。 */
246
257
  runOnce(nowMs: number): Promise<ReconcileStats>;
247
258
  }
259
+ /**
260
+ * {@link bindParkingAskAtGate} 的依赖(三个调用方交同一形):批 / 绑两口、判据 1 的候选读口、warn 与撤卡两个出口。
261
+ */
262
+ export interface GateBindDeps {
263
+ readonly askStore: Pick<ApprovalAskStore, "getBatch" | "bindBatch">;
264
+ /** 缺席 ⇒ 候选集恒空 ⇒ 判据 1 恒不命中(诚实降级,与 {@link ApprovalReconcilerDeps.checkpoints} 同一句)。 */
265
+ readonly checkpoints: ReconcileCheckpointPort | undefined;
266
+ /** warn 的出口(调用方保证它不抛 —— 可观测面绝不成为故障源)。 */
267
+ readonly warn: (event: string, fields: Record<string, unknown>) => void;
268
+ /** 被 `bindBatch` 连坐的兄弟的撤卡帧(帧是通知,真源是行;失败不回滚 CAS)。 */
269
+ readonly revoke: (row: AskRow, askIds: readonly string[], reason: ApprovalRevokeFrame["reason"], nowMs: number) => void;
270
+ }
271
+ /**
272
+ * 🔴 **判据 1 的唯一执行体**([ref] 起一只判官;[ref] 起三个调用方):⓪ 批态先读 → 候选读口 → {@link classifyGateMatch} → 命中即 `bindBatch`
273
+ * (+ 被连坐兄弟的撤卡帧)。调用方:
274
+ * · 周期扫(`createApprovalReconciler` 的 `reconcileOne`)—— 不命中 / 绑失败时拿回批的真实态,续判 ②③④⑤;
275
+ * · park 提交那一拍(`bindCommittedPark`)—— 只问「这一行此刻能不能收口」,不命中即止(其余臂是周期扫的事);
276
+ * · local 车道起服收敛(`approval-crash-convergence.ts`)—— 审计判为孤儿的前世 `PARKING` 行:命中 ⇒ `PARKED`;不命中 ⇒ 调用方收 `VOID`。
277
+ * 判据、读口、写口一个字都不分叉:事件路径与起服都是本函数的即时调用,不是第二份判据。
278
+ */
279
+ export declare function bindParkingAskAtGate(deps: GateBindDeps, ask: AskRow, nowMs: number, deadlineMs?: number): Promise<GateBindAttempt>;
248
280
  export declare function createApprovalReconciler(deps: ApprovalReconcilerDeps): ApprovalReconciler;
249
281
  //# sourceMappingURL=approval-reconciler.d.ts.map
@@ -79,6 +79,45 @@ export function decideReconcileAction(input) {
79
79
  void candidates;
80
80
  return { kind: "hold" };
81
81
  }
82
+ export async function bindParkingAskAtGate(deps, ask, nowMs, deadlineMs) {
83
+ const { askStore, checkpoints, warn, revoke } = deps;
84
+ const expired = () => deadlineMs !== undefined && Date.now() >= deadlineMs;
85
+ const batchRow = await withDeadline(askStore.getBatch(ask.batchId), "getBatch");
86
+ let batchState = batchRow?.state ?? "MISSING";
87
+ let batchBoundAskId = batchRow?.boundAskId ?? null;
88
+ const irreversibleBatch = batchState === "ABORTED" || (batchState === "ROUTING_BOUND" && batchBoundAskId !== null && batchBoundAskId !== ask.askId);
89
+ let gateMatch;
90
+ if (checkpoints && !irreversibleBatch && !expired()) {
91
+ const candidates = await withDeadline(checkpoints.findCheckpointCandidatesForAsk(encodeCheckpointScope(ask.owner), ask.sessionId, ask.toolCallId, ask.createdAtMs), "findCheckpointCandidatesForAsk");
92
+ gateMatch = classifyGateMatch(ask, candidates);
93
+ if (gateMatch.kind === "hash_mismatch") {
94
+ warn("approval_reconcile_hash_mismatch", {
95
+ askId: ask.askId,
96
+ taskId: ask.taskId,
97
+ toolCallId: ask.toolCallId,
98
+ candidates: gateMatch.candidates,
99
+ reason: "ask and checkpoint minted the same bound input under the same (sourceTaskId, toolCallId) identity but the digests differ — " +
100
+ "the known causes are deployment-side (a hook/policy mutating an object it still holds a reference to between the two mints, " +
101
+ "or a stateful getter returning a fresh value per read), not out-of-band interference; this ask keeps its fail-safe route (no park, no denial)",
102
+ });
103
+ }
104
+ const match = gateMatch.kind === "match" ? gateMatch.candidate : undefined;
105
+ if (match && !expired()) {
106
+ const res = await withDeadline(askStore.bindBatch(ask.batchId, ask.askId, {
107
+ gateToken: match.token,
108
+ gateBoundCallId: match.boundCallId,
109
+ gateBoundInputHash: match.boundInputHash,
110
+ }), "bindBatch");
111
+ if (res.ok) {
112
+ revoke(ask, res.voidedSiblings, "superseded_by_park", nowMs);
113
+ return { bound: true, match: gateMatch };
114
+ }
115
+ batchState = res.batchState;
116
+ batchBoundAskId = res.boundAskId ?? null;
117
+ }
118
+ }
119
+ return { bound: false, ...(gateMatch !== undefined ? { match: gateMatch } : {}), batchState, batchBoundAskId, irreversibleBatch };
120
+ }
82
121
  export function createApprovalReconciler(deps) {
83
122
  const { askStore, checkpoints, runs, logger, metrics, batchLimit, pendingGraceMs, adhocGraceMs, orphanTtlMs, unattendedPolicy, emitRevoke } = deps;
84
123
  const warn = (event, fields) => {
@@ -98,44 +137,8 @@ export function createApprovalReconciler(deps) {
98
137
  warn("approval_revoke_emit_failed", { batchId: row.batchId, count: askIds.length, err: err instanceof Error ? err.message : String(err) });
99
138
  }
100
139
  };
101
- const bindAtGate = async (ask, nowMs, deadlineMs) => {
102
- const expired = () => deadlineMs !== undefined && Date.now() >= deadlineMs;
103
- const batchRow = await withDeadline(askStore.getBatch(ask.batchId), "getBatch");
104
- let batchState = batchRow?.state ?? "MISSING";
105
- let batchBoundAskId = batchRow?.boundAskId ?? null;
106
- const irreversibleBatch = batchState === "ABORTED" || (batchState === "ROUTING_BOUND" && batchBoundAskId !== null && batchBoundAskId !== ask.askId);
107
- let gateMatch;
108
- if (checkpoints && !irreversibleBatch && !expired()) {
109
- const candidates = await withDeadline(checkpoints.findCheckpointCandidatesForAsk(encodeCheckpointScope(ask.owner), ask.sessionId, ask.toolCallId, ask.createdAtMs), "findCheckpointCandidatesForAsk");
110
- gateMatch = classifyGateMatch(ask, candidates);
111
- if (gateMatch.kind === "hash_mismatch") {
112
- warn("approval_reconcile_hash_mismatch", {
113
- askId: ask.askId,
114
- taskId: ask.taskId,
115
- toolCallId: ask.toolCallId,
116
- candidates: gateMatch.candidates,
117
- reason: "ask and checkpoint minted the same bound input under the same (sourceTaskId, toolCallId) identity but the digests differ — " +
118
- "the known causes are deployment-side (a hook/policy mutating an object it still holds a reference to between the two mints, " +
119
- "or a stateful getter returning a fresh value per read), not out-of-band interference; this ask keeps its fail-safe route (no park, no denial)",
120
- });
121
- }
122
- const match = gateMatch.kind === "match" ? gateMatch.candidate : undefined;
123
- if (match && !expired()) {
124
- const res = await withDeadline(askStore.bindBatch(ask.batchId, ask.askId, {
125
- gateToken: match.token,
126
- gateBoundCallId: match.boundCallId,
127
- gateBoundInputHash: match.boundInputHash,
128
- }), "bindBatch");
129
- if (res.ok) {
130
- revoke(ask, res.voidedSiblings, "superseded_by_park", nowMs);
131
- return { bound: true, match: gateMatch };
132
- }
133
- batchState = res.batchState;
134
- batchBoundAskId = res.boundAskId ?? null;
135
- }
136
- }
137
- return { bound: false, ...(gateMatch !== undefined ? { match: gateMatch } : {}), batchState, batchBoundAskId, irreversibleBatch };
138
- };
140
+ const gate = { askStore, checkpoints, warn, revoke };
141
+ const bindAtGate = (ask, nowMs, deadlineMs) => bindParkingAskAtGate(gate, ask, nowMs, deadlineMs);
139
142
  const reconcileOne = async (ask, nowMs) => {
140
143
  const attempt = await bindAtGate(ask, nowMs);
141
144
  if (attempt.bound)
@@ -150,11 +153,11 @@ export function createApprovalReconciler(deps) {
150
153
  return { outcome: "held", match: gateMatch };
151
154
  }
152
155
  case "deny": {
153
- const won = await withDeadline(askStore.transitionAsk(ask.askId, "PARKING", "DENIED", { deniedReason: action.reason, updatedAtMs: nowMs }), "transitionAsk(DENIED)");
156
+ const won = await withDeadline(askStore.transitionAsk(ask.askId, "PARKING", "DENIED", { endingCause: action.reason, updatedAtMs: nowMs }), "transitionAsk(DENIED)");
154
157
  return { outcome: won ? "denied" : "held", match: gateMatch };
155
158
  }
156
159
  case "void": {
157
- const won = await withDeadline(askStore.transitionAsk(ask.askId, "PARKING", "VOID", { deniedReason: action.reason, updatedAtMs: nowMs }), "transitionAsk(VOID)");
160
+ const won = await withDeadline(askStore.transitionAsk(ask.askId, "PARKING", "VOID", { endingCause: action.reason, updatedAtMs: nowMs }), "transitionAsk(VOID)");
158
161
  if (!won)
159
162
  return { outcome: "held", match: gateMatch };
160
163
  if (action.reason === APPROVAL_ENDING_CAUSES.ORPHAN_TTL_EXCEEDED) {
@@ -1,4 +1,4 @@
1
- import { type AuthBridgeErrorCode, type VerifiedIdentity } from "@sema-agent/settings-schema/api/auth-bridge";
1
+ import { VerifiedIdentity, type AuthBridgeErrorCode } from "@sema-agent/settings-schema/api/auth-bridge";
2
2
  import { type TokenScopeRefusal } from "@sema-agent/settings-schema/api/scopes";
3
3
  import type { Logger } from "./observability/logger.js";
4
4
  import type { Metrics } from "./observability/metrics.js";
@@ -52,12 +52,13 @@ type BridgeSinks = {
52
52
  readonly logger?: Logger | undefined;
53
53
  readonly metrics?: Pick<Metrics, "inc"> | undefined;
54
54
  };
55
- /** 注入边界收的结局:与 {@link VerifyOutcome} 同形,只是成功身份的 `scope` 是 `unknown` —— 这一位正是边界要判的形,它不信型面
56
- * (席是嵌入方可换的:`""` 过得了 `scope: string`,JS 侧连缺省 / 非串都交得进来)。`VerifyOutcome` 可赋给它,调用方零改动。 */
55
+ /** 注入边界收的结局:与 {@link VerifyOutcome} 同形,只是成功身份的 `scope` 与 `principalInc` 是 `unknown` —— 这两位正是边界要判的形,它不信型面
56
+ * (席是嵌入方可换的:`""` 过得了 `scope: string` / `principalInc?: string`,JS 侧连缺省 / 非串都交得进来)。`VerifyOutcome` 可赋给它,调用方零改动。 */
57
57
  type ReportedOutcome = {
58
58
  ok: true;
59
- identity: Omit<BridgeIdentity, "scope"> & {
59
+ identity: Omit<BridgeIdentity, "scope" | "principalInc"> & {
60
60
  scope?: unknown;
61
+ principalInc?: unknown;
61
62
  };
62
63
  } | {
63
64
  ok: false;
@@ -65,8 +66,10 @@ type ReportedOutcome = {
65
66
  };
66
67
  /**
67
68
  * 注入边界:任何验证器(内建 {@link createWebAdminJwtVerifier},或嵌入方注入的 `ServiceDeps.webAdminJwtVerifier`)交来的结局,在这里定「装不装身份」。
68
- * 成功身份的 scope 经**同一只** `readTokenScope` 再读一次(入参型见 {@link ReportedOutcome})。
69
- * 读不出 ⇒ 同一只记账口、不交身份(调用方按验证失败处置:不设身份 ⇒ 门答 generic 401)。验证失败的结局验证器自己已记过账,这里只答「不装」。
69
+ * 成功身份的 scope 经**同一只** `readTokenScope` 再读一次;化身号经契约的**同一只**判官 `VerifiedIdentity.shape.principalInc`(= `PrincipalIncarnation.optional()`,
70
+ * 契约验证器读 `sub_inc` 用的就是它)再判一次(入参型见 {@link ReportedOutcome})。
71
+ * 读不出 scope ⇒ 记 `readTokenScope` 的拒词;化身号在场而不合形 ⇒ 记 `malformed`(与契约验证器同一条律:声明在场坏形 ⇒ 令牌 malformed)。两种都经同一只记账口、不交身份
72
+ * (调用方按验证失败处置:不设身份 ⇒ 门答 generic 401)。验证失败的结局验证器自己已记过账,这里只答「不装」。
70
73
  */
71
74
  export declare function admitBridgeIdentity(outcome: ReportedOutcome, sinks: BridgeSinks): BridgeIdentity | undefined;
72
75
  /** The live object `createWebAdminJwtVerifier` produces — a single `verify` entry point over the process-level JWKS cache. */
@@ -1,5 +1,5 @@
1
1
  import { createPublicKey, verify as cryptoVerify } from "node:crypto";
2
- import { AUTH_BRIDGE_ALGS, DEFAULT_CLOCK_SKEW_SEC, DEFAULT_JWKS_REFRESH_MIN_INTERVAL_SEC, DEFAULT_OFFLINE_GRACE_SEC, WEB_ADMIN_JWKS_PATH, WebAdminJwks, classifyTimeClaims, jwksWithinOfflineGrace, shouldForceJwksRefresh, toVerifiedIdentity, } from "@sema-agent/settings-schema/api/auth-bridge";
2
+ import { AUTH_BRIDGE_ALGS, DEFAULT_CLOCK_SKEW_SEC, DEFAULT_JWKS_REFRESH_MIN_INTERVAL_SEC, DEFAULT_OFFLINE_GRACE_SEC, WEB_ADMIN_JWKS_PATH, WebAdminJwks, classifyTimeClaims, jwksWithinOfflineGrace, shouldForceJwksRefresh, toVerifiedIdentity, VerifiedIdentity, } from "@sema-agent/settings-schema/api/auth-bridge";
3
3
  import { readTokenScope } from "@sema-agent/settings-schema/api/scopes";
4
4
  import { buildDisplaySafeUrl } from "./trace/redact.js";
5
5
  import { centerJwksFetch } from "./web-admin-client/center-request.js";
@@ -17,12 +17,18 @@ function recordBridgeRefusal(sinks, error, at = {}) {
17
17
  export function admitBridgeIdentity(outcome, sinks) {
18
18
  if (!outcome.ok)
19
19
  return undefined;
20
- const scoped = readTokenScope({ scope: outcome.identity.scope });
20
+ const { scope, principalInc, ...rest } = outcome.identity;
21
+ const scoped = readTokenScope({ scope });
21
22
  if ("refused" in scoped) {
22
23
  recordBridgeRefusal(sinks, scoped.refused);
23
24
  return undefined;
24
25
  }
25
- return { ...outcome.identity, scope: scoped.scope };
26
+ const inc = VerifiedIdentity.shape.principalInc.safeParse(principalInc);
27
+ if (!inc.success) {
28
+ recordBridgeRefusal(sinks, "malformed");
29
+ return undefined;
30
+ }
31
+ return { ...rest, scope: scoped.scope, ...(inc.data !== undefined ? { principalInc: inc.data } : {}) };
26
32
  }
27
33
  export const DEFAULT_JWKS_TTL_SEC = 15 * 60;
28
34
  export const JWKS_FETCH_TIMEOUT_MS = 10_000;
@@ -1,4 +1,5 @@
1
1
  import { createAdoptionRunner } from "../adoption/runner.js";
2
+ import { seatText } from "../trace/exit-seat.js";
2
3
  export async function runAdoptionBootScan(ctx) {
3
4
  const store = ctx.backend?.adoptionLog?.();
4
5
  if (!store)
@@ -29,7 +30,7 @@ export async function runAdoptionBootScan(ctx) {
29
30
  return reading;
30
31
  }
31
32
  catch (err) {
32
- const error = err instanceof Error ? err.message : String(err);
33
+ const error = seatText("operator-log-text", err instanceof Error ? err.message : String(err));
33
34
  ctx.logger.error?.("adoption_boot_scan_failed", { error });
34
35
  return { scanned: 0, resumed: [], stalled: [], rejected: [], failed: [], error };
35
36
  }
@@ -11,6 +11,22 @@ export interface BudgetTracingCtx {
11
11
  backend: StoreBackend | undefined;
12
12
  breakerState: ReturnType<BreakerStateStore["startRefresh"]> | undefined;
13
13
  }
14
+ /**
15
+ * S9 (SILENT-FALLBACK P0-e): write-behind flush/refresh failures previously vanished in a bare catch —
16
+ * the fleet silently degrades to per-replica counting (soft limits stop being fleet-wide). Every failure
17
+ * ticks the counter + streak gauge; warn once per episode (streak hits 3), info on the recovery edge.
18
+ * `info.error` is the client exception's text (the counter turns the rejection into it) ⇒ the warn's field passes the exit seat.
19
+ */
20
+ export declare function createCounterDegradeHook({ logger, metrics }: {
21
+ logger: Pick<Logger, "warn" | "info">;
22
+ metrics: Pick<Metrics, "setGauge" | "inc">;
23
+ }): (info: {
24
+ table: string;
25
+ kind: string;
26
+ streak: number;
27
+ prevStreak: number;
28
+ error?: string;
29
+ }) => void;
14
30
  export declare function createBudgetAndTracing(ctx: BudgetTracingCtx): {
15
31
  brain: import("@sema-agent/core").Brain;
16
32
  degradeRouteWatch: import("../brain.js").DegradeRouteWatch;