@omercnet/paseo-omp 1.3.1 → 1.3.2-next.231.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 +1 -1
- package/README.md +5 -5
- package/index.server.ts +4 -4
- package/npm-shrinkwrap.json +1253 -0
- package/package.json +10 -9
- package/scripts/prepare-dependencies.mjs +1 -1
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omercnet/paseo-omp",
|
|
3
|
-
"version": "1.3.1",
|
|
3
|
+
"version": "1.3.2-next.231.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,10 +38,11 @@
|
|
|
38
38
|
"server",
|
|
39
39
|
"shared",
|
|
40
40
|
"paseo-plugin.json",
|
|
41
|
-
"OVERVIEW.md"
|
|
41
|
+
"OVERVIEW.md",
|
|
42
|
+
"npm-shrinkwrap.json"
|
|
42
43
|
],
|
|
43
44
|
"scripts": {
|
|
44
|
-
"check": "biome check .",
|
|
45
|
+
"check": "biome check . && node ../scripts/shrinkwrap.mjs --check .",
|
|
45
46
|
"check:write": "biome check --write .",
|
|
46
47
|
"test": "vitest run",
|
|
47
48
|
"test:coverage": "PASEO_OMP_COVERAGE=1 vitest run --coverage",
|
|
@@ -52,16 +53,16 @@
|
|
|
52
53
|
"typecheck": "tsc --noEmit -p tsconfig.client.json && tsc --noEmit -p tsconfig.server.json && tsc --noEmit -p tsconfig.tests.json"
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
|
-
"@getpaseo/protocol": "0.11.
|
|
56
|
+
"@getpaseo/protocol": "0.11.1",
|
|
56
57
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
57
58
|
"yaml": "^2.9.0"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@biomejs/biome": "^2.5.10",
|
|
61
|
-
"@getpaseo/cli": "0.11.
|
|
62
|
-
"@getpaseo/client": "0.11.
|
|
63
|
-
"@getpaseo/plugin": "0.11.
|
|
64
|
-
"@getpaseo/server": "0.11.
|
|
62
|
+
"@getpaseo/cli": "0.11.1",
|
|
63
|
+
"@getpaseo/client": "0.11.1",
|
|
64
|
+
"@getpaseo/plugin": "0.11.1",
|
|
65
|
+
"@getpaseo/server": "0.11.1",
|
|
65
66
|
"@oh-my-pi/pi-agent-core": "18.2.0",
|
|
66
67
|
"@oh-my-pi/pi-coding-agent": "18.2.0",
|
|
67
68
|
"@tanstack/react-query": "^5.102.3",
|
|
@@ -69,9 +70,9 @@
|
|
|
69
70
|
"@types/react": "^19.2.18",
|
|
70
71
|
"@vitest/coverage-v8": "^5.0.0",
|
|
71
72
|
"esbuild": "0.28.2",
|
|
73
|
+
"pino": "10.3.1",
|
|
72
74
|
"react": "19.1.0",
|
|
73
75
|
"react-native": "0.81.5",
|
|
74
|
-
"pino": "10.3.1",
|
|
75
76
|
"tsx": "^4.20.6",
|
|
76
77
|
"typescript": "^7.0.2",
|
|
77
78
|
"vitest": "^5.0.0",
|
|
@@ -42,7 +42,7 @@ export function prepareDependencies(root = projectRoot, execute = execFileSync)
|
|
|
42
42
|
stageManifest(root, catalog, () => {
|
|
43
43
|
execute(
|
|
44
44
|
process.platform === "win32" ? "npm.cmd" : "npm",
|
|
45
|
-
["
|
|
45
|
+
["ci", "--omit=dev", "--ignore-scripts", "--workspaces=false"],
|
|
46
46
|
{
|
|
47
47
|
cwd: root,
|
|
48
48
|
stdio: "inherit",
|
|
@@ -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.1"; // x-release-please-version
|
|
2
|
+
export const PASEO_OMP_PACKAGE_VERSION = "1.3.2-next.231.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
|
+
}
|