@pentoshi/clai 3.8.21 → 3.8.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/agent/confirm-port.d.ts +5 -2
  2. package/dist/agent/confirm-port.js +11 -5
  3. package/dist/agent/confirm-port.js.map +1 -1
  4. package/dist/agent/loop-guard.d.ts +6 -1
  5. package/dist/agent/loop-guard.js +17 -1
  6. package/dist/agent/loop-guard.js.map +1 -1
  7. package/dist/agent/plan-decision.js +7 -3
  8. package/dist/agent/plan-decision.js.map +1 -1
  9. package/dist/agent/plan-tool.js +63 -20
  10. package/dist/agent/plan-tool.js.map +1 -1
  11. package/dist/agent/progress-pause-policy.d.ts +21 -0
  12. package/dist/agent/progress-pause-policy.js +30 -0
  13. package/dist/agent/progress-pause-policy.js.map +1 -0
  14. package/dist/agent/runner.js +166 -33
  15. package/dist/agent/runner.js.map +1 -1
  16. package/dist/agent/stop-summary.js +3 -1
  17. package/dist/agent/stop-summary.js.map +1 -1
  18. package/dist/agent/tool-history.d.ts +13 -1
  19. package/dist/agent/tool-history.js +34 -15
  20. package/dist/agent/tool-history.js.map +1 -1
  21. package/dist/agent/tool-output-formatting.js +16 -0
  22. package/dist/agent/tool-output-formatting.js.map +1 -1
  23. package/dist/app/ports/confirm-port.d.ts +1 -1
  24. package/dist/prompts/embedded.js +1 -1
  25. package/dist/prompts/embedded.js.map +1 -1
  26. package/dist/prompts/index.js +6 -0
  27. package/dist/prompts/index.js.map +1 -1
  28. package/dist/prompts/system.agent.md +2 -0
  29. package/dist/tools/web/search.js +22 -3
  30. package/dist/tools/web/search.js.map +1 -1
  31. package/dist/tui-v2/bootstrap/overlay-ports.js +7 -2
  32. package/dist/tui-v2/bootstrap/overlay-ports.js.map +1 -1
  33. package/dist/version.generated.d.ts +2 -2
  34. package/dist/version.generated.js +2 -2
  35. package/package.json +1 -1
@@ -13,7 +13,7 @@ import { groqInputTokenBudget } from "../llm/groq.js";
13
13
  import { classifyToolCall, isPentestToolCall, scopeHint, scopeTargetForToolCall, } from "../safety/classifier.js";
14
14
  import { availableToolNames, normalizeToolCall, runToolCall, BATCH_SAFE_TOOLS, } from "../tools/registry.js";
15
15
  import { getToolDefinitions, getCompactToolDefinitions, PLAN_TOOL_NAMES, } from "../tools/definitions.js";
16
- import { appendAssistantWithTools, appendToolResult, assertValidToolProtocol, fillMissingToolResults, repairToolProtocol, } from "./tool-history.js";
16
+ import { appendAssistantWithTools, ensureUniqueToolCallIds, appendToolResult, assertValidToolProtocol, fillMissingToolResults, repairToolProtocol, } from "./tool-history.js";
17
17
  import { formatViewportHint, registerViewport } from "../ui/output-pane.js";
18
18
  import { compactMessagesWithSummary, estimateMessagesTokens, shouldApplyAutoCompact, COMPACTION_MEMORY_PREFIX, PLAN_IMPLEMENT_MEMORY_PREFIX, isCompactionMemoryMessage, } from "./context-manager.js";
19
19
  import { buildContextBreakdown, contextBreakdownAuditPayload, } from "./context-breakdown.js";
@@ -37,6 +37,7 @@ import { fsWrite, isOutsideWorkingDirectory, resolveFsToolPath, } from "../tools
37
37
  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";
38
38
  import { createSessionPolicy, isPreApprovalAllowedTool, isPlanModeAllowedShellCommand, isPlanModeAllowedTool, isPlanApprovedByStatus, planHasOpenWork, isAbortError, shouldEnableImageOcr, } from "./session-policy.js";
39
39
  import { saveToolOutput, summarizeOutput, formatToolContext, } from "./tool-output-formatting.js";
40
+ import { codingSessionFromContext, isProtocolPlaceholderOutput, progressPauseMode, } from "./progress-pause-policy.js";
40
41
  import { renderPlanForTerminal, planContextMessage, handlePlanTool, resolvePlanTaskId, } from "./plan-tool.js";
41
42
  import { absorbLooseWorkIntoLedger, applyDestinationCwd, canMarkTaskDone, hasLocalRuntimeProof, hasRemoteWorkProof, isDevServerCall, isEvidenceWorkTool, isFeatureImplementationCall, isPackageInstallCommand, isPlanPreflightTool, isPortListeningOutput, isReadOnlyReconTool, isRemoteActiveTestCall, isRemoteObservationTask, isRemoteReconToolCall, isRuntimeObservationTask, isScaffoldCreateCommand, isServerReadyOutput, ledgerFromTaskEvidence, pickPendingTaskForToolCall, recordTaskWorkSuccess, resolveUserDestinationHint, taskEvidenceFromLedger, TOOL_ABORT_GRACE_MS, toolHardBudgetMs, toolStallBudgetMs, userAskedForFeatureApp, } from "./task-evidence.js";
42
43
  import { buildSessionStateBlock, inferNextHint, upsertSessionStateMessage, } from "./session-state.js";
@@ -793,6 +794,15 @@ export async function runAgentTurn(prompt, options = {}) {
793
794
  const hasHistory = (options.history?.length ?? 0) > 0;
794
795
  const buildLike = buildLikeTurn;
795
796
  const pentestLike = looksLikePentestTask(prompt, options.history);
797
+ /** Coding/build sessions never hard-pause mid-turn for the progress governor. */
798
+ let codingSession = codingSessionFromContext({
799
+ buildLike,
800
+ planKind: activePlan?.kind,
801
+ });
802
+ let pauseMode = progressPauseMode({
803
+ codingSession,
804
+ autoConfirm: Boolean(options.autoConfirm),
805
+ });
796
806
  const continueExistingOutcome = /^(?:continue|resume|proceed|keep\s+going|finish|next)\b/i.test(prompt.trim()) ||
797
807
  Boolean(activePlan && !isPlanTerminal(activePlan));
798
808
  const outcomeState = await openOutcomeState({
@@ -911,13 +921,40 @@ export async function runAgentTurn(prompt, options = {}) {
911
921
  environmentChanged: retryEnvironmentChanged,
912
922
  ...(retryReason ? { retryReason } : {}),
913
923
  });
924
+ // Reuse prior SUCCESS instead of re-running or returning ok=false.
925
+ // Models often thrash "tools failed after resume" and re-call the same
926
+ // tool.check/fs.list; answering with a fake failure made the thrash worse.
927
+ const cachedSuccess = loopGuard.getCachedSuccessOutput(call.name, call.args);
928
+ if (cachedSuccess !== undefined && loopGuard.hasSucceeded(call.name, call.args)) {
929
+ const reuseNote = loopCheck.reason ??
930
+ `${call.name} already succeeded with identical arguments earlier this turn — reusing that result. Do not re-call it; proceed to the next step.`;
931
+ writeNotice("info", reuseNote, chalk.dim(` ℹ ${reuseNote}\n`));
932
+ const body = `Prior successful ${call.name} result (reused; identical args — tools are working):\n` +
933
+ cachedSuccess;
934
+ const result = { ok: true, output: body, exitCode: 0 };
935
+ emitToolResult(toolEventId, result, reuseNote);
936
+ return {
937
+ ok: true,
938
+ call,
939
+ result,
940
+ contextOutput: body,
941
+ };
942
+ }
914
943
  if (loopCheck.block) {
915
944
  const reason = loopCheck.reason ??
916
945
  `${call.name} was already called with the same arguments. Use the prior result and choose a different next step.`;
917
946
  writeNotice("warn", reason, chalk.yellow(` ⚠ ${reason}\n`));
918
- const result = { ok: false, output: reason, exitCode: 1 };
947
+ // Prefer ok=true when we previously succeeded but have no cache body,
948
+ // so the model does not treat loop-guard as a tool outage.
949
+ const softOk = loopGuard.hasSucceeded(call.name, call.args);
950
+ const result = {
951
+ ok: softOk,
952
+ output: reason,
953
+ exitCode: softOk ? 0 : 1,
954
+ };
955
+ emitToolResult(toolEventId, result, reason);
919
956
  return {
920
- ok: false,
957
+ ok: softOk,
921
958
  call,
922
959
  result,
923
960
  contextOutput: reason,
@@ -1767,30 +1804,55 @@ export async function runAgentTurn(prompt, options = {}) {
1767
1804
  ((call.name === "shell.exec" || call.name === "shell.start") &&
1768
1805
  isPackageInstallCommand(String(call.args.command ?? "")));
1769
1806
  }
1770
- const governed = governProgress(governorState, "activity", {
1771
- evidenceDelta: newEvidence.length,
1772
- hypothesisDelta,
1773
- repetitionScore: loopGuard.getAttemptCount(call.name, call.args) > 1 ? 1 : 0,
1774
- policy: {
1775
- resourceEnvelope: Math.max(12, maxSteps),
1776
- emergencyCeiling: Math.max(70, maxSteps * 3),
1777
- reflectionAfterNoDelta: 3,
1778
- pauseAfterNoDelta: 6,
1779
- repetitionThreshold: 0.8,
1780
- },
1781
- });
1782
- governorState = governed.state;
1783
- if (governed.recommendation === "reflect") {
1784
- deferredPostToolMessages.push({
1785
- role: "system",
1786
- content: `PROGRESS GOVERNOR: ${governed.reason}. Reassess the current premise and choose the next action that can produce criterion-linked evidence.`,
1807
+ // Protocol-repair placeholders are not live work — never let them
1808
+ // accumulate into a mid-turn pause (they used to look like failed tools).
1809
+ if (!isProtocolPlaceholderOutput(result.output)) {
1810
+ const governed = governProgress(governorState, "activity", {
1811
+ evidenceDelta: newEvidence.length,
1812
+ hypothesisDelta,
1813
+ repetitionScore: loopGuard.getAttemptCount(call.name, call.args) > 1 ? 1 : 0,
1814
+ policy: {
1815
+ resourceEnvelope: Math.max(12, maxSteps),
1816
+ // Coding builds get a much higher ceiling; never use the tight
1817
+ // default that stopped multi-file scaffolds after a handful of steps.
1818
+ emergencyCeiling: codingSession
1819
+ ? Math.max(200, maxSteps * 5)
1820
+ : Math.max(70, maxSteps * 3),
1821
+ reflectionAfterNoDelta: codingSession ? 5 : 3,
1822
+ pauseAfterNoDelta: codingSession ? 24 : 6,
1823
+ repetitionThreshold: 0.8,
1824
+ },
1787
1825
  });
1788
- }
1789
- else if (governed.recommendation === "paused_budget") {
1790
- governorPauseReason = governed.reason;
1826
+ governorState = governed.state;
1827
+ if (governed.recommendation === "reflect") {
1828
+ deferredPostToolMessages.push({
1829
+ role: "system",
1830
+ content: `PROGRESS GOVERNOR: ${governed.reason}. Reassess the current premise and choose the next action that can produce criterion-linked evidence.` +
1831
+ (codingSession
1832
+ ? " Keep working — coding builds do not stop for a continue prompt."
1833
+ : ""),
1834
+ });
1835
+ }
1836
+ else if (governed.recommendation === "paused_budget") {
1837
+ if (pauseMode === "never") {
1838
+ // Soft reset so we do not re-trip every subsequent tool.
1839
+ governorState = {
1840
+ ...governed.state,
1841
+ consecutiveNoDelta: 0,
1842
+ };
1843
+ deferredPostToolMessages.push({
1844
+ role: "system",
1845
+ content: `PROGRESS GOVERNOR (soft, coding build): ${governed.reason}. ` +
1846
+ "Change approach if stuck, but keep implementing — do not stop for user confirmation.",
1847
+ });
1848
+ }
1849
+ else {
1850
+ governorPauseReason = governed.reason;
1851
+ }
1852
+ }
1791
1853
  }
1792
1854
  await saveOutcomeState(outcomeState);
1793
- loopGuard.recordAttempt(step, call.name, call.args, result.ok, result.exitCode);
1855
+ loopGuard.recordAttempt(step, call.name, call.args, result.ok, result.exitCode, result.output);
1794
1856
  // Evidence for verify-before-done: only successful real work counts.
1795
1857
  if (result.ok && isEvidenceWorkTool(call.name)) {
1796
1858
  const liveAfter = await loadPlan(session.sessionId).catch(() => undefined);
@@ -2033,14 +2095,53 @@ export async function runAgentTurn(prompt, options = {}) {
2033
2095
  // advances when the previous iteration actually executed a tool.
2034
2096
  step = productiveSteps;
2035
2097
  if (governorPauseReason) {
2036
- const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
2037
- outcomeState.outcome.status = "paused_budget";
2038
- await saveOutcomeState(outcomeState);
2039
- moveTurn("paused_budget", governorPauseReason);
2040
- lastAnswer = richSummary;
2041
- return finishTurn(lastAnswer, productiveSteps, "paused_budget", outcomeState.outcome.criteria
2042
- .filter((criterion) => criterion.required && criterion.status !== "proven")
2043
- .map((criterion) => criterion.statement), governorPauseReason);
2098
+ // Non-coding: always ask continue/stop. Coding never sets this reason
2099
+ // (pauseMode === "never"), but guard anyway.
2100
+ if (pauseMode === "never") {
2101
+ governorPauseReason = undefined;
2102
+ governorState = {
2103
+ ...governorState,
2104
+ consecutiveNoDelta: 0,
2105
+ };
2106
+ }
2107
+ else {
2108
+ const confirmPort = options.confirm;
2109
+ let keepGoing = false;
2110
+ if (confirmPort?.confirmContinue) {
2111
+ try {
2112
+ keepGoing = await confirmPort.confirmContinue(productiveSteps, governorPauseReason);
2113
+ }
2114
+ catch {
2115
+ keepGoing = false;
2116
+ }
2117
+ finally {
2118
+ restoreInteractiveStdin();
2119
+ }
2120
+ }
2121
+ if (keepGoing) {
2122
+ writeNotice("info", "continuing after progress pause", chalk.dim(` ℹ continuing after pause (${governorPauseReason}) — change approach if stuck\n`));
2123
+ deferredPostToolMessages.push({
2124
+ role: "system",
2125
+ content: `User chose CONTINUE after progress pause (${governorPauseReason}). ` +
2126
+ "Do not repeat the same failing step; change approach and produce new evidence.",
2127
+ });
2128
+ governorPauseReason = undefined;
2129
+ governorState = {
2130
+ ...governorState,
2131
+ consecutiveNoDelta: 0,
2132
+ };
2133
+ }
2134
+ else {
2135
+ const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
2136
+ outcomeState.outcome.status = "paused_budget";
2137
+ await saveOutcomeState(outcomeState);
2138
+ moveTurn("paused_budget", governorPauseReason);
2139
+ lastAnswer = richSummary;
2140
+ return finishTurn(lastAnswer, productiveSteps, "paused_budget", outcomeState.outcome.criteria
2141
+ .filter((criterion) => criterion.required && criterion.status !== "proven")
2142
+ .map((criterion) => criterion.statement), governorPauseReason);
2143
+ }
2144
+ }
2044
2145
  }
2045
2146
  options.signal?.throwIfAborted();
2046
2147
  let call;
@@ -3000,12 +3101,29 @@ export async function runAgentTurn(prompt, options = {}) {
3000
3101
  // Preserve model/document order. In particular, never move a later
3001
3102
  // in_progress transition ahead of the preceding work or done receipt;
3002
3103
  // doing so inverts dependency order and desynchronizes the task pane.
3104
+ // Re-id empty/duplicate native ids BEFORE building toRun/history so
3105
+ // assistant toolCalls and role:tool results always share the same id.
3106
+ // Mismatched ids make results look orphaned → repair injects placeholders
3107
+ // → model thrash-retries tools that already succeeded in the UI.
3108
+ const historyNativeCalls = ensureUniqueToolCallIds(bound.map((b) => b.native));
3109
+ for (let i = 0; i < bound.length; i++) {
3110
+ const fixed = historyNativeCalls[i];
3111
+ bound[i] = {
3112
+ ...bound[i],
3113
+ id: fixed.id,
3114
+ native: fixed,
3115
+ };
3116
+ }
3117
+ // Re-bind toRun to the rewritten bound entries (by original call identity).
3118
+ toRun = toRun.map((old) => {
3119
+ const match = bound.find((b) => b.call === old.call);
3120
+ return match ?? old;
3121
+ });
3003
3122
  // Re-index toRun positions for UI callIds[] (0..n-1 this turn).
3004
3123
  toRun = toRun.map((b, index) => ({ ...b, index }));
3005
3124
  const allCalls = toRun.map((b) => b.call);
3006
3125
  /** Stable call→Bound map (object identity; no indexOf for result ids). */
3007
3126
  const callToBound = new Map(toRun.map((b) => [b.call, b]));
3008
- const historyNativeCalls = bound.map((b) => b.native);
3009
3127
  const runIds = new Set(toRun.map((b) => b.id));
3010
3128
  // Notice BEFORE tool cards so the transcript reads:
3011
3129
  // thinking → response → "N tool calls…" → tool cards (not tools then info).
@@ -3216,6 +3334,19 @@ export async function runAgentTurn(prompt, options = {}) {
3216
3334
  else {
3217
3335
  session.planApproved.value = true;
3218
3336
  }
3337
+ // Re-derive pause policy from the new plan kind (coding builds
3338
+ // must not hard-pause even if the free-text prompt was generic).
3339
+ const kindArg = typeof res.call.args.kind === "string"
3340
+ ? res.call.args.kind
3341
+ : undefined;
3342
+ codingSession = codingSessionFromContext({
3343
+ buildLike,
3344
+ planKind: kindArg,
3345
+ });
3346
+ pauseMode = progressPauseMode({
3347
+ codingSession,
3348
+ autoConfirm: Boolean(options.autoConfirm),
3349
+ });
3219
3350
  }
3220
3351
  // User Esc/Ctrl+C only — never cancel siblings because a delete failed
3221
3352
  // or a confirm was declined; the model must see every tool result.
@@ -3335,6 +3466,8 @@ export async function runAgentTurn(prompt, options = {}) {
3335
3466
  }
3336
3467
  }
3337
3468
  }
3469
+ // Hard iteration ceiling (hundreds of steps) — rare. Mid-turn governor
3470
+ // pauses already confirm for non-coding; coding never hard-pauses there.
3338
3471
  const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
3339
3472
  lastAnswer = richSummary;
3340
3473
  outcomeState.outcome.status = "paused_budget";