@omercnet/paseo-omp 5.1.1-next.336.1 → 5.1.1-next.339.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-omp",
3
- "version": "5.1.1-next.336.1",
3
+ "version": "5.1.1-next.339.1",
4
4
  "type": "module",
5
5
  "description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
6
6
  "license": "MIT",
@@ -64,8 +64,8 @@
64
64
  "@getpaseo/client": "0.14.0",
65
65
  "@getpaseo/plugin": "0.14.0",
66
66
  "@getpaseo/server": "0.14.0",
67
- "@oh-my-pi/pi-agent-core": "18.12.6",
68
- "@oh-my-pi/pi-coding-agent": "18.12.6",
67
+ "@oh-my-pi/pi-agent-core": "18.14.0",
68
+ "@oh-my-pi/pi-coding-agent": "18.14.0",
69
69
  "@tanstack/react-query": "^5.102.3",
70
70
  "@types/node": "^24.10.1",
71
71
  "@types/react": "^19.2.18",
@@ -1,2 +1,2 @@
1
1
  /** Release Please keeps this bundle-safe constant aligned with package.json. */
2
- export const PASEO_OMP_PACKAGE_VERSION = "5.1.1-next.336.1"; // x-release-please-version
2
+ export const PASEO_OMP_PACKAGE_VERSION = "5.1.1-next.339.1"; // x-release-please-version
@@ -71,6 +71,8 @@ export const OMP_SESSION_EVENT_POLICIES = {
71
71
  tool_execution_end: "normalize",
72
72
  auto_compaction_start: "normalize",
73
73
  auto_compaction_end: "normalize",
74
+ manual_compaction_start: "normalize",
75
+ manual_compaction_end: "normalize",
74
76
  auto_retry_start: "normalize",
75
77
  auto_retry_end: "normalize",
76
78
  retry_fallback_applied: "normalize",
@@ -148,9 +150,15 @@ export const OMP_RPC_COMMAND_POLICIES = {
148
150
  get_logout_accounts: "unsupported",
149
151
  goal: "unsupported",
150
152
  live_mute: "unsupported",
153
+ live_connect: "unsupported",
151
154
  live_start: "unsupported",
152
155
  live_stop: "unsupported",
153
156
  logout: "unsupported",
157
+ navigate_tree: "unsupported",
158
+ abort_branch_summary: "unsupported",
159
+ set_label: "unsupported",
160
+ dictation_start: "unsupported",
161
+ dictation_stop: "unsupported",
154
162
  predict_word: "unsupported",
155
163
  predict_word_feedback: "unsupported",
156
164
  promote_queued_message: "unsupported",
@@ -254,9 +262,14 @@ export const OMP_PROTOCOL_EVENT_TYPES = [
254
262
  "todo_auto_clear",
255
263
  "auto_compaction_start",
256
264
  "auto_compaction_end",
265
+ "manual_compaction_start",
266
+ "manual_compaction_end",
257
267
  "available_commands_update",
258
268
  "notice",
259
269
  "command_output",
270
+ "extension_error",
271
+ "session_info_update",
272
+ "config_update",
260
273
  "extension_ui_request",
261
274
  "prompt_result",
262
275
  "host_tool_call",
@@ -403,6 +416,8 @@ function eventSchemaClassification(
403
416
  case "cache_warming_start":
404
417
  case "cache_warming_end":
405
418
  case "queue_update":
419
+ case "session_info_update":
420
+ case "config_update":
406
421
  return { reason: "configuration-event-schema", expected: "valid-configuration-event" };
407
422
  case "extension_ui_request":
408
423
  return { reason: "extension-ui-schema", expected: "valid-extension-ui-event" };
@@ -1606,6 +1621,17 @@ const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
1606
1621
  errorMessage: boundedString(64 * 1024).optional(),
1607
1622
  skipped: z.boolean().optional(),
1608
1623
  }),
1624
+ z.object({
1625
+ type: z.literal("manual_compaction_start"),
1626
+ mode: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES).optional(),
1627
+ }),
1628
+ z.object({
1629
+ type: z.literal("manual_compaction_end"),
1630
+ result: OmpCompactionResultSchema.optional(),
1631
+ aborted: z.boolean(),
1632
+ skipped: z.boolean().optional(),
1633
+ errorMessage: boundedString(64 * 1024).optional(),
1634
+ }),
1609
1635
  z.object({
1610
1636
  type: z.literal("notice"),
1611
1637
  id: IDENTIFIER.optional(),
@@ -1799,6 +1825,14 @@ export const OmpRuntimeEventSchema = z.discriminatedUnion("type", [
1799
1825
  commands: z.array(OmpAvailableCommandSchema).max(MAX_ARRAY_ITEMS),
1800
1826
  }),
1801
1827
  z.object({ type: z.literal("command_output"), text: TEXT.optional() }),
1828
+ z.object({
1829
+ type: z.literal("extension_error"),
1830
+ extensionPath: boundedString(MAX_PATH_LENGTH),
1831
+ event: NAME,
1832
+ error: boundedString(64 * 1024),
1833
+ }),
1834
+ z.object({ type: z.literal("session_info_update") }),
1835
+ z.object({ type: z.literal("config_update") }),
1802
1836
  OmpExtensionUiRequestSchema,
1803
1837
  z.object({
1804
1838
  type: z.literal("prompt_result"),
@@ -1997,6 +2031,7 @@ export function parseOmpHostToolAgentResult(value: unknown): OmpHostToolResult["
1997
2031
  export type OmpRpcEvent =
1998
2032
  | z.infer<typeof OmpRuntimeEventSchema>
1999
2033
  | { type: "prompt_error"; id: string; error: string; code?: string }
2034
+ | { type: "session_settled" }
2000
2035
  | { type: "process_exit"; error: string };
2001
2036
  export type OmpAgentSessionEvent = z.infer<typeof OmpAgentSessionEventSchema>;
2002
2037
  export type OmpSubagentSnapshot = z.infer<typeof OmpSubagentsResultSchema>["subagents"][number];
@@ -1090,8 +1090,11 @@ export class OmpRpcProcess {
1090
1090
  });
1091
1091
  return;
1092
1092
  }
1093
- // Emitted 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", {
@@ -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;
@@ -24,6 +24,7 @@ export function isRuntimeConfigEvent(event: OmpRpcEvent): boolean {
24
24
  return (
25
25
  event.type === "model_changed" ||
26
26
  event.type === "thinking_level_changed" ||
27
+ event.type === "config_update" ||
27
28
  event.type === "retry_fallback_applied" ||
28
29
  event.type === "retry_fallback_succeeded"
29
30
  );
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { ProviderError } from "@getpaseo/plugin/server/provider";
3
3
  import type { OmpMessage, OmpRpcEvent } from "./omp-rpc-protocol";
4
+ import type { CompactionOutcome } from "./session-compaction";
4
5
 
5
6
  export type PendingUser = {
6
7
  clientMessageId: string;
@@ -12,7 +13,7 @@ export type PendingUser = {
12
13
  type TerminalCorrelationEvidence = "fresh-native-user" | "current-assistant";
13
14
 
14
15
  type TerminalCorrelation = {
15
- policy: "initial-turn" | "ordered-legacy" | "native-command";
16
+ policy: "initial-turn" | "ordered-legacy" | "native-command" | "native-run";
16
17
  evidence: Map<TerminalCorrelationEvidence, number>;
17
18
  };
18
19
 
@@ -24,6 +25,7 @@ export type TerminalCandidate = {
24
25
  | "initial-turn"
25
26
  | "ordered-legacy"
26
27
  | "native-command"
28
+ | "native-run"
27
29
  | "interrupted"
28
30
  | "ambiguous";
29
31
  };
@@ -55,6 +57,7 @@ export type ActiveTurn = {
55
57
  usageSampleFloor: number;
56
58
  manualCompaction: boolean;
57
59
  manualCompactionPending: boolean;
60
+ manualCompactionOutcome?: CompactionOutcome;
58
61
  manualCompactionDeadlineTimer?: unknown;
59
62
  agentEndPending: boolean;
60
63
  agentEndRetryTimer?: unknown;
@@ -64,11 +67,14 @@ export type ActiveTurn = {
64
67
  terminalizing: boolean;
65
68
  terminalization?: Promise<void>;
66
69
  terminalOutcome?: TurnOutcome;
70
+ /** Listeners may hold this turn's terminal state from here on, even if publishing threw. */
71
+ terminalEmitAttempted: boolean;
67
72
  operationalTerminalStage?: "failed" | "unresolved";
68
73
  terminalWake?: VoidDeferred;
69
74
  steerReady: VoidDeferred;
70
75
  steersInFlight: number;
71
76
  deferredAgentEnd?: TerminalCandidate;
77
+ heldAgentEnd?: HeldAgentEnd;
72
78
  activeToolCallIds: Set<string>;
73
79
  bufferedEvents: OmpRpcEvent[];
74
80
  pendingUsers: PendingUser[];
@@ -82,6 +88,38 @@ export type ActiveTurn = {
82
88
  lastCompletedAssistantEntryId?: string;
83
89
  };
84
90
 
91
+ type RunEvidence = Pick<
92
+ ActiveTurn,
93
+ | "completedMessageCount"
94
+ | "streamedMessageEntryIds"
95
+ | "streamedMessageIdentityComplete"
96
+ | "lastCompletedAssistantOutcome"
97
+ | "lastCompletedAssistantEntryId"
98
+ >;
99
+
100
+ /** A run that ended with `isTerminal: false`, kept until another run starts or OMP settles. */
101
+ export type HeldAgentEnd = {
102
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>;
103
+ arrivalSequence: number;
104
+ evidence: RunEvidence;
105
+ };
106
+
107
+ /** Hands over the evidence of the run that just ended and starts the next run's evidence empty. */
108
+ export function takeRunEvidence(turn: ActiveTurn): RunEvidence {
109
+ const evidence: RunEvidence = {
110
+ completedMessageCount: turn.completedMessageCount,
111
+ streamedMessageEntryIds: turn.streamedMessageEntryIds.splice(0),
112
+ streamedMessageIdentityComplete: turn.streamedMessageIdentityComplete,
113
+ lastCompletedAssistantOutcome: turn.lastCompletedAssistantOutcome,
114
+ lastCompletedAssistantEntryId: turn.lastCompletedAssistantEntryId,
115
+ };
116
+ turn.completedMessageCount = 0;
117
+ turn.streamedMessageIdentityComplete = true;
118
+ turn.lastCompletedAssistantOutcome = undefined;
119
+ turn.lastCompletedAssistantEntryId = undefined;
120
+ return evidence;
121
+ }
122
+
85
123
  export type VoidDeferred = {
86
124
  promise: Promise<void>;
87
125
  resolve(value?: void | PromiseLike<void>): void;
@@ -249,6 +287,7 @@ export function createActiveTurn(
249
287
  usageSampleFloor: 0,
250
288
  agentEndPending: false,
251
289
  terminalizing: false,
290
+ terminalEmitAttempted: false,
252
291
  manualCompactionPending: manualCompaction,
253
292
  manualCompaction,
254
293
  activitySequence: 0,
@@ -276,6 +315,15 @@ export function createActiveTurn(
276
315
  ],
277
316
  };
278
317
  }
318
+ /** A run OMP started without a client prompt: an extension, goal, or background wake. */
319
+ export function createNativeRunTurn(generation: number): ActiveTurn {
320
+ return {
321
+ ...createActiveTurn("", "", generation, "native-run"),
322
+ promptResultEmitted: true,
323
+ pendingUsers: [],
324
+ };
325
+ }
326
+
279
327
  export function classifyTerminalCandidate(
280
328
  turn: ActiveTurn,
281
329
  event: Extract<OmpRpcEvent, { type: "agent_end" }>,
@@ -286,6 +334,9 @@ export function classifyTerminalCandidate(
286
334
  if (turn.terminalCorrelation.policy === "initial-turn") {
287
335
  return { event, confidence: "initial-turn", arrivalSequence };
288
336
  }
337
+ if (turn.terminalCorrelation.policy === "native-run") {
338
+ return { event, confidence: "native-run", arrivalSequence };
339
+ }
289
340
  const assistantSequence = turn.terminalCorrelation.evidence.get("current-assistant");
290
341
  if (
291
342
  turn.terminalCorrelation.policy === "native-command" &&
@@ -73,10 +73,13 @@ import {
73
73
  assistantTerminalOutcome,
74
74
  classifyTerminalCandidate,
75
75
  createActiveTurn,
76
+ createNativeRunTurn,
77
+ type HeldAgentEnd,
76
78
  historyTerminalOutcome,
77
79
  nativeEntryId,
78
80
  type PendingUser,
79
81
  type TerminalCandidate,
82
+ takeRunEvidence,
80
83
  terminalOutcome,
81
84
  unknownTerminalOutcomeError,
82
85
  } from "./session-terminal";
@@ -330,6 +333,8 @@ export class OmpProviderSession implements OmpOpenedSession {
330
333
  private configRevision = 0;
331
334
  private recoveryUsesNativeConfig = false;
332
335
  private activeAbort: PendingAbort | null = null;
336
+ /** Frames of a run that started while the previous terminal was being confirmed or published. */
337
+ private runStartBacklog: OmpRpcEvent[] | null = null;
333
338
  private readonly usage: OmpSessionUsage;
334
339
  private readonly compaction: OmpSessionCompaction;
335
340
  private revertInFlight = false;
@@ -1458,33 +1463,46 @@ export class OmpProviderSession implements OmpOpenedSession {
1458
1463
  } catch (error) {
1459
1464
  if (this.closed || this.runtimeDead || turn.generation !== this.generation) return;
1460
1465
  turn.manualCompactionPending = false;
1466
+ // OMP 18.13.3+ reports the outcome in manual_compaction_end before rejecting `compact`.
1467
+ const outcome = turn.manualCompactionOutcome;
1468
+ if (outcome === "skipped") {
1469
+ await this.finishTurn(turn, turn.interrupted ? "canceled" : "completed");
1470
+ return;
1471
+ }
1461
1472
  const raw = providerError(error, "OMP compaction failed");
1462
1473
  const failure = { message: this.dataFilter.text(raw.message, 4_096) };
1463
- const state = turn.interrupted ? "canceled" : "failed";
1474
+ const state = turn.interrupted || outcome === "canceled" ? "canceled" : "failed";
1464
1475
  this.compaction.finish(state, { message: failure.message });
1465
1476
  await this.finishTurn(turn, state, state === "failed" ? failure : undefined, true, true);
1466
1477
  }
1467
1478
  }
1468
1479
 
1469
1480
  async canStopRuntime(): Promise<boolean> {
1470
- const hasLocalWork =
1481
+ if (this.hasLocalRuntimeWork()) return false;
1482
+ const state = await this.runtime.getState();
1483
+ // OMP can start a run of its own while the state lookup is pending.
1484
+ if (this.hasLocalRuntimeWork()) return false;
1485
+ // OMP builds that predate these fields report neither, which keeps the runtime alive.
1486
+ return (
1487
+ state.isSettled === true &&
1488
+ state.hasPendingAsyncWork === false &&
1489
+ !state.isStreaming &&
1490
+ !state.isCompacting
1491
+ );
1492
+ }
1493
+
1494
+ private hasLocalRuntimeWork(): boolean {
1495
+ return (
1471
1496
  this.closed ||
1472
1497
  this.runtimeDead !== null ||
1473
1498
  this.recoveryPromise !== null ||
1474
1499
  this.activeTurn !== null ||
1500
+ this.runStartBacklog !== null ||
1475
1501
  this.activeAbort !== null ||
1476
1502
  this.revertInFlight ||
1477
1503
  this.configMutationInFlight ||
1478
1504
  this.permissions.hasPending() ||
1479
- Boolean(this.subsessions?.hasActiveChildren());
1480
- if (hasLocalWork) return false;
1481
- const state = await this.runtime.getState();
1482
- // OMP builds that predate these fields report neither, which keeps the runtime alive.
1483
- return (
1484
- state.isSettled === true &&
1485
- state.hasPendingAsyncWork === false &&
1486
- !state.isStreaming &&
1487
- !state.isCompacting
1505
+ Boolean(this.subsessions?.hasActiveChildren())
1488
1506
  );
1489
1507
  }
1490
1508
 
@@ -2259,6 +2277,17 @@ export class OmpProviderSession implements OmpOpenedSession {
2259
2277
 
2260
2278
  private handleRuntimeEvent(event: OmpRpcEvent): void {
2261
2279
  if (this.closed) return;
2280
+ if (this.runStartBacklog) {
2281
+ this.bufferEvent(this.runStartBacklog, event);
2282
+ return;
2283
+ }
2284
+ // A run that starts while the previous terminal is being confirmed or published waits for it,
2285
+ // so neither the old turn's outcome nor the new run's frames are lost.
2286
+ const pendingTerminal = this.activeTurn?.terminalization ?? this.activeTurn?.agentEndCheck;
2287
+ if (event.type === "agent_start" && pendingTerminal) {
2288
+ this.deferRunStart(event, pendingTerminal);
2289
+ return;
2290
+ }
2262
2291
  if (
2263
2292
  event.type === "subagent_lifecycle" ||
2264
2293
  event.type === "subagent_progress" ||
@@ -2336,7 +2365,8 @@ export class OmpProviderSession implements OmpOpenedSession {
2336
2365
  event.type === "ttsr_triggered" ||
2337
2366
  event.type === "cache_warming_start" ||
2338
2367
  event.type === "cache_warming_end" ||
2339
- event.type === "queue_update"
2368
+ event.type === "queue_update" ||
2369
+ event.type === "session_info_update"
2340
2370
  ) {
2341
2371
  return;
2342
2372
  }
@@ -2353,7 +2383,8 @@ export class OmpProviderSession implements OmpOpenedSession {
2353
2383
  event.type === "auto_retry_end" ||
2354
2384
  event.type === "compaction_start" ||
2355
2385
  event.type === "compaction_end" ||
2356
- event.type === "advisor_yielded"
2386
+ event.type === "advisor_yielded" ||
2387
+ event.type === "extension_error"
2357
2388
  ) {
2358
2389
  this.projector.projectPassive(event);
2359
2390
  return;
@@ -2367,13 +2398,23 @@ export class OmpProviderSession implements OmpOpenedSession {
2367
2398
  this.scheduleCommittedConfigRefresh();
2368
2399
  return;
2369
2400
  }
2370
- if (event.type === "model_changed" || event.type === "thinking_level_changed") {
2401
+ if (
2402
+ event.type === "model_changed" ||
2403
+ event.type === "thinking_level_changed" ||
2404
+ event.type === "config_update"
2405
+ ) {
2371
2406
  this.scheduleCommittedConfigRefresh();
2372
2407
  return;
2373
2408
  }
2374
- const turn = this.activeTurn;
2409
+ const turn =
2410
+ this.activeTurn ?? (event.type === "agent_start" ? this.openNativeRunTurn() : null);
2375
2411
  if (!turn) {
2376
- if (event.type === "auto_compaction_start" || event.type === "auto_compaction_end") {
2412
+ if (
2413
+ event.type === "auto_compaction_start" ||
2414
+ event.type === "auto_compaction_end" ||
2415
+ event.type === "manual_compaction_start" ||
2416
+ event.type === "manual_compaction_end"
2417
+ ) {
2377
2418
  this.projector.projectPassive(event);
2378
2419
  }
2379
2420
  return;
@@ -2386,27 +2427,69 @@ export class OmpProviderSession implements OmpOpenedSession {
2386
2427
  return;
2387
2428
  }
2388
2429
  if (turn.starting) {
2389
- if (
2390
- turn.bufferedEvents.length >= MAX_BUFFERED_TURN_EVENTS ||
2391
- retainedBytes(turn.bufferedEvents, MAX_BUFFERED_TURN_BYTES) +
2392
- boundedJsonBytes(
2393
- event,
2394
- MAX_BUFFERED_TURN_BYTES,
2395
- MAX_BUFFERED_VALUE_ITEMS,
2396
- MAX_BUFFERED_TURN_BYTES,
2397
- MAX_BUFFERED_VALUE_NODES,
2398
- ) >
2399
- MAX_BUFFERED_TURN_BYTES
2400
- ) {
2401
- this.handleRuntimeFailure();
2402
- return;
2403
- }
2404
- turn.bufferedEvents.push(event);
2430
+ this.bufferEvent(turn.bufferedEvents, event);
2405
2431
  return;
2406
2432
  }
2407
2433
  this.handleTurnEvent(turn, event);
2408
2434
  }
2409
2435
 
2436
+ private bufferEvent(buffer: OmpRpcEvent[], event: OmpRpcEvent): void {
2437
+ if (
2438
+ buffer.length >= MAX_BUFFERED_TURN_EVENTS ||
2439
+ retainedBytes(buffer, MAX_BUFFERED_TURN_BYTES) +
2440
+ boundedJsonBytes(
2441
+ event,
2442
+ MAX_BUFFERED_TURN_BYTES,
2443
+ MAX_BUFFERED_VALUE_ITEMS,
2444
+ MAX_BUFFERED_TURN_BYTES,
2445
+ MAX_BUFFERED_VALUE_NODES,
2446
+ ) >
2447
+ MAX_BUFFERED_TURN_BYTES
2448
+ ) {
2449
+ this.handleRuntimeFailure();
2450
+ return;
2451
+ }
2452
+ buffer.push(event);
2453
+ }
2454
+
2455
+ private deferRunStart(event: OmpRpcEvent, pendingTerminal: Promise<void>): void {
2456
+ const backlog = [event];
2457
+ this.runStartBacklog = backlog;
2458
+ const replay = () => {
2459
+ if (this.runStartBacklog !== backlog) return;
2460
+ this.runStartBacklog = null;
2461
+ for (const buffered of backlog) this.handleRuntimeEvent(buffered);
2462
+ };
2463
+ void pendingTerminal.then(replay, replay);
2464
+ }
2465
+
2466
+ private scheduleHeldTurnCheck(turn: ActiveTurn, held: HeldAgentEnd): void {
2467
+ this.scheduler.set(() => this.checkHeldTurn(turn, held), USAGE_POLL_MS);
2468
+ }
2469
+
2470
+ /** Released OMP does not always write `session_settled` once a held run's async work is gone. */
2471
+ private async checkHeldTurn(turn: ActiveTurn, held: HeldAgentEnd): Promise<void> {
2472
+ if (turn.heldAgentEnd !== held || this.activeTurn !== turn) return;
2473
+ const state = await this.usage.boundedSnapshot(turn, FINAL_USAGE_WAIT_MS);
2474
+ if (turn.heldAgentEnd !== held || this.activeTurn !== turn) return;
2475
+ const settled =
2476
+ state?.isSettled === true &&
2477
+ state.hasPendingAsyncWork === false &&
2478
+ !state.isStreaming &&
2479
+ !state.isCompacting;
2480
+ if (settled) this.handleTurnEvent(turn, { type: "session_settled" });
2481
+ else this.scheduleHeldTurnCheck(turn, held);
2482
+ }
2483
+
2484
+ private openNativeRunTurn(): ActiveTurn {
2485
+ const turn = createNativeRunTurn(this.generation);
2486
+ this.activeTurn = turn;
2487
+ turn.acknowledged = true;
2488
+ this.startTurn(turn);
2489
+ this.projector.acceptLiveTurn(turn.turnId);
2490
+ return turn;
2491
+ }
2492
+
2410
2493
  private handleTurnEvent(
2411
2494
  turn: ActiveTurn,
2412
2495
  event: OmpRpcEvent,
@@ -2476,10 +2559,27 @@ export class OmpProviderSession implements OmpOpenedSession {
2476
2559
  this.compaction.start(turn, "auto", event.action);
2477
2560
  return;
2478
2561
  }
2479
- if (event.type === "auto_compaction_end") {
2480
- if (this.compaction.handleAutoEnd(turn, event)) {
2481
- this.usage.schedulePoll(turn, USAGE_REFRESH_MS);
2482
- }
2562
+ if (event.type === "manual_compaction_start") {
2563
+ turn.nativeActivity = true;
2564
+ this.cancelLocalOnlyCompletion(turn);
2565
+ // A no-op when this turn's own `compact` call already opened the operation.
2566
+ this.compaction.start(turn, "manual");
2567
+ return;
2568
+ }
2569
+ if (event.type === "auto_compaction_end" || event.type === "manual_compaction_end") {
2570
+ const outcome = this.compaction.handleEnd(turn, event);
2571
+ if (outcome && turn.manualCompaction) turn.manualCompactionOutcome = outcome;
2572
+ if (outcome) this.usage.schedulePoll(turn, USAGE_REFRESH_MS);
2573
+ return;
2574
+ }
2575
+ if (event.type === "session_settled") {
2576
+ // OMP is quiet, so the held end closes this stretch; a later run opens its own turn.
2577
+ const held = turn.heldAgentEnd;
2578
+ if (!held) return;
2579
+ turn.heldAgentEnd = undefined;
2580
+ Object.assign(turn, held.evidence);
2581
+ // Ownership is judged as of the held end's arrival, never from evidence that came later.
2582
+ this.acceptTerminal(turn, { ...held.event, isTerminal: true }, held.arrivalSequence);
2483
2583
  return;
2484
2584
  }
2485
2585
  if (event.type === "agent_end" && turn.manualCompactionPending) return;
@@ -2502,35 +2602,30 @@ export class OmpProviderSession implements OmpOpenedSession {
2502
2602
  return;
2503
2603
  }
2504
2604
  }
2605
+ if (event.type === "agent_start") {
2606
+ turn.heldAgentEnd = undefined;
2607
+ // Evidence is per run; records OMP writes between runs belong to neither.
2608
+ takeRunEvidence(turn);
2609
+ if (turn.agentEndPending && !turn.interrupted) {
2610
+ // A terminal waiting to retry its idle check is stale once another run starts.
2611
+ this.resetAgentEndProbe(turn);
2612
+ turn.deferredAgentEnd = undefined;
2613
+ this.usage.poll(turn);
2614
+ }
2615
+ }
2505
2616
  if (event.type === "agent_end") {
2506
2617
  if (event.isTerminal === false) {
2507
- turn.completedMessageCount = 0;
2508
- turn.streamedMessageEntryIds.length = 0;
2509
- turn.streamedMessageIdentityComplete = true;
2510
- turn.lastCompletedAssistantOutcome = undefined;
2511
- turn.lastCompletedAssistantEntryId = undefined;
2512
- return;
2513
- }
2514
- const candidate: TerminalCandidate = turn.interrupted
2515
- ? { event, confidence: "interrupted", arrivalSequence: turn.activitySequence }
2516
- : classifyTerminalCandidate(turn, event);
2517
- if (candidate.confidence === "ambiguous") {
2518
- this.deferAmbiguousTerminal(turn, candidate);
2519
- return;
2520
- }
2521
- turn.nativeActivity = true;
2522
- this.cancelLocalOnlyCompletion(turn);
2523
- if (
2524
- (!turn.interrupted &&
2525
- candidate.confidence !== "keyed" &&
2526
- this.hasTerminalConflict(turn, false)) ||
2527
- turn.steersInFlight > 0 ||
2528
- turn.terminalizing
2529
- ) {
2530
- turn.deferredAgentEnd = candidate;
2618
+ const held = {
2619
+ event,
2620
+ arrivalSequence: turn.activitySequence,
2621
+ evidence: takeRunEvidence(turn),
2622
+ };
2623
+ turn.heldAgentEnd = held;
2624
+ this.scheduleHeldTurnCheck(turn, held);
2531
2625
  return;
2532
2626
  }
2533
- this.beginTerminalization(turn, candidate);
2627
+ turn.heldAgentEnd = undefined;
2628
+ this.acceptTerminal(turn, event, turn.activitySequence);
2534
2629
  return;
2535
2630
  }
2536
2631
  if (isNativeTurnActivity(event)) {
@@ -2566,6 +2661,33 @@ export class OmpProviderSession implements OmpOpenedSession {
2566
2661
  if (event.type === "tool_execution_end") this.resumeDeferredAgentEnd();
2567
2662
  }
2568
2663
 
2664
+ private acceptTerminal(
2665
+ turn: ActiveTurn,
2666
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>,
2667
+ arrivalSequence: number,
2668
+ ): void {
2669
+ const candidate: TerminalCandidate = turn.interrupted
2670
+ ? { event, confidence: "interrupted", arrivalSequence }
2671
+ : classifyTerminalCandidate(turn, event, arrivalSequence);
2672
+ if (candidate.confidence === "ambiguous") {
2673
+ this.deferAmbiguousTerminal(turn, candidate);
2674
+ return;
2675
+ }
2676
+ turn.nativeActivity = true;
2677
+ this.cancelLocalOnlyCompletion(turn);
2678
+ if (
2679
+ (!turn.interrupted &&
2680
+ candidate.confidence !== "keyed" &&
2681
+ this.hasTerminalConflict(turn, false)) ||
2682
+ turn.steersInFlight > 0 ||
2683
+ turn.terminalizing
2684
+ ) {
2685
+ turn.deferredAgentEnd = candidate;
2686
+ return;
2687
+ }
2688
+ this.beginTerminalization(turn, candidate);
2689
+ }
2690
+
2569
2691
  private projectUserEcho(turn: ActiveTurn, message: OmpMessage): void {
2570
2692
  turn.userEchoObserved = true;
2571
2693
  const entryId = nativeEntryId(message);
@@ -3033,6 +3155,7 @@ export class OmpProviderSession implements OmpOpenedSession {
3033
3155
  if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
3034
3156
  if (
3035
3157
  candidate.confidence === "keyed" ||
3158
+ candidate.confidence === "native-run" ||
3036
3159
  (candidate.confidence === "initial-turn" && !turn.userEchoObserved)
3037
3160
  ) {
3038
3161
  void this.completeAgentEnd(turn, candidate.event);
@@ -3146,14 +3269,16 @@ export class OmpProviderSession implements OmpOpenedSession {
3146
3269
  return;
3147
3270
  }
3148
3271
  if (state) {
3149
- if (state.isStreaming || state.isCompacting) {
3272
+ // A run start buffered behind this check proves this end was the last one before it, so a
3273
+ // streaming state belongs to that new run, not to this turn.
3274
+ if ((state.isStreaming || state.isCompacting) && !this.runStartBacklog) {
3150
3275
  this.ignoreActiveTerminalCandidate(turn, candidate);
3151
3276
  return;
3152
3277
  }
3153
3278
  await this.completeAgentEnd(turn, candidate.event, true);
3154
3279
  return;
3155
3280
  }
3156
- if (turn.userEchoObserved) {
3281
+ if (turn.userEchoObserved && candidate.confidence !== "native-run") {
3157
3282
  await this.cancelIndeterminateTerminal(
3158
3283
  turn,
3159
3284
  "OMP agent_end state could not be confirmed",
@@ -3389,6 +3514,7 @@ export class OmpProviderSession implements OmpOpenedSession {
3389
3514
  throw error;
3390
3515
  }
3391
3516
  this.unclaimedBranchEntries.length = 0;
3517
+ turn.terminalEmitAttempted = true;
3392
3518
  this.emit({
3393
3519
  type: "session.turn",
3394
3520
  sessionId: this.id,
@@ -3398,17 +3524,42 @@ export class OmpProviderSession implements OmpOpenedSession {
3398
3524
  });
3399
3525
  this.runtimeTurnCompleted = true;
3400
3526
  if (this.activeTurn === turn) this.activeTurn = null;
3401
- })();
3527
+ })().catch(() => this.failTerminalization(turn));
3402
3528
  turn.terminalization = terminalization;
3403
3529
  return terminalization;
3404
3530
  }
3405
3531
 
3532
+ /** A turn whose terminal state could not be published must not keep owning the session. */
3533
+ private failTerminalization(turn: ActiveTurn): void {
3534
+ const message = "OMP turn could not be finalized";
3535
+ turn.terminal = true;
3536
+ if (this.activeTurn === turn) this.activeTurn = null;
3537
+ this.recordOperationalFailure({ category: "terminal-outcome", stage: "failed" });
3538
+ // Once the terminal emit was attempted, earlier listeners may already hold it; a second,
3539
+ // conflicting terminal would be worse than failing the runtime alone.
3540
+ if (!turn.terminalEmitAttempted) {
3541
+ try {
3542
+ this.emit({
3543
+ type: "session.turn",
3544
+ sessionId: this.id,
3545
+ turnId: turn.turnId,
3546
+ state: "failed",
3547
+ error: { message },
3548
+ });
3549
+ } catch {
3550
+ // The listener that failed may fail again; the runtime failure below still applies.
3551
+ }
3552
+ }
3553
+ this.handleRuntimeFailure(message);
3554
+ }
3555
+
3406
3556
  private invalidateRuntime(
3407
3557
  message: string,
3408
3558
  compactionState: "failed" | "canceled" = "failed",
3409
3559
  ): void {
3410
3560
  this.imageMaterializer.clear();
3411
3561
  if (this.closed || this.runtimeDead) return;
3562
+ this.runStartBacklog = null;
3412
3563
  this.compaction.resetDiscardedEnds();
3413
3564
  this.permissions.resolveAllPermissions();
3414
3565
  this.recoveryUsesNativeConfig ||=
@@ -467,6 +467,9 @@ export class OmpTimelineProjector {
467
467
  event.type === "auto_compaction_end" ||
468
468
  event.type === "compaction_start" ||
469
469
  event.type === "compaction_end" ||
470
+ event.type === "manual_compaction_start" ||
471
+ event.type === "manual_compaction_end" ||
472
+ event.type === "extension_error" ||
470
473
  event.type === "advisor_yielded" ||
471
474
  event.type === "config_warnings_changed" ||
472
475
  event.type === "advisor_cost_changed" ||
@@ -743,6 +746,19 @@ export class OmpTimelineProjector {
743
746
  });
744
747
  return;
745
748
  }
749
+ if (event.type === "extension_error") {
750
+ this.noticeSequence += 1;
751
+ this.publish({
752
+ type: "notification",
753
+ id: `omp:extension-error:${this.noticeSequence}`,
754
+ level: "error",
755
+ message: this.dataFilter.text(
756
+ `OMP extension ${event.extensionPath} failed in ${event.event}: ${event.error}`,
757
+ 64 * 1024,
758
+ ),
759
+ });
760
+ return;
761
+ }
746
762
  if (event.type === "advisor_yielded") {
747
763
  this.noticeSequence += 1;
748
764
  this.publish({
@@ -753,7 +769,11 @@ export class OmpTimelineProjector {
753
769
  });
754
770
  return;
755
771
  }
756
- if (event.type === "auto_compaction_start" || event.type === "compaction_start") {
772
+ if (
773
+ event.type === "auto_compaction_start" ||
774
+ event.type === "compaction_start" ||
775
+ event.type === "manual_compaction_start"
776
+ ) {
757
777
  const trigger = event.type === "auto_compaction_start" ? "auto" : "manual";
758
778
  const action = event.type === "auto_compaction_start" ? event.action : undefined;
759
779
  if (this.discardedCompactionEnds > 0) {
@@ -781,7 +801,11 @@ export class OmpTimelineProjector {
781
801
  this.publish({ type: "compaction", id: slot.id, status: "loading", trigger });
782
802
  return;
783
803
  }
784
- if (event.type === "auto_compaction_end" || event.type === "compaction_end") {
804
+ if (
805
+ event.type === "auto_compaction_end" ||
806
+ event.type === "compaction_end" ||
807
+ event.type === "manual_compaction_end"
808
+ ) {
785
809
  if (this.discardedCompactionEnds > 0) {
786
810
  this.discardedCompactionEnds -= 1;
787
811
  return;
@@ -803,7 +827,7 @@ export class OmpTimelineProjector {
803
827
  this.discardedCompactionEnds = 1;
804
828
  return;
805
829
  }
806
- if (event.willRetry) {
830
+ if (event.type !== "manual_compaction_end" && event.willRetry) {
807
831
  slot.retrying = true;
808
832
  return;
809
833
  }
@@ -826,10 +850,9 @@ export class OmpTimelineProjector {
826
850
  type: "notification",
827
851
  id: `${slot.id}:skipped`,
828
852
  level: "warning",
829
- message: "OMP compaction was skipped",
853
+ message: this.dataFilter.text(event.errorMessage ?? "OMP compaction was skipped", 4_096),
830
854
  });
831
- }
832
- if (event.aborted || event.errorMessage) {
855
+ } else if (event.aborted || event.errorMessage) {
833
856
  this.publish({
834
857
  type: "error",
835
858
  id: `${slot.id}:error`,