antechamber 1.0.39 → 1.0.41

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/dist/server.js CHANGED
@@ -1,16 +1,23 @@
1
1
  /** HTTP/SSE composition root for the OpenCode API backed by pi. */
2
2
  import { createHash, randomBytes } from "node:crypto";
3
+ import { execFile } from "node:child_process";
4
+ import { watch } from "node:fs";
3
5
  import { createServer } from "node:http";
6
+ import { basename, dirname, resolve } from "node:path";
7
+ import { promisify } from "node:util";
4
8
  import { disposeMcpAgents, mcpSurfacesChanged } from "./mcp.js";
5
9
  import { debugLog } from "./misc-routes.js";
6
10
  import { SessionManager } from "./sessions.js";
7
- import { loadCatalog } from "./catalog.js";
11
+ import { catalogFingerprint, loadCatalog } from "./catalog.js";
8
12
  import { Http } from "./http.js";
9
13
  import { SSE } from "./sse.js";
10
14
  import { handleV1 } from "./routes-v1.js";
11
15
  import { handleV2 } from "./routes-v2.js";
12
16
  import { selfUpdate } from "./self-update.js";
13
17
  import { handleWorkStatus } from "./work-status.js";
18
+ import { FileWatchRegistry } from "./file-watch.js";
19
+ import { getCommands } from "./session-commands.js";
20
+ import { TextGeneration } from "./text-generation.js";
14
21
  export { isLoopbackHost, serverPassword } from "./http.js";
15
22
  export class AdapterServer {
16
23
  options;
@@ -22,26 +29,149 @@ export class AdapterServer {
22
29
  webSocketEvents = [];
23
30
  nextWebSocketEventID = 0;
24
31
  catalogPromise;
32
+ catalogExpiresAt = 0;
33
+ catalogColdExpiresAt = 0;
34
+ commandSnapshots = new Map();
35
+ commandColdReads = new Map();
36
+ branchSnapshots = new Map();
37
+ branchWatchers = new Map();
38
+ branchChecks = new Map();
39
+ projectDirectories = new Set();
40
+ fileWatches = new FileWatchRegistry((directory, event) => this.broadcast(directory, event));
41
+ pendingRefreshes = new Set();
42
+ textGeneration = new TextGeneration();
43
+ stopping = false;
25
44
  actualPort;
26
45
  /** Bearer token for the `/antechamber/work-status` routes, published in the run record for the OmO Work extension. */
27
46
  extensionToken = randomBytes(32).toString("base64url");
28
47
  http = new Http(() => this.url, this.extensionToken);
29
48
  constructor(options) {
30
49
  this.options = options;
50
+ this.projectDirectories.add(options.defaultDirectory);
31
51
  this.sse = new SSE(options, this.sseClients, (id) => this.sessions.monitors.owns(id));
32
52
  this.sessions = new SessionManager((directory, event) => this.broadcast(directory, event), undefined, options.defaultDirectory);
33
53
  this.sessions.onMcpSurfacesChanged = () => mcpSurfacesChanged((directory, event) => this.broadcast(directory, event));
34
54
  this.server = createServer((req, res) => void this.route(req, res));
35
55
  this.server.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head));
36
56
  }
37
- catalog() {
38
- this.catalogPromise ??= loadCatalog(this.options.defaultDirectory).catch((err) => {
39
- this.catalogPromise = undefined;
57
+ catalog(directory = this.options.defaultDirectory, client, mode = "cached") {
58
+ // A live session's agent can only refresh an existing snapshot: its get_state reports that session's
59
+ // selected model, so the first snapshot (and the configured default) always comes from a fresh agent.
60
+ const pi = this.catalogPromise === undefined ? undefined : (client ?? this.sessions.liveClient(directory));
61
+ if (this.catalogPromise !== undefined &&
62
+ (this.catalogExpiresAt === Infinity ||
63
+ (mode !== "settled" &&
64
+ (Date.now() < this.catalogExpiresAt ||
65
+ (pi === undefined && (mode === "cached" || Date.now() < this.catalogColdExpiresAt))))))
66
+ return this.catalogPromise;
67
+ const previous = this.catalogPromise;
68
+ this.catalogExpiresAt = Infinity;
69
+ this.catalogColdExpiresAt = Date.now() + 60_000;
70
+ this.catalogPromise = (pi === undefined || previous === undefined
71
+ ? loadCatalog(directory)
72
+ : previous.then((last) => loadCatalog(directory, pi, last)))
73
+ .then(async (current) => {
74
+ this.catalogExpiresAt = Date.now() + 5_000;
75
+ this.catalogColdExpiresAt = Date.now() + 60_000;
76
+ if (previous !== undefined) {
77
+ const last = await previous;
78
+ if (catalogFingerprint(last) !== catalogFingerprint(current)) {
79
+ this.broadcast(directory, { type: "provider.updated", properties: {} });
80
+ this.broadcast(directory, { type: "model.updated", properties: {} });
81
+ }
82
+ }
83
+ return current;
84
+ })
85
+ .catch((err) => {
86
+ this.catalogExpiresAt = Date.now() + 5_000;
40
87
  console.error("[catalog] failed to load models from pi:", err);
41
- return { providers: [], defaults: {} };
88
+ if (previous === undefined) {
89
+ // An unsuccessful first read established no snapshot to rate-limit or refresh.
90
+ this.catalogPromise = undefined;
91
+ this.catalogColdExpiresAt = 0;
92
+ }
93
+ return previous ?? { providers: [], defaults: {} };
42
94
  });
43
95
  return this.catalogPromise;
44
96
  }
97
+ async commands(directory, client, refresh = false) {
98
+ const pi = client ?? this.sessions.liveClient(directory);
99
+ const cold = this.commandColdReads.get(directory);
100
+ if (pi === undefined && cold !== undefined && Date.now() < cold.expiresAt)
101
+ return cold.promise;
102
+ const request = getCommands(directory, pi, refresh);
103
+ if (pi === undefined)
104
+ this.commandColdReads.set(directory, { expiresAt: Date.now() + 60_000, promise: request });
105
+ const commands = await request;
106
+ const previous = this.commandSnapshots.get(directory);
107
+ this.commandSnapshots.set(directory, commands);
108
+ this.commandColdReads.set(directory, { expiresAt: Date.now() + 60_000, promise: Promise.resolve(commands) });
109
+ if (previous !== undefined && JSON.stringify(previous) !== JSON.stringify(commands))
110
+ this.broadcast(directory, { type: "command.updated", properties: {} });
111
+ return commands;
112
+ }
113
+ watchBranch(directory) {
114
+ if (this.stopping)
115
+ return Promise.resolve(undefined);
116
+ const existing = this.branchWatchers.get(directory);
117
+ if (existing !== undefined)
118
+ return existing;
119
+ const pending = promisify(execFile)("git", ["rev-parse", "--git-path", "HEAD"], {
120
+ cwd: directory,
121
+ timeout: 3000,
122
+ })
123
+ .then(({ stdout }) => {
124
+ if (this.stopping)
125
+ return undefined;
126
+ const head = resolve(directory, stdout.trim());
127
+ const watcher = watch(dirname(head), (_event, name) => {
128
+ if (this.stopping || (name !== null && name.toString() !== basename(head)))
129
+ return;
130
+ void this.vcsBranch(directory);
131
+ });
132
+ watcher.on("error", () => {
133
+ watcher.close();
134
+ this.branchWatchers.delete(directory);
135
+ });
136
+ watcher.unref();
137
+ return watcher;
138
+ })
139
+ .catch(() => {
140
+ this.branchWatchers.delete(directory);
141
+ return undefined;
142
+ });
143
+ this.branchWatchers.set(directory, pending);
144
+ return pending;
145
+ }
146
+ vcsBranch(directory) {
147
+ const pending = this.branchChecks.get(directory) ?? Promise.resolve();
148
+ const current = pending.then(() => this.readBranch(directory));
149
+ this.branchChecks.set(directory, current.then(() => undefined));
150
+ return current;
151
+ }
152
+ async readBranch(directory) {
153
+ if (this.stopping)
154
+ return this.branchSnapshots.get(directory);
155
+ await this.watchBranch(directory);
156
+ let branch;
157
+ try {
158
+ const { stdout } = await promisify(execFile)("git", ["symbolic-ref", "--short", "HEAD"], {
159
+ cwd: directory,
160
+ timeout: 3000,
161
+ });
162
+ branch = stdout.trim() || undefined;
163
+ }
164
+ catch {
165
+ branch = undefined;
166
+ }
167
+ if (this.branchSnapshots.has(directory) && this.branchSnapshots.get(directory) !== branch)
168
+ this.broadcast(directory, {
169
+ type: "vcs.branch.updated",
170
+ properties: branch === undefined ? {} : { branch },
171
+ });
172
+ this.branchSnapshots.set(directory, branch);
173
+ return branch;
174
+ }
45
175
  async start() {
46
176
  await new Promise((resolve, reject) => {
47
177
  this.server.once("error", reject);
@@ -58,10 +188,20 @@ export class AdapterServer {
58
188
  return `http://${host}:${port}`;
59
189
  }
60
190
  async stop() {
191
+ this.stopping = true;
192
+ await this.textGeneration.dispose();
193
+ await Promise.all([...this.pendingRefreshes]);
194
+ const branchWatchers = [...this.branchWatchers.values()];
195
+ this.branchWatchers.clear();
196
+ await Promise.all(this.branchChecks.values());
197
+ for (const watcher of await Promise.all([...branchWatchers, ...this.branchWatchers.values()]))
198
+ watcher?.close();
199
+ this.branchWatchers.clear();
61
200
  // Stop admission before ending SSE: its clients can reconnect while the
62
201
  // session RPC groups and private task hosts are still being disposed.
63
202
  const closed = new Promise((resolve) => this.server.close(() => resolve()));
64
203
  this.sse.close();
204
+ this.fileWatches.close();
65
205
  for (const socket of this.webSocketClients)
66
206
  socket.destroy();
67
207
  this.webSocketClients.clear();
@@ -78,6 +218,38 @@ export class AdapterServer {
78
218
  // SSE
79
219
  // ---------------------------------------------------------------
80
220
  broadcast(directory, event) {
221
+ if (event.type === "session.idle" && !this.stopping) {
222
+ const session = this.sessions.get(event.properties.sessionID);
223
+ if (session?.pi !== undefined) {
224
+ const reads = [
225
+ ...(this.catalogPromise === undefined ? [] : [this.catalog(session.info.directory, session.pi, "settled")]),
226
+ ...(this.commandSnapshots.has(session.info.directory)
227
+ ? [this.commands(session.info.directory, session.pi, true)]
228
+ : []),
229
+ ];
230
+ if (reads.length > 0) {
231
+ const refresh = Promise.all(reads)
232
+ .then(() => undefined)
233
+ .catch((error) => console.error("[catalog] live refresh failed:", error));
234
+ this.pendingRefreshes.add(refresh);
235
+ void refresh.finally(() => this.pendingRefreshes.delete(refresh));
236
+ }
237
+ }
238
+ }
239
+ if (event.type === "session.created" && !this.projectDirectories.has(directory)) {
240
+ this.projectDirectories.add(directory);
241
+ const now = Date.now();
242
+ this.broadcast(directory, {
243
+ type: "project.updated",
244
+ properties: {
245
+ id: directory,
246
+ canonical: directory,
247
+ time: { created: now, updated: now, active: now },
248
+ sandboxes: [],
249
+ vcs: "git",
250
+ },
251
+ });
252
+ }
81
253
  this.sse.broadcast(directory, event);
82
254
  this.broadcastWebSocket(directory, event);
83
255
  }
@@ -211,9 +383,13 @@ export class AdapterServer {
211
383
  defaultDirectory: this.options.defaultDirectory,
212
384
  url: this.url,
213
385
  body: (request) => this.http.body(request),
214
- catalog: () => this.catalog(),
386
+ catalog: (dir, refresh) => this.catalog(dir, undefined, refresh === true ? "read" : "cached"),
215
387
  broadcast: (dir, event) => this.broadcast(dir, event),
216
388
  selfUpdate: this.options.selfUpdate ?? (() => selfUpdate()),
389
+ fileWatches: this.fileWatches,
390
+ commands: (dir) => this.commands(dir),
391
+ vcsBranch: (dir) => this.vcsBranch(dir),
392
+ textGeneration: this.textGeneration,
217
393
  };
218
394
  if (path === "/api" || path.startsWith("/api/")) {
219
395
  await handleV2(services, req, res, url, path, method, directory);
@@ -1,3 +1,4 @@
1
+ import { PiRpcClient } from "./pi-rpc.js";
1
2
  import type { OCPromptPart } from "./types.js";
2
3
  import type { ManagedSession, SessionManager } from "./sessions.js";
3
4
  import type { OCAssistantMessage, OCEvent, OCMessageWithParts, OCToolPart, OCTextPart, OCUserMessage } from "./types.js";
@@ -11,6 +12,7 @@ type SlashCommandInput = {
11
12
  arguments: string;
12
13
  messageID?: string;
13
14
  parts?: OCPromptPart[];
15
+ delivery?: "steer" | "queue";
14
16
  };
15
17
  type ShellResult = {
16
18
  output: string;
@@ -63,7 +65,7 @@ export declare const unsupportedShare: {
63
65
  };
64
66
  };
65
67
  export declare function shellTitle(command: string): string;
66
- export declare function getCommands(directory: string): Promise<CommandDescriptor[]>;
68
+ export declare function getCommands(directory: string, client?: PiRpcClient, refresh?: boolean): Promise<CommandDescriptor[]>;
67
69
  export declare function parseSlashCommand(value: unknown): SlashCommandInput | undefined;
68
70
  /**
69
71
  * OmO never auto-titles a first prompt that starts with "/", so a session opened
@@ -91,19 +91,19 @@ function mapCommands(data) {
91
91
  }
92
92
  const COMMAND_CACHE_TTL_MS = 5_000;
93
93
  const commandCache = new Map();
94
- export function getCommands(directory) {
94
+ export function getCommands(directory, client, refresh = false) {
95
95
  const now = Date.now();
96
96
  for (const [key, entry] of commandCache) {
97
97
  if (entry.expiresAt <= now)
98
98
  commandCache.delete(key);
99
99
  }
100
100
  const existing = commandCache.get(directory);
101
- if (existing)
101
+ if (existing && (!refresh || existing.expiresAt === Infinity))
102
102
  return existing.promise;
103
103
  const entry = {
104
104
  expiresAt: Infinity,
105
105
  promise: (async () => {
106
- const pi = new PiRpcClient({ cwd: directory, args: ["--no-session"] });
106
+ const pi = client ?? new PiRpcClient({ cwd: directory, args: ["--no-session"] });
107
107
  try {
108
108
  const response = await pi.command({ type: "get_commands" });
109
109
  if (!response.success)
@@ -111,13 +111,16 @@ export function getCommands(directory) {
111
111
  return mapCommands(response.data);
112
112
  }
113
113
  finally {
114
- await pi.dispose();
114
+ if (client === undefined)
115
+ await pi.dispose();
115
116
  }
116
117
  })().then((commands) => {
117
- entry.expiresAt = Date.now() + COMMAND_CACHE_TTL_MS;
118
+ if (commandCache.get(directory) === entry)
119
+ entry.expiresAt = Date.now() + COMMAND_CACHE_TTL_MS;
118
120
  return commands;
119
121
  }, (error) => {
120
- commandCache.delete(directory);
122
+ if (commandCache.get(directory) === entry)
123
+ commandCache.delete(directory);
121
124
  throw error;
122
125
  }),
123
126
  };
@@ -133,11 +136,14 @@ export function parseSlashCommand(value) {
133
136
  const messageID = typeof value.messageID === "string" && value.messageID !== "" ? value.messageID : undefined;
134
137
  const parts = parsePromptParts(value.parts);
135
138
  nativePromptInput("", parts);
139
+ if (value.delivery !== undefined && value.delivery !== "steer" && value.delivery !== "queue")
140
+ return undefined;
136
141
  return {
137
142
  command: value.command,
138
143
  arguments: value.arguments,
139
144
  ...(messageID ? { messageID } : {}),
140
145
  ...(parts.length > 0 ? { parts } : {}),
146
+ ...(value.delivery !== undefined ? { delivery: value.delivery } : {}),
141
147
  };
142
148
  }
143
149
  /**
@@ -168,7 +174,7 @@ function slashCommandPrompt(input) {
168
174
  }
169
175
  export async function runSlashCommand(sessions, sessionId, input) {
170
176
  const text = slashCommandPrompt(input);
171
- await sessions.prompt(sessionId, text, undefined, false, input.messageID, "steer", input.parts ? [{ type: "text", text }, ...input.parts] : undefined);
177
+ await sessions.prompt(sessionId, text, undefined, false, input.messageID, input.delivery ?? "steer", input.parts ? [{ type: "text", text }, ...input.parts] : undefined);
172
178
  await sessions.reloadMessages(sessionId);
173
179
  const messages = sessions.messages(sessionId) ?? [];
174
180
  return messages.findLast((message) => message.info.role === "assistant") ?? messages[messages.length - 1];
@@ -208,6 +208,9 @@ export function parsePiSessionText(s, agentName, raw) {
208
208
  agent: agentName,
209
209
  model: userMessageModel(currentModel, currentThinkingLevel),
210
210
  synthetic: true,
211
+ ...(s.tree.promptDescriptions?.[contextID] !== undefined
212
+ ? { description: s.tree.promptDescriptions[contextID] }
213
+ : {}),
211
214
  },
212
215
  parts: [
213
216
  {
@@ -39,6 +39,10 @@ export interface ScannedSession extends SessionSelection {
39
39
  updated: number;
40
40
  }
41
41
  export type TitleSource = "user" | "native" | "fallback" | "generated";
42
+ export interface InstructionEntryState {
43
+ value: unknown;
44
+ removed?: true;
45
+ }
42
46
  /** Adapter-owned metadata that pi's files do not carry. */
43
47
  export interface SessionRegistryEntry {
44
48
  /** Created by this connector, rather than discovered from another OmO client. */
@@ -50,6 +54,7 @@ export interface SessionRegistryEntry {
50
54
  parentMessageID?: string;
51
55
  agent?: string;
52
56
  metadata?: Record<string, unknown>;
57
+ instructions?: Record<string, InstructionEntryState>;
53
58
  task?: ChildTask;
54
59
  piSessionId: string;
55
60
  piPath: string;
@@ -58,6 +63,8 @@ export interface SessionRegistryEntry {
58
63
  titleSource?: TitleSource;
59
64
  /** Archive timestamp (absent/0 = live) — OpenChamber's archive toggle. */
60
65
  archived?: number;
66
+ /** Last idle watermark acknowledged by a viewer. */
67
+ viewed?: number;
61
68
  /**
62
69
  * Last model pi was known to run. pi's JSONL header does not carry it, so
63
70
  * without this a dormant session would advertise no model at all until it is
@@ -387,12 +387,31 @@ function sessionTreeState(value) {
387
387
  if (value.promptMetadata !== undefined &&
388
388
  (!record(value.promptMetadata) || !Object.values(value.promptMetadata).every((metadata) => record(metadata))))
389
389
  return false;
390
+ if (value.promptDescriptions !== undefined && !stringRecord(value.promptDescriptions))
391
+ return false;
392
+ if (value.compactionReasons !== undefined &&
393
+ (!stringRecord(value.compactionReasons) ||
394
+ !Object.values(value.compactionReasons).every((reason) => reason === "manual" || reason === "auto")))
395
+ return false;
390
396
  if (value.messageParts !== undefined &&
391
397
  (!record(value.messageParts) ||
392
398
  !Object.values(value.messageParts).every((parts) => Array.isArray(parts) && parts.every((part) => typeof part === "string"))))
393
399
  return false;
394
400
  if (value.shellDispatch !== undefined && !booleanRecord(value.shellDispatch))
395
401
  return false;
402
+ const finite = (item) => typeof item === "number" && Number.isFinite(item);
403
+ if (value.lastIdle !== undefined && !finite(value.lastIdle))
404
+ return false;
405
+ if (value.compactionAdmissions !== undefined &&
406
+ (!record(value.compactionAdmissions) || !Object.values(value.compactionAdmissions).every(finite)))
407
+ return false;
408
+ if (value.idleBoundaries !== undefined &&
409
+ (!record(value.idleBoundaries) ||
410
+ !Object.values(value.idleBoundaries).every((boundary) => record(boundary) &&
411
+ finite(boundary.at) &&
412
+ ["succeeded", "failed", "interrupted"].includes(boundary.outcome) &&
413
+ (boundary.error === undefined || record(boundary.error)))))
414
+ return false;
396
415
  if (value.leafId !== undefined && value.leafId !== null && typeof value.leafId !== "string")
397
416
  return false;
398
417
  if (value.fileSize !== undefined && (typeof value.fileSize !== "number" || !Number.isFinite(value.fileSize)))
@@ -448,6 +467,13 @@ function registryEntry(value) {
448
467
  return false;
449
468
  if (value.metadata !== undefined && !record(value.metadata))
450
469
  return false;
470
+ if (value.instructions !== undefined &&
471
+ (!record(value.instructions) ||
472
+ !Object.entries(value.instructions).every(([key, entry]) => /^[a-z0-9][a-z0-9._-]*$/.test(key) &&
473
+ record(entry) &&
474
+ "value" in entry &&
475
+ (entry.removed === undefined || entry.removed === true))))
476
+ return false;
451
477
  if (value.task !== undefined && !childTask(value.task))
452
478
  return false;
453
479
  if (value.title !== undefined && typeof value.title !== "string")
@@ -456,6 +482,8 @@ function registryEntry(value) {
456
482
  return false;
457
483
  if (value.archived !== undefined && (typeof value.archived !== "number" || !Number.isFinite(value.archived)))
458
484
  return false;
485
+ if (value.viewed !== undefined && (typeof value.viewed !== "number" || !Number.isFinite(value.viewed)))
486
+ return false;
459
487
  if (value.model !== undefined &&
460
488
  (!record(value.model) || typeof value.model.providerID !== "string" || typeof value.model.modelID !== "string"))
461
489
  return false;
@@ -1,6 +1,6 @@
1
1
  import { type PiRpcClient } from "./pi-rpc.js";
2
2
  import type { ManagedSession } from "./sessions.js";
3
- import type { OCMessageWithParts } from "./types.js";
3
+ import type { OCAssistantMessage, OCMessageWithParts, OCSession } from "./types.js";
4
4
  import type { OCPromptPart } from "./types.js";
5
5
  export interface SessionTreeState {
6
6
  messageEntries: Record<string, string>;
@@ -10,6 +10,10 @@ export interface SessionTreeState {
10
10
  promptParts?: Record<string, OCPromptPart[]>;
11
11
  /** Client prompt metadata; native RPC does not store it. */
12
12
  promptMetadata?: Record<string, Record<string, unknown>>;
13
+ /** Client-provided synthetic notice labels; native RPC stores only the context text. */
14
+ promptDescriptions?: Record<string, string>;
15
+ /** Native compaction reasons by the durable compaction entry ID. */
16
+ compactionReasons?: Record<string, "manual" | "auto">;
13
17
  /** Actual transport dispatch, not inferred from a card's terminal state. */
14
18
  shellDispatch?: Record<string, boolean>;
15
19
  /** Identity-only dispatched admissions; never rendered without a native entry. */
@@ -25,6 +29,16 @@ export interface SessionTreeState {
25
29
  previousLeafId: string | null;
26
30
  targetId: string | null;
27
31
  };
32
+ /** Execution boundaries observed over RPC, retained with the durable message identities. */
33
+ idleBoundaries?: Record<string, {
34
+ at: number;
35
+ outcome: NonNullable<OCSession["outcome"]>;
36
+ error?: NonNullable<OCAssistantMessage["error"]>;
37
+ }>;
38
+ /** The last idle time, restored as time.idle so /view accepts a watermark seen before a restart. */
39
+ lastIdle?: number;
40
+ /** When each compaction was admitted, keyed by its admitted message ID. */
41
+ compactionAdmissions?: Record<string, number>;
28
42
  }
29
43
  export interface TreeEntry {
30
44
  type: string;
@@ -12,7 +12,7 @@ import { SubagentBridge } from "./subagents.js";
12
12
  import { MonitorSessions } from "./monitor-sessions.js";
13
13
  import { WorkStatusBridge } from "./work-status.js";
14
14
  import { OmoShells } from "./omo-shells.js";
15
- import { SessionStore, type TitleSource } from "./session-store.js";
15
+ import { SessionStore, type InstructionEntryState, type TitleSource } from "./session-store.js";
16
16
  import type { OCAssistantMessage, OCEvent, OCMessageWithParts, OCModelRef, OCPart, OCPromptPart, OCSession, OCSessionStatus, OCToolPart } from "./types.js";
17
17
  export { ADAPTER_VERSION } from "./version.js";
18
18
  interface StreamingState {
@@ -33,6 +33,9 @@ export interface ManagedSession extends ThinkingState {
33
33
  accepted: boolean;
34
34
  delivered: boolean;
35
35
  }>;
36
+ instructions?: Record<string, InstructionEntryState>;
37
+ /** Suppress model-selection events until the session creation record has its final model. */
38
+ initializing?: boolean;
36
39
  tree: SessionTreeState;
37
40
  info: OCSession;
38
41
  /** undefined while dormant (restored from disk, no pi process spawned yet). */
@@ -51,6 +54,8 @@ export interface ManagedSession extends ThinkingState {
51
54
  /** A manual compaction started while idle; its end returns the session to idle. */
52
55
  compactingFromIdle?: boolean;
53
56
  compactionID?: string;
57
+ /** The native reason of the running compaction, for its failure event. */
58
+ compactionReason?: string;
54
59
  /** Retries sessionRpc still owes a `compact` command that OmO cancels on its own. */
55
60
  compactRetry?: {
56
61
  left: number;
@@ -142,6 +147,8 @@ export declare class SessionManager {
142
147
  * global/recent view; `list(directory)` stays the per-project source of truth.
143
148
  */
144
149
  listAll(): OCSession[];
150
+ /** Reuse an already-running agent for directory-scoped catalog reads. Never revives a dormant session. */
151
+ liveClient(directory: string): PiRpcClient | undefined;
145
152
  private sessionInfo;
146
153
  get(sessionId: string): ManagedSession | undefined;
147
154
  remove(sessionId: string): Promise<boolean>;
@@ -158,7 +165,77 @@ export declare class SessionManager {
158
165
  metadata?: Record<string, unknown> | undefined;
159
166
  agent?: string | undefined;
160
167
  }): Promise<OCSession | undefined>;
168
+ /** Mark only the idle boundary that the caller actually observed. */
169
+ view(sessionId: string, idle: number): void;
170
+ putInstruction(sessionId: string, key: string, value: unknown): void;
171
+ removeInstruction(sessionId: string, key: string): void;
161
172
  messages(sessionId: string): OCMessageWithParts[] | undefined;
173
+ compaction(sessionId: string, messageID: string): {
174
+ id: string;
175
+ type: "compaction";
176
+ time: {
177
+ created: number;
178
+ };
179
+ status: "completed";
180
+ reason: "auto" | "manual";
181
+ summary: string;
182
+ } | {
183
+ id: string;
184
+ type: "compaction";
185
+ time: {
186
+ created: number;
187
+ };
188
+ status: "running";
189
+ reason: "manual";
190
+ summary: string;
191
+ } | undefined;
192
+ /** Synchronous admission owns the ID before any native work can yield to another request. */
193
+ admitCompaction(sessionId: string, messageID: string): {
194
+ kind: "conflict";
195
+ item?: never;
196
+ } | {
197
+ kind: "existing";
198
+ item: {
199
+ id: string;
200
+ type: "compaction";
201
+ time: {
202
+ created: number;
203
+ };
204
+ status: "completed";
205
+ reason: "auto" | "manual";
206
+ summary: string;
207
+ } | {
208
+ id: string;
209
+ type: "compaction";
210
+ time: {
211
+ created: number;
212
+ };
213
+ status: "running";
214
+ reason: "manual";
215
+ summary: string;
216
+ };
217
+ } | {
218
+ kind: "admitted";
219
+ item: {
220
+ id: string;
221
+ type: "compaction";
222
+ time: {
223
+ created: number;
224
+ };
225
+ status: "completed";
226
+ reason: "auto" | "manual";
227
+ summary: string;
228
+ } | {
229
+ id: string;
230
+ type: "compaction";
231
+ time: {
232
+ created: number;
233
+ };
234
+ status: "running";
235
+ reason: "manual";
236
+ summary: string;
237
+ };
238
+ };
162
239
  /** Append to canonical history, before the hidden redo branch while reverted. */
163
240
  appendMessage(sessionId: string, message: OCMessageWithParts): void;
164
241
  private operation;
@@ -212,6 +289,13 @@ export declare class SessionManager {
212
289
  private preparePrompt;
213
290
  private publishPrompt;
214
291
  private promptLive;
292
+ /**
293
+ * What the model has is read from the active native branch, not stored beside it: each delivery carries the
294
+ * full delivered map in `details`, so after a revert, unrevert, fork or compaction the last delivery still in
295
+ * context says exactly what to send again. Deliveries before a compaction's kept range are no longer in context
296
+ * (OpenCode rewinds its instruction state the same way, core/session/projector.ts).
297
+ */
298
+ private deliverInstructions;
215
299
  /**
216
300
  * No-reply context (v2 synthetic, v1 noReply) joins the native conversation without a turn:
217
301
  * OmO appends it now when idle, or after the running turn, so the next prompt carries it.