antechamber 0.2.9 → 0.2.10
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 +12 -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.js +1 -1
- 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,18 @@ and this project uses [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.10] - 2026-10-16
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- `/btw` on an idle session no longer fails with "Invalid entry ID for
|
|
15
|
+
forking". Forking before an assistant message (the fork point `/btw` sends)
|
|
16
|
+
now keeps everything before that reply, matching OpenCode's exclusive fork
|
|
17
|
+
boundary. Forks are still refused while the session is busy.
|
|
18
|
+
- Running tool cards no longer flash a red "Interrupted" error until they
|
|
19
|
+
finish. An assistant message is now marked completed only once its tools
|
|
20
|
+
(including subagent task cards) have settled, as OpenCode does.
|
|
21
|
+
|
|
10
22
|
## [0.2.9] - 2026-10-16
|
|
11
23
|
|
|
12
24
|
### 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.js
CHANGED
|
@@ -253,7 +253,7 @@ export class PiRpcClient extends EventEmitter {
|
|
|
253
253
|
write(obj) {
|
|
254
254
|
if (this.closed)
|
|
255
255
|
return;
|
|
256
|
-
this.proc.stdin.write(JSON.stringify(obj)
|
|
256
|
+
this.proc.stdin.write(`${JSON.stringify(obj)}\n`);
|
|
257
257
|
}
|
|
258
258
|
failAll(err) {
|
|
259
259
|
for (const p of this.pending.values())
|
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.10",
|
|
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
|
},
|