@sema-agent/server 7.125.1 → 7.127.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 (275) hide show
  1. package/MIGRATION.md +229 -2
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/USAGE.md +346 -145
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +57 -0
  7. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +32 -1
  8. package/deploy/sema-up/chart/templates/server-v4pro.yaml +3 -2
  9. package/deploy/sema-up/chart/templates/server.yaml +2 -1
  10. package/deploy/sema-up/chart/values.yaml +22 -0
  11. package/deploy/sema-up/cluster-up.sh +10 -10
  12. package/deploy/sema-up/kube-up.sh +20 -13
  13. package/deploy/sema-up/lib/dataroot.sh +1 -1
  14. package/deploy/sema-up/lib/envf.sh +5 -5
  15. package/deploy/sema-up/lib/flagshape.sh +5 -5
  16. package/deploy/sema-up/lib/lanes.py +1 -1
  17. package/deploy/sema-up/lib/node-addrs.sh +42 -0
  18. package/deploy/sema-up/lib/parse-machines.py +2 -2
  19. package/deploy/sema-up/lib/shell-access.sh +1 -1
  20. package/deploy/sema-up/preflight.sh +4 -4
  21. package/deploy/sema-up/sema-up.sh +33 -33
  22. package/deploy/sema-up/smoke.sh +3 -3
  23. package/deploy/sema-up/stack/compose.yaml +1 -1
  24. package/deploy/sema-up/stack/sema-stack-cluster.yaml +26 -3
  25. package/dist/approval-card.d.ts +20 -4
  26. package/dist/approval-content-kind.d.ts +38 -22
  27. package/dist/approval-content-kind.js +20 -4
  28. package/dist/audit.js +3 -0
  29. package/dist/bench/s1/arms.d.ts +9 -37
  30. package/dist/bench/s1/arms.js +4 -61
  31. package/dist/bench/s1/live-deps.d.ts +4 -10
  32. package/dist/bench/s1/live-deps.js +1 -222
  33. package/dist/bench/s1/oracle.d.ts +1 -1
  34. package/dist/bench/s1/reviewer.d.ts +1 -1
  35. package/dist/bench/s1/row.d.ts +5 -7
  36. package/dist/bench/s1/row.js +0 -2
  37. package/dist/bench/s1/run-firm.d.ts +2 -2
  38. package/dist/bench/s1/run-firm.js +1 -4
  39. package/dist/bench/s1/runner-ctx.d.ts +17 -29
  40. package/dist/bench/s1/runner-ctx.js +3 -7
  41. package/dist/bench/s1/tasks.d.ts +9 -46
  42. package/dist/bench/s1/tasks.js +4 -63
  43. package/dist/boot/execution-env.d.ts +38 -26
  44. package/dist/boot/execution-env.js +87 -41
  45. package/dist/boot/leader.d.ts +2 -1
  46. package/dist/boot/memory-boundary.d.ts +1 -1
  47. package/dist/boot/memory-consolidation.d.ts +37 -3
  48. package/dist/boot/memory-consolidation.js +18 -2
  49. package/dist/boot/parked-revive-gate.d.ts +0 -3
  50. package/dist/boot/parked-revive-gate.js +2 -2
  51. package/dist/boot/resolve-spec.d.ts +0 -2
  52. package/dist/boot/resolve-spec.js +12 -17
  53. package/dist/boot/retention-lane.d.ts +12 -1
  54. package/dist/boot/retention-lane.js +2 -0
  55. package/dist/boot/runner-deps.d.ts +7 -6
  56. package/dist/boot/runner-deps.js +5 -16
  57. package/dist/boot/session-faces.d.ts +7 -2
  58. package/dist/boot/session-faces.js +37 -1
  59. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  60. package/dist/boot/stage-03-store-backend.js +2 -2
  61. package/dist/boot/stage-04-stores.d.ts +1 -0
  62. package/dist/boot/stage-05-execution-env.d.ts +2 -2
  63. package/dist/boot/stage-05-execution-env.js +7 -4
  64. package/dist/boot/stage-06-runners.d.ts +2 -3
  65. package/dist/boot/stage-06-runners.js +13 -9
  66. package/dist/boot/stage-07-capability-layer.d.ts +2 -3
  67. package/dist/boot/stage-07-capability-layer.js +3 -4
  68. package/dist/boot/stage-08-reapers.d.ts +2 -3
  69. package/dist/boot/stage-08-reapers.js +2 -2
  70. package/dist/boot/stage-09-leader.d.ts +2 -3
  71. package/dist/boot/stage-09-leader.js +2 -1
  72. package/dist/boot/stage-10-http-server.d.ts +2 -3
  73. package/dist/boot/stage-10-http-server.js +2 -2
  74. package/dist/boot/stores.d.ts +1 -0
  75. package/dist/boot/stores.js +4 -3
  76. package/dist/boot/web-admin.d.ts +5 -0
  77. package/dist/boot/web-admin.js +28 -14
  78. package/dist/brain.d.ts +2 -2
  79. package/dist/brain.js +1 -1
  80. package/dist/capabilities/builtin-tools.d.ts +2 -1
  81. package/dist/capabilities/composite-members.d.ts +1 -1
  82. package/dist/capabilities/composite-members.js +1 -1
  83. package/dist/capabilities/hands-lane.d.ts +9 -6
  84. package/dist/capabilities/hands-lane.js +2 -2
  85. package/dist/capabilities/memory-notice.d.ts +27 -22
  86. package/dist/capabilities/memory-notice.js +15 -17
  87. package/dist/capabilities/scenarios.d.ts +9 -14
  88. package/dist/capabilities/scenarios.js +5 -50
  89. package/dist/capability-facts.d.ts +22 -11
  90. package/dist/capability-facts.js +13 -6
  91. package/dist/config-catalog.js +14 -5
  92. package/dist/config-provider.d.ts +5 -1
  93. package/dist/config-provider.js +23 -4
  94. package/dist/config-types.d.ts +19 -2
  95. package/dist/config.js +41 -6
  96. package/dist/deployment-governance.d.ts +19 -21
  97. package/dist/deployment-governance.js +7 -8
  98. package/dist/env-name-allowlist-knobs.d.ts +5 -5
  99. package/dist/env-name-allowlist-knobs.js +1 -0
  100. package/dist/execution-lane-caps.d.ts +12 -0
  101. package/dist/execution-lane-caps.js +18 -4
  102. package/dist/fleet/subagent-tail-bus.js +15 -5
  103. package/dist/hooks/hook-runner.d.ts +84 -30
  104. package/dist/hooks/hook-runner.js +74 -41
  105. package/dist/http/admission.js +17 -10
  106. package/dist/http/durable-run-leg.d.ts +5 -4
  107. package/dist/http/durable-run-leg.js +2 -4
  108. package/dist/http/idempotency.d.ts +28 -6
  109. package/dist/http/idempotency.js +10 -2
  110. package/dist/http/principal-gate.d.ts +14 -0
  111. package/dist/http/principal-gate.js +7 -0
  112. package/dist/http/request-guards.d.ts +2 -1
  113. package/dist/http/request-guards.js +14 -14
  114. package/dist/http/resume-legs.d.ts +2 -2
  115. package/dist/http/resume-legs.js +20 -10
  116. package/dist/http/route-ctx.d.ts +15 -7
  117. package/dist/http/routes/a2a-serve.js +11 -7
  118. package/dist/http/routes/approvals-assistant.js +18 -3
  119. package/dist/http/routes/background.d.ts +1 -1
  120. package/dist/http/routes/background.js +1 -1
  121. package/dist/http/routes/capabilities.js +10 -2
  122. package/dist/http/routes/mcp-reconnect.js +1 -1
  123. package/dist/http/routes/runs.d.ts +4 -3
  124. package/dist/http/routes/runs.js +35 -14
  125. package/dist/http/routes/session-sync.js +14 -11
  126. package/dist/http/routes/sessions.js +9 -9
  127. package/dist/http/routes/side-query.js +6 -19
  128. package/dist/http/routes/tasks.js +29 -19
  129. package/dist/http/routes/workflows.js +8 -0
  130. package/dist/http/server.d.ts +4 -0
  131. package/dist/http/server.js +3 -5
  132. package/dist/http/submit-replay.d.ts +15 -8
  133. package/dist/http/submit-replay.js +6 -0
  134. package/dist/http/wire-types.d.ts +7 -9
  135. package/dist/index.d.ts +1 -1
  136. package/dist/index.js +1 -1
  137. package/dist/internal-run-env.d.ts +30 -0
  138. package/dist/internal-run-env.js +11 -0
  139. package/dist/leader/wire.js +3 -2
  140. package/dist/lsp/manager.js +1 -1
  141. package/dist/memory-scope.d.ts +19 -7
  142. package/dist/memory-scope.js +25 -8
  143. package/dist/observability/fail-open.d.ts +31 -3
  144. package/dist/observability/fail-open.js +31 -3
  145. package/dist/observability/metrics.d.ts +0 -1
  146. package/dist/observability/metrics.js +1 -4
  147. package/dist/observability/run-terminal.js +72 -4
  148. package/dist/permission-rule-vocab.d.ts +3 -2
  149. package/dist/permission-rule-vocab.js +1 -1
  150. package/dist/plan-cache-probe.d.ts +3 -2
  151. package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
  152. package/dist/plugins/caching-session-store.d.ts +42 -22
  153. package/dist/plugins/caching-session-store.js +19 -9
  154. package/dist/plugins/checkpoint-store-sql.d.ts +42 -28
  155. package/dist/plugins/checkpoint-store-sql.js +57 -11
  156. package/dist/plugins/file-run-store.d.ts +25 -21
  157. package/dist/plugins/file-run-store.js +70 -34
  158. package/dist/plugins/k8s-exec-protocol.js +3 -0
  159. package/dist/plugins/local-checkpoint-store.d.ts +8 -5
  160. package/dist/plugins/local-checkpoint-store.js +5 -15
  161. package/dist/plugins/local-session-store.d.ts +66 -7
  162. package/dist/plugins/local-session-store.js +168 -101
  163. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +1 -1
  164. package/dist/plugins/memory-run-store.d.ts +19 -3
  165. package/dist/plugins/memory-run-store.js +38 -3
  166. package/dist/plugins/one-time-migrations.d.ts +165 -39
  167. package/dist/plugins/one-time-migrations.js +51 -144
  168. package/dist/plugins/pg-pool.js +11 -1
  169. package/dist/plugins/pg-session-storage.d.ts +2 -0
  170. package/dist/plugins/pg-session-storage.js +9 -5
  171. package/dist/plugins/remote-env-adb.d.ts +3 -0
  172. package/dist/plugins/remote-env-adb.js +1 -0
  173. package/dist/plugins/remote-env-device.d.ts +3 -0
  174. package/dist/plugins/remote-env-device.js +1 -0
  175. package/dist/plugins/remote-env-e2b.d.ts +4 -0
  176. package/dist/plugins/remote-env-e2b.js +2 -1
  177. package/dist/plugins/remote-env-host.d.ts +23 -1
  178. package/dist/plugins/remote-env-host.js +20 -2
  179. package/dist/plugins/remote-env-k8s.d.ts +264 -41
  180. package/dist/plugins/remote-env-k8s.js +551 -82
  181. package/dist/plugins/remote-env-local-docker.d.ts +4 -0
  182. package/dist/plugins/remote-env-local-docker.js +2 -1
  183. package/dist/plugins/remote-env-ssh.d.ts +3 -0
  184. package/dist/plugins/remote-env-ssh.js +1 -0
  185. package/dist/plugins/retention-store-sql.d.ts +4 -1
  186. package/dist/plugins/retention-store-sql.js +11 -9
  187. package/dist/plugins/run-store-sql.d.ts +44 -6
  188. package/dist/plugins/run-store-sql.js +92 -27
  189. package/dist/plugins/session-capture-record-store-sql.d.ts +2 -3
  190. package/dist/plugins/session-store.d.ts +4 -3
  191. package/dist/plugins/session-store.js +1 -1
  192. package/dist/plugins/sql-driver.d.ts +2 -1
  193. package/dist/plugins/store-backend.d.ts +12 -5
  194. package/dist/plugins/store-backend.js +13 -4
  195. package/dist/plugins/store-contracts.d.ts +20 -3
  196. package/dist/plugins/store-open-wait.js +3 -0
  197. package/dist/plugins/tidb-pool.js +15 -3
  198. package/dist/plugins/tidb-session-storage.js +3 -2
  199. package/dist/plugins/tidb-session-store.d.ts +2 -0
  200. package/dist/plugins/tidb-session-store.js +6 -2
  201. package/dist/request-key-closure.d.ts +32 -7
  202. package/dist/request-key-closure.js +25 -9
  203. package/dist/router/route-orchestration.d.ts +3 -11
  204. package/dist/router/route-orchestration.js +0 -4
  205. package/dist/rule-entry-list.js +0 -4
  206. package/dist/rules-consent.js +3 -0
  207. package/dist/run-acceptance.d.ts +51 -0
  208. package/dist/run-acceptance.js +27 -0
  209. package/dist/run-local.d.ts +5 -4
  210. package/dist/run-local.js +23 -17
  211. package/dist/runs.d.ts +7 -11
  212. package/dist/runs.js +6 -16
  213. package/dist/sandbox-egress-policy.d.ts +61 -0
  214. package/dist/sandbox-egress-policy.js +235 -0
  215. package/dist/security.d.ts +7 -0
  216. package/dist/security.js +5 -0
  217. package/dist/server-secret-env.d.ts +10 -4
  218. package/dist/server-secret-env.js +4 -1
  219. package/dist/session-error-wire.d.ts +2 -2
  220. package/dist/session-incarnation.d.ts +46 -2
  221. package/dist/session-incarnation.js +14 -3
  222. package/dist/session-sync-kernel.d.ts +10 -3
  223. package/dist/session-titler.d.ts +5 -3
  224. package/dist/spec-fields.d.ts +24 -8
  225. package/dist/spec-fields.js +21 -2
  226. package/dist/task-workflow.d.ts +6 -4
  227. package/dist/task-workflow.js +3 -2
  228. package/dist/tool-approval.d.ts +78 -6
  229. package/dist/tool-approval.js +7 -3
  230. package/dist/trace/core-keyset-guard.d.ts +6 -5
  231. package/dist/trace/engine-notice-wire.d.ts +26 -1
  232. package/dist/trace/engine-notice-wire.js +7 -2
  233. package/dist/trace/ledger-events.d.ts +9 -3
  234. package/dist/trace/ledger-sink.d.ts +7 -0
  235. package/dist/trace/ledger-sink.js +14 -4
  236. package/dist/trace/project.d.ts +57 -11
  237. package/dist/trace/project.js +54 -8
  238. package/dist/trace/self-minted-wire.d.ts +94 -2
  239. package/dist/trace/self-minted-wire.js +28 -0
  240. package/dist/trace/tool-output-media.d.ts +26 -0
  241. package/dist/trace/tool-output-media.js +57 -0
  242. package/dist/trace/wire-projection-faces.d.ts +7 -1
  243. package/dist/trace/wire-projection-faces.js +2 -1
  244. package/dist/web-admin-client/apply-effective.d.ts +13 -7
  245. package/dist/web-admin-client/apply-effective.js +11 -26
  246. package/dist/web-admin-client/apply-ledger.d.ts +2 -2
  247. package/dist/web-admin-client/candidate-gate.d.ts +39 -4
  248. package/dist/web-admin-client/candidate-gate.js +9 -2
  249. package/dist/web-admin-client/facade.d.ts +2 -2
  250. package/dist/web-admin-client/http-client.d.ts +3 -1
  251. package/dist/web-admin-client/http-client.js +4 -3
  252. package/dist/web-admin-client/read-warnings.d.ts +33 -5
  253. package/dist/web-admin-client/read-warnings.js +41 -6
  254. package/dist/web-admin-client/types.d.ts +11 -5
  255. package/dist/workspace-cache.d.ts +45 -0
  256. package/dist/workspace-cache.js +30 -0
  257. package/dist/workspace-space-commit.d.ts +116 -0
  258. package/dist/workspace-space-commit.js +117 -0
  259. package/dist/workspace-space.d.ts +75 -9
  260. package/dist/workspace-space.js +52 -1
  261. package/package.json +4 -4
  262. package/dist/boot/session-incarnation-seat.d.ts +0 -41
  263. package/dist/boot/session-incarnation-seat.js +0 -69
  264. package/dist/capabilities/code-review-council.d.ts +0 -49
  265. package/dist/capabilities/code-review-council.js +0 -111
  266. package/dist/capabilities/collab-wire.d.ts +0 -36
  267. package/dist/capabilities/collab-wire.js +0 -2
  268. package/dist/capabilities/collab-workflows.d.ts +0 -47
  269. package/dist/capabilities/collab-workflows.js +0 -90
  270. package/dist/capabilities/prompts/code-review.d.ts +0 -25
  271. package/dist/capabilities/prompts/code-review.js +0 -39
  272. package/dist/capabilities/prompts/team.d.ts +0 -10
  273. package/dist/capabilities/prompts/team.js +0 -6
  274. package/dist/capabilities/team.d.ts +0 -113
  275. package/dist/capabilities/team.js +0 -70
@@ -6,10 +6,9 @@ const sessionFaceUnavailable = async (sessionId) => {
6
6
  throw new Error(`parked decide cannot verify which occupant session ${sessionId} belongs to: this deployment's session store has no sessionFaceOf read (every built-in store has one) — refusing rather than resuming unverified`);
7
7
  };
8
8
  import { HttpError, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE } from "../security.js";
9
- import { evictIfConflict, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
9
+ import { parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
10
10
  import { failureCodeOf, nestedPauseOf, sealResultForExit, outwardPlane } from "../terminal.js";
11
11
  import { buildCancelledLedgerRow, createRunAbort, mintCancelledResult } from "../run-cancel-context.js";
12
- import {} from "../host-decision.js";
13
12
  import { readTurnActivityMs, recordTurnActivity } from "../turn-activity.js";
14
13
  import { recordResumeResourceWindow } from "../resource-window.js";
15
14
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKeyOf, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
@@ -23,10 +22,10 @@ import { TERMINAL_PARK_GATE_KINDS } from "../plugins/checkpoint-store-sql.js";
23
22
  import { withPrincipal } from "../observability/principal-context.js";
24
23
  import { withModelReachScope } from "../model-reach-scope.js";
25
24
  import { createInstructionSelectionLeg } from "../instruction-selection.js";
26
- import { modelTextEventData, turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, toolRosterDeltaEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "../trace/project.js";
25
+ import { modelTextEventData, turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskStartedEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, toolRosterDeltaEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "../trace/project.js";
27
26
  import { redactSecrets } from "../trace/redact.js";
28
27
  import { buildRunObjectivePreview } from "../user-authored-text.js";
29
- import { createLedgerSink, createSerialLedgerAppend } from "../trace/ledger-sink.js";
28
+ import { createForwardAppendFailedNote, createLedgerSink, createSerialLedgerAppend } from "../trace/ledger-sink.js";
30
29
  import { registerEngineNoticeLeg } from "../trace/engine-notice-wire.js";
31
30
  import { reasoningDeltaBytes, reasoningSegmentEventId } from "../trace/model-segment.js";
32
31
  import { errorText } from "../observability/err-text.js";
@@ -49,6 +48,7 @@ import { readResumeCtx, resumeCtxCorruptAnswer } from "./resume-ctx-read.js";
49
48
  import { buildCheckpointVersionKeys, resumeBlockedByPolicyAnswer, storedBodyReplayAnswer, storedBodyReplayLogFields, withPermissionModeRewritten } from "./checkpoint-ctx.js";
50
49
  import { storedBodyReplayRefusal } from "./stored-body-replay.js";
51
50
  import { httpErrorCode } from "./send.js";
51
+ import { createRunAcceptance } from "../run-acceptance.js";
52
52
  function isCheckpointReopenedFailure(r) {
53
53
  const code = failureCodeOf(r.terminal);
54
54
  return code === "resume.env_failed" || code === "resume.tool_unavailable" || code === "resume.tool_contract_mismatch";
@@ -68,6 +68,7 @@ const WORKER_SWAP_REDEEMABLE = {
68
68
  ledger_event_unknown: true,
69
69
  real_approval_damaged: false,
70
70
  real_approval_forged: false,
71
+ requirements_damaged: false,
71
72
  read_face_damaged: false,
72
73
  read_face_forged: false,
73
74
  write_face_damaged: false,
@@ -162,7 +163,7 @@ function notResumableAnswer() {
162
163
  return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
163
164
  }
164
165
  export function createResumeLegs(ctx) {
165
- const { deps, runnerFor, idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor } = ctx;
166
+ const { deps, runnerFor, idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor } = ctx;
166
167
  const resumeFleetScope = (req, auth) => req ? (gatedPrincipal(req, deps.config) ?? "default") : (auth.principal ?? "default");
167
168
  async function resumeCheckpoint(sessionId, decision, reason, hostDecision, req, answer, binding, onResumeCommitted, acceptEarly, decider) {
168
169
  const cs = deps.checkpointStore;
@@ -769,10 +770,16 @@ export function createResumeLegs(ctx) {
769
770
  if (!rs || !taskId)
770
771
  return;
771
772
  const t = e.type;
772
- const warnAppend = (kind) => (err) => deps.logger?.warn?.("forward_event_append_failed", { type: kind, taskId, err: err instanceof Error ? err.message : String(err) });
773
+ const warnAppend = (kind) => (err) => {
774
+ createForwardAppendFailedNote(kind)(err);
775
+ deps.logger?.warn?.("forward_event_append_failed", { type: kind, taskId, err: err instanceof Error ? err.message : String(err) });
776
+ };
773
777
  if (t === "task_progress") {
774
778
  void append("task_progress", taskProgressEventData(e)).catch(warnAppend(t));
775
779
  }
780
+ else if (t === "task_started") {
781
+ void append("task_started", taskStartedEventData(e)).catch(warnAppend(t));
782
+ }
776
783
  else if (t === "tool_start") {
777
784
  void append("tool_start", toolStartEventData(e)).catch(warnAppend(t));
778
785
  }
@@ -901,6 +908,9 @@ export function createResumeLegs(ctx) {
901
908
  case "message_committed":
902
909
  await anchor.onMessageCommitted(ev.role, ev.entryId);
903
910
  break;
911
+ case "task_started":
912
+ await append("task_started", taskStartedEventData(ev));
913
+ break;
904
914
  case "task_progress":
905
915
  await append("task_progress", taskProgressEventData(ev));
906
916
  break;
@@ -1085,7 +1095,6 @@ export function createResumeLegs(ctx) {
1085
1095
  clearInterval(resumeHeartbeat);
1086
1096
  resumeLegLive = false;
1087
1097
  }
1088
- evictIfConflict(deps.runner, sessionId, result);
1089
1098
  const checkpointReopened = reopenConfirmedPark;
1090
1099
  const sealed = sealResultForExit(result);
1091
1100
  const resumePlane = outwardPlane(sealed.terminal);
@@ -1214,7 +1223,7 @@ export function createResumeLegs(ctx) {
1214
1223
  const { spec, engineNotices } = resolved;
1215
1224
  const hostSpec = { ...spec, objective, sessionId };
1216
1225
  const out = await mintAndDriveDurableRun(routeCtxBase, deps.runStore, {
1217
- prepared: { spec: hostSpec, runner: runnerFor(hostSpec), auth, body: ctx.body, ...(engineNotices.length > 0 ? { engineNotices } : {}) },
1226
+ prepared: { spec: hostSpec, runner: runnerFor(hostSpec), auth, body: ctx.body, acceptance: createRunAcceptance({}, { objective }), ...(engineNotices.length > 0 ? { engineNotices } : {}) },
1218
1227
  fleetScope: resumeFleetScope(req, auth),
1219
1228
  elicitOwner: principal ?? null,
1220
1229
  source: "workflow_parked_decide",
@@ -1377,7 +1386,7 @@ export function createResumeLegs(ctx) {
1377
1386
  return refuse(refuseStoredBodyReplay(sessionId, replay));
1378
1387
  return { ok: true, prepared: { sessionId, token, cp, ctx: ctxRead.ctx } };
1379
1388
  }
1380
- async function drivePlanReview(prepared, decision, editedPlan, reason, req, acceptEarly, permissionModeAfter) {
1389
+ async function drivePlanReview(prepared, decision, editedPlan, reason, hostDecision, req, acceptEarly, permissionModeAfter) {
1381
1390
  const { sessionId, token, cp, ctx } = prepared;
1382
1391
  if ((await deps.checkpointStore.findPendingTokenBySession(sessionId, undefined, { gateKinds: ["plan_review"] })) !== token)
1383
1392
  return notResumableAnswer();
@@ -1399,6 +1408,7 @@ export function createResumeLegs(ctx) {
1399
1408
  decision,
1400
1409
  ...(editedPlan !== undefined ? { editedPlan } : {}),
1401
1410
  ...(reason ? { reason } : {}),
1411
+ hostDecision,
1402
1412
  };
1403
1413
  const out = await driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, resumeCtx: { sessionId, ctx: rebuildCtx }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
1404
1414
  return out;
@@ -1406,7 +1416,7 @@ export function createResumeLegs(ctx) {
1406
1416
  const routeCtxBase = {
1407
1417
  deps,
1408
1418
  registry: { idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters },
1409
- helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
1419
+ helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
1410
1420
  local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal(), capabilities: createCapabilitiesLocal() },
1411
1421
  legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake, preparePlanReview, drivePlanReview },
1412
1422
  };
@@ -72,11 +72,11 @@ export interface RouteHelpers {
72
72
  usageWindowDenied(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
73
73
  safeDecode(seg: string): string | null;
74
74
  isFleetWide(req: IncomingMessage): boolean;
75
- /** 属主门。`notFoundForm` 选 not-yours 臂的 404 拼法:缺省 `"run"`(not_found.run 长文,[ref]);**每一个 session
76
- * 资源面都必须传 `"session"`**(not_found.session + {@link SESSION_NOT_FOUND_MESSAGE})与其 unknown 臂同码同串
77
- * (G52 [ref];S-989 补上了 detail / head / events 三个读口 —— 它们此前吃缺省 "run",单端点内 unknown / not-yours
78
- * 两臂码与串都不同)。缺省 `"run"` 的语义不变:run 面不传。 */
79
- runOwnerOk(req: IncomingMessage, res: ServerResponse, owner: string | null, notFoundForm?: "run" | "session"): boolean;
75
+ /** run 面的属主门(谓词 `runOwnerReadable`;非属主 ⇒ 404 `not_found.run` + 长文,与 run 读口的 unknown 臂同码同串,[ref])。 */
76
+ runOwnerOk(req: IncomingMessage, res: ServerResponse, owner: string | null): boolean;
77
+ /** 会话面的属主门(谓词 `sessionOwnerReadable`,S-1452:没有身份边界的部署不钉人,与会话列表同读;非属主 ⇒ 404 `not_found.session` +
78
+ * {@link SESSION_NOT_FOUND_MESSAGE},与会话读口的 unknown 臂同码同串 —— G52 [ref] / S-989)。每一个按会话寻址的属主判都走它。 */
79
+ sessionOwnerOk(req: IncomingMessage, res: ServerResponse, owner: string | null): boolean;
80
80
  sessionOwnerScope(req: IncomingMessage): {
81
81
  fleetWide: boolean;
82
82
  gateOwner: string | null;
@@ -140,6 +140,12 @@ export interface PreparedTaskSubmission {
140
140
  * 成功点**必须**把它交给 `commitPlacement`(唯一写入属主;类级机器门钉住每个调用点都接了)。
141
141
  */
142
142
  bindingIntent?: import("./device-placement.js").DevicePlacementIntent;
143
+ /**
144
+ * 这次提交的**受理座**(`run-acceptance.ts`):标题器只在引擎受理之后起。提交门(`prepareSpec`)随 spec 一起铸;
145
+ * 三条提交腿只喂它(持流的形逐帧 `observe`,终局落行之后 `settle` 一次),不再各自判「什么时候起标题」。
146
+ * 不经提交门的内部车道(`/decide` 的宿主新 turn)铸一只不带标题器的座:不起标题。
147
+ */
148
+ acceptance: import("../run-acceptance.js").RunAcceptance;
143
149
  }
144
150
  /**
145
151
  * 一条会话的**续跑重建输入**(`checkpoint_ctx` 的行体)—— 提交腿写一次,续跑腿读它重建 spec。
@@ -384,7 +390,7 @@ export interface RouteLegs {
384
390
  liveStream?: boolean;
385
391
  }): Promise<PreparedTaskSubmission | null>;
386
392
  /** 同步腿的终局记账(计费/配额/指标),tasks 域用。 */
387
- finalizeTaskResult(result: TaskResult, principal: string | undefined, objective: string, sessionId: string | undefined): void;
393
+ finalizeTaskResult(result: TaskResult, principal: string | undefined, objective: string): void;
388
394
  /** approvals 决策腿:session → pending checkpoint → markResuming CAS → 驱动续跑。
389
395
  * `hostDecision` = **这次结算的事实**(谁结束了这次等待 + 通道自报的归属),必填:本腿有两个调用方
390
396
  * (HTTP `/decide` = 人;内部 D-D SLA deny-sweep = 窗到期),而函数内部分辨不出谁在叫它 —— 必填参数把
@@ -461,7 +467,9 @@ export interface RouteLegs {
461
467
  readonly prepared: PreparedPlanReview;
462
468
  }>;
463
469
  /** plan_review 三态腿的**驱动段**(S-609):只吃 prepare 的结果,用它选定的 token,不再选卡、不再读 ctx。 */
464
- drivePlanReview(prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined,
470
+ drivePlanReview(prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined,
471
+ /** core 7.49.0 #1396:决策事实(谁决定的 + 归属),语义属主 = `resume-legs.ts` 的同名形参顶注。 */
472
+ hostDecision: HostDecision, req: IncomingMessage | undefined,
465
473
  /** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/plan_review` 腿传 `true`。 */
466
474
  acceptEarly?: boolean,
467
475
  /** S-433:批准之后这条会话继续用的 permission mode(闭集 `PLAN_APPROVE_PERMISSION_MODES_AFTER`;只在把计划带出 plan 的决定
@@ -123,7 +123,11 @@ function asNonEmptyString(v, what, max) {
123
123
  throw new RpcError(JSONRPC_INVALID_PARAMS, `${what} must be at most ${max} characters`);
124
124
  return v;
125
125
  }
126
- const MAX_CONTEXT_ID_CHARS = 64;
126
+ function asString(v, what) {
127
+ if (typeof v !== "string")
128
+ throw new RpcError(JSONRPC_INVALID_PARAMS, `${what} must be a string`);
129
+ return v;
130
+ }
127
131
  const MAX_MESSAGE_ID_CHARS = 200;
128
132
  const MAX_TASK_ID_CHARS = 128;
129
133
  const MAX_SKILL_ID_CHARS = 128;
@@ -224,7 +228,7 @@ async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
224
228
  if (message.role !== "user")
225
229
  throw new RpcError(JSONRPC_INVALID_PARAMS, `params.message.role must be "user" (this is the inbound leg — an agent-role message has no meaning here)`);
226
230
  const objective = objectiveFromParts(message.parts);
227
- const contextId = message.contextId === undefined ? undefined : asNonEmptyString(message.contextId, "params.message.contextId", MAX_CONTEXT_ID_CHARS);
231
+ const contextId = message.contextId === undefined ? undefined : asString(message.contextId, "params.message.contextId");
228
232
  const messageId = asNonEmptyString(message.messageId, "params.message.messageId", MAX_MESSAGE_ID_CHARS);
229
233
  const skill = skillFromMetadata(message.metadata, serve);
230
234
  const skillId = skill?.id;
@@ -238,19 +242,19 @@ async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
238
242
  .update(skillId ?? "")
239
243
  .digest("hex");
240
244
  const submitter = caller.principal ?? undefined;
241
- const idemKey = idemKeyOf(fingerprint, caller, "run");
245
+ const idem = { key: idemKeyOf(fingerprint, caller, "run"), bodyFingerprint: fingerprint };
242
246
  const body = {
243
247
  objective,
244
248
  taskId: durableIdempotencyTaskId(idemIdentityOf(caller), fingerprint),
245
249
  ...(contextId !== undefined ? { sessionId: contextId } : {}),
246
250
  ...(scenario !== undefined ? { scenario } : {}),
247
251
  };
248
- const keyed = (await runStore.getRunByIdem(idemKey).catch(() => undefined)) ?? (await runStore.getRun(body.taskId).catch(() => undefined));
249
- const replay = keyed ? await replayOfExistingRun(keyed, { principal: submitter, clientTaskId: body.taskId }, "durable", deps, runStore) : undefined;
252
+ const keyed = (await runStore.getRunByIdem(idem.key).catch(() => undefined)) ?? (await runStore.getRun(body.taskId).catch(() => undefined));
253
+ const replay = keyed ? await replayOfExistingRun(keyed, { principal: submitter, clientTaskId: body.taskId, idem }, "durable", deps, runStore) : undefined;
250
254
  if (keyed && replay?.status === 202) {
251
255
  return taskFromRun(keyed, isStaleRun(keyed, deps.config.runStaleSec));
252
256
  }
253
- const inflight = replay === undefined ? ctx.registry.idemCache.peek(inflightKeyOf(idemKey, "durable", body.taskId)) : undefined;
257
+ const inflight = replay === undefined ? ctx.registry.idemCache.peek(inflightKeyOf(idem, "durable", body.taskId)) : undefined;
254
258
  let receipt;
255
259
  if (replay !== undefined) {
256
260
  receipt = replay;
@@ -271,7 +275,7 @@ async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
271
275
  throw rpcErrorFromTypedFailure(typed, skillId);
272
276
  if (!prepared)
273
277
  return undefined;
274
- receipt = await createDurableRun(req, res, ctx, prepared, idemKey);
278
+ receipt = await createDurableRun(req, res, ctx, prepared, idem);
275
279
  }
276
280
  if (!receipt)
277
281
  return undefined;
@@ -31,10 +31,10 @@ const DECIDE_BODY_DECLARED_KEYS = [
31
31
  const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "reason"];
32
32
  const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "decidedBy", "remember", "updatedInput"];
33
33
  const PLAN_REVIEW_BODY_DECLARED_KEYS = [
34
- "boundCallId", "decision", "editedPlan", "permissionModeAfter", "reason",
34
+ "boundCallId", "decidedBy", "decision", "editedPlan", "permissionModeAfter", "reason",
35
35
  ];
36
36
  const PLAN_REVIEW_PROOF_COVERED_KEYS = ["boundCallId", "decision", "editedPlan", "permissionModeAfter", "reason"];
37
- const PLAN_REVIEW_NOT_IN_PROOF_KEYS = [];
37
+ const PLAN_REVIEW_NOT_IN_PROOF_KEYS = ["decidedBy"];
38
38
  const PROOF_SLOT_OF = {
39
39
  answer: "answer",
40
40
  boundCallId: "boundCallId",
@@ -522,6 +522,21 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
522
522
  sendError(res, 400, "request.field_conflict", "decision 'edit' requires a non-empty editedPlan (the operator's revised plan)");
523
523
  return;
524
524
  }
525
+ const decidedByReading = readDecidedBy(body.decidedBy, deps.config.directDoorActive);
526
+ switch (decidedByReading.kind) {
527
+ case "not_in_proof":
528
+ sendError(res, 400, "decided_by_not_in_proof", DECIDED_BY_NOT_IN_PROOF);
529
+ return;
530
+ case "outside_closed_set":
531
+ sendError(res, 400, "request.field_invalid", DECIDED_BY_OUTSIDE_CLOSED_SET);
532
+ return;
533
+ case "accepted":
534
+ break;
535
+ default: {
536
+ const unreachable = decidedByReading;
537
+ throw new Error(`unhandled decidedBy reading: ${JSON.stringify(unreachable)}`);
538
+ }
539
+ }
525
540
  if (decision === "edit" && !isWellFormedUnicode(body.editedPlan)) {
526
541
  sendError(res, 400, "request.field_invalid", "editedPlan must be well-formed Unicode text (a lone surrogate cannot be signed unambiguously)");
527
542
  return;
@@ -626,7 +641,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
626
641
  return;
627
642
  }
628
643
  }
629
- const out = await drivePlanReview(plan.prepared, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req, true, isPlanApprovePermissionModeAfter(body.permissionModeAfter) ? body.permissionModeAfter : undefined);
644
+ const out = await drivePlanReview(plan.prepared, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, hostDecisionFacts(decidedByReading.decidedBy, deciderPrincipal), req, true, isPlanApprovePermissionModeAfter(body.permissionModeAfter) ? body.permissionModeAfter : undefined);
630
645
  sendResumeOutcome(res, out, deps.logger);
631
646
  return;
632
647
  }
@@ -29,7 +29,7 @@
29
29
  * `rateLimited`。能力位 `background: { listFace, exitFaces }` 与两口的 501 从同一个谓词推导(会话属主面在场)。
30
30
  *
31
31
  * 🔴 **停止口在无凭据写门内**(`credentialGated: ["POST"]`;车OL F1 = 7.102.0 合并复审 gpt-6 腿 [high]):门的两个合取项它都中
32
- * —— ① 授权的唯一输入是 principal 头(属主判 `runOwnerOk` 与 operator 臂 `explicitOperatorOk` 读的都是 `gatedPrincipal`,
32
+ * —— ① 授权的唯一输入是 principal 头(属主判 `sessionOwnerOk` 与 operator 臂 `explicitOperatorOk` 读的都是 `gatedPrincipal`,
33
33
  * 无 service 凭证的部署上它收调用方自报的头);② 不可撤销、爆炸半径跨租户(知道会话 id + 自报它的属主 ⇒ 停掉那个会话的后台
34
34
  * 进程,`includeRetained:true` 连显式 retain 的活服务一起停)。于是无 service 凭证且未开 `ALLOW_UNAUTHED_WRITES` 的部署上它与
35
35
  * 其余改写门同码同句 503 `auth.service_token_required`(在路由之前,`dispatch.ts` 那一道),同一只会话级收敛函数的另一个调用点
@@ -68,7 +68,7 @@ export async function handleBackground(req, res, match, ctx) {
68
68
  return;
69
69
  }
70
70
  const isOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
71
- if (!isOperator && !ctx.helpers.runOwnerOk(req, res, owner, "session"))
71
+ if (!isOperator && !ctx.helpers.sessionOwnerOk(req, res, owner))
72
72
  return;
73
73
  const scope = owner ?? "default";
74
74
  switch (id) {
@@ -1,6 +1,7 @@
1
1
  import { PERMISSION_MODES, expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
2
2
  import { modelImageInput, isAtMentionable, selectableCatalog } from "../../model-select.js";
3
3
  import { buildDroppedEntriesWire } from "../../web-admin-client/read-warnings.js";
4
+ import { buildCandidateRejectedWire } from "../../web-admin-client/candidate-gate.js";
4
5
  import { modelKeyPoisonOf, resolveModelApiKey } from "../../key-resolver.js";
5
6
  import { projectSealedKeyCapability } from "../../sealed-key.js";
6
7
  import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
@@ -205,7 +206,7 @@ export async function handleCapabilities(req, res, match, ctx) {
205
206
  manualCompact: facts.durableRuns(deps).available,
206
207
  configCatalog: facts.configCatalog(deps).available,
207
208
  sealedKey: projectSealedKeyCapability(deps.sealedKeyPublic),
208
- workspaceSync: ((f) => (f.available ? f.policy : undefined))(facts.workspaceSpace(deps.config)),
209
+ workspaceSync: ((space, cache) => space.available || cache.available ? { ...(space.available ? space.policy : {}), ...(cache.available ? { cache: cache.cache } : {}) } : undefined)(facts.workspaceSpace(deps.config), facts.workspaceCache(deps.config)),
209
210
  }));
210
211
  return;
211
212
  }
@@ -256,7 +257,14 @@ export async function handleCapabilities(req, res, match, ctx) {
256
257
  }));
257
258
  const nonAlias = Object.entries(deps.config.models).filter(([n]) => n !== "default");
258
259
  const defaultName = (nonAlias.find(([, m]) => m === deps.config.model) ?? nonAlias.find(([, m]) => m.id === deps.config.model.id))?.[0];
259
- sendJson(res, 200, { models, default: deps.config.model.id, defaultModel: { id: deps.config.model.id, ...(defaultName ? { name: defaultName } : {}) }, droppedEntries: buildDroppedEntriesWire(deps.config.droppedCatalogEntries) });
260
+ const candidateRejection = deps.candidateRejectionState?.();
261
+ sendJson(res, 200, {
262
+ models,
263
+ default: deps.config.model.id,
264
+ defaultModel: { id: deps.config.model.id, ...(defaultName ? { name: defaultName } : {}) },
265
+ droppedEntries: buildDroppedEntriesWire(deps.config.droppedCatalogEntries),
266
+ ...(candidateRejection !== undefined ? { candidateRejected: buildCandidateRejectedWire(candidateRejection) } : {}),
267
+ });
260
268
  return;
261
269
  }
262
270
  default:
@@ -94,7 +94,7 @@ export async function handleMcpReconnect(req, res, match, ctx) {
94
94
  return;
95
95
  }
96
96
  const isOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
97
- if (!isOperator && !ctx.helpers.runOwnerOk(req, res, owner, "session"))
97
+ if (!isOperator && !ctx.helpers.sessionOwnerOk(req, res, owner))
98
98
  return;
99
99
  const activeTaskId = await runStore.getActiveTaskId(sessionId);
100
100
  const live = activeTaskId === undefined ? undefined : ctx.registry.steerableRuns.get(activeTaskId);
@@ -1,4 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { type RunIdemBinding } from "../../plugins/store-contracts.js";
2
3
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
3
4
  import type { RouteCtx, PreparedTaskSubmission, RouteMatch, RouteIdsOf } from "../route-ctx.js";
4
5
  /** 行上的 `decision_actor`(裸 JSON)投成回显形;形不认 ⇒ undefined(调用点省略该键,不 500)。
@@ -31,9 +32,9 @@ export declare function handleRunVerbs(req: IncomingMessage, res: ServerResponse
31
32
  * 防御性复检(缺席 ⇒ 501),但两个调用方都在自己的入口先判过,那才是各自面上的正确错误形。
32
33
  */
33
34
  export declare function createDurableRun(req: IncomingMessage, res: ServerResponse, ctx: RouteCtx, prepared: PreparedTaskSubmission,
34
- /** 这次提交的幂等键(`idemKeyOf(…, "run")`:`POST /v1/runs` = `Idempotency-Key` 头派生;A2A = `message.messageId`
35
- * + 内容指纹派生)—— 落 `task_run.idem_key`,同时是同进程在飞合流的键。undefined = 调用方没带键。 */
36
- idemKey: string | undefined): Promise<{
35
+ /** 这次提交的账本绑定(键 `idemKeyOf(…, "run")` + 请求体指纹:`POST /v1/runs` = `Idempotency-Key` 头 + 线上收到的体;A2A =
36
+ * `message.messageId` + 内容指纹)—— 落 `task_run.idem_key` / `idem_body_fingerprint`,键同时是同进程在飞合流的键。undefined = 调用方没带键。 */
37
+ idem: RunIdemBinding | undefined): Promise<{
37
38
  status: number;
38
39
  body: unknown;
39
40
  } | null>;
@@ -1,3 +1,4 @@
1
+ import { liveToolEndForBacklog } from "../../trace/tool-output-media.js";
1
2
  import { redactHead, redactLedgerEventForRead } from "../../observability/run-terminal.js";
2
3
  import { z } from "zod";
3
4
  import { uuidv7, mintCheckpointToken, CheckpointError, policyCannotAnswer, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS, isSystemInjectionPriority } from "@sema-agent/core";
@@ -7,7 +8,7 @@ import { redactSecrets } from "../../trace/redact.js";
7
8
  import { HttpError, isUuidV7, encodeCheckpointScope } from "../../security.js";
8
9
  import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from "../../orchestration/workflow-agent-steer.js";
9
10
  import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
10
- import { idemCallerOf, idemKeyOf, inflightKeyOf } from "../idempotency.js";
11
+ import { idemCallerOf, idemKeyOf, inflightKeyOf, submissionIdemOf } from "../idempotency.js";
11
12
  import { streamSseLog } from "../sse-log.js";
12
13
  import { bindSseLifecycle } from "../sse-lifecycle.js";
13
14
  import { buildApprovalPreamble, buildApprovalPreambleSseFrames, readClosedWordSeat, storedCardDemandsAPerson } from "../../approval-card.js";
@@ -33,6 +34,7 @@ import { readResumeCtx } from "../resume-ctx-read.js";
33
34
  import { storedBodyReplayAnswer, storedBodyReplayLogFields } from "../checkpoint-ctx.js";
34
35
  import { serviceVersion } from "../../service-version.js";
35
36
  import { storedBodyReplayRefusal } from "../stored-body-replay.js";
37
+ import { steerBodyKeyIssue } from "../../request-key-closure.js";
36
38
  const STEER_PRIORITY_WORDS = INJECTION_TIERS.map((tier) => `'${tier}'`);
37
39
  const STEER_PRIORITY_UNION = STEER_PRIORITY_WORDS.join(" | ");
38
40
  const STEER_PRIORITY_CHOICES = STEER_PRIORITY_WORDS.length <= 2 ? STEER_PRIORITY_WORDS.join(" or ") : `${STEER_PRIORITY_WORDS.slice(0, -1).join(", ")}, or ${STEER_PRIORITY_WORDS[STEER_PRIORITY_WORDS.length - 1]}`;
@@ -188,19 +190,18 @@ export async function handleRuns(req, res, match, ctx) {
188
190
  const runStore = runs.runStore;
189
191
  const rawIdem = headerStr(req.headers["idempotency-key"]);
190
192
  const submitter = gatedPrincipal(req, deps.config);
191
- const idemKey = rawIdem ? idemKeyOf(rawIdem, idemCallerOf(req, ctx), "run") : undefined;
192
- let keyedBody;
193
- if (idemKey) {
194
- keyedBody = await readJson(req);
193
+ const keyedBody = rawIdem ? await readJson(req) : undefined;
194
+ const idem = rawIdem ? submissionIdemOf(rawIdem, idemCallerOf(req, ctx), keyedBody) : undefined;
195
+ if (idem) {
195
196
  const clientTaskId = clientTaskIdOf(keyedBody);
196
197
  if (clientTaskId === false) {
197
198
  sendError(res, 400, "request.id_invalid", "body.taskId must be a uuidv7 string (caller-minted idempotency key)");
198
199
  return;
199
200
  }
200
- const inflight = idemCache.peek(inflightKeyOf(idemKey, "durable", clientTaskId));
201
- const keyedRow = inflight ? undefined : await runStore.getRunByIdem(idemKey);
201
+ const inflight = idemCache.peek(inflightKeyOf(idem, "durable", clientTaskId));
202
+ const keyedRow = inflight ? undefined : await runStore.getRunByIdem(idem.key);
202
203
  if (inflight || keyedRow) {
203
- const resp = inflight ? await inflight : await replayOfExistingRun(keyedRow, { principal: submitter, ...(clientTaskId !== undefined ? { clientTaskId } : {}) }, "durable", deps, runStore);
204
+ const resp = inflight ? await inflight : await replayOfExistingRun(keyedRow, { principal: submitter, idem, ...(clientTaskId !== undefined ? { clientTaskId } : {}) }, "durable", deps, runStore);
204
205
  sendJson(res, resp.status, resp.body);
205
206
  return;
206
207
  }
@@ -210,7 +211,7 @@ export async function handleRuns(req, res, match, ctx) {
210
211
  const prepared = await prepareSpec(req, res, keyedBody);
211
212
  if (!prepared)
212
213
  return;
213
- const resp = await createDurableRun(req, res, ctx, prepared, idemKey);
214
+ const resp = await createDurableRun(req, res, ctx, prepared, idem);
214
215
  if (!resp)
215
216
  return;
216
217
  sendJson(res, resp.status, resp.body);
@@ -414,6 +415,13 @@ export async function handleRuns(req, res, match, ctx) {
414
415
  sendError(res, 400, "request.body_shape", `body must be { text: string (non-empty), mode?: 'all' | 'one-at-a-time', priority?: ${STEER_PRIORITY_UNION} }`);
415
416
  return;
416
417
  }
418
+ {
419
+ const keyIssue = steerBodyKeyIssue(body, "run");
420
+ if (keyIssue) {
421
+ sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
422
+ return;
423
+ }
424
+ }
417
425
  if (body.mode !== undefined && body.mode !== "all" && body.mode !== "one-at-a-time") {
418
426
  sendError(res, 400, "request.field_invalid", "mode must be 'all' or 'one-at-a-time' when present");
419
427
  return;
@@ -738,6 +746,11 @@ export async function handleRuns(req, res, match, ctx) {
738
746
  return;
739
747
  }
740
748
  text = interruptBody.text;
749
+ const keyIssue = steerBodyKeyIssue(interruptBody, "interrupt");
750
+ if (keyIssue) {
751
+ sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
752
+ return;
753
+ }
741
754
  }
742
755
  const interruptIdemRaw = headerStr(req.headers["idempotency-key"]);
743
756
  const interruptInputId = interruptIdemRaw ? `idem-${idemKeyOf(interruptIdemRaw, idemCallerOf(req, ctx), "run").slice(0, 32)}` : uuidv7();
@@ -1041,7 +1054,8 @@ export async function handleRunVerbs(req, res, match, ctx) {
1041
1054
  const n = await it.next();
1042
1055
  if (n.done || closed || res.writableEnded)
1043
1056
  break;
1044
- res.write(`event: forward\ndata: ${JSON.stringify(n.value)}\n\n`);
1057
+ const frame = n.value.type === "tool_end" ? liveToolEndForBacklog(n.value, res.writableLength) : n.value;
1058
+ res.write(`event: forward\ndata: ${JSON.stringify(frame)}\n\n`);
1045
1059
  if (n.value.type === "task_settled")
1046
1060
  break;
1047
1061
  }
@@ -1162,6 +1176,13 @@ export async function handleRunVerbs(req, res, match, ctx) {
1162
1176
  sendError(res, 400, "request.body_shape", "body must be { content: string (non-empty) }");
1163
1177
  return;
1164
1178
  }
1179
+ {
1180
+ const keyIssue = steerBodyKeyIssue(body, "content");
1181
+ if (keyIssue) {
1182
+ sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
1183
+ return;
1184
+ }
1185
+ }
1165
1186
  if (body.content.length > STEER_IN_MAX_REQUEST_CHARS) {
1166
1187
  sendError(res, 413, "steer.content_too_large", `content exceeds ${STEER_IN_MAX_REQUEST_CHARS} characters (got ${body.content.length}); note only the first ${STEER_IN_MAX_CHARS} are delivered anyway`);
1167
1188
  return;
@@ -1586,7 +1607,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1586
1607
  }
1587
1608
  return assertNever(match.decl.id);
1588
1609
  }
1589
- export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1610
+ export async function createDurableRun(req, res, ctx, prepared, idem) {
1590
1611
  const { deps } = ctx;
1591
1612
  const { idemCache } = ctx.registry;
1592
1613
  const { leaseDenied, usageWindowDenied } = ctx.helpers;
@@ -1603,7 +1624,7 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1603
1624
  return null;
1604
1625
  }
1605
1626
  const submitter = gatedPrincipal(req, deps.config);
1606
- const caller = { principal: submitter, ...(clientTaskId !== undefined ? { clientTaskId } : {}) };
1627
+ const caller = { principal: submitter, idem, ...(clientTaskId !== undefined ? { clientTaskId } : {}) };
1607
1628
  if (clientTaskId) {
1608
1629
  const existing = await runStore.getRun(clientTaskId);
1609
1630
  if (existing)
@@ -1611,11 +1632,11 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1611
1632
  }
1612
1633
  if ((await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
1613
1634
  return null;
1614
- return await idemCache.run(idemKey !== undefined ? inflightKeyOf(idemKey, "durable", clientTaskId) : undefined, async () => {
1635
+ return await idemCache.run(idem !== undefined ? inflightKeyOf(idem, "durable", clientTaskId) : undefined, async () => {
1615
1636
  const out = await mintAndDriveDurableRun({ deps, registry: ctx.registry, helpers: ctx.helpers, local: ctx.local, legs: ctx.legs }, runStore, {
1616
1637
  prepared,
1617
1638
  ...(clientTaskId !== undefined ? { clientTaskId } : {}),
1618
- ...(idemKey !== undefined ? { idemKey } : {}),
1639
+ ...(idem !== undefined ? { idem } : {}),
1619
1640
  fleetScope: submitter ?? "default",
1620
1641
  elicitOwner: submitter ?? null,
1621
1642
  source,
@@ -44,7 +44,6 @@ export async function handleSessionSync(req, res, match, ctx) {
44
44
  return;
45
45
  }
46
46
  const { backend, sessionStorage, historyStore } = sync;
47
- const ownerOf = sessionStorage.ownerOf.bind(sessionStorage);
48
47
  const scope = sessionOwnerScopeForWrite(req);
49
48
  if (!scope.fleetWide && scope.gateOwner === null) {
50
49
  sendUnauthorized(res, "identity");
@@ -55,7 +54,8 @@ export async function handleSessionSync(req, res, match, ctx) {
55
54
  sendError(res, 400, "request.id_invalid", "invalid session id (must be a canonical uuid)");
56
55
  return;
57
56
  }
58
- const owner = await ownerOf(sessionId);
57
+ const gateFace = await sessionStorage.sessionFaceOf(sessionId);
58
+ const owner = gateFace === undefined ? undefined : gateFace.owner;
59
59
  const ownsOrFleet = (o) => scope.fleetWide || o === scope.gateOwner;
60
60
  const pullGateOk = () => {
61
61
  if (owner === undefined) {
@@ -75,13 +75,10 @@ export async function handleSessionSync(req, res, match, ctx) {
75
75
  }
76
76
  return true;
77
77
  };
78
- const replaySessionRow = async (row, importedOwner) => {
78
+ const replaySessionRow = async (row, face) => {
79
79
  if (Object.keys(row).length === 0)
80
- return;
81
- const face = await sessionStorage.sessionFaceOf(sessionId);
82
- if (face === undefined || face.owner !== importedOwner)
83
- return;
84
- await sessionStorage.patchSessionMeta(sessionId, row, face);
80
+ return true;
81
+ return face !== undefined && (await sessionStorage.patchSessionMeta(sessionId, row, face)) !== undefined;
85
82
  };
86
83
  if (match.decl.id === "session-sync-manifest") {
87
84
  if (!pullGateOk())
@@ -331,7 +328,10 @@ export async function handleSessionSync(req, res, match, ctx) {
331
328
  if (deps.resumeAnchorStore)
332
329
  for (const a of anchors)
333
330
  await deps.resumeAnchorStore.put(sessionId, a.eventId, a.entryId, importingP);
334
- await replaySessionRow(row, owner);
331
+ if (!(await replaySessionRow(row, gateFace))) {
332
+ sendError(res, 409, "conflict", "sync conflict");
333
+ return;
334
+ }
335
335
  sendJson(res, 200, {
336
336
  relation: "identical",
337
337
  basis: payloadVerified ? "entry-ids+digest" : "entry-ids",
@@ -521,9 +521,13 @@ export async function handleSessionSync(req, res, match, ctx) {
521
521
  if (anchorStore)
522
522
  for (const a of pending.anchors)
523
523
  await anchorStore.put(sessionId, a.eventId, a.entryId, pending.owner);
524
- await replaySessionRow(pending.row, pending.owner);
524
+ const rowLanded = await replaySessionRow(pending.row, committed.face);
525
525
  sessionStorage.evict?.(sessionId);
526
526
  cleanupStaging(stagingId);
527
+ if (!rowLanded) {
528
+ sendError(res, 409, "conflict", "sync conflict");
529
+ return;
530
+ }
527
531
  sendJson(res, 200, { relation: committed.relation });
528
532
  }
529
533
  catch (e) {
@@ -567,7 +571,6 @@ export async function handleSessionSync(req, res, match, ctx) {
567
571
  function sendSessionSyncUnavailable(res, why) {
568
572
  switch (why) {
569
573
  case "store_backend_absent":
570
- case "session_owner_face_absent":
571
574
  case "session_row_faces_absent":
572
575
  sendError(res, 501, "capability.session_store_required", "session sync requires a durable session store");
573
576
  return;