@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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +83 -20
- package/dist/adoption/plan.d.ts +4 -4
- package/dist/adoption/plan.js +11 -0
- package/dist/approval-card.d.ts +26 -2
- package/dist/approval-card.js +9 -0
- package/dist/approval-hmac.d.ts +1 -1
- package/dist/approval-reconciler.d.ts +2 -2
- package/dist/approval-reconciler.js +1 -1
- package/dist/bench/l8/artifact.d.ts +1 -1
- package/dist/bench/s1/arms.d.ts +1 -1
- package/dist/bench/s1/live-deps.d.ts +1 -1
- package/dist/bench/s1/oracle.d.ts +1 -1
- package/dist/bench/s1/row.d.ts +1 -1
- package/dist/boot/budget-tracing.d.ts +1 -1
- package/dist/boot/budget-tracing.js +2 -5
- package/dist/boot/coordinators.js +3 -1
- package/dist/boot/deferred-sandbox-path-env.d.ts +1 -1
- package/dist/boot/device-lane.d.ts +79 -0
- package/dist/boot/device-lane.js +63 -0
- package/dist/boot/engine-lease.d.ts +1 -1
- package/dist/boot/execution-env.d.ts +48 -2
- package/dist/boot/execution-env.js +57 -5
- package/dist/boot/governance-seams.d.ts +2 -2
- package/dist/boot/leader.d.ts +1 -1
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/memory-consolidation.d.ts +6 -6
- package/dist/boot/org-memory.d.ts +1 -1
- package/dist/boot/parked-revive-gate.d.ts +3 -3
- package/dist/boot/reapers.d.ts +2 -2
- package/dist/boot/reapers.js +17 -4
- package/dist/boot/resolve-spec.d.ts +15 -0
- package/dist/boot/resolve-spec.js +40 -15
- package/dist/boot/retention-lane.d.ts +2 -2
- package/dist/boot/runner-deps.d.ts +17 -7
- package/dist/boot/runner-deps.js +4 -2
- package/dist/boot/runtime-caps.d.ts +23 -0
- package/dist/boot/runtime-caps.js +24 -2
- package/dist/boot/session-faces.d.ts +6 -1
- package/dist/boot/session-faces.js +17 -3
- package/dist/boot/shutdown.d.ts +8 -0
- package/dist/boot/shutdown.js +3 -1
- package/dist/boot/stores.js +40 -1
- package/dist/boot/task-list-lane.d.ts +2 -2
- package/dist/brain.js +51 -31
- package/dist/budget.d.ts +2 -2
- package/dist/capabilities/center-plugins.d.ts +1 -1
- package/dist/capabilities/collab-workflows.d.ts +1 -1
- package/dist/capabilities/hands-lane.d.ts +3 -3
- package/dist/capabilities/scenarios.d.ts +3 -3
- package/dist/capabilities/skills.d.ts +1 -1
- package/dist/config-center/hot-keys-registry.js +5 -0
- package/dist/config-center/http-client.d.ts +2 -2
- package/dist/config-center/read-face.d.ts +1 -1
- package/dist/config-center/restart-signal.d.ts +1 -1
- package/dist/config-center/types.d.ts +10 -3
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +18 -0
- package/dist/config-types.d.ts +90 -18
- package/dist/config.js +91 -19
- package/dist/deployment-governance.d.ts +1 -1
- package/dist/device-enrollment.d.ts +125 -0
- package/dist/device-enrollment.js +156 -0
- package/dist/device-store.d.ts +385 -0
- package/dist/device-store.js +407 -0
- package/dist/device-ws-hub.d.ts +182 -0
- package/dist/device-ws-hub.js +1012 -0
- package/dist/device-ws-protocol.d.ts +429 -0
- package/dist/device-ws-protocol.js +464 -0
- package/dist/env-facts.d.ts +5 -2
- package/dist/env-facts.js +1 -0
- package/dist/execution-lane-caps.d.ts +152 -0
- package/dist/execution-lane-caps.js +166 -0
- package/dist/fleet/fleet-bus.d.ts +1 -1
- package/dist/fleet/fleet-reconciler.d.ts +1 -1
- package/dist/fleet/fleet-terminal-window.d.ts +1 -1
- package/dist/http/active-run-conflict.d.ts +2 -2
- package/dist/http/principal-gate.d.ts +1 -1
- package/dist/http/route-ctx.d.ts +24 -4
- package/dist/http/routes/admin-memory-optout.d.ts +48 -0
- package/dist/http/routes/admin-memory-optout.js +75 -0
- package/dist/http/routes/agents-roster.d.ts +1 -1
- package/dist/http/routes/approvals-assistant.js +21 -21
- package/dist/http/routes/capabilities.js +27 -11
- package/dist/http/routes/diagnostics.d.ts +1 -1
- package/dist/http/routes/leader.d.ts +1 -1
- package/dist/http/routes/memory-bundle.d.ts +1 -1
- package/dist/http/routes/memory-bundle.js +3 -1
- package/dist/http/routes/memory-compliance.d.ts +4 -4
- package/dist/http/routes/memory-origin.d.ts +4 -4
- package/dist/http/routes/memory-origin.js +3 -1
- package/dist/http/routes/memory-policy.js +6 -2
- package/dist/http/routes/retention-ops.d.ts +1 -1
- package/dist/http/routes/rules.d.ts +5 -2
- package/dist/http/routes/rules.js +84 -5
- package/dist/http/routes/run-memory-capture.d.ts +34 -0
- package/dist/http/routes/run-memory-capture.js +97 -0
- package/dist/http/routes/runs.d.ts +1 -0
- package/dist/http/routes/runs.js +212 -6
- package/dist/http/routes/session-sync.d.ts +4 -6
- package/dist/http/routes/session-sync.js +60 -47
- package/dist/http/routes/sessions.js +61 -188
- package/dist/http/routes/shared-memory.d.ts +1 -1
- package/dist/http/routes/tasks.js +1 -1
- package/dist/http/routes/workflows.js +19 -0
- package/dist/http/server.d.ts +49 -13
- package/dist/http/server.js +109 -13
- package/dist/http/verify-rounds.d.ts +1 -1
- package/dist/http/wire-types.d.ts +41 -8
- package/dist/leader/endpoint.d.ts +1 -1
- package/dist/leader/leader.d.ts +1 -1
- package/dist/leader/repair-oracle.d.ts +2 -2
- package/dist/leader/repair-oracle.js +1 -1
- package/dist/leader/wire.js +4 -2
- package/dist/main.js +23 -9
- package/dist/memory-bundle-engine.d.ts +1 -1
- package/dist/memory-operator-faces.d.ts +5 -5
- package/dist/memory-origin-fence.d.ts +50 -0
- package/dist/memory-origin-fence.js +15 -0
- package/dist/memory-posture.d.ts +13 -1
- package/dist/memory-posture.js +1 -0
- package/dist/memory-scope.d.ts +6 -1
- package/dist/memory-scope.js +3 -2
- package/dist/model-select.d.ts +2 -2
- package/dist/observability/fail-open.d.ts +17 -5
- package/dist/observability/fail-open.js +17 -5
- package/dist/orchestration/hardened-vm-runner.d.ts +9 -2
- package/dist/orchestration/hardened-vm-runner.js +11 -1
- package/dist/orchestration/hardened-vm-worker-runner.js +2 -2
- package/dist/orchestration/workflow-completion-inbox.d.ts +2 -2
- package/dist/plugins/background-shell-support.d.ts +2 -2
- package/dist/plugins/blob-backend.d.ts +7 -2
- package/dist/plugins/blob-backend.js +22 -18
- package/dist/plugins/caching-session-store.d.ts +7 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +2 -2
- package/dist/plugins/device-store-sql.d.ts +130 -0
- package/dist/plugins/device-store-sql.js +574 -0
- package/dist/plugins/file-history-store-sql.d.ts +170 -0
- package/dist/plugins/file-history-store-sql.js +827 -0
- package/dist/plugins/file-run-store.d.ts +44 -5
- package/dist/plugins/file-run-store.js +85 -13
- package/dist/plugins/fork-routing-session-store.d.ts +1 -1
- package/dist/plugins/fork-routing-session-store.js +2 -2
- package/dist/plugins/leader-run-store-sql.d.ts +1 -1
- package/dist/plugins/local-session-store.d.ts +9 -1
- package/dist/plugins/memory-embedder-fingerprint.d.ts +1 -1
- package/dist/plugins/memory-optout-grant-store-sql.d.ts +139 -0
- package/dist/plugins/memory-optout-grant-store-sql.js +157 -0
- package/dist/plugins/memory-origin-law.d.ts +2 -2
- package/dist/plugins/memory-run-store.d.ts +4 -3
- package/dist/plugins/memory-run-store.js +2 -1
- package/dist/plugins/permission-rule-store-file.d.ts +16 -6
- package/dist/plugins/permission-rule-store-file.js +77 -13
- package/dist/plugins/permission-rule-store-sql.d.ts +156 -8
- package/dist/plugins/permission-rule-store-sql.js +123 -21
- package/dist/plugins/pg-pool.js +59 -0
- package/dist/plugins/pg-safe-json.d.ts +1 -1
- package/dist/plugins/remote-env-device.d.ts +271 -0
- package/dist/plugins/remote-env-device.js +727 -0
- package/dist/plugins/remote-env-k8s.d.ts +1 -1
- package/dist/plugins/remote-scratchpad.js +1 -1
- package/dist/plugins/retention-lane-store-sql.d.ts +3 -3
- package/dist/plugins/retention-store-sql.d.ts +7 -7
- package/dist/plugins/retention-store-sql.js +6 -0
- package/dist/plugins/run-store-sql.d.ts +9 -3
- package/dist/plugins/run-store-sql.js +2 -2
- package/dist/plugins/send-user-file.d.ts +1 -1
- package/dist/plugins/session-placement.d.ts +1 -1
- package/dist/plugins/sql-errors.d.ts +1 -1
- package/dist/plugins/store-backend.d.ts +68 -29
- package/dist/plugins/store-backend.js +78 -15
- package/dist/plugins/store-contracts.d.ts +5 -3
- package/dist/plugins/task-list-store-sql.d.ts +1 -1
- package/dist/plugins/tidb-pool.js +59 -0
- package/dist/plugins/web-search.d.ts +1 -1
- package/dist/prompts-domain-validate.js +1 -1
- package/dist/resource-suspend.d.ts +1 -1
- package/dist/resource-window.d.ts +3 -3
- package/dist/rules-consent.d.ts +231 -9
- package/dist/rules-consent.js +411 -13
- package/dist/run-local.js +1 -0
- package/dist/runs.d.ts +15 -1
- package/dist/runs.js +19 -0
- package/dist/runtime-caps-resolver.d.ts +36 -1
- package/dist/runtime-caps-resolver.js +25 -0
- package/dist/runtime-governance.d.ts +1 -1
- package/dist/security.d.ts +5 -5
- package/dist/security.js +1 -1
- package/dist/session-sync.d.ts +52 -37
- package/dist/session-sync.js +19 -16
- package/dist/shared-memory-scope-authorizer.d.ts +1 -1
- package/dist/sighup-idle.d.ts +1 -1
- package/dist/spec-fields.d.ts +22 -1
- package/dist/spec-fields.js +11 -0
- package/dist/store-live-probe.d.ts +2 -2
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-cwd.d.ts +25 -0
- package/dist/task-cwd.js +3 -0
- package/dist/task-mcp.d.ts +3 -3
- package/dist/task-settings.d.ts +3 -3
- package/dist/task-settings.js +3 -1
- package/dist/task-workflow.d.ts +1 -1
- package/dist/text-bidi.d.ts +2 -2
- package/dist/tool-approval.d.ts +191 -9
- package/dist/tool-approval.js +248 -29
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/engine-notice-wire.d.ts +6 -4
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/ledger-events.d.ts +1 -1
- package/dist/trace/ledger-sink.d.ts +2 -2
- package/dist/trace/project.d.ts +3 -2
- package/dist/trace/project.js +1 -0
- package/dist/turn-activity.d.ts +1 -1
- package/dist/wall-clock-jump-guard.d.ts +3 -3
- package/package.json +2 -2
- package/dist/http/tar.d.ts +0 -17
- package/dist/http/tar.js +0 -53
- package/dist/http/workspace-content.d.ts +0 -11
- package/dist/http/workspace-content.js +0 -20
- package/dist/plugins/file-snapshot-store-sql.d.ts +0 -170
- package/dist/plugins/file-snapshot-store-sql.js +0 -213
|
@@ -268,7 +268,7 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
|
|
|
268
268
|
maxLines?: number;
|
|
269
269
|
abortSignal?: AbortSignal;
|
|
270
270
|
}): Promise<Result<string[], FileError>>;
|
|
271
|
-
/** 父目录自建(与 host/local-docker/adb/ssh 同口径)。抽成共用是因为 `appendFile`
|
|
271
|
+
/** 父目录自建(与 host/local-docker/adb/ssh 同口径)。抽成共用是因为 `appendFile` 曾**跳过**它:
|
|
272
272
|
* 🔴 2026-07-25 实测的跨腿分叉 —— `appendFile` 直接调 `writeChunked`,整条路上没有 `mkdir -p`,于是
|
|
273
273
|
* 「直接 append 建一个新日志路径」在 k8s 腿上返 `not_found`,而其余 5 腿都成功(host/local-docker/adb 实测,
|
|
274
274
|
* ssh/e2b 由方法体判据)。core 的后台输出镜像正是"每拍 append 一次"的形,这条分叉会让它在 k8s 腿上从第一拍
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const REMOTE_SCRATCHPAD_LANES = new Set(["e2b", "k8s", "local-docker", "ssh"]);
|
|
1
|
+
const REMOTE_SCRATCHPAD_LANES = new Set(["e2b", "k8s", "local-docker", "ssh", "device"]);
|
|
2
2
|
export function isRemoteScratchpadLane(provider) {
|
|
3
3
|
return provider !== undefined && REMOTE_SCRATCHPAD_LANES.has(provider);
|
|
4
4
|
}
|
|
@@ -147,7 +147,7 @@ export declare class SqlRetentionLaneStore implements RetentionLaneStore {
|
|
|
147
147
|
* 判据三合取写进**一条 CAS 的 WHERE**:holder 是我 ∧ fencing token 还是本轮那个 ∧ **尚未过期**。
|
|
148
148
|
* · 第三项是承重的:租约过期而 holder 还写着我,必须读成**不再属于我**(fail-closed)——另一个副本
|
|
149
149
|
* 随时可能抢走,两个副本同时跑三方法是这条腿最不能出的事。
|
|
150
|
-
* · 「复核」与「续租」合成一条语句而不是先读后写(codex
|
|
150
|
+
* · 「复核」与「续租」合成一条语句而不是先读后写(codex 交叉复审 R1-[high],验真后改):
|
|
151
151
|
* ① 读+写两条语句之间有窗口,而 CAS 的谓词与更新在引擎里是原子的;
|
|
152
152
|
* ② 更要紧的是**续租本身**:`startRetentionLane` 的重入守卫会跳过下一 tick,于是一轮的续租机会
|
|
153
153
|
* 只有轮首那一次 —— 一轮跑满 2×interval 之后租约自己到期,而本副本毫不知情、继续删下去。
|
|
@@ -196,7 +196,7 @@ export declare class SqlRetentionLaneStore implements RetentionLaneStore {
|
|
|
196
196
|
nowMs: number;
|
|
197
197
|
}): Promise<void>;
|
|
198
198
|
/**
|
|
199
|
-
* 放置/解除 **+ 治理审计行,同一个事务**(codex
|
|
199
|
+
* 放置/解除 **+ 治理审计行,同一个事务**(codex 交叉复审 R1-[high],验真后加)。
|
|
200
200
|
*
|
|
201
201
|
* 🔴 为什么必须原子(与 §6 F5 的「删除与审计同事务」是同一条判据,只是换了一张表):两次独立提交下,
|
|
202
202
|
* 第二步失败会留下一次**无审计的状态变更**。PUT 那一支是「冻结了但账上没有」;**DELETE 那一支更重**
|
|
@@ -231,7 +231,7 @@ export declare class SqlRetentionLaneStore implements RetentionLaneStore {
|
|
|
231
231
|
*
|
|
232
232
|
* `before` = 上一页最后一行的 `id`(严格小于)。
|
|
233
233
|
*
|
|
234
|
-
* 🔴 读上限是 `MAX_LIMIT + 1` 而不是 `MAX_LIMIT`(codex
|
|
234
|
+
* 🔴 读上限是 `MAX_LIMIT + 1` 而不是 `MAX_LIMIT`(codex 交叉复审 R1-[medium],验真属实):调用方要判
|
|
235
235
|
* 「还有没有下一页」就得多取一行,而**页**的上限是 `MAX_LIMIT`。旧形把 limit 无条件夹到 `MAX_LIMIT`
|
|
236
236
|
* ⇒ 请求 `limit=200` 时 scanned 恒 ≤ 200 ⇒ `hasMore` 恒 false ⇒ **第 201 行之后的审计对这条分页链
|
|
237
237
|
* 永久不可达**。这一格是「窗的上限」与「探路的那一行」两个不同的数被写成同一个数造成的,
|
|
@@ -217,7 +217,7 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
|
|
|
217
217
|
* 一条既存的 hold(两方言的这两个动词都只在缺行时写),只保证**接下来那把行锁有东西可锁**;
|
|
218
218
|
* ② `SELECT held … FOR UPDATE` —— 现在这行必定存在,于是锁真的拿到了。
|
|
219
219
|
*
|
|
220
|
-
* 🔴 为什么必须先造行(codex
|
|
220
|
+
* 🔴 为什么必须先造行(codex 交叉复审 R1-[high],验真后改):`FOR UPDATE` 锁不住缺席行(TiDB 无 gap
|
|
221
221
|
* lock;PG READ COMMITTED 同样)。presence 形(「行在 = 冻结」)下,operator 的 PUT 与本事务可以**同时**
|
|
222
222
|
* 都读到「没有 hold」,PUT 提交返回成功之后本事务照删不误 —— 设计稿 §5 声称由行锁给出的串行化在那一形
|
|
223
223
|
* 上根本不存在。哨兵行把 hold 的写点与删除的读点压到**同一把行锁**上,那句承诺才成立。
|
|
@@ -309,7 +309,7 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
|
|
|
309
309
|
/**
|
|
310
310
|
* **可过期**的候选 token(保护谓词写在 SQL 里、扣在 `LIMIT` **之前**)。
|
|
311
311
|
*
|
|
312
|
-
* 🔴 为什么保护必须下推(codex
|
|
312
|
+
* 🔴 为什么保护必须下推(codex 交叉复审 R1-[medium],验真后改):旧形先按时间取 N 行、再在 JS 里剔除
|
|
313
313
|
* pinned —— 只要最老的一批持续被占着,每轮取回的都是同一批、每轮删 0,**它们后面**那些同样过期、又
|
|
314
314
|
* 没有任何引用的行**永远轮不到**。那不是「下一拍自然收敛」,那是一个域的留存静默停摆(而且读数一直
|
|
315
315
|
* 是 deleted=0,看上去像"没有候选")。判据下推之后 LIMIT 扣的是**真能动的行**,头部被保护多少行都不
|
|
@@ -319,7 +319,7 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
|
|
|
319
319
|
/**
|
|
320
320
|
* 过期视界之内、**当下被 live-pin 护住**的 pending 行数 = receipt 的 `skipped`。
|
|
321
321
|
*
|
|
322
|
-
* 🔴 账法定谳(
|
|
322
|
+
* 🔴 账法定谳(两轮交叉复审各推了一次,最终落在这一形):
|
|
323
323
|
* · 曾经用「候选 + pinned 两读相加当分母、差值当 skipped」——被 codex R2 抓到会把中途翻转 pin 的行数两遍;
|
|
324
324
|
* · 改成「视界总数当分母、差值当 skipped」——被 codex R3 抓到在 PG 的 READ COMMITTED 下可以**为负**
|
|
325
325
|
* (分母读完之后又有行被 put/reopen 成 pending,候选与 UPDATE 都看得见它 ⇒ expired > 分母),而一条
|
|
@@ -356,7 +356,7 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
|
|
|
356
356
|
* **可删**的会话 id:本域、过视界、且四条活引用腿**一条都不命中**(保护谓词下推,扣在 LIMIT 之前 ——
|
|
357
357
|
* 理由与 {@link expiryCandidates} 逐字相同:头部被保护的树不许饿死它后面的树)。
|
|
358
358
|
*
|
|
359
|
-
* `lock = true`(删除事务里的读)追加 `FOR UPDATE`,这一格是**承重**的(codex
|
|
359
|
+
* `lock = true`(删除事务里的读)追加 `FOR UPDATE`,这一格是**承重**的(codex 交叉复审 R1-[high]):
|
|
360
360
|
* · 会话行是**存在**的行 ⇒ 这把锁与 hold 哨兵不同,它真的锁得住;
|
|
361
361
|
* · 锁住之后,并发的 `touch()`(把会话重新拉进视界)与 session-sync 的 **owner 改写**(importEntries /
|
|
362
362
|
* replaceEntries / staged commit 都会重写 `session_meta.owner`)必须排队等我们提交 —— 否则「按旧候选集
|
|
@@ -371,11 +371,11 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
|
|
|
371
371
|
/**
|
|
372
372
|
* 本域**可归属**的孤儿卸载结果(属主 session 已亡 + 墓碑归属本域 + 归属**无歧义**)。
|
|
373
373
|
*
|
|
374
|
-
* 🔴 第三个条件是 fail-closed 的归属门(codex
|
|
374
|
+
* 🔴 第三个条件是 fail-closed 的归属门(codex 交叉复审 R3-[high],验真后加):`tool_result` 没有域列,
|
|
375
375
|
* 它的域完全靠「属主 sessionId 的墓碑」推。而会话 id 是**调用方可自选**的 ⇒ 两个租户先后用过同一个 id
|
|
376
376
|
* 完全合法;两边都删过之后,同一个 row_key 上会有**两个域**的墓碑,这时任何一边的 sweep 拿自己的墓碑
|
|
377
377
|
* 去认领这些字节都只是猜。判据因此是:**存在第二个域的同名墓碑 ⇒ 这批行不可归属 ⇒ 一根不删**,
|
|
378
|
-
* 实体清理退回既有的 TTL 腿(`reapOlderThan`)
|
|
378
|
+
* 实体清理退回既有的 TTL 腿(`reapOlderThan`)。少删一次是可接受的;删掉另一个租户的字节、还跳过它的
|
|
379
379
|
* legal hold 与审计域,不可接受。
|
|
380
380
|
*/
|
|
381
381
|
protected orphanToolResultCandidates(exec: {
|
|
@@ -394,7 +394,7 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
|
|
|
394
394
|
* {@link SqlRetentionStore.sessionCandidates});
|
|
395
395
|
* ③ 按 E21 顺序逐表删 → 墓碑 → 审计行(同事务)→ COMMIT。
|
|
396
396
|
*
|
|
397
|
-
* 🔴 活引用重查为什么是**候选查询本身**而不是第二条查询(codex
|
|
397
|
+
* 🔴 活引用重查为什么是**候选查询本身**而不是第二条查询(codex 交叉复审 R1 的两条 high 合并采纳):
|
|
398
398
|
* · 两条查询之间有窗口,而下推之后「被查到的」按构造就是「四条腿一条都不命中的」;
|
|
399
399
|
* · `FOR UPDATE` 让这些会话行在本事务期间不可被 `touch()` 拉回视界、也不可被 session-sync 改写属主 ——
|
|
400
400
|
* 旧形(不加锁 + 尾部无条件删 `session_event`)可以在属主被并发改写后,把**别的租户**的历史删掉;
|
|
@@ -373,6 +373,12 @@ export class SqlRetentionStore {
|
|
|
373
373
|
["session_policy", "session_id"],
|
|
374
374
|
...(this.opts.taskAttachmentTable ? [["task_attachment", "session_id"]] : []),
|
|
375
375
|
["snapshot_manifest", "scope"],
|
|
376
|
+
["file_history_tracked", "scope"],
|
|
377
|
+
["file_history_version", "scope"],
|
|
378
|
+
["file_history_boundary", "scope"],
|
|
379
|
+
["file_history_boundary_file", "scope"],
|
|
380
|
+
["file_history_scope", "scope"],
|
|
381
|
+
["file_history_reaped_entry", "scope"],
|
|
376
382
|
]) {
|
|
377
383
|
const s = set(col, 1);
|
|
378
384
|
await conn.query(`DELETE FROM ${table} WHERE ${s.sql}`, s.params);
|
|
@@ -152,8 +152,14 @@ export declare class SqlRunStore {
|
|
|
152
152
|
* this the row sits at `suspended`/updated_at=suspend-time for the WHOLE resume execution, so a resume that
|
|
153
153
|
* crosses the TTL is marked `failed` mid-run and its session lock (`task_active`) is released. Returns true
|
|
154
154
|
* if it claimed the flip (false ⇒ the reaper already expired it, or it isn't suspended).
|
|
155
|
+
*
|
|
156
|
+
* S-51(codex R1 [medium],红先双库证):认领 = **接管 `instance_id`**。此前该列只在 createRun 铸、此处不动,
|
|
157
|
+
* 于是 A 创建、C 认领 resume 的行永远记 A —— 而 steer/interrupt 的「他副本」臂与 sendHandleMiss 都拿这列当
|
|
158
|
+
* 「当前持有者」证据:verify 形 resume 腿(不登记 steerable)在 C 上跑时,C 读到 A≠C 就谎报「run 在别的副本」。
|
|
159
|
+
* 语义自此成文:`instance_id` = **当前持有副本**(createRun 铸、markResuming 认领接管);CAS 输不改列
|
|
160
|
+
* (一个输掉的认领不许覆盖赢家的持有记录——同一条条件 UPDATE 天然保证)。
|
|
155
161
|
*/
|
|
156
|
-
markResuming(taskId: string): Promise<boolean>;
|
|
162
|
+
markResuming(taskId: string, instanceId: string): Promise<boolean>;
|
|
157
163
|
/** The active run's taskId for a session (the `task_active` claim) — lets resume find the suspended run row. */
|
|
158
164
|
getActiveTaskId(sessionId: string): Promise<string | undefined>;
|
|
159
165
|
/** Shared row→record mapper — byte-identical between dialects (both drivers hand back the same column
|
|
@@ -215,7 +221,7 @@ export declare class SqlRunStore {
|
|
|
215
221
|
* 右删失效**(anchors 腿的注逐字写着这句)。删除决策的属主门在**路由层**(DELETE /v1/sessions/:id 先
|
|
216
222
|
* `ownerOf` 再比对 scope,fleet-wide 臂同门),本方法的唯一生产调用点是那条路由后面的 purge 协调器。
|
|
217
223
|
*
|
|
218
|
-
* 🔴 **化身围栏**(`expectedSessionOwner`;codex
|
|
224
|
+
* 🔴 **化身围栏**(`expectedSessionOwner`;codex 交叉复审 R1-[high],验真后修)—— 属主门下来了,但会话
|
|
219
225
|
* **身份**必须在同一个事务里重新确认。会话 id 由调用方自选、且删除之后**可被别人重新登记**
|
|
220
226
|
* (`register` 是 INSERT … ON DUPLICATE KEY / ON CONFLICT,老行没了就是一条新行,新 owner):于是
|
|
221
227
|
* 「路由 `ownerOf` 已验权」这句话在两只并发 purge 交错时会过期 —— A 与 A' 同时通过属主门,A' 跑完整条
|
|
@@ -280,7 +286,7 @@ export declare class SqlRunStore {
|
|
|
280
286
|
* the run + RELEASE its task_active (unlock the session). Mirrors the checkpoint reaper's CAS-expire (≈ deny);
|
|
281
287
|
* idempotent across replicas. Returns count.
|
|
282
288
|
*
|
|
283
|
-
* 🔴 design/80 D-D (
|
|
289
|
+
* 🔴 design/80 D-D (blocker fix): SKIPS any row whose checkpoint is STILL pending — that row is
|
|
284
290
|
* owned by a checkpoint-state-driven sweep (the SLA deny-sweep for human/irreversible_ask, or
|
|
285
291
|
* failSuspendedWithExpiredCheckpoint once the checkpoint is expired). Without this guard, this time-based
|
|
286
292
|
* UPDATE fired at the SAME instant as the deny-sweep (deadline == createdAt + ttl, both keyed on the TTL),
|
|
@@ -132,8 +132,8 @@ export class SqlRunStore {
|
|
|
132
132
|
async setNeedsReview(taskId) {
|
|
133
133
|
await this.db.query(this.q("UPDATE task_run SET status = 'needs_review', updated_at = ? WHERE task_id = ? AND status IN ('running','needs_review')", "UPDATE task_run SET status = 'needs_review', updated_at = $1 WHERE task_id = $2 AND status IN ('running','needs_review')"), [new Date(), taskId]);
|
|
134
134
|
}
|
|
135
|
-
async markResuming(taskId) {
|
|
136
|
-
const { affected } = await this.db.query(this.q("UPDATE task_run SET status = 'running', cancel_requested = 0, preempt_requested = 0, updated_at = ? WHERE task_id = ? AND status IN ('suspended','needs_review')", "UPDATE task_run SET status = 'running', cancel_requested = 0, preempt_requested = 0,
|
|
135
|
+
async markResuming(taskId, instanceId) {
|
|
136
|
+
const { affected } = await this.db.query(this.q("UPDATE task_run SET status = 'running', cancel_requested = 0, preempt_requested = 0, instance_id = ?, updated_at = ? WHERE task_id = ? AND status IN ('suspended','needs_review')", "UPDATE task_run SET status = 'running', cancel_requested = 0, preempt_requested = 0, instance_id = $1, updated_at = $2 WHERE task_id = $3 AND status IN ('suspended','needs_review')"), [instanceId, new Date(), taskId]);
|
|
137
137
|
return affected === 1;
|
|
138
138
|
}
|
|
139
139
|
async getActiveTaskId(sessionId) {
|
|
@@ -8,7 +8,7 @@ export declare const PRESIGN_MAX_TTL_SEC = 604800;
|
|
|
8
8
|
* - keyed on the S3 SECRET KEY — server-held and stable, so the mapping is deterministic per deployment but
|
|
9
9
|
* unforgeable/unenumerable from outside (a URL holder cannot test principal guesses without the key);
|
|
10
10
|
* - the "sendfile-scope:" DOMAIN SEPARATOR pins this HMAC use — the same secret signs SigV4 requests, and a
|
|
11
|
-
* cross-protocol collision between "scope segment" and any other HMAC-of-
|
|
11
|
+
* cross-protocol collision between "scope segment" and any other HMAC-of-caller-controlled-string use is
|
|
12
12
|
* exactly what domain separation exists to prevent;
|
|
13
13
|
* - 32 hex chars (128 bits) is a NAMESPACE label, not a secret: the cross-tenant BIRTHDAY bound at even a
|
|
14
14
|
* billion tenants is ~n²/2^129 ≈ 2^-69 — negligible. (The earlier 16-hex/64-bit cut had a ~2.7% birthday
|
|
@@ -96,7 +96,7 @@ export interface PlacementAcquireOpts {
|
|
|
96
96
|
* · 方向①(claim 关门):`persisted` 在场 + 非 `requireExisting` + 调用方没带创建 placement ⇒ 拒。
|
|
97
97
|
* `requireExisting`(读/复活形,义务 5)与「创建调用及其幂等重取」(带 placement 的可信内部链)
|
|
98
98
|
* 是仅有的两个合法形。placement 不可变意味着带 placement 的参数**永远重写不了**任何东西——持久化
|
|
99
|
-
*
|
|
99
|
+
* 的那一份恒赢,所以放行它不开任何口子。
|
|
100
100
|
* · 方向②(镜像/squat):带 placement 的创建形回读到**缺席**或**异组**元组 ⇒ 拒。缺席 = 并发的
|
|
101
101
|
* claim 形抢先建出了普通会话(或带外写把元组抹了);异组 = 同 id 上坐着另一条委派链的转录。
|
|
102
102
|
* 两者都不许让「声明了 durable 分区」这句话在静默中变成谎。
|
|
@@ -50,7 +50,7 @@ export declare const MYSQL_ER_BAD_FIELD_CODE = "ER_BAD_FIELD_ERROR";
|
|
|
50
50
|
* 🔴 单一属主的理由与 {@link isDupKeyError} 逐字同源(本文件顶注的 single-semantic-multi-site-drift):
|
|
51
51
|
* 判据一旦复制,两处的识别集就会各自漂,而这条谓词的错判方向**特别贵** —— 它守的是一句**破坏性**的
|
|
52
52
|
* 错误指路(「去 DROP TABLE」)。超时、连接重置、取消、资源不足、列级权限被拒、表整个不存在,每一种
|
|
53
|
-
* 都会让探针抛错;把它们诊断成「删表重建」比它要挡的缺陷更贵(#340 codex
|
|
53
|
+
* 都会让探针抛错;把它们诊断成「删表重建」比它要挡的缺陷更贵(#340 codex 交叉复审 round2/round3
|
|
54
54
|
* [high] 两轮收窄后的终形)。认不出的一律**不是**缺列(fail-safe:宁可把一次真缺列报成原始错误)。
|
|
55
55
|
*
|
|
56
56
|
* MySQL 侧两个归因键都认(mysql2 的 `code`/`errno` 是同一件事的两种写法),与 dup-key 那条同姿势。
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Pool as MysqlPool } from "mysql2/promise";
|
|
2
2
|
import type { Pool as PgPool } from "pg";
|
|
3
|
-
import type { SessionStore, SessionPolicyStore,
|
|
4
|
-
import { type FileSnapshotBounds } from "@sema-agent/core";
|
|
3
|
+
import type { SessionStore, SessionPolicyStore, FileHistoryStore, WorkflowJournalStore, ToolResultStore } from "@sema-agent/core";
|
|
5
4
|
import { type OutcomeSink } from "./file-outcome-sink.js";
|
|
6
5
|
import { type InboxWarn } from "./workflow-run-store-sql.js";
|
|
7
6
|
import { FileRunStore } from "./file-run-store.js";
|
|
@@ -9,6 +8,7 @@ import { LocalCheckpointStore } from "./local-checkpoint-store.js";
|
|
|
9
8
|
import { FileResumeAnchorStore } from "./file-resume-anchor-store.js";
|
|
10
9
|
import { type ApprovalExemptionStore } from "./approval-exemption-store.js";
|
|
11
10
|
import { type ApprovalAskStore as ApprovalAskStoreType } from "./approval-ask-store-sql.js";
|
|
11
|
+
import { type MemoryOptOutGrantStore } from "./memory-optout-grant-store-sql.js";
|
|
12
12
|
import { type SendFileLedger } from "./send-file-ledger.js";
|
|
13
13
|
import { TiDBResumeAnchorStore, PgResumeAnchorStore } from "./resume-anchor-store-sql.js";
|
|
14
14
|
import type { ServiceConfig } from "../config-types.js";
|
|
@@ -17,6 +17,7 @@ import { TiDBCheckpointStore, PgCheckpointStore } from "./checkpoint-store-sql.j
|
|
|
17
17
|
import { TiDBImageIndex, PgImageIndex } from "./image-index-sql.js";
|
|
18
18
|
import { TiDBImageBake, PgImageBake } from "./image-bake-store-sql.js";
|
|
19
19
|
import { TiDBLeaderRunStore, PgLeaderRunStore } from "./leader-run-store-sql.js";
|
|
20
|
+
import type { DeviceStore } from "../device-store.js";
|
|
20
21
|
import { TiDBBreakerState, PgBreakerState } from "./breaker-state-sql.js";
|
|
21
22
|
import { TiDBCostQuota } from "./tidb-cost-quota.js";
|
|
22
23
|
import { PgCostQuota } from "./pg-cost-quota.js";
|
|
@@ -44,22 +45,27 @@ export type { ApprovalAskStore, AskRow, NewAskRow, AskTransitionPatch, DecideAsk
|
|
|
44
45
|
* it, core's InMemory omits it → optional). tidb/pg = durable twins; local = core's InMemorySessionPolicyStore (works
|
|
45
46
|
* local — rules need no cloud-only infra). The shared env-gated equivalence suite keeps the twins byte-matched to InMemory. */
|
|
46
47
|
export type ServiceSessionPolicyStore = SessionPolicyStore;
|
|
47
|
-
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*
|
|
55
|
-
|
|
48
|
+
/** S-15 per-edited-file rewind FileHistoryStore — core's interface PLUS the service extras only the SQL twins
|
|
49
|
+
* carry (optional so the local lane's core `FileFileHistoryStore` still fits). Replaces the retired E19
|
|
50
|
+
* whole-tree `ServiceFileSnapshotStore` (design/381: the boundary covers the TRACKED set only — files this
|
|
51
|
+
* session edited; there is no whole-tree manifest anymore). Also the 2c history-transfer store
|
|
52
|
+
* (exportHistory/importHistory + the blob negotiation faces below). */
|
|
53
|
+
export type ServiceFileHistoryStore = FileHistoryStore & {
|
|
54
|
+
/** 2c sync blob-face marker — see SqlFileHistoryStore.syncBlobFaces (codex R2-high: name-probing
|
|
55
|
+
* `putBlob` on an arbitrary store collides with the local core store's PRIVATE void helper). */
|
|
56
|
+
syncBlobFaces?: true;
|
|
57
|
+
/** E21 purge / overwrite-dst sync wipe: drop the WHOLE scope incl. v1 baselines + DV-14 markers
|
|
58
|
+
* (`reap(scope, [])` deliberately retains those while the scope lives — not a purge). SQL twins only. */
|
|
56
59
|
deleteBySession?(scope: string): Promise<number>;
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
59
|
-
* twins carry it (scheduled by the main.ts reaper, error-tolerant → eventual consistency); core's InMemory omits it. */
|
|
60
|
+
/** Async byte-GC for the file_history_blob domain (orphans left by reap/purge/lost mint races; grace-window
|
|
61
|
+
* protected — never a synchronous external-store delete). Reaper-driven; SQL twins only. */
|
|
60
62
|
sweepOrphanBlobs?(): Promise<number>;
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
+
/** 2c blob negotiation: upload one content-addressed blob ahead of importHistory. SQL twins only. */
|
|
64
|
+
putBlob?(hash: string, bytes: Uint8Array): Promise<import("@sema-agent/core").FileHistoryResult>;
|
|
65
|
+
/** 2c blob read face (scope-checked by the route against the session's history graph). SQL twins only. */
|
|
66
|
+
getBlob?(hash: string): Promise<Uint8Array | undefined>;
|
|
67
|
+
/** The subset of `hashes` already present — a cheap SQL INDEX probe (the import presence pre-check,
|
|
68
|
+
* never N full getBlob GETs). SQL twins only. */
|
|
63
69
|
hasBlobs?(hashes: string[]): Promise<Set<string>>;
|
|
64
70
|
};
|
|
65
71
|
/** SVC-2 (core CORE-7) WorkflowJournalStore — core's interface PLUS the service `deleteByRun` (GC extra). tidb/pg =
|
|
@@ -159,9 +165,38 @@ export interface StoreBackend {
|
|
|
159
165
|
/** E6 durable SessionPolicyStore (operator-tightened per-session tool rules). REQUIRED on all backends — works local
|
|
160
166
|
* (local = core's InMemorySessionPolicyStore), like resumeAnchor. Backs core's RunnerDeps.sessionPolicyStore. */
|
|
161
167
|
sessionPolicy(): ServiceSessionPolicyStore;
|
|
162
|
-
/**
|
|
163
|
-
* store). REQUIRED on all backends (local = core's
|
|
164
|
-
|
|
168
|
+
/** S-15 per-edited-file rewind FileHistoryStore (trackEdit/annulTrack/snapshot/restore over the tracked set;
|
|
169
|
+
* also the 2c history-transfer store). REQUIRED on all backends (local = core's file-backed
|
|
170
|
+
* `FileFileHistoryStore` under the data root). Backs core's RunnerDeps.fileHistoryStore. */
|
|
171
|
+
fileHistory(): ServiceFileHistoryStore;
|
|
172
|
+
/** design/383 §3.1 / S-50 §一(S-49 S-2):per-principal **memory-capture opt-out 授权表**
|
|
173
|
+
* (`memory_optout_grant`)—— `RuntimeCaps.allowMemoryOptOut` 的本地 verdict 源(三层折叠:per-principal
|
|
174
|
+
* 行 → 部署缺省哨兵行 → 代码缺省 allow)+ `/v1/admin/memory-optout` 四端点的持久层。
|
|
175
|
+
*
|
|
176
|
+
* 🔴 **可选**(与 permissionRule 同族):SQL 后端 = 双方言 twin;`local` 车道 **undefined**,且缺席是一个
|
|
177
|
+
* 真答案 —— 没有 verdict 源时 `allowMemoryOptOut` 键不合成(core 读「无 per-principal 限制」,`open` 姿态
|
|
178
|
+
* 下 opt-out 照常生效),而 `MEMORY_CAPTURE_POLICY=governed` 在启动期对无源部署拒启
|
|
179
|
+
* (`boot/runtime-caps.ts` `assertMemoryCapturePolicyWirable`)。刻意**不给 local 一个 in-memory twin**:
|
|
180
|
+
* 一条授权行是「operator 写下的合规事实」,进程内 Map 会在重启时静默丢掉它,下一次解析读到的是
|
|
181
|
+
* 「没有行 ⇒ 代码缺省 allow」—— 那是**放宽面**上的静默降级(permissionRule 那条禁令的同一理由)。
|
|
182
|
+
* File 形候真实的 local+governed 部署需求出现再落(候需求档)。 */
|
|
183
|
+
memoryOptOutGrant(): MemoryOptOutGrantStore | undefined;
|
|
184
|
+
/** S-1 legacy-epoch discriminant (core `RunnerDeps.legacyRewindBoundaryProbe`): does the RETIRED whole-tree
|
|
185
|
+
* snapshot epoch hold a boundary for (sessionId, entryId)? Reads the legacy `snapshot_manifest` table.
|
|
186
|
+
* SQL backends only (local never had a whole-tree epoch table). Diagnostic input on the refusal path —
|
|
187
|
+
* a probe that cannot tell answers undefined (reads exactly like an absent probe), never throws. */
|
|
188
|
+
legacyRewindBoundaryProbe?(query: {
|
|
189
|
+
sessionId: string;
|
|
190
|
+
entryId: string;
|
|
191
|
+
}): Promise<boolean | undefined>;
|
|
192
|
+
/** Retired-epoch byte hygiene: GC grace-passed orphan `snapshot_blob` rows/objects (rows the retention
|
|
193
|
+
* lane's `snapshot_manifest` sweeps keep orphaning — without this leg their BYTES would outlive every
|
|
194
|
+
* manifest reference forever, incl. MinIO objects). SQL backends only; reaper-driven. */
|
|
195
|
+
legacySnapshotBlobSweep?(): Promise<number>;
|
|
196
|
+
/** Retired-epoch right-to-delete: drop the session's LEGACY `snapshot_manifest` rows on the online E21
|
|
197
|
+
* purge (the retention lane already sweeps them on expiry — the two delete legs must converge on the
|
|
198
|
+
* same row set; a user's DELETE must reach old-epoch data too). Bytes go via the async legacy sweep. */
|
|
199
|
+
legacySnapshotPurge?(sessionId: string): Promise<number>;
|
|
165
200
|
/** SVC-2 (core CORE-7) durable WorkflowJournalStore — the cross-replica resume journal for an LLM-authored workflow
|
|
166
201
|
* (RunWorkflowOptions.journalStore). REQUIRED on all backends (tidb/pg = durable twins; local = core's
|
|
167
202
|
* InMemoryWorkflowJournalStore, single-replica/in-process).
|
|
@@ -212,6 +247,14 @@ export interface StoreBackend {
|
|
|
212
247
|
* 缺席 ⇒ endpoint 落到显式的单副本内存降级臂(见 `leader/endpoint.ts` 的 store 缺席注)。
|
|
213
248
|
*/
|
|
214
249
|
leaderRun?(): LeaderRunStore;
|
|
250
|
+
/**
|
|
251
|
+
* device lane 的四表店(device-executor-lane-v2 §6,车A-2 交店 / 车A-4 接线)。**SQL 后端专有**,
|
|
252
|
+
* `local` 刻意省略 —— 与 {@link leaderRun} 逐字同一条理由:config 层已经把「`REMOTE_EXEC=device` 且
|
|
253
|
+
* 无外部 SQL 店」整个组合拒启了(`device-durable-store` 不变量),所以 local 车道上根本不存在一个需要
|
|
254
|
+
* 这四张表的 device worker。消费点按 `backend?.device?.()` 取,缺席 ⇒ `boot/device-lane.ts` 的
|
|
255
|
+
* `assertDeviceLaneWired` 响亮拒启(**不是**静默落进程内 stub)。
|
|
256
|
+
*/
|
|
257
|
+
device?(): DeviceStore;
|
|
215
258
|
/**
|
|
216
259
|
* #270 —— 托管留存的**破坏性**三方法(core `ManagedRetentionCapability` 的 server 侧真身)。
|
|
217
260
|
* **SQL 后端专有**,`local` 刻意省略:那三条腿是跨十余张表的单事务级联,file/in-memory 形没有事务面
|
|
@@ -252,17 +295,13 @@ export interface StoreBackend {
|
|
|
252
295
|
* splits a fleet window into disjoint buckets = soft-limit leak). OPTIONAL: local backend omits (no fleet). */
|
|
253
296
|
dbNowMs?(): Promise<number>;
|
|
254
297
|
}
|
|
255
|
-
/** (c)(clay 拍 a+c,2026-07-27)
|
|
256
|
-
* SQL 后端(mysql|pg)把 blob
|
|
257
|
-
* (D-1 附件 →
|
|
258
|
-
*
|
|
259
|
-
*
|
|
298
|
+
/** (c)(clay 拍 a+c,2026-07-27;S-15 换店后域=file_history_blob,门与旋钮名不变)云形 blob 姿势门——
|
|
299
|
+
* boot 时调用(boot/stores.ts),纯函数可单测。SQL 后端(mysql|pg)把 blob 字节落单行的形已被两役
|
|
300
|
+
* 实证撞 mysql-协议包墙(D-1 附件 → 对象存储裁定):大字节归对象存储。per-file 历史的 blob=单文件
|
|
301
|
+
* 字节,同一堵墙(>6MiB 的被 track 文件在 TiDB 上落不进 SQL 行 ⇒ trackEdit 拒 ⇒ 默认 DV-14 下该次
|
|
302
|
+
* 编辑被拒)。云形缺 MinIO ⇒ fail-loud(启动了才是假安全);`SNAPSHOT_BLOB_ALLOW_SQL_BYTES=true` =
|
|
303
|
+
* 显式接受 SQL 店 + per-blob 帽(单机/测试台形)。local 后端不经此门(file 店,无 SQL 包墙)。 */
|
|
260
304
|
export declare function assertCloudSnapshotBlobPosture(kind: "mysql" | "pg" | "local", config: Pick<ServiceConfig, "snapshotBlobStore" | "snapshotBlobAllowSql">): void;
|
|
261
|
-
/** REWIND_SNAPSHOT_MAX_MB overrides ONLY the byte cap of core's DEFAULT_SNAPSHOT_BOUNDS (raise for
|
|
262
|
-
* big monorepos, lower for tight DBs). File-count / ignoreDirs stay core defaults — the knob answers the one
|
|
263
|
-
* field the 20G-workspace case needed; more knobs when a case shows up. Exported for tests
|
|
264
|
-
* (fractional-MB rounding + partial-merge semantics need a regression lock). */
|
|
265
|
-
export declare function snapshotBoundsFromConfig(config: ServiceConfig): FileSnapshotBounds;
|
|
266
305
|
/**
|
|
267
306
|
* 启动日志里「跨副本 or 进程内」那一格的标签。
|
|
268
307
|
*
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { AdoptionError, FileStorageBackend, FileSessionRepo
|
|
3
|
+
import { AdoptionError, FileStorageBackend, FileSessionRepo } from "@sema-agent/core";
|
|
4
4
|
import { FileWorkflowJournalStore } from "@sema-agent/core";
|
|
5
5
|
import { TiDBOutcomeLedger, PgOutcomeLedger } from "./outcome-ledger-sql.js";
|
|
6
6
|
import { FileOutcomeSink } from "./file-outcome-sink.js";
|
|
7
7
|
import { TiDBWorkflowRunStore, TiDBWorkflowCompletionInbox, TiDBWorkflowNotifyJournalStore, PgWorkflowRunStore, PgWorkflowCompletionInbox, PgWorkflowNotifyJournalStore } from "./workflow-run-store-sql.js";
|
|
8
|
-
import { FileRunStore } from "./file-run-store.js";
|
|
8
|
+
import { FileRunStore, RunStoreStrictHydrateError } from "./file-run-store.js";
|
|
9
9
|
import { LocalSessionStore } from "./local-session-store.js";
|
|
10
10
|
import { LocalCheckpointStore } from "./local-checkpoint-store.js";
|
|
11
11
|
import { FileResumeAnchorStore } from "./file-resume-anchor-store.js";
|
|
12
12
|
import { TiDBApprovalExemptionStore, PgApprovalExemptionStore, FileApprovalExemptionStore } from "./approval-exemption-store.js";
|
|
13
13
|
import { TiDBApprovalAskStore, PgApprovalAskStore } from "./approval-ask-store-sql.js";
|
|
14
14
|
import { InMemoryApprovalAskStore } from "./approval-ask-store-memory.js";
|
|
15
|
+
import { TiDBMemoryOptOutGrantStore, PgMemoryOptOutGrantStore } from "./memory-optout-grant-store-sql.js";
|
|
15
16
|
import { TiDBSendFileLedger, PgSendFileLedger, FileSendFileLedger } from "./send-file-ledger.js";
|
|
16
17
|
import { TiDBResumeAnchorStore, PgResumeAnchorStore } from "./resume-anchor-store-sql.js";
|
|
17
18
|
import { TiDBSessionPolicyStore, PgSessionPolicyStore } from "./session-policy-store-sql.js";
|
|
18
19
|
import { TiDBWorkflowJournalStore, PgWorkflowJournalStore } from "./workflow-journal-store-sql.js";
|
|
19
|
-
import {
|
|
20
|
+
import { TiDBFileHistoryStore, PgFileHistoryStore, FILE_HISTORY_BLOB_TABLE, FILE_HISTORY_BLOB_KEY_PREFIX } from "./file-history-store-sql.js";
|
|
20
21
|
import { MinioBlobBackend, SqlBlobBackend } from "./blob-backend.js";
|
|
21
22
|
import { createTidbPool, ensureSchema as ensureTidbSchema } from "./tidb-pool.js";
|
|
22
23
|
import { createPgPool, ensurePgSchema, pgPoolOptions } from "./pg-pool.js";
|
|
@@ -26,6 +27,7 @@ import { TiDBToolResultStore, PgToolResultStore } from "./tool-result-store-sql.
|
|
|
26
27
|
import { TiDBImageIndex, PgImageIndex } from "./image-index-sql.js";
|
|
27
28
|
import { TiDBImageBake, PgImageBake } from "./image-bake-store-sql.js";
|
|
28
29
|
import { TiDBLeaderRunStore, PgLeaderRunStore } from "./leader-run-store-sql.js";
|
|
30
|
+
import { TiDBDeviceStore, PgDeviceStore } from "./device-store-sql.js";
|
|
29
31
|
import { TiDBBreakerState, PgBreakerState } from "./breaker-state-sql.js";
|
|
30
32
|
import { TiDBCostQuota } from "./tidb-cost-quota.js";
|
|
31
33
|
import { PgCostQuota } from "./pg-cost-quota.js";
|
|
@@ -40,7 +42,21 @@ import { TiDBRetentionLaneStore, PgRetentionLaneStore } from "./retention-lane-s
|
|
|
40
42
|
import { createSqlPermissionRuleStores } from "./permission-rule-store-sql.js";
|
|
41
43
|
import { createFilePermissionRuleStores } from "./permission-rule-store-file.js";
|
|
42
44
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
43
|
-
function
|
|
45
|
+
function historyBlobBackend(config, dialect, pool) {
|
|
46
|
+
const s = config.snapshotBlobStore;
|
|
47
|
+
if (!s)
|
|
48
|
+
return new SqlBlobBackend(dialect, pool, config.snapshotBlobSqlMaxBytes, FILE_HISTORY_BLOB_TABLE);
|
|
49
|
+
return new MinioBlobBackend({
|
|
50
|
+
endpoint: s.endpoint,
|
|
51
|
+
bucket: s.bucket,
|
|
52
|
+
accessKey: s.accessKey,
|
|
53
|
+
secretKey: s.secretKey,
|
|
54
|
+
...(s.region ? { region: s.region } : {}),
|
|
55
|
+
keyPrefix: `${s.keyPrefix ?? ""}${FILE_HISTORY_BLOB_KEY_PREFIX}`,
|
|
56
|
+
...(s.presignTtlSec ? { presignTtlSec: s.presignTtlSec } : {}),
|
|
57
|
+
}, { dialect, pool, table: FILE_HISTORY_BLOB_TABLE });
|
|
58
|
+
}
|
|
59
|
+
function legacyBlobBackend(config, dialect, pool) {
|
|
44
60
|
const s = config.snapshotBlobStore;
|
|
45
61
|
if (!s)
|
|
46
62
|
return new SqlBlobBackend(dialect, pool, config.snapshotBlobSqlMaxBytes);
|
|
@@ -54,18 +70,51 @@ function snapshotBlobBackend(config, dialect, pool) {
|
|
|
54
70
|
...(s.presignTtlSec ? { presignTtlSec: s.presignTtlSec } : {}),
|
|
55
71
|
}, { dialect, pool });
|
|
56
72
|
}
|
|
73
|
+
async function legacyProbeImpl(dialect, pool, query) {
|
|
74
|
+
try {
|
|
75
|
+
if (dialect === "tidb") {
|
|
76
|
+
const [rows] = await pool.query("SELECT 1 FROM snapshot_manifest WHERE scope = ? AND snap_key = ? LIMIT 1", [query.sessionId, query.entryId]);
|
|
77
|
+
return rows.length > 0;
|
|
78
|
+
}
|
|
79
|
+
const r = await pool.query("SELECT 1 FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2 LIMIT 1", [query.sessionId, query.entryId]);
|
|
80
|
+
return r.rows.length > 0;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function legacyPurgeImpl(dialect, pool, sessionId) {
|
|
87
|
+
if (dialect === "tidb") {
|
|
88
|
+
const [r] = await pool.query("DELETE FROM snapshot_manifest WHERE scope = ?", [sessionId]);
|
|
89
|
+
return Number(r.affectedRows ?? 0);
|
|
90
|
+
}
|
|
91
|
+
const r = await pool.query("DELETE FROM snapshot_manifest WHERE scope = $1", [sessionId]);
|
|
92
|
+
return r.rowCount ?? 0;
|
|
93
|
+
}
|
|
94
|
+
async function legacySweepImpl(config, dialect, pool) {
|
|
95
|
+
const sql = "SELECT blob_hash FROM snapshot_blob WHERE blob_hash NOT IN (SELECT blob_hash FROM snapshot_manifest)";
|
|
96
|
+
let orphans;
|
|
97
|
+
if (dialect === "tidb") {
|
|
98
|
+
const [rows] = await pool.query(sql);
|
|
99
|
+
orphans = rows.map((r) => String(r.blob_hash));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const r = await pool.query(sql);
|
|
103
|
+
orphans = r.rows.map((row) => row.blob_hash);
|
|
104
|
+
}
|
|
105
|
+
if (orphans.length === 0)
|
|
106
|
+
return 0;
|
|
107
|
+
return legacyBlobBackend(config, dialect, pool).deleteBlobs(orphans);
|
|
108
|
+
}
|
|
57
109
|
export function assertCloudSnapshotBlobPosture(kind, config) {
|
|
58
110
|
if (kind === "local" || config.snapshotBlobStore || config.snapshotBlobAllowSql)
|
|
59
111
|
return;
|
|
60
|
-
throw new Error("cloud deployments (DB_BACKEND=mysql|pg) must configure object storage for
|
|
112
|
+
throw new Error("cloud deployments (DB_BACKEND=mysql|pg) must configure object storage for rewind file-history blobs " +
|
|
61
113
|
"(MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY): single-row SQL blob writes hit TiDB's " +
|
|
62
114
|
"txn-entry-size-limit (default 6MiB) / the mysql-protocol packet limit (bytes belong in object storage — " +
|
|
63
115
|
"same ruling as the D-1 attachment store). Single-box/test rigs may explicitly opt out with " +
|
|
64
|
-
"SNAPSHOT_BLOB_ALLOW_SQL_BYTES=true (a per-blob cap then applies;
|
|
65
|
-
|
|
66
|
-
export function snapshotBoundsFromConfig(config) {
|
|
67
|
-
const mb = config.rewindSnapshotMaxMb;
|
|
68
|
-
return mb !== undefined ? { ...DEFAULT_SNAPSHOT_BOUNDS, maxBytes: Math.round(mb * 1024 * 1024) } : DEFAULT_SNAPSHOT_BOUNDS;
|
|
116
|
+
"SNAPSHOT_BLOB_ALLOW_SQL_BYTES=true (a per-blob cap then applies; an over-cap tracked file's first-touch " +
|
|
117
|
+
"backup is refused typed, which under the default DV-14 policy refuses that edit).");
|
|
69
118
|
}
|
|
70
119
|
class TiDBBackend {
|
|
71
120
|
pool;
|
|
@@ -84,7 +133,11 @@ class TiDBBackend {
|
|
|
84
133
|
permissionRule() { return createSqlPermissionRuleStores(mysqlDriver(this.pool)); }
|
|
85
134
|
sendFileLedger() { return new TiDBSendFileLedger(this.pool); }
|
|
86
135
|
sessionPolicy() { return new TiDBSessionPolicyStore(this.pool); }
|
|
87
|
-
|
|
136
|
+
fileHistory() { return new TiDBFileHistoryStore(this.pool, historyBlobBackend(this.config, "tidb", this.pool), this.config.fileHistory); }
|
|
137
|
+
memoryOptOutGrant() { return new TiDBMemoryOptOutGrantStore(this.pool); }
|
|
138
|
+
legacyRewindBoundaryProbe(query) { return legacyProbeImpl("tidb", this.pool, query); }
|
|
139
|
+
legacySnapshotBlobSweep() { return legacySweepImpl(this.config, "tidb", this.pool); }
|
|
140
|
+
legacySnapshotPurge(sessionId) { return legacyPurgeImpl("tidb", this.pool, sessionId); }
|
|
88
141
|
workflowJournal() { return new TiDBWorkflowJournalStore(this.pool); }
|
|
89
142
|
outcomeSink() { return new TiDBOutcomeLedger(this.pool); }
|
|
90
143
|
workflowRun(onWarn) { return new TiDBWorkflowRunStore(this.pool, onWarn); }
|
|
@@ -96,6 +149,7 @@ class TiDBBackend {
|
|
|
96
149
|
imageIndex() { return new TiDBImageIndex(this.pool); }
|
|
97
150
|
imageBake() { return new TiDBImageBake(this.pool); }
|
|
98
151
|
leaderRun() { return new TiDBLeaderRunStore(this.pool); }
|
|
152
|
+
device() { return new TiDBDeviceStore(this.pool); }
|
|
99
153
|
retention(opts) { return new TiDBRetentionStore(this.pool, opts); }
|
|
100
154
|
retentionLane() { return new TiDBRetentionLaneStore(this.pool); }
|
|
101
155
|
breaker(onWriteFail) { return new TiDBBreakerState(this.pool, onWriteFail); }
|
|
@@ -124,7 +178,11 @@ class PgBackend {
|
|
|
124
178
|
permissionRule() { return createSqlPermissionRuleStores(pgDriver(this.pool)); }
|
|
125
179
|
sendFileLedger() { return new PgSendFileLedger(this.pool); }
|
|
126
180
|
sessionPolicy() { return new PgSessionPolicyStore(this.pool); }
|
|
127
|
-
|
|
181
|
+
fileHistory() { return new PgFileHistoryStore(this.pool, historyBlobBackend(this.config, "pg", this.pool), this.config.fileHistory); }
|
|
182
|
+
memoryOptOutGrant() { return new PgMemoryOptOutGrantStore(this.pool); }
|
|
183
|
+
legacyRewindBoundaryProbe(query) { return legacyProbeImpl("pg", this.pool, query); }
|
|
184
|
+
legacySnapshotBlobSweep() { return legacySweepImpl(this.config, "pg", this.pool); }
|
|
185
|
+
legacySnapshotPurge(sessionId) { return legacyPurgeImpl("pg", this.pool, sessionId); }
|
|
128
186
|
workflowJournal() { return new PgWorkflowJournalStore(this.pool); }
|
|
129
187
|
outcomeSink() { return new PgOutcomeLedger(this.pool); }
|
|
130
188
|
workflowRun(onWarn) { return new PgWorkflowRunStore(this.pool, onWarn); }
|
|
@@ -136,6 +194,7 @@ class PgBackend {
|
|
|
136
194
|
imageIndex() { return new PgImageIndex(this.pool); }
|
|
137
195
|
imageBake() { return new PgImageBake(this.pool); }
|
|
138
196
|
leaderRun() { return new PgLeaderRunStore(this.pool); }
|
|
197
|
+
device() { return new PgDeviceStore(this.pool); }
|
|
139
198
|
retention(opts) { return new PgRetentionStore(this.pool, opts); }
|
|
140
199
|
retentionLane() { return new PgRetentionLaneStore(this.pool); }
|
|
141
200
|
breaker(onWriteFail) { return new PgBreakerState(this.pool, onWriteFail); }
|
|
@@ -166,7 +225,7 @@ class LocalBackend {
|
|
|
166
225
|
try {
|
|
167
226
|
this.fileBackend = new FileStorageBackend({
|
|
168
227
|
root,
|
|
169
|
-
...(config.
|
|
228
|
+
...(config.fileHistory !== undefined ? { fileHistory: config.fileHistory } : {}),
|
|
170
229
|
onCorruptRead: (info) => this.onWarn?.("file_store_corrupt_read", {
|
|
171
230
|
path: info.path,
|
|
172
231
|
reason: info.reason,
|
|
@@ -175,7 +234,7 @@ class LocalBackend {
|
|
|
175
234
|
note: "a durable read was treated as ABSENT because the bytes were unreadable (never a plain ENOENT). " +
|
|
176
235
|
"Consequence depends on which store read it: session-policy = that run lost its SUBTRACTIVE session-rule " +
|
|
177
236
|
"layer (deployment policy / hooks / shell gate still applied); session repo = a listing silently omitted " +
|
|
178
|
-
"rows; file-
|
|
237
|
+
"rows; file-history = a rewind boundary or scope read as missing. Inspect the named path before deciding.",
|
|
179
238
|
}),
|
|
180
239
|
});
|
|
181
240
|
}
|
|
@@ -188,6 +247,7 @@ class LocalBackend {
|
|
|
188
247
|
this.sessionStore = new LocalSessionStore(new FileSessionRepo(this.fileBackend.root), this.fileBackend.root);
|
|
189
248
|
this.runStore = new FileRunStore(this.fileBackend.root, {
|
|
190
249
|
error: (msg, fields) => void (this.onError ?? this.onWarn)?.(msg, fields),
|
|
250
|
+
strictHydrate: config.runStoreStrictHydrate,
|
|
191
251
|
});
|
|
192
252
|
this.resumeAnchorStore = new FileResumeAnchorStore(this.fileBackend.root);
|
|
193
253
|
this.approvalExemptionStore = new FileApprovalExemptionStore(this.fileBackend.root);
|
|
@@ -237,7 +297,8 @@ class LocalBackend {
|
|
|
237
297
|
}
|
|
238
298
|
checkpointStoreInst;
|
|
239
299
|
sessionPolicy() { return this.fileBackend.sessionPolicyStore; }
|
|
240
|
-
|
|
300
|
+
fileHistory() { return this.fileBackend.fileHistoryStore; }
|
|
301
|
+
memoryOptOutGrant() { return undefined; }
|
|
241
302
|
workflowJournal() { return this.workflowJournalStore; }
|
|
242
303
|
outcomeSink() { return this.outcomeSinkInst; }
|
|
243
304
|
session() { return this.sessionStore; }
|
|
@@ -272,6 +333,8 @@ export async function openStoreBackendWithFallback(config, logger) {
|
|
|
272
333
|
catch (err) {
|
|
273
334
|
if (err instanceof AdoptionError)
|
|
274
335
|
throw err;
|
|
336
|
+
if (err instanceof RunStoreStrictHydrateError)
|
|
337
|
+
throw err;
|
|
275
338
|
if (!mayFallback)
|
|
276
339
|
throw err;
|
|
277
340
|
logger.warn("store_db_unreachable_fallback_memory", {
|
|
@@ -30,9 +30,11 @@ export interface RunRecord {
|
|
|
30
30
|
/** Which SYSTEM's credential submitted the run (oa/cc-mcp/portal…), derived at the auth gate — never
|
|
31
31
|
* self-declared. null = legacy single-token deployment (authenticated, no system identity). */
|
|
32
32
|
source: string | null;
|
|
33
|
-
/** The replica
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
/** The replica currently HOLDING this run — written at createRun and TAKEN OVER by markResuming (S-51: a
|
|
34
|
+
* cross-replica resume moves the seat, so a reader comparing it with its own instanceId is asking "is the
|
|
35
|
+
* live leg here NOW", not "who minted the row"). Lets a preempt/cancel/steer handler tell "this run is on
|
|
36
|
+
* MY replica" (consult the in-process inflight/steerable map authoritatively) from "held elsewhere" (fall
|
|
37
|
+
* back to the durable cross-replica flag). null for pre-column rows. */
|
|
36
38
|
instanceId: string | null;
|
|
37
39
|
/** First ~120 chars of the objective (secret-redacted at write) — the list's "what is this task" glance.
|
|
38
40
|
* null for rows predating the column. */
|
|
@@ -33,7 +33,7 @@ export declare function ensurePgTaskListSchema(q: (text: string, params?: unknow
|
|
|
33
33
|
* 语义 = 「这个 listKey 的清单**不存在过**」:item 行 + meta 行(高水位 `next_id` / 插入序
|
|
34
34
|
* `next_sort`)一并抹掉。⚠️ **meta 行必须一起删**:留着它,同一个 listKey 被重新登记后
|
|
35
35
|
* (会话 id 由调用方自选、删除后可被别人 `register`)新主的第一条任务会从旧主的高水位续号 ——
|
|
36
|
-
*
|
|
36
|
+
* 那是一条跨化身的信息外泄(「上一位在这里建过 47 条」)。
|
|
37
37
|
*
|
|
38
38
|
* 事务内先对 meta 行 `FOR UPDATE`(与 `mutate` 同一把分区锁)⇒ 与并发的清单写串行,不会删到
|
|
39
39
|
* 一半被插回新行。meta 行不在(从未写过任何任务)= 无锁可拿,DELETE 命中 0 行,幂等。
|