@sema-agent/server 7.123.0 → 7.124.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 (224) hide show
  1. package/MIGRATION.md +132 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/USAGE.md +53 -51
  5. package/dist/approval-ask-audit-store.d.ts +27 -32
  6. package/dist/approval-ask-audit-store.js +10 -9
  7. package/dist/approval-crash-convergence.d.ts +2 -2
  8. package/dist/approval-crash-convergence.js +5 -5
  9. package/dist/approval-deny-reasons.d.ts +163 -51
  10. package/dist/approval-deny-reasons.js +79 -8
  11. package/dist/approval-policy-settlement.d.ts +4 -1
  12. package/dist/approval-policy-settlement.js +3 -2
  13. package/dist/approval-reconciler.d.ts +3 -3
  14. package/dist/approval-reconciler.js +10 -10
  15. package/dist/approval.d.ts +1 -1
  16. package/dist/approval.js +1 -1
  17. package/dist/audit.js +1 -1
  18. package/dist/auth-bridge.d.ts +8 -8
  19. package/dist/auth-bridge.js +6 -6
  20. package/dist/auth-keys.d.ts +2 -2
  21. package/dist/auto-mode-face.d.ts +1 -1
  22. package/dist/boot/budget-tracing.js +1 -1
  23. package/dist/boot/execution-env.js +1 -0
  24. package/dist/boot/leader.d.ts +1 -1
  25. package/dist/boot/memory-consolidation.d.ts +4 -4
  26. package/dist/boot/org-memory.d.ts +3 -3
  27. package/dist/boot/org-memory.js +5 -5
  28. package/dist/boot/resolve-spec.js +7 -7
  29. package/dist/boot/runner-deps.d.ts +1 -1
  30. package/dist/boot/runtime-caps.d.ts +1 -1
  31. package/dist/boot/runtime-caps.js +10 -10
  32. package/dist/boot/shutdown.d.ts +2 -2
  33. package/dist/boot/shutdown.js +2 -2
  34. package/dist/boot/side-query-lane.d.ts +3 -3
  35. package/dist/boot/{stage-02-registry-pull.d.ts → stage-02-web-admin-pull.d.ts} +2 -2
  36. package/dist/boot/{stage-02-registry-pull.js → stage-02-web-admin-pull.js} +4 -4
  37. package/dist/boot/stage-03-store-backend.d.ts +2 -2
  38. package/dist/boot/stage-04-stores.d.ts +1 -1
  39. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  40. package/dist/boot/stage-06-runners.d.ts +2 -2
  41. package/dist/boot/stage-06-runners.js +6 -6
  42. package/dist/boot/stage-07-capability-layer.d.ts +2 -2
  43. package/dist/boot/stage-07-capability-layer.js +6 -5
  44. package/dist/boot/stage-08-reapers.d.ts +3 -3
  45. package/dist/boot/stage-08-reapers.js +3 -3
  46. package/dist/boot/stage-09-leader.d.ts +3 -3
  47. package/dist/boot/stage-09-leader.js +7 -7
  48. package/dist/boot/stage-10-http-server.d.ts +3 -3
  49. package/dist/boot/stage-10-http-server.js +19 -19
  50. package/dist/boot/unpriced-ceiling-verdict.d.ts +1 -1
  51. package/dist/boot/{config-center.d.ts → web-admin.d.ts} +11 -11
  52. package/dist/boot/{config-center.js → web-admin.js} +38 -22
  53. package/dist/boot/webfetch-summarize-lane.d.ts +2 -2
  54. package/dist/brain.d.ts +1 -1
  55. package/dist/brain.js +1 -1
  56. package/dist/budget.d.ts +3 -3
  57. package/dist/canonical-json.d.ts +2 -2
  58. package/dist/capabilities/center-plugins.d.ts +1 -1
  59. package/dist/capabilities/center-wiring.d.ts +12 -5
  60. package/dist/capabilities/center-wiring.js +11 -2
  61. package/dist/capabilities/composite-members.d.ts +1 -1
  62. package/dist/capabilities/composite-members.js +1 -1
  63. package/dist/capabilities/scenarios.js +2 -2
  64. package/dist/capabilities/team.d.ts +1 -1
  65. package/dist/capabilities/tool-defer.d.ts +1 -1
  66. package/dist/center-credential-renewer.d.ts +1 -1
  67. package/dist/center-credential-renewer.js +7 -7
  68. package/dist/center-credential.d.ts +9 -9
  69. package/dist/center-credential.js +6 -6
  70. package/dist/config-catalog.d.ts +7 -10
  71. package/dist/config-catalog.js +15 -15
  72. package/dist/config-d-migration.d.ts +32 -0
  73. package/dist/config-d-migration.js +59 -0
  74. package/dist/config-invariants.d.ts +14 -4
  75. package/dist/config-invariants.js +31 -2
  76. package/dist/config-lkg.d.ts +26 -6
  77. package/dist/config-lkg.js +43 -14
  78. package/dist/config-provider.d.ts +23 -19
  79. package/dist/config-provider.js +24 -26
  80. package/dist/config-types.d.ts +52 -51
  81. package/dist/config.d.ts +9 -46
  82. package/dist/config.js +33 -92
  83. package/dist/execution-lane-caps.d.ts +1 -1
  84. package/dist/fleet/drain-entry.d.ts +1 -1
  85. package/dist/fleet/engine-signal.d.ts +8 -8
  86. package/dist/fleet/fleet-bus.d.ts +12 -3
  87. package/dist/fleet/fleet-bus.js +13 -9
  88. package/dist/fleet/serving-lease.d.ts +3 -3
  89. package/dist/fleet/serving-lease.js +1 -1
  90. package/dist/fleet-client.d.ts +7 -7
  91. package/dist/fleet-client.js +5 -4
  92. package/dist/fleet-lease.d.ts +3 -3
  93. package/dist/fleet-lease.js +2 -2
  94. package/dist/host-lsp-manager.d.ts +1 -1
  95. package/dist/hosted-posture.d.ts +4 -4
  96. package/dist/http/admission.js +4 -4
  97. package/dist/http/checkpoint-ctx.d.ts +20 -3
  98. package/dist/http/checkpoint-ctx.js +13 -5
  99. package/dist/http/dispatch.js +2 -2
  100. package/dist/http/durable-run-leg.js +2 -1
  101. package/dist/http/health-self-report.d.ts +4 -4
  102. package/dist/http/health-self-report.js +3 -3
  103. package/dist/http/idempotency.d.ts +1 -1
  104. package/dist/http/resume-legs.d.ts +4 -3
  105. package/dist/http/resume-legs.js +31 -18
  106. package/dist/http/route-ctx.d.ts +8 -0
  107. package/dist/http/routes/admin-config-refresh.d.ts +1 -1
  108. package/dist/http/routes/admin-config-refresh.js +1 -1
  109. package/dist/http/routes/admin-drain.d.ts +1 -1
  110. package/dist/http/routes/admin-drain.js +5 -4
  111. package/dist/http/routes/approvals-assistant.js +4 -4
  112. package/dist/http/routes/capabilities.js +5 -5
  113. package/dist/http/routes/config-catalog.js +2 -3
  114. package/dist/http/routes/diagnostics.js +1 -1
  115. package/dist/http/routes/runs.js +8 -4
  116. package/dist/http/routes/sessions-list.js +2 -1
  117. package/dist/http/routes/sessions.js +1 -1
  118. package/dist/http/routes/side-query.js +3 -3
  119. package/dist/http/routes/tasks.js +3 -2
  120. package/dist/http/run-meta.d.ts +1 -1
  121. package/dist/http/run-meta.js +2 -2
  122. package/dist/http/server.d.ts +7 -7
  123. package/dist/http/server.js +1 -1
  124. package/dist/http/stored-body-replay.js +3 -2
  125. package/dist/index.d.ts +0 -1
  126. package/dist/index.js +0 -1
  127. package/dist/key-resolver.d.ts +1 -1
  128. package/dist/leader/wire.d.ts +2 -2
  129. package/dist/leader/wire.js +2 -1
  130. package/dist/main.js +1 -1
  131. package/dist/mcp-tool-faces.d.ts +2 -2
  132. package/dist/model-entry-refusal.d.ts +3 -3
  133. package/dist/model-entry-refusal.js +4 -4
  134. package/dist/model-provider.d.ts +2 -2
  135. package/dist/model-route-endpoint.d.ts +1 -1
  136. package/dist/model-route-url.d.ts +1 -1
  137. package/dist/model-route-url.js +2 -1
  138. package/dist/model-select.d.ts +24 -21
  139. package/dist/model-select.js +22 -17
  140. package/dist/observability/metrics.d.ts +1 -1
  141. package/dist/org-memory-admission.d.ts +1 -1
  142. package/dist/plugins/approval-ask-store-file.d.ts +4 -2
  143. package/dist/plugins/approval-ask-store-file.js +3 -3
  144. package/dist/plugins/approval-ask-store-memory.d.ts +4 -3
  145. package/dist/plugins/approval-ask-store-memory.js +10 -4
  146. package/dist/plugins/approval-ask-store-sql.d.ts +16 -2
  147. package/dist/plugins/approval-ask-store-sql.js +20 -13
  148. package/dist/plugins/caching-session-store.d.ts +0 -10
  149. package/dist/plugins/caching-session-store.js +0 -3
  150. package/dist/plugins/file-run-store.js +9 -3
  151. package/dist/plugins/leader-run-store-sql.d.ts +3 -2
  152. package/dist/plugins/leader-run-store-sql.js +3 -2
  153. package/dist/plugins/local-session-store.d.ts +20 -27
  154. package/dist/plugins/local-session-store.js +19 -17
  155. package/dist/plugins/memory-run-store.js +9 -3
  156. package/dist/plugins/one-time-migrations.js +26 -2
  157. package/dist/plugins/scheduler-support.d.ts +26 -7
  158. package/dist/plugins/scheduler-support.js +96 -53
  159. package/dist/plugins/store-backend.js +1 -1
  160. package/dist/plugins/store-contracts.d.ts +20 -4
  161. package/dist/plugins/web-search.d.ts +1 -1
  162. package/dist/plugins/web-search.js +2 -1
  163. package/dist/posture-source.d.ts +1 -1
  164. package/dist/read-face-posture.d.ts +3 -3
  165. package/dist/read-face-posture.js +1 -1
  166. package/dist/run-local.js +6 -3
  167. package/dist/runtime-caps-resolver.d.ts +4 -4
  168. package/dist/runtime-caps-resolver.js +2 -2
  169. package/dist/runtime-governance.d.ts +5 -5
  170. package/dist/sealed-key.d.ts +4 -4
  171. package/dist/sealed-key.js +3 -2
  172. package/dist/security.d.ts +3 -3
  173. package/dist/service-version.d.ts +1 -1
  174. package/dist/service-version.js +5 -0
  175. package/dist/session-titler.d.ts +0 -11
  176. package/dist/session-titler.js +3 -5
  177. package/dist/shared-memory-scope-authorizer.d.ts +4 -4
  178. package/dist/task-a2a.d.ts +1 -1
  179. package/dist/tool-approval.d.ts +43 -4
  180. package/dist/tool-approval.js +147 -98
  181. package/dist/trace/core-keyset-guard.d.ts +1 -1
  182. package/dist/trace/project.d.ts +3 -3
  183. package/dist/trace/redact.d.ts +2 -23
  184. package/dist/trace/redact.js +1 -6
  185. package/dist/trace/self-minted-wire.d.ts +147 -3
  186. package/dist/trace/self-minted-wire.js +50 -9
  187. package/dist/url-refusal.d.ts +35 -0
  188. package/dist/url-refusal.js +15 -0
  189. package/dist/user-authored-text.d.ts +31 -0
  190. package/dist/user-authored-text.js +18 -0
  191. package/dist/{config-center → web-admin-client}/apply-effective.d.ts +13 -70
  192. package/dist/{config-center → web-admin-client}/apply-effective.js +100 -119
  193. package/dist/{config-center → web-admin-client}/apply-ledger.d.ts +2 -2
  194. package/dist/{config-center → web-admin-client}/center-request.d.ts +7 -7
  195. package/dist/{config-center → web-admin-client}/center-request.js +5 -4
  196. package/dist/{config-center → web-admin-client}/effective-keys.d.ts +1 -3
  197. package/dist/{config-center → web-admin-client}/effective-keys.js +0 -9
  198. package/dist/{config-center → web-admin-client}/facade.d.ts +6 -6
  199. package/dist/{config-center → web-admin-client}/facade.js +2 -2
  200. package/dist/{config-center → web-admin-client}/http-client.d.ts +6 -10
  201. package/dist/{config-center → web-admin-client}/http-client.js +12 -12
  202. package/dist/{config-center → web-admin-client}/pull-state.d.ts +1 -1
  203. package/dist/{config-center → web-admin-client}/read-face.js +4 -4
  204. package/dist/{config-center → web-admin-client}/read-warnings.d.ts +23 -16
  205. package/dist/{config-center → web-admin-client}/read-warnings.js +33 -23
  206. package/dist/{config-center → web-admin-client}/restart-signal.d.ts +5 -3
  207. package/dist/{config-center → web-admin-client}/restart-signal.js +3 -4
  208. package/dist/{config-center → web-admin-client}/skills-mcp.d.ts +1 -1
  209. package/dist/{config-center → web-admin-client}/skills-mcp.js +13 -13
  210. package/dist/{config-center → web-admin-client}/stage-limits.d.ts +1 -7
  211. package/dist/{config-center → web-admin-client}/stage-limits.js +6 -7
  212. package/dist/{config-center → web-admin-client}/types.d.ts +12 -39
  213. package/package.json +2 -2
  214. package/skills/find-skills.md +2 -2
  215. package/dist/url-shape.d.ts +0 -31
  216. package/dist/url-shape.js +0 -41
  217. /package/dist/{config-center → web-admin-client}/apply-ledger.js +0 -0
  218. /package/dist/{config-center → web-admin-client}/hot-keys-registry.d.ts +0 -0
  219. /package/dist/{config-center → web-admin-client}/hot-keys-registry.js +0 -0
  220. /package/dist/{config-center → web-admin-client}/mcp-revocation.d.ts +0 -0
  221. /package/dist/{config-center → web-admin-client}/mcp-revocation.js +0 -0
  222. /package/dist/{config-center → web-admin-client}/pull-state.js +0 -0
  223. /package/dist/{config-center → web-admin-client}/read-face.d.ts +0 -0
  224. /package/dist/{config-center → web-admin-client}/types.js +0 -0
@@ -1,5 +1,10 @@
1
1
  import { type Model, type RunnerDeps } from "@sema-agent/core";
2
+ import { type AtMention } from "@sema-agent/settings-schema";
2
3
  import type { ModelEntryRefusal } from "./model-entry-refusal.js";
4
+ /** 座位缺席时的读法 —— 契约 prefault 解出来的值(今天是 `{mode:"all"}`),env 车道(没有中心)的 `config.atMention` 也是它。 */
5
+ export declare const DEFAULT_AT_MENTION: AtMention;
6
+ /** 一个 `models.atMention` 值的读法:缺席 ⇒ {@link DEFAULT_AT_MENTION};在场 ⇒ 契约 schema 判形,判不过 ⇒ `undefined`(调用方 fail-closed 并响亮)。 */
7
+ export declare function readAtMention(raw: unknown): AtMention | undefined;
3
8
  /**
4
9
  * Resolve the model ref for a task spec from the request (CLI `/model` picker + `run --model`).
5
10
  *
@@ -37,7 +42,7 @@ export declare const DEFAULT_MODEL_REF = "default";
37
42
  * 起不再各抄一份「展开 + 解」。
38
43
  */
39
44
  export declare function resolveMainModel(plane: Pick<RunnerDeps, "models" | "tiers">): Model;
40
- export declare function resolveTaskModel(bodyModel: unknown, objective: string, catalog: Record<string, unknown>, allowlist?: readonly string[], refusals?: Readonly<Record<string, ModelEntryRefusal>>): {
45
+ export declare function resolveTaskModel(bodyModel: unknown, objective: string, catalog: Record<string, unknown>, atMention: AtMention, refusals?: Readonly<Record<string, ModelEntryRefusal>>): {
41
46
  model: string;
42
47
  cleanedObjective: string;
43
48
  unknownExplicit?: string;
@@ -84,7 +89,7 @@ export declare function refusedCatalogEntry(ref: string, refusals: Readonly<Reco
84
89
  name: string;
85
90
  refusal: ModelEntryRefusal;
86
91
  } | undefined;
87
- /** [ref] 判别式拒因:名单在场且这次**用户选择**落在名单外。`resolved` 已是目录 name 形(归一后)。 */
92
+ /** [ref] 判别式拒因:这次**用户选择**不在 `atMention` 许可之内(`none`,或 `only` 而不在名里)。`resolved` 已是目录 name 形(归一后)。 */
88
93
  export interface ModelNotAllowed {
89
94
  /** 调用方发来的原始 ref(name / 档位词 / 别名 / id / `@name`),已做过日志安全的 repr 归一。 */
90
95
  requested: string;
@@ -94,28 +99,26 @@ export interface ModelNotAllowed {
94
99
  source: "explicit" | "mention";
95
100
  }
96
101
  /**
97
- * [ref] / [ref] —— **归一条款**:membership 判定前把 ref 归一到**终端 catalog 条目**再比。
102
+ * [ref] / [ref] —— 「这个 ref 此刻可不可以被用户 `@` / 显式点选」的**唯一**判据(`models.atMention`,三态闭集,穷举 switch)。
98
103
  *
99
- * 为什么不能拿字符串直接跟名单比:`expandTiers` 的增广目录里,档位词(`pro`)与 CC 别名(`sonnet`)
100
- * 的值与 `config.models[<name>]` 是**同一个对象引用**——裸串比较会让 `@pro` 跳过一张按 name 写的名单
101
- * (同一只模型,两个键)。所以两边(ref 与每个名单条目)都先经 {@link matchCatalogModel} 解到目录条目,
102
- * 再按 **引用相等** 判同。id 形的 ref 不需要特别处理:`matchCatalogModel` 本身就是 name+id 双键索引,
103
- * 它会把 id 解成目录键,引用相等接着就对上了。
104
+ * 判序(一条规则):先把 ref 归一到**目录条目**({@link matchCatalogModel},name / 档位词 / 别名 / id 同一只);目录里根本没有的 ref ⇒ 恒 `true`
105
+ * —— 那是「未知模型」那道门的辖区(fresh 400 / resume 降级留声),本门只答「在目录里,但准不准点」,两道门同码同拒会让消费端分不清
106
+ * "没这模型"和"不让你点"。然后按 `mode`:
107
+ * · `all` ⇒ `true`;
108
+ * · `none` ⇒ `false`(一个都不许);
109
+ * · `only` ⇒ ref 归一到的那只条目**自己的目录名**在名单里 ⇒ `true`(名单按目录名直比)。
104
110
  *
105
- * 🔴 **刻意没有「id 相等」退路**(codex 复审 R1 [high],已采纳):目录**允许两条不同 name 的条目共享
106
- * 同一个上游 `Model.id`,却各自带不同的 `baseUrl` / `apiKeyEnv` / `cost` / provider** —— 那正是多网关、
107
- * 多账号部署形。按 id 判同会把 `safe` 与 `premium` 合并,于是一张只含 `safe` 的名单会放行 `premium`,
108
- * 而真正执行的是 `premium` 那份坐标与计费。判据只认**目录条目本身**:两条条目就是两只模型,哪怕上游
109
- * id 撞车。(引用相等因此不是"优化",是唯一正确的判据;它天然覆盖 tier 词/别名/id 三形。)
111
+ * 为什么 ref 侧要先归一、不拿 ref 串直比:`expandTiers` 的增广目录里,档位词(`pro`)与 CC 别名(`sonnet`)的值与 `config.models[<name>]`
112
+ * 是**同一个对象引用**——裸串比较会让 `@pro` 跳过一张按 name 写的名单(同一只模型,两个键)。名单侧不归一:`only` 的名在 stage 期已按本世代
113
+ * 可用目录收窄成目录名(settings-schema `narrowAtMention`),名单项是档位词 / 别名 / id 的形只可能来自绕过 stage 的写者 —— 它们准不了任何东西。
114
+ * 🔴 **刻意没有「id 相等」退路**(codex 复审 R1 [high]):目录**允许两条不同 name 的条目共享同一个上游 `Model.id`,却各自带不同的
115
+ * `baseUrl` / `apiKeyEnv` / `cost` / provider** —— 按 id 判同会把 `safe` 与 `premium` 合并,一张只含 `safe` 的名单会放行 `premium`。
116
+ * 🔴 S-704:名里写着一条**不可用**条目的名字 ⇒ 它准不了任何东西(不经 id 回退 / 别名改绑去准另一只模型 —— `matchCatalogModel` 的名字保留律)。
117
+ * `only` 的名在 stage 期已按本世代可用目录收窄过(settings-schema `narrowAtMention`,收窄到一个不剩 ⇒ `none`),所以这里的悬空名只来自绕过 stage 的写者(按名直比,悬空名不等于任何条目的名)。
110
118
  *
111
- * 缺席/空名单 ⇒ 恒 `true`(ship-dark:缺省 = 旧行为,全放行)。
112
- * 目录里根本没有的 ref ⇒ 恒 `true`:那是「未知模型」那道门的辖区(fresh 400 / resume 降级留声),
113
- * 本门只答「在目录里,但准不准点」——两道门同码同拒会让消费端分不清"没这模型"和"不让你点"。
114
- *
115
- * ⚠️ 射程:本门是**提交时**门。core 运行时路由(degrade 降级链 / cascade 梯 / roles 解析)按 operator
116
- * 配置自己走,**不经本门**——那不是一次用户选择,如实声明,不假装是运行时执法。
119
+ * ⚠️ 射程:本门是**提交时**门。core 运行时路由(degrade 降级链 / cascade 梯 / roles 解析)按 operator 配置自己走,**不经本门**。
117
120
  */
118
- export declare function isModelAllowlisted(ref: string, catalog: Record<string, unknown>, allowlist: readonly string[] | undefined, refusals?: Readonly<Record<string, ModelEntryRefusal>>): boolean;
121
+ export declare function isAtMentionable(ref: string, catalog: Record<string, unknown>, atMention: AtMention, refusals?: Readonly<Record<string, ModelEntryRefusal>>): boolean;
119
122
  /**
120
123
  * [ref] —— ref → **终端 catalog name**(拒因/日志的可读形)。
121
124
  *
@@ -144,7 +147,7 @@ export type ModelImageInput = (typeof MODEL_IMAGE_INPUTS)[number];
144
147
  * · 目录行 `GET /v1/models` 的 `vision`:`yes` ⇒ `true`,`no` ⇒ `false`,`undeclared` ⇒ **键缺席**(不替没声明的模型答「收」也不答「不收」);
145
148
  * · 运行期的带图预检 {@link modelSupportsImages}:只有明说 `no` 才出信号。
146
149
  * 此前两处各判各的,且对「没声明」答法相反(目录行报 false、预检放行)——客户端只读得到目录那一位,于是在手写、没声明 `input` 的
147
- * 条目上把用户的图拦在了本地,而引擎其实会把图交给模型。env 车道与配置中心车道恒写 `input`,分叉只落在手写目录条目上。
150
+ * 条目上把用户的图拦在了本地,而引擎其实会把图交给模型。env 车道与 web-admin 车道恒写 `input`,分叉只落在手写目录条目上。
148
151
  * `input` 不是数组(手写错形)与条目不在,同归 `undeclared`:没有可信的声明。
149
152
  */
150
153
  export declare function modelImageInput(model: {
@@ -1,10 +1,17 @@
1
1
  import { expandTiers, parseModelMention, resolveTaskModel as coreResolveTaskModel } from "@sema-agent/core";
2
+ import { DOMAIN_SCHEMAS } from "@sema-agent/settings-schema";
3
+ const AT_MENTION_SEAT = DOMAIN_SCHEMAS.models.shape.atMention;
4
+ export const DEFAULT_AT_MENTION = Object.freeze(AT_MENTION_SEAT.parse(undefined));
5
+ export function readAtMention(raw) {
6
+ const r = AT_MENTION_SEAT.safeParse(raw);
7
+ return r.success ? r.data : undefined;
8
+ }
2
9
  export const DEFAULT_MODEL_REF = "default";
3
10
  export function resolveMainModel(plane) {
4
11
  const expanded = expandTiers(plane.models, plane.tiers ?? {}) ?? plane.models;
5
12
  return coreResolveTaskModel({ model: DEFAULT_MODEL_REF }, { ...(expanded !== undefined ? { models: expanded } : {}) }).model;
6
13
  }
7
- export function resolveTaskModel(bodyModel, objective, catalog, allowlist, refusals) {
14
+ export function resolveTaskModel(bodyModel, objective, catalog, atMention, refusals) {
8
15
  const mention = parseModelMention(objective, [...Object.keys(refusals ?? {}), ...Object.keys(catalog)]);
9
16
  const mentionUsable = mention.model !== undefined && Object.hasOwn(catalog, mention.model) ? mention.model : undefined;
10
17
  const explicit = typeof bodyModel === "string" ? matchCatalogModel(bodyModel, catalog, refusals) : undefined;
@@ -20,7 +27,7 @@ export function resolveTaskModel(bodyModel, objective, catalog, allowlist, refus
20
27
  const unknown = present && explicit === undefined && refusedExplicit === undefined;
21
28
  const model = explicit ?? mentionUsable ?? DEFAULT_MODEL_REF;
22
29
  const picked = explicit !== undefined ? { ref: explicit, source: "explicit", requested: repr } : mentionUsable !== undefined ? { ref: mentionUsable, source: "mention", requested: mentionUsable } : undefined;
23
- const notAllowed = picked !== undefined && !isModelAllowlisted(picked.ref, catalog, allowlist, refusals)
30
+ const notAllowed = picked !== undefined && !isAtMentionable(picked.ref, catalog, atMention, refusals)
24
31
  ? { requested: picked.requested, resolved: terminalCatalogName(picked.ref, catalog, refusals) ?? picked.ref, source: picked.source }
25
32
  : undefined;
26
33
  return {
@@ -51,24 +58,22 @@ export function refusedCatalogEntry(ref, refusals) {
51
58
  return { name, refusal: r };
52
59
  return undefined;
53
60
  }
54
- export function isModelAllowlisted(ref, catalog, allowlist, refusals) {
55
- if (allowlist === undefined || allowlist.length === 0)
56
- return true;
57
- const entry = catalogEntryOf(ref, catalog, refusals);
58
- if (entry === undefined)
61
+ export function isAtMentionable(ref, catalog, atMention, refusals) {
62
+ const name = terminalCatalogName(ref, catalog, refusals);
63
+ if (name === undefined)
59
64
  return true;
60
- for (const name of allowlist) {
61
- const allowed = catalogEntryOf(name, catalog, refusals);
62
- if (allowed === undefined)
63
- continue;
64
- if (allowed === entry)
65
+ switch (atMention.mode) {
66
+ case "all":
65
67
  return true;
68
+ case "none":
69
+ return false;
70
+ case "only":
71
+ return atMention.models.includes(name);
72
+ default: {
73
+ const unhandled = atMention;
74
+ throw new Error(`atMention: unknown mode ${JSON.stringify(unhandled)}`);
75
+ }
66
76
  }
67
- return false;
68
- }
69
- function catalogEntryOf(ref, catalog, refusals) {
70
- const name = matchCatalogModel(ref, catalog, refusals);
71
- return name === undefined ? undefined : catalog[name];
72
77
  }
73
78
  export function terminalCatalogName(ref, catalog, refusals) {
74
79
  const key = matchCatalogModel(ref, catalog, refusals);
@@ -23,7 +23,7 @@ export declare class Metrics {
23
23
  /** Structured snapshot of the whole registry — the OTLP exporter maps this to OTel data points
24
24
  * (counters→Sum monotonic, gauges/dynamic→Gauge, histograms→Histogram). Read-only copy. */
25
25
  snapshot(): MetricSnapshot;
26
- /** Curated health summary (friendly JSON) for a lightweight fleet dashboard — the sema-registry's
26
+ /** Curated health summary (friendly JSON) for a lightweight fleet dashboard — the web-admin's
27
27
  * fleet page fetches this per worker and renders cards + computes rates over polls (no Prometheus
28
28
  * needed). Counters are cumulative totals; histograms are reported as averages (sum/count). */
29
29
  summarize(): MetricsSummary;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Org 记忆准入 resolver — server 侧对接 @sema-agent/core@5.13.0 `RunnerDeps.memoryScopeAdmission` seam。
3
- * 授权事实来自「目录」抽象:远程腿 = config-center per-principal caps 响应里的 `orgMemory` 段(形状
3
+ * 授权事实来自「目录」抽象:远程腿 = web-admin per-principal caps 响应里的 `orgMemory` 段(形状
4
4
  * 同源锚 = registry-core `PrincipalOrgMemoryWire`,zod schema 直接 safeParse);单机腿 = env JSON 静态表
5
5
  * (`MEMORY_ORG_DIRECTORY_JSON`,{@link parseOrgDirectoryStatic} 装载,启动期 fail-loud)。
6
6
  *
@@ -1,5 +1,6 @@
1
1
  import type { AskState } from "../approval-ask-machine.js";
2
- import type { AskCallCoordinate, AskRow, AskTerminalClaimIntent, AskTerminalClaimOutcome, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, DecideResult, EnsureAskResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
2
+ import type { SettleCause } from "../approval-deny-reasons.js";
3
+ import type { AskCallCoordinate, AskRow, AskTerminalClaimIntent, AskTerminalClaimOpts, AskTerminalClaimOutcome, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, DecideResult, EnsureAskResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
3
4
  /** 数据根下的目录名。**按店命名**,与兄弟 File 店同规(`approval-nonces` / `approval-ask-audit` /
4
5
  * `approval-exemptions` / `resume-anchors` / `checkpoint-ctx` / …)—— 首版写的是族名 `approvals`,而那
5
6
  * 正是读者会去找**持久 park 行**的地方(它们其实在 `checkpoint-ctx`),且持久数据面的名字按硬 breaking
@@ -61,6 +62,7 @@ type JournalCall = {
61
62
  askId: string;
62
63
  batchId: string;
63
64
  intent: JournalV1ClaimIntent;
65
+ cause?: SettleCause;
64
66
  }
65
67
  /** NP-2 的部署政策拒(`claimTerminal(…, "refuse")`)—— 新动词而不是老动词带新意图,理由见 {@link JOURNAL_V1_CLAIM_INTENTS}。 */
66
68
  | {
@@ -162,7 +164,7 @@ export declare class FileApprovalAskStore implements ApprovalAskStore {
162
164
  * 「先编码、编码成功才一次性提交」同一条原子性)。
163
165
  */
164
166
  decideAsk(askId: string, batchId: string, decision: DecideAskInput): Promise<DecideResult>;
165
- claimTerminal(askId: string, batchId: string, intent: AskTerminalClaimIntent): Promise<AskTerminalClaimOutcome>;
167
+ claimTerminal(askId: string, batchId: string, intent: AskTerminalClaimIntent, opts?: AskTerminalClaimOpts): Promise<AskTerminalClaimOutcome>;
166
168
  expireAsk(askId: string, batchId: string): Promise<ExpireResult>;
167
169
  bindBatch(batchId: string, askId: string, gate: BindGateInput): Promise<BindResult>;
168
170
  abortBatch(batchId: string): Promise<string[]>;
@@ -182,7 +182,7 @@ export class FileApprovalAskStore {
182
182
  case "decideAsk":
183
183
  return this.core.decideAsk(rec.askId, rec.batchId, rec.decision);
184
184
  case "claimTerminal":
185
- return this.core.claimTerminal(rec.askId, rec.batchId, rec.intent);
185
+ return this.core.claimTerminal(rec.askId, rec.batchId, rec.intent, rec.cause !== undefined ? { cause: rec.cause } : undefined);
186
186
  case "claimTerminalRefuse":
187
187
  return this.core.claimTerminal(rec.askId, rec.batchId, "refuse");
188
188
  case "expireAsk":
@@ -213,11 +213,11 @@ export class FileApprovalAskStore {
213
213
  const journalled = decision.updatedInput === undefined ? decision : { ...decision, updatedInput: encodeDecideUpdatedInput(decision.updatedInput) };
214
214
  return this.write({ op: "decideAsk", askId, batchId, decision: journalled }, (c) => this.core.decideAsk(c.askId, c.batchId, c.decision));
215
215
  }
216
- async claimTerminal(askId, batchId, intent) {
216
+ async claimTerminal(askId, batchId, intent, opts) {
217
217
  switch (intent) {
218
218
  case "expire":
219
219
  case "cancel":
220
- return this.write({ op: "claimTerminal", askId, batchId, intent }, (c) => this.core.claimTerminal(c.askId, c.batchId, c.intent));
220
+ return this.write({ op: "claimTerminal", askId, batchId, intent, ...(opts?.cause !== undefined ? { cause: opts.cause } : {}) }, (c) => this.core.claimTerminal(c.askId, c.batchId, c.intent, c.cause !== undefined ? { cause: c.cause } : undefined));
221
221
  case "refuse":
222
222
  return this.write({ op: "claimTerminalRefuse", askId, batchId }, (c) => this.core.claimTerminal(c.askId, c.batchId, "refuse"));
223
223
  default: {
@@ -25,7 +25,7 @@
25
25
  * - resolveProvisional 故意不经 canAskTransition(同 SQL twin 头注:这是版本化补偿的例外通道)。
26
26
  */
27
27
  import { type AskState, type BatchState } from "../approval-ask-machine.js";
28
- import type { AskCallCoordinate, AskDecision, AskRow, AskTerminalClaimIntent, AskTerminalClaimOutcome, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, EnsureAskResult, DecideResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
28
+ import type { AskCallCoordinate, AskDecision, AskRow, AskTerminalClaimIntent, AskTerminalClaimOpts, AskTerminalClaimOutcome, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, EnsureAskResult, DecideResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
29
29
  /**
30
30
  * [ref] 的 `updatedInput` **入店编码**——两 twin 共用的那一只(本函数是唯一真源)。
31
31
  *
@@ -88,8 +88,9 @@ export declare class InMemoryApprovalAskStore implements ApprovalAskStore {
88
88
  * 论证);await 后读到的行 ≥ CAS 线性化点,而非 pending 态无回边 ⇒ 败方读数自足(与 SQL 侧同一条
89
89
  * 单调性论证)。
90
90
  */
91
- claimTerminal(askId: string, batchId: string, intent: AskTerminalClaimIntent): Promise<AskTerminalClaimOutcome>;
92
- expireAsk(askId: string, batchId: string): Promise<ExpireResult>;
91
+ claimTerminal(askId: string, batchId: string, intent: AskTerminalClaimIntent, opts?: AskTerminalClaimOpts): Promise<AskTerminalClaimOutcome>;
92
+ /** `cause`(可选)= 转 park 的触发成因,赢下 PARKING 的同一步落进 `deniedReason`(SQL twin 同一条 UPDATE 的镜像;接口上的 `expireAsk` 不带它,只有 `claimTerminal(expire, { cause })` 带)。 */
93
+ expireAsk(askId: string, batchId: string, cause?: AskTerminalClaimOpts["cause"]): Promise<ExpireResult>;
93
94
  bindBatch(batchId: string, askId: string, gate: BindGateInput): Promise<BindResult>;
94
95
  deferReconcile(askId: string, expectedState: AskState, expectedRev: number, nowMs: number): Promise<boolean>;
95
96
  abortBatch(batchId: string): Promise<string[]>;
@@ -1,4 +1,5 @@
1
1
  import { canAskTransition } from "../approval-ask-machine.js";
2
+ import { APPROVAL_ENDING_CAUSES } from "../approval-deny-reasons.js";
2
3
  import { askRowOnCall, assertDecideSettledBy, assertIdempotencyKeyShape, assertIntegralEpochMs, ApprovalAskReadAbortedError, claimTargetState, POLICY_REFUSAL_DECISION, projectAskTerminalClaimTruth } from "./approval-ask-store-sql.js";
3
4
  function applyPatch(row, patch) {
4
5
  if (patch.decision !== undefined)
@@ -152,10 +153,10 @@ export class InMemoryApprovalAskStore {
152
153
  ask.updatedAtMs = now;
153
154
  return { ok: true, row: { ...ask } };
154
155
  }
155
- async claimTerminal(askId, batchId, intent) {
156
+ async claimTerminal(askId, batchId, intent, opts) {
156
157
  switch (intent) {
157
158
  case "expire": {
158
- const res = await this.expireAsk(askId, batchId);
159
+ const res = await this.expireAsk(askId, batchId, opts?.cause);
159
160
  if (res.won)
160
161
  return { claimed: true, voidedSiblings: res.voidedSiblings };
161
162
  break;
@@ -164,7 +165,7 @@ export class InMemoryApprovalAskStore {
164
165
  const scoped = this.asks.get(askId);
165
166
  if (scoped !== undefined && scoped.batchId !== batchId)
166
167
  return { claimed: false, current: { state: "absent" } };
167
- const won = await this.transitionAsk(askId, "STREAM_PENDING", "VOID", { updatedAtMs: this.now() });
168
+ const won = await this.transitionAsk(askId, "STREAM_PENDING", "VOID", { updatedAtMs: this.now(), ...(opts?.cause !== undefined ? { deniedReason: opts.cause } : {}) });
168
169
  if (won)
169
170
  return { claimed: true, voidedSiblings: [] };
170
171
  break;
@@ -195,7 +196,7 @@ export class InMemoryApprovalAskStore {
195
196
  const cur = this.asks.get(askId);
196
197
  return { claimed: false, current: projectAskTerminalClaimTruth(cur !== undefined && cur.batchId === batchId ? { ...cur } : null) };
197
198
  }
198
- async expireAsk(askId, batchId) {
199
+ async expireAsk(askId, batchId, cause) {
199
200
  const now = this.now();
200
201
  const batch = this.batches.get(batchId);
201
202
  if (!batch)
@@ -204,6 +205,8 @@ export class InMemoryApprovalAskStore {
204
205
  if (!ask || ask.state !== "STREAM_PENDING" || ask.batchId !== batchId)
205
206
  return { won: false, voidedSiblings: [] };
206
207
  ask.state = "PARKING";
208
+ if (cause !== undefined)
209
+ ask.deniedReason = cause;
207
210
  ask.rev += 1;
208
211
  ask.updatedAtMs = now;
209
212
  if (batch.state === "OPEN") {
@@ -215,6 +218,7 @@ export class InMemoryApprovalAskStore {
215
218
  for (const [id, r] of this.asks) {
216
219
  if (r.batchId === batchId && r.state === "STREAM_PENDING" && id !== askId) {
217
220
  r.state = "VOID";
221
+ r.deniedReason = APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN;
218
222
  r.rev += 1;
219
223
  r.updatedAtMs = now;
220
224
  voided.push(id);
@@ -247,6 +251,7 @@ export class InMemoryApprovalAskStore {
247
251
  for (const [id, r] of this.asks) {
248
252
  if (r.batchId === batchId && r.state === "PARKING" && id !== askId) {
249
253
  r.state = "VOID";
254
+ r.deniedReason = APPROVAL_ENDING_CAUSES.BATCH_BOUND_ELSEWHERE;
250
255
  r.rev += 1;
251
256
  r.updatedAtMs = now;
252
257
  voidedSiblings.push(id);
@@ -274,6 +279,7 @@ export class InMemoryApprovalAskStore {
274
279
  for (const [id, r] of this.asks) {
275
280
  if (r.batchId === batchId && r.state === "STREAM_PENDING") {
276
281
  r.state = "VOID";
282
+ r.deniedReason = APPROVAL_ENDING_CAUSES.BATCH_ABORTED;
277
283
  r.rev += 1;
278
284
  r.updatedAtMs = now;
279
285
  voided.push(id);
@@ -42,6 +42,7 @@ import type { PgQueryFn } from "./pg-query.js";
42
42
  import { type SqlDriver, type SqlDialect } from "./sql-driver.js";
43
43
  import { type AskState, type BatchState } from "../approval-ask-machine.js";
44
44
  import type { HostSettledBy } from "../host-decision.js";
45
+ import { type AskRowCause, type SettleCause } from "../approval-deny-reasons.js";
45
46
  import { type IndexSpec } from "./ensure-index.js";
46
47
  export type AskDecision = "approve" | "deny";
47
48
  export interface AskRow {
@@ -317,6 +318,15 @@ export interface ExpireResult {
317
318
  * WHERE 谓词 {@link pendingClaimWhere} 兑现,不是把不同 verb 塞进一个函数。
318
319
  */
319
320
  export type AskTerminalClaimIntent = "expire" | "cancel" | "refuse";
321
+ /**
322
+ * 终局 claim 的可选实参:`cause` —— **赢下 `cancel`(VOID)/ `expire`(PARKING)时写进这一行 `denied_reason` 列的成因词**(决断口 410 / 晚答 404 / 跨副本的败方读数读同一列,
323
+ * 所以这一行自己记着「为什么」,读面不靠任何进程内的东西)。`cancel` 写终态成因({@link SettleCause} 里的 `run_cancelled` / `run_exited`),
324
+ * `expire` 写转 park 的触发成因({@link ParkTriggerCause}:`ttl_expired` / `broadcast_lost` / `stream_disconnected` / `undeliverable` / `shutdown_before_park`);缺席 ⇒ 列保持 NULL(读面缺席即缺席)。
325
+ * `refuse` 不读它(落的是一条决议,来源位 `policy`,不是结局成因);`expire` 撤兄弟时店自己写 `batch_sibling_withdrawn`(那是这条事务本身的事实)。
326
+ */
327
+ export interface AskTerminalClaimOpts {
328
+ readonly cause?: SettleCause;
329
+ }
320
330
  /**
321
331
  * `refuse` 意图落行的决议(NP-2)—— **部署政策拒**:deny + 结算来源 `policy`。来源词的型取 core 的 `HostSettledBy`
322
332
  * (core 改词 / 退词 ⇒ 这里 tsc 红);协调器交给 core 的那一只(`tool-approval.ts` 的 `UnattendedPolicyRefusal`)读的是
@@ -353,6 +363,10 @@ export type AskTerminalClaimCurrent = {
353
363
  /** S-558:赢家决议的**结算来源**(语义与两态读法见 {@link AskRow.settledBy})。**必填**(不是可选)——
354
364
  * 可选会让投影漏挑这一列时静默编译过,而漏挑的后果恰是本批要修的病:CAS 输家把赢家的策略拒读成人拒。 */
355
365
  settledBy: HostSettledBy | null;
366
+ /** 行上记着的成因,经问账行成因的唯一读口 `causeOfAskRow` 读出(`kind` 说它在这一行上的意思:VOID / DENIED = 终态成因,PARKING / PARKED = 转 park 的触发成因);
367
+ * **只在读得出时在场**(没写 / 与 state 不配 ⇒ 缺席;DECIDED 的败方读数逐字不变)。CAS 输家据它按**行上那个成因**结算(而不是把别人撤的卡读成一次拒绝);
368
+ * 投影的唯一函数是 {@link projectAskTerminalClaimTruth},三 twin 不各自挑列,原始列值不出店层。 */
369
+ cause?: AskRowCause;
356
370
  };
357
371
  /**
358
372
  * 单次原子往返的答案(语义律的唯一规范陈述点 = core `checkpoint-store.ts` `claimTerminal` 契约 JSDoc,
@@ -424,7 +438,7 @@ export interface ApprovalAskStore {
424
438
  * `expireAsk`/`transitionAsk`(InMemory,单线程天然原子)——两向都成立的判据是**没有第二套谓词**。
425
439
  * 方法**必备**(本仓自有三形全实现,无 core 那侧的 presence 探测问题)。
426
440
  */
427
- claimTerminal(askId: string, batchId: string, intent: AskTerminalClaimIntent): Promise<AskTerminalClaimOutcome>;
441
+ claimTerminal(askId: string, batchId: string, intent: AskTerminalClaimIntent, opts?: AskTerminalClaimOpts): Promise<AskTerminalClaimOutcome>;
428
442
  expireAsk(askId: string, batchId: string): Promise<ExpireResult>;
429
443
  bindBatch(batchId: string, askId: string, gate: BindGateInput): Promise<BindResult>;
430
444
  abortBatch(batchId: string): Promise<string[]>;
@@ -624,7 +638,7 @@ export declare class SqlApprovalAskStore implements ApprovalAskStore {
624
638
  * 在状态机上无回边 ⇒ 锁定读读到的恒 ≥ 线性化点,且 DECIDED 决议不可变 —— 答案自足。
625
639
  * · 本事务在输臂上除批行 rev 自增外零写,rollback 无痕。
626
640
  */
627
- claimTerminal(askId: string, batchId: string, intent: AskTerminalClaimIntent): Promise<AskTerminalClaimOutcome>;
641
+ claimTerminal(askId: string, batchId: string, intent: AskTerminalClaimIntent, opts?: AskTerminalClaimOpts): Promise<AskTerminalClaimOutcome>;
628
642
  /** [ref] 起 = {@link claimTerminal}(expire 意图)的薄投影 —— 律 2「在 claimTerminal 之上实现」的
629
643
  * SQL 半场落笔处:谓词/事务/兄弟撤卡全在 claim 里,这里零第二套写路径。败方读数按既有返回形丢弃
630
644
  * (`ExpireResult` 不携真相 —— 要真相的调用方直接用 claimTerminal;对账收敛器的 orphan 腿输了本就
@@ -1,5 +1,6 @@
1
1
  import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
2
2
  import { canAskTransition } from "../approval-ask-machine.js";
3
+ import { APPROVAL_ENDING_CAUSES, causeOfAskRow } from "../approval-deny-reasons.js";
3
4
  import { isDupKeyError, isMissingColumnError } from "./sql-errors.js";
4
5
  import { decodeJsonColumn, foldJsonColumn, jsonColumnText, readJsonObjectColumn, readTriStateJsonValueColumn } from "./sql-json-column.js";
5
6
  import { ensureIndexes } from "./ensure-index.js";
@@ -70,12 +71,14 @@ export function claimTargetState(intent) {
70
71
  export function projectAskTerminalClaimTruth(row) {
71
72
  if (row === null || row.state === "STREAM_PENDING")
72
73
  return { state: "absent" };
74
+ const cause = causeOfAskRow(row);
73
75
  return {
74
76
  state: row.state,
75
77
  rev: row.rev,
76
78
  decision: row.decision,
77
79
  ...(row.updatedInput !== undefined ? { updatedInput: row.updatedInput } : {}),
78
80
  settledBy: row.settledBy,
81
+ ...(cause !== undefined ? { cause } : {}),
79
82
  };
80
83
  }
81
84
  export function askRowOnCall(row, call) {
@@ -501,7 +504,7 @@ export class SqlApprovalAskStore {
501
504
  throw new Error(`decideAsk: row vanished after commit (askId=${askId})`);
502
505
  return { ok: true, row };
503
506
  }
504
- async claimTerminal(askId, batchId, intent) {
507
+ async claimTerminal(askId, batchId, intent, opts) {
505
508
  const to = claimTargetState(intent);
506
509
  if (!canAskTransition("STREAM_PENDING", to)) {
507
510
  throw new Error(`approval-ask-store.claimTerminal: illegal terminal claim STREAM_PENDING -> ${to} (askId=${askId})`);
@@ -522,12 +525,15 @@ export class SqlApprovalAskStore {
522
525
  }
523
526
  const askRes = intent === "refuse"
524
527
  ? await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state = ${ph(dialect, 1)}, decision = ${ph(dialect, 2)}, settled_by = ${ph(dialect, 3)}, decided_at_ms = ${ph(dialect, 4)}, rev = rev + 1, updated_at_ms = ${ph(dialect, 5)} WHERE ${pendingClaimWhere(dialect, 6)}`, [to, POLICY_REFUSAL_DECISION.decision, POLICY_REFUSAL_DECISION.settledBy, now, now, askId, batchId])
525
- : await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state = ${ph(dialect, 1)}, rev = rev + 1, updated_at_ms = ${ph(dialect, 2)} WHERE ${pendingClaimWhere(dialect, 3)}`, [
526
- to,
527
- now,
528
- askId,
529
- batchId,
530
- ]);
528
+ : (intent === "cancel" || intent === "expire") && opts?.cause !== undefined
529
+ ?
530
+ await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state = ${ph(dialect, 1)}, denied_reason = ${ph(dialect, 2)}, rev = rev + 1, updated_at_ms = ${ph(dialect, 3)} WHERE ${pendingClaimWhere(dialect, 4)}`, [to, opts.cause, now, askId, batchId])
531
+ : await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state = ${ph(dialect, 1)}, rev = rev + 1, updated_at_ms = ${ph(dialect, 2)} WHERE ${pendingClaimWhere(dialect, 3)}`, [
532
+ to,
533
+ now,
534
+ askId,
535
+ batchId,
536
+ ]);
531
537
  if (askRes.affected !== 1) {
532
538
  const sel = await conn.query(`SELECT ${askCols(dialect)} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = ${ph(dialect, 1)} AND batch_id = ${ph(dialect, 2)} FOR UPDATE`, [askId, batchId]);
533
539
  await conn.rollback();
@@ -540,7 +546,7 @@ export class SqlApprovalAskStore {
540
546
  await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_UNBOUND', rev=rev+1, updated_at_ms=? WHERE batch_id=? AND state='OPEN'`, `UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_UNBOUND', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='OPEN'`), [now, batchId]);
541
547
  let voidedSiblings;
542
548
  if (this.db.dialect === "pg") {
543
- const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId]);
549
+ const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=$4, rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId, APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN]);
544
550
  voidedSiblings = res.rows.map((r) => String(r.ask_id));
545
551
  }
546
552
  else {
@@ -548,7 +554,8 @@ export class SqlApprovalAskStore {
548
554
  voidedSiblings = sel.rows.map((r) => String(r.ask_id));
549
555
  if (voidedSiblings.length > 0) {
550
556
  const inList = voidedSiblings.map(() => "?").join(", ");
551
- await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='STREAM_PENDING'`, [
557
+ await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=?, rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='STREAM_PENDING'`, [
558
+ APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN,
552
559
  now,
553
560
  ...voidedSiblings,
554
561
  ]);
@@ -593,7 +600,7 @@ export class SqlApprovalAskStore {
593
600
  }
594
601
  let voidedSiblings;
595
602
  if (this.db.dialect === "pg") {
596
- const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='PARKING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId]);
603
+ const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=$4, rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='PARKING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId, APPROVAL_ENDING_CAUSES.BATCH_BOUND_ELSEWHERE]);
597
604
  voidedSiblings = res.rows.map((r) => String(r.ask_id));
598
605
  }
599
606
  else {
@@ -601,7 +608,7 @@ export class SqlApprovalAskStore {
601
608
  voidedSiblings = sel.rows.map((r) => String(r.ask_id));
602
609
  if (voidedSiblings.length > 0) {
603
610
  const inList = voidedSiblings.map(() => "?").join(", ");
604
- await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='PARKING'`, [now, ...voidedSiblings]);
611
+ await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=?, rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='PARKING'`, [APPROVAL_ENDING_CAUSES.BATCH_BOUND_ELSEWHERE, now, ...voidedSiblings]);
605
612
  }
606
613
  }
607
614
  await conn.commit();
@@ -631,7 +638,7 @@ export class SqlApprovalAskStore {
631
638
  }
632
639
  let voided;
633
640
  if (this.db.dialect === "pg") {
634
- const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' RETURNING ask_id`, [now, batchId]);
641
+ const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=$3, rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' RETURNING ask_id`, [now, batchId, APPROVAL_ENDING_CAUSES.BATCH_ABORTED]);
635
642
  voided = res.rows.map((r) => String(r.ask_id));
636
643
  }
637
644
  else {
@@ -639,7 +646,7 @@ export class SqlApprovalAskStore {
639
646
  voided = sel.rows.map((r) => String(r.ask_id));
640
647
  if (voided.length > 0) {
641
648
  const inList = voided.map(() => "?").join(", ");
642
- await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='STREAM_PENDING'`, [now, ...voided]);
649
+ await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=?, rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='STREAM_PENDING'`, [APPROVAL_ENDING_CAUSES.BATCH_ABORTED, now, ...voided]);
643
650
  }
644
651
  }
645
652
  await conn.commit();
@@ -57,16 +57,6 @@ export declare class CachingSessionStore implements SessionStore {
57
57
  }) => Promise<SessionListItem[]>;
58
58
  readonly fork?: (sourceId: string, owner: string | null) => Promise<string | null>;
59
59
  readonly deleteSession?: (sessionId: string, owner: string | null) => Promise<boolean>;
60
- /** K-5c (core 1.155 SessionStore.noteTaskRun seam) — core's Runner calls it at runTask START to record the latest
61
- * run per session (→ listSessions `lastRunId`). MUST be forwarded: the local backend's Runner store is THIS cache
62
- * wrapper, so without forwarding `noteTaskRun?.()` silently no-ops and lastRunId stays null (review BLOCKER). Pure
63
- * pass-through to the inner store's in-mem map — no cache interaction. (TiDB/PG don't implement it → not bound.)
64
- * Return type matches core's `SessionStore.noteTaskRun?(): void | Promise<void>` (review: don't narrow to void).
65
- * [ref](core 5.65 增量批):第三参 = engine runId。绑定用的是 `.bind(inner)`(整函数),运行期本就
66
- * 三参全转 —— 这里把**声明**对齐到 core 的三参形,免得读这行类型的人以为缝上只有两参(fork-routing
67
- * 的两参形正是在运行期真丢过第三参的那个病,同批修)。内店(LocalSessionStore)今日仍是两参消费,
68
- * runId 落地候 sessions 列表 wire 键的 spec 节拍(见本批提货报告件1b)。 */
69
- readonly noteTaskRun?: (sessionId: string, taskId: string, runId?: string) => void | Promise<void>;
70
60
  /** Write-once auto-title (SQL twins + local sidecar carry it; pure pass-through). */
71
61
  readonly setTitleIfNull?: (sessionId: string, title: string) => Promise<boolean>;
72
62
  readonly probeTitle?: (sessionId: string) => Promise<"none" | "untitled" | "titled">;
@@ -11,7 +11,6 @@ export class CachingSessionStore {
11
11
  listSessions;
12
12
  fork;
13
13
  deleteSession;
14
- noteTaskRun;
15
14
  setTitleIfNull;
16
15
  probeTitle;
17
16
  readInstructionSource;
@@ -62,8 +61,6 @@ export class CachingSessionStore {
62
61
  this.recordInstructionSource = ownerAware.recordInstructionSource.bind(inner);
63
62
  if (ownerAware.fork)
64
63
  this.fork = ownerAware.fork.bind(inner);
65
- if (ownerAware.noteTaskRun)
66
- this.noteTaskRun = ownerAware.noteTaskRun.bind(inner);
67
64
  if (ownerAware.getLeafId)
68
65
  this.getLeafId = ownerAware.getLeafId.bind(inner);
69
66
  if (ownerAware.getHead)
@@ -499,9 +499,15 @@ export class FileRunStore {
499
499
  const sorted = [...arr].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime() || (a.taskId < b.taskId ? 1 : a.taskId > b.taskId ? -1 : 0));
500
500
  const latest = sorted[0];
501
501
  const latestRunId = latest.result?.runId;
502
- const times = arr.map((r) => r.createdAt.getTime());
503
- const last = Math.max(...times);
504
- const first = Math.min(...times);
502
+ let last = -Infinity;
503
+ let first = Infinity;
504
+ for (const r of arr) {
505
+ const t = r.createdAt.getTime();
506
+ if (t > last)
507
+ last = t;
508
+ if (t < first)
509
+ first = t;
510
+ }
505
511
  summaries.push({
506
512
  sessionId,
507
513
  owner: latest.owner,
@@ -12,8 +12,9 @@ export interface LeaderRunRecord {
12
12
  status: LeaderRunStatus;
13
13
  /** BL-4 属主(null = 匿名/单租)。GET 的 404 门读它。 */
14
14
  owner: string | null;
15
- /** 运维列表面的可读预览(**不是**完整 objective —— 名字即语义,见 DDL 列注)。 */
16
- objectivePreview: string;
15
+ /** 运维列表面的可读预览(**不是**完整 objective —— 名字即语义,见 DDL 列注)。一个事实位,与 run 行同形:`""` = 建过、剥完没有用户正文;
16
+ * `null` = 没记录(列 NULL:更早版本 / 手改留下的行;本版写点只写 `""` 或正文)。 */
17
+ objectivePreview: string | null;
17
18
  result?: unknown;
18
19
  error?: string;
19
20
  startedAtMs: number;
@@ -4,6 +4,7 @@ import { isDupKeyError } from "./sql-errors.js";
4
4
  import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
5
5
  import { decodeNullableJsonColumn, foldJsonColumn, jsonColumnText, readNonNullJsonValueColumn } from "./sql-json-column.js";
6
6
  import { ensureIndexes } from "./ensure-index.js";
7
+ import { buildObjectivePreview } from "../user-authored-text.js";
7
8
  export const LEADER_RUN_TABLE = "leader_run";
8
9
  const LEADER_RUN_STATUSES = new Set(["running", "completed", "failed", "needs_human"]);
9
10
  export const LEADER_OBJECTIVE_PREVIEW_CHARS = 160;
@@ -86,7 +87,7 @@ function mapRow(r) {
86
87
  id,
87
88
  status: statusOf(r.status, id),
88
89
  owner: r.owner == null ? null : String(r.owner),
89
- objectivePreview: r.objective_preview == null ? "" : String(r.objective_preview),
90
+ objectivePreview: r.objective_preview == null ? null : String(r.objective_preview),
90
91
  ...(result === undefined ? {} : { result }),
91
92
  ...(error === undefined ? {} : { error }),
92
93
  startedAtMs: Number(r.started_at_ms),
@@ -104,7 +105,7 @@ export class SqlLeaderRunStore {
104
105
  async createRun(input) {
105
106
  const id = uuidv7();
106
107
  const now = Date.now();
107
- const preview = input.objective.slice(0, LEADER_OBJECTIVE_PREVIEW_CHARS);
108
+ const preview = buildObjectivePreview(input.objective, LEADER_OBJECTIVE_PREVIEW_CHARS);
108
109
  const params = [id, input.idemKey, input.owner, preview, "running", now];
109
110
  try {
110
111
  await this.db.query(this.q(`INSERT INTO ${LEADER_RUN_TABLE} (leader_run_id, idem_key, owner, objective_preview, status, started_at_ms) VALUES (?,?,?,?,?,?)`, `INSERT INTO ${LEADER_RUN_TABLE} (leader_run_id, idem_key, owner, objective_preview, status, started_at_ms) VALUES ($1,$2,$3,$4,$5,$6)`), params);