@inerrata-corporation/errata 2.0.2-dev.698 → 2.0.2-dev.704

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 (2) hide show
  1. package/errata.mjs +157 -64
  2. package/package.json +1 -1
package/errata.mjs CHANGED
@@ -17361,7 +17361,7 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
17361
17361
  return { count: Number(r.count), total: Number(r.total) };
17362
17362
  }
17363
17363
  liveNodeIds(ids) {
17364
- const live = /* @__PURE__ */ new Set();
17364
+ const live2 = /* @__PURE__ */ new Set();
17365
17365
  const CHUNK = 900;
17366
17366
  for (let i2 = 0; i2 < ids.length; i2 += CHUNK) {
17367
17367
  const slice = ids.slice(i2, i2 + CHUNK);
@@ -17369,9 +17369,9 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
17369
17369
  const rows = this.db.prepare(
17370
17370
  `SELECT id FROM nodes WHERE valid_to IS NULL AND id IN (${slice.map(() => "?").join(",")})`
17371
17371
  ).all(...slice);
17372
- for (const r of rows) live.add(r.id);
17372
+ for (const r of rows) live2.add(r.id);
17373
17373
  }
17374
- return live;
17374
+ return live2;
17375
17375
  }
17376
17376
  /**
17377
17377
  * Live edges WITH their endpoint labels and ids, resolved in ONE join.
@@ -17415,9 +17415,9 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
17415
17415
  // the indexer calls freezeNode inside its own store.transaction(). So this
17416
17416
  // method does NOT open a transaction — callers needing atomicity wrap it.
17417
17417
  freezeNode(liveId, t) {
17418
- const live = this.getNode(liveId);
17419
- if (!live) return null;
17420
- const version2 = live.version ?? 1;
17418
+ const live2 = this.getNode(liveId);
17419
+ if (!live2) return null;
17420
+ const version2 = live2.version ?? 1;
17421
17421
  const frozenId = `${liveId}@v${version2}`;
17422
17422
  if (this.getNode(frozenId)) {
17423
17423
  this.stmts.advanceLive.run({ live_id: liveId, t });
@@ -20216,8 +20216,8 @@ function applyDataDependentMomentum(previousCumulative, newSignal, seqGapSinceLa
20216
20216
  );
20217
20217
  }
20218
20218
  function newPeakSurprise(oldPeak, newCumulative) {
20219
- const peak = oldPeak ?? 0;
20220
- return newCumulative > peak ? newCumulative : peak;
20219
+ const peak2 = oldPeak ?? 0;
20220
+ return newCumulative > peak2 ? newCumulative : peak2;
20221
20221
  }
20222
20222
  var init_momentum = __esm({
20223
20223
  "../../packages/math/src/momentum.ts"() {
@@ -20248,10 +20248,10 @@ function computeAdaptiveHalfLife(stats) {
20248
20248
  return Math.max(1, halfLife);
20249
20249
  }
20250
20250
  function momentumHealth(stats) {
20251
- const peak = stats.peakSurprise ?? 0;
20252
- if (peak <= 0) return 1;
20251
+ const peak2 = stats.peakSurprise ?? 0;
20252
+ if (peak2 <= 0) return 1;
20253
20253
  const cum = Math.max(0, stats.cumulativeSurprise ?? 0);
20254
- return Math.max(MOMENTUM_GATE_FLOOR, Math.min(1, cum / peak));
20254
+ return Math.max(MOMENTUM_GATE_FLOOR, Math.min(1, cum / peak2));
20255
20255
  }
20256
20256
  function computeAdaptiveConfidence(stats, opts) {
20257
20257
  if (stats.memoryTier === "persistent") {
@@ -28482,8 +28482,8 @@ function mergeProblemsByEmbedding(store, opts) {
28482
28482
  }
28483
28483
  function bindCanonicalNeighbors(store, opts) {
28484
28484
  const k = opts.k ?? 3;
28485
- const live = (n) => n.attrs["mergedInto"] === void 0;
28486
- const problems = store.findNodesByLabel("Problem").filter((p) => p.embedding.length > 0 && live(p));
28485
+ const live2 = (n) => n.attrs["mergedInto"] === void 0;
28486
+ const problems = store.findNodesByLabel("Problem").filter((p) => p.embedding.length > 0 && live2(p));
28487
28487
  const patterns = store.findNodesByLabel("Pattern").filter((p) => p.embedding.length > 0);
28488
28488
  const candidates = [...problems, ...patterns];
28489
28489
  let bound = 0;
@@ -30869,12 +30869,12 @@ async function incrementalReindex(store, rootPath, workspaceId2, changedAbsPaths
30869
30869
  if (changedIds.length > 0) {
30870
30870
  store.transaction(() => {
30871
30871
  for (const nid of changedIds) {
30872
- const live = store.getNode(nid);
30873
- if (!live) continue;
30874
- const history = store.nodeHistory(live.logicalId ?? nid);
30872
+ const live2 = store.getNode(nid);
30873
+ if (!live2) continue;
30874
+ const history = store.nodeHistory(live2.logicalId ?? nid);
30875
30875
  const birth = history[0];
30876
30876
  if (!birth || birth.id === nid) continue;
30877
- const currentSim = live.attrs["bodySimhash"];
30877
+ const currentSim = live2.attrs["bodySimhash"];
30878
30878
  const birthSim = birth.attrs["bodySimhash"];
30879
30879
  if (!hasForkedDrift(currentSim, birthSim)) continue;
30880
30880
  const forkId = edgeId(nid, "FORKED_FROM", birth.id);
@@ -38981,13 +38981,13 @@ async function findStaleFiles(store, rootPath, workspaceId2) {
38981
38981
  }
38982
38982
  }
38983
38983
  }
38984
- const live = /* @__PURE__ */ new Set();
38984
+ const live2 = /* @__PURE__ */ new Set();
38985
38985
  for (let i2 = 0; i2 < allTargets.length; i2 += LIVENESS_CHUNK) {
38986
- for (const id of store.liveNodeIds(allTargets.slice(i2, i2 + LIVENESS_CHUNK))) live.add(id);
38986
+ for (const id of store.liveNodeIds(allTargets.slice(i2, i2 + LIVENESS_CHUNK))) live2.add(id);
38987
38987
  if (i2 + LIVENESS_CHUNK < allTargets.length) await new Promise((r) => setImmediate(r));
38988
38988
  }
38989
38989
  for (const { abs, targets } of pending) {
38990
- if (targets.some((id) => !live.has(id))) stale.push(abs);
38990
+ if (targets.some((id) => !live2.has(id))) stale.push(abs);
38991
38991
  }
38992
38992
  return stale;
38993
38993
  }
@@ -39009,12 +39009,12 @@ function reapOrphanedFiles(store, rootPath, workspaceId2, ignoreDirs = []) {
39009
39009
  if (listed === null) return { reaped: 0, symbolsClosed: 0, skipped: "no-git-listing" };
39010
39010
  if (listed.length === 0) return { reaped: 0, symbolsClosed: 0, skipped: "empty-listing" };
39011
39011
  const onDisk = new Set(listed);
39012
- const live = store.findNodesByLabel("File").filter((f) => f.attrs["workspaceId"] === workspaceId2);
39013
- const orphans = live.filter((f) => {
39012
+ const live2 = store.findNodesByLabel("File").filter((f) => f.attrs["workspaceId"] === workspaceId2);
39013
+ const orphans = live2.filter((f) => {
39014
39014
  const rel = f.attrs["relPath"];
39015
39015
  return typeof rel === "string" && !onDisk.has(rel);
39016
39016
  });
39017
- const allowance = Math.max(ORPHAN_REAP_MIN_ALLOWANCE, live.length * ORPHAN_REAP_MAX_FRACTION);
39017
+ const allowance = Math.max(ORPHAN_REAP_MIN_ALLOWANCE, live2.length * ORPHAN_REAP_MAX_FRACTION);
39018
39018
  if (orphans.length > allowance) {
39019
39019
  return {
39020
39020
  reaped: 0,
@@ -49401,11 +49401,11 @@ function isLive2(verb) {
49401
49401
  function nextHtml(n, opts) {
49402
49402
  if (!n) return "";
49403
49403
  const cmd2 = `errata ${n.verb}${n.arg ? " " + n.arg : ""}`;
49404
- const live = isLive2(n.verb);
49405
- if (!live && !opts.includeFutureVerbs) return "";
49404
+ const live2 = isLive2(n.verb);
49405
+ if (!live2 && !opts.includeFutureVerbs) return "";
49406
49406
  const hint = n.hint ? ` \u2014 ${esc3(n.hint)}` : "";
49407
- const cls = live ? "next" : "next soon";
49408
- const tag = live ? `<code>${esc3(cmd2)}</code>` : `<span class="vsoon">${esc3(cmd2)} \xB7 soon</span>`;
49407
+ const cls = live2 ? "next" : "next soon";
49408
+ const tag = live2 ? `<code>${esc3(cmd2)}</code>` : `<span class="vsoon">${esc3(cmd2)} \xB7 soon</span>`;
49409
49409
  return `<div class="${cls}">next: ${tag}${hint}</div>`;
49410
49410
  }
49411
49411
  function shell(title, body2) {
@@ -49770,7 +49770,7 @@ var init_report_render = __esm({
49770
49770
 
49771
49771
  // src/cli.ts
49772
49772
  init_src6();
49773
- import { closeSync as closeSync2, existsSync as existsSync29, openSync as openSync2, readFileSync as readFileSync28, renameSync as renameSync4, statSync as statSync6 } from "node:fs";
49773
+ import { closeSync as closeSync2, existsSync as existsSync30, openSync as openSync2, readFileSync as readFileSync28, renameSync as renameSync4, statSync as statSync6 } from "node:fs";
49774
49774
  import { join as join32 } from "node:path";
49775
49775
  import { spawn as spawn3 } from "node:child_process";
49776
49776
 
@@ -53564,6 +53564,24 @@ init_reconcile();
53564
53564
  // src/pass-ledger.ts
53565
53565
  import { appendFileSync as appendFileSync2, existsSync as existsSync15, readFileSync as readFileSync13, writeFileSync as writeFileSync13 } from "node:fs";
53566
53566
  import { join as join18 } from "node:path";
53567
+
53568
+ // src/isolate-census.ts
53569
+ var live = 0;
53570
+ var spawnedTotal = 0;
53571
+ var peak = 0;
53572
+ function noteIsolateSpawned() {
53573
+ live += 1;
53574
+ spawnedTotal += 1;
53575
+ if (live > peak) peak = live;
53576
+ }
53577
+ function noteIsolateExited() {
53578
+ if (live > 0) live -= 1;
53579
+ }
53580
+ function isolateCensus() {
53581
+ return { live, spawnedTotal, peak };
53582
+ }
53583
+
53584
+ // src/pass-ledger.ts
53567
53585
  var PER_KIND_MAX = 100;
53568
53586
  var TRIM_EVERY = 50;
53569
53587
  var appendsSinceTrim = /* @__PURE__ */ new Map();
@@ -53573,11 +53591,22 @@ function passLedgerPath(configDir) {
53573
53591
  function appendPassLedger(configDir, kind, durationMs, counts) {
53574
53592
  const path2 = passLedgerPath(configDir);
53575
53593
  try {
53594
+ const mem = process.memoryUsage();
53595
+ const census = isolateCensus();
53576
53596
  const entry = {
53577
53597
  ts: Date.now(),
53578
53598
  kind,
53579
53599
  durationMs,
53580
- counts: { ...counts, rssMB: Math.round(process.memoryUsage().rss / 1048576) }
53600
+ counts: {
53601
+ ...counts,
53602
+ rssMB: Math.round(mem.rss / 1048576),
53603
+ heapUsedMB: Math.round(mem.heapUsed / 1048576),
53604
+ externalMB: Math.round(mem.external / 1048576),
53605
+ arrayBuffersMB: Math.round(mem.arrayBuffers / 1048576),
53606
+ isolatesLive: census.live,
53607
+ isolatesSpawned: census.spawnedTotal,
53608
+ isolatesPeak: census.peak
53609
+ }
53581
53610
  };
53582
53611
  appendFileSync2(path2, JSON.stringify(entry) + "\n");
53583
53612
  const n = (appendsSinceTrim.get(path2) ?? 0) + 1;
@@ -53751,8 +53780,8 @@ function recordEpisode(store, workspaceId2, t, delta, causal) {
53751
53780
  minted++;
53752
53781
  }
53753
53782
  if (s.change === "changed") {
53754
- const live = store.getNode(s.nodeId);
53755
- const prev = (live?.version ?? 2) - 1;
53783
+ const live2 = store.getNode(s.nodeId);
53784
+ const prev = (live2?.version ?? 2) - 1;
53756
53785
  const frozenId = `${s.nodeId}@v${prev}`;
53757
53786
  if (store.getNode(frozenId)) {
53758
53787
  store.mergeEdge(producedEdge(id, frozenId, t));
@@ -53773,6 +53802,8 @@ function numEnv(key, fallback) {
53773
53802
  }
53774
53803
  var DAEMON_MAX_HEAP_MB = numEnv("ERRATA_MAX_HEAP_MB", 2048);
53775
53804
  var WORKER_MAX_HEAP_MB = numEnv("ERRATA_WORKER_MAX_HEAP_MB", 1024);
53805
+ var WORKSPACE_IDLE_TTL_MS = numEnv("ERRATA_WORKSPACE_IDLE_TTL_MS", 15 * 6e4);
53806
+ var WORKSPACE_SWEEP_INTERVAL_MS = numEnv("ERRATA_WORKSPACE_SWEEP_MS", 6e4);
53776
53807
  var HARVEST_SLICE_BYTES = numEnv("ERRATA_HARVEST_SLICE_BYTES", 4e6);
53777
53808
  function workerResourceLimits() {
53778
53809
  return { maxOldGenerationSizeMb: WORKER_MAX_HEAP_MB };
@@ -53783,6 +53814,11 @@ function isWorkerOom(err2) {
53783
53814
  if (e.code === "ERR_WORKER_OUT_OF_MEMORY") return true;
53784
53815
  return /out of memory|ERR_WORKER_OUT_OF_MEMORY|heap limit/i.test(e.message ?? "");
53785
53816
  }
53817
+ function retireDecision(input) {
53818
+ if (!input.exists) return "gone";
53819
+ const ttl = input.ttlMs ?? WORKSPACE_IDLE_TTL_MS;
53820
+ return input.now - input.lastActiveAt > ttl ? "idle" : null;
53821
+ }
53786
53822
 
53787
53823
  // src/episode-retention.ts
53788
53824
  var EPISODE_RETAIN_DAYS_DEFAULT = 90;
@@ -53919,6 +53955,7 @@ var PassWorker = class {
53919
53955
  // it inline where the same allocation would take down the hook server.
53920
53956
  resourceLimits: workerResourceLimits()
53921
53957
  });
53958
+ noteIsolateSpawned();
53922
53959
  this.worker = worker;
53923
53960
  this.ready = new Promise((resolve6, reject) => {
53924
53961
  const onReady = (m) => {
@@ -53948,6 +53985,7 @@ var PassWorker = class {
53948
53985
  };
53949
53986
  worker.on("error", teardown);
53950
53987
  worker.on("exit", (code) => {
53988
+ noteIsolateExited();
53951
53989
  if (!this.stopped) teardown(new Error(`pass worker exited (code ${code})`));
53952
53990
  });
53953
53991
  }
@@ -54662,7 +54700,7 @@ async function syncSkills(paths, client, seed, pins = [], techSeed) {
54662
54700
  const res = await client.getSkills(void 0, seed, techSeed);
54663
54701
  const staleDaemon = res.staleDaemon && res.latestDaemonVersion ? { latest: res.latestDaemonVersion } : void 0;
54664
54702
  mkdirSync6(paths.skillsDir, { recursive: true });
54665
- if (res.skills.length === 0 && pins.length === 0) {
54703
+ if (res.skills.length === 0 && pins.length === 0 && !res.skillsGated) {
54666
54704
  const existing = readdirSync7(paths.skillsDir).filter((f) => f.endsWith(".md"));
54667
54705
  if (existing.length > 0) return { written: 0, pruned: 0, ...staleDaemon ? { staleDaemon } : {} };
54668
54706
  }
@@ -54896,7 +54934,7 @@ var INTENT_TTL_MS = 15 * 60 * 1e3;
54896
54934
  function createIntentState(ttlMs = INTENT_TTL_MS) {
54897
54935
  const bySession = /* @__PURE__ */ new Map();
54898
54936
  let recent = null;
54899
- const live = (s, nowMs) => s !== void 0 && nowMs - s.atMs < ttlMs;
54937
+ const live2 = (s, nowMs) => s !== void 0 && nowMs - s.atMs < ttlMs;
54900
54938
  return {
54901
54939
  set(sessionId, text, nowMs) {
54902
54940
  const trimmed = text.trim();
@@ -54910,7 +54948,7 @@ function createIntentState(ttlMs = INTENT_TTL_MS) {
54910
54948
  },
54911
54949
  get(sessionId, nowMs) {
54912
54950
  const s = bySession.get(sessionId);
54913
- return live(s, nowMs) ? s.text : void 0;
54951
+ return live2(s, nowMs) ? s.text : void 0;
54914
54952
  },
54915
54953
  mostRecent(nowMs) {
54916
54954
  if (!recent) return void 0;
@@ -55159,10 +55197,10 @@ function saveWitnessQueue(path2, queue) {
55159
55197
  }
55160
55198
  }
55161
55199
  function pruneWitnessQueue(queue, now) {
55162
- const live = queue.filter(
55200
+ const live2 = queue.filter(
55163
55201
  (w) => now - w.ts < WITNESS_TTL_MS && w.attempts < WITNESS_MAX_ATTEMPTS
55164
55202
  );
55165
- return live.length > WITNESS_QUEUE_CAP ? live.slice(live.length - WITNESS_QUEUE_CAP) : live;
55203
+ return live2.length > WITNESS_QUEUE_CAP ? live2.slice(live2.length - WITNESS_QUEUE_CAP) : live2;
55166
55204
  }
55167
55205
  function enqueueWitnesses(queue, fresh, now) {
55168
55206
  const byKey = new Map(queue.map((w) => [`${w.channel}:${w.witnessKey}`, w]));
@@ -55494,7 +55532,7 @@ function startLoopLagMonitor(thresholdMs = 1e3) {
55494
55532
  }
55495
55533
 
55496
55534
  // src/engine.ts
55497
- var DAEMON_VERSION = true ? "2.0.2-dev.698" : "2.0.0-alpha.0";
55535
+ var DAEMON_VERSION = true ? "2.0.2-dev.704" : "2.0.0-alpha.0";
55498
55536
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
55499
55537
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
55500
55538
  var GIT_OP_MUTE_MS = 4e3;
@@ -55616,6 +55654,7 @@ function createWorkspaceEngine(opts) {
55616
55654
  const cloud = opts.cloud;
55617
55655
  const telemetry = new TelemetryRecorder(profile.id, DAEMON_VERSION);
55618
55656
  let watcher = null;
55657
+ let lastActivityTs = Date.now();
55619
55658
  let stopGit = null;
55620
55659
  let flushTimer = null;
55621
55660
  if (!opts.skipWatchers) {
@@ -55634,6 +55673,7 @@ function createWorkspaceEngine(opts) {
55634
55673
  size = statSync5(path2).size;
55635
55674
  } catch {
55636
55675
  }
55676
+ lastActivityTs = Date.now();
55637
55677
  const base = { ts: Date.now(), kind, workspaceId: profile.id, path: path2, size };
55638
55678
  const ev = { ...base, digest: digest(base) };
55639
55679
  log.append(ev);
@@ -57034,8 +57074,12 @@ function createWorkspaceEngine(opts) {
57034
57074
  onSessionEnd
57035
57075
  },
57036
57076
  emitEvent(ev) {
57077
+ lastActivityTs = Date.now();
57037
57078
  log.append(ev);
57038
57079
  },
57080
+ lastActivityAt() {
57081
+ return lastActivityTs;
57082
+ },
57039
57083
  markContextDirty() {
57040
57084
  contextDirty = true;
57041
57085
  refreshContextNow();
@@ -57497,7 +57541,7 @@ function pidAlive(pid) {
57497
57541
  // src/multi.ts
57498
57542
  init_dist();
57499
57543
  init_src5();
57500
- import { readFileSync as readFileSync27, unlinkSync as unlinkSync3, writeFileSync as writeFileSync22 } from "node:fs";
57544
+ import { existsSync as existsSync29, readFileSync as readFileSync27, unlinkSync as unlinkSync3, writeFileSync as writeFileSync22 } from "node:fs";
57501
57545
 
57502
57546
  // src/principle-sync.ts
57503
57547
  init_src5();
@@ -58282,6 +58326,7 @@ var ConsolidateWorker = class {
58282
58326
  // HZ-footprint-cap — see pass-worker-client.ts for the contract.
58283
58327
  resourceLimits: workerResourceLimits()
58284
58328
  });
58329
+ noteIsolateSpawned();
58285
58330
  this.worker = worker;
58286
58331
  this.ready = new Promise((resolve6, reject) => {
58287
58332
  const onReady = (m) => {
@@ -58314,7 +58359,10 @@ var ConsolidateWorker = class {
58314
58359
  this.ready = null;
58315
58360
  };
58316
58361
  worker.on("error", onGone);
58317
- worker.on("exit", () => onGone());
58362
+ worker.on("exit", () => {
58363
+ noteIsolateExited();
58364
+ onGone();
58365
+ });
58318
58366
  }
58319
58367
  async stop() {
58320
58368
  this.stopped = true;
@@ -58741,6 +58789,7 @@ async function startMultiDaemon(opts = {}) {
58741
58789
  startLoopLagMonitor();
58742
58790
  let baseUrl = "";
58743
58791
  const records = [];
58792
+ const mountedIds = /* @__PURE__ */ new Set();
58744
58793
  const machineDominantLanguage = () => {
58745
58794
  const langCounts = /* @__PURE__ */ new Map();
58746
58795
  for (const r of records) {
@@ -58851,7 +58900,7 @@ async function startMultiDaemon(opts = {}) {
58851
58900
  fireBoundary();
58852
58901
  }
58853
58902
  });
58854
- return { entry, engine, webApp, root: entry.path, id };
58903
+ return { entry, engine, webApp, root: entry.path, id, lastHookAt: Date.now() };
58855
58904
  };
58856
58905
  const pruned = pruneMissingWorkspaces();
58857
58906
  if (pruned.length > 0) {
@@ -58885,7 +58934,10 @@ async function startMultiDaemon(opts = {}) {
58885
58934
  }
58886
58935
  records.push(rec);
58887
58936
  void ambientLinkAll();
58888
- app.route(`/ws/${rec.id}`, rec.webApp);
58937
+ if (!mountedIds.has(rec.id)) {
58938
+ mountedIds.add(rec.id);
58939
+ app.route(`/ws/${rec.id}`, rec.webApp);
58940
+ }
58889
58941
  try {
58890
58942
  writeFileSync22(
58891
58943
  rec.engine.paths.daemonLock,
@@ -59000,11 +59052,20 @@ async function startMultiDaemon(opts = {}) {
59000
59052
  } catch {
59001
59053
  return c.json({ error: "bad-json" }, 400);
59002
59054
  }
59003
- const owner = ownerOf(records, pathInBody(body2));
59055
+ let owner = ownerOf(records, pathInBody(body2));
59056
+ if (!owner) {
59057
+ const missPath = pathInBody(body2);
59058
+ const gitRoot = missPath ? findGitRoot(missPath) : null;
59059
+ if (gitRoot && attachWorkspace(gitRoot).attached) {
59060
+ owner = ownerOf(records, missPath) ?? null;
59061
+ if (owner) console.log(`[errata] woke idle workspace ${owner.entry.name}`);
59062
+ }
59063
+ }
59004
59064
  if (!owner) {
59005
59065
  rootAdopter.noteMiss(pathInBody(body2));
59006
59066
  return c.json({ error: "no-matching-project" }, 404);
59007
59067
  }
59068
+ owner.lastHookAt = Date.now();
59008
59069
  const sub = c.req.path;
59009
59070
  const done = markPass(`${sub}:${owner.entry.name}`);
59010
59071
  try {
@@ -59040,7 +59101,37 @@ async function startMultiDaemon(opts = {}) {
59040
59101
  app.post("/api/message", (c) => forward(c, cwdOf));
59041
59102
  app.post("/api/session-end", (c) => forward(c, cwdOf));
59042
59103
  app.post("/api/context-inject", (c) => forward(c, cwdOf));
59043
- for (const r of records) app.route(`/ws/${r.id}`, r.webApp);
59104
+ for (const r of records) {
59105
+ mountedIds.add(r.id);
59106
+ app.route(`/ws/${r.id}`, r.webApp);
59107
+ }
59108
+ const retireWorkspace = async (rec, reason) => {
59109
+ const idx = records.indexOf(rec);
59110
+ if (idx === -1) return;
59111
+ records.splice(idx, 1);
59112
+ try {
59113
+ await rec.engine.stop();
59114
+ } catch (err2) {
59115
+ console.warn(`[errata] retire ${rec.entry.name}: engine.stop failed \u2014 ${err2 instanceof Error ? err2.message : err2}`);
59116
+ }
59117
+ console.log(
59118
+ `[errata] retired ${rec.entry.name} (${reason})` + (reason === "idle" ? " \u2014 idle; the next hook from it re-attaches" : "")
59119
+ );
59120
+ };
59121
+ const sweepIdleWorkspaces = () => {
59122
+ const now = Date.now();
59123
+ for (const rec of [...records]) {
59124
+ let lastActiveAt = rec.lastHookAt;
59125
+ try {
59126
+ lastActiveAt = Math.max(lastActiveAt, rec.engine.lastActivityAt());
59127
+ } catch {
59128
+ }
59129
+ const reason = retireDecision({ exists: existsSync29(rec.root), lastActiveAt, now });
59130
+ if (reason) void retireWorkspace(rec, reason);
59131
+ }
59132
+ };
59133
+ const idleSweep = setInterval(sweepIdleWorkspaces, WORKSPACE_SWEEP_INTERVAL_MS);
59134
+ idleSweep.unref?.();
59044
59135
  const reindexAll = async (rOpts) => {
59045
59136
  const out2 = /* @__PURE__ */ new Map();
59046
59137
  const toIndex = records.filter((r) => rOpts?.force || r.engine.store.nodeCount() === 0);
@@ -59646,6 +59737,7 @@ async function startMultiDaemon(opts = {}) {
59646
59737
  boundaryListeners.push(cb);
59647
59738
  },
59648
59739
  async stop() {
59740
+ clearInterval(idleSweep);
59649
59741
  try {
59650
59742
  const cur = readFileSync27(lockPath, "utf8");
59651
59743
  if (JSON.parse(cur).pid === process.pid) unlinkSync3(lockPath);
@@ -60678,21 +60770,21 @@ async function cmdInit() {
60678
60770
  if (!skipHooks) {
60679
60771
  console.log("");
60680
60772
  console.log("installing harness hooks...");
60681
- const { existsSync: existsSync30 } = await import("node:fs");
60773
+ const { existsSync: existsSync31 } = await import("node:fs");
60682
60774
  const { join: join33 } = await import("node:path");
60683
60775
  try {
60684
60776
  await installClaudeHooks(port);
60685
60777
  } catch (err2) {
60686
60778
  console.warn(` \u26A0\uFE0F Claude hook install failed: ${err2 instanceof Error ? err2.message : err2}`);
60687
60779
  }
60688
- if (existsSync30(join33(ROOT, ".cursor"))) {
60780
+ if (existsSync31(join33(ROOT, ".cursor"))) {
60689
60781
  try {
60690
60782
  await installCursorMcpConfig();
60691
60783
  } catch (err2) {
60692
60784
  console.warn(` \u26A0\uFE0F Cursor MCP config failed: ${err2 instanceof Error ? err2.message : err2}`);
60693
60785
  }
60694
60786
  }
60695
- if (existsSync30(join33(ROOT, ".codex"))) {
60787
+ if (existsSync31(join33(ROOT, ".codex"))) {
60696
60788
  try {
60697
60789
  await installCodexHooks(port);
60698
60790
  } catch (err2) {
@@ -60849,9 +60941,9 @@ async function cmdStatus() {
60849
60941
  console.log(` stack: ${profile.stack.join(", ") || "(none)"}`);
60850
60942
  console.log(` languages: ${profile.languages.join(", ") || "(none)"}`);
60851
60943
  }
60852
- console.log(` graph db: ${existsSync29(paths.castalia) ? "yes" : "no"} (${paths.castalia})`);
60853
- console.log(` event log: ${existsSync29(paths.eventLog) ? "yes" : "no"} (${paths.eventLog})`);
60854
- if (existsSync29(paths.castalia)) {
60944
+ console.log(` graph db: ${existsSync30(paths.castalia) ? "yes" : "no"} (${paths.castalia})`);
60945
+ console.log(` event log: ${existsSync30(paths.eventLog) ? "yes" : "no"} (${paths.eventLog})`);
60946
+ if (existsSync30(paths.castalia)) {
60855
60947
  try {
60856
60948
  const { openGraphStore: openGraphStore2 } = await Promise.resolve().then(() => (init_src5(), src_exports2));
60857
60949
  const store = openGraphStore2({ path: paths.castalia });
@@ -61532,7 +61624,7 @@ function cmdInstallationProfile(args2) {
61532
61624
  }
61533
61625
  async function cmdReview() {
61534
61626
  const paths = workspacePaths(ROOT);
61535
- if (!existsSync29(paths.reviewQueue)) {
61627
+ if (!existsSync30(paths.reviewQueue)) {
61536
61628
  console.log("(review queue empty)");
61537
61629
  return;
61538
61630
  }
@@ -62207,7 +62299,7 @@ async function gatherRepo(store, ws) {
62207
62299
  };
62208
62300
  }
62209
62301
  async function gatherReportData(generatedAt) {
62210
- const { existsSync: existsSync30 } = await import("node:fs");
62302
+ const { existsSync: existsSync31 } = await import("node:fs");
62211
62303
  const { openGraphStore: openGraphStore2 } = await Promise.resolve().then(() => (init_src5(), src_exports2));
62212
62304
  const cfg = loadConfig();
62213
62305
  const outbound = cfg.consent.sync ? "auto" : "off";
@@ -62215,7 +62307,7 @@ async function gatherReportData(generatedAt) {
62215
62307
  for (const ws of listWorkspaces()) {
62216
62308
  if (ws.missing) continue;
62217
62309
  const dbPath = workspacePaths(ws.path).castalia;
62218
- if (!existsSync30(dbPath)) continue;
62310
+ if (!existsSync31(dbPath)) continue;
62219
62311
  let store = null;
62220
62312
  try {
62221
62313
  store = openGraphStore2({ path: dbPath });
@@ -62387,13 +62479,13 @@ function hookRelayCommand(port, path2) {
62387
62479
  return process.platform === "win32" ? `cmd //c "curl -s --connect-timeout 1 --max-time 2 -X POST -H \\"Content-Type: application/json\\" --data-binary @- ${url2} 2>NUL || echo {}"` : `curl -s --connect-timeout 1 --max-time 2 -X POST -H 'Content-Type: application/json' --data-binary @- ${url2} 2>/dev/null || echo '{}'`;
62388
62480
  }
62389
62481
  async function installClaudeHooks(port) {
62390
- const { mkdirSync: mkdirSync8, existsSync: existsSync30, readFileSync: readFileSync29, writeFileSync: writeFileSync23 } = await import("node:fs");
62482
+ const { mkdirSync: mkdirSync8, existsSync: existsSync31, readFileSync: readFileSync29, writeFileSync: writeFileSync23 } = await import("node:fs");
62391
62483
  const { join: join33 } = await import("node:path");
62392
62484
  const dir = join33(ROOT, ".claude");
62393
- if (!existsSync30(dir)) mkdirSync8(dir, { recursive: true });
62485
+ if (!existsSync31(dir)) mkdirSync8(dir, { recursive: true });
62394
62486
  const file2 = join33(dir, "settings.json");
62395
62487
  let settings = {};
62396
- if (existsSync30(file2)) {
62488
+ if (existsSync31(file2)) {
62397
62489
  try {
62398
62490
  settings = JSON.parse(readFileSync29(file2, "utf8"));
62399
62491
  } catch {
@@ -62456,13 +62548,13 @@ async function installClaudeHooks(port) {
62456
62548
  console.log(` endpoint: http://127.0.0.1:${port}/api/hook`);
62457
62549
  }
62458
62550
  async function installClaudeMcpConfig() {
62459
- const { mkdirSync: mkdirSync8, existsSync: existsSync30, readFileSync: readFileSync29, writeFileSync: writeFileSync23 } = await import("node:fs");
62551
+ const { mkdirSync: mkdirSync8, existsSync: existsSync31, readFileSync: readFileSync29, writeFileSync: writeFileSync23 } = await import("node:fs");
62460
62552
  const { join: join33, dirname: dirname11 } = await import("node:path");
62461
62553
  const file2 = join33(ROOT, ".mcp.json");
62462
62554
  const dir = dirname11(file2);
62463
- if (!existsSync30(dir)) mkdirSync8(dir, { recursive: true });
62555
+ if (!existsSync31(dir)) mkdirSync8(dir, { recursive: true });
62464
62556
  let cfg = {};
62465
- if (existsSync30(file2)) {
62557
+ if (existsSync31(file2)) {
62466
62558
  try {
62467
62559
  cfg = JSON.parse(readFileSync29(file2, "utf8"));
62468
62560
  } catch {
@@ -62478,13 +62570,13 @@ async function installClaudeMcpConfig() {
62478
62570
  console.log(` Tools: errata.search / locate / neighbors / callers_of / what_uses / show / similar`);
62479
62571
  }
62480
62572
  async function installCursorMcpConfig() {
62481
- const { mkdirSync: mkdirSync8, existsSync: existsSync30, readFileSync: readFileSync29, writeFileSync: writeFileSync23 } = await import("node:fs");
62573
+ const { mkdirSync: mkdirSync8, existsSync: existsSync31, readFileSync: readFileSync29, writeFileSync: writeFileSync23 } = await import("node:fs");
62482
62574
  const { join: join33 } = await import("node:path");
62483
62575
  const dir = join33(ROOT, ".cursor");
62484
- if (!existsSync30(dir)) mkdirSync8(dir, { recursive: true });
62576
+ if (!existsSync31(dir)) mkdirSync8(dir, { recursive: true });
62485
62577
  const file2 = join33(dir, "mcp.json");
62486
62578
  let cfg = {};
62487
- if (existsSync30(file2)) {
62579
+ if (existsSync31(file2)) {
62488
62580
  try {
62489
62581
  cfg = JSON.parse(readFileSync29(file2, "utf8"));
62490
62582
  } catch {
@@ -62502,15 +62594,15 @@ async function installCursorMcpConfig() {
62502
62594
  console.log(` \u26A0\uFE0F reload Cursor (Cmd/Ctrl-Shift-P \u2192 "Reload Window") to pick it up.`);
62503
62595
  }
62504
62596
  async function installCodexHooks(port) {
62505
- const { mkdirSync: mkdirSync8, existsSync: existsSync30, readFileSync: readFileSync29, writeFileSync: writeFileSync23 } = await import("node:fs");
62597
+ const { mkdirSync: mkdirSync8, existsSync: existsSync31, readFileSync: readFileSync29, writeFileSync: writeFileSync23 } = await import("node:fs");
62506
62598
  const { join: join33 } = await import("node:path");
62507
62599
  const dir = join33(ROOT, ".codex");
62508
- if (!existsSync30(dir)) mkdirSync8(dir, { recursive: true });
62600
+ if (!existsSync31(dir)) mkdirSync8(dir, { recursive: true });
62509
62601
  const file2 = join33(dir, "config.toml");
62510
62602
  const BEGIN = `# >>> errata hooks (errata-managed)`;
62511
62603
  const END = `# <<< errata hooks`;
62512
62604
  let existing = "";
62513
- if (existsSync30(file2)) {
62605
+ if (existsSync31(file2)) {
62514
62606
  existing = readFileSync29(file2, "utf8");
62515
62607
  const beginIdx = existing.indexOf(BEGIN);
62516
62608
  const endIdx = existing.indexOf(END);
@@ -62692,6 +62784,7 @@ function installLogTimestamps() {
62692
62784
  }
62693
62785
  }
62694
62786
  async function cmdDash(args2) {
62787
+ process.title = "errata-daemon";
62695
62788
  installLogTimestamps();
62696
62789
  const portIdx = args2.indexOf("--port");
62697
62790
  const port = portIdx >= 0 && args2[portIdx + 1] ? Number(args2[portIdx + 1]) : 7891;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.2-dev.698",
3
+ "version": "2.0.2-dev.704",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {