@sema-agent/server 7.138.0 → 7.139.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 (215) hide show
  1. package/MIGRATION.md +276 -7
  2. package/README.md +7 -4
  3. package/README.zh-CN.md +6 -4
  4. package/USAGE.md +73 -29
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/lib/lanes.json +3 -2
  7. package/dist/adoption/wire.d.ts +3 -3
  8. package/dist/approval-card.d.ts +20 -20
  9. package/dist/bench/s1/arms.d.ts +1 -1
  10. package/dist/bench/s1/arms.js +1 -3
  11. package/dist/bench/s1/run-firm.js +2 -2
  12. package/dist/boot/device-lane.d.ts +8 -3
  13. package/dist/boot/device-lane.js +7 -2
  14. package/dist/boot/execution-env.d.ts +4 -2
  15. package/dist/boot/execution-env.js +16 -24
  16. package/dist/boot/execution-lane.d.ts +1 -1
  17. package/dist/boot/governance-seams.d.ts +3 -2
  18. package/dist/boot/governance-seams.js +9 -11
  19. package/dist/boot/memory-consolidation.js +2 -2
  20. package/dist/boot/resolve-spec.d.ts +1 -1
  21. package/dist/boot/resolve-spec.js +8 -9
  22. package/dist/boot/retention-lane.d.ts +5 -5
  23. package/dist/boot/retention-lane.js +0 -6
  24. package/dist/boot/runner-deps.d.ts +3 -1
  25. package/dist/boot/runner-deps.js +2 -1
  26. package/dist/boot/session-faces.d.ts +0 -3
  27. package/dist/boot/session-faces.js +15 -8
  28. package/dist/boot/stage-02-web-admin-pull.js +2 -2
  29. package/dist/boot/stage-05-execution-env.js +2 -2
  30. package/dist/boot/stage-06-runners.js +0 -1
  31. package/dist/boot/stage-09-leader.js +1 -2
  32. package/dist/boot/stage-10-http-server.js +2 -2
  33. package/dist/boot/stores.js +4 -4
  34. package/dist/boot/web-admin.js +2 -2
  35. package/dist/capabilities/mcp-probe.js +1 -1
  36. package/dist/capabilities/scenarios.d.ts +1 -1
  37. package/dist/capability-facts.d.ts +15 -7
  38. package/dist/capability-facts.js +8 -3
  39. package/dist/config-catalog.js +7 -7
  40. package/dist/config-invariants.d.ts +23 -3
  41. package/dist/config-invariants.js +42 -0
  42. package/dist/config-provider.d.ts +19 -24
  43. package/dist/config-provider.js +27 -12
  44. package/dist/config-types.d.ts +10 -5
  45. package/dist/config.d.ts +33 -0
  46. package/dist/config.js +44 -15
  47. package/dist/delegated-session-owner.d.ts +2 -2
  48. package/dist/deployment-shape.d.ts +1 -1
  49. package/dist/device-enrollment.d.ts +41 -2
  50. package/dist/device-enrollment.js +27 -12
  51. package/dist/device-store.d.ts +58 -85
  52. package/dist/device-store.js +19 -49
  53. package/dist/device-ws-hub.js +2 -1
  54. package/dist/env-name-allowlist-knobs.d.ts +5 -4
  55. package/dist/exec-cut.d.ts +60 -0
  56. package/dist/exec-cut.js +36 -0
  57. package/dist/execution-lane-caps.d.ts +19 -0
  58. package/dist/execution-lane-caps.js +10 -0
  59. package/dist/execution-placement.d.ts +259 -0
  60. package/dist/execution-placement.js +199 -0
  61. package/dist/field-conflict.d.ts +3 -2
  62. package/dist/field-conflict.js +1 -0
  63. package/dist/hosted-posture.d.ts +2 -2
  64. package/dist/http/admission.js +8 -19
  65. package/dist/http/checkpoint-ctx.d.ts +21 -12
  66. package/dist/http/checkpoint-ctx.js +4 -2
  67. package/dist/http/claimed-run.d.ts +1 -1
  68. package/dist/http/durable-run-leg.js +9 -5
  69. package/dist/http/execution-context.d.ts +218 -0
  70. package/dist/http/execution-context.js +330 -0
  71. package/dist/http/resume-legs.js +50 -20
  72. package/dist/http/route-ctx.d.ts +14 -8
  73. package/dist/http/routes/background.d.ts +29 -1
  74. package/dist/http/routes/background.js +29 -2
  75. package/dist/http/routes/devices.d.ts +13 -19
  76. package/dist/http/routes/devices.js +21 -26
  77. package/dist/http/routes/session-sync.js +110 -15
  78. package/dist/http/routes/sessions.d.ts +6 -0
  79. package/dist/http/routes/sessions.js +115 -11
  80. package/dist/http/routes/side-query.d.ts +12 -4
  81. package/dist/http/routes/side-query.js +4 -5
  82. package/dist/http/routes/tasks.js +37 -31
  83. package/dist/http/server.d.ts +35 -19
  84. package/dist/http/server.js +11 -0
  85. package/dist/http/session-inherit-env.d.ts +3 -4
  86. package/dist/http/session-inherit-env.js +1 -1
  87. package/dist/http/wire-types.d.ts +8 -12
  88. package/dist/index.d.ts +1 -0
  89. package/dist/memory-sync-client.d.ts +53 -14
  90. package/dist/memory-sync-client.js +196 -116
  91. package/dist/memory-sync.d.ts +1 -1
  92. package/dist/memory-sync.js +3 -2
  93. package/dist/observability/cost-taxonomy.d.ts +2 -2
  94. package/dist/observability/fail-open.d.ts +12 -0
  95. package/dist/observability/fail-open.js +12 -0
  96. package/dist/observability/run-terminal.js +0 -1
  97. package/dist/orchestration/workflow-notify-journal.js +4 -4
  98. package/dist/org-memory-admission.d.ts +4 -3
  99. package/dist/org-memory-admission.js +6 -6
  100. package/dist/parked-decide.d.ts +34 -9
  101. package/dist/parked-decide.js +12 -19
  102. package/dist/parked-revive-surface.d.ts +12 -15
  103. package/dist/parked-revive-surface.js +3 -6
  104. package/dist/peer-seat.d.ts +13 -7
  105. package/dist/peer-seat.js +37 -3
  106. package/dist/plugins/approval-ask-store-file.d.ts +7 -0
  107. package/dist/plugins/approval-ask-store-file.js +11 -0
  108. package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
  109. package/dist/plugins/approval-ask-store-memory.js +12 -0
  110. package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
  111. package/dist/plugins/approval-ask-store-sql.js +30 -12
  112. package/dist/plugins/caching-session-store.d.ts +6 -2
  113. package/dist/plugins/caching-session-store.js +9 -3
  114. package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
  115. package/dist/plugins/checkpoint-store-sql.js +5 -7
  116. package/dist/plugins/device-store-sql.d.ts +25 -51
  117. package/dist/plugins/device-store-sql.js +49 -103
  118. package/dist/plugins/execution-placement-sql.d.ts +151 -0
  119. package/dist/plugins/execution-placement-sql.js +223 -0
  120. package/dist/plugins/file-history-store-sql.d.ts +7 -6
  121. package/dist/plugins/file-history-store-sql.js +5 -5
  122. package/dist/plugins/file-run-store.d.ts +6 -1
  123. package/dist/plugins/file-run-store.js +4 -0
  124. package/dist/plugins/fork-routing-session-store.d.ts +6 -0
  125. package/dist/plugins/fork-routing-session-store.js +16 -0
  126. package/dist/plugins/local-session-store.d.ts +149 -50
  127. package/dist/plugins/local-session-store.js +382 -107
  128. package/dist/plugins/memory-key-guards.d.ts +4 -0
  129. package/dist/plugins/memory-key-guards.js +1 -0
  130. package/dist/plugins/memory-run-store.d.ts +1 -1
  131. package/dist/plugins/one-time-migrations.d.ts +111 -3
  132. package/dist/plugins/one-time-migrations.js +380 -10
  133. package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
  134. package/dist/plugins/permission-rule-store-sql.d.ts +13 -15
  135. package/dist/plugins/permission-rule-store-sql.js +13 -16
  136. package/dist/plugins/pg-pool.d.ts +7 -3
  137. package/dist/plugins/pg-session-storage.d.ts +31 -20
  138. package/dist/plugins/pg-session-storage.js +120 -52
  139. package/dist/plugins/remote-env-adb.d.ts +20 -4
  140. package/dist/plugins/remote-env-adb.js +70 -62
  141. package/dist/plugins/remote-env-device.d.ts +30 -12
  142. package/dist/plugins/remote-env-device.js +18 -17
  143. package/dist/plugins/remote-env-e2b.d.ts +18 -4
  144. package/dist/plugins/remote-env-e2b.js +41 -42
  145. package/dist/plugins/remote-env-file-error.d.ts +2 -0
  146. package/dist/plugins/remote-env-file-error.js +1 -1
  147. package/dist/plugins/remote-env-host.d.ts +21 -8
  148. package/dist/plugins/remote-env-host.js +66 -70
  149. package/dist/plugins/remote-env-k8s.d.ts +11 -16
  150. package/dist/plugins/remote-env-k8s.js +61 -54
  151. package/dist/plugins/remote-env-local-docker.d.ts +29 -6
  152. package/dist/plugins/remote-env-local-docker.js +46 -59
  153. package/dist/plugins/remote-env-ssh.d.ts +16 -4
  154. package/dist/plugins/remote-env-ssh.js +72 -56
  155. package/dist/plugins/remote-shell.d.ts +100 -3
  156. package/dist/plugins/remote-shell.js +166 -2
  157. package/dist/plugins/retention-store-sql.d.ts +9 -6
  158. package/dist/plugins/retention-store-sql.js +7 -1
  159. package/dist/plugins/run-store-sql.d.ts +13 -3
  160. package/dist/plugins/run-store-sql.js +5 -0
  161. package/dist/plugins/schema-consistency.d.ts +15 -5
  162. package/dist/plugins/schema-consistency.js +14 -8
  163. package/dist/plugins/schema-drift-sentence.d.ts +27 -0
  164. package/dist/plugins/schema-drift-sentence.js +38 -0
  165. package/dist/plugins/session-fork.d.ts +54 -0
  166. package/dist/plugins/session-fork.js +38 -1
  167. package/dist/plugins/session-row-sql.d.ts +22 -0
  168. package/dist/plugins/session-row-sql.js +81 -0
  169. package/dist/plugins/session-store.d.ts +1 -1
  170. package/dist/plugins/store-backend.d.ts +1 -1
  171. package/dist/plugins/store-backend.js +19 -7
  172. package/dist/plugins/store-contracts.d.ts +25 -5
  173. package/dist/plugins/tidb-pool.d.ts +12 -7
  174. package/dist/plugins/tidb-pool.js +9 -3
  175. package/dist/plugins/tidb-session-store.d.ts +57 -26
  176. package/dist/plugins/tidb-session-store.js +118 -50
  177. package/dist/plugins/tool-result-store-sql.d.ts +3 -2
  178. package/dist/plugins/tool-result-store-sql.js +18 -10
  179. package/dist/project-memory.d.ts +9 -15
  180. package/dist/project-memory.js +5 -4
  181. package/dist/request-key-closure.js +1 -1
  182. package/dist/request-value-closure.d.ts +6 -7
  183. package/dist/request-value-closure.js +1 -3
  184. package/dist/run-local.d.ts +6 -0
  185. package/dist/run-local.js +24 -20
  186. package/dist/runs.d.ts +2 -1
  187. package/dist/runs.js +3 -1
  188. package/dist/security.d.ts +38 -12
  189. package/dist/security.js +8 -2
  190. package/dist/server-secret-env.d.ts +9 -5
  191. package/dist/session-children-retire.d.ts +27 -9
  192. package/dist/session-children-retire.js +9 -5
  193. package/dist/session-incarnation.d.ts +19 -9
  194. package/dist/session-incarnation.js +4 -2
  195. package/dist/session-row.d.ts +114 -5
  196. package/dist/session-row.js +55 -5
  197. package/dist/session-titler.d.ts +2 -2
  198. package/dist/task-a2a.d.ts +1 -1
  199. package/dist/task-cwd.d.ts +6 -1
  200. package/dist/task-mcp.d.ts +3 -3
  201. package/dist/tool-approval.d.ts +6 -0
  202. package/dist/tool-approval.js +8 -1
  203. package/dist/trace/project.d.ts +1 -0
  204. package/dist/trace/project.js +1 -0
  205. package/dist/trace/self-minted-wire.d.ts +111 -2
  206. package/dist/trace/self-minted-wire.js +38 -1
  207. package/dist/url-refusal.d.ts +1 -1
  208. package/dist/web-admin-client/apply-effective.d.ts +17 -3
  209. package/dist/web-admin-client/apply-effective.js +20 -10
  210. package/dist/web-admin-client/skills-mcp.js +3 -2
  211. package/dist/web-admin-client/types.d.ts +6 -0
  212. package/package.json +3 -3
  213. package/wire-changes/server@7.139.0.json +348 -0
  214. package/dist/http/device-placement.d.ts +0 -175
  215. package/dist/http/device-placement.js +0 -145
@@ -24,7 +24,8 @@ import { collectSsePreamble } from "../sse-log.js";
24
24
  import { buildApprovalPreamble, buildApprovalPreambleSseFrames } from "../../approval-card.js";
25
25
  import { approvalPreambleReplays, createApprovalCardEmitter, declaredApprovalFrameFamilies, resolveApprovalLeg, resolveStreamApprovalGate } from "../../tool-approval.js";
26
26
  import { buildActiveRunConflict, isActiveRunConflictBody, toDoneFrameResult } from "../active-run-conflict.js";
27
- import { commitPlacement, devicePlacementErrorBody } from "../device-placement.js";
27
+ import { commitExecutionContext } from "../execution-context.js";
28
+ import { rootSessionAbsentAnswer } from "../execution-context.js";
28
29
  import { commitSessionInheritEnv } from "../session-inherit-env.js";
29
30
  import { headerStr, gatedPrincipal, explicitOperatorOk, runIdentityOf } from "../principal-gate.js";
30
31
  import { failOpenTagForDroppedFrame, recordFailOpen } from "../../observability/fail-open.js";
@@ -177,7 +178,7 @@ export async function handleTasks(req, res, match, ctx) {
177
178
  sendError(res, 400, "request.field_conflict", "verify / cascade are not supported on /v1/tasks/stream (both are multi-attempt, not a single stream) — use /v1/tasks or /v1/runs", fieldConflictFacts("cascade_verify", prepared.verify ? (prepared.cascade ? ["verify", "cascade"] : ["verify"]) : ["cascade"]));
178
179
  return;
179
180
  }
180
- const earlyDurableTid = facts.durableRuns(deps).available && prepared.spec.sessionId ? uuidv7() : undefined;
181
+ const earlyDurableTid = facts.durableRuns(deps).available ? uuidv7() : undefined;
181
182
  const streamLegStartedAt = Date.now();
182
183
  const detachOnDisconnect = headerStr(req.headers["x-detach-on-disconnect"]) === "true";
183
184
  if (detachOnDisconnect && earlyDurableTid === undefined) {
@@ -289,19 +290,19 @@ export async function handleTasks(req, res, match, ctx) {
289
290
  let runRowSettled = false;
290
291
  let durableTaskId;
291
292
  let durableHeartbeat;
292
- const cancelledResult = (errorMessage, cause) => mintCancelledResult({ taskId: durableTaskId ?? "", sessionId: prepared.spec.sessionId ?? "", startedAtMs: streamLegStartedAt, message: errorMessage, cause });
293
+ const cancelledResult = (errorMessage, cause) => mintCancelledResult({ taskId: durableTaskId ?? "", sessionId: prepared.spec.sessionId, startedAtMs: streamLegStartedAt, message: errorMessage, cause });
293
294
  let userMsgEntryId;
294
295
  const promptCommit = createPromptCommitReader();
295
296
  let anchorPut = false;
296
297
  const putRewindAnchor = () => {
297
- if (anchorPut || !userMsgEntryId || !durableTaskId || !prepared.spec.sessionId || !deps.resumeAnchorStore)
298
+ if (anchorPut || !userMsgEntryId || !durableTaskId || !deps.resumeAnchorStore)
298
299
  return;
299
300
  anchorPut = true;
300
301
  void deps.resumeAnchorStore
301
302
  .put(prepared.spec.sessionId, durableTaskId, userMsgEntryId, principal ?? null)
302
303
  .catch(() => deps.metrics?.inc("resume_anchor_capture_failed"));
303
304
  };
304
- if (deps.runStore && prepared.spec.sessionId) {
305
+ if (deps.runStore) {
305
306
  const tid = earlyDurableTid ?? uuidv7();
306
307
  const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", { ...runMeta(prepared, source), identity: runIdentityOf(req, deps.config, principal ?? null), idem: idem ?? null });
307
308
  if (created.ok)
@@ -313,22 +314,25 @@ export async function handleTasks(req, res, match, ctx) {
313
314
  sseData(res, { type: "done", result: doneResultOfOutcome(replay), replay: true });
314
315
  return replay;
315
316
  }
317
+ if (created.sessionAbsent) {
318
+ const refused = rootSessionAbsentAnswer();
319
+ sseData(res, { type: "done", result: doneResultOfOutcome(refused) });
320
+ return refused;
321
+ }
316
322
  if (!created.ok) {
317
323
  const conflict = await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, prepared.spec.sessionId, created.activeTaskId);
318
324
  const refused = { status: 409, body: conflict };
319
325
  sseData(res, { type: "done", result: doneResultOfOutcome(refused) });
320
326
  return refused;
321
327
  }
322
- const placementReject = await commitPlacement(deps, deps.runStore, tid, prepared.bindingIntent);
328
+ const placementReject = await commitExecutionContext(deps, { kind: "run", runStore: deps.runStore, taskId: tid }, prepared.execution);
323
329
  if (placementReject) {
324
- const body = devicePlacementErrorBody(placementReject);
325
- const refused = { status: placementReject.status, body };
326
- sseData(res, { type: "done", result: doneResultOfOutcome(refused) });
327
- return refused;
330
+ sseData(res, { type: "done", result: doneResultOfOutcome(placementReject) });
331
+ return placementReject;
328
332
  }
329
333
  durableTaskId = tid;
330
334
  }
331
- if (deps.checkpointStore && prepared.spec.sessionId) {
335
+ if (deps.checkpointStore) {
332
336
  const ctxRefusal = await persistSubmitCtx(deps.checkpointStore, deps, durableTaskId !== undefined && deps.runStore ? { runStore: deps.runStore, taskId: durableTaskId } : undefined, prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope, writerVersion: serviceVersion() });
333
337
  if (ctxRefusal) {
334
338
  sseData(res, { type: "done", result: doneResultOfOutcome(ctxRefusal) });
@@ -336,8 +340,7 @@ export async function handleTasks(req, res, match, ctx) {
336
340
  }
337
341
  }
338
342
  commitSessionInheritEnv(deps, prepared.spec.sessionId, prepared.body);
339
- if (prepared.spec.sessionId)
340
- markChildrenStoppedByUserOnAbort(ac.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
343
+ markChildrenStoppedByUserOnAbort(ac.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
341
344
  let ledgerSink;
342
345
  let ledgerTerminal = false;
343
346
  if (durableTaskId && deps.runStore) {
@@ -412,7 +415,7 @@ export async function handleTasks(req, res, match, ctx) {
412
415
  }
413
416
  : {}),
414
417
  });
415
- if (deps.userInputLedger && durableTaskId && prepared.spec.sessionId)
418
+ if (deps.userInputLedger && durableTaskId)
416
419
  await deps.userInputLedger.announceUndrained({ sessionId: prepared.spec.sessionId, taskId: durableTaskId });
417
420
  try {
418
421
  const streamBody = async () => {
@@ -420,7 +423,7 @@ export async function handleTasks(req, res, match, ctx) {
420
423
  const tailContentMode = prepared.spec.forwardSubagentEvents === true ? "on" : "progress_only";
421
424
  const childReasoning = createReasoningSegments();
422
425
  const topReasoning = createReasoningSegments();
423
- const heldDecisions = prepared.spec.sessionId !== undefined ? await heldWorkflowParkDecisions(deps, prepared.spec.sessionId) : {};
426
+ const heldDecisions = await heldWorkflowParkDecisions(deps, prepared.spec.sessionId);
424
427
  const fwdInternals = {
425
428
  ...heldDecisions,
426
429
  onForwardEvent: (e) => {
@@ -476,7 +479,7 @@ export async function handleTasks(req, res, match, ctx) {
476
479
  }
477
480
  const hadRow = isFleetAgentTerminalNotification(n) ? (fleetPub?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false) : false;
478
481
  const deliverable = syncLegLive && !res.writableEnded && !res.destroyed;
479
- const parked = !deliverable && Boolean(deps.workflowCompletionInbox && prepared.spec.sessionId);
482
+ const parked = !deliverable && deps.workflowCompletionInbox !== undefined;
480
483
  deps.logger?.info?.("task_notification_observed", { route: "sync-stream", taskId: n.task_id, taskType: n.task_type, status: n.status, hadFleetRow: hadRow, legLive: syncLegLive, parkedDurable: parked });
481
484
  if (parked) {
482
485
  void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(prepared.spec.sessionId, askOwner, n, Date.now(), durableTaskId, opts?.priority)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "sync-stream", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
@@ -521,7 +524,7 @@ export async function handleTasks(req, res, match, ctx) {
521
524
  if (durableTaskId && finalResult)
522
525
  finalResult = { ...finalResult, taskId: durableTaskId };
523
526
  if (causeAtDone !== undefined && finalResult !== undefined) {
524
- finalResult = mintCancelledResult({ taskId: durableTaskId ?? "", sessionId: prepared.spec.sessionId ?? "", startedAtMs: streamLegStartedAt, message: "cancelled by user", cause: causeAtDone, engineResult: finalResult });
527
+ finalResult = mintCancelledResult({ taskId: durableTaskId ?? "", sessionId: prepared.spec.sessionId, startedAtMs: streamLegStartedAt, message: "cancelled by user", cause: causeAtDone, engineResult: finalResult });
525
528
  }
526
529
  finalizeTaskResult(ev.result, principal, prepared.spec.objective);
527
530
  noteRunOutcome(finalResult);
@@ -714,7 +717,8 @@ export async function handleTasks(req, res, match, ctx) {
714
717
  taskId: askTaskId,
715
718
  owner: askOwner,
716
719
  abortSignal: ac.signal,
717
- ...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}),
720
+ sessionId: prepared.spec.sessionId,
721
+ executionLane: prepared.execution.lane,
718
722
  ...(streamApprovalOn
719
723
  ? {
720
724
  emitCard,
@@ -728,11 +732,12 @@ export async function handleTasks(req, res, match, ctx) {
728
732
  if (deps.toolApproval) {
729
733
  const toolApproval = deps.toolApproval;
730
734
  prepared.spec.onAsk = unattended
731
- ? toolApproval.boundUnattendedAsk({ owner: askOwner, taskId: askTaskId, ...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}) }, unattended)
735
+ ? toolApproval.boundUnattendedAsk({ owner: askOwner, taskId: askTaskId, sessionId: prepared.spec.sessionId, executionLane: prepared.execution.lane }, unattended)
732
736
  : toolApproval.boundAsk({
733
737
  owner: askOwner,
734
738
  taskId: askTaskId,
735
- ...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}),
739
+ sessionId: prepared.spec.sessionId,
740
+ executionLane: prepared.execution.lane,
736
741
  ...(streamApprovalOn ? { legKey: "", ...(legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic } : {}) } : {}),
737
742
  });
738
743
  }
@@ -815,7 +820,7 @@ export async function handleTasks(req, res, match, ctx) {
815
820
  try {
816
821
  const row = await deps.runStore.getRun(durableTaskId);
817
822
  if (row?.status === "running") {
818
- const pending = prepared.spec.sessionId ? await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId) : undefined;
823
+ const pending = await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId);
819
824
  const backstopCause = ac.cause();
820
825
  if (pending) {
821
826
  if (ledgerSink && !ledgerTerminal) {
@@ -896,7 +901,7 @@ export async function handleTasks(req, res, match, ctx) {
896
901
  const syncLegStartedAt = Date.now();
897
902
  let durableTaskId;
898
903
  let durableHeartbeat;
899
- if (deps.runStore && prepared.spec.sessionId) {
904
+ if (deps.runStore) {
900
905
  const tid = uuidv7();
901
906
  const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", { ...runMeta(prepared, source), identity: runIdentityOf(req, deps.config, principal ?? null), idem: idem ?? null });
902
907
  if (created.ok)
@@ -905,22 +910,23 @@ export async function handleTasks(req, res, match, ctx) {
905
910
  recordResourceWindow(tid, prepared.spec.resourceSuspend);
906
911
  if (created.existing)
907
912
  return await replayOfExistingRun(created.existing, { principal: submitter, idem }, "sync", deps, deps.runStore);
913
+ if (created.sessionAbsent)
914
+ return rootSessionAbsentAnswer();
908
915
  if (!created.ok)
909
916
  return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, prepared.spec.sessionId, created.activeTaskId) };
910
- const placementReject = await commitPlacement(deps, deps.runStore, tid, prepared.bindingIntent);
917
+ const placementReject = await commitExecutionContext(deps, { kind: "run", runStore: deps.runStore, taskId: tid }, prepared.execution);
911
918
  if (placementReject)
912
- return { status: placementReject.status, body: devicePlacementErrorBody(placementReject) };
919
+ return placementReject;
913
920
  durableTaskId = tid;
914
921
  }
915
- if (deps.checkpointStore && prepared.spec.sessionId) {
922
+ if (deps.checkpointStore) {
916
923
  const ctxRefusal = await persistSubmitCtx(deps.checkpointStore, deps, durableTaskId !== undefined && deps.runStore ? { runStore: deps.runStore, taskId: durableTaskId } : undefined, prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope, writerVersion: serviceVersion() });
917
924
  if (ctxRefusal)
918
925
  return ctxRefusal;
919
926
  }
920
927
  commitSessionInheritEnv(deps, prepared.spec.sessionId, prepared.body);
921
928
  const syncCancelCtrl = createRunAbort();
922
- if (prepared.spec.sessionId)
923
- markChildrenStoppedByUserOnAbort(syncCancelCtrl.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
929
+ markChildrenStoppedByUserOnAbort(syncCancelCtrl.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
924
930
  if (durableTaskId && deps.runStore) {
925
931
  const rs = deps.runStore;
926
932
  const tid = durableTaskId;
@@ -950,7 +956,7 @@ export async function handleTasks(req, res, match, ctx) {
950
956
  let result;
951
957
  counters.uncountedBillableInflight++;
952
958
  try {
953
- const heldDecisions = prepared.spec.sessionId !== undefined ? await heldWorkflowParkDecisions(deps, prepared.spec.sessionId) : {};
959
+ const heldDecisions = await heldWorkflowParkDecisions(deps, prepared.spec.sessionId);
954
960
  const verifyLeg = (v) => runWithVerification(prepared.runner, specWithSignal, v, heldDecisions);
955
961
  const cascadeLeg = () => runCascade(prepared.runner, specWithSignal, cascadeConfig(deps.config.cascadeLadder, prepared.spec.limits?.maxCostUsd), heldDecisions);
956
962
  const plainLeg = async () => {
@@ -976,7 +982,7 @@ export async function handleTasks(req, res, match, ctx) {
976
982
  if (syncAbortCause !== undefined) {
977
983
  result = mintCancelledResult({
978
984
  taskId: durableTaskId ?? "",
979
- sessionId: prepared.spec.sessionId ?? "",
985
+ sessionId: prepared.spec.sessionId,
980
986
  startedAtMs: syncLegStartedAt,
981
987
  message: "cancelled by user",
982
988
  cause: syncAbortCause,
@@ -998,11 +1004,11 @@ export async function handleTasks(req, res, match, ctx) {
998
1004
  }
999
1005
  const exitResult = await mintSubmitExitResult(result, deps.checkpointStore);
1000
1006
  const syncPlane = outwardPlane(result.terminal);
1001
- if (syncPlane.status === "suspended" && deps.checkpointStore && deps.runStore && durableTaskId && prepared.spec.sessionId) {
1007
+ if (syncPlane.status === "suspended" && deps.checkpointStore && deps.runStore && durableTaskId) {
1002
1008
  await deps.runStore.setSuspended(durableTaskId, exitResult);
1003
1009
  return { status: 200, body: withSyncSubmitKeys(buildSyncSubmitBody(exitResult, { taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "suspended" })) };
1004
1010
  }
1005
- if (syncPlane.status === "needs_review" && deps.checkpointStore && deps.runStore && durableTaskId && prepared.spec.sessionId) {
1011
+ if (syncPlane.status === "needs_review" && deps.checkpointStore && deps.runStore && durableTaskId) {
1006
1012
  await deps.runStore.setNeedsReview(durableTaskId, exitResult);
1007
1013
  return { status: 200, body: withSyncSubmitKeys(buildSyncSubmitBody(exitResult, { taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "needs_review" })) };
1008
1014
  }
@@ -9,6 +9,7 @@ import { type SessionMirrorRuling } from "../web-admin-client/facade.js";
9
9
  import type { RestartSignal } from "@sema-agent/settings-schema/fleet";
10
10
  import { type OwnerAwareSessionStore } from "../security.js";
11
11
  import type { PeerDesk, PeerLease } from "../peer-seat.js";
12
+ import type { ExecutionRoot, ResolvedTaskSpec } from "./execution-context.js";
12
13
  import type { RunStore, ImageIndex, ImageBake, CheckpointStoreFull, ResumeAnchorStore, ApprovalExemptionStore, ApprovalNonceStore, ServiceSessionPolicyStore, ServiceFileHistoryStore, ToolResultStoreFull, StoreBackend } from "../plugins/store-backend.js";
13
14
  import type { RuleConsentLane } from "../rules-consent.js";
14
15
  import { type MemorySyncRequest, type MemorySyncResponse } from "../memory-sync.js";
@@ -52,6 +53,8 @@ export interface RequestAuth {
52
53
  * defaultScopes(memory.scopes 种子)。resume 腿(checkpoint 重建 auth)缺席 — 回落持久化的
53
54
  * body.projectId(原始提交已验形),那条回落腿自己再折一次小写,两腿查同一个键。 */
54
55
  resolvedProjectId?: string;
56
+ /** authorizer 判过的头(`security.ts` `createAuthorizer` 交出;语义见 `AuthContext.head`)⇒ fresh 执行上下文的 `judged`。自建 authorizer 不给 = 解析器不比住客。 */
57
+ head?: import("@sema-agent/core").SessionHead;
55
58
  }
56
59
  /**
57
60
  * [ref] A8 —— ServiceDeps 的**分组窄接口**。
@@ -60,7 +63,7 @@ export interface RequestAuth {
60
63
  * 平铺形的直接后果 = main.ts 一个上千行的装配字面量,键与键之间没有任何「属于同一件事」的编译期表达,
61
64
  * 键落错家/从未接线只能靠源码钉逐个盯(1.300/1.301/1.304/1.307/1.310 五起真缺陷都是这个病族)。
62
65
  *
63
- * 药:七个**窄**接口 + 顶层必填七件。装配点按组构造(每组一个带类型标注的中间 const ⇒ 受 TS 多余
66
+ * 药:七个**窄**接口 + 顶层必填八件。装配点按组构造(每组一个带类型标注的中间 const ⇒ 受 TS 多余
64
67
  * 属性检查保护),键放错组当场编译红。
65
68
  *
66
69
  * 兼容形(本批**不动消费点**):`ServiceDeps` = 平铺视图 ∩ 组槽。旧的平铺构造(96 个测试文件 +
@@ -68,8 +71,8 @@ export interface RequestAuth {
68
71
  * 入口摊平回平铺视图,server.ts 内的 ~700 处 `deps.x` 消费点原样保留。消费点迁移是后批的事——
69
72
  * 一步到位需要同批改 ~700 处 src + ~800 处 test 键位,与「先立形、后搬家」相比没有额外收益。
70
73
  */
71
- /** createHttpServer 的**必填七件**——引擎、部署配置、请求→TaskSpec 的映射、选路席、side-query 执行席、读口场景门、对端座的 desk。
72
- * 七者恒在场,故留在顶层(不进任何可选组):没有它们 server 根本起不来,包一层可选组只会把编译期
74
+ /** createHttpServer 的**必填八件**——引擎、部署配置、提交的身份与根会话(authorizer)、请求→TaskSpec 的映射、选路席、side-query 执行席、读口场景门、对端座的 desk。
75
+ * 八者恒在场,故留在顶层(不进任何可选组):没有它们 server 根本起不来,包一层可选组只会把编译期
73
76
  * 错误推迟到运行期(本仓 E-HIGH-1 病族的成因就是「可选键从未接线、编译期无声」)。 */
74
77
  export interface ServiceCoreDeps {
75
78
  runner: Runner;
@@ -83,9 +86,22 @@ export interface ServiceCoreDeps {
83
86
  sideQuery: (spec: SideQuerySpec) => Promise<SideQueryResult>;
84
87
  config: ServiceConfig;
85
88
  /**
86
- * Map an inbound request body + resolved auth to a full TaskSpec. The SERVICE/deployment owns model
87
- * + tools + mcp here (callers only supply content). `auth` carries the ownership-checked sessionId
88
- * and the principal-derived memory scope — prefer them over anything in the body.
89
+ * Resolve identity + the owned root session BEFORE the spec is built (S6). Throw `HttpError` to reject.
90
+ * 🔴 **必填**:它是一次提交的根会话**唯一**的来源与铸点(体里的 `sessionId` 只由它消费:有属主读的店判属主,单用户嵌入原样采用,
91
+ * 不带 ⇒ 现铸并登记)。省掉是编译错;绕过类型造的 deps 在 {@link createHttpServer} 入口拒启。单用户嵌入传
92
+ * `createAuthorizer(config, runner.sessions)`(包根导出)—— 店没有属主读时它退化为只派 principal 与会话 id。
93
+ */
94
+ authorize: (ctx: {
95
+ req: IncomingMessage;
96
+ body: TaskRequestBody;
97
+ }) => Promise<RequestAuth>;
98
+ /**
99
+ * Map an inbound request body + resolved auth to a TaskSpec. The SERVICE/deployment owns model
100
+ * + tools + mcp here (callers only supply content). `auth` carries the principal-derived memory scope —
101
+ * prefer it over anything in the body.
102
+ * The root session is NOT the resolver's to write: the return type carries no `sessionId`
103
+ * (`ResolvedTaskSpec`); the server stamps the root the authorizer resolved onto every spec it executes
104
+ * (`http/execution-context.ts` `stampSpecRoot`). A resolver that needs the root reads `opts.execution.root`.
89
105
  */
90
106
  /** `opts.leg` (codex R4, appendSystemPrompt 案): "fresh" ⇒ a NEW submit — resolveSpec may fail-loud 400 on
91
107
  * combos that would silently discard caller payload (append rider / outputStyle on an append-less pack).
@@ -108,6 +124,8 @@ export interface ServiceCoreDeps {
108
124
  * `opts.unattendedLeg`([ref] 候裁 18,codex r3 F1;[ref] ① 改形):续跑家族在 ctx 带无人值守标(`CheckpointCtx.unattendedLeg`)时递 `true` ——
109
125
  * 装配器在**全部折叠之后**经首跑同一只合并口 `withUnattendedLeg` 重挂那条腿(`default` + 按类作答的 onAsk)。只由续跑腿
110
126
  * 按服务端写的 ctx 位递,绝不从请求体读。 */
127
+ /** `opts.execution`(必填):这条腿的执行上下文(`http/execution-context.ts`;只 `fresh` / `continuation` 两形,`root` 恒在)。装配器要用这条腿的根会话
128
+ * (场景设施的分区键、受理见证)读 `opts.execution.root` —— 它与 server 随后盖进 spec 的根是同一个值,装配器自己不写根。 */
111
129
  /** `opts.lease`(必填):这条腿的**宿主租约** —— 装配在它之下登记本腿交给引擎的对端声明(`peerDesk.declare(草稿, opts.lease)`)。
112
130
  * 租约由两只漏斗(受理 `prepareSpec` / 续跑 `resolveResumeSpecOrBlocked`)在调用本函数**之前**开、由漏斗的失败出口或拿到产物的那段作用域放;
113
131
  * 装配只借用,不放、不留。实现方自铸的声明归自己的 desk 时可以不理它。 */
@@ -117,7 +135,8 @@ export interface ServiceCoreDeps {
117
135
  onNotice?: (notice: EngineNotice) => void;
118
136
  unattendedLeg?: true;
119
137
  lease: PeerLease;
120
- }) => TaskSpec | Promise<TaskSpec>;
138
+ execution: ExecutionRoot;
139
+ }) => ResolvedTaskSpec | Promise<ResolvedTaskSpec>;
121
140
  /**
122
141
  * [ref] 场景 hands lane 选路:按 `resolveSpec` 刚产出的 TaskSpec 取本任务的执行 Runner。
123
142
  * 装配点(`boot/stage-10-http-server.ts`)传的是 `createHandsRunnerSelector` —— 选路规则只有那一份实现,
@@ -146,7 +165,7 @@ export interface ServiceCoreDeps {
146
165
  * 本层用它开**宿主租约**:受理与续跑两只漏斗在装配之前各开一只(`lease("fresh")` / `lease("resume")`),经 `resolveSpec` 的 `opts.lease` 交给装配,
147
166
  * 随产物(`PreparedTaskSubmission.peerLease` / 续跑结果)交给拿到它的那段作用域,在那段作用域的 `finally` 放;活过那段作用域的驱动经 `heldFor` 自持一只。
148
167
  * 这只 desk 另有三处用法,不止这两只漏斗:① 探测口 `POST /v1/capabilities/mcp/probe` 的调用方臂在走查里开一只 `probe` 租约(三道前置拒之后开,拨号落地才放);
149
- * ② 两张会话面板(`GET /v1/sessions/:id/mcp` · `…/a2a`)与探测走查拨名册声明时递它的座(`desk.seat.answer`),不开租约;③ 名册声明在起服时登记在一只进程寿命的 `roster` 租约下
168
+ * ② 两张会话面板(`GET /v1/sessions/:id/mcp` · `…/a2a`)与探测走查拨名册声明时递它的座(`desk.seat`),不开租约;③ 名册声明在起服时登记在一只进程寿命的 `roster` 租约下
150
169
  * (stage-07 / run-local 开,从不放),并且每只 Runner 的 `peerSeat` 是它的座。登记表的两个读数是 `/metrics` 的 `peer_registry_entries` / `peer_registry_holders`。
151
170
  * 🔴 **必填**:没有 MCP / A2A 凭据的嵌入也给一只 `createPeerDesk()`(零 IO)—— 一个部署只有一张登记表,缺席不是「没有座」的另一种读法。
152
171
  */
@@ -371,7 +390,7 @@ export interface ServiceSeamDeps {
371
390
  deviceStore?: import("../device-store.js").DeviceStore;
372
391
  /**
373
392
  * [ref](车C 余件):device lane 的**准入链**(`createDeviceEnrollment`)—— 管理面三个写动词
374
- * (签发 / 注册 / 吊销)与首绑写协议(`commitPlacement`)的**唯一**判据属主。
393
+ * (签发 / 注册 / 吊销)与首绑写协议(`commitExecutionContext` 的设备那一支)的**唯一**判据属主。
375
394
  *
376
395
  * 🔴 为什么路由拿的是它而不是 {@link deviceStore}:enrollment 那一层持着两件路由造不出来的东西 ——
377
396
  * ①跨副本限速座(`DeviceEnrollRateLimiter`,§7-T5 ⑤:端点车**必须**传一个,而它由 boot 从既有
@@ -598,6 +617,9 @@ export interface ServiceSeamDeps {
598
617
  * per-principal 查询走 caps client 的 executionRuling 同车(零额外 RTT);absent = 无 center(单用户/
599
618
  * dry-run)⇒ 观测面不接线,零行为差。fail-open:观测失败绝不影响 sync 面本身。 */
600
619
  sessionMirrorRuling?: (principal: string | undefined) => Promise<SessionMirrorRuling | undefined>;
620
+ /** 主体的执行车道裁定(caps client 的 `executionRuling`,与场景裁定同车零额外 RTT)—— 执行上下文解析器步 ③ 的唯一输入(`http/execution-context.ts`,
621
+ * 判据 `runtime-caps-resolver.ts` `gateExecutionLane`)。缺席 = 无中心(单用户 / dry-run / 嵌入方)⇒ 不判,与已发同。 */
622
+ executionRuling?: (principal: string | undefined) => Promise<import("../web-admin-client/facade.js").ExecutionRuling | undefined>;
601
623
  /** Degenerate-output a/b instrument: on errorCode=output.degenerate, inspect the session's turns to
602
624
  * classify where the salvageable answer was (counter + log). Fire-and-forget; no-op otherwise. */
603
625
  instrumentDegenerate?: (result: TaskResult) => void;
@@ -606,7 +628,9 @@ export interface ServiceSeamDeps {
606
628
  * (手搭测试 deps;生产 main.ts 恒传)。 */
607
629
  sideQueryAccounting?: SideQueryAccounting;
608
630
  /** [ref](core 7.32.0):parked 赎回面工厂(按行解析驱动哪只 Agent 工具 + 同出一张列表的继承座;属主 `parked-revive-surface.ts`)。
609
- * 缺席 = 这台部署没有 park 底座(checkpointStore / backgroundAgentStore 任一缺席),bg 赎回臂不接线。 */
631
+ * 缺席 = 这台部署没有 park 底座(checkpointStore / backgroundAgentStore 任一缺席),bg 赎回臂不接线。
632
+ * 🔴 **在场 ⇒ `sessionStorage` 必须带执行席 `executionPlacement`**:认领之后对根会话住客的唯一判官是执行席上的写口;缺 ⇒ `createHttpServer` 拒启
633
+ * (句子 = `PARKED_REVIVE_EXECUTION_SEAT_REQUIRED`)。 */
610
634
  parkedReviveSurface?: import("../parked-revive-surface.js").ParkedReviveSurfaceFactory;
611
635
  /** 可选第三件:revive args 的 `subagent_type` roster 成员集([ref] Q1 成员性规则)。当前生产装配
612
636
  * **恒缺席**(boot 内建 default 无自定义 agents roster,per-request 的 spec.agents 不进 boot 工具)
@@ -641,14 +665,6 @@ export interface ServiceObservabilityDeps {
641
665
  /** **准入/治理面**:身份解析与配额/限流/租约。每一件都是**拒绝**的权力(400/403/429),
642
666
  * 缺席 = 该道门不存在(不是「默认放行的门」——两者在事故复盘里语义完全不同)。 */
643
667
  export interface ServiceGovernanceDeps {
644
- /**
645
- * Resolve identity + the owned session BEFORE the spec is built (S6). Throw `HttpError` to reject.
646
- * Optional: when omitted, no principal/ownership enforcement (the body's sessionId is used as-is).
647
- */
648
- authorize?: (ctx: {
649
- req: IncomingMessage;
650
- body: TaskRequestBody;
651
- }) => Promise<RequestAuth>;
652
668
  /** 鉴权桥(registry-core api/auth-bridge):registry 用户 JWT → principal。缺席=分支不存在
653
669
  * (bearer 只认 SERVICE_AUTH_TOKEN(S),现状不变)。 */
654
670
  webAdminJwtVerifier?: {
@@ -867,7 +883,7 @@ export interface ServiceDepGroups {
867
883
  deployment?: ServiceDeploymentDeps;
868
884
  knobs?: ServiceKnobDeps;
869
885
  }
870
- /** 平铺视图 = 必填七件 + 其余七组的全可选并集。**派生**自组接口,故与分组形永不漂。 */
886
+ /** 平铺视图 = 必填八件 + 其余七组的全可选并集。**派生**自组接口,故与分组形永不漂。 */
871
887
  export type FlatServiceDeps = ServiceCoreDeps & Partial<ServiceStoreDeps & ServiceCoordinatorDeps & ServiceSeamDeps & ServiceObservabilityDeps & ServiceGovernanceDeps & ServiceDeploymentDeps & ServiceKnobDeps>;
872
888
  /** createHttpServer 的入参:两形皆可(平铺=存量,分组=推荐)。同名键两形都给时,**顶层显式键赢**
873
889
  * (含显式 `undefined`)——一处装配点只该用一形,混用时以肉眼最近的那个为准。 */
@@ -4,6 +4,7 @@ import { validatePendingSteer } from "@sema-agent/core";
4
4
  import {} from "../web-admin-client/facade.js";
5
5
  import { describeValueShape } from "@sema-agent/settings-schema";
6
6
  import { HttpError } from "../security.js";
7
+ import { SessionForkInProgressError } from "../execution-placement.js";
7
8
  import {} from "../memory-sync.js";
8
9
  import {} from "../orchestration/workflow-agent-steer.js";
9
10
  import {} from "../orchestration/workflow-completion-inbox.js";
@@ -22,6 +23,7 @@ import { createRequestGuards } from "./request-guards.js";
22
23
  import { sqlEngineSeatIssue } from "../sql-engine-posture.js";
23
24
  import { parkDecisionStoreRefusal } from "../orchestration/workflow-park-decision.js";
24
25
  import { CHECKPOINT_CTX_SEALING_REQUIRED } from "./checkpoint-ctx.js";
26
+ import { PARKED_REVIVE_EXECUTION_SEAT_REQUIRED } from "../parked-decide.js";
25
27
  import { retiredServiceDepsRefusal } from "./retired-service-deps.js";
26
28
  import { createAdmission, MAX_SKILL_CONTENT_CHARS, MAX_SYSTEM_PROMPT_CHARS, MAX_OUTPUT_SCHEMA_CHARS, validateUserSkills } from "./admission.js";
27
29
  import { createResumeLegs } from "./resume-legs.js";
@@ -61,6 +63,9 @@ export function createHttpServer(rawDeps) {
61
63
  if (typeof deps.runnerFor !== "function") {
62
64
  throw new Error("createHttpServer: the runnerFor seat is required (it picks each task's Runner from its execution lane) — pass the hands-lane selector, or `runnerFor: () => runner` for a single-Runner embedding; refusing to start rather than run every task on the widest Runner");
63
65
  }
66
+ if (typeof deps.authorize !== "function") {
67
+ throw new Error("createHttpServer: the authorize seat is required — it is the only resolver of a submission's root session; pass `createAuthorizer(config, runner.sessions)` for a single-user embedding; refusing to start rather than run submissions on a root nobody attested");
68
+ }
64
69
  {
65
70
  const retired = retiredServiceDepsRefusal(deps);
66
71
  if (retired !== undefined)
@@ -88,6 +93,8 @@ export function createHttpServer(rawDeps) {
88
93
  }
89
94
  if (deps.checkpointStore !== undefined && deps.checkpointCtxSealing === undefined)
90
95
  throw new Error(`createHttpServer: ${CHECKPOINT_CTX_SEALING_REQUIRED}`);
96
+ if (deps.parkedReviveSurface !== undefined && deps.sessionStorage?.executionPlacement === undefined)
97
+ throw new Error(`createHttpServer: ${PARKED_REVIVE_EXECUTION_SEAT_REQUIRED}`);
91
98
  const idemCache = new IdempotencyCache();
92
99
  const inflightRuns = new Map();
93
100
  const preemptableRuns = new DrainYieldingPreemptBook(() => deps.drainState?.draining === true);
@@ -164,6 +171,10 @@ export function createHttpServer(rawDeps) {
164
171
  }
165
172
  if (sendSessionError(res, err, "open"))
166
173
  return;
174
+ if (err instanceof SessionForkInProgressError) {
175
+ sendError(res, 409, err.code, err.message);
176
+ return;
177
+ }
167
178
  sendError(res, 500, "internal.error", "internal error");
168
179
  });
169
180
  });
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * [ref] × 7.106.0 合并复审 X1(gpt-6 腿 A F1 [high],验证腿 B verified)—— 请求键 `inheritEnv` 的**会话绑定唯一提交点**。
3
3
  *
4
- * ── 分界(与 `device-placement.ts` 的首绑写协议同一句判据)──────────────────────────────────────────────────
4
+ * ── 分界(与 `execution-context.ts` 认领之后的执行放置提交同一句判据)──────────────────────────────────────────────────
5
5
  * 凡需要「这条会话此刻归我这条 run」才成立的事,排在 run 认领之后;其余全排在建任何东西之前。`inheritEnv` 的三道判
6
6
  * (值形 / 车道 / 射程)是**读**,住在 `resolveSpec`(两腿同判,任何副作用之前);而「本会话的 host 工厂下一次造环境按哪个词」
7
7
  * 是**写**,它只属于一条**被受理**的 run —— 修前它写在 `resolveSpec` 的最后一步,仍先于路由层的拒(`/v1/tasks/stream` 的
@@ -28,10 +28,9 @@ export interface SessionInheritEnvCommitCtx {
28
28
  bindSessionInheritEnv?: ((sessionId: string, policy: TaskInheritEnv | undefined) => void) | undefined;
29
29
  }
30
30
  /**
31
- * 在 run 认领之后调用:把受理的那条请求的 `inheritEnv`(缺席 = 清)写进本会话的绑定。`sessionId` 缺席(不接 authorizer 的嵌入形)⇒
32
- * 不写 —— `"all"` 在那一形已被 `resolveSpec` 响亮拒,`"scrub"` / 缺席与工厂缺省同形。
31
+ * 在 run 认领之后调用:把受理的那条请求的 `inheritEnv`(缺席 = 清)写进本会话(这条 run 的根会话)的绑定。
33
32
  */
34
- export declare function commitSessionInheritEnv(ctx: SessionInheritEnvCommitCtx, sessionId: string | undefined, body: {
33
+ export declare function commitSessionInheritEnv(ctx: SessionInheritEnvCommitCtx, sessionId: string, body: {
35
34
  readonly inheritEnv?: TaskInheritEnv | undefined;
36
35
  }): void;
37
36
  //# sourceMappingURL=session-inherit-env.d.ts.map
@@ -1,5 +1,5 @@
1
1
  export function commitSessionInheritEnv(ctx, sessionId, body) {
2
- if (sessionId === undefined || ctx.bindSessionInheritEnv === undefined)
2
+ if (ctx.bindSessionInheritEnv === undefined)
3
3
  return;
4
4
  ctx.bindSessionInheritEnv(sessionId, body.inheritEnv);
5
5
  }
@@ -195,7 +195,7 @@ export interface TaskRequestBody {
195
195
  * after it"). core enforces the "before" edges and the service passes its rejection codes through UNCHANGED for the
196
196
  * shell to render: `resume_at.before_target_not_user` (only plain user-message targets),
197
197
  * `resume_at.before_root_unsupported` (can't rewind past the session root — start a new session),
198
- * `rewind_snapshot.unresolvable` (rewindFiles: no snapshot at/above the "before" branch point). Only meaningful
198
+ * `rewind_snapshot.unresolvable` (restoreFiles: no snapshot at/above the "before" branch point). Only meaningful
199
199
  * WITH `resumeAt` — sent alone it's a 400 (fail-loud, not a silent no-op). */
200
200
  resumeAtMode?: string;
201
201
  /** [ref] Phase3 (reuse-path warm-resume): require the `sessionId` to ALREADY exist. When `true` and the session
@@ -221,11 +221,6 @@ export interface TaskRequestBody {
221
221
  /** DV-15: opt-in tolerance for a PARTIAL file restore (default = loud terminal failure carrying the per-file
222
222
  * ledger; `true` = keep going and disclose the same ledger on the result's rewindNotes). */
223
223
  acceptPartialRestore?: boolean;
224
- /** RETIRED spelling (whole-tree epoch). Still accepted on the wire: its capture sense (true, no resumeAt) is
225
- * a tolerated no-op with a deprecation disclosure; its restore sense (true + resumeAt) is a typed rejection
226
- * naming `restoreFiles` — the restore semantics genuinely changed (whole tree ⇒ tracked set), so that
227
- * migration is explicit by design (core adjudicates; the server never remaps it silently). */
228
- rewindFiles?: boolean;
229
224
  /** R8 code-only rewind (CC "Restore code" mode): converge the TRACKED set to the history boundary at a prior
230
225
  * USER message WITHOUT forking the conversation (the session leaf is untouched — only the files move). The
231
226
  * value is the same handle as `resumeAt` (the prompt's taskId); the service resolves it → the entry id. Honored ONLY
@@ -235,7 +230,7 @@ export interface TaskRequestBody {
235
230
  * 与 `checkpointStore` 同;没有 `"enabled"` —— 缺席就是「部署说了算」的唯一形,声明只能单向收紧)。`"disabled"` ⇒
236
231
  * 这条 run 零历史店调用:编辑照常落盘,但不做首次触碰备份、不铸回合边界、不做 fork 携带;委派出去的子代继承
237
232
  * (core `ParentSpecSeat` 从本位铸,本仓零接线)。`wiring_manifest.fileHistory` 报 `"disabled"`。
238
- * 同一个提交里再要还原(`restoreFiles` / `rewindFilesTo` / 退役 `rewindFiles`)= 体内矛盾 ⇒ **受理时** 400
233
+ * 同一个提交里再要还原(`restoreFiles` / `rewindFilesTo`)= 体内矛盾 ⇒ **受理时** 400
239
234
  * `request.field_conflict`(`request-value-closure.ts` 的 `FILE_HISTORY_DISABLED_NO_RESTORE`):立门时 core 的 `rewind.capture_disabled`
240
235
  * 落在会话按 `resumeAt` 分叉之后、拒后不回滚,所以本层在副作用之前拒([ref] / core 7.35.0 [ref] 起 core 在分叉**之前**拒,
241
236
  * 本层 400 照旧有效)。续跑腿不过这道门。
@@ -472,7 +467,7 @@ export interface TaskRequestBody {
472
467
  * · 仅 `REMOTE_EXEC=device` 的部署接受;其它车道收到它 = **400 响亮拒**(不静默忽略 —— 调用方会
473
468
  * 以为自己指定了执行设备,而那条声明从未存在);
474
469
  * · 仅**根任务**提交携带(子代无条件继承根绑定,core 为 subagent/cascade/verify 另铸 sessionId);
475
- * · 首绑 = INSERT-if-absent CAS(并发单赢家);后续根提交:缺席 = 沿用绑定 / 携且同 = 幂等 /
470
+ * · 首绑 = 根会话行执行格的 absent → writer(device, D)(按受理那一读的住客定址,并发单赢家);后续根提交:缺席 = 沿用绑定 / 携且同 = 幂等 /
476
471
  * 携且异 = 403 `device.identity_mismatch`(**v1 无隐式重绑**,换设备走
477
472
  * `POST /v1/devices/sessions/:rootSessionId/rebind`);
478
473
  * · resume 家族(`/decide` / `/answer` / `/plan_review` / `/wake`)**不收**这个键:车道随树固定。
@@ -491,10 +486,11 @@ export interface TaskRequestBody {
491
486
  /** [ref]⑧/[ref]/[ref] (core 5.23.0 `TaskSpec.oneShot`): this SUBMISSION is one-shot — no later turn exists
492
487
  * in which an async background notification could land (the archetypal case is a headless `sema -p` whose
493
488
  * process exits when the turn ends). PER-REQUEST on purpose: "does this submission expect to be continued" is
494
- * a property of the submission, not of the connection it arrived on. core consumes it as GUIDANCE ONLY (the
495
- * `RunWorkflow` / delegation receipts tell the model to block-wait via `TaskOutput({block:true})` instead of
496
- * "end your turn, you will be notified" — the latter is actively wrong here and loses background results); it
497
- * grants nothing, so there is no tenancy gate. Sibling of `interactiveTools` (the same `-p` posture) and passed
489
+ * a property of the submission, not of the connection it arrived on. Since core 7.60.0 it changes when the
490
+ * submission ends: where a one-shot run would end, the engine waits for the background agents and workflow runs
491
+ * it started (a background command or Monitor watch is never waited for), bounded by the run's walltime, a cancel
492
+ * and a 600 s ceiling; a sync submission therefore returns after that background work. It grants nothing, so
493
+ * there is no tenancy gate. Sibling of `interactiveTools` (the same `-p` posture) and passed
498
494
  * through the same way: a boolean rides, absent/garbage ⇒ key omitted (core's default = interactive). Rides the
499
495
  * persisted body onto resume legs. */
500
496
  oneShot?: boolean;
package/dist/index.d.ts CHANGED
@@ -26,6 +26,7 @@ export type { TaskRequestBody } from "./http/wire-types.js";
26
26
  export { ensureCheckpointCtxSealing } from "./sealed-key.js";
27
27
  export type { CheckpointCtxSealing } from "./http/checkpoint-ctx.js";
28
28
  export { createHttpServer, type ServiceDeps, type RequestAuth, type FlatServiceDeps, type ServiceDepGroups, type ServiceCoreDeps, type ServiceStoreDeps, type ServiceCoordinatorDeps, type ServiceSeamDeps, type ServiceObservabilityDeps, type ServiceGovernanceDeps, type ServiceDeploymentDeps, type ServiceKnobDeps, } from "./http/server.js";
29
+ export type { ExecutionContext, ResolvedTaskSpec, RootedTaskSpec } from "./http/execution-context.js";
29
30
  export { createPeerDesk } from "./peer-seat.js";
30
31
  export type { PeerDesk, PeerLease, McpDraft, A2aDraft } from "./peer-seat.js";
31
32
  export { createSideQueryLane, type KeyResolver, type SideQueryLane, type SideQueryLaneCtx } from "./boot/side-query-lane.js";
@@ -1,29 +1,68 @@
1
1
  import { type MemoryBackend, type MemorySyncClientResult, type MemorySyncTransport } from "@sema-agent/core";
2
+ import { type OneTimeMigrationWindow } from "./plugins/one-time-migrations.js";
2
3
  /** 结构化日志面(src/observability/logger.ts Logger 的结构子集——测试可注入捕获器)。 */
3
4
  export interface MemorySyncLog {
4
5
  info(msg: string, fields?: Record<string, unknown>): void;
5
6
  warn(msg: string, fields?: Record<string, unknown>): void;
6
7
  }
7
- /** 缺省对端名(core `syncMemoryScope` 同款缺省;cursor 按 (scope,peer) 分区)。 */
8
- export declare const DEFAULT_MEMORY_SYNC_PEER = "central";
9
8
  /**
10
- * fetch transport(core `MemorySyncTransport` 的部署实现)。`url` = 中心 server 基址(尾斜杠剥除,
11
- * core 传来的 path 以 `/` 开头);`token` 走 Authorization: Bearer(与中心侧 gatedPrincipal 的
12
- * verified-principal 门对齐)。非 2xx throw(status + body 前 200 字节——中心 4xx/5xx 的 typed error
13
- * body 就是最好的诊断线索);2xx 返回解码后的 JSON(形状校验在 core parseMemorySyncResponse,职责
14
- * 不重复)。`timeoutMs`(缺省 60s,AbortSignal.timeout)是活性保护:fire-and-forget 轮挂死会把
15
- * inflight 节流永久卡住(undici 默认 headers 超时 300s 太钝)。
9
+ * 中心基址 → 同步对端的身份(规范化基址)。本仓唯一的铸点:`config.ts` 读 env 时调它拒启(引擎关着也拒),transport 建时再调它铸身份 —— 同一只纯函数,同一个答案。
10
+ * 两道判,各归各的属主,本函数不另写规则:
11
+ * ① 地址的形 —— 这只地址是「后面要拼路径的基址」座位(post 把同步路径拼在它后面),判官与拒句就是那一座位的那一只({@link pathBaseUrlRefusal}:层级式 http(s),
12
+ * 不带 userinfo / query / fragment;拒句只点名规则、值只到 origin);
13
+ * ② 身份里有没有凭据 —— 判官是引擎包根的 `carriesCredential`(引擎同步客户端判 `transport.peer` 的同一只:任何 userinfo,或同主体出口脱敏会遮掉的任何一段,
14
+ * 例如路径段里的令牌形),判的就是规范化之后的身份 —— 引擎第一轮判的那个值。身份随每个请求体与落盘游标走,所以带凭据的地址拒而不清洗
15
+ * (清洗过的身份是另一个身份;令牌只走 `MEMORY_SYNC_TOKEN`);拒句不引地址的任何部分。
16
+ * 规范化 = WHATWG URL 的 `origin`(协议与主机小写、缺省端口省掉)+ 路径去尾斜杠;规范化之后装不进中心侧 `peer` 列({@link MEMORY_SYNC_PEER_COLUMN_CHARS},
17
+ * 每一轮都会被中心拒)⇒ 同样拒。
18
+ */
19
+ export declare function memorySyncPeerOf(url: string): string;
20
+ /**
21
+ * 起服日志行 `memory_sync_enabled` 的字段(两处起服 —— `boot/stores.ts` 与 `run-local.ts` —— 同读这一只)。`peer` 是运维改绑游标时要抄的那个值:
22
+ * 它已过 {@link memorySyncPeerOf} 的两道判(带凭据的身份起不来);仍过展示铸点({@link buildDisplaySafeUrl}),日志里的 URL 值一律经它 —— 两只判官
23
+ * (引擎的出口脱敏与本仓的展示形状律)不是同一份实现,本仓的那一只若遮了引擎放行的一段,这一行带遮记号而不是逐字(响亮,不漏)。
24
+ */
25
+ export declare function buildMemorySyncEnabledFields(peer: string, scope: string, cursorPath: string): {
26
+ readonly peer: string;
27
+ readonly scope: string;
28
+ readonly cursorPath: string;
29
+ };
30
+ /**
31
+ * fetch transport(core `MemorySyncTransport` 的部署实现)。`peer` = {@link memorySyncPeerOf}(`url`)(建时铸一次;拒的地址在这里抛,一个请求都不发),
32
+ * `post` 打 `${peer}${path}`(core 传来的 path 以 `/` 开头);`token` 走 Authorization: Bearer(与中心侧 gatedPrincipal 的 verified-principal 门对齐)。
33
+ * 非 2xx throw(status + body 前 200 字节——中心 4xx/5xx 的 typed error body 就是最好的诊断线索);2xx 返回解码后的 JSON(形状校验在 core
34
+ * parseMemorySyncResponse,职责不重复)。`timeoutMs`(缺省 60s,AbortSignal.timeout)是活性保护:fire-and-forget 轮挂死会把 inflight 节流永久卡住
35
+ * (undici 默认 headers 超时 300s 太钝)。
16
36
  */
17
37
  export declare function createMemorySyncTransport(opts: {
18
38
  url: string;
19
39
  token: string;
20
40
  timeoutMs?: number;
21
- fetchImpl?: typeof fetch;
41
+ /** The one call shape this transport makes (`fetch` itself fits it). */
42
+ fetchImpl?: (url: string, init: RequestInit) => Promise<Response>;
22
43
  }): MemorySyncTransport;
23
- /** cursor 文件路径:`memoryRoot`(= RunnerDeps.memoryEngineDir 的 resolved config root,memory/ 目录旁)下的
24
- * per-(peer, scope) sidecar(`sync-cursors/<peer>/<scope 摘要>.json`)。peer 段走 core encodeScopeSegment(文件名安全字符纪律与 scope 目录同);scope 段取 sha256 前 32 hex
25
- * (scope 键带 `:` `/` 且可长过段编码的 64 字符帽 —— 名字只需区分,内容里仍记原 scope,读时比对)。 */
26
- export declare function memorySyncCursorPath(memoryRoot: string, peer: string, scope: string): string;
44
+ /** 一层的游标文件路径:`memoryRoot`(= RunnerDeps.memoryEngineDir 的 resolved config root)下 `sync-cursors/<scope 摘要>.json`。一层一只,不随对端变。
45
+ * 落在子目录里:与更早的根下一层文件名 `sync-cursor-<peer>.json` 的空间不相交。 */
46
+ export declare function memorySyncCursorPath(memoryRoot: string, scope: string): string;
47
+ /** M31 一次的读数(测试按它断言)。`window: "closed"` ⇒ 一行都没读。 */
48
+ export interface SyncCursorRelocationM31Report {
49
+ readonly window: "open" | "closed";
50
+ readonly moved: number;
51
+ readonly left: number;
52
+ }
53
+ /**
54
+ * ⑤ 一次性迁移 M31:把升级前的游标搬到各自那一层的路径(`rename`,字节不变;只搬不改,判归引擎)。`today` = UTC 日历日;窗关 ⇒ 不搬。
55
+ * · 上一版的分区目录 `sync-cursors/central/<scope 摘要>.json` ⇒ `sync-cursors/<同名>`(两种布局的文件名是同一只 scope 摘要);搬空的目录删掉;
56
+ * 名字不是摘要形的(写游标的 staging 残片)原地不动。
57
+ * · 更早的 `sync-cursor-central.json` ⇒ 它记的那一层的路径;读不成对象 / 没有 scope ⇒ 原地不动、一行 warn(没法说它是哪一层的基线)。
58
+ * · 目标路径上已有游标 ⇒ 不搬、不删,一行 warn(这一层用路径上的那一份;留下的那只没有读者)。
59
+ * 与同一数据根上并跑的另一个进程(server + run-local)同搬:源已被对方搬走(`ENOENT`)⇒ 跳过。残余(如实):先查目标再 `rename` 之间若有写者在目标路径上
60
+ * 落下游标,会被盖掉 —— 写者只有本模块的 runner,而 runner 写任何游标之前先跑完本步。别的读写错 ⇒ 抛(这一轮整轮失败、下一轮再搬)。
61
+ */
62
+ export declare function relocateSyncCursorsM31(memoryRoot: string, log: MemorySyncLog, opts: {
63
+ readonly today: string;
64
+ readonly window?: Pick<OneTimeMigrationWindow, "removableAfter">;
65
+ }): Promise<SyncCursorRelocationM31Report>;
27
66
  /** 一轮里一层的结果。 */
28
67
  export interface MemorySyncLayerResult {
29
68
  readonly scope: string;
@@ -59,9 +98,9 @@ export declare function createMemorySyncRunner(opts: {
59
98
  /** 同步层(`config.memorySync.scope`);它同属主的项目层随它一起同步。 */
60
99
  scope: string;
61
100
  memoryRoot: string;
101
+ /** 对端 = 它的 `peer`(这一轮的对端只有这一个来源;游标绑的对端由引擎对它判)。 */
62
102
  transport: MemorySyncTransport;
63
103
  log: MemorySyncLog;
64
- peer?: string;
65
104
  now?: () => number;
66
105
  /** S2.5 分批(core 1.284 两 seam):push 集/pull 集单轮上限。未设=不分批(wire 字节不变)。 */
67
106
  maxPushEntries?: number;