@omercnet/paseo-omp 0.2.1-next.72.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 +87 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/SUPPORT.md +42 -0
- package/TESTING.md +150 -0
- package/client/composer-pill-settings.tsx +157 -0
- package/client/hub-icon.tsx +12 -0
- package/client/hub-popover.tsx +132 -0
- package/client/hub-status.ts +29 -0
- package/client/mcp-authorization.tsx +168 -0
- package/client/mcp-popover.tsx +155 -0
- package/client/memory-panel.tsx +76 -0
- package/client/memory-popover.tsx +74 -0
- package/client/omp-config-surface.tsx +1433 -0
- package/client/omp-doc-links.ts +117 -0
- package/client/omp-plugin-manager.tsx +1004 -0
- package/client/omp-store-picker.tsx +89 -0
- package/client/omp-store-state.ts +45 -0
- package/client/provider-diagnostics-state.ts +262 -0
- package/client/provider-icon.tsx +27 -0
- package/client/provider-image.tsx +66 -0
- package/client/quota-popover.tsx +155 -0
- package/client/quota-state.ts +140 -0
- package/client/sessions-popover.tsx +78 -0
- package/docs/alpha-release-checklist.md +68 -0
- package/docs/configuration.md +126 -0
- package/docs/core-provider-issue-audit.md +108 -0
- package/docs/images/mcp-authorization-compact.png +0 -0
- package/docs/images/mcp-controls-wide.png +0 -0
- package/docs/images/plugin-manager.png +0 -0
- package/docs/images/workspace-settings.png +0 -0
- package/docs/installation.md +67 -0
- package/index.client.tsx +488 -0
- package/index.server.ts +81 -0
- package/package.json +84 -0
- package/paseo-plugin.json +5 -0
- package/scripts/prepare-dependencies.mjs +20 -0
- package/server/hub.ts +145 -0
- package/server/mcp-browser.ts +95 -0
- package/server/memory.ts +86 -0
- package/server/mutation-queue.ts +12 -0
- package/server/omp-config.ts +135 -0
- package/server/omp-plugins.ts +676 -0
- package/server/omp-settings.ts +499 -0
- package/server/paths.ts +181 -0
- package/server/provider/catalog.ts +172 -0
- package/server/provider/config-normalization.ts +148 -0
- package/server/provider/connection.ts +1196 -0
- package/server/provider/host-tools.ts +777 -0
- package/server/provider/image.ts +143 -0
- package/server/provider/mcp-transport.ts +394 -0
- package/server/provider/omp-rpc.ts +2806 -0
- package/server/provider/omp.svg +5 -0
- package/server/provider/profile-providers.ts +249 -0
- package/server/provider/provider-options.ts +27 -0
- package/server/provider/registration.ts +162 -0
- package/server/provider/security.ts +317 -0
- package/server/provider/session-descriptors.ts +736 -0
- package/server/provider/session.ts +4796 -0
- package/server/provider/settings.ts +78 -0
- package/server/provider/subsessions.ts +850 -0
- package/server/provider/timeline-projector.ts +1801 -0
- package/server/provider-diagnostics.ts +1143 -0
- package/server/quota.ts +55 -0
- package/server/sessions.ts +58 -0
- package/shared/composer-pill-settings.ts +28 -0
- package/shared/hub.ts +43 -0
- package/shared/mcp.ts +47 -0
- package/shared/memory.ts +24 -0
- package/shared/omp-config.ts +85 -0
- package/shared/omp-plugins.ts +264 -0
- package/shared/omp-settings.ts +214 -0
- package/shared/omp-store.ts +58 -0
- package/shared/provider-diagnostics.ts +126 -0
- package/shared/provider-image.ts +160 -0
- package/shared/quota.ts +23 -0
- package/shared/sessions.ts +24 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { basename, extname } from "node:path";
|
|
3
|
+
import type { ProviderEvent } from "@getpaseo/plugin/server/provider";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import type {
|
|
6
|
+
OmpAgentSessionEvent,
|
|
7
|
+
OmpMessage,
|
|
8
|
+
OmpRuntime,
|
|
9
|
+
OmpRuntimeSession,
|
|
10
|
+
OmpSubagentEvent,
|
|
11
|
+
OmpSubagentSnapshot,
|
|
12
|
+
} from "./omp-rpc";
|
|
13
|
+
import {
|
|
14
|
+
BoundedStringSet,
|
|
15
|
+
boundedJsonBytes,
|
|
16
|
+
boundedJsonMetrics,
|
|
17
|
+
OmpPublicDataSerializer,
|
|
18
|
+
OmpPublicError,
|
|
19
|
+
} from "./security";
|
|
20
|
+
import { OmpTimelineProjector, type OmpTimelineScheduler } from "./timeline-projector";
|
|
21
|
+
|
|
22
|
+
const MAX_CHILDREN = 1_024;
|
|
23
|
+
const MAX_TASK_DISPATCHES = 4_096;
|
|
24
|
+
const MAX_BUFFERED_EVENTS = 1_024;
|
|
25
|
+
const MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
|
|
26
|
+
const MAX_CHILD_MESSAGE_IDENTITIES = 2_048;
|
|
27
|
+
const MAX_REPLAY_MESSAGES = 100_000;
|
|
28
|
+
const MAX_REPLAY_BYTES = 64 * 1024 * 1024;
|
|
29
|
+
const MAX_REPLAY_NODES = 400_000;
|
|
30
|
+
const MAX_REPLAY_DEPTH = 16;
|
|
31
|
+
|
|
32
|
+
type Emit = (event: ProviderEvent) => void;
|
|
33
|
+
type ChildTerminalStatus = "completed" | "failed" | "canceled";
|
|
34
|
+
type ChildStatus = "running" | ChildTerminalStatus;
|
|
35
|
+
type ChildRef = {
|
|
36
|
+
id: string;
|
|
37
|
+
agent?: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
sessionFile?: string;
|
|
40
|
+
parentToolCallId?: string;
|
|
41
|
+
};
|
|
42
|
+
type ReplayChildRef = ChildRef & { status: ChildTerminalStatus | "derive" };
|
|
43
|
+
type ChildState = {
|
|
44
|
+
nativeId: string;
|
|
45
|
+
sessionId: string;
|
|
46
|
+
parentSessionId: string;
|
|
47
|
+
turnId: string;
|
|
48
|
+
turnSequence: number;
|
|
49
|
+
title: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
sessionFile?: string;
|
|
52
|
+
parentToolCallId?: string;
|
|
53
|
+
status: ChildStatus;
|
|
54
|
+
terminalRequested?: ChildTerminalStatus;
|
|
55
|
+
sessionClosed: boolean;
|
|
56
|
+
seenInSnapshot: boolean;
|
|
57
|
+
seenAssistantIdentities: BoundedStringSet;
|
|
58
|
+
projector: OmpTimelineProjector;
|
|
59
|
+
};
|
|
60
|
+
type TaskDispatch = {
|
|
61
|
+
ownerSessionId: string;
|
|
62
|
+
expectedChildren: number;
|
|
63
|
+
childSessionIds: Set<string>;
|
|
64
|
+
acknowledged: boolean;
|
|
65
|
+
};
|
|
66
|
+
type ReplayBudget = { messages: number; bytes: number; nodes: number };
|
|
67
|
+
const TaskArgsSchema = z.object({
|
|
68
|
+
tasks: z.array(z.unknown()).max(MAX_CHILDREN).optional(),
|
|
69
|
+
agent: z.string().optional(),
|
|
70
|
+
subAgentType: z.string().optional(),
|
|
71
|
+
agentType: z.string().optional(),
|
|
72
|
+
type: z.string().optional(),
|
|
73
|
+
description: z.string().optional(),
|
|
74
|
+
task: z.string().optional(),
|
|
75
|
+
prompt: z.string().optional(),
|
|
76
|
+
assignment: z.string().optional(),
|
|
77
|
+
});
|
|
78
|
+
const TaskProgressSchema = z.object({
|
|
79
|
+
index: z
|
|
80
|
+
.number()
|
|
81
|
+
.int()
|
|
82
|
+
.nonnegative()
|
|
83
|
+
.max(MAX_CHILDREN - 1),
|
|
84
|
+
id: z.string().min(1),
|
|
85
|
+
agent: z.string().optional(),
|
|
86
|
+
status: z.enum(["pending", "running", "completed", "failed", "aborted"]),
|
|
87
|
+
});
|
|
88
|
+
const TaskResultDetailsSchema = z.object({
|
|
89
|
+
results: z
|
|
90
|
+
.array(
|
|
91
|
+
z.object({
|
|
92
|
+
id: z.string().min(1),
|
|
93
|
+
agent: z.string().optional(),
|
|
94
|
+
exitCode: z.number().optional(),
|
|
95
|
+
error: z.unknown().optional(),
|
|
96
|
+
aborted: z.boolean().optional(),
|
|
97
|
+
}),
|
|
98
|
+
)
|
|
99
|
+
.max(MAX_CHILDREN),
|
|
100
|
+
progress: z.array(TaskProgressSchema).max(MAX_CHILDREN).optional(),
|
|
101
|
+
});
|
|
102
|
+
const YieldResultSchema = z.object({
|
|
103
|
+
status: z.enum(["success", "completed", "failed", "error", "aborted", "canceled", "cancelled"]),
|
|
104
|
+
});
|
|
105
|
+
const TaskResultEnvelopeSchema = z.object({ details: TaskResultDetailsSchema });
|
|
106
|
+
|
|
107
|
+
function expectedTaskChildren(value: unknown): number {
|
|
108
|
+
const parsed = TaskArgsSchema.safeParse(value);
|
|
109
|
+
if (!parsed.success || !parsed.data.tasks) return 1;
|
|
110
|
+
return Math.max(1, parsed.data.tasks.length);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function taskResultActivity(value: unknown): {
|
|
114
|
+
expectedChildren?: number;
|
|
115
|
+
settleWithoutChildren: boolean;
|
|
116
|
+
} {
|
|
117
|
+
const parsed = TaskResultEnvelopeSchema.safeParse(value);
|
|
118
|
+
if (!parsed.success) return { settleWithoutChildren: false };
|
|
119
|
+
const pending = (parsed.data.details.progress ?? []).filter(
|
|
120
|
+
(item) => item.status === "pending" || item.status === "running",
|
|
121
|
+
);
|
|
122
|
+
const progressCount = pending.reduce((count, item) => Math.max(count, item.index + 1), 0);
|
|
123
|
+
const observed = Math.max(parsed.data.details.results.length, progressCount);
|
|
124
|
+
return {
|
|
125
|
+
...(observed > 0 ? { expectedChildren: observed } : {}),
|
|
126
|
+
settleWithoutChildren: parsed.data.details.results.length === 0 && pending.length === 0,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function waitForReplay<T>(work: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
131
|
+
signal.throwIfAborted();
|
|
132
|
+
const aborted = Promise.withResolvers<never>();
|
|
133
|
+
const onAbort = () => aborted.reject(signal.reason);
|
|
134
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
135
|
+
try {
|
|
136
|
+
return await Promise.race([work, aborted.promise]);
|
|
137
|
+
} finally {
|
|
138
|
+
signal.removeEventListener("abort", onAbort);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function taskDescription(value: unknown): string | undefined {
|
|
142
|
+
const parsed = TaskArgsSchema.safeParse(value);
|
|
143
|
+
if (!parsed.success) return;
|
|
144
|
+
for (const candidate of [
|
|
145
|
+
parsed.data.description,
|
|
146
|
+
parsed.data.task,
|
|
147
|
+
parsed.data.prompt,
|
|
148
|
+
parsed.data.assignment,
|
|
149
|
+
]) {
|
|
150
|
+
if (candidate?.trim()) return candidate;
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function taskCalls(
|
|
156
|
+
messages: readonly OmpMessage[],
|
|
157
|
+
): Map<string, { title: string; description?: string }> {
|
|
158
|
+
const calls = new Map<string, { title: string; description?: string }>();
|
|
159
|
+
for (const message of messages) {
|
|
160
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
161
|
+
for (const part of message.content) {
|
|
162
|
+
if (part.type !== "toolCall" || part.name !== "task" || !part.id) continue;
|
|
163
|
+
const args = TaskArgsSchema.safeParse(part.arguments);
|
|
164
|
+
const data = args.success ? args.data : {};
|
|
165
|
+
const title =
|
|
166
|
+
[data.agent, data.subAgentType, data.agentType, data.type].find(
|
|
167
|
+
(candidate): candidate is string =>
|
|
168
|
+
typeof candidate === "string" && candidate.trim().length > 0,
|
|
169
|
+
) ?? "OMP subagent";
|
|
170
|
+
calls.set(part.id, { title, description: taskDescription(data) });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return calls;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function taskResultDetails(
|
|
177
|
+
message: OmpMessage,
|
|
178
|
+
): z.infer<typeof TaskResultDetailsSchema> | undefined {
|
|
179
|
+
if (message.role !== "toolResult") return;
|
|
180
|
+
const direct = TaskResultDetailsSchema.safeParse(message.details);
|
|
181
|
+
if (direct.success) return direct.data;
|
|
182
|
+
const nested = TaskResultEnvelopeSchema.safeParse(message.content);
|
|
183
|
+
return nested.success ? nested.data.details : undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function replayChildren(messages: readonly OmpMessage[]): ReplayChildRef[] {
|
|
187
|
+
const calls = taskCalls(messages);
|
|
188
|
+
const children: ReplayChildRef[] = [];
|
|
189
|
+
for (const message of messages) {
|
|
190
|
+
if (message.role !== "toolResult" || message.toolName !== "task") continue;
|
|
191
|
+
const call = calls.get(message.toolCallId);
|
|
192
|
+
const details = taskResultDetails(message);
|
|
193
|
+
const results = details?.results ?? [];
|
|
194
|
+
for (const result of results) {
|
|
195
|
+
const failed =
|
|
196
|
+
message.isError === true ||
|
|
197
|
+
Boolean(result.error) ||
|
|
198
|
+
(typeof result.exitCode === "number" && result.exitCode !== 0);
|
|
199
|
+
children.push({
|
|
200
|
+
id: result.id,
|
|
201
|
+
agent: result.agent ?? call?.title,
|
|
202
|
+
description: call?.description,
|
|
203
|
+
parentToolCallId: message.toolCallId,
|
|
204
|
+
status: result.aborted === true ? "canceled" : failed ? "failed" : "completed",
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const resultIds = new Set(results.map((result) => result.id));
|
|
208
|
+
const progress = details?.progress ?? [];
|
|
209
|
+
for (const item of progress) {
|
|
210
|
+
if (resultIds.has(item.id)) continue;
|
|
211
|
+
const status = terminalStatus(item.status);
|
|
212
|
+
children.push({
|
|
213
|
+
id: item.id,
|
|
214
|
+
agent: item.agent ?? call?.title,
|
|
215
|
+
description: call?.description,
|
|
216
|
+
parentToolCallId: message.toolCallId,
|
|
217
|
+
status: status ?? "derive",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (results.length > 0 || progress.length > 0) continue;
|
|
221
|
+
const text = Array.isArray(message.content)
|
|
222
|
+
? message.content
|
|
223
|
+
.flatMap((part) => (part.type === "text" && part.text ? [part.text] : []))
|
|
224
|
+
.join("\n")
|
|
225
|
+
: typeof message.content === "string"
|
|
226
|
+
? message.content
|
|
227
|
+
: "";
|
|
228
|
+
const sessionFile = text.match(/(?:session|transcript)(?: file)?:\s*(?<path>\/\S+\.jsonl)/iu)
|
|
229
|
+
?.groups?.path;
|
|
230
|
+
if (!sessionFile) continue;
|
|
231
|
+
const fileName = basename(sessionFile);
|
|
232
|
+
const extension = extname(fileName);
|
|
233
|
+
children.push({
|
|
234
|
+
id: extension ? fileName.slice(0, -extension.length) : fileName,
|
|
235
|
+
agent: call?.title,
|
|
236
|
+
description: call?.description,
|
|
237
|
+
sessionFile,
|
|
238
|
+
parentToolCallId: message.toolCallId,
|
|
239
|
+
status: message.isError ? "failed" : "completed",
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return children;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function replayTerminalStatus(messages: readonly OmpMessage[]): ChildTerminalStatus {
|
|
246
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
247
|
+
const message = messages[index];
|
|
248
|
+
if (message?.role !== "toolResult" || message.toolName !== "yield") continue;
|
|
249
|
+
const parsed = YieldResultSchema.safeParse(message.details);
|
|
250
|
+
if (!parsed.success) continue;
|
|
251
|
+
if (
|
|
252
|
+
parsed.data.status === "aborted" ||
|
|
253
|
+
parsed.data.status === "canceled" ||
|
|
254
|
+
parsed.data.status === "cancelled"
|
|
255
|
+
) {
|
|
256
|
+
return "canceled";
|
|
257
|
+
}
|
|
258
|
+
if (parsed.data.status === "failed" || parsed.data.status === "error") return "failed";
|
|
259
|
+
return "completed";
|
|
260
|
+
}
|
|
261
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
262
|
+
const message = messages[index];
|
|
263
|
+
if (message?.role !== "assistant") continue;
|
|
264
|
+
const stopReason = message.stopReason?.toLowerCase();
|
|
265
|
+
if (stopReason === "aborted" || stopReason === "canceled" || stopReason === "cancelled") {
|
|
266
|
+
return "canceled";
|
|
267
|
+
}
|
|
268
|
+
if (stopReason === "error" || message.errorMessage) return "failed";
|
|
269
|
+
return "completed";
|
|
270
|
+
}
|
|
271
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
272
|
+
const message = messages[index];
|
|
273
|
+
if (message?.role === "bashExecution" && message.cancelled) return "canceled";
|
|
274
|
+
if (message?.role === "toolResult" && message.isError) return "failed";
|
|
275
|
+
}
|
|
276
|
+
return "completed";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function terminalStatus(status: string): ChildTerminalStatus | undefined {
|
|
280
|
+
if (status === "completed") return "completed";
|
|
281
|
+
if (status === "failed") return "failed";
|
|
282
|
+
if (status === "aborted") return "canceled";
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export class OmpSubsessionProjector {
|
|
287
|
+
private readonly children = new Map<string, ChildState>();
|
|
288
|
+
private readonly sessionIdByNativeId = new Map<string, string>();
|
|
289
|
+
private readonly toolOwners = new Map<string, string>();
|
|
290
|
+
private readonly dispatches = new Map<string, TaskDispatch>();
|
|
291
|
+
private readonly bufferedEvents: OmpSubagentEvent[] = [];
|
|
292
|
+
private bufferedBytes = 0;
|
|
293
|
+
private replaying = false;
|
|
294
|
+
private closed = false;
|
|
295
|
+
|
|
296
|
+
private readonly dataFilter: OmpPublicDataSerializer;
|
|
297
|
+
|
|
298
|
+
constructor(
|
|
299
|
+
private readonly rootSessionId: string,
|
|
300
|
+
private readonly rootIdentityKey: string,
|
|
301
|
+
private readonly rootSessionFile: string | undefined,
|
|
302
|
+
private readonly cwd: string,
|
|
303
|
+
private readonly emit: Emit,
|
|
304
|
+
private readonly scheduler: OmpTimelineScheduler,
|
|
305
|
+
private readonly onActivityChange: () => void,
|
|
306
|
+
private readonly outputRedactionValues: readonly string[],
|
|
307
|
+
private readonly pluginTimelineEnabled = false,
|
|
308
|
+
) {
|
|
309
|
+
this.dataFilter = new OmpPublicDataSerializer(outputRedactionValues);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
observeSessionEvent(ownerSessionId: string, event: OmpAgentSessionEvent): void {
|
|
313
|
+
if (event.type === "tool_execution_start" && event.toolName === "task") {
|
|
314
|
+
if (!this.dispatches.has(event.toolCallId)) {
|
|
315
|
+
if (this.dispatches.size >= MAX_TASK_DISPATCHES) {
|
|
316
|
+
throw new OmpPublicError("OMP subagent dispatch limit reached");
|
|
317
|
+
}
|
|
318
|
+
const dispatch: TaskDispatch = {
|
|
319
|
+
ownerSessionId,
|
|
320
|
+
expectedChildren: expectedTaskChildren(event.args),
|
|
321
|
+
childSessionIds: new Set(),
|
|
322
|
+
acknowledged: false,
|
|
323
|
+
};
|
|
324
|
+
for (const child of this.children.values()) {
|
|
325
|
+
if (
|
|
326
|
+
child.parentToolCallId === event.toolCallId &&
|
|
327
|
+
child.parentSessionId === ownerSessionId
|
|
328
|
+
) {
|
|
329
|
+
dispatch.childSessionIds.add(child.sessionId);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
this.dispatches.set(event.toolCallId, dispatch);
|
|
333
|
+
}
|
|
334
|
+
this.registerToolOwner(event.toolCallId, ownerSessionId);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (event.type !== "tool_execution_end" || event.toolName !== "task") return;
|
|
338
|
+
const dispatch = this.dispatches.get(event.toolCallId);
|
|
339
|
+
if (!dispatch) return;
|
|
340
|
+
if (event.isError) {
|
|
341
|
+
this.dispatches.delete(event.toolCallId);
|
|
342
|
+
this.toolOwners.delete(event.toolCallId);
|
|
343
|
+
this.onActivityChange();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
dispatch.acknowledged = true;
|
|
347
|
+
const activity = taskResultActivity(event.result);
|
|
348
|
+
if (activity.settleWithoutChildren) dispatch.expectedChildren = 0;
|
|
349
|
+
else if (activity.expectedChildren !== undefined) {
|
|
350
|
+
dispatch.expectedChildren = Math.max(dispatch.expectedChildren, activity.expectedChildren);
|
|
351
|
+
}
|
|
352
|
+
this.settleDispatch(event.toolCallId, dispatch);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
handle(event: OmpSubagentEvent): void {
|
|
356
|
+
if (this.closed) return;
|
|
357
|
+
if (this.replaying) {
|
|
358
|
+
if (this.bufferedEvents.length >= MAX_BUFFERED_EVENTS) {
|
|
359
|
+
throw new OmpPublicError("OMP subagent replay event limit reached");
|
|
360
|
+
}
|
|
361
|
+
const bytes = boundedJsonBytes(event, MAX_BUFFERED_BYTES, 1_024, MAX_BUFFERED_BYTES, 4_096);
|
|
362
|
+
if (bytes === Number.POSITIVE_INFINITY || this.bufferedBytes + bytes > MAX_BUFFERED_BYTES) {
|
|
363
|
+
throw new OmpPublicError("OMP subagent replay event limit reached");
|
|
364
|
+
}
|
|
365
|
+
this.bufferedEvents.push(event);
|
|
366
|
+
this.bufferedBytes += bytes;
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
this.apply(event);
|
|
370
|
+
}
|
|
371
|
+
async replay(
|
|
372
|
+
messages: readonly OmpMessage[],
|
|
373
|
+
runtimeSession: OmpRuntimeSession,
|
|
374
|
+
runtime: OmpRuntime,
|
|
375
|
+
signal: AbortSignal,
|
|
376
|
+
): Promise<void> {
|
|
377
|
+
this.replaying = true;
|
|
378
|
+
let completed = false;
|
|
379
|
+
try {
|
|
380
|
+
const budget: ReplayBudget = { messages: 0, bytes: 0, nodes: 0 };
|
|
381
|
+
this.accountReplay(messages, budget, signal);
|
|
382
|
+
const visited = new Set<string>();
|
|
383
|
+
await this.replayChildren(
|
|
384
|
+
this.rootSessionId,
|
|
385
|
+
this.rootSessionFile,
|
|
386
|
+
messages,
|
|
387
|
+
runtime,
|
|
388
|
+
visited,
|
|
389
|
+
budget,
|
|
390
|
+
signal,
|
|
391
|
+
0,
|
|
392
|
+
);
|
|
393
|
+
const snapshots = await waitForReplay(runtimeSession.getSubagents(), signal);
|
|
394
|
+
await this.replaySnapshots(snapshots, runtimeSession, runtime, visited, budget, signal);
|
|
395
|
+
signal.throwIfAborted();
|
|
396
|
+
this.reconcileSnapshots(snapshots);
|
|
397
|
+
completed = true;
|
|
398
|
+
} finally {
|
|
399
|
+
this.replaying = false;
|
|
400
|
+
const buffered = this.bufferedEvents.splice(0);
|
|
401
|
+
this.bufferedBytes = 0;
|
|
402
|
+
if (completed && !signal.aborted) {
|
|
403
|
+
for (const event of buffered) this.apply(event);
|
|
404
|
+
} else {
|
|
405
|
+
this.closed = true;
|
|
406
|
+
for (const child of this.children.values()) child.projector.close();
|
|
407
|
+
this.dispatches.clear();
|
|
408
|
+
this.toolOwners.clear();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async reconcile(runtime: OmpRuntimeSession): Promise<void> {
|
|
414
|
+
this.reconcileSnapshots(await runtime.getSubagents());
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
hasActiveChildren(): boolean {
|
|
418
|
+
for (const child of this.children.values()) if (child.status === "running") return true;
|
|
419
|
+
for (const dispatch of this.dispatches.values()) if (dispatch.acknowledged) return true;
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
terminalize(status: ChildTerminalStatus): void {
|
|
424
|
+
for (const child of this.children.values()) {
|
|
425
|
+
if (child.status === "running") this.finishChild(child, status, true);
|
|
426
|
+
}
|
|
427
|
+
this.dispatches.clear();
|
|
428
|
+
this.toolOwners.clear();
|
|
429
|
+
this.onActivityChange();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
close(): void {
|
|
433
|
+
if (this.closed) return;
|
|
434
|
+
this.terminalize("canceled");
|
|
435
|
+
this.closed = true;
|
|
436
|
+
for (const child of this.children.values()) {
|
|
437
|
+
child.projector.close();
|
|
438
|
+
if (child.sessionClosed) continue;
|
|
439
|
+
child.sessionClosed = true;
|
|
440
|
+
this.emit({ type: "session.closed", sessionId: child.sessionId });
|
|
441
|
+
}
|
|
442
|
+
this.bufferedEvents.length = 0;
|
|
443
|
+
this.bufferedBytes = 0;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private apply(event: OmpSubagentEvent): void {
|
|
447
|
+
if (event.type === "subagent_lifecycle") {
|
|
448
|
+
const child = this.ensureChild(
|
|
449
|
+
{
|
|
450
|
+
id: event.payload.id,
|
|
451
|
+
agent: event.payload.agent,
|
|
452
|
+
description: event.payload.description,
|
|
453
|
+
sessionFile: event.payload.sessionFile,
|
|
454
|
+
parentToolCallId: event.payload.parentToolCallId,
|
|
455
|
+
},
|
|
456
|
+
this.resolveParent(event.payload.parentToolCallId, event.payload.sessionFile),
|
|
457
|
+
);
|
|
458
|
+
const terminal = terminalStatus(event.payload.status);
|
|
459
|
+
if (terminal) this.requestTerminal(child, terminal);
|
|
460
|
+
else if (event.payload.status === "started") this.restartChild(child);
|
|
461
|
+
this.onActivityChange();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (event.type === "subagent_progress") {
|
|
465
|
+
const child = this.ensureChild(
|
|
466
|
+
{
|
|
467
|
+
id: event.payload.progress.id,
|
|
468
|
+
agent: event.payload.agent,
|
|
469
|
+
description: event.payload.progress.description ?? event.payload.assignment,
|
|
470
|
+
sessionFile: event.payload.sessionFile,
|
|
471
|
+
parentToolCallId: event.payload.parentToolCallId,
|
|
472
|
+
},
|
|
473
|
+
this.resolveParent(event.payload.parentToolCallId, event.payload.sessionFile),
|
|
474
|
+
);
|
|
475
|
+
const terminal = terminalStatus(event.payload.progress.status);
|
|
476
|
+
if (terminal) this.requestTerminal(child, terminal);
|
|
477
|
+
else this.restartChild(child);
|
|
478
|
+
this.onActivityChange();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const sessionId = this.sessionIdByNativeId.get(event.payload.id);
|
|
482
|
+
const child = sessionId ? this.children.get(sessionId) : undefined;
|
|
483
|
+
if (child?.status !== "running") return;
|
|
484
|
+
const nested = event.payload.event;
|
|
485
|
+
if (
|
|
486
|
+
(nested.type === "message_start" ||
|
|
487
|
+
nested.type === "message_update" ||
|
|
488
|
+
nested.type === "message_end") &&
|
|
489
|
+
nested.message.role === "assistant"
|
|
490
|
+
) {
|
|
491
|
+
const identity = nested.message.entryId ?? nested.message.responseId ?? nested.message.id;
|
|
492
|
+
if (identity && child.seenAssistantIdentities.has(identity)) return;
|
|
493
|
+
if (identity && nested.type === "message_end") child.seenAssistantIdentities.add(identity);
|
|
494
|
+
}
|
|
495
|
+
this.observeSessionEvent(child.sessionId, nested);
|
|
496
|
+
child.projector.project(nested, child.turnId);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private ensureChild(ref: ChildRef, parentSessionId: string): ChildState {
|
|
500
|
+
const existingSessionId = this.sessionIdByNativeId.get(ref.id);
|
|
501
|
+
const existing = existingSessionId ? this.children.get(existingSessionId) : undefined;
|
|
502
|
+
if (existing) {
|
|
503
|
+
if (ref.parentToolCallId) existing.parentToolCallId = ref.parentToolCallId;
|
|
504
|
+
if (existing.parentToolCallId) {
|
|
505
|
+
const dispatch = this.dispatches.get(existing.parentToolCallId);
|
|
506
|
+
if (dispatch?.ownerSessionId === existing.parentSessionId) {
|
|
507
|
+
dispatch.childSessionIds.add(existing.sessionId);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return existing;
|
|
511
|
+
}
|
|
512
|
+
if (this.children.size >= MAX_CHILDREN) throw new OmpPublicError("OMP subagent limit reached");
|
|
513
|
+
const digest = createHash("sha256")
|
|
514
|
+
.update(this.rootIdentityKey)
|
|
515
|
+
.update("\0")
|
|
516
|
+
.update(ref.id)
|
|
517
|
+
.digest("base64url")
|
|
518
|
+
.slice(0, 32);
|
|
519
|
+
const sessionId = `omp:subsession:${digest}`;
|
|
520
|
+
const turnId = `${sessionId}:turn:1`;
|
|
521
|
+
const title = this.dataFilter.text(ref.agent?.trim() || "OMP subagent", 256);
|
|
522
|
+
const description = ref.description
|
|
523
|
+
? this.dataFilter.text(ref.description, 16 * 1024)
|
|
524
|
+
: undefined;
|
|
525
|
+
const child: ChildState = {
|
|
526
|
+
nativeId: ref.id,
|
|
527
|
+
sessionId,
|
|
528
|
+
parentSessionId,
|
|
529
|
+
turnId,
|
|
530
|
+
turnSequence: 1,
|
|
531
|
+
title,
|
|
532
|
+
...(description ? { description } : {}),
|
|
533
|
+
...(ref.sessionFile ? { sessionFile: ref.sessionFile } : {}),
|
|
534
|
+
...(ref.parentToolCallId ? { parentToolCallId: ref.parentToolCallId } : {}),
|
|
535
|
+
status: "running",
|
|
536
|
+
sessionClosed: false,
|
|
537
|
+
seenAssistantIdentities: new BoundedStringSet(MAX_CHILD_MESSAGE_IDENTITIES),
|
|
538
|
+
seenInSnapshot: false,
|
|
539
|
+
projector: new OmpTimelineProjector(
|
|
540
|
+
sessionId,
|
|
541
|
+
this.emit,
|
|
542
|
+
this.scheduler,
|
|
543
|
+
this.outputRedactionValues,
|
|
544
|
+
false,
|
|
545
|
+
this.pluginTimelineEnabled,
|
|
546
|
+
),
|
|
547
|
+
};
|
|
548
|
+
this.children.set(sessionId, child);
|
|
549
|
+
this.sessionIdByNativeId.set(ref.id, sessionId);
|
|
550
|
+
if (ref.parentToolCallId) {
|
|
551
|
+
const dispatch = this.dispatches.get(ref.parentToolCallId);
|
|
552
|
+
if (dispatch?.ownerSessionId === parentSessionId) dispatch.childSessionIds.add(sessionId);
|
|
553
|
+
}
|
|
554
|
+
this.emit({
|
|
555
|
+
type: "session.opened",
|
|
556
|
+
sessionId,
|
|
557
|
+
parentSessionId,
|
|
558
|
+
capabilities: ["session.subsession"],
|
|
559
|
+
restoration: "parent",
|
|
560
|
+
cwd: this.cwd,
|
|
561
|
+
title,
|
|
562
|
+
...(description ? { description } : {}),
|
|
563
|
+
});
|
|
564
|
+
this.emit({ type: "session.ready", sessionId });
|
|
565
|
+
this.emit({ type: "session.turn", sessionId, turnId, state: "started" });
|
|
566
|
+
return child;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private restartChild(child: ChildState): void {
|
|
570
|
+
if (child.status === "running") return;
|
|
571
|
+
child.status = "running";
|
|
572
|
+
child.terminalRequested = undefined;
|
|
573
|
+
child.seenInSnapshot = false;
|
|
574
|
+
child.turnSequence += 1;
|
|
575
|
+
child.turnId = `${child.sessionId}:turn:${child.turnSequence}`;
|
|
576
|
+
this.emit({
|
|
577
|
+
type: "session.turn",
|
|
578
|
+
sessionId: child.sessionId,
|
|
579
|
+
turnId: child.turnId,
|
|
580
|
+
state: "started",
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private requestTerminal(child: ChildState, status: ChildTerminalStatus): void {
|
|
585
|
+
if (child.status !== "running") return;
|
|
586
|
+
child.terminalRequested = status;
|
|
587
|
+
if (!this.hasDirectActivity(child.sessionId)) this.finishChild(child, status);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private finishChild(child: ChildState, status: ChildTerminalStatus, force = false): void {
|
|
591
|
+
if (child.status !== "running") return;
|
|
592
|
+
if (!force && this.hasDirectActivity(child.sessionId)) {
|
|
593
|
+
child.terminalRequested = status;
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
child.status = status;
|
|
597
|
+
child.projector.finishTurn(child.turnId);
|
|
598
|
+
this.emit({
|
|
599
|
+
type: "session.turn",
|
|
600
|
+
sessionId: child.sessionId,
|
|
601
|
+
turnId: child.turnId,
|
|
602
|
+
state: status,
|
|
603
|
+
...(status === "failed" ? { error: { message: "OMP subagent failed" } } : {}),
|
|
604
|
+
});
|
|
605
|
+
for (const [toolCallId, dispatch] of this.dispatches) {
|
|
606
|
+
if (dispatch.childSessionIds.has(child.sessionId)) this.settleDispatch(toolCallId, dispatch);
|
|
607
|
+
}
|
|
608
|
+
const parent = this.children.get(child.parentSessionId);
|
|
609
|
+
if (parent?.terminalRequested && !this.hasDirectActivity(parent.sessionId)) {
|
|
610
|
+
this.finishChild(parent, parent.terminalRequested);
|
|
611
|
+
}
|
|
612
|
+
this.onActivityChange();
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
private hasDirectActivity(ownerSessionId: string): boolean {
|
|
616
|
+
for (const child of this.children.values()) {
|
|
617
|
+
if (child.parentSessionId === ownerSessionId && child.status === "running") return true;
|
|
618
|
+
}
|
|
619
|
+
for (const dispatch of this.dispatches.values()) {
|
|
620
|
+
if (dispatch.ownerSessionId === ownerSessionId && dispatch.acknowledged) return true;
|
|
621
|
+
}
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
private settleDispatch(toolCallId: string, dispatch: TaskDispatch): void {
|
|
626
|
+
if (!dispatch.acknowledged || dispatch.childSessionIds.size < dispatch.expectedChildren) return;
|
|
627
|
+
for (const sessionId of dispatch.childSessionIds) {
|
|
628
|
+
if (this.children.get(sessionId)?.status === "running") return;
|
|
629
|
+
}
|
|
630
|
+
this.toolOwners.delete(toolCallId);
|
|
631
|
+
this.dispatches.delete(toolCallId);
|
|
632
|
+
const owner = this.children.get(dispatch.ownerSessionId);
|
|
633
|
+
if (owner?.terminalRequested && !this.hasDirectActivity(owner.sessionId)) {
|
|
634
|
+
this.finishChild(owner, owner.terminalRequested);
|
|
635
|
+
}
|
|
636
|
+
this.onActivityChange();
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private resolveParent(parentToolCallId?: string, sessionFile?: string): string {
|
|
640
|
+
if (parentToolCallId) {
|
|
641
|
+
const owner = this.toolOwners.get(parentToolCallId);
|
|
642
|
+
if (owner) return owner;
|
|
643
|
+
}
|
|
644
|
+
if (!sessionFile) return this.rootSessionId;
|
|
645
|
+
let parentSessionId = this.rootSessionId;
|
|
646
|
+
let parentStemLength = this.rootSessionFile
|
|
647
|
+
? this.rootSessionFile.slice(0, -extname(this.rootSessionFile).length).length
|
|
648
|
+
: -1;
|
|
649
|
+
for (const child of this.children.values()) {
|
|
650
|
+
if (!child.sessionFile) continue;
|
|
651
|
+
const stem = child.sessionFile.slice(0, -extname(child.sessionFile).length);
|
|
652
|
+
if (stem.length > parentStemLength && sessionFile.startsWith(`${stem}/`)) {
|
|
653
|
+
parentSessionId = child.sessionId;
|
|
654
|
+
parentStemLength = stem.length;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return parentSessionId;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private reconcileSnapshots(snapshots: readonly OmpSubagentSnapshot[]): void {
|
|
661
|
+
const present = new Set<string>();
|
|
662
|
+
for (const snapshot of snapshots) {
|
|
663
|
+
const child = this.ensureChild(
|
|
664
|
+
{
|
|
665
|
+
id: snapshot.id,
|
|
666
|
+
agent: snapshot.agent,
|
|
667
|
+
description: snapshot.description ?? snapshot.assignment,
|
|
668
|
+
sessionFile: snapshot.sessionFile,
|
|
669
|
+
parentToolCallId: snapshot.parentToolCallId,
|
|
670
|
+
},
|
|
671
|
+
this.resolveParent(snapshot.parentToolCallId, snapshot.sessionFile),
|
|
672
|
+
);
|
|
673
|
+
present.add(child.nativeId);
|
|
674
|
+
const terminal = terminalStatus(snapshot.status);
|
|
675
|
+
if (terminal) this.requestTerminal(child, terminal);
|
|
676
|
+
else this.restartChild(child);
|
|
677
|
+
child.seenInSnapshot = true;
|
|
678
|
+
}
|
|
679
|
+
for (const child of this.children.values()) {
|
|
680
|
+
if (child.status === "running" && child.seenInSnapshot && !present.has(child.nativeId)) {
|
|
681
|
+
this.requestTerminal(child, "completed");
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
this.onActivityChange();
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
private async replaySnapshots(
|
|
688
|
+
snapshots: readonly OmpSubagentSnapshot[],
|
|
689
|
+
runtimeSession: OmpRuntimeSession,
|
|
690
|
+
runtime: OmpRuntime,
|
|
691
|
+
visited: Set<string>,
|
|
692
|
+
budget: ReplayBudget,
|
|
693
|
+
signal: AbortSignal,
|
|
694
|
+
): Promise<void> {
|
|
695
|
+
const ordered = [...snapshots].sort(
|
|
696
|
+
(left, right) =>
|
|
697
|
+
(left.sessionFile?.split("/").length ?? 0) - (right.sessionFile?.split("/").length ?? 0),
|
|
698
|
+
);
|
|
699
|
+
for (const snapshot of ordered) {
|
|
700
|
+
signal.throwIfAborted();
|
|
701
|
+
if (this.sessionIdByNativeId.has(snapshot.id)) continue;
|
|
702
|
+
const history = await waitForReplay(
|
|
703
|
+
runtimeSession.getSubagentMessages({ subagentId: snapshot.id }),
|
|
704
|
+
signal,
|
|
705
|
+
);
|
|
706
|
+
this.accountReplay(history.messages, budget, signal);
|
|
707
|
+
signal.throwIfAborted();
|
|
708
|
+
const parentSessionId = this.resolveParent(snapshot.parentToolCallId, history.sessionFile);
|
|
709
|
+
const child = this.ensureChild(
|
|
710
|
+
{ ...snapshot, sessionFile: history.sessionFile },
|
|
711
|
+
parentSessionId,
|
|
712
|
+
);
|
|
713
|
+
this.projectReplay(child, history.messages, signal);
|
|
714
|
+
visited.add(`${history.sessionFile}\0${snapshot.id}`);
|
|
715
|
+
await this.replayChildren(
|
|
716
|
+
child.sessionId,
|
|
717
|
+
history.sessionFile,
|
|
718
|
+
history.messages,
|
|
719
|
+
runtime,
|
|
720
|
+
visited,
|
|
721
|
+
budget,
|
|
722
|
+
signal,
|
|
723
|
+
1,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
const activeToolCallIds = new Set(
|
|
727
|
+
snapshots.flatMap((snapshot) =>
|
|
728
|
+
snapshot.parentToolCallId ? [snapshot.parentToolCallId] : [],
|
|
729
|
+
),
|
|
730
|
+
);
|
|
731
|
+
for (const toolCallId of this.toolOwners.keys()) {
|
|
732
|
+
if (!activeToolCallIds.has(toolCallId) && !this.dispatches.has(toolCallId)) {
|
|
733
|
+
this.toolOwners.delete(toolCallId);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
private async replayChildren(
|
|
739
|
+
parentSessionId: string,
|
|
740
|
+
parentSessionFile: string | undefined,
|
|
741
|
+
messages: readonly OmpMessage[],
|
|
742
|
+
runtime: OmpRuntime,
|
|
743
|
+
visited: Set<string>,
|
|
744
|
+
budget: ReplayBudget,
|
|
745
|
+
signal: AbortSignal,
|
|
746
|
+
depth: number,
|
|
747
|
+
): Promise<void> {
|
|
748
|
+
signal.throwIfAborted();
|
|
749
|
+
if (depth > MAX_REPLAY_DEPTH) throw new OmpPublicError("OMP subagent history is too deep");
|
|
750
|
+
this.indexTaskCalls(parentSessionId, messages);
|
|
751
|
+
for (const ref of replayChildren(messages)) {
|
|
752
|
+
signal.throwIfAborted();
|
|
753
|
+
if (!parentSessionFile) {
|
|
754
|
+
throw new OmpPublicError("OMP parent transcript identity is unavailable");
|
|
755
|
+
}
|
|
756
|
+
const visitKey = `${parentSessionFile}\0${ref.id}`;
|
|
757
|
+
if (visited.has(visitKey)) continue;
|
|
758
|
+
visited.add(visitKey);
|
|
759
|
+
const history = await waitForReplay(
|
|
760
|
+
runtime.readPersistedSubagentTranscript({
|
|
761
|
+
parentSessionFile,
|
|
762
|
+
childTranscriptId: ref.id,
|
|
763
|
+
cwd: this.cwd,
|
|
764
|
+
signal,
|
|
765
|
+
}),
|
|
766
|
+
signal,
|
|
767
|
+
);
|
|
768
|
+
this.accountReplay(history.messages, budget, signal);
|
|
769
|
+
signal.throwIfAborted();
|
|
770
|
+
const child = this.ensureChild({ ...ref, sessionFile: history.sessionFile }, parentSessionId);
|
|
771
|
+
this.projectReplay(child, history.messages, signal);
|
|
772
|
+
await this.replayChildren(
|
|
773
|
+
child.sessionId,
|
|
774
|
+
history.sessionFile,
|
|
775
|
+
history.messages,
|
|
776
|
+
runtime,
|
|
777
|
+
visited,
|
|
778
|
+
budget,
|
|
779
|
+
signal,
|
|
780
|
+
depth + 1,
|
|
781
|
+
);
|
|
782
|
+
signal.throwIfAborted();
|
|
783
|
+
this.requestTerminal(
|
|
784
|
+
child,
|
|
785
|
+
ref.status === "derive" ? replayTerminalStatus(history.messages) : ref.status,
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
private projectReplay(
|
|
791
|
+
child: ChildState,
|
|
792
|
+
messages: readonly OmpMessage[],
|
|
793
|
+
signal: AbortSignal,
|
|
794
|
+
): void {
|
|
795
|
+
this.indexTaskCalls(child.sessionId, messages);
|
|
796
|
+
for (const message of messages) {
|
|
797
|
+
signal.throwIfAborted();
|
|
798
|
+
child.projector.projectReplayMessage(message);
|
|
799
|
+
if (message.role === "assistant") {
|
|
800
|
+
const identity = message.entryId ?? message.responseId ?? message.id;
|
|
801
|
+
if (identity) child.seenAssistantIdentities.add(identity);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
signal.throwIfAborted();
|
|
805
|
+
child.projector.finishReplay();
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private accountReplay(
|
|
809
|
+
messages: readonly OmpMessage[],
|
|
810
|
+
budget: ReplayBudget,
|
|
811
|
+
signal: AbortSignal,
|
|
812
|
+
): void {
|
|
813
|
+
signal.throwIfAborted();
|
|
814
|
+
if (budget.messages + messages.length > MAX_REPLAY_MESSAGES) {
|
|
815
|
+
throw new OmpPublicError("OMP subagent history exceeds replay limits");
|
|
816
|
+
}
|
|
817
|
+
const metrics = boundedJsonMetrics(
|
|
818
|
+
messages,
|
|
819
|
+
MAX_REPLAY_BYTES - budget.bytes,
|
|
820
|
+
MAX_REPLAY_MESSAGES,
|
|
821
|
+
MAX_REPLAY_BYTES,
|
|
822
|
+
MAX_REPLAY_NODES - budget.nodes,
|
|
823
|
+
);
|
|
824
|
+
if (!metrics) throw new OmpPublicError("OMP subagent history exceeds replay limits");
|
|
825
|
+
budget.messages += messages.length;
|
|
826
|
+
budget.bytes += metrics.bytes;
|
|
827
|
+
budget.nodes += metrics.nodes;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private indexTaskCalls(ownerSessionId: string, messages: readonly OmpMessage[]): void {
|
|
831
|
+
for (const message of messages) {
|
|
832
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
833
|
+
for (const part of message.content) {
|
|
834
|
+
if (part.type !== "toolCall" || part.name !== "task" || !part.id) continue;
|
|
835
|
+
this.registerToolOwner(part.id, ownerSessionId);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
private registerToolOwner(toolCallId: string, ownerSessionId: string): void {
|
|
841
|
+
const existing = this.toolOwners.get(toolCallId);
|
|
842
|
+
if (existing && existing !== ownerSessionId) {
|
|
843
|
+
throw new OmpPublicError("OMP reused a task tool identifier across child sessions");
|
|
844
|
+
}
|
|
845
|
+
if (!existing && this.toolOwners.size >= MAX_TASK_DISPATCHES) {
|
|
846
|
+
throw new OmpPublicError("OMP subagent dispatch limit reached");
|
|
847
|
+
}
|
|
848
|
+
this.toolOwners.set(toolCallId, ownerSessionId);
|
|
849
|
+
}
|
|
850
|
+
}
|