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 +23 -0
- package/dist/pi-event-translation.d.ts +15 -1
- package/dist/pi-event-translation.js +99 -1
- package/dist/pi-rpc.d.ts +6 -0
- package/dist/pi-rpc.js +6 -0
- package/dist/session-commands.d.ts +8 -0
- package/dist/session-commands.js +9 -0
- package/dist/session-history.js +6 -1
- package/dist/session-tree.d.ts +0 -1
- package/dist/session-tree.js +0 -37
- package/dist/sessions.d.ts +2 -1
- package/dist/sessions.js +11 -28
- package/dist/subagents.js +4 -0
- package/dist/types.d.ts +13 -1
- package/dist/v2.js +13 -6
- package/package.json +1 -1
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
|
|
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;
|
package/dist/session-commands.js
CHANGED
|
@@ -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
|
}
|
package/dist/session-history.js
CHANGED
|
@@ -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;
|
package/dist/session-tree.d.ts
CHANGED
|
@@ -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;
|
package/dist/session-tree.js
CHANGED
|
@@ -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
|
-
}
|
package/dist/sessions.d.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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) {
|