@omercnet/paseo-omp 5.1.1 → 5.2.0-next.362.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.
@@ -13,6 +13,7 @@ import type {
13
13
  OmpOperationalFailure,
14
14
  OmpOperationalFailureReporter,
15
15
  } from "../operational-failure-diagnostics";
16
+ import type { OmpSubagentControlRegistry } from "../subagents";
16
17
  import { discoverOmpCatalog } from "./catalog";
17
18
  import { normalizeOmpCatalogOptions } from "./config-normalization";
18
19
  import type { OmpMcpConnector } from "./host-tools";
@@ -34,6 +35,7 @@ import {
34
35
  } from "./security";
35
36
  import { type OmpOpenedSession, OmpProviderSession, ompPersistenceSessionId } from "./session";
36
37
  import { OmpHistorySession } from "./session-history";
38
+ import { OmpSessionLimit } from "./session-limit";
37
39
  import { isOmpHistoryOnlyOpen } from "./session-purpose";
38
40
 
39
41
  type ProviderConfigurationCompat = {
@@ -69,7 +71,6 @@ const SUPPORTED_INPUTS: Readonly<Record<string, true>> = {
69
71
  "session.can_stop_runtime": true,
70
72
  "session.close": true,
71
73
  };
72
- const MAX_CONNECTION_SESSIONS = 32;
73
74
  const MAX_ACTIVE_OPERATIONS = 128;
74
75
  const MAX_PROVIDER_INPUT_BYTES = 2 * 1024 * 1024;
75
76
  const MAX_NESTED_OPTION_BYTES = 256 * 1024;
@@ -723,6 +724,8 @@ export function createOmpConnection(
723
724
  console.error("OMP provider failure", diagnostic),
724
725
  reportOperationalFailure: OmpOperationalFailureReporter = () => {},
725
726
  resolveHostInheritEnv?: () => Promise<readonly string[]>,
727
+ sessionLimit = new OmpSessionLimit(),
728
+ subagentControlRegistry?: OmpSubagentControlRegistry,
726
729
  ): ProviderConnection {
727
730
  const errorDetails = (error: unknown, fallback: string): { message: string } => {
728
731
  if (isOmpPublicError(error)) return { message: error.message };
@@ -757,6 +760,7 @@ export function createOmpConnection(
757
760
  session: OmpOpenedSession;
758
761
  nativeSessionId?: string;
759
762
  removeBrowserAuthorization?: () => void;
763
+ removeSubagentControls?: () => void;
760
764
  }
761
765
  >();
762
766
  const opening = new Map<
@@ -767,6 +771,13 @@ export function createOmpConnection(
767
771
  string,
768
772
  { token: symbol; nativeSessionId?: string; cleanup?: Promise<void> }
769
773
  >();
774
+ // One session id is held by `sessions`, `opening` and `failedCleanup` at different moments, and
775
+ // by two of them at once while it opens or fails. After a failed close only unresolved cleanup
776
+ // stays counted, until it drains.
777
+ const untrackSessions = sessionLimit.track(() => {
778
+ if (closed) return failedCleanup.size;
779
+ return new Set([...sessions.keys(), ...opening.keys(), ...failedCleanup.keys()]).size;
780
+ });
770
781
  const shutdown = new AbortController();
771
782
  let catalogCleanup: Promise<void> | null = null;
772
783
  const activeOperations = new Set<Promise<void>>();
@@ -783,6 +794,7 @@ export function createOmpConnection(
783
794
  if (slot?.token !== token) return false;
784
795
  sessions.delete(sessionId);
785
796
  slot.removeBrowserAuthorization?.();
797
+ slot.removeSubagentControls?.();
786
798
  return true;
787
799
  };
788
800
 
@@ -809,7 +821,9 @@ export function createOmpConnection(
809
821
  ...(quarantinedNativeSessionId ? { nativeSessionId: quarantinedNativeSessionId } : {}),
810
822
  });
811
823
  nativeReservations.quarantine(quarantinedNativeSessionId, token, cleanup, () => {
812
- if (failedCleanup.get(sessionId)?.token === token) failedCleanup.delete(sessionId);
824
+ if (failedCleanup.get(sessionId)?.token !== token) return;
825
+ failedCleanup.delete(sessionId);
826
+ if (closed && failedCleanup.size === 0) untrackSessions();
813
827
  });
814
828
  };
815
829
 
@@ -879,6 +893,7 @@ export function createOmpConnection(
879
893
  query: input.query,
880
894
  limit: input.limit,
881
895
  sessionDir: listingConfig.sessionDir,
896
+ signal: shutdown.signal,
882
897
  })
883
898
  ).map((session) => ({
884
899
  persistence: { version: 1, data: { sessionId: session.id } },
@@ -906,8 +921,13 @@ export function createOmpConnection(
906
921
  requestFailure(input.requestId, new OmpPublicError("OMP session already exists"));
907
922
  return;
908
923
  }
909
- if (sessions.size + opening.size + failedCleanup.size >= MAX_CONNECTION_SESSIONS) {
910
- requestFailure(input.requestId, new OmpPublicError("OMP session limit reached"));
924
+ if (sessionLimit.openSessions >= sessionLimit.maxSessions) {
925
+ requestFailure(
926
+ input.requestId,
927
+ new OmpPublicError(
928
+ `OMP session limit reached (${sessionLimit.maxSessions} open sessions). Archive idle agents or raise the session limit in OMP → Plugin.`,
929
+ ),
930
+ );
911
931
  return;
912
932
  }
913
933
  const token = Symbol(input.sessionId);
@@ -978,6 +998,7 @@ export function createOmpConnection(
978
998
  environment,
979
999
  hostInheritEnv,
980
1000
  scheduler,
1001
+ controller.signal,
981
1002
  )
982
1003
  : await OmpProviderSession.open(
983
1004
  input,
@@ -1018,12 +1039,12 @@ export function createOmpConnection(
1018
1039
  nativeReservations.release(nativeSessionId, token);
1019
1040
  return;
1020
1041
  }
1021
- const browserAgentId = input.config.env.PASEO_AGENT_ID?.trim() || input.sessionId;
1042
+ const agentId = input.config.env.PASEO_AGENT_ID?.trim() || input.sessionId;
1022
1043
  // A history session has no live Paseo browser tools to authorize.
1023
1044
  const browserAuthorization = historyOnly
1024
1045
  ? undefined
1025
1046
  : browserAuthorizationRegistry?.register(
1026
- browserAgentId,
1047
+ agentId,
1027
1048
  session.openPaseoBrowser.bind(session),
1028
1049
  );
1029
1050
  session.setBrowserAuthorizationIssuer(browserAuthorization?.issue ?? null);
@@ -1033,11 +1054,17 @@ export function createOmpConnection(
1033
1054
  browserAuthorization.remove();
1034
1055
  }
1035
1056
  : undefined;
1057
+ // A history session has no runtime, so it has no subagents to control.
1058
+ const removeSubagentControls =
1059
+ session instanceof OmpProviderSession
1060
+ ? subagentControlRegistry?.register(agentId, session)
1061
+ : undefined;
1036
1062
  sessions.set(input.sessionId, {
1037
1063
  token,
1038
1064
  session,
1039
1065
  nativeSessionId,
1040
1066
  ...(removeBrowserAuthorization ? { removeBrowserAuthorization } : {}),
1067
+ ...(removeSubagentControls ? { removeSubagentControls } : {}),
1041
1068
  });
1042
1069
  await session.publishOpened(input.requestId);
1043
1070
  if (
@@ -1189,7 +1216,10 @@ export function createOmpConnection(
1189
1216
  shutdown.abort(shutdownReason);
1190
1217
  for (const { controller } of opening.values()) controller.abort(shutdownReason);
1191
1218
  for (const { session } of sessions.values()) session.beginConnectionShutdown();
1192
- for (const slot of sessions.values()) slot.removeBrowserAuthorization?.();
1219
+ for (const slot of sessions.values()) {
1220
+ slot.removeBrowserAuthorization?.();
1221
+ slot.removeSubagentControls?.();
1222
+ }
1193
1223
  const failures: unknown[] = [];
1194
1224
  const operationBatch = [...activeOperations];
1195
1225
  const operationResults = await Promise.allSettled(operationBatch);
@@ -1313,7 +1343,12 @@ export function createOmpConnection(
1313
1343
  return () => listeners.delete(listener);
1314
1344
  },
1315
1345
  close() {
1316
- closePromise ??= Promise.resolve().then(disposeConnection);
1346
+ closePromise ??= Promise.resolve()
1347
+ .then(disposeConnection)
1348
+ .then(untrackSessions, (error: unknown) => {
1349
+ if (failedCleanup.size === 0) untrackSessions();
1350
+ throw error;
1351
+ });
1317
1352
  return closePromise;
1318
1353
  },
1319
1354
  };
@@ -64,12 +64,15 @@ export async function terminatePosixProcessTree(
64
64
 
65
65
  export type ProcessTreeCleanup = "verified" | "uncertain" | "failed";
66
66
 
67
- export async function stopWindowsTree(pid: number): Promise<ProcessTreeCleanup> {
67
+ export async function stopWindowsTree(
68
+ pid: number,
69
+ spawnTaskkill: typeof spawn = spawn,
70
+ ): Promise<ProcessTreeCleanup> {
68
71
  const result = Promise.withResolvers<ProcessTreeCleanup>();
69
72
  const systemRoot = process.env.SystemRoot ?? WINDOWS_DEFAULT_SYSTEM_ROOT;
70
73
  let taskkill: ChildProcessWithoutNullStreams;
71
74
  try {
72
- taskkill = spawn(
75
+ taskkill = spawnTaskkill(
73
76
  join(systemRoot, "System32", "taskkill.exe"),
74
77
  ["/PID", String(pid), "/T", "/F"],
75
78
  {
@@ -99,13 +102,10 @@ export async function stopWindowsTree(pid: number): Promise<ProcessTreeCleanup>
99
102
  }, PROCESS_STOP_TIMEOUT_MS);
100
103
  taskkill.once("error", () => finish("failed"));
101
104
  taskkill.once("close", (code, signal) => {
102
- finish(
103
- code === 0 && signal === null
104
- ? "verified"
105
- : code === 128 && signal === null
106
- ? "uncertain"
107
- : "failed",
108
- );
105
+ // 128 means taskkill found no such process because it already exited; retrying only repeats
106
+ // 128. Callers still wait for the root OMP process's own exit, so a live root cannot pass.
107
+ // Descendants of an already exited root cannot be enumerated by taskkill and are not checked.
108
+ finish((code === 0 || code === 128) && signal === null ? "verified" : "failed");
109
109
  });
110
110
  return result.promise;
111
111
  }
@@ -71,6 +71,8 @@ export const OMP_SESSION_EVENT_POLICIES = {
71
71
  tool_execution_end: "normalize",
72
72
  auto_compaction_start: "normalize",
73
73
  auto_compaction_end: "normalize",
74
+ manual_compaction_start: "normalize",
75
+ manual_compaction_end: "normalize",
74
76
  auto_retry_start: "normalize",
75
77
  auto_retry_end: "normalize",
76
78
  retry_fallback_applied: "normalize",
@@ -128,6 +130,7 @@ export const OMP_RPC_COMMAND_POLICIES = {
128
130
  bash: "unsupported",
129
131
  abort_bash: "unsupported",
130
132
  get_session_stats: "implemented",
133
+ get_session_lease: "unsupported",
131
134
  export_html: "unsupported",
132
135
  switch_session: "unsupported",
133
136
  branch: "implemented",
@@ -142,15 +145,21 @@ export const OMP_RPC_COMMAND_POLICIES = {
142
145
  abort_and_restore_queue: "unsupported",
143
146
  btw: "unsupported",
144
147
  btw_cancel: "unsupported",
145
- cancel_subagent: "unsupported",
148
+ cancel_subagent: "implemented",
146
149
  fork: "unsupported",
147
150
  get_btw_history: "unsupported",
148
151
  get_logout_accounts: "unsupported",
149
152
  goal: "unsupported",
150
153
  live_mute: "unsupported",
154
+ live_connect: "unsupported",
151
155
  live_start: "unsupported",
152
156
  live_stop: "unsupported",
153
157
  logout: "unsupported",
158
+ navigate_tree: "unsupported",
159
+ abort_branch_summary: "unsupported",
160
+ set_label: "unsupported",
161
+ dictation_start: "unsupported",
162
+ dictation_stop: "unsupported",
154
163
  predict_word: "unsupported",
155
164
  predict_word_feedback: "unsupported",
156
165
  promote_queued_message: "unsupported",
@@ -158,7 +167,7 @@ export const OMP_RPC_COMMAND_POLICIES = {
158
167
  set_ask_dialog: "implemented",
159
168
  set_cache_warming: "unsupported",
160
169
  set_slow_mode: "unsupported",
161
- steer_subagent: "unsupported",
170
+ steer_subagent: "implemented",
162
171
  } as const;
163
172
  export const OMP_RPC_RESPONSE_POLICIES = OMP_RPC_COMMAND_POLICIES;
164
173
  export const OMP_PROTOCOL_VIOLATION_CATEGORIES = [
@@ -254,9 +263,14 @@ export const OMP_PROTOCOL_EVENT_TYPES = [
254
263
  "todo_auto_clear",
255
264
  "auto_compaction_start",
256
265
  "auto_compaction_end",
266
+ "manual_compaction_start",
267
+ "manual_compaction_end",
257
268
  "available_commands_update",
258
269
  "notice",
259
270
  "command_output",
271
+ "extension_error",
272
+ "session_info_update",
273
+ "config_update",
260
274
  "extension_ui_request",
261
275
  "prompt_result",
262
276
  "host_tool_call",
@@ -403,6 +417,8 @@ function eventSchemaClassification(
403
417
  case "cache_warming_start":
404
418
  case "cache_warming_end":
405
419
  case "queue_update":
420
+ case "session_info_update":
421
+ case "config_update":
406
422
  return { reason: "configuration-event-schema", expected: "valid-configuration-event" };
407
423
  case "extension_ui_request":
408
424
  return { reason: "extension-ui-schema", expected: "valid-extension-ui-event" };
@@ -1606,6 +1622,17 @@ const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
1606
1622
  errorMessage: boundedString(64 * 1024).optional(),
1607
1623
  skipped: z.boolean().optional(),
1608
1624
  }),
1625
+ z.object({
1626
+ type: z.literal("manual_compaction_start"),
1627
+ mode: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES).optional(),
1628
+ }),
1629
+ z.object({
1630
+ type: z.literal("manual_compaction_end"),
1631
+ result: OmpCompactionResultSchema.optional(),
1632
+ aborted: z.boolean(),
1633
+ skipped: z.boolean().optional(),
1634
+ errorMessage: boundedString(64 * 1024).optional(),
1635
+ }),
1609
1636
  z.object({
1610
1637
  type: z.literal("notice"),
1611
1638
  id: IDENTIFIER.optional(),
@@ -1799,6 +1826,14 @@ export const OmpRuntimeEventSchema = z.discriminatedUnion("type", [
1799
1826
  commands: z.array(OmpAvailableCommandSchema).max(MAX_ARRAY_ITEMS),
1800
1827
  }),
1801
1828
  z.object({ type: z.literal("command_output"), text: TEXT.optional() }),
1829
+ z.object({
1830
+ type: z.literal("extension_error"),
1831
+ extensionPath: boundedString(MAX_PATH_LENGTH),
1832
+ event: NAME,
1833
+ error: boundedString(64 * 1024),
1834
+ }),
1835
+ z.object({ type: z.literal("session_info_update") }),
1836
+ z.object({ type: z.literal("config_update") }),
1802
1837
  OmpExtensionUiRequestSchema,
1803
1838
  z.object({
1804
1839
  type: z.literal("prompt_result"),
@@ -1967,6 +2002,8 @@ export const OmpSubagentsResultSchema = z.object({
1967
2002
  )
1968
2003
  .max(1_024),
1969
2004
  });
2005
+ export const OmpCancelSubagentResultSchema = z.object({ cancelled: z.boolean() });
2006
+ export const OmpSteerSubagentResultSchema = z.object({}).strict().optional();
1970
2007
  export const OmpSubagentMessagesResultSchema = z.object({
1971
2008
  sessionFile: boundedString(MAX_PATH_LENGTH),
1972
2009
  fromByte: z.number().int().nonnegative(),
@@ -1997,6 +2034,7 @@ export function parseOmpHostToolAgentResult(value: unknown): OmpHostToolResult["
1997
2034
  export type OmpRpcEvent =
1998
2035
  | z.infer<typeof OmpRuntimeEventSchema>
1999
2036
  | { type: "prompt_error"; id: string; error: string; code?: string }
2037
+ | { type: "session_settled" }
2000
2038
  | { type: "process_exit"; error: string };
2001
2039
  export type OmpAgentSessionEvent = z.infer<typeof OmpAgentSessionEventSchema>;
2002
2040
  export type OmpSubagentSnapshot = z.infer<typeof OmpSubagentsResultSchema>["subagents"][number];
@@ -1090,8 +1090,11 @@ export class OmpRpcProcess {
1090
1090
  });
1091
1091
  return;
1092
1092
  }
1093
- // Emitted once the session has no live or background work. Nothing consumes it yet.
1094
- if (type === "session_settled") return;
1093
+ // Emitted when the session is quiet: nothing live, queued, or pending in the background.
1094
+ if (type === "session_settled") {
1095
+ this.emit({ type: "session_settled" });
1096
+ return;
1097
+ }
1095
1098
  if (type === "ready") {
1096
1099
  if (this.readyReceived) {
1097
1100
  this.recordProtocolViolation("duplicate-ready", {
@@ -16,6 +16,7 @@ import {
16
16
  OmpBranchMessagesResultSchema,
17
17
  type OmpBranchResult,
18
18
  OmpBranchResultSchema,
19
+ OmpCancelSubagentResultSchema,
19
20
  type OmpCompactionResult,
20
21
  OmpCompactionResultSchema,
21
22
  type OmpExtensionUiResponse,
@@ -41,6 +42,7 @@ import {
41
42
  OmpSessionStateSchema,
42
43
  type OmpSessionStats,
43
44
  OmpSessionStatsSchema,
45
+ OmpSteerSubagentResultSchema,
44
46
  type OmpSubagentMessagesResult,
45
47
  OmpSubagentMessagesResultSchema,
46
48
  type OmpSubagentSnapshot,
@@ -105,6 +107,9 @@ export interface OmpRuntimeSession {
105
107
  subagentId?: string;
106
108
  sessionFile?: string;
107
109
  }): Promise<OmpSubagentMessagesResult>;
110
+ /** Resolves `false` when OMP no longer lists the subagent as running for this session. */
111
+ cancelSubagent(subagentId: string): Promise<boolean>;
112
+ steerSubagent(subagentId: string, message: string): Promise<void>;
108
113
  prompt(
109
114
  message: string,
110
115
  images?: readonly OmpImage[],
@@ -466,6 +471,26 @@ class OmpRpcSession implements OmpRuntimeSession {
466
471
  );
467
472
  }
468
473
 
474
+ async cancelSubagent(subagentId: string): Promise<boolean> {
475
+ const safeId = validateBoundedText(subagentId, "subagent identifier", MAX_ID_LENGTH);
476
+ return OmpCancelSubagentResultSchema.parse(
477
+ await this.process.request({ type: "cancel_subagent", subagentId: safeId }),
478
+ ).cancelled;
479
+ }
480
+
481
+ async steerSubagent(subagentId: string, message: string): Promise<void> {
482
+ const safeId = validateBoundedText(subagentId, "subagent identifier", MAX_ID_LENGTH);
483
+ const safeMessage = validateBoundedText(message, "subagent steering message", MAX_TEXT_LENGTH);
484
+ if (!safeMessage.trim()) throw new Error("Invalid OMP subagent steering message");
485
+ OmpSteerSubagentResultSchema.parse(
486
+ await this.process.request({
487
+ type: "steer_subagent",
488
+ subagentId: safeId,
489
+ message: safeMessage,
490
+ }),
491
+ );
492
+ }
493
+
469
494
  async getBranchMessages(): Promise<Array<{ entryId: string; text: string }>> {
470
495
  const result = OmpBranchMessagesResultSchema.parse(
471
496
  await this.process.request({ type: "get_branch_messages" }),
@@ -5,6 +5,7 @@ import { z } from "zod";
5
5
  import type { OmpBrowserAuthorizationRegistry } from "../mcp-browser";
6
6
  import type { OmpOperationalFailureReporter } from "../operational-failure-diagnostics";
7
7
  import { probeOmpAvailability } from "../provider-diagnostics";
8
+ import type { OmpSubagentControlRegistry } from "../subagents";
8
9
  import { mergeOmpInheritedEnvironmentNames } from "./config-normalization";
9
10
  import { createOmpConnection, OmpNativeSessionReservations } from "./connection";
10
11
  import type { OmpMcpConnector } from "./host-tools";
@@ -12,6 +13,7 @@ import { OmpRpcRuntime, type OmpRuntime } from "./omp-rpc";
12
13
  import type { OmpProtocolViolationDiagnostic } from "./omp-rpc-protocol";
13
14
  import { parseOmpProviderOptions } from "./provider-options";
14
15
  import { boundedJsonBytes } from "./security";
16
+ import { OmpSessionLimit } from "./session-limit";
15
17
  import { OmpProviderOptionsSchema } from "./settings";
16
18
  import type { OmpTimelineScheduler } from "./timeline-projector";
17
19
 
@@ -116,11 +118,14 @@ export interface OmpProviderOptions {
116
118
  timelineScheduler?: OmpTimelineScheduler;
117
119
  replayTimeoutMs?: number;
118
120
  environment?: NodeJS.ProcessEnv;
121
+ platform?: NodeJS.Platform;
119
122
  /** Fixed, non-secret profile/store identity for providers whose configuration is server-owned. */
120
123
  catalogIdentity?: Readonly<Record<string, string>>;
121
124
  mcpInitializationTimeoutMs?: number;
122
125
  mcpConnector?: OmpMcpConnector;
123
126
  browserAuthorizationRegistry?: OmpBrowserAuthorizationRegistry;
127
+ /** Shared by every provider of one plugin instance; each provider has its own otherwise. */
128
+ sessionLimit?: OmpSessionLimit;
124
129
  reportProtocolViolation?: (diagnostic: OmpProtocolViolationDiagnostic) => void | Promise<void>;
125
130
  reportOperationalFailure?: OmpOperationalFailureReporter;
126
131
  availabilityProbe?: (
@@ -128,6 +133,7 @@ export interface OmpProviderOptions {
128
133
  timeoutMs: number | undefined,
129
134
  ) => Promise<ProviderAvailabilityCompat>;
130
135
  resolveHostInheritEnv?: () => Promise<readonly string[]>;
136
+ subagentControlRegistry?: OmpSubagentControlRegistry;
131
137
  }
132
138
 
133
139
  function stableJson(value: unknown): string {
@@ -150,6 +156,8 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
150
156
  reportProtocolViolation: options.reportProtocolViolation,
151
157
  });
152
158
  const nativeReservations = new OmpNativeSessionReservations();
159
+ const sessionLimit = options.sessionLimit ?? new OmpSessionLimit();
160
+ const platform = options.platform ?? process.platform;
153
161
  const checkAvailability: NonNullable<ProviderRegistrationCompat["checkAvailability"]> = async (
154
162
  catalogOptions,
155
163
  context,
@@ -159,6 +167,12 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
159
167
  }
160
168
  const providerOptions = parseOmpProviderOptions(catalogOptions.providerOptions);
161
169
  const environment = { ...(options.environment ?? process.env), ...providerOptions.env };
170
+ // Windows env names are case-insensitive, but a copy keeps only the literal key (often
171
+ // `Path`), so `environment.PATH` would miss it and OMP would look not installed.
172
+ if (platform === "win32" && environment.PATH === undefined) {
173
+ const pathKey = Object.keys(environment).find((name) => name.toUpperCase() === "PATH");
174
+ if (pathKey !== undefined) environment.PATH = environment[pathKey];
175
+ }
162
176
  const configuredCommand: readonly [string, ...string[]] = providerOptions.command?.[0]
163
177
  ? [providerOptions.command[0], ...providerOptions.command.slice(1)]
164
178
  : [environment.OMP_COMMAND || "omp"];
@@ -167,6 +181,7 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
167
181
  cwd:
168
182
  catalogOptions.scope === "workspace" && catalogOptions.cwd ? catalogOptions.cwd : homedir(),
169
183
  environment,
184
+ platform,
170
185
  timeoutMs: context?.timeoutMs,
171
186
  });
172
187
  };
@@ -216,6 +231,7 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
216
231
  const capabilities = CAPABILITIES.filter((capability) =>
217
232
  requestedCapabilities.has(capability),
218
233
  );
234
+ await sessionLimit.refresh();
219
235
  return createOmpConnection(
220
236
  runtime,
221
237
  capabilities,
@@ -229,6 +245,8 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
229
245
  undefined,
230
246
  options.reportOperationalFailure,
231
247
  options.resolveHostInheritEnv,
248
+ sessionLimit,
249
+ options.subagentControlRegistry,
232
250
  );
233
251
  },
234
252
  };
@@ -7,6 +7,7 @@ import type { OmpSessionUsage } from "./session-usage";
7
7
  import type { OmpTimelineProjector } from "./timeline-projector";
8
8
 
9
9
  type Emit = (event: ProviderEvent) => void;
10
+ export type CompactionOutcome = "completed" | "failed" | "canceled" | "skipped";
10
11
 
11
12
  export interface ActiveCompaction {
12
13
  id: string;
@@ -73,51 +74,54 @@ export class OmpSessionCompaction {
73
74
  });
74
75
  }
75
76
 
76
- handleAutoEnd(
77
+ handleEnd(
77
78
  turn: ActiveTurn,
78
- event: Extract<OmpRpcEvent, { type: "auto_compaction_end" }>,
79
- ): boolean {
79
+ event: Extract<OmpRpcEvent, { type: "auto_compaction_end" | "manual_compaction_end" }>,
80
+ ): CompactionOutcome | undefined {
80
81
  if (this.discardedEnds > 0) {
81
82
  this.discardedEnds -= 1;
82
- return false;
83
+ return undefined;
83
84
  }
84
85
  const operation = this.operation;
86
+ const trigger = event.type === "auto_compaction_end" ? "auto" : "manual";
85
87
  if (
86
- operation?.trigger !== "auto" ||
88
+ operation?.trigger !== trigger ||
87
89
  operation.turnId !== turn.turnId ||
88
90
  operation.generation !== turn.generation
89
91
  ) {
90
- return false;
92
+ return undefined;
91
93
  }
92
- if (event.action !== undefined && operation.action !== event.action) {
93
- this.retire("OMP emitted overlapping compactions");
94
- this.discardedEnds = 1;
95
- return false;
96
- }
97
- if (event.willRetry) {
98
- operation.retrying = true;
99
- return false;
100
- }
101
- if (operation.retrying) {
102
- operation.retrying = false;
103
- return false;
94
+ if (event.type === "auto_compaction_end") {
95
+ if (event.action !== undefined && operation.action !== event.action) {
96
+ this.retire("OMP emitted overlapping compactions");
97
+ this.discardedEnds = 1;
98
+ return undefined;
99
+ }
100
+ if (event.willRetry) {
101
+ operation.retrying = true;
102
+ return undefined;
103
+ }
104
+ if (operation.retrying) {
105
+ operation.retrying = false;
106
+ return undefined;
107
+ }
104
108
  }
105
- const state = event.aborted
109
+ const state: CompactionOutcome = event.aborted
106
110
  ? "canceled"
107
- : event.errorMessage
108
- ? "failed"
109
- : event.skipped
110
- ? "skipped"
111
+ : event.skipped
112
+ ? "skipped"
113
+ : event.errorMessage
114
+ ? "failed"
111
115
  : "completed";
112
116
  this.finish(state, {
113
117
  tokensBefore: event.result?.tokensBefore ?? event.result?.preTokens,
114
118
  message: event.errorMessage,
115
119
  });
116
- return true;
120
+ return state;
117
121
  }
118
122
 
119
123
  finish(
120
- state: "completed" | "failed" | "canceled" | "skipped",
124
+ state: CompactionOutcome,
121
125
  options: { tokensBefore?: number | null; message?: string } = {},
122
126
  ): void {
123
127
  const operation = this.operation;