antechamber 0.1.0 → 0.1.1

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,29 @@ and this project uses [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.1] - 2026-10-14
11
+
12
+ ### Added
13
+
14
+ - Sessions now report `cost` (USD) from OmO's per-message usage, live and from
15
+ history, on v1 session payloads and v2 session reads. Subagent sessions carry
16
+ their own cost, so OpenChamber's work-status panel shows the session total
17
+ with its session/subagents breakdown.
18
+
19
+ ### Fixed
20
+
21
+ - `/compact` now shows progress and its result. The session turns busy while
22
+ OmO compacts, and the summary appears as a `/compact` turn with a summary
23
+ reply, live and after reload. The compact RPC now waits up to OmO's own
24
+ 30-minute compaction budget instead of giving up after 60 seconds, which left
25
+ the compaction running unseen until a later abort reported it cancelled.
26
+ - Reverting no longer adds a "Conversation reverted. Files were not changed."
27
+ card to the transcript; OpenChamber's own reverted-message dock (driven by
28
+ `session.revert`) already shows the state.
29
+ - Sessions opened with a slash command (for example `/skill:plan Build the
30
+ importer`) now get a generated title. OmO skips titling slash-led prompts, so
31
+ the adapter passes the command's arguments as the native `sessionTitlePrompt`.
32
+
10
33
  ## [0.1.0] - 2026-10-14
11
34
 
12
35
  First standalone release under the `antechamber` name.
@@ -1,5 +1,5 @@
1
1
  /** Pi message/part translation and the live event bridge, with explicit manager services. */
2
- import type { OCAssistantMessage, OCMessageWithParts, OCEvent, OCSessionStatus, OCPart } from "./types.js";
2
+ import type { OCAssistantMessage, OCMessageWithParts, OCModelRef, OCEvent, OCSessionStatus, OCPart } from "./types.js";
3
3
  import type { ManagedSession } from "./sessions.js";
4
4
  import type { SessionStore } from "./session-store.js";
5
5
  import type { SubagentBridge } from "./subagents.js";
@@ -24,6 +24,20 @@ export declare function piStopReason(stopReason: string | undefined, errorMessag
24
24
  export declare function textOf(content: Array<Record<string, unknown>> | undefined): string;
25
25
  export declare function newPart(kind: "text" | "reasoning", sessionID: string, messageID: string): OCPart;
26
26
  export declare function applyUsage(msg: OCAssistantMessage, usage: PiUsage | undefined): void;
27
+ /**
28
+ * Recompute the session's own spend from its loaded assistant messages. Subagent
29
+ * spend stays on the child sessions: OpenChamber sums the parentID subtree, so
30
+ * folding it in here would count it twice. Returns whether the value changed.
31
+ */
32
+ export declare function syncSessionCost(s: ManagedSession): boolean;
33
+ /**
34
+ * An OmO compaction as OpenCode shows one: a user turn holding a `compaction`
35
+ * part (OpenChamber renders it as `/compact`) answered by a `summary` message.
36
+ * IDs derive from the compaction's first kept entry, which the live
37
+ * `compaction_end` result and the persisted entry share, so a history reload
38
+ * reconciles with the live messages instead of duplicating them.
39
+ */
40
+ export declare function compactionMessages(s: ManagedSession, key: string, summary: string, created: number, model: OCModelRef | undefined): [OCMessageWithParts, OCMessageWithParts];
27
41
  interface PiEventServices {
28
42
  readonly emit: (directory: string, event: OCEvent | TodoUpdated) => void;
29
43
  readonly subagents: SubagentBridge;
@@ -66,6 +66,69 @@ export function applyUsage(msg, usage) {
66
66
  if (usage.cost?.total !== undefined)
67
67
  msg.cost = usage.cost.total;
68
68
  }
69
+ /**
70
+ * Recompute the session's own spend from its loaded assistant messages. Subagent
71
+ * spend stays on the child sessions: OpenChamber sums the parentID subtree, so
72
+ * folding it in here would count it twice. Returns whether the value changed.
73
+ */
74
+ export function syncSessionCost(s) {
75
+ if (s.messages === undefined)
76
+ return false;
77
+ let cost = 0;
78
+ for (const message of s.messages)
79
+ if (message.info.role === "assistant")
80
+ cost += message.info.cost;
81
+ if ((s.info.cost ?? 0) === cost)
82
+ return false; // absent means zero; avoid no-op updates
83
+ s.info.cost = cost;
84
+ return true;
85
+ }
86
+ /**
87
+ * An OmO compaction as OpenCode shows one: a user turn holding a `compaction`
88
+ * part (OpenChamber renders it as `/compact`) answered by a `summary` message.
89
+ * IDs derive from the compaction's first kept entry, which the live
90
+ * `compaction_end` result and the persisted entry share, so a history reload
91
+ * reconciles with the live messages instead of duplicating them.
92
+ */
93
+ export function compactionMessages(s, key, summary, created, model) {
94
+ const sessionID = s.info.id;
95
+ const userID = `msg_compaction_${key}`;
96
+ const summaryID = `${userID}_summary`;
97
+ const served = model ?? { providerID: "unknown", modelID: "unknown" };
98
+ return [
99
+ {
100
+ info: { id: userID, sessionID, role: "user", time: { created }, agent: getAgentName(), model: served },
101
+ parts: [{ id: `prt_compaction_${key}`, sessionID, messageID: userID, type: "compaction", auto: false }],
102
+ },
103
+ {
104
+ info: {
105
+ id: summaryID,
106
+ sessionID,
107
+ role: "assistant",
108
+ time: { created, completed: created },
109
+ parentID: userID,
110
+ modelID: served.modelID,
111
+ providerID: served.providerID,
112
+ mode: getAgentName(),
113
+ path: { cwd: s.info.directory, root: s.info.directory },
114
+ cost: 0,
115
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
116
+ finish: "stop",
117
+ summary: true,
118
+ },
119
+ parts: [
120
+ {
121
+ id: `prt_compaction_${key}_summary`,
122
+ sessionID,
123
+ messageID: summaryID,
124
+ type: "text",
125
+ text: summary,
126
+ time: { start: created, end: created },
127
+ },
128
+ ],
129
+ },
130
+ ];
131
+ }
69
132
  /** Translates per-session events; registry and lifecycle operations stay with SessionManager. */
70
133
  export class PiEventTranslator {
71
134
  services;
@@ -289,6 +352,8 @@ export class PiEventTranslator {
289
352
  s.streaming.message.time.completed = Date.now();
290
353
  this.services.emit(dir, { type: "message.updated", properties: { info: s.streaming.message } });
291
354
  this.finishStreaming(s);
355
+ if (syncSessionCost(s))
356
+ this.services.emit(dir, { type: "session.updated", properties: { info: s.info } });
292
357
  }
293
358
  break;
294
359
  }
@@ -309,8 +374,41 @@ export class PiEventTranslator {
309
374
  },
310
375
  });
311
376
  break;
377
+ case "compaction_start": {
378
+ s.info.time.compacting = Date.now();
379
+ this.services.emit(dir, { type: "session.updated", properties: { info: s.info } });
380
+ // A manual /compact runs no agent turn, so nothing else marks the session busy.
381
+ if (s.status.type === "idle") {
382
+ s.compactingFromIdle = true;
383
+ this.setStatus(s, { type: "busy" });
384
+ }
385
+ break;
386
+ }
387
+ case "compaction_end": {
388
+ delete s.info.time.compacting;
389
+ const result = ev.result;
390
+ if (ev.aborted !== true && typeof result?.summary === "string" && typeof result.firstKeptEntryId === "string") {
391
+ const messages = this.services.ensureMessages(s);
392
+ for (const message of compactionMessages(s, result.firstKeptEntryId, result.summary, Date.now(), s.model)) {
393
+ if (messages.some((item) => item.info.id === message.info.id))
394
+ continue;
395
+ // Keep a hidden redo branch after the new turn, as appendMessage does.
396
+ const boundary = messages.findIndex((item) => item.info.id === s.tree.revert?.messageID);
397
+ messages.splice(boundary < 0 ? messages.length : boundary, 0, message);
398
+ this.services.emit(dir, { type: "message.updated", properties: { info: message.info } });
399
+ for (const part of message.parts)
400
+ this.emitPart(s, part);
401
+ }
402
+ }
403
+ this.services.emit(dir, { type: "session.updated", properties: { info: s.info } });
404
+ if (s.compactingFromIdle) {
405
+ delete s.compactingFromIdle;
406
+ this.setStatus(s, { type: "idle" });
407
+ }
408
+ break;
409
+ }
312
410
  default:
313
- break; // turn_start, queue_update, compaction_* — future milestones
411
+ break; // turn_start, queue_update — future milestones
314
412
  }
315
413
  }
316
414
  finishStreaming(s) {
package/dist/pi-rpc.d.ts CHANGED
@@ -15,6 +15,12 @@ export interface PiRpcCommand {
15
15
  [key: string]: unknown;
16
16
  }
17
17
  export declare const RPC_COMMAND_TIMEOUT_MS = 60000;
18
+ /**
19
+ * `compact` answers only when the summary is written. OmO bounds one compaction by
20
+ * its SUMMARIZATION_MAX_DURATION_CAP_MS (30 min); the generic 60 s budget gave up
21
+ * long before a large session finished, leaving the run orphaned.
22
+ */
23
+ export declare const COMPACT_RPC_TIMEOUT_MS: number;
18
24
  export declare const MAX_JSONL_FRAME_BYTES: number;
19
25
  export type PiRpcResponse<T = unknown> = {
20
26
  readonly id: string;
package/dist/pi-rpc.js CHANGED
@@ -14,6 +14,12 @@ import { randomUUID } from "node:crypto";
14
14
  import { systemClock } from "./lifecycle.js";
15
15
  import { agentBinary, envSetting } from "./config.js";
16
16
  export const RPC_COMMAND_TIMEOUT_MS = 60_000;
17
+ /**
18
+ * `compact` answers only when the summary is written. OmO bounds one compaction by
19
+ * its SUMMARIZATION_MAX_DURATION_CAP_MS (30 min); the generic 60 s budget gave up
20
+ * long before a large session finished, leaving the run orphaned.
21
+ */
22
+ export const COMPACT_RPC_TIMEOUT_MS = 31 * 60_000;
17
23
  export const MAX_JSONL_FRAME_BYTES = 64 * 1024 * 1024;
18
24
  const PROCESS_KILL_GRACE_MS = 5_000;
19
25
  export class PiRpcClient extends EventEmitter {
@@ -61,6 +61,14 @@ export declare const unsupportedShare: {
61
61
  export declare function shellTitle(command: string): string;
62
62
  export declare function getCommands(directory: string): Promise<CommandDescriptor[]>;
63
63
  export declare function parseSlashCommand(value: unknown): SlashCommandInput | undefined;
64
+ /**
65
+ * OmO never auto-titles a first prompt that starts with "/", so a session opened
66
+ * with `/skill:plan Build the importer` kept its first-message title. Title from
67
+ * the command's arguments instead; a bare command has nothing to title from.
68
+ */
69
+ export declare function slashTitlePrompt(text: string): {
70
+ sessionTitlePrompt?: string;
71
+ };
64
72
  export declare function slashCommandPrompt(input: SlashCommandInput): string;
65
73
  export declare function runSlashCommand(sessions: SessionManager, sessionId: string, input: SlashCommandInput): Promise<OCMessageWithParts | undefined>;
66
74
  export declare function parseShellCommand(value: unknown): string | undefined;
@@ -114,6 +114,15 @@ export function parseSlashCommand(value) {
114
114
  const messageID = typeof value.messageID === "string" && value.messageID !== "" ? value.messageID : undefined;
115
115
  return { command: value.command, arguments: value.arguments, ...(messageID ? { messageID } : {}) };
116
116
  }
117
+ /**
118
+ * OmO never auto-titles a first prompt that starts with "/", so a session opened
119
+ * with `/skill:plan Build the importer` kept its first-message title. Title from
120
+ * the command's arguments instead; a bare command has nothing to title from.
121
+ */
122
+ export function slashTitlePrompt(text) {
123
+ const match = /^\s*\/\S+\s+([\s\S]*\S)/.exec(text);
124
+ return match?.[1] !== undefined && !match[1].startsWith("/") ? { sessionTitlePrompt: match[1] } : {};
125
+ }
117
126
  export function slashCommandPrompt(input) {
118
127
  return input.arguments === "" ? `/${input.command}` : `/${input.command} ${input.arguments}`;
119
128
  }
@@ -5,7 +5,7 @@ import { TodoBridge } from "./todo-bridge.js";
5
5
  import { bindMessageEntries } from "./session-tree.js";
6
6
  import { taskParts, updateTaskParts, taskCompletions } from "./subagents.js";
7
7
  import { shellTitle, shellUserMessage } from "./session-commands.js";
8
- import { piModelId, piStopReason, textOf } from "./pi-event-translation.js";
8
+ import { compactionMessages, piModelId, piStopReason, textOf } from "./pi-event-translation.js";
9
9
  function entryTime(e) {
10
10
  if (typeof e.timestamp === "number")
11
11
  return e.timestamp;
@@ -131,6 +131,11 @@ export function parsePiSessionText(s, agentName, raw) {
131
131
  currentModel = { providerID: e.provider, modelID: e.modelId };
132
132
  continue;
133
133
  }
134
+ if (e.type === "compaction" && typeof e.summary === "string" && typeof e.firstKeptEntryId === "string") {
135
+ // Not a run boundary: an automatic compaction can sit between steps of one turn.
136
+ out.push(...compactionMessages(s, e.firstKeptEntryId, e.summary, entryTime(e), currentModel));
137
+ continue;
138
+ }
134
139
  if (e.type !== "message")
135
140
  continue;
136
141
  const m = e.message;
@@ -56,4 +56,3 @@ export declare function forkConversation(s: ManagedSession, pi: PiRpcClient, tre
56
56
  export declare function revertConversation(s: ManagedSession, pi: PiRpcClient, tree: Entries, messageID: string): Promise<boolean>;
57
57
  export declare function unrevertConversation(s: ManagedSession, pi: PiRpcClient): Promise<boolean>;
58
58
  export declare function navigate(pi: PiRpcClient, targetId: string | null, signal?: AbortSignal): Promise<string | null>;
59
- export declare function revertNotice(s: ManagedSession): OCMessageWithParts | undefined;
@@ -285,40 +285,3 @@ export async function navigate(pi, targetId, signal) {
285
285
  throw new SessionTreeError("Conversation navigation was cancelled");
286
286
  return response.data.leafId;
287
287
  }
288
- export function revertNotice(s) {
289
- const revert = s.tree.revert;
290
- if (!revert)
291
- return undefined;
292
- const boundary = s.messages?.find((message) => message.info.id === revert.messageID);
293
- const boundaryIndex = s.messages?.findIndex((message) => message.info.id === revert.messageID) ?? 0;
294
- const parent = s.messages
295
- ?.slice(0, boundaryIndex)
296
- .reverse()
297
- .find((message) => message.info.role === "user");
298
- const messageID = `msg_revert_${revert.messageID}`;
299
- const created = (boundary?.info.time.created ?? s.info.time.updated) - 1;
300
- return {
301
- info: {
302
- id: messageID,
303
- sessionID: s.info.id,
304
- role: "assistant",
305
- time: { created, completed: created },
306
- parentID: parent?.info.id ?? "",
307
- modelID: s.model?.modelID ?? "unknown",
308
- providerID: s.model?.providerID ?? "unknown",
309
- mode: "info",
310
- path: { cwd: s.info.directory, root: s.info.directory },
311
- cost: 0,
312
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
313
- },
314
- parts: [
315
- {
316
- id: `prt_revert_${revert.messageID}`,
317
- sessionID: s.info.id,
318
- messageID,
319
- type: "text",
320
- text: "Conversation reverted. Files were not changed.",
321
- },
322
- ],
323
- };
324
- }
@@ -40,6 +40,8 @@ export interface ManagedSession extends ThinkingState {
40
40
  /** undefined while dormant; resolved via get_state on create/revive. */
41
41
  model: OCModelRef | undefined;
42
42
  streaming: StreamingState | undefined;
43
+ /** A manual compaction started while idle; its end returns the session to idle. */
44
+ compactingFromIdle?: boolean;
43
45
  /** Cancels the adapter-owned user bash waiter and its OmO execution. */
44
46
  cancelShell?: (interrupted?: boolean) => void;
45
47
  /**
@@ -119,7 +121,6 @@ export declare class SessionManager {
119
121
  fork(sessionId: string, messageID?: string): Promise<OCSession>;
120
122
  revert(sessionId: string, messageID: string): Promise<OCSession>;
121
123
  unrevert(sessionId: string): Promise<OCSession>;
122
- private removeRevertNotice;
123
124
  reloadMessages(sessionId: string, signal?: AbortSignal): Promise<OCMessageWithParts[] | undefined>;
124
125
  /** Called only within the conversation operation queue, including RPC refreshes. */
125
126
  private reloadMessagesLive;
package/dist/sessions.js CHANGED
@@ -2,20 +2,20 @@
2
2
  * Session registry: one OpenCode session maps to one `pi --mode rpc` process.
3
3
  * Composes the live event bridge and restored-session history reconstruction.
4
4
  */
5
- import { id, PiEventTranslator } from "./pi-event-translation.js";
5
+ import { id, PiEventTranslator, syncSessionCost } from "./pi-event-translation.js";
6
6
  import { parsePiSessionFile, parsePiSessionText } from "./session-history.js";
7
7
  import { existsSync, readdirSync, statSync, unlinkSync } from "node:fs";
8
8
  import { join } from "node:path";
9
- import { PiRpcClient, RPC_COMMAND_TIMEOUT_MS } from "./pi-rpc.js";
9
+ import { COMPACT_RPC_TIMEOUT_MS, PiRpcClient, RPC_COMMAND_TIMEOUT_MS, } from "./pi-rpc.js";
10
10
  import { stopSessionHosts } from "./session-hosts.js";
11
11
  import { setThinkingLevel } from "./thinking-levels.js";
12
12
  import { SessionLifecycle } from "./lifecycle.js";
13
13
  import { ApprovalBridge, extensionUiRequestFrom } from "./approvals.js";
14
14
  import { getAgentName } from "./agent-name.js";
15
- import { bindMessageEntries, durableShellTree, forkConversation, getEntries, navigate, readSessionTree, revertConversation, revertNotice, SessionTreeError, unrevertConversation, } from "./session-tree.js";
15
+ import { bindMessageEntries, durableShellTree, forkConversation, getEntries, navigate, readSessionTree, revertConversation, SessionTreeError, unrevertConversation, } from "./session-tree.js";
16
16
  import { SubagentBridge } from "./subagents.js";
17
17
  import { sessionDiff } from "./session-diff.js";
18
- import { announceSessionNotice } from "./session-commands.js";
18
+ import { announceSessionNotice, slashTitlePrompt } from "./session-commands.js";
19
19
  import { ocIdFor, piSessionDir, piSessionsRootMtime, scanAllPiSessions, scanPiSessions, SessionPersistenceError, SessionStore, readSessionTitle, } from "./session-store.js";
20
20
  import { ADAPTER_VERSION } from "./version.js";
21
21
  export { ADAPTER_VERSION } from "./version.js";
@@ -324,11 +324,7 @@ export class SessionManager {
324
324
  properties: { sessionID: s.info.id, todos: s.todoBridge.todos },
325
325
  });
326
326
  }
327
- const notice = revertNotice(s);
328
- if (!notice)
329
- return messages;
330
- const boundary = messages.findIndex((message) => message.info.id === s.tree.revert?.messageID);
331
- return [...messages.slice(0, boundary), notice, ...messages.slice(boundary)];
327
+ return messages;
332
328
  }
333
329
  /** Append to canonical history, before the hidden redo branch while reverted. */
334
330
  appendMessage(sessionId, message) {
@@ -440,40 +436,24 @@ export class SessionManager {
440
436
  async revert(sessionId, messageID) {
441
437
  return this.treeSession(sessionId, async (s, pi) => {
442
438
  const tree = await this.syncTree(s, true);
443
- const previousNotice = revertNotice(s);
439
+ // OpenChamber renders its own reverted-message dock from session.revert.
444
440
  if (!(await revertConversation(s, pi, tree, messageID)))
445
441
  return s.info;
446
- this.removeRevertNotice(s, previousNotice);
447
442
  this.persistMeta(s);
448
443
  this.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
449
- const notice = revertNotice(s);
450
- if (notice) {
451
- this.emit(s.info.directory, { type: "message.updated", properties: { info: notice.info } });
452
- for (const part of notice.parts)
453
- this.events.emitPart(s, part);
454
- }
455
444
  return s.info;
456
445
  });
457
446
  }
458
447
  async unrevert(sessionId) {
459
448
  return this.treeSession(sessionId, async (s, pi) => {
460
449
  await this.syncTree(s, true);
461
- const previousNotice = revertNotice(s);
462
450
  if (!(await unrevertConversation(s, pi)))
463
451
  return s.info;
464
- this.removeRevertNotice(s, previousNotice);
465
452
  this.persistMeta(s);
466
453
  this.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
467
454
  return s.info;
468
455
  });
469
456
  }
470
- removeRevertNotice(s, notice) {
471
- if (notice)
472
- this.emit(s.info.directory, {
473
- type: "message.removed",
474
- properties: { sessionID: s.info.id, messageID: notice.info.id },
475
- });
476
- }
477
457
  async reloadMessages(sessionId, signal) {
478
458
  const s = this.get(sessionId);
479
459
  if (!s)
@@ -620,7 +600,8 @@ export class SessionManager {
620
600
  if (pi === undefined)
621
601
  throw new Error(`session cannot be resumed: ${sessionId}`);
622
602
  this.lifecycle.touch(s);
623
- const response = await pi.command(command, RPC_COMMAND_TIMEOUT_MS, signal, onDispatch);
603
+ const timeout = command.type === "compact" ? COMPACT_RPC_TIMEOUT_MS : RPC_COMMAND_TIMEOUT_MS;
604
+ const response = await pi.command(command, timeout, signal, onDispatch);
624
605
  if (!signal?.aborted && response.success && (command.type === "compact" || command.type === "bash"))
625
606
  await this.reloadMessagesLive(s, signal);
626
607
  return response;
@@ -744,7 +725,6 @@ export class SessionManager {
744
725
  });
745
726
  }
746
727
  }
747
- this.removeRevertNotice(s, revertNotice(s));
748
728
  delete s.tree.revert;
749
729
  delete s.info.revert;
750
730
  bindMessageEntries(s, await getEntries(pi));
@@ -829,6 +809,7 @@ export class SessionManager {
829
809
  message: text,
830
810
  streamingBehavior: delivery === "queue" ? "followUp" : "steer",
831
811
  clientMessageId: userMsg.id,
812
+ ...slashTitlePrompt(text),
832
813
  })
833
814
  .catch((error) => {
834
815
  rejectInput();
@@ -969,6 +950,8 @@ export class SessionManager {
969
950
  this.subagents.discover(s, s.messages);
970
951
  if (admissions)
971
952
  this.persistMeta(s);
953
+ if (syncSessionCost(s))
954
+ this.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
972
955
  }
973
956
  return s.messages;
974
957
  }
package/dist/subagents.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, watch, writeFileSync, } from "node:fs";
3
3
  import { basename, dirname, join } from "node:path";
4
4
  import { piAgentDir } from "./agent-dir.js";
5
+ import { syncSessionCost } from "./pi-event-translation.js";
5
6
  import { ocIdFor, SessionPersistenceError } from "./session-store.js";
6
7
  import { mapTool, resultText, toolMappers } from "./tool-map.js";
7
8
  import { ADAPTER_VERSION } from "./version.js";
@@ -457,6 +458,9 @@ export class SubagentBridge {
457
458
  child.session.messages = this.fallback(child);
458
459
  }
459
460
  child.loaded = true;
461
+ // Each child reports only its own spend; OpenChamber sums the subtree by parentID.
462
+ if (syncSessionCost(child.session))
463
+ this.emit(child.session.info.directory, { type: "session.updated", properties: { info: child.session.info } });
460
464
  child.session.status = { type: running ? "busy" : "idle" };
461
465
  if (!running)
462
466
  this.closeWatchers(child);
package/dist/types.d.ts CHANGED
@@ -17,6 +17,8 @@ export interface OCSession {
17
17
  revert?: {
18
18
  messageID: string;
19
19
  };
20
+ /** USD spent by this session's own assistant messages; OpenChamber adds child sessions via parentID. */
21
+ cost?: number;
20
22
  time: {
21
23
  created: number;
22
24
  updated: number;
@@ -68,6 +70,8 @@ export interface OCAssistantMessage {
68
70
  message: string;
69
71
  };
70
72
  };
73
+ /** Marks a compaction summary (OpenCode `AssistantMessage.summary`). */
74
+ summary?: boolean;
71
75
  }
72
76
  export type OCMessage = OCUserMessage | OCAssistantMessage;
73
77
  export interface OCTextPart {
@@ -144,7 +148,15 @@ export interface OCToolPart {
144
148
  tool: string;
145
149
  state: OCToolState;
146
150
  }
147
- export type OCPart = OCTextPart | OCReasoningPart | OCToolPart;
151
+ /** OpenChamber renders a user message holding this part as `/compact`. */
152
+ export interface OCCompactionPart {
153
+ id: string;
154
+ sessionID: string;
155
+ messageID: string;
156
+ type: "compaction";
157
+ auto: boolean;
158
+ }
159
+ export type OCPart = OCTextPart | OCReasoningPart | OCToolPart | OCCompactionPart;
148
160
  export interface OCMessageWithParts {
149
161
  info: OCMessage;
150
162
  parts: OCPart[];
package/dist/v2.js CHANGED
@@ -89,7 +89,7 @@ export function toV2Session(info, opts = {}) {
89
89
  id: info.id,
90
90
  projectID: info.projectID,
91
91
  agent: opts.agent ?? getAgentName(),
92
- cost: 0,
92
+ cost: info.cost ?? 0,
93
93
  tokens: zeroTokens(),
94
94
  time: {
95
95
  created: info.time.created,
@@ -109,10 +109,7 @@ export function toV2Session(info, opts = {}) {
109
109
  }
110
110
  /** v2 user message (list + prompt response): the UI reads `.text`. */
111
111
  export function toV2UserMessage(info) {
112
- const text = info.parts
113
- .filter((p) => p.type === "text")
114
- .map((p) => p.text)
115
- .join("");
112
+ const text = info.parts.map((p) => (p.type === "text" ? p.text : p.type === "compaction" ? "/compact" : "")).join("");
116
113
  return {
117
114
  id: info.info.id,
118
115
  time: { created: info.info.time.created },
@@ -142,7 +139,7 @@ export function toV2AssistantMessage(info) {
142
139
  },
143
140
  });
144
141
  }
145
- else {
142
+ else if (part.type === "tool") {
146
143
  content.push(toolContent(part));
147
144
  }
148
145
  }
@@ -412,6 +409,16 @@ export class V2EventTranslator {
412
409
  ];
413
410
  }
414
411
  onPartUpdated(directory, part, delta) {
412
+ if (part.type === "compaction")
413
+ // Like shell turns, the server creates this user turn; there is no optimistic prompt.
414
+ return [
415
+ this.frame(directory, "session.prompted", {
416
+ sessionID: part.sessionID,
417
+ messageID: part.messageID,
418
+ prompt: { text: "/compact" },
419
+ delivery: "queue",
420
+ }),
421
+ ];
415
422
  return part.type === "tool" ? this.onToolPart(directory, part) : this.onTextPart(directory, part, delta);
416
423
  }
417
424
  onTextPart(directory, part, delta) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",