@nexus-cortex/core 4.90.2 → 4.91.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +19 -5
- package/dist/config/RuntimeConfigRegistry.d.ts.map +1 -1
- package/dist/config/RuntimeConfigRegistry.js +2 -0
- package/dist/config/RuntimeConfigRegistry.js.map +1 -1
- package/dist/config/SettingsLoader.d.ts.map +1 -1
- package/dist/config/SettingsLoader.js +2 -0
- package/dist/config/SettingsLoader.js.map +1 -1
- package/dist/config/SettingsSchema.d.ts +4 -0
- package/dist/config/SettingsSchema.d.ts.map +1 -1
- package/dist/config/SettingsSchema.js +18 -0
- package/dist/config/SettingsSchema.js.map +1 -1
- package/dist/config/effectiveConfig.d.ts.map +1 -1
- package/dist/config/effectiveConfig.js +3 -0
- package/dist/config/effectiveConfig.js.map +1 -1
- package/dist/middleware/HelperModelMiddleware.d.ts.map +1 -1
- package/dist/middleware/HelperModelMiddleware.js +2 -1
- package/dist/middleware/HelperModelMiddleware.js.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.d.ts +11 -0
- package/dist/orchestrator/CortexOrchestrator.d.ts.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.js +138 -222
- package/dist/orchestrator/CortexOrchestrator.js.map +1 -1
- package/dist/orchestrator/citationVerification.d.ts.map +1 -1
- package/dist/orchestrator/citationVerification.js +19 -1
- package/dist/orchestrator/citationVerification.js.map +1 -1
- package/dist/orchestrator/endTurnGates.d.ts +63 -0
- package/dist/orchestrator/endTurnGates.d.ts.map +1 -0
- package/dist/orchestrator/endTurnGates.js +203 -0
- package/dist/orchestrator/endTurnGates.js.map +1 -0
- package/dist/orchestrator/requirementsVerification.d.ts.map +1 -1
- package/dist/orchestrator/requirementsVerification.js +2 -1
- package/dist/orchestrator/requirementsVerification.js.map +1 -1
- package/dist/tools/ToolProfile.d.ts +6 -0
- package/dist/tools/ToolProfile.d.ts.map +1 -1
- package/dist/tools/ToolProfile.js +14 -0
- package/dist/tools/ToolProfile.js.map +1 -1
- package/dist/tools/registries/BaseToolRegistry.js +1 -1
- package/dist/tools/registries/BaseToolRegistry.js.map +1 -1
- package/package.json +3 -3
|
@@ -12,7 +12,6 @@ import { ToolNamingHandler } from '../adapters/ToolNamingHandler.js';
|
|
|
12
12
|
// Phase 2.4: Server-Side Tools Support
|
|
13
13
|
import { shouldUseServerSideTools, isServerSideToolsEnabled, modelSupportsServerSideTools } from '../adapters/ServerSideToolDetection.js';
|
|
14
14
|
import { shouldAutoInjectMcp } from './mcpAutoInjectPolicy.js';
|
|
15
|
-
import { verifyCitationsGrounded } from './citationVerification.js';
|
|
16
15
|
import { verifyCoordinates, deterministicCoordinateScore } from './coordinateVerification.js';
|
|
17
16
|
import { buildRouterSample, appendJsonlRotating } from './cortexTrainingRecord.js';
|
|
18
17
|
import { join as pathJoin } from 'path';
|
|
@@ -30,7 +29,8 @@ import { CacheMetricsAccumulator } from '../session/CacheMetricsAccumulator.js';
|
|
|
30
29
|
import { SubAgentProcessManager, createSubAgentProcessManager } from './SubAgentProcessManager.js';
|
|
31
30
|
// Phase 1: Tool Architecture Refactor - Unified Tool Registry
|
|
32
31
|
import { toolFactory } from '../tools/ToolFactory.js';
|
|
33
|
-
import { resolveToolProfile, resolveToolAnchor, resolveFrameProfile, resolveLiftNudge, resolveHeadlessDropAskUser, resolveDeferredLoading, isNarrowProfile, isToolAllowedByProfile, applyToolProfile, webToolBlocked, resolveVisionHelperModel } from '../tools/ToolProfile.js';
|
|
32
|
+
import { resolveToolProfile, resolveToolAnchor, resolveFrameProfile, resolveLiftNudge, resolveHeadlessDropAskUser, resolveDeferredLoading, isNarrowProfile, isToolAllowedByProfile, applyToolProfile, webToolBlocked, resolveVisionHelperModel, resolveVisionHandoffMax, resolveSliceNudge } from '../tools/ToolProfile.js';
|
|
33
|
+
import { TurnEvidence, evaluateEndTurnGates, buildMissingEndTurnReminder } from './endTurnGates.js';
|
|
34
34
|
import { readStagedDoctrine, applyCuratedDoctrine, runOrientForStaging, withTimeout } from './doctrineCuration.js';
|
|
35
35
|
import { ExactRepeatTracker } from '../training/loopLadder.js';
|
|
36
36
|
import { slashCommandRegistry } from '../commands/SlashCommandRegistry.js';
|
|
@@ -44,8 +44,6 @@ import { classifyToolOutcome } from '../training/toolOutcome.js';
|
|
|
44
44
|
import { LoopLadder, formatLadderSignal } from '../training/loopLadder.js';
|
|
45
45
|
import { shouldNudgeInaction, formatInactionNudge } from './inactionGuard.js';
|
|
46
46
|
import { applyImageTtlForRequest } from './imageTtl.js';
|
|
47
|
-
import { verifyRequirements, resolveEndTurnRequirementsMode, resolveEndTurnRequirementsStrict } from './requirementsVerification.js';
|
|
48
|
-
import { verifyIntegrity, resolveEndTurnIntegrityMode } from './integrityVerification.js';
|
|
49
47
|
import { detectSurrenderText, resolveSurrenderNudgeMode, SURRENDER_REMINDER } from './turnEndGuards.js';
|
|
50
48
|
import { ModelRouterMatrix } from '../training/ModelRouterMatrix.js';
|
|
51
49
|
import { classifyTask } from '../training/TaskClassifier.js';
|
|
@@ -1191,7 +1189,8 @@ export class CortexOrchestrator {
|
|
|
1191
1189
|
// calls the EndTurn attestation tool. Steering is ignored by some
|
|
1192
1190
|
// models (grok-4.3); a required tool call is not. Bounded so a model
|
|
1193
1191
|
// that refuses cannot hang the turn (fallback: accept after N nudges).
|
|
1194
|
-
|
|
1192
|
+
const ev = new TurnEvidence(); // per-turn evidence for the EndTurn gates (shared module, 4.91.0)
|
|
1193
|
+
this.visionHandoffsThisTurn = 0;
|
|
1195
1194
|
let endTurnNudges = 0;
|
|
1196
1195
|
let surrenderNudgeUsed = false; // item 13b: one execute-your-plan nudge per turn
|
|
1197
1196
|
const END_TURN_MAX_NUDGES = 2;
|
|
@@ -1207,14 +1206,12 @@ export class CortexOrchestrator {
|
|
|
1207
1206
|
// answer's line-number claims can be verified against what was actually
|
|
1208
1207
|
// read this turn (verifyCoordinates). Stage-2-grounded citations are the
|
|
1209
1208
|
// trusted baseline a coordinate must map to.
|
|
1210
|
-
let lastEndTurnCitations;
|
|
1211
1209
|
// EFFORT TAIL one-shot (def-efdbb67fd8): the first EndTurn of this turn has
|
|
1212
1210
|
// been bounced for a high-effort re-attestation cycle.
|
|
1213
|
-
let effortTailBounced = false;
|
|
1214
1211
|
let endTurnLastToolName; // for the cortex training record
|
|
1215
1212
|
// R60: the ROUTING decision = the FIRST real tool called in response to the
|
|
1216
1213
|
// user prompt, WITH ITS ACTUAL INPUT ARGS. Previously the router sample's
|
|
1217
|
-
// selected_args_json was filled with turn-quality metadata (endTurnCalled/
|
|
1214
|
+
// selected_args_json was filled with turn-quality metadata (ev.endTurnCalled/
|
|
1218
1215
|
// nudges/violations/draftPreview) — fine-tunes on that corpus learned to
|
|
1219
1216
|
// emit garbage args (proven by the v2p3 r9b pilot adapter).
|
|
1220
1217
|
let routerFirstToolName;
|
|
@@ -1222,25 +1219,18 @@ export class CortexOrchestrator {
|
|
|
1222
1219
|
// Scope: the gate ONLY arms when the turn actually used a (non-EndTurn)
|
|
1223
1220
|
// tool. A pure-language turn has no tool-derived artifact to
|
|
1224
1221
|
// misattribute, so forcing EndTurn there is pure overhead → bypass.
|
|
1225
|
-
let turnUsedTools = false;
|
|
1226
1222
|
// Drives the adaptive reminder (verification emphasis vs citation
|
|
1227
1223
|
// emphasis) keyed to what the turn actually did.
|
|
1228
|
-
let turnUsedMutatingTool = false; // Edit/Write/Bash/NotebookEdit
|
|
1229
|
-
let turnUsedReadishTool = false; // Read/Grep/Glob
|
|
1230
1224
|
// Stage 2: concatenated text of every (non-EndTurn) tool result the
|
|
1231
1225
|
// model saw THIS turn — the haystack EndTurn citations are verified
|
|
1232
1226
|
// against. A cited verbatim_source not found here is a regurgitated
|
|
1233
1227
|
// prior, not an observation: the EndTurn call is rejected.
|
|
1234
|
-
const thisTurnToolOutputs = [];
|
|
1235
1228
|
// Loop detection: Track recent tool calls to detect repetitive loops
|
|
1236
1229
|
const recentToolCalls = [];
|
|
1237
1230
|
const exactRepeatTracker = new ExactRepeatTracker();
|
|
1238
1231
|
// EndTurn Stage 5 (integrity): turn-level evidence accumulators — web
|
|
1239
1232
|
// activity and mutating-tool inputs can land many batches before the
|
|
1240
1233
|
// EndTurn call, so per-batch state is not enough.
|
|
1241
|
-
const integrityWebQueries = [];
|
|
1242
|
-
const integrityWebContent = [];
|
|
1243
|
-
const integrityWriteInputs = [];
|
|
1244
1234
|
// Unified Outcome Ladder (docs/UNIFIED_OUTCOME_LADDER.md): per-approach
|
|
1245
1235
|
// FAILURE escalation — remind(2)→diversify(4)→break(6) over normalized
|
|
1246
1236
|
// near-duplicate approaches. Complements isToolProgressStalled (which is
|
|
@@ -1252,6 +1242,7 @@ export class CortexOrchestrator {
|
|
|
1252
1242
|
const allExecutedToolUses = [];
|
|
1253
1243
|
// Track tool call counts for diversity warning
|
|
1254
1244
|
const toolCallCounts = new Map();
|
|
1245
|
+
let lastExecutedBatchUuid = null; // 4.90.3 re-execution guard (def-7f510b5635)
|
|
1255
1246
|
while (toolCallIteration < MAX_TOOL_ITERATIONS) {
|
|
1256
1247
|
try {
|
|
1257
1248
|
// DEBUG: Log canonical message content structure for tool extraction
|
|
@@ -1561,14 +1552,14 @@ export class CortexOrchestrator {
|
|
|
1561
1552
|
// Q1 proved the fabrication is intrinsic regurgitation — only a
|
|
1562
1553
|
// deterministic reject closes it.
|
|
1563
1554
|
let stage3Violations = [];
|
|
1564
|
-
if (endTurnGateEnabled &&
|
|
1555
|
+
if (endTurnGateEnabled && ev.usedTools && ev.endTurnCalled) {
|
|
1565
1556
|
const draft = Array.isArray(currentAssistantCanonicalMessage.content)
|
|
1566
1557
|
? currentAssistantCanonicalMessage.content
|
|
1567
1558
|
.filter((b) => b?.type === 'text')
|
|
1568
1559
|
.map((b) => b.text || '')
|
|
1569
1560
|
.join('\n')
|
|
1570
1561
|
: String(currentAssistantCanonicalMessage.content ?? '');
|
|
1571
|
-
const cv = verifyCoordinates(draft,
|
|
1562
|
+
const cv = verifyCoordinates(draft, ev.outputs.join('\n'), ev.lastCitations);
|
|
1572
1563
|
if (!cv.ok)
|
|
1573
1564
|
stage3Violations = cv.violations;
|
|
1574
1565
|
}
|
|
@@ -1577,7 +1568,7 @@ export class CortexOrchestrator {
|
|
|
1577
1568
|
// anyway (liveness beats purity) — but that fallback-accept was
|
|
1578
1569
|
// previously silent. Bank an event so the distiller can see
|
|
1579
1570
|
// un-attested passes.
|
|
1580
|
-
const endTurnGateUnsatisfied = endTurnGateEnabled &&
|
|
1571
|
+
const endTurnGateUnsatisfied = endTurnGateEnabled && ev.usedTools && (!ev.endTurnCalled || stage3Violations.length > 0);
|
|
1581
1572
|
// Item 13b (surrender guard): honest capitulation-with-a-plan — the
|
|
1582
1573
|
// final text enumerates remaining steps instead of executing them.
|
|
1583
1574
|
// One nudge per turn, own flag, shares the continuation plumbing.
|
|
@@ -1587,10 +1578,10 @@ export class CortexOrchestrator {
|
|
|
1587
1578
|
.map((b) => b.text || '')
|
|
1588
1579
|
.join('\n')
|
|
1589
1580
|
: String(currentAssistantCanonicalMessage.content ?? '');
|
|
1590
|
-
const surrenderUnsatisfied = resolveSurrenderNudgeMode() &&
|
|
1581
|
+
const surrenderUnsatisfied = resolveSurrenderNudgeMode() && ev.usedTools && !surrenderNudgeUsed &&
|
|
1591
1582
|
detectSurrenderText(surrenderDraftText);
|
|
1592
1583
|
if (endTurnGateUnsatisfied && endTurnNudges >= END_TURN_MAX_NUDGES) {
|
|
1593
|
-
const fallbackReason = !endTurnCalled ? 'missing-EndTurn' : 'coordinate-violation';
|
|
1584
|
+
const fallbackReason = !ev.endTurnCalled ? 'missing-EndTurn' : 'coordinate-violation';
|
|
1594
1585
|
console.warn(`[Orchestrator] EndTurn gate FALLBACK-ACCEPT: ${fallbackReason} after ${endTurnNudges} nudges — turn ships un-attested.`);
|
|
1595
1586
|
const store = this.getDecisionStore();
|
|
1596
1587
|
if (store) {
|
|
@@ -1619,7 +1610,7 @@ export class CortexOrchestrator {
|
|
|
1619
1610
|
else {
|
|
1620
1611
|
endTurnNudges++;
|
|
1621
1612
|
}
|
|
1622
|
-
const gateReason = !endTurnCalled ? 'missing-EndTurn' : 'coordinate-violation';
|
|
1613
|
+
const gateReason = !ev.endTurnCalled ? 'missing-EndTurn' : 'coordinate-violation';
|
|
1623
1614
|
console.warn(`[Orchestrator] EndTurn gate: ${gateReason} ` +
|
|
1624
1615
|
`(nudge ${endTurnNudges}/${END_TURN_MAX_NUDGES}, iteration=${toolCallIteration}` +
|
|
1625
1616
|
`${stage3Violations.length ? `, ungrounded lines: ${stage3Violations.map((v) => v.line).join(',')}` : ''}).`);
|
|
@@ -1640,24 +1631,10 @@ export class CortexOrchestrator {
|
|
|
1640
1631
|
}
|
|
1641
1632
|
// Adaptive reminder: name what THIS turn actually did (specific
|
|
1642
1633
|
// imperatives bind harder than generic boilerplate).
|
|
1643
|
-
const citEmphasis = turnUsedReadishTool
|
|
1644
|
-
? ' You read code/files this turn: for EACH reference in your draft give the EXACT verbatim source you copied it from; if you cannot, DELETE that reference and quote the code instead of asserting a coordinate.'
|
|
1645
|
-
: '';
|
|
1646
|
-
const verEmphasis = turnUsedMutatingTool
|
|
1647
|
-
? ' You ran edit/write/bash this turn: in `verification` list every build/test/lint command you ACTUALLY ran with the real result line you saw — do not claim a check you did not run.'
|
|
1648
|
-
: '';
|
|
1649
1634
|
const endTurnReminderText = surrenderUnsatisfied
|
|
1650
1635
|
? SURRENDER_REMINDER
|
|
1651
|
-
: !endTurnCalled
|
|
1652
|
-
? (
|
|
1653
|
-
'You MUST call EndTurn before any final answer. It is generative, not a checkbox: ' +
|
|
1654
|
-
'reconstruct `citations` (array of {reference, verbatim_source}), `verification` ' +
|
|
1655
|
-
'(array of {command, observed_result}), `summary`, `open_items`, and a skeptical ' +
|
|
1656
|
-
'`self_review` (what you did NOT check, what is assumed/possibly wrong, what one ' +
|
|
1657
|
-
'more tool call would verify).' +
|
|
1658
|
-
citEmphasis +
|
|
1659
|
-
verEmphasis +
|
|
1660
|
-
' Call EndTurn now — do not produce a final answer until you have.</system-reminder>')
|
|
1636
|
+
: !ev.endTurnCalled
|
|
1637
|
+
? buildMissingEndTurnReminder(ev)
|
|
1661
1638
|
: ('<system-reminder>EndTurn REJECTED. Your drafted answer asserts line number(s) ' +
|
|
1662
1639
|
stage3Violations.map((v) => v.line).join(', ') +
|
|
1663
1640
|
' that are NOT backed by any citation whose verbatim_source actually sits at that ' +
|
|
@@ -1758,7 +1735,7 @@ export class CortexOrchestrator {
|
|
|
1758
1735
|
// IS the deterministic verdict — the slot the nexus schema reserves
|
|
1759
1736
|
// ("alignment scorer / CHALLENGE-graded review") but leaves empty.
|
|
1760
1737
|
// Fail-safe: training telemetry must never break a turn.
|
|
1761
|
-
if (
|
|
1738
|
+
if (ev.usedTools) {
|
|
1762
1739
|
try {
|
|
1763
1740
|
const draftText = Array.isArray(currentAssistantCanonicalMessage.content)
|
|
1764
1741
|
? currentAssistantCanonicalMessage.content
|
|
@@ -1771,8 +1748,8 @@ export class CortexOrchestrator {
|
|
|
1771
1748
|
// line-number claims that correspond to a real line in this
|
|
1772
1749
|
// turn's cat -n reads (not a flat 0). With the ReadFileTool
|
|
1773
1750
|
// root-cause fix the model transcribes real numbers → ~1.0.
|
|
1774
|
-
const outcomeScore = !endTurnCalled
|
|
1775
|
-
? deterministicCoordinateScore(draftText,
|
|
1751
|
+
const outcomeScore = !ev.endTurnCalled
|
|
1752
|
+
? deterministicCoordinateScore(draftText, ev.outputs.join('\n'))
|
|
1776
1753
|
: stage3Violations.length > 0
|
|
1777
1754
|
? 0.3
|
|
1778
1755
|
: endTurnNudges > 0
|
|
@@ -1836,6 +1813,7 @@ export class CortexOrchestrator {
|
|
|
1836
1813
|
}
|
|
1837
1814
|
// Track these tool uses for the response
|
|
1838
1815
|
allExecutedToolUses.push(...toolUseBlocks);
|
|
1816
|
+
ev.noteToolUses(toolUseBlocks); // per-turn evidence (shared with the streaming loop — endTurnGates.ts)
|
|
1839
1817
|
// Track per-tool call counts for diversity warning
|
|
1840
1818
|
for (const tu of toolUseBlocks) {
|
|
1841
1819
|
toolCallCounts.set(tu.name, (toolCallCounts.get(tu.name) || 0) + 1);
|
|
@@ -1843,20 +1821,23 @@ export class CortexOrchestrator {
|
|
|
1843
1821
|
// verify against this turn's tool output (set post-execution
|
|
1844
1822
|
// below). A non-EndTurn tool arms the gate + records category.
|
|
1845
1823
|
if (tu.name !== 'EndTurn') {
|
|
1846
|
-
turnUsedTools = true;
|
|
1847
1824
|
endTurnLastToolName = tu.name; // last real tool — for cortex record
|
|
1848
1825
|
if (routerFirstToolName === undefined) {
|
|
1849
1826
|
routerFirstToolName = tu.name; // the routing decision (R60)
|
|
1850
1827
|
routerFirstToolArgs = tu.input ?? {};
|
|
1851
1828
|
}
|
|
1852
|
-
if (['Edit', 'Write', 'Bash', 'NotebookEdit'].includes(tu.name)) {
|
|
1853
|
-
turnUsedMutatingTool = true;
|
|
1854
|
-
}
|
|
1855
|
-
if (['Read', 'Grep', 'Glob'].includes(tu.name)) {
|
|
1856
|
-
turnUsedReadishTool = true;
|
|
1857
|
-
}
|
|
1858
1829
|
}
|
|
1859
1830
|
}
|
|
1831
|
+
// 4.90.3 (def-7f510b5635): a batch that ALREADY EXECUTED must never run again. When an exception
|
|
1832
|
+
// escapes after execution (gate/lens/banking path), the catch below emits error tool_results but
|
|
1833
|
+
// the loop re-enters with the SAME assistant message — re-running its tools (real side effects)
|
|
1834
|
+
// until the error cap (v4 flash/dna-insert: 1000 iterations). Leave the loop instead; the
|
|
1835
|
+
// post-loop synthesis (R29a) gives the model one call to answer over the error results.
|
|
1836
|
+
if (lastExecutedBatchUuid !== null && currentAssistantCanonicalMessage.uuid === lastExecutedBatchUuid) {
|
|
1837
|
+
console.error(`[Orchestrator Phase 2.5] Tool batch ${lastExecutedBatchUuid} already executed — an exception escaped after execution; NOT re-running the batch (leaving the tool loop).`);
|
|
1838
|
+
break;
|
|
1839
|
+
}
|
|
1840
|
+
lastExecutedBatchUuid = currentAssistantCanonicalMessage.uuid ?? null;
|
|
1860
1841
|
toolCallIteration++;
|
|
1861
1842
|
if (this.config.debug) {
|
|
1862
1843
|
console.log(`[Orchestrator Phase 2.5] Iteration ${toolCallIteration}: Model requested ${toolUseBlocks.length} tool call(s)`);
|
|
@@ -1867,30 +1848,6 @@ export class CortexOrchestrator {
|
|
|
1867
1848
|
const inputHash = JSON.stringify(toolUse.input);
|
|
1868
1849
|
const toolSignature = { name: toolUse.name, inputHash };
|
|
1869
1850
|
recentToolCalls.push(toolSignature);
|
|
1870
|
-
// Stage-5 evidence accumulation (cheap, always-on; verdicts only
|
|
1871
|
-
// computed at the gate when CORTEX_ENDTURN_INTEGRITY is armed).
|
|
1872
|
-
if (toolUse.name === 'WebSearch') {
|
|
1873
|
-
const q = toolUse.input?.query;
|
|
1874
|
-
if (typeof q === 'string' && q)
|
|
1875
|
-
integrityWebQueries.push(q);
|
|
1876
|
-
}
|
|
1877
|
-
else if (toolUse.name === 'Browse') {
|
|
1878
|
-
// Browse renders full pages — the richest retrieval channel; its
|
|
1879
|
-
// task text rides the query checks and triggers attestation.
|
|
1880
|
-
const t = toolUse.input?.task;
|
|
1881
|
-
if (typeof t === 'string' && t)
|
|
1882
|
-
integrityWebQueries.push(t);
|
|
1883
|
-
}
|
|
1884
|
-
else if (toolUse.name === 'Write') {
|
|
1885
|
-
const c = toolUse.input?.content;
|
|
1886
|
-
if (typeof c === 'string' && c)
|
|
1887
|
-
integrityWriteInputs.push(c);
|
|
1888
|
-
}
|
|
1889
|
-
else if (toolUse.name === 'Edit') {
|
|
1890
|
-
const ns = toolUse.input?.new_string;
|
|
1891
|
-
if (typeof ns === 'string' && ns)
|
|
1892
|
-
integrityWriteInputs.push(ns);
|
|
1893
|
-
}
|
|
1894
1851
|
// CONSECUTIVE byte-identical repeats only (2026-08-26 fix — the old
|
|
1895
1852
|
// whole-turn occurrence count killed legitimate scattered repeats,
|
|
1896
1853
|
// e.g. identical `npm test` after each fix; see ExactRepeatTracker).
|
|
@@ -1925,6 +1882,7 @@ export class CortexOrchestrator {
|
|
|
1925
1882
|
const processedToolUseIds = new Set();
|
|
1926
1883
|
try {
|
|
1927
1884
|
const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState);
|
|
1885
|
+
this.testPostExecThrow(); // test-only fault injection for the re-execution guard (inert in prod)
|
|
1928
1886
|
clearTimeout(timeoutId);
|
|
1929
1887
|
// Unified Outcome Ladder: observe each result's TRUE outcome (exit
|
|
1930
1888
|
// codes, not wire is_error) and escalate on repeated failing
|
|
@@ -2034,155 +1992,9 @@ export class CortexOrchestrator {
|
|
|
2034
1992
|
// ungrounded verbatim_source is a regurgitated prior, not an
|
|
2035
1993
|
// observation → reject the EndTurn (mutate its result into an
|
|
2036
1994
|
// error the model must act on) and leave the gate unsatisfied.
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
const txt = typeof tr.content === 'string' ? tr.content : JSON.stringify(tr.content);
|
|
2041
|
-
if (txt)
|
|
2042
|
-
thisTurnToolOutputs.push(txt);
|
|
2043
|
-
if ((tr.tool_name === 'WebSearch' || tr.tool_name === 'WebFetch' || tr.tool_name === 'Browse') && txt) {
|
|
2044
|
-
integrityWebContent.push(txt);
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
// 4.90.2: the whole EndTurn gate block is exception-safe. A throw here (4.90.0 strict
|
|
2048
|
-
// null.filter) escaped to the tool-loop catch, which RE-RUNS the already-executed batch
|
|
2049
|
-
// without a model call (v4 flash/dna-insert: 1000 iterations, 4 identical results) —
|
|
2050
|
-
// a gate bug must never re-execute tools; it becomes an is_error EndTurn result instead.
|
|
2051
|
-
try {
|
|
2052
|
-
for (const tr of toolResults) {
|
|
2053
|
-
if (tr.tool_name !== 'EndTurn')
|
|
2054
|
-
continue;
|
|
2055
|
-
const etUse = toolUseBlocks.find((t) => t.name === 'EndTurn');
|
|
2056
|
-
// EFFORT TAIL (def-efdbb67fd8, finish-discipline half): the FIRST
|
|
2057
|
-
// EndTurn is the model's own "I'm done" declaration — generated at
|
|
2058
|
-
// body effort. Bounce it ONCE ("verify at depth") and arm the effort
|
|
2059
|
-
// pulse so the re-attestation cycle runs elevated: structure forces
|
|
2060
|
-
// the check (the gate), effort funds it (the pulse). One-shot per
|
|
2061
|
-
// turn; skipped when a thrash pulse is already active (chain already
|
|
2062
|
-
// elevated). Rides the same is_error rejection channel as Stages
|
|
2063
|
-
// 2/4/5; liveness bounded by the gate's existing fallback-accept.
|
|
2064
|
-
// Requires CORTEX_ENDTURN_GATE (EndTurn must be offered). Ship dark.
|
|
2065
|
-
if (process.env.CORTEX_EFFORT_TAIL === 'true' &&
|
|
2066
|
-
!effortTailBounced &&
|
|
2067
|
-
this.effortPulseRemaining <= 0) {
|
|
2068
|
-
effortTailBounced = true;
|
|
2069
|
-
const tailTurns = Math.max(1, parseInt(process.env.CORTEX_EFFORT_TAIL_TURNS || '2', 10) || 2);
|
|
2070
|
-
this.effortPulseRemaining = tailTurns;
|
|
2071
|
-
// Mechanism-engagement evidence (bench adjudication rule): bank the
|
|
2072
|
-
// tail-fired fact — the fd cell (2026-09-02) had to read engagement
|
|
2073
|
-
// from session text because the tail armed the counter silently.
|
|
2074
|
-
const tailStore = this.getDecisionStore();
|
|
2075
|
-
if (tailStore) {
|
|
2076
|
-
void tailStore.recordEvent({
|
|
2077
|
-
sessionId: this.currentSessionId ?? 'unknown',
|
|
2078
|
-
kind: 'effort_pulse',
|
|
2079
|
-
toolName: 'EndTurn',
|
|
2080
|
-
detail: { trigger: 'tail', cumFailures: 0, turns: tailTurns, level: process.env.CORTEX_EFFORT_PULSE_LEVEL || 'high' },
|
|
2081
|
-
}).catch(() => { });
|
|
2082
|
-
}
|
|
2083
|
-
tr.is_error = true;
|
|
2084
|
-
tr.content =
|
|
2085
|
-
'Before finishing: verify at depth. Re-read the original task statement; ' +
|
|
2086
|
-
're-open and check each requirement against the ACTUAL files/outputs you produced this turn ' +
|
|
2087
|
-
'(do not rely on memory of them); fix anything that does not match; then call EndTurn again ' +
|
|
2088
|
-
'with the verified attestation.';
|
|
2089
|
-
if (this.config.debug) {
|
|
2090
|
-
console.log(`[EffortTail] first EndTurn bounced — re-attestation at elevated effort (${this.effortPulseRemaining} continuation(s))`);
|
|
2091
|
-
}
|
|
2092
|
-
continue;
|
|
2093
|
-
}
|
|
2094
|
-
const cits = etUse?.input?.citations;
|
|
2095
|
-
lastEndTurnCitations = Array.isArray(cits) ? cits : undefined; // Stage 3 baseline
|
|
2096
|
-
const verdict = verifyCitationsGrounded(cits, thisTurnToolOutputs.join('\n'));
|
|
2097
|
-
if (verdict.grounded) {
|
|
2098
|
-
// Stage 4 (backlog item 1, CORTEX_ENDTURN_REQUIREMENTS): verify
|
|
2099
|
-
// the requirements attestation — the wrong-artifact weapon.
|
|
2100
|
-
// Stage 2 verifies citations; nothing before this forced
|
|
2101
|
-
// re-reading the TASK. Rides the same nudge budget: a Stage-4
|
|
2102
|
-
// reject leaves the gate unsatisfied so the bounded
|
|
2103
|
-
// fallback-accept still guarantees liveness.
|
|
2104
|
-
if (resolveEndTurnRequirementsMode()) {
|
|
2105
|
-
const s4 = verifyRequirements({
|
|
2106
|
-
requirements: etUse?.input?.requirements,
|
|
2107
|
-
verification: etUse?.input?.verification,
|
|
2108
|
-
userTaskText: this.lastRealUserText(),
|
|
2109
|
-
turnUsedMutatingTool,
|
|
2110
|
-
strict: resolveEndTurnRequirementsStrict(process.env),
|
|
2111
|
-
toolOutputs: thisTurnToolOutputs.join('\n'),
|
|
2112
|
-
});
|
|
2113
|
-
if (s4.ok) {
|
|
2114
|
-
endTurnCalled = true;
|
|
2115
|
-
}
|
|
2116
|
-
else {
|
|
2117
|
-
tr.is_error = true;
|
|
2118
|
-
tr.content = s4.nudge;
|
|
2119
|
-
console.warn('[Orchestrator] Stage4: EndTurn rejected — requirements attestation unsatisfied.');
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
|
-
else {
|
|
2123
|
-
endTurnCalled = true;
|
|
2124
|
-
}
|
|
2125
|
-
// Stage 5 (item 12 layer 4, CORTEX_ENDTURN_INTEGRITY): mechanical
|
|
2126
|
-
// task-integrity checks — web-content transplant into artifacts +
|
|
2127
|
-
// solution-seeking queries. Flags ALWAYS bank as integrity_flag
|
|
2128
|
-
// events (the distiller lens reads them); the nudge rides the
|
|
2129
|
-
// same bounded budget, fallback-accept preserves liveness.
|
|
2130
|
-
if (endTurnCalled && resolveEndTurnIntegrityMode()) {
|
|
2131
|
-
const s5 = verifyIntegrity({
|
|
2132
|
-
webQueries: integrityWebQueries,
|
|
2133
|
-
webContent: integrityWebContent,
|
|
2134
|
-
writeInputs: integrityWriteInputs,
|
|
2135
|
-
userTaskText: this.lastRealUserText(),
|
|
2136
|
-
sourcesAttestation: etUse?.input?.sources,
|
|
2137
|
-
});
|
|
2138
|
-
if (s5.flags.length > 0) {
|
|
2139
|
-
const store = this.getDecisionStore();
|
|
2140
|
-
if (store) {
|
|
2141
|
-
for (const f of s5.flags) {
|
|
2142
|
-
void store.recordEvent({
|
|
2143
|
-
sessionId: this.currentSessionId ?? 'unknown',
|
|
2144
|
-
kind: 'integrity_flag',
|
|
2145
|
-
detail: { check: f.check, detail: f.detail },
|
|
2146
|
-
}).catch(() => { });
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
}
|
|
2150
|
-
if (!s5.ok) {
|
|
2151
|
-
endTurnCalled = false;
|
|
2152
|
-
tr.is_error = true;
|
|
2153
|
-
tr.content = s5.nudge;
|
|
2154
|
-
console.warn(`[Orchestrator] Stage5: EndTurn rejected — ${s5.flags.length} integrity flag(s).`);
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
else {
|
|
2159
|
-
const bad = verdict.ungrounded
|
|
2160
|
-
.map(u => ` - "${u.reference}" — not found in this turn's tool output: ${String(u.verbatim_source).slice(0, 120)}`)
|
|
2161
|
-
.join('\n');
|
|
2162
|
-
tr.is_error = true;
|
|
2163
|
-
tr.content =
|
|
2164
|
-
`EndTurn REJECTED — these citations are not grounded in anything you read this turn:\n${bad}\n\n` +
|
|
2165
|
-
`A quote or coordinate you did not transcribe from this turn's tool output is a fabrication (a regurgitated guess), exactly like a non-matching edit old_string. ` +
|
|
2166
|
-
`Either RE-READ the exact region and copy the real text, or DELETE that reference from your answer (quote only code you can ground), then call EndTurn again. ` +
|
|
2167
|
-
`Grounding is per-TURN: if the line you want to cite is not in THIS turn's tool output (you read it earlier, or it is a file you wrote), RE-RUN the command that displays it now (cat/grep/sed the file, run the test) and copy the line from THAT output — do not resubmit the same citations.`;
|
|
2168
|
-
console.warn(`[Orchestrator] Stage2: EndTurn rejected — ${verdict.ungrounded.length} ungrounded citation(s).`);
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
catch (gateErr) {
|
|
2173
|
-
const msg = String(gateErr?.message ?? gateErr).slice(0, 200);
|
|
2174
|
-
console.error(`[Orchestrator] EndTurn gate evaluation threw (converted to an error result, batch NOT re-run): ${msg}`);
|
|
2175
|
-
for (const tr of toolResults) {
|
|
2176
|
-
if (tr.tool_name !== 'EndTurn')
|
|
2177
|
-
continue;
|
|
2178
|
-
tr.is_error = true;
|
|
2179
|
-
tr.content =
|
|
2180
|
-
`EndTurn gate evaluation failed internally (${msg}). This is a harness fault, not your attestation. ` +
|
|
2181
|
-
'Call EndTurn again with well-formed arrays: citations [{reference, verbatim_source}], verification [{command, observed_result}], requirements [{requirement, satisfied_by, verified_how}].';
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2184
|
-
// Item 7: scrub ReadImage payloads from metadata BEFORE persist;
|
|
2185
|
-
// the images inject as a user message after the results land.
|
|
1995
|
+
ev.noteToolResults(toolResults);
|
|
1996
|
+
// Stages 2/4/5 + effort tail — ONE implementation shared with the streaming loop (endTurnGates.ts, 4.91.0).
|
|
1997
|
+
evaluateEndTurnGates(ev, toolResults, toolUseBlocks, this.gateDeps());
|
|
2186
1998
|
const pendingImages = this.collectPendingImages(toolResults, effectiveModel.vision === true);
|
|
2187
1999
|
await this.applyVisionHandoffs();
|
|
2188
2000
|
// Create tool_result messages and add to history
|
|
@@ -2695,7 +2507,7 @@ export class CortexOrchestrator {
|
|
|
2695
2507
|
// Item 13a: the R29a path structurally bypasses the EndTurn gate (it
|
|
2696
2508
|
// runs post-loop with tools suppressed). When the gate was armed and
|
|
2697
2509
|
// owed an attestation, make the un-attested pass VISIBLE.
|
|
2698
|
-
if (endTurnGateEnabled &&
|
|
2510
|
+
if (endTurnGateEnabled && ev.usedTools && !ev.endTurnCalled) {
|
|
2699
2511
|
const store = this.getDecisionStore();
|
|
2700
2512
|
if (store) {
|
|
2701
2513
|
void store.recordEvent({
|
|
@@ -3474,7 +3286,25 @@ export class CortexOrchestrator {
|
|
|
3474
3286
|
// (no reminders, no force-synthesis cap). Runaway protection then rests on
|
|
3475
3287
|
// loop detection, the consecutive-error breaker, and MAX_TOOL_ITERATIONS.
|
|
3476
3288
|
const TOOL_BUDGET_HARD = TOOL_BUDGET_SOFT > 0 ? TOOL_BUDGET_SOFT * 2 : Infinity; // force-synthesis cap
|
|
3289
|
+
let lastExecutedBatchUuid = null; // 4.90.3 re-execution guard (def-7f510b5635)
|
|
3290
|
+
const ev = new TurnEvidence(); // 4.91.0: per-turn evidence for the EndTurn gates (streaming)
|
|
3291
|
+
this.visionHandoffsThisTurn = 0;
|
|
3292
|
+
let streamEndTurnNudges = 0;
|
|
3293
|
+
let pendingGateRequest = false; // Stage-1: a reminder was appended; run one continuation with NO tool batch
|
|
3294
|
+
const streamEndTurnGateEnabled = process.env.CORTEX_ENDTURN_GATE === 'true';
|
|
3477
3295
|
while (hasToolUse && toolCallIteration < MAX_TOOL_ITERATIONS) {
|
|
3296
|
+
const gateReRequest = pendingGateRequest;
|
|
3297
|
+
pendingGateRequest = false;
|
|
3298
|
+
// 4.90.3 (def-7f510b5635): a batch that ALREADY EXECUTED must never run again. When an exception
|
|
3299
|
+
// escapes after execution (gate/lens/banking path), the catch below emits error tool_results but
|
|
3300
|
+
// the loop re-enters with the SAME assistant message — re-running its tools (real side effects)
|
|
3301
|
+
// until the error cap (v4 flash/dna-insert: 1000 iterations). Leave the loop instead; the
|
|
3302
|
+
// post-loop synthesis (R29a) gives the model one call to answer over the error results.
|
|
3303
|
+
if (!gateReRequest && lastExecutedBatchUuid !== null && currentAssistantCanonicalMessage.uuid === lastExecutedBatchUuid) {
|
|
3304
|
+
console.error(`[Orchestrator Streaming] Tool batch ${lastExecutedBatchUuid} already executed — an exception escaped after execution; NOT re-running the batch (leaving the tool loop).`);
|
|
3305
|
+
break;
|
|
3306
|
+
}
|
|
3307
|
+
lastExecutedBatchUuid = currentAssistantCanonicalMessage.uuid ?? null;
|
|
3478
3308
|
toolCallIteration++;
|
|
3479
3309
|
// Extract tool use blocks from current message
|
|
3480
3310
|
const toolUseBlocks = currentAssistantCanonicalMessage.content
|
|
@@ -3484,7 +3314,8 @@ export class CortexOrchestrator {
|
|
|
3484
3314
|
name: block.toolUse.name,
|
|
3485
3315
|
input: block.toolUse.input
|
|
3486
3316
|
}));
|
|
3487
|
-
|
|
3317
|
+
ev.noteToolUses(toolUseBlocks);
|
|
3318
|
+
if (toolUseBlocks.length === 0 && !gateReRequest) {
|
|
3488
3319
|
// R32 (streaming R18b parity): detect empty/thinking-only responses.
|
|
3489
3320
|
// sendMessage retries ONCE inside the loop with tools preserved.
|
|
3490
3321
|
const hasVisibleText = hasVisibleAssistantText(currentAssistantCanonicalMessage.content);
|
|
@@ -3776,7 +3607,11 @@ export class CortexOrchestrator {
|
|
|
3776
3607
|
try {
|
|
3777
3608
|
// Execute tools (reuse existing method)
|
|
3778
3609
|
const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState);
|
|
3610
|
+
this.testPostExecThrow(); // test-only fault injection for the re-execution guard (inert in prod)
|
|
3779
3611
|
clearTimeout(timeoutId);
|
|
3612
|
+
// 4.91.0: EndTurn gates for STREAMING (were sendMessage-only) — same evidence + evaluation.
|
|
3613
|
+
ev.noteToolResults(toolResults);
|
|
3614
|
+
evaluateEndTurnGates(ev, toolResults, toolUseBlocks, this.gateDeps());
|
|
3780
3615
|
// Unified Outcome Ladder (streaming parity with sendMessage).
|
|
3781
3616
|
ladderSignal = null;
|
|
3782
3617
|
{
|
|
@@ -4236,6 +4071,31 @@ export class CortexOrchestrator {
|
|
|
4236
4071
|
input: block.toolUse.input
|
|
4237
4072
|
}));
|
|
4238
4073
|
hasToolUse = continuationToolUses.length > 0;
|
|
4074
|
+
// 4.91.0 Stage-1 for STREAMING: tools were used but no valid EndTurn yet → append the reminder and
|
|
4075
|
+
// run ONE more continuation with no tool batch (pendingGateRequest bypasses the empty-batch break
|
|
4076
|
+
// and the re-execution guard). Two nudges, then fallback-accept (parity with sendMessage).
|
|
4077
|
+
if (!hasToolUse && streamEndTurnGateEnabled && ev.usedTools && !ev.endTurnCalled) {
|
|
4078
|
+
if (streamEndTurnNudges < 2) {
|
|
4079
|
+
streamEndTurnNudges++;
|
|
4080
|
+
const reminderMsg = {
|
|
4081
|
+
uuid: uuidv4(),
|
|
4082
|
+
timestamp: new Date().toISOString(),
|
|
4083
|
+
type: 'user',
|
|
4084
|
+
message: { role: 'user', content: [{ type: 'text', text: buildMissingEndTurnReminder(ev) }] },
|
|
4085
|
+
timeline: { sessionId: this.currentSessionId, conversationId: this.currentConversationId, turnNumber: this.turnNumber + 1 },
|
|
4086
|
+
model: { id: effectiveModel.id, provider: effectiveModel.provider, apiPattern: effectiveModel.api.pattern },
|
|
4087
|
+
};
|
|
4088
|
+
this.messageHistory.push(reminderMsg);
|
|
4089
|
+
await this.historyStore.appendMessage(this.currentSessionId, reminderMsg);
|
|
4090
|
+
console.warn(`[Orchestrator Streaming] EndTurn gate: missing-EndTurn (nudge ${streamEndTurnNudges}/2, iteration=${toolCallIteration}) — re-requesting.`);
|
|
4091
|
+
pendingGateRequest = true;
|
|
4092
|
+
hasToolUse = true;
|
|
4093
|
+
}
|
|
4094
|
+
else {
|
|
4095
|
+
console.warn('[Orchestrator Streaming] EndTurn gate FALLBACK-ACCEPT: missing-EndTurn after 2 nudges — turn ships un-attested.');
|
|
4096
|
+
this.gateDeps().recordEvent('endturn_gate_fallback', { reason: 'missing-EndTurn', nudges: streamEndTurnNudges, iteration: toolCallIteration, streaming: true });
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4239
4099
|
// Stop if too many errors
|
|
4240
4100
|
if (totalToolErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
4241
4101
|
console.warn(`[Orchestrator Streaming] Stopping loop due to accumulated errors`);
|
|
@@ -6836,16 +6696,71 @@ export class CortexOrchestrator {
|
|
|
6836
6696
|
pendingVisionHandoffs = [];
|
|
6837
6697
|
/** Resolve queued ReadImage hand-offs through the helper middleware: the vision helper's TEXT
|
|
6838
6698
|
* replaces the tool_result content. Failures degrade to the pre-hand-off guidance (bash OCR). */
|
|
6699
|
+
/** Per-turn ReadImage→vision-helper hand-off count (VISION_HANDOFF_MAX); reset at the start of each turn. */
|
|
6700
|
+
visionHandoffsThisTurn = 0;
|
|
6701
|
+
/** Per-session bash slice-read counts per file (CORTEX_SLICE_NUDGE). */
|
|
6702
|
+
sliceReads = new Map();
|
|
6703
|
+
/** Dependencies the shared EndTurn gates need from the orchestrator (endTurnGates.ts). */
|
|
6704
|
+
gateDeps() {
|
|
6705
|
+
return {
|
|
6706
|
+
userTaskText: this.lastRealUserText(),
|
|
6707
|
+
env: process.env,
|
|
6708
|
+
debug: this.config.debug,
|
|
6709
|
+
recordEvent: (kind, detail, toolName) => {
|
|
6710
|
+
const store = this.getDecisionStore();
|
|
6711
|
+
if (!store)
|
|
6712
|
+
return;
|
|
6713
|
+
void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind, toolName, detail }).catch(() => { });
|
|
6714
|
+
},
|
|
6715
|
+
effortTail: { remaining: () => this.effortPulseRemaining, arm: (turns) => { this.effortPulseRemaining = turns; } },
|
|
6716
|
+
};
|
|
6717
|
+
}
|
|
6718
|
+
/** Test-only fault injection: CORTEX_TEST_POST_EXEC_THROW=once throws exactly once right after a tool
|
|
6719
|
+
* batch executed, to prove the loop never re-runs an executed batch (def-7f510b5635). Inert otherwise. */
|
|
6720
|
+
testPostExecThrow() {
|
|
6721
|
+
if (process.env.CORTEX_TEST_POST_EXEC_THROW === 'once') {
|
|
6722
|
+
process.env.CORTEX_TEST_POST_EXEC_THROW = 'fired';
|
|
6723
|
+
throw new Error('CORTEX_TEST_POST_EXEC_THROW: synthetic post-execution fault');
|
|
6724
|
+
}
|
|
6725
|
+
}
|
|
6726
|
+
/** CORTEX_SLICE_NUDGE: after the 3rd bash slice-read of the same file, append a one-line Read reminder. */
|
|
6727
|
+
applySliceNudge(toolUse, result) {
|
|
6728
|
+
if (toolUse.name !== 'Bash' || result.is_error || !resolveSliceNudge(process.env))
|
|
6729
|
+
return;
|
|
6730
|
+
const cmd = String(toolUse.input?.command ?? '');
|
|
6731
|
+
const m = cmd.match(/(?:sed\s+-n\s+['"]?\d+\s*,\s*\d+\s*p['"]?|head\s+-n?\s*\d+|tail\s+-n?\s*\d+)\s+([^\s;|&>]+)/);
|
|
6732
|
+
if (!m)
|
|
6733
|
+
return;
|
|
6734
|
+
const file = m[1];
|
|
6735
|
+
const n = (this.sliceReads.get(file) ?? 0) + 1;
|
|
6736
|
+
this.sliceReads.set(file, n);
|
|
6737
|
+
if (n !== 3)
|
|
6738
|
+
return;
|
|
6739
|
+
if (typeof result.content === 'string') {
|
|
6740
|
+
result.content += `\n\n<system-reminder>You have now read ${file} in ${n} bash slices. Read it ONCE with the Read tool (offset/limit for long files) instead of repeated sed/head/tail slices — slicing loses context and burns turns.</system-reminder>`;
|
|
6741
|
+
}
|
|
6742
|
+
this.gateDeps().recordEvent('steering_injected', { kind: 'slice_read', file, count: n }, 'Bash');
|
|
6743
|
+
}
|
|
6839
6744
|
async applyVisionHandoffs() {
|
|
6840
6745
|
const queue = this.pendingVisionHandoffs;
|
|
6841
6746
|
this.pendingVisionHandoffs = [];
|
|
6747
|
+
const max = resolveVisionHandoffMax(process.env);
|
|
6842
6748
|
for (const h of queue) {
|
|
6843
6749
|
const modelId = resolveVisionHelperModel(process.env) || 'deepseek-v4-flash-vision-exp';
|
|
6750
|
+
if (max > 0 && this.visionHandoffsThisTurn >= max) {
|
|
6751
|
+
h.tr.is_error = false;
|
|
6752
|
+
h.tr.content =
|
|
6753
|
+
`[ReadImage → vision budget] This turn has already used ${this.visionHandoffsThisTurn} vision reads (VISION_HANDOFF_MAX=${max}); this read was NOT sent. ` +
|
|
6754
|
+
'Consolidate: tile the regions you still need into ONE labeled montage and ask one focused question, or switch to a non-visual approach (parse the source data directly).';
|
|
6755
|
+
this.gateDeps().recordEvent('vision_handoff_capped', { max, used: this.visionHandoffsThisTurn, filePath: h.filePath }, 'ReadImage');
|
|
6756
|
+
continue;
|
|
6757
|
+
}
|
|
6844
6758
|
try {
|
|
6845
6759
|
const t0 = Date.now();
|
|
6846
6760
|
const text = await this.helperMiddleware.describeImage({
|
|
6847
6761
|
mediaType: h.payload.mediaType, base64: h.payload.base64, prompt: h.prompt, filePath: h.filePath, helperModelId: modelId,
|
|
6848
6762
|
});
|
|
6763
|
+
this.visionHandoffsThisTurn++;
|
|
6849
6764
|
h.tr.is_error = false;
|
|
6850
6765
|
h.tr.content =
|
|
6851
6766
|
`[ReadImage → vision helper ${modelId}${h.filePath ? ` on ${h.filePath}` : ''}]\n` +
|
|
@@ -7130,6 +7045,7 @@ export class CortexOrchestrator {
|
|
|
7130
7045
|
return { success: true, llmContent: hint, metadata: { source: 'mentor-consult', rung, helperModel: this.config.reactiveMentorship?.helperModelId } };
|
|
7131
7046
|
}
|
|
7132
7047
|
async processToolTraining(toolUse, result) {
|
|
7048
|
+
this.applySliceNudge(toolUse, result); // universal post-execution hook: every path funnels through processToolTraining
|
|
7133
7049
|
const store = this.getDecisionStore();
|
|
7134
7050
|
if (!store)
|
|
7135
7051
|
return result;
|