@sema-agent/server 7.111.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.
- package/MIGRATION.md +8 -0
- package/dist/hooks/hook-runner.d.ts +5 -0
- package/dist/hooks/hook-runner.js +45 -1
- package/dist/http/admission.js +6 -1
- package/dist/http/resume-legs.d.ts +6 -15
- package/dist/http/resume-legs.js +27 -11
- package/dist/http/route-ctx.d.ts +28 -11
- package/dist/http/routes/approvals-assistant.js +1 -1
- package/dist/http/routes/runs.js +3 -6
- package/dist/http/routes/tasks.d.ts +3 -2
- package/dist/http/routes/tasks.js +37 -45
- package/dist/http/server.js +1 -2
- package/dist/observability/fail-open.d.ts +5 -1
- package/dist/observability/fail-open.js +2 -1
- package/dist/plugins/store-contracts.d.ts +40 -7
- package/dist/plugins/store-contracts.js +1 -0
- package/dist/run-cancel-context.d.ts +59 -7
- package/dist/run-cancel-context.js +26 -0
- package/dist/runs.d.ts +7 -5
- package/dist/runs.js +17 -13
- package/dist/trace/self-minted-wire.d.ts +22 -0
- package/dist/trace/self-minted-wire.js +8 -0
- package/package.json +3 -3
package/MIGRATION.md
CHANGED
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
> ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
|
|
8
8
|
> 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
|
|
9
9
|
|
|
10
|
+
## 7.112.0(车UG,S-924)—— `settings.hooks` 里按工具别名写的 matcher 提交当场 400(此前静默不命中)
|
|
11
|
+
|
|
12
|
+
- **🔴 settings 面硬破坏(响亮拒,无迁移代码、无兼容形)**。① *谁受影响*:提交体 `settings.hooks` 里 PreToolUse / PostToolUse / PostToolUseFailure 的 `matcher`
|
|
13
|
+
是**引擎工具别名**的(今天只有 `ListPeers` → `ListAgents`;别名表读 core 静态工具目录)—— 它们**早就不命中**(post 两事件自 7.107.0 / core 7.36.0,PreToolUse 一直),只是没人告诉写的人。
|
|
14
|
+
② *怎么改*:把 matcher 改成**正名**(错误句直接给出);`正名|别名` 的滚动期写法不受影响(命中正名)。③ *不改会怎样*:该**新提交** `400 request.field_invalid`(错误句点名别名与正名)——
|
|
15
|
+
不再是「接受、hook 永不跑」。耐久 resume 重放**不判**(会砖 parked 任务):升级前就 parked 的任务,其存量 body 里的别名 matcher 在续跑腿照旧永不命中、同一份 hooks 里其余钩子照常生效(与 7.111.0 逐字同)。
|
|
16
|
+
嵌入方经 `resolveSpec` 席挂的工具别名在受理面看不见,不在本门射程。
|
|
17
|
+
|
|
10
18
|
## 7.111.0(车TA,S-961 / S-972)—— `wiring_manifest` 帧 +`instructions` 段(additive,零动作)
|
|
11
19
|
|
|
12
20
|
- **wire(additive,零动作)**:`wiring_manifest` 帧(四条腿:流式 SSE / 账本行 / 非流式 `wiringManifest` 回执 / 续跑腿)多一段本服务自铸的 `instructions: { selected: string[] }` = 这条腿选中的项目指令文件(相对 cwd 的链成员);`[]` = 没有,缺席 = 无陈述(契约附录 G.21)。按封闭 schema 校验该帧的消费方加这一可选键;不要它的部署 `PROJECT_MEMORY_ENABLED=false`(连同项目记忆注入一起关)。`task.user_steer_undrained{cause:"crashed"}` 的 `message` 改句序(行动句在前、< 400 字符),`detail` 不变 —— 按文本匹配的脚本改读 `detail.cause`(S-972)。
|
|
@@ -35,6 +35,11 @@ export interface ParseHooksConfigResult {
|
|
|
35
35
|
/** 人可读的第一条校验错误(HTTP 层用它 400 fail-loud;resume 防御路径用它 warn)。 */
|
|
36
36
|
error?: string;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* fresh 提交面专用判据(S-924):配置里第一条「永远打不中」的工具事件 matcher 的拒句;没有 ⇒ undefined。
|
|
40
|
+
* 入参是 {@link parseHooksConfig} 已校验过的配置。**只有 `admission.ts` 的 fresh 提交门调它** —— resume 重放不调(理由见上面的头注)。
|
|
41
|
+
*/
|
|
42
|
+
export declare function deadToolMatcherIssue(config: HooksConfig): string | undefined;
|
|
38
43
|
/**
|
|
39
44
|
* 把 wire 上的 untrusted `settings.hooks` 校验成契约 {@link HooksConfig} + 服务侧上限。
|
|
40
45
|
* 错误返回 error 字符串而不 throw——submit 路径 400 fail-loud、resume 防御路径 warn-drop,由调用方选。
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { hostShell, resolveHostShell, adoptHostForegroundChild } from "../plugins/host-platform.js";
|
|
3
|
-
import { HooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_EVENT_OWNER, } from "@sema-agent/settings-schema/hooks";
|
|
3
|
+
import { HooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_EVENT_OWNER, HOOK_EVENT_MATCHER_FIELD, } from "@sema-agent/settings-schema/hooks";
|
|
4
|
+
import { describeToolCatalog } from "@sema-agent/core";
|
|
4
5
|
import { boundedNoticeText } from "../fleet/fleet-bus.js";
|
|
5
6
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
6
7
|
import { redactSecrets, buildDisplaySafeUrl } from "../trace/redact.js";
|
|
@@ -56,6 +57,49 @@ function toAssistantTurnError(errorKind) {
|
|
|
56
57
|
return "unknown";
|
|
57
58
|
return Object.prototype.hasOwnProperty.call(BRAIN_TO_TURN_ERROR, errorKind) ? (BRAIN_TO_TURN_ERROR[errorKind] ?? "unknown") : "unknown";
|
|
58
59
|
}
|
|
60
|
+
const NAME_ALTERNATION = /^[A-Za-z0-9_-]+(?:\|[A-Za-z0-9_-]+)*$/;
|
|
61
|
+
function matcherReadsWireToolName(event) {
|
|
62
|
+
return HOOK_EVENT_OWNER[event] === "engine" && HOOK_EVENT_MATCHER_FIELD[event] === "tool_name";
|
|
63
|
+
}
|
|
64
|
+
let engineToolAliasTable;
|
|
65
|
+
function engineToolAliases() {
|
|
66
|
+
if (engineToolAliasTable !== undefined)
|
|
67
|
+
return engineToolAliasTable;
|
|
68
|
+
const entries = describeToolCatalog();
|
|
69
|
+
const wireNames = new Set(entries.map((e) => e.name));
|
|
70
|
+
const table = new Map();
|
|
71
|
+
for (const e of entries) {
|
|
72
|
+
for (const alias of e.face.aliases ?? []) {
|
|
73
|
+
if (!wireNames.has(alias) && !table.has(alias))
|
|
74
|
+
table.set(alias, e.name);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
engineToolAliasTable = table;
|
|
78
|
+
return table;
|
|
79
|
+
}
|
|
80
|
+
function deadAliasInMatcher(event, matcher) {
|
|
81
|
+
if (matcher === undefined || !matcherReadsWireToolName(event) || !NAME_ALTERNATION.test(matcher))
|
|
82
|
+
return undefined;
|
|
83
|
+
const branches = matcher.split("|");
|
|
84
|
+
const aliases = engineToolAliases();
|
|
85
|
+
for (const branch of branches) {
|
|
86
|
+
const canonical = aliases.get(branch);
|
|
87
|
+
if (canonical !== undefined && !branches.includes(canonical))
|
|
88
|
+
return { alias: branch, canonical };
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
export function deadToolMatcherIssue(config) {
|
|
93
|
+
for (const [event, groups] of Object.entries(config)) {
|
|
94
|
+
for (const group of groups ?? []) {
|
|
95
|
+
const dead = deadAliasInMatcher(event, group.matcher);
|
|
96
|
+
if (dead) {
|
|
97
|
+
return `settings.hooks.${event}: matcher "${dead.alias}" is an alias; tool-event matchers match the tool's wire name (PostToolUse / PostToolUseFailure since 7.107.0) — use "${dead.canonical}"`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
59
103
|
export function parseHooksConfig(raw) {
|
|
60
104
|
const parsed = HooksConfig.safeParse(raw);
|
|
61
105
|
if (!parsed.success) {
|
package/dist/http/admission.js
CHANGED
|
@@ -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 字段
|
|
11
|
-
*
|
|
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
|
-
|
|
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;
|
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";
|
|
@@ -75,7 +75,7 @@ function notResumableAnswer() {
|
|
|
75
75
|
return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
|
|
76
76
|
}
|
|
77
77
|
export function createResumeLegs(ctx) {
|
|
78
|
-
const { deps, runnerFor, idemCache, inflightRuns, preemptableRuns,
|
|
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;
|
|
79
79
|
const resumeFleetScope = (req, auth) => req ? (gatedPrincipal(req, deps.config) ?? "default") : (auth.principal ?? "default");
|
|
80
80
|
async function resumeCheckpoint(sessionId, decision, reason, hostDecision, req, answer, binding, onResumeCommitted, acceptEarly, decider) {
|
|
81
81
|
const cs = deps.checkpointStore;
|
|
@@ -225,7 +225,7 @@ export function createResumeLegs(ctx) {
|
|
|
225
225
|
const noteLateSettleIfAccepted = (out) => {
|
|
226
226
|
if (lateSettleSink === undefined || lateSettleAnchor === undefined || out.status !== 200)
|
|
227
227
|
return;
|
|
228
|
-
if (out.body?.errorCode === "cancelled")
|
|
228
|
+
if (out.body?.errorCode === "cancelled" && out.decisionCommitted !== true)
|
|
229
229
|
return;
|
|
230
230
|
try {
|
|
231
231
|
lateSettleSink.recordLateSettle({ ...lateSettleAnchor, outcome: decision === "approve" ? "allowed" : "denied", decidedBy: hostDecision.decidedBy, ...(hostDecision.approver !== undefined ? { approver: hostDecision.approver } : {}), settledAtMs: Date.now() });
|
|
@@ -472,7 +472,8 @@ export function createResumeLegs(ctx) {
|
|
|
472
472
|
let resumeLegLive = false;
|
|
473
473
|
const subagentHandleEvictions = [];
|
|
474
474
|
let claimedRow = false;
|
|
475
|
-
const cancelCtrl =
|
|
475
|
+
const cancelCtrl = createRunAbort();
|
|
476
|
+
let cancelLedgerAppend;
|
|
476
477
|
const preemptCtrl = new AbortController();
|
|
477
478
|
if (taskId && deps.runStore) {
|
|
478
479
|
claimedRow = freshRunClaimed ? true : await deps.runStore.markResuming(taskId, deps.instanceId ?? "default");
|
|
@@ -488,7 +489,7 @@ export function createResumeLegs(ctx) {
|
|
|
488
489
|
void rs.heartbeat(taskId, owner).catch(() => undefined);
|
|
489
490
|
if (!cancelCtrl.signal.aborted) {
|
|
490
491
|
void rs.isCancelRequested(taskId, owner).then((c) => { if (c)
|
|
491
|
-
cancelCtrl.abort(); }).catch(() => undefined);
|
|
492
|
+
cancelCtrl.abort("cancel_verb"); }).catch(() => undefined);
|
|
492
493
|
}
|
|
493
494
|
if (!preemptCtrl.signal.aborted) {
|
|
494
495
|
void rs.isPreemptRequested(taskId, owner).then((p) => { if (p)
|
|
@@ -531,6 +532,7 @@ export function createResumeLegs(ctx) {
|
|
|
531
532
|
instructionsSegment: instructionLeg.segment,
|
|
532
533
|
});
|
|
533
534
|
const vAppend = (type, data) => verifySink.append(type, data);
|
|
535
|
+
cancelLedgerAppend = vAppend;
|
|
534
536
|
const verifyTailContentMode = resumeTaskConfig.forwardSubagentEvents === true ? "on" : "progress_only";
|
|
535
537
|
const verifyInternals = {
|
|
536
538
|
...(taskId ? { onActivity: () => recordTurnActivity(taskId) } : {}),
|
|
@@ -579,7 +581,9 @@ export function createResumeLegs(ctx) {
|
|
|
579
581
|
: {}),
|
|
580
582
|
};
|
|
581
583
|
resumeLegLive = true;
|
|
582
|
-
const
|
|
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;
|
|
583
587
|
await commitResumeCtx();
|
|
584
588
|
let safe = sealResultForExit(vr);
|
|
585
589
|
const vrNested = nestedPauseOf(vr.terminal);
|
|
@@ -622,6 +626,7 @@ export function createResumeLegs(ctx) {
|
|
|
622
626
|
const persistThinking = deps.config.traceThinking;
|
|
623
627
|
const serialAppend = createSerialLedgerAppend((s, type, data) => rs.appendEvent(taskId, s, type, data), startSeq);
|
|
624
628
|
const append = (type, data) => (rs && taskId ? serialAppend(type, data) : Promise.resolve());
|
|
629
|
+
cancelLedgerAppend = append;
|
|
625
630
|
unregisterEngineNotice = registerEngineNoticeLeg({
|
|
626
631
|
sessionId,
|
|
627
632
|
...(args.engineNotices !== undefined && args.engineNotices.length > 0 ? { pending: [...args.engineNotices] } : {}),
|
|
@@ -715,7 +720,10 @@ export function createResumeLegs(ctx) {
|
|
|
715
720
|
if (!taskId || !rs)
|
|
716
721
|
return sealResultForExit(await stream.result());
|
|
717
722
|
onAccepted?.(acceptedResumingReceipt(taskId, sessionId));
|
|
723
|
+
let causeAtDone;
|
|
718
724
|
for await (const ev of stream) {
|
|
725
|
+
if (ev.type === "done")
|
|
726
|
+
causeAtDone = cancelCtrl.cause();
|
|
719
727
|
recordTurnActivity(taskId);
|
|
720
728
|
fleetPub?.onEvent(ev);
|
|
721
729
|
await appendPromptManifest(append, deps.promptManifests, taskId);
|
|
@@ -823,7 +831,8 @@ export function createResumeLegs(ctx) {
|
|
|
823
831
|
}
|
|
824
832
|
}
|
|
825
833
|
await flush();
|
|
826
|
-
const
|
|
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;
|
|
827
836
|
const rPark = r.terminal.kind === "paused" ? outwardPlane(r.terminal).status : undefined;
|
|
828
837
|
if (rPark === "suspended") {
|
|
829
838
|
const parkCallId = await parkToolCallId(r, deps.checkpointStore);
|
|
@@ -898,7 +907,11 @@ export function createResumeLegs(ctx) {
|
|
|
898
907
|
}
|
|
899
908
|
}
|
|
900
909
|
if (claimedRow && taskId && deps.runStore) {
|
|
901
|
-
const
|
|
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)}`));
|
|
902
915
|
await deps.runStore.setTerminal(taskId, "failed", c, e.message).catch(() => undefined);
|
|
903
916
|
}
|
|
904
917
|
settleFleet("failed");
|
|
@@ -953,9 +966,11 @@ export function createResumeLegs(ctx) {
|
|
|
953
966
|
},
|
|
954
967
|
};
|
|
955
968
|
}
|
|
956
|
-
|
|
969
|
+
const thrownCause = cancelCtrl.cause();
|
|
970
|
+
if (thrownCause !== undefined) {
|
|
957
971
|
if (taskId && deps.runStore) {
|
|
958
|
-
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)}`));
|
|
959
974
|
await deps.runStore.setTerminal(taskId, "failed", c, "cancelled by user").catch(() => undefined);
|
|
960
975
|
}
|
|
961
976
|
settleFleet("failed");
|
|
@@ -1002,6 +1017,7 @@ export function createResumeLegs(ctx) {
|
|
|
1002
1017
|
settleFleet(checkpointReopened ? "suspended" : resumePlane.status, fleetRunResiduals(sealed));
|
|
1003
1018
|
return {
|
|
1004
1019
|
status: 200,
|
|
1020
|
+
decisionCommitted: true,
|
|
1005
1021
|
body: {
|
|
1006
1022
|
taskId, sessionId, status: resumePlane.status,
|
|
1007
1023
|
bindingEnforced: true,
|
|
@@ -1301,7 +1317,7 @@ export function createResumeLegs(ctx) {
|
|
|
1301
1317
|
}
|
|
1302
1318
|
const routeCtxBase = {
|
|
1303
1319
|
deps,
|
|
1304
|
-
registry: { idemCache, inflightRuns, preemptableRuns,
|
|
1320
|
+
registry: { idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters },
|
|
1305
1321
|
helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
|
|
1306
1322
|
local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal(), capabilities: createCapabilitiesLocal() },
|
|
1307
1323
|
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` 拆不出闭包)—— 装进盒子才能跨模块自增/自减。 */
|
|
@@ -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;
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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)) {
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* · {@link handleTasksBody} 内的 `runStreamSubmitLeg` —— plain-stream 腿(原 703 行匿名体);
|
|
12
12
|
* · `runSyncSubmitLeg` —— 同步腿(原 128 行匿名体);
|
|
13
13
|
* · 同步腿内 `verifyLeg` / `cascadeLeg` / `plainLeg` —— 三条模型驱动腿,判别顺序与短路语义逐字不变。
|
|
14
|
-
*
|
|
15
|
-
* (
|
|
14
|
+
* 提名**只改名字不改行为**:闭包捕获的自由变量、语句顺序、settle/registry 生命周期注释全部随代码原样迁移。
|
|
15
|
+
* (S-947 起「这次是不是被 server 中止、为什么」读每腿自己的中止句柄 `RunAbort`,不再有按 taskId 键控的
|
|
16
|
+
* `cancelledViaVerb` 旁表与它「身份守卫内先取后删」的取值时点。)
|
|
16
17
|
*
|
|
17
18
|
* 与 runs 域共用 `ctx.legs.prepareSpec`(读体/校验/建 spec 的同一条前段)。
|
|
18
19
|
*/
|
|
@@ -10,7 +10,7 @@ import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotific
|
|
|
10
10
|
import { createLedgerSink } from "../../trace/ledger-sink.js";
|
|
11
11
|
import { registerEngineNoticeLeg } from "../../trace/engine-notice-wire.js";
|
|
12
12
|
import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../../turn-activity.js";
|
|
13
|
-
import { mintCancelledResult } from "../../run-cancel-context.js";
|
|
13
|
+
import { buildCancelledLedgerRow, createRunAbort, mintCancelledResult } from "../../run-cancel-context.js";
|
|
14
14
|
import { recordResourceWindow } from "../../resource-window.js";
|
|
15
15
|
import { redactSecrets } from "../../trace/redact.js";
|
|
16
16
|
import { contextUsageEventData, turnEndEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestFaceEventData, toolRosterDeltaEventData, toolProgressEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage, textEndEventData, liveIdentityFields } from "../../trace/project.js";
|
|
@@ -57,7 +57,7 @@ function sendSubmitResult(res, resp) {
|
|
|
57
57
|
const MULTI_ATTEMPT_NO_EVENT_CHANNEL_NOTICE = "verify / cascade ran with their normal multi-attempt semantics, but the synchronous /v1/tasks endpoint has no event channel — no status/progress frames exist to replay on this shape. For status / progress / delegated-child frames and the terminal done, submit via POST /v1/runs and read GET /v1/runs/:id/events; note that verify / cascade legs carry no per-turn stream there either (no wiring_manifest / text / turn_end frames — the engine runs those legs as result-returning attempts).";
|
|
58
58
|
export async function handleTasks(req, res, match, ctx) {
|
|
59
59
|
const { deps } = ctx;
|
|
60
|
-
const { idemCache, inflightRuns,
|
|
60
|
+
const { idemCache, inflightRuns, steerableRuns, counters } = ctx.registry;
|
|
61
61
|
const { rateLimited, quotaExceeded, leaseDenied, usageWindowDenied } = ctx.helpers;
|
|
62
62
|
const { prepareSpec, finalizeTaskResult } = ctx.legs;
|
|
63
63
|
const reqState = ctx.req;
|
|
@@ -136,7 +136,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
136
136
|
if (earlyDurableTid) {
|
|
137
137
|
res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", taskId: earlyDurableTid, ...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}) })}\n\n`);
|
|
138
138
|
}
|
|
139
|
-
const ac =
|
|
139
|
+
const ac = createRunAbort();
|
|
140
140
|
let closed = false;
|
|
141
141
|
let detachLogged = false;
|
|
142
142
|
let disconnectParkGrace;
|
|
@@ -158,14 +158,14 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
158
158
|
deps.logger?.info("stream_disconnect_ask_parked", { taskId: earlyDurableTid, sessionId: prepared.spec.sessionId, asks: parked, graceMs: DISCONNECT_PARK_GRACE_MS });
|
|
159
159
|
disconnectParkGrace = setTimeout(() => {
|
|
160
160
|
closed = true;
|
|
161
|
-
ac.abort();
|
|
161
|
+
ac.abort("ask_park_grace_exhausted");
|
|
162
162
|
}, DISCONNECT_PARK_GRACE_MS);
|
|
163
163
|
disconnectParkGrace.unref?.();
|
|
164
164
|
return;
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
closed = true;
|
|
168
|
-
ac.abort();
|
|
168
|
+
ac.abort("client_disconnected");
|
|
169
169
|
};
|
|
170
170
|
req.on("close", onDisconnect);
|
|
171
171
|
res.on("close", () => {
|
|
@@ -213,13 +213,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
213
213
|
let runRowSettled = false;
|
|
214
214
|
let durableTaskId;
|
|
215
215
|
let durableHeartbeat;
|
|
216
|
-
const cancelledResult = (errorMessage,
|
|
217
|
-
taskId: durableTaskId ?? "",
|
|
218
|
-
sessionId: prepared.spec.sessionId ?? "",
|
|
219
|
-
startedAtMs: streamLegStartedAt,
|
|
220
|
-
message: errorMessage,
|
|
221
|
-
...(engineResult !== undefined ? { engineResult } : {}),
|
|
222
|
-
});
|
|
216
|
+
const cancelledResult = (errorMessage, cause) => mintCancelledResult({ taskId: durableTaskId ?? "", sessionId: prepared.spec.sessionId ?? "", startedAtMs: streamLegStartedAt, message: errorMessage, cause });
|
|
223
217
|
let userMsgEntryId;
|
|
224
218
|
let anchorPut = false;
|
|
225
219
|
const putRewindAnchor = () => {
|
|
@@ -267,10 +261,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
267
261
|
usageKey = tid;
|
|
268
262
|
deps.modelUsage?.register(tid);
|
|
269
263
|
const owner = principal ?? null;
|
|
270
|
-
const abortFromVerb = () =>
|
|
271
|
-
cancelledViaVerb.add(tid);
|
|
272
|
-
ac.abort();
|
|
273
|
-
};
|
|
264
|
+
const abortFromVerb = () => ac.abort("cancel_verb");
|
|
274
265
|
durableHeartbeat = setInterval(() => {
|
|
275
266
|
void rs.heartbeat(tid, owner).catch(() => undefined);
|
|
276
267
|
if (!ac.signal.aborted) {
|
|
@@ -292,10 +283,8 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
292
283
|
catch (e) {
|
|
293
284
|
if (durableHeartbeat)
|
|
294
285
|
clearInterval(durableHeartbeat);
|
|
295
|
-
if (durableTaskId && inflightRuns.get(durableTaskId) === ac)
|
|
286
|
+
if (durableTaskId && inflightRuns.get(durableTaskId) === ac)
|
|
296
287
|
inflightRuns.delete(durableTaskId);
|
|
297
|
-
cancelledViaVerb.delete(durableTaskId);
|
|
298
|
-
}
|
|
299
288
|
throw e;
|
|
300
289
|
}
|
|
301
290
|
let fleetSettled = false;
|
|
@@ -315,10 +304,8 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
315
304
|
catch (e) {
|
|
316
305
|
if (durableHeartbeat)
|
|
317
306
|
clearInterval(durableHeartbeat);
|
|
318
|
-
if (durableTaskId && inflightRuns.get(durableTaskId) === ac)
|
|
307
|
+
if (durableTaskId && inflightRuns.get(durableTaskId) === ac)
|
|
319
308
|
inflightRuns.delete(durableTaskId);
|
|
320
|
-
cancelledViaVerb.delete(durableTaskId);
|
|
321
|
-
}
|
|
322
309
|
settleFleet("failed");
|
|
323
310
|
throw e;
|
|
324
311
|
}
|
|
@@ -430,13 +417,13 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
430
417
|
sseData(res, end);
|
|
431
418
|
if (ev.type === "message_committed" && ev.role === "user")
|
|
432
419
|
userMsgEntryId = ev.entryId;
|
|
433
|
-
const
|
|
420
|
+
const causeAtDone = ev.type === "done" ? ac.cause() : undefined;
|
|
434
421
|
if (ev.type === "done") {
|
|
435
422
|
finalResult = await mintSubmitExitResult(ev.result, deps.checkpointStore);
|
|
436
423
|
if (durableTaskId && finalResult)
|
|
437
424
|
finalResult = { ...finalResult, taskId: durableTaskId };
|
|
438
|
-
if (
|
|
439
|
-
finalResult =
|
|
425
|
+
if (causeAtDone !== undefined && finalResult !== undefined) {
|
|
426
|
+
finalResult = mintCancelledResult({ taskId: durableTaskId ?? "", sessionId: prepared.spec.sessionId ?? "", startedAtMs: streamLegStartedAt, message: "cancelled by user", cause: causeAtDone, engineResult: finalResult });
|
|
440
427
|
}
|
|
441
428
|
finalizeTaskResult(ev.result, principal, prepared.spec.objective, prepared.spec.sessionId);
|
|
442
429
|
noteRunOutcome(finalResult);
|
|
@@ -672,6 +659,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
672
659
|
else {
|
|
673
660
|
putRewindAnchor();
|
|
674
661
|
const pending = await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId);
|
|
662
|
+
const dcCause = ac.cause();
|
|
675
663
|
if (pending) {
|
|
676
664
|
if (ledgerSink && !ledgerTerminal) {
|
|
677
665
|
ledgerTerminal = true;
|
|
@@ -681,18 +669,28 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
681
669
|
await deps.runStore.setSuspended(durableTaskId, null);
|
|
682
670
|
settleFleet("suspended");
|
|
683
671
|
}
|
|
684
|
-
else {
|
|
672
|
+
else if (dcCause !== undefined) {
|
|
685
673
|
const dcErr = "stream client disconnected before completion (run aborted)";
|
|
686
|
-
const dc = cancelledResult(dcErr);
|
|
674
|
+
const dc = cancelledResult(dcErr, dcCause);
|
|
687
675
|
if (ledgerSink && !ledgerTerminal) {
|
|
688
676
|
ledgerTerminal = true;
|
|
689
677
|
await ledgerSink.flush().catch(() => undefined);
|
|
690
|
-
await ledgerSink.append("failed",
|
|
678
|
+
await ledgerSink.append("failed", buildCancelledLedgerRow(dc)).catch(() => undefined);
|
|
691
679
|
}
|
|
692
680
|
await deps.runStore.setTerminal(durableTaskId, "failed", dc, dcErr);
|
|
693
681
|
noteRunOutcome(dc);
|
|
694
682
|
settleFleet("failed");
|
|
695
683
|
}
|
|
684
|
+
else {
|
|
685
|
+
const err = "stream ended before a terminal event";
|
|
686
|
+
if (ledgerSink && !ledgerTerminal) {
|
|
687
|
+
ledgerTerminal = true;
|
|
688
|
+
await ledgerSink.flush().catch(() => undefined);
|
|
689
|
+
await ledgerSink.append("failed", { errorMessage: err }).catch(() => undefined);
|
|
690
|
+
}
|
|
691
|
+
await deps.runStore.setTerminal(durableTaskId, "failed", null, err);
|
|
692
|
+
settleFleet("failed");
|
|
693
|
+
}
|
|
696
694
|
}
|
|
697
695
|
runRowSettled = true;
|
|
698
696
|
}
|
|
@@ -706,10 +704,8 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
706
704
|
steerableRuns.delete(durableTaskId);
|
|
707
705
|
if (liveStreamRef !== undefined)
|
|
708
706
|
await deps.userInputLedger?.settleLeg(liveStreamRef);
|
|
709
|
-
if (durableTaskId && inflightRuns.get(durableTaskId) === ac)
|
|
707
|
+
if (durableTaskId && inflightRuns.get(durableTaskId) === ac)
|
|
710
708
|
inflightRuns.delete(durableTaskId);
|
|
711
|
-
cancelledViaVerb.delete(durableTaskId);
|
|
712
|
-
}
|
|
713
709
|
for (const evict of subagentHandleEvictions)
|
|
714
710
|
evict();
|
|
715
711
|
if (!runRowSettled && durableTaskId && deps.runStore) {
|
|
@@ -717,6 +713,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
717
713
|
const row = await deps.runStore.getRun(durableTaskId);
|
|
718
714
|
if (row?.status === "running") {
|
|
719
715
|
const pending = prepared.spec.sessionId ? await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId) : undefined;
|
|
716
|
+
const backstopCause = ac.cause();
|
|
720
717
|
if (pending) {
|
|
721
718
|
if (ledgerSink && !ledgerTerminal) {
|
|
722
719
|
ledgerTerminal = true;
|
|
@@ -735,13 +732,13 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
735
732
|
await deps.runStore.setTerminal(durableTaskId, retryPlane.status, finalResult, retryPlane.errorMessage ?? null);
|
|
736
733
|
settleFleet(retryPlane.status, fleetRunResiduals(finalResult));
|
|
737
734
|
}
|
|
738
|
-
else if (
|
|
735
|
+
else if (backstopCause !== undefined) {
|
|
739
736
|
const err = "stream aborted before completion (run cancelled)";
|
|
740
|
-
const c = cancelledResult(err);
|
|
737
|
+
const c = cancelledResult(err, backstopCause);
|
|
741
738
|
if (ledgerSink && !ledgerTerminal) {
|
|
742
739
|
ledgerTerminal = true;
|
|
743
740
|
await ledgerSink.flush().catch(() => undefined);
|
|
744
|
-
await ledgerSink.append("failed",
|
|
741
|
+
await ledgerSink.append("failed", buildCancelledLedgerRow(c)).catch(() => undefined);
|
|
745
742
|
}
|
|
746
743
|
await deps.runStore.setTerminal(durableTaskId, "failed", c, err);
|
|
747
744
|
}
|
|
@@ -808,17 +805,14 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
808
805
|
if (deps.checkpointStore && prepared.spec.sessionId) {
|
|
809
806
|
await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
|
|
810
807
|
}
|
|
811
|
-
const syncCancelCtrl =
|
|
808
|
+
const syncCancelCtrl = createRunAbort();
|
|
812
809
|
if (prepared.spec.sessionId)
|
|
813
810
|
markChildrenStoppedByUserOnAbort(syncCancelCtrl.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
|
|
814
811
|
if (durableTaskId && deps.runStore) {
|
|
815
812
|
const rs = deps.runStore;
|
|
816
813
|
const tid = durableTaskId;
|
|
817
814
|
const owner = principal ?? null;
|
|
818
|
-
const abortFromVerb = () =>
|
|
819
|
-
cancelledViaVerb.add(tid);
|
|
820
|
-
syncCancelCtrl.abort();
|
|
821
|
-
};
|
|
815
|
+
const abortFromVerb = () => syncCancelCtrl.abort("cancel_verb");
|
|
822
816
|
durableHeartbeat = setInterval(() => {
|
|
823
817
|
void rs.heartbeat(tid, owner).catch(() => undefined);
|
|
824
818
|
if (!syncCancelCtrl.signal.aborted) {
|
|
@@ -841,7 +835,6 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
841
835
|
wiringReceipt = wiringManifestFaceEventData(ev, wiringOperatorFace, instructionLeg.segment());
|
|
842
836
|
};
|
|
843
837
|
let result;
|
|
844
|
-
let cancelVerbLabel = false;
|
|
845
838
|
counters.uncountedBillableInflight++;
|
|
846
839
|
try {
|
|
847
840
|
const verifyLeg = (v) => runWithVerification(prepared.runner, specWithSignal, v);
|
|
@@ -858,18 +851,17 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
858
851
|
counters.uncountedBillableInflight--;
|
|
859
852
|
if (durableHeartbeat)
|
|
860
853
|
clearInterval(durableHeartbeat);
|
|
861
|
-
if (durableTaskId && inflightRuns.get(durableTaskId) === syncCancelCtrl)
|
|
854
|
+
if (durableTaskId && inflightRuns.get(durableTaskId) === syncCancelCtrl)
|
|
862
855
|
inflightRuns.delete(durableTaskId);
|
|
863
|
-
cancelVerbLabel = cancelledViaVerb.has(durableTaskId);
|
|
864
|
-
cancelledViaVerb.delete(durableTaskId);
|
|
865
|
-
}
|
|
866
856
|
}
|
|
867
|
-
|
|
857
|
+
const syncAbortCause = syncCancelCtrl.cause();
|
|
858
|
+
if (syncAbortCause !== undefined) {
|
|
868
859
|
result = mintCancelledResult({
|
|
869
860
|
taskId: durableTaskId ?? "",
|
|
870
861
|
sessionId: prepared.spec.sessionId ?? "",
|
|
871
862
|
startedAtMs: syncLegStartedAt,
|
|
872
863
|
message: "cancelled by user",
|
|
864
|
+
cause: syncAbortCause,
|
|
873
865
|
engineResult: result,
|
|
874
866
|
});
|
|
875
867
|
}
|
package/dist/http/server.js
CHANGED
|
@@ -43,7 +43,6 @@ export function createHttpServer(rawDeps) {
|
|
|
43
43
|
}
|
|
44
44
|
const idemCache = new IdempotencyCache();
|
|
45
45
|
const inflightRuns = new Map();
|
|
46
|
-
const cancelledViaVerb = new Set();
|
|
47
46
|
const preemptableRuns = new Map();
|
|
48
47
|
const steerableRuns = new Map();
|
|
49
48
|
const wakeParkMints = new Map();
|
|
@@ -97,7 +96,7 @@ export function createHttpServer(rawDeps) {
|
|
|
97
96
|
}
|
|
98
97
|
const { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, runSessionAcceptOk, readJson, readRawBody } = createRequestGuards({ deps });
|
|
99
98
|
const { prepareSpec } = createAdmission({ deps, readJson, runnerFor });
|
|
100
|
-
const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake } = createResumeLegs({ deps, runnerFor, idemCache, inflightRuns, preemptableRuns,
|
|
99
|
+
const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake } = createResumeLegs({ 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 });
|
|
101
100
|
const { denyExpiredApprovals, redeemParkedAsk } = createApprovalSweeps({ deps, resumeCheckpoint, mayDiscloseRowError });
|
|
102
101
|
const { handle } = createDispatch({ deps, noteActivity, routeCtxBase, counters });
|
|
103
102
|
const server = http.createServer((req, res) => {
|
|
@@ -281,9 +281,13 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
281
281
|
readonly cls: "F";
|
|
282
282
|
readonly note: "S-107:持久 error 脱后 UTF-8 字节 > 65,000(TiDB TEXT 65,535 字节)⇒ 脱后切并带标记(切在占位里不是泄漏);计数=多少条终态文案被字节封顶截断";
|
|
283
283
|
};
|
|
284
|
+
readonly "server.runs.abort-cause-missing": {
|
|
285
|
+
readonly cls: "F";
|
|
286
|
+
readonly note: "S-947 候裁 ⑤(主会话裁:默认值保留,miss 臂必须响):续跑腿 `http/resume-legs.ts` 的 `checkpoint.resume_aborted` 臂 —— core 只在本腿递进去的信号**已中止**时铸该码,而那只信号唯一的写者是取消动词(快路 / 耐久旗轮询,句柄登记 `cancel_verb`),所以**只有 core 契约外的形**(桩 / 非 core Runner 直接抛该码,本腿句柄从未中止)才到得了「句柄上读不到成因」这一臂。取消终局照铸,成因取本腿唯一的中止词 `cancel_verb`(取消终局恒带成因由铸点签名保证,不让它缺);detail `task=<taskId>`;计数 = 多少次取消成因是默认值而不是句柄登记的。生产上恒零是证据,不归零 = 有一条不走 core 契约的 Runner 在跑续跑腿。";
|
|
287
|
+
};
|
|
284
288
|
readonly "server.runs.terminal-ledger-write-failed": {
|
|
285
289
|
readonly cls: "F";
|
|
286
|
-
readonly note: "S-107(全量门 silent-degradation 棘轮抓出的五处静默 `catch {}`):run 终局链里**账本腿**(flush 缓冲 text/reasoning、append `failed` 行)与终态翻转(setTerminal)已不共命运——账本 INSERT 抛不再让 run 卡 running 到 reaper;但那条抛掉的账本行之前是**静默**吞的。detail `arm=` 四臂(flush-clean-cancel / append-clean-cancel / flush-catch / append-thrown-cancel / append-failed):缺的是 run_events 尾巴(缓冲文本或 failed 事件),run 行本身仍是 durable truth,读面/reaper 容忍缺失,故 F 类;计数=多少次终局账本写失败。";
|
|
290
|
+
readonly note: "S-107(全量门 silent-degradation 棘轮抓出的五处静默 `catch {}`):run 终局链里**账本腿**(flush 缓冲 text/reasoning、append `failed` 行)与终态翻转(setTerminal)已不共命运——账本 INSERT 抛不再让 run 卡 running 到 reaper;但那条抛掉的账本行之前是**静默**吞的。detail `arm=` 四臂(flush-clean-cancel / append-clean-cancel / flush-catch / append-thrown-cancel / append-failed);S-947 起续跑腿(`http/resume-legs.ts`)两座同 tag:resume-append-resume-aborted / resume-append-thrown-cancel(取消抛出两臂的 `failed` 行,修前那两臂根本不落账本终局行):缺的是 run_events 尾巴(缓冲文本或 failed 事件),run 行本身仍是 durable truth,读面/reaper 容忍缺失,故 F 类;计数=多少次终局账本写失败。";
|
|
287
291
|
};
|
|
288
292
|
readonly "server.redact.ppk-eat-to-end": {
|
|
289
293
|
readonly cls: "F";
|
|
@@ -260,7 +260,8 @@ export const FAIL_OPEN_TAGS = {
|
|
|
260
260
|
"server.run-terminal.artifact-capped": { cls: "F", note: "S-107:leader_run 行上的工件字节(salvaged[].patch / candidatePatch.patch / merge.candidatePatch / merge.rej)长度 > ARTIFACT_INPUT_CAP(256KB)⇒ 整份换 «redacted:oversized-artifact»(fail-closed 取证丢弃;原字节仍在 worker 分支/merge 咽喉);计数=多少份工件被整份丢弃" },
|
|
261
261
|
"server.run-terminal.diag-capped": { cls: "F", note: "S-107:诊断文本座(merge details/measure/grader trace、leader 日志座、planner 样本、journal 预览、workflow_run error cut)长度 > DIAG_INPUT_CAP(256KB)⇒ 整段换 «redacted:oversized-diagnostic» [len=N](fail-closed 取证丢弃;固定窗不 fail-closed 故不开窗);计数=多少段诊断文本被整段丢弃" },
|
|
262
262
|
"server.run-terminal.output-truncated": { cls: "F", note: "S-107:持久 error 脱后 UTF-8 字节 > 65,000(TiDB TEXT 65,535 字节)⇒ 脱后切并带标记(切在占位里不是泄漏);计数=多少条终态文案被字节封顶截断" },
|
|
263
|
-
"server.runs.
|
|
263
|
+
"server.runs.abort-cause-missing": { cls: "F", note: "S-947 候裁 ⑤(主会话裁:默认值保留,miss 臂必须响):续跑腿 `http/resume-legs.ts` 的 `checkpoint.resume_aborted` 臂 —— core 只在本腿递进去的信号**已中止**时铸该码,而那只信号唯一的写者是取消动词(快路 / 耐久旗轮询,句柄登记 `cancel_verb`),所以**只有 core 契约外的形**(桩 / 非 core Runner 直接抛该码,本腿句柄从未中止)才到得了「句柄上读不到成因」这一臂。取消终局照铸,成因取本腿唯一的中止词 `cancel_verb`(取消终局恒带成因由铸点签名保证,不让它缺);detail `task=<taskId>`;计数 = 多少次取消成因是默认值而不是句柄登记的。生产上恒零是证据,不归零 = 有一条不走 core 契约的 Runner 在跑续跑腿。" },
|
|
264
|
+
"server.runs.terminal-ledger-write-failed": { cls: "F", note: "S-107(全量门 silent-degradation 棘轮抓出的五处静默 `catch {}`):run 终局链里**账本腿**(flush 缓冲 text/reasoning、append `failed` 行)与终态翻转(setTerminal)已不共命运——账本 INSERT 抛不再让 run 卡 running 到 reaper;但那条抛掉的账本行之前是**静默**吞的。detail `arm=` 四臂(flush-clean-cancel / append-clean-cancel / flush-catch / append-thrown-cancel / append-failed);S-947 起续跑腿(`http/resume-legs.ts`)两座同 tag:resume-append-resume-aborted / resume-append-thrown-cancel(取消抛出两臂的 `failed` 行,修前那两臂根本不落账本终局行):缺的是 run_events 尾巴(缓冲文本或 failed 事件),run 行本身仍是 durable truth,读面/reaper 容忍缺失,故 F 类;计数=多少次终局账本写失败。" },
|
|
264
265
|
"server.redact.ppk-eat-to-end": { cls: "F", note: "S-107 第三十二轮重扫:PuTTY .ppk 容器扫描器(redact.ts redactPpkContainers)在「无合格终止 / 收口后同容器范围再见私钥字段 / 收口后到下一版本头之间夹非空白」三形下从头**吃到输入末尾**(fail-closed 取证丢弃)——终态 errorMessage 座上=整条错误文案变成 «redacted:private-key»,与 redactions_applied_total{private-key} 的诚实替换同形不可分,故要有自己的计数/探针;影子扫不计;计数=多少次整段吃尾。" },
|
|
265
266
|
"server.approval-card.closed-word-out-of-set": {
|
|
266
267
|
cls: "F",
|
|
@@ -94,16 +94,49 @@ export interface RunCancelContext {
|
|
|
94
94
|
elapsedMs?: number;
|
|
95
95
|
}
|
|
96
96
|
/**
|
|
97
|
-
*
|
|
97
|
+
* 🔴 S-947(cli [ref] 第 3 件 / L-212)—— **server 为什么中止了这条 run**:取消终局的机读成因,闭集。
|
|
98
98
|
*
|
|
99
|
-
*
|
|
100
|
-
* (`
|
|
101
|
-
*
|
|
102
|
-
*
|
|
99
|
+
* 病:非 detach 流断连、`POST /v1/runs/:id/cancel`、断连时有 ask 但 park 宽限用尽 —— 三件事在终局上**逐字同形**
|
|
100
|
+
* (`failed` + `terminal.code:"cancelled"`),壳只能按 `errorMessage` 文案猜。
|
|
101
|
+
*
|
|
102
|
+
* 每个词都必须有一个**真站点**(没有站点的词不许进表;普查与站点坐标见契约 §5.0d):
|
|
103
|
+
* · `client_disconnected` —— 非 detach 的 `POST /v1/tasks/stream` 客户端断连,断连那一刻本 run 没有流内未决 ask
|
|
104
|
+
* (`http/routes/tasks.ts` 的断连臂,立即中止);
|
|
105
|
+
* · `ask_park_grace_exhausted` —— 同一断连臂,但断连那一刻有流内未决 ask、已按窗到期同路转 park,宽限计时器用尽时 park 仍没落成;
|
|
106
|
+
* · `cancel_verb` —— `POST /v1/runs/:id/cancel`:同副本快路、跨副本耐久取消旗的轮询(四条腿的心跳 / 登记追平),
|
|
107
|
+
* 以及 park 中的 run 被它直接终局化(那一形不经 abort,成因照样是这条动词)。
|
|
108
|
+
* **刻意不在表里**(普查过、没有真站点):抢占(`preempt` 是优雅让出,终局是 `paused`,不是取消)、有序关停
|
|
109
|
+
* (S-929 只把未决卡转 park,从不中止活 run;进程退出后的行由收割器落「持有者死亡」判决,那不是本仓铸的取消终局)、
|
|
110
|
+
* `interrupt`(引擎的 turn 动词,终局 `completed` + `haltedByUser`)、detach 断连(不中止)。
|
|
111
|
+
*
|
|
112
|
+
* 词表属主 = 本文件(与它所在的 {@link PersistedTaskResult} 同住);**单写者** = `run-cancel-context.ts` 的 `createRunAbort`
|
|
113
|
+
* (中止那一刻登记)与 `mintCancelledResult`(终局铸点读它),自铸 wire 值登记 = `trace/self-minted-wire.ts` 的 `abortCause`
|
|
114
|
+
* (`satisfies Record<AbortCause, …>`:加 / 删一个词而登记没跟 ⇒ tsc 红)。
|
|
103
115
|
*/
|
|
104
|
-
export
|
|
116
|
+
export declare const ABORT_CAUSES: readonly ["client_disconnected", "ask_park_grace_exhausted", "cancel_verb"];
|
|
117
|
+
export type AbortCause = (typeof ABORT_CAUSES)[number];
|
|
118
|
+
/**
|
|
119
|
+
* server 自铸、挂在终局结果上的**侧记键**全集(都描述「server 这一侧发生了什么」,core 那一侧无从知道)。
|
|
120
|
+
* 本型是这一族键的**唯一登记处**(`trace/core-keyset-guard.ts` 的辖域线指向这里;那边另有一道编译期钉:
|
|
121
|
+
* core 的 `TaskResult` 哪天长出同名键 ⇒ 当场红 —— 同一个键两个铸者是本型存在要防的那件事)。
|
|
122
|
+
* · `cancelContext`([ref])—— 取消现场快照(分诊材料:多久没动静、起跑多久);
|
|
123
|
+
* · `abortCause`(S-947)—— 这次取消是谁 / 什么发起的({@link AbortCause})。
|
|
124
|
+
* 两键**同一只铸点**(`mintCancelledResult`)、**同一条在场闸**(「只补空座」:终局就是被那次中止打断的那一条,
|
|
125
|
+
* 才挂;与中止赛跑跑完的终局、引擎自带码的失败一律原样立住、两键都不挂)。差别只在缺席纪律:快照一键都证不出 ⇒ 整只缺席;
|
|
126
|
+
* 成因在铸点那一刻恒已知 ⇒ 取消终局上恒在场。
|
|
127
|
+
*/
|
|
128
|
+
export interface PersistedResultSideKeys {
|
|
105
129
|
cancelContext?: RunCancelContext;
|
|
106
|
-
|
|
130
|
+
abortCause?: AbortCause;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 落库/上 wire 的终局结果 = core 的 `TaskResult` **加上** server 自铸的可缺席侧记键({@link PersistedResultSideKeys})。
|
|
134
|
+
*
|
|
135
|
+
* additive 且可缺席,所以旧行、旧消费端逐字不受影响(`TaskResult` 仍可原样赋给本型)。⚠️ 这不是给「往 core 结果里塞私货」
|
|
136
|
+
* 开的口子:每加一个键都要能回答「为什么它属于 run 账本而不属于 core 的任务结果」—— 两键的答案都是它们描述的是
|
|
137
|
+
* **server 侧的取消现场**,core 那一侧根本不知道有人按了取消、谁按的。落点 = `result` JSON 列(三孪生整只存),**零新列**。
|
|
138
|
+
*/
|
|
139
|
+
export type PersistedTaskResult = TaskResult & PersistedResultSideKeys;
|
|
107
140
|
/**
|
|
108
141
|
* 🔴 run 店「持有它的实例丢了」那一判的**行上落款**(S-928 codex r1-F1 提为常量):三只 run 店的 `reapStale` 把心跳过期的 running 行翻成
|
|
109
142
|
* `failed` 并写下这一句。它是 run 店死亡判决留在行上的证据之一(另一只 = {@link RUN_ORPHANED_ERROR})—— 用户输入受理账本的收敛判词
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export class StoreBootRefusalError extends Error {
|
|
2
2
|
}
|
|
3
|
+
export const ABORT_CAUSES = ["client_disconnected", "ask_park_grace_exhausted", "cancel_verb"];
|
|
3
4
|
export const RUN_STALLED_ERROR = "run stalled (instance lost?)";
|
|
4
5
|
export const RUN_ORPHANED_ERROR = "run orphaned by engine restart";
|
|
5
6
|
export function buildRunOrphanedError(heldBy) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PersistedTaskResult, RunCancelContext } from "./plugins/store-contracts.js";
|
|
1
|
+
import type { AbortCause, PersistedTaskResult, RunCancelContext } from "./plugins/store-contracts.js";
|
|
2
2
|
/**
|
|
3
3
|
* @param taskId 这条 run 的账本 id(turn 活性登记的键)。
|
|
4
4
|
* @param startedAtMs 这条 run 起跑的墙钟时刻(epoch ms):取消腿手上有行就用行的 `createdAt`,
|
|
@@ -43,11 +43,23 @@ export declare const CANCELLED_CODE = "cancelled";
|
|
|
43
43
|
* · `cancelContext`:已经有快照就不重铸(重铸会把「上一条引擎事件多久以前」重新计时)。
|
|
44
44
|
*
|
|
45
45
|
* ## 为什么不判「这条 failed 是不是 abort 形」
|
|
46
|
-
*
|
|
47
|
-
* `
|
|
46
|
+
* 判据在**调用点**:调用点问的是「本腿这次是不是被 server 中止了」—— 本腿 {@link RunAbort} 句柄上登记的成因
|
|
47
|
+
* (`cause()` 在场),那是 server 自己的事实。让本函数去嗅 `terminal.message` 的字面就是再造事实
|
|
48
48
|
* —— [ref] 二那次事故的同一个形。
|
|
49
|
+
*
|
|
50
|
+
* ## S-947:成因(`abortCause`)与码、快照**同进同退**
|
|
51
|
+
* `cause` 是**必填**:本函数是 server 铸取消终局的唯一出口,而每一次取消都有一个发起者 ⇒ 「本仓铸的取消终局上恒有
|
|
52
|
+
* `abortCause`」由签名保证,不靠调用方记得。它走与补码、补快照**同一条**空座闸 —— 引擎给的终局不是无码的 failed
|
|
53
|
+
* (与中止赛跑跑完了 / 引擎自带码)⇒ 整只原样返回,成因也不挂(那条终局不是这次中止的结果,挂上就是「完成了却带着取消便签」)。
|
|
49
54
|
*/
|
|
50
|
-
export declare function mintCancelledResult(args: {
|
|
55
|
+
export declare function mintCancelledResult<R extends PersistedTaskResult>(args: CancelMintArgs & {
|
|
56
|
+
engineResult: R;
|
|
57
|
+
}): R;
|
|
58
|
+
export declare function mintCancelledResult(args: CancelMintArgs & {
|
|
59
|
+
engineResult?: undefined;
|
|
60
|
+
}): PersistedTaskResult;
|
|
61
|
+
/** {@link mintCancelledResult} 的公共实参(`engineResult` 另列:在场与否决定返回型)。 */
|
|
62
|
+
interface CancelMintArgs {
|
|
51
63
|
/** 这条 run 的账本 id(活性登记的键;账本不在场时给空串,快照按「证不出」处理)。 */
|
|
52
64
|
taskId: string;
|
|
53
65
|
/** 会话 id(合成形要写进结果;引擎给了结果时不用)。 */
|
|
@@ -56,7 +68,47 @@ export declare function mintCancelledResult(args: {
|
|
|
56
68
|
startedAtMs: number;
|
|
57
69
|
/** 合成形的失败文案(引擎给了终局时不用)。 */
|
|
58
70
|
message: string;
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
}
|
|
71
|
+
/** S-947:这次取消的发起者(本腿 {@link RunAbort} 句柄上登记的那一个;park 中直接终局化的取消动词给 `"cancel_verb"`)。 */
|
|
72
|
+
cause: AbortCause;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* S-947 —— 没有 `done` 可落的取消形在账本上的那一行(`failed`):**从铸好的取消终局投影**,不手抄。
|
|
76
|
+
*
|
|
77
|
+
* 账本 `failed` 行是平面形(`errorMessage` / `errorCode` 顶层),所以成因也在顶层、与 `errorCode` 并列;有 `done` 可落的
|
|
78
|
+
* 那几形,成因随 `done.result` 走(结果本身带它)。两形同一个来源 = 同一只取消终局,不是两处各写一份。
|
|
79
|
+
* 返回纯数据 ⇒ `build*`。
|
|
80
|
+
*/
|
|
81
|
+
export declare function buildCancelledLedgerRow(c: PersistedTaskResult): {
|
|
82
|
+
errorMessage?: string;
|
|
83
|
+
errorCode?: string;
|
|
84
|
+
abortCause?: AbortCause;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* S-947 —— 一条 run 腿的**中止句柄**:server 中止这条腿的唯一入口,也是「为什么中止」的唯一登记处。
|
|
88
|
+
*
|
|
89
|
+
* ## 为什么是句柄、不是一张按 taskId 键控的表
|
|
90
|
+
* 修前这件事由 `cancelledViaVerb: Set<taskId>`(「这次是取消动词」一个词)+ 各腿手写的生命周期(登记 / finally 里按身份守卫删 /
|
|
91
|
+
* 同步腿「删之前先取」)承担:一个键控在 **taskId** 上的旁表,要防的病都来自键与腿不同寿 —— 快速续跑在旧腿 finally 之前
|
|
92
|
+
* 用同一 taskId 登记新腿、旧腿收尾删掉新腿的条目;同步腿必须在删之前取值。成因挂在**腿自己的中止控制器**上之后,
|
|
93
|
+
* 这些规则一条都不需要:句柄与腿同寿,换腿就是换句柄,没有可删、可串、可漏的旁表。
|
|
94
|
+
*
|
|
95
|
+
* ## 先到先得(first-writer-wins)
|
|
96
|
+
* 第一次 `abort(cause)` 生效并登记成因;之后的调用全是 no-op(信号早已中止,成因不改写)—— 断连之后又到了取消动词、
|
|
97
|
+
* 取消之后又断连,终局说的都是**真正让这条腿停下来**的那一个。`AbortController.abort()` 自身就是幂等的,本句柄只是在
|
|
98
|
+
* 「第一次」那一刻把词记下。
|
|
99
|
+
*
|
|
100
|
+
* ## 引擎看到的东西不变
|
|
101
|
+
* 中止走 `AbortController.abort()` 的默认原因(引擎、provider 的 fetch、子进程看到的仍是标准 `AbortError`)—— 成因**不**塞进
|
|
102
|
+
* `signal.reason`:那会改掉引擎与 provider 层收到的拒绝值,是在下游改上游接缝的输入(立宪禁区)。
|
|
103
|
+
*/
|
|
104
|
+
export interface RunAbort {
|
|
105
|
+
/** 交给引擎的那只信号(`spec.signal` / 审批 / 提问 ALS 的 `abortSignal` / 子代 stoppedBy 归因都用它)。 */
|
|
106
|
+
readonly signal: AbortSignal;
|
|
107
|
+
/** 以 `cause` 中止本腿。已中止 ⇒ no-op(成因不改写)。 */
|
|
108
|
+
abort(cause: AbortCause): void;
|
|
109
|
+
/** 让本腿停下来的那次中止的成因;未中止 ⇒ `undefined`。 */
|
|
110
|
+
cause(): AbortCause | undefined;
|
|
111
|
+
}
|
|
112
|
+
export declare function createRunAbort(): RunAbort;
|
|
113
|
+
export {};
|
|
62
114
|
//# sourceMappingURL=run-cancel-context.d.ts.map
|
|
@@ -23,6 +23,7 @@ export function mintCancelledResult(args) {
|
|
|
23
23
|
...engine,
|
|
24
24
|
terminal: { ...engine.terminal, code: CANCELLED_CODE },
|
|
25
25
|
...(engine.cancelContext === undefined && ctx !== undefined ? { cancelContext: ctx } : {}),
|
|
26
|
+
abortCause: args.cause,
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
return {
|
|
@@ -32,6 +33,31 @@ export function mintCancelledResult(args) {
|
|
|
32
33
|
result: "",
|
|
33
34
|
stats: { turns: 0, tokens: 0, usageMissing: true },
|
|
34
35
|
...(ctx ? { cancelContext: ctx } : {}),
|
|
36
|
+
abortCause: args.cause,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function buildCancelledLedgerRow(c) {
|
|
40
|
+
const t = c.terminal;
|
|
41
|
+
return {
|
|
42
|
+
...(t.kind === "failed" && t.message !== undefined ? { errorMessage: t.message } : {}),
|
|
43
|
+
...(t.kind === "failed" && t.code !== undefined ? { errorCode: t.code } : {}),
|
|
44
|
+
...(c.abortCause !== undefined ? { abortCause: c.abortCause } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function createRunAbort() {
|
|
48
|
+
const ctrl = new AbortController();
|
|
49
|
+
let landed;
|
|
50
|
+
return {
|
|
51
|
+
signal: ctrl.signal,
|
|
52
|
+
abort(cause) {
|
|
53
|
+
if (ctrl.signal.aborted)
|
|
54
|
+
return;
|
|
55
|
+
landed = cause;
|
|
56
|
+
ctrl.abort();
|
|
57
|
+
},
|
|
58
|
+
cause() {
|
|
59
|
+
return landed;
|
|
60
|
+
},
|
|
35
61
|
};
|
|
36
62
|
}
|
|
37
63
|
//# sourceMappingURL=run-cancel-context.js.map
|
package/dist/runs.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { defaultTaskRegistry, type CascadeConfig, type CheckpointToken, type Run
|
|
|
2
2
|
import type { SendUserFileEmitter } from "./capabilities/send-user-file-tool.js";
|
|
3
3
|
import type { RunStore } from "./plugins/store-backend.js";
|
|
4
4
|
import type { Metrics } from "./observability/metrics.js";
|
|
5
|
+
import { type RunAbort } from "./run-cancel-context.js";
|
|
5
6
|
import type { ModelUsageTracker, PromptManifestTracker } from "./budget.js";
|
|
6
7
|
import type { ElicitationCoordinator } from "./elicitation.js";
|
|
7
8
|
import type { QuestionCoordinator } from "./question.js";
|
|
@@ -259,10 +260,11 @@ export declare function evictIfConflict(runner: Runner, sessionId: string | unde
|
|
|
259
260
|
* text); tool/lifecycle events are recorded individually. Per-token live streaming stays on the
|
|
260
261
|
* synchronous `/v1/tasks/stream` endpoint. Never throws — failures are recorded on the run.
|
|
261
262
|
*
|
|
262
|
-
* Cancellation (POST /v1/runs/:id/cancel): the run registers
|
|
263
|
-
* fast path — the cancel handler aborts it directly) and its heartbeat tick polls the durable
|
|
264
|
-
* `cancel_requested` flag (cross-replica, ≤HEARTBEAT_MS lag)
|
|
265
|
-
* status "failed" + errorCode "cancelled" (metric label "cancelled"); the entry is removed in
|
|
263
|
+
* Cancellation (POST /v1/runs/:id/cancel): the run registers its abort handle (S-947 `RunAbort`) in `inflight`
|
|
264
|
+
* (same-replica fast path — the cancel handler aborts it directly) and its heartbeat tick polls the durable
|
|
265
|
+
* `cancel_requested` flag (cross-replica, ≤HEARTBEAT_MS lag); both abort with `cancel_verb`. Either way the run settles
|
|
266
|
+
* to status "failed" + errorCode "cancelled" + `result.abortCause` (metric label "cancelled"); the entry is removed in
|
|
267
|
+
* `finally`.
|
|
266
268
|
*
|
|
267
269
|
* Preemption (POST /v1/assistant/tasks/:id/preempt, [ref] seam #2): a SECOND, INDEPENDENT controller
|
|
268
270
|
* (`preemptCtrl`, registered in `preemptable`) carries the scheduler's graceful "yield this task" — when raised,
|
|
@@ -281,7 +283,7 @@ export declare function evictIfConflict(runner: Runner, sessionId: string | unde
|
|
|
281
283
|
export declare function runInBackground(runner: Runner, spec: TaskSpec, runStore: RunStore, taskId: string, metrics?: Metrics, principal?: string, verify?: VerifyRoundsSpec, // 快审 F1:number 参升整对象——cost 顶随传
|
|
282
284
|
cascade?: CascadeConfig, instrumentDegenerate?: (result: TaskResult) => void, planCacheProbe?: {
|
|
283
285
|
record: (scope: string | undefined, objective: string, completed: boolean) => void;
|
|
284
|
-
}, persistThinking?: boolean, inflight?: Map<string,
|
|
286
|
+
}, persistThinking?: boolean, inflight?: Map<string, RunAbort>, preemptable?: Map<string, AbortController>, steerable?: Map<string, TaskStream>, modelUsage?: ModelUsageTracker, elicitation?: ElicitationCoordinator,
|
|
285
287
|
/** E23: the VERIFIED principal (gatedPrincipal) that may answer this run's elicitations — matches the respond
|
|
286
288
|
* owner-gate's identity source (NOT the cost-attribution `principal`, which on a direct door is the self-asserted
|
|
287
289
|
* header → can be null while a JWT principal exists). Falls back to the run owner when not supplied. */
|
package/dist/runs.js
CHANGED
|
@@ -12,7 +12,7 @@ import { taskNotificationEventData, appendModelUsageDelta, appendPromptManifest,
|
|
|
12
12
|
import { createLedgerSink } from "./trace/ledger-sink.js";
|
|
13
13
|
import { registerEngineNoticeLeg } from "./trace/engine-notice-wire.js";
|
|
14
14
|
import { recordTurnActivity } from "./turn-activity.js";
|
|
15
|
-
import { CANCELLED_CODE, mintCancelledResult } from "./run-cancel-context.js";
|
|
15
|
+
import { buildCancelledLedgerRow, CANCELLED_CODE, createRunAbort, mintCancelledResult } from "./run-cancel-context.js";
|
|
16
16
|
import { createApprovalCardEmitter, resolveApprovalLeg } from "./tool-approval.js";
|
|
17
17
|
import { fleetRunResiduals, isFleetAgentTerminalNotification } from "./fleet/fleet-bus.js";
|
|
18
18
|
import { defaultSubagentTailBus } from "./fleet/subagent-tail-bus.js";
|
|
@@ -178,14 +178,16 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
178
178
|
const startedAt = Date.now();
|
|
179
179
|
metrics?.addGauge("runs_active", 1);
|
|
180
180
|
fleetPublisher?.onStart();
|
|
181
|
-
const cancelCtrl =
|
|
181
|
+
const cancelCtrl = createRunAbort();
|
|
182
182
|
inflight?.set(taskId, cancelCtrl);
|
|
183
183
|
markChildrenStoppedByUserOnAbort(cancelCtrl.signal, taskId, principal);
|
|
184
184
|
const preemptEligible = verify === undefined && cascade === undefined;
|
|
185
185
|
const preemptCtrl = new AbortController();
|
|
186
186
|
if (preemptEligible)
|
|
187
187
|
preemptable?.set(taskId, preemptCtrl);
|
|
188
|
-
const cancelledResult = (engineResult) =>
|
|
188
|
+
const cancelledResult = (cause, engineResult) => engineResult !== undefined
|
|
189
|
+
? mintCancelledResult({ taskId, sessionId: spec.sessionId ?? "", startedAtMs: startedAt, message: "cancelled by user", cause, engineResult })
|
|
190
|
+
: mintCancelledResult({ taskId, sessionId: spec.sessionId ?? "", startedAtMs: startedAt, message: "cancelled by user", cause });
|
|
189
191
|
const recordDone = (result, status) => {
|
|
190
192
|
metrics?.addGauge("runs_active", -1);
|
|
191
193
|
metrics?.inc("tasks_total", { status });
|
|
@@ -222,7 +224,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
222
224
|
void runStore.heartbeat(taskId, owner).catch(() => { metrics?.inc("run_signal_poll_errors_total", { kind: "heartbeat" }); });
|
|
223
225
|
if (!cancelCtrl.signal.aborted) {
|
|
224
226
|
void runStore.isCancelRequested(taskId, owner).then((c) => { if (c)
|
|
225
|
-
cancelCtrl.abort(); }).catch(() => { metrics?.inc("run_signal_poll_errors_total", { kind: "cancel" }); });
|
|
227
|
+
cancelCtrl.abort("cancel_verb"); }).catch(() => { metrics?.inc("run_signal_poll_errors_total", { kind: "cancel" }); });
|
|
226
228
|
}
|
|
227
229
|
if (preemptEligible && !preemptCtrl.signal.aborted) {
|
|
228
230
|
void runStore.isPreemptRequested(taskId, owner).then((p) => { if (p)
|
|
@@ -233,7 +235,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
233
235
|
const anchor = new TurnAnchorCapture(captureTurnAnchor, () => metrics?.inc("resume_anchor_capture_failed"), captureUserMessageAnchor);
|
|
234
236
|
let reached;
|
|
235
237
|
let observedDone;
|
|
236
|
-
let
|
|
238
|
+
let causeAtDone;
|
|
237
239
|
let settledAsCancel = false;
|
|
238
240
|
let steerableStreamRef;
|
|
239
241
|
let legLive = true;
|
|
@@ -364,7 +366,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
364
366
|
for await (const ev of stream) {
|
|
365
367
|
if (ev.type === "done") {
|
|
366
368
|
observedDone = ev.result;
|
|
367
|
-
|
|
369
|
+
causeAtDone = cancelCtrl.cause();
|
|
368
370
|
}
|
|
369
371
|
fleetPublisher?.onEvent(ev);
|
|
370
372
|
await flushPromptManifest();
|
|
@@ -402,7 +404,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
402
404
|
}
|
|
403
405
|
evictIfConflict(runner, spec.sessionId, ev.result);
|
|
404
406
|
{
|
|
405
|
-
const settled =
|
|
407
|
+
const settled = causeAtDone !== undefined ? cancelledResult(causeAtDone, ev.result) : ev.result;
|
|
406
408
|
settledAsCancel = failureCodeOf(settled.terminal) === CANCELLED_CODE;
|
|
407
409
|
const safe = await withModelUsage(sealResultForExit(settled));
|
|
408
410
|
await sink.appendDone(safe);
|
|
@@ -448,17 +450,18 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
448
450
|
? sendUserFile.runWithContext({ taskId, emit: (frame) => { const { type, ...rest } = frame; return append(type, rest); } }, withApproval)
|
|
449
451
|
: withApproval();
|
|
450
452
|
})));
|
|
451
|
-
|
|
453
|
+
const cleanCancelCause = cancelCtrl.cause();
|
|
454
|
+
if (!reached && cleanCancelCause !== undefined) {
|
|
452
455
|
try {
|
|
453
456
|
await flush();
|
|
454
457
|
}
|
|
455
458
|
catch (e) {
|
|
456
459
|
recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=flush-clean-cancel ${errText(e)}`);
|
|
457
460
|
}
|
|
458
|
-
const c = cancelledResult();
|
|
461
|
+
const c = cancelledResult(cleanCancelCause);
|
|
459
462
|
settledAsCancel = failureCodeOf(c.terminal) === CANCELLED_CODE;
|
|
460
463
|
try {
|
|
461
|
-
await append("failed",
|
|
464
|
+
await append("failed", buildCancelledLedgerRow(c));
|
|
462
465
|
}
|
|
463
466
|
catch (e) {
|
|
464
467
|
recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=append-clean-cancel ${errText(e)}`);
|
|
@@ -469,6 +472,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
469
472
|
}
|
|
470
473
|
}
|
|
471
474
|
catch (err) {
|
|
475
|
+
const thrownCancelCause = cancelCtrl.cause();
|
|
472
476
|
const message = errText(err);
|
|
473
477
|
try {
|
|
474
478
|
try {
|
|
@@ -488,11 +492,11 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
488
492
|
else if (reached?.kind === "needs_review") {
|
|
489
493
|
await runStore.setNeedsReview(taskId, reached.result);
|
|
490
494
|
}
|
|
491
|
-
else if (
|
|
492
|
-
const c = cancelledResult();
|
|
495
|
+
else if (thrownCancelCause !== undefined && observedDone === undefined) {
|
|
496
|
+
const c = cancelledResult(thrownCancelCause);
|
|
493
497
|
settledAsCancel = failureCodeOf(c.terminal) === CANCELLED_CODE;
|
|
494
498
|
try {
|
|
495
|
-
await append("failed",
|
|
499
|
+
await append("failed", buildCancelledLedgerRow(c));
|
|
496
500
|
}
|
|
497
501
|
catch (e) {
|
|
498
502
|
recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=append-thrown-cancel ${errText(e)}`);
|
|
@@ -58,6 +58,28 @@ export declare const SELF_MINTED_WIRE: {
|
|
|
58
58
|
};
|
|
59
59
|
};
|
|
60
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* S-947(7.112.0):server 发起的中止在取消终局上留下的机读成因 —— `PersistedTaskResult.abortCause`,与 `cancelContext` 同住、同铸点、
|
|
63
|
+
* 同在场闸(在场 ⇔ 这条终局就是 server 那次中止铸出来的取消终局)。逐词站点见契约 §5.0d。键本身是 run 结果上的 server 侧记键
|
|
64
|
+
* (`store-contracts.ts` 的 `PersistedResultSideKeys`),不是事件投影面的键 ⇒ 不在 `wire-projection-faces.ts`。
|
|
65
|
+
*/
|
|
66
|
+
readonly abortCause: {
|
|
67
|
+
readonly at: "run 结果 result.abortCause(GET /v1/runs/:id → result · done.result · POST /v1/tasks 200 体顶层 · 账本 done 行 result / 无 done 的取消形账本 failed 行顶层)";
|
|
68
|
+
readonly members: {
|
|
69
|
+
client_disconnected: {
|
|
70
|
+
attr: string;
|
|
71
|
+
since: string;
|
|
72
|
+
};
|
|
73
|
+
ask_park_grace_exhausted: {
|
|
74
|
+
attr: string;
|
|
75
|
+
since: string;
|
|
76
|
+
};
|
|
77
|
+
cancel_verb: {
|
|
78
|
+
attr: string;
|
|
79
|
+
since: string;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
};
|
|
61
83
|
/**
|
|
62
84
|
* 会话所有者自助抹除面的**已分类拒** → HTTP 状态(S-930 起与 operator 面 `POST /v1/memory/erase` 同一只映射;`since` 记的是**本面**
|
|
63
85
|
* 自哪一版起这样答 —— 7.107.0 及以前本面只认前两只,其余落 500 `internal.error`)。
|
|
@@ -21,6 +21,14 @@ export const SELF_MINTED_WIRE = {
|
|
|
21
21
|
".claude/AGENTS.md": { attr: "-", since: "7.111.0" },
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
|
+
abortCause: {
|
|
25
|
+
at: "run 结果 result.abortCause(GET /v1/runs/:id → result · done.result · POST /v1/tasks 200 体顶层 · 账本 done 行 result / 无 done 的取消形账本 failed 行顶层)",
|
|
26
|
+
members: {
|
|
27
|
+
client_disconnected: { attr: "-", since: "7.112.0" },
|
|
28
|
+
ask_park_grace_exhausted: { attr: "-", since: "7.112.0" },
|
|
29
|
+
cancel_verb: { attr: "-", since: "7.112.0" },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
24
32
|
sessionMemoryEraseStatus: {
|
|
25
33
|
at: "POST /v1/sessions/:id/memory/erase(已分类拒 errorCode → HTTP 状态)",
|
|
26
34
|
members: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.112.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@sema-agent/core": "7.38.
|
|
64
|
+
"@sema-agent/core": "7.38.3",
|
|
65
65
|
"@sema-agent/settings-schema": "6.4.0",
|
|
66
66
|
"e2b": "^2.28.0",
|
|
67
67
|
"libsodium-wrappers": "^0.8.4",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"sharp": "^0.35.3"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
|
-
"@sema-agent/sdk": "15.
|
|
80
|
+
"@sema-agent/sdk": "15.3.0",
|
|
81
81
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
82
82
|
"@types/node": "22.10.2",
|
|
83
83
|
"@types/pg": "^8.20.0",
|