@sema-agent/server 7.148.0 → 7.150.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 (360) hide show
  1. package/MIGRATION.md +551 -9
  2. package/README.md +17 -2
  3. package/README.zh-CN.md +5 -1
  4. package/USAGE.md +126 -68
  5. package/dist/adoption/wire.d.ts +3 -3
  6. package/dist/approval-card.d.ts +33 -33
  7. package/dist/approval-gate-currency.d.ts +1 -1
  8. package/dist/approval-leg-origin.d.ts +33 -0
  9. package/dist/approval-leg-origin.js +4 -0
  10. package/dist/audit.d.ts +1 -1
  11. package/dist/audit.js +2 -2
  12. package/dist/auth-bridge.d.ts +9 -1
  13. package/dist/auth-bridge.js +4 -4
  14. package/dist/auth-keys.d.ts +10 -1
  15. package/dist/auth-keys.js +8 -0
  16. package/dist/background-session-reap.d.ts +1 -1
  17. package/dist/boot/deferred-sandbox-path-env.d.ts +5 -3
  18. package/dist/boot/deferred-sandbox-path-env.js +1 -2
  19. package/dist/boot/execution-env.js +10 -11
  20. package/dist/boot/execution-lane.d.ts +6 -4
  21. package/dist/boot/execution-lane.js +3 -1
  22. package/dist/boot/org-delivery.d.ts +3 -2
  23. package/dist/boot/parked-revive-gate.d.ts +5 -5
  24. package/dist/boot/parked-revive-gate.js +2 -3
  25. package/dist/boot/resolve-spec.d.ts +4 -5
  26. package/dist/boot/resolve-spec.js +66 -28
  27. package/dist/boot/retention-lane.d.ts +1 -1
  28. package/dist/boot/runner-deps.js +3 -1
  29. package/dist/boot/session-faces.js +21 -21
  30. package/dist/boot/stage-01-config.d.ts +1 -0
  31. package/dist/boot/stage-01-config.js +4 -3
  32. package/dist/boot/stage-02-web-admin-pull.d.ts +1 -0
  33. package/dist/boot/stage-02-web-admin-pull.js +10 -2
  34. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  35. package/dist/boot/stage-03-store-backend.js +4 -4
  36. package/dist/boot/stage-04-stores.d.ts +1 -0
  37. package/dist/boot/stage-05-execution-env.d.ts +1 -0
  38. package/dist/boot/stage-06-runners.d.ts +2 -1
  39. package/dist/boot/stage-06-runners.js +1 -2
  40. package/dist/boot/stage-07-capability-layer.d.ts +2 -1
  41. package/dist/boot/stage-07-capability-layer.js +1 -1
  42. package/dist/boot/stage-08-reapers.d.ts +2 -1
  43. package/dist/boot/stage-09-leader.d.ts +2 -1
  44. package/dist/boot/stage-10-http-server.d.ts +35 -2
  45. package/dist/boot/stage-10-http-server.js +9 -4
  46. package/dist/boot/stores.d.ts +3 -0
  47. package/dist/boot/stores.js +22 -19
  48. package/dist/boot/web-admin.d.ts +1 -1
  49. package/dist/capabilities/tool-defer.d.ts +1 -1
  50. package/dist/capability-facts.d.ts +70 -60
  51. package/dist/capability-facts.js +45 -49
  52. package/dist/card-settle-stored-scope.d.ts +56 -0
  53. package/dist/card-settle-stored-scope.js +27 -0
  54. package/dist/card-void.d.ts +7 -12
  55. package/dist/card-void.js +2 -7
  56. package/dist/config-catalog.d.ts +12 -0
  57. package/dist/config-catalog.js +37 -33
  58. package/dist/config-invariants.d.ts +5 -4
  59. package/dist/config-invariants.js +4 -4
  60. package/dist/config-types.d.ts +49 -16
  61. package/dist/config.d.ts +6 -5
  62. package/dist/config.js +35 -24
  63. package/dist/context-window-notice.d.ts +31 -0
  64. package/dist/context-window-notice.js +28 -0
  65. package/dist/deployment-governance.d.ts +7 -28
  66. package/dist/deployment-governance.js +1 -14
  67. package/dist/deployment-shape.d.ts +20 -11
  68. package/dist/deployment-shape.js +1 -0
  69. package/dist/device-ws-hub.d.ts +1 -0
  70. package/dist/device-ws-hub.js +64 -28
  71. package/dist/device-ws-protocol.d.ts +20 -6
  72. package/dist/device-ws-protocol.js +9 -2
  73. package/dist/engine-identity.d.ts +7 -1
  74. package/dist/engine-identity.js +5 -0
  75. package/dist/env-name-allowlist-knobs.d.ts +6 -5
  76. package/dist/env-name-allowlist-knobs.js +5 -0
  77. package/dist/execution-lane-caps.d.ts +20 -5
  78. package/dist/execution-lane-caps.js +18 -4
  79. package/dist/field-conflict.d.ts +2 -2
  80. package/dist/fleet/engine-signal.d.ts +14 -1
  81. package/dist/fleet/engine-signal.js +5 -1
  82. package/dist/fleet/fleet-reconciler.d.ts +1 -1
  83. package/dist/fleet/serving-lease.d.ts +18 -3
  84. package/dist/fleet/serving-lease.js +81 -35
  85. package/dist/fleet-client.d.ts +13 -11
  86. package/dist/fleet-client.js +53 -33
  87. package/dist/history-anchor.js +1 -1
  88. package/dist/hitl-delivery.d.ts +1 -3
  89. package/dist/hitl-delivery.js +1 -1
  90. package/dist/hooks/command-spawn.d.ts +7 -2
  91. package/dist/hooks/command-spawn.js +13 -6
  92. package/dist/hooks/hook-runner.d.ts +5 -0
  93. package/dist/hooks/hook-runner.js +11 -7
  94. package/dist/http/active-run-conflict.d.ts +3 -7
  95. package/dist/http/active-run-conflict.js +2 -2
  96. package/dist/http/admission.js +5 -5
  97. package/dist/http/approval-sweeps.js +5 -3
  98. package/dist/http/caller-trust.d.ts +2 -2
  99. package/dist/http/caller-trust.js +3 -3
  100. package/dist/http/checkpoint-ctx.d.ts +18 -63
  101. package/dist/http/checkpoint-ctx.js +1 -24
  102. package/dist/http/claim-heartbeat.d.ts +6 -6
  103. package/dist/http/claim-heartbeat.js +6 -6
  104. package/dist/http/claimed-run.d.ts +16 -9
  105. package/dist/http/claimed-run.js +8 -0
  106. package/dist/http/dispatch.js +7 -5
  107. package/dist/http/durable-run-leg.d.ts +8 -1
  108. package/dist/http/durable-run-leg.js +34 -15
  109. package/dist/http/execution-context.d.ts +115 -14
  110. package/dist/http/execution-context.js +130 -40
  111. package/dist/http/owner-access.d.ts +246 -21
  112. package/dist/http/owner-access.js +186 -8
  113. package/dist/http/principal-gate.d.ts +18 -11
  114. package/dist/http/principal-gate.js +3 -1
  115. package/dist/http/readiness.d.ts +4 -2
  116. package/dist/http/resume-ctx-read.d.ts +14 -3
  117. package/dist/http/resume-ctx-read.js +24 -3
  118. package/dist/http/resume-legs.d.ts +35 -3
  119. package/dist/http/resume-legs.js +262 -106
  120. package/dist/http/route-ctx.d.ts +29 -11
  121. package/dist/http/routes/a2a-serve.js +17 -14
  122. package/dist/http/routes/approvals-assistant.d.ts +3 -2
  123. package/dist/http/routes/approvals-assistant.js +11 -11
  124. package/dist/http/routes/capabilities.js +5 -4
  125. package/dist/http/routes/config-catalog.d.ts +1 -1
  126. package/dist/http/routes/diagnostics.d.ts +3 -1
  127. package/dist/http/routes/diagnostics.js +30 -5
  128. package/dist/http/routes/mcp-reconnect.js +2 -2
  129. package/dist/http/routes/memory-origin.d.ts +1 -1
  130. package/dist/http/routes/notify-wake.js +2 -3
  131. package/dist/http/routes/principal-inbox.d.ts +3 -1
  132. package/dist/http/routes/principal-inbox.js +3 -2
  133. package/dist/http/routes/projects.d.ts +1 -0
  134. package/dist/http/routes/projects.js +5 -3
  135. package/dist/http/routes/retention-ops.d.ts +1 -1
  136. package/dist/http/routes/run-permission-mode.d.ts +1 -1
  137. package/dist/http/routes/run-permission-mode.js +2 -2
  138. package/dist/http/routes/runs.d.ts +1 -1
  139. package/dist/http/routes/runs.js +33 -37
  140. package/dist/http/routes/session-memory-sync.js +4 -5
  141. package/dist/http/routes/session-sync.d.ts +24 -5
  142. package/dist/http/routes/session-sync.js +45 -37
  143. package/dist/http/routes/sessions-list.d.ts +1 -1
  144. package/dist/http/routes/sessions.d.ts +1 -1
  145. package/dist/http/routes/sessions.js +18 -44
  146. package/dist/http/routes/tasks.js +136 -101
  147. package/dist/http/routes/trace-usage.js +14 -14
  148. package/dist/http/routes/workflows.d.ts +1 -0
  149. package/dist/http/routes/workflows.js +20 -38
  150. package/dist/http/server.d.ts +12 -9
  151. package/dist/http/server.js +35 -6
  152. package/dist/http/stored-body-replay.d.ts +3 -24
  153. package/dist/http/stored-body-replay.js +2 -12
  154. package/dist/http/submit-ctx.d.ts +2 -2
  155. package/dist/http/submit-ctx.js +5 -5
  156. package/dist/http/submit-replay.d.ts +15 -2
  157. package/dist/http/submit-replay.js +10 -4
  158. package/dist/http/wire-types.d.ts +6 -3
  159. package/dist/index.d.ts +3 -1
  160. package/dist/index.js +1 -0
  161. package/dist/leader/endpoint.js +9 -8
  162. package/dist/leg-facts.d.ts +13 -2
  163. package/dist/leg-facts.js +2 -0
  164. package/dist/local-store-declaration.d.ts +54 -0
  165. package/dist/local-store-declaration.js +59 -0
  166. package/dist/mcp-assertion-header.d.ts +1 -1
  167. package/dist/mcp-assertion-header.js +1 -1
  168. package/dist/mcp-authorization-face.d.ts +18 -14
  169. package/dist/mcp-authorization-face.js +3 -2
  170. package/dist/memory-project-layer.js +1 -1
  171. package/dist/memory-scope.d.ts +4 -4
  172. package/dist/memory-scope.js +4 -4
  173. package/dist/memory-store-refusal.d.ts +1 -1
  174. package/dist/memory-sync-client.js +2 -2
  175. package/dist/model-context-window.d.ts +50 -11
  176. package/dist/model-context-window.js +20 -1
  177. package/dist/model-entry-refusal.d.ts +5 -4
  178. package/dist/model-entry-refusal.js +17 -3
  179. package/dist/model-reach-scope.d.ts +2 -1
  180. package/dist/observability/corrupt-read-seat.d.ts +1 -1
  181. package/dist/observability/fail-open.d.ts +19 -7
  182. package/dist/observability/fail-open.js +19 -7
  183. package/dist/observability/logger.d.ts +7 -0
  184. package/dist/observability/logger.js +3 -1
  185. package/dist/observability/metrics.js +1 -1
  186. package/dist/observability/run-terminal.d.ts +1 -1
  187. package/dist/observability/run-terminal.js +1 -0
  188. package/dist/observability/secret-env-scrub.d.ts +1 -1
  189. package/dist/observability/secret-env-scrub.js +1 -1
  190. package/dist/org-delivery-gate.d.ts +31 -6
  191. package/dist/org-delivery-gate.js +7 -4
  192. package/dist/owner-faces.d.ts +30 -5
  193. package/dist/owner-faces.js +2 -0
  194. package/dist/park-row-commit.d.ts +67 -0
  195. package/dist/park-row-commit.js +76 -0
  196. package/dist/parked-decide.d.ts +9 -7
  197. package/dist/parked-decide.js +25 -6
  198. package/dist/peer-directory.d.ts +1 -1
  199. package/dist/peer-seat.d.ts +1 -1
  200. package/dist/peer-seat.js +52 -59
  201. package/dist/plugins/adoption-log-sql.d.ts +3 -3
  202. package/dist/plugins/adoption-log-sql.js +19 -19
  203. package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
  204. package/dist/plugins/approval-ask-store-sql.js +3 -5
  205. package/dist/plugins/approval-exemption-skill-cutoff.d.ts +64 -0
  206. package/dist/plugins/approval-exemption-skill-cutoff.js +122 -0
  207. package/dist/plugins/approval-exemption-skill-m46.d.ts +0 -6
  208. package/dist/plugins/approval-exemption-skill-m46.js +6 -10
  209. package/dist/plugins/approval-exemption-store.d.ts +26 -1
  210. package/dist/plugins/approval-exemption-store.js +28 -19
  211. package/dist/plugins/ensure-schema-lock-policy.d.ts +36 -3
  212. package/dist/plugins/ensure-schema-lock-policy.js +75 -9
  213. package/dist/plugins/exec-path-law.d.ts +34 -0
  214. package/dist/plugins/exec-path-law.js +13 -0
  215. package/dist/plugins/file-run-store.d.ts +22 -9
  216. package/dist/plugins/file-run-store.js +51 -23
  217. package/dist/plugins/local-checkpoint-store.d.ts +8 -1
  218. package/dist/plugins/local-checkpoint-store.js +5 -1
  219. package/dist/plugins/local-data-root-admission.d.ts +52 -0
  220. package/dist/plugins/local-data-root-admission.js +100 -0
  221. package/dist/plugins/local-engine-high-water.d.ts +28 -7
  222. package/dist/plugins/local-engine-high-water.js +27 -22
  223. package/dist/plugins/local-session-store.d.ts +134 -49
  224. package/dist/plugins/local-session-store.js +149 -108
  225. package/dist/plugins/memory-run-store.d.ts +14 -9
  226. package/dist/plugins/memory-run-store.js +48 -22
  227. package/dist/plugins/one-time-migration-marker.d.ts +19 -3
  228. package/dist/plugins/one-time-migration-marker.js +7 -1
  229. package/dist/plugins/one-time-migrations.d.ts +110 -59
  230. package/dist/plugins/one-time-migrations.js +224 -136
  231. package/dist/plugins/org-delivery-quota-store.d.ts +34 -11
  232. package/dist/plugins/org-delivery-quota-store.js +29 -14
  233. package/dist/plugins/permission-rule-store-sql.d.ts +1 -1
  234. package/dist/plugins/permission-rule-store-sql.js +5 -9
  235. package/dist/plugins/pg-pool.d.ts +1 -1
  236. package/dist/plugins/pg-pool.js +14 -12
  237. package/dist/plugins/posix-shell-fs.js +4 -2
  238. package/dist/plugins/remote-env-adb.js +3 -2
  239. package/dist/plugins/remote-env-device.d.ts +3 -2
  240. package/dist/plugins/remote-env-device.js +8 -6
  241. package/dist/plugins/remote-env-e2b.d.ts +4 -8
  242. package/dist/plugins/remote-env-e2b.js +21 -15
  243. package/dist/plugins/remote-env-host.d.ts +51 -10
  244. package/dist/plugins/remote-env-host.js +97 -31
  245. package/dist/plugins/remote-env-k8s.js +7 -4
  246. package/dist/plugins/remote-env-local-docker.d.ts +6 -3
  247. package/dist/plugins/remote-env-local-docker.js +11 -25
  248. package/dist/plugins/remote-env-ssh.js +4 -3
  249. package/dist/plugins/remote-shell.d.ts +3 -1
  250. package/dist/plugins/remote-shell.js +3 -3
  251. package/dist/plugins/replica-registration.d.ts +35 -0
  252. package/dist/plugins/replica-registration.js +48 -0
  253. package/dist/plugins/retention-lane-store-sql.d.ts +1 -1
  254. package/dist/plugins/run-store-sql.d.ts +27 -8
  255. package/dist/plugins/run-store-sql.js +46 -18
  256. package/dist/plugins/scenario-prefix-switch-m42.d.ts +8 -12
  257. package/dist/plugins/scenario-prefix-switch-m42.js +9 -6
  258. package/dist/plugins/schema-consistency.d.ts +91 -5
  259. package/dist/plugins/schema-consistency.js +189 -7
  260. package/dist/plugins/schema-drift-sentence.d.ts +6 -5
  261. package/dist/plugins/schema-drift-sentence.js +13 -5
  262. package/dist/plugins/serving-requirement.d.ts +49 -0
  263. package/dist/plugins/serving-requirement.js +53 -0
  264. package/dist/plugins/session-placement.d.ts +5 -4
  265. package/dist/plugins/shell-sentinel-owner-m48.d.ts +22 -0
  266. package/dist/plugins/shell-sentinel-owner-m48.js +6 -0
  267. package/dist/plugins/stop-then-start-sentence.d.ts +55 -0
  268. package/dist/plugins/stop-then-start-sentence.js +51 -0
  269. package/dist/plugins/store-backend.d.ts +23 -9
  270. package/dist/plugins/store-backend.js +58 -24
  271. package/dist/plugins/store-contracts.d.ts +45 -5
  272. package/dist/plugins/store-contracts.js +19 -0
  273. package/dist/plugins/tidb-pool.d.ts +11 -6
  274. package/dist/plugins/tidb-pool.js +22 -12
  275. package/dist/plugins/workspace-custody.d.ts +44 -0
  276. package/dist/plugins/workspace-custody.js +47 -0
  277. package/dist/process-instance.d.ts +3 -0
  278. package/dist/process-instance.js +7 -0
  279. package/dist/project-context.d.ts +66 -0
  280. package/dist/project-context.js +70 -0
  281. package/dist/project-identity.d.ts +8 -7
  282. package/dist/project-identity.js +40 -17
  283. package/dist/project-layer-key.d.ts +13 -2
  284. package/dist/project-layer-key.js +9 -1
  285. package/dist/question.d.ts +9 -21
  286. package/dist/question.js +28 -19
  287. package/dist/read-deny-floor-notice.d.ts +36 -0
  288. package/dist/read-deny-floor-notice.js +40 -0
  289. package/dist/{plain-string-record.d.ts → request-declaration-disposition.d.ts} +9 -23
  290. package/dist/request-declaration-disposition.js +39 -0
  291. package/dist/run-cancel-card.d.ts +28 -0
  292. package/dist/run-cancel-card.js +20 -0
  293. package/dist/run-cancel-context.d.ts +10 -0
  294. package/dist/run-cancel-context.js +1 -0
  295. package/dist/run-local.js +28 -13
  296. package/dist/runs.d.ts +31 -7
  297. package/dist/runs.js +34 -16
  298. package/dist/security.d.ts +3 -3
  299. package/dist/server-secret-env.d.ts +34 -20
  300. package/dist/server-secret-env.js +61 -18
  301. package/dist/session-agent.d.ts +4 -12
  302. package/dist/session-agent.js +0 -6
  303. package/dist/session-children-retire.d.ts +6 -3
  304. package/dist/session-children-retire.js +2 -5
  305. package/dist/session-incarnation.d.ts +18 -4
  306. package/dist/session-incarnation.js +4 -1
  307. package/dist/session-notify-door.d.ts +4 -4
  308. package/dist/session-notify-door.js +5 -6
  309. package/dist/session-rules-seat.d.ts +4 -3
  310. package/dist/session-rules-seat.js +3 -11
  311. package/dist/session-sync.js +3 -3
  312. package/dist/session-workspace.d.ts +3 -3
  313. package/dist/store-admission.d.ts +33 -0
  314. package/dist/store-admission.js +49 -0
  315. package/dist/store-posture.d.ts +5 -0
  316. package/dist/store-posture.js +18 -1
  317. package/dist/stores-sql/checkpoint-store-sql.d.ts +22 -3
  318. package/dist/stores-sql/checkpoint-store-sql.js +109 -23
  319. package/dist/stores-sql/file-history-store-sql.d.ts +1 -1
  320. package/dist/stores-sql/file-history-store-sql.js +3 -5
  321. package/dist/stores-sql/retention-store-sql.d.ts +1 -1
  322. package/dist/stores-sql/tool-result-store-sql.d.ts +1 -1
  323. package/dist/stores-sql/tool-result-store-sql.js +4 -6
  324. package/dist/task-a2a.d.ts +3 -8
  325. package/dist/task-a2a.js +25 -27
  326. package/dist/task-cwd.d.ts +27 -10
  327. package/dist/task-cwd.js +3 -0
  328. package/dist/task-mcp.d.ts +3 -3
  329. package/dist/task-mcp.js +1 -1
  330. package/dist/task-settings.d.ts +13 -0
  331. package/dist/task-settings.js +14 -7
  332. package/dist/terminal.d.ts +2 -0
  333. package/dist/terminal.js +21 -6
  334. package/dist/tool-approval.d.ts +50 -41
  335. package/dist/tool-approval.js +96 -73
  336. package/dist/trace/engine-notice-wire.d.ts +38 -21
  337. package/dist/trace/engine-notice-wire.js +25 -4
  338. package/dist/trace/ledger-sink.d.ts +3 -1
  339. package/dist/trace/project.d.ts +8 -5
  340. package/dist/trace/project.js +11 -9
  341. package/dist/trace/projection-table.d.ts +33 -2
  342. package/dist/trace/redact.d.ts +12 -0
  343. package/dist/trace/redact.js +5 -1
  344. package/dist/trace/self-minted-wire.d.ts +268 -56
  345. package/dist/trace/self-minted-wire.js +110 -40
  346. package/dist/trace/tool-card.d.ts +3 -0
  347. package/dist/trace/tool-card.js +24 -0
  348. package/dist/trace/tool-output-media.d.ts +2 -1
  349. package/dist/unattended-permission-mode.d.ts +1 -1
  350. package/dist/web-admin-client/apply-effective.d.ts +1 -1
  351. package/dist/web-admin-client/apply-effective.js +22 -9
  352. package/dist/web-admin-client/effective-keys.d.ts +1 -1
  353. package/dist/web-admin-client/skills-mcp.d.ts +1 -1
  354. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +57 -29
  355. package/docs/error-text-lexicon.json +317 -0
  356. package/package.json +4 -2
  357. package/scripts/embed-minimal.mjs +183 -0
  358. package/wire-changes/server@7.149.0.json +372 -0
  359. package/wire-changes/server@7.150.0.json +335 -0
  360. package/dist/plain-string-record.js +0 -70
@@ -13,7 +13,7 @@ export declare function handleRuns(req: IncomingMessage, res: ServerResponse, ma
13
13
  /** 同上纪律(子代理 / 任务句柄 / HITL 应答段)。 */
14
14
  export declare function handleRunVerbs(req: IncomingMessage, res: ServerResponse, match: RouteMatch<RunVerbsRouteId>, ctx: RouteCtx): Promise<void>;
15
15
  /**
16
- * DESIGN-269 车2 件2 —— `POST /v1/runs` 的**建行 + 驱动**段,从路由体里原样搬出来的具名腿。
16
+ * [ref] 车2 件2 —— `POST /v1/runs` 的**建行 + 驱动**段,从路由体里原样搬出来的具名腿。
17
17
  *
18
18
  * 🔴 为什么提取:A2A 的 `message/send`(routes/a2a-serve.ts)必须走**同一条**提交链 —— 第二条提交
19
19
  * 路径就是第二个写者,而这条链上挂着 durable taskId 幂等重放、lease/治理窗 pre-admission、
@@ -6,6 +6,8 @@ import { uuidv7, mintCheckpointToken, CheckpointError, policyCannotAnswer, valid
6
6
  import { isHolderDeathVerdict, isTerminalRunStatus, isParkedRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
7
7
  import { askRowCarriesOwner } from "../../plugins/approval-ask-store-sql.js";
8
8
  import { LIVE_RUN_GATE_KINDS, TERMINAL_PARK_GATE_KINDS } from "../../plugins/checkpoint-store-face.js";
9
+ import { runCancelCardOf } from "../../run-cancel-card.js";
10
+ import { CARD_EXPIRE_OUTCOME, expireCardUnderStoredScope } from "../../card-settle-stored-scope.js";
9
11
  import { redactSecrets } from "../../trace/redact.js";
10
12
  import { isUuidV7, encodeCheckpointScope } from "../../security.js";
11
13
  import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from "../../orchestration/workflow-agent-steer.js";
@@ -28,15 +30,15 @@ import { ActorAssertionWire } from "@sema-agent/settings-schema";
28
30
  import * as facts from "../../capability-facts.js";
29
31
  import { assertNever } from "../route-ctx.js";
30
32
  import { readRunFace } from "../run-read-face.js";
31
- import { replayOfExistingRun } from "../submit-replay.js";
33
+ import { replayOfExistingRun, replayOfKey } from "../submit-replay.js";
32
34
  import { mintAndDriveDurableRun } from "../durable-run-leg.js";
33
- import { admitRunById, admitRunRow, enter, runRowAt } from "../owner-access.js";
35
+ import { SUBMISSION_CLAIM_FACE } from "../execution-context.js";
36
+ import { admitRunById, admitRunByIdem, enter, readVisibleRun, sessionFactsReadOf } from "../owner-access.js";
34
37
  import { keyAdmission, ownerCellOf, recoveredSubject } from "../../owner-faces.js";
35
38
  import { personOf } from "../../owner-brands.js";
36
39
  import { withSubject } from "../../observability/principal-context.js";
37
40
  import { readResumeCtx, resumeCtxUnreadableAnswer } from "../resume-ctx-read.js";
38
41
  import { storedBodyReplayAnswer, storedBodyReplayLogFields } from "../checkpoint-ctx.js";
39
- import { headReadOf } from "../../session-incarnation.js";
40
42
  import { serviceVersion } from "../../service-version.js";
41
43
  import { storedBodyReplayRefusal } from "../stored-body-replay.js";
42
44
  import { haltBodyIssue, steerBodyKeyIssue } from "../../request-key-closure.js";
@@ -103,7 +105,7 @@ function sendHandleMiss(res, run, thisInstanceId) {
103
105
  }
104
106
  return false;
105
107
  }
106
- async function streamRunEvents(req, res, deps, runStore, taskId) {
108
+ async function streamRunEvents(req, res, deps, runStore, entry, taskId) {
107
109
  await streamSseLog(req, res, {
108
110
  preamble: async (signal) => {
109
111
  const gate = resolveStreamApprovalGate({
@@ -132,10 +134,7 @@ async function streamRunEvents(req, res, deps, runStore, taskId) {
132
134
  return buildApprovalPreambleSseFrames(frames);
133
135
  },
134
136
  onPreambleFailure: (reason, err) => deps.logger?.warn?.("approval_preamble_failed", { taskId, reason, face: "run.events", error: err instanceof Error ? err.message : String(err ?? "") }),
135
- statusOf: async (id) => {
136
- const run = await runStore.getRun(id);
137
- return run ? runRowAt("run-events", runStore, run) : undefined;
138
- },
137
+ statusOf: async (id) => (await readVisibleRun(entry, runStore, id))?.row,
139
138
  getEvents: (id, after) => runStore.getEvents(id, after),
140
139
  formatEvent: (ev) => {
141
140
  const type = normalizeRunEventType(ev.type);
@@ -205,9 +204,9 @@ export async function handleRuns(req, res, match, ctx) {
205
204
  return;
206
205
  }
207
206
  const inflight = idemCache.peek(inflightKeyOf(idem, "durable", clientTaskId));
208
- const keyedRow = inflight ? undefined : await runStore.getRunByIdem(idem.key);
209
- if (inflight || keyedRow) {
210
- const resp = inflight ? await inflight : await replayOfExistingRun(keyedRow, { subject: submitter, idem, ...(clientTaskId !== undefined ? { clientTaskId } : {}) }, "durable", deps, runStore);
207
+ const replayed = inflight ? undefined : await replayOfKey({ idemKey: idem.key }, { subject: submitter, idem, ...(clientTaskId !== undefined ? { clientTaskId } : {}) }, "durable", deps, runStore);
208
+ const resp = inflight ? await inflight : replayed;
209
+ if (resp !== undefined) {
211
210
  sendJson(res, resp.status, resp.body);
212
211
  return;
213
212
  }
@@ -243,26 +242,23 @@ export async function handleRuns(req, res, match, ctx) {
243
242
  const entry = enter(req, res, deps, match.decl.id);
244
243
  if (entry === undefined)
245
244
  return;
246
- let run;
247
- let pathId;
245
+ let admitted;
248
246
  if (match.decl.id === "runs-by-idem") {
249
247
  const rawKey = new URLSearchParams(match.search).get("idempotencyKey");
250
248
  if (!rawKey) {
251
249
  sendError(res, 400, "request.query_invalid", "idempotencyKey is required: pass the raw Idempotency-Key the submission was sent with");
252
250
  return;
253
251
  }
254
- run = await runStore.getRunByIdem(idemKeyOf(rawKey, idemCallerOf(req, ctx), "run"));
252
+ admitted = await admitRunByIdem(entry, res, runStore, idemKeyOf(rawKey, idemCallerOf(req, ctx), "run"));
255
253
  }
256
254
  else {
257
- pathId = match.params.taskId;
258
- run = await runStore.getRun(pathId);
255
+ admitted = await admitRunById(entry, res, runStore, match.params.taskId);
259
256
  }
260
- const admitted = await admitRunRow(entry, res, runStore, run, pathId);
261
257
  if (admitted === undefined)
262
258
  return;
263
259
  const taskId = admitted.row.taskId;
264
260
  if (match.decl.id === "run-events") {
265
- await streamRunEvents(req, res, deps, runStore, taskId);
261
+ await streamRunEvents(req, res, deps, runStore, entry, taskId);
266
262
  }
267
263
  else {
268
264
  sendJson(res, 200, await readRunFace(deps, runStore, admitted.row));
@@ -285,16 +281,15 @@ export async function handleRuns(req, res, match, ctx) {
285
281
  return;
286
282
  const run = cancelAdmitted.row;
287
283
  const rs = runStore;
288
- const cancelSuspended = async () => {
284
+ const cancelSuspended = async (parked) => {
289
285
  const cs = deps.checkpointStore;
290
286
  let note = "cancelled while suspended (pending approval settled)";
291
287
  if (cs) {
292
- const token = await cs.findPendingTokenBySession(run.sessionId, undefined, { gateKinds: LIVE_RUN_GATE_KINDS, axis: "run" });
288
+ const token = await runCancelCardOf(cs, run.sessionId);
293
289
  if (token) {
294
- const cp = await cs.get(token);
295
- const won = cp ? await cs.expire(token, cp.scope) : false;
290
+ const won = (await expireCardUnderStoredScope(cs, token)) === CARD_EXPIRE_OUTCOME.expired;
296
291
  if (!won) {
297
- const now = await rs.getRun(taskId);
292
+ const now = (await readVisibleRun(entry, rs, taskId))?.row;
298
293
  const st = now?.status;
299
294
  if (st !== undefined && (isParkedRunStatus(st) || st === "running")) {
300
295
  sendError(res, 409, "conflict.approval_settled", "pending approval was settled concurrently (decided or expired) — re-check the run and retry cancel if it is still active", { taskId, status: st });
@@ -308,7 +303,7 @@ export async function handleRuns(req, res, match, ctx) {
308
303
  else {
309
304
  const claimed = await rs.markResuming(taskId, deps.instanceId ?? "default");
310
305
  if (!claimed) {
311
- const now = await rs.getRun(taskId);
306
+ const now = (await readVisibleRun(entry, rs, taskId))?.row;
312
307
  const st = now?.status;
313
308
  if (st !== undefined && (st === "running" || isParkedRunStatus(st))) {
314
309
  sendError(res, 409, "conflict.approval_settled", "pending approval was settled concurrently (decided or expired) — re-check the run and retry cancel if it is still active", { taskId, status: st });
@@ -326,7 +321,7 @@ export async function handleRuns(req, res, match, ctx) {
326
321
  }
327
322
  const err = note;
328
323
  try {
329
- const result = mintCancelledResult({ taskId, sessionId: run.sessionId, startedAtMs: Date.parse(run.createdAt), message: err, cause: "cancel_verb" });
324
+ const result = mintCancelledResult({ taskId, sessionId: run.sessionId, startedAtMs: Date.parse(run.createdAt), message: err, cause: "cancel_verb", parkedResult: parked });
330
325
  let lastErr;
331
326
  for (let attempt = 0;; attempt++) {
332
327
  try {
@@ -348,7 +343,7 @@ export async function handleRuns(req, res, match, ctx) {
348
343
  sendError(res, 500, "internal.cancel_not_terminalized", `cancel progressed (${note}) but could not terminalize the run row (retried) — retry cancel; a stuck row is reaped after the stale window (${e instanceof Error ? e.message : String(e)})`, { taskId });
349
344
  return;
350
345
  }
351
- const finalRow = await rs.getRun(taskId).catch(() => undefined);
346
+ const finalRow = (await readVisibleRun(entry, rs, taskId).catch(() => undefined))?.row;
352
347
  if (finalRow && finalRow.errorCode !== "cancelled" && isTerminalRunStatus(finalRow.status)) {
353
348
  sendJson(res, 202, { taskId, status: finalRow.status, errorCode: finalRow.errorCode ?? null, cancelRequested: finalRow.cancelRequested, note: `session unlocked; the run was terminalized concurrently (${finalRow.errorCode ?? finalRow.status}) before this cancel's write — reporting the actual ledger state` });
354
349
  return;
@@ -359,9 +354,9 @@ export async function handleRuns(req, res, match, ctx) {
359
354
  let flagged = await runStore.requestCancel(taskId, run.owner);
360
355
  const CANCEL_RECLASSIFY_ATTEMPTS = 3;
361
356
  for (let attempt = 0; !flagged; attempt++) {
362
- const now = await runStore.getRun(taskId);
357
+ const now = (await readVisibleRun(entry, runStore, taskId))?.row;
363
358
  if (now !== undefined && isParkedRunStatus(now.status)) {
364
- await cancelSuspended();
359
+ await cancelSuspended(now.result);
365
360
  return;
366
361
  }
367
362
  if (now?.status === "running" && attempt < CANCEL_RECLASSIFY_ATTEMPTS) {
@@ -379,7 +374,7 @@ export async function handleRuns(req, res, match, ctx) {
379
374
  sendJson(res, 202, { taskId, status: "cancelling", cancelRequested: true });
380
375
  }
381
376
  else if (isParkedRunStatus(run.status)) {
382
- await cancelSuspended();
377
+ await cancelSuspended(run.result);
383
378
  }
384
379
  else {
385
380
  sendJson(res, 202, { taskId, status: run.status, cancelRequested: run.cancelRequested, note: "run already terminal — cancel is a no-op" });
@@ -503,7 +498,7 @@ export async function handleRuns(req, res, match, ctx) {
503
498
  }
504
499
  if (code !== "steering.not_running")
505
500
  throw e;
506
- const freshBeforePark = await runStore.getRun(taskId).catch(() => undefined);
501
+ const freshBeforePark = (await readVisibleRun(entry, runStore, taskId).catch(() => undefined))?.row;
507
502
  const raced = await tryPark();
508
503
  if (raced === "parked") {
509
504
  sendParked(freshBeforePark?.status ?? run.status);
@@ -552,8 +547,8 @@ export async function handleRuns(req, res, match, ctx) {
552
547
  }
553
548
  return;
554
549
  }
555
- const sessionHeadOf = headReadOf(deps.sessionStorage);
556
- if (deps.checkpointStore && sessionHeadOf) {
550
+ const sessionFactsOf = sessionFactsReadOf(deps.sessionStorage);
551
+ if (deps.checkpointStore && sessionFactsOf) {
557
552
  const activeTaskId = await runStore.getActiveTaskId?.(run.sessionId).catch(() => undefined);
558
553
  if (activeTaskId !== undefined && activeTaskId !== null && activeTaskId !== taskId) {
559
554
  sendNotRunning(`run is ${run.status} and its session has an ACTIVE run (${activeTaskId}) — steer that run instead`);
@@ -603,7 +598,7 @@ export async function handleRuns(req, res, match, ctx) {
603
598
  mintResult.duplicateKey = true;
604
599
  return;
605
600
  }
606
- const leafId = await sessionHeadOf(run.sessionId).then((h) => h?.leafId).catch(() => undefined);
601
+ const leafId = await sessionFactsOf(run.sessionId).then((h) => h?.leafId).catch(() => undefined);
607
602
  if (leafId === undefined || leafId === null)
608
603
  return;
609
604
  const resumeRead = await readResumeCtx(cs.getCtx(run.sessionId), { sessionId: run.sessionId, opener: deps.checkpointCtxSealing });
@@ -777,7 +772,7 @@ export async function handleRuns(req, res, match, ctx) {
777
772
  catch (e) {
778
773
  if (e.code !== "steering.not_running")
779
774
  throw e;
780
- const freshRun = await runStore.getRun(taskId).catch(() => undefined);
775
+ const freshRun = (await readVisibleRun(entry, runStore, taskId).catch(() => undefined))?.row;
781
776
  sendNoLiveTurn(freshRun?.status ?? run.status, true);
782
777
  return;
783
778
  }
@@ -1507,9 +1502,9 @@ async function createDurableRunHeld(req, res, ctx, prepared, idem) {
1507
1502
  const submitter = gatedSubject(req, deps.config);
1508
1503
  const caller = { subject: submitter, idem, ...(clientTaskId !== undefined ? { clientTaskId } : {}) };
1509
1504
  if (clientTaskId) {
1510
- const existing = await runStore.getRun(clientTaskId);
1511
- if (existing)
1512
- return await replayOfExistingRun(existing, caller, "durable", deps, runStore);
1505
+ const replayed = await replayOfKey({ taskId: clientTaskId }, caller, "durable", deps, runStore);
1506
+ if (replayed !== undefined)
1507
+ return replayed;
1513
1508
  }
1514
1509
  if ((await leaseDenied(req, res, seat)) || (await usageWindowDenied(req, res, seat)))
1515
1510
  return null;
@@ -1526,6 +1521,7 @@ async function createDurableRunHeld(req, res, ctx, prepared, idem) {
1526
1521
  source,
1527
1522
  identity: runIdentityOf(req, deps.config, prepared.auth?.subject ?? null),
1528
1523
  runSlot,
1524
+ face: SUBMISSION_CLAIM_FACE,
1529
1525
  });
1530
1526
  const receipt = out.existing ? await replayOfExistingRun(out.existing, caller, "durable", deps, runStore) : out;
1531
1527
  await prepared.seat.close();
@@ -1,6 +1,6 @@
1
1
  import { memorySyncReportOf } from "@sema-agent/core";
2
2
  import { sendJson, sendError } from "../send.js";
3
- import { admitSessionById, enter } from "../owner-access.js";
3
+ import { admitSessionById, enter, latestRunIdOf, readVisibleSession } from "../owner-access.js";
4
4
  import { assertNever } from "../route-ctx.js";
5
5
  import * as facts from "../../capability-facts.js";
6
6
  import { leafOfAdmittedHead } from "../../session-incarnation.js";
@@ -94,7 +94,6 @@ export async function handleSessionMemorySync(req, res, match, ctx) {
94
94
  sendError(res, 400, "request.path_malformed", "session id segment is not valid percent-encoding");
95
95
  return;
96
96
  }
97
- const readHead = () => sessionStorage.headOf(sessionId);
98
97
  const admitted = await admitSessionById(entry, res, sessionStorage, sessionId);
99
98
  if (admitted === undefined)
100
99
  return;
@@ -122,10 +121,10 @@ export async function handleSessionMemorySync(req, res, match, ctx) {
122
121
  const quiet = async () => {
123
122
  if ((await runStore.getActiveTaskId(sessionId)) !== undefined)
124
123
  return false;
125
- if ((await runStore.latestRunForSession(sessionId))?.taskId !== observedTaskId)
124
+ if ((await latestRunIdOf(runStore, sessionId)) !== observedTaskId)
126
125
  return false;
127
- const now = await readHead();
128
- return now !== undefined && leafOfAdmittedHead(now) === sourceRevision;
126
+ const now = await readVisibleSession(entry, sessionStorage, sessionId);
127
+ return now !== undefined && leafOfAdmittedHead(now.row) === sourceRevision;
129
128
  };
130
129
  const fenced = () => reportOf({ target, sourceRevision, scopes, refused: { code: "session_active", message: SESSION_ACTIVE_SYNC_MESSAGE } });
131
130
  let report;
@@ -36,16 +36,35 @@ export type PendingStaging = {
36
36
  /** The owner cell the gate judged when it opened this staging (head-read reading `headOf(id)?.owner`: `undefined` = no such session) — the commit lands only on
37
37
  * this same cell (`StagedCommitOptions.expectedOwner`, `session-sync-kernel.ts`); `owner` above (the re-stamped importing principal) is derived from it for a fleet caller. */
38
38
  expectedOwner: string | null | undefined;
39
+ /** 开这张暂存时占下的导入租约的键({@link importLeaseKey});续期与放掉都按它,不再从会话 id 重算。 */
40
+ leaseKey: string;
41
+ };
42
+ /**
43
+ * 导入租约的键 = 这次导入**落在哪一格属主上**(与提交的属主 CAS 同一格)+ 会话 id。一条读法:
44
+ * · id 被占着(活行 ∪ 在删行 —— 「被占着」的唯一读法,门读的 `occupant`)⇒ 落点是住客的属主格(`null` = 无主):所有碰得到这一行的调用方(属主、fleet、存量无主互认)共一把 —— 同一份会话上第二份在飞导入,谁发起的都 409;
45
+ * · id 空闲 ⇒ 还没有谁的会话,落点是这位调用方自己将成的属主(`importingPrincipal`):各占各的,先提交者得(提交的属主 CAS 要求那一格仍空着,输的那一份
46
+ * 在提交前复判被拒)。一个主体的在飞导入既不挡、也不透给另一个主体 —— 租约键不含主体时,空闲 id 上任一主体都能占位,并从 409 读到「此 id 此刻有人在导」。
47
+ * 键的主体只来自门读的 `occupant` 与 `importingPrincipal`(两者都派生自属主存取器的 `listFilterOf` 与门读的会话面,不另读调用方),且占租约在 `pushGateOk` 之后。
48
+ */
49
+ export declare function importLeaseKey(landingOwner: string | null, sessionId: string): string;
50
+ /** 一把导入租约:最后活动时刻 + 持有它的暂存 + 它守的会话 id(清扫与运维日志用,不从键里反解)。 */
51
+ export type ImportLease = {
52
+ at: number;
53
+ stagingId: string;
54
+ sessionId: string;
39
55
  };
40
56
  /** 单域独占状态 + 它的两个操作。每个 server 实例一份。 */
41
57
  export interface SessionSyncLocal {
42
58
  pendingStagings: Map<string, PendingStaging>;
43
- importLeases: Map<string, {
44
- at: number;
45
- stagingId: string;
46
- }>;
47
- touchImportLease(sessionId: string): void;
59
+ importLeases: Map<string, ImportLease>;
60
+ touchImportLease(leaseKey: string): void;
48
61
  cleanupStaging(stagingId: string): void;
62
+ /** 把闲置 ≥ `staleMs` 的租约连同它的暂存登记一并放掉(`staleMs <= 0` = 关,什么都不清);返回被放掉的(供运维日志)。 */
63
+ sweepStaleLeases(staleMs: number): Array<{
64
+ readonly sessionId: string;
65
+ readonly stagingId: string;
66
+ readonly ageMs: number;
67
+ }>;
49
68
  }
50
69
  export declare function createSessionSyncLocal(): SessionSyncLocal;
51
70
  /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。 */
@@ -7,10 +7,9 @@ import { classifySyncRelationshipByIds, createStagedLogFeed, MalformedImportEntr
7
7
  import { isUuidShape } from "../../security.js";
8
8
  import { sendJson, sendError, sendSessionError, sendSessionPolicyError, logRequestError, msg } from "../send.js";
9
9
  import { StampedSessionArchiveError } from "../../session-error-wire.js";
10
- import { readSessionAgent } from "../../session-agent.js";
11
10
  import { buildDisplaySafeUrl } from "../../trace/redact.js";
12
11
  import { storeFaultClassOf } from "../../plugins/store-fault.js";
13
- import { enter, judgeSession, refuse } from "../owner-access.js";
12
+ import { enter, readSessionAgent, readSyncGate, refuse } from "../owner-access.js";
14
13
  import { listFilterOf, ownerKeyOf, sameKey } from "../../owner-faces.js";
15
14
  const SYNC_BLOB_MAX_BYTES = 64 * 1024 * 1024;
16
15
  const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
@@ -18,26 +17,43 @@ import { SHA256_HEX_RE } from "../../digest-form.js";
18
17
  import { assertNever } from "../route-ctx.js";
19
18
  import { readSessionRulesRecords, describeSessionRulesRecordProblem } from "../../session-policy-wire.js";
20
19
  import { readSessionPinned, readSessionTitle } from "../../session-row.js";
21
- import { sessionFaceOfHead } from "../../session-incarnation.js";
22
20
  import { defaultTaskRegistry } from "@sema-agent/core";
23
21
  import { SessionActiveRunError, SessionForkInProgressError, SessionWriterPresentError } from "../../execution-placement.js";
24
22
  import { probeSessionChildren, sessionTenantScope } from "../../session-children-retire.js";
25
23
  import { liveChildrenRefusal } from "./background.js";
24
+ export function importLeaseKey(landingOwner, sessionId) {
25
+ return JSON.stringify([landingOwner, sessionId]);
26
+ }
26
27
  export function createSessionSyncLocal() {
27
28
  const pendingStagings = new Map();
28
29
  const importLeases = new Map();
29
- const touchImportLease = (sessionId) => {
30
- const cur = importLeases.get(sessionId);
30
+ const touchImportLease = (leaseKey) => {
31
+ const cur = importLeases.get(leaseKey);
31
32
  if (cur)
32
- importLeases.set(sessionId, { ...cur, at: Date.now() });
33
+ importLeases.set(leaseKey, { ...cur, at: Date.now() });
33
34
  };
34
35
  const cleanupStaging = (stagingId) => {
35
36
  const p = pendingStagings.get(stagingId);
36
37
  if (p)
37
- importLeases.delete(p.realSessionId);
38
+ importLeases.delete(p.leaseKey);
38
39
  pendingStagings.delete(stagingId);
39
40
  };
40
- return { pendingStagings, importLeases, touchImportLease, cleanupStaging };
41
+ const sweepStaleLeases = (staleMs) => {
42
+ if (staleMs <= 0)
43
+ return [];
44
+ const now = Date.now();
45
+ const swept = [];
46
+ for (const [key, lease] of [...importLeases]) {
47
+ const ageMs = now - lease.at;
48
+ if (ageMs < staleMs)
49
+ continue;
50
+ swept.push({ sessionId: lease.sessionId, stagingId: lease.stagingId, ageMs });
51
+ cleanupStaging(lease.stagingId);
52
+ importLeases.delete(key);
53
+ }
54
+ return swept;
55
+ };
56
+ return { pendingStagings, importLeases, touchImportLease, cleanupStaging, sweepStaleLeases };
41
57
  }
42
58
  async function firstPhaseRefusal(sessionStorage, sessionId, relation, consented) {
43
59
  if (consented || (relation !== "fork" && relation !== "stale"))
@@ -57,7 +73,7 @@ async function firstPhaseRefusal(sessionStorage, sessionId, relation, consented)
57
73
  export async function handleSessionSync(req, res, match, ctx) {
58
74
  const { deps } = ctx;
59
75
  const { readJson, readRawBody, safeDecode, rateLimited } = ctx.helpers;
60
- const { pendingStagings, importLeases, touchImportLease, cleanupStaging } = ctx.local.sessionSync;
76
+ const { pendingStagings, importLeases, touchImportLease, cleanupStaging, sweepStaleLeases } = ctx.local.sessionSync;
61
77
  {
62
78
  {
63
79
  const sync = facts.sessionSync(deps);
@@ -76,9 +92,8 @@ export async function handleSessionSync(req, res, match, ctx) {
76
92
  return;
77
93
  }
78
94
  const judgeSessionGate = async () => {
79
- const face = sessionFaceOfHead(sessionId, await sessionStorage.headOf(sessionId));
80
- const mayTouch = face !== undefined && (await judgeSession(syncEntry, sessionStorage, sessionId, face)) !== undefined;
81
- return { face, mayPull: mayTouch, mayPush: face === undefined || mayTouch };
95
+ const { face, occupant, mayTouch } = await readSyncGate(syncEntry, sessionStorage, sessionId);
96
+ return { face, occupant, mayPull: face !== undefined && mayTouch, mayPush: occupant === undefined || mayTouch };
82
97
  };
83
98
  let gate;
84
99
  try {
@@ -408,6 +423,8 @@ export async function handleSessionSync(req, res, match, ctx) {
408
423
  if (!payloadVerified)
409
424
  digestSaysDiffer = true;
410
425
  }
426
+ const importingPrincipal = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
427
+ const operatorOk = syncEntry.caller.operator;
411
428
  const placed = gateFace === undefined ? undefined : await sessionStorage.executionPlacement?.read(sessionId);
412
429
  if (placed !== undefined) {
413
430
  switch (placed.cell.role) {
@@ -464,10 +481,9 @@ export async function handleSessionSync(req, res, match, ctx) {
464
481
  return;
465
482
  }
466
483
  }
467
- const opOk = syncEntry.caller.operator;
468
484
  if (deps.sessionPolicyStore) {
469
485
  try {
470
- await replayPolicyRecords(deps.sessionPolicyStore, sessionId, policy, opOk);
486
+ await replayPolicyRecords(deps.sessionPolicyStore, sessionId, policy, operatorOk);
471
487
  }
472
488
  catch (e) {
473
489
  if (sendSessionPolicyError(res, e))
@@ -475,10 +491,9 @@ export async function handleSessionSync(req, res, match, ctx) {
475
491
  throw e;
476
492
  }
477
493
  }
478
- const importingP = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
479
494
  if (deps.resumeAnchorStore)
480
495
  for (const a of anchors)
481
- await deps.resumeAnchorStore.put(sessionId, a.eventId, a.entryId, importingP);
496
+ await deps.resumeAnchorStore.put(sessionId, a.eventId, a.entryId, importingPrincipal);
482
497
  if (!(await replaySessionRow(row, gateFace))) {
483
498
  sendError(res, 409, "conflict", "sync conflict");
484
499
  return;
@@ -490,33 +505,26 @@ export async function handleSessionSync(req, res, match, ctx) {
490
505
  });
491
506
  return;
492
507
  }
493
- const heldLease = importLeases.get(sessionId);
508
+ const leaseKey = importLeaseKey(gate.occupant !== undefined ? gate.occupant.owner : importingPrincipal, sessionId);
509
+ const staleSecRaw = deps.config.syncImportLeaseStaleSec;
510
+ const staleSec = typeof staleSecRaw === "number" && Number.isFinite(staleSecRaw) ? staleSecRaw : 600;
511
+ const staleMs = staleSec * 1000;
512
+ for (const swept of sweepStaleLeases(staleMs))
513
+ deps.logger?.warn?.("sync_import_lease_taken_over", { sessionId: swept.sessionId, ageMs: swept.ageMs, staleMs, stagingId: swept.stagingId });
514
+ const heldLease = importLeases.get(leaseKey);
494
515
  if (heldLease) {
495
- const staleSecRaw = deps.config.syncImportLeaseStaleSec;
496
- const staleSec = typeof staleSecRaw === "number" && Number.isFinite(staleSecRaw) ? staleSecRaw : 600;
497
- const staleMs = staleSec * 1000;
498
- const ageMs = Date.now() - heldLease.at;
499
- if (staleMs > 0 && ageMs >= staleMs) {
500
- deps.logger?.warn?.("sync_import_lease_taken_over", { sessionId, ageMs, staleMs, stagingId: heldLease.stagingId });
501
- cleanupStaging(heldLease.stagingId);
502
- importLeases.delete(sessionId);
503
- }
504
- else {
505
- const retryAfterSec = staleMs > 0 ? Math.max(1, Math.ceil((staleMs - ageMs) / 1000)) : undefined;
506
- sendError(res, 409, "import_in_flight", "an import is already in flight for this session"
507
- + (retryAfterSec !== undefined ? ` (a stalled holder is taken over after ${staleSec}s of inactivity; retry in ~${retryAfterSec}s)` : ""), retryAfterSec !== undefined ? { retryAfterSec } : undefined);
508
- return;
509
- }
516
+ const retryAfterSec = staleMs > 0 ? Math.max(1, Math.ceil((staleMs - (Date.now() - heldLease.at)) / 1000)) : undefined;
517
+ sendError(res, 409, "import_in_flight", "an import is already in flight for this session"
518
+ + (retryAfterSec !== undefined ? ` (a stalled holder is taken over after ${staleSec}s of inactivity; retry in ~${retryAfterSec}s)` : ""), retryAfterSec !== undefined ? { retryAfterSec } : undefined);
519
+ return;
510
520
  }
511
- const importingPrincipal = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
512
- const operatorOk = syncEntry.caller.operator;
513
521
  const token = newStagingToken();
514
522
  const handle = sessionStorage.beginImportStaging(sessionId, token);
515
523
  const stagingId = handle.stagingId;
516
- importLeases.set(sessionId, { at: Date.now(), stagingId });
524
+ importLeases.set(leaseKey, { at: Date.now(), stagingId, sessionId });
517
525
  pendingStagings.set(stagingId, {
518
526
  handle, realSessionId: sessionId, owner: importingPrincipal, ...(resolution ? { resolution } : {}), operatorOk,
519
- fileHistory, policy, anchors, row, declaredEntryIds: entryIds, openedBy: ownerKeyOf(scope.gateOwner), expectedOwner: owner,
527
+ fileHistory, policy, anchors, row, declaredEntryIds: entryIds, openedBy: ownerKeyOf(scope.gateOwner), expectedOwner: owner, leaseKey,
520
528
  });
521
529
  sendJson(res, 200, { stagingId, relation: relA.relation });
522
530
  return;
@@ -550,7 +558,7 @@ export async function handleSessionSync(req, res, match, ctx) {
550
558
  const utf8 = new StringDecoder("utf8");
551
559
  try {
552
560
  for await (const chunk of req) {
553
- touchImportLease(pending.realSessionId);
561
+ touchImportLease(pending.leaseKey);
554
562
  lineBuf += utf8.write(chunk);
555
563
  if (Buffer.byteLength(lineBuf, "utf8") > SYNC_NDJSON_LINE_MAX_BYTES)
556
564
  throw new PrecheckError(413, "NDJSON line too large");
@@ -2,7 +2,7 @@
2
2
  * [ref] A9 域模块 —— 会话列表 / 发件链接列表。
3
3
  *
4
4
  * 路由:`GET /v1/sessions`(CC /resume picker,keyset 分页)、`GET /v1/sendfile-links`。
5
- * owner-scope 与 trace 列表同一权威模型(列表面 `list.read` 读档(`listFilterOf`):service 凭据不报主体〔部署声明 `SERVICE_TOKEN_ACTS_FOR_DEPLOYMENT=false` 时不算〕/ 只读 trace 凭据 / dev-open = fleet-wide;带验过的主体 = 钉住那位主体)。本域零可变状态。
5
+ * owner-scope 与 trace 列表同一权威模型(列表面 `list.read` 读档(`listFilterOf`):service 凭据不报主体〔部署声明 `SERVICE_TOKEN_ACTS_FOR_DEPLOYMENT=false` 时不算〕/ 只读 trace 凭据 / dev-open 上不报主体 = fleet-wide;报了主体 = 钉住那位主体,部署形不改这一句)。本域零可变状态。
6
6
  */
7
7
  import type { IncomingMessage, ServerResponse } from "node:http";
8
8
  import { type SessionListItem } from "../../security.js";
@@ -26,7 +26,7 @@ export interface SessionsLocal {
26
26
  degraded?: boolean;
27
27
  }>;
28
28
  }>;
29
- /** DESIGN-269 件4:`/a2a` 面板的短 TTL single-flight 缓存。与 `mcpStatusCache` **分开一张表**而不是
29
+ /** [ref] 件4:`/a2a` 面板的短 TTL single-flight 缓存。与 `mcpStatusCache` **分开一张表**而不是
30
30
  * 共用带前缀的键:两张面板的体不同形(`peers` vs `servers`),塞进一张表就得靠 unknown 兜住两形,
31
31
  * 于是「取到的是哪一形」变成运行期约定 —— 两张表让它回到类型上。 */
32
32
  a2aStatusCache: Map<string, {
@@ -14,8 +14,8 @@ import { readWritableSessionPermissionRules } from "../../session-policy-wire.js
14
14
  import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders, httpErrorBody } from "../send.js";
15
15
  import { gatedPerson, gatedSubject, sendPrincipalRefusal } from "../principal-gate.js";
16
16
  import { personOf } from "../../owner-brands.js";
17
- import { headReadOf, leafOfAdmittedHead, sessionFaceOfHead } from "../../session-incarnation.js";
18
- import { admitSessionById, admitSessionRow, admittedAsFleet, callerEntry, callerOf, deliverVerdict, enter, judgeSessionRow, protocolEntry, readOwnerCell, refuse, seesRun, sessionRowPresent } from "../owner-access.js";
17
+ import { leafOfAdmittedHead, sessionFaceOfHead } from "../../session-incarnation.js";
18
+ import { admitSessionById, admitSessionForDeletion, admitSessionRow, admitSessionView, admittedAsFleet, callerEntry, callerOf, deliverVerdict, enter, judgeSessionById, protocolEntry, readOwnerCell, readVisibleLatestRun, refuse, sessionOwnerUnchanged } from "../owner-access.js";
19
19
  import { assertNever } from "../route-ctx.js";
20
20
  import * as facts from "../../capability-facts.js";
21
21
  import { rosterAgentId } from "./agents-roster.js";
@@ -67,11 +67,9 @@ async function readLastLegMcp(deps, sessionId, entry) {
67
67
  const runStore = deps.runStore;
68
68
  if (!runStore || sessionId === null)
69
69
  return undefined;
70
- const latest = await runStore.latestRunForSession(sessionId);
70
+ const latest = await readVisibleLatestRun(entry, runStore, sessionId);
71
71
  if (latest === undefined)
72
72
  return undefined;
73
- if (!seesRun(entry, latest.owner))
74
- return undefined;
75
73
  const events = await runStore.getEvents(latest.taskId, 0);
76
74
  for (let i = events.length - 1; i >= 0; i--) {
77
75
  const ev = events[i];
@@ -87,26 +85,7 @@ async function readLastLegMcp(deps, sessionId, entry) {
87
85
  export async function handleSessions(req, res, match, ctx) {
88
86
  const { deps } = ctx;
89
87
  const { readJson, safeDecode } = ctx.helpers;
90
- const readOwnedSessionView = async (entry, sessionAudit, sessionId, readOpts) => {
91
- const headOf = headReadOf(deps.sessionStorage);
92
- if (headOf !== undefined) {
93
- const head = await readOwnerCell(entry, sessionId, () => headOf(sessionId));
94
- if ((await admitSessionRow(entry, res, deps.sessionStorage, sessionId, head)) === undefined)
95
- return undefined;
96
- }
97
- let audit;
98
- try {
99
- audit = await sessionAudit(sessionId, readOpts);
100
- }
101
- catch (e) {
102
- if (headOf !== undefined)
103
- throw e;
104
- throw new Error(`session view read failed before ownership could be established: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
105
- }
106
- if ((await admitSessionRow(entry, res, deps.sessionStorage, sessionId, audit)) === undefined)
107
- return undefined;
108
- return audit;
109
- };
88
+ const readOwnedSessionView = (entry, sessionAudit, sessionId, readOpts) => admitSessionView(entry, res, deps.sessionStorage, sessionAudit, sessionId, readOpts);
110
89
  const sse = ctx.local.sessions;
111
90
  const { mcpStatusCache, a2aStatusCache } = ctx.local.sessions;
112
91
  {
@@ -262,8 +241,8 @@ export async function handleSessions(req, res, match, ctx) {
262
241
  }
263
242
  {
264
243
  if (match.decl.id === "session-head") {
265
- const headOf = headReadOf(deps.sessionStorage);
266
- if (!headOf) {
244
+ const headFact = facts.sessionOwnership(deps);
245
+ if (!headFact.available) {
267
246
  sendError(res, 501, "capability.session_store_required", "session head probe requires a durable session store (DB_BACKEND=mysql|pg|local)");
268
247
  return;
269
248
  }
@@ -273,8 +252,7 @@ export async function handleSessions(req, res, match, ctx) {
273
252
  return;
274
253
  }
275
254
  const headEntry = callerEntry(req, deps, match.decl.id);
276
- const headRead = await readOwnerCell(headEntry, headSession, () => headOf(headSession));
277
- const admittedHead = await admitSessionRow(headEntry, res, deps.sessionStorage, headSession, headRead);
255
+ const admittedHead = await admitSessionById(headEntry, res, headFact.sessionStorage, headSession);
278
256
  if (admittedHead === undefined)
279
257
  return;
280
258
  const leafId = leafOfAdmittedHead(admittedHead.row);
@@ -317,8 +295,6 @@ export async function handleSessions(req, res, match, ctx) {
317
295
  return;
318
296
  }
319
297
  const evEntry = callerEntry(req, deps, match.decl.id);
320
- const readHead = (id) => readOwnerCell(evEntry, id, () => evFact.sessionStorage.headOf(id));
321
- const readOwner = async (id) => (await readHead(id))?.owner;
322
298
  const watch = evFact.watch;
323
299
  const evSession = safeDecode(match.params.sessionId);
324
300
  if (evSession === null) {
@@ -354,7 +330,7 @@ export async function handleSessions(req, res, match, ctx) {
354
330
  return;
355
331
  }
356
332
  sse.sseOwnerLookups++;
357
- const admissionP = readHead(evSession).then(async (head) => (!sessionRowPresent(head) ? undefined : { head, verdict: await judgeSessionRow(evEntry, evFact.sessionStorage, evSession, head) }));
333
+ const admissionP = judgeSessionById(evEntry, evFact.sessionStorage, evSession);
358
334
  void admissionP.catch(() => undefined).then(() => { sse.sseOwnerLookups--; });
359
335
  let stallTimer;
360
336
  let admission;
@@ -381,12 +357,13 @@ export async function handleSessions(req, res, match, ctx) {
381
357
  refuse(evEntry, res, "missing");
382
358
  return;
383
359
  }
384
- if (deliverVerdict(evEntry, res, admission.verdict) === undefined) {
360
+ const evAdmitted = deliverVerdict(evEntry, res, admission);
361
+ if (evAdmitted === undefined) {
385
362
  cleanup();
386
363
  return;
387
364
  }
388
- leafOfAdmittedHead(admission.head);
389
- const owner = admission.head.owner;
365
+ leafOfAdmittedHead(evAdmitted.row);
366
+ const owner = evAdmitted.row.owner;
390
367
  const q = new URLSearchParams(match.search);
391
368
  const lastLeafId = q.get("lastLeafId");
392
369
  let first = true;
@@ -485,9 +462,9 @@ export async function handleSessions(req, res, match, ctx) {
485
462
  }
486
463
  ownerCheckPending = true;
487
464
  ownerCheckStartedAt = Date.now();
488
- void readOwner(evSession).then((o) => {
465
+ void sessionOwnerUnchanged(evEntry, evFact.sessionStorage, evSession, owner).then((same) => {
489
466
  ownerCheckPending = false;
490
- if (!closed && o !== owner)
467
+ if (!closed && !same)
491
468
  onEnd();
492
469
  }).catch(() => { ownerCheckPending = false; });
493
470
  }, hbMs);
@@ -706,14 +683,11 @@ export async function handleSessions(req, res, match, ctx) {
706
683
  const store = rowFact.sessionStorage;
707
684
  const sessionId = match.params.sessionId;
708
685
  const patchEntry = callerEntry(req, deps, match.decl.id);
709
- const head = await store.headOf(sessionId);
710
- if (head === undefined) {
711
- refuse(patchEntry, res, "missing");
686
+ const patchAdmitted = await admitSessionById(patchEntry, res, store, sessionId);
687
+ if (patchAdmitted === undefined)
712
688
  return;
713
- }
689
+ const head = patchAdmitted.row;
714
690
  const face = sessionFaceOfHead(sessionId, head);
715
- if ((await admitSessionRow(patchEntry, res, store, sessionId, face)) === undefined)
716
- return;
717
691
  leafOfAdmittedHead(head);
718
692
  const read = readSessionRowPatch(await readJson(req));
719
693
  if ("problem" in read) {
@@ -903,7 +877,7 @@ export async function handleSessions(req, res, match, ctx) {
903
877
  sendError(res, 400, "request.id_invalid", "invalid session id (must be a canonical uuid)");
904
878
  return;
905
879
  }
906
- const deleteAdmitted = await admitSessionById(deleteEntry, res, deleteFact.gateStore, sessionId);
880
+ const deleteAdmitted = await admitSessionForDeletion(deleteEntry, res, deleteFact.sessionStorage, sessionId);
907
881
  if (deleteAdmitted === undefined)
908
882
  return;
909
883
  const owner = deleteAdmitted.row.owner;