@omercnet/paseo-omp 1.3.0 → 1.3.1-next.229.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/OVERVIEW.md +24 -0
- package/README.md +5 -5
- package/index.server.ts +4 -4
- package/package.json +9 -8
- package/server/package-version.ts +1 -1
- package/server/provider/config-normalization.ts +3 -2
- package/server/provider/connection.ts +48 -26
- package/server/provider/host-tools.ts +17 -0
- package/server/provider/session-history.ts +330 -0
- package/server/provider/session-purpose.ts +56 -0
- package/server/provider/session.ts +22 -5
- package/server/provider/subsessions.ts +9 -6
package/OVERVIEW.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
OMP adds an agent provider for the OMP coding agent, plus an OMP sidebar and workspace panel for configuring it. Choose **OMP Plugin** when creating an agent, and Paseo runs OMP on the daemon host and maps its prompts, images, steering, tool permissions, subagents, session history and conversation rewind into Paseo. It runs alongside Paseo's bundled `omp` provider without changing it or migrating existing `omp` agents. Sessions created by an earlier alpha preview may need to be re-imported after an upgrade.
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
- Paseo 0.9.2 through 0.9.x, 0.10.x or 0.11.x.
|
|
6
|
+
- OMP 18.1.15 or newer on the daemon host, speaking RPC protocol v2. Older runtimes are rejected.
|
|
7
|
+
- Provider options let you change the OMP command, set environment values, choose an output redaction mode and set a session directory and timeout. Named OMP profiles appear as separate `OMP · <profile>` providers.
|
|
8
|
+
|
|
9
|
+
## What the sidebar and panel do
|
|
10
|
+
|
|
11
|
+
- Edit OMP settings and project configuration for the default store or a named profile.
|
|
12
|
+
- Manage OMP's own plugins, and view quota history, memory, hub processes, session history and a support report.
|
|
13
|
+
- An **MCP** composer control runs OMP's MCP management in the current session, with authorization prompts shown in the chat.
|
|
14
|
+
|
|
15
|
+
## Reads and sends
|
|
16
|
+
|
|
17
|
+
- Runs the OMP command as a process on the daemon host. Its output is passed to Paseo as produced, apart from validation and length limits.
|
|
18
|
+
- Reads OMP's settings, configuration, session transcripts, quota history, memory and hub state on the host, and writes OMP's settings file when you edit settings in the panel.
|
|
19
|
+
- With the default output redaction, nothing is redacted, so do not put credentials in prompts or tool output. The optional `configured-values` mode replaces configured credential values on a best-effort basis.
|
|
20
|
+
- Opens only http and https links, through Paseo.
|
|
21
|
+
|
|
22
|
+
## Limits
|
|
23
|
+
|
|
24
|
+
Structured output schemas, archive and unarchive, revert of files, and exact MCP tool pre-approval are not supported, and requests for them fail visibly. Changing the approval mode of a running session needs a new session.
|
package/README.md
CHANGED
|
@@ -54,8 +54,8 @@ Profile selection is request-local: concurrent clients cannot change each other'
|
|
|
54
54
|
|
|
55
55
|
## Paseo provider SDK coverage
|
|
56
56
|
|
|
57
|
-
The capability inventory is pinned to `@getpaseo/plugin` 0.11.
|
|
58
|
-
|
|
57
|
+
The capability inventory is pinned to `@getpaseo/plugin` 0.11.1, with nightly compatibility checks
|
|
58
|
+
against supported 0.9 and 0.10 hosts and the latest/beta channels. Provider capability scoring covers
|
|
59
59
|
all 17 entries in the SDK's `PROVIDER_CAPABILITIES`; the separate provider/client SDK table below
|
|
60
60
|
also tracks the 0.11 registration, screen, and sidebar APIs. This measures strict SDK surface
|
|
61
61
|
coverage, not general product quality.
|
|
@@ -79,7 +79,7 @@ Scoring is deliberately mechanical so releases remain comparable:
|
|
|
79
79
|
| `session.archive` | **0%** | Not advertised. Paseo may archive its agent record, but OMP has no native transcript archive operation. |
|
|
80
80
|
| `session.configure` | **50%** | Live model and thinking changes commit and republish native state. Live approval-mode changes require a new session, and non-empty SDK `settings` are rejected. |
|
|
81
81
|
| `session.list` | **100%** | Bounded host-wide and cwd-scoped discovery, search, previews, profile-specific session roots, and cleanup-reservation guards. |
|
|
82
|
-
| `session.persistence` | **100%** | Versioned opaque handles, exact resume, import, replay-before-ready, runtime recovery, and cleanup quarantine. |
|
|
82
|
+
| `session.persistence` | **100%** | Versioned opaque handles, exact resume, import, replay-before-ready, runtime recovery, and cleanup quarantine. Archived agents whose recorded workspace has been removed load read-only from their authorized transcript when Paseo opens them with its `history` purpose, without spawning OMP or connecting MCP servers; interactive create and resume still require the workspace. |
|
|
83
83
|
| `session.revert.both` | **0%** | Not advertised because OMP cannot atomically rewind conversation and workspace files. |
|
|
84
84
|
| `session.revert.conversation` | **100%** | Native branch-based conversation rewind, replay deduplication, ownership transfer, and failure quarantine. |
|
|
85
85
|
| `session.revert.files` | **0%** | Not advertised because OMP does not expose a native file-only rewind contract. |
|
|
@@ -95,7 +95,7 @@ Provider functionality outside the capability flags is tracked separately:
|
|
|
95
95
|
| --- | ---: | --- |
|
|
96
96
|
| Registration metadata and sanitized SVG icon | **100%** | Stable `omp-plugin` identity, label, description, and bundled icon. |
|
|
97
97
|
| Strict `providerOptionsSchema` | **100%** | Command, literal environment, names-only profile inherited environment, output-redaction mode, session directory, RPC timeout, and role-model options are validated and normalized. Host-wide inherited names are separately persisted and merged at launch. |
|
|
98
|
-
| Availability diagnostics | **50%** | The plugin implements a `checkAvailability` hook with bounded checks that distinguish missing, unrunnable, incompatible, and available OMP runtimes, but
|
|
98
|
+
| Availability diagnostics | **50%** | The plugin implements a `checkAvailability` hook with bounded checks that distinguish missing, unrunnable, incompatible, and available OMP runtimes, but Paseo 0.10.2 and 0.11.1 do not call it. Hosts treat the provider as available when a connection opens, and launch failures surface at session start. |
|
|
99
99
|
| Catalog cache identity | **100%** | Hash includes effective options, merged host/profile inherited-environment names, settings, scope, cwd, and default command, but never resolves or fingerprints inherited values. |
|
|
100
100
|
| Models, modes, and thinking catalog | **100%** | Native catalog is mapped to opaque public model IDs with committed defaults and permission-gated modes. |
|
|
101
101
|
| Connection `send` / `onEvent` / `close` lifecycle | **100%** | Request correlation, multi-session ownership, process recovery, teardown, and provider reload/removal are covered. |
|
|
@@ -124,7 +124,7 @@ Existing agents whose provider is `omp` remain owned by the bundled provider. Ne
|
|
|
124
124
|
|
|
125
125
|
OMP `18.1.15` is the oldest version tested end to end. The direct provider's hard compatibility gate is `rpc-ui` protocol v2: metadata-free legacy ready frames and v1-only runtimes are rejected before a provider session opens because they cannot support the advertised persistence and conversation-rewind capabilities. Typed tool approvals remain capability-gated and fall back as described above. For terminal completion, a matching `agent_end.requestId` is authoritative and mismatches are ignored before state changes. Released OMP 18.2.x builds that omit that field use an ordered fallback requiring a fresh correlated native user entry followed by current-turn assistant activity, an idle non-compacting runtime, and no conflicting permission, tool, steer, or child-session work. Ambiguous events while OMP is active are ignored; unresolved ambiguity after confirmed idle fails only the Paseo turn. This trades a bounded residual same-agent stale-event risk for compatibility with released OMP instead of terminating and lazily restarting the runtime after every later prompt. Paseo clients, including mobile clients inside reconnect grace, do not own or terminate the daemon-managed OMP session.
|
|
126
126
|
|
|
127
|
-
Paseo 0.9.2 is the minimum supported stable release, and 0.10.1 is supported. The controlled Docker canary
|
|
127
|
+
Paseo 0.9.2 is the minimum supported stable release, and 0.10.1 is supported. The controlled Docker canary is pinned to official 0.11.1; nightly SDK checks cover the newest 0.9 and 0.10 patches plus the latest and beta channels. Older Paseo lines are covered by that typecheck/unit matrix, not the current Docker canary. The canary preserves nested provider-subagent ancestry and spawning-tool links, and validates npm-managed plugin sources, platform-owned external URL opening, and whole-item timeline transforms before Overview grouping.
|
|
128
128
|
|
|
129
129
|
Install, update, disable, or remove `paseo-omp` independently of the bundled provider. Verify the provider snapshot contains `omp-plugin` after installation; a bundled `omp` entry may remain present and is not modified by this plugin.
|
|
130
130
|
|
package/index.server.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { resolveListOmpSettings, resolveUpdateOmpSettings } from "./server/omp-s
|
|
|
17
17
|
import { OmpOperationalFailureCollector } from "./server/operational-failure-diagnostics";
|
|
18
18
|
import { withOmpStore } from "./server/paths";
|
|
19
19
|
import { OmpProtocolViolationCollector } from "./server/protocol-violation-diagnostics";
|
|
20
|
-
import {
|
|
20
|
+
import { withOmpSessionOpenEnv } from "./server/provider/host-tools";
|
|
21
21
|
import {
|
|
22
22
|
createProfileOmpProvider,
|
|
23
23
|
discoverOmpProfiles,
|
|
@@ -97,9 +97,9 @@ export default function contribute(server: PluginServerContext) {
|
|
|
97
97
|
server.handle(openOmpMcpAuthorizationInPaseoBrowser, (input) =>
|
|
98
98
|
resolveOpenOmpMcpAuthorizationInPaseoBrowser(input, browserAuthorizationRegistry),
|
|
99
99
|
);
|
|
100
|
-
const
|
|
100
|
+
const removeSessionOpenHook = server.before("agent.session_open", ({ request }) => {
|
|
101
101
|
if (request.provider !== "omp-plugin" && !request.provider.startsWith("omp-plugin-")) return;
|
|
102
|
-
return
|
|
102
|
+
return withOmpSessionOpenEnv(request);
|
|
103
103
|
});
|
|
104
104
|
server.registerProvider(
|
|
105
105
|
createOmpProvider({
|
|
@@ -111,6 +111,6 @@ export default function contribute(server: PluginServerContext) {
|
|
|
111
111
|
);
|
|
112
112
|
return () => {
|
|
113
113
|
browserAuthorizationRegistry.clear();
|
|
114
|
-
|
|
114
|
+
removeSessionOpenHook();
|
|
115
115
|
};
|
|
116
116
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omercnet/paseo-omp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1-next.229.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"client",
|
|
38
38
|
"server",
|
|
39
39
|
"shared",
|
|
40
|
-
"paseo-plugin.json"
|
|
40
|
+
"paseo-plugin.json",
|
|
41
|
+
"OVERVIEW.md"
|
|
41
42
|
],
|
|
42
43
|
"scripts": {
|
|
43
44
|
"check": "biome check .",
|
|
@@ -51,16 +52,16 @@
|
|
|
51
52
|
"typecheck": "tsc --noEmit -p tsconfig.client.json && tsc --noEmit -p tsconfig.server.json && tsc --noEmit -p tsconfig.tests.json"
|
|
52
53
|
},
|
|
53
54
|
"dependencies": {
|
|
54
|
-
"@getpaseo/protocol": "0.11.
|
|
55
|
+
"@getpaseo/protocol": "0.11.1",
|
|
55
56
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
56
57
|
"yaml": "^2.9.0"
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
59
60
|
"@biomejs/biome": "^2.5.10",
|
|
60
|
-
"@getpaseo/cli": "0.11.
|
|
61
|
-
"@getpaseo/client": "0.11.
|
|
62
|
-
"@getpaseo/plugin": "0.11.
|
|
63
|
-
"@getpaseo/server": "0.11.
|
|
61
|
+
"@getpaseo/cli": "0.11.1",
|
|
62
|
+
"@getpaseo/client": "0.11.1",
|
|
63
|
+
"@getpaseo/plugin": "0.11.1",
|
|
64
|
+
"@getpaseo/server": "0.11.1",
|
|
64
65
|
"@oh-my-pi/pi-agent-core": "18.2.0",
|
|
65
66
|
"@oh-my-pi/pi-coding-agent": "18.2.0",
|
|
66
67
|
"@tanstack/react-query": "^5.102.3",
|
|
@@ -68,9 +69,9 @@
|
|
|
68
69
|
"@types/react": "^19.2.18",
|
|
69
70
|
"@vitest/coverage-v8": "^5.0.0",
|
|
70
71
|
"esbuild": "0.28.2",
|
|
72
|
+
"pino": "10.3.1",
|
|
71
73
|
"react": "19.1.0",
|
|
72
74
|
"react-native": "0.81.5",
|
|
73
|
-
"pino": "10.3.1",
|
|
74
75
|
"tsx": "^4.20.6",
|
|
75
76
|
"typescript": "^7.0.2",
|
|
76
77
|
"vitest": "^5.0.0",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Release Please keeps this bundle-safe constant aligned with package.json. */
|
|
2
|
-
export const PASEO_OMP_PACKAGE_VERSION = "1.3.
|
|
2
|
+
export const PASEO_OMP_PACKAGE_VERSION = "1.3.1-next.229.1"; // x-release-please-version
|
|
@@ -12,6 +12,7 @@ type ProviderCatalogOptionsCompat = {
|
|
|
12
12
|
import { MAX_INHERITED_ENVIRONMENT_NAMES } from "../../shared/provider-launch-settings";
|
|
13
13
|
import { parseOmpProviderOptions } from "./provider-options";
|
|
14
14
|
import { OmpPublicError } from "./security";
|
|
15
|
+
import { withoutOmpSessionPurpose } from "./session-purpose";
|
|
15
16
|
import { OmpModeSchema } from "./settings";
|
|
16
17
|
|
|
17
18
|
const OMP_BUILTIN_TOOL_NAMES = [
|
|
@@ -84,7 +85,7 @@ export function normalizeOmpCatalogOptions(
|
|
|
84
85
|
mode: "full",
|
|
85
86
|
noSession: true,
|
|
86
87
|
...(providerOptions.command ? { command: providerOptions.command } : {}),
|
|
87
|
-
...(providerOptions.env ? { env: providerOptions.env } : {}),
|
|
88
|
+
...(providerOptions.env ? { env: withoutOmpSessionPurpose(providerOptions.env) } : {}),
|
|
88
89
|
...(inheritEnv ? { inheritEnv } : {}),
|
|
89
90
|
outputRedaction: providerOptions.outputRedaction,
|
|
90
91
|
...(params.sessionDir ? { sessionDir: params.sessionDir } : {}),
|
|
@@ -136,7 +137,7 @@ export function normalizeOmpSessionConfig(
|
|
|
136
137
|
.deniedTools;
|
|
137
138
|
const tools = allowedOmpTools(deniedTools);
|
|
138
139
|
const params = options.params ?? {};
|
|
139
|
-
const env = { ...options.env, ...config.env };
|
|
140
|
+
const env = withoutOmpSessionPurpose({ ...options.env, ...config.env });
|
|
140
141
|
const inheritEnv = mergeOmpInheritedEnvironmentNames(hostInheritEnv, options.inheritEnv);
|
|
141
142
|
return {
|
|
142
143
|
cwd: config.cwd,
|
|
@@ -32,7 +32,9 @@ import {
|
|
|
32
32
|
OmpPublicError,
|
|
33
33
|
utf8Bytes,
|
|
34
34
|
} from "./security";
|
|
35
|
-
import { OmpProviderSession, ompPersistenceSessionId } from "./session";
|
|
35
|
+
import { type OmpOpenedSession, OmpProviderSession, ompPersistenceSessionId } from "./session";
|
|
36
|
+
import { OmpHistorySession } from "./session-history";
|
|
37
|
+
import { isOmpHistoryOnlyOpen } from "./session-purpose";
|
|
36
38
|
|
|
37
39
|
type ProviderConfigurationCompat = {
|
|
38
40
|
providerOptions?: Readonly<Record<string, unknown>>;
|
|
@@ -747,7 +749,7 @@ export function createOmpConnection(
|
|
|
747
749
|
string,
|
|
748
750
|
{
|
|
749
751
|
token: symbol;
|
|
750
|
-
session:
|
|
752
|
+
session: OmpOpenedSession;
|
|
751
753
|
nativeSessionId?: string;
|
|
752
754
|
removeBrowserAuthorization?: () => void;
|
|
753
755
|
}
|
|
@@ -909,10 +911,15 @@ export function createOmpConnection(
|
|
|
909
911
|
opening.set(input.sessionId, { token, controller, settled: openingSettled.promise });
|
|
910
912
|
let nativeSessionId: string | undefined;
|
|
911
913
|
let persistentOpening = false;
|
|
912
|
-
let
|
|
914
|
+
let historyOnly = false;
|
|
915
|
+
let session: OmpOpenedSession | undefined;
|
|
913
916
|
try {
|
|
917
|
+
historyOnly = isOmpHistoryOnlyOpen(input);
|
|
914
918
|
nativeSessionId = ompPersistenceSessionId(input);
|
|
915
|
-
if (
|
|
919
|
+
if (historyOnly) {
|
|
920
|
+
// History reads one authorized transcript and runs nothing, so it takes no native
|
|
921
|
+
// session reservation and neither blocks nor is blocked by an interactive session.
|
|
922
|
+
} else if (nativeSessionId) {
|
|
916
923
|
await nativeReservations.reserve(nativeSessionId, token, controller.signal);
|
|
917
924
|
} else if (input.config.persist) {
|
|
918
925
|
await nativeReservations.beginPersistentOpen(token, controller.signal);
|
|
@@ -955,23 +962,35 @@ export function createOmpConnection(
|
|
|
955
962
|
const retireRewindSession = () => {
|
|
956
963
|
deleteSession(input.sessionId, token);
|
|
957
964
|
};
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
965
|
+
const hostInheritEnv = (await resolveHostInheritEnv?.()) ?? [];
|
|
966
|
+
session = historyOnly
|
|
967
|
+
? await OmpHistorySession.open(
|
|
968
|
+
input,
|
|
969
|
+
runtime,
|
|
970
|
+
safeCapabilities,
|
|
971
|
+
sessionEmit,
|
|
972
|
+
replayTimeoutMs,
|
|
973
|
+
environment,
|
|
974
|
+
hostInheritEnv,
|
|
975
|
+
scheduler,
|
|
976
|
+
)
|
|
977
|
+
: await OmpProviderSession.open(
|
|
978
|
+
input,
|
|
979
|
+
runtime,
|
|
980
|
+
safeCapabilities,
|
|
981
|
+
sessionEmit,
|
|
982
|
+
transitionNativeSession,
|
|
983
|
+
quarantineRewindCleanup,
|
|
984
|
+
retireRewindSession,
|
|
985
|
+
scheduler,
|
|
986
|
+
replayTimeoutMs,
|
|
987
|
+
controller.signal,
|
|
988
|
+
environment,
|
|
989
|
+
mcpConnector,
|
|
990
|
+
mcpInitializationTimeoutMs,
|
|
991
|
+
reportOperationalFailure,
|
|
992
|
+
hostInheritEnv,
|
|
993
|
+
);
|
|
975
994
|
const discoveredNativeSessionId = session.persistenceSessionId;
|
|
976
995
|
if (discoveredNativeSessionId) nativeSessionId = discoveredNativeSessionId;
|
|
977
996
|
if (persistentOpening) {
|
|
@@ -995,10 +1014,13 @@ export function createOmpConnection(
|
|
|
995
1014
|
return;
|
|
996
1015
|
}
|
|
997
1016
|
const browserAgentId = input.config.env.PASEO_AGENT_ID?.trim() || input.sessionId;
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1017
|
+
// A history session has no live Paseo browser tools to authorize.
|
|
1018
|
+
const browserAuthorization = historyOnly
|
|
1019
|
+
? undefined
|
|
1020
|
+
: browserAuthorizationRegistry?.register(
|
|
1021
|
+
browserAgentId,
|
|
1022
|
+
session.openPaseoBrowser.bind(session),
|
|
1023
|
+
);
|
|
1002
1024
|
session.setBrowserAuthorizationIssuer(browserAuthorization?.issue ?? null);
|
|
1003
1025
|
const removeBrowserAuthorization = browserAuthorization
|
|
1004
1026
|
? () => {
|
|
@@ -1155,7 +1177,7 @@ export function createOmpConnection(
|
|
|
1155
1177
|
for (const result of operationResults) {
|
|
1156
1178
|
if (result.status === "rejected") failures.push(result.reason);
|
|
1157
1179
|
}
|
|
1158
|
-
const seenSessions = new Set<
|
|
1180
|
+
const seenSessions = new Set<OmpOpenedSession>();
|
|
1159
1181
|
const seenCleanups = new Set<Promise<void>>();
|
|
1160
1182
|
const cleanupBatch: Promise<void>[] = [];
|
|
1161
1183
|
for (const [sessionId, { session, nativeSessionId, token }] of sessions) {
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
truncateUtf8,
|
|
30
30
|
utf8Bytes,
|
|
31
31
|
} from "./security";
|
|
32
|
+
import { withOmpSessionPurpose } from "./session-purpose";
|
|
32
33
|
|
|
33
34
|
type HostToolFailureStage = Extract<
|
|
34
35
|
OmpOperationalFailure,
|
|
@@ -1025,3 +1026,19 @@ export function withOmpWorkspaceIdentity<
|
|
|
1025
1026
|
else delete env.PASEO_WORKSPACE_ID;
|
|
1026
1027
|
return { ...request, env };
|
|
1027
1028
|
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* The complete env transform for an OMP `agent.session_open` hook: the caller's identity plus the
|
|
1032
|
+
* host-decided launch purpose. Registered once by the plugin and exercised directly by tests.
|
|
1033
|
+
*/
|
|
1034
|
+
export function withOmpSessionOpenEnv<
|
|
1035
|
+
T extends {
|
|
1036
|
+
agentId: string;
|
|
1037
|
+
workspaceId: string | null;
|
|
1038
|
+
reason?: string;
|
|
1039
|
+
purpose?: string;
|
|
1040
|
+
env: Record<string, string>;
|
|
1041
|
+
},
|
|
1042
|
+
>(request: T) {
|
|
1043
|
+
return withOmpSessionPurpose(withOmpWorkspaceIdentity(request));
|
|
1044
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProviderConfigState,
|
|
3
|
+
ProviderEvent,
|
|
4
|
+
ProviderInput,
|
|
5
|
+
ProviderSessionConfig,
|
|
6
|
+
} from "@getpaseo/plugin/server/provider";
|
|
7
|
+
import { normalizeOmpSessionConfig } from "./config-normalization";
|
|
8
|
+
import type { OmpRuntime } from "./omp-rpc";
|
|
9
|
+
import { buildOmpSpawnRequest } from "./omp-rpc-environment";
|
|
10
|
+
import {
|
|
11
|
+
configuredOutputRedactionValues,
|
|
12
|
+
OmpPublicDataSerializer,
|
|
13
|
+
OmpPublicError,
|
|
14
|
+
utf8Bytes,
|
|
15
|
+
} from "./security";
|
|
16
|
+
import {
|
|
17
|
+
authorizeNativeSession,
|
|
18
|
+
fixedSessionMode,
|
|
19
|
+
type OmpOpenedSession,
|
|
20
|
+
ompPersistenceSessionId,
|
|
21
|
+
REPLAY_TIMEOUT_MS,
|
|
22
|
+
waitForReplay,
|
|
23
|
+
} from "./session";
|
|
24
|
+
import { OmpSubsessionProjector } from "./subsessions";
|
|
25
|
+
import {
|
|
26
|
+
defaultOmpTimelineScheduler,
|
|
27
|
+
OmpTimelineProjector,
|
|
28
|
+
type OmpTimelineScheduler,
|
|
29
|
+
} from "./timeline-projector";
|
|
30
|
+
|
|
31
|
+
type SessionOpenInput = Extract<ProviderInput, { type: "session.open" }>;
|
|
32
|
+
type SessionPromptInput = Extract<ProviderInput, { type: "session.prompt" }>;
|
|
33
|
+
type SessionInterruptInput = Extract<ProviderInput, { type: "session.interrupt" }>;
|
|
34
|
+
type SessionConfigureInput = Extract<ProviderInput, { type: "session.configure" }>;
|
|
35
|
+
type SessionRevertInput = Extract<ProviderInput, { type: "session.revert" }>;
|
|
36
|
+
type SessionCloseInput = Extract<ProviderInput, { type: "session.close" }>;
|
|
37
|
+
type SessionPermissionInput = Extract<ProviderInput, { type: "session.permission" }>;
|
|
38
|
+
type Emit = (event: ProviderEvent) => void;
|
|
39
|
+
|
|
40
|
+
// A history session replays a persisted journal and runs nothing. It advertises no prompt,
|
|
41
|
+
// permission, configuration, or rewind capability, so the host rejects those requests itself.
|
|
42
|
+
const HISTORY_CAPABILITIES: readonly string[] = [
|
|
43
|
+
"session.persistence",
|
|
44
|
+
"session.subsession",
|
|
45
|
+
"timeline.plugin",
|
|
46
|
+
];
|
|
47
|
+
const READ_ONLY_MESSAGE = "OMP history sessions are read-only";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Serves a persisted OMP session from its authorized transcript alone. It exists for sessions
|
|
51
|
+
* whose recorded working directory is gone: no runtime process is spawned, no MCP server is
|
|
52
|
+
* connected, and the working directory is never touched. The native session identity, workspace
|
|
53
|
+
* authorization, and transcript ownership checks are the same ones a live resume performs.
|
|
54
|
+
*/
|
|
55
|
+
export class OmpHistorySession implements OmpOpenedSession {
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly cwd: string;
|
|
58
|
+
|
|
59
|
+
private readonly lifetime = new AbortController();
|
|
60
|
+
private readonly dataFilter: OmpPublicDataSerializer;
|
|
61
|
+
private readonly projector: OmpTimelineProjector;
|
|
62
|
+
private readonly subsessions: OmpSubsessionProjector | null;
|
|
63
|
+
private disposed = false;
|
|
64
|
+
private sessionClosedPublished = false;
|
|
65
|
+
|
|
66
|
+
private constructor(
|
|
67
|
+
id: string,
|
|
68
|
+
private readonly config: ProviderSessionConfig,
|
|
69
|
+
private readonly runtime: OmpRuntime,
|
|
70
|
+
private readonly readTranscript: NonNullable<OmpRuntime["readPersistedSessionTranscript"]>,
|
|
71
|
+
private readonly nativeSessionId: string,
|
|
72
|
+
private readonly transcriptFile: string,
|
|
73
|
+
private readonly configState: ProviderConfigState,
|
|
74
|
+
redactionValues: readonly string[],
|
|
75
|
+
private readonly capabilities: readonly string[],
|
|
76
|
+
private readonly emit: Emit,
|
|
77
|
+
private readonly replayTimeoutMs: number,
|
|
78
|
+
scheduler: OmpTimelineScheduler,
|
|
79
|
+
) {
|
|
80
|
+
this.id = id;
|
|
81
|
+
this.cwd = config.cwd;
|
|
82
|
+
this.dataFilter = new OmpPublicDataSerializer(redactionValues);
|
|
83
|
+
const pluginTimeline = capabilities.includes("timeline.plugin");
|
|
84
|
+
this.projector = new OmpTimelineProjector(
|
|
85
|
+
id,
|
|
86
|
+
emit,
|
|
87
|
+
scheduler,
|
|
88
|
+
redactionValues,
|
|
89
|
+
false,
|
|
90
|
+
pluginTimeline,
|
|
91
|
+
);
|
|
92
|
+
// Same identity key as a live persisted session, so child session ids stay stable across
|
|
93
|
+
// a history load and a later interactive resume of the same native session.
|
|
94
|
+
this.subsessions = capabilities.includes("session.subsession")
|
|
95
|
+
? new OmpSubsessionProjector(
|
|
96
|
+
id,
|
|
97
|
+
`persisted:${nativeSessionId}`,
|
|
98
|
+
transcriptFile,
|
|
99
|
+
config.cwd,
|
|
100
|
+
emit,
|
|
101
|
+
scheduler,
|
|
102
|
+
() => {},
|
|
103
|
+
redactionValues,
|
|
104
|
+
pluginTimeline,
|
|
105
|
+
)
|
|
106
|
+
: null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get persistenceSessionId(): string {
|
|
110
|
+
return this.nativeSessionId;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static async open(
|
|
114
|
+
input: SessionOpenInput,
|
|
115
|
+
runtime: OmpRuntime,
|
|
116
|
+
capabilities: readonly string[],
|
|
117
|
+
emit: Emit,
|
|
118
|
+
replayTimeoutMs = REPLAY_TIMEOUT_MS,
|
|
119
|
+
environment?: NodeJS.ProcessEnv,
|
|
120
|
+
hostInheritEnv: readonly string[] = [],
|
|
121
|
+
scheduler: OmpTimelineScheduler = defaultOmpTimelineScheduler,
|
|
122
|
+
): Promise<OmpHistorySession> {
|
|
123
|
+
const nativeSessionId = ompPersistenceSessionId(input);
|
|
124
|
+
if (!nativeSessionId || input.history !== "replay" || !input.config.persist) {
|
|
125
|
+
throw new OmpPublicError("OMP history purpose requires a persisted session replay");
|
|
126
|
+
}
|
|
127
|
+
const normalized = normalizeOmpSessionConfig(
|
|
128
|
+
input.config,
|
|
129
|
+
capabilities.includes("permission"),
|
|
130
|
+
hostInheritEnv,
|
|
131
|
+
);
|
|
132
|
+
if (input.config.title && utf8Bytes(input.config.title) > 256) {
|
|
133
|
+
throw new OmpPublicError("OMP session title is too large");
|
|
134
|
+
}
|
|
135
|
+
const readTranscript = runtime.readPersistedSessionTranscript?.bind(runtime);
|
|
136
|
+
if (!readTranscript) throw new OmpPublicError("OMP session history cannot be replayed safely");
|
|
137
|
+
// The same workspace and native-identity authorization as a live resume; only the spawn is
|
|
138
|
+
// skipped. The recorded cwd is compared, never opened.
|
|
139
|
+
const descriptor = await authorizeNativeSession(
|
|
140
|
+
runtime,
|
|
141
|
+
nativeSessionId,
|
|
142
|
+
input.config.cwd,
|
|
143
|
+
normalized.sessionDir,
|
|
144
|
+
);
|
|
145
|
+
if (!descriptor.transcriptFile) {
|
|
146
|
+
throw new OmpPublicError("OMP session transcript could not be resolved in this workspace");
|
|
147
|
+
}
|
|
148
|
+
const configuredValues = configuredOutputRedactionValues(
|
|
149
|
+
normalized.outputRedaction ?? "none",
|
|
150
|
+
normalized.env,
|
|
151
|
+
input.config.mcpServers,
|
|
152
|
+
);
|
|
153
|
+
// Inherited values are redacted exactly as a live session would: they come from the same
|
|
154
|
+
// spawn-request builder, which only reads the server environment.
|
|
155
|
+
const redactionValues =
|
|
156
|
+
normalized.outputRedaction === "configured-values"
|
|
157
|
+
? [
|
|
158
|
+
...configuredValues,
|
|
159
|
+
...buildOmpSpawnRequest({
|
|
160
|
+
...normalized,
|
|
161
|
+
environment,
|
|
162
|
+
// A resume never reapplies the system prompt or thinking selection.
|
|
163
|
+
systemPrompt: undefined,
|
|
164
|
+
thinkingOption: undefined,
|
|
165
|
+
resumeSessionId: nativeSessionId,
|
|
166
|
+
resumeSessionFile: descriptor.transcriptFile,
|
|
167
|
+
}).inheritedRedactionValues,
|
|
168
|
+
]
|
|
169
|
+
: configuredValues;
|
|
170
|
+
const configState: ProviderConfigState = {
|
|
171
|
+
...(input.config.model ? { model: input.config.model } : {}),
|
|
172
|
+
mode: normalized.mode ?? "full",
|
|
173
|
+
...(input.config.thinkingOption ? { thinkingOption: input.config.thinkingOption } : {}),
|
|
174
|
+
models: [],
|
|
175
|
+
modes: [fixedSessionMode(normalized.mode)],
|
|
176
|
+
thinkingOptions: [],
|
|
177
|
+
settings: [],
|
|
178
|
+
};
|
|
179
|
+
return new OmpHistorySession(
|
|
180
|
+
input.sessionId,
|
|
181
|
+
input.config,
|
|
182
|
+
runtime,
|
|
183
|
+
readTranscript,
|
|
184
|
+
nativeSessionId,
|
|
185
|
+
descriptor.transcriptFile,
|
|
186
|
+
configState,
|
|
187
|
+
redactionValues,
|
|
188
|
+
capabilities.filter((capability) => HISTORY_CAPABILITIES.includes(capability)),
|
|
189
|
+
emit,
|
|
190
|
+
replayTimeoutMs,
|
|
191
|
+
scheduler,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async publishOpened(requestId: string): Promise<void> {
|
|
196
|
+
this.emit({
|
|
197
|
+
type: "session.opened",
|
|
198
|
+
requestId,
|
|
199
|
+
sessionId: this.id,
|
|
200
|
+
capabilities: this.capabilities,
|
|
201
|
+
restoration: "core",
|
|
202
|
+
cwd: this.cwd,
|
|
203
|
+
persistence: { version: 1, data: { sessionId: this.nativeSessionId } },
|
|
204
|
+
...(this.config.title ? { title: this.dataFilter.text(this.config.title, 256) } : {}),
|
|
205
|
+
});
|
|
206
|
+
this.emit({ type: "session.config", sessionId: this.id, config: this.configState });
|
|
207
|
+
await this.replayHistory();
|
|
208
|
+
this.emit({ type: "session.ready", requestId, sessionId: this.id });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private async replayHistory(): Promise<void> {
|
|
212
|
+
this.lifetime.signal.throwIfAborted();
|
|
213
|
+
const replay = new AbortController();
|
|
214
|
+
const onAbort = () =>
|
|
215
|
+
replay.abort(new OmpPublicError("OMP session history replay was canceled"));
|
|
216
|
+
this.lifetime.signal.addEventListener("abort", onAbort, { once: true });
|
|
217
|
+
const timeoutHandle = setTimeout(
|
|
218
|
+
() => replay.abort(new OmpPublicError("OMP session history replay timed out")),
|
|
219
|
+
this.replayTimeoutMs,
|
|
220
|
+
);
|
|
221
|
+
try {
|
|
222
|
+
const transcript = await waitForReplay(
|
|
223
|
+
this.readTranscript({
|
|
224
|
+
sessionFile: this.transcriptFile,
|
|
225
|
+
sessionId: this.nativeSessionId,
|
|
226
|
+
cwd: this.cwd,
|
|
227
|
+
signal: replay.signal,
|
|
228
|
+
}),
|
|
229
|
+
replay.signal,
|
|
230
|
+
);
|
|
231
|
+
if (transcript.imageReplayWarning) {
|
|
232
|
+
this.emit({
|
|
233
|
+
type: "timeline.item",
|
|
234
|
+
sessionId: this.id,
|
|
235
|
+
item: {
|
|
236
|
+
id: "omp:replay-image-unavailable",
|
|
237
|
+
type: "notification",
|
|
238
|
+
level: "warning",
|
|
239
|
+
message: "OMP skipped one or more unavailable images while replaying this session.",
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
for (const message of transcript.messages) {
|
|
244
|
+
replay.signal.throwIfAborted();
|
|
245
|
+
this.projector.projectReplayMessage(message);
|
|
246
|
+
}
|
|
247
|
+
this.projector.finishReplay();
|
|
248
|
+
await this.subsessions?.replay(transcript.messages, undefined, this.runtime, replay.signal);
|
|
249
|
+
replay.signal.throwIfAborted();
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (replay.signal.aborted) throw replay.signal.reason;
|
|
252
|
+
throw error;
|
|
253
|
+
} finally {
|
|
254
|
+
clearTimeout(timeoutHandle);
|
|
255
|
+
this.lifetime.signal.removeEventListener("abort", onAbort);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
openPaseoBrowser(): Promise<void> {
|
|
260
|
+
return Promise.reject(new OmpPublicError(READ_ONLY_MESSAGE));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
setBrowserAuthorizationIssuer(issue: ((url: string) => string | undefined) | null): void {
|
|
264
|
+
this.projector.setBrowserAuthorizationIssuer(issue);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
prompt(input: SessionPromptInput): Promise<void> {
|
|
268
|
+
this.emit({
|
|
269
|
+
type: "session.prompt_result",
|
|
270
|
+
sessionId: this.id,
|
|
271
|
+
clientMessageId: input.prompt.clientMessageId,
|
|
272
|
+
result: { type: "failed", error: { message: READ_ONLY_MESSAGE } },
|
|
273
|
+
});
|
|
274
|
+
return Promise.resolve();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
permission(_input: SessionPermissionInput): Promise<void> {
|
|
278
|
+
return Promise.reject(new OmpPublicError("OMP history sessions have no pending permissions"));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
configure(input: SessionConfigureInput): Promise<void> {
|
|
282
|
+
this.emit({
|
|
283
|
+
type: "request.failed",
|
|
284
|
+
requestId: input.requestId,
|
|
285
|
+
error: { message: READ_ONLY_MESSAGE },
|
|
286
|
+
});
|
|
287
|
+
return Promise.resolve();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
revert(input: SessionRevertInput): Promise<void> {
|
|
291
|
+
this.emit({
|
|
292
|
+
type: "request.failed",
|
|
293
|
+
requestId: input.requestId,
|
|
294
|
+
error: { message: READ_ONLY_MESSAGE },
|
|
295
|
+
});
|
|
296
|
+
return Promise.resolve();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
interrupt(input: SessionInterruptInput): Promise<void> {
|
|
300
|
+
this.emit({ type: "request.completed", requestId: input.requestId });
|
|
301
|
+
return Promise.resolve();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
beginConnectionShutdown(): void {
|
|
305
|
+
this.lifetime.abort(new Error("OMP provider connection closed"));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
abortOpen(): Promise<void> {
|
|
309
|
+
this.dispose();
|
|
310
|
+
return Promise.resolve();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
close(input?: SessionCloseInput): Promise<void> {
|
|
314
|
+
this.dispose();
|
|
315
|
+
if (!this.sessionClosedPublished) {
|
|
316
|
+
this.sessionClosedPublished = true;
|
|
317
|
+
this.emit({ type: "session.closed", sessionId: this.id });
|
|
318
|
+
}
|
|
319
|
+
if (input) this.emit({ type: "request.completed", requestId: input.requestId });
|
|
320
|
+
return Promise.resolve();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private dispose(): void {
|
|
324
|
+
if (this.disposed) return;
|
|
325
|
+
this.disposed = true;
|
|
326
|
+
this.lifetime.abort(new Error("OMP provider session closed"));
|
|
327
|
+
this.subsessions?.close();
|
|
328
|
+
this.projector.close();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ProviderInput } from "@getpaseo/plugin/server/provider";
|
|
2
|
+
import { OmpPublicError } from "./security";
|
|
3
|
+
|
|
4
|
+
type SessionOpenInput = Extract<ProviderInput, { type: "session.open" }>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Paseo tells plugins why a session is being opened only through `before("agent.session_open")`
|
|
8
|
+
* hooks, whose sole writable field is `env`; the plugin provider protocol forwards that env
|
|
9
|
+
* untouched as `session.open.config.env`. The hook is therefore the single writer of this launch
|
|
10
|
+
* marker and the connection is the single reader. It is never part of the stored agent config.
|
|
11
|
+
*/
|
|
12
|
+
export const OMP_SESSION_PURPOSE_ENV = "PASEO_OMP_SESSION_PURPOSE";
|
|
13
|
+
const HISTORY_PURPOSE = "history";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Derives the launch marker from the host-decided purpose. Any marker already present is caller
|
|
17
|
+
* input (for example `env` on agent creation), so it is always discarded first: only a host
|
|
18
|
+
* resume whose purpose is history can produce it, and an absent purpose (older hosts) never does.
|
|
19
|
+
*/
|
|
20
|
+
export function withOmpSessionPurpose<
|
|
21
|
+
T extends { reason?: string; purpose?: string; env: Record<string, string> },
|
|
22
|
+
>(request: T): Omit<T, "env"> & { env: Record<string, string> } {
|
|
23
|
+
const env: Record<string, string> = { ...request.env };
|
|
24
|
+
delete env[OMP_SESSION_PURPOSE_ENV];
|
|
25
|
+
if (request.purpose === HISTORY_PURPOSE && request.reason === "resume") {
|
|
26
|
+
env[OMP_SESSION_PURPOSE_ENV] = HISTORY_PURPOSE;
|
|
27
|
+
}
|
|
28
|
+
return { ...request, env };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* True when this open must be served from the persisted transcript alone, without a runtime
|
|
33
|
+
* process or any working-directory access. The marker only narrows a persisted replay: it can
|
|
34
|
+
* never be satisfied by a create, a non-replaying open, or a non-persisted config.
|
|
35
|
+
*/
|
|
36
|
+
export function isOmpHistoryOnlyOpen(input: SessionOpenInput): boolean {
|
|
37
|
+
const marker = input.config.env[OMP_SESSION_PURPOSE_ENV];
|
|
38
|
+
if (marker === undefined) return false;
|
|
39
|
+
if (marker !== HISTORY_PURPOSE) throw new OmpPublicError("Invalid OMP session purpose");
|
|
40
|
+
if (input.history !== "replay" || !input.persistence || !input.config.persist) {
|
|
41
|
+
throw new OmpPublicError("OMP history purpose requires a persisted session replay");
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Removes the reserved launch marker from any env bound for an OMP process. The marker steers
|
|
48
|
+
* the plugin only; it must never be forwarded to, or settable through per-agent provider options
|
|
49
|
+
* for, a real OMP runtime.
|
|
50
|
+
*/
|
|
51
|
+
export function withoutOmpSessionPurpose(
|
|
52
|
+
env: Readonly<Record<string, string>>,
|
|
53
|
+
): Record<string, string> {
|
|
54
|
+
const { [OMP_SESSION_PURPOSE_ENV]: _reserved, ...rest } = env;
|
|
55
|
+
return rest;
|
|
56
|
+
}
|
|
@@ -93,6 +93,23 @@ type RewindCleanupQuarantine = (cleanup: Promise<void>) => void;
|
|
|
93
93
|
type RewindSessionRetirement = () => void;
|
|
94
94
|
type SessionPermissionInput = Extract<ProviderInput, { type: "session.permission" }>;
|
|
95
95
|
type Emit = (event: ProviderEvent) => void;
|
|
96
|
+
|
|
97
|
+
/** The surface a connection drives for every opened session, live or history-only. */
|
|
98
|
+
export interface OmpOpenedSession {
|
|
99
|
+
readonly persistenceSessionId: string | undefined;
|
|
100
|
+
openPaseoBrowser(url: string): Promise<void>;
|
|
101
|
+
setBrowserAuthorizationIssuer(issue: ((url: string) => string | undefined) | null): void;
|
|
102
|
+
publishOpened(requestId: string): Promise<void>;
|
|
103
|
+
prompt(input: SessionPromptInput): Promise<void>;
|
|
104
|
+
permission(input: SessionPermissionInput): Promise<void>;
|
|
105
|
+
configure(input: SessionConfigureInput): Promise<void>;
|
|
106
|
+
revert(input: SessionRevertInput): Promise<void>;
|
|
107
|
+
interrupt(input: SessionInterruptInput): Promise<void>;
|
|
108
|
+
close(input?: SessionCloseInput): Promise<void>;
|
|
109
|
+
abortOpen(): Promise<void>;
|
|
110
|
+
beginConnectionShutdown(): void;
|
|
111
|
+
}
|
|
112
|
+
|
|
96
113
|
const LOCAL_ONLY_SETTLE_MS = 5_000;
|
|
97
114
|
const AGENT_END_STATE_TIMEOUT_MS = 2_000;
|
|
98
115
|
const AGENT_END_HISTORY_TIMEOUT_MS = 2_000;
|
|
@@ -119,7 +136,7 @@ const MAX_PENDING_USER_BYTES = 2 * 1024 * 1024;
|
|
|
119
136
|
const MAX_UNCLAIMED_BRANCH_BYTES = 4 * 1024 * 1024;
|
|
120
137
|
class OmpCatalogEscape extends OmpPublicError {}
|
|
121
138
|
const MAX_REPLAY_MESSAGES = 100_000;
|
|
122
|
-
const REPLAY_TIMEOUT_MS = 20_000;
|
|
139
|
+
export const REPLAY_TIMEOUT_MS = 20_000;
|
|
123
140
|
const _OMP_ASK_USER_FREEFORM_SENTINEL = "✏️ Type custom response...";
|
|
124
141
|
const _MAX_FREEFORM_RESPONSE_BYTES = 64 * 1024;
|
|
125
142
|
const OMP_BUILTIN_COMMANDS: readonly OmpAvailableCommand[] = [
|
|
@@ -162,7 +179,7 @@ function applicableThinkingLevel(
|
|
|
162
179
|
return model?.reasoning === false ? undefined : level;
|
|
163
180
|
}
|
|
164
181
|
|
|
165
|
-
function fixedSessionMode(modeId = "full") {
|
|
182
|
+
export function fixedSessionMode(modeId = "full") {
|
|
166
183
|
const mode = OMP_MODES.find((candidate) => candidate.id === modeId);
|
|
167
184
|
if (!mode) throw new OmpPublicError("OMP mode is unavailable");
|
|
168
185
|
return {
|
|
@@ -194,7 +211,7 @@ export function ompPersistenceSessionId(input: SessionOpenInput): string | undef
|
|
|
194
211
|
}
|
|
195
212
|
}
|
|
196
213
|
|
|
197
|
-
async function authorizeNativeSession(
|
|
214
|
+
export async function authorizeNativeSession(
|
|
198
215
|
runtime: OmpRuntime,
|
|
199
216
|
sessionId: string,
|
|
200
217
|
cwd: string,
|
|
@@ -228,7 +245,7 @@ function retainedBytes(values: readonly unknown[], maxBytes: number): number {
|
|
|
228
245
|
return total;
|
|
229
246
|
}
|
|
230
247
|
|
|
231
|
-
async function waitForReplay<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
248
|
+
export async function waitForReplay<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
232
249
|
signal.throwIfAborted();
|
|
233
250
|
const aborted = Promise.withResolvers<never>();
|
|
234
251
|
const onAbort = () => aborted.reject(signal.reason);
|
|
@@ -271,7 +288,7 @@ async function settleSessionCleanup(promises: readonly Promise<void>[]): Promise
|
|
|
271
288
|
throw new AggregateError(failures, "OMP session initialization cleanup failed");
|
|
272
289
|
}
|
|
273
290
|
}
|
|
274
|
-
export class OmpProviderSession {
|
|
291
|
+
export class OmpProviderSession implements OmpOpenedSession {
|
|
275
292
|
readonly id: string;
|
|
276
293
|
readonly cwd: string;
|
|
277
294
|
|
|
@@ -390,7 +390,8 @@ export class OmpSubsessionProjector {
|
|
|
390
390
|
}
|
|
391
391
|
async replay(
|
|
392
392
|
messages: readonly OmpMessage[],
|
|
393
|
-
|
|
393
|
+
// Undefined replays the persisted journal only; history sessions have no live runtime to ask.
|
|
394
|
+
runtimeSession: OmpRuntimeSession | undefined,
|
|
394
395
|
runtime: OmpRuntime,
|
|
395
396
|
signal: AbortSignal,
|
|
396
397
|
): Promise<void> {
|
|
@@ -411,12 +412,14 @@ export class OmpSubsessionProjector {
|
|
|
411
412
|
0,
|
|
412
413
|
);
|
|
413
414
|
let snapshots: OmpSubagentSnapshot[] = [];
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
415
|
+
if (runtimeSession) {
|
|
416
|
+
try {
|
|
417
|
+
snapshots = await waitForReplay(runtimeSession.getSubagents(), signal);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
if (signal.aborted) throw error;
|
|
420
|
+
}
|
|
421
|
+
await this.replaySnapshots(snapshots, runtimeSession, runtime, visited, budget, signal);
|
|
418
422
|
}
|
|
419
|
-
await this.replaySnapshots(snapshots, runtimeSession, runtime, visited, budget, signal);
|
|
420
423
|
signal.throwIfAborted();
|
|
421
424
|
this.reconcileSnapshots(snapshots);
|
|
422
425
|
completed = true;
|