antechamber 0.2.23 → 0.2.25
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 +16 -0
- package/dist/background-results.d.ts +51 -0
- package/dist/background-results.js +277 -0
- package/dist/monitor-sessions.d.ts +6 -0
- package/dist/monitor-sessions.js +18 -0
- package/dist/pi-event-translation.d.ts +5 -1
- package/dist/pi-event-translation.js +96 -33
- package/dist/session-history.js +41 -29
- package/dist/session-tree.d.ts +1 -0
- package/dist/session-tree.js +16 -0
- package/dist/sessions.js +1 -0
- package/dist/subagents.d.ts +6 -0
- package/dist/subagents.js +32 -0
- package/dist/tool-map.d.ts +15 -0
- package/dist/tool-map.js +71 -0
- package/extensions/omo-compat/package.json +1 -1
- package/extensions/omo-dag/package.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project uses [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.25] - 2026-10-17
|
|
11
|
+
|
|
12
|
+
- feat(tools): show subagents and monitors started inside eval (#30)
|
|
13
|
+
|
|
14
|
+
## [0.2.24] - 2026-10-17
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Show completed background bash output and detached eval results in OpenChamber, including after reloading history.
|
|
19
|
+
A card that started background work keeps running until that work finishes.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Show subagents and monitors started inside an eval cell as subagent cards that open their session, live and after
|
|
24
|
+
reloading history. A failed `agent()` call keeps its error.
|
|
25
|
+
|
|
10
26
|
## [0.2.23] - 2026-10-17
|
|
11
27
|
|
|
12
28
|
- feat(monitors): show matched lines in monitor sessions (#32)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ManagedSession } from "./sessions.js";
|
|
2
|
+
import type { OCMessageWithParts, OCModelRef, OCToolPart } from "./types.js";
|
|
3
|
+
import { type ToolResult } from "./tool-map.js";
|
|
4
|
+
export interface BackgroundResult {
|
|
5
|
+
/** Content hash; identical notifications are told apart by `backgroundID`'s occurrence index. */
|
|
6
|
+
readonly key: string;
|
|
7
|
+
readonly kind: "terminal" | "eval";
|
|
8
|
+
readonly callID: string;
|
|
9
|
+
readonly title: string;
|
|
10
|
+
readonly output: string;
|
|
11
|
+
readonly failed: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Raw RPC capture of installed omo-ai 5.1.33 / senpi runtime, 2026-10-17. */
|
|
14
|
+
export declare function backgroundResult(customType: unknown, content: unknown): BackgroundResult | undefined;
|
|
15
|
+
/** Retain only the command needed to label a later terminal completion. */
|
|
16
|
+
export declare function backgroundCommands(result: ToolResult): {
|
|
17
|
+
backgroundCommands?: Record<string, string>;
|
|
18
|
+
};
|
|
19
|
+
export declare function launchedShells(text: string): string[];
|
|
20
|
+
/**
|
|
21
|
+
* What a tool result leaves running: the background shells it started, and the eval cell
|
|
22
|
+
* itself when it detached. The card keeps running until each one reports finishing.
|
|
23
|
+
*/
|
|
24
|
+
export declare function backgroundWaits(callID: string, text: string, commands?: Record<string, unknown>): string[];
|
|
25
|
+
interface Final {
|
|
26
|
+
output: string;
|
|
27
|
+
failed: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** Keep a finished card running, with its result already shown, until `waits` report finishing. */
|
|
30
|
+
export declare function holdForBackground(part: OCToolPart, waits: string[], final: Final): void;
|
|
31
|
+
/** `id` finished at `at`: drop it from every card waiting on it; cards left with nothing to wait on end. */
|
|
32
|
+
export declare function releaseBackground(messages: readonly OCMessageWithParts[], id: string, at: number): OCToolPart[];
|
|
33
|
+
/** Nothing can still be running (the agent exited, or history is rebuilt): end every waiting card. */
|
|
34
|
+
export declare function settleBackground(messages: readonly OCMessageWithParts[], at: number): OCToolPart[];
|
|
35
|
+
export declare function completeBackgroundEval(messages: readonly OCMessageWithParts[], result: BackgroundResult, completed: number): OCToolPart | undefined;
|
|
36
|
+
/** A tool result finishes its card at `at`; a result that left work running keeps it running. */
|
|
37
|
+
export declare function finishToolPart(part: OCToolPart, result: ToolResult, callID: string, at: number): void;
|
|
38
|
+
/**
|
|
39
|
+
* OmO persists these notifications but sends no entry_appended frame for them, so the live
|
|
40
|
+
* path never learns the native entry ID (raw RPC capture of installed omo-ai 5.1.33 / senpi
|
|
41
|
+
* runtime, 2026-10-17). Live, history and entry binding all derive the same ID from the
|
|
42
|
+
* content and its occurrence count in conversation order instead.
|
|
43
|
+
*/
|
|
44
|
+
export declare function backgroundID(messages: readonly OCMessageWithParts[], result: BackgroundResult): string;
|
|
45
|
+
export declare function backgroundMessage(s: Pick<ManagedSession, "info" | "messages">, result: BackgroundResult, entry: {
|
|
46
|
+
id: string;
|
|
47
|
+
created: number;
|
|
48
|
+
model: OCModelRef;
|
|
49
|
+
agent: string;
|
|
50
|
+
}): OCMessageWithParts;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { resultMetadata, resultText, toolFailed } from "./tool-map.js";
|
|
3
|
+
/** Raw RPC capture of installed omo-ai 5.1.33 / senpi runtime, 2026-10-17. */
|
|
4
|
+
export function backgroundResult(customType, content) {
|
|
5
|
+
if (customType !== "senpi-terminal:notification" && customType !== "senpi-codemode:notification")
|
|
6
|
+
return undefined;
|
|
7
|
+
const raw = typeof content === "string" ? content : resultText(content);
|
|
8
|
+
const key = createHash("sha256").update(`${customType}\n${raw}`).digest("hex").slice(0, 16);
|
|
9
|
+
const text = raw.replaceAll(/<\/?system-reminder>/g, "").replace(/^\n/, "");
|
|
10
|
+
const newline = text.indexOf("\n");
|
|
11
|
+
const header = newline < 0 ? text : text.slice(0, newline);
|
|
12
|
+
const body = newline < 0 ? "" : text.slice(newline + 1);
|
|
13
|
+
if (customType === "senpi-terminal:notification") {
|
|
14
|
+
const match = /^Background terminal session (\S+) finished: (.+?)\.?$/.exec(header);
|
|
15
|
+
if (match?.[1] === undefined || match[2] === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
return {
|
|
18
|
+
key,
|
|
19
|
+
kind: "terminal",
|
|
20
|
+
callID: match[1],
|
|
21
|
+
title: `${match[1]} finished: ${match[2]}`,
|
|
22
|
+
output: body.replace(/^Final output:\r?\n?/, ""),
|
|
23
|
+
failed: false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const match = /^Detached eval cell (\S+)(?: \([^)]+\))? (completed|failed|error)\b.*$/i.exec(header);
|
|
27
|
+
if (match?.[1] === undefined || match[2] === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
return {
|
|
30
|
+
key,
|
|
31
|
+
kind: "eval",
|
|
32
|
+
callID: match[1],
|
|
33
|
+
title: header.replace(/^Detached eval cell /, "").replace(/\.$/, ""),
|
|
34
|
+
output: body,
|
|
35
|
+
failed: match[2].toLowerCase() !== "completed",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Retain only the command needed to label a later terminal completion. */
|
|
39
|
+
export function backgroundCommands(result) {
|
|
40
|
+
const details = result.details;
|
|
41
|
+
if (details === null || typeof details !== "object" || !("toolCalls" in details))
|
|
42
|
+
return {};
|
|
43
|
+
if (!Array.isArray(details.toolCalls))
|
|
44
|
+
return {};
|
|
45
|
+
const commands = {};
|
|
46
|
+
for (const call of details.toolCalls) {
|
|
47
|
+
if (call?.name !== "bash" || typeof call.resultPreview !== "string" || typeof call.args?.command !== "string")
|
|
48
|
+
continue;
|
|
49
|
+
for (const id of launchedShells(call.resultPreview))
|
|
50
|
+
commands[id] = call.args.command;
|
|
51
|
+
}
|
|
52
|
+
return Object.keys(commands).length === 0 ? {} : { backgroundCommands: commands };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Background shells a result says it left running (raw RPC captures, 2026-10-17). A cell's output is
|
|
56
|
+
* trimmed to its tail, which can cut the "with ID" line and leave only the bash_output hint.
|
|
57
|
+
*/
|
|
58
|
+
const LAUNCHED = /\b(?:running in background|auto-detached to background) with ID:\s*(bash_\d+)\b|Use bash_output\(\{ bash_id: "(bash_\d+)" \}\) only to peek/g;
|
|
59
|
+
export function launchedShells(text) {
|
|
60
|
+
return [...new Set([...text.matchAll(LAUNCHED)].map((match) => match[1] ?? match[2] ?? "").filter(Boolean))];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* What a tool result leaves running: the background shells it started, and the eval cell
|
|
64
|
+
* itself when it detached. The card keeps running until each one reports finishing.
|
|
65
|
+
*/
|
|
66
|
+
export function backgroundWaits(callID, text, commands = {}) {
|
|
67
|
+
const waits = new Set();
|
|
68
|
+
for (const id of launchedShells(text))
|
|
69
|
+
waits.add(id);
|
|
70
|
+
for (const id of Object.keys(commands))
|
|
71
|
+
waits.add(id);
|
|
72
|
+
if (/^Eval cell \S+ detached and is running/.test(text))
|
|
73
|
+
waits.add(callID);
|
|
74
|
+
return [...waits];
|
|
75
|
+
}
|
|
76
|
+
function waitsOf(part) {
|
|
77
|
+
const waits = part.state.status === "running" ? part.state.metadata?.backgroundWaits : undefined;
|
|
78
|
+
return Array.isArray(waits) ? waits.filter((id) => typeof id === "string") : [];
|
|
79
|
+
}
|
|
80
|
+
/** Keep a finished card running, with its result already shown, until `waits` report finishing. */
|
|
81
|
+
export function holdForBackground(part, waits, final) {
|
|
82
|
+
const state = part.state;
|
|
83
|
+
if (state.status === "pending" || waits.length === 0)
|
|
84
|
+
return;
|
|
85
|
+
const title = state.status === "running" || state.status === "completed" ? state.title : undefined;
|
|
86
|
+
part.state = {
|
|
87
|
+
status: "running",
|
|
88
|
+
input: state.input,
|
|
89
|
+
...(title !== undefined ? { title } : {}),
|
|
90
|
+
metadata: { ...state.metadata, output: final.output, backgroundWaits: waits, backgroundFailed: final.failed },
|
|
91
|
+
time: { start: state.time.start },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function finish(part, final, at) {
|
|
95
|
+
const state = part.state;
|
|
96
|
+
if (state.status === "pending")
|
|
97
|
+
return;
|
|
98
|
+
const { backgroundWaits: _waits, backgroundFailed: _failed, output: _output, ...metadata } = state.metadata ?? {};
|
|
99
|
+
const time = { start: state.time.start, end: Math.max(at, state.time.start) };
|
|
100
|
+
const title = state.status === "running" || state.status === "completed" ? (state.title ?? part.tool) : part.tool;
|
|
101
|
+
part.state = final.failed
|
|
102
|
+
? { status: "error", input: state.input, error: final.output || `tool ${part.tool} failed`, metadata, time }
|
|
103
|
+
: { status: "completed", input: state.input, output: final.output, title, metadata, time };
|
|
104
|
+
}
|
|
105
|
+
function finalOf(part) {
|
|
106
|
+
const metadata = part.state.status === "running" ? part.state.metadata : undefined;
|
|
107
|
+
if (typeof metadata?.backgroundFailed !== "boolean")
|
|
108
|
+
return undefined;
|
|
109
|
+
return { output: typeof metadata.output === "string" ? metadata.output : "", failed: metadata.backgroundFailed };
|
|
110
|
+
}
|
|
111
|
+
/** `id` finished at `at`: drop it from every card waiting on it; cards left with nothing to wait on end. */
|
|
112
|
+
export function releaseBackground(messages, id, at) {
|
|
113
|
+
const changed = [];
|
|
114
|
+
for (const message of messages) {
|
|
115
|
+
for (const part of message.parts) {
|
|
116
|
+
if (part.type !== "tool" || part.state.status !== "running")
|
|
117
|
+
continue;
|
|
118
|
+
const waits = waitsOf(part);
|
|
119
|
+
if (!waits.includes(id))
|
|
120
|
+
continue;
|
|
121
|
+
const left = waits.filter((wait) => wait !== id);
|
|
122
|
+
const final = finalOf(part);
|
|
123
|
+
if (left.length > 0 || final === undefined)
|
|
124
|
+
part.state.metadata = { ...part.state.metadata, backgroundWaits: left };
|
|
125
|
+
else
|
|
126
|
+
finish(part, final, at);
|
|
127
|
+
changed.push(part);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return changed;
|
|
131
|
+
}
|
|
132
|
+
/** Nothing can still be running (the agent exited, or history is rebuilt): end every waiting card. */
|
|
133
|
+
export function settleBackground(messages, at) {
|
|
134
|
+
const changed = [];
|
|
135
|
+
for (const message of messages) {
|
|
136
|
+
for (const part of message.parts) {
|
|
137
|
+
if (part.type !== "tool")
|
|
138
|
+
continue;
|
|
139
|
+
const final = finalOf(part);
|
|
140
|
+
if (final === undefined)
|
|
141
|
+
continue;
|
|
142
|
+
finish(part, final, at);
|
|
143
|
+
changed.push(part);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return changed;
|
|
147
|
+
}
|
|
148
|
+
export function completeBackgroundEval(messages, result, completed) {
|
|
149
|
+
if (result.kind !== "eval")
|
|
150
|
+
return undefined;
|
|
151
|
+
for (const message of messages) {
|
|
152
|
+
const part = message.parts.find((part) => part.type === "tool" && part.callID === result.callID);
|
|
153
|
+
if (part === undefined)
|
|
154
|
+
continue;
|
|
155
|
+
const final = { output: result.output, failed: result.failed };
|
|
156
|
+
// The cell is done; shells it moved to the background may still run.
|
|
157
|
+
const waits = [
|
|
158
|
+
...waitsOf(part).filter((wait) => wait !== result.callID),
|
|
159
|
+
...backgroundWaits(result.callID, result.output),
|
|
160
|
+
];
|
|
161
|
+
const remaining = [...new Set(waits)];
|
|
162
|
+
if (part.state.status !== "pending" && remaining.length > 0) {
|
|
163
|
+
holdForBackground(part, remaining, final);
|
|
164
|
+
}
|
|
165
|
+
else
|
|
166
|
+
finish(part, final, completed);
|
|
167
|
+
return part;
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
/** A tool result finishes its card at `at`; a result that left work running keeps it running. */
|
|
172
|
+
export function finishToolPart(part, result, callID, at) {
|
|
173
|
+
const state = part.state;
|
|
174
|
+
const text = resultText(result.content);
|
|
175
|
+
const commands = backgroundCommands(result);
|
|
176
|
+
const metadata = {
|
|
177
|
+
...(state.status !== "pending" ? state.metadata : {}),
|
|
178
|
+
...resultMetadata(part.tool, result),
|
|
179
|
+
...commands,
|
|
180
|
+
};
|
|
181
|
+
const time = { start: state.status !== "pending" ? state.time.start : at, end: at };
|
|
182
|
+
const title = state.status === "running" || state.status === "completed" ? (state.title ?? part.tool) : part.tool;
|
|
183
|
+
const failed = toolFailed(result);
|
|
184
|
+
part.state = failed
|
|
185
|
+
? { status: "error", input: state.input ?? {}, error: text || `tool ${part.tool} failed`, metadata, time }
|
|
186
|
+
: { status: "completed", input: state.input ?? {}, output: text, title, metadata, time };
|
|
187
|
+
const waits = backgroundWaits(callID, text, commands.backgroundCommands);
|
|
188
|
+
if (waits.length > 0)
|
|
189
|
+
holdForBackground(part, waits, { output: text, failed });
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* OmO persists these notifications but sends no entry_appended frame for them, so the live
|
|
193
|
+
* path never learns the native entry ID (raw RPC capture of installed omo-ai 5.1.33 / senpi
|
|
194
|
+
* runtime, 2026-10-17). Live, history and entry binding all derive the same ID from the
|
|
195
|
+
* content and its occurrence count in conversation order instead.
|
|
196
|
+
*/
|
|
197
|
+
export function backgroundID(messages, result) {
|
|
198
|
+
const prefix = `msg_background_${result.key}_`;
|
|
199
|
+
return `${result.key}_${messages.filter((message) => message.info.id.startsWith(prefix)).length}`;
|
|
200
|
+
}
|
|
201
|
+
export function backgroundMessage(s, result, entry) {
|
|
202
|
+
const messages = s.messages ?? [];
|
|
203
|
+
let command = result.callID;
|
|
204
|
+
// The card that launched the command started it, so the finished card shows the real run time.
|
|
205
|
+
let started = entry.created;
|
|
206
|
+
if (result.kind === "terminal") {
|
|
207
|
+
// Newest first, stopping at an earlier finish of the same id: OmO numbers shells from bash_1 again
|
|
208
|
+
// after its process restarts, so an older launch of that id is not this command's.
|
|
209
|
+
search: for (const message of messages.toReversed()) {
|
|
210
|
+
for (const part of message.parts.toReversed()) {
|
|
211
|
+
if (part.type !== "tool" || part.state.status === "pending")
|
|
212
|
+
continue;
|
|
213
|
+
if (part.callID.startsWith("call_background_") &&
|
|
214
|
+
part.state.input.command?.toString().split("\n")[0] === result.callID)
|
|
215
|
+
break search;
|
|
216
|
+
const commands = part.state.metadata?.backgroundCommands;
|
|
217
|
+
const nested = commands !== null && typeof commands === "object" && result.callID in commands
|
|
218
|
+
? Reflect.get(commands, result.callID)
|
|
219
|
+
: undefined;
|
|
220
|
+
const held = part.state.metadata?.output;
|
|
221
|
+
const output = part.state.status === "completed"
|
|
222
|
+
? part.state.output
|
|
223
|
+
: part.state.status === "error"
|
|
224
|
+
? part.state.error
|
|
225
|
+
: typeof held === "string"
|
|
226
|
+
? held
|
|
227
|
+
: "";
|
|
228
|
+
const original = typeof nested === "string"
|
|
229
|
+
? nested
|
|
230
|
+
: launchedShells(output).includes(result.callID)
|
|
231
|
+
? (part.state.input.command ?? part.state.input.code)
|
|
232
|
+
: undefined;
|
|
233
|
+
if (typeof original === "string") {
|
|
234
|
+
command += `\n${original}`;
|
|
235
|
+
started = Math.min(part.state.time.start, entry.created);
|
|
236
|
+
break search;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const sessionID = s.info.id;
|
|
242
|
+
const messageID = `msg_background_${entry.id}`;
|
|
243
|
+
return {
|
|
244
|
+
info: {
|
|
245
|
+
id: messageID,
|
|
246
|
+
sessionID,
|
|
247
|
+
role: "assistant",
|
|
248
|
+
parentID: messages.findLast((message) => message.info.role === "user")?.info.id ?? "",
|
|
249
|
+
time: { created: entry.created, completed: entry.created },
|
|
250
|
+
modelID: entry.model.modelID,
|
|
251
|
+
providerID: entry.model.providerID,
|
|
252
|
+
mode: entry.agent,
|
|
253
|
+
path: { cwd: s.info.directory, root: s.info.directory },
|
|
254
|
+
cost: 0,
|
|
255
|
+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
256
|
+
finish: "stop",
|
|
257
|
+
},
|
|
258
|
+
parts: [
|
|
259
|
+
{
|
|
260
|
+
id: `prt_background_${entry.id}`,
|
|
261
|
+
sessionID,
|
|
262
|
+
messageID,
|
|
263
|
+
type: "tool",
|
|
264
|
+
tool: "bash",
|
|
265
|
+
callID: `call_background_${entry.id}`,
|
|
266
|
+
state: {
|
|
267
|
+
status: "completed",
|
|
268
|
+
input: { command },
|
|
269
|
+
output: result.output,
|
|
270
|
+
title: result.title,
|
|
271
|
+
metadata: {},
|
|
272
|
+
time: { start: started, end: entry.created },
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
@@ -37,6 +37,12 @@ export declare class MonitorSessions {
|
|
|
37
37
|
observe(parent: ManagedSession, ev: Record<string, unknown>): void;
|
|
38
38
|
/** A hidden `senpi-monitor:notification` custom message: append its lines to each watch it names. */
|
|
39
39
|
notify(parent: ManagedSession, message: Record<string, unknown>): void;
|
|
40
|
+
/**
|
|
41
|
+
* The child session of the newest watch a parent started with these monitor arguments.
|
|
42
|
+
* A monitor call recorded inside eval keeps only its arguments, not the `bash_N` id the
|
|
43
|
+
* child is keyed by, so command and description identify it.
|
|
44
|
+
*/
|
|
45
|
+
findChild(parentID: string, args: Record<string, unknown>): string | undefined;
|
|
40
46
|
/** The agent process is gone: OmO's in-process watches died with it. */
|
|
41
47
|
stopParent(parentID: string): void;
|
|
42
48
|
/** Forget a deleted child, or every child of a deleted parent. */
|
package/dist/monitor-sessions.js
CHANGED
|
@@ -120,6 +120,24 @@ export class MonitorSessions {
|
|
|
120
120
|
this.publish(watch);
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* The child session of the newest watch a parent started with these monitor arguments.
|
|
125
|
+
* A monitor call recorded inside eval keeps only its arguments, not the `bash_N` id the
|
|
126
|
+
* child is keyed by, so command and description identify it.
|
|
127
|
+
*/
|
|
128
|
+
findChild(parentID, args) {
|
|
129
|
+
let found;
|
|
130
|
+
for (const watch of this.watches.values()) {
|
|
131
|
+
const { monitor } = watch;
|
|
132
|
+
if (watch.session.info.parentID !== parentID || monitor.command !== args.command)
|
|
133
|
+
continue;
|
|
134
|
+
if (typeof args.description === "string" && monitor.description !== args.description)
|
|
135
|
+
continue;
|
|
136
|
+
if (found === undefined || monitor.startedAtMs > found.monitor.startedAtMs)
|
|
137
|
+
found = watch;
|
|
138
|
+
}
|
|
139
|
+
return found?.session.info.id;
|
|
140
|
+
}
|
|
123
141
|
/** The agent process is gone: OmO's in-process watches died with it. */
|
|
124
142
|
stopParent(parentID) {
|
|
125
143
|
for (const watch of this.watches.values()) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
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
|
-
import type
|
|
5
|
+
import { type SubagentBridge } from "./subagents.js";
|
|
6
6
|
import { type MonitorSessions } from "./monitor-sessions.js";
|
|
7
7
|
import { type TodoUpdated } from "./todo-bridge.js";
|
|
8
8
|
export interface PiUsage {
|
|
@@ -62,6 +62,7 @@ export declare class PiEventTranslator {
|
|
|
62
62
|
private reconcileUser;
|
|
63
63
|
onPiEvent(s: ManagedSession, ev: Record<string, unknown>): void;
|
|
64
64
|
private onTodoEntry;
|
|
65
|
+
private onBackgroundEntry;
|
|
65
66
|
private onLifecycleEvent;
|
|
66
67
|
private onToolEvent;
|
|
67
68
|
private onMessageEvent;
|
|
@@ -81,6 +82,9 @@ export declare class PiEventTranslator {
|
|
|
81
82
|
private onToolcallEvent;
|
|
82
83
|
/** turn_end.toolResults → finalize tool parts (completed / error). */
|
|
83
84
|
private applyToolResults;
|
|
85
|
+
/** The agent process is gone, and its background shells with it: end cards still waiting on them. */
|
|
86
|
+
settleBackground(s: ManagedSession): void;
|
|
87
|
+
private onNestedTools;
|
|
84
88
|
/** Complete an ended assistant message whose completion waited on its tools; true when it changed. */
|
|
85
89
|
private completeSettled;
|
|
86
90
|
/** Tool parts that never received a result (abort, crash) must not spin forever. */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Pi message/part translation and the live event bridge, with explicit manager services. */
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { evalAgentSession } from "./subagents.js";
|
|
3
4
|
import { MONITOR_NOTIFICATION } from "./monitor-sessions.js";
|
|
4
|
-
import { applyProviderToolResults, finishProviderToolParts, mapTool,
|
|
5
|
+
import { applyProviderToolResults, finishProviderToolParts, mapTool, nestedToolParts, providerToolParts, resultText, toolFailed, } from "./tool-map.js";
|
|
5
6
|
import { TodoBridge } from "./todo-bridge.js";
|
|
6
7
|
import { readSessionTree, recordTurn } from "./session-tree.js";
|
|
7
8
|
import { touchedFiles } from "./session-diff.js";
|
|
@@ -9,6 +10,7 @@ import { getAgentName } from "./agent-name.js";
|
|
|
9
10
|
import { userMessageModel } from "./thinking-levels.js";
|
|
10
11
|
import { debugLog } from "./misc-routes.js";
|
|
11
12
|
import { envSetting } from "./config.js";
|
|
13
|
+
import { backgroundID, backgroundMessage, backgroundResult, completeBackgroundEval, finishToolPart, releaseBackground, settleBackground, } from "./background-results.js";
|
|
12
14
|
export function id(prefix) {
|
|
13
15
|
return `${prefix}_${randomUUID().replaceAll("-", "")}`;
|
|
14
16
|
}
|
|
@@ -191,7 +193,7 @@ export class PiEventTranslator {
|
|
|
191
193
|
});
|
|
192
194
|
if (status.type === "idle") {
|
|
193
195
|
s.info.time.idle = Date.now();
|
|
194
|
-
const last = s.messages?.findLast((message) => message.info.role === "assistant");
|
|
196
|
+
const last = s.messages?.findLast((message) => message.info.role === "assistant" && !message.info.id.startsWith("msg_background_"));
|
|
195
197
|
s.info.outcome =
|
|
196
198
|
s.interruptRequested || (last?.info.role === "assistant" && last.info.error?.name === "MessageAbortedError")
|
|
197
199
|
? "interrupted"
|
|
@@ -269,6 +271,7 @@ export class PiEventTranslator {
|
|
|
269
271
|
switch (ev.type) {
|
|
270
272
|
case "entry_appended":
|
|
271
273
|
this.onTodoEntry(s, ev);
|
|
274
|
+
this.onBackgroundEntry(s, ev.entry, "entry");
|
|
272
275
|
break;
|
|
273
276
|
case "session_info_changed":
|
|
274
277
|
this.services.applyNativeTitle(s, ev.name);
|
|
@@ -308,6 +311,54 @@ export class PiEventTranslator {
|
|
|
308
311
|
if (event !== undefined)
|
|
309
312
|
this.services.emit(s.info.directory, event);
|
|
310
313
|
}
|
|
314
|
+
onBackgroundEntry(s, value, source = "message") {
|
|
315
|
+
if (value === null || typeof value !== "object")
|
|
316
|
+
return;
|
|
317
|
+
if (!("customType" in value) || !("content" in value))
|
|
318
|
+
return;
|
|
319
|
+
const result = backgroundResult(value.customType, value.content);
|
|
320
|
+
if (result === undefined)
|
|
321
|
+
return;
|
|
322
|
+
const messages = this.services.ensureMessages(s);
|
|
323
|
+
const timestamp = "timestamp" in value ? value.timestamp : undefined;
|
|
324
|
+
const parsed = typeof timestamp === "string" ? Date.parse(timestamp) : timestamp;
|
|
325
|
+
const created = typeof parsed === "number" && Number.isFinite(parsed) ? parsed : Date.now();
|
|
326
|
+
const part = completeBackgroundEval(messages, result, created);
|
|
327
|
+
const prefix = `msg_background_${result.key}_`;
|
|
328
|
+
const unbound = messages.findLast((message) => message.info.id.startsWith(prefix) && s.tree.messageEntries[message.info.id] === undefined);
|
|
329
|
+
// A finished shell also ends the wait of the card that launched it (once, from the live message).
|
|
330
|
+
if (result.kind === "terminal" && source === "message")
|
|
331
|
+
for (const waiting of releaseBackground(messages, result.callID, created))
|
|
332
|
+
this.emitPart(s, waiting);
|
|
333
|
+
if (part !== undefined)
|
|
334
|
+
this.emitPart(s, part);
|
|
335
|
+
else if (source === "entry" && unbound !== undefined) {
|
|
336
|
+
// The live custom message already created this card; the entry only supplies its native ID.
|
|
337
|
+
if ("id" in value && typeof value.id === "string")
|
|
338
|
+
s.tree.messageEntries[unbound.info.id] = value.id;
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
const message = backgroundMessage(s, result, {
|
|
342
|
+
id: backgroundID(messages, result),
|
|
343
|
+
created,
|
|
344
|
+
model: s.model ?? { providerID: "unknown", modelID: "unknown" },
|
|
345
|
+
agent: getAgentName(),
|
|
346
|
+
});
|
|
347
|
+
if (messages.some((existing) => existing.info.id === message.info.id))
|
|
348
|
+
return;
|
|
349
|
+
if ("id" in value && typeof value.id === "string")
|
|
350
|
+
s.tree.messageEntries[message.info.id] = value.id;
|
|
351
|
+
const boundary = messages.findIndex((item) => item.info.id === s.tree.revert?.messageID);
|
|
352
|
+
messages.splice(boundary < 0 ? messages.length : boundary, 0, message);
|
|
353
|
+
this.services.emit(s.info.directory, { type: "message.updated", properties: { info: message.info } });
|
|
354
|
+
for (const part of message.parts)
|
|
355
|
+
this.emitPart(s, part);
|
|
356
|
+
}
|
|
357
|
+
if ("id" in value && typeof value.id === "string") {
|
|
358
|
+
recordTurn(s);
|
|
359
|
+
this.services.persistMeta(s);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
311
362
|
onLifecycleEvent(s, ev) {
|
|
312
363
|
switch (ev.type) {
|
|
313
364
|
case "agent_start":
|
|
@@ -426,6 +477,7 @@ export class PiEventTranslator {
|
|
|
426
477
|
if (output.trim() !== "")
|
|
427
478
|
part.state.metadata = { ...part.state.metadata, output };
|
|
428
479
|
this.emitPart(s, part);
|
|
480
|
+
this.onNestedTools(s, part, partial !== null && typeof partial === "object" && "details" in partial ? partial.details : undefined);
|
|
429
481
|
return;
|
|
430
482
|
}
|
|
431
483
|
case "tool_execution_end": {
|
|
@@ -449,6 +501,13 @@ export class PiEventTranslator {
|
|
|
449
501
|
this.services.monitors.notify(s, custom);
|
|
450
502
|
return;
|
|
451
503
|
}
|
|
504
|
+
if (ev.message !== null &&
|
|
505
|
+
typeof ev.message === "object" &&
|
|
506
|
+
"role" in ev.message &&
|
|
507
|
+
ev.message.role === "custom") {
|
|
508
|
+
this.onBackgroundEntry(s, ev.message);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
452
511
|
const msg = ev.message;
|
|
453
512
|
if (msg?.role !== "assistant")
|
|
454
513
|
return; // user message already recorded at prompt()
|
|
@@ -695,8 +754,11 @@ export class PiEventTranslator {
|
|
|
695
754
|
// Task expansion replaces the streaming placeholder with one or more
|
|
696
755
|
// cards sharing its callID. Order those cards without reconstructing it.
|
|
697
756
|
const indices = new Map([...streaming.parts].map(([index, part]) => [part.type === "tool" ? part.callID : part.id, index]));
|
|
698
|
-
|
|
699
|
-
|
|
757
|
+
const order = (part) => {
|
|
758
|
+
const parent = part.type === "tool" && part.state.status !== "pending" ? part.state.metadata?.parentCallID : undefined;
|
|
759
|
+
return (indices.get(typeof parent === "string" ? parent : part.type === "tool" ? part.callID : part.id) ?? Infinity);
|
|
760
|
+
};
|
|
761
|
+
owner.parts.sort((a, b) => order(a) - order(b));
|
|
700
762
|
}
|
|
701
763
|
}
|
|
702
764
|
/**
|
|
@@ -793,39 +855,17 @@ export class PiEventTranslator {
|
|
|
793
855
|
continue;
|
|
794
856
|
}
|
|
795
857
|
const part = callID !== undefined ? s.toolByCallId.get(callID) : undefined;
|
|
858
|
+
const parent = part ??
|
|
859
|
+
s.messages
|
|
860
|
+
?.flatMap((message) => message.parts)
|
|
861
|
+
.find((item) => item.type === "tool" && item.callID === callID);
|
|
862
|
+
if (parent !== undefined)
|
|
863
|
+
this.onNestedTools(s, parent, tr.details);
|
|
796
864
|
if (!part || callID === undefined)
|
|
797
865
|
continue;
|
|
798
866
|
s.toolByCallId.delete(callID);
|
|
799
867
|
s.todoBridge?.finish(part, toolFailed(tr));
|
|
800
|
-
|
|
801
|
-
const startedAt = part.state.status !== "pending" ? part.state.time.start : Date.now();
|
|
802
|
-
const input = part.state.input ?? {};
|
|
803
|
-
const metadata = {
|
|
804
|
-
...(part.state.status !== "pending" ? part.state.metadata : {}),
|
|
805
|
-
...resultMetadata(part.tool, tr),
|
|
806
|
-
};
|
|
807
|
-
const title = part.state.status === "running" || part.state.status === "completed"
|
|
808
|
-
? (part.state.title ?? part.tool)
|
|
809
|
-
: part.tool;
|
|
810
|
-
if (toolFailed(tr)) {
|
|
811
|
-
part.state = {
|
|
812
|
-
status: "error",
|
|
813
|
-
input,
|
|
814
|
-
error: text || `tool ${part.tool} failed`,
|
|
815
|
-
metadata,
|
|
816
|
-
time: { start: startedAt, end: Date.now() },
|
|
817
|
-
};
|
|
818
|
-
}
|
|
819
|
-
else {
|
|
820
|
-
part.state = {
|
|
821
|
-
status: "completed",
|
|
822
|
-
input,
|
|
823
|
-
output: text,
|
|
824
|
-
title,
|
|
825
|
-
metadata,
|
|
826
|
-
time: { start: startedAt, end: Date.now() },
|
|
827
|
-
};
|
|
828
|
-
}
|
|
868
|
+
finishToolPart(part, tr, callID, Date.now());
|
|
829
869
|
if (touchedFiles([part]).length > 0)
|
|
830
870
|
this.services.dirtyDiffs.add(s.info.id);
|
|
831
871
|
this.emitPart(s, part);
|
|
@@ -836,6 +876,29 @@ export class PiEventTranslator {
|
|
|
836
876
|
this.services.emit(s.info.directory, { type: "message.updated", properties: { info: owner.info } });
|
|
837
877
|
}
|
|
838
878
|
}
|
|
879
|
+
/** The agent process is gone, and its background shells with it: end cards still waiting on them. */
|
|
880
|
+
settleBackground(s) {
|
|
881
|
+
for (const part of settleBackground(s.messages ?? [], Date.now()))
|
|
882
|
+
this.emitPart(s, part);
|
|
883
|
+
}
|
|
884
|
+
onNestedTools(s, parent, details) {
|
|
885
|
+
const nested = nestedToolParts(parent, details, {
|
|
886
|
+
monitor: (args) => this.services.monitors.findChild(s.info.id, args),
|
|
887
|
+
agent: (args, claimed, after) => evalAgentSession(s.piSessionId ?? s.restored?.piSessionId, args, claimed, after),
|
|
888
|
+
});
|
|
889
|
+
if (nested.length === 0)
|
|
890
|
+
return;
|
|
891
|
+
const owner = s.messages?.find((message) => message.info.id === parent.messageID);
|
|
892
|
+
if (owner === undefined)
|
|
893
|
+
return;
|
|
894
|
+
for (const [index, part] of nested.entries()) {
|
|
895
|
+
const existing = owner.parts.findIndex((item) => item.id === part.id);
|
|
896
|
+
if (existing >= 0)
|
|
897
|
+
owner.parts.splice(existing, 1);
|
|
898
|
+
owner.parts.splice(owner.parts.indexOf(parent) + 1 + index, 0, part);
|
|
899
|
+
this.emitPart(s, part);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
839
902
|
/** Complete an ended assistant message whose completion waited on its tools; true when it changed. */
|
|
840
903
|
completeSettled(s, info) {
|
|
841
904
|
if (!this.awaitingTools.has(info) || info.role !== "assistant" || hasActiveTools(s, info.id))
|
package/dist/session-history.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/** Reconstruct OpenCode history from pi JSONL session files. */
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { applyProviderToolResults, finishProviderToolParts, mapTool,
|
|
3
|
+
import { applyProviderToolResults, finishProviderToolParts, mapTool, nestedToolParts, providerToolParts, } from "./tool-map.js";
|
|
4
4
|
import { TodoBridge } from "./todo-bridge.js";
|
|
5
5
|
import { userMessageModel } from "./thinking-levels.js";
|
|
6
6
|
import { contextMessageID } from "./prompt-parts.js";
|
|
7
7
|
import { bindMessageEntries } from "./session-tree.js";
|
|
8
|
-
import { taskParts, updateTaskParts, taskCompletions } from "./subagents.js";
|
|
8
|
+
import { evalAgentSession, taskParts, updateTaskParts, taskCompletions } from "./subagents.js";
|
|
9
9
|
import { shellTitle, shellUserMessage } from "./session-commands.js";
|
|
10
10
|
import { compactionMessages, piModelId, piStopReason, textOf } from "./pi-event-translation.js";
|
|
11
|
+
import { backgroundID, backgroundMessage, backgroundResult, completeBackgroundEval, finishToolPart, releaseBackground, settleBackground, } from "./background-results.js";
|
|
11
12
|
function entryTime(e) {
|
|
12
13
|
if (typeof e.timestamp === "number")
|
|
13
14
|
return e.timestamp;
|
|
@@ -139,7 +140,9 @@ export function parsePiSessionText(s, agentName, raw) {
|
|
|
139
140
|
finishProviderToolParts([...toolParts.values()].flat(), entryTime(runAssistant), runAssistant.message?.stopReason === "aborted" ? "Cancelled" : "No result");
|
|
140
141
|
runAssistant = undefined;
|
|
141
142
|
};
|
|
143
|
+
let lastAt = 0;
|
|
142
144
|
for (const e of chain) {
|
|
145
|
+
lastAt = Math.max(lastAt, entryTime(e));
|
|
143
146
|
if (e.type === "model_change" && typeof e.provider === "string" && typeof e.modelId === "string") {
|
|
144
147
|
currentModel = { providerID: e.provider, modelID: e.modelId };
|
|
145
148
|
continue;
|
|
@@ -153,6 +156,22 @@ export function parsePiSessionText(s, agentName, raw) {
|
|
|
153
156
|
out.push(...compactionMessages(s, e.id ?? `r${indexOf.get(e) ?? 0}`, e.summary, entryTime(e), currentModel, currentThinkingLevel));
|
|
154
157
|
continue;
|
|
155
158
|
}
|
|
159
|
+
if (e.type === "custom_message") {
|
|
160
|
+
const result = backgroundResult(e.customType, e.content);
|
|
161
|
+
if (result?.kind === "terminal")
|
|
162
|
+
releaseBackground(out, result.callID, entryTime(e));
|
|
163
|
+
if (result !== undefined && completeBackgroundEval(out, result, entryTime(e)) === undefined) {
|
|
164
|
+
const message = backgroundMessage({ info: s.info, messages: out }, result, {
|
|
165
|
+
id: backgroundID(out, result),
|
|
166
|
+
created: entryTime(e),
|
|
167
|
+
model: currentModel,
|
|
168
|
+
agent: agentName,
|
|
169
|
+
});
|
|
170
|
+
if (e.id !== undefined)
|
|
171
|
+
s.tree.messageEntries[message.info.id] = e.id;
|
|
172
|
+
out.push(message);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
156
175
|
const contextID = contextMessageID(e);
|
|
157
176
|
if (contextID !== undefined) {
|
|
158
177
|
endRun();
|
|
@@ -386,32 +405,20 @@ export function parsePiSessionText(s, agentName, raw) {
|
|
|
386
405
|
updateTaskParts(matching, m, true, created);
|
|
387
406
|
continue;
|
|
388
407
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
? {
|
|
401
|
-
status: "error",
|
|
402
|
-
input,
|
|
403
|
-
error: text !== "" ? text : `tool ${part.tool} failed`,
|
|
404
|
-
metadata,
|
|
405
|
-
time: { start: startedAt, end: created },
|
|
408
|
+
if (callID !== undefined)
|
|
409
|
+
finishToolPart(part, m, callID, created);
|
|
410
|
+
const nested = nestedToolParts(part, m.details, {
|
|
411
|
+
agent: (args, claimed, after) => evalAgentSession(s.piSessionId ?? s.restored?.piSessionId, args, claimed, after),
|
|
412
|
+
});
|
|
413
|
+
const owner = out.find((message) => message.info.id === part.messageID);
|
|
414
|
+
if (owner !== undefined) {
|
|
415
|
+
for (const card of nested) {
|
|
416
|
+
const existing = owner.parts.findIndex((item) => item.id === card.id);
|
|
417
|
+
if (existing >= 0)
|
|
418
|
+
owner.parts.splice(existing, 1);
|
|
406
419
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
input,
|
|
410
|
-
output: text,
|
|
411
|
-
title,
|
|
412
|
-
metadata,
|
|
413
|
-
time: { start: startedAt, end: created },
|
|
414
|
-
};
|
|
420
|
+
owner.parts.splice(owner.parts.indexOf(part) + 1, 0, ...nested);
|
|
421
|
+
}
|
|
415
422
|
}
|
|
416
423
|
// system / custom entries are not rendered
|
|
417
424
|
}
|
|
@@ -480,12 +487,17 @@ export function parsePiSessionText(s, agentName, raw) {
|
|
|
480
487
|
sessionID,
|
|
481
488
|
messageID: message.info.id,
|
|
482
489
|
}));
|
|
483
|
-
const
|
|
484
|
-
|
|
490
|
+
const nestedIDs = new Set(message.parts.flatMap((part) => part.type === "tool" && part.state.status !== "pending" && part.state.metadata?.nested === true
|
|
491
|
+
? [part.id]
|
|
492
|
+
: []));
|
|
493
|
+
const identities = s.tree.messageParts?.[message.info.id]?.filter((identity) => !nestedIDs.has(identity));
|
|
494
|
+
for (const [index, part] of message.parts.filter((part) => !nestedIDs.has(part.id)).entries()) {
|
|
485
495
|
const identity = identities?.[index];
|
|
486
496
|
if (identity !== undefined)
|
|
487
497
|
part.id = identity;
|
|
488
498
|
}
|
|
489
499
|
}
|
|
500
|
+
// The agent that ran these shells is gone by the time history is rebuilt.
|
|
501
|
+
settleBackground(out, lastAt);
|
|
490
502
|
return out;
|
|
491
503
|
}
|
package/dist/session-tree.d.ts
CHANGED
package/dist/session-tree.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RPC_COMMAND_TIMEOUT_MS } from "./pi-rpc.js";
|
|
2
2
|
import { readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { contextMessageID, nativePromptInput } from "./prompt-parts.js";
|
|
4
|
+
import { backgroundResult } from "./background-results.js";
|
|
4
5
|
export class SessionTreeError extends Error {
|
|
5
6
|
}
|
|
6
7
|
export function durableShellTree(tree) {
|
|
@@ -132,6 +133,7 @@ function bindOrdinaryEntry(s, entry, messages, cursor) {
|
|
|
132
133
|
.join("");
|
|
133
134
|
const index = messages.findIndex((message, position) => position >= cursor &&
|
|
134
135
|
!s.localMessageIds?.has(message.info.id) &&
|
|
136
|
+
!message.info.id.startsWith("msg_background_") &&
|
|
135
137
|
s.tree.messageEntries[message.info.id] === undefined &&
|
|
136
138
|
message.info.role === entry.message?.role &&
|
|
137
139
|
(message.info.role !== "user" ||
|
|
@@ -158,6 +160,20 @@ export function bindMessageEntries(s, tree) {
|
|
|
158
160
|
const branch = branchEntries(tree, s.tree.revert ? s.tree.revert.previousLeafId : leafId);
|
|
159
161
|
const positions = new Map(branch.map((entry, index) => [entry.id, index]));
|
|
160
162
|
pruneStaleShellTurns(s, positions);
|
|
163
|
+
// Background completion cards carry no native ID live; recompute their content key in branch order.
|
|
164
|
+
const seen = new Map();
|
|
165
|
+
for (const entry of branch) {
|
|
166
|
+
if (entry.type !== "custom_message")
|
|
167
|
+
continue;
|
|
168
|
+
const result = backgroundResult(entry.customType, entry.content);
|
|
169
|
+
if (result === undefined)
|
|
170
|
+
continue;
|
|
171
|
+
const occurrence = seen.get(result.key) ?? 0;
|
|
172
|
+
seen.set(result.key, occurrence + 1);
|
|
173
|
+
const messageID = `msg_background_${result.key}_${occurrence}`;
|
|
174
|
+
if (s.messages?.some((message) => message.info.id === messageID))
|
|
175
|
+
s.tree.messageEntries[messageID] = entry.id;
|
|
176
|
+
}
|
|
161
177
|
// Admitted context carries its OpenChamber id in the native custom message.
|
|
162
178
|
for (const entry of branch) {
|
|
163
179
|
const messageID = contextMessageID(entry);
|
package/dist/sessions.js
CHANGED
|
@@ -250,6 +250,7 @@ export class SessionManager {
|
|
|
250
250
|
this.reportPersistenceError(s, error);
|
|
251
251
|
}
|
|
252
252
|
delete s.interruptRequested;
|
|
253
|
+
this.events.settleBackground(s);
|
|
253
254
|
this.subagents.stopParent(s.info.id);
|
|
254
255
|
this.monitors.stopParent(s.info.id);
|
|
255
256
|
void this.lifecycle
|
package/dist/subagents.d.ts
CHANGED
|
@@ -14,6 +14,12 @@ export declare function findTaskBySession(sessionId: string): {
|
|
|
14
14
|
path: string;
|
|
15
15
|
data: Record<string, unknown>;
|
|
16
16
|
} | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* The session of a subagent an eval cell started with `agent()`. The recorded call keeps only its
|
|
19
|
+
* arguments, so the task record is found by parent, prompt and label: the oldest one created since
|
|
20
|
+
* the cell started and not yet claimed by an earlier call of the same cell.
|
|
21
|
+
*/
|
|
22
|
+
export declare function evalAgentSession(parentPiId: string | undefined, args: Record<string, unknown>, claimed: Set<string>, after: number): string | undefined;
|
|
17
23
|
export declare function findTask(id: string, parentPiId: string): {
|
|
18
24
|
path: string;
|
|
19
25
|
data: Record<string, unknown>;
|
package/dist/subagents.js
CHANGED
|
@@ -193,6 +193,38 @@ export function findTaskBySession(sessionId) {
|
|
|
193
193
|
}
|
|
194
194
|
return undefined;
|
|
195
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* The session of a subagent an eval cell started with `agent()`. The recorded call keeps only its
|
|
198
|
+
* arguments, so the task record is found by parent, prompt and label: the oldest one created since
|
|
199
|
+
* the cell started and not yet claimed by an earlier call of the same cell.
|
|
200
|
+
*/
|
|
201
|
+
export function evalAgentSession(parentPiId, args, claimed, after) {
|
|
202
|
+
if (parentPiId === undefined || typeof args.prompt !== "string")
|
|
203
|
+
return undefined;
|
|
204
|
+
let best;
|
|
205
|
+
for (const { id, path } of taskFiles()) {
|
|
206
|
+
if (claimed.has(id))
|
|
207
|
+
continue;
|
|
208
|
+
const data = childRecord(id, path);
|
|
209
|
+
if (data?.parent_session_id !== parentPiId)
|
|
210
|
+
continue;
|
|
211
|
+
const spec = record(data.spawn_spec) ? data.spawn_spec : {};
|
|
212
|
+
if (spec.prompt !== args.prompt)
|
|
213
|
+
continue;
|
|
214
|
+
if (typeof args.label === "string" && data.name !== args.label)
|
|
215
|
+
continue;
|
|
216
|
+
const created = typeof data.created_at === "string" ? Date.parse(data.created_at) : Number.NaN;
|
|
217
|
+
// The cell's start is taken when its card opens, a moment after OmO starts it.
|
|
218
|
+
if (!Number.isFinite(created) || created < after - 5_000)
|
|
219
|
+
continue;
|
|
220
|
+
if (best === undefined || created < best.created)
|
|
221
|
+
best = { id, created };
|
|
222
|
+
}
|
|
223
|
+
if (best === undefined)
|
|
224
|
+
return undefined;
|
|
225
|
+
claimed.add(best.id);
|
|
226
|
+
return ocIdFor(best.id);
|
|
227
|
+
}
|
|
196
228
|
export function findTask(id, parentPiId) {
|
|
197
229
|
if (!taskId(id))
|
|
198
230
|
return undefined;
|
package/dist/tool-map.d.ts
CHANGED
|
@@ -25,6 +25,21 @@ export declare function resultText(content: unknown): string;
|
|
|
25
25
|
/** Only edit cards need patch metadata; write cards already preview input.content. */
|
|
26
26
|
export declare function resultMetadata(tool: string, result: ToolResult): Record<string, unknown>;
|
|
27
27
|
export declare function mapTool(omoName: string, args: unknown, result?: ToolResult): MappedTool;
|
|
28
|
+
/**
|
|
29
|
+
* What an eval cell started that has a session of its own. Its other calls are not shown:
|
|
30
|
+
* the cell's output already holds what it printed, and OmO records only a one-line preview.
|
|
31
|
+
*/
|
|
32
|
+
export interface NestedSessions {
|
|
33
|
+
/** The monitor child session for a recorded `monitor` call. */
|
|
34
|
+
monitor?: (args: Record<string, unknown>) => string | undefined;
|
|
35
|
+
/** The subagent session for a recorded `agent()` call; `claimed` keeps repeated calls apart. */
|
|
36
|
+
agent?: (args: Record<string, unknown>, claimed: Set<string>, after: number) => string | undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Finished eval calls, from the raw RPC capture of installed omo-ai 5.1.33 / senpi runtime, 2026-10-17.
|
|
40
|
+
* Monitors and `agent()` calls (recorded as `__agent__`) become subagent cards; everything else is skipped.
|
|
41
|
+
*/
|
|
42
|
+
export declare function nestedToolParts(parent: OCToolPart, details: unknown, sessions?: NestedSessions): OCToolPart[];
|
|
28
43
|
/** One card at each call's original block index; result blocks are not cards.
|
|
29
44
|
* Shared by RPC snapshots and history so ids and renderer state survive reloads.
|
|
30
45
|
*/
|
package/dist/tool-map.js
CHANGED
|
@@ -134,6 +134,77 @@ export function mapTool(omoName, args, result) {
|
|
|
134
134
|
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
|
+
/** A subagent-style card, as OpenChamber draws `task`: it opens `sessionID` when there is one. */
|
|
138
|
+
function nestedSubagent(description, prompt, type, sessionID) {
|
|
139
|
+
return {
|
|
140
|
+
tool: "task",
|
|
141
|
+
input: { description, prompt, subagent_type: type },
|
|
142
|
+
title: description.slice(0, 60),
|
|
143
|
+
metadata: sessionID !== undefined ? { sessionID } : {},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Finished eval calls, from the raw RPC capture of installed omo-ai 5.1.33 / senpi runtime, 2026-10-17.
|
|
148
|
+
* Monitors and `agent()` calls (recorded as `__agent__`) become subagent cards; everything else is skipped.
|
|
149
|
+
*/
|
|
150
|
+
export function nestedToolParts(parent, details, sessions = {}) {
|
|
151
|
+
if (!record(details) || !Array.isArray(details.toolCalls))
|
|
152
|
+
return [];
|
|
153
|
+
const parts = [];
|
|
154
|
+
const start = parent.state.status === "pending" ? 0 : parent.state.time.start;
|
|
155
|
+
const claimed = new Set();
|
|
156
|
+
details.toolCalls.forEach((entry, index) => {
|
|
157
|
+
if (!record(entry) ||
|
|
158
|
+
(entry.name !== "monitor" && entry.name !== "__agent__") ||
|
|
159
|
+
typeof entry.ok !== "boolean" ||
|
|
160
|
+
typeof entry.durationMs !== "number" ||
|
|
161
|
+
!Number.isFinite(entry.durationMs) ||
|
|
162
|
+
entry.durationMs < 0)
|
|
163
|
+
return;
|
|
164
|
+
const args = record(entry.args) ? entry.args : {};
|
|
165
|
+
let mapped;
|
|
166
|
+
if (entry.name === "monitor") {
|
|
167
|
+
const sessionID = entry.ok ? sessions.monitor?.(args) : undefined;
|
|
168
|
+
if (sessionID === undefined)
|
|
169
|
+
return; // nothing to open; the cell's code already shows the call
|
|
170
|
+
const command = typeof args.command === "string" ? args.command : "";
|
|
171
|
+
const label = typeof args.description === "string" && args.description !== "" ? args.description : command;
|
|
172
|
+
mapped = nestedSubagent(`Monitor: ${label}`, command, "monitor", sessionID);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const prompt = typeof args.prompt === "string" ? args.prompt : "";
|
|
176
|
+
const label = [args.label, args.description, prompt.split("\n")[0]].find((value) => typeof value === "string" && value !== "");
|
|
177
|
+
const sessionID = entry.ok ? sessions.agent?.(args, claimed, start) : undefined;
|
|
178
|
+
const type = typeof args.agent === "string" && args.agent !== "" ? args.agent : "subagent";
|
|
179
|
+
mapped = nestedSubagent(label ?? "subagent", prompt, type, sessionID);
|
|
180
|
+
}
|
|
181
|
+
const key = typeof entry.callId === "string" ? `id:${encodeURIComponent(entry.callId)}` : `index:${index}`;
|
|
182
|
+
const callID = `${encodeURIComponent(parent.callID)}:${key}`;
|
|
183
|
+
const state = {
|
|
184
|
+
input: mapped.input,
|
|
185
|
+
title: mapped.title,
|
|
186
|
+
output: "",
|
|
187
|
+
metadata: { ...mapped.metadata, nested: true, parentCallID: parent.callID, durationMs: entry.durationMs },
|
|
188
|
+
time: { start, end: start + entry.durationMs },
|
|
189
|
+
};
|
|
190
|
+
parts.push({
|
|
191
|
+
id: `prt_${parent.sessionID}_nested_${callID}`,
|
|
192
|
+
sessionID: parent.sessionID,
|
|
193
|
+
messageID: parent.messageID,
|
|
194
|
+
type: "tool",
|
|
195
|
+
callID: `nested:${callID}`,
|
|
196
|
+
tool: mapped.tool,
|
|
197
|
+
state: entry.ok
|
|
198
|
+
? { ...state, status: "completed" }
|
|
199
|
+
: {
|
|
200
|
+
...state,
|
|
201
|
+
status: "error",
|
|
202
|
+
error: typeof entry.error === "string" && entry.error !== "" ? entry.error : "failed",
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
return parts;
|
|
207
|
+
}
|
|
137
208
|
/** JSON first, then the literal-only Python repr used by older session files.
|
|
138
209
|
* Never execute provider content; invalid or incomplete literals are skipped.
|
|
139
210
|
*/
|