antechamber 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +17 -14
- package/bin/opencode-antechamber +10 -1
- package/bin/opencode-pi +11 -1
- package/dist/approvals.d.ts +4 -1
- package/dist/approvals.js +51 -33
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +1 -1
- package/dist/commands/context.d.ts +0 -1
- package/dist/commands/context.js +1 -1
- package/dist/commands/doctor.d.ts +3 -2
- package/dist/commands/serve.d.ts +2 -1
- package/dist/commands/setup.d.ts +2 -3
- package/dist/commands/setup.js +16 -4
- package/dist/http.d.ts +8 -1
- package/dist/http.js +42 -19
- package/dist/pi-event-translation.d.ts +21 -3
- package/dist/pi-event-translation.js +317 -65
- package/dist/pi-rpc.d.ts +26 -2
- package/dist/pi-rpc.js +102 -29
- package/dist/prompt-parts.d.ts +40 -0
- package/dist/prompt-parts.js +157 -0
- package/dist/route-services.d.ts +45 -2
- package/dist/route-services.js +70 -3
- package/dist/routes-v1.js +110 -65
- package/dist/routes-v2.js +376 -73
- package/dist/server.d.ts +9 -1
- package/dist/server.js +109 -9
- package/dist/session-commands.d.ts +20 -15
- package/dist/session-commands.js +61 -11
- package/dist/session-diff.d.ts +1 -1
- package/dist/session-diff.js +10 -2
- package/dist/session-history.js +113 -42
- package/dist/session-hosts.js +2 -2
- package/dist/session-store.d.ts +8 -4
- package/dist/session-store.js +183 -33
- package/dist/session-tree.d.ts +9 -2
- package/dist/session-tree.js +51 -63
- package/dist/sessions.d.ts +34 -6
- package/dist/sessions.js +340 -146
- package/dist/sse.d.ts +16 -6
- package/dist/sse.js +77 -21
- package/dist/subagents.d.ts +5 -0
- package/dist/subagents.js +86 -69
- package/dist/tool-map.d.ts +3 -2
- package/dist/types.d.ts +164 -1
- package/dist/v2.d.ts +18 -4
- package/dist/v2.js +141 -50
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/omo-dag/README.md +2 -0
- package/extensions/omo-dag/docs/DESIGN.md +10 -1
- package/extensions/omo-dag/panel/index.html +1 -1
- package/extensions/omo-dag/panel/main.js +60 -60
- package/extensions/omo-dag/service/main.js +72 -8
- package/package.json +1 -1
package/dist/session-hosts.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { once } from "node:events";
|
|
4
|
-
import { closeSync, constants, openSync, readFileSync, readdirSync, readlinkSync } from "node:fs";
|
|
4
|
+
import { closeSync, constants, openSync, readFileSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
|
|
5
5
|
import { basename } from "node:path";
|
|
6
6
|
import { findPidfdPython } from "./pidfd.js";
|
|
7
7
|
/** Capability and warning state are captured once, before any shutdown work. */
|
|
@@ -144,7 +144,7 @@ async function stopSupportedSessionHosts(created, python) {
|
|
|
144
144
|
try {
|
|
145
145
|
// The original leader must still exist after the process-table scan:
|
|
146
146
|
// a recycled group number cannot confer its ownership on new members.
|
|
147
|
-
return readlinkSync(`/proc/self/fd/${info.fd}/cwd`) === directory;
|
|
147
|
+
return readlinkSync(`/proc/self/fd/${info.fd}/cwd`) === realpathSync(directory);
|
|
148
148
|
}
|
|
149
149
|
catch (error) {
|
|
150
150
|
if (error instanceof Error && "code" in error && ["ENOENT", "ESRCH"].includes(String(error.code)))
|
package/dist/session-store.d.ts
CHANGED
|
@@ -37,6 +37,9 @@ export interface SessionRegistryEntry {
|
|
|
37
37
|
connectorOwned?: true;
|
|
38
38
|
tree?: SessionTreeState;
|
|
39
39
|
parentID?: string | undefined;
|
|
40
|
+
parentMessageID?: string;
|
|
41
|
+
agent?: string;
|
|
42
|
+
metadata?: Record<string, unknown>;
|
|
40
43
|
task?: ChildTask;
|
|
41
44
|
piSessionId: string;
|
|
42
45
|
piPath: string;
|
|
@@ -53,6 +56,7 @@ export interface SessionRegistryEntry {
|
|
|
53
56
|
model?: {
|
|
54
57
|
providerID: string;
|
|
55
58
|
modelID: string;
|
|
59
|
+
variant?: string;
|
|
56
60
|
};
|
|
57
61
|
created: number;
|
|
58
62
|
updated: number;
|
|
@@ -81,18 +85,18 @@ export declare function piSessionsRootMtime(): number | undefined;
|
|
|
81
85
|
export declare function scanAllPiSessions(): ScannedSession[];
|
|
82
86
|
/** All restorable pi sessions recorded for `directory`. Corrupt files are skipped. */
|
|
83
87
|
export declare function scanPiSessions(directory: string): ScannedSession[];
|
|
84
|
-
/**
|
|
85
|
-
* Persistent oc-session-id → metadata registry (title overrides, known pi
|
|
86
|
-
* paths). Atomic writes, lazy load, corrupt file degrades to an empty map.
|
|
87
|
-
*/
|
|
88
88
|
export declare class SessionStore {
|
|
89
89
|
readonly stateDir: string;
|
|
90
90
|
private readonly file;
|
|
91
91
|
private entries;
|
|
92
92
|
constructor(stateDir?: string);
|
|
93
93
|
private load;
|
|
94
|
+
private readEntries;
|
|
95
|
+
private withRegistryLock;
|
|
94
96
|
private persist;
|
|
97
|
+
private mutate;
|
|
95
98
|
get(ocSessionId: string): SessionRegistryEntry | undefined;
|
|
99
|
+
idFor(piSessionId: string): string | undefined;
|
|
96
100
|
childIds(): string[];
|
|
97
101
|
set(ocSessionId: string, entry: SessionRegistryEntry): void;
|
|
98
102
|
/** Merge a partial update (e.g. title override, updated timestamp). */
|
package/dist/session-store.js
CHANGED
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
* the same OpenChamber id, even if the registry file is lost.
|
|
15
15
|
*/
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, writeFileSync, } from "node:fs";
|
|
17
|
+
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
import { piAgentDir } from "./agent-dir.js";
|
|
20
|
+
import { isPromptPart } from "./prompt-parts.js";
|
|
20
21
|
import { defaultStateDir, envSetting } from "./config.js";
|
|
21
22
|
/** pi's session directory for a given cwd (see pi docs/session-format.md). */
|
|
22
23
|
export function piSessionDir(cwd) {
|
|
@@ -38,6 +39,12 @@ export class SessionPersistenceError extends Error {
|
|
|
38
39
|
/** Read at most this many leading bytes when sniffing headers/titles. */
|
|
39
40
|
const SNIFF_BYTES = 256 * 1024;
|
|
40
41
|
const TITLE_MAX_LINES = 200;
|
|
42
|
+
const TITLE_MAX_CHARS = 60;
|
|
43
|
+
const REGISTRY_DIRECTORY_MODE = 0o700;
|
|
44
|
+
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));
|
|
41
48
|
function readHead(path, bytes = SNIFF_BYTES) {
|
|
42
49
|
const fd = openSync(path, "r");
|
|
43
50
|
try {
|
|
@@ -70,7 +77,7 @@ function titleFromHead(head, piSessionId) {
|
|
|
70
77
|
.join("\n")
|
|
71
78
|
.trim();
|
|
72
79
|
if (text !== "")
|
|
73
|
-
return text.slice(0,
|
|
80
|
+
return text.slice(0, TITLE_MAX_CHARS);
|
|
74
81
|
}
|
|
75
82
|
return `pi session ${piSessionId.slice(0, 8)}`;
|
|
76
83
|
}
|
|
@@ -261,6 +268,105 @@ export function scanPiSessions(directory) {
|
|
|
261
268
|
* Persistent oc-session-id → metadata registry (title overrides, known pi
|
|
262
269
|
* paths). Atomic writes, lazy load, corrupt file degrades to an empty map.
|
|
263
270
|
*/
|
|
271
|
+
function record(value) {
|
|
272
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
273
|
+
}
|
|
274
|
+
function stringRecord(value) {
|
|
275
|
+
return record(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
276
|
+
}
|
|
277
|
+
function booleanRecord(value) {
|
|
278
|
+
return record(value) && Object.values(value).every((entry) => typeof entry === "boolean");
|
|
279
|
+
}
|
|
280
|
+
function persistedMessage(value) {
|
|
281
|
+
return (record(value) &&
|
|
282
|
+
record(value.info) &&
|
|
283
|
+
typeof value.info.id === "string" &&
|
|
284
|
+
typeof value.info.role === "string" &&
|
|
285
|
+
Array.isArray(value.parts));
|
|
286
|
+
}
|
|
287
|
+
function sessionTreeState(value) {
|
|
288
|
+
if (!record(value) || !stringRecord(value.messageEntries))
|
|
289
|
+
return false;
|
|
290
|
+
if (value.promptParts !== undefined &&
|
|
291
|
+
(!record(value.promptParts) ||
|
|
292
|
+
!Object.values(value.promptParts).every((parts) => Array.isArray(parts) && parts.every(isPromptPart))))
|
|
293
|
+
return false;
|
|
294
|
+
if (value.promptMetadata !== undefined &&
|
|
295
|
+
(!record(value.promptMetadata) || !Object.values(value.promptMetadata).every((metadata) => record(metadata))))
|
|
296
|
+
return false;
|
|
297
|
+
if (value.messageParts !== undefined &&
|
|
298
|
+
(!record(value.messageParts) ||
|
|
299
|
+
!Object.values(value.messageParts).every((parts) => Array.isArray(parts) && parts.every((part) => typeof part === "string"))))
|
|
300
|
+
return false;
|
|
301
|
+
if (value.shellDispatch !== undefined && !booleanRecord(value.shellDispatch))
|
|
302
|
+
return false;
|
|
303
|
+
if (value.leafId !== undefined && value.leafId !== null && typeof value.leafId !== "string")
|
|
304
|
+
return false;
|
|
305
|
+
if (value.revert !== undefined) {
|
|
306
|
+
if (!record(value.revert) ||
|
|
307
|
+
typeof value.revert.messageID !== "string" ||
|
|
308
|
+
(value.revert.previousLeafId !== null && typeof value.revert.previousLeafId !== "string") ||
|
|
309
|
+
(value.revert.targetId !== null && typeof value.revert.targetId !== "string"))
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
return (value.localShellTurns === undefined ||
|
|
313
|
+
(Array.isArray(value.localShellTurns) &&
|
|
314
|
+
value.localShellTurns.every((turn) => record(turn) &&
|
|
315
|
+
(turn.afterEntryId === null || typeof turn.afterEntryId === "string") &&
|
|
316
|
+
Array.isArray(turn.messages) &&
|
|
317
|
+
turn.messages.length === 2 &&
|
|
318
|
+
persistedMessage(turn.messages[0]) &&
|
|
319
|
+
persistedMessage(turn.messages[1]))));
|
|
320
|
+
}
|
|
321
|
+
function childTask(value) {
|
|
322
|
+
return (record(value) &&
|
|
323
|
+
typeof value.id === "string" &&
|
|
324
|
+
typeof value.parentPiId === "string" &&
|
|
325
|
+
record(value.input) &&
|
|
326
|
+
record(value.details) &&
|
|
327
|
+
(value.terminalAt === undefined || (typeof value.terminalAt === "number" && Number.isFinite(value.terminalAt))));
|
|
328
|
+
}
|
|
329
|
+
function titleSource(value) {
|
|
330
|
+
return value === "user" || value === "native" || value === "fallback" || value === "generated";
|
|
331
|
+
}
|
|
332
|
+
function registryEntry(value) {
|
|
333
|
+
if (!record(value) ||
|
|
334
|
+
typeof value.piPath !== "string" ||
|
|
335
|
+
typeof value.piSessionId !== "string" ||
|
|
336
|
+
typeof value.directory !== "string" ||
|
|
337
|
+
typeof value.created !== "number" ||
|
|
338
|
+
!Number.isFinite(value.created) ||
|
|
339
|
+
typeof value.updated !== "number" ||
|
|
340
|
+
!Number.isFinite(value.updated))
|
|
341
|
+
return false;
|
|
342
|
+
if (value.connectorOwned !== undefined && value.connectorOwned !== true)
|
|
343
|
+
return false;
|
|
344
|
+
if (value.tree !== undefined && !sessionTreeState(value.tree))
|
|
345
|
+
return false;
|
|
346
|
+
if (value.parentID !== undefined && typeof value.parentID !== "string")
|
|
347
|
+
return false;
|
|
348
|
+
if (value.parentMessageID !== undefined && typeof value.parentMessageID !== "string")
|
|
349
|
+
return false;
|
|
350
|
+
if (value.agent !== undefined && typeof value.agent !== "string")
|
|
351
|
+
return false;
|
|
352
|
+
if (value.metadata !== undefined && !record(value.metadata))
|
|
353
|
+
return false;
|
|
354
|
+
if (value.task !== undefined && !childTask(value.task))
|
|
355
|
+
return false;
|
|
356
|
+
if (value.title !== undefined && typeof value.title !== "string")
|
|
357
|
+
return false;
|
|
358
|
+
if (value.titleSource !== undefined && !titleSource(value.titleSource))
|
|
359
|
+
return false;
|
|
360
|
+
if (value.archived !== undefined && (typeof value.archived !== "number" || !Number.isFinite(value.archived)))
|
|
361
|
+
return false;
|
|
362
|
+
if (value.model !== undefined &&
|
|
363
|
+
(!record(value.model) || typeof value.model.providerID !== "string" || typeof value.model.modelID !== "string"))
|
|
364
|
+
return false;
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
function errorCode(error) {
|
|
368
|
+
return error instanceof Error && "code" in error ? String(error.code) : undefined;
|
|
369
|
+
}
|
|
264
370
|
export class SessionStore {
|
|
265
371
|
stateDir;
|
|
266
372
|
file;
|
|
@@ -273,64 +379,108 @@ export class SessionStore {
|
|
|
273
379
|
load() {
|
|
274
380
|
if (this.entries !== undefined)
|
|
275
381
|
return this.entries;
|
|
276
|
-
this.entries =
|
|
382
|
+
this.entries = this.readEntries();
|
|
383
|
+
return this.entries;
|
|
384
|
+
}
|
|
385
|
+
readEntries() {
|
|
386
|
+
const entries = new Map();
|
|
277
387
|
try {
|
|
278
388
|
const raw = JSON.parse(readFileSync(this.file, "utf8"));
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
(typeof v.model.providerID !== "string" || typeof v.model.modelID !== "string")) {
|
|
285
|
-
delete v.model;
|
|
286
|
-
}
|
|
287
|
-
this.entries.set(k, v);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
389
|
+
if (!record(raw))
|
|
390
|
+
return entries;
|
|
391
|
+
for (const [id, value] of Object.entries(raw))
|
|
392
|
+
if (registryEntry(value))
|
|
393
|
+
entries.set(id, value);
|
|
290
394
|
}
|
|
291
395
|
catch {
|
|
292
396
|
/* missing or corrupt — start empty */
|
|
293
397
|
}
|
|
294
|
-
return
|
|
398
|
+
return entries;
|
|
295
399
|
}
|
|
296
|
-
|
|
297
|
-
const
|
|
400
|
+
withRegistryLock(action) {
|
|
401
|
+
const lock = `${this.file}.lock`;
|
|
402
|
+
let fd;
|
|
298
403
|
try {
|
|
299
|
-
mkdirSync(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
404
|
+
mkdirSync(this.stateDir, { recursive: true, mode: REGISTRY_DIRECTORY_MODE });
|
|
405
|
+
chmodSync(this.stateDir, REGISTRY_DIRECTORY_MODE);
|
|
406
|
+
for (;;) {
|
|
407
|
+
try {
|
|
408
|
+
fd = openSync(lock, "wx", REGISTRY_FILE_MODE);
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
if (errorCode(error) !== "EEXIST")
|
|
413
|
+
throw error;
|
|
414
|
+
try {
|
|
415
|
+
if (Date.now() - statSync(lock).mtimeMs >= REGISTRY_LOCK_STALE_MS) {
|
|
416
|
+
unlinkSync(lock);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (staleLockError) {
|
|
421
|
+
if (errorCode(staleLockError) !== "ENOENT")
|
|
422
|
+
throw staleLockError;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
Atomics.wait(registryLockWaiter, 0, 0, REGISTRY_LOCK_RETRY_MS);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return action();
|
|
303
429
|
}
|
|
304
430
|
catch (error) {
|
|
305
431
|
throw new SessionPersistenceError(this.file, error);
|
|
306
432
|
}
|
|
307
|
-
|
|
433
|
+
finally {
|
|
434
|
+
if (fd !== undefined) {
|
|
435
|
+
closeSync(fd);
|
|
436
|
+
unlinkSync(lock);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
persist(entries) {
|
|
441
|
+
const obj = Object.fromEntries(entries);
|
|
442
|
+
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
443
|
+
writeFileSync(tmp, JSON.stringify(obj, null, 2), { mode: REGISTRY_FILE_MODE });
|
|
444
|
+
chmodSync(tmp, REGISTRY_FILE_MODE);
|
|
445
|
+
renameSync(tmp, this.file);
|
|
446
|
+
}
|
|
447
|
+
mutate(change) {
|
|
448
|
+
this.withRegistryLock(() => {
|
|
449
|
+
const entries = this.readEntries();
|
|
450
|
+
if (change(entries))
|
|
451
|
+
this.persist(entries);
|
|
452
|
+
this.entries = entries;
|
|
453
|
+
});
|
|
308
454
|
}
|
|
309
455
|
get(ocSessionId) {
|
|
310
456
|
return this.load().get(ocSessionId);
|
|
311
457
|
}
|
|
458
|
+
idFor(piSessionId) {
|
|
459
|
+
return [...this.load()].find(([, entry]) => entry.piSessionId === piSessionId)?.[0];
|
|
460
|
+
}
|
|
312
461
|
childIds() {
|
|
313
462
|
return [...this.load()]
|
|
314
463
|
.filter(([, entry]) => entry.task !== undefined && entry.parentID !== undefined)
|
|
315
464
|
.map(([id]) => id);
|
|
316
465
|
}
|
|
317
466
|
set(ocSessionId, entry) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
467
|
+
this.mutate((entries) => {
|
|
468
|
+
entries.set(ocSessionId, entry);
|
|
469
|
+
return true;
|
|
470
|
+
});
|
|
321
471
|
}
|
|
322
472
|
/** Merge a partial update (e.g. title override, updated timestamp). */
|
|
323
473
|
update(ocSessionId, patch) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
474
|
+
this.mutate((entries) => {
|
|
475
|
+
const previous = entries.get(ocSessionId);
|
|
476
|
+
if (previous === undefined)
|
|
477
|
+
return false;
|
|
478
|
+
entries.set(ocSessionId, { ...previous, ...patch });
|
|
479
|
+
return true;
|
|
480
|
+
});
|
|
329
481
|
}
|
|
330
482
|
remove(ocSessionId) {
|
|
331
|
-
|
|
332
|
-
if (entries.delete(ocSessionId))
|
|
333
|
-
this.persist(entries);
|
|
483
|
+
this.mutate((entries) => entries.delete(ocSessionId));
|
|
334
484
|
}
|
|
335
485
|
/** Every directory the registry knows about — used to resolve bare session ids. */
|
|
336
486
|
directories() {
|
package/dist/session-tree.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { type PiRpcClient } from "./pi-rpc.js";
|
|
2
2
|
import type { ManagedSession } from "./sessions.js";
|
|
3
3
|
import type { OCMessageWithParts } from "./types.js";
|
|
4
|
+
import type { OCPromptPart } from "./types.js";
|
|
4
5
|
export interface SessionTreeState {
|
|
5
6
|
messageEntries: Record<string, string>;
|
|
7
|
+
/** Published part identities in native content order, keyed by durable message identity. */
|
|
8
|
+
messageParts?: Record<string, string[]>;
|
|
9
|
+
/** Original rich user input; native RPC stores only adapted text and images. */
|
|
10
|
+
promptParts?: Record<string, OCPromptPart[]>;
|
|
11
|
+
/** Client prompt metadata; native RPC does not store it. */
|
|
12
|
+
promptMetadata?: Record<string, Record<string, unknown>>;
|
|
6
13
|
/** Actual transport dispatch, not inferred from a card's terminal state. */
|
|
7
14
|
shellDispatch?: Record<string, boolean>;
|
|
8
15
|
/** Identity-only dispatched admissions; never rendered without a native entry. */
|
|
@@ -21,6 +28,8 @@ export interface TreeEntry {
|
|
|
21
28
|
type: string;
|
|
22
29
|
id: string;
|
|
23
30
|
parentId: string | null;
|
|
31
|
+
customType?: string;
|
|
32
|
+
details?: unknown;
|
|
24
33
|
message?: {
|
|
25
34
|
role: string;
|
|
26
35
|
clientMessageId?: string;
|
|
@@ -48,7 +57,6 @@ export declare function recordTurn(s: ManagedSession): void;
|
|
|
48
57
|
export declare function branchEntries(tree: Entries, leafId?: string | null): TreeEntry[];
|
|
49
58
|
/** Live ids belong to the UI; restored ids belong to JSONL offsets. Save both. */
|
|
50
59
|
export declare function bindMessageEntries(s: ManagedSession, tree: Entries): void;
|
|
51
|
-
export declare function entryForMessage(s: ManagedSession, tree: Entries, messageID: string): TreeEntry;
|
|
52
60
|
export declare function forkConversation(s: ManagedSession, pi: PiRpcClient, tree: Entries, messageID?: string): Promise<{
|
|
53
61
|
sessionId: string;
|
|
54
62
|
sessionFile: string;
|
|
@@ -56,4 +64,3 @@ export declare function forkConversation(s: ManagedSession, pi: PiRpcClient, tre
|
|
|
56
64
|
export declare function revertConversation(s: ManagedSession, pi: PiRpcClient, tree: Entries, messageID: string): Promise<boolean>;
|
|
57
65
|
export declare function unrevertConversation(s: ManagedSession, pi: PiRpcClient): Promise<boolean>;
|
|
58
66
|
export declare function navigate(pi: PiRpcClient, targetId: string | null, signal?: AbortSignal): Promise<string | null>;
|
|
59
|
-
export declare function revertNotice(s: ManagedSession): OCMessageWithParts | undefined;
|
package/dist/session-tree.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RPC_COMMAND_TIMEOUT_MS } from "./pi-rpc.js";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
+
import { contextMessageID, nativePromptInput } from "./prompt-parts.js";
|
|
3
4
|
export class SessionTreeError extends Error {
|
|
4
5
|
}
|
|
5
6
|
export function durableShellTree(tree) {
|
|
@@ -59,19 +60,19 @@ export function branchEntries(tree, leafId = tree.leafId) {
|
|
|
59
60
|
}
|
|
60
61
|
return branch.reverse();
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
-
export function bindMessageEntries(s, tree) {
|
|
63
|
+
function selectedLeafId(s, tree) {
|
|
64
64
|
let leafId = tree.leafId;
|
|
65
65
|
if (!s.tree.revert && s.tree.leafId !== undefined && s.tree.leafId !== null) {
|
|
66
66
|
const selected = branchEntries(tree, s.tree.leafId);
|
|
67
67
|
// A stale snapshot cannot discard a selection or its identity candidates.
|
|
68
68
|
if (selected.length === 0)
|
|
69
|
-
return;
|
|
69
|
+
return undefined;
|
|
70
70
|
if (tree.leafId === null || selected.some((entry) => entry.id === tree.leafId))
|
|
71
71
|
leafId = s.tree.leafId;
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
return leafId;
|
|
74
|
+
}
|
|
75
|
+
function pruneStaleShellTurns(s, positions) {
|
|
75
76
|
s.tree.localShellTurns = (s.tree.localShellTurns ?? []).filter((turn) => {
|
|
76
77
|
if (turn.afterEntryId === null || positions.has(turn.afterEntryId))
|
|
77
78
|
return true;
|
|
@@ -83,13 +84,54 @@ export function bindMessageEntries(s, tree) {
|
|
|
83
84
|
delete s.tree.shellDispatch[turn.messages[1].info.id];
|
|
84
85
|
return false;
|
|
85
86
|
});
|
|
87
|
+
}
|
|
88
|
+
function bindOrdinaryEntry(s, entry, messages, cursor) {
|
|
89
|
+
const text = entry.message?.content
|
|
90
|
+
?.filter((part) => part.type === "text")
|
|
91
|
+
.map((part) => part.text ?? "")
|
|
92
|
+
.join("");
|
|
93
|
+
const index = messages.findIndex((message, position) => position >= cursor &&
|
|
94
|
+
!s.localMessageIds?.has(message.info.id) &&
|
|
95
|
+
s.tree.messageEntries[message.info.id] === undefined &&
|
|
96
|
+
message.info.role === entry.message?.role &&
|
|
97
|
+
(message.info.role !== "user" ||
|
|
98
|
+
(s.tree.promptParts?.[message.info.id]
|
|
99
|
+
? nativePromptInput(message.parts
|
|
100
|
+
.filter((part) => part.type === "text")
|
|
101
|
+
.map((part) => part.text)
|
|
102
|
+
.join("\n"), s.tree.promptParts[message.info.id] ?? []).message
|
|
103
|
+
: message.parts
|
|
104
|
+
.filter((part) => part.type === "text")
|
|
105
|
+
.map((part) => part.text)
|
|
106
|
+
.join("")) === text));
|
|
107
|
+
const message = messages[index];
|
|
108
|
+
if (message === undefined)
|
|
109
|
+
return cursor;
|
|
110
|
+
s.tree.messageEntries[message.info.id] = entry.id;
|
|
111
|
+
return index + 1;
|
|
112
|
+
}
|
|
113
|
+
/** Live ids belong to the UI; restored ids belong to JSONL offsets. Save both. */
|
|
114
|
+
export function bindMessageEntries(s, tree) {
|
|
115
|
+
const leafId = selectedLeafId(s, tree);
|
|
116
|
+
if (leafId === undefined)
|
|
117
|
+
return;
|
|
118
|
+
const branch = branchEntries(tree, s.tree.revert ? s.tree.revert.previousLeafId : leafId);
|
|
119
|
+
const positions = new Map(branch.map((entry, index) => [entry.id, index]));
|
|
120
|
+
pruneStaleShellTurns(s, positions);
|
|
121
|
+
// Admitted context carries its OpenChamber id in the native custom message.
|
|
122
|
+
for (const entry of branch) {
|
|
123
|
+
const messageID = contextMessageID(entry);
|
|
124
|
+
if (messageID !== undefined && s.messages?.some((message) => message.info.id === messageID))
|
|
125
|
+
s.tree.messageEntries[messageID] = entry.id;
|
|
126
|
+
}
|
|
86
127
|
const entries = branch.filter((entry) => entry.type === "message" &&
|
|
87
128
|
(entry.message?.role === "user" ||
|
|
88
129
|
entry.message?.role === "assistant" ||
|
|
89
130
|
entry.message?.role === "bashExecution"));
|
|
131
|
+
const localShellTurns = s.tree.localShellTurns ?? [];
|
|
90
132
|
const visible = s.messages ?? [];
|
|
91
133
|
const visibleIds = new Set(visible.map((message) => message.info.id));
|
|
92
|
-
const candidates =
|
|
134
|
+
const candidates = localShellTurns
|
|
93
135
|
.flatMap((turn) => turn.messages)
|
|
94
136
|
.filter((message) => !visibleIds.has(message.info.id));
|
|
95
137
|
for (const message of candidates)
|
|
@@ -167,32 +209,15 @@ export function bindMessageEntries(s, tree) {
|
|
|
167
209
|
}
|
|
168
210
|
continue;
|
|
169
211
|
}
|
|
170
|
-
|
|
171
|
-
?.filter((part) => part.type === "text")
|
|
172
|
-
.map((part) => part.text ?? "")
|
|
173
|
-
.join("");
|
|
174
|
-
const index = messages.findIndex((message, i) => i >= cursor &&
|
|
175
|
-
!s.localMessageIds?.has(message.info.id) &&
|
|
176
|
-
s.tree.messageEntries[message.info.id] === undefined &&
|
|
177
|
-
message.info.role === entry.message?.role &&
|
|
178
|
-
(message.info.role !== "user" ||
|
|
179
|
-
message.parts
|
|
180
|
-
.filter((part) => part.type === "text")
|
|
181
|
-
.map((part) => part.text)
|
|
182
|
-
.join("") === text));
|
|
183
|
-
const message = messages[index];
|
|
184
|
-
if (message) {
|
|
185
|
-
s.tree.messageEntries[message.info.id] = entry.id;
|
|
186
|
-
cursor = index + 1;
|
|
187
|
-
}
|
|
212
|
+
cursor = bindOrdinaryEntry(s, entry, messages, cursor);
|
|
188
213
|
}
|
|
189
214
|
// Syncs/agent events preserve an active revert's navigation pin. Only
|
|
190
215
|
// revert/unrevert select it explicitly; prompt clears the pin before branching.
|
|
191
216
|
if (!s.tree.revert)
|
|
192
217
|
s.tree.leafId = leafId;
|
|
193
|
-
s.tree.localShellTurns =
|
|
218
|
+
s.tree.localShellTurns = localShellTurns.filter((turn) => s.tree.messageEntries[turn.messages[1].info.id] === undefined);
|
|
194
219
|
}
|
|
195
|
-
|
|
220
|
+
function entryForMessage(s, tree, messageID) {
|
|
196
221
|
const entryId = s.tree.messageEntries[messageID];
|
|
197
222
|
const entry = tree.entries.find((candidate) => candidate.id === entryId);
|
|
198
223
|
if (!entry || !s.messages?.some((message) => message.info.id === messageID)) {
|
|
@@ -285,40 +310,3 @@ export async function navigate(pi, targetId, signal) {
|
|
|
285
310
|
throw new SessionTreeError("Conversation navigation was cancelled");
|
|
286
311
|
return response.data.leafId;
|
|
287
312
|
}
|
|
288
|
-
export function revertNotice(s) {
|
|
289
|
-
const revert = s.tree.revert;
|
|
290
|
-
if (!revert)
|
|
291
|
-
return undefined;
|
|
292
|
-
const boundary = s.messages?.find((message) => message.info.id === revert.messageID);
|
|
293
|
-
const boundaryIndex = s.messages?.findIndex((message) => message.info.id === revert.messageID) ?? 0;
|
|
294
|
-
const parent = s.messages
|
|
295
|
-
?.slice(0, boundaryIndex)
|
|
296
|
-
.reverse()
|
|
297
|
-
.find((message) => message.info.role === "user");
|
|
298
|
-
const messageID = `msg_revert_${revert.messageID}`;
|
|
299
|
-
const created = (boundary?.info.time.created ?? s.info.time.updated) - 1;
|
|
300
|
-
return {
|
|
301
|
-
info: {
|
|
302
|
-
id: messageID,
|
|
303
|
-
sessionID: s.info.id,
|
|
304
|
-
role: "assistant",
|
|
305
|
-
time: { created, completed: created },
|
|
306
|
-
parentID: parent?.info.id ?? "",
|
|
307
|
-
modelID: s.model?.modelID ?? "unknown",
|
|
308
|
-
providerID: s.model?.providerID ?? "unknown",
|
|
309
|
-
mode: "info",
|
|
310
|
-
path: { cwd: s.info.directory, root: s.info.directory },
|
|
311
|
-
cost: 0,
|
|
312
|
-
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
313
|
-
},
|
|
314
|
-
parts: [
|
|
315
|
-
{
|
|
316
|
-
id: `prt_revert_${revert.messageID}`,
|
|
317
|
-
sessionID: s.info.id,
|
|
318
|
-
messageID,
|
|
319
|
-
type: "text",
|
|
320
|
-
text: "Conversation reverted. Files were not changed.",
|
|
321
|
-
},
|
|
322
|
-
],
|
|
323
|
-
};
|
|
324
|
-
}
|
package/dist/sessions.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { TodoBridge, Todo, TodoUpdated } from "./todo-bridge.js";
|
|
|
10
10
|
import { type SessionTreeState } from "./session-tree.js";
|
|
11
11
|
import { SubagentBridge } from "./subagents.js";
|
|
12
12
|
import { SessionStore, type TitleSource } from "./session-store.js";
|
|
13
|
-
import type { OCAssistantMessage, OCEvent, OCMessageWithParts, OCModelRef, OCPart, OCSession, OCSessionStatus, OCToolPart } from "./types.js";
|
|
13
|
+
import type { OCAssistantMessage, OCEvent, OCMessageWithParts, OCModelRef, OCPart, OCPromptPart, OCSession, OCSessionStatus, OCToolPart } from "./types.js";
|
|
14
14
|
export { ADAPTER_VERSION } from "./version.js";
|
|
15
15
|
interface StreamingState {
|
|
16
16
|
message: OCAssistantMessage;
|
|
@@ -18,6 +18,7 @@ interface StreamingState {
|
|
|
18
18
|
parts: Map<number, OCPart>;
|
|
19
19
|
}
|
|
20
20
|
export interface ManagedSession extends ThinkingState {
|
|
21
|
+
interruptRequested?: boolean;
|
|
21
22
|
titleSource?: TitleSource;
|
|
22
23
|
activeUserMessageID?: string;
|
|
23
24
|
inputs?: Map<string, {
|
|
@@ -40,6 +41,9 @@ export interface ManagedSession extends ThinkingState {
|
|
|
40
41
|
/** undefined while dormant; resolved via get_state on create/revive. */
|
|
41
42
|
model: OCModelRef | undefined;
|
|
42
43
|
streaming: StreamingState | undefined;
|
|
44
|
+
/** A manual compaction started while idle; its end returns the session to idle. */
|
|
45
|
+
compactingFromIdle?: boolean;
|
|
46
|
+
compactionID?: string;
|
|
43
47
|
/** Cancels the adapter-owned user bash waiter and its OmO execution. */
|
|
44
48
|
cancelShell?: (interrupted?: boolean) => void;
|
|
45
49
|
/**
|
|
@@ -83,8 +87,14 @@ export declare class SessionManager {
|
|
|
83
87
|
/** freshness of the all-directories scan (see listAll). */
|
|
84
88
|
private scanAllCache;
|
|
85
89
|
constructor(emit: EventSink, store?: SessionStore, defaultDirectory?: string | undefined, lifecycleOptions?: LifecycleOptions);
|
|
86
|
-
create(directory: string, title?: string, parentID?: string
|
|
90
|
+
create(directory: string, title?: string, parentID?: string, options?: {
|
|
91
|
+
id?: string;
|
|
92
|
+
agent?: string;
|
|
93
|
+
model?: ModelSelection;
|
|
94
|
+
metadata?: Record<string, unknown>;
|
|
95
|
+
}): Promise<OCSession>;
|
|
87
96
|
private createLive;
|
|
97
|
+
private rollbackCreate;
|
|
88
98
|
private applyNativeTitle;
|
|
89
99
|
private refreshNativeTitles;
|
|
90
100
|
private wirePi;
|
|
@@ -109,23 +119,32 @@ export declare class SessionManager {
|
|
|
109
119
|
update(sessionId: string, patch: {
|
|
110
120
|
title?: string | undefined;
|
|
111
121
|
archived?: number | undefined;
|
|
112
|
-
|
|
122
|
+
metadata?: Record<string, unknown> | undefined;
|
|
123
|
+
agent?: string | undefined;
|
|
124
|
+
}): Promise<OCSession | undefined>;
|
|
113
125
|
messages(sessionId: string): OCMessageWithParts[] | undefined;
|
|
114
126
|
/** Append to canonical history, before the hidden redo branch while reverted. */
|
|
115
127
|
appendMessage(sessionId: string, message: OCMessageWithParts): void;
|
|
116
128
|
private operation;
|
|
129
|
+
/** Cancellation releases only the caller; the native action keeps its queue lease. */
|
|
130
|
+
private waitForOperation;
|
|
117
131
|
private syncTree;
|
|
118
132
|
private treeSession;
|
|
119
133
|
fork(sessionId: string, messageID?: string): Promise<OCSession>;
|
|
120
134
|
revert(sessionId: string, messageID: string): Promise<OCSession>;
|
|
121
135
|
unrevert(sessionId: string): Promise<OCSession>;
|
|
122
|
-
private removeRevertNotice;
|
|
123
136
|
reloadMessages(sessionId: string, signal?: AbortSignal): Promise<OCMessageWithParts[] | undefined>;
|
|
124
137
|
/** Called only within the conversation operation queue, including RPC refreshes. */
|
|
125
138
|
private reloadMessagesLive;
|
|
126
139
|
statuses(): Record<string, OCSessionStatus>;
|
|
127
140
|
todos(sessionId: string): Todo[] | undefined;
|
|
128
|
-
|
|
141
|
+
commitRevert(sessionId: string): Promise<void>;
|
|
142
|
+
private commitRevertLive;
|
|
143
|
+
diff(sessionId: string, options?: {
|
|
144
|
+
from?: string | undefined;
|
|
145
|
+
to?: string | undefined;
|
|
146
|
+
context?: number | undefined;
|
|
147
|
+
}): Promise<import("./session-diff.js").SessionFileDiff[]>;
|
|
129
148
|
private emitDiff;
|
|
130
149
|
abort(sessionId: string): Promise<boolean>;
|
|
131
150
|
permissions(sessionId?: string): PendingPermission[];
|
|
@@ -148,8 +167,17 @@ export declare class SessionManager {
|
|
|
148
167
|
* the next cold start, and tell the UI. Returns whether anything changed.
|
|
149
168
|
*/
|
|
150
169
|
private applyPiModel;
|
|
151
|
-
prompt(sessionId: string, text: string, model?: ModelSelection, noReply?: boolean, messageID?: string, delivery?: "steer" | "queue"): Promise<void>;
|
|
170
|
+
prompt(sessionId: string, text: string, model?: ModelSelection, noReply?: boolean, messageID?: string, delivery?: "steer" | "queue", parts?: readonly OCPromptPart[], metadata?: Record<string, unknown>): Promise<void>;
|
|
171
|
+
private preparePrompt;
|
|
172
|
+
private publishPrompt;
|
|
152
173
|
private promptLive;
|
|
174
|
+
/**
|
|
175
|
+
* No-reply context (v2 synthetic, v1 noReply) joins the native conversation without a turn:
|
|
176
|
+
* OmO appends it now when idle, or after the running turn, so the next prompt carries it.
|
|
177
|
+
*/
|
|
178
|
+
private admitContext;
|
|
179
|
+
private dispatchPrompt;
|
|
180
|
+
private bindPromptState;
|
|
153
181
|
disposeAll(): Promise<void>;
|
|
154
182
|
/** Spawn pi against the session's JSONL file and adopt it as live. */
|
|
155
183
|
private revive;
|