{"version":3,"file":"long-running-guard.d.ts","sourceRoot":"","sources":["../../../../src/runs/shared/long-running-guard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAEnE,UAAU,wBAAwB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CACf;AAED,KAAK,wBAAwB,GAAG,gBAAgB,GAAG,gBAAgB,GAAG,iBAAiB,CAAC;AAExF,UAAU,qBAAqB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;CACX;AAED,UAAU,oBAAoB;IAC7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,qBAAqB,EAAE,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oBAAoB,EAAE,MAAM,CAAC;CAC7B;AA8BD,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GACvC,MAAM,GAAG,SAAS,CAcpB;AA0ED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAM9D;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,OAAO,CAW/G;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED,wBAAgB,sBAAsB,CACrC,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE,wBAAwB,GAC/B,wBAAwB,GAAG,SAAS,CAOtC;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI,CAM3E;AAED,wBAAgB,0BAA0B,IAAI,oBAAoB,CAMjE;AAED,wBAAgB,qBAAqB,CACpC,KAAK,EAAE,oBAAoB,EAC3B,KAAK,EAAE,qBAAqB,EAC5B,QAAQ,EAAE,MAAM,GACd,IAAI,CAiBN;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAEtG;AAED,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,GAAG,SAAS,CAK/F","sourcesContent":["import type { ResolvedControlConfig } from \"../../shared/types.ts\";\n\ninterface LongRunningNoticeMetrics {\n\tstartedAt: number;\n\tnow: number;\n\tturns: number;\n\ttokens: number;\n}\n\ntype LongRunningTriggerReason = \"time_threshold\" | \"turn_threshold\" | \"token_threshold\";\n\ninterface FailedMutatingAttempt {\n\ttool: string;\n\tpath?: string;\n\terror: string;\n\tts: number;\n}\n\ninterface MutatingFailureState {\n\tconsecutiveFailures: number;\n\tlastFailureAt?: number;\n\trecentFailures: FailedMutatingAttempt[];\n\tlastMutatingPath?: string;\n\trepeatedPathFailures: number;\n}\n\nconst MUTATING_BASH_PATTERNS = [\n\t/(^|[;&|()\\s])rm\\s+/,\n\t/(^|[;&|()\\s])mv\\s+/,\n\t/(^|[;&|()\\s])cp\\s+/,\n\t/(^|[;&|()\\s])mkdir\\s+/,\n\t/(^|[;&|()\\s])touch\\s+/,\n\t/(^|[;&|()\\s])git\\s+apply\\b/,\n\t/(^|[;&|()\\s])patch\\s+/,\n\t/(^|[;&|()\\s])sed\\s+[^\\n;&|]*\\s-i\\b/,\n\t/(^|[;&|()\\s])perl\\s+[^\\n;&|]*\\s-pi\\b/,\n\t/(^|[;&|()]|\\n)\\s*tee\\s+[^|&;]+/,\n\t/\\b(writeFile|writeFileSync|appendFile|appendFileSync)\\b/,\n\t/\\bwrite_text\\s*\\(/,\n\t/\\bopen\\s*\\([^)]*,\\s*[\"'][wa]/,\n];\n\nconst MUTATING_FAILURE_HINTS = [\n\t\"failed\",\n\t\"error\",\n\t\"no exact match\",\n\t\"did not match\",\n\t\"malformed\",\n\t\"rejected\",\n\t\"unable\",\n\t\"cannot\",\n\t\"could not\",\n];\n\nexport function resolveCurrentPath(\n\ttoolName: string | undefined,\n\targs: Record<string, unknown> | undefined,\n): string | undefined {\n\tif (!toolName || !args) return undefined;\n\tconst direct = [\"path\", \"file\", \"filename\", \"target\", \"cwd\"];\n\tfor (const key of direct) {\n\t\tconst value = args[key];\n\t\tif (typeof value === \"string\" && value.trim()) return value.trim();\n\t}\n\tif (toolName === \"bash\") {\n\t\tconst command = typeof args.command === \"string\" ? args.command : undefined;\n\t\tif (!command) return undefined;\n\t\tconst redirect = command.match(/(?:>|>>|tee\\s+)(\\S+)/);\n\t\tif (redirect?.[1]) return redirect[1];\n\t}\n\treturn undefined;\n}\n\nfunction hasUnquotedFileRedirection(command: string): boolean {\n\tlet inSingle = false;\n\tlet inDouble = false;\n\tfor (let i = 0; i < command.length; i++) {\n\t\tconst char = command[i]!;\n\t\tif (char === \"'\" && !inDouble) {\n\t\t\tinSingle = !inSingle;\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === '\"' && !inSingle) {\n\t\t\tinDouble = !inDouble;\n\t\t\tcontinue;\n\t\t}\n\t\tif (inSingle || inDouble) continue;\n\t\tif (char !== \">\") continue;\n\t\tif (command[i - 1] === \"-\") continue;\n\t\tconst isDouble = command[i + 1] === \">\";\n\t\tlet cursor = i + (isDouble ? 2 : 1);\n\t\twhile (cursor < command.length && /\\s/.test(command[cursor]!)) cursor++;\n\t\tif (cursor >= command.length) continue;\n\t\tconst targetStart = command[cursor]!;\n\t\tif (targetStart === \"&\" || targetStart === \"|\" || targetStart === \";\") continue;\n\t\tif (targetStart === \"(\" || targetStart === \")\") continue;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nfunction unquotedShellText(command: string): string {\n\tlet inSingle = false;\n\tlet inDouble = false;\n\tlet escaped = false;\n\tlet result = \"\";\n\tfor (const char of command) {\n\t\tif (escaped) {\n\t\t\tresult += inSingle || inDouble ? \"_\" : char;\n\t\t\tescaped = false;\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"\\\\\" && !inSingle) {\n\t\t\tescaped = true;\n\t\t\tresult += inDouble ? \"_\" : char;\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"'\" && !inDouble) {\n\t\t\tinSingle = !inSingle;\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === '\"' && !inSingle) {\n\t\t\tinDouble = !inDouble;\n\t\t\tcontinue;\n\t\t}\n\t\tresult += inSingle || inDouble ? \"_\" : char;\n\t}\n\treturn result;\n}\n\nfunction hasMutatingGitCommand(command: string): boolean {\n\tconst unquoted = unquotedShellText(command);\n\tfor (const segment of unquoted.split(/(?:&&|\\|\\||[;|()\\n])/)) {\n\t\tconst trimmed = segment.trim();\n\t\tif (!trimmed || /^(?:echo|printf)\\b/.test(trimmed)) continue;\n\t\tif (\n\t\t\t/^git\\s+(?:(?:(?:-C|--git-dir|--work-tree)\\s+\\S+|(?:--git-dir|--work-tree)=\\S+|--paginate)\\s+)*(?:add|commit|push)\\b/.test(\n\t\t\t\ttrimmed,\n\t\t\t)\n\t\t)\n\t\t\treturn true;\n\t}\n\treturn false;\n}\n\nexport function isMutatingBashCommand(command: string): boolean {\n\treturn (\n\t\thasUnquotedFileRedirection(command) ||\n\t\thasMutatingGitCommand(command) ||\n\t\tMUTATING_BASH_PATTERNS.some((pattern) => pattern.test(command))\n\t);\n}\n\nexport function isMutatingTool(toolName: string | undefined, args: Record<string, unknown> | undefined): boolean {\n\tif (!toolName) return false;\n\tif (toolName === \"edit\" || toolName === \"write\") return true;\n\tif (toolName === \"cursor\") {\n\t\tconst activityTitle = typeof args?.activityTitle === \"string\" ? args.activityTitle : \"\";\n\t\treturn /^Cursor (?:edit|write)\\b/i.test(activityTitle);\n\t}\n\tif (toolName !== \"bash\") return false;\n\tconst command = typeof args?.command === \"string\" ? args.command : \"\";\n\tif (!command.trim()) return false;\n\treturn isMutatingBashCommand(command);\n}\n\nexport function didMutatingToolFail(text: string): boolean {\n\tconst lowered = text.toLowerCase();\n\treturn MUTATING_FAILURE_HINTS.some((hint) => lowered.includes(hint));\n}\n\nexport function nextLongRunningTrigger(\n\tconfig: ResolvedControlConfig,\n\tmetrics: LongRunningNoticeMetrics,\n): LongRunningTriggerReason | undefined {\n\tif (metrics.now - metrics.startedAt >= config.activeNoticeAfterMs) return \"time_threshold\";\n\tif (config.activeNoticeAfterTurns !== undefined && metrics.turns >= config.activeNoticeAfterTurns)\n\t\treturn \"turn_threshold\";\n\tif (config.activeNoticeAfterTokens !== undefined && metrics.tokens >= config.activeNoticeAfterTokens)\n\t\treturn \"token_threshold\";\n\treturn undefined;\n}\n\nexport function resetMutatingFailureState(state: MutatingFailureState): void {\n\tstate.consecutiveFailures = 0;\n\tstate.lastFailureAt = undefined;\n\tstate.recentFailures = [];\n\tstate.lastMutatingPath = undefined;\n\tstate.repeatedPathFailures = 0;\n}\n\nexport function createMutatingFailureState(): MutatingFailureState {\n\treturn {\n\t\tconsecutiveFailures: 0,\n\t\trecentFailures: [],\n\t\trepeatedPathFailures: 0,\n\t};\n}\n\nexport function recordMutatingFailure(\n\tstate: MutatingFailureState,\n\tinput: FailedMutatingAttempt,\n\twindowMs: number,\n): void {\n\tif (state.lastFailureAt === undefined || input.ts - state.lastFailureAt > windowMs) {\n\t\tstate.consecutiveFailures = 0;\n\t\tstate.recentFailures = [];\n\t\tstate.repeatedPathFailures = 0;\n\t\tstate.lastMutatingPath = undefined;\n\t}\n\tstate.lastFailureAt = input.ts;\n\tstate.consecutiveFailures += 1;\n\tif (input.path && state.lastMutatingPath === input.path) {\n\t\tstate.repeatedPathFailures += 1;\n\t} else if (input.path) {\n\t\tstate.lastMutatingPath = input.path;\n\t\tstate.repeatedPathFailures = 1;\n\t}\n\tstate.recentFailures.push(input);\n\tif (state.recentFailures.length > 3) state.recentFailures.shift();\n}\n\nexport function shouldEscalateMutatingFailures(state: MutatingFailureState, threshold: number): boolean {\n\treturn state.consecutiveFailures >= threshold || state.repeatedPathFailures >= threshold;\n}\n\nexport function summarizeRecentMutatingFailures(state: MutatingFailureState): string | undefined {\n\tif (state.recentFailures.length === 0) return undefined;\n\treturn state.recentFailures\n\t\t.map((entry) => `${entry.tool}${entry.path ? `(${entry.path})` : \"\"}: ${entry.error}`)\n\t\t.join(\" | \");\n}\n"]}