@sema-agent/server 7.148.0 → 7.149.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 (247) hide show
  1. package/MIGRATION.md +429 -0
  2. package/README.md +16 -1
  3. package/README.zh-CN.md +4 -0
  4. package/USAGE.md +89 -55
  5. package/dist/adoption/wire.d.ts +3 -3
  6. package/dist/approval-card.d.ts +32 -32
  7. package/dist/approval-leg-origin.d.ts +33 -0
  8. package/dist/approval-leg-origin.js +4 -0
  9. package/dist/auth-bridge.d.ts +9 -1
  10. package/dist/auth-bridge.js +4 -4
  11. package/dist/auth-keys.d.ts +10 -1
  12. package/dist/auth-keys.js +8 -0
  13. package/dist/boot/execution-env.js +10 -11
  14. package/dist/boot/execution-lane.d.ts +6 -4
  15. package/dist/boot/execution-lane.js +3 -1
  16. package/dist/boot/org-delivery.d.ts +3 -2
  17. package/dist/boot/resolve-spec.js +6 -4
  18. package/dist/boot/runner-deps.js +3 -1
  19. package/dist/boot/stage-01-config.d.ts +1 -0
  20. package/dist/boot/stage-01-config.js +4 -3
  21. package/dist/boot/stage-02-web-admin-pull.d.ts +1 -0
  22. package/dist/boot/stage-02-web-admin-pull.js +10 -2
  23. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  24. package/dist/boot/stage-03-store-backend.js +2 -2
  25. package/dist/boot/stage-04-stores.d.ts +1 -0
  26. package/dist/boot/stage-05-execution-env.d.ts +1 -0
  27. package/dist/boot/stage-06-runners.d.ts +2 -1
  28. package/dist/boot/stage-07-capability-layer.d.ts +2 -1
  29. package/dist/boot/stage-08-reapers.d.ts +2 -1
  30. package/dist/boot/stage-09-leader.d.ts +2 -1
  31. package/dist/boot/stage-10-http-server.d.ts +2 -1
  32. package/dist/boot/stores.d.ts +3 -0
  33. package/dist/boot/stores.js +22 -19
  34. package/dist/capabilities/tool-defer.d.ts +1 -1
  35. package/dist/capability-facts.d.ts +5 -4
  36. package/dist/capability-facts.js +3 -14
  37. package/dist/card-settle-stored-scope.d.ts +56 -0
  38. package/dist/card-settle-stored-scope.js +27 -0
  39. package/dist/card-void.d.ts +7 -12
  40. package/dist/card-void.js +2 -7
  41. package/dist/config-catalog.d.ts +12 -0
  42. package/dist/config-catalog.js +36 -32
  43. package/dist/config-invariants.d.ts +5 -4
  44. package/dist/config-invariants.js +4 -4
  45. package/dist/config-types.d.ts +37 -5
  46. package/dist/config.d.ts +6 -5
  47. package/dist/config.js +35 -24
  48. package/dist/context-window-notice.d.ts +31 -0
  49. package/dist/context-window-notice.js +28 -0
  50. package/dist/deployment-shape.d.ts +20 -11
  51. package/dist/deployment-shape.js +1 -0
  52. package/dist/env-name-allowlist-knobs.d.ts +6 -5
  53. package/dist/env-name-allowlist-knobs.js +5 -0
  54. package/dist/execution-lane-caps.d.ts +20 -5
  55. package/dist/execution-lane-caps.js +18 -4
  56. package/dist/hooks/command-spawn.d.ts +7 -2
  57. package/dist/hooks/command-spawn.js +13 -6
  58. package/dist/hooks/hook-runner.d.ts +5 -0
  59. package/dist/hooks/hook-runner.js +11 -7
  60. package/dist/http/admission.js +3 -3
  61. package/dist/http/approval-sweeps.js +3 -2
  62. package/dist/http/claim-heartbeat.d.ts +3 -3
  63. package/dist/http/claim-heartbeat.js +3 -3
  64. package/dist/http/claimed-run.d.ts +16 -9
  65. package/dist/http/claimed-run.js +8 -0
  66. package/dist/http/dispatch.js +5 -3
  67. package/dist/http/durable-run-leg.d.ts +8 -1
  68. package/dist/http/durable-run-leg.js +30 -11
  69. package/dist/http/execution-context.d.ts +115 -14
  70. package/dist/http/execution-context.js +130 -40
  71. package/dist/http/owner-access.d.ts +3 -2
  72. package/dist/http/owner-access.js +2 -3
  73. package/dist/http/principal-gate.d.ts +18 -11
  74. package/dist/http/principal-gate.js +3 -1
  75. package/dist/http/resume-ctx-read.d.ts +14 -3
  76. package/dist/http/resume-ctx-read.js +8 -2
  77. package/dist/http/resume-legs.d.ts +35 -3
  78. package/dist/http/resume-legs.js +244 -92
  79. package/dist/http/route-ctx.d.ts +6 -0
  80. package/dist/http/routes/a2a-serve.js +2 -0
  81. package/dist/http/routes/approvals-assistant.js +3 -2
  82. package/dist/http/routes/capabilities.js +1 -1
  83. package/dist/http/routes/diagnostics.d.ts +3 -1
  84. package/dist/http/routes/diagnostics.js +30 -5
  85. package/dist/http/routes/principal-inbox.d.ts +3 -1
  86. package/dist/http/routes/principal-inbox.js +3 -2
  87. package/dist/http/routes/runs.js +10 -7
  88. package/dist/http/routes/session-sync.d.ts +24 -5
  89. package/dist/http/routes/session-sync.js +46 -34
  90. package/dist/http/routes/sessions-list.d.ts +1 -1
  91. package/dist/http/routes/tasks.js +127 -93
  92. package/dist/http/server.d.ts +4 -5
  93. package/dist/http/server.js +30 -5
  94. package/dist/http/submit-ctx.d.ts +1 -0
  95. package/dist/http/submit-ctx.js +2 -3
  96. package/dist/index.d.ts +3 -1
  97. package/dist/index.js +1 -0
  98. package/dist/leg-facts.d.ts +7 -2
  99. package/dist/leg-facts.js +1 -0
  100. package/dist/local-store-declaration.d.ts +54 -0
  101. package/dist/local-store-declaration.js +59 -0
  102. package/dist/mcp-assertion-header.d.ts +1 -1
  103. package/dist/mcp-assertion-header.js +1 -1
  104. package/dist/memory-project-layer.js +1 -1
  105. package/dist/memory-scope.d.ts +4 -4
  106. package/dist/memory-scope.js +4 -4
  107. package/dist/memory-sync-client.js +2 -2
  108. package/dist/model-context-window.d.ts +50 -11
  109. package/dist/model-context-window.js +20 -1
  110. package/dist/model-entry-refusal.d.ts +5 -4
  111. package/dist/model-entry-refusal.js +17 -3
  112. package/dist/model-reach-scope.d.ts +2 -1
  113. package/dist/observability/fail-open.d.ts +14 -2
  114. package/dist/observability/fail-open.js +14 -2
  115. package/dist/observability/metrics.js +1 -1
  116. package/dist/observability/run-terminal.js +1 -0
  117. package/dist/observability/secret-env-scrub.d.ts +1 -1
  118. package/dist/observability/secret-env-scrub.js +1 -1
  119. package/dist/org-delivery-gate.d.ts +31 -6
  120. package/dist/org-delivery-gate.js +7 -4
  121. package/dist/owner-faces.d.ts +3 -2
  122. package/dist/park-row-commit.d.ts +67 -0
  123. package/dist/park-row-commit.js +76 -0
  124. package/dist/parked-decide.d.ts +9 -7
  125. package/dist/parked-decide.js +25 -6
  126. package/dist/peer-directory.d.ts +1 -1
  127. package/dist/plugins/adoption-log-sql.d.ts +3 -3
  128. package/dist/plugins/adoption-log-sql.js +19 -19
  129. package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
  130. package/dist/plugins/approval-ask-store-sql.js +3 -5
  131. package/dist/plugins/approval-exemption-skill-cutoff.d.ts +64 -0
  132. package/dist/plugins/approval-exemption-skill-cutoff.js +122 -0
  133. package/dist/plugins/approval-exemption-skill-m46.d.ts +0 -6
  134. package/dist/plugins/approval-exemption-skill-m46.js +6 -10
  135. package/dist/plugins/approval-exemption-store.d.ts +26 -1
  136. package/dist/plugins/approval-exemption-store.js +28 -19
  137. package/dist/plugins/ensure-schema-lock-policy.d.ts +36 -3
  138. package/dist/plugins/ensure-schema-lock-policy.js +75 -9
  139. package/dist/plugins/file-run-store.d.ts +22 -9
  140. package/dist/plugins/file-run-store.js +50 -22
  141. package/dist/plugins/local-checkpoint-store.d.ts +4 -0
  142. package/dist/plugins/local-checkpoint-store.js +2 -1
  143. package/dist/plugins/local-data-root-admission.d.ts +52 -0
  144. package/dist/plugins/local-data-root-admission.js +100 -0
  145. package/dist/plugins/local-engine-high-water.d.ts +28 -7
  146. package/dist/plugins/local-engine-high-water.js +27 -22
  147. package/dist/plugins/local-session-store.d.ts +134 -49
  148. package/dist/plugins/local-session-store.js +149 -108
  149. package/dist/plugins/memory-run-store.d.ts +14 -9
  150. package/dist/plugins/memory-run-store.js +47 -21
  151. package/dist/plugins/one-time-migration-marker.d.ts +19 -3
  152. package/dist/plugins/one-time-migration-marker.js +7 -1
  153. package/dist/plugins/one-time-migrations.d.ts +46 -20
  154. package/dist/plugins/one-time-migrations.js +102 -79
  155. package/dist/plugins/org-delivery-quota-store.d.ts +34 -11
  156. package/dist/plugins/org-delivery-quota-store.js +29 -14
  157. package/dist/plugins/permission-rule-store-sql.d.ts +1 -1
  158. package/dist/plugins/permission-rule-store-sql.js +5 -9
  159. package/dist/plugins/pg-pool.d.ts +1 -1
  160. package/dist/plugins/pg-pool.js +10 -12
  161. package/dist/plugins/remote-env-device.d.ts +3 -2
  162. package/dist/plugins/remote-env-e2b.d.ts +1 -7
  163. package/dist/plugins/remote-env-e2b.js +10 -7
  164. package/dist/plugins/remote-env-host.d.ts +50 -9
  165. package/dist/plugins/remote-env-host.js +77 -20
  166. package/dist/plugins/remote-env-k8s.js +4 -2
  167. package/dist/plugins/remote-env-local-docker.d.ts +6 -3
  168. package/dist/plugins/remote-env-local-docker.js +8 -23
  169. package/dist/plugins/run-store-sql.d.ts +26 -7
  170. package/dist/plugins/run-store-sql.js +44 -16
  171. package/dist/plugins/scenario-prefix-switch-m42.d.ts +3 -1
  172. package/dist/plugins/scenario-prefix-switch-m42.js +4 -3
  173. package/dist/plugins/schema-consistency.d.ts +89 -5
  174. package/dist/plugins/schema-consistency.js +176 -7
  175. package/dist/plugins/schema-drift-sentence.d.ts +6 -5
  176. package/dist/plugins/schema-drift-sentence.js +13 -5
  177. package/dist/plugins/session-placement.d.ts +5 -4
  178. package/dist/plugins/shell-sentinel-owner-m48.d.ts +22 -0
  179. package/dist/plugins/shell-sentinel-owner-m48.js +6 -0
  180. package/dist/plugins/stop-then-start-sentence.d.ts +16 -0
  181. package/dist/plugins/stop-then-start-sentence.js +7 -0
  182. package/dist/plugins/store-backend.d.ts +22 -8
  183. package/dist/plugins/store-backend.js +58 -24
  184. package/dist/plugins/store-contracts.d.ts +33 -3
  185. package/dist/plugins/store-contracts.js +16 -0
  186. package/dist/plugins/tidb-pool.d.ts +11 -6
  187. package/dist/plugins/tidb-pool.js +18 -12
  188. package/dist/plugins/workspace-custody.d.ts +44 -0
  189. package/dist/plugins/workspace-custody.js +47 -0
  190. package/dist/project-identity.d.ts +8 -7
  191. package/dist/project-identity.js +40 -17
  192. package/dist/project-layer-key.d.ts +13 -2
  193. package/dist/project-layer-key.js +9 -1
  194. package/dist/read-deny-floor-notice.d.ts +36 -0
  195. package/dist/read-deny-floor-notice.js +40 -0
  196. package/dist/{plain-string-record.d.ts → request-declaration-disposition.d.ts} +9 -23
  197. package/dist/request-declaration-disposition.js +39 -0
  198. package/dist/run-cancel-card.d.ts +28 -0
  199. package/dist/run-cancel-card.js +19 -0
  200. package/dist/run-cancel-context.d.ts +10 -0
  201. package/dist/run-cancel-context.js +1 -0
  202. package/dist/run-local.js +28 -13
  203. package/dist/runs.d.ts +31 -7
  204. package/dist/runs.js +34 -16
  205. package/dist/security.d.ts +2 -2
  206. package/dist/server-secret-env.d.ts +34 -20
  207. package/dist/server-secret-env.js +61 -18
  208. package/dist/session-agent.d.ts +3 -2
  209. package/dist/session-children-retire.d.ts +6 -3
  210. package/dist/session-children-retire.js +2 -5
  211. package/dist/session-workspace.d.ts +3 -3
  212. package/dist/store-admission.d.ts +33 -0
  213. package/dist/store-admission.js +49 -0
  214. package/dist/store-posture.d.ts +5 -0
  215. package/dist/store-posture.js +18 -1
  216. package/dist/stores-sql/checkpoint-store-sql.d.ts +1 -1
  217. package/dist/stores-sql/checkpoint-store-sql.js +3 -5
  218. package/dist/stores-sql/file-history-store-sql.d.ts +1 -1
  219. package/dist/stores-sql/file-history-store-sql.js +3 -5
  220. package/dist/stores-sql/tool-result-store-sql.d.ts +1 -1
  221. package/dist/stores-sql/tool-result-store-sql.js +4 -6
  222. package/dist/task-a2a.d.ts +3 -8
  223. package/dist/task-a2a.js +25 -27
  224. package/dist/task-cwd.d.ts +23 -7
  225. package/dist/task-cwd.js +3 -0
  226. package/dist/task-mcp.d.ts +3 -3
  227. package/dist/task-mcp.js +1 -1
  228. package/dist/task-settings.js +1 -0
  229. package/dist/terminal.d.ts +2 -0
  230. package/dist/terminal.js +21 -6
  231. package/dist/tool-approval.d.ts +44 -36
  232. package/dist/tool-approval.js +66 -47
  233. package/dist/trace/engine-notice-wire.d.ts +24 -21
  234. package/dist/trace/engine-notice-wire.js +7 -3
  235. package/dist/trace/ledger-sink.d.ts +3 -1
  236. package/dist/trace/project.d.ts +4 -2
  237. package/dist/trace/project.js +8 -7
  238. package/dist/trace/redact.d.ts +12 -0
  239. package/dist/trace/redact.js +5 -1
  240. package/dist/trace/self-minted-wire.d.ts +131 -52
  241. package/dist/trace/self-minted-wire.js +71 -39
  242. package/dist/web-admin-client/apply-effective.js +22 -9
  243. package/docs/error-text-lexicon.json +317 -0
  244. package/package.json +3 -1
  245. package/scripts/embed-minimal.mjs +183 -0
  246. package/wire-changes/server@7.149.0.json +372 -0
  247. package/dist/plain-string-record.js +0 -70
@@ -36,6 +36,7 @@ import type { RootedTaskSpec } from "./execution-context.js";
36
36
  import type { GateCommitment, GateSettlementEvidence } from "../gate-settlement.js";
37
37
  import type { ResumeRestatement } from "./resume-restatement.js";
38
38
  import type { RememberLaneCannotRecord } from "../parked-decide.js";
39
+ import type { RowWayOut } from "./resume-legs.js";
39
40
  import type { KeyCaller } from "../owner-faces.js";
40
41
  import type { Subject } from "../owner-brands.js";
41
42
  /** 跨域可变状态 = lens1 §C2 实测的那 8 条「运行期登记簿」,一个对象。
@@ -291,6 +292,11 @@ export interface DriveResumeArgs {
291
292
  sessionId: string;
292
293
  ctx: CheckpointCtx;
293
294
  };
295
+ /**
296
+ * 判官判本引擎续不了这条腿要续的那张卡时,这张卡的出路(`resume-legs.ts` 的 `rowWayOutOf`;判 `ok` / 说不出 ⇒ 缺席)。`/plan_review` 与 `/resume` 在驱动前对载入行求它;
297
+ * catch 臂在引擎的拒留卡 pending(处置 `retriable`)时把它作 `wayOut` 加进拒体 —— 引擎的拒原样不改(码、`field`、`blockedBy`、`thrownName`、诊断都在)。
298
+ */
299
+ rowWayOut?: RowWayOut;
294
300
  /**
295
301
  * 这条续跑的**宿主租约**(续跑漏斗 `resolveResumeSpecOrBlocked` 开、装配在它之下登记对端声明;调用方作用域的 `finally` 放)。
296
302
  * 驱动腿经 `heldFor(args.peerLease, () => driveResumeLeg(…))` 自持一只:受理早回的那一形里,腿活过调用方作用域,自己落定才放。
@@ -311,6 +311,8 @@ async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
311
311
  function rpcErrorFromRefusedReceipt(receipt) {
312
312
  const code = receipt.body?.errorCode;
313
313
  const head = `the serving deployment did not accept this submission (it answered ${receipt.status}${typeof code === "string" ? ` ${code}` : ""})`;
314
+ if (receipt.status >= 500)
315
+ return new RpcError(JSONRPC_INTERNAL_ERROR, `${head}; the failure is on the serving side and nothing was started, so the same message can be sent again later`);
314
316
  if (isActiveRunConflictBody(receipt.body))
315
317
  return new RpcError(JSONRPC_INVALID_PARAMS, `${head}; a session already running a turn must finish or be cancelled before the next message on the same contextId`);
316
318
  if (isRunExistsConflictBody(receipt.body))
@@ -334,10 +334,11 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
334
334
  return;
335
335
  }
336
336
  const pending = [...(await projectDurableQueue(durableRows, deps.config, deps.sessionStorage)), ...peerInboundRowsFor(defaultPeerInboundFace, principal, ownerParam)()];
337
+ const livePending = deps.toolApproval ? await deps.toolApproval.listLivePending(scope) : undefined;
337
338
  sendJson(res, 200, {
338
339
  pending,
339
340
  serverNowMs: Date.now(),
340
- ...(deps.toolApproval ? { livePending: deps.toolApproval.listLivePending(scope) } : {}),
341
+ ...(livePending !== undefined ? { livePending } : {}),
341
342
  ...(deps.approvalAskAudit ? { crashConverged: deps.approvalAskAudit.listCrashConverged(scope) } : {}),
342
343
  });
343
344
  return;
@@ -1122,7 +1123,7 @@ export async function streamApprovals(req, res, cs, live, peerInbound, sessions,
1122
1123
  let snapshot;
1123
1124
  try {
1124
1125
  const pending = [...(await projectDurableQueue(await cs.listPending(scope), governance, sessions)), ...(peerInbound?.() ?? [])];
1125
- snapshot = { pending, livePending: live ? live.listLivePending(scope) : [] };
1126
+ snapshot = { pending, livePending: live ? await live.listLivePending(scope) : [] };
1126
1127
  }
1127
1128
  catch (err) {
1128
1129
  const failure = approvalsReadFailureOf(err);
@@ -299,7 +299,7 @@ export async function handleCapabilities(req, res, match, ctx) {
299
299
  ...(modelReasoningDecl(m) !== undefined ? { reasoning: modelReasoningDecl(m) } : {}),
300
300
  ...(modelImageInput(m) !== "undeclared" ? { vision: modelImageInput(m) === "yes" } : {}),
301
301
  supportsThinkingOff: thinkingOffSpelled(m),
302
- ...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
302
+ ...(m.contextWindow ? { contextWindow: m.contextWindow, ...(Object.hasOwn(deps.config.contextWindowSources, name) ? { contextWindowSource: deps.config.contextWindowSources[name] } : {}) } : {}),
303
303
  ...(m.maxTokens ? { maxOutputTokens: m.maxTokens } : {}),
304
304
  ...((levels) => (levels !== undefined ? { supportedEffortLevels: [...levels] } : {}))(reasoningEffortLevelsOf(m)),
305
305
  ...(m.compat !== undefined ? { compat: m.compat } : {}),
@@ -86,7 +86,9 @@ export interface StaticWiringFacts {
86
86
  * 白白关掉一个可用的协议。`capable` 真而 `effective` 不真同样致命:能力在、这条部署形上却不会真的
87
87
  * park,窗到期依旧无降级目的地。
88
88
  * ② 会话店持久性:manifest 声明 `declared_durable`,而会话店的介质(店持久性判定里的**数据根**那一行:SQL 两方言 = 委托给库、local = 数据根所在挂载、
89
- * 没有后端 = 内存)判为 `ephemeral` ⇒ 「替换后还在」的声明没有介质兑现 ⇒ 拒启 `config.store_not_durable`(拒句逐根列三根判定 + 两条出路)。
89
+ * 没有后端 = 内存)判为 `ephemeral` ⇒ 「替换后还在」的声明没有介质兑现 ⇒ 拒启 `config.store_not_durable`(拒句逐根列三根判定 + 三条出路)。
90
+ * local 店的声明不是常量:它由 `local-store-declaration.ts` 的唯一读者定(运维声明 `LOCAL_STORE_DURABILITY=ephemeral`,或店落在调用方自己那块易失盘上 ⇒
91
+ * 会话店与检查点店两半一起声明 `process-local`)—— 那时清单读 `process_local`,本条恒过,改出 warn 一行(带依据与挂卷出路;同一件事 park 那条不再另说)。
90
92
  * 介质判不出(`unknown`:非 Linux / 挂载表读不到 / 介质形不在规则表里)⇒ 只 warn —— 判不了的部署不能因自报位停机;
91
93
  * 数据根持久而另两根(封签钥匙目录 / file 记忆根)易失或判不出 ⇒ 也只 warn:会话店的声明兑现了,掉的是停驻行的封签钥匙 / 记忆,不是会话(能力位如实报最弱)。
92
94
  * 声明不是 `declared_durable` ⇒ 恒过。这条与 memory+durable 两条既有拒启同族(声明与介质必须对得上)。
@@ -8,6 +8,7 @@ import { gatedPerson, explicitOperatorOk, isLoopbackAddress, sendOperatorPrincip
8
8
  import * as facts from "../../capability-facts.js";
9
9
  import { assertNever } from "../route-ctx.js";
10
10
  import { STORE_NOT_DURABLE_CODE, StoreNotDurableError, describeStoreRoot, projectStoreDurabilityWiring, unwiredStorePosture } from "../../store-posture.js";
11
+ import { describeLocalStoreDeclaration } from "../../local-store-declaration.js";
11
12
  export function buildServerWiringGates(input) {
12
13
  const gate = resolveStreamApprovalGate({
13
14
  toolApprovalEnabled: input.toolApprovalEnabled,
@@ -44,6 +45,21 @@ export function buildServerWiringGates(input) {
44
45
  approvalGrant: [...input.approvalGrant],
45
46
  };
46
47
  }
48
+ function processLocalMediumSentence(dataRoot, aggregate, listed) {
49
+ const readFaces = `Store durability by root: ${listed}; GET /v1/capabilities store.durability and /health.store.durability say "${aggregate}" (the weakest root).`;
50
+ switch (dataRoot) {
51
+ case "ephemeral":
52
+ return `The data root's medium does not survive a replacement either — sessions and parked approvals on it go with the next replacement. ${readFaces}`;
53
+ case "durable":
54
+ return `The data root's medium measures durable today — the declaration is what this deployment promises, not what the disk does today. ${readFaces}`;
55
+ case "unknown":
56
+ return `This server could not prove whether the data root survives a replacement. ${readFaces}`;
57
+ default: {
58
+ const unhandled = dataRoot;
59
+ throw new Error(`unhandled store durability ${JSON.stringify(unhandled)}`);
60
+ }
61
+ }
62
+ }
47
63
  export function buildStaticWiringAudit(facts) {
48
64
  const refusals = [];
49
65
  const warnings = [];
@@ -58,15 +74,16 @@ export function buildStaticWiringAudit(facts) {
58
74
  `A window expiry would then have no degradation target and adjudicate as a fail-closed deny — refusing to start.`);
59
75
  }
60
76
  let refusalCode;
77
+ const roots = facts.store.roots;
78
+ const listed = roots.map((r) => `${describeStoreRoot(r)}`).join("; ");
79
+ const processLocalDeclaration = facts.store.localDeclaration?.durability === "process-local" ? facts.store.localDeclaration : undefined;
61
80
  if (facts.manifest.session.store === "declared_durable") {
62
- const roots = facts.store.roots;
63
81
  const session = roots.find((r) => r.root === "data");
64
- const listed = roots.map((r) => `${describeStoreRoot(r)}`).join("; ");
65
82
  if (session === undefined || session.verdict === "ephemeral") {
66
83
  refusalCode = STORE_NOT_DURABLE_CODE;
67
84
  const wayOut = session?.rule === "backend_memory" || session === undefined
68
85
  ? `No store backend was assembled (env-only worker or a degraded boot) — wire DB_BACKEND=mysql|pg|local, or fix the session store's durability declaration.`
69
- : `Two ways out: mount a persistent volume at ${session.path ?? "the data root"} (or point LOCAL_DATA_ROOT at one), or declare this deployment non-durable (DB_BACKEND=memory).`;
86
+ : `Three ways out: mount a persistent volume at ${session.path ?? "the data root"} (or point LOCAL_DATA_ROOT at one); or declare the local store ephemeral (LOCAL_STORE_DURABILITY=ephemeral — sessions, parked approvals and memory then do not survive a container / Pod replacement); or keep nothing on disk (DB_BACKEND=memory).`;
70
87
  refusals.push(`${STORE_NOT_DURABLE_CODE}: the engine's static wiring reports session.store="declared_durable" while the session store's medium does not survive a container / Pod replacement (store durability by root: ${listed}). ` +
71
88
  `A durable declaration on an ephemeral medium is a claim nothing can honor — sessions, parked approvals and memory would vanish with the next replacement while every read face said "durable". Refusing to start. ${wayOut}`);
72
89
  }
@@ -74,11 +91,19 @@ export function buildStaticWiringAudit(facts) {
74
91
  warnings.push(`store durability is "${facts.store.durability}" (by root: ${listed}). ` +
75
92
  (facts.store.durability === "unknown"
76
93
  ? `This server could not prove that every root survives a container / Pod replacement (no mount evidence for it — not Linux, an unreadable mount table, or a mount shape outside the rule table); GET /v1/capabilities store.durability and /health.store.durability say "unknown". `
77
- : `The session store's medium holds, but a root marked ephemeral does not survive a container / Pod replacement — mount a persistent volume there (SEALED_KEY_DIR / MEMORY_ENGINE_DIR name those directories). `) +
94
+ : `The session store's medium holds, but a root marked ephemeral does not survive a container / Pod replacement — mount a persistent volume there (SEALED_KEY_DIR / MEMORY_ENGINE_DIR name those directories). ` +
95
+ `A sealing-key directory declared with SEALED_KEY_DIR on such a medium is refused at boot while parked approvals are durable (the parked rows would outlive the key their credentials are sealed to); ` +
96
+ `the default <data root>/sealed-key and the file memory root are reported here, not refused. `) +
78
97
  `Per-root detail: GET /v1/diagnostics/wiring storeDurability.roots.`);
79
98
  }
80
99
  }
81
- if (facts.parkFacility && facts.manifest.parkLane.checkpointDurability !== "declared_durable") {
100
+ else if (processLocalDeclaration !== undefined) {
101
+ const declaration = processLocalDeclaration;
102
+ warnings.push(`the local store declares itself process-local (${describeLocalStoreDeclaration(declaration)}): its session store and checkpoint store promise their rows survive a process restart, not a container / Pod replacement. ` +
103
+ `${processLocalMediumSentence(roots.find((r) => r.root === "data")?.verdict ?? "unknown", facts.store.durability, listed)} ` +
104
+ `To keep them across replacements, mount a persistent volume at each root above that does not read durable (LOCAL_DATA_ROOT / SEALED_KEY_DIR / MEMORY_ENGINE_DIR point the three roots at one)${declaration.rule === "declared_ephemeral" ? " and unset LOCAL_STORE_DURABILITY" : ""}.`);
105
+ }
106
+ if (facts.parkFacility && facts.manifest.parkLane.checkpointDurability !== "declared_durable" && processLocalDeclaration === undefined) {
82
107
  warnings.push(`park facility is enabled but the checkpoint store does not declare durable storage ` +
83
108
  `(parkLane.checkpointDurability=${JSON.stringify(facts.manifest.parkLane.checkpointDurability ?? null)}). ` +
84
109
  `A parked approval survives a restart only on a durable store, and this reading folds "declared process-local" together with "declared nothing" — every in-tree adapter declares "durable", so a non-durable reading here means a store this build does not own. ` +
@@ -15,10 +15,12 @@
15
15
  *
16
16
  * 拒绝词 → HTTP 状态(穷举 `switch`,core 加词 ⇒ 编译红):
17
17
  * · `not_member` → 404:「不同 org」「查无此人」「目录答不上来」同一份字节(门保证),404 与本仓反枚举的既有形一致;
18
- * · `rate_limited` → 429;
18
+ * · `rate_limited` → 429,体带 `retryAfterSec`、头带同值 `Retry-After`(门答案里的那一个数 —— 配额店按店钟算的到当前窗结束的整秒数;本口不再算一遍,
19
+ * 一个数两个投影,先例 `run-concurrency-cap.ts`);三界(尝试 / 对偶 / 聚合)同一句同一个数,另三只词没有这两样;
19
20
  * · `inbox_full` → 409(收件人那一侧的状态,不是发件人的速率;与 `steering.queue_full` 同档);
20
21
  * · `recipient_tombstoned` → 410(收件人正在被删除,这条路关了)。
21
22
  * 门缺席(本部署没声明收件箱消费方)⇒ 501 `capability.unavailable`,能力位 `principalInboxDelivery` 读同一只事实。
23
+ * 配额店读不出自己的状态 ⇒ 门抛 `OrgDeliveryQuotaUnreadableError`,由 HTTP 兜底(`http/server.ts`)答带因的 500(fail-closed,一封都没投)。
22
24
  */
23
25
  import type { IncomingMessage, ServerResponse } from "node:http";
24
26
  import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
@@ -10,7 +10,8 @@ function sendRefusal(res, outcome) {
10
10
  sendError(res, 404, "org_delivery.not_member", outcome.message);
11
11
  return;
12
12
  case "org_delivery.rate_limited":
13
- sendError(res, 429, "org_delivery.rate_limited", outcome.message);
13
+ res.setHeader("retry-after", String(outcome.retryAfterSec));
14
+ sendError(res, 429, "org_delivery.rate_limited", outcome.message, { retryAfterSec: outcome.retryAfterSec });
14
15
  return;
15
16
  case "org_delivery.inbox_full":
16
17
  sendError(res, 409, "org_delivery.inbox_full", outcome.message);
@@ -19,7 +20,7 @@ function sendRefusal(res, outcome) {
19
20
  sendError(res, 410, "org_delivery.recipient_tombstoned", outcome.message);
20
21
  return;
21
22
  default:
22
- return assertNever(outcome.code);
23
+ return assertNever(outcome);
23
24
  }
24
25
  }
25
26
  export async function handlePrincipalInbox(req, res, match, ctx) {
@@ -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";
@@ -30,6 +32,7 @@ import { assertNever } from "../route-ctx.js";
30
32
  import { readRunFace } from "../run-read-face.js";
31
33
  import { replayOfExistingRun } from "../submit-replay.js";
32
34
  import { mintAndDriveDurableRun } from "../durable-run-leg.js";
35
+ import { SUBMISSION_CLAIM_FACE } from "../execution-context.js";
33
36
  import { admitRunById, admitRunRow, enter, runRowAt } from "../owner-access.js";
34
37
  import { keyAdmission, ownerCellOf, recoveredSubject } from "../../owner-faces.js";
35
38
  import { personOf } from "../../owner-brands.js";
@@ -285,14 +288,13 @@ export async function handleRuns(req, res, match, ctx) {
285
288
  return;
286
289
  const run = cancelAdmitted.row;
287
290
  const rs = runStore;
288
- const cancelSuspended = async () => {
291
+ const cancelSuspended = async (parked) => {
289
292
  const cs = deps.checkpointStore;
290
293
  let note = "cancelled while suspended (pending approval settled)";
291
294
  if (cs) {
292
- const token = await cs.findPendingTokenBySession(run.sessionId, undefined, { gateKinds: LIVE_RUN_GATE_KINDS, axis: "run" });
295
+ const token = await runCancelCardOf(cs, run.sessionId);
293
296
  if (token) {
294
- const cp = await cs.get(token);
295
- const won = cp ? await cs.expire(token, cp.scope) : false;
297
+ const won = (await expireCardUnderStoredScope(cs, token)) === CARD_EXPIRE_OUTCOME.expired;
296
298
  if (!won) {
297
299
  const now = await rs.getRun(taskId);
298
300
  const st = now?.status;
@@ -326,7 +328,7 @@ export async function handleRuns(req, res, match, ctx) {
326
328
  }
327
329
  const err = note;
328
330
  try {
329
- const result = mintCancelledResult({ taskId, sessionId: run.sessionId, startedAtMs: Date.parse(run.createdAt), message: err, cause: "cancel_verb" });
331
+ const result = mintCancelledResult({ taskId, sessionId: run.sessionId, startedAtMs: Date.parse(run.createdAt), message: err, cause: "cancel_verb", parkedResult: parked });
330
332
  let lastErr;
331
333
  for (let attempt = 0;; attempt++) {
332
334
  try {
@@ -361,7 +363,7 @@ export async function handleRuns(req, res, match, ctx) {
361
363
  for (let attempt = 0; !flagged; attempt++) {
362
364
  const now = await runStore.getRun(taskId);
363
365
  if (now !== undefined && isParkedRunStatus(now.status)) {
364
- await cancelSuspended();
366
+ await cancelSuspended(now.result);
365
367
  return;
366
368
  }
367
369
  if (now?.status === "running" && attempt < CANCEL_RECLASSIFY_ATTEMPTS) {
@@ -379,7 +381,7 @@ export async function handleRuns(req, res, match, ctx) {
379
381
  sendJson(res, 202, { taskId, status: "cancelling", cancelRequested: true });
380
382
  }
381
383
  else if (isParkedRunStatus(run.status)) {
382
- await cancelSuspended();
384
+ await cancelSuspended(run.result);
383
385
  }
384
386
  else {
385
387
  sendJson(res, 202, { taskId, status: run.status, cancelRequested: run.cancelRequested, note: "run already terminal — cancel is a no-op" });
@@ -1526,6 +1528,7 @@ async function createDurableRunHeld(req, res, ctx, prepared, idem) {
1526
1528
  source,
1527
1529
  identity: runIdentityOf(req, deps.config, prepared.auth?.subject ?? null),
1528
1530
  runSlot,
1531
+ face: SUBMISSION_CLAIM_FACE,
1529
1532
  });
1530
1533
  const receipt = out.existing ? await replayOfExistingRun(out.existing, caller, "durable", deps, runStore) : out;
1531
1534
  await prepared.seat.close();
@@ -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()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。 */
@@ -18,26 +18,44 @@ import { SHA256_HEX_RE } from "../../digest-form.js";
18
18
  import { assertNever } from "../route-ctx.js";
19
19
  import { readSessionRulesRecords, describeSessionRulesRecordProblem } from "../../session-policy-wire.js";
20
20
  import { readSessionPinned, readSessionTitle } from "../../session-row.js";
21
- import { sessionFaceOfHead } from "../../session-incarnation.js";
21
+ import { deletionHeadReadOf, sessionFaceOfHead } from "../../session-incarnation.js";
22
22
  import { defaultTaskRegistry } from "@sema-agent/core";
23
23
  import { SessionActiveRunError, SessionForkInProgressError, SessionWriterPresentError } from "../../execution-placement.js";
24
24
  import { probeSessionChildren, sessionTenantScope } from "../../session-children-retire.js";
25
25
  import { liveChildrenRefusal } from "./background.js";
26
+ export function importLeaseKey(landingOwner, sessionId) {
27
+ return JSON.stringify([landingOwner, sessionId]);
28
+ }
26
29
  export function createSessionSyncLocal() {
27
30
  const pendingStagings = new Map();
28
31
  const importLeases = new Map();
29
- const touchImportLease = (sessionId) => {
30
- const cur = importLeases.get(sessionId);
32
+ const touchImportLease = (leaseKey) => {
33
+ const cur = importLeases.get(leaseKey);
31
34
  if (cur)
32
- importLeases.set(sessionId, { ...cur, at: Date.now() });
35
+ importLeases.set(leaseKey, { ...cur, at: Date.now() });
33
36
  };
34
37
  const cleanupStaging = (stagingId) => {
35
38
  const p = pendingStagings.get(stagingId);
36
39
  if (p)
37
- importLeases.delete(p.realSessionId);
40
+ importLeases.delete(p.leaseKey);
38
41
  pendingStagings.delete(stagingId);
39
42
  };
40
- return { pendingStagings, importLeases, touchImportLease, cleanupStaging };
43
+ const sweepStaleLeases = (staleMs) => {
44
+ if (staleMs <= 0)
45
+ return [];
46
+ const now = Date.now();
47
+ const swept = [];
48
+ for (const [key, lease] of [...importLeases]) {
49
+ const ageMs = now - lease.at;
50
+ if (ageMs < staleMs)
51
+ continue;
52
+ swept.push({ sessionId: lease.sessionId, stagingId: lease.stagingId, ageMs });
53
+ cleanupStaging(lease.stagingId);
54
+ importLeases.delete(key);
55
+ }
56
+ return swept;
57
+ };
58
+ return { pendingStagings, importLeases, touchImportLease, cleanupStaging, sweepStaleLeases };
41
59
  }
42
60
  async function firstPhaseRefusal(sessionStorage, sessionId, relation, consented) {
43
61
  if (consented || (relation !== "fork" && relation !== "stale"))
@@ -57,7 +75,7 @@ async function firstPhaseRefusal(sessionStorage, sessionId, relation, consented)
57
75
  export async function handleSessionSync(req, res, match, ctx) {
58
76
  const { deps } = ctx;
59
77
  const { readJson, readRawBody, safeDecode, rateLimited } = ctx.helpers;
60
- const { pendingStagings, importLeases, touchImportLease, cleanupStaging } = ctx.local.sessionSync;
78
+ const { pendingStagings, importLeases, touchImportLease, cleanupStaging, sweepStaleLeases } = ctx.local.sessionSync;
61
79
  {
62
80
  {
63
81
  const sync = facts.sessionSync(deps);
@@ -77,8 +95,9 @@ export async function handleSessionSync(req, res, match, ctx) {
77
95
  }
78
96
  const judgeSessionGate = async () => {
79
97
  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 };
98
+ const held = face ?? sessionFaceOfHead(sessionId, await deletionHeadReadOf(sessionStorage)?.(sessionId));
99
+ const mayTouch = held !== undefined && (await judgeSession(syncEntry, sessionStorage, sessionId, held)) !== undefined;
100
+ return { face, occupant: held, mayPull: face !== undefined && mayTouch, mayPush: held === undefined || mayTouch };
82
101
  };
83
102
  let gate;
84
103
  try {
@@ -408,6 +427,8 @@ export async function handleSessionSync(req, res, match, ctx) {
408
427
  if (!payloadVerified)
409
428
  digestSaysDiffer = true;
410
429
  }
430
+ const importingPrincipal = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
431
+ const operatorOk = syncEntry.caller.operator;
411
432
  const placed = gateFace === undefined ? undefined : await sessionStorage.executionPlacement?.read(sessionId);
412
433
  if (placed !== undefined) {
413
434
  switch (placed.cell.role) {
@@ -464,10 +485,9 @@ export async function handleSessionSync(req, res, match, ctx) {
464
485
  return;
465
486
  }
466
487
  }
467
- const opOk = syncEntry.caller.operator;
468
488
  if (deps.sessionPolicyStore) {
469
489
  try {
470
- await replayPolicyRecords(deps.sessionPolicyStore, sessionId, policy, opOk);
490
+ await replayPolicyRecords(deps.sessionPolicyStore, sessionId, policy, operatorOk);
471
491
  }
472
492
  catch (e) {
473
493
  if (sendSessionPolicyError(res, e))
@@ -475,10 +495,9 @@ export async function handleSessionSync(req, res, match, ctx) {
475
495
  throw e;
476
496
  }
477
497
  }
478
- const importingP = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
479
498
  if (deps.resumeAnchorStore)
480
499
  for (const a of anchors)
481
- await deps.resumeAnchorStore.put(sessionId, a.eventId, a.entryId, importingP);
500
+ await deps.resumeAnchorStore.put(sessionId, a.eventId, a.entryId, importingPrincipal);
482
501
  if (!(await replaySessionRow(row, gateFace))) {
483
502
  sendError(res, 409, "conflict", "sync conflict");
484
503
  return;
@@ -490,33 +509,26 @@ export async function handleSessionSync(req, res, match, ctx) {
490
509
  });
491
510
  return;
492
511
  }
493
- const heldLease = importLeases.get(sessionId);
512
+ const leaseKey = importLeaseKey(gate.occupant !== undefined ? gate.occupant.owner : importingPrincipal, sessionId);
513
+ const staleSecRaw = deps.config.syncImportLeaseStaleSec;
514
+ const staleSec = typeof staleSecRaw === "number" && Number.isFinite(staleSecRaw) ? staleSecRaw : 600;
515
+ const staleMs = staleSec * 1000;
516
+ for (const swept of sweepStaleLeases(staleMs))
517
+ deps.logger?.warn?.("sync_import_lease_taken_over", { sessionId: swept.sessionId, ageMs: swept.ageMs, staleMs, stagingId: swept.stagingId });
518
+ const heldLease = importLeases.get(leaseKey);
494
519
  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
- }
520
+ const retryAfterSec = staleMs > 0 ? Math.max(1, Math.ceil((staleMs - (Date.now() - heldLease.at)) / 1000)) : undefined;
521
+ sendError(res, 409, "import_in_flight", "an import is already in flight for this session"
522
+ + (retryAfterSec !== undefined ? ` (a stalled holder is taken over after ${staleSec}s of inactivity; retry in ~${retryAfterSec}s)` : ""), retryAfterSec !== undefined ? { retryAfterSec } : undefined);
523
+ return;
510
524
  }
511
- const importingPrincipal = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
512
- const operatorOk = syncEntry.caller.operator;
513
525
  const token = newStagingToken();
514
526
  const handle = sessionStorage.beginImportStaging(sessionId, token);
515
527
  const stagingId = handle.stagingId;
516
- importLeases.set(sessionId, { at: Date.now(), stagingId });
528
+ importLeases.set(leaseKey, { at: Date.now(), stagingId, sessionId });
517
529
  pendingStagings.set(stagingId, {
518
530
  handle, realSessionId: sessionId, owner: importingPrincipal, ...(resolution ? { resolution } : {}), operatorOk,
519
- fileHistory, policy, anchors, row, declaredEntryIds: entryIds, openedBy: ownerKeyOf(scope.gateOwner), expectedOwner: owner,
531
+ fileHistory, policy, anchors, row, declaredEntryIds: entryIds, openedBy: ownerKeyOf(scope.gateOwner), expectedOwner: owner, leaseKey,
520
532
  });
521
533
  sendJson(res, 200, { stagingId, relation: relA.relation });
522
534
  return;
@@ -550,7 +562,7 @@ export async function handleSessionSync(req, res, match, ctx) {
550
562
  const utf8 = new StringDecoder("utf8");
551
563
  try {
552
564
  for await (const chunk of req) {
553
- touchImportLease(pending.realSessionId);
565
+ touchImportLease(pending.leaseKey);
554
566
  lineBuf += utf8.write(chunk);
555
567
  if (Buffer.byteLength(lineBuf, "utf8") > SYNC_NDJSON_LINE_MAX_BYTES)
556
568
  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";