@sema-agent/server 7.138.0 → 7.139.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 +276 -7
- package/README.md +7 -4
- package/README.zh-CN.md +6 -4
- package/USAGE.md +73 -29
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/lib/lanes.json +3 -2
- package/dist/adoption/wire.d.ts +3 -3
- package/dist/approval-card.d.ts +20 -20
- package/dist/bench/s1/arms.d.ts +1 -1
- package/dist/bench/s1/arms.js +1 -3
- package/dist/bench/s1/run-firm.js +2 -2
- package/dist/boot/device-lane.d.ts +8 -3
- package/dist/boot/device-lane.js +7 -2
- package/dist/boot/execution-env.d.ts +4 -2
- package/dist/boot/execution-env.js +16 -24
- package/dist/boot/execution-lane.d.ts +1 -1
- package/dist/boot/governance-seams.d.ts +3 -2
- package/dist/boot/governance-seams.js +9 -11
- package/dist/boot/memory-consolidation.js +2 -2
- package/dist/boot/resolve-spec.d.ts +1 -1
- package/dist/boot/resolve-spec.js +8 -9
- package/dist/boot/retention-lane.d.ts +5 -5
- package/dist/boot/retention-lane.js +0 -6
- package/dist/boot/runner-deps.d.ts +3 -1
- package/dist/boot/runner-deps.js +2 -1
- package/dist/boot/session-faces.d.ts +0 -3
- package/dist/boot/session-faces.js +15 -8
- package/dist/boot/stage-02-web-admin-pull.js +2 -2
- package/dist/boot/stage-05-execution-env.js +2 -2
- package/dist/boot/stage-06-runners.js +0 -1
- package/dist/boot/stage-09-leader.js +1 -2
- package/dist/boot/stage-10-http-server.js +2 -2
- package/dist/boot/stores.js +4 -4
- package/dist/boot/web-admin.js +2 -2
- package/dist/capabilities/mcp-probe.js +1 -1
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +15 -7
- package/dist/capability-facts.js +8 -3
- package/dist/config-catalog.js +7 -7
- package/dist/config-invariants.d.ts +23 -3
- package/dist/config-invariants.js +42 -0
- package/dist/config-provider.d.ts +19 -24
- package/dist/config-provider.js +27 -12
- package/dist/config-types.d.ts +10 -5
- package/dist/config.d.ts +33 -0
- package/dist/config.js +44 -15
- package/dist/delegated-session-owner.d.ts +2 -2
- package/dist/deployment-shape.d.ts +1 -1
- package/dist/device-enrollment.d.ts +41 -2
- package/dist/device-enrollment.js +27 -12
- package/dist/device-store.d.ts +58 -85
- package/dist/device-store.js +19 -49
- package/dist/device-ws-hub.js +2 -1
- package/dist/env-name-allowlist-knobs.d.ts +5 -4
- package/dist/exec-cut.d.ts +60 -0
- package/dist/exec-cut.js +36 -0
- package/dist/execution-lane-caps.d.ts +19 -0
- package/dist/execution-lane-caps.js +10 -0
- package/dist/execution-placement.d.ts +259 -0
- package/dist/execution-placement.js +199 -0
- package/dist/field-conflict.d.ts +3 -2
- package/dist/field-conflict.js +1 -0
- package/dist/hosted-posture.d.ts +2 -2
- package/dist/http/admission.js +8 -19
- package/dist/http/checkpoint-ctx.d.ts +21 -12
- package/dist/http/checkpoint-ctx.js +4 -2
- package/dist/http/claimed-run.d.ts +1 -1
- package/dist/http/durable-run-leg.js +9 -5
- package/dist/http/execution-context.d.ts +218 -0
- package/dist/http/execution-context.js +330 -0
- package/dist/http/resume-legs.js +50 -20
- package/dist/http/route-ctx.d.ts +14 -8
- package/dist/http/routes/background.d.ts +29 -1
- package/dist/http/routes/background.js +29 -2
- package/dist/http/routes/devices.d.ts +13 -19
- package/dist/http/routes/devices.js +21 -26
- package/dist/http/routes/session-sync.js +110 -15
- package/dist/http/routes/sessions.d.ts +6 -0
- package/dist/http/routes/sessions.js +115 -11
- package/dist/http/routes/side-query.d.ts +12 -4
- package/dist/http/routes/side-query.js +4 -5
- package/dist/http/routes/tasks.js +37 -31
- package/dist/http/server.d.ts +35 -19
- package/dist/http/server.js +11 -0
- package/dist/http/session-inherit-env.d.ts +3 -4
- package/dist/http/session-inherit-env.js +1 -1
- package/dist/http/wire-types.d.ts +8 -12
- package/dist/index.d.ts +1 -0
- package/dist/memory-sync-client.d.ts +53 -14
- package/dist/memory-sync-client.js +196 -116
- package/dist/memory-sync.d.ts +1 -1
- package/dist/memory-sync.js +3 -2
- package/dist/observability/cost-taxonomy.d.ts +2 -2
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/run-terminal.js +0 -1
- package/dist/orchestration/workflow-notify-journal.js +4 -4
- package/dist/org-memory-admission.d.ts +4 -3
- package/dist/org-memory-admission.js +6 -6
- package/dist/parked-decide.d.ts +34 -9
- package/dist/parked-decide.js +12 -19
- package/dist/parked-revive-surface.d.ts +12 -15
- package/dist/parked-revive-surface.js +3 -6
- package/dist/peer-seat.d.ts +13 -7
- package/dist/peer-seat.js +37 -3
- package/dist/plugins/approval-ask-store-file.d.ts +7 -0
- package/dist/plugins/approval-ask-store-file.js +11 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
- package/dist/plugins/approval-ask-store-memory.js +12 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
- package/dist/plugins/approval-ask-store-sql.js +30 -12
- package/dist/plugins/caching-session-store.d.ts +6 -2
- package/dist/plugins/caching-session-store.js +9 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
- package/dist/plugins/checkpoint-store-sql.js +5 -7
- package/dist/plugins/device-store-sql.d.ts +25 -51
- package/dist/plugins/device-store-sql.js +49 -103
- package/dist/plugins/execution-placement-sql.d.ts +151 -0
- package/dist/plugins/execution-placement-sql.js +223 -0
- package/dist/plugins/file-history-store-sql.d.ts +7 -6
- package/dist/plugins/file-history-store-sql.js +5 -5
- package/dist/plugins/file-run-store.d.ts +6 -1
- package/dist/plugins/file-run-store.js +4 -0
- package/dist/plugins/fork-routing-session-store.d.ts +6 -0
- package/dist/plugins/fork-routing-session-store.js +16 -0
- package/dist/plugins/local-session-store.d.ts +149 -50
- package/dist/plugins/local-session-store.js +382 -107
- package/dist/plugins/memory-key-guards.d.ts +4 -0
- package/dist/plugins/memory-key-guards.js +1 -0
- package/dist/plugins/memory-run-store.d.ts +1 -1
- package/dist/plugins/one-time-migrations.d.ts +111 -3
- package/dist/plugins/one-time-migrations.js +380 -10
- package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +13 -15
- package/dist/plugins/permission-rule-store-sql.js +13 -16
- package/dist/plugins/pg-pool.d.ts +7 -3
- package/dist/plugins/pg-session-storage.d.ts +31 -20
- package/dist/plugins/pg-session-storage.js +120 -52
- package/dist/plugins/remote-env-adb.d.ts +20 -4
- package/dist/plugins/remote-env-adb.js +70 -62
- package/dist/plugins/remote-env-device.d.ts +30 -12
- package/dist/plugins/remote-env-device.js +18 -17
- package/dist/plugins/remote-env-e2b.d.ts +18 -4
- package/dist/plugins/remote-env-e2b.js +41 -42
- package/dist/plugins/remote-env-file-error.d.ts +2 -0
- package/dist/plugins/remote-env-file-error.js +1 -1
- package/dist/plugins/remote-env-host.d.ts +21 -8
- package/dist/plugins/remote-env-host.js +66 -70
- package/dist/plugins/remote-env-k8s.d.ts +11 -16
- package/dist/plugins/remote-env-k8s.js +61 -54
- package/dist/plugins/remote-env-local-docker.d.ts +29 -6
- package/dist/plugins/remote-env-local-docker.js +46 -59
- package/dist/plugins/remote-env-ssh.d.ts +16 -4
- package/dist/plugins/remote-env-ssh.js +72 -56
- package/dist/plugins/remote-shell.d.ts +100 -3
- package/dist/plugins/remote-shell.js +166 -2
- package/dist/plugins/retention-store-sql.d.ts +9 -6
- package/dist/plugins/retention-store-sql.js +7 -1
- package/dist/plugins/run-store-sql.d.ts +13 -3
- package/dist/plugins/run-store-sql.js +5 -0
- package/dist/plugins/schema-consistency.d.ts +15 -5
- package/dist/plugins/schema-consistency.js +14 -8
- package/dist/plugins/schema-drift-sentence.d.ts +27 -0
- package/dist/plugins/schema-drift-sentence.js +38 -0
- package/dist/plugins/session-fork.d.ts +54 -0
- package/dist/plugins/session-fork.js +38 -1
- package/dist/plugins/session-row-sql.d.ts +22 -0
- package/dist/plugins/session-row-sql.js +81 -0
- package/dist/plugins/session-store.d.ts +1 -1
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-backend.js +19 -7
- package/dist/plugins/store-contracts.d.ts +25 -5
- package/dist/plugins/tidb-pool.d.ts +12 -7
- package/dist/plugins/tidb-pool.js +9 -3
- package/dist/plugins/tidb-session-store.d.ts +57 -26
- package/dist/plugins/tidb-session-store.js +118 -50
- package/dist/plugins/tool-result-store-sql.d.ts +3 -2
- package/dist/plugins/tool-result-store-sql.js +18 -10
- package/dist/project-memory.d.ts +9 -15
- package/dist/project-memory.js +5 -4
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +6 -7
- package/dist/request-value-closure.js +1 -3
- package/dist/run-local.d.ts +6 -0
- package/dist/run-local.js +24 -20
- package/dist/runs.d.ts +2 -1
- package/dist/runs.js +3 -1
- package/dist/security.d.ts +38 -12
- package/dist/security.js +8 -2
- package/dist/server-secret-env.d.ts +9 -5
- package/dist/session-children-retire.d.ts +27 -9
- package/dist/session-children-retire.js +9 -5
- package/dist/session-incarnation.d.ts +19 -9
- package/dist/session-incarnation.js +4 -2
- package/dist/session-row.d.ts +114 -5
- package/dist/session-row.js +55 -5
- package/dist/session-titler.d.ts +2 -2
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-cwd.d.ts +6 -1
- package/dist/task-mcp.d.ts +3 -3
- package/dist/tool-approval.d.ts +6 -0
- package/dist/tool-approval.js +8 -1
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +1 -0
- package/dist/trace/self-minted-wire.d.ts +111 -2
- package/dist/trace/self-minted-wire.js +38 -1
- package/dist/url-refusal.d.ts +1 -1
- package/dist/web-admin-client/apply-effective.d.ts +17 -3
- package/dist/web-admin-client/apply-effective.js +20 -10
- package/dist/web-admin-client/skills-mcp.js +3 -2
- package/dist/web-admin-client/types.d.ts +6 -0
- package/package.json +3 -3
- package/wire-changes/server@7.139.0.json +348 -0
- package/dist/http/device-placement.d.ts +0 -175
- package/dist/http/device-placement.js +0 -145
|
@@ -1,110 +1,202 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, readdir, rename, rmdir, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { carriesCredential, syncMemoryScope, } from "@sema-agent/core";
|
|
5
5
|
import { replicationCovers } from "./memory-scope.js";
|
|
6
6
|
import { parseProjectLayerKey } from "./project-layer-key.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
import { MEMORY_SYNC_PEER_COLUMN_CHARS } from "./plugins/memory-key-guards.js";
|
|
8
|
+
import { ONE_TIME_MIGRATIONS, migrationWindowOpen } from "./plugins/one-time-migrations.js";
|
|
9
|
+
import { pathBaseUrlRefusal } from "./web-admin-client/center-request.js";
|
|
10
|
+
import { buildDisplaySafeUrl } from "./trace/redact.js";
|
|
11
|
+
export function memorySyncPeerOf(url) {
|
|
12
|
+
const refusal = pathBaseUrlRefusal("memory sync: MEMORY_SYNC_URL", url);
|
|
13
|
+
if (refusal !== undefined)
|
|
14
|
+
throw new Error(refusal);
|
|
15
|
+
const u = new URL(url);
|
|
16
|
+
const identity = `${u.origin}${u.pathname.replace(/\/+$/, "")}`;
|
|
17
|
+
if (carriesCredential(identity)) {
|
|
18
|
+
throw new Error("memory sync: MEMORY_SYNC_URL holds a credential (a token-shaped part of its path) — the sync peer identity travels verbatim in every request body and " +
|
|
19
|
+
"cursor, so it is refused, never cleaned: put the token in MEMORY_SYNC_TOKEN and set MEMORY_SYNC_URL to the central base address itself");
|
|
20
|
+
}
|
|
21
|
+
if (identity.length > MEMORY_SYNC_PEER_COLUMN_CHARS) {
|
|
22
|
+
throw new Error(`memory sync: MEMORY_SYNC_URL normalizes to ${identity.length} characters — the peer identity is the central request's peer, which holds at most ${MEMORY_SYNC_PEER_COLUMN_CHARS}: use a shorter base URL`);
|
|
23
|
+
}
|
|
24
|
+
return identity;
|
|
25
|
+
}
|
|
26
|
+
export function buildMemorySyncEnabledFields(peer, scope, cursorPath) {
|
|
27
|
+
return { peer: buildDisplaySafeUrl(peer), scope, cursorPath };
|
|
28
|
+
}
|
|
14
29
|
export function createMemorySyncTransport(opts) {
|
|
15
|
-
const
|
|
30
|
+
const peer = memorySyncPeerOf(opts.url);
|
|
16
31
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
17
32
|
const timeoutMs = opts.timeoutMs ?? 60_000;
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
peer,
|
|
35
|
+
async post(path, body) {
|
|
36
|
+
const res = await fetchImpl(`${peer}${path}`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: { authorization: `Bearer ${opts.token}`, "content-type": "application/json" },
|
|
39
|
+
body: JSON.stringify(body),
|
|
40
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
41
|
+
});
|
|
42
|
+
const text = await res.text();
|
|
43
|
+
if (!res.ok)
|
|
44
|
+
throw new Error(`memory sync POST ${path}: HTTP ${res.status} ${text.slice(0, 200)}`);
|
|
45
|
+
return JSON.parse(text);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const SYNC_CURSOR_DIR = "sync-cursors";
|
|
50
|
+
function scopeDigest(scope) {
|
|
51
|
+
return createHash("sha256").update(scope, "utf8").digest("hex").slice(0, 32);
|
|
30
52
|
}
|
|
31
|
-
export function memorySyncCursorPath(memoryRoot,
|
|
32
|
-
return join(memoryRoot,
|
|
53
|
+
export function memorySyncCursorPath(memoryRoot, scope) {
|
|
54
|
+
return join(memoryRoot, SYNC_CURSOR_DIR, `${scopeDigest(scope)}.json`);
|
|
33
55
|
}
|
|
34
|
-
function
|
|
35
|
-
return
|
|
56
|
+
function unusableCursor(scope, path, why) {
|
|
57
|
+
return new Error(`memory sync: layer ${JSON.stringify(scope)} not synced — the cursor file ${path} cannot be read as a sync cursor (${why}); the layer stays unsynced until the operator picks one of two ways out: ` +
|
|
58
|
+
"(1) restore the file from a backup; (2) delete it to accept re-convergence from an empty baseline — entries changed on both sides are reported as conflicts, " +
|
|
59
|
+
"and entries deleted on one side since the last round come back from the other side");
|
|
36
60
|
}
|
|
37
|
-
|
|
61
|
+
function cursorOf(parsed) {
|
|
62
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
63
|
+
return "not a JSON object";
|
|
64
|
+
const c = parsed;
|
|
65
|
+
const baseRevs = c["baseRevs"];
|
|
66
|
+
if (baseRevs === null || typeof baseRevs !== "object" || Array.isArray(baseRevs) || !Object.values(baseRevs).every((v) => typeof v === "string" && v.length > 0)) {
|
|
67
|
+
return "baseRevs is not a map of entry id to a non-empty rev";
|
|
68
|
+
}
|
|
69
|
+
const updatedAtMs = c["updatedAtMs"];
|
|
70
|
+
if (typeof updatedAtMs !== "number" || !Number.isFinite(updatedAtMs))
|
|
71
|
+
return "updatedAtMs is not a finite number";
|
|
72
|
+
return { scope: c["scope"], peer: c["peer"], store: c["store"], baseRevs: { ...baseRevs }, updatedAtMs };
|
|
73
|
+
}
|
|
74
|
+
async function loadLayerBaseline(path, scope) {
|
|
38
75
|
let raw;
|
|
39
76
|
try {
|
|
40
77
|
raw = await readFile(path, "utf8");
|
|
41
78
|
}
|
|
42
79
|
catch (err) {
|
|
43
|
-
|
|
80
|
+
if (err.code === "ENOENT")
|
|
81
|
+
return { kind: "none" };
|
|
82
|
+
throw new Error(`memory sync: layer ${JSON.stringify(scope)} not synced — cannot read the cursor file ${path} (${String(err)}); the next round reads it again once it is readable`);
|
|
44
83
|
}
|
|
84
|
+
let parsed;
|
|
45
85
|
try {
|
|
46
|
-
|
|
47
|
-
return { kind: "parsed", value };
|
|
86
|
+
parsed = JSON.parse(raw);
|
|
48
87
|
}
|
|
49
|
-
catch
|
|
50
|
-
|
|
88
|
+
catch {
|
|
89
|
+
throw unusableCursor(scope, path, "not JSON");
|
|
51
90
|
}
|
|
91
|
+
const cursor = cursorOf(parsed);
|
|
92
|
+
if (typeof cursor === "string")
|
|
93
|
+
throw unusableCursor(scope, path, cursor);
|
|
94
|
+
return { kind: "cursor", cursor };
|
|
52
95
|
}
|
|
53
|
-
function
|
|
96
|
+
async function saveCursor(path, cursor) {
|
|
97
|
+
await mkdir(dirname(path), { recursive: true });
|
|
98
|
+
const tmp = `${path}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
99
|
+
await writeFile(tmp, JSON.stringify(cursor), "utf8");
|
|
100
|
+
await rename(tmp, path);
|
|
101
|
+
}
|
|
102
|
+
const LEGACY_PEER_SEGMENT = "central";
|
|
103
|
+
const LEGACY_ROOT_CURSOR_FILE = "sync-cursor-central.json";
|
|
104
|
+
function m31Window() {
|
|
105
|
+
const row = ONE_TIME_MIGRATIONS.find((w) => w.id === "M31");
|
|
106
|
+
if (row === undefined)
|
|
107
|
+
throw new Error("memory-sync-client: the one-time migration registry has no M31 row — this migration outlived its registry row; delete it with that row");
|
|
108
|
+
return row;
|
|
109
|
+
}
|
|
110
|
+
function scopeOfLegacyCursor(raw) {
|
|
111
|
+
let parsed;
|
|
112
|
+
try {
|
|
113
|
+
parsed = JSON.parse(raw);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
54
118
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
55
|
-
return
|
|
56
|
-
const
|
|
57
|
-
return
|
|
119
|
+
return undefined;
|
|
120
|
+
const scope = parsed["scope"];
|
|
121
|
+
return typeof scope === "string" && scope.length > 0 ? scope : undefined;
|
|
58
122
|
}
|
|
59
|
-
async function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
123
|
+
export async function relocateSyncCursorsM31(memoryRoot, log, opts) {
|
|
124
|
+
if (!migrationWindowOpen(opts.window ?? m31Window(), opts.today))
|
|
125
|
+
return { window: "closed", moved: 0, left: 0 };
|
|
126
|
+
let moved = 0;
|
|
127
|
+
let left = 0;
|
|
128
|
+
const codeOf = (err) => err?.code;
|
|
129
|
+
const exists = async (path) => {
|
|
130
|
+
try {
|
|
131
|
+
await stat(path);
|
|
132
|
+
return true;
|
|
67
133
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return { kind: "cursor", cursor };
|
|
80
|
-
log.warn("memory_sync_cursor_invalid", { path, scope, peer, note: "shape/identity mismatch — treating as first round" });
|
|
81
|
-
return { kind: "none" };
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (codeOf(err) === "ENOENT")
|
|
136
|
+
return false;
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const move = async (from, to) => {
|
|
141
|
+
if (await exists(to)) {
|
|
142
|
+
left += 1;
|
|
143
|
+
log.warn("memory_sync_cursor_not_relocated", { path: from, to, note: "this layer already has a cursor at its path, so the file written by an earlier version stays where it is and no round reads it; delete it" });
|
|
144
|
+
return;
|
|
82
145
|
}
|
|
146
|
+
await mkdir(dirname(to), { recursive: true });
|
|
147
|
+
try {
|
|
148
|
+
await rename(from, to);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
if (codeOf(err) === "ENOENT")
|
|
152
|
+
return;
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
moved += 1;
|
|
156
|
+
log.info("memory_sync_cursor_relocated", { from, to });
|
|
157
|
+
};
|
|
158
|
+
const legacyDir = join(memoryRoot, SYNC_CURSOR_DIR, LEGACY_PEER_SEGMENT);
|
|
159
|
+
let names = [];
|
|
160
|
+
try {
|
|
161
|
+
names = await readdir(legacyDir);
|
|
83
162
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
163
|
+
catch (err) {
|
|
164
|
+
if (codeOf(err) !== "ENOENT")
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
for (const name of [...names].sort()) {
|
|
168
|
+
if (/^[0-9a-f]{32}\.json$/.test(name))
|
|
169
|
+
await move(join(legacyDir, name), join(memoryRoot, SYNC_CURSOR_DIR, name));
|
|
170
|
+
}
|
|
171
|
+
if (names.length > 0) {
|
|
172
|
+
try {
|
|
173
|
+
await rmdir(legacyDir);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
if (codeOf(err) !== "ENOENT" && codeOf(err) !== "ENOTEMPTY" && codeOf(err) !== "EEXIST")
|
|
177
|
+
throw err;
|
|
99
178
|
}
|
|
100
179
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
180
|
+
const legacyFile = join(memoryRoot, LEGACY_ROOT_CURSOR_FILE);
|
|
181
|
+
let raw;
|
|
182
|
+
try {
|
|
183
|
+
raw = await readFile(legacyFile, "utf8");
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
if (codeOf(err) !== "ENOENT")
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
if (raw !== undefined) {
|
|
190
|
+
const scope = scopeOfLegacyCursor(raw);
|
|
191
|
+
if (scope === undefined) {
|
|
192
|
+
left += 1;
|
|
193
|
+
log.warn("memory_sync_cursor_not_relocated", { path: legacyFile, note: "a cursor file written by an earlier version that is not a JSON object naming its scope, so no layer can take it as its baseline; restore it from a backup and restart, or delete it" });
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
await move(legacyFile, memorySyncCursorPath(memoryRoot, scope));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { window: "open", moved, left };
|
|
108
200
|
}
|
|
109
201
|
export const MAX_SYNC_ROUNDS = 32;
|
|
110
202
|
function enumerateScopes(backend) {
|
|
@@ -112,7 +204,7 @@ function enumerateScopes(backend) {
|
|
|
112
204
|
return typeof face === "function" ? face.call(backend) : { supported: false, reason: "the memory backend has no listScopes face" };
|
|
113
205
|
}
|
|
114
206
|
export function createMemorySyncRunner(opts) {
|
|
115
|
-
const
|
|
207
|
+
const now = opts.now ?? Date.now;
|
|
116
208
|
const coverage = { kind: "layers", layers: [opts.scope] };
|
|
117
209
|
const tracked = new Set();
|
|
118
210
|
let enumerationWarned = false;
|
|
@@ -121,49 +213,34 @@ export function createMemorySyncRunner(opts) {
|
|
|
121
213
|
const local = enumerateScopes(opts.backend);
|
|
122
214
|
if (!local.supported && !enumerationWarned) {
|
|
123
215
|
enumerationWarned = true;
|
|
124
|
-
opts.log.warn("memory_sync_layer_enumeration_unsupported", { scope: opts.scope,
|
|
216
|
+
opts.log.warn("memory_sync_layer_enumeration_unsupported", { scope: opts.scope, reason: local.reason, note: "only the sync layer and tracked project layers are synced" });
|
|
125
217
|
}
|
|
126
218
|
const projectLayers = new Set([...(local.supported ? local.scopes : []), ...tracked].filter(isCoveredProjectLayer));
|
|
127
219
|
return [opts.scope, ...[...projectLayers].sort()];
|
|
128
220
|
};
|
|
129
221
|
const syncLayer = async (scope, reason) => {
|
|
130
|
-
const cursorPath = memorySyncCursorPath(opts.memoryRoot,
|
|
131
|
-
const baseline = await loadLayerBaseline(cursorPath, scope
|
|
132
|
-
if (baseline.kind === "predates-store") {
|
|
133
|
-
opts.log.warn(CURSOR_PREDATES_STORE_NOTICE, { scope, peer, path: baseline.path, note: CURSOR_PREDATES_STORE_OUTS });
|
|
134
|
-
throw new Error(`memory sync: layer ${JSON.stringify(scope)} not synced — its baseline ${baseline.path} predates the store binding (see warn ${CURSOR_PREDATES_STORE_NOTICE})`);
|
|
135
|
-
}
|
|
222
|
+
const cursorPath = memorySyncCursorPath(opts.memoryRoot, scope);
|
|
223
|
+
const baseline = await loadLayerBaseline(cursorPath, scope);
|
|
136
224
|
let cursor = baseline.kind === "cursor" ? baseline.cursor : undefined;
|
|
137
225
|
let result;
|
|
138
226
|
let subRound = 0;
|
|
139
227
|
for (;;) {
|
|
140
228
|
subRound += 1;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
...(opts.maxPullEntries !== undefined ? { maxPullEntries: opts.maxPullEntries } : {}),
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
catch (err) {
|
|
154
|
-
if (cursor === undefined || err?.code !== CURSOR_MISMATCH_CODE)
|
|
155
|
-
throw err;
|
|
156
|
-
opts.log.warn("memory_sync_cursor_rebound", { scope, peer, subRound, note: "the persisted cursor is bound to another store — running this round from no cursor and replacing it" });
|
|
157
|
-
cursor = undefined;
|
|
158
|
-
subRound -= 1;
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
229
|
+
result = await syncMemoryScope({
|
|
230
|
+
backend: opts.backend,
|
|
231
|
+
scope,
|
|
232
|
+
transport: opts.transport,
|
|
233
|
+
...(cursor !== undefined ? { cursor } : {}),
|
|
234
|
+
...(opts.now !== undefined ? { now: opts.now } : {}),
|
|
235
|
+
...(opts.maxPushEntries !== undefined ? { maxPushEntries: opts.maxPushEntries } : {}),
|
|
236
|
+
...(opts.maxPullEntries !== undefined ? { maxPullEntries: opts.maxPullEntries } : {}),
|
|
237
|
+
});
|
|
161
238
|
for (const w of result.warnings ?? [])
|
|
162
|
-
opts.log.warn("memory_sync_warning", { scope,
|
|
239
|
+
opts.log.warn("memory_sync_warning", { scope, subRound, warning: w });
|
|
163
240
|
if (!(result.pushTruncated === true || result.pullTruncated === true))
|
|
164
241
|
break;
|
|
165
242
|
if (subRound >= MAX_SYNC_ROUNDS) {
|
|
166
|
-
opts.log.warn("memory_sync_rounds_exhausted", { scope,
|
|
243
|
+
opts.log.warn("memory_sync_rounds_exhausted", { scope, rounds: subRound, note: "truncation flag still set at the round cap — remaining items ride the next trigger" });
|
|
167
244
|
break;
|
|
168
245
|
}
|
|
169
246
|
if (result.cursorAdvanced && result.cursor !== undefined) {
|
|
@@ -175,12 +252,11 @@ export function createMemorySyncRunner(opts) {
|
|
|
175
252
|
}
|
|
176
253
|
}
|
|
177
254
|
cursor = result.cursor ?? cursor;
|
|
178
|
-
opts.log.info("memory_sync_continuation", { reason, scope,
|
|
255
|
+
opts.log.info("memory_sync_continuation", { reason, scope, subRound, pushTruncated: result.pushTruncated === true, pullTruncated: result.pullTruncated === true });
|
|
179
256
|
}
|
|
180
257
|
for (const c of result.conflicts) {
|
|
181
258
|
opts.log.warn("memory_sync_conflict", {
|
|
182
259
|
scope,
|
|
183
|
-
peer,
|
|
184
260
|
side: c.side,
|
|
185
261
|
id: c.id,
|
|
186
262
|
reason: c.reason,
|
|
@@ -199,7 +275,6 @@ export function createMemorySyncRunner(opts) {
|
|
|
199
275
|
opts.log.info("memory_sync_round", {
|
|
200
276
|
reason,
|
|
201
277
|
scope,
|
|
202
|
-
peer,
|
|
203
278
|
ok: result.ok,
|
|
204
279
|
cursorAdvanced: result.cursorAdvanced,
|
|
205
280
|
pushedEntries: result.pushed.entries.length,
|
|
@@ -210,7 +285,12 @@ export function createMemorySyncRunner(opts) {
|
|
|
210
285
|
});
|
|
211
286
|
return result;
|
|
212
287
|
};
|
|
288
|
+
let relocated = false;
|
|
213
289
|
const round = async (reason) => {
|
|
290
|
+
if (!relocated) {
|
|
291
|
+
await relocateSyncCursorsM31(opts.memoryRoot, opts.log, { today: new Date(now()).toISOString().slice(0, 10) });
|
|
292
|
+
relocated = true;
|
|
293
|
+
}
|
|
214
294
|
const layers = [];
|
|
215
295
|
const failures = [];
|
|
216
296
|
for (const scope of layersOfRound()) {
|
|
@@ -219,7 +299,7 @@ export function createMemorySyncRunner(opts) {
|
|
|
219
299
|
}
|
|
220
300
|
catch (err) {
|
|
221
301
|
failures.push({ scope, err });
|
|
222
|
-
opts.log.warn("memory_sync_layer_failed", { reason, scope,
|
|
302
|
+
opts.log.warn("memory_sync_layer_failed", { reason, scope, cursorPath: memorySyncCursorPath(opts.memoryRoot, scope), err: String(err) });
|
|
223
303
|
}
|
|
224
304
|
}
|
|
225
305
|
if (failures.length > 0) {
|
|
@@ -242,7 +322,7 @@ export function createMemorySyncRunner(opts) {
|
|
|
242
322
|
const syncOnce = (reason) => exclusive(() => round(reason));
|
|
243
323
|
const queueRound = (reason) => {
|
|
244
324
|
void syncOnce(reason).catch((err) => {
|
|
245
|
-
opts.log.warn("memory_sync_failed", { reason, scope: opts.scope,
|
|
325
|
+
opts.log.warn("memory_sync_failed", { reason, scope: opts.scope, err: String(err) });
|
|
246
326
|
});
|
|
247
327
|
};
|
|
248
328
|
const trigger = (reason) => {
|
|
@@ -259,6 +339,6 @@ export function createMemorySyncRunner(opts) {
|
|
|
259
339
|
tracked.add(scope);
|
|
260
340
|
queueRound("track");
|
|
261
341
|
};
|
|
262
|
-
return { syncOnce, trigger, track, exclusive, cursorPathFor: (scope) => memorySyncCursorPath(opts.memoryRoot,
|
|
342
|
+
return { syncOnce, trigger, track, exclusive, cursorPathFor: (scope) => memorySyncCursorPath(opts.memoryRoot, scope), pending: () => pending };
|
|
263
343
|
}
|
|
264
344
|
//# sourceMappingURL=memory-sync-client.js.map
|
package/dist/memory-sync.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type MemoryBackend, type MemoryEntry, type PatchReport } from "@sema-ag
|
|
|
2
2
|
import type { MemorySyncStore } from "./plugins/memory-sync-store-pg.js";
|
|
3
3
|
/** 一轮同步请求(验型后的干净形状——`parseMemorySyncRequest` 是唯一铸造点)。 */
|
|
4
4
|
export interface MemorySyncRequest {
|
|
5
|
-
/** 对端标识(`CentralSyncCursor.peer`,per-(scope,peer) 游标分区键)。 */
|
|
5
|
+
/** 对端标识(`CentralSyncCursor.peer`,per-(scope,peer) 游标分区键)= 客户端 transport 的身份(本仓的客户端腿:规范化的中心基址,`memory-sync-client.ts` `memorySyncPeerOf`)。 */
|
|
6
6
|
peer: string;
|
|
7
7
|
/** 客户端游标的共同基线:id → rev at last completed sync round(空 map = 首轮)。 */
|
|
8
8
|
baseRevs: Record<string, string>;
|
package/dist/memory-sync.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { computeEntryRev, MEMORY_ORIGIN_CAUSES, nextSyncBaseline, reconcileMemoryEntries, screenInboundEntries, } from "@sema-agent/core";
|
|
2
2
|
import { exportMemoryScope } from "./memory-export.js";
|
|
3
|
+
import { MEMORY_SYNC_PEER_COLUMN_CHARS } from "./plugins/memory-key-guards.js";
|
|
3
4
|
export const MEMORY_SYNC_MAX_ENTRIES = 500;
|
|
4
5
|
export const MEMORY_SYNC_MAX_DELETES = 2000;
|
|
5
6
|
export function parseMemorySyncRequest(body, scope) {
|
|
@@ -7,8 +8,8 @@ export function parseMemorySyncRequest(body, scope) {
|
|
|
7
8
|
return { ok: false, error: "body must be a JSON object { peer, baseRevs, entries, deletes }" };
|
|
8
9
|
const b = body;
|
|
9
10
|
const peer = b["peer"];
|
|
10
|
-
if (typeof peer !== "string" || peer.length === 0 || peer.length >
|
|
11
|
-
return { ok: false, error:
|
|
11
|
+
if (typeof peer !== "string" || peer.length === 0 || peer.length > MEMORY_SYNC_PEER_COLUMN_CHARS)
|
|
12
|
+
return { ok: false, error: `peer must be a non-empty string of at most ${MEMORY_SYNC_PEER_COLUMN_CHARS} characters` };
|
|
12
13
|
const rawBase = b["baseRevs"];
|
|
13
14
|
if (rawBase === null || typeof rawBase !== "object" || Array.isArray(rawBase))
|
|
14
15
|
return { ok: false, error: "baseRevs must be an object mapping entry id → rev (may be empty on the first round)" };
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* batch; stacking a second wide-fanout move on it in one session was the higher-risk option for no added
|
|
11
11
|
* clarity.)
|
|
12
12
|
*
|
|
13
|
-
* The engine prices ONLY LLM tokens (core's `Stats.costBreakdown` = llmRoot / nestedSubagent /
|
|
14
|
-
*
|
|
13
|
+
* The engine prices ONLY LLM tokens (core's `Stats.costBreakdown` = llmRoot / nestedSubagent / compaction, plus the
|
|
14
|
+
* suggestions and side-call lines). It has NO $ for tool calls, sandbox wall-clock, or network egress — those are INFRA/service costs
|
|
15
15
|
* the service owns the data + rates for. This module is the thin report layer: it turns a
|
|
16
16
|
* per-run infra USAGE + the deployment's RATES into an infra breakdown, then composes it with the core LLM
|
|
17
17
|
* breakdown into one supervisor-facing total. Pure + rate-driven (rates default 0 ⇒ infra reads $0, exactly like
|
|
@@ -87,6 +87,10 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
87
87
|
readonly cls: "P-DEBT";
|
|
88
88
|
readonly note: "件6(#43 案 [4281]):`FileRunStore.hydrate()` 的 `readdirSync(runs/)` 失败 ⇒ 引擎以**空账**起动。旧形是空 catch + return:data root 不可读(权限/挂载丢失/路径被换走)时零告警,此后每一次 getRun 恒 miss —— 而「恒 miss」正是 #43 现场看到的形,一次环境故障被伪装成「本来就没有这条 run」。方向问题在于吞掉之后被读成的是「证不出有行 ⇒ 没有行」,两句话不等价。**缺省不拒启是刻意保留的行为**,所以这条留作债:响亮化(error 级启动日志带 runs 绝对路径+异常文案)+ 本计数,让它在遥测里显形而不是只活在一行日志里。⚠️ 2026-08-29(S-46 件B)起终局格已兑现:部署可用 `RUN_STORE_STRICT_HYDRATE=on` 把这一格换成 **fail-closed 启动门**(拒启,遗言同样带绝对路径+异常文案);本计数因此只在**缺省 off** 档上产生 —— 读遥测时别把「计数为零」直接读成「没有部署踩到这条路」,也可能是它们开了拒启门、根本起不来。";
|
|
89
89
|
};
|
|
90
|
+
readonly "server.run-store.file-active-claims-unreadable": {
|
|
91
|
+
readonly cls: "P-DEBT";
|
|
92
|
+
readonly note: "`FileRunStore.hydrate()` 的第二处 `readdirSync(runs/active/)`(claim 平面 = 单活索引)失败,且 `RUN_STORE_STRICT_HYDRATE` 关(缺省)⇒ 服务照常起动、单活索引为**空**。放行的后果两条,都不报错:① 已有 run 行仍可读(`GET /v1/runs/:id` 照常答),但此后对这些会话的审批决定在请求里同步跑完、却找不到要更新的 run 行 —— run 行停在决前的 `suspended`;② 读单活索引的各处读到「没人占用」—— `getActiveTaskId` 恒答空,例如会话同步推送前的「会话有在飞 run」检查(与 `server.run-store.file-hydrate-unreadable` 同一句错话:「证不出有占用 ⇒ 没有占用」,只是换了一个盘面);新提交不读这张索引,claim 文件完好时仍按盘上的 claim 裁定,不受影响。同一条债的另一个盘面:缺省不拒启是刻意保留的行为,所以留作 P-DEBT —— error 级启动日志(`file_run_store_active_claims_unreadable`,带 `runs/active/` 绝对路径)早已在,本计数补的是遥测轴(此前 `runs/` 臂有计数、这一臂没有)。`RUN_STORE_STRICT_HYDRATE=on` 把这一格换成 fail-closed 启动门,本计数因此只在缺省 off 档的真放行时产生(拒启那一形不记)—— 读遥测时「计数为零」也可能是部署开了拒启门、根本起不来。";
|
|
93
|
+
};
|
|
90
94
|
readonly "server.fork-routing.placed-probe-host-error": {
|
|
91
95
|
readonly cls: "F";
|
|
92
96
|
readonly note: "#292 placement 半场:hostPlacedProbe 对 host 店的 requireExisting 探针失败且**不是 not_found**(host 读故障)时折叠为 absent —— 路由回旧规则(claim-create 会在 transient 建新会话)。放行的是「host 暂不可达时 transient 腿保持独立可用」;最坏后果=故障窗内对一个真 placed id 铸出 transient 影子(host 恢复后双身,requireExisting 冷探针会重新指回 host 真身)。not_found 是常态路径不计数;只有真故障臂进本计数。";
|
|
@@ -549,6 +553,14 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
549
553
|
readonly cls: "P-DEBT";
|
|
550
554
|
readonly note: "S-163(`config-dir-trust.ts` `judgeWin32`,本地配置源 `config.d` 读之前的目录信任判官在 win32 上的那一半):Node 在 win32 上 `uid` 恒 0、模式位是只读属性的投影 ⇒ 「属主 ∈ {本服务用户, root, CONFIG_LOCAL_OWNER_UID}」与「不得组 / 他人可写」两判不可做;结构判(常规文件 / 16 MiB 上界 / 链接成环)照做。每一次读照常进行(起服一次 + 每拍一次)记一次。放行的最坏后果 = 一台多用户 Windows 机器上,别的本机用户若能写这台服务的 `config.d`(缺省的用户目录 ACL 不允许),改一份 `mcp.json` 就能让本服务以它的身份起命令 —— 与 7.132.x 逐字同。照 POSIX 拒启会让每个 Windows 本机引擎(含 cli 起的)升级即起不来,而让 cli 替用户设豁免键是中间层替部署者设安全旋钮。收口 = 有了 ACL 判据或一次裁定(改 R 或改拒启)那一版,本 tag 删或改类。detail = `config.d=<物理路径>`。";
|
|
551
555
|
};
|
|
556
|
+
readonly "server.remote-write.staging-dir-left": {
|
|
557
|
+
readonly cls: "F";
|
|
558
|
+
readonly note: "远端五条车道的写(`plugins/remote-shell.ts` 的暂存编排):准备 / 提交脚本删一只私有暂存目录(`.<名>.stage-<十六进制>/`,生来 0700;只删这次写放进去的 `data` 与这只空目录,从不递归删)没删掉、在 stdout 写一条记录点名 —— 五处:`mv` 已把暂存件换到目标上之后删当前那只;跟一跳到别的目录重新暂存成功之后删原来那只;重新暂存却没用上(收紧不成 / 拷不成)的那一只;就地拷入 / 追加时删暂存名失败、或写完关掉 fd 后删私有目录失败(有的文件系统为仍打开着的文件留隐藏项:NFS / libfuse / SMB);准备时收不成私有 / 里面已有 `data` 的那一只。目录里多出了不是这次写放进去的东西(同一身份或 root 的别的进程往里写过 / 收紧之前别人放进来的)、目录被换成了符号链接、或文件系统拒删。写照它自己的结局答(提交已成 ⇒ 成功:答失败会让调用方以为目标没变),残留只在这里留痕。放行的最坏后果 = 目标(或链接所指)目录里多一只隐藏目录(空的,或装着别人放进去的东西;重新暂存没用上的那只可能装着这次写的半份新内容),只有写入方进得去;不是门、不是凭据、不碰目标内容。计数非零 = 那台目标机上有东西在往别人的暂存目录里写,或删不动,去看那只目录。detail = 残留目录的路径(一次写最多两只,逐只计)。";
|
|
559
|
+
};
|
|
560
|
+
readonly "server.remote-write.stage-seal-fallback": {
|
|
561
|
+
readonly cls: "F";
|
|
562
|
+
readonly note: "远端五条车道的写(`plugins/remote-shell.ts` 的暂存编排):准备脚本把刚建的私有暂存目录收成 0700 并读回核实(`chmod 700` 退出 0 ∧ `stat` 读回的模式低三位恰是 700 ∧ 属主 = 本身份),任一步不成 —— `chmod` 被文件系统 / 内核拒;退出 0 却没生效(不认权限位的文件系统);父目录带缺省 ACL 而掩码收不下来;目标机上没有 `stat` —— 准备退出 4、这次写**不暂存**,两只文件面、五条腿一律改为直接写目标(与「目标目录不收新名字」同一条回落;能追加写的腿直接追加,e2b / adb 的追加 = 读旧 + 拼 + 整份写目标 —— adb 的这一形就是 7.138.x 的追加,push 没有追加写)。修前这一步是 `chmod 700 … || :`:加固失败照样放行上传,字节落在一只别的身份可能读得到的目录里(P 轴 fail-open),那一臂已删,字节从不进一只没核成私有的目录。放行的最坏后果 = 这一次写失去「整旧或整新」(非原子:中途断 / 取消 ⇒ 目标可能只写了一半,整份写目标的 e2b / adb 上可能没了或只剩半份;写目标那一步因取消中断答 `unknown`,那一步开始之前取消答 `aborted`、目标未碰,那一步不收取消(ssh 的 sftp)照样写完);ssh / local-docker / k8s 写进原 inode、权限位不变,e2b 的 `files.write` 与 adb 的 push 整份写目标、inode / 权限位由对端定(adbd 不保,与 7.138.x 同形);不是门、不是凭据。Android 共享存储(`/sdcard` 一类:权限位由挂载派生、不认 chmod)上 adb 的写按 sdcardfs / MediaProvider FUSE 的权限语义**推断**每一次都走这里(未上真机,真机读数候 test;结构性,运维修不掉;工作区放在 `/data/local/tmp` 一类认权限位的目录就不走)。计数非零 = 这台目标机的这个目录上私有目录收不成私有(文件系统不认权限位 / 缺省 ACL 掩码收不下来 / 缺 `stat`),写在走非原子的退路,去看那个目录。detail = `<目标路径>: <准备脚本写的原因句>`。";
|
|
563
|
+
};
|
|
552
564
|
};
|
|
553
565
|
/** 词表键推导的闭集类型——未登记的 tag 传不进 {@link recordFailOpen}(编译期拒)。 */
|
|
554
566
|
export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
|
|
@@ -68,6 +68,10 @@ export const FAIL_OPEN_TAGS = {
|
|
|
68
68
|
cls: "P-DEBT",
|
|
69
69
|
note: "件6(#43 案 [4281]):`FileRunStore.hydrate()` 的 `readdirSync(runs/)` 失败 ⇒ 引擎以**空账**起动。旧形是空 catch + return:data root 不可读(权限/挂载丢失/路径被换走)时零告警,此后每一次 getRun 恒 miss —— 而「恒 miss」正是 #43 现场看到的形,一次环境故障被伪装成「本来就没有这条 run」。方向问题在于吞掉之后被读成的是「证不出有行 ⇒ 没有行」,两句话不等价。**缺省不拒启是刻意保留的行为**,所以这条留作债:响亮化(error 级启动日志带 runs 绝对路径+异常文案)+ 本计数,让它在遥测里显形而不是只活在一行日志里。⚠️ 2026-08-29(S-46 件B)起终局格已兑现:部署可用 `RUN_STORE_STRICT_HYDRATE=on` 把这一格换成 **fail-closed 启动门**(拒启,遗言同样带绝对路径+异常文案);本计数因此只在**缺省 off** 档上产生 —— 读遥测时别把「计数为零」直接读成「没有部署踩到这条路」,也可能是它们开了拒启门、根本起不来。",
|
|
70
70
|
},
|
|
71
|
+
"server.run-store.file-active-claims-unreadable": {
|
|
72
|
+
cls: "P-DEBT",
|
|
73
|
+
note: "`FileRunStore.hydrate()` 的第二处 `readdirSync(runs/active/)`(claim 平面 = 单活索引)失败,且 `RUN_STORE_STRICT_HYDRATE` 关(缺省)⇒ 服务照常起动、单活索引为**空**。放行的后果两条,都不报错:① 已有 run 行仍可读(`GET /v1/runs/:id` 照常答),但此后对这些会话的审批决定在请求里同步跑完、却找不到要更新的 run 行 —— run 行停在决前的 `suspended`;② 读单活索引的各处读到「没人占用」—— `getActiveTaskId` 恒答空,例如会话同步推送前的「会话有在飞 run」检查(与 `server.run-store.file-hydrate-unreadable` 同一句错话:「证不出有占用 ⇒ 没有占用」,只是换了一个盘面);新提交不读这张索引,claim 文件完好时仍按盘上的 claim 裁定,不受影响。同一条债的另一个盘面:缺省不拒启是刻意保留的行为,所以留作 P-DEBT —— error 级启动日志(`file_run_store_active_claims_unreadable`,带 `runs/active/` 绝对路径)早已在,本计数补的是遥测轴(此前 `runs/` 臂有计数、这一臂没有)。`RUN_STORE_STRICT_HYDRATE=on` 把这一格换成 fail-closed 启动门,本计数因此只在缺省 off 档的真放行时产生(拒启那一形不记)—— 读遥测时「计数为零」也可能是部署开了拒启门、根本起不来。",
|
|
74
|
+
},
|
|
71
75
|
"server.fork-routing.placed-probe-host-error": {
|
|
72
76
|
cls: "F",
|
|
73
77
|
note: "#292 placement 半场:hostPlacedProbe 对 host 店的 requireExisting 探针失败且**不是 not_found**(host 读故障)时折叠为 absent —— 路由回旧规则(claim-create 会在 transient 建新会话)。放行的是「host 暂不可达时 transient 腿保持独立可用」;最坏后果=故障窗内对一个真 placed id 铸出 transient 影子(host 恢复后双身,requireExisting 冷探针会重新指回 host 真身)。not_found 是常态路径不计数;只有真故障臂进本计数。",
|
|
@@ -509,6 +513,14 @@ export const FAIL_OPEN_TAGS = {
|
|
|
509
513
|
cls: "P-DEBT",
|
|
510
514
|
note: "S-163(`config-dir-trust.ts` `judgeWin32`,本地配置源 `config.d` 读之前的目录信任判官在 win32 上的那一半):Node 在 win32 上 `uid` 恒 0、模式位是只读属性的投影 ⇒ 「属主 ∈ {本服务用户, root, CONFIG_LOCAL_OWNER_UID}」与「不得组 / 他人可写」两判不可做;结构判(常规文件 / 16 MiB 上界 / 链接成环)照做。每一次读照常进行(起服一次 + 每拍一次)记一次。放行的最坏后果 = 一台多用户 Windows 机器上,别的本机用户若能写这台服务的 `config.d`(缺省的用户目录 ACL 不允许),改一份 `mcp.json` 就能让本服务以它的身份起命令 —— 与 7.132.x 逐字同。照 POSIX 拒启会让每个 Windows 本机引擎(含 cli 起的)升级即起不来,而让 cli 替用户设豁免键是中间层替部署者设安全旋钮。收口 = 有了 ACL 判据或一次裁定(改 R 或改拒启)那一版,本 tag 删或改类。detail = `config.d=<物理路径>`。",
|
|
511
515
|
},
|
|
516
|
+
"server.remote-write.staging-dir-left": {
|
|
517
|
+
cls: "F",
|
|
518
|
+
note: "远端五条车道的写(`plugins/remote-shell.ts` 的暂存编排):准备 / 提交脚本删一只私有暂存目录(`.<名>.stage-<十六进制>/`,生来 0700;只删这次写放进去的 `data` 与这只空目录,从不递归删)没删掉、在 stdout 写一条记录点名 —— 五处:`mv` 已把暂存件换到目标上之后删当前那只;跟一跳到别的目录重新暂存成功之后删原来那只;重新暂存却没用上(收紧不成 / 拷不成)的那一只;就地拷入 / 追加时删暂存名失败、或写完关掉 fd 后删私有目录失败(有的文件系统为仍打开着的文件留隐藏项:NFS / libfuse / SMB);准备时收不成私有 / 里面已有 `data` 的那一只。目录里多出了不是这次写放进去的东西(同一身份或 root 的别的进程往里写过 / 收紧之前别人放进来的)、目录被换成了符号链接、或文件系统拒删。写照它自己的结局答(提交已成 ⇒ 成功:答失败会让调用方以为目标没变),残留只在这里留痕。放行的最坏后果 = 目标(或链接所指)目录里多一只隐藏目录(空的,或装着别人放进去的东西;重新暂存没用上的那只可能装着这次写的半份新内容),只有写入方进得去;不是门、不是凭据、不碰目标内容。计数非零 = 那台目标机上有东西在往别人的暂存目录里写,或删不动,去看那只目录。detail = 残留目录的路径(一次写最多两只,逐只计)。",
|
|
519
|
+
},
|
|
520
|
+
"server.remote-write.stage-seal-fallback": {
|
|
521
|
+
cls: "F",
|
|
522
|
+
note: "远端五条车道的写(`plugins/remote-shell.ts` 的暂存编排):准备脚本把刚建的私有暂存目录收成 0700 并读回核实(`chmod 700` 退出 0 ∧ `stat` 读回的模式低三位恰是 700 ∧ 属主 = 本身份),任一步不成 —— `chmod` 被文件系统 / 内核拒;退出 0 却没生效(不认权限位的文件系统);父目录带缺省 ACL 而掩码收不下来;目标机上没有 `stat` —— 准备退出 4、这次写**不暂存**,两只文件面、五条腿一律改为直接写目标(与「目标目录不收新名字」同一条回落;能追加写的腿直接追加,e2b / adb 的追加 = 读旧 + 拼 + 整份写目标 —— adb 的这一形就是 7.138.x 的追加,push 没有追加写)。修前这一步是 `chmod 700 … || :`:加固失败照样放行上传,字节落在一只别的身份可能读得到的目录里(P 轴 fail-open),那一臂已删,字节从不进一只没核成私有的目录。放行的最坏后果 = 这一次写失去「整旧或整新」(非原子:中途断 / 取消 ⇒ 目标可能只写了一半,整份写目标的 e2b / adb 上可能没了或只剩半份;写目标那一步因取消中断答 `unknown`,那一步开始之前取消答 `aborted`、目标未碰,那一步不收取消(ssh 的 sftp)照样写完);ssh / local-docker / k8s 写进原 inode、权限位不变,e2b 的 `files.write` 与 adb 的 push 整份写目标、inode / 权限位由对端定(adbd 不保,与 7.138.x 同形);不是门、不是凭据。Android 共享存储(`/sdcard` 一类:权限位由挂载派生、不认 chmod)上 adb 的写按 sdcardfs / MediaProvider FUSE 的权限语义**推断**每一次都走这里(未上真机,真机读数候 test;结构性,运维修不掉;工作区放在 `/data/local/tmp` 一类认权限位的目录就不走)。计数非零 = 这台目标机的这个目录上私有目录收不成私有(文件系统不认权限位 / 缺省 ACL 掩码收不下来 / 缺 `stat`),写在走非原子的退路,去看那个目录。detail = `<目标路径>: <准备脚本写的原因句>`。",
|
|
523
|
+
},
|
|
512
524
|
};
|
|
513
525
|
export function failOpenTagForDroppedFrame(frameType) {
|
|
514
526
|
switch (frameType) {
|
|
@@ -593,7 +593,6 @@ const STATS_KEY_TREATMENT = {
|
|
|
593
593
|
humanReview: "human-review",
|
|
594
594
|
};
|
|
595
595
|
const STATS_MECHANISMS_TREATMENT = {
|
|
596
|
-
finalVerifyInjected: "verbatim",
|
|
597
596
|
finalVerifyInjections: "verbatim",
|
|
598
597
|
attachmentsInjected: "verbatim",
|
|
599
598
|
approachNoticesSent: "verbatim",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { AppendLog, ensureDir } from "@sema-agent/core";
|
|
2
|
+
import { AppendLog, REDACTION_MARKS, ensureDir } from "@sema-agent/core";
|
|
3
3
|
import { buildFleetWorkflowRow } from "../fleet/fleet-bus.js";
|
|
4
4
|
import { isTerminalWorkflowRowStatus, seedTerminalWorkflowRow } from "../fleet/fleet-terminal-window.js";
|
|
5
5
|
import { buildParkSessionRows } from "./workflow-agent-session-index.js";
|
|
@@ -26,11 +26,11 @@ export function recoverySummary(status, error) {
|
|
|
26
26
|
return "workflow completed";
|
|
27
27
|
const base = error ? `workflow failed: ${error}` : "workflow failed";
|
|
28
28
|
const redacted = base
|
|
29
|
-
.replace(/https?:\/\/[^\s"'`]+/gi,
|
|
29
|
+
.replace(/https?:\/\/[^\s"'`]+/gi, REDACTION_MARKS.url)
|
|
30
30
|
.replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, "[redacted-token]")
|
|
31
31
|
.replace(/\b(?:api[_-]?key|secret|token|password|authorization)\b\s*[:=]\s*"?[^\s"',}]+/gi, "[redacted-secret]")
|
|
32
|
-
.replace(/(?:\/[A-Za-z0-9_.-]+){2,}/g,
|
|
33
|
-
.replace(/(?:[A-Za-z]:)?(?:\\{1,2}[A-Za-z0-9_.$-]+){2,}/g,
|
|
32
|
+
.replace(/(?:\/[A-Za-z0-9_.-]+){2,}/g, REDACTION_MARKS.path)
|
|
33
|
+
.replace(/(?:[A-Za-z]:)?(?:\\{1,2}[A-Za-z0-9_.$-]+){2,}/g, REDACTION_MARKS.path);
|
|
34
34
|
return redacted.length > SUMMARY_MAX ? `${redacted.slice(0, SUMMARY_MAX)}…[+${redacted.length - SUMMARY_MAX} chars]` : redacted;
|
|
35
35
|
}
|
|
36
36
|
export class WorkflowNotifyGate {
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* core 侧契约(RunnerDeps.memoryScopeAdmission,core types.d.ts):resolver 返回 {ok:false} ⇒ core 铸终局码
|
|
13
13
|
* memory.admission_denied;resolver throw 一个带 `retryAfterMs`(ms)属性的 Error ⇒ core 铸瞬时码
|
|
14
14
|
* memory.admission_required 并透传 retryAfterMs;{ok:true}.scopes 只能 ⊆ requested(多给 = resolver fault
|
|
15
|
-
* 会被 core 整拒)
|
|
16
|
-
*
|
|
15
|
+
* 会被 core 整拒)。「谁在问」只有引擎一处读法:入参按调用方在不在场分两形,无调用方那一形只装部署来源的 scope
|
|
16
|
+
* (请求来源的 scope 没有调用方 ⇒ 引擎在自己的门上先拒,不问本解析器)—— 本模块按这一判别式分支,不另判一次缺席;
|
|
17
|
+
* 引擎型不可达的那一组合(无调用方 ∧ 请求来源 scope)若仍到了这里 ⇒ 抛,不放行。
|
|
17
18
|
*/
|
|
18
19
|
import type { MemoryScopeAdmission } from "@sema-agent/core";
|
|
19
20
|
/**
|
|
@@ -72,7 +73,7 @@ export interface MemoryScopeAdmissionOptions {
|
|
|
72
73
|
/** "audit": 判决照算但从不真拒——会拒的结果(终局 ok:false 或瞬时 throw)一律换成全额放行 + 观测记录
|
|
73
74
|
* would-deny;真通过的判决(含 write 收窄为 null)照常返回。"enforce": 判决即结果。 */
|
|
74
75
|
mode: "audit" | "enforce";
|
|
75
|
-
/** 观测 hook。outcome ∈ "ok" | "denied" | "
|
|
76
|
+
/** 观测 hook。outcome ∈ "ok" | "denied" | "directory_absent" | "directory_stale" |
|
|
76
77
|
* "directory_malformed"。audit 模式下的 would-deny 也走这里,`details.audited === true`。hook 本身绝不
|
|
77
78
|
* 允许打断准入判决——一律吞掉它可能抛出的异常。 */
|
|
78
79
|
onOutcome?: (outcome: string, details?: Record<string, unknown>) => void;
|
|
@@ -164,8 +164,9 @@ export function createMemoryScopeAdmission(directory, opts) {
|
|
|
164
164
|
catch {
|
|
165
165
|
}
|
|
166
166
|
};
|
|
167
|
-
return async (
|
|
168
|
-
const
|
|
167
|
+
return async (input) => {
|
|
168
|
+
const { requested, requestedWriteScope } = input;
|
|
169
|
+
const attributed = (details) => input.principal === undefined ? details : { principal: input.principal, ...details };
|
|
169
170
|
const requestOriginRequested = requested.filter((r) => r.origin === "request");
|
|
170
171
|
const writeIsRequestOrigin = requestedWriteScope?.origin === "request";
|
|
171
172
|
const hasRequestOrigin = requestOriginRequested.length > 0 || writeIsRequestOrigin;
|
|
@@ -175,10 +176,9 @@ export function createMemoryScopeAdmission(directory, opts) {
|
|
|
175
176
|
async function decide() {
|
|
176
177
|
if (!hasRequestOrigin)
|
|
177
178
|
return { kind: "admit", verdict: admitFull() };
|
|
178
|
-
if (principal === undefined)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const lookup = await directory.lookup(principal);
|
|
179
|
+
if (input.principal === undefined)
|
|
180
|
+
throw new Error("org memory admission was asked with no caller yet holds a request-origin scope — the engine's caller-less arm carries deployment-origin scopes only, so this input is an engine contract break and is refused, never admitted");
|
|
181
|
+
const lookup = await directory.lookup(input.principal);
|
|
182
182
|
if (lookup.kind === "unavailable") {
|
|
183
183
|
const outcome = lookup.reason === "stale_generation" ? "directory_stale" : lookup.reason === "malformed" ? "directory_malformed" : "directory_absent";
|
|
184
184
|
return { kind: "unavailable", retryAfterMs: lookup.retryAfterMs, outcome, lookupReason: lookup.reason, details: { reason: lookup.reason } };
|