@voybio/ace-swarm 2.4.4 → 2.4.7
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 +41 -12
- 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 +623 -11
- 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 +127 -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
package/dist/shared.d.ts
CHANGED
|
@@ -87,6 +87,15 @@ export declare const EVENT_TYPE: {
|
|
|
87
87
|
readonly GIT_INITIALIZED: "GIT_INITIALIZED";
|
|
88
88
|
readonly GIT_COMMITTED: "GIT_COMMITTED";
|
|
89
89
|
readonly HANDOFF_SCHEDULER_SYNCED: "HANDOFF_SCHEDULER_SYNCED";
|
|
90
|
+
readonly MCP_BINARY_PARSE_ERROR: "MCP_BINARY_PARSE_ERROR";
|
|
91
|
+
readonly MODEL_BRIDGE_COMPLETION_BLOCKED: "MODEL_BRIDGE_COMPLETION_BLOCKED";
|
|
92
|
+
readonly MODEL_CONTEXT_OVERFLOW_SUSPECTED: "MODEL_CONTEXT_OVERFLOW_SUSPECTED";
|
|
93
|
+
readonly OLLAMA_MODEL_LOAD_ERROR: "OLLAMA_MODEL_LOAD_ERROR";
|
|
94
|
+
readonly OLLAMA_REPAIR_ATTEMPTED: "OLLAMA_REPAIR_ATTEMPTED";
|
|
95
|
+
readonly ORCHESTRATOR_STEP: "ORCHESTRATOR_STEP";
|
|
96
|
+
readonly PLAN_UPDATE: "PLAN_UPDATE";
|
|
97
|
+
readonly PLAYBOOK_CURATED: "PLAYBOOK_CURATED";
|
|
98
|
+
readonly PLAYBOOK_REFLECT_FAILED: "PLAYBOOK_REFLECT_FAILED";
|
|
90
99
|
readonly RUNTIME_EXECUTOR_SESSION_BLOCKED: "RUNTIME_EXECUTOR_SESSION_BLOCKED";
|
|
91
100
|
readonly RUNTIME_EXECUTOR_SESSION_COMPLETED: "RUNTIME_EXECUTOR_SESSION_COMPLETED";
|
|
92
101
|
readonly RUNTIME_EXECUTOR_SESSION_FAILED: "RUNTIME_EXECUTOR_SESSION_FAILED";
|
|
@@ -109,6 +118,8 @@ export declare const EVENT_TYPE: {
|
|
|
109
118
|
readonly SCHEDULER_JOB_STARTED: "SCHEDULER_JOB_STARTED";
|
|
110
119
|
readonly SCHEDULER_RECOVERY: "SCHEDULER_RECOVERY";
|
|
111
120
|
readonly SCHEDULER_TICK: "SCHEDULER_TICK";
|
|
121
|
+
readonly SCHEDULER_WATCHDOG: "SCHEDULER_WATCHDOG";
|
|
122
|
+
readonly STRUCTURAL_LANE_FENCED: "STRUCTURAL_LANE_FENCED";
|
|
112
123
|
readonly TESTS_PASSED: "TESTS_PASSED";
|
|
113
124
|
readonly TESTS_FAILED: "TESTS_FAILED";
|
|
114
125
|
readonly TRACKER_COMMENT_CREATED: "TRACKER_COMMENT_CREATED";
|
|
@@ -120,6 +131,7 @@ export declare const EVENT_TYPE: {
|
|
|
120
131
|
readonly WORKSPACE_SESSION_CREATED: "WORKSPACE_SESSION_CREATED";
|
|
121
132
|
readonly WORKSPACE_SESSION_FAILED: "WORKSPACE_SESSION_FAILED";
|
|
122
133
|
readonly WORKSPACE_SESSION_REMOVED: "WORKSPACE_SESSION_REMOVED";
|
|
134
|
+
readonly WORKSPACE_WRITE_REJECTED: "WORKSPACE_WRITE_REJECTED";
|
|
123
135
|
};
|
|
124
136
|
export interface DomainKeyword {
|
|
125
137
|
keyword: string;
|
package/dist/shared.js
CHANGED
|
@@ -132,6 +132,15 @@ export const EVENT_TYPE = {
|
|
|
132
132
|
GIT_INITIALIZED: "GIT_INITIALIZED",
|
|
133
133
|
GIT_COMMITTED: "GIT_COMMITTED",
|
|
134
134
|
HANDOFF_SCHEDULER_SYNCED: "HANDOFF_SCHEDULER_SYNCED",
|
|
135
|
+
MCP_BINARY_PARSE_ERROR: "MCP_BINARY_PARSE_ERROR",
|
|
136
|
+
MODEL_BRIDGE_COMPLETION_BLOCKED: "MODEL_BRIDGE_COMPLETION_BLOCKED",
|
|
137
|
+
MODEL_CONTEXT_OVERFLOW_SUSPECTED: "MODEL_CONTEXT_OVERFLOW_SUSPECTED",
|
|
138
|
+
OLLAMA_MODEL_LOAD_ERROR: "OLLAMA_MODEL_LOAD_ERROR",
|
|
139
|
+
OLLAMA_REPAIR_ATTEMPTED: "OLLAMA_REPAIR_ATTEMPTED",
|
|
140
|
+
ORCHESTRATOR_STEP: "ORCHESTRATOR_STEP",
|
|
141
|
+
PLAN_UPDATE: "PLAN_UPDATE",
|
|
142
|
+
PLAYBOOK_CURATED: "PLAYBOOK_CURATED",
|
|
143
|
+
PLAYBOOK_REFLECT_FAILED: "PLAYBOOK_REFLECT_FAILED",
|
|
135
144
|
RUNTIME_EXECUTOR_SESSION_BLOCKED: "RUNTIME_EXECUTOR_SESSION_BLOCKED",
|
|
136
145
|
RUNTIME_EXECUTOR_SESSION_COMPLETED: "RUNTIME_EXECUTOR_SESSION_COMPLETED",
|
|
137
146
|
RUNTIME_EXECUTOR_SESSION_FAILED: "RUNTIME_EXECUTOR_SESSION_FAILED",
|
|
@@ -154,6 +163,8 @@ export const EVENT_TYPE = {
|
|
|
154
163
|
SCHEDULER_JOB_STARTED: "SCHEDULER_JOB_STARTED",
|
|
155
164
|
SCHEDULER_RECOVERY: "SCHEDULER_RECOVERY",
|
|
156
165
|
SCHEDULER_TICK: "SCHEDULER_TICK",
|
|
166
|
+
SCHEDULER_WATCHDOG: "SCHEDULER_WATCHDOG",
|
|
167
|
+
STRUCTURAL_LANE_FENCED: "STRUCTURAL_LANE_FENCED",
|
|
157
168
|
TESTS_PASSED: "TESTS_PASSED",
|
|
158
169
|
TESTS_FAILED: "TESTS_FAILED",
|
|
159
170
|
TRACKER_COMMENT_CREATED: "TRACKER_COMMENT_CREATED",
|
|
@@ -164,7 +175,8 @@ export const EVENT_TYPE = {
|
|
|
164
175
|
WORKSPACE_HOOK_FAILED: "WORKSPACE_HOOK_FAILED",
|
|
165
176
|
WORKSPACE_SESSION_CREATED: "WORKSPACE_SESSION_CREATED",
|
|
166
177
|
WORKSPACE_SESSION_FAILED: "WORKSPACE_SESSION_FAILED",
|
|
167
|
-
WORKSPACE_SESSION_REMOVED: "WORKSPACE_SESSION_REMOVED"
|
|
178
|
+
WORKSPACE_SESSION_REMOVED: "WORKSPACE_SESSION_REMOVED",
|
|
179
|
+
WORKSPACE_WRITE_REJECTED: "WORKSPACE_WRITE_REJECTED",
|
|
168
180
|
};
|
|
169
181
|
export const DOMAIN_KEYWORDS = {
|
|
170
182
|
venture: [
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type SourceEditEventType = "SOURCE_EDIT_LEASE_ACQUIRED" | "SOURCE_EDIT_CONFLICT" | "SOURCE_EDIT_COMMITTED";
|
|
2
|
+
export interface SourceEditEvent {
|
|
3
|
+
event: SourceEditEventType;
|
|
4
|
+
target_path: string;
|
|
5
|
+
timestamp: string;
|
|
6
|
+
pid: number;
|
|
7
|
+
hostname: string;
|
|
8
|
+
lock_path?: string;
|
|
9
|
+
waited_ms?: number;
|
|
10
|
+
base_hash?: string;
|
|
11
|
+
expected_hash?: string;
|
|
12
|
+
observed_hash?: string;
|
|
13
|
+
committed_hash?: string;
|
|
14
|
+
reason_code?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SourceMutationCoordinatorOptions {
|
|
17
|
+
agent_id?: string;
|
|
18
|
+
wait_ms?: number;
|
|
19
|
+
poll_interval_ms?: number;
|
|
20
|
+
lease_ttl_ms?: number;
|
|
21
|
+
stale_grace_ms?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface SourceEditLeaseInfo {
|
|
24
|
+
token: string;
|
|
25
|
+
targetPath: string;
|
|
26
|
+
lockPath: string;
|
|
27
|
+
metadataPath: string;
|
|
28
|
+
pid: number;
|
|
29
|
+
hostname: string;
|
|
30
|
+
agentId?: string;
|
|
31
|
+
acquiredAt: string;
|
|
32
|
+
expiresAt: string;
|
|
33
|
+
ttlMs: number;
|
|
34
|
+
waitedMs: number;
|
|
35
|
+
}
|
|
36
|
+
export declare class SourceEditLeaseBusyError extends Error {
|
|
37
|
+
readonly reason_code = "source_edit_lease_busy";
|
|
38
|
+
readonly targetPath: string;
|
|
39
|
+
readonly lockPath: string;
|
|
40
|
+
readonly waitedMs: number;
|
|
41
|
+
readonly currentHolder?: SourceEditLeaseInfo;
|
|
42
|
+
constructor(input: {
|
|
43
|
+
targetPath: string;
|
|
44
|
+
lockPath: string;
|
|
45
|
+
waitedMs: number;
|
|
46
|
+
currentHolder?: SourceEditLeaseInfo;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export declare function sourceEditLockPath(targetPath: string): string;
|
|
50
|
+
export declare function appendSourceEditEvent(event: Omit<SourceEditEvent, "timestamp" | "pid" | "hostname">): void;
|
|
51
|
+
/** Test/CLI hook: resolves when everything buffered so far has been flushed. */
|
|
52
|
+
export declare function waitForSourceEditEventFlush(): Promise<void>;
|
|
53
|
+
export declare function withSourceMutationCoordinatorSync<T>(targetPath: string, fn: (lease: SourceEditLeaseInfo) => T, options?: SourceMutationCoordinatorOptions): T;
|
|
54
|
+
//# sourceMappingURL=source-mutation-coordinator.d.ts.map
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { wsPath } from "./helpers.js";
|
|
6
|
+
import { normalizeRelPath, slugify } from "./shared.js";
|
|
7
|
+
import { getWorkspaceStorePath, readStoreBlobSync } from "./store/store-snapshot.js";
|
|
8
|
+
import { writeStoreBlobSync } from "./store/store-artifacts.js";
|
|
9
|
+
import { withStoreWriteCoordinator } from "./store/write-coordinator.js";
|
|
10
|
+
const DEFAULT_WAIT_MS = 15_000;
|
|
11
|
+
const DEFAULT_POLL_INTERVAL_MS = 100;
|
|
12
|
+
const DEFAULT_LEASE_TTL_MS = 30_000;
|
|
13
|
+
const DEFAULT_STALE_GRACE_MS = 1_000;
|
|
14
|
+
const SOURCE_EDIT_EVENTS_KEY = "state/source-edit-events.ndjson";
|
|
15
|
+
const SOURCE_EDIT_EVENTS_ARCHIVE_KEY = "state/source-edit-events-archive.ndjson";
|
|
16
|
+
const MAX_SOURCE_EDIT_EVENT_LINES = 2000; // mirror status-events.ts MAX_EVENT_LINES
|
|
17
|
+
let pendingSourceEditLines = [];
|
|
18
|
+
let sourceEditFlush;
|
|
19
|
+
export class SourceEditLeaseBusyError extends Error {
|
|
20
|
+
reason_code = "source_edit_lease_busy";
|
|
21
|
+
targetPath;
|
|
22
|
+
lockPath;
|
|
23
|
+
waitedMs;
|
|
24
|
+
currentHolder;
|
|
25
|
+
constructor(input) {
|
|
26
|
+
super(input.currentHolder
|
|
27
|
+
? `Source edit lease busy for ${input.targetPath}; held by pid=${input.currentHolder.pid} hostname=${input.currentHolder.hostname}`
|
|
28
|
+
: `Source edit lease busy for ${input.targetPath}; no lease metadata available`);
|
|
29
|
+
this.name = "SourceEditLeaseBusyError";
|
|
30
|
+
this.targetPath = input.targetPath;
|
|
31
|
+
this.lockPath = input.lockPath;
|
|
32
|
+
this.waitedMs = input.waitedMs;
|
|
33
|
+
this.currentHolder = input.currentHolder;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function sleepSync(ms) {
|
|
37
|
+
if (ms <= 0)
|
|
38
|
+
return;
|
|
39
|
+
const shared = new SharedArrayBuffer(4);
|
|
40
|
+
const view = new Int32Array(shared);
|
|
41
|
+
Atomics.wait(view, 0, 0, ms);
|
|
42
|
+
}
|
|
43
|
+
function lockSlugForPath(targetPath) {
|
|
44
|
+
const normalized = normalizeRelPath(targetPath);
|
|
45
|
+
const digest = randomStableSuffix(normalized);
|
|
46
|
+
return `${slugify(normalized) || "root"}-${digest}`;
|
|
47
|
+
}
|
|
48
|
+
function randomStableSuffix(value) {
|
|
49
|
+
let hash = 0x811c9dc5;
|
|
50
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
51
|
+
hash ^= value.charCodeAt(i);
|
|
52
|
+
hash = Math.imul(hash, 0x01000193);
|
|
53
|
+
}
|
|
54
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
55
|
+
}
|
|
56
|
+
export function sourceEditLockPath(targetPath) {
|
|
57
|
+
return wsPath(".ace-staging", "locks", `file__${lockSlugForPath(targetPath)}.lock`);
|
|
58
|
+
}
|
|
59
|
+
function leaseMetadataPath(lockPath) {
|
|
60
|
+
return join(lockPath, "lease.json");
|
|
61
|
+
}
|
|
62
|
+
function normalizeLeaseRecord(record) {
|
|
63
|
+
if (!record)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (typeof record.token !== "string" ||
|
|
66
|
+
typeof record.targetPath !== "string" ||
|
|
67
|
+
typeof record.lockPath !== "string" ||
|
|
68
|
+
typeof record.metadataPath !== "string" ||
|
|
69
|
+
typeof record.pid !== "number" ||
|
|
70
|
+
typeof record.hostname !== "string" ||
|
|
71
|
+
typeof record.acquiredAt !== "string" ||
|
|
72
|
+
typeof record.expiresAt !== "string" ||
|
|
73
|
+
typeof record.ttlMs !== "number" ||
|
|
74
|
+
typeof record.waitedMs !== "number") {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
token: record.token,
|
|
79
|
+
targetPath: record.targetPath,
|
|
80
|
+
lockPath: record.lockPath,
|
|
81
|
+
metadataPath: record.metadataPath,
|
|
82
|
+
pid: record.pid,
|
|
83
|
+
hostname: record.hostname,
|
|
84
|
+
agentId: typeof record.agentId === "string" ? record.agentId : undefined,
|
|
85
|
+
acquiredAt: record.acquiredAt,
|
|
86
|
+
expiresAt: record.expiresAt,
|
|
87
|
+
ttlMs: record.ttlMs,
|
|
88
|
+
waitedMs: record.waitedMs,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function readLeaseRecord(lockPath) {
|
|
92
|
+
const metadataPath = leaseMetadataPath(lockPath);
|
|
93
|
+
if (!existsSync(metadataPath))
|
|
94
|
+
return undefined;
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(readFileSync(metadataPath, "utf-8"));
|
|
97
|
+
return normalizeLeaseRecord(parsed);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function leaseIsStale(lease, staleGraceMs) {
|
|
104
|
+
const expiresAt = Date.parse(lease.expiresAt);
|
|
105
|
+
if (!Number.isFinite(expiresAt))
|
|
106
|
+
return true;
|
|
107
|
+
return Date.now() > expiresAt + staleGraceMs;
|
|
108
|
+
}
|
|
109
|
+
function canRecoverStaleLease(lease, staleGraceMs) {
|
|
110
|
+
if (!leaseIsStale(lease, staleGraceMs))
|
|
111
|
+
return false;
|
|
112
|
+
if (lease.hostname !== hostname())
|
|
113
|
+
return false;
|
|
114
|
+
try {
|
|
115
|
+
process.kill(lease.pid, 0);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
return error instanceof Error && "code" in error && error.code === "ESRCH";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function createLeaseRecord(targetPath, lockPath, options, waitedMs) {
|
|
123
|
+
const ttlMs = options.lease_ttl_ms ?? DEFAULT_LEASE_TTL_MS;
|
|
124
|
+
const acquiredAtMs = Date.now();
|
|
125
|
+
return {
|
|
126
|
+
token: randomUUID(),
|
|
127
|
+
targetPath,
|
|
128
|
+
lockPath,
|
|
129
|
+
metadataPath: leaseMetadataPath(lockPath),
|
|
130
|
+
pid: process.pid,
|
|
131
|
+
hostname: hostname(),
|
|
132
|
+
agentId: options.agent_id,
|
|
133
|
+
acquiredAt: new Date(acquiredAtMs).toISOString(),
|
|
134
|
+
expiresAt: new Date(acquiredAtMs + ttlMs).toISOString(),
|
|
135
|
+
ttlMs,
|
|
136
|
+
waitedMs,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function tryAcquireLease(targetPath, lockPath, options, waitedMs) {
|
|
140
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
141
|
+
mkdirSync(lockPath, { recursive: false });
|
|
142
|
+
const lease = createLeaseRecord(targetPath, lockPath, options, waitedMs);
|
|
143
|
+
writeFileSync(lease.metadataPath, JSON.stringify({ kind: "source_edit_lease", ...lease }, null, 2), "utf-8");
|
|
144
|
+
return lease;
|
|
145
|
+
}
|
|
146
|
+
function acquireLease(targetPath, options) {
|
|
147
|
+
const lockPath = sourceEditLockPath(targetPath);
|
|
148
|
+
const waitMs = options.wait_ms ?? DEFAULT_WAIT_MS;
|
|
149
|
+
const pollIntervalMs = options.poll_interval_ms ?? DEFAULT_POLL_INTERVAL_MS;
|
|
150
|
+
const staleGraceMs = options.stale_grace_ms ?? DEFAULT_STALE_GRACE_MS;
|
|
151
|
+
const startedAt = Date.now();
|
|
152
|
+
while (Date.now() - startedAt <= waitMs) {
|
|
153
|
+
try {
|
|
154
|
+
return tryAcquireLease(targetPath, lockPath, options, Date.now() - startedAt);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const isBusy = error instanceof Error &&
|
|
158
|
+
"code" in error &&
|
|
159
|
+
error.code === "EEXIST";
|
|
160
|
+
if (!isBusy)
|
|
161
|
+
throw error;
|
|
162
|
+
const current = readLeaseRecord(lockPath);
|
|
163
|
+
if (current && canRecoverStaleLease(current, staleGraceMs)) {
|
|
164
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
sleepSync(Math.min(pollIntervalMs, waitMs));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw new SourceEditLeaseBusyError({
|
|
171
|
+
targetPath,
|
|
172
|
+
lockPath,
|
|
173
|
+
waitedMs: Date.now() - startedAt,
|
|
174
|
+
currentHolder: readLeaseRecord(lockPath),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function releaseLease(lease) {
|
|
178
|
+
const current = readLeaseRecord(lease.lockPath);
|
|
179
|
+
if (!current) {
|
|
180
|
+
rmSync(lease.lockPath, { recursive: true, force: true });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (current.token !== lease.token) {
|
|
184
|
+
throw new Error(`Source edit lease token mismatch while releasing ${lease.targetPath}`);
|
|
185
|
+
}
|
|
186
|
+
rmSync(lease.lockPath, { recursive: true, force: true });
|
|
187
|
+
}
|
|
188
|
+
export function appendSourceEditEvent(event) {
|
|
189
|
+
const payload = {
|
|
190
|
+
...event,
|
|
191
|
+
timestamp: new Date().toISOString(),
|
|
192
|
+
pid: process.pid,
|
|
193
|
+
hostname: hostname(),
|
|
194
|
+
};
|
|
195
|
+
const workspaceRoot = wsPath();
|
|
196
|
+
if (existsSync(getWorkspaceStorePath(workspaceRoot))) {
|
|
197
|
+
pendingSourceEditLines.push(JSON.stringify(payload));
|
|
198
|
+
scheduleSourceEditFlush(workspaceRoot);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const eventsPath = wsPath("agent-state", "source-edit-events.ndjson");
|
|
202
|
+
mkdirSync(dirname(eventsPath), { recursive: true });
|
|
203
|
+
appendFileSync(eventsPath, `${JSON.stringify(payload)}\n`, "utf-8");
|
|
204
|
+
}
|
|
205
|
+
function scheduleSourceEditFlush(workspaceRoot) {
|
|
206
|
+
if (sourceEditFlush)
|
|
207
|
+
return; // one in-flight flush drains everything queued behind it
|
|
208
|
+
sourceEditFlush = flushSourceEditEvents(workspaceRoot).finally(() => {
|
|
209
|
+
sourceEditFlush = undefined;
|
|
210
|
+
if (pendingSourceEditLines.length > 0)
|
|
211
|
+
scheduleSourceEditFlush(workspaceRoot);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
async function flushSourceEditEvents(workspaceRoot) {
|
|
215
|
+
const storePath = getWorkspaceStorePath(workspaceRoot);
|
|
216
|
+
try {
|
|
217
|
+
await withStoreWriteCoordinator(storePath, async () => {
|
|
218
|
+
const lines = pendingSourceEditLines;
|
|
219
|
+
if (lines.length === 0)
|
|
220
|
+
return;
|
|
221
|
+
pendingSourceEditLines = [];
|
|
222
|
+
const existing = readStoreBlobSync(workspaceRoot, SOURCE_EDIT_EVENTS_KEY) ?? "";
|
|
223
|
+
const merged = `${existing.replace(/\n?$/, existing ? "\n" : "")}${lines.join("\n")}\n`;
|
|
224
|
+
const all = merged.split("\n").filter((l) => l.trim().length > 0);
|
|
225
|
+
if (all.length > MAX_SOURCE_EDIT_EVENT_LINES) {
|
|
226
|
+
const excess = all.splice(0, all.length - MAX_SOURCE_EDIT_EVENT_LINES);
|
|
227
|
+
const archive = readStoreBlobSync(workspaceRoot, SOURCE_EDIT_EVENTS_ARCHIVE_KEY) ?? "";
|
|
228
|
+
writeStoreBlobSync(workspaceRoot, SOURCE_EDIT_EVENTS_ARCHIVE_KEY, `${archive.replace(/\n?$/, archive ? "\n" : "")}${excess.join("\n")}\n`);
|
|
229
|
+
}
|
|
230
|
+
writeStoreBlobSync(workspaceRoot, SOURCE_EDIT_EVENTS_KEY, all.join("\n") + "\n");
|
|
231
|
+
}, { operation_label: "sourceEditEvents.flush" });
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
console.error(`source-edit event flush skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** Test/CLI hook: resolves when everything buffered so far has been flushed. */
|
|
238
|
+
export async function waitForSourceEditEventFlush() {
|
|
239
|
+
while (sourceEditFlush || pendingSourceEditLines.length > 0) {
|
|
240
|
+
await (sourceEditFlush ?? Promise.resolve());
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
export function withSourceMutationCoordinatorSync(targetPath, fn, options = {}) {
|
|
244
|
+
const normalizedTarget = normalizeRelPath(targetPath);
|
|
245
|
+
const lease = acquireLease(normalizedTarget, options);
|
|
246
|
+
appendSourceEditEvent({
|
|
247
|
+
event: "SOURCE_EDIT_LEASE_ACQUIRED",
|
|
248
|
+
target_path: normalizedTarget,
|
|
249
|
+
lock_path: resolve(lease.lockPath),
|
|
250
|
+
waited_ms: lease.waitedMs,
|
|
251
|
+
});
|
|
252
|
+
try {
|
|
253
|
+
return fn(lease);
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
releaseLease(lease);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=source-mutation-coordinator.js.map
|
package/dist/status-events.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export interface AppendStatusEventInput {
|
|
|
10
10
|
objective_id?: string;
|
|
11
11
|
decision_id?: string;
|
|
12
12
|
}
|
|
13
|
+
export interface AppendStatusEventOptions {
|
|
14
|
+
workspaceRoot?: string;
|
|
15
|
+
}
|
|
13
16
|
export interface StatusEventRecord {
|
|
14
17
|
schema_version: "1.0.0";
|
|
15
18
|
event_id: string;
|
|
@@ -31,7 +34,7 @@ export declare function appendStatusEvent(input: AppendStatusEventInput): {
|
|
|
31
34
|
* Concurrency-safe variant of appendStatusEvent.
|
|
32
35
|
* Use this from async tool handlers where parallel subagents may fire.
|
|
33
36
|
*/
|
|
34
|
-
export declare function appendStatusEventSafe(input: AppendStatusEventInput): Promise<{
|
|
37
|
+
export declare function appendStatusEventSafe(input: AppendStatusEventInput, options?: AppendStatusEventOptions): Promise<{
|
|
35
38
|
path: string;
|
|
36
39
|
event: StatusEventRecord;
|
|
37
40
|
}>;
|
package/dist/status-events.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
3
4
|
import { resolveWorkspaceRoot, resolveWorkspaceArtifactPath as resolveWorkspaceArtifactPathHelper, safeRead, safeWrite, } from "./helpers.js";
|
|
4
5
|
import { isReadError } from "./shared.js";
|
|
5
6
|
import { validateStatusEventPayload, PROVENANCE_CRITICAL_EVENT_TYPES } from "./schemas.js";
|
|
@@ -52,8 +53,8 @@ function safeReadWorkspaceFile(filePath) {
|
|
|
52
53
|
function safeWriteWorkspaceFile(filePath, content) {
|
|
53
54
|
return safeWrite(filePath, content);
|
|
54
55
|
}
|
|
55
|
-
async function withDynamicFileLock(path, fn) {
|
|
56
|
-
const key = resolveWorkspaceArtifactPath(path);
|
|
56
|
+
async function withDynamicFileLock(path, fn, root) {
|
|
57
|
+
const key = root ? join(root, path) : resolveWorkspaceArtifactPath(path);
|
|
57
58
|
const prev = _fileLocks.get(key) ?? Promise.resolve();
|
|
58
59
|
let releaseLock;
|
|
59
60
|
const next = new Promise((resolveLock) => {
|
|
@@ -77,6 +78,12 @@ function readExistingNdjson() {
|
|
|
77
78
|
}
|
|
78
79
|
return existing.trimEnd();
|
|
79
80
|
}
|
|
81
|
+
function readExistingNdjsonAtRoot(root) {
|
|
82
|
+
const path = join(root, STATUS_EVENTS_REL_PATH);
|
|
83
|
+
if (!existsSync(path))
|
|
84
|
+
return "";
|
|
85
|
+
return readFileSync(path, "utf-8").trimEnd();
|
|
86
|
+
}
|
|
80
87
|
function rotateIfNeeded(content) {
|
|
81
88
|
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
82
89
|
if (lines.length <= MAX_EVENT_LINES)
|
|
@@ -90,6 +97,31 @@ function rotateIfNeeded(content) {
|
|
|
90
97
|
safeWriteWorkspaceFile(STATUS_EVENTS_ARCHIVE_REL, archiveFull);
|
|
91
98
|
return lines.join("\n") + "\n";
|
|
92
99
|
}
|
|
100
|
+
function rotateIfNeededAtRoot(root, content) {
|
|
101
|
+
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
102
|
+
if (lines.length <= MAX_EVENT_LINES)
|
|
103
|
+
return content;
|
|
104
|
+
const excess = lines.splice(0, lines.length - MAX_EVENT_LINES);
|
|
105
|
+
const archiveContent = excess.join("\n") + "\n";
|
|
106
|
+
const archivePath = join(root, STATUS_EVENTS_ARCHIVE_REL);
|
|
107
|
+
const existingArchive = existsSync(archivePath) ? readFileSync(archivePath, "utf-8") : "";
|
|
108
|
+
const archiveFull = existingArchive.trim().length === 0
|
|
109
|
+
? archiveContent
|
|
110
|
+
: `${existingArchive.trimEnd()}\n${archiveContent}`;
|
|
111
|
+
mkdirSync(dirname(archivePath), { recursive: true });
|
|
112
|
+
writeFileSync(archivePath, archiveFull, "utf-8");
|
|
113
|
+
return lines.join("\n") + "\n";
|
|
114
|
+
}
|
|
115
|
+
function appendStatusEventToRoot(root, event) {
|
|
116
|
+
const existing = readExistingNdjsonAtRoot(root);
|
|
117
|
+
const line = JSON.stringify(event);
|
|
118
|
+
const combined = existing.length > 0 ? `${existing}\n${line}\n` : `${line}\n`;
|
|
119
|
+
const next = rotateIfNeededAtRoot(root, combined);
|
|
120
|
+
const path = join(root, STATUS_EVENTS_REL_PATH);
|
|
121
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
122
|
+
writeFileSync(path, next, "utf-8");
|
|
123
|
+
return { path, event };
|
|
124
|
+
}
|
|
93
125
|
async function mirrorStatusEventToStore(root, event) {
|
|
94
126
|
const storePath = getWorkspaceStorePath(root);
|
|
95
127
|
if (!existsSync(storePath))
|
|
@@ -186,16 +218,19 @@ export function appendStatusEvent(input) {
|
|
|
186
218
|
* Concurrency-safe variant of appendStatusEvent.
|
|
187
219
|
* Use this from async tool handlers where parallel subagents may fire.
|
|
188
220
|
*/
|
|
189
|
-
export async function appendStatusEventSafe(input) {
|
|
190
|
-
const root = workspaceRoot();
|
|
221
|
+
export async function appendStatusEventSafe(input, options = {}) {
|
|
222
|
+
const root = options.workspaceRoot ?? workspaceRoot();
|
|
191
223
|
return withDynamicFileLock(STATUS_EVENTS_REL_PATH, async () => {
|
|
192
224
|
const event = buildStatusEvent(input);
|
|
193
225
|
validateStatusEvent(event);
|
|
194
226
|
if (storeExistsSync(root)) {
|
|
195
227
|
return appendStatusEventStoreBacked(root, event);
|
|
196
228
|
}
|
|
229
|
+
if (options.workspaceRoot) {
|
|
230
|
+
return appendStatusEventToRoot(root, event);
|
|
231
|
+
}
|
|
197
232
|
return appendStatusEvent(input);
|
|
198
|
-
});
|
|
233
|
+
}, options.workspaceRoot);
|
|
199
234
|
}
|
|
200
235
|
function mapStoreTrackerEvent(record) {
|
|
201
236
|
const metadata = record.metadata ?? {};
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* AcePackedStore — ACEPACK v2
|
|
3
3
|
*
|
|
4
4
|
* Single-file binary store. One file, no subdirectories, no companion files.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Recent events live in the hot columnar section. Older contiguous event
|
|
6
|
+
* prefixes can be moved into BGZF-compressed KV archive batches with
|
|
7
|
+
* archiveColdEvents(); getEntries() reads both layers transparently.
|
|
8
8
|
*
|
|
9
9
|
* File layout:
|
|
10
10
|
*
|
|
@@ -45,14 +45,27 @@
|
|
|
45
45
|
* Timestamp/kind scanning reads only the fixed-width columns, never the pool.
|
|
46
46
|
*
|
|
47
47
|
* compact() removes dead KV space (overwritten keys) via atomic tmp-rename.
|
|
48
|
-
* Events are preserved across compact()
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* Events are preserved across compact(). For very long-lived workspaces,
|
|
49
|
+
* archiveColdEvents() moves old event prefixes into archive/events/* BGZF
|
|
50
|
+
* batches and advances evtBaseId.
|
|
51
51
|
*
|
|
52
52
|
* Backward compat: v1 files (64-byte header) are migrated on first commit().
|
|
53
53
|
* v1 events live in core/log/ KV blobs and are pulled into the columnar section.
|
|
54
54
|
*/
|
|
55
55
|
import { IAcePackedStore, UnifiedEntry, EntryFilter, TopologyEntry, ContentSourceCode } from "./types.js";
|
|
56
|
+
export declare class AcePackedStoreCorruptionError extends Error {
|
|
57
|
+
constructor(message: string);
|
|
58
|
+
}
|
|
59
|
+
interface ArchiveColdEventsOptions {
|
|
60
|
+
olderThanMs?: number;
|
|
61
|
+
maxEventsPerBatch?: number;
|
|
62
|
+
now?: number;
|
|
63
|
+
}
|
|
64
|
+
export interface ArchiveColdEventsResult {
|
|
65
|
+
batches_written: number;
|
|
66
|
+
events_archived: number;
|
|
67
|
+
bytes_written: number;
|
|
68
|
+
}
|
|
56
69
|
export declare class AcePackedStore implements IAcePackedStore {
|
|
57
70
|
private storePath;
|
|
58
71
|
private readOnly;
|
|
@@ -65,6 +78,7 @@ export declare class AcePackedStore implements IAcePackedStore {
|
|
|
65
78
|
private committed;
|
|
66
79
|
private pending;
|
|
67
80
|
private evtBaseId;
|
|
81
|
+
private skippedMalformedCount;
|
|
68
82
|
open(path: string, opts?: {
|
|
69
83
|
readOnly?: boolean;
|
|
70
84
|
}): Promise<void>;
|
|
@@ -73,13 +87,14 @@ export declare class AcePackedStore implements IAcePackedStore {
|
|
|
73
87
|
commit(): Promise<void>;
|
|
74
88
|
/**
|
|
75
89
|
* Compacts the KV chunk region — removes dead space left by overwritten keys.
|
|
76
|
-
* Events are
|
|
77
|
-
*
|
|
78
|
-
* See HOT_COLD_EVENT_TIERING.md for the future branch that adds BGZF archiving.
|
|
90
|
+
* Events are preserved exactly as currently tiered. Use archiveColdEvents()
|
|
91
|
+
* first when old hot events should move into BGZF archive batches.
|
|
79
92
|
*/
|
|
80
93
|
compact(): Promise<void>;
|
|
81
94
|
close(): Promise<void>;
|
|
82
95
|
get(key: string): Promise<Uint8Array | undefined>;
|
|
96
|
+
/** Events dropped as malformed since this instance was opened (v1 migration only). */
|
|
97
|
+
get skippedMalformedEventCount(): number;
|
|
83
98
|
set(key: string, value: Uint8Array): Promise<void>;
|
|
84
99
|
delete(key: string): Promise<boolean>;
|
|
85
100
|
list(): AsyncGenerator<string>;
|
|
@@ -88,6 +103,9 @@ export declare class AcePackedStore implements IAcePackedStore {
|
|
|
88
103
|
getBlob(key: string): Promise<string | undefined>;
|
|
89
104
|
setBlob(key: string, content: string): Promise<void>;
|
|
90
105
|
appendEntry(entry: Omit<UnifiedEntry, "id" | "ts">): Promise<UnifiedEntry>;
|
|
106
|
+
private readArchiveIndex;
|
|
107
|
+
private getArchivedEntries;
|
|
108
|
+
archiveColdEvents(options?: ArchiveColdEventsOptions): Promise<ArchiveColdEventsResult>;
|
|
91
109
|
getEntries(filter?: EntryFilter): Promise<UnifiedEntry[]>;
|
|
92
110
|
getEntry(_key: string): Promise<UnifiedEntry | undefined>;
|
|
93
111
|
getTopology(kind: string): Promise<TopologyEntry[]>;
|
|
@@ -106,4 +124,5 @@ export declare class AcePackedStore implements IAcePackedStore {
|
|
|
106
124
|
export declare function openStore(storePath: string, opts?: {
|
|
107
125
|
readOnly?: boolean;
|
|
108
126
|
}): Promise<AcePackedStore>;
|
|
127
|
+
export {};
|
|
109
128
|
//# sourceMappingURL=ace-packed-store.d.ts.map
|