@sema-agent/server 7.122.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 (256) hide show
  1. package/MIGRATION.md +209 -0
  2. package/README.md +7 -2
  3. package/README.zh-CN.md +5 -2
  4. package/USAGE.md +76 -56
  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.d.ts +1 -1
  18. package/dist/audit.js +53 -23
  19. package/dist/auth-bridge.d.ts +8 -8
  20. package/dist/auth-bridge.js +6 -6
  21. package/dist/auth-keys.d.ts +2 -2
  22. package/dist/auto-mode-face.d.ts +13 -9
  23. package/dist/auto-mode-face.js +8 -7
  24. package/dist/boot/budget-tracing.js +1 -1
  25. package/dist/boot/execution-env.js +1 -0
  26. package/dist/boot/leader.d.ts +1 -1
  27. package/dist/boot/memory-consolidation.d.ts +4 -4
  28. package/dist/boot/org-memory.d.ts +3 -3
  29. package/dist/boot/org-memory.js +5 -5
  30. package/dist/boot/resolve-spec.js +7 -7
  31. package/dist/boot/runner-deps.d.ts +1 -1
  32. package/dist/boot/runtime-caps.d.ts +1 -1
  33. package/dist/boot/runtime-caps.js +10 -10
  34. package/dist/boot/shutdown.d.ts +2 -2
  35. package/dist/boot/shutdown.js +2 -2
  36. package/dist/boot/side-query-lane.d.ts +3 -3
  37. package/dist/boot/{stage-02-registry-pull.d.ts → stage-02-web-admin-pull.d.ts} +2 -2
  38. package/dist/boot/{stage-02-registry-pull.js → stage-02-web-admin-pull.js} +4 -4
  39. package/dist/boot/stage-03-store-backend.d.ts +2 -2
  40. package/dist/boot/stage-04-stores.d.ts +1 -1
  41. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  42. package/dist/boot/stage-06-runners.d.ts +2 -2
  43. package/dist/boot/stage-06-runners.js +6 -6
  44. package/dist/boot/stage-07-capability-layer.d.ts +2 -2
  45. package/dist/boot/stage-07-capability-layer.js +6 -5
  46. package/dist/boot/stage-08-reapers.d.ts +3 -3
  47. package/dist/boot/stage-08-reapers.js +3 -3
  48. package/dist/boot/stage-09-leader.d.ts +3 -3
  49. package/dist/boot/stage-09-leader.js +7 -7
  50. package/dist/boot/stage-10-http-server.d.ts +3 -3
  51. package/dist/boot/stage-10-http-server.js +19 -19
  52. package/dist/boot/unpriced-ceiling-verdict.d.ts +1 -1
  53. package/dist/boot/{config-center.d.ts → web-admin.d.ts} +11 -11
  54. package/dist/boot/{config-center.js → web-admin.js} +39 -20
  55. package/dist/boot/webfetch-summarize-lane.d.ts +2 -2
  56. package/dist/brain.d.ts +1 -1
  57. package/dist/brain.js +1 -1
  58. package/dist/budget.d.ts +3 -3
  59. package/dist/canonical-json.d.ts +2 -2
  60. package/dist/capabilities/center-plugins.d.ts +1 -1
  61. package/dist/capabilities/center-wiring.d.ts +12 -5
  62. package/dist/capabilities/center-wiring.js +11 -2
  63. package/dist/capabilities/composite-members.d.ts +1 -1
  64. package/dist/capabilities/composite-members.js +1 -1
  65. package/dist/capabilities/scenarios.js +2 -2
  66. package/dist/capabilities/team.d.ts +1 -1
  67. package/dist/capabilities/tool-defer.d.ts +1 -1
  68. package/dist/center-credential-renewer.d.ts +1 -1
  69. package/dist/center-credential-renewer.js +7 -7
  70. package/dist/center-credential.d.ts +9 -9
  71. package/dist/center-credential.js +6 -6
  72. package/dist/config-catalog.d.ts +7 -10
  73. package/dist/config-catalog.js +16 -16
  74. package/dist/config-d-migration.d.ts +32 -0
  75. package/dist/config-d-migration.js +59 -0
  76. package/dist/config-invariants.d.ts +14 -4
  77. package/dist/config-invariants.js +31 -2
  78. package/dist/config-lkg.d.ts +34 -11
  79. package/dist/config-lkg.js +76 -48
  80. package/dist/config-provider.d.ts +26 -21
  81. package/dist/config-provider.js +25 -27
  82. package/dist/config-types.d.ts +59 -50
  83. package/dist/config.d.ts +17 -54
  84. package/dist/config.js +99 -181
  85. package/dist/execution-lane-caps.d.ts +1 -1
  86. package/dist/fleet/drain-entry.d.ts +1 -1
  87. package/dist/fleet/engine-signal.d.ts +61 -30
  88. package/dist/fleet/engine-signal.js +29 -5
  89. package/dist/fleet/fleet-bus.d.ts +12 -3
  90. package/dist/fleet/fleet-bus.js +13 -9
  91. package/dist/fleet/serving-lease.d.ts +3 -3
  92. package/dist/fleet/serving-lease.js +1 -1
  93. package/dist/fleet-client.d.ts +15 -13
  94. package/dist/fleet-client.js +9 -7
  95. package/dist/fleet-lease.d.ts +3 -3
  96. package/dist/fleet-lease.js +2 -2
  97. package/dist/host-lsp-manager.d.ts +1 -1
  98. package/dist/hosted-posture.d.ts +4 -4
  99. package/dist/http/admission.js +4 -4
  100. package/dist/http/checkpoint-ctx.d.ts +20 -3
  101. package/dist/http/checkpoint-ctx.js +13 -5
  102. package/dist/http/dispatch.js +7 -16
  103. package/dist/http/durable-run-leg.js +2 -1
  104. package/dist/http/health-self-report.d.ts +7 -4
  105. package/dist/http/health-self-report.js +3 -3
  106. package/dist/http/idempotency.d.ts +1 -1
  107. package/dist/http/resume-legs.d.ts +4 -3
  108. package/dist/http/resume-legs.js +31 -18
  109. package/dist/http/route-ctx.d.ts +8 -0
  110. package/dist/http/routes/admin-config-refresh.d.ts +1 -1
  111. package/dist/http/routes/admin-config-refresh.js +1 -1
  112. package/dist/http/routes/admin-drain.d.ts +1 -1
  113. package/dist/http/routes/admin-drain.js +5 -4
  114. package/dist/http/routes/approvals-assistant.js +4 -4
  115. package/dist/http/routes/capabilities.js +6 -5
  116. package/dist/http/routes/config-catalog.js +2 -3
  117. package/dist/http/routes/diagnostics.js +1 -1
  118. package/dist/http/routes/runs.js +8 -4
  119. package/dist/http/routes/sessions-list.js +2 -1
  120. package/dist/http/routes/sessions.js +2 -5
  121. package/dist/http/routes/side-query.js +3 -3
  122. package/dist/http/routes/tasks.js +3 -2
  123. package/dist/http/run-meta.d.ts +1 -1
  124. package/dist/http/run-meta.js +2 -2
  125. package/dist/http/server.d.ts +7 -7
  126. package/dist/http/server.js +1 -1
  127. package/dist/http/stored-body-replay.d.ts +4 -3
  128. package/dist/http/stored-body-replay.js +5 -5
  129. package/dist/index.d.ts +2 -1
  130. package/dist/index.js +1 -1
  131. package/dist/key-resolver.d.ts +1 -1
  132. package/dist/leader/leader.d.ts +7 -6
  133. package/dist/leader/leader.js +3 -1
  134. package/dist/leader/run-entry.d.ts +2 -2
  135. package/dist/leader/run-entry.js +3 -3
  136. package/dist/leader/wire.d.ts +4 -4
  137. package/dist/leader/wire.js +2 -1
  138. package/dist/main.js +1 -1
  139. package/dist/mcp-tool-faces.d.ts +2 -2
  140. package/dist/model-entry-refusal.d.ts +20 -11
  141. package/dist/model-entry-refusal.js +22 -17
  142. package/dist/model-provider.d.ts +2 -2
  143. package/dist/model-route-endpoint.d.ts +22 -2
  144. package/dist/model-route-endpoint.js +9 -3
  145. package/dist/model-route-gap.d.ts +103 -0
  146. package/dist/model-route-gap.js +80 -0
  147. package/dist/model-route-url.d.ts +1 -1
  148. package/dist/model-route-url.js +2 -1
  149. package/dist/model-select.d.ts +24 -21
  150. package/dist/model-select.js +22 -17
  151. package/dist/namespaced-rule-name.d.ts +16 -30
  152. package/dist/namespaced-rule-name.js +2 -20
  153. package/dist/observability/fail-open.d.ts +0 -4
  154. package/dist/observability/fail-open.js +0 -4
  155. package/dist/observability/metrics.d.ts +1 -1
  156. package/dist/org-memory-admission.d.ts +1 -1
  157. package/dist/plugins/approval-ask-store-file.d.ts +4 -2
  158. package/dist/plugins/approval-ask-store-file.js +3 -3
  159. package/dist/plugins/approval-ask-store-memory.d.ts +4 -3
  160. package/dist/plugins/approval-ask-store-memory.js +10 -4
  161. package/dist/plugins/approval-ask-store-sql.d.ts +16 -2
  162. package/dist/plugins/approval-ask-store-sql.js +20 -13
  163. package/dist/plugins/caching-session-store.d.ts +0 -10
  164. package/dist/plugins/caching-session-store.js +0 -3
  165. package/dist/plugins/file-run-store.js +9 -3
  166. package/dist/plugins/leader-run-store-sql.d.ts +3 -2
  167. package/dist/plugins/leader-run-store-sql.js +3 -2
  168. package/dist/plugins/local-session-store.d.ts +20 -27
  169. package/dist/plugins/local-session-store.js +19 -17
  170. package/dist/plugins/memory-run-store.js +9 -3
  171. package/dist/plugins/one-time-migrations.js +51 -0
  172. package/dist/plugins/scheduler-support.d.ts +26 -7
  173. package/dist/plugins/scheduler-support.js +96 -53
  174. package/dist/plugins/session-policy-store-sql.js +1 -2
  175. package/dist/plugins/store-backend.js +1 -1
  176. package/dist/plugins/store-contracts.d.ts +20 -4
  177. package/dist/plugins/web-search.d.ts +1 -1
  178. package/dist/plugins/web-search.js +2 -1
  179. package/dist/plugins/workflow-run-store-sql.d.ts +4 -2
  180. package/dist/posture-source.d.ts +1 -1
  181. package/dist/read-face-posture.d.ts +3 -3
  182. package/dist/read-face-posture.js +1 -1
  183. package/dist/rule-entry-list.d.ts +3 -13
  184. package/dist/rule-entry-list.js +1 -12
  185. package/dist/run-local.js +10 -7
  186. package/dist/runtime-caps-resolver.d.ts +4 -4
  187. package/dist/runtime-caps-resolver.js +2 -2
  188. package/dist/runtime-governance.d.ts +36 -13
  189. package/dist/runtime-governance.js +7 -2
  190. package/dist/sealed-key.d.ts +4 -4
  191. package/dist/sealed-key.js +3 -2
  192. package/dist/security.d.ts +3 -3
  193. package/dist/service-version.d.ts +1 -1
  194. package/dist/service-version.js +5 -0
  195. package/dist/session-policy-wire.d.ts +47 -49
  196. package/dist/session-policy-wire.js +41 -36
  197. package/dist/session-sync.d.ts +5 -1
  198. package/dist/session-sync.js +2 -1
  199. package/dist/session-titler.d.ts +0 -11
  200. package/dist/session-titler.js +3 -5
  201. package/dist/shared-memory-scope-authorizer.d.ts +4 -4
  202. package/dist/spec-fields.d.ts +4 -4
  203. package/dist/spec-fields.js +1 -4
  204. package/dist/task-a2a.d.ts +1 -1
  205. package/dist/task-settings.d.ts +4 -5
  206. package/dist/task-settings.js +5 -8
  207. package/dist/tool-approval.d.ts +43 -4
  208. package/dist/tool-approval.js +147 -98
  209. package/dist/trace/core-keyset-guard.d.ts +1 -1
  210. package/dist/trace/project.d.ts +3 -3
  211. package/dist/trace/redact.d.ts +2 -23
  212. package/dist/trace/redact.js +1 -6
  213. package/dist/trace/self-minted-wire.d.ts +147 -3
  214. package/dist/trace/self-minted-wire.js +50 -9
  215. package/dist/unattended-permission-mode.d.ts +3 -3
  216. package/dist/unattended-permission-mode.js +2 -2
  217. package/dist/url-refusal.d.ts +35 -0
  218. package/dist/url-refusal.js +15 -0
  219. package/dist/user-authored-text.d.ts +31 -0
  220. package/dist/user-authored-text.js +18 -0
  221. package/dist/{config-center → web-admin-client}/apply-effective.d.ts +13 -70
  222. package/dist/{config-center → web-admin-client}/apply-effective.js +104 -121
  223. package/dist/{config-center → web-admin-client}/apply-ledger.d.ts +2 -2
  224. package/dist/{config-center → web-admin-client}/center-request.d.ts +7 -7
  225. package/dist/{config-center → web-admin-client}/center-request.js +5 -4
  226. package/dist/{config-center → web-admin-client}/effective-keys.d.ts +1 -3
  227. package/dist/{config-center → web-admin-client}/effective-keys.js +0 -9
  228. package/dist/{config-center → web-admin-client}/facade.d.ts +6 -6
  229. package/dist/{config-center → web-admin-client}/facade.js +2 -2
  230. package/dist/{config-center → web-admin-client}/http-client.d.ts +6 -10
  231. package/dist/{config-center → web-admin-client}/http-client.js +15 -15
  232. package/dist/{config-center → web-admin-client}/pull-state.d.ts +1 -1
  233. package/dist/{config-center → web-admin-client}/read-face.js +4 -4
  234. package/dist/web-admin-client/read-warnings.d.ts +137 -0
  235. package/dist/web-admin-client/read-warnings.js +168 -0
  236. package/dist/{config-center → web-admin-client}/restart-signal.d.ts +5 -3
  237. package/dist/{config-center → web-admin-client}/restart-signal.js +3 -4
  238. package/dist/{config-center → web-admin-client}/skills-mcp.d.ts +1 -1
  239. package/dist/{config-center → web-admin-client}/skills-mcp.js +13 -13
  240. package/dist/{config-center → web-admin-client}/stage-limits.d.ts +1 -7
  241. package/dist/{config-center → web-admin-client}/stage-limits.js +6 -7
  242. package/dist/{config-center → web-admin-client}/types.d.ts +19 -47
  243. package/package.json +3 -3
  244. package/skills/find-skills.md +2 -2
  245. package/dist/config-center/read-warnings.d.ts +0 -109
  246. package/dist/config-center/read-warnings.js +0 -110
  247. package/dist/url-shape.d.ts +0 -31
  248. package/dist/url-shape.js +0 -41
  249. /package/dist/{config-center → web-admin-client}/apply-ledger.js +0 -0
  250. /package/dist/{config-center → web-admin-client}/hot-keys-registry.d.ts +0 -0
  251. /package/dist/{config-center → web-admin-client}/hot-keys-registry.js +0 -0
  252. /package/dist/{config-center → web-admin-client}/mcp-revocation.d.ts +0 -0
  253. /package/dist/{config-center → web-admin-client}/mcp-revocation.js +0 -0
  254. /package/dist/{config-center → web-admin-client}/pull-state.js +0 -0
  255. /package/dist/{config-center → web-admin-client}/read-face.d.ts +0 -0
  256. /package/dist/{config-center → web-admin-client}/types.js +0 -0
@@ -1,5 +1,5 @@
1
1
  import { FileSessionRepo, type AcquiredSession, type SessionStore, type SessionRepo, type SessionTreeEntry } from "@sema-agent/core";
2
- import type { SessionSummary } from "./store-contracts.js";
2
+ import type { SessionRunRollupSource, SessionSummary } from "./store-contracts.js";
3
3
  import { type Logger } from "../observability/logger.js";
4
4
  import { type SessionFace } from "../session-incarnation.js";
5
5
  import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
@@ -14,6 +14,7 @@ import { type StagingHandle } from "../session-sync-kernel.js";
14
14
  export declare function createLocalSessionRepo(root: string): FileSessionRepo;
15
15
  export declare class LocalSessionStore implements SessionStore {
16
16
  private readonly repo;
17
+ private readonly runs;
17
18
  /** [ref] 车1:托管留存声明 —— **诚实的 none**。本店的行没有任何按期删除的腿(留存 lane 的三方法只实现在
18
19
  * SQL 聚合店上),所以 locked policy + 本店 ⇒ core 的 `assertRetentionCapability` 拒启,这是契约的
19
20
  * **正确行为**不是缺口(要留存治理先上 DB 后端;设计稿 §1「不做清单」逐字)。缺席也会被读成 none,
@@ -27,17 +28,6 @@ export declare class LocalSessionStore implements SessionStore {
27
28
  * shows the wrong "most recent" — TOC-review #10). In-memory only (no sidecar) when `dataDir` is absent (the
28
29
  * process-local `InMemorySessionRepo` case, where the sessions themselves are lost on restart anyway). */
29
30
  private readonly lastActivity;
30
- /** K-5c (core 1.155 SessionStore.noteTaskRun seam): the latest task run per session, recorded by core's Runner at
31
- * runTask START (so a running/suspended run surfaces too) → projected as `lastRunId` on listSessions. The cloud
32
- * TiDB/PG twins derive it from `task_run`; the local backend has no runs ledger, so this is an in-memory map.
33
- * ⚠️ IN-MEMORY BEST-EFFORT (honest degrade, NOT sidecar-persisted — review Q6): it covers the PRIMARY /resume use
34
- * case (re-attach to a RUNNING run — recorded at the run's start, the process can't have restarted while that run
35
- * is still live), and is LOST on a process restart. That's acceptable: a session whose last run predates a restart
36
- * has only TERMINAL runs (no live tail to re-attach), so `lastRunId` falls back to null and the shell uses its
37
- * no-anchor path — same posture as the other local degrades here (runCount:0, lastStatus:""). Persisting it would
38
- * need a sidecar-format change (the #10 clobber area); deferred until a real local-file-backed need. */
39
- /** K-5c/[ref]/[ref]:per-session 最近一次 run 的 {taskId, runId?}(noteTaskRun 席;runId omit ⇒ 清座)。 */
40
- private readonly lastTaskRun;
41
31
  /** 2c P1d-β — live staged-import handles keyed by stagingId, so a local staging's in-memory buffer is readable via
42
32
  * readStagedEntries (the durable twins re-read the staged session_event rows; local has no such rows). */
43
33
  private readonly stagings;
@@ -81,12 +71,14 @@ export declare class LocalSessionStore implements SessionStore {
81
71
  /** 认领行(`session_owner_claimed`)的出口 —— 一次性迁移 M10 的认领臂专用(窗口见 `plugins/one-time-migrations.ts` 登记表)。 */
82
72
  private readonly log;
83
73
  /** @param repo the core SessionRepo this store projects into the §0.5 SessionStore shape.
74
+ * @param runs the run store whose per-session projection is the ONLY source of a row's `runCount` / `lastStatus` / `lastRunId` /
75
+ * `lastTaskRunId` (LocalBackend passes its `FileRunStore`). Required — a run-less local session store does not exist.
84
76
  * @param dataDir the SAME data dir the file-backed repo persists under (LocalBackend passes `fileBackend.root`).
85
77
  * When given, lastActivity is persisted to `<dataDir>/session-last-activity.json` so it survives a
86
78
  * restart (the file-backed deployment); omit it for the in-memory deployment (nothing survives).
87
79
  * @param opts.log the `session_owner_claimed` line's sink (LocalBackend passes a logger at the deployment's LOG_LEVEL;
88
80
  * absent ⇒ an info-level JSON-line logger — a claim is never silent). */
89
- constructor(repo: SessionRepo, dataDir?: string, opts?: {
81
+ constructor(repo: SessionRepo, runs: SessionRunRollupSource, dataDir?: string, opts?: {
90
82
  log?: Pick<Logger, "info">;
91
83
  });
92
84
  /**
@@ -211,9 +203,11 @@ export declare class LocalSessionStore implements SessionStore {
211
203
  * - `firstActivityAt` — the session's `createdAt` (repo metadata).
212
204
  * - `lastActivityAt` — the touch marker (falls back to `createdAt`); the keyset sort key.
213
205
  * - `objectivePreview`— the last USER-text entry, synthesized from the session tree (no run ledger).
214
- * - `runCount`/`lastStatus` — DEGRADED (0 / ""): the session abstraction has no runs ledger; the TiDB twin
215
- * LEFT-JOINs `task_run` for these, which the local backend (MemoryRunStore, separate
216
- * ledger) intentionally does NOT correlate here (variant-2 is session-abstraction-only).
206
+ * - `runCount`/`lastStatus`/`lastRunId`/`lastTaskRunId` — S-1371:read from the run store handed in (`runs`; the TiDB
207
+ * twin LEFT-JOINs `task_run` for the same values, the run store's own `listSessions` is that
208
+ * join's same-semantics projection — latest run by `created_at DESC, task_id DESC`, count of all,
209
+ * `lastTaskRunId` = that run's `result.runId`). One call per list (not per row); a session with no
210
+ * run row ⇒ `0 / "" / null` (TiDB's null→"" mapping); a run-store read failure throws (never `0 / ""`).
217
211
  *
218
212
  * Keyset matches the contract: STRICTLY older than the cursor, ties broken by id DESC; `limit` is honored after
219
213
  * the in-memory sort. Cheap (a local single-user store holds few sessions). Errors reading one session's entries
@@ -230,17 +224,16 @@ export declare class LocalSessionStore implements SessionStore {
230
224
  limit: number;
231
225
  q?: string;
232
226
  }): Promise<SessionSummary[]>;
233
- /** K-5c (core 1.155 SessionStore seam): core's Runner calls this at runTask START with the run's taskId, so the
234
- * latest run surfaces as `lastRunId` on listSessions even while it is still running/suspended (the local backend
235
- * has no runs ledger to derive it from, unlike the TiDB/PG twins). In-memory, last-write-wins per session.
236
- * [ref] → [ref]([ref] 兑现;上一版「有意缓议」注即本批债锚,兑现随批撤):第三参 engine runId
237
- * 自本批起**消费** —— map 值 {taskId, runId?} + 行投影 `lastTaskRunId` + **两座同动清座**(omit ⇒
238
- * 清 runId 座,对齐 core TtlSessionStore.noteTaskRun 的 omit⇒clear 契约,core session.d.ts
239
- * 亲读:旧 run 的 runId 挂在新 task 旁 = 把上一条 run 的身份错标给新 run)。存储与行投影同批落
240
- * (codex 5.65 R1-[medium]「存储先行、投影另批」之驳仍成立:本店 runId 唯一读点即行投影)。
241
- * wire 半场:HTTP 面零投影透传(sessions-list.ts,additive 键随行上 wire);sema-sdk spec
242
- * `SessionSummary` 闭集候 SDK 开键([ref]/.82 班车)——SDK 落键即端到端闭合。 */
243
- noteTaskRun(sessionId: string, taskId: string, runId?: string): void;
227
+ /** S-1371:run 店里每个会话的 run 投影(按 sessionId 索引)。
228
+ * **一次读**:run 店的 `listSessions` 本来就按会话聚合全账(无会话过滤口),这里取全量再按会话仓的 id 查表 —— 比逐会话 N 次读便宜,
229
+ * 也不在会话店里另存一份 run 账。读失败 ⇒ 抛(不吞成 `0 / ""`:那会把「读不到」说成「没跑过」)。 */
230
+ private runRollups;
231
+ /**
232
+ * 一行里 run 身份与计数的**唯一出处**(S-1371,会话行四位一源):四位全读 {@link runRollups} 的投影(与 SQL 双生的 `task_run` rn=1 JOIN 同口径);
233
+ * 该会话没有 run 行 ⇒ `0` / `""` / `null`、`lastTaskRunId` 缺席(TiDB 的 `COALESCE(…,0)` / null→"" 映射)。在途的最新一条:`lastRunId` 在场(run 行建出来就有)、
234
+ * `lastTaskRunId` 缺席(`result` 在终局与 park 时才写)。
235
+ */
236
+ private runProjectionOf;
244
237
  /** Synthesize a session's objective preview = the most-recent USER text message (the objective the run last saw).
245
238
  * Reads the session's entries through the repo; any read error degrades to null (never fails the list). */
246
239
  private previewOf;
@@ -12,9 +12,11 @@ import { carriedIncarnation, mintSessionIncarnation, sessionFaceFromRow } from "
12
12
  import { decodeInstructionSource, encodeInstructionSource, sameInstructionSourceBaseline } from "../instruction-source-notice.js";
13
13
  import { noteReplayInSessionOpenScope, openSessionForItself } from "../session-records-skipped-notice.js";
14
14
  import { createCorruptReadFailOpenSeat } from "../observability/corrupt-read-seat.js";
15
+ import { buildObjectivePreview } from "../user-authored-text.js";
15
16
  import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, } from "../session-sync-kernel.js";
16
17
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
17
18
  const isNotFound = (e) => e instanceof SessionError && e.code === "not_found";
19
+ const SESSION_OBJECTIVE_PREVIEW_CHARS = 200;
18
20
  function userText(entry) {
19
21
  if (entry.type !== "message")
20
22
  return null;
@@ -23,14 +25,12 @@ function userText(entry) {
23
25
  return null;
24
26
  const c = msg.content;
25
27
  if (typeof c === "string")
26
- return c.trim() || null;
28
+ return c;
27
29
  if (Array.isArray(c)) {
28
- const text = c
30
+ return c
29
31
  .filter((p) => !!p && typeof p === "object" && p.type === "text")
30
32
  .map((p) => p.text)
31
- .join(" ")
32
- .trim();
33
- return text || null;
33
+ .join(" ");
34
34
  }
35
35
  return null;
36
36
  }
@@ -143,10 +143,10 @@ export function createLocalSessionRepo(root) {
143
143
  }
144
144
  export class LocalSessionStore {
145
145
  repo;
146
+ runs;
146
147
  retention = UNMANAGED_RETENTION;
147
148
  pending = new Map();
148
149
  lastActivity = new Map();
149
- lastTaskRun = new Map();
150
150
  stagings = new Map();
151
151
  loaded;
152
152
  flushTimer;
@@ -159,8 +159,9 @@ export class LocalSessionStore {
159
159
  instructionSources;
160
160
  meta;
161
161
  log;
162
- constructor(repo, dataDir, opts) {
162
+ constructor(repo, runs, dataDir, opts) {
163
163
  this.repo = repo;
164
+ this.runs = runs;
164
165
  this.sidecarPath = dataDir ? join(dataDir, "session-last-activity.json") : undefined;
165
166
  this.log = opts?.log ?? createLogger();
166
167
  const sidecar = (file, which, read, strict = false) => new SidecarMap(dataDir ? resolve(join(dataDir, file)) : undefined, `${file} (the local ${which} sidecar)`, read, (path, e) => this.refuseSidecar(path, `LocalSessionStore ${which} sidecar`, e, which), (e) => this.warnSidecar("write", e, which), strict);
@@ -403,6 +404,7 @@ export class LocalSessionStore {
403
404
  await this.titles.ensureLoaded();
404
405
  await this.meta.ensureLoaded();
405
406
  const metas = await this.repo.list();
407
+ const rollups = await this.runRollups();
406
408
  const all = [];
407
409
  for (const meta of metas) {
408
410
  const cell = this.meta.state.entries.get(meta.id);
@@ -411,17 +413,13 @@ export class LocalSessionStore {
411
413
  continue;
412
414
  const firstActivityAt = meta.createdAt || new Date(0).toISOString();
413
415
  const lastActivityAt = this.lastActivity.get(meta.id) ?? firstActivityAt;
414
- const lastRun = this.lastTaskRun.get(meta.id);
415
416
  all.push({
416
417
  sessionId: meta.id,
417
418
  owner,
418
419
  lastActivityAt,
419
420
  firstActivityAt,
420
- runCount: 0,
421
421
  objectivePreview: await this.previewOf(meta),
422
- lastStatus: "",
423
- lastRunId: lastRun?.taskId ?? null,
424
- ...(lastRun?.runId !== undefined ? { lastTaskRunId: lastRun.runId } : {}),
422
+ ...this.runProjectionOf(meta.id, rollups),
425
423
  title: this.titles.state.entries.get(meta.id) ?? null,
426
424
  });
427
425
  }
@@ -436,8 +434,12 @@ export class LocalSessionStore {
436
434
  : filtered;
437
435
  return after.slice(0, opts.limit);
438
436
  }
439
- noteTaskRun(sessionId, taskId, runId) {
440
- this.lastTaskRun.set(sessionId, { taskId, ...(runId !== undefined ? { runId } : {}) });
437
+ async runRollups() {
438
+ const rows = await this.runs.listSessions({ limit: Number.MAX_SAFE_INTEGER });
439
+ return new Map(rows.map((r) => [r.sessionId, { runCount: r.runCount, lastStatus: r.lastStatus, lastRunId: r.lastRunId, ...(r.lastTaskRunId !== undefined ? { lastTaskRunId: r.lastTaskRunId } : {}) }]));
440
+ }
441
+ runProjectionOf(sessionId, rollups) {
442
+ return rollups.get(sessionId) ?? { runCount: 0, lastStatus: "", lastRunId: null };
441
443
  }
442
444
  async previewOf(meta) {
443
445
  try {
@@ -445,8 +447,9 @@ export class LocalSessionStore {
445
447
  const entries = await session.getEntries();
446
448
  for (let i = entries.length - 1; i >= 0; i--) {
447
449
  const text = userText(entries[i]);
448
- if (text)
449
- return text.slice(0, 200);
450
+ const preview = text === null ? "" : buildObjectivePreview(text, SESSION_OBJECTIVE_PREVIEW_CHARS);
451
+ if (preview !== "")
452
+ return preview;
450
453
  }
451
454
  return null;
452
455
  }
@@ -541,7 +544,6 @@ export class LocalSessionStore {
541
544
  }
542
545
  await this.dropMeta(sessionId);
543
546
  this.lastActivity.delete(sessionId);
544
- this.lastTaskRun.delete(sessionId);
545
547
  for (const map of [this.titles, this.instructionSources]) {
546
548
  await map.ensureLoaded();
547
549
  if (map.state.entries.delete(sessionId))
@@ -224,9 +224,15 @@ export class MemoryRunStore {
224
224
  const sorted = [...arr].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime() || (a.taskId < b.taskId ? 1 : a.taskId > b.taskId ? -1 : 0));
225
225
  const latest = sorted[0];
226
226
  const latestRunId = latest.result?.runId;
227
- const times = arr.map((r) => r.createdAt.getTime());
228
- const last = Math.max(...times);
229
- const first = Math.min(...times);
227
+ let last = -Infinity;
228
+ let first = Infinity;
229
+ for (const r of arr) {
230
+ const t = r.createdAt.getTime();
231
+ if (t > last)
232
+ last = t;
233
+ if (t < first)
234
+ first = t;
235
+ }
230
236
  summaries.push({
231
237
  sessionId,
232
238
  owner: latest.owner,
@@ -162,6 +162,57 @@ export const ONE_TIME_MIGRATIONS = [
162
162
  "`AUTONOMY=plan` 的部署上按体里的原词重放(运维的 plan 不再被按段重钳)—— 升级前把这些停驻的 run 决掉 / 取消,或升级窗内设 `AUTONOMY=read-only`",
163
163
  reportKeys: [],
164
164
  },
165
+ {
166
+ id: "M13",
167
+ introducedIn: "7.123.0",
168
+ releasedOn: "2026-10-10",
169
+ removableAfter: "2026-11-09",
170
+ removal: "删 `config-lkg.ts` 的 formatVersion 2 读回迁移:`migrateV2Lkg` / `migrateDroppedIdentityRow` / `RETIRED_CATALOG_COORDINATE` 与其上的窗注;`loadLkg` 的 2 臂" +
171
+ "(版本判据里的 `2` 与 `formatVersion === 2 ? migrateV2Lkg(…)` 那一支 —— 2 回到「unknown formatVersion」拒)、返回型的 `migrated` 位与 JSDoc 里说它的那一行、" +
172
+ "文件头注「2 读回时一次性迁移」那一句;`boot/web-admin.ts` 读 `read.migrated` 打 `config_lkg_migrated` 的那一行(formatVersion 1 迁移的同名事件那一行留);" +
173
+ "`migrateV1Lkg` 里不带 v1 的 etag 那一句旁注对 `migrateV2Lkg` 的引用(不带 etag 的规则本身留 —— v1 载荷同样没有完整的被丢条目记录);契约 §A.1d " +
174
+ "`droppedEntries` 一节「升级窗(如实)」那一条与 §A.1d `[config.model_refused_name_minted]` 那一条里「LKG 载体 `droppedIdentities` 读回时一次性迁移」那半句;手写 formatVersion 2 信封的测试夹具" +
175
+ "(`test/config-lkg.test.ts`、`test/config-hot/batch1-version-ordinal.test.ts` 改写成 3;`test/models-dropped-entries-s1295.test.ts` 的旧 LKG 迁移格改钉 2 读回不可用)",
176
+ afterRemoval: "既有响亮臂 `config_lkg_unusable`(`loadLkg` 对 2 答「unknown formatVersion 2」):2 读回 ⇒ 不可用,中心可达照拉(整拉,读腿铸出完整载体,新 LKG 按 3 覆盖)、" +
177
+ "断着则 env 兜底,下一份干净候选重写 —— 与回滚到 7.122.0 读 3 同一种处置",
178
+ reportKeys: [],
179
+ },
180
+ {
181
+ id: "M14",
182
+ introducedIn: "7.99.0",
183
+ releasedOn: "2026-09-24",
184
+ removableAfter: "2026-10-24",
185
+ removal: "删 `config-lkg.ts` 的 formatVersion 1 迁移:`legacyV1LkgPath` / `migrateV1Lkg` 与其上的窗注(`loadLkg` 的 1 臂今天已是拒句,不动);" +
186
+ "`boot/web-admin.ts` `bootFromLkg` 里调用 `migrateV1Lkg` 的那一段(含它打 `config_lkg_migrated` / 推不出身份时响亮拒的那两行);" +
187
+ "`migrateV1Lkg` 的测试格(`test/config-lkg.test.ts` v1 迁移组)",
188
+ afterRemoval: "既有响亮臂 `config_lkg_unusable`(`loadLkg` 对 1 答「pre-identity LKG (formatVersion 1) — not read back …」):旧默认路径上的 v1 文件不再被迁进箱、" +
189
+ "原样留在盘上;中心可达照拉(整拉,新 LKG 按现行箱名落盘)、断着则 env 兜底",
190
+ reportKeys: [],
191
+ },
192
+ {
193
+ id: "M15",
194
+ introducedIn: "7.124.0",
195
+ releasedOn: "2026-10-11",
196
+ removableAfter: "2026-11-10",
197
+ removal: "删 `config-lkg.ts` 的载荷迁移:`migrateLkgPayload` 与其上的窗注、`loadLkg` 里调它的那一段(载荷照旧过 `lkgPayloadProblem`)、`LkgReadMigration.receipts`" +
198
+ "(信封 2 → 3 的 M13 若已先删,`LkgReadMigration` 与 `migrated` 位一并删)、对 `@sema-agent/settings-schema/one-time-migrations` 的 import;`loadLkg` 的版本判据收成" +
199
+ "只认 4(3 回到「unknown formatVersion」拒);`boot/web-admin.ts` 读回后按 4 写回的那一段;文件头注 formatVersion 4 一段里「每次读回都过迁移」那一句;" +
200
+ "`test/ss11-stored-migrations.test.ts` 的 M15 组(4 原样读回那一格留)",
201
+ afterRemoval: "既有响亮臂 `config_lkg_unusable`(`loadLkg` 对 3 答「unknown formatVersion 3」):旧载荷的 LKG 读回 ⇒ 不可用,中心可达照拉(整拉,新 LKG 按 4 覆盖)、" +
202
+ "断着则 env 兜底 —— 与回滚到 7.123.0 读 4 同一种处置",
203
+ reportKeys: [],
204
+ },
205
+ {
206
+ id: "M16",
207
+ introducedIn: "7.124.0",
208
+ releasedOn: "2026-10-11",
209
+ removableAfter: "2026-11-10",
210
+ removal: "删 `config-d-migration.ts` 整只文件与两个入口的调用(`boot/web-admin.ts` 建本地配置源之后、`run-local.ts` 建配置源之后各一行 + import);" +
211
+ "`test/ss11-stored-migrations.test.ts` 的 M16 组;部署文档里「起服自动迁 config.d」那一句(运维仍可手跑契约命令行,直到契约删族)",
212
+ afterRemoval: "契约读腿的墓碑(永久):config.d 里还带旧形的域文档 —— `runtime.json` 带事实 ⇒ `RetiredDomainError` 整份读不出;`atModelAllowlist` / `models.default` ⇒ " +
213
+ "models 域降级、rosters 整份读不出 —— 拒句点名迁移 id;出路是运维手跑契约命令行(契约删族之后只剩手改)",
214
+ reportKeys: [],
215
+ },
165
216
  ];
166
217
  export function migrationWindowOpen(window, today) {
167
218
  return window.removableAfter === null || today <= window.removableAfter;
@@ -1,6 +1,6 @@
1
1
  import { SchedulerError } from "@sema-agent/core";
2
2
  import type { SchedulerCapability, ScheduledIntent, SchedulerContext, ScheduledTaskId, ScheduledTaskSummary } from "@sema-agent/core";
3
- import type { SchedulerRecord } from "@sema-agent/settings-schema/scheduler-store";
3
+ import type { SchedulerParseWarning, SchedulerRecord } from "@sema-agent/settings-schema/scheduler-store";
4
4
  export type { SchedulerRecord };
5
5
  /** core `Result<T,E>` shape (harness/types) — `{ok:true,value} | {ok:false,error}`. */
6
6
  type Result<T, E> = {
@@ -31,23 +31,42 @@ export declare function validateIntent(intent: ScheduledIntent, caps: Caps, nowM
31
31
  export declare function defaultSchedulerStorePath(config: {
32
32
  readonly agentConfigDir?: string;
33
33
  }): string;
34
+ /** 调度店读写的日志口(logger 的子集)。 */
35
+ export interface SchedulerStoreLog {
36
+ warn(msg: string, meta?: Record<string, unknown>): void;
37
+ info(msg: string, meta?: Record<string, unknown>): void;
38
+ }
39
+ /**
40
+ * 一次读的告警 → 日志(settings-schema 11 的调度店读口恒交回告警,没有静默模式)。告警是闭集,穷举;**不带记录原文**(`malformed-record`
41
+ * 的 `record` 可能是任意内容 —— 只报下标):
42
+ * · `unreadable-file` ⇒ warn(整份文件不是 JSON 数组:读成零条,写者一律不写 —— 见 `mutateSchedulerStore`);
43
+ * · `malformed-record` ⇒ warn(这一条读不懂、被跳过;下一个写者落盘时它就没了);
44
+ * · `stale-session-record` ⇒ info(会话链上过期 ≥ 7 天的记录,读时丢、下一个写者落盘);
45
+ * · `one-time-migration` ⇒ info(SS-M3 的读时改写收据:durable 记录换上新键,下一个写者落盘)。
46
+ */
47
+ export declare function logSchedulerWarnings(log: SchedulerStoreLog, path: string, warnings: readonly SchedulerParseWarning[]): void;
34
48
  /**
35
- * File-backed SchedulerCapability (durable across daemon restarts). Store = a single JSON file (atomic write via
36
- * the registry-core /node binding). The resident shell daemon reads the same store to fire intents. `now`
37
- * injectable for tests.
49
+ * File-backed SchedulerCapability (durable across daemon restarts). Store = a single JSON file shared with the resident shell
50
+ * daemon. Every WRITE goes through the contract's ONE mutation primitive `mutateSchedulerStore` (optimistic witness + atomic
51
+ * write; a naked load → save is the lost-update race against the daemon's fire / reap writers). A store file that is not a JSON
52
+ * array is `unreadable_file`: nothing is written over it (the caller gets an `io` error). Reads (`list`) use the contract's read
53
+ * port; every read's warnings go to the log ({@link logSchedulerWarnings}). `now` injectable for tests.
38
54
  */
39
55
  export declare class FileSchedulerBackend implements SchedulerCapability {
40
56
  readonly schedulerCapabilities: Caps;
41
57
  private readonly storePath;
42
58
  private readonly now;
43
- /** `storePath` 必填:路径是装配层从配置定的事实(`SCHEDULER_STORE_PATH` ?? {@link defaultSchedulerStorePath}),本类不自己找缺省。 */
59
+ private readonly log;
60
+ /** `storePath` 必填:路径是装配层从配置定的事实(`SCHEDULER_STORE_PATH` ?? {@link defaultSchedulerStorePath}),本类不自己找缺省。
61
+ * `log` 必填:店的每次读都交回告警(读不懂的文件 / 被跳过的记录 / 读时改写),没有不出声的读法。 */
44
62
  constructor(opts: {
45
63
  storePath: string;
64
+ log: SchedulerStoreLog;
46
65
  caps?: Partial<Caps>;
47
66
  now?: () => number;
48
67
  });
49
- private load;
50
- private save;
68
+ /** 一次读改写(契约的唯一写原语)。`fn` 必须纯、可重放(并发写者落地时整段重放);返回 `null` = 不写。拿不到结果的三形各成一个 `io` 错。 */
69
+ private mutate;
51
70
  schedule(intent: ScheduledIntent, ctx: SchedulerContext): Promise<Result<{
52
71
  id: ScheduledTaskId;
53
72
  }, SchedulerError>>;
@@ -1,7 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import { randomBytes, createHash } from "node:crypto";
3
3
  import { SchedulerError, isValidCronExpr } from "@sema-agent/core";
4
- import { loadSchedulerStore, saveSchedulerStore } from "@sema-agent/settings-schema/node";
4
+ import { loadSchedulerStore, mutateSchedulerStore } from "@sema-agent/settings-schema/node";
5
5
  import { configRootOf } from "../sema-config-dir.js";
6
6
  const ok = (value) => ({ ok: true, value });
7
7
  const err = (error) => ({ ok: false, error });
@@ -62,20 +62,67 @@ function computeNextRunMs(when, nowMs) {
62
62
  export function defaultSchedulerStorePath(config) {
63
63
  return join(configRootOf(config), "scheduled_tasks.json");
64
64
  }
65
+ export function logSchedulerWarnings(log, path, warnings) {
66
+ for (const w of warnings) {
67
+ switch (w.kind) {
68
+ case "unreadable-file":
69
+ log.warn("scheduler_store_unreadable", { path, why: w.why, note: "the scheduled-task store is not a JSON array — it reads as zero records and no writer overwrites it; fix or remove the file" });
70
+ break;
71
+ case "malformed-record":
72
+ log.warn("scheduler_store_record_malformed", { path, index: w.index, note: "this record does not read and is skipped (the next write drops it)" });
73
+ break;
74
+ case "stale-session-record":
75
+ log.info("scheduler_store_record_stale", { path, index: w.index, id: w.id, note: "a session-chained record whose next run is ≥ 7 days past — dropped on read (the next write persists the drop)" });
76
+ break;
77
+ case "one-time-migration":
78
+ log.info("scheduler_store_record_migrated", { path, index: w.index, migration: w.receipt.migration, appliedAs: w.receipt.appliedAs, note: "settings-schema one-time migration rewrote this record on read (the next write persists it)" });
79
+ break;
80
+ default: {
81
+ const unhandled = w;
82
+ throw new Error(`scheduler store: unknown read warning ${JSON.stringify(unhandled)}`);
83
+ }
84
+ }
85
+ }
86
+ }
65
87
  export class FileSchedulerBackend {
66
88
  schedulerCapabilities;
67
89
  storePath;
68
90
  now;
91
+ log;
69
92
  constructor(opts) {
70
93
  this.schedulerCapabilities = { ...DEFAULT_SCHEDULER_CAPS, ...(opts.caps ?? {}) };
71
94
  this.storePath = opts.storePath;
95
+ this.log = opts.log;
72
96
  this.now = opts.now ?? (() => Date.now());
73
97
  }
74
- load() {
75
- return loadSchedulerStore(this.storePath);
76
- }
77
- save(records) {
78
- saveSchedulerStore(this.storePath, records);
98
+ mutate(fn, what) {
99
+ let result;
100
+ let produced = false;
101
+ let mutation;
102
+ try {
103
+ mutation = mutateSchedulerStore(this.storePath, (records) => {
104
+ const r = fn(records);
105
+ result = r.result;
106
+ produced = true;
107
+ return r.next;
108
+ }, { nowMs: this.now() });
109
+ }
110
+ catch (e) {
111
+ return err(new SchedulerError("io", `failed to ${what}`, e));
112
+ }
113
+ logSchedulerWarnings(this.log, this.storePath, mutation.warnings);
114
+ if (mutation.ok || mutation.reason === "aborted")
115
+ return produced ? ok(result) : err(new SchedulerError("io", `failed to ${what}`));
116
+ switch (mutation.reason) {
117
+ case "unreadable_file":
118
+ return err(new SchedulerError("io", `failed to ${what}: the scheduled-task store file is not a JSON array — nothing was written over it`));
119
+ case "conflict_exhausted":
120
+ return err(new SchedulerError("io", `failed to ${what}: concurrent writers kept changing the store — nothing was written`));
121
+ default: {
122
+ const unhandled = mutation;
123
+ throw new Error(`scheduler store: unknown mutation outcome ${JSON.stringify(unhandled)}`);
124
+ }
125
+ }
79
126
  }
80
127
  async schedule(intent, ctx) {
81
128
  const nowMs = this.now();
@@ -88,60 +135,56 @@ export class FileSchedulerBackend {
88
135
  if (intent.lifetime === "session" && this.schedulerCapabilities.supportsSessionLifetime === false) {
89
136
  return err(new SchedulerError("invalid_schedule", 'lifetime:"session" is not supported on this deployment (no resident daemon reaps session-lifetime records — the row would be an orphan)'));
90
137
  }
91
- const records = this.load();
92
138
  const key = dedupKey(ctx.scope, intent, ctx.sessionId);
93
- const existingIdx = records.findIndex((r) => {
94
- const rr = r;
95
- return r.scope === ctx.scope && dedupKey(r.scope, { when: r.when, label: r.label, lifetime: rr.lifetime }, r.sessionId) === key;
96
- });
97
- const scopeCount = records.filter((r) => r.scope === ctx.scope).length;
98
- if (existingIdx < 0 && scopeCount >= this.schedulerCapabilities.maxScheduledPerScope) {
99
- return err(new SchedulerError("limit_exceeded", `scope at cap (${this.schedulerCapabilities.maxScheduledPerScope})`));
100
- }
101
- const id = (existingIdx >= 0 ? records[existingIdx].id : randomBytes(12).toString("base64url"));
102
- const rec = {
103
- id,
104
- scope: ctx.scope,
105
- ...(ctx.principal ? { principal: ctx.principal } : {}),
106
- ...(ctx.sessionId ? { sessionId: ctx.sessionId } : {}),
107
- prompt: intent.prompt,
108
- when: intent.when,
109
- ...(intent.label ? { label: intent.label } : {}),
110
- ...(intent.mode ? { mode: intent.mode } : {}),
111
- ...(intent.lifetime ? { lifetime: intent.lifetime } : {}),
112
- ...(intent.recurring !== undefined ? { recurring: intent.recurring } : {}),
113
- createdMs: nowMs,
114
- ...(computeNextRunMs(intent.when, nowMs) !== undefined ? { nextRunMs: computeNextRunMs(intent.when, nowMs) } : {}),
115
- };
116
- if (existingIdx >= 0)
117
- records[existingIdx] = rec;
118
- else
119
- records.push(rec);
120
- try {
121
- this.save(records);
122
- }
123
- catch (e) {
124
- return err(new SchedulerError("io", "failed to persist scheduled task", e));
125
- }
126
- return ok({ id });
139
+ const decided = this.mutate((records) => {
140
+ const existingIdx = records.findIndex((r) => {
141
+ const rr = r;
142
+ return r.scope === ctx.scope && dedupKey(r.scope, { when: r.when, label: r.label, lifetime: rr.lifetime }, r.sessionId) === key;
143
+ });
144
+ const scopeCount = records.filter((r) => r.scope === ctx.scope).length;
145
+ if (existingIdx < 0 && scopeCount >= this.schedulerCapabilities.maxScheduledPerScope) {
146
+ return { next: null, result: err(new SchedulerError("limit_exceeded", `scope at cap (${this.schedulerCapabilities.maxScheduledPerScope})`)) };
147
+ }
148
+ const id = (existingIdx >= 0 ? records[existingIdx].id : randomBytes(12).toString("base64url"));
149
+ const rec = {
150
+ id,
151
+ scope: ctx.scope,
152
+ ...(ctx.principal ? { principal: ctx.principal } : {}),
153
+ ...(ctx.sessionId ? { sessionId: ctx.sessionId } : {}),
154
+ prompt: intent.prompt,
155
+ when: intent.when,
156
+ ...(intent.label ? { label: intent.label } : {}),
157
+ ...(intent.mode ? { mode: intent.mode } : {}),
158
+ ...(intent.lifetime ? { lifetime: intent.lifetime } : {}),
159
+ ...(intent.recurring !== undefined ? { recurring: intent.recurring } : {}),
160
+ createdMs: nowMs,
161
+ ...(computeNextRunMs(intent.when, nowMs) !== undefined ? { nextRunMs: computeNextRunMs(intent.when, nowMs) } : {}),
162
+ };
163
+ const next = existingIdx >= 0 ? records.map((r, i) => (i === existingIdx ? rec : r)) : [...records, rec];
164
+ return { next, result: ok({ id }) };
165
+ }, "persist scheduled task");
166
+ return decided.ok ? decided.value : decided;
127
167
  }
128
168
  async cancel(id, ctx) {
129
- const records = this.load();
130
- const idx = records.findIndex((r) => r.id === id);
131
- if (idx < 0 || records[idx].scope !== ctx.scope) {
132
- return err(new SchedulerError("not_found", `no scheduled task ${id} in your scope (wrong id, already cancelled, or not yours)`));
133
- }
134
- records.splice(idx, 1);
169
+ const decided = this.mutate((records) => {
170
+ const idx = records.findIndex((r) => r.id === id);
171
+ if (idx < 0 || records[idx].scope !== ctx.scope) {
172
+ return { next: null, result: err(new SchedulerError("not_found", `no scheduled task ${id} in your scope (wrong id, already cancelled, or not yours)`)) };
173
+ }
174
+ return { next: records.filter((_, i) => i !== idx), result: ok(undefined) };
175
+ }, "persist cancellation");
176
+ return decided.ok ? decided.value : decided;
177
+ }
178
+ async list(ctx) {
179
+ let read;
135
180
  try {
136
- this.save(records);
181
+ read = loadSchedulerStore(this.storePath, { nowMs: this.now() });
137
182
  }
138
183
  catch (e) {
139
- return err(new SchedulerError("io", "failed to persist cancellation", e));
184
+ return err(new SchedulerError("io", "failed to read the scheduled-task store", e));
140
185
  }
141
- return ok(undefined);
142
- }
143
- async list(ctx) {
144
- const summaries = this.load()
186
+ logSchedulerWarnings(this.log, this.storePath, read.warnings);
187
+ const summaries = read.records
145
188
  .filter((r) => r.scope === ctx.scope)
146
189
  .map((r) => ({
147
190
  id: r.id,
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { assertSessionRuleNamesCurrent, loosenReasons, SessionPolicyError, normalizeRules, stripRev } from "@sema-agent/core";
3
- import { assertSessionRuleNamesReachable, readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../session-policy-wire.js";
3
+ import { 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";
@@ -105,7 +105,6 @@ export class SqlSessionPolicyStore {
105
105
  }
106
106
  const clean = normalizeRules(stripRev(rules));
107
107
  assertSessionRuleNamesCurrent(clean);
108
- assertSessionRuleNamesReachable(clean);
109
108
  if (prior && opts?.operator !== true) {
110
109
  const loosens = loosenReasons(prior, clean);
111
110
  if (loosens.length > 0) {
@@ -242,12 +242,12 @@ class LocalBackend {
242
242
  throw new Error(`DB_BACKEND=local cannot open data dir ${root}: ${e instanceof Error ? e.message : String(e)}` +
243
243
  (tail === undefined ? "" : ` ${tail}`));
244
244
  }
245
- this.sessionStore = new LocalSessionStore(createLocalSessionRepo(this.fileBackend.root), this.fileBackend.root, { log: createLogger(config.logLevel) });
246
245
  const corruptReadWarn = (msg, fields) => this.discloseWarn(msg, fields);
247
246
  this.runStore = new FileRunStore(this.fileBackend.root, {
248
247
  error: (msg, fields) => void (this.onError ?? this.onWarn)?.(msg, fields),
249
248
  strictHydrate: config.runStoreStrictHydrate,
250
249
  });
250
+ this.sessionStore = new LocalSessionStore(createLocalSessionRepo(this.fileBackend.root), this.runStore, this.fileBackend.root, { log: createLogger(config.logLevel) });
251
251
  this.resumeAnchorStore = new FileResumeAnchorStore(this.fileBackend.root);
252
252
  this.approvalExemptionStore = new FileApprovalExemptionStore(this.fileBackend.root, { warn: corruptReadWarn });
253
253
  this.approvalNonceStore = new FileApprovalNonceStore(this.fileBackend.root, { warn: corruptReadWarn });
@@ -216,7 +216,7 @@ export interface RunRecord {
216
216
  * 被取消了没有」看终局码;判「一条还在跑的 run 上有没有人按过取消」才看这一位。 */
217
217
  cancelRequested: boolean;
218
218
  /** First ~120 chars of the objective (secret-redacted at write) — the list's "what is this task" glance.
219
- * null for rows predating the column. */
219
+ * 一个事实位:`""` = 建过预览、剥完没有用户正文(只有注入块 / 空 objective);null = 没记录(更早版本建的行 / 列前的行)。 */
220
220
  objectivePreview: string | null;
221
221
  createdAt: string;
222
222
  updatedAt: string;
@@ -337,9 +337,8 @@ export declare function isParkedRunStatus(status: RunRecord["status"]): boolean;
337
337
  * 两个不同的忙闲。
338
338
  *
339
339
  * 🔴 **未知词 / 无读数 ⇒ 词缺席(`undefined`),不是 `idle`**(诚实缺席,不是静默兜底):
340
- * · `lastStatus === ""` 有两个真来源 ——(a)SQL 双生:这条会话**还没有任何 run**;(b)local 文件车道:
341
- * 该 lister **结构上说不出** run 状态(`local-session-store.ts:465` 自陈的诚实降级 `lastStatus: ""`)。
342
- * 两者都不是「我知道它闲着」,所以都不许铸出 `idle` 这个断言;
340
+ * · `lastStatus === ""` 的真来源:这条会话**还没有任何 run**(SQL 双生与 local 会话店同;local 会话店的 run 店是必填位,没有「说不出 run 状态」的装配形)。
341
+ * 它不是「我知道它闲着」,所以不许铸出 `idle` 这个断言;
343
342
  * · 读不懂的词(滚动升级里新副本写入的新状态词经裸 cast 读到本进程)同判 —— 与
344
343
  * {@link isTerminalRunStatus} 族的保守方向一致。
345
344
  * · **缺席怎么渲染是 core 的辖域,本仓一个字都不复述**(S-514-JU:此处原写着「core 对缺席渲染成 `idle`」
@@ -407,6 +406,23 @@ export interface SessionSummary {
407
406
  * titled OR the lister face has no session_meta (the legacy task_run aggregate / memory twins — honest degrade). */
408
407
  title: string | null;
409
408
  }
409
+ /**
410
+ * S-1371 —— 会话**枚举面**读 run 账里「每会话的 run 投影」的窄读面:`runCount` / `lastStatus` / `lastRunId` / `lastTaskRunId` 四位,
411
+ * 同一次读、同一条「最新一条」的口径。
412
+ *
413
+ * 为什么是 run 店自己的 `listSessions`、不另造一只读口:这四位在 SQL 双生里就是 `task_run` 的 `rn=1` JOIN(计数、最新一条的状态与 task_id、
414
+ * 最新一条 `result.runId`),在文件 / 内存 run 店里就是 `listSessions` 的同口径聚合(`created_at DESC, task_id DESC` 取最新一条、数全部)——
415
+ * 它们本来就是**同一个问题的同一份答案**。local 会话店(会话身份来自会话仓、不是 run 账)读这一只,一行里就不会出现两个来源各答一遍
416
+ * 「最新一条是谁」(此前 `runCount` / `lastStatus` 读 run 店、`lastRunId` / `lastTaskRunId` 读进程内座:重启后同一行「有终态词、`lastRunId` 为 null」);
417
+ * 不另造的另一半理由:再造一只 `listBySession` 就要在三个孪生里各落一遍并重新证一遍同口径。
418
+ *
419
+ * 只收四位:`Pick` 把读面钉死在它真用的键上(其余键 —— 预览 / 属主 / 时刻 —— 在会话仓那边另有来源,不许从这里混读)。
420
+ */
421
+ export interface SessionRunRollupSource {
422
+ listSessions(opts: {
423
+ limit: number;
424
+ }): Promise<ReadonlyArray<Pick<SessionSummary, "sessionId" | "runCount" | "lastStatus" | "lastRunId" | "lastTaskRunId">>>;
425
+ }
410
426
  /**
411
427
  * S-297 —— 「这个会话**最近一条腿**是哪一条 run」的窄读面(`latestRunForSession` 的回值)。
412
428
  *
@@ -40,7 +40,7 @@
40
40
  * 换的时候要核对的等价点:本文件的 `normalize()` 统一形状 + `max` 截断 +
41
41
  * `setWebSearchBadPayloadObserver` 观测器,core 的 adapter 都没有,得在外层补回来。
42
42
  *
43
- * ③ **四级优先级语义未定**(env / config-center / per-scenario / per-request 谁压谁)。
43
+ * ③ **四级优先级语义未定**(env / web-admin / per-scenario / per-request 谁压谁)。
44
44
  * 🔴 三问里「谁被伤」这条决定了默认值:一个**只配了模型 key** 的既有部署,若新语义让某一级
45
45
  * 能自行打开搜索,它就在升级后**凭空长出一条出网工具** —— 那是安全姿态变更,不是特性。
46
46
  * ⇒ 语义定稿前,任何新的开启通道**默认 OFF**;开启必须是部署方的显式动作。