@voybio/ace-swarm 2.4.4 → 2.4.6
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/CHANGELOG.md +69 -0
- package/README.md +72 -532
- package/assets/eval-battery/battery.json +85 -0
- package/assets/eval-battery/fixtures/edit-target.js +3 -0
- package/assets/scripts/render-mcp-configs.sh +12 -0
- package/dist/ace-context.d.ts +18 -0
- package/dist/ace-context.js +161 -16
- package/dist/ace-server-instructions.js +36 -9
- package/dist/bridge-trace.d.ts +17 -0
- package/dist/bridge-trace.js +23 -0
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +619 -10
- package/dist/discovery-runtime-wrappers.d.ts +16 -1
- package/dist/discovery-runtime-wrappers.js +65 -9
- package/dist/eval-harness.d.ts +110 -0
- package/dist/eval-harness.js +254 -0
- package/dist/experience-reflector.d.ts +13 -0
- package/dist/experience-reflector.js +62 -0
- package/dist/goal-engine.d.ts +155 -0
- package/dist/goal-engine.js +795 -0
- package/dist/helpers/store-resolution.js +40 -39
- package/dist/hermes/session-manager.d.ts +10 -0
- package/dist/hermes/session-manager.js +130 -27
- package/dist/job-scheduler.d.ts +58 -0
- package/dist/job-scheduler.js +202 -2
- package/dist/job-worker.d.ts +87 -0
- package/dist/job-worker.js +182 -0
- package/dist/local-model-policy.d.ts +8 -2
- package/dist/local-model-policy.js +31 -17
- package/dist/local-model-runtime.d.ts +6 -0
- package/dist/local-model-runtime.js +130 -2
- package/dist/measured-classing.d.ts +66 -0
- package/dist/measured-classing.js +165 -0
- package/dist/model-bridge.d.ts +45 -0
- package/dist/model-bridge.js +502 -44
- package/dist/orchestrator-supervisor.js +1 -1
- package/dist/plan-proposal.d.ts +7 -0
- package/dist/plan-proposal.js +74 -4
- package/dist/playbook-adaptation.d.ts +37 -0
- package/dist/playbook-adaptation.js +159 -0
- package/dist/playbook-curator.d.ts +16 -0
- package/dist/playbook-curator.js +106 -0
- package/dist/playbook-llm-reflector.d.ts +26 -0
- package/dist/playbook-llm-reflector.js +157 -0
- package/dist/public-surface.d.ts +2 -0
- package/dist/public-surface.js +26 -14
- package/dist/safe-edit.d.ts +20 -0
- package/dist/safe-edit.js +355 -78
- package/dist/server.d.ts +11 -2
- package/dist/server.js +55 -5
- package/dist/shared.d.ts +12 -0
- package/dist/shared.js +13 -1
- package/dist/source-mutation-coordinator.d.ts +54 -0
- package/dist/source-mutation-coordinator.js +259 -0
- package/dist/status-events.d.ts +4 -1
- package/dist/status-events.js +41 -6
- package/dist/store/ace-packed-store.d.ts +28 -9
- package/dist/store/ace-packed-store.js +324 -57
- package/dist/store/bgzf.d.ts +4 -0
- package/dist/store/bgzf.js +94 -0
- package/dist/store/bootstrap-store.d.ts +4 -1
- package/dist/store/bootstrap-store.js +57 -3
- package/dist/store/cache-workspace.d.ts +8 -0
- package/dist/store/cache-workspace.js +67 -1
- package/dist/store/crc32.d.ts +2 -0
- package/dist/store/crc32.js +16 -0
- package/dist/store/fsck.d.ts +16 -0
- package/dist/store/fsck.js +240 -0
- package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
- package/dist/store/materializers/context-snapshot-materializer.js +4 -1
- package/dist/store/materializers/projection-manager.d.ts +7 -0
- package/dist/store/materializers/projection-manager.js +37 -0
- package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
- package/dist/store/repositories/goal-repository.d.ts +112 -0
- package/dist/store/repositories/goal-repository.js +157 -0
- package/dist/store/repositories/playbook-repository.d.ts +107 -0
- package/dist/store/repositories/playbook-repository.js +314 -0
- package/dist/store/store-artifacts.js +44 -2
- package/dist/store/store-authority-audit.js +1 -0
- package/dist/store/store-cli.d.ts +30 -0
- package/dist/store/store-cli.js +242 -0
- package/dist/store/store-snapshot.d.ts +12 -0
- package/dist/store/store-snapshot.js +74 -11
- package/dist/store/types.d.ts +1 -0
- package/dist/store/write-coordinator.d.ts +7 -0
- package/dist/store/write-coordinator.js +11 -0
- package/dist/todo-state.d.ts +21 -0
- package/dist/todo-state.js +150 -53
- package/dist/tools-agent.js +86 -22
- package/dist/tools-discovery.js +5 -3
- package/dist/tools-files.js +39 -18
- package/dist/tools-framework.js +31 -3
- package/dist/tools-goal.d.ts +11 -0
- package/dist/tools-goal.js +108 -0
- package/dist/tools-handoff.js +7 -11
- package/dist/tools-lifecycle.js +2 -0
- package/dist/tools-scheduler.js +44 -1
- package/dist/tools-skills.js +3 -2
- package/dist/tools-todo.js +64 -3
- package/dist/tools.js +2 -0
- package/dist/tui/chat.js +4 -0
- package/dist/tui/index.js +4 -1
- package/dist/tui/ollama.d.ts +2 -0
- package/dist/tui/openai-compatible.d.ts +14 -0
- package/dist/tui/openai-compatible.js +15 -2
- package/dist/tui/provider-discovery.d.ts +1 -0
- package/dist/tui/provider-discovery.js +2 -2
- package/docs/cli-reference.md +125 -0
- package/docs/development.md +46 -0
- package/docs/evaluation-safety.md +27 -0
- package/docs/experience-playbook.md +35 -0
- package/docs/hermes.md +129 -0
- package/docs/library-api.md +31 -0
- package/docs/mcp-tool-reference.md +290 -0
- package/docs/provider-security.md +85 -0
- package/docs/state-model.md +113 -0
- package/docs/troubleshooting.md +95 -0
- package/docs/tui-operator-guide.md +75 -0
- package/package.json +3 -1
- package/scripts/dev/run_hermes_crispr_live.mjs +212 -0
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* 9. Materialize hook context snapshot
|
|
16
16
|
* 10. Write tasks/todo.md starter
|
|
17
17
|
*/
|
|
18
|
-
import { mkdirSync, existsSync } from "fs";
|
|
18
|
+
import { mkdirSync, existsSync, writeFileSync } from "fs";
|
|
19
19
|
import { join, dirname, resolve } from "path";
|
|
20
20
|
import { fileURLToPath } from "url";
|
|
21
21
|
import { openStore } from "./ace-packed-store.js";
|
|
@@ -219,6 +219,39 @@ export async function bootstrapStoreWorkspace(opts) {
|
|
|
219
219
|
}, { operation_label: "bootstrapStoreWorkspace" });
|
|
220
220
|
}
|
|
221
221
|
// ── ace repair ────────────────────────────────────────────────────────────────
|
|
222
|
+
const GOAL_STORE_KEY_PREFIX = "state/artifacts/agent-state/goals/";
|
|
223
|
+
/**
|
|
224
|
+
* G1 recovery: restore goal records that a pre-fix `ace compact` swept into the
|
|
225
|
+
* store. `GoalRepository` reads plain files, so a swept goal is invisible to the
|
|
226
|
+
* engine even though its bytes survive in ACEPACK. Idempotent: never overwrites
|
|
227
|
+
* a goal file that is already on disk (disk is authoritative for goals).
|
|
228
|
+
*/
|
|
229
|
+
async function restoreSweptGoalState(store, workspaceRoot, warnings) {
|
|
230
|
+
const goalsDir = join(workspaceRoot, "agent-state", "goals");
|
|
231
|
+
const keys = [];
|
|
232
|
+
for await (const key of store.list()) {
|
|
233
|
+
if (key.startsWith(GOAL_STORE_KEY_PREFIX))
|
|
234
|
+
keys.push(key);
|
|
235
|
+
}
|
|
236
|
+
let restored = 0;
|
|
237
|
+
for (const key of keys) {
|
|
238
|
+
const name = key.slice(GOAL_STORE_KEY_PREFIX.length);
|
|
239
|
+
if (!name || name.includes("/"))
|
|
240
|
+
continue;
|
|
241
|
+
const dest = join(goalsDir, name);
|
|
242
|
+
if (existsSync(dest))
|
|
243
|
+
continue;
|
|
244
|
+
const body = await store.getBlob(key);
|
|
245
|
+
if (body === undefined) {
|
|
246
|
+
warnings.push(`goal restore: key ${key} present in index but unreadable`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
mkdirSync(goalsDir, { recursive: true });
|
|
250
|
+
writeFileSync(dest, body);
|
|
251
|
+
restored += 1;
|
|
252
|
+
}
|
|
253
|
+
return restored;
|
|
254
|
+
}
|
|
222
255
|
export async function repairWorkspace(workspaceRoot) {
|
|
223
256
|
const resolved = await ensureCanonicalWorkspaceStore(workspaceRoot, {
|
|
224
257
|
operationLabel: "repairWorkspace",
|
|
@@ -239,6 +272,21 @@ export async function repairWorkspace(workspaceRoot) {
|
|
|
239
272
|
const store = await openStore(storePath);
|
|
240
273
|
const assetsRoot = getAssetsRoot();
|
|
241
274
|
try {
|
|
275
|
+
// A pre-fix `ace cache` could have ingested the write-coordinator's lease
|
|
276
|
+
// file as a permanent store key (it scanned .write.lock/ before that
|
|
277
|
+
// path was excluded). Delete it if present — it's transient runtime
|
|
278
|
+
// state, never a durable artifact.
|
|
279
|
+
const pollutedLeaseKeyPrefix = "state/artifacts/agent-state/ace-state.ace.write.lock/";
|
|
280
|
+
for await (const key of store.list()) {
|
|
281
|
+
if (key.startsWith(pollutedLeaseKeyPrefix)) {
|
|
282
|
+
await store.delete(key);
|
|
283
|
+
warnings.push(`Removed stale write-lock lease key ingested by a prior 'ace cache' run: ${key}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const restoredGoals = await restoreSweptGoalState(store, workspaceRoot, warnings);
|
|
287
|
+
if (restoredGoals > 0) {
|
|
288
|
+
warnings.push(`Restored ${restoredGoals} goal file(s) swept into the store by a pre-fix \`ace compact\`.`);
|
|
289
|
+
}
|
|
242
290
|
await migrateExistingStoreGateContract(store, workspaceRoot, assetsRoot, warnings);
|
|
243
291
|
const hostPolicy = (await store.getJSON("meta/host_materialization")) ?? {};
|
|
244
292
|
// Re-materialize host files
|
|
@@ -260,7 +308,7 @@ export async function repairWorkspace(workspaceRoot) {
|
|
|
260
308
|
return warnings;
|
|
261
309
|
}
|
|
262
310
|
// ── ace compact ───────────────────────────────────────────────────────────────
|
|
263
|
-
export async function compactWorkspace(workspaceRoot) {
|
|
311
|
+
export async function compactWorkspace(workspaceRoot, options = {}) {
|
|
264
312
|
const resolved = await ensureCanonicalWorkspaceStore(workspaceRoot, {
|
|
265
313
|
operationLabel: "compactWorkspace",
|
|
266
314
|
});
|
|
@@ -278,6 +326,12 @@ export async function compactWorkspace(workspaceRoot) {
|
|
|
278
326
|
await store.close();
|
|
279
327
|
}
|
|
280
328
|
const after = statSync(storePath).size;
|
|
281
|
-
|
|
329
|
+
const sweep = options.sweep === false
|
|
330
|
+
? undefined
|
|
331
|
+
: await (await import("./cache-workspace.js")).cacheWorkspaceArtifacts(workspaceRoot, {
|
|
332
|
+
clean: true,
|
|
333
|
+
minAgeMs: 0,
|
|
334
|
+
});
|
|
335
|
+
return { before, after, sweep };
|
|
282
336
|
}
|
|
283
337
|
//# sourceMappingURL=bootstrap-store.js.map
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export interface CacheWorkspaceOptions {
|
|
2
2
|
dryRun?: boolean;
|
|
3
3
|
clean?: boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Sweep-lite guard: do not ingest or delete files newer than this many
|
|
6
|
+
* milliseconds. This keeps lifecycle-triggered sweeps from racing a host
|
|
7
|
+
* agent that just wrote a loose compatibility file.
|
|
8
|
+
*/
|
|
9
|
+
minAgeMs?: number;
|
|
10
|
+
gcSemanticCache?: boolean;
|
|
4
11
|
}
|
|
5
12
|
export interface CacheWorkspaceResult {
|
|
6
13
|
storePath: string;
|
|
@@ -9,6 +16,7 @@ export interface CacheWorkspaceResult {
|
|
|
9
16
|
scanned_files: number;
|
|
10
17
|
cached_files: number;
|
|
11
18
|
removed_files: number;
|
|
19
|
+
removed_semantic_cache_files: number;
|
|
12
20
|
kept_projected_files: number;
|
|
13
21
|
skipped_files: number;
|
|
14
22
|
warnings: string[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
3
3
|
import { ACE_ROOT_REL, isProjectedAceWorkspacePath, mapAceWorkspaceRelativePath, resolveStoreFallbackKeysForPath, } from "../helpers.js";
|
|
4
4
|
import { normalizeRelPath } from "../shared.js";
|
|
@@ -9,6 +9,10 @@ const SCAN_ROOTS_REL = [
|
|
|
9
9
|
".agents/ACE/agent-state",
|
|
10
10
|
".agents/ACE/tasks",
|
|
11
11
|
".agents/ACE/skills",
|
|
12
|
+
// Root-level tasks/ — home of create_handoff's SWARM_HANDOFF.*.json payloads
|
|
13
|
+
// (see resolveStoreFallbackKeys). Distinct from .agents/ACE/tasks/, which
|
|
14
|
+
// holds the projected todo.md and is scanned separately above.
|
|
15
|
+
"tasks",
|
|
12
16
|
];
|
|
13
17
|
function isInside(base, target) {
|
|
14
18
|
const rel = relative(base, target);
|
|
@@ -49,7 +53,58 @@ function isTextBuffer(input) {
|
|
|
49
53
|
function selectScanRoots(workspaceRoot) {
|
|
50
54
|
return SCAN_ROOTS_REL.map((relPath) => join(workspaceRoot, relPath)).filter((abs) => existsSync(abs));
|
|
51
55
|
}
|
|
56
|
+
function removeSemanticCache(workspaceRoot, minAgeMs, dryRun) {
|
|
57
|
+
const root = join(workspaceRoot, ".tmp", "cache", "semantic");
|
|
58
|
+
if (!existsSync(root))
|
|
59
|
+
return 0;
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
let removed = 0;
|
|
62
|
+
for (const absPath of collectFiles(root)) {
|
|
63
|
+
const stats = statSync(absPath);
|
|
64
|
+
if (now - stats.mtimeMs < minAgeMs)
|
|
65
|
+
continue;
|
|
66
|
+
if (!dryRun) {
|
|
67
|
+
rmSync(absPath, { force: true });
|
|
68
|
+
pruneEmptyParents(absPath, root);
|
|
69
|
+
}
|
|
70
|
+
removed += 1;
|
|
71
|
+
}
|
|
72
|
+
return removed;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Paths that must never be scanned/ingested by the sweeper, even though they
|
|
76
|
+
* live under a scanned root: an active write-coordinator lease dir (ingesting
|
|
77
|
+
* it destroys a live lease or pollutes the store with a permanent key for a
|
|
78
|
+
* transient file), temp/rename-in-flight files, and quarantined corrupt-store
|
|
79
|
+
* copies left by M0.5's write guard.
|
|
80
|
+
*/
|
|
81
|
+
function isUnsafeToScan(canonicalPath) {
|
|
82
|
+
return (/(^|\/)ace-state\.ace\.write\.lock(\/|$)/.test(canonicalPath) ||
|
|
83
|
+
/\.tmp$/.test(canonicalPath) ||
|
|
84
|
+
/\.corrupt-\d+$/.test(canonicalPath) ||
|
|
85
|
+
/ace-state\.ace(\.[^/]+)?$/.test(canonicalPath));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Engine-owned durable state that lives under a scanned root but is NOT
|
|
89
|
+
* store-managed. `GoalRepository` (src/store/repositories/goal-repository.ts)
|
|
90
|
+
* reads and writes these as plain files via `readFileSync`/`renameSync`;
|
|
91
|
+
* ingesting and deleting them makes every goal unreadable (`Unknown goal_id`)
|
|
92
|
+
* and kills any detached `ace goal run` mid-flight. The sweep must leave them
|
|
93
|
+
* on disk. See REPORT_GOAL_EVAL_PLAYBOOK_MITIGATIONS_2026-07-09.md (G1).
|
|
94
|
+
*/
|
|
95
|
+
const SWEEP_EXCLUDED_DIRS = ["agent-state/goals"];
|
|
96
|
+
function isSweepExcluded(canonicalPath) {
|
|
97
|
+
return SWEEP_EXCLUDED_DIRS.some((dir) => canonicalPath === dir || canonicalPath.startsWith(`${dir}/`));
|
|
98
|
+
}
|
|
52
99
|
function isCacheableAcePath(path) {
|
|
100
|
+
if (isUnsafeToScan(path))
|
|
101
|
+
return false;
|
|
102
|
+
// Sits above the SWARM_HANDOFF early-return: an exclusion must never be
|
|
103
|
+
// overridden by a later allow.
|
|
104
|
+
if (isSweepExcluded(path))
|
|
105
|
+
return false;
|
|
106
|
+
if (/^tasks\/SWARM_HANDOFF\..+\.json$/.test(path))
|
|
107
|
+
return true;
|
|
53
108
|
return path === "agent-state"
|
|
54
109
|
|| path.startsWith("agent-state/")
|
|
55
110
|
|| path.startsWith(`${ACE_ROOT_REL}/`);
|
|
@@ -57,6 +112,8 @@ function isCacheableAcePath(path) {
|
|
|
57
112
|
export async function cacheWorkspaceArtifacts(workspaceRoot, options = {}) {
|
|
58
113
|
const dryRun = options.dryRun ?? false;
|
|
59
114
|
const clean = options.clean ?? true;
|
|
115
|
+
const minAgeMs = options.minAgeMs ?? 0;
|
|
116
|
+
const gcSemanticCache = options.gcSemanticCache ?? true;
|
|
60
117
|
const storePath = getWorkspaceStorePath(workspaceRoot);
|
|
61
118
|
if (!existsSync(storePath)) {
|
|
62
119
|
throw new Error(`No store found at ${storePath}. Run 'ace init' first.`);
|
|
@@ -72,6 +129,11 @@ export async function cacheWorkspaceArtifacts(workspaceRoot, options = {}) {
|
|
|
72
129
|
scanned += 1;
|
|
73
130
|
const relPath = normalizeRelPath(relative(workspaceRoot, absPath));
|
|
74
131
|
const canonicalPath = mapAceWorkspaceRelativePath(relPath);
|
|
132
|
+
const stats = statSync(absPath);
|
|
133
|
+
if (minAgeMs > 0 && Date.now() - stats.mtimeMs < minAgeMs) {
|
|
134
|
+
skipped += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
75
137
|
if (!isCacheableAcePath(canonicalPath)) {
|
|
76
138
|
skipped += 1;
|
|
77
139
|
continue;
|
|
@@ -129,6 +191,9 @@ export async function cacheWorkspaceArtifacts(workspaceRoot, options = {}) {
|
|
|
129
191
|
pruneEmptyParents(candidate.absPath, pruneRoot);
|
|
130
192
|
}
|
|
131
193
|
}
|
|
194
|
+
const removedSemanticCache = gcSemanticCache
|
|
195
|
+
? removeSemanticCache(workspaceRoot, minAgeMs, dryRun)
|
|
196
|
+
: 0;
|
|
132
197
|
return {
|
|
133
198
|
storePath,
|
|
134
199
|
dry_run: dryRun,
|
|
@@ -136,6 +201,7 @@ export async function cacheWorkspaceArtifacts(workspaceRoot, options = {}) {
|
|
|
136
201
|
scanned_files: scanned,
|
|
137
202
|
cached_files: candidates.length,
|
|
138
203
|
removed_files: removed,
|
|
204
|
+
removed_semantic_cache_files: removedSemanticCache,
|
|
139
205
|
kept_projected_files: keptProjected,
|
|
140
206
|
skipped_files: skipped,
|
|
141
207
|
warnings,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const CRC32_TABLE = new Uint32Array(256);
|
|
2
|
+
for (let i = 0; i < CRC32_TABLE.length; i += 1) {
|
|
3
|
+
let c = i;
|
|
4
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
5
|
+
c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
|
|
6
|
+
}
|
|
7
|
+
CRC32_TABLE[i] = c >>> 0;
|
|
8
|
+
}
|
|
9
|
+
export function crc32(bytes) {
|
|
10
|
+
let crc = 0xffffffff;
|
|
11
|
+
for (const byte of bytes) {
|
|
12
|
+
crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
13
|
+
}
|
|
14
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=crc32.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface StoreFsckCheck {
|
|
2
|
+
name: string;
|
|
3
|
+
ok: boolean;
|
|
4
|
+
detail: string;
|
|
5
|
+
}
|
|
6
|
+
export interface StoreFsckResult {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
repaired: boolean;
|
|
9
|
+
storePath: string;
|
|
10
|
+
checks: StoreFsckCheck[];
|
|
11
|
+
repaired_paths: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function fsckWorkspaceStore(workspaceRoot: string, options?: {
|
|
14
|
+
repair?: boolean;
|
|
15
|
+
}): StoreFsckResult;
|
|
16
|
+
//# sourceMappingURL=fsck.d.ts.map
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { basename, isAbsolute, join, relative } from "node:path";
|
|
3
|
+
import { HEADER_SIZE, HEADER_SIZE_V1, MAGIC } from "./types.js";
|
|
4
|
+
import { getCanonicalWorkspaceStorePath, getLegacyWorkspaceStorePath, getWorkspaceStorePath } from "./store-snapshot.js";
|
|
5
|
+
import { crc32 } from "./crc32.js";
|
|
6
|
+
import { classifyStoreWriteLockDir } from "./write-coordinator.js";
|
|
7
|
+
const DECODE = new TextDecoder();
|
|
8
|
+
function u32(bytes, offset) {
|
|
9
|
+
return new DataView(bytes.buffer, bytes.byteOffset).getUint32(offset, false);
|
|
10
|
+
}
|
|
11
|
+
function u64(bytes, offset) {
|
|
12
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset);
|
|
13
|
+
return view.getUint32(offset, false) * 2 ** 32 + view.getUint32(offset + 4, false);
|
|
14
|
+
}
|
|
15
|
+
function minEventSectionLength(count) {
|
|
16
|
+
const pad = (4 - (count % 4)) % 4;
|
|
17
|
+
return 4 + 8 * count + count + count + count + pad + 4 * count + 4 * count + 4;
|
|
18
|
+
}
|
|
19
|
+
function isInside(base, target) {
|
|
20
|
+
const rel = relative(base, target);
|
|
21
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
22
|
+
}
|
|
23
|
+
function collectOrphans(root) {
|
|
24
|
+
const stateRoot = join(root, "agent-state");
|
|
25
|
+
if (!existsSync(stateRoot))
|
|
26
|
+
return [];
|
|
27
|
+
const found = [];
|
|
28
|
+
const walk = (dir) => {
|
|
29
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
30
|
+
const abs = join(dir, entry.name);
|
|
31
|
+
const rel = relative(root, abs);
|
|
32
|
+
if (/(^|\/)ace-state\.ace\.write\.lock(\/|$)/.test(rel)) {
|
|
33
|
+
found.push({ path: abs, kind: "lock" });
|
|
34
|
+
if (entry.isDirectory())
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
else if (/\.tmp$/.test(entry.name)) {
|
|
38
|
+
found.push({ path: abs, kind: "tmp" });
|
|
39
|
+
if (entry.isDirectory())
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
else if (/\.corrupt-\d+$/.test(entry.name)) {
|
|
43
|
+
found.push({ path: abs, kind: "corrupt" });
|
|
44
|
+
if (entry.isDirectory())
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (entry.isDirectory())
|
|
48
|
+
walk(abs);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
walk(stateRoot);
|
|
52
|
+
return found;
|
|
53
|
+
}
|
|
54
|
+
function validateEventSection(bytes) {
|
|
55
|
+
if (bytes.length < 4)
|
|
56
|
+
return "event section shorter than count field";
|
|
57
|
+
const count = u32(bytes, 0);
|
|
58
|
+
const min = minEventSectionLength(count);
|
|
59
|
+
if (bytes.length < min)
|
|
60
|
+
return `event section count=${count} requires >= ${min} bytes, got ${bytes.length}`;
|
|
61
|
+
let pos = 4 + 8 * count + count + count + count + ((4 - (count % 4)) % 4);
|
|
62
|
+
const offsets = [];
|
|
63
|
+
const lengths = [];
|
|
64
|
+
for (let i = 0; i < count; i += 1) {
|
|
65
|
+
offsets.push(u32(bytes, pos));
|
|
66
|
+
pos += 4;
|
|
67
|
+
}
|
|
68
|
+
for (let i = 0; i < count; i += 1) {
|
|
69
|
+
lengths.push(u32(bytes, pos));
|
|
70
|
+
pos += 4;
|
|
71
|
+
}
|
|
72
|
+
const poolLength = u32(bytes, pos);
|
|
73
|
+
pos += 4;
|
|
74
|
+
if (bytes.length < pos + poolLength) {
|
|
75
|
+
return `event payload pool truncated (declared=${poolLength}, available=${bytes.length - pos})`;
|
|
76
|
+
}
|
|
77
|
+
for (let i = 0; i < count; i += 1) {
|
|
78
|
+
if (offsets[i] + lengths[i] > poolLength) {
|
|
79
|
+
return `event ${i} payload exceeds pool bounds (offset=${offsets[i]}, length=${lengths[i]}, pool=${poolLength})`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
export function fsckWorkspaceStore(workspaceRoot, options = {}) {
|
|
85
|
+
const storePath = getWorkspaceStorePath(workspaceRoot);
|
|
86
|
+
const checks = [];
|
|
87
|
+
const repairedPaths = [];
|
|
88
|
+
const canonicalPath = getCanonicalWorkspaceStorePath(workspaceRoot);
|
|
89
|
+
const legacyPath = getLegacyWorkspaceStorePath(workspaceRoot);
|
|
90
|
+
checks.push({
|
|
91
|
+
name: "store:exists",
|
|
92
|
+
ok: existsSync(storePath),
|
|
93
|
+
detail: existsSync(storePath) ? storePath : `missing: ${storePath}`,
|
|
94
|
+
});
|
|
95
|
+
checks.push({
|
|
96
|
+
name: "store:legacy-dual-store",
|
|
97
|
+
ok: !(existsSync(canonicalPath) && existsSync(legacyPath)),
|
|
98
|
+
detail: existsSync(canonicalPath) && existsSync(legacyPath)
|
|
99
|
+
? `both canonical and legacy stores exist (${canonicalPath}, ${legacyPath})`
|
|
100
|
+
: "no dual-store conflict",
|
|
101
|
+
});
|
|
102
|
+
if (existsSync(storePath)) {
|
|
103
|
+
try {
|
|
104
|
+
const file = readFileSync(storePath);
|
|
105
|
+
checks.push({
|
|
106
|
+
name: "header:size",
|
|
107
|
+
ok: file.length >= HEADER_SIZE,
|
|
108
|
+
detail: `${file.length} bytes`,
|
|
109
|
+
});
|
|
110
|
+
if (file.length >= HEADER_SIZE_V1) {
|
|
111
|
+
const magic = DECODE.decode(file.subarray(0, 8));
|
|
112
|
+
const version = u32(file, 8);
|
|
113
|
+
const kvIndexOffset = u64(file, 16);
|
|
114
|
+
const kvIndexLength = u64(file, 24);
|
|
115
|
+
const kvChunkEnd = version >= 2 ? u64(file, 32) : kvIndexOffset;
|
|
116
|
+
const evtOffset = version >= 2 ? u64(file, 40) : 0;
|
|
117
|
+
const evtLength = version >= 2 ? u64(file, 48) : 0;
|
|
118
|
+
const evtCount = version >= 2 ? u32(file, 56) : 0;
|
|
119
|
+
checks.push({ name: "header:magic", ok: magic === MAGIC, detail: magic });
|
|
120
|
+
checks.push({ name: "header:version", ok: version >= 1 && version <= 2, detail: String(version) });
|
|
121
|
+
const headerOrderOk = version >= 2
|
|
122
|
+
? HEADER_SIZE <= kvChunkEnd &&
|
|
123
|
+
kvChunkEnd <= evtOffset &&
|
|
124
|
+
evtOffset <= kvIndexOffset &&
|
|
125
|
+
kvIndexOffset + kvIndexLength <= file.length
|
|
126
|
+
: HEADER_SIZE_V1 <= kvIndexOffset && kvIndexOffset + kvIndexLength <= file.length;
|
|
127
|
+
checks.push({
|
|
128
|
+
name: "header:regions",
|
|
129
|
+
ok: headerOrderOk,
|
|
130
|
+
detail: `kvChunkEnd=${kvChunkEnd}, evtOffset=${evtOffset}, evtLength=${evtLength}, kvIndexOffset=${kvIndexOffset}, kvIndexLength=${kvIndexLength}, fileLength=${file.length}`,
|
|
131
|
+
});
|
|
132
|
+
if (headerOrderOk) {
|
|
133
|
+
const indexRaw = file.subarray(kvIndexOffset, kvIndexOffset + kvIndexLength);
|
|
134
|
+
let index;
|
|
135
|
+
try {
|
|
136
|
+
index = JSON.parse(DECODE.decode(indexRaw));
|
|
137
|
+
checks.push({ name: "index:parse", ok: true, detail: `${Object.keys(index).length} entries` });
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
checks.push({ name: "index:parse", ok: false, detail: error instanceof Error ? error.message : String(error) });
|
|
141
|
+
}
|
|
142
|
+
if (index) {
|
|
143
|
+
const minChunkOffset = version >= 2 ? HEADER_SIZE : HEADER_SIZE_V1;
|
|
144
|
+
const maxChunkEnd = version >= 2 ? kvChunkEnd : kvIndexOffset;
|
|
145
|
+
for (const [key, entry] of Object.entries(index)) {
|
|
146
|
+
const boundsOk = Number.isFinite(entry.offset) &&
|
|
147
|
+
Number.isFinite(entry.length) &&
|
|
148
|
+
entry.offset >= minChunkOffset &&
|
|
149
|
+
entry.offset + 4 + entry.length <= maxChunkEnd;
|
|
150
|
+
checks.push({
|
|
151
|
+
name: `chunk:${key}:bounds`,
|
|
152
|
+
ok: boundsOk,
|
|
153
|
+
detail: `offset=${entry.offset}, length=${entry.length}, maxChunkEnd=${maxChunkEnd}`,
|
|
154
|
+
});
|
|
155
|
+
if (!boundsOk)
|
|
156
|
+
continue;
|
|
157
|
+
const prefix = u32(file, entry.offset);
|
|
158
|
+
checks.push({
|
|
159
|
+
name: `chunk:${key}:length-prefix`,
|
|
160
|
+
ok: prefix === entry.length,
|
|
161
|
+
detail: `prefix=${prefix}, index=${entry.length}`,
|
|
162
|
+
});
|
|
163
|
+
if (typeof entry.crc === "number") {
|
|
164
|
+
const chunk = file.subarray(entry.offset + 4, entry.offset + 4 + entry.length);
|
|
165
|
+
const actual = crc32(chunk);
|
|
166
|
+
checks.push({
|
|
167
|
+
name: `chunk:${key}:crc32`,
|
|
168
|
+
ok: actual === entry.crc,
|
|
169
|
+
detail: `expected=${entry.crc}, actual=${actual}`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (version >= 2) {
|
|
175
|
+
const eventError = validateEventSection(file.subarray(evtOffset, evtOffset + evtLength));
|
|
176
|
+
checks.push({
|
|
177
|
+
name: "events:section",
|
|
178
|
+
ok: !eventError,
|
|
179
|
+
detail: eventError ?? `count=${evtCount}, length=${evtLength}`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
checks.push({
|
|
187
|
+
name: "store:read",
|
|
188
|
+
ok: false,
|
|
189
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const allOrphans = collectOrphans(workspaceRoot);
|
|
194
|
+
const failingOrphans = [];
|
|
195
|
+
for (const orphan of allOrphans) {
|
|
196
|
+
if (orphan.kind !== "lock") {
|
|
197
|
+
failingOrphans.push(orphan);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const classification = classifyStoreWriteLockDir(orphan.path);
|
|
201
|
+
if (classification === "active") {
|
|
202
|
+
checks.push({
|
|
203
|
+
name: "workspace:write-lock",
|
|
204
|
+
ok: true,
|
|
205
|
+
detail: `active write lease (holder pid in lease.json) — not an orphan: ${relative(workspaceRoot, orphan.path)}`,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
failingOrphans.push(orphan);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
checks.push({
|
|
213
|
+
name: "workspace:orphan-store-artifacts",
|
|
214
|
+
ok: failingOrphans.length === 0,
|
|
215
|
+
detail: failingOrphans.length === 0
|
|
216
|
+
? "none"
|
|
217
|
+
: failingOrphans.map((o) => relative(workspaceRoot, o.path)).join(", "),
|
|
218
|
+
});
|
|
219
|
+
if (options.repair) {
|
|
220
|
+
for (const orphan of failingOrphans) {
|
|
221
|
+
const rel = relative(workspaceRoot, orphan.path);
|
|
222
|
+
if (/\.corrupt-\d+$/.test(basename(orphan.path)))
|
|
223
|
+
continue;
|
|
224
|
+
if (!isInside(workspaceRoot, orphan.path))
|
|
225
|
+
continue;
|
|
226
|
+
if (orphan.kind === "lock" && classifyStoreWriteLockDir(orphan.path) === "active")
|
|
227
|
+
continue;
|
|
228
|
+
rmSync(orphan.path, { recursive: true, force: true });
|
|
229
|
+
repairedPaths.push(rel);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
ok: checks.every((check) => check.ok),
|
|
234
|
+
repaired: repairedPaths.length > 0,
|
|
235
|
+
storePath,
|
|
236
|
+
checks,
|
|
237
|
+
repaired_paths: repairedPaths,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=fsck.js.map
|
|
@@ -6,5 +6,6 @@ export declare class ContextSnapshotMaterializer {
|
|
|
6
6
|
constructor(store: AcePackedStore, workspaceRoot: string);
|
|
7
7
|
private directoryPath;
|
|
8
8
|
materializeAll(): Promise<void>;
|
|
9
|
+
materializeAllForExport(outputDir: string): Promise<void>;
|
|
9
10
|
}
|
|
10
11
|
//# sourceMappingURL=context-snapshot-materializer.d.ts.map
|
|
@@ -22,7 +22,10 @@ export class ContextSnapshotMaterializer {
|
|
|
22
22
|
return join(this.workspaceRoot, "agent-state", "context-snapshots");
|
|
23
23
|
}
|
|
24
24
|
async materializeAll() {
|
|
25
|
-
|
|
25
|
+
throw new Error("Context snapshots are store-only by default. Use `ace read agent-state/context-snapshots/<name>.json --out <file>` to export a copy.");
|
|
26
|
+
}
|
|
27
|
+
async materializeAllForExport(outputDir) {
|
|
28
|
+
const dir = outputDir;
|
|
26
29
|
if (!existsSync(dir))
|
|
27
30
|
mkdirSync(dir, { recursive: true });
|
|
28
31
|
const snapshots = await this.repo.listSnapshots();
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { AcePackedStore } from "../ace-packed-store.js";
|
|
2
2
|
export type RuntimeProjectionTarget = "context_snapshots" | "handoffs" | "ledger" | "scheduler" | "status_events" | "todo_state" | "vericify_posts" | "hook_context";
|
|
3
|
+
/**
|
|
4
|
+
* Awaitable handle for the detached lifecycle sweep. CLI entry points and
|
|
5
|
+
* tests MUST await this before process exit / assertions; the MCP server may
|
|
6
|
+
* ignore it (long-lived process, sweep completes in background).
|
|
7
|
+
*/
|
|
8
|
+
export declare function waitForPendingLifecycleSweep(): Promise<void>;
|
|
3
9
|
export declare class ProjectionManager {
|
|
4
10
|
private store;
|
|
5
11
|
private workspaceRoot;
|
|
@@ -7,6 +13,7 @@ export declare class ProjectionManager {
|
|
|
7
13
|
private todoSyncer;
|
|
8
14
|
private hookContext;
|
|
9
15
|
private scheduler;
|
|
16
|
+
private static lifecycleSweepActive;
|
|
10
17
|
constructor(store: AcePackedStore, workspaceRoot: string);
|
|
11
18
|
projectAfterCommit(targets: readonly RuntimeProjectionTarget[]): Promise<void>;
|
|
12
19
|
}
|
|
@@ -2,6 +2,18 @@ import { HookContextMaterializer } from "./hook-context-materializer.js";
|
|
|
2
2
|
import { SchedulerProjectionMaterializer } from "./scheduler-projection-materializer.js";
|
|
3
3
|
import { TodoSyncer } from "./todo-syncer.js";
|
|
4
4
|
import { VericifyProjector } from "./vericify-projector.js";
|
|
5
|
+
import { cacheWorkspaceArtifacts } from "../cache-workspace.js";
|
|
6
|
+
let pendingLifecycleSweep;
|
|
7
|
+
let lastLifecycleSweepStartedAt = 0;
|
|
8
|
+
const LIFECYCLE_SWEEP_MIN_INTERVAL_MS = 60_000;
|
|
9
|
+
/**
|
|
10
|
+
* Awaitable handle for the detached lifecycle sweep. CLI entry points and
|
|
11
|
+
* tests MUST await this before process exit / assertions; the MCP server may
|
|
12
|
+
* ignore it (long-lived process, sweep completes in background).
|
|
13
|
+
*/
|
|
14
|
+
export function waitForPendingLifecycleSweep() {
|
|
15
|
+
return pendingLifecycleSweep ?? Promise.resolve();
|
|
16
|
+
}
|
|
5
17
|
export class ProjectionManager {
|
|
6
18
|
store;
|
|
7
19
|
workspaceRoot;
|
|
@@ -9,6 +21,7 @@ export class ProjectionManager {
|
|
|
9
21
|
todoSyncer;
|
|
10
22
|
hookContext;
|
|
11
23
|
scheduler;
|
|
24
|
+
static lifecycleSweepActive = false;
|
|
12
25
|
constructor(store, workspaceRoot) {
|
|
13
26
|
this.store = store;
|
|
14
27
|
this.workspaceRoot = workspaceRoot;
|
|
@@ -55,6 +68,30 @@ export class ProjectionManager {
|
|
|
55
68
|
if (stagedStoreWrites) {
|
|
56
69
|
await this.store.commit();
|
|
57
70
|
}
|
|
71
|
+
if (stagedStoreWrites &&
|
|
72
|
+
!ProjectionManager.lifecycleSweepActive &&
|
|
73
|
+
Date.now() - lastLifecycleSweepStartedAt >= LIFECYCLE_SWEEP_MIN_INTERVAL_MS) {
|
|
74
|
+
ProjectionManager.lifecycleSweepActive = true;
|
|
75
|
+
lastLifecycleSweepStartedAt = Date.now();
|
|
76
|
+
const workspaceRoot = this.workspaceRoot;
|
|
77
|
+
// Deliberately NOT awaited: projectAfterCommit runs inside
|
|
78
|
+
// withStoreWriteCoordinator in most callers, and the sweep's own
|
|
79
|
+
// writeStoreBlobsQueued must enqueue BEHIND the current operation in the
|
|
80
|
+
// non-reentrant write queue. Awaiting here deadlocks (see
|
|
81
|
+
// REPORT_STORE_WIRING_MITIGATIONS_2026-07-09.md F1). The detached promise
|
|
82
|
+
// is exposed via waitForPendingLifecycleSweep() for CLI/test flows.
|
|
83
|
+
pendingLifecycleSweep = cacheWorkspaceArtifacts(workspaceRoot, {
|
|
84
|
+
clean: true,
|
|
85
|
+
minAgeMs: 5 * 60 * 1000,
|
|
86
|
+
})
|
|
87
|
+
.then(() => undefined)
|
|
88
|
+
.catch((error) => {
|
|
89
|
+
console.error(`ACE lifecycle sweep-lite skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
90
|
+
})
|
|
91
|
+
.finally(() => {
|
|
92
|
+
ProjectionManager.lifecycleSweepActive = false;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
58
95
|
for (const target of uniqueTargets) {
|
|
59
96
|
if (target === "todo_state") {
|
|
60
97
|
await this.todoSyncer.writeToFile();
|
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { operationalArtifactKey } from "../store-artifacts.js";
|
|
4
4
|
import { SchedulerRepository } from "../repositories/scheduler-repository.js";
|
|
5
|
+
import { isProjectedAceWorkspacePath } from "../../helpers/path-utils.js";
|
|
5
6
|
function writeText(path, content) {
|
|
6
7
|
const dir = dirname(path);
|
|
7
8
|
if (!existsSync(dir))
|
|
@@ -40,9 +41,15 @@ export class SchedulerProjectionMaterializer {
|
|
|
40
41
|
}
|
|
41
42
|
async materializeFiles(rendered) {
|
|
42
43
|
const content = rendered ?? (await this.render());
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
if (isProjectedAceWorkspacePath("agent-state/job-queue.json")) {
|
|
45
|
+
writeText(this.filePath("job-queue.json"), content.queue);
|
|
46
|
+
}
|
|
47
|
+
if (isProjectedAceWorkspacePath("agent-state/job-locks.json")) {
|
|
48
|
+
writeText(this.filePath("job-locks.json"), content.locks);
|
|
49
|
+
}
|
|
50
|
+
if (isProjectedAceWorkspacePath("agent-state/scheduler-lease.json")) {
|
|
51
|
+
writeText(this.filePath("scheduler-lease.json"), content.lease);
|
|
52
|
+
}
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
55
|
//# sourceMappingURL=scheduler-projection-materializer.js.map
|