@omercnet/paseo-omp 5.1.1-next.335.1 → 5.1.1-next.339.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/client/provider-launch-settings.tsx +46 -2
- package/index.server.ts +9 -0
- package/package.json +3 -3
- package/server/mcp-browser.ts +3 -1
- package/server/package-version.ts +1 -1
- package/server/provider/connection.ts +25 -5
- package/server/provider/omp-rpc-protocol.ts +35 -0
- package/server/provider/omp-rpc-transport.ts +5 -2
- package/server/provider/registration.ts +6 -0
- package/server/provider/session-compaction.ts +29 -25
- package/server/provider/session-events.ts +1 -0
- package/server/provider/session-limit.ts +32 -0
- package/server/provider/session-terminal.ts +52 -1
- package/server/provider/session.ts +215 -64
- package/server/provider/timeline-projector.ts +29 -6
- package/shared/provider-launch-settings.ts +4 -0
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
9
9
|
import { Text, View } from "react-native";
|
|
10
10
|
import {
|
|
11
|
+
MAX_SESSIONS_UPPER_BOUND,
|
|
11
12
|
providerLaunchSettings,
|
|
12
13
|
providerLaunchSettingsSchema,
|
|
13
14
|
} from "../shared/provider-launch-settings";
|
|
@@ -23,6 +24,11 @@ function ProviderLaunchSettingsControls({ settings }: { settings: ReadySettings
|
|
|
23
24
|
useEffect(() => {
|
|
24
25
|
setRawNames(savedNames);
|
|
25
26
|
}, [savedNames]);
|
|
27
|
+
const savedMaxSessions = String(settings.values.maxSessions);
|
|
28
|
+
const [rawMaxSessions, setRawMaxSessions] = useState(() => savedMaxSessions);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setRawMaxSessions(savedMaxSessions);
|
|
31
|
+
}, [savedMaxSessions]);
|
|
26
32
|
const inheritEnv = [
|
|
27
33
|
...new Set(
|
|
28
34
|
rawNames
|
|
@@ -31,7 +37,7 @@ function ProviderLaunchSettingsControls({ settings }: { settings: ReadySettings
|
|
|
31
37
|
.filter(Boolean),
|
|
32
38
|
),
|
|
33
39
|
];
|
|
34
|
-
const next = providerLaunchSettingsSchema.safeParse({ inheritEnv });
|
|
40
|
+
const next = providerLaunchSettingsSchema.safeParse({ ...settings.values, inheritEnv });
|
|
35
41
|
const error = next.success
|
|
36
42
|
? null
|
|
37
43
|
: (next.error.issues[0]?.message ?? "Enter valid variable names.");
|
|
@@ -39,6 +45,14 @@ function ProviderLaunchSettingsControls({ settings }: { settings: ReadySettings
|
|
|
39
45
|
if (!next.success) return;
|
|
40
46
|
await settings.save(next.data, settings.revision);
|
|
41
47
|
}, [next, settings]);
|
|
48
|
+
const nextMaxSessions = providerLaunchSettingsSchema.safeParse({
|
|
49
|
+
...settings.values,
|
|
50
|
+
maxSessions: Number(rawMaxSessions.trim()),
|
|
51
|
+
});
|
|
52
|
+
const saveMaxSessions = useCallback(async () => {
|
|
53
|
+
if (!nextMaxSessions.success) return;
|
|
54
|
+
await settings.save(nextMaxSessions.data, settings.revision);
|
|
55
|
+
}, [nextMaxSessions, settings]);
|
|
42
56
|
|
|
43
57
|
return (
|
|
44
58
|
<View style={{ gap: 12 }}>
|
|
@@ -69,7 +83,9 @@ function ProviderLaunchSettingsControls({ settings }: { settings: ReadySettings
|
|
|
69
83
|
actionLabel="Clear"
|
|
70
84
|
disabled={settings.saving || settings.values.inheritEnv.length === 0}
|
|
71
85
|
onPress={async () => {
|
|
72
|
-
if (await settings.save({ inheritEnv: [] }, settings.revision))
|
|
86
|
+
if (await settings.save({ ...settings.values, inheritEnv: [] }, settings.revision)) {
|
|
87
|
+
setRawNames("");
|
|
88
|
+
}
|
|
73
89
|
}}
|
|
74
90
|
/>
|
|
75
91
|
{settings.saveError ? (
|
|
@@ -83,6 +99,34 @@ function ProviderLaunchSettingsControls({ settings }: { settings: ReadySettings
|
|
|
83
99
|
) : null}
|
|
84
100
|
</SettingsCard>
|
|
85
101
|
</SettingsSection>
|
|
102
|
+
<SettingsSection
|
|
103
|
+
title="Session limit"
|
|
104
|
+
info="Applies to all OMP Plugin agents on this host together, including idle ones."
|
|
105
|
+
>
|
|
106
|
+
<SettingsCard>
|
|
107
|
+
<SettingsInput
|
|
108
|
+
key={savedMaxSessions}
|
|
109
|
+
label="Open sessions"
|
|
110
|
+
hint="New OMP Plugin agents are refused while this many sessions are open. Archive idle agents to free sessions."
|
|
111
|
+
initialValue={savedMaxSessions}
|
|
112
|
+
onChangeText={setRawMaxSessions}
|
|
113
|
+
error={
|
|
114
|
+
nextMaxSessions.success
|
|
115
|
+
? undefined
|
|
116
|
+
: `Enter a whole number from 1 to ${MAX_SESSIONS_UPPER_BOUND}.`
|
|
117
|
+
}
|
|
118
|
+
disabled={settings.saving}
|
|
119
|
+
/>
|
|
120
|
+
<SettingsAction
|
|
121
|
+
label="Save session limit"
|
|
122
|
+
actionLabel="Save"
|
|
123
|
+
disabled={
|
|
124
|
+
settings.saving || rawMaxSessions === savedMaxSessions || !nextMaxSessions.success
|
|
125
|
+
}
|
|
126
|
+
onPress={() => void saveMaxSessions()}
|
|
127
|
+
/>
|
|
128
|
+
</SettingsCard>
|
|
129
|
+
</SettingsSection>
|
|
86
130
|
</View>
|
|
87
131
|
);
|
|
88
132
|
}
|
package/index.server.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "./server/provider/profile-providers";
|
|
26
26
|
import { createOmpProvider } from "./server/provider/registration";
|
|
27
27
|
import { OmpPublicError } from "./server/provider/security";
|
|
28
|
+
import { OmpSessionLimit } from "./server/provider/session-limit";
|
|
28
29
|
import { resolveGetOmpProviderHealth } from "./server/provider-diagnostics";
|
|
29
30
|
import { resolveListOmpQuotas } from "./server/quota";
|
|
30
31
|
import { resolveListOmpSessions } from "./server/sessions";
|
|
@@ -62,6 +63,11 @@ export default function contribute(server: PluginServerContext) {
|
|
|
62
63
|
throw new OmpPublicError("OMP host launch settings are unavailable");
|
|
63
64
|
};
|
|
64
65
|
const browserAuthorizationRegistry = new OmpBrowserAuthorizationRegistry();
|
|
66
|
+
const sessionLimit = new OmpSessionLimit(async () => {
|
|
67
|
+
const current = await launchSettings.read();
|
|
68
|
+
return current.status === "ready" ? current.values.maxSessions : undefined;
|
|
69
|
+
});
|
|
70
|
+
const removeSessionLimitListener = launchSettings.subscribe(() => sessionLimit.refresh());
|
|
65
71
|
const protocolViolations = new OmpProtocolViolationCollector();
|
|
66
72
|
const operationalFailures = new OmpOperationalFailureCollector();
|
|
67
73
|
const profiles = discoverOmpProfilesSync();
|
|
@@ -70,6 +76,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
70
76
|
server.registerProvider(
|
|
71
77
|
createProfileOmpProvider(profile, {
|
|
72
78
|
browserAuthorizationRegistry,
|
|
79
|
+
sessionLimit,
|
|
73
80
|
reportProtocolViolation: protocolViolations.report,
|
|
74
81
|
reportOperationalFailure: operationalFailures.report,
|
|
75
82
|
resolveHostInheritEnv,
|
|
@@ -104,6 +111,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
104
111
|
server.registerProvider(
|
|
105
112
|
createOmpProvider({
|
|
106
113
|
browserAuthorizationRegistry,
|
|
114
|
+
sessionLimit,
|
|
107
115
|
reportProtocolViolation: protocolViolations.report,
|
|
108
116
|
reportOperationalFailure: operationalFailures.report,
|
|
109
117
|
resolveHostInheritEnv,
|
|
@@ -111,6 +119,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
111
119
|
);
|
|
112
120
|
return () => {
|
|
113
121
|
browserAuthorizationRegistry.clear();
|
|
122
|
+
removeSessionLimitListener();
|
|
114
123
|
removeSessionOpenHook();
|
|
115
124
|
};
|
|
116
125
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omercnet/paseo-omp",
|
|
3
|
-
"version": "5.1.1-next.
|
|
3
|
+
"version": "5.1.1-next.339.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
"@getpaseo/client": "0.14.0",
|
|
65
65
|
"@getpaseo/plugin": "0.14.0",
|
|
66
66
|
"@getpaseo/server": "0.14.0",
|
|
67
|
-
"@oh-my-pi/pi-agent-core": "18.
|
|
68
|
-
"@oh-my-pi/pi-coding-agent": "18.
|
|
67
|
+
"@oh-my-pi/pi-agent-core": "18.14.0",
|
|
68
|
+
"@oh-my-pi/pi-coding-agent": "18.14.0",
|
|
69
69
|
"@tanstack/react-query": "^5.102.3",
|
|
70
70
|
"@types/node": "^24.10.1",
|
|
71
71
|
"@types/react": "^19.2.18",
|
package/server/mcp-browser.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { RpcInput } from "@getpaseo/plugin";
|
|
3
3
|
import type { openOmpMcpAuthorizationInPaseoBrowser } from "../shared/mcp";
|
|
4
|
+
import { MAX_SESSIONS_UPPER_BOUND } from "../shared/provider-launch-settings";
|
|
4
5
|
import { OmpPublicError } from "./provider/security";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
// One registration per live session. The shared session limit refuses sessions first.
|
|
8
|
+
const MAX_REGISTERED_SESSIONS = MAX_SESSIONS_UPPER_BOUND;
|
|
7
9
|
const MAX_AUTHORIZATIONS_PER_SESSION = 16;
|
|
8
10
|
|
|
9
11
|
type BrowserAuthorizationOpener = (url: string) => Promise<void>;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Release Please keeps this bundle-safe constant aligned with package.json. */
|
|
2
|
-
export const PASEO_OMP_PACKAGE_VERSION = "5.1.1-next.
|
|
2
|
+
export const PASEO_OMP_PACKAGE_VERSION = "5.1.1-next.339.1"; // x-release-please-version
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "./security";
|
|
35
35
|
import { type OmpOpenedSession, OmpProviderSession, ompPersistenceSessionId } from "./session";
|
|
36
36
|
import { OmpHistorySession } from "./session-history";
|
|
37
|
+
import { OmpSessionLimit } from "./session-limit";
|
|
37
38
|
import { isOmpHistoryOnlyOpen } from "./session-purpose";
|
|
38
39
|
|
|
39
40
|
type ProviderConfigurationCompat = {
|
|
@@ -69,7 +70,6 @@ const SUPPORTED_INPUTS: Readonly<Record<string, true>> = {
|
|
|
69
70
|
"session.can_stop_runtime": true,
|
|
70
71
|
"session.close": true,
|
|
71
72
|
};
|
|
72
|
-
const MAX_CONNECTION_SESSIONS = 32;
|
|
73
73
|
const MAX_ACTIVE_OPERATIONS = 128;
|
|
74
74
|
const MAX_PROVIDER_INPUT_BYTES = 2 * 1024 * 1024;
|
|
75
75
|
const MAX_NESTED_OPTION_BYTES = 256 * 1024;
|
|
@@ -723,6 +723,7 @@ export function createOmpConnection(
|
|
|
723
723
|
console.error("OMP provider failure", diagnostic),
|
|
724
724
|
reportOperationalFailure: OmpOperationalFailureReporter = () => {},
|
|
725
725
|
resolveHostInheritEnv?: () => Promise<readonly string[]>,
|
|
726
|
+
sessionLimit = new OmpSessionLimit(),
|
|
726
727
|
): ProviderConnection {
|
|
727
728
|
const errorDetails = (error: unknown, fallback: string): { message: string } => {
|
|
728
729
|
if (isOmpPublicError(error)) return { message: error.message };
|
|
@@ -767,6 +768,13 @@ export function createOmpConnection(
|
|
|
767
768
|
string,
|
|
768
769
|
{ token: symbol; nativeSessionId?: string; cleanup?: Promise<void> }
|
|
769
770
|
>();
|
|
771
|
+
// One session id is held by `sessions`, `opening` and `failedCleanup` at different moments, and
|
|
772
|
+
// by two of them at once while it opens or fails. After a failed close only unresolved cleanup
|
|
773
|
+
// stays counted, until it drains.
|
|
774
|
+
const untrackSessions = sessionLimit.track(() => {
|
|
775
|
+
if (closed) return failedCleanup.size;
|
|
776
|
+
return new Set([...sessions.keys(), ...opening.keys(), ...failedCleanup.keys()]).size;
|
|
777
|
+
});
|
|
770
778
|
const shutdown = new AbortController();
|
|
771
779
|
let catalogCleanup: Promise<void> | null = null;
|
|
772
780
|
const activeOperations = new Set<Promise<void>>();
|
|
@@ -809,7 +817,9 @@ export function createOmpConnection(
|
|
|
809
817
|
...(quarantinedNativeSessionId ? { nativeSessionId: quarantinedNativeSessionId } : {}),
|
|
810
818
|
});
|
|
811
819
|
nativeReservations.quarantine(quarantinedNativeSessionId, token, cleanup, () => {
|
|
812
|
-
if (failedCleanup.get(sessionId)?.token
|
|
820
|
+
if (failedCleanup.get(sessionId)?.token !== token) return;
|
|
821
|
+
failedCleanup.delete(sessionId);
|
|
822
|
+
if (closed && failedCleanup.size === 0) untrackSessions();
|
|
813
823
|
});
|
|
814
824
|
};
|
|
815
825
|
|
|
@@ -906,8 +916,13 @@ export function createOmpConnection(
|
|
|
906
916
|
requestFailure(input.requestId, new OmpPublicError("OMP session already exists"));
|
|
907
917
|
return;
|
|
908
918
|
}
|
|
909
|
-
if (
|
|
910
|
-
requestFailure(
|
|
919
|
+
if (sessionLimit.openSessions >= sessionLimit.maxSessions) {
|
|
920
|
+
requestFailure(
|
|
921
|
+
input.requestId,
|
|
922
|
+
new OmpPublicError(
|
|
923
|
+
`OMP session limit reached (${sessionLimit.maxSessions} open sessions). Archive idle agents or raise the session limit in OMP → Plugin.`,
|
|
924
|
+
),
|
|
925
|
+
);
|
|
911
926
|
return;
|
|
912
927
|
}
|
|
913
928
|
const token = Symbol(input.sessionId);
|
|
@@ -1313,7 +1328,12 @@ export function createOmpConnection(
|
|
|
1313
1328
|
return () => listeners.delete(listener);
|
|
1314
1329
|
},
|
|
1315
1330
|
close() {
|
|
1316
|
-
closePromise ??= Promise.resolve()
|
|
1331
|
+
closePromise ??= Promise.resolve()
|
|
1332
|
+
.then(disposeConnection)
|
|
1333
|
+
.then(untrackSessions, (error: unknown) => {
|
|
1334
|
+
if (failedCleanup.size === 0) untrackSessions();
|
|
1335
|
+
throw error;
|
|
1336
|
+
});
|
|
1317
1337
|
return closePromise;
|
|
1318
1338
|
},
|
|
1319
1339
|
};
|
|
@@ -71,6 +71,8 @@ export const OMP_SESSION_EVENT_POLICIES = {
|
|
|
71
71
|
tool_execution_end: "normalize",
|
|
72
72
|
auto_compaction_start: "normalize",
|
|
73
73
|
auto_compaction_end: "normalize",
|
|
74
|
+
manual_compaction_start: "normalize",
|
|
75
|
+
manual_compaction_end: "normalize",
|
|
74
76
|
auto_retry_start: "normalize",
|
|
75
77
|
auto_retry_end: "normalize",
|
|
76
78
|
retry_fallback_applied: "normalize",
|
|
@@ -148,9 +150,15 @@ export const OMP_RPC_COMMAND_POLICIES = {
|
|
|
148
150
|
get_logout_accounts: "unsupported",
|
|
149
151
|
goal: "unsupported",
|
|
150
152
|
live_mute: "unsupported",
|
|
153
|
+
live_connect: "unsupported",
|
|
151
154
|
live_start: "unsupported",
|
|
152
155
|
live_stop: "unsupported",
|
|
153
156
|
logout: "unsupported",
|
|
157
|
+
navigate_tree: "unsupported",
|
|
158
|
+
abort_branch_summary: "unsupported",
|
|
159
|
+
set_label: "unsupported",
|
|
160
|
+
dictation_start: "unsupported",
|
|
161
|
+
dictation_stop: "unsupported",
|
|
154
162
|
predict_word: "unsupported",
|
|
155
163
|
predict_word_feedback: "unsupported",
|
|
156
164
|
promote_queued_message: "unsupported",
|
|
@@ -254,9 +262,14 @@ export const OMP_PROTOCOL_EVENT_TYPES = [
|
|
|
254
262
|
"todo_auto_clear",
|
|
255
263
|
"auto_compaction_start",
|
|
256
264
|
"auto_compaction_end",
|
|
265
|
+
"manual_compaction_start",
|
|
266
|
+
"manual_compaction_end",
|
|
257
267
|
"available_commands_update",
|
|
258
268
|
"notice",
|
|
259
269
|
"command_output",
|
|
270
|
+
"extension_error",
|
|
271
|
+
"session_info_update",
|
|
272
|
+
"config_update",
|
|
260
273
|
"extension_ui_request",
|
|
261
274
|
"prompt_result",
|
|
262
275
|
"host_tool_call",
|
|
@@ -403,6 +416,8 @@ function eventSchemaClassification(
|
|
|
403
416
|
case "cache_warming_start":
|
|
404
417
|
case "cache_warming_end":
|
|
405
418
|
case "queue_update":
|
|
419
|
+
case "session_info_update":
|
|
420
|
+
case "config_update":
|
|
406
421
|
return { reason: "configuration-event-schema", expected: "valid-configuration-event" };
|
|
407
422
|
case "extension_ui_request":
|
|
408
423
|
return { reason: "extension-ui-schema", expected: "valid-extension-ui-event" };
|
|
@@ -1606,6 +1621,17 @@ const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
|
|
|
1606
1621
|
errorMessage: boundedString(64 * 1024).optional(),
|
|
1607
1622
|
skipped: z.boolean().optional(),
|
|
1608
1623
|
}),
|
|
1624
|
+
z.object({
|
|
1625
|
+
type: z.literal("manual_compaction_start"),
|
|
1626
|
+
mode: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES).optional(),
|
|
1627
|
+
}),
|
|
1628
|
+
z.object({
|
|
1629
|
+
type: z.literal("manual_compaction_end"),
|
|
1630
|
+
result: OmpCompactionResultSchema.optional(),
|
|
1631
|
+
aborted: z.boolean(),
|
|
1632
|
+
skipped: z.boolean().optional(),
|
|
1633
|
+
errorMessage: boundedString(64 * 1024).optional(),
|
|
1634
|
+
}),
|
|
1609
1635
|
z.object({
|
|
1610
1636
|
type: z.literal("notice"),
|
|
1611
1637
|
id: IDENTIFIER.optional(),
|
|
@@ -1799,6 +1825,14 @@ export const OmpRuntimeEventSchema = z.discriminatedUnion("type", [
|
|
|
1799
1825
|
commands: z.array(OmpAvailableCommandSchema).max(MAX_ARRAY_ITEMS),
|
|
1800
1826
|
}),
|
|
1801
1827
|
z.object({ type: z.literal("command_output"), text: TEXT.optional() }),
|
|
1828
|
+
z.object({
|
|
1829
|
+
type: z.literal("extension_error"),
|
|
1830
|
+
extensionPath: boundedString(MAX_PATH_LENGTH),
|
|
1831
|
+
event: NAME,
|
|
1832
|
+
error: boundedString(64 * 1024),
|
|
1833
|
+
}),
|
|
1834
|
+
z.object({ type: z.literal("session_info_update") }),
|
|
1835
|
+
z.object({ type: z.literal("config_update") }),
|
|
1802
1836
|
OmpExtensionUiRequestSchema,
|
|
1803
1837
|
z.object({
|
|
1804
1838
|
type: z.literal("prompt_result"),
|
|
@@ -1997,6 +2031,7 @@ export function parseOmpHostToolAgentResult(value: unknown): OmpHostToolResult["
|
|
|
1997
2031
|
export type OmpRpcEvent =
|
|
1998
2032
|
| z.infer<typeof OmpRuntimeEventSchema>
|
|
1999
2033
|
| { type: "prompt_error"; id: string; error: string; code?: string }
|
|
2034
|
+
| { type: "session_settled" }
|
|
2000
2035
|
| { type: "process_exit"; error: string };
|
|
2001
2036
|
export type OmpAgentSessionEvent = z.infer<typeof OmpAgentSessionEventSchema>;
|
|
2002
2037
|
export type OmpSubagentSnapshot = z.infer<typeof OmpSubagentsResultSchema>["subagents"][number];
|
|
@@ -1090,8 +1090,11 @@ export class OmpRpcProcess {
|
|
|
1090
1090
|
});
|
|
1091
1091
|
return;
|
|
1092
1092
|
}
|
|
1093
|
-
// Emitted
|
|
1094
|
-
if (type === "session_settled")
|
|
1093
|
+
// Emitted when the session is quiet: nothing live, queued, or pending in the background.
|
|
1094
|
+
if (type === "session_settled") {
|
|
1095
|
+
this.emit({ type: "session_settled" });
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1095
1098
|
if (type === "ready") {
|
|
1096
1099
|
if (this.readyReceived) {
|
|
1097
1100
|
this.recordProtocolViolation("duplicate-ready", {
|
|
@@ -12,6 +12,7 @@ import { OmpRpcRuntime, type OmpRuntime } from "./omp-rpc";
|
|
|
12
12
|
import type { OmpProtocolViolationDiagnostic } from "./omp-rpc-protocol";
|
|
13
13
|
import { parseOmpProviderOptions } from "./provider-options";
|
|
14
14
|
import { boundedJsonBytes } from "./security";
|
|
15
|
+
import { OmpSessionLimit } from "./session-limit";
|
|
15
16
|
import { OmpProviderOptionsSchema } from "./settings";
|
|
16
17
|
import type { OmpTimelineScheduler } from "./timeline-projector";
|
|
17
18
|
|
|
@@ -121,6 +122,8 @@ export interface OmpProviderOptions {
|
|
|
121
122
|
mcpInitializationTimeoutMs?: number;
|
|
122
123
|
mcpConnector?: OmpMcpConnector;
|
|
123
124
|
browserAuthorizationRegistry?: OmpBrowserAuthorizationRegistry;
|
|
125
|
+
/** Shared by every provider of one plugin instance; each provider has its own otherwise. */
|
|
126
|
+
sessionLimit?: OmpSessionLimit;
|
|
124
127
|
reportProtocolViolation?: (diagnostic: OmpProtocolViolationDiagnostic) => void | Promise<void>;
|
|
125
128
|
reportOperationalFailure?: OmpOperationalFailureReporter;
|
|
126
129
|
availabilityProbe?: (
|
|
@@ -150,6 +153,7 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
|
|
|
150
153
|
reportProtocolViolation: options.reportProtocolViolation,
|
|
151
154
|
});
|
|
152
155
|
const nativeReservations = new OmpNativeSessionReservations();
|
|
156
|
+
const sessionLimit = options.sessionLimit ?? new OmpSessionLimit();
|
|
153
157
|
const checkAvailability: NonNullable<ProviderRegistrationCompat["checkAvailability"]> = async (
|
|
154
158
|
catalogOptions,
|
|
155
159
|
context,
|
|
@@ -216,6 +220,7 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
|
|
|
216
220
|
const capabilities = CAPABILITIES.filter((capability) =>
|
|
217
221
|
requestedCapabilities.has(capability),
|
|
218
222
|
);
|
|
223
|
+
await sessionLimit.refresh();
|
|
219
224
|
return createOmpConnection(
|
|
220
225
|
runtime,
|
|
221
226
|
capabilities,
|
|
@@ -229,6 +234,7 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
|
|
|
229
234
|
undefined,
|
|
230
235
|
options.reportOperationalFailure,
|
|
231
236
|
options.resolveHostInheritEnv,
|
|
237
|
+
sessionLimit,
|
|
232
238
|
);
|
|
233
239
|
},
|
|
234
240
|
};
|
|
@@ -7,6 +7,7 @@ import type { OmpSessionUsage } from "./session-usage";
|
|
|
7
7
|
import type { OmpTimelineProjector } from "./timeline-projector";
|
|
8
8
|
|
|
9
9
|
type Emit = (event: ProviderEvent) => void;
|
|
10
|
+
export type CompactionOutcome = "completed" | "failed" | "canceled" | "skipped";
|
|
10
11
|
|
|
11
12
|
export interface ActiveCompaction {
|
|
12
13
|
id: string;
|
|
@@ -73,51 +74,54 @@ export class OmpSessionCompaction {
|
|
|
73
74
|
});
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
handleEnd(
|
|
77
78
|
turn: ActiveTurn,
|
|
78
|
-
event: Extract<OmpRpcEvent, { type: "auto_compaction_end" }>,
|
|
79
|
-
):
|
|
79
|
+
event: Extract<OmpRpcEvent, { type: "auto_compaction_end" | "manual_compaction_end" }>,
|
|
80
|
+
): CompactionOutcome | undefined {
|
|
80
81
|
if (this.discardedEnds > 0) {
|
|
81
82
|
this.discardedEnds -= 1;
|
|
82
|
-
return
|
|
83
|
+
return undefined;
|
|
83
84
|
}
|
|
84
85
|
const operation = this.operation;
|
|
86
|
+
const trigger = event.type === "auto_compaction_end" ? "auto" : "manual";
|
|
85
87
|
if (
|
|
86
|
-
operation?.trigger !==
|
|
88
|
+
operation?.trigger !== trigger ||
|
|
87
89
|
operation.turnId !== turn.turnId ||
|
|
88
90
|
operation.generation !== turn.generation
|
|
89
91
|
) {
|
|
90
|
-
return
|
|
92
|
+
return undefined;
|
|
91
93
|
}
|
|
92
|
-
if (event.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
operation.retrying
|
|
103
|
-
|
|
94
|
+
if (event.type === "auto_compaction_end") {
|
|
95
|
+
if (event.action !== undefined && operation.action !== event.action) {
|
|
96
|
+
this.retire("OMP emitted overlapping compactions");
|
|
97
|
+
this.discardedEnds = 1;
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
if (event.willRetry) {
|
|
101
|
+
operation.retrying = true;
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
if (operation.retrying) {
|
|
105
|
+
operation.retrying = false;
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
104
108
|
}
|
|
105
|
-
const state = event.aborted
|
|
109
|
+
const state: CompactionOutcome = event.aborted
|
|
106
110
|
? "canceled"
|
|
107
|
-
: event.
|
|
108
|
-
? "
|
|
109
|
-
: event.
|
|
110
|
-
? "
|
|
111
|
+
: event.skipped
|
|
112
|
+
? "skipped"
|
|
113
|
+
: event.errorMessage
|
|
114
|
+
? "failed"
|
|
111
115
|
: "completed";
|
|
112
116
|
this.finish(state, {
|
|
113
117
|
tokensBefore: event.result?.tokensBefore ?? event.result?.preTokens,
|
|
114
118
|
message: event.errorMessage,
|
|
115
119
|
});
|
|
116
|
-
return
|
|
120
|
+
return state;
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
finish(
|
|
120
|
-
state:
|
|
124
|
+
state: CompactionOutcome,
|
|
121
125
|
options: { tokensBefore?: number | null; message?: string } = {},
|
|
122
126
|
): void {
|
|
123
127
|
const operation = this.operation;
|
|
@@ -24,6 +24,7 @@ export function isRuntimeConfigEvent(event: OmpRpcEvent): boolean {
|
|
|
24
24
|
return (
|
|
25
25
|
event.type === "model_changed" ||
|
|
26
26
|
event.type === "thinking_level_changed" ||
|
|
27
|
+
event.type === "config_update" ||
|
|
27
28
|
event.type === "retry_fallback_applied" ||
|
|
28
29
|
event.type === "retry_fallback_succeeded"
|
|
29
30
|
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DEFAULT_MAX_SESSIONS } from "../../shared/provider-launch-settings";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Counts open sessions across every provider connection that shares this instance. The plugin
|
|
5
|
+
* shares one instance between all of its OMP providers, so the limit applies to the whole host.
|
|
6
|
+
*/
|
|
7
|
+
export class OmpSessionLimit {
|
|
8
|
+
private readonly counts = new Set<() => number>();
|
|
9
|
+
maxSessions = DEFAULT_MAX_SESSIONS;
|
|
10
|
+
|
|
11
|
+
/** `readConfigured` returns the configured limit, or `undefined` to keep the current one. */
|
|
12
|
+
constructor(private readonly readConfigured?: () => Promise<number | undefined>) {}
|
|
13
|
+
|
|
14
|
+
async refresh(): Promise<void> {
|
|
15
|
+
const configured = await this.readConfigured?.();
|
|
16
|
+
if (configured !== undefined) this.maxSessions = configured;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Adds a connection's open sessions to the total until the returned function runs. */
|
|
20
|
+
track(count: () => number): () => void {
|
|
21
|
+
this.counts.add(count);
|
|
22
|
+
return () => {
|
|
23
|
+
this.counts.delete(count);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get openSessions(): number {
|
|
28
|
+
let total = 0;
|
|
29
|
+
for (const count of this.counts) total += count();
|
|
30
|
+
return total;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { ProviderError } from "@getpaseo/plugin/server/provider";
|
|
3
3
|
import type { OmpMessage, OmpRpcEvent } from "./omp-rpc-protocol";
|
|
4
|
+
import type { CompactionOutcome } from "./session-compaction";
|
|
4
5
|
|
|
5
6
|
export type PendingUser = {
|
|
6
7
|
clientMessageId: string;
|
|
@@ -12,7 +13,7 @@ export type PendingUser = {
|
|
|
12
13
|
type TerminalCorrelationEvidence = "fresh-native-user" | "current-assistant";
|
|
13
14
|
|
|
14
15
|
type TerminalCorrelation = {
|
|
15
|
-
policy: "initial-turn" | "ordered-legacy" | "native-command";
|
|
16
|
+
policy: "initial-turn" | "ordered-legacy" | "native-command" | "native-run";
|
|
16
17
|
evidence: Map<TerminalCorrelationEvidence, number>;
|
|
17
18
|
};
|
|
18
19
|
|
|
@@ -24,6 +25,7 @@ export type TerminalCandidate = {
|
|
|
24
25
|
| "initial-turn"
|
|
25
26
|
| "ordered-legacy"
|
|
26
27
|
| "native-command"
|
|
28
|
+
| "native-run"
|
|
27
29
|
| "interrupted"
|
|
28
30
|
| "ambiguous";
|
|
29
31
|
};
|
|
@@ -55,6 +57,7 @@ export type ActiveTurn = {
|
|
|
55
57
|
usageSampleFloor: number;
|
|
56
58
|
manualCompaction: boolean;
|
|
57
59
|
manualCompactionPending: boolean;
|
|
60
|
+
manualCompactionOutcome?: CompactionOutcome;
|
|
58
61
|
manualCompactionDeadlineTimer?: unknown;
|
|
59
62
|
agentEndPending: boolean;
|
|
60
63
|
agentEndRetryTimer?: unknown;
|
|
@@ -64,11 +67,14 @@ export type ActiveTurn = {
|
|
|
64
67
|
terminalizing: boolean;
|
|
65
68
|
terminalization?: Promise<void>;
|
|
66
69
|
terminalOutcome?: TurnOutcome;
|
|
70
|
+
/** Listeners may hold this turn's terminal state from here on, even if publishing threw. */
|
|
71
|
+
terminalEmitAttempted: boolean;
|
|
67
72
|
operationalTerminalStage?: "failed" | "unresolved";
|
|
68
73
|
terminalWake?: VoidDeferred;
|
|
69
74
|
steerReady: VoidDeferred;
|
|
70
75
|
steersInFlight: number;
|
|
71
76
|
deferredAgentEnd?: TerminalCandidate;
|
|
77
|
+
heldAgentEnd?: HeldAgentEnd;
|
|
72
78
|
activeToolCallIds: Set<string>;
|
|
73
79
|
bufferedEvents: OmpRpcEvent[];
|
|
74
80
|
pendingUsers: PendingUser[];
|
|
@@ -82,6 +88,38 @@ export type ActiveTurn = {
|
|
|
82
88
|
lastCompletedAssistantEntryId?: string;
|
|
83
89
|
};
|
|
84
90
|
|
|
91
|
+
type RunEvidence = Pick<
|
|
92
|
+
ActiveTurn,
|
|
93
|
+
| "completedMessageCount"
|
|
94
|
+
| "streamedMessageEntryIds"
|
|
95
|
+
| "streamedMessageIdentityComplete"
|
|
96
|
+
| "lastCompletedAssistantOutcome"
|
|
97
|
+
| "lastCompletedAssistantEntryId"
|
|
98
|
+
>;
|
|
99
|
+
|
|
100
|
+
/** A run that ended with `isTerminal: false`, kept until another run starts or OMP settles. */
|
|
101
|
+
export type HeldAgentEnd = {
|
|
102
|
+
event: Extract<OmpRpcEvent, { type: "agent_end" }>;
|
|
103
|
+
arrivalSequence: number;
|
|
104
|
+
evidence: RunEvidence;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Hands over the evidence of the run that just ended and starts the next run's evidence empty. */
|
|
108
|
+
export function takeRunEvidence(turn: ActiveTurn): RunEvidence {
|
|
109
|
+
const evidence: RunEvidence = {
|
|
110
|
+
completedMessageCount: turn.completedMessageCount,
|
|
111
|
+
streamedMessageEntryIds: turn.streamedMessageEntryIds.splice(0),
|
|
112
|
+
streamedMessageIdentityComplete: turn.streamedMessageIdentityComplete,
|
|
113
|
+
lastCompletedAssistantOutcome: turn.lastCompletedAssistantOutcome,
|
|
114
|
+
lastCompletedAssistantEntryId: turn.lastCompletedAssistantEntryId,
|
|
115
|
+
};
|
|
116
|
+
turn.completedMessageCount = 0;
|
|
117
|
+
turn.streamedMessageIdentityComplete = true;
|
|
118
|
+
turn.lastCompletedAssistantOutcome = undefined;
|
|
119
|
+
turn.lastCompletedAssistantEntryId = undefined;
|
|
120
|
+
return evidence;
|
|
121
|
+
}
|
|
122
|
+
|
|
85
123
|
export type VoidDeferred = {
|
|
86
124
|
promise: Promise<void>;
|
|
87
125
|
resolve(value?: void | PromiseLike<void>): void;
|
|
@@ -249,6 +287,7 @@ export function createActiveTurn(
|
|
|
249
287
|
usageSampleFloor: 0,
|
|
250
288
|
agentEndPending: false,
|
|
251
289
|
terminalizing: false,
|
|
290
|
+
terminalEmitAttempted: false,
|
|
252
291
|
manualCompactionPending: manualCompaction,
|
|
253
292
|
manualCompaction,
|
|
254
293
|
activitySequence: 0,
|
|
@@ -276,6 +315,15 @@ export function createActiveTurn(
|
|
|
276
315
|
],
|
|
277
316
|
};
|
|
278
317
|
}
|
|
318
|
+
/** A run OMP started without a client prompt: an extension, goal, or background wake. */
|
|
319
|
+
export function createNativeRunTurn(generation: number): ActiveTurn {
|
|
320
|
+
return {
|
|
321
|
+
...createActiveTurn("", "", generation, "native-run"),
|
|
322
|
+
promptResultEmitted: true,
|
|
323
|
+
pendingUsers: [],
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
279
327
|
export function classifyTerminalCandidate(
|
|
280
328
|
turn: ActiveTurn,
|
|
281
329
|
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
@@ -286,6 +334,9 @@ export function classifyTerminalCandidate(
|
|
|
286
334
|
if (turn.terminalCorrelation.policy === "initial-turn") {
|
|
287
335
|
return { event, confidence: "initial-turn", arrivalSequence };
|
|
288
336
|
}
|
|
337
|
+
if (turn.terminalCorrelation.policy === "native-run") {
|
|
338
|
+
return { event, confidence: "native-run", arrivalSequence };
|
|
339
|
+
}
|
|
289
340
|
const assistantSequence = turn.terminalCorrelation.evidence.get("current-assistant");
|
|
290
341
|
if (
|
|
291
342
|
turn.terminalCorrelation.policy === "native-command" &&
|
|
@@ -73,10 +73,13 @@ import {
|
|
|
73
73
|
assistantTerminalOutcome,
|
|
74
74
|
classifyTerminalCandidate,
|
|
75
75
|
createActiveTurn,
|
|
76
|
+
createNativeRunTurn,
|
|
77
|
+
type HeldAgentEnd,
|
|
76
78
|
historyTerminalOutcome,
|
|
77
79
|
nativeEntryId,
|
|
78
80
|
type PendingUser,
|
|
79
81
|
type TerminalCandidate,
|
|
82
|
+
takeRunEvidence,
|
|
80
83
|
terminalOutcome,
|
|
81
84
|
unknownTerminalOutcomeError,
|
|
82
85
|
} from "./session-terminal";
|
|
@@ -330,6 +333,8 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
330
333
|
private configRevision = 0;
|
|
331
334
|
private recoveryUsesNativeConfig = false;
|
|
332
335
|
private activeAbort: PendingAbort | null = null;
|
|
336
|
+
/** Frames of a run that started while the previous terminal was being confirmed or published. */
|
|
337
|
+
private runStartBacklog: OmpRpcEvent[] | null = null;
|
|
333
338
|
private readonly usage: OmpSessionUsage;
|
|
334
339
|
private readonly compaction: OmpSessionCompaction;
|
|
335
340
|
private revertInFlight = false;
|
|
@@ -1458,33 +1463,46 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
1458
1463
|
} catch (error) {
|
|
1459
1464
|
if (this.closed || this.runtimeDead || turn.generation !== this.generation) return;
|
|
1460
1465
|
turn.manualCompactionPending = false;
|
|
1466
|
+
// OMP 18.13.3+ reports the outcome in manual_compaction_end before rejecting `compact`.
|
|
1467
|
+
const outcome = turn.manualCompactionOutcome;
|
|
1468
|
+
if (outcome === "skipped") {
|
|
1469
|
+
await this.finishTurn(turn, turn.interrupted ? "canceled" : "completed");
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1461
1472
|
const raw = providerError(error, "OMP compaction failed");
|
|
1462
1473
|
const failure = { message: this.dataFilter.text(raw.message, 4_096) };
|
|
1463
|
-
const state = turn.interrupted ? "canceled" : "failed";
|
|
1474
|
+
const state = turn.interrupted || outcome === "canceled" ? "canceled" : "failed";
|
|
1464
1475
|
this.compaction.finish(state, { message: failure.message });
|
|
1465
1476
|
await this.finishTurn(turn, state, state === "failed" ? failure : undefined, true, true);
|
|
1466
1477
|
}
|
|
1467
1478
|
}
|
|
1468
1479
|
|
|
1469
1480
|
async canStopRuntime(): Promise<boolean> {
|
|
1470
|
-
|
|
1481
|
+
if (this.hasLocalRuntimeWork()) return false;
|
|
1482
|
+
const state = await this.runtime.getState();
|
|
1483
|
+
// OMP can start a run of its own while the state lookup is pending.
|
|
1484
|
+
if (this.hasLocalRuntimeWork()) return false;
|
|
1485
|
+
// OMP builds that predate these fields report neither, which keeps the runtime alive.
|
|
1486
|
+
return (
|
|
1487
|
+
state.isSettled === true &&
|
|
1488
|
+
state.hasPendingAsyncWork === false &&
|
|
1489
|
+
!state.isStreaming &&
|
|
1490
|
+
!state.isCompacting
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
private hasLocalRuntimeWork(): boolean {
|
|
1495
|
+
return (
|
|
1471
1496
|
this.closed ||
|
|
1472
1497
|
this.runtimeDead !== null ||
|
|
1473
1498
|
this.recoveryPromise !== null ||
|
|
1474
1499
|
this.activeTurn !== null ||
|
|
1500
|
+
this.runStartBacklog !== null ||
|
|
1475
1501
|
this.activeAbort !== null ||
|
|
1476
1502
|
this.revertInFlight ||
|
|
1477
1503
|
this.configMutationInFlight ||
|
|
1478
1504
|
this.permissions.hasPending() ||
|
|
1479
|
-
Boolean(this.subsessions?.hasActiveChildren())
|
|
1480
|
-
if (hasLocalWork) return false;
|
|
1481
|
-
const state = await this.runtime.getState();
|
|
1482
|
-
// OMP builds that predate these fields report neither, which keeps the runtime alive.
|
|
1483
|
-
return (
|
|
1484
|
-
state.isSettled === true &&
|
|
1485
|
-
state.hasPendingAsyncWork === false &&
|
|
1486
|
-
!state.isStreaming &&
|
|
1487
|
-
!state.isCompacting
|
|
1505
|
+
Boolean(this.subsessions?.hasActiveChildren())
|
|
1488
1506
|
);
|
|
1489
1507
|
}
|
|
1490
1508
|
|
|
@@ -2259,6 +2277,17 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2259
2277
|
|
|
2260
2278
|
private handleRuntimeEvent(event: OmpRpcEvent): void {
|
|
2261
2279
|
if (this.closed) return;
|
|
2280
|
+
if (this.runStartBacklog) {
|
|
2281
|
+
this.bufferEvent(this.runStartBacklog, event);
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
// A run that starts while the previous terminal is being confirmed or published waits for it,
|
|
2285
|
+
// so neither the old turn's outcome nor the new run's frames are lost.
|
|
2286
|
+
const pendingTerminal = this.activeTurn?.terminalization ?? this.activeTurn?.agentEndCheck;
|
|
2287
|
+
if (event.type === "agent_start" && pendingTerminal) {
|
|
2288
|
+
this.deferRunStart(event, pendingTerminal);
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2262
2291
|
if (
|
|
2263
2292
|
event.type === "subagent_lifecycle" ||
|
|
2264
2293
|
event.type === "subagent_progress" ||
|
|
@@ -2336,7 +2365,8 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2336
2365
|
event.type === "ttsr_triggered" ||
|
|
2337
2366
|
event.type === "cache_warming_start" ||
|
|
2338
2367
|
event.type === "cache_warming_end" ||
|
|
2339
|
-
event.type === "queue_update"
|
|
2368
|
+
event.type === "queue_update" ||
|
|
2369
|
+
event.type === "session_info_update"
|
|
2340
2370
|
) {
|
|
2341
2371
|
return;
|
|
2342
2372
|
}
|
|
@@ -2353,7 +2383,8 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2353
2383
|
event.type === "auto_retry_end" ||
|
|
2354
2384
|
event.type === "compaction_start" ||
|
|
2355
2385
|
event.type === "compaction_end" ||
|
|
2356
|
-
event.type === "advisor_yielded"
|
|
2386
|
+
event.type === "advisor_yielded" ||
|
|
2387
|
+
event.type === "extension_error"
|
|
2357
2388
|
) {
|
|
2358
2389
|
this.projector.projectPassive(event);
|
|
2359
2390
|
return;
|
|
@@ -2367,13 +2398,23 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2367
2398
|
this.scheduleCommittedConfigRefresh();
|
|
2368
2399
|
return;
|
|
2369
2400
|
}
|
|
2370
|
-
if (
|
|
2401
|
+
if (
|
|
2402
|
+
event.type === "model_changed" ||
|
|
2403
|
+
event.type === "thinking_level_changed" ||
|
|
2404
|
+
event.type === "config_update"
|
|
2405
|
+
) {
|
|
2371
2406
|
this.scheduleCommittedConfigRefresh();
|
|
2372
2407
|
return;
|
|
2373
2408
|
}
|
|
2374
|
-
const turn =
|
|
2409
|
+
const turn =
|
|
2410
|
+
this.activeTurn ?? (event.type === "agent_start" ? this.openNativeRunTurn() : null);
|
|
2375
2411
|
if (!turn) {
|
|
2376
|
-
if (
|
|
2412
|
+
if (
|
|
2413
|
+
event.type === "auto_compaction_start" ||
|
|
2414
|
+
event.type === "auto_compaction_end" ||
|
|
2415
|
+
event.type === "manual_compaction_start" ||
|
|
2416
|
+
event.type === "manual_compaction_end"
|
|
2417
|
+
) {
|
|
2377
2418
|
this.projector.projectPassive(event);
|
|
2378
2419
|
}
|
|
2379
2420
|
return;
|
|
@@ -2386,27 +2427,69 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2386
2427
|
return;
|
|
2387
2428
|
}
|
|
2388
2429
|
if (turn.starting) {
|
|
2389
|
-
|
|
2390
|
-
turn.bufferedEvents.length >= MAX_BUFFERED_TURN_EVENTS ||
|
|
2391
|
-
retainedBytes(turn.bufferedEvents, MAX_BUFFERED_TURN_BYTES) +
|
|
2392
|
-
boundedJsonBytes(
|
|
2393
|
-
event,
|
|
2394
|
-
MAX_BUFFERED_TURN_BYTES,
|
|
2395
|
-
MAX_BUFFERED_VALUE_ITEMS,
|
|
2396
|
-
MAX_BUFFERED_TURN_BYTES,
|
|
2397
|
-
MAX_BUFFERED_VALUE_NODES,
|
|
2398
|
-
) >
|
|
2399
|
-
MAX_BUFFERED_TURN_BYTES
|
|
2400
|
-
) {
|
|
2401
|
-
this.handleRuntimeFailure();
|
|
2402
|
-
return;
|
|
2403
|
-
}
|
|
2404
|
-
turn.bufferedEvents.push(event);
|
|
2430
|
+
this.bufferEvent(turn.bufferedEvents, event);
|
|
2405
2431
|
return;
|
|
2406
2432
|
}
|
|
2407
2433
|
this.handleTurnEvent(turn, event);
|
|
2408
2434
|
}
|
|
2409
2435
|
|
|
2436
|
+
private bufferEvent(buffer: OmpRpcEvent[], event: OmpRpcEvent): void {
|
|
2437
|
+
if (
|
|
2438
|
+
buffer.length >= MAX_BUFFERED_TURN_EVENTS ||
|
|
2439
|
+
retainedBytes(buffer, MAX_BUFFERED_TURN_BYTES) +
|
|
2440
|
+
boundedJsonBytes(
|
|
2441
|
+
event,
|
|
2442
|
+
MAX_BUFFERED_TURN_BYTES,
|
|
2443
|
+
MAX_BUFFERED_VALUE_ITEMS,
|
|
2444
|
+
MAX_BUFFERED_TURN_BYTES,
|
|
2445
|
+
MAX_BUFFERED_VALUE_NODES,
|
|
2446
|
+
) >
|
|
2447
|
+
MAX_BUFFERED_TURN_BYTES
|
|
2448
|
+
) {
|
|
2449
|
+
this.handleRuntimeFailure();
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
buffer.push(event);
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
private deferRunStart(event: OmpRpcEvent, pendingTerminal: Promise<void>): void {
|
|
2456
|
+
const backlog = [event];
|
|
2457
|
+
this.runStartBacklog = backlog;
|
|
2458
|
+
const replay = () => {
|
|
2459
|
+
if (this.runStartBacklog !== backlog) return;
|
|
2460
|
+
this.runStartBacklog = null;
|
|
2461
|
+
for (const buffered of backlog) this.handleRuntimeEvent(buffered);
|
|
2462
|
+
};
|
|
2463
|
+
void pendingTerminal.then(replay, replay);
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
private scheduleHeldTurnCheck(turn: ActiveTurn, held: HeldAgentEnd): void {
|
|
2467
|
+
this.scheduler.set(() => this.checkHeldTurn(turn, held), USAGE_POLL_MS);
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
/** Released OMP does not always write `session_settled` once a held run's async work is gone. */
|
|
2471
|
+
private async checkHeldTurn(turn: ActiveTurn, held: HeldAgentEnd): Promise<void> {
|
|
2472
|
+
if (turn.heldAgentEnd !== held || this.activeTurn !== turn) return;
|
|
2473
|
+
const state = await this.usage.boundedSnapshot(turn, FINAL_USAGE_WAIT_MS);
|
|
2474
|
+
if (turn.heldAgentEnd !== held || this.activeTurn !== turn) return;
|
|
2475
|
+
const settled =
|
|
2476
|
+
state?.isSettled === true &&
|
|
2477
|
+
state.hasPendingAsyncWork === false &&
|
|
2478
|
+
!state.isStreaming &&
|
|
2479
|
+
!state.isCompacting;
|
|
2480
|
+
if (settled) this.handleTurnEvent(turn, { type: "session_settled" });
|
|
2481
|
+
else this.scheduleHeldTurnCheck(turn, held);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
private openNativeRunTurn(): ActiveTurn {
|
|
2485
|
+
const turn = createNativeRunTurn(this.generation);
|
|
2486
|
+
this.activeTurn = turn;
|
|
2487
|
+
turn.acknowledged = true;
|
|
2488
|
+
this.startTurn(turn);
|
|
2489
|
+
this.projector.acceptLiveTurn(turn.turnId);
|
|
2490
|
+
return turn;
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2410
2493
|
private handleTurnEvent(
|
|
2411
2494
|
turn: ActiveTurn,
|
|
2412
2495
|
event: OmpRpcEvent,
|
|
@@ -2476,10 +2559,27 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2476
2559
|
this.compaction.start(turn, "auto", event.action);
|
|
2477
2560
|
return;
|
|
2478
2561
|
}
|
|
2479
|
-
if (event.type === "
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2562
|
+
if (event.type === "manual_compaction_start") {
|
|
2563
|
+
turn.nativeActivity = true;
|
|
2564
|
+
this.cancelLocalOnlyCompletion(turn);
|
|
2565
|
+
// A no-op when this turn's own `compact` call already opened the operation.
|
|
2566
|
+
this.compaction.start(turn, "manual");
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
if (event.type === "auto_compaction_end" || event.type === "manual_compaction_end") {
|
|
2570
|
+
const outcome = this.compaction.handleEnd(turn, event);
|
|
2571
|
+
if (outcome && turn.manualCompaction) turn.manualCompactionOutcome = outcome;
|
|
2572
|
+
if (outcome) this.usage.schedulePoll(turn, USAGE_REFRESH_MS);
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
if (event.type === "session_settled") {
|
|
2576
|
+
// OMP is quiet, so the held end closes this stretch; a later run opens its own turn.
|
|
2577
|
+
const held = turn.heldAgentEnd;
|
|
2578
|
+
if (!held) return;
|
|
2579
|
+
turn.heldAgentEnd = undefined;
|
|
2580
|
+
Object.assign(turn, held.evidence);
|
|
2581
|
+
// Ownership is judged as of the held end's arrival, never from evidence that came later.
|
|
2582
|
+
this.acceptTerminal(turn, { ...held.event, isTerminal: true }, held.arrivalSequence);
|
|
2483
2583
|
return;
|
|
2484
2584
|
}
|
|
2485
2585
|
if (event.type === "agent_end" && turn.manualCompactionPending) return;
|
|
@@ -2502,35 +2602,30 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2502
2602
|
return;
|
|
2503
2603
|
}
|
|
2504
2604
|
}
|
|
2605
|
+
if (event.type === "agent_start") {
|
|
2606
|
+
turn.heldAgentEnd = undefined;
|
|
2607
|
+
// Evidence is per run; records OMP writes between runs belong to neither.
|
|
2608
|
+
takeRunEvidence(turn);
|
|
2609
|
+
if (turn.agentEndPending && !turn.interrupted) {
|
|
2610
|
+
// A terminal waiting to retry its idle check is stale once another run starts.
|
|
2611
|
+
this.resetAgentEndProbe(turn);
|
|
2612
|
+
turn.deferredAgentEnd = undefined;
|
|
2613
|
+
this.usage.poll(turn);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2505
2616
|
if (event.type === "agent_end") {
|
|
2506
2617
|
if (event.isTerminal === false) {
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
const candidate: TerminalCandidate = turn.interrupted
|
|
2515
|
-
? { event, confidence: "interrupted", arrivalSequence: turn.activitySequence }
|
|
2516
|
-
: classifyTerminalCandidate(turn, event);
|
|
2517
|
-
if (candidate.confidence === "ambiguous") {
|
|
2518
|
-
this.deferAmbiguousTerminal(turn, candidate);
|
|
2519
|
-
return;
|
|
2520
|
-
}
|
|
2521
|
-
turn.nativeActivity = true;
|
|
2522
|
-
this.cancelLocalOnlyCompletion(turn);
|
|
2523
|
-
if (
|
|
2524
|
-
(!turn.interrupted &&
|
|
2525
|
-
candidate.confidence !== "keyed" &&
|
|
2526
|
-
this.hasTerminalConflict(turn, false)) ||
|
|
2527
|
-
turn.steersInFlight > 0 ||
|
|
2528
|
-
turn.terminalizing
|
|
2529
|
-
) {
|
|
2530
|
-
turn.deferredAgentEnd = candidate;
|
|
2618
|
+
const held = {
|
|
2619
|
+
event,
|
|
2620
|
+
arrivalSequence: turn.activitySequence,
|
|
2621
|
+
evidence: takeRunEvidence(turn),
|
|
2622
|
+
};
|
|
2623
|
+
turn.heldAgentEnd = held;
|
|
2624
|
+
this.scheduleHeldTurnCheck(turn, held);
|
|
2531
2625
|
return;
|
|
2532
2626
|
}
|
|
2533
|
-
|
|
2627
|
+
turn.heldAgentEnd = undefined;
|
|
2628
|
+
this.acceptTerminal(turn, event, turn.activitySequence);
|
|
2534
2629
|
return;
|
|
2535
2630
|
}
|
|
2536
2631
|
if (isNativeTurnActivity(event)) {
|
|
@@ -2566,6 +2661,33 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2566
2661
|
if (event.type === "tool_execution_end") this.resumeDeferredAgentEnd();
|
|
2567
2662
|
}
|
|
2568
2663
|
|
|
2664
|
+
private acceptTerminal(
|
|
2665
|
+
turn: ActiveTurn,
|
|
2666
|
+
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
2667
|
+
arrivalSequence: number,
|
|
2668
|
+
): void {
|
|
2669
|
+
const candidate: TerminalCandidate = turn.interrupted
|
|
2670
|
+
? { event, confidence: "interrupted", arrivalSequence }
|
|
2671
|
+
: classifyTerminalCandidate(turn, event, arrivalSequence);
|
|
2672
|
+
if (candidate.confidence === "ambiguous") {
|
|
2673
|
+
this.deferAmbiguousTerminal(turn, candidate);
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
turn.nativeActivity = true;
|
|
2677
|
+
this.cancelLocalOnlyCompletion(turn);
|
|
2678
|
+
if (
|
|
2679
|
+
(!turn.interrupted &&
|
|
2680
|
+
candidate.confidence !== "keyed" &&
|
|
2681
|
+
this.hasTerminalConflict(turn, false)) ||
|
|
2682
|
+
turn.steersInFlight > 0 ||
|
|
2683
|
+
turn.terminalizing
|
|
2684
|
+
) {
|
|
2685
|
+
turn.deferredAgentEnd = candidate;
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
this.beginTerminalization(turn, candidate);
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2569
2691
|
private projectUserEcho(turn: ActiveTurn, message: OmpMessage): void {
|
|
2570
2692
|
turn.userEchoObserved = true;
|
|
2571
2693
|
const entryId = nativeEntryId(message);
|
|
@@ -3033,6 +3155,7 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
3033
3155
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
3034
3156
|
if (
|
|
3035
3157
|
candidate.confidence === "keyed" ||
|
|
3158
|
+
candidate.confidence === "native-run" ||
|
|
3036
3159
|
(candidate.confidence === "initial-turn" && !turn.userEchoObserved)
|
|
3037
3160
|
) {
|
|
3038
3161
|
void this.completeAgentEnd(turn, candidate.event);
|
|
@@ -3146,14 +3269,16 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
3146
3269
|
return;
|
|
3147
3270
|
}
|
|
3148
3271
|
if (state) {
|
|
3149
|
-
|
|
3272
|
+
// A run start buffered behind this check proves this end was the last one before it, so a
|
|
3273
|
+
// streaming state belongs to that new run, not to this turn.
|
|
3274
|
+
if ((state.isStreaming || state.isCompacting) && !this.runStartBacklog) {
|
|
3150
3275
|
this.ignoreActiveTerminalCandidate(turn, candidate);
|
|
3151
3276
|
return;
|
|
3152
3277
|
}
|
|
3153
3278
|
await this.completeAgentEnd(turn, candidate.event, true);
|
|
3154
3279
|
return;
|
|
3155
3280
|
}
|
|
3156
|
-
if (turn.userEchoObserved) {
|
|
3281
|
+
if (turn.userEchoObserved && candidate.confidence !== "native-run") {
|
|
3157
3282
|
await this.cancelIndeterminateTerminal(
|
|
3158
3283
|
turn,
|
|
3159
3284
|
"OMP agent_end state could not be confirmed",
|
|
@@ -3389,6 +3514,7 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
3389
3514
|
throw error;
|
|
3390
3515
|
}
|
|
3391
3516
|
this.unclaimedBranchEntries.length = 0;
|
|
3517
|
+
turn.terminalEmitAttempted = true;
|
|
3392
3518
|
this.emit({
|
|
3393
3519
|
type: "session.turn",
|
|
3394
3520
|
sessionId: this.id,
|
|
@@ -3398,17 +3524,42 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
3398
3524
|
});
|
|
3399
3525
|
this.runtimeTurnCompleted = true;
|
|
3400
3526
|
if (this.activeTurn === turn) this.activeTurn = null;
|
|
3401
|
-
})();
|
|
3527
|
+
})().catch(() => this.failTerminalization(turn));
|
|
3402
3528
|
turn.terminalization = terminalization;
|
|
3403
3529
|
return terminalization;
|
|
3404
3530
|
}
|
|
3405
3531
|
|
|
3532
|
+
/** A turn whose terminal state could not be published must not keep owning the session. */
|
|
3533
|
+
private failTerminalization(turn: ActiveTurn): void {
|
|
3534
|
+
const message = "OMP turn could not be finalized";
|
|
3535
|
+
turn.terminal = true;
|
|
3536
|
+
if (this.activeTurn === turn) this.activeTurn = null;
|
|
3537
|
+
this.recordOperationalFailure({ category: "terminal-outcome", stage: "failed" });
|
|
3538
|
+
// Once the terminal emit was attempted, earlier listeners may already hold it; a second,
|
|
3539
|
+
// conflicting terminal would be worse than failing the runtime alone.
|
|
3540
|
+
if (!turn.terminalEmitAttempted) {
|
|
3541
|
+
try {
|
|
3542
|
+
this.emit({
|
|
3543
|
+
type: "session.turn",
|
|
3544
|
+
sessionId: this.id,
|
|
3545
|
+
turnId: turn.turnId,
|
|
3546
|
+
state: "failed",
|
|
3547
|
+
error: { message },
|
|
3548
|
+
});
|
|
3549
|
+
} catch {
|
|
3550
|
+
// The listener that failed may fail again; the runtime failure below still applies.
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
this.handleRuntimeFailure(message);
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3406
3556
|
private invalidateRuntime(
|
|
3407
3557
|
message: string,
|
|
3408
3558
|
compactionState: "failed" | "canceled" = "failed",
|
|
3409
3559
|
): void {
|
|
3410
3560
|
this.imageMaterializer.clear();
|
|
3411
3561
|
if (this.closed || this.runtimeDead) return;
|
|
3562
|
+
this.runStartBacklog = null;
|
|
3412
3563
|
this.compaction.resetDiscardedEnds();
|
|
3413
3564
|
this.permissions.resolveAllPermissions();
|
|
3414
3565
|
this.recoveryUsesNativeConfig ||=
|
|
@@ -467,6 +467,9 @@ export class OmpTimelineProjector {
|
|
|
467
467
|
event.type === "auto_compaction_end" ||
|
|
468
468
|
event.type === "compaction_start" ||
|
|
469
469
|
event.type === "compaction_end" ||
|
|
470
|
+
event.type === "manual_compaction_start" ||
|
|
471
|
+
event.type === "manual_compaction_end" ||
|
|
472
|
+
event.type === "extension_error" ||
|
|
470
473
|
event.type === "advisor_yielded" ||
|
|
471
474
|
event.type === "config_warnings_changed" ||
|
|
472
475
|
event.type === "advisor_cost_changed" ||
|
|
@@ -743,6 +746,19 @@ export class OmpTimelineProjector {
|
|
|
743
746
|
});
|
|
744
747
|
return;
|
|
745
748
|
}
|
|
749
|
+
if (event.type === "extension_error") {
|
|
750
|
+
this.noticeSequence += 1;
|
|
751
|
+
this.publish({
|
|
752
|
+
type: "notification",
|
|
753
|
+
id: `omp:extension-error:${this.noticeSequence}`,
|
|
754
|
+
level: "error",
|
|
755
|
+
message: this.dataFilter.text(
|
|
756
|
+
`OMP extension ${event.extensionPath} failed in ${event.event}: ${event.error}`,
|
|
757
|
+
64 * 1024,
|
|
758
|
+
),
|
|
759
|
+
});
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
746
762
|
if (event.type === "advisor_yielded") {
|
|
747
763
|
this.noticeSequence += 1;
|
|
748
764
|
this.publish({
|
|
@@ -753,7 +769,11 @@ export class OmpTimelineProjector {
|
|
|
753
769
|
});
|
|
754
770
|
return;
|
|
755
771
|
}
|
|
756
|
-
if (
|
|
772
|
+
if (
|
|
773
|
+
event.type === "auto_compaction_start" ||
|
|
774
|
+
event.type === "compaction_start" ||
|
|
775
|
+
event.type === "manual_compaction_start"
|
|
776
|
+
) {
|
|
757
777
|
const trigger = event.type === "auto_compaction_start" ? "auto" : "manual";
|
|
758
778
|
const action = event.type === "auto_compaction_start" ? event.action : undefined;
|
|
759
779
|
if (this.discardedCompactionEnds > 0) {
|
|
@@ -781,7 +801,11 @@ export class OmpTimelineProjector {
|
|
|
781
801
|
this.publish({ type: "compaction", id: slot.id, status: "loading", trigger });
|
|
782
802
|
return;
|
|
783
803
|
}
|
|
784
|
-
if (
|
|
804
|
+
if (
|
|
805
|
+
event.type === "auto_compaction_end" ||
|
|
806
|
+
event.type === "compaction_end" ||
|
|
807
|
+
event.type === "manual_compaction_end"
|
|
808
|
+
) {
|
|
785
809
|
if (this.discardedCompactionEnds > 0) {
|
|
786
810
|
this.discardedCompactionEnds -= 1;
|
|
787
811
|
return;
|
|
@@ -803,7 +827,7 @@ export class OmpTimelineProjector {
|
|
|
803
827
|
this.discardedCompactionEnds = 1;
|
|
804
828
|
return;
|
|
805
829
|
}
|
|
806
|
-
if (event.willRetry) {
|
|
830
|
+
if (event.type !== "manual_compaction_end" && event.willRetry) {
|
|
807
831
|
slot.retrying = true;
|
|
808
832
|
return;
|
|
809
833
|
}
|
|
@@ -826,10 +850,9 @@ export class OmpTimelineProjector {
|
|
|
826
850
|
type: "notification",
|
|
827
851
|
id: `${slot.id}:skipped`,
|
|
828
852
|
level: "warning",
|
|
829
|
-
message: "OMP compaction was skipped",
|
|
853
|
+
message: this.dataFilter.text(event.errorMessage ?? "OMP compaction was skipped", 4_096),
|
|
830
854
|
});
|
|
831
|
-
}
|
|
832
|
-
if (event.aborted || event.errorMessage) {
|
|
855
|
+
} else if (event.aborted || event.errorMessage) {
|
|
833
856
|
this.publish({
|
|
834
857
|
type: "error",
|
|
835
858
|
id: `${slot.id}:error`,
|
|
@@ -3,9 +3,13 @@ import { z } from "zod";
|
|
|
3
3
|
|
|
4
4
|
export const MAX_INHERITED_ENVIRONMENT_NAMES = 256;
|
|
5
5
|
export const environmentNameSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u);
|
|
6
|
+
export const DEFAULT_MAX_SESSIONS = 32;
|
|
7
|
+
// Every allowed session can open at once without reaching the 128 in-flight operations bound.
|
|
8
|
+
export const MAX_SESSIONS_UPPER_BOUND = 64;
|
|
6
9
|
|
|
7
10
|
export const providerLaunchSettingsSchema = z.object({
|
|
8
11
|
inheritEnv: z.array(environmentNameSchema).max(MAX_INHERITED_ENVIRONMENT_NAMES).default([]),
|
|
12
|
+
maxSessions: z.number().int().min(1).max(MAX_SESSIONS_UPPER_BOUND).default(DEFAULT_MAX_SESSIONS),
|
|
9
13
|
});
|
|
10
14
|
|
|
11
15
|
export type ProviderLaunchSettings = z.infer<typeof providerLaunchSettingsSchema>;
|