@leanandmean/coding-agent 0.74.1-scramjet.73 → 0.74.1-scramjet.74

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.
@@ -86,6 +86,15 @@ function rejectDeferred(deferred, error) {
86
86
  function settleDeferred(deferred, error) {
87
87
  return error ? rejectDeferred(deferred, error) : resolveDeferred(deferred, undefined);
88
88
  }
89
+ function createRetryChain() {
90
+ return {
91
+ ...createDeferred(),
92
+ result: createDeferred(),
93
+ executions: 0,
94
+ terminal: false,
95
+ cancelled: false,
96
+ };
97
+ }
89
98
  function validRecordedCost(value) {
90
99
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
91
100
  }
@@ -320,6 +329,8 @@ export class AgentSession {
320
329
  this._agentEventQueue = processing;
321
330
  if (harnessAck)
322
331
  harnessAck.persistenceTail = processing;
332
+ if (runSettlement && this.agent.getRunExecution())
333
+ runSettlement.processingTail = processing;
323
334
  // Keep queue alive if an event handler fails
324
335
  processing.catch(() => { });
325
336
  // SCRAMJET-DIVERGENCE: turn_end tool/prompt changes must settle before Agent snapshots
@@ -396,11 +407,44 @@ export class AgentSession {
396
407
  if (event.type === "agent_start") {
397
408
  // A retry continuation's run adopts the chain its originating prompt awaits; the chain stays
398
409
  // bound to the retry phase until this start consumes it, however late Agent emits it.
399
- const chain = this._retry?.phase === "continuing" ? this._retry.chain : createDeferred();
410
+ const chain = this._pendingAgentRunStart?.chain ??
411
+ (this._retry?.phase === "continuing" ? this._retry.chain : createRetryChain());
400
412
  this._unsettledRetryChains.add(chain);
401
- const run = { ...createDeferred(), chain };
413
+ const run = {
414
+ ...createDeferred(),
415
+ chain,
416
+ execution: this._pendingAgentRunStart?.execution ?? createDeferred(),
417
+ processingTail: Promise.resolve(),
418
+ };
419
+ if (!this._pendingAgentRunStart) {
420
+ chain.executions++;
421
+ void this.agent
422
+ .getRunExecution()
423
+ .catch((error) => {
424
+ chain.error ??= error instanceof Error ? error : new Error(String(error));
425
+ })
426
+ .then(async () => {
427
+ resolveDeferred(run.execution, undefined);
428
+ try {
429
+ await run.processingTail;
430
+ }
431
+ catch (error) {
432
+ chain.error ??= error instanceof Error ? error : new Error(String(error));
433
+ }
434
+ finally {
435
+ chain.executions--;
436
+ this._trySettleRetryChain(chain);
437
+ }
438
+ });
439
+ }
402
440
  this._activeAgentRunSettlement = run;
441
+ void run.execution.promise.then(() => {
442
+ if (this._activeAgentRunSettlement === run)
443
+ this._activeAgentRunSettlement = undefined;
444
+ });
403
445
  this._unsettledAgentRuns.add(run);
446
+ if (chain.cancelled)
447
+ this.agent.abort();
404
448
  const reservation = this._pendingAgentRunStart;
405
449
  if (reservation) {
406
450
  this._pendingAgentRunStart = undefined;
@@ -410,9 +454,9 @@ export class AgentSession {
410
454
  return run;
411
455
  }
412
456
  const run = this._activeAgentRunSettlement;
413
- if (event.type === "agent_end" && this._activeAgentRunSettlement === run) {
414
- this._activeAgentRunSettlement = undefined;
415
- }
457
+ // An awaited end listener can still fail and emit another end before execution unwinds.
458
+ if (event.type === "agent_end" && run)
459
+ run.chain.terminal = false;
416
460
  return run;
417
461
  }
418
462
  _settleAgentRun(run, error) {
@@ -420,17 +464,50 @@ export class AgentSession {
420
464
  this._unsettledAgentRuns.delete(run);
421
465
  }
422
466
  _settleRetryChain(chain, error) {
423
- if (settleDeferred(chain, error))
467
+ chain.terminal = true;
468
+ if (error)
469
+ chain.error = error;
470
+ this._trySettleRetryChain(chain);
471
+ }
472
+ _trySettleRetryChain(chain) {
473
+ if (!chain.terminal || chain.executions > 0 || chain.handoff)
474
+ return;
475
+ if (chain.error || chain.resultError) {
476
+ rejectDeferred(chain.result, chain.error ?? chain.resultError);
477
+ }
478
+ else {
479
+ const assistantMessage = chain.assistantMessage;
480
+ const result = chain.cancelled || assistantMessage?.stopReason === "aborted"
481
+ ? { status: "cancelled", assistantMessage }
482
+ : chain.errorMessage || !assistantMessage || assistantMessage.stopReason === "error"
483
+ ? {
484
+ status: "failed",
485
+ assistantMessage,
486
+ errorMessage: chain.errorMessage ??
487
+ assistantMessage?.errorMessage ??
488
+ "Run ended without a provider assistant result.",
489
+ }
490
+ : { status: "completed", assistantMessage };
491
+ resolveDeferred(chain.result, result);
492
+ }
493
+ if (settleDeferred(chain, chain.error))
424
494
  this._unsettledRetryChains.delete(chain);
425
495
  }
426
496
  _completeAgentRun(run, error) {
427
497
  this._settleAgentRun(run, error);
428
498
  this._settleRetryChain(run.chain, error);
429
499
  }
430
- _reserveAgentRunStart() {
500
+ _reserveAgentRunStart(chain = createRetryChain()) {
501
+ if (this._disposed)
502
+ throw new Error("AgentSession disposed before run start.");
431
503
  if (this._pendingAgentRunStart)
432
504
  throw new Error("An Agent prompt is already waiting to start.");
433
- const reservation = createDeferred();
505
+ this._unsettledRetryChains.add(chain);
506
+ const reservation = {
507
+ ...createDeferred(),
508
+ chain,
509
+ execution: createDeferred(),
510
+ };
434
511
  this._pendingAgentRunStart = reservation;
435
512
  return reservation;
436
513
  }
@@ -438,6 +515,7 @@ export class AgentSession {
438
515
  if (this._pendingAgentRunStart === reservation)
439
516
  this._pendingAgentRunStart = undefined;
440
517
  rejectDeferred(reservation, error);
518
+ this._settleRetryChain(reservation.chain, error);
441
519
  }
442
520
  // SCRAMJET-DIVERGENCE: harness-tool persisted settlement (#341). Synchronous ownership keeps queued
443
521
  // invocation events attributed after early failures; public settlement awaits execution and the final persistence tail.
@@ -525,7 +603,7 @@ export class AgentSession {
525
603
  }
526
604
  }
527
605
  // Emit to extensions first
528
- await this._emitExtensionEvent(event);
606
+ await this._emitExtensionEvent(event, fromHarnessInvocation ? undefined : run?.chain.result.promise);
529
607
  if (event.type === "message_end" && event.message.role === "toolResult" && toolCost?.cost !== undefined) {
530
608
  event.message.cost = toolCost.cost;
531
609
  }
@@ -617,6 +695,14 @@ export class AgentSession {
617
695
  }
618
696
  const msg = run.persistedAssistantSnapshot;
619
697
  run.persistedAssistantSnapshot = undefined;
698
+ if (msg && msg.origin !== "harness")
699
+ run.chain.assistantMessage = msg;
700
+ if (run.chain.cancelled) {
701
+ if (this._retry)
702
+ this._finishCancelledRetry("cancelled_during_continuation");
703
+ this._completeAgentRun(run);
704
+ return;
705
+ }
620
706
  if (!msg) {
621
707
  this._releaseRetryForFailedRun(new Error("Retry continuation ended without an assistant message."));
622
708
  this._completeAgentRun(run);
@@ -628,6 +714,8 @@ export class AgentSession {
628
714
  return;
629
715
  }
630
716
  if (msg.stopReason !== "error") {
717
+ if (msg.stopReason === "aborted")
718
+ run.chain.cancelled = true;
631
719
  if (this._retry) {
632
720
  const attempt = this._runRetryCount;
633
721
  this._finishRetry({
@@ -637,11 +725,17 @@ export class AgentSession {
637
725
  cumulativeErrors: this._bounded(attempt),
638
726
  }, { type: "auto_retry_end", success: true, attempt });
639
727
  }
728
+ await this._checkCompaction(msg, "settlement", run);
640
729
  this._completeAgentRun(run);
641
- await this._checkCompaction(msg);
642
730
  return;
643
731
  }
644
732
  const classification = this._classifyRetry(msg);
733
+ if (msg.origin === "harness" && classification.kind !== "context_overflow") {
734
+ this._releaseRetryForFailedRun(new Error(msg.errorMessage ?? "Agent execution failed."));
735
+ run.chain.resultError = new Error(msg.errorMessage ?? "Agent execution failed.");
736
+ this._completeAgentRun(run);
737
+ return;
738
+ }
645
739
  if (classification.kind === "context_overflow") {
646
740
  this._finishRetryWithoutSuccess({
647
741
  schemaVersion: 1,
@@ -649,8 +743,8 @@ export class AgentSession {
649
743
  reason: "context_overflow_compaction",
650
744
  evidence: "context_overflow",
651
745
  }, msg);
746
+ await this._checkCompaction(msg, "settlement", run);
652
747
  this._completeAgentRun(run);
653
- await this._checkCompaction(msg);
654
748
  return;
655
749
  }
656
750
  const settings = this.settingsManager.getRetrySettings();
@@ -661,12 +755,12 @@ export class AgentSession {
661
755
  reason: "retry_disabled",
662
756
  evidence: classification.evidence,
663
757
  }, msg);
758
+ await this._checkCompaction(msg, "settlement", run);
664
759
  this._completeAgentRun(run);
665
- await this._checkCompaction(msg);
666
760
  return;
667
761
  }
668
762
  if (classification.kind === "retry") {
669
- const didRetry = await this._handleRetryableError(msg, classification.evidence, run);
763
+ const didRetry = this._handleRetryableError(msg, classification.evidence, run);
670
764
  if (didRetry)
671
765
  return;
672
766
  }
@@ -674,8 +768,8 @@ export class AgentSession {
674
768
  const { kind: _kind, ...cause } = classification;
675
769
  this._finishRetryWithoutSuccess({ schemaVersion: 1, outcome: "not_attempted", ...cause }, msg);
676
770
  }
771
+ await this._checkCompaction(msg, "settlement", run);
677
772
  this._completeAgentRun(run);
678
- await this._checkCompaction(msg);
679
773
  }
680
774
  }
681
775
  /** Extract text content from a message */
@@ -714,13 +808,19 @@ export class AgentSession {
714
808
  Object.assign(targetRecord, replacement);
715
809
  }
716
810
  /** Emit extension events based on agent events */
717
- async _emitExtensionEvent(event) {
811
+ async _emitExtensionEvent(event, settlement) {
812
+ // SCRAMJET-DIVERGENCE: message provenance is independent of Session persistence acknowledgements (#611).
813
+ if ((event.type === "message_start" || event.type === "message_update" || event.type === "message_end") &&
814
+ event.message.role === "assistant" &&
815
+ event.message.origin === "harness") {
816
+ settlement = undefined;
817
+ }
718
818
  if (event.type === "agent_start") {
719
819
  this._turnIndex = 0;
720
- await this._extensionRunner.emit({ type: "agent_start" });
820
+ await this._extensionRunner.emit({ type: "agent_start" }, settlement);
721
821
  }
722
822
  else if (event.type === "agent_end") {
723
- await this._extensionRunner.emit({ type: "agent_end", messages: event.messages });
823
+ await this._extensionRunner.emit({ type: "agent_end", messages: event.messages }, settlement);
724
824
  }
725
825
  else if (event.type === "turn_start") {
726
826
  const extensionEvent = {
@@ -728,7 +828,7 @@ export class AgentSession {
728
828
  turnIndex: this._turnIndex,
729
829
  timestamp: Date.now(),
730
830
  };
731
- await this._extensionRunner.emit(extensionEvent);
831
+ await this._extensionRunner.emit(extensionEvent, settlement);
732
832
  }
733
833
  else if (event.type === "turn_end") {
734
834
  const extensionEvent = {
@@ -737,7 +837,7 @@ export class AgentSession {
737
837
  message: event.message,
738
838
  toolResults: event.toolResults,
739
839
  };
740
- await this._extensionRunner.emit(extensionEvent);
840
+ await this._extensionRunner.emit(extensionEvent, settlement);
741
841
  this._turnIndex++;
742
842
  }
743
843
  else if (event.type === "message_start") {
@@ -745,7 +845,7 @@ export class AgentSession {
745
845
  type: "message_start",
746
846
  message: event.message,
747
847
  };
748
- await this._extensionRunner.emit(extensionEvent);
848
+ await this._extensionRunner.emit(extensionEvent, settlement);
749
849
  }
750
850
  else if (event.type === "message_update") {
751
851
  const extensionEvent = {
@@ -753,14 +853,14 @@ export class AgentSession {
753
853
  message: event.message,
754
854
  assistantMessageEvent: event.assistantMessageEvent,
755
855
  };
756
- await this._extensionRunner.emit(extensionEvent);
856
+ await this._extensionRunner.emit(extensionEvent, settlement);
757
857
  }
758
858
  else if (event.type === "message_end") {
759
859
  const extensionEvent = {
760
860
  type: "message_end",
761
861
  message: event.message,
762
862
  };
763
- const replacement = await this._extensionRunner.emitMessageEnd(extensionEvent);
863
+ const replacement = await this._extensionRunner.emitMessageEnd(extensionEvent, settlement);
764
864
  if (replacement) {
765
865
  this._replaceMessageInPlace(event.message, replacement);
766
866
  }
@@ -772,7 +872,7 @@ export class AgentSession {
772
872
  toolName: event.toolName,
773
873
  args: event.args,
774
874
  };
775
- await this._extensionRunner.emit(extensionEvent);
875
+ await this._extensionRunner.emit(extensionEvent, settlement);
776
876
  }
777
877
  else if (event.type === "tool_execution_update") {
778
878
  const extensionEvent = {
@@ -782,7 +882,7 @@ export class AgentSession {
782
882
  args: event.args,
783
883
  partialResult: event.partialResult,
784
884
  };
785
- await this._extensionRunner.emit(extensionEvent);
885
+ await this._extensionRunner.emit(extensionEvent, settlement);
786
886
  }
787
887
  else if (event.type === "tool_execution_end") {
788
888
  const extensionEvent = {
@@ -792,7 +892,7 @@ export class AgentSession {
792
892
  result: event.result,
793
893
  isError: event.isError,
794
894
  };
795
- await this._extensionRunner.emit(extensionEvent);
895
+ await this._extensionRunner.emit(extensionEvent, settlement);
796
896
  }
797
897
  }
798
898
  /**
@@ -844,6 +944,13 @@ export class AgentSession {
844
944
  this._pendingToolCosts.clear();
845
945
  this._resetOutputThroughput();
846
946
  const retryDisposeError = new Error("AgentSession disposed before retry settlement completed.");
947
+ for (const chain of this._unsettledRetryChains) {
948
+ if (chain.handoff)
949
+ clearTimeout(chain.handoff);
950
+ chain.handoff = undefined;
951
+ rejectDeferred(chain.result, retryDisposeError);
952
+ }
953
+ this._autoCompactionAbortController?.abort();
847
954
  const unsettledRuns = [...this._unsettledAgentRuns];
848
955
  const retry = this._retry;
849
956
  const unclassifiedError = unsettledRuns.find((run) => run.persistedAssistantSnapshot?.stopReason === "error")?.persistedAssistantSnapshot;
@@ -896,8 +1003,10 @@ export class AgentSession {
896
1003
  retryChains.add(pendingRetryContinuation);
897
1004
  for (const run of unsettledRuns)
898
1005
  this._settleAgentRun(run, retryDisposeError);
899
- for (const chain of retryChains)
900
- this._settleRetryChain(chain, retryDisposeError);
1006
+ for (const chain of retryChains) {
1007
+ settleDeferred(chain, retryDisposeError);
1008
+ this._unsettledRetryChains.delete(chain);
1009
+ }
901
1010
  this._disconnectFromAgent();
902
1011
  this._eventListeners = [];
903
1012
  cleanupSessionResources(this.sessionId);
@@ -1387,7 +1496,7 @@ export class AgentSession {
1387
1496
  }
1388
1497
  acceptInput?.();
1389
1498
  preflightResult?.(true);
1390
- await this._runAgentPrompt(messages);
1499
+ await this._runAgentPrompt(messages, options?.onRunSettlement);
1391
1500
  }
1392
1501
  /**
1393
1502
  * Try to execute an extension command. Returns true if command was found and executed.
@@ -2003,6 +2112,7 @@ export class AgentSession {
2003
2112
  * Cancel in-progress compaction (manual or auto).
2004
2113
  */
2005
2114
  abortCompaction() {
2115
+ this._cancelRunSettlements();
2006
2116
  this._compactionAbortController?.abort();
2007
2117
  this._autoCompactionAbortController?.abort();
2008
2118
  }
@@ -2022,8 +2132,8 @@ export class AgentSession {
2022
2132
  *
2023
2133
  * @param assistantMessage The assistant message to check
2024
2134
  */
2025
- async _checkCompaction(assistantMessage, trigger = "settlement") {
2026
- if (this._manualCompactionDrain)
2135
+ async _checkCompaction(assistantMessage, trigger = "settlement", run) {
2136
+ if (this._manualCompactionDrain || run?.chain.cancelled)
2027
2137
  return;
2028
2138
  const settings = this.settingsManager.getCompactionSettings();
2029
2139
  if (!settings.enabled)
@@ -2055,7 +2165,7 @@ export class AgentSession {
2055
2165
  const window = this.model?.contextWindow ?? 0;
2056
2166
  if (shouldCompact(tokens, window, settings) ||
2057
2167
  (window > settings.reserveTokens && tokens > (this.model?.maxInputTokens ?? Infinity)))
2058
- await this._runAutoCompaction("threshold", false);
2168
+ await this._runAutoCompaction("threshold", false, undefined, false);
2059
2169
  return;
2060
2170
  }
2061
2171
  // SCRAMJET-DIVERGENCE: Total context and independent input limits are separate constraints.
@@ -2094,7 +2204,7 @@ export class AgentSession {
2094
2204
  if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
2095
2205
  this.agent.state.messages = messages.slice(0, -1);
2096
2206
  }
2097
- await this._runAutoCompaction("overflow", true);
2207
+ await this._runAutoCompaction("overflow", true, run);
2098
2208
  return;
2099
2209
  }
2100
2210
  // Case 2: Threshold - context is getting large
@@ -2122,14 +2232,19 @@ export class AgentSession {
2122
2232
  }
2123
2233
  if (shouldCompact(contextTokens, contextWindow, settings) ||
2124
2234
  (contextWindow > settings.reserveTokens && contextTokens > (this.model?.maxInputTokens ?? Infinity))) {
2125
- await this._runAutoCompaction("threshold", false);
2235
+ await this._runAutoCompaction("threshold", false, run);
2126
2236
  }
2127
2237
  }
2128
2238
  /**
2129
2239
  * Internal: Run auto-compaction with events.
2130
2240
  */
2131
- async _runAutoCompaction(reason, willRetry) {
2241
+ async _runAutoCompaction(reason, willRetry, run, continueQueued = true) {
2132
2242
  const settings = this.settingsManager.getCompactionSettings();
2243
+ const chain = run?.chain;
2244
+ let compacted = false;
2245
+ let persisting = false;
2246
+ if (chain?.cancelled || this._disposed)
2247
+ return;
2133
2248
  // SCRAMJET-DIVERGENCE: reentrant subscribers must observe compaction ownership (#598).
2134
2249
  this._autoCompactionAbortController = new AbortController();
2135
2250
  try {
@@ -2177,7 +2292,7 @@ export class AgentSession {
2177
2292
  branchEntries: pathEntries,
2178
2293
  customInstructions: undefined,
2179
2294
  signal: this._autoCompactionAbortController.signal,
2180
- }));
2295
+ }, chain?.result.promise));
2181
2296
  if (extensionResult?.cancel) {
2182
2297
  this._emit({
2183
2298
  type: "compaction_end",
@@ -2212,7 +2327,7 @@ export class AgentSession {
2212
2327
  tokensBefore = compactResult.tokensBefore;
2213
2328
  details = compactResult.details;
2214
2329
  }
2215
- if (this._autoCompactionAbortController.signal.aborted) {
2330
+ if (this._autoCompactionAbortController.signal.aborted || chain?.cancelled || this._disposed) {
2216
2331
  this._emit({
2217
2332
  type: "compaction_end",
2218
2333
  reason,
@@ -2222,7 +2337,9 @@ export class AgentSession {
2222
2337
  });
2223
2338
  return;
2224
2339
  }
2340
+ persisting = true;
2225
2341
  this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
2342
+ compacted = true;
2226
2343
  const newEntries = this.sessionManager.getEntries();
2227
2344
  const sessionContext = this.sessionManager.buildSessionContext();
2228
2345
  this.agent.state.messages = sessionContext.messages;
@@ -2233,7 +2350,7 @@ export class AgentSession {
2233
2350
  type: "session_compact",
2234
2351
  compactionEntry: savedCompactionEntry,
2235
2352
  fromExtension,
2236
- });
2353
+ }, chain?.result.promise);
2237
2354
  }
2238
2355
  const result = {
2239
2356
  summary,
@@ -2248,20 +2365,23 @@ export class AgentSession {
2248
2365
  if (lastMsg?.role === "assistant" && lastMsg.stopReason === "error") {
2249
2366
  this.agent.state.messages = messages.slice(0, -1);
2250
2367
  }
2251
- setTimeout(() => {
2252
- this.agent.continue().catch(() => { });
2253
- }, 100);
2254
2368
  }
2255
- else if (this.agent.hasQueuedMessages()) {
2256
- // Auto-compaction can complete while follow-up/steering/custom messages are waiting.
2257
- // Kick the loop so queued messages are actually delivered.
2258
- setTimeout(() => {
2259
- this.agent.continue().catch(() => { });
2260
- }, 100);
2369
+ if (!chain?.cancelled &&
2370
+ !this._disposed &&
2371
+ (willRetry || (continueQueued && this.agent.hasQueuedMessages()))) {
2372
+ this._scheduleCompactionContinuation(chain, run?.execution.promise);
2261
2373
  }
2262
2374
  }
2263
2375
  catch (error) {
2264
2376
  const errorMessage = error instanceof Error ? error.message : "compaction failed";
2377
+ if (chain) {
2378
+ if (persisting)
2379
+ chain.error = error instanceof Error ? error : new Error(String(error));
2380
+ chain.errorMessage =
2381
+ reason === "overflow"
2382
+ ? `Context overflow recovery failed: ${errorMessage}`
2383
+ : `Auto-compaction failed: ${errorMessage}`;
2384
+ }
2265
2385
  this._emit({
2266
2386
  type: "compaction_end",
2267
2387
  reason,
@@ -2274,9 +2394,34 @@ export class AgentSession {
2274
2394
  });
2275
2395
  }
2276
2396
  finally {
2397
+ if (chain) {
2398
+ if (this._autoCompactionAbortController?.signal.aborted)
2399
+ chain.cancelled = true;
2400
+ if (willRetry && !compacted && !chain.cancelled)
2401
+ chain.errorMessage ??=
2402
+ "Context overflow recovery could not compact. Reduce context or select a larger-context model.";
2403
+ }
2277
2404
  this._autoCompactionAbortController = undefined;
2278
2405
  }
2279
2406
  }
2407
+ _scheduleCompactionContinuation(chain, execution) {
2408
+ const owner = chain ?? createRetryChain();
2409
+ this._unsettledRetryChains.add(owner);
2410
+ owner.handoff = setTimeout(() => {
2411
+ void (async () => {
2412
+ await execution;
2413
+ owner.handoff = undefined;
2414
+ if (owner.cancelled || this._disposed || this._manualCompactionDrain || owner.error) {
2415
+ this._settleRetryChain(owner);
2416
+ return;
2417
+ }
2418
+ owner.terminal = false;
2419
+ await this._continueAgentRun(owner);
2420
+ })().catch((error) => {
2421
+ this._settleRetryChain(owner, this._reportRetryFailure(error));
2422
+ });
2423
+ }, 100);
2424
+ }
2280
2425
  /**
2281
2426
  * Toggle auto-compaction setting.
2282
2427
  */
@@ -2454,6 +2599,7 @@ export class AgentSession {
2454
2599
  })();
2455
2600
  },
2456
2601
  getSystemPrompt: () => this.systemPrompt,
2602
+ sendMessage: (message, options) => this.sendCustomMessage(message, options),
2457
2603
  dispatchUserInput: (input, options) => this.dispatchUserInput(input, options),
2458
2604
  }, {
2459
2605
  registerProvider: (name, config) => {
@@ -2736,7 +2882,7 @@ export class AgentSession {
2736
2882
  });
2737
2883
  return normalized;
2738
2884
  }
2739
- async _handleRetryableError(message, evidence, run) {
2885
+ _handleRetryableError(message, evidence, run) {
2740
2886
  const settings = this.settingsManager.getRetrySettings();
2741
2887
  const attempt = (this._retry?.attempt ?? 0) + 1;
2742
2888
  const cumulativeCap = Math.min(Number.MAX_SAFE_INTEGER, settings.maxRetries * 2);
@@ -2808,51 +2954,72 @@ export class AgentSession {
2808
2954
  errorMessage: message.errorMessage || "Unknown error",
2809
2955
  cumulativeErrors: this._runRetryCount,
2810
2956
  });
2811
- if (controller.signal.aborted || this._disposed) {
2957
+ if (controller.signal.aborted || run.chain.cancelled || this._disposed) {
2812
2958
  return this._finishCancelledRetry("cancelled_during_backoff", backoff);
2813
2959
  }
2814
2960
  const messages = this.agent.state.messages;
2815
2961
  if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
2816
2962
  this.agent.state.messages = messages.slice(0, -1);
2817
2963
  }
2964
+ // Execution unwind may need queued failure events, so retry waiting cannot occupy that queue.
2965
+ void this._resumeRetry(backoff, delayMs).catch((error) => {
2966
+ if (this._disposed)
2967
+ return;
2968
+ const failure = this._reportRetryFailure(error);
2969
+ if (this._retry === backoff)
2970
+ this._releaseRetryForFailedRun(failure);
2971
+ this._completeAgentRun(run, failure);
2972
+ });
2973
+ return true;
2974
+ }
2975
+ async _resumeRetry(backoff, delayMs) {
2976
+ const { controller, run, attempt } = backoff;
2818
2977
  try {
2819
2978
  await sleep(delayMs, controller.signal);
2820
2979
  await sleep(0, controller.signal);
2821
2980
  }
2822
2981
  catch {
2823
- return this._finishCancelledRetry("cancelled_during_backoff", backoff);
2982
+ this._finishCancelledRetry("cancelled_during_backoff", backoff);
2983
+ this._completeAgentRun(run);
2984
+ return;
2824
2985
  }
2825
- if (controller.signal.aborted || this._disposed) {
2826
- return this._finishCancelledRetry("cancelled_during_backoff", backoff);
2986
+ await run.execution.promise;
2987
+ await this._drainAgentEventQueue();
2988
+ if (this._disposed || this._retry !== backoff)
2989
+ return;
2990
+ if (run.chain.error || run.chain.resultError)
2991
+ throw run.chain.error ?? run.chain.resultError;
2992
+ if (controller.signal.aborted || run.chain.cancelled || this._manualCompactionDrain) {
2993
+ this._finishCancelledRetry("cancelled_during_backoff", backoff);
2994
+ this._completeAgentRun(run);
2995
+ return;
2827
2996
  }
2828
- if (this._manualCompactionDrain)
2829
- return this._finishCancelledRetry("cancelled_during_backoff", backoff);
2830
2997
  // Hand the chain to the continuation before `continue()`; the run that eventually emits
2831
2998
  // `agent_start` adopts it (see `_captureAgentRunSettlement`), however long Agent defers that start.
2832
2999
  const continuing = { phase: "continuing", attempt, chain: run.chain };
2833
3000
  this._retry = continuing;
2834
- const continuation = this.agent.continue();
3001
+ const continuation = this._continueAgentRun(run.chain);
2835
3002
  this._settleAgentRun(run);
2836
3003
  continuation.catch((error) => {
2837
- if (this._disposed || this._retry !== continuing)
3004
+ if (this._disposed)
2838
3005
  return;
2839
3006
  const continuationError = this._reportRetryFailure(error);
2840
3007
  let settlementError = continuationError;
2841
3008
  try {
2842
- this._finishRetry({
2843
- schemaVersion: 1,
2844
- outcome: "failed",
2845
- reason: "continuation_rejected",
2846
- attempt: this._bounded(attempt),
2847
- cumulativeErrors: this._bounded(this._runRetryCount),
2848
- }, { type: "auto_retry_end", success: false, attempt, finalError: continuationError.message });
3009
+ if (this._retry === continuing)
3010
+ this._finishRetry({
3011
+ schemaVersion: 1,
3012
+ outcome: "failed",
3013
+ reason: "continuation_rejected",
3014
+ attempt: this._bounded(attempt),
3015
+ cumulativeErrors: this._bounded(this._runRetryCount),
3016
+ }, { type: "auto_retry_end", success: false, attempt, finalError: continuationError.message });
2849
3017
  }
2850
3018
  catch (persistenceError) {
2851
3019
  settlementError = new AggregateError([continuationError, persistenceError], "Retry continuation and outcome persistence both failed.", { cause: continuationError });
2852
3020
  }
2853
3021
  this._settleRetryChain(run.chain, settlementError);
2854
3022
  });
2855
- return true;
2856
3023
  }
2857
3024
  _finishCancelledRetry(reason, expected) {
2858
3025
  if (this._disposed) {
@@ -2876,29 +3043,46 @@ export class AgentSession {
2876
3043
  * Cancel in-progress retry.
2877
3044
  */
2878
3045
  abortRetry() {
3046
+ this._cancelRunSettlements();
2879
3047
  const retry = this._retry;
2880
- if (!retry)
3048
+ if (!retry) {
3049
+ this.agent.abort();
2881
3050
  return;
3051
+ }
2882
3052
  if (retry.phase === "backoff")
2883
3053
  retry.controller.abort();
2884
3054
  this.agent.abort();
2885
3055
  }
2886
3056
  /** Agent's own `activeRun` guard is false during backoff; the retry phase owns the idle Agent then. */
2887
3057
  _assertRetryNotOwningIdleAgent() {
2888
- if (this._retry && !this.isStreaming) {
3058
+ if ((this._retry || [...this._unsettledRetryChains].some((chain) => chain.handoff)) && !this.isStreaming) {
2889
3059
  throw new Error("Automatic retry is in progress. Cancel the retry before starting another prompt.");
2890
3060
  }
2891
3061
  }
2892
- async _runAgentPrompt(messages) {
3062
+ async _runAgentPrompt(messages, onRunSettlement) {
2893
3063
  this._assertRetryNotOwningIdleAgent();
2894
3064
  const start = this._reserveAgentRunStart();
3065
+ try {
3066
+ onRunSettlement?.(start.chain.result.promise);
3067
+ }
3068
+ catch (error) {
3069
+ this._rejectAgentRunStart(start, error instanceof Error ? error : new Error(String(error)));
3070
+ throw error;
3071
+ }
2895
3072
  this._runRetryCount = 0;
2896
3073
  let promptFailure;
3074
+ start.chain.executions++;
2897
3075
  try {
2898
3076
  await this.agent.prompt(messages);
2899
3077
  }
2900
3078
  catch (error) {
2901
3079
  promptFailure = { error };
3080
+ start.chain.error ??= error instanceof Error ? error : new Error(String(error));
3081
+ }
3082
+ finally {
3083
+ start.chain.executions--;
3084
+ resolveDeferred(start.execution, undefined);
3085
+ this._trySettleRetryChain(start.chain);
2902
3086
  }
2903
3087
  if (!start.run && this._pendingAgentRunStart === start) {
2904
3088
  const startError = promptFailure
@@ -2918,7 +3102,7 @@ export class AgentSession {
2918
3102
  catch (error) {
2919
3103
  settlementFailure = { error };
2920
3104
  }
2921
- if (promptFailure && settlementFailure) {
3105
+ if (promptFailure && settlementFailure && promptFailure.error !== settlementFailure.error) {
2922
3106
  throw new AggregateError([promptFailure.error, settlementFailure.error], "Agent prompt and retry settlement both failed.", { cause: promptFailure.error });
2923
3107
  }
2924
3108
  if (promptFailure)
@@ -2926,12 +3110,53 @@ export class AgentSession {
2926
3110
  if (settlementFailure)
2927
3111
  throw settlementFailure.error;
2928
3112
  }
3113
+ async _continueAgentRun(chain) {
3114
+ const start = this._reserveAgentRunStart(chain);
3115
+ chain.executions++;
3116
+ let failure;
3117
+ try {
3118
+ if (!chain.cancelled)
3119
+ await this.agent.continue();
3120
+ }
3121
+ catch (error) {
3122
+ failure = error instanceof Error ? error : new Error(String(error));
3123
+ throw error;
3124
+ }
3125
+ finally {
3126
+ chain.executions--;
3127
+ if (failure)
3128
+ chain.error ??= failure;
3129
+ resolveDeferred(start.execution, undefined);
3130
+ if (!start.run) {
3131
+ if (failure) {
3132
+ if (this._pendingAgentRunStart === start)
3133
+ this._pendingAgentRunStart = undefined;
3134
+ rejectDeferred(start, failure);
3135
+ }
3136
+ else {
3137
+ this._rejectAgentRunStart(start, new Error("Agent continuation completed before agent_start."));
3138
+ }
3139
+ }
3140
+ if (!failure)
3141
+ this._trySettleRetryChain(chain);
3142
+ }
3143
+ }
3144
+ _cancelRunSettlements() {
3145
+ for (const chain of this._unsettledRetryChains) {
3146
+ chain.cancelled = true;
3147
+ if (chain.handoff) {
3148
+ clearTimeout(chain.handoff);
3149
+ chain.handoff = undefined;
3150
+ this._settleRetryChain(chain);
3151
+ }
3152
+ }
3153
+ this._autoCompactionAbortController?.abort();
3154
+ }
2929
3155
  async waitForRetry(settlement) {
2930
3156
  if (!settlement)
2931
3157
  return;
2932
3158
  await settlement.promise;
2933
3159
  await settlement.chain.promise;
2934
- await this.agent.waitForIdle();
2935
3160
  }
2936
3161
  /** Whether auto-retry is currently in progress */
2937
3162
  get isRetrying() {