pi-context-engine 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/memory/db.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  /**
2
- * Per-project memory.db — port of upstream `memory/db.py` (MIT), schema v3
2
+ * Per-project memory.db — port of upstream `memory/db.py` (MIT), schema v4
3
3
  * (minus `migrated_files`, which only served upstream's session-file import).
4
+ * Schema is enforced idempotently on every open (no version-step migrations),
5
+ * so databases from any earlier release converge to the current shape in place.
4
6
  * Synchronous better-sqlite3; vec tables degrade to FTS-only when the
5
7
  * sqlite-vec extension can't load.
6
8
  */
@@ -27,7 +29,7 @@ export const BUCKETS = [
27
29
  export type Bucket = (typeof BUCKETS)[number];
28
30
 
29
31
  const SCHEMA_V1 = `
30
- CREATE TABLE sessions (
32
+ CREATE TABLE IF NOT EXISTS sessions (
31
33
  id TEXT PRIMARY KEY,
32
34
  project TEXT NOT NULL,
33
35
  started_at_epoch INTEGER NOT NULL,
@@ -40,9 +42,9 @@ CREATE TABLE sessions (
40
42
  rollup_summary TEXT,
41
43
  rollup_summary_at_epoch INTEGER
42
44
  );
43
- CREATE INDEX idx_sessions_started ON sessions(started_at_epoch DESC);
45
+ CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at_epoch DESC);
44
46
 
45
- CREATE TABLE prompts (
47
+ CREATE TABLE IF NOT EXISTS prompts (
46
48
  id INTEGER PRIMARY KEY AUTOINCREMENT,
47
49
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
48
50
  prompt_number INTEGER NOT NULL,
@@ -51,16 +53,16 @@ CREATE TABLE prompts (
51
53
  created_at TEXT NOT NULL,
52
54
  UNIQUE(session_id, prompt_number)
53
55
  );
54
- CREATE INDEX idx_prompts_session ON prompts(session_id, prompt_number);
56
+ CREATE INDEX IF NOT EXISTS idx_prompts_session ON prompts(session_id, prompt_number);
55
57
 
56
- CREATE TABLE tool_event_payloads (
58
+ CREATE TABLE IF NOT EXISTS tool_event_payloads (
57
59
  id INTEGER PRIMARY KEY AUTOINCREMENT,
58
60
  raw_input TEXT NOT NULL,
59
61
  raw_output TEXT,
60
62
  size_bytes INTEGER NOT NULL
61
63
  );
62
64
 
63
- CREATE TABLE tool_events (
65
+ CREATE TABLE IF NOT EXISTS tool_events (
64
66
  id INTEGER PRIMARY KEY AUTOINCREMENT,
65
67
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
66
68
  prompt_number INTEGER NOT NULL,
@@ -70,9 +72,9 @@ CREATE TABLE tool_events (
70
72
  created_at_epoch INTEGER NOT NULL,
71
73
  created_at TEXT NOT NULL
72
74
  );
73
- CREATE INDEX idx_events_session_turn ON tool_events(session_id, prompt_number);
75
+ CREATE INDEX IF NOT EXISTS idx_events_session_turn ON tool_events(session_id, prompt_number);
74
76
 
75
- CREATE TABLE turn_summaries (
77
+ CREATE TABLE IF NOT EXISTS turn_summaries (
76
78
  id INTEGER PRIMARY KEY AUTOINCREMENT,
77
79
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
78
80
  prompt_number INTEGER NOT NULL,
@@ -82,7 +84,7 @@ CREATE TABLE turn_summaries (
82
84
  UNIQUE(session_id, prompt_number)
83
85
  );
84
86
 
85
- CREATE TABLE decisions (
87
+ CREATE TABLE IF NOT EXISTS decisions (
86
88
  id INTEGER PRIMARY KEY AUTOINCREMENT,
87
89
  session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
88
90
  decision TEXT NOT NULL,
@@ -91,10 +93,10 @@ CREATE TABLE decisions (
91
93
  created_at_epoch INTEGER NOT NULL,
92
94
  created_at TEXT NOT NULL
93
95
  );
94
- CREATE INDEX idx_decisions_created ON decisions(created_at_epoch DESC);
95
- CREATE INDEX idx_decisions_source ON decisions(source);
96
+ CREATE INDEX IF NOT EXISTS idx_decisions_created ON decisions(created_at_epoch DESC);
97
+ CREATE INDEX IF NOT EXISTS idx_decisions_source ON decisions(source);
96
98
 
97
- CREATE TABLE code_areas (
99
+ CREATE TABLE IF NOT EXISTS code_areas (
98
100
  id INTEGER PRIMARY KEY AUTOINCREMENT,
99
101
  session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
100
102
  file_path TEXT NOT NULL,
@@ -102,9 +104,9 @@ CREATE TABLE code_areas (
102
104
  source TEXT NOT NULL CHECK(source IN ('manual','migrated','auto')) DEFAULT 'manual',
103
105
  created_at_epoch INTEGER NOT NULL
104
106
  );
105
- CREATE INDEX idx_code_areas_file ON code_areas(file_path);
107
+ CREATE INDEX IF NOT EXISTS idx_code_areas_file ON code_areas(file_path);
106
108
 
107
- CREATE TABLE pending_compressions (
109
+ CREATE TABLE IF NOT EXISTS pending_compressions (
108
110
  id INTEGER PRIMARY KEY AUTOINCREMENT,
109
111
  kind TEXT NOT NULL CHECK(kind IN ('turn','session_rollup')),
110
112
  session_id TEXT NOT NULL,
@@ -115,44 +117,44 @@ CREATE TABLE pending_compressions (
115
117
  UNIQUE(kind, session_id, prompt_number)
116
118
  );
117
119
 
118
- CREATE VIRTUAL TABLE prompts_fts USING fts5(prompt_text, content='prompts', content_rowid='id');
119
- CREATE VIRTUAL TABLE decisions_fts USING fts5(decision, reason, content='decisions', content_rowid='id');
120
- CREATE VIRTUAL TABLE turn_summaries_fts USING fts5(summary, content='turn_summaries', content_rowid='id');
120
+ CREATE VIRTUAL TABLE IF NOT EXISTS prompts_fts USING fts5(prompt_text, content='prompts', content_rowid='id');
121
+ CREATE VIRTUAL TABLE IF NOT EXISTS decisions_fts USING fts5(decision, reason, content='decisions', content_rowid='id');
122
+ CREATE VIRTUAL TABLE IF NOT EXISTS turn_summaries_fts USING fts5(summary, content='turn_summaries', content_rowid='id');
121
123
 
122
- CREATE TRIGGER prompts_ai AFTER INSERT ON prompts BEGIN
124
+ CREATE TRIGGER IF NOT EXISTS prompts_ai AFTER INSERT ON prompts BEGIN
123
125
  INSERT INTO prompts_fts(rowid, prompt_text) VALUES (new.id, new.prompt_text);
124
126
  END;
125
- CREATE TRIGGER prompts_ad AFTER DELETE ON prompts BEGIN
127
+ CREATE TRIGGER IF NOT EXISTS prompts_ad AFTER DELETE ON prompts BEGIN
126
128
  INSERT INTO prompts_fts(prompts_fts, rowid, prompt_text) VALUES('delete', old.id, old.prompt_text);
127
129
  END;
128
- CREATE TRIGGER prompts_au AFTER UPDATE ON prompts BEGIN
130
+ CREATE TRIGGER IF NOT EXISTS prompts_au AFTER UPDATE ON prompts BEGIN
129
131
  INSERT INTO prompts_fts(prompts_fts, rowid, prompt_text) VALUES('delete', old.id, old.prompt_text);
130
132
  INSERT INTO prompts_fts(rowid, prompt_text) VALUES (new.id, new.prompt_text);
131
133
  END;
132
134
 
133
- CREATE TRIGGER decisions_ai AFTER INSERT ON decisions BEGIN
135
+ CREATE TRIGGER IF NOT EXISTS decisions_ai AFTER INSERT ON decisions BEGIN
134
136
  INSERT INTO decisions_fts(rowid, decision, reason) VALUES (new.id, new.decision, new.reason);
135
137
  END;
136
- CREATE TRIGGER decisions_ad AFTER DELETE ON decisions BEGIN
138
+ CREATE TRIGGER IF NOT EXISTS decisions_ad AFTER DELETE ON decisions BEGIN
137
139
  INSERT INTO decisions_fts(decisions_fts, rowid, decision, reason) VALUES('delete', old.id, old.decision, old.reason);
138
140
  END;
139
- CREATE TRIGGER decisions_au AFTER UPDATE ON decisions BEGIN
141
+ CREATE TRIGGER IF NOT EXISTS decisions_au AFTER UPDATE ON decisions BEGIN
140
142
  INSERT INTO decisions_fts(decisions_fts, rowid, decision, reason) VALUES('delete', old.id, old.decision, old.reason);
141
143
  INSERT INTO decisions_fts(rowid, decision, reason) VALUES (new.id, new.decision, new.reason);
142
144
  END;
143
145
 
144
- CREATE TRIGGER turn_summaries_ai AFTER INSERT ON turn_summaries BEGIN
146
+ CREATE TRIGGER IF NOT EXISTS turn_summaries_ai AFTER INSERT ON turn_summaries BEGIN
145
147
  INSERT INTO turn_summaries_fts(rowid, summary) VALUES (new.id, new.summary);
146
148
  END;
147
- CREATE TRIGGER turn_summaries_ad AFTER DELETE ON turn_summaries BEGIN
149
+ CREATE TRIGGER IF NOT EXISTS turn_summaries_ad AFTER DELETE ON turn_summaries BEGIN
148
150
  INSERT INTO turn_summaries_fts(turn_summaries_fts, rowid, summary) VALUES('delete', old.id, old.summary);
149
151
  END;
150
- CREATE TRIGGER turn_summaries_au AFTER UPDATE ON turn_summaries BEGIN
152
+ CREATE TRIGGER IF NOT EXISTS turn_summaries_au AFTER UPDATE ON turn_summaries BEGIN
151
153
  INSERT INTO turn_summaries_fts(turn_summaries_fts, rowid, summary) VALUES('delete', old.id, old.summary);
152
154
  INSERT INTO turn_summaries_fts(rowid, summary) VALUES (new.id, new.summary);
153
155
  END;
154
156
 
155
- CREATE TABLE schema_versions (
157
+ CREATE TABLE IF NOT EXISTS schema_versions (
156
158
  version INTEGER PRIMARY KEY,
157
159
  applied_at_epoch INTEGER NOT NULL
158
160
  );
@@ -213,47 +215,31 @@ export class MemoryDb {
213
215
  }
214
216
 
215
217
  private ensureSchema(): void {
216
- const bootstrap = this.db
217
- .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='schema_versions'")
218
- .get();
219
- if (!bootstrap) {
220
- const run = this.db.transaction(() => {
221
- this.db.exec(SCHEMA_V1);
222
- if (this.hasVec) {
223
- this.db.exec(this.vecTableStmts(VEC_DIM));
224
- this.db.exec(VEC_TRIGGERS);
225
- }
226
- this.db.exec(SCHEMA_V3);
227
- this.applyV4Columns();
228
- this.db.exec(SCHEMA_V4);
229
- this.db
230
- .prepare("INSERT INTO schema_versions (version, applied_at_epoch) VALUES (?, ?)")
231
- .run(this.hasVec ? CURRENT_VERSION : 1, epochSeconds());
232
- });
233
- run();
234
- return;
235
- }
236
- const current = this.schemaVersion();
237
- if (current >= CURRENT_VERSION) return;
218
+ // Shape enforcement instead of a version ladder: every statement below
219
+ // is idempotent, so a database from ANY earlier release converges to the
220
+ // current schema on open without version bookkeeping or stepwise
221
+ // migrations. Vec tables are created whenever sqlite-vec is available
222
+ // (and skipped when it isn't), which subsumes the old "created without
223
+ // vec, upgrade later" version dance.
238
224
  const run = this.db.transaction(() => {
239
- if (current < 2 && this.hasVec) {
225
+ this.db.exec(SCHEMA_V1);
226
+ this.applyV4Columns();
227
+ this.db.exec(SCHEMA_V3);
228
+ this.db.exec(SCHEMA_V4);
229
+ if (this.hasVec) {
240
230
  this.db.exec(this.vecTableStmts(VEC_DIM));
241
231
  this.db.exec(VEC_TRIGGERS);
242
232
  }
243
- if (current < 3) this.db.exec(SCHEMA_V3);
244
- if (current < 4) {
245
- this.applyV4Columns();
246
- this.db.exec(SCHEMA_V4);
247
- }
248
- if (current < 2 && !this.hasVec) return; // vec step still pending
233
+ // Informational stamp only — nothing branches on it anymore.
249
234
  this.db
250
- .prepare("INSERT INTO schema_versions (version, applied_at_epoch) VALUES (?, ?)")
235
+ .prepare("INSERT OR REPLACE INTO schema_versions (version, applied_at_epoch) VALUES (?, ?)")
251
236
  .run(CURRENT_VERSION, epochSeconds());
252
237
  });
253
238
  run();
254
239
  }
255
240
 
256
- /** v4 adds `decisions.tags` / `decisions.files` (JSON arrays); ALTER is guarded so re-runs and partial states stay safe. */
241
+ /** `decisions.tags` / `decisions.files` (JSON arrays) on databases created
242
+ * before R13; guarded so re-runs and partial states stay safe. */
257
243
  private applyV4Columns(): void {
258
244
  const cols = new Set(
259
245
  (this.db.prepare("PRAGMA table_info(decisions)").all() as { name: string }[]).map((c) => c.name),
@@ -20,10 +20,6 @@ export class LocalBackend {
20
20
  this.graphStore = new GraphStore(path.join(basePath, "graph"));
21
21
  }
22
22
 
23
- get metricRebuilt(): boolean {
24
- return this.vectorStore.metricRebuilt;
25
- }
26
-
27
23
  ingest(chunks: Chunk[], nodes: GraphNode[], edges: GraphEdge[]): void {
28
24
  this.vectorStore.ingest(chunks);
29
25
  this.ftsStore.ingest(chunks);
@@ -27,8 +27,6 @@ type Sqlite = SqliteHandle;
27
27
  export class VectorStore {
28
28
  private db: Sqlite;
29
29
  private dim: number | null = null;
30
- /** Set when a legacy L2 vec table was wiped — pipeline forces a full reindex. */
31
- metricRebuilt = false;
32
30
  constructor(dbPath: string) {
33
31
  fs.mkdirSync(dbPath, { recursive: true });
34
32
  // Without sqlite-vec the store still keeps chunk metadata; search()
@@ -37,10 +35,6 @@ export class VectorStore {
37
35
  this.ensureTables();
38
36
  }
39
37
 
40
- private get vecAvailable(): boolean {
41
- return this.dim !== null || this.vecTableExists();
42
- }
43
-
44
38
  private vecTableExists(): boolean {
45
39
  const row = this.db
46
40
  .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='chunks_vec'")
@@ -61,10 +55,6 @@ export class VectorStore {
61
55
  modified_ts REAL
62
56
  )
63
57
  `);
64
- const cols = (this.db.pragma("table_info(chunks)") as { name: string }[]).map((c) => c.name);
65
- if (!cols.includes("modified_ts")) {
66
- this.db.exec("ALTER TABLE chunks ADD COLUMN modified_ts REAL");
67
- }
68
58
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_file_path ON chunks(file_path)");
69
59
  this.db.exec(`
70
60
  CREATE TABLE IF NOT EXISTS chunk_compressions (
@@ -75,25 +65,19 @@ export class VectorStore {
75
65
  )
76
66
  `);
77
67
 
78
- const row = this.db
79
- .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='chunks_vec'")
80
- .get() as { sql: string | null } | undefined;
81
- if (row) {
82
- if (!(row.sql ?? "").includes("distance_metric=cosine")) {
83
- this.db.exec("DROP TABLE IF EXISTS chunks_vec");
84
- this.db.exec("DELETE FROM chunks");
85
- this.db.exec("DELETE FROM chunk_compressions");
86
- this.metricRebuilt = true;
87
- } else {
88
- const r = this.db.prepare("SELECT rowid FROM chunks_vec LIMIT 1").get() as
89
- | { rowid: number }
90
- | undefined;
91
- if (r) {
92
- const len = this.db
93
- .prepare("SELECT vec_length(embedding) AS len FROM chunks_vec LIMIT 1")
94
- .get() as { len: number };
95
- this.dim = len.len;
96
- }
68
+ if (this.vecTableExists()) {
69
+ // The table exists — read its dimension. There is no migration path
70
+ // for tables this package did not write (older metric/column shapes):
71
+ // an unrecognized shape cannot occur from our own DDL, and foreign
72
+ // stores are unsupported by design.
73
+ const r = this.db.prepare("SELECT rowid FROM chunks_vec LIMIT 1").get() as
74
+ | { rowid: number }
75
+ | undefined;
76
+ if (r) {
77
+ const len = this.db
78
+ .prepare("SELECT vec_length(embedding) AS len FROM chunks_vec LIMIT 1")
79
+ .get() as { len: number };
80
+ this.dim = len.len;
97
81
  }
98
82
  }
99
83
  }
package/src/utils.ts CHANGED
@@ -79,6 +79,19 @@ export function epochSeconds(): number {
79
79
  return Math.floor(Date.now() / 1000);
80
80
  }
81
81
 
82
+ /**
83
+ * Cooperative macrotask yield for long-running indexing work. ONNX
84
+ * inference, tokenization and tree-sitter parsing all run on the main
85
+ * thread, so a long stretch of synchronous work starves pi's UI loop
86
+ * (measured by scripts/bench-event-loop.mts: ~0.5-0.9 s stalls per 64-text
87
+ * embed batch). setImmediate keeps the yield cheap — same loop turn's check
88
+ * phase — while still letting pending timers and I/O (keystrokes, renders)
89
+ * run between units of work.
90
+ */
91
+ export function yieldToEventLoop(): Promise<void> {
92
+ return new Promise((resolve) => setImmediate(resolve));
93
+ }
94
+
82
95
  /**
83
96
  * Fractional seconds. Session-capture ordering needs sub-second resolution:
84
97
  * several decisions are commonly recorded within the same wall-clock second,