@pentoshi/clai 3.11.6 → 3.11.8

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.
@@ -58,12 +58,13 @@ import { buildDurableEnvelope, WorkLedger, } from "./durable-envelope.js";
58
58
  import { COMPACTION_SYSTEM_PROMPT, } from "./compaction-summary.js";
59
59
  import { maybeAppendPlanModeReminder, PLAN_REMINDER_TOAST, } from "./plan-mode-reminders.js";
60
60
  import { LoopGuard } from "./loop-guard.js";
61
+ import { appendInterruptedReasoning, interruptedReasoningBrief, isMeaningfulResumptionYield, } from "./interrupted-reasoning.js";
61
62
  import { CompactionAttemptLedger, compactionAttemptKey, } from "./compaction-attempt.js";
62
63
  import { resolveRequestBudget } from "./request-budget.js";
63
64
  import { loadPlan, mutatePlan, markTask, appendPlanTask, readyPlanTasks, foregroundRemaining, responderOpenTasks, isPlanTerminal, isPlanSuccessful, } from "../store/plan.js";
64
65
  import { stat } from "node:fs/promises";
65
66
  import { isOutsideWorkingDirectory, resolveFsToolPath, } from "../tools/fs.js";
66
- import { stripSentinelTokens, parseToolCall, recognizeBareToolJson, looksLikeTruncatedToolCall, salvageTruncatedWrite, salvageTruncatedWriteFromNative, countToolFences, parseAllToolCalls, groupToolCallsForExecution, buildTurnHistory, collapseRepeatedText, textBeforeToolCall, formatToolArgs, looksLikePentestTask, looksLikeBuildTask, looksLikeInformationalQuery, looksLikeIdleOrSocialPrompt, looksLikeActionNarration, looksLikeWebActionNarration, looksLikePlanNarration, looksLikeErrorDiagnosisWithFixIntent, localHttpProbeIsFailure, localHttpProbeIsSuccess, requiresFreshWebSearch, freshnessGuardMessage, buildWorkflowDirective, narrowNmapOperationDirective, pentestWorkflowDirective, pentestNoLocalServerDirective, shouldDimToolChatter, looksLikePromptLeak, } from "./tool-call-parser.js";
67
+ import { stripSentinelTokens, parseToolCall, recognizeBareToolJson, looksLikeTruncatedToolCall, salvageTruncatedWrite, salvageTruncatedWriteFromNative, countToolFences, parseAllToolCalls, groupToolCallsForExecution, buildTurnHistory, collapseRepeatedText, textBeforeToolCall, formatToolArgs, looksLikePentestTask, looksLikeBuildTask, looksLikeInformationalQuery, looksLikeIdleOrSocialPrompt, looksLikeActionNarration, looksLikeWebActionNarration, localHttpProbeIsFailure, localHttpProbeIsSuccess, requiresFreshWebSearch, freshnessGuardMessage, buildWorkflowDirective, narrowNmapOperationDirective, pentestWorkflowDirective, pentestNoLocalServerDirective, shouldDimToolChatter, looksLikePromptLeak, } from "./tool-call-parser.js";
67
68
  import { createSessionPolicy, isPreApprovalAllowedTool, isPlanModeAllowedShellCommand, isPlanModeAllowedTool, isPlanApprovedByStatus, planHasOpenWork, isAbortError, shouldEnableImageOcr, } from "./session-policy.js";
68
69
  import { saveToolOutput, summarizeOutput, formatToolContext, } from "./tool-output-formatting.js";
69
70
  import { codingSessionFromContext, isProtocolPlaceholderOutput, progressPauseMode, } from "./progress-pause-policy.js";
@@ -74,7 +75,8 @@ import { absorbLooseWorkIntoLedger, applyDestinationCwd, canMarkTaskDone, hasLoc
74
75
  import { buildSessionStateBlock, inferNextHint, upsertSessionStateMessage, } from "./session-state.js";
75
76
  import { buildContinueOrientation, looksLikeContinueOrResumePrompt, } from "./continue-orient.js";
76
77
  import { detectPackageManager } from "./workspace-orient.js";
77
- import { budgetRemaining, consumeBudget, createRecoveryBudgets, freestyleClaimsAppReady, looksLikeShallowPentestReport, recoveryForErrorDiagnosis, recoveryForFailedProbe, recoveryForFreshness, recoveryForMissingFeature, recoveryForMissingPlan, recoveryForNarration, recoveryForPrematureComplete, recoveryForRuntimeVerify, recoveryForShallowPentest, } from "./must-continue.js";
78
+ import { budgetRemaining, consumeBudget, createRecoveryBudgets, } from "./must-continue.js";
79
+ import { chooseFinalizeRecovery } from "./finalize-gate.js";
78
80
  import { outOfScopeToolMessage, scopeContextMessage } from "./scope-context.js";
79
81
  import { EngagementPolicyEngine, actionFromUrl, engagementActionsForToolCall, evaluateEngagementAction, } from "../safety/engagement-policy.js";
80
82
  import { patchPlanMeta } from "../store/plan.js";
@@ -169,6 +171,8 @@ export async function runAgentTurn(prompt, options = {}) {
169
171
  // at the top of every loop iteration.
170
172
  let visibleCommitted = false;
171
173
  let interruptedVisible = "";
174
+ let interruptedReasoning = "";
175
+ let lowYieldResumptions = 0;
172
176
  const trimExactContinuationOverlap = (previous, current, minLength = 32) => {
173
177
  if (previous.length > 0 && current.startsWith(previous)) {
174
178
  return current.slice(previous.length);
@@ -540,6 +544,9 @@ export async function runAgentTurn(prompt, options = {}) {
540
544
  // Request, project, workspace, recovery, scope, and plan state are appended
541
545
  // later as system-marked turns so a changing byte cannot invalidate the
542
546
  // constitution (and, on Anthropic, the native tool schemas before it).
547
+ // The red-team methodology block is ~940 tokens on every request. Attach it
548
+ // only when this turn is actually a remote-security engagement.
549
+ const pentestPromptTurn = pentestLikeTurn || activePlan?.kind === "pentest";
543
550
  const buildStableSystemContent = (native) => {
544
551
  const reliability = getReliabilityPolicy();
545
552
  const visionAvailable = modelSupportsVision(provider, model);
@@ -551,6 +558,7 @@ export async function runAgentTurn(prompt, options = {}) {
551
558
  imageView: visionAvailable,
552
559
  // E6: slim native constitution when API tool schemas are attached.
553
560
  ...(native ? { slimNative: reliability.slimNativePrompt } : {}),
561
+ ...(pentestPromptTurn ? { pentest: true } : {}),
554
562
  });
555
563
  };
556
564
  const systemSections = [renderRequestEnvironmentContext()];
@@ -3095,6 +3103,30 @@ export async function runAgentTurn(prompt, options = {}) {
3095
3103
  let accumulatedText = "";
3096
3104
  const callIds = [];
3097
3105
  let streamedCallsCount = 0;
3106
+ // A model can think silently for minutes. Without a heartbeat the UI
3107
+ // shows a frozen label and the turn looks hung, so surface elapsed time
3108
+ // and the current phase on a timer rather than only on token arrival.
3109
+ const streamStartedAt = Date.now();
3110
+ const streamPhase = () => {
3111
+ const seconds = Math.round((Date.now() - streamStartedAt) / 1000);
3112
+ const elapsed = seconds < 60
3113
+ ? `${seconds}s`
3114
+ : `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, "0")}s`;
3115
+ if (generatedTokens > 0 && !inThinking) {
3116
+ return `generating response · ${generatedTokens} tokens · ${elapsed}`;
3117
+ }
3118
+ if (sawReasoning)
3119
+ return `thinking · ${elapsed}`;
3120
+ return `waiting for model · ${elapsed}`;
3121
+ };
3122
+ const heartbeat = setInterval(() => {
3123
+ const text = streamPhase();
3124
+ if (writesDirectly)
3125
+ spinner.setLabel(text);
3126
+ else
3127
+ emit({ type: "status", text });
3128
+ }, 10_000);
3129
+ heartbeat.unref?.();
3098
3130
  const deferredToolCalls = [];
3099
3131
  const deltaParser = writesDirectly
3100
3132
  ? undefined
@@ -3336,6 +3368,7 @@ export async function runAgentTurn(prompt, options = {}) {
3336
3368
  resetStreamRecoveryState(recoveryState);
3337
3369
  allowModelFallback = false;
3338
3370
  preferModelFallback = false;
3371
+ lowYieldResumptions = 0;
3339
3372
  }
3340
3373
  catch (streamError) {
3341
3374
  // User cancelled (double-Esc) — never try to recover, just stop.
@@ -3363,47 +3396,89 @@ export async function runAgentTurn(prompt, options = {}) {
3363
3396
  // We only rethrow (stop the turn) in the worst case: every approach
3364
3397
  // for that failure class is exhausted or the total budget is spent.
3365
3398
  const failureKind = classifyStreamFailure(streamError);
3399
+ const partialStream = streamAlreadyEmitted(streamError) || accumulatedText.length > 0;
3400
+ const partial = rememberThinkingFromText(accumulatedText);
3401
+ const rawPartialVisible = partialStream
3402
+ ? textBeforeToolCall(stripThinking(collapseRepeatedText(accumulatedText)).visible)
3403
+ : "";
3404
+ const normalizedPartialVisible = trimExactContinuationOverlap(interruptedVisible, rawPartialVisible);
3405
+ const partialVisible = normalizedPartialVisible.trim();
3406
+ // A route that drops after a handful of characters is not making
3407
+ // progress, however many times it is retried. Only a substantial
3408
+ // yield unlocks the generous resumption budget; anything less is
3409
+ // charged to the failure class and escalates to another route.
3410
+ const meaningfulProgress = partialStream &&
3411
+ isMeaningfulResumptionYield(normalizedPartialVisible.length + partial.thinkContent.length);
3412
+ if (partialStream) {
3413
+ lowYieldResumptions = meaningfulProgress
3414
+ ? 0
3415
+ : lowYieldResumptions + 1;
3416
+ }
3366
3417
  const plan = planStreamRecovery({
3367
3418
  kind: failureKind,
3368
3419
  state: recoveryState,
3420
+ progressed: meaningfulProgress,
3369
3421
  });
3370
- const partialStream = streamAlreadyEmitted(streamError) || accumulatedText.length > 0;
3422
+ const terminalFailure = plan.action === "give-up";
3371
3423
  let continuationNudge = "";
3372
3424
  if (partialStream) {
3373
3425
  spinner.stop();
3374
3426
  deltaParser?.finish();
3375
- const partial = rememberThinkingFromText(accumulatedText);
3376
3427
  const hasShownToolCall = deferredToolCalls.some((entry) => entry.shown);
3377
- const rawPartialVisible = textBeforeToolCall(stripThinking(collapseRepeatedText(accumulatedText)).visible);
3378
- const normalizedPartialVisible = trimExactContinuationOverlap(interruptedVisible, rawPartialVisible);
3379
- const partialVisible = normalizedPartialVisible.trim();
3380
3428
  if (partialVisible) {
3381
- writeAssistantMessage(partialVisible);
3382
- pushAssistantHistory(partialVisible);
3429
+ // Finalizing here would close the streaming card and split one
3430
+ // answer across a card per interruption. Keep it open and let
3431
+ // the single commit below paint the stitched text; only a
3432
+ // terminal failure has to flush it now.
3433
+ if (terminalFailure) {
3434
+ writeAssistantMessage(interruptedVisible + normalizedPartialVisible);
3435
+ }
3436
+ else {
3437
+ visibleCommitted = true;
3438
+ }
3439
+ messages.push({
3440
+ role: "assistant",
3441
+ content: sanitizeAssistantText(partialVisible),
3442
+ });
3383
3443
  interruptedVisible += normalizedPartialVisible;
3384
3444
  }
3385
- else if (!writesDirectly) {
3445
+ else if (terminalFailure && !writesDirectly) {
3386
3446
  emit({ type: "assistant-message", text: "" });
3387
3447
  }
3388
3448
  if (partial.hasThinking && !hasShownToolCall) {
3389
3449
  writeThinkingBlock(partial.thinkContent);
3390
3450
  }
3451
+ if (partial.hasThinking) {
3452
+ interruptedReasoning = appendInterruptedReasoning(interruptedReasoning, partial.thinkContent);
3453
+ }
3391
3454
  for (const deferred of deferredToolCalls) {
3392
3455
  if (!deferred.shown || deferred.call.name === "…")
3393
3456
  continue;
3394
3457
  writeToolBlocked(deferred.eventId, deferred.call.name, "Incomplete tool call discarded after the provider stream was interrupted.", chalk.yellow(" ⚠ incomplete tool call discarded after stream interruption\n"));
3395
3458
  }
3396
- continuationNudge =
3397
- "The provider stream was interrupted after partial output. Continue from the exact stopping point without repeating prior text. Any incomplete tool call was discarded and must be reissued in full.";
3398
- const restartNotice = plan.action === "give-up"
3459
+ continuationNudge = [
3460
+ partialVisible
3461
+ ? "The provider stream was interrupted after partial output. Continue from the exact stopping point without repeating prior text. Any incomplete tool call was discarded and must be reissued in full."
3462
+ : "The provider stream was interrupted before any answer was produced. Any incomplete tool call was discarded and must be reissued in full. Do not restart your analysis from the beginning.",
3463
+ interruptedReasoningBrief(interruptedReasoning),
3464
+ ]
3465
+ .filter((part) => Boolean(part))
3466
+ .join("\n\n");
3467
+ const restartNotice = terminalFailure
3399
3468
  ? "partial response preserved before terminal provider failure"
3400
- : "partial response preserved — resuming from the interruption";
3469
+ : lowYieldResumptions > 1
3470
+ ? `route is dropping after almost no output (${lowYieldResumptions} in a row) — switching model`
3471
+ : "partial response preserved — resuming from the interruption";
3401
3472
  writeNotice("warn", restartNotice, chalk.yellow(` ⚠ ${restartNotice}\n`));
3402
3473
  }
3403
- if (plan.action === "give-up") {
3474
+ if (terminalFailure) {
3404
3475
  throw streamError;
3405
3476
  }
3406
- recordRecoveryAttempt(recoveryState, failureKind);
3477
+ recordRecoveryAttempt(recoveryState, failureKind, meaningfulProgress);
3478
+ if (lowYieldResumptions > 1) {
3479
+ allowModelFallback = true;
3480
+ preferModelFallback = true;
3481
+ }
3407
3482
  if (plan.notice) {
3408
3483
  writeNotice("warn", plan.notice, chalk.yellow(` ⚠ ${plan.notice}\n`));
3409
3484
  }
@@ -3434,6 +3509,7 @@ export async function runAgentTurn(prompt, options = {}) {
3434
3509
  }
3435
3510
  finally {
3436
3511
  // Always clear the spinner — abort, network error, or success.
3512
+ clearInterval(heartbeat);
3437
3513
  spinner.stop();
3438
3514
  }
3439
3515
  if (responderDelivery) {
@@ -3487,7 +3563,7 @@ export async function runAgentTurn(prompt, options = {}) {
3487
3563
  const commitAssistantRetry = (historyText) => {
3488
3564
  const hasShownToolCall = deferredToolCalls.some((entry) => entry.shown);
3489
3565
  if (!hasShownToolCall) {
3490
- const displayText = textBeforeToolCall(collapseRepeatedText(assistantText.visible)).trim();
3566
+ const displayText = textBeforeToolCall(collapseRepeatedText(canonicalAssistantVisible)).trim();
3491
3567
  if (displayText) {
3492
3568
  writeAssistantMessage(displayText);
3493
3569
  }
@@ -3503,6 +3579,8 @@ export async function runAgentTurn(prompt, options = {}) {
3503
3579
  }
3504
3580
  pushAssistantHistory(historyText);
3505
3581
  interruptedVisible = "";
3582
+ interruptedReasoning = "";
3583
+ lowYieldResumptions = 0;
3506
3584
  };
3507
3585
  // Only emit a thinking-block event when the classic renderer is
3508
3586
  // active (writesDirectly / no deltaParser). In TUI v2 the
@@ -3949,182 +4027,61 @@ export async function runAgentTurn(prompt, options = {}) {
3949
4027
  "If analysis is incomplete, call only the bounded evidence tool needed now. If each result has been analyzed and is satisfactory, you MUST call job.read with its jobId or exact notificationId before giving a final response. job.read does not require an active plan; do not create or update a plan merely to acknowledge a result."));
3950
4028
  continue;
3951
4029
  }
3952
- const planNarrated = (buildLikeTurn || pentestLikeTurn) &&
3953
- !activePlan &&
3954
- looksLikePlanNarration(cleaned);
3955
- // Only force "diagnosed but not fixed" when the model is still
3956
- // narrating a fix without having applied one this turn. Post-fix
3957
- // summaries ("I've fixed…", build passed) must never re-enter.
3958
- const errorFixNarration = !sawSuccessfulMutation &&
3959
- looksLikeErrorDiagnosisWithFixIntent(cleaned);
3960
- const shouldRetryBeforeFinalizing = productiveSteps === 0 ||
3961
- planNarrated ||
3962
- ((narratedAction || narratedWebAction) && !informationalQuery) ||
3963
- (session.planApproved.value &&
3964
- planHasOpenWorkNow &&
3965
- (narratedAction || errorFixNarration)) ||
3966
- // errorFix only when no mutation yet (gate is in errorFixNarration)
3967
- (session.planApproved.value && errorFixNarration) ||
3968
- (buildLikeTurn && errorFixNarration);
3969
- if (wantsAction &&
3970
- cleaned.trim().length > 0 &&
3971
- shouldRetryBeforeFinalizing) {
3972
- let action;
3973
- if (errorFixNarration && budgetRemaining(recovery, "errorFix")) {
3974
- action = recoveryForErrorDiagnosis(toolsAttached);
3975
- }
3976
- else if (budgetRemaining(recovery, "actionIntent") &&
3977
- planHasOpenWorkNow &&
3978
- session.planApproved.value) {
3979
- action = recoveryForNarration(toolsAttached, "plan_open");
3980
- }
3981
- else if (budgetRemaining(recovery, "actionIntent") &&
3982
- pentestLikeTurn) {
3983
- action = recoveryForNarration(toolsAttached, "pentest");
3984
- }
3985
- else if (budgetRemaining(recovery, "actionIntent") &&
3986
- (freshWebSearchRequired || narratedWebAction)) {
3987
- action = recoveryForNarration(toolsAttached, "web");
3988
- }
3989
- else if (budgetRemaining(recovery, "actionIntent") &&
3990
- buildLikeTurn &&
3991
- (planNarrated || productiveSteps > 0)) {
3992
- action = recoveryForNarration(toolsAttached, "build_plan_prose");
3993
- }
3994
- else if (budgetRemaining(recovery, "actionIntent") &&
3995
- buildLikeTurn) {
3996
- action = recoveryForNarration(toolsAttached, "build");
3997
- }
3998
- else if (budgetRemaining(recovery, "actionIntent")) {
3999
- action = recoveryForNarration(toolsAttached, "generic");
4000
- }
4001
- if (action) {
4002
- consumeBudget(recovery, action.budgetKey);
4003
- commitAssistantRetry(assistantText.visible);
4004
- messages.push(recoveryUserMessage(action.message));
4005
- continue;
4006
- }
4007
- }
4008
- if (freshWebSearchRequired &&
4009
- !sawFreshWebSearch &&
4010
- budgetRemaining(recovery, "freshnessUsed")) {
4011
- const action = recoveryForFreshness(freshnessGuardMessage() +
4012
- (toolsAttached
4013
- ? " Call the web_search tool now."
4014
- : " Reply with ONLY a fenced ```tool block for web.search now."));
4015
- consumeBudget(recovery, action.budgetKey);
4016
- commitAssistantRetry(assistantText.visible);
4017
- messages.push(recoveryUserMessage(action.message));
4018
- continue;
4019
- }
4020
- if (isPlanMode &&
4021
- !informationalQuery &&
4022
- !idleOrSocialPrompt &&
4023
- budgetRemaining(recovery, "forcePlan")) {
4024
- const planAtEnd = await loadPlan(session.sessionId).catch(() => undefined);
4025
- if (!planAtEnd && !sawPlanCreateOk) {
4026
- const action = recoveryForMissingPlan(toolsAttached);
4027
- consumeBudget(recovery, action.budgetKey);
4028
- commitAssistantRetry(assistantText.visible);
4029
- messages.push(recoveryUserMessage(action.message));
4030
- continue;
4031
- }
4032
- }
4033
- if (buildLike &&
4034
- !pentestLike &&
4035
- !pentestSession &&
4036
- session.planApproved.value &&
4037
- featureAppAsk &&
4038
- !sawFeatureImplWrite &&
4039
- (sawScaffoldOk || sawLocalAppMaterialWork) &&
4040
- productiveSteps > 0 &&
4041
- budgetRemaining(recovery, "featureImpl")) {
4042
- const action = recoveryForMissingFeature(getActiveProjectRoot());
4043
- consumeBudget(recovery, action.budgetKey);
4044
- commitAssistantRetry(assistantText.visible);
4045
- messages.push(recoveryUserMessage(action.message));
4046
- continue;
4047
- }
4048
- if (buildLike &&
4049
- !pentestLike &&
4050
- !pentestSession &&
4051
- budgetRemaining(recovery, "runtimeVerify") &&
4052
- (!featureAppAsk || sawFeatureImplWrite)) {
4053
- const runtimePlan = await loadPlan(session.sessionId).catch(() => undefined);
4054
- // Durable plan evidence or multi-signal proof this turn is enough
4055
- const planRuntimeOk = Boolean(runtimePlan && planHasVerifiedRuntime(runtimePlan));
4056
- const sessionRuntimeOk = sawServerStart &&
4057
- (sawServerTail || sawLocalHttpProbe || planRuntimeOk);
4058
- if (!planRuntimeOk && !sessionRuntimeOk) {
4059
- const codingPlanFinished = Boolean(runtimePlan &&
4060
- session.planApproved.value &&
4061
- runtimePlan.kind !== "pentest" &&
4062
- runtimePlan.tasks.length > 0 &&
4063
- runtimePlan.tasks.every((task) => task.state === "done" || task.state === "skipped"));
4064
- const freestyleLocalAppDone = !session.planApproved.value &&
4065
- sawLocalAppMaterialWork &&
4066
- productiveSteps > 0 &&
4067
- freestyleClaimsAppReady(cleaned) &&
4068
- (getActiveProjectRoot() !== undefined ||
4069
- /\b(?:npm|pnpm|yarn|bun)\s+run\s+dev\b/i.test(cleaned) ||
4070
- /\bopen\s+http:\/\/localhost\b/i.test(cleaned));
4071
- if (codingPlanFinished || freestyleLocalAppDone) {
4072
- const action = recoveryForRuntimeVerify(getActiveProjectRoot());
4073
- consumeBudget(recovery, action.budgetKey);
4074
- commitAssistantRetry(assistantText.visible);
4075
- messages.push(recoveryUserMessage(action.message));
4076
- continue;
4030
+ const deferResponderReport = session.planApproved.value &&
4031
+ budgetRemaining(recovery, "prematureComplete")
4032
+ ? shouldYieldForDeclaredResponderDependency(livePlanAtCompletion, jobManager.getRunningJobs(session.sessionId), jobManager.getPendingNotifications(session.sessionId), responderWakeNotificationId)
4033
+ : false;
4034
+ const finalizeRecovery = chooseFinalizeRecovery({
4035
+ cleaned,
4036
+ recovery,
4037
+ toolsAttached,
4038
+ productiveSteps,
4039
+ planApproved: session.planApproved.value,
4040
+ planHasOpenWork: planHasOpenWorkNow,
4041
+ activePlanExists: Boolean(activePlan),
4042
+ wantsAction,
4043
+ narratedAction,
4044
+ narratedWebAction,
4045
+ isPlanMode,
4046
+ buildLikeTurn,
4047
+ pentestLikeTurn,
4048
+ buildLike,
4049
+ pentestLike,
4050
+ pentestSession,
4051
+ informationalQuery,
4052
+ idleOrSocialPrompt,
4053
+ freshWebSearchRequired,
4054
+ freshnessGuardText: freshWebSearchRequired
4055
+ ? freshnessGuardMessage()
4056
+ : "",
4057
+ sawFreshWebSearch,
4058
+ sawPlanCreateOk,
4059
+ sawFeatureImplWrite,
4060
+ sawScaffoldOk,
4061
+ sawLocalAppMaterialWork,
4062
+ sawServerStart,
4063
+ sawServerTail,
4064
+ sawLocalHttpProbe,
4065
+ sawFailedLocalHttpProbe,
4066
+ sawActivePentestTest,
4067
+ sawSuccessfulMutation,
4068
+ featureAppAsk,
4069
+ projectRoot: getActiveProjectRoot(),
4070
+ plan: livePlanAtCompletion
4071
+ ? {
4072
+ kind: livePlanAtCompletion.kind,
4073
+ hasVerifiedRuntime: planHasVerifiedRuntime(livePlanAtCompletion),
4074
+ tasks: livePlanAtCompletion.tasks,
4077
4075
  }
4078
- }
4079
- }
4080
- if (buildLike &&
4081
- !pentestLike &&
4082
- !pentestSession &&
4083
- sawFailedLocalHttpProbe &&
4084
- !sawLocalHttpProbe &&
4085
- budgetRemaining(recovery, "failedProbe") &&
4086
- cleaned.trim().length > 0) {
4087
- const action = recoveryForFailedProbe();
4088
- consumeBudget(recovery, action.budgetKey);
4089
- commitAssistantRetry(assistantText.visible);
4090
- messages.push(recoveryUserMessage(action.message));
4091
- continue;
4092
- }
4093
- if ((pentestLike || pentestSession) &&
4094
- budgetRemaining(recovery, "shallowPentest") &&
4095
- looksLikeShallowPentestReport(cleaned, {
4096
- productiveSteps,
4097
- sawActiveTest: sawActivePentestTest,
4098
- })) {
4099
- const action = recoveryForShallowPentest();
4100
- consumeBudget(recovery, action.budgetKey);
4076
+ : undefined,
4077
+ deferResponderReport,
4078
+ });
4079
+ if (finalizeRecovery) {
4080
+ consumeBudget(recovery, finalizeRecovery.budgetKey);
4101
4081
  commitAssistantRetry(assistantText.visible);
4102
- messages.push(recoveryUserMessage(action.message));
4082
+ messages.push(recoveryUserMessage(finalizeRecovery.message));
4103
4083
  continue;
4104
4084
  }
4105
- if (session.planApproved.value &&
4106
- budgetRemaining(recovery, "prematureComplete")) {
4107
- const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
4108
- const unfinished = livePlan?.tasks.filter((task) => !task.responderOwned &&
4109
- (task.state === "pending" || task.state === "in_progress"));
4110
- const deferReport = shouldYieldForDeclaredResponderDependency(livePlan, jobManager.getRunningJobs(session.sessionId), jobManager.getPendingNotifications(session.sessionId), responderWakeNotificationId);
4111
- if (livePlan &&
4112
- unfinished &&
4113
- unfinished.length > 0 &&
4114
- !deferReport) {
4115
- const next = unfinished[0];
4116
- const action = recoveryForPrematureComplete({
4117
- unfinished,
4118
- next,
4119
- pentest: livePlan.kind === "pentest" || pentestSession,
4120
- errorFix: errorFixNarration,
4121
- });
4122
- consumeBudget(recovery, action.budgetKey);
4123
- commitAssistantRetry(assistantText.visible);
4124
- messages.push(recoveryUserMessage(action.message));
4125
- continue;
4126
- }
4127
- }
4128
4085
  let outcomeStatus = "succeeded";
4129
4086
  const remainingCriteria = [];
4130
4087
  if (session.planApproved.value) {
@@ -4186,6 +4143,8 @@ export async function runAgentTurn(prompt, options = {}) {
4186
4143
  emit({ type: "assistant-message", text: "" });
4187
4144
  }
4188
4145
  interruptedVisible = "";
4146
+ interruptedReasoning = "";
4147
+ lowYieldResumptions = 0;
4189
4148
  let bound = [];
4190
4149
  if (nativeToolCalls.length) {
4191
4150
  bound = nativeToolCalls.map((tc, index) => {