@nexus-cortex/core 4.69.1 → 4.70.0
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/dist/models/configurators/DeepSeekConfigurator.d.ts +3 -0
- package/dist/models/configurators/DeepSeekConfigurator.d.ts.map +1 -1
- package/dist/models/configurators/DeepSeekConfigurator.js +1 -0
- package/dist/models/configurators/DeepSeekConfigurator.js.map +1 -1
- package/dist/models/configurators/XAIConfigurator.d.ts +3 -0
- package/dist/models/configurators/XAIConfigurator.d.ts.map +1 -1
- package/dist/models/configurators/XAIConfigurator.js +1 -0
- package/dist/models/configurators/XAIConfigurator.js.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.d.ts +9 -0
- package/dist/orchestrator/CortexOrchestrator.d.ts.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.js +527 -15
- package/dist/orchestrator/CortexOrchestrator.js.map +1 -1
- package/dist/orchestrator/inactionGuard.d.ts +42 -0
- package/dist/orchestrator/inactionGuard.d.ts.map +1 -0
- package/dist/orchestrator/inactionGuard.js +47 -0
- package/dist/orchestrator/inactionGuard.js.map +1 -0
- package/dist/orchestrator/requirementsVerification.d.ts +43 -0
- package/dist/orchestrator/requirementsVerification.d.ts.map +1 -0
- package/dist/orchestrator/requirementsVerification.js +87 -0
- package/dist/orchestrator/requirementsVerification.js.map +1 -0
- package/dist/tools/ToolProfile.d.ts +9 -0
- package/dist/tools/ToolProfile.d.ts.map +1 -1
- package/dist/tools/ToolProfile.js +19 -0
- package/dist/tools/ToolProfile.js.map +1 -1
- package/dist/tools/registries/BaseToolRegistry.d.ts.map +1 -1
- package/dist/tools/registries/BaseToolRegistry.js +14 -0
- package/dist/tools/registries/BaseToolRegistry.js.map +1 -1
- package/dist/training/DecisionPriorInjector.d.ts +7 -0
- package/dist/training/DecisionPriorInjector.d.ts.map +1 -1
- package/dist/training/DecisionPriorInjector.js +22 -0
- package/dist/training/DecisionPriorInjector.js.map +1 -1
- package/dist/training/DecisionStore.d.ts +39 -0
- package/dist/training/DecisionStore.d.ts.map +1 -1
- package/dist/training/DecisionStore.js +94 -0
- package/dist/training/DecisionStore.js.map +1 -1
- package/dist/training/errorFamily.d.ts +15 -0
- package/dist/training/errorFamily.d.ts.map +1 -0
- package/dist/training/errorFamily.js +40 -0
- package/dist/training/errorFamily.js.map +1 -0
- package/dist/training/loopLadder.d.ts +35 -0
- package/dist/training/loopLadder.d.ts.map +1 -0
- package/dist/training/loopLadder.js +58 -0
- package/dist/training/loopLadder.js.map +1 -0
- package/dist/training/toolOutcome.d.ts +20 -0
- package/dist/training/toolOutcome.d.ts.map +1 -0
- package/dist/training/toolOutcome.js +100 -0
- package/dist/training/toolOutcome.js.map +1 -0
- package/package.json +3 -3
|
@@ -30,11 +30,16 @@ import { CacheMetricsAccumulator } from '../session/CacheMetricsAccumulator.js';
|
|
|
30
30
|
import { SubAgentProcessManager, createSubAgentProcessManager } from './SubAgentProcessManager.js';
|
|
31
31
|
// Phase 1: Tool Architecture Refactor - Unified Tool Registry
|
|
32
32
|
import { toolFactory } from '../tools/ToolFactory.js';
|
|
33
|
-
import { resolveToolProfile, resolveToolAnchor, isNarrowProfile, isToolAllowedByProfile, applyToolProfile } from '../tools/ToolProfile.js';
|
|
33
|
+
import { resolveToolProfile, resolveToolAnchor, resolveFrameProfile, isNarrowProfile, isToolAllowedByProfile, applyToolProfile } from '../tools/ToolProfile.js';
|
|
34
34
|
import { slashCommandRegistry } from '../commands/SlashCommandRegistry.js';
|
|
35
35
|
import { prefixMcpToolName, parseMcpToolName } from '../mcp/mcpToolNamespacing.js';
|
|
36
|
-
import { DecisionStore
|
|
37
|
-
import { formatPriorReminder } from '../training/DecisionPriorInjector.js';
|
|
36
|
+
import { DecisionStore } from '../training/DecisionStore.js';
|
|
37
|
+
import { formatPriorReminder, formatFamilyReminder } from '../training/DecisionPriorInjector.js';
|
|
38
|
+
import { classifyErrorFamily } from '../training/errorFamily.js';
|
|
39
|
+
import { classifyToolOutcome } from '../training/toolOutcome.js';
|
|
40
|
+
import { LoopLadder, formatLadderSignal } from '../training/loopLadder.js';
|
|
41
|
+
import { shouldNudgeInaction, formatInactionNudge } from './inactionGuard.js';
|
|
42
|
+
import { verifyRequirements, resolveEndTurnRequirementsMode } from './requirementsVerification.js';
|
|
38
43
|
import { ModelRouterMatrix } from '../training/ModelRouterMatrix.js';
|
|
39
44
|
import { classifyTask } from '../training/TaskClassifier.js';
|
|
40
45
|
import { closestToolMatches } from './toolNameMatcher.js';
|
|
@@ -143,6 +148,10 @@ export class CortexOrchestrator {
|
|
|
143
148
|
/** The active model card's anchorProfile (captured at request assembly) —
|
|
144
149
|
* env CORTEX_TOOL_ANCHOR still overrides inside resolveToolAnchor. */
|
|
145
150
|
cardAnchorProfile = null;
|
|
151
|
+
/** Card frameProfile ('lifted'|'persist') captured with the anchor — under
|
|
152
|
+
* 'persist' the anchor never lifts (backlog item 5; env
|
|
153
|
+
* CORTEX_TOOL_ANCHOR_PERSIST overrides in resolveFrameProfile). */
|
|
154
|
+
cardFrameProfile = null;
|
|
146
155
|
/** P6 deferral (CORTEX_PROMPT_MASS=defer): the static corpus is delivered
|
|
147
156
|
* exactly once, at the anchor-lift boundary. One-shot per orchestrator. */
|
|
148
157
|
deferredCorpusDelivered = false;
|
|
@@ -699,6 +708,7 @@ export class CortexOrchestrator {
|
|
|
699
708
|
// Card-level home door: capture the active card's anchorProfile (env
|
|
700
709
|
// CORTEX_TOOL_ANCHOR overrides inside the resolver; env 'none' disables).
|
|
701
710
|
this.cardAnchorProfile = effectiveModel.anchorProfile ?? null;
|
|
711
|
+
this.cardFrameProfile = effectiveModel.frameProfile ?? null;
|
|
702
712
|
// Thread the CARD's anchor to the executors (per-orchestrator config, by
|
|
703
713
|
// reference) so framing-aware executor behavior (ShellTool steering
|
|
704
714
|
// default) is model-card/registry-scoped, not process-global env.
|
|
@@ -978,6 +988,9 @@ export class CortexOrchestrator {
|
|
|
978
988
|
// Round 18b: track whether we've already retried on empty response so
|
|
979
989
|
// we don't loop forever. See empty-detection block below.
|
|
980
990
|
let emptyResponseRetryUsed = false;
|
|
991
|
+
// Inaction guard (backlog item 2 — the ladder's inverse): single-nudge
|
|
992
|
+
// bound for the actless-verbose retry. Default OFF via env.
|
|
993
|
+
let inactionNudgeUsed = false;
|
|
981
994
|
// EndTurn gate (Stage 1): the turn must not complete until the model
|
|
982
995
|
// calls the EndTurn attestation tool. Steering is ignored by some
|
|
983
996
|
// models (grok-4.3); a required tool call is not. Bounded so a model
|
|
@@ -1021,6 +1034,13 @@ export class CortexOrchestrator {
|
|
|
1021
1034
|
const thisTurnToolOutputs = [];
|
|
1022
1035
|
// Loop detection: Track recent tool calls to detect repetitive loops
|
|
1023
1036
|
const recentToolCalls = [];
|
|
1037
|
+
// Unified Outcome Ladder (docs/UNIFIED_OUTCOME_LADDER.md): per-approach
|
|
1038
|
+
// FAILURE escalation — remind(2)→diversify(4)→break(6) over normalized
|
|
1039
|
+
// near-duplicate approaches. Complements isToolProgressStalled (which is
|
|
1040
|
+
// outcome-blind exact-hash cycling) by firing earlier on failing thrash.
|
|
1041
|
+
const loopLadder = new LoopLadder();
|
|
1042
|
+
let ladderBreak = null;
|
|
1043
|
+
let ladderSignal = null;
|
|
1024
1044
|
// Track all tool uses across iterations for response
|
|
1025
1045
|
const allExecutedToolUses = [];
|
|
1026
1046
|
// Track tool call counts for diversity warning
|
|
@@ -1198,6 +1218,130 @@ export class CortexOrchestrator {
|
|
|
1198
1218
|
continue;
|
|
1199
1219
|
}
|
|
1200
1220
|
}
|
|
1221
|
+
// Inaction guard (backlog item 2 — the loop ladder's inverse).
|
|
1222
|
+
// TB2 2×2: persist-frame arms produced never-acted failures (5
|
|
1223
|
+
// pro-persist) — a huge reason-to-the-wall text dump with ZERO tool
|
|
1224
|
+
// calls, invisible to every call-counting guard. One bounded
|
|
1225
|
+
// steering retry, R18b-style; default OFF (CORTEX_INACTION_NUDGE).
|
|
1226
|
+
{
|
|
1227
|
+
const responseChars = Array.isArray(currentAssistantCanonicalMessage.content)
|
|
1228
|
+
? currentAssistantCanonicalMessage.content
|
|
1229
|
+
.map((b) => b?.type === 'text' ? (b.text || '') : b?.type === 'thinking' ? (b.thinking || '') : '')
|
|
1230
|
+
.join('').length
|
|
1231
|
+
: String(currentAssistantCanonicalMessage.content ?? '').length;
|
|
1232
|
+
if (hasVisibleText &&
|
|
1233
|
+
!structuredOutputState?.result &&
|
|
1234
|
+
shouldNudgeInaction({
|
|
1235
|
+
responseChars,
|
|
1236
|
+
toolUseBlocksThisResponse: 0,
|
|
1237
|
+
executedToolCallsThisTurn: allExecutedToolUses.length,
|
|
1238
|
+
toolsOffered: toolsToUse.length,
|
|
1239
|
+
turnNumber: this.turnNumber,
|
|
1240
|
+
alreadyNudged: inactionNudgeUsed,
|
|
1241
|
+
})) {
|
|
1242
|
+
inactionNudgeUsed = true;
|
|
1243
|
+
console.warn(`[Orchestrator] Inaction guard: actless-verbose response (${responseChars} chars, 0 tool calls). ` +
|
|
1244
|
+
`Injecting act-first nudge (single retry).`);
|
|
1245
|
+
// Observability (backlog item 3): bank the event either way it
|
|
1246
|
+
// resolves — the distiller counts nudge→acted conversions.
|
|
1247
|
+
{
|
|
1248
|
+
const store = this.getDecisionStore();
|
|
1249
|
+
if (store) {
|
|
1250
|
+
void store
|
|
1251
|
+
.recordEvent({
|
|
1252
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
1253
|
+
kind: 'inaction_nudge',
|
|
1254
|
+
detail: { responseChars, turnNumber: this.turnNumber },
|
|
1255
|
+
})
|
|
1256
|
+
.catch(() => { });
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
const inactionUserMessage = {
|
|
1260
|
+
uuid: uuidv4(),
|
|
1261
|
+
timestamp: new Date().toISOString(),
|
|
1262
|
+
type: 'user',
|
|
1263
|
+
message: {
|
|
1264
|
+
role: 'user',
|
|
1265
|
+
content: [{
|
|
1266
|
+
type: 'text',
|
|
1267
|
+
text: `<system-reminder>${formatInactionNudge()}</system-reminder>`,
|
|
1268
|
+
}],
|
|
1269
|
+
},
|
|
1270
|
+
timeline: {
|
|
1271
|
+
sessionId: this.currentSessionId,
|
|
1272
|
+
conversationId: this.currentConversationId,
|
|
1273
|
+
turnNumber: this.turnNumber + 1,
|
|
1274
|
+
},
|
|
1275
|
+
model: {
|
|
1276
|
+
id: effectiveModel.id,
|
|
1277
|
+
provider: effectiveModel.provider,
|
|
1278
|
+
apiPattern: effectiveModel.api.pattern,
|
|
1279
|
+
},
|
|
1280
|
+
};
|
|
1281
|
+
this.messageHistory.push(inactionUserMessage);
|
|
1282
|
+
await this.historyStore.appendMessage(this.currentSessionId, inactionUserMessage);
|
|
1283
|
+
await this.ensureHistoryFitsModel(effectiveModel);
|
|
1284
|
+
const inactionCanonicalHistory = this.convertToCanonicalMessages([...this.messageHistory]);
|
|
1285
|
+
const inactionRequest = this.gatewayTranslation.prepareRequest(inactionCanonicalHistory, toolsToUse, effectiveModel, {
|
|
1286
|
+
temperature: options.parameters?.temperature,
|
|
1287
|
+
maxTokens: options.parameters?.maxTokens,
|
|
1288
|
+
topP: options.parameters?.topP,
|
|
1289
|
+
reasoningEffort: options.parameters?.reasoningEffort,
|
|
1290
|
+
stream: options.streaming,
|
|
1291
|
+
staticSystemPrompt: this.currentStaticSystemPrompt, // R28
|
|
1292
|
+
conversationId: this.currentConversationId, // R28b
|
|
1293
|
+
});
|
|
1294
|
+
if (this.lastResponseId && effectiveModel.api.pattern === 'responses') {
|
|
1295
|
+
inactionRequest.previousResponseId = this.lastResponseId;
|
|
1296
|
+
}
|
|
1297
|
+
inactionRequest.conversationId = this.currentSessionId;
|
|
1298
|
+
let inactionApiResponse;
|
|
1299
|
+
if (this.retryMiddleware) {
|
|
1300
|
+
const r = await this.retryMiddleware.executeWithRetry(() => this.apiClient.sendRequest(inactionRequest, effectiveModel), 'inaction_nudge_retry');
|
|
1301
|
+
inactionApiResponse = r.result;
|
|
1302
|
+
}
|
|
1303
|
+
else {
|
|
1304
|
+
inactionApiResponse = await this.apiClient.sendRequest(inactionRequest, effectiveModel);
|
|
1305
|
+
}
|
|
1306
|
+
const inactionConverted = this.gatewayTranslation.convertResponse(inactionApiResponse.data, effectiveModel, {
|
|
1307
|
+
sessionId: this.currentSessionId,
|
|
1308
|
+
conversationId: this.currentConversationId,
|
|
1309
|
+
turnNumber: this.turnNumber + 1,
|
|
1310
|
+
});
|
|
1311
|
+
if (effectiveModel.api.pattern === 'responses' && inactionApiResponse.data?.id) {
|
|
1312
|
+
this.lastResponseId = inactionApiResponse.data.id;
|
|
1313
|
+
this.lastResponseIdProvider = effectiveModel.provider; // R20a
|
|
1314
|
+
}
|
|
1315
|
+
if (inactionConverted.messages && inactionConverted.messages.length > 0) {
|
|
1316
|
+
const inactionAssistantCanonical = inactionConverted.messages[0];
|
|
1317
|
+
const inactionAssistantMessage = {
|
|
1318
|
+
uuid: inactionAssistantCanonical.uuid,
|
|
1319
|
+
timestamp: inactionAssistantCanonical.timestamp,
|
|
1320
|
+
type: 'assistant',
|
|
1321
|
+
message: {
|
|
1322
|
+
role: 'assistant',
|
|
1323
|
+
content: inactionAssistantCanonical.content,
|
|
1324
|
+
},
|
|
1325
|
+
timeline: {
|
|
1326
|
+
sessionId: this.currentSessionId,
|
|
1327
|
+
conversationId: this.currentConversationId,
|
|
1328
|
+
turnNumber: this.turnNumber + 1,
|
|
1329
|
+
},
|
|
1330
|
+
model: {
|
|
1331
|
+
id: effectiveModel.id,
|
|
1332
|
+
provider: effectiveModel.provider,
|
|
1333
|
+
apiPattern: effectiveModel.api.pattern,
|
|
1334
|
+
},
|
|
1335
|
+
...(inactionConverted.usage && { usage: inactionConverted.usage }),
|
|
1336
|
+
};
|
|
1337
|
+
this.messageHistory.push(inactionAssistantMessage);
|
|
1338
|
+
await this.historyStore.appendMessage(this.currentSessionId, inactionAssistantMessage);
|
|
1339
|
+
currentAssistantMessage = inactionAssistantMessage;
|
|
1340
|
+
currentAssistantCanonicalMessage = inactionAssistantCanonical;
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1201
1345
|
// EndTurn gate (Stage 1): refuse to finalize until the model has
|
|
1202
1346
|
// called the EndTurn attestation tool. Only arms when the turn
|
|
1203
1347
|
// actually used a (non-EndTurn) tool — pure-language turns bypass.
|
|
@@ -1221,8 +1365,27 @@ export class CortexOrchestrator {
|
|
|
1221
1365
|
if (!cv.ok)
|
|
1222
1366
|
stage3Violations = cv.violations;
|
|
1223
1367
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1368
|
+
// Fallback observability (backlog item 1.4): when the nudge bound
|
|
1369
|
+
// is exhausted and the gate is STILL unsatisfied, the turn ships
|
|
1370
|
+
// anyway (liveness beats purity) — but that fallback-accept was
|
|
1371
|
+
// previously silent. Bank an event so the distiller can see
|
|
1372
|
+
// un-attested passes.
|
|
1373
|
+
const endTurnGateUnsatisfied = endTurnGateEnabled && turnUsedTools && (!endTurnCalled || stage3Violations.length > 0);
|
|
1374
|
+
if (endTurnGateUnsatisfied && endTurnNudges >= END_TURN_MAX_NUDGES) {
|
|
1375
|
+
const fallbackReason = !endTurnCalled ? 'missing-EndTurn' : 'coordinate-violation';
|
|
1376
|
+
console.warn(`[Orchestrator] EndTurn gate FALLBACK-ACCEPT: ${fallbackReason} after ${endTurnNudges} nudges — turn ships un-attested.`);
|
|
1377
|
+
const store = this.getDecisionStore();
|
|
1378
|
+
if (store) {
|
|
1379
|
+
void store
|
|
1380
|
+
.recordEvent({
|
|
1381
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
1382
|
+
kind: 'endturn_gate_fallback',
|
|
1383
|
+
detail: { reason: fallbackReason, nudges: endTurnNudges, iteration: toolCallIteration },
|
|
1384
|
+
})
|
|
1385
|
+
.catch(() => { });
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
if (endTurnGateUnsatisfied && endTurnNudges < END_TURN_MAX_NUDGES) {
|
|
1226
1389
|
endTurnNudges++;
|
|
1227
1390
|
const gateReason = !endTurnCalled ? 'missing-EndTurn' : 'coordinate-violation';
|
|
1228
1391
|
console.warn(`[Orchestrator] EndTurn gate: ${gateReason} ` +
|
|
@@ -1498,6 +1661,49 @@ export class CortexOrchestrator {
|
|
|
1498
1661
|
try {
|
|
1499
1662
|
const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState);
|
|
1500
1663
|
clearTimeout(timeoutId);
|
|
1664
|
+
// Unified Outcome Ladder: observe each result's TRUE outcome (exit
|
|
1665
|
+
// codes, not wire is_error) and escalate on repeated failing
|
|
1666
|
+
// approaches. The strongest signal this batch is injected at the
|
|
1667
|
+
// signals block below; a 'break' rung ends the loop R29b-style.
|
|
1668
|
+
ladderSignal = null;
|
|
1669
|
+
{
|
|
1670
|
+
const inputById = new Map(toolUseBlocks.map((b) => [b.id, b.input]));
|
|
1671
|
+
for (const tr of toolResults) {
|
|
1672
|
+
if (tr.tool_name === 'EndTurn')
|
|
1673
|
+
continue;
|
|
1674
|
+
const input = inputById.get(tr.tool_use_id);
|
|
1675
|
+
if (input === undefined)
|
|
1676
|
+
continue;
|
|
1677
|
+
const outcome = classifyToolOutcome(tr.tool_name, input, tr);
|
|
1678
|
+
const ladder = loopLadder.observe(tr.tool_name, outcome);
|
|
1679
|
+
const sig = formatLadderSignal(tr.tool_name, ladder);
|
|
1680
|
+
if (sig)
|
|
1681
|
+
ladderSignal = sig;
|
|
1682
|
+
if (ladder.action === 'break')
|
|
1683
|
+
ladderBreak = sig;
|
|
1684
|
+
// Observability (backlog item 3): escalations are invisible in
|
|
1685
|
+
// the session record (signals inject post-persist) — bank each
|
|
1686
|
+
// rung as a decision-store event for the distiller.
|
|
1687
|
+
if (ladder.action !== 'none') {
|
|
1688
|
+
const store = this.getDecisionStore();
|
|
1689
|
+
if (store) {
|
|
1690
|
+
void store
|
|
1691
|
+
.recordEvent({
|
|
1692
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
1693
|
+
kind: 'loop_escalation',
|
|
1694
|
+
toolName: tr.tool_name,
|
|
1695
|
+
detail: {
|
|
1696
|
+
rung: ladder.action,
|
|
1697
|
+
count: ladder.count,
|
|
1698
|
+
approachHash: outcome.approachHash,
|
|
1699
|
+
...(outcome.family ? { family: outcome.family } : {}),
|
|
1700
|
+
},
|
|
1701
|
+
})
|
|
1702
|
+
.catch(() => { });
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1501
1707
|
// File-history snapshots (Claude Code parity — previously built but
|
|
1502
1708
|
// UNWIRED: FileCheckpointManager existed with zero markModified
|
|
1503
1709
|
// callers and its snapshot message was discarded). Mark files
|
|
@@ -1578,7 +1784,31 @@ export class CortexOrchestrator {
|
|
|
1578
1784
|
lastEndTurnCitations = Array.isArray(cits) ? cits : undefined; // Stage 3 baseline
|
|
1579
1785
|
const verdict = verifyCitationsGrounded(cits, thisTurnToolOutputs.join('\n'));
|
|
1580
1786
|
if (verdict.grounded) {
|
|
1581
|
-
|
|
1787
|
+
// Stage 4 (backlog item 1, CORTEX_ENDTURN_REQUIREMENTS): verify
|
|
1788
|
+
// the requirements attestation — the wrong-artifact weapon.
|
|
1789
|
+
// Stage 2 verifies citations; nothing before this forced
|
|
1790
|
+
// re-reading the TASK. Rides the same nudge budget: a Stage-4
|
|
1791
|
+
// reject leaves the gate unsatisfied so the bounded
|
|
1792
|
+
// fallback-accept still guarantees liveness.
|
|
1793
|
+
if (resolveEndTurnRequirementsMode()) {
|
|
1794
|
+
const s4 = verifyRequirements({
|
|
1795
|
+
requirements: etUse?.input?.requirements,
|
|
1796
|
+
verification: etUse?.input?.verification,
|
|
1797
|
+
userTaskText: this.lastRealUserText(),
|
|
1798
|
+
turnUsedMutatingTool,
|
|
1799
|
+
});
|
|
1800
|
+
if (s4.ok) {
|
|
1801
|
+
endTurnCalled = true;
|
|
1802
|
+
}
|
|
1803
|
+
else {
|
|
1804
|
+
tr.is_error = true;
|
|
1805
|
+
tr.content = s4.nudge;
|
|
1806
|
+
console.warn('[Orchestrator] Stage4: EndTurn rejected — requirements attestation unsatisfied.');
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
else {
|
|
1810
|
+
endTurnCalled = true;
|
|
1811
|
+
}
|
|
1582
1812
|
}
|
|
1583
1813
|
else {
|
|
1584
1814
|
const bad = verdict.ungrounded
|
|
@@ -1726,14 +1956,43 @@ export class CortexOrchestrator {
|
|
|
1726
1956
|
const progressStalled = isToolProgressStalled(recentToolCalls);
|
|
1727
1957
|
const budgetSignal = computeToolBudgetSignal(effectiveToolBudgetCount, TOOL_BUDGET_SOFT, progressStalled);
|
|
1728
1958
|
const diversityWarning = this.getDiversityWarning(toolCallCounts);
|
|
1729
|
-
if (budgetSignal || diversityWarning) {
|
|
1730
|
-
const signals = [budgetSignal, diversityWarning].filter(Boolean).join('\n');
|
|
1959
|
+
if (budgetSignal || diversityWarning || ladderSignal) {
|
|
1960
|
+
const signals = [budgetSignal, diversityWarning, ladderSignal].filter(Boolean).join('\n');
|
|
1731
1961
|
const lastMsg = this.messageHistory[this.messageHistory.length - 1];
|
|
1732
1962
|
if (lastMsg?.message?.content?.[0]?.type === 'tool_result') {
|
|
1733
1963
|
const block = lastMsg.message.content[0];
|
|
1734
1964
|
const existing = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
1735
1965
|
block.content = existing + '\n\n' + signals;
|
|
1966
|
+
// Observability (backlog item 3, micro-suite defect #4): this
|
|
1967
|
+
// mutation happens AFTER the tool_result was persisted — the
|
|
1968
|
+
// durable session lacks the steering the model saw. Bank a
|
|
1969
|
+
// steering_injected event so harvest/distill sees it. Session rows
|
|
1970
|
+
// stay append-only (never rewritten).
|
|
1971
|
+
const store = this.getDecisionStore();
|
|
1972
|
+
if (store) {
|
|
1973
|
+
const kinds = [
|
|
1974
|
+
budgetSignal ? 'budget' : null,
|
|
1975
|
+
diversityWarning ? 'diversity' : null,
|
|
1976
|
+
ladderSignal ? 'ladder' : null,
|
|
1977
|
+
].filter(Boolean);
|
|
1978
|
+
void store
|
|
1979
|
+
.recordEvent({
|
|
1980
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
1981
|
+
kind: 'steering_injected',
|
|
1982
|
+
detail: { kinds, iteration: toolCallIteration, chars: signals.length },
|
|
1983
|
+
})
|
|
1984
|
+
.catch(() => { });
|
|
1985
|
+
}
|
|
1736
1986
|
}
|
|
1987
|
+
ladderSignal = null; // consumed — never re-inject on later iterations
|
|
1988
|
+
}
|
|
1989
|
+
// Ladder break (teach-then-break): the break instruction is already in
|
|
1990
|
+
// the tool result above; exit the loop R29b-style so the post-loop
|
|
1991
|
+
// synthesis net produces an honest final answer — never the blunt
|
|
1992
|
+
// toolCallIteration=MAX turn-kill of the exact-match detector.
|
|
1993
|
+
if (ladderBreak) {
|
|
1994
|
+
console.warn('[Orchestrator] LoopLadder break: repeated failing approach — forcing synthesis.');
|
|
1995
|
+
break;
|
|
1737
1996
|
}
|
|
1738
1997
|
// R29b hard cap: a weaker model may ignore the firm budget reminders
|
|
1739
1998
|
// and keep spinning (benchmark: deepseek 46 successful local calls,
|
|
@@ -1782,7 +2041,11 @@ export class CortexOrchestrator {
|
|
|
1782
2041
|
// full profile applies from here. Rebuild from allTools because the
|
|
1783
2042
|
// armed anchor narrowed toolsToUse at assembly. (When deferred loading
|
|
1784
2043
|
// is on, the re-filter block below performs the rebuild.)
|
|
1785
|
-
if (!this.anchorLifted &&
|
|
2044
|
+
if (!this.anchorLifted &&
|
|
2045
|
+
resolveToolAnchor(process.env, this.cardAnchorProfile) &&
|
|
2046
|
+
// Persist frame (backlog item 5): the anchored surface IS the session
|
|
2047
|
+
// surface — never lift, never deliver the deferred corpus.
|
|
2048
|
+
resolveFrameProfile(process.env, this.cardFrameProfile) !== 'persist') {
|
|
1786
2049
|
this.anchorLifted = true;
|
|
1787
2050
|
if (!(this.config.enableDeferredToolLoading && !isPTCEnabled)) {
|
|
1788
2051
|
toolsToUse = allTools;
|
|
@@ -2564,6 +2827,7 @@ export class CortexOrchestrator {
|
|
|
2564
2827
|
// Card-level home door: capture the active card's anchorProfile (env
|
|
2565
2828
|
// CORTEX_TOOL_ANCHOR overrides inside the resolver; env 'none' disables).
|
|
2566
2829
|
this.cardAnchorProfile = effectiveModel.anchorProfile ?? null;
|
|
2830
|
+
this.cardFrameProfile = effectiveModel.frameProfile ?? null;
|
|
2567
2831
|
// Thread the CARD's anchor to the executors (per-orchestrator config, by
|
|
2568
2832
|
// reference) so framing-aware executor behavior (ShellTool steering
|
|
2569
2833
|
// default) is model-card/registry-scoped, not process-global env.
|
|
@@ -2766,7 +3030,12 @@ export class CortexOrchestrator {
|
|
|
2766
3030
|
let toolCallIteration = 0;
|
|
2767
3031
|
let totalToolErrors = 0;
|
|
2768
3032
|
let emptyResponseRetryUsed = false; // R32: parity with sendMessage R18b guard
|
|
3033
|
+
let inactionNudgeUsed = false; // Inaction guard (backlog item 2) — streaming parity
|
|
2769
3034
|
const allToolCalls = [];
|
|
3035
|
+
// Unified Outcome Ladder (streaming parity — see sendMessage).
|
|
3036
|
+
const loopLadder = new LoopLadder();
|
|
3037
|
+
let ladderBreak = null;
|
|
3038
|
+
let ladderSignal = null;
|
|
2770
3039
|
const allExecutedToolUses = [];
|
|
2771
3040
|
const toolCallCounts = new Map();
|
|
2772
3041
|
// DEBUG: Log canonical message content structure for tool extraction (streaming path)
|
|
@@ -2933,6 +3202,136 @@ export class CortexOrchestrator {
|
|
|
2933
3202
|
console.warn(`[Orchestrator Streaming] R32 retry failed:`, retryErr);
|
|
2934
3203
|
}
|
|
2935
3204
|
}
|
|
3205
|
+
// Inaction guard (backlog item 2 — streaming parity with sendMessage).
|
|
3206
|
+
// Actless-verbose first-turn response in a tool-capable request: one
|
|
3207
|
+
// bounded act-first steering retry. Default OFF (CORTEX_INACTION_NUDGE).
|
|
3208
|
+
{
|
|
3209
|
+
const responseChars = Array.isArray(currentAssistantCanonicalMessage.content)
|
|
3210
|
+
? currentAssistantCanonicalMessage.content
|
|
3211
|
+
.map((b) => b?.type === 'text' ? (b.text || '') : b?.type === 'thinking' ? (b.thinking || '') : '')
|
|
3212
|
+
.join('').length
|
|
3213
|
+
: String(currentAssistantCanonicalMessage.content ?? '').length;
|
|
3214
|
+
if (hasVisibleText &&
|
|
3215
|
+
shouldNudgeInaction({
|
|
3216
|
+
responseChars,
|
|
3217
|
+
toolUseBlocksThisResponse: 0,
|
|
3218
|
+
executedToolCallsThisTurn: allExecutedToolUses.length,
|
|
3219
|
+
toolsOffered: toolsToUse.length,
|
|
3220
|
+
turnNumber: this.turnNumber,
|
|
3221
|
+
alreadyNudged: inactionNudgeUsed,
|
|
3222
|
+
})) {
|
|
3223
|
+
inactionNudgeUsed = true;
|
|
3224
|
+
console.warn(`[Orchestrator Streaming] Inaction guard: actless-verbose response (${responseChars} chars, 0 tool calls). ` +
|
|
3225
|
+
`Injecting act-first nudge (single retry).`);
|
|
3226
|
+
{
|
|
3227
|
+
const store = this.getDecisionStore();
|
|
3228
|
+
if (store) {
|
|
3229
|
+
void store
|
|
3230
|
+
.recordEvent({
|
|
3231
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
3232
|
+
kind: 'inaction_nudge',
|
|
3233
|
+
detail: { responseChars, turnNumber: this.turnNumber, streaming: true },
|
|
3234
|
+
})
|
|
3235
|
+
.catch(() => { });
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
const inactionUserMessage = {
|
|
3239
|
+
uuid: uuidv4(),
|
|
3240
|
+
timestamp: new Date().toISOString(),
|
|
3241
|
+
type: 'user',
|
|
3242
|
+
message: {
|
|
3243
|
+
role: 'user',
|
|
3244
|
+
content: [{
|
|
3245
|
+
type: 'text',
|
|
3246
|
+
text: `<system-reminder>${formatInactionNudge()}</system-reminder>`,
|
|
3247
|
+
}],
|
|
3248
|
+
},
|
|
3249
|
+
timeline: {
|
|
3250
|
+
sessionId: this.currentSessionId,
|
|
3251
|
+
conversationId: this.currentConversationId,
|
|
3252
|
+
turnNumber: this.turnNumber + 1,
|
|
3253
|
+
},
|
|
3254
|
+
model: {
|
|
3255
|
+
id: effectiveModel.id,
|
|
3256
|
+
provider: effectiveModel.provider,
|
|
3257
|
+
apiPattern: effectiveModel.api.pattern,
|
|
3258
|
+
},
|
|
3259
|
+
};
|
|
3260
|
+
this.messageHistory.push(inactionUserMessage);
|
|
3261
|
+
await this.historyStore.appendMessage(this.currentSessionId, inactionUserMessage);
|
|
3262
|
+
try {
|
|
3263
|
+
await this.ensureHistoryFitsModel(effectiveModel);
|
|
3264
|
+
const retryCanonicalHistory = this.convertToCanonicalMessages([...this.messageHistory]);
|
|
3265
|
+
const retryRequest = this.gatewayTranslation.prepareRequest(retryCanonicalHistory, toolsToUse, effectiveModel, {
|
|
3266
|
+
temperature: options.parameters?.temperature,
|
|
3267
|
+
maxTokens: options.parameters?.maxTokens,
|
|
3268
|
+
topP: options.parameters?.topP,
|
|
3269
|
+
reasoningEffort: options.parameters?.reasoningEffort,
|
|
3270
|
+
stream: false,
|
|
3271
|
+
staticSystemPrompt: this.currentStaticSystemPrompt,
|
|
3272
|
+
conversationId: this.currentConversationId,
|
|
3273
|
+
});
|
|
3274
|
+
if (this.lastResponseId && effectiveModel.api.pattern === 'responses') {
|
|
3275
|
+
retryRequest.previousResponseId = this.lastResponseId;
|
|
3276
|
+
}
|
|
3277
|
+
retryRequest.conversationId = this.currentSessionId;
|
|
3278
|
+
let retryApiResponse;
|
|
3279
|
+
if (this.retryMiddleware) {
|
|
3280
|
+
const retryResult = await this.retryMiddleware.executeWithRetry(() => this.apiClient.sendRequest(retryRequest, effectiveModel), 'inaction_nudge_retry');
|
|
3281
|
+
retryApiResponse = retryResult.result;
|
|
3282
|
+
}
|
|
3283
|
+
else {
|
|
3284
|
+
retryApiResponse = await this.apiClient.sendRequest(retryRequest, effectiveModel);
|
|
3285
|
+
}
|
|
3286
|
+
const retryConverted = this.gatewayTranslation.convertResponse(retryApiResponse.data, effectiveModel, {
|
|
3287
|
+
sessionId: this.currentSessionId,
|
|
3288
|
+
conversationId: this.currentConversationId,
|
|
3289
|
+
turnNumber: this.turnNumber + 1,
|
|
3290
|
+
});
|
|
3291
|
+
if (effectiveModel.api.pattern === 'responses' && retryApiResponse.data?.id) {
|
|
3292
|
+
this.lastResponseId = retryApiResponse.data.id;
|
|
3293
|
+
this.lastResponseIdProvider = effectiveModel.provider;
|
|
3294
|
+
}
|
|
3295
|
+
if (retryConverted.messages && retryConverted.messages.length > 0) {
|
|
3296
|
+
const retryAssistantCanonical = retryConverted.messages[0];
|
|
3297
|
+
const retryAssistantMessage = {
|
|
3298
|
+
uuid: retryAssistantCanonical.uuid,
|
|
3299
|
+
timestamp: retryAssistantCanonical.timestamp,
|
|
3300
|
+
type: 'assistant',
|
|
3301
|
+
message: {
|
|
3302
|
+
role: 'assistant',
|
|
3303
|
+
content: retryAssistantCanonical.content,
|
|
3304
|
+
},
|
|
3305
|
+
timeline: {
|
|
3306
|
+
sessionId: this.currentSessionId,
|
|
3307
|
+
conversationId: this.currentConversationId,
|
|
3308
|
+
turnNumber: this.turnNumber + 1,
|
|
3309
|
+
},
|
|
3310
|
+
model: {
|
|
3311
|
+
id: effectiveModel.id,
|
|
3312
|
+
provider: effectiveModel.provider,
|
|
3313
|
+
apiPattern: effectiveModel.api.pattern,
|
|
3314
|
+
},
|
|
3315
|
+
...(retryConverted.usage && { usage: retryConverted.usage }),
|
|
3316
|
+
};
|
|
3317
|
+
this.messageHistory.push(retryAssistantMessage);
|
|
3318
|
+
await this.historyStore.appendMessage(this.currentSessionId, retryAssistantMessage);
|
|
3319
|
+
currentAssistantCanonicalMessage = retryAssistantCanonical;
|
|
3320
|
+
const retryText = retryAssistantCanonical.content
|
|
3321
|
+
.filter((b) => b && b.type === 'text' && typeof b.text === 'string')
|
|
3322
|
+
.map((b) => b.text)
|
|
3323
|
+
.join('');
|
|
3324
|
+
if (retryText.trim().length > 0) {
|
|
3325
|
+
yield { type: 'text_delta', delta: retryText };
|
|
3326
|
+
}
|
|
3327
|
+
continue; // re-enter loop — retry may have produced tool_use
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
catch (retryErr) {
|
|
3331
|
+
console.warn(`[Orchestrator Streaming] Inaction-nudge retry failed:`, retryErr);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
2936
3335
|
hasToolUse = false;
|
|
2937
3336
|
break;
|
|
2938
3337
|
}
|
|
@@ -2972,6 +3371,45 @@ export class CortexOrchestrator {
|
|
|
2972
3371
|
// Execute tools (reuse existing method)
|
|
2973
3372
|
const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState);
|
|
2974
3373
|
clearTimeout(timeoutId);
|
|
3374
|
+
// Unified Outcome Ladder (streaming parity with sendMessage).
|
|
3375
|
+
ladderSignal = null;
|
|
3376
|
+
{
|
|
3377
|
+
const inputById = new Map(toolUseBlocks.map((b) => [b.id, b.input]));
|
|
3378
|
+
for (const tr of toolResults) {
|
|
3379
|
+
if (tr.tool_name === 'EndTurn')
|
|
3380
|
+
continue;
|
|
3381
|
+
const input = inputById.get(tr.tool_use_id);
|
|
3382
|
+
if (input === undefined)
|
|
3383
|
+
continue;
|
|
3384
|
+
const outcome = classifyToolOutcome(tr.tool_name, input, tr);
|
|
3385
|
+
const ladder = loopLadder.observe(tr.tool_name, outcome);
|
|
3386
|
+
const sig = formatLadderSignal(tr.tool_name, ladder);
|
|
3387
|
+
if (sig)
|
|
3388
|
+
ladderSignal = sig;
|
|
3389
|
+
if (ladder.action === 'break')
|
|
3390
|
+
ladderBreak = sig;
|
|
3391
|
+
// Observability (backlog item 3): streaming parity — bank each
|
|
3392
|
+
// escalation rung as a decision-store event.
|
|
3393
|
+
if (ladder.action !== 'none') {
|
|
3394
|
+
const store = this.getDecisionStore();
|
|
3395
|
+
if (store) {
|
|
3396
|
+
void store
|
|
3397
|
+
.recordEvent({
|
|
3398
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
3399
|
+
kind: 'loop_escalation',
|
|
3400
|
+
toolName: tr.tool_name,
|
|
3401
|
+
detail: {
|
|
3402
|
+
rung: ladder.action,
|
|
3403
|
+
count: ladder.count,
|
|
3404
|
+
approachHash: outcome.approachHash,
|
|
3405
|
+
...(outcome.family ? { family: outcome.family } : {}),
|
|
3406
|
+
},
|
|
3407
|
+
})
|
|
3408
|
+
.catch(() => { });
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
2975
3413
|
// R21 (2026-05-15): same fix as non-streaming path — count consecutive
|
|
2976
3414
|
// ITERATIONS WITH ZERO SUCCESSFUL TOOLS, not cumulative individual
|
|
2977
3415
|
// failures. Reset whenever at least one tool succeeded.
|
|
@@ -3123,14 +3561,38 @@ export class CortexOrchestrator {
|
|
|
3123
3561
|
const progressStalled = isToolProgressStalled(allToolCalls);
|
|
3124
3562
|
const budgetSignal = computeToolBudgetSignal(effectiveToolBudgetCount, TOOL_BUDGET_SOFT, progressStalled);
|
|
3125
3563
|
const diversityWarning = this.getDiversityWarning(toolCallCounts);
|
|
3126
|
-
if (budgetSignal || diversityWarning) {
|
|
3127
|
-
const signals = [budgetSignal, diversityWarning].filter(Boolean).join('\n');
|
|
3564
|
+
if (budgetSignal || diversityWarning || ladderSignal) {
|
|
3565
|
+
const signals = [budgetSignal, diversityWarning, ladderSignal].filter(Boolean).join('\n');
|
|
3128
3566
|
const lastMsg = this.messageHistory[this.messageHistory.length - 1];
|
|
3129
3567
|
if (lastMsg?.message?.content?.[0]?.type === 'tool_result') {
|
|
3130
3568
|
const block = lastMsg.message.content[0];
|
|
3131
3569
|
const existing = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
3132
3570
|
block.content = existing + '\n\n' + signals;
|
|
3571
|
+
// Observability (backlog item 3): post-persist mutation — bank a
|
|
3572
|
+
// steering_injected event (streaming parity).
|
|
3573
|
+
const store = this.getDecisionStore();
|
|
3574
|
+
if (store) {
|
|
3575
|
+
const kinds = [
|
|
3576
|
+
budgetSignal ? 'budget' : null,
|
|
3577
|
+
diversityWarning ? 'diversity' : null,
|
|
3578
|
+
ladderSignal ? 'ladder' : null,
|
|
3579
|
+
].filter(Boolean);
|
|
3580
|
+
void store
|
|
3581
|
+
.recordEvent({
|
|
3582
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
3583
|
+
kind: 'steering_injected',
|
|
3584
|
+
detail: { kinds, iteration: toolCallIteration, chars: signals.length, streaming: true },
|
|
3585
|
+
})
|
|
3586
|
+
.catch(() => { });
|
|
3587
|
+
}
|
|
3133
3588
|
}
|
|
3589
|
+
ladderSignal = null; // consumed
|
|
3590
|
+
}
|
|
3591
|
+
// Ladder break (streaming parity): instruction already injected above;
|
|
3592
|
+
// exit R29b-style so the streaming synthesis net closes the turn.
|
|
3593
|
+
if (ladderBreak) {
|
|
3594
|
+
console.warn('[Orchestrator Streaming] LoopLadder break: repeated failing approach — forcing synthesis.');
|
|
3595
|
+
break;
|
|
3134
3596
|
}
|
|
3135
3597
|
// R29b hard cap (streaming parity): force-stop at 2x soft budget;
|
|
3136
3598
|
// the post-loop R29a streaming net then synthesizes a deliverable.
|
|
@@ -3170,7 +3632,11 @@ export class CortexOrchestrator {
|
|
|
3170
3632
|
// Proactive context management - ensure history fits before streaming continuation
|
|
3171
3633
|
await this.ensureHistoryFitsModel(effectiveModel);
|
|
3172
3634
|
// Anchor lift — streaming mirror of the sendMessage continuation path.
|
|
3173
|
-
if (!this.anchorLifted &&
|
|
3635
|
+
if (!this.anchorLifted &&
|
|
3636
|
+
resolveToolAnchor(process.env, this.cardAnchorProfile) &&
|
|
3637
|
+
// Persist frame (backlog item 5): the anchored surface IS the session
|
|
3638
|
+
// surface — never lift, never deliver the deferred corpus.
|
|
3639
|
+
resolveFrameProfile(process.env, this.cardFrameProfile) !== 'persist') {
|
|
3174
3640
|
this.anchorLifted = true;
|
|
3175
3641
|
if (!(this.config.enableDeferredToolLoading && !isPTCEnabled)) {
|
|
3176
3642
|
toolsToUse = allTools;
|
|
@@ -5886,6 +6352,28 @@ export class CortexOrchestrator {
|
|
|
5886
6352
|
}
|
|
5887
6353
|
return this.routerMatrix;
|
|
5888
6354
|
}
|
|
6355
|
+
/** The turn's real user prompt text — the most recent user message that is
|
|
6356
|
+
* neither a tool_result carrier nor an injected <system-reminder> steering
|
|
6357
|
+
* message. Used by EndTurn Stage 4 to judge task shape. Empty string when
|
|
6358
|
+
* none found (Stage 4 then never fires its task-shape branch). */
|
|
6359
|
+
lastRealUserText() {
|
|
6360
|
+
for (let i = this.messageHistory.length - 1; i >= 0; i--) {
|
|
6361
|
+
const m = this.messageHistory[i];
|
|
6362
|
+
const isUser = m?.type === 'user' || m?.message?.role === 'user';
|
|
6363
|
+
if (!isUser)
|
|
6364
|
+
continue;
|
|
6365
|
+
const content = m?.message?.content;
|
|
6366
|
+
if (Array.isArray(content) && content.some((b) => b?.type === 'tool_result'))
|
|
6367
|
+
continue;
|
|
6368
|
+
const text = Array.isArray(content)
|
|
6369
|
+
? content.filter((b) => b?.type === 'text').map((b) => b.text || '').join('\n')
|
|
6370
|
+
: typeof content === 'string' ? content : '';
|
|
6371
|
+
if (!text.trim() || text.trimStart().startsWith('<system-reminder>'))
|
|
6372
|
+
continue;
|
|
6373
|
+
return text;
|
|
6374
|
+
}
|
|
6375
|
+
return '';
|
|
6376
|
+
}
|
|
5889
6377
|
getDecisionStore() {
|
|
5890
6378
|
if (this.isRecordingEnabled() === false && this.isLookupEnabled() === false) {
|
|
5891
6379
|
return undefined;
|
|
@@ -5915,7 +6403,14 @@ export class CortexOrchestrator {
|
|
|
5915
6403
|
const store = this.getDecisionStore();
|
|
5916
6404
|
if (!store)
|
|
5917
6405
|
return result;
|
|
5918
|
-
|
|
6406
|
+
// Unified outcome layer (docs/UNIFIED_OUTCOME_LADDER.md): ONE failure
|
|
6407
|
+
// semantics for recording + reminders. Critically, a bash command that
|
|
6408
|
+
// ran and exited nonzero is a FAILURE here even though its tool result
|
|
6409
|
+
// is not is_error on the wire (ShellTool returns success results for
|
|
6410
|
+
// exit!=0) — previously those loops were recorded as successes, so the
|
|
6411
|
+
// prior/family reminders never fired on the dominant thrash mode.
|
|
6412
|
+
const outcome = classifyToolOutcome(toolUse.name, toolUse.input, result);
|
|
6413
|
+
const inputHash = outcome.exactHash;
|
|
5919
6414
|
let augmented = result;
|
|
5920
6415
|
// 1. Lookup priors (read side) — only emits reminder when failures exist.
|
|
5921
6416
|
// Fetch the 3 most recent matching decisions so the formatter can
|
|
@@ -5930,6 +6425,21 @@ export class CortexOrchestrator {
|
|
|
5930
6425
|
if (reminder && result.content) {
|
|
5931
6426
|
augmented = { ...result, content: reminder + result.content };
|
|
5932
6427
|
}
|
|
6428
|
+
else if (outcome.status !== 'ok' && result.content) {
|
|
6429
|
+
// Family lens (AHE borrow): no exact-input prior fired, but the
|
|
6430
|
+
// CURRENT failure may be the latest of a repeated-approach family
|
|
6431
|
+
// across different inputs — hint once the family has >=2 prior
|
|
6432
|
+
// failures spanning >=2 distinct inputs. Gated on the UNIFIED
|
|
6433
|
+
// outcome (not is_error) so failing-command loops are covered.
|
|
6434
|
+
const family = outcome.family ?? classifyErrorFamily(String(result.content).slice(0, 200));
|
|
6435
|
+
if (family) {
|
|
6436
|
+
const ff = await store.familyFailures(toolUse.name, family);
|
|
6437
|
+
const famReminder = formatFamilyReminder(toolUse.name, family, ff.count, ff.distinctInputs, ff.recent);
|
|
6438
|
+
if (famReminder) {
|
|
6439
|
+
augmented = { ...result, content: famReminder + result.content };
|
|
6440
|
+
}
|
|
6441
|
+
}
|
|
6442
|
+
}
|
|
5933
6443
|
}
|
|
5934
6444
|
catch (err) {
|
|
5935
6445
|
if (this.config.debug) {
|
|
@@ -5945,8 +6455,10 @@ export class CortexOrchestrator {
|
|
|
5945
6455
|
sessionId: this.currentSessionId ?? 'unknown',
|
|
5946
6456
|
toolName: toolUse.name,
|
|
5947
6457
|
input: toolUse.input,
|
|
5948
|
-
|
|
5949
|
-
|
|
6458
|
+
// Unified outcome: exit!=0 commands are FAILURES for the store even
|
|
6459
|
+
// though their wire result is not is_error (see outcome above).
|
|
6460
|
+
success: outcome.status === 'ok',
|
|
6461
|
+
...(outcome.status !== 'ok'
|
|
5950
6462
|
? { errorSnippet: String(result.content).slice(0, 200) }
|
|
5951
6463
|
: {}),
|
|
5952
6464
|
});
|