antechamber 1.0.41 → 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 +97 -0
- package/README.md +88 -141
- package/bin/opencode-antechamber +14 -4
- 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/latency.d.ts +10 -0
- package/dist/latency.js +115 -0
- package/dist/misc-routes.js +2 -10
- package/dist/route-services.d.ts +0 -2
- package/dist/routes-v1.d.ts +1 -1
- package/dist/routes-v1.js +29 -418
- package/dist/routes-v2.js +1 -1
- package/dist/self-update.d.ts +2 -1
- package/dist/self-update.js +5 -1
- package/dist/server.d.ts +1 -3
- package/dist/server.js +9 -12
- package/dist/session-store.d.ts +12 -10
- package/dist/session-store.js +238 -128
- package/dist/session-tree.js +2 -2
- package/dist/sse.d.ts +0 -2
- package/dist/sse.js +13 -46
- package/dist/tool-map.js +1 -3
- 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.d.ts
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
|
|
@@ -111,23 +111,25 @@ export declare function scanPiSessions(directory: string): ScannedSession[];
|
|
|
111
111
|
export declare class SessionStore {
|
|
112
112
|
readonly stateDir: string;
|
|
113
113
|
private readonly file;
|
|
114
|
-
private
|
|
115
|
-
|
|
116
|
-
private
|
|
114
|
+
private db;
|
|
115
|
+
private version;
|
|
116
|
+
private readonly entries;
|
|
117
117
|
private batchState;
|
|
118
118
|
constructor(stateDir?: string);
|
|
119
|
-
private
|
|
120
|
-
private
|
|
121
|
-
private
|
|
122
|
-
private
|
|
123
|
-
private
|
|
119
|
+
private database;
|
|
120
|
+
private migrate;
|
|
121
|
+
private upsert;
|
|
122
|
+
private writeRow;
|
|
123
|
+
private refresh;
|
|
124
|
+
private read;
|
|
124
125
|
private persist;
|
|
125
126
|
private mutate;
|
|
126
|
-
/**
|
|
127
|
+
/** Stage only touched rows; replay partial updates against the latest rows under SQLite's write lock. */
|
|
127
128
|
batch<T>(action: () => T): T;
|
|
128
129
|
get(ocSessionId: string): SessionRegistryEntry | undefined;
|
|
129
130
|
idFor(piSessionId: string): string | undefined;
|
|
130
131
|
childIds(): string[];
|
|
132
|
+
private matching;
|
|
131
133
|
set(ocSessionId: string, entry: SessionRegistryEntry): void;
|
|
132
134
|
/** Merge a partial update (e.g. title override, updated timestamp). */
|
|
133
135
|
update(ocSessionId: string, patch: Partial<SessionRegistryEntry>): void;
|
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);
|
|
@@ -512,140 +510,231 @@ function errorCode(error) {
|
|
|
512
510
|
export class SessionStore {
|
|
513
511
|
stateDir;
|
|
514
512
|
file;
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
513
|
+
db;
|
|
514
|
+
version;
|
|
515
|
+
entries = new Map();
|
|
518
516
|
batchState;
|
|
519
517
|
constructor(stateDir) {
|
|
520
518
|
const dir = stateDir ?? envSetting("STATE_DIR") ?? defaultStateDir();
|
|
521
519
|
this.stateDir = dir;
|
|
522
|
-
this.file = join(dir, "sessions.
|
|
520
|
+
this.file = join(dir, "sessions.sqlite");
|
|
523
521
|
}
|
|
524
|
-
|
|
525
|
-
if (this.
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
return this.committed();
|
|
531
|
-
}
|
|
532
|
-
committed() {
|
|
533
|
-
if (this.entries !== undefined)
|
|
534
|
-
return this.entries;
|
|
535
|
-
this.raw = this.readRaw();
|
|
536
|
-
this.entries = this.parseEntries(this.raw);
|
|
537
|
-
return this.entries;
|
|
538
|
-
}
|
|
539
|
-
readRaw() {
|
|
540
|
-
try {
|
|
541
|
-
return readFileSync(this.file);
|
|
542
|
-
}
|
|
543
|
-
catch {
|
|
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))
|
|
544
527
|
return undefined;
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
parseEntries(raw) {
|
|
548
|
-
const entries = new Map();
|
|
549
|
-
if (raw === undefined)
|
|
550
|
-
return entries;
|
|
528
|
+
let db;
|
|
551
529
|
try {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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;
|
|
558
553
|
}
|
|
559
|
-
catch {
|
|
560
|
-
|
|
554
|
+
catch (error) {
|
|
555
|
+
db?.close();
|
|
556
|
+
throw new SessionPersistenceError(this.file, error);
|
|
561
557
|
}
|
|
562
|
-
return entries;
|
|
563
558
|
}
|
|
564
|
-
|
|
565
|
-
const
|
|
566
|
-
|
|
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;
|
|
567
570
|
try {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
for (;;) {
|
|
571
|
+
const imported = meta("legacy_backup");
|
|
572
|
+
const parse = () => {
|
|
571
573
|
try {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
+
const parsed = JSON.parse(readFileSync(legacy, "utf8"));
|
|
575
|
+
return record(parsed) ? parsed : undefined;
|
|
574
576
|
}
|
|
575
577
|
catch (error) {
|
|
576
|
-
if (
|
|
578
|
+
if (!(error instanceof SyntaxError))
|
|
577
579
|
throw error;
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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)
|
|
581
611
|
continue;
|
|
582
|
-
|
|
612
|
+
this.writeRow(upsert, id, entry);
|
|
613
|
+
merged += 1;
|
|
583
614
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
continue;
|
|
615
|
+
if (merged > 0) {
|
|
616
|
+
backup = `${legacy}.merged-${Date.now()}-${process.pid}`;
|
|
617
|
+
setMeta("reconciled_at", String(Date.now()));
|
|
588
618
|
}
|
|
589
|
-
Atomics.wait(registryLockWaiter, 0, 0, REGISTRY_LOCK_RETRY_MS);
|
|
590
619
|
}
|
|
591
620
|
}
|
|
592
|
-
|
|
621
|
+
db.exec("COMMIT");
|
|
593
622
|
}
|
|
594
623
|
catch (error) {
|
|
595
|
-
|
|
624
|
+
db.exec("ROLLBACK");
|
|
625
|
+
throw error;
|
|
596
626
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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;
|
|
601
641
|
}
|
|
602
642
|
}
|
|
603
643
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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;
|
|
618
660
|
}
|
|
619
|
-
this.withRegistryLock(() => {
|
|
620
|
-
// Another process may have written since; only parse again when the bytes differ.
|
|
621
|
-
const raw = this.readRaw();
|
|
622
|
-
const entries = this.entries !== undefined && raw !== undefined && this.raw !== undefined && raw.equals(this.raw)
|
|
623
|
-
? new Map(this.entries)
|
|
624
|
-
: this.parseEntries(raw);
|
|
625
|
-
const written = change(entries) ? this.persist(entries) : raw;
|
|
626
|
-
this.entries = entries;
|
|
627
|
-
this.raw = written;
|
|
628
|
-
});
|
|
629
661
|
}
|
|
630
|
-
|
|
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);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
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. */
|
|
631
728
|
batch(action) {
|
|
632
729
|
if (this.batchState !== undefined)
|
|
633
730
|
return action();
|
|
634
|
-
const
|
|
635
|
-
const batch = { entries: undefined, isolated: new WeakSet(), changes };
|
|
731
|
+
const batch = { entries: new Map(), changes: [] };
|
|
636
732
|
this.batchState = batch;
|
|
637
733
|
try {
|
|
638
734
|
const result = action();
|
|
639
735
|
this.batchState = undefined;
|
|
640
|
-
if (batch.changes.length !== 0)
|
|
641
|
-
this.
|
|
642
|
-
let changed = false;
|
|
643
|
-
for (const change of batch.changes)
|
|
644
|
-
if (change(entries))
|
|
645
|
-
changed = true;
|
|
646
|
-
return changed;
|
|
647
|
-
});
|
|
648
|
-
}
|
|
736
|
+
if (batch.changes.length !== 0)
|
|
737
|
+
this.persist(batch.changes);
|
|
649
738
|
return result;
|
|
650
739
|
}
|
|
651
740
|
finally {
|
|
@@ -653,49 +742,70 @@ export class SessionStore {
|
|
|
653
742
|
}
|
|
654
743
|
}
|
|
655
744
|
get(ocSessionId) {
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
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)
|
|
659
754
|
return entry;
|
|
660
|
-
const staged = structuredClone(entry);
|
|
661
|
-
entries.set(ocSessionId, staged);
|
|
662
|
-
this.batchState.isolated.add(staged);
|
|
755
|
+
const staged = entry === undefined ? undefined : structuredClone(entry);
|
|
756
|
+
batch.entries.set(ocSessionId, staged);
|
|
663
757
|
return staged;
|
|
664
758
|
}
|
|
665
759
|
idFor(piSessionId) {
|
|
666
|
-
return
|
|
760
|
+
return this.matching("SELECT id FROM sessions WHERE pi_session_id = ? ORDER BY rowid", [piSessionId], (entry) => entry.piSessionId === piSessionId)[0];
|
|
667
761
|
}
|
|
668
762
|
childIds() {
|
|
669
|
-
return
|
|
670
|
-
|
|
671
|
-
|
|
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
|
+
});
|
|
672
781
|
}
|
|
673
782
|
set(ocSessionId, entry) {
|
|
674
783
|
const saved = this.batchState === undefined ? entry : structuredClone(entry);
|
|
675
|
-
this.mutate((
|
|
676
|
-
entries.set(ocSessionId, this.batchState === undefined ? saved : structuredClone(saved));
|
|
677
|
-
return true;
|
|
678
|
-
});
|
|
784
|
+
this.mutate(ocSessionId, () => (this.batchState === undefined ? saved : structuredClone(saved)));
|
|
679
785
|
}
|
|
680
786
|
/** Merge a partial update (e.g. title override, updated timestamp). */
|
|
681
787
|
update(ocSessionId, patch) {
|
|
682
788
|
const saved = this.batchState === undefined ? patch : structuredClone(patch);
|
|
683
|
-
this.mutate((
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
return false;
|
|
687
|
-
entries.set(ocSessionId, {
|
|
789
|
+
this.mutate(ocSessionId, (previous) => previous === undefined
|
|
790
|
+
? undefined
|
|
791
|
+
: {
|
|
688
792
|
...previous,
|
|
689
793
|
...(this.batchState === undefined ? saved : structuredClone(saved)),
|
|
690
794
|
});
|
|
691
|
-
return true;
|
|
692
|
-
});
|
|
693
795
|
}
|
|
694
796
|
remove(ocSessionId) {
|
|
695
|
-
this.mutate((
|
|
797
|
+
this.mutate(ocSessionId, () => undefined);
|
|
696
798
|
}
|
|
697
799
|
/** Every directory the registry knows about — used to resolve bare session ids. */
|
|
698
800
|
directories() {
|
|
699
|
-
|
|
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))];
|
|
700
810
|
}
|
|
701
811
|
}
|
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;
|
package/dist/sse.d.ts
CHANGED
|
@@ -3,9 +3,7 @@ import type { ServerResponse } from "node:http";
|
|
|
3
3
|
import type { OCEvent } from "./types.js";
|
|
4
4
|
import type { TodoUpdated } from "./todo-bridge.js";
|
|
5
5
|
export type SSESubscriber = {
|
|
6
|
-
flavor: "v1" | "v2";
|
|
7
6
|
directory?: string;
|
|
8
|
-
global: boolean;
|
|
9
7
|
};
|
|
10
8
|
/** The session an event belongs to, when it names one. */
|
|
11
9
|
export declare function eventSessionID(event: OCEvent | TodoUpdated): string | undefined;
|
package/dist/sse.js
CHANGED
|
@@ -48,9 +48,7 @@ export class SSE {
|
|
|
48
48
|
: event.type === "todo.updated"
|
|
49
49
|
? [{ id: `evt_${randomUUID()}`, type: event.type, location: { directory }, data: event.properties }]
|
|
50
50
|
: this.v2events.translate(directory, event);
|
|
51
|
-
const records = v2Frames.
|
|
52
|
-
? [this.record(directory, { v1: event })]
|
|
53
|
-
: v2Frames.map((v2, index) => this.record(directory, index === 0 ? { v1: event, v2 } : { v2 }));
|
|
51
|
+
const records = v2Frames.map((v2) => this.record(directory, { v2 }));
|
|
54
52
|
for (const record of records)
|
|
55
53
|
this.retain(record);
|
|
56
54
|
for (const [res, subscriber] of this.clients) {
|
|
@@ -59,7 +57,7 @@ export class SSE {
|
|
|
59
57
|
continue;
|
|
60
58
|
}
|
|
61
59
|
for (const record of records) {
|
|
62
|
-
if (
|
|
60
|
+
if (subscriber.directory !== undefined && subscriber.directory !== directory)
|
|
63
61
|
continue;
|
|
64
62
|
if (!this.writeRecord(res, subscriber, record))
|
|
65
63
|
break;
|
|
@@ -102,22 +100,14 @@ export class SSE {
|
|
|
102
100
|
this.clients.delete(res);
|
|
103
101
|
});
|
|
104
102
|
// OpenCode emits server.connected on subscribe; OpenChamber waits for it.
|
|
105
|
-
const connected =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
: { type: "server.connected", properties: {} };
|
|
114
|
-
const initial = subscriber.flavor === "v1" && subscriber.global
|
|
115
|
-
? {
|
|
116
|
-
directory: subscriber.directory ?? this.options.defaultDirectory,
|
|
117
|
-
payload: { id: `evt_${randomUUID()}`, ...connected },
|
|
118
|
-
}
|
|
119
|
-
: connected;
|
|
120
|
-
write(res, `data: ${JSON.stringify(initial)}\n\n`);
|
|
103
|
+
const connected = {
|
|
104
|
+
id: `evt_${Date.now()}`,
|
|
105
|
+
created: Date.now(),
|
|
106
|
+
type: "server.connected",
|
|
107
|
+
location: { directory: subscriber.directory ?? this.options.defaultDirectory },
|
|
108
|
+
data: {},
|
|
109
|
+
};
|
|
110
|
+
write(res, `data: ${JSON.stringify(connected)}\n\n`);
|
|
121
111
|
const cursor = Number(lastEventID);
|
|
122
112
|
if (!Number.isSafeInteger(cursor) || cursor < 0)
|
|
123
113
|
return;
|
|
@@ -135,35 +125,12 @@ export class SSE {
|
|
|
135
125
|
this.replay.shift();
|
|
136
126
|
}
|
|
137
127
|
writeRecord(res, subscriber, record) {
|
|
138
|
-
if (
|
|
128
|
+
if (subscriber.directory !== undefined && subscriber.directory !== record.directory)
|
|
139
129
|
return true;
|
|
140
|
-
const data =
|
|
141
|
-
? record.v2
|
|
142
|
-
: record.v1 === undefined
|
|
143
|
-
? undefined
|
|
144
|
-
: subscriber.global
|
|
145
|
-
? {
|
|
146
|
-
directory: record.directory,
|
|
147
|
-
project: record.directory,
|
|
148
|
-
payload: {
|
|
149
|
-
id: `evt_${record.id}`,
|
|
150
|
-
...record.v1,
|
|
151
|
-
properties: {
|
|
152
|
-
...record.v1.properties,
|
|
153
|
-
...(record.v1.type === "session.created" ||
|
|
154
|
-
record.v1.type === "session.updated" ||
|
|
155
|
-
record.v1.type === "session.deleted"
|
|
156
|
-
? { sessionID: record.v1.properties.info.id }
|
|
157
|
-
: {}),
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
}
|
|
161
|
-
: record.v1;
|
|
130
|
+
const data = record.v2;
|
|
162
131
|
if (data === undefined)
|
|
163
132
|
return true;
|
|
164
|
-
const frame =
|
|
165
|
-
? `data: ${JSON.stringify(data)}\n\n`
|
|
166
|
-
: `id: ${record.id}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
133
|
+
const frame = `id: ${record.id}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
167
134
|
return this.writeSSE(res, frame);
|
|
168
135
|
}
|
|
169
136
|
}
|
package/dist/tool-map.js
CHANGED
|
@@ -257,9 +257,7 @@ export function nestedToolParts(parent, details, sessions = {}) {
|
|
|
257
257
|
messageID: parent.messageID,
|
|
258
258
|
type: "tool",
|
|
259
259
|
callID: `nested:${callID}`,
|
|
260
|
-
|
|
261
|
-
// and a failed spawn has no session to open, so it goes out as a plain card that does both.
|
|
262
|
-
tool: failed && mapped.tool === "task" ? "agent" : mapped.tool,
|
|
260
|
+
tool: mapped.tool,
|
|
263
261
|
state: !failed
|
|
264
262
|
? { ...state, status: "completed" }
|
|
265
263
|
: {
|
package/dist/work-status.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Background work of one session for the OmO Work status section (extensions/
|
|
2
|
+
* Background work of one session for the OmO Work status section (extensions/antechamber):
|
|
3
3
|
* todo phases, terminal monitors, detached eval cells, background terminal sessions and the
|
|
4
4
|
* goal continuation timer. OpenChamber 2.x has no native surface for any of these.
|
|
5
5
|
*
|