antechamber 0.1.1 → 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 +28 -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 +9 -5
- package/dist/pi-event-translation.js +233 -79
- package/dist/pi-rpc.d.ts +20 -2
- package/dist/pi-rpc.js +96 -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 +12 -15
- package/dist/session-commands.js +52 -11
- package/dist/session-diff.d.ts +1 -1
- package/dist/session-diff.js +10 -2
- package/dist/session-history.js +108 -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 -1
- package/dist/session-tree.js +51 -26
- package/dist/sessions.d.ts +32 -5
- package/dist/sessions.js +330 -119
- package/dist/sse.d.ts +16 -6
- package/dist/sse.js +77 -21
- package/dist/subagents.d.ts +5 -0
- package/dist/subagents.js +84 -71
- package/dist/tool-map.d.ts +3 -2
- package/dist/types.d.ts +152 -1
- package/dist/v2.d.ts +18 -4
- package/dist/v2.js +128 -44
- 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-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;
|
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)) {
|
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, {
|
|
@@ -42,6 +43,7 @@ export interface ManagedSession extends ThinkingState {
|
|
|
42
43
|
streaming: StreamingState | undefined;
|
|
43
44
|
/** A manual compaction started while idle; its end returns the session to idle. */
|
|
44
45
|
compactingFromIdle?: boolean;
|
|
46
|
+
compactionID?: string;
|
|
45
47
|
/** Cancels the adapter-owned user bash waiter and its OmO execution. */
|
|
46
48
|
cancelShell?: (interrupted?: boolean) => void;
|
|
47
49
|
/**
|
|
@@ -85,8 +87,14 @@ export declare class SessionManager {
|
|
|
85
87
|
/** freshness of the all-directories scan (see listAll). */
|
|
86
88
|
private scanAllCache;
|
|
87
89
|
constructor(emit: EventSink, store?: SessionStore, defaultDirectory?: string | undefined, lifecycleOptions?: LifecycleOptions);
|
|
88
|
-
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>;
|
|
89
96
|
private createLive;
|
|
97
|
+
private rollbackCreate;
|
|
90
98
|
private applyNativeTitle;
|
|
91
99
|
private refreshNativeTitles;
|
|
92
100
|
private wirePi;
|
|
@@ -111,11 +119,15 @@ export declare class SessionManager {
|
|
|
111
119
|
update(sessionId: string, patch: {
|
|
112
120
|
title?: string | undefined;
|
|
113
121
|
archived?: number | undefined;
|
|
114
|
-
|
|
122
|
+
metadata?: Record<string, unknown> | undefined;
|
|
123
|
+
agent?: string | undefined;
|
|
124
|
+
}): Promise<OCSession | undefined>;
|
|
115
125
|
messages(sessionId: string): OCMessageWithParts[] | undefined;
|
|
116
126
|
/** Append to canonical history, before the hidden redo branch while reverted. */
|
|
117
127
|
appendMessage(sessionId: string, message: OCMessageWithParts): void;
|
|
118
128
|
private operation;
|
|
129
|
+
/** Cancellation releases only the caller; the native action keeps its queue lease. */
|
|
130
|
+
private waitForOperation;
|
|
119
131
|
private syncTree;
|
|
120
132
|
private treeSession;
|
|
121
133
|
fork(sessionId: string, messageID?: string): Promise<OCSession>;
|
|
@@ -126,7 +138,13 @@ export declare class SessionManager {
|
|
|
126
138
|
private reloadMessagesLive;
|
|
127
139
|
statuses(): Record<string, OCSessionStatus>;
|
|
128
140
|
todos(sessionId: string): Todo[] | undefined;
|
|
129
|
-
|
|
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[]>;
|
|
130
148
|
private emitDiff;
|
|
131
149
|
abort(sessionId: string): Promise<boolean>;
|
|
132
150
|
permissions(sessionId?: string): PendingPermission[];
|
|
@@ -149,8 +167,17 @@ export declare class SessionManager {
|
|
|
149
167
|
* the next cold start, and tell the UI. Returns whether anything changed.
|
|
150
168
|
*/
|
|
151
169
|
private applyPiModel;
|
|
152
|
-
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;
|
|
153
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;
|
|
154
181
|
disposeAll(): Promise<void>;
|
|
155
182
|
/** Spawn pi against the session's JSONL file and adopt it as live. */
|
|
156
183
|
private revive;
|