@sema-agent/server 7.50.0 → 7.52.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 (222) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/USAGE.md +83 -20
  4. package/dist/adoption/plan.d.ts +4 -4
  5. package/dist/adoption/plan.js +11 -0
  6. package/dist/approval-card.d.ts +26 -2
  7. package/dist/approval-card.js +9 -0
  8. package/dist/approval-hmac.d.ts +1 -1
  9. package/dist/approval-reconciler.d.ts +2 -2
  10. package/dist/approval-reconciler.js +1 -1
  11. package/dist/bench/l8/artifact.d.ts +1 -1
  12. package/dist/bench/s1/arms.d.ts +1 -1
  13. package/dist/bench/s1/live-deps.d.ts +1 -1
  14. package/dist/bench/s1/oracle.d.ts +1 -1
  15. package/dist/bench/s1/row.d.ts +1 -1
  16. package/dist/boot/budget-tracing.d.ts +1 -1
  17. package/dist/boot/budget-tracing.js +2 -5
  18. package/dist/boot/coordinators.js +3 -1
  19. package/dist/boot/deferred-sandbox-path-env.d.ts +1 -1
  20. package/dist/boot/device-lane.d.ts +79 -0
  21. package/dist/boot/device-lane.js +63 -0
  22. package/dist/boot/engine-lease.d.ts +1 -1
  23. package/dist/boot/execution-env.d.ts +48 -2
  24. package/dist/boot/execution-env.js +57 -5
  25. package/dist/boot/governance-seams.d.ts +2 -2
  26. package/dist/boot/leader.d.ts +1 -1
  27. package/dist/boot/memory-boundary.d.ts +2 -2
  28. package/dist/boot/memory-consolidation.d.ts +6 -6
  29. package/dist/boot/org-memory.d.ts +1 -1
  30. package/dist/boot/parked-revive-gate.d.ts +3 -3
  31. package/dist/boot/reapers.d.ts +2 -2
  32. package/dist/boot/reapers.js +17 -4
  33. package/dist/boot/resolve-spec.d.ts +15 -0
  34. package/dist/boot/resolve-spec.js +40 -15
  35. package/dist/boot/retention-lane.d.ts +2 -2
  36. package/dist/boot/runner-deps.d.ts +17 -7
  37. package/dist/boot/runner-deps.js +4 -2
  38. package/dist/boot/runtime-caps.d.ts +23 -0
  39. package/dist/boot/runtime-caps.js +24 -2
  40. package/dist/boot/session-faces.d.ts +6 -1
  41. package/dist/boot/session-faces.js +17 -3
  42. package/dist/boot/shutdown.d.ts +8 -0
  43. package/dist/boot/shutdown.js +3 -1
  44. package/dist/boot/stores.js +40 -1
  45. package/dist/boot/task-list-lane.d.ts +2 -2
  46. package/dist/brain.js +51 -31
  47. package/dist/budget.d.ts +2 -2
  48. package/dist/capabilities/center-plugins.d.ts +1 -1
  49. package/dist/capabilities/collab-workflows.d.ts +1 -1
  50. package/dist/capabilities/hands-lane.d.ts +3 -3
  51. package/dist/capabilities/scenarios.d.ts +3 -3
  52. package/dist/capabilities/skills.d.ts +1 -1
  53. package/dist/config-center/hot-keys-registry.js +5 -0
  54. package/dist/config-center/http-client.d.ts +2 -2
  55. package/dist/config-center/read-face.d.ts +1 -1
  56. package/dist/config-center/restart-signal.d.ts +1 -1
  57. package/dist/config-center/types.d.ts +10 -3
  58. package/dist/config-invariants.d.ts +2 -2
  59. package/dist/config-invariants.js +18 -0
  60. package/dist/config-types.d.ts +90 -18
  61. package/dist/config.js +91 -19
  62. package/dist/deployment-governance.d.ts +1 -1
  63. package/dist/device-enrollment.d.ts +125 -0
  64. package/dist/device-enrollment.js +156 -0
  65. package/dist/device-store.d.ts +385 -0
  66. package/dist/device-store.js +407 -0
  67. package/dist/device-ws-hub.d.ts +182 -0
  68. package/dist/device-ws-hub.js +1012 -0
  69. package/dist/device-ws-protocol.d.ts +429 -0
  70. package/dist/device-ws-protocol.js +464 -0
  71. package/dist/env-facts.d.ts +5 -2
  72. package/dist/env-facts.js +1 -0
  73. package/dist/execution-lane-caps.d.ts +152 -0
  74. package/dist/execution-lane-caps.js +166 -0
  75. package/dist/fleet/fleet-bus.d.ts +1 -1
  76. package/dist/fleet/fleet-reconciler.d.ts +1 -1
  77. package/dist/fleet/fleet-terminal-window.d.ts +1 -1
  78. package/dist/http/active-run-conflict.d.ts +2 -2
  79. package/dist/http/principal-gate.d.ts +1 -1
  80. package/dist/http/route-ctx.d.ts +24 -4
  81. package/dist/http/routes/admin-memory-optout.d.ts +48 -0
  82. package/dist/http/routes/admin-memory-optout.js +75 -0
  83. package/dist/http/routes/agents-roster.d.ts +1 -1
  84. package/dist/http/routes/approvals-assistant.js +21 -21
  85. package/dist/http/routes/capabilities.js +27 -11
  86. package/dist/http/routes/diagnostics.d.ts +1 -1
  87. package/dist/http/routes/leader.d.ts +1 -1
  88. package/dist/http/routes/memory-bundle.d.ts +1 -1
  89. package/dist/http/routes/memory-bundle.js +3 -1
  90. package/dist/http/routes/memory-compliance.d.ts +4 -4
  91. package/dist/http/routes/memory-origin.d.ts +4 -4
  92. package/dist/http/routes/memory-origin.js +3 -1
  93. package/dist/http/routes/memory-policy.js +6 -2
  94. package/dist/http/routes/retention-ops.d.ts +1 -1
  95. package/dist/http/routes/rules.d.ts +5 -2
  96. package/dist/http/routes/rules.js +84 -5
  97. package/dist/http/routes/run-memory-capture.d.ts +34 -0
  98. package/dist/http/routes/run-memory-capture.js +97 -0
  99. package/dist/http/routes/runs.d.ts +1 -0
  100. package/dist/http/routes/runs.js +212 -6
  101. package/dist/http/routes/session-sync.d.ts +4 -6
  102. package/dist/http/routes/session-sync.js +60 -47
  103. package/dist/http/routes/sessions.js +61 -188
  104. package/dist/http/routes/shared-memory.d.ts +1 -1
  105. package/dist/http/routes/tasks.js +1 -1
  106. package/dist/http/routes/workflows.js +19 -0
  107. package/dist/http/server.d.ts +49 -13
  108. package/dist/http/server.js +109 -13
  109. package/dist/http/verify-rounds.d.ts +1 -1
  110. package/dist/http/wire-types.d.ts +41 -8
  111. package/dist/leader/endpoint.d.ts +1 -1
  112. package/dist/leader/leader.d.ts +1 -1
  113. package/dist/leader/repair-oracle.d.ts +2 -2
  114. package/dist/leader/repair-oracle.js +1 -1
  115. package/dist/leader/wire.js +4 -2
  116. package/dist/main.js +23 -9
  117. package/dist/memory-bundle-engine.d.ts +1 -1
  118. package/dist/memory-operator-faces.d.ts +5 -5
  119. package/dist/memory-origin-fence.d.ts +50 -0
  120. package/dist/memory-origin-fence.js +15 -0
  121. package/dist/memory-posture.d.ts +13 -1
  122. package/dist/memory-posture.js +1 -0
  123. package/dist/memory-scope.d.ts +6 -1
  124. package/dist/memory-scope.js +3 -2
  125. package/dist/model-select.d.ts +2 -2
  126. package/dist/observability/fail-open.d.ts +17 -5
  127. package/dist/observability/fail-open.js +17 -5
  128. package/dist/orchestration/hardened-vm-runner.d.ts +9 -2
  129. package/dist/orchestration/hardened-vm-runner.js +11 -1
  130. package/dist/orchestration/hardened-vm-worker-runner.js +2 -2
  131. package/dist/orchestration/workflow-completion-inbox.d.ts +2 -2
  132. package/dist/plugins/background-shell-support.d.ts +2 -2
  133. package/dist/plugins/blob-backend.d.ts +7 -2
  134. package/dist/plugins/blob-backend.js +22 -18
  135. package/dist/plugins/caching-session-store.d.ts +7 -3
  136. package/dist/plugins/checkpoint-store-sql.d.ts +2 -2
  137. package/dist/plugins/device-store-sql.d.ts +130 -0
  138. package/dist/plugins/device-store-sql.js +574 -0
  139. package/dist/plugins/file-history-store-sql.d.ts +170 -0
  140. package/dist/plugins/file-history-store-sql.js +827 -0
  141. package/dist/plugins/file-run-store.d.ts +44 -5
  142. package/dist/plugins/file-run-store.js +85 -13
  143. package/dist/plugins/fork-routing-session-store.d.ts +1 -1
  144. package/dist/plugins/fork-routing-session-store.js +2 -2
  145. package/dist/plugins/leader-run-store-sql.d.ts +1 -1
  146. package/dist/plugins/local-session-store.d.ts +9 -1
  147. package/dist/plugins/memory-embedder-fingerprint.d.ts +1 -1
  148. package/dist/plugins/memory-optout-grant-store-sql.d.ts +139 -0
  149. package/dist/plugins/memory-optout-grant-store-sql.js +157 -0
  150. package/dist/plugins/memory-origin-law.d.ts +2 -2
  151. package/dist/plugins/memory-run-store.d.ts +4 -3
  152. package/dist/plugins/memory-run-store.js +2 -1
  153. package/dist/plugins/permission-rule-store-file.d.ts +16 -6
  154. package/dist/plugins/permission-rule-store-file.js +77 -13
  155. package/dist/plugins/permission-rule-store-sql.d.ts +156 -8
  156. package/dist/plugins/permission-rule-store-sql.js +123 -21
  157. package/dist/plugins/pg-pool.js +59 -0
  158. package/dist/plugins/pg-safe-json.d.ts +1 -1
  159. package/dist/plugins/remote-env-device.d.ts +271 -0
  160. package/dist/plugins/remote-env-device.js +727 -0
  161. package/dist/plugins/remote-env-k8s.d.ts +1 -1
  162. package/dist/plugins/remote-scratchpad.js +1 -1
  163. package/dist/plugins/retention-lane-store-sql.d.ts +3 -3
  164. package/dist/plugins/retention-store-sql.d.ts +7 -7
  165. package/dist/plugins/retention-store-sql.js +6 -0
  166. package/dist/plugins/run-store-sql.d.ts +9 -3
  167. package/dist/plugins/run-store-sql.js +2 -2
  168. package/dist/plugins/send-user-file.d.ts +1 -1
  169. package/dist/plugins/session-placement.d.ts +1 -1
  170. package/dist/plugins/sql-errors.d.ts +1 -1
  171. package/dist/plugins/store-backend.d.ts +68 -29
  172. package/dist/plugins/store-backend.js +78 -15
  173. package/dist/plugins/store-contracts.d.ts +5 -3
  174. package/dist/plugins/task-list-store-sql.d.ts +1 -1
  175. package/dist/plugins/tidb-pool.js +59 -0
  176. package/dist/plugins/web-search.d.ts +1 -1
  177. package/dist/prompts-domain-validate.js +1 -1
  178. package/dist/resource-suspend.d.ts +1 -1
  179. package/dist/resource-window.d.ts +3 -3
  180. package/dist/rules-consent.d.ts +231 -9
  181. package/dist/rules-consent.js +411 -13
  182. package/dist/run-local.js +1 -0
  183. package/dist/runs.d.ts +15 -1
  184. package/dist/runs.js +19 -0
  185. package/dist/runtime-caps-resolver.d.ts +36 -1
  186. package/dist/runtime-caps-resolver.js +25 -0
  187. package/dist/runtime-governance.d.ts +1 -1
  188. package/dist/security.d.ts +5 -5
  189. package/dist/security.js +1 -1
  190. package/dist/session-sync.d.ts +52 -37
  191. package/dist/session-sync.js +19 -16
  192. package/dist/shared-memory-scope-authorizer.d.ts +1 -1
  193. package/dist/sighup-idle.d.ts +1 -1
  194. package/dist/spec-fields.d.ts +22 -1
  195. package/dist/spec-fields.js +11 -0
  196. package/dist/store-live-probe.d.ts +2 -2
  197. package/dist/task-a2a.d.ts +1 -1
  198. package/dist/task-cwd.d.ts +25 -0
  199. package/dist/task-cwd.js +3 -0
  200. package/dist/task-mcp.d.ts +3 -3
  201. package/dist/task-settings.d.ts +3 -3
  202. package/dist/task-settings.js +3 -1
  203. package/dist/task-workflow.d.ts +1 -1
  204. package/dist/text-bidi.d.ts +2 -2
  205. package/dist/tool-approval.d.ts +191 -9
  206. package/dist/tool-approval.js +248 -29
  207. package/dist/trace/core-keyset-guard.d.ts +1 -1
  208. package/dist/trace/engine-notice-wire.d.ts +6 -4
  209. package/dist/trace/engine-notice-wire.js +2 -0
  210. package/dist/trace/ledger-events.d.ts +1 -1
  211. package/dist/trace/ledger-sink.d.ts +2 -2
  212. package/dist/trace/project.d.ts +3 -2
  213. package/dist/trace/project.js +1 -0
  214. package/dist/turn-activity.d.ts +1 -1
  215. package/dist/wall-clock-jump-guard.d.ts +3 -3
  216. package/package.json +2 -2
  217. package/dist/http/tar.d.ts +0 -17
  218. package/dist/http/tar.js +0 -53
  219. package/dist/http/workspace-content.d.ts +0 -11
  220. package/dist/http/workspace-content.js +0 -20
  221. package/dist/plugins/file-snapshot-store-sql.d.ts +0 -170
  222. package/dist/plugins/file-snapshot-store-sql.js +0 -213
@@ -34,6 +34,20 @@ import type { TaskResult, TaskStatus } from "@sema-agent/core";
34
34
  import type { RunRecord, SessionSummary, RunEvent } from "./store-contracts.js";
35
35
  import type { RunStoreCheckpointProbe } from "./memory-run-store.js";
36
36
  import type { LedgerEventType } from "../trace/ledger-events.js";
37
+ /**
38
+ * `RUN_STORE_STRICT_HYDRATE=on` 下 hydrate 列不出 `runs/` 时的**拒启**类型(S-46 件B;codex 对抗复审
39
+ * R1-[high] 验真后从裸 `Error` 升成具名类型)。
40
+ *
41
+ * 🔴 **为什么必须是一个类型而不是一句文案**:`openStoreBackendWithFallback` 对**默认推导**的 local
42
+ * (`DB_BACKEND` 未设)有一条既有降级臂 —— mkdir / 只读盘那类「这台机器存不了盘」的失败降级到内存继续起。
43
+ * 本拒启从 `new LocalBackend(...)` 里冒出来,长得和它们一模一样,不给它一个可判别的身份就会被同一个
44
+ * catch 吞掉:operator 明确要的 **fail-closed 反而变成 fail-open**,而且丢的正是持久 run 账本(这根旋钮
45
+ * 存在的全部理由)。判据锚在**类型**上,不在文案上 —— 文案会改,类型不会(同文件族先例:`AdoptionError`
46
+ * 在同一个降级臂里也是按类型无条件重抛)。
47
+ */
48
+ export declare class RunStoreStrictHydrateError extends Error {
49
+ readonly name = "RunStoreStrictHydrateError";
50
+ }
37
51
  export declare class FileRunStore {
38
52
  private readonly runsDir;
39
53
  private readonly activeDir;
@@ -55,8 +69,12 @@ export declare class FileRunStore {
55
69
  private readonly claimLocks;
56
70
  /** 件6(#43 案 [4281]):启动期告警座(可选)。既有调用方不传 ⇒ 只走 fail-open 遥测那一半,不炸。 */
57
71
  private readonly logger;
58
- constructor(root: string, logger?: {
72
+ /** S-46 件B(census 29 行终局):`RUN_STORE_STRICT_HYDRATE` 的**部署级**表态。
73
+ * `true` ⇒ hydrate 的 readdir 失败拒启;缺席/`false` ⇒ 件6 的记债腿,行为字节不变。 */
74
+ private readonly strictHydrate;
75
+ constructor(root: string, opts?: {
59
76
  error?(msg: string, fields?: Record<string, unknown>): void;
77
+ strictHydrate?: boolean;
60
78
  });
61
79
  private runDir;
62
80
  private runJsonPath;
@@ -69,6 +87,26 @@ export declare class FileRunStore {
69
87
  * partial-write-safe (run.json via atomicWriteFile can never be half-written; the active file via writeThenLink is
70
88
  * all-or-nothing; events.jsonl via readJsonlRecords drops a crash tail). A corrupt run.json (failed parse) is
71
89
  * skipped — that run is lost, not a poison that fails the boot. */
90
+ /**
91
+ * hydrate 期**逐文件**读盘的统一口(S-46 件B / codex 对抗复审 R4-[high])。
92
+ *
93
+ * `readJsonlRecords` 对 ENOENT 答 `[]`、对其余读错(EACCES / EIO / EISDIR / ENOTDIR)**抛普通 Error**
94
+ * (亲验 core `dist/stores/file/fs-atomic.js:110-112`)。那个普通 Error 从构造器冒出去之后,在**默认
95
+ * 推导**的 local 后端上会被 `openStoreBackendWithFallback` 的降级臂吞成 memory —— 也就是说 strict 开着
96
+ * 也照样带着残缺账本(或直接换成内存)起动,两处 readdir 门白守。
97
+ *
98
+ * ⇒ strict 档:非 ENOENT 的读盘失败一律转成 {@link RunStoreStrictHydrateError}(带路径 + 原文),由
99
+ * `store-backend.ts` 的按类型重抛穿过降级臂。**strict 关时逐字不变**:原样把那只 Error 抛回去(既不
100
+ * 吞、也不改形),既有的「坏行跳过、不毒 boot」宽容契约与既有的抛法一起原封保留。
101
+ */
102
+ private replay;
103
+ /**
104
+ * strict 档下的**存在性判别**(codex R4-[high] 的前半):`existsSync` 对 ENOENT 与 EACCES 同答 false,
105
+ * 而这两句话在本门下必须分开 —— 前者是常态(`runs/` 下的非目录杂物、还没落盘的目录),后者是「我读
106
+ * 不出它有没有」。只在 strict 档多付一次 `statSync`;ENOENT/ENOTDIR 一族原样放行(继续跳过),其余
107
+ * 一律拒启。
108
+ */
109
+ private refuseIfUnreadable;
72
110
  private hydrate;
73
111
  /** Commit a RunRow to disk (whole-file atomic replace) AND the index — the single write path for every status change. */
74
112
  private persist;
@@ -103,8 +141,9 @@ export declare class FileRunStore {
103
141
  setSuspended(taskId: string): Promise<void>;
104
142
  /** design/80 D-B: park NON-terminal `needs_review`, KEEP the claim. CAS on running/needs_review. */
105
143
  setNeedsReview(taskId: string): Promise<void>;
106
- /** Durable F4 resume: flip suspended/needs_review → running (CAS) + reset cancel/preempt flags. */
107
- markResuming(taskId: string): Promise<boolean>;
144
+ /** Durable F4 resume: flip suspended/needs_review → running (CAS) + reset cancel/preempt flags.
145
+ * S-51(codex R1 [medium]):认领 = 接管 `instanceId`(= 当前持有副本;SQL 孪生同注)。CAS 输不改。 */
146
+ markResuming(taskId: string, instanceId: string): Promise<boolean>;
108
147
  getActiveTaskId(sessionId: string): Promise<string | undefined>;
109
148
  getRun(taskId: string): Promise<RunRecord | undefined>;
110
149
  /** Runs newest-first, keyset on (createdAt, taskId) DESC; optional exact-match filters (owner is exact, like the SQL). */
@@ -182,7 +221,7 @@ export declare class FileRunStore {
182
221
  * probe instead of a SQL JOIN (formerly a NO-OP — one of the five closed doors in the "suspended run locks the
183
222
  * session forever" incident). Semantics verbatim from the SQL twin: suspended ∧ updatedAt < cutoff ∧ NOT
184
223
  * probe.hasPending(session) → failed 'approval expired before decision' / errorCode 'approval.expired' +
185
- * release the task_active claim. The NOT-pending clause is the design/80 D-D adversarial-blocker guard
224
+ * release the task_active claim. The NOT-pending clause is the design/80 D-D blocker guard
186
225
  * (SQL 孪生现在的真身:`run-store-sql.ts` 的 `reapSuspended`;`tidb-run-store.ts` 该壳文件已删(5.0.0)): a row whose checkpoint is STILL pending belongs to the checkpoint-state sweeps —
187
226
  * time-reaping it would orphan a model leg on a now-unlocked session. Probe absent ⇒ honest NO-OP.
188
227
  * MemoryRunStore is the parity oracle (byte-identical predicate; only the persist differs).
@@ -202,7 +241,7 @@ export declare class FileRunStore {
202
241
  *
203
242
  * 复审 R2 [high]:反过来(索引优先)会在「同进程两只实例、两边索引都热」时出事 —— A 把 park 行
204
243
  * `markResuming` 成 running 并消费掉 checkpoint,B 的索引里还留着**旧的 park 对象**;B 于是读不到
205
- * 变化、拿自己那份陈旧对象通过复核,把 A 正在跑的行盖成 failed 再放锁。盘是唯一权威,判据只能读盘。
244
+ * 变化、拿自己那份陈旧对象通过复核,把 A 正在跑的行覆盖为 failed 再放锁。盘是唯一权威,判据只能读盘。
206
245
  * 回退内存索引只为「行还没落盘」这一种形(理论上不该出现,留着不让判据凭空缺行)。
207
246
  * 代价:只在 EEXIST 争用路径上多一次读文件,常路零开销。
208
247
  */
@@ -1,6 +1,6 @@
1
1
  import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
2
2
  import { join } from "node:path";
3
- import { existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { AppendLog, atomicWriteFile, ensureDir, readJsonlRecords, sanitizePathComponent, writeThenLink } from "@sema-agent/core";
6
6
  import { recordFailOpen } from "../observability/fail-open.js";
@@ -32,6 +32,9 @@ function isPidAlive(pid) {
32
32
  }
33
33
  }
34
34
  const PARK_ORPHAN_SETTLE_MS = 30_000;
35
+ export class RunStoreStrictHydrateError extends Error {
36
+ name = "RunStoreStrictHydrateError";
37
+ }
35
38
  export class FileRunStore {
36
39
  runsDir;
37
40
  activeDir;
@@ -44,14 +47,27 @@ export class FileRunStore {
44
47
  claimOwner = { nonce: randomBytes(12).toString("hex"), ...PROCESS_FACTS };
45
48
  claimLocks = new Map();
46
49
  logger;
47
- constructor(root, logger) {
48
- this.logger = logger;
50
+ strictHydrate;
51
+ constructor(root, opts) {
52
+ this.logger = opts;
53
+ this.strictHydrate = opts?.strictHydrate === true;
49
54
  this.runsDir = join(root, "runs");
50
55
  this.activeDir = join(root, "runs", "active");
51
56
  this.tmpDir = join(root, "tmp");
52
- ensureDir(this.runsDir);
53
- ensureDir(this.activeDir);
54
- ensureDir(this.tmpDir);
57
+ try {
58
+ ensureDir(this.runsDir);
59
+ ensureDir(this.activeDir);
60
+ ensureDir(this.tmpDir);
61
+ }
62
+ catch (err) {
63
+ if (!this.strictHydrate)
64
+ throw err;
65
+ const detail = err instanceof Error ? err.message : String(err);
66
+ this.logger?.error?.("file_run_store_dirs_uninitializable", { runsDir: this.runsDir, error: detail, strictHydrate: true });
67
+ throw new RunStoreStrictHydrateError(`RUN_STORE_STRICT_HYDRATE=on and the file run-store could not initialize its directories under ${this.runsDir}: ${detail}. ` +
68
+ "Refusing to boot without a run ledger and a single-active claim index (both would be silently empty). " +
69
+ "Check that runs/ and runs/active/ are directories (not files) and writable, point LOCAL_DATA_ROOT at a usable dir, or unset RUN_STORE_STRICT_HYDRATE to boot anyway (the pre-existing behavior).");
70
+ }
55
71
  this.hydrate();
56
72
  }
57
73
  runDir(taskId) {
@@ -89,6 +105,36 @@ export class FileRunStore {
89
105
  updatedAt: r.updatedAt.toISOString(),
90
106
  };
91
107
  }
108
+ replay(path) {
109
+ try {
110
+ return readJsonlRecords(path);
111
+ }
112
+ catch (err) {
113
+ if (!this.strictHydrate)
114
+ throw err;
115
+ const detail = err instanceof Error ? err.message : String(err);
116
+ this.logger?.error?.("file_run_store_entry_unreadable", { path, error: detail, strictHydrate: true });
117
+ throw new RunStoreStrictHydrateError(`RUN_STORE_STRICT_HYDRATE=on and the file run-store could not read ${path}: ${detail}. ` +
118
+ "Refusing to boot with a PARTIAL run ledger (that row would silently vanish, which reads as 'no such run'). " +
119
+ "Fix the permissions/mount on that path, point LOCAL_DATA_ROOT at a readable dir, or unset RUN_STORE_STRICT_HYDRATE to boot anyway (the pre-existing behavior).");
120
+ }
121
+ }
122
+ refuseIfUnreadable(jsonPath, dir) {
123
+ try {
124
+ statSync(jsonPath);
125
+ return;
126
+ }
127
+ catch (err) {
128
+ const code = err.code;
129
+ if (code === "ENOENT" || code === "ENOTDIR")
130
+ return;
131
+ const detail = err instanceof Error ? err.message : String(err);
132
+ this.logger?.error?.("file_run_store_entry_unreadable", { path: dir, error: detail, strictHydrate: true });
133
+ throw new RunStoreStrictHydrateError(`RUN_STORE_STRICT_HYDRATE=on and the file run-store could not determine whether ${jsonPath} exists: ${detail}. ` +
134
+ "Refusing to boot with a PARTIAL run ledger (that run would silently vanish, which reads as 'no such run'). " +
135
+ "Fix the permissions/mount on that path, point LOCAL_DATA_ROOT at a readable dir, or unset RUN_STORE_STRICT_HYDRATE to boot anyway (the pre-existing behavior).");
136
+ }
137
+ }
92
138
  hydrate() {
93
139
  let entries;
94
140
  try {
@@ -99,8 +145,16 @@ export class FileRunStore {
99
145
  this.logger?.error?.("file_run_store_hydrate_unreadable", {
100
146
  runsDir: this.runsDir,
101
147
  error: detail,
102
- note: "the file run-store booted with an EMPTY ledger because its runs/ directory could not be listed — every getRun will miss until this is fixed. This is NOT 'no such run'; check permissions/mount on the path above.",
148
+ strictHydrate: this.strictHydrate,
149
+ note: this.strictHydrate
150
+ ? "RUN_STORE_STRICT_HYDRATE=on: the file run-store REFUSES to boot because its runs/ directory could not be listed. Check permissions/mount on the path above."
151
+ : "the file run-store booted with an EMPTY ledger because its runs/ directory could not be listed — every getRun will miss until this is fixed. This is NOT 'no such run'; check permissions/mount on the path above.",
103
152
  });
153
+ if (this.strictHydrate) {
154
+ throw new RunStoreStrictHydrateError(`RUN_STORE_STRICT_HYDRATE=on and the file run-store could not list its runs directory ${this.runsDir}: ${detail}. ` +
155
+ "Refusing to boot with an EMPTY run ledger (every getRun would miss, which reads as 'no such run'). " +
156
+ "Fix the permissions/mount on that path, point LOCAL_DATA_ROOT at a readable dir, or unset RUN_STORE_STRICT_HYDRATE to boot anyway (the pre-existing behavior — the failure then only shows up as the file_run_store_hydrate_unreadable log line and the server.run-store.file-hydrate-unreadable counter).");
157
+ }
104
158
  recordFailOpen("server.run-store.file-hydrate-unreadable", detail);
105
159
  return;
106
160
  }
@@ -109,9 +163,12 @@ export class FileRunStore {
109
163
  continue;
110
164
  const dir = join(this.runsDir, name);
111
165
  const jsonPath = join(dir, "run.json");
112
- if (!existsSync(jsonPath))
166
+ if (!existsSync(jsonPath)) {
167
+ if (this.strictHydrate)
168
+ this.refuseIfUnreadable(jsonPath, dir);
113
169
  continue;
114
- const rows = readJsonlRecords(jsonPath);
170
+ }
171
+ const rows = this.replay(jsonPath);
115
172
  const disk = rows[rows.length - 1];
116
173
  if (!disk || typeof disk.taskId !== "string")
117
174
  continue;
@@ -120,7 +177,7 @@ export class FileRunStore {
120
177
  continue;
121
178
  this.runs.set(disk.taskId, this.toRow(disk));
122
179
  const eventsPath = join(dir, "events.jsonl");
123
- const recs = readJsonlRecords(eventsPath);
180
+ const recs = this.replay(eventsPath);
124
181
  if (recs.length) {
125
182
  const list = [];
126
183
  const seen = new Set();
@@ -137,7 +194,21 @@ export class FileRunStore {
137
194
  try {
138
195
  claims = readdirSync(this.activeDir);
139
196
  }
140
- catch {
197
+ catch (err) {
198
+ const detail = err instanceof Error ? err.message : String(err);
199
+ this.logger?.error?.("file_run_store_active_claims_unreadable", {
200
+ activeDir: this.activeDir,
201
+ error: detail,
202
+ strictHydrate: this.strictHydrate,
203
+ note: this.strictHydrate
204
+ ? "RUN_STORE_STRICT_HYDRATE=on: the file run-store REFUSES to boot because its runs/active/ (single-active claim index) could not be listed."
205
+ : "the file run-store booted with an EMPTY single-active claim index because runs/active/ could not be listed — getActiveTaskId will report 'no active run' for every session until this is fixed. Check permissions/mount on the path above.",
206
+ });
207
+ if (this.strictHydrate) {
208
+ throw new RunStoreStrictHydrateError(`RUN_STORE_STRICT_HYDRATE=on and the file run-store could not list its active-claim directory ${this.activeDir}: ${detail}. ` +
209
+ "Refusing to boot with an EMPTY single-active claim index (every session would look unclaimed, so a delete or a fresh run could race a live/parked one). " +
210
+ "Fix the permissions/mount on that path, point LOCAL_DATA_ROOT at a readable dir, or unset RUN_STORE_STRICT_HYDRATE to boot anyway (the pre-existing behavior).");
211
+ }
141
212
  claims = [];
142
213
  }
143
214
  const nonTerminalBySanitizedSession = new Map();
@@ -153,7 +224,7 @@ export class FileRunStore {
153
224
  }
154
225
  for (const sanitized of claims) {
155
226
  const path = join(this.activeDir, sanitized);
156
- const recs = readJsonlRecords(path);
227
+ const recs = this.replay(path);
157
228
  const claim = recs[recs.length - 1];
158
229
  if (claim && typeof claim.sessionId === "string" && typeof claim.taskId === "string" && isSafePathComponent(claim.taskId)) {
159
230
  this.active.set(claim.sessionId, claim.taskId);
@@ -328,11 +399,12 @@ export class FileRunStore {
328
399
  this.persist(r);
329
400
  }
330
401
  }
331
- async markResuming(taskId) {
402
+ async markResuming(taskId, instanceId) {
332
403
  const r = this.runs.get(taskId);
333
404
  if (!r || (r.status !== "suspended" && r.status !== "needs_review"))
334
405
  return false;
335
406
  r.status = "running";
407
+ r.instanceId = instanceId;
336
408
  r.cancelRequested = false;
337
409
  r.preemptRequested = false;
338
410
  r.updatedAt = new Date();
@@ -67,7 +67,7 @@ export declare class ForkRoutingSessionStore implements SessionStore {
67
67
  forget(sessionId: string): void | Promise<void>;
68
68
  pin(sessionId: string): void | Promise<void>;
69
69
  unpin(sessionId: string): void | Promise<void>;
70
- noteTaskRun(sessionId: string, taskId: string): void | Promise<void>;
70
+ noteTaskRun(sessionId: string, taskId: string, runId?: string): void | Promise<void>;
71
71
  list(): Promise<SessionStoreSummary[]>;
72
72
  /**
73
73
  * [1593] 子代 park 晋升:把住在 transient(TTL)店的子会话**整树拷贝**进 host durable 店,然后
@@ -164,8 +164,8 @@ export class ForkRoutingSessionStore {
164
164
  }
165
165
  return this.pick(sessionId).unpin?.(sessionId);
166
166
  }
167
- noteTaskRun(sessionId, taskId) {
168
- return this.pick(sessionId).noteTaskRun?.(sessionId, taskId);
167
+ noteTaskRun(sessionId, taskId, runId) {
168
+ return this.pick(sessionId).noteTaskRun?.(sessionId, taskId, runId);
169
169
  }
170
170
  list() {
171
171
  return this.transient.list?.() ?? Promise.resolve([]);
@@ -74,7 +74,7 @@ export declare class SqlLeaderRunStore {
74
74
  /** 按幂等键读回既有行(dup-key 观察臂;也给集成套件直查)。 */
75
75
  getByIdem(idemKey: string): Promise<LeaderRunRecord | undefined>;
76
76
  /**
77
- * 陈旧 `running` 清算(codex 对抗复审 R1-high②,验真后修)。
77
+ * 陈旧 `running` 清算(codex 交叉复审 R1-high②,验真后修)。
78
78
  *
79
79
  * 病:准入(INSERT)与驱动(后台腿)是**两步**,而驱动腿只活在受理它的那个进程里。副本在这两步之间
80
80
  * 挂掉、或在一条 24h 的 run 中途挂掉,行就永远停在 `running`:跨副本读面永远显示「在跑」,而没有任何
@@ -172,7 +172,15 @@ export declare class LocalSessionStore implements SessionStore {
172
172
  }): Promise<SessionSummary[]>;
173
173
  /** K-5c (core 1.155 SessionStore seam): core's Runner calls this at runTask START with the run's taskId, so the
174
174
  * latest run surfaces as `lastRunId` on listSessions even while it is still running/suspended (the local backend
175
- * has no runs ledger to derive it from, unlike the TiDB/PG twins). In-memory, last-write-wins per session. */
175
+ * has no runs ledger to derive it from, unlike the TiDB/PG twins). In-memory, last-write-wins per session.
176
+ * #499(core 5.65 增量批,**有意缓议**):core 的第三参 engine runId 在此**尚未消费**——它唯一的
177
+ * 合法出口是 listSessions 行上的 `lastTaskRunId`,而 `SessionSummary` 是 canonical spec 的**闭集**
178
+ * (sema-sdk spec/openapi.yaml `additionalProperties:false`,producer-contract sessionsList 格咬着),
179
+ * 加键是跨仓 spec 节拍的事。spec 开键那一批再落:map 值扩成 `{taskId, runId?}` + 两座同动清座语义
180
+ * (对齐 core TtlSessionStore.noteTaskRun 的 omit⇒clear,session-store.js 亲读)+ 行投影。
181
+ * codex 5.65 增量批 R1-[medium] 曾建议「存储先行、投影另批」——**驳**:本店 runId 的唯一读点就是
182
+ * 行投影,先写不读=黑盒不可测的死状态(红先律无红可铺);清座语义单独存在也无观察面。存储与投影
183
+ * 必须同批,债主=sessions 列表 wire 键的 spec 节拍批(本注即债的定位锚)。 */
176
184
  noteTaskRun(sessionId: string, taskId: string): void;
177
185
  /** Synthesize a session's objective preview = the most-recent USER text message (the objective the run last saw).
178
186
  * Reads the session's entries through the repo; any read error degrades to null (never fails the list). */
@@ -49,7 +49,7 @@
49
49
  * 3. **门自身失败 = 抛(拒启)**。吞掉 = 元表停留旧值 = 下次 boot 再清一次,把一次性代价变成每次 boot
50
50
  * 的代价;且「门在场但没跑成」静默 = fail-open。
51
51
  *
52
- * ## 两条已知残余(codex 对抗复审 R1-F1/R1-F3,均判为真;处置=成文,不落码面)
52
+ * ## 两条已知残余(codex 交叉复审 R1-F1/R1-F3,均判为真;处置=成文,不落码面)
53
53
  *
54
54
  * - **跨门回滚**(F1):指纹门**之前**的版本(7.14.x/7.15.x:有 embedder、无本门)照写 `embedding` 列却
55
55
  * 不认识元表 ⇒「装本版(元表=B)→ 回滚旧版跑一段(写 A 空间向量)→ 升回本版仍配 B」走完,元表与配置
@@ -0,0 +1,139 @@
1
+ /**
2
+ * design/383 §3.1 / S-50 §一(S-49 S-2)—— per-principal **memory-capture opt-out 授权表**
3
+ * `memory_optout_grant`:`RuntimeCaps.allowMemoryOptOut` 的本地 verdict 源 + operator 管理面的持久层。
4
+ * SINGLE-FILE DUAL-DIALECT(design/158 A12 形,照 approval-ask-store-sql.ts:一个 `SqlMemoryOptOutGrantStore`
5
+ * 接 `SqlDriver`,两个薄 ctor 子类落方言绑定)。
6
+ *
7
+ * ── 数据形(S-50 §1.1 的施工形;**稿偏离如实记**)────────────────────────────────────────────────
8
+ * S-50 稿写的是 `PRIMARY KEY (org_id, principal)` + `principal=''` 为 org 级缺省哨兵行。亲读本仓
9
+ * (侦察 2026-08-30):server **没有 org 身份维** —— 租户维 = 单 `principal`(`gatedPrincipal` 供给),
10
+ * `org` 只是记忆 scope 目录里的一个词(`org-memory-admission.ts`:授权目录三态,不是 principal 的属性),
11
+ * 任何 SQL 表都没有 `org_id` 列。给表铸一根没人能填的 `org_id` 就是编数据(「不编」律)。所以:
12
+ * · **一台 worker = 一个租户面**,稿中「org 级缺省行」落成**部署级缺省行**(同一哨兵 `principal=''`);
13
+ * · 折叠仍是三层:per-principal 行 → 部署缺省行 → 代码缺省 `allow`(governed 姿态「verdict 必答」由此
14
+ * 结构性满足,新增用户零建行义务 —— 稿 §1.1 F6 折叠律原样保住);
15
+ * · 将来 server 长出 principal→org 身份面时,升 (org_id, principal) 复合键是一次 additive 迁移
16
+ * (缺省行按 org 复制),不是重设计。
17
+ *
18
+ * ── 列 ──────────────────────────────────────────────────────────────────────────────────────────
19
+ * principal VARCHAR(190) PK '' = 部署缺省哨兵行;宽度与全仓 principal 列同(approval_ask.owner)
20
+ * allow TINYINT(1)/BOOLEAN NOT NULL
21
+ * updated_at_ms BIGINT NOT NULL 审计:何时
22
+ * updated_by VARCHAR(190) 审计:谁改的(operator principal,恒在场 —— 四端点都过 operator 门)
23
+ * note VARCHAR(500) NULL 审计:为何(可缺席,不编)
24
+ * 审计**行内**,不另建审计表(S-50 §1.3 v1 体量裁定;变更史需求真出现再升 retention 面同族表)。
25
+ *
26
+ * ── 方言差异(显式写在调用点,A12 doctrine)──────────────────────────────────────────────────────
27
+ * · `?` vs `$n` 占位符;
28
+ * · upsert:TiDB `INSERT … ON DUPLICATE KEY UPDATE` vs PG `INSERT … ON CONFLICT (principal) DO UPDATE`;
29
+ * · boolean:TiDB `TINYINT(1)` 读回来是 0/1 **number**,PG `BOOLEAN` 读回来是 boolean —— `readAllow`
30
+ * 两形都认、其余一律 throw(一列存 verdict 的表不许 truthiness 读:`"0"`/`"false"` 串读成 true 正是
31
+ * 本键最怕的方向)。
32
+ *
33
+ * ── 缓存姿态 ────────────────────────────────────────────────────────────────────────────────────
34
+ * **零缓存直读**(S-50 §1.4 撤销窗:server 只保证 resolver 下一次读到新 verdict)。core 每任务 prepare 调
35
+ * 一次 resolver;本表两行以内的 PK 点查在任何 SQL 后端都是亚毫秒级,加缓存 = 为不存在的负载引入一致性窗
36
+ * (若未来实测成压力源,升级形 = TTL ≤ 60s 且写穿,稿已裁)。
37
+ */
38
+ import type { Pool as MySqlPool } from "mysql2/promise";
39
+ import type { Pool as PgPool } from "pg";
40
+ import type { PgQueryFn } from "./pg-query.js";
41
+ import { type SqlDriver, type SqlDialect } from "./sql-driver.js";
42
+ import type { MemoryOptOutVerdictSource } from "../runtime-caps-resolver.js";
43
+ export declare const MEMORY_OPTOUT_GRANT_TABLE = "memory_optout_grant";
44
+ /** 部署缺省哨兵行的 principal 值(S-50 §1.1;'' 是**真值**不是缺席 —— NOT NULL PK)。 */
45
+ export declare const MEMORY_OPTOUT_DEFAULT_PRINCIPAL = "";
46
+ /** 一行的读投影(wire 形逐字:S-50 §1.3 的 `{principal, allow, updatedAtMs, updatedBy, note?}`)。 */
47
+ export interface MemoryOptOutGrantRow {
48
+ principal: string;
49
+ allow: boolean;
50
+ updatedAtMs: number;
51
+ updatedBy: string;
52
+ /** 只在真有 note 时在场(不铸 `null` 键:缺席=没写理由,不编)。 */
53
+ note?: string;
54
+ }
55
+ /** 折叠输入 = 表里与本 principal 相关的行(至多两行:缺省行 + 本 principal 行)。 */
56
+ export interface MemoryOptOutGrantFoldRow {
57
+ principal: string;
58
+ allow: boolean;
59
+ }
60
+ export type MemoryOptOutVerdictSourceKind = "principal" | "deployment-default" | "code-default";
61
+ /**
62
+ * S-50 §1.1 **F6 折叠律**(纯函数,`build*`:产纯数据):per-principal 行 → 部署缺省哨兵行 → 代码缺省 allow。
63
+ * 三层折叠后**恒能给显式 verdict**。`principal` 缺席(匿名/系统 run)只看缺省行 —— 别人的覆盖行绝不算到
64
+ * 匿名头上。rows 里不相干的 principal 行被忽略(SQL 层已按 `IN (?, '')` 收窄,这里再判一次是让纯函数对
65
+ * 任意输入都正确,不依赖调用方的查询形)。
66
+ */
67
+ export declare function buildMemoryOptOutVerdict(rows: readonly MemoryOptOutGrantFoldRow[], principal: string | undefined): {
68
+ allow: boolean;
69
+ source: MemoryOptOutVerdictSourceKind;
70
+ };
71
+ /** 管理面(S-50 §1.3 四端点)+ resolver 面(S-1)共用的店接口。 */
72
+ export interface MemoryOptOutGrantStore extends MemoryOptOutVerdictSource {
73
+ /** 全表读投影:缺省行(缺席 ⇒ null,不编)+ 全部覆盖行(按 principal 升序,稳定)。 */
74
+ list(): Promise<{
75
+ deploymentDefault: MemoryOptOutGrantRow | null;
76
+ overrides: MemoryOptOutGrantRow[];
77
+ }>;
78
+ /** 部署缺省行 upsert。 */
79
+ putDefault(input: {
80
+ allow: boolean;
81
+ updatedBy: string;
82
+ note?: string;
83
+ }): Promise<MemoryOptOutGrantRow>;
84
+ /** per-principal 覆盖行 upsert(`principal` 非空:空串是哨兵,走 putDefault —— 调用方先验型,店内再拒一次)。 */
85
+ putOverride(principal: string, input: {
86
+ allow: boolean;
87
+ updatedBy: string;
88
+ note?: string;
89
+ }): Promise<MemoryOptOutGrantRow>;
90
+ /** 撤覆盖(回落部署缺省)。true = 真删了一行;false = 本来就没有(幂等,诚实)。哨兵串恒拒。 */
91
+ deleteOverride(principal: string): Promise<boolean>;
92
+ }
93
+ /**
94
+ * principal 的**字节形状法**(店与路由共用一只谓词;返回问题句,`undefined` = 合形)。
95
+ *
96
+ * 🔴 codex R1-[high](第二份,验真后修):TiDB/MySQL 的 `utf8mb4_bin` 是 **PAD SPACE** 语义 —— `'alice'` 与
97
+ * `'alice '`(尾空格)在 PK 比较上**相等**,PG 的 `COLLATE "C"` 则字节精确。一个带尾空格的 principal 在 TiDB 上会
98
+ * 撞进别人的行(PUT 改了别人的 verdict、DELETE 删了别人的覆盖),在 PG 上则是另一条行 —— 双方言分叉,而且方向是
99
+ * 授权面上的串台。不改 collation(TiDB 对 NO PAD 排序规则的支持随版本走,且存量表要迁移);改在**边界**:任何
100
+ * 首尾空白 / 控制字符的 principal 一律拒(合法身份从不长这样 —— `verifiedPrincipal` 铸的 principal 是 header/JWT
101
+ * sub 的裁剪值),于是 PAD SPACE 永远碰不到两条只差空白的键。同时拒内部哨兵(`RESERVED_PRINCIPALS`,security.ts
102
+ * 的单一登记表)与超列宽。
103
+ */
104
+ export declare function memoryOptOutPrincipalShapeProblem(principal: string): string | undefined;
105
+ /** 入口断言(店内第二道墙;路由已用同一只谓词答 400):哨兵串不许经覆盖面进出;形状法见上。 */
106
+ export declare function assertOverridePrincipal(principal: string): void;
107
+ /** 双方言 MemoryOptOutGrantStore。见文件头的姿态 + 方言差异账。 */
108
+ export declare class SqlMemoryOptOutGrantStore implements MemoryOptOutGrantStore {
109
+ private readonly driver;
110
+ private readonly dialect;
111
+ private readonly now;
112
+ constructor(driver: SqlDriver, dialect: SqlDialect, now?: () => number);
113
+ resolveAllow(principal: string | undefined): Promise<boolean>;
114
+ list(): Promise<{
115
+ deploymentDefault: MemoryOptOutGrantRow | null;
116
+ overrides: MemoryOptOutGrantRow[];
117
+ }>;
118
+ putDefault(input: {
119
+ allow: boolean;
120
+ updatedBy: string;
121
+ note?: string;
122
+ }): Promise<MemoryOptOutGrantRow>;
123
+ putOverride(principal: string, input: {
124
+ allow: boolean;
125
+ updatedBy: string;
126
+ note?: string;
127
+ }): Promise<MemoryOptOutGrantRow>;
128
+ deleteOverride(principal: string): Promise<boolean>;
129
+ private upsert;
130
+ }
131
+ export declare class TiDBMemoryOptOutGrantStore extends SqlMemoryOptOutGrantStore {
132
+ constructor(pool: MySqlPool, now?: () => number);
133
+ }
134
+ export declare class PgMemoryOptOutGrantStore extends SqlMemoryOptOutGrantStore {
135
+ constructor(pool: PgPool, now?: () => number);
136
+ }
137
+ export declare const TIDB_MEMORY_OPTOUT_GRANT_STATEMENTS: readonly string[];
138
+ export declare function ensurePgMemoryOptOutGrantSchema(q: PgQueryFn): Promise<void>;
139
+ //# sourceMappingURL=memory-optout-grant-store-sql.d.ts.map
@@ -0,0 +1,157 @@
1
+ import { mysqlDriver, pgDriver } from "./sql-driver.js";
2
+ import { RESERVED_PRINCIPALS, RESERVED_PRINCIPAL_PREFIXES } from "../security.js";
3
+ export const MEMORY_OPTOUT_GRANT_TABLE = "memory_optout_grant";
4
+ export const MEMORY_OPTOUT_DEFAULT_PRINCIPAL = "";
5
+ export function buildMemoryOptOutVerdict(rows, principal) {
6
+ if (principal !== undefined && principal !== MEMORY_OPTOUT_DEFAULT_PRINCIPAL) {
7
+ const own = rows.find((r) => r.principal === principal);
8
+ if (own !== undefined)
9
+ return { allow: own.allow, source: "principal" };
10
+ }
11
+ const dflt = rows.find((r) => r.principal === MEMORY_OPTOUT_DEFAULT_PRINCIPAL);
12
+ if (dflt !== undefined)
13
+ return { allow: dflt.allow, source: "deployment-default" };
14
+ return { allow: true, source: "code-default" };
15
+ }
16
+ export function memoryOptOutPrincipalShapeProblem(principal) {
17
+ if (principal.length === 0)
18
+ return "principal is empty (the empty string is the deployment-default sentinel row, not an addressable principal)";
19
+ if (principal.length > 190)
20
+ return "principal exceeds the 190-char column width";
21
+ if (/^\s|\s$/u.test(principal))
22
+ return "principal has leading/trailing whitespace (rejected at the boundary: MySQL-protocol PAD SPACE collation would fold it onto another principal's row)";
23
+ if (/[\u0000-\u001f\u007f]/u.test(principal))
24
+ return "principal contains control characters";
25
+ if (RESERVED_PRINCIPALS.includes(principal) || RESERVED_PRINCIPAL_PREFIXES.some((p) => principal.startsWith(p)))
26
+ return "principal is a reserved internal scope sentinel";
27
+ return undefined;
28
+ }
29
+ export function assertOverridePrincipal(principal) {
30
+ if (principal === MEMORY_OPTOUT_DEFAULT_PRINCIPAL)
31
+ throw new Error("memory opt-out grant: the empty principal is the deployment-default sentinel row — use the default face, not the override face");
32
+ const problem = memoryOptOutPrincipalShapeProblem(principal);
33
+ if (problem !== undefined)
34
+ throw new Error(`memory opt-out grant: ${problem}`);
35
+ }
36
+ function ph(dialect, n) {
37
+ return dialect === "pg" ? `$${n}` : "?";
38
+ }
39
+ function readAllow(v) {
40
+ if (typeof v === "boolean")
41
+ return v;
42
+ if (v === 1 || v === 0)
43
+ return v === 1;
44
+ throw new Error(`memory_optout_grant.allow read back as ${typeof v} (${String(v)}) — refusing to truthiness-read a verdict column`);
45
+ }
46
+ function readNum(v, col) {
47
+ if (typeof v === "number" && Number.isFinite(v))
48
+ return v;
49
+ if (typeof v === "string" && /^\d+$/.test(v))
50
+ return Number(v);
51
+ if (typeof v === "bigint")
52
+ return Number(v);
53
+ throw new Error(`memory_optout_grant.${col} read back as ${typeof v} — not a number`);
54
+ }
55
+ function toRow(r) {
56
+ const note = r.note;
57
+ return {
58
+ principal: String(r.principal),
59
+ allow: readAllow(r.allow),
60
+ updatedAtMs: readNum(r.updated_at_ms, "updated_at_ms"),
61
+ updatedBy: String(r.updated_by),
62
+ ...(typeof note === "string" && note.length > 0 ? { note } : {}),
63
+ };
64
+ }
65
+ export class SqlMemoryOptOutGrantStore {
66
+ driver;
67
+ dialect;
68
+ now;
69
+ constructor(driver, dialect, now = () => Date.now()) {
70
+ this.driver = driver;
71
+ this.dialect = dialect;
72
+ this.now = now;
73
+ }
74
+ async resolveAllow(principal) {
75
+ const d = this.dialect;
76
+ if (principal !== undefined && principal !== MEMORY_OPTOUT_DEFAULT_PRINCIPAL) {
77
+ const problem = memoryOptOutPrincipalShapeProblem(principal);
78
+ if (problem !== undefined)
79
+ throw new Error(`memory opt-out grant: cannot resolve a verdict for a malformed principal — ${problem}`);
80
+ }
81
+ const res = principal !== undefined && principal !== MEMORY_OPTOUT_DEFAULT_PRINCIPAL
82
+ ? await this.driver.query(`SELECT principal, allow FROM ${MEMORY_OPTOUT_GRANT_TABLE} WHERE principal IN (${ph(d, 1)}, ${ph(d, 2)})`, [principal, MEMORY_OPTOUT_DEFAULT_PRINCIPAL])
83
+ : await this.driver.query(`SELECT principal, allow FROM ${MEMORY_OPTOUT_GRANT_TABLE} WHERE principal = ${ph(d, 1)}`, [MEMORY_OPTOUT_DEFAULT_PRINCIPAL]);
84
+ const rows = res.rows.map((r) => ({ principal: String(r.principal), allow: readAllow(r.allow) }));
85
+ return buildMemoryOptOutVerdict(rows, principal).allow;
86
+ }
87
+ async list() {
88
+ const res = await this.driver.query(`SELECT principal, allow, updated_at_ms, updated_by, note FROM ${MEMORY_OPTOUT_GRANT_TABLE} ORDER BY principal ASC`);
89
+ let deploymentDefault = null;
90
+ const overrides = [];
91
+ for (const r of res.rows) {
92
+ const row = toRow(r);
93
+ if (row.principal === MEMORY_OPTOUT_DEFAULT_PRINCIPAL)
94
+ deploymentDefault = row;
95
+ else
96
+ overrides.push(row);
97
+ }
98
+ return { deploymentDefault, overrides };
99
+ }
100
+ putDefault(input) {
101
+ return this.upsert(MEMORY_OPTOUT_DEFAULT_PRINCIPAL, input);
102
+ }
103
+ async putOverride(principal, input) {
104
+ assertOverridePrincipal(principal);
105
+ return this.upsert(principal, input);
106
+ }
107
+ async deleteOverride(principal) {
108
+ assertOverridePrincipal(principal);
109
+ const res = await this.driver.query(`DELETE FROM ${MEMORY_OPTOUT_GRANT_TABLE} WHERE principal = ${ph(this.dialect, 1)}`, [principal]);
110
+ return res.affected === 1;
111
+ }
112
+ async upsert(principal, input) {
113
+ const at = this.now();
114
+ const note = input.note !== undefined && input.note.length > 0 ? input.note : null;
115
+ const allowParam = this.dialect === "pg" ? input.allow : input.allow ? 1 : 0;
116
+ if (this.dialect === "pg") {
117
+ await this.driver.query(`INSERT INTO ${MEMORY_OPTOUT_GRANT_TABLE} (principal, allow, updated_at_ms, updated_by, note) VALUES ($1, $2, $3, $4, $5)
118
+ ON CONFLICT (principal) DO UPDATE SET allow = EXCLUDED.allow, updated_at_ms = EXCLUDED.updated_at_ms, updated_by = EXCLUDED.updated_by, note = EXCLUDED.note`, [principal, allowParam, at, input.updatedBy, note]);
119
+ }
120
+ else {
121
+ await this.driver.query(`INSERT INTO ${MEMORY_OPTOUT_GRANT_TABLE} (principal, allow, updated_at_ms, updated_by, note) VALUES (?, ?, ?, ?, ?)
122
+ ON DUPLICATE KEY UPDATE allow = VALUES(allow), updated_at_ms = VALUES(updated_at_ms), updated_by = VALUES(updated_by), note = VALUES(note)`, [principal, allowParam, at, input.updatedBy, note]);
123
+ }
124
+ return { principal, allow: input.allow, updatedAtMs: at, updatedBy: input.updatedBy, ...(note !== null ? { note } : {}) };
125
+ }
126
+ }
127
+ export class TiDBMemoryOptOutGrantStore extends SqlMemoryOptOutGrantStore {
128
+ constructor(pool, now) {
129
+ super(mysqlDriver(pool), "tidb", now);
130
+ }
131
+ }
132
+ export class PgMemoryOptOutGrantStore extends SqlMemoryOptOutGrantStore {
133
+ constructor(pool, now) {
134
+ super(pgDriver(pool), "pg", now);
135
+ }
136
+ }
137
+ export const TIDB_MEMORY_OPTOUT_GRANT_STATEMENTS = [
138
+ `CREATE TABLE IF NOT EXISTS ${MEMORY_OPTOUT_GRANT_TABLE} (
139
+ principal VARCHAR(190) NOT NULL,
140
+ allow TINYINT(1) NOT NULL,
141
+ updated_at_ms BIGINT NOT NULL,
142
+ updated_by VARCHAR(190) NOT NULL,
143
+ note VARCHAR(500) NULL,
144
+ PRIMARY KEY (principal)
145
+ ) COLLATE utf8mb4_bin`,
146
+ ];
147
+ export async function ensurePgMemoryOptOutGrantSchema(q) {
148
+ await q(`CREATE TABLE IF NOT EXISTS ${MEMORY_OPTOUT_GRANT_TABLE} (
149
+ principal VARCHAR(190) COLLATE "C" NOT NULL,
150
+ allow BOOLEAN NOT NULL,
151
+ updated_at_ms BIGINT NOT NULL,
152
+ updated_by VARCHAR(190) COLLATE "C" NOT NULL,
153
+ note VARCHAR(500) COLLATE "C",
154
+ PRIMARY KEY (principal)
155
+ )`);
156
+ }
157
+ //# sourceMappingURL=memory-optout-grant-store-sql.js.map
@@ -29,7 +29,7 @@
29
29
  *
30
30
  * ① **判据本身**({@link originWhitewashRefusal}):`committedOriginOf` / `originEquals` 都用 core 的
31
31
  * 导出实现,**不重写** —— 它们要处理 `frontmatter.origin` 与「保留在 `extra` 里的 origin 形字节块」
32
- * 两种承载形(legacy / 外来),自己写一份近似的等于给洗白留一条后门。
32
+ * 两种承载形(legacy / 外来),自己写一份近似的等于给洗白留一条旁路。
33
33
  * ② **批次基线**({@link rememberBatchOrigin}):两支 SQL 后端逐条 patch 直打真库(没有 File backend
34
34
  * 那种「先规划整批再落盘」的阶段),所以同批 `delete` 之后再 `add` 时,库里已经没有那一行了 ——
35
35
  * 只看当下的库会把它误判成合法新生命。基线 map 按**本批第一次触碰**记下该 id 的已提交 origin,
@@ -58,7 +58,7 @@ import { type MemoryEntryFrontmatter, type MemoryEntryHeader } from "@sema-agent
58
58
  * ## 判据禁手抄
59
59
  *
60
60
  * 谓词一律用 core 导出的 {@link ambiguousOriginRepresentation}:它要解 `extra` 里的 origin 形块
61
- * (跨块字段组合、解析期块重排都算歧义),自己写一份近似的等于给洗白开后门 —— 这条在 #307 就写死过。
61
+ * (跨块字段组合、解析期块重排都算歧义),自己写一份近似的等于给洗白开口子 —— 这条在 #307 就写死过。
62
62
  * core 5.47.0 起该谓词进公开导出面([4698] 请托兑现),本仓的「撤除条件钉」也因此在 5.47 提货当天
63
63
  * 准时翻红,本批就是那条钉指定的兑现动作。
64
64
  *