@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
@@ -0,0 +1,79 @@
1
+ /**
2
+ * design/158 A10 分段 —— **device lane 的装配件**(device-executor-lane-v2 §4.6/§4.7/§5,车A-4)。
3
+ *
4
+ * 一处建齐三件活对象:**店**(四表双库的 `DeviceStore`)→ **准入链**(`DeviceEnrollment`,含跨副本
5
+ * 限速座)→ **WS 汇聚端**(`DeviceWsHub`)。三件的依赖是单向的(hub 吃 store+enrollment),所以它们
6
+ * 必须在同一处按序建 —— 拆到三处装配点会让「hub 拿到的是不是同一只店」变成一个要读三个文件才答得出
7
+ * 的问题(而答错的后果是 presence 与租约各写各的)。
8
+ *
9
+ * ⚠️ **位置即契约**:必须在 `openStores`(拿 backend)之后、`createExecutionEnv`(第七臂要这三件)之前
10
+ * 调用;`attach(server)` 则要等 `createHttpServer` 造出裸 `http.Server` 之后(main.ts 的顺序)。
11
+ *
12
+ * 🔴 本模块**不**做车道选择:`REMOTE_EXEC=device` 与否由调用方判(main.ts)。它只回答一件事 ——
13
+ * 「这台机器上,device lane 的三件装配得起来吗」。装不起来就返回 `undefined`,由
14
+ * {@link assertDeviceLaneWired} 响亮拒启(#157:执行车道上没有静默降级臂)。
15
+ */
16
+ import { type DeviceEnrollRateLimiter, type DeviceEnrollment } from "../device-enrollment.js";
17
+ import type { DeviceStore } from "../device-store.js";
18
+ import { type DeviceWsHub } from "../device-ws-hub.js";
19
+ import type { ServiceConfig } from "../config.js";
20
+ import type { Logger } from "../observability/logger.js";
21
+ import type { Metrics } from "../observability/metrics.js";
22
+ import type { RateGate } from "../observability/rate-limit.js";
23
+ import type { StoreBackend } from "../plugins/store-backend.js";
24
+ export interface DeviceLaneCtx {
25
+ config: ServiceConfig;
26
+ logger: Logger;
27
+ metrics: Metrics;
28
+ backend: StoreBackend | undefined;
29
+ /** 本副本 id(写进连接租约行 —— §4.6 的跨副本排障面)。 */
30
+ replicaId: string;
31
+ /**
32
+ * 既有的**跨副本** RateGate(SQL 后端形 = `TiDBRateLimiter`/`PgRateLimiter`;单副本形 = 进程内
33
+ * `RateLimiter`)。A-2 的移交义务③:enrollment 的限速不自建第二套计数面,适配这一只。
34
+ *
35
+ * 🔴 传的是**取值器**不是值:那一只建在 main.ts 的下游(它要 `counterDegradeHook`),而 device lane
36
+ * 必须建在 `createExecutionEnv` 之前 —— 顺序对不上。取值器让「谁先建」不再是正确性问题:boot 期
37
+ * 不可能有 enroll 请求,取不到 ⇒ 座**抛**(fail-closed,enrollment 翻成 429),不是放行。
38
+ */
39
+ rateGate?: (() => RateGate | undefined) | undefined;
40
+ }
41
+ export interface DeviceLane {
42
+ store: DeviceStore;
43
+ enrollment: DeviceEnrollment;
44
+ hub: DeviceWsHub;
45
+ }
46
+ /**
47
+ * 把既有的**同步** {@link RateGate} 适配成 enrollment 要的异步座(A-2 移交义务③)。
48
+ *
49
+ * 🔴 为什么是适配而不是新建一只:限速面在本仓已有属主(`observability/rate-limit.ts` + 两支 SQL 孪生),
50
+ * 而 SQL 孪生是**跨副本**的 —— enrollment 自带的 `createFixedWindowRateLimiter` 诚实地只是每副本窗
51
+ * (它自己的头注就这么写),对 token 撞库来说「N 个副本 = N 倍配额」。适配既有的那一只,撞库预算才是
52
+ * 全局的一份。
53
+ *
54
+ * 键前缀 `device-enroll:` 是必须的:RateGate 的键空间与 HTTP 面的 principal 键**共用一张表**,不加前缀
55
+ * 会让一次 enrollment 尝试吃掉调用方的普通请求配额(反过来也一样)。
56
+ */
57
+ export declare function createDeviceEnrollRateLimiter(gate: () => RateGate | undefined): DeviceEnrollRateLimiter;
58
+ /**
59
+ * 建齐 device lane 的三件。返回 `undefined` = **建不起来**(没有 SQL 后端 ⇒ 没有四表店)。
60
+ *
61
+ * 只在调用方判定本部署是 device 车道时调用;非 device 车道调用它是纯浪费(会白建一只 WS server)。
62
+ */
63
+ export declare function createDeviceLane(ctx: DeviceLaneCtx): DeviceLane | undefined;
64
+ /**
65
+ * **装配后**的拒启门(car A-1 的 `device-executor-unwired` 换来的真门;先例逐字 =
66
+ * `boot/leader.ts` 的 `assertLeaderDurableStore`)。
67
+ *
68
+ * config 层的门①判的是「运维配没配 SQL 坐标」——那句话回答的是**意图**。它回答不了「这次启动**真的
69
+ * 拿到**了店吗」:`openStoreBackendWithFallback` 在 `SESSION_BACKEND=auto`(默认)下,SQL 连不上 /
70
+ * 建表失败 ⇒ `backend=undefined` + 一条 warn 就继续启动。那次启动里 device lane 的四表整个不在,
71
+ * 而 `boot/execution-env.ts` 的第七臂拿不到三件 ⇒ `executionEnvFactory` 落 `undefined` ⇒ core 拿
72
+ * `StubExecutionEnv`:**一台自称在员工设备上执行的 worker 实际在自己进程里跑,而且回执同形**。
73
+ * 那正是 #157「执行车道禁静默降级」点名的样板病,也正是 A-1 那条临时门当初要拦的东西 —— 换执法点,
74
+ * 不是取消执法。
75
+ *
76
+ * 抛而不是 warn:降级的代价由用户承担(他以为命令跑在自己机器上、看到的却是云上一个空目录的结果)。
77
+ */
78
+ export declare function assertDeviceLaneWired(isDeviceLane: boolean, lane: DeviceLane | undefined, hasFactory: boolean): void;
79
+ //# sourceMappingURL=device-lane.d.ts.map
@@ -0,0 +1,63 @@
1
+ import { createDeviceEnrollment, createFixedWindowRateLimiter } from "../device-enrollment.js";
2
+ import { createDeviceWsHub } from "../device-ws-hub.js";
3
+ export function createDeviceEnrollRateLimiter(gate) {
4
+ return {
5
+ check: async (key) => {
6
+ const g = gate();
7
+ if (!g)
8
+ throw new Error("device enrollment rate gate is not wired yet (boot window) — refusing rather than admitting unmetered enrollment attempts");
9
+ const verdict = g.check(`device-enroll:${key}`);
10
+ return { allowed: verdict.allowed, retryAfterSec: verdict.retryAfterSec };
11
+ },
12
+ };
13
+ }
14
+ export function createDeviceLane(ctx) {
15
+ const { config, logger, metrics, backend, replicaId } = ctx;
16
+ const store = backend?.device?.();
17
+ if (!store)
18
+ return undefined;
19
+ const knobs = config.remoteExec?.provider === "device" ? config.remoteExec : undefined;
20
+ const enrollment = createDeviceEnrollment({
21
+ store,
22
+ ...(ctx.rateGate ? { rateLimiter: createDeviceEnrollRateLimiter(ctx.rateGate) } : { rateLimiter: createFixedWindowRateLimiter({ limit: 10, windowMs: 60_000 }) }),
23
+ });
24
+ const hub = createDeviceWsHub({
25
+ store,
26
+ enrollment,
27
+ replicaId,
28
+ logger: {
29
+ warn: (msg, fields) => logger.warn(msg, fields),
30
+ error: (msg, fields) => logger.error(msg, fields),
31
+ info: (msg, fields) => logger.info(msg, fields),
32
+ },
33
+ metrics: { inc: (name, labels) => metrics.inc(name, labels) },
34
+ ...(knobs?.deliveryTimeoutMs != null ? { dispatchTimeoutMs: knobs.deliveryTimeoutMs } : {}),
35
+ ...(knobs?.reconnectGraceMs != null ? { reconnectGraceMs: knobs.reconnectGraceMs } : {}),
36
+ ...(knobs?.maxInflightPerDevice != null ? { maxInflightPerDevice: knobs.maxInflightPerDevice } : {}),
37
+ ...(knobs?.supersede ? { supersede: knobs.supersede } : {}),
38
+ });
39
+ logger.info("device_lane_enabled", {
40
+ replicaId,
41
+ maxInflightPerDevice: knobs?.maxInflightPerDevice ?? 4,
42
+ crossReplicaEnrollRateLimit: ctx.rateGate !== undefined,
43
+ });
44
+ return { store, enrollment, hub };
45
+ }
46
+ export function assertDeviceLaneWired(isDeviceLane, lane, hasFactory) {
47
+ if (!isDeviceLane)
48
+ return;
49
+ if (!lane) {
50
+ throw new Error("REMOTE_EXEC=device but the device lane could not be wired at boot [device-lane-unwired]: the SQL backend is " +
51
+ "configured yet this replica did not get one (an unreachable DB / failed DDL degrades to the in-memory fallback " +
52
+ "when SESSION_BACKEND=auto), so the device registry / session↔device binding / audit tables are absent. " +
53
+ "Refusing to serve a lane that would silently fall back to the in-process stub env — a worker claiming to " +
54
+ "execute on the employee's device while actually running inside its own process (#157). Fix the database " +
55
+ "(or unset REMOTE_EXEC) and restart (device-executor-lane-v2 §4.7)");
56
+ }
57
+ if (!hasFactory) {
58
+ throw new Error("REMOTE_EXEC=device but no DeviceExecutionEnv factory was built [device-lane-unwired]: the lane's stores are up " +
59
+ "yet `boot/execution-env.ts` produced no factory, so core would fall back to the in-process stub env. This is " +
60
+ "an assembly bug, not a configuration one — refusing to start rather than silently execute in-process (#157)");
61
+ }
62
+ }
63
+ //# sourceMappingURL=device-lane.js.map
@@ -68,7 +68,7 @@ export interface EngineLeaseDeps {
68
68
  lastActivityAt: () => number;
69
69
  /** 最后一条**附着流断开**的单调时刻(`drainState.lastAttachEndedAt`)。
70
70
  *
71
- * 🔴 codex 对抗复审 R1-F1(验真后修):`attachedStreams` 是**采样**面,`lastActivityAt` 记的是
71
+ * 🔴 codex 交叉复审 R1-F1(验真后修):`attachedStreams` 是**采样**面,`lastActivityAt` 记的是
72
72
  * **入场**(admission)时刻 —— 两者合起来仍漏一个形:一条**整个生命周期都落在两拍之间**的流
73
73
  * (活得比拍频短)从没被任何一拍看见,于是 `idleSince` 保持着这条流出现**之前**的旧值,而
74
74
  * 「已经没人附着满 lingerMs」这句承诺可以提前至多一整拍兑现。补这条**离场**触点之后,判据变成
@@ -5,19 +5,65 @@ import type { ServiceConfig } from "../config.js";
5
5
  import type { Logger } from "../observability/logger.js";
6
6
  import type { Metrics } from "../observability/metrics.js";
7
7
  import { PerTaskImageRegistry } from "../per-task-image.js";
8
+ import type { DeviceEnrollment } from "../device-enrollment.js";
9
+ import type { DeviceStore } from "../device-store.js";
10
+ import type { DeviceWsHub } from "../device-ws-hub.js";
8
11
  import { type TaskAttachmentStore } from "../plugins/task-attachment-store.js";
12
+ /**
13
+ * per-session 的 **cwd / shellEnv 登记簿**(TOC cwd seam + R-survey shellEnv seam)+ 它们的**租户围栏**。
14
+ *
15
+ * ── 为什么围栏不是可选的(codex R1-F1,判真)────────────────────────────────────────────────────
16
+ * 这两张表只按 `sessionId` 键控。在 host lane 上那是安全的:那条车道的闸带 `requirePrincipal !== true`,
17
+ * 整台机器就一个用户。**device lane 把这个前提撤掉了** —— 它的闸(`deviceCwdHonored`)刻意不要求单用户
18
+ * (员工各连各的设备)。于是一条真实的超范围路径出现了:
19
+ * ① sessionId 由**调用方自选**,而且 `DELETE /v1/sessions/:id` 之后可以被**别人** `register` 重新登记
20
+ * (`boot/session-faces.ts` 的化身围栏注逐字写着这件事);
21
+ * ② 会话删除的级联删的是库里的行,**够不着这两张进程内的表**;
22
+ * ③ 新主如果没有重发 `settings.env`,工厂读到的就是**上一位主人的** shell env —— 那是秘密面(NPM_TOKEN
23
+ * 一类),而且会被注进新主自己设备上的指令;cwd 也会跨化身残留。
24
+ *
25
+ * 所以围栏做在**身份已知**的那一处:每次提交经 resolve-spec 调 {@link SessionScopedRegistries.fenceSessionOwner}
26
+ * 对拍,换主即清。删除侧再加一条 {@link SessionScopedRegistries.dropSession}(E21 级联)——那条不是为了
27
+ * 关这个洞(读面必经提交、提交必过围栏),而是**右侧删除权**:一次删除之后,秘密不该还留在进程内存里。
28
+ *
29
+ * ⚠️ 诚实残余(如实登记,不假装关掉):两张表是**每副本进程内**的。围栏与清除都只在处理该请求/该删除的
30
+ * 那一个副本上发生。跨副本的陈值靠「读面必经本副本的一次提交 ⇒ 必过本副本的围栏」兜住 —— 这条推理成立
31
+ * 是因为 run 在受理它的那个副本上执行(ARCHITECTURE 副本模型)。device lane 的部署形本身也是单副本(§4.6)。
32
+ */
33
+ export interface SessionScopedRegistries {
34
+ readonly perSessionCwd: Map<string, string>;
35
+ readonly perSessionShellEnv: Map<string, Record<string, string>>;
36
+ setSessionCwd(sid: string, cwd: string): void;
37
+ setSessionShellEnv(sid: string, env: Record<string, string>): void;
38
+ /** 记录/对拍本会话的主人。与上次不同 ⇒ **先清两张表**再记新主(换主即清)。`null` = 匿名会话,
39
+ * 它同样是一个**稳定**的身份值(不是「未知」),所以匿名重入不会被判成换主。 */
40
+ fenceSessionOwner(sid: string, owner: string | null): void;
41
+ /** 会话删除:两张表 + 身份记录一起清(留着身份记录会让同 id 的下一位与一个幽灵身份比对成「同主」)。 */
42
+ dropSession(sid: string): void;
43
+ }
44
+ export declare function createSessionScopedRegistries(maxEntries?: number): SessionScopedRegistries;
45
+ /** device lane 的三件活对象(店 / 汇聚端 / 准入链)。缺席 = 本部署没有 device 车道。 */
46
+ export interface DeviceLaneWiring {
47
+ store: DeviceStore;
48
+ hub: DeviceWsHub;
49
+ enrollment: DeviceEnrollment;
50
+ }
9
51
  export interface ExecutionEnvCtx {
10
52
  config: ServiceConfig;
11
53
  logger: Logger;
12
54
  metrics: Metrics;
13
55
  taskAttachmentStore: TaskAttachmentStore | undefined;
56
+ /** 车A-4:device lane 的装配件(`REMOTE_EXEC=device` 且 SQL 店在场时由 main.ts 建好传进来)。 */
57
+ deviceLane?: DeviceLaneWiring;
14
58
  }
15
59
  export declare function createExecutionEnv(ctx: ExecutionEnvCtx): {
16
60
  perTaskImage: PerTaskImageRegistry;
17
61
  sessionEnvSelection: SessionEnvironmentSelection;
18
62
  perSessionCwd: Map<string, string>;
19
- setSessionCwd: (sid: string, value: string) => void;
20
- setSessionShellEnv: (sid: string, value: Record<string, string>) => void;
63
+ setSessionCwd: (sid: string, cwd: string) => void;
64
+ setSessionShellEnv: (sid: string, env: Record<string, string>) => void;
65
+ fenceSessionOwner: (sid: string, owner: string | null) => void;
66
+ dropSessionScoped: (sid: string) => void;
21
67
  executionEnvFactory: import("@sema-agent/core").ExecutionEnvFactory | undefined;
22
68
  worktreeReap: (() => Promise<void>) | undefined;
23
69
  sendUserFileTaskEnvs: TaskEnvRegistry | undefined;
@@ -7,6 +7,7 @@ import { createE2bLspManager } from "../lsp/e2b-manager.js";
7
7
  import { evictLspOnDestroy } from "../lsp-evict.js";
8
8
  import { PerTaskImageRegistry } from "../per-task-image.js";
9
9
  import { adbExecutionEnvFactory } from "../plugins/remote-env-adb.js";
10
+ import { deviceExecutionEnvFactory } from "../plugins/remote-env-device.js";
10
11
  import { e2bExecutionEnvFactory } from "../plugins/remote-env-e2b.js";
11
12
  import { hostExecutionEnvFactory, RemoteHostExecutionEnv } from "../plugins/remote-env-host.js";
12
13
  import { k8sExecutionEnvFactory } from "../plugins/remote-env-k8s.js";
@@ -27,15 +28,51 @@ function boundedSessionSetter(map, maxEntries) {
27
28
  map.delete(map.keys().next().value);
28
29
  };
29
30
  }
31
+ const MAX_CWD_SESSIONS = 4096;
32
+ export function createSessionScopedRegistries(maxEntries = MAX_CWD_SESSIONS) {
33
+ const perSessionCwd = new Map();
34
+ const perSessionShellEnv = new Map();
35
+ const owners = new Map();
36
+ const dropSession = (sid) => {
37
+ perSessionCwd.delete(sid);
38
+ perSessionShellEnv.delete(sid);
39
+ owners.delete(sid);
40
+ };
41
+ const touch = (sid, owner) => {
42
+ const next = owner === undefined ? (owners.has(sid) ? owners.get(sid) : null) : owner;
43
+ owners.delete(sid);
44
+ owners.set(sid, next);
45
+ while (owners.size > maxEntries)
46
+ dropSession(owners.keys().next().value);
47
+ };
48
+ return {
49
+ perSessionCwd,
50
+ perSessionShellEnv,
51
+ setSessionCwd: (sid, cwd) => {
52
+ perSessionCwd.set(sid, cwd);
53
+ touch(sid, undefined);
54
+ },
55
+ setSessionShellEnv: (sid, env) => {
56
+ perSessionShellEnv.set(sid, env);
57
+ touch(sid, undefined);
58
+ },
59
+ fenceSessionOwner: (sid, owner) => {
60
+ if (owners.has(sid) && owners.get(sid) !== owner) {
61
+ perSessionCwd.delete(sid);
62
+ perSessionShellEnv.delete(sid);
63
+ }
64
+ touch(sid, owner);
65
+ },
66
+ dropSession,
67
+ };
68
+ }
30
69
  export function createExecutionEnv(ctx) {
31
70
  const { config, logger, metrics, taskAttachmentStore } = ctx;
32
71
  const perTaskImage = new PerTaskImageRegistry();
33
72
  const sessionEnvSelection = new SessionEnvironmentSelection();
34
- const MAX_CWD_SESSIONS = 4096;
35
- const perSessionCwd = new Map();
36
- const setSessionCwd = boundedSessionSetter(perSessionCwd, MAX_CWD_SESSIONS);
37
- const perSessionShellEnv = new Map();
38
- const setSessionShellEnv = boundedSessionSetter(perSessionShellEnv, MAX_CWD_SESSIONS);
73
+ const sessionScoped = createSessionScopedRegistries();
74
+ const { perSessionCwd, setSessionCwd } = sessionScoped;
75
+ const { perSessionShellEnv, setSessionShellEnv } = sessionScoped;
39
76
  let executionEnvFactory;
40
77
  let worktreeReap;
41
78
  if (config.remoteExec?.provider === "e2b") {
@@ -85,6 +122,19 @@ export function createExecutionEnv(ctx) {
85
122
  ...(config.remoteExec.hostKey !== undefined ? { hostKey: config.remoteExec.hostKey } : {}),
86
123
  });
87
124
  }
125
+ else if (config.remoteExec?.provider === "device") {
126
+ if (ctx.deviceLane) {
127
+ executionEnvFactory = deviceExecutionEnvFactory({
128
+ hub: ctx.deviceLane.hub,
129
+ store: ctx.deviceLane.store,
130
+ enrollment: ctx.deviceLane.enrollment,
131
+ ...(config.remoteExec.execTimeoutMs != null ? { execTimeoutMs: config.remoteExec.execTimeoutMs } : {}),
132
+ perSessionCwd,
133
+ perSessionShellEnv,
134
+ logger,
135
+ });
136
+ }
137
+ }
88
138
  else if (config.remoteExec?.provider === "adb") {
89
139
  executionEnvFactory = adbExecutionEnvFactory({
90
140
  serial: config.remoteExec.serial,
@@ -236,6 +286,8 @@ export function createExecutionEnv(ctx) {
236
286
  logger.info("lsp_enabled", { provider: lspProvider });
237
287
  return {
238
288
  perTaskImage, sessionEnvSelection, perSessionCwd, setSessionCwd, setSessionShellEnv,
289
+ fenceSessionOwner: sessionScoped.fenceSessionOwner,
290
+ dropSessionScoped: sessionScoped.dropSession,
239
291
  executionEnvFactory, worktreeReap, sendUserFileTaskEnvs, lspManager,
240
292
  };
241
293
  }
@@ -75,7 +75,7 @@ export interface GovernanceSeamsCtx {
75
75
  * #270 车1 —— **本 build 是否真的接了留存执行器**(sweep lane;车2 的 `startRetentionLane()`)。
76
76
  * 缺席 = `false`。
77
77
  *
78
- * 🔴 为什么这一位必须存在(codex 对抗复审 R2-[critical],验真后修):core 的
78
+ * 🔴 为什么这一位必须存在(codex 交叉复审 R2-[critical],验真后修):core 的
79
79
  * `assertRetentionCapability` 只读**店的声明位**。#270 车1 让三只 SQL 店如实声明了 `"managed"`
80
80
  * (它们的行确实由托管留存删),于是那道门从此**放行** locked policy —— 可执行器还在车2 手里。
81
81
  * 两者之间的窗口如果什么都不做,后果正是本仓最忌的那种:部署自述「策略已受管」、审计与能力面
@@ -96,7 +96,7 @@ export declare function assertLockAssemblable(input: {
96
96
  deploymentMcpConfigured: boolean;
97
97
  }): void;
98
98
  /**
99
- * 治理面的**装配相容性**总判据(codex 对抗复审 R2-F2 起扩面)——锁与合规档位在这条轴上是同一件事:
99
+ * 治理面的**装配相容性**总判据(codex 交叉复审 R2-F2 起扩面)——锁与合规档位在这条轴上是同一件事:
100
100
  * 两者都可能把「本部署自己的装配」判成 core 门口的违规,而那不是「配得更严」,是**这台从此每条腿必失败**。
101
101
  *
102
102
  * 合规半场的判据来自亲核 core 的 prepare 门(`prepare-task` 的 compliance 段):被否决的能力若**已在
@@ -40,7 +40,7 @@ export interface LeaderCtx {
40
40
  */
41
41
  export declare function leaderK8sMinioGap(leaderEnabled: boolean, leaderProvider: string | undefined, hasS3: boolean): string[] | null;
42
42
  /**
43
- * #193 车4 件2 的**能力面**门(codex 对抗复审 R1-high①,验真后修)。
43
+ * #193 车4 件2 的**能力面**门(codex 交叉复审 R1-high①,验真后修)。
44
44
  *
45
45
  * config 层的门②判的是 `DB_BACKEND` 的字面值 —— 那句话回答的是「运维要没要 durable」。它回答不了
46
46
  * 「这次启动**真的拿到**了 durable 吗」:`openStoreBackendWithFallback` 在 `SESSION_BACKEND=auto`
@@ -2,8 +2,8 @@
2
2
  * #214 记忆边界不变式(板 [3492]② 立案,[3521] 裁1 与 core #151 同窗)—— 启动期把「结构性写不进的记忆
3
3
  * 部署形」说出来。
4
4
  *
5
- * 病根(test 仓 [3479] 实测,[3490]② 定位):core 的记忆工具面是 `memory_search`/`memory_get` 两只**只读**
6
- * 工具;**写**记忆没有专用工具 —— 模型用 fs 工具往记忆根写文件,harvest 再从那里收编。于是整条链的成立
5
+ * 根本问题(test 仓 [3479] 实测,[3490]② 定位):core 的记忆工具面是 `memory_search`/`memory_get`/
6
+ * `memory_index`(#493 第五单起第三只,枚举面,仍只读)三只**只读**工具;**写**记忆没有专用工具 —— 模型用 fs 工具往记忆根写文件,harvest 再从那里收编。于是整条链的成立
7
7
  * 条件是「记忆根落在该次任务的 fs 授权边界之内」。server 把根交给 core(`RunnerDeps.memoryEngineDir`)
8
8
  * 之后,没有任何一处保证这一点,也没有任何一处说过「这台部署的记忆写面够不着」。默认根是
9
9
  * `~/.ai-agent`(或 `AGENT_DATA_DIR`),默认围栏是任务 cwd —— 两者天然不相交:用户说「记住 X」,模型照
@@ -31,7 +31,7 @@ export interface ConsolidationDriverSeat {
31
31
  */
32
32
  export declare function resolveConsolidationDriverSeat(deps: MemoryConsolidationDriverDeps): ConsolidationDriverSeat;
33
33
  /**
34
- * **每 scope 单飞**(codex 对抗复审 [high],验真后修)—— 同一 scope 上在飞的那一轮被后来者**并入**,
34
+ * **每 scope 单飞**(codex 交叉复审 [high],验真后修)—— 同一 scope 上在飞的那一轮被后来者**并入**,
35
35
  * 而不是各起一轮。`create*`:返回带状态的活对象。
36
36
  *
37
37
  * ## 病(不是理论)
@@ -47,7 +47,7 @@ export declare function resolveConsolidationDriverSeat(deps: MemoryConsolidation
47
47
  * 本仓没有 —— 同 §遗留 遗-1 的那条基建缺口)。今天够得着这条阀门的部署形态是 file 记忆后端,那种部署
48
48
  * 实际上是单副本,所以进程内单飞覆盖了真实的失败形;但话按**它真正保证的**说,不按它常常够用说。
49
49
  *
50
- * 单飞**不是缓存**:那一轮落定(无论成败)即刻放行下一轮 —— 否则一次失败会把这个 scope 永久钉死。
50
+ * 单飞**不是缓存**:那一轮落定(无论成败)即刻放行下一轮 —— 否则一次失败会把这个 scope 永久卡住。
51
51
  */
52
52
  export declare function createPerScopeSingleFlight<T>(): (scope: string, run: () => Promise<T>) => Promise<T>;
53
53
  /**
@@ -80,7 +80,7 @@ export declare function assertConsolidationDriverWirable(input: {
80
80
  provenance?: "off" | "carry";
81
81
  }): void;
82
82
  /**
83
- * 阀门上了场之后的**可达性**告警两条(自查 + codex 对抗复审;姿态=warn 不拒启)。
83
+ * 阀门上了场之后的**可达性**告警两条(自查 + codex 交叉复审;姿态=warn 不拒启)。
84
84
  *
85
85
  * ## 病(结构性,而且**只在**这条阀门够得着的那种部署上成立)
86
86
  *
@@ -114,11 +114,11 @@ export declare function buildConsolidationValveAudit(input: {
114
114
  }>;
115
115
  };
116
116
  /**
117
- * 停因诊断行的**运维日志**投影(codex 对抗复审 R2-[medium],验真后修)。`build*`:纯数据。
117
+ * 停因诊断行的**运维日志**投影(codex 交叉复审 R2-[medium],验真后修)。`build*`:纯数据。
118
118
  *
119
119
  * `stopDetail` 已经不上 wire(`routes/memory-consolidation.ts` 的同名段:core 的 `driver_failed` 支把
120
120
  * 模型座抛出的 `Error.message` 逐字拼进去)。但**落日志同样要有信任边界** —— 集中式日志是人读的,
121
- * 而一段来路不明的文本混在自家诊断里,既可能夹带凭据,也可能把攻击者写的"操作建议"摆成本服务的口吻。
121
+ * 而一段来路不明的文本混在自家诊断里,既可能夹带凭据,也可能把不当调用方写的"操作建议"摆成本服务的口吻。
122
122
  * 三件:
123
123
  * ① 字段名 `untrustedDetail` **自陈**(一个叫 `detail` 的键会被下一个读日志的人当成本服务自己的诊断);
124
124
  * ② `redactSecrets` 脱敏(与审批卡片面同一只属主函数,不在本层另写一份模式表);
@@ -159,7 +159,7 @@ export interface MemoryConsolidationFaces {
159
159
  *
160
160
  * 本仓**没有**一个长命的 `MemoryEngine` 实例:`boot/stores.ts` 装配出来的 `memoryEngine` 是
161
161
  * `{ backend, root }` 二元组,真引擎由 core 的 Runner **每任务现构**。HTTP 面要一个引擎就得自己构。
162
- * 两只引擎不会各说各话:控制面由后端钉死(`backend.controlPlaneRoot`),gate / run 账 / plan 归档
162
+ * 两只引擎不会各说各话:控制面由后端写死(`backend.controlPlaneRoot`),gate / run 账 / plan 归档
163
163
  * 全住在那里,所以操作面引擎与每任务引擎看的是**同一本**账。
164
164
  *
165
165
  * ## `consolidation: {}` = 出厂缺省,**显式在场**
@@ -35,7 +35,7 @@ export interface OrgMemoryAdmissionWiring {
35
35
  * 🔴 A-057.40:这一席**故意**留快照,不跟 `config.projects` 的热应用走,理由是 core 根本不优先吃它:
36
36
  * 本仓每个请求都在 `boot/resolve-spec.ts` 现读 `config.projects[...].defaultScopes`(热加的项目当场
37
37
  * 进 spec),并在 `memory-scope.ts` 按 `originPolicy.multiTenant` 给每个 `org:` 键**盖章**;core 的
38
- * `memory-admission` 先看盖章、只有没盖章时才查这张表 ⇒ 单用户部署下热注册项目的 org 键恒被盖成
38
+ * `memory-admission` 先看盖章、只有没盖章时才查这张表 ⇒ 单用户部署下热注册项目的 org 键恒被覆盖为
39
39
  * `deployment`,这张表陈旧只会让 `governed` / `orgGovernedProvenance` 两个布尔偏保守,**不产生拒绝**。
40
40
  * 需要跟着热应用走的是另一个消费点(共享记忆读面)—— 见 {@link deploymentMemoryScopesLive}。
41
41
  */
@@ -23,7 +23,7 @@ export interface ParkedReviveGateDeps {
23
23
  * 部署级活体审批席 —— `RunnerDeps.onAsk` 的**同源铸法**(main.ts 递 `createRunnerDepsOnAsk(toolApproval)`,
24
24
  * 单一属主同 boot/runner-deps.ts)。
25
25
  *
26
- * ⚠️ **本仓确有 per-task `spec.onAsk` 装配点**(codex 对抗复审 R1-中1 验真纠正 —— 别照抄
26
+ * ⚠️ **本仓确有 per-task `spec.onAsk` 装配点**(codex 交叉复审 R1-中1 验真纠正 —— 别照抄
27
27
  * `createRunnerDepsOnAsk` 头注那句「bg/resume 两条腿没有 per-task 装配点」,那句话的射程只是那两条腿):
28
28
  * `http/routes/tasks.ts` 的宿主 sync/stream 腿在装配点写 `spec.onAsk = toolApproval.boundAsk({owner,
29
29
  * taskId, sessionId, legKey…})`(windowZero 形则写一个恒返 `"unavailable"` 的闭包)。core 冻的是
@@ -72,7 +72,7 @@ export interface ParkedReviveGateDeps {
72
72
  /**
73
73
  * core 1.396 的父约束席位形(`parkedResume.inheritedGate`)—— 从**载体自己的签名位推导**,绝不手写。
74
74
  *
75
- * 🔴 手写这个形状会**藏住轴**(codex 对抗复审 R1-高2 的真回归):第一版在这里手抄了一个只有
75
+ * 🔴 手写这个形状会**藏住轴**(codex 交叉复审 R1-高2 的真回归):第一版在这里手抄了一个只有
76
76
  * `parentConstraints` 的接口,于是 core 载体上那个 `shellGate` 槽在类型层面根本看不见,「shellGate 轴
77
77
  * 也要交回」这件事就无声地漏了一整刀。推导形让「core 又加了一条能收紧的轴」变成本仓的编译期/钉子面
78
78
  * 事件(载体键集钉见 test/parked-revive-gate-wiring.test.ts)。
@@ -124,7 +124,7 @@ export type RebuiltParentConstraint = NonNullable<RebuiltInheritedGate["parentCo
124
124
  * (test/parked-revive-e2e.test.ts);真 settings→park→赎回的端到端钉未建(候件,随收口件一起)。
125
125
  */
126
126
  /**
127
- * 🔴 A-057.44 第二半(codex 对抗复审 medium,验真后采纳)—— 席位多收一位**权威 principal**。
127
+ * 🔴 A-057.44 第二半(codex 交叉复审 medium,验真后采纳)—— 席位多收一位**权威 principal**。
128
128
  *
129
129
  * 为什么不能只看 `row.scope`:`principal ?? "default"` 这套约定在那一列上是**有损**的(匿名与一个真名叫
130
130
  * `default` 的租户同形),而匿名别名匹配(见 `parked-decide.ts` 的 `backgroundScopesForCheckpointScope`)
@@ -69,7 +69,7 @@ export interface ReapersCtx {
69
69
  rateLimiter: RateLimiter | RateLimiterStore | undefined;
70
70
  costQuota: CostQuota | CostQuotaStore | undefined;
71
71
  toolResultStore: ToolResultStoreFull | undefined;
72
- fileSnapshotStore: ReturnType<StoreBackend["fileSnapshot"]> | undefined;
72
+ fileHistoryStore: ReturnType<StoreBackend["fileHistory"]> | undefined;
73
73
  taskAttachmentStore: TaskAttachmentStore | undefined;
74
74
  imageBakes: ImageBake | undefined;
75
75
  worktreeReap: (() => Promise<void>) | undefined;
@@ -154,7 +154,7 @@ interface ReapSessionsInputs {
154
154
  * 的 id 会回落 transient(进程内 no-op),那会让 core 数出一次**从未发生**的 release
155
155
  * (`orphanPlacedReleased` 当场变成谎)而字节还在库里。故先按**持久化元组**判(一次 PK 探针),
156
156
  * placed 走 host 店的真删除;其余 id 原样保留 [1522] 裁定的双店语义。
157
- * · 🔴 探针本身失败 ⇒ **响亮抛**(codex 对抗复审 R1-[medium],验真后修:本函数的初版把探针错误
157
+ * · 🔴 探针本身失败 ⇒ **响亮抛**(codex 交叉复审 R1-[medium],验真后修:本函数的初版把探针错误
158
158
  * 改写成 `undefined` 再落路由店 —— 而重启后路由店不认识 placed id ⇒ transient no-op,core 却因为
159
159
  * release 正常 resolve 而 `orphanPlacedReleased++`。间歇性 DB 故障于是被**伪装成 GC 成功**:监控
160
160
  * 报着「收掉了 N 条孤儿」,durable 字节一条没动。这正是本函数顶注要防的那个形,初版自己踩了。
@@ -56,7 +56,7 @@ export function createPlacementAwareReapSessions(deps) {
56
56
  };
57
57
  }
58
58
  export function startReapers(ctx) {
59
- const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, rateLimiter, costQuota, toolResultStore, fileSnapshotStore, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, permissionRuleStores, getRunDenySweep, } = ctx;
59
+ const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, rateLimiter, costQuota, toolResultStore, fileHistoryStore, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, permissionRuleStores, getRunDenySweep, } = ctx;
60
60
  const leg = (p) => {
61
61
  if (p !== undefined)
62
62
  trackMaintenanceLeg(p);
@@ -85,7 +85,8 @@ export function startReapers(ctx) {
85
85
  const runsReapSuspendedGuard = createThrottledReaperCatch("runs_reap_suspended", logger);
86
86
  const checkpointReapCtxGuard = createThrottledReaperCatch("checkpoint_reap_ctx", logger);
87
87
  const toolResultReapOlderThanGuard = createThrottledReaperCatch("tool_result_reap_older_than", logger);
88
- const fileSnapshotSweepOrphanBlobsGuard = createThrottledReaperCatch("file_snapshot_sweep_orphan_blobs", logger);
88
+ const fileHistorySweepOrphanBlobsGuard = createThrottledReaperCatch("file_history_sweep_orphan_blobs", logger);
89
+ const legacySnapshotBlobSweepGuard = createThrottledReaperCatch("legacy_snapshot_blob_sweep", logger);
89
90
  const attachmentReapUnboundGuard = createThrottledReaperCatch("attachment_reap_unbound", logger);
90
91
  const sessionSweepStagingGuard = createThrottledReaperCatch("session_sweep_staging", logger);
91
92
  const imageBakesReapStaleGuard = createThrottledReaperCatch("image_bakes_reap_stale", logger);
@@ -100,6 +101,8 @@ export function startReapers(ctx) {
100
101
  const leaderRunStore = config.leaderEnabled ? backend?.leaderRun?.() : undefined;
101
102
  const scratchpadSweepStaleGuard = createThrottledReaperCatch("scratchpad_sweep_stale", logger);
102
103
  const approvalReconcileGuard = createThrottledReaperCatch("approval_reconcile", logger);
104
+ const approvalSuspendedPollGuard = createThrottledReaperCatch("approval_suspended_poll", logger);
105
+ let approvalSuspendedPollInFlight = false;
103
106
  const permissionRuleReapGuard = createThrottledReaperCatch("permission_rule_reap_expired", logger);
104
107
  let permissionRuleReapInFlight = false;
105
108
  let approvalReconcileInFlight = false;
@@ -220,9 +223,12 @@ export function startReapers(ctx) {
220
223
  leg(toolResultStore
221
224
  ?.reapOlderThan?.(Date.now() - config.toolResultTtlSec * 1000)
222
225
  ?.then(() => toolResultReapOlderThanGuard.onSuccess(), toolResultReapOlderThanGuard.onError));
223
- leg(fileSnapshotStore
226
+ leg(fileHistoryStore
224
227
  ?.sweepOrphanBlobs?.()
225
- .then(() => fileSnapshotSweepOrphanBlobsGuard.onSuccess(), fileSnapshotSweepOrphanBlobsGuard.onError));
228
+ .then(() => fileHistorySweepOrphanBlobsGuard.onSuccess(), fileHistorySweepOrphanBlobsGuard.onError));
229
+ leg(backend?.legacySnapshotBlobSweep?.()
230
+ .then(reapCount("legacy_snapshot_blobs_swept_total", {}))
231
+ .then(() => legacySnapshotBlobSweepGuard.onSuccess(), legacySnapshotBlobSweepGuard.onError));
226
232
  leg(taskAttachmentStore
227
233
  ?.reapUnbound(Date.now() - config.attachmentUnboundTtlMs)
228
234
  .then(reapCount("attachments_reaped_total", {}))
@@ -295,6 +301,13 @@ export function startReapers(ctx) {
295
301
  .then(() => approvalReconcileGuard.onSuccess(), approvalReconcileGuard.onError)
296
302
  .finally(() => (approvalReconcileInFlight = false)));
297
303
  }
304
+ if (toolApproval && !approvalSuspendedPollInFlight) {
305
+ approvalSuspendedPollInFlight = true;
306
+ leg(toolApproval
307
+ .pollSuspendedDecisions()
308
+ .then(() => approvalSuspendedPollGuard.onSuccess(), approvalSuspendedPollGuard.onError)
309
+ .finally(() => (approvalSuspendedPollInFlight = false)));
310
+ }
298
311
  if (!permissionRuleReapInFlight) {
299
312
  permissionRuleReapInFlight = true;
300
313
  leg(permissionRuleStores
@@ -18,6 +18,7 @@ import type { StoreBackend } from "../plugins/store-backend.js";
18
18
  import { type TaskAttachmentStore } from "../plugins/task-attachment-store.js";
19
19
  import { type createPrincipalEntitlementsClient } from "../runtime-caps-resolver.js";
20
20
  import { type OwnerAwareSessionStore } from "../security.js";
21
+ import type { RunnerDepsOnAsk } from "./runner-deps.js";
21
22
  type PrincipalCaps = ReturnType<typeof createPrincipalEntitlementsClient>;
22
23
  /** `resolveSpec` 原先从 `main()` 闭包里拿到的全部 boot 局部量。 */
23
24
  export interface ResolveSpecCtx {
@@ -40,6 +41,9 @@ export interface ResolveSpecCtx {
40
41
  perSessionCwd: Map<string, string>;
41
42
  setSessionCwd: (sid: string, cwd: string) => void;
42
43
  setSessionShellEnv: (sid: string, env: Record<string, string>) => void;
44
+ /** 两张 per-session 登记簿的**租户围栏**(codex R1-F1):每次提交对拍本会话的主人,换主即清。
45
+ * 论证(为什么 device lane 把这条从「不需要」变成「必需」)逐字见 `createSessionScopedRegistries` 头注。 */
46
+ fenceSessionOwner: (sid: string, owner: string | null) => void;
43
47
  hookLlm: ReturnType<typeof createHookLlm>["hookLlm"];
44
48
  hookAgent: HookLlmCall;
45
49
  fleetBus: FleetEventBus;
@@ -65,6 +69,17 @@ export interface ResolveSpecCtx {
65
69
  * ASK_QUESTION_ENABLED)时 durable question 门按活流上下文分腿、spec 不再 stamp QUESTION_AWAITS_RESUME
66
70
  * (否则 spec.onQuestion 恒遮蔽 RunnerDeps.onQuestion 的活人腿);缺席时行为与旧形逐字一致。 */
67
71
  liveQuestionFace: LiveQuestionFace | undefined;
72
+ /**
73
+ * S-46 件C —— 部署级**活体审批席**(`RunnerDeps.onAsk` 的同源铸法,main.ts 递
74
+ * `createRunnerDepsOnAsk(toolApproval)`;单一属主同 `boot/runner-deps.ts`,与 `parked-revive-gate.ts`
75
+ * 的 `approverSeat` 是同一格)。
76
+ *
77
+ * 本域**只**用于逐腿 `interactionPosture` 判据(见下方 `declaresInteractivePosture`),不参与任何
78
+ * 装配 —— spec 上的 per-task `onAsk` 由 `http/routes/tasks.ts` 在 resolveSpec **之后**写(bound 席),
79
+ * 而 core 的 posture 门读的是 `spec.onAsk ?? deps.onAsk`,两个装配点是**同一个条件**的两种表达
80
+ * (spec 位只在 `deps.toolApproval` 在场时才写,且写的也是函数)⇒ 拿 deps 位判「席位形」恒等。
81
+ */
82
+ approverSeat: RunnerDepsOnAsk | undefined;
68
83
  approvalExemptionStore: ReturnType<StoreBackend["approvalExemption"]> | undefined;
69
84
  singleUserAutoAcceptBaseline: boolean;
70
85
  checkpointStore: ReturnType<NonNullable<StoreBackend["checkpoint"]>> | undefined;