evil-omo 3.12.0 → 3.12.2

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) });
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
+ }
17697
17712
  }
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;
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
+ }
17704
17736
  }
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;
17737
+ function hasProviderModelsCache() {
17738
+ const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE);
17739
+ return existsSync8(cacheFile);
17716
17740
  }
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) });
17736
- }
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();
@@ -20446,15 +20487,18 @@ async function handleSessionIdle(args) {
20446
20487
  shouldSkipContinuation
20447
20488
  } = args;
20448
20489
  log(`[${HOOK_NAME}] session.idle`, { sessionID });
20490
+ console.error(`[TODO-DIAG] session.idle fired for ${sessionID}`);
20449
20491
  const state2 = sessionStateStore.getState(sessionID);
20450
20492
  if (state2.isRecovering) {
20451
20493
  log(`[${HOOK_NAME}] Skipped: in recovery`, { sessionID });
20494
+ console.error(`[TODO-DIAG] BLOCKED: isRecovering=true`);
20452
20495
  return;
20453
20496
  }
20454
20497
  if (state2.abortDetectedAt) {
20455
20498
  const timeSinceAbort = Date.now() - state2.abortDetectedAt;
20456
20499
  if (timeSinceAbort < ABORT_WINDOW_MS) {
20457
20500
  log(`[${HOOK_NAME}] Skipped: abort detected via event ${timeSinceAbort}ms ago`, { sessionID });
20501
+ console.error(`[TODO-DIAG] BLOCKED: abort detected ${timeSinceAbort}ms ago`);
20458
20502
  state2.abortDetectedAt = undefined;
20459
20503
  return;
20460
20504
  }
@@ -20463,6 +20507,7 @@ async function handleSessionIdle(args) {
20463
20507
  const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((task) => task.status === "running") : false;
20464
20508
  if (hasRunningBgTasks) {
20465
20509
  log(`[${HOOK_NAME}] Skipped: background tasks running`, { sessionID });
20510
+ console.error(`[TODO-DIAG] BLOCKED: background tasks running`, backgroundManager?.getTasksByParentSession(sessionID).filter((t) => t.status === "running").map((t) => t.id));
20466
20511
  return;
20467
20512
  }
20468
20513
  try {
@@ -20473,10 +20518,12 @@ async function handleSessionIdle(args) {
20473
20518
  const messages = normalizeSDKResponse(messagesResp, []);
20474
20519
  if (isLastAssistantMessageAborted(messages)) {
20475
20520
  log(`[${HOOK_NAME}] Skipped: last assistant message was aborted (API fallback)`, { sessionID });
20521
+ console.error(`[TODO-DIAG] BLOCKED: last assistant message aborted`);
20476
20522
  return;
20477
20523
  }
20478
20524
  if (hasUnansweredQuestion(messages)) {
20479
20525
  log(`[${HOOK_NAME}] Skipped: pending question awaiting user response`, { sessionID });
20526
+ console.error(`[TODO-DIAG] BLOCKED: hasUnansweredQuestion=true`);
20480
20527
  return;
20481
20528
  }
20482
20529
  } catch (error) {
@@ -20488,21 +20535,27 @@ async function handleSessionIdle(args) {
20488
20535
  todos = normalizeSDKResponse(response, [], { preferResponseOnMissingData: true });
20489
20536
  } catch (error) {
20490
20537
  log(`[${HOOK_NAME}] Todo fetch failed`, { sessionID, error: String(error) });
20538
+ console.error(`[TODO-DIAG] BLOCKED: todo fetch failed`, String(error));
20491
20539
  return;
20492
20540
  }
20493
20541
  if (!todos || todos.length === 0) {
20542
+ sessionStateStore.resetContinuationProgress(sessionID);
20494
20543
  sessionStateStore.resetContinuationProgress(sessionID);
20495
20544
  log(`[${HOOK_NAME}] No todos`, { sessionID });
20545
+ console.error(`[TODO-DIAG] BLOCKED: no todos`);
20496
20546
  return;
20497
20547
  }
20498
20548
  const incompleteCount = getIncompleteCount(todos);
20499
20549
  if (incompleteCount === 0) {
20550
+ sessionStateStore.resetContinuationProgress(sessionID);
20500
20551
  sessionStateStore.resetContinuationProgress(sessionID);
20501
20552
  log(`[${HOOK_NAME}] All todos complete`, { sessionID, total: todos.length });
20553
+ console.error(`[TODO-DIAG] BLOCKED: all todos complete (${todos.length})`);
20502
20554
  return;
20503
20555
  }
20504
20556
  if (state2.inFlight) {
20505
20557
  log(`[${HOOK_NAME}] Skipped: injection in flight`, { sessionID });
20558
+ console.error(`[TODO-DIAG] BLOCKED: inFlight=true`);
20506
20559
  return;
20507
20560
  }
20508
20561
  if (state2.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES && state2.lastInjectedAt && Date.now() - state2.lastInjectedAt >= FAILURE_RESET_WINDOW_MS) {
@@ -20510,20 +20563,14 @@ async function handleSessionIdle(args) {
20510
20563
  log(`[${HOOK_NAME}] Reset consecutive failures after recovery window`, { sessionID, failureResetWindowMs: FAILURE_RESET_WINDOW_MS });
20511
20564
  }
20512
20565
  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
- });
20566
+ log(`[${HOOK_NAME}] Skipped: max consecutive failures reached`, { sessionID, consecutiveFailures: state2.consecutiveFailures });
20567
+ console.error(`[TODO-DIAG] BLOCKED: consecutiveFailures=${state2.consecutiveFailures} >= ${MAX_CONSECUTIVE_FAILURES}`);
20518
20568
  return;
20519
20569
  }
20520
20570
  const effectiveCooldown = CONTINUATION_COOLDOWN_MS * Math.pow(2, Math.min(state2.consecutiveFailures, 5));
20521
20571
  if (state2.lastInjectedAt && Date.now() - state2.lastInjectedAt < effectiveCooldown) {
20522
- log(`[${HOOK_NAME}] Skipped: cooldown active`, {
20523
- sessionID,
20524
- effectiveCooldown,
20525
- consecutiveFailures: state2.consecutiveFailures
20526
- });
20572
+ log(`[${HOOK_NAME}] Skipped: cooldown active`, { sessionID, effectiveCooldown, consecutiveFailures: state2.consecutiveFailures });
20573
+ console.error(`[TODO-DIAG] BLOCKED: cooldown active (${effectiveCooldown}ms, failures=${state2.consecutiveFailures})`);
20527
20574
  return;
20528
20575
  }
20529
20576
  let resolvedInfo;
@@ -20544,10 +20591,12 @@ async function handleSessionIdle(args) {
20544
20591
  const resolvedAgentName = resolvedInfo?.agent;
20545
20592
  if (resolvedAgentName && skipAgents.some((s) => getAgentConfigKey(s) === getAgentConfigKey(resolvedAgentName))) {
20546
20593
  log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: resolvedAgentName });
20594
+ console.error(`[TODO-DIAG] BLOCKED: agent '${resolvedAgentName}' in skipAgents`);
20547
20595
  return;
20548
20596
  }
20549
20597
  if ((compactionGuardActive || encounteredCompaction) && !resolvedInfo?.agent) {
20550
20598
  log(`[${HOOK_NAME}] Skipped: compaction occurred but no agent info resolved`, { sessionID });
20599
+ console.error(`[TODO-DIAG] BLOCKED: compaction guard + no agent`);
20551
20600
  return;
20552
20601
  }
20553
20602
  if (state2.recentCompactionAt && resolvedInfo?.agent) {
@@ -20555,16 +20604,20 @@ async function handleSessionIdle(args) {
20555
20604
  }
20556
20605
  if (isContinuationStopped?.(sessionID)) {
20557
20606
  log(`[${HOOK_NAME}] Skipped: continuation stopped for session`, { sessionID });
20607
+ console.error(`[TODO-DIAG] BLOCKED: isContinuationStopped=true`);
20558
20608
  return;
20559
20609
  }
20560
20610
  if (shouldSkipContinuation?.(sessionID)) {
20561
20611
  log(`[${HOOK_NAME}] Skipped: another continuation hook already injected`, { sessionID });
20612
+ console.error(`[TODO-DIAG] BLOCKED: shouldSkipContinuation=true (gptPermissionContinuation recently injected)`);
20562
20613
  return;
20563
20614
  }
20564
20615
  const progressUpdate = sessionStateStore.trackContinuationProgress(sessionID, incompleteCount, todos);
20565
20616
  if (shouldStopForStagnation({ sessionID, incompleteCount, progressUpdate })) {
20617
+ console.error(`[TODO-DIAG] BLOCKED: stagnation detected (count=${progressUpdate.stagnationCount})`);
20566
20618
  return;
20567
20619
  }
20620
+ console.error(`[TODO-DIAG] PASSED all gates! Starting countdown (${incompleteCount}/${todos.length} incomplete)`);
20568
20621
  startCountdown({
20569
20622
  ctx,
20570
20623
  sessionID,
@@ -20655,6 +20708,9 @@ function createTodoContinuationHandler(args) {
20655
20708
  } = args;
20656
20709
  return async ({ event }) => {
20657
20710
  const props = event.properties;
20711
+ if (event.type === "session.idle") {
20712
+ console.error(`[TODO-DIAG] handler received session.idle event`, { sessionID: props?.sessionID });
20713
+ }
20658
20714
  if (event.type === "session.error") {
20659
20715
  const sessionID = props?.sessionID;
20660
20716
  if (!sessionID)
@@ -36377,6 +36433,15 @@ function takePendingCall(callID) {
36377
36433
  import * as fs6 from "fs";
36378
36434
  import { tmpdir as tmpdir4 } from "os";
36379
36435
  import { join as join31 } from "path";
36436
+ var ApplyPatchMetadataSchema = zod_default.object({
36437
+ files: zod_default.array(zod_default.object({
36438
+ filePath: zod_default.string(),
36439
+ movePath: zod_default.string().optional(),
36440
+ before: zod_default.string(),
36441
+ after: zod_default.string(),
36442
+ type: zod_default.string().optional()
36443
+ }))
36444
+ });
36380
36445
  var DEBUG3 = process.env.COMMENT_CHECKER_DEBUG === "1";
36381
36446
  var DEBUG_FILE3 = join31(tmpdir4(), "comment-checker-debug.log");
36382
36447
  function debugLog3(...args) {
@@ -36437,15 +36502,6 @@ function createCommentCheckerHooks(config2) {
36437
36502
  debugLog3("skipping due to tool failure in output");
36438
36503
  return;
36439
36504
  }
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
36505
  if (toolLower === "apply_patch") {
36450
36506
  const parsed = ApplyPatchMetadataSchema.safeParse(output.metadata);
36451
36507
  if (!parsed.success) {
@@ -36916,7 +36972,7 @@ function isTokenLimitError(text) {
36916
36972
  return false;
36917
36973
  }
36918
36974
  const lower = text.toLowerCase();
36919
- return TOKEN_LIMIT_KEYWORDS.some((kw) => lower.includes(kw.toLowerCase()));
36975
+ return TOKEN_LIMIT_KEYWORDS.some((kw) => lower.includes(kw));
36920
36976
  }
36921
36977
  function parseAnthropicTokenLimitError(err) {
36922
36978
  try {
@@ -38334,7 +38390,6 @@ function createAnthropicContextWindowLimitRecoveryHook(ctx, options) {
38334
38390
  };
38335
38391
  }
38336
38392
  // src/hooks/think-mode/detector.ts
38337
- var ENGLISH_PATTERNS = [/\bultrathink\b/i, /\bthink\b/i];
38338
38393
  var MULTILINGUAL_KEYWORDS = [
38339
38394
  "\uC0DD\uAC01",
38340
38395
  "\uAC80\uD1A0",
@@ -38420,8 +38475,7 @@ var MULTILINGUAL_KEYWORDS = [
38420
38475
  "fikir",
38421
38476
  "berfikir"
38422
38477
  ];
38423
- var MULTILINGUAL_PATTERNS = MULTILINGUAL_KEYWORDS.map((kw) => new RegExp(kw, "i"));
38424
- var THINK_PATTERNS = [...ENGLISH_PATTERNS, ...MULTILINGUAL_PATTERNS];
38478
+ var COMBINED_THINK_PATTERN = new RegExp(`\\b(?:ultrathink|think)\\b|${MULTILINGUAL_KEYWORDS.join("|")}`, "i");
38425
38479
  var CODE_BLOCK_PATTERN = /```[\s\S]*?```/g;
38426
38480
  var INLINE_CODE_PATTERN = /`[^`]+`/g;
38427
38481
  function removeCodeBlocks(text) {
@@ -38429,7 +38483,7 @@ function removeCodeBlocks(text) {
38429
38483
  }
38430
38484
  function detectThinkKeyword(text) {
38431
38485
  const textWithoutCode = removeCodeBlocks(text);
38432
- return THINK_PATTERNS.some((pattern) => pattern.test(textWithoutCode));
38486
+ return COMBINED_THINK_PATTERN.test(textWithoutCode);
38433
38487
  }
38434
38488
  function extractPromptText(parts) {
38435
38489
  return parts.filter((p) => p.type === "text").map((p) => p.text || "").join("");
@@ -39086,16 +39140,16 @@ async function loadPluginExtendedConfig() {
39086
39140
  }
39087
39141
  return merged;
39088
39142
  }
39089
- var regexCache = new Map;
39143
+ var regexCache2 = new Map;
39090
39144
  function getRegex(pattern) {
39091
- let regex = regexCache.get(pattern);
39145
+ let regex = regexCache2.get(pattern);
39092
39146
  if (!regex) {
39093
39147
  try {
39094
39148
  regex = new RegExp(pattern);
39095
- regexCache.set(pattern, regex);
39149
+ regexCache2.set(pattern, regex);
39096
39150
  } catch {
39097
39151
  regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
39098
- regexCache.set(pattern, regex);
39152
+ regexCache2.set(pattern, regex);
39099
39153
  }
39100
39154
  }
39101
39155
  return regex;
@@ -39961,8 +40015,6 @@ function createToolExecuteAfterHandler(ctx, config2) {
39961
40015
  if (!output) {
39962
40016
  return;
39963
40017
  }
39964
- const claudeConfig = await loadClaudeHooksConfig();
39965
- const extendedConfig = await loadPluginExtendedConfig();
39966
40018
  const cachedInput = getToolInput(input.sessionID, input.tool, input.callID) || {};
39967
40019
  appendTranscriptEntry(input.sessionID, {
39968
40020
  type: "tool_result",
@@ -39974,6 +40026,8 @@ function createToolExecuteAfterHandler(ctx, config2) {
39974
40026
  if (isHookDisabled(config2, "PostToolUse")) {
39975
40027
  return;
39976
40028
  }
40029
+ const claudeConfig = await loadClaudeHooksConfig();
40030
+ const extendedConfig = await loadPluginExtendedConfig();
39977
40031
  const postClient = {
39978
40032
  session: {
39979
40033
  messages: (opts) => ctx.client.session.messages(opts)
@@ -40154,8 +40208,6 @@ function createToolExecuteBeforeHandler(ctx, config2) {
40154
40208
  output.args.todos = parsed;
40155
40209
  log("todowrite: parsed todos string to array", { sessionID: input.sessionID });
40156
40210
  }
40157
- const claudeConfig = await loadClaudeHooksConfig();
40158
- const extendedConfig = await loadPluginExtendedConfig();
40159
40211
  appendTranscriptEntry(input.sessionID, {
40160
40212
  type: "tool_use",
40161
40213
  timestamp: new Date().toISOString(),
@@ -40166,6 +40218,8 @@ function createToolExecuteBeforeHandler(ctx, config2) {
40166
40218
  if (isHookDisabled(config2, "PreToolUse")) {
40167
40219
  return;
40168
40220
  }
40221
+ const claudeConfig = await loadClaudeHooksConfig();
40222
+ const extendedConfig = await loadPluginExtendedConfig();
40169
40223
  const preCtx = {
40170
40224
  sessionId: input.sessionID,
40171
40225
  toolName: input.tool,
@@ -43759,6 +43813,9 @@ async function injectContinuationPrompt(ctx, options) {
43759
43813
  async function handleDetectedCompletion(ctx, input) {
43760
43814
  const { sessionID, state: state3, loopState, directory, apiTimeoutMs } = input;
43761
43815
  if (state3.ultrawork && !state3.verification_pending) {
43816
+ if (state3.verification_session_id) {
43817
+ ctx.client.session.abort({ path: { id: state3.verification_session_id } }).catch(() => {});
43818
+ }
43762
43819
  const verificationState = loopState.markVerificationPending(sessionID);
43763
43820
  if (!verificationState) {
43764
43821
  log(`[${HOOK_NAME3}] Failed to transition ultrawork loop to verification`, {
@@ -43995,6 +44052,9 @@ async function handleFailedVerification(ctx, input) {
43995
44052
  });
43996
44053
  return false;
43997
44054
  }
44055
+ if (state3.verification_session_id) {
44056
+ ctx.client.session.abort({ path: { id: state3.verification_session_id } }).catch(() => {});
44057
+ }
43998
44058
  const resumedState = loopState.restartAfterFailedVerification(parentSessionID, messageCountAtStart);
43999
44059
  if (!resumedState) {
44000
44060
  log(`[${HOOK_NAME3}] Failed to restart loop after verification failure`, {
@@ -47600,9 +47660,9 @@ var BabysittingConfigSchema = exports_external.object({
47600
47660
  });
47601
47661
  // src/config/schema/background-task.ts
47602
47662
  var CircuitBreakerConfigSchema = exports_external.object({
47663
+ enabled: exports_external.boolean().optional(),
47603
47664
  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()
47665
+ consecutiveThreshold: exports_external.number().int().min(5).optional()
47606
47666
  });
47607
47667
  var BackgroundTaskConfigSchema = exports_external.object({
47608
47668
  defaultConcurrency: exports_external.number().min(1).optional(),
@@ -47798,7 +47858,8 @@ var HookNameSchema = exports_external.enum([
47798
47858
  "write-existing-file-guard",
47799
47859
  "anthropic-effort",
47800
47860
  "hashline-read-enhancer",
47801
- "read-image-resizer"
47861
+ "read-image-resizer",
47862
+ "todo-description-override"
47802
47863
  ]);
47803
47864
  // src/config/schema/notification.ts
47804
47865
  var NotificationConfigSchema = exports_external.object({
@@ -49201,7 +49262,7 @@ var START_WORK_TEMPLATE = `You are starting a Sisyphus work session.
49201
49262
  - \`--worktree <path>\` (optional): absolute path to an existing git worktree to work in
49202
49263
  - If specified and valid: hook pre-sets worktree_path in boulder.json
49203
49264
  - 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)
49265
+ - If omitted: work directly in the current project directory (no worktree)
49205
49266
 
49206
49267
  ## WHAT TO DO
49207
49268
 
@@ -49218,7 +49279,7 @@ var START_WORK_TEMPLATE = `You are starting a Sisyphus work session.
49218
49279
  - If ONE plan: auto-select it
49219
49280
  - If MULTIPLE plans: show list with timestamps, ask user to select
49220
49281
 
49221
- 4. **Worktree Setup** (when \`worktree_path\` not already set in boulder.json):
49282
+ 4. **Worktree Setup** (ONLY when \`--worktree\` was explicitly specified and \`worktree_path\` not already set in boulder.json):
49222
49283
  1. \`git worktree list --porcelain\` \u2014 see available worktrees
49223
49284
  2. Create: \`git worktree add <absolute-path> <branch-or-HEAD>\`
49224
49285
  3. Update boulder.json to add \`"worktree_path": "<absolute-path>"\`
@@ -49280,9 +49341,41 @@ Reading plan and beginning execution...
49280
49341
 
49281
49342
  - The session_id is injected by the hook - use it directly
49282
49343
  - Always update boulder.json BEFORE starting work
49283
- - Always set worktree_path in boulder.json before executing any tasks
49344
+ - If worktree_path is set in boulder.json, all work happens inside that worktree directory
49284
49345
  - Read the FULL plan file before delegating any tasks
49285
- - Follow atlas delegation protocols (7-section format)`;
49346
+ - Follow atlas delegation protocols (7-section format)
49347
+
49348
+ ## TASK BREAKDOWN (MANDATORY)
49349
+
49350
+ 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.
49351
+
49352
+ **How to break down**:
49353
+ - Each plan checkbox item (e.g., \`- [ ] Add user authentication\`) must be split into concrete, actionable sub-tasks
49354
+ - Sub-tasks should be specific enough that each one touches a clear set of files/functions
49355
+ - Include: file to modify, what to change, expected behavior, and how to verify
49356
+ - 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
49357
+
49358
+ **Example breakdown**:
49359
+ Plan task: \`- [ ] Add rate limiting to API\`
49360
+ \u2192 Todo items:
49361
+ 1. Create \`src/middleware/rate-limiter.ts\` with sliding window algorithm (max 100 req/min per IP)
49362
+ 2. Add RateLimiter middleware to \`src/app.ts\` router chain, before auth middleware
49363
+ 3. Add rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining) to response in \`rate-limiter.ts\`
49364
+ 4. Add test: verify 429 response after exceeding limit in \`src/middleware/rate-limiter.test.ts\`
49365
+ 5. Add test: verify headers are present on normal responses
49366
+
49367
+ Register these as task/todo items so progress is tracked and visible throughout the session.
49368
+
49369
+ ## WORKTREE COMPLETION
49370
+
49371
+ When working in a worktree (\`worktree_path\` is set in boulder.json) and ALL plan tasks are complete:
49372
+ 1. Commit all remaining changes in the worktree
49373
+ 2. Switch to the main working directory (the original repo, NOT the worktree)
49374
+ 3. Merge the worktree branch into the current branch: \`git merge <worktree-branch>\`
49375
+ 4. If merge succeeds, clean up: \`git worktree remove <worktree-path>\`
49376
+ 5. Remove the boulder.json state
49377
+
49378
+ 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
49379
 
49287
49380
  // src/features/builtin-commands/templates/handoff.ts
49288
49381
  var HANDOFF_TEMPLATE = `# Handoff Command
@@ -49664,9 +49757,6 @@ function skillToCommandInfo(skill) {
49664
49757
  lazyContentLoader: skill.lazyContent
49665
49758
  };
49666
49759
  }
49667
- function filterDiscoveredCommandsByScope(commands3, scope) {
49668
- return commands3.filter((command) => command.scope === scope);
49669
- }
49670
49760
  async function discoverAllCommands(options) {
49671
49761
  const discoveredCommands = discoverCommandsSync(process.cwd(), {
49672
49762
  pluginsEnabled: options?.pluginsEnabled,
@@ -49674,14 +49764,17 @@ async function discoverAllCommands(options) {
49674
49764
  });
49675
49765
  const skills2 = options?.skills ?? await discoverAllSkills();
49676
49766
  const skillCommands = skills2.map(skillToCommandInfo);
49767
+ const scopeOrder = ["project", "user", "opencode-project", "opencode", "builtin", "plugin"];
49768
+ const grouped = new Map;
49769
+ for (const cmd of discoveredCommands) {
49770
+ const list = grouped.get(cmd.scope) ?? [];
49771
+ list.push(cmd);
49772
+ grouped.set(cmd.scope, list);
49773
+ }
49774
+ const orderedCommands = scopeOrder.flatMap((scope) => grouped.get(scope) ?? []);
49677
49775
  return [
49678
49776
  ...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")
49777
+ ...orderedCommands
49685
49778
  ];
49686
49779
  }
49687
49780
  async function findCommand2(commandName, options) {
@@ -53440,6 +53533,7 @@ function getErrorMessage2(error48) {
53440
53533
  return "";
53441
53534
  }
53442
53535
  }
53536
+ var DEFAULT_RETRY_PATTERN = new RegExp(`\\b(${DEFAULT_CONFIG2.retry_on_errors.join("|")})\\b`);
53443
53537
  function extractStatusCode(error48, retryOnErrors) {
53444
53538
  if (!error48)
53445
53539
  return;
@@ -53454,8 +53548,7 @@ function extractStatusCode(error48, retryOnErrors) {
53454
53548
  if (statusCode !== undefined) {
53455
53549
  return statusCode;
53456
53550
  }
53457
- const codes = retryOnErrors ?? DEFAULT_CONFIG2.retry_on_errors;
53458
- const pattern = new RegExp(`\\b(${codes.join("|")})\\b`);
53551
+ const pattern = retryOnErrors ? new RegExp(`\\b(${retryOnErrors.join("|")})\\b`) : DEFAULT_RETRY_PATTERN;
53459
53552
  const message = getErrorMessage2(error48);
53460
53553
  const statusMatch = message.match(pattern);
53461
53554
  if (statusMatch) {
@@ -55146,6 +55239,46 @@ function createReadImageResizerHook(_ctx) {
55146
55239
  }
55147
55240
  };
55148
55241
  }
55242
+ // src/hooks/todo-description-override/description.ts
55243
+ var TODOWRITE_DESCRIPTION = `Use this tool to create and manage a structured task list for tracking progress on multi-step work.
55244
+
55245
+ ## Todo Format (MANDATORY)
55246
+
55247
+ Each todo title MUST encode four elements: WHERE, WHY, HOW, and EXPECTED RESULT.
55248
+
55249
+ Format: "[WHERE] [HOW] to [WHY] \u2014 expect [RESULT]"
55250
+
55251
+ GOOD:
55252
+ - "src/utils/validation.ts: Add validateEmail() for input sanitization \u2014 returns boolean"
55253
+ - "UserService.create(): Call validateEmail() before DB insert \u2014 rejects invalid emails with 400"
55254
+ - "validation.test.ts: Add test for missing @ sign \u2014 expect validateEmail('foo') to return false"
55255
+
55256
+ BAD:
55257
+ - "Implement email validation" (where? how? what result?)
55258
+ - "Add dark mode" (this is a feature, not a todo)
55259
+ - "Fix auth" (what file? what changes? what's expected?)
55260
+
55261
+ ## Granularity Rules
55262
+
55263
+ Each todo MUST be a single atomic action completable in 1-3 tool calls. If it needs more, split it.
55264
+
55265
+ **Size test**: Can you complete this todo by editing one file or running one command? If not, it's too big.
55266
+
55267
+ ## Task Management
55268
+ - One in_progress at a time. Complete it before starting the next.
55269
+ - Mark completed immediately after finishing each item.
55270
+ - Skip this tool for single trivial tasks (one-step, obvious action).`;
55271
+
55272
+ // src/hooks/todo-description-override/hook.ts
55273
+ function createTodoDescriptionOverrideHook() {
55274
+ return {
55275
+ "tool.definition": async (input, output) => {
55276
+ if (input.toolID === "todowrite") {
55277
+ output.description = TODOWRITE_DESCRIPTION;
55278
+ }
55279
+ }
55280
+ };
55281
+ }
55149
55282
  // src/hooks/anthropic-effort/hook.ts
55150
55283
  var OPUS_4_6_PATTERN = /claude-opus-4[-.]6/i;
55151
55284
  function isClaudeProvider(providerID, modelID) {
@@ -75723,10 +75856,11 @@ Returns summary format: id, subject, status, owner, blockedBy (not full descript
75723
75856
  allTasks.push(task);
75724
75857
  }
75725
75858
  }
75859
+ const taskMap = new Map(allTasks.map((t) => [t.id, t]));
75726
75860
  const activeTasks = allTasks.filter((task) => task.status !== "completed" && task.status !== "deleted");
75727
75861
  const summaries = activeTasks.map((task) => {
75728
75862
  const unresolvedBlockers = task.blockedBy.filter((blockerId) => {
75729
- const blockerTask = allTasks.find((t) => t.id === blockerId);
75863
+ const blockerTask = taskMap.get(blockerId);
75730
75864
  return !blockerTask || blockerTask.status !== "completed";
75731
75865
  });
75732
75866
  return {
@@ -76415,6 +76549,15 @@ function applyPrepend(lines, text) {
76415
76549
  }
76416
76550
 
76417
76551
  // src/tools/hashline-edit/edit-operations.ts
76552
+ function arraysEqual(a, b) {
76553
+ if (a.length !== b.length)
76554
+ return false;
76555
+ for (let i2 = 0;i2 < a.length; i2++) {
76556
+ if (a[i2] !== b[i2])
76557
+ return false;
76558
+ }
76559
+ return true;
76560
+ }
76418
76561
  function applyHashlineEditsWithReport(content, edits) {
76419
76562
  if (edits.length === 0) {
76420
76563
  return {
@@ -76444,9 +76587,7 @@ function applyHashlineEditsWithReport(content, edits) {
76444
76587
  switch (edit.op) {
76445
76588
  case "replace": {
76446
76589
  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
- `)) {
76590
+ if (arraysEqual(next, lines)) {
76450
76591
  noopEdits += 1;
76451
76592
  break;
76452
76593
  }
@@ -76455,9 +76596,7 @@ function applyHashlineEditsWithReport(content, edits) {
76455
76596
  }
76456
76597
  case "append": {
76457
76598
  const next = edit.pos ? applyInsertAfter(lines, edit.pos, edit.lines, { skipValidation: true }) : applyAppend(lines, edit.lines);
76458
- if (next.join(`
76459
- `) === lines.join(`
76460
- `)) {
76599
+ if (arraysEqual(next, lines)) {
76461
76600
  noopEdits += 1;
76462
76601
  break;
76463
76602
  }
@@ -76466,9 +76605,7 @@ function applyHashlineEditsWithReport(content, edits) {
76466
76605
  }
76467
76606
  case "prepend": {
76468
76607
  const next = edit.pos ? applyInsertBefore(lines, edit.pos, edit.lines, { skipValidation: true }) : applyPrepend(lines, edit.lines);
76469
- if (next.join(`
76470
- `) === lines.join(`
76471
- `)) {
76608
+ if (arraysEqual(next, lines)) {
76472
76609
  noopEdits += 1;
76473
76610
  break;
76474
76611
  }
@@ -77493,6 +77630,7 @@ function createToolGuardHooks(args) {
77493
77630
  const hashlineReadEnhancer = isHookEnabled("hashline-read-enhancer") ? safeHook("hashline-read-enhancer", () => createHashlineReadEnhancerHook(ctx, { hashline_edit: { enabled: pluginConfig.hashline_edit ?? false } })) : null;
77494
77631
  const jsonErrorRecovery = isHookEnabled("json-error-recovery") ? safeHook("json-error-recovery", () => createJsonErrorRecoveryHook(ctx)) : null;
77495
77632
  const readImageResizer = isHookEnabled("read-image-resizer") ? safeHook("read-image-resizer", () => createReadImageResizerHook(ctx)) : null;
77633
+ const todoDescriptionOverride = isHookEnabled("todo-description-override") ? safeHook("todo-description-override", () => createTodoDescriptionOverrideHook()) : null;
77496
77634
  return {
77497
77635
  commentChecker,
77498
77636
  toolOutputTruncator,
@@ -77504,7 +77642,8 @@ function createToolGuardHooks(args) {
77504
77642
  writeExistingFileGuard,
77505
77643
  hashlineReadEnhancer,
77506
77644
  jsonErrorRecovery,
77507
- readImageResizer
77645
+ readImageResizer,
77646
+ todoDescriptionOverride
77508
77647
  };
77509
77648
  }
77510
77649
 
@@ -78031,8 +78170,8 @@ var MIN_STABILITY_TIME_MS2 = 10 * 1000;
78031
78170
  var DEFAULT_STALE_TIMEOUT_MS = 1200000;
78032
78171
  var DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS = 1800000;
78033
78172
  var DEFAULT_MAX_TOOL_CALLS = 200;
78034
- var DEFAULT_CIRCUIT_BREAKER_WINDOW_SIZE = 20;
78035
- var DEFAULT_CIRCUIT_BREAKER_REPETITION_THRESHOLD_PERCENT = 80;
78173
+ var DEFAULT_CIRCUIT_BREAKER_CONSECUTIVE_THRESHOLD = 20;
78174
+ var DEFAULT_CIRCUIT_BREAKER_ENABLED = true;
78036
78175
  var MIN_RUNTIME_BEFORE_STALE_MS = 30000;
78037
78176
  var MIN_IDLE_TIME_MS = 5000;
78038
78177
  var POLLING_INTERVAL_MS = 3000;
@@ -78453,6 +78592,22 @@ function removeTaskToastTracking(taskId) {
78453
78592
  }
78454
78593
  }
78455
78594
 
78595
+ // src/features/background-agent/session-status-classifier.ts
78596
+ var ACTIVE_SESSION_STATUSES = new Set(["busy", "retry", "running"]);
78597
+ var KNOWN_TERMINAL_STATUSES = new Set(["idle", "interrupted"]);
78598
+ function isActiveSessionStatus(type2) {
78599
+ if (ACTIVE_SESSION_STATUSES.has(type2)) {
78600
+ return true;
78601
+ }
78602
+ if (!KNOWN_TERMINAL_STATUSES.has(type2)) {
78603
+ log("[background-agent] Unknown session status type encountered:", type2);
78604
+ }
78605
+ return false;
78606
+ }
78607
+ function isTerminalSessionStatus(type2) {
78608
+ return KNOWN_TERMINAL_STATUSES.has(type2) && type2 !== "idle";
78609
+ }
78610
+
78456
78611
  // src/features/background-agent/task-poller.ts
78457
78612
  var TERMINAL_TASK_STATUSES = new Set([
78458
78613
  "completed",
@@ -78531,7 +78686,7 @@ async function checkAndInterruptStaleTasks(args) {
78531
78686
  if (!startedAt || !sessionID)
78532
78687
  continue;
78533
78688
  const sessionStatus = sessionStatuses?.[sessionID]?.type;
78534
- const sessionIsRunning = sessionStatus !== undefined && sessionStatus !== "idle";
78689
+ const sessionIsRunning = sessionStatus !== undefined && isActiveSessionStatus(sessionStatus);
78535
78690
  const runtime = now - startedAt.getTime();
78536
78691
  if (!task.progress?.lastUpdate) {
78537
78692
  if (sessionIsRunning)
@@ -78587,51 +78742,57 @@ async function checkAndInterruptStaleTasks(args) {
78587
78742
  // src/features/background-agent/loop-detector.ts
78588
78743
  function resolveCircuitBreakerSettings(config4) {
78589
78744
  return {
78745
+ enabled: config4?.circuitBreaker?.enabled ?? DEFAULT_CIRCUIT_BREAKER_ENABLED,
78590
78746
  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
78747
+ consecutiveThreshold: config4?.circuitBreaker?.consecutiveThreshold ?? DEFAULT_CIRCUIT_BREAKER_CONSECUTIVE_THRESHOLD
78593
78748
  };
78594
78749
  }
78595
- function recordToolCall(window, toolName, settings) {
78596
- const previous = window?.toolNames ?? [];
78597
- const toolNames = [...previous, toolName].slice(-settings.windowSize);
78750
+ function recordToolCall(window, toolName, settings, toolInput) {
78751
+ const signature = createToolCallSignature(toolName, toolInput);
78752
+ if (window && window.lastSignature === signature) {
78753
+ return {
78754
+ lastSignature: signature,
78755
+ consecutiveCount: window.consecutiveCount + 1,
78756
+ threshold: settings.consecutiveThreshold
78757
+ };
78758
+ }
78598
78759
  return {
78599
- toolNames,
78600
- windowSize: settings.windowSize,
78601
- thresholdPercent: settings.repetitionThresholdPercent
78760
+ lastSignature: signature,
78761
+ consecutiveCount: 1,
78762
+ threshold: settings.consecutiveThreshold
78602
78763
  };
78603
78764
  }
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);
78765
+ function sortObject2(obj) {
78766
+ if (obj === null || obj === undefined)
78767
+ return obj;
78768
+ if (typeof obj !== "object")
78769
+ return obj;
78770
+ if (Array.isArray(obj))
78771
+ return obj.map(sortObject2);
78772
+ const sorted = {};
78773
+ const keys = Object.keys(obj).sort();
78774
+ for (const key of keys) {
78775
+ sorted[key] = sortObject2(obj[key]);
78611
78776
  }
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
- }
78777
+ return sorted;
78778
+ }
78779
+ function createToolCallSignature(toolName, toolInput) {
78780
+ if (toolInput === undefined || toolInput === null) {
78781
+ return toolName;
78619
78782
  }
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 };
78783
+ if (Object.keys(toolInput).length === 0) {
78784
+ return toolName;
78624
78785
  }
78625
- const thresholdCount = Math.ceil(sampleSize * window.thresholdPercent / 100);
78626
- if (!repeatedTool || repeatedCount < thresholdCount) {
78786
+ return `${toolName}::${JSON.stringify(sortObject2(toolInput))}`;
78787
+ }
78788
+ function detectRepetitiveToolUse(window) {
78789
+ if (!window || window.consecutiveCount < window.threshold) {
78627
78790
  return { triggered: false };
78628
78791
  }
78629
78792
  return {
78630
78793
  triggered: true,
78631
- toolName: repeatedTool,
78632
- repeatedCount,
78633
- sampleSize,
78634
- thresholdPercent: window.thresholdPercent
78794
+ toolName: window.lastSignature.split("::")[0],
78795
+ repeatedCount: window.consecutiveCount
78635
78796
  };
78636
78797
  }
78637
78798
 
@@ -78730,6 +78891,7 @@ class BackgroundManager {
78730
78891
  preStartDescendantReservations;
78731
78892
  enableParentSessionNotifications;
78732
78893
  taskHistory = new TaskHistory;
78894
+ cachedCircuitBreakerSettings;
78733
78895
  constructor(ctx, config4, options) {
78734
78896
  this.tasks = new Map;
78735
78897
  this.notifications = new Map;
@@ -79322,35 +79484,36 @@ class BackgroundManager {
79322
79484
  }
79323
79485
  task.progress.lastUpdate = new Date;
79324
79486
  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);
79487
+ const countedToolPartIDs = task.progress.countedToolPartIDs ?? new Set;
79488
+ const shouldCountToolCall = !partInfo.id || partInfo.state?.status !== "running" || !countedToolPartIDs.has(partInfo.id);
79327
79489
  if (!shouldCountToolCall) {
79328
79490
  return;
79329
79491
  }
79330
79492
  if (partInfo.id && partInfo.state?.status === "running") {
79331
- task.progress.countedToolPartIDs = [...countedToolPartIDs, partInfo.id];
79493
+ countedToolPartIDs.add(partInfo.id);
79494
+ task.progress.countedToolPartIDs = countedToolPartIDs;
79332
79495
  }
79333
79496
  task.progress.toolCalls += 1;
79334
79497
  task.progress.lastTool = partInfo.tool;
79335
- const circuitBreaker = resolveCircuitBreakerSettings(this.config);
79498
+ const circuitBreaker = this.cachedCircuitBreakerSettings ?? (this.cachedCircuitBreakerSettings = resolveCircuitBreakerSettings(this.config));
79336
79499
  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;
79500
+ task.progress.toolCallWindow = recordToolCall(task.progress.toolCallWindow, partInfo.tool, circuitBreaker, partInfo.state?.input);
79501
+ if (circuitBreaker.enabled) {
79502
+ const loopDetection = detectRepetitiveToolUse(task.progress.toolCallWindow);
79503
+ if (loopDetection.triggered) {
79504
+ log("[background-agent] Circuit breaker: consecutive tool usage detected", {
79505
+ taskId: task.id,
79506
+ agent: task.agent,
79507
+ sessionID,
79508
+ toolName: loopDetection.toolName,
79509
+ repeatedCount: loopDetection.repeatedCount
79510
+ });
79511
+ this.cancelTask(task.id, {
79512
+ source: "circuit-breaker",
79513
+ 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.`
79514
+ });
79515
+ return;
79516
+ }
79354
79517
  }
79355
79518
  }
79356
79519
  const maxToolCalls = circuitBreaker.maxToolCalls;
@@ -79997,7 +80160,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
79997
80160
  continue;
79998
80161
  }
79999
80162
  }
80000
- if (sessionStatus && sessionStatus.type !== "idle") {
80163
+ if (sessionStatus && isActiveSessionStatus(sessionStatus.type)) {
80001
80164
  log("[background-agent] Session still running, relying on event-based progress:", {
80002
80165
  taskId: task.id,
80003
80166
  sessionID,
@@ -80006,6 +80169,17 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
80006
80169
  });
80007
80170
  continue;
80008
80171
  }
80172
+ if (sessionStatus && isTerminalSessionStatus(sessionStatus.type)) {
80173
+ await this.tryCompleteTask(task, `polling (terminal session status: ${sessionStatus.type})`);
80174
+ continue;
80175
+ }
80176
+ if (sessionStatus && sessionStatus.type !== "idle") {
80177
+ log("[background-agent] Unknown session status, treating as potentially idle:", {
80178
+ taskId: task.id,
80179
+ sessionID,
80180
+ sessionStatus: sessionStatus.type
80181
+ });
80182
+ }
80009
80183
  const completionSource = sessionStatus?.type === "idle" ? "polling (idle status)" : "polling (session gone from status)";
80010
80184
  const hasValidOutput = await this.validateSessionHasOutput(sessionID);
80011
80185
  if (!hasValidOutput) {
@@ -97783,10 +97957,7 @@ function createPluginInterface(args) {
97783
97957
  const { ctx, pluginConfig, firstMessageVariantGate, managers, hooks: hooks2, tools } = args;
97784
97958
  return {
97785
97959
  tool: tools,
97786
- "chat.params": async (input, output) => {
97787
- const handler = createChatParamsHandler({ anthropicEffort: hooks2.anthropicEffort });
97788
- await handler(input, output);
97789
- },
97960
+ "chat.params": createChatParamsHandler({ anthropicEffort: hooks2.anthropicEffort }),
97790
97961
  "chat.headers": createChatHeadersHandler({ ctx }),
97791
97962
  "chat.message": createChatMessageHandler3({
97792
97963
  ctx,
@@ -97813,7 +97984,10 @@ function createPluginInterface(args) {
97813
97984
  "tool.execute.after": createToolExecuteAfterHandler3({
97814
97985
  ctx,
97815
97986
  hooks: hooks2
97816
- })
97987
+ }),
97988
+ "tool.definition": async (input, output) => {
97989
+ await hooks2.todoDescriptionOverride?.["tool.definition"]?.(input, output);
97990
+ }
97817
97991
  };
97818
97992
  }
97819
97993