oh-my-opencode 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
@@ -55,20 +55,43 @@ __export(exports_logger, {
55
55
  import * as fs from "fs";
56
56
  import * as os from "os";
57
57
  import * as path from "path";
58
+ function flush() {
59
+ if (buffer.length === 0)
60
+ return;
61
+ const data = buffer.join("");
62
+ buffer = [];
63
+ try {
64
+ fs.appendFileSync(logFile, data);
65
+ } catch {}
66
+ }
67
+ function scheduleFlush() {
68
+ if (flushTimer)
69
+ return;
70
+ flushTimer = setTimeout(() => {
71
+ flushTimer = null;
72
+ flush();
73
+ }, FLUSH_INTERVAL_MS);
74
+ }
58
75
  function log(message, data) {
59
76
  try {
60
77
  const timestamp2 = new Date().toISOString();
61
78
  const logEntry = `[${timestamp2}] ${message} ${data ? JSON.stringify(data) : ""}
62
79
  `;
63
- fs.appendFileSync(logFile, logEntry);
80
+ buffer.push(logEntry);
81
+ if (buffer.length >= BUFFER_SIZE_LIMIT) {
82
+ flush();
83
+ } else {
84
+ scheduleFlush();
85
+ }
64
86
  } catch {}
65
87
  }
66
88
  function getLogFilePath() {
67
89
  return logFile;
68
90
  }
69
- var logFile;
91
+ var logFile, buffer, flushTimer = null, FLUSH_INTERVAL_MS = 500, BUFFER_SIZE_LIMIT = 50;
70
92
  var init_logger = __esm(() => {
71
93
  logFile = path.join(os.tmpdir(), "oh-my-opencode.log");
94
+ buffer = [];
72
95
  });
73
96
 
74
97
  // src/shared/truncate-description.ts
@@ -2454,18 +2477,18 @@ var require_sharedArrayCancellation = __commonJS((exports) => {
2454
2477
  if (request.id === null) {
2455
2478
  return;
2456
2479
  }
2457
- const buffer = new SharedArrayBuffer(4);
2458
- const data = new Int32Array(buffer, 0, 1);
2480
+ const buffer2 = new SharedArrayBuffer(4);
2481
+ const data = new Int32Array(buffer2, 0, 1);
2459
2482
  data[0] = CancellationState.Continue;
2460
- this.buffers.set(request.id, buffer);
2461
- request.$cancellationData = buffer;
2483
+ this.buffers.set(request.id, buffer2);
2484
+ request.$cancellationData = buffer2;
2462
2485
  }
2463
2486
  async sendCancellation(_conn, id) {
2464
- const buffer = this.buffers.get(id);
2465
- if (buffer === undefined) {
2487
+ const buffer2 = this.buffers.get(id);
2488
+ if (buffer2 === undefined) {
2466
2489
  return;
2467
2490
  }
2468
- const data = new Int32Array(buffer, 0, 1);
2491
+ const data = new Int32Array(buffer2, 0, 1);
2469
2492
  Atomics.store(data, 0, CancellationState.Cancelled);
2470
2493
  }
2471
2494
  cleanup(id) {
@@ -2478,8 +2501,8 @@ var require_sharedArrayCancellation = __commonJS((exports) => {
2478
2501
  exports.SharedArraySenderStrategy = SharedArraySenderStrategy;
2479
2502
 
2480
2503
  class SharedArrayBufferCancellationToken {
2481
- constructor(buffer) {
2482
- this.data = new Int32Array(buffer, 0, 1);
2504
+ constructor(buffer2) {
2505
+ this.data = new Int32Array(buffer2, 0, 1);
2483
2506
  }
2484
2507
  get isCancellationRequested() {
2485
2508
  return Atomics.load(this.data, 0) === CancellationState.Cancelled;
@@ -2490,8 +2513,8 @@ var require_sharedArrayCancellation = __commonJS((exports) => {
2490
2513
  }
2491
2514
 
2492
2515
  class SharedArrayBufferCancellationTokenSource {
2493
- constructor(buffer) {
2494
- this.token = new SharedArrayBufferCancellationToken(buffer);
2516
+ constructor(buffer2) {
2517
+ this.token = new SharedArrayBufferCancellationToken(buffer2);
2495
2518
  }
2496
2519
  cancel() {}
2497
2520
  dispose() {}
@@ -2502,11 +2525,11 @@ var require_sharedArrayCancellation = __commonJS((exports) => {
2502
2525
  this.kind = "request";
2503
2526
  }
2504
2527
  createCancellationTokenSource(request) {
2505
- const buffer = request.$cancellationData;
2506
- if (buffer === undefined) {
2528
+ const buffer2 = request.$cancellationData;
2529
+ if (buffer2 === undefined) {
2507
2530
  return new cancellation_1.CancellationTokenSource;
2508
2531
  }
2509
- return new SharedArrayBufferCancellationTokenSource(buffer);
2532
+ return new SharedArrayBufferCancellationTokenSource(buffer2);
2510
2533
  }
2511
2534
  }
2512
2535
  exports.SharedArrayReceiverStrategy = SharedArrayReceiverStrategy;
@@ -2840,18 +2863,18 @@ var require_messageWriter = __commonJS((exports) => {
2840
2863
  }
2841
2864
  async write(msg) {
2842
2865
  return this.writeSemaphore.lock(async () => {
2843
- const payload = this.options.contentTypeEncoder.encode(msg, this.options).then((buffer) => {
2866
+ const payload = this.options.contentTypeEncoder.encode(msg, this.options).then((buffer2) => {
2844
2867
  if (this.options.contentEncoder !== undefined) {
2845
- return this.options.contentEncoder.encode(buffer);
2868
+ return this.options.contentEncoder.encode(buffer2);
2846
2869
  } else {
2847
- return buffer;
2870
+ return buffer2;
2848
2871
  }
2849
2872
  });
2850
- return payload.then((buffer) => {
2873
+ return payload.then((buffer2) => {
2851
2874
  const headers = [];
2852
- headers.push(ContentLength, buffer.byteLength.toString(), CRLF);
2875
+ headers.push(ContentLength, buffer2.byteLength.toString(), CRLF);
2853
2876
  headers.push(CRLF);
2854
- return this.doWrite(msg, headers, buffer);
2877
+ return this.doWrite(msg, headers, buffer2);
2855
2878
  }, (error48) => {
2856
2879
  this.fireError(error48);
2857
2880
  throw error48;
@@ -2953,9 +2976,9 @@ var require_messageBuffer = __commonJS((exports) => {
2953
2976
  if (state3 !== 4) {
2954
2977
  return;
2955
2978
  }
2956
- const buffer = this._read(chunkBytesRead + offset);
2979
+ const buffer2 = this._read(chunkBytesRead + offset);
2957
2980
  const result = new Map;
2958
- const headers = this.toString(buffer, "ascii").split(CRLF);
2981
+ const headers = this.toString(buffer2, "ascii").split(CRLF);
2959
2982
  if (headers.length < 2) {
2960
2983
  return result;
2961
2984
  }
@@ -4379,11 +4402,11 @@ var require_ril = __commonJS((exports) => {
4379
4402
  return new util_1.TextDecoder(encoding).decode(value);
4380
4403
  }
4381
4404
  }
4382
- asNative(buffer, length) {
4405
+ asNative(buffer2, length) {
4383
4406
  if (length === undefined) {
4384
- return buffer instanceof Buffer ? buffer : Buffer.from(buffer);
4407
+ return buffer2 instanceof Buffer ? buffer2 : Buffer.from(buffer2);
4385
4408
  } else {
4386
- return buffer instanceof Buffer ? buffer.slice(0, length) : Buffer.from(buffer, 0, length);
4409
+ return buffer2 instanceof Buffer ? buffer2.slice(0, length) : Buffer.from(buffer2, 0, length);
4387
4410
  }
4388
4411
  }
4389
4412
  allocNative(length) {
@@ -4467,12 +4490,12 @@ var require_ril = __commonJS((exports) => {
4467
4490
  }),
4468
4491
  decoder: Object.freeze({
4469
4492
  name: "application/json",
4470
- decode: (buffer, options) => {
4493
+ decode: (buffer2, options) => {
4471
4494
  try {
4472
- if (buffer instanceof Buffer) {
4473
- return Promise.resolve(JSON.parse(buffer.toString(options.charset)));
4495
+ if (buffer2 instanceof Buffer) {
4496
+ return Promise.resolve(JSON.parse(buffer2.toString(options.charset)));
4474
4497
  } else {
4475
- return Promise.resolve(JSON.parse(new util_1.TextDecoder(options.charset).decode(buffer)));
4498
+ return Promise.resolve(JSON.parse(new util_1.TextDecoder(options.charset).decode(buffer2)));
4476
4499
  }
4477
4500
  } catch (err) {
4478
4501
  return Promise.reject(err);
@@ -8405,20 +8428,20 @@ var require_utils2 = __commonJS((exports, module) => {
8405
8428
  return acc;
8406
8429
  }
8407
8430
  var nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u);
8408
- function consumeIsZone(buffer) {
8409
- buffer.length = 0;
8431
+ function consumeIsZone(buffer2) {
8432
+ buffer2.length = 0;
8410
8433
  return true;
8411
8434
  }
8412
- function consumeHextets(buffer, address, output) {
8413
- if (buffer.length) {
8414
- const hex5 = stringArrayToHexStripped(buffer);
8435
+ function consumeHextets(buffer2, address, output) {
8436
+ if (buffer2.length) {
8437
+ const hex5 = stringArrayToHexStripped(buffer2);
8415
8438
  if (hex5 !== "") {
8416
8439
  address.push(hex5);
8417
8440
  } else {
8418
8441
  output.error = true;
8419
8442
  return false;
8420
8443
  }
8421
- buffer.length = 0;
8444
+ buffer2.length = 0;
8422
8445
  }
8423
8446
  return true;
8424
8447
  }
@@ -8426,7 +8449,7 @@ var require_utils2 = __commonJS((exports, module) => {
8426
8449
  let tokenCount = 0;
8427
8450
  const output = { error: false, address: "", zone: "" };
8428
8451
  const address = [];
8429
- const buffer = [];
8452
+ const buffer2 = [];
8430
8453
  let endipv6Encountered = false;
8431
8454
  let endIpv6 = false;
8432
8455
  let consume = consumeHextets;
@@ -8439,7 +8462,7 @@ var require_utils2 = __commonJS((exports, module) => {
8439
8462
  if (endipv6Encountered === true) {
8440
8463
  endIpv6 = true;
8441
8464
  }
8442
- if (!consume(buffer, address, output)) {
8465
+ if (!consume(buffer2, address, output)) {
8443
8466
  break;
8444
8467
  }
8445
8468
  if (++tokenCount > 7) {
@@ -8452,22 +8475,22 @@ var require_utils2 = __commonJS((exports, module) => {
8452
8475
  address.push(":");
8453
8476
  continue;
8454
8477
  } else if (cursor === "%") {
8455
- if (!consume(buffer, address, output)) {
8478
+ if (!consume(buffer2, address, output)) {
8456
8479
  break;
8457
8480
  }
8458
8481
  consume = consumeIsZone;
8459
8482
  } else {
8460
- buffer.push(cursor);
8483
+ buffer2.push(cursor);
8461
8484
  continue;
8462
8485
  }
8463
8486
  }
8464
- if (buffer.length) {
8487
+ if (buffer2.length) {
8465
8488
  if (consume === consumeIsZone) {
8466
- output.zone = buffer.join("");
8489
+ output.zone = buffer2.join("");
8467
8490
  } else if (endIpv6) {
8468
- address.push(buffer.join(""));
8491
+ address.push(buffer2.join(""));
8469
8492
  } else {
8470
- address.push(stringArrayToHexStripped(buffer));
8493
+ address.push(stringArrayToHexStripped(buffer2));
8471
8494
  }
8472
8495
  }
8473
8496
  output.address = address.join("");
@@ -12160,14 +12183,14 @@ var require_readShebang = __commonJS((exports, module) => {
12160
12183
  var shebangCommand = require_shebang_command();
12161
12184
  function readShebang(command) {
12162
12185
  const size = 150;
12163
- const buffer = Buffer.alloc(size);
12186
+ const buffer2 = Buffer.alloc(size);
12164
12187
  let fd;
12165
12188
  try {
12166
12189
  fd = fs19.openSync(command, "r");
12167
- fs19.readSync(fd, buffer, 0, size, 0);
12190
+ fs19.readSync(fd, buffer2, 0, size, 0);
12168
12191
  fs19.closeSync(fd);
12169
12192
  } catch (e) {}
12170
- return shebangCommand(buffer.toString());
12193
+ return shebangCommand(buffer2.toString());
12171
12194
  }
12172
12195
  module.exports = readShebang;
12173
12196
  });
@@ -15246,7 +15269,7 @@ async function resolveFileReferencesInText(text, cwd = process.cwd(), depth = 0,
15246
15269
  }
15247
15270
  let resolved = text;
15248
15271
  for (const [pattern, replacement] of replacements.entries()) {
15249
- resolved = resolved.split(pattern).join(replacement);
15272
+ resolved = resolved.replaceAll(pattern, replacement);
15250
15273
  }
15251
15274
  if (findFileReferences(resolved).length > 0 && depth + 1 < maxDepth) {
15252
15275
  return resolveFileReferencesInText(resolved, cwd, depth + 1, maxDepth);
@@ -15345,6 +15368,7 @@ function transformToolName(toolName) {
15345
15368
  function escapeRegexExceptAsterisk(str2) {
15346
15369
  return str2.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
15347
15370
  }
15371
+ var regexCache = new Map;
15348
15372
  function matchesToolMatcher(toolName, matcher) {
15349
15373
  if (!matcher) {
15350
15374
  return true;
@@ -15352,8 +15376,12 @@ function matchesToolMatcher(toolName, matcher) {
15352
15376
  const patterns = matcher.split("|").map((p) => p.trim());
15353
15377
  return patterns.some((p) => {
15354
15378
  if (p.includes("*")) {
15355
- const escaped = escapeRegexExceptAsterisk(p);
15356
- const regex = new RegExp(`^${escaped.replace(/\*/g, ".*")}$`, "i");
15379
+ let regex = regexCache.get(p);
15380
+ if (!regex) {
15381
+ const escaped = escapeRegexExceptAsterisk(p);
15382
+ regex = new RegExp(`^${escaped.replace(/\*/g, ".*")}$`, "i");
15383
+ regexCache.set(p, regex);
15384
+ }
15357
15385
  return regex.test(toolName);
15358
15386
  }
15359
15387
  return p.toLowerCase() === toolName.toLowerCase();
@@ -17584,120 +17612,156 @@ import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync
17584
17612
  import { join as join9 } from "path";
17585
17613
  var CONNECTED_PROVIDERS_CACHE_FILE = "connected-providers.json";
17586
17614
  var PROVIDER_MODELS_CACHE_FILE = "provider-models.json";
17587
- function getCacheFilePath(filename) {
17588
- return join9(getOmoOpenCodeCacheDir(), filename);
17589
- }
17590
- function ensureCacheDir2() {
17591
- const cacheDir = getOmoOpenCodeCacheDir();
17592
- if (!existsSync8(cacheDir)) {
17593
- mkdirSync2(cacheDir, { recursive: true });
17594
- }
17595
- }
17596
- function readConnectedProvidersCache() {
17597
- const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE);
17598
- if (!existsSync8(cacheFile)) {
17599
- log("[connected-providers-cache] Cache file not found", { cacheFile });
17600
- return null;
17601
- }
17602
- try {
17603
- const content = readFileSync4(cacheFile, "utf-8");
17604
- const data = JSON.parse(content);
17605
- log("[connected-providers-cache] Read cache", { count: data.connected.length, updatedAt: data.updatedAt });
17606
- return data.connected;
17607
- } catch (err) {
17608
- log("[connected-providers-cache] Error reading cache", { error: String(err) });
17609
- return null;
17615
+ function createConnectedProvidersCacheStore(getCacheDir2 = getOmoOpenCodeCacheDir) {
17616
+ function getCacheFilePath(filename) {
17617
+ return join9(getCacheDir2(), filename);
17618
+ }
17619
+ let memConnected;
17620
+ let memProviderModels;
17621
+ function ensureCacheDir2() {
17622
+ const cacheDir = getCacheDir2();
17623
+ if (!existsSync8(cacheDir)) {
17624
+ mkdirSync2(cacheDir, { recursive: true });
17625
+ }
17626
+ }
17627
+ function readConnectedProvidersCache() {
17628
+ if (memConnected !== undefined)
17629
+ return memConnected;
17630
+ const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE);
17631
+ if (!existsSync8(cacheFile)) {
17632
+ log("[connected-providers-cache] Cache file not found", { cacheFile });
17633
+ memConnected = null;
17634
+ return null;
17635
+ }
17636
+ try {
17637
+ const content = readFileSync4(cacheFile, "utf-8");
17638
+ const data = JSON.parse(content);
17639
+ log("[connected-providers-cache] Read cache", { count: data.connected.length, updatedAt: data.updatedAt });
17640
+ memConnected = data.connected;
17641
+ return data.connected;
17642
+ } catch (err) {
17643
+ log("[connected-providers-cache] Error reading cache", { error: String(err) });
17644
+ memConnected = null;
17645
+ return null;
17646
+ }
17610
17647
  }
17611
- }
17612
- function hasConnectedProvidersCache() {
17613
- const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE);
17614
- return existsSync8(cacheFile);
17615
- }
17616
- function writeConnectedProvidersCache(connected) {
17617
- ensureCacheDir2();
17618
- const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE);
17619
- const data = {
17620
- connected,
17621
- updatedAt: new Date().toISOString()
17622
- };
17623
- try {
17624
- writeFileSync2(cacheFile, JSON.stringify(data, null, 2));
17625
- log("[connected-providers-cache] Cache written", { count: connected.length });
17626
- } catch (err) {
17627
- log("[connected-providers-cache] Error writing cache", { error: String(err) });
17648
+ function hasConnectedProvidersCache() {
17649
+ const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE);
17650
+ return existsSync8(cacheFile);
17628
17651
  }
17629
- }
17630
- function readProviderModelsCache() {
17631
- const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE);
17632
- if (!existsSync8(cacheFile)) {
17633
- log("[connected-providers-cache] Provider-models cache file not found", { cacheFile });
17634
- return null;
17652
+ function writeConnectedProvidersCache(connected) {
17653
+ ensureCacheDir2();
17654
+ const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE);
17655
+ const data = {
17656
+ connected,
17657
+ updatedAt: new Date().toISOString()
17658
+ };
17659
+ try {
17660
+ writeFileSync2(cacheFile, JSON.stringify(data, null, 2));
17661
+ memConnected = connected;
17662
+ log("[connected-providers-cache] Cache written", { count: connected.length });
17663
+ } catch (err) {
17664
+ log("[connected-providers-cache] Error writing cache", { error: String(err) });
17665
+ }
17635
17666
  }
17636
- try {
17637
- const content = readFileSync4(cacheFile, "utf-8");
17638
- const data = JSON.parse(content);
17639
- log("[connected-providers-cache] Read provider-models cache", {
17640
- providerCount: Object.keys(data.models).length,
17641
- updatedAt: data.updatedAt
17642
- });
17643
- return data;
17644
- } catch (err) {
17645
- log("[connected-providers-cache] Error reading provider-models cache", { error: String(err) });
17646
- return null;
17667
+ function readProviderModelsCache() {
17668
+ if (memProviderModels !== undefined)
17669
+ return memProviderModels;
17670
+ const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE);
17671
+ if (!existsSync8(cacheFile)) {
17672
+ log("[connected-providers-cache] Provider-models cache file not found", { cacheFile });
17673
+ memProviderModels = null;
17674
+ return null;
17675
+ }
17676
+ try {
17677
+ const content = readFileSync4(cacheFile, "utf-8");
17678
+ const data = JSON.parse(content);
17679
+ log("[connected-providers-cache] Read provider-models cache", {
17680
+ providerCount: Object.keys(data.models).length,
17681
+ updatedAt: data.updatedAt
17682
+ });
17683
+ memProviderModels = data;
17684
+ return data;
17685
+ } catch (err) {
17686
+ log("[connected-providers-cache] Error reading provider-models cache", { error: String(err) });
17687
+ memProviderModels = null;
17688
+ return null;
17689
+ }
17647
17690
  }
17648
- }
17649
- function hasProviderModelsCache() {
17650
- const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE);
17651
- return existsSync8(cacheFile);
17652
- }
17653
- function writeProviderModelsCache(data) {
17654
- ensureCacheDir2();
17655
- const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE);
17656
- const cacheData = {
17657
- ...data,
17658
- updatedAt: new Date().toISOString()
17659
- };
17660
- try {
17661
- writeFileSync2(cacheFile, JSON.stringify(cacheData, null, 2));
17662
- log("[connected-providers-cache] Provider-models cache written", {
17663
- providerCount: Object.keys(data.models).length
17664
- });
17665
- } catch (err) {
17666
- log("[connected-providers-cache] Error writing provider-models cache", { error: String(err) });
17691
+ function hasProviderModelsCache() {
17692
+ const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE);
17693
+ return existsSync8(cacheFile);
17667
17694
  }
17668
- }
17669
- async function updateConnectedProvidersCache(client) {
17670
- if (!client?.provider?.list) {
17671
- log("[connected-providers-cache] client.provider.list not available");
17672
- return;
17695
+ function writeProviderModelsCache(data) {
17696
+ ensureCacheDir2();
17697
+ const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE);
17698
+ const cacheData = {
17699
+ ...data,
17700
+ updatedAt: new Date().toISOString()
17701
+ };
17702
+ try {
17703
+ writeFileSync2(cacheFile, JSON.stringify(cacheData, null, 2));
17704
+ memProviderModels = cacheData;
17705
+ log("[connected-providers-cache] Provider-models cache written", {
17706
+ providerCount: Object.keys(data.models).length
17707
+ });
17708
+ } catch (err) {
17709
+ log("[connected-providers-cache] Error writing provider-models cache", { error: String(err) });
17710
+ }
17673
17711
  }
17674
- try {
17675
- const result = await client.provider.list();
17676
- const connected = result.data?.connected ?? [];
17677
- log("[connected-providers-cache] Fetched connected providers", { count: connected.length, providers: connected });
17678
- writeConnectedProvidersCache(connected);
17679
- const modelsByProvider = {};
17680
- const allProviders = result.data?.all ?? [];
17681
- for (const provider of allProviders) {
17682
- if (provider.models) {
17683
- const modelIds = Object.keys(provider.models);
17684
- if (modelIds.length > 0) {
17685
- modelsByProvider[provider.id] = modelIds;
17712
+ async function updateConnectedProvidersCache(client) {
17713
+ if (!client?.provider?.list) {
17714
+ log("[connected-providers-cache] client.provider.list not available");
17715
+ return;
17716
+ }
17717
+ try {
17718
+ const result = await client.provider.list();
17719
+ const connected = result.data?.connected ?? [];
17720
+ log("[connected-providers-cache] Fetched connected providers", {
17721
+ count: connected.length,
17722
+ providers: connected
17723
+ });
17724
+ writeConnectedProvidersCache(connected);
17725
+ const modelsByProvider = {};
17726
+ const allProviders = result.data?.all ?? [];
17727
+ for (const provider of allProviders) {
17728
+ if (provider.models) {
17729
+ const modelIds = Object.keys(provider.models);
17730
+ if (modelIds.length > 0) {
17731
+ modelsByProvider[provider.id] = modelIds;
17732
+ }
17686
17733
  }
17687
17734
  }
17735
+ log("[connected-providers-cache] Extracted models from provider list", {
17736
+ providerCount: Object.keys(modelsByProvider).length,
17737
+ totalModels: Object.values(modelsByProvider).reduce((sum, ids) => sum + ids.length, 0)
17738
+ });
17739
+ writeProviderModelsCache({
17740
+ models: modelsByProvider,
17741
+ connected
17742
+ });
17743
+ } catch (err) {
17744
+ log("[connected-providers-cache] Error updating cache", { error: String(err) });
17688
17745
  }
17689
- log("[connected-providers-cache] Extracted models from provider list", {
17690
- providerCount: Object.keys(modelsByProvider).length,
17691
- totalModels: Object.values(modelsByProvider).reduce((sum, ids) => sum + ids.length, 0)
17692
- });
17693
- writeProviderModelsCache({
17694
- models: modelsByProvider,
17695
- connected
17696
- });
17697
- } catch (err) {
17698
- log("[connected-providers-cache] Error updating cache", { error: String(err) });
17699
17746
  }
17747
+ return {
17748
+ readConnectedProvidersCache,
17749
+ hasConnectedProvidersCache,
17750
+ readProviderModelsCache,
17751
+ hasProviderModelsCache,
17752
+ writeProviderModelsCache,
17753
+ updateConnectedProvidersCache
17754
+ };
17700
17755
  }
17756
+ var defaultConnectedProvidersCacheStore = createConnectedProvidersCacheStore(() => getOmoOpenCodeCacheDir());
17757
+ var {
17758
+ readConnectedProvidersCache,
17759
+ hasConnectedProvidersCache,
17760
+ readProviderModelsCache,
17761
+ hasProviderModelsCache,
17762
+ writeProviderModelsCache,
17763
+ updateConnectedProvidersCache
17764
+ } = defaultConnectedProvidersCacheStore;
17701
17765
 
17702
17766
  // src/shared/model-availability.ts
17703
17767
  init_logger();
@@ -20372,15 +20436,18 @@ async function handleSessionIdle(args) {
20372
20436
  shouldSkipContinuation
20373
20437
  } = args;
20374
20438
  log(`[${HOOK_NAME}] session.idle`, { sessionID });
20439
+ console.error(`[TODO-DIAG] session.idle fired for ${sessionID}`);
20375
20440
  const state2 = sessionStateStore.getState(sessionID);
20376
20441
  if (state2.isRecovering) {
20377
20442
  log(`[${HOOK_NAME}] Skipped: in recovery`, { sessionID });
20443
+ console.error(`[TODO-DIAG] BLOCKED: isRecovering=true`);
20378
20444
  return;
20379
20445
  }
20380
20446
  if (state2.abortDetectedAt) {
20381
20447
  const timeSinceAbort = Date.now() - state2.abortDetectedAt;
20382
20448
  if (timeSinceAbort < ABORT_WINDOW_MS) {
20383
20449
  log(`[${HOOK_NAME}] Skipped: abort detected via event ${timeSinceAbort}ms ago`, { sessionID });
20450
+ console.error(`[TODO-DIAG] BLOCKED: abort detected ${timeSinceAbort}ms ago`);
20384
20451
  state2.abortDetectedAt = undefined;
20385
20452
  return;
20386
20453
  }
@@ -20389,6 +20456,7 @@ async function handleSessionIdle(args) {
20389
20456
  const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((task) => task.status === "running") : false;
20390
20457
  if (hasRunningBgTasks) {
20391
20458
  log(`[${HOOK_NAME}] Skipped: background tasks running`, { sessionID });
20459
+ console.error(`[TODO-DIAG] BLOCKED: background tasks running`, backgroundManager?.getTasksByParentSession(sessionID).filter((t) => t.status === "running").map((t) => t.id));
20392
20460
  return;
20393
20461
  }
20394
20462
  try {
@@ -20399,10 +20467,12 @@ async function handleSessionIdle(args) {
20399
20467
  const messages = normalizeSDKResponse(messagesResp, []);
20400
20468
  if (isLastAssistantMessageAborted(messages)) {
20401
20469
  log(`[${HOOK_NAME}] Skipped: last assistant message was aborted (API fallback)`, { sessionID });
20470
+ console.error(`[TODO-DIAG] BLOCKED: last assistant message aborted`);
20402
20471
  return;
20403
20472
  }
20404
20473
  if (hasUnansweredQuestion(messages)) {
20405
20474
  log(`[${HOOK_NAME}] Skipped: pending question awaiting user response`, { sessionID });
20475
+ console.error(`[TODO-DIAG] BLOCKED: hasUnansweredQuestion=true`);
20406
20476
  return;
20407
20477
  }
20408
20478
  } catch (error) {
@@ -20414,21 +20484,27 @@ async function handleSessionIdle(args) {
20414
20484
  todos = normalizeSDKResponse(response, [], { preferResponseOnMissingData: true });
20415
20485
  } catch (error) {
20416
20486
  log(`[${HOOK_NAME}] Todo fetch failed`, { sessionID, error: String(error) });
20487
+ console.error(`[TODO-DIAG] BLOCKED: todo fetch failed`, String(error));
20417
20488
  return;
20418
20489
  }
20419
20490
  if (!todos || todos.length === 0) {
20491
+ sessionStateStore.resetContinuationProgress(sessionID);
20420
20492
  sessionStateStore.resetContinuationProgress(sessionID);
20421
20493
  log(`[${HOOK_NAME}] No todos`, { sessionID });
20494
+ console.error(`[TODO-DIAG] BLOCKED: no todos`);
20422
20495
  return;
20423
20496
  }
20424
20497
  const incompleteCount = getIncompleteCount(todos);
20425
20498
  if (incompleteCount === 0) {
20499
+ sessionStateStore.resetContinuationProgress(sessionID);
20426
20500
  sessionStateStore.resetContinuationProgress(sessionID);
20427
20501
  log(`[${HOOK_NAME}] All todos complete`, { sessionID, total: todos.length });
20502
+ console.error(`[TODO-DIAG] BLOCKED: all todos complete (${todos.length})`);
20428
20503
  return;
20429
20504
  }
20430
20505
  if (state2.inFlight) {
20431
20506
  log(`[${HOOK_NAME}] Skipped: injection in flight`, { sessionID });
20507
+ console.error(`[TODO-DIAG] BLOCKED: inFlight=true`);
20432
20508
  return;
20433
20509
  }
20434
20510
  if (state2.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES && state2.lastInjectedAt && Date.now() - state2.lastInjectedAt >= FAILURE_RESET_WINDOW_MS) {
@@ -20436,20 +20512,14 @@ async function handleSessionIdle(args) {
20436
20512
  log(`[${HOOK_NAME}] Reset consecutive failures after recovery window`, { sessionID, failureResetWindowMs: FAILURE_RESET_WINDOW_MS });
20437
20513
  }
20438
20514
  if (state2.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
20439
- log(`[${HOOK_NAME}] Skipped: max consecutive failures reached`, {
20440
- sessionID,
20441
- consecutiveFailures: state2.consecutiveFailures,
20442
- maxConsecutiveFailures: MAX_CONSECUTIVE_FAILURES
20443
- });
20515
+ log(`[${HOOK_NAME}] Skipped: max consecutive failures reached`, { sessionID, consecutiveFailures: state2.consecutiveFailures });
20516
+ console.error(`[TODO-DIAG] BLOCKED: consecutiveFailures=${state2.consecutiveFailures} >= ${MAX_CONSECUTIVE_FAILURES}`);
20444
20517
  return;
20445
20518
  }
20446
20519
  const effectiveCooldown = CONTINUATION_COOLDOWN_MS * Math.pow(2, Math.min(state2.consecutiveFailures, 5));
20447
20520
  if (state2.lastInjectedAt && Date.now() - state2.lastInjectedAt < effectiveCooldown) {
20448
- log(`[${HOOK_NAME}] Skipped: cooldown active`, {
20449
- sessionID,
20450
- effectiveCooldown,
20451
- consecutiveFailures: state2.consecutiveFailures
20452
- });
20521
+ log(`[${HOOK_NAME}] Skipped: cooldown active`, { sessionID, effectiveCooldown, consecutiveFailures: state2.consecutiveFailures });
20522
+ console.error(`[TODO-DIAG] BLOCKED: cooldown active (${effectiveCooldown}ms, failures=${state2.consecutiveFailures})`);
20453
20523
  return;
20454
20524
  }
20455
20525
  let resolvedInfo;
@@ -20470,10 +20540,12 @@ async function handleSessionIdle(args) {
20470
20540
  const resolvedAgentName = resolvedInfo?.agent;
20471
20541
  if (resolvedAgentName && skipAgents.some((s) => getAgentConfigKey(s) === getAgentConfigKey(resolvedAgentName))) {
20472
20542
  log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: resolvedAgentName });
20543
+ console.error(`[TODO-DIAG] BLOCKED: agent '${resolvedAgentName}' in skipAgents`);
20473
20544
  return;
20474
20545
  }
20475
20546
  if ((compactionGuardActive || encounteredCompaction) && !resolvedInfo?.agent) {
20476
20547
  log(`[${HOOK_NAME}] Skipped: compaction occurred but no agent info resolved`, { sessionID });
20548
+ console.error(`[TODO-DIAG] BLOCKED: compaction guard + no agent`);
20477
20549
  return;
20478
20550
  }
20479
20551
  if (state2.recentCompactionAt && resolvedInfo?.agent) {
@@ -20481,16 +20553,20 @@ async function handleSessionIdle(args) {
20481
20553
  }
20482
20554
  if (isContinuationStopped?.(sessionID)) {
20483
20555
  log(`[${HOOK_NAME}] Skipped: continuation stopped for session`, { sessionID });
20556
+ console.error(`[TODO-DIAG] BLOCKED: isContinuationStopped=true`);
20484
20557
  return;
20485
20558
  }
20486
20559
  if (shouldSkipContinuation?.(sessionID)) {
20487
20560
  log(`[${HOOK_NAME}] Skipped: another continuation hook already injected`, { sessionID });
20561
+ console.error(`[TODO-DIAG] BLOCKED: shouldSkipContinuation=true (gptPermissionContinuation recently injected)`);
20488
20562
  return;
20489
20563
  }
20490
20564
  const progressUpdate = sessionStateStore.trackContinuationProgress(sessionID, incompleteCount, todos);
20491
20565
  if (shouldStopForStagnation({ sessionID, incompleteCount, progressUpdate })) {
20566
+ console.error(`[TODO-DIAG] BLOCKED: stagnation detected (count=${progressUpdate.stagnationCount})`);
20492
20567
  return;
20493
20568
  }
20569
+ console.error(`[TODO-DIAG] PASSED all gates! Starting countdown (${incompleteCount}/${todos.length} incomplete)`);
20494
20570
  startCountdown({
20495
20571
  ctx,
20496
20572
  sessionID,
@@ -20581,6 +20657,9 @@ function createTodoContinuationHandler(args) {
20581
20657
  } = args;
20582
20658
  return async ({ event }) => {
20583
20659
  const props = event.properties;
20660
+ if (event.type === "session.idle") {
20661
+ console.error(`[TODO-DIAG] handler received session.idle event`, { sessionID: props?.sessionID });
20662
+ }
20584
20663
  if (event.type === "session.error") {
20585
20664
  const sessionID = props?.sessionID;
20586
20665
  if (!sessionID)
@@ -36302,6 +36381,15 @@ function takePendingCall(callID) {
36302
36381
  import * as fs6 from "fs";
36303
36382
  import { tmpdir as tmpdir4 } from "os";
36304
36383
  import { join as join30 } from "path";
36384
+ var ApplyPatchMetadataSchema = zod_default.object({
36385
+ files: zod_default.array(zod_default.object({
36386
+ filePath: zod_default.string(),
36387
+ movePath: zod_default.string().optional(),
36388
+ before: zod_default.string(),
36389
+ after: zod_default.string(),
36390
+ type: zod_default.string().optional()
36391
+ }))
36392
+ });
36305
36393
  var DEBUG3 = process.env.COMMENT_CHECKER_DEBUG === "1";
36306
36394
  var DEBUG_FILE3 = join30(tmpdir4(), "comment-checker-debug.log");
36307
36395
  function debugLog3(...args) {
@@ -36362,15 +36450,6 @@ function createCommentCheckerHooks(config2) {
36362
36450
  debugLog3("skipping due to tool failure in output");
36363
36451
  return;
36364
36452
  }
36365
- const ApplyPatchMetadataSchema = zod_default.object({
36366
- files: zod_default.array(zod_default.object({
36367
- filePath: zod_default.string(),
36368
- movePath: zod_default.string().optional(),
36369
- before: zod_default.string(),
36370
- after: zod_default.string(),
36371
- type: zod_default.string().optional()
36372
- }))
36373
- });
36374
36453
  if (toolLower === "apply_patch") {
36375
36454
  const parsed = ApplyPatchMetadataSchema.safeParse(output.metadata);
36376
36455
  if (!parsed.success) {
@@ -36841,7 +36920,7 @@ function isTokenLimitError(text) {
36841
36920
  return false;
36842
36921
  }
36843
36922
  const lower = text.toLowerCase();
36844
- return TOKEN_LIMIT_KEYWORDS.some((kw) => lower.includes(kw.toLowerCase()));
36923
+ return TOKEN_LIMIT_KEYWORDS.some((kw) => lower.includes(kw));
36845
36924
  }
36846
36925
  function parseAnthropicTokenLimitError(err) {
36847
36926
  try {
@@ -38259,7 +38338,6 @@ function createAnthropicContextWindowLimitRecoveryHook(ctx, options) {
38259
38338
  };
38260
38339
  }
38261
38340
  // src/hooks/think-mode/detector.ts
38262
- var ENGLISH_PATTERNS = [/\bultrathink\b/i, /\bthink\b/i];
38263
38341
  var MULTILINGUAL_KEYWORDS = [
38264
38342
  "\uC0DD\uAC01",
38265
38343
  "\uAC80\uD1A0",
@@ -38345,8 +38423,7 @@ var MULTILINGUAL_KEYWORDS = [
38345
38423
  "fikir",
38346
38424
  "berfikir"
38347
38425
  ];
38348
- var MULTILINGUAL_PATTERNS = MULTILINGUAL_KEYWORDS.map((kw) => new RegExp(kw, "i"));
38349
- var THINK_PATTERNS = [...ENGLISH_PATTERNS, ...MULTILINGUAL_PATTERNS];
38426
+ var COMBINED_THINK_PATTERN = new RegExp(`\\b(?:ultrathink|think)\\b|${MULTILINGUAL_KEYWORDS.join("|")}`, "i");
38350
38427
  var CODE_BLOCK_PATTERN = /```[\s\S]*?```/g;
38351
38428
  var INLINE_CODE_PATTERN = /`[^`]+`/g;
38352
38429
  function removeCodeBlocks(text) {
@@ -38354,7 +38431,7 @@ function removeCodeBlocks(text) {
38354
38431
  }
38355
38432
  function detectThinkKeyword(text) {
38356
38433
  const textWithoutCode = removeCodeBlocks(text);
38357
- return THINK_PATTERNS.some((pattern) => pattern.test(textWithoutCode));
38434
+ return COMBINED_THINK_PATTERN.test(textWithoutCode);
38358
38435
  }
38359
38436
  function extractPromptText(parts) {
38360
38437
  return parts.filter((p) => p.type === "text").map((p) => p.text || "").join("");
@@ -39011,16 +39088,16 @@ async function loadPluginExtendedConfig() {
39011
39088
  }
39012
39089
  return merged;
39013
39090
  }
39014
- var regexCache = new Map;
39091
+ var regexCache2 = new Map;
39015
39092
  function getRegex(pattern) {
39016
- let regex = regexCache.get(pattern);
39093
+ let regex = regexCache2.get(pattern);
39017
39094
  if (!regex) {
39018
39095
  try {
39019
39096
  regex = new RegExp(pattern);
39020
- regexCache.set(pattern, regex);
39097
+ regexCache2.set(pattern, regex);
39021
39098
  } catch {
39022
39099
  regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
39023
- regexCache.set(pattern, regex);
39100
+ regexCache2.set(pattern, regex);
39024
39101
  }
39025
39102
  }
39026
39103
  return regex;
@@ -39886,8 +39963,6 @@ function createToolExecuteAfterHandler(ctx, config2) {
39886
39963
  if (!output) {
39887
39964
  return;
39888
39965
  }
39889
- const claudeConfig = await loadClaudeHooksConfig();
39890
- const extendedConfig = await loadPluginExtendedConfig();
39891
39966
  const cachedInput = getToolInput(input.sessionID, input.tool, input.callID) || {};
39892
39967
  appendTranscriptEntry(input.sessionID, {
39893
39968
  type: "tool_result",
@@ -39899,6 +39974,8 @@ function createToolExecuteAfterHandler(ctx, config2) {
39899
39974
  if (isHookDisabled(config2, "PostToolUse")) {
39900
39975
  return;
39901
39976
  }
39977
+ const claudeConfig = await loadClaudeHooksConfig();
39978
+ const extendedConfig = await loadPluginExtendedConfig();
39902
39979
  const postClient = {
39903
39980
  session: {
39904
39981
  messages: (opts) => ctx.client.session.messages(opts)
@@ -40079,8 +40156,6 @@ function createToolExecuteBeforeHandler(ctx, config2) {
40079
40156
  output.args.todos = parsed;
40080
40157
  log("todowrite: parsed todos string to array", { sessionID: input.sessionID });
40081
40158
  }
40082
- const claudeConfig = await loadClaudeHooksConfig();
40083
- const extendedConfig = await loadPluginExtendedConfig();
40084
40159
  appendTranscriptEntry(input.sessionID, {
40085
40160
  type: "tool_use",
40086
40161
  timestamp: new Date().toISOString(),
@@ -40091,6 +40166,8 @@ function createToolExecuteBeforeHandler(ctx, config2) {
40091
40166
  if (isHookDisabled(config2, "PreToolUse")) {
40092
40167
  return;
40093
40168
  }
40169
+ const claudeConfig = await loadClaudeHooksConfig();
40170
+ const extendedConfig = await loadPluginExtendedConfig();
40094
40171
  const preCtx = {
40095
40172
  sessionId: input.sessionID,
40096
40173
  toolName: input.tool,
@@ -43673,6 +43750,9 @@ async function injectContinuationPrompt(ctx, options) {
43673
43750
  async function handleDetectedCompletion(ctx, input) {
43674
43751
  const { sessionID, state: state3, loopState, directory, apiTimeoutMs } = input;
43675
43752
  if (state3.ultrawork && !state3.verification_pending) {
43753
+ if (state3.verification_session_id) {
43754
+ ctx.client.session.abort({ path: { id: state3.verification_session_id } }).catch(() => {});
43755
+ }
43676
43756
  const verificationState = loopState.markVerificationPending(sessionID);
43677
43757
  if (!verificationState) {
43678
43758
  log(`[${HOOK_NAME3}] Failed to transition ultrawork loop to verification`, {
@@ -43909,6 +43989,9 @@ async function handleFailedVerification(ctx, input) {
43909
43989
  });
43910
43990
  return false;
43911
43991
  }
43992
+ if (state3.verification_session_id) {
43993
+ ctx.client.session.abort({ path: { id: state3.verification_session_id } }).catch(() => {});
43994
+ }
43912
43995
  const resumedState = loopState.restartAfterFailedVerification(parentSessionID, messageCountAtStart);
43913
43996
  if (!resumedState) {
43914
43997
  log(`[${HOOK_NAME3}] Failed to restart loop after verification failure`, {
@@ -47514,9 +47597,9 @@ var BabysittingConfigSchema = exports_external.object({
47514
47597
  });
47515
47598
  // src/config/schema/background-task.ts
47516
47599
  var CircuitBreakerConfigSchema = exports_external.object({
47600
+ enabled: exports_external.boolean().optional(),
47517
47601
  maxToolCalls: exports_external.number().int().min(10).optional(),
47518
- windowSize: exports_external.number().int().min(5).optional(),
47519
- repetitionThresholdPercent: exports_external.number().gt(0).max(100).optional()
47602
+ consecutiveThreshold: exports_external.number().int().min(5).optional()
47520
47603
  });
47521
47604
  var BackgroundTaskConfigSchema = exports_external.object({
47522
47605
  defaultConcurrency: exports_external.number().min(1).optional(),
@@ -47712,7 +47795,8 @@ var HookNameSchema = exports_external.enum([
47712
47795
  "write-existing-file-guard",
47713
47796
  "anthropic-effort",
47714
47797
  "hashline-read-enhancer",
47715
- "read-image-resizer"
47798
+ "read-image-resizer",
47799
+ "todo-description-override"
47716
47800
  ]);
47717
47801
  // src/config/schema/notification.ts
47718
47802
  var NotificationConfigSchema = exports_external.object({
@@ -49115,7 +49199,7 @@ var START_WORK_TEMPLATE = `You are starting a Sisyphus work session.
49115
49199
  - \`--worktree <path>\` (optional): absolute path to an existing git worktree to work in
49116
49200
  - If specified and valid: hook pre-sets worktree_path in boulder.json
49117
49201
  - If specified but invalid: you must run \`git worktree add <path> <branch>\` first
49118
- - If omitted: you MUST choose or create a worktree (see Worktree Setup below)
49202
+ - If omitted: work directly in the current project directory (no worktree)
49119
49203
 
49120
49204
  ## WHAT TO DO
49121
49205
 
@@ -49132,7 +49216,7 @@ var START_WORK_TEMPLATE = `You are starting a Sisyphus work session.
49132
49216
  - If ONE plan: auto-select it
49133
49217
  - If MULTIPLE plans: show list with timestamps, ask user to select
49134
49218
 
49135
- 4. **Worktree Setup** (when \`worktree_path\` not already set in boulder.json):
49219
+ 4. **Worktree Setup** (ONLY when \`--worktree\` was explicitly specified and \`worktree_path\` not already set in boulder.json):
49136
49220
  1. \`git worktree list --porcelain\` \u2014 see available worktrees
49137
49221
  2. Create: \`git worktree add <absolute-path> <branch-or-HEAD>\`
49138
49222
  3. Update boulder.json to add \`"worktree_path": "<absolute-path>"\`
@@ -49194,9 +49278,41 @@ Reading plan and beginning execution...
49194
49278
 
49195
49279
  - The session_id is injected by the hook - use it directly
49196
49280
  - Always update boulder.json BEFORE starting work
49197
- - Always set worktree_path in boulder.json before executing any tasks
49281
+ - If worktree_path is set in boulder.json, all work happens inside that worktree directory
49198
49282
  - Read the FULL plan file before delegating any tasks
49199
- - Follow atlas delegation protocols (7-section format)`;
49283
+ - Follow atlas delegation protocols (7-section format)
49284
+
49285
+ ## TASK BREAKDOWN (MANDATORY)
49286
+
49287
+ 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.
49288
+
49289
+ **How to break down**:
49290
+ - Each plan checkbox item (e.g., \`- [ ] Add user authentication\`) must be split into concrete, actionable sub-tasks
49291
+ - Sub-tasks should be specific enough that each one touches a clear set of files/functions
49292
+ - Include: file to modify, what to change, expected behavior, and how to verify
49293
+ - 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
49294
+
49295
+ **Example breakdown**:
49296
+ Plan task: \`- [ ] Add rate limiting to API\`
49297
+ \u2192 Todo items:
49298
+ 1. Create \`src/middleware/rate-limiter.ts\` with sliding window algorithm (max 100 req/min per IP)
49299
+ 2. Add RateLimiter middleware to \`src/app.ts\` router chain, before auth middleware
49300
+ 3. Add rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining) to response in \`rate-limiter.ts\`
49301
+ 4. Add test: verify 429 response after exceeding limit in \`src/middleware/rate-limiter.test.ts\`
49302
+ 5. Add test: verify headers are present on normal responses
49303
+
49304
+ Register these as task/todo items so progress is tracked and visible throughout the session.
49305
+
49306
+ ## WORKTREE COMPLETION
49307
+
49308
+ When working in a worktree (\`worktree_path\` is set in boulder.json) and ALL plan tasks are complete:
49309
+ 1. Commit all remaining changes in the worktree
49310
+ 2. Switch to the main working directory (the original repo, NOT the worktree)
49311
+ 3. Merge the worktree branch into the current branch: \`git merge <worktree-branch>\`
49312
+ 4. If merge succeeds, clean up: \`git worktree remove <worktree-path>\`
49313
+ 5. Remove the boulder.json state
49314
+
49315
+ 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).`;
49200
49316
 
49201
49317
  // src/features/builtin-commands/templates/handoff.ts
49202
49318
  var HANDOFF_TEMPLATE = `# Handoff Command
@@ -49578,9 +49694,6 @@ function skillToCommandInfo(skill) {
49578
49694
  lazyContentLoader: skill.lazyContent
49579
49695
  };
49580
49696
  }
49581
- function filterDiscoveredCommandsByScope(commands3, scope) {
49582
- return commands3.filter((command) => command.scope === scope);
49583
- }
49584
49697
  async function discoverAllCommands(options) {
49585
49698
  const discoveredCommands = discoverCommandsSync(process.cwd(), {
49586
49699
  pluginsEnabled: options?.pluginsEnabled,
@@ -49588,14 +49701,17 @@ async function discoverAllCommands(options) {
49588
49701
  });
49589
49702
  const skills2 = options?.skills ?? await discoverAllSkills();
49590
49703
  const skillCommands = skills2.map(skillToCommandInfo);
49704
+ const scopeOrder = ["project", "user", "opencode-project", "opencode", "builtin", "plugin"];
49705
+ const grouped = new Map;
49706
+ for (const cmd of discoveredCommands) {
49707
+ const list = grouped.get(cmd.scope) ?? [];
49708
+ list.push(cmd);
49709
+ grouped.set(cmd.scope, list);
49710
+ }
49711
+ const orderedCommands = scopeOrder.flatMap((scope) => grouped.get(scope) ?? []);
49591
49712
  return [
49592
49713
  ...skillCommands,
49593
- ...filterDiscoveredCommandsByScope(discoveredCommands, "project"),
49594
- ...filterDiscoveredCommandsByScope(discoveredCommands, "user"),
49595
- ...filterDiscoveredCommandsByScope(discoveredCommands, "opencode-project"),
49596
- ...filterDiscoveredCommandsByScope(discoveredCommands, "opencode"),
49597
- ...filterDiscoveredCommandsByScope(discoveredCommands, "builtin"),
49598
- ...filterDiscoveredCommandsByScope(discoveredCommands, "plugin")
49714
+ ...orderedCommands
49599
49715
  ];
49600
49716
  }
49601
49717
  async function findCommand2(commandName, options) {
@@ -53356,6 +53472,7 @@ function getErrorMessage2(error48) {
53356
53472
  return "";
53357
53473
  }
53358
53474
  }
53475
+ var DEFAULT_RETRY_PATTERN = new RegExp(`\\b(${DEFAULT_CONFIG2.retry_on_errors.join("|")})\\b`);
53359
53476
  function extractStatusCode(error48, retryOnErrors) {
53360
53477
  if (!error48)
53361
53478
  return;
@@ -53370,8 +53487,7 @@ function extractStatusCode(error48, retryOnErrors) {
53370
53487
  if (statusCode !== undefined) {
53371
53488
  return statusCode;
53372
53489
  }
53373
- const codes = retryOnErrors ?? DEFAULT_CONFIG2.retry_on_errors;
53374
- const pattern = new RegExp(`\\b(${codes.join("|")})\\b`);
53490
+ const pattern = retryOnErrors ? new RegExp(`\\b(${retryOnErrors.join("|")})\\b`) : DEFAULT_RETRY_PATTERN;
53375
53491
  const message = getErrorMessage2(error48);
53376
53492
  const statusMatch = message.match(pattern);
53377
53493
  if (statusMatch) {
@@ -54652,97 +54768,97 @@ function toImageDimensions(width, height) {
54652
54768
  }
54653
54769
  return { width, height };
54654
54770
  }
54655
- function parsePngDimensions(buffer) {
54656
- if (buffer.length < 24) {
54771
+ function parsePngDimensions(buffer2) {
54772
+ if (buffer2.length < 24) {
54657
54773
  return null;
54658
54774
  }
54659
- const isPngSignature = buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71 && buffer[4] === 13 && buffer[5] === 10 && buffer[6] === 26 && buffer[7] === 10;
54660
- if (!isPngSignature || buffer.toString("ascii", 12, 16) !== "IHDR") {
54775
+ const isPngSignature = buffer2[0] === 137 && buffer2[1] === 80 && buffer2[2] === 78 && buffer2[3] === 71 && buffer2[4] === 13 && buffer2[5] === 10 && buffer2[6] === 26 && buffer2[7] === 10;
54776
+ if (!isPngSignature || buffer2.toString("ascii", 12, 16) !== "IHDR") {
54661
54777
  return null;
54662
54778
  }
54663
- const width = buffer.readUInt32BE(16);
54664
- const height = buffer.readUInt32BE(20);
54779
+ const width = buffer2.readUInt32BE(16);
54780
+ const height = buffer2.readUInt32BE(20);
54665
54781
  return toImageDimensions(width, height);
54666
54782
  }
54667
- function parseGifDimensions(buffer) {
54668
- if (buffer.length < 10) {
54783
+ function parseGifDimensions(buffer2) {
54784
+ if (buffer2.length < 10) {
54669
54785
  return null;
54670
54786
  }
54671
- if (buffer.toString("ascii", 0, 4) !== "GIF8") {
54787
+ if (buffer2.toString("ascii", 0, 4) !== "GIF8") {
54672
54788
  return null;
54673
54789
  }
54674
- const width = buffer.readUInt16LE(6);
54675
- const height = buffer.readUInt16LE(8);
54790
+ const width = buffer2.readUInt16LE(6);
54791
+ const height = buffer2.readUInt16LE(8);
54676
54792
  return toImageDimensions(width, height);
54677
54793
  }
54678
- function parseJpegDimensions(buffer) {
54679
- if (buffer.length < 4 || buffer[0] !== 255 || buffer[1] !== 216) {
54794
+ function parseJpegDimensions(buffer2) {
54795
+ if (buffer2.length < 4 || buffer2[0] !== 255 || buffer2[1] !== 216) {
54680
54796
  return null;
54681
54797
  }
54682
54798
  let offset = 2;
54683
- while (offset < buffer.length) {
54684
- if (buffer[offset] !== 255) {
54799
+ while (offset < buffer2.length) {
54800
+ if (buffer2[offset] !== 255) {
54685
54801
  offset += 1;
54686
54802
  continue;
54687
54803
  }
54688
- while (offset < buffer.length && buffer[offset] === 255) {
54804
+ while (offset < buffer2.length && buffer2[offset] === 255) {
54689
54805
  offset += 1;
54690
54806
  }
54691
- if (offset >= buffer.length) {
54807
+ if (offset >= buffer2.length) {
54692
54808
  return null;
54693
54809
  }
54694
- const marker = buffer[offset];
54810
+ const marker = buffer2[offset];
54695
54811
  offset += 1;
54696
54812
  if (marker === 217 || marker === 218) {
54697
54813
  break;
54698
54814
  }
54699
- if (offset + 1 >= buffer.length) {
54815
+ if (offset + 1 >= buffer2.length) {
54700
54816
  return null;
54701
54817
  }
54702
- const segmentLength = buffer.readUInt16BE(offset);
54818
+ const segmentLength = buffer2.readUInt16BE(offset);
54703
54819
  if (segmentLength < 2) {
54704
54820
  return null;
54705
54821
  }
54706
- if ((marker === 192 || marker === 194) && offset + 7 < buffer.length) {
54707
- const height = buffer.readUInt16BE(offset + 3);
54708
- const width = buffer.readUInt16BE(offset + 5);
54822
+ if ((marker === 192 || marker === 194) && offset + 7 < buffer2.length) {
54823
+ const height = buffer2.readUInt16BE(offset + 3);
54824
+ const width = buffer2.readUInt16BE(offset + 5);
54709
54825
  return toImageDimensions(width, height);
54710
54826
  }
54711
54827
  offset += segmentLength;
54712
54828
  }
54713
54829
  return null;
54714
54830
  }
54715
- function readUInt24LE(buffer, offset) {
54716
- return buffer[offset] | buffer[offset + 1] << 8 | buffer[offset + 2] << 16;
54831
+ function readUInt24LE(buffer2, offset) {
54832
+ return buffer2[offset] | buffer2[offset + 1] << 8 | buffer2[offset + 2] << 16;
54717
54833
  }
54718
- function parseWebpDimensions(buffer) {
54719
- if (buffer.length < 16) {
54834
+ function parseWebpDimensions(buffer2) {
54835
+ if (buffer2.length < 16) {
54720
54836
  return null;
54721
54837
  }
54722
- if (buffer.toString("ascii", 0, 4) !== "RIFF" || buffer.toString("ascii", 8, 12) !== "WEBP") {
54838
+ if (buffer2.toString("ascii", 0, 4) !== "RIFF" || buffer2.toString("ascii", 8, 12) !== "WEBP") {
54723
54839
  return null;
54724
54840
  }
54725
- const chunkType = buffer.toString("ascii", 12, 16);
54841
+ const chunkType = buffer2.toString("ascii", 12, 16);
54726
54842
  if (chunkType === "VP8 ") {
54727
- if (buffer[23] !== 157 || buffer[24] !== 1 || buffer[25] !== 42) {
54843
+ if (buffer2[23] !== 157 || buffer2[24] !== 1 || buffer2[25] !== 42) {
54728
54844
  return null;
54729
54845
  }
54730
- const width = buffer.readUInt16LE(26) & 16383;
54731
- const height = buffer.readUInt16LE(28) & 16383;
54846
+ const width = buffer2.readUInt16LE(26) & 16383;
54847
+ const height = buffer2.readUInt16LE(28) & 16383;
54732
54848
  return toImageDimensions(width, height);
54733
54849
  }
54734
54850
  if (chunkType === "VP8L") {
54735
- if (buffer.length < 25 || buffer[20] !== 47) {
54851
+ if (buffer2.length < 25 || buffer2[20] !== 47) {
54736
54852
  return null;
54737
54853
  }
54738
- const bits = buffer.readUInt32LE(21);
54854
+ const bits = buffer2.readUInt32LE(21);
54739
54855
  const width = (bits & 16383) + 1;
54740
54856
  const height = (bits >>> 14 & 16383) + 1;
54741
54857
  return toImageDimensions(width, height);
54742
54858
  }
54743
54859
  if (chunkType === "VP8X") {
54744
- const width = readUInt24LE(buffer, 24) + 1;
54745
- const height = readUInt24LE(buffer, 27) + 1;
54860
+ const width = readUInt24LE(buffer2, 24) + 1;
54861
+ const height = readUInt24LE(buffer2, 27) + 1;
54746
54862
  return toImageDimensions(width, height);
54747
54863
  }
54748
54864
  return null;
@@ -54757,22 +54873,22 @@ function parseImageDimensions(base64DataUrl, mimeType) {
54757
54873
  return null;
54758
54874
  }
54759
54875
  const headerBase64 = rawBase64.length > HEADER_BASE64_CHARS ? rawBase64.slice(0, HEADER_BASE64_CHARS) : rawBase64;
54760
- const buffer = Buffer.from(headerBase64, "base64");
54761
- if (buffer.length === 0) {
54876
+ const buffer2 = Buffer.from(headerBase64, "base64");
54877
+ if (buffer2.length === 0) {
54762
54878
  return null;
54763
54879
  }
54764
54880
  const normalizedMime = mimeType.toLowerCase();
54765
54881
  if (normalizedMime === "image/png") {
54766
- return parsePngDimensions(buffer);
54882
+ return parsePngDimensions(buffer2);
54767
54883
  }
54768
54884
  if (normalizedMime === "image/gif") {
54769
- return parseGifDimensions(buffer);
54885
+ return parseGifDimensions(buffer2);
54770
54886
  }
54771
54887
  if (normalizedMime === "image/jpeg" || normalizedMime === "image/jpg") {
54772
- return parseJpegDimensions(buffer);
54888
+ return parseJpegDimensions(buffer2);
54773
54889
  }
54774
54890
  if (normalizedMime === "image/webp") {
54775
- return parseWebpDimensions(buffer);
54891
+ return parseWebpDimensions(buffer2);
54776
54892
  }
54777
54893
  return null;
54778
54894
  } catch {
@@ -55062,6 +55178,46 @@ function createReadImageResizerHook(_ctx) {
55062
55178
  }
55063
55179
  };
55064
55180
  }
55181
+ // src/hooks/todo-description-override/description.ts
55182
+ var TODOWRITE_DESCRIPTION = `Use this tool to create and manage a structured task list for tracking progress on multi-step work.
55183
+
55184
+ ## Todo Format (MANDATORY)
55185
+
55186
+ Each todo title MUST encode four elements: WHERE, WHY, HOW, and EXPECTED RESULT.
55187
+
55188
+ Format: "[WHERE] [HOW] to [WHY] \u2014 expect [RESULT]"
55189
+
55190
+ GOOD:
55191
+ - "src/utils/validation.ts: Add validateEmail() for input sanitization \u2014 returns boolean"
55192
+ - "UserService.create(): Call validateEmail() before DB insert \u2014 rejects invalid emails with 400"
55193
+ - "validation.test.ts: Add test for missing @ sign \u2014 expect validateEmail('foo') to return false"
55194
+
55195
+ BAD:
55196
+ - "Implement email validation" (where? how? what result?)
55197
+ - "Add dark mode" (this is a feature, not a todo)
55198
+ - "Fix auth" (what file? what changes? what's expected?)
55199
+
55200
+ ## Granularity Rules
55201
+
55202
+ Each todo MUST be a single atomic action completable in 1-3 tool calls. If it needs more, split it.
55203
+
55204
+ **Size test**: Can you complete this todo by editing one file or running one command? If not, it's too big.
55205
+
55206
+ ## Task Management
55207
+ - One in_progress at a time. Complete it before starting the next.
55208
+ - Mark completed immediately after finishing each item.
55209
+ - Skip this tool for single trivial tasks (one-step, obvious action).`;
55210
+
55211
+ // src/hooks/todo-description-override/hook.ts
55212
+ function createTodoDescriptionOverrideHook() {
55213
+ return {
55214
+ "tool.definition": async (input, output) => {
55215
+ if (input.toolID === "todowrite") {
55216
+ output.description = TODOWRITE_DESCRIPTION;
55217
+ }
55218
+ }
55219
+ };
55220
+ }
55065
55221
  // src/hooks/anthropic-effort/hook.ts
55066
55222
  var OPUS_4_6_PATTERN = /claude-opus-4[-.]6/i;
55067
55223
  function isClaudeProvider(providerID, modelID) {
@@ -73166,8 +73322,8 @@ function convertBase64ImageToJpeg(base64Data, mimeType) {
73166
73322
  const tempFiles = [inputPath];
73167
73323
  try {
73168
73324
  const cleanBase64 = base64Data.replace(/^data:[^;]+;base64,/, "");
73169
- const buffer = Buffer.from(cleanBase64, "base64");
73170
- writeFileSync19(inputPath, buffer);
73325
+ const buffer2 = Buffer.from(cleanBase64, "base64");
73326
+ writeFileSync19(inputPath, buffer2);
73171
73327
  log(`[image-converter] Converting Base64 ${mimeType} to JPEG`);
73172
73328
  const outputPath = convertImageToJpeg(inputPath, mimeType);
73173
73329
  tempFiles.push(outputPath);
@@ -75636,10 +75792,11 @@ Returns summary format: id, subject, status, owner, blockedBy (not full descript
75636
75792
  allTasks.push(task);
75637
75793
  }
75638
75794
  }
75795
+ const taskMap = new Map(allTasks.map((t) => [t.id, t]));
75639
75796
  const activeTasks = allTasks.filter((task) => task.status !== "completed" && task.status !== "deleted");
75640
75797
  const summaries = activeTasks.map((task) => {
75641
75798
  const unresolvedBlockers = task.blockedBy.filter((blockerId) => {
75642
- const blockerTask = allTasks.find((t) => t.id === blockerId);
75799
+ const blockerTask = taskMap.get(blockerId);
75643
75800
  return !blockerTask || blockerTask.status !== "completed";
75644
75801
  });
75645
75802
  return {
@@ -76328,6 +76485,15 @@ function applyPrepend(lines, text) {
76328
76485
  }
76329
76486
 
76330
76487
  // src/tools/hashline-edit/edit-operations.ts
76488
+ function arraysEqual(a, b) {
76489
+ if (a.length !== b.length)
76490
+ return false;
76491
+ for (let i2 = 0;i2 < a.length; i2++) {
76492
+ if (a[i2] !== b[i2])
76493
+ return false;
76494
+ }
76495
+ return true;
76496
+ }
76331
76497
  function applyHashlineEditsWithReport(content, edits) {
76332
76498
  if (edits.length === 0) {
76333
76499
  return {
@@ -76357,9 +76523,7 @@ function applyHashlineEditsWithReport(content, edits) {
76357
76523
  switch (edit.op) {
76358
76524
  case "replace": {
76359
76525
  const next = edit.end ? applyReplaceLines(lines, edit.pos, edit.end, edit.lines, { skipValidation: true }) : applySetLine(lines, edit.pos, edit.lines, { skipValidation: true });
76360
- if (next.join(`
76361
- `) === lines.join(`
76362
- `)) {
76526
+ if (arraysEqual(next, lines)) {
76363
76527
  noopEdits += 1;
76364
76528
  break;
76365
76529
  }
@@ -76368,9 +76532,7 @@ function applyHashlineEditsWithReport(content, edits) {
76368
76532
  }
76369
76533
  case "append": {
76370
76534
  const next = edit.pos ? applyInsertAfter(lines, edit.pos, edit.lines, { skipValidation: true }) : applyAppend(lines, edit.lines);
76371
- if (next.join(`
76372
- `) === lines.join(`
76373
- `)) {
76535
+ if (arraysEqual(next, lines)) {
76374
76536
  noopEdits += 1;
76375
76537
  break;
76376
76538
  }
@@ -76379,9 +76541,7 @@ function applyHashlineEditsWithReport(content, edits) {
76379
76541
  }
76380
76542
  case "prepend": {
76381
76543
  const next = edit.pos ? applyInsertBefore(lines, edit.pos, edit.lines, { skipValidation: true }) : applyPrepend(lines, edit.lines);
76382
- if (next.join(`
76383
- `) === lines.join(`
76384
- `)) {
76544
+ if (arraysEqual(next, lines)) {
76385
76545
  noopEdits += 1;
76386
76546
  break;
76387
76547
  }
@@ -77406,6 +77566,7 @@ function createToolGuardHooks(args) {
77406
77566
  const hashlineReadEnhancer = isHookEnabled("hashline-read-enhancer") ? safeHook("hashline-read-enhancer", () => createHashlineReadEnhancerHook(ctx, { hashline_edit: { enabled: pluginConfig.hashline_edit ?? false } })) : null;
77407
77567
  const jsonErrorRecovery = isHookEnabled("json-error-recovery") ? safeHook("json-error-recovery", () => createJsonErrorRecoveryHook(ctx)) : null;
77408
77568
  const readImageResizer = isHookEnabled("read-image-resizer") ? safeHook("read-image-resizer", () => createReadImageResizerHook(ctx)) : null;
77569
+ const todoDescriptionOverride = isHookEnabled("todo-description-override") ? safeHook("todo-description-override", () => createTodoDescriptionOverrideHook()) : null;
77409
77570
  return {
77410
77571
  commentChecker,
77411
77572
  toolOutputTruncator,
@@ -77417,7 +77578,8 @@ function createToolGuardHooks(args) {
77417
77578
  writeExistingFileGuard,
77418
77579
  hashlineReadEnhancer,
77419
77580
  jsonErrorRecovery,
77420
- readImageResizer
77581
+ readImageResizer,
77582
+ todoDescriptionOverride
77421
77583
  };
77422
77584
  }
77423
77585
 
@@ -77944,8 +78106,8 @@ var MIN_STABILITY_TIME_MS2 = 10 * 1000;
77944
78106
  var DEFAULT_STALE_TIMEOUT_MS = 1200000;
77945
78107
  var DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS = 1800000;
77946
78108
  var DEFAULT_MAX_TOOL_CALLS = 200;
77947
- var DEFAULT_CIRCUIT_BREAKER_WINDOW_SIZE = 20;
77948
- var DEFAULT_CIRCUIT_BREAKER_REPETITION_THRESHOLD_PERCENT = 80;
78109
+ var DEFAULT_CIRCUIT_BREAKER_CONSECUTIVE_THRESHOLD = 20;
78110
+ var DEFAULT_CIRCUIT_BREAKER_ENABLED = true;
77949
78111
  var MIN_RUNTIME_BEFORE_STALE_MS = 30000;
77950
78112
  var MIN_IDLE_TIME_MS = 5000;
77951
78113
  var POLLING_INTERVAL_MS = 3000;
@@ -78366,6 +78528,22 @@ function removeTaskToastTracking(taskId) {
78366
78528
  }
78367
78529
  }
78368
78530
 
78531
+ // src/features/background-agent/session-status-classifier.ts
78532
+ var ACTIVE_SESSION_STATUSES = new Set(["busy", "retry", "running"]);
78533
+ var KNOWN_TERMINAL_STATUSES = new Set(["idle", "interrupted"]);
78534
+ function isActiveSessionStatus(type2) {
78535
+ if (ACTIVE_SESSION_STATUSES.has(type2)) {
78536
+ return true;
78537
+ }
78538
+ if (!KNOWN_TERMINAL_STATUSES.has(type2)) {
78539
+ log("[background-agent] Unknown session status type encountered:", type2);
78540
+ }
78541
+ return false;
78542
+ }
78543
+ function isTerminalSessionStatus(type2) {
78544
+ return KNOWN_TERMINAL_STATUSES.has(type2) && type2 !== "idle";
78545
+ }
78546
+
78369
78547
  // src/features/background-agent/task-poller.ts
78370
78548
  var TERMINAL_TASK_STATUSES = new Set([
78371
78549
  "completed",
@@ -78444,7 +78622,7 @@ async function checkAndInterruptStaleTasks(args) {
78444
78622
  if (!startedAt || !sessionID)
78445
78623
  continue;
78446
78624
  const sessionStatus = sessionStatuses?.[sessionID]?.type;
78447
- const sessionIsRunning = sessionStatus !== undefined && sessionStatus !== "idle";
78625
+ const sessionIsRunning = sessionStatus !== undefined && isActiveSessionStatus(sessionStatus);
78448
78626
  const runtime = now - startedAt.getTime();
78449
78627
  if (!task.progress?.lastUpdate) {
78450
78628
  if (sessionIsRunning)
@@ -78500,51 +78678,57 @@ async function checkAndInterruptStaleTasks(args) {
78500
78678
  // src/features/background-agent/loop-detector.ts
78501
78679
  function resolveCircuitBreakerSettings(config4) {
78502
78680
  return {
78681
+ enabled: config4?.circuitBreaker?.enabled ?? DEFAULT_CIRCUIT_BREAKER_ENABLED,
78503
78682
  maxToolCalls: config4?.circuitBreaker?.maxToolCalls ?? config4?.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS,
78504
- windowSize: config4?.circuitBreaker?.windowSize ?? DEFAULT_CIRCUIT_BREAKER_WINDOW_SIZE,
78505
- repetitionThresholdPercent: config4?.circuitBreaker?.repetitionThresholdPercent ?? DEFAULT_CIRCUIT_BREAKER_REPETITION_THRESHOLD_PERCENT
78683
+ consecutiveThreshold: config4?.circuitBreaker?.consecutiveThreshold ?? DEFAULT_CIRCUIT_BREAKER_CONSECUTIVE_THRESHOLD
78506
78684
  };
78507
78685
  }
78508
- function recordToolCall(window, toolName, settings) {
78509
- const previous = window?.toolNames ?? [];
78510
- const toolNames = [...previous, toolName].slice(-settings.windowSize);
78686
+ function recordToolCall(window, toolName, settings, toolInput) {
78687
+ const signature = createToolCallSignature(toolName, toolInput);
78688
+ if (window && window.lastSignature === signature) {
78689
+ return {
78690
+ lastSignature: signature,
78691
+ consecutiveCount: window.consecutiveCount + 1,
78692
+ threshold: settings.consecutiveThreshold
78693
+ };
78694
+ }
78511
78695
  return {
78512
- toolNames,
78513
- windowSize: settings.windowSize,
78514
- thresholdPercent: settings.repetitionThresholdPercent
78696
+ lastSignature: signature,
78697
+ consecutiveCount: 1,
78698
+ threshold: settings.consecutiveThreshold
78515
78699
  };
78516
78700
  }
78517
- function detectRepetitiveToolUse(window) {
78518
- if (!window || window.toolNames.length === 0) {
78519
- return { triggered: false };
78520
- }
78521
- const counts = new Map;
78522
- for (const toolName of window.toolNames) {
78523
- counts.set(toolName, (counts.get(toolName) ?? 0) + 1);
78701
+ function sortObject2(obj) {
78702
+ if (obj === null || obj === undefined)
78703
+ return obj;
78704
+ if (typeof obj !== "object")
78705
+ return obj;
78706
+ if (Array.isArray(obj))
78707
+ return obj.map(sortObject2);
78708
+ const sorted = {};
78709
+ const keys = Object.keys(obj).sort();
78710
+ for (const key of keys) {
78711
+ sorted[key] = sortObject2(obj[key]);
78524
78712
  }
78525
- let repeatedTool;
78526
- let repeatedCount = 0;
78527
- for (const [toolName, count] of counts.entries()) {
78528
- if (count > repeatedCount) {
78529
- repeatedTool = toolName;
78530
- repeatedCount = count;
78531
- }
78713
+ return sorted;
78714
+ }
78715
+ function createToolCallSignature(toolName, toolInput) {
78716
+ if (toolInput === undefined || toolInput === null) {
78717
+ return toolName;
78532
78718
  }
78533
- const sampleSize = window.toolNames.length;
78534
- const minimumSampleSize = Math.min(window.windowSize, Math.ceil(window.windowSize * window.thresholdPercent / 100));
78535
- if (sampleSize < minimumSampleSize) {
78536
- return { triggered: false };
78719
+ if (Object.keys(toolInput).length === 0) {
78720
+ return toolName;
78537
78721
  }
78538
- const thresholdCount = Math.ceil(sampleSize * window.thresholdPercent / 100);
78539
- if (!repeatedTool || repeatedCount < thresholdCount) {
78722
+ return `${toolName}::${JSON.stringify(sortObject2(toolInput))}`;
78723
+ }
78724
+ function detectRepetitiveToolUse(window) {
78725
+ if (!window || window.consecutiveCount < window.threshold) {
78540
78726
  return { triggered: false };
78541
78727
  }
78542
78728
  return {
78543
78729
  triggered: true,
78544
- toolName: repeatedTool,
78545
- repeatedCount,
78546
- sampleSize,
78547
- thresholdPercent: window.thresholdPercent
78730
+ toolName: window.lastSignature.split("::")[0],
78731
+ repeatedCount: window.consecutiveCount
78548
78732
  };
78549
78733
  }
78550
78734
 
@@ -78643,6 +78827,7 @@ class BackgroundManager {
78643
78827
  preStartDescendantReservations;
78644
78828
  enableParentSessionNotifications;
78645
78829
  taskHistory = new TaskHistory;
78830
+ cachedCircuitBreakerSettings;
78646
78831
  constructor(ctx, config4, options) {
78647
78832
  this.tasks = new Map;
78648
78833
  this.notifications = new Map;
@@ -79235,35 +79420,36 @@ class BackgroundManager {
79235
79420
  }
79236
79421
  task.progress.lastUpdate = new Date;
79237
79422
  if (partInfo?.type === "tool" || partInfo?.tool) {
79238
- const countedToolPartIDs = task.progress.countedToolPartIDs ?? [];
79239
- const shouldCountToolCall = !partInfo.id || partInfo.state?.status !== "running" || !countedToolPartIDs.includes(partInfo.id);
79423
+ const countedToolPartIDs = task.progress.countedToolPartIDs ?? new Set;
79424
+ const shouldCountToolCall = !partInfo.id || partInfo.state?.status !== "running" || !countedToolPartIDs.has(partInfo.id);
79240
79425
  if (!shouldCountToolCall) {
79241
79426
  return;
79242
79427
  }
79243
79428
  if (partInfo.id && partInfo.state?.status === "running") {
79244
- task.progress.countedToolPartIDs = [...countedToolPartIDs, partInfo.id];
79429
+ countedToolPartIDs.add(partInfo.id);
79430
+ task.progress.countedToolPartIDs = countedToolPartIDs;
79245
79431
  }
79246
79432
  task.progress.toolCalls += 1;
79247
79433
  task.progress.lastTool = partInfo.tool;
79248
- const circuitBreaker = resolveCircuitBreakerSettings(this.config);
79434
+ const circuitBreaker = this.cachedCircuitBreakerSettings ?? (this.cachedCircuitBreakerSettings = resolveCircuitBreakerSettings(this.config));
79249
79435
  if (partInfo.tool) {
79250
- task.progress.toolCallWindow = recordToolCall(task.progress.toolCallWindow, partInfo.tool, circuitBreaker);
79251
- const loopDetection = detectRepetitiveToolUse(task.progress.toolCallWindow);
79252
- if (loopDetection.triggered) {
79253
- log("[background-agent] Circuit breaker: repetitive tool usage detected", {
79254
- taskId: task.id,
79255
- agent: task.agent,
79256
- sessionID,
79257
- toolName: loopDetection.toolName,
79258
- repeatedCount: loopDetection.repeatedCount,
79259
- sampleSize: loopDetection.sampleSize,
79260
- thresholdPercent: loopDetection.thresholdPercent
79261
- });
79262
- this.cancelTask(task.id, {
79263
- source: "circuit-breaker",
79264
- 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.`
79265
- });
79266
- return;
79436
+ task.progress.toolCallWindow = recordToolCall(task.progress.toolCallWindow, partInfo.tool, circuitBreaker, partInfo.state?.input);
79437
+ if (circuitBreaker.enabled) {
79438
+ const loopDetection = detectRepetitiveToolUse(task.progress.toolCallWindow);
79439
+ if (loopDetection.triggered) {
79440
+ log("[background-agent] Circuit breaker: consecutive tool usage detected", {
79441
+ taskId: task.id,
79442
+ agent: task.agent,
79443
+ sessionID,
79444
+ toolName: loopDetection.toolName,
79445
+ repeatedCount: loopDetection.repeatedCount
79446
+ });
79447
+ this.cancelTask(task.id, {
79448
+ source: "circuit-breaker",
79449
+ 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.`
79450
+ });
79451
+ return;
79452
+ }
79267
79453
  }
79268
79454
  }
79269
79455
  const maxToolCalls = circuitBreaker.maxToolCalls;
@@ -79910,7 +80096,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
79910
80096
  continue;
79911
80097
  }
79912
80098
  }
79913
- if (sessionStatus && sessionStatus.type !== "idle") {
80099
+ if (sessionStatus && isActiveSessionStatus(sessionStatus.type)) {
79914
80100
  log("[background-agent] Session still running, relying on event-based progress:", {
79915
80101
  taskId: task.id,
79916
80102
  sessionID,
@@ -79919,6 +80105,17 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
79919
80105
  });
79920
80106
  continue;
79921
80107
  }
80108
+ if (sessionStatus && isTerminalSessionStatus(sessionStatus.type)) {
80109
+ await this.tryCompleteTask(task, `polling (terminal session status: ${sessionStatus.type})`);
80110
+ continue;
80111
+ }
80112
+ if (sessionStatus && sessionStatus.type !== "idle") {
80113
+ log("[background-agent] Unknown session status, treating as potentially idle:", {
80114
+ taskId: task.id,
80115
+ sessionID,
80116
+ sessionStatus: sessionStatus.type
80117
+ });
80118
+ }
79922
80119
  const completionSource = sessionStatus?.type === "idle" ? "polling (idle status)" : "polling (session gone from status)";
79923
80120
  const hasValidOutput = await this.validateSessionHasOutput(sessionID);
79924
80121
  if (!hasValidOutput) {
@@ -82575,8 +82772,8 @@ async function generateVerifier(length) {
82575
82772
  return await random(length);
82576
82773
  }
82577
82774
  async function generateChallenge(code_verifier) {
82578
- const buffer = await (await crypto3).subtle.digest("SHA-256", new TextEncoder().encode(code_verifier));
82579
- return btoa(String.fromCharCode(...new Uint8Array(buffer))).replace(/\//g, "_").replace(/\+/g, "-").replace(/=/g, "");
82775
+ const buffer2 = await (await crypto3).subtle.digest("SHA-256", new TextEncoder().encode(code_verifier));
82776
+ return btoa(String.fromCharCode(...new Uint8Array(buffer2))).replace(/\//g, "_").replace(/\+/g, "-").replace(/=/g, "");
82580
82777
  }
82581
82778
  async function pkceChallenge(length) {
82582
82779
  if (!length)
@@ -97677,10 +97874,7 @@ function createPluginInterface(args) {
97677
97874
  const { ctx, pluginConfig, firstMessageVariantGate, managers, hooks: hooks2, tools } = args;
97678
97875
  return {
97679
97876
  tool: tools,
97680
- "chat.params": async (input, output) => {
97681
- const handler = createChatParamsHandler({ anthropicEffort: hooks2.anthropicEffort });
97682
- await handler(input, output);
97683
- },
97877
+ "chat.params": createChatParamsHandler({ anthropicEffort: hooks2.anthropicEffort }),
97684
97878
  "chat.headers": createChatHeadersHandler({ ctx }),
97685
97879
  "chat.message": createChatMessageHandler3({
97686
97880
  ctx,
@@ -97707,7 +97901,10 @@ function createPluginInterface(args) {
97707
97901
  "tool.execute.after": createToolExecuteAfterHandler3({
97708
97902
  ctx,
97709
97903
  hooks: hooks2
97710
- })
97904
+ }),
97905
+ "tool.definition": async (input, output) => {
97906
+ await hooks2.todoDescriptionOverride?.["tool.definition"]?.(input, output);
97907
+ }
97711
97908
  };
97712
97909
  }
97713
97910