@sema-agent/server 7.63.0 → 7.65.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/USAGE.md +35 -20
- package/deploy/sema-up/smoke.sh +2 -2
- package/dist/approval-ask-audit-store.d.ts +149 -3
- package/dist/approval-ask-audit-store.js +11 -4
- package/dist/bench/s1/arms.d.ts +2 -2
- package/dist/bench/s1/arms.js +20 -15
- package/dist/bench/s1/live-deps.js +3 -1
- package/dist/bench/s1/run-firm.js +2 -2
- package/dist/boot/resolve-spec.d.ts +0 -5
- package/dist/boot/resolve-spec.js +3 -4
- package/dist/capabilities/hands-lane.d.ts +52 -12
- package/dist/capabilities/hands-lane.js +19 -8
- package/dist/config-catalog.js +1 -1
- package/dist/config-center/read-face.js +10 -8
- package/dist/config-types.d.ts +12 -3
- package/dist/config.js +7 -4
- package/dist/degenerate-instrument.d.ts +0 -33
- package/dist/degenerate-instrument.js +2 -1
- package/dist/fleet/fleet-bus.d.ts +9 -4
- package/dist/fleet/fleet-bus.js +2 -1
- package/dist/host-decision.d.ts +39 -0
- package/dist/host-decision.js +6 -0
- package/dist/http/route-ctx.d.ts +8 -5
- package/dist/http/routes/a2a-serve.js +4 -2
- package/dist/http/routes/approvals-assistant.d.ts +0 -13
- package/dist/http/routes/approvals-assistant.js +2 -1
- package/dist/http/routes/diagnostics.d.ts +0 -11
- package/dist/http/routes/diagnostics.js +2 -0
- package/dist/http/routes/runs.js +3 -2
- package/dist/http/routes/tasks.js +33 -29
- package/dist/http/routes/workflows.d.ts +0 -11
- package/dist/http/routes/workflows.js +18 -14
- package/dist/http/server.js +50 -46
- package/dist/leader/fanout.d.ts +6 -23
- package/dist/leader/fanout.js +8 -19
- package/dist/leader/leader.d.ts +13 -10
- package/dist/leader/leader.js +30 -22
- package/dist/leader/repair-wire.d.ts +4 -1
- package/dist/leader/repair-wire.js +7 -5
- package/dist/leader/wire.js +5 -4
- package/dist/main.js +7 -7
- package/dist/observability/fail-open.d.ts +8 -0
- package/dist/observability/fail-open.js +8 -0
- package/dist/observability/metrics.js +1 -1
- package/dist/observability/run-terminal-log.d.ts +25 -7
- package/dist/observability/run-terminal-log.js +15 -4
- package/dist/observability/tool-trace.d.ts +13 -12
- package/dist/observability/tool-trace.js +8 -13
- package/dist/parked-decide.d.ts +8 -4
- package/dist/parked-decide.js +1 -1
- package/dist/plugins/file-run-store.js +12 -21
- package/dist/plugins/memory-run-store.js +3 -2
- package/dist/plugins/run-store-sql.js +3 -2
- package/dist/read-face-posture.d.ts +52 -0
- package/dist/read-face-posture.js +20 -0
- package/dist/run-local.js +6 -4
- package/dist/runs.d.ts +4 -19
- package/dist/runs.js +49 -47
- package/dist/terminal.d.ts +100 -0
- package/dist/terminal.js +55 -0
- package/dist/trace/core-keyset-guard.d.ts +20 -4
- package/dist/trace/ledger-sink.d.ts +2 -1
- package/dist/trace/ledger-sink.js +14 -7
- package/dist/trace/project.d.ts +9 -12
- package/dist/trace/project.js +54 -9
- package/package.json +4 -4
package/dist/leader/leader.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { fanOut
|
|
1
|
+
import { fanOut } from "./fanout.js";
|
|
2
2
|
import { redactErrorMessage, redactHead, redactPatchHead } from "../observability/run-terminal-log.js";
|
|
3
3
|
import { pullDiff } from "./diffout.js";
|
|
4
4
|
import { mergeBranches } from "./merge.js";
|
|
5
|
-
import { Runner, runWithVerification, resumeWithVerification, delimitUntrusted } from "@sema-agent/core";
|
|
5
|
+
import { Runner, runWithVerification, resumeWithVerification, delimitUntrusted, terminalProjection } from "@sema-agent/core";
|
|
6
|
+
import { pausedCauseOf } from "../terminal.js";
|
|
6
7
|
const MAX_RESUME_SLICES = 200;
|
|
7
8
|
export async function autoResumeOnResource(initial, signal, resume, maxSlices = MAX_RESUME_SLICES) {
|
|
8
9
|
let vr = initial;
|
|
9
|
-
for (let slice = 0;
|
|
10
|
-
|
|
10
|
+
for (let slice = 0; slice < maxSlices && !signal.aborted; slice++) {
|
|
11
|
+
const paused = pausedCauseOf(vr.terminal);
|
|
12
|
+
if (paused === undefined || paused.gate.kind !== "resource_limit")
|
|
13
|
+
break;
|
|
14
|
+
vr = await resume(paused.token, slice + 1);
|
|
11
15
|
}
|
|
12
16
|
return vr;
|
|
13
17
|
}
|
|
@@ -55,21 +59,23 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
|
|
|
55
59
|
deps.logger?.info?.("worker_resource_resume", { workerId: w.workerId, slice });
|
|
56
60
|
return runner.resume(token, { gate: "resource_limit", decision: "continue" }, { ...w.spec, sessionId: w.sessionId, signal });
|
|
57
61
|
});
|
|
58
|
-
|
|
62
|
+
const plane = terminalProjection(r.terminal);
|
|
63
|
+
const paused = pausedCauseOf(r.terminal);
|
|
64
|
+
if (r.terminal.kind !== "completed") {
|
|
59
65
|
deps.logger?.warn?.("worker_failed", {
|
|
60
|
-
workerId: w.workerId, status:
|
|
61
|
-
...(
|
|
62
|
-
...(
|
|
63
|
-
...(
|
|
66
|
+
workerId: w.workerId, status: plane.status, turns: r.stats?.turns ?? 0,
|
|
67
|
+
...(plane.errorCode ? { errorCode: plane.errorCode } : {}),
|
|
68
|
+
...(plane.errorMessage ? { error: redactHead(plane.errorMessage, 200) } : {}),
|
|
69
|
+
...(plane.blockedReason ? { blockedReason: redactHead(plane.blockedReason, 200) } : {}),
|
|
64
70
|
});
|
|
65
71
|
}
|
|
66
72
|
return {
|
|
67
73
|
...base,
|
|
68
|
-
status: r.
|
|
74
|
+
status: r.terminal.kind === "completed" ? "completed" : paused !== undefined ? "suspended" : "failed",
|
|
69
75
|
stats: r.stats,
|
|
70
|
-
...(r.
|
|
71
|
-
...(
|
|
72
|
-
...(
|
|
76
|
+
...(r.terminal.kind === "completed" ? {} : { error: plane.errorMessage ?? plane.blockedReason ?? `worker ${plane.status}` }),
|
|
77
|
+
...(paused ? { checkpointGate: paused.gate } : {}),
|
|
78
|
+
...(plane.errorCode ? { errorCode: plane.errorCode } : {}),
|
|
73
79
|
};
|
|
74
80
|
},
|
|
75
81
|
});
|
|
@@ -86,16 +92,18 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
|
|
|
86
92
|
deps.logger?.info?.("worker_resource_resume", { workerId: w.workerId, slice });
|
|
87
93
|
return resumeWithVerification(w.runner, token, { gate: "resource_limit", decision: "continue" }, { ...w.spec, sessionId: w.sessionId, signal }, w.spec.objective ?? "", cfg);
|
|
88
94
|
});
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
95
|
+
const vrPlane = terminalProjection(vr.terminal);
|
|
96
|
+
deps.logger?.info?.("verify_outcome", { workerId: w.workerId, status: vrPlane.status, verdict: vr.verification.verdict, rounds: vr.verification.rounds, finding: redactHead(vr.verification.findings[0] ?? "", 220) });
|
|
97
|
+
const pass = vr.terminal.kind === "completed" && vr.verification.verdict === "PASS";
|
|
98
|
+
const vrPaused = pausedCauseOf(vr.terminal);
|
|
99
|
+
const stillSuspended = vrPaused !== undefined;
|
|
92
100
|
const reportStatus = pass ? "completed" : stillSuspended ? "suspended" : "failed";
|
|
93
101
|
if (reportStatus !== "completed") {
|
|
94
102
|
deps.logger?.warn?.("worker_failed", {
|
|
95
103
|
workerId: w.workerId, status: reportStatus, turns: vr.stats?.turns ?? 0, verdict: vr.verification.verdict,
|
|
96
|
-
...(
|
|
97
|
-
...(
|
|
98
|
-
...(
|
|
104
|
+
...(vrPlane.errorCode ? { errorCode: vrPlane.errorCode } : {}),
|
|
105
|
+
...(vrPlane.errorMessage ? { error: redactHead(vrPlane.errorMessage, 200) } : {}),
|
|
106
|
+
...(vrPlane.blockedReason ? { blockedReason: redactHead(vrPlane.blockedReason, 200) } : {}),
|
|
99
107
|
});
|
|
100
108
|
}
|
|
101
109
|
return {
|
|
@@ -105,11 +113,11 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
|
|
|
105
113
|
? {}
|
|
106
114
|
: {
|
|
107
115
|
error: vr.verification.verdict === "unverified"
|
|
108
|
-
? (
|
|
116
|
+
? (vrPlane.errorMessage ?? vrPlane.blockedReason ?? `worker ${vrPlane.status}`)
|
|
109
117
|
: `verify ${vr.verification.verdict}: ${redactHead(vr.verification.findings[0] ?? "", 200)}`,
|
|
110
118
|
}),
|
|
111
|
-
...(
|
|
112
|
-
...(
|
|
119
|
+
...(vrPaused ? { checkpointGate: vrPaused.gate } : {}),
|
|
120
|
+
...(vrPlane.errorCode ? { errorCode: vrPlane.errorCode } : {}),
|
|
113
121
|
};
|
|
114
122
|
},
|
|
115
123
|
});
|
|
@@ -83,9 +83,12 @@ export interface RepairWireCfg {
|
|
|
83
83
|
verifierModelId: string;
|
|
84
84
|
};
|
|
85
85
|
/** Per-attempt observability callback. */
|
|
86
|
+
/** S-136(core 7.6.0 D-8):键名随 core 从 `terminal` 改成 `repairTerminal` —— `terminal` 这个名字在
|
|
87
|
+
* `TaskResult` 上已经归**运行终局因由**所有,修复循环的判词与它同名会是两个不同东西共用一个名字
|
|
88
|
+
* (本仓 status/kind 同名词混淆病族)。 */
|
|
86
89
|
onAttempt?: (info: {
|
|
87
90
|
attempt: number;
|
|
88
|
-
|
|
91
|
+
repairTerminal?: RepairTerminal;
|
|
89
92
|
oracleTier: OracleTier;
|
|
90
93
|
passed: boolean;
|
|
91
94
|
flaky: boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runRepairLoop, delimitUntrusted, } from "@sema-agent/core";
|
|
2
2
|
import { mkRepairOracle } from "./repair-oracle.js";
|
|
3
|
+
import { terminalProjection } from "@sema-agent/core";
|
|
3
4
|
import { redactHead } from "../observability/run-terminal-log.js";
|
|
4
5
|
function statusForTerminal(t) {
|
|
5
6
|
return t === "fixed" || t === "candidate_only" ? "completed" : "failed";
|
|
@@ -40,7 +41,7 @@ async function runRepairLoopForLeaderInner(solo, implSpec, cfg) {
|
|
|
40
41
|
...(cfg.costCeilingMicroUsd !== undefined ? { costCeilingMicroUsd: cfg.costCeilingMicroUsd } : {}),
|
|
41
42
|
...(cfg.totalTimeoutMs !== undefined ? { totalTimeoutMs: cfg.totalTimeoutMs } : {}),
|
|
42
43
|
...(cfg.onAttempt
|
|
43
|
-
? { onAttempt: (i) => cfg.onAttempt({ attempt: i.attempt,
|
|
44
|
+
? { onAttempt: (i) => cfg.onAttempt({ attempt: i.attempt, repairTerminal: i.repairTerminal, oracleTier: i.oracle.tier, passed: i.oracle.passed, flaky: i.oracle.flaky }) }
|
|
44
45
|
: {}),
|
|
45
46
|
onWarn: (e) => cfg.log?.("repair_loop_warn", { warning: redactHead(e.message, 2000) }),
|
|
46
47
|
});
|
|
@@ -50,13 +51,14 @@ async function runRepairLoopForLeaderInner(solo, implSpec, cfg) {
|
|
|
50
51
|
cfg.log?.("repair_loop_threw", { error: safe });
|
|
51
52
|
return { report: { ...base, status: "failed", error: safe }, result: undefined };
|
|
52
53
|
}
|
|
53
|
-
const terminal = result.
|
|
54
|
+
const terminal = result.repairTerminal;
|
|
55
|
+
const plane = terminalProjection(result.terminal);
|
|
54
56
|
const report = {
|
|
55
57
|
...base,
|
|
56
|
-
status: statusForTerminal(terminal),
|
|
57
|
-
repairTerminal: terminal,
|
|
58
|
+
status: terminal === undefined ? "failed" : statusForTerminal(terminal),
|
|
59
|
+
...(terminal !== undefined ? { repairTerminal: terminal } : {}),
|
|
58
60
|
...(result.stats ? { stats: result.stats } : {}),
|
|
59
|
-
...(
|
|
61
|
+
...(plane.errorMessage ? { error: plane.errorMessage } : {}),
|
|
60
62
|
};
|
|
61
63
|
let candidatePatch;
|
|
62
64
|
if (terminal !== "oracle.unprotected" && terminal !== "fixed") {
|
package/dist/leader/wire.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { terminalProjection } from "@sema-agent/core";
|
|
1
2
|
import { parseNumOrFail } from "../config.js";
|
|
2
3
|
import { execFileSync } from "node:child_process";
|
|
3
4
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -188,8 +189,8 @@ export function createLeaderRunner(cfg) {
|
|
|
188
189
|
limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
|
|
189
190
|
})
|
|
190
191
|
.result();
|
|
191
|
-
if (res.
|
|
192
|
-
throw new Error(`repair round ${round} did not complete (status=${res.status})`);
|
|
192
|
+
if (res.terminal.kind !== "completed")
|
|
193
|
+
throw new Error(`repair round ${round} did not complete (status=${terminalProjection(res.terminal).status})`);
|
|
193
194
|
return { costUsd: res.stats?.costMicroUsd !== undefined ? res.stats.costMicroUsd / 1e6 : undefined, note: res.result?.slice(0, 200) };
|
|
194
195
|
}
|
|
195
196
|
finally {
|
|
@@ -225,8 +226,8 @@ export function createLeaderRunner(cfg) {
|
|
|
225
226
|
limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
|
|
226
227
|
})
|
|
227
228
|
.result();
|
|
228
|
-
if (res.
|
|
229
|
-
throw new Error(`conflict-resolve round ${round} did not complete (status=${res.status})`);
|
|
229
|
+
if (res.terminal.kind !== "completed")
|
|
230
|
+
throw new Error(`conflict-resolve round ${round} did not complete (status=${terminalProjection(res.terminal).status})`);
|
|
230
231
|
return { costUsd: res.stats?.costMicroUsd !== undefined ? res.stats.costMicroUsd / 1e6 : undefined, note: res.result?.slice(0, 200) };
|
|
231
232
|
}
|
|
232
233
|
finally {
|
package/dist/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { Runner, createPermissionRuleStoreProvider, InMemoryToolResultStore, TtlSessionStore, uuidv7, defaultTaskRegistry, createAllowDenyPolicy, workflowsCapability, probeSearchBackend, describeStaticWiring, expandTiers, DEFAULT_SUBAGENT_TOOL_NAME } from "@sema-agent/core";
|
|
4
|
+
import { Runner, terminalProjection, createPermissionRuleStoreProvider, InMemoryToolResultStore, TtlSessionStore, uuidv7, defaultTaskRegistry, createAllowDenyPolicy, workflowsCapability, probeSearchBackend, describeStaticWiring, expandTiers, DEFAULT_SUBAGENT_TOOL_NAME } from "@sema-agent/core";
|
|
5
5
|
import { createSessionTitler } from "./session-titler.js";
|
|
6
6
|
import { posIntEnv } from "./session-watch.js";
|
|
7
7
|
import { selectEnvironmentTool } from "./capabilities/select-environment-tool.js";
|
|
@@ -20,7 +20,7 @@ import { assertGateIntentServiceable, buildForceDurableGateInertNotice, hasOpera
|
|
|
20
20
|
import { loadSkills } from "./capabilities/skills.js";
|
|
21
21
|
import { createRepoClient } from "./capabilities/repo-tools.js";
|
|
22
22
|
import { buildScenarios, builtinScenarioDetails } from "./capabilities/scenarios.js";
|
|
23
|
-
import {
|
|
23
|
+
import { createHandsRunnerSelector, withoutExecutionEnv } from "./capabilities/hands-lane.js";
|
|
24
24
|
import { createLogger } from "./observability/logger.js";
|
|
25
25
|
import { createToolTracer, createPermissionDeniedMeter } from "./observability/tool-trace.js";
|
|
26
26
|
import { createRegistryJwtVerifier } from "./auth-bridge.js";
|
|
@@ -302,8 +302,10 @@ async function main() {
|
|
|
302
302
|
toolPolicy: createAllowDenyPolicy({}),
|
|
303
303
|
});
|
|
304
304
|
metrics.inc("hook_llm_calls_total", { type: "agent" });
|
|
305
|
-
if (result.
|
|
306
|
-
|
|
305
|
+
if (result.terminal.kind !== "completed") {
|
|
306
|
+
const plane = terminalProjection(result.terminal);
|
|
307
|
+
return { ok: false, error: `hook agent ${plane.status}: ${clipHookErr(plane.errorMessage)}` };
|
|
308
|
+
}
|
|
307
309
|
return { ok: true, text: result.result ?? "" };
|
|
308
310
|
}
|
|
309
311
|
catch (e) {
|
|
@@ -728,9 +730,7 @@ async function main() {
|
|
|
728
730
|
: undefined,
|
|
729
731
|
sessionEventsMaxConnections: sessionWatchRegistry ? posIntEnv(process.env.SESSION_EVENTS_MAX_CONNS, 256, 100_000) : undefined,
|
|
730
732
|
};
|
|
731
|
-
const handsLanes = createHandsLaneRegistry();
|
|
732
733
|
const resolveSpec = createResolveSpec({
|
|
733
|
-
handsLanes,
|
|
734
734
|
config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver,
|
|
735
735
|
getCenterPrompts: () => configCenter.getCenterPrompts(),
|
|
736
736
|
getKeyResolver: () => configCenter.getKeyResolver(),
|
|
@@ -744,7 +744,7 @@ async function main() {
|
|
|
744
744
|
liveQuestionFace: question,
|
|
745
745
|
approverSeat: createRunnerDepsOnAsk(toolApproval),
|
|
746
746
|
});
|
|
747
|
-
const runnerFor = (
|
|
747
|
+
const runnerFor = createHandsRunnerSelector({ full: runner, handsless: handslessRunner });
|
|
748
748
|
const sideQuery = createSideQueryLane({ brain, config, getKeyResolver: () => configCenter.getKeyResolver() });
|
|
749
749
|
const server = createHttpServer({ runner, runnerFor, sideQuery, config, resolveSpec, stores, coordinators, seams, observability, governance, deployment, knobs });
|
|
750
750
|
runDenySweep = server.denyExpiredApprovals;
|
|
@@ -9,6 +9,14 @@ export interface FailOpenTagEntry {
|
|
|
9
9
|
}
|
|
10
10
|
/** 闭集 tag 词表。形=`<repo>.<domain>.<site>`(跨仓同形,便于三仓遥测并表)。 */
|
|
11
11
|
export declare const FAIL_OPEN_TAGS: {
|
|
12
|
+
readonly "server.terminal.persisted-plane-malformed": {
|
|
13
|
+
readonly cls: "F";
|
|
14
|
+
readonly note: "S-136 合并重扫确认项(`terminal.ts persistedPlaneOf`,持久 blob 的平面读面):盘上一条 `terminal` 因由形不合(`paused` 缺 gate / 词出闭集)让 core 的 `terminalProjection` 抛 ⇒ 本读面对这一行答**全格缺席**而不是整只 500。放行的最坏后果=一条坏行在列表/舰队读面上显示为无终局词;计数 + probe 带原句,便于按行回溯。";
|
|
15
|
+
};
|
|
16
|
+
readonly "server.trace.gate-outcome-defective": {
|
|
17
|
+
readonly cls: "F";
|
|
18
|
+
readonly note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`,写腿与两条读腿共用):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。";
|
|
19
|
+
};
|
|
12
20
|
readonly "server.task-mcp.source-label-dropped": {
|
|
13
21
|
readonly cls: "F";
|
|
14
22
|
readonly note: "B-036 / S-143(`mcp-source-label.ts readMcpSourceLabel`,请求腿 + center 腿共用):`mcpServers[].source`(分组标签,显示用、开集)坏形——空串 / 超 80 字 / 非字符串——**丢键留 server**。放行的最坏后果=这台 server 在 wiring_manifest.mcp[] 里没有分组标签(难看),不是门/权限/凭证;本地 config.d 腿由 settings-schema 1.7.0 在解析处拒,故本臂只在请求腿与远端 /effective 直读 JSON 腿可达。";
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { appendFileSync } from "node:fs";
|
|
2
2
|
export const FAIL_OPEN_TAGS = {
|
|
3
|
+
"server.terminal.persisted-plane-malformed": {
|
|
4
|
+
cls: "F",
|
|
5
|
+
note: "S-136 合并重扫确认项(`terminal.ts persistedPlaneOf`,持久 blob 的平面读面):盘上一条 `terminal` 因由形不合(`paused` 缺 gate / 词出闭集)让 core 的 `terminalProjection` 抛 ⇒ 本读面对这一行答**全格缺席**而不是整只 500。放行的最坏后果=一条坏行在列表/舰队读面上显示为无终局词;计数 + probe 带原句,便于按行回溯。",
|
|
6
|
+
},
|
|
7
|
+
"server.trace.gate-outcome-defective": {
|
|
8
|
+
cls: "F",
|
|
9
|
+
note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`,写腿与两条读腿共用):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。",
|
|
10
|
+
},
|
|
3
11
|
"server.task-mcp.source-label-dropped": {
|
|
4
12
|
cls: "F",
|
|
5
13
|
note: "B-036 / S-143(`mcp-source-label.ts readMcpSourceLabel`,请求腿 + center 腿共用):`mcpServers[].source`(分组标签,显示用、开集)坏形——空串 / 超 80 字 / 非字符串——**丢键留 server**。放行的最坏后果=这台 server 在 wiring_manifest.mcp[] 里没有分组标签(难看),不是门/权限/凭证;本地 config.d 腿由 settings-schema 1.7.0 在解析处拒,故本臂只在请求腿与远端 /effective 直读 JSON 腿可达。",
|
|
@@ -260,7 +260,7 @@ export function createMetrics() {
|
|
|
260
260
|
m.counter("stale_cache_conflict_total", "Warm session-cache entries evicted by a cross-instance write while cached (S25)");
|
|
261
261
|
m.counter("breaker_writethrough_failed_total", "Circuit-breaker cross-replica write-through failures (LOW), by backend");
|
|
262
262
|
m.counter("web_search_bad_payload_total", "Web-search provider responses whose results field was not an array (LOW), by provider");
|
|
263
|
-
m.counter("permission_denied_total", "Tool-gate denials by
|
|
263
|
+
m.counter("permission_denied_total", "Tool-gate denials by the LAYER whose verdict is the deny (core's DeniedBy: policy/hook/org/classifier/plan_mode/compliance/write_protection/ask_resolution; 'other' = this build cannot name the layer; label is `deniedBy` since server 7.64.0 — it was `source` over core's retired PermissionDeniedSource; goal B4 always-on deny meter)");
|
|
264
264
|
m.counter("permission_rule_events_total", "Persisted allow-rule seam events by event (persisted_rule_allowed = a rule authorized a call with nobody asked; rule_store_unreadable = the store failed to read so the call was adjudicated with ZERO rules)");
|
|
265
265
|
m.counter("compaction_events_total", "Compaction lifecycle events by outcome (started/completed/failed/skipped) and trigger");
|
|
266
266
|
m.counter("hook_llm_calls_total", "prompt/agent hook entries that completed a model call (hooks 3b), by type");
|
|
@@ -52,11 +52,26 @@ export declare const REDACT_INPUT_CAP = 65536;
|
|
|
52
52
|
* 脱敏**之后**按字节封顶(占位已替换完,切在占位中间不是泄漏)并带标记。 */
|
|
53
53
|
export declare const REDACT_OUTPUT_MAX_BYTES = 65000;
|
|
54
54
|
export declare function redactErrorMessage(raw: string | undefined): string | undefined;
|
|
55
|
-
/**
|
|
56
|
-
*
|
|
55
|
+
/**
|
|
56
|
+
* 同口:终态 `result` 整块也持久化(`GET /v1/runs/:id` 直出),写入前把里面的**自由文本座**脱敏,其余键
|
|
57
|
+
* 逐字不动;`undefined`/`null` 透传。
|
|
58
|
+
*
|
|
59
|
+
* 🔴 S-136(core 7.6.0 [ref] D-8):自由文本座**搬家了** —— 失败文案与阻塞理由从顶层平面键
|
|
60
|
+
* (`errorMessage` / `blockedReason`)搬进因由 `terminal` 的两条臂(`failed.message` / `blocked.reason`)。
|
|
61
|
+
* 本函数**两种形都认**,而这不是「兼容读臂」(codex 对抗复审 r1 [high],亲读验真后**改判**;首版删掉了旧键
|
|
62
|
+
* 臂,那是错的):本函数是**读盘面**,而 `task_run.result` / `task_event(done).result` 里躺着 7.63.0 及以前
|
|
63
|
+
* 写下的**真字节**,它们本来就是平面形。写口只剩因由一条(没有任何东西会再写出旧形),所以这里不存在
|
|
64
|
+
* 「两个写者」;而不认旧形的代价是**可复现的凭据回放**:上线前落盘的原文经 `GET /v1/runs/:id/events` 重新
|
|
65
|
+
* 明文外发(读面这一层纵深正是为那批行存在的)。⇒ 规则仍只有一条:**读盘认盘上真有的形,写盘只写一种形**。
|
|
66
|
+
*
|
|
67
|
+
* 读面也走本函数,所以 `terminal` 形上取 `unknown`:旧持久 blob 与非 core 的 Runner 实现给的形不可信,
|
|
68
|
+
* 运行期逐臂判形、不抛(与 `remoteEnvFailures` / `editedFiles` 两座同款不信任)。
|
|
69
|
+
*/
|
|
57
70
|
export declare function redactTerminalResult<T extends {
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
terminal?: unknown;
|
|
72
|
+
/** 旧形(≤7.63.0)的两个顶层自由文本座;形上取 `unknown` —— 读盘面对盘上字节不作假设。 */
|
|
73
|
+
errorMessage?: unknown;
|
|
74
|
+
blockedReason?: unknown;
|
|
60
75
|
remoteEnvFailures?: readonly {
|
|
61
76
|
message?: string | undefined;
|
|
62
77
|
}[] | undefined;
|
|
@@ -114,9 +129,12 @@ export declare function redactLeaderResultWithFlag(result: unknown): {
|
|
|
114
129
|
changed: boolean;
|
|
115
130
|
};
|
|
116
131
|
export declare function memoRedactedRead<T>(seat: string, raw: T, redact: (v: T) => T): T;
|
|
117
|
-
/** run 终态 result
|
|
118
|
-
*
|
|
119
|
-
*
|
|
132
|
+
/** run 终态 result 的读面记忆只按**可脱敏的那几叶**(`terminal` 的自由文本臂 / 旧形的两个顶层座 /
|
|
133
|
+
* `remoteEnvFailures[].message`)做键与留体——
|
|
134
|
+
* 第十六轮重扫:按整块 result 摘要每次轮询要 stringify 一份可能数 MB 的 result,且大 result 的脏行永远存不进去。两叶缺席
|
|
135
|
+
* ⇒ O(1) 直返(`redactTerminalResult` 本就同引用)。
|
|
136
|
+
* 🔴 S-136(core 7.6.0 D-8):新形把前两片搬进了 `terminal`(同属一个键,按它整只做叶更省);旧形的两片
|
|
137
|
+
* **照留**——盘上真有那样的行,而读面纵深正是为它们存在的(codex 对抗复审 r1 [high] 验真后补)。 */
|
|
120
138
|
export declare function memoRedactTerminalResult<T extends Parameters<typeof redactTerminalResult>[0]>(seat: string, result: T): T;
|
|
121
139
|
export declare function diffScanShadowCountForTest(): number;
|
|
122
140
|
export declare function resetDiffScanShadowCountForTest(): void;
|
|
@@ -81,14 +81,25 @@ export function redactTerminalResult(result) {
|
|
|
81
81
|
const r = result;
|
|
82
82
|
let changed = false;
|
|
83
83
|
const out = { ...r };
|
|
84
|
-
if (r.errorMessage
|
|
84
|
+
if (typeof r.errorMessage === "string") {
|
|
85
85
|
out.errorMessage = redactErrorMessage(r.errorMessage);
|
|
86
86
|
changed = true;
|
|
87
87
|
}
|
|
88
|
-
if (r.blockedReason
|
|
88
|
+
if (typeof r.blockedReason === "string") {
|
|
89
89
|
out.blockedReason = redactErrorMessage(r.blockedReason);
|
|
90
90
|
changed = true;
|
|
91
91
|
}
|
|
92
|
+
if (r.terminal !== null && typeof r.terminal === "object") {
|
|
93
|
+
const t = r.terminal;
|
|
94
|
+
if (t.kind === "failed" && typeof t.message === "string") {
|
|
95
|
+
out.terminal = { ...t, message: redactErrorMessage(t.message) };
|
|
96
|
+
changed = true;
|
|
97
|
+
}
|
|
98
|
+
else if (t.kind === "blocked" && typeof t.reason === "string") {
|
|
99
|
+
out.terminal = { ...t, reason: redactErrorMessage(t.reason) };
|
|
100
|
+
changed = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
92
103
|
if (r.remoteEnvFailures !== undefined) {
|
|
93
104
|
out.remoteEnvFailures = Array.isArray(r.remoteEnvFailures)
|
|
94
105
|
? r.remoteEnvFailures.map((f) => (f && typeof f === "object" && typeof f.message === "string" ? { ...f, message: redactErrorMessage(f.message) } : f))
|
|
@@ -355,9 +366,9 @@ export function memoRedactTerminalResult(seat, result) {
|
|
|
355
366
|
if (result === undefined || result === null)
|
|
356
367
|
return result;
|
|
357
368
|
const r = result;
|
|
358
|
-
if (r.errorMessage === undefined && r.blockedReason === undefined && r.remoteEnvFailures === undefined)
|
|
369
|
+
if (r.terminal === undefined && r.errorMessage === undefined && r.blockedReason === undefined && r.remoteEnvFailures === undefined)
|
|
359
370
|
return result;
|
|
360
|
-
const leaves = { errorMessage: r.errorMessage, blockedReason: r.blockedReason, remoteEnvFailures: r.remoteEnvFailures };
|
|
371
|
+
const leaves = { terminal: r.terminal, errorMessage: r.errorMessage, blockedReason: r.blockedReason, remoteEnvFailures: r.remoteEnvFailures };
|
|
361
372
|
const redacted = memoRedactedRead(seat, leaves, (l) => redactTerminalResult(l));
|
|
362
373
|
if (redacted === leaves)
|
|
363
374
|
return result;
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Hooks } from "@sema-agent/core";
|
|
2
2
|
import type { Logger } from "./logger.js";
|
|
3
3
|
import type { Metrics } from "./metrics.js";
|
|
4
|
-
/** 表内 ⇒ 收窄成 `PermissionDeniedSource`(类型守卫)。计量点用它判「这个词是不是本 build 认得的真词」。
|
|
5
|
-
* 导出是为了让运行期锚(`test/tool-trace.test.ts`,从装树 core 的 `.d.ts` 解析真词表)**复用同一个谓词**
|
|
6
|
-
* —— 测试自己另写一份判据 = 又一张手抄表,正是本文件刚消灭的那个病。 */
|
|
7
|
-
export declare function isKnownDenySource(v: unknown): v is PermissionDeniedSource;
|
|
8
4
|
/**
|
|
9
|
-
* ALWAYS-ON gate-deny meter (goal B4, 2026-07-08): `permission_denied_total{
|
|
10
|
-
* adjudicate chain DENIES a tool call, by
|
|
11
|
-
* the tracer is a default-OFF diagnostic (TOOL_TRACE=true), but deny frequency is an
|
|
12
|
-
* signal that must reach /metrics on every deployment — a spike in `
|
|
13
|
-
* asking for something the operator forbids; `
|
|
14
|
-
*
|
|
15
|
-
* permissionDenied slot, which composeHooks double-runs
|
|
5
|
+
* ALWAYS-ON gate-deny meter (goal B4, 2026-07-08): `permission_denied_total{deniedBy}` — how often the
|
|
6
|
+
* adjudicate chain DENIES a tool call, by the LAYER whose verdict is the deny. Deliberately SEPARATE from
|
|
7
|
+
* {@link createToolTracer}: the tracer is a default-OFF diagnostic (TOOL_TRACE=true), but deny frequency is an
|
|
8
|
+
* operational/security signal that must reach /metrics on every deployment — a spike in `deniedBy="policy"`
|
|
9
|
+
* means a task keeps asking for something the operator forbids; `deniedBy="org"` is the organization layer
|
|
10
|
+
* refusing, the one class an operator most wants to see on its own. main.ts composes this UNDER the tracer via
|
|
11
|
+
* composeHooks (both are void observers on the permissionDenied slot, which composeHooks double-runs
|
|
12
|
+
* deployment-first).
|
|
13
|
+
*
|
|
14
|
+
* 🔴 BREAKING(server 7.64.0,core 7.6.0 提货):标签名 `source` → `deniedBy`,词表从七词
|
|
15
|
+
* (`policy/hook/safety/shellGate/planMode/classifier/org`)换成八词
|
|
16
|
+
* (`policy/hook/org/classifier/plan_mode/compliance/write_protection/ask_resolution`)。仪表盘/告警规则要改。
|
|
16
17
|
*/
|
|
17
18
|
export declare function createPermissionDeniedMeter(metrics: Pick<Metrics, "inc">): Hooks;
|
|
18
19
|
/**
|
|
@@ -1,20 +1,13 @@
|
|
|
1
|
+
import { isDeniedBy } from "@sema-agent/core";
|
|
1
2
|
import { redactSecrets } from "../trace/redact.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
safety: true,
|
|
6
|
-
shellGate: true,
|
|
7
|
-
planMode: true,
|
|
8
|
-
classifier: true,
|
|
9
|
-
org: true,
|
|
10
|
-
};
|
|
11
|
-
export function isKnownDenySource(v) {
|
|
12
|
-
return typeof v === "string" && Object.hasOwn(KNOWN_DENY_SOURCE_TABLE, v);
|
|
3
|
+
function deniedByLabel(gate) {
|
|
4
|
+
const d = gate.disposition;
|
|
5
|
+
return d.kind === "denied" && isDeniedBy(d.deniedBy) ? d.deniedBy : "other";
|
|
13
6
|
}
|
|
14
7
|
export function createPermissionDeniedMeter(metrics) {
|
|
15
8
|
return {
|
|
16
9
|
permissionDenied(payload) {
|
|
17
|
-
metrics.inc("permission_denied_total", {
|
|
10
|
+
metrics.inc("permission_denied_total", { deniedBy: deniedByLabel(payload.gate) });
|
|
18
11
|
},
|
|
19
12
|
};
|
|
20
13
|
}
|
|
@@ -52,7 +45,9 @@ export function createToolTracer(logger) {
|
|
|
52
45
|
phase: "denied",
|
|
53
46
|
id: payload.toolCallId,
|
|
54
47
|
tool: payload.toolName,
|
|
55
|
-
|
|
48
|
+
deniedBy: deniedByLabel(payload.gate),
|
|
49
|
+
...(payload.gate.origin === undefined ? {} : { origin: payload.gate.origin }),
|
|
50
|
+
...(payload.gate.settlement === undefined ? {} : { settlement: payload.gate.settlement.kind }),
|
|
56
51
|
reason: clip(redactSecrets(payload.reason), 200),
|
|
57
52
|
arg: summarize(payload.input),
|
|
58
53
|
});
|
package/dist/parked-decide.d.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* 判别必须 parked-first:legacy 腿的 `resumeStream` 会绕开 bg registry(无 consumeParkedFlip、
|
|
9
9
|
* 行永 parked、生命周期分叉)。
|
|
10
10
|
*/
|
|
11
|
-
import type {
|
|
11
|
+
import type { HostDecision } from "./host-decision.js";
|
|
12
|
+
import type { BackgroundAgentRecord, BackgroundAgentStore, CheckpointStore, QuestionAnswer, TaskRegistry, ToolSpec } from "@sema-agent/core";
|
|
12
13
|
export interface ParkedAgentMatch {
|
|
13
14
|
handle: string;
|
|
14
15
|
row: BackgroundAgentRecord;
|
|
@@ -64,11 +65,14 @@ export interface ParkedDecideRequest {
|
|
|
64
65
|
/** cp.pendingAction 原样(outcome 铸造的 persisted 回落源,与 legacy 腿 D-1 姿势逐字同形)。 */
|
|
65
66
|
pendingAction: unknown;
|
|
66
67
|
decision: "approve" | "deny";
|
|
67
|
-
/** [ref] 件6③ core
|
|
68
|
+
/** [ref] 件6③ / S-136(core 7.6.0 S6-A):**这次结算的事实**,由调用方陈述(必填,理由与 legacy 腿
|
|
68
69
|
* `resumeCheckpoint` 的同名参数逐字同源 —— 本函数分辨不出谁在叫它,漏报要是编译错而不是假出处)。
|
|
70
|
+
* 🔴 宿主报**事实**(谁结束的 + 通道自报的归属),core 铸**词**:`human_allowed` / `human_refused` /
|
|
71
|
+
* `park_sla_expired` 由 core 在 resume 入口铸出来,写进行的 `resolvedOutcome.gateOutcome` 与那次调用的
|
|
72
|
+
* `tool_end.gate`。server 一个结算词都不再拼(core 对宿主自铸的词是 pre-CAS 响亮拒)。
|
|
69
73
|
* 今天唯一的调用方是 `server.ts` 的人为 `/decide`(那道门写着 `req !== undefined`,而内部 D-D SLA
|
|
70
|
-
* deny-sweep 恰是 req 缺席的那条腿,它对 parked 行另有静默跳过臂)⇒ 恒 `"
|
|
71
|
-
|
|
74
|
+
* deny-sweep 恰是 req 缺席的那条腿,它对 parked 行另有静默跳过臂)⇒ 恒 `decidedBy:"person"`。 */
|
|
75
|
+
hostDecision: HostDecision;
|
|
72
76
|
reason?: string;
|
|
73
77
|
/** D-1 decision-binding echo(操作员在 GET 看到的 boundCallId/boundInputHash 原样透传;server
|
|
74
78
|
* 绝不重算,core resume 侧做 fail-closed 等值校验——与 legacy 腿同姿势)。 */
|
package/dist/parked-decide.js
CHANGED
|
@@ -92,7 +92,7 @@ export async function decideParkedAgent(deps, req) {
|
|
|
92
92
|
...(req.decision === "approve" && req.binding?.updatedInput !== undefined ? { updatedInput: req.binding.updatedInput } : {}),
|
|
93
93
|
...(req.answer !== undefined ? { answer: req.answer } : {}),
|
|
94
94
|
...(req.reason !== undefined ? { reason: req.reason } : {}),
|
|
95
|
-
|
|
95
|
+
hostDecision: req.hostDecision,
|
|
96
96
|
};
|
|
97
97
|
const ctx = {
|
|
98
98
|
toolCallId: `drv-${ticket.claimId}`,
|
|
@@ -3,14 +3,11 @@ import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
|
-
import {
|
|
6
|
+
import { terminalProjection } from "@sema-agent/core";
|
|
7
|
+
import { AppendLog, atomicWriteFile, ensureDir, isProcessLive, processFingerprint, readJsonlRecords, sanitizePathComponent, writeThenLink } from "@sema-agent/core";
|
|
7
8
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
8
9
|
import { notifyRunTerminal } from "../observability/run-terminal-log.js";
|
|
9
10
|
const ownerEq = (a, b) => (a ?? null) === (b ?? null);
|
|
10
|
-
const PROCESS_FACTS = {
|
|
11
|
-
pid: process.pid,
|
|
12
|
-
startedAtMs: Date.now() - Math.round(process.uptime() * 1000),
|
|
13
|
-
};
|
|
14
11
|
const CLAIM_TAKEOVER_ATTEMPTS = 3;
|
|
15
12
|
const SAFE_PATH_COMPONENT = /^[A-Za-z0-9_.-]+$/;
|
|
16
13
|
const isSafePathComponent = (s) => s !== "" && s !== "." && s !== ".." && SAFE_PATH_COMPONENT.test(s);
|
|
@@ -22,17 +19,6 @@ function readClaimRecord(recs, sessionId) {
|
|
|
22
19
|
return undefined;
|
|
23
20
|
return c;
|
|
24
21
|
}
|
|
25
|
-
function isPidAlive(pid) {
|
|
26
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
27
|
-
return true;
|
|
28
|
-
try {
|
|
29
|
-
process.kill(pid, 0);
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
catch (e) {
|
|
33
|
-
return e.code !== "ESRCH";
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
22
|
const PARK_ORPHAN_SETTLE_MS = 30_000;
|
|
37
23
|
export class RunStoreStrictHydrateError extends Error {
|
|
38
24
|
name = "RunStoreStrictHydrateError";
|
|
@@ -46,7 +32,7 @@ export class FileRunStore {
|
|
|
46
32
|
events = new Map();
|
|
47
33
|
eventLogs = new Map();
|
|
48
34
|
checkpointProbe;
|
|
49
|
-
claimOwner = { nonce: randomBytes(12).toString("hex"), ...
|
|
35
|
+
claimOwner = { nonce: randomBytes(12).toString("hex"), ...processFingerprint() };
|
|
50
36
|
claimLocks = new Map();
|
|
51
37
|
logger;
|
|
52
38
|
strictHydrate;
|
|
@@ -382,7 +368,7 @@ export class FileRunStore {
|
|
|
382
368
|
r.status = status;
|
|
383
369
|
r.result = result;
|
|
384
370
|
r.error = error;
|
|
385
|
-
r.errorCode = result
|
|
371
|
+
r.errorCode = result === null ? null : (terminalProjection(result.terminal).errorCode ?? null);
|
|
386
372
|
r.updatedAt = new Date();
|
|
387
373
|
this.persist(r);
|
|
388
374
|
pending = {
|
|
@@ -393,7 +379,7 @@ export class FileRunStore {
|
|
|
393
379
|
resultTaskId: result?.taskId,
|
|
394
380
|
model: result?.model,
|
|
395
381
|
errorCode: r.errorCode ?? undefined,
|
|
396
|
-
errorMessage: result
|
|
382
|
+
errorMessage: (result === null ? undefined : terminalProjection(result.terminal).errorMessage) ?? error ?? undefined,
|
|
397
383
|
elapsedMs: Math.max(0, Date.now() - r.createdAt.getTime()),
|
|
398
384
|
};
|
|
399
385
|
}
|
|
@@ -728,7 +714,7 @@ export class FileRunStore {
|
|
|
728
714
|
async judgeClaim(sessionId, claim, holderTaskId) {
|
|
729
715
|
const owner = claim?.owner;
|
|
730
716
|
const who = owner
|
|
731
|
-
? `pid ${owner.pid} started ${new Date(owner.startedAtMs).toISOString()}`
|
|
717
|
+
? `pid ${owner.pid} on ${owner.host ?? "an unnamed host"} started ${owner.startedAtMs === null || owner.startedAtMs === undefined ? "(start time unobtainable)" : new Date(owner.startedAtMs).toISOString()}`
|
|
732
718
|
: "an owner-less claim (written before the liveness stamp existed)";
|
|
733
719
|
const torn = claim === undefined;
|
|
734
720
|
let row = !torn && holderTaskId ? this.authoritativeRow(holderTaskId) : undefined;
|
|
@@ -770,7 +756,7 @@ export class FileRunStore {
|
|
|
770
756
|
return { kind: "stale", reason: `parked run (${row.status}) has no resumable checkpoint — claim reclaimed`, snapshot };
|
|
771
757
|
}
|
|
772
758
|
const mine = owner?.nonce === this.claimOwner.nonce;
|
|
773
|
-
const ownerGone = owner !== undefined && !
|
|
759
|
+
const ownerGone = owner !== undefined && !isProcessLive(owner);
|
|
774
760
|
if (!mine && !ownerGone)
|
|
775
761
|
return live();
|
|
776
762
|
if (!row)
|
|
@@ -782,6 +768,11 @@ export class FileRunStore {
|
|
|
782
768
|
const probe = this.checkpointProbe;
|
|
783
769
|
if (probe && (await probe.hasPending(row.sessionId)))
|
|
784
770
|
return live();
|
|
771
|
+
this.logger?.error?.("file_run_store_claim_taken_over", {
|
|
772
|
+
sessionId, holder: holder || null, owner: owner ?? null,
|
|
773
|
+
pidStillRunning: owner !== undefined && isProcessLive({ pid: owner.pid }),
|
|
774
|
+
note: "the claim's owner process is provably not the one that wrote it (fingerprint falsified, or the pid is gone) and the row has nothing left to resume — taking the claim over",
|
|
775
|
+
});
|
|
785
776
|
return { kind: "stale", reason: `run orphaned by engine restart (claim held by ${who}, which is gone)`, snapshot };
|
|
786
777
|
}
|
|
787
778
|
takeOverClaim(sessionId, reason, snapshot) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { redactErrorMessage, redactLedgerEventData, redactTerminalResult } from "../observability/run-terminal-log.js";
|
|
2
2
|
import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
|
|
3
|
+
import { terminalProjection } from "@sema-agent/core";
|
|
3
4
|
import { notifyRunTerminal } from "../observability/run-terminal-log.js";
|
|
4
5
|
const ownerEq = (a, b) => (a ?? null) === (b ?? null);
|
|
5
6
|
export class MemoryRunStore {
|
|
@@ -119,7 +120,7 @@ export class MemoryRunStore {
|
|
|
119
120
|
r.status = status;
|
|
120
121
|
r.result = result;
|
|
121
122
|
r.error = error;
|
|
122
|
-
r.errorCode = result
|
|
123
|
+
r.errorCode = result === null ? null : (terminalProjection(result.terminal).errorCode ?? null);
|
|
123
124
|
r.updatedAt = new Date();
|
|
124
125
|
pending = {
|
|
125
126
|
runId: taskId,
|
|
@@ -129,7 +130,7 @@ export class MemoryRunStore {
|
|
|
129
130
|
resultTaskId: result?.taskId,
|
|
130
131
|
model: result?.model,
|
|
131
132
|
errorCode: r.errorCode ?? undefined,
|
|
132
|
-
errorMessage: result
|
|
133
|
+
errorMessage: (result === null ? undefined : terminalProjection(result.terminal).errorMessage) ?? error ?? undefined,
|
|
133
134
|
elapsedMs: Math.max(0, Date.now() - r.createdAt.getTime()),
|
|
134
135
|
};
|
|
135
136
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { redactErrorMessage, redactLedgerEventData, redactTerminalResult } from "../observability/run-terminal-log.js";
|
|
2
|
+
import { terminalProjection } from "@sema-agent/core";
|
|
2
3
|
import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
|
|
3
4
|
import { escapeLike } from "./sql-escape.js";
|
|
4
5
|
import { pgSanitizeText } from "./pg-safe-json.js";
|
|
@@ -118,7 +119,7 @@ export class SqlRunStore {
|
|
|
118
119
|
async setTerminal(taskId, status, result, error) {
|
|
119
120
|
result = redactTerminalResult(result);
|
|
120
121
|
error = error === null ? null : (redactErrorMessage(error) ?? null);
|
|
121
|
-
const errorCode = result
|
|
122
|
+
const errorCode = result === null ? null : (terminalProjection(result.terminal).errorCode ?? null);
|
|
122
123
|
let observed;
|
|
123
124
|
await this.tx(async (conn) => {
|
|
124
125
|
const w = await conn.query(this.q("UPDATE task_run SET status = ?, result = ?, error = ?, error_code = ?, updated_at = ? WHERE task_id = ? AND status IN ('running','suspended','needs_review')", "UPDATE task_run SET status = $1, result = $2::jsonb, error = $3, error_code = $4, updated_at = $5 WHERE task_id = $6 AND status IN ('running','suspended','needs_review')"), [
|
|
@@ -150,7 +151,7 @@ export class SqlRunStore {
|
|
|
150
151
|
resultTaskId: result?.taskId,
|
|
151
152
|
model: result?.model,
|
|
152
153
|
errorCode: errorCode ?? undefined,
|
|
153
|
-
errorMessage: result
|
|
154
|
+
errorMessage: (result === null ? undefined : terminalProjection(result.terminal).errorMessage) ?? error ?? undefined,
|
|
154
155
|
elapsedMs: observed.createdAt === undefined ? undefined : Math.max(0, Date.now() - observed.createdAt),
|
|
155
156
|
});
|
|
156
157
|
}
|