antechamber 1.0.40 → 2.0.1
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/CHANGELOG.md +126 -0
- package/README.md +88 -141
- package/bin/opencode-antechamber +14 -4
- package/dist/approvals.d.ts +9 -1
- package/dist/approvals.js +10 -2
- package/dist/catalog.d.ts +13 -2
- package/dist/catalog.js +88 -52
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/commands/context.d.ts +5 -2
- package/dist/commands/context.js +8 -4
- package/dist/commands/doctor.js +4 -3
- package/dist/commands/setup.js +9 -28
- package/dist/config.d.ts +6 -2
- package/dist/config.js +18 -4
- package/dist/file-watch.d.ts +35 -0
- package/dist/file-watch.js +163 -0
- package/dist/latency.d.ts +10 -0
- package/dist/latency.js +115 -0
- package/dist/misc-routes.js +14 -12
- package/dist/pi-event-translation.js +44 -2
- package/dist/route-services.d.ts +8 -3
- package/dist/routes-v1.d.ts +1 -1
- package/dist/routes-v1.js +29 -418
- package/dist/routes-v2.js +252 -34
- package/dist/self-update.d.ts +2 -1
- package/dist/self-update.js +5 -1
- package/dist/server.d.ts +17 -3
- package/dist/server.js +191 -18
- package/dist/session-commands.d.ts +3 -1
- package/dist/session-commands.js +13 -7
- package/dist/session-history.js +3 -0
- package/dist/session-store.d.ts +19 -10
- package/dist/session-store.js +266 -128
- package/dist/session-tree.d.ts +15 -1
- package/dist/session-tree.js +2 -2
- package/dist/sessions.d.ts +85 -1
- package/dist/sessions.js +238 -10
- package/dist/sse.d.ts +0 -2
- package/dist/sse.js +13 -46
- package/dist/text-generation.d.ts +20 -0
- package/dist/text-generation.js +93 -0
- package/dist/tool-map.js +1 -3
- package/dist/types.d.ts +60 -0
- package/dist/v2.d.ts +8 -1
- package/dist/v2.js +24 -4
- package/dist/work-status.js +1 -1
- package/extensions/antechamber/README.md +100 -0
- package/extensions/antechamber/bin/opencode-antechamber +42 -0
- package/extensions/{omo-dag/docs/DESIGN.md → antechamber/docs/GRAPH.md} +5 -5
- package/extensions/{omo-work/docs/DESIGN.md → antechamber/docs/WORK.md} +3 -3
- package/extensions/antechamber/package.json +60 -0
- package/extensions/{omo-dag → antechamber}/panel/index.html +8 -1
- package/extensions/antechamber/panel/main.js +261 -0
- package/extensions/antechamber/runtime/launcher.js +201 -0
- package/extensions/antechamber/service/main.js +884 -0
- package/package.json +75 -26
- package/extensions/omo-compat/README.md +0 -48
- package/extensions/omo-compat/icon.svg +0 -1
- package/extensions/omo-compat/package.json +0 -43
- package/extensions/omo-dag/README.md +0 -167
- package/extensions/omo-dag/package.json +0 -33
- package/extensions/omo-dag/panel/main.js +0 -176
- package/extensions/omo-dag/service/main.js +0 -457
- package/extensions/omo-work/README.md +0 -88
- package/extensions/omo-work/package.json +0 -30
- package/extensions/omo-work/service/main.js +0 -149
- /package/extensions/{omo-dag → antechamber}/icon.svg +0 -0
- /package/extensions/{omo-work → antechamber}/status/index.html +0 -0
- /package/extensions/{omo-work → antechamber}/status/main.js +0 -0
package/dist/session-store.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* so the adapter never needs its own transcript store — only:
|
|
7
7
|
*
|
|
8
8
|
* 1. a scan of that directory to rebuild the sidebar after a restart, and
|
|
9
|
-
* 2. a
|
|
9
|
+
* 2. a SQLite registry for adapter-owned metadata that pi does not
|
|
10
10
|
* keep (titles, conversation cursors and child-task metadata).
|
|
11
11
|
*
|
|
12
12
|
* OpenChamber session ids for restored sessions are derived deterministically
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* the same OpenChamber id, even if the registry file is lost.
|
|
15
15
|
*/
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
|
-
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync,
|
|
17
|
+
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, } from "node:fs";
|
|
18
18
|
import { join } from "node:path";
|
|
19
|
+
import { DatabaseSync } from "node:sqlite";
|
|
19
20
|
import { piAgentDir } from "./agent-dir.js";
|
|
20
21
|
import { isPromptPart } from "./prompt-parts.js";
|
|
21
22
|
import { defaultStateDir, envSetting } from "./config.js";
|
|
@@ -42,9 +43,6 @@ const TITLE_MAX_LINES = 200;
|
|
|
42
43
|
const TITLE_MAX_CHARS = 60;
|
|
43
44
|
const REGISTRY_DIRECTORY_MODE = 0o700;
|
|
44
45
|
const REGISTRY_FILE_MODE = 0o600;
|
|
45
|
-
const REGISTRY_LOCK_RETRY_MS = 10;
|
|
46
|
-
const REGISTRY_LOCK_STALE_MS = 5_000;
|
|
47
|
-
const registryLockWaiter = new Int32Array(new SharedArrayBuffer(4));
|
|
48
46
|
function readHead(path, bytes = SNIFF_BYTES) {
|
|
49
47
|
const fd = openSync(path, "r");
|
|
50
48
|
try {
|
|
@@ -359,7 +357,7 @@ export function scanPiSessions(directory) {
|
|
|
359
357
|
}
|
|
360
358
|
/**
|
|
361
359
|
* Persistent oc-session-id → metadata registry (title overrides, known pi
|
|
362
|
-
* paths).
|
|
360
|
+
* paths). Legacy JSON records are validated at migration; SQLite writes are atomic.
|
|
363
361
|
*/
|
|
364
362
|
function record(value) {
|
|
365
363
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -387,12 +385,31 @@ function sessionTreeState(value) {
|
|
|
387
385
|
if (value.promptMetadata !== undefined &&
|
|
388
386
|
(!record(value.promptMetadata) || !Object.values(value.promptMetadata).every((metadata) => record(metadata))))
|
|
389
387
|
return false;
|
|
388
|
+
if (value.promptDescriptions !== undefined && !stringRecord(value.promptDescriptions))
|
|
389
|
+
return false;
|
|
390
|
+
if (value.compactionReasons !== undefined &&
|
|
391
|
+
(!stringRecord(value.compactionReasons) ||
|
|
392
|
+
!Object.values(value.compactionReasons).every((reason) => reason === "manual" || reason === "auto")))
|
|
393
|
+
return false;
|
|
390
394
|
if (value.messageParts !== undefined &&
|
|
391
395
|
(!record(value.messageParts) ||
|
|
392
396
|
!Object.values(value.messageParts).every((parts) => Array.isArray(parts) && parts.every((part) => typeof part === "string"))))
|
|
393
397
|
return false;
|
|
394
398
|
if (value.shellDispatch !== undefined && !booleanRecord(value.shellDispatch))
|
|
395
399
|
return false;
|
|
400
|
+
const finite = (item) => typeof item === "number" && Number.isFinite(item);
|
|
401
|
+
if (value.lastIdle !== undefined && !finite(value.lastIdle))
|
|
402
|
+
return false;
|
|
403
|
+
if (value.compactionAdmissions !== undefined &&
|
|
404
|
+
(!record(value.compactionAdmissions) || !Object.values(value.compactionAdmissions).every(finite)))
|
|
405
|
+
return false;
|
|
406
|
+
if (value.idleBoundaries !== undefined &&
|
|
407
|
+
(!record(value.idleBoundaries) ||
|
|
408
|
+
!Object.values(value.idleBoundaries).every((boundary) => record(boundary) &&
|
|
409
|
+
finite(boundary.at) &&
|
|
410
|
+
["succeeded", "failed", "interrupted"].includes(boundary.outcome) &&
|
|
411
|
+
(boundary.error === undefined || record(boundary.error)))))
|
|
412
|
+
return false;
|
|
396
413
|
if (value.leafId !== undefined && value.leafId !== null && typeof value.leafId !== "string")
|
|
397
414
|
return false;
|
|
398
415
|
if (value.fileSize !== undefined && (typeof value.fileSize !== "number" || !Number.isFinite(value.fileSize)))
|
|
@@ -448,6 +465,13 @@ function registryEntry(value) {
|
|
|
448
465
|
return false;
|
|
449
466
|
if (value.metadata !== undefined && !record(value.metadata))
|
|
450
467
|
return false;
|
|
468
|
+
if (value.instructions !== undefined &&
|
|
469
|
+
(!record(value.instructions) ||
|
|
470
|
+
!Object.entries(value.instructions).every(([key, entry]) => /^[a-z0-9][a-z0-9._-]*$/.test(key) &&
|
|
471
|
+
record(entry) &&
|
|
472
|
+
"value" in entry &&
|
|
473
|
+
(entry.removed === undefined || entry.removed === true))))
|
|
474
|
+
return false;
|
|
451
475
|
if (value.task !== undefined && !childTask(value.task))
|
|
452
476
|
return false;
|
|
453
477
|
if (value.title !== undefined && typeof value.title !== "string")
|
|
@@ -456,6 +480,8 @@ function registryEntry(value) {
|
|
|
456
480
|
return false;
|
|
457
481
|
if (value.archived !== undefined && (typeof value.archived !== "number" || !Number.isFinite(value.archived)))
|
|
458
482
|
return false;
|
|
483
|
+
if (value.viewed !== undefined && (typeof value.viewed !== "number" || !Number.isFinite(value.viewed)))
|
|
484
|
+
return false;
|
|
459
485
|
if (value.model !== undefined &&
|
|
460
486
|
(!record(value.model) || typeof value.model.providerID !== "string" || typeof value.model.modelID !== "string"))
|
|
461
487
|
return false;
|
|
@@ -484,140 +510,231 @@ function errorCode(error) {
|
|
|
484
510
|
export class SessionStore {
|
|
485
511
|
stateDir;
|
|
486
512
|
file;
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
513
|
+
db;
|
|
514
|
+
version;
|
|
515
|
+
entries = new Map();
|
|
490
516
|
batchState;
|
|
491
517
|
constructor(stateDir) {
|
|
492
518
|
const dir = stateDir ?? envSetting("STATE_DIR") ?? defaultStateDir();
|
|
493
519
|
this.stateDir = dir;
|
|
494
|
-
this.file = join(dir, "sessions.
|
|
495
|
-
}
|
|
496
|
-
load() {
|
|
497
|
-
if (this.batchState !== undefined) {
|
|
498
|
-
// Shallow: changes replace whole entries, and get() clones an entry before handing it out.
|
|
499
|
-
this.batchState.entries ??= new Map(this.committed());
|
|
500
|
-
return this.batchState.entries;
|
|
501
|
-
}
|
|
502
|
-
return this.committed();
|
|
503
|
-
}
|
|
504
|
-
committed() {
|
|
505
|
-
if (this.entries !== undefined)
|
|
506
|
-
return this.entries;
|
|
507
|
-
this.raw = this.readRaw();
|
|
508
|
-
this.entries = this.parseEntries(this.raw);
|
|
509
|
-
return this.entries;
|
|
520
|
+
this.file = join(dir, "sessions.sqlite");
|
|
510
521
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
return
|
|
514
|
-
|
|
515
|
-
|
|
522
|
+
database(write = false) {
|
|
523
|
+
if (this.db !== undefined)
|
|
524
|
+
return this.db;
|
|
525
|
+
const legacy = join(this.stateDir, "sessions.json");
|
|
526
|
+
if (!write && !existsSync(this.file) && !existsSync(legacy))
|
|
516
527
|
return undefined;
|
|
517
|
-
|
|
518
|
-
}
|
|
519
|
-
parseEntries(raw) {
|
|
520
|
-
const entries = new Map();
|
|
521
|
-
if (raw === undefined)
|
|
522
|
-
return entries;
|
|
528
|
+
let db;
|
|
523
529
|
try {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
+
mkdirSync(this.stateDir, { recursive: true, mode: REGISTRY_DIRECTORY_MODE });
|
|
531
|
+
chmodSync(this.stateDir, REGISTRY_DIRECTORY_MODE);
|
|
532
|
+
// SQLite inherits the database mode for WAL/SHM; set it before enabling WAL.
|
|
533
|
+
closeSync(openSync(this.file, "a", REGISTRY_FILE_MODE));
|
|
534
|
+
chmodSync(this.file, REGISTRY_FILE_MODE);
|
|
535
|
+
db = new DatabaseSync(this.file);
|
|
536
|
+
db.exec("PRAGMA busy_timeout = 5000; PRAGMA journal_mode = WAL;");
|
|
537
|
+
db.exec(`CREATE TABLE IF NOT EXISTS sessions (
|
|
538
|
+
id TEXT PRIMARY KEY, payload TEXT NOT NULL, pi_session_id TEXT NOT NULL,
|
|
539
|
+
directory TEXT NOT NULL, parent_id TEXT, has_task INTEGER NOT NULL
|
|
540
|
+
);
|
|
541
|
+
CREATE INDEX IF NOT EXISTS sessions_pi ON sessions(pi_session_id);
|
|
542
|
+
CREATE INDEX IF NOT EXISTS sessions_directory ON sessions(directory);
|
|
543
|
+
CREATE INDEX IF NOT EXISTS sessions_children ON sessions(parent_id) WHERE has_task = 1;
|
|
544
|
+
CREATE TABLE IF NOT EXISTS registry_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);`);
|
|
545
|
+
this.migrate(db, legacy);
|
|
546
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
547
|
+
const path = this.file + suffix;
|
|
548
|
+
if (existsSync(path))
|
|
549
|
+
chmodSync(path, REGISTRY_FILE_MODE);
|
|
550
|
+
}
|
|
551
|
+
this.db = db;
|
|
552
|
+
return db;
|
|
530
553
|
}
|
|
531
|
-
catch {
|
|
532
|
-
|
|
554
|
+
catch (error) {
|
|
555
|
+
db?.close();
|
|
556
|
+
throw new SessionPersistenceError(this.file, error);
|
|
533
557
|
}
|
|
534
|
-
return entries;
|
|
535
558
|
}
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
559
|
+
migrate(db, legacy) {
|
|
560
|
+
const meta = (key) => db.prepare("SELECT value FROM registry_meta WHERE key = ?").get(key)?.value;
|
|
561
|
+
const setMeta = (key, value) => db
|
|
562
|
+
.prepare("INSERT INTO registry_meta VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value")
|
|
563
|
+
.run(key, value);
|
|
564
|
+
db.prepare("INSERT OR IGNORE INTO registry_meta VALUES ('created_at', ?)").run(String(Date.now()));
|
|
565
|
+
if (!existsSync(legacy))
|
|
566
|
+
return;
|
|
567
|
+
db.exec("BEGIN IMMEDIATE");
|
|
568
|
+
let backup;
|
|
569
|
+
let merged = 0;
|
|
539
570
|
try {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
for (;;) {
|
|
571
|
+
const imported = meta("legacy_backup");
|
|
572
|
+
const parse = () => {
|
|
543
573
|
try {
|
|
544
|
-
|
|
545
|
-
|
|
574
|
+
const parsed = JSON.parse(readFileSync(legacy, "utf8"));
|
|
575
|
+
return record(parsed) ? parsed : undefined;
|
|
546
576
|
}
|
|
547
577
|
catch (error) {
|
|
548
|
-
if (
|
|
578
|
+
if (!(error instanceof SyntaxError))
|
|
549
579
|
throw error;
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
580
|
+
// Preserve malformed JSON untouched; a later repair can still be imported.
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
if (typeof imported === "string" && !existsSync(imported))
|
|
585
|
+
backup = imported;
|
|
586
|
+
else if (imported === undefined && db.prepare("SELECT 1 FROM sessions LIMIT 1").get() === undefined) {
|
|
587
|
+
const parsed = parse();
|
|
588
|
+
if (parsed !== undefined) {
|
|
589
|
+
const upsert = this.upsert(db);
|
|
590
|
+
for (const [id, entry] of Object.entries(parsed))
|
|
591
|
+
if (registryEntry(entry))
|
|
592
|
+
this.writeRow(upsert, id, entry);
|
|
593
|
+
backup = `${legacy}.migrated-${Date.now()}-${process.pid}`;
|
|
594
|
+
setMeta("legacy_backup", backup);
|
|
595
|
+
setMeta("reconciled_at", String(Date.now()));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
// A sessions.json next to a populated registry was written by an older antechamber after the migration (a
|
|
600
|
+
// rollback). Sessions it changed since then are newer than the rows here; older entries, including ones
|
|
601
|
+
// deleted since, stay as the registry has them.
|
|
602
|
+
const since = Number(meta("reconciled_at") ?? /\.migrated-(\d+)-/.exec(String(imported))?.[1] ?? meta("created_at") ?? Infinity);
|
|
603
|
+
const parsed = parse();
|
|
604
|
+
if (parsed !== undefined) {
|
|
605
|
+
const upsert = this.upsert(db);
|
|
606
|
+
for (const [id, entry] of Object.entries(parsed)) {
|
|
607
|
+
if (!registryEntry(entry) || !(entry.updated > since))
|
|
608
|
+
continue;
|
|
609
|
+
const current = this.read(db, id);
|
|
610
|
+
if (current !== undefined && current.updated >= entry.updated)
|
|
553
611
|
continue;
|
|
554
|
-
|
|
612
|
+
this.writeRow(upsert, id, entry);
|
|
613
|
+
merged += 1;
|
|
555
614
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
continue;
|
|
615
|
+
if (merged > 0) {
|
|
616
|
+
backup = `${legacy}.merged-${Date.now()}-${process.pid}`;
|
|
617
|
+
setMeta("reconciled_at", String(Date.now()));
|
|
560
618
|
}
|
|
561
|
-
Atomics.wait(registryLockWaiter, 0, 0, REGISTRY_LOCK_RETRY_MS);
|
|
562
619
|
}
|
|
563
620
|
}
|
|
564
|
-
|
|
621
|
+
db.exec("COMMIT");
|
|
565
622
|
}
|
|
566
623
|
catch (error) {
|
|
567
|
-
|
|
624
|
+
db.exec("ROLLBACK");
|
|
625
|
+
throw error;
|
|
568
626
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
627
|
+
if (merged > 0)
|
|
628
|
+
this.entries.clear();
|
|
629
|
+
if (merged > 0)
|
|
630
|
+
console.warn(`[session-store] merged ${merged} sessions an older antechamber wrote to ${legacy}`);
|
|
631
|
+
// The durable marker handles a crash after COMMIT but before rename, including an empty import.
|
|
632
|
+
// Never overwrite an existing backup or import the same legacy file after sessions are deleted.
|
|
633
|
+
if (backup !== undefined && !existsSync(backup)) {
|
|
634
|
+
try {
|
|
635
|
+
chmodSync(legacy, REGISTRY_FILE_MODE);
|
|
636
|
+
renameSync(legacy, backup);
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
if (errorCode(error) !== "ENOENT")
|
|
640
|
+
throw error;
|
|
573
641
|
}
|
|
574
642
|
}
|
|
575
643
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
644
|
+
upsert(db) {
|
|
645
|
+
return db.prepare(`INSERT INTO sessions (id, payload, pi_session_id, directory, parent_id, has_task)
|
|
646
|
+
VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET
|
|
647
|
+
payload=excluded.payload, pi_session_id=excluded.pi_session_id, directory=excluded.directory,
|
|
648
|
+
parent_id=excluded.parent_id, has_task=excluded.has_task`);
|
|
649
|
+
}
|
|
650
|
+
writeRow(statement, id, entry) {
|
|
651
|
+
statement.run(id, JSON.stringify(persistedEntry(entry)), entry.piSessionId, entry.directory, entry.parentID ?? null, entry.task === undefined ? 0 : 1);
|
|
652
|
+
}
|
|
653
|
+
refresh(db) {
|
|
654
|
+
const version = db.prepare("PRAGMA data_version").get()?.data_version;
|
|
655
|
+
if (typeof version !== "number" && typeof version !== "bigint")
|
|
656
|
+
throw new Error("Invalid SQLite data_version");
|
|
657
|
+
if (this.version !== version) {
|
|
658
|
+
this.entries.clear();
|
|
659
|
+
this.version = version;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
read(db, id) {
|
|
663
|
+
const cached = this.entries.get(id);
|
|
664
|
+
if (cached !== undefined)
|
|
665
|
+
return cached;
|
|
666
|
+
const payload = db.prepare("SELECT payload FROM sessions WHERE id = ?").get(id)?.payload;
|
|
667
|
+
if (typeof payload !== "string")
|
|
668
|
+
return undefined;
|
|
669
|
+
const entry = JSON.parse(payload);
|
|
670
|
+
if (!registryEntry(entry))
|
|
671
|
+
return undefined;
|
|
672
|
+
this.entries.set(id, entry);
|
|
673
|
+
return entry;
|
|
674
|
+
}
|
|
675
|
+
persist(changes) {
|
|
676
|
+
try {
|
|
677
|
+
mkdirSync(this.stateDir, { recursive: true, mode: REGISTRY_DIRECTORY_MODE });
|
|
678
|
+
chmodSync(this.stateDir, REGISTRY_DIRECTORY_MODE);
|
|
679
|
+
const db = this.database(true);
|
|
680
|
+
db.exec("BEGIN IMMEDIATE");
|
|
681
|
+
const changed = new Map();
|
|
682
|
+
try {
|
|
683
|
+
this.refresh(db);
|
|
684
|
+
for (const { id, apply } of changes) {
|
|
685
|
+
const previous = changed.has(id) ? changed.get(id) : this.read(db, id);
|
|
686
|
+
const next = apply(previous);
|
|
687
|
+
if (next !== undefined || previous !== undefined)
|
|
688
|
+
changed.set(id, next);
|
|
689
|
+
}
|
|
690
|
+
const upsert = this.upsert(db);
|
|
691
|
+
const remove = db.prepare("DELETE FROM sessions WHERE id = ?");
|
|
692
|
+
for (const [id, entry] of changed) {
|
|
693
|
+
if (entry === undefined)
|
|
694
|
+
remove.run(id);
|
|
695
|
+
else
|
|
696
|
+
this.writeRow(upsert, id, entry);
|
|
697
|
+
}
|
|
698
|
+
db.exec("COMMIT");
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
db.exec("ROLLBACK");
|
|
702
|
+
throw error;
|
|
703
|
+
}
|
|
704
|
+
for (const [id, entry] of changed) {
|
|
705
|
+
if (entry === undefined)
|
|
706
|
+
this.entries.delete(id);
|
|
707
|
+
else
|
|
708
|
+
this.entries.set(id, entry);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
if (error instanceof SessionPersistenceError)
|
|
713
|
+
throw error;
|
|
714
|
+
throw new SessionPersistenceError(this.file, error);
|
|
590
715
|
}
|
|
591
|
-
this.withRegistryLock(() => {
|
|
592
|
-
// Another process may have written since; only parse again when the bytes differ.
|
|
593
|
-
const raw = this.readRaw();
|
|
594
|
-
const entries = this.entries !== undefined && raw !== undefined && this.raw !== undefined && raw.equals(this.raw)
|
|
595
|
-
? new Map(this.entries)
|
|
596
|
-
: this.parseEntries(raw);
|
|
597
|
-
const written = change(entries) ? this.persist(entries) : raw;
|
|
598
|
-
this.entries = entries;
|
|
599
|
-
this.raw = written;
|
|
600
|
-
});
|
|
601
716
|
}
|
|
602
|
-
|
|
717
|
+
mutate(id, apply) {
|
|
718
|
+
const batch = this.batchState;
|
|
719
|
+
if (batch === undefined)
|
|
720
|
+
this.persist([{ id, apply }]);
|
|
721
|
+
else {
|
|
722
|
+
const previous = this.get(id);
|
|
723
|
+
batch.changes.push({ id, apply });
|
|
724
|
+
batch.entries.set(id, apply(previous));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/** Stage only touched rows; replay partial updates against the latest rows under SQLite's write lock. */
|
|
603
728
|
batch(action) {
|
|
604
729
|
if (this.batchState !== undefined)
|
|
605
730
|
return action();
|
|
606
|
-
const
|
|
607
|
-
const batch = { entries: undefined, isolated: new WeakSet(), changes };
|
|
731
|
+
const batch = { entries: new Map(), changes: [] };
|
|
608
732
|
this.batchState = batch;
|
|
609
733
|
try {
|
|
610
734
|
const result = action();
|
|
611
735
|
this.batchState = undefined;
|
|
612
|
-
if (batch.changes.length !== 0)
|
|
613
|
-
this.
|
|
614
|
-
let changed = false;
|
|
615
|
-
for (const change of batch.changes)
|
|
616
|
-
if (change(entries))
|
|
617
|
-
changed = true;
|
|
618
|
-
return changed;
|
|
619
|
-
});
|
|
620
|
-
}
|
|
736
|
+
if (batch.changes.length !== 0)
|
|
737
|
+
this.persist(batch.changes);
|
|
621
738
|
return result;
|
|
622
739
|
}
|
|
623
740
|
finally {
|
|
@@ -625,49 +742,70 @@ export class SessionStore {
|
|
|
625
742
|
}
|
|
626
743
|
}
|
|
627
744
|
get(ocSessionId) {
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
745
|
+
const batch = this.batchState;
|
|
746
|
+
if (batch?.entries.has(ocSessionId))
|
|
747
|
+
return batch.entries.get(ocSessionId);
|
|
748
|
+
const db = this.database();
|
|
749
|
+
if (db === undefined)
|
|
750
|
+
return undefined;
|
|
751
|
+
this.refresh(db);
|
|
752
|
+
const entry = this.read(db, ocSessionId);
|
|
753
|
+
if (batch === undefined)
|
|
631
754
|
return entry;
|
|
632
|
-
const staged = structuredClone(entry);
|
|
633
|
-
entries.set(ocSessionId, staged);
|
|
634
|
-
this.batchState.isolated.add(staged);
|
|
755
|
+
const staged = entry === undefined ? undefined : structuredClone(entry);
|
|
756
|
+
batch.entries.set(ocSessionId, staged);
|
|
635
757
|
return staged;
|
|
636
758
|
}
|
|
637
759
|
idFor(piSessionId) {
|
|
638
|
-
return
|
|
760
|
+
return this.matching("SELECT id FROM sessions WHERE pi_session_id = ? ORDER BY rowid", [piSessionId], (entry) => entry.piSessionId === piSessionId)[0];
|
|
639
761
|
}
|
|
640
762
|
childIds() {
|
|
641
|
-
return
|
|
642
|
-
|
|
643
|
-
|
|
763
|
+
return this.matching("SELECT id FROM sessions WHERE has_task = 1 AND parent_id IS NOT NULL ORDER BY rowid", [], (entry) => entry.task !== undefined && entry.parentID !== undefined);
|
|
764
|
+
}
|
|
765
|
+
matching(query, parameters, matches) {
|
|
766
|
+
const db = this.database();
|
|
767
|
+
if (db !== undefined)
|
|
768
|
+
this.refresh(db);
|
|
769
|
+
const ids = new Set();
|
|
770
|
+
for (const row of db?.prepare(query).all(...parameters) ?? [])
|
|
771
|
+
if (typeof row.id === "string")
|
|
772
|
+
ids.add(row.id);
|
|
773
|
+
if (this.batchState === undefined)
|
|
774
|
+
return [...ids];
|
|
775
|
+
for (const id of this.batchState?.entries.keys() ?? [])
|
|
776
|
+
ids.add(id);
|
|
777
|
+
return [...ids].filter((id) => {
|
|
778
|
+
const entry = this.get(id);
|
|
779
|
+
return entry !== undefined && matches(entry);
|
|
780
|
+
});
|
|
644
781
|
}
|
|
645
782
|
set(ocSessionId, entry) {
|
|
646
783
|
const saved = this.batchState === undefined ? entry : structuredClone(entry);
|
|
647
|
-
this.mutate((
|
|
648
|
-
entries.set(ocSessionId, this.batchState === undefined ? saved : structuredClone(saved));
|
|
649
|
-
return true;
|
|
650
|
-
});
|
|
784
|
+
this.mutate(ocSessionId, () => (this.batchState === undefined ? saved : structuredClone(saved)));
|
|
651
785
|
}
|
|
652
786
|
/** Merge a partial update (e.g. title override, updated timestamp). */
|
|
653
787
|
update(ocSessionId, patch) {
|
|
654
788
|
const saved = this.batchState === undefined ? patch : structuredClone(patch);
|
|
655
|
-
this.mutate((
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
return false;
|
|
659
|
-
entries.set(ocSessionId, {
|
|
789
|
+
this.mutate(ocSessionId, (previous) => previous === undefined
|
|
790
|
+
? undefined
|
|
791
|
+
: {
|
|
660
792
|
...previous,
|
|
661
793
|
...(this.batchState === undefined ? saved : structuredClone(saved)),
|
|
662
794
|
});
|
|
663
|
-
return true;
|
|
664
|
-
});
|
|
665
795
|
}
|
|
666
796
|
remove(ocSessionId) {
|
|
667
|
-
this.mutate((
|
|
797
|
+
this.mutate(ocSessionId, () => undefined);
|
|
668
798
|
}
|
|
669
799
|
/** Every directory the registry knows about — used to resolve bare session ids. */
|
|
670
800
|
directories() {
|
|
671
|
-
|
|
801
|
+
const db = this.database();
|
|
802
|
+
const directories = new Set();
|
|
803
|
+
for (const row of db?.prepare("SELECT DISTINCT directory FROM sessions").all() ?? [])
|
|
804
|
+
if (typeof row.directory === "string")
|
|
805
|
+
directories.add(row.directory);
|
|
806
|
+
if (this.batchState === undefined)
|
|
807
|
+
return [...directories];
|
|
808
|
+
const ids = this.matching("SELECT id FROM sessions", [], () => true);
|
|
809
|
+
return [...new Set(ids.map((id) => this.get(id).directory))];
|
|
672
810
|
}
|
|
673
811
|
}
|
package/dist/session-tree.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type PiRpcClient } from "./pi-rpc.js";
|
|
2
2
|
import type { ManagedSession } from "./sessions.js";
|
|
3
|
-
import type { OCMessageWithParts } from "./types.js";
|
|
3
|
+
import type { OCAssistantMessage, OCMessageWithParts, OCSession } from "./types.js";
|
|
4
4
|
import type { OCPromptPart } from "./types.js";
|
|
5
5
|
export interface SessionTreeState {
|
|
6
6
|
messageEntries: Record<string, string>;
|
|
@@ -10,6 +10,10 @@ export interface SessionTreeState {
|
|
|
10
10
|
promptParts?: Record<string, OCPromptPart[]>;
|
|
11
11
|
/** Client prompt metadata; native RPC does not store it. */
|
|
12
12
|
promptMetadata?: Record<string, Record<string, unknown>>;
|
|
13
|
+
/** Client-provided synthetic notice labels; native RPC stores only the context text. */
|
|
14
|
+
promptDescriptions?: Record<string, string>;
|
|
15
|
+
/** Native compaction reasons by the durable compaction entry ID. */
|
|
16
|
+
compactionReasons?: Record<string, "manual" | "auto">;
|
|
13
17
|
/** Actual transport dispatch, not inferred from a card's terminal state. */
|
|
14
18
|
shellDispatch?: Record<string, boolean>;
|
|
15
19
|
/** Identity-only dispatched admissions; never rendered without a native entry. */
|
|
@@ -25,6 +29,16 @@ export interface SessionTreeState {
|
|
|
25
29
|
previousLeafId: string | null;
|
|
26
30
|
targetId: string | null;
|
|
27
31
|
};
|
|
32
|
+
/** Execution boundaries observed over RPC, retained with the durable message identities. */
|
|
33
|
+
idleBoundaries?: Record<string, {
|
|
34
|
+
at: number;
|
|
35
|
+
outcome: NonNullable<OCSession["outcome"]>;
|
|
36
|
+
error?: NonNullable<OCAssistantMessage["error"]>;
|
|
37
|
+
}>;
|
|
38
|
+
/** The last idle time, restored as time.idle so /view accepts a watermark seen before a restart. */
|
|
39
|
+
lastIdle?: number;
|
|
40
|
+
/** When each compaction was admitted, keyed by its admitted message ID. */
|
|
41
|
+
compactionAdmissions?: Record<string, number>;
|
|
28
42
|
}
|
|
29
43
|
export interface TreeEntry {
|
|
30
44
|
type: string;
|
package/dist/session-tree.js
CHANGED
|
@@ -332,8 +332,8 @@ function entryForMessage(s, tree, messageID) {
|
|
|
332
332
|
}
|
|
333
333
|
/**
|
|
334
334
|
* The last entry of an assistant reply: the reply, or the latest result of its own tool calls.
|
|
335
|
-
*
|
|
336
|
-
*
|
|
335
|
+
* A final reply has no tool calls. When an API caller selects a tool-bearing
|
|
336
|
+
* reply, its completed tool results belong to the same message.
|
|
337
337
|
*/
|
|
338
338
|
function replyEnd(tree, reply, leafId) {
|
|
339
339
|
const content = reply.message?.content;
|