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.
Files changed (56) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +17 -14
  3. package/bin/opencode-antechamber +10 -1
  4. package/bin/opencode-pi +11 -1
  5. package/dist/approvals.d.ts +4 -1
  6. package/dist/approvals.js +51 -33
  7. package/dist/catalog.d.ts +1 -1
  8. package/dist/catalog.js +1 -1
  9. package/dist/commands/context.d.ts +0 -1
  10. package/dist/commands/context.js +1 -1
  11. package/dist/commands/doctor.d.ts +3 -2
  12. package/dist/commands/serve.d.ts +2 -1
  13. package/dist/commands/setup.d.ts +2 -3
  14. package/dist/commands/setup.js +16 -4
  15. package/dist/http.d.ts +8 -1
  16. package/dist/http.js +42 -19
  17. package/dist/pi-event-translation.d.ts +21 -3
  18. package/dist/pi-event-translation.js +317 -65
  19. package/dist/pi-rpc.d.ts +26 -2
  20. package/dist/pi-rpc.js +102 -29
  21. package/dist/prompt-parts.d.ts +40 -0
  22. package/dist/prompt-parts.js +157 -0
  23. package/dist/route-services.d.ts +45 -2
  24. package/dist/route-services.js +70 -3
  25. package/dist/routes-v1.js +110 -65
  26. package/dist/routes-v2.js +376 -73
  27. package/dist/server.d.ts +9 -1
  28. package/dist/server.js +109 -9
  29. package/dist/session-commands.d.ts +20 -15
  30. package/dist/session-commands.js +61 -11
  31. package/dist/session-diff.d.ts +1 -1
  32. package/dist/session-diff.js +10 -2
  33. package/dist/session-history.js +113 -42
  34. package/dist/session-hosts.js +2 -2
  35. package/dist/session-store.d.ts +8 -4
  36. package/dist/session-store.js +183 -33
  37. package/dist/session-tree.d.ts +9 -2
  38. package/dist/session-tree.js +51 -63
  39. package/dist/sessions.d.ts +34 -6
  40. package/dist/sessions.js +340 -146
  41. package/dist/sse.d.ts +16 -6
  42. package/dist/sse.js +77 -21
  43. package/dist/subagents.d.ts +5 -0
  44. package/dist/subagents.js +86 -69
  45. package/dist/tool-map.d.ts +3 -2
  46. package/dist/types.d.ts +164 -1
  47. package/dist/v2.d.ts +18 -4
  48. package/dist/v2.js +141 -50
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/extensions/omo-dag/README.md +2 -0
  52. package/extensions/omo-dag/docs/DESIGN.md +10 -1
  53. package/extensions/omo-dag/panel/index.html +1 -1
  54. package/extensions/omo-dag/panel/main.js +60 -60
  55. package/extensions/omo-dag/service/main.js +72 -8
  56. package/package.json +1 -1
@@ -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)))
@@ -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). */
@@ -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, 60);
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 = new Map();
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
- for (const [k, v] of Object.entries(raw)) {
280
- if (v && typeof v.piPath === "string" && typeof v.piSessionId === "string") {
281
- // A hand-edited or partially written registry must not be able to
282
- // poison the model a dormant session advertises.
283
- if (v.model !== undefined &&
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 this.entries;
398
+ return entries;
295
399
  }
296
- persist(entries) {
297
- const obj = Object.fromEntries(entries);
400
+ withRegistryLock(action) {
401
+ const lock = `${this.file}.lock`;
402
+ let fd;
298
403
  try {
299
- mkdirSync(join(this.file, ".."), { recursive: true });
300
- const tmp = `${this.file}.${process.pid}.tmp`;
301
- writeFileSync(tmp, JSON.stringify(obj, null, 2));
302
- renameSync(tmp, this.file);
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
- this.entries = entries;
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
- const entries = new Map(this.load());
319
- entries.set(ocSessionId, entry);
320
- this.persist(entries);
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
- const map = this.load();
325
- const prev = map.get(ocSessionId);
326
- if (prev === undefined)
327
- return;
328
- this.set(ocSessionId, { ...prev, ...patch });
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
- const entries = new Map(this.load());
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() {
@@ -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;
@@ -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
- /** Live ids belong to the UI; restored ids belong to JSONL offsets. Save both. */
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
- const branch = branchEntries(tree, s.tree.revert ? s.tree.revert.previousLeafId : leafId);
74
- const positions = new Map(branch.map((entry, index) => [entry.id, index]));
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 = s.tree.localShellTurns
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
- const text = entry.message?.content
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 = s.tree.localShellTurns.filter((turn) => s.tree.messageEntries[turn.messages[1].info.id] === undefined);
218
+ s.tree.localShellTurns = localShellTurns.filter((turn) => s.tree.messageEntries[turn.messages[1].info.id] === undefined);
194
219
  }
195
- export function entryForMessage(s, tree, messageID) {
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
- }
@@ -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): Promise<OCSession>;
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
- }): OCSession | undefined;
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
- diff(sessionId: string): Promise<import("./session-diff.js").SessionFileDiff[]>;
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;