@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/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.0-beta.5",
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.0-beta.5",
62
- "@getpaseo/client": "0.11.0-beta.5",
63
- "@getpaseo/plugin": "0.11.0-beta.5",
64
- "@getpaseo/server": "0.11.0-beta.5",
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
- ["install", "--omit=dev", "--ignore-scripts", "--no-package-lock", "--workspaces=false"],
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: OmpProviderSession;
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 session: OmpProviderSession | undefined;
914
+ let historyOnly = false;
915
+ let session: OmpOpenedSession | undefined;
913
916
  try {
917
+ historyOnly = isOmpHistoryOnlyOpen(input);
914
918
  nativeSessionId = ompPersistenceSessionId(input);
915
- if (nativeSessionId) {
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
- session = await OmpProviderSession.open(
959
- input,
960
- runtime,
961
- safeCapabilities,
962
- sessionEmit,
963
- transitionNativeSession,
964
- quarantineRewindCleanup,
965
- retireRewindSession,
966
- scheduler,
967
- replayTimeoutMs,
968
- controller.signal,
969
- environment,
970
- mcpConnector,
971
- mcpInitializationTimeoutMs,
972
- reportOperationalFailure,
973
- (await resolveHostInheritEnv?.()) ?? [],
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
- const browserAuthorization = browserAuthorizationRegistry?.register(
999
- browserAgentId,
1000
- session.openPaseoBrowser.bind(session),
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<OmpProviderSession>();
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
+ }