@wrongstack/core 0.264.0 → 0.267.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 (90) hide show
  1. package/dist/{agent-bridge-D8sa1vtv.d.ts → agent-bridge-STJ3JwwK.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-c9DLkaas.d.ts → agent-subagent-runner-CzPGP3jA.d.ts} +131 -11
  3. package/dist/{brain-O1IdKPaK.d.ts → brain-Cdg77tVN.d.ts} +103 -2
  4. package/dist/{compactor-BBy0rCtB.d.ts → compactor-iMZ84CXq.d.ts} +19 -1
  5. package/dist/{config-Dz2F3H2K.d.ts → config-Du3pYYln.d.ts} +132 -13
  6. package/dist/{context-BGSpZNSE.d.ts → context-dT5Ueund.d.ts} +90 -12
  7. package/dist/coordination/index.d.ts +78 -22
  8. package/dist/coordination/index.js +695 -273
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/{default-config-CXsDvOmP.d.ts → default-config-B0cj-Hry.d.ts} +11 -1
  11. package/dist/defaults/index.d.ts +28 -28
  12. package/dist/defaults/index.js +2327 -965
  13. package/dist/defaults/index.js.map +1 -1
  14. package/dist/execution/index.d.ts +16 -16
  15. package/dist/execution/index.js +1500 -371
  16. package/dist/execution/index.js.map +1 -1
  17. package/dist/execution/prompt-enhancer.d.ts +2 -2
  18. package/dist/execution/prompt-enhancer.js +1 -1
  19. package/dist/execution/prompt-enhancer.js.map +1 -1
  20. package/dist/extension/index.d.ts +6 -6
  21. package/dist/{goal-preamble-DzjFuN3p.d.ts → goal-preamble-SulMTowG.d.ts} +33 -12
  22. package/dist/{goal-store-CxWmCGbH.d.ts → goal-store-CABDwdFE.d.ts} +1 -1
  23. package/dist/{index-CbLSI66_.d.ts → index-Bms0m4oy.d.ts} +5 -5
  24. package/dist/{index-CYIQrXVF.d.ts → index-DtCVWel4.d.ts} +8 -8
  25. package/dist/index-IEuxQd-E.d.ts +82 -0
  26. package/dist/index.d.ts +261 -57
  27. package/dist/index.js +4799 -2212
  28. package/dist/index.js.map +1 -1
  29. package/dist/infrastructure/index.d.ts +6 -6
  30. package/dist/infrastructure/index.js +84 -9
  31. package/dist/infrastructure/index.js.map +1 -1
  32. package/dist/kernel/index.d.ts +9 -9
  33. package/dist/kernel/index.js +1 -1
  34. package/dist/kernel/index.js.map +1 -1
  35. package/dist/{mcp-servers-DC4QRPUI.d.ts → mcp-servers-C2cBTxUR.d.ts} +3 -3
  36. package/dist/models/index.d.ts +5 -5
  37. package/dist/models/index.js +104 -31
  38. package/dist/models/index.js.map +1 -1
  39. package/dist/{models-registry-B_siPxqN.d.ts → models-registry-BqGZNJQ-.d.ts} +1 -1
  40. package/dist/{multi-agent-coordinator-CK5Jdj9K.d.ts → multi-agent-coordinator-B8R43uPz.d.ts} +1 -1
  41. package/dist/{null-fleet-bus-DgvD4SCO.d.ts → null-fleet-bus-CnXa5oTH.d.ts} +14 -9
  42. package/dist/observability/index.d.ts +2 -2
  43. package/dist/{parallel-eternal-engine-bK0JQBR_.d.ts → parallel-eternal-engine-DdNnw9BQ.d.ts} +11 -9
  44. package/dist/{path-resolver-BPEDlN38.d.ts → path-resolver-COIMLCQL.d.ts} +3 -3
  45. package/dist/{permission-4yvGmMRB.d.ts → permission-B75JAi3-.d.ts} +1 -1
  46. package/dist/{permission-policy-C6XpsBOy.d.ts → permission-policy-DlR9eJAM.d.ts} +2 -2
  47. package/dist/{pipeline-CXCeMz8J.d.ts → pipeline-BfD2k1rT.d.ts} +3 -3
  48. package/dist/{plan-templates-BvzRBkJc.d.ts → plan-templates-DSIKCXZN.d.ts} +32 -8
  49. package/dist/provider-model-resolve-BNRsNuJx.d.ts +107 -0
  50. package/dist/{provider-runner-C5aQpDWE.d.ts → provider-runner-CX7iIvox.d.ts} +3 -3
  51. package/dist/{retry-policy-CFhdtRzz.d.ts → retry-policy-BilV1ujH.d.ts} +1 -1
  52. package/dist/sdd/index.d.ts +8 -8
  53. package/dist/sdd/index.js +286 -105
  54. package/dist/sdd/index.js.map +1 -1
  55. package/dist/secret-vault-BAKpgFw_.d.ts +57 -0
  56. package/dist/{secret-vault-CxiVLbt1.d.ts → secret-vault-gkvEZZfE.d.ts} +43 -4
  57. package/dist/security/index.d.ts +6 -68
  58. package/dist/security/index.js +296 -95
  59. package/dist/security/index.js.map +1 -1
  60. package/dist/{selector-gIuhRTkN.d.ts → selector-Bc7eWtT3.d.ts} +1 -1
  61. package/dist/{session-event-bridge-DkvvrpDt.d.ts → session-event-bridge-D-araDEz.d.ts} +1 -1
  62. package/dist/{session-reader-KdfVwkKP.d.ts → session-reader-D7Dapswh.d.ts} +1 -1
  63. package/dist/storage/index.d.ts +112 -15
  64. package/dist/storage/index.js +491 -156
  65. package/dist/storage/index.js.map +1 -1
  66. package/dist/tools/index.d.ts +4 -2
  67. package/dist/tools/index.js.map +1 -1
  68. package/dist/types/index.d.ts +21 -21
  69. package/dist/types/index.js +1523 -450
  70. package/dist/types/index.js.map +1 -1
  71. package/dist/utils/index.d.ts +455 -407
  72. package/dist/utils/index.js +2191 -1203
  73. package/dist/utils/index.js.map +1 -1
  74. package/dist/{wstack-paths-CJjEwPXn.d.ts → wstack-paths-hOpNLmvf.d.ts} +2 -0
  75. package/package.json +1 -1
  76. package/skills/api-design/SKILL.md +1 -1
  77. package/skills/audit-log/SKILL.md +6 -6
  78. package/skills/bug-hunter/SKILL.md +5 -5
  79. package/skills/chimera/SKILL.md +4 -4
  80. package/skills/docker-deploy/SKILL.md +1 -1
  81. package/skills/git-flow/SKILL.md +3 -3
  82. package/skills/multi-agent/SKILL.md +3 -3
  83. package/skills/node-modern/SKILL.md +1 -0
  84. package/skills/observability/SKILL.md +2 -2
  85. package/skills/output-standards/SKILL.md +51 -28
  86. package/skills/refactor-planner/SKILL.md +3 -3
  87. package/skills/security-scanner/SKILL.md +4 -3
  88. package/skills/tech-stack/SKILL.md +1 -2
  89. package/dist/llm-selector-DzxuZnNz.d.ts +0 -58
  90. package/dist/secret-vault-BJDY28ev.d.ts +0 -25
@@ -35,8 +35,8 @@ async function atomicWrite(targetPath, content, opts = {}) {
35
35
  }
36
36
  let mode;
37
37
  try {
38
- const stat6 = await fsp.stat(targetPath);
39
- mode = stat6.mode & 511;
38
+ const stat7 = await fsp.stat(targetPath);
39
+ mode = stat7.mode & 511;
40
40
  } catch {
41
41
  mode = opts.mode;
42
42
  }
@@ -76,8 +76,8 @@ async function withFileLock(targetPath, fn, opts = {}) {
76
76
  }
77
77
  if (code !== "EEXIST") throw err;
78
78
  try {
79
- const stat6 = await fsp.stat(lockPath);
80
- if (Date.now() - stat6.mtimeMs > staleMs) {
79
+ const stat7 = await fsp.stat(lockPath);
80
+ if (Date.now() - stat7.mtimeMs > staleMs) {
81
81
  await fsp.unlink(lockPath);
82
82
  continue;
83
83
  }
@@ -87,7 +87,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
87
87
  if (Date.now() - started >= timeoutMs) {
88
88
  throw new Error(`Timed out waiting for file lock: ${targetPath}`);
89
89
  }
90
- await new Promise((resolve5) => setTimeout(resolve5, 25));
90
+ await new Promise((resolve6) => setTimeout(resolve6, 25));
91
91
  }
92
92
  }
93
93
  try {
@@ -121,7 +121,7 @@ async function renameWithRetry(from, to) {
121
121
  if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
122
122
  throw err;
123
123
  }
124
- await new Promise((resolve5) => setTimeout(resolve5, delays[i]));
124
+ await new Promise((resolve6) => setTimeout(resolve6, delays[i]));
125
125
  }
126
126
  }
127
127
  throw lastErr;
@@ -219,26 +219,6 @@ function isEmptyMessage(msg) {
219
219
  return msg.content.length === 0;
220
220
  }
221
221
 
222
- // src/utils/error.ts
223
- function toErrorMessage(err) {
224
- return err instanceof Error ? err.message : String(err);
225
- }
226
-
227
- // src/utils/safe-json.ts
228
- function safeParse(input, maxBytes = 5e6) {
229
- if (input.length > maxBytes) {
230
- return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
231
- }
232
- try {
233
- return { ok: true, value: JSON.parse(input) };
234
- } catch (err) {
235
- return {
236
- ok: false,
237
- error: toErrorMessage(err)
238
- };
239
- }
240
- }
241
-
242
222
  // src/utils/term.ts
243
223
  var hasStdout = () => typeof process !== "undefined" && !!process.stdout;
244
224
  function isStdoutTTY() {
@@ -276,62 +256,6 @@ var color = {
276
256
  bgRed: wrap("41", "49"),
277
257
  bgGreen: wrap("42", "49")
278
258
  };
279
- function projectHash(absRoot) {
280
- return createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 12);
281
- }
282
- function projectSlug(absRoot) {
283
- const base = slugify(path2.basename(absRoot));
284
- const hash = createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 6);
285
- return `${base}-${hash}`;
286
- }
287
- function slugify(name) {
288
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
289
- }
290
- function wstackGlobalRoot() {
291
- const fromEnv = process.env["WRONGSTACK_HOME"];
292
- if (fromEnv && fromEnv.trim().length > 0) return path2.resolve(fromEnv);
293
- return path2.join(os.homedir(), ".wrongstack");
294
- }
295
- function resolveWstackPaths(opts) {
296
- const globalRoot = opts.globalRoot ?? (opts.userHome ? path2.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
297
- const hash = projectHash(opts.projectRoot);
298
- const slug = projectSlug(opts.projectRoot);
299
- const projectDir = path2.join(globalRoot, "projects", slug);
300
- return {
301
- globalRoot,
302
- configDir: globalRoot,
303
- globalConfig: path2.join(globalRoot, "config.json"),
304
- secretsKey: path2.join(globalRoot, ".key"),
305
- globalMemory: path2.join(globalRoot, "memory.md"),
306
- globalSkills: path2.join(globalRoot, "skills"),
307
- globalPrompts: path2.join(globalRoot, "prompts"),
308
- cacheDir: path2.join(globalRoot, "cache"),
309
- modelsCache: path2.join(globalRoot, "cache", "models.dev.json"),
310
- modelsOverlayCache: path2.join(globalRoot, "cache", "models-overlay.json"),
311
- historyFile: path2.join(globalRoot, "history"),
312
- logFile: path2.join(globalRoot, "logs", "wrongstack.log"),
313
- projectDir,
314
- projectCodebaseIndex: path2.join(projectDir, "codebase-index"),
315
- projectMemory: path2.join(projectDir, "memory.md"),
316
- projectSessions: path2.join(projectDir, "sessions"),
317
- projectTrust: path2.join(projectDir, "trust.json"),
318
- projectMeta: path2.join(projectDir, "meta.json"),
319
- projectLocalConfig: path2.join(projectDir, "config.local.json"),
320
- inProjectConfig: path2.join(opts.projectRoot, ".wrongstack", "config.json"),
321
- inProjectAgentsFile: path2.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
322
- inProjectSkills: path2.join(opts.projectRoot, ".wrongstack", "skills"),
323
- inProjectWorktrees: path2.join(opts.projectRoot, ".wrongstack", "worktrees"),
324
- projectHash: hash,
325
- projectSlug: slug,
326
- projectGoal: path2.join(projectDir, "goal.json"),
327
- projectSpecs: path2.join(projectDir, "specs"),
328
- projectTaskGraphs: path2.join(projectDir, "task-graphs"),
329
- projectSddSession: path2.join(projectDir, "sdd-session.json"),
330
- projectPlan: path2.join(projectDir, "plan.json"),
331
- projectAutophase: path2.join(projectDir, "autophase"),
332
- syncConfig: path2.join(globalRoot, "sync.json")
333
- };
334
- }
335
259
 
336
260
  // src/utils/deep-merge.ts
337
261
  var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
@@ -392,6 +316,11 @@ function deepMerge(base, patch, options = {}) {
392
316
  return out;
393
317
  }
394
318
 
319
+ // src/utils/error.ts
320
+ function toErrorMessage(err) {
321
+ return err instanceof Error ? err.message : String(err);
322
+ }
323
+
395
324
  // src/utils/regex-guard.ts
396
325
  var MAX_PATTERN_LEN = 512;
397
326
  var DANGEROUS_PATTERNS = [
@@ -428,6 +357,78 @@ function compileUserRegex(pattern, flags) {
428
357
  }
429
358
  }
430
359
 
360
+ // src/utils/safe-json.ts
361
+ function safeParse(input, maxBytes = 5e6) {
362
+ if (input.length > maxBytes) {
363
+ return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
364
+ }
365
+ try {
366
+ return { ok: true, value: JSON.parse(input) };
367
+ } catch (err) {
368
+ return {
369
+ ok: false,
370
+ error: toErrorMessage(err)
371
+ };
372
+ }
373
+ }
374
+ function projectHash(absRoot) {
375
+ return createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 12);
376
+ }
377
+ function projectSlug(absRoot) {
378
+ const base = slugify(path2.basename(absRoot));
379
+ const hash = createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 6);
380
+ return `${base}-${hash}`;
381
+ }
382
+ function slugify(name) {
383
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
384
+ }
385
+ function wstackGlobalRoot() {
386
+ const fromEnv = process.env["WRONGSTACK_HOME"];
387
+ if (fromEnv && fromEnv.trim().length > 0) return path2.resolve(fromEnv);
388
+ return path2.join(os.homedir(), ".wrongstack");
389
+ }
390
+ function resolveWstackPaths(opts) {
391
+ const globalRoot = opts.globalRoot ?? (opts.userHome ? path2.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
392
+ const hash = projectHash(opts.projectRoot);
393
+ const slug = projectSlug(opts.projectRoot);
394
+ const projectDir = path2.join(globalRoot, "projects", slug);
395
+ return {
396
+ globalRoot,
397
+ configDir: globalRoot,
398
+ globalConfig: path2.join(globalRoot, "config.json"),
399
+ secretsKey: path2.join(globalRoot, ".key"),
400
+ globalMemory: path2.join(globalRoot, "memory.md"),
401
+ globalSkills: path2.join(globalRoot, "skills"),
402
+ globalPrompts: path2.join(globalRoot, "prompts"),
403
+ cacheDir: path2.join(globalRoot, "cache"),
404
+ modelsCache: path2.join(globalRoot, "cache", "models.dev.json"),
405
+ modelsOverlayCache: path2.join(globalRoot, "cache", "models-overlay.json"),
406
+ historyFile: path2.join(globalRoot, "history"),
407
+ logFile: path2.join(globalRoot, "logs", "wrongstack.log"),
408
+ projectDir,
409
+ projectCodebaseIndex: path2.join(projectDir, "codebase-index"),
410
+ projectMemory: path2.join(projectDir, "memory.md"),
411
+ projectSessions: path2.join(projectDir, "sessions"),
412
+ projectTrust: path2.join(projectDir, "trust.json"),
413
+ projectMeta: path2.join(projectDir, "meta.json"),
414
+ projectLocalConfig: path2.join(projectDir, "config.local.json"),
415
+ inProjectConfig: path2.join(opts.projectRoot, ".wrongstack", "config.json"),
416
+ inProjectAgentsFile: path2.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
417
+ inProjectSkills: path2.join(opts.projectRoot, ".wrongstack", "skills"),
418
+ inProjectWorktrees: path2.join(opts.projectRoot, ".wrongstack", "worktrees"),
419
+ projectHash: hash,
420
+ projectSlug: slug,
421
+ projectGoal: path2.join(projectDir, "goal.json"),
422
+ projectSpecs: path2.join(projectDir, "specs"),
423
+ projectTaskGraphs: path2.join(projectDir, "task-graphs"),
424
+ projectSddSession: path2.join(projectDir, "sdd-session.json"),
425
+ projectPlan: path2.join(projectDir, "plan.json"),
426
+ projectAutophase: path2.join(projectDir, "autophase"),
427
+ syncConfig: path2.join(globalRoot, "sync.json"),
428
+ projectStatus: (projectHash2) => path2.join(globalRoot, "projects", projectHash2, "status.json")
429
+ };
430
+ }
431
+
431
432
  // src/storage/session-store.ts
432
433
  function sanitizeModel(model) {
433
434
  return model.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
@@ -443,11 +444,34 @@ var DefaultSessionStore = class _DefaultSessionStore {
443
444
  dir;
444
445
  events;
445
446
  secretScrubber;
447
+ /**
448
+ * In-memory cache for load() results, keyed by session ID. The cache is
449
+ * invalidated when the file's mtimeMs or size changes (indicating the
450
+ * file was written to). This eliminates redundant full-file reads and
451
+ * JSON parses when the same session is loaded multiple times within the
452
+ * store's lifetime (e.g., webui session detail views, list() fallbacks).
453
+ *
454
+ * Max size is capped to prevent unbounded memory growth in long-running
455
+ * processes. When the limit is reached, the oldest entry is evicted.
456
+ */
457
+ _loadCache = /* @__PURE__ */ new Map();
458
+ static LOAD_CACHE_MAX_ENTRIES = 50;
446
459
  constructor(opts) {
447
460
  this.dir = opts.dir;
448
461
  this.events = opts.events;
449
462
  this.secretScrubber = opts.secretScrubber;
450
463
  }
464
+ /**
465
+ * Clear the load() cache. Useful for testing or when the caller knows
466
+ * the file has changed externally (e.g., another process wrote to it).
467
+ */
468
+ clearLoadCache(sessionId) {
469
+ if (sessionId !== void 0) {
470
+ this._loadCache.delete(sessionId);
471
+ } else {
472
+ this._loadCache.clear();
473
+ }
474
+ }
451
475
  // ── Storage event helpers ───────────────────────────────────────────────────
452
476
  emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
453
477
  this.events?.emit("storage.read", {
@@ -590,7 +614,20 @@ var DefaultSessionStore = class _DefaultSessionStore {
590
614
  const t0 = Date.now();
591
615
  let outcome = "success";
592
616
  let errorMsg;
617
+ let cacheHit = false;
593
618
  try {
619
+ let stat7;
620
+ try {
621
+ const s = await fsp.stat(file);
622
+ stat7 = { mtimeMs: s.mtimeMs, size: s.size };
623
+ } catch (err) {
624
+ throw err;
625
+ }
626
+ const cached = this._loadCache.get(id);
627
+ if (cached && cached.mtimeMs === stat7.mtimeMs && cached.size === stat7.size) {
628
+ cacheHit = true;
629
+ return cached.data;
630
+ }
594
631
  const raw = await fsp.readFile(file, "utf8");
595
632
  const lines = raw.split("\n").filter((l) => l.trim());
596
633
  const events = [];
@@ -606,13 +643,30 @@ var DefaultSessionStore = class _DefaultSessionStore {
606
643
  const meta = this.metaFromEvents(id, events);
607
644
  const { messages, usage } = this.replay(events, id);
608
645
  const toolCallEnds = extractToolCallEnds(events);
609
- return { metadata: meta, events, messages, usage, toolCallEnds };
646
+ const data = { metadata: meta, events, messages, usage, toolCallEnds };
647
+ if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
648
+ const oldest = this._loadCache.keys().next().value;
649
+ if (oldest !== void 0) {
650
+ this._loadCache.delete(oldest);
651
+ }
652
+ }
653
+ this._loadCache.set(id, { mtimeMs: stat7.mtimeMs, size: stat7.size, data });
654
+ return data;
610
655
  } catch (err) {
611
656
  outcome = "failure";
612
657
  errorMsg = toErrorMessage(err);
613
658
  throw err;
614
659
  } finally {
615
660
  this.emitRead(id, file, "load", outcome, Date.now() - t0, errorMsg);
661
+ if (cacheHit) {
662
+ this.events?.emit("storage.cache_hit", {
663
+ sessionId: id,
664
+ store: "session",
665
+ filePath: file,
666
+ operation: "load",
667
+ durationMs: Date.now() - t0
668
+ });
669
+ }
616
670
  }
617
671
  }
618
672
  async list(limit = 20) {
@@ -780,8 +834,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
780
834
  return JSON.parse(raw);
781
835
  } catch {
782
836
  const full = this.sessionPath(id, ".jsonl");
783
- const stat6 = await fsp.stat(full);
784
- const summary = await this.summarize(id, stat6.mtime.toISOString());
837
+ const stat7 = await fsp.stat(full);
838
+ const summary = await this.summarize(id, stat7.mtime.toISOString());
785
839
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
786
840
  const msg = toErrorMessage(err);
787
841
  this.emitError(id, manifest, "summary_fallback", msg, true);
@@ -863,8 +917,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
863
917
  const pruneFile = async (dir, name, prefix) => {
864
918
  const jsonlPath = path2.join(dir, name);
865
919
  try {
866
- const stat6 = await fsp.stat(jsonlPath);
867
- if (stat6.mtimeMs >= cutoff) return;
920
+ const stat7 = await fsp.stat(jsonlPath);
921
+ if (stat7.mtimeMs >= cutoff) return;
868
922
  } catch {
869
923
  return;
870
924
  }
@@ -3137,6 +3191,9 @@ function deepFreeze(obj) {
3137
3191
  }
3138
3192
  return Object.freeze(obj);
3139
3193
  }
3194
+ var KEY_BYTES = 32;
3195
+ var KEY_FILE_MAGIC = Buffer.from("WSKV", "ascii");
3196
+ KEY_FILE_MAGIC.length + 1 + KEY_BYTES;
3140
3197
  function decryptConfigSecrets(cfg, vault, opts) {
3141
3198
  const warn = ((msg) => console.warn(msg));
3142
3199
  return walk(cfg, vault, (v, key) => {
@@ -3234,7 +3291,8 @@ var DEFAULT_TOOLS_CONFIG = Object.freeze({
3234
3291
  iterationTimeoutMs: 3e5,
3235
3292
  sessionTimeoutMs: 18e5,
3236
3293
  perIterationOutputCapBytes: 1e5,
3237
- autoExtendLimit: true
3294
+ autoExtendLimit: true,
3295
+ restrictToProjectRoot: false
3238
3296
  });
3239
3297
  var DEFAULT_CONTEXT_CONFIG = Object.freeze({
3240
3298
  preserveK: 10,
@@ -3274,7 +3332,8 @@ var BEHAVIOR_DEFAULTS = {
3274
3332
  iterationTimeoutMs: DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
3275
3333
  sessionTimeoutMs: DEFAULT_TOOLS_CONFIG.sessionTimeoutMs,
3276
3334
  perIterationOutputCapBytes: DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
3277
- autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit
3335
+ autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit,
3336
+ restrictToProjectRoot: DEFAULT_TOOLS_CONFIG.restrictToProjectRoot
3278
3337
  },
3279
3338
  log: { level: "info" },
3280
3339
  features: {
@@ -4710,15 +4769,15 @@ var SessionRecovery = class {
4710
4769
  async detectStale(sessionId) {
4711
4770
  const fp = this.filePath(sessionId);
4712
4771
  const TAIL_SIZE = 8192;
4713
- let stat6;
4772
+ let stat7;
4714
4773
  try {
4715
- stat6 = await fsp.stat(fp);
4774
+ stat7 = await fsp.stat(fp);
4716
4775
  } catch (err) {
4717
4776
  if (err.code === "ENOENT") return null;
4718
4777
  return null;
4719
4778
  }
4720
- if (stat6.size === 0) return null;
4721
- const position = Math.max(0, stat6.size - TAIL_SIZE);
4779
+ if (stat7.size === 0) return null;
4780
+ const position = Math.max(0, stat7.size - TAIL_SIZE);
4722
4781
  const buf = Buffer.alloc(TAIL_SIZE);
4723
4782
  let fh;
4724
4783
  try {
@@ -5281,6 +5340,8 @@ var SessionAnalyzer = class {
5281
5340
  var REGISTRY_FILE = "session-registry.json";
5282
5341
  var HEARTBEAT_INTERVAL_MS = 5e3;
5283
5342
  var STALE_TIMEOUT_MS = 3e4;
5343
+ var CLOSING_GRACE_MS = 15e3;
5344
+ var STALE_LOCK_MS = 1e4;
5284
5345
  function pidAlive(pid) {
5285
5346
  try {
5286
5347
  process.kill(pid, 0);
@@ -5293,6 +5354,12 @@ var SessionRegistry = class {
5293
5354
  filePath;
5294
5355
  heartbeatTimer = null;
5295
5356
  currentSessionId = null;
5357
+ /**
5358
+ * Last full entry this process registered. Kept so the heartbeat can
5359
+ * re-create our entry if it ever goes missing — e.g. our initial register()
5360
+ * write was dropped (a wedged lock), the file was reset, or we were pruned.
5361
+ */
5362
+ lastEntry = null;
5296
5363
  constructor(globalRoot) {
5297
5364
  this.filePath = path2.join(globalRoot, REGISTRY_FILE);
5298
5365
  }
@@ -5314,6 +5381,7 @@ var SessionRegistry = class {
5314
5381
  agentCount: entry.agents?.length ?? 0,
5315
5382
  agents: entry.agents ?? []
5316
5383
  };
5384
+ this.lastEntry = full;
5317
5385
  await this.atomicUpdate((registry) => {
5318
5386
  const now = Date.now();
5319
5387
  for (const [id, existing] of Object.entries(registry)) {
@@ -5339,16 +5407,28 @@ var SessionRegistry = class {
5339
5407
  */
5340
5408
  async updateAgents(agents) {
5341
5409
  if (!this.currentSessionId) return;
5410
+ const hasRunning = agents.some((a) => a.status === "running" || a.status === "streaming");
5411
+ const hasWaiting = agents.some((a) => a.status === "waiting_user");
5412
+ const hasError = agents.some((a) => a.status === "error");
5413
+ const status = hasRunning || hasWaiting || hasError ? "active" : "idle";
5414
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
5415
+ if (this.lastEntry) {
5416
+ this.lastEntry.agents = agents;
5417
+ this.lastEntry.agentCount = agents.length;
5418
+ this.lastEntry.status = status;
5419
+ this.lastEntry.lastHeartbeatAt = nowIso;
5420
+ }
5342
5421
  await this.atomicUpdate((registry) => {
5343
- const entry = registry[this.currentSessionId];
5344
- if (!entry) return;
5422
+ let entry = registry[this.currentSessionId];
5423
+ if (!entry) {
5424
+ if (!this.lastEntry) return;
5425
+ entry = { ...this.lastEntry };
5426
+ registry[this.currentSessionId] = entry;
5427
+ }
5345
5428
  entry.agents = agents;
5346
5429
  entry.agentCount = agents.length;
5347
- const hasRunning = agents.some((a) => a.status === "running" || a.status === "streaming");
5348
- const hasWaiting = agents.some((a) => a.status === "waiting_user");
5349
- const hasError = agents.some((a) => a.status === "error");
5350
- entry.status = hasRunning ? "active" : hasWaiting ? "active" : hasError ? "active" : "idle";
5351
- entry.lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
5430
+ entry.status = status;
5431
+ entry.lastHeartbeatAt = nowIso;
5352
5432
  });
5353
5433
  }
5354
5434
  /**
@@ -5426,6 +5506,12 @@ var SessionRegistry = class {
5426
5506
  entry.status = hasRunning ? "active" : "idle";
5427
5507
  }
5428
5508
  await this.writeAtomic(registry);
5509
+ } else if (this.lastEntry) {
5510
+ await this.atomicUpdate((reg) => {
5511
+ if (!reg[this.currentSessionId] && this.lastEntry) {
5512
+ reg[this.currentSessionId] = { ...this.lastEntry, lastHeartbeatAt: (/* @__PURE__ */ new Date()).toISOString() };
5513
+ }
5514
+ });
5429
5515
  }
5430
5516
  } catch {
5431
5517
  }
@@ -5438,6 +5524,11 @@ var SessionRegistry = class {
5438
5524
  let pruned = false;
5439
5525
  for (const [id, entry] of Object.entries(registry)) {
5440
5526
  const heartbeatAge = now - new Date(entry.lastHeartbeatAt).getTime();
5527
+ if (entry.status === "closing" && heartbeatAge > CLOSING_GRACE_MS) {
5528
+ delete registry[id];
5529
+ pruned = true;
5530
+ continue;
5531
+ }
5441
5532
  if (heartbeatAge > STALE_TIMEOUT_MS && !pidAlive(entry.pid)) {
5442
5533
  entry.status = "stale";
5443
5534
  const startedAge = now - new Date(entry.startedAt).getTime();
@@ -5457,17 +5548,23 @@ var SessionRegistry = class {
5457
5548
  }
5458
5549
  async atomicUpdate(fn) {
5459
5550
  const lockPath = `${this.filePath}.lock`;
5460
- const maxRetries = 5;
5551
+ const maxRetries = 8;
5461
5552
  const retryDelayMs = 20;
5462
5553
  for (let attempt = 0; attempt < maxRetries; attempt++) {
5463
5554
  try {
5464
5555
  await fsp.mkdir(path2.dirname(this.filePath), { recursive: true });
5465
- const lockHandle = await fsp.open(lockPath, "wx").catch(() => null);
5556
+ let lockHandle = await fsp.open(lockPath, "wx").catch(() => null);
5466
5557
  if (!lockHandle) {
5467
- await new Promise((r) => setTimeout(r, retryDelayMs * (attempt + 1)));
5468
- continue;
5558
+ if (await this.breakStaleLock(lockPath)) {
5559
+ lockHandle = await fsp.open(lockPath, "wx").catch(() => null);
5560
+ }
5561
+ if (!lockHandle) {
5562
+ await new Promise((r) => setTimeout(r, retryDelayMs * (attempt + 1)));
5563
+ continue;
5564
+ }
5469
5565
  }
5470
5566
  try {
5567
+ await lockHandle.writeFile(String(process.pid)).catch(() => void 0);
5471
5568
  const raw = await fsp.readFile(this.filePath, "utf8").catch(() => "{}");
5472
5569
  const registry = JSON.parse(raw);
5473
5570
  fn(registry);
@@ -5482,6 +5579,31 @@ var SessionRegistry = class {
5482
5579
  }
5483
5580
  }
5484
5581
  }
5582
+ /**
5583
+ * Break a contended lock if it is stale: the recorded owner pid is no longer
5584
+ * alive, or the lock is older than {@link STALE_LOCK_MS}. Returns true when the
5585
+ * lock was removed (caller should retry acquisition). Best-effort and
5586
+ * race-tolerant — a fresh lock (age ~0, live owner) is never broken, so the
5587
+ * common concurrent case self-heals on the next heartbeat.
5588
+ */
5589
+ async breakStaleLock(lockPath) {
5590
+ try {
5591
+ const [stat7, content] = await Promise.all([
5592
+ fsp.stat(lockPath),
5593
+ fsp.readFile(lockPath, "utf8").catch(() => "")
5594
+ ]);
5595
+ const ageMs = Date.now() - stat7.mtimeMs;
5596
+ const ownerPid = Number.parseInt(content.trim(), 10);
5597
+ const ownerDead = Number.isInteger(ownerPid) && ownerPid > 0 && ownerPid !== process.pid && !pidAlive(ownerPid);
5598
+ if (ownerDead || ageMs > STALE_LOCK_MS) {
5599
+ await fsp.unlink(lockPath).catch(() => void 0);
5600
+ return true;
5601
+ }
5602
+ return false;
5603
+ } catch {
5604
+ return true;
5605
+ }
5606
+ }
5485
5607
  async writeAtomicLocked(registry) {
5486
5608
  const tmp = `${this.filePath}.${randomUUID().slice(0, 8)}.tmp`;
5487
5609
  await fsp.writeFile(tmp, JSON.stringify(registry, null, 2), "utf8");
@@ -5509,6 +5631,10 @@ function hasSessionRegistry() {
5509
5631
  }
5510
5632
 
5511
5633
  // src/agent-status-tracker.ts
5634
+ var AGENT_REAP_MS = 3e4;
5635
+ var AGENT_SWEEP_INTERVAL_MS = 1e4;
5636
+ var PARTIAL_TEXT_CAP = 1200;
5637
+ var PARTIAL_FLUSH_THROTTLE_MS = 300;
5512
5638
  var AgentStatusTracker = class {
5513
5639
  events;
5514
5640
  registry;
@@ -5520,11 +5646,21 @@ var AgentStatusTracker = class {
5520
5646
  leaderCurrentTool;
5521
5647
  leaderIterations = 0;
5522
5648
  leaderToolCalls = 0;
5649
+ leaderCostUsd = 0;
5650
+ leaderTokensIn = 0;
5651
+ leaderTokensOut = 0;
5652
+ leaderCtxPct;
5653
+ leaderModel;
5654
+ leaderPartialText = "";
5523
5655
  unsubscribers = [];
5656
+ onUpdate;
5657
+ sweepTimer = null;
5658
+ partialTimer = null;
5524
5659
  constructor(opts) {
5525
5660
  this.events = opts.events;
5526
5661
  this.registry = opts.registry;
5527
5662
  this.leaderName = opts.leaderName ?? "leader";
5663
+ this.onUpdate = opts.onUpdate;
5528
5664
  }
5529
5665
  start() {
5530
5666
  this.unsubscribers.push(
@@ -5534,10 +5670,22 @@ var AgentStatusTracker = class {
5534
5670
  this.flush();
5535
5671
  })
5536
5672
  );
5673
+ this.unsubscribers.push(
5674
+ this.events.onPattern("iteration.started", (_e, payload) => {
5675
+ const ctx = payload?.ctx;
5676
+ if (!ctx) return;
5677
+ if (ctx.model) this.leaderModel = ctx.model;
5678
+ if (typeof ctx.tokenCount === "number" && typeof ctx.maxContext === "number" && ctx.maxContext > 0) {
5679
+ this.leaderCtxPct = Math.round(ctx.tokenCount / ctx.maxContext * 100);
5680
+ }
5681
+ this.flush();
5682
+ })
5683
+ );
5537
5684
  this.unsubscribers.push(
5538
5685
  this.events.onPattern("agent.run.completed", () => {
5539
5686
  this.leaderStatus = "idle";
5540
5687
  this.leaderCurrentTool = void 0;
5688
+ this.leaderPartialText = "";
5541
5689
  this.flush();
5542
5690
  })
5543
5691
  );
@@ -5545,6 +5693,7 @@ var AgentStatusTracker = class {
5545
5693
  this.events.onPattern("agent.run.error", () => {
5546
5694
  this.leaderStatus = "error";
5547
5695
  this.leaderCurrentTool = void 0;
5696
+ this.leaderPartialText = "";
5548
5697
  this.flush();
5549
5698
  })
5550
5699
  );
@@ -5574,74 +5723,120 @@ var AgentStatusTracker = class {
5574
5723
  this.unsubscribers.push(
5575
5724
  this.events.onPattern("llm.stream_started", () => {
5576
5725
  this.leaderStatus = "streaming";
5726
+ this.leaderPartialText = "";
5577
5727
  this.flush();
5578
5728
  })
5579
5729
  );
5580
5730
  this.unsubscribers.push(
5581
- this.events.onPattern("fleet.subagent.spawned", (_event, payload) => {
5731
+ this.events.onPattern("provider.text_delta", (_e, payload) => {
5732
+ const text = payload?.text;
5733
+ if (!text) return;
5734
+ this.leaderStatus = "streaming";
5735
+ const next = this.leaderPartialText + text;
5736
+ this.leaderPartialText = next.length > PARTIAL_TEXT_CAP ? next.slice(next.length - PARTIAL_TEXT_CAP) : next;
5737
+ this.schedulePartialFlush();
5738
+ })
5739
+ );
5740
+ this.unsubscribers.push(
5741
+ this.events.onPattern("token.accounted", (_e, payload) => {
5582
5742
  const p = payload;
5583
- if (p?.subagentId) {
5584
- this.agents.set(p.subagentId, {
5585
- id: p.subagentId,
5586
- name: p.name ?? p.subagentId,
5587
- status: "idle",
5588
- iterations: 0,
5589
- toolCalls: 0,
5590
- lastActivityAt: (/* @__PURE__ */ new Date()).toISOString()
5591
- });
5592
- this.flush();
5593
- }
5743
+ if (!p) return;
5744
+ this.leaderTokensIn += p.usage?.input ?? 0;
5745
+ this.leaderTokensOut += p.usage?.output ?? 0;
5746
+ this.leaderCostUsd += p.cost?.total ?? 0;
5747
+ this.flush();
5594
5748
  })
5595
5749
  );
5750
+ const touch = (id) => {
5751
+ let entry = this.agents.get(id);
5752
+ if (!entry) {
5753
+ entry = { id, name: id, status: "idle", iterations: 0, toolCalls: 0, lastActivityAt: (/* @__PURE__ */ new Date()).toISOString() };
5754
+ this.agents.set(id, entry);
5755
+ }
5756
+ entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
5757
+ return entry;
5758
+ };
5596
5759
  this.unsubscribers.push(
5597
- this.events.onPattern("fleet.subagent.task_started", (_event, payload) => {
5760
+ this.events.onPattern("subagent.spawned", (_e, payload) => {
5598
5761
  const p = payload;
5599
- if (p?.subagentId) {
5600
- const entry = this.agents.get(p.subagentId);
5601
- if (entry) {
5602
- entry.status = "running";
5603
- entry.iterations++;
5604
- entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
5605
- this.flush();
5606
- }
5607
- }
5762
+ if (!p?.subagentId) return;
5763
+ const entry = touch(p.subagentId);
5764
+ entry.name = p.name?.trim() || entry.name;
5765
+ if (p.model) entry.model = p.model;
5766
+ entry.status = "running";
5767
+ this.flush();
5608
5768
  })
5609
5769
  );
5610
5770
  this.unsubscribers.push(
5611
- this.events.onPattern("fleet.subagent.task_completed", (_event, payload) => {
5771
+ this.events.onPattern("subagent.ctx_pct", (_e, payload) => {
5612
5772
  const p = payload;
5613
- if (p?.subagentId) {
5614
- const entry = this.agents.get(p.subagentId);
5615
- if (entry) {
5616
- entry.status = "idle";
5617
- entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
5618
- this.flush();
5619
- }
5620
- }
5773
+ if (!p?.subagentId) return;
5774
+ const entry = touch(p.subagentId);
5775
+ if (typeof p.load === "number") entry.ctxPct = Math.round(p.load * 100);
5776
+ this.flush();
5621
5777
  })
5622
5778
  );
5623
5779
  this.unsubscribers.push(
5624
- this.events.onPattern("fleet.subagent.error", (_event, payload) => {
5780
+ this.events.onPattern("subagent.task_started", (_e, payload) => {
5625
5781
  const p = payload;
5626
- if (p?.subagentId) {
5627
- const entry = this.agents.get(p.subagentId);
5628
- if (entry) {
5629
- entry.status = "error";
5630
- entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
5631
- this.flush();
5632
- }
5633
- }
5782
+ if (!p?.subagentId) return;
5783
+ const entry = touch(p.subagentId);
5784
+ entry.status = "running";
5785
+ entry.iterations++;
5786
+ this.flush();
5787
+ })
5788
+ );
5789
+ this.unsubscribers.push(
5790
+ this.events.onPattern("subagent.tool_executed", (_e, payload) => {
5791
+ const p = payload;
5792
+ if (!p?.subagentId) return;
5793
+ const entry = touch(p.subagentId);
5794
+ entry.status = "running";
5795
+ entry.currentTool = p.name;
5796
+ entry.toolCalls++;
5797
+ this.flush();
5634
5798
  })
5635
5799
  );
5636
5800
  this.unsubscribers.push(
5637
- this.events.onPattern("fleet.subagent.stopped", (_event, payload) => {
5801
+ this.events.onPattern("subagent.iteration_summary", (_e, payload) => {
5638
5802
  const p = payload;
5639
- if (p?.subagentId) {
5640
- this.agents.delete(p.subagentId);
5641
- this.flush();
5803
+ if (!p?.subagentId) return;
5804
+ const entry = touch(p.subagentId);
5805
+ entry.status = "running";
5806
+ if (typeof p.iteration === "number") entry.iterations = p.iteration;
5807
+ if (typeof p.toolCalls === "number") entry.toolCalls = p.toolCalls;
5808
+ if (typeof p.costUsd === "number") entry.costUsd = p.costUsd;
5809
+ if (p.currentTool) entry.currentTool = p.currentTool;
5810
+ if (typeof p.partialText === "string") {
5811
+ entry.partialText = p.partialText.length > PARTIAL_TEXT_CAP ? p.partialText.slice(p.partialText.length - PARTIAL_TEXT_CAP) : p.partialText;
5642
5812
  }
5813
+ this.flush();
5814
+ })
5815
+ );
5816
+ this.unsubscribers.push(
5817
+ this.events.onPattern("subagent.task_completed", (_e, payload) => {
5818
+ const p = payload;
5819
+ if (!p?.subagentId) return;
5820
+ const entry = this.agents.get(p.subagentId);
5821
+ if (!entry) return;
5822
+ entry.status = p.status === "failed" || p.status === "timeout" ? "error" : "idle";
5823
+ entry.currentTool = void 0;
5824
+ entry.partialText = void 0;
5825
+ if (typeof p.iterations === "number") entry.iterations = p.iterations;
5826
+ if (typeof p.toolCalls === "number") entry.toolCalls = p.toolCalls;
5827
+ entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
5828
+ this.flush();
5829
+ })
5830
+ );
5831
+ this.unsubscribers.push(
5832
+ this.events.onPattern("subagent.stopped", (_e, payload) => {
5833
+ const p = payload;
5834
+ if (!p?.subagentId) return;
5835
+ if (this.agents.delete(p.subagentId)) this.flush();
5643
5836
  })
5644
5837
  );
5838
+ this.sweepTimer = setInterval(() => this.sweep(), AGENT_SWEEP_INTERVAL_MS);
5839
+ if (typeof this.sweepTimer.unref === "function") this.sweepTimer.unref();
5645
5840
  }
5646
5841
  stop() {
5647
5842
  for (const unsub of this.unsubscribers) {
@@ -5651,6 +5846,45 @@ var AgentStatusTracker = class {
5651
5846
  }
5652
5847
  }
5653
5848
  this.unsubscribers = [];
5849
+ if (this.sweepTimer) {
5850
+ clearInterval(this.sweepTimer);
5851
+ this.sweepTimer = null;
5852
+ }
5853
+ if (this.partialTimer) {
5854
+ clearTimeout(this.partialTimer);
5855
+ this.partialTimer = null;
5856
+ }
5857
+ }
5858
+ /**
5859
+ * Coalesce streamed-text flushes: at most one registry write per
5860
+ * {@link PARTIAL_FLUSH_THROTTLE_MS} while text streams in, so per-token
5861
+ * deltas never thrash the cross-process registry file.
5862
+ */
5863
+ schedulePartialFlush() {
5864
+ if (this.partialTimer) return;
5865
+ this.partialTimer = setTimeout(() => {
5866
+ this.partialTimer = null;
5867
+ this.flush();
5868
+ }, PARTIAL_FLUSH_THROTTLE_MS);
5869
+ if (typeof this.partialTimer.unref === "function") this.partialTimer.unref();
5870
+ }
5871
+ /**
5872
+ * Remove subagents that have been finished (idle/error) for longer than
5873
+ * {@link AGENT_REAP_MS}. Running / streaming / waiting_user agents are kept
5874
+ * regardless of age — only *not-working* agents are reaped.
5875
+ */
5876
+ sweep() {
5877
+ const now = Date.now();
5878
+ let removed = false;
5879
+ for (const [id, a] of this.agents) {
5880
+ const finished = a.status !== "running" && a.status !== "streaming" && a.status !== "waiting_user";
5881
+ const age = now - Date.parse(a.lastActivityAt);
5882
+ if (finished && Number.isFinite(age) && age > AGENT_REAP_MS) {
5883
+ this.agents.delete(id);
5884
+ removed = true;
5885
+ }
5886
+ }
5887
+ if (removed) this.flush();
5654
5888
  }
5655
5889
  flush() {
5656
5890
  const leaderEntry = {
@@ -5660,12 +5894,102 @@ var AgentStatusTracker = class {
5660
5894
  currentTool: this.leaderCurrentTool,
5661
5895
  iterations: this.leaderIterations,
5662
5896
  toolCalls: this.leaderToolCalls,
5897
+ costUsd: this.leaderCostUsd,
5898
+ tokensIn: this.leaderTokensIn,
5899
+ tokensOut: this.leaderTokensOut,
5900
+ ctxPct: this.leaderCtxPct,
5901
+ model: this.leaderModel,
5902
+ partialText: this.leaderPartialText || void 0,
5663
5903
  lastActivityAt: (/* @__PURE__ */ new Date()).toISOString()
5664
5904
  };
5665
5905
  const allAgents = [leaderEntry, ...this.agents.values()];
5666
- this.registry.updateAgents(allAgents).catch(() => void 0);
5906
+ this.registry.updateAgents(allAgents).then(() => {
5907
+ try {
5908
+ this.onUpdate?.();
5909
+ } catch {
5910
+ }
5911
+ }).catch(() => void 0);
5912
+ }
5913
+ };
5914
+ var INSTANCES_FILE = "webui-instances.json";
5915
+ var DISCOVERY_TTL_MS = 2500;
5916
+ var COALESCE_MS = 50;
5917
+ var POST_TIMEOUT_MS = 500;
5918
+ function pidAlive2(pid) {
5919
+ if (!Number.isInteger(pid) || pid <= 0) return false;
5920
+ try {
5921
+ process.kill(pid, 0);
5922
+ return true;
5923
+ } catch (err) {
5924
+ return err.code !== "ESRCH";
5925
+ }
5926
+ }
5927
+ function normRoot(root) {
5928
+ const resolved = path2.resolve(root);
5929
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
5930
+ }
5931
+ var FleetNotifier = class {
5932
+ baseDir;
5933
+ projectRoot;
5934
+ selfPid;
5935
+ doPost;
5936
+ cache = null;
5937
+ timer = null;
5938
+ disposed = false;
5939
+ constructor(opts) {
5940
+ this.baseDir = opts.baseDir;
5941
+ this.projectRoot = normRoot(opts.projectRoot);
5942
+ this.selfPid = opts.selfPid ?? process.pid;
5943
+ this.doPost = opts.post ?? defaultPost;
5944
+ }
5945
+ /** Coalesced, best-effort nudge. Safe to call on every status change. */
5946
+ notify() {
5947
+ if (this.disposed || this.timer) return;
5948
+ this.timer = setTimeout(() => {
5949
+ this.timer = null;
5950
+ void this.flush();
5951
+ }, COALESCE_MS);
5952
+ if (typeof this.timer.unref === "function") this.timer.unref();
5953
+ }
5954
+ /** Resolve same-project WebUI ping URLs (cached briefly). Exposed for tests. */
5955
+ async endpoints() {
5956
+ const now = Date.now();
5957
+ if (this.cache && now - this.cache.at < DISCOVERY_TTL_MS) return this.cache.urls;
5958
+ const urls = await this.discover();
5959
+ this.cache = { at: now, urls };
5960
+ return urls;
5961
+ }
5962
+ dispose() {
5963
+ this.disposed = true;
5964
+ if (this.timer) {
5965
+ clearTimeout(this.timer);
5966
+ this.timer = null;
5967
+ }
5968
+ }
5969
+ async flush() {
5970
+ const urls = await this.endpoints();
5971
+ await Promise.all(urls.map((u) => this.doPost(u).catch(() => void 0)));
5972
+ }
5973
+ async discover() {
5974
+ try {
5975
+ const raw = await fsp.readFile(path2.join(this.baseDir, INSTANCES_FILE), "utf8");
5976
+ const data = JSON.parse(raw);
5977
+ const list = Array.isArray(data?.instances) ? data.instances : [];
5978
+ return list.filter((i) => i && typeof i.httpPort === "number").filter((i) => i.pid !== this.selfPid).filter((i) => normRoot(i.projectRoot) === this.projectRoot).filter((i) => pidAlive2(i.pid)).map((i) => {
5979
+ const host = i.host === "0.0.0.0" || i.host === "::" || !i.host ? "127.0.0.1" : i.host;
5980
+ return `http://${host}:${i.httpPort}/api/fleet/ping`;
5981
+ });
5982
+ } catch {
5983
+ return [];
5984
+ }
5667
5985
  }
5668
5986
  };
5987
+ async function defaultPost(url) {
5988
+ await fetch(url, {
5989
+ method: "POST",
5990
+ signal: AbortSignal.timeout(POST_TIMEOUT_MS)
5991
+ });
5992
+ }
5669
5993
  var DefaultSessionRewinder = class {
5670
5994
  constructor(sessionsDir, projectRoot) {
5671
5995
  this.sessionsDir = sessionsDir;
@@ -6034,6 +6358,7 @@ async function savePlan(filePath, plan, events) {
6034
6358
  outcome: "success",
6035
6359
  durationMs: Date.now() - t0
6036
6360
  });
6361
+ return true;
6037
6362
  } catch (err) {
6038
6363
  events?.emit("storage.error", {
6039
6364
  sessionId: "~boot~",
@@ -6047,6 +6372,7 @@ async function savePlan(filePath, plan, events) {
6047
6372
  "[plan-store] save failed:",
6048
6373
  toErrorMessage(err)
6049
6374
  );
6375
+ return false;
6050
6376
  }
6051
6377
  }
6052
6378
  function emptyPlan(sessionId, title) {
@@ -6148,7 +6474,10 @@ async function mutatePlan(filePath, sessionId, fn) {
6148
6474
  return withFileLock(filePath, async () => {
6149
6475
  const plan = await loadPlan(filePath) ?? emptyPlan(sessionId);
6150
6476
  const updated = await fn(plan);
6151
- await savePlan(filePath, updated);
6477
+ const persisted = await savePlan(filePath, updated);
6478
+ if (!persisted) {
6479
+ throw new Error(`Failed to persist plan to ${filePath} \u2014 the change was NOT saved.`);
6480
+ }
6152
6481
  return updated;
6153
6482
  });
6154
6483
  }
@@ -6346,6 +6675,7 @@ async function saveTasks(filePath, tasks, events, traceId) {
6346
6675
  durationMs: Date.now() - t0,
6347
6676
  ...traceId !== void 0 && { traceId }
6348
6677
  });
6678
+ return true;
6349
6679
  } catch (err) {
6350
6680
  events?.emit("storage.error", {
6351
6681
  sessionId: traceId ?? "~boot~",
@@ -6361,13 +6691,17 @@ async function saveTasks(filePath, tasks, events, traceId) {
6361
6691
  "[task-store] save failed:",
6362
6692
  toErrorMessage(err)
6363
6693
  );
6694
+ return false;
6364
6695
  }
6365
6696
  }
6366
6697
  async function mutateTasks(filePath, sessionId, fn, events, traceId) {
6367
6698
  return withFileLock(filePath, async () => {
6368
6699
  const file = await loadTasks(filePath, events, traceId) ?? emptyTaskFile(sessionId);
6369
6700
  const updated = await fn(file);
6370
- await saveTasks(filePath, updated, events, traceId);
6701
+ const persisted = await saveTasks(filePath, updated, events, traceId);
6702
+ if (!persisted) {
6703
+ throw new Error(`Failed to persist tasks to ${filePath} \u2014 the change was NOT saved.`);
6704
+ }
6371
6705
  return updated;
6372
6706
  });
6373
6707
  }
@@ -7091,8 +7425,8 @@ var CloudSync = class {
7091
7425
  const localPath = this.categoryToPath(cat);
7092
7426
  if (!localPath) continue;
7093
7427
  try {
7094
- const stat6 = await fsp.stat(localPath);
7095
- if (stat6.isDirectory()) {
7428
+ const stat7 = await fsp.stat(localPath);
7429
+ if (stat7.isDirectory()) {
7096
7430
  const files = await this.walkDir(localPath, localPath);
7097
7431
  for (const file of files) {
7098
7432
  const content = await fsp.readFile(file, "utf8");
@@ -7117,8 +7451,8 @@ var CloudSync = class {
7117
7451
  const localPath = this.categoryToPath(cat);
7118
7452
  if (!localPath) continue;
7119
7453
  try {
7120
- const stat6 = await fsp.stat(localPath);
7121
- if (stat6.isDirectory()) {
7454
+ const stat7 = await fsp.stat(localPath);
7455
+ if (stat7.isDirectory()) {
7122
7456
  const files = await this.walkDir(localPath, localPath);
7123
7457
  for (const file of files) {
7124
7458
  const content = await fsp.readFile(file);
@@ -7145,6 +7479,7 @@ var CloudSync = class {
7145
7479
  return this.paths.globalMemory;
7146
7480
  case "history":
7147
7481
  return this.paths.historyFile;
7482
+ /* v8 ignore next -- unreachable: SyncCategory is exhaustively matched above */
7148
7483
  default:
7149
7484
  return null;
7150
7485
  }
@@ -7323,6 +7658,6 @@ function resolveSessionLoggingConfig(cfg) {
7323
7658
  };
7324
7659
  }
7325
7660
 
7326
- export { ALL_SYNC_CATEGORIES, AgentStatusTracker, AnnotationsStore, CORE_RECONSTRUCT_EVENTS, CloudSync, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultMemoryStore, DefaultPromptStore, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DirectorStateCheckpoint, FileMemoryBackend, GraphMemoryBackend, MAX_JOURNAL_ENTRIES, MAX_PROGRESS_HISTORY, QueueStore, RecoveryLock, ReplayLogStore, STANDARD_AUDIT_EVENTS, SessionAnalyzer, SessionMemoryConsolidator, SessionRecovery, SessionRegistry, ToolAuditLog, addPlanItem, appendJournal, attachPlanCheckpoint, attachTodosCheckpoint, clearPlan, createSessionEventBridge, deriveTodosFromPlanItem, emptyGoal, emptyPlan, emptyTaskFile, formatGoal, formatPlan, formatPlanTemplates, getPlanTemplate, getSessionRegistry, goalFilePath, hasSessionRegistry, listPlanTemplates, loadDirectorState, loadGoal, loadPlan, loadTasks, loadTodosCheckpoint, mutatePlan, mutateTasks, parseEntries, parseProgressFromText, recordProgress, removePlanItem, resolveAuditLevel, resolveSessionLoggingConfig, runConfigMigrations, saveGoal, savePlan, saveTasks, saveTodosCheckpoint, setPlanItemStatus, setProgress, summarizeUsage };
7661
+ export { ALL_SYNC_CATEGORIES, AgentStatusTracker, AnnotationsStore, CORE_RECONSTRUCT_EVENTS, CloudSync, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultMemoryStore, DefaultPromptStore, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DirectorStateCheckpoint, FileMemoryBackend, FleetNotifier, GraphMemoryBackend, MAX_JOURNAL_ENTRIES, MAX_PROGRESS_HISTORY, QueueStore, RecoveryLock, ReplayLogStore, STANDARD_AUDIT_EVENTS, SessionAnalyzer, SessionMemoryConsolidator, SessionRecovery, SessionRegistry, ToolAuditLog, addPlanItem, appendJournal, attachPlanCheckpoint, attachTodosCheckpoint, clearPlan, createSessionEventBridge, deriveTodosFromPlanItem, emptyGoal, emptyPlan, emptyTaskFile, formatGoal, formatPlan, formatPlanTemplates, getPlanTemplate, getSessionRegistry, goalFilePath, hasSessionRegistry, listPlanTemplates, loadDirectorState, loadGoal, loadPlan, loadTasks, loadTodosCheckpoint, mutatePlan, mutateTasks, parseEntries, parseProgressFromText, recordProgress, removePlanItem, resolveAuditLevel, resolveSessionLoggingConfig, runConfigMigrations, saveGoal, savePlan, saveTasks, saveTodosCheckpoint, setPlanItemStatus, setProgress, summarizeUsage };
7327
7662
  //# sourceMappingURL=index.js.map
7328
7663
  //# sourceMappingURL=index.js.map