@nexus-cortex/core 4.91.1 → 4.93.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 +296 -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,214 @@ 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 t0 = Date.now();
|
|
398
|
+
const plan = await withTimeout(this.helperMiddleware.generateTaskPlan({
|
|
399
|
+
task: this.lastRealUserText(),
|
|
400
|
+
observations,
|
|
401
|
+
envReport,
|
|
402
|
+
helperModelId: this.config.reactiveMentorship?.helperModelId,
|
|
403
|
+
}), timeoutMs);
|
|
404
|
+
const latencyMs = Date.now() - t0;
|
|
405
|
+
if (!plan || !plan.trim()) {
|
|
406
|
+
if (store)
|
|
407
|
+
void store.recordEvent({
|
|
408
|
+
sessionId, kind: 'lift_plan', detail: { fired: true, planChars: 0, empty: true },
|
|
409
|
+
}).catch(() => { });
|
|
410
|
+
return; // fail-open: proceed without a plan
|
|
411
|
+
}
|
|
412
|
+
const retire = /\bRETIRE\b/i.test(plan);
|
|
413
|
+
const criteriaStated = /criteri|grader|test\.sh|expected|verify/i.test(plan);
|
|
414
|
+
lastMsg.message.content.push({
|
|
415
|
+
type: 'text',
|
|
416
|
+
text: `<system-reminder>\nA senior engineer prepared this PLAN OF ATTACK for the task — follow it. ` +
|
|
417
|
+
`It is anchored to the REAL success criteria (what the task's own grader checks), NOT your own ` +
|
|
418
|
+
`tests. Your own tests are a means, never the finish line.\n\n${plan}\n</system-reminder>`,
|
|
419
|
+
});
|
|
420
|
+
if (store)
|
|
421
|
+
void store.recordEvent({
|
|
422
|
+
sessionId,
|
|
423
|
+
kind: 'lift_plan',
|
|
424
|
+
// OBSERVABILITY (resolver-AB follow-up): bank the plan TEXT + latency, not just counts —
|
|
425
|
+
// so a k=5 run can score plan QUALITY (and read the reasoning behind a RETIRE).
|
|
426
|
+
detail: { fired: true, planChars: plan.length, retire, criteriaStated, latencyMs, planText: plan.slice(0, 4000) },
|
|
427
|
+
}).catch(() => { });
|
|
428
|
+
if (this.config.debug) {
|
|
429
|
+
console.log(`[LiftPlan] plan delivered at lift (${plan.length} chars, retire=${retire}, criteria=${criteriaStated})`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch (e) {
|
|
433
|
+
if (store)
|
|
434
|
+
void store.recordEvent({
|
|
435
|
+
sessionId, kind: 'lift_plan', detail: { fired: true, error: String(e?.message ?? e).slice(0, 120) },
|
|
436
|
+
}).catch(() => { });
|
|
437
|
+
if (this.config.debug)
|
|
438
|
+
console.warn(`[LiftPlan] delivery failed (continuing without): ${e?.message ?? e}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/** Bounded read-only environment recon (ENV_RECON_COMMAND), gathered ONCE and cached — shared by the
|
|
442
|
+
* lift planner and the EndTurn resolver. Fail-open: partial stdout on timeout/non-zero, else empty. */
|
|
443
|
+
gatherEnvReport() {
|
|
444
|
+
if (this.cachedEnvReport !== undefined)
|
|
445
|
+
return this.cachedEnvReport;
|
|
446
|
+
let r = '';
|
|
447
|
+
try {
|
|
448
|
+
r = String(execSync(ENV_RECON_COMMAND, {
|
|
449
|
+
cwd: this.config.projectPath || process.cwd(),
|
|
450
|
+
timeout: resolveLiftPlanConfig().reconTimeoutMs,
|
|
451
|
+
encoding: 'utf8',
|
|
452
|
+
maxBuffer: 512 * 1024,
|
|
453
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
454
|
+
shell: '/bin/sh',
|
|
455
|
+
})).slice(0, 6000);
|
|
456
|
+
}
|
|
457
|
+
catch (re) {
|
|
458
|
+
r = String(re?.stdout ?? '').slice(0, 6000);
|
|
459
|
+
}
|
|
460
|
+
this.cachedEnvReport = r;
|
|
461
|
+
return r;
|
|
462
|
+
}
|
|
463
|
+
/** The most recent assistant message's text (the draft/work product the model just produced). */
|
|
464
|
+
lastAssistantText() {
|
|
465
|
+
for (let i = this.messageHistory.length - 1; i >= 0; i--) {
|
|
466
|
+
const m = this.messageHistory[i];
|
|
467
|
+
if (m?.message?.role !== 'assistant')
|
|
468
|
+
continue;
|
|
469
|
+
const c = m.message.content;
|
|
470
|
+
if (typeof c === 'string')
|
|
471
|
+
return c;
|
|
472
|
+
if (Array.isArray(c))
|
|
473
|
+
return c.filter((b) => b?.type === 'text').map((b) => b.text || '').join('\n');
|
|
474
|
+
return '';
|
|
475
|
+
}
|
|
476
|
+
return '';
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* endTurnResolver (operator design 2026-09-04) — the mentor-as-EndTurn-judge, the finish-side twin of
|
|
480
|
+
* deliverLiftPlanAtLift. Runs AFTER evaluateEndTurnGates: only when the mechanical gate ACCEPTED the
|
|
481
|
+
* EndTurn (ev.endTurnCalled === true). A bounded max-reasoning mentor adjudicates "does the work product
|
|
482
|
+
* meet the task's real requirements?" — MEETS leaves the finish accepted; GAP VETOES it (ev.endTurnCalled
|
|
483
|
+
* = false) and mutates the EndTurn tool_result into the mentor's fix plan (same channel the gate uses), so
|
|
484
|
+
* the narrow-door model executes a plan instead of reasoning its way out of a mechanical rejection.
|
|
485
|
+
* Bounded by CORTEX_ENDTURN_RESOLVER_MAX_REJECTS then fallback-accepts (liveness). No-op when the resolver
|
|
486
|
+
* is unarmed → byte-identical. Fail-open on any error → leaves the finish accepted.
|
|
487
|
+
*/
|
|
488
|
+
async adjudicateEndTurn(ev, toolResults, toolUses, model) {
|
|
489
|
+
if (!resolveEndTurnResolver(process.env, model.endTurnResolver))
|
|
490
|
+
return;
|
|
491
|
+
if (ev.endTurnCalled !== true)
|
|
492
|
+
return; // gate rejected (or no EndTurn) — the resolver only vetoes PASSES
|
|
493
|
+
if (!this.helperMiddleware?.evaluateEndTurn)
|
|
494
|
+
return;
|
|
495
|
+
const task = this.lastRealUserText();
|
|
496
|
+
if (!isTaskShaped(task))
|
|
497
|
+
return; // only adjudicate real work tasks
|
|
498
|
+
const cfg = resolveEndTurnResolverConfig();
|
|
499
|
+
if (this.endTurnResolverRejects >= cfg.maxRejects)
|
|
500
|
+
return; // fallback-accept: liveness beats loops
|
|
501
|
+
const et = toolResults.find((tr) => tr.tool_name === 'EndTurn');
|
|
502
|
+
if (!et)
|
|
503
|
+
return;
|
|
504
|
+
const store = this.getDecisionStore();
|
|
505
|
+
const sessionId = this.currentSessionId ?? 'unknown';
|
|
506
|
+
try {
|
|
507
|
+
const etInput = toolUses.find((t) => t.id === et.tool_use_id || t.name === 'EndTurn')?.input ?? {};
|
|
508
|
+
const outputs = (ev.outputs ?? []).join('\n').slice(0, 4000);
|
|
509
|
+
const workProduct = `${this.lastAssistantText()}\n\n--- checks / tool outputs this task ---\n${outputs}`.slice(0, 8000);
|
|
510
|
+
const attestation = JSON.stringify(etInput).slice(0, 2000);
|
|
511
|
+
const timeoutMs = parseInt(process.env.CORTEX_ENDTURN_RESOLVER_TIMEOUT_MS ?? '90000', 10);
|
|
512
|
+
const t0 = Date.now();
|
|
513
|
+
const text = await withTimeout(this.helperMiddleware.evaluateEndTurn({
|
|
514
|
+
task,
|
|
515
|
+
envReport: this.gatherEnvReport(),
|
|
516
|
+
workProduct,
|
|
517
|
+
attestation,
|
|
518
|
+
helperModelId: this.config.reactiveMentorship?.helperModelId,
|
|
519
|
+
}), timeoutMs);
|
|
520
|
+
const latencyMs = Date.now() - t0;
|
|
521
|
+
const verdict = parseResolverVerdict(text ?? ''); // withTimeout → null on timeout; fail-open to MEETS
|
|
522
|
+
// OBSERVABILITY (resolver-AB follow-up, 2026-09-05): bank the TEXT the resolver produced +
|
|
523
|
+
// what it judged, not just counts — so a k=5 run can score the resolver's JUDGMENT QUALITY
|
|
524
|
+
// (was the GAP warranted? was the fix plan good? did it judge the right artifact?) and not
|
|
525
|
+
// merely that it fired. Bounded caps keep the decisions row sane. See HARNESS_IMPROVEMENT_BACKLOG.
|
|
526
|
+
if (store)
|
|
527
|
+
void store.recordEvent({
|
|
528
|
+
sessionId,
|
|
529
|
+
kind: 'endturn_resolver',
|
|
530
|
+
toolName: 'EndTurn',
|
|
531
|
+
detail: {
|
|
532
|
+
meets: verdict.meets, planChars: verdict.plan.length, rejects: this.endTurnResolverRejects, parsed: verdict.parsed,
|
|
533
|
+
latencyMs, rawLen: (text ?? '').length,
|
|
534
|
+
planText: verdict.plan.slice(0, 4000),
|
|
535
|
+
workProductSample: workProduct.slice(0, 1500),
|
|
536
|
+
attestation: attestation.slice(0, 800),
|
|
537
|
+
},
|
|
538
|
+
}).catch(() => { });
|
|
539
|
+
if (!verdict.meets && verdict.plan) {
|
|
540
|
+
this.endTurnResolverRejects += 1;
|
|
541
|
+
ev.endTurnCalled = false; // VETO the finish
|
|
542
|
+
et.is_error = true;
|
|
543
|
+
et.content =
|
|
544
|
+
`EndTurn HELD (finish review) — the work does not yet meet the task's requirements. Fix plan:\n\n` +
|
|
545
|
+
`${verdict.plan}\n\nDo this, verify against the task's own criteria (not your own tests), then call EndTurn again.`;
|
|
546
|
+
if (this.config.debug)
|
|
547
|
+
console.warn(`[EndTurnResolver] GAP — vetoed finish (${verdict.plan.length}-char plan, reject ${this.endTurnResolverRejects}/${cfg.maxRejects})`);
|
|
548
|
+
}
|
|
549
|
+
else if (this.config.debug) {
|
|
550
|
+
console.log(`[EndTurnResolver] MEETS — finish confirmed (parsed=${verdict.parsed})`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
catch (e) {
|
|
554
|
+
if (store)
|
|
555
|
+
void store.recordEvent({ sessionId, kind: 'endturn_resolver', detail: { error: String(e?.message ?? e).slice(0, 120) } }).catch(() => { });
|
|
556
|
+
// fail-open: leave the finish accepted
|
|
557
|
+
}
|
|
558
|
+
}
|
|
342
559
|
/** Narrow `tools` to the anchor profile while the anchor is armed; no-op
|
|
343
560
|
* once lifted or when CORTEX_TOOL_ANCHOR is unset. Applied AFTER the
|
|
344
561
|
* deferred filter at both request-assembly sites. */
|
|
@@ -1178,6 +1395,10 @@ export class CortexOrchestrator {
|
|
|
1178
1395
|
// (no reminders, no force-synthesis cap). Runaway protection then rests on
|
|
1179
1396
|
// loop detection, the consecutive-error breaker, and MAX_TOOL_ITERATIONS.
|
|
1180
1397
|
const TOOL_BUDGET_HARD = TOOL_BUDGET_SOFT > 0 ? TOOL_BUDGET_SOFT * 2 : Infinity; // force-synthesis cap
|
|
1398
|
+
// #2 (2026-09-04): per-turn wall-clock budget (opt-in — 0 disables, no behaviour change when unset).
|
|
1399
|
+
const TURN_DEADLINE_MS = loopDefaults.turnDeadlineMs;
|
|
1400
|
+
const loopStartMs = Date.now();
|
|
1401
|
+
let timeWarnFired = false;
|
|
1181
1402
|
let totalToolErrors = 0;
|
|
1182
1403
|
// Round 18b: track whether we've already retried on empty response so
|
|
1183
1404
|
// we don't loop forever. See empty-detection block below.
|
|
@@ -1294,7 +1515,9 @@ export class CortexOrchestrator {
|
|
|
1294
1515
|
// no_visible_content (nothing at all) for observability + a nudge
|
|
1295
1516
|
// tailored to the failure shape. Does NOT change the one-bounded-retry
|
|
1296
1517
|
// decision — only the log + nudge text.
|
|
1297
|
-
|
|
1518
|
+
// D-E (2026-09-04): pass stopReason so a max_tokens truncation is classified 'truncated'
|
|
1519
|
+
// (continue), not 'reasoning_only' (which demands a final answer and just re-truncates).
|
|
1520
|
+
const emptyClass = classifyEmptyResponse(currentAssistantCanonicalMessage.content, convertedResponse.stopReason);
|
|
1298
1521
|
console.warn(`[Orchestrator] Empty response detected (${emptyClass.kind}, hadReasoning=${emptyClass.hadReasoning}, iteration=${toolCallIteration}). ` +
|
|
1299
1522
|
`Retrying once with explicit completion prompt.`);
|
|
1300
1523
|
// R26 (2026-05-15, surfaced by A/B benchmark): the empty assistant
|
|
@@ -1335,7 +1558,7 @@ export class CortexOrchestrator {
|
|
|
1335
1558
|
role: 'user',
|
|
1336
1559
|
content: [{
|
|
1337
1560
|
type: 'text',
|
|
1338
|
-
text: `<system-reminder>${emptyResponseNudge(emptyClass.kind)} Do not call any more tools
|
|
1561
|
+
text: `<system-reminder>${emptyResponseNudge(emptyClass.kind)}${nudgeForbidsTools(emptyClass.kind) ? ' Do not call any more tools.' : ''}</system-reminder>`,
|
|
1339
1562
|
}],
|
|
1340
1563
|
},
|
|
1341
1564
|
timeline: {
|
|
@@ -1874,10 +2097,13 @@ export class CortexOrchestrator {
|
|
|
1874
2097
|
// 0 promotions). The outer timer is the last-resort cap for hung tools: it fires a grace
|
|
1875
2098
|
// period AFTER the tool's own deadline.
|
|
1876
2099
|
const abortController = new AbortController();
|
|
2100
|
+
// D-A (2026-09-04): outer cap must sit ABOVE the model's REQUESTED Bash timeout, not a static
|
|
2101
|
+
// ~150s, or a legit `Bash({ timeout: 250000 })` is killed before ShellTool's own promote-at-deadline.
|
|
2102
|
+
const outerDeadlineMs = resolveOuterToolDeadlineMs(toolUseBlocks, TOOL_TIMEOUT_MS, OUTER_TIMEOUT_GRACE_MS);
|
|
1877
2103
|
const timeoutId = setTimeout(() => {
|
|
1878
|
-
console.warn(`[Orchestrator Phase 2.5] Tool execution timeout after ${
|
|
2104
|
+
console.warn(`[Orchestrator Phase 2.5] Tool execution timeout after ${outerDeadlineMs}ms`);
|
|
1879
2105
|
abortController.abort();
|
|
1880
|
-
},
|
|
2106
|
+
}, outerDeadlineMs);
|
|
1881
2107
|
// Track which tool_use_ids have been processed to avoid duplicates in error handler
|
|
1882
2108
|
const processedToolUseIds = new Set();
|
|
1883
2109
|
try {
|
|
@@ -1995,6 +2221,7 @@ export class CortexOrchestrator {
|
|
|
1995
2221
|
ev.noteToolResults(toolResults);
|
|
1996
2222
|
// Stages 2/4/5 + effort tail — ONE implementation shared with the streaming loop (endTurnGates.ts, 4.91.0).
|
|
1997
2223
|
evaluateEndTurnGates(ev, toolResults, toolUseBlocks, this.gateDeps());
|
|
2224
|
+
await this.adjudicateEndTurn(ev, toolResults, toolUseBlocks, effectiveModel); // endTurnResolver (dark unless CORTEX_ENDTURN_RESOLVER)
|
|
1998
2225
|
const pendingImages = this.collectPendingImages(toolResults, effectiveModel.vision === true);
|
|
1999
2226
|
await this.applyVisionHandoffs();
|
|
2000
2227
|
// Create tool_result messages and add to history
|
|
@@ -2131,8 +2358,14 @@ export class CortexOrchestrator {
|
|
|
2131
2358
|
const progressStalled = isToolProgressStalled(recentToolCalls);
|
|
2132
2359
|
const budgetSignal = computeToolBudgetSignal(effectiveToolBudgetCount, TOOL_BUDGET_SOFT, progressStalled);
|
|
2133
2360
|
const diversityWarning = this.getDiversityWarning(toolCallCounts);
|
|
2134
|
-
|
|
2135
|
-
|
|
2361
|
+
// #2: one-shot wall-clock warning at 90% of the deadline — tell the model to converge + EndTurn.
|
|
2362
|
+
let timeSignal = null;
|
|
2363
|
+
if (!timeWarnFired && timeBudgetState(Date.now() - loopStartMs, TURN_DEADLINE_MS) === 'warn') {
|
|
2364
|
+
timeSignal = timeBudgetWarnNudge(Date.now() - loopStartMs, TURN_DEADLINE_MS);
|
|
2365
|
+
timeWarnFired = true;
|
|
2366
|
+
}
|
|
2367
|
+
if (budgetSignal || diversityWarning || ladderSignal || timeSignal) {
|
|
2368
|
+
const signals = [budgetSignal, diversityWarning, ladderSignal, timeSignal].filter(Boolean).join('\n');
|
|
2136
2369
|
const lastMsg = this.messageHistory[this.messageHistory.length - 1];
|
|
2137
2370
|
if (lastMsg?.message?.content?.[0]?.type === 'tool_result') {
|
|
2138
2371
|
const block = lastMsg.message.content[0];
|
|
@@ -2149,6 +2382,7 @@ export class CortexOrchestrator {
|
|
|
2149
2382
|
budgetSignal ? 'budget' : null,
|
|
2150
2383
|
diversityWarning ? 'diversity' : null,
|
|
2151
2384
|
ladderSignal ? 'ladder' : null,
|
|
2385
|
+
timeSignal ? 'time' : null,
|
|
2152
2386
|
].filter(Boolean);
|
|
2153
2387
|
void store
|
|
2154
2388
|
.recordEvent({
|
|
@@ -2185,6 +2419,23 @@ export class CortexOrchestrator {
|
|
|
2185
2419
|
`recent calls cycling). Forcing synthesis.`);
|
|
2186
2420
|
break;
|
|
2187
2421
|
}
|
|
2422
|
+
// #2 (2026-09-04) wall-clock break — UNCONDITIONAL (unlike R29b's progress-gate): a grinder
|
|
2423
|
+
// making novel-but-non-converging calls never trips the count/stall guards but burns to
|
|
2424
|
+
// MAX_TOOL_ITERATIONS (the loop-killed 24.8M-token tasks that drove the run's real cost). The
|
|
2425
|
+
// 90% warn above gave the model its chance; break now → post-loop R29a synthesis yields a
|
|
2426
|
+
// best-effort answer instead of a zero-deliverable timeout.
|
|
2427
|
+
if (timeBudgetState(Date.now() - loopStartMs, TURN_DEADLINE_MS) === 'break') {
|
|
2428
|
+
console.warn(`[Orchestrator] #2: turn wall-clock deadline reached (${TURN_DEADLINE_MS}ms) — forcing synthesis.`);
|
|
2429
|
+
const store = this.getDecisionStore();
|
|
2430
|
+
if (store) {
|
|
2431
|
+
void store.recordEvent({
|
|
2432
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
2433
|
+
kind: 'time_budget_break',
|
|
2434
|
+
detail: { deadlineMs: TURN_DEADLINE_MS, elapsedMs: Date.now() - loopStartMs, iteration: toolCallIteration },
|
|
2435
|
+
}).catch(() => { });
|
|
2436
|
+
}
|
|
2437
|
+
break;
|
|
2438
|
+
}
|
|
2188
2439
|
// Interleaved thinking: inject subtle reflection between tool iterations (iteration 2+)
|
|
2189
2440
|
if (this.shouldUseInterleavedThinking() && this.helperMiddleware && toolCallIteration > 1) {
|
|
2190
2441
|
try {
|
|
@@ -2236,6 +2487,7 @@ export class CortexOrchestrator {
|
|
|
2236
2487
|
// full tool catalog (no-op unless CORTEX_PROMPT_MASS=defer).
|
|
2237
2488
|
await this.deliverDeferredCorpusAtLift(effectiveModel);
|
|
2238
2489
|
this.deliverLiftNudge(allTools, effectiveModel); // A′ proposal-1: SearchTools/AskForAdvice signpost at lift
|
|
2490
|
+
await this.deliverLiftPlanAtLift(effectiveModel); // LIFT_MENTOR_PLANNER: bounded mentor-planner at lift (dark unless CORTEX_LIFT_PLAN)
|
|
2239
2491
|
if (this.config.debug)
|
|
2240
2492
|
console.log('[Anchor] lifted at first tool_result boundary — session profile applies');
|
|
2241
2493
|
}
|
|
@@ -3292,6 +3544,10 @@ export class CortexOrchestrator {
|
|
|
3292
3544
|
let streamEndTurnNudges = 0;
|
|
3293
3545
|
let pendingGateRequest = false; // Stage-1: a reminder was appended; run one continuation with NO tool batch
|
|
3294
3546
|
const streamEndTurnGateEnabled = process.env.CORTEX_ENDTURN_GATE === 'true';
|
|
3547
|
+
// #2 (2026-09-04): per-turn wall-clock budget (opt-in — 0 disables; streaming parity with non-stream).
|
|
3548
|
+
const TURN_DEADLINE_MS = loopDefaults.turnDeadlineMs;
|
|
3549
|
+
const loopStartMs = Date.now();
|
|
3550
|
+
let timeWarnFired = false;
|
|
3295
3551
|
while (hasToolUse && toolCallIteration < MAX_TOOL_ITERATIONS) {
|
|
3296
3552
|
const gateReRequest = pendingGateRequest;
|
|
3297
3553
|
pendingGateRequest = false;
|
|
@@ -3598,12 +3854,14 @@ export class CortexOrchestrator {
|
|
|
3598
3854
|
// Execute tools with timeout (outer cap fires a grace period AFTER the tool's own deadline —
|
|
3599
3855
|
// see the non-stream site: ShellTool's promote-at-deadline must win the race; 4.90.1)
|
|
3600
3856
|
const abortController = new AbortController();
|
|
3857
|
+
// D-A (2026-09-04): outer cap sits above the requested Bash timeout (see the non-stream site).
|
|
3858
|
+
const outerDeadlineMs = resolveOuterToolDeadlineMs(toolUseBlocks, TOOL_TIMEOUT_MS, OUTER_TIMEOUT_GRACE_MS);
|
|
3601
3859
|
const timeoutId = setTimeout(() => {
|
|
3602
3860
|
if (this.config.debug) {
|
|
3603
|
-
console.warn(`[Orchestrator Streaming] Tool execution timeout after ${
|
|
3861
|
+
console.warn(`[Orchestrator Streaming] Tool execution timeout after ${outerDeadlineMs}ms`);
|
|
3604
3862
|
}
|
|
3605
3863
|
abortController.abort();
|
|
3606
|
-
},
|
|
3864
|
+
}, outerDeadlineMs);
|
|
3607
3865
|
try {
|
|
3608
3866
|
// Execute tools (reuse existing method)
|
|
3609
3867
|
const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState);
|
|
@@ -3612,6 +3870,7 @@ export class CortexOrchestrator {
|
|
|
3612
3870
|
// 4.91.0: EndTurn gates for STREAMING (were sendMessage-only) — same evidence + evaluation.
|
|
3613
3871
|
ev.noteToolResults(toolResults);
|
|
3614
3872
|
evaluateEndTurnGates(ev, toolResults, toolUseBlocks, this.gateDeps());
|
|
3873
|
+
await this.adjudicateEndTurn(ev, toolResults, toolUseBlocks, effectiveModel); // endTurnResolver (dark unless CORTEX_ENDTURN_RESOLVER)
|
|
3615
3874
|
// Unified Outcome Ladder (streaming parity with sendMessage).
|
|
3616
3875
|
ladderSignal = null;
|
|
3617
3876
|
{
|
|
@@ -3805,8 +4064,14 @@ export class CortexOrchestrator {
|
|
|
3805
4064
|
const progressStalled = isToolProgressStalled(allToolCalls);
|
|
3806
4065
|
const budgetSignal = computeToolBudgetSignal(effectiveToolBudgetCount, TOOL_BUDGET_SOFT, progressStalled);
|
|
3807
4066
|
const diversityWarning = this.getDiversityWarning(toolCallCounts);
|
|
3808
|
-
|
|
3809
|
-
|
|
4067
|
+
// #2: one-shot wall-clock warning at 90% of the deadline (streaming parity).
|
|
4068
|
+
let timeSignal = null;
|
|
4069
|
+
if (!timeWarnFired && timeBudgetState(Date.now() - loopStartMs, TURN_DEADLINE_MS) === 'warn') {
|
|
4070
|
+
timeSignal = timeBudgetWarnNudge(Date.now() - loopStartMs, TURN_DEADLINE_MS);
|
|
4071
|
+
timeWarnFired = true;
|
|
4072
|
+
}
|
|
4073
|
+
if (budgetSignal || diversityWarning || ladderSignal || timeSignal) {
|
|
4074
|
+
const signals = [budgetSignal, diversityWarning, ladderSignal, timeSignal].filter(Boolean).join('\n');
|
|
3810
4075
|
const lastMsg = this.messageHistory[this.messageHistory.length - 1];
|
|
3811
4076
|
if (lastMsg?.message?.content?.[0]?.type === 'tool_result') {
|
|
3812
4077
|
const block = lastMsg.message.content[0];
|
|
@@ -3853,6 +4118,22 @@ export class CortexOrchestrator {
|
|
|
3853
4118
|
hasToolUse = false;
|
|
3854
4119
|
break;
|
|
3855
4120
|
}
|
|
4121
|
+
// #2 (2026-09-04) wall-clock break — UNCONDITIONAL (streaming parity; the 90% warn above gave
|
|
4122
|
+
// the model its chance). A grinder making novel-but-non-converging calls never trips the
|
|
4123
|
+
// count/stall guards but burns to MAX_TOOL_ITERATIONS → post-loop synthesis yields best-effort.
|
|
4124
|
+
if (timeBudgetState(Date.now() - loopStartMs, TURN_DEADLINE_MS) === 'break') {
|
|
4125
|
+
console.warn(`[Orchestrator Streaming] #2: turn wall-clock deadline reached (${TURN_DEADLINE_MS}ms) — forcing synthesis.`);
|
|
4126
|
+
const store = this.getDecisionStore();
|
|
4127
|
+
if (store) {
|
|
4128
|
+
void store.recordEvent({
|
|
4129
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
4130
|
+
kind: 'time_budget_break',
|
|
4131
|
+
detail: { deadlineMs: TURN_DEADLINE_MS, elapsedMs: Date.now() - loopStartMs, iteration: toolCallIteration, streaming: true },
|
|
4132
|
+
}).catch(() => { });
|
|
4133
|
+
}
|
|
4134
|
+
hasToolUse = false;
|
|
4135
|
+
break;
|
|
4136
|
+
}
|
|
3856
4137
|
// Interleaved thinking: inject subtle reflection between tool iterations
|
|
3857
4138
|
if (this.shouldUseInterleavedThinking() && this.helperMiddleware && toolCallIteration > 1) {
|
|
3858
4139
|
try {
|
|
@@ -3895,6 +4176,7 @@ export class CortexOrchestrator {
|
|
|
3895
4176
|
// P6 deferral: same one-shot corpus delivery as the sendMessage path.
|
|
3896
4177
|
await this.deliverDeferredCorpusAtLift(effectiveModel);
|
|
3897
4178
|
this.deliverLiftNudge(allTools, effectiveModel); // A′ proposal-1: SearchTools/AskForAdvice signpost at lift
|
|
4179
|
+
await this.deliverLiftPlanAtLift(effectiveModel); // LIFT_MENTOR_PLANNER: bounded mentor-planner at lift (dark unless CORTEX_LIFT_PLAN)
|
|
3898
4180
|
if (this.config.debug)
|
|
3899
4181
|
console.log('[Anchor] lifted at first tool_result boundary — session profile applies');
|
|
3900
4182
|
}
|
|
@@ -4534,6 +4816,8 @@ export class CortexOrchestrator {
|
|
|
4534
4816
|
toolBudgetSoft: Number.isFinite(softRaw) && softRaw >= 0 ? softRaw : 400,
|
|
4535
4817
|
toolTimeoutMs: this.config.loopControl?.toolTimeoutMs ?? 120000,
|
|
4536
4818
|
maxLoopRepetitions: this.config.loopControl?.maxLoopRepetitions ?? 5,
|
|
4819
|
+
// #2 wall-clock deadline: opt-in (0 = disabled), env CORTEX_TURN_DEADLINE_MS is the fallback.
|
|
4820
|
+
turnDeadlineMs: resolveTurnDeadlineMs(this.config.loopControl?.turnDeadlineMs),
|
|
4537
4821
|
};
|
|
4538
4822
|
}
|
|
4539
4823
|
// Item 13c: fire on threshold CROSSINGS only (10, 20, 40… doubling) — the
|