@pasko70/pibo 1.13.0 → 1.14.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.
Files changed (51) hide show
  1. package/dist/apps/chat/static-assets.js +3 -1
  2. package/dist/apps/chat/web-app.js +10 -0
  3. package/dist/apps/chat-ui/assets/{dist-B_8EcoB6.js → dist-1eQpSfYj.js} +1 -1
  4. package/dist/apps/chat-ui/assets/{dist-DVPXynJE.js → dist-BG1J2hGI.js} +1 -1
  5. package/dist/apps/chat-ui/assets/{dist-UYBI2F8E.js → dist-BIwXuAMQ.js} +1 -1
  6. package/dist/apps/{context-files-ui/assets/dist-D6pYe6d_.js → chat-ui/assets/dist-BeCHnL7P.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-CVLKu7NV.js → dist-CtsdsIpj.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-DugRsLaG.js → dist-DfdQQObC.js} +1 -1
  9. package/dist/apps/{context-files-ui/assets/dist-BFRDpzcv.js → chat-ui/assets/dist-DmqwP_tM.js} +1 -1
  10. package/dist/apps/{context-files-ui/assets/dist-B5mpygOZ.js → chat-ui/assets/dist-LWXXqiGT.js} +1 -1
  11. package/dist/apps/{context-files-ui/assets/dist-B_zXPNMb.js → chat-ui/assets/dist-hoDORUlL.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-XmG4qG-A.js → dist-ur4yXrlj.js} +1 -1
  13. package/dist/apps/chat-ui/assets/index-CoCl8FG8.css +1 -0
  14. package/dist/apps/chat-ui/assets/index-DLNTzhdh.js +237 -0
  15. package/dist/apps/chat-ui/index.html +2 -2
  16. package/dist/apps/chat-vscode-web/assets/index-R_7kgvvD.js +41 -0
  17. package/dist/apps/chat-vscode-web/index.html +1 -1
  18. package/dist/apps/{chat-ui/assets/dist-CnX234jg.js → context-files-ui/assets/dist-B7gXtTZC.js} +1 -1
  19. package/dist/apps/{chat-ui/assets/dist-CELRUR8b.js → context-files-ui/assets/dist-BD262Ouw.js} +1 -1
  20. package/dist/apps/context-files-ui/assets/{dist-Em3LWmNi.js → dist-BKuGdcnX.js} +1 -1
  21. package/dist/apps/{chat-ui/assets/dist-CVmpu9FK.js → context-files-ui/assets/dist-C-Jm45P4.js} +1 -1
  22. package/dist/apps/context-files-ui/assets/{dist-DM6yvMZv.js → dist-CHdx1Jri.js} +1 -1
  23. package/dist/apps/context-files-ui/assets/{dist-BLI6sQZG.js → dist-CULMH-fB.js} +1 -1
  24. package/dist/apps/context-files-ui/assets/{dist-CLrlJjPP.js → dist-Cjq6RWEt.js} +1 -1
  25. package/dist/apps/context-files-ui/assets/{dist-D8ppb7gk.js → dist-CntqFySz.js} +1 -1
  26. package/dist/apps/context-files-ui/assets/{dist-SEP88Pg7.js → dist-DxEGebCR.js} +1 -1
  27. package/dist/apps/{chat-ui/assets/dist-KOXty1JB.js → context-files-ui/assets/dist-kPuQud7t.js} +1 -1
  28. package/dist/apps/context-files-ui/assets/index-DZWsbuvg.js +202 -0
  29. package/dist/apps/context-files-ui/index.html +1 -1
  30. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  31. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.14.0.vsix +0 -0
  32. package/dist/cli-session/localSessionSource.js +2 -0
  33. package/dist/core/session-router.js +3 -0
  34. package/dist/gateway/client.js +8 -0
  35. package/dist/gateway/server.js +18 -2
  36. package/dist/gateway/web.js +1 -0
  37. package/dist/local/extension.js +2 -0
  38. package/dist/loops/plugin.js +34 -1
  39. package/dist/loops/service.js +85 -0
  40. package/dist/mcp/config-command.js +5 -3
  41. package/dist/plugins/context-files-store.js +287 -101
  42. package/dist/plugins/context-files.js +102 -154
  43. package/dist/shared/trace-live-reducer.js +6 -1
  44. package/dist/web/channel.js +105 -16
  45. package/dist/web/http.js +14 -2
  46. package/package.json +11 -7
  47. package/dist/apps/chat-ui/assets/index-DhFQ1th-.js +0 -237
  48. package/dist/apps/chat-ui/assets/index-hjygk9Ol.css +0 -1
  49. package/dist/apps/chat-vscode-web/assets/index-1wWonouX.js +0 -41
  50. package/dist/apps/context-files-ui/assets/index-C7X2UvWe.js +0 -202
  51. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.13.0.vsix +0 -0
@@ -1,7 +1,49 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { DatabaseSync } from "node:sqlite";
5
+ const CONTEXT_FILE_SCHEMA_VERSION_KEY = "schema-version";
6
+ const CONTEXT_FILE_SCHEMA_VERSION = "2";
7
+ const MANUAL_REVISION_MIGRATION_KEY = "manual-revisions-v2";
8
+ const CONTEXT_FILE_BASE_COLUMNS = [
9
+ "key",
10
+ "label",
11
+ "managed_path",
12
+ "scope",
13
+ "source_type",
14
+ "agent_profile_name",
15
+ "active_revision_id",
16
+ "source_ref",
17
+ "source_hash",
18
+ "created_at",
19
+ "updated_at",
20
+ ];
21
+ const CONTEXT_FILE_COLUMNS = [
22
+ ...CONTEXT_FILE_BASE_COLUMNS,
23
+ "working_content",
24
+ "source_content",
25
+ ];
26
+ const AUTOMATIC_REVISION_COLUMNS = [
27
+ "id",
28
+ "context_file_key",
29
+ "kind",
30
+ "content_hash",
31
+ "content",
32
+ "created_at",
33
+ "actor_id",
34
+ "based_on_revision_id",
35
+ "source_hash_at_creation",
36
+ "note",
37
+ ];
38
+ const MANUAL_REVISION_COLUMNS = [
39
+ "id",
40
+ "context_file_key",
41
+ "name",
42
+ "content_hash",
43
+ "content",
44
+ "created_at",
45
+ "actor_id",
46
+ ];
5
47
  function readLegacyManagedStore(storePath) {
6
48
  if (!existsSync(storePath))
7
49
  return { files: [] };
@@ -40,9 +82,10 @@ function fileRowToRecord(row) {
40
82
  scope: row.scope,
41
83
  sourceType: row.source_type,
42
84
  ...(row.agent_profile_name ? { agentProfileName: row.agent_profile_name } : {}),
43
- ...(row.active_revision_id ? { activeRevisionId: row.active_revision_id } : {}),
85
+ workingContent: row.working_content ?? "",
44
86
  ...(row.source_ref ? { sourceRef: row.source_ref } : {}),
45
87
  ...(row.source_hash ? { sourceHash: row.source_hash } : {}),
88
+ ...(row.source_content !== null ? { sourceContent: row.source_content } : {}),
46
89
  createdAt: row.created_at,
47
90
  updatedAt: row.updated_at,
48
91
  };
@@ -51,14 +94,11 @@ function revisionRowToRecord(row) {
51
94
  return {
52
95
  id: row.id,
53
96
  contextFileKey: row.context_file_key,
54
- kind: row.kind,
97
+ name: row.name,
55
98
  contentHash: row.content_hash,
56
99
  content: row.content,
57
100
  createdAt: row.created_at,
58
101
  ...(row.actor_id ? { actorId: row.actor_id } : {}),
59
- ...(row.based_on_revision_id ? { basedOnRevisionId: row.based_on_revision_id } : {}),
60
- ...(row.source_hash_at_creation ? { sourceHashAtCreation: row.source_hash_at_creation } : {}),
61
- ...(row.note ? { note: row.note } : {}),
62
102
  };
63
103
  }
64
104
  export class ContextFileMetadataStore {
@@ -71,51 +111,22 @@ export class ContextFileMetadataStore {
71
111
  this.db = new DatabaseSync(resolvedPath);
72
112
  this.db.exec("PRAGMA busy_timeout = 5000");
73
113
  this.db.exec("PRAGMA journal_mode = WAL");
74
- this.db.exec(`
75
- CREATE TABLE IF NOT EXISTS context_files (
76
- key TEXT PRIMARY KEY,
77
- label TEXT NOT NULL,
78
- managed_path TEXT NOT NULL,
79
- scope TEXT NOT NULL,
80
- source_type TEXT NOT NULL,
81
- agent_profile_name TEXT,
82
- active_revision_id TEXT,
83
- source_ref TEXT,
84
- source_hash TEXT,
85
- created_at TEXT NOT NULL,
86
- updated_at TEXT NOT NULL
87
- );
88
-
89
- CREATE INDEX IF NOT EXISTS idx_context_files_scope
90
- ON context_files(scope, updated_at);
91
-
92
- CREATE TABLE IF NOT EXISTS context_file_revisions (
93
- id TEXT PRIMARY KEY,
94
- context_file_key TEXT NOT NULL,
95
- kind TEXT NOT NULL,
96
- content_hash TEXT NOT NULL,
97
- content TEXT NOT NULL,
98
- created_at TEXT NOT NULL,
99
- actor_id TEXT,
100
- based_on_revision_id TEXT,
101
- source_hash_at_creation TEXT,
102
- note TEXT
103
- );
104
-
105
- CREATE INDEX IF NOT EXISTS idx_context_file_revisions_key
106
- ON context_file_revisions(context_file_key, created_at DESC);
107
- `);
114
+ this.migrateStorage();
115
+ this.assertCompatibleSchema();
108
116
  this.migrateLegacyStore();
109
117
  }
110
118
  listFiles() {
119
+ this.assertCompatibleSchema();
111
120
  const rows = this.db.prepare("SELECT * FROM context_files ORDER BY updated_at DESC").all();
112
121
  return rows.map(fileRowToRecord);
113
122
  }
114
123
  getFile(key) {
124
+ this.assertCompatibleSchema();
115
125
  const row = this.db.prepare("SELECT * FROM context_files WHERE key = ?").get(key);
116
126
  return row ? fileRowToRecord(row) : undefined;
117
127
  }
118
128
  createFile(input) {
129
+ this.assertCompatibleSchema();
119
130
  const createdAt = input.createdAt ?? new Date().toISOString();
120
131
  const updatedAt = input.updatedAt ?? createdAt;
121
132
  this.db
@@ -128,19 +139,22 @@ export class ContextFileMetadataStore {
128
139
  source_type,
129
140
  agent_profile_name,
130
141
  active_revision_id,
142
+ working_content,
131
143
  source_ref,
132
144
  source_hash,
145
+ source_content,
133
146
  created_at,
134
147
  updated_at
135
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
148
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
136
149
  `)
137
- .run(input.key, input.label, input.managedPath, input.scope, "managed", input.agentProfileName ?? null, null, input.sourceRef ?? null, input.sourceHash ?? null, createdAt, updatedAt);
150
+ .run(input.key, input.label, input.managedPath, input.scope, "managed", input.agentProfileName ?? null, null, input.workingContent, input.sourceRef ?? null, input.sourceHash ?? null, input.sourceContent ?? null, createdAt, updatedAt);
138
151
  const record = this.getFile(input.key);
139
152
  if (!record)
140
153
  throw new Error(`Failed to create context file record "${input.key}"`);
141
154
  return record;
142
155
  }
143
156
  updateFile(input) {
157
+ this.assertCompatibleSchema();
144
158
  this.db
145
159
  .prepare(`
146
160
  UPDATE context_files SET
@@ -149,22 +163,26 @@ export class ContextFileMetadataStore {
149
163
  scope = ?,
150
164
  source_type = ?,
151
165
  agent_profile_name = ?,
152
- active_revision_id = ?,
166
+ active_revision_id = NULL,
167
+ working_content = ?,
153
168
  source_ref = ?,
154
169
  source_hash = ?,
170
+ source_content = ?,
155
171
  created_at = ?,
156
172
  updated_at = ?
157
173
  WHERE key = ?
158
174
  `)
159
- .run(input.label, input.managedPath, input.scope, "managed", input.agentProfileName ?? null, input.activeRevisionId ?? null, input.sourceRef ?? null, input.sourceHash ?? null, input.createdAt, input.updatedAt, input.key);
175
+ .run(input.label, input.managedPath, input.scope, "managed", input.agentProfileName ?? null, input.workingContent, input.sourceRef ?? null, input.sourceHash ?? null, input.sourceContent ?? null, input.createdAt, input.updatedAt, input.key);
160
176
  const record = this.getFile(input.key);
161
177
  if (!record)
162
178
  throw new Error(`Failed to update context file record "${input.key}"`);
163
179
  return record;
164
180
  }
165
181
  deleteFile(key) {
182
+ this.assertCompatibleSchema();
166
183
  this.db.exec("BEGIN");
167
184
  try {
185
+ this.db.prepare("DELETE FROM context_file_manual_revisions WHERE context_file_key = ?").run(key);
168
186
  this.db.prepare("DELETE FROM context_file_revisions WHERE context_file_key = ?").run(key);
169
187
  this.db.prepare("DELETE FROM context_files WHERE key = ?").run(key);
170
188
  this.db.exec("COMMIT");
@@ -175,62 +193,250 @@ export class ContextFileMetadataStore {
175
193
  }
176
194
  }
177
195
  appendRevision(input) {
196
+ this.assertCompatibleSchema();
178
197
  const id = `rev_${randomUUID()}`;
179
198
  const createdAt = input.createdAt ?? new Date().toISOString();
180
199
  this.db
181
200
  .prepare(`
182
- INSERT INTO context_file_revisions (
201
+ INSERT INTO context_file_manual_revisions (
183
202
  id,
184
203
  context_file_key,
185
- kind,
204
+ name,
186
205
  content_hash,
187
206
  content,
188
207
  created_at,
189
- actor_id,
190
- based_on_revision_id,
191
- source_hash_at_creation,
192
- note
193
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
208
+ actor_id
209
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
194
210
  `)
195
- .run(id, input.contextFileKey, input.kind, input.contentHash, input.content, createdAt, input.actorId ?? null, input.basedOnRevisionId ?? null, input.sourceHashAtCreation ?? null, input.note ?? null);
211
+ .run(id, input.contextFileKey, input.name, input.contentHash, input.content, createdAt, input.actorId ?? null);
196
212
  const revision = this.getRevision(id);
197
213
  if (!revision)
198
214
  throw new Error(`Failed to create context file revision "${id}"`);
199
215
  return revision;
200
216
  }
201
217
  getRevision(id) {
202
- const row = this.db.prepare("SELECT * FROM context_file_revisions WHERE id = ?").get(id);
218
+ this.assertCompatibleSchema();
219
+ const row = this.db.prepare("SELECT * FROM context_file_manual_revisions WHERE id = ?").get(id);
203
220
  return row ? revisionRowToRecord(row) : undefined;
204
221
  }
205
222
  listRevisions(contextFileKey) {
223
+ this.assertCompatibleSchema();
206
224
  const rows = this.db
207
- .prepare("SELECT * FROM context_file_revisions WHERE context_file_key = ? ORDER BY created_at DESC")
225
+ .prepare("SELECT * FROM context_file_manual_revisions WHERE context_file_key = ? ORDER BY created_at DESC")
208
226
  .all(contextFileKey);
209
227
  return rows.map(revisionRowToRecord);
210
228
  }
211
- findLatestSourceSnapshot(contextFileKey, sourceHash) {
212
- const row = sourceHash
213
- ? this.db
214
- .prepare(`
215
- SELECT * FROM context_file_revisions
216
- WHERE context_file_key = ? AND kind = 'source-snapshot' AND content_hash = ?
217
- ORDER BY created_at DESC
218
- LIMIT 1
219
- `)
220
- .get(contextFileKey, sourceHash)
221
- : this.db
222
- .prepare(`
223
- SELECT * FROM context_file_revisions
224
- WHERE context_file_key = ? AND kind = 'source-snapshot'
225
- ORDER BY created_at DESC
226
- LIMIT 1
227
- `)
228
- .get(contextFileKey);
229
- return row ? revisionRowToRecord(row) : undefined;
230
- }
231
229
  close() {
232
230
  this.db.close();
233
231
  }
232
+ columnsFor(table) {
233
+ if (!this.tableExists(table))
234
+ return new Set();
235
+ return new Set(this.db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name));
236
+ }
237
+ hasColumns(table, required) {
238
+ const columns = this.columnsFor(table);
239
+ return required.every((column) => columns.has(column));
240
+ }
241
+ ensureColumn(table, column, type) {
242
+ if (this.columnsFor(table).has(column))
243
+ return;
244
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
245
+ }
246
+ tableExists(table) {
247
+ return Boolean(this.db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table));
248
+ }
249
+ ensureAutomaticRevisionTable() {
250
+ this.db.exec(`
251
+ CREATE TABLE IF NOT EXISTS context_file_revisions (
252
+ id TEXT PRIMARY KEY,
253
+ context_file_key TEXT NOT NULL,
254
+ kind TEXT NOT NULL,
255
+ content_hash TEXT NOT NULL,
256
+ content TEXT NOT NULL,
257
+ created_at TEXT NOT NULL,
258
+ actor_id TEXT,
259
+ based_on_revision_id TEXT,
260
+ source_hash_at_creation TEXT,
261
+ note TEXT
262
+ );
263
+
264
+ CREATE INDEX IF NOT EXISTS idx_context_file_revisions_key
265
+ ON context_file_revisions(context_file_key, created_at DESC);
266
+ `);
267
+ }
268
+ ensureManualRevisionTable() {
269
+ this.db.exec(`
270
+ CREATE TABLE IF NOT EXISTS context_file_manual_revisions (
271
+ id TEXT PRIMARY KEY,
272
+ context_file_key TEXT NOT NULL,
273
+ name TEXT NOT NULL,
274
+ content_hash TEXT NOT NULL,
275
+ content TEXT NOT NULL,
276
+ created_at TEXT NOT NULL,
277
+ actor_id TEXT
278
+ );
279
+
280
+ CREATE INDEX IF NOT EXISTS idx_context_file_manual_revisions_key
281
+ ON context_file_manual_revisions(context_file_key, created_at DESC);
282
+ `);
283
+ }
284
+ assertCompatibleSchema() {
285
+ const version = this.db.prepare("SELECT value FROM context_file_store_meta WHERE key = ?").get(CONTEXT_FILE_SCHEMA_VERSION_KEY);
286
+ const compatible = version?.value === CONTEXT_FILE_SCHEMA_VERSION
287
+ && this.hasColumns("context_files", CONTEXT_FILE_COLUMNS)
288
+ && this.hasColumns("context_file_revisions", AUTOMATIC_REVISION_COLUMNS)
289
+ && this.hasColumns("context_file_manual_revisions", MANUAL_REVISION_COLUMNS);
290
+ if (compatible)
291
+ return;
292
+ throw new Error(`Context Files metadata schema is incompatible with this Pibo version (expected ${CONTEXT_FILE_SCHEMA_VERSION}, found ${version?.value ?? "unversioned"}). ` +
293
+ "Activate a matching package version or use an isolated PIBO_HOME before accessing Context Files storage.");
294
+ }
295
+ migrateStorage() {
296
+ let recovered = [];
297
+ this.db.exec("BEGIN IMMEDIATE");
298
+ try {
299
+ this.db.exec(`
300
+ CREATE TABLE IF NOT EXISTS context_file_store_meta (
301
+ key TEXT PRIMARY KEY,
302
+ value TEXT NOT NULL
303
+ );
304
+ `);
305
+ const version = this.db.prepare("SELECT value FROM context_file_store_meta WHERE key = ?").get(CONTEXT_FILE_SCHEMA_VERSION_KEY);
306
+ if (version?.value === CONTEXT_FILE_SCHEMA_VERSION) {
307
+ this.db.exec("COMMIT");
308
+ return;
309
+ }
310
+ if (version?.value !== undefined && version.value !== "1") {
311
+ throw new Error(`Unsupported Context Files metadata schema version ${version.value}; expected ${CONTEXT_FILE_SCHEMA_VERSION}`);
312
+ }
313
+ this.db.exec(`
314
+ CREATE TABLE IF NOT EXISTS context_files (
315
+ key TEXT PRIMARY KEY,
316
+ label TEXT NOT NULL,
317
+ managed_path TEXT NOT NULL,
318
+ scope TEXT NOT NULL,
319
+ source_type TEXT NOT NULL,
320
+ agent_profile_name TEXT,
321
+ active_revision_id TEXT,
322
+ working_content TEXT,
323
+ source_ref TEXT,
324
+ source_hash TEXT,
325
+ source_content TEXT,
326
+ created_at TEXT NOT NULL,
327
+ updated_at TEXT NOT NULL
328
+ );
329
+
330
+ CREATE INDEX IF NOT EXISTS idx_context_files_scope
331
+ ON context_files(scope, updated_at);
332
+ `);
333
+ if (!this.hasColumns("context_files", CONTEXT_FILE_BASE_COLUMNS)) {
334
+ throw new Error("Context Files metadata has an unsupported context_files table shape");
335
+ }
336
+ this.ensureColumn("context_files", "working_content", "TEXT");
337
+ this.ensureColumn("context_files", "source_content", "TEXT");
338
+ const revisionTableExists = this.tableExists("context_file_revisions");
339
+ const revisionColumns = this.columnsFor("context_file_revisions");
340
+ const hasAutomaticRevisionSchema = revisionTableExists
341
+ && AUTOMATIC_REVISION_COLUMNS.every((column) => revisionColumns.has(column));
342
+ const hasManualOnlyRevisionSchema = revisionTableExists
343
+ && !hasAutomaticRevisionSchema
344
+ && MANUAL_REVISION_COLUMNS.every((column) => revisionColumns.has(column));
345
+ if (revisionTableExists && !hasAutomaticRevisionSchema && !hasManualOnlyRevisionSchema) {
346
+ throw new Error("Context Files metadata has an unsupported context_file_revisions table shape");
347
+ }
348
+ const files = this.db.prepare(`
349
+ SELECT key, managed_path, active_revision_id, working_content, source_content
350
+ FROM context_files
351
+ `).all();
352
+ recovered = files.map((file) => {
353
+ const fileExists = existsSync(file.managed_path);
354
+ const activeContent = !fileExists && revisionTableExists && file.active_revision_id
355
+ ? this.db.prepare("SELECT content FROM context_file_revisions WHERE id = ?").get(file.active_revision_id)?.content
356
+ : undefined;
357
+ const workingContent = fileExists
358
+ ? readFileSync(file.managed_path, "utf8")
359
+ : file.working_content ?? activeContent ?? "";
360
+ const sourceContent = file.source_content ?? (hasAutomaticRevisionSchema
361
+ ? this.db.prepare(`
362
+ SELECT content FROM context_file_revisions
363
+ WHERE context_file_key = ? AND kind = 'source-snapshot'
364
+ ORDER BY created_at DESC
365
+ LIMIT 1
366
+ `).get(file.key)?.content
367
+ : undefined);
368
+ return {
369
+ key: file.key,
370
+ managedPath: file.managed_path,
371
+ workingContent,
372
+ sourceContent,
373
+ restoreManagedFile: !fileExists && (file.working_content !== null || activeContent !== undefined),
374
+ };
375
+ });
376
+ if (hasManualOnlyRevisionSchema) {
377
+ this.db.exec("DROP INDEX IF EXISTS idx_context_file_revisions_key");
378
+ if (this.tableExists("context_file_manual_revisions")) {
379
+ if (!this.hasColumns("context_file_manual_revisions", MANUAL_REVISION_COLUMNS)) {
380
+ throw new Error("Context Files metadata has an unsupported context_file_manual_revisions table shape");
381
+ }
382
+ this.db.exec(`
383
+ INSERT OR IGNORE INTO context_file_manual_revisions (
384
+ id, context_file_key, name, content_hash, content, created_at, actor_id
385
+ )
386
+ SELECT id, context_file_key, name, content_hash, content, created_at, actor_id
387
+ FROM context_file_revisions;
388
+ DROP TABLE context_file_revisions;
389
+ `);
390
+ }
391
+ else {
392
+ this.db.exec("ALTER TABLE context_file_revisions RENAME TO context_file_manual_revisions");
393
+ }
394
+ this.db.exec(`
395
+ CREATE INDEX IF NOT EXISTS idx_context_file_manual_revisions_key
396
+ ON context_file_manual_revisions(context_file_key, created_at DESC);
397
+ `);
398
+ }
399
+ this.ensureAutomaticRevisionTable();
400
+ this.ensureManualRevisionTable();
401
+ if (hasAutomaticRevisionSchema && revisionColumns.has("name")) {
402
+ this.db.exec(`
403
+ INSERT OR IGNORE INTO context_file_manual_revisions (
404
+ id, context_file_key, name, content_hash, content, created_at, actor_id
405
+ )
406
+ SELECT id, context_file_key, name, content_hash, content, created_at, actor_id
407
+ FROM context_file_revisions
408
+ WHERE name IS NOT NULL;
409
+ `);
410
+ }
411
+ for (const file of recovered) {
412
+ this.db.prepare(`
413
+ UPDATE context_files
414
+ SET active_revision_id = NULL, working_content = ?, source_content = ?
415
+ WHERE key = ?
416
+ `).run(file.workingContent, file.sourceContent ?? null, file.key);
417
+ }
418
+ const migratedAt = new Date().toISOString();
419
+ this.db.prepare(`
420
+ INSERT INTO context_file_store_meta (key, value) VALUES (?, ?)
421
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
422
+ `).run(CONTEXT_FILE_SCHEMA_VERSION_KEY, CONTEXT_FILE_SCHEMA_VERSION);
423
+ this.db.prepare(`
424
+ INSERT INTO context_file_store_meta (key, value) VALUES (?, ?)
425
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
426
+ `).run(MANUAL_REVISION_MIGRATION_KEY, migratedAt);
427
+ this.db.exec("COMMIT");
428
+ }
429
+ catch (error) {
430
+ this.db.exec("ROLLBACK");
431
+ throw error;
432
+ }
433
+ for (const file of recovered) {
434
+ if (!file.restoreManagedFile)
435
+ continue;
436
+ mkdirSync(dirname(file.managedPath), { recursive: true });
437
+ writeFileSync(file.managedPath, file.workingContent, "utf8");
438
+ }
439
+ }
234
440
  migrateLegacyStore() {
235
441
  if (!this.legacyStorePath)
236
442
  return;
@@ -244,9 +450,11 @@ export class ContextFileMetadataStore {
244
450
  try {
245
451
  for (const file of legacyStore.files) {
246
452
  const resolvedPath = resolve(file.path);
247
- const createdAt = existsSync(resolvedPath)
453
+ const fileExists = existsSync(resolvedPath);
454
+ const createdAt = fileExists
248
455
  ? statSync(resolvedPath).mtime.toISOString()
249
456
  : new Date().toISOString();
457
+ const workingContent = fileExists ? readFileSync(resolvedPath, "utf8") : "";
250
458
  this.db
251
459
  .prepare(`
252
460
  INSERT INTO context_files (
@@ -257,37 +465,15 @@ export class ContextFileMetadataStore {
257
465
  source_type,
258
466
  agent_profile_name,
259
467
  active_revision_id,
468
+ working_content,
260
469
  source_ref,
261
470
  source_hash,
471
+ source_content,
262
472
  created_at,
263
473
  updated_at
264
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
474
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
265
475
  `)
266
- .run(file.key, file.label, resolvedPath, file.scope, "managed", file.agentProfileName ?? null, null, null, null, createdAt, createdAt);
267
- if (!existsSync(resolvedPath))
268
- continue;
269
- const content = readFileSync(resolvedPath, "utf8");
270
- const contentHash = hashContextFileContent(content);
271
- const revisionId = `rev_${randomUUID()}`;
272
- this.db
273
- .prepare(`
274
- INSERT INTO context_file_revisions (
275
- id,
276
- context_file_key,
277
- kind,
278
- content_hash,
279
- content,
280
- created_at,
281
- actor_id,
282
- based_on_revision_id,
283
- source_hash_at_creation,
284
- note
285
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
286
- `)
287
- .run(revisionId, file.key, "working", contentHash, content, createdAt, null, null, null, "Legacy managed file import");
288
- this.db
289
- .prepare("UPDATE context_files SET active_revision_id = ?, updated_at = ? WHERE key = ?")
290
- .run(revisionId, createdAt, file.key);
476
+ .run(file.key, file.label, resolvedPath, file.scope, "managed", file.agentProfileName ?? null, null, workingContent, null, null, null, createdAt, createdAt);
291
477
  }
292
478
  this.db.exec("COMMIT");
293
479
  }