@voybio/ace-swarm 2.4.3 → 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.
Files changed (144) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +72 -532
  3. package/assets/agent-state/INTERFACE_REGISTRY.md +2 -2
  4. package/assets/agent-state/MODULES/gates/gate-autonomy.json +4 -2
  5. package/assets/agent-state/MODULES/gates/gate-completeness.json +4 -2
  6. package/assets/agent-state/MODULES/gates/gate-operability.json +4 -2
  7. package/assets/agent-state/MODULES/gates/gate-security.json +3 -1
  8. package/assets/agent-state/MODULES/registry.json +1 -4
  9. package/assets/agent-state/MODULES/roles/capability-build.json +1 -2
  10. package/assets/agent-state/MODULES/roles/capability-eval.json +1 -1
  11. package/assets/agent-state/MODULES/roles/capability-qa.json +1 -2
  12. package/assets/agent-state/QUALITY_GATES.md +25 -9
  13. package/assets/agent-state/TEAL_CONFIG.md +2 -11
  14. package/assets/eval-battery/battery.json +85 -0
  15. package/assets/eval-battery/fixtures/edit-target.js +3 -0
  16. package/assets/scripts/render-mcp-configs.sh +12 -0
  17. package/dist/ace-context.d.ts +18 -0
  18. package/dist/ace-context.js +161 -16
  19. package/dist/ace-server-instructions.js +36 -9
  20. package/dist/astgrep-index.d.ts +7 -1
  21. package/dist/astgrep-index.js +66 -39
  22. package/dist/bridge-trace.d.ts +17 -0
  23. package/dist/bridge-trace.js +23 -0
  24. package/dist/cli.d.ts +9 -1
  25. package/dist/cli.js +619 -10
  26. package/dist/discovery-runtime-wrappers.d.ts +16 -1
  27. package/dist/discovery-runtime-wrappers.js +65 -9
  28. package/dist/eval-harness.d.ts +110 -0
  29. package/dist/eval-harness.js +254 -0
  30. package/dist/experience-reflector.d.ts +13 -0
  31. package/dist/experience-reflector.js +62 -0
  32. package/dist/gate-contract.d.ts +63 -0
  33. package/dist/gate-contract.js +178 -0
  34. package/dist/goal-engine.d.ts +155 -0
  35. package/dist/goal-engine.js +795 -0
  36. package/dist/helpers/store-resolution.js +40 -39
  37. package/dist/hermes/session-manager.d.ts +10 -0
  38. package/dist/hermes/session-manager.js +130 -27
  39. package/dist/job-scheduler.d.ts +58 -0
  40. package/dist/job-scheduler.js +202 -2
  41. package/dist/job-worker.d.ts +87 -0
  42. package/dist/job-worker.js +182 -0
  43. package/dist/local-model-policy.d.ts +8 -2
  44. package/dist/local-model-policy.js +31 -17
  45. package/dist/local-model-runtime.d.ts +6 -0
  46. package/dist/local-model-runtime.js +130 -2
  47. package/dist/measured-classing.d.ts +66 -0
  48. package/dist/measured-classing.js +165 -0
  49. package/dist/model-bridge.d.ts +45 -0
  50. package/dist/model-bridge.js +502 -44
  51. package/dist/orchestrator-supervisor.js +1 -1
  52. package/dist/plan-proposal.d.ts +7 -0
  53. package/dist/plan-proposal.js +74 -4
  54. package/dist/playbook-adaptation.d.ts +37 -0
  55. package/dist/playbook-adaptation.js +159 -0
  56. package/dist/playbook-curator.d.ts +16 -0
  57. package/dist/playbook-curator.js +106 -0
  58. package/dist/playbook-llm-reflector.d.ts +26 -0
  59. package/dist/playbook-llm-reflector.js +157 -0
  60. package/dist/public-surface.d.ts +2 -0
  61. package/dist/public-surface.js +26 -14
  62. package/dist/runtime-executor.js +45 -2
  63. package/dist/runtime-tool-specs.d.ts +8 -1
  64. package/dist/runtime-tool-specs.js +19 -1
  65. package/dist/safe-edit.d.ts +20 -0
  66. package/dist/safe-edit.js +355 -78
  67. package/dist/schemas.js +16 -15
  68. package/dist/server.d.ts +11 -2
  69. package/dist/server.js +55 -5
  70. package/dist/shared.d.ts +12 -0
  71. package/dist/shared.js +13 -1
  72. package/dist/source-mutation-coordinator.d.ts +54 -0
  73. package/dist/source-mutation-coordinator.js +259 -0
  74. package/dist/status-events.d.ts +4 -1
  75. package/dist/status-events.js +41 -6
  76. package/dist/store/ace-packed-store.d.ts +28 -9
  77. package/dist/store/ace-packed-store.js +324 -57
  78. package/dist/store/bgzf.d.ts +4 -0
  79. package/dist/store/bgzf.js +94 -0
  80. package/dist/store/bootstrap-store.d.ts +4 -1
  81. package/dist/store/bootstrap-store.js +87 -9
  82. package/dist/store/cache-workspace.d.ts +8 -0
  83. package/dist/store/cache-workspace.js +67 -1
  84. package/dist/store/crc32.d.ts +2 -0
  85. package/dist/store/crc32.js +16 -0
  86. package/dist/store/fsck.d.ts +16 -0
  87. package/dist/store/fsck.js +240 -0
  88. package/dist/store/gate-contract-migration.d.ts +10 -0
  89. package/dist/store/gate-contract-migration.js +413 -0
  90. package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
  91. package/dist/store/materializers/context-snapshot-materializer.js +4 -1
  92. package/dist/store/materializers/projection-manager.d.ts +7 -0
  93. package/dist/store/materializers/projection-manager.js +37 -0
  94. package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
  95. package/dist/store/repositories/goal-repository.d.ts +112 -0
  96. package/dist/store/repositories/goal-repository.js +157 -0
  97. package/dist/store/repositories/playbook-repository.d.ts +107 -0
  98. package/dist/store/repositories/playbook-repository.js +314 -0
  99. package/dist/store/store-artifacts.js +44 -2
  100. package/dist/store/store-authority-audit.js +1 -0
  101. package/dist/store/store-cli.d.ts +30 -0
  102. package/dist/store/store-cli.js +242 -0
  103. package/dist/store/store-snapshot.d.ts +12 -0
  104. package/dist/store/store-snapshot.js +74 -11
  105. package/dist/store/types.d.ts +1 -0
  106. package/dist/store/write-coordinator.d.ts +7 -0
  107. package/dist/store/write-coordinator.js +11 -0
  108. package/dist/todo-state.d.ts +21 -0
  109. package/dist/todo-state.js +150 -53
  110. package/dist/tools-agent.js +86 -22
  111. package/dist/tools-discovery.js +5 -3
  112. package/dist/tools-files.js +107 -23
  113. package/dist/tools-framework.js +146 -40
  114. package/dist/tools-goal.d.ts +11 -0
  115. package/dist/tools-goal.js +108 -0
  116. package/dist/tools-handoff.js +7 -11
  117. package/dist/tools-lifecycle.js +2 -0
  118. package/dist/tools-scheduler.js +44 -1
  119. package/dist/tools-skills.js +3 -2
  120. package/dist/tools-todo.js +64 -3
  121. package/dist/tools.js +2 -0
  122. package/dist/tui/chat.js +4 -0
  123. package/dist/tui/index.js +4 -1
  124. package/dist/tui/ollama.d.ts +2 -0
  125. package/dist/tui/openai-compatible.d.ts +14 -0
  126. package/dist/tui/openai-compatible.js +15 -2
  127. package/dist/tui/provider-discovery.d.ts +1 -0
  128. package/dist/tui/provider-discovery.js +2 -2
  129. package/docs/cli-reference.md +125 -0
  130. package/docs/development.md +46 -0
  131. package/docs/evaluation-safety.md +27 -0
  132. package/docs/experience-playbook.md +35 -0
  133. package/docs/hermes.md +129 -0
  134. package/docs/library-api.md +31 -0
  135. package/docs/mcp-tool-reference.md +290 -0
  136. package/docs/provider-security.md +85 -0
  137. package/docs/state-model.md +113 -0
  138. package/docs/troubleshooting.md +95 -0
  139. package/docs/tui-operator-guide.md +75 -0
  140. package/package.json +5 -1
  141. package/scripts/dev/run_hermes_crispr_live.mjs +212 -0
  142. package/assets/agent-state/MODULES/gates/gate-correctness.json +0 -7
  143. package/assets/agent-state/MODULES/gates/gate-evaluation.json +0 -7
  144. package/assets/agent-state/MODULES/gates/gate-typescript-public-surface.json +0 -7
package/dist/server.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
5
6
  import { buildServerInstructions, installAceToolGovernance, } from "./ace-server-instructions.js";
6
7
  import { resolveWorkspaceRoot } from "./helpers.js";
7
8
  import { openStore } from "./store/ace-packed-store.js";
@@ -11,7 +12,21 @@ import { registerResources } from "./resources.js";
11
12
  import { registerTools } from "./tools.js";
12
13
  import { backfillHandoffsIntoScheduler } from "./tools-handoff.js";
13
14
  export const ACE_MCP_SERVER_NAME = "ace-swarm";
14
- export const ACE_MCP_SERVER_VERSION = "0.2.1";
15
+ // Report the true package version to MCP clients instead of a stale hard-coded string.
16
+ function resolvePackageVersion() {
17
+ try {
18
+ // dist/server.js → package root is one level up from dist/.
19
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
20
+ const parsed = JSON.parse(readFileSync(pkgPath, "utf-8"));
21
+ if (typeof parsed.version === "string" && parsed.version.length > 0)
22
+ return parsed.version;
23
+ }
24
+ catch {
25
+ // fall through to the compiled fallback
26
+ }
27
+ return "0.0.0";
28
+ }
29
+ export const ACE_MCP_SERVER_VERSION = resolvePackageVersion();
15
30
  const HERMES_SHADOW_INSTRUCTIONS = "ACE filtered shadow MCP tool transport for Hermes-local execution. Only tools allowed by the active ACE capability snapshot are registered; public ACE prompts and resources are not exposed.";
16
31
  export function createAceServer(options = {}) {
17
32
  const workspaceRoot = resolveWorkspaceRoot();
@@ -77,7 +92,40 @@ async function verifyMcpStoreStartup(workspaceRoot, logStartup) {
77
92
  console.error(message);
78
93
  return false;
79
94
  }
80
- export async function startStdioServer(logStartup = true) {
95
+ /**
96
+ * Curated premium-host surface (report WS-G). ~15–25 high-value tools instead of the
97
+ * full tool surface, cutting the per-session schema tax. Everything omitted here remains reachable
98
+ * via `run_orchestrator` delegation or by starting with `--profile full`. Goal-engine
99
+ * tools (start_goal/…) are intentionally absent until WS-F ships.
100
+ */
101
+ export const HOST_MIN_TOOL_PROFILE = [
102
+ "recall_context",
103
+ "route_task",
104
+ "run_orchestrator",
105
+ "propose_plan",
106
+ "validate_plan",
107
+ "create_handoff",
108
+ "ack_handoff",
109
+ "get_handoff_status",
110
+ "execute_gates",
111
+ "emit_status_event",
112
+ "append_vericify_process_post",
113
+ "get_vericify_context",
114
+ "get_vericify_delta",
115
+ "astgrep_locate",
116
+ "compile_structural_edit",
117
+ "preview_structural_edit",
118
+ "astgrep_rewrite",
119
+ "safe_edit_file",
120
+ "run_tests",
121
+ "run_local_model",
122
+ "context_snapshot",
123
+ "build_continuity_packet",
124
+ ];
125
+ export function resolveHostProfileAllowlist(profile) {
126
+ return profile === "host-min" ? HOST_MIN_TOOL_PROFILE : undefined;
127
+ }
128
+ export async function startStdioServer(logStartup = true, profile = "full") {
81
129
  const workspaceRoot = resolveWorkspaceRoot();
82
130
  if (!(await verifyMcpStoreStartup(workspaceRoot, logStartup))) {
83
131
  return;
@@ -93,11 +141,13 @@ export async function startStdioServer(logStartup = true) {
93
141
  console.error(`Scheduler backfill warning: ${error instanceof Error ? error.message : String(error)}`);
94
142
  }
95
143
  }
96
- const server = createAceServer();
144
+ const toolAllowlist = resolveHostProfileAllowlist(profile);
145
+ const server = createAceServer({ toolAllowlist });
97
146
  const transport = new StdioServerTransport();
98
147
  await server.connect(transport);
99
148
  if (logStartup) {
100
- console.error(`ACE MCP Server (${ACE_MCP_SERVER_NAME}) running on stdio`);
149
+ console.error(`ACE MCP Server (${ACE_MCP_SERVER_NAME}) running on stdio` +
150
+ (toolAllowlist ? ` (profile=${profile}, ${toolAllowlist.length} tools)` : ` (profile=${profile})`));
101
151
  }
102
152
  }
103
153
  export async function startHermesShadowStdioServer(toolAllowlist, logStartup = false) {
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
@@ -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
  }>;
@@ -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
- * Events grow in the columnar section indefinitely (Option A). compact() only
6
- * reclaims dead space in the KV chunk region — it does NOT reset or archive
7
- * events. Historical event replay across all past runs is always available.
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() — they accumulate for the workspace
49
- * lifetime. See HOT_COLD_EVENT_TIERING.md for a future branch that adds
50
- * BGZF batch archiving for very long-lived workspaces.
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 NOT reset or archived; they accumulate in the columnar section
77
- * for the workspace lifetime (Option A — grow forever).
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