antechamber 0.2.9 → 0.2.11
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 +19 -0
- package/dist/catalog.js +1 -1
- package/dist/misc-routes.js +1 -1
- package/dist/pi-event-translation.d.ts +6 -0
- package/dist/pi-event-translation.js +55 -4
- package/dist/pi-rpc.d.ts +2 -0
- package/dist/pi-rpc.js +45 -5
- package/dist/pidfd.js +1 -0
- package/dist/session-history.js +0 -1
- package/dist/session-hosts.js +1 -0
- package/dist/session-tree.js +9 -4
- package/extensions/omo-dag/package.json +1 -1
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project uses [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.11] - 2026-10-16
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Keep pending prompts alive while the agent is running or compacting instead of terminating useful work at the
|
|
15
|
+
ordinary RPC deadline. Silent requests still time out, and prompt admission has a bounded overall wait.
|
|
16
|
+
|
|
17
|
+
## [0.2.10] - 2026-10-16
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `/btw` on an idle session no longer fails with "Invalid entry ID for
|
|
22
|
+
forking". Forking before an assistant message (the fork point `/btw` sends)
|
|
23
|
+
now keeps everything before that reply, matching OpenCode's exclusive fork
|
|
24
|
+
boundary. Forks are still refused while the session is busy.
|
|
25
|
+
- Running tool cards no longer flash a red "Interrupted" error until they
|
|
26
|
+
finish. An assistant message is now marked completed only once its tools
|
|
27
|
+
(including subagent task cards) have settled, as OpenCode does.
|
|
28
|
+
|
|
10
29
|
## [0.2.9] - 2026-10-16
|
|
11
30
|
|
|
12
31
|
### Fixed
|
package/dist/catalog.js
CHANGED
|
@@ -32,7 +32,7 @@ function toOCModel(m) {
|
|
|
32
32
|
toolcall: true,
|
|
33
33
|
input: { text: modality("text"), audio: false, image: modality("image"), video: false, pdf: false },
|
|
34
34
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
35
|
-
interleaved: m.reasoning
|
|
35
|
+
interleaved: !!m.reasoning,
|
|
36
36
|
},
|
|
37
37
|
cost: {
|
|
38
38
|
input: m.cost?.input ?? 0,
|
package/dist/misc-routes.js
CHANGED
|
@@ -15,7 +15,7 @@ export function debugLog(line) {
|
|
|
15
15
|
if (!envSetting("DEBUG") || loggingDisabled)
|
|
16
16
|
return;
|
|
17
17
|
try {
|
|
18
|
-
appendFileSync(envSetting("DEBUG_LOG") ?? join(tmpdir(), `antechamber-debug-${process.getuid?.() ?? "user"}.log`), line
|
|
18
|
+
appendFileSync(envSetting("DEBUG_LOG") ?? join(tmpdir(), `antechamber-debug-${process.getuid?.() ?? "user"}.log`), `${line}\n`, { flag: "a", mode: 0o600 });
|
|
19
19
|
}
|
|
20
20
|
catch (error) {
|
|
21
21
|
loggingDisabled = true;
|
|
@@ -51,6 +51,8 @@ interface PiEventServices {
|
|
|
51
51
|
/** Translates per-session events; registry and lifecycle operations stay with SessionManager. */
|
|
52
52
|
export declare class PiEventTranslator {
|
|
53
53
|
private services;
|
|
54
|
+
/** Ended assistant messages whose completion waits on their tools. */
|
|
55
|
+
private awaitingTools;
|
|
54
56
|
constructor(services: PiEventServices);
|
|
55
57
|
/** Close dead-runtime UI state before an error or idle event can reach the client. */
|
|
56
58
|
finishInterruptedRun(s: ManagedSession, error: NonNullable<OCAssistantMessage["error"]>): void;
|
|
@@ -79,7 +81,11 @@ export declare class PiEventTranslator {
|
|
|
79
81
|
private onToolcallEvent;
|
|
80
82
|
/** turn_end.toolResults → finalize tool parts (completed / error). */
|
|
81
83
|
private applyToolResults;
|
|
84
|
+
/** Complete an ended assistant message whose completion waited on its tools; true when it changed. */
|
|
85
|
+
private completeSettled;
|
|
82
86
|
/** Tool parts that never received a result (abort, crash) must not spin forever. */
|
|
83
87
|
private finalizeDanglingTools;
|
|
88
|
+
/** Complete every ended message of this session still waiting on its tools. */
|
|
89
|
+
private completeAwaitingTools;
|
|
84
90
|
}
|
|
85
91
|
export {};
|
|
@@ -142,6 +142,8 @@ export function compactionMessages(s, key, summary, created, model) {
|
|
|
142
142
|
/** Translates per-session events; registry and lifecycle operations stay with SessionManager. */
|
|
143
143
|
export class PiEventTranslator {
|
|
144
144
|
services;
|
|
145
|
+
/** Ended assistant messages whose completion waits on their tools. */
|
|
146
|
+
awaitingTools = new WeakSet();
|
|
145
147
|
constructor(services) {
|
|
146
148
|
this.services = services;
|
|
147
149
|
}
|
|
@@ -420,6 +422,10 @@ export class PiEventTranslator {
|
|
|
420
422
|
const msg = ev.message;
|
|
421
423
|
if (msg?.role !== "assistant")
|
|
422
424
|
return; // user message already recorded at prompt()
|
|
425
|
+
// pi starts the next step only after the previous step's tool results, so
|
|
426
|
+
// close any earlier step still waiting on cards (e.g. a background task)
|
|
427
|
+
// before this one opens; v2 clients must never see an ended step reopen.
|
|
428
|
+
this.completeAwaitingTools(s);
|
|
423
429
|
// pi stamps the assistant message with the model really serving it.
|
|
424
430
|
// Trust that over our snapshot, which goes stale whenever pi switches on
|
|
425
431
|
// its own side (subagent routing, an in-session /model, a resumed file).
|
|
@@ -468,7 +474,15 @@ export class PiEventTranslator {
|
|
|
468
474
|
}
|
|
469
475
|
applyUsage(s.streaming.message, ended.usage);
|
|
470
476
|
Object.assign(s.streaming.message, piStopReason(ended.stopReason, ended.errorMessage));
|
|
471
|
-
|
|
477
|
+
// OpenCode completes an assistant message only after its tools settle, and
|
|
478
|
+
// OpenChamber marks active tools under a completed message as Interrupted.
|
|
479
|
+
// pi ends a toolUse message before executing its tool calls, so defer its
|
|
480
|
+
// completion to the last tool result (applyToolResults), the next step, or
|
|
481
|
+
// the run end. Errored/aborted messages never run their tools.
|
|
482
|
+
if (ended.stopReason === "toolUse" && hasActiveTools(s, s.streaming.message.id))
|
|
483
|
+
this.awaitingTools.add(s.streaming.message);
|
|
484
|
+
else
|
|
485
|
+
s.streaming.message.time.completed = Date.now();
|
|
472
486
|
this.services.emit(s.info.directory, { type: "message.updated", properties: { info: s.streaming.message } });
|
|
473
487
|
this.finishStreaming(s);
|
|
474
488
|
if (syncSessionCost(s))
|
|
@@ -482,6 +496,8 @@ export class PiEventTranslator {
|
|
|
482
496
|
onCompactionEvent(s, ev) {
|
|
483
497
|
switch (ev.type) {
|
|
484
498
|
case "compaction_start":
|
|
499
|
+
// An auto-compaction can fall between steps; its summary is a new step.
|
|
500
|
+
this.completeAwaitingTools(s);
|
|
485
501
|
s.compactionID = id("compaction");
|
|
486
502
|
this.services.emit(s.info.directory, {
|
|
487
503
|
type: "session.compaction.started",
|
|
@@ -738,8 +754,12 @@ export class PiEventTranslator {
|
|
|
738
754
|
applyToolResults(s, results) {
|
|
739
755
|
for (const tr of results) {
|
|
740
756
|
const callID = typeof tr.toolCallId === "string" ? tr.toolCallId : undefined;
|
|
741
|
-
if (callID !== undefined && this.services.subagents.result(s, callID, tr, true))
|
|
757
|
+
if (callID !== undefined && this.services.subagents.result(s, callID, tr, true)) {
|
|
758
|
+
const owner = s.messages?.find((m) => m.parts.some((p) => p.type === "tool" && p.callID === callID));
|
|
759
|
+
if (owner && this.completeSettled(s, owner.info))
|
|
760
|
+
this.services.emit(s.info.directory, { type: "message.updated", properties: { info: owner.info } });
|
|
742
761
|
continue;
|
|
762
|
+
}
|
|
743
763
|
const part = callID !== undefined ? s.toolByCallId.get(callID) : undefined;
|
|
744
764
|
if (!part || callID === undefined)
|
|
745
765
|
continue;
|
|
@@ -778,10 +798,20 @@ export class PiEventTranslator {
|
|
|
778
798
|
this.services.dirtyDiffs.add(s.info.id);
|
|
779
799
|
this.emitPart(s, part);
|
|
780
800
|
const owner = s.messages?.find((m) => m.info.id === part.messageID);
|
|
781
|
-
if (owner)
|
|
782
|
-
|
|
801
|
+
if (!owner)
|
|
802
|
+
continue;
|
|
803
|
+
this.completeSettled(s, owner.info);
|
|
804
|
+
this.services.emit(s.info.directory, { type: "message.updated", properties: { info: owner.info } });
|
|
783
805
|
}
|
|
784
806
|
}
|
|
807
|
+
/** Complete an ended assistant message whose completion waited on its tools; true when it changed. */
|
|
808
|
+
completeSettled(s, info) {
|
|
809
|
+
if (!this.awaitingTools.has(info) || info.role !== "assistant" || hasActiveTools(s, info.id))
|
|
810
|
+
return false;
|
|
811
|
+
this.awaitingTools.delete(info);
|
|
812
|
+
info.time.completed = Date.now();
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
785
815
|
/** Tool parts that never received a result (abort, crash) must not spin forever. */
|
|
786
816
|
finalizeDanglingTools(s) {
|
|
787
817
|
for (const part of finishProviderToolParts(s.toolByCallId.values(), Date.now()))
|
|
@@ -799,5 +829,26 @@ export class PiEventTranslator {
|
|
|
799
829
|
this.emitPart(s, part);
|
|
800
830
|
}
|
|
801
831
|
s.toolByCallId.clear();
|
|
832
|
+
this.completeAwaitingTools(s); // the run is over
|
|
802
833
|
}
|
|
834
|
+
/** Complete every ended message of this session still waiting on its tools. */
|
|
835
|
+
completeAwaitingTools(s) {
|
|
836
|
+
const now = Date.now();
|
|
837
|
+
for (const { info } of s.messages ?? []) {
|
|
838
|
+
if (!this.awaitingTools.has(info) || info.role !== "assistant")
|
|
839
|
+
continue;
|
|
840
|
+
this.awaitingTools.delete(info);
|
|
841
|
+
info.time.completed = now;
|
|
842
|
+
this.services.emit(s.info.directory, { type: "message.updated", properties: { info } });
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/** A tool part of this message (including subagent task cards) still waiting for its result. */
|
|
847
|
+
function hasActiveTools(s, messageID) {
|
|
848
|
+
const active = (part) => part.type === "tool" &&
|
|
849
|
+
part.messageID === messageID &&
|
|
850
|
+
(part.state.status === "pending" || part.state.status === "running");
|
|
851
|
+
if ([...s.toolByCallId.values()].some(active))
|
|
852
|
+
return true;
|
|
853
|
+
return s.messages?.find((m) => m.info.id === messageID)?.parts.some(active) ?? false;
|
|
803
854
|
}
|
package/dist/pi-rpc.d.ts
CHANGED
|
@@ -69,6 +69,8 @@ export declare class PiRpcClient extends EventEmitter {
|
|
|
69
69
|
private closed;
|
|
70
70
|
private stderrTail;
|
|
71
71
|
private disposing;
|
|
72
|
+
private agentActive;
|
|
73
|
+
private compacting;
|
|
72
74
|
get pid(): number | undefined;
|
|
73
75
|
get isBusy(): boolean;
|
|
74
76
|
onEvent(listener: (payload: Record<string, unknown>) => void): this;
|
package/dist/pi-rpc.js
CHANGED
|
@@ -21,6 +21,7 @@ export const RPC_COMMAND_TIMEOUT_MS = 60_000;
|
|
|
21
21
|
* long before a large session finished, leaving the run orphaned.
|
|
22
22
|
*/
|
|
23
23
|
export const COMPACT_RPC_TIMEOUT_MS = 31 * 60_000;
|
|
24
|
+
const PROMPT_ADMISSION_TIMEOUT_MS = COMPACT_RPC_TIMEOUT_MS + RPC_COMMAND_TIMEOUT_MS;
|
|
24
25
|
const MAX_JSONL_FRAME_BYTES = 64 * 1024 * 1024;
|
|
25
26
|
export class UpstreamProtocolError extends Error {
|
|
26
27
|
constructor(message) {
|
|
@@ -67,6 +68,8 @@ export class PiRpcClient extends EventEmitter {
|
|
|
67
68
|
closed = false;
|
|
68
69
|
stderrTail = "";
|
|
69
70
|
disposing;
|
|
71
|
+
agentActive = false;
|
|
72
|
+
compacting = false;
|
|
70
73
|
get pid() {
|
|
71
74
|
return this.proc.pid;
|
|
72
75
|
}
|
|
@@ -235,6 +238,29 @@ export class PiRpcClient extends EventEmitter {
|
|
|
235
238
|
this.handleExtensionUI(msg);
|
|
236
239
|
return;
|
|
237
240
|
}
|
|
241
|
+
const wasWorking = this.agentActive || this.compacting;
|
|
242
|
+
switch (msg.type) {
|
|
243
|
+
case "agent_start":
|
|
244
|
+
case "auto_retry_start":
|
|
245
|
+
this.agentActive = true;
|
|
246
|
+
break;
|
|
247
|
+
case "agent_end":
|
|
248
|
+
if (msg.willRetry !== true)
|
|
249
|
+
this.agentActive = false;
|
|
250
|
+
break;
|
|
251
|
+
case "agent_idle":
|
|
252
|
+
this.agentActive = false;
|
|
253
|
+
break;
|
|
254
|
+
case "compaction_start":
|
|
255
|
+
this.compacting = true;
|
|
256
|
+
break;
|
|
257
|
+
case "compaction_end":
|
|
258
|
+
this.compacting = false;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
if (wasWorking !== (this.agentActive || this.compacting))
|
|
262
|
+
for (const request of this.pending.values())
|
|
263
|
+
request.onWorkState?.();
|
|
238
264
|
this.emit("event", msg);
|
|
239
265
|
}
|
|
240
266
|
handleExtensionUI(msg) {
|
|
@@ -253,7 +279,7 @@ export class PiRpcClient extends EventEmitter {
|
|
|
253
279
|
write(obj) {
|
|
254
280
|
if (this.closed)
|
|
255
281
|
return;
|
|
256
|
-
this.proc.stdin.write(JSON.stringify(obj)
|
|
282
|
+
this.proc.stdin.write(`${JSON.stringify(obj)}\n`);
|
|
257
283
|
}
|
|
258
284
|
failAll(err) {
|
|
259
285
|
for (const p of this.pending.values())
|
|
@@ -261,16 +287,21 @@ export class PiRpcClient extends EventEmitter {
|
|
|
261
287
|
this.pending.clear();
|
|
262
288
|
}
|
|
263
289
|
/** Send a command and wait for its correlated response. */
|
|
264
|
-
command(cmd, timeoutMs
|
|
290
|
+
command(cmd, timeoutMs, signal, onDispatch) {
|
|
265
291
|
if (this.closed)
|
|
266
292
|
return Promise.reject(new Error("pi process is not running"));
|
|
267
293
|
if (signal?.aborted)
|
|
268
294
|
return Promise.reject(signal.reason);
|
|
269
295
|
const id = randomUUID();
|
|
270
296
|
const expectedCommand = String(cmd.type);
|
|
297
|
+
const deadlineMs = timeoutMs ?? RPC_COMMAND_TIMEOUT_MS;
|
|
298
|
+
const waitsForWork = expectedCommand === "prompt" && timeoutMs === undefined;
|
|
271
299
|
return new Promise((resolve, reject) => {
|
|
300
|
+
let timer;
|
|
301
|
+
let admissionTimer;
|
|
272
302
|
const cleanup = () => {
|
|
273
303
|
clearTimeout(timer);
|
|
304
|
+
clearTimeout(admissionTimer);
|
|
274
305
|
signal?.removeEventListener("abort", onAbort);
|
|
275
306
|
this.pending.delete(id);
|
|
276
307
|
};
|
|
@@ -281,9 +312,17 @@ export class PiRpcClient extends EventEmitter {
|
|
|
281
312
|
const onAbort = () => {
|
|
282
313
|
rejectCommand(signal?.reason);
|
|
283
314
|
};
|
|
284
|
-
const
|
|
285
|
-
this.closeForTransportFailure(new Error(`pi command "${expectedCommand}" timed out after ${
|
|
286
|
-
}
|
|
315
|
+
const expire = (duration) => {
|
|
316
|
+
this.closeForTransportFailure(new Error(`pi command "${expectedCommand}" timed out after ${duration}ms`));
|
|
317
|
+
};
|
|
318
|
+
const armDeadline = () => {
|
|
319
|
+
clearTimeout(timer);
|
|
320
|
+
if (!waitsForWork || !(this.agentActive || this.compacting))
|
|
321
|
+
timer = setTimeout(() => expire(deadlineMs), deadlineMs);
|
|
322
|
+
};
|
|
323
|
+
armDeadline();
|
|
324
|
+
if (waitsForWork)
|
|
325
|
+
admissionTimer = setTimeout(() => expire(PROMPT_ADMISSION_TIMEOUT_MS), PROMPT_ADMISSION_TIMEOUT_MS);
|
|
287
326
|
this.pending.set(id, {
|
|
288
327
|
command: expectedCommand,
|
|
289
328
|
resolve: (r) => {
|
|
@@ -292,6 +331,7 @@ export class PiRpcClient extends EventEmitter {
|
|
|
292
331
|
resolve(r.success ? { ...r, data: r.data } : r);
|
|
293
332
|
},
|
|
294
333
|
reject: rejectCommand,
|
|
334
|
+
...(waitsForWork ? { onWorkState: armDeadline } : {}),
|
|
295
335
|
});
|
|
296
336
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
297
337
|
this.write({ ...cmd, id });
|
package/dist/pidfd.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { delimiter, resolve } from "node:path";
|
|
3
|
+
// biome-ignore lint/complexity/noUselessStringRaw: embedded Python must keep any backslashes verbatim.
|
|
3
4
|
const pidfdProbe = String.raw `
|
|
4
5
|
import os, selectors, signal
|
|
5
6
|
fd = os.pidfd_open(os.getpid())
|
package/dist/session-history.js
CHANGED
package/dist/session-hosts.js
CHANGED
|
@@ -50,6 +50,7 @@ function processInfo(pid) {
|
|
|
50
50
|
// Node 22 has no pidfd API. This bounded, stdlib-only helper holds all pidfds
|
|
51
51
|
// across TERM, readiness-based death observation, and KILL. -I ignores Python
|
|
52
52
|
// environment customizations. No numeric-PID signal fallback is permitted.
|
|
53
|
+
// biome-ignore lint/complexity/noUselessStringRaw: embedded Python must keep any backslashes verbatim.
|
|
53
54
|
const stopBoundHosts = String.raw `
|
|
54
55
|
import contextlib, json, os, selectors, signal, sys, time
|
|
55
56
|
|
package/dist/session-tree.js
CHANGED
|
@@ -235,13 +235,18 @@ export async function forkConversation(s, pi, tree, messageID) {
|
|
|
235
235
|
throw new SessionTreeError("Cannot fork an empty conversation");
|
|
236
236
|
const shellUser = entry.message?.role === "bashExecution" &&
|
|
237
237
|
s.messages?.find((message) => message.info.id === messageID)?.info.role === "user";
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
// OpenCode forks strictly before any selected message. OmO's default "before"
|
|
239
|
+
// position only accepts user entries, so other boundaries (an assistant reply,
|
|
240
|
+
// as /btw sends) fork "at" the entry preceding them.
|
|
241
|
+
const beforeOther = messageID !== undefined && entry.message?.role !== "user" && entry.message?.role !== "bashExecution";
|
|
242
|
+
const forkEntryId = shellUser || beforeOther ? entry.parentId : entry.id;
|
|
243
|
+
if (forkEntryId === null) {
|
|
244
|
+
throw new SessionTreeError(shellUser ? "Cannot fork before a root shell entry" : "Cannot fork before the first entry");
|
|
245
|
+
}
|
|
241
246
|
const response = await pi.command({
|
|
242
247
|
type: "fork",
|
|
243
248
|
entryId: forkEntryId,
|
|
244
|
-
...(messageID === undefined || entry.message?.role === "bashExecution" ? { position: "at" } : {}),
|
|
249
|
+
...(messageID === undefined || beforeOther || entry.message?.role === "bashExecution" ? { position: "at" } : {}),
|
|
245
250
|
});
|
|
246
251
|
if (!response.success)
|
|
247
252
|
throw new SessionTreeError(response.error ?? "OmO fork failed");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "antechamber",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "OpenCode-compatible HTTP/SSE server that lets OpenChamber drive the pi coding agent (and OmO) over RPC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"CHANGELOG.md"
|
|
41
41
|
],
|
|
42
42
|
"bin": {
|
|
43
|
-
"antechamber": "
|
|
43
|
+
"antechamber": "dist/cli.js"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsc -p tsconfig.json && node --input-type=module -e \"import { chmodSync } from 'node:fs'; chmodSync('dist/cli.js', 0o755)\" && npm run build:extension",
|
|
@@ -60,6 +60,9 @@
|
|
|
60
60
|
"model-smoke": "tsx contrib/model-consistency.ts",
|
|
61
61
|
"v2-smoke": "tsx contrib/v2-smoke.ts"
|
|
62
62
|
},
|
|
63
|
+
"allowScripts": {
|
|
64
|
+
"esbuild@0.28.2": true
|
|
65
|
+
},
|
|
63
66
|
"engines": {
|
|
64
67
|
"node": ">=22"
|
|
65
68
|
},
|