@leanandmean/coding-agent 0.74.1-scramjet.66 → 0.74.1-scramjet.68

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.
@@ -87,6 +87,9 @@ function rejectDeferred(deferred, error) {
87
87
  function settleDeferred(deferred, error) {
88
88
  return error ? rejectDeferred(deferred, error) : resolveDeferred(deferred, undefined);
89
89
  }
90
+ function validRecordedCost(value) {
91
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
92
+ }
90
93
  export class AgentSession {
91
94
  agent;
92
95
  sessionManager;
@@ -97,6 +100,10 @@ export class AgentSession {
97
100
  _unsubscribeAgent;
98
101
  _eventListeners = [];
99
102
  _agentEventQueue = Promise.resolve();
103
+ // SCRAMJET-DIVERGENCE: execution ownership survives final-event queue latency until append succeeds (#598).
104
+ _toolCostExecutions = new Map();
105
+ _pendingToolCosts = new Set();
106
+ _historicalCostDefinitions = new Map();
100
107
  _outputThroughputTracker = new OutputThroughputTracker();
101
108
  _outputThroughputHistory;
102
109
  _outputThroughputGeneration;
@@ -115,6 +122,7 @@ export class AgentSession {
115
122
  _inputSessionEntries = new WeakMap();
116
123
  // Compaction state
117
124
  _compactionAbortController = undefined;
125
+ _manualCompactionDrain;
118
126
  _autoCompactionAbortController = undefined;
119
127
  _overflowRecoveryAttempted = false;
120
128
  // Branch summarization state
@@ -299,10 +307,13 @@ export class AgentSession {
299
307
  }
300
308
  /** Internal handler for agent events - shared by subscribe and reconnect */
301
309
  _handleAgentEvent = (event) => {
310
+ if (this._disposed)
311
+ return;
302
312
  this._captureOutputThroughput(event);
313
+ const toolCost = this._captureToolCost(event);
303
314
  const runSettlement = this._captureAgentRunSettlement(event);
304
315
  const harnessAck = this._harnessPersistenceAckForEvent(event);
305
- const processing = this._agentEventQueue.then(() => this._processAgentEventTracked(event, runSettlement, harnessAck), () => this._processAgentEventTracked(event, runSettlement, harnessAck));
316
+ const processing = this._agentEventQueue.then(() => this._processAgentEventTracked(event, runSettlement, harnessAck, toolCost), () => this._processAgentEventTracked(event, runSettlement, harnessAck, toolCost));
306
317
  this._agentEventQueue = processing;
307
318
  if (harnessAck)
308
319
  harnessAck.persistenceTail = processing;
@@ -313,6 +324,42 @@ export class AgentSession {
313
324
  if (event.type === "turn_end")
314
325
  return this._agentEventQueue;
315
326
  };
327
+ // SCRAMJET-DIVERGENCE: scalar reports are captured before mutable events enter async hooks (#598).
328
+ _captureToolCost(event) {
329
+ if (event.type === "tool_execution_start") {
330
+ const execution = {};
331
+ this._toolCostExecutions.set(event.toolCallId, execution);
332
+ this._pendingToolCosts.add(execution);
333
+ return;
334
+ }
335
+ let toolCallId;
336
+ let reported;
337
+ const final = event.type === "message_end" && event.message.role === "toolResult";
338
+ if (event.type === "tool_execution_update" || event.type === "tool_execution_end") {
339
+ toolCallId = event.toolCallId;
340
+ reported = event.type === "tool_execution_update" ? event.partialResult?.cost : event.result?.cost;
341
+ }
342
+ else if (event.type === "message_end" && event.message.role === "toolResult") {
343
+ toolCallId = event.message.toolCallId;
344
+ reported = event.message.cost;
345
+ }
346
+ else {
347
+ return;
348
+ }
349
+ let execution = this._toolCostExecutions.get(toolCallId);
350
+ if (!execution && validRecordedCost(reported)) {
351
+ execution = {};
352
+ this._toolCostExecutions.set(toolCallId, execution);
353
+ this._pendingToolCosts.add(execution);
354
+ }
355
+ if (execution && validRecordedCost(reported))
356
+ execution.cost = reported;
357
+ if (final) {
358
+ this._toolCostExecutions.delete(toolCallId);
359
+ return execution;
360
+ }
361
+ return;
362
+ }
316
363
  // SCRAMJET-DIVERGENCE: Capture provider events synchronously and reset stale throughput lifecycles (#476).
317
364
  _captureOutputThroughput(event) {
318
365
  if (event.type === "message_start" && event.message.role === "assistant" && event.message.origin === "provider") {
@@ -391,16 +438,18 @@ export class AgentSession {
391
438
  }
392
439
  // SCRAMJET-DIVERGENCE: harness-tool persisted settlement (#341). Synchronous ownership keeps queued
393
440
  // invocation events attributed after early failures; public settlement awaits execution and the final persistence tail.
394
- async _processAgentEventTracked(event, run, harnessAck) {
441
+ async _processAgentEventTracked(event, run, harnessAck, toolCost) {
395
442
  try {
396
- await this._processAgentEvent(event, run, harnessAck !== undefined);
443
+ await this._processAgentEvent(event, run, harnessAck !== undefined, toolCost);
397
444
  }
398
445
  catch (err) {
399
446
  const error = err instanceof Error ? err : new Error(String(err));
447
+ if (this._manualCompactionDrain)
448
+ this._manualCompactionDrain.firstError ??= error;
400
449
  if (harnessAck) {
401
450
  harnessAck.firstPersistenceError ??= error;
402
451
  }
403
- else if (run) {
452
+ else if (run && !this._manualCompactionDrain) {
404
453
  if (event.type === "message_end" && event.message.role === "assistant") {
405
454
  run.assistantPersistenceError = error;
406
455
  }
@@ -437,7 +486,19 @@ export class AgentSession {
437
486
  }
438
487
  return toolCallId === undefined ? undefined : this._harnessPersistenceAcks.get(toolCallId);
439
488
  }
440
- async _processAgentEvent(event, run, fromHarnessInvocation = false) {
489
+ async _processAgentEvent(event, run, fromHarnessInvocation = false, toolCost) {
490
+ if (this._disposed)
491
+ return;
492
+ // SCRAMJET-DIVERGENCE: queued lifecycle events must respect manual compaction ownership (#598).
493
+ if (this._manualCompactionDrain && event.type !== "message_end" && event.type !== "agent_end")
494
+ return;
495
+ if (this._manualCompactionDrain && event.type === "agent_end") {
496
+ if (this._retry)
497
+ this._finishCancelledRetry("cancelled_during_continuation");
498
+ if (run)
499
+ this._completeAgentRun(run, this._manualCompactionDrain.firstError);
500
+ return;
501
+ }
441
502
  // When a user message starts, check if it's from either queue and remove it BEFORE emitting
442
503
  // This ensures the UI sees the updated queue state
443
504
  if (event.type === "message_start" && event.message.role === "user") {
@@ -462,6 +523,18 @@ export class AgentSession {
462
523
  }
463
524
  // Emit to extensions first
464
525
  await this._emitExtensionEvent(event);
526
+ if (event.type === "message_end" && event.message.role === "toolResult" && toolCost?.cost !== undefined) {
527
+ event.message.cost = toolCost.cost;
528
+ }
529
+ // An awaited hook may have acquired manual ownership while this event was in flight.
530
+ if (this._manualCompactionDrain && event.type !== "message_end") {
531
+ if (event.type === "agent_end" && run) {
532
+ if (this._retry)
533
+ this._finishCancelledRetry("cancelled_during_continuation");
534
+ this._completeAgentRun(run, this._manualCompactionDrain.firstError);
535
+ }
536
+ return;
537
+ }
465
538
  // Notify all listeners
466
539
  this._emit(event);
467
540
  if (this._disposed)
@@ -482,6 +555,8 @@ export class AgentSession {
482
555
  ? JSON.parse(JSON.stringify(event.message))
483
556
  : event.message;
484
557
  const messageEntryId = this.sessionManager.appendMessage(persistedMessage);
558
+ if (toolCost)
559
+ this._pendingToolCosts.delete(toolCost);
485
560
  if (persistedMessage.role === "assistant" && !fromHarnessInvocation && run) {
486
561
  run.persistedAssistantSnapshot = persistedMessage;
487
562
  }
@@ -514,7 +589,8 @@ export class AgentSession {
514
589
  if (event.message.role === "assistant" &&
515
590
  event.message.stopReason !== "error" &&
516
591
  event.message.stopReason !== "aborted" &&
517
- !fromHarnessInvocation) {
592
+ !fromHarnessInvocation &&
593
+ !this._manualCompactionDrain) {
518
594
  this._overflowRecoveryAttempted = false;
519
595
  if (this._retry)
520
596
  this._retry = { ...this._retry, attempt: 0 };
@@ -523,6 +599,12 @@ export class AgentSession {
523
599
  if (event.type === "agent_end") {
524
600
  if (this._disposed || !run)
525
601
  return;
602
+ if (this._manualCompactionDrain) {
603
+ if (this._retry)
604
+ this._finishCancelledRetry("cancelled_during_continuation");
605
+ this._completeAgentRun(run, this._manualCompactionDrain.firstError);
606
+ return;
607
+ }
526
608
  if (run.assistantPersistenceError) {
527
609
  const error = run.assistantPersistenceError;
528
610
  run.assistantPersistenceError = undefined;
@@ -741,8 +823,8 @@ export class AgentSession {
741
823
  * Preserves all existing listeners.
742
824
  */
743
825
  _reconnectToAgent() {
744
- if (this._unsubscribeAgent)
745
- return; // Already connected
826
+ if (this._disposed || this._unsubscribeAgent)
827
+ return; // Already connected or disposed
746
828
  this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
747
829
  }
748
830
  /**
@@ -755,6 +837,8 @@ export class AgentSession {
755
837
  if (this._disposed)
756
838
  return;
757
839
  this._disposed = true;
840
+ this._toolCostExecutions.clear();
841
+ this._pendingToolCosts.clear();
758
842
  this._resetOutputThroughput();
759
843
  const retryDisposeError = new Error("AgentSession disposed before retry settlement completed.");
760
844
  const unsettledRuns = [...this._unsettledAgentRuns];
@@ -966,6 +1050,10 @@ export class AgentSession {
966
1050
  if (this._disposed) {
967
1051
  throw new Error(`Cannot invoke harness tool "${name}": the session has been disposed.`);
968
1052
  }
1053
+ // SCRAMJET-DIVERGENCE: reject new session work before allocating invocation ownership (#598).
1054
+ if (this._compactionAbortController) {
1055
+ throw new Error(`Cannot invoke harness tool "${name}": manual compaction is in progress. Retry after it settles.`);
1056
+ }
969
1057
  const tool = this._toolRegistry.get(name);
970
1058
  if (!tool) {
971
1059
  throw new Error(`Cannot invoke harness tool "${name}": no tool with that name is registered.`);
@@ -1759,12 +1847,32 @@ export class AgentSession {
1759
1847
  * @param customInstructions Optional instructions for the compaction summary
1760
1848
  */
1761
1849
  async compact(customInstructions) {
1762
- this._resetOutputThroughput();
1763
- this._disconnectFromAgent();
1764
- await this.abort();
1765
- this._compactionAbortController = new AbortController();
1766
- this._emit({ type: "compaction_start", reason: "manual" });
1850
+ // SCRAMJET-DIVERGENCE: acquire one owner before awaits and drain terminal persistence (#598).
1851
+ if (this._disposed)
1852
+ throw new Error("Cannot compact a disposed session.");
1853
+ if (this.isCompacting)
1854
+ throw new Error("Compaction or branch summarization is already in progress. Retry after it settles.");
1855
+ if (this.agent.hasActiveTransientHarnessTools) {
1856
+ throw new Error("A harness tool is still executing outside an Agent run. Retry compaction after its persisted settlement.");
1857
+ }
1858
+ const controller = new AbortController();
1859
+ this._compactionAbortController = controller;
1860
+ const drain = {};
1861
+ this._manualCompactionDrain = drain;
1862
+ const runs = [...this._unsettledAgentRuns];
1767
1863
  try {
1864
+ this._resetOutputThroughput();
1865
+ this._disconnectFromAgent();
1866
+ this._unsubscribeAgent = this.agent.subscribe((event) => this._handleAgentEvent(event));
1867
+ this._emit({ type: "compaction_start", reason: "manual" });
1868
+ await this.abort();
1869
+ await this._agentEventQueue;
1870
+ if (drain.firstError)
1871
+ throw drain.firstError;
1872
+ if (this._disposed)
1873
+ throw new Error("Session disposed during compaction.");
1874
+ if (controller.signal.aborted)
1875
+ throw new Error("Compaction cancelled");
1768
1876
  if (!this.model) {
1769
1877
  throw new Error(formatNoModelSelectedMessage());
1770
1878
  }
@@ -1788,7 +1896,7 @@ export class AgentSession {
1788
1896
  preparation,
1789
1897
  branchEntries: pathEntries,
1790
1898
  customInstructions,
1791
- signal: this._compactionAbortController.signal,
1899
+ signal: controller.signal,
1792
1900
  }));
1793
1901
  if (result?.cancel) {
1794
1902
  throw new Error("Compaction cancelled");
@@ -1811,13 +1919,15 @@ export class AgentSession {
1811
1919
  }
1812
1920
  else {
1813
1921
  // Generate compaction result
1814
- const result = await compact(preparation, this.model, apiKey, headers, customInstructions, this._compactionAbortController.signal, this.thinkingLevel);
1922
+ const result = await compact(preparation, this.model, apiKey, headers, customInstructions, controller.signal, this.thinkingLevel);
1815
1923
  summary = result.summary;
1816
1924
  firstKeptEntryId = result.firstKeptEntryId;
1817
1925
  tokensBefore = result.tokensBefore;
1818
1926
  details = result.details;
1819
1927
  }
1820
- if (this._compactionAbortController.signal.aborted) {
1928
+ if (this._disposed)
1929
+ throw new Error("Session disposed during compaction.");
1930
+ if (controller.signal.aborted) {
1821
1931
  throw new Error("Compaction cancelled");
1822
1932
  }
1823
1933
  this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
@@ -1849,8 +1959,13 @@ export class AgentSession {
1849
1959
  return compactionResult;
1850
1960
  }
1851
1961
  catch (error) {
1852
- const message = error instanceof Error ? error.message : String(error);
1853
- const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
1962
+ const failure = drain.firstError ?? error;
1963
+ const settlementError = failure instanceof Error ? failure : new Error(String(failure));
1964
+ this._releaseRetryForFailedRun(settlementError);
1965
+ for (const run of runs)
1966
+ this._completeAgentRun(run, settlementError);
1967
+ const message = settlementError.message;
1968
+ const aborted = message === "Compaction cancelled" || settlementError.name === "AbortError";
1854
1969
  this._emit({
1855
1970
  type: "compaction_end",
1856
1971
  reason: "manual",
@@ -1859,9 +1974,11 @@ export class AgentSession {
1859
1974
  willRetry: false,
1860
1975
  errorMessage: aborted ? undefined : `Compaction failed: ${message}`,
1861
1976
  });
1862
- throw error;
1977
+ throw failure;
1863
1978
  }
1864
1979
  finally {
1980
+ this._disconnectFromAgent();
1981
+ this._manualCompactionDrain = undefined;
1865
1982
  this._compactionAbortController = undefined;
1866
1983
  this._reconnectToAgent();
1867
1984
  }
@@ -1891,6 +2008,8 @@ export class AgentSession {
1891
2008
  * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
1892
2009
  */
1893
2010
  async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
2011
+ if (this._manualCompactionDrain)
2012
+ return;
1894
2013
  const settings = this.settingsManager.getCompactionSettings();
1895
2014
  if (!settings.enabled)
1896
2015
  return;
@@ -1969,9 +2088,10 @@ export class AgentSession {
1969
2088
  */
1970
2089
  async _runAutoCompaction(reason, willRetry) {
1971
2090
  const settings = this.settingsManager.getCompactionSettings();
1972
- this._emit({ type: "compaction_start", reason });
2091
+ // SCRAMJET-DIVERGENCE: reentrant subscribers must observe compaction ownership (#598).
1973
2092
  this._autoCompactionAbortController = new AbortController();
1974
2093
  try {
2094
+ this._emit({ type: "compaction_start", reason });
1975
2095
  if (!this.model) {
1976
2096
  this._emit({
1977
2097
  type: "compaction_end",
@@ -2316,7 +2436,13 @@ export class AgentSession {
2316
2436
  definition,
2317
2437
  sourceInfo: createSyntheticSourceInfo(`<sdk:${definition.name}>`, { source: "sdk" }),
2318
2438
  })),
2319
- ].filter((tool) => isAllowedTool(tool.definition.name));
2439
+ ];
2440
+ // SCRAMJET-DIVERGENCE: historical interpretation follows definition collision winners, not execution allowlists (#598).
2441
+ this._historicalCostDefinitions = new Map(this._baseToolDefinitions);
2442
+ for (const tool of allCustomTools) {
2443
+ this._historicalCostDefinitions.set(tool.definition.name, tool.definition);
2444
+ }
2445
+ const allowedCustomTools = allCustomTools.filter((tool) => isAllowedTool(tool.definition.name));
2320
2446
  const definitionRegistry = new Map(Array.from(this._baseToolDefinitions.entries())
2321
2447
  .filter(([name]) => isAllowedTool(name))
2322
2448
  .map(([name, definition]) => [
@@ -2326,7 +2452,7 @@ export class AgentSession {
2326
2452
  sourceInfo: createSyntheticSourceInfo(`<builtin:${name}>`, { source: "builtin" }),
2327
2453
  },
2328
2454
  ]));
2329
- for (const tool of allCustomTools) {
2455
+ for (const tool of allowedCustomTools) {
2330
2456
  definitionRegistry.set(tool.definition.name, {
2331
2457
  definition: tool.definition,
2332
2458
  sourceInfo: tool.sourceInfo,
@@ -2346,7 +2472,7 @@ export class AgentSession {
2346
2472
  })
2347
2473
  .filter((entry) => entry !== undefined));
2348
2474
  const runner = this._extensionRunner;
2349
- const wrappedExtensionTools = wrapRegisteredTools(allCustomTools, runner);
2475
+ const wrappedExtensionTools = wrapRegisteredTools(allowedCustomTools, runner);
2350
2476
  const wrappedBuiltInTools = wrapRegisteredTools(Array.from(this._baseToolDefinitions.values())
2351
2477
  .filter((definition) => isAllowedTool(definition.name))
2352
2478
  .map((definition) => ({
@@ -2498,7 +2624,7 @@ export class AgentSession {
2498
2624
  this._appendAutoRetryRecord(record);
2499
2625
  }
2500
2626
  catch (error) {
2501
- if (retry) {
2627
+ if (retry && !this._manualCompactionDrain) {
2502
2628
  this._emit({
2503
2629
  type: "auto_retry_end",
2504
2630
  success: false,
@@ -2508,7 +2634,7 @@ export class AgentSession {
2508
2634
  }
2509
2635
  throw this._reportRetryFailure(error);
2510
2636
  }
2511
- if (retry && end)
2637
+ if (retry && end && !this._manualCompactionDrain)
2512
2638
  this._emit(end);
2513
2639
  }
2514
2640
  _finishRetryWithoutSuccess(record, message) {
@@ -2645,6 +2771,8 @@ export class AgentSession {
2645
2771
  if (controller.signal.aborted || this._disposed) {
2646
2772
  return this._finishCancelledRetry("cancelled_during_backoff", backoff);
2647
2773
  }
2774
+ if (this._manualCompactionDrain)
2775
+ return this._finishCancelledRetry("cancelled_during_backoff", backoff);
2648
2776
  // Hand the chain to the continuation before `continue()`; the run that eventually emits
2649
2777
  // `agent_start` adopts it (see `_captureAgentRunSettlement`), however long Agent defers that start.
2650
2778
  const continuing = { phase: "continuing", attempt, chain: run.chain };
@@ -3051,6 +3179,43 @@ export class AgentSession {
3051
3179
  }
3052
3180
  return result;
3053
3181
  }
3182
+ // SCRAMJET-DIVERGENCE: whole-journal parent/tool costs plus reports awaiting successful append (#598).
3183
+ getRecordedSessionCost() {
3184
+ let total = 0;
3185
+ for (const entry of this.sessionManager.getEntries()) {
3186
+ if (entry.type !== "message")
3187
+ continue;
3188
+ const message = entry.message;
3189
+ if (message.role === "assistant") {
3190
+ if (validRecordedCost(message.usage?.cost?.total))
3191
+ total += message.usage.cost.total;
3192
+ }
3193
+ else if (message.role === "toolResult") {
3194
+ if (validRecordedCost(message.cost)) {
3195
+ total += message.cost;
3196
+ continue;
3197
+ }
3198
+ const definition = this._historicalCostDefinitions.get(message.toolName);
3199
+ if (!definition?.getHistoricalCost)
3200
+ continue;
3201
+ try {
3202
+ const cost = definition.getHistoricalCost(message.details);
3203
+ if (validRecordedCost(cost))
3204
+ total += cost;
3205
+ }
3206
+ catch (error) {
3207
+ this._extensionRunner.emitError({
3208
+ extensionPath: `tool:${message.toolName}`,
3209
+ event: "historical_cost",
3210
+ error: `Failed to interpret historical tool cost: ${error instanceof Error ? error.message : String(error)}`,
3211
+ });
3212
+ }
3213
+ }
3214
+ }
3215
+ for (const execution of this._pendingToolCosts)
3216
+ total += execution.cost ?? 0;
3217
+ return total;
3218
+ }
3054
3219
  /**
3055
3220
  * Get session statistics.
3056
3221
  */