@codelia/storage 0.1.2 → 0.1.10
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/dist/index.cjs +342 -23
- package/dist/index.d.cts +18 -3
- package/dist/index.d.ts +18 -3
- package/dist/index.js +342 -23
- package/package.json +6 -2
package/dist/index.cjs
CHANGED
|
@@ -255,8 +255,12 @@ var StoragePathServiceImpl = class {
|
|
|
255
255
|
var import_node_fs3 = require("fs");
|
|
256
256
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
257
257
|
var import_core = require("@codelia/core");
|
|
258
|
-
var
|
|
259
|
-
var
|
|
258
|
+
var LEGACY_STATE_DIRNAME = "state";
|
|
259
|
+
var MESSAGES_DIRNAME = "messages";
|
|
260
|
+
var STATE_DB_FILENAME = "state.db";
|
|
261
|
+
var resolveLegacyStateDir = (paths) => import_node_path3.default.join(paths.sessionsDir, LEGACY_STATE_DIRNAME);
|
|
262
|
+
var resolveMessagesDir = (paths) => import_node_path3.default.join(paths.sessionsDir, MESSAGES_DIRNAME);
|
|
263
|
+
var resolveStateDbPath = (paths) => import_node_path3.default.join(paths.sessionsDir, STATE_DB_FILENAME);
|
|
260
264
|
var extractLastUserMessage = (messages) => {
|
|
261
265
|
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
262
266
|
const message = messages[idx];
|
|
@@ -276,69 +280,384 @@ var toSummary = (state) => ({
|
|
|
276
280
|
message_count: Array.isArray(state.messages) ? state.messages.length : void 0,
|
|
277
281
|
last_user_message: Array.isArray(state.messages) ? extractLastUserMessage(state.messages) : void 0
|
|
278
282
|
});
|
|
283
|
+
var fromSummaryRow = (row) => ({
|
|
284
|
+
session_id: row.session_id,
|
|
285
|
+
updated_at: row.updated_at,
|
|
286
|
+
run_id: row.run_id ?? void 0,
|
|
287
|
+
message_count: row.message_count ?? void 0,
|
|
288
|
+
last_user_message: row.last_user_message ?? void 0
|
|
289
|
+
});
|
|
290
|
+
var serializeMessages = (messages) => {
|
|
291
|
+
if (messages.length === 0) return "";
|
|
292
|
+
return `${messages.map((message) => JSON.stringify(message)).join("\n")}
|
|
293
|
+
`;
|
|
294
|
+
};
|
|
295
|
+
var deserializeMessages = (payload) => {
|
|
296
|
+
if (!payload.trim()) return [];
|
|
297
|
+
const messages = [];
|
|
298
|
+
const lines = payload.split(/\r?\n/);
|
|
299
|
+
for (const rawLine of lines) {
|
|
300
|
+
const line = rawLine.trim();
|
|
301
|
+
if (!line) continue;
|
|
302
|
+
const parsed = JSON.parse(line);
|
|
303
|
+
if (!parsed || typeof parsed !== "object") {
|
|
304
|
+
throw new Error("Invalid session message entry");
|
|
305
|
+
}
|
|
306
|
+
messages.push(parsed);
|
|
307
|
+
}
|
|
308
|
+
return messages;
|
|
309
|
+
};
|
|
310
|
+
var atomicWriteFile = async (filePath, payload) => {
|
|
311
|
+
const dirname = import_node_path3.default.dirname(filePath);
|
|
312
|
+
const basename = import_node_path3.default.basename(filePath);
|
|
313
|
+
const tempFile = import_node_path3.default.join(
|
|
314
|
+
dirname,
|
|
315
|
+
`${basename}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`
|
|
316
|
+
);
|
|
317
|
+
let wroteTemp = false;
|
|
318
|
+
try {
|
|
319
|
+
await import_node_fs3.promises.writeFile(tempFile, payload, "utf8");
|
|
320
|
+
wroteTemp = true;
|
|
321
|
+
await import_node_fs3.promises.rename(tempFile, filePath);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (wroteTemp) {
|
|
324
|
+
await import_node_fs3.promises.rm(tempFile, { force: true }).catch(() => {
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
279
330
|
var SessionStateStoreImpl = class {
|
|
280
|
-
|
|
281
|
-
|
|
331
|
+
legacyStateDir;
|
|
332
|
+
messagesDir;
|
|
333
|
+
stateDbPath;
|
|
334
|
+
ensureDirs;
|
|
335
|
+
db;
|
|
336
|
+
schemaInit;
|
|
282
337
|
onError;
|
|
338
|
+
lastDbUnavailableError;
|
|
283
339
|
constructor(options = {}) {
|
|
284
340
|
const paths = options.paths ?? resolveStoragePaths();
|
|
285
|
-
this.
|
|
286
|
-
this.
|
|
341
|
+
this.legacyStateDir = resolveLegacyStateDir(paths);
|
|
342
|
+
this.messagesDir = resolveMessagesDir(paths);
|
|
343
|
+
this.stateDbPath = resolveStateDbPath(paths);
|
|
344
|
+
this.ensureDirs = Promise.all([
|
|
345
|
+
import_node_fs3.promises.mkdir(paths.sessionsDir, { recursive: true }),
|
|
346
|
+
import_node_fs3.promises.mkdir(this.legacyStateDir, { recursive: true }),
|
|
347
|
+
import_node_fs3.promises.mkdir(this.messagesDir, { recursive: true })
|
|
348
|
+
]).then(() => {
|
|
287
349
|
});
|
|
288
350
|
this.onError = options.onError;
|
|
351
|
+
this.db = null;
|
|
352
|
+
this.schemaInit = null;
|
|
353
|
+
this.lastDbUnavailableError = null;
|
|
289
354
|
}
|
|
290
|
-
|
|
291
|
-
|
|
355
|
+
getOrCreateDb() {
|
|
356
|
+
if (!this.db) {
|
|
357
|
+
this.db = this.openDatabase();
|
|
358
|
+
}
|
|
359
|
+
return this.db;
|
|
292
360
|
}
|
|
293
|
-
|
|
361
|
+
resolveLegacyPath(sessionId) {
|
|
362
|
+
return import_node_path3.default.join(this.legacyStateDir, `${sessionId}.json`);
|
|
363
|
+
}
|
|
364
|
+
resolveMessagePath(sessionId) {
|
|
365
|
+
return import_node_path3.default.join(this.messagesDir, `${sessionId}.jsonl`);
|
|
366
|
+
}
|
|
367
|
+
async openDatabase() {
|
|
368
|
+
await this.ensureDirs;
|
|
369
|
+
if (process.versions.bun) {
|
|
370
|
+
const bunSqliteSpecifier = "bun:sqlite";
|
|
371
|
+
const { Database } = await import(bunSqliteSpecifier);
|
|
372
|
+
const db2 = new Database(this.stateDbPath, { create: true });
|
|
373
|
+
return {
|
|
374
|
+
exec: (sql) => {
|
|
375
|
+
db2.exec(sql);
|
|
376
|
+
},
|
|
377
|
+
run: (sql, params = []) => {
|
|
378
|
+
db2.query(sql).run(...params);
|
|
379
|
+
},
|
|
380
|
+
get: (sql, params = []) => {
|
|
381
|
+
const row = db2.query(sql).get(...params);
|
|
382
|
+
return row ?? void 0;
|
|
383
|
+
},
|
|
384
|
+
all: (sql, params = []) => db2.query(sql).all(...params)
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const betterSqliteSpecifier = "better-sqlite3";
|
|
388
|
+
const betterSqliteModule = await import(betterSqliteSpecifier);
|
|
389
|
+
const BetterSqlite3 = betterSqliteModule.default ?? betterSqliteModule;
|
|
390
|
+
const db = new BetterSqlite3(this.stateDbPath);
|
|
391
|
+
return {
|
|
392
|
+
exec: (sql) => {
|
|
393
|
+
db.exec(sql);
|
|
394
|
+
},
|
|
395
|
+
run: (sql, params = []) => {
|
|
396
|
+
db.prepare(sql).run(...params);
|
|
397
|
+
},
|
|
398
|
+
get: (sql, params = []) => {
|
|
399
|
+
const row = db.prepare(sql).get(...params);
|
|
400
|
+
return row ?? void 0;
|
|
401
|
+
},
|
|
402
|
+
all: (sql, params = []) => db.prepare(sql).all(...params)
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
initDatabaseSchema(db) {
|
|
406
|
+
db.exec("PRAGMA journal_mode = WAL;");
|
|
407
|
+
db.exec(`
|
|
408
|
+
CREATE TABLE IF NOT EXISTS session_state (
|
|
409
|
+
session_id TEXT PRIMARY KEY,
|
|
410
|
+
updated_at TEXT NOT NULL,
|
|
411
|
+
run_id TEXT,
|
|
412
|
+
invoke_seq INTEGER,
|
|
413
|
+
schema_version INTEGER NOT NULL,
|
|
414
|
+
meta_json TEXT,
|
|
415
|
+
message_count INTEGER,
|
|
416
|
+
last_user_message TEXT
|
|
417
|
+
);
|
|
418
|
+
`);
|
|
419
|
+
db.exec(`
|
|
420
|
+
CREATE INDEX IF NOT EXISTS idx_session_state_updated_at
|
|
421
|
+
ON session_state(updated_at DESC);
|
|
422
|
+
`);
|
|
423
|
+
}
|
|
424
|
+
async tryGetDb(action, detail) {
|
|
294
425
|
try {
|
|
295
|
-
const
|
|
426
|
+
const db = await this.getOrCreateDb();
|
|
427
|
+
if (!this.schemaInit) {
|
|
428
|
+
this.schemaInit = Promise.resolve().then(() => {
|
|
429
|
+
this.initDatabaseSchema(db);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
await this.schemaInit;
|
|
433
|
+
this.lastDbUnavailableError = null;
|
|
434
|
+
return db;
|
|
435
|
+
} catch (error) {
|
|
436
|
+
this.db = null;
|
|
437
|
+
this.schemaInit = null;
|
|
438
|
+
this.lastDbUnavailableError = error;
|
|
439
|
+
this.onError?.(error, { action: `${action}.db_unavailable`, detail });
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async requireDb(action, detail) {
|
|
444
|
+
const db = await this.tryGetDb(action, detail);
|
|
445
|
+
if (!db) {
|
|
446
|
+
const reason = this.lastDbUnavailableError ? `: ${String(this.lastDbUnavailableError)}` : "";
|
|
447
|
+
throw new Error(`Session index database unavailable${reason}`);
|
|
448
|
+
}
|
|
449
|
+
return db;
|
|
450
|
+
}
|
|
451
|
+
async loadLegacy(sessionId) {
|
|
452
|
+
try {
|
|
453
|
+
const file = await import_node_fs3.promises.readFile(this.resolveLegacyPath(sessionId), "utf8");
|
|
296
454
|
const parsed = JSON.parse(file);
|
|
297
455
|
if (!parsed || typeof parsed !== "object") return null;
|
|
298
|
-
return
|
|
456
|
+
if (!parsed.session_id || !parsed.updated_at) return null;
|
|
457
|
+
const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
458
|
+
return {
|
|
459
|
+
schema_version: typeof parsed.schema_version === "number" ? parsed.schema_version : 1,
|
|
460
|
+
session_id: parsed.session_id,
|
|
461
|
+
updated_at: parsed.updated_at,
|
|
462
|
+
run_id: parsed.run_id,
|
|
463
|
+
invoke_seq: parsed.invoke_seq,
|
|
464
|
+
messages,
|
|
465
|
+
meta: parsed.meta && typeof parsed.meta === "object" ? parsed.meta : void 0
|
|
466
|
+
};
|
|
299
467
|
} catch (error) {
|
|
300
468
|
if (error.code === "ENOENT") {
|
|
301
469
|
return null;
|
|
302
470
|
}
|
|
303
|
-
this.onError?.(error, { action: "load", detail: sessionId });
|
|
471
|
+
this.onError?.(error, { action: "legacy.load", detail: sessionId });
|
|
304
472
|
throw error;
|
|
305
473
|
}
|
|
306
474
|
}
|
|
475
|
+
async loadFromIndex(sessionId, db) {
|
|
476
|
+
let row;
|
|
477
|
+
try {
|
|
478
|
+
row = db.get(
|
|
479
|
+
`SELECT session_id, updated_at, run_id, invoke_seq, schema_version, meta_json
|
|
480
|
+
FROM session_state
|
|
481
|
+
WHERE session_id = ?`,
|
|
482
|
+
[sessionId]
|
|
483
|
+
);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
this.onError?.(error, { action: "index.load", detail: sessionId });
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
488
|
+
if (!row) return null;
|
|
489
|
+
let messages;
|
|
490
|
+
try {
|
|
491
|
+
const payload = await import_node_fs3.promises.readFile(
|
|
492
|
+
this.resolveMessagePath(sessionId),
|
|
493
|
+
"utf8"
|
|
494
|
+
);
|
|
495
|
+
messages = deserializeMessages(payload);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
if (error.code === "ENOENT") {
|
|
498
|
+
messages = [];
|
|
499
|
+
} else {
|
|
500
|
+
this.onError?.(error, { action: "messages.load", detail: sessionId });
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
let meta;
|
|
505
|
+
if (row.meta_json) {
|
|
506
|
+
try {
|
|
507
|
+
const parsed = JSON.parse(row.meta_json);
|
|
508
|
+
if (parsed && typeof parsed === "object") {
|
|
509
|
+
meta = parsed;
|
|
510
|
+
}
|
|
511
|
+
} catch (error) {
|
|
512
|
+
this.onError?.(error, {
|
|
513
|
+
action: "index.meta.parse",
|
|
514
|
+
detail: sessionId
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
schema_version: 1,
|
|
520
|
+
session_id: row.session_id,
|
|
521
|
+
updated_at: row.updated_at,
|
|
522
|
+
run_id: row.run_id ?? void 0,
|
|
523
|
+
invoke_seq: row.invoke_seq ?? void 0,
|
|
524
|
+
messages,
|
|
525
|
+
meta
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
async saveToIndex(state, db) {
|
|
529
|
+
const messagePayload = serializeMessages(
|
|
530
|
+
Array.isArray(state.messages) ? state.messages : []
|
|
531
|
+
);
|
|
532
|
+
await atomicWriteFile(
|
|
533
|
+
this.resolveMessagePath(state.session_id),
|
|
534
|
+
messagePayload
|
|
535
|
+
);
|
|
536
|
+
const summary = toSummary(state);
|
|
537
|
+
db.run(
|
|
538
|
+
`INSERT INTO session_state (
|
|
539
|
+
session_id,
|
|
540
|
+
updated_at,
|
|
541
|
+
run_id,
|
|
542
|
+
invoke_seq,
|
|
543
|
+
schema_version,
|
|
544
|
+
meta_json,
|
|
545
|
+
message_count,
|
|
546
|
+
last_user_message
|
|
547
|
+
)
|
|
548
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
549
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
550
|
+
updated_at = excluded.updated_at,
|
|
551
|
+
run_id = excluded.run_id,
|
|
552
|
+
invoke_seq = excluded.invoke_seq,
|
|
553
|
+
schema_version = excluded.schema_version,
|
|
554
|
+
meta_json = excluded.meta_json,
|
|
555
|
+
message_count = excluded.message_count,
|
|
556
|
+
last_user_message = excluded.last_user_message`,
|
|
557
|
+
[
|
|
558
|
+
state.session_id,
|
|
559
|
+
state.updated_at,
|
|
560
|
+
state.run_id ?? null,
|
|
561
|
+
state.invoke_seq ?? null,
|
|
562
|
+
state.schema_version ?? 1,
|
|
563
|
+
state.meta ? JSON.stringify(state.meta) : null,
|
|
564
|
+
summary.message_count ?? null,
|
|
565
|
+
summary.last_user_message ?? null
|
|
566
|
+
]
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
async load(sessionId) {
|
|
570
|
+
const db = await this.requireDb("load", sessionId);
|
|
571
|
+
try {
|
|
572
|
+
const indexed = await this.loadFromIndex(sessionId, db);
|
|
573
|
+
if (indexed) return indexed;
|
|
574
|
+
} catch (error) {
|
|
575
|
+
this.onError?.(error, { action: "load.index", detail: sessionId });
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
const legacy = await this.loadLegacy(sessionId);
|
|
579
|
+
if (!legacy) return null;
|
|
580
|
+
if (db) {
|
|
581
|
+
try {
|
|
582
|
+
await this.saveToIndex(legacy, db);
|
|
583
|
+
} catch (error) {
|
|
584
|
+
this.onError?.(error, {
|
|
585
|
+
action: "load.migrate_legacy",
|
|
586
|
+
detail: sessionId
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return legacy;
|
|
591
|
+
}
|
|
307
592
|
async save(state) {
|
|
308
|
-
await this.
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
593
|
+
await this.ensureDirs;
|
|
594
|
+
const db = await this.requireDb("save", state.session_id);
|
|
595
|
+
try {
|
|
596
|
+
await this.saveToIndex(state, db);
|
|
597
|
+
} catch (error) {
|
|
598
|
+
this.onError?.(error, { action: "save.index", detail: state.session_id });
|
|
599
|
+
throw error;
|
|
600
|
+
}
|
|
312
601
|
}
|
|
313
|
-
async
|
|
314
|
-
await this.ensureDir;
|
|
602
|
+
async listLegacySummaries() {
|
|
315
603
|
let entries;
|
|
316
604
|
try {
|
|
317
|
-
entries = await import_node_fs3.promises.readdir(this.
|
|
605
|
+
entries = await import_node_fs3.promises.readdir(this.legacyStateDir, {
|
|
318
606
|
withFileTypes: true,
|
|
319
607
|
encoding: "utf8"
|
|
320
608
|
});
|
|
321
609
|
} catch (error) {
|
|
322
|
-
this.onError?.(error, { action: "list" });
|
|
610
|
+
this.onError?.(error, { action: "legacy.list" });
|
|
323
611
|
throw error;
|
|
324
612
|
}
|
|
325
613
|
const summaries = [];
|
|
326
614
|
for (const entry of entries) {
|
|
327
615
|
if (!entry.isFile()) continue;
|
|
328
616
|
if (!entry.name.endsWith(".json")) continue;
|
|
329
|
-
const filePath = import_node_path3.default.join(this.
|
|
617
|
+
const filePath = import_node_path3.default.join(this.legacyStateDir, entry.name);
|
|
330
618
|
try {
|
|
331
619
|
const contents = await import_node_fs3.promises.readFile(filePath, "utf8");
|
|
332
620
|
const parsed = JSON.parse(contents);
|
|
333
621
|
if (!parsed || typeof parsed !== "object") continue;
|
|
334
622
|
if (!parsed.session_id || !parsed.updated_at) continue;
|
|
335
|
-
summaries.push(
|
|
623
|
+
summaries.push(
|
|
624
|
+
toSummary({
|
|
625
|
+
...parsed,
|
|
626
|
+
schema_version: typeof parsed.schema_version === "number" ? parsed.schema_version : 1,
|
|
627
|
+
messages: Array.isArray(parsed.messages) ? parsed.messages : []
|
|
628
|
+
})
|
|
629
|
+
);
|
|
336
630
|
} catch (error) {
|
|
337
|
-
this.onError?.(error, { action: "parse", detail: filePath });
|
|
631
|
+
this.onError?.(error, { action: "legacy.parse", detail: filePath });
|
|
338
632
|
}
|
|
339
633
|
}
|
|
340
634
|
return summaries;
|
|
341
635
|
}
|
|
636
|
+
async list() {
|
|
637
|
+
await this.ensureDirs;
|
|
638
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
639
|
+
const db = await this.requireDb("list");
|
|
640
|
+
try {
|
|
641
|
+
const rows = db.all(
|
|
642
|
+
`SELECT session_id, updated_at, run_id, message_count, last_user_message
|
|
643
|
+
FROM session_state
|
|
644
|
+
ORDER BY updated_at DESC`
|
|
645
|
+
);
|
|
646
|
+
for (const row of rows) {
|
|
647
|
+
summaries.set(row.session_id, fromSummaryRow(row));
|
|
648
|
+
}
|
|
649
|
+
} catch (error) {
|
|
650
|
+
this.onError?.(error, { action: "list.index" });
|
|
651
|
+
throw error;
|
|
652
|
+
}
|
|
653
|
+
const legacy = await this.listLegacySummaries();
|
|
654
|
+
for (const item of legacy) {
|
|
655
|
+
if (!summaries.has(item.session_id)) {
|
|
656
|
+
summaries.set(item.session_id, item);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return [...summaries.values()];
|
|
660
|
+
}
|
|
342
661
|
};
|
|
343
662
|
|
|
344
663
|
// src/tool-output-cache.ts
|
package/dist/index.d.cts
CHANGED
|
@@ -40,13 +40,28 @@ type SessionStateStoreOptions = {
|
|
|
40
40
|
}) => void;
|
|
41
41
|
};
|
|
42
42
|
declare class SessionStateStoreImpl implements SessionStateStore {
|
|
43
|
-
private readonly
|
|
44
|
-
private readonly
|
|
43
|
+
private readonly legacyStateDir;
|
|
44
|
+
private readonly messagesDir;
|
|
45
|
+
private readonly stateDbPath;
|
|
46
|
+
private readonly ensureDirs;
|
|
47
|
+
private db;
|
|
48
|
+
private schemaInit;
|
|
45
49
|
private readonly onError?;
|
|
50
|
+
private lastDbUnavailableError;
|
|
46
51
|
constructor(options?: SessionStateStoreOptions);
|
|
47
|
-
private
|
|
52
|
+
private getOrCreateDb;
|
|
53
|
+
private resolveLegacyPath;
|
|
54
|
+
private resolveMessagePath;
|
|
55
|
+
private openDatabase;
|
|
56
|
+
private initDatabaseSchema;
|
|
57
|
+
private tryGetDb;
|
|
58
|
+
private requireDb;
|
|
59
|
+
private loadLegacy;
|
|
60
|
+
private loadFromIndex;
|
|
61
|
+
private saveToIndex;
|
|
48
62
|
load(sessionId: string): Promise<SessionState | null>;
|
|
49
63
|
save(state: SessionState): Promise<void>;
|
|
64
|
+
private listLegacySummaries;
|
|
50
65
|
list(): Promise<SessionStateSummary[]>;
|
|
51
66
|
}
|
|
52
67
|
|
package/dist/index.d.ts
CHANGED
|
@@ -40,13 +40,28 @@ type SessionStateStoreOptions = {
|
|
|
40
40
|
}) => void;
|
|
41
41
|
};
|
|
42
42
|
declare class SessionStateStoreImpl implements SessionStateStore {
|
|
43
|
-
private readonly
|
|
44
|
-
private readonly
|
|
43
|
+
private readonly legacyStateDir;
|
|
44
|
+
private readonly messagesDir;
|
|
45
|
+
private readonly stateDbPath;
|
|
46
|
+
private readonly ensureDirs;
|
|
47
|
+
private db;
|
|
48
|
+
private schemaInit;
|
|
45
49
|
private readonly onError?;
|
|
50
|
+
private lastDbUnavailableError;
|
|
46
51
|
constructor(options?: SessionStateStoreOptions);
|
|
47
|
-
private
|
|
52
|
+
private getOrCreateDb;
|
|
53
|
+
private resolveLegacyPath;
|
|
54
|
+
private resolveMessagePath;
|
|
55
|
+
private openDatabase;
|
|
56
|
+
private initDatabaseSchema;
|
|
57
|
+
private tryGetDb;
|
|
58
|
+
private requireDb;
|
|
59
|
+
private loadLegacy;
|
|
60
|
+
private loadFromIndex;
|
|
61
|
+
private saveToIndex;
|
|
48
62
|
load(sessionId: string): Promise<SessionState | null>;
|
|
49
63
|
save(state: SessionState): Promise<void>;
|
|
64
|
+
private listLegacySummaries;
|
|
50
65
|
list(): Promise<SessionStateSummary[]>;
|
|
51
66
|
}
|
|
52
67
|
|
package/dist/index.js
CHANGED
|
@@ -212,8 +212,12 @@ var StoragePathServiceImpl = class {
|
|
|
212
212
|
import { promises as fs3 } from "fs";
|
|
213
213
|
import path3 from "path";
|
|
214
214
|
import { stringifyContent } from "@codelia/core";
|
|
215
|
-
var
|
|
216
|
-
var
|
|
215
|
+
var LEGACY_STATE_DIRNAME = "state";
|
|
216
|
+
var MESSAGES_DIRNAME = "messages";
|
|
217
|
+
var STATE_DB_FILENAME = "state.db";
|
|
218
|
+
var resolveLegacyStateDir = (paths) => path3.join(paths.sessionsDir, LEGACY_STATE_DIRNAME);
|
|
219
|
+
var resolveMessagesDir = (paths) => path3.join(paths.sessionsDir, MESSAGES_DIRNAME);
|
|
220
|
+
var resolveStateDbPath = (paths) => path3.join(paths.sessionsDir, STATE_DB_FILENAME);
|
|
217
221
|
var extractLastUserMessage = (messages) => {
|
|
218
222
|
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
219
223
|
const message = messages[idx];
|
|
@@ -233,69 +237,384 @@ var toSummary = (state) => ({
|
|
|
233
237
|
message_count: Array.isArray(state.messages) ? state.messages.length : void 0,
|
|
234
238
|
last_user_message: Array.isArray(state.messages) ? extractLastUserMessage(state.messages) : void 0
|
|
235
239
|
});
|
|
240
|
+
var fromSummaryRow = (row) => ({
|
|
241
|
+
session_id: row.session_id,
|
|
242
|
+
updated_at: row.updated_at,
|
|
243
|
+
run_id: row.run_id ?? void 0,
|
|
244
|
+
message_count: row.message_count ?? void 0,
|
|
245
|
+
last_user_message: row.last_user_message ?? void 0
|
|
246
|
+
});
|
|
247
|
+
var serializeMessages = (messages) => {
|
|
248
|
+
if (messages.length === 0) return "";
|
|
249
|
+
return `${messages.map((message) => JSON.stringify(message)).join("\n")}
|
|
250
|
+
`;
|
|
251
|
+
};
|
|
252
|
+
var deserializeMessages = (payload) => {
|
|
253
|
+
if (!payload.trim()) return [];
|
|
254
|
+
const messages = [];
|
|
255
|
+
const lines = payload.split(/\r?\n/);
|
|
256
|
+
for (const rawLine of lines) {
|
|
257
|
+
const line = rawLine.trim();
|
|
258
|
+
if (!line) continue;
|
|
259
|
+
const parsed = JSON.parse(line);
|
|
260
|
+
if (!parsed || typeof parsed !== "object") {
|
|
261
|
+
throw new Error("Invalid session message entry");
|
|
262
|
+
}
|
|
263
|
+
messages.push(parsed);
|
|
264
|
+
}
|
|
265
|
+
return messages;
|
|
266
|
+
};
|
|
267
|
+
var atomicWriteFile = async (filePath, payload) => {
|
|
268
|
+
const dirname = path3.dirname(filePath);
|
|
269
|
+
const basename = path3.basename(filePath);
|
|
270
|
+
const tempFile = path3.join(
|
|
271
|
+
dirname,
|
|
272
|
+
`${basename}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`
|
|
273
|
+
);
|
|
274
|
+
let wroteTemp = false;
|
|
275
|
+
try {
|
|
276
|
+
await fs3.writeFile(tempFile, payload, "utf8");
|
|
277
|
+
wroteTemp = true;
|
|
278
|
+
await fs3.rename(tempFile, filePath);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (wroteTemp) {
|
|
281
|
+
await fs3.rm(tempFile, { force: true }).catch(() => {
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
236
287
|
var SessionStateStoreImpl = class {
|
|
237
|
-
|
|
238
|
-
|
|
288
|
+
legacyStateDir;
|
|
289
|
+
messagesDir;
|
|
290
|
+
stateDbPath;
|
|
291
|
+
ensureDirs;
|
|
292
|
+
db;
|
|
293
|
+
schemaInit;
|
|
239
294
|
onError;
|
|
295
|
+
lastDbUnavailableError;
|
|
240
296
|
constructor(options = {}) {
|
|
241
297
|
const paths = options.paths ?? resolveStoragePaths();
|
|
242
|
-
this.
|
|
243
|
-
this.
|
|
298
|
+
this.legacyStateDir = resolveLegacyStateDir(paths);
|
|
299
|
+
this.messagesDir = resolveMessagesDir(paths);
|
|
300
|
+
this.stateDbPath = resolveStateDbPath(paths);
|
|
301
|
+
this.ensureDirs = Promise.all([
|
|
302
|
+
fs3.mkdir(paths.sessionsDir, { recursive: true }),
|
|
303
|
+
fs3.mkdir(this.legacyStateDir, { recursive: true }),
|
|
304
|
+
fs3.mkdir(this.messagesDir, { recursive: true })
|
|
305
|
+
]).then(() => {
|
|
244
306
|
});
|
|
245
307
|
this.onError = options.onError;
|
|
308
|
+
this.db = null;
|
|
309
|
+
this.schemaInit = null;
|
|
310
|
+
this.lastDbUnavailableError = null;
|
|
246
311
|
}
|
|
247
|
-
|
|
248
|
-
|
|
312
|
+
getOrCreateDb() {
|
|
313
|
+
if (!this.db) {
|
|
314
|
+
this.db = this.openDatabase();
|
|
315
|
+
}
|
|
316
|
+
return this.db;
|
|
249
317
|
}
|
|
250
|
-
|
|
318
|
+
resolveLegacyPath(sessionId) {
|
|
319
|
+
return path3.join(this.legacyStateDir, `${sessionId}.json`);
|
|
320
|
+
}
|
|
321
|
+
resolveMessagePath(sessionId) {
|
|
322
|
+
return path3.join(this.messagesDir, `${sessionId}.jsonl`);
|
|
323
|
+
}
|
|
324
|
+
async openDatabase() {
|
|
325
|
+
await this.ensureDirs;
|
|
326
|
+
if (process.versions.bun) {
|
|
327
|
+
const bunSqliteSpecifier = "bun:sqlite";
|
|
328
|
+
const { Database } = await import(bunSqliteSpecifier);
|
|
329
|
+
const db2 = new Database(this.stateDbPath, { create: true });
|
|
330
|
+
return {
|
|
331
|
+
exec: (sql) => {
|
|
332
|
+
db2.exec(sql);
|
|
333
|
+
},
|
|
334
|
+
run: (sql, params = []) => {
|
|
335
|
+
db2.query(sql).run(...params);
|
|
336
|
+
},
|
|
337
|
+
get: (sql, params = []) => {
|
|
338
|
+
const row = db2.query(sql).get(...params);
|
|
339
|
+
return row ?? void 0;
|
|
340
|
+
},
|
|
341
|
+
all: (sql, params = []) => db2.query(sql).all(...params)
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const betterSqliteSpecifier = "better-sqlite3";
|
|
345
|
+
const betterSqliteModule = await import(betterSqliteSpecifier);
|
|
346
|
+
const BetterSqlite3 = betterSqliteModule.default ?? betterSqliteModule;
|
|
347
|
+
const db = new BetterSqlite3(this.stateDbPath);
|
|
348
|
+
return {
|
|
349
|
+
exec: (sql) => {
|
|
350
|
+
db.exec(sql);
|
|
351
|
+
},
|
|
352
|
+
run: (sql, params = []) => {
|
|
353
|
+
db.prepare(sql).run(...params);
|
|
354
|
+
},
|
|
355
|
+
get: (sql, params = []) => {
|
|
356
|
+
const row = db.prepare(sql).get(...params);
|
|
357
|
+
return row ?? void 0;
|
|
358
|
+
},
|
|
359
|
+
all: (sql, params = []) => db.prepare(sql).all(...params)
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
initDatabaseSchema(db) {
|
|
363
|
+
db.exec("PRAGMA journal_mode = WAL;");
|
|
364
|
+
db.exec(`
|
|
365
|
+
CREATE TABLE IF NOT EXISTS session_state (
|
|
366
|
+
session_id TEXT PRIMARY KEY,
|
|
367
|
+
updated_at TEXT NOT NULL,
|
|
368
|
+
run_id TEXT,
|
|
369
|
+
invoke_seq INTEGER,
|
|
370
|
+
schema_version INTEGER NOT NULL,
|
|
371
|
+
meta_json TEXT,
|
|
372
|
+
message_count INTEGER,
|
|
373
|
+
last_user_message TEXT
|
|
374
|
+
);
|
|
375
|
+
`);
|
|
376
|
+
db.exec(`
|
|
377
|
+
CREATE INDEX IF NOT EXISTS idx_session_state_updated_at
|
|
378
|
+
ON session_state(updated_at DESC);
|
|
379
|
+
`);
|
|
380
|
+
}
|
|
381
|
+
async tryGetDb(action, detail) {
|
|
251
382
|
try {
|
|
252
|
-
const
|
|
383
|
+
const db = await this.getOrCreateDb();
|
|
384
|
+
if (!this.schemaInit) {
|
|
385
|
+
this.schemaInit = Promise.resolve().then(() => {
|
|
386
|
+
this.initDatabaseSchema(db);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
await this.schemaInit;
|
|
390
|
+
this.lastDbUnavailableError = null;
|
|
391
|
+
return db;
|
|
392
|
+
} catch (error) {
|
|
393
|
+
this.db = null;
|
|
394
|
+
this.schemaInit = null;
|
|
395
|
+
this.lastDbUnavailableError = error;
|
|
396
|
+
this.onError?.(error, { action: `${action}.db_unavailable`, detail });
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async requireDb(action, detail) {
|
|
401
|
+
const db = await this.tryGetDb(action, detail);
|
|
402
|
+
if (!db) {
|
|
403
|
+
const reason = this.lastDbUnavailableError ? `: ${String(this.lastDbUnavailableError)}` : "";
|
|
404
|
+
throw new Error(`Session index database unavailable${reason}`);
|
|
405
|
+
}
|
|
406
|
+
return db;
|
|
407
|
+
}
|
|
408
|
+
async loadLegacy(sessionId) {
|
|
409
|
+
try {
|
|
410
|
+
const file = await fs3.readFile(this.resolveLegacyPath(sessionId), "utf8");
|
|
253
411
|
const parsed = JSON.parse(file);
|
|
254
412
|
if (!parsed || typeof parsed !== "object") return null;
|
|
255
|
-
return
|
|
413
|
+
if (!parsed.session_id || !parsed.updated_at) return null;
|
|
414
|
+
const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
415
|
+
return {
|
|
416
|
+
schema_version: typeof parsed.schema_version === "number" ? parsed.schema_version : 1,
|
|
417
|
+
session_id: parsed.session_id,
|
|
418
|
+
updated_at: parsed.updated_at,
|
|
419
|
+
run_id: parsed.run_id,
|
|
420
|
+
invoke_seq: parsed.invoke_seq,
|
|
421
|
+
messages,
|
|
422
|
+
meta: parsed.meta && typeof parsed.meta === "object" ? parsed.meta : void 0
|
|
423
|
+
};
|
|
256
424
|
} catch (error) {
|
|
257
425
|
if (error.code === "ENOENT") {
|
|
258
426
|
return null;
|
|
259
427
|
}
|
|
260
|
-
this.onError?.(error, { action: "load", detail: sessionId });
|
|
428
|
+
this.onError?.(error, { action: "legacy.load", detail: sessionId });
|
|
261
429
|
throw error;
|
|
262
430
|
}
|
|
263
431
|
}
|
|
432
|
+
async loadFromIndex(sessionId, db) {
|
|
433
|
+
let row;
|
|
434
|
+
try {
|
|
435
|
+
row = db.get(
|
|
436
|
+
`SELECT session_id, updated_at, run_id, invoke_seq, schema_version, meta_json
|
|
437
|
+
FROM session_state
|
|
438
|
+
WHERE session_id = ?`,
|
|
439
|
+
[sessionId]
|
|
440
|
+
);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
this.onError?.(error, { action: "index.load", detail: sessionId });
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
if (!row) return null;
|
|
446
|
+
let messages;
|
|
447
|
+
try {
|
|
448
|
+
const payload = await fs3.readFile(
|
|
449
|
+
this.resolveMessagePath(sessionId),
|
|
450
|
+
"utf8"
|
|
451
|
+
);
|
|
452
|
+
messages = deserializeMessages(payload);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
if (error.code === "ENOENT") {
|
|
455
|
+
messages = [];
|
|
456
|
+
} else {
|
|
457
|
+
this.onError?.(error, { action: "messages.load", detail: sessionId });
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
let meta;
|
|
462
|
+
if (row.meta_json) {
|
|
463
|
+
try {
|
|
464
|
+
const parsed = JSON.parse(row.meta_json);
|
|
465
|
+
if (parsed && typeof parsed === "object") {
|
|
466
|
+
meta = parsed;
|
|
467
|
+
}
|
|
468
|
+
} catch (error) {
|
|
469
|
+
this.onError?.(error, {
|
|
470
|
+
action: "index.meta.parse",
|
|
471
|
+
detail: sessionId
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
schema_version: 1,
|
|
477
|
+
session_id: row.session_id,
|
|
478
|
+
updated_at: row.updated_at,
|
|
479
|
+
run_id: row.run_id ?? void 0,
|
|
480
|
+
invoke_seq: row.invoke_seq ?? void 0,
|
|
481
|
+
messages,
|
|
482
|
+
meta
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
async saveToIndex(state, db) {
|
|
486
|
+
const messagePayload = serializeMessages(
|
|
487
|
+
Array.isArray(state.messages) ? state.messages : []
|
|
488
|
+
);
|
|
489
|
+
await atomicWriteFile(
|
|
490
|
+
this.resolveMessagePath(state.session_id),
|
|
491
|
+
messagePayload
|
|
492
|
+
);
|
|
493
|
+
const summary = toSummary(state);
|
|
494
|
+
db.run(
|
|
495
|
+
`INSERT INTO session_state (
|
|
496
|
+
session_id,
|
|
497
|
+
updated_at,
|
|
498
|
+
run_id,
|
|
499
|
+
invoke_seq,
|
|
500
|
+
schema_version,
|
|
501
|
+
meta_json,
|
|
502
|
+
message_count,
|
|
503
|
+
last_user_message
|
|
504
|
+
)
|
|
505
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
506
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
507
|
+
updated_at = excluded.updated_at,
|
|
508
|
+
run_id = excluded.run_id,
|
|
509
|
+
invoke_seq = excluded.invoke_seq,
|
|
510
|
+
schema_version = excluded.schema_version,
|
|
511
|
+
meta_json = excluded.meta_json,
|
|
512
|
+
message_count = excluded.message_count,
|
|
513
|
+
last_user_message = excluded.last_user_message`,
|
|
514
|
+
[
|
|
515
|
+
state.session_id,
|
|
516
|
+
state.updated_at,
|
|
517
|
+
state.run_id ?? null,
|
|
518
|
+
state.invoke_seq ?? null,
|
|
519
|
+
state.schema_version ?? 1,
|
|
520
|
+
state.meta ? JSON.stringify(state.meta) : null,
|
|
521
|
+
summary.message_count ?? null,
|
|
522
|
+
summary.last_user_message ?? null
|
|
523
|
+
]
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
async load(sessionId) {
|
|
527
|
+
const db = await this.requireDb("load", sessionId);
|
|
528
|
+
try {
|
|
529
|
+
const indexed = await this.loadFromIndex(sessionId, db);
|
|
530
|
+
if (indexed) return indexed;
|
|
531
|
+
} catch (error) {
|
|
532
|
+
this.onError?.(error, { action: "load.index", detail: sessionId });
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
const legacy = await this.loadLegacy(sessionId);
|
|
536
|
+
if (!legacy) return null;
|
|
537
|
+
if (db) {
|
|
538
|
+
try {
|
|
539
|
+
await this.saveToIndex(legacy, db);
|
|
540
|
+
} catch (error) {
|
|
541
|
+
this.onError?.(error, {
|
|
542
|
+
action: "load.migrate_legacy",
|
|
543
|
+
detail: sessionId
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return legacy;
|
|
548
|
+
}
|
|
264
549
|
async save(state) {
|
|
265
|
-
await this.
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
550
|
+
await this.ensureDirs;
|
|
551
|
+
const db = await this.requireDb("save", state.session_id);
|
|
552
|
+
try {
|
|
553
|
+
await this.saveToIndex(state, db);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
this.onError?.(error, { action: "save.index", detail: state.session_id });
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
269
558
|
}
|
|
270
|
-
async
|
|
271
|
-
await this.ensureDir;
|
|
559
|
+
async listLegacySummaries() {
|
|
272
560
|
let entries;
|
|
273
561
|
try {
|
|
274
|
-
entries = await fs3.readdir(this.
|
|
562
|
+
entries = await fs3.readdir(this.legacyStateDir, {
|
|
275
563
|
withFileTypes: true,
|
|
276
564
|
encoding: "utf8"
|
|
277
565
|
});
|
|
278
566
|
} catch (error) {
|
|
279
|
-
this.onError?.(error, { action: "list" });
|
|
567
|
+
this.onError?.(error, { action: "legacy.list" });
|
|
280
568
|
throw error;
|
|
281
569
|
}
|
|
282
570
|
const summaries = [];
|
|
283
571
|
for (const entry of entries) {
|
|
284
572
|
if (!entry.isFile()) continue;
|
|
285
573
|
if (!entry.name.endsWith(".json")) continue;
|
|
286
|
-
const filePath = path3.join(this.
|
|
574
|
+
const filePath = path3.join(this.legacyStateDir, entry.name);
|
|
287
575
|
try {
|
|
288
576
|
const contents = await fs3.readFile(filePath, "utf8");
|
|
289
577
|
const parsed = JSON.parse(contents);
|
|
290
578
|
if (!parsed || typeof parsed !== "object") continue;
|
|
291
579
|
if (!parsed.session_id || !parsed.updated_at) continue;
|
|
292
|
-
summaries.push(
|
|
580
|
+
summaries.push(
|
|
581
|
+
toSummary({
|
|
582
|
+
...parsed,
|
|
583
|
+
schema_version: typeof parsed.schema_version === "number" ? parsed.schema_version : 1,
|
|
584
|
+
messages: Array.isArray(parsed.messages) ? parsed.messages : []
|
|
585
|
+
})
|
|
586
|
+
);
|
|
293
587
|
} catch (error) {
|
|
294
|
-
this.onError?.(error, { action: "parse", detail: filePath });
|
|
588
|
+
this.onError?.(error, { action: "legacy.parse", detail: filePath });
|
|
295
589
|
}
|
|
296
590
|
}
|
|
297
591
|
return summaries;
|
|
298
592
|
}
|
|
593
|
+
async list() {
|
|
594
|
+
await this.ensureDirs;
|
|
595
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
596
|
+
const db = await this.requireDb("list");
|
|
597
|
+
try {
|
|
598
|
+
const rows = db.all(
|
|
599
|
+
`SELECT session_id, updated_at, run_id, message_count, last_user_message
|
|
600
|
+
FROM session_state
|
|
601
|
+
ORDER BY updated_at DESC`
|
|
602
|
+
);
|
|
603
|
+
for (const row of rows) {
|
|
604
|
+
summaries.set(row.session_id, fromSummaryRow(row));
|
|
605
|
+
}
|
|
606
|
+
} catch (error) {
|
|
607
|
+
this.onError?.(error, { action: "list.index" });
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
const legacy = await this.listLegacySummaries();
|
|
611
|
+
for (const item of legacy) {
|
|
612
|
+
if (!summaries.has(item.session_id)) {
|
|
613
|
+
summaries.set(item.session_id, item);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return [...summaries.values()];
|
|
617
|
+
}
|
|
299
618
|
};
|
|
300
619
|
|
|
301
620
|
// src/tool-output-cache.ts
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codelia/storage",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=20 <25"
|
|
7
|
+
},
|
|
5
8
|
"files": [
|
|
6
9
|
"dist"
|
|
7
10
|
],
|
|
@@ -20,7 +23,8 @@
|
|
|
20
23
|
"typecheck": "tsc --noEmit"
|
|
21
24
|
},
|
|
22
25
|
"dependencies": {
|
|
23
|
-
"@codelia/core": "0.1.
|
|
26
|
+
"@codelia/core": "0.1.10",
|
|
27
|
+
"better-sqlite3": "^12.6.2"
|
|
24
28
|
},
|
|
25
29
|
"publishConfig": {
|
|
26
30
|
"access": "public"
|