@sema-agent/server 7.110.0 → 7.112.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 (67) hide show
  1. package/MIGRATION.md +16 -0
  2. package/dist/approval-ask-audit-store.d.ts +38 -25
  3. package/dist/approval-ask-audit-store.js +26 -59
  4. package/dist/bench/s1/run-firm.d.ts +11 -0
  5. package/dist/bench/s1/run-firm.js +47 -32
  6. package/dist/boot/coordinators.js +1 -1
  7. package/dist/boot/session-faces.d.ts +4 -0
  8. package/dist/boot/session-faces.js +3 -1
  9. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  10. package/dist/boot/stage-07-capability-layer.js +4 -3
  11. package/dist/boot/stage-08-reapers.d.ts +1 -0
  12. package/dist/boot/stage-09-leader.d.ts +1 -0
  13. package/dist/boot/stage-09-leader.js +2 -1
  14. package/dist/boot/stage-10-http-server.d.ts +1 -0
  15. package/dist/boot/workflow-orchestration.js +1 -1
  16. package/dist/hooks/hook-runner.d.ts +5 -0
  17. package/dist/hooks/hook-runner.js +45 -1
  18. package/dist/http/admission.js +6 -1
  19. package/dist/http/resume-legs.d.ts +6 -15
  20. package/dist/http/resume-legs.js +33 -14
  21. package/dist/http/route-ctx.d.ts +28 -11
  22. package/dist/http/routes/approvals-assistant.js +1 -1
  23. package/dist/http/routes/runs.js +17 -7
  24. package/dist/http/routes/tasks.d.ts +3 -2
  25. package/dist/http/routes/tasks.js +45 -51
  26. package/dist/http/server.d.ts +1 -1
  27. package/dist/http/server.js +1 -2
  28. package/dist/index.d.ts +1 -1
  29. package/dist/instruction-selection.d.ts +34 -0
  30. package/dist/instruction-selection.js +23 -0
  31. package/dist/observability/corrupt-read-seat.d.ts +18 -0
  32. package/dist/observability/corrupt-read-seat.js +18 -0
  33. package/dist/observability/fail-open.d.ts +20 -4
  34. package/dist/observability/fail-open.js +17 -4
  35. package/dist/orchestration/workflow-agent-session-index.d.ts +12 -23
  36. package/dist/orchestration/workflow-agent-session-index.js +11 -50
  37. package/dist/orchestration/workflow-notify-journal.d.ts +11 -8
  38. package/dist/orchestration/workflow-notify-journal.js +20 -36
  39. package/dist/plugins/file-outcome-sink.d.ts +4 -1
  40. package/dist/plugins/file-outcome-sink.js +9 -3
  41. package/dist/plugins/file-run-store.js +3 -3
  42. package/dist/plugins/retention-store-sql.d.ts +1 -1
  43. package/dist/plugins/retention-store-sql.js +2 -0
  44. package/dist/plugins/store-backend.js +1 -0
  45. package/dist/plugins/store-contracts.d.ts +59 -9
  46. package/dist/plugins/store-contracts.js +10 -0
  47. package/dist/plugins/user-input-ledger-file.d.ts +4 -2
  48. package/dist/plugins/user-input-ledger-file.js +11 -1
  49. package/dist/plugins/user-input-ledger-sql.d.ts +14 -4
  50. package/dist/plugins/user-input-ledger-sql.js +46 -18
  51. package/dist/project-memory.d.ts +10 -2
  52. package/dist/project-memory.js +5 -2
  53. package/dist/run-cancel-context.d.ts +59 -7
  54. package/dist/run-cancel-context.js +26 -0
  55. package/dist/runs.d.ts +7 -5
  56. package/dist/runs.js +22 -16
  57. package/dist/trace/ledger-sink.d.ts +4 -0
  58. package/dist/trace/ledger-sink.js +1 -1
  59. package/dist/trace/project.d.ts +11 -3
  60. package/dist/trace/project.js +6 -5
  61. package/dist/trace/self-minted-wire.d.ts +48 -0
  62. package/dist/trace/self-minted-wire.js +17 -0
  63. package/dist/trace/wire-projection-faces.d.ts +3 -3
  64. package/dist/trace/wire-projection-faces.js +4 -2
  65. package/dist/user-input-ledger.d.ts +101 -48
  66. package/dist/user-input-ledger.js +52 -35
  67. package/package.json +3 -3
@@ -7,7 +7,7 @@ import { taskBodyKeyIssue } from "../request-key-closure.js";
7
7
  import { FILE_HISTORY_DISABLED_NO_RESTORE, fileHistoryRestoreConflict, inheritEnvFieldIssue, singleMemberOffFieldIssue } from "../request-value-closure.js";
8
8
  import { selfOrchestrationDenial } from "../task-workflow.js";
9
9
  import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
10
- import { parseHooksConfig } from "../hooks/hook-runner.js";
10
+ import { deadToolMatcherIssue, parseHooksConfig } from "../hooks/hook-runner.js";
11
11
  import { judgeRequestWebSearch } from "../plugins/web-search.js";
12
12
  import { normalizeApproachNotice, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
13
13
  import { isValidCwd, MAX_ADDITIONAL_DIRS, MAX_CWD_CHARS } from "../task-cwd.js";
@@ -200,6 +200,11 @@ export function createAdmission(ctx) {
200
200
  sendError(res, 400, "request.field_invalid", hooksParsed.error ?? "settings.hooks: invalid shape");
201
201
  return null;
202
202
  }
203
+ const deadMatcher = deadToolMatcherIssue(hooksParsed.config);
204
+ if (deadMatcher !== undefined) {
205
+ sendError(res, 400, "request.field_invalid", deadMatcher);
206
+ return null;
207
+ }
203
208
  }
204
209
  {
205
210
  const webSearchVerdict = judgeRequestWebSearch(st.webSearch);
@@ -7,8 +7,8 @@
7
7
  * 字节不变)。同居是第三条,也是唯一一条既保住静态序又保住 move-only 的。序由 `const` 的 TDZ 亲自执法:
8
8
  * 腿是提升的函数声明、ctx 是文件尾的 `const`,谁也不会在工厂执行期读到对方。
9
9
  *
10
- * ctx 字段 28 只(registry 7 + helpers 14 + deps/runnerFor/prepareSpec/finalizeTaskResult + 三只配额/租约/用量窗
11
- * 判据)—— 这是**如实披露**的耦合读数(设计稿 §9:把它变小是波 3 的事,本刀不承诺)。型一律从既有型派生
10
+ * ctx 字段 27 只(registry 6 + helpers 14 + deps/runnerFor/prepareSpec/finalizeTaskResult + 三只配额/租约/用量窗
11
+ * 判据;S-947 删 `cancelledViaVerb` 旁表后 28 → 27,与 `test/split-ctx-parity.test.ts` 的读数同)—— 这是**如实披露**的耦合读数(设计稿 §9:把它变小是波 3 的事,本刀不承诺)。型一律从既有型派生
12
12
  * (`RunRegistry` / `RouteHelpers` / `RouteLegs` / `ReturnType<typeof createRequestGuards>`),不手抄签名 ⇒ 不会漂。
13
13
  */
14
14
  import type { IncomingMessage } from "node:http";
@@ -17,7 +17,7 @@ import type { DecideBinding } from "./wire-types.js";
17
17
  import type { CheckpointStoreFull } from "../plugins/store-backend.js";
18
18
  import { type HostDecision } from "../host-decision.js";
19
19
  import { type PlanApprovePermissionModeAfter } from "../task-settings.js";
20
- import type { RouteCtxBase, DriveResumeArgs, PreparedPlanReview } from "./route-ctx.js";
20
+ import type { RouteCtxBase, DriveResumeArgs, PreparedPlanReview, ResumeLegOutcome } from "./route-ctx.js";
21
21
  import { type TrustedRunInternals } from "./durable-run-leg.js";
22
22
  import type { RunRegistry, RouteHelpers, RouteLegs } from "./route-ctx.js";
23
23
  import type { createRequestGuards } from "./request-guards.js";
@@ -33,21 +33,12 @@ export declare function createResumeLegs(ctx: {
33
33
  }) => Promise<void>, acceptEarly?: boolean, decider?: {
34
34
  principal?: string;
35
35
  explicitOperator: boolean;
36
- }) => Promise<{
37
- status: number;
38
- body: object;
39
- }>;
40
- driveResumeIntoRunLog: (args: DriveResumeArgs) => Promise<{
41
- status: number;
42
- body: object;
43
- }>;
36
+ }) => Promise<ResumeLegOutcome>;
37
+ driveResumeIntoRunLog: (args: DriveResumeArgs) => Promise<ResumeLegOutcome>;
44
38
  driveResumeLeg: (args: DriveResumeArgs, onAccepted?: (receipt: {
45
39
  status: number;
46
40
  body: object;
47
- }) => void) => Promise<{
48
- status: number;
49
- body: object;
50
- }>;
41
+ }) => void) => Promise<ResumeLegOutcome>;
51
42
  mayDiscloseRowError: (cs: CheckpointStoreFull, token: CheckpointToken, decider: {
52
43
  principal?: string;
53
44
  explicitOperator: boolean;
@@ -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";
@@ -20,6 +20,7 @@ import { buildCheckpointAnchorHash } from "../approval-ask-audit-store.js";
20
20
  import { TERMINAL_PARK_GATE_KINDS } from "../plugins/checkpoint-store-sql.js";
21
21
  import { withPrincipal } from "../observability/principal-context.js";
22
22
  import { withModelReachScope } from "../model-reach-scope.js";
23
+ import { createInstructionSelectionLeg } from "../instruction-selection.js";
23
24
  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";
24
25
  import { redactSecrets } from "../trace/redact.js";
25
26
  import { createLedgerSink, createSerialLedgerAppend } from "../trace/ledger-sink.js";
@@ -74,7 +75,7 @@ function notResumableAnswer() {
74
75
  return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
75
76
  }
76
77
  export function createResumeLegs(ctx) {
77
- 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;
78
+ 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;
78
79
  const resumeFleetScope = (req, auth) => req ? (gatedPrincipal(req, deps.config) ?? "default") : (auth.principal ?? "default");
79
80
  async function resumeCheckpoint(sessionId, decision, reason, hostDecision, req, answer, binding, onResumeCommitted, acceptEarly, decider) {
80
81
  const cs = deps.checkpointStore;
@@ -224,7 +225,7 @@ export function createResumeLegs(ctx) {
224
225
  const noteLateSettleIfAccepted = (out) => {
225
226
  if (lateSettleSink === undefined || lateSettleAnchor === undefined || out.status !== 200)
226
227
  return;
227
- if (out.body?.errorCode === "cancelled")
228
+ if (out.body?.errorCode === "cancelled" && out.decisionCommitted !== true)
228
229
  return;
229
230
  try {
230
231
  lateSettleSink.recordLateSettle({ ...lateSettleAnchor, outcome: decision === "approve" ? "allowed" : "denied", decidedBy: hostDecision.decidedBy, ...(hostDecision.approver !== undefined ? { approver: hostDecision.approver } : {}), settledAtMs: Date.now() });
@@ -471,7 +472,8 @@ export function createResumeLegs(ctx) {
471
472
  let resumeLegLive = false;
472
473
  const subagentHandleEvictions = [];
473
474
  let claimedRow = false;
474
- const cancelCtrl = new AbortController();
475
+ const cancelCtrl = createRunAbort();
476
+ let cancelLedgerAppend;
475
477
  const preemptCtrl = new AbortController();
476
478
  if (taskId && deps.runStore) {
477
479
  claimedRow = freshRunClaimed ? true : await deps.runStore.markResuming(taskId, deps.instanceId ?? "default");
@@ -487,7 +489,7 @@ export function createResumeLegs(ctx) {
487
489
  void rs.heartbeat(taskId, owner).catch(() => undefined);
488
490
  if (!cancelCtrl.signal.aborted) {
489
491
  void rs.isCancelRequested(taskId, owner).then((c) => { if (c)
490
- cancelCtrl.abort(); }).catch(() => undefined);
492
+ cancelCtrl.abort("cancel_verb"); }).catch(() => undefined);
491
493
  }
492
494
  if (!preemptCtrl.signal.aborted) {
493
495
  void rs.isPreemptRequested(taskId, owner).then((p) => { if (p)
@@ -503,7 +505,8 @@ export function createResumeLegs(ctx) {
503
505
  await args.onResumeCommitted();
504
506
  let unregisterEngineNotice;
505
507
  try {
506
- result = await withModelReachScope(sessionId, () => withPrincipal(principal, async () => {
508
+ const instructionLeg = createInstructionSelectionLeg();
509
+ result = await instructionLeg.run(sessionId, () => withModelReachScope(sessionId, () => withPrincipal(principal, async () => {
507
510
  const resumeNotifiedKeys = new NotifiedKeys();
508
511
  if (deps.runStore && taskId) {
509
512
  const rs0 = deps.runStore;
@@ -526,8 +529,10 @@ export function createResumeLegs(ctx) {
526
529
  persistThinking: deps.config.traceThinking,
527
530
  notifiedKeys: resumeNotifiedKeys,
528
531
  ...(taskId ? { onActivity: () => recordTurnActivity(taskId) } : {}),
532
+ instructionsSegment: instructionLeg.segment,
529
533
  });
530
534
  const vAppend = (type, data) => verifySink.append(type, data);
535
+ cancelLedgerAppend = vAppend;
531
536
  const verifyTailContentMode = resumeTaskConfig.forwardSubagentEvents === true ? "on" : "progress_only";
532
537
  const verifyInternals = {
533
538
  ...(taskId ? { onActivity: () => recordTurnActivity(taskId) } : {}),
@@ -576,7 +581,9 @@ export function createResumeLegs(ctx) {
576
581
  : {}),
577
582
  };
578
583
  resumeLegLive = true;
579
- const vr = await resumeWithVerification(legRunner, token, outcome, { ...resumeTaskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, resumeObjective, verifyRounds, verifyInternals);
584
+ const vrEngine = await resumeWithVerification(legRunner, token, outcome, { ...resumeTaskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, resumeObjective, verifyRounds, verifyInternals);
585
+ const vrCause = cancelCtrl.cause();
586
+ const vr = vrCause !== undefined && taskId !== undefined ? mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: "cancelled by user", engineResult: vrEngine, cause: vrCause }) : vrEngine;
580
587
  await commitResumeCtx();
581
588
  let safe = sealResultForExit(vr);
582
589
  const vrNested = nestedPauseOf(vr.terminal);
@@ -619,6 +626,7 @@ export function createResumeLegs(ctx) {
619
626
  const persistThinking = deps.config.traceThinking;
620
627
  const serialAppend = createSerialLedgerAppend((s, type, data) => rs.appendEvent(taskId, s, type, data), startSeq);
621
628
  const append = (type, data) => (rs && taskId ? serialAppend(type, data) : Promise.resolve());
629
+ cancelLedgerAppend = append;
622
630
  unregisterEngineNotice = registerEngineNoticeLeg({
623
631
  sessionId,
624
632
  ...(args.engineNotices !== undefined && args.engineNotices.length > 0 ? { pending: [...args.engineNotices] } : {}),
@@ -712,7 +720,10 @@ export function createResumeLegs(ctx) {
712
720
  if (!taskId || !rs)
713
721
  return sealResultForExit(await stream.result());
714
722
  onAccepted?.(acceptedResumingReceipt(taskId, sessionId));
723
+ let causeAtDone;
715
724
  for await (const ev of stream) {
725
+ if (ev.type === "done")
726
+ causeAtDone = cancelCtrl.cause();
716
727
  recordTurnActivity(taskId);
717
728
  fleetPub?.onEvent(ev);
718
729
  await appendPromptManifest(append, deps.promptManifests, taskId);
@@ -771,7 +782,7 @@ export function createResumeLegs(ctx) {
771
782
  break;
772
783
  case "wiring_manifest":
773
784
  await flush();
774
- await append("wiring_manifest", wiringManifestEventData(ev));
785
+ await append("wiring_manifest", wiringManifestEventData(ev, instructionLeg.segment()));
775
786
  break;
776
787
  case "tool_roster_delta": {
777
788
  await flush();
@@ -820,7 +831,8 @@ export function createResumeLegs(ctx) {
820
831
  }
821
832
  }
822
833
  await flush();
823
- const r = await stream.result();
834
+ const rEngine = await stream.result();
835
+ const r = causeAtDone !== undefined ? mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: "cancelled by user", engineResult: rEngine, cause: causeAtDone }) : rEngine;
824
836
  const rPark = r.terminal.kind === "paused" ? outwardPlane(r.terminal).status : undefined;
825
837
  if (rPark === "suspended") {
826
838
  const parkCallId = await parkToolCallId(r, deps.checkpointStore);
@@ -875,7 +887,7 @@ export function createResumeLegs(ctx) {
875
887
  return await (deps.sendUserFile && taskId
876
888
  ? deps.sendUserFile.runWithContext({ taskId, emit: async (frame) => { const { type, ...rest } = frame; await append(type, rest); } }, withElicitation)
877
889
  : withElicitation());
878
- }));
890
+ })));
879
891
  }
880
892
  catch (e) {
881
893
  if (e instanceof CheckpointError) {
@@ -895,7 +907,11 @@ export function createResumeLegs(ctx) {
895
907
  }
896
908
  }
897
909
  if (claimedRow && taskId && deps.runStore) {
898
- const c = mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: e.message });
910
+ const handleCause = cancelCtrl.cause();
911
+ if (handleCause === undefined)
912
+ recordFailOpen("server.runs.abort-cause-missing", `task=${taskId} arm=resume-aborted`);
913
+ const c = mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: e.message, cause: handleCause ?? "cancel_verb" });
914
+ await cancelLedgerAppend?.("failed", buildCancelledLedgerRow(c)).catch((err) => recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=resume-append-resume-aborted ${errorText(err)}`));
899
915
  await deps.runStore.setTerminal(taskId, "failed", c, e.message).catch(() => undefined);
900
916
  }
901
917
  settleFleet("failed");
@@ -950,9 +966,11 @@ export function createResumeLegs(ctx) {
950
966
  },
951
967
  };
952
968
  }
953
- if (cancelCtrl.signal.aborted) {
969
+ const thrownCause = cancelCtrl.cause();
970
+ if (thrownCause !== undefined) {
954
971
  if (taskId && deps.runStore) {
955
- const c = mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: "cancelled by user" });
972
+ const c = mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: "cancelled by user", cause: thrownCause });
973
+ await cancelLedgerAppend?.("failed", buildCancelledLedgerRow(c)).catch((err) => recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=resume-append-thrown-cancel ${errorText(err)}`));
956
974
  await deps.runStore.setTerminal(taskId, "failed", c, "cancelled by user").catch(() => undefined);
957
975
  }
958
976
  settleFleet("failed");
@@ -999,6 +1017,7 @@ export function createResumeLegs(ctx) {
999
1017
  settleFleet(checkpointReopened ? "suspended" : resumePlane.status, fleetRunResiduals(sealed));
1000
1018
  return {
1001
1019
  status: 200,
1020
+ decisionCommitted: true,
1002
1021
  body: {
1003
1022
  taskId, sessionId, status: resumePlane.status,
1004
1023
  bindingEnforced: true,
@@ -1298,7 +1317,7 @@ export function createResumeLegs(ctx) {
1298
1317
  }
1299
1318
  const routeCtxBase = {
1300
1319
  deps,
1301
- registry: { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters },
1320
+ registry: { idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters },
1302
1321
  helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
1303
1322
  local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal(), capabilities: createCapabilitiesLocal() },
1304
1323
  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` 拆不出闭包)—— 装进盒子才能跨模块自增/自减。 */
@@ -380,15 +383,9 @@ export interface RouteLegs {
380
383
  decider?: {
381
384
  principal?: string;
382
385
  explicitOperator: boolean;
383
- }): Promise<{
384
- status: number;
385
- body: object;
386
- }>;
386
+ }): Promise<ResumeLegOutcome>;
387
387
  /** 全 resume 家族的共同下半场(lease admission + 写 run log + fleet 发布)。 */
388
- driveResumeIntoRunLog(args: DriveResumeArgs): Promise<{
389
- status: number;
390
- body: object;
391
- }>;
388
+ driveResumeIntoRunLog(args: DriveResumeArgs): Promise<ResumeLegOutcome>;
392
389
  /** assistant 抢占腿的复位(gate-kind 守卫 + 行级属主复核 + 驱动)。 */
393
390
  resumePreempted(sessionId: string, req: IncomingMessage | undefined,
394
391
  /** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/resume` 腿传 `true`。 */
@@ -628,6 +625,26 @@ export type RouteDecl = RouteDispatchDecl | RouteGateDecl;
628
625
  * 两臂逐字节同形,指路价值不丢。钉=audit-fixes-http「[ref]」格(两臂全文比对);文案变更走
629
626
  * api-error-text-freeze 基线流程。
630
627
  */
628
+ /**
629
+ * resume 家族一条腿的结局:HTTP 应答(`status` / `body`)+ 一位**进程内**事实(不上 wire)。
630
+ *
631
+ * `decisionCommitted` —— **这条腿的终局是在引擎受理了这次决定之后才结出来的**([ref] X5,7.112.0):本腿走到了「引擎交回结果」那一步
632
+ * (`resumeStream` / `resumeWithVerification` 解析 = core 的全部 pre-CAS 门 + checkpoint resolve CAS 都过了 —— 受理点的判据属主
633
+ * = {@link DriveResumeArgs.acceptEarly} 顶注),之后才收场。只由 `driveResumeLeg` 走完引擎那一步的终局出口铸;`CheckpointError` 臂、
634
+ * 「中止抛穿」臂、一切 4xx / 拒绝出口一律**不铸**(缺席 ⇒ 证不出提交,读者按「没提交」读 —— fail-closed)。
635
+ *
636
+ * 读者:`/decide` 的两道「这次决定提交了没有」门 —— [ref] 迟到结算留痕(`noteLateSettleIfAccepted`)与 remember 豁免落店
637
+ * (`routes/approvals-assistant.ts`)。它们原先只读 wire 体的 `errorCode !== "cancelled"`(「取消 = 决定没提交」),S-947 给续跑腿补了
638
+ * 「引擎受理之后才被取消」的取消终局之后,`cancelled` 不再等价「没提交」⇒ 两门改读本位:`200 ∧ (errorCode≠cancelled ∨ decisionCommitted)`。
639
+ * 🔴 为什么不放进 `result.cancelContext`(主会话初稿的落点):那只快照的契约是「分诊材料,不是状态机输入:任何门 / CAS / resume 判定都不许
640
+ * 读它」(`plugins/store-contracts.ts` `RunCancelContext` 顶注)—— 这两道门正是判定;而且门读的是 `/decide` 的平面体、看不到结果对象。
641
+ * 受理与否是腿自己的控制流事实,在腿上说,零 wire 变更。
642
+ */
643
+ export interface ResumeLegOutcome {
644
+ status: number;
645
+ body: object;
646
+ decisionCommitted?: true;
647
+ }
631
648
  /**
632
649
  * **「决定/续跑已受理」那一枚 200 收据的单一铸点**(S-218)。
633
650
  *
@@ -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;
@@ -148,7 +148,7 @@ async function acceptLiveUserInput(deps, res, at) {
148
148
  }
149
149
  export async function handleRuns(req, res, match, ctx) {
150
150
  const { deps } = ctx;
151
- const { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints } = ctx.registry;
151
+ const { idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints } = ctx.registry;
152
152
  const { readJson, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied, runOwnerOk, runSessionAcceptOk } = ctx.helpers;
153
153
  const { prepareSpec } = ctx.legs;
154
154
  const source = ctx.req.source;
@@ -319,7 +319,7 @@ export async function handleRuns(req, res, match, ctx) {
319
319
  }
320
320
  const err = note;
321
321
  try {
322
- const result = mintCancelledResult({ taskId, sessionId: run.sessionId, startedAtMs: Date.parse(run.createdAt), message: err });
322
+ const result = mintCancelledResult({ taskId, sessionId: run.sessionId, startedAtMs: Date.parse(run.createdAt), message: err, cause: "cancel_verb" });
323
323
  let lastErr;
324
324
  for (let attempt = 0;; attempt++) {
325
325
  try {
@@ -368,10 +368,7 @@ export async function handleRuns(req, res, match, ctx) {
368
368
  sendJson(res, 202, { taskId, status: now?.status ?? "failed", cancelRequested: now?.cancelRequested ?? false, note: "run already terminal — cancel is a no-op" });
369
369
  return;
370
370
  }
371
- if (inflightRuns.has(taskId)) {
372
- cancelledViaVerb.add(taskId);
373
- inflightRuns.get(taskId).abort();
374
- }
371
+ inflightRuns.get(taskId)?.abort("cancel_verb");
375
372
  sendJson(res, 202, { taskId, status: "cancelling", cancelRequested: true });
376
373
  }
377
374
  else if (isParkedRunStatus(run.status)) {
@@ -1155,6 +1152,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1155
1152
  }
1156
1153
  const handle = resolution?.handle;
1157
1154
  if (handle) {
1155
+ let acceptance;
1158
1156
  try {
1159
1157
  if (verb === "resume") {
1160
1158
  if (!handle.resume) {
@@ -1170,11 +1168,23 @@ export async function handleRunVerbs(req, res, match, ctx) {
1170
1168
  sendJson(res, 200, { taskId: runId, target, status: "running", delivery: "applied", marker, note: `Message queued for delivery to ${handle.agentName ?? target}; it will continue in the background.` });
1171
1169
  return;
1172
1170
  }
1171
+ const messageId = uuidv7();
1172
+ const accepted = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId: runId, leg: handle });
1173
+ if (accepted === "refused")
1174
+ return;
1175
+ acceptance = accepted;
1173
1176
  const marker = await handle.steer(redacted);
1174
- sendJson(res, 200, { taskId: runId, target, status: "running", delivery: "applied", marker, note: `Message queued for delivery to ${handle.agentName ?? target} at its next tool round.` });
1177
+ if (acceptance !== undefined) {
1178
+ const ledger = deps.userInputLedger;
1179
+ const settle = () => ledger.settleLeg(handle);
1180
+ void handle.settled.then(settle, settle);
1181
+ }
1182
+ sendJson(res, 200, { taskId: runId, target, status: "running", delivery: "applied", messageId, marker, note: `Message queued for delivery to ${handle.agentName ?? target} at its next tool round.` });
1175
1183
  return;
1176
1184
  }
1177
1185
  catch (e) {
1186
+ if (acceptance !== undefined)
1187
+ await deps.userInputLedger.withdraw(acceptance);
1178
1188
  const code = e.code;
1179
1189
  if (code === "steering.not_running") {
1180
1190
  send409(code, "sub-agent just finished — no longer accepting steers");
@@ -11,8 +11,9 @@
11
11
  * · {@link handleTasksBody} 内的 `runStreamSubmitLeg` —— plain-stream 腿(原 703 行匿名体);
12
12
  * · `runSyncSubmitLeg` —— 同步腿(原 128 行匿名体);
13
13
  * · 同步腿内 `verifyLeg` / `cascadeLeg` / `plainLeg` —— 三条模型驱动腿,判别顺序与短路语义逐字不变。
14
- * 提名**只改名字不改行为**:闭包捕获的自由变量、语句顺序、`cancelledViaVerb` 的 first-writer-wins 取值时点
15
- * ([1.207 codex H2]:在 `inflightRuns` 身份守卫内先取后删)、settle/registry 生命周期注释全部随代码原样迁移。
14
+ * 提名**只改名字不改行为**:闭包捕获的自由变量、语句顺序、settle/registry 生命周期注释全部随代码原样迁移。
15
+ * (S-947 起「这次是不是被 server 中止、为什么」读每腿自己的中止句柄 `RunAbort`,不再有按 taskId 键控的
16
+ * `cancelledViaVerb` 旁表与它「身份守卫内先取后删」的取值时点。)
16
17
  *
17
18
  * 与 runs 域共用 `ctx.legs.prepareSpec`(读体/校验/建 spec 的同一条前段)。
18
19
  */