antechamber 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +17 -14
- package/bin/opencode-antechamber +10 -1
- package/bin/opencode-pi +11 -1
- package/dist/approvals.d.ts +4 -1
- package/dist/approvals.js +51 -33
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +1 -1
- package/dist/commands/context.d.ts +0 -1
- package/dist/commands/context.js +1 -1
- package/dist/commands/doctor.d.ts +3 -2
- package/dist/commands/serve.d.ts +2 -1
- package/dist/commands/setup.d.ts +2 -3
- package/dist/commands/setup.js +16 -4
- package/dist/http.d.ts +8 -1
- package/dist/http.js +42 -19
- package/dist/pi-event-translation.d.ts +9 -5
- package/dist/pi-event-translation.js +233 -79
- package/dist/pi-rpc.d.ts +20 -2
- package/dist/pi-rpc.js +96 -29
- package/dist/prompt-parts.d.ts +40 -0
- package/dist/prompt-parts.js +157 -0
- package/dist/route-services.d.ts +45 -2
- package/dist/route-services.js +70 -3
- package/dist/routes-v1.js +110 -65
- package/dist/routes-v2.js +376 -73
- package/dist/server.d.ts +9 -1
- package/dist/server.js +109 -9
- package/dist/session-commands.d.ts +12 -15
- package/dist/session-commands.js +52 -11
- package/dist/session-diff.d.ts +1 -1
- package/dist/session-diff.js +10 -2
- package/dist/session-history.js +108 -42
- package/dist/session-hosts.js +2 -2
- package/dist/session-store.d.ts +8 -4
- package/dist/session-store.js +183 -33
- package/dist/session-tree.d.ts +9 -1
- package/dist/session-tree.js +51 -26
- package/dist/sessions.d.ts +32 -5
- package/dist/sessions.js +330 -119
- package/dist/sse.d.ts +16 -6
- package/dist/sse.js +77 -21
- package/dist/subagents.d.ts +5 -0
- package/dist/subagents.js +84 -71
- package/dist/tool-map.d.ts +3 -2
- package/dist/types.d.ts +152 -1
- package/dist/v2.d.ts +18 -4
- package/dist/v2.js +128 -44
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/omo-dag/README.md +2 -0
- package/extensions/omo-dag/docs/DESIGN.md +10 -1
- package/extensions/omo-dag/panel/index.html +1 -1
- package/extensions/omo-dag/panel/main.js +60 -60
- package/extensions/omo-dag/service/main.js +72 -8
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { applyProviderToolResults, finishProviderToolParts, mapTool, providerToolParts, resultMetadata, resultText, } from "./tool-map.js";
|
|
4
4
|
import { TodoBridge } from "./todo-bridge.js";
|
|
5
|
-
import { recordTurn } from "./session-tree.js";
|
|
5
|
+
import { readSessionTree, recordTurn } from "./session-tree.js";
|
|
6
6
|
import { touchedFiles } from "./session-diff.js";
|
|
7
7
|
import { getAgentName } from "./agent-name.js";
|
|
8
8
|
import { debugLog } from "./misc-routes.js";
|
|
@@ -42,7 +42,7 @@ export function textOf(content) {
|
|
|
42
42
|
return "";
|
|
43
43
|
return content.map((b) => (b?.type === "text" && typeof b.text === "string" ? b.text : "")).join("");
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
function newPart(kind, sessionID, messageID) {
|
|
46
46
|
const base = {
|
|
47
47
|
id: id("prt"),
|
|
48
48
|
sessionID,
|
|
@@ -54,7 +54,7 @@ export function newPart(kind, sessionID, messageID) {
|
|
|
54
54
|
? { ...base, type: "text" }
|
|
55
55
|
: { ...base, type: "reasoning" };
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
function applyUsage(msg, usage) {
|
|
58
58
|
if (!usage)
|
|
59
59
|
return;
|
|
60
60
|
msg.tokens = {
|
|
@@ -75,9 +75,17 @@ export function syncSessionCost(s) {
|
|
|
75
75
|
if (s.messages === undefined)
|
|
76
76
|
return false;
|
|
77
77
|
let cost = 0;
|
|
78
|
+
const tokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } };
|
|
78
79
|
for (const message of s.messages)
|
|
79
|
-
if (message.info.role === "assistant")
|
|
80
|
+
if (message.info.role === "assistant") {
|
|
80
81
|
cost += message.info.cost;
|
|
82
|
+
tokens.input += message.info.tokens.input;
|
|
83
|
+
tokens.output += message.info.tokens.output;
|
|
84
|
+
tokens.reasoning += message.info.tokens.reasoning;
|
|
85
|
+
tokens.cache.read += message.info.tokens.cache.read;
|
|
86
|
+
tokens.cache.write += message.info.tokens.cache.write;
|
|
87
|
+
}
|
|
88
|
+
s.info.tokens = tokens;
|
|
81
89
|
if ((s.info.cost ?? 0) === cost)
|
|
82
90
|
return false; // absent means zero; avoid no-op updates
|
|
83
91
|
s.info.cost = cost;
|
|
@@ -86,9 +94,8 @@ export function syncSessionCost(s) {
|
|
|
86
94
|
/**
|
|
87
95
|
* An OmO compaction as OpenCode shows one: a user turn holding a `compaction`
|
|
88
96
|
* part (OpenChamber renders it as `/compact`) answered by a `summary` message.
|
|
89
|
-
* IDs derive from the compaction
|
|
90
|
-
*
|
|
91
|
-
* reconciles with the live messages instead of duplicating them.
|
|
97
|
+
* IDs derive from the native compaction entry, not its retained boundary:
|
|
98
|
+
* multiple compactions can retain the same entry.
|
|
92
99
|
*/
|
|
93
100
|
export function compactionMessages(s, key, summary, created, model) {
|
|
94
101
|
const sessionID = s.info.id;
|
|
@@ -135,6 +142,33 @@ export class PiEventTranslator {
|
|
|
135
142
|
constructor(services) {
|
|
136
143
|
this.services = services;
|
|
137
144
|
}
|
|
145
|
+
/** Close dead-runtime UI state before an error or idle event can reach the client. */
|
|
146
|
+
finishInterruptedRun(s, error) {
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
s.approvals?.finishTurn();
|
|
149
|
+
for (const part of s.streaming?.parts.values() ?? []) {
|
|
150
|
+
if (part.type === "tool")
|
|
151
|
+
s.toolByCallId.set(part.callID, part);
|
|
152
|
+
else if ((part.type === "text" || part.type === "reasoning") &&
|
|
153
|
+
part.time !== undefined &&
|
|
154
|
+
part.time.end === undefined) {
|
|
155
|
+
part.time.end = now;
|
|
156
|
+
this.emitPart(s, part);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
this.finalizeDanglingTools(s);
|
|
160
|
+
this.services.subagents.finishCalls(s.info.id);
|
|
161
|
+
if (s.streaming !== undefined) {
|
|
162
|
+
const message = s.streaming.message;
|
|
163
|
+
message.finish = "error";
|
|
164
|
+
message.error = error;
|
|
165
|
+
message.time.completed = now;
|
|
166
|
+
this.services.emit(s.info.directory, { type: "message.updated", properties: { info: message } });
|
|
167
|
+
this.finishStreaming(s);
|
|
168
|
+
}
|
|
169
|
+
delete s.compactingFromIdle;
|
|
170
|
+
delete s.info.time.compacting;
|
|
171
|
+
}
|
|
138
172
|
setStatus(s, status) {
|
|
139
173
|
s.status = status;
|
|
140
174
|
this.services.emit(s.info.directory, {
|
|
@@ -142,6 +176,14 @@ export class PiEventTranslator {
|
|
|
142
176
|
properties: { sessionID: s.info.id, status },
|
|
143
177
|
});
|
|
144
178
|
if (status.type === "idle") {
|
|
179
|
+
s.info.time.idle = Date.now();
|
|
180
|
+
const last = [...(s.messages ?? [])].reverse().find((message) => message.info.role === "assistant");
|
|
181
|
+
s.info.outcome =
|
|
182
|
+
s.interruptRequested || (last?.info.role === "assistant" && last.info.error?.name === "MessageAbortedError")
|
|
183
|
+
? "interrupted"
|
|
184
|
+
: last?.info.role === "assistant" && last.info.finish === "error"
|
|
185
|
+
? "failed"
|
|
186
|
+
: "succeeded";
|
|
145
187
|
this.services.emit(s.info.directory, { type: "session.idle", properties: { sessionID: s.info.id } });
|
|
146
188
|
}
|
|
147
189
|
}
|
|
@@ -206,28 +248,59 @@ export class PiEventTranslator {
|
|
|
206
248
|
}
|
|
207
249
|
onPiEvent(s, ev) {
|
|
208
250
|
this.services.subagents.wake(s, ev);
|
|
209
|
-
const dir = s.info.directory;
|
|
210
251
|
if (envSetting("DEBUG")) {
|
|
211
252
|
const ame = ev.assistantMessageEvent?.type;
|
|
212
253
|
debugLog(`[pi-event] ${String(ev.type)}${ame ? `/${ame}` : ""} ${JSON.stringify(ev).slice(0, 300)}`);
|
|
213
254
|
}
|
|
214
255
|
switch (ev.type) {
|
|
215
|
-
case "entry_appended":
|
|
216
|
-
|
|
217
|
-
const event = s.todoBridge.entry(ev.entry, (part) => this.emitPart(s, part));
|
|
218
|
-
if (event !== undefined)
|
|
219
|
-
this.services.emit(dir, event);
|
|
256
|
+
case "entry_appended":
|
|
257
|
+
this.onTodoEntry(s, ev);
|
|
220
258
|
break;
|
|
221
|
-
}
|
|
222
259
|
case "session_info_changed":
|
|
223
260
|
this.services.applyNativeTitle(s, ev.name);
|
|
224
261
|
break;
|
|
225
262
|
case "agent_start":
|
|
226
|
-
|
|
263
|
+
case "agent_idle":
|
|
264
|
+
case "agent_end":
|
|
265
|
+
case "auto_retry_start":
|
|
266
|
+
case "turn_end":
|
|
267
|
+
case "extension_error":
|
|
268
|
+
this.onLifecycleEvent(s, ev);
|
|
269
|
+
break;
|
|
270
|
+
case "tool_execution_start":
|
|
271
|
+
case "tool_execution_update":
|
|
272
|
+
case "tool_execution_end":
|
|
273
|
+
this.onToolEvent(s, ev);
|
|
227
274
|
break;
|
|
228
|
-
case "
|
|
275
|
+
case "message_start":
|
|
276
|
+
case "message_update":
|
|
277
|
+
case "message_end":
|
|
278
|
+
this.onMessageEvent(s, ev);
|
|
279
|
+
break;
|
|
280
|
+
case "compaction_start":
|
|
281
|
+
case "compaction_end":
|
|
282
|
+
this.onCompactionEvent(s, ev);
|
|
283
|
+
break;
|
|
284
|
+
default:
|
|
285
|
+
break; // turn_start, queue_update — future milestones
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
onTodoEntry(s, ev) {
|
|
289
|
+
s.todoBridge ??= new TodoBridge(s.info.id);
|
|
290
|
+
const event = s.todoBridge.entry(ev.entry, (part) => this.emitPart(s, part));
|
|
291
|
+
if (event !== undefined)
|
|
292
|
+
this.services.emit(s.info.directory, event);
|
|
293
|
+
}
|
|
294
|
+
onLifecycleEvent(s, ev) {
|
|
295
|
+
switch (ev.type) {
|
|
296
|
+
case "agent_start":
|
|
297
|
+
this.setStatus(s, { type: "busy" });
|
|
298
|
+
return;
|
|
299
|
+
case "agent_end":
|
|
229
300
|
if (ev.willRetry === true)
|
|
230
|
-
|
|
301
|
+
return; // auto-retry keeps the session busy
|
|
302
|
+
if (s.interruptRequested)
|
|
303
|
+
this.finishInterruptedRun(s, { name: "MessageAbortedError", data: { message: "Cancelled" } });
|
|
231
304
|
s.approvals?.finishTurn();
|
|
232
305
|
this.finalizeDanglingTools(s);
|
|
233
306
|
this.services.subagents.finishCalls(s.info.id);
|
|
@@ -235,18 +308,28 @@ export class PiEventTranslator {
|
|
|
235
308
|
this.services.emitDiff(s);
|
|
236
309
|
s.info.time.updated = Date.now();
|
|
237
310
|
this.services.persistMeta(s);
|
|
238
|
-
this.services.emit(
|
|
311
|
+
this.services.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
|
|
239
312
|
if (s.piPath !== undefined)
|
|
240
313
|
this.services.store.update(s.info.id, { updated: Date.now() });
|
|
241
314
|
recordTurn(s);
|
|
242
315
|
this.services.persistMeta(s);
|
|
243
|
-
|
|
244
|
-
}
|
|
316
|
+
return;
|
|
245
317
|
case "agent_idle":
|
|
246
318
|
// agent_end closes one run, not the session: OmO may still retry or
|
|
247
319
|
// start a deferred continuation. Only agent_idle settles that work.
|
|
320
|
+
if (s.interruptRequested) {
|
|
321
|
+
this.finishInterruptedRun(s, { name: "MessageAbortedError", data: { message: "Cancelled" } });
|
|
322
|
+
this.services.emit(s.info.directory, {
|
|
323
|
+
type: "session.error",
|
|
324
|
+
properties: {
|
|
325
|
+
sessionID: s.info.id,
|
|
326
|
+
error: { name: "MessageAbortedError", data: { message: "Cancelled" } },
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
delete s.interruptRequested;
|
|
330
|
+
}
|
|
248
331
|
this.setStatus(s, { type: "idle" });
|
|
249
|
-
|
|
332
|
+
return;
|
|
250
333
|
case "turn_end": {
|
|
251
334
|
s.approvals?.finishTurn();
|
|
252
335
|
// pi delivers tool results here, matched back to parts by toolCallId.
|
|
@@ -254,18 +337,41 @@ export class PiEventTranslator {
|
|
|
254
337
|
if (Array.isArray(results))
|
|
255
338
|
this.applyToolResults(s, results);
|
|
256
339
|
this.services.emitDiff(s);
|
|
257
|
-
|
|
340
|
+
return;
|
|
258
341
|
}
|
|
342
|
+
case "auto_retry_start":
|
|
343
|
+
this.setStatus(s, {
|
|
344
|
+
type: "retry",
|
|
345
|
+
attempt: Number(ev.attempt ?? 1),
|
|
346
|
+
message: String(ev.errorMessage ?? "retrying"),
|
|
347
|
+
next: Date.now() + Number(ev.delayMs ?? 0),
|
|
348
|
+
});
|
|
349
|
+
return;
|
|
350
|
+
case "extension_error":
|
|
351
|
+
this.services.emit(s.info.directory, {
|
|
352
|
+
type: "session.error",
|
|
353
|
+
properties: {
|
|
354
|
+
sessionID: s.info.id,
|
|
355
|
+
error: { name: "UnknownError", data: { message: String(ev.error ?? "pi extension error") } },
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
return;
|
|
359
|
+
default:
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
onToolEvent(s, ev) {
|
|
364
|
+
switch (ev.type) {
|
|
259
365
|
case "tool_execution_start": {
|
|
260
366
|
if (typeof ev.toolCallId === "string" && typeof ev.toolName === "string") {
|
|
261
367
|
s.approvals?.noteToolStart(ev.toolCallId, ev.toolName, ev.args);
|
|
262
368
|
}
|
|
263
369
|
if (ev.toolName === "task")
|
|
264
|
-
|
|
370
|
+
return; // Expanded when the argument stream closes.
|
|
265
371
|
const callID = typeof ev.toolCallId === "string" ? ev.toolCallId : undefined;
|
|
266
372
|
const part = callID !== undefined ? s.toolByCallId.get(callID) : undefined;
|
|
267
373
|
if (part === undefined)
|
|
268
|
-
|
|
374
|
+
return;
|
|
269
375
|
const name = typeof ev.toolName === "string" ? ev.toolName : part.tool;
|
|
270
376
|
const mapped = mapTool(name, name === "todo" ? part.state.input : ev.args);
|
|
271
377
|
part.tool = mapped.tool;
|
|
@@ -277,31 +383,37 @@ export class PiEventTranslator {
|
|
|
277
383
|
time: { start: part.state.status !== "pending" ? part.state.time.start : Date.now() },
|
|
278
384
|
};
|
|
279
385
|
this.emitPart(s, part);
|
|
280
|
-
|
|
386
|
+
return;
|
|
281
387
|
}
|
|
282
388
|
case "tool_execution_update": {
|
|
283
389
|
if (typeof ev.toolCallId === "string" &&
|
|
284
390
|
this.services.subagents.result(s, ev.toolCallId, ev.partialResult !== null && typeof ev.partialResult === "object" ? ev.partialResult : {}, false))
|
|
285
|
-
|
|
391
|
+
return;
|
|
286
392
|
const part = typeof ev.toolCallId === "string" ? s.toolByCallId.get(ev.toolCallId) : undefined;
|
|
287
393
|
if (part === undefined || part.state.status !== "running")
|
|
288
|
-
|
|
394
|
+
return;
|
|
289
395
|
this.emitPart(s, part);
|
|
290
|
-
|
|
396
|
+
return;
|
|
291
397
|
}
|
|
292
398
|
case "tool_execution_end": {
|
|
293
399
|
if (typeof ev.toolCallId === "string")
|
|
294
400
|
s.approvals?.noteToolEnd(ev.toolCallId);
|
|
295
401
|
const result = ev.result !== null && typeof ev.result === "object" ? ev.result : {};
|
|
296
402
|
this.applyToolResults(s, [{ ...result, toolCallId: ev.toolCallId, isError: ev.isError }]);
|
|
297
|
-
|
|
403
|
+
return;
|
|
298
404
|
}
|
|
405
|
+
default:
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
onMessageEvent(s, ev) {
|
|
410
|
+
switch (ev.type) {
|
|
299
411
|
case "message_start": {
|
|
300
412
|
if (this.reconcileUser(s, ev))
|
|
301
|
-
|
|
413
|
+
return;
|
|
302
414
|
const msg = ev.message;
|
|
303
415
|
if (msg?.role !== "assistant")
|
|
304
|
-
|
|
416
|
+
return; // user message already recorded at prompt()
|
|
305
417
|
// pi stamps the assistant message with the model really serving it.
|
|
306
418
|
// Trust that over our snapshot, which goes stale whenever pi switches on
|
|
307
419
|
// its own side (subagent routing, an in-session /model, a resumed file).
|
|
@@ -324,91 +436,121 @@ export class PiEventTranslator {
|
|
|
324
436
|
};
|
|
325
437
|
s.streaming = { message: assistant, parts: new Map() };
|
|
326
438
|
s.messages?.push({ info: assistant, parts: [] });
|
|
327
|
-
this.services.emit(
|
|
328
|
-
|
|
439
|
+
this.services.emit(s.info.directory, { type: "message.updated", properties: { info: assistant } });
|
|
440
|
+
return;
|
|
329
441
|
}
|
|
330
442
|
case "message_update":
|
|
331
443
|
this.onMessageUpdate(s, ev);
|
|
332
|
-
|
|
444
|
+
return;
|
|
333
445
|
case "message_end": {
|
|
334
446
|
if (this.reconcileUser(s, ev))
|
|
335
|
-
|
|
447
|
+
return;
|
|
336
448
|
const ended = ev.message;
|
|
337
|
-
if (ended?.role
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
// Last chance to correct attribution: some providers only reveal the
|
|
344
|
-
// concrete model once the stream has closed.
|
|
345
|
-
if (this.services.applyPiModel(s, ended.provider, piModelId(ended.responseModel, ended.model)) &&
|
|
346
|
-
s.model !== undefined) {
|
|
347
|
-
s.streaming.message.providerID = s.model.providerID;
|
|
348
|
-
s.streaming.message.modelID = s.model.modelID;
|
|
349
|
-
}
|
|
350
|
-
applyUsage(s.streaming.message, ended.usage);
|
|
351
|
-
Object.assign(s.streaming.message, piStopReason(ended.stopReason, ended.errorMessage));
|
|
352
|
-
s.streaming.message.time.completed = Date.now();
|
|
353
|
-
this.services.emit(dir, { type: "message.updated", properties: { info: s.streaming.message } });
|
|
354
|
-
this.finishStreaming(s);
|
|
355
|
-
if (syncSessionCost(s))
|
|
356
|
-
this.services.emit(dir, { type: "session.updated", properties: { info: s.info } });
|
|
449
|
+
if (ended?.role !== "assistant" || !s.streaming)
|
|
450
|
+
return;
|
|
451
|
+
this.onProviderTools(s, ev.message);
|
|
452
|
+
if (ended.stopReason === "aborted" || ended.stopReason === "error") {
|
|
453
|
+
for (const part of finishProviderToolParts(s.toolByCallId.values(), Date.now(), ended.stopReason === "aborted" ? "Cancelled" : "No result"))
|
|
454
|
+
this.emitPart(s, part);
|
|
357
455
|
}
|
|
358
|
-
|
|
456
|
+
// Last chance to correct attribution: some providers only reveal the
|
|
457
|
+
// concrete model once the stream has closed.
|
|
458
|
+
if (this.services.applyPiModel(s, ended.provider, piModelId(ended.responseModel, ended.model)) &&
|
|
459
|
+
s.model !== undefined) {
|
|
460
|
+
s.streaming.message.providerID = s.model.providerID;
|
|
461
|
+
s.streaming.message.modelID = s.model.modelID;
|
|
462
|
+
}
|
|
463
|
+
applyUsage(s.streaming.message, ended.usage);
|
|
464
|
+
Object.assign(s.streaming.message, piStopReason(ended.stopReason, ended.errorMessage));
|
|
465
|
+
s.streaming.message.time.completed = Date.now();
|
|
466
|
+
this.services.emit(s.info.directory, { type: "message.updated", properties: { info: s.streaming.message } });
|
|
467
|
+
this.finishStreaming(s);
|
|
468
|
+
if (syncSessionCost(s))
|
|
469
|
+
this.services.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
|
|
470
|
+
return;
|
|
359
471
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
type: "session.error",
|
|
472
|
+
default:
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
onCompactionEvent(s, ev) {
|
|
477
|
+
switch (ev.type) {
|
|
478
|
+
case "compaction_start":
|
|
479
|
+
s.compactionID = id("compaction");
|
|
480
|
+
this.services.emit(s.info.directory, {
|
|
481
|
+
type: "session.compaction.started",
|
|
371
482
|
properties: {
|
|
372
483
|
sessionID: s.info.id,
|
|
373
|
-
|
|
484
|
+
inputID: s.compactionID,
|
|
485
|
+
reason: typeof ev.reason === "string" ? ev.reason : "manual",
|
|
374
486
|
},
|
|
375
487
|
});
|
|
376
|
-
break;
|
|
377
|
-
case "compaction_start": {
|
|
378
488
|
s.info.time.compacting = Date.now();
|
|
379
|
-
this.services.emit(
|
|
489
|
+
this.services.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
|
|
380
490
|
// A manual /compact runs no agent turn, so nothing else marks the session busy.
|
|
381
491
|
if (s.status.type === "idle") {
|
|
382
492
|
s.compactingFromIdle = true;
|
|
383
493
|
this.setStatus(s, { type: "busy" });
|
|
384
494
|
}
|
|
385
|
-
|
|
386
|
-
}
|
|
495
|
+
return;
|
|
387
496
|
case "compaction_end": {
|
|
388
497
|
delete s.info.time.compacting;
|
|
389
498
|
const result = ev.result;
|
|
499
|
+
const inputID = s.compactionID ?? id("compaction");
|
|
500
|
+
delete s.compactionID;
|
|
501
|
+
this.services.emit(s.info.directory, ev.aborted === true || typeof result?.summary !== "string"
|
|
502
|
+
? {
|
|
503
|
+
type: "session.compaction.failed",
|
|
504
|
+
properties: {
|
|
505
|
+
sessionID: s.info.id,
|
|
506
|
+
inputID,
|
|
507
|
+
reason: typeof ev.reason === "string" ? ev.reason : "manual",
|
|
508
|
+
error: {
|
|
509
|
+
name: "CompactionError",
|
|
510
|
+
data: { message: ev.aborted === true ? "Compaction cancelled" : "Compaction failed" },
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
}
|
|
514
|
+
: {
|
|
515
|
+
type: "session.compaction.ended",
|
|
516
|
+
properties: {
|
|
517
|
+
sessionID: s.info.id,
|
|
518
|
+
inputID,
|
|
519
|
+
reason: typeof ev.reason === "string" ? ev.reason : "manual",
|
|
520
|
+
text: result.summary,
|
|
521
|
+
recent: "",
|
|
522
|
+
},
|
|
523
|
+
});
|
|
390
524
|
if (ev.aborted !== true && typeof result?.summary === "string" && typeof result.firstKeptEntryId === "string") {
|
|
391
525
|
const messages = this.services.ensureMessages(s);
|
|
392
|
-
|
|
526
|
+
let key;
|
|
527
|
+
// compaction_end follows persistence, but its result omits the entry ID.
|
|
528
|
+
if (s.piPath !== undefined) {
|
|
529
|
+
for (const entry of readSessionTree(s.piPath).entries) {
|
|
530
|
+
if (entry.type === "compaction")
|
|
531
|
+
key = entry.id;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
for (const message of compactionMessages(s, key ?? id("compaction"), result.summary, Date.now(), s.model)) {
|
|
393
535
|
if (messages.some((item) => item.info.id === message.info.id))
|
|
394
536
|
continue;
|
|
395
537
|
// Keep a hidden redo branch after the new turn, as appendMessage does.
|
|
396
538
|
const boundary = messages.findIndex((item) => item.info.id === s.tree.revert?.messageID);
|
|
397
539
|
messages.splice(boundary < 0 ? messages.length : boundary, 0, message);
|
|
398
|
-
this.services.emit(
|
|
540
|
+
this.services.emit(s.info.directory, { type: "message.updated", properties: { info: message.info } });
|
|
399
541
|
for (const part of message.parts)
|
|
400
542
|
this.emitPart(s, part);
|
|
401
543
|
}
|
|
402
544
|
}
|
|
403
|
-
this.services.emit(
|
|
545
|
+
this.services.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
|
|
404
546
|
if (s.compactingFromIdle) {
|
|
405
547
|
delete s.compactingFromIdle;
|
|
406
548
|
this.setStatus(s, { type: "idle" });
|
|
407
549
|
}
|
|
408
|
-
|
|
550
|
+
return;
|
|
409
551
|
}
|
|
410
552
|
default:
|
|
411
|
-
|
|
553
|
+
return;
|
|
412
554
|
}
|
|
413
555
|
}
|
|
414
556
|
finishStreaming(s) {
|
|
@@ -449,7 +591,19 @@ export class PiEventTranslator {
|
|
|
449
591
|
const textPart = part;
|
|
450
592
|
if (ame.type === `${wire}_delta` && typeof ame.delta === "string") {
|
|
451
593
|
textPart.text += ame.delta;
|
|
452
|
-
this.
|
|
594
|
+
this.services.emit(dir, {
|
|
595
|
+
type: "message.part.delta",
|
|
596
|
+
properties: {
|
|
597
|
+
sessionID: s.info.id,
|
|
598
|
+
messageID: textPart.messageID,
|
|
599
|
+
partID: textPart.id,
|
|
600
|
+
field: "text",
|
|
601
|
+
delta: ame.delta,
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
// Append before publishing the authoritative snapshot. Sending another
|
|
605
|
+
// append delta on the snapshot would duplicate text in streaming clients.
|
|
606
|
+
this.emitPart(s, textPart);
|
|
453
607
|
}
|
|
454
608
|
else if (ame.type === `${wire}_end`) {
|
|
455
609
|
if (typeof ame.content === "string")
|
package/dist/pi-rpc.d.ts
CHANGED
|
@@ -21,7 +21,20 @@ export declare const RPC_COMMAND_TIMEOUT_MS = 60000;
|
|
|
21
21
|
* long before a large session finished, leaving the run orphaned.
|
|
22
22
|
*/
|
|
23
23
|
export declare const COMPACT_RPC_TIMEOUT_MS: number;
|
|
24
|
-
export declare
|
|
24
|
+
export declare class UpstreamProtocolError extends Error {
|
|
25
|
+
constructor(message: string);
|
|
26
|
+
}
|
|
27
|
+
/** A process's group and kernel start time: with the PID, the identity that survives PID reuse checks. */
|
|
28
|
+
interface ProcessIdentity {
|
|
29
|
+
readonly pid: number;
|
|
30
|
+
readonly group: number;
|
|
31
|
+
readonly start: string;
|
|
32
|
+
}
|
|
33
|
+
/** Reads process identities so disposal can tell owned survivors from a reused PID. */
|
|
34
|
+
export interface ProcessTable {
|
|
35
|
+
identity(pid: number): ProcessIdentity | undefined;
|
|
36
|
+
groupMembers(group: number): ProcessIdentity[];
|
|
37
|
+
}
|
|
25
38
|
export type PiRpcResponse<T = unknown> = {
|
|
26
39
|
readonly id: string;
|
|
27
40
|
readonly type: "response";
|
|
@@ -35,13 +48,14 @@ export type PiRpcResponse<T = unknown> = {
|
|
|
35
48
|
readonly error?: string;
|
|
36
49
|
readonly data?: never;
|
|
37
50
|
});
|
|
38
|
-
|
|
51
|
+
interface PiRpcClientOptions {
|
|
39
52
|
cwd: string;
|
|
40
53
|
/** Path/name of the pi binary. Defaults to ANTECHAMBER_PI_BINARY env or "omo" from PATH. */
|
|
41
54
|
piBinary?: string;
|
|
42
55
|
/** Extra CLI flags, e.g. ["--no-session"] or ["--provider", "x"]. */
|
|
43
56
|
args?: string[];
|
|
44
57
|
clock?: LifecycleClock;
|
|
58
|
+
processTable?: ProcessTable;
|
|
45
59
|
}
|
|
46
60
|
export declare class PiRpcClient extends EventEmitter {
|
|
47
61
|
private options;
|
|
@@ -61,6 +75,9 @@ export declare class PiRpcClient extends EventEmitter {
|
|
|
61
75
|
constructor(options: PiRpcClientOptions);
|
|
62
76
|
private drain;
|
|
63
77
|
private frameOverflow;
|
|
78
|
+
/** Node has reaped the leader, so its PID (and group ID) may already belong to another process. */
|
|
79
|
+
private reaped;
|
|
80
|
+
private closeForTransportFailure;
|
|
64
81
|
private handleLine;
|
|
65
82
|
private handleExtensionUI;
|
|
66
83
|
respondExtensionUI(response: Record<string, unknown>): void;
|
|
@@ -72,3 +89,4 @@ export declare class PiRpcClient extends EventEmitter {
|
|
|
72
89
|
notify(cmd: Omit<PiRpcCommand, "id">): void;
|
|
73
90
|
dispose(): Promise<void>;
|
|
74
91
|
}
|
|
92
|
+
export {};
|