Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ agent.log
plan/
.tmp
temp_reference/
nul
2 changes: 1 addition & 1 deletion src/cli/print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2849,7 +2849,7 @@ function runHeadlessStreaming(
}))
}
if (abortController) {
abortController.abort()
abortController.abort('interrupt')
}
suggestionState.abortController?.abort()
suggestionState.abortController = null
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/toolPermission/PermissionContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ function createPermissionContext(
logForDebugging(
`Hook interrupt: tool=${tool.name} hookMessage=${decision.message}`,
)
toolUseContext.abortController.abort()
toolUseContext.abortController.abort('interrupt')
}
return this.buildDeny(
decision.message || 'Permission denied by hook',
Expand Down
32 changes: 16 additions & 16 deletions src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ function* yieldMissingToolResultBlocks(
* rules, ye will be punished with an entire day of debugging and hair pulling.
*/
const MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3
const MAX_CONTINUATION_NUDGES = 3
const MAX_CONTINUATION_NUDGES = 20

function formatAutoCompactRetryDelay(delayMs: number): string {
const totalSeconds = Math.max(1, Math.ceil(delayMs / 1000))
Expand Down Expand Up @@ -500,21 +500,21 @@ async function* queryLoop(
queryCheckpoint('query_snip_end')
}

// Apply microcompact before autocompact
queryCheckpoint('query_microcompact_start')
const microcompactResult = await deps.microcompact(
messagesForQuery,
toolUseContext,
querySource,
)
messagesForQuery = microcompactResult.messages
// For cached microcompact (cache editing), defer boundary message until after
// the API response so we can use actual cache_deleted_input_tokens.
// Gated behind feature() so the string is eliminated from external builds.
const pendingCacheEdits = feature('CACHED_MICROCOMPACT')
? microcompactResult.compactionInfo?.pendingCacheEdits
: undefined
queryCheckpoint('query_microcompact_end')
// Apply microcompact before autocompact
queryCheckpoint('query_microcompact_start')
const microcompactResult = await deps.microcompact(
messagesForQuery,
toolUseContext,
querySource,
)
messagesForQuery = microcompactResult.messages
// For cached microcompact (cache editing), defer boundary message until after
// the API response so we can use actual cache_deleted_input_tokens.
// Gated behind feature() so the string is eliminated from external builds.
const pendingCacheEdits = feature('CACHED_MICROCOMPACT')
? microcompactResult.compactionInfo?.pendingCacheEdits
: undefined
queryCheckpoint('query_microcompact_end')

// Project the collapsed context view and maybe commit more collapses.
// Runs BEFORE autocompact so that if collapse gets us under the
Expand Down
27 changes: 18 additions & 9 deletions src/utils/continuation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { tokenCountWithEstimation } from './tokens.js'

export const CONTINUATION_SIGNALS = [
// English: Action-transition phrases (requires intent + action)
/\bso now (i|let me|we) (need to|have to|should|must|will) (do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|start|begin|apply|identify|inspect|analyze|review|search)\b/i,
/\bnow i('ll| will) (do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|go|proceed|start|begin|apply|identify|inspect|analyze|review|search)\b/i,
/\bi (will|shall|now|need to|have to|must|should) (now )?(do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|go|proceed|start|begin|apply|identify|inspect|analyze|review|search)\b/i,
/\blet me (go ahead and |now )?(do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|proceed|start|begin|apply|update|create|identify|inspect|analyze|review|search|summarize)\b/i,
/\btime to (do|create|write|edit|update|fix|implement|add|run|check|make|build|get started|begin|start|inspect|analyze|review|search)\b/i,
/\b(moving on to|next step is to|starting to|proceeding to|applying (the|these) changes|inspecting|analyzing|reviewing|searching)\b/i,
/\bso now (i|let me|we) (need to|have to|should|must|will) (do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|start|begin|apply|identify|inspect|analyze|review|search|process|download|upload|convert|compile|train|evaluate|test|continue|generate|extract|merge|deploy|install|configure|refactor|optimize)\b/i,
/\bnow i('ll| will) (do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|go|proceed|start|begin|apply|identify|inspect|analyze|review|search|process|download|upload|convert|compile|train|evaluate|test|continue|generate|extract|merge|deploy|install|configure|refactor|optimize)\b/i,
/\bi (will|shall|now|need to|have to|must|should) (now )?(do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|go|proceed|start|begin|apply|identify|inspect|analyze|review|search|process|download|upload|convert|compile|train|evaluate|test|continue|generate|extract|merge|deploy|install|configure|refactor|optimize)\b/i,
/\blet me (go ahead and |now )?(do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|proceed|start|begin|apply|update|create|identify|inspect|analyze|review|search|summarize|process|download|upload|convert|compile|train|evaluate|test|continue|generate|extract|merge|deploy|install|configure|refactor|optimize)\b/i,
/\btime to (do|create|write|edit|update|fix|implement|add|run|check|make|build|get started|begin|start|inspect|analyze|review|search|process|download|upload|convert|compile|train|evaluate|test|continue|generate|extract|merge|deploy|install|configure|refactor|optimize)\b/i,
/\b(moving on to|next step is to|starting to|proceeding to|continuing with|applying (the|these) changes|creating|writing|editing|updating|fixing|implementing|adding|running|checking|making|building|starting|beginning|going|proceeding|identifying|inspecting|analyzing|reviewing|searching|processing|downloading|uploading|converting|compiling|training|evaluating|testing|continuing|generating|summarizing|extracting|merging|deploying|installing|configuring|refactoring|optimizing|doing)\b/i,
// French: Support for common continuation phrasing (relaxed boundaries for accents and apostrophes)
/(^|\s)(je passe (à|au)|ensuite|l'étape suivante est de|je continue avec|au suivant|passons à|je reviens vers vous|je suis en train d'|je vais maintenant)(\s|$|[a-zà-ÿ])/i,
/(^|\s)(je (vais|dois|dois maintenant|vais maintenant) (faire|créer|écrire|modifier|ajouter|tester|vérifier|lancer|exécuter|procéder|démarrer|commencer|identifier|analyser|inspecter|revoir|chercher))(\s|$|[a-zà-ÿ])/i,
Expand All @@ -21,6 +21,11 @@ export const CONTINUATION_SIGNALS = [
/:\s*$/,
// Universal: Open task marker indicates pending work
/◻/,
// Imperative/declarative patterns (no subject required — handles cases like
// "Need to process files", "Now create files", "Next I process files")
/\bneed to (do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|start|begin|go|proceed|apply|identify|inspect|analyze|review|search|process|download|upload|convert|compile|train|evaluate|test|continue|generate|extract|merge|deploy|install|configure|refactor|optimize|summarize)\b/i,
/\bnow (do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|start|begin|go|proceed|apply|identify|inspect|analyze|review|search|process|download|upload|convert|compile|train|evaluate|test|continue|generate|extract|merge|deploy|install|configure|refactor|optimize|summarize)\b(?!\s+you\b)/i,
/\bnext (i|we)\s+(need to|will|shall|should|must)?\s*(do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|start|begin|go|proceed|apply|identify|inspect|analyze|review|search|process|download|upload|convert|compile|train|evaluate|test|continue|generate|extract|merge|deploy|install|configure|refactor|optimize|summarize)\b/i,
]

export const COMPLETION_MARKERS = /\b(done|finished|completed|complete|summary|that's all|that is all|all set|hope this helps|let me know if|no issues|lgtm)\b/i
Expand Down Expand Up @@ -99,10 +104,11 @@ export function analyzeContinuationIntent(
// it's a strong 1st person intent or open tasks are present.
const hasTerminalPunctuation = /[.!??"'`)\]]\s*$/.test(lastText) || lastText.endsWith('`')
if (hasTerminalPunctuation) {
const strongIntent = /\b(i (will|shall|need to|must|should|now)|let (me|us)|je (vais|reviens)|passons à|moving on to|next step is to)\b/i.test(lowerText) ||
const strongIntent = /\b(i (will|shall|need to|must|should|now)|let (me|us)|je (vais|reviens)|passons à|moving on to|continuing with|proceeding to|next step is to)\b/i.test(lowerText) ||
/je suis en train d'/i.test(lowerText) || /◻/.test(lastText)
const presentProgressive = /\bnow \w+ing\b/i.test(lateText)
const endsWithColon = /:\s*$/.test(lastText)
if (strongIntent || endsWithColon) {
if (strongIntent || endsWithColon || presentProgressive) {
return { shouldNudge: true, reason: 'continuation_signal' }
}
} else {
Expand All @@ -111,7 +117,10 @@ export function analyzeContinuationIntent(
}

// 3. Completion Marker Guard (Final check for sound, completed messages)
if (COMPLETION_MARKERS.test(lowerText)) {
// Only block continuation if no continuation signal is present (prevents false
// positives when "complete" or "done" appears mid-sentence, e.g. "The download
// is complete. Now processing the files...")
if (COMPLETION_MARKERS.test(lowerText) && !hasLateContinuationSignal && !CONTINUATION_SIGNALS.some(re => re.test(lowerText))) {
return { shouldNudge: false }
}

Expand Down
10 changes: 5 additions & 5 deletions src/utils/permissions/PermissionPromptToolResultSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ export function permissionPromptToolResultToPermissionDecision(
updatedInput,
decisionReason,
}
} else if (result.behavior === 'deny' && result.interrupt) {
logForDebugging(
`SDK permission prompt deny+interrupt: tool=${tool.name} message=${result.message}`,
)
toolUseContext.abortController.abort()
} else if (result.behavior === 'deny' && result.interrupt) {
logForDebugging(
`SDK permission prompt deny+interrupt: tool=${tool.name} message=${result.message}`,
)
toolUseContext.abortController.abort('interrupt')
}
return {
...result,
Expand Down
10 changes: 5 additions & 5 deletions src/utils/permissions/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,11 +462,11 @@ async function runPermissionRequestHooksForHeadlessAgent(
}
}
if (decision.behavior === 'deny') {
if (decision.interrupt) {
logForDebugging(
`Hook interrupt: tool=${tool.name} hookMessage=${decision.message}`,
)
context.abortController.abort()
if (decision.interrupt) {
logForDebugging(
`Hook interrupt: tool=${tool.name} hookMessage=${decision.message}`,
)
context.abortController.abort('interrupt')
}
return {
behavior: 'deny',
Expand Down