antechamber 1.0.40 → 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/CHANGELOG.md +29 -0
- 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/file-watch.d.ts +35 -0
- package/dist/file-watch.js +163 -0
- package/dist/misc-routes.js +12 -2
- package/dist/pi-event-translation.js +44 -2
- package/dist/route-services.d.ts +8 -1
- package/dist/routes-v2.js +251 -33
- package/dist/server.d.ts +16 -0
- package/dist/server.js +182 -6
- 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 +7 -0
- package/dist/session-store.js +28 -0
- package/dist/session-tree.d.ts +15 -1
- package/dist/sessions.d.ts +85 -1
- package/dist/sessions.js +238 -10
- package/dist/text-generation.d.ts +20 -0
- package/dist/text-generation.js +93 -0
- package/dist/types.d.ts +60 -0
- package/dist/v2.d.ts +8 -1
- package/dist/v2.js +24 -4
- package/extensions/omo-compat/package.json +1 -1
- package/extensions/omo-dag/package.json +1 -1
- package/extensions/omo-work/package.json +1 -1
- package/package.json +1 -1
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
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
|
@@ -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
|
package/dist/session-store.js
CHANGED
|
@@ -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;
|
package/dist/session-tree.d.ts
CHANGED
|
@@ -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;
|
package/dist/sessions.d.ts
CHANGED
|
@@ -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.
|