@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.
- package/MIGRATION.md +68 -0
- package/USAGE.md +3 -3
- package/dist/approval-ask-audit-store.d.ts +3 -4
- package/dist/approval-ask-audit-store.js +3 -3
- package/dist/approval-card.d.ts +4 -4
- package/dist/boot/coordinators.js +1 -1
- package/dist/boot/stores.js +4 -6
- package/dist/boot/workflow-orchestration.js +4 -4
- package/dist/config-types.d.ts +1 -1
- package/dist/config.js +2 -1
- package/dist/hooks/hook-runner.d.ts +7 -2
- package/dist/hooks/hook-runner.js +48 -4
- package/dist/host-decision.d.ts +5 -3
- package/dist/http/admission.js +12 -3
- package/dist/http/request-guards.js +2 -2
- package/dist/http/resume-legs.d.ts +14 -24
- package/dist/http/resume-legs.js +91 -29
- package/dist/http/route-ctx.d.ts +64 -17
- package/dist/http/route-ctx.js +4 -0
- package/dist/http/routes/approvals-assistant.js +1 -1
- package/dist/http/routes/background.js +2 -2
- package/dist/http/routes/mcp-reconnect.js +2 -2
- package/dist/http/routes/notify-wake.js +3 -3
- package/dist/http/routes/runs.js +22 -19
- package/dist/http/routes/session-sync.js +6 -6
- package/dist/http/routes/sessions.js +20 -20
- package/dist/http/routes/tasks.d.ts +3 -2
- package/dist/http/routes/tasks.js +38 -45
- package/dist/http/send.d.ts +1 -1
- package/dist/http/send.js +1 -1
- package/dist/http/server.js +1 -2
- package/dist/http/wire-types.d.ts +1 -1
- package/dist/local-data-root.d.ts +14 -0
- package/dist/local-data-root.js +5 -0
- package/dist/observability/corrupt-read-seat.d.ts +46 -66
- package/dist/observability/corrupt-read-seat.js +74 -59
- package/dist/observability/fail-open.d.ts +29 -1
- package/dist/observability/fail-open.js +26 -1
- package/dist/observability/run-terminal.js +39 -0
- package/dist/orchestration/workflow-agent-session-index.d.ts +1 -5
- package/dist/orchestration/workflow-agent-session-index.js +2 -2
- package/dist/orchestration/workflow-notify-journal.d.ts +1 -3
- package/dist/orchestration/workflow-notify-journal.js +2 -2
- package/dist/plugins/approval-ask-store-file.d.ts +1 -4
- package/dist/plugins/approval-ask-store-file.js +2 -2
- package/dist/plugins/file-resume-anchor-store.d.ts +1 -6
- package/dist/plugins/file-resume-anchor-store.js +2 -4
- package/dist/plugins/file-run-store.d.ts +0 -4
- package/dist/plugins/file-run-store.js +1 -3
- package/dist/plugins/send-file-ledger.d.ts +1 -5
- package/dist/plugins/send-file-ledger.js +2 -2
- package/dist/plugins/store-backend.js +8 -9
- package/dist/plugins/store-contracts.d.ts +40 -7
- package/dist/plugins/store-contracts.js +1 -0
- package/dist/plugins/user-input-ledger-file.d.ts +1 -5
- package/dist/plugins/user-input-ledger-file.js +2 -2
- package/dist/run-cancel-context.d.ts +59 -7
- package/dist/run-cancel-context.js +26 -0
- package/dist/run-local.d.ts +1 -1
- package/dist/run-local.js +2 -2
- package/dist/runs.d.ts +7 -5
- package/dist/runs.js +18 -13
- package/dist/spec-fields.js +0 -1
- package/dist/tool-approval.d.ts +1 -1
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/project.js +2 -3
- package/dist/trace/redact.d.ts +18 -0
- package/dist/trace/redact.js +4 -0
- package/dist/trace/self-minted-wire.d.ts +22 -0
- package/dist/trace/self-minted-wire.js +8 -0
- package/dist/unattended-permission-mode.d.ts +9 -2
- package/dist/unattended-permission-mode.js +5 -1
- package/dist/user-input-ledger.d.ts +24 -5
- package/dist/user-input-ledger.js +46 -11
- package/package.json +4 -3
- package/scripts/retired-mode-seed-drain-local.mjs +317 -0
package/dist/http/resume-legs.js
CHANGED
|
@@ -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
|
|
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,
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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 },
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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])
|
|
73
|
-
*
|
|
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 {};
|
package/dist/http/route-ctx.js
CHANGED
|
@@ -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",
|
|
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",
|
|
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",
|
|
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",
|
|
71
|
+
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
const payload = {
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
1176
|
-
if (
|
|
1181
|
+
const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId: runId, leg: handle });
|
|
1182
|
+
if (acceptance === "refused")
|
|
1177
1183
|
return;
|
|
1178
|
-
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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" });
|