@nexus-cortex/core 4.124.32 → 4.124.34

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.
Files changed (31) hide show
  1. package/.env.defaults +45 -0
  2. package/dist/config/effectiveConfig.d.ts.map +1 -1
  3. package/dist/config/effectiveConfig.js +4 -0
  4. package/dist/config/effectiveConfig.js.map +1 -1
  5. package/dist/orchestrator/CortexOrchestrator.d.ts +34 -0
  6. package/dist/orchestrator/CortexOrchestrator.d.ts.map +1 -1
  7. package/dist/orchestrator/CortexOrchestrator.js +152 -10
  8. package/dist/orchestrator/CortexOrchestrator.js.map +1 -1
  9. package/dist/orchestrator/OrchestratorFactory.d.ts.map +1 -1
  10. package/dist/orchestrator/OrchestratorFactory.js +3 -1
  11. package/dist/orchestrator/OrchestratorFactory.js.map +1 -1
  12. package/dist/orchestrator/computeNudge.d.ts +31 -0
  13. package/dist/orchestrator/computeNudge.d.ts.map +1 -0
  14. package/dist/orchestrator/computeNudge.js +59 -0
  15. package/dist/orchestrator/computeNudge.js.map +1 -0
  16. package/dist/orchestrator/effortRamp.d.ts +24 -0
  17. package/dist/orchestrator/effortRamp.d.ts.map +1 -0
  18. package/dist/orchestrator/effortRamp.js +21 -0
  19. package/dist/orchestrator/effortRamp.js.map +1 -0
  20. package/dist/orchestrator/emptyResponseClassifier.d.ts +15 -3
  21. package/dist/orchestrator/emptyResponseClassifier.d.ts.map +1 -1
  22. package/dist/orchestrator/emptyResponseClassifier.js +34 -8
  23. package/dist/orchestrator/emptyResponseClassifier.js.map +1 -1
  24. package/dist/orchestrator/turnStatus.d.ts +51 -0
  25. package/dist/orchestrator/turnStatus.d.ts.map +1 -0
  26. package/dist/orchestrator/turnStatus.js +126 -0
  27. package/dist/orchestrator/turnStatus.js.map +1 -0
  28. package/dist/training/DecisionStore.d.ts +1 -1
  29. package/dist/training/DecisionStore.d.ts.map +1 -1
  30. package/dist/training/DecisionStore.js.map +1 -1
  31. package/package.json +3 -3
@@ -47,6 +47,7 @@ import { sliceReadFile, decideSliceBlock, sliceReadStep } from './sliceBlock.js'
47
47
  import { TurnEvidence, evaluateEndTurnGates, buildMissingEndTurnReminder } from './endTurnGates.js';
48
48
  import { resolveCompactionResume, pickDropped, buildCompactionResumeReminder, isCompactionResumeMessage, resolveTaskText, resumeMemoryTargetTokens, coversAll, buildRollingSeedMessage, memoryIndexLine, upsertMemoryIndex, scaleEstimateToRequestView, approxCharsOf, anchoredRequestEstimate, resolveCompactionHandoffQA, resolveHandoffQAMaxQuestions, runHandoffQA } from './compactionResume.js';
49
49
  import { renderResumeMemoryPrompt, buildRebuildInstructions, resolveCheckpointBand, resolveRearmBand } from './compactionResumeTemplate.js';
50
+ import { resolveTurnStatusMode, turnStatusActive, buildTurnStatusLine, appendStatusToNewestToolResult } from './turnStatus.js';
50
51
  import { resolveCortexStateDir, cortexStatePath, resolveWorkspaceClean, cortexProjectDir } from '../utils/stateDir.js';
51
52
  import { collectWorkspaceDelta, detectCheckCommand, runCheck, resolveJudgeGroundingConfig, classifyCheckRun, isInvestigateCommandAllowed, specCheckParses, readFileSlice, formatEvidenceRound } from '../training/judgeEvidence.js';
52
53
  import { resolveMentorRoleConfig, describeMentorWire, describeMentorDelivery, mentorSurfaceTimeoutMs } from '../training/mentorRole.js';
@@ -88,7 +89,9 @@ import { ListAvailableMcpServers, SearchMcpServers, GetMcpConfig, EnableMcpServe
88
89
  import { InitCortexContext, MemoryWrite, MemoryRecall } from '../tools/context-management/index.js';
89
90
  import { pruneAgedToolResults } from '../conversation/ToolResultPruner.js';
90
91
  import { detectTailRepetition, tailLoopGuardEnabled } from './tailRepetitionDetector.js';
91
- import { classifyEmptyResponse, emptyResponseNudge, nudgeForbidsTools, emptyResponseNudgeFor, isReasoningExhaustion, stepDownEffort, resolveReasoningExhaustBackoff } from './emptyResponseClassifier.js';
92
+ import { resolveEffortRamp, rampEffortFor } from './effortRamp.js';
93
+ import { resolveComputeNudge, computeNudgeDecision, buildComputeNudgeLine, lastAssistantReasoningTokens } from './computeNudge.js';
94
+ import { classifyEmptyResponse, emptyResponseNudge, nudgeForbidsTools, emptyResponseNudgeFor, isReasoningExhaustion, stepDownEffort, effortLadderFor, resolveExhaustLadderMode, resolveReasoningExhaustBackoff } from './emptyResponseClassifier.js';
92
95
  import { isImageRejectionError, stripRejectedImages } from './imageRejectionHeal.js'; // R154
93
96
  // PTC: Progressive tool loading for non-PTC providers
94
97
  import { ClientSideToolFilter } from '../tools/ClientSideToolFilter.js';
@@ -154,6 +157,10 @@ export class CortexOrchestrator {
154
157
  // R132 HB-COMPACTION-ESTIMATE: the last REAL prompt_tokens the provider reported + the request-view char size at that moment.
155
158
  // ensureHistoryFitsModel anchors its estimate here (anchor + grown chars/4) instead of the ~5-7x over-reading heuristic.
156
159
  lastUsageAnchor = null;
160
+ /** HB-TURN-STATUS: running background shells (executors' BackgroundProcessRegistry, wired by OrchestratorFactory). */
161
+ backgroundShellsProvider = null;
162
+ /** HB-TURN-STATUS: status tails appended this turn (banked once per turn as a turn_status event). */
163
+ turnStatusLines = 0;
157
164
  /** R150: last error message that ended (or interrupted) the tool loop — banked on the abnormal-exit-bypass event. */
158
165
  lastToolLoopError = '';
159
166
  // Responses API stateful chaining: track last response ID for XAI/OpenAI
@@ -2106,7 +2113,7 @@ export class CortexOrchestrator {
2106
2113
  temperature: options.parameters?.temperature,
2107
2114
  maxTokens: options.parameters?.maxTokens,
2108
2115
  topP: options.parameters?.topP,
2109
- reasoningEffort: options.parameters?.reasoningEffort, // GPT-5.1 reasoning level
2116
+ reasoningEffort: this.nextRampEffort(true) ?? options.parameters?.reasoningEffort, // HB-EFFORT-RAMP (dark) > request param
2110
2117
  stream: options.streaming,
2111
2118
  staticSystemPrompt: this.currentStaticSystemPrompt, // R28
2112
2119
  conversationId: this.currentConversationId, // R28b
@@ -2411,6 +2418,7 @@ export class CortexOrchestrator {
2411
2418
  this.herdrReport('working', this.herdrTurnLabel); // R145
2412
2419
  let timeWarnFired = false;
2413
2420
  let lastBudgetBand = -1; // R151: budget-visibility band already announced
2421
+ this.turnStatusLines = 0; // HB-TURN-STATUS
2414
2422
  let totalToolErrors = 0;
2415
2423
  // Round 18b: track whether we've already retried on empty response so
2416
2424
  // we don't loop forever. See empty-detection block below.
@@ -2563,7 +2571,7 @@ export class CortexOrchestrator {
2563
2571
  }
2564
2572
  console.warn(`[Orchestrator] Empty response detected (${emptyClass.kind}, hadReasoning=${emptyClass.hadReasoning}, iteration=${toolCallIteration}). ` +
2565
2573
  `Retrying once with explicit completion prompt.`);
2566
- const exhaustionLevel = this.armReasoningBackoffIfExhausted(emptyClass, options.parameters?.reasoningEffort ?? effectiveModel?.reasoning?.effort, toolCallIteration, currentAssistantMessage?.usage?.outputTokens);
2574
+ const exhaustionLevel = this.armReasoningBackoffIfExhausted(emptyClass, options.parameters?.reasoningEffort ?? effectiveModel?.reasoning?.effort, toolCallIteration, currentAssistantMessage?.usage?.outputTokens, effectiveModel.provider);
2567
2575
  // R26 (2026-05-15, surfaced by A/B benchmark): the empty assistant
2568
2576
  // turn is already in messageHistory. The retry below rebuilds the
2569
2577
  // full history and re-sends it. Anthropic's Messages API tolerates
@@ -3496,6 +3504,10 @@ export class CortexOrchestrator {
3496
3504
  }
3497
3505
  ladderSignal = null; // consumed — never re-inject on later iterations
3498
3506
  }
3507
+ // HB-COMPUTE-NUDGE (CORTEX_COMPUTE_NUDGE, dark): after a heavily-reasoned step, 'compute/test instead' at the same tail.
3508
+ this.injectComputeNudge(toolCallIteration, false);
3509
+ // HB-TURN-STATUS (CORTEX_TURN_STATUS, dark): clock + context + bg shells at the TAIL of this round's newest tool_result.
3510
+ this.injectTurnStatus(loopStartMs, TURN_DEADLINE_MS, effectiveModel, toolCallIteration);
3499
3511
  // Ladder break (teach-then-break): the break instruction is already in
3500
3512
  // the tool result above; exit the loop R29b-style so the post-loop
3501
3513
  // synthesis net produces an honest final answer — never the blunt
@@ -3625,7 +3637,7 @@ export class CortexOrchestrator {
3625
3637
  // where the window is empty). Attaching it only to the initial assembly made the force unreachable.
3626
3638
  const continuationForcedChoice = await this.resolveForcedMentorChoice(toolsToUse, toolCallIteration);
3627
3639
  // R153: resolve the effort ONCE (the backoff/pulse counters are consumed per call, not per build).
3628
- const continuationEffort = this.consumeReasoningBackoff() ?? this.consumeEffortPulse() ?? options.parameters?.reasoningEffort; // R153 backoff > effort pulse > request param > card
3640
+ const continuationEffort = this.consumeReasoningBackoff() ?? this.consumeEffortPulse() ?? this.nextRampEffort() ?? options.parameters?.reasoningEffort; // R153 backoff > effort pulse > HB-EFFORT-RAMP > request param > card
3629
3641
  // R154: the request is built by a closure so the image-rejection heal can rebuild it from the
3630
3642
  // (stubbed) history and retry once. Byte-identical request when no heal is needed.
3631
3643
  const buildContinuationRequest = () => {
@@ -4062,6 +4074,7 @@ export class CortexOrchestrator {
4062
4074
  // 16. Build orchestrator response (use final message from multi-turn loop)
4063
4075
  // Include all executed tool uses from all iterations (not just final message)
4064
4076
  this.herdrReport('idle', this.herdrTurnLabel); // R145: turn returned
4077
+ this.bankTurnStatusEvent(false); // HB-TURN-STATUS
4065
4078
  return {
4066
4079
  messageId: currentAssistantMessage.uuid,
4067
4080
  content: currentAssistantCanonicalMessage.content,
@@ -4441,7 +4454,7 @@ export class CortexOrchestrator {
4441
4454
  temperature: options.parameters?.temperature,
4442
4455
  maxTokens: options.parameters?.maxTokens,
4443
4456
  topP: options.parameters?.topP,
4444
- reasoningEffort: options.parameters?.reasoningEffort, // GPT-5.1 reasoning level
4457
+ reasoningEffort: this.nextRampEffort(true) ?? options.parameters?.reasoningEffort, // HB-EFFORT-RAMP (dark) > request param
4445
4458
  stream: true, // Enable streaming!
4446
4459
  staticSystemPrompt: this.currentStaticSystemPrompt, // R28
4447
4460
  conversationId: this.currentConversationId, // R28b
@@ -4707,6 +4720,7 @@ export class CortexOrchestrator {
4707
4720
  this.herdrReport('working', this.herdrTurnLabel); // R145
4708
4721
  let timeWarnFired = false;
4709
4722
  let lastBudgetBand = -1; // R151: budget-visibility band already announced
4723
+ this.turnStatusLines = 0; // HB-TURN-STATUS
4710
4724
  // HB-ENDTURN-TERMINAL (2026-09-08): dark gate + bounded continue-counter (streaming parity).
4711
4725
  const EMPTY_TURN_CONTINUE = loopDefaults.emptyTurnContinue;
4712
4726
  const EMPTY_CONTINUE_MAX = 3;
@@ -4763,7 +4777,7 @@ export class CortexOrchestrator {
4763
4777
  }
4764
4778
  console.warn(`[Orchestrator Streaming] R32/R18b: Empty response (${emptyClass.kind}, iteration=${toolCallIteration}). ` +
4765
4779
  `Retrying with tools preserved.`);
4766
- const exhaustionLevel = this.armReasoningBackoffIfExhausted(emptyClass, options.parameters?.reasoningEffort ?? effectiveModel?.reasoning?.effort, toolCallIteration, this.messageHistory[this.messageHistory.length - 1]?.usage?.outputTokens);
4780
+ const exhaustionLevel = this.armReasoningBackoffIfExhausted(emptyClass, options.parameters?.reasoningEffort ?? effectiveModel?.reasoning?.effort, toolCallIteration, this.messageHistory[this.messageHistory.length - 1]?.usage?.outputTokens, effectiveModel.provider);
4767
4781
  // R26 repair: ensure the empty assistant turn has content (xAI hard-400s on empty)
4768
4782
  for (let i = this.messageHistory.length - 1; i >= 0; i--) {
4769
4783
  const m = this.messageHistory[i];
@@ -5314,6 +5328,10 @@ export class CortexOrchestrator {
5314
5328
  }
5315
5329
  ladderSignal = null; // consumed
5316
5330
  }
5331
+ // HB-COMPUTE-NUDGE (streaming parity).
5332
+ this.injectComputeNudge(toolCallIteration, true);
5333
+ // HB-TURN-STATUS (streaming parity): same tail, same carrier.
5334
+ this.injectTurnStatus(loopStartMs, TURN_DEADLINE_MS, effectiveModel, toolCallIteration);
5317
5335
  // Ladder break (streaming parity): instruction already injected above;
5318
5336
  // exit R29b-style so the streaming synthesis net closes the turn.
5319
5337
  if (ladderBreak) {
@@ -5419,7 +5437,7 @@ export class CortexOrchestrator {
5419
5437
  // §13-B2: re-evaluate the forced mentor choice on THIS streaming continuation — thrash
5420
5438
  // develops here, not on the initial request (mirror of the non-stream continuation fix).
5421
5439
  const continuationForcedChoice = await this.resolveForcedMentorChoice(toolsToUse, toolCallIteration);
5422
- const streamContinuationEffort = this.consumeReasoningBackoff() ?? this.consumeEffortPulse() ?? options.parameters?.reasoningEffort; // R153 backoff > effort pulse > request param > card
5440
+ const streamContinuationEffort = this.consumeReasoningBackoff() ?? this.consumeEffortPulse() ?? this.nextRampEffort() ?? options.parameters?.reasoningEffort; // R153 backoff > effort pulse > HB-EFFORT-RAMP > request param > card
5423
5441
  // Phase 2.8: Prepare continuation request (R154: built by a closure so the image-rejection heal can
5424
5442
  // rebuild it from the stubbed history and re-stream once; byte-identical request otherwise).
5425
5443
  // All providers maintain thinking throughout multi-turn execution
@@ -5907,6 +5925,7 @@ export class CortexOrchestrator {
5907
5925
  }
5908
5926
  // Yield message_stop with usage data for CLI turn summary display
5909
5927
  this.herdrReport('idle', this.herdrTurnLabel); // R145: turn returned (streaming)
5928
+ this.bankTurnStatusEvent(true); // HB-TURN-STATUS
5910
5929
  const finalUsage = convertedResponse?.usage || { inputTokens: 0, outputTokens: 0 };
5911
5930
  yield {
5912
5931
  type: 'message_stop',
@@ -6750,6 +6769,65 @@ export class CortexOrchestrator {
6750
6769
  return '';
6751
6770
  }
6752
6771
  }
6772
+ /** HB-TURN-STATUS: OrchestratorFactory wires executors' BackgroundProcessRegistry here (keeps core free of the dependency). */
6773
+ setBackgroundShellsProvider(provider) {
6774
+ this.backgroundShellsProvider = provider;
6775
+ }
6776
+ /**
6777
+ * HB-TURN-STATUS (CORTEX_TURN_STATUS=off|auto|on; default off = byte-identical): append one STATUS line (elapsed/remaining
6778
+ * on the turn clock the deadline rungs use, usage-anchored context estimate, running background shells) to the tail of the
6779
+ * newest tool_result of this round. Persisted in the in-memory history like the R151 band line, so later requests keep a
6780
+ * byte-identical prefix and only the new tail varies. Fail-open.
6781
+ */
6782
+ injectTurnStatus(loopStartMs, deadlineMs, model, iteration) {
6783
+ try {
6784
+ if (!turnStatusActive(resolveTurnStatusMode(), deadlineMs))
6785
+ return;
6786
+ const now = Date.now();
6787
+ let contextTokens = 0;
6788
+ const anchor = this.lastUsageAnchor;
6789
+ if (anchor && anchor.promptTokens > 0) {
6790
+ try {
6791
+ const currentChars = approxCharsOf(this.convertToCanonicalMessages(this.messageHistory));
6792
+ contextTokens = anchoredRequestEstimate({ anchorTokens: anchor.promptTokens, anchorChars: anchor.requestChars, currentChars, heuristicTokens: anchor.promptTokens }).tokens;
6793
+ }
6794
+ catch {
6795
+ contextTokens = anchor.promptTokens;
6796
+ }
6797
+ }
6798
+ let shells = [];
6799
+ try {
6800
+ shells = this.backgroundShellsProvider?.() ?? [];
6801
+ }
6802
+ catch {
6803
+ shells = [];
6804
+ }
6805
+ const line = buildTurnStatusLine({
6806
+ elapsedMs: now - loopStartMs,
6807
+ deadlineMs,
6808
+ contextTokens,
6809
+ contextWindow: model.limits?.contextWindow ?? this.lastKnownContextWindow,
6810
+ shells,
6811
+ nowMs: now,
6812
+ });
6813
+ if (appendStatusToNewestToolResult(this.messageHistory, line)) {
6814
+ this.turnStatusLines++;
6815
+ if (this.config.debug)
6816
+ console.log(`[TurnStatus] iteration ${iteration}: ${line}`);
6817
+ }
6818
+ }
6819
+ catch { /* fail-open: never break the loop for a status line */ }
6820
+ }
6821
+ /** HB-TURN-STATUS: one turn_status event per turn (lines appended) — proves engagement without a per-round row. */
6822
+ bankTurnStatusEvent(streaming) {
6823
+ if (this.turnStatusLines <= 0)
6824
+ return;
6825
+ const lines = this.turnStatusLines;
6826
+ this.turnStatusLines = 0;
6827
+ const store = this.getDecisionStore();
6828
+ if (store)
6829
+ void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'turn_status', detail: { lines, mode: resolveTurnStatusMode(), streaming } }).catch(() => { });
6830
+ }
6753
6831
  /**
6754
6832
  * R132 HB-COMPACTION-ESTIMATE: bank the last real prompt_tokens as the compaction estimate anchor. Called right after
6755
6833
  * every cacheMetricsAccumulator.addUsage; a 0/absent inputTokens (the synthesized streaming chat/completions usage) is
@@ -9101,8 +9179,70 @@ export class CortexOrchestrator {
9101
9179
  this.reasoningBackoffRemaining -= 1;
9102
9180
  return this.reasoningBackoffLevel;
9103
9181
  }
9182
+ /** HB-EFFORT-RAMP: index of the next MAIN action call in this turn (0 = the initial request); retry/synthesis calls don't count. */
9183
+ effortRampCallIndex = 0;
9184
+ effortRampCalls = 0;
9185
+ /** HB-COMPUTE-NUDGE per-turn state. */
9186
+ computeNudgeLastRound = -1_000_000;
9187
+ computeNudgeFired = 0;
9188
+ /**
9189
+ * HB-EFFORT-RAMP (CORTEX_EFFORT_RAMP, dark): the ramp level for the next main action call, or undefined (off / ramp over).
9190
+ * `startTurn` (the initial request of sendMessage/streamMessage) resets the per-turn counters (ramp + compute nudge).
9191
+ * Banks one effort_ramp event when the ramp hands back to the configured effort (and at the first ramped call).
9192
+ */
9193
+ nextRampEffort(startTurn = false) {
9194
+ if (startTurn) {
9195
+ this.effortRampCallIndex = 0;
9196
+ this.effortRampCalls = 0;
9197
+ this.computeNudgeLastRound = -1_000_000;
9198
+ this.computeNudgeFired = 0;
9199
+ }
9200
+ try {
9201
+ const cfg = resolveEffortRamp();
9202
+ if (!cfg.enabled)
9203
+ return undefined;
9204
+ const idx = this.effortRampCallIndex++;
9205
+ const level = rampEffortFor(cfg, idx);
9206
+ const store = this.getDecisionStore();
9207
+ if (level) {
9208
+ this.effortRampCalls++;
9209
+ if (idx === 0 && store)
9210
+ void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'effort_ramp', detail: { phase: 'start', level, calls: cfg.calls } }).catch(() => { });
9211
+ }
9212
+ else if (idx === cfg.calls && store) {
9213
+ void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'effort_ramp', detail: { phase: 'end', level: cfg.level, rampedCalls: this.effortRampCalls } }).catch(() => { });
9214
+ }
9215
+ return level;
9216
+ }
9217
+ catch {
9218
+ return undefined;
9219
+ }
9220
+ }
9221
+ /**
9222
+ * HB-COMPUTE-NUDGE (CORTEX_COMPUTE_NUDGE=on, dark): when the response that issued this round reasoned past the threshold,
9223
+ * append one 'compute/test instead of deliberating' line to the tail of the newest tool_result (the HB-TURN-STATUS carrier —
9224
+ * earlier request prefixes stay byte-identical). Cooldown + per-turn cap; one compute_nudge event per firing. Fail-open.
9225
+ */
9226
+ injectComputeNudge(round, streaming) {
9227
+ try {
9228
+ const cfg = resolveComputeNudge();
9229
+ if (!cfg.enabled)
9230
+ return;
9231
+ const reasoningTokens = lastAssistantReasoningTokens(this.messageHistory);
9232
+ if (!computeNudgeDecision(cfg, { reasoningTokens, round, lastFiredRound: this.computeNudgeLastRound, firedThisTurn: this.computeNudgeFired }))
9233
+ return;
9234
+ if (!appendStatusToNewestToolResult(this.messageHistory, buildComputeNudgeLine(reasoningTokens)))
9235
+ return;
9236
+ this.computeNudgeLastRound = round;
9237
+ this.computeNudgeFired++;
9238
+ const store = this.getDecisionStore();
9239
+ if (store)
9240
+ void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'compute_nudge', detail: { round, reasoningTokens, fired: this.computeNudgeFired, threshold: cfg.thresholdTokens, streaming } }).catch(() => { });
9241
+ }
9242
+ catch { /* fail-open */ }
9243
+ }
9104
9244
  /** Arms the backoff when the empty turn is a reasoning exhaustion; returns the level the next calls will use. */
9105
- armReasoningBackoffIfExhausted(cls, effectiveEffort, iteration, outputTokens) {
9245
+ armReasoningBackoffIfExhausted(cls, effectiveEffort, iteration, outputTokens, provider) {
9106
9246
  if (!isReasoningExhaustion(cls))
9107
9247
  return undefined;
9108
9248
  const cfg = resolveReasoningExhaustBackoff();
@@ -9116,13 +9256,15 @@ export class CortexOrchestrator {
9116
9256
  // A repeat exhaustion in the same turn steps down AGAIN from the last level (high → medium → low), whether or not the
9117
9257
  // counter is still armed (R153b). The counter covers the empty-retry call PLUS cfg.turns continuations.
9118
9258
  const base = this.reasoningBackoffLastLevel ?? effectiveEffort;
9119
- const level = stepDownEffort(base);
9259
+ // R153c: step down the PROVIDER's real ladder (DeepSeek: low | high | max — the legacy high → medium step ran as high).
9260
+ const ladderMode = resolveExhaustLadderMode();
9261
+ const level = stepDownEffort(base, ladderMode === 'provider' ? effortLadderFor(provider) : undefined);
9120
9262
  this.reasoningBackoffLevel = level;
9121
9263
  this.reasoningBackoffLastLevel = level;
9122
9264
  this.reasoningBackoffRemaining = cfg.turns + 1;
9123
9265
  console.warn(`[Orchestrator] R153: reasoning exhaustion at iteration ${iteration} (${outputTokens ?? '?'} output tokens, no text/tool) — next ${cfg.turns} continuation(s) at effort '${level}' (was '${base ?? 'card default'}').`);
9124
9266
  if (store)
9125
- void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'reasoning_exhaustion', detail: { iteration, outputTokens, from: base ?? null, level, turns: cfg.turns } }).catch(() => { });
9267
+ void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'reasoning_exhaustion', detail: { iteration, outputTokens, from: base ?? null, level, turns: cfg.turns, ladder: ladderMode, provider: provider ?? null } }).catch(() => { });
9126
9268
  return level;
9127
9269
  }
9128
9270
  /** R154 HB-IMAGE-REJECTION-HEAL: send a request built from messageHistory; if the provider rejects an image