antechamber 1.0.40 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/README.md +88 -141
  3. package/bin/opencode-antechamber +14 -4
  4. package/dist/approvals.d.ts +9 -1
  5. package/dist/approvals.js +10 -2
  6. package/dist/catalog.d.ts +13 -2
  7. package/dist/catalog.js +88 -52
  8. package/dist/cli.d.ts +1 -1
  9. package/dist/cli.js +1 -1
  10. package/dist/commands/context.d.ts +5 -2
  11. package/dist/commands/context.js +8 -4
  12. package/dist/commands/doctor.js +4 -3
  13. package/dist/commands/setup.js +9 -28
  14. package/dist/config.d.ts +6 -2
  15. package/dist/config.js +18 -4
  16. package/dist/file-watch.d.ts +35 -0
  17. package/dist/file-watch.js +163 -0
  18. package/dist/latency.d.ts +10 -0
  19. package/dist/latency.js +115 -0
  20. package/dist/misc-routes.js +14 -12
  21. package/dist/pi-event-translation.js +44 -2
  22. package/dist/route-services.d.ts +8 -3
  23. package/dist/routes-v1.d.ts +1 -1
  24. package/dist/routes-v1.js +29 -418
  25. package/dist/routes-v2.js +252 -34
  26. package/dist/self-update.d.ts +2 -1
  27. package/dist/self-update.js +5 -1
  28. package/dist/server.d.ts +17 -3
  29. package/dist/server.js +191 -18
  30. package/dist/session-commands.d.ts +3 -1
  31. package/dist/session-commands.js +13 -7
  32. package/dist/session-history.js +3 -0
  33. package/dist/session-store.d.ts +19 -10
  34. package/dist/session-store.js +266 -128
  35. package/dist/session-tree.d.ts +15 -1
  36. package/dist/session-tree.js +2 -2
  37. package/dist/sessions.d.ts +85 -1
  38. package/dist/sessions.js +238 -10
  39. package/dist/sse.d.ts +0 -2
  40. package/dist/sse.js +13 -46
  41. package/dist/text-generation.d.ts +20 -0
  42. package/dist/text-generation.js +93 -0
  43. package/dist/tool-map.js +1 -3
  44. package/dist/types.d.ts +60 -0
  45. package/dist/v2.d.ts +8 -1
  46. package/dist/v2.js +24 -4
  47. package/dist/work-status.js +1 -1
  48. package/extensions/antechamber/README.md +100 -0
  49. package/extensions/antechamber/bin/opencode-antechamber +42 -0
  50. package/extensions/{omo-dag/docs/DESIGN.md → antechamber/docs/GRAPH.md} +5 -5
  51. package/extensions/{omo-work/docs/DESIGN.md → antechamber/docs/WORK.md} +3 -3
  52. package/extensions/antechamber/package.json +60 -0
  53. package/extensions/{omo-dag → antechamber}/panel/index.html +8 -1
  54. package/extensions/antechamber/panel/main.js +261 -0
  55. package/extensions/antechamber/runtime/launcher.js +201 -0
  56. package/extensions/antechamber/service/main.js +884 -0
  57. package/package.json +75 -26
  58. package/extensions/omo-compat/README.md +0 -48
  59. package/extensions/omo-compat/icon.svg +0 -1
  60. package/extensions/omo-compat/package.json +0 -43
  61. package/extensions/omo-dag/README.md +0 -167
  62. package/extensions/omo-dag/package.json +0 -33
  63. package/extensions/omo-dag/panel/main.js +0 -176
  64. package/extensions/omo-dag/service/main.js +0 -457
  65. package/extensions/omo-work/README.md +0 -88
  66. package/extensions/omo-work/package.json +0 -30
  67. package/extensions/omo-work/service/main.js +0 -149
  68. /package/extensions/{omo-dag → antechamber}/icon.svg +0 -0
  69. /package/extensions/{omo-work → antechamber}/status/index.html +0 -0
  70. /package/extensions/{omo-work → antechamber}/status/main.js +0 -0
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
- import { selfUpdate } from "./self-update.js";
13
16
  import { handleWorkStatus } from "./work-status.js";
17
+ import { FileWatchRegistry } from "./file-watch.js";
18
+ import { getCommands } from "./session-commands.js";
19
+ import { TextGeneration } from "./text-generation.js";
20
+ import { LatencyMonitor } from "./latency.js";
14
21
  export { isLoopbackHost, serverPassword } from "./http.js";
15
22
  export class AdapterServer {
16
23
  options;
@@ -22,26 +29,150 @@ 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
+ latency = new LatencyMonitor();
44
+ stopping = false;
25
45
  actualPort;
26
46
  /** Bearer token for the `/antechamber/work-status` routes, published in the run record for the OmO Work extension. */
27
47
  extensionToken = randomBytes(32).toString("base64url");
28
48
  http = new Http(() => this.url, this.extensionToken);
29
49
  constructor(options) {
30
50
  this.options = options;
51
+ this.projectDirectories.add(options.defaultDirectory);
31
52
  this.sse = new SSE(options, this.sseClients, (id) => this.sessions.monitors.owns(id));
32
53
  this.sessions = new SessionManager((directory, event) => this.broadcast(directory, event), undefined, options.defaultDirectory);
33
54
  this.sessions.onMcpSurfacesChanged = () => mcpSurfacesChanged((directory, event) => this.broadcast(directory, event));
34
55
  this.server = createServer((req, res) => void this.route(req, res));
35
56
  this.server.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head));
36
57
  }
37
- catalog() {
38
- this.catalogPromise ??= loadCatalog(this.options.defaultDirectory).catch((err) => {
39
- this.catalogPromise = undefined;
58
+ catalog(directory = this.options.defaultDirectory, client, mode = "cached") {
59
+ // A live session's agent can only refresh an existing snapshot: its get_state reports that session's
60
+ // selected model, so the first snapshot (and the configured default) always comes from a fresh agent.
61
+ const pi = this.catalogPromise === undefined ? undefined : (client ?? this.sessions.liveClient(directory));
62
+ if (this.catalogPromise !== undefined &&
63
+ (this.catalogExpiresAt === Infinity ||
64
+ (mode !== "settled" &&
65
+ (Date.now() < this.catalogExpiresAt ||
66
+ (pi === undefined && (mode === "cached" || Date.now() < this.catalogColdExpiresAt))))))
67
+ return this.catalogPromise;
68
+ const previous = this.catalogPromise;
69
+ this.catalogExpiresAt = Infinity;
70
+ this.catalogColdExpiresAt = Date.now() + 60_000;
71
+ this.catalogPromise = (pi === undefined || previous === undefined
72
+ ? loadCatalog(directory)
73
+ : previous.then((last) => loadCatalog(directory, pi, last)))
74
+ .then(async (current) => {
75
+ this.catalogExpiresAt = Date.now() + 5_000;
76
+ this.catalogColdExpiresAt = Date.now() + 60_000;
77
+ if (previous !== undefined) {
78
+ const last = await previous;
79
+ if (catalogFingerprint(last) !== catalogFingerprint(current)) {
80
+ this.broadcast(directory, { type: "provider.updated", properties: {} });
81
+ this.broadcast(directory, { type: "model.updated", properties: {} });
82
+ }
83
+ }
84
+ return current;
85
+ })
86
+ .catch((err) => {
87
+ this.catalogExpiresAt = Date.now() + 5_000;
40
88
  console.error("[catalog] failed to load models from pi:", err);
41
- return { providers: [], defaults: {} };
89
+ if (previous === undefined) {
90
+ // An unsuccessful first read established no snapshot to rate-limit or refresh.
91
+ this.catalogPromise = undefined;
92
+ this.catalogColdExpiresAt = 0;
93
+ }
94
+ return previous ?? { providers: [], defaults: {} };
42
95
  });
43
96
  return this.catalogPromise;
44
97
  }
98
+ async commands(directory, client, refresh = false) {
99
+ const pi = client ?? this.sessions.liveClient(directory);
100
+ const cold = this.commandColdReads.get(directory);
101
+ if (pi === undefined && cold !== undefined && Date.now() < cold.expiresAt)
102
+ return cold.promise;
103
+ const request = getCommands(directory, pi, refresh);
104
+ if (pi === undefined)
105
+ this.commandColdReads.set(directory, { expiresAt: Date.now() + 60_000, promise: request });
106
+ const commands = await request;
107
+ const previous = this.commandSnapshots.get(directory);
108
+ this.commandSnapshots.set(directory, commands);
109
+ this.commandColdReads.set(directory, { expiresAt: Date.now() + 60_000, promise: Promise.resolve(commands) });
110
+ if (previous !== undefined && JSON.stringify(previous) !== JSON.stringify(commands))
111
+ this.broadcast(directory, { type: "command.updated", properties: {} });
112
+ return commands;
113
+ }
114
+ watchBranch(directory) {
115
+ if (this.stopping)
116
+ return Promise.resolve(undefined);
117
+ const existing = this.branchWatchers.get(directory);
118
+ if (existing !== undefined)
119
+ return existing;
120
+ const pending = promisify(execFile)("git", ["rev-parse", "--git-path", "HEAD"], {
121
+ cwd: directory,
122
+ timeout: 3000,
123
+ })
124
+ .then(({ stdout }) => {
125
+ if (this.stopping)
126
+ return undefined;
127
+ const head = resolve(directory, stdout.trim());
128
+ const watcher = watch(dirname(head), (_event, name) => {
129
+ if (this.stopping || (name !== null && name.toString() !== basename(head)))
130
+ return;
131
+ void this.vcsBranch(directory);
132
+ });
133
+ watcher.on("error", () => {
134
+ watcher.close();
135
+ this.branchWatchers.delete(directory);
136
+ });
137
+ watcher.unref();
138
+ return watcher;
139
+ })
140
+ .catch(() => {
141
+ this.branchWatchers.delete(directory);
142
+ return undefined;
143
+ });
144
+ this.branchWatchers.set(directory, pending);
145
+ return pending;
146
+ }
147
+ vcsBranch(directory) {
148
+ const pending = this.branchChecks.get(directory) ?? Promise.resolve();
149
+ const current = pending.then(() => this.readBranch(directory));
150
+ this.branchChecks.set(directory, current.then(() => undefined));
151
+ return current;
152
+ }
153
+ async readBranch(directory) {
154
+ if (this.stopping)
155
+ return this.branchSnapshots.get(directory);
156
+ await this.watchBranch(directory);
157
+ let branch;
158
+ try {
159
+ const { stdout } = await promisify(execFile)("git", ["symbolic-ref", "--short", "HEAD"], {
160
+ cwd: directory,
161
+ timeout: 3000,
162
+ });
163
+ branch = stdout.trim() || undefined;
164
+ }
165
+ catch {
166
+ branch = undefined;
167
+ }
168
+ if (this.branchSnapshots.has(directory) && this.branchSnapshots.get(directory) !== branch)
169
+ this.broadcast(directory, {
170
+ type: "vcs.branch.updated",
171
+ properties: branch === undefined ? {} : { branch },
172
+ });
173
+ this.branchSnapshots.set(directory, branch);
174
+ return branch;
175
+ }
45
176
  async start() {
46
177
  await new Promise((resolve, reject) => {
47
178
  this.server.once("error", reject);
@@ -49,6 +180,7 @@ export class AdapterServer {
49
180
  });
50
181
  const addr = this.server.address();
51
182
  this.actualPort = typeof addr === "object" && addr !== null ? addr.port : this.options.port;
183
+ this.latency.start();
52
184
  }
53
185
  get url() {
54
186
  const port = this.actualPort ?? this.options.port;
@@ -58,10 +190,21 @@ export class AdapterServer {
58
190
  return `http://${host}:${port}`;
59
191
  }
60
192
  async stop() {
193
+ this.stopping = true;
194
+ this.latency.stop();
195
+ await this.textGeneration.dispose();
196
+ await Promise.all([...this.pendingRefreshes]);
197
+ const branchWatchers = [...this.branchWatchers.values()];
198
+ this.branchWatchers.clear();
199
+ await Promise.all(this.branchChecks.values());
200
+ for (const watcher of await Promise.all([...branchWatchers, ...this.branchWatchers.values()]))
201
+ watcher?.close();
202
+ this.branchWatchers.clear();
61
203
  // Stop admission before ending SSE: its clients can reconnect while the
62
204
  // session RPC groups and private task hosts are still being disposed.
63
205
  const closed = new Promise((resolve) => this.server.close(() => resolve()));
64
206
  this.sse.close();
207
+ this.fileWatches.close();
65
208
  for (const socket of this.webSocketClients)
66
209
  socket.destroy();
67
210
  this.webSocketClients.clear();
@@ -78,15 +221,47 @@ export class AdapterServer {
78
221
  // SSE
79
222
  // ---------------------------------------------------------------
80
223
  broadcast(directory, event) {
224
+ if (event.type === "session.idle" && !this.stopping) {
225
+ const session = this.sessions.get(event.properties.sessionID);
226
+ if (session?.pi !== undefined) {
227
+ const reads = [
228
+ ...(this.catalogPromise === undefined ? [] : [this.catalog(session.info.directory, session.pi, "settled")]),
229
+ ...(this.commandSnapshots.has(session.info.directory)
230
+ ? [this.commands(session.info.directory, session.pi, true)]
231
+ : []),
232
+ ];
233
+ if (reads.length > 0) {
234
+ const refresh = Promise.all(reads)
235
+ .then(() => undefined)
236
+ .catch((error) => console.error("[catalog] live refresh failed:", error));
237
+ this.pendingRefreshes.add(refresh);
238
+ void refresh.finally(() => this.pendingRefreshes.delete(refresh));
239
+ }
240
+ }
241
+ }
242
+ if (event.type === "session.created" && !this.projectDirectories.has(directory)) {
243
+ this.projectDirectories.add(directory);
244
+ const now = Date.now();
245
+ this.broadcast(directory, {
246
+ type: "project.updated",
247
+ properties: {
248
+ id: directory,
249
+ canonical: directory,
250
+ time: { created: now, updated: now, active: now },
251
+ sandboxes: [],
252
+ vcs: "git",
253
+ },
254
+ });
255
+ }
81
256
  this.sse.broadcast(directory, event);
82
257
  this.broadcastWebSocket(directory, event);
83
258
  }
84
259
  writeSSE(res, frame) {
85
260
  return this.sse.writeSSE(res, frame);
86
261
  }
87
- handleSSE(req, res, flavor, directory, global) {
262
+ handleSSE(req, res, directory) {
88
263
  const lastEventID = req.headers["last-event-id"];
89
- this.sse.handleSSE(res, { flavor, global, ...(global ? {} : { directory }) }, Array.isArray(lastEventID) ? lastEventID[0] : lastEventID, this.writeSSE.bind(this));
264
+ this.sse.handleSSE(res, { ...(directory !== undefined ? { directory } : {}) }, Array.isArray(lastEventID) ? lastEventID[0] : lastEventID, this.writeSSE.bind(this));
90
265
  }
91
266
  handleUpgrade(req, socket, _head) {
92
267
  const url = new URL(req.url ?? "/", this.url);
@@ -170,7 +345,7 @@ export class AdapterServer {
170
345
  // ---------------------------------------------------------------
171
346
  async route(req, res) {
172
347
  debugLog(`[http] ${req.method} ${(req.url ?? "/").split("?")[0]}`);
173
- await this.http.route(req, res, () => this.handle(req, res));
348
+ await this.latency.request(req, res, () => this.http.route(req, res, () => this.handle(req, res)));
174
349
  }
175
350
  async handle(req, res) {
176
351
  const url = new URL(req.url ?? "/", this.url);
@@ -191,15 +366,10 @@ export class AdapterServer {
191
366
  headerDirectory = decodeURIComponent(encodedDirectory);
192
367
  }
193
368
  const directory = directoryQuery ?? headerDirectory ?? this.options.defaultDirectory;
194
- // SSE stream (v2 clients subscribe at /api/event; v1 at /event)
195
- if (method === "GET" && (path === "/event" || path === "/global/event")) {
196
- this.handleSSE(req, res, "v1", directory, path === "/global/event");
197
- return;
198
- }
199
- if (method === "GET" && path === "/api/event") {
369
+ if (method === "GET" && (path === "/api/event" || path === "/api/global/event")) {
200
370
  // OpenChamber 2.3.2 opens one unscoped /api/event as its global hub (captured);
201
371
  // only an explicit directory selector scopes the stream.
202
- this.handleSSE(req, res, "v2", directory, directoryQuery === null && headerDirectory === undefined);
372
+ this.handleSSE(req, res, directoryQuery === null && headerDirectory === undefined ? undefined : directory);
203
373
  return;
204
374
  }
205
375
  if (path.startsWith("/antechamber/work-status/")) {
@@ -211,9 +381,12 @@ export class AdapterServer {
211
381
  defaultDirectory: this.options.defaultDirectory,
212
382
  url: this.url,
213
383
  body: (request) => this.http.body(request),
214
- catalog: () => this.catalog(),
384
+ catalog: (dir, refresh) => this.catalog(dir, undefined, refresh === true ? "read" : "cached"),
215
385
  broadcast: (dir, event) => this.broadcast(dir, event),
216
- selfUpdate: this.options.selfUpdate ?? (() => selfUpdate()),
386
+ fileWatches: this.fileWatches,
387
+ commands: (dir) => this.commands(dir),
388
+ vcsBranch: (dir) => this.vcsBranch(dir),
389
+ textGeneration: this.textGeneration,
217
390
  };
218
391
  if (path === "/api" || path.startsWith("/api/")) {
219
392
  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
  {
@@ -6,7 +6,7 @@
6
6
  * so the adapter never needs its own transcript store — only:
7
7
  *
8
8
  * 1. a scan of that directory to rebuild the sidebar after a restart, and
9
- * 2. a small registry JSON for adapter-owned metadata that pi does not
9
+ * 2. a SQLite registry for adapter-owned metadata that pi does not
10
10
  * keep (titles, conversation cursors and child-task metadata).
11
11
  *
12
12
  * OpenChamber session ids for restored sessions are derived deterministically
@@ -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
@@ -104,23 +111,25 @@ export declare function scanPiSessions(directory: string): ScannedSession[];
104
111
  export declare class SessionStore {
105
112
  readonly stateDir: string;
106
113
  private readonly file;
107
- private entries;
108
- /** The registry bytes `entries` was parsed from or last written as; equal bytes on disk mean the cache is current. */
109
- private raw;
114
+ private db;
115
+ private version;
116
+ private readonly entries;
110
117
  private batchState;
111
118
  constructor(stateDir?: string);
112
- private load;
113
- private committed;
114
- private readRaw;
115
- private parseEntries;
116
- private withRegistryLock;
119
+ private database;
120
+ private migrate;
121
+ private upsert;
122
+ private writeRow;
123
+ private refresh;
124
+ private read;
117
125
  private persist;
118
126
  private mutate;
119
- /** Synchronous restoration reads its own changes, then merges them under one short-lived write lock. */
127
+ /** Stage only touched rows; replay partial updates against the latest rows under SQLite's write lock. */
120
128
  batch<T>(action: () => T): T;
121
129
  get(ocSessionId: string): SessionRegistryEntry | undefined;
122
130
  idFor(piSessionId: string): string | undefined;
123
131
  childIds(): string[];
132
+ private matching;
124
133
  set(ocSessionId: string, entry: SessionRegistryEntry): void;
125
134
  /** Merge a partial update (e.g. title override, updated timestamp). */
126
135
  update(ocSessionId: string, patch: Partial<SessionRegistryEntry>): void;