@nexus-cortex/core 4.91.1 → 4.92.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/.env.example +42 -0
- package/dist/config/effectiveConfig.d.ts.map +1 -1
- package/dist/config/effectiveConfig.js +2 -0
- package/dist/config/effectiveConfig.js.map +1 -1
- package/dist/middleware/HelperModelMiddleware.d.ts +28 -0
- package/dist/middleware/HelperModelMiddleware.d.ts.map +1 -1
- package/dist/middleware/HelperModelMiddleware.js +63 -1
- package/dist/middleware/HelperModelMiddleware.js.map +1 -1
- package/dist/middleware/helpers/helperFrame.d.ts +4 -0
- package/dist/middleware/helpers/helperFrame.d.ts.map +1 -1
- package/dist/middleware/helpers/helperFrame.js.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.d.ts +36 -0
- package/dist/orchestrator/CortexOrchestrator.d.ts.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.js +280 -12
- package/dist/orchestrator/CortexOrchestrator.js.map +1 -1
- package/dist/orchestrator/emptyResponseClassifier.d.ts +5 -2
- package/dist/orchestrator/emptyResponseClassifier.d.ts.map +1 -1
- package/dist/orchestrator/emptyResponseClassifier.js +18 -1
- package/dist/orchestrator/emptyResponseClassifier.js.map +1 -1
- package/dist/orchestrator/outerToolTimeout.d.ts +5 -0
- package/dist/orchestrator/outerToolTimeout.d.ts.map +1 -0
- package/dist/orchestrator/outerToolTimeout.js +28 -0
- package/dist/orchestrator/outerToolTimeout.js.map +1 -0
- package/dist/orchestrator/requirementsVerification.d.ts.map +1 -1
- package/dist/orchestrator/requirementsVerification.js +22 -3
- package/dist/orchestrator/requirementsVerification.js.map +1 -1
- package/dist/orchestrator/timeBudget.d.ts +19 -0
- package/dist/orchestrator/timeBudget.d.ts.map +1 -0
- package/dist/orchestrator/timeBudget.js +24 -0
- package/dist/orchestrator/timeBudget.js.map +1 -0
- package/dist/tools/ToolProfile.d.ts +5 -0
- package/dist/tools/ToolProfile.d.ts.map +1 -1
- package/dist/tools/ToolProfile.js +15 -0
- package/dist/tools/ToolProfile.js.map +1 -1
- package/dist/training/DecisionStore.d.ts +1 -1
- package/dist/training/DecisionStore.d.ts.map +1 -1
- package/dist/training/DecisionStore.js.map +1 -1
- package/dist/training/endTurnResolver.d.ts +46 -0
- package/dist/training/endTurnResolver.d.ts.map +1 -0
- package/dist/training/endTurnResolver.js +71 -0
- package/dist/training/endTurnResolver.js.map +1 -0
- package/dist/training/liftPlanner.d.ts +65 -0
- package/dist/training/liftPlanner.d.ts.map +1 -0
- package/dist/training/liftPlanner.js +93 -0
- package/dist/training/liftPlanner.js.map +1 -0
- package/package.json +3 -3
|
@@ -16,6 +16,10 @@ import { verifyCoordinates, deterministicCoordinateScore } from './coordinateVer
|
|
|
16
16
|
import { buildRouterSample, appendJsonlRotating } from './cortexTrainingRecord.js';
|
|
17
17
|
import { join as pathJoin } from 'path';
|
|
18
18
|
import { existsSync } from 'fs';
|
|
19
|
+
import { execSync } from 'node:child_process';
|
|
20
|
+
import { ENV_RECON_COMMAND, resolveLiftPlanConfig } from '../training/liftPlanner.js';
|
|
21
|
+
import { resolveEndTurnResolverConfig, parseResolverVerdict } from '../training/endTurnResolver.js';
|
|
22
|
+
import { isTaskShaped } from './requirementsVerification.js';
|
|
19
23
|
import { extractServerSideMetadata, XAIServerSideTools, OpenAIServerSideTools, toCanonicalTool } from '../tools/ServerSideTools.js';
|
|
20
24
|
// Phase 2.2.1: Timeline and History Storage
|
|
21
25
|
import { SessionTimeline } from '../session/SessionTimeline.js';
|
|
@@ -29,8 +33,10 @@ import { CacheMetricsAccumulator } from '../session/CacheMetricsAccumulator.js';
|
|
|
29
33
|
import { SubAgentProcessManager, createSubAgentProcessManager } from './SubAgentProcessManager.js';
|
|
30
34
|
// Phase 1: Tool Architecture Refactor - Unified Tool Registry
|
|
31
35
|
import { toolFactory } from '../tools/ToolFactory.js';
|
|
32
|
-
import { resolveToolProfile, resolveToolAnchor, resolveFrameProfile, resolveLiftNudge, resolveHeadlessDropAskUser, resolveDeferredLoading, isNarrowProfile, isToolAllowedByProfile, applyToolProfile, webToolBlocked, resolveVisionHelperModel, resolveVisionHandoffMax, resolveSliceNudge } from '../tools/ToolProfile.js';
|
|
36
|
+
import { resolveToolProfile, resolveToolAnchor, resolveFrameProfile, resolveLiftNudge, resolveLiftPlan, resolveEndTurnResolver, resolveHeadlessDropAskUser, resolveDeferredLoading, isNarrowProfile, isToolAllowedByProfile, applyToolProfile, webToolBlocked, resolveVisionHelperModel, resolveVisionHandoffMax, resolveSliceNudge } from '../tools/ToolProfile.js';
|
|
33
37
|
import { TurnEvidence, evaluateEndTurnGates, buildMissingEndTurnReminder } from './endTurnGates.js';
|
|
38
|
+
import { resolveOuterToolDeadlineMs } from './outerToolTimeout.js';
|
|
39
|
+
import { resolveTurnDeadlineMs, timeBudgetState, timeBudgetWarnNudge } from './timeBudget.js';
|
|
34
40
|
import { readStagedDoctrine, applyCuratedDoctrine, runOrientForStaging, withTimeout } from './doctrineCuration.js';
|
|
35
41
|
import { ExactRepeatTracker } from '../training/loopLadder.js';
|
|
36
42
|
import { slashCommandRegistry } from '../commands/SlashCommandRegistry.js';
|
|
@@ -59,7 +65,7 @@ import { ListAvailableMcpServers, SearchMcpServers, GetMcpConfig, EnableMcpServe
|
|
|
59
65
|
import { InitCortexContext, MemoryWrite, MemoryRecall } from '../tools/context-management/index.js';
|
|
60
66
|
import { pruneAgedToolResults } from '../conversation/ToolResultPruner.js';
|
|
61
67
|
import { detectTailRepetition, tailLoopGuardEnabled } from './tailRepetitionDetector.js';
|
|
62
|
-
import { classifyEmptyResponse, emptyResponseNudge } from './emptyResponseClassifier.js';
|
|
68
|
+
import { classifyEmptyResponse, emptyResponseNudge, nudgeForbidsTools } from './emptyResponseClassifier.js';
|
|
63
69
|
// PTC: Progressive tool loading for non-PTC providers
|
|
64
70
|
import { ClientSideToolFilter } from '../tools/ClientSideToolFilter.js';
|
|
65
71
|
/** Grace the orchestrator's outer tool-abort waits past the tool's own deadline (ShellTool promote/kill fires first; 4.90.1). */
|
|
@@ -163,6 +169,9 @@ export class CortexOrchestrator {
|
|
|
163
169
|
* exactly once, at the anchor-lift boundary. One-shot per orchestrator. */
|
|
164
170
|
deferredCorpusDelivered = false;
|
|
165
171
|
liftNudgeDelivered = false; // A′ proposal-1: lift-boundary SearchTools/AskForAdvice nudge, one-shot
|
|
172
|
+
liftPlanDelivered = false; // LIFT_MENTOR_PLANNER: bounded mentor-planner at the lift, one-shot
|
|
173
|
+
cachedEnvReport; // ENV_RECON_COMMAND output, gathered once, shared by lift-planner + endturn-resolver
|
|
174
|
+
endTurnResolverRejects = 0; // endTurnResolver: GAP vetoes so far this task (bounded by maxRejects → fallback-accept)
|
|
166
175
|
effectiveDeferredLoading = true; // per-turn resolved deferred-loading (card > env > settings); set at assembly
|
|
167
176
|
/**
|
|
168
177
|
* Item 10 — helper-curated doctrine freshness (operator-designed; docs/
|
|
@@ -339,6 +348,198 @@ export class CortexOrchestrator {
|
|
|
339
348
|
console.warn(`[Anchor] lift nudge delivery failed (continuing without): ${e?.message ?? e}`);
|
|
340
349
|
}
|
|
341
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* LIFT_MENTOR_PLANNER (spec §2) — at the anchor-lift boundary, when CORTEX_LIFT_PLAN is armed, a
|
|
353
|
+
* bounded max-reasoning mentor plans the task ONCE (adversarial analysis + confirm the REAL grader
|
|
354
|
+
* criteria + a criteria-anchored step plan, or a RETIRE plan) and the plan is appended as a
|
|
355
|
+
* system-reminder to the just-recorded first tool_result. The narrow-door model resumes with the
|
|
356
|
+
* plan in front of it, never having issued a planning call (forced tool_choice is dead on DeepSeek
|
|
357
|
+
* once history holds a prior tool_call; orchestrator-direct-invoke + system-reminder delivery is
|
|
358
|
+
* the path that provably works — same pattern as the CORTEX_MENTOR_AUTO consult). One-shot per
|
|
359
|
+
* orchestrator; bounded by a timeout + the helper's output budget (NOT a token cap, which would
|
|
360
|
+
* truncate DeepSeek mid-thought). Fail-open: on timeout/error the model just proceeds. Mirrors
|
|
361
|
+
* deliverDeferredCorpusAtLift + ensureDoctrineFresh. No-op unless CORTEX_LIFT_PLAN truthy. */
|
|
362
|
+
async deliverLiftPlanAtLift(model) {
|
|
363
|
+
if (this.liftPlanDelivered)
|
|
364
|
+
return;
|
|
365
|
+
if (!resolveLiftPlan(process.env, model.liftPlan))
|
|
366
|
+
return;
|
|
367
|
+
// 🔴 INTERACTIVE GATE (operator caveat 2026-09-04): the planner adds a ~30-60s silent pause at
|
|
368
|
+
// the lift (pro @ max reasoning). In a headless/one-shot/bench session (autoApproveActions=true)
|
|
369
|
+
// there is no human watching — fine. In an interactive TUI a silent pause reads as a frozen UI,
|
|
370
|
+
// so the planner is SUPPRESSED there UNLESS a thinking-indicator is wired and opted in via
|
|
371
|
+
// CORTEX_LIFT_PLAN_INTERACTIVE=true. (autoApproveActions = !(stdin.isTTY && stdout.isTTY).)
|
|
372
|
+
if (!this.approvalMode.autoApproveActions && process.env.CORTEX_LIFT_PLAN_INTERACTIVE !== 'true') {
|
|
373
|
+
return; // interactive session, no thinking-indicator opt-in → do not fire (do not consume one-shot)
|
|
374
|
+
}
|
|
375
|
+
this.liftPlanDelivered = true; // one-shot even if the call fails (never re-fire mid-task)
|
|
376
|
+
if (!this.helperMiddleware?.generateTaskPlan)
|
|
377
|
+
return;
|
|
378
|
+
const store = this.getDecisionStore();
|
|
379
|
+
const sessionId = this.currentSessionId ?? 'unknown';
|
|
380
|
+
try {
|
|
381
|
+
const lastMsg = this.messageHistory[this.messageHistory.length - 1];
|
|
382
|
+
// Same shape guard as the corpus/nudge deliveries: only when the last message is the first
|
|
383
|
+
// tool_result — that IS the environment observation the planner reasons from.
|
|
384
|
+
if (lastMsg?.message?.content?.[0]?.type !== 'tool_result')
|
|
385
|
+
return;
|
|
386
|
+
const observations = lastMsg.message.content
|
|
387
|
+
.filter((b) => b?.type === 'tool_result')
|
|
388
|
+
.map((b) => (typeof b.content === 'string' ? b.content : JSON.stringify(b.content)))
|
|
389
|
+
.join('\n')
|
|
390
|
+
.slice(0, 4000);
|
|
391
|
+
// v1.1 (2026-09-04): gather a GUARANTEED environment report so the planner steers installs +
|
|
392
|
+
// Bash timeouts from the box's REAL resources (tooling/packages/disk/tests) — not just whatever
|
|
393
|
+
// the model's first action happened to observe. Bounded + fail-open (partial stdout on
|
|
394
|
+
// timeout/non-zero is still useful; empty on total failure → plan from observations alone).
|
|
395
|
+
const envReport = this.gatherEnvReport();
|
|
396
|
+
const timeoutMs = parseInt(process.env.CORTEX_LIFT_PLAN_TIMEOUT_MS ?? '90000', 10);
|
|
397
|
+
const plan = await withTimeout(this.helperMiddleware.generateTaskPlan({
|
|
398
|
+
task: this.lastRealUserText(),
|
|
399
|
+
observations,
|
|
400
|
+
envReport,
|
|
401
|
+
helperModelId: this.config.reactiveMentorship?.helperModelId,
|
|
402
|
+
}), timeoutMs);
|
|
403
|
+
if (!plan || !plan.trim()) {
|
|
404
|
+
if (store)
|
|
405
|
+
void store.recordEvent({
|
|
406
|
+
sessionId, kind: 'lift_plan', detail: { fired: true, planChars: 0, empty: true },
|
|
407
|
+
}).catch(() => { });
|
|
408
|
+
return; // fail-open: proceed without a plan
|
|
409
|
+
}
|
|
410
|
+
const retire = /\bRETIRE\b/i.test(plan);
|
|
411
|
+
const criteriaStated = /criteri|grader|test\.sh|expected|verify/i.test(plan);
|
|
412
|
+
lastMsg.message.content.push({
|
|
413
|
+
type: 'text',
|
|
414
|
+
text: `<system-reminder>\nA senior engineer prepared this PLAN OF ATTACK for the task — follow it. ` +
|
|
415
|
+
`It is anchored to the REAL success criteria (what the task's own grader checks), NOT your own ` +
|
|
416
|
+
`tests. Your own tests are a means, never the finish line.\n\n${plan}\n</system-reminder>`,
|
|
417
|
+
});
|
|
418
|
+
if (store)
|
|
419
|
+
void store.recordEvent({
|
|
420
|
+
sessionId,
|
|
421
|
+
kind: 'lift_plan',
|
|
422
|
+
detail: { fired: true, planChars: plan.length, retire, criteriaStated },
|
|
423
|
+
}).catch(() => { });
|
|
424
|
+
if (this.config.debug) {
|
|
425
|
+
console.log(`[LiftPlan] plan delivered at lift (${plan.length} chars, retire=${retire}, criteria=${criteriaStated})`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
catch (e) {
|
|
429
|
+
if (store)
|
|
430
|
+
void store.recordEvent({
|
|
431
|
+
sessionId, kind: 'lift_plan', detail: { fired: true, error: String(e?.message ?? e).slice(0, 120) },
|
|
432
|
+
}).catch(() => { });
|
|
433
|
+
if (this.config.debug)
|
|
434
|
+
console.warn(`[LiftPlan] delivery failed (continuing without): ${e?.message ?? e}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/** Bounded read-only environment recon (ENV_RECON_COMMAND), gathered ONCE and cached — shared by the
|
|
438
|
+
* lift planner and the EndTurn resolver. Fail-open: partial stdout on timeout/non-zero, else empty. */
|
|
439
|
+
gatherEnvReport() {
|
|
440
|
+
if (this.cachedEnvReport !== undefined)
|
|
441
|
+
return this.cachedEnvReport;
|
|
442
|
+
let r = '';
|
|
443
|
+
try {
|
|
444
|
+
r = String(execSync(ENV_RECON_COMMAND, {
|
|
445
|
+
cwd: this.config.projectPath || process.cwd(),
|
|
446
|
+
timeout: resolveLiftPlanConfig().reconTimeoutMs,
|
|
447
|
+
encoding: 'utf8',
|
|
448
|
+
maxBuffer: 512 * 1024,
|
|
449
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
450
|
+
shell: '/bin/sh',
|
|
451
|
+
})).slice(0, 6000);
|
|
452
|
+
}
|
|
453
|
+
catch (re) {
|
|
454
|
+
r = String(re?.stdout ?? '').slice(0, 6000);
|
|
455
|
+
}
|
|
456
|
+
this.cachedEnvReport = r;
|
|
457
|
+
return r;
|
|
458
|
+
}
|
|
459
|
+
/** The most recent assistant message's text (the draft/work product the model just produced). */
|
|
460
|
+
lastAssistantText() {
|
|
461
|
+
for (let i = this.messageHistory.length - 1; i >= 0; i--) {
|
|
462
|
+
const m = this.messageHistory[i];
|
|
463
|
+
if (m?.message?.role !== 'assistant')
|
|
464
|
+
continue;
|
|
465
|
+
const c = m.message.content;
|
|
466
|
+
if (typeof c === 'string')
|
|
467
|
+
return c;
|
|
468
|
+
if (Array.isArray(c))
|
|
469
|
+
return c.filter((b) => b?.type === 'text').map((b) => b.text || '').join('\n');
|
|
470
|
+
return '';
|
|
471
|
+
}
|
|
472
|
+
return '';
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* endTurnResolver (operator design 2026-09-04) — the mentor-as-EndTurn-judge, the finish-side twin of
|
|
476
|
+
* deliverLiftPlanAtLift. Runs AFTER evaluateEndTurnGates: only when the mechanical gate ACCEPTED the
|
|
477
|
+
* EndTurn (ev.endTurnCalled === true). A bounded max-reasoning mentor adjudicates "does the work product
|
|
478
|
+
* meet the task's real requirements?" — MEETS leaves the finish accepted; GAP VETOES it (ev.endTurnCalled
|
|
479
|
+
* = false) and mutates the EndTurn tool_result into the mentor's fix plan (same channel the gate uses), so
|
|
480
|
+
* the narrow-door model executes a plan instead of reasoning its way out of a mechanical rejection.
|
|
481
|
+
* Bounded by CORTEX_ENDTURN_RESOLVER_MAX_REJECTS then fallback-accepts (liveness). No-op when the resolver
|
|
482
|
+
* is unarmed → byte-identical. Fail-open on any error → leaves the finish accepted.
|
|
483
|
+
*/
|
|
484
|
+
async adjudicateEndTurn(ev, toolResults, toolUses, model) {
|
|
485
|
+
if (!resolveEndTurnResolver(process.env, model.endTurnResolver))
|
|
486
|
+
return;
|
|
487
|
+
if (ev.endTurnCalled !== true)
|
|
488
|
+
return; // gate rejected (or no EndTurn) — the resolver only vetoes PASSES
|
|
489
|
+
if (!this.helperMiddleware?.evaluateEndTurn)
|
|
490
|
+
return;
|
|
491
|
+
const task = this.lastRealUserText();
|
|
492
|
+
if (!isTaskShaped(task))
|
|
493
|
+
return; // only adjudicate real work tasks
|
|
494
|
+
const cfg = resolveEndTurnResolverConfig();
|
|
495
|
+
if (this.endTurnResolverRejects >= cfg.maxRejects)
|
|
496
|
+
return; // fallback-accept: liveness beats loops
|
|
497
|
+
const et = toolResults.find((tr) => tr.tool_name === 'EndTurn');
|
|
498
|
+
if (!et)
|
|
499
|
+
return;
|
|
500
|
+
const store = this.getDecisionStore();
|
|
501
|
+
const sessionId = this.currentSessionId ?? 'unknown';
|
|
502
|
+
try {
|
|
503
|
+
const etInput = toolUses.find((t) => t.id === et.tool_use_id || t.name === 'EndTurn')?.input ?? {};
|
|
504
|
+
const outputs = (ev.outputs ?? []).join('\n').slice(0, 4000);
|
|
505
|
+
const workProduct = `${this.lastAssistantText()}\n\n--- checks / tool outputs this task ---\n${outputs}`.slice(0, 8000);
|
|
506
|
+
const attestation = JSON.stringify(etInput).slice(0, 2000);
|
|
507
|
+
const timeoutMs = parseInt(process.env.CORTEX_ENDTURN_RESOLVER_TIMEOUT_MS ?? '90000', 10);
|
|
508
|
+
const text = await withTimeout(this.helperMiddleware.evaluateEndTurn({
|
|
509
|
+
task,
|
|
510
|
+
envReport: this.gatherEnvReport(),
|
|
511
|
+
workProduct,
|
|
512
|
+
attestation,
|
|
513
|
+
helperModelId: this.config.reactiveMentorship?.helperModelId,
|
|
514
|
+
}), timeoutMs);
|
|
515
|
+
const verdict = parseResolverVerdict(text ?? ''); // withTimeout → null on timeout; fail-open to MEETS
|
|
516
|
+
if (store)
|
|
517
|
+
void store.recordEvent({
|
|
518
|
+
sessionId,
|
|
519
|
+
kind: 'endturn_resolver',
|
|
520
|
+
toolName: 'EndTurn',
|
|
521
|
+
detail: { meets: verdict.meets, planChars: verdict.plan.length, rejects: this.endTurnResolverRejects, parsed: verdict.parsed },
|
|
522
|
+
}).catch(() => { });
|
|
523
|
+
if (!verdict.meets && verdict.plan) {
|
|
524
|
+
this.endTurnResolverRejects += 1;
|
|
525
|
+
ev.endTurnCalled = false; // VETO the finish
|
|
526
|
+
et.is_error = true;
|
|
527
|
+
et.content =
|
|
528
|
+
`EndTurn HELD (finish review) — the work does not yet meet the task's requirements. Fix plan:\n\n` +
|
|
529
|
+
`${verdict.plan}\n\nDo this, verify against the task's own criteria (not your own tests), then call EndTurn again.`;
|
|
530
|
+
if (this.config.debug)
|
|
531
|
+
console.warn(`[EndTurnResolver] GAP — vetoed finish (${verdict.plan.length}-char plan, reject ${this.endTurnResolverRejects}/${cfg.maxRejects})`);
|
|
532
|
+
}
|
|
533
|
+
else if (this.config.debug) {
|
|
534
|
+
console.log(`[EndTurnResolver] MEETS — finish confirmed (parsed=${verdict.parsed})`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch (e) {
|
|
538
|
+
if (store)
|
|
539
|
+
void store.recordEvent({ sessionId, kind: 'endturn_resolver', detail: { error: String(e?.message ?? e).slice(0, 120) } }).catch(() => { });
|
|
540
|
+
// fail-open: leave the finish accepted
|
|
541
|
+
}
|
|
542
|
+
}
|
|
342
543
|
/** Narrow `tools` to the anchor profile while the anchor is armed; no-op
|
|
343
544
|
* once lifted or when CORTEX_TOOL_ANCHOR is unset. Applied AFTER the
|
|
344
545
|
* deferred filter at both request-assembly sites. */
|
|
@@ -1178,6 +1379,10 @@ export class CortexOrchestrator {
|
|
|
1178
1379
|
// (no reminders, no force-synthesis cap). Runaway protection then rests on
|
|
1179
1380
|
// loop detection, the consecutive-error breaker, and MAX_TOOL_ITERATIONS.
|
|
1180
1381
|
const TOOL_BUDGET_HARD = TOOL_BUDGET_SOFT > 0 ? TOOL_BUDGET_SOFT * 2 : Infinity; // force-synthesis cap
|
|
1382
|
+
// #2 (2026-09-04): per-turn wall-clock budget (opt-in — 0 disables, no behaviour change when unset).
|
|
1383
|
+
const TURN_DEADLINE_MS = loopDefaults.turnDeadlineMs;
|
|
1384
|
+
const loopStartMs = Date.now();
|
|
1385
|
+
let timeWarnFired = false;
|
|
1181
1386
|
let totalToolErrors = 0;
|
|
1182
1387
|
// Round 18b: track whether we've already retried on empty response so
|
|
1183
1388
|
// we don't loop forever. See empty-detection block below.
|
|
@@ -1294,7 +1499,9 @@ export class CortexOrchestrator {
|
|
|
1294
1499
|
// no_visible_content (nothing at all) for observability + a nudge
|
|
1295
1500
|
// tailored to the failure shape. Does NOT change the one-bounded-retry
|
|
1296
1501
|
// decision — only the log + nudge text.
|
|
1297
|
-
|
|
1502
|
+
// D-E (2026-09-04): pass stopReason so a max_tokens truncation is classified 'truncated'
|
|
1503
|
+
// (continue), not 'reasoning_only' (which demands a final answer and just re-truncates).
|
|
1504
|
+
const emptyClass = classifyEmptyResponse(currentAssistantCanonicalMessage.content, convertedResponse.stopReason);
|
|
1298
1505
|
console.warn(`[Orchestrator] Empty response detected (${emptyClass.kind}, hadReasoning=${emptyClass.hadReasoning}, iteration=${toolCallIteration}). ` +
|
|
1299
1506
|
`Retrying once with explicit completion prompt.`);
|
|
1300
1507
|
// R26 (2026-05-15, surfaced by A/B benchmark): the empty assistant
|
|
@@ -1335,7 +1542,7 @@ export class CortexOrchestrator {
|
|
|
1335
1542
|
role: 'user',
|
|
1336
1543
|
content: [{
|
|
1337
1544
|
type: 'text',
|
|
1338
|
-
text: `<system-reminder>${emptyResponseNudge(emptyClass.kind)} Do not call any more tools
|
|
1545
|
+
text: `<system-reminder>${emptyResponseNudge(emptyClass.kind)}${nudgeForbidsTools(emptyClass.kind) ? ' Do not call any more tools.' : ''}</system-reminder>`,
|
|
1339
1546
|
}],
|
|
1340
1547
|
},
|
|
1341
1548
|
timeline: {
|
|
@@ -1874,10 +2081,13 @@ export class CortexOrchestrator {
|
|
|
1874
2081
|
// 0 promotions). The outer timer is the last-resort cap for hung tools: it fires a grace
|
|
1875
2082
|
// period AFTER the tool's own deadline.
|
|
1876
2083
|
const abortController = new AbortController();
|
|
2084
|
+
// D-A (2026-09-04): outer cap must sit ABOVE the model's REQUESTED Bash timeout, not a static
|
|
2085
|
+
// ~150s, or a legit `Bash({ timeout: 250000 })` is killed before ShellTool's own promote-at-deadline.
|
|
2086
|
+
const outerDeadlineMs = resolveOuterToolDeadlineMs(toolUseBlocks, TOOL_TIMEOUT_MS, OUTER_TIMEOUT_GRACE_MS);
|
|
1877
2087
|
const timeoutId = setTimeout(() => {
|
|
1878
|
-
console.warn(`[Orchestrator Phase 2.5] Tool execution timeout after ${
|
|
2088
|
+
console.warn(`[Orchestrator Phase 2.5] Tool execution timeout after ${outerDeadlineMs}ms`);
|
|
1879
2089
|
abortController.abort();
|
|
1880
|
-
},
|
|
2090
|
+
}, outerDeadlineMs);
|
|
1881
2091
|
// Track which tool_use_ids have been processed to avoid duplicates in error handler
|
|
1882
2092
|
const processedToolUseIds = new Set();
|
|
1883
2093
|
try {
|
|
@@ -1995,6 +2205,7 @@ export class CortexOrchestrator {
|
|
|
1995
2205
|
ev.noteToolResults(toolResults);
|
|
1996
2206
|
// Stages 2/4/5 + effort tail — ONE implementation shared with the streaming loop (endTurnGates.ts, 4.91.0).
|
|
1997
2207
|
evaluateEndTurnGates(ev, toolResults, toolUseBlocks, this.gateDeps());
|
|
2208
|
+
await this.adjudicateEndTurn(ev, toolResults, toolUseBlocks, effectiveModel); // endTurnResolver (dark unless CORTEX_ENDTURN_RESOLVER)
|
|
1998
2209
|
const pendingImages = this.collectPendingImages(toolResults, effectiveModel.vision === true);
|
|
1999
2210
|
await this.applyVisionHandoffs();
|
|
2000
2211
|
// Create tool_result messages and add to history
|
|
@@ -2131,8 +2342,14 @@ export class CortexOrchestrator {
|
|
|
2131
2342
|
const progressStalled = isToolProgressStalled(recentToolCalls);
|
|
2132
2343
|
const budgetSignal = computeToolBudgetSignal(effectiveToolBudgetCount, TOOL_BUDGET_SOFT, progressStalled);
|
|
2133
2344
|
const diversityWarning = this.getDiversityWarning(toolCallCounts);
|
|
2134
|
-
|
|
2135
|
-
|
|
2345
|
+
// #2: one-shot wall-clock warning at 90% of the deadline — tell the model to converge + EndTurn.
|
|
2346
|
+
let timeSignal = null;
|
|
2347
|
+
if (!timeWarnFired && timeBudgetState(Date.now() - loopStartMs, TURN_DEADLINE_MS) === 'warn') {
|
|
2348
|
+
timeSignal = timeBudgetWarnNudge(Date.now() - loopStartMs, TURN_DEADLINE_MS);
|
|
2349
|
+
timeWarnFired = true;
|
|
2350
|
+
}
|
|
2351
|
+
if (budgetSignal || diversityWarning || ladderSignal || timeSignal) {
|
|
2352
|
+
const signals = [budgetSignal, diversityWarning, ladderSignal, timeSignal].filter(Boolean).join('\n');
|
|
2136
2353
|
const lastMsg = this.messageHistory[this.messageHistory.length - 1];
|
|
2137
2354
|
if (lastMsg?.message?.content?.[0]?.type === 'tool_result') {
|
|
2138
2355
|
const block = lastMsg.message.content[0];
|
|
@@ -2149,6 +2366,7 @@ export class CortexOrchestrator {
|
|
|
2149
2366
|
budgetSignal ? 'budget' : null,
|
|
2150
2367
|
diversityWarning ? 'diversity' : null,
|
|
2151
2368
|
ladderSignal ? 'ladder' : null,
|
|
2369
|
+
timeSignal ? 'time' : null,
|
|
2152
2370
|
].filter(Boolean);
|
|
2153
2371
|
void store
|
|
2154
2372
|
.recordEvent({
|
|
@@ -2185,6 +2403,23 @@ export class CortexOrchestrator {
|
|
|
2185
2403
|
`recent calls cycling). Forcing synthesis.`);
|
|
2186
2404
|
break;
|
|
2187
2405
|
}
|
|
2406
|
+
// #2 (2026-09-04) wall-clock break — UNCONDITIONAL (unlike R29b's progress-gate): a grinder
|
|
2407
|
+
// making novel-but-non-converging calls never trips the count/stall guards but burns to
|
|
2408
|
+
// MAX_TOOL_ITERATIONS (the loop-killed 24.8M-token tasks that drove the run's real cost). The
|
|
2409
|
+
// 90% warn above gave the model its chance; break now → post-loop R29a synthesis yields a
|
|
2410
|
+
// best-effort answer instead of a zero-deliverable timeout.
|
|
2411
|
+
if (timeBudgetState(Date.now() - loopStartMs, TURN_DEADLINE_MS) === 'break') {
|
|
2412
|
+
console.warn(`[Orchestrator] #2: turn wall-clock deadline reached (${TURN_DEADLINE_MS}ms) — forcing synthesis.`);
|
|
2413
|
+
const store = this.getDecisionStore();
|
|
2414
|
+
if (store) {
|
|
2415
|
+
void store.recordEvent({
|
|
2416
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
2417
|
+
kind: 'time_budget_break',
|
|
2418
|
+
detail: { deadlineMs: TURN_DEADLINE_MS, elapsedMs: Date.now() - loopStartMs, iteration: toolCallIteration },
|
|
2419
|
+
}).catch(() => { });
|
|
2420
|
+
}
|
|
2421
|
+
break;
|
|
2422
|
+
}
|
|
2188
2423
|
// Interleaved thinking: inject subtle reflection between tool iterations (iteration 2+)
|
|
2189
2424
|
if (this.shouldUseInterleavedThinking() && this.helperMiddleware && toolCallIteration > 1) {
|
|
2190
2425
|
try {
|
|
@@ -2236,6 +2471,7 @@ export class CortexOrchestrator {
|
|
|
2236
2471
|
// full tool catalog (no-op unless CORTEX_PROMPT_MASS=defer).
|
|
2237
2472
|
await this.deliverDeferredCorpusAtLift(effectiveModel);
|
|
2238
2473
|
this.deliverLiftNudge(allTools, effectiveModel); // A′ proposal-1: SearchTools/AskForAdvice signpost at lift
|
|
2474
|
+
await this.deliverLiftPlanAtLift(effectiveModel); // LIFT_MENTOR_PLANNER: bounded mentor-planner at lift (dark unless CORTEX_LIFT_PLAN)
|
|
2239
2475
|
if (this.config.debug)
|
|
2240
2476
|
console.log('[Anchor] lifted at first tool_result boundary — session profile applies');
|
|
2241
2477
|
}
|
|
@@ -3292,6 +3528,10 @@ export class CortexOrchestrator {
|
|
|
3292
3528
|
let streamEndTurnNudges = 0;
|
|
3293
3529
|
let pendingGateRequest = false; // Stage-1: a reminder was appended; run one continuation with NO tool batch
|
|
3294
3530
|
const streamEndTurnGateEnabled = process.env.CORTEX_ENDTURN_GATE === 'true';
|
|
3531
|
+
// #2 (2026-09-04): per-turn wall-clock budget (opt-in — 0 disables; streaming parity with non-stream).
|
|
3532
|
+
const TURN_DEADLINE_MS = loopDefaults.turnDeadlineMs;
|
|
3533
|
+
const loopStartMs = Date.now();
|
|
3534
|
+
let timeWarnFired = false;
|
|
3295
3535
|
while (hasToolUse && toolCallIteration < MAX_TOOL_ITERATIONS) {
|
|
3296
3536
|
const gateReRequest = pendingGateRequest;
|
|
3297
3537
|
pendingGateRequest = false;
|
|
@@ -3598,12 +3838,14 @@ export class CortexOrchestrator {
|
|
|
3598
3838
|
// Execute tools with timeout (outer cap fires a grace period AFTER the tool's own deadline —
|
|
3599
3839
|
// see the non-stream site: ShellTool's promote-at-deadline must win the race; 4.90.1)
|
|
3600
3840
|
const abortController = new AbortController();
|
|
3841
|
+
// D-A (2026-09-04): outer cap sits above the requested Bash timeout (see the non-stream site).
|
|
3842
|
+
const outerDeadlineMs = resolveOuterToolDeadlineMs(toolUseBlocks, TOOL_TIMEOUT_MS, OUTER_TIMEOUT_GRACE_MS);
|
|
3601
3843
|
const timeoutId = setTimeout(() => {
|
|
3602
3844
|
if (this.config.debug) {
|
|
3603
|
-
console.warn(`[Orchestrator Streaming] Tool execution timeout after ${
|
|
3845
|
+
console.warn(`[Orchestrator Streaming] Tool execution timeout after ${outerDeadlineMs}ms`);
|
|
3604
3846
|
}
|
|
3605
3847
|
abortController.abort();
|
|
3606
|
-
},
|
|
3848
|
+
}, outerDeadlineMs);
|
|
3607
3849
|
try {
|
|
3608
3850
|
// Execute tools (reuse existing method)
|
|
3609
3851
|
const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState);
|
|
@@ -3612,6 +3854,7 @@ export class CortexOrchestrator {
|
|
|
3612
3854
|
// 4.91.0: EndTurn gates for STREAMING (were sendMessage-only) — same evidence + evaluation.
|
|
3613
3855
|
ev.noteToolResults(toolResults);
|
|
3614
3856
|
evaluateEndTurnGates(ev, toolResults, toolUseBlocks, this.gateDeps());
|
|
3857
|
+
await this.adjudicateEndTurn(ev, toolResults, toolUseBlocks, effectiveModel); // endTurnResolver (dark unless CORTEX_ENDTURN_RESOLVER)
|
|
3615
3858
|
// Unified Outcome Ladder (streaming parity with sendMessage).
|
|
3616
3859
|
ladderSignal = null;
|
|
3617
3860
|
{
|
|
@@ -3805,8 +4048,14 @@ export class CortexOrchestrator {
|
|
|
3805
4048
|
const progressStalled = isToolProgressStalled(allToolCalls);
|
|
3806
4049
|
const budgetSignal = computeToolBudgetSignal(effectiveToolBudgetCount, TOOL_BUDGET_SOFT, progressStalled);
|
|
3807
4050
|
const diversityWarning = this.getDiversityWarning(toolCallCounts);
|
|
3808
|
-
|
|
3809
|
-
|
|
4051
|
+
// #2: one-shot wall-clock warning at 90% of the deadline (streaming parity).
|
|
4052
|
+
let timeSignal = null;
|
|
4053
|
+
if (!timeWarnFired && timeBudgetState(Date.now() - loopStartMs, TURN_DEADLINE_MS) === 'warn') {
|
|
4054
|
+
timeSignal = timeBudgetWarnNudge(Date.now() - loopStartMs, TURN_DEADLINE_MS);
|
|
4055
|
+
timeWarnFired = true;
|
|
4056
|
+
}
|
|
4057
|
+
if (budgetSignal || diversityWarning || ladderSignal || timeSignal) {
|
|
4058
|
+
const signals = [budgetSignal, diversityWarning, ladderSignal, timeSignal].filter(Boolean).join('\n');
|
|
3810
4059
|
const lastMsg = this.messageHistory[this.messageHistory.length - 1];
|
|
3811
4060
|
if (lastMsg?.message?.content?.[0]?.type === 'tool_result') {
|
|
3812
4061
|
const block = lastMsg.message.content[0];
|
|
@@ -3853,6 +4102,22 @@ export class CortexOrchestrator {
|
|
|
3853
4102
|
hasToolUse = false;
|
|
3854
4103
|
break;
|
|
3855
4104
|
}
|
|
4105
|
+
// #2 (2026-09-04) wall-clock break — UNCONDITIONAL (streaming parity; the 90% warn above gave
|
|
4106
|
+
// the model its chance). A grinder making novel-but-non-converging calls never trips the
|
|
4107
|
+
// count/stall guards but burns to MAX_TOOL_ITERATIONS → post-loop synthesis yields best-effort.
|
|
4108
|
+
if (timeBudgetState(Date.now() - loopStartMs, TURN_DEADLINE_MS) === 'break') {
|
|
4109
|
+
console.warn(`[Orchestrator Streaming] #2: turn wall-clock deadline reached (${TURN_DEADLINE_MS}ms) — forcing synthesis.`);
|
|
4110
|
+
const store = this.getDecisionStore();
|
|
4111
|
+
if (store) {
|
|
4112
|
+
void store.recordEvent({
|
|
4113
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
4114
|
+
kind: 'time_budget_break',
|
|
4115
|
+
detail: { deadlineMs: TURN_DEADLINE_MS, elapsedMs: Date.now() - loopStartMs, iteration: toolCallIteration, streaming: true },
|
|
4116
|
+
}).catch(() => { });
|
|
4117
|
+
}
|
|
4118
|
+
hasToolUse = false;
|
|
4119
|
+
break;
|
|
4120
|
+
}
|
|
3856
4121
|
// Interleaved thinking: inject subtle reflection between tool iterations
|
|
3857
4122
|
if (this.shouldUseInterleavedThinking() && this.helperMiddleware && toolCallIteration > 1) {
|
|
3858
4123
|
try {
|
|
@@ -3895,6 +4160,7 @@ export class CortexOrchestrator {
|
|
|
3895
4160
|
// P6 deferral: same one-shot corpus delivery as the sendMessage path.
|
|
3896
4161
|
await this.deliverDeferredCorpusAtLift(effectiveModel);
|
|
3897
4162
|
this.deliverLiftNudge(allTools, effectiveModel); // A′ proposal-1: SearchTools/AskForAdvice signpost at lift
|
|
4163
|
+
await this.deliverLiftPlanAtLift(effectiveModel); // LIFT_MENTOR_PLANNER: bounded mentor-planner at lift (dark unless CORTEX_LIFT_PLAN)
|
|
3898
4164
|
if (this.config.debug)
|
|
3899
4165
|
console.log('[Anchor] lifted at first tool_result boundary — session profile applies');
|
|
3900
4166
|
}
|
|
@@ -4534,6 +4800,8 @@ export class CortexOrchestrator {
|
|
|
4534
4800
|
toolBudgetSoft: Number.isFinite(softRaw) && softRaw >= 0 ? softRaw : 400,
|
|
4535
4801
|
toolTimeoutMs: this.config.loopControl?.toolTimeoutMs ?? 120000,
|
|
4536
4802
|
maxLoopRepetitions: this.config.loopControl?.maxLoopRepetitions ?? 5,
|
|
4803
|
+
// #2 wall-clock deadline: opt-in (0 = disabled), env CORTEX_TURN_DEADLINE_MS is the fallback.
|
|
4804
|
+
turnDeadlineMs: resolveTurnDeadlineMs(this.config.loopControl?.turnDeadlineMs),
|
|
4537
4805
|
};
|
|
4538
4806
|
}
|
|
4539
4807
|
// Item 13c: fire on threshold CROSSINGS only (10, 20, 40… doubling) — the
|