@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.
@@ -6,10 +6,12 @@ import type {
6
6
  ProviderInput,
7
7
  ProviderSessionConfig,
8
8
  } from "@getpaseo/plugin/server/provider";
9
+ import { isLiveOmpSubagentStatus } from "../../shared/subagents";
9
10
  import type {
10
11
  OmpOperationalFailure,
11
12
  OmpOperationalFailureReporter,
12
13
  } from "../operational-failure-diagnostics";
14
+ import { type OmpSubagentControls, OmpSubagentNotRunningError } from "../subagents";
13
15
  import {
14
16
  mapOmpModels,
15
17
  nativeOmpModelId,
@@ -73,15 +75,18 @@ import {
73
75
  assistantTerminalOutcome,
74
76
  classifyTerminalCandidate,
75
77
  createActiveTurn,
78
+ createNativeRunTurn,
79
+ type HeldAgentEnd,
76
80
  historyTerminalOutcome,
77
81
  nativeEntryId,
78
82
  type PendingUser,
79
83
  type TerminalCandidate,
84
+ takeRunEvidence,
80
85
  terminalOutcome,
81
86
  unknownTerminalOutcomeError,
82
87
  } from "./session-terminal";
83
88
  import { OmpSessionUsage } from "./session-usage";
84
- import { OmpSubsessionProjector } from "./subsessions";
89
+ import { OmpSubsessionProjector, ompSubagentHandle } from "./subsessions";
85
90
  import {
86
91
  defaultOmpTimelineScheduler,
87
92
  OmpTimelineProjector,
@@ -223,8 +228,9 @@ export async function authorizeNativeSession(
223
228
  sessionId: string,
224
229
  cwd: string,
225
230
  sessionDir?: string,
231
+ signal?: AbortSignal,
226
232
  ): Promise<OmpSessionDescriptor> {
227
- const matches = await runtime.listSessions({ sessionId, cwd, limit: 2, sessionDir });
233
+ const matches = await runtime.listSessions({ sessionId, cwd, limit: 2, sessionDir, signal });
228
234
  const descriptor = matches[0];
229
235
  if (matches.length !== 1 || descriptor?.id !== sessionId) {
230
236
  throw new OmpPublicError("OMP session could not be resolved in this workspace");
@@ -295,7 +301,7 @@ async function settleSessionCleanup(promises: readonly Promise<void>[]): Promise
295
301
  throw new AggregateError(failures, "OMP session initialization cleanup failed");
296
302
  }
297
303
  }
298
- export class OmpProviderSession implements OmpOpenedSession {
304
+ export class OmpProviderSession implements OmpOpenedSession, OmpSubagentControls {
299
305
  readonly id: string;
300
306
  readonly cwd: string;
301
307
 
@@ -330,6 +336,8 @@ export class OmpProviderSession implements OmpOpenedSession {
330
336
  private configRevision = 0;
331
337
  private recoveryUsesNativeConfig = false;
332
338
  private activeAbort: PendingAbort | null = null;
339
+ /** Frames of a run that started while the previous terminal was being confirmed or published. */
340
+ private runStartBacklog: OmpRpcEvent[] | null = null;
333
341
  private readonly usage: OmpSessionUsage;
334
342
  private readonly compaction: OmpSessionCompaction;
335
343
  private revertInFlight = false;
@@ -460,6 +468,36 @@ export class OmpProviderSession implements OmpOpenedSession {
460
468
  this.projector.setBrowserAuthorizationIssuer(issue);
461
469
  }
462
470
 
471
+ async cancelSubagent(handle: string): Promise<boolean> {
472
+ const target = await this.liveSubagent(handle);
473
+ return target ? await target.runtime.cancelSubagent(target.nativeId) : false;
474
+ }
475
+
476
+ async steerSubagent(handle: string, message: string): Promise<void> {
477
+ const target = await this.liveSubagent(handle);
478
+ if (!target) throw new OmpSubagentNotRunningError();
479
+ await target.runtime.steerSubagent(target.nativeId, message);
480
+ }
481
+
482
+ /**
483
+ * The native ID and runtime of a subagent this session's current runtime lists as running, or
484
+ * `null`. OMP repeats the check; this scopes the request to this session and answers exactly.
485
+ */
486
+ private async liveSubagent(
487
+ handle: string,
488
+ ): Promise<{ runtime: OmpRuntimeSession; nativeId: string } | null> {
489
+ if (this.closed || this.runtimeDead) return null;
490
+ const runtime = this.runtime;
491
+ const snapshots = await runtime.getSubagents();
492
+ if (runtime !== this.runtime || this.closed || this.runtimeDead) return null;
493
+ const live = snapshots.find(
494
+ (snapshot) =>
495
+ ompSubagentHandle(this.id, snapshot.id) === handle &&
496
+ isLiveOmpSubagentStatus(snapshot.status),
497
+ );
498
+ return live ? { runtime, nativeId: live.id } : null;
499
+ }
500
+
463
501
  private readonly imageMaterializer = new OmpImageMaterializer();
464
502
  static async open(
465
503
  input: SessionOpenInput,
@@ -505,6 +543,7 @@ export class OmpProviderSession implements OmpOpenedSession {
505
543
  resumeSessionId,
506
544
  input.config.cwd,
507
545
  normalizedConfig.sessionDir,
546
+ signal,
508
547
  )
509
548
  : undefined;
510
549
  validateOmpHostToolConfig(effectiveConfig);
@@ -1458,33 +1497,46 @@ export class OmpProviderSession implements OmpOpenedSession {
1458
1497
  } catch (error) {
1459
1498
  if (this.closed || this.runtimeDead || turn.generation !== this.generation) return;
1460
1499
  turn.manualCompactionPending = false;
1500
+ // OMP 18.13.3+ reports the outcome in manual_compaction_end before rejecting `compact`.
1501
+ const outcome = turn.manualCompactionOutcome;
1502
+ if (outcome === "skipped") {
1503
+ await this.finishTurn(turn, turn.interrupted ? "canceled" : "completed");
1504
+ return;
1505
+ }
1461
1506
  const raw = providerError(error, "OMP compaction failed");
1462
1507
  const failure = { message: this.dataFilter.text(raw.message, 4_096) };
1463
- const state = turn.interrupted ? "canceled" : "failed";
1508
+ const state = turn.interrupted || outcome === "canceled" ? "canceled" : "failed";
1464
1509
  this.compaction.finish(state, { message: failure.message });
1465
1510
  await this.finishTurn(turn, state, state === "failed" ? failure : undefined, true, true);
1466
1511
  }
1467
1512
  }
1468
1513
 
1469
1514
  async canStopRuntime(): Promise<boolean> {
1470
- const hasLocalWork =
1515
+ if (this.hasLocalRuntimeWork()) return false;
1516
+ const state = await this.runtime.getState();
1517
+ // OMP can start a run of its own while the state lookup is pending.
1518
+ if (this.hasLocalRuntimeWork()) return false;
1519
+ // OMP builds that predate these fields report neither, which keeps the runtime alive.
1520
+ return (
1521
+ state.isSettled === true &&
1522
+ state.hasPendingAsyncWork === false &&
1523
+ !state.isStreaming &&
1524
+ !state.isCompacting
1525
+ );
1526
+ }
1527
+
1528
+ private hasLocalRuntimeWork(): boolean {
1529
+ return (
1471
1530
  this.closed ||
1472
1531
  this.runtimeDead !== null ||
1473
1532
  this.recoveryPromise !== null ||
1474
1533
  this.activeTurn !== null ||
1534
+ this.runStartBacklog !== null ||
1475
1535
  this.activeAbort !== null ||
1476
1536
  this.revertInFlight ||
1477
1537
  this.configMutationInFlight ||
1478
1538
  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
1539
+ Boolean(this.subsessions?.hasActiveChildren())
1488
1540
  );
1489
1541
  }
1490
1542
 
@@ -2259,6 +2311,17 @@ export class OmpProviderSession implements OmpOpenedSession {
2259
2311
 
2260
2312
  private handleRuntimeEvent(event: OmpRpcEvent): void {
2261
2313
  if (this.closed) return;
2314
+ if (this.runStartBacklog) {
2315
+ this.bufferEvent(this.runStartBacklog, event);
2316
+ return;
2317
+ }
2318
+ // A run that starts while the previous terminal is being confirmed or published waits for it,
2319
+ // so neither the old turn's outcome nor the new run's frames are lost.
2320
+ const pendingTerminal = this.activeTurn?.terminalization ?? this.activeTurn?.agentEndCheck;
2321
+ if (event.type === "agent_start" && pendingTerminal) {
2322
+ this.deferRunStart(event, pendingTerminal);
2323
+ return;
2324
+ }
2262
2325
  if (
2263
2326
  event.type === "subagent_lifecycle" ||
2264
2327
  event.type === "subagent_progress" ||
@@ -2336,7 +2399,8 @@ export class OmpProviderSession implements OmpOpenedSession {
2336
2399
  event.type === "ttsr_triggered" ||
2337
2400
  event.type === "cache_warming_start" ||
2338
2401
  event.type === "cache_warming_end" ||
2339
- event.type === "queue_update"
2402
+ event.type === "queue_update" ||
2403
+ event.type === "session_info_update"
2340
2404
  ) {
2341
2405
  return;
2342
2406
  }
@@ -2353,7 +2417,8 @@ export class OmpProviderSession implements OmpOpenedSession {
2353
2417
  event.type === "auto_retry_end" ||
2354
2418
  event.type === "compaction_start" ||
2355
2419
  event.type === "compaction_end" ||
2356
- event.type === "advisor_yielded"
2420
+ event.type === "advisor_yielded" ||
2421
+ event.type === "extension_error"
2357
2422
  ) {
2358
2423
  this.projector.projectPassive(event);
2359
2424
  return;
@@ -2367,13 +2432,23 @@ export class OmpProviderSession implements OmpOpenedSession {
2367
2432
  this.scheduleCommittedConfigRefresh();
2368
2433
  return;
2369
2434
  }
2370
- if (event.type === "model_changed" || event.type === "thinking_level_changed") {
2435
+ if (
2436
+ event.type === "model_changed" ||
2437
+ event.type === "thinking_level_changed" ||
2438
+ event.type === "config_update"
2439
+ ) {
2371
2440
  this.scheduleCommittedConfigRefresh();
2372
2441
  return;
2373
2442
  }
2374
- const turn = this.activeTurn;
2443
+ const turn =
2444
+ this.activeTurn ?? (event.type === "agent_start" ? this.openNativeRunTurn() : null);
2375
2445
  if (!turn) {
2376
- if (event.type === "auto_compaction_start" || event.type === "auto_compaction_end") {
2446
+ if (
2447
+ event.type === "auto_compaction_start" ||
2448
+ event.type === "auto_compaction_end" ||
2449
+ event.type === "manual_compaction_start" ||
2450
+ event.type === "manual_compaction_end"
2451
+ ) {
2377
2452
  this.projector.projectPassive(event);
2378
2453
  }
2379
2454
  return;
@@ -2386,27 +2461,69 @@ export class OmpProviderSession implements OmpOpenedSession {
2386
2461
  return;
2387
2462
  }
2388
2463
  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);
2464
+ this.bufferEvent(turn.bufferedEvents, event);
2405
2465
  return;
2406
2466
  }
2407
2467
  this.handleTurnEvent(turn, event);
2408
2468
  }
2409
2469
 
2470
+ private bufferEvent(buffer: OmpRpcEvent[], event: OmpRpcEvent): void {
2471
+ if (
2472
+ buffer.length >= MAX_BUFFERED_TURN_EVENTS ||
2473
+ retainedBytes(buffer, MAX_BUFFERED_TURN_BYTES) +
2474
+ boundedJsonBytes(
2475
+ event,
2476
+ MAX_BUFFERED_TURN_BYTES,
2477
+ MAX_BUFFERED_VALUE_ITEMS,
2478
+ MAX_BUFFERED_TURN_BYTES,
2479
+ MAX_BUFFERED_VALUE_NODES,
2480
+ ) >
2481
+ MAX_BUFFERED_TURN_BYTES
2482
+ ) {
2483
+ this.handleRuntimeFailure();
2484
+ return;
2485
+ }
2486
+ buffer.push(event);
2487
+ }
2488
+
2489
+ private deferRunStart(event: OmpRpcEvent, pendingTerminal: Promise<void>): void {
2490
+ const backlog = [event];
2491
+ this.runStartBacklog = backlog;
2492
+ const replay = () => {
2493
+ if (this.runStartBacklog !== backlog) return;
2494
+ this.runStartBacklog = null;
2495
+ for (const buffered of backlog) this.handleRuntimeEvent(buffered);
2496
+ };
2497
+ void pendingTerminal.then(replay, replay);
2498
+ }
2499
+
2500
+ private scheduleHeldTurnCheck(turn: ActiveTurn, held: HeldAgentEnd): void {
2501
+ this.scheduler.set(() => this.checkHeldTurn(turn, held), USAGE_POLL_MS);
2502
+ }
2503
+
2504
+ /** Released OMP does not always write `session_settled` once a held run's async work is gone. */
2505
+ private async checkHeldTurn(turn: ActiveTurn, held: HeldAgentEnd): Promise<void> {
2506
+ if (turn.heldAgentEnd !== held || this.activeTurn !== turn) return;
2507
+ const state = await this.usage.boundedSnapshot(turn, FINAL_USAGE_WAIT_MS);
2508
+ if (turn.heldAgentEnd !== held || this.activeTurn !== turn) return;
2509
+ const settled =
2510
+ state?.isSettled === true &&
2511
+ state.hasPendingAsyncWork === false &&
2512
+ !state.isStreaming &&
2513
+ !state.isCompacting;
2514
+ if (settled) this.handleTurnEvent(turn, { type: "session_settled" });
2515
+ else this.scheduleHeldTurnCheck(turn, held);
2516
+ }
2517
+
2518
+ private openNativeRunTurn(): ActiveTurn {
2519
+ const turn = createNativeRunTurn(this.generation);
2520
+ this.activeTurn = turn;
2521
+ turn.acknowledged = true;
2522
+ this.startTurn(turn);
2523
+ this.projector.acceptLiveTurn(turn.turnId);
2524
+ return turn;
2525
+ }
2526
+
2410
2527
  private handleTurnEvent(
2411
2528
  turn: ActiveTurn,
2412
2529
  event: OmpRpcEvent,
@@ -2476,10 +2593,27 @@ export class OmpProviderSession implements OmpOpenedSession {
2476
2593
  this.compaction.start(turn, "auto", event.action);
2477
2594
  return;
2478
2595
  }
2479
- if (event.type === "auto_compaction_end") {
2480
- if (this.compaction.handleAutoEnd(turn, event)) {
2481
- this.usage.schedulePoll(turn, USAGE_REFRESH_MS);
2482
- }
2596
+ if (event.type === "manual_compaction_start") {
2597
+ turn.nativeActivity = true;
2598
+ this.cancelLocalOnlyCompletion(turn);
2599
+ // A no-op when this turn's own `compact` call already opened the operation.
2600
+ this.compaction.start(turn, "manual");
2601
+ return;
2602
+ }
2603
+ if (event.type === "auto_compaction_end" || event.type === "manual_compaction_end") {
2604
+ const outcome = this.compaction.handleEnd(turn, event);
2605
+ if (outcome && turn.manualCompaction) turn.manualCompactionOutcome = outcome;
2606
+ if (outcome) this.usage.schedulePoll(turn, USAGE_REFRESH_MS);
2607
+ return;
2608
+ }
2609
+ if (event.type === "session_settled") {
2610
+ // OMP is quiet, so the held end closes this stretch; a later run opens its own turn.
2611
+ const held = turn.heldAgentEnd;
2612
+ if (!held) return;
2613
+ turn.heldAgentEnd = undefined;
2614
+ Object.assign(turn, held.evidence);
2615
+ // Ownership is judged as of the held end's arrival, never from evidence that came later.
2616
+ this.acceptTerminal(turn, { ...held.event, isTerminal: true }, held.arrivalSequence);
2483
2617
  return;
2484
2618
  }
2485
2619
  if (event.type === "agent_end" && turn.manualCompactionPending) return;
@@ -2502,35 +2636,30 @@ export class OmpProviderSession implements OmpOpenedSession {
2502
2636
  return;
2503
2637
  }
2504
2638
  }
2639
+ if (event.type === "agent_start") {
2640
+ turn.heldAgentEnd = undefined;
2641
+ // Evidence is per run; records OMP writes between runs belong to neither.
2642
+ takeRunEvidence(turn);
2643
+ if (turn.agentEndPending && !turn.interrupted) {
2644
+ // A terminal waiting to retry its idle check is stale once another run starts.
2645
+ this.resetAgentEndProbe(turn);
2646
+ turn.deferredAgentEnd = undefined;
2647
+ this.usage.poll(turn);
2648
+ }
2649
+ }
2505
2650
  if (event.type === "agent_end") {
2506
2651
  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;
2652
+ const held = {
2653
+ event,
2654
+ arrivalSequence: turn.activitySequence,
2655
+ evidence: takeRunEvidence(turn),
2656
+ };
2657
+ turn.heldAgentEnd = held;
2658
+ this.scheduleHeldTurnCheck(turn, held);
2531
2659
  return;
2532
2660
  }
2533
- this.beginTerminalization(turn, candidate);
2661
+ turn.heldAgentEnd = undefined;
2662
+ this.acceptTerminal(turn, event, turn.activitySequence);
2534
2663
  return;
2535
2664
  }
2536
2665
  if (isNativeTurnActivity(event)) {
@@ -2563,9 +2692,39 @@ export class OmpProviderSession implements OmpOpenedSession {
2563
2692
  });
2564
2693
  throw error;
2565
2694
  }
2695
+ if (event.type === "tool_execution_start" && event.toolName === "task") {
2696
+ this.subsessions?.anchorRootTask(event.toolCallId);
2697
+ }
2566
2698
  if (event.type === "tool_execution_end") this.resumeDeferredAgentEnd();
2567
2699
  }
2568
2700
 
2701
+ private acceptTerminal(
2702
+ turn: ActiveTurn,
2703
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>,
2704
+ arrivalSequence: number,
2705
+ ): void {
2706
+ const candidate: TerminalCandidate = turn.interrupted
2707
+ ? { event, confidence: "interrupted", arrivalSequence }
2708
+ : classifyTerminalCandidate(turn, event, arrivalSequence);
2709
+ if (candidate.confidence === "ambiguous") {
2710
+ this.deferAmbiguousTerminal(turn, candidate);
2711
+ return;
2712
+ }
2713
+ turn.nativeActivity = true;
2714
+ this.cancelLocalOnlyCompletion(turn);
2715
+ if (
2716
+ (!turn.interrupted &&
2717
+ candidate.confidence !== "keyed" &&
2718
+ this.hasTerminalConflict(turn, false)) ||
2719
+ turn.steersInFlight > 0 ||
2720
+ turn.terminalizing
2721
+ ) {
2722
+ turn.deferredAgentEnd = candidate;
2723
+ return;
2724
+ }
2725
+ this.beginTerminalization(turn, candidate);
2726
+ }
2727
+
2569
2728
  private projectUserEcho(turn: ActiveTurn, message: OmpMessage): void {
2570
2729
  turn.userEchoObserved = true;
2571
2730
  const entryId = nativeEntryId(message);
@@ -3033,6 +3192,7 @@ export class OmpProviderSession implements OmpOpenedSession {
3033
3192
  if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
3034
3193
  if (
3035
3194
  candidate.confidence === "keyed" ||
3195
+ candidate.confidence === "native-run" ||
3036
3196
  (candidate.confidence === "initial-turn" && !turn.userEchoObserved)
3037
3197
  ) {
3038
3198
  void this.completeAgentEnd(turn, candidate.event);
@@ -3146,14 +3306,16 @@ export class OmpProviderSession implements OmpOpenedSession {
3146
3306
  return;
3147
3307
  }
3148
3308
  if (state) {
3149
- if (state.isStreaming || state.isCompacting) {
3309
+ // A run start buffered behind this check proves this end was the last one before it, so a
3310
+ // streaming state belongs to that new run, not to this turn.
3311
+ if ((state.isStreaming || state.isCompacting) && !this.runStartBacklog) {
3150
3312
  this.ignoreActiveTerminalCandidate(turn, candidate);
3151
3313
  return;
3152
3314
  }
3153
3315
  await this.completeAgentEnd(turn, candidate.event, true);
3154
3316
  return;
3155
3317
  }
3156
- if (turn.userEchoObserved) {
3318
+ if (turn.userEchoObserved && candidate.confidence !== "native-run") {
3157
3319
  await this.cancelIndeterminateTerminal(
3158
3320
  turn,
3159
3321
  "OMP agent_end state could not be confirmed",
@@ -3389,6 +3551,7 @@ export class OmpProviderSession implements OmpOpenedSession {
3389
3551
  throw error;
3390
3552
  }
3391
3553
  this.unclaimedBranchEntries.length = 0;
3554
+ turn.terminalEmitAttempted = true;
3392
3555
  this.emit({
3393
3556
  type: "session.turn",
3394
3557
  sessionId: this.id,
@@ -3398,17 +3561,42 @@ export class OmpProviderSession implements OmpOpenedSession {
3398
3561
  });
3399
3562
  this.runtimeTurnCompleted = true;
3400
3563
  if (this.activeTurn === turn) this.activeTurn = null;
3401
- })();
3564
+ })().catch(() => this.failTerminalization(turn));
3402
3565
  turn.terminalization = terminalization;
3403
3566
  return terminalization;
3404
3567
  }
3405
3568
 
3569
+ /** A turn whose terminal state could not be published must not keep owning the session. */
3570
+ private failTerminalization(turn: ActiveTurn): void {
3571
+ const message = "OMP turn could not be finalized";
3572
+ turn.terminal = true;
3573
+ if (this.activeTurn === turn) this.activeTurn = null;
3574
+ this.recordOperationalFailure({ category: "terminal-outcome", stage: "failed" });
3575
+ // Once the terminal emit was attempted, earlier listeners may already hold it; a second,
3576
+ // conflicting terminal would be worse than failing the runtime alone.
3577
+ if (!turn.terminalEmitAttempted) {
3578
+ try {
3579
+ this.emit({
3580
+ type: "session.turn",
3581
+ sessionId: this.id,
3582
+ turnId: turn.turnId,
3583
+ state: "failed",
3584
+ error: { message },
3585
+ });
3586
+ } catch {
3587
+ // The listener that failed may fail again; the runtime failure below still applies.
3588
+ }
3589
+ }
3590
+ this.handleRuntimeFailure(message);
3591
+ }
3592
+
3406
3593
  private invalidateRuntime(
3407
3594
  message: string,
3408
3595
  compactionState: "failed" | "canceled" = "failed",
3409
3596
  ): void {
3410
3597
  this.imageMaterializer.clear();
3411
3598
  if (this.closed || this.runtimeDead) return;
3599
+ this.runStartBacklog = null;
3412
3600
  this.compaction.resetDiscardedEnds();
3413
3601
  this.permissions.resolveAllPermissions();
3414
3602
  this.recoveryUsesNativeConfig ||=