@sema-agent/server 7.56.0 → 7.57.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/README.md +3 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +101 -9
- package/dist/approval-ask-audit-store.d.ts +100 -1
- package/dist/approval-ask-audit-store.js +103 -2
- package/dist/approval-card.d.ts +148 -13
- package/dist/approval-card.js +82 -13
- package/dist/approval.d.ts +31 -0
- package/dist/approval.js +18 -0
- package/dist/auto-mode-face.d.ts +111 -0
- package/dist/auto-mode-face.js +99 -0
- package/dist/bench/s1/arms.js +5 -5
- package/dist/bench/s1/live-deps.js +11 -11
- package/dist/bench/s1/run-firm.js +3 -0
- package/dist/bench/s1/runner-ctx.d.ts +4 -0
- package/dist/bench/s1/runner-ctx.js +7 -0
- package/dist/boot/config-center.js +23 -1
- package/dist/boot/coordinators.js +13 -1
- package/dist/boot/parked-revive-gate.d.ts +7 -2
- package/dist/boot/parked-revive-gate.js +12 -1
- package/dist/boot/resolve-spec.d.ts +3 -0
- package/dist/boot/resolve-spec.js +3 -1
- package/dist/boot/runner-deps.d.ts +12 -0
- package/dist/boot/runner-deps.js +28 -3
- package/dist/boot/runtime-caps.d.ts +19 -9
- package/dist/boot/runtime-caps.js +49 -21
- package/dist/boot/session-shell-gate-registry.d.ts +39 -0
- package/dist/boot/session-shell-gate-registry.js +21 -0
- package/dist/config-catalog.js +14 -5
- package/dist/config-types.d.ts +41 -10
- package/dist/config.d.ts +21 -0
- package/dist/config.js +40 -5
- package/dist/http/route-ctx.d.ts +75 -0
- package/dist/http/routes/a2a-serve.js +5 -3
- package/dist/http/routes/approvals-assistant.js +19 -6
- package/dist/http/routes/capabilities.js +21 -4
- package/dist/http/routes/memory-origin.d.ts +2 -2
- package/dist/http/routes/rules.js +3 -2
- package/dist/http/routes/runs.d.ts +0 -14
- package/dist/http/routes/runs.js +20 -8
- package/dist/http/routes/tasks.js +34 -8
- package/dist/http/routes/workflows.js +19 -4
- package/dist/http/server.d.ts +11 -1
- package/dist/http/server.js +215 -43
- package/dist/http/wire-types.d.ts +9 -4
- package/dist/leader/diffout.js +2 -1
- package/dist/leader/endpoint.js +45 -12
- package/dist/leader/fanout.js +6 -5
- package/dist/leader/leader.js +17 -14
- package/dist/leader/merge.js +17 -12
- package/dist/leader/planner.js +3 -2
- package/dist/leader/repair-oracle.js +6 -4
- package/dist/leader/repair-wire.js +5 -3
- package/dist/leader/wire.d.ts +30 -1
- package/dist/leader/wire.js +36 -19
- package/dist/main.js +43 -7
- package/dist/observability/err-text.d.ts +6 -0
- package/dist/observability/err-text.js +10 -0
- package/dist/observability/fail-open.d.ts +40 -0
- package/dist/observability/fail-open.js +19 -0
- package/dist/observability/metrics.js +1 -1
- package/dist/observability/run-terminal-log.d.ts +120 -0
- package/dist/observability/run-terminal-log.js +360 -0
- package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
- package/dist/orchestration/workflow-completion-inbox.js +8 -0
- package/dist/permission-rule-vocab.d.ts +40 -0
- package/dist/permission-rule-vocab.js +17 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +14 -0
- package/dist/plugins/checkpoint-store-sql.js +19 -3
- package/dist/plugins/file-run-store.d.ts +3 -34
- package/dist/plugins/file-run-store.js +19 -0
- package/dist/plugins/local-checkpoint-store.d.ts +10 -0
- package/dist/plugins/local-checkpoint-store.js +3 -0
- package/dist/plugins/memory-run-store.d.ts +3 -15
- package/dist/plugins/memory-run-store.js +19 -0
- package/dist/plugins/permission-rule-store-file.js +8 -3
- package/dist/plugins/permission-rule-store-sql.js +16 -10
- package/dist/plugins/remote-scratchpad.js +3 -2
- package/dist/plugins/run-store-sql.d.ts +3 -42
- package/dist/plugins/run-store-sql.js +30 -1
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-contracts.d.ts +66 -1
- package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
- package/dist/plugins/workflow-run-store-sql.js +10 -2
- package/dist/rules-consent.js +20 -13
- package/dist/run-cancel-context.d.ts +13 -0
- package/dist/run-cancel-context.js +15 -0
- package/dist/run-local.js +2 -2
- package/dist/runs.d.ts +4 -1
- package/dist/runs.js +41 -10
- package/dist/runtime-caps-resolver.d.ts +133 -17
- package/dist/runtime-caps-resolver.js +41 -3
- package/dist/task-settings.d.ts +57 -3
- package/dist/task-settings.js +78 -5
- package/dist/task-workflow.d.ts +32 -2
- package/dist/task-workflow.js +8 -3
- package/dist/tool-approval.d.ts +26 -3
- package/dist/tool-approval.js +96 -13
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/ledger-sink.d.ts +13 -4
- package/dist/trace/ledger-sink.js +14 -6
- package/dist/trace/project.d.ts +13 -0
- package/dist/trace/project.js +10 -1
- package/dist/trace/redact.d.ts +22 -11
- package/dist/trace/redact.js +655 -20
- package/dist/trace/sema-provenance.d.ts +26 -0
- package/dist/trace/sema-provenance.js +11 -0
- package/dist/turn-activity.d.ts +32 -2
- package/dist/turn-activity.js +29 -4
- package/package.json +2 -2
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { redactSecrets } from "../trace/redact.js";
|
|
3
|
+
import { errorText } from "./err-text.js";
|
|
4
|
+
import { recordFailOpen } from "./fail-open.js";
|
|
5
|
+
const MESSAGE_CAP = 1024;
|
|
6
|
+
export function runTerminalLogLevel(status, errorCode) {
|
|
7
|
+
switch (status) {
|
|
8
|
+
case "failed":
|
|
9
|
+
return errorCode === "cancelled" ? "info" : "error";
|
|
10
|
+
case "blocked":
|
|
11
|
+
case "completed":
|
|
12
|
+
case "running":
|
|
13
|
+
case "suspended":
|
|
14
|
+
case "needs_review":
|
|
15
|
+
return undefined;
|
|
16
|
+
default: {
|
|
17
|
+
const unhandled = status;
|
|
18
|
+
void unhandled;
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
let observer;
|
|
24
|
+
export function setRunTerminalObserver(fn) {
|
|
25
|
+
observer = fn;
|
|
26
|
+
}
|
|
27
|
+
export function notifyRunTerminal(rec) {
|
|
28
|
+
const level = runTerminalLogLevel(rec.status, rec.errorCode);
|
|
29
|
+
if (level === undefined || !observer)
|
|
30
|
+
return;
|
|
31
|
+
const raw = rec.errorMessage;
|
|
32
|
+
const message = raw === undefined ? undefined : (redactErrorMessage(raw) ?? "").slice(0, MESSAGE_CAP);
|
|
33
|
+
observer(level, {
|
|
34
|
+
runId: rec.runId,
|
|
35
|
+
sessionId: rec.sessionId,
|
|
36
|
+
...(rec.owner !== null ? { owner: rec.owner } : {}),
|
|
37
|
+
status: rec.status,
|
|
38
|
+
...(rec.resultTaskId !== undefined && rec.resultTaskId !== rec.runId ? { taskId: rec.resultTaskId } : {}),
|
|
39
|
+
...(rec.model !== undefined ? { model: rec.model } : {}),
|
|
40
|
+
...(rec.errorCode !== undefined ? { errorCode: rec.errorCode } : {}),
|
|
41
|
+
...(message !== undefined ? { errorMessage: message } : {}),
|
|
42
|
+
...(rec.elapsedMs !== undefined ? { elapsedMs: rec.elapsedMs } : {}),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export const OVERSIZED_REDACTION_PLACEHOLDER = "«redacted:oversized»";
|
|
46
|
+
export const REDACT_INPUT_CAP = 65_536;
|
|
47
|
+
export const REDACT_OUTPUT_MAX_BYTES = 65_000;
|
|
48
|
+
const OUTPUT_TRUNCATED_MARK = "…[truncated: exceeds store column]";
|
|
49
|
+
function fitOutputBytes(s) {
|
|
50
|
+
if (Buffer.byteLength(s) <= REDACT_OUTPUT_MAX_BYTES)
|
|
51
|
+
return s;
|
|
52
|
+
recordFailOpen("server.run-terminal.output-truncated", `bytes=${Buffer.byteLength(s)} cap=${REDACT_OUTPUT_MAX_BYTES}`);
|
|
53
|
+
const budget = REDACT_OUTPUT_MAX_BYTES - Buffer.byteLength(OUTPUT_TRUNCATED_MARK);
|
|
54
|
+
let cut = Buffer.from(s).subarray(0, budget).toString();
|
|
55
|
+
if (cut.endsWith("\uFFFD"))
|
|
56
|
+
cut = cut.slice(0, -1);
|
|
57
|
+
return cut + OUTPUT_TRUNCATED_MARK;
|
|
58
|
+
}
|
|
59
|
+
export function redactErrorMessage(raw) {
|
|
60
|
+
if (raw === undefined)
|
|
61
|
+
return undefined;
|
|
62
|
+
let len = -1;
|
|
63
|
+
try {
|
|
64
|
+
const text = typeof raw === "string" ? raw : String(raw);
|
|
65
|
+
len = text.length;
|
|
66
|
+
if (len > REDACT_INPUT_CAP) {
|
|
67
|
+
recordFailOpen("server.run-terminal.redact-capped", `len=${len} cap=${REDACT_INPUT_CAP}`);
|
|
68
|
+
return OVERSIZED_REDACTION_PLACEHOLDER;
|
|
69
|
+
}
|
|
70
|
+
return fitOutputBytes(redactSecrets(text));
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
recordFailOpen("server.run-terminal.redact-threw", `len=${len} err=${errorText(err)}`);
|
|
74
|
+
return OVERSIZED_REDACTION_PLACEHOLDER;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function redactTerminalResult(result) {
|
|
78
|
+
if (result === undefined || result === null)
|
|
79
|
+
return result;
|
|
80
|
+
const r = result;
|
|
81
|
+
let changed = false;
|
|
82
|
+
const out = { ...r };
|
|
83
|
+
if (r.errorMessage !== undefined) {
|
|
84
|
+
out.errorMessage = redactErrorMessage(r.errorMessage);
|
|
85
|
+
changed = true;
|
|
86
|
+
}
|
|
87
|
+
if (r.blockedReason !== undefined) {
|
|
88
|
+
out.blockedReason = redactErrorMessage(r.blockedReason);
|
|
89
|
+
changed = true;
|
|
90
|
+
}
|
|
91
|
+
if (r.remoteEnvFailures !== undefined) {
|
|
92
|
+
out.remoteEnvFailures = Array.isArray(r.remoteEnvFailures)
|
|
93
|
+
? r.remoteEnvFailures.map((f) => (f && typeof f === "object" && typeof f.message === "string" ? { ...f, message: redactErrorMessage(f.message) } : f))
|
|
94
|
+
: r.remoteEnvFailures;
|
|
95
|
+
changed = true;
|
|
96
|
+
}
|
|
97
|
+
return changed ? out : result;
|
|
98
|
+
}
|
|
99
|
+
export function redactLedgerEventData(type, data) {
|
|
100
|
+
if (data === null || typeof data !== "object" || Array.isArray(data))
|
|
101
|
+
return data;
|
|
102
|
+
if (type === "done") {
|
|
103
|
+
const result = data.result;
|
|
104
|
+
if (result === null || typeof result !== "object")
|
|
105
|
+
return data;
|
|
106
|
+
return { ...data, result: redactTerminalResult(result) };
|
|
107
|
+
}
|
|
108
|
+
if (type === "failed") {
|
|
109
|
+
const msg = data.errorMessage;
|
|
110
|
+
if (typeof msg !== "string")
|
|
111
|
+
return data;
|
|
112
|
+
return { ...data, errorMessage: redactErrorMessage(msg) };
|
|
113
|
+
}
|
|
114
|
+
return data;
|
|
115
|
+
}
|
|
116
|
+
export function redactWorkerReportError(report) {
|
|
117
|
+
const gate = redactCheckpointGate(report.checkpointGate);
|
|
118
|
+
const error = report.error === undefined ? undefined : (redactErrorMessage(report.error) ?? "");
|
|
119
|
+
if (error === report.error && gate === report.checkpointGate)
|
|
120
|
+
return report;
|
|
121
|
+
return { ...report, ...(error !== report.error ? { error } : {}), ...(gate !== report.checkpointGate ? { checkpointGate: gate } : {}) };
|
|
122
|
+
}
|
|
123
|
+
export function redactCheckpointGate(gate) {
|
|
124
|
+
if (gate === null || typeof gate !== "object" || Array.isArray(gate))
|
|
125
|
+
return gate;
|
|
126
|
+
const g = gate;
|
|
127
|
+
let changed = false;
|
|
128
|
+
const out = { ...g };
|
|
129
|
+
const str = (v) => { const o = redactErrorMessage(v) ?? ""; if (o !== v)
|
|
130
|
+
changed = true; return o; };
|
|
131
|
+
const strs = (v) => (Array.isArray(v) ? v.map((x) => (typeof x === "string" ? str(x) : x)) : v);
|
|
132
|
+
if (typeof g.reason === "string")
|
|
133
|
+
out.reason = str(g.reason);
|
|
134
|
+
const rd = g.riskDescriptor;
|
|
135
|
+
if (rd && typeof rd === "object" && !Array.isArray(rd)) {
|
|
136
|
+
const r = rd;
|
|
137
|
+
const nr = { ...r };
|
|
138
|
+
for (const k of ["summary", "shadowedRule", "probeReason"])
|
|
139
|
+
if (typeof r[k] === "string")
|
|
140
|
+
nr[k] = str(r[k]);
|
|
141
|
+
if (Array.isArray(r.touchedPaths))
|
|
142
|
+
nr.touchedPaths = strs(r.touchedPaths);
|
|
143
|
+
const pc = r.probeCause;
|
|
144
|
+
if (pc && typeof pc === "object" && !Array.isArray(pc)) {
|
|
145
|
+
const p = pc;
|
|
146
|
+
const np = { ...p };
|
|
147
|
+
for (const k of ["roots", "further"]) {
|
|
148
|
+
const ops = p[k];
|
|
149
|
+
if (ops && typeof ops === "object" && !Array.isArray(ops) && Array.isArray(ops.shown))
|
|
150
|
+
np[k] = { ...ops, shown: strs(ops.shown) };
|
|
151
|
+
}
|
|
152
|
+
nr.probeCause = np;
|
|
153
|
+
}
|
|
154
|
+
out.riskDescriptor = nr;
|
|
155
|
+
}
|
|
156
|
+
return changed ? out : gate;
|
|
157
|
+
}
|
|
158
|
+
export const DIAG_INPUT_CAP = 262_144;
|
|
159
|
+
export const OVERSIZED_DIAG_PLACEHOLDER = "«redacted:oversized-diagnostic»";
|
|
160
|
+
export function redactDiagnosticText(text) {
|
|
161
|
+
if (text.length > DIAG_INPUT_CAP) {
|
|
162
|
+
recordFailOpen("server.run-terminal.diag-capped", `len=${text.length} cap=${DIAG_INPUT_CAP}`);
|
|
163
|
+
return `${OVERSIZED_DIAG_PLACEHOLDER} [len=${text.length}]`;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
return redactSecrets(text);
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
recordFailOpen("server.run-terminal.redact-threw", `diag len=${text.length} err=${err instanceof Error ? err.name : String(err)}`);
|
|
170
|
+
return OVERSIZED_REDACTION_PLACEHOLDER;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export function redactHead(text, keep) {
|
|
174
|
+
return redactDiagnosticText(text).slice(0, keep);
|
|
175
|
+
}
|
|
176
|
+
export const OVERSIZED_PATCH_PLACEHOLDER = "«redacted:oversized-patch-not-inlined»";
|
|
177
|
+
function diffScanRefusal(patch, direct) {
|
|
178
|
+
if (/^diff --(?:cc|combined) |^@{3,} /m.test(patch))
|
|
179
|
+
return "combined-diff";
|
|
180
|
+
const fence = /-{4,5} ?BEGIN[^\n-]*PRIVATE KEY(?: BLOCK)?[^\n-]*-{4,5}/;
|
|
181
|
+
if (fence.test(direct))
|
|
182
|
+
return "pem-fence-survived";
|
|
183
|
+
const ppkField = /Private-(?:MAC|Hash): [0-9a-fA-F]{32}/;
|
|
184
|
+
const ppkLinesField = /Private-Lines: \d+/;
|
|
185
|
+
const b64Line = /^[ +-]?[A-Za-z0-9+/=]{16,}[ \t]*$/m;
|
|
186
|
+
const ppkAnchorInSections = (t) => t.split(/^(?=diff --git )/m).some((sec) => ppkLinesField.test(sec) && b64Line.test(sec));
|
|
187
|
+
if (ppkAnchorInSections(patch))
|
|
188
|
+
return "ppk-field-survived";
|
|
189
|
+
if (ppkField.test(direct))
|
|
190
|
+
return "ppk-field-survived";
|
|
191
|
+
const strip1 = (t) => t.replace(/^[+-]/gm, "");
|
|
192
|
+
const stripFull = (t) => t.replace(/^[+-]+/gm, "");
|
|
193
|
+
const layers = [["1", strip1]];
|
|
194
|
+
if (/^[+-]{2}/m.test(patch))
|
|
195
|
+
layers.push(["full", stripFull]);
|
|
196
|
+
for (const [name, strip] of layers) {
|
|
197
|
+
shadowScans += 1;
|
|
198
|
+
const shadow = redactSecrets(strip(patch), { observe: false });
|
|
199
|
+
if (strip(direct) !== shadow)
|
|
200
|
+
return `shadow-diverged@${name}`;
|
|
201
|
+
if (fence.test(shadow))
|
|
202
|
+
return "pem-fence-survived";
|
|
203
|
+
if (ppkField.test(shadow))
|
|
204
|
+
return "ppk-field-survived";
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
export function redactPatchHead(patch, keep) {
|
|
209
|
+
if (patch.length > DIAG_INPUT_CAP) {
|
|
210
|
+
recordFailOpen("server.run-terminal.patch-head-cut", `not-inlined len=${patch.length} keep=${keep}`);
|
|
211
|
+
return `${OVERSIZED_PATCH_PLACEHOLDER} [len=${patch.length}]`;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const direct = redactSecrets(patch);
|
|
215
|
+
const refusal = diffScanRefusal(patch, direct);
|
|
216
|
+
if (refusal !== undefined) {
|
|
217
|
+
recordFailOpen("server.run-terminal.patch-head-cut", `${refusal} len=${patch.length} keep=${keep}`);
|
|
218
|
+
return `${OVERSIZED_PATCH_PLACEHOLDER} [len=${patch.length}]`;
|
|
219
|
+
}
|
|
220
|
+
return direct.slice(0, keep);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
recordFailOpen("server.run-terminal.redact-threw", `patch-head len=${patch.length} err=${err instanceof Error ? err.name : String(err)}`);
|
|
224
|
+
return OVERSIZED_REDACTION_PLACEHOLDER;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export function redactTail(text, keep) {
|
|
228
|
+
return redactDiagnosticText(text).slice(-keep);
|
|
229
|
+
}
|
|
230
|
+
export const ARTIFACT_INPUT_CAP = 262_144;
|
|
231
|
+
export const OVERSIZED_ARTIFACT_PLACEHOLDER = "«redacted:oversized-artifact»";
|
|
232
|
+
export function redactArtifactText(raw) {
|
|
233
|
+
if (raw.length > ARTIFACT_INPUT_CAP) {
|
|
234
|
+
recordFailOpen("server.run-terminal.artifact-capped", `len=${raw.length} cap=${ARTIFACT_INPUT_CAP}`);
|
|
235
|
+
return OVERSIZED_ARTIFACT_PLACEHOLDER;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const direct = redactSecrets(raw);
|
|
239
|
+
const refusal = diffScanRefusal(raw, direct);
|
|
240
|
+
if (refusal !== undefined) {
|
|
241
|
+
recordFailOpen("server.run-terminal.artifact-capped", `${refusal} len=${raw.length}`);
|
|
242
|
+
return `${OVERSIZED_ARTIFACT_PLACEHOLDER} [${refusal} len=${raw.length}]`;
|
|
243
|
+
}
|
|
244
|
+
return direct;
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
recordFailOpen("server.run-terminal.redact-threw", `artifact len=${raw.length} err=${err instanceof Error ? err.name : String(err)}`);
|
|
248
|
+
return OVERSIZED_REDACTION_PLACEHOLDER;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
export function redactLeaderResult(result) {
|
|
252
|
+
return redactLeaderResultWithFlag(result).value;
|
|
253
|
+
}
|
|
254
|
+
export function redactLeaderResultWithFlag(result) {
|
|
255
|
+
if (result === null || typeof result !== "object" || Array.isArray(result))
|
|
256
|
+
return { value: result, changed: false };
|
|
257
|
+
let changed = false;
|
|
258
|
+
const str = (v) => { if (typeof v !== "string")
|
|
259
|
+
return undefined; const o = redactErrorMessage(v) ?? ""; if (o !== v)
|
|
260
|
+
changed = true; return o; };
|
|
261
|
+
const artMemo = new Map();
|
|
262
|
+
const art = (v) => { if (typeof v !== "string")
|
|
263
|
+
return v; let o = artMemo.get(v); if (o === undefined) {
|
|
264
|
+
o = redactArtifactText(v);
|
|
265
|
+
artMemo.set(v, o);
|
|
266
|
+
} if (o !== v)
|
|
267
|
+
changed = true; return o; };
|
|
268
|
+
const obj = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : undefined);
|
|
269
|
+
const r = result;
|
|
270
|
+
const out = { ...r };
|
|
271
|
+
if (Array.isArray(r.reports))
|
|
272
|
+
out.reports = r.reports.map((w) => {
|
|
273
|
+
if (!obj(w))
|
|
274
|
+
return w;
|
|
275
|
+
const nw = redactWorkerReportError(w);
|
|
276
|
+
if (nw !== w)
|
|
277
|
+
changed = true;
|
|
278
|
+
return nw;
|
|
279
|
+
});
|
|
280
|
+
if (typeof r.error === "string")
|
|
281
|
+
out.error = str(r.error);
|
|
282
|
+
const replan = obj(r.replan);
|
|
283
|
+
if (replan && typeof replan.collapsed === "string")
|
|
284
|
+
out.replan = { ...replan, collapsed: str(replan.collapsed) };
|
|
285
|
+
const merge = obj(r.merge);
|
|
286
|
+
if (merge) {
|
|
287
|
+
const m = { ...merge };
|
|
288
|
+
for (const k of ["reason", "details"])
|
|
289
|
+
if (typeof merge[k] === "string")
|
|
290
|
+
m[k] = str(merge[k]);
|
|
291
|
+
for (const k of ["rej", "candidatePatch"])
|
|
292
|
+
if (typeof merge[k] === "string")
|
|
293
|
+
m[k] = art(merge[k]);
|
|
294
|
+
const measure = obj(merge.measure);
|
|
295
|
+
if (measure && typeof measure.reason === "string")
|
|
296
|
+
m.measure = { ...measure, reason: str(measure.reason) };
|
|
297
|
+
out.merge = m;
|
|
298
|
+
}
|
|
299
|
+
const cp = obj(r.candidatePatch);
|
|
300
|
+
if (cp)
|
|
301
|
+
out.candidatePatch = { ...cp, ...(typeof cp.reason === "string" ? { reason: str(cp.reason) } : {}), ...(typeof cp.patch === "string" ? { patch: art(cp.patch) } : {}) };
|
|
302
|
+
if (Array.isArray(r.salvaged))
|
|
303
|
+
out.salvaged = r.salvaged.map((x) => (obj(x) && typeof x.patch === "string" ? { ...x, patch: art(x.patch) } : x));
|
|
304
|
+
return { value: changed ? out : result, changed };
|
|
305
|
+
}
|
|
306
|
+
const READ_MEMO_CLEAN_CAP = 8192;
|
|
307
|
+
const READ_MEMO_DIRTY_CAP = 512;
|
|
308
|
+
const READ_MEMO_DIRTY_MAX_ENTRY_BYTES = 262_144;
|
|
309
|
+
const READ_MEMO_DIRTY_MAX_TOTAL_BYTES = 16 * 1_048_576;
|
|
310
|
+
const readMemoClean = new Set();
|
|
311
|
+
const readMemoDirty = new Map();
|
|
312
|
+
let readMemoDirtyBytes = 0;
|
|
313
|
+
export function memoRedactedRead(seat, raw, redact) {
|
|
314
|
+
const key = `${seat}\u0000${createHash("sha256").update(typeof raw === "string" ? raw : JSON.stringify(raw) ?? "").digest("hex")}`;
|
|
315
|
+
if (readMemoClean.has(key))
|
|
316
|
+
return raw;
|
|
317
|
+
const hit = readMemoDirty.get(key);
|
|
318
|
+
if (hit)
|
|
319
|
+
return hit.value;
|
|
320
|
+
const out = redact(raw);
|
|
321
|
+
const clean = out === raw || (typeof out === "string" && out === raw) || JSON.stringify(out) === JSON.stringify(raw);
|
|
322
|
+
if (clean) {
|
|
323
|
+
if (readMemoClean.size >= READ_MEMO_CLEAN_CAP)
|
|
324
|
+
readMemoClean.delete(readMemoClean.values().next().value);
|
|
325
|
+
readMemoClean.add(key);
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
const bytes = Buffer.byteLength(typeof out === "string" ? out : JSON.stringify(out));
|
|
329
|
+
if (bytes <= READ_MEMO_DIRTY_MAX_ENTRY_BYTES) {
|
|
330
|
+
while (readMemoDirty.size > 0 && (readMemoDirty.size >= READ_MEMO_DIRTY_CAP || readMemoDirtyBytes + bytes > READ_MEMO_DIRTY_MAX_TOTAL_BYTES)) {
|
|
331
|
+
const oldest = readMemoDirty.keys().next().value;
|
|
332
|
+
readMemoDirtyBytes -= readMemoDirty.get(oldest).bytes;
|
|
333
|
+
readMemoDirty.delete(oldest);
|
|
334
|
+
}
|
|
335
|
+
readMemoDirty.set(key, { value: out, bytes });
|
|
336
|
+
readMemoDirtyBytes += bytes;
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
export function memoRedactTerminalResult(seat, result) {
|
|
341
|
+
if (result === undefined || result === null)
|
|
342
|
+
return result;
|
|
343
|
+
const r = result;
|
|
344
|
+
if (r.errorMessage === undefined && r.blockedReason === undefined && r.remoteEnvFailures === undefined)
|
|
345
|
+
return result;
|
|
346
|
+
const leaves = { errorMessage: r.errorMessage, blockedReason: r.blockedReason, remoteEnvFailures: r.remoteEnvFailures };
|
|
347
|
+
const redacted = memoRedactedRead(seat, leaves, (l) => redactTerminalResult(l));
|
|
348
|
+
if (redacted === leaves)
|
|
349
|
+
return result;
|
|
350
|
+
return { ...result, ...redacted };
|
|
351
|
+
}
|
|
352
|
+
let shadowScans = 0;
|
|
353
|
+
export function diffScanShadowCountForTest() { return shadowScans; }
|
|
354
|
+
export function resetDiffScanShadowCountForTest() { shadowScans = 0; }
|
|
355
|
+
export function resetReadRedactionMemoForTest() {
|
|
356
|
+
readMemoClean.clear();
|
|
357
|
+
readMemoDirty.clear();
|
|
358
|
+
readMemoDirtyBytes = 0;
|
|
359
|
+
}
|
|
360
|
+
//# sourceMappingURL=run-terminal-log.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SemaProvenanceInput } from "../trace/sema-provenance.js";
|
|
1
2
|
import type { SystemInjectionPriority } from "@sema-agent/core";
|
|
2
3
|
import type { LedgerEventType } from "../trace/ledger-events.js";
|
|
3
4
|
/** One pending completion, scoped to the session that must be told about it. `summary` is ALREADY
|
|
@@ -259,6 +260,14 @@ export declare function taskNotificationInboxEntry(sessionId: string, owner: str
|
|
|
259
260
|
resumable?: boolean;
|
|
260
261
|
/** core 1.353([ref]):诊断槽(journal 坐标+教句,引擎值单源)——park 透传,drain 帧原样带。 */
|
|
261
262
|
diagnostics?: string;
|
|
263
|
+
/** [ref] [ref] 片6 d1(core 7.2.0):子代→主代 uplink 帧的渲染判别成员 + typed 归因侧记。park 是
|
|
264
|
+
* **空闲态**(父 leg 已收尾)到达的那条路——恰是 [ref](b) 的场景;不过桥 ⇒ drain 帧无判别键、cli 渲老形卡。
|
|
265
|
+
* body = UNTRUSTED 子代文本 → redact+cap(与 result 同姿势);provenance 成员逐挑 verbatim(引擎值)。 */
|
|
266
|
+
agentMessage?: {
|
|
267
|
+
from: string;
|
|
268
|
+
body: string;
|
|
269
|
+
};
|
|
270
|
+
_sema_provenance?: SemaProvenanceInput;
|
|
262
271
|
}, enqueuedAt: number,
|
|
263
272
|
/** lead-b (dual-ID audit): the PARENT run's durable taskId (the uuid the shell's footer rows
|
|
264
273
|
* key on) — rides the drained frame so a consumer can bind the a*-keyed child notification to its uuid-keyed
|
|
@@ -2,6 +2,7 @@ import { mkdirSync, readFileSync, existsSync, openSync, writeSync, fsyncSync, cl
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { redactSecrets } from "../trace/redact.js";
|
|
4
4
|
import { injectionTierOrAbsent } from "../trace/injection-tier.js";
|
|
5
|
+
import { semaProvenanceOrAbsent } from "../trace/sema-provenance.js";
|
|
5
6
|
export const MAX_PENDING_PER_SESSION = 100;
|
|
6
7
|
export const COMPACT_EVERY = 512;
|
|
7
8
|
export const PURGE_FENCE_MS = 10 * 60 * 1000;
|
|
@@ -314,6 +315,13 @@ export function taskNotificationInboxEntry(sessionId, owner, n, enqueuedAt, pare
|
|
|
314
315
|
...(n.resumable !== undefined ? { resumable: n.resumable } : {}),
|
|
315
316
|
...(n.result ? { result: cap(redactSecrets(n.result), 4000) } : {}),
|
|
316
317
|
...(n.output_file ? { output_file: cap(redactSecrets(n.output_file), 1024) } : {}),
|
|
318
|
+
...(n.agentMessage !== undefined ? { agentMessage: { from: n.agentMessage.from, body: cap(redactSecrets(n.agentMessage.body), 4000) } } : {}),
|
|
319
|
+
...(() => {
|
|
320
|
+
if (n._sema_provenance === undefined)
|
|
321
|
+
return {};
|
|
322
|
+
const p = semaProvenanceOrAbsent(n._sema_provenance);
|
|
323
|
+
return p !== undefined ? { _sema_provenance: p } : {};
|
|
324
|
+
})(),
|
|
317
325
|
};
|
|
318
326
|
return {
|
|
319
327
|
sessionId,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [ref] + [ref](core `e298c9e0`,随 7.2.x 提货生效;[ref])—— 规则车道**闭词表的单一属主**。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要一个模块而不是各处各写一份 `z.enum([...])`:同一张词表此前在四处各抄一份字面量
|
|
5
|
+
* (`approval-card.ts` 两腿的 offer schema、`http/routes/rules.ts` 的直通票口行形、
|
|
6
|
+
* `plugins/permission-rule-store-sql.ts` 的 `rules_json` 读/写形),core 给 `PersistedRuleMatch` 加两员
|
|
7
|
+
* (wildcard / subpath)、给 `PersistedRuleTool` 加 Read 时,四份抄件**没有一份**会编译红 —— zod 的 enum
|
|
8
|
+
* 只是运行期的一个 Set,静默把一次上游语义扩张翻成一整类合法行的拒收([ref] 词表纪律说的正是这形:
|
|
9
|
+
* 未知词必须是**编译**错,不是运行期一个安静的 false)。
|
|
10
|
+
*
|
|
11
|
+
* 🔴 每张表都带一条**双向全等钉**(`Exact<>`):元组的成员并 ≡ core 的联合型。core 加员 ⇒ 这里编译红,
|
|
12
|
+
* 由人裁定新词在每个消费面的处置(收 / 拒 / 剥),而不是让它在某一处静默落进「不认识」臂;本仓多写一个
|
|
13
|
+
* core 没有的词 ⇒ 同样红(词表属主是 core,本仓只转录)。
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ 转录**不是**判据:一个词在表里只说「这是 core 定义的一员」,不说「本仓每条腿都接受它」。逐腿的收窄
|
|
16
|
+
* (例如卡编辑臂对 `subpath` 的拒)写在那条腿自己的穷举 `switch` 里,并各带理由。
|
|
17
|
+
*/
|
|
18
|
+
import type { RuleApprovalRecord } from "@sema-agent/core";
|
|
19
|
+
/** `PersistedRuleTool`([ref] §2.2:`"Read"` 只说一种文法 —— `Read(//abs-dir/**)` 目录只读授权)。 */
|
|
20
|
+
export declare const PERSISTED_RULE_TOOLS: readonly ["Bash", "Read"];
|
|
21
|
+
/**
|
|
22
|
+
* `PersistedRuleMatch` 四员(core 顶注逐字):`exact` / `prefix`(历史 colon-star,永久兼容读、复合前缀形的
|
|
23
|
+
* 唯一拼写)/ `wildcard`([ref] 尾 space-star,单命令体上与 prefix **同一谓词**,建议面自此铸它)/
|
|
24
|
+
* `subpath`(Read 目录形,永不容纳任何命令)。
|
|
25
|
+
*/
|
|
26
|
+
export declare const PERSISTED_RULE_MATCHES: readonly ["exact", "prefix", "wildcard", "subpath"];
|
|
27
|
+
/** [ref] §2.3 B3:batch offer 成员的判别位(`command` = 历史逐段 Bash 规则;`directoryRead` = cd 段铸的目录只读授权)。 */
|
|
28
|
+
export declare const RULE_OFFER_BATCH_MEMBER_KINDS: readonly ["command", "directoryRead"];
|
|
29
|
+
/** [ref] §3.5:一段「为什么仍未被覆盖」的闭三词集(`redirection` 永久逐次 / `no_rule_form` 无可覆盖形 / `cap_overflow` 去重+帽挤出)。 */
|
|
30
|
+
export declare const UNCOVERED_SEGMENT_REASONS: readonly ["redirection", "no_rule_form", "cap_overflow"];
|
|
31
|
+
/** [ref] §3.3-5:卡编辑面的 breadth 警告码(记录 `edited.warnings` 存的就是这两个字面)。 */
|
|
32
|
+
export declare const EDITED_RULE_BREADTH_WARNING_CODES: readonly ["broad_prefix", "compound_prefix"];
|
|
33
|
+
/**
|
|
34
|
+
* [ref] §4.5 / [ref] §3.5 —— 审批记录的**形版本号**(core `RuleApprovalRecord.schema`,本批 2→3)。
|
|
35
|
+
* 类型绑 core 的字面型:core 再升号 ⇒ 这里编译红,两只 durable 店(SQL 双方言 + File)的 stale 判别与写侧钉
|
|
36
|
+
* 同批随之;判别是**等式**(`!==`)不是下限 —— 旧号的行按 stale 信封交还(不迁移、不 widen、不丢行),
|
|
37
|
+
* pending 卡短命,重触发即取新卡(B7/B8 既诺)。
|
|
38
|
+
*/
|
|
39
|
+
export declare const RULE_APPROVAL_RECORD_SCHEMA: RuleApprovalRecord["schema"];
|
|
40
|
+
//# sourceMappingURL=permission-rule-vocab.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const PERSISTED_RULE_TOOLS = ["Bash", "Read"];
|
|
2
|
+
const TOOLS_PIN = true;
|
|
3
|
+
void TOOLS_PIN;
|
|
4
|
+
export const PERSISTED_RULE_MATCHES = ["exact", "prefix", "wildcard", "subpath"];
|
|
5
|
+
const MATCHES_PIN = true;
|
|
6
|
+
void MATCHES_PIN;
|
|
7
|
+
export const RULE_OFFER_BATCH_MEMBER_KINDS = ["command", "directoryRead"];
|
|
8
|
+
const MEMBER_KINDS_PIN = true;
|
|
9
|
+
void MEMBER_KINDS_PIN;
|
|
10
|
+
export const UNCOVERED_SEGMENT_REASONS = ["redirection", "no_rule_form", "cap_overflow"];
|
|
11
|
+
const REASONS_PIN = true;
|
|
12
|
+
void REASONS_PIN;
|
|
13
|
+
export const EDITED_RULE_BREADTH_WARNING_CODES = ["broad_prefix", "compound_prefix"];
|
|
14
|
+
const WARNING_CODES_PIN = true;
|
|
15
|
+
void WARNING_CODES_PIN;
|
|
16
|
+
export const RULE_APPROVAL_RECORD_SCHEMA = 3;
|
|
17
|
+
//# sourceMappingURL=permission-rule-vocab.js.map
|
|
@@ -653,6 +653,20 @@ export declare class SqlCheckpointStore implements CheckpointStore {
|
|
|
653
653
|
* caller may only decide a checkpoint in their OWN scope. Returns `undefined` (no pending row), `null`
|
|
654
654
|
* (anonymous/dev session — open), or the owner scope string.
|
|
655
655
|
*/
|
|
656
|
+
/**
|
|
657
|
+
* [ref]:**按 token** 的 scope 轻读 —— `undefined`(无该行)/ `null`(匿名/开放)/ 属主 scope 串。
|
|
658
|
+
*
|
|
659
|
+
* 为什么必须有这一口(codex [ref] r1-[medium],验真后修):`peekPendingScope` 是**按 session** 的读,答的
|
|
660
|
+
* 是「此刻绑定下最新的那一行」;而 `get(token)` 读的是**那一个** token。表无 per-session pending 唯一
|
|
661
|
+
* 约束、两读又非事务 ⇒ 并发轮换窗内两者可以是不同的行。于是「按 session 轻读出的属主」不能拿来当
|
|
662
|
+
* 「可否把 `get(token)` 的失败细节回显给这位调用方」的准入位 —— 那正是 [ref] 要封的存在性 oracle 在
|
|
663
|
+
* TOCTOU 面上的复活形。本口把属主证明与**将要载入的那个 token** 原子绑定(同一 `WHERE token = ?`)。
|
|
664
|
+
*
|
|
665
|
+
* 刻意**不带** `status='pending'` 谓词:判据是「这一行是不是你的」,与它此刻处在哪一态无关(`get()`
|
|
666
|
+
* 自己也是裸 `WHERE token = ?`);加了反而在「刚被并发消费」的窗里把属主自己的诊断吞掉。
|
|
667
|
+
* 只 SELECT `scope` 一列 ⇒ 版本闸/坏 blob 都影响不到它(那是 `get()` 读 `version`/`checkpoint` 的事)。
|
|
668
|
+
*/
|
|
669
|
+
peekScopeByToken(token: CheckpointToken): Promise<string | null | undefined>;
|
|
656
670
|
peekPendingScope(sessionId: string, binding?: PendingRowBinding): Promise<string | null | undefined>;
|
|
657
671
|
/** Upsert the resume rebuild inputs for a session (written at SUBMIT when durable mode is on). */
|
|
658
672
|
putCtx(sessionId: string, ctx: unknown): Promise<void>;
|
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { APPROVAL_GATE_KINDS_SQL_IN } from "../tool-approval.js";
|
|
4
4
|
import { CheckpointError, MAX_RULE_TEXT_CHARS, validatePendingSteer, appendPendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION, } from "@sema-agent/core";
|
|
5
5
|
import { redactDeep, redactSecrets } from "../trace/redact.js";
|
|
6
|
-
import { MAX_RULE_OFFERS, MAX_RULE_OFFER_BATCH_MEMBERS, RuleOfferRawSchema } from "../approval-card.js";
|
|
6
|
+
import { MAX_RULE_OFFERS, MAX_RULE_OFFER_BATCH_MEMBERS, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat } from "../approval-card.js";
|
|
7
7
|
import { parseJsonStrict as parseJson } from "./sql-row-helpers.js";
|
|
8
8
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
9
9
|
import { mysqlDriver, pgDriver, dialectProtocolJsonEncoder } from "./sql-driver.js";
|
|
@@ -69,7 +69,7 @@ export function boundedRuleOffers(raw, opts = {}) {
|
|
|
69
69
|
for (const item of raw.slice(0, MAX_RULE_OFFER_SCAN)) {
|
|
70
70
|
if (out.length >= MAX_RULE_OFFERS)
|
|
71
71
|
break;
|
|
72
|
-
const parsed = RuleOfferRawSchema.safeParse(preboundBatchMembers(item));
|
|
72
|
+
const parsed = RuleOfferRawSchema.safeParse(screenUncoveredDetailSeat(backfillLegacyBatchMemberKind(preboundBatchMembers(item))));
|
|
73
73
|
if (!parsed.success)
|
|
74
74
|
continue;
|
|
75
75
|
if (parsed.data.kind === "single") {
|
|
@@ -78,8 +78,18 @@ export function boundedRuleOffers(raw, opts = {}) {
|
|
|
78
78
|
else {
|
|
79
79
|
out.push({
|
|
80
80
|
kind: "batch",
|
|
81
|
-
rules: parsed.data.rules.slice(0, MAX_RULE_OFFER_BATCH_MEMBERS).map((r) =>
|
|
81
|
+
rules: parsed.data.rules.slice(0, MAX_RULE_OFFER_BATCH_MEMBERS).map((r) => {
|
|
82
|
+
switch (r.kind) {
|
|
83
|
+
case "command":
|
|
84
|
+
return { kind: "command", rule: scrub(r.rule), match: r.match, command: scrub(r.command), segment: scrub(r.segment) };
|
|
85
|
+
case "directoryRead":
|
|
86
|
+
return { kind: "directoryRead", rule: scrub(r.rule), directory: scrub(r.directory), segment: scrub(r.segment) };
|
|
87
|
+
default:
|
|
88
|
+
throw new Error(`unknown rule-offer batch member kind: ${JSON.stringify(r)}`);
|
|
89
|
+
}
|
|
90
|
+
}),
|
|
82
91
|
uncoveredSegments: parsed.data.uncoveredSegments,
|
|
92
|
+
...(parsed.data.uncoveredDetail !== undefined ? { uncoveredDetail: parsed.data.uncoveredDetail.map((d) => ({ segment: scrub(d.segment), reason: d.reason })) } : {}),
|
|
83
93
|
});
|
|
84
94
|
}
|
|
85
95
|
}
|
|
@@ -539,6 +549,12 @@ export class SqlCheckpointStore {
|
|
|
539
549
|
};
|
|
540
550
|
});
|
|
541
551
|
}
|
|
552
|
+
async peekScopeByToken(token) {
|
|
553
|
+
const { rows } = await this.db.query(this.q("SELECT scope FROM checkpoint WHERE token = ?", "SELECT scope FROM checkpoint WHERE token = $1"), [token]);
|
|
554
|
+
if (!rows[0])
|
|
555
|
+
return undefined;
|
|
556
|
+
return rows[0].scope ?? null;
|
|
557
|
+
}
|
|
542
558
|
async peekPendingScope(sessionId, binding) {
|
|
543
559
|
const { sql, params } = this.pendingRowQuery("scope", sessionId, undefined, binding);
|
|
544
560
|
const { rows } = await this.db.query(sql, params);
|
|
@@ -1,37 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File-backed async run registry + replayable event log — the DURABLE local twin of {@link MemoryRunStore} (and the
|
|
3
|
-
* file-backed sibling of {@link TiDBRunStore}/{@link PgRunStore}). P0.5 variant-1 (clay 2026-06-26): the file-backed
|
|
4
|
-
* follow-on the Memory twin's header promised — a locally-run HTTP service now keeps its runs ledger + trace event log
|
|
5
|
-
* across a process restart, not just in memory. Behaviour is byte-identical to MemoryRunStore (the shared parity oracle
|
|
6
|
-
* — same single-active claim, the suspend/needs_review parks that KEEP the claim, the markResuming CAS that resets
|
|
7
|
-
* cancel/preempt, the owner null-safe guard, the keyset list order, the (taskId,seq) event dedup, and the structuredClone
|
|
8
|
-
* write/read isolation); the only difference is durability.
|
|
9
|
-
*
|
|
10
|
-
* THREE on-disk data planes (all under `<root>/runs/`), each using core's crash-safe fs primitives — never hand-rolled
|
|
11
|
-
* fs (zero drift, no zero-length/torn corruption):
|
|
12
|
-
* 1. run registry — `<root>/runs/<taskId>/run.json` = the RunRow, replaced WHOLE on every status change via
|
|
13
|
-
* `atomicWriteFile` (tmp→fsync→rename→fsync-dir, the 5-step atomic replace).
|
|
14
|
-
* 2. single-active claim — `<root>/runs/active/<sanitizePathComponent(sessionId)>` carrying the active taskId, created
|
|
15
|
-
* with `writeThenLink` (atomic create; EEXIST ⇒ the session already has an active run → the
|
|
16
|
-
* createRun loses the claim, returns `{ok:false, activeTaskId}`). The file version of task_active's
|
|
17
|
-
* unique key.
|
|
18
|
-
* 3. per-run event log — `<root>/runs/<taskId>/events.jsonl` = an `AppendLog` (one line per event, fsync:false — events
|
|
19
|
-
* are high-frequency, the torn-tail-drop on replay covers a crash tail; only the run.json status
|
|
20
|
-
* commit + the active-claim create use fsync). (taskId,seq) is deduped on replay.
|
|
21
|
-
*
|
|
22
|
-
* An in-memory INDEX (the same Maps MemoryRunStore holds) is hydrated on construction by scanning each runs/<taskId>/
|
|
23
|
-
* run.json + the active claim files — so listRuns/listSessions/getActiveTaskId are O(in-mem) with byte-identical keyset sort to the SQL/Memory
|
|
24
|
-
* twins. Every write touches BOTH the disk and the index on one path. Single-process (the FileStorageBackend boot lock
|
|
25
|
-
* guarantees one writer per data dir), so there is no per-op lock — with ONE exception: [ref]'s claim acquisition
|
|
26
|
-
* (`withClaimLock`) runs under a per-session in-process critical section, because judging a claim stale involves
|
|
27
|
-
* awaits (the checkpoint probe) and the claim + run row must commit as one unit (the SQL twin's transaction). The
|
|
28
|
-
* checkpoint-coupled suspended-run reapers run
|
|
29
|
-
* off an injected {@link RunStoreCheckpointProbe} ([ref]; wired by the local backend where checkpoint + run store
|
|
30
|
-
* share one root) and degrade to honest no-ops when the probe is absent — exactly like MemoryRunStore.
|
|
31
|
-
*/
|
|
32
1
|
import { type UsageRow } from "../usage-analytics.js";
|
|
33
|
-
import type {
|
|
34
|
-
import type { RunRecord, SessionSummary, RunEvent } from "./store-contracts.js";
|
|
2
|
+
import type { TaskStatus } from "@sema-agent/core";
|
|
3
|
+
import type { RunRecord, SessionSummary, RunEvent, PersistedTaskResult } from "./store-contracts.js";
|
|
35
4
|
import type { RunStoreCheckpointProbe } from "./memory-run-store.js";
|
|
36
5
|
import type { LedgerEventType } from "../trace/ledger-events.js";
|
|
37
6
|
/**
|
|
@@ -136,7 +105,7 @@ export declare class FileRunStore {
|
|
|
136
105
|
getEvents(taskId: string, afterSeq: number): Promise<RunEvent[]>;
|
|
137
106
|
/** [ref] FIRST terminal writer wins(SQL 孪生同款正向 CAS on running/suspended/needs_review,
|
|
138
107
|
* [1.207 codex M1] 负向形漏 blocked/timeout);claim 释放保持无条件(幂等)。 */
|
|
139
|
-
setTerminal(taskId: string, status: TaskStatus, result:
|
|
108
|
+
setTerminal(taskId: string, status: TaskStatus, result: PersistedTaskResult | null, error: string | null): Promise<void>;
|
|
140
109
|
/** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard). */
|
|
141
110
|
setSuspended(taskId: string): Promise<void>;
|
|
142
111
|
/** [ref] D-B: park NON-terminal `needs_review`, KEEP the claim. CAS on running/needs_review. */
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { redactErrorMessage, redactLedgerEventData, redactTerminalResult } from "../observability/run-terminal-log.js";
|
|
1
2
|
import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
4
5
|
import { randomBytes } from "node:crypto";
|
|
5
6
|
import { AppendLog, atomicWriteFile, ensureDir, readJsonlRecords, sanitizePathComponent, writeThenLink } from "@sema-agent/core";
|
|
6
7
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
8
|
+
import { notifyRunTerminal } from "../observability/run-terminal-log.js";
|
|
7
9
|
const ownerEq = (a, b) => (a ?? null) === (b ?? null);
|
|
8
10
|
const PROCESS_FACTS = {
|
|
9
11
|
pid: process.pid,
|
|
@@ -338,6 +340,7 @@ export class FileRunStore {
|
|
|
338
340
|
}
|
|
339
341
|
}
|
|
340
342
|
async appendEvent(taskId, seq, type, data) {
|
|
343
|
+
data = redactLedgerEventData(type, data);
|
|
341
344
|
const list = this.events.get(taskId) ?? [];
|
|
342
345
|
if (list.some((e) => e.seq === seq))
|
|
343
346
|
return;
|
|
@@ -363,6 +366,8 @@ export class FileRunStore {
|
|
|
363
366
|
.map((e) => ({ ...e, data: e.data == null ? null : structuredClone(e.data) }));
|
|
364
367
|
}
|
|
365
368
|
async setTerminal(taskId, status, result, error) {
|
|
369
|
+
result = redactTerminalResult(result);
|
|
370
|
+
error = error === null ? null : (redactErrorMessage(error) ?? null);
|
|
366
371
|
const r = this.runs.get(taskId);
|
|
367
372
|
if (!r) {
|
|
368
373
|
for (const [sessionId, tid] of this.active)
|
|
@@ -372,6 +377,7 @@ export class FileRunStore {
|
|
|
372
377
|
}
|
|
373
378
|
return;
|
|
374
379
|
}
|
|
380
|
+
let pending;
|
|
375
381
|
if (r.status === "running" || r.status === "suspended" || r.status === "needs_review") {
|
|
376
382
|
r.status = status;
|
|
377
383
|
r.result = result;
|
|
@@ -379,9 +385,22 @@ export class FileRunStore {
|
|
|
379
385
|
r.errorCode = result?.errorCode ?? null;
|
|
380
386
|
r.updatedAt = new Date();
|
|
381
387
|
this.persist(r);
|
|
388
|
+
pending = {
|
|
389
|
+
runId: taskId,
|
|
390
|
+
sessionId: r.sessionId,
|
|
391
|
+
owner: r.owner,
|
|
392
|
+
status,
|
|
393
|
+
resultTaskId: result?.taskId,
|
|
394
|
+
model: result?.model,
|
|
395
|
+
errorCode: r.errorCode ?? undefined,
|
|
396
|
+
errorMessage: result?.errorMessage ?? error ?? undefined,
|
|
397
|
+
elapsedMs: Math.max(0, Date.now() - r.createdAt.getTime()),
|
|
398
|
+
};
|
|
382
399
|
}
|
|
383
400
|
if (this.active.get(r.sessionId) === taskId)
|
|
384
401
|
this.releaseClaim(r.sessionId);
|
|
402
|
+
if (pending)
|
|
403
|
+
notifyRunTerminal(pending);
|
|
385
404
|
}
|
|
386
405
|
async setSuspended(taskId) {
|
|
387
406
|
const r = this.runs.get(taskId);
|
|
@@ -112,6 +112,16 @@ export declare class LocalCheckpointStore {
|
|
|
112
112
|
* 投了就是「SQL 车道有、local 车道没有」的键集分岔。
|
|
113
113
|
*/
|
|
114
114
|
findDecidedApprovalsForBinding(sessionId: string, boundCallId: string): Promise<DecidedApprovalRecord[]>;
|
|
115
|
+
/**
|
|
116
|
+
* [ref]:按 token 的 scope 读 —— 与 SQL 孪生**同一条契约**(`undefined` 无行 / `null` 匿名 / 属主串),
|
|
117
|
+
* 语义与存在理由的唯一属主 = SQL 侧 `peekScopeByToken` 顶注。
|
|
118
|
+
*
|
|
119
|
+
* ⚠️ 形差如实记:SQL 孪生那口是真「轻读」(只 SELECT scope 一列),本形没有列可选 —— core 的 File 店
|
|
120
|
+
* 只给 `get(token)`,所以这里读的是**整行**。后果对本形无害:本形的 `get()` 没有行级版本闸(唯一的
|
|
121
|
+
* 读不动形是账本重放级 = 整店同时读不动,`findPendingTokenBySession` 先抛),所以「行体读得出而 scope
|
|
122
|
+
* 读不出」这一支在本形结构上不存在。哪天本形补了行级闸,这里要跟着改成不碰行体的读。
|
|
123
|
+
*/
|
|
124
|
+
peekScopeByToken(token: CheckpointToken): Promise<string | null | undefined>;
|
|
115
125
|
/** `undefined` = no pending row; else the owner scope (local scopes are always strings — never null).
|
|
116
126
|
* [ref]:与 `findPendingTokenBySession` 同一个 {@link pickPendingRow}(同 binding ⇒ 同一行)。 */
|
|
117
127
|
peekPendingScope(sessionId: string, binding?: PendingRowBinding): Promise<string | null | undefined>;
|