@sema-agent/server 7.111.0 → 7.113.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 (76) hide show
  1. package/MIGRATION.md +68 -0
  2. package/USAGE.md +3 -3
  3. package/dist/approval-ask-audit-store.d.ts +3 -4
  4. package/dist/approval-ask-audit-store.js +3 -3
  5. package/dist/approval-card.d.ts +4 -4
  6. package/dist/boot/coordinators.js +1 -1
  7. package/dist/boot/stores.js +4 -6
  8. package/dist/boot/workflow-orchestration.js +4 -4
  9. package/dist/config-types.d.ts +1 -1
  10. package/dist/config.js +2 -1
  11. package/dist/hooks/hook-runner.d.ts +7 -2
  12. package/dist/hooks/hook-runner.js +48 -4
  13. package/dist/host-decision.d.ts +5 -3
  14. package/dist/http/admission.js +12 -3
  15. package/dist/http/request-guards.js +2 -2
  16. package/dist/http/resume-legs.d.ts +14 -24
  17. package/dist/http/resume-legs.js +91 -29
  18. package/dist/http/route-ctx.d.ts +64 -17
  19. package/dist/http/route-ctx.js +4 -0
  20. package/dist/http/routes/approvals-assistant.js +1 -1
  21. package/dist/http/routes/background.js +2 -2
  22. package/dist/http/routes/mcp-reconnect.js +2 -2
  23. package/dist/http/routes/notify-wake.js +3 -3
  24. package/dist/http/routes/runs.js +22 -19
  25. package/dist/http/routes/session-sync.js +6 -6
  26. package/dist/http/routes/sessions.js +20 -20
  27. package/dist/http/routes/tasks.d.ts +3 -2
  28. package/dist/http/routes/tasks.js +38 -45
  29. package/dist/http/send.d.ts +1 -1
  30. package/dist/http/send.js +1 -1
  31. package/dist/http/server.js +1 -2
  32. package/dist/http/wire-types.d.ts +1 -1
  33. package/dist/local-data-root.d.ts +14 -0
  34. package/dist/local-data-root.js +5 -0
  35. package/dist/observability/corrupt-read-seat.d.ts +46 -66
  36. package/dist/observability/corrupt-read-seat.js +74 -59
  37. package/dist/observability/fail-open.d.ts +29 -1
  38. package/dist/observability/fail-open.js +26 -1
  39. package/dist/observability/run-terminal.js +39 -0
  40. package/dist/orchestration/workflow-agent-session-index.d.ts +1 -5
  41. package/dist/orchestration/workflow-agent-session-index.js +2 -2
  42. package/dist/orchestration/workflow-notify-journal.d.ts +1 -3
  43. package/dist/orchestration/workflow-notify-journal.js +2 -2
  44. package/dist/plugins/approval-ask-store-file.d.ts +1 -4
  45. package/dist/plugins/approval-ask-store-file.js +2 -2
  46. package/dist/plugins/file-resume-anchor-store.d.ts +1 -6
  47. package/dist/plugins/file-resume-anchor-store.js +2 -4
  48. package/dist/plugins/file-run-store.d.ts +0 -4
  49. package/dist/plugins/file-run-store.js +1 -3
  50. package/dist/plugins/send-file-ledger.d.ts +1 -5
  51. package/dist/plugins/send-file-ledger.js +2 -2
  52. package/dist/plugins/store-backend.js +8 -9
  53. package/dist/plugins/store-contracts.d.ts +40 -7
  54. package/dist/plugins/store-contracts.js +1 -0
  55. package/dist/plugins/user-input-ledger-file.d.ts +1 -5
  56. package/dist/plugins/user-input-ledger-file.js +2 -2
  57. package/dist/run-cancel-context.d.ts +59 -7
  58. package/dist/run-cancel-context.js +26 -0
  59. package/dist/run-local.d.ts +1 -1
  60. package/dist/run-local.js +2 -2
  61. package/dist/runs.d.ts +7 -5
  62. package/dist/runs.js +18 -13
  63. package/dist/spec-fields.js +0 -1
  64. package/dist/tool-approval.d.ts +1 -1
  65. package/dist/trace/core-keyset-guard.d.ts +1 -1
  66. package/dist/trace/project.js +2 -3
  67. package/dist/trace/redact.d.ts +18 -0
  68. package/dist/trace/redact.js +4 -0
  69. package/dist/trace/self-minted-wire.d.ts +22 -0
  70. package/dist/trace/self-minted-wire.js +8 -0
  71. package/dist/unattended-permission-mode.d.ts +9 -2
  72. package/dist/unattended-permission-mode.js +5 -1
  73. package/dist/user-input-ledger.d.ts +24 -5
  74. package/dist/user-input-ledger.js +46 -11
  75. package/package.json +4 -3
  76. package/scripts/retired-mode-seed-drain-local.mjs +317 -0
@@ -8,7 +8,7 @@ const sessionFaceUnavailable = async (sessionId) => {
8
8
  import { HttpError, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE } from "../security.js";
9
9
  import { evictIfConflict, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
10
10
  import { failureCodeOf, nestedPauseOf, sealResultForExit, outwardPlane } from "../terminal.js";
11
- import { mintCancelledResult } from "../run-cancel-context.js";
11
+ import { buildCancelledLedgerRow, createRunAbort, mintCancelledResult } from "../run-cancel-context.js";
12
12
  import {} from "../host-decision.js";
13
13
  import { readTurnActivityMs, recordTurnActivity } from "../turn-activity.js";
14
14
  import { recordResumeResourceWindow } from "../resource-window.js";
@@ -30,7 +30,7 @@ import { errorText } from "../observability/err-text.js";
30
30
  import { recordFailOpen } from "../observability/fail-open.js";
31
31
  import { verifyRoundsFromBody } from "./verify-rounds.js";
32
32
  import { storedBodyPermissionMode } from "../task-settings.js";
33
- import { acceptedResumingReceipt } from "./route-ctx.js";
33
+ import { acceptedResumingReceipt, buildHostLegOutcome } from "./route-ctx.js";
34
34
  import { mintAndDriveDurableRun } from "./durable-run-leg.js";
35
35
  import { createSessionsLocal } from "./routes/sessions.js";
36
36
  import { createSessionSyncLocal } from "./routes/session-sync.js";
@@ -70,12 +70,68 @@ const WORKER_SWAP_REDEEMABLE = {
70
70
  function checkpointRowRedeemableElsewhere(reason) {
71
71
  return reason !== undefined && WORKER_SWAP_REDEEMABLE[reason] === true;
72
72
  }
73
- const NON_BINDING_INVALID_OUTCOME_FIELDS = new Set(["hostDecision", "approver"]);
73
+ const CHECKPOINT_ERROR_DISPOSITION = {
74
+ "checkpoint.resume_aborted": "cancelled",
75
+ "checkpoint.invalid_outcome": "retriable",
76
+ "checkpoint.reopen_revote": "retriable",
77
+ "checkpoint.reopened_concurrently": "retriable",
78
+ "resume.parent_constraint_missing": "retriable",
79
+ "resume.parent_constraint_mismatch": "retriable",
80
+ "resume.constraint_rejected": "retriable",
81
+ "resume.constraint_unprojectable": "retriable",
82
+ "resume.usage_window_exhausted": "retriable",
83
+ "resume.principal_mismatch": "retriable",
84
+ "resume.placement_mismatch": "retriable",
85
+ "resume.preflight_rejected": "retriable",
86
+ "wake.gate_pending": "retriable",
87
+ "wake.nothing_to_deliver": "retriable",
88
+ "checkpoint.config_refused": "retriable",
89
+ "checkpoint.walltime_axis_retired": "retriable",
90
+ "steering.blocked_by_hook": "retriable",
91
+ "checkpoint.unsupported_version": "by_reason",
92
+ "checkpoint.already_resolved": "terminal",
93
+ "checkpoint.not_found": "terminal",
94
+ "checkpoint.gate_mismatch": "terminal",
95
+ "checkpoint.reopen_failed": "terminal",
96
+ "checkpoint.cwd_conflicts_restore": "terminal",
97
+ "checkpoint.already_exists": "terminal",
98
+ "checkpoint.execution_outcome_conflict": "terminal",
99
+ "steering.duplicate_input_id": "terminal",
100
+ "steering.invalid_content": "terminal",
101
+ "steering.queue_full": "terminal",
102
+ };
103
+ export function checkpointErrorDisposition(e) {
104
+ const word = Object.hasOwn(CHECKPOINT_ERROR_DISPOSITION, e.code) ? CHECKPOINT_ERROR_DISPOSITION[e.code] : undefined;
105
+ switch (word) {
106
+ case "cancelled":
107
+ case "retriable":
108
+ case "terminal":
109
+ return word;
110
+ case "by_reason":
111
+ return checkpointRowRedeemableElsewhere(e.detail?.reason) ? "retriable" : "terminal";
112
+ case undefined:
113
+ return "terminal";
114
+ default: {
115
+ const unhandled = word;
116
+ throw new Error(`unhandled checkpoint-error disposition: ${String(unhandled)}`);
117
+ }
118
+ }
119
+ }
120
+ const CHECKPOINT_ERROR_FIELD_ROLE = {
121
+ boundCallId: "binding",
122
+ boundInputHash: "binding",
123
+ answer: "answer",
124
+ hostDecision: "outcome_cell",
125
+ approver: "outcome_cell",
126
+ taskConfig: "config",
127
+ inheritedGate: "config",
128
+ };
129
+ const NON_BINDING_INVALID_OUTCOME_FIELDS = new Set(Object.keys(CHECKPOINT_ERROR_FIELD_ROLE).filter((f) => CHECKPOINT_ERROR_FIELD_ROLE[f] === "outcome_cell"));
74
130
  function notResumableAnswer() {
75
131
  return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
76
132
  }
77
133
  export function createResumeLegs(ctx) {
78
- const { deps, runnerFor, idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor } = ctx;
134
+ 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;
79
135
  const resumeFleetScope = (req, auth) => req ? (gatedPrincipal(req, deps.config) ?? "default") : (auth.principal ?? "default");
80
136
  async function resumeCheckpoint(sessionId, decision, reason, hostDecision, req, answer, binding, onResumeCommitted, acceptEarly, decider) {
81
137
  const cs = deps.checkpointStore;
@@ -225,7 +281,7 @@ export function createResumeLegs(ctx) {
225
281
  const noteLateSettleIfAccepted = (out) => {
226
282
  if (lateSettleSink === undefined || lateSettleAnchor === undefined || out.status !== 200)
227
283
  return;
228
- if (out.body?.errorCode === "cancelled")
284
+ if (out.body?.errorCode === "cancelled" && out.decisionCommitted !== true)
229
285
  return;
230
286
  try {
231
287
  lateSettleSink.recordLateSettle({ ...lateSettleAnchor, outcome: decision === "approve" ? "allowed" : "denied", decidedBy: hostDecision.decidedBy, ...(hostDecision.approver !== undefined ? { approver: hostDecision.approver } : {}), settledAtMs: Date.now() });
@@ -472,7 +528,8 @@ export function createResumeLegs(ctx) {
472
528
  let resumeLegLive = false;
473
529
  const subagentHandleEvictions = [];
474
530
  let claimedRow = false;
475
- const cancelCtrl = new AbortController();
531
+ const cancelCtrl = createRunAbort();
532
+ let cancelLedgerAppend;
476
533
  const preemptCtrl = new AbortController();
477
534
  if (taskId && deps.runStore) {
478
535
  claimedRow = freshRunClaimed ? true : await deps.runStore.markResuming(taskId, deps.instanceId ?? "default");
@@ -488,7 +545,7 @@ export function createResumeLegs(ctx) {
488
545
  void rs.heartbeat(taskId, owner).catch(() => undefined);
489
546
  if (!cancelCtrl.signal.aborted) {
490
547
  void rs.isCancelRequested(taskId, owner).then((c) => { if (c)
491
- cancelCtrl.abort(); }).catch(() => undefined);
548
+ cancelCtrl.abort("cancel_verb"); }).catch(() => undefined);
492
549
  }
493
550
  if (!preemptCtrl.signal.aborted) {
494
551
  void rs.isPreemptRequested(taskId, owner).then((p) => { if (p)
@@ -531,6 +588,7 @@ export function createResumeLegs(ctx) {
531
588
  instructionsSegment: instructionLeg.segment,
532
589
  });
533
590
  const vAppend = (type, data) => verifySink.append(type, data);
591
+ cancelLedgerAppend = vAppend;
534
592
  const verifyTailContentMode = resumeTaskConfig.forwardSubagentEvents === true ? "on" : "progress_only";
535
593
  const verifyInternals = {
536
594
  ...(taskId ? { onActivity: () => recordTurnActivity(taskId) } : {}),
@@ -579,7 +637,9 @@ export function createResumeLegs(ctx) {
579
637
  : {}),
580
638
  };
581
639
  resumeLegLive = true;
582
- const vr = await resumeWithVerification(legRunner, token, outcome, { ...resumeTaskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, resumeObjective, verifyRounds, verifyInternals);
640
+ const vrEngine = await resumeWithVerification(legRunner, token, outcome, { ...resumeTaskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, resumeObjective, verifyRounds, verifyInternals);
641
+ const vrCause = cancelCtrl.cause();
642
+ const vr = vrCause !== undefined && taskId !== undefined ? mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: "cancelled by user", engineResult: vrEngine, cause: vrCause }) : vrEngine;
583
643
  await commitResumeCtx();
584
644
  let safe = sealResultForExit(vr);
585
645
  const vrNested = nestedPauseOf(vr.terminal);
@@ -622,6 +682,7 @@ export function createResumeLegs(ctx) {
622
682
  const persistThinking = deps.config.traceThinking;
623
683
  const serialAppend = createSerialLedgerAppend((s, type, data) => rs.appendEvent(taskId, s, type, data), startSeq);
624
684
  const append = (type, data) => (rs && taskId ? serialAppend(type, data) : Promise.resolve());
685
+ cancelLedgerAppend = append;
625
686
  unregisterEngineNotice = registerEngineNoticeLeg({
626
687
  sessionId,
627
688
  ...(args.engineNotices !== undefined && args.engineNotices.length > 0 ? { pending: [...args.engineNotices] } : {}),
@@ -715,9 +776,13 @@ export function createResumeLegs(ctx) {
715
776
  if (!taskId || !rs)
716
777
  return sealResultForExit(await stream.result());
717
778
  onAccepted?.(acceptedResumingReceipt(taskId, sessionId));
779
+ let causeAtDone;
718
780
  for await (const ev of stream) {
781
+ if (ev.type === "done")
782
+ causeAtDone = cancelCtrl.cause();
719
783
  recordTurnActivity(taskId);
720
784
  fleetPub?.onEvent(ev);
785
+ await deps.userInputLedger?.observeLegEvent(stream, ev);
721
786
  await appendPromptManifest(append, deps.promptManifests, taskId);
722
787
  switch (ev.type) {
723
788
  case "text_delta":
@@ -823,7 +888,8 @@ export function createResumeLegs(ctx) {
823
888
  }
824
889
  }
825
890
  await flush();
826
- const r = await stream.result();
891
+ const rEngine = await stream.result();
892
+ const r = causeAtDone !== undefined ? mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: "cancelled by user", engineResult: rEngine, cause: causeAtDone }) : rEngine;
827
893
  const rPark = r.terminal.kind === "paused" ? outwardPlane(r.terminal).status : undefined;
828
894
  if (rPark === "suspended") {
829
895
  const parkCallId = await parkToolCallId(r, deps.checkpointStore);
@@ -882,7 +948,8 @@ export function createResumeLegs(ctx) {
882
948
  }
883
949
  catch (e) {
884
950
  if (e instanceof CheckpointError) {
885
- if (e.code === "checkpoint.resume_aborted") {
951
+ const disposition = checkpointErrorDisposition(e);
952
+ if (disposition === "cancelled") {
886
953
  const cs2 = deps.checkpointStore;
887
954
  if (cs2) {
888
955
  try {
@@ -898,25 +965,17 @@ export function createResumeLegs(ctx) {
898
965
  }
899
966
  }
900
967
  if (claimedRow && taskId && deps.runStore) {
901
- const c = mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: e.message });
968
+ const handleCause = cancelCtrl.cause();
969
+ if (handleCause === undefined)
970
+ recordFailOpen("server.runs.abort-cause-missing", `task=${taskId} arm=resume-aborted`);
971
+ const c = mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: e.message, cause: handleCause ?? "cancel_verb" });
972
+ await cancelLedgerAppend?.("failed", buildCancelledLedgerRow(c)).catch((err) => recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=resume-append-resume-aborted ${errorText(err)}`));
902
973
  await deps.runStore.setTerminal(taskId, "failed", c, e.message).catch(() => undefined);
903
974
  }
904
975
  settleFleet("failed");
905
976
  return { status: 200, body: { taskId, sessionId, status: "failed", errorCode: "cancelled" } };
906
977
  }
907
- const retriable = e.code === "checkpoint.invalid_outcome" ||
908
- e.code === "checkpoint.reopen_revote" ||
909
- e.code === "checkpoint.reopened_concurrently" ||
910
- e.code === "resume.parent_constraint_missing" ||
911
- e.code === "resume.parent_constraint_mismatch" ||
912
- e.code === "resume.constraint_rejected" ||
913
- e.code === "resume.constraint_unprojectable" ||
914
- e.code === "resume.usage_window_exhausted" ||
915
- e.code === "resume.principal_mismatch" ||
916
- e.code === "resume.placement_mismatch" ||
917
- e.code === "resume.preflight_rejected" ||
918
- e.code.startsWith("wake.") ||
919
- (e.code === "checkpoint.unsupported_version" && checkpointRowRedeemableElsewhere(e.detail?.reason));
978
+ const retriable = disposition === "retriable";
920
979
  if (claimedRow && taskId && deps.runStore) {
921
980
  if (retriable && freshRunClaimed) {
922
981
  const c = { taskId, sessionId, terminal: { kind: "failed", code: e.code, message: e.message }, stats: { turns: 0, tokens: 0, usageMissing: true } };
@@ -953,9 +1012,11 @@ export function createResumeLegs(ctx) {
953
1012
  },
954
1013
  };
955
1014
  }
956
- if (cancelCtrl.signal.aborted) {
1015
+ const thrownCause = cancelCtrl.cause();
1016
+ if (thrownCause !== undefined) {
957
1017
  if (taskId && deps.runStore) {
958
- const c = mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: "cancelled by user" });
1018
+ const c = mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: "cancelled by user", cause: thrownCause });
1019
+ await cancelLedgerAppend?.("failed", buildCancelledLedgerRow(c)).catch((err) => recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=resume-append-thrown-cancel ${errorText(err)}`));
959
1020
  await deps.runStore.setTerminal(taskId, "failed", c, "cancelled by user").catch(() => undefined);
960
1021
  }
961
1022
  settleFleet("failed");
@@ -1002,6 +1063,7 @@ export function createResumeLegs(ctx) {
1002
1063
  settleFleet(checkpointReopened ? "suspended" : resumePlane.status, fleetRunResiduals(sealed));
1003
1064
  return {
1004
1065
  status: 200,
1066
+ decisionCommitted: true,
1005
1067
  body: {
1006
1068
  taskId, sessionId, status: resumePlane.status,
1007
1069
  bindingEnforced: true,
@@ -1188,7 +1250,7 @@ export function createResumeLegs(ctx) {
1188
1250
  return resolved.answer;
1189
1251
  const { spec, engineNotices } = resolved;
1190
1252
  const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
1191
- return driveResumeIntoRunLog({
1253
+ return buildHostLegOutcome(await driveResumeIntoRunLog({
1192
1254
  runner: runnerFor(spec),
1193
1255
  token,
1194
1256
  sessionId: cp.sessionId,
@@ -1205,7 +1267,7 @@ export function createResumeLegs(ctx) {
1205
1267
  ...(req !== undefined ? { callerInitiated: true } : {}),
1206
1268
  ...(workflowParkedResume !== undefined ? { workflowParkedResume } : {}),
1207
1269
  ...(ownerLeaseAdmittedFor !== undefined && auth.principal === ownerLeaseAdmittedFor ? { ownerLeaseAdmitted: true } : {}),
1208
- });
1270
+ }));
1209
1271
  }
1210
1272
  async function preparePlanReview(sessionId, log, decider) {
1211
1273
  const cs = deps.checkpointStore;
@@ -1301,7 +1363,7 @@ export function createResumeLegs(ctx) {
1301
1363
  }
1302
1364
  const routeCtxBase = {
1303
1365
  deps,
1304
- registry: { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters },
1366
+ registry: { idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters },
1305
1367
  helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
1306
1368
  local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal(), capabilities: createCapabilitiesLocal() },
1307
1369
  legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake, preparePlanReview, drivePlanReview },
@@ -26,12 +26,15 @@ import type { SessionsLocal } from "./routes/sessions.js";
26
26
  import type { SessionSyncLocal } from "./routes/session-sync.js";
27
27
  import type { PlanApprovePermissionModeAfter } from "../task-settings.js";
28
28
  import type { VerifyRoundsSpec } from "./verify-rounds.js";
29
- /** 跨域可变状态 = lens1 §C2 实测的那 8 条「运行期登记簿」,一个对象。 */
29
+ import type { RunAbort } from "../run-cancel-context.js";
30
+ /** 跨域可变状态 = lens1 §C2 实测的那 8 条「运行期登记簿」,一个对象。
31
+ * S-947:`cancelledViaVerb`(按 taskId 键控的「这次是取消动词」旁表)删除 —— 中止成因改挂在每条腿自己的
32
+ * {@link RunAbort} 句柄上(`inflightRuns` 的值),同寿同删,不再有一张与腿不同寿的旁表。 */
30
33
  export interface RunRegistry {
31
34
  idemCache: IdempotencyCache<SubmitOutcome>;
32
- inflightRuns: Map<string, AbortController>;
35
+ /** 本副本在飞的 durable 腿 → 它的中止句柄(取消动词的同副本快路在这里按 `cancel_verb` 中止它)。 */
36
+ inflightRuns: Map<string, RunAbort>;
33
37
  preemptableRuns: Map<string, AbortController>;
34
- cancelledViaVerb: Set<string>;
35
38
  steerableRuns: Map<string, TaskStream>;
36
39
  wakeParkMints: Map<string, Promise<void>>;
37
40
  /** 裸 number 的登记簿(`let` 拆不出闭包)—— 装进盒子才能跨模块自增/自减。 */
@@ -69,8 +72,10 @@ export interface RouteHelpers {
69
72
  usageWindowDenied(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
70
73
  safeDecode(seg: string): string | null;
71
74
  isFleetWide(req: IncomingMessage): boolean;
72
- /** 属主门。`notFoundForm` 选 not-yours 臂的 404 拼法:缺省 `"run"`(not_found.run 长文,[ref]);session
73
- * 资源面传 `"session"`(not_found.session + "session not found")与其 unknown 臂同码同串(G52 [ref])。 */
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 面不传。 */
74
79
  runOwnerOk(req: IncomingMessage, res: ServerResponse, owner: string | null, notFoundForm?: "run" | "session"): boolean;
75
80
  sessionOwnerScope(req: IncomingMessage): {
76
81
  fleetWide: boolean;
@@ -380,15 +385,9 @@ export interface RouteLegs {
380
385
  decider?: {
381
386
  principal?: string;
382
387
  explicitOperator: boolean;
383
- }): Promise<{
384
- status: number;
385
- body: object;
386
- }>;
388
+ }): Promise<ResumeLegOutcome>;
387
389
  /** 全 resume 家族的共同下半场(lease admission + 写 run log + fleet 发布)。 */
388
- driveResumeIntoRunLog(args: DriveResumeArgs): Promise<{
389
- status: number;
390
- body: object;
391
- }>;
390
+ driveResumeIntoRunLog(args: DriveResumeArgs): Promise<ResumeLegOutcome>;
392
391
  /** assistant 抢占腿的复位(gate-kind 守卫 + 行级属主复核 + 驱动)。 */
393
392
  resumePreempted(sessionId: string, req: IncomingMessage | undefined,
394
393
  /** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/resume` 腿传 `true`。 */
@@ -411,10 +410,7 @@ export interface RouteLegs {
411
410
  * 顶注)。HTTP `/wake` 腿**不设**此位 —— 它是 `/decide` 第三条车道的专用席,而两条腿共用这一条驱动
412
411
  * 路径正是 `notify-wake.ts` 那条「wake 会烧模型,所以复用 resume 家族的同一条腿、不自建第二条驱动
413
412
  * 路径」的成文纪律。 */
414
- workflowParkedResume?: DriveResumeArgs["workflowParkedResume"]): Promise<{
415
- status: number;
416
- body: object;
417
- }>;
413
+ workflowParkedResume?: DriveResumeArgs["workflowParkedResume"]): Promise<HostLegOutcome>;
418
414
  /**
419
415
  * assistant plan_review 三态腿的**纯读段**(S-609,7.97.0):选卡 → 属主轻读 → 载入 → 行级 scope → 门类 → 这条决断
420
416
  * 唯一的一次 ctx 读。任何一步拒 ⇒ `{ok:false, answer}`(不改任何状态;端点在消费一次性 nonce **之前**把它答出去);
@@ -628,6 +624,49 @@ export type RouteDecl = RouteDispatchDecl | RouteGateDecl;
628
624
  * 两臂逐字节同形,指路价值不丢。钉=audit-fixes-http「[ref]」格(两臂全文比对);文案变更走
629
625
  * api-error-text-freeze 基线流程。
630
626
  */
627
+ /**
628
+ * resume 家族**引擎腿**的结局:HTTP 应答(`status` / `body`)+ 一位**进程内**事实(不上 wire)。workflow 宿主腿的结局另型({@link HostLegOutcome}),并集 = {@link ResumeLegOutcome}。
629
+ *
630
+ * `decisionCommitted` —— **这条腿的终局是在引擎受理了这次决定之后才结出来的**([ref] X5,7.112.0):本腿走到了「引擎交回结果」那一步
631
+ * (`resumeStream` / `resumeWithVerification` 解析 = core 的全部 pre-CAS 门 + checkpoint resolve CAS 都过了 —— 受理点的判据属主
632
+ * = {@link DriveResumeArgs.acceptEarly} 顶注),之后才收场。只由 `driveResumeLeg` 走完引擎那一步的终局出口铸;`CheckpointError` 臂、
633
+ * 「中止抛穿」臂、一切 4xx / 拒绝出口一律**不铸**(缺席 ⇒ 证不出提交,读者按「没提交」读 —— fail-closed)。
634
+ *
635
+ * 读者:`/decide` 的两道「这次决定提交了没有」门 —— [ref] 迟到结算留痕(`noteLateSettleIfAccepted`)与 remember 豁免落店
636
+ * (`routes/approvals-assistant.ts`)。它们原先只读 wire 体的 `errorCode !== "cancelled"`(「取消 = 决定没提交」),S-947 给续跑腿补了
637
+ * 「引擎受理之后才被取消」的取消终局之后,`cancelled` 不再等价「没提交」⇒ 两门改读本位:`200 ∧ (errorCode≠cancelled ∨ decisionCommitted)`。
638
+ * 🔴 为什么不放进 `result.cancelContext`(主会话初稿的落点):那只快照的契约是「分诊材料,不是状态机输入:任何门 / CAS / resume 判定都不许
639
+ * 读它」(`plugins/store-contracts.ts` `RunCancelContext` 顶注)—— 这两道门正是判定;而且门读的是 `/decide` 的平面体、看不到结果对象。
640
+ * 受理与否是腿自己的控制流事实,在腿上说,零 wire 变更。
641
+ */
642
+ export interface EngineLegOutcome {
643
+ status: number;
644
+ body: object;
645
+ decisionCommitted?: true;
646
+ }
647
+ /**
648
+ * **workflow 宿主腿的结局(wake / fresh)—— 型上就没有 `decisionCommitted`**(S-992 ②,7.113.0;`never` 让「这条路径不铸」成为**编译事实**,不是靠人记)。
649
+ *
650
+ * 为什么这条路径不该有该位:`/decide` 的 workflow 第三条车道(`resumeCheckpoint` 的 parked 分流,`planWorkflowParkedDecide` 之后)与 `POST /v1/sessions/:id/wake`
651
+ * 把决定 / 消息交给**宿主会话的下一跑**(`resumeWake` / `startFreshHostTurn`)。它们的 200 只到「已交给宿主的下一跑」—— 子代 checkpoint 的 resolve CAS 发生在
652
+ * workflow 引擎自己驱动那一步,server 观察不到(`resume-legs.ts` 该分流处「刻意不落 `late_settle`」同源)。「这次决定被引擎受理了」这句话在这里**没有可说的对象**,
653
+ * 于是 `decisionCommitted` 在这条路径上语义含混(修前:`resumeWake` 的收尾是 `driveResumeIntoRunLog` 的 `ResumeLegOutcome`,非受理点早返形(无 `acceptEarly`)下它会把
654
+ * 引擎腿的 `decisionCommitted:true` 原样带出去 —— 型被声明返回型 `{status, body}` 擦掉了,运行期仍在)。读者核实:两道提交门(`noteLateSettleIfAccepted` 迟到结算留痕、
655
+ * `/decide` 的 remember 豁免落店)在这条路径上今天**零读取**(前者该分流直接 `return`、不调;后者的 `pendingGrant` 只由引擎腿的 `onResumeCommitted` 回调置位,宿主腿不调它,且 `planWorkflowParkedDecide` 对 workflow 车道的 `remember` 本就 fail-closed 拒 —— `decide.workflow_remember_unsupported`,`parked-decide.ts`)——
656
+ * 无读者 ⇒ 剥掉该位,而不是留一个「铸了也没人读、读了不知道什么意思」的位。出口经 {@link buildHostLegOutcome} 只带 `status` / `body`。
657
+ */
658
+ export interface HostLegOutcome {
659
+ status: number;
660
+ body: object;
661
+ decisionCommitted?: never;
662
+ }
663
+ /** resume 家族一条腿的结局:引擎腿(可带 `decisionCommitted`)∪ workflow 宿主腿(型上没有)。`out.decisionCommitted === true` 同时收窄到引擎腿。 */
664
+ export type ResumeLegOutcome = EngineLegOutcome | HostLegOutcome;
665
+ /** 把任意腿的结局折成宿主腿结局:**只带 `status` / `body`**(丢掉引擎腿的 `decisionCommitted` —— 见 {@link HostLegOutcome})。纯数据。 */
666
+ export declare function buildHostLegOutcome(out: {
667
+ status: number;
668
+ body: object;
669
+ }): HostLegOutcome;
631
670
  /**
632
671
  * **「决定/续跑已受理」那一枚 200 收据的单一铸点**(S-218)。
633
672
  *
@@ -643,6 +682,14 @@ export declare function acceptedResumingReceipt(taskId: string, sessionId: strin
643
682
  body: object;
644
683
  };
645
684
  export declare const RUN_NOT_FOUND_MESSAGE = "run not found \u2014 the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)";
685
+ /**
686
+ * 会话资源面 404 的**唯一一句**(与 {@link RUN_NOT_FOUND_MESSAGE} 同一冻结纪律,常量锚在 `test/api-error-text-freeze.test.ts`)。
687
+ *
688
+ * 为什么是常量不是各点字面量:这句话要在**每一个**会话面的「不存在」臂与「非属主」臂上逐字节同串(G52 [ref] /
689
+ * S-989 —— 两臂差一个码或一个字就是单端点内的存在性 oracle)。十余个出站点各抄一份,漂开的那天没有任何东西会说话;
690
+ * 走一只常量 + `runOwnerOk(..., "session")` 的会话拼法,「改一处」就是改全域。
691
+ */
692
+ export declare const SESSION_NOT_FOUND_MESSAGE = "session not found";
646
693
  /** [ref] 前缀=域判别子既成契约:a\* 与 wa\* 形 id 给 TaskOutput/journal 指路;其余给 {@link RUN_NOT_FOUND_MESSAGE}。 */
647
694
  export declare function runNotFoundMessage(id: string): string;
648
695
  export {};
@@ -21,10 +21,14 @@ export function assertNever(x) {
21
21
  throw new Error(`unreachable route id: ${String(x)}`);
22
22
  }
23
23
  export const ROUTE_METHOD_ORDER = ["GET", "POST", "PUT", "DELETE"];
24
+ export function buildHostLegOutcome(out) {
25
+ return { status: out.status, body: out.body };
26
+ }
24
27
  export function acceptedResumingReceipt(taskId, sessionId) {
25
28
  return { status: 200, body: { taskId, sessionId, status: "resuming", bindingEnforced: true } };
26
29
  }
27
30
  export const RUN_NOT_FOUND_MESSAGE = "run not found — the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)";
31
+ export const SESSION_NOT_FOUND_MESSAGE = "session not found";
28
32
  export function runNotFoundMessage(id) {
29
33
  const aStar = /^(?:a|wa)[0-9a-f]{4,}$/i.test(id);
30
34
  return aStar
@@ -764,7 +764,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
764
764
  : undefined;
765
765
  const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(settledBy, deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
766
766
  const outErrorCode = out.body.errorCode;
767
- if (pendingGrant !== undefined && out.status === 200 && outErrorCode !== "cancelled" && deps.approvalExemptionStore) {
767
+ if (pendingGrant !== undefined && out.status === 200 && (outErrorCode !== "cancelled" || out.decisionCommitted === true) && deps.approvalExemptionStore) {
768
768
  try {
769
769
  await deps.approvalExemptionStore.grant(pendingGrant.sessionId, pendingGrant.toolName, deciderPrincipal ?? null);
770
770
  rememberApplied = true;
@@ -5,7 +5,7 @@ import { redactSecrets } from "../../trace/redact.js";
5
5
  import { sendJson, sendError } from "../send.js";
6
6
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
7
7
  import {} from "../route-ctx.js";
8
- import { assertNever } from "../route-ctx.js";
8
+ import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
9
9
  const LIST_LIMIT = 500;
10
10
  export const DESCRIPTION_MAX = 500;
11
11
  const ROW_KEY_DISPOSITION = {
@@ -65,7 +65,7 @@ export async function handleBackground(req, res, match, ctx) {
65
65
  }
66
66
  const owner = await deps.sessionStorage.ownerOf(sessionId);
67
67
  if (owner === undefined) {
68
- sendError(res, 404, "not_found.session", "session not found");
68
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
69
69
  return;
70
70
  }
71
71
  const isOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
@@ -4,7 +4,7 @@ import { redactSecrets } from "../../trace/redact.js";
4
4
  import { sendJson, sendError } from "../send.js";
5
5
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
6
  import {} from "../route-ctx.js";
7
- import { assertNever } from "../route-ctx.js";
7
+ import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
8
8
  const BodySchema = z.object({ server: z.string().min(1).max(190) }).strict();
9
9
  const NEUTRALIZED_TEXT_MAX = 200;
10
10
  const humanText = (s) => redactSecrets(s).slice(0, NEUTRALIZED_TEXT_MAX);
@@ -82,7 +82,7 @@ export async function handleMcpReconnect(req, res, match, ctx) {
82
82
  }
83
83
  const owner = await deps.sessionStorage.ownerOf(sessionId);
84
84
  if (owner === undefined) {
85
- sendError(res, 404, "not_found.session", "session not found");
85
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
86
86
  return;
87
87
  }
88
88
  const isOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
@@ -2,7 +2,7 @@ import { taskNotificationInboxEntry } from "../../orchestration/workflow-complet
2
2
  import { sendJson, sendError } from "../send.js";
3
3
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
4
4
  import { sendResumeOutcome } from "../route-ctx.js";
5
- import { assertNever } from "../route-ctx.js";
5
+ import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
6
6
  export async function handleNotifyWake(req, res, match, ctx) {
7
7
  const { deps } = ctx;
8
8
  const { steerableRuns } = ctx.registry;
@@ -63,12 +63,12 @@ export async function handleNotifyWake(req, res, match, ctx) {
63
63
  }
64
64
  const notifyOwner = await deps.sessionStorage.ownerOf(notifySession);
65
65
  if (notifyOwner === undefined) {
66
- sendError(res, 404, "not_found.session", "session not found");
66
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
67
67
  return;
68
68
  }
69
69
  const notifyOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
70
70
  if (!notifyOperator && notifyOwner !== null && notifyOwner !== principal) {
71
- sendError(res, 404, "not_found.session", "session not found");
71
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
72
72
  return;
73
73
  }
74
74
  const payload = {
@@ -146,9 +146,23 @@ async function acceptLiveUserInput(deps, res, at) {
146
146
  return "refused";
147
147
  }
148
148
  }
149
+ async function relayAcceptedInput(deps, acceptance, call) {
150
+ let out;
151
+ try {
152
+ out = await call();
153
+ }
154
+ catch (e) {
155
+ if (acceptance !== undefined)
156
+ await deps.userInputLedger.withdraw(acceptance);
157
+ throw e;
158
+ }
159
+ if (acceptance !== undefined)
160
+ await deps.userInputLedger.confirm(acceptance);
161
+ return out;
162
+ }
149
163
  export async function handleRuns(req, res, match, ctx) {
150
164
  const { deps } = ctx;
151
- const { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints } = ctx.registry;
165
+ const { idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints } = ctx.registry;
152
166
  const { readJson, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied, runOwnerOk, runSessionAcceptOk } = ctx.helpers;
153
167
  const { prepareSpec } = ctx.legs;
154
168
  const source = ctx.req.source;
@@ -319,7 +333,7 @@ export async function handleRuns(req, res, match, ctx) {
319
333
  }
320
334
  const err = note;
321
335
  try {
322
- const result = mintCancelledResult({ taskId, sessionId: run.sessionId, startedAtMs: Date.parse(run.createdAt), message: err });
336
+ const result = mintCancelledResult({ taskId, sessionId: run.sessionId, startedAtMs: Date.parse(run.createdAt), message: err, cause: "cancel_verb" });
323
337
  let lastErr;
324
338
  for (let attempt = 0;; attempt++) {
325
339
  try {
@@ -368,10 +382,7 @@ export async function handleRuns(req, res, match, ctx) {
368
382
  sendJson(res, 202, { taskId, status: now?.status ?? "failed", cancelRequested: now?.cancelRequested ?? false, note: "run already terminal — cancel is a no-op" });
369
383
  return;
370
384
  }
371
- if (inflightRuns.has(taskId)) {
372
- cancelledViaVerb.add(taskId);
373
- inflightRuns.get(taskId).abort();
374
- }
385
+ inflightRuns.get(taskId)?.abort("cancel_verb");
375
386
  sendJson(res, 202, { taskId, status: "cancelling", cancelRequested: true });
376
387
  }
377
388
  else if (isParkedRunStatus(run.status)) {
@@ -484,13 +495,11 @@ export async function handleRuns(req, res, match, ctx) {
484
495
  if (acceptance === "refused")
485
496
  return;
486
497
  try {
487
- await live.steer(text, { trusted, ...(steerInputId !== undefined ? { inputId: steerInputId } : {}) });
498
+ await relayAcceptedInput(deps, acceptance, () => live.steer(text, { trusted, inputId: messageId }));
488
499
  sendJson(res, 200, { taskId, status: "running", delivery: "applied", messageId, ...(priority ? { priority } : {}) });
489
500
  return;
490
501
  }
491
502
  catch (e) {
492
- if (acceptance !== undefined)
493
- await deps.userInputLedger.withdraw(acceptance);
494
503
  const code = e.code;
495
504
  if (code === "steering.invalid_content") {
496
505
  sendError(res, 422, "steering.invalid_content", e instanceof Error ? e.message : "invalid steering content");
@@ -797,11 +806,9 @@ export async function handleRuns(req, res, match, ctx) {
797
806
  if (acceptance === "refused")
798
807
  return;
799
808
  try {
800
- await liveStream.steer(text, { trusted, priority: "now", ...(interruptInputId !== undefined ? { inputId: interruptInputId } : {}) });
809
+ await relayAcceptedInput(deps, acceptance, () => liveStream.steer(text, { trusted, priority: "now", ...(interruptInputId !== undefined ? { inputId: interruptInputId } : {}) }));
801
810
  }
802
811
  catch (e) {
803
- if (acceptance !== undefined)
804
- await deps.userInputLedger.withdraw(acceptance);
805
812
  const code = e.code;
806
813
  if (code === "steering.invalid_content") {
807
814
  sendError(res, 422, "steering.invalid_content", e instanceof Error ? e.message : "invalid steering content");
@@ -1155,7 +1162,6 @@ export async function handleRunVerbs(req, res, match, ctx) {
1155
1162
  }
1156
1163
  const handle = resolution?.handle;
1157
1164
  if (handle) {
1158
- let acceptance;
1159
1165
  try {
1160
1166
  if (verb === "resume") {
1161
1167
  if (!handle.resume) {
@@ -1172,11 +1178,10 @@ export async function handleRunVerbs(req, res, match, ctx) {
1172
1178
  return;
1173
1179
  }
1174
1180
  const messageId = uuidv7();
1175
- const accepted = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId: runId, leg: handle });
1176
- if (accepted === "refused")
1181
+ const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId: runId, leg: handle });
1182
+ if (acceptance === "refused")
1177
1183
  return;
1178
- acceptance = accepted;
1179
- const marker = await handle.steer(redacted);
1184
+ const marker = await relayAcceptedInput(deps, acceptance, () => handle.steer(redacted));
1180
1185
  if (acceptance !== undefined) {
1181
1186
  const ledger = deps.userInputLedger;
1182
1187
  const settle = () => ledger.settleLeg(handle);
@@ -1186,8 +1191,6 @@ export async function handleRunVerbs(req, res, match, ctx) {
1186
1191
  return;
1187
1192
  }
1188
1193
  catch (e) {
1189
- if (acceptance !== undefined)
1190
- await deps.userInputLedger.withdraw(acceptance);
1191
1194
  const code = e.code;
1192
1195
  if (code === "steering.not_running") {
1193
1196
  send409(code, "sub-agent just finished — no longer accepting steers");
@@ -12,7 +12,7 @@ const SYNC_BLOB_MAX_BYTES = 64 * 1024 * 1024;
12
12
  const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
13
13
  const SYNC_IMPORT_BATCH = 500;
14
14
  import { SHA256_HEX_RE } from "../../digest-form.js";
15
- import { assertNever } from "../route-ctx.js";
15
+ import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
16
16
  import { readSessionRulesRecords, describeSessionRulesRecordProblem, legacyToolNameProblemOf } from "../../session-policy-wire.js";
17
17
  export function createSessionSyncLocal() {
18
18
  const pendingStagings = new Map();
@@ -60,18 +60,18 @@ export async function handleSessionSync(req, res, match, ctx) {
60
60
  const ownsOrFleet = (o) => scope.fleetWide || o === scope.gateOwner;
61
61
  const pullGateOk = () => {
62
62
  if (owner === undefined) {
63
- sendError(res, 404, "not_found.session", "session not found");
63
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
64
64
  return false;
65
65
  }
66
66
  if (!ownsOrFleet(owner)) {
67
- sendError(res, 404, "not_found.session", "session not found");
67
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
68
68
  return false;
69
69
  }
70
70
  return true;
71
71
  };
72
72
  const pushGateOk = () => {
73
73
  if (owner !== undefined && !ownsOrFleet(owner)) {
74
- sendError(res, 404, "not_found.session", "session not found");
74
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
75
75
  return false;
76
76
  }
77
77
  return true;
@@ -89,7 +89,7 @@ export async function handleSessionSync(req, res, match, ctx) {
89
89
  throw e;
90
90
  }
91
91
  if (out === null) {
92
- sendError(res, 404, "not_found.session", "session not found");
92
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
93
93
  return;
94
94
  }
95
95
  sendJson(res, 200, { manifest: out.manifest });
@@ -115,7 +115,7 @@ export async function handleSessionSync(req, res, match, ctx) {
115
115
  }
116
116
  const it = await stream(sessionId, afterSeq === undefined ? undefined : { afterSeq });
117
117
  if (it === null) {
118
- sendError(res, 404, "not_found.session", "session not found");
118
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
119
119
  return;
120
120
  }
121
121
  res.writeHead(200, { "content-type": "application/x-ndjson", "transfer-encoding": "chunked" });