@sema-agent/server 7.63.0 → 7.64.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 (55) hide show
  1. package/MIGRATION.md +8 -0
  2. package/USAGE.md +17 -15
  3. package/deploy/sema-up/smoke.sh +2 -2
  4. package/dist/approval-ask-audit-store.d.ts +149 -3
  5. package/dist/approval-ask-audit-store.js +11 -4
  6. package/dist/bench/s1/arms.d.ts +2 -2
  7. package/dist/bench/s1/arms.js +20 -15
  8. package/dist/bench/s1/live-deps.js +3 -1
  9. package/dist/bench/s1/run-firm.js +2 -2
  10. package/dist/degenerate-instrument.d.ts +0 -33
  11. package/dist/degenerate-instrument.js +2 -1
  12. package/dist/fleet/fleet-bus.d.ts +9 -4
  13. package/dist/fleet/fleet-bus.js +2 -1
  14. package/dist/host-decision.d.ts +39 -0
  15. package/dist/host-decision.js +6 -0
  16. package/dist/http/route-ctx.d.ts +8 -5
  17. package/dist/http/routes/a2a-serve.js +4 -2
  18. package/dist/http/routes/approvals-assistant.d.ts +0 -13
  19. package/dist/http/routes/approvals-assistant.js +2 -1
  20. package/dist/http/routes/runs.js +3 -2
  21. package/dist/http/routes/tasks.js +33 -29
  22. package/dist/http/routes/workflows.d.ts +0 -11
  23. package/dist/http/routes/workflows.js +18 -14
  24. package/dist/http/server.js +50 -46
  25. package/dist/leader/fanout.d.ts +6 -23
  26. package/dist/leader/fanout.js +8 -19
  27. package/dist/leader/leader.d.ts +13 -10
  28. package/dist/leader/leader.js +30 -22
  29. package/dist/leader/repair-wire.d.ts +4 -1
  30. package/dist/leader/repair-wire.js +7 -5
  31. package/dist/leader/wire.js +5 -4
  32. package/dist/main.js +5 -3
  33. package/dist/observability/fail-open.d.ts +8 -0
  34. package/dist/observability/fail-open.js +8 -0
  35. package/dist/observability/metrics.js +1 -1
  36. package/dist/observability/run-terminal-log.d.ts +25 -7
  37. package/dist/observability/run-terminal-log.js +15 -4
  38. package/dist/observability/tool-trace.d.ts +13 -12
  39. package/dist/observability/tool-trace.js +8 -13
  40. package/dist/parked-decide.d.ts +8 -4
  41. package/dist/parked-decide.js +1 -1
  42. package/dist/plugins/file-run-store.js +12 -21
  43. package/dist/plugins/memory-run-store.js +3 -2
  44. package/dist/plugins/run-store-sql.js +3 -2
  45. package/dist/run-local.js +6 -4
  46. package/dist/runs.d.ts +4 -19
  47. package/dist/runs.js +49 -47
  48. package/dist/terminal.d.ts +100 -0
  49. package/dist/terminal.js +55 -0
  50. package/dist/trace/core-keyset-guard.d.ts +20 -4
  51. package/dist/trace/ledger-sink.d.ts +2 -1
  52. package/dist/trace/ledger-sink.js +14 -7
  53. package/dist/trace/project.d.ts +9 -12
  54. package/dist/trace/project.js +54 -9
  55. package/package.json +4 -4
@@ -15,7 +15,8 @@
15
15
  * 本文件对 server.ts 的引用一律 `import type`(tsc 擦除,非装载边)。
16
16
  */
17
17
  import type { IncomingMessage, ServerResponse } from "node:http";
18
- import type { ApprovalSettledBy, TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, Runner } from "@sema-agent/core";
18
+ import type { HostDecision } from "../host-decision.js";
19
+ import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, Runner } from "@sema-agent/core";
19
20
  import type { FlatServiceDeps, RequestAuth } from "./server.js";
20
21
  import type { TaskRequestBody, DecideBinding } from "./wire-types.js";
21
22
  import type { IdempotencyCache } from "./idempotency.js";
@@ -244,10 +245,12 @@ export interface RouteLegs {
244
245
  /** 同步腿的终局记账(计费/配额/指标),tasks 域用。 */
245
246
  finalizeTaskResult(result: TaskResult, principal: string | undefined, objective: string, sessionId: string | undefined): void;
246
247
  /** approvals 决策腿:session → pending checkpoint → markResuming CAS → 驱动续跑。
247
- * `settledBy` = **这次结算的出处**(core `ApprovalSettledBy`),必填:本腿有两个调用方(HTTP
248
- * `/decide` = 人;内部 D-D SLA deny-sweep = 窗到期),而函数内部分辨不出谁在叫它 —— 必填参数把
249
- * 「漏报出处」变成编译错,不是一条下游把「没人答」渲染成「有人拒」的静默假出处。 */
250
- resumeCheckpoint(sessionId: string, decision: "approve" | "deny", reason: string | undefined, settledBy: ApprovalSettledBy, req?: IncomingMessage, answer?: QuestionAnswer, binding?: DecideBinding,
248
+ * `hostDecision` = **这次结算的事实**(谁结束了这次等待 + 通道自报的归属),必填:本腿有两个调用方
249
+ * (HTTP `/decide` = 人;内部 D-D SLA deny-sweep = 窗到期),而函数内部分辨不出谁在叫它 —— 必填参数把
250
+ * 「漏报出处」变成编译错,不是一条下游把「没人答」渲染成「有人拒」的静默假出处。
251
+ * 🔴 S-136(core 7.6.0 S6-A):宿主报事实、core 铸词 —— server 不再传结算词(旧形传 core 的三词之一,
252
+ * 于是「哪个词配哪个 decision」这条相容规则要 server 自己记,而记错的方向恰是上面那句要防的)。 */
253
+ resumeCheckpoint(sessionId: string, decision: "approve" | "deny", reason: string | undefined, hostDecision: HostDecision, req?: IncomingMessage, answer?: QuestionAnswer, binding?: DecideBinding,
251
254
  /** [ref] codex R4(验真后修):remember 豁免的 grant 挂点。**工具名不再由路由自己读**(那是与本函数
252
255
  * 载入行分歧的第三次 scope-less 读,三读分歧下会给他人工具落豁免=提权)——由本函数把**已属主校验的
253
256
  * 载入 cp** 的工具名(`validatedToolName`,非 tool_approval / AskUserQuestion / legacy null ⇒ `null`
@@ -1,3 +1,4 @@
1
+ import { persistedPlaneOf } from "../../terminal.js";
1
2
  import { createHash } from "node:crypto";
2
3
  import { memoRedactedRead, redactErrorMessage } from "../../observability/run-terminal-log.js";
3
4
  import { A2A_TASK_STATES } from "@sema-agent/core";
@@ -84,8 +85,9 @@ function statusTextForRun(run, state) {
84
85
  const memo = (seat, raw) => (isTerminalRunStatus(run.status) ? memoRedactedRead(`a2a.${seat}`, raw, (v) => redactErrorMessage(v) ?? "") : (redactErrorMessage(raw) ?? ""));
85
86
  if (run.error)
86
87
  return memo("error", run.error);
87
- if (run.result?.errorMessage)
88
- return memo("errorMessage", run.result.errorMessage);
88
+ const runPlaneMessage = persistedPlaneOf(run.result).errorMessage;
89
+ if (runPlaneMessage)
90
+ return memo("errorMessage", runPlaneMessage);
89
91
  return undefined;
90
92
  }
91
93
  function taskFromRun(run, stale) {
@@ -1,16 +1,3 @@
1
- /**
2
- * [ref] A9 域模块 —— 人在环审批面(durable F4 + legacy)与 assistant-scheduler 三动词。
3
- *
4
- * 路由(前缀 `/v1/approvals` 与 `/v1/assistant`):
5
- * · durable 腿(接了 checkpointStore 时优先):list / stream(SSE 原生推送)/ decide|answer / preempt /
6
- * resume / plan_review;
7
- * · legacy 腿(approvalStore 轮询车道)已于 5.0.0 整体退役([ref]/[ref] A-5)——无 checkpointStore 时
8
- * `/v1/approvals` 落 404(诚实缺席),不再有 trusted-header 决裁面。
9
- *
10
- * 所有会**烧模型**的续跑都经 `ctx.legs.*`(resumeCheckpoint / resumePreempted / resumePlanReview)走 resume
11
- * 家族的同一条腿:lease admission、markResuming CAS、写 run log、发 fleet 帧都在腿里,本域只做入站鉴权/形状门。
12
- * 本域零可变状态(抢占登记簿 `preemptableRuns` 属跨域 registry)。
13
- */
14
1
  import type { IncomingMessage, ServerResponse } from "node:http";
15
2
  import type { QuestionAnswer } from "@sema-agent/core";
16
3
  import type { CheckpointStoreFull } from "../../plugins/store-backend.js";
@@ -1,3 +1,4 @@
1
+ import { personDecision } from "../../host-decision.js";
1
2
  import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugins/checkpoint-store-sql.js";
2
3
  import { APPROVAL_GATE_KINDS } from "../../tool-approval.js";
3
4
  import { isParkedRunStatus } from "../../plugins/store-contracts.js";
@@ -483,7 +484,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
483
484
  pendingGrant = { sessionId: grant.overrideSessionId ?? sessionId, toolName: canonical };
484
485
  }
485
486
  : undefined;
486
- const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, "human", req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
487
+ const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, personDecision(deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
487
488
  const outErrorCode = out.body.errorCode;
488
489
  if (pendingGrant !== undefined && out.status === 200 && outErrorCode !== "cancelled" && deps.approvalExemptionStore) {
489
490
  try {
@@ -1,3 +1,4 @@
1
+ import { persistedPlaneOf } from "../../terminal.js";
1
2
  import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventData, redactTerminalResult } from "../../observability/run-terminal-log.js";
2
3
  import { z } from "zod";
3
4
  import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer } from "@sema-agent/core";
@@ -260,7 +261,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
260
261
  result: isTerminalRunStatus(run.status) ? memoRedactTerminalResult("run.result", run.result ?? undefined) : redactTerminalResult(run.result ?? undefined),
261
262
  supervisorCost,
262
263
  suggestions,
263
- errorCode: stale ? undefined : (run.result?.errorCode ?? undefined),
264
+ errorCode: stale ? undefined : (persistedPlaneOf(run.result).errorCode ?? undefined),
264
265
  error: stale ? "run stalled (instance lost?)" : isTerminalRunStatus(run.status) ? memoRedactedRead("run.error", run.error ?? undefined, redactErrorMessage) : redactErrorMessage(run.error ?? undefined),
265
266
  jobId: run.jobId ?? undefined,
266
267
  source: run.source ?? undefined,
@@ -331,7 +332,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
331
332
  const err = note;
332
333
  try {
333
334
  const cancelContext = buildCancelContext(taskId, Date.parse(run.createdAt));
334
- const result = { taskId, sessionId: run.sessionId, status: "failed", errorCode: "cancelled", errorMessage: err, stats: { turns: 0, tokens: 0 }, ...(cancelContext ? { cancelContext } : {}) };
335
+ const result = { taskId, sessionId: run.sessionId, terminal: { kind: "failed", code: "cancelled", message: err }, stats: { turns: 0, tokens: 0 }, ...(cancelContext ? { cancelContext } : {}) };
335
336
  let lastErr;
336
337
  for (let attempt = 0;; attempt++) {
337
338
  try {
@@ -1,6 +1,7 @@
1
1
  import { redactErrorMessage, redactHead, redactTerminalResult } from "../../observability/run-terminal-log.js";
2
- import { runWithVerification, runCascade, uuidv7 } from "@sema-agent/core";
3
- import { graftParkToolCallId, markChildrenStoppedByUserOnAbort, resumeAtHttpStatus, stripCheckpointToken, HEARTBEAT_MS } from "../../runs.js";
2
+ import { runWithVerification, runCascade, uuidv7, terminalProjection } from "@sema-agent/core";
3
+ import { graftParkToolCallId, markChildrenStoppedByUserOnAbort, resumeAtHttpStatus, HEARTBEAT_MS } from "../../runs.js";
4
+ import { stripResumeToken } from "../../terminal.js";
4
5
  import { withPrincipal } from "../../observability/principal-context.js";
5
6
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../../fleet/fleet-bus.js";
6
7
  import { defaultSubagentTailBus, projectTailFrame } from "../../fleet/subagent-tail-bus.js";
@@ -50,11 +51,12 @@ async function handleTasksBody(req, res, url, ctx, miss) {
50
51
  const reqState = ctx.req;
51
52
  const source = ctx.req.source;
52
53
  const noteRunOutcome = (r) => {
53
- if (!r?.status)
54
+ if (!r?.terminal)
54
55
  return;
55
- reqState.runStatus = r.status;
56
- if (r.errorCode)
57
- reqState.runErrorCode = r.errorCode;
56
+ const plane = terminalProjection(r.terminal);
57
+ reqState.runStatus = plane.status;
58
+ if (plane.errorCode)
59
+ reqState.runErrorCode = plane.errorCode;
58
60
  };
59
61
  if (req.method === "POST" && (url === "/v1/tasks" || url === "/v1/tasks/stream")) {
60
62
  const rawIdem = headerStr(req.headers["idempotency-key"]);
@@ -193,10 +195,8 @@ async function handleTasksBody(req, res, url, ctx, miss) {
193
195
  return {
194
196
  taskId: durableTaskId ?? "",
195
197
  sessionId: prepared.spec.sessionId ?? "",
196
- status: "failed",
198
+ terminal: { kind: "failed", code: "cancelled", message: errorMessage },
197
199
  result: "",
198
- errorCode: "cancelled",
199
- errorMessage,
200
200
  stats: { turns: 0, tokens: 0 },
201
201
  ...(ctx ? { cancelContext: ctx } : {}),
202
202
  };
@@ -403,16 +403,16 @@ async function handleTasksBody(req, res, url, ctx, miss) {
403
403
  if (ev.type === "message_committed" && ev.role === "user")
404
404
  userMsgEntryId = ev.entryId;
405
405
  if (ev.type === "done") {
406
- finalResult = redactTerminalResult(stripCheckpointToken(await graftParkToolCallId(ev.result, deps.checkpointStore)));
406
+ finalResult = redactTerminalResult(stripResumeToken(await graftParkToolCallId(ev.result, deps.checkpointStore)));
407
407
  if (durableTaskId && finalResult)
408
408
  finalResult = { ...finalResult, taskId: durableTaskId };
409
- if (durableTaskId && cancelledViaVerb.has(durableTaskId) && finalResult?.status === "failed" && !finalResult.errorCode) {
410
- finalResult = { ...finalResult, errorCode: "cancelled" };
409
+ if (durableTaskId && cancelledViaVerb.has(durableTaskId) && finalResult?.terminal.kind === "failed" && !finalResult.terminal.code) {
410
+ finalResult = { ...finalResult, terminal: { ...finalResult.terminal, code: "cancelled" } };
411
411
  }
412
412
  finalizeTaskResult(ev.result, principal, prepared.spec.objective, prepared.spec.sessionId);
413
413
  noteRunOutcome(finalResult);
414
414
  putRewindAnchor();
415
- settleFleet(finalResult?.status ?? "completed", fleetRunResiduals(finalResult));
415
+ settleFleet(finalResult ? terminalProjection(finalResult.terminal).status : "completed", fleetRunResiduals(finalResult));
416
416
  if (ledgerSink && finalResult) {
417
417
  if (usageKey && deps.modelUsage && deps.runStore && durableTaskId) {
418
418
  const rs2 = deps.runStore;
@@ -622,12 +622,13 @@ async function handleTasksBody(req, res, url, ctx, miss) {
622
622
  }, withApproval)
623
623
  : withApproval());
624
624
  if (durableTaskId && deps.runStore) {
625
- if (finalResult?.status === "suspended" && deps.checkpointStore)
625
+ const finalPlane = finalResult ? terminalProjection(finalResult.terminal) : undefined;
626
+ if (finalPlane?.status === "suspended" && deps.checkpointStore)
626
627
  await deps.runStore.setSuspended(durableTaskId);
627
- else if (finalResult?.status === "needs_review" && deps.checkpointStore)
628
+ else if (finalPlane?.status === "needs_review" && deps.checkpointStore)
628
629
  await deps.runStore.setNeedsReview(durableTaskId);
629
- else if (finalResult)
630
- await deps.runStore.setTerminal(durableTaskId, finalResult.status, finalResult, finalResult.errorMessage ?? null);
630
+ else if (finalResult && finalPlane)
631
+ await deps.runStore.setTerminal(durableTaskId, finalPlane.status, finalResult, finalPlane.errorMessage ?? null);
631
632
  else {
632
633
  putRewindAnchor();
633
634
  const pending = await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId);
@@ -688,8 +689,9 @@ async function handleTasksBody(req, res, url, ctx, miss) {
688
689
  ledgerTerminal = true;
689
690
  await ledgerSink.onDone(finalResult).catch(() => undefined);
690
691
  }
691
- await deps.runStore.setTerminal(durableTaskId, finalResult.status, finalResult, finalResult.errorMessage ?? null);
692
- settleFleet(finalResult.status, fleetRunResiduals(finalResult));
692
+ const retryPlane = terminalProjection(finalResult.terminal);
693
+ await deps.runStore.setTerminal(durableTaskId, retryPlane.status, finalResult, retryPlane.errorMessage ?? null);
694
+ settleFleet(retryPlane.status, fleetRunResiduals(finalResult));
693
695
  }
694
696
  else if (ac.signal.aborted) {
695
697
  const err = "stream aborted before completion (run cancelled)";
@@ -721,7 +723,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
721
723
  };
722
724
  counters.uncountedBillableInflight++;
723
725
  try {
724
- resp = await idemCache.run(idemKey, runStreamSubmitLeg, (r) => r.body?.status === "completed");
726
+ resp = await idemCache.run(idemKey, runStreamSubmitLeg, (r) => r.body?.terminal.kind === "completed");
725
727
  }
726
728
  catch (e) {
727
729
  deps.logger?.error("stream_error", { closed, err: redactHead(e instanceof Error ? e.message : String(e), 2000) });
@@ -805,33 +807,35 @@ async function handleTasksBody(req, res, url, ctx, miss) {
805
807
  cancelledViaVerb.delete(durableTaskId);
806
808
  }
807
809
  }
808
- if (cancelVerbLabel && result.status === "failed" && !result.errorCode) {
809
- result = { ...result, errorCode: "cancelled" };
810
+ if (cancelVerbLabel && result.terminal.kind === "failed" && !result.terminal.code) {
811
+ result = { ...result, terminal: { ...result.terminal, code: "cancelled" } };
810
812
  }
811
813
  const resumeAtStatus = resumeAtHttpStatus(result);
812
814
  if (resumeAtStatus) {
815
+ const resumeAtPlane = terminalProjection(result.terminal);
813
816
  if (durableTaskId && deps.runStore)
814
- await deps.runStore.setTerminal(durableTaskId, "failed", stripCheckpointToken(result), result.errorMessage ?? null);
815
- const retryAfterSec = result.errorCode === "memory.admission_required" && result.retryAfterMs !== undefined
817
+ await deps.runStore.setTerminal(durableTaskId, "failed", stripResumeToken(result), resumeAtPlane.errorMessage ?? null);
818
+ const retryAfterSec = resumeAtPlane.errorCode === "memory.admission_required" && result.retryAfterMs !== undefined
816
819
  ? Math.max(1, Math.ceil(result.retryAfterMs / 1000))
817
820
  : undefined;
818
- return { status: resumeAtStatus, body: { errorCode: result.errorCode, error: redactErrorMessage(result.errorMessage) ?? "resume-at failed", ...(retryAfterSec !== undefined ? { retryAfterSec } : {}) } };
821
+ return { status: resumeAtStatus, body: { errorCode: resumeAtPlane.errorCode, error: redactErrorMessage(resumeAtPlane.errorMessage) ?? "resume-at failed", ...(retryAfterSec !== undefined ? { retryAfterSec } : {}) } };
819
822
  }
820
- if (result.status === "suspended" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
823
+ const syncPlane = terminalProjection(result.terminal);
824
+ if (syncPlane.status === "suspended" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
821
825
  if (durableTaskId)
822
826
  await deps.runStore.setSuspended(durableTaskId);
823
827
  return { status: 200, body: withDiscoveryNotice({ taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "suspended" }) };
824
828
  }
825
- if (result.status === "needs_review" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
829
+ if (syncPlane.status === "needs_review" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
826
830
  if (durableTaskId)
827
831
  await deps.runStore.setNeedsReview(durableTaskId);
828
832
  return { status: 200, body: withDiscoveryNotice({ taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "needs_review" }) };
829
833
  }
830
834
  if (durableTaskId && deps.runStore)
831
- await deps.runStore.setTerminal(durableTaskId, result.status, stripCheckpointToken(result), result.errorMessage ?? null);
835
+ await deps.runStore.setTerminal(durableTaskId, syncPlane.status, stripResumeToken(result), syncPlane.errorMessage ?? null);
832
836
  finalizeTaskResult(result, principal, prepared.spec.objective, prepared.spec.sessionId);
833
837
  noteRunOutcome(result);
834
- return { status: 200, body: withDiscoveryNotice(redactTerminalResult(stripCheckpointToken(result))) };
838
+ return { status: 200, body: withDiscoveryNotice(redactTerminalResult(stripResumeToken(result))) };
835
839
  };
836
840
  const resp = await idemCache.run(idemKey, runSyncSubmitLeg, (r) => r.status < 400);
837
841
  sendSubmitResult(res, resp);
@@ -1,14 +1,3 @@
1
- /**
2
- * [ref] A9 域模块 —— S8 自编排 workflow 面([ref]/98)。
3
- *
4
- * 路由:`GET /v1/workflows`(列表)· `GET /v1/workflows/:id[/journal]`(详情)· `GET /v1/workflows/:id/stream`
5
- * (在进程内 live SSE)· `POST /v1/workflows/:id/agents/:label/steer`(SVC-5:给 workflow 里某个仍在跑的
6
- * agent 注入引导)。
7
- *
8
- * ⚠️ 只读三路跑在全局 service-credential 门**之前**(与 trace/fleet 同列),因此块内那道按 scope 的 per-tenant
9
- * 门就是这些读的唯一租户边界;steer 那路在门之后。两段在 `handle()` 里原本相隔很远(中间隔着 tasks/runs 两域),
10
- * 拆分保持了各自的相对位置 —— 本文件里的先后只是同域归并,不代表路由顺序变化。本域零可变状态。
11
- */
12
1
  import type { IncomingMessage, ServerResponse } from "node:http";
13
2
  import type { RouteCtx } from "../route-ctx.js";
14
3
  export declare function handleWorkflows(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
@@ -1,3 +1,4 @@
1
+ import { persistedPlaneOf } from "../../terminal.js";
1
2
  import { z } from "zod";
2
3
  import { getWorkflowRun, subscribeWorkflow, deriveAgentDisplayStatus, callKeyOrdinal } from "@sema-agent/core";
3
4
  import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from "../../orchestration/workflow-agent-steer.js";
@@ -85,20 +86,23 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
85
86
  journalProjectionMemo.set(memoKey, out);
86
87
  return out;
87
88
  };
88
- const projectResultUncached = (callKey, r) => ({
89
- callKey,
90
- ordinal: callKeyOrdinal(callKey),
91
- status: r.status,
92
- ...(r.errorMessage ? { error: firstLine(r.errorMessage) } : {}),
93
- ...(r.result
94
- ? {
95
- result: redactHead(r.result, MAX_RESULT_CHARS),
96
- ...(r.result.length > MAX_RESULT_CHARS ? { resultTruncated: true, resultChars: r.result.length } : {}),
97
- }
98
- : {}),
99
- ...(r.stats ? { tokens: r.stats.tokens, turns: r.stats.turns } : {}),
100
- ...(r.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}),
101
- });
89
+ const projectResultUncached = (callKey, r) => {
90
+ const plane = persistedPlaneOf(r);
91
+ return {
92
+ callKey,
93
+ ordinal: callKeyOrdinal(callKey),
94
+ status: plane.status,
95
+ ...(plane.errorMessage ? { error: firstLine(plane.errorMessage) } : {}),
96
+ ...(r.result
97
+ ? {
98
+ result: redactHead(r.result, MAX_RESULT_CHARS),
99
+ ...(r.result.length > MAX_RESULT_CHARS ? { resultTruncated: true, resultChars: r.result.length } : {}),
100
+ }
101
+ : {}),
102
+ ...(r.stats ? { tokens: r.stats.tokens, turns: r.stats.turns } : {}),
103
+ ...(r.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}),
104
+ };
105
+ };
102
106
  const js = deps.workflowJournalStore;
103
107
  let projected;
104
108
  if (js.loadPage) {
@@ -2,7 +2,7 @@ import http from "node:http";
2
2
  import { redactErrorMessage, redactHead } from "../observability/run-terminal-log.js";
3
3
  import { once } from "node:events";
4
4
  import { createHash } from "node:crypto";
5
- import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs, RUN_WORKFLOW_TOOL_NAME } from "@sema-agent/core";
5
+ import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs, RUN_WORKFLOW_TOOL_NAME, terminalProjection, PAUSE_REGISTRY } from "@sema-agent/core";
6
6
  import { decideParkedAgent, findParkedAgentForCheckpoint } from "../parked-decide.js";
7
7
  import { isModelAllowlisted, matchCatalogModel } from "../model-select.js";
8
8
  import {} from "../config-center/facade.js";
@@ -15,7 +15,9 @@ import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
15
15
  import { parseHooksConfig } from "../hooks/hook-runner.js";
16
16
  import { normalizeApproachNotice, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
17
17
  import { isValidCwd, MAX_ADDITIONAL_DIRS } from "../task-cwd.js";
18
- import { runInBackground, evictIfConflict, stripCheckpointToken, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
18
+ import { runInBackground, evictIfConflict, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
19
+ import { failureCodeOf, nestedPauseOf, stripResumeToken } from "../terminal.js";
20
+ import { personDecision } from "../host-decision.js";
19
21
  import { readTurnActivityMs, recordTurnActivity } from "../turn-activity.js";
20
22
  import { recordResumeResourceWindow } from "../resource-window.js";
21
23
  import { publicStoreProbeError } from "../store-live-probe.js";
@@ -122,17 +124,14 @@ function mentionableNames(config, catalog) {
122
124
  return Object.keys(catalog).filter((n) => n !== "default" && isModelAllowlisted(n, catalog, allowlist));
123
125
  }
124
126
  function isCheckpointReopenedFailure(r) {
125
- return r.status === "failed" && (r.errorCode === "resume.env_failed" || r.errorCode === "resume.tool_unavailable");
127
+ const code = failureCodeOf(r.terminal);
128
+ return code === "resume.env_failed" || code === "resume.tool_unavailable";
126
129
  }
127
130
  function nestedParkStatusOf(r) {
128
- switch (r.errorCode) {
129
- case "unexpected.needs_review":
130
- return "needs_review";
131
- case "unexpected.suspended":
132
- return "suspended";
133
- default:
134
- return undefined;
135
- }
131
+ const paused = nestedPauseOf(r.terminal);
132
+ if (paused === undefined)
133
+ return undefined;
134
+ return PAUSE_REGISTRY[paused.gate.kind].taskStatus;
136
135
  }
137
136
  const ROUTE_DOMAINS = [
138
137
  handleTraceUsage,
@@ -1014,7 +1013,7 @@ export function createHttpServer(rawDeps) {
1014
1013
  }
1015
1014
  }
1016
1015
  const resumeFleetScope = (req, auth) => req ? (gatedPrincipal(req, deps.config) ?? "default") : (auth.principal ?? "default");
1017
- async function resumeCheckpoint(sessionId, decision, reason, settledBy, req, answer, binding, onResumeCommitted, acceptEarly, decider) {
1016
+ async function resumeCheckpoint(sessionId, decision, reason, hostDecision, req, answer, binding, onResumeCommitted, acceptEarly, decider) {
1018
1017
  const cs = deps.checkpointStore;
1019
1018
  const token = (await cs.findPendingTokenBySession(sessionId, undefined, { gateKinds: APPROVAL_GATE_KINDS })) ?? (await cs.findPendingTokenBySession(sessionId));
1020
1019
  if (binding?.checkpointToken && binding.checkpointToken !== token) {
@@ -1162,7 +1161,7 @@ export function createHttpServer(rawDeps) {
1162
1161
  if (out.body?.errorCode === "cancelled")
1163
1162
  return;
1164
1163
  try {
1165
- lateSettleSink.recordLateSettle({ ...lateSettleAnchor, outcome: decision === "approve" ? "allowed" : "denied", settledBy, settledAtMs: Date.now() });
1164
+ lateSettleSink.recordLateSettle({ ...lateSettleAnchor, outcome: decision === "approve" ? "allowed" : "denied", decidedBy: hostDecision.decidedBy, ...(hostDecision.approver !== undefined ? { approver: hostDecision.approver } : {}), settledAtMs: Date.now() });
1166
1165
  }
1167
1166
  catch (e) {
1168
1167
  recordFailOpen("server.approval-ask-audit.append-failed", `kind=late_settle err=${e instanceof Error ? e.message : String(e)}`);
@@ -1188,7 +1187,7 @@ export function createHttpServer(rawDeps) {
1188
1187
  ...(deps.logger ? { warn: (event, fields) => deps.logger?.warn?.(event, fields) } : {}),
1189
1188
  }, {
1190
1189
  token, scope: cp.scope, sessionId: cp.sessionId, pendingAction: cp.pendingAction,
1191
- decision, settledBy, ...(reason !== undefined ? { reason } : {}),
1190
+ decision, hostDecision, ...(reason !== undefined ? { reason } : {}),
1192
1191
  ...(binding !== undefined ? { binding: { ...(binding.boundCallId !== undefined ? { boundCallId: binding.boundCallId } : {}), ...(binding.boundInputHash !== undefined ? { boundInputHash: binding.boundInputHash } : {}), ...(binding.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}) } } : {}),
1193
1192
  ...(boundGrant ? { grantRemember: (rootSessionId) => boundGrant(rootSessionId) } : {}),
1194
1193
  ...(answer !== undefined ? { answer } : {}),
@@ -1257,7 +1256,7 @@ export function createHttpServer(rawDeps) {
1257
1256
  ...(decision === "approve" && binding?.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}),
1258
1257
  ...(reason ? { reason } : {}),
1259
1258
  ...(answer !== undefined ? { answer } : {}),
1260
- settledBy,
1259
+ hostDecision,
1261
1260
  };
1262
1261
  const verifyRounds = verifyRoundsFromBody(ctx.body);
1263
1262
  const driven = await driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(boundGrant ? { onResumeCommitted: boundGrant } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}), ...(ownerLeaseAdmittedHere ? { ownerLeaseAdmitted: true } : {}) });
@@ -1477,9 +1476,10 @@ export function createHttpServer(rawDeps) {
1477
1476
  };
1478
1477
  resumeLegLive = true;
1479
1478
  const vr = await resumeWithVerification(legRunner, token, outcome, { ...resumeTaskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, resumeObjective, verifyRounds, verifyInternals);
1480
- let safe = stripCheckpointToken(vr);
1479
+ let safe = stripResumeToken(vr);
1480
+ const vrNested = nestedPauseOf(vr.terminal);
1481
1481
  const reParkStatus = nestedParkStatusOf(vr);
1482
- const reSuspended = reParkStatus !== undefined;
1482
+ const reSuspended = vrNested !== undefined;
1483
1483
  const vrReopened = isCheckpointReopenedFailure(vr) && (await confirmCheckpointReopened());
1484
1484
  if (vrReopened)
1485
1485
  reopenConfirmedPark = true;
@@ -1490,8 +1490,8 @@ export function createHttpServer(rawDeps) {
1490
1490
  await appendPromptManifest(ap, deps.promptManifests, taskId);
1491
1491
  if (reSuspended || vrReopened) {
1492
1492
  await appendModelUsageDelta(ap, deps.modelUsage, taskId);
1493
- const vrParkCallId = reSuspended ? await parkToolCallId({ ...vr, status: reParkStatus }, deps.checkpointStore) : undefined;
1494
- await ap(reSuspended ? reParkStatus : "suspended", { gate: vr.checkpointGate ?? null, ...(vrParkCallId !== undefined ? { toolCallId: vrParkCallId } : {}), ...(vrReopened ? { reopened: vr.errorCode ?? null } : {}) });
1493
+ const vrParkCallId = vrNested !== undefined ? await parkToolCallId({ terminal: vrNested }, deps.checkpointStore) : undefined;
1494
+ await ap(reParkStatus ?? "suspended", { gate: vrNested?.gate ?? null, ...(vrParkCallId !== undefined ? { toolCallId: vrParkCallId } : {}), ...(vrReopened ? { reopened: failureCodeOf(vr.terminal) ?? null } : {}) });
1495
1495
  }
1496
1496
  else {
1497
1497
  safe = await attachModelUsage(safe, { append: ap, getEvents: (id, a) => rs2.getEvents(id, a), modelUsage: deps.modelUsage, taskId });
@@ -1503,8 +1503,8 @@ export function createHttpServer(rawDeps) {
1503
1503
  throw e;
1504
1504
  }
1505
1505
  }
1506
- if (reSuspended)
1507
- return { ...safe, status: reParkStatus };
1506
+ if (vrNested !== undefined)
1507
+ return { ...safe, terminal: vrNested };
1508
1508
  if (vr.verification?.verdict)
1509
1509
  deps.metrics?.inc("verifications_total", { verdict: vr.verification.verdict });
1510
1510
  return safe;
@@ -1699,30 +1699,31 @@ export function createHttpServer(rawDeps) {
1699
1699
  }
1700
1700
  await flush();
1701
1701
  const r = await stream.result();
1702
- if (r.status === "suspended") {
1702
+ const rPark = r.terminal.kind === "paused" ? terminalProjection(r.terminal).status : undefined;
1703
+ if (rPark === "suspended") {
1703
1704
  const parkCallId = await parkToolCallId(r, deps.checkpointStore);
1704
- await append("suspended", { gate: r.checkpointGate ?? null, ...(parkCallId !== undefined ? { toolCallId: parkCallId } : {}) });
1705
+ await append("suspended", { gate: r.terminal.kind === "paused" ? r.terminal.gate : null, ...(parkCallId !== undefined ? { toolCallId: parkCallId } : {}) });
1705
1706
  await appendModelUsageDelta(append, deps.modelUsage, taskId);
1706
1707
  return r;
1707
1708
  }
1708
- if (r.status === "needs_review") {
1709
- await append("needs_review", { gate: r.checkpointGate ?? null });
1709
+ if (rPark === "needs_review") {
1710
+ await append("needs_review", { gate: r.terminal.kind === "paused" ? r.terminal.gate : null });
1710
1711
  await appendModelUsageDelta(append, deps.modelUsage, taskId);
1711
1712
  return r;
1712
1713
  }
1713
1714
  if (isCheckpointReopenedFailure(r) && (await confirmCheckpointReopened())) {
1714
1715
  reopenConfirmedPark = true;
1715
1716
  try {
1716
- await append("suspended", { gate: null, reopened: r.errorCode ?? null });
1717
+ await append("suspended", { gate: null, reopened: failureCodeOf(r.terminal) ?? null });
1717
1718
  await appendModelUsageDelta(append, deps.modelUsage, taskId);
1718
1719
  }
1719
1720
  catch {
1720
1721
  }
1721
1722
  return r;
1722
1723
  }
1723
- const safe = await attachModelUsage(stripCheckpointToken(r), { append, getEvents: (id, a) => rs.getEvents(id, a), modelUsage: deps.modelUsage, taskId });
1724
+ const safe = await attachModelUsage(stripResumeToken(r), { append, getEvents: (id, a) => rs.getEvents(id, a), modelUsage: deps.modelUsage, taskId });
1724
1725
  await append("done", { result: safe });
1725
- if (taskConfig.suggestNextPrompts && safe.status === "completed") {
1726
+ if (taskConfig.suggestNextPrompts && safe.terminal.kind === "completed") {
1726
1727
  const suggestions = await stream.suggestions().catch(() => []);
1727
1728
  if (suggestions.length)
1728
1729
  await append("suggestions", { suggestions: suggestions.map((s) => redactSecrets(s)) });
@@ -1772,8 +1773,8 @@ export function createHttpServer(rawDeps) {
1772
1773
  }
1773
1774
  }
1774
1775
  if (claimedRow && taskId && deps.runStore) {
1775
- const c = { taskId, sessionId, status: "failed", errorCode: "cancelled", errorMessage: e.message, stats: { turns: 0, tokens: 0 } };
1776
- await deps.runStore.setTerminal(taskId, "failed", c, c.errorMessage ?? null).catch(() => undefined);
1776
+ const c = { taskId, sessionId, terminal: { kind: "failed", code: "cancelled", message: e.message }, stats: { turns: 0, tokens: 0 } };
1777
+ await deps.runStore.setTerminal(taskId, "failed", c, e.message).catch(() => undefined);
1777
1778
  }
1778
1779
  settleFleet("failed");
1779
1780
  return { status: 200, body: { taskId, sessionId, status: "failed", errorCode: "cancelled" } };
@@ -1793,8 +1794,8 @@ export function createHttpServer(rawDeps) {
1793
1794
  (e.code === "checkpoint.unsupported_version" && checkpointRowRedeemableElsewhere(e.detail?.reason));
1794
1795
  if (claimedRow && taskId && deps.runStore) {
1795
1796
  if (retriable && freshRunClaimed) {
1796
- const c = { taskId, sessionId, status: "failed", errorCode: e.code, errorMessage: e.message, stats: { turns: 0, tokens: 0 } };
1797
- await deps.runStore.setTerminal(taskId, "failed", c, c.errorMessage ?? null).catch((err) => {
1797
+ const c = { taskId, sessionId, terminal: { kind: "failed", code: e.code, message: e.message }, stats: { turns: 0, tokens: 0 } };
1798
+ await deps.runStore.setTerminal(taskId, "failed", c, e.message).catch((err) => {
1798
1799
  deps.logger?.warn?.("wake_synthetic_run_release_failed", { taskId, sessionId, errorCode: e.code, error: err instanceof Error ? err.message : String(err), note: "the synthetic run row for this refused fresh-run resume could not be settled — its task_active claim is still held and the next attempt on this session will 409 conflict.session_active_run until reapStale collects it (runStaleSec)" });
1799
1800
  });
1800
1801
  settleFleet("failed");
@@ -1827,8 +1828,8 @@ export function createHttpServer(rawDeps) {
1827
1828
  }
1828
1829
  if (cancelCtrl.signal.aborted) {
1829
1830
  if (taskId && deps.runStore) {
1830
- const c = { taskId, sessionId, status: "failed", errorCode: "cancelled", errorMessage: "cancelled by user", stats: { turns: 0, tokens: 0 } };
1831
- await deps.runStore.setTerminal(taskId, "failed", c, c.errorMessage ?? null).catch(() => undefined);
1831
+ const c = { taskId, sessionId, terminal: { kind: "failed", code: "cancelled", message: "cancelled by user" }, stats: { turns: 0, tokens: 0 } };
1832
+ await deps.runStore.setTerminal(taskId, "failed", c, "cancelled by user").catch(() => undefined);
1832
1833
  }
1833
1834
  settleFleet("failed");
1834
1835
  return { status: 200, body: { taskId, sessionId, status: "failed", errorCode: "cancelled" } };
@@ -1859,22 +1860,23 @@ export function createHttpServer(rawDeps) {
1859
1860
  }
1860
1861
  evictIfConflict(deps.runner, sessionId, result);
1861
1862
  const checkpointReopened = reopenConfirmedPark;
1863
+ const resumePlane = terminalProjection(result.terminal);
1862
1864
  if (taskId && deps.runStore) {
1863
- if (result.status === "suspended" || checkpointReopened)
1865
+ if (resumePlane.status === "suspended" || checkpointReopened)
1864
1866
  await deps.runStore.setSuspended(taskId);
1865
- else if (result.status === "needs_review")
1867
+ else if (resumePlane.status === "needs_review")
1866
1868
  await deps.runStore.setNeedsReview(taskId);
1867
1869
  else
1868
- await deps.runStore.setTerminal(taskId, result.status, stripCheckpointToken(result), result.errorMessage ?? null);
1870
+ await deps.runStore.setTerminal(taskId, resumePlane.status, stripResumeToken(result), resumePlane.errorMessage ?? null);
1869
1871
  }
1870
- settleFleet(checkpointReopened ? "suspended" : result.status, fleetRunResiduals(result));
1872
+ settleFleet(checkpointReopened ? "suspended" : resumePlane.status, fleetRunResiduals(result));
1871
1873
  return {
1872
1874
  status: 200,
1873
1875
  body: {
1874
- taskId, sessionId, status: result.status,
1876
+ taskId, sessionId, status: resumePlane.status,
1875
1877
  bindingEnforced: true,
1876
- ...(result.errorCode ? { errorCode: result.errorCode } : {}),
1877
- ...(result.errorMessage ? { errorMessage: redactErrorMessage(result.errorMessage) ?? "" } : {}),
1878
+ ...(resumePlane.errorCode ? { errorCode: resumePlane.errorCode } : {}),
1879
+ ...(resumePlane.errorMessage ? { errorMessage: redactErrorMessage(resumePlane.errorMessage) ?? "" } : {}),
1878
1880
  ...(checkpointReopened ? { retriable: true } : {}),
1879
1881
  },
1880
1882
  };
@@ -2091,12 +2093,13 @@ export function createHttpServer(rawDeps) {
2091
2093
  return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
2092
2094
  }
2093
2095
  function recordTaskResult(result) {
2094
- deps.metrics?.inc("tasks_total", { status: result.status });
2096
+ const plane = terminalProjection(result.terminal);
2097
+ deps.metrics?.inc("tasks_total", { status: plane.status });
2095
2098
  if (result.stats.tokens)
2096
2099
  deps.metrics?.inc("task_tokens_total", {}, result.stats.tokens);
2097
2100
  if (result.stats.cacheHitRate !== undefined)
2098
2101
  deps.metrics?.observe("task_cache_hit_rate", result.stats.cacheHitRate);
2099
- const code = result.errorCode;
2102
+ const code = plane.errorCode;
2100
2103
  if (code && code.startsWith("limits.")) {
2101
2104
  deps.metrics?.inc("budget_exceeded_total", { code });
2102
2105
  }
@@ -2110,7 +2113,7 @@ export function createHttpServer(rawDeps) {
2110
2113
  }
2111
2114
  function finalizeTaskResult(result, principal, objective, sessionId) {
2112
2115
  recordTaskResult(result);
2113
- deps.planCacheProbe?.record(principal, objective, result.status === "completed");
2116
+ deps.planCacheProbe?.record(principal, objective, result.terminal.kind === "completed");
2114
2117
  evictIfConflict(deps.runner, sessionId, result);
2115
2118
  }
2116
2119
  function quotaExceeded(req, res) {
@@ -2347,7 +2350,7 @@ export function createHttpServer(rawDeps) {
2347
2350
  parkedSkipped += 1;
2348
2351
  continue;
2349
2352
  }
2350
- await resumeCheckpoint(sessionId, "deny", "approval SLA expired — auto-denied", "timeout").then((r) => { if (r.status >= 400)
2353
+ await resumeCheckpoint(sessionId, "deny", "approval SLA expired — auto-denied", { decidedBy: "sla_timeout" }).then((r) => { if (r.status >= 400)
2351
2354
  deps.logger?.warn?.("deny_sweep_resume_blocked", { sessionId, status: r.status, errorCode: r.body?.errorCode }); }, (e) => deps.logger?.warn?.("deny_sweep_resume_failed", { sessionId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) }));
2352
2355
  }
2353
2356
  if (parkedSkipped > 0)
@@ -2420,7 +2423,7 @@ export function createHttpServer(rawDeps) {
2420
2423
  ...(req.gateBoundInputHash !== null ? { boundInputHash: req.gateBoundInputHash } : {}),
2421
2424
  ...(req.updatedInput !== undefined ? { updatedInput: req.updatedInput } : {}),
2422
2425
  };
2423
- const out = await resumeCheckpoint(cp.sessionId, req.decision, req.note, "human", req.httpReq, undefined, binding, undefined, true, { principal: req.principal, explicitOperator: false });
2426
+ const out = await resumeCheckpoint(cp.sessionId, req.decision, req.note, personDecision(req.principal), req.httpReq, undefined, binding, undefined, true, { principal: req.principal, explicitOperator: false });
2424
2427
  return { status: out.status, body: out.body };
2425
2428
  }
2426
2429
  const isRunLiveOnThisReplica = (taskId) => inflightRuns.has(taskId) || steerableRuns.has(taskId);
@@ -2430,6 +2433,7 @@ const WORKER_SWAP_REDEEMABLE = {
2430
2433
  version_newer: true,
2431
2434
  env_factory_missing: true,
2432
2435
  governed_unwired: true,
2436
+ origin_missing: true,
2433
2437
  real_approval_damaged: false,
2434
2438
  real_approval_forged: false,
2435
2439
  read_face_damaged: false,