@omercnet/paseo-omp 0.4.0 → 1.0.0
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/README.md +8 -8
- package/client/external-url.ts +2 -11
- package/client/omp-config-styles.ts +284 -0
- package/client/omp-config-surface.tsx +16 -286
- package/client/omp-routing-editor.tsx +1 -3
- package/client/provider-launch-settings.tsx +118 -0
- package/index.client.tsx +21 -2
- package/index.server.ts +10 -0
- package/package.json +14 -9
- package/paseo-plugin.json +2 -1
- package/scripts/prepare-dependencies.mjs +44 -11
- package/server/omp-models.ts +2 -6
- package/server/package-version.ts +1 -1
- package/server/protocol-violation-diagnostics.ts +1 -1
- package/server/provider/catalog.ts +2 -1
- package/server/provider/config-normalization.ts +19 -3
- package/server/provider/connection.ts +30 -0
- package/server/provider/host-tools.ts +2 -2
- package/server/provider/mcp-transport.ts +9 -2
- package/server/provider/omp-rpc-environment.ts +368 -0
- package/server/provider/omp-rpc-process.ts +119 -0
- package/server/provider/omp-rpc-protocol.ts +1949 -0
- package/server/provider/omp-rpc-transport.ts +1373 -0
- package/server/provider/omp-rpc-values.ts +31 -0
- package/server/provider/omp-rpc.ts +267 -3311
- package/server/provider/profile-providers.ts +2 -1
- package/server/provider/prompt-payload.ts +253 -0
- package/server/provider/registration.ts +10 -2
- package/server/provider/session-compaction.ts +187 -0
- package/server/provider/session-descriptors.ts +52 -18
- package/server/provider/session-events.ts +42 -0
- package/server/provider/session-permissions.ts +987 -0
- package/server/provider/session-terminal.ts +307 -0
- package/server/provider/session-usage.ts +234 -0
- package/server/provider/session.ts +145 -1853
- package/server/provider/settings.ts +8 -5
- package/server/provider/subsessions.ts +30 -9
- package/server/provider/timeline-projector.ts +140 -29
- package/shared/external-url.ts +0 -9
- package/shared/provider-launch-settings.ts +18 -0
|
@@ -4,7 +4,8 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { basename, join, resolve } from "node:path";
|
|
5
5
|
import { isOmpProfileName } from "../../shared/omp-store";
|
|
6
6
|
import { ompAgentDir, ompCacheDir, ompDataDir, ompSessionDir, ompStateDir } from "../paths";
|
|
7
|
-
import { OmpRpcRuntime, type OmpRuntime
|
|
7
|
+
import { OmpRpcRuntime, type OmpRuntime } from "./omp-rpc";
|
|
8
|
+
import type { OmpStartOptions } from "./omp-rpc-environment";
|
|
8
9
|
import { parseOmpProviderOptions } from "./provider-options";
|
|
9
10
|
import { createOmpProvider, type OmpProviderOptions } from "./registration";
|
|
10
11
|
import { OmpPublicError } from "./security";
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type { ProviderContent, ProviderInput } from "@getpaseo/plugin/server/provider";
|
|
2
|
+
import { getForgeDefinitionOrNeutral } from "@getpaseo/protocol/forge-manifest";
|
|
3
|
+
import { isValidImagePayload } from "./image";
|
|
4
|
+
import type { OmpImage } from "./omp-rpc-protocol";
|
|
5
|
+
import { OmpPublicError, utf8Bytes } from "./security";
|
|
6
|
+
|
|
7
|
+
type SessionPromptInput = Extract<ProviderInput, { type: "session.prompt" }>;
|
|
8
|
+
const MAX_PROMPT_PARTS = 64;
|
|
9
|
+
export const MAX_PROMPT_TEXT_LENGTH = 1024 * 1024;
|
|
10
|
+
const RPC_REQUEST_ID_BYTES = 36;
|
|
11
|
+
|
|
12
|
+
export function isSafeCommandName(name: string): boolean {
|
|
13
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*(?::[A-Za-z0-9][A-Za-z0-9._-]*)*$/u.test(name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type OmpPromptPayload = { text: string; images: OmpImage[]; commandName?: string };
|
|
17
|
+
export function buildOmpPromptRequest(message: string, images: readonly OmpImage[] = []) {
|
|
18
|
+
return {
|
|
19
|
+
type: "prompt" as const,
|
|
20
|
+
message,
|
|
21
|
+
streamingBehavior: "followUp" as const,
|
|
22
|
+
...(images.length > 0 ? { images } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const REVIEW_LINE_MARKERS = { add: "+", remove: "-", context: " " } as const;
|
|
27
|
+
|
|
28
|
+
function renderPromptAttachmentAsText(part: Exclude<ProviderContent, { type: "image" }>): string {
|
|
29
|
+
switch (part.type) {
|
|
30
|
+
case "forge_change_request": {
|
|
31
|
+
return renderChangeRequestAttachment({
|
|
32
|
+
forge: part.forge ?? "github",
|
|
33
|
+
number: part.number,
|
|
34
|
+
title: part.title,
|
|
35
|
+
url: part.url,
|
|
36
|
+
body: part.body,
|
|
37
|
+
projectPath: part.projectPath,
|
|
38
|
+
baseRefName: part.baseRefName,
|
|
39
|
+
headRefName: part.headRefName,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
case "github_pr": {
|
|
43
|
+
return renderChangeRequestAttachment({
|
|
44
|
+
forge: "github",
|
|
45
|
+
number: part.number,
|
|
46
|
+
title: part.title,
|
|
47
|
+
url: part.url,
|
|
48
|
+
body: part.body,
|
|
49
|
+
baseRefName: part.baseRefName,
|
|
50
|
+
headRefName: part.headRefName,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
case "forge_issue": {
|
|
54
|
+
return renderIssueAttachment({
|
|
55
|
+
forge: part.forge ?? "github",
|
|
56
|
+
number: part.number,
|
|
57
|
+
title: part.title,
|
|
58
|
+
url: part.url,
|
|
59
|
+
body: part.body,
|
|
60
|
+
projectPath: part.projectPath,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
case "github_issue": {
|
|
64
|
+
return renderIssueAttachment({
|
|
65
|
+
forge: "github",
|
|
66
|
+
number: part.number,
|
|
67
|
+
title: part.title,
|
|
68
|
+
url: part.url,
|
|
69
|
+
body: part.body,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
case "text": {
|
|
73
|
+
return part.text;
|
|
74
|
+
}
|
|
75
|
+
case "review": {
|
|
76
|
+
const lines = [`Paseo review attachment (${part.mode})`, `CWD: ${part.cwd}`];
|
|
77
|
+
if (part.baseRef) {
|
|
78
|
+
lines.push(`Base: ${part.baseRef}`);
|
|
79
|
+
}
|
|
80
|
+
part.comments.forEach((comment, index) => {
|
|
81
|
+
lines.push(
|
|
82
|
+
"",
|
|
83
|
+
`Comment ${index + 1}: ${comment.filePath}:${comment.side}:${comment.lineNumber}`,
|
|
84
|
+
comment.body,
|
|
85
|
+
comment.context.hunkHeader,
|
|
86
|
+
);
|
|
87
|
+
const target = comment.context.targetLine;
|
|
88
|
+
for (const line of comment.context.lines) {
|
|
89
|
+
const isTarget =
|
|
90
|
+
line.oldLineNumber === target.oldLineNumber &&
|
|
91
|
+
line.newLineNumber === target.newLineNumber &&
|
|
92
|
+
line.type === target.type &&
|
|
93
|
+
line.content === target.content;
|
|
94
|
+
const prefix = isTarget ? "> " : " ";
|
|
95
|
+
const oldLn = padLineNumber(line.oldLineNumber);
|
|
96
|
+
const newLn = padLineNumber(line.newLineNumber);
|
|
97
|
+
lines.push(`${prefix}${oldLn} ${newLn} ${REVIEW_LINE_MARKERS[line.type]}${line.content}`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return lines.join("\n");
|
|
101
|
+
}
|
|
102
|
+
case "uploaded_file": {
|
|
103
|
+
return [
|
|
104
|
+
`Uploaded file: ${part.fileName}`,
|
|
105
|
+
`Path: ${part.path}`,
|
|
106
|
+
`MIME: ${part.mimeType}`,
|
|
107
|
+
`Size: ${part.size} bytes`,
|
|
108
|
+
].join("\n");
|
|
109
|
+
}
|
|
110
|
+
default:
|
|
111
|
+
throw new Error("unreachable");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderChangeRequestAttachment(input: {
|
|
116
|
+
forge: string;
|
|
117
|
+
number: number;
|
|
118
|
+
title: string;
|
|
119
|
+
url: string;
|
|
120
|
+
body?: string | null;
|
|
121
|
+
projectPath?: string;
|
|
122
|
+
baseRefName?: string | null;
|
|
123
|
+
headRefName?: string | null;
|
|
124
|
+
}): string {
|
|
125
|
+
const lines = [
|
|
126
|
+
`${formatForgeLabel(input.forge)} ${formatChangeRequestAbbrev(input.forge)} ${formatChangeRequestNumber(input.forge, input.number)}: ${input.title}`,
|
|
127
|
+
input.url,
|
|
128
|
+
];
|
|
129
|
+
if (input.projectPath) {
|
|
130
|
+
lines.push(`Project: ${input.projectPath}`);
|
|
131
|
+
}
|
|
132
|
+
if (input.baseRefName) {
|
|
133
|
+
lines.push(`Base: ${input.baseRefName}`);
|
|
134
|
+
}
|
|
135
|
+
if (input.headRefName) {
|
|
136
|
+
lines.push(`Head: ${input.headRefName}`);
|
|
137
|
+
}
|
|
138
|
+
if (input.body) {
|
|
139
|
+
lines.push("", input.body);
|
|
140
|
+
}
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function renderIssueAttachment(input: {
|
|
145
|
+
forge: string;
|
|
146
|
+
number: number;
|
|
147
|
+
title: string;
|
|
148
|
+
url: string;
|
|
149
|
+
body?: string | null;
|
|
150
|
+
projectPath?: string;
|
|
151
|
+
}): string {
|
|
152
|
+
const lines = [
|
|
153
|
+
`${formatForgeLabel(input.forge)} Issue ${formatIssueNumber(input.forge, input.number)}: ${input.title}`,
|
|
154
|
+
input.url,
|
|
155
|
+
];
|
|
156
|
+
if (input.projectPath) {
|
|
157
|
+
lines.push(`Project: ${input.projectPath}`);
|
|
158
|
+
}
|
|
159
|
+
if (input.body) {
|
|
160
|
+
lines.push("", input.body);
|
|
161
|
+
}
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function formatForgeLabel(forge: string): string {
|
|
166
|
+
return getForgeDefinitionOrNeutral(forge).displayName;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function formatChangeRequestAbbrev(forge: string): string {
|
|
170
|
+
return getForgeDefinitionOrNeutral(forge).changeRequestAbbrev;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function formatChangeRequestNumber(forge: string, number: number): string {
|
|
174
|
+
return `${getForgeDefinitionOrNeutral(forge).changeRequestNumberPrefix}${number}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function formatIssueNumber(forge: string, number: number): string {
|
|
178
|
+
return `${getForgeDefinitionOrNeutral(forge).issueNumberPrefix}${number}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function padLineNumber(lineNumber: number | null): string {
|
|
182
|
+
return (lineNumber?.toString() ?? "-").padStart(2);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function promptPayload(input: SessionPromptInput): OmpPromptPayload {
|
|
186
|
+
if (input.prompt.outputSchema !== undefined || input.prompt.clearPendingPermissions) {
|
|
187
|
+
throw new OmpPublicError("OMP does not support structured output or permission controls");
|
|
188
|
+
}
|
|
189
|
+
if (input.prompt.input.type === "command") {
|
|
190
|
+
const name = input.prompt.input.name.trim();
|
|
191
|
+
if (!isSafeCommandName(name)) throw new OmpPublicError("Invalid OMP command name");
|
|
192
|
+
const argumentsText = input.prompt.input.arguments.trim();
|
|
193
|
+
const text = `/${name}${argumentsText ? ` ${argumentsText}` : ""}`;
|
|
194
|
+
if (utf8Bytes(text) > MAX_PROMPT_TEXT_LENGTH)
|
|
195
|
+
throw new OmpPublicError("OMP command is too large");
|
|
196
|
+
return { text, images: [], commandName: name };
|
|
197
|
+
}
|
|
198
|
+
if (input.prompt.input.content.length > MAX_PROMPT_PARTS) {
|
|
199
|
+
throw new OmpPublicError("OMP prompt has too many content parts");
|
|
200
|
+
}
|
|
201
|
+
const parts: string[] = [];
|
|
202
|
+
const images: OmpImage[] = [];
|
|
203
|
+
let length = 0;
|
|
204
|
+
const appendText = (text: string) => {
|
|
205
|
+
length += utf8Bytes(text) + (parts.length > 0 ? 2 : 0);
|
|
206
|
+
if (length > MAX_PROMPT_TEXT_LENGTH) throw new OmpPublicError("OMP prompt is too large");
|
|
207
|
+
parts.push(text);
|
|
208
|
+
};
|
|
209
|
+
for (const part of input.prompt.input.content) {
|
|
210
|
+
if (part.type === "text") {
|
|
211
|
+
appendText(part.text);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (part.type === "image") {
|
|
215
|
+
if (!isValidImagePayload(part.data, part.mimeType, 8 * 1024 * 1024)) {
|
|
216
|
+
throw new OmpPublicError("OMP prompt image is invalid");
|
|
217
|
+
}
|
|
218
|
+
images.push({ type: "image", data: part.data, mimeType: part.mimeType });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
appendText(renderPromptAttachmentAsText(part));
|
|
222
|
+
}
|
|
223
|
+
const text = parts.join("\n\n").trim();
|
|
224
|
+
if (!text && images.length === 0) throw new OmpPublicError("OMP prompt cannot be empty");
|
|
225
|
+
return { text, images };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function inlinePromptFrameBytes(
|
|
229
|
+
payload: OmpPromptPayload,
|
|
230
|
+
delivery: "prompt" | "steer",
|
|
231
|
+
): number {
|
|
232
|
+
const images = payload.images.map(({ type, mimeType }) => ({ type, data: "", mimeType }));
|
|
233
|
+
const frame =
|
|
234
|
+
delivery === "prompt"
|
|
235
|
+
? { ...buildOmpPromptRequest(payload.text, images), id: "" }
|
|
236
|
+
: { type: "steer", message: payload.text, ...(images.length > 0 ? { images } : {}) };
|
|
237
|
+
const requestIdBytes = delivery === "prompt" ? RPC_REQUEST_ID_BYTES : 0;
|
|
238
|
+
return (
|
|
239
|
+
utf8Bytes(JSON.stringify(frame)) +
|
|
240
|
+
requestIdBytes +
|
|
241
|
+
payload.images.reduce((total, image) => total + image.data.length, 0) +
|
|
242
|
+
1
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function slashCommandName(text: string): string | undefined {
|
|
247
|
+
if (!text.startsWith("/")) return undefined;
|
|
248
|
+
const body = text.slice(1);
|
|
249
|
+
if (!body) return undefined;
|
|
250
|
+
const firstWhitespace = body.search(/\s/u);
|
|
251
|
+
const name = firstWhitespace === -1 ? body : body.slice(0, firstWhitespace);
|
|
252
|
+
return name || undefined;
|
|
253
|
+
}
|
|
@@ -5,9 +5,11 @@ import { z } from "zod";
|
|
|
5
5
|
import type { OmpBrowserAuthorizationRegistry } from "../mcp-browser";
|
|
6
6
|
import type { OmpOperationalFailureReporter } from "../operational-failure-diagnostics";
|
|
7
7
|
import { probeOmpAvailability } from "../provider-diagnostics";
|
|
8
|
+
import { mergeOmpInheritedEnvironmentNames } from "./config-normalization";
|
|
8
9
|
import { createOmpConnection, OmpNativeSessionReservations } from "./connection";
|
|
9
10
|
import type { OmpMcpConnector } from "./host-tools";
|
|
10
|
-
import {
|
|
11
|
+
import { OmpRpcRuntime, type OmpRuntime } from "./omp-rpc";
|
|
12
|
+
import type { OmpProtocolViolationDiagnostic } from "./omp-rpc-protocol";
|
|
11
13
|
import { parseOmpProviderOptions } from "./provider-options";
|
|
12
14
|
import { boundedJsonBytes } from "./security";
|
|
13
15
|
import { OmpProviderOptionsSchema } from "./settings";
|
|
@@ -73,6 +75,7 @@ export interface OmpProviderOptions {
|
|
|
73
75
|
options: ProviderCatalogOptionsCompat,
|
|
74
76
|
timeoutMs: number | undefined,
|
|
75
77
|
) => Promise<ProviderAvailabilityCompat>;
|
|
78
|
+
resolveHostInheritEnv?: () => Promise<readonly string[]>;
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
function stableJson(value: unknown): string {
|
|
@@ -103,6 +106,10 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
|
|
|
103
106
|
providerOptionsSchema: OmpProviderOptionsSchema,
|
|
104
107
|
async getCatalogCacheKey(catalogOptions) {
|
|
105
108
|
const providerOptions = parseOmpProviderOptions(catalogOptions.providerOptions);
|
|
109
|
+
const inheritEnv = mergeOmpInheritedEnvironmentNames(
|
|
110
|
+
(await options.resolveHostInheritEnv?.()) ?? [],
|
|
111
|
+
providerOptions.inheritEnv,
|
|
112
|
+
);
|
|
106
113
|
// Profile providers cannot safely hash explicit environment values because they may be
|
|
107
114
|
// credentials. Disable sharing for that case; otherwise retain every normalized option
|
|
108
115
|
// that can change discovery alongside the fixed, non-secret store identity.
|
|
@@ -111,7 +118,7 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
|
|
|
111
118
|
scope: catalogOptions.scope,
|
|
112
119
|
...(catalogOptions.scope === "workspace" ? { cwd: catalogOptions.cwd } : {}),
|
|
113
120
|
...(options.catalogIdentity ? { store: options.catalogIdentity } : {}),
|
|
114
|
-
providerOptions,
|
|
121
|
+
providerOptions: { ...providerOptions, ...(inheritEnv ? { inheritEnv } : {}) },
|
|
115
122
|
settings: catalogOptions.settings ?? {},
|
|
116
123
|
defaultCommand: (options.environment ?? process.env).OMP_COMMAND ?? "omp",
|
|
117
124
|
};
|
|
@@ -166,6 +173,7 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
|
|
|
166
173
|
options.browserAuthorizationRegistry,
|
|
167
174
|
undefined,
|
|
168
175
|
options.reportOperationalFailure,
|
|
176
|
+
options.resolveHostInheritEnv,
|
|
169
177
|
);
|
|
170
178
|
},
|
|
171
179
|
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { ProviderEvent } from "@getpaseo/plugin/server/provider";
|
|
3
|
+
import type { OmpRpcEvent } from "./omp-rpc-protocol";
|
|
4
|
+
import type { OmpPublicDataSerializer } from "./security";
|
|
5
|
+
import type { ActiveTurn } from "./session-terminal";
|
|
6
|
+
import type { OmpSessionUsage } from "./session-usage";
|
|
7
|
+
import type { OmpTimelineProjector } from "./timeline-projector";
|
|
8
|
+
|
|
9
|
+
type Emit = (event: ProviderEvent) => void;
|
|
10
|
+
|
|
11
|
+
export interface ActiveCompaction {
|
|
12
|
+
id: string;
|
|
13
|
+
trigger: "auto" | "manual";
|
|
14
|
+
turnId: string;
|
|
15
|
+
generation: number;
|
|
16
|
+
retrying: boolean;
|
|
17
|
+
action?: string;
|
|
18
|
+
preTokens?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class OmpSessionCompaction {
|
|
22
|
+
private operation: ActiveCompaction | null = null;
|
|
23
|
+
private discardedEnds = 0;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
private readonly sessionId: string,
|
|
27
|
+
private readonly emit: Emit,
|
|
28
|
+
private readonly projector: OmpTimelineProjector,
|
|
29
|
+
private readonly dataFilter: OmpPublicDataSerializer,
|
|
30
|
+
private readonly usage: OmpSessionUsage,
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
get active(): ActiveCompaction | null {
|
|
34
|
+
return this.operation;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
start(turn: ActiveTurn, trigger: "auto" | "manual", action?: string): void {
|
|
38
|
+
if (this.discardedEnds > 0) {
|
|
39
|
+
this.discardedEnds += 1;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const active = this.operation;
|
|
43
|
+
if (active && active.trigger === trigger && active.action === action) {
|
|
44
|
+
active.retrying = false;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (active) {
|
|
48
|
+
this.retire("OMP emitted overlapping compactions");
|
|
49
|
+
this.discardedEnds = 2;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const operation: ActiveCompaction = {
|
|
53
|
+
id: randomUUID(),
|
|
54
|
+
trigger,
|
|
55
|
+
turnId: turn.turnId,
|
|
56
|
+
generation: turn.generation,
|
|
57
|
+
action,
|
|
58
|
+
retrying: false,
|
|
59
|
+
preTokens: this.usage.latest?.contextWindowUsedTokens,
|
|
60
|
+
};
|
|
61
|
+
this.operation = operation;
|
|
62
|
+
this.projector.flush(true);
|
|
63
|
+
this.emit({
|
|
64
|
+
type: "timeline.item",
|
|
65
|
+
sessionId: this.sessionId,
|
|
66
|
+
item: {
|
|
67
|
+
id: operation.id,
|
|
68
|
+
type: "compaction",
|
|
69
|
+
status: "loading",
|
|
70
|
+
trigger,
|
|
71
|
+
...(operation.preTokens !== undefined ? { preTokens: operation.preTokens } : {}),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
handleAutoEnd(
|
|
77
|
+
turn: ActiveTurn,
|
|
78
|
+
event: Extract<OmpRpcEvent, { type: "auto_compaction_end" }>,
|
|
79
|
+
): boolean {
|
|
80
|
+
if (this.discardedEnds > 0) {
|
|
81
|
+
this.discardedEnds -= 1;
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const operation = this.operation;
|
|
85
|
+
if (
|
|
86
|
+
operation?.trigger !== "auto" ||
|
|
87
|
+
operation.turnId !== turn.turnId ||
|
|
88
|
+
operation.generation !== turn.generation
|
|
89
|
+
) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (event.action !== undefined && operation.action !== event.action) {
|
|
93
|
+
this.retire("OMP emitted overlapping compactions");
|
|
94
|
+
this.discardedEnds = 1;
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (event.willRetry) {
|
|
98
|
+
operation.retrying = true;
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (operation.retrying) {
|
|
102
|
+
operation.retrying = false;
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
const state = event.aborted
|
|
106
|
+
? "canceled"
|
|
107
|
+
: event.errorMessage
|
|
108
|
+
? "failed"
|
|
109
|
+
: event.skipped
|
|
110
|
+
? "skipped"
|
|
111
|
+
: "completed";
|
|
112
|
+
this.finish(state, {
|
|
113
|
+
tokensBefore: event.result?.tokensBefore ?? event.result?.preTokens,
|
|
114
|
+
message: event.errorMessage,
|
|
115
|
+
});
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
finish(
|
|
120
|
+
state: "completed" | "failed" | "canceled" | "skipped",
|
|
121
|
+
options: { tokensBefore?: number | null; message?: string } = {},
|
|
122
|
+
): void {
|
|
123
|
+
const operation = this.operation;
|
|
124
|
+
if (!operation) return;
|
|
125
|
+
this.operation = null;
|
|
126
|
+
this.projector.flush(true);
|
|
127
|
+
if (state === "completed") {
|
|
128
|
+
this.usage.completeCompaction();
|
|
129
|
+
}
|
|
130
|
+
if (state !== "completed") {
|
|
131
|
+
const defaultMessage =
|
|
132
|
+
state === "failed"
|
|
133
|
+
? "OMP compaction failed"
|
|
134
|
+
: state === "canceled"
|
|
135
|
+
? "OMP compaction canceled"
|
|
136
|
+
: "OMP compaction skipped";
|
|
137
|
+
this.emit({
|
|
138
|
+
type: "timeline.item",
|
|
139
|
+
sessionId: this.sessionId,
|
|
140
|
+
item: {
|
|
141
|
+
id: operation.id,
|
|
142
|
+
type: "notification",
|
|
143
|
+
level: state === "failed" ? "error" : "info",
|
|
144
|
+
message: this.dataFilter.text(options.message ?? defaultMessage, 4_096),
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const tokensBefore = options.tokensBefore ?? operation.preTokens;
|
|
150
|
+
this.emit({
|
|
151
|
+
type: "timeline.item",
|
|
152
|
+
sessionId: this.sessionId,
|
|
153
|
+
item: {
|
|
154
|
+
id: operation.id,
|
|
155
|
+
type: "compaction",
|
|
156
|
+
status: "completed",
|
|
157
|
+
trigger: operation.trigger,
|
|
158
|
+
...(tokensBefore !== undefined ? { preTokens: tokensBefore } : {}),
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
retire(message: string): void {
|
|
164
|
+
const operation = this.operation;
|
|
165
|
+
if (!operation) return;
|
|
166
|
+
this.operation = null;
|
|
167
|
+
this.emit({
|
|
168
|
+
type: "timeline.item",
|
|
169
|
+
sessionId: this.sessionId,
|
|
170
|
+
item: {
|
|
171
|
+
id: operation.id,
|
|
172
|
+
type: "compaction",
|
|
173
|
+
status: "completed",
|
|
174
|
+
trigger: operation.trigger,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
this.emit({
|
|
178
|
+
type: "timeline.item",
|
|
179
|
+
sessionId: this.sessionId,
|
|
180
|
+
item: { id: `${operation.id}:error`, type: "error", message },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
resetDiscardedEnds(): void {
|
|
185
|
+
this.discardedEnds = 0;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { constants, type Dir } from "node:fs";
|
|
3
3
|
import { type FileHandle, lstat, open, opendir, realpath } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { basename,
|
|
5
|
+
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { ompSessionDir } from "../paths";
|
|
7
7
|
import { isValidImagePayload } from "./image";
|
|
8
8
|
|
|
@@ -510,16 +510,19 @@ interface PersistedTranscriptNode {
|
|
|
510
510
|
message?: unknown;
|
|
511
511
|
}
|
|
512
512
|
|
|
513
|
-
function persistedTranscriptMessage(record: Record<string, unknown>, entryId
|
|
513
|
+
function persistedTranscriptMessage(record: Record<string, unknown>, entryId?: string): unknown {
|
|
514
514
|
if (record.type === "message") {
|
|
515
515
|
if (!record.message || typeof record.message !== "object" || Array.isArray(record.message))
|
|
516
516
|
return;
|
|
517
|
-
return {
|
|
517
|
+
return {
|
|
518
|
+
...(record.message as Record<string, unknown>),
|
|
519
|
+
...(entryId ? { entryId } : {}),
|
|
520
|
+
};
|
|
518
521
|
}
|
|
519
522
|
if (record.type !== "custom_message") return;
|
|
520
523
|
return {
|
|
521
524
|
role: "custom",
|
|
522
|
-
entryId,
|
|
525
|
+
...(entryId ? { entryId } : {}),
|
|
523
526
|
customType: record.customType,
|
|
524
527
|
content: record.content,
|
|
525
528
|
display: record.display,
|
|
@@ -680,6 +683,7 @@ export async function readOmpPersistedSubagentTranscript(
|
|
|
680
683
|
childTranscriptId: string,
|
|
681
684
|
cwd: string,
|
|
682
685
|
signal?: AbortSignal,
|
|
686
|
+
childSessionFile?: string,
|
|
683
687
|
): Promise<OmpPersistedSubagentTranscript> {
|
|
684
688
|
signal?.throwIfAborted();
|
|
685
689
|
if (
|
|
@@ -687,7 +691,12 @@ export async function readOmpPersistedSubagentTranscript(
|
|
|
687
691
|
!parentSessionFile.endsWith(".jsonl") ||
|
|
688
692
|
parentSessionFile.includes("\0") ||
|
|
689
693
|
!CHILD_TRANSCRIPT_ID.test(childTranscriptId) ||
|
|
690
|
-
basename(childTranscriptId) !== childTranscriptId
|
|
694
|
+
basename(childTranscriptId) !== childTranscriptId ||
|
|
695
|
+
(childSessionFile !== undefined &&
|
|
696
|
+
(!isAbsolute(childSessionFile) ||
|
|
697
|
+
!childSessionFile.endsWith(".jsonl") ||
|
|
698
|
+
childSessionFile.includes("\0") ||
|
|
699
|
+
basename(childSessionFile) !== `${childTranscriptId}.jsonl`))
|
|
691
700
|
) {
|
|
692
701
|
throw new Error("Invalid OMP child transcript descriptor");
|
|
693
702
|
}
|
|
@@ -707,34 +716,53 @@ export async function readOmpPersistedSubagentTranscript(
|
|
|
707
716
|
await parentHandle.close().catch(() => undefined);
|
|
708
717
|
}
|
|
709
718
|
const canonicalParent = await realpath(parentSessionFile);
|
|
710
|
-
const
|
|
711
|
-
const expectedDirectory = canonicalParent.slice(0, -parentExtension.length);
|
|
719
|
+
const expectedDirectory = canonicalParent.slice(0, -".jsonl".length);
|
|
712
720
|
const canonicalDirectory = await realpath(expectedDirectory).catch(() => undefined);
|
|
713
721
|
if (!canonicalDirectory || canonicalDirectory !== expectedDirectory) {
|
|
714
722
|
throw new Error("OMP child transcript directory is not canonically owned by its parent");
|
|
715
723
|
}
|
|
716
|
-
const
|
|
724
|
+
const requestedFile = resolve(
|
|
725
|
+
childSessionFile ?? join(canonicalDirectory, `${childTranscriptId}.jsonl`),
|
|
726
|
+
);
|
|
717
727
|
let handle: FileHandle;
|
|
718
728
|
try {
|
|
719
729
|
handle = await open(
|
|
720
|
-
|
|
730
|
+
requestedFile,
|
|
721
731
|
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0),
|
|
722
732
|
);
|
|
723
733
|
} catch {
|
|
724
734
|
throw new Error("OMP child transcript could not be opened");
|
|
725
735
|
}
|
|
726
736
|
try {
|
|
727
|
-
const [stat, canonicalChild] = await Promise.all([
|
|
737
|
+
const [stat, pathStat, canonicalChild] = await Promise.all([
|
|
738
|
+
handle.stat(),
|
|
739
|
+
lstat(requestedFile),
|
|
740
|
+
realpath(requestedFile),
|
|
741
|
+
]);
|
|
728
742
|
if (
|
|
729
743
|
!stat.isFile() ||
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
744
|
+
!pathStat.isFile() ||
|
|
745
|
+
pathStat.isSymbolicLink() ||
|
|
746
|
+
stat.dev !== pathStat.dev ||
|
|
747
|
+
stat.ino !== pathStat.ino ||
|
|
748
|
+
stat.size > MAX_CHILD_TRANSCRIPT_BYTES
|
|
733
749
|
) {
|
|
734
750
|
throw new Error("OMP child transcript failed ownership validation");
|
|
735
751
|
}
|
|
736
|
-
const
|
|
737
|
-
|
|
752
|
+
const canonicalRelativeChild = relative(canonicalDirectory, canonicalChild);
|
|
753
|
+
if (
|
|
754
|
+
!canonicalRelativeChild ||
|
|
755
|
+
canonicalRelativeChild.startsWith("..") ||
|
|
756
|
+
isAbsolute(canonicalRelativeChild)
|
|
757
|
+
) {
|
|
758
|
+
throw new Error("OMP child transcript failed ownership validation");
|
|
759
|
+
}
|
|
760
|
+
const bytes = await readStableFile(
|
|
761
|
+
handle,
|
|
762
|
+
stat.size,
|
|
763
|
+
signal,
|
|
764
|
+
"OMP child transcript changed while reading",
|
|
765
|
+
);
|
|
738
766
|
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
739
767
|
const messages: unknown[] = [];
|
|
740
768
|
let nativeSessionId: string | undefined;
|
|
@@ -752,15 +780,21 @@ export async function readOmpPersistedSubagentTranscript(
|
|
|
752
780
|
nativeSessionId ??= candidateId;
|
|
753
781
|
if (nativeSessionId !== candidateId)
|
|
754
782
|
throw new Error("OMP child transcript identity changed");
|
|
755
|
-
} else
|
|
783
|
+
} else {
|
|
784
|
+
const entryId =
|
|
785
|
+
typeof record.id === "string" && CHILD_TRANSCRIPT_ID.test(record.id)
|
|
786
|
+
? record.id
|
|
787
|
+
: undefined;
|
|
788
|
+
const message = persistedTranscriptMessage(record, entryId);
|
|
789
|
+
if (message === undefined) continue;
|
|
756
790
|
if (messages.length >= MAX_CHILD_TRANSCRIPT_MESSAGES) {
|
|
757
791
|
throw new Error("OMP child transcript exceeds message limits");
|
|
758
792
|
}
|
|
759
|
-
messages.push(
|
|
793
|
+
messages.push(message);
|
|
760
794
|
}
|
|
761
795
|
}
|
|
762
796
|
if (!nativeSessionId) throw new Error("OMP child transcript is missing session identity");
|
|
763
|
-
return { sessionFile, nativeSessionId, byteLength: bytes.byteLength, messages };
|
|
797
|
+
return { sessionFile: canonicalChild, nativeSessionId, byteLength: bytes.byteLength, messages };
|
|
764
798
|
} catch (error) {
|
|
765
799
|
if (error instanceof Error) throw error;
|
|
766
800
|
throw new Error("OMP child transcript could not be decoded");
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { OmpRpcEvent } from "./omp-rpc-protocol";
|
|
2
|
+
|
|
3
|
+
export function isNativeTurnActivity(event: OmpRpcEvent): boolean {
|
|
4
|
+
if (
|
|
5
|
+
event.type === "agent_start" ||
|
|
6
|
+
event.type === "turn_start" ||
|
|
7
|
+
event.type === "turn_end" ||
|
|
8
|
+
event.type === "agent_end" ||
|
|
9
|
+
event.type === "auto_compaction_start" ||
|
|
10
|
+
event.type === "auto_compaction_end"
|
|
11
|
+
) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (
|
|
15
|
+
event.type === "message_start" ||
|
|
16
|
+
event.type === "message_update" ||
|
|
17
|
+
event.type === "message_end"
|
|
18
|
+
) {
|
|
19
|
+
return event.message.role === "assistant";
|
|
20
|
+
}
|
|
21
|
+
return event.type.startsWith("tool_execution_");
|
|
22
|
+
}
|
|
23
|
+
export function isRuntimeConfigEvent(event: OmpRpcEvent): boolean {
|
|
24
|
+
return (
|
|
25
|
+
event.type === "model_changed" ||
|
|
26
|
+
event.type === "thinking_level_changed" ||
|
|
27
|
+
event.type === "retry_fallback_applied" ||
|
|
28
|
+
event.type === "retry_fallback_succeeded"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isPassiveUiMethod(method: string): boolean {
|
|
33
|
+
return (
|
|
34
|
+
method === "cancel" ||
|
|
35
|
+
method === "notify" ||
|
|
36
|
+
method === "open_url" ||
|
|
37
|
+
method === "setStatus" ||
|
|
38
|
+
method === "setWidget" ||
|
|
39
|
+
method === "setTitle" ||
|
|
40
|
+
method === "set_editor_text"
|
|
41
|
+
);
|
|
42
|
+
}
|