@nexus-cortex/core 4.69.0 → 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.
Files changed (48) hide show
  1. package/dist/models/configurators/DeepSeekConfigurator.d.ts +3 -0
  2. package/dist/models/configurators/DeepSeekConfigurator.d.ts.map +1 -1
  3. package/dist/models/configurators/DeepSeekConfigurator.js +1 -0
  4. package/dist/models/configurators/DeepSeekConfigurator.js.map +1 -1
  5. package/dist/models/configurators/XAIConfigurator.d.ts +3 -0
  6. package/dist/models/configurators/XAIConfigurator.d.ts.map +1 -1
  7. package/dist/models/configurators/XAIConfigurator.js +1 -0
  8. package/dist/models/configurators/XAIConfigurator.js.map +1 -1
  9. package/dist/orchestrator/CortexOrchestrator.d.ts +9 -0
  10. package/dist/orchestrator/CortexOrchestrator.d.ts.map +1 -1
  11. package/dist/orchestrator/CortexOrchestrator.js +535 -15
  12. package/dist/orchestrator/CortexOrchestrator.js.map +1 -1
  13. package/dist/orchestrator/inactionGuard.d.ts +42 -0
  14. package/dist/orchestrator/inactionGuard.d.ts.map +1 -0
  15. package/dist/orchestrator/inactionGuard.js +47 -0
  16. package/dist/orchestrator/inactionGuard.js.map +1 -0
  17. package/dist/orchestrator/requirementsVerification.d.ts +43 -0
  18. package/dist/orchestrator/requirementsVerification.d.ts.map +1 -0
  19. package/dist/orchestrator/requirementsVerification.js +87 -0
  20. package/dist/orchestrator/requirementsVerification.js.map +1 -0
  21. package/dist/tools/ToolProfile.d.ts +9 -0
  22. package/dist/tools/ToolProfile.d.ts.map +1 -1
  23. package/dist/tools/ToolProfile.js +19 -0
  24. package/dist/tools/ToolProfile.js.map +1 -1
  25. package/dist/tools/registries/BaseToolRegistry.d.ts.map +1 -1
  26. package/dist/tools/registries/BaseToolRegistry.js +14 -0
  27. package/dist/tools/registries/BaseToolRegistry.js.map +1 -1
  28. package/dist/training/DecisionPriorInjector.d.ts +7 -0
  29. package/dist/training/DecisionPriorInjector.d.ts.map +1 -1
  30. package/dist/training/DecisionPriorInjector.js +22 -0
  31. package/dist/training/DecisionPriorInjector.js.map +1 -1
  32. package/dist/training/DecisionStore.d.ts +39 -0
  33. package/dist/training/DecisionStore.d.ts.map +1 -1
  34. package/dist/training/DecisionStore.js +94 -0
  35. package/dist/training/DecisionStore.js.map +1 -1
  36. package/dist/training/errorFamily.d.ts +15 -0
  37. package/dist/training/errorFamily.d.ts.map +1 -0
  38. package/dist/training/errorFamily.js +40 -0
  39. package/dist/training/errorFamily.js.map +1 -0
  40. package/dist/training/loopLadder.d.ts +35 -0
  41. package/dist/training/loopLadder.d.ts.map +1 -0
  42. package/dist/training/loopLadder.js +58 -0
  43. package/dist/training/loopLadder.js.map +1 -0
  44. package/dist/training/toolOutcome.d.ts +20 -0
  45. package/dist/training/toolOutcome.d.ts.map +1 -0
  46. package/dist/training/toolOutcome.js +100 -0
  47. package/dist/training/toolOutcome.js.map +1 -0
  48. 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, stableInputHash } from '../training/DecisionStore.js';
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,11 @@ 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;
712
+ // Thread the CARD's anchor to the executors (per-orchestrator config, by
713
+ // reference) so framing-aware executor behavior (ShellTool steering
714
+ // default) is model-card/registry-scoped, not process-global env.
715
+ this.executorRegistry.updateConfig({ activeAnchorProfile: this.cardAnchorProfile });
702
716
  if (toolsToUse && toolsToUse.length > 0) {
703
717
  toolsToUse = this.applyAnchorIfArmed(toolsToUse);
704
718
  }
@@ -974,6 +988,9 @@ export class CortexOrchestrator {
974
988
  // Round 18b: track whether we've already retried on empty response so
975
989
  // we don't loop forever. See empty-detection block below.
976
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;
977
994
  // EndTurn gate (Stage 1): the turn must not complete until the model
978
995
  // calls the EndTurn attestation tool. Steering is ignored by some
979
996
  // models (grok-4.3); a required tool call is not. Bounded so a model
@@ -1017,6 +1034,13 @@ export class CortexOrchestrator {
1017
1034
  const thisTurnToolOutputs = [];
1018
1035
  // Loop detection: Track recent tool calls to detect repetitive loops
1019
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;
1020
1044
  // Track all tool uses across iterations for response
1021
1045
  const allExecutedToolUses = [];
1022
1046
  // Track tool call counts for diversity warning
@@ -1194,6 +1218,130 @@ export class CortexOrchestrator {
1194
1218
  continue;
1195
1219
  }
1196
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
+ }
1197
1345
  // EndTurn gate (Stage 1): refuse to finalize until the model has
1198
1346
  // called the EndTurn attestation tool. Only arms when the turn
1199
1347
  // actually used a (non-EndTurn) tool — pure-language turns bypass.
@@ -1217,8 +1365,27 @@ export class CortexOrchestrator {
1217
1365
  if (!cv.ok)
1218
1366
  stage3Violations = cv.violations;
1219
1367
  }
1220
- if (endTurnGateEnabled && turnUsedTools && endTurnNudges < END_TURN_MAX_NUDGES &&
1221
- (!endTurnCalled || stage3Violations.length > 0)) {
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) {
1222
1389
  endTurnNudges++;
1223
1390
  const gateReason = !endTurnCalled ? 'missing-EndTurn' : 'coordinate-violation';
1224
1391
  console.warn(`[Orchestrator] EndTurn gate: ${gateReason} ` +
@@ -1494,6 +1661,49 @@ export class CortexOrchestrator {
1494
1661
  try {
1495
1662
  const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState);
1496
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
+ }
1497
1707
  // File-history snapshots (Claude Code parity — previously built but
1498
1708
  // UNWIRED: FileCheckpointManager existed with zero markModified
1499
1709
  // callers and its snapshot message was discarded). Mark files
@@ -1574,7 +1784,31 @@ export class CortexOrchestrator {
1574
1784
  lastEndTurnCitations = Array.isArray(cits) ? cits : undefined; // Stage 3 baseline
1575
1785
  const verdict = verifyCitationsGrounded(cits, thisTurnToolOutputs.join('\n'));
1576
1786
  if (verdict.grounded) {
1577
- endTurnCalled = true;
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
+ }
1578
1812
  }
1579
1813
  else {
1580
1814
  const bad = verdict.ungrounded
@@ -1722,14 +1956,43 @@ export class CortexOrchestrator {
1722
1956
  const progressStalled = isToolProgressStalled(recentToolCalls);
1723
1957
  const budgetSignal = computeToolBudgetSignal(effectiveToolBudgetCount, TOOL_BUDGET_SOFT, progressStalled);
1724
1958
  const diversityWarning = this.getDiversityWarning(toolCallCounts);
1725
- if (budgetSignal || diversityWarning) {
1726
- const signals = [budgetSignal, diversityWarning].filter(Boolean).join('\n');
1959
+ if (budgetSignal || diversityWarning || ladderSignal) {
1960
+ const signals = [budgetSignal, diversityWarning, ladderSignal].filter(Boolean).join('\n');
1727
1961
  const lastMsg = this.messageHistory[this.messageHistory.length - 1];
1728
1962
  if (lastMsg?.message?.content?.[0]?.type === 'tool_result') {
1729
1963
  const block = lastMsg.message.content[0];
1730
1964
  const existing = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
1731
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
+ }
1732
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;
1733
1996
  }
1734
1997
  // R29b hard cap: a weaker model may ignore the firm budget reminders
1735
1998
  // and keep spinning (benchmark: deepseek 46 successful local calls,
@@ -1778,7 +2041,11 @@ export class CortexOrchestrator {
1778
2041
  // full profile applies from here. Rebuild from allTools because the
1779
2042
  // armed anchor narrowed toolsToUse at assembly. (When deferred loading
1780
2043
  // is on, the re-filter block below performs the rebuild.)
1781
- if (!this.anchorLifted && resolveToolAnchor(process.env, this.cardAnchorProfile)) {
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') {
1782
2049
  this.anchorLifted = true;
1783
2050
  if (!(this.config.enableDeferredToolLoading && !isPTCEnabled)) {
1784
2051
  toolsToUse = allTools;
@@ -2560,6 +2827,11 @@ export class CortexOrchestrator {
2560
2827
  // Card-level home door: capture the active card's anchorProfile (env
2561
2828
  // CORTEX_TOOL_ANCHOR overrides inside the resolver; env 'none' disables).
2562
2829
  this.cardAnchorProfile = effectiveModel.anchorProfile ?? null;
2830
+ this.cardFrameProfile = effectiveModel.frameProfile ?? null;
2831
+ // Thread the CARD's anchor to the executors (per-orchestrator config, by
2832
+ // reference) so framing-aware executor behavior (ShellTool steering
2833
+ // default) is model-card/registry-scoped, not process-global env.
2834
+ this.executorRegistry.updateConfig({ activeAnchorProfile: this.cardAnchorProfile });
2563
2835
  if (toolsToUse && toolsToUse.length > 0) {
2564
2836
  toolsToUse = this.applyAnchorIfArmed(toolsToUse);
2565
2837
  }
@@ -2758,7 +3030,12 @@ export class CortexOrchestrator {
2758
3030
  let toolCallIteration = 0;
2759
3031
  let totalToolErrors = 0;
2760
3032
  let emptyResponseRetryUsed = false; // R32: parity with sendMessage R18b guard
3033
+ let inactionNudgeUsed = false; // Inaction guard (backlog item 2) — streaming parity
2761
3034
  const allToolCalls = [];
3035
+ // Unified Outcome Ladder (streaming parity — see sendMessage).
3036
+ const loopLadder = new LoopLadder();
3037
+ let ladderBreak = null;
3038
+ let ladderSignal = null;
2762
3039
  const allExecutedToolUses = [];
2763
3040
  const toolCallCounts = new Map();
2764
3041
  // DEBUG: Log canonical message content structure for tool extraction (streaming path)
@@ -2925,6 +3202,136 @@ export class CortexOrchestrator {
2925
3202
  console.warn(`[Orchestrator Streaming] R32 retry failed:`, retryErr);
2926
3203
  }
2927
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
+ }
2928
3335
  hasToolUse = false;
2929
3336
  break;
2930
3337
  }
@@ -2964,6 +3371,45 @@ export class CortexOrchestrator {
2964
3371
  // Execute tools (reuse existing method)
2965
3372
  const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState);
2966
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
+ }
2967
3413
  // R21 (2026-05-15): same fix as non-streaming path — count consecutive
2968
3414
  // ITERATIONS WITH ZERO SUCCESSFUL TOOLS, not cumulative individual
2969
3415
  // failures. Reset whenever at least one tool succeeded.
@@ -3115,14 +3561,38 @@ export class CortexOrchestrator {
3115
3561
  const progressStalled = isToolProgressStalled(allToolCalls);
3116
3562
  const budgetSignal = computeToolBudgetSignal(effectiveToolBudgetCount, TOOL_BUDGET_SOFT, progressStalled);
3117
3563
  const diversityWarning = this.getDiversityWarning(toolCallCounts);
3118
- if (budgetSignal || diversityWarning) {
3119
- const signals = [budgetSignal, diversityWarning].filter(Boolean).join('\n');
3564
+ if (budgetSignal || diversityWarning || ladderSignal) {
3565
+ const signals = [budgetSignal, diversityWarning, ladderSignal].filter(Boolean).join('\n');
3120
3566
  const lastMsg = this.messageHistory[this.messageHistory.length - 1];
3121
3567
  if (lastMsg?.message?.content?.[0]?.type === 'tool_result') {
3122
3568
  const block = lastMsg.message.content[0];
3123
3569
  const existing = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
3124
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
+ }
3125
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;
3126
3596
  }
3127
3597
  // R29b hard cap (streaming parity): force-stop at 2x soft budget;
3128
3598
  // the post-loop R29a streaming net then synthesizes a deliverable.
@@ -3162,7 +3632,11 @@ export class CortexOrchestrator {
3162
3632
  // Proactive context management - ensure history fits before streaming continuation
3163
3633
  await this.ensureHistoryFitsModel(effectiveModel);
3164
3634
  // Anchor lift — streaming mirror of the sendMessage continuation path.
3165
- if (!this.anchorLifted && resolveToolAnchor(process.env, this.cardAnchorProfile)) {
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') {
3166
3640
  this.anchorLifted = true;
3167
3641
  if (!(this.config.enableDeferredToolLoading && !isPTCEnabled)) {
3168
3642
  toolsToUse = allTools;
@@ -5878,6 +6352,28 @@ export class CortexOrchestrator {
5878
6352
  }
5879
6353
  return this.routerMatrix;
5880
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
+ }
5881
6377
  getDecisionStore() {
5882
6378
  if (this.isRecordingEnabled() === false && this.isLookupEnabled() === false) {
5883
6379
  return undefined;
@@ -5907,7 +6403,14 @@ export class CortexOrchestrator {
5907
6403
  const store = this.getDecisionStore();
5908
6404
  if (!store)
5909
6405
  return result;
5910
- const inputHash = stableInputHash(toolUse.input);
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;
5911
6414
  let augmented = result;
5912
6415
  // 1. Lookup priors (read side) — only emits reminder when failures exist.
5913
6416
  // Fetch the 3 most recent matching decisions so the formatter can
@@ -5922,6 +6425,21 @@ export class CortexOrchestrator {
5922
6425
  if (reminder && result.content) {
5923
6426
  augmented = { ...result, content: reminder + result.content };
5924
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
+ }
5925
6443
  }
5926
6444
  catch (err) {
5927
6445
  if (this.config.debug) {
@@ -5937,8 +6455,10 @@ export class CortexOrchestrator {
5937
6455
  sessionId: this.currentSessionId ?? 'unknown',
5938
6456
  toolName: toolUse.name,
5939
6457
  input: toolUse.input,
5940
- success: !result.is_error,
5941
- ...(result.is_error
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'
5942
6462
  ? { errorSnippet: String(result.content).slice(0, 200) }
5943
6463
  : {}),
5944
6464
  });