evil-omo 3.12.0 → 3.12.3

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/dist/index.js CHANGED
@@ -16190,7 +16190,7 @@ async function resolveFileReferencesInText(text, cwd = process.cwd(), depth = 0,
16190
16190
  }
16191
16191
  let resolved = text;
16192
16192
  for (const [pattern, replacement] of replacements.entries()) {
16193
- resolved = resolved.split(pattern).join(replacement);
16193
+ resolved = resolved.replaceAll(pattern, replacement);
16194
16194
  }
16195
16195
  if (findFileReferences(resolved).length > 0 && depth + 1 < maxDepth) {
16196
16196
  return resolveFileReferencesInText(resolved, cwd, depth + 1, maxDepth);
@@ -16289,6 +16289,7 @@ function transformToolName(toolName) {
16289
16289
  function escapeRegexExceptAsterisk(str2) {
16290
16290
  return str2.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
16291
16291
  }
16292
+ var regexCache = new Map;
16292
16293
  function matchesToolMatcher(toolName, matcher) {
16293
16294
  if (!matcher) {
16294
16295
  return true;
@@ -16296,8 +16297,12 @@ function matchesToolMatcher(toolName, matcher) {
16296
16297
  const patterns = matcher.split("|").map((p) => p.trim());
16297
16298
  return patterns.some((p) => {
16298
16299
  if (p.includes("*")) {
16299
- const escaped = escapeRegexExceptAsterisk(p);
16300
- const regex = new RegExp(`^${escaped.replace(/\*/g, ".*")}$`, "i");
16300
+ let regex = regexCache.get(p);
16301
+ if (!regex) {
16302
+ const escaped = escapeRegexExceptAsterisk(p);
16303
+ regex = new RegExp(`^${escaped.replace(/\*/g, ".*")}$`, "i");
16304
+ regexCache.set(p, regex);
16305
+ }
16301
16306
  return regex.test(toolName);
16302
16307
  }
16303
16308
  return p.toLowerCase() === toolName.toLowerCase();
@@ -17653,120 +17658,156 @@ import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync
17653
17658
  import { join as join10 } from "path";
17654
17659
  var CONNECTED_PROVIDERS_CACHE_FILE = "connected-providers.json";
17655
17660
  var PROVIDER_MODELS_CACHE_FILE = "provider-models.json";
17656
- function getCacheFilePath(filename) {
17657
- return join10(getOmoOpenCodeCacheDir(), filename);
17658
- }
17659
- function ensureCacheDir2() {
17660
- const cacheDir = getOmoOpenCodeCacheDir();
17661
- if (!existsSync8(cacheDir)) {
17662
- mkdirSync2(cacheDir, { recursive: true });
17663
- }
17664
- }
17665
- function readConnectedProvidersCache() {
17666
- const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE);
17667
- if (!existsSync8(cacheFile)) {
17668
- log("[connected-providers-cache] Cache file not found", { cacheFile });
17669
- return null;
17661
+ function createConnectedProvidersCacheStore(getCacheDir2 = getOmoOpenCodeCacheDir) {
17662
+ function getCacheFilePath(filename) {
17663
+ return join10(getCacheDir2(), filename);
17664
+ }
17665
+ let memConnected;
17666
+ let memProviderModels;
17667
+ function ensureCacheDir2() {
17668
+ const cacheDir = getCacheDir2();
17669
+ if (!existsSync8(cacheDir)) {
17670
+ mkdirSync2(cacheDir, { recursive: true });
17671
+ }
17672
+ }
17673
+ function readConnectedProvidersCache() {
17674
+ if (memConnected !== undefined)
17675
+ return memConnected;
17676
+ const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE);
17677
+ if (!existsSync8(cacheFile)) {
17678
+ log("[connected-providers-cache] Cache file not found", { cacheFile });
17679
+ memConnected = null;
17680
+ return null;
17681
+ }
17682
+ try {
17683
+ const content = readFileSync4(cacheFile, "utf-8");
17684
+ const data = JSON.parse(content);
17685
+ log("[connected-providers-cache] Read cache", { count: data.connected.length, updatedAt: data.updatedAt });
17686
+ memConnected = data.connected;
17687
+ return data.connected;
17688
+ } catch (err) {
17689
+ log("[connected-providers-cache] Error reading cache", { error: String(err) });
17690
+ memConnected = null;
17691
+ return null;
17692
+ }
17670
17693
  }
17671
- try {
17672
- const content = readFileSync4(cacheFile, "utf-8");
17673
- const data = JSON.parse(content);
17674
- log("[connected-providers-cache] Read cache", { count: data.connected.length, updatedAt: data.updatedAt });
17675
- return data.connected;
17676
- } catch (err) {
17677
- log("[connected-providers-cache] Error reading cache", { error: String(err) });
17678
- return null;
17694
+ function hasConnectedProvidersCache() {
17695
+ const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE);
17696
+ return existsSync8(cacheFile);
17679
17697
  }
17680
- }
17681
- function hasConnectedProvidersCache() {
17682
- const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE);
17683
- return existsSync8(cacheFile);
17684
- }
17685
- function writeConnectedProvidersCache(connected) {
17686
- ensureCacheDir2();
17687
- const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE);
17688
- const data = {
17689
- connected,
17690
- updatedAt: new Date().toISOString()
17691
- };
17692
- try {
17693
- writeFileSync2(cacheFile, JSON.stringify(data, null, 2));
17694
- log("[connected-providers-cache] Cache written", { count: connected.length });
17695
- } catch (err) {
17696
- log("[connected-providers-cache] Error writing cache", { error: String(err) });
17697
- }
17698
- }
17699
- function readProviderModelsCache() {
17700
- const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE);
17701
- if (!existsSync8(cacheFile)) {
17702
- log("[connected-providers-cache] Provider-models cache file not found", { cacheFile });
17703
- return null;
17698
+ function writeConnectedProvidersCache(connected) {
17699
+ ensureCacheDir2();
17700
+ const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE);
17701
+ const data = {
17702
+ connected,
17703
+ updatedAt: new Date().toISOString()
17704
+ };
17705
+ try {
17706
+ writeFileSync2(cacheFile, JSON.stringify(data, null, 2));
17707
+ memConnected = connected;
17708
+ log("[connected-providers-cache] Cache written", { count: connected.length });
17709
+ } catch (err) {
17710
+ log("[connected-providers-cache] Error writing cache", { error: String(err) });
17711
+ }
17704
17712
  }
17705
- try {
17706
- const content = readFileSync4(cacheFile, "utf-8");
17707
- const data = JSON.parse(content);
17708
- log("[connected-providers-cache] Read provider-models cache", {
17709
- providerCount: Object.keys(data.models).length,
17710
- updatedAt: data.updatedAt
17711
- });
17712
- return data;
17713
- } catch (err) {
17714
- log("[connected-providers-cache] Error reading provider-models cache", { error: String(err) });
17715
- return null;
17713
+ function readProviderModelsCache() {
17714
+ if (memProviderModels !== undefined)
17715
+ return memProviderModels;
17716
+ const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE);
17717
+ if (!existsSync8(cacheFile)) {
17718
+ log("[connected-providers-cache] Provider-models cache file not found", { cacheFile });
17719
+ memProviderModels = null;
17720
+ return null;
17721
+ }
17722
+ try {
17723
+ const content = readFileSync4(cacheFile, "utf-8");
17724
+ const data = JSON.parse(content);
17725
+ log("[connected-providers-cache] Read provider-models cache", {
17726
+ providerCount: Object.keys(data.models).length,
17727
+ updatedAt: data.updatedAt
17728
+ });
17729
+ memProviderModels = data;
17730
+ return data;
17731
+ } catch (err) {
17732
+ log("[connected-providers-cache] Error reading provider-models cache", { error: String(err) });
17733
+ memProviderModels = null;
17734
+ return null;
17735
+ }
17716
17736
  }
17717
- }
17718
- function hasProviderModelsCache() {
17719
- const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE);
17720
- return existsSync8(cacheFile);
17721
- }
17722
- function writeProviderModelsCache(data) {
17723
- ensureCacheDir2();
17724
- const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE);
17725
- const cacheData = {
17726
- ...data,
17727
- updatedAt: new Date().toISOString()
17728
- };
17729
- try {
17730
- writeFileSync2(cacheFile, JSON.stringify(cacheData, null, 2));
17731
- log("[connected-providers-cache] Provider-models cache written", {
17732
- providerCount: Object.keys(data.models).length
17733
- });
17734
- } catch (err) {
17735
- log("[connected-providers-cache] Error writing provider-models cache", { error: String(err) });
17737
+ function hasProviderModelsCache() {
17738
+ const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE);
17739
+ return existsSync8(cacheFile);
17736
17740
  }
17737
- }
17738
- async function updateConnectedProvidersCache(client) {
17739
- if (!client?.provider?.list) {
17740
- log("[connected-providers-cache] client.provider.list not available");
17741
- return;
17741
+ function writeProviderModelsCache(data) {
17742
+ ensureCacheDir2();
17743
+ const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE);
17744
+ const cacheData = {
17745
+ ...data,
17746
+ updatedAt: new Date().toISOString()
17747
+ };
17748
+ try {
17749
+ writeFileSync2(cacheFile, JSON.stringify(cacheData, null, 2));
17750
+ memProviderModels = cacheData;
17751
+ log("[connected-providers-cache] Provider-models cache written", {
17752
+ providerCount: Object.keys(data.models).length
17753
+ });
17754
+ } catch (err) {
17755
+ log("[connected-providers-cache] Error writing provider-models cache", { error: String(err) });
17756
+ }
17742
17757
  }
17743
- try {
17744
- const result = await client.provider.list();
17745
- const connected = result.data?.connected ?? [];
17746
- log("[connected-providers-cache] Fetched connected providers", { count: connected.length, providers: connected });
17747
- writeConnectedProvidersCache(connected);
17748
- const modelsByProvider = {};
17749
- const allProviders = result.data?.all ?? [];
17750
- for (const provider of allProviders) {
17751
- if (provider.models) {
17752
- const modelIds = Object.keys(provider.models);
17753
- if (modelIds.length > 0) {
17754
- modelsByProvider[provider.id] = modelIds;
17758
+ async function updateConnectedProvidersCache(client) {
17759
+ if (!client?.provider?.list) {
17760
+ log("[connected-providers-cache] client.provider.list not available");
17761
+ return;
17762
+ }
17763
+ try {
17764
+ const result = await client.provider.list();
17765
+ const connected = result.data?.connected ?? [];
17766
+ log("[connected-providers-cache] Fetched connected providers", {
17767
+ count: connected.length,
17768
+ providers: connected
17769
+ });
17770
+ writeConnectedProvidersCache(connected);
17771
+ const modelsByProvider = {};
17772
+ const allProviders = result.data?.all ?? [];
17773
+ for (const provider of allProviders) {
17774
+ if (provider.models) {
17775
+ const modelIds = Object.keys(provider.models);
17776
+ if (modelIds.length > 0) {
17777
+ modelsByProvider[provider.id] = modelIds;
17778
+ }
17755
17779
  }
17756
17780
  }
17781
+ log("[connected-providers-cache] Extracted models from provider list", {
17782
+ providerCount: Object.keys(modelsByProvider).length,
17783
+ totalModels: Object.values(modelsByProvider).reduce((sum, ids) => sum + ids.length, 0)
17784
+ });
17785
+ writeProviderModelsCache({
17786
+ models: modelsByProvider,
17787
+ connected
17788
+ });
17789
+ } catch (err) {
17790
+ log("[connected-providers-cache] Error updating cache", { error: String(err) });
17757
17791
  }
17758
- log("[connected-providers-cache] Extracted models from provider list", {
17759
- providerCount: Object.keys(modelsByProvider).length,
17760
- totalModels: Object.values(modelsByProvider).reduce((sum, ids) => sum + ids.length, 0)
17761
- });
17762
- writeProviderModelsCache({
17763
- models: modelsByProvider,
17764
- connected
17765
- });
17766
- } catch (err) {
17767
- log("[connected-providers-cache] Error updating cache", { error: String(err) });
17768
17792
  }
17793
+ return {
17794
+ readConnectedProvidersCache,
17795
+ hasConnectedProvidersCache,
17796
+ readProviderModelsCache,
17797
+ hasProviderModelsCache,
17798
+ writeProviderModelsCache,
17799
+ updateConnectedProvidersCache
17800
+ };
17769
17801
  }
17802
+ var defaultConnectedProvidersCacheStore = createConnectedProvidersCacheStore(() => getOmoOpenCodeCacheDir());
17803
+ var {
17804
+ readConnectedProvidersCache,
17805
+ hasConnectedProvidersCache,
17806
+ readProviderModelsCache,
17807
+ hasProviderModelsCache,
17808
+ writeProviderModelsCache,
17809
+ updateConnectedProvidersCache
17810
+ } = defaultConnectedProvidersCacheStore;
17770
17811
 
17771
17812
  // src/shared/model-availability.ts
17772
17813
  init_logger();
@@ -20491,12 +20532,14 @@ async function handleSessionIdle(args) {
20491
20532
  return;
20492
20533
  }
20493
20534
  if (!todos || todos.length === 0) {
20535
+ sessionStateStore.resetContinuationProgress(sessionID);
20494
20536
  sessionStateStore.resetContinuationProgress(sessionID);
20495
20537
  log(`[${HOOK_NAME}] No todos`, { sessionID });
20496
20538
  return;
20497
20539
  }
20498
20540
  const incompleteCount = getIncompleteCount(todos);
20499
20541
  if (incompleteCount === 0) {
20542
+ sessionStateStore.resetContinuationProgress(sessionID);
20500
20543
  sessionStateStore.resetContinuationProgress(sessionID);
20501
20544
  log(`[${HOOK_NAME}] All todos complete`, { sessionID, total: todos.length });
20502
20545
  return;
@@ -20510,20 +20553,12 @@ async function handleSessionIdle(args) {
20510
20553
  log(`[${HOOK_NAME}] Reset consecutive failures after recovery window`, { sessionID, failureResetWindowMs: FAILURE_RESET_WINDOW_MS });
20511
20554
  }
20512
20555
  if (state2.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
20513
- log(`[${HOOK_NAME}] Skipped: max consecutive failures reached`, {
20514
- sessionID,
20515
- consecutiveFailures: state2.consecutiveFailures,
20516
- maxConsecutiveFailures: MAX_CONSECUTIVE_FAILURES
20517
- });
20556
+ log(`[${HOOK_NAME}] Skipped: max consecutive failures reached`, { sessionID, consecutiveFailures: state2.consecutiveFailures });
20518
20557
  return;
20519
20558
  }
20520
20559
  const effectiveCooldown = CONTINUATION_COOLDOWN_MS * Math.pow(2, Math.min(state2.consecutiveFailures, 5));
20521
20560
  if (state2.lastInjectedAt && Date.now() - state2.lastInjectedAt < effectiveCooldown) {
20522
- log(`[${HOOK_NAME}] Skipped: cooldown active`, {
20523
- sessionID,
20524
- effectiveCooldown,
20525
- consecutiveFailures: state2.consecutiveFailures
20526
- });
20561
+ log(`[${HOOK_NAME}] Skipped: cooldown active`, { sessionID, effectiveCooldown, consecutiveFailures: state2.consecutiveFailures });
20527
20562
  return;
20528
20563
  }
20529
20564
  let resolvedInfo;
@@ -36377,6 +36412,15 @@ function takePendingCall(callID) {
36377
36412
  import * as fs6 from "fs";
36378
36413
  import { tmpdir as tmpdir4 } from "os";
36379
36414
  import { join as join31 } from "path";
36415
+ var ApplyPatchMetadataSchema = zod_default.object({
36416
+ files: zod_default.array(zod_default.object({
36417
+ filePath: zod_default.string(),
36418
+ movePath: zod_default.string().optional(),
36419
+ before: zod_default.string(),
36420
+ after: zod_default.string(),
36421
+ type: zod_default.string().optional()
36422
+ }))
36423
+ });
36380
36424
  var DEBUG3 = process.env.COMMENT_CHECKER_DEBUG === "1";
36381
36425
  var DEBUG_FILE3 = join31(tmpdir4(), "comment-checker-debug.log");
36382
36426
  function debugLog3(...args) {
@@ -36437,15 +36481,6 @@ function createCommentCheckerHooks(config2) {
36437
36481
  debugLog3("skipping due to tool failure in output");
36438
36482
  return;
36439
36483
  }
36440
- const ApplyPatchMetadataSchema = zod_default.object({
36441
- files: zod_default.array(zod_default.object({
36442
- filePath: zod_default.string(),
36443
- movePath: zod_default.string().optional(),
36444
- before: zod_default.string(),
36445
- after: zod_default.string(),
36446
- type: zod_default.string().optional()
36447
- }))
36448
- });
36449
36484
  if (toolLower === "apply_patch") {
36450
36485
  const parsed = ApplyPatchMetadataSchema.safeParse(output.metadata);
36451
36486
  if (!parsed.success) {
@@ -36916,7 +36951,7 @@ function isTokenLimitError(text) {
36916
36951
  return false;
36917
36952
  }
36918
36953
  const lower = text.toLowerCase();
36919
- return TOKEN_LIMIT_KEYWORDS.some((kw) => lower.includes(kw.toLowerCase()));
36954
+ return TOKEN_LIMIT_KEYWORDS.some((kw) => lower.includes(kw));
36920
36955
  }
36921
36956
  function parseAnthropicTokenLimitError(err) {
36922
36957
  try {
@@ -38334,7 +38369,6 @@ function createAnthropicContextWindowLimitRecoveryHook(ctx, options) {
38334
38369
  };
38335
38370
  }
38336
38371
  // src/hooks/think-mode/detector.ts
38337
- var ENGLISH_PATTERNS = [/\bultrathink\b/i, /\bthink\b/i];
38338
38372
  var MULTILINGUAL_KEYWORDS = [
38339
38373
  "\uC0DD\uAC01",
38340
38374
  "\uAC80\uD1A0",
@@ -38420,8 +38454,7 @@ var MULTILINGUAL_KEYWORDS = [
38420
38454
  "fikir",
38421
38455
  "berfikir"
38422
38456
  ];
38423
- var MULTILINGUAL_PATTERNS = MULTILINGUAL_KEYWORDS.map((kw) => new RegExp(kw, "i"));
38424
- var THINK_PATTERNS = [...ENGLISH_PATTERNS, ...MULTILINGUAL_PATTERNS];
38457
+ var COMBINED_THINK_PATTERN = new RegExp(`\\b(?:ultrathink|think)\\b|${MULTILINGUAL_KEYWORDS.join("|")}`, "i");
38425
38458
  var CODE_BLOCK_PATTERN = /```[\s\S]*?```/g;
38426
38459
  var INLINE_CODE_PATTERN = /`[^`]+`/g;
38427
38460
  function removeCodeBlocks(text) {
@@ -38429,7 +38462,7 @@ function removeCodeBlocks(text) {
38429
38462
  }
38430
38463
  function detectThinkKeyword(text) {
38431
38464
  const textWithoutCode = removeCodeBlocks(text);
38432
- return THINK_PATTERNS.some((pattern) => pattern.test(textWithoutCode));
38465
+ return COMBINED_THINK_PATTERN.test(textWithoutCode);
38433
38466
  }
38434
38467
  function extractPromptText(parts) {
38435
38468
  return parts.filter((p) => p.type === "text").map((p) => p.text || "").join("");
@@ -39086,16 +39119,16 @@ async function loadPluginExtendedConfig() {
39086
39119
  }
39087
39120
  return merged;
39088
39121
  }
39089
- var regexCache = new Map;
39122
+ var regexCache2 = new Map;
39090
39123
  function getRegex(pattern) {
39091
- let regex = regexCache.get(pattern);
39124
+ let regex = regexCache2.get(pattern);
39092
39125
  if (!regex) {
39093
39126
  try {
39094
39127
  regex = new RegExp(pattern);
39095
- regexCache.set(pattern, regex);
39128
+ regexCache2.set(pattern, regex);
39096
39129
  } catch {
39097
39130
  regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
39098
- regexCache.set(pattern, regex);
39131
+ regexCache2.set(pattern, regex);
39099
39132
  }
39100
39133
  }
39101
39134
  return regex;
@@ -39961,8 +39994,6 @@ function createToolExecuteAfterHandler(ctx, config2) {
39961
39994
  if (!output) {
39962
39995
  return;
39963
39996
  }
39964
- const claudeConfig = await loadClaudeHooksConfig();
39965
- const extendedConfig = await loadPluginExtendedConfig();
39966
39997
  const cachedInput = getToolInput(input.sessionID, input.tool, input.callID) || {};
39967
39998
  appendTranscriptEntry(input.sessionID, {
39968
39999
  type: "tool_result",
@@ -39974,6 +40005,8 @@ function createToolExecuteAfterHandler(ctx, config2) {
39974
40005
  if (isHookDisabled(config2, "PostToolUse")) {
39975
40006
  return;
39976
40007
  }
40008
+ const claudeConfig = await loadClaudeHooksConfig();
40009
+ const extendedConfig = await loadPluginExtendedConfig();
39977
40010
  const postClient = {
39978
40011
  session: {
39979
40012
  messages: (opts) => ctx.client.session.messages(opts)
@@ -40154,8 +40187,6 @@ function createToolExecuteBeforeHandler(ctx, config2) {
40154
40187
  output.args.todos = parsed;
40155
40188
  log("todowrite: parsed todos string to array", { sessionID: input.sessionID });
40156
40189
  }
40157
- const claudeConfig = await loadClaudeHooksConfig();
40158
- const extendedConfig = await loadPluginExtendedConfig();
40159
40190
  appendTranscriptEntry(input.sessionID, {
40160
40191
  type: "tool_use",
40161
40192
  timestamp: new Date().toISOString(),
@@ -40166,6 +40197,8 @@ function createToolExecuteBeforeHandler(ctx, config2) {
40166
40197
  if (isHookDisabled(config2, "PreToolUse")) {
40167
40198
  return;
40168
40199
  }
40200
+ const claudeConfig = await loadClaudeHooksConfig();
40201
+ const extendedConfig = await loadPluginExtendedConfig();
40169
40202
  const preCtx = {
40170
40203
  sessionId: input.sessionID,
40171
40204
  toolName: input.tool,
@@ -43759,6 +43792,9 @@ async function injectContinuationPrompt(ctx, options) {
43759
43792
  async function handleDetectedCompletion(ctx, input) {
43760
43793
  const { sessionID, state: state3, loopState, directory, apiTimeoutMs } = input;
43761
43794
  if (state3.ultrawork && !state3.verification_pending) {
43795
+ if (state3.verification_session_id) {
43796
+ ctx.client.session.abort({ path: { id: state3.verification_session_id } }).catch(() => {});
43797
+ }
43762
43798
  const verificationState = loopState.markVerificationPending(sessionID);
43763
43799
  if (!verificationState) {
43764
43800
  log(`[${HOOK_NAME3}] Failed to transition ultrawork loop to verification`, {
@@ -43995,6 +44031,9 @@ async function handleFailedVerification(ctx, input) {
43995
44031
  });
43996
44032
  return false;
43997
44033
  }
44034
+ if (state3.verification_session_id) {
44035
+ ctx.client.session.abort({ path: { id: state3.verification_session_id } }).catch(() => {});
44036
+ }
43998
44037
  const resumedState = loopState.restartAfterFailedVerification(parentSessionID, messageCountAtStart);
43999
44038
  if (!resumedState) {
44000
44039
  log(`[${HOOK_NAME3}] Failed to restart loop after verification failure`, {
@@ -47600,9 +47639,9 @@ var BabysittingConfigSchema = exports_external.object({
47600
47639
  });
47601
47640
  // src/config/schema/background-task.ts
47602
47641
  var CircuitBreakerConfigSchema = exports_external.object({
47642
+ enabled: exports_external.boolean().optional(),
47603
47643
  maxToolCalls: exports_external.number().int().min(10).optional(),
47604
- windowSize: exports_external.number().int().min(5).optional(),
47605
- repetitionThresholdPercent: exports_external.number().gt(0).max(100).optional()
47644
+ consecutiveThreshold: exports_external.number().int().min(5).optional()
47606
47645
  });
47607
47646
  var BackgroundTaskConfigSchema = exports_external.object({
47608
47647
  defaultConcurrency: exports_external.number().min(1).optional(),
@@ -47798,7 +47837,8 @@ var HookNameSchema = exports_external.enum([
47798
47837
  "write-existing-file-guard",
47799
47838
  "anthropic-effort",
47800
47839
  "hashline-read-enhancer",
47801
- "read-image-resizer"
47840
+ "read-image-resizer",
47841
+ "todo-description-override"
47802
47842
  ]);
47803
47843
  // src/config/schema/notification.ts
47804
47844
  var NotificationConfigSchema = exports_external.object({
@@ -49201,7 +49241,7 @@ var START_WORK_TEMPLATE = `You are starting a Sisyphus work session.
49201
49241
  - \`--worktree <path>\` (optional): absolute path to an existing git worktree to work in
49202
49242
  - If specified and valid: hook pre-sets worktree_path in boulder.json
49203
49243
  - If specified but invalid: you must run \`git worktree add <path> <branch>\` first
49204
- - If omitted: you MUST choose or create a worktree (see Worktree Setup below)
49244
+ - If omitted: work directly in the current project directory (no worktree)
49205
49245
 
49206
49246
  ## WHAT TO DO
49207
49247
 
@@ -49218,7 +49258,7 @@ var START_WORK_TEMPLATE = `You are starting a Sisyphus work session.
49218
49258
  - If ONE plan: auto-select it
49219
49259
  - If MULTIPLE plans: show list with timestamps, ask user to select
49220
49260
 
49221
- 4. **Worktree Setup** (when \`worktree_path\` not already set in boulder.json):
49261
+ 4. **Worktree Setup** (ONLY when \`--worktree\` was explicitly specified and \`worktree_path\` not already set in boulder.json):
49222
49262
  1. \`git worktree list --porcelain\` \u2014 see available worktrees
49223
49263
  2. Create: \`git worktree add <absolute-path> <branch-or-HEAD>\`
49224
49264
  3. Update boulder.json to add \`"worktree_path": "<absolute-path>"\`
@@ -49280,9 +49320,41 @@ Reading plan and beginning execution...
49280
49320
 
49281
49321
  - The session_id is injected by the hook - use it directly
49282
49322
  - Always update boulder.json BEFORE starting work
49283
- - Always set worktree_path in boulder.json before executing any tasks
49323
+ - If worktree_path is set in boulder.json, all work happens inside that worktree directory
49284
49324
  - Read the FULL plan file before delegating any tasks
49285
- - Follow atlas delegation protocols (7-section format)`;
49325
+ - Follow atlas delegation protocols (7-section format)
49326
+
49327
+ ## TASK BREAKDOWN (MANDATORY)
49328
+
49329
+ After reading the plan file, you MUST decompose every plan task into granular, implementation-level sub-steps and register ALL of them as task/todo items BEFORE starting any work.
49330
+
49331
+ **How to break down**:
49332
+ - Each plan checkbox item (e.g., \`- [ ] Add user authentication\`) must be split into concrete, actionable sub-tasks
49333
+ - Sub-tasks should be specific enough that each one touches a clear set of files/functions
49334
+ - Include: file to modify, what to change, expected behavior, and how to verify
49335
+ - Do NOT leave any task vague \u2014 "implement feature X" is NOT acceptable; "add validateToken() to src/auth/middleware.ts that checks JWT expiry and returns 401" IS acceptable
49336
+
49337
+ **Example breakdown**:
49338
+ Plan task: \`- [ ] Add rate limiting to API\`
49339
+ \u2192 Todo items:
49340
+ 1. Create \`src/middleware/rate-limiter.ts\` with sliding window algorithm (max 100 req/min per IP)
49341
+ 2. Add RateLimiter middleware to \`src/app.ts\` router chain, before auth middleware
49342
+ 3. Add rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining) to response in \`rate-limiter.ts\`
49343
+ 4. Add test: verify 429 response after exceeding limit in \`src/middleware/rate-limiter.test.ts\`
49344
+ 5. Add test: verify headers are present on normal responses
49345
+
49346
+ Register these as task/todo items so progress is tracked and visible throughout the session.
49347
+
49348
+ ## WORKTREE COMPLETION
49349
+
49350
+ When working in a worktree (\`worktree_path\` is set in boulder.json) and ALL plan tasks are complete:
49351
+ 1. Commit all remaining changes in the worktree
49352
+ 2. Switch to the main working directory (the original repo, NOT the worktree)
49353
+ 3. Merge the worktree branch into the current branch: \`git merge <worktree-branch>\`
49354
+ 4. If merge succeeds, clean up: \`git worktree remove <worktree-path>\`
49355
+ 5. Remove the boulder.json state
49356
+
49357
+ This is the DEFAULT behavior when \`--worktree\` was used. Skip merge only if the user explicitly instructs otherwise (e.g., asks to create a PR instead).`;
49286
49358
 
49287
49359
  // src/features/builtin-commands/templates/handoff.ts
49288
49360
  var HANDOFF_TEMPLATE = `# Handoff Command
@@ -49664,9 +49736,6 @@ function skillToCommandInfo(skill) {
49664
49736
  lazyContentLoader: skill.lazyContent
49665
49737
  };
49666
49738
  }
49667
- function filterDiscoveredCommandsByScope(commands3, scope) {
49668
- return commands3.filter((command) => command.scope === scope);
49669
- }
49670
49739
  async function discoverAllCommands(options) {
49671
49740
  const discoveredCommands = discoverCommandsSync(process.cwd(), {
49672
49741
  pluginsEnabled: options?.pluginsEnabled,
@@ -49674,14 +49743,17 @@ async function discoverAllCommands(options) {
49674
49743
  });
49675
49744
  const skills2 = options?.skills ?? await discoverAllSkills();
49676
49745
  const skillCommands = skills2.map(skillToCommandInfo);
49746
+ const scopeOrder = ["project", "user", "opencode-project", "opencode", "builtin", "plugin"];
49747
+ const grouped = new Map;
49748
+ for (const cmd of discoveredCommands) {
49749
+ const list = grouped.get(cmd.scope) ?? [];
49750
+ list.push(cmd);
49751
+ grouped.set(cmd.scope, list);
49752
+ }
49753
+ const orderedCommands = scopeOrder.flatMap((scope) => grouped.get(scope) ?? []);
49677
49754
  return [
49678
49755
  ...skillCommands,
49679
- ...filterDiscoveredCommandsByScope(discoveredCommands, "project"),
49680
- ...filterDiscoveredCommandsByScope(discoveredCommands, "user"),
49681
- ...filterDiscoveredCommandsByScope(discoveredCommands, "opencode-project"),
49682
- ...filterDiscoveredCommandsByScope(discoveredCommands, "opencode"),
49683
- ...filterDiscoveredCommandsByScope(discoveredCommands, "builtin"),
49684
- ...filterDiscoveredCommandsByScope(discoveredCommands, "plugin")
49756
+ ...orderedCommands
49685
49757
  ];
49686
49758
  }
49687
49759
  async function findCommand2(commandName, options) {
@@ -53440,6 +53512,7 @@ function getErrorMessage2(error48) {
53440
53512
  return "";
53441
53513
  }
53442
53514
  }
53515
+ var DEFAULT_RETRY_PATTERN = new RegExp(`\\b(${DEFAULT_CONFIG2.retry_on_errors.join("|")})\\b`);
53443
53516
  function extractStatusCode(error48, retryOnErrors) {
53444
53517
  if (!error48)
53445
53518
  return;
@@ -53454,8 +53527,7 @@ function extractStatusCode(error48, retryOnErrors) {
53454
53527
  if (statusCode !== undefined) {
53455
53528
  return statusCode;
53456
53529
  }
53457
- const codes = retryOnErrors ?? DEFAULT_CONFIG2.retry_on_errors;
53458
- const pattern = new RegExp(`\\b(${codes.join("|")})\\b`);
53530
+ const pattern = retryOnErrors ? new RegExp(`\\b(${retryOnErrors.join("|")})\\b`) : DEFAULT_RETRY_PATTERN;
53459
53531
  const message = getErrorMessage2(error48);
53460
53532
  const statusMatch = message.match(pattern);
53461
53533
  if (statusMatch) {
@@ -55146,6 +55218,46 @@ function createReadImageResizerHook(_ctx) {
55146
55218
  }
55147
55219
  };
55148
55220
  }
55221
+ // src/hooks/todo-description-override/description.ts
55222
+ var TODOWRITE_DESCRIPTION = `Use this tool to create and manage a structured task list for tracking progress on multi-step work.
55223
+
55224
+ ## Todo Format (MANDATORY)
55225
+
55226
+ Each todo title MUST encode four elements: WHERE, WHY, HOW, and EXPECTED RESULT.
55227
+
55228
+ Format: "[WHERE] [HOW] to [WHY] \u2014 expect [RESULT]"
55229
+
55230
+ GOOD:
55231
+ - "src/utils/validation.ts: Add validateEmail() for input sanitization \u2014 returns boolean"
55232
+ - "UserService.create(): Call validateEmail() before DB insert \u2014 rejects invalid emails with 400"
55233
+ - "validation.test.ts: Add test for missing @ sign \u2014 expect validateEmail('foo') to return false"
55234
+
55235
+ BAD:
55236
+ - "Implement email validation" (where? how? what result?)
55237
+ - "Add dark mode" (this is a feature, not a todo)
55238
+ - "Fix auth" (what file? what changes? what's expected?)
55239
+
55240
+ ## Granularity Rules
55241
+
55242
+ Each todo MUST be a single atomic action completable in 1-3 tool calls. If it needs more, split it.
55243
+
55244
+ **Size test**: Can you complete this todo by editing one file or running one command? If not, it's too big.
55245
+
55246
+ ## Task Management
55247
+ - One in_progress at a time. Complete it before starting the next.
55248
+ - Mark completed immediately after finishing each item.
55249
+ - Skip this tool for single trivial tasks (one-step, obvious action).`;
55250
+
55251
+ // src/hooks/todo-description-override/hook.ts
55252
+ function createTodoDescriptionOverrideHook() {
55253
+ return {
55254
+ "tool.definition": async (input, output) => {
55255
+ if (input.toolID === "todowrite") {
55256
+ output.description = TODOWRITE_DESCRIPTION;
55257
+ }
55258
+ }
55259
+ };
55260
+ }
55149
55261
  // src/hooks/anthropic-effort/hook.ts
55150
55262
  var OPUS_4_6_PATTERN = /claude-opus-4[-.]6/i;
55151
55263
  function isClaudeProvider(providerID, modelID) {
@@ -75723,10 +75835,11 @@ Returns summary format: id, subject, status, owner, blockedBy (not full descript
75723
75835
  allTasks.push(task);
75724
75836
  }
75725
75837
  }
75838
+ const taskMap = new Map(allTasks.map((t) => [t.id, t]));
75726
75839
  const activeTasks = allTasks.filter((task) => task.status !== "completed" && task.status !== "deleted");
75727
75840
  const summaries = activeTasks.map((task) => {
75728
75841
  const unresolvedBlockers = task.blockedBy.filter((blockerId) => {
75729
- const blockerTask = allTasks.find((t) => t.id === blockerId);
75842
+ const blockerTask = taskMap.get(blockerId);
75730
75843
  return !blockerTask || blockerTask.status !== "completed";
75731
75844
  });
75732
75845
  return {
@@ -76415,6 +76528,15 @@ function applyPrepend(lines, text) {
76415
76528
  }
76416
76529
 
76417
76530
  // src/tools/hashline-edit/edit-operations.ts
76531
+ function arraysEqual(a, b) {
76532
+ if (a.length !== b.length)
76533
+ return false;
76534
+ for (let i2 = 0;i2 < a.length; i2++) {
76535
+ if (a[i2] !== b[i2])
76536
+ return false;
76537
+ }
76538
+ return true;
76539
+ }
76418
76540
  function applyHashlineEditsWithReport(content, edits) {
76419
76541
  if (edits.length === 0) {
76420
76542
  return {
@@ -76444,9 +76566,7 @@ function applyHashlineEditsWithReport(content, edits) {
76444
76566
  switch (edit.op) {
76445
76567
  case "replace": {
76446
76568
  const next = edit.end ? applyReplaceLines(lines, edit.pos, edit.end, edit.lines, { skipValidation: true }) : applySetLine(lines, edit.pos, edit.lines, { skipValidation: true });
76447
- if (next.join(`
76448
- `) === lines.join(`
76449
- `)) {
76569
+ if (arraysEqual(next, lines)) {
76450
76570
  noopEdits += 1;
76451
76571
  break;
76452
76572
  }
@@ -76455,9 +76575,7 @@ function applyHashlineEditsWithReport(content, edits) {
76455
76575
  }
76456
76576
  case "append": {
76457
76577
  const next = edit.pos ? applyInsertAfter(lines, edit.pos, edit.lines, { skipValidation: true }) : applyAppend(lines, edit.lines);
76458
- if (next.join(`
76459
- `) === lines.join(`
76460
- `)) {
76578
+ if (arraysEqual(next, lines)) {
76461
76579
  noopEdits += 1;
76462
76580
  break;
76463
76581
  }
@@ -76466,9 +76584,7 @@ function applyHashlineEditsWithReport(content, edits) {
76466
76584
  }
76467
76585
  case "prepend": {
76468
76586
  const next = edit.pos ? applyInsertBefore(lines, edit.pos, edit.lines, { skipValidation: true }) : applyPrepend(lines, edit.lines);
76469
- if (next.join(`
76470
- `) === lines.join(`
76471
- `)) {
76587
+ if (arraysEqual(next, lines)) {
76472
76588
  noopEdits += 1;
76473
76589
  break;
76474
76590
  }
@@ -77493,6 +77609,7 @@ function createToolGuardHooks(args) {
77493
77609
  const hashlineReadEnhancer = isHookEnabled("hashline-read-enhancer") ? safeHook("hashline-read-enhancer", () => createHashlineReadEnhancerHook(ctx, { hashline_edit: { enabled: pluginConfig.hashline_edit ?? false } })) : null;
77494
77610
  const jsonErrorRecovery = isHookEnabled("json-error-recovery") ? safeHook("json-error-recovery", () => createJsonErrorRecoveryHook(ctx)) : null;
77495
77611
  const readImageResizer = isHookEnabled("read-image-resizer") ? safeHook("read-image-resizer", () => createReadImageResizerHook(ctx)) : null;
77612
+ const todoDescriptionOverride = isHookEnabled("todo-description-override") ? safeHook("todo-description-override", () => createTodoDescriptionOverrideHook()) : null;
77496
77613
  return {
77497
77614
  commentChecker,
77498
77615
  toolOutputTruncator,
@@ -77504,7 +77621,8 @@ function createToolGuardHooks(args) {
77504
77621
  writeExistingFileGuard,
77505
77622
  hashlineReadEnhancer,
77506
77623
  jsonErrorRecovery,
77507
- readImageResizer
77624
+ readImageResizer,
77625
+ todoDescriptionOverride
77508
77626
  };
77509
77627
  }
77510
77628
 
@@ -78031,8 +78149,8 @@ var MIN_STABILITY_TIME_MS2 = 10 * 1000;
78031
78149
  var DEFAULT_STALE_TIMEOUT_MS = 1200000;
78032
78150
  var DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS = 1800000;
78033
78151
  var DEFAULT_MAX_TOOL_CALLS = 200;
78034
- var DEFAULT_CIRCUIT_BREAKER_WINDOW_SIZE = 20;
78035
- var DEFAULT_CIRCUIT_BREAKER_REPETITION_THRESHOLD_PERCENT = 80;
78152
+ var DEFAULT_CIRCUIT_BREAKER_CONSECUTIVE_THRESHOLD = 20;
78153
+ var DEFAULT_CIRCUIT_BREAKER_ENABLED = true;
78036
78154
  var MIN_RUNTIME_BEFORE_STALE_MS = 30000;
78037
78155
  var MIN_IDLE_TIME_MS = 5000;
78038
78156
  var POLLING_INTERVAL_MS = 3000;
@@ -78453,6 +78571,22 @@ function removeTaskToastTracking(taskId) {
78453
78571
  }
78454
78572
  }
78455
78573
 
78574
+ // src/features/background-agent/session-status-classifier.ts
78575
+ var ACTIVE_SESSION_STATUSES = new Set(["busy", "retry", "running"]);
78576
+ var KNOWN_TERMINAL_STATUSES = new Set(["idle", "interrupted"]);
78577
+ function isActiveSessionStatus(type2) {
78578
+ if (ACTIVE_SESSION_STATUSES.has(type2)) {
78579
+ return true;
78580
+ }
78581
+ if (!KNOWN_TERMINAL_STATUSES.has(type2)) {
78582
+ log("[background-agent] Unknown session status type encountered:", type2);
78583
+ }
78584
+ return false;
78585
+ }
78586
+ function isTerminalSessionStatus(type2) {
78587
+ return KNOWN_TERMINAL_STATUSES.has(type2) && type2 !== "idle";
78588
+ }
78589
+
78456
78590
  // src/features/background-agent/task-poller.ts
78457
78591
  var TERMINAL_TASK_STATUSES = new Set([
78458
78592
  "completed",
@@ -78531,7 +78665,7 @@ async function checkAndInterruptStaleTasks(args) {
78531
78665
  if (!startedAt || !sessionID)
78532
78666
  continue;
78533
78667
  const sessionStatus = sessionStatuses?.[sessionID]?.type;
78534
- const sessionIsRunning = sessionStatus !== undefined && sessionStatus !== "idle";
78668
+ const sessionIsRunning = sessionStatus !== undefined && isActiveSessionStatus(sessionStatus);
78535
78669
  const runtime = now - startedAt.getTime();
78536
78670
  if (!task.progress?.lastUpdate) {
78537
78671
  if (sessionIsRunning)
@@ -78587,51 +78721,57 @@ async function checkAndInterruptStaleTasks(args) {
78587
78721
  // src/features/background-agent/loop-detector.ts
78588
78722
  function resolveCircuitBreakerSettings(config4) {
78589
78723
  return {
78724
+ enabled: config4?.circuitBreaker?.enabled ?? DEFAULT_CIRCUIT_BREAKER_ENABLED,
78590
78725
  maxToolCalls: config4?.circuitBreaker?.maxToolCalls ?? config4?.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS,
78591
- windowSize: config4?.circuitBreaker?.windowSize ?? DEFAULT_CIRCUIT_BREAKER_WINDOW_SIZE,
78592
- repetitionThresholdPercent: config4?.circuitBreaker?.repetitionThresholdPercent ?? DEFAULT_CIRCUIT_BREAKER_REPETITION_THRESHOLD_PERCENT
78726
+ consecutiveThreshold: config4?.circuitBreaker?.consecutiveThreshold ?? DEFAULT_CIRCUIT_BREAKER_CONSECUTIVE_THRESHOLD
78593
78727
  };
78594
78728
  }
78595
- function recordToolCall(window, toolName, settings) {
78596
- const previous = window?.toolNames ?? [];
78597
- const toolNames = [...previous, toolName].slice(-settings.windowSize);
78729
+ function recordToolCall(window, toolName, settings, toolInput) {
78730
+ const signature = createToolCallSignature(toolName, toolInput);
78731
+ if (window && window.lastSignature === signature) {
78732
+ return {
78733
+ lastSignature: signature,
78734
+ consecutiveCount: window.consecutiveCount + 1,
78735
+ threshold: settings.consecutiveThreshold
78736
+ };
78737
+ }
78598
78738
  return {
78599
- toolNames,
78600
- windowSize: settings.windowSize,
78601
- thresholdPercent: settings.repetitionThresholdPercent
78739
+ lastSignature: signature,
78740
+ consecutiveCount: 1,
78741
+ threshold: settings.consecutiveThreshold
78602
78742
  };
78603
78743
  }
78604
- function detectRepetitiveToolUse(window) {
78605
- if (!window || window.toolNames.length === 0) {
78606
- return { triggered: false };
78607
- }
78608
- const counts = new Map;
78609
- for (const toolName of window.toolNames) {
78610
- counts.set(toolName, (counts.get(toolName) ?? 0) + 1);
78744
+ function sortObject2(obj) {
78745
+ if (obj === null || obj === undefined)
78746
+ return obj;
78747
+ if (typeof obj !== "object")
78748
+ return obj;
78749
+ if (Array.isArray(obj))
78750
+ return obj.map(sortObject2);
78751
+ const sorted = {};
78752
+ const keys = Object.keys(obj).sort();
78753
+ for (const key of keys) {
78754
+ sorted[key] = sortObject2(obj[key]);
78611
78755
  }
78612
- let repeatedTool;
78613
- let repeatedCount = 0;
78614
- for (const [toolName, count] of counts.entries()) {
78615
- if (count > repeatedCount) {
78616
- repeatedTool = toolName;
78617
- repeatedCount = count;
78618
- }
78756
+ return sorted;
78757
+ }
78758
+ function createToolCallSignature(toolName, toolInput) {
78759
+ if (toolInput === undefined || toolInput === null) {
78760
+ return toolName;
78619
78761
  }
78620
- const sampleSize = window.toolNames.length;
78621
- const minimumSampleSize = Math.min(window.windowSize, Math.ceil(window.windowSize * window.thresholdPercent / 100));
78622
- if (sampleSize < minimumSampleSize) {
78623
- return { triggered: false };
78762
+ if (Object.keys(toolInput).length === 0) {
78763
+ return toolName;
78624
78764
  }
78625
- const thresholdCount = Math.ceil(sampleSize * window.thresholdPercent / 100);
78626
- if (!repeatedTool || repeatedCount < thresholdCount) {
78765
+ return `${toolName}::${JSON.stringify(sortObject2(toolInput))}`;
78766
+ }
78767
+ function detectRepetitiveToolUse(window) {
78768
+ if (!window || window.consecutiveCount < window.threshold) {
78627
78769
  return { triggered: false };
78628
78770
  }
78629
78771
  return {
78630
78772
  triggered: true,
78631
- toolName: repeatedTool,
78632
- repeatedCount,
78633
- sampleSize,
78634
- thresholdPercent: window.thresholdPercent
78773
+ toolName: window.lastSignature.split("::")[0],
78774
+ repeatedCount: window.consecutiveCount
78635
78775
  };
78636
78776
  }
78637
78777
 
@@ -78730,6 +78870,7 @@ class BackgroundManager {
78730
78870
  preStartDescendantReservations;
78731
78871
  enableParentSessionNotifications;
78732
78872
  taskHistory = new TaskHistory;
78873
+ cachedCircuitBreakerSettings;
78733
78874
  constructor(ctx, config4, options) {
78734
78875
  this.tasks = new Map;
78735
78876
  this.notifications = new Map;
@@ -79322,35 +79463,36 @@ class BackgroundManager {
79322
79463
  }
79323
79464
  task.progress.lastUpdate = new Date;
79324
79465
  if (partInfo?.type === "tool" || partInfo?.tool) {
79325
- const countedToolPartIDs = task.progress.countedToolPartIDs ?? [];
79326
- const shouldCountToolCall = !partInfo.id || partInfo.state?.status !== "running" || !countedToolPartIDs.includes(partInfo.id);
79466
+ const countedToolPartIDs = task.progress.countedToolPartIDs ?? new Set;
79467
+ const shouldCountToolCall = !partInfo.id || partInfo.state?.status !== "running" || !countedToolPartIDs.has(partInfo.id);
79327
79468
  if (!shouldCountToolCall) {
79328
79469
  return;
79329
79470
  }
79330
79471
  if (partInfo.id && partInfo.state?.status === "running") {
79331
- task.progress.countedToolPartIDs = [...countedToolPartIDs, partInfo.id];
79472
+ countedToolPartIDs.add(partInfo.id);
79473
+ task.progress.countedToolPartIDs = countedToolPartIDs;
79332
79474
  }
79333
79475
  task.progress.toolCalls += 1;
79334
79476
  task.progress.lastTool = partInfo.tool;
79335
- const circuitBreaker = resolveCircuitBreakerSettings(this.config);
79477
+ const circuitBreaker = this.cachedCircuitBreakerSettings ?? (this.cachedCircuitBreakerSettings = resolveCircuitBreakerSettings(this.config));
79336
79478
  if (partInfo.tool) {
79337
- task.progress.toolCallWindow = recordToolCall(task.progress.toolCallWindow, partInfo.tool, circuitBreaker);
79338
- const loopDetection = detectRepetitiveToolUse(task.progress.toolCallWindow);
79339
- if (loopDetection.triggered) {
79340
- log("[background-agent] Circuit breaker: repetitive tool usage detected", {
79341
- taskId: task.id,
79342
- agent: task.agent,
79343
- sessionID,
79344
- toolName: loopDetection.toolName,
79345
- repeatedCount: loopDetection.repeatedCount,
79346
- sampleSize: loopDetection.sampleSize,
79347
- thresholdPercent: loopDetection.thresholdPercent
79348
- });
79349
- this.cancelTask(task.id, {
79350
- source: "circuit-breaker",
79351
- reason: `Subagent repeatedly called ${loopDetection.toolName} ${loopDetection.repeatedCount}/${loopDetection.sampleSize} times in the recent tool-call window (${loopDetection.thresholdPercent}% threshold). This usually indicates an infinite loop. The task was automatically cancelled to prevent excessive token usage.`
79352
- });
79353
- return;
79479
+ task.progress.toolCallWindow = recordToolCall(task.progress.toolCallWindow, partInfo.tool, circuitBreaker, partInfo.state?.input);
79480
+ if (circuitBreaker.enabled) {
79481
+ const loopDetection = detectRepetitiveToolUse(task.progress.toolCallWindow);
79482
+ if (loopDetection.triggered) {
79483
+ log("[background-agent] Circuit breaker: consecutive tool usage detected", {
79484
+ taskId: task.id,
79485
+ agent: task.agent,
79486
+ sessionID,
79487
+ toolName: loopDetection.toolName,
79488
+ repeatedCount: loopDetection.repeatedCount
79489
+ });
79490
+ this.cancelTask(task.id, {
79491
+ source: "circuit-breaker",
79492
+ reason: `Subagent called ${loopDetection.toolName} ${loopDetection.repeatedCount} consecutive times (threshold: ${circuitBreaker.consecutiveThreshold}). This usually indicates an infinite loop. The task was automatically cancelled to prevent excessive token usage.`
79493
+ });
79494
+ return;
79495
+ }
79354
79496
  }
79355
79497
  }
79356
79498
  const maxToolCalls = circuitBreaker.maxToolCalls;
@@ -79997,7 +80139,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
79997
80139
  continue;
79998
80140
  }
79999
80141
  }
80000
- if (sessionStatus && sessionStatus.type !== "idle") {
80142
+ if (sessionStatus && isActiveSessionStatus(sessionStatus.type)) {
80001
80143
  log("[background-agent] Session still running, relying on event-based progress:", {
80002
80144
  taskId: task.id,
80003
80145
  sessionID,
@@ -80006,6 +80148,17 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
80006
80148
  });
80007
80149
  continue;
80008
80150
  }
80151
+ if (sessionStatus && isTerminalSessionStatus(sessionStatus.type)) {
80152
+ await this.tryCompleteTask(task, `polling (terminal session status: ${sessionStatus.type})`);
80153
+ continue;
80154
+ }
80155
+ if (sessionStatus && sessionStatus.type !== "idle") {
80156
+ log("[background-agent] Unknown session status, treating as potentially idle:", {
80157
+ taskId: task.id,
80158
+ sessionID,
80159
+ sessionStatus: sessionStatus.type
80160
+ });
80161
+ }
80009
80162
  const completionSource = sessionStatus?.type === "idle" ? "polling (idle status)" : "polling (session gone from status)";
80010
80163
  const hasValidOutput = await this.validateSessionHasOutput(sessionID);
80011
80164
  if (!hasValidOutput) {
@@ -97783,10 +97936,7 @@ function createPluginInterface(args) {
97783
97936
  const { ctx, pluginConfig, firstMessageVariantGate, managers, hooks: hooks2, tools } = args;
97784
97937
  return {
97785
97938
  tool: tools,
97786
- "chat.params": async (input, output) => {
97787
- const handler = createChatParamsHandler({ anthropicEffort: hooks2.anthropicEffort });
97788
- await handler(input, output);
97789
- },
97939
+ "chat.params": createChatParamsHandler({ anthropicEffort: hooks2.anthropicEffort }),
97790
97940
  "chat.headers": createChatHeadersHandler({ ctx }),
97791
97941
  "chat.message": createChatMessageHandler3({
97792
97942
  ctx,
@@ -97813,7 +97963,10 @@ function createPluginInterface(args) {
97813
97963
  "tool.execute.after": createToolExecuteAfterHandler3({
97814
97964
  ctx,
97815
97965
  hooks: hooks2
97816
- })
97966
+ }),
97967
+ "tool.definition": async (input, output) => {
97968
+ await hooks2.todoDescriptionOverride?.["tool.definition"]?.(input, output);
97969
+ }
97817
97970
  };
97818
97971
  }
97819
97972