@inerrata-corporation/errata 2.0.22-dev.4676 → 2.0.23-dev.4682

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.
@@ -17935,7 +17935,12 @@ CREATE UNIQUE INDEX IF NOT EXISTS edges_triple ON edges(from_id, type, to_id);
17935
17935
  var TEMPORAL_INDEX_SQL = `
17936
17936
  CREATE INDEX IF NOT EXISTS nodes_live ON nodes(label, valid_to);
17937
17937
  CREATE INDEX IF NOT EXISTS nodes_live_count ON nodes(valid_to);
17938
- CREATE INDEX IF NOT EXISTS nodes_live_recent ON nodes(label, last_updated_at DESC) WHERE valid_to IS NULL;
17938
+ -- Problems by recency (the only label read newest-first). Over every live row,
17939
+ -- each write that stamps last_updated_at moved a code symbol's entry from a
17940
+ -- scattered leaf: ~19 KB per position-only symbol rewrite on a 1.1 GB store.
17941
+ -- label stays the leading column so label = 'Problem' plans without a sort.
17942
+ DROP INDEX IF EXISTS nodes_live_recent;
17943
+ CREATE INDEX IF NOT EXISTS nodes_live_problem_recent ON nodes(label, last_updated_at DESC) WHERE valid_to IS NULL AND label = 'Problem';
17939
17944
  -- "Anything created since the watermark?" guards (the fold, regression and
17940
17945
  -- canonical-bind sweeps run one per session boundary). Without it the planner
17941
17946
  -- walks nodes_live and reads every live row of the label to test created_at:
@@ -18009,24 +18014,34 @@ CREATE INDEX IF NOT EXISTS nodes_closed_relpath ON nodes(json_extract(attrs_json
18009
18014
  -- Live rows by name, for looking up the few symbols a statement mentions
18010
18015
  -- instead of hydrating every code symbol per minted problem.
18011
18016
  CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) WHERE valid_to IS NULL;
18017
+ -- The six single-attr lookups below are partial on the attr being present: each
18018
+ -- is read only by equality (or IN), which implies it. Keyed on NULL for the ~95%
18019
+ -- of rows that lack the attr, every write to such a row's attrs rewrote its NULL
18020
+ -- entry: ~25 KB per position-only symbol rewrite on a 1.1 GB store (2026-10-21).
18021
+ DROP INDEX IF EXISTS nodes_anchor_hint_path;
18022
+ DROP INDEX IF EXISTS nodes_command_signature;
18023
+ DROP INDEX IF EXISTS nodes_diag_command;
18024
+ DROP INDEX IF EXISTS nodes_lsp_rel_path;
18025
+ DROP INDEX IF EXISTS nodes_cloud_node_id;
18026
+ DROP INDEX IF EXISTS nodes_enrichment_pending;
18012
18027
  -- Problems by the file their anchor hint names: the harvest upgrades hints for
18013
18028
  -- every edited file of every turn, and a scan parses every Problem's attrs.
18014
- CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path ON nodes(json_extract(attrs_json, '$.anchorHint.path')) WHERE valid_to IS NULL;
18029
+ CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path_v2 ON nodes(json_extract(attrs_json, '$.anchorHint.path')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.anchorHint.path') IS NOT NULL;
18015
18030
  -- Problems by the command / checker / file that opened them. The generalizer
18016
18031
  -- looked these up per shell or tool EVENT by hydrating every Problem: ~12s of
18017
18032
  -- every minute of daemon main-thread time on a 3.4k-Problem store (2026-10-05).
18018
- CREATE INDEX IF NOT EXISTS nodes_command_signature ON nodes(json_extract(attrs_json, '$.commandSignature')) WHERE valid_to IS NULL;
18019
- CREATE INDEX IF NOT EXISTS nodes_diag_command ON nodes(json_extract(attrs_json, '$.diagCommand')) WHERE valid_to IS NULL;
18020
- CREATE INDEX IF NOT EXISTS nodes_lsp_rel_path ON nodes(json_extract(attrs_json, '$.lspRelPath')) WHERE valid_to IS NULL;
18033
+ CREATE INDEX IF NOT EXISTS nodes_command_signature_v2 ON nodes(json_extract(attrs_json, '$.commandSignature')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.commandSignature') IS NOT NULL;
18034
+ CREATE INDEX IF NOT EXISTS nodes_diag_command_v2 ON nodes(json_extract(attrs_json, '$.diagCommand')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.diagCommand') IS NOT NULL;
18035
+ CREATE INDEX IF NOT EXISTS nodes_lsp_rel_path_v2 ON nodes(json_extract(attrs_json, '$.lspRelPath')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.lspRelPath') IS NOT NULL;
18021
18036
  -- Local nodes by the cloud twin their upload receipt recorded. Every burst /
18022
18037
  -- search fusion folds the Network copy of the caller's own knowledge back onto
18023
18038
  -- the local node; unindexed, the reverse lookup was a ~300ms attrs scan per call.
18024
- CREATE INDEX IF NOT EXISTS nodes_cloud_node_id ON nodes(json_extract(attrs_json, '$.cloudNodeId')) WHERE valid_to IS NULL;
18039
+ CREATE INDEX IF NOT EXISTS nodes_cloud_node_id_v2 ON nodes(json_extract(attrs_json, '$.cloudNodeId')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.cloudNodeId') IS NOT NULL;
18025
18040
  -- Nodes still waiting on a root-cause ask. The MCP server checks this on EVERY
18026
18041
  -- tool response (the enrichment piggyback), so it must not parse every
18027
- -- Solution's and Problem's attrs per call. Not partial: the Problem pull reads
18042
+ -- Solution's and Problem's attrs per call. Any version: the Problem pull reads
18028
18043
  -- closed versions too.
18029
- CREATE INDEX IF NOT EXISTS nodes_enrichment_pending ON nodes(json_extract(attrs_json, '$.enrichmentPending'));
18044
+ CREATE INDEX IF NOT EXISTS nodes_enrichment_pending_v2 ON nodes(json_extract(attrs_json, '$.enrichmentPending')) WHERE json_extract(attrs_json, '$.enrichmentPending') IS NOT NULL;
18030
18045
  -- Revisit flags, any label, any version: listNeedsRevisit's as-of read runs on
18031
18046
  -- every context render and otherwise json-checks every row (10.3 s on a 324k-row
18032
18047
  -- store, to return zero flags). The predicate must stay textually identical to
@@ -18092,6 +18107,30 @@ function rowToEdge(r) {
18092
18107
  }
18093
18108
  var FINGERPRINT_INDEXED_LABELS = ["Problem", "Vulnerability", "Solution", "RootCause"];
18094
18109
  var NODE_LABELS_CHUNK = 256;
18110
+ var NODE_UPSERT_COLUMNS = [
18111
+ "label",
18112
+ "description",
18113
+ "extraction_confidence",
18114
+ "embedding_json",
18115
+ "embedding_blob",
18116
+ "cumulative_surprise",
18117
+ "peak_surprise",
18118
+ "cumulative_hits",
18119
+ "last_updated_at",
18120
+ "memory_tier",
18121
+ "page_rank",
18122
+ "is_landmark",
18123
+ "community",
18124
+ "stability",
18125
+ "last_reinforced_at_seq",
18126
+ "attrs_json"
18127
+ ];
18128
+ var EDGE_UPSERT_COLUMNS = ["confidence", "last_seen_at", "nav_successes", "nav_failures", "valid_to", "attrs_json"];
18129
+ function sameSqlValue(a, b) {
18130
+ if (a === b || a == null && b == null) return true;
18131
+ if (a instanceof Uint8Array && b instanceof Uint8Array) return Buffer.compare(a, b) === 0;
18132
+ return false;
18133
+ }
18095
18134
  var PRIOR_TAG_MARKER = '"priorTag":true';
18096
18135
  var SqliteGraphStore = class {
18097
18136
  db;
@@ -18099,6 +18138,8 @@ var SqliteGraphStore = class {
18099
18138
  // used to prepare it on every call: ~600ms of a 2.5s drain build on a live
18100
18139
  // 225k-node store was statement compilation alone. Keyed by the SQL text.
18101
18140
  typedEdgeStmts = /* @__PURE__ */ new Map();
18141
+ /** Narrow merge updates, keyed by table and the columns they set. */
18142
+ changedColumnStmts = /* @__PURE__ */ new Map();
18102
18143
  stmts;
18103
18144
  /** Unified graph-mutation counter (see GraphStore.mutationCount). Bumped on
18104
18145
  * every node/edge upsert — code-index and capture alike — so the consolidation
@@ -18182,6 +18223,8 @@ var SqliteGraphStore = class {
18182
18223
  last_reinforced_at_seq = excluded.last_reinforced_at_seq,
18183
18224
  attrs_json = excluded.attrs_json
18184
18225
  `),
18226
+ nodeUpsertTarget: this.db.prepare(`SELECT rowid AS rid, ${NODE_UPSERT_COLUMNS.join(", ")} FROM nodes WHERE id = ?`),
18227
+ edgeUpsertTarget: this.db.prepare(`SELECT rowid AS rid, ${EDGE_UPSERT_COLUMNS.join(", ")} FROM edges WHERE from_id = ? AND type = ? AND to_id = ?`),
18185
18228
  getNode: this.db.prepare("SELECT nodes.*, (SELECT x.attrs_json FROM node_exposure x WHERE x.id = nodes.id) AS exposure_json FROM nodes WHERE id = ?"),
18186
18229
  exposureOf: this.db.prepare("SELECT attrs_json FROM node_exposure WHERE id = ?"),
18187
18230
  writeExposure: this.db.prepare("INSERT INTO node_exposure(id, attrs_json) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET attrs_json = excluded.attrs_json"),
@@ -18550,7 +18593,7 @@ var SqliteGraphStore = class {
18550
18593
  }
18551
18594
  mergeNode(node) {
18552
18595
  this.mutations++;
18553
- this.stmts.insertNode.run({
18596
+ const values = {
18554
18597
  id: node.id,
18555
18598
  label: node.label,
18556
18599
  description: node.description,
@@ -18577,7 +18620,23 @@ var SqliteGraphStore = class {
18577
18620
  created_at_seq: node.createdAtSeq ?? null,
18578
18621
  last_reinforced_at_seq: node.lastReinforcedAtSeq ?? null,
18579
18622
  attrs_json: this.nodeAttrsJson(node.id, node.attrs ?? {})
18580
- });
18623
+ };
18624
+ const current = this.stmts.nodeUpsertTarget.get(node.id);
18625
+ if (current) this.writeChangedColumns("nodes", NODE_UPSERT_COLUMNS, current, values);
18626
+ else this.stmts.insertNode.run(values);
18627
+ }
18628
+ /** What the upsert's ON CONFLICT would write over `current`, setting only the
18629
+ * columns whose value moves (NODE_UPSERT_COLUMNS says why). */
18630
+ writeChangedColumns(table, columns2, current, next) {
18631
+ const changed = columns2.filter((c) => !sameSqlValue(current[c], next[c]));
18632
+ if (changed.length === 0) return;
18633
+ const key = `${table}:${changed.join(",")}`;
18634
+ let stmt2 = this.changedColumnStmts.get(key);
18635
+ if (!stmt2) {
18636
+ stmt2 = this.db.prepare(`UPDATE ${table} SET ${changed.map((c) => `${c} = :${c}`).join(", ")} WHERE rowid = :rid`);
18637
+ this.changedColumnStmts.set(key, stmt2);
18638
+ }
18639
+ stmt2.run({ rid: current["rid"], ...Object.fromEntries(changed.map((c) => [c, next[c]])) });
18581
18640
  }
18582
18641
  /** mergeNode, unless the live row already holds this content. Content is what
18583
18642
  * the columns a merge rewrites from the producer (label, description, attrs,
@@ -18711,7 +18770,7 @@ var SqliteGraphStore = class {
18711
18770
  return;
18712
18771
  }
18713
18772
  this.mutations++;
18714
- this.stmts.insertEdge.run({
18773
+ const values = {
18715
18774
  id: edge.id,
18716
18775
  from_id: edge.from,
18717
18776
  to_id: edge.to,
@@ -18725,6 +18784,16 @@ var SqliteGraphStore = class {
18725
18784
  valid_from: edge.validFrom ?? edge.createdAt,
18726
18785
  valid_to: edge.validTo ?? null,
18727
18786
  attrs_json: JSON.stringify(edge.attrs ?? {})
18787
+ };
18788
+ const current = this.stmts.edgeUpsertTarget.get(edge.from, edge.type, edge.to);
18789
+ if (!current) {
18790
+ this.stmts.insertEdge.run(values);
18791
+ return;
18792
+ }
18793
+ this.writeChangedColumns("edges", EDGE_UPSERT_COLUMNS, current, {
18794
+ ...values,
18795
+ confidence: Math.max(current["confidence"], edge.confidence),
18796
+ valid_to: null
18728
18797
  });
18729
18798
  }
18730
18799
  /** mergeEdge, unless the live edge already holds this content: same attrs, and
@@ -18939,8 +19008,8 @@ var SqliteGraphStore = class {
18939
19008
  after = rows[rows.length - 1].rid;
18940
19009
  }
18941
19010
  }
18942
- recentNodeIds(label, limit) {
18943
- const rows = this.db.prepare("SELECT id FROM nodes WHERE label = ? AND valid_to IS NULL ORDER BY last_updated_at DESC LIMIT ?").all(label, Math.max(0, limit));
19011
+ recentProblemIds(limit) {
19012
+ const rows = this.db.prepare("SELECT id FROM nodes WHERE label = 'Problem' AND valid_to IS NULL ORDER BY last_updated_at DESC LIMIT ?").all(Math.max(0, limit));
18944
19013
  return rows.map((r) => r.id);
18945
19014
  }
18946
19015
  recentOpenProblemCandidates(opts) {
@@ -19381,7 +19450,7 @@ var SqliteGraphStore = class {
19381
19450
  const slice = cloudIds.slice(i, i + CHUNK);
19382
19451
  if (slice.length === 0) continue;
19383
19452
  const rows = this.db.prepare(
19384
- `SELECT id, json_extract(attrs_json, '$.cloudNodeId') AS cloud FROM nodes INDEXED BY nodes_cloud_node_id WHERE json_extract(attrs_json, '$.cloudNodeId') IN (${slice.map(() => "?").join(",")}) AND valid_to IS NULL ORDER BY id`
19453
+ `SELECT id, json_extract(attrs_json, '$.cloudNodeId') AS cloud FROM nodes INDEXED BY nodes_cloud_node_id_v2 WHERE json_extract(attrs_json, '$.cloudNodeId') IN (${slice.map(() => "?").join(",")}) AND valid_to IS NULL ORDER BY id`
19385
19454
  ).all(...slice);
19386
19455
  for (const r of rows) out.set(r.cloud, [...out.get(r.cloud) ?? [], r.id]);
19387
19456
  }
package/errata.mjs CHANGED
@@ -20756,10 +20756,15 @@ function rowToEdge(r) {
20756
20756
  attrs: JSON.parse(r.attrs_json)
20757
20757
  };
20758
20758
  }
20759
+ function sameSqlValue(a, b) {
20760
+ if (a === b || a == null && b == null) return true;
20761
+ if (a instanceof Uint8Array && b instanceof Uint8Array) return Buffer.compare(a, b) === 0;
20762
+ return false;
20763
+ }
20759
20764
  function openGraphStore(opts) {
20760
20765
  return new SqliteGraphStore(opts);
20761
20766
  }
20762
- var PAGE_RANK_SCALE_KEY, TEXT_SEARCH_COLUMNS, LOCAL_RULE_OVERRIDES, SCHEMA_VERSION, SESSION_REPAIR_KEY, EXPOSURE_SIDE_KEY, SESSION_EVIDENCE_PREDICATE, SCHEMA_SQL, TEMPORAL_INDEX_SQL, FINGERPRINT_INDEXED_LABELS, NODE_LABELS_CHUNK, PRIOR_TAG_MARKER, SqliteGraphStore;
20767
+ var PAGE_RANK_SCALE_KEY, TEXT_SEARCH_COLUMNS, LOCAL_RULE_OVERRIDES, SCHEMA_VERSION, SESSION_REPAIR_KEY, EXPOSURE_SIDE_KEY, SESSION_EVIDENCE_PREDICATE, SCHEMA_SQL, TEMPORAL_INDEX_SQL, FINGERPRINT_INDEXED_LABELS, NODE_LABELS_CHUNK, NODE_UPSERT_COLUMNS, EDGE_UPSERT_COLUMNS, PRIOR_TAG_MARKER, SqliteGraphStore;
20763
20768
  var init_store = __esm({
20764
20769
  "../../packages/local-graph/src/store.ts"() {
20765
20770
  "use strict";
@@ -20914,7 +20919,12 @@ CREATE UNIQUE INDEX IF NOT EXISTS edges_triple ON edges(from_id, type, to_id);
20914
20919
  TEMPORAL_INDEX_SQL = `
20915
20920
  CREATE INDEX IF NOT EXISTS nodes_live ON nodes(label, valid_to);
20916
20921
  CREATE INDEX IF NOT EXISTS nodes_live_count ON nodes(valid_to);
20917
- CREATE INDEX IF NOT EXISTS nodes_live_recent ON nodes(label, last_updated_at DESC) WHERE valid_to IS NULL;
20922
+ -- Problems by recency (the only label read newest-first). Over every live row,
20923
+ -- each write that stamps last_updated_at moved a code symbol's entry from a
20924
+ -- scattered leaf: ~19 KB per position-only symbol rewrite on a 1.1 GB store.
20925
+ -- label stays the leading column so label = 'Problem' plans without a sort.
20926
+ DROP INDEX IF EXISTS nodes_live_recent;
20927
+ CREATE INDEX IF NOT EXISTS nodes_live_problem_recent ON nodes(label, last_updated_at DESC) WHERE valid_to IS NULL AND label = 'Problem';
20918
20928
  -- "Anything created since the watermark?" guards (the fold, regression and
20919
20929
  -- canonical-bind sweeps run one per session boundary). Without it the planner
20920
20930
  -- walks nodes_live and reads every live row of the label to test created_at:
@@ -20988,24 +20998,34 @@ CREATE INDEX IF NOT EXISTS nodes_closed_relpath ON nodes(json_extract(attrs_json
20988
20998
  -- Live rows by name, for looking up the few symbols a statement mentions
20989
20999
  -- instead of hydrating every code symbol per minted problem.
20990
21000
  CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) WHERE valid_to IS NULL;
21001
+ -- The six single-attr lookups below are partial on the attr being present: each
21002
+ -- is read only by equality (or IN), which implies it. Keyed on NULL for the ~95%
21003
+ -- of rows that lack the attr, every write to such a row's attrs rewrote its NULL
21004
+ -- entry: ~25 KB per position-only symbol rewrite on a 1.1 GB store (2026-10-21).
21005
+ DROP INDEX IF EXISTS nodes_anchor_hint_path;
21006
+ DROP INDEX IF EXISTS nodes_command_signature;
21007
+ DROP INDEX IF EXISTS nodes_diag_command;
21008
+ DROP INDEX IF EXISTS nodes_lsp_rel_path;
21009
+ DROP INDEX IF EXISTS nodes_cloud_node_id;
21010
+ DROP INDEX IF EXISTS nodes_enrichment_pending;
20991
21011
  -- Problems by the file their anchor hint names: the harvest upgrades hints for
20992
21012
  -- every edited file of every turn, and a scan parses every Problem's attrs.
20993
- CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path ON nodes(json_extract(attrs_json, '$.anchorHint.path')) WHERE valid_to IS NULL;
21013
+ CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path_v2 ON nodes(json_extract(attrs_json, '$.anchorHint.path')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.anchorHint.path') IS NOT NULL;
20994
21014
  -- Problems by the command / checker / file that opened them. The generalizer
20995
21015
  -- looked these up per shell or tool EVENT by hydrating every Problem: ~12s of
20996
21016
  -- every minute of daemon main-thread time on a 3.4k-Problem store (2026-10-05).
20997
- CREATE INDEX IF NOT EXISTS nodes_command_signature ON nodes(json_extract(attrs_json, '$.commandSignature')) WHERE valid_to IS NULL;
20998
- CREATE INDEX IF NOT EXISTS nodes_diag_command ON nodes(json_extract(attrs_json, '$.diagCommand')) WHERE valid_to IS NULL;
20999
- CREATE INDEX IF NOT EXISTS nodes_lsp_rel_path ON nodes(json_extract(attrs_json, '$.lspRelPath')) WHERE valid_to IS NULL;
21017
+ CREATE INDEX IF NOT EXISTS nodes_command_signature_v2 ON nodes(json_extract(attrs_json, '$.commandSignature')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.commandSignature') IS NOT NULL;
21018
+ CREATE INDEX IF NOT EXISTS nodes_diag_command_v2 ON nodes(json_extract(attrs_json, '$.diagCommand')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.diagCommand') IS NOT NULL;
21019
+ CREATE INDEX IF NOT EXISTS nodes_lsp_rel_path_v2 ON nodes(json_extract(attrs_json, '$.lspRelPath')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.lspRelPath') IS NOT NULL;
21000
21020
  -- Local nodes by the cloud twin their upload receipt recorded. Every burst /
21001
21021
  -- search fusion folds the Network copy of the caller's own knowledge back onto
21002
21022
  -- the local node; unindexed, the reverse lookup was a ~300ms attrs scan per call.
21003
- CREATE INDEX IF NOT EXISTS nodes_cloud_node_id ON nodes(json_extract(attrs_json, '$.cloudNodeId')) WHERE valid_to IS NULL;
21023
+ CREATE INDEX IF NOT EXISTS nodes_cloud_node_id_v2 ON nodes(json_extract(attrs_json, '$.cloudNodeId')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.cloudNodeId') IS NOT NULL;
21004
21024
  -- Nodes still waiting on a root-cause ask. The MCP server checks this on EVERY
21005
21025
  -- tool response (the enrichment piggyback), so it must not parse every
21006
- -- Solution's and Problem's attrs per call. Not partial: the Problem pull reads
21026
+ -- Solution's and Problem's attrs per call. Any version: the Problem pull reads
21007
21027
  -- closed versions too.
21008
- CREATE INDEX IF NOT EXISTS nodes_enrichment_pending ON nodes(json_extract(attrs_json, '$.enrichmentPending'));
21028
+ CREATE INDEX IF NOT EXISTS nodes_enrichment_pending_v2 ON nodes(json_extract(attrs_json, '$.enrichmentPending')) WHERE json_extract(attrs_json, '$.enrichmentPending') IS NOT NULL;
21009
21029
  -- Revisit flags, any label, any version: listNeedsRevisit's as-of read runs on
21010
21030
  -- every context render and otherwise json-checks every row (10.3 s on a 324k-row
21011
21031
  -- store, to return zero flags). The predicate must stay textually identical to
@@ -21014,6 +21034,25 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
21014
21034
  `;
21015
21035
  FINGERPRINT_INDEXED_LABELS = ["Problem", "Vulnerability", "Solution", "RootCause"];
21016
21036
  NODE_LABELS_CHUNK = 256;
21037
+ NODE_UPSERT_COLUMNS = [
21038
+ "label",
21039
+ "description",
21040
+ "extraction_confidence",
21041
+ "embedding_json",
21042
+ "embedding_blob",
21043
+ "cumulative_surprise",
21044
+ "peak_surprise",
21045
+ "cumulative_hits",
21046
+ "last_updated_at",
21047
+ "memory_tier",
21048
+ "page_rank",
21049
+ "is_landmark",
21050
+ "community",
21051
+ "stability",
21052
+ "last_reinforced_at_seq",
21053
+ "attrs_json"
21054
+ ];
21055
+ EDGE_UPSERT_COLUMNS = ["confidence", "last_seen_at", "nav_successes", "nav_failures", "valid_to", "attrs_json"];
21017
21056
  PRIOR_TAG_MARKER = '"priorTag":true';
21018
21057
  SqliteGraphStore = class {
21019
21058
  db;
@@ -21021,6 +21060,8 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
21021
21060
  // used to prepare it on every call: ~600ms of a 2.5s drain build on a live
21022
21061
  // 225k-node store was statement compilation alone. Keyed by the SQL text.
21023
21062
  typedEdgeStmts = /* @__PURE__ */ new Map();
21063
+ /** Narrow merge updates, keyed by table and the columns they set. */
21064
+ changedColumnStmts = /* @__PURE__ */ new Map();
21024
21065
  stmts;
21025
21066
  /** Unified graph-mutation counter (see GraphStore.mutationCount). Bumped on
21026
21067
  * every node/edge upsert — code-index and capture alike — so the consolidation
@@ -21104,6 +21145,8 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
21104
21145
  last_reinforced_at_seq = excluded.last_reinforced_at_seq,
21105
21146
  attrs_json = excluded.attrs_json
21106
21147
  `),
21148
+ nodeUpsertTarget: this.db.prepare(`SELECT rowid AS rid, ${NODE_UPSERT_COLUMNS.join(", ")} FROM nodes WHERE id = ?`),
21149
+ edgeUpsertTarget: this.db.prepare(`SELECT rowid AS rid, ${EDGE_UPSERT_COLUMNS.join(", ")} FROM edges WHERE from_id = ? AND type = ? AND to_id = ?`),
21107
21150
  getNode: this.db.prepare("SELECT nodes.*, (SELECT x.attrs_json FROM node_exposure x WHERE x.id = nodes.id) AS exposure_json FROM nodes WHERE id = ?"),
21108
21151
  exposureOf: this.db.prepare("SELECT attrs_json FROM node_exposure WHERE id = ?"),
21109
21152
  writeExposure: this.db.prepare("INSERT INTO node_exposure(id, attrs_json) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET attrs_json = excluded.attrs_json"),
@@ -21472,7 +21515,7 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
21472
21515
  }
21473
21516
  mergeNode(node2) {
21474
21517
  this.mutations++;
21475
- this.stmts.insertNode.run({
21518
+ const values = {
21476
21519
  id: node2.id,
21477
21520
  label: node2.label,
21478
21521
  description: node2.description,
@@ -21499,7 +21542,23 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
21499
21542
  created_at_seq: node2.createdAtSeq ?? null,
21500
21543
  last_reinforced_at_seq: node2.lastReinforcedAtSeq ?? null,
21501
21544
  attrs_json: this.nodeAttrsJson(node2.id, node2.attrs ?? {})
21502
- });
21545
+ };
21546
+ const current = this.stmts.nodeUpsertTarget.get(node2.id);
21547
+ if (current) this.writeChangedColumns("nodes", NODE_UPSERT_COLUMNS, current, values);
21548
+ else this.stmts.insertNode.run(values);
21549
+ }
21550
+ /** What the upsert's ON CONFLICT would write over `current`, setting only the
21551
+ * columns whose value moves (NODE_UPSERT_COLUMNS says why). */
21552
+ writeChangedColumns(table, columns2, current, next) {
21553
+ const changed = columns2.filter((c) => !sameSqlValue(current[c], next[c]));
21554
+ if (changed.length === 0) return;
21555
+ const key = `${table}:${changed.join(",")}`;
21556
+ let stmt2 = this.changedColumnStmts.get(key);
21557
+ if (!stmt2) {
21558
+ stmt2 = this.db.prepare(`UPDATE ${table} SET ${changed.map((c) => `${c} = :${c}`).join(", ")} WHERE rowid = :rid`);
21559
+ this.changedColumnStmts.set(key, stmt2);
21560
+ }
21561
+ stmt2.run({ rid: current["rid"], ...Object.fromEntries(changed.map((c) => [c, next[c]])) });
21503
21562
  }
21504
21563
  /** mergeNode, unless the live row already holds this content. Content is what
21505
21564
  * the columns a merge rewrites from the producer (label, description, attrs,
@@ -21633,7 +21692,7 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
21633
21692
  return;
21634
21693
  }
21635
21694
  this.mutations++;
21636
- this.stmts.insertEdge.run({
21695
+ const values = {
21637
21696
  id: edge2.id,
21638
21697
  from_id: edge2.from,
21639
21698
  to_id: edge2.to,
@@ -21647,6 +21706,16 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
21647
21706
  valid_from: edge2.validFrom ?? edge2.createdAt,
21648
21707
  valid_to: edge2.validTo ?? null,
21649
21708
  attrs_json: JSON.stringify(edge2.attrs ?? {})
21709
+ };
21710
+ const current = this.stmts.edgeUpsertTarget.get(edge2.from, edge2.type, edge2.to);
21711
+ if (!current) {
21712
+ this.stmts.insertEdge.run(values);
21713
+ return;
21714
+ }
21715
+ this.writeChangedColumns("edges", EDGE_UPSERT_COLUMNS, current, {
21716
+ ...values,
21717
+ confidence: Math.max(current["confidence"], edge2.confidence),
21718
+ valid_to: null
21650
21719
  });
21651
21720
  }
21652
21721
  /** mergeEdge, unless the live edge already holds this content: same attrs, and
@@ -21861,8 +21930,8 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
21861
21930
  after = rows[rows.length - 1].rid;
21862
21931
  }
21863
21932
  }
21864
- recentNodeIds(label, limit) {
21865
- const rows = this.db.prepare("SELECT id FROM nodes WHERE label = ? AND valid_to IS NULL ORDER BY last_updated_at DESC LIMIT ?").all(label, Math.max(0, limit));
21933
+ recentProblemIds(limit) {
21934
+ const rows = this.db.prepare("SELECT id FROM nodes WHERE label = 'Problem' AND valid_to IS NULL ORDER BY last_updated_at DESC LIMIT ?").all(Math.max(0, limit));
21866
21935
  return rows.map((r) => r.id);
21867
21936
  }
21868
21937
  recentOpenProblemCandidates(opts) {
@@ -22303,7 +22372,7 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
22303
22372
  const slice = cloudIds.slice(i2, i2 + CHUNK2);
22304
22373
  if (slice.length === 0) continue;
22305
22374
  const rows = this.db.prepare(
22306
- `SELECT id, json_extract(attrs_json, '$.cloudNodeId') AS cloud FROM nodes INDEXED BY nodes_cloud_node_id WHERE json_extract(attrs_json, '$.cloudNodeId') IN (${slice.map(() => "?").join(",")}) AND valid_to IS NULL ORDER BY id`
22375
+ `SELECT id, json_extract(attrs_json, '$.cloudNodeId') AS cloud FROM nodes INDEXED BY nodes_cloud_node_id_v2 WHERE json_extract(attrs_json, '$.cloudNodeId') IN (${slice.map(() => "?").join(",")}) AND valid_to IS NULL ORDER BY id`
22307
22376
  ).all(...slice);
22308
22377
  for (const r of rows) out2.set(r.cloud, [...out2.get(r.cloud) ?? [], r.id]);
22309
22378
  }
@@ -87394,7 +87463,7 @@ function createWatchBreaker(deps) {
87394
87463
  }
87395
87464
 
87396
87465
  // src/engine.ts
87397
- var DAEMON_VERSION = true ? "2.0.22-dev.4676" : "2.0.0-alpha.0";
87466
+ var DAEMON_VERSION = true ? "2.0.23-dev.4682" : "2.0.0-alpha.0";
87398
87467
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
87399
87468
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
87400
87469
  var GIT_OP_MUTE_MS = 4e3;
@@ -88036,7 +88105,7 @@ function createWorkspaceEngine(opts) {
88036
88105
  void outboundStack().then((stack) => {
88037
88106
  if (!(opts.syncConsent ?? consentNow().sync)) return;
88038
88107
  const norm = normStackTokens;
88039
- const seed = store.recentNodeIds("Problem", 6);
88108
+ const seed = store.recentProblemIds(6);
88040
88109
  const q = {
88041
88110
  stack,
88042
88111
  domains: norm(profile.domains),
@@ -16332,7 +16332,12 @@ CREATE UNIQUE INDEX IF NOT EXISTS edges_triple ON edges(from_id, type, to_id);
16332
16332
  var TEMPORAL_INDEX_SQL = `
16333
16333
  CREATE INDEX IF NOT EXISTS nodes_live ON nodes(label, valid_to);
16334
16334
  CREATE INDEX IF NOT EXISTS nodes_live_count ON nodes(valid_to);
16335
- CREATE INDEX IF NOT EXISTS nodes_live_recent ON nodes(label, last_updated_at DESC) WHERE valid_to IS NULL;
16335
+ -- Problems by recency (the only label read newest-first). Over every live row,
16336
+ -- each write that stamps last_updated_at moved a code symbol's entry from a
16337
+ -- scattered leaf: ~19 KB per position-only symbol rewrite on a 1.1 GB store.
16338
+ -- label stays the leading column so label = 'Problem' plans without a sort.
16339
+ DROP INDEX IF EXISTS nodes_live_recent;
16340
+ CREATE INDEX IF NOT EXISTS nodes_live_problem_recent ON nodes(label, last_updated_at DESC) WHERE valid_to IS NULL AND label = 'Problem';
16336
16341
  -- "Anything created since the watermark?" guards (the fold, regression and
16337
16342
  -- canonical-bind sweeps run one per session boundary). Without it the planner
16338
16343
  -- walks nodes_live and reads every live row of the label to test created_at:
@@ -16406,24 +16411,34 @@ CREATE INDEX IF NOT EXISTS nodes_closed_relpath ON nodes(json_extract(attrs_json
16406
16411
  -- Live rows by name, for looking up the few symbols a statement mentions
16407
16412
  -- instead of hydrating every code symbol per minted problem.
16408
16413
  CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) WHERE valid_to IS NULL;
16414
+ -- The six single-attr lookups below are partial on the attr being present: each
16415
+ -- is read only by equality (or IN), which implies it. Keyed on NULL for the ~95%
16416
+ -- of rows that lack the attr, every write to such a row's attrs rewrote its NULL
16417
+ -- entry: ~25 KB per position-only symbol rewrite on a 1.1 GB store (2026-10-21).
16418
+ DROP INDEX IF EXISTS nodes_anchor_hint_path;
16419
+ DROP INDEX IF EXISTS nodes_command_signature;
16420
+ DROP INDEX IF EXISTS nodes_diag_command;
16421
+ DROP INDEX IF EXISTS nodes_lsp_rel_path;
16422
+ DROP INDEX IF EXISTS nodes_cloud_node_id;
16423
+ DROP INDEX IF EXISTS nodes_enrichment_pending;
16409
16424
  -- Problems by the file their anchor hint names: the harvest upgrades hints for
16410
16425
  -- every edited file of every turn, and a scan parses every Problem's attrs.
16411
- CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path ON nodes(json_extract(attrs_json, '$.anchorHint.path')) WHERE valid_to IS NULL;
16426
+ CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path_v2 ON nodes(json_extract(attrs_json, '$.anchorHint.path')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.anchorHint.path') IS NOT NULL;
16412
16427
  -- Problems by the command / checker / file that opened them. The generalizer
16413
16428
  -- looked these up per shell or tool EVENT by hydrating every Problem: ~12s of
16414
16429
  -- every minute of daemon main-thread time on a 3.4k-Problem store (2026-10-05).
16415
- CREATE INDEX IF NOT EXISTS nodes_command_signature ON nodes(json_extract(attrs_json, '$.commandSignature')) WHERE valid_to IS NULL;
16416
- CREATE INDEX IF NOT EXISTS nodes_diag_command ON nodes(json_extract(attrs_json, '$.diagCommand')) WHERE valid_to IS NULL;
16417
- CREATE INDEX IF NOT EXISTS nodes_lsp_rel_path ON nodes(json_extract(attrs_json, '$.lspRelPath')) WHERE valid_to IS NULL;
16430
+ CREATE INDEX IF NOT EXISTS nodes_command_signature_v2 ON nodes(json_extract(attrs_json, '$.commandSignature')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.commandSignature') IS NOT NULL;
16431
+ CREATE INDEX IF NOT EXISTS nodes_diag_command_v2 ON nodes(json_extract(attrs_json, '$.diagCommand')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.diagCommand') IS NOT NULL;
16432
+ CREATE INDEX IF NOT EXISTS nodes_lsp_rel_path_v2 ON nodes(json_extract(attrs_json, '$.lspRelPath')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.lspRelPath') IS NOT NULL;
16418
16433
  -- Local nodes by the cloud twin their upload receipt recorded. Every burst /
16419
16434
  -- search fusion folds the Network copy of the caller's own knowledge back onto
16420
16435
  -- the local node; unindexed, the reverse lookup was a ~300ms attrs scan per call.
16421
- CREATE INDEX IF NOT EXISTS nodes_cloud_node_id ON nodes(json_extract(attrs_json, '$.cloudNodeId')) WHERE valid_to IS NULL;
16436
+ CREATE INDEX IF NOT EXISTS nodes_cloud_node_id_v2 ON nodes(json_extract(attrs_json, '$.cloudNodeId')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.cloudNodeId') IS NOT NULL;
16422
16437
  -- Nodes still waiting on a root-cause ask. The MCP server checks this on EVERY
16423
16438
  -- tool response (the enrichment piggyback), so it must not parse every
16424
- -- Solution's and Problem's attrs per call. Not partial: the Problem pull reads
16439
+ -- Solution's and Problem's attrs per call. Any version: the Problem pull reads
16425
16440
  -- closed versions too.
16426
- CREATE INDEX IF NOT EXISTS nodes_enrichment_pending ON nodes(json_extract(attrs_json, '$.enrichmentPending'));
16441
+ CREATE INDEX IF NOT EXISTS nodes_enrichment_pending_v2 ON nodes(json_extract(attrs_json, '$.enrichmentPending')) WHERE json_extract(attrs_json, '$.enrichmentPending') IS NOT NULL;
16427
16442
  -- Revisit flags, any label, any version: listNeedsRevisit's as-of read runs on
16428
16443
  -- every context render and otherwise json-checks every row (10.3 s on a 324k-row
16429
16444
  -- store, to return zero flags). The predicate must stay textually identical to
@@ -16489,6 +16504,30 @@ function rowToEdge(r) {
16489
16504
  }
16490
16505
  var FINGERPRINT_INDEXED_LABELS = ["Problem", "Vulnerability", "Solution", "RootCause"];
16491
16506
  var NODE_LABELS_CHUNK = 256;
16507
+ var NODE_UPSERT_COLUMNS = [
16508
+ "label",
16509
+ "description",
16510
+ "extraction_confidence",
16511
+ "embedding_json",
16512
+ "embedding_blob",
16513
+ "cumulative_surprise",
16514
+ "peak_surprise",
16515
+ "cumulative_hits",
16516
+ "last_updated_at",
16517
+ "memory_tier",
16518
+ "page_rank",
16519
+ "is_landmark",
16520
+ "community",
16521
+ "stability",
16522
+ "last_reinforced_at_seq",
16523
+ "attrs_json"
16524
+ ];
16525
+ var EDGE_UPSERT_COLUMNS = ["confidence", "last_seen_at", "nav_successes", "nav_failures", "valid_to", "attrs_json"];
16526
+ function sameSqlValue(a, b) {
16527
+ if (a === b || a == null && b == null) return true;
16528
+ if (a instanceof Uint8Array && b instanceof Uint8Array) return Buffer.compare(a, b) === 0;
16529
+ return false;
16530
+ }
16492
16531
  var PRIOR_TAG_MARKER = '"priorTag":true';
16493
16532
  var SqliteGraphStore = class {
16494
16533
  db;
@@ -16496,6 +16535,8 @@ var SqliteGraphStore = class {
16496
16535
  // used to prepare it on every call: ~600ms of a 2.5s drain build on a live
16497
16536
  // 225k-node store was statement compilation alone. Keyed by the SQL text.
16498
16537
  typedEdgeStmts = /* @__PURE__ */ new Map();
16538
+ /** Narrow merge updates, keyed by table and the columns they set. */
16539
+ changedColumnStmts = /* @__PURE__ */ new Map();
16499
16540
  stmts;
16500
16541
  /** Unified graph-mutation counter (see GraphStore.mutationCount). Bumped on
16501
16542
  * every node/edge upsert — code-index and capture alike — so the consolidation
@@ -16579,6 +16620,8 @@ var SqliteGraphStore = class {
16579
16620
  last_reinforced_at_seq = excluded.last_reinforced_at_seq,
16580
16621
  attrs_json = excluded.attrs_json
16581
16622
  `),
16623
+ nodeUpsertTarget: this.db.prepare(`SELECT rowid AS rid, ${NODE_UPSERT_COLUMNS.join(", ")} FROM nodes WHERE id = ?`),
16624
+ edgeUpsertTarget: this.db.prepare(`SELECT rowid AS rid, ${EDGE_UPSERT_COLUMNS.join(", ")} FROM edges WHERE from_id = ? AND type = ? AND to_id = ?`),
16582
16625
  getNode: this.db.prepare("SELECT nodes.*, (SELECT x.attrs_json FROM node_exposure x WHERE x.id = nodes.id) AS exposure_json FROM nodes WHERE id = ?"),
16583
16626
  exposureOf: this.db.prepare("SELECT attrs_json FROM node_exposure WHERE id = ?"),
16584
16627
  writeExposure: this.db.prepare("INSERT INTO node_exposure(id, attrs_json) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET attrs_json = excluded.attrs_json"),
@@ -16947,7 +16990,7 @@ var SqliteGraphStore = class {
16947
16990
  }
16948
16991
  mergeNode(node) {
16949
16992
  this.mutations++;
16950
- this.stmts.insertNode.run({
16993
+ const values = {
16951
16994
  id: node.id,
16952
16995
  label: node.label,
16953
16996
  description: node.description,
@@ -16974,7 +17017,23 @@ var SqliteGraphStore = class {
16974
17017
  created_at_seq: node.createdAtSeq ?? null,
16975
17018
  last_reinforced_at_seq: node.lastReinforcedAtSeq ?? null,
16976
17019
  attrs_json: this.nodeAttrsJson(node.id, node.attrs ?? {})
16977
- });
17020
+ };
17021
+ const current = this.stmts.nodeUpsertTarget.get(node.id);
17022
+ if (current) this.writeChangedColumns("nodes", NODE_UPSERT_COLUMNS, current, values);
17023
+ else this.stmts.insertNode.run(values);
17024
+ }
17025
+ /** What the upsert's ON CONFLICT would write over `current`, setting only the
17026
+ * columns whose value moves (NODE_UPSERT_COLUMNS says why). */
17027
+ writeChangedColumns(table, columns2, current, next) {
17028
+ const changed = columns2.filter((c) => !sameSqlValue(current[c], next[c]));
17029
+ if (changed.length === 0) return;
17030
+ const key = `${table}:${changed.join(",")}`;
17031
+ let stmt2 = this.changedColumnStmts.get(key);
17032
+ if (!stmt2) {
17033
+ stmt2 = this.db.prepare(`UPDATE ${table} SET ${changed.map((c) => `${c} = :${c}`).join(", ")} WHERE rowid = :rid`);
17034
+ this.changedColumnStmts.set(key, stmt2);
17035
+ }
17036
+ stmt2.run({ rid: current["rid"], ...Object.fromEntries(changed.map((c) => [c, next[c]])) });
16978
17037
  }
16979
17038
  /** mergeNode, unless the live row already holds this content. Content is what
16980
17039
  * the columns a merge rewrites from the producer (label, description, attrs,
@@ -17108,7 +17167,7 @@ var SqliteGraphStore = class {
17108
17167
  return;
17109
17168
  }
17110
17169
  this.mutations++;
17111
- this.stmts.insertEdge.run({
17170
+ const values = {
17112
17171
  id: edge.id,
17113
17172
  from_id: edge.from,
17114
17173
  to_id: edge.to,
@@ -17122,6 +17181,16 @@ var SqliteGraphStore = class {
17122
17181
  valid_from: edge.validFrom ?? edge.createdAt,
17123
17182
  valid_to: edge.validTo ?? null,
17124
17183
  attrs_json: JSON.stringify(edge.attrs ?? {})
17184
+ };
17185
+ const current = this.stmts.edgeUpsertTarget.get(edge.from, edge.type, edge.to);
17186
+ if (!current) {
17187
+ this.stmts.insertEdge.run(values);
17188
+ return;
17189
+ }
17190
+ this.writeChangedColumns("edges", EDGE_UPSERT_COLUMNS, current, {
17191
+ ...values,
17192
+ confidence: Math.max(current["confidence"], edge.confidence),
17193
+ valid_to: null
17125
17194
  });
17126
17195
  }
17127
17196
  /** mergeEdge, unless the live edge already holds this content: same attrs, and
@@ -17336,8 +17405,8 @@ var SqliteGraphStore = class {
17336
17405
  after = rows[rows.length - 1].rid;
17337
17406
  }
17338
17407
  }
17339
- recentNodeIds(label, limit) {
17340
- const rows = this.db.prepare("SELECT id FROM nodes WHERE label = ? AND valid_to IS NULL ORDER BY last_updated_at DESC LIMIT ?").all(label, Math.max(0, limit));
17408
+ recentProblemIds(limit) {
17409
+ const rows = this.db.prepare("SELECT id FROM nodes WHERE label = 'Problem' AND valid_to IS NULL ORDER BY last_updated_at DESC LIMIT ?").all(Math.max(0, limit));
17341
17410
  return rows.map((r) => r.id);
17342
17411
  }
17343
17412
  recentOpenProblemCandidates(opts) {
@@ -17778,7 +17847,7 @@ var SqliteGraphStore = class {
17778
17847
  const slice = cloudIds.slice(i, i + CHUNK);
17779
17848
  if (slice.length === 0) continue;
17780
17849
  const rows = this.db.prepare(
17781
- `SELECT id, json_extract(attrs_json, '$.cloudNodeId') AS cloud FROM nodes INDEXED BY nodes_cloud_node_id WHERE json_extract(attrs_json, '$.cloudNodeId') IN (${slice.map(() => "?").join(",")}) AND valid_to IS NULL ORDER BY id`
17850
+ `SELECT id, json_extract(attrs_json, '$.cloudNodeId') AS cloud FROM nodes INDEXED BY nodes_cloud_node_id_v2 WHERE json_extract(attrs_json, '$.cloudNodeId') IN (${slice.map(() => "?").join(",")}) AND valid_to IS NULL ORDER BY id`
17782
17851
  ).all(...slice);
17783
17852
  for (const r of rows) out.set(r.cloud, [...out.get(r.cloud) ?? [], r.id]);
17784
17853
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.22-dev.4676",
3
+ "version": "2.0.23-dev.4682",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/pass-worker.mjs CHANGED
@@ -27304,7 +27304,12 @@ CREATE UNIQUE INDEX IF NOT EXISTS edges_triple ON edges(from_id, type, to_id);
27304
27304
  var TEMPORAL_INDEX_SQL = `
27305
27305
  CREATE INDEX IF NOT EXISTS nodes_live ON nodes(label, valid_to);
27306
27306
  CREATE INDEX IF NOT EXISTS nodes_live_count ON nodes(valid_to);
27307
- CREATE INDEX IF NOT EXISTS nodes_live_recent ON nodes(label, last_updated_at DESC) WHERE valid_to IS NULL;
27307
+ -- Problems by recency (the only label read newest-first). Over every live row,
27308
+ -- each write that stamps last_updated_at moved a code symbol's entry from a
27309
+ -- scattered leaf: ~19 KB per position-only symbol rewrite on a 1.1 GB store.
27310
+ -- label stays the leading column so label = 'Problem' plans without a sort.
27311
+ DROP INDEX IF EXISTS nodes_live_recent;
27312
+ CREATE INDEX IF NOT EXISTS nodes_live_problem_recent ON nodes(label, last_updated_at DESC) WHERE valid_to IS NULL AND label = 'Problem';
27308
27313
  -- "Anything created since the watermark?" guards (the fold, regression and
27309
27314
  -- canonical-bind sweeps run one per session boundary). Without it the planner
27310
27315
  -- walks nodes_live and reads every live row of the label to test created_at:
@@ -27378,24 +27383,34 @@ CREATE INDEX IF NOT EXISTS nodes_closed_relpath ON nodes(json_extract(attrs_json
27378
27383
  -- Live rows by name, for looking up the few symbols a statement mentions
27379
27384
  -- instead of hydrating every code symbol per minted problem.
27380
27385
  CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) WHERE valid_to IS NULL;
27386
+ -- The six single-attr lookups below are partial on the attr being present: each
27387
+ -- is read only by equality (or IN), which implies it. Keyed on NULL for the ~95%
27388
+ -- of rows that lack the attr, every write to such a row's attrs rewrote its NULL
27389
+ -- entry: ~25 KB per position-only symbol rewrite on a 1.1 GB store (2026-10-21).
27390
+ DROP INDEX IF EXISTS nodes_anchor_hint_path;
27391
+ DROP INDEX IF EXISTS nodes_command_signature;
27392
+ DROP INDEX IF EXISTS nodes_diag_command;
27393
+ DROP INDEX IF EXISTS nodes_lsp_rel_path;
27394
+ DROP INDEX IF EXISTS nodes_cloud_node_id;
27395
+ DROP INDEX IF EXISTS nodes_enrichment_pending;
27381
27396
  -- Problems by the file their anchor hint names: the harvest upgrades hints for
27382
27397
  -- every edited file of every turn, and a scan parses every Problem's attrs.
27383
- CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path ON nodes(json_extract(attrs_json, '$.anchorHint.path')) WHERE valid_to IS NULL;
27398
+ CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path_v2 ON nodes(json_extract(attrs_json, '$.anchorHint.path')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.anchorHint.path') IS NOT NULL;
27384
27399
  -- Problems by the command / checker / file that opened them. The generalizer
27385
27400
  -- looked these up per shell or tool EVENT by hydrating every Problem: ~12s of
27386
27401
  -- every minute of daemon main-thread time on a 3.4k-Problem store (2026-10-05).
27387
- CREATE INDEX IF NOT EXISTS nodes_command_signature ON nodes(json_extract(attrs_json, '$.commandSignature')) WHERE valid_to IS NULL;
27388
- CREATE INDEX IF NOT EXISTS nodes_diag_command ON nodes(json_extract(attrs_json, '$.diagCommand')) WHERE valid_to IS NULL;
27389
- CREATE INDEX IF NOT EXISTS nodes_lsp_rel_path ON nodes(json_extract(attrs_json, '$.lspRelPath')) WHERE valid_to IS NULL;
27402
+ CREATE INDEX IF NOT EXISTS nodes_command_signature_v2 ON nodes(json_extract(attrs_json, '$.commandSignature')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.commandSignature') IS NOT NULL;
27403
+ CREATE INDEX IF NOT EXISTS nodes_diag_command_v2 ON nodes(json_extract(attrs_json, '$.diagCommand')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.diagCommand') IS NOT NULL;
27404
+ CREATE INDEX IF NOT EXISTS nodes_lsp_rel_path_v2 ON nodes(json_extract(attrs_json, '$.lspRelPath')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.lspRelPath') IS NOT NULL;
27390
27405
  -- Local nodes by the cloud twin their upload receipt recorded. Every burst /
27391
27406
  -- search fusion folds the Network copy of the caller's own knowledge back onto
27392
27407
  -- the local node; unindexed, the reverse lookup was a ~300ms attrs scan per call.
27393
- CREATE INDEX IF NOT EXISTS nodes_cloud_node_id ON nodes(json_extract(attrs_json, '$.cloudNodeId')) WHERE valid_to IS NULL;
27408
+ CREATE INDEX IF NOT EXISTS nodes_cloud_node_id_v2 ON nodes(json_extract(attrs_json, '$.cloudNodeId')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.cloudNodeId') IS NOT NULL;
27394
27409
  -- Nodes still waiting on a root-cause ask. The MCP server checks this on EVERY
27395
27410
  -- tool response (the enrichment piggyback), so it must not parse every
27396
- -- Solution's and Problem's attrs per call. Not partial: the Problem pull reads
27411
+ -- Solution's and Problem's attrs per call. Any version: the Problem pull reads
27397
27412
  -- closed versions too.
27398
- CREATE INDEX IF NOT EXISTS nodes_enrichment_pending ON nodes(json_extract(attrs_json, '$.enrichmentPending'));
27413
+ CREATE INDEX IF NOT EXISTS nodes_enrichment_pending_v2 ON nodes(json_extract(attrs_json, '$.enrichmentPending')) WHERE json_extract(attrs_json, '$.enrichmentPending') IS NOT NULL;
27399
27414
  -- Revisit flags, any label, any version: listNeedsRevisit's as-of read runs on
27400
27415
  -- every context render and otherwise json-checks every row (10.3 s on a 324k-row
27401
27416
  -- store, to return zero flags). The predicate must stay textually identical to
@@ -27461,6 +27476,30 @@ function rowToEdge(r) {
27461
27476
  }
27462
27477
  var FINGERPRINT_INDEXED_LABELS = ["Problem", "Vulnerability", "Solution", "RootCause"];
27463
27478
  var NODE_LABELS_CHUNK = 256;
27479
+ var NODE_UPSERT_COLUMNS = [
27480
+ "label",
27481
+ "description",
27482
+ "extraction_confidence",
27483
+ "embedding_json",
27484
+ "embedding_blob",
27485
+ "cumulative_surprise",
27486
+ "peak_surprise",
27487
+ "cumulative_hits",
27488
+ "last_updated_at",
27489
+ "memory_tier",
27490
+ "page_rank",
27491
+ "is_landmark",
27492
+ "community",
27493
+ "stability",
27494
+ "last_reinforced_at_seq",
27495
+ "attrs_json"
27496
+ ];
27497
+ var EDGE_UPSERT_COLUMNS = ["confidence", "last_seen_at", "nav_successes", "nav_failures", "valid_to", "attrs_json"];
27498
+ function sameSqlValue(a, b) {
27499
+ if (a === b || a == null && b == null) return true;
27500
+ if (a instanceof Uint8Array && b instanceof Uint8Array) return Buffer.compare(a, b) === 0;
27501
+ return false;
27502
+ }
27464
27503
  var PRIOR_TAG_MARKER = '"priorTag":true';
27465
27504
  var SqliteGraphStore = class {
27466
27505
  db;
@@ -27468,6 +27507,8 @@ var SqliteGraphStore = class {
27468
27507
  // used to prepare it on every call: ~600ms of a 2.5s drain build on a live
27469
27508
  // 225k-node store was statement compilation alone. Keyed by the SQL text.
27470
27509
  typedEdgeStmts = /* @__PURE__ */ new Map();
27510
+ /** Narrow merge updates, keyed by table and the columns they set. */
27511
+ changedColumnStmts = /* @__PURE__ */ new Map();
27471
27512
  stmts;
27472
27513
  /** Unified graph-mutation counter (see GraphStore.mutationCount). Bumped on
27473
27514
  * every node/edge upsert — code-index and capture alike — so the consolidation
@@ -27551,6 +27592,8 @@ var SqliteGraphStore = class {
27551
27592
  last_reinforced_at_seq = excluded.last_reinforced_at_seq,
27552
27593
  attrs_json = excluded.attrs_json
27553
27594
  `),
27595
+ nodeUpsertTarget: this.db.prepare(`SELECT rowid AS rid, ${NODE_UPSERT_COLUMNS.join(", ")} FROM nodes WHERE id = ?`),
27596
+ edgeUpsertTarget: this.db.prepare(`SELECT rowid AS rid, ${EDGE_UPSERT_COLUMNS.join(", ")} FROM edges WHERE from_id = ? AND type = ? AND to_id = ?`),
27554
27597
  getNode: this.db.prepare("SELECT nodes.*, (SELECT x.attrs_json FROM node_exposure x WHERE x.id = nodes.id) AS exposure_json FROM nodes WHERE id = ?"),
27555
27598
  exposureOf: this.db.prepare("SELECT attrs_json FROM node_exposure WHERE id = ?"),
27556
27599
  writeExposure: this.db.prepare("INSERT INTO node_exposure(id, attrs_json) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET attrs_json = excluded.attrs_json"),
@@ -27919,7 +27962,7 @@ var SqliteGraphStore = class {
27919
27962
  }
27920
27963
  mergeNode(node) {
27921
27964
  this.mutations++;
27922
- this.stmts.insertNode.run({
27965
+ const values = {
27923
27966
  id: node.id,
27924
27967
  label: node.label,
27925
27968
  description: node.description,
@@ -27946,7 +27989,23 @@ var SqliteGraphStore = class {
27946
27989
  created_at_seq: node.createdAtSeq ?? null,
27947
27990
  last_reinforced_at_seq: node.lastReinforcedAtSeq ?? null,
27948
27991
  attrs_json: this.nodeAttrsJson(node.id, node.attrs ?? {})
27949
- });
27992
+ };
27993
+ const current = this.stmts.nodeUpsertTarget.get(node.id);
27994
+ if (current) this.writeChangedColumns("nodes", NODE_UPSERT_COLUMNS, current, values);
27995
+ else this.stmts.insertNode.run(values);
27996
+ }
27997
+ /** What the upsert's ON CONFLICT would write over `current`, setting only the
27998
+ * columns whose value moves (NODE_UPSERT_COLUMNS says why). */
27999
+ writeChangedColumns(table, columns2, current, next) {
28000
+ const changed = columns2.filter((c) => !sameSqlValue(current[c], next[c]));
28001
+ if (changed.length === 0) return;
28002
+ const key = `${table}:${changed.join(",")}`;
28003
+ let stmt2 = this.changedColumnStmts.get(key);
28004
+ if (!stmt2) {
28005
+ stmt2 = this.db.prepare(`UPDATE ${table} SET ${changed.map((c) => `${c} = :${c}`).join(", ")} WHERE rowid = :rid`);
28006
+ this.changedColumnStmts.set(key, stmt2);
28007
+ }
28008
+ stmt2.run({ rid: current["rid"], ...Object.fromEntries(changed.map((c) => [c, next[c]])) });
27950
28009
  }
27951
28010
  /** mergeNode, unless the live row already holds this content. Content is what
27952
28011
  * the columns a merge rewrites from the producer (label, description, attrs,
@@ -28080,7 +28139,7 @@ var SqliteGraphStore = class {
28080
28139
  return;
28081
28140
  }
28082
28141
  this.mutations++;
28083
- this.stmts.insertEdge.run({
28142
+ const values = {
28084
28143
  id: edge.id,
28085
28144
  from_id: edge.from,
28086
28145
  to_id: edge.to,
@@ -28094,6 +28153,16 @@ var SqliteGraphStore = class {
28094
28153
  valid_from: edge.validFrom ?? edge.createdAt,
28095
28154
  valid_to: edge.validTo ?? null,
28096
28155
  attrs_json: JSON.stringify(edge.attrs ?? {})
28156
+ };
28157
+ const current = this.stmts.edgeUpsertTarget.get(edge.from, edge.type, edge.to);
28158
+ if (!current) {
28159
+ this.stmts.insertEdge.run(values);
28160
+ return;
28161
+ }
28162
+ this.writeChangedColumns("edges", EDGE_UPSERT_COLUMNS, current, {
28163
+ ...values,
28164
+ confidence: Math.max(current["confidence"], edge.confidence),
28165
+ valid_to: null
28097
28166
  });
28098
28167
  }
28099
28168
  /** mergeEdge, unless the live edge already holds this content: same attrs, and
@@ -28308,8 +28377,8 @@ var SqliteGraphStore = class {
28308
28377
  after = rows[rows.length - 1].rid;
28309
28378
  }
28310
28379
  }
28311
- recentNodeIds(label, limit) {
28312
- const rows = this.db.prepare("SELECT id FROM nodes WHERE label = ? AND valid_to IS NULL ORDER BY last_updated_at DESC LIMIT ?").all(label, Math.max(0, limit));
28380
+ recentProblemIds(limit) {
28381
+ const rows = this.db.prepare("SELECT id FROM nodes WHERE label = 'Problem' AND valid_to IS NULL ORDER BY last_updated_at DESC LIMIT ?").all(Math.max(0, limit));
28313
28382
  return rows.map((r) => r.id);
28314
28383
  }
28315
28384
  recentOpenProblemCandidates(opts) {
@@ -28750,7 +28819,7 @@ var SqliteGraphStore = class {
28750
28819
  const slice = cloudIds.slice(i2, i2 + CHUNK);
28751
28820
  if (slice.length === 0) continue;
28752
28821
  const rows = this.db.prepare(
28753
- `SELECT id, json_extract(attrs_json, '$.cloudNodeId') AS cloud FROM nodes INDEXED BY nodes_cloud_node_id WHERE json_extract(attrs_json, '$.cloudNodeId') IN (${slice.map(() => "?").join(",")}) AND valid_to IS NULL ORDER BY id`
28822
+ `SELECT id, json_extract(attrs_json, '$.cloudNodeId') AS cloud FROM nodes INDEXED BY nodes_cloud_node_id_v2 WHERE json_extract(attrs_json, '$.cloudNodeId') IN (${slice.map(() => "?").join(",")}) AND valid_to IS NULL ORDER BY id`
28754
28823
  ).all(...slice);
28755
28824
  for (const r of rows) out2.set(r.cloud, [...out2.get(r.cloud) ?? [], r.id]);
28756
28825
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "errata-context",
3
- "version": "2.0.22-dev.4676",
3
+ "version": "2.0.23-dev.4682",
4
4
  "description": "A minimal 3D view of loaded Errata priors and recent graph bursts.",
5
5
  "author": {
6
6
  "name": "inErrata"
@@ -1,4 +1,4 @@
1
- aed45b59cddfbffc63052b049122c7b5303eb3f921379af8ac2e969ded158d58 .codex-plugin/plugin.json
1
+ 51434f3e703060bee5abb7c7eebe1446b66132c9cb7e43ae630daf855d83bc02 .codex-plugin/plugin.json
2
2
  a7bebae9fb01c838d6c75fc784129e28be571233fa66ff5ee8039b574f35ccf7 assets/icon.png
3
3
  fc38b64e4df1e0b72c35764bef6c4c05430d97c9009ad0ecc8a9e75b4deab232 configure-local.mjs
4
4
  73dc6028b3b070257c7c31f252d11e55a9e096f4aed0f187f10cb21716e1e6d7 dist/client.js