antechamber 0.2.10 → 0.2.12

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 CHANGED
@@ -7,6 +7,21 @@ and this project uses [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.12] - 2026-10-16
11
+
12
+ ### Fixed
13
+
14
+ - Preserve recorded conversation progress when the agent process exits before
15
+ `agent_end`, and retain successful background compaction across revival.
16
+ Checkpoints respect reverted selections and do not switch to sibling branches.
17
+
18
+ ## [0.2.11] - 2026-10-16
19
+
20
+ ### Fixed
21
+
22
+ - Keep pending prompts alive while the agent is running or compacting instead of terminating useful work at the
23
+ ordinary RPC deadline. Silent requests still time out, and prompt admission has a bounded overall wait.
24
+
10
25
  ## [0.2.10] - 2026-10-16
11
26
 
12
27
  ### Fixed
@@ -544,6 +544,7 @@ export class PiEventTranslator {
544
544
  },
545
545
  });
546
546
  if (ev.aborted !== true && typeof result?.summary === "string" && typeof result.firstKeptEntryId === "string") {
547
+ recordTurn(s);
547
548
  const messages = this.services.ensureMessages(s);
548
549
  let key;
549
550
  // compaction_end follows persistence, but its result omits the entry ID.
@@ -563,6 +564,7 @@ export class PiEventTranslator {
563
564
  for (const part of message.parts)
564
565
  this.emitPart(s, part);
565
566
  }
567
+ this.services.persistMeta(s);
566
568
  }
567
569
  this.services.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
568
570
  if (s.compactingFromIdle) {
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) {
@@ -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 = RPC_COMMAND_TIMEOUT_MS, signal, onDispatch) {
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 timer = setTimeout(() => {
285
- this.closeForTransportFailure(new Error(`pi command "${expectedCommand}" timed out after ${timeoutMs}ms`));
286
- }, timeoutMs);
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 });
@@ -52,7 +52,7 @@ export declare class SessionTreeError extends Error {
52
52
  export declare function durableShellTree(tree: SessionTreeState): SessionTreeState;
53
53
  export declare function getEntries(pi: PiRpcClient, signal?: AbortSignal): Promise<Entries>;
54
54
  export declare function readSessionTree(piPath: string): Entries;
55
- /** agent_end runs after message persistence; no async lease or timing race. */
55
+ /** Checkpoint persisted progress after agent_end, successful compaction, or process exit. */
56
56
  export declare function recordTurn(s: ManagedSession): void;
57
57
  export declare function branchEntries(tree: Entries, leafId?: string | null): TreeEntry[];
58
58
  /** Live ids belong to the UI; restored ids belong to JSONL offsets. Save both. */
@@ -44,10 +44,19 @@ export function readSessionTree(piPath) {
44
44
  }
45
45
  return { entries, leafId: entries.at(-1)?.id ?? null };
46
46
  }
47
- /** agent_end runs after message persistence; no async lease or timing race. */
47
+ /** Checkpoint persisted progress after agent_end, successful compaction, or process exit. */
48
48
  export function recordTurn(s) {
49
- if (s.piPath)
50
- bindMessageEntries(s, readSessionTree(s.piPath));
49
+ if (!s.piPath)
50
+ return;
51
+ const tree = readSessionTree(s.piPath);
52
+ // Disk order is not navigation: only a descendant can advance a saved selection.
53
+ // An ancestor, missing selection, or newer sibling must leave its bindings intact.
54
+ if (!s.tree.revert &&
55
+ s.tree.leafId !== undefined &&
56
+ s.tree.leafId !== null &&
57
+ !branchEntries(tree).some((entry) => entry.id === s.tree.leafId))
58
+ return;
59
+ bindMessageEntries(s, tree);
51
60
  }
52
61
  export function branchEntries(tree, leafId = tree.leafId) {
53
62
  const byId = new Map(tree.entries.map((entry) => [entry.id, entry]));
package/dist/sessions.js CHANGED
@@ -14,7 +14,7 @@ import { SessionLifecycle } from "./lifecycle.js";
14
14
  import { ApprovalBridge, extensionUiRequestFrom } from "./approvals.js";
15
15
  import { getAgentName, getExposedAgentNames } from "./agent-name.js";
16
16
  import { RequestBodyError } from "./http.js";
17
- import { bindMessageEntries, durableShellTree, forkConversation, getEntries, navigate, readSessionTree, revertConversation, SessionTreeError, unrevertConversation, } from "./session-tree.js";
17
+ import { bindMessageEntries, durableShellTree, forkConversation, getEntries, navigate, recordTurn, readSessionTree, revertConversation, SessionTreeError, unrevertConversation, } from "./session-tree.js";
18
18
  import { SubagentBridge } from "./subagents.js";
19
19
  import { MonitorSessions } from "./monitor-sessions.js";
20
20
  import { sessionDiff } from "./session-diff.js";
@@ -239,6 +239,13 @@ export class SessionManager {
239
239
  name: "UnknownError",
240
240
  data: { message: `pi process exited (code=${code})` },
241
241
  });
242
+ try {
243
+ recordTurn(s);
244
+ this.persistMeta(s);
245
+ }
246
+ catch (error) {
247
+ this.reportPersistenceError(s, error);
248
+ }
242
249
  delete s.interruptRequested;
243
250
  this.subagents.stopParent(s.info.id);
244
251
  this.monitors.stopParent(s.info.id);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-graph",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "OpenChamber extension: live, interactive graph of the OmO sub-agents behind the current session",
5
5
  "private": true,
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
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",