@voybio/ace-swarm 0.2.4 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +20 -13
  3. package/assets/.agents/skills/eval-harness/SKILL.md +14 -0
  4. package/assets/.agents/skills/handoff-lint/SKILL.md +14 -0
  5. package/assets/.agents/skills/incident-commander/SKILL.md +14 -0
  6. package/assets/.agents/skills/memory-curator/SKILL.md +14 -0
  7. package/assets/.agents/skills/release-sentry/SKILL.md +14 -0
  8. package/assets/.agents/skills/risk-quant/SKILL.md +14 -0
  9. package/assets/.agents/skills/schema-forge/SKILL.md +14 -0
  10. package/assets/.agents/skills/state-auditor/SKILL.md +14 -0
  11. package/assets/agent-state/EVIDENCE_LOG.md +1 -1
  12. package/assets/agent-state/MODULES/gates/gate-correctness.json +1 -1
  13. package/assets/agent-state/MODULES/roles/capability-framework.json +41 -0
  14. package/assets/agent-state/MODULES/roles/capability-git.json +33 -0
  15. package/assets/agent-state/MODULES/roles/capability-safety.json +37 -0
  16. package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +21 -0
  17. package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +43 -0
  18. package/assets/agent-state/MODULES/schemas/WORKSPACE_SESSION_REGISTRY.schema.json +11 -0
  19. package/assets/agent-state/STATUS.md +2 -2
  20. package/assets/scripts/ace-hook-dispatch.mjs +70 -6
  21. package/assets/scripts/render-mcp-configs.sh +19 -5
  22. package/dist/ace-context.js +22 -1
  23. package/dist/ace-server-instructions.js +3 -3
  24. package/dist/ace-state-resolver.js +5 -3
  25. package/dist/astgrep-index.d.ts +9 -1
  26. package/dist/astgrep-index.js +14 -3
  27. package/dist/cli.js +52 -20
  28. package/dist/handoff-registry.js +5 -5
  29. package/dist/helpers/artifacts.d.ts +19 -0
  30. package/dist/helpers/artifacts.js +152 -0
  31. package/dist/helpers/bootstrap.d.ts +24 -0
  32. package/dist/helpers/bootstrap.js +894 -0
  33. package/dist/helpers/constants.d.ts +53 -0
  34. package/dist/helpers/constants.js +288 -0
  35. package/dist/helpers/drift.d.ts +13 -0
  36. package/dist/helpers/drift.js +45 -0
  37. package/dist/helpers/path-utils.d.ts +17 -0
  38. package/dist/helpers/path-utils.js +104 -0
  39. package/dist/helpers/store-resolution.d.ts +19 -0
  40. package/dist/helpers/store-resolution.js +301 -0
  41. package/dist/helpers/workspace-root.d.ts +3 -0
  42. package/dist/helpers/workspace-root.js +80 -0
  43. package/dist/helpers.d.ts +8 -123
  44. package/dist/helpers.js +8 -1747
  45. package/dist/job-scheduler.js +3 -3
  46. package/dist/local-model-runtime.js +12 -1
  47. package/dist/model-bridge.d.ts +7 -0
  48. package/dist/model-bridge.js +75 -5
  49. package/dist/orchestrator-supervisor.d.ts +14 -0
  50. package/dist/orchestrator-supervisor.js +72 -1
  51. package/dist/run-ledger.js +3 -3
  52. package/dist/runtime-command.d.ts +8 -0
  53. package/dist/runtime-command.js +38 -6
  54. package/dist/runtime-executor.d.ts +14 -0
  55. package/dist/runtime-executor.js +669 -171
  56. package/dist/runtime-profile.d.ts +32 -0
  57. package/dist/runtime-profile.js +89 -13
  58. package/dist/runtime-tool-specs.d.ts +21 -0
  59. package/dist/runtime-tool-specs.js +78 -3
  60. package/dist/safe-edit.d.ts +7 -0
  61. package/dist/safe-edit.js +163 -37
  62. package/dist/schemas.js +19 -0
  63. package/dist/shared.d.ts +2 -2
  64. package/dist/status-events.js +9 -6
  65. package/dist/store/ace-packed-store.d.ts +3 -2
  66. package/dist/store/ace-packed-store.js +188 -110
  67. package/dist/store/bootstrap-store.d.ts +1 -1
  68. package/dist/store/bootstrap-store.js +94 -81
  69. package/dist/store/cache-workspace.d.ts +22 -0
  70. package/dist/store/cache-workspace.js +149 -0
  71. package/dist/store/materializers/context-snapshot-materializer.js +6 -7
  72. package/dist/store/materializers/hook-context-materializer.d.ts +6 -9
  73. package/dist/store/materializers/hook-context-materializer.js +11 -21
  74. package/dist/store/materializers/host-file-materializer.js +6 -0
  75. package/dist/store/materializers/projection-manager.d.ts +0 -1
  76. package/dist/store/materializers/projection-manager.js +5 -13
  77. package/dist/store/materializers/scheduler-projection-materializer.js +1 -1
  78. package/dist/store/materializers/vericify-projector.d.ts +7 -7
  79. package/dist/store/materializers/vericify-projector.js +11 -11
  80. package/dist/store/repositories/local-model-runtime-repository.d.ts +120 -3
  81. package/dist/store/repositories/local-model-runtime-repository.js +242 -6
  82. package/dist/store/skills-install.d.ts +4 -0
  83. package/dist/store/skills-install.js +21 -12
  84. package/dist/store/state-reader.d.ts +2 -0
  85. package/dist/store/state-reader.js +20 -0
  86. package/dist/store/store-artifacts.d.ts +7 -0
  87. package/dist/store/store-artifacts.js +27 -1
  88. package/dist/store/store-authority-audit.d.ts +18 -1
  89. package/dist/store/store-authority-audit.js +115 -5
  90. package/dist/store/store-snapshot.d.ts +3 -0
  91. package/dist/store/store-snapshot.js +22 -2
  92. package/dist/store/workspace-store-paths.d.ts +39 -0
  93. package/dist/store/workspace-store-paths.js +94 -0
  94. package/dist/store/write-coordinator.d.ts +65 -0
  95. package/dist/store/write-coordinator.js +386 -0
  96. package/dist/todo-state.js +5 -5
  97. package/dist/tools-agent.js +319 -34
  98. package/dist/tools-discovery.js +1 -1
  99. package/dist/tools-files.d.ts +7 -0
  100. package/dist/tools-files.js +299 -10
  101. package/dist/tools-framework.js +107 -27
  102. package/dist/tools-handoff.js +2 -2
  103. package/dist/tools-lifecycle.js +4 -4
  104. package/dist/tools-memory.js +6 -6
  105. package/dist/tools-todo.js +2 -2
  106. package/dist/tracker-adapters.d.ts +1 -1
  107. package/dist/tracker-adapters.js +13 -18
  108. package/dist/tracker-sync.js +5 -3
  109. package/dist/tui/agent-runner.js +3 -1
  110. package/dist/tui/chat.js +103 -7
  111. package/dist/tui/dashboard.d.ts +1 -0
  112. package/dist/tui/dashboard.js +43 -0
  113. package/dist/tui/layout.d.ts +20 -0
  114. package/dist/tui/layout.js +31 -1
  115. package/dist/tui/local-model-contract.d.ts +6 -2
  116. package/dist/tui/local-model-contract.js +16 -3
  117. package/dist/vericify-bridge.d.ts +5 -0
  118. package/dist/vericify-bridge.js +27 -3
  119. package/dist/workspace-manager.d.ts +30 -3
  120. package/dist/workspace-manager.js +257 -27
  121. package/package.json +1 -2
  122. package/dist/internal-tool-runtime.d.ts +0 -21
  123. package/dist/internal-tool-runtime.js +0 -136
  124. package/dist/store/workspace-snapshot.d.ts +0 -26
  125. package/dist/store/workspace-snapshot.js +0 -107
package/dist/schemas.js CHANGED
@@ -186,6 +186,8 @@ const workspaceSessionRecordSchema = z
186
186
  last_error: z.string().optional(),
187
187
  created_at: z.string().datetime({ offset: true }),
188
188
  updated_at: z.string().datetime({ offset: true }),
189
+ hook_health: z.enum(["ok", "degraded", "failed"]).optional(),
190
+ hook_summary: z.string().optional(),
189
191
  hooks: z
190
192
  .object({
191
193
  after_create: workspaceHookStateSchema,
@@ -352,6 +354,15 @@ const unattendedTurnRecordSchema = z
352
354
  stdout: z.string(),
353
355
  stderr: z.string(),
354
356
  tool_calls: z.array(unattendedToolCallRecordSchema),
357
+ turn_outcome: z.enum(["no_op_success", "meaningful_completion", "escalation_blocker"]).optional(),
358
+ outcome_reason: z.string().optional(),
359
+ })
360
+ .strict();
361
+ const runtimeOutputPolicySchema = z
362
+ .object({
363
+ emit_to: z.array(z.enum(["tui", "tracker", "handoff", "vericify"])),
364
+ silent_unless_blocked: z.boolean(),
365
+ require_approval_before_emit: z.boolean(),
355
366
  })
356
367
  .strict();
357
368
  const runtimeExecutorSessionRecordSchema = z
@@ -377,6 +388,7 @@ const runtimeExecutorSessionRecordSchema = z
377
388
  last_error: z.string().optional(),
378
389
  cleanup_error: z.string().optional(),
379
390
  workspace_cleanup_status: z.enum(["pending", "removed", "archived", "failed"]),
391
+ output_policy: runtimeOutputPolicySchema.optional(),
380
392
  turns: z.array(unattendedTurnRecordSchema),
381
393
  })
382
394
  .strict();
@@ -453,6 +465,13 @@ const vericifyBridgeSnapshotSchema = z
453
465
  })
454
466
  .strict(),
455
467
  active_run_refs: z.array(vericifyBridgeActiveRunRefSchema),
468
+ ace_runtime_enrichment: z
469
+ .object({
470
+ live_session_id: z.string().optional(),
471
+ last_turn_outcome: z.string().optional(),
472
+ last_turn_outcome_reason: z.string().optional(),
473
+ })
474
+ .optional(),
456
475
  })
457
476
  .strict();
458
477
  /**
package/dist/shared.d.ts CHANGED
@@ -19,10 +19,10 @@ export declare function looksLikeSwarmHandoffPath(path: string): boolean;
19
19
  export declare const ROLE_TITLES: Record<string, string>;
20
20
  export declare function getRoleTitle(role: string): string;
21
21
  export declare const MCP_CLIENT_ENUM: z.ZodEnum<{
22
- claude: "claude";
23
- cursor: "cursor";
24
22
  codex: "codex";
25
23
  vscode: "vscode";
24
+ claude: "claude";
25
+ cursor: "cursor";
26
26
  antigravity: "antigravity";
27
27
  }>;
28
28
  export declare const ROLE_ENUM: z.ZodEnum<{
@@ -8,7 +8,7 @@ import { ProjectionManager } from "./store/materializers/projection-manager.js";
8
8
  import { TrackerRepository } from "./store/repositories/tracker-repository.js";
9
9
  import { getWorkspaceStorePath, listStoreKeysSync, readStoreJsonSync, storeExistsSync, } from "./store/store-snapshot.js";
10
10
  import { operationalArtifactVirtualPath } from "./store/store-artifacts.js";
11
- import { withStoreWriteQueue } from "./store/write-queue.js";
11
+ import { withStoreWriteCoordinator } from "./store/write-coordinator.js";
12
12
  export const STATUS_EVENTS_REL_PATH = "agent-state/STATUS_EVENTS.ndjson";
13
13
  const STATUS_EVENTS_ARCHIVE_REL = "agent-state/STATUS_EVENTS-archive.ndjson";
14
14
  const MAX_EVENT_LINES = 2000;
@@ -94,7 +94,7 @@ async function mirrorStatusEventToStore(root, event) {
94
94
  const storePath = getWorkspaceStorePath(root);
95
95
  if (!existsSync(storePath))
96
96
  return;
97
- await withStoreWriteQueue(storePath, async () => {
97
+ await withStoreWriteCoordinator(storePath, async () => {
98
98
  const store = await openStore(storePath);
99
99
  try {
100
100
  const tracker = new TrackerRepository(store);
@@ -120,7 +120,7 @@ async function mirrorStatusEventToStore(root, event) {
120
120
  finally {
121
121
  await store.close();
122
122
  }
123
- });
123
+ }, { operation_label: "mirrorStatusEventToStore" });
124
124
  }
125
125
  function scheduleStatusEventMirror(event) {
126
126
  const root = workspaceRoot();
@@ -135,7 +135,7 @@ export async function waitForPendingStatusEventMirrors() {
135
135
  }
136
136
  async function appendStatusEventStoreBacked(root, event) {
137
137
  const storePath = getWorkspaceStorePath(root);
138
- return withStoreWriteQueue(storePath, async () => {
138
+ return withStoreWriteCoordinator(storePath, async () => {
139
139
  const store = await openStore(storePath);
140
140
  try {
141
141
  const tracker = new TrackerRepository(store);
@@ -165,9 +165,10 @@ async function appendStatusEventStoreBacked(root, event) {
165
165
  finally {
166
166
  await store.close();
167
167
  }
168
- });
168
+ }, { operation_label: "appendStatusEventStoreBacked" });
169
169
  }
170
170
  export function appendStatusEvent(input) {
171
+ const root = workspaceRoot();
171
172
  const event = buildStatusEvent(input);
172
173
  validateStatusEvent(event);
173
174
  // Atomic append under file lock to prevent lost writes under parallelism
@@ -176,7 +177,9 @@ export function appendStatusEvent(input) {
176
177
  const combined = existing.length > 0 ? `${existing}\n${line}\n` : `${line}\n`;
177
178
  const next = rotateIfNeeded(combined);
178
179
  const path = safeWriteWorkspaceFile(STATUS_EVENTS_REL_PATH, next);
179
- scheduleStatusEventMirror(event);
180
+ if (storeExistsSync(root)) {
181
+ scheduleStatusEventMirror(event);
182
+ }
180
183
  return { path, event };
181
184
  }
182
185
  /**
@@ -59,6 +59,9 @@ export declare class AcePackedStore implements IAcePackedStore {
59
59
  private fh;
60
60
  private kvIndex;
61
61
  private kvChunkEnd;
62
+ private pendingKv;
63
+ private pendingDeletes;
64
+ private dirty;
62
65
  private committed;
63
66
  private pending;
64
67
  private evtBaseId;
@@ -67,8 +70,6 @@ export declare class AcePackedStore implements IAcePackedStore {
67
70
  }): Promise<void>;
68
71
  private _initNew;
69
72
  private _loadExisting;
70
- /** Read a KV blob directly from a loaded file buffer (used during migration). */
71
- private _readKvBlobDirect;
72
73
  commit(): Promise<void>;
73
74
  /**
74
75
  * Compacts the KV chunk region — removes dead space left by overwritten keys.
@@ -56,6 +56,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "
56
56
  import { open as fsOpen } from "node:fs/promises";
57
57
  import { dirname } from "node:path";
58
58
  import { MAGIC, STORE_VERSION, HEADER_SIZE, HEADER_SIZE_V1, ContentSource, EntityKind, } from "./types.js";
59
+ import { invalidateStoreSnapshotCache } from "./store-snapshot.js";
59
60
  // ── Constants ─────────────────────────────────────────────────────────────────
60
61
  // Header field byte offsets (big-endian throughout)
61
62
  const H_KV_IDX_OFF = 16; // uint64: KV index offset
@@ -204,6 +205,69 @@ function deserializeEventSection(buf) {
204
205
  blob: DECODE.decode(buf.slice(poolStart + payOffs[i], poolStart + payOffs[i] + payLens[i])),
205
206
  }));
206
207
  }
208
+ function readKvBlobFromBytes(file, index, key) {
209
+ const entry = index.get(key);
210
+ if (!entry)
211
+ return undefined;
212
+ const start = entry.offset + 4;
213
+ const end = start + entry.length;
214
+ if (start < 0 || end > file.length) {
215
+ throw new Error(`AcePackedStore: KV entry for '${key}' exceeds file bounds`);
216
+ }
217
+ return file.slice(start, end);
218
+ }
219
+ function loadCommittedStateFromBytes(file) {
220
+ if (file.length < HEADER_SIZE_V1)
221
+ throw new Error("AcePackedStore: file too short");
222
+ const hdr = readHeaderV2(file.slice(0, Math.max(HEADER_SIZE, file.length)));
223
+ const isV1 = hdr.version < 2;
224
+ const idxRaw = file.slice(hdr.kvIndexOffset, hdr.kvIndexOffset + hdr.kvIndexLength);
225
+ const idxObj = JSON.parse(DECODE.decode(idxRaw));
226
+ const kvIndex = new Map(Object.entries(idxObj));
227
+ if (isV1) {
228
+ const logKeys = [...kvIndex.keys()]
229
+ .filter((key) => key.startsWith("core/log/") && key !== "core/log/__seq")
230
+ .sort();
231
+ const committed = [];
232
+ for (const key of logKeys) {
233
+ const entry = readKvBlobFromBytes(file, kvIndex, key);
234
+ if (!entry)
235
+ continue;
236
+ try {
237
+ const parsed = JSON.parse(DECODE.decode(entry));
238
+ committed.push({
239
+ ts: parsed.ts,
240
+ kind: parsed.kind,
241
+ source: parsed.content_source,
242
+ flags: parsed.flags ?? 0,
243
+ blob: JSON.stringify({ key: parsed.key, payload: parsed.payload, parent_id: parsed.parent_id }),
244
+ });
245
+ }
246
+ catch {
247
+ // Skip malformed legacy events during migration.
248
+ }
249
+ }
250
+ for (const key of [...kvIndex.keys()]) {
251
+ if (key.startsWith("core/log/") || key === "meta/log_entry_seq") {
252
+ kvIndex.delete(key);
253
+ }
254
+ }
255
+ return {
256
+ kvIndex,
257
+ kvChunkEnd: hdr.kvChunkEnd,
258
+ committed,
259
+ evtBaseId: 0,
260
+ };
261
+ }
262
+ return {
263
+ kvIndex,
264
+ kvChunkEnd: hdr.kvChunkEnd,
265
+ committed: hdr.evtLength > 0
266
+ ? deserializeEventSection(file.slice(hdr.evtOffset, hdr.evtOffset + hdr.evtLength))
267
+ : [],
268
+ evtBaseId: hdr.evtBaseId,
269
+ };
270
+ }
207
271
  // ── File I/O helpers ──────────────────────────────────────────────────────────
208
272
  async function readExact(fh, buf, position) {
209
273
  let off = 0;
@@ -214,15 +278,6 @@ async function readExact(fh, buf, position) {
214
278
  off += bytesRead;
215
279
  }
216
280
  }
217
- async function writeExact(fh, data, position) {
218
- let off = 0;
219
- while (off < data.length) {
220
- const { bytesWritten } = await fh.write(data, off, data.length - off, position + off);
221
- if (bytesWritten <= 0)
222
- throw new Error("AcePackedStore: write failed");
223
- off += bytesWritten;
224
- }
225
- }
226
281
  // ── AcePackedStore ────────────────────────────────────────────────────────────
227
282
  export class AcePackedStore {
228
283
  storePath = "";
@@ -231,6 +286,9 @@ export class AcePackedStore {
231
286
  // KV region
232
287
  kvIndex = new Map();
233
288
  kvChunkEnd = HEADER_SIZE; // end of KV chunk region in file
289
+ pendingKv = new Map();
290
+ pendingDeletes = new Set();
291
+ dirty = false;
234
292
  // Event log (in-memory)
235
293
  committed = []; // loaded from file
236
294
  pending = []; // written since last commit
@@ -276,101 +334,90 @@ export class AcePackedStore {
276
334
  this.committed = [];
277
335
  this.pending = [];
278
336
  this.evtBaseId = 0;
337
+ this.pendingKv.clear();
338
+ this.pendingDeletes.clear();
339
+ this.dirty = false;
279
340
  }
280
341
  async _loadExisting(path) {
281
342
  const file = readFileSync(path);
282
- if (file.length < HEADER_SIZE_V1)
283
- throw new Error("AcePackedStore: file too short");
284
- const hdr = readHeaderV2(file.slice(0, Math.max(HEADER_SIZE, file.length)));
285
- const isV1 = hdr.version < 2;
286
- // Load KV index
287
- const idxRaw = file.slice(hdr.kvIndexOffset, hdr.kvIndexOffset + hdr.kvIndexLength);
288
- const idxObj = JSON.parse(DECODE.decode(idxRaw));
289
- this.kvIndex = new Map(Object.entries(idxObj));
290
- this.kvChunkEnd = hdr.kvChunkEnd;
291
- if (isV1) {
292
- // v1 → v2 migration: pull events from core/log/ KV entries
293
- const logKeys = [...this.kvIndex.keys()]
294
- .filter(k => k.startsWith("core/log/") && k !== "core/log/__seq")
295
- .sort();
296
- this.committed = [];
297
- for (const lk of logKeys) {
298
- const entry = await this._readKvBlobDirect(file, lk);
299
- if (!entry)
300
- continue;
301
- try {
302
- const e = JSON.parse(entry);
303
- this.committed.push({
304
- ts: e.ts,
305
- kind: e.kind,
306
- source: e.content_source,
307
- flags: e.flags ?? 0,
308
- blob: JSON.stringify({ key: e.key, payload: e.payload, parent_id: e.parent_id }),
309
- });
310
- }
311
- catch { /* skip malformed */ }
312
- }
313
- // Remove core/log/ and meta/log_entry_seq from KV (they go to columnar section)
314
- for (const k of [...this.kvIndex.keys()]) {
315
- if (k.startsWith("core/log/") || k === "meta/log_entry_seq")
316
- this.kvIndex.delete(k);
317
- }
318
- this.evtBaseId = 0;
319
- // force commit to write v2 format (pending is non-empty from migration)
320
- }
321
- else {
322
- // v2: load event section
323
- if (hdr.evtLength > 0) {
324
- const evtBuf = file.slice(hdr.evtOffset, hdr.evtOffset + hdr.evtLength);
325
- this.committed = deserializeEventSection(evtBuf);
326
- }
327
- else {
328
- this.committed = [];
329
- }
330
- this.evtBaseId = hdr.evtBaseId;
331
- }
343
+ const loaded = loadCommittedStateFromBytes(file);
344
+ this.kvIndex = loaded.kvIndex;
345
+ this.kvChunkEnd = loaded.kvChunkEnd;
346
+ this.committed = loaded.committed;
347
+ this.evtBaseId = loaded.evtBaseId;
332
348
  this.pending = [];
333
- }
334
- /** Read a KV blob directly from a loaded file buffer (used during migration). */
335
- async _readKvBlobDirect(file, key) {
336
- const entry = this.kvIndex.get(key);
337
- if (!entry)
338
- return undefined;
339
- const start = entry.offset + 4;
340
- const end = start + entry.length;
341
- if (end > file.length)
342
- return undefined;
343
- return DECODE.decode(file.slice(start, end));
349
+ this.pendingKv.clear();
350
+ this.pendingDeletes.clear();
351
+ this.dirty = false;
344
352
  }
345
353
  async commit() {
346
- if (this.readOnly || !this.fh)
354
+ if (this.readOnly || !this.fh || !this.storePath || !this.dirty)
347
355
  return;
348
- // Merge pending into committed
349
- this.committed.push(...this.pending);
350
- this.pending = [];
351
- // Serialize event section
352
- const evtBytes = serializeEventSection(this.committed);
353
- // Write KV index after event section
354
- const idxJson = JSON.stringify(Object.fromEntries(this.kvIndex));
356
+ const currentFile = readFileSync(this.storePath);
357
+ const latest = loadCommittedStateFromBytes(new Uint8Array(currentFile.buffer, currentFile.byteOffset, currentFile.byteLength));
358
+ const nextCommitted = [...latest.committed, ...this.pending];
359
+ const evtBytes = serializeEventSection(nextCommitted);
360
+ const nextKvIndex = new Map();
361
+ const liveChunks = [];
362
+ let writeOff = HEADER_SIZE;
363
+ for (const [key] of latest.kvIndex) {
364
+ if (this.pendingDeletes.has(key) || this.pendingKv.has(key))
365
+ continue;
366
+ const value = readKvBlobFromBytes(currentFile, latest.kvIndex, key);
367
+ if (!value)
368
+ continue;
369
+ liveChunks.push({ value });
370
+ nextKvIndex.set(key, { offset: writeOff, length: value.length });
371
+ writeOff += 4 + value.length;
372
+ }
373
+ for (const [key, value] of this.pendingKv) {
374
+ const blob = value.slice();
375
+ liveChunks.push({ value: blob });
376
+ nextKvIndex.set(key, { offset: writeOff, length: blob.length });
377
+ writeOff += 4 + blob.length;
378
+ }
379
+ const idxJson = JSON.stringify(Object.fromEntries(nextKvIndex));
355
380
  const idxBytes = TEXT.encode(idxJson);
356
- const evtOff = this.kvChunkEnd;
381
+ const evtOff = writeOff;
357
382
  const kvIdxOff = evtOff + evtBytes.length;
358
- await writeExact(this.fh, evtBytes, evtOff);
359
- await writeExact(this.fh, idxBytes, kvIdxOff);
360
- await this.fh.truncate(kvIdxOff + idxBytes.length);
361
- // Update header
383
+ const totalSize = kvIdxOff + idxBytes.length;
362
384
  const header = new Uint8Array(HEADER_SIZE);
363
385
  writeHeaderV2(header, {
364
386
  kvIndexOffset: kvIdxOff,
365
387
  kvIndexLength: idxBytes.length,
366
- kvChunkEnd: this.kvChunkEnd,
388
+ kvChunkEnd: writeOff,
367
389
  evtOffset: evtOff,
368
390
  evtLength: evtBytes.length,
369
- evtCount: this.committed.length,
370
- evtBaseId: this.evtBaseId,
391
+ evtCount: nextCommitted.length,
392
+ evtBaseId: latest.evtBaseId,
371
393
  });
372
- await writeExact(this.fh, header, 0);
373
394
  await this.fh.datasync();
395
+ const out = new Uint8Array(totalSize);
396
+ out.set(header, 0);
397
+ let cursor = HEADER_SIZE;
398
+ for (const chunk of liveChunks) {
399
+ out.set(u32be(chunk.value.length), cursor);
400
+ out.set(chunk.value, cursor + 4);
401
+ cursor += 4 + chunk.value.length;
402
+ }
403
+ out.set(evtBytes, evtOff);
404
+ out.set(idxBytes, kvIdxOff);
405
+ const tmpPath = `${this.storePath}.${process.pid}.${Date.now()}.commit.tmp`;
406
+ writeFileSync(tmpPath, out);
407
+ readHeaderV2(readFileSync(tmpPath).slice(0, HEADER_SIZE));
408
+ await this.fh.close();
409
+ this.fh = null;
410
+ renameSync(tmpPath, this.storePath);
411
+ invalidateStoreSnapshotCache(this.storePath);
412
+ this.fh = await fsOpen(this.storePath, "r+");
413
+ this.kvIndex = nextKvIndex;
414
+ this.kvChunkEnd = writeOff;
415
+ this.committed = nextCommitted;
416
+ this.pending = [];
417
+ this.pendingKv.clear();
418
+ this.pendingDeletes.clear();
419
+ this.evtBaseId = latest.evtBaseId;
420
+ this.dirty = false;
374
421
  }
375
422
  /**
376
423
  * Compacts the KV chunk region — removes dead space left by overwritten keys.
@@ -381,23 +428,29 @@ export class AcePackedStore {
381
428
  async compact() {
382
429
  if (!this.storePath || this.readOnly)
383
430
  return;
384
- // Flush pending events into committed so they survive the rewrite
385
- this.committed.push(...this.pending);
386
- this.pending = [];
431
+ if (this.dirty) {
432
+ await this.commit();
433
+ }
387
434
  // Rebuild KV chunk region with only live keys (removes dead space)
388
435
  const srcFile = readFileSync(this.storePath);
436
+ const latest = loadCommittedStateFromBytes(new Uint8Array(srcFile.buffer, srcFile.byteOffset, srcFile.byteLength));
389
437
  const liveKv = new Map();
390
438
  const chunks = [];
391
439
  let writeOff = HEADER_SIZE;
392
- for (const [key, { offset, length }] of this.kvIndex) {
393
- chunks.push(srcFile.slice(offset + 4, offset + 4 + length));
394
- liveKv.set(key, { offset: writeOff, length });
395
- writeOff += 4 + length;
440
+ for (const [key] of latest.kvIndex) {
441
+ const chunk = readKvBlobFromBytes(srcFile, latest.kvIndex, key);
442
+ if (!chunk)
443
+ continue;
444
+ chunks.push(chunk);
445
+ liveKv.set(key, { offset: writeOff, length: chunk.length });
446
+ writeOff += 4 + chunk.length;
396
447
  }
397
448
  this.kvIndex = liveKv;
398
449
  this.kvChunkEnd = writeOff;
450
+ this.committed = latest.committed;
451
+ this.evtBaseId = latest.evtBaseId;
399
452
  // Serialize events (preserved in full) + new KV index
400
- const evtBytes = serializeEventSection(this.committed);
453
+ const evtBytes = serializeEventSection(latest.committed);
401
454
  const idxBytes = TEXT.encode(JSON.stringify(Object.fromEntries(liveKv)));
402
455
  const evtOff = writeOff;
403
456
  const kvIdxOff = evtOff + evtBytes.length;
@@ -410,8 +463,8 @@ export class AcePackedStore {
410
463
  kvChunkEnd: writeOff,
411
464
  evtOffset: evtOff,
412
465
  evtLength: evtBytes.length,
413
- evtCount: this.committed.length,
414
- evtBaseId: this.evtBaseId,
466
+ evtCount: latest.committed.length,
467
+ evtBaseId: latest.evtBaseId,
415
468
  });
416
469
  out.set(header, 0);
417
470
  let pos = HEADER_SIZE;
@@ -431,10 +484,17 @@ export class AcePackedStore {
431
484
  this.fh = null;
432
485
  }
433
486
  renameSync(tmpPath, this.storePath);
487
+ invalidateStoreSnapshotCache(this.storePath);
434
488
  this.fh = await fsOpen(this.storePath, "r+");
489
+ this.pending = [];
490
+ this.pendingKv.clear();
491
+ this.pendingDeletes.clear();
492
+ this.dirty = false;
435
493
  }
436
494
  async close() {
437
- await this.commit();
495
+ if (this.dirty) {
496
+ await this.commit();
497
+ }
438
498
  if (this.fh) {
439
499
  await this.fh.close();
440
500
  this.fh = null;
@@ -442,6 +502,12 @@ export class AcePackedStore {
442
502
  }
443
503
  // ── Core KV ─────────────────────────────────────────────────────────────────
444
504
  async get(key) {
505
+ if (this.pendingKv.has(key)) {
506
+ return this.pendingKv.get(key)?.slice();
507
+ }
508
+ if (this.pendingDeletes.has(key)) {
509
+ return undefined;
510
+ }
445
511
  const entry = this.kvIndex.get(key);
446
512
  if (!entry || !this.fh)
447
513
  return undefined;
@@ -454,19 +520,26 @@ export class AcePackedStore {
454
520
  throw new Error("AcePackedStore: read-only");
455
521
  if (!this.fh)
456
522
  throw new Error("AcePackedStore: not open");
457
- // Append chunk to KV chunk region (at kvChunkEnd)
458
- await writeExact(this.fh, u32be(value.length), this.kvChunkEnd);
459
- await writeExact(this.fh, value, this.kvChunkEnd + 4);
460
- this.kvIndex.set(key, { offset: this.kvChunkEnd, length: value.length });
461
- this.kvChunkEnd += 4 + value.length;
523
+ this.pendingDeletes.delete(key);
524
+ this.pendingKv.set(key, value.slice());
525
+ this.dirty = true;
462
526
  }
463
527
  async delete(key) {
464
- const existed = this.kvIndex.has(key);
465
- this.kvIndex.delete(key);
528
+ const existed = this.pendingKv.has(key) || this.kvIndex.has(key);
529
+ this.pendingKv.delete(key);
530
+ if (this.kvIndex.has(key)) {
531
+ this.pendingDeletes.add(key);
532
+ }
533
+ this.dirty = this.dirty || existed;
466
534
  return existed;
467
535
  }
468
536
  async *list() {
469
- for (const key of this.kvIndex.keys())
537
+ const keys = new Set(this.kvIndex.keys());
538
+ for (const key of this.pendingDeletes)
539
+ keys.delete(key);
540
+ for (const key of this.pendingKv.keys())
541
+ keys.add(key);
542
+ for (const key of keys)
470
543
  yield key;
471
544
  }
472
545
  // ── JSON / Blob convenience ──────────────────────────────────────────────────
@@ -503,6 +576,7 @@ export class AcePackedStore {
503
576
  blob: JSON.stringify({ key: entry.key, payload: entry.payload, parent_id: entry.parent_id }),
504
577
  };
505
578
  this.pending.push(rec);
579
+ this.dirty = true;
506
580
  return { id, ts, kind: entry.kind, content_source: entry.content_source, key: entry.key, payload: entry.payload, flags: entry.flags };
507
581
  }
508
582
  async getEntries(filter) {
@@ -569,7 +643,7 @@ export class AcePackedStore {
569
643
  }
570
644
  async listAgents() {
571
645
  const agents = new Set();
572
- for (const k of this.kvIndex.keys()) {
646
+ for await (const k of this.list()) {
573
647
  if (k.startsWith("knowledge/agents/")) {
574
648
  const p = k.split("/");
575
649
  if (p[2])
@@ -589,7 +663,7 @@ export class AcePackedStore {
589
663
  }
590
664
  async listSkills() {
591
665
  const skills = new Set();
592
- for (const k of this.kvIndex.keys()) {
666
+ for await (const k of this.list()) {
593
667
  if (k.startsWith("knowledge/skills/")) {
594
668
  const p = k.split("/");
595
669
  if (p[2])
@@ -601,10 +675,14 @@ export class AcePackedStore {
601
675
  // ── Introspection ─────────────────────────────────────────────────────────────
602
676
  /** Dead space ratio in the KV chunk region. Used to decide if compaction is needed. */
603
677
  get deadSpaceRatio() {
604
- const totalKv = this.kvChunkEnd - HEADER_SIZE;
678
+ const pendingBytes = [...this.pendingKv.values()].reduce((sum, value) => sum + 4 + value.length, 0);
679
+ const totalKv = this.kvChunkEnd - HEADER_SIZE + pendingBytes;
605
680
  if (totalKv <= 0)
606
681
  return 0;
607
- const liveKv = [...this.kvIndex.values()].reduce((s, e) => s + 4 + e.length, 0);
682
+ const liveKv = [...this.kvIndex.entries()]
683
+ .filter(([key]) => !this.pendingDeletes.has(key) && !this.pendingKv.has(key))
684
+ .reduce((sum, [, entry]) => sum + 4 + entry.length, 0) +
685
+ pendingBytes;
608
686
  return Math.max(0, 1 - liveKv / totalKv);
609
687
  }
610
688
  /** Total events in log (committed + pending). */
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Replaces the old copyTree()-based bootstrap with:
7
7
  * 1. Create minimal directories
8
- * 2. Initialize AcePackedStore at .agents/ACE/ace-state.ace
8
+ * 2. Initialize AcePackedStore at agent-state/ace-state.ace
9
9
  * 3. Bake core knowledge (agents, modules, kernel)
10
10
  * 4. Write topology records
11
11
  * 5. Write initial runtime state seeds