@sema-agent/server 7.114.0 → 7.116.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 (159) hide show
  1. package/MIGRATION.md +237 -2
  2. package/USAGE.md +39 -3
  3. package/deploy/sema-up/cluster-up.sh +13 -1
  4. package/deploy/sema-up/kube-up.sh +13 -1
  5. package/deploy/sema-up/preflight.sh +13 -1
  6. package/deploy/sema-up/sema-up.sh +13 -1
  7. package/deploy/sema-up/smoke.sh +13 -1
  8. package/dist/adoption/wire.d.ts +0 -3
  9. package/dist/approval-ask-audit-store.d.ts +25 -6
  10. package/dist/approval-ask-audit-store.js +6 -1
  11. package/dist/approval-card.d.ts +25 -24
  12. package/dist/approval-card.js +14 -0
  13. package/dist/approval-crash-convergence.d.ts +60 -0
  14. package/dist/approval-crash-convergence.js +75 -0
  15. package/dist/approval-deny-reasons.d.ts +13 -0
  16. package/dist/approval-deny-reasons.js +2 -0
  17. package/dist/approval.js +7 -3
  18. package/dist/boot/coordinators.js +0 -12
  19. package/dist/boot/execution-env.js +2 -1
  20. package/dist/boot/exit-boundary.d.ts +3 -2
  21. package/dist/boot/exit-boundary.js +3 -0
  22. package/dist/boot/resolve-spec.js +5 -1
  23. package/dist/boot/shutdown.d.ts +19 -0
  24. package/dist/boot/shutdown.js +12 -1
  25. package/dist/boot/stage-07-capability-layer.js +21 -15
  26. package/dist/boot/stores.js +4 -4
  27. package/dist/capabilities/composite-members.d.ts +1 -0
  28. package/dist/capabilities/composite-members.js +1 -0
  29. package/dist/capabilities/hands-lane.d.ts +0 -23
  30. package/dist/capabilities/hands-lane.js +3 -2
  31. package/dist/capabilities/send-user-file-tool.d.ts +10 -22
  32. package/dist/capabilities/send-user-file-tool.js +1 -8
  33. package/dist/capability-facts.d.ts +296 -0
  34. package/dist/capability-facts.js +354 -0
  35. package/dist/config-catalog.js +2 -1
  36. package/dist/config-center/apply-effective.js +11 -11
  37. package/dist/config-provider.d.ts +16 -0
  38. package/dist/config-provider.js +11 -2
  39. package/dist/config-types.d.ts +20 -1
  40. package/dist/config.d.ts +55 -29
  41. package/dist/config.js +48 -25
  42. package/dist/deployment-governance.d.ts +15 -4
  43. package/dist/deployment-governance.js +20 -2
  44. package/dist/fleet/fleet-bus.d.ts +1 -22
  45. package/dist/hooks/hook-runner.d.ts +29 -3
  46. package/dist/hooks/hook-runner.js +30 -17
  47. package/dist/http/admission.d.ts +6 -0
  48. package/dist/http/admission.js +29 -7
  49. package/dist/http/approval-sweeps.js +20 -0
  50. package/dist/http/checkpoint-ctx.d.ts +68 -0
  51. package/dist/http/checkpoint-ctx.js +36 -0
  52. package/dist/http/durable-run-leg.js +2 -1
  53. package/dist/http/principal-gate.d.ts +3 -0
  54. package/dist/http/principal-gate.js +3 -0
  55. package/dist/http/resume-legs.js +19 -1
  56. package/dist/http/retired-skill-manifest.d.ts +23 -0
  57. package/dist/http/retired-skill-manifest.js +22 -0
  58. package/dist/http/route-ctx.d.ts +7 -0
  59. package/dist/http/route-table.d.ts +3 -2
  60. package/dist/http/route-table.js +5 -5
  61. package/dist/http/routes/a2a-serve.d.ts +1 -3
  62. package/dist/http/routes/a2a-serve.js +14 -23
  63. package/dist/http/routes/admin-config-refresh.js +7 -2
  64. package/dist/http/routes/admin-drain.js +7 -2
  65. package/dist/http/routes/admin-memory-optout.d.ts +0 -2
  66. package/dist/http/routes/admin-memory-optout.js +20 -7
  67. package/dist/http/routes/adoption.js +19 -5
  68. package/dist/http/routes/agents-roster.js +4 -2
  69. package/dist/http/routes/approvals-assistant.js +41 -18
  70. package/dist/http/routes/background.d.ts +0 -2
  71. package/dist/http/routes/background.js +4 -5
  72. package/dist/http/routes/capabilities.d.ts +2 -1
  73. package/dist/http/routes/capabilities.js +63 -65
  74. package/dist/http/routes/config-catalog.d.ts +4 -5
  75. package/dist/http/routes/config-catalog.js +16 -2
  76. package/dist/http/routes/devices.js +17 -11
  77. package/dist/http/routes/diagnostics.d.ts +5 -0
  78. package/dist/http/routes/diagnostics.js +8 -1
  79. package/dist/http/routes/fleet.js +4 -2
  80. package/dist/http/routes/images.js +30 -3
  81. package/dist/http/routes/leader.js +7 -1
  82. package/dist/http/routes/mcp-reconnect.js +17 -10
  83. package/dist/http/routes/memory-bundle.js +20 -6
  84. package/dist/http/routes/memory-compliance.js +19 -5
  85. package/dist/http/routes/memory-consolidation.d.ts +9 -12
  86. package/dist/http/routes/memory-consolidation.js +17 -3
  87. package/dist/http/routes/memory-origin.js +19 -5
  88. package/dist/http/routes/notify-wake.js +5 -3
  89. package/dist/http/routes/observability.js +19 -5
  90. package/dist/http/routes/retention-ops.js +6 -1
  91. package/dist/http/routes/rules.js +49 -21
  92. package/dist/http/routes/run-memory-capture.js +5 -2
  93. package/dist/http/routes/runs.js +90 -60
  94. package/dist/http/routes/session-sync.js +64 -53
  95. package/dist/http/routes/sessions-list.js +8 -5
  96. package/dist/http/routes/sessions.js +40 -25
  97. package/dist/http/routes/shared-memory.js +5 -3
  98. package/dist/http/routes/tasks.js +5 -3
  99. package/dist/http/routes/trace-usage.js +18 -11
  100. package/dist/http/routes/workflows.js +36 -13
  101. package/dist/http/server.js +4 -1
  102. package/dist/http/wire-types.d.ts +6 -0
  103. package/dist/index.d.ts +1 -1
  104. package/dist/leader/wire.js +2 -1
  105. package/dist/local-data-root.d.ts +1 -1
  106. package/dist/mcp-content-origin.d.ts +0 -8
  107. package/dist/model-entry-refusal.d.ts +10 -2
  108. package/dist/model-entry-refusal.js +19 -3
  109. package/dist/namespaced-rule-name.d.ts +54 -0
  110. package/dist/namespaced-rule-name.js +33 -0
  111. package/dist/observability/corrupt-read-seat.d.ts +4 -3
  112. package/dist/observability/corrupt-read-seat.js +4 -3
  113. package/dist/observability/fail-open.d.ts +10 -9
  114. package/dist/observability/fail-open.js +5 -1
  115. package/dist/observability/permission-rule-events.d.ts +3 -0
  116. package/dist/observability/permission-rule-events.js +4 -0
  117. package/dist/permission-rule-list.d.ts +81 -0
  118. package/dist/permission-rule-list.js +17 -0
  119. package/dist/plugins/file-run-store.d.ts +7 -1
  120. package/dist/plugins/file-run-store.js +20 -7
  121. package/dist/plugins/memory-run-store.js +2 -1
  122. package/dist/plugins/one-time-migrations.d.ts +4 -2
  123. package/dist/plugins/one-time-migrations.js +26 -3
  124. package/dist/plugins/pg-session-storage.js +1 -1
  125. package/dist/plugins/run-store-sql.js +4 -4
  126. package/dist/plugins/session-policy-store-sql.js +4 -2
  127. package/dist/plugins/store-backend.d.ts +6 -1
  128. package/dist/plugins/store-backend.js +2 -1
  129. package/dist/plugins/store-contracts.d.ts +27 -8
  130. package/dist/plugins/store-contracts.js +12 -3
  131. package/dist/plugins/tidb-session-store.js +1 -1
  132. package/dist/request-key-closure.js +1 -1
  133. package/dist/run-local.d.ts +37 -3
  134. package/dist/run-local.js +52 -6
  135. package/dist/runs.d.ts +0 -58
  136. package/dist/runs.js +15 -20
  137. package/dist/runtime-governance.d.ts +21 -1
  138. package/dist/runtime-governance.js +6 -1
  139. package/dist/security.js +2 -2
  140. package/dist/session-policy-wire.d.ts +40 -59
  141. package/dist/session-policy-wire.js +60 -32
  142. package/dist/spec-fields.d.ts +32 -0
  143. package/dist/spec-fields.js +31 -16
  144. package/dist/standing-answer-grant.d.ts +8 -0
  145. package/dist/standing-answer-grant.js +5 -0
  146. package/dist/task-settings.d.ts +17 -6
  147. package/dist/task-settings.js +16 -30
  148. package/dist/tool-approval.d.ts +32 -4
  149. package/dist/tool-approval.js +31 -8
  150. package/dist/trace/core-keyset-guard.d.ts +13 -8
  151. package/dist/trace/project.js +9 -0
  152. package/dist/trace/self-minted-wire.d.ts +42 -0
  153. package/dist/trace/self-minted-wire.js +13 -0
  154. package/dist/user-input-ledger.d.ts +8 -5
  155. package/package.json +6 -4
  156. package/scripts/drain-check-local.mjs +528 -0
  157. package/scripts/drain-check.mysql.sql +67 -0
  158. package/scripts/drain-check.pg.sql +71 -0
  159. package/scripts/retired-mode-seed-drain-local.mjs +0 -317
@@ -0,0 +1,81 @@
1
+ /**
2
+ * **规则表的唯一编译口** —— 一张工具名规则表(`allow` / `ask` / `deny` 之一)编成一只 `ToolPolicy`。
3
+ *
4
+ * 两类读者,同一只编译口(编译与校验各只有一份,不在别处另写第二份文法):
5
+ * · 请求腿的 `settings.permissions.{allow,ask,deny}` 三桶(`task-settings.ts` 的 `deriveSettingsPolicy`);
6
+ * · 运维的 allow 规则 `APPROVAL_GRANT`(`runtime-governance.ts` 的 `applyRuntimeGovernance`,经部署治理链的单一构造口
7
+ * `deployment-governance.ts` 交进来 —— HTTP 提交 / durable 续跑 / 停驻后台 agent 的跨副本赎回 / run-local 四条腿同装)。
8
+ *
9
+ * 为什么住在一只叶模块里(不留在 `task-settings.ts`):运维那一侧的启动校验在 `config.ts` 里跑(坏值 = 拒启),而
10
+ * `task-settings.ts` 值引 `config.ts` —— 编译口留在那里,`config.ts` 要调它就闭合一只 ESM 装载环。本模块只 import core 与
11
+ * `text-bidi.ts`(纯叶),三处读者引它都不成环。
12
+ */
13
+ import { type PathRuleBases, type RuleBehavior, type ToolPolicy } from "@sema-agent/core";
14
+ /** 一张表里规则条数的上限 —— 请求腿 wire 门(`task-settings.ts` 的三桶判形)读的也是这一个名字:core 的缺省帽是 256,
15
+ * 不对齐的话第 257 条起编译当场抛,等于服务端自己收下的东西自己反悔。 */
16
+ export declare const MAX_PERMISSION_RULES_PER_LIST = 1000;
17
+ /** A name-keyed rule-list ToolPolicy: a tool named by `names` gets `behavior` (an `allow` hit states the allow with
18
+ * `decisionReason: "rule"`; `ask` tightens to an approval gate; `deny` hard-blocks); every other tool is out of
19
+ * scope (`defaultAction: "allow"` = this policy has no opinion about anything it does not name). Composed deny-wins
20
+ * with the rest of the fold, so `ask` / `deny` only ever tighten and an `allow` never overrides a deny or an ask.
21
+ *
22
+ * `source` = 这张表**写在哪**(请求腿 `settings.permissions.<桶>`;运维腿 `APPROVAL_GRANT`)。它盖进每条规则(core 的 deny / ask
23
+ * 句里点名它:`flagged by permission rule "mcp__figma__*" (settings.permissions.ask)`),编不出来时的错误句也点名它;只是出处,
24
+ * 不带任何优先级(层序在各自的折叠里)。
25
+ *
26
+ * 🔴 [ref] / core [ref] —— 本函数曾是 `new Set(askNames).has(req.toolName)` 的**逐字**比对,那是
27
+ * **安全轴上的静默 fail-open**:CC 自家 settings validator 推荐的 covering 拼法
28
+ * (`"MCP rules do not support patterns in parentheses … use mcp__srv__*"`)在逐字比对下永远配不上真工具名
29
+ * `mcp__figma__get_file` ⇒ 一次不问、直跑、审计静音。两道兜底也各自放行:`unmatchableToolNamesRefusal`
30
+ * 对 `mcp__<peer>__*` 判「合法 MCP 全名」(server 不持挂载表,动态宇宙不能按名单否定),`nameSets` 审计面
31
+ * 报的是「没匹配上的名字」而 covering 名在 core 眼里**是**匹配得上的。同一份 settings 里
32
+ * `deny:["mcp__figma__*"]` 却**真拦** —— deny 走 `createAllowDenyPolicy`,它用的是 core 的
33
+ * `namespacedCoveringEntries/Hit`。同族分家,差的只是解析器。
34
+ *
35
+ * ⇒ **规则表的 policy 面现在同源**:三种态度都由 `createPermissionRulePolicy` 的 covering 车道解,底下是
36
+ * core 的**同一只** `namespacedRuleNameCovers`(协议无关:`mcp__`/`a2a__` 同解;peer 段精确比;工具段 `*`/glob 覆盖)。
37
+ * 本仓**不自写第二套 glob** —— 那正是当年分家的成因。
38
+ *
39
+ * ⚠️ 「同源」的**射程**说准了免得下一个人当成全仓结论(codex 交叉轮 finding-1 追出来的边):
40
+ * · 同源的是**本编译口**认的 covering。表**进门前**还有一道更外层的判据(`config.ts` 的 `unmatchableToolNamesRefusal`,
41
+ * 请求腿经 `task-settings.ts` 的 `rejectUnmatchableSettingsNames`、运维腿经 `config.ts` 的 `approvalGrantRefusal`),它把
42
+ * `<ns>__<peer>` **两段形**(`mcp__figma`)判成「缺段名」拒掉 —— 而 core 三只 policy 都认这个形(实测:
43
+ * `createAllowDenyPolicy`/`createPermissionRulePolicy`/`createApprovalPolicy` 对 `mcp__figma` 一律解成 covering)。
44
+ * 所以各表收 `mcp__figma__*`(等价、且被推荐)、拒 `mcp__figma`。
45
+ * · **同源的射程已扩到 operator 名单腿**([ref] 孪生修,[ref]):`createDurableAskPolicy`(`src/approval.ts`)的
46
+ * deny/neverAuto/require 三集合过去是逐字 `Set.has`,自 core 5.60.1 谓词根导出后改成「精确 ∪ covering」,用的是
47
+ * **同一只** `namespacedRuleNameCovers`。⇒ env `APPROVAL_*` 腿 / 中心下发 `runtime.approvalRequire` 腿 / 规则表三家
48
+ * 现在对 covering 拼法的解读逐字一致,`mcp__figma__*` 在哪条腿上都是真判。
49
+ * · **两段形为什么仍不放开**(理由已换,别再引「approval.ts 还不认」):它一名两义——「整台服务器」与「三段名写漏一段」
50
+ * 在 boot 期不可分辨,所以各条腿一律响亮拒 + 指引给无歧义的 `mcp__figma__*`。放宽是各腿同时变宽的 behavior-facing
51
+ * 改动,走独立批的三问。详见 `unmatchableToolNamesRefusal` 顶注。
52
+ *
53
+ * 三处刻意的取舍,都是亲读 core dist 后定的:
54
+ * · `onInvalidRule:"throw"`(缺省)—— 一条编不出来的规则整表拒(请求腿 → resolve-spec 翻 422;运维腿 → 拒启),抛之前补上
55
+ * 出处名(core 的原文只说 "createPermissionRulePolicy: …",不说是哪张表出的错)。[ref] 的括号形在**更外层**已被拒,够不着
56
+ * 这里;真正新增的是「名字里带反斜杠」「单条 > 512 字符」这两族永不匹配的拼法。
57
+ * ✅ **不对称已收口([ref]①,[ref] 销)**:codex finding-2 当年如实登记的「同样两族拼法在 `deny`
58
+ * 上仍是静默 no-op」已修 —— 判据搬进单点成文的 `unmatchableToolNamesRefusal`,它拿 core 自己的 `validatePermissionRules`
59
+ * 对同一串跑一遍文法(不自铸第二套判词),于是三张表对这两族说同一句话。S-654 (c) 起 `allow` 也收。
60
+ * ⚠️ 存量 resume:带这两族拼法的**已存 body** 在 resume 腿会 422(resolve-spec 翻)。这**不是**新造的姿态——A3
61
+ * (`rejectUnmatchableSettingsNames`)早就对存量 body 里的退役名/短名同样抛,且 `applyTaskSettings` 腿有专门的回归钉。
62
+ * 判据同一条:一个**静默不存在的审批门**比一次响亮失败更贵。codex 建议的「给非法存量条目保留旧的逐字比对」被**驳回**:
63
+ * 那正是把 fail-open 请回来。
64
+ * · `caps.maxRules` 对齐 {@link MAX_PERMISSION_RULES_PER_LIST}(理由见那只常量)。core 的 set-level cap 连 `skip` 都照抛,
65
+ * 所以运维腿的启动校验还要**试编一次**(`approvalGrantRefusal`)才拦得住超帽 —— 判据仍是本函数这一只。
66
+ * · 不再自铸 `decisionReason:"rule"` —— core 的规则策略本就不铸这个键(allow 命中由 core 自己铸 `"rule"`),且 core 侧唯一区分它的
67
+ * 两处(prepare-task 的继承 grant 复用判据、park 的 rule-suggestion 门)对 `"rule"` 与 `undefined` **同待**(只把 `"hook"`
68
+ * 排除在外),dist 亲读在案。审计归因改由 `matchedAskRule` + `source` 承担,后者比前者强:它还让 core 的 auto-mode 分类器
69
+ * **拒绝**替人清掉这条 ask(「运维写下的『每次问我』不是分类器的犹豫」),方向是收紧。
70
+ *
71
+ * 🔴 **S-654 (c)(7.115.0,core 7.40.0 #1065):三种态度同一个编译口,`allow` 也走这里** —— `behavior: "allow"` 的规则命中 = core 带
72
+ * `decisionReason: "rule"` 的放行 —— allow 规则给出「有人表态」—— 它回答的是没人表态的服务器工具 / A2A 技能调用;内置工具上引擎自己的写门、shell 检查照问,deny / ask 照赢(与参照实现的 `permissions.allow` 在内置工具上**不同**:那边 `allow: ["Edit"]` 跳过编辑确认,这里不放掉引擎的写门);未命中 = `defaultAction: "allow"` 的裸 allow
73
+ * = 不表态、照常判。修前请求腿的 allow 编成 `createAllowDenyPolicy({ allow })` 的**名单**(名单在场 ⇒ 名单外的工具一律拒),与写它的人要的
74
+ * 「替这些工具表态」方向相反;终端因此从不投这只桶。core 7.40.0 起没人表态的服务器工具调用要问,allow 规则就是那张卡的常设答案
75
+ * (`mcp__<server>__*` 一条覆盖整台服务器)。allow 规则**不越过** deny / ask:折叠 deny > ask > allow,任何一层的 deny / ask 照赢;
76
+ * 它只在没人反对时给出那句表态。core 的 allow 文法只收工具名形(精确名 / `mcp__`·`a2a__` covering 形),内容形放行(`Bash(npm:*)` /
77
+ * `Edit(src/**)` / 参数级)core 拒编,两条腿与 deny / ask 同一个出口(判据 `unmatchableToolNamesRefusal`)。
78
+ * 7.115.0 起运维的 `APPROVAL_GRANT` 也编在这里(无人值守腿没有请求体,那是运维为自己接的服务器表态的唯一位置)。 */
79
+ /** `behavior` = 这张表的态度 —— 型直接是 core 的 `RuleBehavior`(不手抄:core 加一种态度,这里与它同步、读者 tsc 上见到)。 */
80
+ export declare function createRuleListPolicy(names: readonly string[], behavior: RuleBehavior, source: string, pathBases: PathRuleBases | undefined): ToolPolicy;
81
+ //# sourceMappingURL=permission-rule-list.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { createPermissionRulePolicy } from "@sema-agent/core";
2
+ import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
3
+ export const MAX_PERMISSION_RULES_PER_LIST = 1_000;
4
+ export function createRuleListPolicy(names, behavior, source, pathBases) {
5
+ try {
6
+ return createPermissionRulePolicy(names.map((name) => ({ rule: name, behavior, source })), {
7
+ defaultAction: "allow",
8
+ caps: { maxRules: MAX_PERMISSION_RULES_PER_LIST },
9
+ ...(pathBases !== undefined ? { pathBases } : {}),
10
+ });
11
+ }
12
+ catch (e) {
13
+ const coreMsg = (e instanceof Error ? e.message : String(e)).replace(CONTROL_AND_BIDI_STRIP_RE, "");
14
+ throw new Error(`${source.replace("settings.", "")} has rule(s) core cannot compile — ${coreMsg}`, { cause: e });
15
+ }
16
+ }
17
+ //# sourceMappingURL=permission-rule-list.js.map
@@ -182,7 +182,7 @@ export declare class FileRunStore {
182
182
  /** Release any claim whose row is now terminal; KEEP suspended/needs_review (resume re-finds them). One place for
183
183
  * the claim-release invariant, shared by reapRunning + reclaimOrphanedAtBoot. */
184
184
  private sweepClaims;
185
- /** Flip every `running` row matching `pred` → failed (with `reason`), then sweep terminal claims. */
185
+ /** Flip every `running` row matching `pred` → failed (with the death sentence `reason` + its machine `code`), then sweep terminal claims. */
186
186
  private reapRunning;
187
187
  reapStale(olderThanMs: number): Promise<number>;
188
188
  /** TOC integration ask③ (INTEGRATION-DESIGN §11): a host-lane engine restart leaves the PREVIOUS process's
@@ -320,6 +320,12 @@ export declare class FileRunStore {
320
320
  * @returns 被改写的账本**行**数(不是 run 数)。
321
321
  */
322
322
  migrateRetiredAskOriginRows(): Promise<number>;
323
+ /**
324
+ * 一次性迁移 **M6 的 local 腿**(窗、删法与删后谁接住:窗口见 `plugins/one-time-migrations.ts` 登记表):把 ≤7.115.x 写下的死亡判决行
325
+ * (`failed` ∧ 码列空 ∧ 句子是那两句,判据 = `readLegacyHolderDeathCode`)补上机读码,盘上(`run.json` 整份原子替换,同 `persist`)与内存同改。
326
+ * 起服时调(`LocalBackend.runOneTimeMigrations`,任何读者之前);数据根独占 ⇒ 本车道没有滚动升级窗,不需要读侧半场。幂等:第二跑零命中零写盘。
327
+ */
328
+ migrateLegacyHolderDeathCodes(): number;
323
329
  /** Release every open event-log fd (called by LocalBackend.close so a graceful restart can re-open the data dir). */
324
330
  dispose(): void;
325
331
  }
@@ -7,7 +7,7 @@ import { randomBytes } from "node:crypto";
7
7
  import { AppendLog, atomicWriteFile, ensureDir, isProcessLive, processFingerprint, sanitizePathComponent, writeThenLink } from "@sema-agent/core";
8
8
  import { recordFailOpen } from "../observability/fail-open.js";
9
9
  import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
10
- import { RUN_ORPHANED_ERROR, RUN_STALLED_ERROR, StoreBootRefusalError, buildRunOrphanedError } from "./store-contracts.js";
10
+ import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR, StoreBootRefusalError, buildRunOrphanedError, readLegacyHolderDeathCode } from "./store-contracts.js";
11
11
  import { notifyRunTerminal } from "../observability/run-terminal.js";
12
12
  import { migrateRetiredAskOrigin } from "./one-time-migrations.js";
13
13
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
@@ -573,13 +573,14 @@ export class FileRunStore {
573
573
  this.releaseClaim(sessionId);
574
574
  }
575
575
  }
576
- reapRunning(pred, reason) {
576
+ reapRunning(pred, reason, code) {
577
577
  let reaped = 0;
578
578
  for (const r of this.runs.values()) {
579
579
  if (r.status === "running" && pred(r)) {
580
580
  r.status = "failed";
581
581
  r.result = null;
582
582
  r.error = reason;
583
+ r.errorCode = code;
583
584
  r.updatedAt = new Date();
584
585
  this.persist(r);
585
586
  reaped += 1;
@@ -590,7 +591,7 @@ export class FileRunStore {
590
591
  }
591
592
  async reapStale(olderThanMs) {
592
593
  const cutoff = Date.now() - olderThanMs;
593
- return this.reapRunning((r) => r.updatedAt.getTime() < cutoff, RUN_STALLED_ERROR);
594
+ return this.reapRunning((r) => r.updatedAt.getTime() < cutoff, RUN_STALLED_ERROR, RUN_HOLDER_DEATH_CODES.stalled);
594
595
  }
595
596
  async reclaimOrphanedAtBoot(isResumable) {
596
597
  const orphans = [];
@@ -602,6 +603,7 @@ export class FileRunStore {
602
603
  r.status = "failed";
603
604
  r.result = null;
604
605
  r.error = RUN_ORPHANED_ERROR;
606
+ r.errorCode = RUN_HOLDER_DEATH_CODES.orphaned;
605
607
  r.updatedAt = new Date();
606
608
  this.persist(r);
607
609
  }
@@ -730,7 +732,7 @@ export class FileRunStore {
730
732
  const verdict = await this.judgeClaim(sessionId, claim, holder);
731
733
  if (verdict.kind === "live")
732
734
  return { ok: false, activeTaskId: verdict.holderTaskId ?? holder };
733
- this.takeOverClaim(sessionId, verdict.reason, verdict.snapshot);
735
+ this.takeOverClaim(sessionId, verdict.reason, verdict.snapshot, verdict.code);
734
736
  }
735
737
  }
736
738
  try {
@@ -806,9 +808,9 @@ export class FileRunStore {
806
808
  pidStillRunning: owner !== undefined && isProcessLive({ pid: owner.pid }),
807
809
  note: "the claim's owner process is provably not the one that wrote it (fingerprint falsified, or the pid is gone) and the row has nothing left to resume — taking the claim over",
808
810
  });
809
- return { kind: "stale", reason: buildRunOrphanedError(who), snapshot };
811
+ return { kind: "stale", reason: buildRunOrphanedError(who), snapshot, code: RUN_HOLDER_DEATH_CODES.orphaned };
810
812
  }
811
- takeOverClaim(sessionId, reason, snapshot) {
813
+ takeOverClaim(sessionId, reason, snapshot, code) {
812
814
  const nowClaim = readClaimRecord(singleRecordOf(this.readRecords(this.activePath(sessionId), CLAIM_RECORD_SHAPE)), sessionId);
813
815
  if (snapshot.torn) {
814
816
  if (nowClaim !== undefined)
@@ -820,7 +822,7 @@ export class FileRunStore {
820
822
  if (row?.status !== snapshot.status || row?.updatedAt.getTime() !== snapshot.updatedAtMs)
821
823
  return false;
822
824
  if (row && (row.status === "running" || row.status === "suspended" || row.status === "needs_review")) {
823
- this.persist({ ...row, status: "failed", error: reason, updatedAt: new Date() });
825
+ this.persist({ ...row, status: "failed", error: reason, ...(code !== undefined ? { errorCode: code } : {}), updatedAt: new Date() });
824
826
  }
825
827
  const quarantine = join(this.tmpDir, `stale-claim.${sanitizePathComponent(sessionId)}.${process.pid}.${randomBytes(6).toString("hex")}`);
826
828
  try {
@@ -899,6 +901,17 @@ export class FileRunStore {
899
901
  }
900
902
  return migrated;
901
903
  }
904
+ migrateLegacyHolderDeathCodes() {
905
+ let migrated = 0;
906
+ for (const r of this.runs.values()) {
907
+ const code = readLegacyHolderDeathCode(r);
908
+ if (code === null || r.errorCode === code)
909
+ continue;
910
+ this.persist({ ...r, errorCode: code });
911
+ migrated += 1;
912
+ }
913
+ return migrated;
914
+ }
902
915
  dispose() {
903
916
  for (const log of this.eventLogs.values()) {
904
917
  try {
@@ -1,7 +1,7 @@
1
1
  import { redactErrorMessage, redactLedgerEventData, sealRunRowResult } from "../observability/run-terminal.js";
2
2
  import { outwardPlane } from "../terminal.js";
3
3
  import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
4
- import { RUN_STALLED_ERROR } from "./store-contracts.js";
4
+ import { RUN_HOLDER_DEATH_CODES, RUN_STALLED_ERROR } from "./store-contracts.js";
5
5
  import { notifyRunTerminal } from "../observability/run-terminal.js";
6
6
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
7
7
  export class MemoryRunStore {
@@ -300,6 +300,7 @@ export class MemoryRunStore {
300
300
  r.status = "failed";
301
301
  r.result = null;
302
302
  r.error = RUN_STALLED_ERROR;
303
+ r.errorCode = RUN_HOLDER_DEATH_CODES.stalled;
303
304
  r.updatedAt = new Date();
304
305
  reaped += 1;
305
306
  }
@@ -22,6 +22,8 @@ export interface OneTimeMigrationReport {
22
22
  readonly sessionIncarnationColumn: "added" | "present" | "no-table" | "lazy-on-read";
23
23
  /** M5:本次给空化身铸了令牌的行数。非零 = 这次迁移真写了存量行,必须让运维看见(与 M4 同一条纪律)。 */
24
24
  readonly sessionIncarnationsBackfilled: number;
25
+ /** M6:本次给「只有句子」的死亡判决行补上机读码的 run 行数(SQL `task_run` / local `run.json`)。非零 = 这次迁移真写了存量行(与 M4 同一条纪律)。 */
26
+ readonly holderDeathCodeRowsBackfilled: number;
25
27
  }
26
28
  /**
27
29
  * S-590 —— 一条一次性迁移的窗(登记表的一行;纯数据)。字段注写着每个值的取证方式,改一行之前照着重核。
@@ -54,8 +56,8 @@ export interface OneTimeMigrationWindow {
54
56
  * 而留着 M3 会让 M3 永远判「跑过了」。三条同版引入、同一天到期,本来就同一次删。
55
57
  */
56
58
  export declare const ONE_TIME_MIGRATIONS: readonly OneTimeMigrationWindow[];
57
- /** local 车道上 SQL 独有的四条(M1 / M2 / M4 / M5)的读数:那几张表 / 列在文件底座上从来不存在 ⇒ 如实答 absent / 0。 */
58
- export declare const NO_SQL_MIGRATIONS: Omit<OneTimeMigrationReport, "askOriginRowsMigrated">;
59
+ /** local 车道上 SQL 独有的四条(M1 / M2 / M4 / M5)的读数(M3 / M6 两车道都有,local 腿的读数由 `LocalBackend.runOneTimeMigrations` 现算):那几张表 / 列在文件底座上从来不存在 ⇒ 如实答 absent / 0。 */
60
+ export declare const NO_SQL_MIGRATIONS: Omit<OneTimeMigrationReport, "askOriginRowsMigrated" | "holderDeathCodeRowsBackfilled">;
59
61
  /** 启动期的日志口(logger 的子集)。两级各有其用,见 {@link logOneTimeMigrations}。 */
60
62
  export interface MigrationLogSink {
61
63
  warn(msg: string, meta?: Record<string, unknown>): void;
@@ -2,6 +2,7 @@ import { acquireEnsureSchemaLock, releaseEnsureSchemaLock } from "./tidb-pool.js
2
2
  import { acquirePgEnsureSchemaLock, releasePgEnsureSchemaLock } from "./pg-pool.js";
3
3
  import { jsonColumnTextExpr } from "./sql-json-column.js";
4
4
  import { SESSION_META_INCARNATION_COLUMN, mintSessionIncarnation } from "../session-incarnation.js";
5
+ import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR } from "./store-contracts.js";
5
6
  export const ONE_TIME_MIGRATIONS = [
6
7
  {
7
8
  id: "M1",
@@ -62,6 +63,19 @@ export const ONE_TIME_MIGRATIONS = [
62
63
  "`session.incarnation_missing`(parked 决定腿因此拒、记忆座退回按 id 绑定并 warn)—— 运维手工给那些行填一只随机令牌即可",
63
64
  reportKeys: ["sessionIncarnationColumn", "sessionIncarnationsBackfilled"],
64
65
  },
66
+ {
67
+ id: "M6",
68
+ introducedIn: "7.116.0",
69
+ releasedOn: "2026-10-05",
70
+ removableAfter: "2026-11-04",
71
+ removal: "删 migrateOn 的 M6 步(`task_run` 两条 `UPDATE … SET error_code`)与 local 腿 `FileRunStore.migrateLegacyHolderDeathCodes`(及 " +
72
+ "`LocalBackend.runOneTimeMigrations` 里那一格);报告与日志行的 `holderDeathCodeRowsBackfilled`;**连同读侧半场一起删**:" +
73
+ "`store-contracts.ts` 的 `readLegacyHolderDeathCode` 与 SQL 行映射 `run-store-sql.ts#rowToRun` 里对它的调用(码列改回原样读)",
74
+ afterRemoval: "没有响亮臂:窗关删码之后仍只有句子、码列为空的行(跳过了所有带 M6 的版本直接升的库,或窗关之后还在写的 ≤7.115.x 副本)不再被判成持有者死亡 —— " +
75
+ "用户输入受理账本的收敛判词(`user-input-ledger.ts` `judgeOpen`)对这种行落到「其余」臂 `leg_over`:补落 `settled{leg_end}`、不告知,那条 run 上" +
76
+ "丢掉的 steer 不再告诉用户;`GET /v1/runs/:id` 上 `errorCode` 缺席、只剩 `error` 句子",
77
+ reportKeys: ["holderDeathCodeRowsBackfilled"],
78
+ },
65
79
  ];
66
80
  export const NO_SQL_MIGRATIONS = {
67
81
  retiredSnapshotTable: "absent",
@@ -204,6 +218,12 @@ async function migrateOn(db, dialect) {
204
218
  : await db.exec(`UPDATE ${AUDIT_TABLE} SET ${AUDIT_DETAIL_COLUMN} = NULL ` +
205
219
  `WHERE ${AUDIT_DETAIL_COLUMN} IS NOT NULL AND ${jsonColumnTextExpr(dialect, AUDIT_DETAIL_COLUMN)} = 'null'`);
206
220
  const { sessionIncarnationColumn, sessionIncarnationsBackfilled } = await migrateSessionIncarnations(db, dialect);
221
+ const holderDeathCodeRowsBackfilled = (await db.exec(dialect === "tidb"
222
+ ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND error = ?"
223
+ : "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND error = $2", [RUN_HOLDER_DEATH_CODES.stalled, RUN_STALLED_ERROR])) +
224
+ (await db.exec(dialect === "tidb"
225
+ ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND (error = ? OR error LIKE ?)"
226
+ : "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND (error = $2 OR error LIKE $3)", [RUN_HOLDER_DEATH_CODES.orphaned, RUN_ORPHANED_ERROR, `${RUN_ORPHANED_ERROR} (%`]));
207
227
  let retiredSnapshotTable = "absent";
208
228
  let retiredSnapshotRows = 0;
209
229
  let retiredSteerSeatColumn = "absent";
@@ -236,7 +256,7 @@ async function migrateOn(db, dialect) {
236
256
  }
237
257
  return {
238
258
  retiredSnapshotTable, retiredSnapshotRows, retiredSteerSeatColumn, retiredSteerSeatStaleRows, askOriginRowsMigrated, auditDetailJsonNullRows,
239
- sessionIncarnationColumn, sessionIncarnationsBackfilled,
259
+ sessionIncarnationColumn, sessionIncarnationsBackfilled, holderDeathCodeRowsBackfilled,
240
260
  };
241
261
  }
242
262
  const INCARNATION_BACKFILL_BATCH = 500;
@@ -309,7 +329,7 @@ export function migrateRetiredAskOrigin(type, data) {
309
329
  }
310
330
  export function logOneTimeMigrations(log, backend, report) {
311
331
  const changed = report.retiredSnapshotTable === "dropped" || report.retiredSteerSeatColumn === "dropped" || report.askOriginRowsMigrated > 0 || report.auditDetailJsonNullRows > 0 ||
312
- report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0;
332
+ report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0 || report.holderDeathCodeRowsBackfilled > 0;
313
333
  (changed ? log.warn : log.info).call(log, "one_time_migrations_applied", {
314
334
  backend,
315
335
  retiredSnapshotTable: report.retiredSnapshotTable,
@@ -320,11 +340,14 @@ export function logOneTimeMigrations(log, backend, report) {
320
340
  auditDetailJsonNullRows: report.auditDetailJsonNullRows,
321
341
  sessionIncarnationColumn: report.sessionIncarnationColumn,
322
342
  sessionIncarnationsBackfilled: report.sessionIncarnationsBackfilled,
343
+ holderDeathCodeRowsBackfilled: report.holderDeathCodeRowsBackfilled,
323
344
  removableAfter: Object.fromEntries(ONE_TIME_MIGRATIONS.map((w) => [w.id, w.removableAfter])),
324
345
  note: "one-time persisted-data migrations (M1: retired whole-tree snapshot index table dropped; M2: retired single-seat " +
325
346
  "steering column dropped; M3: retired ask-origin spelling rewritten in place; M4: audit details written as the JSON " +
326
347
  "text `null` by an earlier version folded to a SQL NULL, so \"no detail\" has exactly one on-disk spelling; M5: the " +
327
- "session incarnation column added and every session row without one minted its own token). Each step " +
348
+ "session incarnation column added and every session row without one minted its own token; M6: run rows failed by a " +
349
+ "holder-death verdict written by an earlier version with only the sentence got the machine-readable code run.stalled / " +
350
+ "run.orphaned). Each step " +
328
351
  "runs on every boot until its window closes (the release that introduced it + 30 days; the per-step date is in " +
329
352
  "removableAfter, null while that release is not published yet) and is deleted in the first minor after that — start a " +
330
353
  "version that still carries a step at least once before upgrading past its deletion.",
@@ -317,7 +317,7 @@ export class PgSessionStore {
317
317
  return `$${params.length}`;
318
318
  };
319
319
  const where = ["sm.placement_kind IS NULL"];
320
- if (opts.owner)
320
+ if (opts.owner !== undefined)
321
321
  where.push(opts.includeUnowned ? `(sm.owner = ${ph(opts.owner)} OR sm.owner IS NULL)` : `sm.owner = ${ph(opts.owner)}`);
322
322
  if (opts.q)
323
323
  where.push(`EXISTS (SELECT 1 FROM task_run tr WHERE tr.session_id = sm.session_id AND LOWER(tr.objective_preview) LIKE LOWER(${ph(`%${escapeLike(opts.q)}%`)}) ESCAPE '\\')`);
@@ -6,7 +6,7 @@ import { pgSanitizeText } from "./pg-safe-json.js";
6
6
  import { toIso as iso } from "./sql-row-helpers.js";
7
7
  import { recordFailOpen } from "../observability/fail-open.js";
8
8
  import { createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, isJsonColumnBodyProblem, jsonColumnText, readJsonObjectColumn, readNonNullJsonValueColumn } from "./sql-json-column.js";
9
- import { RUN_STALLED_ERROR } from "./store-contracts.js";
9
+ import { RUN_HOLDER_DEATH_CODES, RUN_STALLED_ERROR, readLegacyHolderDeathCode } from "./store-contracts.js";
10
10
  import { notifyRunTerminal, runTerminalLogLevel } from "../observability/run-terminal.js";
11
11
  import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
12
12
  import { isDupKeyError } from "./sql-errors.js";
@@ -214,7 +214,7 @@ export class SqlRunStore {
214
214
  status: r.status,
215
215
  result: refusing("serve a finished run as if it had produced nothing", () => decodeResultColumn(r.result, String(r.task_id))) ?? null,
216
216
  error: r.error ?? null,
217
- errorCode: r.error_code ?? null,
217
+ errorCode: readLegacyHolderDeathCode({ status: String(r.status), error: r.error ?? null, errorCode: r.error_code ?? null }),
218
218
  jobId: r.job_id ?? null,
219
219
  source: r.source ?? null,
220
220
  instanceId: r.instance_id ?? null,
@@ -424,8 +424,8 @@ export class SqlRunStore {
424
424
  const cutoff = new Date(Date.now() - olderThanMs);
425
425
  let reaped = 0;
426
426
  await this.tx(async (conn) => {
427
- const { affected } = await conn.query(this.q(`UPDATE task_run SET status = 'failed', result = NULL, error = '${RUN_STALLED_ERROR}', updated_at = ? ` +
428
- "WHERE status = 'running' AND updated_at < ?", `UPDATE task_run SET status = 'failed', result = NULL, error = '${RUN_STALLED_ERROR}', updated_at = $1 ` +
427
+ const { affected } = await conn.query(this.q(`UPDATE task_run SET status = 'failed', result = NULL, error = '${RUN_STALLED_ERROR}', error_code = '${RUN_HOLDER_DEATH_CODES.stalled}', updated_at = ? ` +
428
+ "WHERE status = 'running' AND updated_at < ?", `UPDATE task_run SET status = 'failed', result = NULL, error = '${RUN_STALLED_ERROR}', error_code = '${RUN_HOLDER_DEATH_CODES.stalled}', updated_at = $1 ` +
429
429
  "WHERE status = 'running' AND updated_at < $2"), [new Date(), cutoff]);
430
430
  reaped = affected;
431
431
  await conn.query(this.q("DELETE ta FROM task_active ta JOIN task_run tr ON ta.task_id = tr.task_id WHERE tr.status NOT IN ('running','suspended','needs_review')", "DELETE FROM task_active ta USING task_run tr WHERE ta.task_id = tr.task_id AND tr.status NOT IN ('running','suspended','needs_review')"));
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { loosenReasons, SessionPolicyError, normalizeRules, stripRev } from "@sema-agent/core";
3
- import { readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../session-policy-wire.js";
2
+ import { assertSessionRuleNamesCurrent, loosenReasons, SessionPolicyError, normalizeRules, stripRev } from "@sema-agent/core";
3
+ import { assertSessionRuleNamesReachable, readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../session-policy-wire.js";
4
4
  import { decodeJsonColumn, isJsonColumnBodyProblem, jsonColumnText } from "./sql-json-column.js";
5
5
  import { pgHasUnstorable } from "./pg-safe-json.js";
6
6
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
@@ -104,6 +104,8 @@ export class SqlSessionPolicyStore {
104
104
  throw new SessionPolicyError("corrupt", `the session rules rev mark for session ${JSON.stringify(sessionId)} is unreadable (rev ${String(markRows[0].rev)}, gen ${String(markRows[0].gen)}) — refusing rather than stamp a rev/lineage nothing proves`);
105
105
  }
106
106
  const clean = normalizeRules(stripRev(rules));
107
+ assertSessionRuleNamesCurrent(clean);
108
+ assertSessionRuleNamesReachable(clean);
107
109
  if (prior && opts?.operator !== true) {
108
110
  const loosens = loosenReasons(prior, clean);
109
111
  if (loosens.length > 0) {
@@ -1,6 +1,6 @@
1
1
  import type { Pool as MysqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
- import type { SessionStore, SessionPolicyStore, FileHistoryStore, WorkflowJournalStore, ToolResultStore, SessionCaptureRecordStore } from "@sema-agent/core";
3
+ import type { SessionStore, SessionPolicyStore, FileHistoryStore, WorkflowJournalStore, ToolResultStore, SessionCaptureRecordStore, UsageWindowStore } from "@sema-agent/core";
4
4
  import { type OutcomeSink } from "./file-outcome-sink.js";
5
5
  import { type InboxWarn } from "./workflow-run-store-sql.js";
6
6
  import { FileRunStore } from "./file-run-store.js";
@@ -355,6 +355,11 @@ export interface StoreBackend {
355
355
  * clock to expose replica clock skew (the write-behind counters bucket on LOCAL floor(now/windowMs); skew
356
356
  * splits a fleet window into disjoint buckets = soft-limit leak). OPTIONAL: local backend omits (no fleet). */
357
357
  dbNowMs?(): Promise<number>;
358
+ /** S-1014(core 7.41.0 #1308):**local 后端专有**的 usage-window 账本店 = core `FileStorageBackend` 自己那一只(`usageWindowStore`)——
359
+ * 它的坏读披露座是 backend 构造时那**一只** `onCorruptRead`(core:「`FileStorageBackend` forwards its one sink」),所以坏账本拒(`usage_window.store_corrupt`,
360
+ * 拒绝句不带盘上位置)的同时,路径进本 backend 的 `file_store_corrupt_read` 披露座(逐命中 warn)。修前 `boot/stores.ts` 在同一数据根上自己
361
+ * `new FileUsageWindowStore(root)` 第二只实例、不接座 ⇒ 坏值只拒不披露,运维找不到是哪只文件。SQL 后端省略(它们有自己的 usage-window 双方言店)。 */
362
+ fileUsageWindowStore?(): UsageWindowStore;
358
363
  }
359
364
  /** (c)(clay 拍 a+c,2026-07-27;[ref] 换店后域=file_history_blob,门与旋钮名不变)云形 blob 姿势门——
360
365
  * boot 时调用(boot/stores.ts),纯函数可单测。SQL 后端(mysql|pg)把 blob 字节落单行的形已被两役
@@ -265,7 +265,7 @@ class LocalBackend {
265
265
  }
266
266
  async ensureSchema() { }
267
267
  async runOneTimeMigrations() {
268
- return { ...NO_SQL_MIGRATIONS, askOriginRowsMigrated: await this.runStore.migrateRetiredAskOriginRows() };
268
+ return { ...NO_SQL_MIGRATIONS, askOriginRowsMigrated: await this.runStore.migrateRetiredAskOriginRows(), holderDeathCodeRowsBackfilled: this.runStore.migrateLegacyHolderDeathCodes() };
269
269
  }
270
270
  async close() {
271
271
  await this.sessionStore.dispose();
@@ -315,6 +315,7 @@ class LocalBackend {
315
315
  }
316
316
  checkpointStoreInst;
317
317
  sessionPolicy() { return this.fileBackend.sessionPolicyStore; }
318
+ fileUsageWindowStore() { return this.fileBackend.usageWindowStore; }
318
319
  fileHistory() { return this.fileBackend.fileHistoryStore; }
319
320
  memoryOptOutGrant() { return undefined; }
320
321
  sessionCaptureRecords() { return this.captureRecordsSource; }
@@ -140,7 +140,7 @@ export type PersistedTaskResult = TaskResult & PersistedResultSideKeys;
140
140
  /**
141
141
  * 🔴 run 店「持有它的实例丢了」那一判的**行上落款**(S-928 codex r1-F1 提为常量):三只 run 店的 `reapStale` 把心跳过期的 running 行翻成
142
142
  * `failed` 并写下这一句。它是 run 店死亡判决留在行上的证据之一(另一只 = {@link RUN_ORPHANED_ERROR})—— 用户输入受理账本的收敛判词
143
- * (S-969 L8 起两车道同一只)据 {@link isHolderDeathVerdict} 在 reap **之后**仍能判出「写者死了」。改这一句 = 改那条判据;所以三个写者与那一个读者都只引用本常量。读时折叠成同一句话的 wire 展示站点
143
+ * (S-969 L8 起两车道同一只)据 {@link isHolderDeathVerdict} 在 reap **之后**仍能判出「写者死了」—— 7.116.0 起判据是同行的机读码 `run.stalled`({@link RUN_HOLDER_DEATH_CODES}),本句只给人读;三个写者同写句子与码。读时折叠成同一句话的 wire 展示站点
144
144
  * (`routes/runs.ts` 的 stale 折叠、`routes/trace-usage.ts` 的 WORKER_DOWN 帧)刻意留字面量:它们由文案冻结门逐字钉着,且不参与任何判据。
145
145
  */
146
146
  export declare const RUN_STALLED_ERROR = "run stalled (instance lost?)";
@@ -148,19 +148,38 @@ export declare const RUN_STALLED_ERROR = "run stalled (instance lost?)";
148
148
  * 🔴 File 车道「持有它的进程随重启没了」那一判的行上落款(S-969 L8 提为常量):boot 孤儿回收(`FileRunStore.reclaimOrphanedAtBoot`,
149
149
  * 数据根 LOCK 作证前世进程已死 ⇒ 仍 running 的行无条件判孤儿)与运行期 claim 接管的 running 臂(`judgeClaim`,外来 + owner 已死 + 无可续)
150
150
  * 都写这一句;接管臂在括号里附上前任持有者的进程指纹(取证用,形 = {@link buildRunOrphanedError})。与 {@link RUN_STALLED_ERROR} 同一种身份:
151
- * run 店写在行上的**死亡判决**,读者 = {@link isHolderDeathVerdict}。
151
+ * run 店写在行上的**死亡判决**,读者 = {@link isHolderDeathVerdict}(按同行的机读码 `run.orphaned` 判,本句只给人读)。
152
152
  */
153
153
  export declare const RUN_ORPHANED_ERROR = "run orphaned by engine restart";
154
154
  /** File 车道两个孤儿落款写口共用的那一句(`heldBy` = 接管臂的前任持有者描述;boot 回收臂不带)。 */
155
155
  export declare function buildRunOrphanedError(heldBy?: string): string;
156
156
  /**
157
- * run 行上是不是一份**持有者死亡判决**:`failed`,且 `error` 是 run 店的两只死亡落款之一 —— {@link RUN_STALLED_ERROR}(三只 run 店的
158
- * `reapStale`:心跳过期 ⇒ 实例丢了)或 {@link RUN_ORPHANED_ERROR}(File 车道:前世进程随重启没了;接管臂带括注)。判决只说「持有它的那个进程
159
- * 死在这条腿收场之前」,不说是谁 —— 持有者是谁读行上的 `instanceId`(两只落款都不改它)。唯一读者 = 用户输入受理账本的收敛判词
160
- * (`src/user-input-ledger.ts` `judgeOpen`);改任一句落款 = 改那条判据,所以写者与读者都只引用本模块的常量。
161
- * 正常失败(别的 error)/ park 过期收割(`approval expired before decision`:腿 park 过)都**不是**死亡判决。
157
+ * run 店的两只**死亡判决机读码**(行上 `errorCode` 列;`GET /v1/runs/:id` 的 `errorCode` 读它)—— 与两句死亡落款一一对应:
158
+ * {@link RUN_STALLED_ERROR} ⇔ `run.stalled`(三只 run 店的 `reapStale`:心跳过期 ⇒ 实例丢了)、{@link RUN_ORPHANED_ERROR}(含接管臂的括注形)⇔
159
+ * `run.orphaned`(File 车道:前世进程随重启没了)。码是判据,句子只给人读。族 `run.` = run 店自己写在行上的生命周期判决(core 不铸这一族)。
162
160
  */
163
- export declare function isHolderDeathVerdict(run: Pick<RunRecord, "status" | "error">): boolean;
161
+ export declare const RUN_HOLDER_DEATH_CODES: {
162
+ readonly stalled: "run.stalled";
163
+ readonly orphaned: "run.orphaned";
164
+ };
165
+ export type RunHolderDeathCode = (typeof RUN_HOLDER_DEATH_CODES)[keyof typeof RUN_HOLDER_DEATH_CODES];
166
+ /**
167
+ * 一次性迁移 **M6** 的判据(窗、删法与删后谁接住:窗口见 `plugins/one-time-migrations.ts` 登记表):7.116.0 之前写下的死亡判决行只有句子、码列为空 ——
168
+ * 该行应有的码。两处用:local 腿的起服写回(`FileRunStore.migrateLegacyHolderDeathCodes`)与 SQL 两孪生行映射里的**读侧半场**(滚动升级窗里旧副本
169
+ * 还会写这种行,起服写回覆盖不到;本函数随 M6 一起删)。只认两句落款的原字节(接管臂括注形按前缀认);码列已有值 / 非 `failed` / 别的句子 ⇒ 原样。
170
+ */
171
+ export declare function readLegacyHolderDeathCode(row: {
172
+ readonly status: string;
173
+ readonly error: string | null;
174
+ readonly errorCode: string | null;
175
+ }): string | null;
176
+ /**
177
+ * run 行上是不是一份**持有者死亡判决**:`failed`,且行上的码是 {@link RUN_HOLDER_DEATH_CODES} 之一。判决只说「持有它的那个进程死在这条腿收场之前」,
178
+ * 不说是谁 —— 持有者是谁读行上的 `instanceId`(判决不改它)。读者 = 用户输入受理账本的收敛判词(`src/user-input-ledger.ts` `judgeOpen`)。
179
+ * 旧行(只有句子)由一次性迁移 M6 补上码(起服写回 + SQL 滚动窗的读侧半场,{@link readLegacyHolderDeathCode}),所以这里不再按句子判。
180
+ * 正常失败(别的码 / 无码)/ park 过期收割(`approval.expired`:腿 park 过)都**不是**死亡判决。
181
+ */
182
+ export declare function isHolderDeathVerdict(run: Pick<RunRecord, "status" | "errorCode">): boolean;
164
183
  export interface RunRecord {
165
184
  taskId: string;
166
185
  sessionId: string;
@@ -6,10 +6,19 @@ export const RUN_ORPHANED_ERROR = "run orphaned by engine restart";
6
6
  export function buildRunOrphanedError(heldBy) {
7
7
  return heldBy === undefined ? RUN_ORPHANED_ERROR : `${RUN_ORPHANED_ERROR} (claim held by ${heldBy}, which is gone)`;
8
8
  }
9
+ export const RUN_HOLDER_DEATH_CODES = { stalled: "run.stalled", orphaned: "run.orphaned" };
10
+ const RUN_HOLDER_DEATH_CODE_WORDS = new Set(Object.values(RUN_HOLDER_DEATH_CODES));
11
+ export function readLegacyHolderDeathCode(row) {
12
+ if (row.errorCode !== null || row.status !== "failed" || row.error === null)
13
+ return row.errorCode;
14
+ if (row.error === RUN_STALLED_ERROR)
15
+ return RUN_HOLDER_DEATH_CODES.stalled;
16
+ if (row.error === RUN_ORPHANED_ERROR || row.error.startsWith(`${RUN_ORPHANED_ERROR} (`))
17
+ return RUN_HOLDER_DEATH_CODES.orphaned;
18
+ return null;
19
+ }
9
20
  export function isHolderDeathVerdict(run) {
10
- if (run.status !== "failed" || run.error === null)
11
- return false;
12
- return run.error === RUN_STALLED_ERROR || run.error === RUN_ORPHANED_ERROR || run.error.startsWith(`${RUN_ORPHANED_ERROR} (`);
21
+ return run.status === "failed" && run.errorCode !== null && RUN_HOLDER_DEATH_CODE_WORDS.has(run.errorCode);
13
22
  }
14
23
  export const RUN_STATUS_CLASS = {
15
24
  running: "live",
@@ -141,7 +141,7 @@ export class TiDBSessionStore {
141
141
  async listSessions(opts) {
142
142
  const params = [];
143
143
  const where = ["sm.placement_kind IS NULL"];
144
- if (opts.owner) {
144
+ if (opts.owner !== undefined) {
145
145
  where.push(opts.includeUnowned ? "(sm.owner = ? OR sm.owner IS NULL)" : "sm.owner = ?");
146
146
  params.push(opts.owner);
147
147
  }
@@ -28,7 +28,7 @@ const TASK_BODY_DECLARED_KEYS = [
28
28
  "resilience", "restoreFiles", "resumeAt", "resumeAtMode", "retainBackgroundProcesses", "retainSubagentSessions",
29
29
  "rewindFiles", "rewindFilesTo", "sandboxImageProfile", "scenario", "scratchpadDir", "selfOrchestration",
30
30
  "sessionId", "settings", "skills", "suggestNextPrompts", "systemPrompt", "taskId", "toolMaterializeStrategy",
31
- "verify", "verifyRounds",
31
+ "userInvokedSkills", "verify", "verifyRounds",
32
32
  ];
33
33
  const TASK_BODY_SCENARIO_KEYS = ["businessContext", "lenses", "repo", "rounds", "team"];
34
34
  const ACCEPTED_BODY = new Set([...TASK_BODY_DECLARED_KEYS, ...TASK_BODY_SCENARIO_KEYS]);
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { type AskRequest, type Brain, type TaskSpec } from "@sema-agent/core";
2
+ import { type AskClass, type AskRequest, type Brain, type TaskSpec } from "@sema-agent/core";
3
3
  import { type UnattendedPolicyRefusal } from "./tool-approval.js";
4
4
  import { type ApprovalBaselineConfigView, type DeploymentGovernanceConfigView } from "./deployment-governance.js";
5
5
  import { type ServiceConfig } from "./config.js";
@@ -131,8 +131,10 @@ export interface LocalApproverIo {
131
131
  *
132
132
  * 🔴 S-694 起本席也服**三张审批名单**产的 ask(审批基线取 live 席,见 {@link RUN_LOCAL_APPROVAL_SEAT}):
133
133
  * `APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO` 命中 ⇒ 策略 `ask` ⇒ 落到这里。`AskRequest` 不带是哪条声明产的
134
- * ask(core 把 `matchedAskRule` 刻意留在闸内、不上请求),所以无 TTY 拒句**列出**可能产门的全部旋钮,
135
- * `APPROVAL_DENY` 也点名(它当场拒、不经本席 —— 运维排查「为什么这只工具不跑」时要在同一句里看到它)。
134
+ * ask(core 把 `matchedAskRule` 刻意留在闸内、不上请求),但带这一问的**类**(`classes`,core 7.37.0 起)。
135
+ * 🔴 无 TTY 拒句的出路**按这一问的类说话**({@link NO_TTY_ASK_CLASS_ROW} 一张闭集表:每类 ⇒ 一种出路):没人表态的服务器工具调用指 `APPROVAL_GRANT`,
136
+ * 写下的 ask 名单指名单旋钮(`APPROVAL_DENY` 也点名 —— 它当场拒、不经本席),部署强制要人答的问(`operator_forced`)列「one of」能铸它的来源、shell 地板(`shell_floor`)另一句只列铸它的旋钮,其余各类**不点任何旋钮**、只说去终端答;
137
+ * 类缺席 ⇒ 说实话(「引擎没有说明这一问的类」),不回落成全量旋钮清单(那句对多数类是错的出路)。
136
138
  *
137
139
  * 🔴 非 TTY 臂答**拒**(真 deny),**绝不答 `"unavailable"`**:后者被 core 读成「席位够不到」并**改道
138
140
  * `suspendAsk`**(dist `core/hooks.js` 的 `approverUnavailable` 重投)。今天本腿无店 ⇒ 那条改道无处
@@ -149,6 +151,37 @@ export interface LocalApproverIo {
149
151
  /** 本席的形:core `OnAsk` 的函数臂(`"allow"` / `"deny"` 两个字面臂本席从不用)—— 人的答是布尔,没有人被问的拒是部署政策拒对象;
150
152
  * 结构上可赋给 `OnAsk`。 */
151
153
  export type LocalApprover = (req: AskRequest, signal?: AbortSignal) => Promise<boolean | UnattendedPolicyRefusal>;
154
+ /**
155
+ * 无 TTY 拒句里**出路**的种类(闭集)。每一类问对应一种({@link NO_TTY_ASK_CLASS_ROW});`terminal_only` = 没有本部署的旋钮能放行这一问,只剩去终端答。
156
+ * 说出口的三种按 {@link NO_TTY_SPOKEN_REMEDIES} 的顺序各说一次(一问可以折叠多类 ⇒ 多种出路,顺序恒定)。
157
+ */
158
+ declare const NO_TTY_SPOKEN_REMEDIES: readonly ["grant", "approval_lists", "deployment_mandate", "shell_gate"];
159
+ type NoTtySpokenRemedy = (typeof NO_TTY_SPOKEN_REMEDIES)[number];
160
+ type NoTtyRemedy = NoTtySpokenRemedy | "terminal_only";
161
+ /**
162
+ * 一类问 ⇒ 这条腿上它的出路。**穷尽表**:core 的 `ASK_CLASSES` 加一类 ⇒ 这里 tsc 红(`Record<AskClass, …>` 少一键),新类必须在这里表态,
163
+ * 不会悄悄落成「没有出路」。本仓知道而 core 不知道的只有一件:**哪只部署旋钮会产哪一类问**(逐类的产地见各行):
164
+ * · `unstated`:没有任何层表态的服务器工具调用(core 7.40.0)—— 运维为自己接的服务器表态的地方是 `APPROVAL_GRANT`(`run-local` 没有请求体,写不了 `settings.permissions.allow`);
165
+ * · `ask_rule`:写下的 ask 规则 —— `APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO`(`operatorAsk`)与治理层的 commandPolicy ask 名单(同一只铸点);请求体的
166
+ * `settings.permissions.ask` 到不了这条腿(本文件不 import 任何请求受理 / `task-settings`,`run-local` 的 spec 自铸);
167
+ * · `operator_forced`:部署**强制**要人答的问 —— 能铸它的来源(真 Runner × `applyLocalGovernance` 亲读类集,读数在 `test/run-local.test.ts` S-1100 ⑨ 各格):`APPROVAL_NEVER_AUTO`
168
+ * (`operatorAlwaysHumanAsk` 盖部署出身;类集 `[ask_rule, operator_forced]`)与 `runtime.commandPolicy`(ask 条目 ⇒ `[ask_rule, operator_forced]`,同一只铸点;allowlist 形里没列的命令 ⇒ `[operator_forced]`)。
169
+ * 读码补全(未亲跑):耐久资源耗尽的「恒问人」同走 `operatorAlwaysHumanAsk`、SUP 路由的 capability-danger 门同走它 —— 两者都要 checkpoint 店 / 路由器,run-local 没有;本仓没有给 `spec.tools` 盖部署工具标记的旋钮
170
+ * (grep `egress: true` / `irreversibility` 字面量零命中),core 的 A2A 隔离对端形本仓也不设。所以「one of」两项就是这条腿上能铸它的全部来源;
171
+ * · `shell_floor`:shell 地板被抬到 always —— `AUTONOMY=ask`(`autonomyOverrides`)与 `MANUAL_MODE_SHELL_GATE=always`,真 Runner 读数两者的类集都是 `[shell_floor]`(**不是** `operator_forced`):
172
+ * 单独一句,不与 `operator_forced` 的来源混在一起 —— 混说会把 `APPROVAL_NEVER_AUTO` 的问指到一定没设的 `AUTONOMY`;
173
+ * · 其余:引擎自己的问(写门 / 形状层 / 写保护 / 探针 / 钩子 / 组织规则 / 引擎的守卫 / 工具标记 …)没有一只本部署旋钮能放行 —— 只说去终端答。
174
+ * 不借 core 的 `headlessApprovalRemedy`:那一句是给**库的调用方**的出路(「wire an approver (`onAsk`)」「run with `permissionMode: "bypassPermissions"`」),
175
+ * 而本腿的席**已经接好**(缺的是终端),也没有任何命令行旗给 `permissionMode` —— 照搬会把运维指向一个不存在的旋钮。
176
+ *
177
+ * `autoBudgetReleases`:`APPROVAL_AUTO_BUDGET` 能放行这类问吗(读 `createDurableAskPolicy` 的预算臂:只放行**没进 `APPROVAL_NEVER_AUTO`、耐久资源没耗尽**的
178
+ * `APPROVAL_REQUIRE` 命中,换成一句带理由的 allow)。按 `classes` **整体**读:集里每一类都放得行才建议预算 —— 折叠了别的类(`operator_forced` 等)说明另一层的问仍在,
179
+ * 预算放不掉它(与 {@link unattendedAnswers} 读整体同一律)。
180
+ */
181
+ export declare const NO_TTY_ASK_CLASS_ROW: Readonly<Record<AskClass, {
182
+ readonly remedy: NoTtyRemedy;
183
+ readonly autoBudgetReleases: boolean;
184
+ }>>;
152
185
  export declare function createLocalApprover(io: LocalApproverIo): LocalApprover;
153
186
  export declare function promptYesNoOnTty(prompt: string, signal?: AbortSignal, streams?: {
154
187
  input: NodeJS.ReadableStream;
@@ -176,4 +209,5 @@ export declare function installRunLocalExitBoundary(deps: {
176
209
  logger: Logger;
177
210
  exit?: (code: number) => void;
178
211
  }): RunLocalExitBoundary;
212
+ export {};
179
213
  //# sourceMappingURL=run-local.d.ts.map