@inerrata-corporation/errata 2.0.6-dev.2654 → 2.0.6-dev.2657

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.
@@ -16206,6 +16206,10 @@ function rowToEdge(r) {
16206
16206
  }
16207
16207
  var SqliteGraphStore = class {
16208
16208
  db;
16209
+ // Typed edge lookups build their SQL from the number of types asked for, and
16210
+ // used to prepare it on every call: ~600ms of a 2.5s drain build on a live
16211
+ // 225k-node store was statement compilation alone. Keyed by the SQL text.
16212
+ typedEdgeStmts = /* @__PURE__ */ new Map();
16209
16213
  stmts;
16210
16214
  /** Unified graph-mutation counter (see GraphStore.mutationCount). Bumped on
16211
16215
  * every node/edge upsert — code-index and capture alike — so the consolidation
@@ -16710,6 +16714,14 @@ var SqliteGraphStore = class {
16710
16714
  const rows = this.db.prepare(sql).all(params);
16711
16715
  return rows.map(rowToNode);
16712
16716
  }
16717
+ typedEdgeStmt(sql) {
16718
+ let stmt = this.typedEdgeStmts.get(sql);
16719
+ if (!stmt) {
16720
+ stmt = this.db.prepare(sql);
16721
+ this.typedEdgeStmts.set(sql, stmt);
16722
+ }
16723
+ return stmt;
16724
+ }
16713
16725
  outEdges(nodeId, types) {
16714
16726
  if (!types || types.length === 0) {
16715
16727
  const rows2 = this.stmts.outEdges.all(nodeId);
@@ -16720,7 +16732,7 @@ var SqliteGraphStore = class {
16720
16732
  types.forEach((t, i) => {
16721
16733
  params[`t${i}`] = t;
16722
16734
  });
16723
- const rows = this.db.prepare(
16735
+ const rows = this.typedEdgeStmt(
16724
16736
  `SELECT * FROM edges WHERE from_id = :from_id AND type IN (${placeholders}) AND valid_to IS NULL`
16725
16737
  ).all(params);
16726
16738
  return rows.map(rowToEdge);
@@ -16735,7 +16747,7 @@ var SqliteGraphStore = class {
16735
16747
  types.forEach((t, i) => {
16736
16748
  params[`t${i}`] = t;
16737
16749
  });
16738
- const rows = this.db.prepare(
16750
+ const rows = this.typedEdgeStmt(
16739
16751
  `SELECT * FROM edges WHERE to_id = :to_id AND type IN (${placeholders}) AND valid_to IS NULL`
16740
16752
  ).all(params);
16741
16753
  return rows.map(rowToEdge);
@@ -16813,6 +16825,15 @@ var SqliteGraphStore = class {
16813
16825
  const rows = this.db.prepare("SELECT id FROM nodes WHERE valid_to IS NULL AND last_updated_at > ?").all(ts);
16814
16826
  return rows.map((r) => r.id);
16815
16827
  }
16828
+ labelFingerprint(labels) {
16829
+ if (labels.length === 0) return "0";
16830
+ const r = this.db.prepare(
16831
+ `SELECT COUNT(*) AS n, MAX(last_updated_at) AS t,
16832
+ MAX(COALESCE(last_reinforced_at_seq, created_at_seq, 0)) AS s
16833
+ FROM nodes WHERE valid_to IS NULL AND label IN (${labels.map(() => "?").join(",")})`
16834
+ ).get(...labels);
16835
+ return `${r.n}:${r.t ?? 0}:${r.s ?? 0}`;
16836
+ }
16816
16837
  /** Endpoints of edges TOUCHED since `ts` — created, re-seen, or CLOSED.
16817
16838
  * Closures matter as much as additions: a node that lost inflow is rank-dirty
16818
16839
  * while its own row never updated, so removal endpoints must seed the
@@ -18254,10 +18275,16 @@ function internalPackageNames(workspaceRoot) {
18254
18275
  return names;
18255
18276
  }
18256
18277
  var cache = /* @__PURE__ */ new Map();
18278
+ var cacheKeys = /* @__PURE__ */ new Map();
18257
18279
  function publicSymbolIndex(store, workspaceRoot, deps = {}) {
18258
18280
  const now = deps.nowMs ?? Date.now();
18281
+ const fileRows = store.labelFingerprint?.(["File"]);
18282
+ const key = fileRows === void 0 ? void 0 : `${fileRows}|${store.countNodesByLabel("Module")}`;
18259
18283
  const cached2 = cache.get(workspaceRoot);
18260
- if (cached2 && now - cached2.builtAt < INDEX_TTL_MS) return cached2;
18284
+ if (cached2) {
18285
+ const fresh = key !== void 0 ? cacheKeys.get(workspaceRoot) === key : now - cached2.builtAt < INDEX_TTL_MS;
18286
+ if (fresh) return cached2;
18287
+ }
18261
18288
  const readHead = deps.readHead ?? ((absPath) => {
18262
18289
  try {
18263
18290
  return readFileSync(absPath, "utf8").slice(0, HEAD_BYTES);
@@ -18294,6 +18321,8 @@ function publicSymbolIndex(store, workspaceRoot, deps = {}) {
18294
18321
  }
18295
18322
  const index = { symbols, builtAt: now };
18296
18323
  cache.set(workspaceRoot, index);
18324
+ if (key !== void 0) cacheKeys.set(workspaceRoot, key);
18325
+ else cacheKeys.delete(workspaceRoot);
18297
18326
  return index;
18298
18327
  }
18299
18328
  var PRESERVED_SYMBOLS_CAP = 8;
@@ -19082,7 +19111,16 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19082
19111
  ...profile.repoLocatorAliases ? { aliases: profile.repoLocatorAliases } : {},
19083
19112
  ...profile.aka ? { aka: profile.aka } : {}
19084
19113
  };
19085
- const fileLexicon = buildFileLexicon(store);
19114
+ const fileLexicon = opts.fileLexicon ?? buildFileLexicon(store);
19115
+ const nodeMemo = /* @__PURE__ */ new Map();
19116
+ const nodeOf = (id) => {
19117
+ let n = nodeMemo.get(id);
19118
+ if (n === void 0) {
19119
+ n = store.getNode(id) ?? opts.sharedStore?.getNode(id) ?? null;
19120
+ nodeMemo.set(id, n);
19121
+ }
19122
+ return n;
19123
+ };
19086
19124
  const shareable = (n) => {
19087
19125
  const preserved = symbolIndex ? preservedSymbolsFor(n.description, symbolIndex) : [];
19088
19126
  const fileRefs = generalizeFileRefs(n.description, fileLexicon, level);
@@ -19207,7 +19245,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19207
19245
  for (const sourceId of anchorSources) {
19208
19246
  const targets = [];
19209
19247
  for (const e of store.outEdges(sourceId, [...ANCHOR_EDGES])) {
19210
- const t = store.getNode(e.to);
19248
+ const t = nodeOf(e.to);
19211
19249
  if (!t || !ANCHOR_TARGET_LABELS.includes(t.label)) continue;
19212
19250
  if (e.type === "OCCURS_IN" && t.label !== "Language") continue;
19213
19251
  if (e.type === "DEPENDS_ON" && t.label !== "Package") continue;
@@ -19230,7 +19268,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19230
19268
  }
19231
19269
  }
19232
19270
  const dg = digest(targets.map(({ e, wireId }) => `${e.type}>${wireId}`).sort());
19233
- const sourceNode = store.getNode(sourceId);
19271
+ const sourceNode = nodeOf(sourceId);
19234
19272
  if (sourceNode?.attrs["anchorsContributedDigest"] === dg) continue;
19235
19273
  anchorDigests[sourceId] = dg;
19236
19274
  const wireFrom = seen.has(sourceId) || !sourceNode ? sourceId : contributedWireId(sourceNode);
@@ -19260,12 +19298,12 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19260
19298
  if (project) {
19261
19299
  const now = Date.now();
19262
19300
  for (const sourceId of anchorSources) {
19263
- const source = store.getNode(sourceId);
19301
+ const source = nodeOf(sourceId);
19264
19302
  if (!source) continue;
19265
19303
  const wireFrom = seen.has(sourceId) ? sourceId : contributedWireId(source);
19266
19304
  const planned = [];
19267
19305
  for (const e of store.outEdges(sourceId, ["ANCHORED_AT"])) {
19268
- const target = store.getNode(e.to);
19306
+ const target = nodeOf(e.to);
19269
19307
  if (!target) continue;
19270
19308
  if (!isAnchorLabelKind(target.label)) continue;
19271
19309
  const isFile = target.label === "File";
@@ -19342,7 +19380,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19342
19380
  const outs = [...dedup.values()].map((e) => ({ e, wireTo: semanticEndpoints.get(e.to) })).filter((x) => x.wireTo != null);
19343
19381
  if (outs.length === 0) continue;
19344
19382
  const dg = digest(outs.map(({ e, wireTo }) => `${e.type}>${wireTo}`).sort());
19345
- const owner = store.getNode(sourceId) ?? opts.sharedStore?.getNode(sourceId);
19383
+ const owner = nodeOf(sourceId);
19346
19384
  const digestSkip = owner?.attrs["semanticEdgesContributedDigest"] === dg;
19347
19385
  if (!digestSkip) semanticEdgeDigests[sourceId] = dg;
19348
19386
  for (const { e, wireTo } of outs) {
@@ -19399,7 +19437,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19399
19437
  const heldOrphans = /* @__PURE__ */ new Set();
19400
19438
  for (const wn of nodes) {
19401
19439
  if (wn.label !== "RootCause" && wn.label !== "Solution") continue;
19402
- const src = store.getNode(wn.id) ?? opts.sharedStore?.getNode(wn.id);
19440
+ const src = nodeOf(wn.id);
19403
19441
  if (typeof src?.attrs["contributedAtSeq"] === "number") continue;
19404
19442
  if (bindingEndpoints.has(wn.id)) continue;
19405
19443
  heldOrphans.add(wn.id);
@@ -19444,6 +19482,21 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19444
19482
  }
19445
19483
 
19446
19484
  // src/drain-build.ts
19485
+ var LEXICON_SYMBOL_LABELS = ["Function", "Method", "Class", "Interface", "Const", "Comment"];
19486
+ function codeGraphKey(store) {
19487
+ const files = store.labelFingerprint?.(["File"]);
19488
+ if (files === void 0) return null;
19489
+ return [files, ...LEXICON_SYMBOL_LABELS.map((l) => store.countNodesByLabel(l))].join("|");
19490
+ }
19491
+ var lexiconCache = /* @__PURE__ */ new WeakMap();
19492
+ function lexiconsFor(store) {
19493
+ const key = codeGraphKey(store);
19494
+ const hit = key !== null ? lexiconCache.get(store) : void 0;
19495
+ if (hit && hit.key === key) return hit;
19496
+ const built = { symbols: buildSymbolLexicon(store), files: buildFileLexicon(store) };
19497
+ if (key !== null) lexiconCache.set(store, { key, ...built });
19498
+ return built;
19499
+ }
19447
19500
  function buildDrainContext(store, input) {
19448
19501
  return buildContextIngest(store, input.profile, input.daemonVersion, input.ignorePatterns, {
19449
19502
  ...input.includePackages !== void 0 ? { includePackages: input.includePackages } : {}
@@ -19451,9 +19504,10 @@ function buildDrainContext(store, input) {
19451
19504
  }
19452
19505
  function buildDrainInstances(store, sharedStore, input) {
19453
19506
  let lexicon;
19507
+ let fileLexicon;
19454
19508
  let lexiconError;
19455
19509
  try {
19456
- lexicon = buildSymbolLexicon(store);
19510
+ ({ symbols: lexicon, files: fileLexicon } = lexiconsFor(store));
19457
19511
  } catch (err) {
19458
19512
  lexiconError = err instanceof Error ? err.message : String(err);
19459
19513
  }
@@ -19466,6 +19520,7 @@ function buildDrainInstances(store, sharedStore, input) {
19466
19520
  composition = c;
19467
19521
  },
19468
19522
  ...lexicon ? { lexicon } : {},
19523
+ ...fileLexicon ? { fileLexicon } : {},
19469
19524
  ...input.project ? { project: input.project } : {}
19470
19525
  });
19471
19526
  return {
package/errata.mjs CHANGED
@@ -17566,6 +17566,10 @@ CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) W
17566
17566
  `;
17567
17567
  SqliteGraphStore = class {
17568
17568
  db;
17569
+ // Typed edge lookups build their SQL from the number of types asked for, and
17570
+ // used to prepare it on every call: ~600ms of a 2.5s drain build on a live
17571
+ // 225k-node store was statement compilation alone. Keyed by the SQL text.
17572
+ typedEdgeStmts = /* @__PURE__ */ new Map();
17569
17573
  stmts;
17570
17574
  /** Unified graph-mutation counter (see GraphStore.mutationCount). Bumped on
17571
17575
  * every node/edge upsert — code-index and capture alike — so the consolidation
@@ -18070,6 +18074,14 @@ CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) W
18070
18074
  const rows = this.db.prepare(sql).all(params);
18071
18075
  return rows.map(rowToNode);
18072
18076
  }
18077
+ typedEdgeStmt(sql) {
18078
+ let stmt = this.typedEdgeStmts.get(sql);
18079
+ if (!stmt) {
18080
+ stmt = this.db.prepare(sql);
18081
+ this.typedEdgeStmts.set(sql, stmt);
18082
+ }
18083
+ return stmt;
18084
+ }
18073
18085
  outEdges(nodeId, types) {
18074
18086
  if (!types || types.length === 0) {
18075
18087
  const rows2 = this.stmts.outEdges.all(nodeId);
@@ -18080,7 +18092,7 @@ CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) W
18080
18092
  types.forEach((t, i2) => {
18081
18093
  params[`t${i2}`] = t;
18082
18094
  });
18083
- const rows = this.db.prepare(
18095
+ const rows = this.typedEdgeStmt(
18084
18096
  `SELECT * FROM edges WHERE from_id = :from_id AND type IN (${placeholders}) AND valid_to IS NULL`
18085
18097
  ).all(params);
18086
18098
  return rows.map(rowToEdge);
@@ -18095,7 +18107,7 @@ CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) W
18095
18107
  types.forEach((t, i2) => {
18096
18108
  params[`t${i2}`] = t;
18097
18109
  });
18098
- const rows = this.db.prepare(
18110
+ const rows = this.typedEdgeStmt(
18099
18111
  `SELECT * FROM edges WHERE to_id = :to_id AND type IN (${placeholders}) AND valid_to IS NULL`
18100
18112
  ).all(params);
18101
18113
  return rows.map(rowToEdge);
@@ -18173,6 +18185,15 @@ CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) W
18173
18185
  const rows = this.db.prepare("SELECT id FROM nodes WHERE valid_to IS NULL AND last_updated_at > ?").all(ts);
18174
18186
  return rows.map((r) => r.id);
18175
18187
  }
18188
+ labelFingerprint(labels) {
18189
+ if (labels.length === 0) return "0";
18190
+ const r = this.db.prepare(
18191
+ `SELECT COUNT(*) AS n, MAX(last_updated_at) AS t,
18192
+ MAX(COALESCE(last_reinforced_at_seq, created_at_seq, 0)) AS s
18193
+ FROM nodes WHERE valid_to IS NULL AND label IN (${labels.map(() => "?").join(",")})`
18194
+ ).get(...labels);
18195
+ return `${r.n}:${r.t ?? 0}:${r.s ?? 0}`;
18196
+ }
18176
18197
  /** Endpoints of edges TOUCHED since `ts` — created, re-seen, or CLOSED.
18177
18198
  * Closures matter as much as additions: a node that lost inflow is rank-dirty
18178
18199
  * while its own row never updated, so removal endpoints must seed the
@@ -78515,13 +78536,14 @@ function createWatchBreaker(deps) {
78515
78536
  }
78516
78537
 
78517
78538
  // src/engine.ts
78518
- var DAEMON_VERSION = true ? "2.0.6-dev.2654" : "2.0.0-alpha.0";
78539
+ var DAEMON_VERSION = true ? "2.0.6-dev.2657" : "2.0.0-alpha.0";
78519
78540
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
78520
78541
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
78521
78542
  var GIT_OP_MUTE_MS = 4e3;
78522
78543
  var REINDEX_DEBOUNCE_MS = 200;
78523
78544
  var IDENTITY_AUDIT_MAX_BYTES = 16 * 1024 * 1024;
78524
78545
  var TURN_REPLAY_LOOKBACK_MS = 7 * 24 * 60 * 6e4;
78546
+ var INDUCE_DISABLED_RECHECK_MS = 6 * 60 * 6e4;
78525
78547
  var CODEX_SWEEP_LOOKBACK_MS = 10 * 6e4;
78526
78548
  function appendIdentityAudit(path2, record2, line) {
78527
78549
  if (!record2.accepted && record2.score <= 0) return;
@@ -80434,11 +80456,16 @@ B: ${b}`,
80434
80456
  }
80435
80457
  });
80436
80458
  }
80459
+ let induceDisabledUntil = 0;
80437
80460
  const pullSkills = async () => {
80438
- if ((opts.syncConsent ?? consentNow().sync) === true) {
80461
+ if ((opts.syncConsent ?? consentNow().sync) === true && Date.now() >= induceDisabledUntil) {
80439
80462
  for (const req of communityInductionRequests(store)) {
80440
80463
  try {
80441
80464
  const r = await cloud.induceSkill({ members: req.members, context: req.context });
80465
+ if (r.status === "disabled") {
80466
+ induceDisabledUntil = Date.now() + INDUCE_DISABLED_RECHECK_MS;
80467
+ break;
80468
+ }
80442
80469
  if (r.status === "induced" || r.status === "refreshed") {
80443
80470
  console.log(`[errata] skill ${r.status} for community ${req.communityId}: ${r.title ?? r.skillId}`);
80444
80471
  }
@@ -81230,10 +81257,16 @@ function internalPackageNames(workspaceRoot) {
81230
81257
  return names;
81231
81258
  }
81232
81259
  var cache = /* @__PURE__ */ new Map();
81260
+ var cacheKeys = /* @__PURE__ */ new Map();
81233
81261
  function publicSymbolIndex(store, workspaceRoot, deps = {}) {
81234
81262
  const now = deps.nowMs ?? Date.now();
81263
+ const fileRows = store.labelFingerprint?.(["File"]);
81264
+ const key = fileRows === void 0 ? void 0 : `${fileRows}|${store.countNodesByLabel("Module")}`;
81235
81265
  const cached2 = cache.get(workspaceRoot);
81236
- if (cached2 && now - cached2.builtAt < INDEX_TTL_MS) return cached2;
81266
+ if (cached2) {
81267
+ const fresh = key !== void 0 ? cacheKeys.get(workspaceRoot) === key : now - cached2.builtAt < INDEX_TTL_MS;
81268
+ if (fresh) return cached2;
81269
+ }
81237
81270
  const readHead = deps.readHead ?? ((absPath) => {
81238
81271
  try {
81239
81272
  return readFileSync28(absPath, "utf8").slice(0, HEAD_BYTES);
@@ -81270,6 +81303,8 @@ function publicSymbolIndex(store, workspaceRoot, deps = {}) {
81270
81303
  }
81271
81304
  const index = { symbols, builtAt: now };
81272
81305
  cache.set(workspaceRoot, index);
81306
+ if (key !== void 0) cacheKeys.set(workspaceRoot, key);
81307
+ else cacheKeys.delete(workspaceRoot);
81273
81308
  return index;
81274
81309
  }
81275
81310
  var PRESERVED_SYMBOLS_CAP = 8;
@@ -81404,7 +81439,16 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
81404
81439
  ...profile.repoLocatorAliases ? { aliases: profile.repoLocatorAliases } : {},
81405
81440
  ...profile.aka ? { aka: profile.aka } : {}
81406
81441
  };
81407
- const fileLexicon = buildFileLexicon(store);
81442
+ const fileLexicon = opts.fileLexicon ?? buildFileLexicon(store);
81443
+ const nodeMemo = /* @__PURE__ */ new Map();
81444
+ const nodeOf = (id) => {
81445
+ let n = nodeMemo.get(id);
81446
+ if (n === void 0) {
81447
+ n = store.getNode(id) ?? opts.sharedStore?.getNode(id) ?? null;
81448
+ nodeMemo.set(id, n);
81449
+ }
81450
+ return n;
81451
+ };
81408
81452
  const shareable = (n) => {
81409
81453
  const preserved = symbolIndex ? preservedSymbolsFor(n.description, symbolIndex) : [];
81410
81454
  const fileRefs = generalizeFileRefs(n.description, fileLexicon, level);
@@ -81529,7 +81573,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
81529
81573
  for (const sourceId of anchorSources) {
81530
81574
  const targets = [];
81531
81575
  for (const e of store.outEdges(sourceId, [...ANCHOR_EDGES])) {
81532
- const t = store.getNode(e.to);
81576
+ const t = nodeOf(e.to);
81533
81577
  if (!t || !ANCHOR_TARGET_LABELS.includes(t.label)) continue;
81534
81578
  if (e.type === "OCCURS_IN" && t.label !== "Language") continue;
81535
81579
  if (e.type === "DEPENDS_ON" && t.label !== "Package") continue;
@@ -81552,7 +81596,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
81552
81596
  }
81553
81597
  }
81554
81598
  const dg = digest(targets.map(({ e, wireId }) => `${e.type}>${wireId}`).sort());
81555
- const sourceNode = store.getNode(sourceId);
81599
+ const sourceNode = nodeOf(sourceId);
81556
81600
  if (sourceNode?.attrs["anchorsContributedDigest"] === dg) continue;
81557
81601
  anchorDigests[sourceId] = dg;
81558
81602
  const wireFrom = seen.has(sourceId) || !sourceNode ? sourceId : contributedWireId(sourceNode);
@@ -81582,12 +81626,12 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
81582
81626
  if (project) {
81583
81627
  const now = Date.now();
81584
81628
  for (const sourceId of anchorSources) {
81585
- const source = store.getNode(sourceId);
81629
+ const source = nodeOf(sourceId);
81586
81630
  if (!source) continue;
81587
81631
  const wireFrom = seen.has(sourceId) ? sourceId : contributedWireId(source);
81588
81632
  const planned = [];
81589
81633
  for (const e of store.outEdges(sourceId, ["ANCHORED_AT"])) {
81590
- const target = store.getNode(e.to);
81634
+ const target = nodeOf(e.to);
81591
81635
  if (!target) continue;
81592
81636
  if (!isAnchorLabelKind(target.label)) continue;
81593
81637
  const isFile = target.label === "File";
@@ -81664,7 +81708,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
81664
81708
  const outs = [...dedup.values()].map((e) => ({ e, wireTo: semanticEndpoints.get(e.to) })).filter((x) => x.wireTo != null);
81665
81709
  if (outs.length === 0) continue;
81666
81710
  const dg = digest(outs.map(({ e, wireTo }) => `${e.type}>${wireTo}`).sort());
81667
- const owner = store.getNode(sourceId) ?? opts.sharedStore?.getNode(sourceId);
81711
+ const owner = nodeOf(sourceId);
81668
81712
  const digestSkip = owner?.attrs["semanticEdgesContributedDigest"] === dg;
81669
81713
  if (!digestSkip) semanticEdgeDigests[sourceId] = dg;
81670
81714
  for (const { e, wireTo } of outs) {
@@ -81721,7 +81765,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
81721
81765
  const heldOrphans = /* @__PURE__ */ new Set();
81722
81766
  for (const wn of nodes) {
81723
81767
  if (wn.label !== "RootCause" && wn.label !== "Solution") continue;
81724
- const src = store.getNode(wn.id) ?? opts.sharedStore?.getNode(wn.id);
81768
+ const src = nodeOf(wn.id);
81725
81769
  if (typeof src?.attrs["contributedAtSeq"] === "number") continue;
81726
81770
  if (bindingEndpoints.has(wn.id)) continue;
81727
81771
  heldOrphans.add(wn.id);
@@ -81805,8 +81849,58 @@ function buildCausalEdgeBackfill(store, profile, daemonVersion) {
81805
81849
  return { ...base, payloadDigest: digest(base) };
81806
81850
  }
81807
81851
 
81852
+ // src/live-sync.ts
81853
+ var LIVE_SYNC_DEFAULTS = {
81854
+ enabled: true,
81855
+ debounceMs: 1e4,
81856
+ maxWaitMs: 6e4
81857
+ };
81858
+ var BACKOFF_BASE_MS = 5 * 6e4;
81859
+ var BACKOFF_MAX_MS = 60 * 6e4;
81860
+ function positiveMs(raw2, fallback) {
81861
+ if (raw2 === void 0 || raw2.trim() === "") return fallback;
81862
+ const n = Number(raw2);
81863
+ return Number.isFinite(n) && n > 0 ? n : fallback;
81864
+ }
81865
+ function liveSyncConfig(env2 = process.env) {
81866
+ const flag = (env2["ERRATA_LIVE_SYNC"] ?? "").trim().toLowerCase();
81867
+ const debounceMs = positiveMs(env2["ERRATA_LIVE_SYNC_DEBOUNCE_MS"], LIVE_SYNC_DEFAULTS.debounceMs);
81868
+ return {
81869
+ enabled: !["0", "false", "off", "no"].includes(flag),
81870
+ debounceMs,
81871
+ // A ceiling below the debounce would make the debounce meaningless.
81872
+ maxWaitMs: Math.max(debounceMs, positiveMs(env2["ERRATA_LIVE_SYNC_MAX_WAIT_MS"], LIVE_SYNC_DEFAULTS.maxWaitMs))
81873
+ };
81874
+ }
81875
+ function liveSyncEnabled(env2 = process.env) {
81876
+ return liveSyncConfig(env2).enabled;
81877
+ }
81878
+ function liveShipDelay(now, dirtySince, cfg) {
81879
+ const ceiling = dirtySince + cfg.maxWaitMs - now;
81880
+ return Math.max(0, Math.min(cfg.debounceMs, ceiling));
81881
+ }
81882
+ function liveBackoffMs(failures) {
81883
+ if (failures <= 0) return 0;
81884
+ return Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** (failures - 1));
81885
+ }
81886
+
81808
81887
  // src/drain-build.ts
81809
81888
  init_generalize_graph();
81889
+ var LEXICON_SYMBOL_LABELS = ["Function", "Method", "Class", "Interface", "Const", "Comment"];
81890
+ function codeGraphKey(store) {
81891
+ const files = store.labelFingerprint?.(["File"]);
81892
+ if (files === void 0) return null;
81893
+ return [files, ...LEXICON_SYMBOL_LABELS.map((l) => store.countNodesByLabel(l))].join("|");
81894
+ }
81895
+ var lexiconCache = /* @__PURE__ */ new WeakMap();
81896
+ function lexiconsFor(store) {
81897
+ const key = codeGraphKey(store);
81898
+ const hit = key !== null ? lexiconCache.get(store) : void 0;
81899
+ if (hit && hit.key === key) return hit;
81900
+ const built = { symbols: buildSymbolLexicon(store), files: buildFileLexicon(store) };
81901
+ if (key !== null) lexiconCache.set(store, { key, ...built });
81902
+ return built;
81903
+ }
81810
81904
  function buildDrainContext(store, input) {
81811
81905
  return buildContextIngest(store, input.profile, input.daemonVersion, input.ignorePatterns, {
81812
81906
  ...input.includePackages !== void 0 ? { includePackages: input.includePackages } : {}
@@ -81814,9 +81908,10 @@ function buildDrainContext(store, input) {
81814
81908
  }
81815
81909
  function buildDrainInstances(store, sharedStore, input) {
81816
81910
  let lexicon;
81911
+ let fileLexicon;
81817
81912
  let lexiconError;
81818
81913
  try {
81819
- lexicon = buildSymbolLexicon(store);
81914
+ ({ symbols: lexicon, files: fileLexicon } = lexiconsFor(store));
81820
81915
  } catch (err2) {
81821
81916
  lexiconError = err2 instanceof Error ? err2.message : String(err2);
81822
81917
  }
@@ -81829,6 +81924,7 @@ function buildDrainInstances(store, sharedStore, input) {
81829
81924
  composition = c;
81830
81925
  },
81831
81926
  ...lexicon ? { lexicon } : {},
81927
+ ...fileLexicon ? { fileLexicon } : {},
81832
81928
  ...input.project ? { project: input.project } : {}
81833
81929
  });
81834
81930
  return {
@@ -85209,6 +85305,8 @@ async function startMultiDaemon(opts = {}) {
85209
85305
  let serverPending = null;
85210
85306
  const boundaryListeners = [];
85211
85307
  let boundaryFlushing = false;
85308
+ let pushRequested = false;
85309
+ let shippedFingerprint = "";
85212
85310
  let settleRunning = false;
85213
85311
  let boundaryFlushStartedAt = 0;
85214
85312
  let boundaryFlushStage = "";
@@ -85278,6 +85376,36 @@ async function startMultiDaemon(opts = {}) {
85278
85376
  boundaryFlushStage = "";
85279
85377
  endFlushPass?.();
85280
85378
  endFlushPass = null;
85379
+ if (pushRequested) {
85380
+ pushRequested = false;
85381
+ setImmediate(() => {
85382
+ void daemon.liveShip().catch(() => {
85383
+ });
85384
+ });
85385
+ }
85386
+ };
85387
+ const instanceFingerprint = () => {
85388
+ const cfg2 = loadConfig();
85389
+ const parts2 = [];
85390
+ for (const r of records) {
85391
+ if (!effectiveConsent(cfg2, r.engine.profile).sync) continue;
85392
+ parts2.push(`${r.id}=${r.engine.store.labelFingerprint?.(INSTANCE_LABELS) ?? `?${Date.now()}`}`);
85393
+ }
85394
+ if (parts2.length === 0) return "";
85395
+ parts2.push(`shared=${sharedStore.labelFingerprint?.(["RootCause"]) ?? `?${Date.now()}`}`);
85396
+ return parts2.join("|");
85397
+ };
85398
+ const runSettle = async () => {
85399
+ if (settleRunning) return;
85400
+ settleRunning = true;
85401
+ try {
85402
+ for (const r of records) {
85403
+ const donePass = markPass(`settle:${r.engine.profile.name}`);
85404
+ await r.engine.embedSettled().finally(donePass);
85405
+ }
85406
+ } finally {
85407
+ settleRunning = false;
85408
+ }
85281
85409
  };
85282
85410
  const fireBoundary = () => {
85283
85411
  for (const l of boundaryListeners) {
@@ -86559,22 +86687,17 @@ async function startMultiDaemon(opts = {}) {
86559
86687
  return { written, pruned: pruned2 };
86560
86688
  },
86561
86689
  async sessionBoundaryFlush() {
86562
- if (!settleRunning) {
86563
- settleRunning = true;
86564
- try {
86565
- for (const r of records) {
86566
- const donePass = markPass(`settle:${r.engine.profile.name}`);
86567
- await r.engine.embedSettled().finally(donePass);
86568
- }
86569
- } finally {
86570
- settleRunning = false;
86571
- }
86690
+ await runSettle();
86691
+ if (boundaryFlushing) {
86692
+ pushRequested = true;
86693
+ return { uploaded: 0, written: 0, pruned: 0, skipped: "in-flight" };
86572
86694
  }
86573
- if (boundaryFlushing) return { uploaded: 0, written: 0, pruned: 0, skipped: "in-flight" };
86574
86695
  beginFlush("instances");
86575
86696
  try {
86576
86697
  flushStage("instances");
86698
+ const startedFrom = instanceFingerprint();
86577
86699
  const inst = await daemon.syncInstancesPublic();
86700
+ shippedFingerprint = startedFrom;
86578
86701
  flushStage("skills");
86579
86702
  const skills = await daemon.syncSkillsAll();
86580
86703
  return {
@@ -86587,6 +86710,36 @@ async function startMultiDaemon(opts = {}) {
86587
86710
  endFlush();
86588
86711
  }
86589
86712
  },
86713
+ pendingLiveFingerprint() {
86714
+ const fp = instanceFingerprint();
86715
+ return fp && fp !== shippedFingerprint ? fp : null;
86716
+ },
86717
+ async liveShip() {
86718
+ if (!liveSyncEnabled()) return { uploaded: 0, skipped: "disabled" };
86719
+ if (daemon.pendingLiveFingerprint() === null) return { uploaded: 0, skipped: "unchanged" };
86720
+ if (boundaryFlushing) {
86721
+ pushRequested = true;
86722
+ return { uploaded: 0, skipped: "in-flight" };
86723
+ }
86724
+ await runSettle();
86725
+ if (boundaryFlushing) {
86726
+ pushRequested = true;
86727
+ return { uploaded: 0, skipped: "in-flight" };
86728
+ }
86729
+ beginFlush("live");
86730
+ try {
86731
+ const startedFrom = instanceFingerprint();
86732
+ const inst = await daemon.syncInstancesPublic();
86733
+ shippedFingerprint = startedFrom;
86734
+ return {
86735
+ uploaded: inst.uploaded,
86736
+ ...inst.skipped ? { skipped: inst.skipped } : {},
86737
+ ...inst.errors ? { errors: inst.errors } : {}
86738
+ };
86739
+ } finally {
86740
+ endFlush();
86741
+ }
86742
+ },
86590
86743
  onSessionBoundary(cb) {
86591
86744
  boundaryListeners.push(cb);
86592
86745
  },
@@ -90053,8 +90206,53 @@ async function cmdDash(args2) {
90053
90206
  void flushed.finally(() => triggerConsolidation(force));
90054
90207
  };
90055
90208
  handle2.onSessionBoundary(() => settleThenConsolidate("session-end", true));
90209
+ const liveCfg = liveSyncConfig();
90210
+ let liveTimer = null;
90211
+ let liveDirtySince = 0;
90212
+ let liveSeenFingerprint = "";
90213
+ let liveFailures = 0;
90214
+ let liveBackoffUntil = 0;
90215
+ const fireLiveShip = () => {
90216
+ liveTimer = null;
90217
+ void handle2.liveShip().then((r) => {
90218
+ if (r.skipped === "in-flight") return;
90219
+ liveDirtySince = 0;
90220
+ if (r.errors && r.errors.length > 0) {
90221
+ liveFailures++;
90222
+ const pause = liveBackoffMs(liveFailures);
90223
+ liveBackoffUntil = Date.now() + pause;
90224
+ console.warn(
90225
+ `[live] push had ${r.errors.length} error(s); pausing the live push ${Math.round(pause / 6e4)}m (quiescence / session-end still flush)`
90226
+ );
90227
+ return;
90228
+ }
90229
+ liveFailures = 0;
90230
+ if (r.uploaded > 0) console.log(`[live] uploaded ${r.uploaded} node(s)/edge(s)`);
90231
+ }).catch(() => {
90232
+ });
90233
+ };
90234
+ const probeLivePush = () => {
90235
+ if (!liveCfg.enabled || Date.now() < liveBackoffUntil) return;
90236
+ let fp;
90237
+ try {
90238
+ fp = handle2.pendingLiveFingerprint();
90239
+ } catch {
90240
+ return;
90241
+ }
90242
+ if (fp === null) {
90243
+ liveSeenFingerprint = "";
90244
+ return;
90245
+ }
90246
+ if (fp === liveSeenFingerprint) return;
90247
+ liveSeenFingerprint = fp;
90248
+ const now = Date.now();
90249
+ if (!liveDirtySince) liveDirtySince = now;
90250
+ if (liveTimer) clearTimeout(liveTimer);
90251
+ liveTimer = setTimeout(fireLiveShip, liveShipDelay(now, liveDirtySince, liveCfg));
90252
+ };
90056
90253
  let ticking = false;
90057
90254
  const interval = setInterval(() => {
90255
+ probeLivePush();
90058
90256
  if (ticking) return;
90059
90257
  ticking = true;
90060
90258
  void handle2.tickAll().then((m) => {
@@ -90295,6 +90493,7 @@ shutting down\u2026 (${signal})`);
90295
90493
  clearInterval(interval);
90296
90494
  clearInterval(nightlyInterval);
90297
90495
  if (quiesceTimer) clearTimeout(quiesceTimer);
90496
+ if (liveTimer) clearTimeout(liveTimer);
90298
90497
  await handle2.stop();
90299
90498
  process.exit(0);
90300
90499
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.6-dev.2654",
3
+ "version": "2.0.6-dev.2657",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/pass-worker.mjs CHANGED
@@ -24588,6 +24588,10 @@ function rowToEdge(r) {
24588
24588
  }
24589
24589
  var SqliteGraphStore = class {
24590
24590
  db;
24591
+ // Typed edge lookups build their SQL from the number of types asked for, and
24592
+ // used to prepare it on every call: ~600ms of a 2.5s drain build on a live
24593
+ // 225k-node store was statement compilation alone. Keyed by the SQL text.
24594
+ typedEdgeStmts = /* @__PURE__ */ new Map();
24591
24595
  stmts;
24592
24596
  /** Unified graph-mutation counter (see GraphStore.mutationCount). Bumped on
24593
24597
  * every node/edge upsert — code-index and capture alike — so the consolidation
@@ -25092,6 +25096,14 @@ var SqliteGraphStore = class {
25092
25096
  const rows = this.db.prepare(sql).all(params);
25093
25097
  return rows.map(rowToNode);
25094
25098
  }
25099
+ typedEdgeStmt(sql) {
25100
+ let stmt = this.typedEdgeStmts.get(sql);
25101
+ if (!stmt) {
25102
+ stmt = this.db.prepare(sql);
25103
+ this.typedEdgeStmts.set(sql, stmt);
25104
+ }
25105
+ return stmt;
25106
+ }
25095
25107
  outEdges(nodeId, types) {
25096
25108
  if (!types || types.length === 0) {
25097
25109
  const rows2 = this.stmts.outEdges.all(nodeId);
@@ -25102,7 +25114,7 @@ var SqliteGraphStore = class {
25102
25114
  types.forEach((t, i2) => {
25103
25115
  params[`t${i2}`] = t;
25104
25116
  });
25105
- const rows = this.db.prepare(
25117
+ const rows = this.typedEdgeStmt(
25106
25118
  `SELECT * FROM edges WHERE from_id = :from_id AND type IN (${placeholders}) AND valid_to IS NULL`
25107
25119
  ).all(params);
25108
25120
  return rows.map(rowToEdge);
@@ -25117,7 +25129,7 @@ var SqliteGraphStore = class {
25117
25129
  types.forEach((t, i2) => {
25118
25130
  params[`t${i2}`] = t;
25119
25131
  });
25120
- const rows = this.db.prepare(
25132
+ const rows = this.typedEdgeStmt(
25121
25133
  `SELECT * FROM edges WHERE to_id = :to_id AND type IN (${placeholders}) AND valid_to IS NULL`
25122
25134
  ).all(params);
25123
25135
  return rows.map(rowToEdge);
@@ -25195,6 +25207,15 @@ var SqliteGraphStore = class {
25195
25207
  const rows = this.db.prepare("SELECT id FROM nodes WHERE valid_to IS NULL AND last_updated_at > ?").all(ts);
25196
25208
  return rows.map((r) => r.id);
25197
25209
  }
25210
+ labelFingerprint(labels) {
25211
+ if (labels.length === 0) return "0";
25212
+ const r = this.db.prepare(
25213
+ `SELECT COUNT(*) AS n, MAX(last_updated_at) AS t,
25214
+ MAX(COALESCE(last_reinforced_at_seq, created_at_seq, 0)) AS s
25215
+ FROM nodes WHERE valid_to IS NULL AND label IN (${labels.map(() => "?").join(",")})`
25216
+ ).get(...labels);
25217
+ return `${r.n}:${r.t ?? 0}:${r.s ?? 0}`;
25218
+ }
25198
25219
  /** Endpoints of edges TOUCHED since `ts` — created, re-seen, or CLOSED.
25199
25220
  * Closures matter as much as additions: a node that lost inflow is rank-dirty
25200
25221
  * while its own row never updated, so removal endpoints must seed the