@nexus-cortex/core 4.130.0 → 4.131.2

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 (184) hide show
  1. package/.env.defaults +172 -2
  2. package/dist/adapters/ChatCompletionsAPIAdapter.d.ts.map +1 -1
  3. package/dist/adapters/ChatCompletionsAPIAdapter.js +5 -1
  4. package/dist/adapters/ChatCompletionsAPIAdapter.js.map +1 -1
  5. package/dist/commands/SlashCommandRegistry.d.ts.map +1 -1
  6. package/dist/commands/SlashCommandRegistry.js +7 -0
  7. package/dist/commands/SlashCommandRegistry.js.map +1 -1
  8. package/dist/config/AnthropicCredentialService.d.ts.map +1 -1
  9. package/dist/config/AnthropicCredentialService.js +2 -1
  10. package/dist/config/AnthropicCredentialService.js.map +1 -1
  11. package/dist/config/RuntimeConfigRegistry.d.ts.map +1 -1
  12. package/dist/config/RuntimeConfigRegistry.js +18 -0
  13. package/dist/config/RuntimeConfigRegistry.js.map +1 -1
  14. package/dist/config/SettingsLoader.d.ts +3 -0
  15. package/dist/config/SettingsLoader.d.ts.map +1 -1
  16. package/dist/config/SettingsLoader.js +42 -0
  17. package/dist/config/SettingsLoader.js.map +1 -1
  18. package/dist/config/SettingsSchema.d.ts +19 -0
  19. package/dist/config/SettingsSchema.d.ts.map +1 -1
  20. package/dist/config/SettingsSchema.js +172 -0
  21. package/dist/config/SettingsSchema.js.map +1 -1
  22. package/dist/config/effectiveConfig.d.ts.map +1 -1
  23. package/dist/config/effectiveConfig.js +20 -2
  24. package/dist/config/effectiveConfig.js.map +1 -1
  25. package/dist/config/index.d.ts +4 -0
  26. package/dist/config/index.d.ts.map +1 -1
  27. package/dist/config/index.js +4 -0
  28. package/dist/config/index.js.map +1 -1
  29. package/dist/config/localBrowser.d.ts +13 -0
  30. package/dist/config/localBrowser.d.ts.map +1 -0
  31. package/dist/config/localBrowser.js +53 -0
  32. package/dist/config/localBrowser.js.map +1 -0
  33. package/dist/config/logRedaction.d.ts +5 -0
  34. package/dist/config/logRedaction.d.ts.map +1 -0
  35. package/dist/config/logRedaction.js +68 -0
  36. package/dist/config/logRedaction.js.map +1 -0
  37. package/dist/config/secretProviders.d.ts +16 -0
  38. package/dist/config/secretProviders.d.ts.map +1 -0
  39. package/dist/config/secretProviders.js +26 -0
  40. package/dist/config/secretProviders.js.map +1 -0
  41. package/dist/config/secretsVault.d.ts +121 -0
  42. package/dist/config/secretsVault.d.ts.map +1 -0
  43. package/dist/config/secretsVault.js +320 -0
  44. package/dist/config/secretsVault.js.map +1 -0
  45. package/dist/config/spaSecretTypes.d.ts +329 -0
  46. package/dist/config/spaSecretTypes.d.ts.map +1 -0
  47. package/dist/config/spaSecretTypes.js +326 -0
  48. package/dist/config/spaSecretTypes.js.map +1 -0
  49. package/dist/index.d.ts +9 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +5 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/mcp/McpClient.d.ts +2 -0
  54. package/dist/mcp/McpClient.d.ts.map +1 -1
  55. package/dist/mcp/McpClient.js +9 -1
  56. package/dist/mcp/McpClient.js.map +1 -1
  57. package/dist/mcp/McpServerRegistry.d.ts +2 -13
  58. package/dist/mcp/McpServerRegistry.d.ts.map +1 -1
  59. package/dist/mcp/McpServerRegistry.js +17 -2
  60. package/dist/mcp/McpServerRegistry.js.map +1 -1
  61. package/dist/mcp/essentialMcpTools.d.ts +11 -0
  62. package/dist/mcp/essentialMcpTools.d.ts.map +1 -0
  63. package/dist/mcp/essentialMcpTools.js +22 -0
  64. package/dist/mcp/essentialMcpTools.js.map +1 -0
  65. package/dist/mcp/mcpSchemaCompat.d.ts +9 -0
  66. package/dist/mcp/mcpSchemaCompat.d.ts.map +1 -0
  67. package/dist/mcp/mcpSchemaCompat.js +32 -0
  68. package/dist/mcp/mcpSchemaCompat.js.map +1 -0
  69. package/dist/middleware/HelperModelMiddleware.d.ts +4 -0
  70. package/dist/middleware/HelperModelMiddleware.d.ts.map +1 -1
  71. package/dist/middleware/HelperModelMiddleware.js +2 -2
  72. package/dist/middleware/HelperModelMiddleware.js.map +1 -1
  73. package/dist/middleware/SystemMessageMiddleware.d.ts.map +1 -1
  74. package/dist/middleware/SystemMessageMiddleware.js +6 -0
  75. package/dist/middleware/SystemMessageMiddleware.js.map +1 -1
  76. package/dist/middleware/permissions/DefaultPolicies.d.ts.map +1 -1
  77. package/dist/middleware/permissions/DefaultPolicies.js +2 -0
  78. package/dist/middleware/permissions/DefaultPolicies.js.map +1 -1
  79. package/dist/orchestrator/APIClient.d.ts.map +1 -1
  80. package/dist/orchestrator/APIClient.js +46 -1
  81. package/dist/orchestrator/APIClient.js.map +1 -1
  82. package/dist/orchestrator/CortexOrchestrator.d.ts +43 -8
  83. package/dist/orchestrator/CortexOrchestrator.d.ts.map +1 -1
  84. package/dist/orchestrator/CortexOrchestrator.js +341 -40
  85. package/dist/orchestrator/CortexOrchestrator.js.map +1 -1
  86. package/dist/orchestrator/HerdrSubAgentRunner.d.ts.map +1 -1
  87. package/dist/orchestrator/HerdrSubAgentRunner.js +2 -1
  88. package/dist/orchestrator/HerdrSubAgentRunner.js.map +1 -1
  89. package/dist/orchestrator/SubAgentProcessManager.d.ts.map +1 -1
  90. package/dist/orchestrator/SubAgentProcessManager.js +2 -0
  91. package/dist/orchestrator/SubAgentProcessManager.js.map +1 -1
  92. package/dist/orchestrator/browserWallNudge.d.ts +93 -0
  93. package/dist/orchestrator/browserWallNudge.d.ts.map +1 -0
  94. package/dist/orchestrator/browserWallNudge.js +227 -0
  95. package/dist/orchestrator/browserWallNudge.js.map +1 -0
  96. package/dist/orchestrator/citationVerification.d.ts.map +1 -1
  97. package/dist/orchestrator/citationVerification.js +16 -0
  98. package/dist/orchestrator/citationVerification.js.map +1 -1
  99. package/dist/orchestrator/cortexProxyFetch.d.ts +8 -0
  100. package/dist/orchestrator/cortexProxyFetch.d.ts.map +1 -1
  101. package/dist/orchestrator/cortexProxyFetch.js +32 -0
  102. package/dist/orchestrator/cortexProxyFetch.js.map +1 -1
  103. package/dist/orchestrator/emptyResponseClassifier.d.ts +1 -1
  104. package/dist/orchestrator/emptyResponseClassifier.d.ts.map +1 -1
  105. package/dist/orchestrator/emptyResponseClassifier.js +8 -2
  106. package/dist/orchestrator/emptyResponseClassifier.js.map +1 -1
  107. package/dist/orchestrator/endTurnGates.d.ts +39 -0
  108. package/dist/orchestrator/endTurnGates.d.ts.map +1 -1
  109. package/dist/orchestrator/endTurnGates.js +159 -3
  110. package/dist/orchestrator/endTurnGates.js.map +1 -1
  111. package/dist/orchestrator/imageTtl.d.ts.map +1 -1
  112. package/dist/orchestrator/imageTtl.js +14 -5
  113. package/dist/orchestrator/imageTtl.js.map +1 -1
  114. package/dist/orchestrator/integrityVerification.d.ts +2 -0
  115. package/dist/orchestrator/integrityVerification.d.ts.map +1 -1
  116. package/dist/orchestrator/integrityVerification.js +2 -2
  117. package/dist/orchestrator/integrityVerification.js.map +1 -1
  118. package/dist/orchestrator/requestDebugLog.d.ts +4 -0
  119. package/dist/orchestrator/requestDebugLog.d.ts.map +1 -0
  120. package/dist/orchestrator/requestDebugLog.js +55 -0
  121. package/dist/orchestrator/requestDebugLog.js.map +1 -0
  122. package/dist/orchestrator/requirementsVerification.d.ts +4 -0
  123. package/dist/orchestrator/requirementsVerification.d.ts.map +1 -1
  124. package/dist/orchestrator/requirementsVerification.js +12 -6
  125. package/dist/orchestrator/requirementsVerification.js.map +1 -1
  126. package/dist/orchestrator/subAgentTimeout.d.ts +3 -0
  127. package/dist/orchestrator/subAgentTimeout.d.ts.map +1 -1
  128. package/dist/orchestrator/subAgentTimeout.js +6 -0
  129. package/dist/orchestrator/subAgentTimeout.js.map +1 -1
  130. package/dist/orchestrator/toolAllowlist.d.ts +29 -0
  131. package/dist/orchestrator/toolAllowlist.d.ts.map +1 -1
  132. package/dist/orchestrator/toolAllowlist.js +45 -0
  133. package/dist/orchestrator/toolAllowlist.js.map +1 -1
  134. package/dist/tools/ToolProfile.d.ts +5 -21
  135. package/dist/tools/ToolProfile.d.ts.map +1 -1
  136. package/dist/tools/ToolProfile.js +17 -2
  137. package/dist/tools/ToolProfile.js.map +1 -1
  138. package/dist/tools/registries/BaseToolRegistry.d.ts.map +1 -1
  139. package/dist/tools/registries/BaseToolRegistry.js +26 -6
  140. package/dist/tools/registries/BaseToolRegistry.js.map +1 -1
  141. package/dist/training/AutoResearchConfirm.d.ts +105 -0
  142. package/dist/training/AutoResearchConfirm.d.ts.map +1 -0
  143. package/dist/training/AutoResearchConfirm.js +111 -0
  144. package/dist/training/AutoResearchConfirm.js.map +1 -0
  145. package/dist/training/AutoResearchGate.d.ts +6 -0
  146. package/dist/training/AutoResearchGate.d.ts.map +1 -1
  147. package/dist/training/AutoResearchGate.js +8 -2
  148. package/dist/training/AutoResearchGate.js.map +1 -1
  149. package/dist/training/AutoResearchGuards.d.ts +86 -0
  150. package/dist/training/AutoResearchGuards.d.ts.map +1 -0
  151. package/dist/training/AutoResearchGuards.js +81 -0
  152. package/dist/training/AutoResearchGuards.js.map +1 -0
  153. package/dist/training/BenchRunner.d.ts +21 -5
  154. package/dist/training/BenchRunner.d.ts.map +1 -1
  155. package/dist/training/BenchRunner.js +126 -54
  156. package/dist/training/BenchRunner.js.map +1 -1
  157. package/dist/training/EvalGuard.d.ts +43 -0
  158. package/dist/training/EvalGuard.d.ts.map +1 -0
  159. package/dist/training/EvalGuard.js +131 -0
  160. package/dist/training/EvalGuard.js.map +1 -0
  161. package/dist/training/ExperimentRunner.d.ts +22 -0
  162. package/dist/training/ExperimentRunner.d.ts.map +1 -1
  163. package/dist/training/ExperimentRunner.js +45 -9
  164. package/dist/training/ExperimentRunner.js.map +1 -1
  165. package/dist/training/endTurnResolver.d.ts +23 -1
  166. package/dist/training/endTurnResolver.d.ts.map +1 -1
  167. package/dist/training/endTurnResolver.js +35 -3
  168. package/dist/training/endTurnResolver.js.map +1 -1
  169. package/dist/training/mentorRole.d.ts.map +1 -1
  170. package/dist/training/mentorRole.js +6 -1
  171. package/dist/training/mentorRole.js.map +1 -1
  172. package/dist/training/scopeCheck.d.ts +41 -0
  173. package/dist/training/scopeCheck.d.ts.map +1 -0
  174. package/dist/training/scopeCheck.js +119 -0
  175. package/dist/training/scopeCheck.js.map +1 -0
  176. package/dist/training/usageAccounting.d.ts +13 -0
  177. package/dist/training/usageAccounting.d.ts.map +1 -1
  178. package/dist/training/usageAccounting.js +11 -0
  179. package/dist/training/usageAccounting.js.map +1 -1
  180. package/dist/utils/repairToolJson.d.ts +19 -0
  181. package/dist/utils/repairToolJson.d.ts.map +1 -0
  182. package/dist/utils/repairToolJson.js +122 -0
  183. package/dist/utils/repairToolJson.js.map +1 -0
  184. package/package.json +5 -4
@@ -9,6 +9,9 @@
9
9
  */
10
10
  import { liftPlanAllowedInSession } from '../config/interactiveProfile.js';
11
11
  import { featureAppliedKeys } from '../config/featurePayload.js';
12
+ import { maskToolResults, maskSecretText } from '../config/secretsVault.js';
13
+ import { resolveBrowserBackend } from '../config/localBrowser.js';
14
+ import { normalizeMcpSchema } from '../mcp/mcpSchemaCompat.js';
12
15
  import { v4 as uuidv4 } from 'uuid';
13
16
  import { ToolNamingHandler } from '../adapters/ToolNamingHandler.js';
14
17
  // Phase 2.4: Server-Side Tools Support
@@ -22,7 +25,7 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
22
25
  import { execSync } from 'node:child_process';
23
26
  import { ENV_RECON_COMMAND, resolveLiftPlanConfig, parsePlannerResponse, bankPlanText } from '../training/liftPlanner.js';
24
27
  import { clipIn, FULL_CAPS, HINTS, resolveInventory, resolveInventoryPath } from '../training/steerInputs.js';
25
- import { resolveEndTurnResolverConfig, parseResolverVerdict, effectiveMaxRejects, budgetFloorActive, applyBudgetFloor, flooredEvidenceCap, budgetedVetoEscalation, decideVetoAction, shouldFinishConfirm, buildFinishConfirmMessage, buildMeetsConfirmMessage, gapHoldable, parseSpecChecks, groundSpecCheck, specFailFraction, applyVetoFloor, holdProgressed, planSimilarity, specCheckEvidence, specBlockForJudge, dropSpecEchoes, specBlockForWriter, vetoMessageLead, RESOLVER_SCALE_CLAUSE } from '../training/endTurnResolver.js';
28
+ import { resolveEndTurnResolverConfig, parseResolverVerdict, effectiveMaxRejects, budgetFloorActive, applyBudgetFloor, flooredEvidenceCap, budgetedVetoEscalation, decideVetoAction, shouldFinishConfirm, webTurnEvidenced, webFinishStands, resolverWebRuleEnabled, buildFinishConfirmMessage, buildMeetsConfirmMessage, gapHoldable, parseSpecChecks, groundSpecCheck, specFailFraction, applyVetoFloor, holdProgressed, planSimilarity, specCheckEvidence, specBlockForJudge, dropSpecEchoes, specBlockForWriter, vetoMessageLead, RESOLVER_SCALE_CLAUSE } from '../training/endTurnResolver.js';
26
29
  import { jevAvailable, jevNoul, buildGapHoldState, GAP_HOLD_QUESTIONS } from '../training/jevGate.js'; // R173b
27
30
  import { resolveAcceptGateConfig, buildAcceptGateState, decideAcceptGate, decisionNoul, formatAcceptGateHoldMessage, isAcceptClass, ACCEPT_GATE_QUESTIONS } from '../training/acceptGate.js'; // R260
28
31
  import { resolveFrameConfig, FRAME_CONTRACT, FRAME_TOOL_NAME } from '../frames/frameConfig.js'; // R179
@@ -46,19 +49,22 @@ import { CacheMetricsAccumulator } from '../session/CacheMetricsAccumulator.js';
46
49
  import { SubAgentProcessManager, createSubAgentProcessManager } from './SubAgentProcessManager.js';
47
50
  // Phase 1: Tool Architecture Refactor - Unified Tool Registry
48
51
  import { toolFactory } from '../tools/ToolFactory.js';
49
- import { resolveToolProfile, resolveToolAnchor, resolveFrameProfile, resolveLiftNudge, resolveLiftPlan, resolveEndTurnResolver, resolveHeadlessDropAskUser, resolveDeferredLoading, isNarrowProfile, isToolAllowedByProfile, applyToolProfile, webToolBlocked, resolveVisionHelperModel, resolveVisionHandoffMax, resolveSliceNudge, resolveSliceBlock, resolveSliceBlockAt, resolveSliceBlockMax, anchorAppliesToProcess } from '../tools/ToolProfile.js';
52
+ import { applyToolProfile, resolveToolProfile, resolveToolAnchor, resolveFrameProfile, resolveLiftNudge, resolveLiftPlan, resolveEndTurnResolver, resolveHeadlessDropAskUser, resolveDeferredLoading, isNarrowProfile, isToolAllowedByProfile, webToolBlocked, resolveVisionHelperModel, resolveVisionHandoffMax, resolveSliceNudge, resolveSliceBlock, resolveSliceBlockAt, resolveSliceBlockMax, anchorAppliesToProcess } from '../tools/ToolProfile.js';
50
53
  import { sliceReadFile, decideSliceBlock, sliceReadStep } from './sliceBlock.js';
51
- import { TurnEvidence, evaluateEndTurnGates, buildMissingEndTurnReminder } from './endTurnGates.js';
54
+ import { normalizeEndTurnInput, TurnEvidence, evaluateEndTurnGates, buildMissingEndTurnReminder } from './endTurnGates.js';
52
55
  import { resolveCompactionResume, pickDropped, buildCompactionResumeReminder, isCompactionResumeMessage, resolveTaskText, resumeMemoryTargetTokens, coversAll, buildRollingSeedMessage, memoryIndexLine, upsertMemoryIndex, scaleEstimateToRequestView, approxCharsOf, anchoredRequestEstimate, resolveCompactionHandoffQA, resolveHandoffQAMaxQuestions, runHandoffQA } from './compactionResume.js';
53
56
  import { resolveCompactionTargetTokens } from './compactionTarget.js'; // R256
54
57
  import { resolveToolResultPruneGate, pruneGateTokens, CompactionThrashTracker } from './contextPressure.js'; // R258
55
58
  import { renderResumeMemoryPrompt, buildRebuildInstructions, resolveCheckpointBand, resolveRearmBand } from './compactionResumeTemplate.js';
56
59
  import { resolveTurnStatusMode, turnStatusActive, buildTurnStatusLine, appendStatusToNewestToolResult } from './turnStatus.js';
60
+ import { browseCarryEnabled, browseCarryBlock, browseCarryApplies, lastUrlIn, sawBotWall, sawScriptedBrowser, BROWSER_WALL_LINE, countDirectBrowserCalls, directBrowseLine, directBrowseNudgeThreshold, looksMultiStepBrowseTask, directConfirmEnabled, DIRECT_CONFIRM_TEXT, browseDirectAvailable, withoutBrowseDirect, WEB_TOOL_NAMES, SOURCES_REMINDER_LINE, looksLikeReverification, reverifyGuardEnabled, REVERIFY_TEXT, REVERIFY_MAX, reverifyKey, subAgentEndTurnEnv, looksLikeProbe, probeGuardEnabled, PROBE_TEXT } from './browserWallNudge.js';
61
+ import { resolveEndTurnIntegrityMode } from './integrityVerification.js';
62
+ import { isEssentialMcpTool } from '../mcp/essentialMcpTools.js';
57
63
  import { resolveCortexStateDir, cortexStatePath, resolveWorkspaceClean, cortexProjectDir } from '../utils/stateDir.js';
58
64
  import { collectWorkspaceDelta, detectCheckCommand, runCheck, resolveJudgeGroundingConfig, classifyCheckRun, isInvestigateCommandAllowed, specCheckParses, readFileSlice, formatEvidenceRound } from '../training/judgeEvidence.js';
59
65
  import { resolveMentorRoleConfig, describeMentorWire, describeMentorDelivery, mentorSurfaceTimeoutMs } from '../training/mentorRole.js';
60
66
  import { resolveOuterToolDeadlineMs, resolveOuterToolTimeoutFloorMs } from './outerToolTimeout.js';
61
- import { resolveSubAgentTimeoutMs } from './subAgentTimeout.js';
67
+ import { resolveSubAgentTimeoutMs, browseDelegateTimeoutMs } from './subAgentTimeout.js';
62
68
  import { resolveHerdrReporting, HerdrReporter, sanitizeHerdrAgentName } from './herdrReporter.js';
63
69
  import { runDelegateInHerdr, readSubAgentRuntimeLever, resolveSubAgentRuntime } from './HerdrSubAgentRunner.js';
64
70
  import { randomBytes } from 'crypto';
@@ -85,7 +91,7 @@ import { classifyTask } from '../training/TaskClassifier.js';
85
91
  import { closestToolMatches } from './toolNameMatcher.js';
86
92
  import { classifyApiError } from './apiErrorClassifier.js';
87
93
  import { pinStaticSystemPrompt } from './staticSystemPromptPin.js';
88
- import { applyAllowlistKeepingCore } from './toolAllowlist.js';
94
+ import { filterAgentTools, isBlockedBaseToolCall, isBrowseAgent, mayNotSpawn, agentDepth, maxAgentDepth } from './toolAllowlist.js';
89
95
  import { hasVisibleAssistantText, shouldForceSynthesis } from './assistantTextPresence.js';
90
96
  import { resolveTurnContractEnforce, checkTurnFormat, buildFormatRejectMessage, resolveActionPlanFields, ACTION_PLAN_TOOLS, planFieldsMissing, stripPlanFields, buildPlanFieldsRejectMessage } from './turnContractValidator.js'; // HB-TURN-CONTRACT-ENFORCE / R172
91
97
  import { computeToolBudgetSignal, isToolProgressStalled } from './toolBudgetSignal.js';
@@ -119,6 +125,28 @@ const OUTER_TIMEOUT_GRACE_MS = 30_000;
119
125
  * Phase 2.2: Timeline, compaction, checkpoints
120
126
  * Phase 2.3: Historical retrieval
121
127
  */
128
+ /**
129
+ * In an agent that may not spawn (the browse agent, or one at CORTEX_MAX_AGENT_DEPTH), a tool result that asks to start another
130
+ * sub-agent becomes an error and nothing is spawned — the one choke point every spawning tool (Browse, Task, WebSearch interactive,
131
+ * any future one) passes through (operator 2026-11-03).
132
+ */
133
+ function refuseSpawnInBrowseAgent(result) {
134
+ const r = result;
135
+ if (!mayNotSpawn() || !r?.metadata?.shouldSpawnSubAgent)
136
+ return result;
137
+ const msg = isBrowseAgent()
138
+ ? 'The browse agent cannot start another sub-agent. Use the nexus-browser__* tools directly.'
139
+ : `Sub-agents may nest at most ${maxAgentDepth()} level(s) deep and this agent is at depth ${agentDepth()}: do the work yourself with the tools you have.`;
140
+ return { ...result, success: false, llmContent: `Error: ${msg}`, returnDisplay: `Error: ${msg}`, error: msg,
141
+ metadata: { ...r.metadata, shouldSpawnSubAgent: false, refusedByAgentPolicy: true } };
142
+ }
143
+ /** Appends the user's original request to a browse delegate's task (once; trimmed) so its scope rules have the real scope to keep. */
144
+ export function withUserRequest(taskPrompt, userText) {
145
+ const u = (userText || '').trim();
146
+ if (!u || taskPrompt.includes(u.slice(0, 120)))
147
+ return taskPrompt;
148
+ return `${taskPrompt}\n\n---\nThe user's original request (your result must satisfy its scope — place, filters, dates — even when the task above does not repeat it):\n${u.slice(0, 1500)}`;
149
+ }
122
150
  export class CortexOrchestrator {
123
151
  adapterRegistry;
124
152
  gatewayTranslation;
@@ -220,6 +248,10 @@ export class CortexOrchestrator {
220
248
  // Until the first executed tool call, requests present only the anchor
221
249
  // profile's schemas; the lift happens at the first tool_result boundary.
222
250
  anchorLifted = false;
251
+ /** The model of the turn in progress (2026-11-02). The server passes the model PER REQUEST (options.modelId) and never sets
252
+ * currentModelId, so a sub-agent with model 'inherit' (the Browse delegate) fell back to the server default — Sonnet's Browse ran on
253
+ * deepseek-flash. Inherit resolves to this first. */
254
+ turnModelId = null;
223
255
  /** The active model card's anchorProfile (captured at request assembly) —
224
256
  * env CORTEX_TOOL_ANCHOR still overrides inside resolveToolAnchor. */
225
257
  cardAnchorProfile = null;
@@ -420,6 +452,9 @@ export class CortexOrchestrator {
420
452
  // choke point, but MCP tools (nexus-browser__*) join the list HERE, past it —
421
453
  // strip them at this seam too so the web surface is gone from every request.
422
454
  tools = tools.filter((t) => !webToolBlocked(t.name));
455
+ // Browse mode "direct" is for a person watching; headless sessions get the delegate only (browserWallNudge.ts browseDirectAvailable).
456
+ if (!browseDirectAvailable(this.approvalMode.autoApproveActions))
457
+ tools = tools.map((t) => (t.name === 'Browse' ? withoutBrowseDirect(t) : t));
423
458
  if (!resolveHeadlessDropAskUser(process.env, model?.headlessDropAskUser))
424
459
  return tools; // card > env > false
425
460
  if (!this.approvalMode.autoApproveActions)
@@ -1058,7 +1093,7 @@ export class CortexOrchestrator {
1058
1093
  const store = this.getDecisionStore();
1059
1094
  const sessionId = this.currentSessionId ?? 'unknown';
1060
1095
  try {
1061
- const etInput = toolUses.find((t) => t.id === et.tool_use_id || t.name === 'EndTurn')?.input ?? {};
1096
+ const etInput = normalizeEndTurnInput(toolUses.find((t) => t.id === et.tool_use_id || t.name === 'EndTurn')?.input); // stringified arrays parsed (endTurnGates.ts)
1062
1097
  // HB-JUDGE-GROUNDING (2026-09-10): the LATEST outputs (the checks it ran before finishing), not the
1063
1098
  // oldest 4K; plus the deliverable itself (workspace delta) and, when an evident check entry point
1064
1099
  // exists, a real check result — the evidence the judge prompt already promises.
@@ -1198,6 +1233,8 @@ export class CortexOrchestrator {
1198
1233
  const t0 = Date.now();
1199
1234
  const evidenceRounds = []; // R170
1200
1235
  let judgeEnvReport = ''; // the ENVIRONMENT REPORT the judge last saw (banked on the event — it is not in the session record)
1236
+ // Web-only turns (web results, no file writes) can be judged at lower effort: a review took 67–133 s at max (CORTEX_RESOLVER_WEB_EFFORT).
1237
+ const webJudgeEffort = webTurnEvidenced(ev) ? String(process.env.CORTEX_RESOLVER_WEB_EFFORT ?? '').trim() : '';
1201
1238
  const judgeOnce = (reasoning, investigate) => withTimeout(this.helperMiddleware.evaluateEndTurn({
1202
1239
  task,
1203
1240
  liftPlan: this.liftPlanText || undefined,
@@ -1212,6 +1249,8 @@ export class CortexOrchestrator {
1212
1249
  evidenceRounds: evidenceRounds.length ? evidenceRounds.slice() : undefined, // R170
1213
1250
  investigate,
1214
1251
  helperModelId: this.config.reactiveMentorship?.helperModelId,
1252
+ ...(webJudgeEffort ? { effort: webJudgeEffort } : {}),
1253
+ webAnswer: webTurnEvidenced(ev) && resolverWebRuleEnabled(), // CORTEX_RESOLVER_WEB_RULE (2026-11-04)
1215
1254
  }), reasoning === 'on' ? mentorSurfaceTimeoutMs('endturn-resolver', Math.max(timeoutMs, 180000)) : timeoutMs);
1216
1255
  // R170 HB-JUDGE-TOOL-LOOP: up to cfg.toolRounds calls; while rounds and the aggregate budget remain the judge may
1217
1256
  // INVESTIGATE (checks run via the existing runCheck surface behind a read-only denylist; file slices read in-workspace),
@@ -1551,7 +1590,7 @@ export class CortexOrchestrator {
1551
1590
  et.content = acceptGateHold.message;
1552
1591
  console.warn(`[EndTurnResolver] R260 ACCEPT-GATE HOLD — finish held (${this.acceptGateHolds}/${agc.max})`);
1553
1592
  }
1554
- else if ((action === 'accept-with-gap' || action === 'accept-low-confidence') &&
1593
+ else if ((action === 'accept-with-gap' || action === 'accept-low-confidence') && !webFinishStands(ev) &&
1555
1594
  shouldFinishConfirm({ action, remainingFrac: resolverRemainingFrac, confirmsUsed: this.finishConfirms, cfg })) {
1556
1595
  // R167 HB-FINISH-CONFIRM: the judge would accept with a gap and budget remains — hold ONCE with an informed
1557
1596
  // confirmation (zero model calls): budget left, the reviewer's gap plan, the junior's own open_items. The next
@@ -1578,7 +1617,10 @@ export class CortexOrchestrator {
1578
1617
  else if (action === 'accept' && verdict.meets) {
1579
1618
  // R168 HB-MEETS-CONFIRM: a MEETS stands only when a proving check passed. Otherwise, with budget left, hold ONCE
1580
1619
  // (shared counter with R167) showing what was (not) verified; the next EndTurn stands.
1581
- const evidencePassed = namedPassed > 0 || (!!checkResult && /→ PASSED/.test(checkResult));
1620
+ // A web turn's proof is the browser output the judge just read: no command can re-prove "the page shows X" (2026-11-03,
1621
+ // multi-site browser runs: the hold told a correct Parkers answer "named NO command", and the agent spent a whole extra Browse
1622
+ // delegate producing citable output). Web evidence + no file writes = evidenced (webTurnEvidenced; CORTEX_MEETS_CONFIRM_WEB).
1623
+ const evidencePassed = namedPassed > 0 || (!!checkResult && /→ PASSED/.test(checkResult)) || webTurnEvidenced(ev);
1582
1624
  if (cfg.meetsConfirm && !evidencePassed)
1583
1625
  this.judgeMeetsUnevidenced = true; // let the budget-continue / surrender nudges see this finish
1584
1626
  if (shouldFinishConfirm({ action, remainingFrac: resolverRemainingFrac, confirmsUsed: this.finishConfirms, cfg, meets: true, evidencePassed })) {
@@ -1695,6 +1737,18 @@ export class CortexOrchestrator {
1695
1737
  return tools;
1696
1738
  return applyToolProfile(tools, anchor);
1697
1739
  }
1740
+ /** Refusal text for a tool the guard blocks. While the turn-1 anchor is armed and the tool WILL be available once it lifts, say so —
1741
+ * 2026-11-02: Sonnet's first call was Browse; "not available under the active tool profile (bash-edit). Use the tools offered" read as
1742
+ * "there is no browser", and it did the whole task with curl, never calling Browse after the lift. */
1743
+ profileRefusal(name) {
1744
+ const profile = this.effectiveGuardProfile();
1745
+ const anchorArmed = !this.anchorLifted && anchorAppliesToProcess(process.env) && !!resolveToolAnchor(process.env, this.cardAnchorProfile);
1746
+ if (anchorArmed && isToolAllowedByProfile(name, (n) => toolFactory.getTool(n)?.discoveryTier, resolveToolProfile())) {
1747
+ return `Tool '${name}' is available, but not yet: this session opens with ${profile} only (the orient step). Make one Bash call to ` +
1748
+ `orient (e.g. pwd && ls), then call ${name} again — the full tool set, including ${name}, is offered from your next turn on.`;
1749
+ }
1750
+ return `Tool '${name}' is not available under the active tool profile (${profile}). Use the tools offered in this session.`;
1751
+ }
1698
1752
  /** The profile the dispatch guard enforces RIGHT NOW: the anchor while
1699
1753
  * armed (pre-lift calls to hidden tools are refused like any hidden
1700
1754
  * tool), the session profile after. */
@@ -1716,6 +1770,10 @@ export class CortexOrchestrator {
1716
1770
  checkpointManager;
1717
1771
  fileCheckpointManager; // Created in createSession() with sessionId
1718
1772
  mcpAutoInject = false;
1773
+ /** Browse mode:'direct' (2026-11-03): MCP servers connected mid-session for the MAIN model — pinned like CORTEX_MCP_AUTOCONNECT ones. */
1774
+ directPinnedServers = new Set();
1775
+ /** This turn's tool array, so tools connected mid-turn (Browse direct) are usable before the next user message. */
1776
+ currentTurnTools = null;
1719
1777
  // Phase 2.7: Cache metrics tracking
1720
1778
  cacheMetricsAccumulator = new CacheMetricsAccumulator();
1721
1779
  // Sub-Agent System: Manages spawning and tracking of sub-agents as child processes
@@ -1821,7 +1879,8 @@ export class CortexOrchestrator {
1821
1879
  const extras = [...this.getMcpManagementTools(), ...this.getContextManagementTools()];
1822
1880
  const seen = new Set();
1823
1881
  const catalogVision = this.getModel(this.currentModelId ?? '')?.vision === true;
1824
- return [...toolFactory.getAllTools(), ...extras]
1882
+ // The catalog obeys the agent's allowlist too: a restricted delegate must not DISCOVER tools it may not use (2026-11-03).
1883
+ return [...this.applyBaseToolAllowlist(toolFactory.getAllTools()), ...extras]
1825
1884
  .filter((t) => endTurnGateOn || t.name !== 'EndTurn')
1826
1885
  .filter((t) => catalogVision || !!resolveVisionHelperModel(process.env) || t.name !== 'ReadImage')
1827
1886
  .filter((t) => (seen.has(t.name) ? false : (seen.add(t.name), true)))
@@ -1972,6 +2031,7 @@ export class CortexOrchestrator {
1972
2031
  }
1973
2032
  modelId = recommended;
1974
2033
  }
2034
+ this.turnModelId = modelId ?? null; // the model THIS turn runs (a sub-agent 'inherit' follows it — see turnModelId)
1975
2035
  const model = this.getModel(modelId);
1976
2036
  // Phase 1 & Phase 2: Get tools (used for both injection context and API request)
1977
2037
  const endTurnGateOn = (process.env.CORTEX_ENDTURN_GATE) === 'true';
@@ -2000,6 +2060,7 @@ export class CortexOrchestrator {
2000
2060
  const allTools = this.stripHeadlessOnlyTools(options.tools !== undefined
2001
2061
  ? [...factoryTools, ...mcpTools, ...mcpManagementTools, ...contextManagementTools, ...options.tools]
2002
2062
  : [...factoryTools, ...mcpTools, ...mcpManagementTools, ...contextManagementTools], model);
2063
+ this.currentTurnTools = allTools; // Browse direct appends the browser tools here mid-turn
2003
2064
  // Phase 2.3: Inject server-side tools when enabled and model supports them
2004
2065
  // R20: provider-aware — XAI gets web/x/code; OpenAI gets web/code/file/image
2005
2066
  // (Responses API hosted tools); other providers stay client-only.
@@ -2594,7 +2655,9 @@ export class CortexOrchestrator {
2594
2655
  // loop detection, the consecutive-error breaker, and MAX_TOOL_ITERATIONS.
2595
2656
  const TOOL_BUDGET_HARD = TOOL_BUDGET_SOFT > 0 ? TOOL_BUDGET_SOFT * 2 : Infinity; // force-synthesis cap
2596
2657
  // #2 (2026-09-04): per-turn wall-clock budget (opt-in — 0 disables, no behaviour change when unset).
2597
- const TURN_DEADLINE_MS = loopDefaults.turnDeadlineMs;
2658
+ const TURN_DEADLINE_MS = Number.isFinite(options.turnDeadlineMs) && options.turnDeadlineMs > 0 ? options.turnDeadlineMs : loopDefaults.turnDeadlineMs;
2659
+ if (this.config.debug && TURN_DEADLINE_MS > 0)
2660
+ console.log(`[Orchestrator] turn deadline ${TURN_DEADLINE_MS}ms (${Number(options.turnDeadlineMs) > 0 ? 'request' : 'config/env'})`);
2598
2661
  const loopStartMs = Date.now();
2599
2662
  this.turnLoopStartMs = loopStartMs;
2600
2663
  this.turnDeadlineMsActive = TURN_DEADLINE_MS; // R133: sub-agent timeouts read the residual
@@ -2743,7 +2806,7 @@ export class CortexOrchestrator {
2743
2806
  ? classifyEmptyResponse(currentAssistantCanonicalMessage.content, lastStopReason, loopHasBudget)
2744
2807
  : null;
2745
2808
  const isEmptyContinueKind = EMPTY_TURN_CONTINUE && !!emptyClass
2746
- && (emptyClass.kind === 'truncated' || emptyClass.kind === 'reasoning_only_active');
2809
+ && (emptyClass.kind === 'truncated' || emptyClass.kind === 'reasoning_only_active' || emptyClass.kind === 'empty_active');
2747
2810
  const canRetryEmpty = isEmptyContinueKind
2748
2811
  ? emptyContinueCount < EMPTY_CONTINUE_MAX
2749
2812
  : !emptyResponseRetryUsed;
@@ -3400,7 +3463,8 @@ export class CortexOrchestrator {
3400
3463
  const processedToolUseIds = new Set();
3401
3464
  try {
3402
3465
  this.herdrReport('working', `tool:${toolUseBlocks[0]?.name ?? 'batch'}`); // R145
3403
- const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState);
3466
+ // Secrets vault Phase 2: stored values never reach the model or the screen («secret:NAME»); identity without a vault.
3467
+ const toolResults = maskToolResults(await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState));
3404
3468
  this.testPostExecThrow(); // test-only fault injection for the re-execution guard (inert in prod)
3405
3469
  clearTimeout(timeoutId);
3406
3470
  notePollResults(exactRepeatTracker, toolUseBlocks, toolResults); // R137: poll repeats count only on identical STALLED output
@@ -3725,6 +3789,9 @@ export class CortexOrchestrator {
3725
3789
  }
3726
3790
  // HB-COMPUTE-NUDGE (CORTEX_COMPUTE_NUDGE, dark): after a heavily-reasoned step, 'compute/test instead' at the same tail.
3727
3791
  this.injectComputeNudge(toolCallIteration, false);
3792
+ this.injectBrowserWallNudge(toolResults, toolCallIteration, false, toolUseBlocks);
3793
+ this.injectDirectBrowseNudge(toolCallIteration, false, toolUseBlocks);
3794
+ this.injectSourcesReminder(toolResults);
3728
3795
  // HB-TURN-STATUS (CORTEX_TURN_STATUS, dark): clock + context + bg shells at the TAIL of this round's newest tool_result.
3729
3796
  this.injectTurnStatus(loopStartMs, TURN_DEADLINE_MS, effectiveModel, toolCallIteration);
3730
3797
  // Ladder break (teach-then-break): the break instruction is already in
@@ -4388,6 +4455,7 @@ export class CortexOrchestrator {
4388
4455
  if (!modelId) {
4389
4456
  throw new Error('No model specified and no current model set');
4390
4457
  }
4458
+ this.turnModelId = modelId;
4391
4459
  // Central lookup — includes the back-compat alias fallback (deprecated
4392
4460
  // names like deepseek-chat resolve to their successor cards).
4393
4461
  const model = this.getModel(modelId);
@@ -4459,6 +4527,7 @@ export class CortexOrchestrator {
4459
4527
  const allTools = this.stripHeadlessOnlyTools(options.tools !== undefined
4460
4528
  ? [...factoryTools, ...mcpTools, ...mcpManagementTools, ...contextManagementTools, ...options.tools]
4461
4529
  : [...factoryTools, ...mcpTools, ...mcpManagementTools, ...contextManagementTools], model);
4530
+ this.currentTurnTools = allTools; // Browse direct appends the browser tools here mid-turn
4462
4531
  // Phase 2.3: Inject server-side tools when enabled and model supports them
4463
4532
  // R20: provider-aware (streaming path mirror of non-streaming injection above).
4464
4533
  if (isServerSideToolsEnabled() && modelSupportsServerSideTools(model)) {
@@ -4952,7 +5021,9 @@ export class CortexOrchestrator {
4952
5021
  let pendingGateRequest = false; // Stage-1: a reminder was appended; run one continuation with NO tool batch
4953
5022
  const streamEndTurnGateEnabled = process.env.CORTEX_ENDTURN_GATE === 'true';
4954
5023
  // #2 (2026-09-04): per-turn wall-clock budget (opt-in — 0 disables; streaming parity with non-stream).
4955
- const TURN_DEADLINE_MS = loopDefaults.turnDeadlineMs;
5024
+ const TURN_DEADLINE_MS = Number.isFinite(options.turnDeadlineMs) && options.turnDeadlineMs > 0 ? options.turnDeadlineMs : loopDefaults.turnDeadlineMs;
5025
+ if (this.config.debug && TURN_DEADLINE_MS > 0)
5026
+ console.log(`[Orchestrator] turn deadline ${TURN_DEADLINE_MS}ms (${Number(options.turnDeadlineMs) > 0 ? 'request' : 'config/env'})`);
4956
5027
  const loopStartMs = Date.now();
4957
5028
  this.turnLoopStartMs = loopStartMs;
4958
5029
  this.turnDeadlineMsActive = TURN_DEADLINE_MS; // R133: sub-agent timeouts read the residual
@@ -5004,7 +5075,7 @@ export class CortexOrchestrator {
5004
5075
  ? classifyEmptyResponse(currentAssistantCanonicalMessage.content, lastStopReason, loopHasBudget)
5005
5076
  : null;
5006
5077
  const isEmptyContinueKind = EMPTY_TURN_CONTINUE && !!emptyClass
5007
- && (emptyClass.kind === 'truncated' || emptyClass.kind === 'reasoning_only_active');
5078
+ && (emptyClass.kind === 'truncated' || emptyClass.kind === 'reasoning_only_active' || emptyClass.kind === 'empty_active');
5008
5079
  const canRetryEmpty = isEmptyContinueKind
5009
5080
  ? emptyContinueCount < EMPTY_CONTINUE_MAX
5010
5081
  : !emptyResponseRetryUsed;
@@ -5314,7 +5385,8 @@ export class CortexOrchestrator {
5314
5385
  try {
5315
5386
  // Execute tools (reuse existing method)
5316
5387
  this.herdrReport('working', `tool:${toolUseBlocks[0]?.name ?? 'batch'}`); // R145
5317
- const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState);
5388
+ // Secrets vault Phase 2: stored values never reach the model or the screen («secret:NAME»); identity without a vault.
5389
+ const toolResults = maskToolResults(await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState));
5318
5390
  this.testPostExecThrow(); // test-only fault injection for the re-execution guard (inert in prod)
5319
5391
  clearTimeout(timeoutId);
5320
5392
  notePollResults(streamRepeatTracker, toolUseBlocks, toolResults); // R137 (streaming parity)
@@ -5583,6 +5655,9 @@ export class CortexOrchestrator {
5583
5655
  }
5584
5656
  // HB-COMPUTE-NUDGE (streaming parity).
5585
5657
  this.injectComputeNudge(toolCallIteration, true);
5658
+ this.injectBrowserWallNudge(toolResults, toolCallIteration, true, toolUseBlocks);
5659
+ this.injectDirectBrowseNudge(toolCallIteration, true, toolUseBlocks);
5660
+ this.injectSourcesReminder(toolResults);
5586
5661
  // HB-TURN-STATUS (streaming parity): same tail, same carrier.
5587
5662
  this.injectTurnStatus(loopStartMs, TURN_DEADLINE_MS, effectiveModel, toolCallIteration);
5588
5663
  // Ladder break (streaming parity): instruction already injected above;
@@ -6353,8 +6428,52 @@ export class CortexOrchestrator {
6353
6428
  const elapsedMs = this.turnLoopStartMs > 0 ? Date.now() - this.turnLoopStartMs : 0;
6354
6429
  return this.getFrameRunner().step({ action: input, task: this.lastRealUserText(), elapsedMs, deadlineMs: this.turnDeadlineMsActive, cwd: this.config.workingDirectory || process.cwd(), sessionId: this.currentSessionId ?? 'session', signal, predictorModel: 'generator' });
6355
6430
  }
6431
+ /**
6432
+ * Browse mode:"direct": connect the nexus-browser MCP server (local or hosted per NEXUS_BROWSER_MODE — the registry entry decides)
6433
+ * into this session, pin its tools (essential tier) and append them to the current turn so the model can call them immediately.
6434
+ */
6435
+ async enableBrowserDirect(url) {
6436
+ if (!this.mcpManager)
6437
+ return { text: 'Direct browsing needs MCP, which is disabled in this session. Use Browse without mode (a browse agent does the task).', isError: true };
6438
+ if (isNarrowProfile() || resolveFrameConfig(process.env).frame === 'terminus')
6439
+ return { text: 'Direct browsing is not available in this tool profile. Use Browse without mode.', isError: true };
6440
+ const name = 'nexus-browser';
6441
+ try {
6442
+ const connected = this.mcpManager.getServerInfo().some((s) => s.name === name && s.status === 'connected');
6443
+ if (!connected) {
6444
+ const def = this.getMcpServerRegistry()?.getServer(name);
6445
+ if (!def)
6446
+ return { text: 'nexus-browser is not in the MCP registry.', isError: true };
6447
+ this.mcpManager.addServerConfig(name, this.registryDefinitionToConfig(def));
6448
+ await this.mcpManager.connectToServer(name);
6449
+ await this.mcpManager.discoverAll();
6450
+ }
6451
+ this.directPinnedServers.add(name);
6452
+ this.mcpAutoInject = true;
6453
+ const tools = this.getMcpToolsAsCanonical().filter((t) => t.metadata?.serverName === name);
6454
+ if (!tools.length)
6455
+ return { text: 'Connected to nexus-browser but it reported no tools.', isError: true };
6456
+ if (this.currentTurnTools) {
6457
+ const have = new Set(this.currentTurnTools.map((t) => t.name));
6458
+ for (const t of tools)
6459
+ if (!have.has(t.name))
6460
+ this.currentTurnTools.push(t);
6461
+ }
6462
+ const backend = resolveBrowserBackend().using;
6463
+ const first = tools.find((t) => /__browse$/.test(t.name))?.name ?? `${name}__browse`;
6464
+ return { text: `Browser connected for you to drive directly (${backend}). ${tools.length} tools are now available, e.g. ${tools.slice(0, 8).map((t) => t.name).join(', ')}, … ` +
6465
+ `Next: call ${first}({ url${url ? `: ${JSON.stringify(url)}` : ''} }), then scan / get_content / click / type, and close_session when done. ` +
6466
+ `Before you report a result, check it fits the task's scope and scale: if the task names a place, store or other filter, confirm on the page that ` +
6467
+ `the RESULTS are limited to it (a distance / "this store" filter, the result cards' locations) — a location in the header is not proof, and a ` +
6468
+ `total equal to the site-wide filter counts is unfiltered.`, isError: false };
6469
+ }
6470
+ catch (e) {
6471
+ return { text: `Could not connect the browser: ${String(e?.message ?? e).slice(0, 300)}. Use Browse without mode (a browse agent) instead.`, isError: true };
6472
+ }
6473
+ }
6356
6474
  applyBaseToolAllowlist(tools) {
6357
- return applyAllowlistKeepingCore(tools, this.config.allowedBaseTools); // R266: SearchTools + EndTurn always survive
6475
+ // R266: SearchTools + EndTurn always survive; the browse agent never gets a sub-agent-spawning tool (toolAllowlist.ts).
6476
+ return filterAgentTools(tools, this.config.allowedBaseTools);
6358
6477
  }
6359
6478
  /**
6360
6479
  * Get adapter registry (for advanced use cases)
@@ -7623,6 +7742,8 @@ export class CortexOrchestrator {
7623
7742
  config.args = def.defaultArgs;
7624
7743
  if (def.defaultEnv)
7625
7744
  config.env = def.defaultEnv;
7745
+ if (def.connectTimeoutMs)
7746
+ config.timeout = def.connectTimeoutMs;
7626
7747
  return config;
7627
7748
  }
7628
7749
  // ============================================
@@ -7649,6 +7770,69 @@ export class CortexOrchestrator {
7649
7770
  }).catch(() => { });
7650
7771
  }
7651
7772
  async handleToolCalls(toolUseBlocks, signal, structuredOutputState) {
7773
+ // Agent tool policy at EXECUTION: a base tool outside this agent's allowlist (or a spawning tool inside the browse agent) is refused even if the
7774
+ // model calls it unoffered — the offered list and the SearchTools catalog already exclude it (filterAgentTools). 2026-11-03.
7775
+ {
7776
+ const baseNames = toolFactory.getAllTools().map((t) => t.name);
7777
+ const refused = toolUseBlocks.filter((t) => isBlockedBaseToolCall(t.name, baseNames, this.config.allowedBaseTools));
7778
+ if (refused.length) {
7779
+ const rest = toolUseBlocks.filter((t) => !refused.includes(t));
7780
+ const restResults = rest.length ? await this.handleToolCalls(rest, signal, structuredOutputState) : [];
7781
+ const msg = (n) => `${n} is not available to this agent${isBrowseAgent() ? ' (the browse agent cannot start another sub-agent: use the nexus-browser__* tools directly)' : mayNotSpawn() ? ` (sub-agents nest at most ${maxAgentDepth()} level(s) deep)` : ''}. Use only the tools you were given.`;
7782
+ return [...refused.map((t) => ({ tool_use_id: t.id, tool_name: t.name, content: msg(t.name), is_error: true, metadata: { refusedByAgentPolicy: true } })), ...restResults];
7783
+ }
7784
+ }
7785
+ // Re-verification of a completed browse report → one explanation instead of a delegate (browserWallNudge.ts REVERIFY_TEXT).
7786
+ // Probe-first: the turn's first Browse call only asks whether the site loads → one explanation (browserWallNudge.ts PROBE_TEXT).
7787
+ if (!this.browseCompletedThisTurn && !this.probeExplained && probeGuardEnabled()) {
7788
+ const probes = toolUseBlocks.filter((t) => t.name === 'Browse' && looksLikeProbe(t.input?.task));
7789
+ if (probes.length) {
7790
+ this.probeExplained = true;
7791
+ const store = this.getDecisionStore();
7792
+ if (store)
7793
+ void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'steering_injected', detail: { kinds: ['browse_probe'], task: String(probes[0].input?.task ?? '').slice(0, 200) } }).catch(() => { });
7794
+ const rest = toolUseBlocks.filter((t) => !probes.includes(t));
7795
+ const restResults = rest.length ? await this.handleToolCalls(rest, signal, structuredOutputState) : [];
7796
+ return [...probes.map((t) => ({ tool_use_id: t.id, tool_name: t.name, content: PROBE_TEXT, is_error: false, metadata: { browseProbe: true } })), ...restResults];
7797
+ }
7798
+ }
7799
+ // Each NEW verification-shaped task is explained once (2026-11-03 val-W BBB: after the one-shot guard the agent re-worded four more
7800
+ // passes); re-sending the SAME task runs it; at most REVERIFY_MAX explanations per turn, so the agent can never be stuck.
7801
+ if (this.browseCompletedThisTurn && this.reverifyExplained.size < REVERIFY_MAX && reverifyGuardEnabled()) {
7802
+ const rev = toolUseBlocks.filter((t) => t.name === 'Browse' && looksLikeReverification(t.input?.task) && !this.reverifyExplained.has(reverifyKey(t.input?.task)));
7803
+ if (rev.length) {
7804
+ for (const t of rev)
7805
+ this.reverifyExplained.add(reverifyKey(t.input?.task));
7806
+ const store = this.getDecisionStore();
7807
+ if (store)
7808
+ void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'steering_injected', detail: { kinds: ['browse_reverify'], task: String(rev[0].input?.task ?? '').slice(0, 200) } }).catch(() => { });
7809
+ const rest = toolUseBlocks.filter((t) => !rev.includes(t));
7810
+ const restResults = rest.length ? await this.handleToolCalls(rest, signal, structuredOutputState) : [];
7811
+ return [...rev.map((t) => ({ tool_use_id: t.id, tool_name: t.name, content: REVERIFY_TEXT, is_error: false, metadata: { browseReverify: true } })), ...restResults];
7812
+ }
7813
+ }
7814
+ // Browse mode:"direct" — the MAIN model drives the browser itself (the SPA's browse anchor-lift pattern): connect nexus-browser into
7815
+ // THIS session and put its tools in the current turn, instead of dispatching a browse sub-agent. 2026-11-03.
7816
+ {
7817
+ // Headless: direct is withheld — a stray direct call is not intercepted here and runs as a delegate (executor site below).
7818
+ const direct = browseDirectAvailable(this.approvalMode.autoApproveActions)
7819
+ ? toolUseBlocks.filter((t) => t.name === 'Browse' && t.input?.mode === 'direct') : [];
7820
+ if (direct.length) {
7821
+ const rest = toolUseBlocks.filter((t) => !direct.includes(t));
7822
+ const alreadyConnected = this.directPinnedServers.has('nexus-browser');
7823
+ if (!alreadyConnected && !this.directConfirmAsked && directConfirmEnabled() && looksMultiStepBrowseTask(direct[0].input?.task)) {
7824
+ this.directConfirmAsked = true;
7825
+ const store = this.getDecisionStore();
7826
+ if (store)
7827
+ void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'steering_injected', detail: { kinds: ['direct_browse_confirm'] } }).catch(() => { });
7828
+ const restResults = rest.length ? await this.handleToolCalls(rest, signal, structuredOutputState) : [];
7829
+ return [...direct.map((t) => ({ tool_use_id: t.id, tool_name: t.name, content: DIRECT_CONFIRM_TEXT, is_error: false, metadata: { browseDirect: 'confirm' } })), ...restResults];
7830
+ }
7831
+ const content = await this.enableBrowserDirect(direct[0].input?.url);
7832
+ const restResults = rest.length ? await this.handleToolCalls(rest, signal, structuredOutputState) : [];
7833
+ return [...direct.map((t, i) => ({ tool_use_id: t.id, tool_name: t.name, content: i === 0 ? content.text : 'Already handled by the first direct Browse call.', is_error: content.isError, metadata: { browseDirect: true } })), ...restResults];
7834
+ }
7835
+ }
7652
7836
  // HB-TURN-CONTRACT-ENFORCE: a batch the loop rejected on format carries the re-prompt back as error results; nothing runs.
7653
7837
  if (this.pendingFormatReject) {
7654
7838
  const rej = this.pendingFormatReject;
@@ -7746,7 +7930,7 @@ export class CortexOrchestrator {
7746
7930
  models: taskTools.map(t => String(t.input?.model ?? 'inherit')),
7747
7931
  turn: this.turnNumber,
7748
7932
  // R133 HB-SUBAGENT-TIMEOUT: the resolved per-dispatch timeout + the parent's residual budget at dispatch
7749
- timeoutMs: taskTools.map(t => this.resolveSubAgentTimeoutFor(t.input).timeoutMs),
7933
+ timeoutMs: taskTools.map(t => this.resolveSubAgentTimeoutFor(t.input, t.name).timeoutMs),
7750
7934
  remainingMs: this.remainingTurnMs(),
7751
7935
  },
7752
7936
  }).catch(() => { });
@@ -7883,7 +8067,7 @@ export class CortexOrchestrator {
7883
8067
  results.push({
7884
8068
  tool_use_id: toolUse.id,
7885
8069
  tool_name: toolUse.name,
7886
- content: `Tool '${toolUse.name}' is not available under the active tool profile (${this.effectiveGuardProfile()}). Use the tools offered in this session.`,
8070
+ content: this.profileRefusal(toolUse.name),
7887
8071
  is_error: true,
7888
8072
  });
7889
8073
  continue;
@@ -8064,7 +8248,8 @@ export class CortexOrchestrator {
8064
8248
  }
8065
8249
  }
8066
8250
  // Execute regular executor tool
8067
- result = await this.executorRegistry.execute(toolUse.name, toolUse.input, signal);
8251
+ result = await this.executorRegistry.execute(toolUse.name, this.executorInputFor(toolUse), signal);
8252
+ result = refuseSpawnInBrowseAgent(result); // the browse agent may not start another sub-agent (toolAllowlist.ts)
8068
8253
  // Sub-Agent Spawning (Task, Browse, etc. — any tool emitting
8069
8254
  // shouldSpawnSubAgent). The dispatch gate is the metadata
8070
8255
  // contract, not the tool name, so new entry tools can reuse it.
@@ -8080,7 +8265,17 @@ export class CortexOrchestrator {
8080
8265
  const processManager = this.getOrCreateSubAgentProcessManager();
8081
8266
  // Extract agent definition from metadata
8082
8267
  const agentDef = result.metadata.agentDefinition;
8083
- const taskPrompt = result.metadata.taskPrompt;
8268
+ let taskPrompt = result.metadata.taskPrompt;
8269
+ // CORTEX_BROWSE_CARRY (2026-11-04): a follow-up Browse after a COMPLETED report starts where that pass ended (browserWallNudge.ts).
8270
+ if (agentDef.name === 'browse-agent') {
8271
+ if (this.lastBrowseReport && browseCarryEnabled() && browseCarryApplies(this.lastBrowseReport, taskPrompt)) {
8272
+ taskPrompt = browseCarryBlock(this.lastBrowseReport, /^Start at: /.test(taskPrompt)) + taskPrompt;
8273
+ const store = this.getDecisionStore();
8274
+ if (store)
8275
+ void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'steering_injected', detail: { kinds: ['browse_carry'], url: lastUrlIn(this.lastBrowseReport.report) ?? null } }).catch(() => { });
8276
+ }
8277
+ this.lastBrowseTask = String(result.metadata.taskPrompt);
8278
+ }
8084
8279
  // Per-fork env overrides let Browse enable MCP_AUTO_INJECT
8085
8280
  // for its subagent without affecting the parent process.
8086
8281
  const envOverrides = result.metadata.envOverrides;
@@ -8099,8 +8294,8 @@ export class CortexOrchestrator {
8099
8294
  }
8100
8295
  }
8101
8296
  else {
8102
- // No model or 'inherit' - use parent's current model
8103
- resolvedModelId = this.currentModelId || this.config.defaultModelId;
8297
+ // No model or 'inherit' - use the model of the turn in progress (the request's), else the parent's current model
8298
+ resolvedModelId = this.turnModelId || this.currentModelId || this.config.defaultModelId;
8104
8299
  }
8105
8300
  if (this.config.debug) {
8106
8301
  console.log(`[Orchestrator SubAgent] Model resolved: ${agentDef.model} -> ${resolvedModelId}`);
@@ -8114,7 +8309,7 @@ export class CortexOrchestrator {
8114
8309
  // R133 HB-SUBAGENT-TIMEOUT: derive the child's limit from the parent's remaining turn budget (or the
8115
8310
  // Task `timeout_ms`), and hand the child the SAME limit as its own turn deadline — it otherwise inherits
8116
8311
  // the parent's full CORTEX_TURN_DEADLINE_MS via process.env and never converges before the kill.
8117
- const { timeoutMs, source: timeoutSource } = this.resolveSubAgentTimeoutFor(toolUse.input);
8312
+ const { timeoutMs, source: timeoutSource } = this.resolveSubAgentTimeoutFor(toolUse.input, toolUse.name);
8118
8313
  if (this.config.debug)
8119
8314
  console.log(`[SubAgent] timeout ${timeoutMs}ms (${timeoutSource})`);
8120
8315
  // R147 HB-HERDR-DELEGATES: runtime = herdr pane or forked IPC child (lever / Task `runtime`).
@@ -8245,7 +8440,7 @@ export class CortexOrchestrator {
8245
8440
  * sides are individually env-gated and never block tool execution.
8246
8441
  */
8247
8442
  async executeSingleToolCallWithTraining(toolUse, signal) {
8248
- const result = await this.executeSingleToolCall(toolUse, signal);
8443
+ const result = await this.executeSingleToolCall(toolUse, signal); // masked at the top of processToolTraining
8249
8444
  return await this.processToolTraining(toolUse, result);
8250
8445
  }
8251
8446
  async executeSingleToolCall(toolUse, signal) {
@@ -8314,7 +8509,7 @@ export class CortexOrchestrator {
8314
8509
  return {
8315
8510
  tool_use_id: toolUse.id,
8316
8511
  tool_name: toolUse.name,
8317
- content: `Tool '${toolUse.name}' is not available under the active tool profile (${this.effectiveGuardProfile()}). Use the tools offered in this session.`,
8512
+ content: this.profileRefusal(toolUse.name),
8318
8513
  is_error: true,
8319
8514
  };
8320
8515
  }
@@ -8339,7 +8534,7 @@ export class CortexOrchestrator {
8339
8534
  }
8340
8535
  }
8341
8536
  // Execute the tool (using executor registry)
8342
- const result = await this.executorRegistry.execute(toolUse.name, toolUse.input, signal);
8537
+ const result = refuseSpawnInBrowseAgent(await this.executorRegistry.execute(toolUse.name, this.executorInputFor(toolUse), signal)); // the browse agent may not start another sub-agent
8343
8538
  // Handle any sub-agent-spawning tool (Task, Browse, etc.). The dispatch
8344
8539
  // gate is the metadata contract, not the tool name — that way Browse and
8345
8540
  // future entry tools can reuse the same path without duplication.
@@ -8360,7 +8555,7 @@ export class CortexOrchestrator {
8360
8555
  resolvedModelId = resolved?.modelId || this.currentModelId || this.config.defaultModelId;
8361
8556
  }
8362
8557
  else {
8363
- resolvedModelId = this.currentModelId || this.config.defaultModelId;
8558
+ resolvedModelId = this.turnModelId || this.currentModelId || this.config.defaultModelId;
8364
8559
  }
8365
8560
  if (this.config.debug) {
8366
8561
  console.log(`[Orchestrator] Spawning sub-agent: ${agentDef.name} with model ${resolvedModelId}${envOverrides ? ` (env: ${Object.keys(envOverrides).join(',')})` : ''}`);
@@ -8370,7 +8565,7 @@ export class CortexOrchestrator {
8370
8565
  const progressListeners = this.setupSubAgentProgressListeners(eventEmitter, agentDef.name);
8371
8566
  try {
8372
8567
  // R133 HB-SUBAGENT-TIMEOUT: see the handleToolCalls site — residual-derived limit, mirrored into the child's turn deadline.
8373
- const { timeoutMs, source: timeoutSource } = this.resolveSubAgentTimeoutFor(toolUse.input);
8568
+ const { timeoutMs, source: timeoutSource } = this.resolveSubAgentTimeoutFor(toolUse.input, toolUse.name);
8374
8569
  if (this.config.debug)
8375
8570
  console.log(`[SubAgent] timeout ${timeoutMs}ms (${timeoutSource})`);
8376
8571
  // R147 HB-HERDR-DELEGATES: runtime = herdr pane or forked IPC child (lever / Task `runtime`).
@@ -8520,12 +8715,15 @@ export class CortexOrchestrator {
8520
8715
  // R266: a server named in CORTEX_MCP_AUTOCONNECT was asked for explicitly (the Browse delegate's
8521
8716
  // envOverrides) — its tools are ESSENTIAL so the deferred filter cannot strip them; other MCP tools
8522
8717
  // keep no tier (deferred as before).
8523
- const pinnedServers = new Set((process.env.CORTEX_MCP_AUTOCONNECT ?? '').split(',').map((x) => x.trim()).filter(Boolean));
8718
+ const pinnedServers = new Set([...(process.env.CORTEX_MCP_AUTOCONNECT ?? '').split(',').map((x) => x.trim()).filter(Boolean), ...this.directPinnedServers]);
8524
8719
  return mcpTools.map(tool => ({
8525
8720
  name: prefixMcpToolName(tool.serverName, tool.name),
8526
8721
  description: tool.description || `MCP tool from ${tool.serverName}`,
8527
- schema: tool.inputSchema || { type: 'object', properties: {} },
8528
- ...(pinnedServers.has(tool.serverName) ? { discoveryTier: 'essential' } : {}),
8722
+ schema: normalizeMcpSchema(tool.inputSchema || { type: 'object', properties: {} }), // tuple items → providers reject (mcpSchemaCompat.ts)
8723
+ // Only a server's ESSENTIAL set is pinned (2026-11-02): pinning all 50 nexus-browser tools sent ~49k chars of schemas on EVERY
8724
+ // delegate request; the rest stay deferred and discoverable via SearchTools (the canonical progressive-discovery form the MCP
8725
+ // server itself uses). Servers without a curated set keep the old all-pinned behaviour.
8726
+ ...(pinnedServers.has(tool.serverName) && isEssentialMcpTool(tool.serverName, tool.name) ? { discoveryTier: 'essential' } : {}),
8529
8727
  metadata: {
8530
8728
  source: 'mcp',
8531
8729
  serverName: tool.serverName,
@@ -8877,12 +9075,16 @@ export class CortexOrchestrator {
8877
9075
  return Math.max(0, this.turnDeadlineMsActive - (Date.now() - this.turnLoopStartMs));
8878
9076
  }
8879
9077
  /** R133: the sub-agent timeout for one Task/Browse tool input (`timeout_ms` request → residual deadline → env → 5 min). */
8880
- resolveSubAgentTimeoutFor(input) {
8881
- const requested = Number(input?.timeout_ms);
9078
+ resolveSubAgentTimeoutFor(input, toolName) {
9079
+ let requested = Number(input?.timeout_ms);
9080
+ // Browse delegates get their own default (2026-11-03): 4 of 8 cars.com/CarMax delegates hit the generic 5-min limit mid-task
9081
+ // (~1.1M input tokens each, nothing usable returned); the ones that finished took 3–4 min. A parent deadline still caps it.
9082
+ if (!(Number.isFinite(requested) && requested > 0) && toolName === 'Browse')
9083
+ requested = browseDelegateTimeoutMs(process.env);
8882
9084
  return resolveSubAgentTimeoutMs({ requestedMs: Number.isFinite(requested) && requested > 0 ? requested : undefined, remainingMs: this.remainingTurnMs() });
8883
9085
  }
8884
9086
  /** R133: per-block lookup for resolveOuterToolDeadlineMs — a sub-agent-spawning block contributes its resolved timeout. */
8885
- subAgentBlockTimeoutMs = (block) => block?.name === 'Task' || block?.name === 'Browse' ? this.resolveSubAgentTimeoutFor(block.input).timeoutMs : undefined;
9087
+ subAgentBlockTimeoutMs = (block) => block?.name === 'Task' || block?.name === 'Browse' ? this.resolveSubAgentTimeoutFor(block.input, block.name).timeoutMs : undefined;
8886
9088
  /**
8887
9089
  * R147 HB-HERDR-DELEGATES: herdr delegates are possible only inside a herdr pane (HERDR_ENV=1 +
8888
9090
  * HERDR_PANE_ID + binary) with the herdr terminal backend resolving (socket reachable). Probed once.
@@ -8914,9 +9116,17 @@ export class CortexOrchestrator {
8914
9116
  }
8915
9117
  /** R147: run the sub-agent in a herdr pane (HerdrSubAgentRunner) or as the forked IPC child (unchanged path). */
8916
9118
  async dispatchSubAgent(processManager, input, agentDef, taskPrompt, options) {
9119
+ // EndTurn review inside sub-agents (2026-11-03, multi-site browser pass): the resolver judge took 90 of 296 task-minutes INSIDE browse
9120
+ // delegates (67–133 s per review, holds, 5 timeouts at the 10-min cap) while the parent reviews the delegate's result anyway.
9121
+ options = { ...options, envOverrides: { ...subAgentEndTurnEnv(), ...options.envOverrides } };
8917
9122
  const { runtime, reason } = await this.resolveSubAgentRuntimeFor(input);
8918
9123
  if (this.config.debug)
8919
9124
  console.log(`[SubAgent] runtime ${runtime} (${reason})`);
9125
+ // The browse agent always sees the USER's request, not only the parent's paraphrase (2026-11-03, Sonnet CarMax): a follow-up Browse
9126
+ // call ("open the URL in a fresh browser and report") dropped "near Kentwood", so that delegate kept no scope and the final answer
9127
+ // hedged between 20 (25 mi) and the nationwide 14,332. The parent's task still says WHAT to do; the request says what must hold.
9128
+ if (agentDef.name === 'browse-agent')
9129
+ taskPrompt = withUserRequest(taskPrompt, this.lastRealUserText());
8920
9130
  // R266b: the delegate inherits the parent's approval mode — auto-approving parents (headless, one-shot, TUI yolo)
8921
9131
  // must not leave a child blocked on an approval the operator never sees; interactive parents keep the IPC forward.
8922
9132
  const permissionMode = this.approvalMode?.autoApproveActions === true ? 'auto' : 'interactive';
@@ -8956,6 +9166,10 @@ export class CortexOrchestrator {
8956
9166
  }
8957
9167
  formatSubAgentResultForLLM(result) {
8958
9168
  const lines = [];
9169
+ if (result.agentName === 'browse-agent' && result.status === 'completed') {
9170
+ this.browseCompletedThisTurn = true;
9171
+ this.lastBrowseReport = { task: this.lastBrowseTask, report: String(result.fullResponse || result.summary || '') }; // CORTEX_BROWSE_CARRY
9172
+ }
8959
9173
  // Header
8960
9174
  lines.push(`# Sub-Agent Result: ${result.agentName}`);
8961
9175
  lines.push('');
@@ -9005,7 +9219,7 @@ export class CortexOrchestrator {
9005
9219
  lines.push(`- Input: ${result.cost.inputTokens.toLocaleString()}`);
9006
9220
  lines.push(`- Output: ${result.cost.outputTokens.toLocaleString()}`);
9007
9221
  if (result.cost.cacheHits > 0) {
9008
- lines.push(`- Cache Hits: ${result.cost.cacheHits}`);
9222
+ lines.push(`- Cache-read tokens: ${result.cost.cacheHits.toLocaleString()}`);
9009
9223
  }
9010
9224
  lines.push(`- Estimated Cost: $${result.cost.estimatedCost.toFixed(4)}`);
9011
9225
  lines.push('');
@@ -9021,11 +9235,13 @@ export class CortexOrchestrator {
9021
9235
  if (result.fullResponse) {
9022
9236
  lines.push('## Full Response');
9023
9237
  lines.push('');
9024
- const maxLen = 5000;
9238
+ // Keep the END (2026-11-02): fullResponse is the sub-agent's text across ALL its turns — narration first, the answer last. Keeping the
9239
+ // first 5000 chars dropped the answer itself (fuzzycortex: "the browser sub-agent's report was cut off partway"). Drop early narration.
9240
+ const maxLen = 8000;
9025
9241
  if (result.fullResponse.length > maxLen) {
9026
- lines.push(result.fullResponse.slice(0, maxLen));
9242
+ lines.push(`... (${result.fullResponse.length - maxLen} chars of earlier narration omitted — the sub-agent's final answer follows)`);
9027
9243
  lines.push('');
9028
- lines.push(`... (truncated, ${result.fullResponse.length - maxLen} chars omitted)`);
9244
+ lines.push(result.fullResponse.slice(-maxLen));
9029
9245
  }
9030
9246
  else {
9031
9247
  lines.push(result.fullResponse);
@@ -9539,6 +9755,16 @@ export class CortexOrchestrator {
9539
9755
  this.effortRampCalls = 0;
9540
9756
  this.computeNudgeLastRound = -1_000_000;
9541
9757
  this.computeNudgeFired = 0;
9758
+ this.browserWallNudged = false;
9759
+ this.directBrowseCalls = 0;
9760
+ this.directBrowseNudged = false;
9761
+ this.directConfirmAsked = false;
9762
+ this.sourcesReminded = false;
9763
+ this.browseCompletedThisTurn = false;
9764
+ this.lastBrowseReport = null;
9765
+ this.lastBrowseTask = '';
9766
+ this.reverifyExplained = new Set();
9767
+ this.probeExplained = false;
9542
9768
  }
9543
9769
  try {
9544
9770
  const cfg = resolveEffortRamp();
@@ -9628,6 +9854,78 @@ export class CortexOrchestrator {
9628
9854
  * append one 'compute/test instead of deliberating' line to the tail of the newest tool_result (the HB-TURN-STATUS carrier —
9629
9855
  * earlier request prefixes stay byte-identical). Cooldown + per-turn cap; one compute_nudge event per firing. Fail-open.
9630
9856
  */
9857
+ browserWallNudged = false;
9858
+ directBrowseCalls = 0;
9859
+ directBrowseNudged = false;
9860
+ directConfirmAsked = false;
9861
+ sourcesReminded = false;
9862
+ browseCompletedThisTurn = false;
9863
+ /** CORTEX_BROWSE_CARRY: the last COMPLETED browse delegate's task + report this turn (reset per turn). */
9864
+ lastBrowseReport = null;
9865
+ lastBrowseTask = '';
9866
+ reverifyExplained = new Set();
9867
+ probeExplained = false;
9868
+ /** A Browse call naming mode "direct" where direct is withheld (headless) runs as a delegate — same task, no error round-trip. Used by
9869
+ * EVERY executor call site (2026-11-03: the batch path still passed mode:"direct" through and the executor answered with an error). */
9870
+ executorInputFor(toolUse) {
9871
+ if (toolUse.name === 'Browse' && toolUse.input?.mode === 'direct' && !browseDirectAvailable(this.approvalMode.autoApproveActions)) {
9872
+ const { mode: _m, ...rest } = toolUse.input;
9873
+ void _m;
9874
+ return rest;
9875
+ }
9876
+ return toolUse.input;
9877
+ }
9878
+ /** Stage 5 needs `sources` after web use; say so at the first web result of the turn instead of after a rejected EndTurn. */
9879
+ injectSourcesReminder(toolResults) {
9880
+ try {
9881
+ if (this.sourcesReminded || !resolveEndTurnIntegrityMode(process.env))
9882
+ return;
9883
+ if (!toolResults.some((r) => WEB_TOOL_NAMES.includes(String(r.tool_name))))
9884
+ return;
9885
+ if (!appendStatusToNewestToolResult(this.messageHistory, SOURCES_REMINDER_LINE))
9886
+ return;
9887
+ this.invalidateTailUnitConversions();
9888
+ this.sourcesReminded = true;
9889
+ }
9890
+ catch { /* fail-open */ }
9891
+ }
9892
+ /** Long direct browsing → once per turn, steer the rest of the task to the browse sub-agent (browserWallNudge.ts directBrowseLine). */
9893
+ injectDirectBrowseNudge(round, streaming, toolUses = []) {
9894
+ try {
9895
+ if (this.directBrowseNudged || process.env.CORTEX_BROWSE_AGENT === 'true')
9896
+ return;
9897
+ this.directBrowseCalls += countDirectBrowserCalls(toolUses);
9898
+ const threshold = directBrowseNudgeThreshold();
9899
+ if (threshold <= 0 || this.directBrowseCalls < threshold || webToolBlocked('Browse'))
9900
+ return;
9901
+ if (!appendStatusToNewestToolResult(this.messageHistory, directBrowseLine(this.directBrowseCalls)))
9902
+ return;
9903
+ this.invalidateTailUnitConversions(); // R221 safety net
9904
+ this.directBrowseNudged = true;
9905
+ const store = this.getDecisionStore();
9906
+ if (store)
9907
+ void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'steering_injected', detail: { kinds: ['direct_browse_long'], calls: this.directBrowseCalls, iteration: round, streaming } }).catch(() => { });
9908
+ }
9909
+ catch { /* fail-open */ }
9910
+ }
9911
+ /** Browser-wall nudge (browserWallNudge.ts): once per turn, when a Bash/WebFetch result is a bot-check page and Browse is enabled. */
9912
+ injectBrowserWallNudge(toolResults, round, streaming, toolUses = []) {
9913
+ try {
9914
+ if (this.browserWallNudged || webToolBlocked('Browse'))
9915
+ return;
9916
+ const trigger = sawBotWall(toolResults) ? 'wall' : sawScriptedBrowser(toolUses) ? 'scripted_browser' : null;
9917
+ if (!trigger)
9918
+ return;
9919
+ if (!appendStatusToNewestToolResult(this.messageHistory, BROWSER_WALL_LINE))
9920
+ return;
9921
+ this.invalidateTailUnitConversions(); // R221 safety net
9922
+ this.browserWallNudged = true;
9923
+ const store = this.getDecisionStore();
9924
+ if (store)
9925
+ void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'steering_injected', detail: { kinds: ['browser_wall'], trigger, iteration: round, streaming } }).catch(() => { });
9926
+ }
9927
+ catch { /* fail-open */ }
9928
+ }
9631
9929
  injectComputeNudge(round, streaming) {
9632
9930
  try {
9633
9931
  const cfg = resolveComputeNudge();
@@ -9910,6 +10208,9 @@ export class CortexOrchestrator {
9910
10208
  };
9911
10209
  }
9912
10210
  async processToolTraining(toolUse, result) {
10211
+ // Secrets vault Phase 2: mask FIRST — every tool path funnels through here, and the decision store below persists errorSnippet
10212
+ // (later re-injected into prompts as priors) and training records. Identity without a vault.
10213
+ result = maskToolResults([result])[0];
9913
10214
  this.applySliceNudge(toolUse, result); // universal post-execution hook: every path funnels through processToolTraining
9914
10215
  const store = this.getDecisionStore();
9915
10216
  if (!store)
@@ -10516,7 +10817,7 @@ export class CortexOrchestrator {
10516
10817
  if (process.env.DEBUG === 'true') {
10517
10818
  console.log(`[Orchestrator] R30 recovery: found persisted sub-agent result for tool_use_id=${toolUse.id}`);
10518
10819
  }
10519
- const formatted = this.formatSubAgentResultForLLM(persisted);
10820
+ const formatted = maskSecretText(this.formatSubAgentResultForLLM(persisted)); // secrets vault Phase 2
10520
10821
  return {
10521
10822
  type: 'tool_result',
10522
10823
  toolResult: {
@@ -11184,7 +11485,7 @@ export class CortexOrchestrator {
11184
11485
  'Session & History': [
11185
11486
  'ListSessions', 'LoadSession', 'SearchConversationHistory',
11186
11487
  'GetConversationSegment', 'ListCompactionBoundaries', 'RequestHistoricalContext',
11187
- 'CanonListSessions', 'CanonPullSession',
11488
+ 'CanonListSessions', 'CanonPullSession', 'SecretsList',
11188
11489
  ],
11189
11490
  'Sandbox & Artifacts': [
11190
11491
  'CreateArtifactTool', 'InteractWithSandbox', 'ModifySandbox',