@sema-agent/server 7.4.0 → 7.6.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 (201) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +18 -3
  3. package/README.zh-CN.md +14 -3
  4. package/USAGE.md +80 -1
  5. package/dist/approval-card.d.ts +15 -3
  6. package/dist/approval-card.js +41 -7
  7. package/dist/approval-reconciler.d.ts +109 -12
  8. package/dist/approval-reconciler.js +152 -24
  9. package/dist/boot/config-center.js +15 -2
  10. package/dist/boot/coordinators.js +10 -2
  11. package/dist/boot/execution-env.js +1 -1
  12. package/dist/boot/org-memory.d.ts +6 -0
  13. package/dist/boot/org-memory.js +1 -1
  14. package/dist/boot/parked-revive-gate.d.ts +78 -0
  15. package/dist/boot/parked-revive-gate.js +114 -0
  16. package/dist/boot/reapers.d.ts +2 -0
  17. package/dist/boot/reapers.js +11 -4
  18. package/dist/boot/resolve-spec.d.ts +3 -19
  19. package/dist/boot/resolve-spec.js +73 -67
  20. package/dist/boot/runner-deps.d.ts +23 -1
  21. package/dist/boot/runner-deps.js +8 -11
  22. package/dist/boot/workflow-orchestration.d.ts +8 -3
  23. package/dist/boot/workflow-orchestration.js +23 -1
  24. package/dist/budget.d.ts +1 -1
  25. package/dist/budget.js +1 -1
  26. package/dist/capabilities/repo-tools.d.ts +1 -1
  27. package/dist/capabilities/repo-tools.js +8 -2
  28. package/dist/config-center/apply-effective.js +33 -10
  29. package/dist/config-provider.d.ts +1 -0
  30. package/dist/config-provider.js +23 -3
  31. package/dist/config-types.d.ts +27 -9
  32. package/dist/config.d.ts +6 -1
  33. package/dist/config.js +61 -15
  34. package/dist/deployment-governance.d.ts +168 -0
  35. package/dist/deployment-governance.js +206 -0
  36. package/dist/env-facts.d.ts +3 -1
  37. package/dist/env-facts.js +3 -1
  38. package/dist/fleet/fleet-bus.d.ts +17 -2
  39. package/dist/fleet/fleet-bus.js +68 -3
  40. package/dist/fleet/fleet-terminal-window.d.ts +98 -0
  41. package/dist/fleet/fleet-terminal-window.js +316 -0
  42. package/dist/governance-ask-marks.d.ts +31 -0
  43. package/dist/governance-ask-marks.js +122 -0
  44. package/dist/hooks/hook-runner.d.ts +28 -0
  45. package/dist/hooks/hook-runner.js +149 -25
  46. package/dist/http/routes/approvals-assistant.js +6 -7
  47. package/dist/http/routes/diagnostics.js +10 -5
  48. package/dist/http/routes/fleet.js +160 -14
  49. package/dist/http/routes/memory-policy.d.ts +2 -1
  50. package/dist/http/routes/memory-policy.js +77 -13
  51. package/dist/http/routes/runs.js +6 -2
  52. package/dist/http/routes/tasks.js +59 -22
  53. package/dist/http/routes/trace-usage.js +3 -4
  54. package/dist/http/send.d.ts +23 -0
  55. package/dist/http/send.js +23 -0
  56. package/dist/http/server.d.ts +9 -0
  57. package/dist/http/server.js +28 -14
  58. package/dist/http/sse-log.js +3 -4
  59. package/dist/http/wire-types.d.ts +7 -2
  60. package/dist/leader/diffout.d.ts +10 -0
  61. package/dist/leader/diffout.js +14 -2
  62. package/dist/leader/diffup.js +3 -2
  63. package/dist/leader/planner.js +7 -0
  64. package/dist/main.js +39 -31
  65. package/dist/observability/fail-open.d.ts +17 -2
  66. package/dist/observability/fail-open.js +19 -4
  67. package/dist/observability/prompt-manifest.d.ts +5 -1
  68. package/dist/orchestration/workflow-notify-journal.d.ts +58 -2
  69. package/dist/orchestration/workflow-notify-journal.js +130 -45
  70. package/dist/parked-decide.js +9 -4
  71. package/dist/plugins/approval-ask-store-memory.d.ts +2 -2
  72. package/dist/plugins/approval-ask-store-memory.js +3 -2
  73. package/dist/plugins/approval-ask-store-sql.d.ts +60 -5
  74. package/dist/plugins/approval-ask-store-sql.js +75 -35
  75. package/dist/plugins/background-agent-store-sql.js +16 -16
  76. package/dist/plugins/background-shell-support.d.ts +1 -1
  77. package/dist/plugins/background-shell-support.js +2 -2
  78. package/dist/plugins/breaker-state-sql.js +2 -2
  79. package/dist/plugins/checkpoint-store-sql.d.ts +67 -8
  80. package/dist/plugins/checkpoint-store-sql.js +76 -13
  81. package/dist/plugins/image-bake-store-sql.d.ts +1 -1
  82. package/dist/plugins/image-bake-store-sql.js +27 -27
  83. package/dist/plugins/image-index-sql.js +15 -15
  84. package/dist/plugins/local-checkpoint-store.d.ts +20 -1
  85. package/dist/plugins/local-checkpoint-store.js +19 -0
  86. package/dist/plugins/mailbox-store-sql.d.ts +4 -10
  87. package/dist/plugins/mailbox-store-sql.js +59 -6
  88. package/dist/plugins/memory-engine-pg.js +9 -9
  89. package/dist/plugins/memory-engine-tidb.js +7 -7
  90. package/dist/plugins/memory-sync-store-pg.js +13 -13
  91. package/dist/plugins/memory-sync-store-tidb.js +5 -5
  92. package/dist/plugins/outcome-ledger-sql.js +7 -7
  93. package/dist/plugins/pg-cost-quota.js +3 -3
  94. package/dist/plugins/pg-pool.js +84 -75
  95. package/dist/plugins/pg-rate-limiter.js +3 -3
  96. package/dist/plugins/pg-session-storage.d.ts +1 -1
  97. package/dist/plugins/pg-session-storage.js +12 -13
  98. package/dist/plugins/remote-env-host.js +3 -1
  99. package/dist/plugins/remote-env-local-docker.js +6 -3
  100. package/dist/plugins/remote-env-ssh.d.ts +13 -1
  101. package/dist/plugins/roster-store-sql.js +8 -8
  102. package/dist/plugins/store-contracts.d.ts +19 -0
  103. package/dist/plugins/store-contracts.js +42 -0
  104. package/dist/plugins/task-attachment-store.js +5 -5
  105. package/dist/plugins/task-list-store-sql.js +1 -1
  106. package/dist/plugins/tidb-cost-quota.js +1 -1
  107. package/dist/plugins/tidb-pool.js +83 -60
  108. package/dist/plugins/tidb-rate-limiter.js +1 -1
  109. package/dist/plugins/tidb-session-store.js +2 -5
  110. package/dist/plugins/tool-result-store-sql.js +2 -2
  111. package/dist/plugins/usage-window-store-sql.js +13 -13
  112. package/dist/plugins/write-behind-counter.d.ts +10 -2
  113. package/dist/plugins/write-behind-counter.js +13 -3
  114. package/dist/resource-suspend.d.ts +3 -1
  115. package/dist/resource-suspend.js +3 -1
  116. package/dist/run-local.d.ts +73 -1
  117. package/dist/run-local.js +146 -5
  118. package/dist/runs.d.ts +11 -1
  119. package/dist/runs.js +18 -3
  120. package/dist/runtime-governance.d.ts +18 -0
  121. package/dist/runtime-governance.js +90 -3
  122. package/dist/security.d.ts +12 -0
  123. package/dist/security.js +12 -0
  124. package/dist/session-sync-kernel.d.ts +13 -0
  125. package/dist/session-sync-kernel.js +13 -0
  126. package/dist/task-settings.d.ts +3 -9
  127. package/dist/task-settings.js +16 -13
  128. package/dist/tool-approval.d.ts +33 -6
  129. package/dist/tool-approval.js +80 -23
  130. package/dist/trace/core-keyset-guard.d.ts +18 -4
  131. package/dist/trace/project.d.ts +10 -1
  132. package/dist/trace/project.js +31 -0
  133. package/package.json +3 -3
  134. package/dist/boot/lexical-path-env.d.ts +0 -10
  135. package/dist/boot/lexical-path-env.js +0 -88
  136. package/dist/capabilities/oa-tools.d.ts +0 -15
  137. package/dist/capabilities/oa-tools.js +0 -54
  138. package/dist/finance/cost-taxonomy.d.ts +0 -34
  139. package/dist/finance/cost-taxonomy.js +0 -26
  140. package/dist/plugins/approval-store-sql.d.ts +0 -116
  141. package/dist/plugins/approval-store-sql.js +0 -151
  142. package/dist/plugins/file-workflow-journal-store.d.ts +0 -12
  143. package/dist/plugins/file-workflow-journal-store.js +0 -12
  144. package/dist/plugins/pg-approval-store.d.ts +0 -9
  145. package/dist/plugins/pg-approval-store.js +0 -9
  146. package/dist/plugins/pg-breaker-state.d.ts +0 -8
  147. package/dist/plugins/pg-breaker-state.js +0 -8
  148. package/dist/plugins/pg-checkpoint-store.d.ts +0 -10
  149. package/dist/plugins/pg-checkpoint-store.js +0 -10
  150. package/dist/plugins/pg-file-snapshot-store.d.ts +0 -8
  151. package/dist/plugins/pg-file-snapshot-store.js +0 -8
  152. package/dist/plugins/pg-image-bake.d.ts +0 -12
  153. package/dist/plugins/pg-image-bake.js +0 -11
  154. package/dist/plugins/pg-image-index.d.ts +0 -12
  155. package/dist/plugins/pg-image-index.js +0 -11
  156. package/dist/plugins/pg-outcome-ledger.d.ts +0 -12
  157. package/dist/plugins/pg-outcome-ledger.js +0 -11
  158. package/dist/plugins/pg-resume-anchor-store.d.ts +0 -7
  159. package/dist/plugins/pg-resume-anchor-store.js +0 -7
  160. package/dist/plugins/pg-run-store.d.ts +0 -9
  161. package/dist/plugins/pg-run-store.js +0 -9
  162. package/dist/plugins/pg-session-policy-store.d.ts +0 -7
  163. package/dist/plugins/pg-session-policy-store.js +0 -7
  164. package/dist/plugins/pg-session-store.d.ts +0 -12
  165. package/dist/plugins/pg-session-store.js +0 -12
  166. package/dist/plugins/pg-tool-result-store.d.ts +0 -9
  167. package/dist/plugins/pg-tool-result-store.js +0 -9
  168. package/dist/plugins/pg-workflow-journal-store.d.ts +0 -9
  169. package/dist/plugins/pg-workflow-journal-store.js +0 -9
  170. package/dist/plugins/pg-workflow-run-store.d.ts +0 -9
  171. package/dist/plugins/pg-workflow-run-store.js +0 -9
  172. package/dist/plugins/tidb-approval-store.d.ts +0 -8
  173. package/dist/plugins/tidb-approval-store.js +0 -8
  174. package/dist/plugins/tidb-breaker-state.d.ts +0 -7
  175. package/dist/plugins/tidb-breaker-state.js +0 -7
  176. package/dist/plugins/tidb-checkpoint-store.d.ts +0 -9
  177. package/dist/plugins/tidb-checkpoint-store.js +0 -9
  178. package/dist/plugins/tidb-file-snapshot-store.d.ts +0 -8
  179. package/dist/plugins/tidb-file-snapshot-store.js +0 -8
  180. package/dist/plugins/tidb-image-bake.d.ts +0 -12
  181. package/dist/plugins/tidb-image-bake.js +0 -11
  182. package/dist/plugins/tidb-image-index.d.ts +0 -12
  183. package/dist/plugins/tidb-image-index.js +0 -11
  184. package/dist/plugins/tidb-outcome-ledger.d.ts +0 -12
  185. package/dist/plugins/tidb-outcome-ledger.js +0 -12
  186. package/dist/plugins/tidb-resume-anchor-store.d.ts +0 -7
  187. package/dist/plugins/tidb-resume-anchor-store.js +0 -7
  188. package/dist/plugins/tidb-run-store.d.ts +0 -10
  189. package/dist/plugins/tidb-run-store.js +0 -9
  190. package/dist/plugins/tidb-session-policy-store.d.ts +0 -7
  191. package/dist/plugins/tidb-session-policy-store.js +0 -7
  192. package/dist/plugins/tidb-tool-result-store.d.ts +0 -8
  193. package/dist/plugins/tidb-tool-result-store.js +0 -10
  194. package/dist/plugins/tidb-workflow-journal-store.d.ts +0 -9
  195. package/dist/plugins/tidb-workflow-journal-store.js +0 -9
  196. package/dist/plugins/tidb-workflow-run-store.d.ts +0 -10
  197. package/dist/plugins/tidb-workflow-run-store.js +0 -10
  198. package/dist/plugins/workflow-journal-limits.d.ts +0 -12
  199. package/dist/plugins/workflow-journal-limits.js +0 -12
  200. package/dist/sema-registry.d.ts +0 -41
  201. package/dist/sema-registry.js +0 -40
@@ -1,10 +1,11 @@
1
+ import { isTerminalRunStatus } from "./plugins/store-contracts.js";
1
2
  import { DENY_REASONS, VOID_REASONS } from "./approval-deny-reasons.js";
2
3
  import { buildRevokeFrame } from "./approval-card.js";
3
4
  import { encodeCheckpointScope } from "./security.js";
4
- /** 判据 2 的「非 suspended 终局」词表(§8 A-1;`RunRecord["status"]` = `"running" | TaskStatus`,
5
- * TaskStatus = completed|blocked|failed|suspended|needs_review)。`suspended`/`needs_review`/`running`
6
- * 都不是终局 落判据 3 保持。 */
7
- const TERMINAL_RUN_STATUS = new Set(["completed", "failed", "blocked"]);
5
+ /* 判据 2 的「非 suspended 终局」判据 = `store-contracts.ts` {@link isTerminalRunStatus}(词表属主处的
6
+ * 穷举 switch,core 加/删 `TaskStatus` 成员当场 tsc 红)。此处**不再手抄**一份 `ReadonlySet<string>`:
7
+ * 那份的门对象是 `string`,对词表增删两向都无钉,而这张词表真变过(core 5.8.0 `"timeout"`)。
8
+ * `suspended`/`needs_review`/`running` 都不是终局 落判据 3 保持。 */
8
9
  /** core 给「被取消」的 run 打的 `errorCode`(§9 C3 的取消判别键)。 */
9
10
  const CANCELLED_ERROR_CODE = "cancelled";
10
11
  /**
@@ -42,24 +43,82 @@ function withDeadline(op, label, timeoutMs = RECONCILE_STORE_TIMEOUT_MS) {
42
43
  ]);
43
44
  }
44
45
  /**
45
- * 判据 1 的**硬谓词**(纯函数,§9 C2 + §8 D-2)。
46
+ * 这只 ask 是不是**祖先冻结 approver 层**在委派 fold 中途铸的那一份(#168 件1④,黑板 [2912]③)。
46
47
  *
47
- * 返回选中的候选,或 `undefined` = 不命中。逐条:
48
+ * 判别位是**产品自己发的**,不是外部约定:core `withDelegationProvenance` 只包**子代自己那条缝**,
49
+ * 祖先层那次走未包装的原函数 ⇒ `AskRequest.delegation` 在不在,就是「这一份是哪一层发的」。落到行上,
50
+ * `delegation.parentToolCallId` 是必填字段,铸行时逐字透传进 `parent_tool_call_id` 列 ⇒ **列非 NULL
51
+ * ⇔ delegation 在场**(不需要新列,也不需要回 `req` 里再读一次)。
52
+ *
53
+ * 第二维 `sourceTaskId !== sessionId` 把「根腿」摘出去:根腿的 `sourceTaskId` 恒等于会话锚(core 给
54
+ * `AskRequest.sourceTaskId` 填的就是该腿 sessionId),它根本不在任何 fold 里,`parent_tool_call_id`
55
+ * 为 NULL 是它的常态而不是信号。
56
+ *
57
+ * 命中 ⇒ 退出硬相等(判据 1 结构上不命中,行落 ②③④⑤)。实测(test AI 围栏)checkpoint 存的摘要**逐字
58
+ * 等于子代自己那条缝**、只不等于祖先层那一次 —— 所以收窄到这一层,子代自己的缝照常参与。
59
+ */
60
+ export function isAncestorFoldMint(ask) {
61
+ return ask.parentToolCallId === null && ask.sourceTaskId !== ask.sessionId;
62
+ }
63
+ /**
64
+ * 判据 1 的**硬谓词**(纯函数,§9 C2 + §8 D-2;#168 件1 起返回分类而不是布尔)。
65
+ *
66
+ * 逐条:
67
+ * - 祖先层 fold 中途铸点 ⇒ 直接退出(见 {@link isAncestorFoldMint});
48
68
  * - `unparseable` 候选直接出局(读不出 ⇒ 不确定 ⇒ 不命中);
49
- * - `boundCallId` 必须逐字等于 `ask.toolCallId`(读口已按它查,这里是纵深防御);
50
- * - **因果下界**:`cp.createdAtMs >= ask.createdAtMs`(park 不可能早于它要 park 的那次 ask);
51
- * - **hash 双等**:两侧都必须在场且相等 —— 任一侧缺席即不命中(禁「能取到时才比」的可选谓词)。
69
+ * - **身份三元组**:`sourceTaskId` **相等** `toolCallId` **相等** ∧ 时间维的**因果下界**
70
+ * `candidate.createdAtMs >= ask.createdAtMs`(⚠️ 第三维**不是等式** —— 把它读成等式会让合法的 park
71
+ * 几乎命不中,行随后落 ②③⑤ 被误判)—— 摘要**不是**身份([2897]③①:
72
+ * 同 args 的两次调用摘要天然相同,只靠摘要硬相等会把第二次投递并进第一次的票);任一维在候选侧
73
+ * 缺席(读不出的 blob 没有 `sourceTaskId`)即身份不成立;
74
+ * - **hash 双等**:两侧都必须在场且相等,任一侧缺席一律不 bind;禁「能取到时才比」的可选谓词。
75
+ * 归因分两级(顺序即代码顺序):身份这一层先判 —— 同身份候选为空**且**候选集非空 ⇒ `identity_miss`;
76
+ * 走到 hash 这一层才把缺席记成**单铸** `single_mint`(见 {@link GateMatchOutcome})。
52
77
  *
53
78
  * 多候选时的取舍:优先 `status === "pending"`(活着的那张 gate),否则取最早的一条(读口按
54
79
  * `created_at ASC` 返回)。两者都满足全部硬谓词,选谁都不会错配;取 pending 只是让 `PARKED` 行落到
55
80
  * 一个还能被 resume 的坐标上,对壳更有用。
56
81
  */
57
- export function selectGateCandidate(ask, candidates) {
82
+ export function classifyGateMatch(ask, candidates) {
83
+ const outcome = classifyGateMatchBeforeFoldVeto(ask, candidates);
84
+ // 🔴 祖先层否决**排在最后**、而且只否决已经成立的命中(纯减法)。放在最前面判会让这条谓词的每一次
85
+ // 假阳性(`sourceTaskId ≠ sessionId` 还有别的成因,例如 session-less 的 adhoc 腿)都白白关掉判据 1,
86
+ // 并把一条本该记成 `single_mint`/`identity_miss` 的行错记成「祖先层铸点」。放在这里,它最坏只是**不
87
+ // 命中**(与判据 1 的既有 fail-safe 同向),而计数从此只统计「本来会 bind、被这条裁定拦下」的行 ——
88
+ // 非零读数因此是真信号,不是噪声底。
89
+ if (outcome.kind === "match" && isAncestorFoldMint(ask))
90
+ return { kind: "ancestor_fold_mint" };
91
+ return outcome;
92
+ }
93
+ function classifyGateMatchBeforeFoldVeto(ask, candidates) {
94
+ // 身份三元组的前两维 + 因果下界(摘要不参与这一层——它是身份之外的**第二道**等式)。
95
+ const sameIdentity = candidates.filter((c) => c.unparseable !== true && c.sourceTaskId !== null && c.sourceTaskId === ask.sourceTaskId && c.boundCallId === ask.toolCallId && c.createdAtMs >= ask.createdAtMs);
58
96
  const askHash = ask.boundInputHash;
97
+ if (sameIdentity.length === 0) {
98
+ // 「同身份的 checkpoint 一条都没有」有两种成因,不能混:
99
+ // · 候选集**空** ⇒ 这条弧上根本没有对家,只有 ask 侧铸过(或两侧都没铸)= 单铸;
100
+ // · 候选集非空 ⇒ 有对家、只是不是这一只(别的 sourceTaskId/callId),或者读不出(`unparseable`,
101
+ // 「不确定」)。两者都**不是**「只有一侧铸过」这个事实陈述,一律记 `identity_miss`——
102
+ // 把一条读不出的坏行说成「对家从来不存在」是拿不确定冒充确定。
103
+ if (candidates.length > 0)
104
+ return { kind: "identity_miss" };
105
+ return { kind: "single_mint", side: askHash === null ? "neither" : "ask_only" };
106
+ }
107
+ const withHash = sameIdentity.filter((c) => c.boundInputHash !== null);
59
108
  if (askHash === null)
60
- return undefined; // hash 缺席 = 结构上永不满足判据 1(见 AskRow.boundInputHash 顶注)
61
- const matches = candidates.filter((c) => c.unparseable !== true && c.boundCallId === ask.toolCallId && c.createdAtMs >= ask.createdAtMs && c.boundInputHash !== null && c.boundInputHash === askHash);
62
- return matches.find((c) => c.status === "pending") ?? matches[0];
109
+ return { kind: "single_mint", side: withHash.length > 0 ? "checkpoint_only" : "neither" };
110
+ if (withHash.length === 0)
111
+ return { kind: "single_mint", side: "ask_only" }; // 同身份的 park 一条摘要都没有
112
+ const matches = withHash.filter((c) => c.boundInputHash === askHash);
113
+ const picked = matches.find((c) => c.status === "pending") ?? matches[0];
114
+ if (picked !== undefined)
115
+ return { kind: "match", candidate: picked };
116
+ return { kind: "hash_mismatch", candidates: withHash.length };
117
+ }
118
+ /** {@link classifyGateMatch} 的布尔面(判据 1 命中即返回那条候选)。分类信息由调用方按需另取。 */
119
+ export function selectGateCandidate(ask, candidates) {
120
+ const outcome = classifyGateMatch(ask, candidates);
121
+ return outcome.kind === "match" ? outcome.candidate : undefined;
63
122
  }
64
123
  /** 判据表 v2 的判定(纯函数;顺序 = 设计稿 §9 尾的五臂汇总,注见文件头)。 */
65
124
  export function decideReconcileAction(input) {
@@ -82,7 +141,7 @@ export function decideReconcileAction(input) {
82
141
  return { kind: "void", reason: VOID_REASONS.BATCH_BOUND_ELSEWHERE };
83
142
  }
84
143
  // ② run 终局分臂
85
- if (run !== null && TERMINAL_RUN_STATUS.has(run.status)) {
144
+ if (run !== null && isTerminalRunStatus(run.status)) {
86
145
  if (run.status === "failed" && run.errorCode === CANCELLED_ERROR_CODE) {
87
146
  return { kind: "void", reason: VOID_REASONS.RUN_CANCELLED };
88
147
  }
@@ -119,7 +178,8 @@ export function createApprovalReconciler(deps) {
119
178
  }
120
179
  }
121
180
  };
122
- /** 一条 `PARKING` 行的收敛(读事实 → 纯判定 → 一次 CAS)。返回本行落到哪一臂(供统计)。 */
181
+ /** 一条 `PARKING` 行的收敛(读事实 → 纯判定 → 一次 CAS)。返回本行落到哪一臂 + 判据 1 的分类(供统计;
182
+ * `match` 缺席 = 本轮压根没走到判据 1,与「走了但没命中」是两回事,不能记成同一格)。 */
123
183
  const reconcileOne = async (ask, nowMs) => {
124
184
  // ⓪ 批态**先读**(codex round3 R3-2):它是唯一一个能让本行「结构上再也不可能 bind」的事实,而且是
125
185
  // 一次主键读。批已 `ABORTED` 或已绑给**别人** ⇒ 判据 1 的 `bindBatch` 谓词(`ROUTING_UNBOUND`)
@@ -133,9 +193,42 @@ export function createApprovalReconciler(deps) {
133
193
  // 判据 1:窄谓词精确查(scope 必填 —— 租户门是读口自己的责任,§8 D-3)。因果下界直接下推成
134
194
  // `sinceMs`,SQL 侧就把早于本 ask 的 checkpoint 排掉了(`selectGateCandidate` 里还会再判一次,
135
195
  // 纵深防御)。
196
+ let gateMatch;
136
197
  if (checkpoints && !irreversibleBatch) {
198
+ // 🔴 **已知缺口,登记而不假称已解**(codex 交叉复审 2026-08-07 [high] 一,验真;**先存**,不是本批
199
+ // 引入):这里的 session 维传的是 `ask.sessionId` = **投递上下文**的会话(`tool-approval.ts` 铸行时
200
+ // 取 `primary.sessionId`),而委派子代那条腿的 checkpoint 落在**子代自己**的 sessionId 上
201
+ // (core 铸 `cp.sessionId = 该腿 sessionId`,同值也写进 `cp.sourceTaskId`)。⇒ 子代自己那条 approver
202
+ // 缝的 park 在 SQL 谓词这一层就被滤掉了,判据 1 对委派腿结构上够不着 —— 黑板 [2912]③「子代自己那条
203
+ // 缝可以参与硬相等」的那一半,在本仓当前接线下还落不了地(行改走 ②③④⑤,fail-safe)。
204
+ //
205
+ // 顺带说清本批**改善**了什么:三元组把 `cp.sourceTaskId === ask.sourceTaskId` 变成硬谓词之后,
206
+ // 一只子代 ask 再也不可能认领**宿主**腿的 park(同 session 查出来的候选,callId 撞车 + 同 args 摘要
207
+ // 相同就够了,而 `PARKED` 不可回滚)——那是本批之前真实存在的错配面。
208
+ //
209
+ // 修法(未做,需自带判据):把 session 维换成「铸 park 的那条腿的会话」。风险在于 `ask.sourceTaskId`
210
+ // 有 `?? origin.taskId` 兜底,core 万一不填就会把一个 taskId 当 session 去查(比今天更差),所以
211
+ // **必须**配一条用真 checkpoint 店、宿主/子代两个不同 sessionId 的端到端钉才动。开题上报。
137
212
  const candidates = await withDeadline(checkpoints.findCheckpointCandidatesForAsk(encodeCheckpointScope(ask.owner), ask.sessionId, ask.toolCallId, ask.createdAtMs), "findCheckpointCandidatesForAsk");
138
- const match = selectGateCandidate(ask, candidates);
213
+ gateMatch = classifyGateMatch(ask, candidates);
214
+ if (gateMatch.kind === "hash_mismatch") {
215
+ // 留痕不拒(见 ReconcileStats.hashMismatch 顶注):warn 说清成因方向,行继续按 ②③④⑤ 判。
216
+ try {
217
+ logger.warn("approval_reconcile_hash_mismatch", {
218
+ askId: ask.askId,
219
+ taskId: ask.taskId,
220
+ toolCallId: ask.toolCallId,
221
+ candidates: gateMatch.candidates,
222
+ reason: "ask and checkpoint minted the same bound input under the same (sourceTaskId, toolCallId) identity but the digests differ — " +
223
+ "the known causes are deployment-side (a hook/policy mutating an object it still holds a reference to between the two mints, " +
224
+ "or a stateful getter returning a fresh value per read), not tampering; this ask keeps its fail-safe route (no park, no denial)",
225
+ });
226
+ }
227
+ catch {
228
+ /* 可观测面绝不成为故障源 */
229
+ }
230
+ }
231
+ const match = gateMatch.kind === "match" ? gateMatch.candidate : undefined;
139
232
  if (match) {
140
233
  // 🔴 `bindBatch` 内部**一次**完成 `PARKING→PARKED` + gate 落行 + 兄弟连坐 VOID(§8 A-2:正文
141
234
  // 「先 transitionAsk 再 bindBatch」是死锁形,已作废)。只调它一次。
@@ -146,7 +239,7 @@ export function createApprovalReconciler(deps) {
146
239
  }), "bindBatch");
147
240
  if (res.ok) {
148
241
  revoke(ask, res.voidedSiblings, "superseded_by_park", nowMs);
149
- return "parked";
242
+ return { outcome: "parked", match: gateMatch };
150
243
  }
151
244
  // 判别式失败臂(§9 C3):拿到批的**真实**态 + 中选者,降级续判 —— `ABORTED` ⇒ VOID(abort 语义),
152
245
  // `ROUTING_BOUND` 且中选者是兄弟 ⇒ VOID(bind-once 落选者,R2-3),其余按 ②③④⑤。
@@ -162,13 +255,13 @@ export function createApprovalReconciler(deps) {
162
255
  case "bind": {
163
256
  // `allowBind: false` 下结构上不可达;留一条防御性 hold,绝不在这里第二次调 bindBatch。
164
257
  await withDeadline(askStore.deferReconcile(ask.askId, "PARKING", ask.version, nowMs), "deferReconcile");
165
- return "held";
258
+ return { outcome: "held", match: gateMatch };
166
259
  }
167
260
  case "deny": {
168
261
  // 🔴 全仓唯一的 `DENIED` 写点(grep 钉)。归因是判据 2 的**终局证据**,不是超时推断。
169
262
  // CAS 输(行在本轮判定期间被别人收走)= 正常,如实记成 held(没动过行),下轮重扫。
170
263
  const won = await withDeadline(askStore.transitionAsk(ask.askId, "PARKING", "DENIED", { deniedReason: action.reason, updatedAtMs: nowMs }), "transitionAsk(DENIED)");
171
- return won ? "denied" : "held";
264
+ return { outcome: won ? "denied" : "held", match: gateMatch };
172
265
  }
173
266
  case "void": {
174
267
  // 🔴 codex 交叉复审 C3(2026-08-06 真缺陷):**必须看 CAS 的返回值**。原先无条件记账 + 发撤卡帧
@@ -177,7 +270,7 @@ export function createApprovalReconciler(deps) {
177
270
  // 壳会清掉一张仍然有效、还在等 gate 的卡。输 ⇒ 什么都不做,下轮按新态重判。
178
271
  const won = await withDeadline(askStore.transitionAsk(ask.askId, "PARKING", "VOID", { deniedReason: action.reason, updatedAtMs: nowMs }), "transitionAsk(VOID)");
179
272
  if (!won)
180
- return "held";
273
+ return { outcome: "held", match: gateMatch };
181
274
  if (action.reason === VOID_REASONS.ORPHAN_TTL_EXCEEDED) {
182
275
  logger.warn("approval_ask_orphan_voided", { askId: ask.askId, taskId: ask.taskId, ageMs: nowMs - ask.createdAtMs, ttlMs: orphanTtlMs });
183
276
  }
@@ -190,19 +283,34 @@ export function createApprovalReconciler(deps) {
190
283
  // 落选者(R2-3)是**真的**被别人的 park 顶掉 ⇒ 用 `superseded_by_park`;其余单行 VOID 用 `"aborted"`
191
284
  // (两员闭集里唯一诚实的那个,见上注)。
192
285
  revoke(ask, [ask.askId], action.reason === VOID_REASONS.BATCH_BOUND_ELSEWHERE ? "superseded_by_park" : "aborted", nowMs);
193
- return "voided";
286
+ return { outcome: "voided", match: gateMatch };
194
287
  }
195
288
  case "hold": {
196
289
  // 队列轮转(§8 D-4 / §9 C5):推 `updated_at_ms` 排到队尾。CAS 输(版本已被别人推进)= 正常,
197
290
  // 本轮判定已过期,下轮重来。
198
291
  await withDeadline(askStore.deferReconcile(ask.askId, "PARKING", ask.version, nowMs), "deferReconcile");
199
- return "held";
292
+ return { outcome: "held", match: gateMatch };
200
293
  }
201
294
  }
202
295
  };
203
296
  return {
204
297
  async runOnce(nowMs) {
205
- const stats = { scanned: 0, unmatchableNoHash: 0, parked: 0, denied: 0, voided: 0, held: 0, failed: 0, budgetExhausted: 0, orphansExpired: 0 };
298
+ const stats = {
299
+ scanned: 0,
300
+ unmatchableNoHash: 0,
301
+ hashMismatch: 0,
302
+ singleMintAskOnly: 0,
303
+ singleMintCheckpointOnly: 0,
304
+ singleMintNeither: 0,
305
+ ancestorFoldMint: 0,
306
+ parked: 0,
307
+ denied: 0,
308
+ voided: 0,
309
+ held: 0,
310
+ failed: 0,
311
+ budgetExhausted: 0,
312
+ orphansExpired: 0,
313
+ };
206
314
  // ── 段一:PARKING 判据表 ────────────────────────────────────────────────────────────────
207
315
  const parking = await withDeadline(askStore.listByState("PARKING", batchLimit), "listByState(PARKING)");
208
316
  stats.scanned = parking.length;
@@ -221,7 +329,7 @@ export function createApprovalReconciler(deps) {
221
329
  if (ask.boundInputHash === null)
222
330
  stats.unmatchableNoHash += 1; // 判据 1 盲区(见字段顶注)
223
331
  try {
224
- const outcome = await reconcileOne(ask, nowMs);
332
+ const { outcome, match } = await reconcileOne(ask, nowMs);
225
333
  if (outcome === "parked")
226
334
  stats.parked += 1;
227
335
  else if (outcome === "denied")
@@ -230,6 +338,26 @@ export function createApprovalReconciler(deps) {
230
338
  stats.voided += 1;
231
339
  else
232
340
  stats.held += 1;
341
+ // 判据 1 的分类计数(#168 件1②③④)——`match` 缺席 = 本轮没走到判据 1(无 checkpoint 面 /
342
+ // 批已不可逆),那既不是 mismatch 也不是单铸,一格都不记。
343
+ switch (match?.kind) {
344
+ case "hash_mismatch":
345
+ stats.hashMismatch += 1;
346
+ break;
347
+ case "ancestor_fold_mint":
348
+ stats.ancestorFoldMint += 1;
349
+ break;
350
+ case "single_mint":
351
+ if (match.side === "ask_only")
352
+ stats.singleMintAskOnly += 1;
353
+ else if (match.side === "checkpoint_only")
354
+ stats.singleMintCheckpointOnly += 1;
355
+ else
356
+ stats.singleMintNeither += 1;
357
+ break;
358
+ default:
359
+ break; // match / identity_miss / 未走到判据 1:无计数格
360
+ }
233
361
  }
234
362
  catch (err) {
235
363
  // 🔴 per-row 隔离(邻居 bg-agent per-scope 先例):一条行的坏 blob / 一次瞬时错误不许饿死同批
@@ -450,8 +450,21 @@ export async function createConfigCenterRuntime(ctx) {
450
450
  const cfgModels = (r.effective.models?.models ?? []).filter((m) => m.enabled !== false).length;
451
451
  const cfgDir = configProvider.kind === "local" ? { dir: configProvider.root } : {};
452
452
  logger.info("config_loaded", { source: configProvider.kind, version: r.effective.version, worker: cc?.worker ?? "(global)", models: cfgModels, ...cfgDir });
453
- if (configProvider.kind === "local" && cfgModels === 0)
454
- logger.warn("config_local_no_models", { ...cfgDir, hint: "config.d/models.json 空或 CONFIG_LOCAL_DIR 指错目录(选目录旋钮是 CONFIG_LOCAL_DIR,不是 LOCAL_DATA_ROOT)— 模型继续走 env 兜底" });
453
+ if (configProvider.kind === "local" && cfgModels === 0) {
454
+ // [3073]#204④ hint 真因三臂:域判形失败(文件在场却喊「空/指错」=对排障者说谎,真因在
455
+ // config_domain_invalid 里但 hint 不指路)/ 全 enabled:false / 真空缺,三态分开点名。
456
+ const modelsDomainErr = (r.domainErrors ?? []).find((de) => de.domain === "models");
457
+ const totalModels = (r.effective.models?.models ?? []).length;
458
+ logger.warn("config_local_no_models", {
459
+ ...cfgDir,
460
+ ...(modelsDomainErr ? { domainError: modelsDomainErr.error } : {}),
461
+ hint: modelsDomainErr
462
+ ? "config.d/models.json 在场但 models 域判形失败落 default(真因见本行 domainError)— 模型继续走 env 兜底"
463
+ : totalModels > 0
464
+ ? `config.d/models.json 有 ${totalModels} 条模型但全部 enabled:false — 模型继续走 env 兜底`
465
+ : "config.d/models.json 空或 CONFIG_LOCAL_DIR 指错目录(选目录旋钮是 CONFIG_LOCAL_DIR,不是 LOCAL_DATA_ROOT)— 模型继续走 env 兜底",
466
+ });
467
+ }
455
468
  }
456
469
  }
457
470
  }
@@ -43,7 +43,11 @@ export function createLiveCoordinators(ctx) {
43
43
  parkFacility: durableEnabled,
44
44
  });
45
45
  // §8.4:协议**开着**却上不了场 = 合法部署形(不是配置错误)⇒ 一行 **info**(不是 warn),把原因说明白。
46
- // 反过来「开关本来就没开」不打日志:那是默认形,每次启动刷一行噪音没有信息量。
46
+ // 反过来「开关本来就没开」不打日志:显式关掉的部署自己知道为什么关,一行噪音没有信息量。
47
+ // ⚠️ 读数已反转:总开关默认在树上已是 ON(版本坐标见 config.ts 的 `streamApprovalConfig()` 头注),
48
+ // 所以这一行在**默认部署**上是常态 —— 任何没有 durable
49
+ // backend / park 设施的默认部署每次启动都会打一行 `stream_approval_disabled`(每次启动一行,不是每
50
+ // tick)。写这条判据时的前提「默认形绕开本路径」已不成立;要收紧,收紧点在本 if 而不是在开关处。
47
51
  if (config.streamApproval.enabled && !streamApprovalGate.active) {
48
52
  logger.info("stream_approval_disabled", {
49
53
  reason: streamApprovalGate.reason,
@@ -55,8 +59,12 @@ export function createLiveCoordinators(ctx) {
55
59
  }
56
60
  const toolApproval = config.toolApprovalEnabled
57
61
  ? new ToolApprovalCoordinator({
58
- // 协议上场 ⇒ 持久层 + 60s 窗(§7:`STREAM_ASK_WINDOW_MS` **只在开关内**生效,
62
+ // 协议上场 ⇒ 持久层 + 配置窗(§7:`STREAM_ASK_WINDOW_MS` **只在开关内**生效,
59
63
  // `DEFAULT_APPROVAL_TTL_MS` 一个字不动);不上场 ⇒ 两者都不传,窗保持既有 5min(D1 逐字不变)。
64
+ // ⚠️ 两臂的窗**默认已同长**:窗默认自 7.5.0 起 = 300000 = `DEFAULT_APPROVAL_TTL_MS`(clay 同裁把
65
+ // 「窗变短」从翻真里摘出去)。原注画的「上场 = 60s / 不上场 = 5min」这个对比在默认配置下不存在
66
+ // 了,别据它得出「开了协议窗会缩短」的运维结论:上场与否改变的是**持久层与协议帧**,窗只有在
67
+ // 部署显式设了 `STREAM_ASK_WINDOW_MS` 时才分叉。
60
68
  ...(streamApprovalGate.active ? { askStore: streamApprovalGate.askStore, ttlMs: config.streamApproval.windowMs } : {}),
61
69
  // 余量旋钮恒传(既有键,窗长三元公式的第二项;legDeadlineMonotonic 缺席时它不参与计算)。
62
70
  windowMarginMs: config.streamAskWindowMarginMs,
@@ -365,7 +365,7 @@ export function createExecutionEnv(ctx) {
365
365
  // LSP sidecar (design/64 §13.1, the 1.86.2 seam): ONE stateless manager on RunnerDeps — core passes each
366
366
  // task's env into sessionFor at the tool mount point, so the manager reaches the SAME sandbox the agent edits
367
367
  // (no sessionId registry / per-task construction). Opt-in (LSP_ENABLED — the SANDBOX lane's knob; the host lane
368
- // below has its own, LSP_HOST_ENABLED, because its default is the opposite); with the baked `sema-code-lsp`
368
+ // below has its own, LSP_HOST_ENABLED, because its default is the opposite); with the baked `ai-agent-code-lsp`
369
369
  // template the first call is fast, without it the language server installs on first use (slow once per sandbox).
370
370
  // k8s lane (gate#2 alignment): same manager/bridge over the pod network — `ws://podIP:port` instead of E2B's
371
371
  // public wss proxy, so it needs an IN-CLUSTER worker and a sandbox image with node + the language servers baked
@@ -26,9 +26,15 @@ import type { RunnerDeps } from "@sema-agent/core";
26
26
  import type { ServiceConfig } from "../config.js";
27
27
  import type { Logger } from "../observability/logger.js";
28
28
  import type { Metrics } from "../observability/metrics.js";
29
+ import { type OrgMemoryDirectory } from "../org-memory-admission.js";
29
30
  export interface OrgMemoryAdmissionWiring {
30
31
  memoryScopeAdmission: RunnerDeps["memoryScopeAdmission"];
31
32
  deploymentMemoryScopes: RunnerDeps["deploymentMemoryScopes"];
33
+ /** §7 第二消费者收编:同一个目录实例也喂 memory-policy 面的 `org:` 属主门(`ServiceDeps.orgMemoryDirectory`)。
34
+ * **同实例**是这条收编的全部意义——「谁属于 org:acme」在一个进程里必须只有一个答案,两面共享同一份
35
+ * TTL 缓存/退避窗/gen 高水位;各建各的目录会让准入门与策略面在吊销窗内公开分歧。缺席=无目录源
36
+ * (单用户/无 center 且无 env 表)⇒ 策略面逐字保持收编前的 operator-only 行为。 */
37
+ directory: OrgMemoryDirectory | undefined;
32
38
  }
33
39
  /** 依赖收窄到真实消费面(warn/inc 各一手)——接口隔离让测试用真形字面量,零铸形([2704] §1)。 */
34
40
  export declare function createOrgMemoryAdmissionWiring(opts: {
@@ -82,6 +82,6 @@ export function createOrgMemoryAdmissionWiring(opts) {
82
82
  },
83
83
  })
84
84
  : undefined;
85
- return { memoryScopeAdmission, deploymentMemoryScopes };
85
+ return { memoryScopeAdmission, deploymentMemoryScopes, directory };
86
86
  }
87
87
  //# sourceMappingURL=org-memory.js.map
@@ -0,0 +1,78 @@
1
+ import { type BackgroundAgentRecord, type ToolExecuteContext } from "@sema-agent/core";
2
+ import type { ServiceConfig } from "../config.js";
3
+ import { type ApprovalBaselineConfigView, type DeploymentGovernanceConfigView, type LiveQuestionFace } from "../deployment-governance.js";
4
+ import { type SandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
5
+ /** 本腿读的配置切面(`ServiceConfig` 结构满足)。两个构造口各自的切面 + lane 判别式读的 `remoteExec`。 */
6
+ export interface ParkedReviveGateConfigView extends DeploymentGovernanceConfigView, ApprovalBaselineConfigView {
7
+ readonly remoteExec?: ServiceConfig["remoteExec"];
8
+ /** host 腿裁决 env 的 cwd 占位根(见下方 `hostGateCwd`)。 */
9
+ readonly localDataRoot?: ServiceConfig["localDataRoot"];
10
+ }
11
+ /** 会话豁免探针店的所需切面(canonical toolName 键空间;`StoreBackend.approvalExemption()` 结构满足)。 */
12
+ export interface ApprovalExemptionProbe {
13
+ has(sessionId: string, toolName: string): Promise<boolean>;
14
+ }
15
+ export interface ParkedReviveGateDeps {
16
+ /** ⚠️ **活引用**:热改字段(autonomy/commandPolicy/守卫集/审批四旋钮)每次赎回现读,见下方「禁 memoize」。 */
17
+ readonly config: ParkedReviveGateConfigView;
18
+ /** #152:活体 AskUserQuestion 面。赎回腿今天无 ALS ctx ⇒ 判 ask,与原形同判。 */
19
+ readonly question: LiveQuestionFace | undefined;
20
+ readonly approvalExemptionStore: ApprovalExemptionProbe | undefined;
21
+ readonly logger: {
22
+ info(event: string, fields?: Record<string, unknown>): void;
23
+ warn(event: string, fields?: Record<string, unknown>): void;
24
+ };
25
+ /** host 腿 cwd 占位的落地根(main.ts 的 `localRoot`;`config.localDataRoot` 优先)。 */
26
+ readonly localRoot: string;
27
+ /** 沙箱 lane 的真 env 登记簿。缺省 = 进程内唯一那只(工厂装饰器写的同一个);测试注入自建实例。 */
28
+ readonly slots?: SandboxPathEnvSlots;
29
+ }
30
+ /**
31
+ * core 1.396 的父约束席位形(`parkedResume.inheritedGate`)—— 从**载体自己的签名位推导**,绝不手写。
32
+ *
33
+ * 🔴 手写这个形状会**藏住轴**(codex 对抗复审 R1-高2 的真回归):第一版在这里手抄了一个只有
34
+ * `parentConstraints` 的接口,于是 core 载体上那个 `shellGate` 槽在类型层面根本看不见,「shellGate 轴
35
+ * 也要交回」这件事就无声地漏了一整刀。推导形让「core 又加了一条能收紧的轴」变成本仓的编译期/钉子面
36
+ * 事件(载体键集钉见 test/parked-revive-gate-wiring.test.ts)。
37
+ */
38
+ export type RebuiltInheritedGate = NonNullable<NonNullable<NonNullable<ToolExecuteContext["reviveClaim"]>["parkedResume"]>["inheritedGate"]>;
39
+ /**
40
+ * 赎回腿的父约束链重建(design/181 件二)。
41
+ *
42
+ * 语义:**同/跨副本一致地重建「部署 ⊇ 操作员」两层完整链**——审批基线(durable question 门 + F4 高危写
43
+ * 审批门 + 会话豁免探针)作 base 的 `toolPolicy` 座,经**折叠属主** `applyRuntimeGovernance`
44
+ * (→ core `tightenTaskSpec`)叠上部署治理段(autonomy / commandPolicy / MANUAL_MODE_SHELL_GATE /
45
+ * 守卫集),取其 `toolPolicy` 装进单层 `parentConstraints`。
46
+ *
47
+ * 🔴 **禁 memoize**:构造整体在 per-row lambda **内**。`autonomy`/`commandPolicy`/守卫集/审批四旋钮都是
48
+ * 热改字段(registry 热应用换 config 引用),boot 期铸一次 = 把治理冻在启动那一刻的值上,而 resume 腿的
49
+ * 书面语义是「按**当前** config 重折」(与 resolve-spec 的审批基线读活 config 同一姿势)。
50
+ *
51
+ * 🔴 **单层链是正确的响亮拒**(design/181 §2.5 特征化格):本腿恒建 count=1。嵌套子代(孙代 park,
52
+ * count≥2)的重建本腿做不到——跨副本连「祖先各层分别是什么」都没有持久化——于是 core 的 pre-CAS
53
+ * `resume.parent_constraint_mismatch` 会响亮拒绝,checkpoint 留 pending。**不许**顺手把它补齐成静默单层:
54
+ * 那等于让孙代在一条比它挂起时更松的祖先链下复活。
55
+ *
56
+ * 交回**两条轴**(载体上本仓供得出的全部,见 {@link RebuiltInheritedGate} 的推导注):
57
+ * · `parentConstraints` —— 上面那条重建链;
58
+ * · `shellGate` —— 治理段折出来的 shell 门档位。**必须交**:core 按 rank 取 `max(live, seed)`,不交
59
+ * 等于 `live` 缺席、max 恒等于 park 时的旧种子,于是「挂起期间运维收紧 AUTONOMY/MANUAL_MODE_SHELL_GATE」
60
+ * 在赎回腿上整条失效(codex R1-高2 的真回归,红先复现)。交了之后收紧被兑现、放松不被兑现(fail-safe)。
61
+ *
62
+ * 载体上**故意不供**的三个槽(都不是本腿产得出的):`ancestorRules`(会话权限规则,属会话面)、
63
+ * `admittedOrgScopes` / `orgAdmissionGoverned`(组织记忆准入,属 memory 面)—— 交一个瞎猜的值比不交更坏
64
+ * (core 对这三个都是 seed 兜底)。
65
+ *
66
+ * 时点语义(成文进 docs/ASSISTANT-WIRE-CONTRACT.md,免运维当 bug 报):
67
+ * · toolPolicy 轴 = **当前** config(与 resume 重折一致,收紧与放松都被兑现——放松是一次显式运维动作);
68
+ * · shellGate 轴 = core 取 max(live, park 时 seed)—— 收紧兑现、放松**不**兑现(fail-safe)。
69
+ * · `onAsk` 不供,但**这不等于 auto-deny**(codex R3-中1 验真纠正):带 `durableMandate` 的继承层
70
+ * 判 `ask` 时,core 只要还看得见 checkpoint 店 + durable 审批配置就把它**重新浮回 durable 门**
71
+ * (再 park 一次,人再批一次);deny 只是「durable 设施不在场」的兜底臂。⛔ 但**不是无条件**
72
+ * (codex R4-中1):core 对挂起链计 `suspendCount`,到 `maxSuspends`(默认 5)即终止任务而不再 park。
73
+ * · 客户端每请求 settings 折出来的层(`settings.permissions` 的 allow/deny/ask、模式派生的 fs-write
74
+ * ask 门、scratchpad 豁免……整类)park 时未持久化 ⇒ 重建不出。§2.3 已披露的残余,机制面有特征化钉
75
+ * (test/parked-revive-e2e.test.ts);真 settings→park→赎回的端到端钉未建(候件,随收口件一起)。
76
+ */
77
+ export declare function createParkedReviveInheritedGate(deps: ParkedReviveGateDeps): (row: BackgroundAgentRecord) => RebuiltInheritedGate;
78
+ //# sourceMappingURL=parked-revive-gate.d.ts.map
@@ -0,0 +1,114 @@
1
+ /**
2
+ * [1596]/[1597] 跨副本父约束重供席工厂 —— durable park 的**赎回腿**(design/181 件二 / #179)。
3
+ *
4
+ * 从 main.ts 的装配体里提出来的**唯一理由不是内聚,是可测**:main.ts 顶层 `void main()`,谁 import 它
5
+ * 谁就启动一整个服务,所以那条腿的**运行期**语义在原地一格都钉不住(此前只有源码字符串钉)。提出来之后
6
+ * 「重建出来的链到底裁决成什么样」变成可断言的事实(test/parked-revive-gate-wiring.test.ts 运行期钉 +
7
+ * 治理矩阵的第三维消费腿)。装配次序契约不变:main.ts 仍在同一位置构造,构造条件逐字保持。
8
+ */
9
+ import { join } from "node:path";
10
+ import { NodeExecutionEnv } from "@sema-agent/core";
11
+ import { createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "../deployment-governance.js";
12
+ import { applyRuntimeGovernance } from "../runtime-governance.js";
13
+ import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
14
+ /**
15
+ * host 腿裁决 env 的 cwd:**从不创建**的占位目录(resolve-spec 的 `fs-write-gate-unrooted` 同族)。
16
+ *
17
+ * 赎回腿拿不到子代的工作目录(park 时未持久化,design/181 §2.2 的 v2 候件票),而 `NodeExecutionEnv`
18
+ * 的构造要求一个 cwd。占位形的语义:**绝对形写目标照常按真身裁决**(cwd 与它们无关),相对形则在
19
+ * 一个不存在的根上 canonicalize —— 段匹配照旧命中(守卫段是路径**段**判据),方向 fail-closed。
20
+ * 真正兜住相对形的是构造口内的 `RelativeTargetLexicalEnv` 词法臂(`PathAdjudication.cwd` 缺席即自动补层)。
21
+ * ⚠️ core 5.19.0(#108)后这句话的射程变窄了:守卫改按 `ToolCallRequest.cwd ?? rootPath` 解析写目标,
22
+ * 引擎盖了戳的调用**根本走不到**本占位根(戳优先),词法臂也随之让位给真身裁决(全文见
23
+ * `deployment-governance.ts` 的 `RelativeTargetLexicalEnv` 类注)。本占位根今天只在缺戳形上承重,
24
+ * 语义不变、方向仍 fail-closed,故保留不动。
25
+ * 绝不回落 `process.cwd()`:那是**服务自己的**启动目录,把它当成子代的盘是拿错基准(resolve-spec W4 同案)。
26
+ */
27
+ function hostGateCwd(config, localRoot) {
28
+ return join(config.localDataRoot ?? localRoot, "parked-revive-unrooted");
29
+ }
30
+ /**
31
+ * 赎回腿的父约束链重建(design/181 件二)。
32
+ *
33
+ * 语义:**同/跨副本一致地重建「部署 ⊇ 操作员」两层完整链**——审批基线(durable question 门 + F4 高危写
34
+ * 审批门 + 会话豁免探针)作 base 的 `toolPolicy` 座,经**折叠属主** `applyRuntimeGovernance`
35
+ * (→ core `tightenTaskSpec`)叠上部署治理段(autonomy / commandPolicy / MANUAL_MODE_SHELL_GATE /
36
+ * 守卫集),取其 `toolPolicy` 装进单层 `parentConstraints`。
37
+ *
38
+ * 🔴 **禁 memoize**:构造整体在 per-row lambda **内**。`autonomy`/`commandPolicy`/守卫集/审批四旋钮都是
39
+ * 热改字段(registry 热应用换 config 引用),boot 期铸一次 = 把治理冻在启动那一刻的值上,而 resume 腿的
40
+ * 书面语义是「按**当前** config 重折」(与 resolve-spec 的审批基线读活 config 同一姿势)。
41
+ *
42
+ * 🔴 **单层链是正确的响亮拒**(design/181 §2.5 特征化格):本腿恒建 count=1。嵌套子代(孙代 park,
43
+ * count≥2)的重建本腿做不到——跨副本连「祖先各层分别是什么」都没有持久化——于是 core 的 pre-CAS
44
+ * `resume.parent_constraint_mismatch` 会响亮拒绝,checkpoint 留 pending。**不许**顺手把它补齐成静默单层:
45
+ * 那等于让孙代在一条比它挂起时更松的祖先链下复活。
46
+ *
47
+ * 交回**两条轴**(载体上本仓供得出的全部,见 {@link RebuiltInheritedGate} 的推导注):
48
+ * · `parentConstraints` —— 上面那条重建链;
49
+ * · `shellGate` —— 治理段折出来的 shell 门档位。**必须交**:core 按 rank 取 `max(live, seed)`,不交
50
+ * 等于 `live` 缺席、max 恒等于 park 时的旧种子,于是「挂起期间运维收紧 AUTONOMY/MANUAL_MODE_SHELL_GATE」
51
+ * 在赎回腿上整条失效(codex R1-高2 的真回归,红先复现)。交了之后收紧被兑现、放松不被兑现(fail-safe)。
52
+ *
53
+ * 载体上**故意不供**的三个槽(都不是本腿产得出的):`ancestorRules`(会话权限规则,属会话面)、
54
+ * `admittedOrgScopes` / `orgAdmissionGoverned`(组织记忆准入,属 memory 面)—— 交一个瞎猜的值比不交更坏
55
+ * (core 对这三个都是 seed 兜底)。
56
+ *
57
+ * 时点语义(成文进 docs/ASSISTANT-WIRE-CONTRACT.md,免运维当 bug 报):
58
+ * · toolPolicy 轴 = **当前** config(与 resume 重折一致,收紧与放松都被兑现——放松是一次显式运维动作);
59
+ * · shellGate 轴 = core 取 max(live, park 时 seed)—— 收紧兑现、放松**不**兑现(fail-safe)。
60
+ * · `onAsk` 不供,但**这不等于 auto-deny**(codex R3-中1 验真纠正):带 `durableMandate` 的继承层
61
+ * 判 `ask` 时,core 只要还看得见 checkpoint 店 + durable 审批配置就把它**重新浮回 durable 门**
62
+ * (再 park 一次,人再批一次);deny 只是「durable 设施不在场」的兜底臂。⛔ 但**不是无条件**
63
+ * (codex R4-中1):core 对挂起链计 `suspendCount`,到 `maxSuspends`(默认 5)即终止任务而不再 park。
64
+ * · 客户端每请求 settings 折出来的层(`settings.permissions` 的 allow/deny/ask、模式派生的 fs-write
65
+ * ask 门、scratchpad 豁免……整类)park 时未持久化 ⇒ 重建不出。§2.3 已披露的残余,机制面有特征化钉
66
+ * (test/parked-revive-e2e.test.ts);真 settings→park→赎回的端到端钉未建(候件,随收口件一起)。
67
+ */
68
+ export function createParkedReviveInheritedGate(deps) {
69
+ const { config, question, approvalExemptionStore, logger, localRoot } = deps;
70
+ const slots = deps.slots ?? sandboxPathEnvSlots;
71
+ return (row) => {
72
+ // lane 分形与 resolve-spec 同一判别式(单一属主,取值处只此一个)。
73
+ // 🔑 沙箱腿的 slot 键 = **row.sessionId**(子代自己的会话,design/181 §2.2 裁定):revive 后 core 以
74
+ // 该 sessionId 铸 env 并注册 slot(subagent → prepare-task → 工厂装饰器),这个键真能绑上。
75
+ // 键 rootSessionId 跨副本恒 unbound ⇒ 每一次结构化写 fail-closed ⇒ 整条腿瘫痪。
76
+ // 未绑时转发面报错 → core 读成 unresolvedSymlink → 守卫策略 deny(成文的 fail-closed 行为)。
77
+ const pathAdjudication = isSandboxPathAdjudicationLane(config.remoteExec?.provider)
78
+ ? {
79
+ env: new DeferredSandboxPathEnv({
80
+ sessionId: row.sessionId,
81
+ slots,
82
+ logger: { warn: (event, fields) => logger.warn(event, { ...(fields ?? {}), lane: config.remoteExec?.provider, leg: "parked_revive" }) },
83
+ }),
84
+ }
85
+ : { env: createHostAdjudicationEnv(config, localRoot) };
86
+ // 审批基线:durable 轴(问答门 + F4 审批门 + 会话豁免探针)。exempt 锚 = row.rootSessionId
87
+ // ——与 /decide 的 remember grant 同键([1591] 候裁②),重建链上的豁免语义与原链一致。
88
+ const rootSessionId = row.rootSessionId;
89
+ const baseline = createApprovalBaselinePolicy(config, {
90
+ question,
91
+ ...(approvalExemptionStore && rootSessionId
92
+ ? {
93
+ exempt: (toolName) => approvalExemptionStore.has(rootSessionId, toolName),
94
+ onExempted: (toolName, rawToolName) => logger.info("approval_exempted", { sessionId: rootSessionId, toolName, rawToolName, leg: "parked_revive" }),
95
+ }
96
+ : {}),
97
+ });
98
+ // 折叠属主 = applyRuntimeGovernance → core tightenTaskSpec(本腿一条 policy 都不自己合成)。
99
+ // `objective: ""` 只是折叠所需的最小 TaskSpec 座——本腿只取回 toolPolicy,spec 的其余面不出本函数。
100
+ const governed = applyRuntimeGovernance({ objective: "", toolPolicy: baseline }, createDeploymentGovernanceInputs(config, pathAdjudication));
101
+ // 治理段无可施加时 applyRuntimeGovernance 原样返回 base ⇒ 基线本身即为链(恒非 undefined,无需断言)。
102
+ // shellGate 只在治理段真产出档位时出现在载体上(缺席 = 本次没有 live 值可交,core 用 seed 兜底)。
103
+ return {
104
+ parentConstraints: [{ policy: governed.toolPolicy ?? baseline, durableMandate: true }],
105
+ ...(governed.shellGate !== undefined ? { shellGate: governed.shellGate } : {}),
106
+ };
107
+ };
108
+ }
109
+ /** host 腿的裁决 env(真 fs;构造是纯字段赋值,不 spawn)。**每次赎回现铸**——与「禁 memoize」同因:
110
+ * cwd 占位读的是活 config 的 `localDataRoot`。 */
111
+ function createHostAdjudicationEnv(config, localRoot) {
112
+ return new NodeExecutionEnv({ cwd: hostGateCwd(config, localRoot) });
113
+ }
114
+ //# sourceMappingURL=parked-revive-gate.js.map
@@ -68,6 +68,8 @@ export interface ReapersCtx {
68
68
  imageBakes: ImageBake | undefined;
69
69
  worktreeReap: (() => Promise<void>) | undefined;
70
70
  workflowNotifyGate: WorkflowNotifyGate | undefined;
71
+ /** [2995]/[2999] recover 的两半 opts(boot 与周期扫描共用,单点合成在 workflow-orchestration)。 */
72
+ workflowRecoverOpts: Parameters<WorkflowNotifyGate["recover"]>[0];
71
73
  workflowJournalStore: ServiceWorkflowJournalStore | undefined;
72
74
  sqlWorkflowRunStore: WorkflowRunStore | undefined;
73
75
  workflowNotifyJournal: WorkflowNotifyJournalStore | undefined;
@@ -74,7 +74,7 @@ export function createThrottledReaperCatch(name, logger, threshold = REAPER_FAIL
74
74
  }
75
75
  /** 起后台维护 tick,返回定时器 handle(收尾段 clearInterval 用)。 */
76
76
  export function startReapers(ctx) {
77
- const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, rateLimiter, costQuota, toolResultStore, fileSnapshotStore, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, getRunDenySweep, } = ctx;
77
+ const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, rateLimiter, costQuota, toolResultStore, fileSnapshotStore, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, getRunDenySweep, } = ctx;
78
78
  // S7 (SILENT-FALLBACK P0-d): the sweeps' return counts were discarded — an instance death that batch-fails
79
79
  // N orphans was indistinguishable from organic failures. Count + log ONLY when a sweep flipped rows (>0),
80
80
  // so healthy ticks stay silent. Tolerant of void-returning stores (typeof guard).
@@ -112,8 +112,15 @@ export function startReapers(ctx) {
112
112
  const scratchpadSweepStaleGuard = createThrottledReaperCatch("scratchpad_sweep_stale", logger);
113
113
  const approvalReconcileGuard = createThrottledReaperCatch("approval_reconcile", logger);
114
114
  let approvalReconcileInFlight = false; // 车5 §8 C-8:重入守卫(邻居四先例同形——本腿时长随 PARKING 行数增长)
115
- // #151 车5(design/172 §3.0 对账收敛器 + 崩溃恢复扫描):**总开关默认 OFF 本腿根本不注册**
116
- // (`STREAM_APPROVAL_ENABLED=false` 的部署一行库都不扫,零新增查询)。开且有 durable backend 时才建。
115
+ // #151 车5(design/172 §3.0 对账收敛器 + 崩溃恢复扫描):建腿条件 = 开关开 ∧ **有 `StoreBackend`**
116
+ // (见下方判据行 —— 是**任意** backend,不限 SQL twins:`local` 车道也算,它的 `approvalAsk()` 是进程内
117
+ // `InMemoryApprovalAskStore`,扫的是内存不是库)。无 backend 或显式关 ⇒ 本腿根本不注册,零扫描。
118
+ // ⚠️ 运维读数已反转:总开关默认**在树上已是 ON**(发布线上落在 7.4.0 之后的下一发布;版本坐标与
119
+ // 「别按 /health 版本号推默认」的告诫见属主 config.ts 的 `streamApprovalConfig()` 头注),
120
+ // 所以「零新增扫描」从**默认态**变成了 **opt-out 态** —— 任何配了 backend 的部署(含单用户 local)
121
+ // 默认都会注册本腿并每 tick 扫两段(PARKING + 孤儿 STREAM_PENDING,每段
122
+ // `STREAM_APPROVAL_RECONCILE_BATCH` 行,默认 200);SQL twins 上这就是**真新增的周期性查询**,local 上
123
+ // 只是内存遍历。存量部署升级后要回到零扫描,需显式把开关关掉。
117
124
  // 判据 1/2 的两个读口按能力面接:checkpoint 店的窄读口只有 SQL twins 有(local 车道的店没有这个面),
118
125
  // 缺席 ⇒ 判据 1 恒不命中(诚实降级,行落 ②③④⑤),不是「假装没 checkpoint 就该 DENY」。
119
126
  const approvalReconciler = (() => {
@@ -354,7 +361,7 @@ export function startReapers(ctx) {
354
361
  // run whose in-process notify was lost, AND finalizes-as-abandoned a `running` run orphaned past the grace
355
362
  // window (core never resumes/reaps a prior `running` row, so nothing else would). Idempotent; best-effort.
356
363
  void workflowNotifyGate
357
- ?.recover({ orphanGraceMs: config.workflowOrphanGraceMs })
364
+ ?.recover(workflowRecoverOpts)
358
365
  .then(() => workflowNotifyRecoverGuard.onSuccess(), workflowNotifyRecoverGuard.onError);
359
366
  // SVC-2 (adversarial-review HIGH): time-based GC of the workflow_journal table (the heaviest, TaskResult-bearing
360
367
  // one) — the per-run deleteByRun has no run-store reap hook, so this bounded sweep is what stops unbounded