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.
- package/CHANGELOG.md +126 -0
- package/README.md +88 -141
- package/bin/opencode-antechamber +14 -4
- package/dist/approvals.d.ts +9 -1
- package/dist/approvals.js +10 -2
- package/dist/catalog.d.ts +13 -2
- package/dist/catalog.js +88 -52
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/commands/context.d.ts +5 -2
- package/dist/commands/context.js +8 -4
- package/dist/commands/doctor.js +4 -3
- package/dist/commands/setup.js +9 -28
- package/dist/config.d.ts +6 -2
- package/dist/config.js +18 -4
- package/dist/file-watch.d.ts +35 -0
- package/dist/file-watch.js +163 -0
- package/dist/latency.d.ts +10 -0
- package/dist/latency.js +115 -0
- package/dist/misc-routes.js +14 -12
- package/dist/pi-event-translation.js +44 -2
- package/dist/route-services.d.ts +8 -3
- package/dist/routes-v1.d.ts +1 -1
- package/dist/routes-v1.js +29 -418
- package/dist/routes-v2.js +252 -34
- package/dist/self-update.d.ts +2 -1
- package/dist/self-update.js +5 -1
- package/dist/server.d.ts +17 -3
- package/dist/server.js +191 -18
- package/dist/session-commands.d.ts +3 -1
- package/dist/session-commands.js +13 -7
- package/dist/session-history.js +3 -0
- package/dist/session-store.d.ts +19 -10
- package/dist/session-store.js +266 -128
- package/dist/session-tree.d.ts +15 -1
- package/dist/session-tree.js +2 -2
- package/dist/sessions.d.ts +85 -1
- package/dist/sessions.js +238 -10
- package/dist/sse.d.ts +0 -2
- package/dist/sse.js +13 -46
- package/dist/text-generation.d.ts +20 -0
- package/dist/text-generation.js +93 -0
- package/dist/tool-map.js +1 -3
- package/dist/types.d.ts +60 -0
- package/dist/v2.d.ts +8 -1
- package/dist/v2.js +24 -4
- package/dist/work-status.js +1 -1
- package/extensions/antechamber/README.md +100 -0
- package/extensions/antechamber/bin/opencode-antechamber +42 -0
- package/extensions/{omo-dag/docs/DESIGN.md → antechamber/docs/GRAPH.md} +5 -5
- package/extensions/{omo-work/docs/DESIGN.md → antechamber/docs/WORK.md} +3 -3
- package/extensions/antechamber/package.json +60 -0
- package/extensions/{omo-dag → antechamber}/panel/index.html +8 -1
- package/extensions/antechamber/panel/main.js +261 -0
- package/extensions/antechamber/runtime/launcher.js +201 -0
- package/extensions/antechamber/service/main.js +884 -0
- package/package.json +75 -26
- package/extensions/omo-compat/README.md +0 -48
- package/extensions/omo-compat/icon.svg +0 -1
- package/extensions/omo-compat/package.json +0 -43
- package/extensions/omo-dag/README.md +0 -167
- package/extensions/omo-dag/package.json +0 -33
- package/extensions/omo-dag/panel/main.js +0 -176
- package/extensions/omo-dag/service/main.js +0 -457
- package/extensions/omo-work/README.md +0 -88
- package/extensions/omo-work/package.json +0 -30
- package/extensions/omo-work/service/main.js +0 -149
- /package/extensions/{omo-dag → antechamber}/icon.svg +0 -0
- /package/extensions/{omo-work → antechamber}/status/index.html +0 -0
- /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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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,
|
|
262
|
+
handleSSE(req, res, directory) {
|
|
88
263
|
const lastEventID = req.headers["last-event-id"];
|
|
89
|
-
this.sse.handleSSE(res, {
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
package/dist/session-commands.js
CHANGED
|
@@ -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
|
-
|
|
114
|
+
if (client === undefined)
|
|
115
|
+
await pi.dispose();
|
|
115
116
|
}
|
|
116
117
|
})().then((commands) => {
|
|
117
|
-
|
|
118
|
+
if (commandCache.get(directory) === entry)
|
|
119
|
+
entry.expiresAt = Date.now() + COMMAND_CACHE_TTL_MS;
|
|
118
120
|
return commands;
|
|
119
121
|
}, (error) => {
|
|
120
|
-
commandCache.
|
|
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];
|
package/dist/session-history.js
CHANGED
|
@@ -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
|
{
|
package/dist/session-store.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
108
|
-
|
|
109
|
-
private
|
|
114
|
+
private db;
|
|
115
|
+
private version;
|
|
116
|
+
private readonly entries;
|
|
110
117
|
private batchState;
|
|
111
118
|
constructor(stateDir?: string);
|
|
112
|
-
private
|
|
113
|
-
private
|
|
114
|
-
private
|
|
115
|
-
private
|
|
116
|
-
private
|
|
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
|
-
/**
|
|
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;
|