@inerrata-corporation/errata 2.0.17-dev.3426 → 2.0.17-dev.3435

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.
@@ -15884,13 +15884,13 @@ function loadNodeSqlite() {
15884
15884
  process.emitWarning = original;
15885
15885
  }
15886
15886
  }
15887
- function openDatabase(path) {
15888
- const db = new DatabaseSync(path);
15887
+ function openDatabase(path, opts) {
15888
+ const db = opts ? new DatabaseSync(path, opts) : new DatabaseSync(path);
15889
15889
  try {
15890
15890
  db.exec("PRAGMA foreign_keys = ON");
15891
15891
  } catch {
15892
15892
  }
15893
- if (path !== ":memory:") {
15893
+ if (path !== ":memory:" && !opts?.readOnly) {
15894
15894
  try {
15895
15895
  db.exec("PRAGMA journal_mode = WAL");
15896
15896
  db.exec("PRAGMA synchronous = NORMAL");
@@ -16157,8 +16157,9 @@ init_src2();
16157
16157
  init_src();
16158
16158
 
16159
16159
  // ../../packages/local-graph/src/history-cache-floor.ts
16160
+ var HISTORY_CACHE_FLOOR_KEY = "history-cache-withdrawal-floor-3";
16160
16161
  function installHistoryCacheFloor(db) {
16161
- const key = "history-cache-withdrawal-floor-3";
16162
+ const key = HISTORY_CACHE_FLOOR_KEY;
16162
16163
  if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(key)) return;
16163
16164
  db.transaction(() => {
16164
16165
  db.exec("DROP TRIGGER IF EXISTS history_cache_nodes_insert; DROP TRIGGER IF EXISTS history_cache_nodes_update;");
@@ -16320,6 +16321,7 @@ function localEdgeViolation(fromLabel, type, toLabel) {
16320
16321
  return verdict.ok ? null : verdict.reason ?? "edge rule violation";
16321
16322
  }
16322
16323
  var SCHEMA_VERSION = 6;
16324
+ var SESSION_REPAIR_KEY = "session-evidence-v1";
16323
16325
  var SESSION_EVIDENCE_PREDICATE = SESSION_EVIDENCE_FIELDS.map((field) => `instr(attrs_json, '${JSON.stringify(field)}') > 0`).join(" OR ");
16324
16326
  var SCHEMA_SQL = `
16325
16327
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -16531,10 +16533,10 @@ var SqliteGraphStore = class {
16531
16533
  }
16532
16534
  this.db.exec(TEMPORAL_INDEX_SQL);
16533
16535
  installHistoryCacheFloor(this.db);
16534
- if (!this.db.prepare("SELECT value FROM store_meta WHERE key = 'session-evidence-v1'").get()) {
16536
+ if (!this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(SESSION_REPAIR_KEY)) {
16535
16537
  this.db.transaction(() => {
16536
16538
  this.repairSessionAliases();
16537
- this.db.prepare("INSERT OR REPLACE INTO store_meta (key, value) VALUES ('session-evidence-v1', 'done')").run();
16539
+ this.db.prepare("INSERT OR REPLACE INTO store_meta (key, value) VALUES (?, 'done')").run(SESSION_REPAIR_KEY);
16538
16540
  });
16539
16541
  }
16540
16542
  this.stmts = {
@@ -16729,6 +16731,34 @@ var SqliteGraphStore = class {
16729
16731
  const seqRow = this.db.prepare("SELECT value FROM store_meta WHERE key = 'ingest_seq'").get();
16730
16732
  this.ingestSeq = seqRow ? Number(seqRow.value) : 0;
16731
16733
  }
16734
+ strandedSymbols() {
16735
+ return this.db.prepare(`
16736
+ SELECT f.id AS fileId, e.to_id AS symbolId
16737
+ -- Start from the small File population. An unconstrained JOIN lets
16738
+ -- SQLite start at edges_live and visit every live edge in the graph.
16739
+ FROM nodes f CROSS JOIN edges e ON e.from_id = f.id
16740
+ LEFT JOIN nodes t ON t.id = e.to_id
16741
+ WHERE f.label = 'File' AND f.valid_to IS NULL AND e.valid_to IS NULL
16742
+ AND e.type IN ('DEFINES', 'CONTAINS')
16743
+ AND (t.id IS NULL OR t.valid_to IS NOT NULL)
16744
+ `).all();
16745
+ }
16746
+ fileIndexStates(ids) {
16747
+ if (ids.length === 0) return [];
16748
+ if (ids.length > 500) throw new RangeError("fileIndexStates accepts at most 500 ids per read");
16749
+ const rows = this.db.prepare(`
16750
+ SELECT f.id, f.last_updated_at AS lastUpdatedAt, f.valid_to AS validTo,
16751
+ CASE WHEN count(e.id) = 0 THEN
16752
+ (json_type(f.attrs_json, '$.symbolCount') IN ('integer','real') AND json_extract(f.attrs_json, '$.symbolCount') = 0)
16753
+ OR json_type(f.attrs_json, '$.indexSkipped') = 'text'
16754
+ ELSE max(CASE WHEN t.id IS NULL OR t.valid_to IS NOT NULL THEN 1 ELSE 0 END) = 0 END AS complete
16755
+ FROM nodes f
16756
+ LEFT JOIN edges e ON e.from_id = f.id AND e.valid_to IS NULL AND e.type IN ('DEFINES','CONTAINS')
16757
+ LEFT JOIN nodes t ON t.id = e.to_id
16758
+ WHERE f.id IN (${ids.map(() => "?").join(",")}) GROUP BY f.id
16759
+ `).all(...ids);
16760
+ return rows.map((row) => ({ ...row, complete: row.complete === 1 }));
16761
+ }
16732
16762
  /** Forward-migrate the on-disk schema to the current SCHEMA_VERSION. */
16733
16763
  migrate(from) {
16734
16764
  this.db.transaction(() => {
@@ -18265,17 +18295,8 @@ var STALL_MIN_SPAN_MS = 60 * 60 * 1e3;
18265
18295
 
18266
18296
  // src/reconcile.ts
18267
18297
  init_src3();
18268
- function isLive(n) {
18269
- return n != null && (n.validTo ?? null) === null;
18270
- }
18271
18298
  function findStrandedSymbols(store) {
18272
- const stranded = [];
18273
- for (const file2 of store.findNodesByLabel("File")) {
18274
- for (const e of store.outEdges(file2.id, ["DEFINES", "CONTAINS"])) {
18275
- if (!isLive(store.getNode(e.to))) stranded.push({ fileId: file2.id, symbolId: e.to });
18276
- }
18277
- }
18278
- return stranded;
18299
+ return store.strandedSymbols();
18279
18300
  }
18280
18301
 
18281
18302
  // src/package-index.ts
@@ -19592,7 +19613,8 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19592
19613
  sidecarNoLexicon: 0,
19593
19614
  sidecarNoSummary: 0,
19594
19615
  fileRefsSubstituted: 0,
19595
- fileRefLexiconMisses: 0
19616
+ fileRefLexiconMisses: 0,
19617
+ collapsedEdges: 0
19596
19618
  };
19597
19619
  const sharedCauses = /* @__PURE__ */ new Map();
19598
19620
  const causalQueue = store.findNodesByLabel("Problem").filter(
@@ -19817,7 +19839,13 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19817
19839
  ];
19818
19840
  const dedup = /* @__PURE__ */ new Map();
19819
19841
  for (const e of rawOuts) dedup.set(`${e.type}>${e.to}`, e);
19820
- const outs = [...dedup.values()].map((e) => ({ e, wireTo: semanticEndpoints.get(e.to) })).filter((x) => x.wireTo != null);
19842
+ const outs = [...dedup.values()].map((e) => ({ e, wireTo: semanticEndpoints.get(e.to) })).filter((x) => {
19843
+ if (x.wireTo === wireFrom) {
19844
+ composition.collapsedEdges++;
19845
+ return false;
19846
+ }
19847
+ return x.wireTo != null;
19848
+ });
19821
19849
  if (outs.length === 0) continue;
19822
19850
  const dg = digest(outs.map(({ e, wireTo }) => `${e.type}>${wireTo}`).sort());
19823
19851
  const owner = nodeOf(sourceId);
package/errata.mjs CHANGED
@@ -17838,13 +17838,13 @@ function loadNodeSqlite() {
17838
17838
  process.emitWarning = original;
17839
17839
  }
17840
17840
  }
17841
- function openDatabase(path2) {
17842
- const db = new DatabaseSync(path2);
17841
+ function openDatabase(path2, opts) {
17842
+ const db = opts ? new DatabaseSync(path2, opts) : new DatabaseSync(path2);
17843
17843
  try {
17844
17844
  db.exec("PRAGMA foreign_keys = ON");
17845
17845
  } catch {
17846
17846
  }
17847
- if (path2 !== ":memory:") {
17847
+ if (path2 !== ":memory:" && !opts?.readOnly) {
17848
17848
  try {
17849
17849
  db.exec("PRAGMA journal_mode = WAL");
17850
17850
  db.exec("PRAGMA synchronous = NORMAL");
@@ -17945,7 +17945,7 @@ var init_src2 = __esm({
17945
17945
 
17946
17946
  // ../../packages/local-graph/src/history-cache-floor.ts
17947
17947
  function installHistoryCacheFloor(db) {
17948
- const key = "history-cache-withdrawal-floor-3";
17948
+ const key = HISTORY_CACHE_FLOOR_KEY;
17949
17949
  if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(key)) return;
17950
17950
  db.transaction(() => {
17951
17951
  db.exec("DROP TRIGGER IF EXISTS history_cache_nodes_insert; DROP TRIGGER IF EXISTS history_cache_nodes_update;");
@@ -18001,9 +18001,11 @@ function installHistoryCacheFloor(db) {
18001
18001
  db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(key, "installed");
18002
18002
  });
18003
18003
  }
18004
+ var HISTORY_CACHE_FLOOR_KEY;
18004
18005
  var init_history_cache_floor = __esm({
18005
18006
  "../../packages/local-graph/src/history-cache-floor.ts"() {
18006
18007
  "use strict";
18008
+ HISTORY_CACHE_FLOOR_KEY = "history-cache-withdrawal-floor-3";
18007
18009
  }
18008
18010
  });
18009
18011
 
@@ -18095,6 +18097,23 @@ function localEdgeViolation(fromLabel, type, toLabel) {
18095
18097
  const verdict = isValidEdge(fromLabel, type, toLabel);
18096
18098
  return verdict.ok ? null : verdict.reason ?? "edge rule violation";
18097
18099
  }
18100
+ function graphStoreNeedsPreparation(path2) {
18101
+ if (path2 === ":memory:") return false;
18102
+ let db;
18103
+ try {
18104
+ db = openDatabase(path2, { readOnly: true });
18105
+ if (db.prepare("SELECT version FROM schema_version LIMIT 1").get()?.["version"] !== SCHEMA_VERSION) return true;
18106
+ const indexes = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type = 'index'").all().map((r) => r["name"]));
18107
+ for (const match2 of (SCHEMA_SQL + TEMPORAL_INDEX_SQL).matchAll(/CREATE INDEX IF NOT EXISTS (\w+)/g)) {
18108
+ if (!indexes.has(match2[1])) return true;
18109
+ }
18110
+ return [SESSION_REPAIR_KEY, HISTORY_CACHE_FLOOR_KEY].some((key) => !db.prepare("SELECT value FROM store_meta WHERE key = ?").get(key));
18111
+ } catch {
18112
+ return true;
18113
+ } finally {
18114
+ db?.close();
18115
+ }
18116
+ }
18098
18117
  function encodeEmbedding(emb) {
18099
18118
  if (!emb || emb.length === 0) return null;
18100
18119
  const f = Float32Array.from(emb);
@@ -18155,7 +18174,7 @@ function rowToEdge(r) {
18155
18174
  function openGraphStore(opts) {
18156
18175
  return new SqliteGraphStore(opts);
18157
18176
  }
18158
- var LOCAL_RULE_OVERRIDES, SCHEMA_VERSION, SESSION_EVIDENCE_PREDICATE, SCHEMA_SQL, TEMPORAL_INDEX_SQL, FINGERPRINT_INDEXED_LABELS, SqliteGraphStore;
18177
+ var LOCAL_RULE_OVERRIDES, SCHEMA_VERSION, SESSION_REPAIR_KEY, SESSION_EVIDENCE_PREDICATE, SCHEMA_SQL, TEMPORAL_INDEX_SQL, FINGERPRINT_INDEXED_LABELS, SqliteGraphStore;
18159
18178
  var init_store = __esm({
18160
18179
  "../../packages/local-graph/src/store.ts"() {
18161
18180
  "use strict";
@@ -18186,6 +18205,7 @@ var init_store = __esm({
18186
18205
  REGRESSION_OF: { from: ["Problem"], to: ["Problem"] }
18187
18206
  };
18188
18207
  SCHEMA_VERSION = 6;
18208
+ SESSION_REPAIR_KEY = "session-evidence-v1";
18189
18209
  SESSION_EVIDENCE_PREDICATE = SESSION_EVIDENCE_FIELDS.map((field) => `instr(attrs_json, '${JSON.stringify(field)}') > 0`).join(" OR ");
18190
18210
  SCHEMA_SQL = `
18191
18211
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -18340,10 +18360,10 @@ CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path ON nodes(json_extract(attrs_js
18340
18360
  }
18341
18361
  this.db.exec(TEMPORAL_INDEX_SQL);
18342
18362
  installHistoryCacheFloor(this.db);
18343
- if (!this.db.prepare("SELECT value FROM store_meta WHERE key = 'session-evidence-v1'").get()) {
18363
+ if (!this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(SESSION_REPAIR_KEY)) {
18344
18364
  this.db.transaction(() => {
18345
18365
  this.repairSessionAliases();
18346
- this.db.prepare("INSERT OR REPLACE INTO store_meta (key, value) VALUES ('session-evidence-v1', 'done')").run();
18366
+ this.db.prepare("INSERT OR REPLACE INTO store_meta (key, value) VALUES (?, 'done')").run(SESSION_REPAIR_KEY);
18347
18367
  });
18348
18368
  }
18349
18369
  this.stmts = {
@@ -18538,6 +18558,34 @@ CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path ON nodes(json_extract(attrs_js
18538
18558
  const seqRow = this.db.prepare("SELECT value FROM store_meta WHERE key = 'ingest_seq'").get();
18539
18559
  this.ingestSeq = seqRow ? Number(seqRow.value) : 0;
18540
18560
  }
18561
+ strandedSymbols() {
18562
+ return this.db.prepare(`
18563
+ SELECT f.id AS fileId, e.to_id AS symbolId
18564
+ -- Start from the small File population. An unconstrained JOIN lets
18565
+ -- SQLite start at edges_live and visit every live edge in the graph.
18566
+ FROM nodes f CROSS JOIN edges e ON e.from_id = f.id
18567
+ LEFT JOIN nodes t ON t.id = e.to_id
18568
+ WHERE f.label = 'File' AND f.valid_to IS NULL AND e.valid_to IS NULL
18569
+ AND e.type IN ('DEFINES', 'CONTAINS')
18570
+ AND (t.id IS NULL OR t.valid_to IS NOT NULL)
18571
+ `).all();
18572
+ }
18573
+ fileIndexStates(ids) {
18574
+ if (ids.length === 0) return [];
18575
+ if (ids.length > 500) throw new RangeError("fileIndexStates accepts at most 500 ids per read");
18576
+ const rows = this.db.prepare(`
18577
+ SELECT f.id, f.last_updated_at AS lastUpdatedAt, f.valid_to AS validTo,
18578
+ CASE WHEN count(e.id) = 0 THEN
18579
+ (json_type(f.attrs_json, '$.symbolCount') IN ('integer','real') AND json_extract(f.attrs_json, '$.symbolCount') = 0)
18580
+ OR json_type(f.attrs_json, '$.indexSkipped') = 'text'
18581
+ ELSE max(CASE WHEN t.id IS NULL OR t.valid_to IS NOT NULL THEN 1 ELSE 0 END) = 0 END AS complete
18582
+ FROM nodes f
18583
+ LEFT JOIN edges e ON e.from_id = f.id AND e.valid_to IS NULL AND e.type IN ('DEFINES','CONTAINS')
18584
+ LEFT JOIN nodes t ON t.id = e.to_id
18585
+ WHERE f.id IN (${ids.map(() => "?").join(",")}) GROUP BY f.id
18586
+ `).all(...ids);
18587
+ return rows.map((row) => ({ ...row, complete: row.complete === 1 }));
18588
+ }
18541
18589
  /** Forward-migrate the on-disk schema to the current SCHEMA_VERSION. */
18542
18590
  migrate(from) {
18543
18591
  this.db.transaction(() => {
@@ -23353,7 +23401,7 @@ function triageOf(l2, q, mathOpts) {
23353
23401
  if (!triage) return { triage: "", presenting: presentingId, routes: [] };
23354
23402
  const seen = triage.attrs["perContextSeen"] ?? {};
23355
23403
  const buckets = q.context ? triageContextBuckets(canonicalizeContext(q.context)) : [];
23356
- const active2 = new Set(q.activeProblemIds ?? []);
23404
+ const active3 = new Set(q.activeProblemIds ?? []);
23357
23405
  const routes = [];
23358
23406
  for (const e of l2.outEdges(triageId, [...ROUTE_EDGE_TYPES])) {
23359
23407
  const perContext = e.attrs["perContext"] ?? {};
@@ -23374,13 +23422,13 @@ function triageOf(l2, q, mathOpts) {
23374
23422
  }
23375
23423
  const post = triagePosterior(counts, buckets, mathOpts);
23376
23424
  let probability = post.probability;
23377
- if (active2.size > 0) {
23425
+ if (active3.size > 0) {
23378
23426
  const neighbors = /* @__PURE__ */ new Set([
23379
23427
  ...l2.outEdges(e.to).map((x) => x.to),
23380
23428
  ...l2.inEdges(e.to).map((x) => x.from)
23381
23429
  ]);
23382
23430
  let hits = 0;
23383
- for (const id of active2) if (neighbors.has(id)) hits++;
23431
+ for (const id of active3) if (neighbors.has(id)) hits++;
23384
23432
  if (hits > 0) probability = Math.min(1, probability * (1 + COOCCUR_BONUS * Math.tanh(hits)));
23385
23433
  }
23386
23434
  const cause = l2.getNode(e.to);
@@ -24990,6 +25038,7 @@ __export(src_exports2, {
24990
25038
  foldSameAnchorProblem: () => foldSameAnchorProblem,
24991
25039
  formatMechanismStatus: () => formatMechanismStatus,
24992
25040
  getClaim: () => getClaim,
25041
+ graphStoreNeedsPreparation: () => graphStoreNeedsPreparation,
24993
25042
  harvestAbstractionFences: () => harvestAbstractionFences,
24994
25043
  harvestTriageFences: () => harvestTriageFences,
24995
25044
  hasStarvedMechanism: () => hasStarvedMechanism,
@@ -25123,7 +25172,7 @@ function buildSnapshot(opts) {
25123
25172
  node: p,
25124
25173
  anchors: opts.store.outEdges(p.id, ["MANIFESTED_IN", "EVIDENCED_BY"])
25125
25174
  }));
25126
- const recent3 = openPool.slice(0, RECENT_OPEN);
25175
+ const recent4 = openPool.slice(0, RECENT_OPEN);
25127
25176
  const constraintPool = stillOpen.filter((p) => isConstraintProblem(p)).sort((a, b) => b.lastUpdatedAt - a.lastUpdatedAt).slice(0, RECENT_CONSTRAINTS * SCOPE_POOL_FACTOR);
25128
25177
  const recentConstraints = constraintPool.slice(0, RECENT_CONSTRAINTS);
25129
25178
  const regressions = problems.filter((p) => p.attrs["resolvedAt"] == null && p.attrs["mergedInto"] === void 0).flatMap(
@@ -25195,10 +25244,10 @@ function buildSnapshot(opts) {
25195
25244
  packages: opts.profile.stack.map((name2) => ({ name: name2, node: matchNode(pkgNodes, pkgBase(name2)) })),
25196
25245
  tools: toolNodes.map((node2) => ({ name: String(node2.attrs["name"] ?? node2.description), node: node2 }))
25197
25246
  };
25198
- const causalNudge = recent3.length >= 2 && recent3.every(
25247
+ const causalNudge = recent4.length >= 2 && recent4.every(
25199
25248
  (r) => opts.store.outEdges(r.node.id, ["CAUSED_BY"]).length === 0 && opts.store.inEdges(r.node.id, ["CAUSED_BY"]).length === 0
25200
25249
  );
25201
- const abstractUndomained = recent3.filter((r) => {
25250
+ const abstractUndomained = recent4.filter((r) => {
25202
25251
  const grounded = opts.store.outEdges(r.node.id, ["ANCHORED_AT", "OCCURS_IN", "DEPENDS_ON"]).length > 0;
25203
25252
  const hasDomain = opts.store.outEdges(r.node.id, ["PERTAIN_TO"]).length > 0;
25204
25253
  return !grounded && !hasDomain;
@@ -25207,7 +25256,7 @@ function buildSnapshot(opts) {
25207
25256
  return {
25208
25257
  profile: opts.profile,
25209
25258
  profileContext,
25210
- recentProblems: recent3,
25259
+ recentProblems: recent4,
25211
25260
  recentResolved,
25212
25261
  recentConstraints,
25213
25262
  regressions,
@@ -26597,11 +26646,27 @@ var init_client = __esm({
26597
26646
  }
26598
26647
  }
26599
26648
  for (const edgeChunk of chunkArray(pendingEdges, MAX_EDGES_PER_PAYLOAD)) {
26600
- const rewritten = edgeChunk.map((e) => ({
26649
+ const projected = edgeChunk.map((e) => ({
26601
26650
  ...e,
26602
26651
  from: echoedCloudId.get(e.from) ?? e.from,
26603
26652
  to: echoedCloudId.get(e.to) ?? e.to
26604
26653
  }));
26654
+ const rewritten = projected.filter((e, i2) => {
26655
+ if (e.from !== e.to) return true;
26656
+ const original = edgeChunk[i2];
26657
+ merged.edges.push({
26658
+ from: original.from,
26659
+ to: original.to,
26660
+ type: original.type,
26661
+ decision: "rejected",
26662
+ reason: "endpoints collapsed to the same cloud node"
26663
+ });
26664
+ return false;
26665
+ });
26666
+ if (rewritten.length === 0) {
26667
+ emit([], 0);
26668
+ continue;
26669
+ }
26605
26670
  const r = await this.ingestWire(toWirePayload({ ...batch, nodes: [], edges: rewritten }, randomUUID()));
26606
26671
  emit([], summarizeIngestResult(r).accepted);
26607
26672
  merged.edges.push(...r.edges);
@@ -43231,58 +43296,32 @@ function listSourceFiles(root) {
43231
43296
  }
43232
43297
  async function findStaleFiles(store, rootPath, workspaceId2) {
43233
43298
  const stale = [];
43234
- const pending = [];
43235
- const allTargets = [];
43299
+ let pending = [];
43300
+ const check2 = () => {
43301
+ const states = new Map(store.fileIndexStates(pending.map((f) => f.id)).map((s) => [s.id, s]));
43302
+ for (const f of pending) {
43303
+ const state = states.get(f.id);
43304
+ if (!state || state.validTo !== null || state.lastUpdatedAt < f.mtimeMs || !state.complete) stale.push(f.abs);
43305
+ }
43306
+ pending = [];
43307
+ };
43236
43308
  let scanned = 0;
43237
43309
  for (const abs of listSourceFiles(rootPath)) {
43238
- if (++scanned % SCAN_YIELD_EVERY === 0) await new Promise((r) => setImmediate(r));
43239
- let mtimeMs;
43240
43310
  try {
43241
- mtimeMs = statSync4(abs).mtimeMs;
43311
+ const rel = relative3(rootPath, abs).split(sep2).join("/");
43312
+ pending.push({ abs, id: fileNodeId(workspaceId2, rel), mtimeMs: statSync4(abs).mtimeMs });
43242
43313
  } catch {
43243
- continue;
43244
43314
  }
43245
- const rel = relative3(rootPath, abs).split(sep2).join("/");
43246
- const fnode = store.getNode(fileNodeId(workspaceId2, rel));
43247
- if (!fnode) {
43248
- stale.push(abs);
43249
- } else if ((fnode.validTo ?? null) !== null) {
43250
- stale.push(abs);
43251
- } else if (fnode.lastUpdatedAt < mtimeMs) {
43252
- stale.push(abs);
43253
- } else {
43254
- const edges = store.outEdges(fnode.id, ["DEFINES", "CONTAINS"]);
43255
- if (edges.length === 0) {
43256
- const converged = fnode.attrs["symbolCount"] === 0 || typeof fnode.attrs["indexSkipped"] === "string";
43257
- if (!converged) stale.push(abs);
43258
- } else {
43259
- const targets = edges.map((e) => e.to);
43260
- pending.push({ abs, targets });
43261
- allTargets.push(...targets);
43262
- }
43315
+ if (++scanned % SCAN_YIELD_EVERY === 0) {
43316
+ check2();
43317
+ await new Promise((resolve9) => setImmediate(resolve9));
43263
43318
  }
43264
43319
  }
43265
- const live3 = /* @__PURE__ */ new Set();
43266
- for (let i2 = 0; i2 < allTargets.length; i2 += LIVENESS_CHUNK) {
43267
- for (const id of store.liveNodeIds(allTargets.slice(i2, i2 + LIVENESS_CHUNK))) live3.add(id);
43268
- if (i2 + LIVENESS_CHUNK < allTargets.length) await new Promise((r) => setImmediate(r));
43269
- }
43270
- for (const { abs, targets } of pending) {
43271
- if (targets.some((id) => !live3.has(id))) stale.push(abs);
43272
- }
43320
+ check2();
43273
43321
  return stale;
43274
43322
  }
43275
- function isLive(n) {
43276
- return n != null && (n.validTo ?? null) === null;
43277
- }
43278
43323
  function findStrandedSymbols(store) {
43279
- const stranded = [];
43280
- for (const file2 of store.findNodesByLabel("File")) {
43281
- for (const e of store.outEdges(file2.id, ["DEFINES", "CONTAINS"])) {
43282
- if (!isLive(store.getNode(e.to))) stranded.push({ fileId: file2.id, symbolId: e.to });
43283
- }
43284
- }
43285
- return stranded;
43324
+ return store.strandedSymbols();
43286
43325
  }
43287
43326
  function reapOrphanedFiles(store, rootPath, workspaceId2, ignoreDirs = []) {
43288
43327
  const ignores = /* @__PURE__ */ new Set([...DEFAULT_IGNORES, ...ignoreDirs]);
@@ -43344,7 +43383,7 @@ async function reconcileStaleFiles(store, rootPath, workspaceId2) {
43344
43383
  }
43345
43384
  return total;
43346
43385
  }
43347
- var IGNORED, SOURCE_RE, SCAN_YIELD_EVERY, LIVENESS_CHUNK, ORPHAN_REAP_MAX_FRACTION, ORPHAN_REAP_MIN_ALLOWANCE;
43386
+ var IGNORED, SOURCE_RE, SCAN_YIELD_EVERY, ORPHAN_REAP_MAX_FRACTION, ORPHAN_REAP_MIN_ALLOWANCE;
43348
43387
  var init_reconcile = __esm({
43349
43388
  "src/reconcile.ts"() {
43350
43389
  "use strict";
@@ -43352,7 +43391,6 @@ var init_reconcile = __esm({
43352
43391
  IGNORED = /[\\/](?:\.git|node_modules|\.errata|dist|__pycache__)(?:[\\/]|$)/;
43353
43392
  SOURCE_RE = /\.(ts|tsx|js|jsx|mjs|cjs|py)$/i;
43354
43393
  SCAN_YIELD_EVERY = 100;
43355
- LIVENESS_CHUNK = 4e3;
43356
43394
  ORPHAN_REAP_MAX_FRACTION = 0.25;
43357
43395
  ORPHAN_REAP_MIN_ALLOWANCE = 50;
43358
43396
  }
@@ -72709,13 +72747,13 @@ function esc3(s) {
72709
72747
  function slug(name2) {
72710
72748
  return name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
72711
72749
  }
72712
- function isLive3(verb) {
72750
+ function isLive2(verb) {
72713
72751
  return LIVE_VERBS.has(verb);
72714
72752
  }
72715
72753
  function nextHtml(n, opts) {
72716
72754
  if (!n) return "";
72717
72755
  const cmd2 = `errata ${n.verb}${n.arg ? " " + n.arg : ""}`;
72718
- const live3 = isLive3(n.verb);
72756
+ const live3 = isLive2(n.verb);
72719
72757
  if (!live3 && !opts.includeFutureVerbs) return "";
72720
72758
  const hint = n.hint ? ` \u2014 ${esc3(n.hint)}` : "";
72721
72759
  const cls = live3 ? "next" : "next soon";
@@ -78352,14 +78390,42 @@ import { Worker } from "node:worker_threads";
78352
78390
  // src/pass-gate.ts
78353
78391
  var QUEUE_WARN_MS = 12e4;
78354
78392
  var chain = Promise.resolve();
78393
+ var nextId = 0;
78394
+ var waiting = /* @__PURE__ */ new Map();
78395
+ var active = null;
78396
+ var recent2 = [];
78397
+ function heavyPassState() {
78398
+ const now = Date.now();
78399
+ return {
78400
+ active: active ? { label: active.label, runningMs: now - active.startedAt, waitedMs: active.waitedMs } : null,
78401
+ waiting: waiting.size,
78402
+ pending: [...waiting.values()].slice(0, 32).map((p) => ({ label: p.label, waitedMs: now - p.queuedAt })),
78403
+ recent: [...recent2]
78404
+ };
78405
+ }
78355
78406
  function withHeavyPassSlot(label, fn) {
78356
78407
  const queuedAt = Date.now();
78357
- const run3 = chain.then(() => {
78408
+ const id = nextId++;
78409
+ waiting.set(id, { label, queuedAt });
78410
+ const run3 = chain.then(async () => {
78358
78411
  const waited = Date.now() - queuedAt;
78412
+ waiting.delete(id);
78413
+ const startedAt = Date.now();
78414
+ active = { label, startedAt, waitedMs: waited };
78359
78415
  if (waited > QUEUE_WARN_MS) {
78360
78416
  console.warn(`[errata] heavy-pass slot: ${label} waited ${Math.round(waited / 1e3)}s`);
78361
78417
  }
78362
- return fn();
78418
+ let failed = false;
78419
+ try {
78420
+ return await fn();
78421
+ } catch (err2) {
78422
+ failed = true;
78423
+ throw err2;
78424
+ } finally {
78425
+ recent2.push({ label, waitedMs: waited, durationMs: Date.now() - startedAt, failed });
78426
+ if (recent2.length > 16) recent2.shift();
78427
+ active = null;
78428
+ }
78363
78429
  });
78364
78430
  chain = run3.then(
78365
78431
  () => void 0,
@@ -78378,6 +78444,12 @@ var PassWorker = class {
78378
78444
  pending = /* @__PURE__ */ new Map();
78379
78445
  nextId = 1;
78380
78446
  stopped = false;
78447
+ /** Open/migrate/index the database on the worker before the serving thread
78448
+ * opens its connection. Additive indexes and one-time repairs can take tens
78449
+ * of seconds on an existing workspace; a prepared reopen is cheap. */
78450
+ prepareStore() {
78451
+ return this.call("prepareStore");
78452
+ }
78381
78453
  /** Run the nightly graph-math + induction pass on the worker. */
78382
78454
  nightly() {
78383
78455
  return this.call("nightly");
@@ -81326,7 +81398,7 @@ ${GITIGNORE_LINES.join("\n")}
81326
81398
  var INTENT_TTL_MS = 15 * 60 * 1e3;
81327
81399
  function createIntentState(ttlMs = INTENT_TTL_MS) {
81328
81400
  const bySession = /* @__PURE__ */ new Map();
81329
- let recent3 = null;
81401
+ let recent4 = null;
81330
81402
  const live3 = (s, nowMs) => s !== void 0 && nowMs - s.atMs < ttlMs;
81331
81403
  return {
81332
81404
  set(sessionId, text, nowMs) {
@@ -81334,8 +81406,8 @@ function createIntentState(ttlMs = INTENT_TTL_MS) {
81334
81406
  if (!trimmed) return false;
81335
81407
  const prev = bySession.get(sessionId);
81336
81408
  bySession.set(sessionId, { text: trimmed, atMs: nowMs });
81337
- const changed = !(recent3 && recent3.text === trimmed);
81338
- recent3 = { sessionId, text: trimmed, atMs: nowMs };
81409
+ const changed = !(recent4 && recent4.text === trimmed);
81410
+ recent4 = { sessionId, text: trimmed, atMs: nowMs };
81339
81411
  if (!changed && prev && prev.text === trimmed) return false;
81340
81412
  return changed;
81341
81413
  },
@@ -81344,12 +81416,12 @@ function createIntentState(ttlMs = INTENT_TTL_MS) {
81344
81416
  return live3(s, nowMs) ? s.text : void 0;
81345
81417
  },
81346
81418
  mostRecent(nowMs) {
81347
- if (!recent3) return void 0;
81348
- return nowMs - recent3.atMs < ttlMs ? recent3.text : void 0;
81419
+ if (!recent4) return void 0;
81420
+ return nowMs - recent4.atMs < ttlMs ? recent4.text : void 0;
81349
81421
  },
81350
81422
  clear(sessionId) {
81351
81423
  bySession.delete(sessionId);
81352
- if (recent3?.sessionId === sessionId) recent3 = null;
81424
+ if (recent4?.sessionId === sessionId) recent4 = null;
81353
81425
  }
81354
81426
  };
81355
81427
  }
@@ -81357,19 +81429,19 @@ function createIntentState(ttlMs = INTENT_TTL_MS) {
81357
81429
  // src/working-file-state.ts
81358
81430
  function createWorkingFileState() {
81359
81431
  const bySession = /* @__PURE__ */ new Map();
81360
- let recent3 = null;
81432
+ let recent4 = null;
81361
81433
  return {
81362
81434
  set(sessionId, rel) {
81363
81435
  bySession.set(sessionId, rel);
81364
- if (rel === recent3) return false;
81365
- recent3 = rel;
81436
+ if (rel === recent4) return false;
81437
+ recent4 = rel;
81366
81438
  return true;
81367
81439
  },
81368
81440
  get(sessionId) {
81369
81441
  return bySession.get(sessionId);
81370
81442
  },
81371
81443
  mostRecent() {
81372
- return recent3;
81444
+ return recent4;
81373
81445
  }
81374
81446
  };
81375
81447
  }
@@ -81475,11 +81547,11 @@ function pendingFor(queue, channel) {
81475
81547
  }
81476
81548
  function partitionReplayable(queue, channel, isShipped) {
81477
81549
  const replay2 = [];
81478
- const waiting = [];
81550
+ const waiting2 = [];
81479
81551
  for (const w of pendingFor(queue, channel)) {
81480
- (isShipped(w.nodeId) ? replay2 : waiting).push(w);
81552
+ (isShipped(w.nodeId) ? replay2 : waiting2).push(w);
81481
81553
  }
81482
- return { replay: replay2, waiting };
81554
+ return { replay: replay2, waiting: waiting2 };
81483
81555
  }
81484
81556
  function retireWitnesses(queue, channel, settledKeys) {
81485
81557
  return queue.filter((w) => !(w.channel === channel && settledKeys.has(w.witnessKey)));
@@ -81749,25 +81821,25 @@ import { join as join34 } from "node:path";
81749
81821
  // src/loop-lag.ts
81750
81822
  var HEARTBEAT_MS = 500;
81751
81823
  var RECENT_PASS_LIMIT = 12;
81752
- var active = /* @__PURE__ */ new Map();
81824
+ var active2 = /* @__PURE__ */ new Map();
81753
81825
  var nextPassId = 1;
81754
81826
  var started = false;
81755
- var recent2 = [];
81827
+ var recent3 = [];
81756
81828
  function markPass(name2) {
81757
81829
  const id = nextPassId++;
81758
81830
  const startedAt = Date.now();
81759
- active.set(id, { name: name2, startedAt });
81831
+ active2.set(id, { name: name2, startedAt });
81760
81832
  let ended = false;
81761
81833
  return () => {
81762
81834
  if (ended) return;
81763
81835
  ended = true;
81764
- active.delete(id);
81765
- recent2.unshift({ name: name2, ms: Date.now() - startedAt, endedAt: Date.now() });
81766
- if (recent2.length > RECENT_PASS_LIMIT) recent2.pop();
81836
+ active2.delete(id);
81837
+ recent3.unshift({ name: name2, ms: Date.now() - startedAt, endedAt: Date.now() });
81838
+ if (recent3.length > RECENT_PASS_LIMIT) recent3.pop();
81767
81839
  };
81768
81840
  }
81769
81841
  function oldestActive() {
81770
- for (const p of active.values()) return p;
81842
+ for (const p of active2.values()) return p;
81771
81843
  return null;
81772
81844
  }
81773
81845
  function passState() {
@@ -81776,8 +81848,8 @@ function passState() {
81776
81848
  return {
81777
81849
  current: oldest?.name ?? "idle",
81778
81850
  runningMs: oldest === null ? null : now - oldest.startedAt,
81779
- active: [...active.values()].map((p) => ({ name: p.name, runningMs: now - p.startedAt })),
81780
- recent: [...recent2]
81851
+ active: [...active2.values()].map((p) => ({ name: p.name, runningMs: now - p.startedAt })),
81852
+ recent: [...recent3]
81781
81853
  };
81782
81854
  }
81783
81855
  function describeHolders(running, finished, windowStart, now) {
@@ -81796,7 +81868,7 @@ function startLoopLagMonitor(thresholdMs = 1e3) {
81796
81868
  const now = Date.now();
81797
81869
  const lag = now - last - HEARTBEAT_MS;
81798
81870
  if (lag > thresholdMs) {
81799
- const who = describeHolders(active.values(), recent2, last, now);
81871
+ const who = describeHolders(active2.values(), recent3, last, now);
81800
81872
  console.warn(
81801
81873
  `[loop-lag] ${(/* @__PURE__ */ new Date()).toISOString()} event loop held ~${(lag / 1e3).toFixed(1)}s during "${who}"`
81802
81874
  );
@@ -82087,7 +82159,7 @@ function createWatchBreaker(deps) {
82087
82159
  }
82088
82160
 
82089
82161
  // src/engine.ts
82090
- var DAEMON_VERSION = true ? "2.0.17-dev.3426" : "2.0.0-alpha.0";
82162
+ var DAEMON_VERSION = true ? "2.0.17-dev.3435" : "2.0.0-alpha.0";
82091
82163
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
82092
82164
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
82093
82165
  var GIT_OP_MUTE_MS = 4e3;
@@ -83821,15 +83893,15 @@ function createWorkspaceEngine(opts) {
83821
83893
  }
83822
83894
  witnessQueue = healed;
83823
83895
  fresh = routedFresh;
83824
- const { replay: queued, waiting } = partitionReplayable(witnessQueue, channel, isShipped);
83896
+ const { replay: queued, waiting: waiting2 } = partitionReplayable(witnessQueue, channel, isShipped);
83825
83897
  if (fresh.length === 0 && queued.length === 0) {
83826
83898
  if (undeliverable > 0) {
83827
83899
  saveWitnessQueue(witnessQueuePath(paths.configDir), witnessQueue);
83828
83900
  appendWitnessLedger(paths.configDir, { ts: Date.now(), channel, undeliverable, parked: pendingFor(witnessQueue, channel).length });
83829
83901
  console.log(`[errata] ${channel}: ${undeliverable} witness(es) retired \u2014 cited node class never ships (counted, not parked)`);
83830
83902
  }
83831
- if (waiting.length > 0) {
83832
- console.log(`[errata] ${channel}: ${waiting.length} parked awaiting node drain`);
83903
+ if (waiting2.length > 0) {
83904
+ console.log(`[errata] ${channel}: ${waiting2.length} parked awaiting node drain`);
83833
83905
  }
83834
83906
  return;
83835
83907
  }
@@ -83888,7 +83960,7 @@ function createWorkspaceEngine(opts) {
83888
83960
  appendPassLedger(paths.configDir, "witness-queue", 0, {
83889
83961
  recovered,
83890
83962
  replayed: queued.length,
83891
- awaitingDrain: waiting.length,
83963
+ awaitingDrain: waiting2.length,
83892
83964
  parked: pendingFor(witnessQueue, channel).length,
83893
83965
  expiredTtl: enq.dropped.expiredTtl,
83894
83966
  exhaustedAttempts: enq.dropped.exhaustedAttempts,
@@ -83897,7 +83969,7 @@ function createWorkspaceEngine(opts) {
83897
83969
  });
83898
83970
  }
83899
83971
  console.log(
83900
- `[errata] ${channel}: ${formatDisposition({ recorded, unmatched, duplicate, selfGated })}` + (queued.length > 0 ? ` (incl. ${queued.length} replayed)` : "") + (waiting.length > 0 ? ` (${waiting.length} awaiting drain)` : "")
83972
+ `[errata] ${channel}: ${formatDisposition({ recorded, unmatched, duplicate, selfGated })}` + (queued.length > 0 ? ` (incl. ${queued.length} replayed)` : "") + (waiting2.length > 0 ? ` (${waiting2.length} awaiting drain)` : "")
83901
83973
  );
83902
83974
  appendWitnessLedger(paths.configDir, {
83903
83975
  ts: Date.now(),
@@ -83907,7 +83979,7 @@ function createWorkspaceEngine(opts) {
83907
83979
  duplicate,
83908
83980
  selfGated,
83909
83981
  parked: pendingFor(witnessQueue, channel).length,
83910
- awaitingDrain: waiting.length,
83982
+ awaitingDrain: waiting2.length,
83911
83983
  ...undeliverable > 0 ? { undeliverable } : {}
83912
83984
  });
83913
83985
  };
@@ -85560,7 +85632,8 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
85560
85632
  sidecarNoLexicon: 0,
85561
85633
  sidecarNoSummary: 0,
85562
85634
  fileRefsSubstituted: 0,
85563
- fileRefLexiconMisses: 0
85635
+ fileRefLexiconMisses: 0,
85636
+ collapsedEdges: 0
85564
85637
  };
85565
85638
  const sharedCauses = /* @__PURE__ */ new Map();
85566
85639
  const causalQueue = store.findNodesByLabel("Problem").filter(
@@ -85785,7 +85858,13 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
85785
85858
  ];
85786
85859
  const dedup = /* @__PURE__ */ new Map();
85787
85860
  for (const e of rawOuts) dedup.set(`${e.type}>${e.to}`, e);
85788
- const outs = [...dedup.values()].map((e) => ({ e, wireTo: semanticEndpoints.get(e.to) })).filter((x) => x.wireTo != null);
85861
+ const outs = [...dedup.values()].map((e) => ({ e, wireTo: semanticEndpoints.get(e.to) })).filter((x) => {
85862
+ if (x.wireTo === wireFrom) {
85863
+ composition.collapsedEdges++;
85864
+ return false;
85865
+ }
85866
+ return x.wireTo != null;
85867
+ });
85789
85868
  if (outs.length === 0) continue;
85790
85869
  const dg = digest(outs.map(({ e, wireTo }) => `${e.type}>${wireTo}`).sort());
85791
85870
  const owner = nodeOf(sourceId);
@@ -86115,6 +86194,19 @@ var ConsolidateWorker = class {
86115
86194
  }
86116
86195
  };
86117
86196
 
86197
+ // src/prepare-store.ts
86198
+ init_src5();
86199
+ async function prepareStore(path2, root, workspaceId2) {
86200
+ if (!import.meta.url.endsWith(".mjs") || path2 === ":memory:") return;
86201
+ if (!graphStoreNeedsPreparation(path2)) return;
86202
+ const worker = new PassWorker({ castaliaPath: path2, workspaceRoot: root, workspaceId: workspaceId2 });
86203
+ try {
86204
+ await worker.prepareStore();
86205
+ } finally {
86206
+ await worker.stop();
86207
+ }
86208
+ }
86209
+
86118
86210
  // src/multi.ts
86119
86211
  init_paths();
86120
86212
 
@@ -86551,11 +86643,11 @@ function cachedMap(cache2, store, key, build) {
86551
86643
  }
86552
86644
  async function lookupLive(store, m, id) {
86553
86645
  const first = (await m.get()).get(id);
86554
- if (first && isLive2(store, first)) return first;
86646
+ if (first && isLive(store, first)) return first;
86555
86647
  const second = (await m.refresh()).get(id);
86556
- return second && isLive2(store, second) ? second : null;
86648
+ return second && isLive(store, second) ? second : null;
86557
86649
  }
86558
- function isLive2(store, id) {
86650
+ function isLive(store, id) {
86559
86651
  const n = store.getNode(id);
86560
86652
  return n !== null && (n.validTo === null || n.validTo === void 0);
86561
86653
  }
@@ -87589,6 +87681,10 @@ async function walk10(root, input, client, opts, readBlob) {
87589
87681
  const launched = /* @__PURE__ */ new Map();
87590
87682
  const cap = opts.maxUnits;
87591
87683
  const edgeParked = /* @__PURE__ */ new Set();
87684
+ const parkedEarlier = new Set((profile.historyStage?.coverageGaps ?? []).filter((g) => g.reason === "edge-blocked").map((g) => `${g.sha}/${g.subIndex}`));
87685
+ units.forEach((u, j) => {
87686
+ if (parkedEarlier.has(`${u.sha}/${u.subIndex}`)) edgeParked.add(j);
87687
+ });
87592
87688
  const predecessor = (k) => {
87593
87689
  for (let j = k - 1; j >= 0; j--) if (!units[j].skip && !edgeParked.has(j)) return units[j];
87594
87690
  return void 0;
@@ -87654,7 +87750,7 @@ async function walk10(root, input, client, opts, readBlob) {
87654
87750
  let ahead = 0;
87655
87751
  for (let j = k + 1; j < units.length && ahead < lookahead; j++) {
87656
87752
  if (cap !== void 0 && processed + (j - k) >= cap) break;
87657
- if (units[j].skip) continue;
87753
+ if (units[j].skip || edgeParked.has(j)) continue;
87658
87754
  ahead += 1;
87659
87755
  await launch(j);
87660
87756
  }
@@ -87675,6 +87771,13 @@ async function walk10(root, input, client, opts, readBlob) {
87675
87771
  log(`[errata] history ${label} skipped (${u.skip})`);
87676
87772
  continue;
87677
87773
  }
87774
+ if (edgeParked.has(k)) {
87775
+ profile = stage(profile, { state: dryRun ? "extracting" : "contributing", unitsDone: doneSoFar, counts, ...lastInPosition ? { watermarkSha: positionSha } : {} }, now);
87776
+ persist(profile);
87777
+ processed += 1;
87778
+ log(`[errata] history ${label}: edge-blocked on an earlier run; gap kept, not re-sent`);
87779
+ continue;
87780
+ }
87678
87781
  const { body: body2, prepMs } = await prepare(k);
87679
87782
  profile = stage(profile, { state: dryRun ? "extracting" : "contributing", unitsDone: doneSoFar - 1, lastError: void 0 }, now);
87680
87783
  persist(profile);
@@ -88806,6 +88909,7 @@ async function startMultiDaemon(opts = {}) {
88806
88909
  };
88807
88910
  const sharedDbPath = sharedStorePath();
88808
88911
  ensureParent(sharedDbPath);
88912
+ await prepareStore(sharedDbPath, process.cwd(), "shared");
88809
88913
  const sharedStore = openGraphStore({ path: sharedDbPath });
88810
88914
  const bundledRun = import.meta.url.endsWith(".mjs");
88811
88915
  const consolidateWorker = bundledRun && sharedDbPath !== ":memory:" ? new ConsolidateWorker() : null;
@@ -88964,8 +89068,13 @@ async function startMultiDaemon(opts = {}) {
88964
89068
  }
88965
89069
  }
88966
89070
  };
88967
- const buildRecord = (entry) => {
89071
+ let stopping = false;
89072
+ const buildRecord = async (entry) => {
88968
89073
  const id = entry.id;
89074
+ const castaliaPath = workspacePaths(entry.path).castalia;
89075
+ ensureParent(castaliaPath);
89076
+ await prepareStore(castaliaPath, entry.path, id);
89077
+ if (stopping) throw new Error("daemon stopping");
88969
89078
  const wsIdentity = identityFor(entry);
88970
89079
  let wsCloud = cloud;
88971
89080
  if (wsIdentity.rule === "workspace-binding") {
@@ -89048,7 +89157,7 @@ async function startMultiDaemon(opts = {}) {
89048
89157
  if (opts.eagerWorkspaces ?? EAGER_WORKSPACES_AT_BOOT) {
89049
89158
  for (const entry of entries) {
89050
89159
  try {
89051
- records.push(buildRecord(entry));
89160
+ records.push(await buildRecord(entry));
89052
89161
  } catch (err2) {
89053
89162
  console.warn(`[errata] skipping ${entry.name} (${entry.path}): ${err2 instanceof Error ? err2.message : err2}`);
89054
89163
  }
@@ -89066,14 +89175,24 @@ async function startMultiDaemon(opts = {}) {
89066
89175
  }
89067
89176
  const app = new Hono2();
89068
89177
  const enqueueWakeIndex = serialQueue();
89178
+ const attaching = /* @__PURE__ */ new Map();
89069
89179
  const attachWorkspace = (root) => {
89180
+ const norm = normPath(root);
89181
+ const pending = attaching.get(norm);
89182
+ if (pending) return pending;
89183
+ const run3 = attachWorkspaceOnce(root).finally(() => attaching.delete(norm));
89184
+ attaching.set(norm, run3);
89185
+ return run3;
89186
+ };
89187
+ const attachWorkspaceOnce = async (root) => {
89188
+ if (stopping) return { attached: false, id: null, reason: "daemon-stopping" };
89070
89189
  const norm = normPath(root);
89071
89190
  if (records.some((r) => normPath(r.root) === norm)) return { attached: false, id: null, reason: "already-watched" };
89072
89191
  const entry = listWorkspaceEntries().find((e) => normPath(e.path) === norm);
89073
89192
  if (!entry) return { attached: false, id: null, reason: "not-registered" };
89074
89193
  let rec;
89075
89194
  try {
89076
- rec = buildRecord(entry);
89195
+ rec = await buildRecord(entry);
89077
89196
  } catch (err2) {
89078
89197
  return { attached: false, id: null, reason: err2 instanceof Error ? err2.message : "build-failed" };
89079
89198
  }
@@ -89117,7 +89236,7 @@ async function startMultiDaemon(opts = {}) {
89117
89236
  }
89118
89237
  const root = typeof body2["path"] === "string" ? body2["path"] : null;
89119
89238
  if (!root) return c.json({ error: "path-required" }, 400);
89120
- return c.json(attachWorkspace(root));
89239
+ return c.json(await attachWorkspace(root));
89121
89240
  });
89122
89241
  app.post("/api/notify-test", (c) => {
89123
89242
  notify("Errata \xB7 test", "Fired from the daemon \u2014 if you see this, daemon toasts render.");
@@ -89202,6 +89321,7 @@ async function startMultiDaemon(opts = {}) {
89202
89321
  // pegged daemon is only diagnosable by attaching a CPU profiler to the
89203
89322
  // live process — see watch-census.ts for the incident that proved it.
89204
89323
  pass: passState(),
89324
+ heavyPass: heavyPassState(),
89205
89325
  watch: watchCensus(),
89206
89326
  // Registered but with no engine running (WS-cold-start). Listed so a cold
89207
89327
  // daemon is legible as "nothing has needed waking yet" rather than as
@@ -89245,7 +89365,7 @@ async function startMultiDaemon(opts = {}) {
89245
89365
  if (!main2) continue;
89246
89366
  owner = ownerOf(records, main2);
89247
89367
  if (!owner && listWorkspaceEntries().some((e) => pickOwnerRoot([e.path], main2))) {
89248
- if (attachWorkspace(main2).attached) owner = ownerOf(records, main2);
89368
+ if ((await attachWorkspace(main2)).attached) owner = ownerOf(records, main2);
89249
89369
  }
89250
89370
  if (owner) break;
89251
89371
  }
@@ -89253,7 +89373,7 @@ async function startMultiDaemon(opts = {}) {
89253
89373
  if (!owner && others.length > 0) {
89254
89374
  const registryRoots = listWorkspaceEntries().map((e) => e.path);
89255
89375
  const registered = others.map((p) => pickOwnerRoot(registryRoots, p)).find((r) => r != null);
89256
- if (registered && attachWorkspace(registered).attached) {
89376
+ if (registered && (await attachWorkspace(registered)).attached) {
89257
89377
  owner = records.find((r) => r.root === registered) ?? null;
89258
89378
  if (owner) console.log(`[errata] woke workspace ${owner.entry.name}`);
89259
89379
  }
@@ -89262,7 +89382,7 @@ async function startMultiDaemon(opts = {}) {
89262
89382
  const missPath = pathInBody(body2);
89263
89383
  const registeredRoot = missPath ? pickOwnerRoot(listWorkspaceEntries().map((e) => e.path), missPath) : null;
89264
89384
  const wakeRoot = registeredRoot ?? (missPath ? findGitRoot(missPath) : null);
89265
- if (wakeRoot && attachWorkspace(wakeRoot).attached) {
89385
+ if (wakeRoot && (await attachWorkspace(wakeRoot)).attached) {
89266
89386
  owner = ownerOf(records, missPath) ?? null;
89267
89387
  if (owner) console.log(`[errata] woke workspace ${owner.entry.name}`);
89268
89388
  }
@@ -89271,7 +89391,7 @@ async function startMultiDaemon(opts = {}) {
89271
89391
  const root = sessionAffinity.rootFor(body2["session_id"]);
89272
89392
  if (root) {
89273
89393
  owner = records.find((r) => r.root === root) ?? null;
89274
- if (!owner && attachWorkspace(root).attached) owner = records.find((r) => r.root === root) ?? null;
89394
+ if (!owner && (await attachWorkspace(root)).attached) owner = records.find((r) => r.root === root) ?? null;
89275
89395
  }
89276
89396
  }
89277
89397
  if (!owner) {
@@ -89409,9 +89529,11 @@ async function startMultiDaemon(opts = {}) {
89409
89529
  });
89410
89530
  candidates.forEach((c, i2) => {
89411
89531
  const wake = setTimeout(() => {
89412
- if (attachWorkspace(c.path).attached) {
89413
- console.log(`[errata] woke workspace ${c.name} \u2014 recent Codex/Gemini/OpenCode transcript (headless capture)`);
89414
- }
89532
+ void attachWorkspace(c.path).then((result) => {
89533
+ if (result.attached) {
89534
+ console.log(`[errata] woke workspace ${c.name} \u2014 recent Codex/Gemini/OpenCode transcript (headless capture)`);
89535
+ }
89536
+ });
89415
89537
  }, wakeDelay(i2));
89416
89538
  wake.unref?.();
89417
89539
  });
@@ -89497,7 +89619,7 @@ async function startMultiDaemon(opts = {}) {
89497
89619
  saveProfile(root, profile);
89498
89620
  }
89499
89621
  registerWorkspace(profile, root);
89500
- attachWorkspace(root);
89622
+ await attachWorkspace(root);
89501
89623
  },
89502
89624
  // eslint-disable-next-line no-console
89503
89625
  log: (line) => console.log(line)
@@ -90434,6 +90556,7 @@ async function startMultiDaemon(opts = {}) {
90434
90556
  boundaryListeners.push(cb);
90435
90557
  },
90436
90558
  async stop() {
90559
+ stopping = true;
90437
90560
  clearInterval(idleSweep);
90438
90561
  clearInterval(codexWake);
90439
90562
  clearInterval(anchorPull);
@@ -90448,6 +90571,7 @@ async function startMultiDaemon(opts = {}) {
90448
90571
  );
90449
90572
  updatePoller?.stop();
90450
90573
  healthSentinel.stop();
90574
+ await Promise.allSettled(attaching.values());
90451
90575
  for (const r of records) await r.engine.stop();
90452
90576
  if (consolidateWorker) await consolidateWorker.stop();
90453
90577
  try {
@@ -90528,18 +90652,18 @@ async function runUse(action, ctx) {
90528
90652
  }
90529
90653
  }
90530
90654
  function runClear(ctx) {
90531
- const active2 = ctx.getActiveAgent();
90655
+ const active3 = ctx.getActiveAgent();
90532
90656
  ctx.clearActiveAgent();
90533
- if (active2) {
90534
- ctx.log(`no longer acting as ${active2.handle} \u2014 the daemon acts as your default identity`);
90657
+ if (active3) {
90658
+ ctx.log(`no longer acting as ${active3.handle} \u2014 the daemon acts as your default identity`);
90535
90659
  } else {
90536
90660
  ctx.log(`already on the default identity \u2014 nothing to clear`);
90537
90661
  }
90538
90662
  return 0;
90539
90663
  }
90540
90664
  async function runShow(ctx) {
90541
- const active2 = ctx.getActiveAgent();
90542
- ctx.log(active2 ? `active agent: ${active2.handle} (id ${active2.id})` : `active agent: (default identity)`);
90665
+ const active3 = ctx.getActiveAgent();
90666
+ ctx.log(active3 ? `active agent: ${active3.handle} (id ${active3.id})` : `active agent: (default identity)`);
90543
90667
  if (!ctx.loggedIn) {
90544
90668
  ctx.log(` log in to list the agents you can act as: errata login`);
90545
90669
  return 0;
@@ -90551,7 +90675,7 @@ async function runShow(ctx) {
90551
90675
  ctx.log(` couldn't list actable agents: ${errText(err2)}`);
90552
90676
  return 1;
90553
90677
  }
90554
- printActable(resp, active2, ctx);
90678
+ printActable(resp, active3, ctx);
90555
90679
  return 0;
90556
90680
  }
90557
90681
  async function runSet(handle2, ctx) {
@@ -90580,7 +90704,7 @@ async function runSet(handle2, ctx) {
90580
90704
  ctx.log(`now acting as ${match2.handle} (id ${match2.id}) \u2014 saved; persists across daemon restarts`);
90581
90705
  return 0;
90582
90706
  }
90583
- function printActable(resp, active2, ctx) {
90707
+ function printActable(resp, active3, ctx) {
90584
90708
  if (resp.agents.length === 0) {
90585
90709
  ctx.log(` (no agents you can act as)`);
90586
90710
  return;
@@ -90588,7 +90712,7 @@ function printActable(resp, active2, ctx) {
90588
90712
  ctx.log(` agents you can act as:`);
90589
90713
  for (const a of resp.agents) {
90590
90714
  const marks = [];
90591
- if (active2 && a.id === active2.id) marks.push("active");
90715
+ if (active3 && a.id === active3.id) marks.push("active");
90592
90716
  if (a.id === resp.defaultAgentId) marks.push("default");
90593
90717
  const suffix = marks.length ? ` (${marks.join(", ")})` : "";
90594
90718
  const scope = a.scope ? ` \xB7 ${a.scope}` : "";
@@ -92271,9 +92395,9 @@ async function cmdStatus() {
92271
92395
  } else {
92272
92396
  console.log(` logged in: yes (${cfg.email}) \u2014 key capability enforced by Console installation/scopes`);
92273
92397
  }
92274
- const active2 = getActiveAgent(cfg);
92398
+ const active3 = getActiveAgent(cfg);
92275
92399
  console.log(
92276
- ` active agent: ${active2 ? `${active2.handle} (acting-as; \`errata use --none\` to clear)` : "default identity"}`
92400
+ ` active agent: ${active3 ? `${active3.handle} (acting-as; \`errata use --none\` to clear)` : "default identity"}`
92277
92401
  );
92278
92402
  const inspection = await inspectCloudEndpoint(cfg.cloudUrl);
92279
92403
  const decision = evaluateCloudEndpoint(cfg.cloudUrl, inspection);
@@ -92393,8 +92517,8 @@ async function cmdWhoami(args2) {
92393
92517
  return;
92394
92518
  }
92395
92519
  try {
92396
- const active2 = getActiveAgent(cfg);
92397
- const response = await resolveWhoami(authedCloudClient(cfg), active2?.handle ?? null);
92520
+ const active3 = getActiveAgent(cfg);
92521
+ const response = await resolveWhoami(authedCloudClient(cfg), active3?.handle ?? null);
92398
92522
  if (args2.includes("--json")) {
92399
92523
  console.log(JSON.stringify(response, null, 2));
92400
92524
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.17-dev.3426",
3
+ "version": "2.0.17-dev.3435",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/pass-worker.mjs CHANGED
@@ -16283,13 +16283,13 @@ function loadNodeSqlite() {
16283
16283
  process.emitWarning = original;
16284
16284
  }
16285
16285
  }
16286
- function openDatabase(path) {
16287
- const db = new DatabaseSync(path);
16286
+ function openDatabase(path, opts) {
16287
+ const db = opts ? new DatabaseSync(path, opts) : new DatabaseSync(path);
16288
16288
  try {
16289
16289
  db.exec("PRAGMA foreign_keys = ON");
16290
16290
  } catch {
16291
16291
  }
16292
- if (path !== ":memory:") {
16292
+ if (path !== ":memory:" && !opts?.readOnly) {
16293
16293
  try {
16294
16294
  db.exec("PRAGMA journal_mode = WAL");
16295
16295
  db.exec("PRAGMA synchronous = NORMAL");
@@ -25171,8 +25171,9 @@ init_src2();
25171
25171
  init_src();
25172
25172
 
25173
25173
  // ../../packages/local-graph/src/history-cache-floor.ts
25174
+ var HISTORY_CACHE_FLOOR_KEY = "history-cache-withdrawal-floor-3";
25174
25175
  function installHistoryCacheFloor(db) {
25175
- const key = "history-cache-withdrawal-floor-3";
25176
+ const key = HISTORY_CACHE_FLOOR_KEY;
25176
25177
  if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(key)) return;
25177
25178
  db.transaction(() => {
25178
25179
  db.exec("DROP TRIGGER IF EXISTS history_cache_nodes_insert; DROP TRIGGER IF EXISTS history_cache_nodes_update;");
@@ -25334,6 +25335,7 @@ function localEdgeViolation(fromLabel, type, toLabel) {
25334
25335
  return verdict.ok ? null : verdict.reason ?? "edge rule violation";
25335
25336
  }
25336
25337
  var SCHEMA_VERSION = 6;
25338
+ var SESSION_REPAIR_KEY = "session-evidence-v1";
25337
25339
  var SESSION_EVIDENCE_PREDICATE = SESSION_EVIDENCE_FIELDS.map((field) => `instr(attrs_json, '${JSON.stringify(field)}') > 0`).join(" OR ");
25338
25340
  var SCHEMA_SQL = `
25339
25341
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -25545,10 +25547,10 @@ var SqliteGraphStore = class {
25545
25547
  }
25546
25548
  this.db.exec(TEMPORAL_INDEX_SQL);
25547
25549
  installHistoryCacheFloor(this.db);
25548
- if (!this.db.prepare("SELECT value FROM store_meta WHERE key = 'session-evidence-v1'").get()) {
25550
+ if (!this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(SESSION_REPAIR_KEY)) {
25549
25551
  this.db.transaction(() => {
25550
25552
  this.repairSessionAliases();
25551
- this.db.prepare("INSERT OR REPLACE INTO store_meta (key, value) VALUES ('session-evidence-v1', 'done')").run();
25553
+ this.db.prepare("INSERT OR REPLACE INTO store_meta (key, value) VALUES (?, 'done')").run(SESSION_REPAIR_KEY);
25552
25554
  });
25553
25555
  }
25554
25556
  this.stmts = {
@@ -25743,6 +25745,34 @@ var SqliteGraphStore = class {
25743
25745
  const seqRow = this.db.prepare("SELECT value FROM store_meta WHERE key = 'ingest_seq'").get();
25744
25746
  this.ingestSeq = seqRow ? Number(seqRow.value) : 0;
25745
25747
  }
25748
+ strandedSymbols() {
25749
+ return this.db.prepare(`
25750
+ SELECT f.id AS fileId, e.to_id AS symbolId
25751
+ -- Start from the small File population. An unconstrained JOIN lets
25752
+ -- SQLite start at edges_live and visit every live edge in the graph.
25753
+ FROM nodes f CROSS JOIN edges e ON e.from_id = f.id
25754
+ LEFT JOIN nodes t ON t.id = e.to_id
25755
+ WHERE f.label = 'File' AND f.valid_to IS NULL AND e.valid_to IS NULL
25756
+ AND e.type IN ('DEFINES', 'CONTAINS')
25757
+ AND (t.id IS NULL OR t.valid_to IS NOT NULL)
25758
+ `).all();
25759
+ }
25760
+ fileIndexStates(ids) {
25761
+ if (ids.length === 0) return [];
25762
+ if (ids.length > 500) throw new RangeError("fileIndexStates accepts at most 500 ids per read");
25763
+ const rows = this.db.prepare(`
25764
+ SELECT f.id, f.last_updated_at AS lastUpdatedAt, f.valid_to AS validTo,
25765
+ CASE WHEN count(e.id) = 0 THEN
25766
+ (json_type(f.attrs_json, '$.symbolCount') IN ('integer','real') AND json_extract(f.attrs_json, '$.symbolCount') = 0)
25767
+ OR json_type(f.attrs_json, '$.indexSkipped') = 'text'
25768
+ ELSE max(CASE WHEN t.id IS NULL OR t.valid_to IS NOT NULL THEN 1 ELSE 0 END) = 0 END AS complete
25769
+ FROM nodes f
25770
+ LEFT JOIN edges e ON e.from_id = f.id AND e.valid_to IS NULL AND e.type IN ('DEFINES','CONTAINS')
25771
+ LEFT JOIN nodes t ON t.id = e.to_id
25772
+ WHERE f.id IN (${ids.map(() => "?").join(",")}) GROUP BY f.id
25773
+ `).all(...ids);
25774
+ return rows.map((row) => ({ ...row, complete: row.complete === 1 }));
25775
+ }
25746
25776
  /** Forward-migrate the on-disk schema to the current SCHEMA_VERSION. */
25747
25777
  migrate(from) {
25748
25778
  this.db.transaction(() => {
@@ -29118,48 +29148,30 @@ function listSourceFiles(root) {
29118
29148
  return gitSourceFiles(root) ?? walkSource(root);
29119
29149
  }
29120
29150
  var SCAN_YIELD_EVERY = 100;
29121
- var LIVENESS_CHUNK = 4e3;
29122
29151
  async function findStaleFiles(store2, rootPath, workspaceId) {
29123
29152
  const stale = [];
29124
- const pending = [];
29125
- const allTargets = [];
29153
+ let pending = [];
29154
+ const check2 = () => {
29155
+ const states = new Map(store2.fileIndexStates(pending.map((f) => f.id)).map((s) => [s.id, s]));
29156
+ for (const f of pending) {
29157
+ const state = states.get(f.id);
29158
+ if (!state || state.validTo !== null || state.lastUpdatedAt < f.mtimeMs || !state.complete) stale.push(f.abs);
29159
+ }
29160
+ pending = [];
29161
+ };
29126
29162
  let scanned = 0;
29127
29163
  for (const abs of listSourceFiles(rootPath)) {
29128
- if (++scanned % SCAN_YIELD_EVERY === 0) await new Promise((r) => setImmediate(r));
29129
- let mtimeMs;
29130
29164
  try {
29131
- mtimeMs = statSync3(abs).mtimeMs;
29165
+ const rel = relative2(rootPath, abs).split(sep2).join("/");
29166
+ pending.push({ abs, id: fileNodeId(workspaceId, rel), mtimeMs: statSync3(abs).mtimeMs });
29132
29167
  } catch {
29133
- continue;
29134
29168
  }
29135
- const rel = relative2(rootPath, abs).split(sep2).join("/");
29136
- const fnode = store2.getNode(fileNodeId(workspaceId, rel));
29137
- if (!fnode) {
29138
- stale.push(abs);
29139
- } else if ((fnode.validTo ?? null) !== null) {
29140
- stale.push(abs);
29141
- } else if (fnode.lastUpdatedAt < mtimeMs) {
29142
- stale.push(abs);
29143
- } else {
29144
- const edges = store2.outEdges(fnode.id, ["DEFINES", "CONTAINS"]);
29145
- if (edges.length === 0) {
29146
- const converged = fnode.attrs["symbolCount"] === 0 || typeof fnode.attrs["indexSkipped"] === "string";
29147
- if (!converged) stale.push(abs);
29148
- } else {
29149
- const targets = edges.map((e) => e.to);
29150
- pending.push({ abs, targets });
29151
- allTargets.push(...targets);
29152
- }
29169
+ if (++scanned % SCAN_YIELD_EVERY === 0) {
29170
+ check2();
29171
+ await new Promise((resolve) => setImmediate(resolve));
29153
29172
  }
29154
29173
  }
29155
- const live = /* @__PURE__ */ new Set();
29156
- for (let i2 = 0; i2 < allTargets.length; i2 += LIVENESS_CHUNK) {
29157
- for (const id of store2.liveNodeIds(allTargets.slice(i2, i2 + LIVENESS_CHUNK))) live.add(id);
29158
- if (i2 + LIVENESS_CHUNK < allTargets.length) await new Promise((r) => setImmediate(r));
29159
- }
29160
- for (const { abs, targets } of pending) {
29161
- if (targets.some((id) => !live.has(id))) stale.push(abs);
29162
- }
29174
+ check2();
29163
29175
  return stale;
29164
29176
  }
29165
29177
  async function reconcileStaleFiles(store2, rootPath, workspaceId) {
@@ -29574,6 +29586,9 @@ port.on("message", (msg) => {
29574
29586
  });
29575
29587
  function runPass(msg) {
29576
29588
  switch (msg.kind) {
29589
+ case "prepareStore":
29590
+ return void 0;
29591
+ // opening the connection above completed all preparation
29577
29592
  case "semanticMaintenance":
29578
29593
  return runWorkspaceMaintenance(store, init2.workspaceId);
29579
29594
  case "liveness":