oh-my-opencode-slim 2.1.1 → 2.2.0

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.
Files changed (40) hide show
  1. package/README.ja-JP.md +13 -12
  2. package/README.ko-KR.md +13 -12
  3. package/README.md +29 -16
  4. package/README.zh-CN.md +13 -12
  5. package/dist/agents/index.d.ts +0 -4
  6. package/dist/cli/index.js +49 -6
  7. package/dist/config/constants.d.ts +10 -0
  8. package/dist/config/schema.d.ts +680 -2
  9. package/dist/config/strip-orchestrator-model.d.ts +9 -0
  10. package/dist/hooks/command-hook-utils.d.ts +5 -0
  11. package/dist/hooks/foreground-fallback/index.d.ts +40 -28
  12. package/dist/hooks/image-hook.d.ts +1 -0
  13. package/dist/hooks/index.d.ts +1 -1
  14. package/dist/index.js +1871 -326
  15. package/dist/interview/document.d.ts +2 -1
  16. package/dist/interview/ui.d.ts +0 -1
  17. package/dist/multiplexer/cmux/close-policy.d.ts +20 -0
  18. package/dist/multiplexer/cmux/index.d.ts +102 -0
  19. package/dist/multiplexer/cmux/session-lifecycle.d.ts +92 -0
  20. package/dist/multiplexer/cmux/session-state.d.ts +45 -0
  21. package/dist/multiplexer/factory.d.ts +0 -9
  22. package/dist/multiplexer/herdr/index.d.ts +2 -0
  23. package/dist/multiplexer/index.d.ts +3 -1
  24. package/dist/multiplexer/session-manager.d.ts +10 -2
  25. package/dist/multiplexer/shared.d.ts +8 -1
  26. package/dist/multiplexer/types.d.ts +5 -2
  27. package/dist/tools/smartfetch/utils.d.ts +3 -1
  28. package/dist/tui-state.d.ts +5 -5
  29. package/dist/tui.js +86 -20
  30. package/dist/utils/escape-html.d.ts +1 -0
  31. package/dist/utils/frontmatter.d.ts +6 -0
  32. package/dist/utils/index.d.ts +1 -1
  33. package/dist/utils/logger.d.ts +0 -2
  34. package/oh-my-opencode-slim.schema.json +717 -2
  35. package/package.json +1 -1
  36. package/src/skills/clonedeps/SKILL.md +32 -29
  37. package/src/skills/codemap.md +7 -3
  38. package/src/skills/deepwork/SKILL.md +17 -6
  39. package/src/skills/verification-planning/SKILL.md +103 -0
  40. package/src/skills/worktrees/SKILL.md +14 -7
package/dist/index.js CHANGED
@@ -184,7 +184,7 @@ var init_zip_extractor = __esm(() => {
184
184
  init_compat();
185
185
  });
186
186
 
187
- // node_modules/@mozilla/readability/Readability.js
187
+ // node_modules/.pnpm/@mozilla+readability@0.6.0/node_modules/@mozilla/readability/Readability.js
188
188
  var require_Readability = __commonJS((exports, module) => {
189
189
  function Readability(doc, options) {
190
190
  if (options && options.documentElement) {
@@ -1755,7 +1755,7 @@ var require_Readability = __commonJS((exports, module) => {
1755
1755
  }
1756
1756
  });
1757
1757
 
1758
- // node_modules/@mozilla/readability/Readability-readerable.js
1758
+ // node_modules/.pnpm/@mozilla+readability@0.6.0/node_modules/@mozilla/readability/Readability-readerable.js
1759
1759
  var require_Readability_readerable = __commonJS((exports, module) => {
1760
1760
  var REGEXPS = {
1761
1761
  unlikelyCandidates: /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,
@@ -1811,7 +1811,7 @@ var require_Readability_readerable = __commonJS((exports, module) => {
1811
1811
  }
1812
1812
  });
1813
1813
 
1814
- // node_modules/@mozilla/readability/index.js
1814
+ // node_modules/.pnpm/@mozilla+readability@0.6.0/node_modules/@mozilla/readability/index.js
1815
1815
  var require_readability = __commonJS((exports, module) => {
1816
1816
  var Readability = require_Readability();
1817
1817
  var isProbablyReaderable = require_Readability_readerable();
@@ -1821,7 +1821,7 @@ var require_readability = __commonJS((exports, module) => {
1821
1821
  };
1822
1822
  });
1823
1823
 
1824
- // node_modules/@mixmark-io/domino/lib/Event.js
1824
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/Event.js
1825
1825
  var require_Event = __commonJS((exports, module) => {
1826
1826
  module.exports = Event;
1827
1827
  Event.CAPTURING_PHASE = 1;
@@ -1878,7 +1878,7 @@ var require_Event = __commonJS((exports, module) => {
1878
1878
  });
1879
1879
  });
1880
1880
 
1881
- // node_modules/@mixmark-io/domino/lib/UIEvent.js
1881
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/UIEvent.js
1882
1882
  var require_UIEvent = __commonJS((exports, module) => {
1883
1883
  var Event = require_Event();
1884
1884
  module.exports = UIEvent;
@@ -1897,7 +1897,7 @@ var require_UIEvent = __commonJS((exports, module) => {
1897
1897
  });
1898
1898
  });
1899
1899
 
1900
- // node_modules/@mixmark-io/domino/lib/MouseEvent.js
1900
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/MouseEvent.js
1901
1901
  var require_MouseEvent = __commonJS((exports, module) => {
1902
1902
  var UIEvent = require_UIEvent();
1903
1903
  module.exports = MouseEvent;
@@ -1955,7 +1955,7 @@ var require_MouseEvent = __commonJS((exports, module) => {
1955
1955
  });
1956
1956
  });
1957
1957
 
1958
- // node_modules/@mixmark-io/domino/lib/DOMException.js
1958
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/DOMException.js
1959
1959
  var require_DOMException = __commonJS((exports, module) => {
1960
1960
  module.exports = DOMException;
1961
1961
  var INDEX_SIZE_ERR = 1;
@@ -2079,12 +2079,12 @@ var require_DOMException = __commonJS((exports, module) => {
2079
2079
  var c;
2080
2080
  });
2081
2081
 
2082
- // node_modules/@mixmark-io/domino/lib/config.js
2082
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/config.js
2083
2083
  var require_config = __commonJS((exports) => {
2084
2084
  exports.isApiWritable = !globalThis.__domino_frozen__;
2085
2085
  });
2086
2086
 
2087
- // node_modules/@mixmark-io/domino/lib/utils.js
2087
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/utils.js
2088
2088
  var require_utils = __commonJS((exports) => {
2089
2089
  var DOMException = require_DOMException();
2090
2090
  var ERR = DOMException;
@@ -2197,7 +2197,7 @@ var require_utils = __commonJS((exports) => {
2197
2197
  };
2198
2198
  });
2199
2199
 
2200
- // node_modules/@mixmark-io/domino/lib/EventTarget.js
2200
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/EventTarget.js
2201
2201
  var require_EventTarget = __commonJS((exports, module) => {
2202
2202
  var Event = require_Event();
2203
2203
  var MouseEvent = require_MouseEvent();
@@ -2385,7 +2385,7 @@ var require_EventTarget = __commonJS((exports, module) => {
2385
2385
  };
2386
2386
  });
2387
2387
 
2388
- // node_modules/@mixmark-io/domino/lib/LinkedList.js
2388
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/LinkedList.js
2389
2389
  var require_LinkedList = __commonJS((exports, module) => {
2390
2390
  var utils = require_utils();
2391
2391
  var LinkedList = module.exports = {
@@ -2428,7 +2428,7 @@ var require_LinkedList = __commonJS((exports, module) => {
2428
2428
  };
2429
2429
  });
2430
2430
 
2431
- // node_modules/@mixmark-io/domino/lib/NodeUtils.js
2431
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/NodeUtils.js
2432
2432
  var require_NodeUtils = __commonJS((exports, module) => {
2433
2433
  module.exports = {
2434
2434
  serializeOne,
@@ -2604,7 +2604,7 @@ var require_NodeUtils = __commonJS((exports, module) => {
2604
2604
  }
2605
2605
  });
2606
2606
 
2607
- // node_modules/@mixmark-io/domino/lib/Node.js
2607
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/Node.js
2608
2608
  var require_Node = __commonJS((exports, module) => {
2609
2609
  module.exports = Node;
2610
2610
  var EventTarget = require_EventTarget();
@@ -3165,7 +3165,7 @@ var require_Node = __commonJS((exports, module) => {
3165
3165
  });
3166
3166
  });
3167
3167
 
3168
- // node_modules/@mixmark-io/domino/lib/NodeList.es6.js
3168
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/NodeList.es6.js
3169
3169
  var require_NodeList_es6 = __commonJS((exports, module) => {
3170
3170
  module.exports = class NodeList extends Array {
3171
3171
  constructor(a) {
@@ -3182,7 +3182,7 @@ var require_NodeList_es6 = __commonJS((exports, module) => {
3182
3182
  };
3183
3183
  });
3184
3184
 
3185
- // node_modules/@mixmark-io/domino/lib/NodeList.es5.js
3185
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/NodeList.es5.js
3186
3186
  var require_NodeList_es5 = __commonJS((exports, module) => {
3187
3187
  function item(i) {
3188
3188
  return this[i] || null;
@@ -3196,7 +3196,7 @@ var require_NodeList_es5 = __commonJS((exports, module) => {
3196
3196
  module.exports = NodeList;
3197
3197
  });
3198
3198
 
3199
- // node_modules/@mixmark-io/domino/lib/NodeList.js
3199
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/NodeList.js
3200
3200
  var require_NodeList = __commonJS((exports, module) => {
3201
3201
  var NodeList;
3202
3202
  try {
@@ -3207,7 +3207,7 @@ var require_NodeList = __commonJS((exports, module) => {
3207
3207
  module.exports = NodeList;
3208
3208
  });
3209
3209
 
3210
- // node_modules/@mixmark-io/domino/lib/ContainerNode.js
3210
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/ContainerNode.js
3211
3211
  var require_ContainerNode = __commonJS((exports, module) => {
3212
3212
  module.exports = ContainerNode;
3213
3213
  var Node = require_Node();
@@ -3275,7 +3275,7 @@ var require_ContainerNode = __commonJS((exports, module) => {
3275
3275
  });
3276
3276
  });
3277
3277
 
3278
- // node_modules/@mixmark-io/domino/lib/xmlnames.js
3278
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/xmlnames.js
3279
3279
  var require_xmlnames = __commonJS((exports) => {
3280
3280
  exports.isValidName = isValidName;
3281
3281
  exports.isValidQName = isValidQName;
@@ -3324,7 +3324,7 @@ var require_xmlnames = __commonJS((exports) => {
3324
3324
  }
3325
3325
  });
3326
3326
 
3327
- // node_modules/@mixmark-io/domino/lib/attributes.js
3327
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/attributes.js
3328
3328
  var require_attributes = __commonJS((exports) => {
3329
3329
  var utils = require_utils();
3330
3330
  exports.property = function(attr) {
@@ -3460,7 +3460,7 @@ var require_attributes = __commonJS((exports) => {
3460
3460
  };
3461
3461
  });
3462
3462
 
3463
- // node_modules/@mixmark-io/domino/lib/FilteredElementList.js
3463
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/FilteredElementList.js
3464
3464
  var require_FilteredElementList = __commonJS((exports, module) => {
3465
3465
  module.exports = FilteredElementList;
3466
3466
  var Node = require_Node();
@@ -3526,7 +3526,7 @@ var require_FilteredElementList = __commonJS((exports, module) => {
3526
3526
  });
3527
3527
  });
3528
3528
 
3529
- // node_modules/@mixmark-io/domino/lib/DOMTokenList.js
3529
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/DOMTokenList.js
3530
3530
  var require_DOMTokenList = __commonJS((exports, module) => {
3531
3531
  var utils = require_utils();
3532
3532
  module.exports = DOMTokenList;
@@ -3688,7 +3688,7 @@ var require_DOMTokenList = __commonJS((exports, module) => {
3688
3688
  }
3689
3689
  });
3690
3690
 
3691
- // node_modules/@mixmark-io/domino/lib/select.js
3691
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/select.js
3692
3692
  var require_select = __commonJS((exports, module) => {
3693
3693
  var window2 = Object.create(null, {
3694
3694
  location: { get: function() {
@@ -4431,7 +4431,7 @@ var require_select = __commonJS((exports, module) => {
4431
4431
  };
4432
4432
  });
4433
4433
 
4434
- // node_modules/@mixmark-io/domino/lib/ChildNode.js
4434
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/ChildNode.js
4435
4435
  var require_ChildNode = __commonJS((exports, module) => {
4436
4436
  var Node = require_Node();
4437
4437
  var LinkedList = require_LinkedList();
@@ -4521,7 +4521,7 @@ var require_ChildNode = __commonJS((exports, module) => {
4521
4521
  module.exports = ChildNode;
4522
4522
  });
4523
4523
 
4524
- // node_modules/@mixmark-io/domino/lib/NonDocumentTypeChildNode.js
4524
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/NonDocumentTypeChildNode.js
4525
4525
  var require_NonDocumentTypeChildNode = __commonJS((exports, module) => {
4526
4526
  var Node = require_Node();
4527
4527
  var NonDocumentTypeChildNode = {
@@ -4547,7 +4547,7 @@ var require_NonDocumentTypeChildNode = __commonJS((exports, module) => {
4547
4547
  module.exports = NonDocumentTypeChildNode;
4548
4548
  });
4549
4549
 
4550
- // node_modules/@mixmark-io/domino/lib/NamedNodeMap.js
4550
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/NamedNodeMap.js
4551
4551
  var require_NamedNodeMap = __commonJS((exports, module) => {
4552
4552
  module.exports = NamedNodeMap;
4553
4553
  var utils = require_utils();
@@ -4584,7 +4584,7 @@ var require_NamedNodeMap = __commonJS((exports, module) => {
4584
4584
  });
4585
4585
  });
4586
4586
 
4587
- // node_modules/@mixmark-io/domino/lib/Element.js
4587
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/Element.js
4588
4588
  var require_Element = __commonJS((exports, module) => {
4589
4589
  module.exports = Element;
4590
4590
  var xml = require_xmlnames();
@@ -5485,7 +5485,7 @@ var require_Element = __commonJS((exports, module) => {
5485
5485
  }
5486
5486
  });
5487
5487
 
5488
- // node_modules/@mixmark-io/domino/lib/Leaf.js
5488
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/Leaf.js
5489
5489
  var require_Leaf = __commonJS((exports, module) => {
5490
5490
  module.exports = Leaf;
5491
5491
  var Node = require_Node();
@@ -5526,7 +5526,7 @@ var require_Leaf = __commonJS((exports, module) => {
5526
5526
  });
5527
5527
  });
5528
5528
 
5529
- // node_modules/@mixmark-io/domino/lib/CharacterData.js
5529
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/CharacterData.js
5530
5530
  var require_CharacterData = __commonJS((exports, module) => {
5531
5531
  module.exports = CharacterData;
5532
5532
  var Leaf = require_Leaf();
@@ -5583,7 +5583,7 @@ var require_CharacterData = __commonJS((exports, module) => {
5583
5583
  Object.defineProperties(CharacterData.prototype, NonDocumentTypeChildNode);
5584
5584
  });
5585
5585
 
5586
- // node_modules/@mixmark-io/domino/lib/Text.js
5586
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/Text.js
5587
5587
  var require_Text = __commonJS((exports, module) => {
5588
5588
  module.exports = Text;
5589
5589
  var utils = require_utils();
@@ -5653,7 +5653,7 @@ var require_Text = __commonJS((exports, module) => {
5653
5653
  });
5654
5654
  });
5655
5655
 
5656
- // node_modules/@mixmark-io/domino/lib/Comment.js
5656
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/Comment.js
5657
5657
  var require_Comment = __commonJS((exports, module) => {
5658
5658
  module.exports = Comment;
5659
5659
  var Node = require_Node();
@@ -5696,7 +5696,7 @@ var require_Comment = __commonJS((exports, module) => {
5696
5696
  });
5697
5697
  });
5698
5698
 
5699
- // node_modules/@mixmark-io/domino/lib/DocumentFragment.js
5699
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/DocumentFragment.js
5700
5700
  var require_DocumentFragment = __commonJS((exports, module) => {
5701
5701
  module.exports = DocumentFragment;
5702
5702
  var Node = require_Node();
@@ -5753,7 +5753,7 @@ var require_DocumentFragment = __commonJS((exports, module) => {
5753
5753
  });
5754
5754
  });
5755
5755
 
5756
- // node_modules/@mixmark-io/domino/lib/ProcessingInstruction.js
5756
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/ProcessingInstruction.js
5757
5757
  var require_ProcessingInstruction = __commonJS((exports, module) => {
5758
5758
  module.exports = ProcessingInstruction;
5759
5759
  var Node = require_Node();
@@ -5802,7 +5802,7 @@ var require_ProcessingInstruction = __commonJS((exports, module) => {
5802
5802
  });
5803
5803
  });
5804
5804
 
5805
- // node_modules/@mixmark-io/domino/lib/NodeFilter.js
5805
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/NodeFilter.js
5806
5806
  var require_NodeFilter = __commonJS((exports, module) => {
5807
5807
  var NodeFilter = {
5808
5808
  FILTER_ACCEPT: 1,
@@ -5825,7 +5825,7 @@ var require_NodeFilter = __commonJS((exports, module) => {
5825
5825
  module.exports = NodeFilter.constructor = NodeFilter.prototype = NodeFilter;
5826
5826
  });
5827
5827
 
5828
- // node_modules/@mixmark-io/domino/lib/NodeTraversal.js
5828
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/NodeTraversal.js
5829
5829
  var require_NodeTraversal = __commonJS((exports, module) => {
5830
5830
  var NodeTraversal = module.exports = {
5831
5831
  nextSkippingChildren,
@@ -5889,7 +5889,7 @@ var require_NodeTraversal = __commonJS((exports, module) => {
5889
5889
  }
5890
5890
  });
5891
5891
 
5892
- // node_modules/@mixmark-io/domino/lib/TreeWalker.js
5892
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/TreeWalker.js
5893
5893
  var require_TreeWalker = __commonJS((exports, module) => {
5894
5894
  module.exports = TreeWalker;
5895
5895
  var Node = require_Node();
@@ -6119,7 +6119,7 @@ var require_TreeWalker = __commonJS((exports, module) => {
6119
6119
  });
6120
6120
  });
6121
6121
 
6122
- // node_modules/@mixmark-io/domino/lib/NodeIterator.js
6122
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/NodeIterator.js
6123
6123
  var require_NodeIterator = __commonJS((exports, module) => {
6124
6124
  module.exports = NodeIterator;
6125
6125
  var NodeFilter = require_NodeFilter();
@@ -6260,7 +6260,7 @@ var require_NodeIterator = __commonJS((exports, module) => {
6260
6260
  });
6261
6261
  });
6262
6262
 
6263
- // node_modules/@mixmark-io/domino/lib/URL.js
6263
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/URL.js
6264
6264
  var require_URL = __commonJS((exports, module) => {
6265
6265
  module.exports = URL4;
6266
6266
  function URL4(url) {
@@ -6433,7 +6433,7 @@ var require_URL = __commonJS((exports, module) => {
6433
6433
  };
6434
6434
  });
6435
6435
 
6436
- // node_modules/@mixmark-io/domino/lib/CustomEvent.js
6436
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/CustomEvent.js
6437
6437
  var require_CustomEvent = __commonJS((exports, module) => {
6438
6438
  module.exports = CustomEvent;
6439
6439
  var Event = require_Event();
@@ -6445,7 +6445,7 @@ var require_CustomEvent = __commonJS((exports, module) => {
6445
6445
  });
6446
6446
  });
6447
6447
 
6448
- // node_modules/@mixmark-io/domino/lib/events.js
6448
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/events.js
6449
6449
  var require_events = __commonJS((exports, module) => {
6450
6450
  module.exports = {
6451
6451
  Event: require_Event(),
@@ -6455,7 +6455,7 @@ var require_events = __commonJS((exports, module) => {
6455
6455
  };
6456
6456
  });
6457
6457
 
6458
- // node_modules/@mixmark-io/domino/lib/style_parser.js
6458
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/style_parser.js
6459
6459
  var require_style_parser = __commonJS((exports) => {
6460
6460
  Object.defineProperty(exports, "__esModule", { value: true });
6461
6461
  exports.hyphenate = exports.parse = undefined;
@@ -6522,7 +6522,7 @@ var require_style_parser = __commonJS((exports) => {
6522
6522
  exports.hyphenate = hyphenate;
6523
6523
  });
6524
6524
 
6525
- // node_modules/@mixmark-io/domino/lib/CSSStyleDeclaration.js
6525
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/CSSStyleDeclaration.js
6526
6526
  var require_CSSStyleDeclaration = __commonJS((exports, module) => {
6527
6527
  var { parse } = require_style_parser();
6528
6528
  module.exports = function(elt) {
@@ -6698,7 +6698,7 @@ var require_CSSStyleDeclaration = __commonJS((exports, module) => {
6698
6698
  });
6699
6699
  });
6700
6700
 
6701
- // node_modules/@mixmark-io/domino/lib/URLUtils.js
6701
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/URLUtils.js
6702
6702
  var require_URLUtils = __commonJS((exports, module) => {
6703
6703
  var URL4 = require_URL();
6704
6704
  module.exports = URLUtils;
@@ -6932,7 +6932,7 @@ var require_URLUtils = __commonJS((exports, module) => {
6932
6932
  };
6933
6933
  });
6934
6934
 
6935
- // node_modules/@mixmark-io/domino/lib/defineElement.js
6935
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/defineElement.js
6936
6936
  var require_defineElement = __commonJS((exports, module) => {
6937
6937
  var attributes = require_attributes();
6938
6938
  var isApiWritable = require_config().isApiWritable;
@@ -6994,7 +6994,7 @@ var require_defineElement = __commonJS((exports, module) => {
6994
6994
  }
6995
6995
  });
6996
6996
 
6997
- // node_modules/@mixmark-io/domino/lib/htmlelts.js
6997
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/htmlelts.js
6998
6998
  var require_htmlelts = __commonJS((exports) => {
6999
6999
  var Node = require_Node();
7000
7000
  var Element = require_Element();
@@ -8458,7 +8458,7 @@ var require_htmlelts = __commonJS((exports) => {
8458
8458
  });
8459
8459
  });
8460
8460
 
8461
- // node_modules/@mixmark-io/domino/lib/svg.js
8461
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/svg.js
8462
8462
  var require_svg = __commonJS((exports) => {
8463
8463
  var Element = require_Element();
8464
8464
  var defineElement = require_defineElement();
@@ -8584,7 +8584,7 @@ var require_svg = __commonJS((exports) => {
8584
8584
  });
8585
8585
  });
8586
8586
 
8587
- // node_modules/@mixmark-io/domino/lib/MutationConstants.js
8587
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/MutationConstants.js
8588
8588
  var require_MutationConstants = __commonJS((exports, module) => {
8589
8589
  module.exports = {
8590
8590
  VALUE: 1,
@@ -8596,7 +8596,7 @@ var require_MutationConstants = __commonJS((exports, module) => {
8596
8596
  };
8597
8597
  });
8598
8598
 
8599
- // node_modules/@mixmark-io/domino/lib/Document.js
8599
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/Document.js
8600
8600
  var require_Document = __commonJS((exports, module) => {
8601
8601
  module.exports = Document;
8602
8602
  var Node = require_Node();
@@ -9308,7 +9308,7 @@ var require_Document = __commonJS((exports, module) => {
9308
9308
  };
9309
9309
  });
9310
9310
 
9311
- // node_modules/@mixmark-io/domino/lib/DocumentType.js
9311
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/DocumentType.js
9312
9312
  var require_DocumentType = __commonJS((exports, module) => {
9313
9313
  module.exports = DocumentType;
9314
9314
  var Node = require_Node();
@@ -9342,7 +9342,7 @@ var require_DocumentType = __commonJS((exports, module) => {
9342
9342
  Object.defineProperties(DocumentType.prototype, ChildNode);
9343
9343
  });
9344
9344
 
9345
- // node_modules/@mixmark-io/domino/lib/HTMLParser.js
9345
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/HTMLParser.js
9346
9346
  var require_HTMLParser = __commonJS((exports, module) => {
9347
9347
  module.exports = HTMLParser;
9348
9348
  var Document = require_Document();
@@ -17393,7 +17393,7 @@ var require_HTMLParser = __commonJS((exports, module) => {
17393
17393
  }
17394
17394
  });
17395
17395
 
17396
- // node_modules/@mixmark-io/domino/lib/DOMImplementation.js
17396
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/DOMImplementation.js
17397
17397
  var require_DOMImplementation = __commonJS((exports, module) => {
17398
17398
  module.exports = DOMImplementation;
17399
17399
  var Document = require_Document();
@@ -17467,7 +17467,7 @@ var require_DOMImplementation = __commonJS((exports, module) => {
17467
17467
  };
17468
17468
  });
17469
17469
 
17470
- // node_modules/@mixmark-io/domino/lib/Location.js
17470
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/Location.js
17471
17471
  var require_Location = __commonJS((exports, module) => {
17472
17472
  var URL4 = require_URL();
17473
17473
  var URLUtils = require_URLUtils();
@@ -17503,7 +17503,7 @@ var require_Location = __commonJS((exports, module) => {
17503
17503
  });
17504
17504
  });
17505
17505
 
17506
- // node_modules/@mixmark-io/domino/lib/NavigatorID.js
17506
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/NavigatorID.js
17507
17507
  var require_NavigatorID = __commonJS((exports, module) => {
17508
17508
  var NavigatorID = Object.create(null, {
17509
17509
  appCodeName: { value: "Mozilla" },
@@ -17522,7 +17522,7 @@ var require_NavigatorID = __commonJS((exports, module) => {
17522
17522
  module.exports = NavigatorID;
17523
17523
  });
17524
17524
 
17525
- // node_modules/@mixmark-io/domino/lib/WindowTimers.js
17525
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/WindowTimers.js
17526
17526
  var require_WindowTimers = __commonJS((exports, module) => {
17527
17527
  var WindowTimers = {
17528
17528
  setTimeout,
@@ -17533,7 +17533,7 @@ var require_WindowTimers = __commonJS((exports, module) => {
17533
17533
  module.exports = WindowTimers;
17534
17534
  });
17535
17535
 
17536
- // node_modules/@mixmark-io/domino/lib/impl.js
17536
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/impl.js
17537
17537
  var require_impl = __commonJS((exports, module) => {
17538
17538
  var utils = require_utils();
17539
17539
  exports = module.exports = {
@@ -17561,7 +17561,7 @@ var require_impl = __commonJS((exports, module) => {
17561
17561
  utils.merge(exports, require_svg().elements);
17562
17562
  });
17563
17563
 
17564
- // node_modules/@mixmark-io/domino/lib/Window.js
17564
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/Window.js
17565
17565
  var require_Window = __commonJS((exports, module) => {
17566
17566
  var DOMImplementation = require_DOMImplementation();
17567
17567
  var EventTarget = require_EventTarget();
@@ -17616,7 +17616,7 @@ var require_Window = __commonJS((exports, module) => {
17616
17616
  utils.expose(require_impl(), Window);
17617
17617
  });
17618
17618
 
17619
- // node_modules/@mixmark-io/domino/lib/index.js
17619
+ // node_modules/.pnpm/@mixmark-io+domino@2.2.0/node_modules/@mixmark-io/domino/lib/index.js
17620
17620
  var require_lib = __commonJS((exports) => {
17621
17621
  var DOMImplementation = require_DOMImplementation();
17622
17622
  var HTMLParser = require_HTMLParser();
@@ -17666,7 +17666,7 @@ var require_lib = __commonJS((exports) => {
17666
17666
  exports.impl = impl;
17667
17667
  });
17668
17668
 
17669
- // node_modules/turndown/lib/turndown.cjs.js
17669
+ // node_modules/.pnpm/turndown@7.2.4/node_modules/turndown/lib/turndown.cjs.js
17670
17670
  var require_turndown_cjs = __commonJS((exports, module) => {
17671
17671
  function extend(destination) {
17672
17672
  for (var i = 1;i < arguments.length; i++) {
@@ -18363,6 +18363,12 @@ var CUSTOM_SKILLS = [
18363
18363
  allowedAgents: ["orchestrator"],
18364
18364
  sourcePath: "src/skills/deepwork"
18365
18365
  },
18366
+ {
18367
+ name: "verification-planning",
18368
+ description: "Plan credible, proportionate evidence before non-trivial implementation",
18369
+ allowedAgents: ["orchestrator"],
18370
+ sourcePath: "src/skills/verification-planning"
18371
+ },
18366
18372
  {
18367
18373
  name: "reflect",
18368
18374
  description: "Review repeated work and suggest reusable workflow improvements",
@@ -18521,6 +18527,13 @@ var NO_SHELL_READONLY_FILE_OPERATIONS_RULES = `**File Operations Rules**:
18521
18527
  var TMUX_SPAWN_DELAY_MS = 500;
18522
18528
  var COUNCILLOR_STAGGER_MS = 250;
18523
18529
  var DEFAULT_DISABLED_AGENTS = ["observer"];
18530
+ var DEFAULT_MAX_SESSIONS_PER_AGENT = 2;
18531
+ var DEFAULT_READ_CONTEXT_MIN_LINES = 10;
18532
+ var DEFAULT_READ_CONTEXT_MAX_FILES = 8;
18533
+ var DEFAULT_IMAGE_ROUTING = "auto";
18534
+ function resolveImageRouting(imageRouting) {
18535
+ return imageRouting ?? DEFAULT_IMAGE_ROUTING;
18536
+ }
18524
18537
  // src/config/council-schema.ts
18525
18538
  import { z } from "zod";
18526
18539
 
@@ -18686,6 +18699,33 @@ var ManualPlanSchema = z2.object({
18686
18699
  librarian: ManualAgentPlanSchema,
18687
18700
  fixer: ManualAgentPlanSchema
18688
18701
  }).strict();
18702
+ var PermissionActionSchema = z2.enum(["ask", "allow", "deny"]);
18703
+ var PermissionRuleSchema = z2.union([
18704
+ PermissionActionSchema,
18705
+ z2.record(z2.string(), PermissionActionSchema)
18706
+ ]);
18707
+ var PermissionObjectSchema = z2.object({
18708
+ read: PermissionRuleSchema.optional(),
18709
+ edit: PermissionRuleSchema.optional(),
18710
+ glob: PermissionRuleSchema.optional(),
18711
+ grep: PermissionRuleSchema.optional(),
18712
+ list: PermissionRuleSchema.optional(),
18713
+ bash: PermissionRuleSchema.optional(),
18714
+ task: PermissionRuleSchema.optional(),
18715
+ external_directory: PermissionRuleSchema.optional(),
18716
+ lsp: PermissionRuleSchema.optional(),
18717
+ skill: PermissionRuleSchema.optional(),
18718
+ todowrite: PermissionActionSchema.optional(),
18719
+ question: PermissionActionSchema.optional(),
18720
+ webfetch: PermissionActionSchema.optional(),
18721
+ websearch: PermissionActionSchema.optional(),
18722
+ codesearch: PermissionActionSchema.optional(),
18723
+ doom_loop: PermissionActionSchema.optional()
18724
+ }).catchall(PermissionRuleSchema);
18725
+ var PermissionConfigSchema = z2.union([
18726
+ PermissionActionSchema,
18727
+ PermissionObjectSchema
18728
+ ]);
18689
18729
  var AgentOverrideConfigSchema = z2.object({
18690
18730
  model: z2.union([
18691
18731
  z2.string(),
@@ -18704,13 +18744,15 @@ var AgentOverrideConfigSchema = z2.object({
18704
18744
  prompt: z2.string().min(1).optional(),
18705
18745
  orchestratorPrompt: z2.string().min(1).optional(),
18706
18746
  options: z2.record(z2.string(), z2.unknown()).optional(),
18707
- displayName: z2.string().min(1).optional()
18747
+ displayName: z2.string().min(1).optional(),
18748
+ permission: PermissionConfigSchema.optional()
18708
18749
  }).strict();
18709
18750
  var MultiplexerTypeSchema = z2.enum([
18710
18751
  "auto",
18711
18752
  "tmux",
18712
18753
  "zellij",
18713
18754
  "herdr",
18755
+ "cmux",
18714
18756
  "none"
18715
18757
  ]);
18716
18758
  var MultiplexerLayoutSchema = z2.enum([
@@ -18756,7 +18798,7 @@ var FailoverConfigSchema = z2.object({
18756
18798
  retryDelayMs: z2.number().min(0).default(500),
18757
18799
  maxRetries: z2.number().int().min(0).default(3).describe("Number of consecutive 429/rate-limit responses tolerated on the " + "same model before aborting (or swapping to the next fallback " + "model when a chain is configured)."),
18758
18800
  retry_on_empty: z2.boolean().default(true).describe("When true (default), empty provider responses are treated as failures, " + "triggering fallback/retry. Set to false to treat them as successes."),
18759
- runtimeOverride: z2.boolean().default(true).describe("When true (default), a runtime model selected via /model that is " + "outside the configured fallback chain will still trigger the chain " + "on rate-limit errors. When false, out-of-chain runtime picks are " + "respected and the error surfaces instead of silently falling back " + "to the chain. Models that are members of the chain always fall back " + "regardless of this setting.")
18801
+ runtimeOverride: z2.boolean().optional().describe("DEPRECATED: no longer used. Previously controlled whether out-of-chain " + "runtime model picks triggered fallback. Fallback is now always " + "disabled when a user explicitly selects a model via /model.")
18760
18802
  }).strict();
18761
18803
  var CompanionConfigSchema = z2.object({
18762
18804
  enabled: z2.boolean().optional(),
@@ -18798,11 +18840,13 @@ var PluginConfigSchema = z2.object({
18798
18840
  preset: z2.string().optional(),
18799
18841
  setDefaultAgent: z2.boolean().optional(),
18800
18842
  compactSidebar: z2.boolean().optional().describe("Use the compact TUI sidebar layout. Defaults to true; set false to use the expanded layout."),
18843
+ stripOrchestratorModel: z2.boolean().optional().describe("When true, omit orchestrator.model and orchestrator.variant from the SDK config so OpenCode uses the session model selected with /model after subagent dispatch. An explicitly selected preset that sets orchestrator.model is preserved. Defaults to false."),
18801
18844
  autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
18802
18845
  presets: z2.record(z2.string(), PresetSchema).optional(),
18803
18846
  agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
18804
18847
  disabled_agents: z2.array(z2.string()).optional().describe("Agent names to disable completely. " + "Disabled agents are not instantiated and cannot be delegated to. " + "Orchestrator and council internal agents (councillor) cannot be disabled. " + "By default, 'observer' is disabled. Remove it from this list and configure a vision-capable model to enable."),
18805
- disabled_mcps: z2.array(z2.string()).optional(),
18848
+ image_routing: z2.enum(["auto", "direct"]).optional().describe("How image attachments are handled. " + "When omitted, preserves legacy conditional behavior: intercept " + 'attachments only when observer is enabled. "auto": requires ' + "observer to be enabled and saves attachments to disk before " + 'nudging delegation to @observer. "direct": always passes ' + "attachments to the orchestrator untouched."),
18849
+ disabled_mcps: z2.array(z2.string()).optional().describe("MCP server names to disable completely. Disabled servers are not " + "started and cannot be used by agents."),
18806
18850
  disabled_tools: z2.array(z2.string()).optional().describe("Tool names to disable completely. Disabled tools are not registered with OpenCode and cannot be used by agents."),
18807
18851
  disabled_skills: z2.array(z2.string()).optional().describe("Skill names to disable completely. Disabled skills are not granted to agents, even when referenced by presets or agent overrides."),
18808
18852
  multiplexer: MultiplexerConfigSchema.optional(),
@@ -18899,6 +18943,23 @@ function findConfigPathInDirs(configDirs, baseName) {
18899
18943
  }
18900
18944
  return null;
18901
18945
  }
18946
+ function validateFinalImageRouting(config, configPath, options) {
18947
+ if (config.image_routing !== "auto")
18948
+ return true;
18949
+ const disabledAgents = config.disabled_agents ?? DEFAULT_DISABLED_AGENTS;
18950
+ if (!disabledAgents.includes("observer"))
18951
+ return true;
18952
+ const message = 'image_routing "auto" requires observer to be enabled. ' + 'Remove "observer" from disabled_agents.';
18953
+ options?.onWarning?.({
18954
+ path: configPath,
18955
+ kind: "invalid-schema",
18956
+ message
18957
+ });
18958
+ if (!options?.silent) {
18959
+ console.warn(`[oh-my-opencode-slim] Invalid config: ${message}`);
18960
+ }
18961
+ return false;
18962
+ }
18902
18963
  function findPluginConfigPaths(directory) {
18903
18964
  const userConfigPath = findConfigPathInDirs(getConfigSearchDirs(), "oh-my-opencode-slim");
18904
18965
  const projectConfigBasePath = path.join(directory, ".opencode", "oh-my-opencode-slim");
@@ -18980,6 +19041,7 @@ function loadPluginConfig(directory, options) {
18980
19041
  debug: config.companion.debug ?? false
18981
19042
  };
18982
19043
  }
19044
+ validateFinalImageRouting(config, projectConfigPath ?? userConfigPath ?? "", options);
18983
19045
  return config;
18984
19046
  }
18985
19047
  function loadAgentPrompt(agentName, optionsOrPreset) {
@@ -19241,13 +19303,6 @@ var AGENT_DESCRIPTIONS = {
19241
19303
  - **Rule of thumb:** Even if your model supports vision, delegate visual analysis to @observer - it isolates large image/PDF bytes from your context window, returning only concise structured text. Need exact file contents for routing? → Read only the minimal context yourself.
19242
19304
  - **IMPORTANT:** When delegating to @observer, always include the **full file path** in the prompt so it can read the file. Example: "Analyze the screenshot at /path/to/file.png - describe the UI elements and error messages."`
19243
19305
  };
19244
- var VALIDATION_ROUTING = [
19245
- "- Route UI/UX validation and review to @designer",
19246
- "- Route code review, code simplification and maintainability review checks to @oracle",
19247
- "- Route implementation to @fixer or multiple @fixer instances for maximum parallel execution",
19248
- "- Route visual/media analysis and interpretation to @observer",
19249
- "- If a request spans multiple lanes, delegate only the lanes that add clear value"
19250
- ];
19251
19306
  var PARALLEL_DELEGATION_EXAMPLES = [
19252
19307
  "- Multiple @explorer searches across different domains?",
19253
19308
  "- @explorer + @librarian research in parallel?",
@@ -19257,13 +19312,6 @@ var PARALLEL_DELEGATION_EXAMPLES = [
19257
19312
  function buildOrchestratorPrompt(disabledAgents) {
19258
19313
  const enabledAgents = Object.entries(AGENT_DESCRIPTIONS).filter(([name]) => !disabledAgents?.has(name)).map(([, desc]) => desc).join(`
19259
19314
 
19260
- `);
19261
- const enabledValidationRouting = VALIDATION_ROUTING.filter((line) => {
19262
- const mentions = [...line.matchAll(/@(\w+)/g)].map((m) => m[1]);
19263
- if (mentions.length === 0)
19264
- return true;
19265
- return mentions.every((name) => !disabledAgents?.has(name));
19266
- }).join(`
19267
19315
  `);
19268
19316
  const enabledParallelExamples = PARALLEL_DELEGATION_EXAMPLES.filter((line) => {
19269
19317
  const mentions = [...line.matchAll(/@(\w+)/g)].map((m) => m[1]);
@@ -19351,16 +19399,13 @@ Balance: respect dependencies, avoid parallelizing what must be sequential, and
19351
19399
  - If the Background Job Board lists \`fix-1 / ses_abc / fixer\`, call task with \`subagent_type: "fixer"\` and \`task_id: "fix-1"\` or \`task_id: "ses_abc"\`.
19352
19400
  - Do not leave \`task_id\` empty when intending to reuse; omitted or empty \`task_id\` creates a new specialist session.
19353
19401
 
19354
- ### Validation routing
19355
- - Validation is a workflow stage owned by the Orchestrator, not a separate specialist
19356
- ${enabledValidationRouting}
19357
-
19358
19402
  ## 6. Verify
19359
- - Run relevant checks/diagnostics for the change
19360
- - Use validation routing when applicable instead of doing all review work yourself
19361
- - If test files are involved, prefer @fixer for bounded test changes and @oracle only for test strategy or quality review
19362
- - Confirm specialists completed successfully
19363
- - Verify solution meets requirements
19403
+ - Define the observable success criteria from the user's request.
19404
+ - Run the smallest relevant checks: targeted tests, typecheck, lint, build, or a manual behavior check.
19405
+ - Inspect the changed diff and verify behavior; passing checks alone are not sufficient.
19406
+ - If a check fails, diagnose, fix, and rerun the relevant verification.
19407
+ - For risky or ambiguous changes, obtain an independent review when its value justifies the cost.
19408
+ - Report verification performed and any remaining limitations.
19364
19409
 
19365
19410
  </Workflow>
19366
19411
 
@@ -20045,6 +20090,9 @@ function applyOverrides(agent, override) {
20045
20090
  if (override.displayName) {
20046
20091
  agent.displayName = override.displayName;
20047
20092
  }
20093
+ if (override.permission) {
20094
+ agent.config.permission = override.permission;
20095
+ }
20048
20096
  }
20049
20097
  function isKnownAgentName(name) {
20050
20098
  return ALL_AGENT_NAMES.includes(name);
@@ -20085,6 +20133,9 @@ function injectDisplayNames(orchestrator, nameMap) {
20085
20133
  orchestrator.config.prompt = prompt;
20086
20134
  }
20087
20135
  function applyDefaultPermissions(agent, configuredSkills, disabledSkills) {
20136
+ if (typeof agent.config.permission === "string") {
20137
+ return;
20138
+ }
20088
20139
  const existing = agent.config.permission ?? {};
20089
20140
  const skillPermissions = getSkillPermissionsForAgent(agent.name, configuredSkills, disabledSkills);
20090
20141
  const questionPerm = existing.question === "deny" ? "deny" : "allow";
@@ -20231,10 +20282,10 @@ function createAgents(config, options) {
20231
20282
  const defaultOrchestratorPrompt = orchestrator.config.prompt ?? "";
20232
20283
  const baseOrchestratorPrompt = inlineOrchestratorPrompt !== undefined ? inlineOrchestratorPrompt : defaultOrchestratorPrompt;
20233
20284
  orchestrator.config.prompt = resolvePrompt(baseOrchestratorPrompt, orchestratorPrompts.prompt, orchestratorPrompts.appendPrompt);
20234
- applyDefaultPermissions(orchestrator, orchestratorOverride?.skills, config?.disabled_skills);
20235
20285
  if (orchestratorOverride) {
20236
20286
  applyOverrides(orchestrator, orchestratorOverride);
20237
20287
  }
20288
+ applyDefaultPermissions(orchestrator, orchestratorOverride?.skills, config?.disabled_skills);
20238
20289
  const displayNameMap = new Map;
20239
20290
  if (orchestrator.displayName) {
20240
20291
  displayNameMap.set("orchestrator", orchestrator.displayName);
@@ -20488,7 +20539,9 @@ function acquirePidFileLock(file) {
20488
20539
  return () => {
20489
20540
  try {
20490
20541
  rmSync(lock, { recursive: true, force: true });
20491
- } catch {}
20542
+ } catch (err) {
20543
+ log("[companion] lock release failed", String(err));
20544
+ }
20492
20545
  };
20493
20546
  } catch (err) {
20494
20547
  const code = err.code;
@@ -20516,10 +20569,13 @@ function pidFileLockHasLiveOwner(lock) {
20516
20569
  const owner = parsePidFile(readFileSync2(path3.join(lock, "owner"), "utf8"));
20517
20570
  if (owner !== null)
20518
20571
  return isProcessAlive(owner);
20519
- } catch {
20572
+ } catch (err) {
20573
+ log("[companion] lock owner check failed", String(err));
20520
20574
  try {
20521
20575
  return Date.now() - statSync2(lock).mtimeMs < 5000;
20522
- } catch {}
20576
+ } catch (err2) {
20577
+ log("[companion] lock owner check failed", String(err2));
20578
+ }
20523
20579
  }
20524
20580
  return false;
20525
20581
  }
@@ -20541,7 +20597,9 @@ function readState() {
20541
20597
  if (parsed?.version === 1 && Array.isArray(parsed.sessions)) {
20542
20598
  return parsed;
20543
20599
  }
20544
- } catch {}
20600
+ } catch (err) {
20601
+ log("[companion] state load failed", String(err));
20602
+ }
20545
20603
  return { version: 1, sessions: [] };
20546
20604
  }
20547
20605
  function writeState(mutator) {
@@ -20570,7 +20628,9 @@ function acquireStateLock(file) {
20570
20628
  return () => {
20571
20629
  try {
20572
20630
  rmSync(lock, { recursive: true, force: true });
20573
- } catch {}
20631
+ } catch (err) {
20632
+ log("[companion] lock release failed", String(err));
20633
+ }
20574
20634
  };
20575
20635
  } catch (err) {
20576
20636
  const code = err.code;
@@ -20605,7 +20665,9 @@ class CompanionManager {
20605
20665
  writeState((state) => {
20606
20666
  state.sessions = state.sessions.filter((s) => s.session_id !== this.id);
20607
20667
  });
20608
- } catch {}
20668
+ } catch (err) {
20669
+ log("[companion] status update failed", String(err));
20670
+ }
20609
20671
  return;
20610
20672
  }
20611
20673
  this.registerActiveManager();
@@ -20643,9 +20705,7 @@ class CompanionManager {
20643
20705
  return;
20644
20706
  }
20645
20707
  if (status === "busy") {
20646
- if (!agent)
20647
- return;
20648
- this.busyAgentSessions.set(sessionId, agent);
20708
+ this.busyAgentSessions.set(sessionId, agent ?? sessionId);
20649
20709
  } else {
20650
20710
  this.busyAgentSessions.delete(sessionId);
20651
20711
  }
@@ -20677,7 +20737,9 @@ class CompanionManager {
20677
20737
  if (activeManagers.size === 0 && activeExitListener) {
20678
20738
  try {
20679
20739
  process.removeListener("exit", activeExitListener);
20680
- } catch {}
20740
+ } catch (err) {
20741
+ log("[companion] exit listener removal failed", String(err));
20742
+ }
20681
20743
  activeExitListener = null;
20682
20744
  }
20683
20745
  if (this.config?.enabled !== true)
@@ -20835,7 +20897,9 @@ class CompanionManager {
20835
20897
  if (spawnedChild && !this.wasSpawner) {
20836
20898
  try {
20837
20899
  spawnedChild.kill();
20838
- } catch {}
20900
+ } catch (killErr) {
20901
+ log("[companion] spawn failed", String(killErr));
20902
+ }
20839
20903
  }
20840
20904
  log("[companion] spawn guard failed", String(err));
20841
20905
  } finally {
@@ -20914,7 +20978,9 @@ function loadCompanionManifestFromPackageRoot(packageRoot) {
20914
20978
  checksums: parsed.checksums
20915
20979
  };
20916
20980
  }
20917
- } catch {}
20981
+ } catch (err) {
20982
+ log("[updater] manifest read failed", String(err));
20983
+ }
20918
20984
  return null;
20919
20985
  }
20920
20986
  async function ensureCompanionVersion(options) {
@@ -21082,7 +21148,9 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
21082
21148
  if (tempDir) {
21083
21149
  try {
21084
21150
  rmSync2(tempDir, { recursive: true, force: true });
21085
- } catch {}
21151
+ } catch (err) {
21152
+ log("[updater] install cleanup failed", String(err));
21153
+ }
21086
21154
  }
21087
21155
  }
21088
21156
  }
@@ -21092,7 +21160,9 @@ function readInstallMetadata(binaryPath) {
21092
21160
  if (parsed?.version && parsed.tag && parsed.target) {
21093
21161
  return parsed;
21094
21162
  }
21095
- } catch {}
21163
+ } catch (err) {
21164
+ log("[updater] metadata read failed", String(err));
21165
+ }
21096
21166
  return null;
21097
21167
  }
21098
21168
  function writeInstallMetadata(binaryPath, metadata) {
@@ -21114,7 +21184,9 @@ async function withCompanionInstallLock(binaryPath, timeoutMs, staleMs, run) {
21114
21184
  } finally {
21115
21185
  try {
21116
21186
  rmSync2(lock, { recursive: true, force: true });
21117
- } catch {}
21187
+ } catch (err) {
21188
+ log("[updater] lock release failed", String(err));
21189
+ }
21118
21190
  }
21119
21191
  } catch (err) {
21120
21192
  const code = err.code;
@@ -21185,6 +21257,24 @@ function setActiveRuntimePresetWithPrevious(name) {
21185
21257
  activeRuntimePreset = name;
21186
21258
  }
21187
21259
 
21260
+ // src/config/strip-orchestrator-model.ts
21261
+ function isRecord(value) {
21262
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21263
+ }
21264
+ function stripOrchestratorModel(agents, enabled, preset) {
21265
+ if (enabled !== true || preset?.orchestrator?.model !== undefined)
21266
+ return;
21267
+ const orchestrator = agents.orchestrator;
21268
+ if (!isRecord(orchestrator))
21269
+ return;
21270
+ delete orchestrator.model;
21271
+ delete orchestrator.variant;
21272
+ }
21273
+ function applyOrchestratorModelConfig(input) {
21274
+ const presetName = input.runtimePreset ?? input.configPreset;
21275
+ stripOrchestratorModel(input.agents, input.enabled, presetName ? input.presets?.[presetName] : undefined);
21276
+ }
21277
+
21188
21278
  // src/council/council-manager.ts
21189
21279
  class CouncilManager {
21190
21280
  client;
@@ -24696,9 +24786,9 @@ class BackgroundJobBoard {
24696
24786
  readContextMinLines;
24697
24787
  readContextMaxFiles;
24698
24788
  constructor(options = {}) {
24699
- this.maxReusablePerAgent = options.maxReusablePerAgent ?? 2;
24700
- this.readContextMinLines = options.readContextMinLines ?? 10;
24701
- this.readContextMaxFiles = options.readContextMaxFiles ?? 8;
24789
+ this.maxReusablePerAgent = options.maxReusablePerAgent ?? DEFAULT_MAX_SESSIONS_PER_AGENT;
24790
+ this.readContextMinLines = options.readContextMinLines ?? DEFAULT_READ_CONTEXT_MIN_LINES;
24791
+ this.readContextMaxFiles = options.readContextMaxFiles ?? DEFAULT_READ_CONTEXT_MAX_FILES;
24702
24792
  }
24703
24793
  addTerminalStateListener(listener) {
24704
24794
  this.terminalStateListeners.push(listener);
@@ -25239,7 +25329,7 @@ class BackgroundJobCoordinator {
25239
25329
  }
25240
25330
  }
25241
25331
  // src/utils/guards.ts
25242
- function isRecord(value) {
25332
+ function isRecord2(value) {
25243
25333
  return typeof value === "object" && value !== null;
25244
25334
  }
25245
25335
 
@@ -25256,10 +25346,10 @@ ${SLIM_INTERNAL_INITIATOR_MARKER}`,
25256
25346
  };
25257
25347
  }
25258
25348
  function isInternalInitiatorPart(part) {
25259
- if (!isRecord(part) || part.type !== "text") {
25349
+ if (!isRecord2(part) || part.type !== "text") {
25260
25350
  return false;
25261
25351
  }
25262
- if (part.synthetic !== true || !isRecord(part.metadata)) {
25352
+ if (part.synthetic !== true || !isRecord2(part.metadata)) {
25263
25353
  return false;
25264
25354
  }
25265
25355
  return part.metadata[INTERNAL_INITIATOR_METADATA_KEY] === true;
@@ -25318,6 +25408,17 @@ function createChatHeadersHook(ctx) {
25318
25408
  }
25319
25409
  };
25320
25410
  }
25411
+ // src/hooks/command-hook-utils.ts
25412
+ function registerCommandHook(opencodeConfig, commandName, template, description) {
25413
+ const cmdConfig = opencodeConfig.command;
25414
+ if (cmdConfig?.[commandName])
25415
+ return false;
25416
+ if (!opencodeConfig.command)
25417
+ opencodeConfig.command = {};
25418
+ opencodeConfig.command[commandName] = { template, description };
25419
+ return true;
25420
+ }
25421
+
25321
25422
  // src/hooks/deepwork/index.ts
25322
25423
  var COMMAND_NAME = "deepwork";
25323
25424
  function activationPrompt(task2) {
@@ -25325,6 +25426,7 @@ function activationPrompt(task2) {
25325
25426
  "Use the deepwork skill for this task. Treat it as a heavy coding session.",
25326
25427
  "",
25327
25428
  "Deepwork requirements:",
25429
+ "- before planning, delegation, or creating state, inspect existing `.gitignore` and `.ignore`; add only missing entries without duplicates: `.gitignore` must contain `.slim/deepwork/`, and `.ignore` must contain `!.slim/deepwork/` and `!.slim/deepwork/**`; this keeps state git-local yet OpenCode-readable;",
25328
25430
  "- create/update a `.slim/deepwork/` progress file;",
25329
25431
  "- keep OpenCode todos synced with the current phase;",
25330
25432
  "- draft a plan and get `@oracle` review before implementation;",
@@ -25342,15 +25444,7 @@ function activationPrompt(task2) {
25342
25444
  function createDeepworkCommandHook() {
25343
25445
  return {
25344
25446
  registerCommand: (opencodeConfig) => {
25345
- const commandConfig = opencodeConfig.command;
25346
- if (commandConfig?.[COMMAND_NAME])
25347
- return;
25348
- if (!opencodeConfig.command)
25349
- opencodeConfig.command = {};
25350
- opencodeConfig.command[COMMAND_NAME] = {
25351
- template: "Start a deepwork session for a complex coding task",
25352
- description: "Use the deepwork workflow for heavy multi-phase coding work"
25353
- };
25447
+ registerCommandHook(opencodeConfig, COMMAND_NAME, "Start a deepwork session for a complex coding task", "Use the deepwork workflow for heavy multi-phase coding work");
25354
25448
  },
25355
25449
  handleCommandExecuteBefore: async (input, output) => {
25356
25450
  if (input.command !== COMMAND_NAME)
@@ -25565,7 +25659,7 @@ function createFilterAvailableSkillsHook(_ctx, config) {
25565
25659
  };
25566
25660
  }
25567
25661
  // src/hooks/foreground-fallback/index.ts
25568
- var RATE_LIMIT_PATTERNS = [
25662
+ var RETRYABLE_ERROR_PATTERNS = [
25569
25663
  /\b429\b/,
25570
25664
  /rate.?limit/i,
25571
25665
  /too many requests/i,
@@ -25579,22 +25673,76 @@ var RATE_LIMIT_PATTERNS = [
25579
25673
  /insufficient.?(quota|balance)/i,
25580
25674
  /high concurrency/i,
25581
25675
  /reduce concurrency/i,
25582
- /service unavailable/i,
25583
25676
  /monthly usage limit/i,
25584
25677
  /5-hour usage limit/i,
25585
- /weekly usage limit/i
25678
+ /weekly usage limit/i,
25679
+ /\b403\b/,
25680
+ /forbidden/i,
25681
+ /blocked by gateway/i
25682
+ ];
25683
+ var OUTAGE_STATUS_CODES = new Set([500, 502, 503, 504]);
25684
+ var TRANSPORT_CODES = new Set([
25685
+ "ECONNREFUSED",
25686
+ "ECONNRESET",
25687
+ "ENOTFOUND",
25688
+ "ETIMEDOUT",
25689
+ "EAI_AGAIN"
25690
+ ]);
25691
+ var TRANSPORT_MESSAGE_PATTERNS = [
25692
+ /^fetch failed$/i,
25693
+ /^socket hang up$/i,
25694
+ /^provider request timeout$/i,
25695
+ /^request timeout$/i,
25696
+ /^connect ECONNREFUSED\b/i,
25697
+ /^getaddrinfo ENOTFOUND\b/i
25586
25698
  ];
25587
- function isRateLimitError(error) {
25588
- if (!error || typeof error !== "object")
25699
+ var PROVIDER_OUTAGE_PATTERNS = [
25700
+ /\binternal server error\b/i,
25701
+ /\bbad gateway\b/i,
25702
+ /\bgateway timeout\b/i,
25703
+ /\bservice unavailable\b/i,
25704
+ /\bupstream outage\b/i,
25705
+ /\bprovider outage\b/i,
25706
+ /\bprovider unavailable\b/i
25707
+ ];
25708
+ function extractStatusCode(error) {
25709
+ const value = error.statusCode ?? error.data?.statusCode;
25710
+ return typeof value === "number" ? value : undefined;
25711
+ }
25712
+ function eventSessionID(props) {
25713
+ return props.sessionID ?? props.info?.id;
25714
+ }
25715
+ function isFailoverError(error) {
25716
+ if (!error)
25717
+ return false;
25718
+ if (typeof error === "string") {
25719
+ return RETRYABLE_ERROR_PATTERNS.some((pattern) => pattern.test(error)) || PROVIDER_OUTAGE_PATTERNS.some((pattern) => pattern.test(error)) || TRANSPORT_MESSAGE_PATTERNS.some((pattern) => pattern.test(error));
25720
+ }
25721
+ if (typeof error !== "object")
25589
25722
  return false;
25590
25723
  const err = error;
25724
+ const statusCode = extractStatusCode(err);
25725
+ if (statusCode === 429 || statusCode === 403 || statusCode !== undefined && OUTAGE_STATUS_CODES.has(statusCode)) {
25726
+ return true;
25727
+ }
25728
+ if ([err.code, err.cause?.code, err.data?.code].some((code) => typeof code === "string" && TRANSPORT_CODES.has(code))) {
25729
+ return true;
25730
+ }
25731
+ const messages = [
25732
+ err.message ?? "",
25733
+ err.data?.message ?? "",
25734
+ err.data?.responseBody ?? ""
25735
+ ];
25736
+ if (messages.some((message) => TRANSPORT_MESSAGE_PATTERNS.some((p) => p.test(message)))) {
25737
+ return true;
25738
+ }
25591
25739
  const text = [
25592
25740
  err.message ?? "",
25593
- String(err.data?.statusCode ?? ""),
25594
25741
  err.data?.message ?? "",
25595
25742
  err.data?.responseBody ?? ""
25596
25743
  ].join(" ");
25597
- return RATE_LIMIT_PATTERNS.some((p) => p.test(text));
25744
+ const hasFailoverReason = RETRYABLE_ERROR_PATTERNS.some((p) => p.test(text)) || PROVIDER_OUTAGE_PATTERNS.some((p) => p.test(text));
25745
+ return hasFailoverReason;
25598
25746
  }
25599
25747
  var DEDUP_WINDOW_MS = 5000;
25600
25748
  var REPROMPT_DELAY_MS = 500;
@@ -25604,7 +25752,6 @@ class ForegroundFallbackManager {
25604
25752
  chains;
25605
25753
  enabled;
25606
25754
  maxRetries;
25607
- runtimeOverride;
25608
25755
  sessionModel = new Map;
25609
25756
  sessionAgent = new Map;
25610
25757
  sessionTried = new Map;
@@ -25615,18 +25762,26 @@ class ForegroundFallbackManager {
25615
25762
  isFallbackInProgress(sessionID) {
25616
25763
  return this.inProgress.has(sessionID);
25617
25764
  }
25618
- constructor(client, chains, enabled, maxRetries = 3, coordinator, runtimeOverride = true) {
25765
+ disableChain(agentName) {
25766
+ this.chains[agentName] = [];
25767
+ }
25768
+ registerSessionAgent(sessionID, agentName) {
25769
+ const normalizedAgentName = agentName.trim();
25770
+ if (!sessionID || !normalizedAgentName || this.sessionAgent.has(sessionID)) {
25771
+ return;
25772
+ }
25773
+ this.sessionAgent.set(sessionID, normalizedAgentName);
25774
+ }
25775
+ constructor(client, chains, enabled, maxRetries = 3, coordinator) {
25619
25776
  this.client = client;
25620
25777
  this.chains = chains;
25621
25778
  this.enabled = enabled;
25622
25779
  this.maxRetries = maxRetries;
25623
- this.runtimeOverride = runtimeOverride;
25624
25780
  if (coordinator) {
25625
25781
  coordinator.onSessionDeleted((id) => {
25626
25782
  this.sessionModel.delete(id);
25627
25783
  this.sessionAgent.delete(id);
25628
25784
  this.sessionTried.delete(id);
25629
- this.inProgress.delete(id);
25630
25785
  this.lastTrigger.delete(id);
25631
25786
  this.lastTriggerModel.delete(id);
25632
25787
  this.sessionRetries.delete(id);
@@ -25648,13 +25803,13 @@ class ForegroundFallbackManager {
25648
25803
  if (!sessionID)
25649
25804
  break;
25650
25805
  if (typeof info.agent === "string") {
25651
- this.sessionAgent.set(sessionID, info.agent);
25806
+ this.registerSessionAgent(sessionID, info.agent);
25652
25807
  }
25653
25808
  if (typeof info.providerID === "string" && typeof info.modelID === "string") {
25654
25809
  this.sessionModel.set(sessionID, `${info.providerID}/${info.modelID}`);
25655
25810
  }
25656
- if (info.error && isRateLimitError(info.error)) {
25657
- if (this.shouldIntervene(sessionID)) {
25811
+ if (info.error && isFailoverError(info.error)) {
25812
+ if (this.shouldTriggerFallback(sessionID)) {
25658
25813
  await this.tryFallback(sessionID);
25659
25814
  }
25660
25815
  } else {
@@ -25664,29 +25819,46 @@ class ForegroundFallbackManager {
25664
25819
  }
25665
25820
  case "session.error": {
25666
25821
  const props = event.properties;
25667
- if (props?.sessionID && props.error && isRateLimitError(props.error) && this.shouldIntervene(props.sessionID)) {
25668
- await this.tryFallback(props.sessionID);
25822
+ if (!props)
25823
+ break;
25824
+ const sessionID = eventSessionID(props);
25825
+ if (sessionID && props.error && isFailoverError(props.error) && this.shouldTriggerFallback(sessionID)) {
25826
+ await this.tryFallback(sessionID);
25669
25827
  }
25670
25828
  break;
25671
25829
  }
25672
25830
  case "session.status": {
25673
25831
  const props = event.properties;
25674
- if (!props?.sessionID || !props.status?.message)
25832
+ if (!props)
25675
25833
  break;
25676
- const msg = props.status.message.toLowerCase();
25677
- if (msg.includes("rate limit") || msg.includes("usage limit") || msg.includes("usage exceeded") || msg.includes("quota exceeded") || msg.includes("exceededbudget") || msg.includes("over budget") || msg.includes("insufficient") || msg.includes("high concurrency") || msg.includes("reduce concurrency")) {
25678
- if (this.checkRetryBudget(props.sessionID)) {
25679
- await this.tryFallback(props.sessionID);
25834
+ const sessionID = eventSessionID(props);
25835
+ if (!sessionID)
25836
+ break;
25837
+ const isFailoverRetry = props.status?.type === "retry" && (isFailoverError(props.error) || props.status.message !== undefined && isFailoverError({ message: props.status.message }));
25838
+ if (isFailoverRetry) {
25839
+ const prevModel = this.lastTriggerModel.get(sessionID);
25840
+ const curModel = this.sessionModel.get(sessionID);
25841
+ const lastTriggerTime = this.lastTrigger.get(sessionID) ?? 0;
25842
+ const attempt = props.status?.attempt ?? 1;
25843
+ const modelChanged = prevModel !== undefined && curModel !== undefined && prevModel !== curModel;
25844
+ const withinDedupWindow = Date.now() - lastTriggerTime < DEDUP_WINDOW_MS;
25845
+ if (modelChanged && withinDedupWindow && attempt > 1) {
25846
+ break;
25680
25847
  }
25681
- } else {
25682
- this.sessionRetries.delete(props.sessionID);
25848
+ if (this.shouldTriggerFallback(sessionID)) {
25849
+ await this.tryFallbackWithAbort(sessionID);
25850
+ }
25851
+ break;
25852
+ }
25853
+ if (this.isRecoveredStatus(props.status?.type)) {
25854
+ this.sessionRetries.delete(sessionID);
25683
25855
  }
25684
25856
  break;
25685
25857
  }
25686
25858
  case "subagent.session.created": {
25687
25859
  const props = event.properties;
25688
25860
  if (props?.sessionID && typeof props.agentName === "string") {
25689
- this.sessionAgent.set(props.sessionID, props.agentName);
25861
+ this.registerSessionAgent(props.sessionID, props.agentName);
25690
25862
  }
25691
25863
  break;
25692
25864
  }
@@ -25702,7 +25874,7 @@ class ForegroundFallbackManager {
25702
25874
  }
25703
25875
  }
25704
25876
  }
25705
- checkRetryBudget(sessionID) {
25877
+ consumeRetryBudget(sessionID) {
25706
25878
  const tried = this.sessionRetries.get(sessionID) ?? 0;
25707
25879
  if (tried < this.maxRetries - 1) {
25708
25880
  this.sessionRetries.set(sessionID, tried + 1);
@@ -25716,27 +25888,57 @@ class ForegroundFallbackManager {
25716
25888
  this.sessionRetries.delete(sessionID);
25717
25889
  return true;
25718
25890
  }
25719
- shouldIntervene(sessionID) {
25891
+ shouldTriggerFallback(sessionID) {
25720
25892
  const tried = this.sessionRetries.get(sessionID) ?? 0;
25721
25893
  if (tried === 0)
25722
25894
  return true;
25723
- return this.checkRetryBudget(sessionID);
25895
+ return this.consumeRetryBudget(sessionID);
25896
+ }
25897
+ isRecoveredStatus(statusType) {
25898
+ return statusType === "idle" || statusType === "complete" || statusType === "completed" || statusType === "success" || statusType === "terminal";
25724
25899
  }
25725
25900
  async tryFallback(sessionID) {
25726
25901
  if (!sessionID)
25727
25902
  return;
25728
25903
  if (this.inProgress.has(sessionID))
25729
25904
  return;
25905
+ if (this.isDeduped(sessionID))
25906
+ return;
25907
+ this.inProgress.add(sessionID);
25908
+ try {
25909
+ await this.execFallback(sessionID);
25910
+ } finally {
25911
+ this.inProgress.delete(sessionID);
25912
+ }
25913
+ }
25914
+ async tryFallbackWithAbort(sessionID) {
25915
+ if (!sessionID)
25916
+ return;
25917
+ if (this.inProgress.has(sessionID))
25918
+ return;
25919
+ if (this.isDeduped(sessionID))
25920
+ return;
25921
+ this.inProgress.add(sessionID);
25922
+ try {
25923
+ await abortSessionWithTimeout(this.client, sessionID);
25924
+ await this.execFallback(sessionID);
25925
+ } finally {
25926
+ this.inProgress.delete(sessionID);
25927
+ }
25928
+ }
25929
+ isDeduped(sessionID) {
25730
25930
  const now = Date.now();
25731
25931
  const curModel = this.sessionModel.get(sessionID);
25732
25932
  const modelChanged = this.lastTriggerModel.has(sessionID) && this.lastTriggerModel.get(sessionID) !== curModel;
25733
25933
  if (!modelChanged && now - (this.lastTrigger.get(sessionID) ?? 0) < DEDUP_WINDOW_MS)
25734
- return;
25934
+ return true;
25735
25935
  this.lastTrigger.set(sessionID, now);
25736
25936
  if (curModel !== undefined) {
25737
25937
  this.lastTriggerModel.set(sessionID, curModel);
25738
25938
  }
25739
- this.inProgress.add(sessionID);
25939
+ return false;
25940
+ }
25941
+ async execFallback(sessionID) {
25740
25942
  try {
25741
25943
  let currentModel = this.sessionModel.get(sessionID);
25742
25944
  const agentName = this.sessionAgent.get(sessionID);
@@ -25751,16 +25953,6 @@ class ForegroundFallbackManager {
25751
25953
  if (!currentModel && agentName && chain.length > 0) {
25752
25954
  currentModel = chain[0];
25753
25955
  }
25754
- if (!this.runtimeOverride && currentModel && !chain.includes(currentModel)) {
25755
- log("[foreground-fallback] current model not in chain, skipping fallback (runtimeOverride=false)", {
25756
- sessionID,
25757
- agentName,
25758
- currentModel,
25759
- chain
25760
- });
25761
- await abortSessionWithTimeout(this.client, sessionID);
25762
- return;
25763
- }
25764
25956
  if (!this.sessionTried.has(sessionID)) {
25765
25957
  this.sessionTried.set(sessionID, new Set);
25766
25958
  }
@@ -25820,10 +26012,15 @@ class ForegroundFallbackManager {
25820
26012
  log("[foreground-fallback] promptAsync unavailable", { sessionID });
25821
26013
  return;
25822
26014
  }
26015
+ const promptBody = {
26016
+ parts: lastUser.parts,
26017
+ model: ref,
26018
+ ...agentName ? { agent: agentName } : {}
26019
+ };
25823
26020
  try {
25824
26021
  await sessionClient.promptAsync({
25825
26022
  path: { id: sessionID },
25826
- body: { parts: lastUser.parts, model: ref }
26023
+ body: promptBody
25827
26024
  });
25828
26025
  } catch (_promptErr) {
25829
26026
  log("[foreground-fallback] promptAsync on busy session, aborting", {
@@ -25833,7 +26030,7 @@ class ForegroundFallbackManager {
25833
26030
  await new Promise((r) => setTimeout(r, REPROMPT_DELAY_MS));
25834
26031
  await sessionClient.promptAsync({
25835
26032
  path: { id: sessionID },
25836
- body: { parts: lastUser.parts, model: ref }
26033
+ body: promptBody
25837
26034
  });
25838
26035
  }
25839
26036
  this.sessionModel.set(sessionID, nextModel);
@@ -25848,8 +26045,6 @@ class ForegroundFallbackManager {
25848
26045
  sessionID,
25849
26046
  error: err instanceof Error ? err.message : String(err)
25850
26047
  });
25851
- } finally {
25852
- this.inProgress.delete(sessionID);
25853
26048
  }
25854
26049
  }
25855
26050
  resolveChain(agentName, currentModel) {
@@ -25857,8 +26052,7 @@ class ForegroundFallbackManager {
25857
26052
  const chain = this.chains[agentName];
25858
26053
  if (chain)
25859
26054
  return chain;
25860
- if (ALL_AGENT_NAMES.includes(agentName))
25861
- return [];
26055
+ return [];
25862
26056
  }
25863
26057
  if (currentModel) {
25864
26058
  for (const chain of Object.values(this.chains)) {
@@ -25945,10 +26139,14 @@ function cleanupAllSessions(saveDir) {
25945
26139
  try {
25946
26140
  if (now - statSync5(fp).mtimeMs > maxAge)
25947
26141
  unlinkSync3(fp);
25948
- } catch {}
26142
+ } catch (err) {
26143
+ log("[image-hook] file cleanup failed", String(err));
26144
+ }
25949
26145
  }
25950
26146
  }
25951
- } catch {}
26147
+ } catch (err) {
26148
+ log("[image-hook] directory scan failed", String(err));
26149
+ }
25952
26150
  for (const dir of dirsToScan) {
25953
26151
  try {
25954
26152
  let isEmpty = true;
@@ -25962,16 +26160,21 @@ function cleanupAllSessions(saveDir) {
25962
26160
  } else {
25963
26161
  allRemoved = false;
25964
26162
  }
25965
- } catch {
26163
+ } catch (err) {
26164
+ log("[image-hook] file cleanup failed", String(err));
25966
26165
  allRemoved = false;
25967
26166
  }
25968
26167
  }
25969
26168
  if (!isEmpty && allRemoved) {
25970
26169
  try {
25971
26170
  rmdirSync(dir);
25972
- } catch {}
26171
+ } catch (err) {
26172
+ log("[image-hook] directory removal failed", String(err));
26173
+ }
25973
26174
  }
25974
- } catch {}
26175
+ } catch (err) {
26176
+ log("[image-hook] session cleanup failed", String(err));
26177
+ }
25975
26178
  }
25976
26179
  }
25977
26180
  function writeUniqueFile(dir, name, data, log2) {
@@ -26001,7 +26204,9 @@ function writeUniqueFile(dir, name, data, log2) {
26001
26204
  return null;
26002
26205
  }
26003
26206
  function processImageAttachments(args) {
26004
- const { messages, workDir, disabledAgents, log: log2 } = args;
26207
+ const { messages, workDir, imageRouting, disabledAgents, log: log2 } = args;
26208
+ if (imageRouting === "direct")
26209
+ return;
26005
26210
  const observerEnabled = !disabledAgents.has("observer");
26006
26211
  if (!observerEnabled)
26007
26212
  return;
@@ -26039,6 +26244,7 @@ function processImageAttachments(args) {
26039
26244
  log2(`[image-hook] failed to create target image directory: ${e}`);
26040
26245
  }
26041
26246
  const savedPaths = [];
26247
+ const savedImageParts = new Set;
26042
26248
  for (const p of imageParts) {
26043
26249
  const url = p.url;
26044
26250
  const filename = p.filename ?? p.name;
@@ -26051,14 +26257,21 @@ function processImageAttachments(args) {
26051
26257
  const ext = sanitizedFilename ? extname(sanitizedFilename) || extFromMime(decoded.mime) : extFromMime(decoded.mime);
26052
26258
  const name = `${baseName}-${hash}${ext}`;
26053
26259
  const filePath = writeUniqueFile(targetDir, name, decoded.data, log2);
26054
- if (filePath)
26260
+ if (filePath) {
26055
26261
  savedPaths.push(filePath);
26262
+ savedImageParts.add(p);
26263
+ }
26056
26264
  }
26057
26265
  }
26058
26266
  }
26059
- const pathsText = savedPaths.length > 0 ? ` Saved to: ${savedPaths.join(", ")}` : "";
26060
- log2(`[image-hook] stripping image/file parts, saving to disk${pathsText}`);
26061
- msg.parts = msg.parts.filter((p) => !isImagePart(p)).concat([
26267
+ if (savedPaths.length === 0) {
26268
+ log2("[image-hook] no images saved; leaving original parts in message");
26269
+ continue;
26270
+ }
26271
+ const pathsText = ` Saved to: ${savedPaths.join(", ")}`;
26272
+ log2(`[image-hook] saved image/file parts to disk${pathsText}`);
26273
+ log2(`[image-routing] auto mode: intercepted ${savedImageParts.size} image(s), delegating to @observer`);
26274
+ msg.parts = msg.parts.filter((p) => !savedImageParts.has(p)).concat([
26062
26275
  {
26063
26276
  type: "text",
26064
26277
  text: `[Image attachment detected.${pathsText} Your model may not support image input. Delegate to @observer with the file path(s) above so it can read the file with its read tool.]`
@@ -26161,15 +26374,7 @@ function helpPrompt() {
26161
26374
  function createLoopCommandHook() {
26162
26375
  return {
26163
26376
  registerCommand: (opencodeConfig) => {
26164
- const cfg = opencodeConfig.command;
26165
- if (cfg?.[COMMAND_NAME2])
26166
- return;
26167
- if (!opencodeConfig.command)
26168
- opencodeConfig.command = {};
26169
- opencodeConfig.command[COMMAND_NAME2] = {
26170
- template: "Run an automated execute-verify loop",
26171
- description: "Dispatch fixer, verify, iterate with file-based history on disk."
26172
- };
26377
+ registerCommandHook(opencodeConfig, COMMAND_NAME2, "Run an automated execute-verify loop", "Dispatch fixer, verify, iterate with file-based history on disk.");
26173
26378
  },
26174
26379
  handleCommandExecuteBefore: async (input, output) => {
26175
26380
  if (input.command !== COMMAND_NAME2)
@@ -26223,7 +26428,7 @@ function createPhaseReminderHook(coordinator) {
26223
26428
  if (isInternalInitiatorPart(originalPart)) {
26224
26429
  return;
26225
26430
  }
26226
- if (lastUserMessage.parts.some((part) => part.synthetic === true && isRecord(part.metadata) && part.metadata[PHASE_REMINDER_METADATA_KEY] === true)) {
26431
+ if (lastUserMessage.parts.some((part) => part.synthetic === true && isRecord2(part.metadata) && part.metadata[PHASE_REMINDER_METADATA_KEY] === true)) {
26227
26432
  return;
26228
26433
  }
26229
26434
  lastUserMessage.parts.push({
@@ -26297,18 +26502,7 @@ function createReflectCommandHook() {
26297
26502
  let shouldHandleCommand = false;
26298
26503
  return {
26299
26504
  registerCommand: (opencodeConfig) => {
26300
- const commandConfig = opencodeConfig.command;
26301
- if (commandConfig?.[COMMAND_NAME3]) {
26302
- shouldHandleCommand = false;
26303
- return;
26304
- }
26305
- if (!opencodeConfig.command)
26306
- opencodeConfig.command = {};
26307
- opencodeConfig.command[COMMAND_NAME3] = {
26308
- template: "Review repeated work and suggest workflow improvements",
26309
- description: "Use reflect to learn from repeated workflows and suggest reusable improvements"
26310
- };
26311
- shouldHandleCommand = true;
26505
+ shouldHandleCommand ||= registerCommandHook(opencodeConfig, COMMAND_NAME3, "Review repeated work and suggest workflow improvements", "Use reflect to learn from repeated workflows and suggest reusable improvements");
26312
26506
  },
26313
26507
  handleCommandExecuteBefore: async (input, output) => {
26314
26508
  if (input.command !== COMMAND_NAME3 || !shouldHandleCommand)
@@ -26534,8 +26728,10 @@ function createTaskSessionManagerHook(_ctx, options) {
26534
26728
  const terminalJobsInjectedByParent = new Map;
26535
26729
  if (options.coordinator) {
26536
26730
  options.coordinator.onSessionDeleted((sessionId) => {
26537
- backgroundJobBoard.drop(sessionId);
26538
- backgroundJobBoard.clearParent(sessionId);
26731
+ if (!options.isFallbackInProgress?.(sessionId)) {
26732
+ backgroundJobBoard.drop(sessionId);
26733
+ backgroundJobBoard.clearParent(sessionId);
26734
+ }
26539
26735
  terminalJobsInjectedByParent.delete(sessionId);
26540
26736
  taskContextTracker.clearSession(sessionId);
26541
26737
  taskContextTracker.prune(backgroundJobBoard);
@@ -26705,7 +26901,7 @@ function createTaskSessionManagerHook(_ctx, options) {
26705
26901
  sessionID: input.sessionID
26706
26902
  });
26707
26903
  }
26708
- if (!isRecord(output.args))
26904
+ if (!isRecord2(output.args))
26709
26905
  return;
26710
26906
  const args = output.args;
26711
26907
  if (typeof args.subagent_type !== "string" || args.subagent_type.trim() === "") {
@@ -26890,7 +27086,7 @@ function createTaskSessionManagerHook(_ctx, options) {
26890
27086
  if (isInternalInitiatorPart(textPart)) {
26891
27087
  return;
26892
27088
  }
26893
- if (message.parts.some((part) => part.synthetic === true && isRecord(part.metadata) && part.metadata[BACKGROUND_JOB_BOARD_METADATA_KEY] === true)) {
27089
+ if (message.parts.some((part) => part.synthetic === true && isRecord2(part.metadata) && part.metadata[BACKGROUND_JOB_BOARD_METADATA_KEY] === true)) {
26894
27090
  return;
26895
27091
  }
26896
27092
  rememberInjectedTerminalJobs(message.info.sessionID);
@@ -26964,7 +27160,7 @@ function createTaskSessionManagerHook(_ctx, options) {
26964
27160
  }
26965
27161
  if (sessionId2 && options.shouldManageSession(sessionId2)) {
26966
27162
  const props = input.event.properties;
26967
- if (!props?.error || !isRateLimitError(props.error)) {
27163
+ if (!props?.error || !isFailoverError(props.error)) {
26968
27164
  terminalJobsInjectedByParent.delete(sessionId2);
26969
27165
  }
26970
27166
  }
@@ -27035,7 +27231,7 @@ function createTaskSessionManagerHook(_ctx, options) {
27035
27231
  result: status.result
27036
27232
  });
27037
27233
  output.output = formatCancelledTaskStatusOutput(status.taskID, backgroundJobBoard.getResultSummary(status.taskID));
27038
- if (isRecord(output) && isRecord(output.metadata)) {
27234
+ if (isRecord2(output) && isRecord2(output.metadata)) {
27039
27235
  output.metadata.state = "cancelled";
27040
27236
  }
27041
27237
  }
@@ -27073,6 +27269,23 @@ import { URL as URL2 } from "node:url";
27073
27269
  import * as fsSync from "node:fs";
27074
27270
  import * as fs6 from "node:fs/promises";
27075
27271
  import * as path13 from "node:path";
27272
+
27273
+ // src/utils/frontmatter.ts
27274
+ function parseFrontmatter(content) {
27275
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
27276
+ if (!match)
27277
+ return null;
27278
+ const result = {};
27279
+ for (const line of match[1].split(/\r?\n/)) {
27280
+ const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.+?)\s*$/);
27281
+ if (!kv)
27282
+ continue;
27283
+ result[kv[1]] = kv[2].replace(/^(['"])(.*)\1$/, "$2");
27284
+ }
27285
+ return result;
27286
+ }
27287
+
27288
+ // src/interview/document.ts
27076
27289
  var DEFAULT_OUTPUT_FOLDER = "interview";
27077
27290
  function normalizeOutputFolder(outputFolder) {
27078
27291
  const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
@@ -27182,20 +27395,7 @@ function buildInterviewDocument(idea, summary, history, meta) {
27182
27395
  ].join(`
27183
27396
  `);
27184
27397
  }
27185
- function parseFrontmatter(content) {
27186
- const match = content.match(/^---\n([\s\S]*?)\n---\n/);
27187
- if (!match)
27188
- return null;
27189
- const result = {};
27190
- for (const line of match[1].split(`
27191
- `)) {
27192
- const colonIdx = line.indexOf(":");
27193
- if (colonIdx > 0) {
27194
- result[line.slice(0, colonIdx).trim()] = line.slice(colonIdx + 1).trim();
27195
- }
27196
- }
27197
- return result;
27198
- }
27398
+ var parseFrontmatter2 = parseFrontmatter;
27199
27399
  async function ensureInterviewFile(record) {
27200
27400
  await fs6.mkdir(path13.dirname(record.markdownPath), { recursive: true });
27201
27401
  try {
@@ -27333,11 +27533,13 @@ async function readJsonBody(request) {
27333
27533
  return raw ? JSON.parse(raw) : {};
27334
27534
  }
27335
27535
 
27336
- // src/interview/ui.ts
27337
- var BRAND_LOGO_URL = "https://ohmyopencodeslim.com/android-chrome-512x512.png";
27536
+ // src/utils/escape-html.ts
27338
27537
  function escapeHtml(value) {
27339
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
27538
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
27340
27539
  }
27540
+
27541
+ // src/interview/ui.ts
27542
+ var BRAND_LOGO_URL = "https://ohmyopencodeslim.com/android-chrome-512x512.png";
27341
27543
  function clipboardHelperJs() {
27342
27544
  return `
27343
27545
  function copyCommand(text, btn) {
@@ -29513,7 +29715,7 @@ data: ${JSON.stringify(formatSseState(entry))}
29513
29715
  const title = extractTitle(content) || entry.replace(/\.md$/, "");
29514
29716
  const summary = extractSummarySection(content);
29515
29717
  const baseName = entry.replace(/\.md$/, "");
29516
- const fm = parseFrontmatter(content);
29718
+ const fm = parseFrontmatter2(content);
29517
29719
  items.push({
29518
29720
  fileName: entry,
29519
29721
  resumeCommand: `/interview ${baseName}`,
@@ -29548,7 +29750,7 @@ data: ${JSON.stringify(formatSseState(entry))}
29548
29750
  } catch {
29549
29751
  continue;
29550
29752
  }
29551
- const fm = parseFrontmatter(content);
29753
+ const fm = parseFrontmatter2(content);
29552
29754
  if (!fm?.sessionID)
29553
29755
  continue;
29554
29756
  const title = extractTitle(content) || entry.replace(/\.md$/, "");
@@ -31301,6 +31503,14 @@ function createInterviewService(ctx, config, deps) {
31301
31503
  const interview = await createInterview(input.sessionID, idea);
31302
31504
  await notifyInterviewUrl(input.sessionID, interview);
31303
31505
  output.parts.push(createInternalAgentTextPart(buildKickoffPrompt(idea, maxQuestions)));
31506
+ let sessionTitle = `Interview: ${idea}`;
31507
+ if (sessionTitle.length > 50) {
31508
+ sessionTitle = `${sessionTitle.slice(0, 49)}…`;
31509
+ }
31510
+ ctx.client.session.update?.({
31511
+ path: { id: input.sessionID },
31512
+ body: { title: sessionTitle }
31513
+ })?.catch(() => {});
31304
31514
  }
31305
31515
  async function handleEvent(input) {
31306
31516
  const { event } = input;
@@ -32061,21 +32271,23 @@ function createBuiltinMcps(disabledMcps = [], websearchConfig) {
32061
32271
  return mcps;
32062
32272
  }
32063
32273
 
32064
- // src/multiplexer/herdr/index.ts
32274
+ // src/multiplexer/cmux/index.ts
32065
32275
  init_compat();
32066
32276
 
32067
32277
  // src/multiplexer/shared.ts
32068
32278
  init_compat();
32279
+ import { existsSync as existsSync9 } from "node:fs";
32280
+ import { basename as basename6, isAbsolute as isAbsolute5 } from "node:path";
32069
32281
  function quoteShellArg(value) {
32070
32282
  return `'${value.replace(/'/g, `'\\''`)}'`;
32071
32283
  }
32072
32284
  function normalizePathForShell(directory) {
32073
32285
  return process.platform === "win32" ? directory.replace(/\\/g, "/") : directory;
32074
32286
  }
32075
- function buildOpencodeAttachCommand(sessionId, serverUrl, directory) {
32287
+ function buildOpencodeAttachCommand(sessionId, serverUrl, directory, executable = "opencode") {
32076
32288
  const attachDir = normalizePathForShell(directory);
32077
32289
  return [
32078
- "opencode",
32290
+ executable === "opencode" ? executable : quoteShellArg(executable),
32079
32291
  "attach",
32080
32292
  quoteShellArg(serverUrl),
32081
32293
  "--session",
@@ -32084,6 +32296,20 @@ function buildOpencodeAttachCommand(sessionId, serverUrl, directory) {
32084
32296
  quoteShellArg(attachDir)
32085
32297
  ].join(" ");
32086
32298
  }
32299
+ function resolveHostOpencodeBinary(options = {}) {
32300
+ const pathExists = options.pathExists ?? existsSync9;
32301
+ for (const candidate of [
32302
+ options.override,
32303
+ options.envOverride ?? process.env.OPENCODE_BIN,
32304
+ options.execPath ?? process.execPath,
32305
+ options.argv0 ?? process.argv[0]
32306
+ ]) {
32307
+ if (candidate && isAbsolute5(candidate) && /^opencode(?:\.exe)?$/i.test(basename6(candidate)) && pathExists(candidate)) {
32308
+ return candidate;
32309
+ }
32310
+ }
32311
+ return null;
32312
+ }
32087
32313
  async function findBinary(binaryName, options = {}) {
32088
32314
  const isWindows = process.platform === "win32";
32089
32315
  const cmd = isWindows ? "where" : "which";
@@ -32165,7 +32391,536 @@ async function gracefulClosePane(binary, paneId, options) {
32165
32391
  }
32166
32392
  }
32167
32393
 
32394
+ // src/multiplexer/cmux/index.ts
32395
+ var MINIMUM_VERSION = "0.64.14";
32396
+ var READINESS_DELAYS_MS = [50, 100, 200, 400, 500, 500, 250];
32397
+ var registries = new Map;
32398
+ var mutationQueue = Promise.resolve();
32399
+ var mutationSequence = 0;
32400
+
32401
+ class CmuxMultiplexer {
32402
+ client;
32403
+ type = "cmux";
32404
+ versionAvailable = false;
32405
+ availabilityError = "unavailable";
32406
+ checkSessionReady;
32407
+ delay;
32408
+ readinessAttemptTimeoutMs;
32409
+ opencodeBinary;
32410
+ constructor(client = new CliCmuxClient, options = {}) {
32411
+ this.client = client;
32412
+ this.checkSessionReady = options.checkSessionReady ?? defaultSessionReady;
32413
+ this.delay = options.delay ?? defaultDelay;
32414
+ this.readinessAttemptTimeoutMs = options.readinessAttemptTimeoutMs ?? 1000;
32415
+ this.opencodeBinary = resolveHostOpencodeBinary({
32416
+ override: options.opencodeBinary,
32417
+ pathExists: options.pathExists
32418
+ });
32419
+ }
32420
+ async isAvailable() {
32421
+ if (this.versionAvailable)
32422
+ return true;
32423
+ const version = await this.client.version().catch(() => null);
32424
+ if (version && compareVersions2(version, MINIMUM_VERSION) >= 0) {
32425
+ this.versionAvailable = true;
32426
+ return true;
32427
+ }
32428
+ this.availabilityError = version ? "hard" : this.client.getVersionError?.() ?? "unavailable";
32429
+ return false;
32430
+ }
32431
+ isInsideSession() {
32432
+ return Boolean(process.env.CMUX_SOCKET_PATH && process.env.CMUX_WORKSPACE_ID && process.env.CMUX_SURFACE_ID);
32433
+ }
32434
+ async spawnPane(sessionId, _description, serverUrl, directory) {
32435
+ if (!this.opencodeBinary)
32436
+ return { success: false, error: "hard" };
32437
+ const opencodeBinary = this.opencodeBinary;
32438
+ const statusUrl = new URL("/session/status", serverUrl);
32439
+ if (!await this.waitForSession(statusUrl, sessionId)) {
32440
+ log("[cmux] spawnPane failed", {
32441
+ stage: "readinessTimeout",
32442
+ sessionId
32443
+ });
32444
+ return { success: false, error: "unavailable" };
32445
+ }
32446
+ return enqueueMutation("spawn", async (sequence) => {
32447
+ if (!await this.isAvailable()) {
32448
+ log("[cmux] spawnPane failed", {
32449
+ sequence,
32450
+ stage: "version",
32451
+ sessionId
32452
+ });
32453
+ return {
32454
+ success: false,
32455
+ error: this.availabilityError
32456
+ };
32457
+ }
32458
+ const root = await this.client.identify();
32459
+ if (!root) {
32460
+ log("[cmux] spawnPane failed", {
32461
+ sequence,
32462
+ stage: "identify",
32463
+ sessionId
32464
+ });
32465
+ return {
32466
+ success: false,
32467
+ error: this.client.getIdentifyError?.() ?? "unavailable"
32468
+ };
32469
+ }
32470
+ const key = registryKey(root.socketPath, root.workspaceId);
32471
+ const registry = registries.get(key) ?? { root, agents: [] };
32472
+ registries.set(key, registry);
32473
+ const previous = registry.agents.at(-1);
32474
+ const targetSurfaceId = previous?.surfaceId ?? registry.root.surfaceId;
32475
+ const direction = previous ? "down" : "right";
32476
+ const created = await this.client.createSurface({
32477
+ workspaceId: root.workspaceId,
32478
+ targetSurfaceId,
32479
+ direction,
32480
+ focus: false
32481
+ }, root.socketPath);
32482
+ if (!created) {
32483
+ log("[cmux] spawnPane failed", {
32484
+ sequence,
32485
+ stage: "createSurface",
32486
+ sessionId,
32487
+ workspaceId: root.workspaceId,
32488
+ targetSurfaceId,
32489
+ direction
32490
+ });
32491
+ return {
32492
+ success: false,
32493
+ error: this.client.getCreateError?.() ?? "hard"
32494
+ };
32495
+ }
32496
+ const handle = {
32497
+ v: 1,
32498
+ socketPath: root.socketPath,
32499
+ workspaceId: root.workspaceId,
32500
+ paneId: created.paneId,
32501
+ surfaceId: created.surfaceId
32502
+ };
32503
+ const encodedHandle = encodeHandle(handle);
32504
+ const command = buildOpencodeAttachCommand(sessionId, serverUrl, directory, opencodeBinary);
32505
+ try {
32506
+ const started = await this.client.respawnSurface(root.workspaceId, created.surfaceId, command, root.socketPath);
32507
+ if (!started) {
32508
+ log("[cmux] spawnPane failed", {
32509
+ sequence,
32510
+ stage: "respawn",
32511
+ sessionId,
32512
+ workspaceId: root.workspaceId,
32513
+ targetSurfaceId,
32514
+ direction
32515
+ });
32516
+ throw new Error("cmux respawn-pane failed");
32517
+ }
32518
+ } catch (error) {
32519
+ log("[cmux] spawnPane respawn exception", {
32520
+ sequence,
32521
+ stage: "respawn",
32522
+ sessionId,
32523
+ workspaceId: root.workspaceId,
32524
+ targetSurfaceId,
32525
+ direction,
32526
+ errorType: errorName(error)
32527
+ });
32528
+ const cleaned = await this.cleanupPane(root.workspaceId, created.surfaceId, root.socketPath);
32529
+ return cleaned ? { success: false, error: "unavailable" } : {
32530
+ success: false,
32531
+ error: "unavailable",
32532
+ orphanPaneId: encodedHandle
32533
+ };
32534
+ }
32535
+ registry.agents.push(handle);
32536
+ await this.equalize(root.workspaceId, root.socketPath);
32537
+ return { success: true, paneId: encodeHandle(handle) };
32538
+ });
32539
+ }
32540
+ closePane(paneId) {
32541
+ const handle = decodeHandle(paneId);
32542
+ if (!handle)
32543
+ return Promise.resolve(false);
32544
+ return enqueueMutation("close", async () => {
32545
+ if (!await this.isAvailable())
32546
+ return false;
32547
+ const result = await this.client.closeSurface(handle.workspaceId, handle.surfaceId, handle.socketPath);
32548
+ if (result === "failed")
32549
+ return false;
32550
+ const key = registryKey(handle.socketPath, handle.workspaceId);
32551
+ const registry = registries.get(key);
32552
+ if (registry) {
32553
+ const index = registry.agents.findIndex((agent) => agent.surfaceId === handle.surfaceId);
32554
+ if (index >= 0)
32555
+ registry.agents.splice(index, 1);
32556
+ if (registry.agents.length === 0)
32557
+ registries.delete(key);
32558
+ }
32559
+ await this.equalize(handle.workspaceId, handle.socketPath);
32560
+ return true;
32561
+ });
32562
+ }
32563
+ async applyLayout(_layout, _mainPaneSize) {}
32564
+ async cleanupPane(workspaceId, surfaceId, socketPath) {
32565
+ try {
32566
+ const result = await this.client.closeSurface(workspaceId, surfaceId, socketPath);
32567
+ if (result === "failed") {
32568
+ log("[cmux] failed to close pre-respawn surface", {
32569
+ workspaceId,
32570
+ surfaceId
32571
+ });
32572
+ return false;
32573
+ }
32574
+ return true;
32575
+ } catch (error) {
32576
+ log("[cmux] failed to close pre-respawn surface", {
32577
+ workspaceId,
32578
+ surfaceId,
32579
+ errorType: errorName(error)
32580
+ });
32581
+ return false;
32582
+ }
32583
+ }
32584
+ async waitForSession(url, sessionId) {
32585
+ for (let attempt = 0;attempt <= READINESS_DELAYS_MS.length; attempt++) {
32586
+ const controller = new AbortController;
32587
+ const timeout = setTimeout(() => controller.abort(), this.readinessAttemptTimeoutMs);
32588
+ timeout.unref?.();
32589
+ try {
32590
+ if (await Promise.race([
32591
+ this.checkSessionReady(url, sessionId, controller.signal),
32592
+ new Promise((resolve4) => controller.signal.addEventListener("abort", () => resolve4(false), {
32593
+ once: true
32594
+ }))
32595
+ ])) {
32596
+ return true;
32597
+ }
32598
+ } catch {} finally {
32599
+ clearTimeout(timeout);
32600
+ }
32601
+ const delay2 = READINESS_DELAYS_MS[attempt];
32602
+ if (delay2 === undefined)
32603
+ return false;
32604
+ await this.delay(delay2);
32605
+ }
32606
+ return false;
32607
+ }
32608
+ async equalize(workspaceId, socketPath) {
32609
+ try {
32610
+ const success = await this.client.equalizeSplits({
32611
+ workspace_id: workspaceId,
32612
+ orientation: "vertical"
32613
+ }, socketPath);
32614
+ if (!success)
32615
+ log("[cmux] workspace.equalize_splits failed", { workspaceId });
32616
+ } catch (error) {
32617
+ log("[cmux] workspace.equalize_splits failed", {
32618
+ workspaceId,
32619
+ error: String(error)
32620
+ });
32621
+ }
32622
+ }
32623
+ }
32624
+
32625
+ class SpawnCommandRunner {
32626
+ timeoutMs;
32627
+ spawn;
32628
+ constructor(timeoutMs = 5000, spawn3 = crossSpawn) {
32629
+ this.timeoutMs = timeoutMs;
32630
+ this.spawn = spawn3;
32631
+ }
32632
+ async run(argv) {
32633
+ const proc = this.spawn(argv, { stdout: "pipe", stderr: "pipe" });
32634
+ let timeout;
32635
+ try {
32636
+ return await Promise.race([
32637
+ Promise.all([proc.exited, proc.stdout(), proc.stderr()]).then(([exitCode, stdout, stderr]) => ({ exitCode, stdout, stderr })),
32638
+ new Promise((resolve4) => {
32639
+ timeout = setTimeout(() => {
32640
+ proc.kill("SIGTERM");
32641
+ resolve4({
32642
+ exitCode: 124,
32643
+ stdout: "",
32644
+ stderr: "unavailable: cmux command timed out"
32645
+ });
32646
+ }, this.timeoutMs);
32647
+ timeout.unref?.();
32648
+ })
32649
+ ]);
32650
+ } finally {
32651
+ if (timeout)
32652
+ clearTimeout(timeout);
32653
+ }
32654
+ }
32655
+ }
32656
+
32657
+ class CliCmuxClient {
32658
+ runner;
32659
+ binary = null;
32660
+ versionError = "unavailable";
32661
+ identifyError = "unavailable";
32662
+ lastRunThrew = false;
32663
+ createError = "hard";
32664
+ constructor(runner = new SpawnCommandRunner, binary) {
32665
+ this.runner = runner;
32666
+ this.binary = binary ?? null;
32667
+ }
32668
+ async version() {
32669
+ this.versionError = "unavailable";
32670
+ const result = await this.run(["--version"]);
32671
+ if (!result || result.exitCode !== 0) {
32672
+ this.versionError = "unavailable";
32673
+ return null;
32674
+ }
32675
+ const version = result.stdout.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
32676
+ this.versionError = "hard";
32677
+ return version;
32678
+ }
32679
+ getVersionError() {
32680
+ return this.versionError;
32681
+ }
32682
+ async identify() {
32683
+ this.identifyError = "unavailable";
32684
+ const result = await this.run(["--id-format", "uuids", "identify"]);
32685
+ if (!result || result.exitCode !== 0) {
32686
+ this.identifyError = "unavailable";
32687
+ return null;
32688
+ }
32689
+ const value = asRecord(parseJson(result.stdout));
32690
+ const caller = asRecord(value?.caller);
32691
+ const focused = asRecord(value?.focused);
32692
+ const workspaceId = stringField(caller, "workspace_id") ?? stringField(focused, "workspace_id");
32693
+ const paneId = stringField(caller, "pane_id") ?? stringField(focused, "pane_id");
32694
+ const surfaceId = stringField(caller, "surface_id") ?? stringField(focused, "surface_id");
32695
+ const socketPath = stringField(value, "socket_path");
32696
+ if (workspaceId && paneId && surfaceId && socketPath) {
32697
+ return { workspaceId, paneId, surfaceId, socketPath };
32698
+ }
32699
+ log("[cmux] response parse failed", {
32700
+ operation: "identify",
32701
+ reason: value ? "missing_fields" : "invalid_json",
32702
+ stdoutLength: result.stdout.length
32703
+ });
32704
+ this.identifyError = "hard";
32705
+ return null;
32706
+ }
32707
+ getIdentifyError() {
32708
+ return this.identifyError;
32709
+ }
32710
+ async createSurface(input, socketPath) {
32711
+ this.createError = "hard";
32712
+ const result = await this.run(withSocket(socketPath, [
32713
+ "--json",
32714
+ "--id-format",
32715
+ "uuids",
32716
+ "new-split",
32717
+ input.direction,
32718
+ "--workspace",
32719
+ input.workspaceId,
32720
+ "--surface",
32721
+ input.targetSurfaceId,
32722
+ "--focus",
32723
+ "false"
32724
+ ]));
32725
+ if (!result || result.exitCode !== 0) {
32726
+ this.createError = this.lastRunThrew ? "unavailable" : classifyCreateError(result?.stderr ?? "");
32727
+ return null;
32728
+ }
32729
+ const value = asRecord(parseJson(result.stdout));
32730
+ const resultValue = asRecord(value?.result);
32731
+ const pane = asRecord(resultValue?.pane);
32732
+ const paneId = stringField(value, "pane_id") ?? stringField(resultValue, "pane_id") ?? stringField(pane, "pane_id");
32733
+ const surfaceId = stringField(value, "surface_id") ?? stringField(resultValue, "surface_id") ?? stringField(pane, "surface_id");
32734
+ if (paneId && surfaceId)
32735
+ return { paneId, surfaceId };
32736
+ log("[cmux] response parse failed", {
32737
+ operation: "new-split",
32738
+ reason: value ? "missing_fields" : "invalid_json",
32739
+ stdoutLength: result.stdout.length
32740
+ });
32741
+ return null;
32742
+ }
32743
+ getCreateError() {
32744
+ return this.createError;
32745
+ }
32746
+ async respawnSurface(workspaceId, surfaceId, command, socketPath) {
32747
+ const result = await this.run(withSocket(socketPath, [
32748
+ "respawn-pane",
32749
+ "--workspace",
32750
+ workspaceId,
32751
+ "--surface",
32752
+ surfaceId,
32753
+ "--command",
32754
+ command
32755
+ ]));
32756
+ return result?.exitCode === 0;
32757
+ }
32758
+ async closeSurface(workspaceId, surfaceId, socketPath) {
32759
+ const result = await this.run(withSocket(socketPath, [
32760
+ "close-surface",
32761
+ "--workspace",
32762
+ workspaceId,
32763
+ "--surface",
32764
+ surfaceId
32765
+ ]));
32766
+ if (result?.exitCode === 0)
32767
+ return "closed";
32768
+ return result?.stderr.toLowerCase().includes("not_found") || result?.stderr.toLowerCase().includes("not found") ? "not_found" : "failed";
32769
+ }
32770
+ async equalizeSplits(params, socketPath) {
32771
+ const result = await this.run(withSocket(socketPath, [
32772
+ "rpc",
32773
+ "workspace.equalize_splits",
32774
+ JSON.stringify(params)
32775
+ ]));
32776
+ return result?.exitCode === 0;
32777
+ }
32778
+ async run(args) {
32779
+ this.lastRunThrew = false;
32780
+ this.binary ??= await findBinary("cmux");
32781
+ if (!this.binary)
32782
+ return null;
32783
+ let result;
32784
+ try {
32785
+ result = await this.runner.run([this.binary, ...args]);
32786
+ } catch (error) {
32787
+ this.lastRunThrew = true;
32788
+ log("[cmux] command threw", {
32789
+ operation: commandOperation(args),
32790
+ errorType: errorName(error)
32791
+ });
32792
+ return null;
32793
+ }
32794
+ if (result.exitCode !== 0) {
32795
+ const operation = commandOperation(args);
32796
+ log("[cmux] command failed", {
32797
+ operation,
32798
+ exitCode: result.exitCode,
32799
+ stderr: operation === "respawn-pane" ? "[redacted: may contain attach command]" : safeSummary(result.stderr)
32800
+ });
32801
+ }
32802
+ return result;
32803
+ }
32804
+ }
32805
+ function enqueueMutation(operation, mutation) {
32806
+ const sequence = ++mutationSequence;
32807
+ log("[cmux] mutation enqueue", {
32808
+ sequence,
32809
+ operation,
32810
+ agentCount: registryAgentCount()
32811
+ });
32812
+ const run = async () => {
32813
+ log("[cmux] mutation start", {
32814
+ sequence,
32815
+ operation,
32816
+ agentCount: registryAgentCount()
32817
+ });
32818
+ try {
32819
+ return await mutation(sequence);
32820
+ } finally {
32821
+ log("[cmux] mutation end", {
32822
+ sequence,
32823
+ operation,
32824
+ agentCount: registryAgentCount()
32825
+ });
32826
+ }
32827
+ };
32828
+ const result = mutationQueue.then(run, run);
32829
+ mutationQueue = result.then(() => {
32830
+ return;
32831
+ }, () => {
32832
+ return;
32833
+ });
32834
+ return result;
32835
+ }
32836
+ function registryAgentCount() {
32837
+ let count = 0;
32838
+ for (const registry of registries.values())
32839
+ count += registry.agents.length;
32840
+ return count;
32841
+ }
32842
+ function commandOperation(args) {
32843
+ return args.find((arg) => [
32844
+ "identify",
32845
+ "new-split",
32846
+ "respawn-pane",
32847
+ "close-surface",
32848
+ "rpc"
32849
+ ].includes(arg)) ?? "version";
32850
+ }
32851
+ function withSocket(socketPath, args) {
32852
+ return socketPath ? ["--socket", socketPath, ...args] : args;
32853
+ }
32854
+ function safeSummary(value) {
32855
+ const trimmed = value.trim();
32856
+ return trimmed.length > 300 ? `${trimmed.slice(0, 300)}…` : trimmed;
32857
+ }
32858
+ function classifyCreateError(stderr) {
32859
+ const normalized = stderr.toLowerCase();
32860
+ if (normalized.includes("not_found") || normalized.includes("not found")) {
32861
+ return "not_found";
32862
+ }
32863
+ if (normalized.includes("unavailable"))
32864
+ return "unavailable";
32865
+ if (normalized.includes("invalid_state") || normalized.includes("invalid state")) {
32866
+ return "invalid_state";
32867
+ }
32868
+ return "hard";
32869
+ }
32870
+ function errorName(error) {
32871
+ return error instanceof Error ? error.name : typeof error;
32872
+ }
32873
+ async function defaultSessionReady(url, sessionId, signal) {
32874
+ const response = await fetch(url, { signal });
32875
+ if (!response.ok)
32876
+ return false;
32877
+ const statuses = await response.json();
32878
+ return ["idle", "running", "busy", "retry"].includes(statuses[sessionId]?.type ?? "");
32879
+ }
32880
+ function defaultDelay(milliseconds) {
32881
+ return new Promise((resolve4) => setTimeout(resolve4, milliseconds));
32882
+ }
32883
+ function registryKey(socketPath, workspaceId) {
32884
+ return `${socketPath}\x00${workspaceId}`;
32885
+ }
32886
+ function encodeHandle(handle) {
32887
+ return `cmux:v1:${Buffer.from(JSON.stringify(handle)).toString("base64url")}`;
32888
+ }
32889
+ function decodeHandle(value) {
32890
+ if (!value.startsWith("cmux:v1:"))
32891
+ return null;
32892
+ try {
32893
+ const parsed = JSON.parse(Buffer.from(value.slice("cmux:v1:".length), "base64url").toString());
32894
+ return parsed.v === 1 && typeof parsed.socketPath === "string" && typeof parsed.workspaceId === "string" && typeof parsed.paneId === "string" && typeof parsed.surfaceId === "string" ? parsed : null;
32895
+ } catch {
32896
+ return null;
32897
+ }
32898
+ }
32899
+ function compareVersions2(left, right) {
32900
+ const a = left.split(".").map(Number);
32901
+ const b = right.split(".").map(Number);
32902
+ for (let index = 0;index < 3; index++) {
32903
+ if (a[index] !== b[index])
32904
+ return (a[index] ?? 0) - (b[index] ?? 0);
32905
+ }
32906
+ return 0;
32907
+ }
32908
+ function parseJson(stdout) {
32909
+ try {
32910
+ return JSON.parse(stdout);
32911
+ } catch {
32912
+ return null;
32913
+ }
32914
+ }
32915
+ function asRecord(value) {
32916
+ return value && typeof value === "object" ? value : null;
32917
+ }
32918
+ function stringField(value, field) {
32919
+ const candidate = value?.[field];
32920
+ return typeof candidate === "string" ? candidate : null;
32921
+ }
32168
32922
  // src/multiplexer/herdr/index.ts
32923
+ init_compat();
32169
32924
  class HerdrMultiplexer {
32170
32925
  type = "herdr";
32171
32926
  binaryPath = null;
@@ -32174,6 +32929,7 @@ class HerdrMultiplexer {
32174
32929
  layout;
32175
32930
  paneDirection;
32176
32931
  agentAreaPaneId = null;
32932
+ spawnMutex = Promise.resolve();
32177
32933
  constructor(layout = "main-vertical", mainPaneSize = 60) {
32178
32934
  this.layout = layout;
32179
32935
  this.paneDirection = getPaneDirection(layout);
@@ -32190,6 +32946,17 @@ class HerdrMultiplexer {
32190
32946
  return !!(process.env.HERDR_ENV || process.env.HERDR_PANE_ID);
32191
32947
  }
32192
32948
  async spawnPane(sessionId, description, serverUrl, directory) {
32949
+ const prev = this.spawnMutex;
32950
+ let release2;
32951
+ this.spawnMutex = new Promise((r) => release2 = r);
32952
+ await prev;
32953
+ try {
32954
+ return await this.doSpawn(sessionId, description, serverUrl, directory);
32955
+ } finally {
32956
+ release2();
32957
+ }
32958
+ }
32959
+ async doSpawn(sessionId, description, serverUrl, directory) {
32193
32960
  const herdr = await this.getBinary();
32194
32961
  if (!herdr) {
32195
32962
  log("[herdr] spawnPane: herdr binary not found");
@@ -32900,8 +33667,15 @@ function getMultiplexer(config) {
32900
33667
  multiplexer = new HerdrMultiplexer(config.layout, config.main_pane_size);
32901
33668
  actualType = "herdr";
32902
33669
  break;
33670
+ case "cmux":
33671
+ multiplexer = new CmuxMultiplexer;
33672
+ actualType = "cmux";
33673
+ break;
32903
33674
  case "auto": {
32904
- if (process.env.TMUX) {
33675
+ if (process.env.CMUX_SOCKET_PATH && process.env.CMUX_WORKSPACE_ID && process.env.CMUX_SURFACE_ID) {
33676
+ multiplexer = new CmuxMultiplexer;
33677
+ actualType = "cmux";
33678
+ } else if (process.env.TMUX) {
32905
33679
  multiplexer = new TmuxMultiplexer(config.layout, config.main_pane_size);
32906
33680
  actualType = "tmux";
32907
33681
  } else if (process.env.ZELLIJ) {
@@ -32929,6 +33703,692 @@ function startAvailabilityCheck(config) {
32929
33703
  multiplexer.isAvailable().catch(() => {});
32930
33704
  }
32931
33705
  }
33706
+ // src/multiplexer/types.ts
33707
+ async function isServerRunning(serverUrl, timeoutMs = 3000, maxAttempts = 2) {
33708
+ const healthUrl = new URL("/health", serverUrl).toString();
33709
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
33710
+ const controller = new AbortController;
33711
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
33712
+ let response = null;
33713
+ try {
33714
+ response = await fetch(healthUrl, { signal: controller.signal }).catch(() => null);
33715
+ } finally {
33716
+ clearTimeout(timeout);
33717
+ }
33718
+ if (response?.ok) {
33719
+ return true;
33720
+ }
33721
+ if (attempt < maxAttempts) {
33722
+ await new Promise((r) => setTimeout(r, 250));
33723
+ }
33724
+ }
33725
+ return false;
33726
+ }
33727
+
33728
+ // src/multiplexer/cmux/close-policy.ts
33729
+ class CmuxClosePolicy {
33730
+ budgetMs;
33731
+ maxAttempts;
33732
+ constructor(budgetMs = 30000, maxAttempts = 4) {
33733
+ this.budgetMs = budgetMs;
33734
+ this.maxAttempts = maxAttempts;
33735
+ }
33736
+ request(reason, version, now, current) {
33737
+ if (!current || reason === "deleted" && current.reason === "idle") {
33738
+ return {
33739
+ reason,
33740
+ expectedActivityVersion: version,
33741
+ attempts: 0,
33742
+ deadline: now + this.budgetMs,
33743
+ phase: "pending",
33744
+ nextAttemptAt: now,
33745
+ cooldowns: 0
33746
+ };
33747
+ }
33748
+ return current;
33749
+ }
33750
+ activity(intent) {
33751
+ return intent?.reason === "idle" ? undefined : intent;
33752
+ }
33753
+ failed(intent, now) {
33754
+ const attempts = intent.attempts + 1;
33755
+ if (attempts >= this.maxAttempts || now >= intent.deadline) {
33756
+ const cooldowns = intent.cooldowns + 1;
33757
+ const delay2 = cooldowns === 1 ? 30000 : cooldowns === 2 ? 60000 : Infinity;
33758
+ return {
33759
+ ...intent,
33760
+ attempts,
33761
+ phase: "cooldown",
33762
+ nextAttemptAt: now + delay2,
33763
+ cooldowns
33764
+ };
33765
+ }
33766
+ return { ...intent, attempts, nextAttemptAt: now + 1000 };
33767
+ }
33768
+ resume(intent, now) {
33769
+ if (intent.phase !== "cooldown" || now < intent.nextAttemptAt)
33770
+ return intent;
33771
+ return {
33772
+ ...intent,
33773
+ attempts: 0,
33774
+ deadline: now + this.budgetMs,
33775
+ phase: "pending",
33776
+ nextAttemptAt: now
33777
+ };
33778
+ }
33779
+ complete() {
33780
+ return;
33781
+ }
33782
+ }
33783
+
33784
+ // src/multiplexer/cmux/session-state.ts
33785
+ var STORE_KEY = Symbol.for("oh-my-opencode-slim.cmux-session-store");
33786
+ function records() {
33787
+ const globalStore = globalThis;
33788
+ globalStore[STORE_KEY] ??= new Map;
33789
+ return globalStore[STORE_KEY];
33790
+ }
33791
+
33792
+ class CmuxSessionStore {
33793
+ claimCreated(record) {
33794
+ const existing = records().get(record.session);
33795
+ if (existing) {
33796
+ if (existing.directory !== record.directory || existing.lifecycle !== "orphaned" && existing.lifecycle !== "deleted")
33797
+ return false;
33798
+ existing.closeTimer?.cancel();
33799
+ Object.assign(record, existing, {
33800
+ owner: record.owner,
33801
+ closeTimer: undefined
33802
+ });
33803
+ }
33804
+ records().set(record.session, record);
33805
+ return true;
33806
+ }
33807
+ get(session2) {
33808
+ return records().get(session2);
33809
+ }
33810
+ ownedBy(owner) {
33811
+ return [...records().values()].filter((record) => record.owner === owner);
33812
+ }
33813
+ claimOrphans(owner, directory) {
33814
+ const claimed = [...records().values()].filter((record) => record.directory === directory && Boolean(record.paneId) && (record.lifecycle === "orphaned" || record.lifecycle === "deleted"));
33815
+ for (const record of claimed) {
33816
+ record.closeTimer?.cancel();
33817
+ record.closeTimer = undefined;
33818
+ record.owner = owner;
33819
+ }
33820
+ return claimed;
33821
+ }
33822
+ markAttached(session2, paneId, now) {
33823
+ const record = records().get(session2);
33824
+ if (!record)
33825
+ return;
33826
+ Object.assign(record, {
33827
+ paneId,
33828
+ attachedAt: now,
33829
+ lastActivityAt: now,
33830
+ spawnState: "attached",
33831
+ deferredSpawn: undefined
33832
+ });
33833
+ }
33834
+ markActivity(session2, now) {
33835
+ const record = records().get(session2);
33836
+ if (!record)
33837
+ return;
33838
+ record.lastActivityAt = now;
33839
+ record.activityVersion += 1;
33840
+ record.idleConsecutive = 0;
33841
+ record.statusMissingSince = undefined;
33842
+ }
33843
+ markDeleted(session2) {
33844
+ const record = records().get(session2);
33845
+ if (record)
33846
+ record.lifecycle = "deleted";
33847
+ }
33848
+ markOrphaned(session2) {
33849
+ const record = records().get(session2);
33850
+ if (record)
33851
+ record.lifecycle = "orphaned";
33852
+ }
33853
+ removeAfterConfirmedClose(session2) {
33854
+ const record = records().get(session2);
33855
+ return record?.paneId ? records().delete(session2) : false;
33856
+ }
33857
+ removeWithoutPane(session2) {
33858
+ const record = records().get(session2);
33859
+ return record && !record.paneId ? records().delete(session2) : false;
33860
+ }
33861
+ resetForTests() {
33862
+ for (const record of records().values()) {
33863
+ record.deferredSpawn?.timer?.cancel();
33864
+ record.closeTimer?.cancel();
33865
+ }
33866
+ records().clear();
33867
+ }
33868
+ }
33869
+
33870
+ // src/multiplexer/cmux/session-lifecycle.ts
33871
+ var ACTIVITY_EVENTS = new Set([
33872
+ "message.updated",
33873
+ "message.removed",
33874
+ "message.part.updated",
33875
+ "message.part.delta",
33876
+ "message.part.removed"
33877
+ ]);
33878
+ var MIN_LIFETIME_MS = 1e4;
33879
+ var IDLE_CONFIRMATIONS = 3;
33880
+
33881
+ class ServerUrlUnavailableError extends Error {
33882
+ constructor() {
33883
+ super("OpenCode server URL is unavailable");
33884
+ this.name = "ServerUrlUnavailableError";
33885
+ }
33886
+ }
33887
+
33888
+ class CmuxSessionLifecycle {
33889
+ owner;
33890
+ multiplexer;
33891
+ resolveServerUrl;
33892
+ defaultDirectory;
33893
+ backgroundJobs;
33894
+ store = new CmuxSessionStore;
33895
+ policy;
33896
+ now;
33897
+ delay;
33898
+ injectedDelay;
33899
+ deferredRetryMs;
33900
+ deferredTtlMs;
33901
+ missingGraceMs;
33902
+ closeRetryMs;
33903
+ shutdownTimeoutMs;
33904
+ serverCheck;
33905
+ fetchStatuses;
33906
+ pollTimer;
33907
+ polling = false;
33908
+ cleanupPromise;
33909
+ disposed = false;
33910
+ spawnGeneration = 0;
33911
+ constructor(owner, multiplexer, resolveServerUrl, defaultDirectory, backgroundJobs, options = {}) {
33912
+ this.owner = owner;
33913
+ this.multiplexer = multiplexer;
33914
+ this.resolveServerUrl = resolveServerUrl;
33915
+ this.defaultDirectory = defaultDirectory;
33916
+ this.backgroundJobs = backgroundJobs;
33917
+ this.now = options.now ?? Date.now;
33918
+ this.injectedDelay = Boolean(options.delay);
33919
+ this.delay = options.delay ?? ((milliseconds) => new Promise((resolve4) => setTimeout(resolve4, milliseconds)));
33920
+ this.deferredRetryMs = options.deferredRetryMs ?? 2000;
33921
+ this.deferredTtlMs = options.deferredTtlMs ?? 300000;
33922
+ this.missingGraceMs = options.missingGraceMs ?? 30000;
33923
+ this.closeRetryMs = options.closeRetryMs ?? 1000;
33924
+ this.shutdownTimeoutMs = options.shutdownTimeoutMs ?? 5000;
33925
+ this.policy = new CmuxClosePolicy(options.closeRetryTtlMs, options.closeRetryMaxAttempts);
33926
+ this.serverCheck = options.isServerRunning ?? isServerRunning;
33927
+ this.fetchStatuses = options.fetchStatuses ?? (() => this.loadStatuses());
33928
+ for (const orphan of this.store.claimOrphans(owner, defaultDirectory)) {
33929
+ if (orphan.closeIntent?.phase === "cooldown" && Number.isFinite(orphan.closeIntent.nextAttemptAt)) {
33930
+ this.scheduleCooldown(orphan);
33931
+ } else {
33932
+ orphan.closeIntent = undefined;
33933
+ this.requestClose(orphan, "cleanup");
33934
+ }
33935
+ }
33936
+ }
33937
+ async onSessionCreated(event) {
33938
+ if (this.disposed)
33939
+ return;
33940
+ if (event.type !== "session.created")
33941
+ return;
33942
+ const info = event.properties?.info;
33943
+ if (!info?.id || !info.parentID)
33944
+ return;
33945
+ const now = this.now();
33946
+ const record = {
33947
+ session: info.id,
33948
+ owner: this.owner,
33949
+ parent: info.parentID,
33950
+ title: info.title ?? "Subagent",
33951
+ directory: info.directory ?? this.defaultDirectory,
33952
+ spawnState: "known",
33953
+ lifecycle: "active",
33954
+ lastActivityAt: now,
33955
+ activityVersion: 0,
33956
+ idleConsecutive: 0
33957
+ };
33958
+ if (!this.store.claimCreated(record))
33959
+ return;
33960
+ if (record.paneId && record.lifecycle !== "active") {
33961
+ record.closeIntent = undefined;
33962
+ await this.requestClose(record, "cleanup");
33963
+ return;
33964
+ }
33965
+ await this.spawn(record);
33966
+ }
33967
+ async onSessionStatus(event) {
33968
+ if (this.disposed)
33969
+ return;
33970
+ const session2 = this.eventSession(event);
33971
+ if (!session2)
33972
+ return;
33973
+ const owned = this.store.get(session2);
33974
+ if (!owned || owned.owner !== this.owner)
33975
+ return;
33976
+ if (ACTIVITY_EVENTS.has(event.type)) {
33977
+ this.activity(session2);
33978
+ return;
33979
+ }
33980
+ const status = event.type === "session.idle" ? "idle" : event.type === "session.status" ? event.properties?.status?.type : undefined;
33981
+ if (!status)
33982
+ return;
33983
+ if (status !== "idle") {
33984
+ this.activity(session2);
33985
+ this.backgroundJobs?.clearDeferredClose(session2);
33986
+ const record = this.store.get(session2);
33987
+ if (status === "busy" && record && !record.paneId)
33988
+ await this.spawn(record);
33989
+ }
33990
+ if (owned.paneId)
33991
+ this.startPolling();
33992
+ }
33993
+ async onSessionDeleted(event) {
33994
+ if (event.type !== "session.deleted")
33995
+ return;
33996
+ const session2 = this.eventSession(event);
33997
+ if (!session2)
33998
+ return;
33999
+ const record = this.store.get(session2);
34000
+ if (!record)
34001
+ return;
34002
+ if (record.owner !== this.owner)
34003
+ return;
34004
+ this.store.markDeleted(session2);
34005
+ this.cancelDeferred(record);
34006
+ this.backgroundJobs?.clearDeferredClose(session2);
34007
+ if (!record.paneId) {
34008
+ if (!record.spawnPromise)
34009
+ this.store.removeWithoutPane(session2);
34010
+ return;
34011
+ }
34012
+ await this.requestClose(record, "deleted");
34013
+ }
34014
+ async closeSessionFromCoordinator(session2) {
34015
+ if (this.disposed)
34016
+ return;
34017
+ const record = this.store.get(session2);
34018
+ if (record?.paneId && record.owner === this.owner)
34019
+ this.startPolling();
34020
+ }
34021
+ cleanup() {
34022
+ this.cleanupPromise ??= this.runCleanup();
34023
+ return this.cleanupPromise;
34024
+ }
34025
+ pollOnce() {
34026
+ return this.poll();
34027
+ }
34028
+ async spawn(record, deferred = false) {
34029
+ if (this.disposed || record.owner !== this.owner)
34030
+ return;
34031
+ if (record.spawnState === "spawning" || record.paneId)
34032
+ return;
34033
+ const generation = this.spawnGeneration;
34034
+ const token = record.deferredSpawn?.generation;
34035
+ record.spawnState = "spawning";
34036
+ const operation = this.spawnOperation(record);
34037
+ record.spawnPromise = operation;
34038
+ const result = await operation;
34039
+ if (record.spawnPromise === operation)
34040
+ record.spawnPromise = undefined;
34041
+ const current = this.store.get(record.session);
34042
+ if (this.disposed || generation !== this.spawnGeneration) {
34043
+ const latePane = result.paneId ?? result.orphanPaneId;
34044
+ if (latePane)
34045
+ await this.closeLatePane(record, latePane);
34046
+ else if (current && !current.paneId)
34047
+ this.store.removeWithoutPane(record.session);
34048
+ return;
34049
+ }
34050
+ if (!current) {
34051
+ if (result.success && result.paneId)
34052
+ await this.adoptAndClose(record, result.paneId);
34053
+ if (result.orphanPaneId)
34054
+ await this.adoptAndClose(record, result.orphanPaneId);
34055
+ return;
34056
+ }
34057
+ if (deferred && token !== undefined && current.deferredSpawn?.generation !== token) {
34058
+ const stalePane = result.paneId ?? result.orphanPaneId;
34059
+ if (stalePane)
34060
+ await this.adoptAndClose(current, stalePane);
34061
+ return;
34062
+ }
34063
+ const paneId = result.paneId ?? result.orphanPaneId;
34064
+ if (paneId) {
34065
+ this.store.markAttached(record.session, paneId, this.now());
34066
+ if (result.orphanPaneId)
34067
+ this.store.markOrphaned(record.session);
34068
+ if (current.lifecycle !== "active" || result.orphanPaneId) {
34069
+ await this.requestClose(current, current.lifecycle === "active" ? "cleanup" : "deleted");
34070
+ } else
34071
+ this.startPolling();
34072
+ return;
34073
+ }
34074
+ current.spawnState = "failed";
34075
+ if (current.lifecycle !== "active") {
34076
+ this.store.removeWithoutPane(current.session);
34077
+ } else if (result.error === "unavailable" || result.error === "not_found" || result.error === "invalid_state") {
34078
+ this.deferSpawn(current);
34079
+ }
34080
+ }
34081
+ async spawnOperation(record) {
34082
+ const serverUrl = this.resolveServerUrl();
34083
+ if (!serverUrl) {
34084
+ log("[cmux-session-lifecycle] no valid server URL; skipping spawn");
34085
+ return { success: false, error: "unavailable" };
34086
+ }
34087
+ if (!await this.serverCheck(serverUrl)) {
34088
+ return { success: false, error: "unavailable" };
34089
+ }
34090
+ try {
34091
+ return await this.multiplexer.spawnPane(record.session, record.title, serverUrl, record.directory);
34092
+ } catch {
34093
+ return { success: false, error: "hard" };
34094
+ }
34095
+ }
34096
+ deferSpawn(record) {
34097
+ if (this.disposed || record.owner !== this.owner)
34098
+ return;
34099
+ const existing = record.deferredSpawn;
34100
+ const deferred = existing ?? {
34101
+ deadline: this.now() + this.deferredTtlMs,
34102
+ generation: 0
34103
+ };
34104
+ deferred.generation += 1;
34105
+ deferred.timer?.cancel();
34106
+ record.deferredSpawn = deferred;
34107
+ if (this.now() >= deferred.deadline) {
34108
+ this.cancelDeferred(record);
34109
+ return;
34110
+ }
34111
+ deferred.timer = this.timer(async () => {
34112
+ deferred.timer = undefined;
34113
+ if (this.disposed || this.store.get(record.session) !== record || record.lifecycle !== "active" || record.owner !== this.owner)
34114
+ return;
34115
+ await this.spawn(record, true);
34116
+ }, this.deferredRetryMs);
34117
+ }
34118
+ cancelDeferred(record) {
34119
+ record.deferredSpawn?.timer?.cancel();
34120
+ record.deferredSpawn = undefined;
34121
+ }
34122
+ activity(session2) {
34123
+ const record = this.store.get(session2);
34124
+ if (!record || record.owner !== this.owner || this.disposed)
34125
+ return;
34126
+ this.store.markActivity(session2, this.now());
34127
+ const next = this.policy.activity(record.closeIntent);
34128
+ if (!next && record.closeIntent)
34129
+ record.closeTimer?.cancel();
34130
+ record.closeIntent = next;
34131
+ if (!next)
34132
+ record.closeTimer = undefined;
34133
+ }
34134
+ async requestClose(record, reason) {
34135
+ if (!record.paneId || record.owner !== this.owner)
34136
+ return;
34137
+ if (reason === "idle" && !(this.backgroundJobs?.deferIfRunning(record.session) ?? true))
34138
+ return;
34139
+ const previous = record.closeIntent;
34140
+ record.closeIntent = this.policy.request(reason, record.activityVersion, this.now(), previous);
34141
+ if (previous !== record.closeIntent)
34142
+ record.closeTimer?.cancel();
34143
+ await this.attemptClose(record);
34144
+ }
34145
+ async attemptClose(record) {
34146
+ const intent = record.closeIntent;
34147
+ if (!intent || !record.paneId || record.owner !== this.owner || this.store.get(record.session) !== record)
34148
+ return;
34149
+ if (intent.phase === "cooldown" && this.now() < intent.nextAttemptAt) {
34150
+ this.scheduleCooldown(record);
34151
+ return;
34152
+ }
34153
+ record.closeIntent = this.policy.resume(intent, this.now());
34154
+ if (record.closeIntent !== intent)
34155
+ return this.attemptClose(record);
34156
+ if (intent.reason === "idle" && intent.expectedActivityVersion !== record.activityVersion) {
34157
+ record.closeIntent = this.policy.activity(intent);
34158
+ return;
34159
+ }
34160
+ let closed = false;
34161
+ try {
34162
+ closed = await this.multiplexer.closePane(record.paneId);
34163
+ } catch {}
34164
+ if (this.disposed || this.store.get(record.session) !== record || record.owner !== this.owner || record.closeIntent !== intent)
34165
+ return;
34166
+ const intentStillCurrent = record.closeIntent === intent;
34167
+ const idleStillCurrent = intent.reason !== "idle" || intent.expectedActivityVersion === record.activityVersion;
34168
+ if (closed) {
34169
+ record.closeTimer?.cancel();
34170
+ record.closeTimer = undefined;
34171
+ if (record.lifecycle !== "active") {
34172
+ this.store.removeAfterConfirmedClose(record.session);
34173
+ } else {
34174
+ record.paneId = undefined;
34175
+ record.spawnState = "known";
34176
+ if (intentStillCurrent)
34177
+ record.closeIntent = this.policy.complete();
34178
+ if (intent.reason === "idle" && record.owner === this.owner && !this.disposed && record.activityVersion !== intent.expectedActivityVersion) {
34179
+ await this.spawn(record);
34180
+ }
34181
+ }
34182
+ this.updatePolling();
34183
+ return;
34184
+ }
34185
+ if (!intentStillCurrent || !idleStillCurrent)
34186
+ return;
34187
+ record.closeIntent = this.policy.failed(intent, this.now());
34188
+ if (record.closeIntent.phase === "cooldown") {
34189
+ if (!Number.isFinite(record.closeIntent.nextAttemptAt))
34190
+ this.store.markOrphaned(record.session);
34191
+ this.scheduleCooldown(record);
34192
+ return;
34193
+ }
34194
+ record.closeTimer?.cancel();
34195
+ record.closeTimer = this.timer(() => this.attemptClose(record), this.closeRetryMs);
34196
+ }
34197
+ scheduleCooldown(record) {
34198
+ record.closeTimer?.cancel();
34199
+ record.closeTimer = undefined;
34200
+ const intent = record.closeIntent;
34201
+ if (!intent || !Number.isFinite(intent.nextAttemptAt) || this.disposed)
34202
+ return;
34203
+ record.closeTimer = this.timer(() => this.attemptClose(record), Math.max(0, intent.nextAttemptAt - this.now()));
34204
+ }
34205
+ async poll() {
34206
+ if (this.polling || this.disposed)
34207
+ return;
34208
+ this.polling = true;
34209
+ try {
34210
+ const statuses = await this.fetchStatuses();
34211
+ for (const record of this.store.ownedBy(this.owner)) {
34212
+ if (!record.paneId || record.lifecycle !== "active")
34213
+ continue;
34214
+ const status = statuses[record.session];
34215
+ if (!status) {
34216
+ record.statusMissingSince ??= this.now();
34217
+ if (this.now() - record.statusMissingSince < this.missingGraceMs) {
34218
+ record.idleConsecutive = 0;
34219
+ continue;
34220
+ }
34221
+ }
34222
+ if (status)
34223
+ record.statusMissingSince = undefined;
34224
+ if (status && status.type !== "idle") {
34225
+ this.activity(record.session);
34226
+ continue;
34227
+ }
34228
+ if (this.now() - (record.attachedAt ?? this.now()) < MIN_LIFETIME_MS || this.now() - record.lastActivityAt < MIN_LIFETIME_MS) {
34229
+ record.idleConsecutive = 0;
34230
+ continue;
34231
+ }
34232
+ record.idleConsecutive += 1;
34233
+ if (record.idleConsecutive < IDLE_CONFIRMATIONS)
34234
+ continue;
34235
+ const version = record.activityVersion;
34236
+ const final = await this.fetchStatuses();
34237
+ if ((final[record.session]?.type === "idle" || !final[record.session] && record.statusMissingSince !== undefined && this.now() - record.statusMissingSince >= this.missingGraceMs) && version === record.activityVersion)
34238
+ await this.requestClose(record, "idle");
34239
+ else
34240
+ this.activity(record.session);
34241
+ }
34242
+ } catch {} finally {
34243
+ this.polling = false;
34244
+ }
34245
+ }
34246
+ startPolling() {
34247
+ if (this.pollTimer || this.disposed)
34248
+ return;
34249
+ this.pollTimer = setInterval(() => void this.poll().catch(() => {
34250
+ return;
34251
+ }), POLL_INTERVAL_BACKGROUND_MS);
34252
+ this.pollTimer.unref?.();
34253
+ }
34254
+ updatePolling() {
34255
+ if (this.store.ownedBy(this.owner).some((record) => record.paneId))
34256
+ this.startPolling();
34257
+ else if (this.pollTimer) {
34258
+ clearInterval(this.pollTimer);
34259
+ this.pollTimer = undefined;
34260
+ }
34261
+ }
34262
+ async runCleanup() {
34263
+ this.disposed = true;
34264
+ this.spawnGeneration += 1;
34265
+ if (this.pollTimer)
34266
+ clearInterval(this.pollTimer);
34267
+ this.pollTimer = undefined;
34268
+ const records2 = this.store.ownedBy(this.owner);
34269
+ for (const record of records2)
34270
+ this.cancelDeferred(record);
34271
+ const pending = records2.flatMap((record) => record.spawnPromise ? [record.spawnPromise] : []);
34272
+ if (pending.length) {
34273
+ await Promise.race([
34274
+ Promise.allSettled(pending),
34275
+ this.delay(this.shutdownTimeoutMs)
34276
+ ]);
34277
+ }
34278
+ for (const record of this.store.ownedBy(this.owner)) {
34279
+ if (!record.paneId) {
34280
+ if (!record.spawnPromise)
34281
+ this.store.removeWithoutPane(record.session);
34282
+ continue;
34283
+ }
34284
+ record.closeTimer?.cancel();
34285
+ record.closeTimer = undefined;
34286
+ record.closeIntent = this.policy.request("cleanup", record.activityVersion, this.now());
34287
+ while (record.closeIntent?.phase === "pending" && this.store.get(record.session) === record && record.owner === this.owner) {
34288
+ await this.attemptCloseWithoutTimer(record);
34289
+ if (record.closeIntent?.phase === "pending")
34290
+ await this.delay(this.closeRetryMs);
34291
+ }
34292
+ if (record.closeIntent && this.store.get(record.session) === record && record.owner === this.owner)
34293
+ this.store.markOrphaned(record.session);
34294
+ }
34295
+ }
34296
+ async attemptCloseWithoutTimer(record) {
34297
+ const intent = record.closeIntent;
34298
+ if (!intent || !record.paneId)
34299
+ return;
34300
+ const paneId = record.paneId;
34301
+ let closed = false;
34302
+ try {
34303
+ closed = await this.multiplexer.closePane(paneId);
34304
+ } catch {}
34305
+ if (this.store.get(record.session) !== record || record.owner !== this.owner || record.closeIntent !== intent || record.paneId !== paneId)
34306
+ return;
34307
+ if (closed) {
34308
+ record.closeIntent = undefined;
34309
+ this.store.removeAfterConfirmedClose(record.session);
34310
+ } else
34311
+ record.closeIntent = this.policy.failed(intent, this.now());
34312
+ }
34313
+ async adoptAndClose(record, paneId) {
34314
+ if (!this.store.get(record.session))
34315
+ this.store.claimCreated(record);
34316
+ this.store.markAttached(record.session, paneId, this.now());
34317
+ this.store.markOrphaned(record.session);
34318
+ await this.requestClose(record, "cleanup");
34319
+ }
34320
+ async closeLatePane(source, paneId) {
34321
+ const existing = this.store.get(source.session);
34322
+ if (existing && existing.owner !== this.owner) {
34323
+ let closed = false;
34324
+ try {
34325
+ closed = await this.multiplexer.closePane(paneId);
34326
+ } catch {}
34327
+ if (!closed)
34328
+ this.trackStalePane(source, paneId);
34329
+ return;
34330
+ }
34331
+ const record = existing ?? source;
34332
+ if (!existing)
34333
+ this.store.claimCreated(record);
34334
+ record.paneId = paneId;
34335
+ record.spawnState = "attached";
34336
+ record.lifecycle = "orphaned";
34337
+ record.closeIntent = this.policy.request("cleanup", record.activityVersion, this.now());
34338
+ while (record.closeIntent?.phase === "pending") {
34339
+ await this.attemptCloseWithoutTimer(record);
34340
+ if (record.closeIntent?.phase === "pending")
34341
+ await this.delay(this.closeRetryMs);
34342
+ }
34343
+ }
34344
+ trackStalePane(source, paneId) {
34345
+ const session2 = `${source.session}\x00late\x00${paneId}`;
34346
+ this.store.claimCreated({
34347
+ session: session2,
34348
+ owner: this.owner,
34349
+ parent: source.parent,
34350
+ title: source.title,
34351
+ directory: source.directory,
34352
+ paneId,
34353
+ spawnState: "attached",
34354
+ lifecycle: "orphaned",
34355
+ attachedAt: this.now(),
34356
+ lastActivityAt: source.lastActivityAt,
34357
+ activityVersion: source.activityVersion,
34358
+ idleConsecutive: 0
34359
+ });
34360
+ }
34361
+ eventSession(event) {
34362
+ return event.properties?.sessionID ?? event.properties?.info?.sessionID ?? event.properties?.part?.sessionID ?? event.properties?.info?.id;
34363
+ }
34364
+ timer(callback, milliseconds) {
34365
+ let cancelled = false;
34366
+ if (this.injectedDelay) {
34367
+ this.delay(milliseconds).then(() => {
34368
+ if (!cancelled)
34369
+ callback();
34370
+ });
34371
+ return { cancel: () => cancelled = true };
34372
+ }
34373
+ const timer = setTimeout(() => void callback(), milliseconds);
34374
+ timer.unref?.();
34375
+ return { cancel: () => clearTimeout(timer) };
34376
+ }
34377
+ async loadStatuses() {
34378
+ const serverUrl = this.resolveServerUrl();
34379
+ if (!serverUrl) {
34380
+ log("[cmux-session-lifecycle] no valid server URL; skipping poll");
34381
+ throw new ServerUrlUnavailableError;
34382
+ }
34383
+ const response = await fetch(new URL("/session/status", serverUrl), {
34384
+ signal: AbortSignal.timeout(2000)
34385
+ });
34386
+ if (!response.ok)
34387
+ throw new Error(`session status failed: ${response.status}`);
34388
+ return await response.json();
34389
+ }
34390
+ }
34391
+
32932
34392
  // src/multiplexer/session-manager.ts
32933
34393
  var SHARED_STATE_KEY = Symbol.for("oh-my-opencode-slim.multiplexer-session-manager.state");
32934
34394
  function getSharedState() {
@@ -32941,10 +34401,53 @@ function getSharedState() {
32941
34401
  };
32942
34402
  return globalWithState[SHARED_STATE_KEY];
32943
34403
  }
34404
+ function validServerUrl(value) {
34405
+ if (typeof value !== "string" && !(value instanceof URL))
34406
+ return null;
34407
+ try {
34408
+ const url = new URL(value.toString());
34409
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : null;
34410
+ } catch {
34411
+ return null;
34412
+ }
34413
+ }
34414
+ function clientBaseUrl(client) {
34415
+ try {
34416
+ if (!client || typeof client !== "object" || !("_client" in client))
34417
+ return null;
34418
+ const internal = client._client;
34419
+ if (!internal || typeof internal !== "object" || !("getConfig" in internal))
34420
+ return null;
34421
+ const getConfig = internal.getConfig;
34422
+ if (typeof getConfig !== "function")
34423
+ return null;
34424
+ const config = getConfig.call(internal);
34425
+ if (!config || typeof config !== "object" || !("baseUrl" in config))
34426
+ return null;
34427
+ return validServerUrl(config.baseUrl);
34428
+ } catch {
34429
+ return null;
34430
+ }
34431
+ }
34432
+ function createServerUrlResolver(ctx) {
34433
+ return () => {
34434
+ try {
34435
+ const serverUrl = validServerUrl(ctx.serverUrl);
34436
+ if (serverUrl)
34437
+ return serverUrl;
34438
+ } catch {}
34439
+ try {
34440
+ return clientBaseUrl(ctx.client);
34441
+ } catch {
34442
+ return null;
34443
+ }
34444
+ };
34445
+ }
34446
+
32944
34447
  class MultiplexerSessionManager {
32945
34448
  backgroundJobBoard;
32946
34449
  instanceId = Math.random().toString(36).slice(2, 8);
32947
- serverUrl;
34450
+ resolveServerUrl;
32948
34451
  directory;
32949
34452
  multiplexer = null;
32950
34453
  sessions;
@@ -32953,7 +34456,8 @@ class MultiplexerSessionManager {
32953
34456
  closingSessions;
32954
34457
  pollInterval;
32955
34458
  enabled = false;
32956
- constructor(ctx, config, backgroundJobBoard) {
34459
+ cmuxLifecycle;
34460
+ constructor(ctx, config, backgroundJobBoard, options = {}) {
32957
34461
  this.backgroundJobBoard = backgroundJobBoard;
32958
34462
  const sharedState = getSharedState();
32959
34463
  this.sessions = sharedState.sessions;
@@ -32961,20 +34465,24 @@ class MultiplexerSessionManager {
32961
34465
  this.spawningSessions = sharedState.spawningSessions;
32962
34466
  this.closingSessions = sharedState.closingSessions;
32963
34467
  this.directory = ctx.directory;
32964
- const defaultPort = process.env.OPENCODE_PORT ?? "4096";
32965
- this.serverUrl = ctx.serverUrl?.toString() ?? `http://localhost:${defaultPort}`;
34468
+ this.resolveServerUrl = createServerUrlResolver(ctx);
32966
34469
  this.multiplexer = getMultiplexer(config);
32967
34470
  this.enabled = config.type !== "none" && this.multiplexer !== null && this.multiplexer.isInsideSession();
34471
+ if (this.enabled && this.multiplexer?.type === "cmux") {
34472
+ this.cmuxLifecycle = new CmuxSessionLifecycle(this.instanceId, this.multiplexer, this.resolveServerUrl, this.directory, this.backgroundJobBoard, options);
34473
+ }
32968
34474
  log("[multiplexer-session-manager] initialized", {
32969
34475
  instanceId: this.instanceId,
32970
34476
  enabled: this.enabled,
32971
34477
  type: config.type,
32972
- serverUrl: this.serverUrl,
34478
+ serverUrl: "dynamic",
32973
34479
  trackedSessions: this.sessions.size,
32974
34480
  knownSessions: this.knownSessions.size
32975
34481
  });
32976
34482
  }
32977
34483
  async onSessionCreated(event) {
34484
+ if (this.cmuxLifecycle)
34485
+ return this.cmuxLifecycle.onSessionCreated(event);
32978
34486
  if (!this.enabled || !this.multiplexer)
32979
34487
  return;
32980
34488
  if (event.type !== "session.created")
@@ -33006,11 +34514,19 @@ class MultiplexerSessionManager {
33006
34514
  });
33007
34515
  this.spawningSessions.add(sessionId);
33008
34516
  try {
33009
- const serverRunning = await isServerRunning(this.serverUrl);
34517
+ const serverUrl = this.resolveServerUrl();
34518
+ if (!serverUrl) {
34519
+ log("[multiplexer-session-manager] no valid server URL, skipping spawn", {
34520
+ instanceId: this.instanceId,
34521
+ sessionId
34522
+ });
34523
+ return;
34524
+ }
34525
+ const serverRunning = await isServerRunning(serverUrl);
33010
34526
  if (!serverRunning) {
33011
34527
  log("[multiplexer-session-manager] server not running, skipping", {
33012
34528
  instanceId: this.instanceId,
33013
- serverUrl: this.serverUrl
34529
+ serverUrl
33014
34530
  });
33015
34531
  return;
33016
34532
  }
@@ -33023,7 +34539,7 @@ class MultiplexerSessionManager {
33023
34539
  title,
33024
34540
  instanceId: this.instanceId
33025
34541
  });
33026
- const paneResult = await this.multiplexer.spawnPane(sessionId, title, this.serverUrl, directory).catch((err) => {
34542
+ const paneResult = await this.multiplexer.spawnPane(sessionId, title, serverUrl, directory).catch((err) => {
33027
34543
  log("[multiplexer-session-manager] failed to spawn pane", {
33028
34544
  instanceId: this.instanceId,
33029
34545
  error: String(err)
@@ -33060,6 +34576,8 @@ class MultiplexerSessionManager {
33060
34576
  }
33061
34577
  }
33062
34578
  async onSessionStatus(event) {
34579
+ if (this.cmuxLifecycle)
34580
+ return this.cmuxLifecycle.onSessionStatus(event);
33063
34581
  if (!this.enabled)
33064
34582
  return;
33065
34583
  if (event.type === "session.idle") {
@@ -33112,6 +34630,8 @@ class MultiplexerSessionManager {
33112
34630
  }
33113
34631
  }
33114
34632
  async onSessionDeleted(event) {
34633
+ if (this.cmuxLifecycle)
34634
+ return this.cmuxLifecycle.onSessionDeleted(event);
33115
34635
  if (!this.enabled)
33116
34636
  return;
33117
34637
  if (event.type !== "session.deleted")
@@ -33147,6 +34667,8 @@ class MultiplexerSessionManager {
33147
34667
  }
33148
34668
  }
33149
34669
  async pollSessions() {
34670
+ if (this.cmuxLifecycle)
34671
+ return this.cmuxLifecycle.pollOnce();
33150
34672
  if (this.sessions.size === 0) {
33151
34673
  this.stopPolling();
33152
34674
  return;
@@ -33181,7 +34703,14 @@ class MultiplexerSessionManager {
33181
34703
  }
33182
34704
  }
33183
34705
  async fetchSessionStatuses() {
33184
- const url = new URL("/session/status", this.serverUrl);
34706
+ const serverUrl = this.resolveServerUrl();
34707
+ if (!serverUrl) {
34708
+ log("[multiplexer-session-manager] no valid server URL, skipping poll", {
34709
+ instanceId: this.instanceId
34710
+ });
34711
+ return {};
34712
+ }
34713
+ const url = new URL("/session/status", serverUrl);
33185
34714
  const response = await fetch(url, { signal: AbortSignal.timeout(2000) });
33186
34715
  if (!response.ok) {
33187
34716
  throw new Error(`session status request failed: ${response.status} ${response.statusText}`);
@@ -33283,11 +34812,19 @@ class MultiplexerSessionManager {
33283
34812
  return;
33284
34813
  this.spawningSessions.add(sessionId);
33285
34814
  try {
33286
- const serverRunning = await isServerRunning(this.serverUrl);
34815
+ const serverUrl = this.resolveServerUrl();
34816
+ if (!serverUrl) {
34817
+ log("[multiplexer-session-manager] no valid server URL, skipping respawn", {
34818
+ instanceId: this.instanceId,
34819
+ sessionId
34820
+ });
34821
+ return;
34822
+ }
34823
+ const serverRunning = await isServerRunning(serverUrl);
33287
34824
  if (!serverRunning) {
33288
34825
  log("[multiplexer-session-manager] server not running, skipping busy respawn", {
33289
34826
  instanceId: this.instanceId,
33290
- serverUrl: this.serverUrl,
34827
+ serverUrl,
33291
34828
  sessionId
33292
34829
  });
33293
34830
  return;
@@ -33301,7 +34838,7 @@ class MultiplexerSessionManager {
33301
34838
  parentId: known.parentId,
33302
34839
  title: known.title
33303
34840
  });
33304
- const paneResult = await this.multiplexer.spawnPane(sessionId, known.title, this.serverUrl, known.directory).catch((err) => {
34841
+ const paneResult = await this.multiplexer.spawnPane(sessionId, known.title, serverUrl, known.directory).catch((err) => {
33305
34842
  log("[multiplexer-session-manager] failed to respawn pane", {
33306
34843
  instanceId: this.instanceId,
33307
34844
  error: String(err)
@@ -33358,11 +34895,15 @@ class MultiplexerSessionManager {
33358
34895
  return this.backgroundJobBoard?.deferIfRunning(sessionId) ?? true;
33359
34896
  }
33360
34897
  async closeSessionFromCoordinator(sessionId) {
34898
+ if (this.cmuxLifecycle)
34899
+ return this.cmuxLifecycle.closeSessionFromCoordinator(sessionId);
33361
34900
  if (!this.enabled)
33362
34901
  return;
33363
34902
  await this.closeSession(sessionId, "idle", true);
33364
34903
  }
33365
34904
  async cleanup() {
34905
+ if (this.cmuxLifecycle)
34906
+ return this.cmuxLifecycle.cleanup();
33366
34907
  this.stopPolling();
33367
34908
  if (this.closingSessions.size > 0) {
33368
34909
  await Promise.all(this.closingSessions.values());
@@ -33384,27 +34925,10 @@ class MultiplexerSessionManager {
33384
34925
  this.closingSessions.clear();
33385
34926
  log("[multiplexer-session-manager] cleanup complete");
33386
34927
  }
33387
- }
33388
- // src/multiplexer/types.ts
33389
- async function isServerRunning(serverUrl, timeoutMs = 3000, maxAttempts = 2) {
33390
- const healthUrl = new URL("/health", serverUrl).toString();
33391
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
33392
- const controller = new AbortController;
33393
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
33394
- let response = null;
33395
- try {
33396
- response = await fetch(healthUrl, { signal: controller.signal }).catch(() => null);
33397
- } finally {
33398
- clearTimeout(timeout);
33399
- }
33400
- if (response?.ok) {
33401
- return true;
33402
- }
33403
- if (attempt < maxAttempts) {
33404
- await new Promise((r) => setTimeout(r, 250));
33405
- }
34928
+ async cleanupOnInstanceDisposed() {
34929
+ if (this.cmuxLifecycle)
34930
+ await this.cmuxLifecycle.cleanup();
33406
34931
  }
33407
- return false;
33408
34932
  }
33409
34933
  // src/tools/acp-run.ts
33410
34934
  import { spawn as spawn3 } from "node:child_process";
@@ -33605,7 +35129,7 @@ class AcpClient {
33605
35129
  return;
33606
35130
  this.lastUpdate = Date.now();
33607
35131
  const update = message.params?.update;
33608
- if (!isRecord2(update))
35132
+ if (!isRecord3(update))
33609
35133
  return;
33610
35134
  collectText(update, this.chunks);
33611
35135
  }
@@ -33685,16 +35209,16 @@ function createAcpRunTool(agents = {}) {
33685
35209
  });
33686
35210
  }
33687
35211
  function readSessionId(value) {
33688
- if (!isRecord2(value) || typeof value.sessionId !== "string") {
35212
+ if (!isRecord3(value) || typeof value.sessionId !== "string") {
33689
35213
  throw new Error("ACP agent did not return a sessionId");
33690
35214
  }
33691
35215
  return value.sessionId;
33692
35216
  }
33693
35217
  function readAuthMethods(value) {
33694
- if (!isRecord2(value) || !Array.isArray(value.authMethods))
35218
+ if (!isRecord3(value) || !Array.isArray(value.authMethods))
33695
35219
  return [];
33696
35220
  const methods = value.authMethods;
33697
- return methods.filter(isRecord2);
35221
+ return methods.filter(isRecord3);
33698
35222
  }
33699
35223
  function rpcError(error) {
33700
35224
  const err = new Error(error.message ?? "ACP request failed");
@@ -33708,11 +35232,11 @@ function isAuthError(error) {
33708
35232
  const meta = error;
33709
35233
  return meta.code === -32001 || error.message.toLowerCase().includes("auth_required") || error.message.toLowerCase().includes("auth required");
33710
35234
  }
33711
- function isRecord2(value) {
35235
+ function isRecord3(value) {
33712
35236
  return typeof value === "object" && value !== null && !Array.isArray(value);
33713
35237
  }
33714
35238
  function readPermissionTitle(params) {
33715
- const tool2 = isRecord2(params?.toolCall) ? params.toolCall : undefined;
35239
+ const tool2 = isRecord3(params?.toolCall) ? params.toolCall : undefined;
33716
35240
  if (typeof tool2?.title === "string")
33717
35241
  return tool2.title;
33718
35242
  if (typeof params?.permission === "string")
@@ -33721,7 +35245,7 @@ function readPermissionTitle(params) {
33721
35245
  }
33722
35246
  function selectPermissionOption(params, mode) {
33723
35247
  const options = Array.isArray(params?.options) ? params.options : [];
33724
- const choices = options.filter(isRecord2).filter((item) => typeof item.optionId === "string");
35248
+ const choices = options.filter(isRecord3).filter((item) => typeof item.optionId === "string");
33725
35249
  const reject = choices.find((item) => typeof item.kind === "string" && item.kind.startsWith("reject"));
33726
35250
  if (mode === "reject")
33727
35251
  return reject?.optionId;
@@ -33738,7 +35262,7 @@ function collectText(update, chunks) {
33738
35262
  function readText(value) {
33739
35263
  if (typeof value === "string")
33740
35264
  return value;
33741
- if (isRecord2(value) && typeof value.text === "string")
35265
+ if (isRecord3(value) && typeof value.text === "string")
33742
35266
  return value.text;
33743
35267
  return;
33744
35268
  }
@@ -33747,15 +35271,15 @@ import { tool as tool2 } from "@opencode-ai/plugin";
33747
35271
 
33748
35272
  // src/tools/ast-grep/cli.ts
33749
35273
  init_compat();
33750
- import { existsSync as existsSync11 } from "node:fs";
35274
+ import { existsSync as existsSync12 } from "node:fs";
33751
35275
 
33752
35276
  // src/tools/ast-grep/constants.ts
33753
- import { existsSync as existsSync10, statSync as statSync6 } from "node:fs";
35277
+ import { existsSync as existsSync11, statSync as statSync6 } from "node:fs";
33754
35278
  import { createRequire as createRequire3 } from "node:module";
33755
35279
  import { dirname as dirname10, join as join15 } from "node:path";
33756
35280
 
33757
35281
  // src/tools/ast-grep/downloader.ts
33758
- import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "node:fs";
35282
+ import { chmodSync as chmodSync2, existsSync as existsSync10, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "node:fs";
33759
35283
  import { createRequire as createRequire2 } from "node:module";
33760
35284
  import { homedir as homedir6 } from "node:os";
33761
35285
  import { join as join14 } from "node:path";
@@ -33795,7 +35319,7 @@ function getBinaryName() {
33795
35319
  }
33796
35320
  function getCachedBinaryPath() {
33797
35321
  const binaryPath = join14(getCacheDir2(), getBinaryName());
33798
- return existsSync9(binaryPath) ? binaryPath : null;
35322
+ return existsSync10(binaryPath) ? binaryPath : null;
33799
35323
  }
33800
35324
  async function downloadAstGrep(version = DEFAULT_VERSION) {
33801
35325
  const platformKey = `${process.platform}-${process.arch}`;
@@ -33807,7 +35331,7 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
33807
35331
  const cacheDir = getCacheDir2();
33808
35332
  const binaryName = getBinaryName();
33809
35333
  const binaryPath = join14(cacheDir, binaryName);
33810
- if (existsSync9(binaryPath)) {
35334
+ if (existsSync10(binaryPath)) {
33811
35335
  return binaryPath;
33812
35336
  }
33813
35337
  const { arch, os: os6 } = platformInfo;
@@ -33815,7 +35339,7 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
33815
35339
  const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`;
33816
35340
  console.log(`[oh-my-opencode-slim] Downloading ast-grep binary...`);
33817
35341
  try {
33818
- if (!existsSync9(cacheDir)) {
35342
+ if (!existsSync10(cacheDir)) {
33819
35343
  mkdirSync7(cacheDir, { recursive: true });
33820
35344
  }
33821
35345
  const response = await fetch(downloadUrl, { redirect: "follow" });
@@ -33826,10 +35350,10 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
33826
35350
  const arrayBuffer = await response.arrayBuffer();
33827
35351
  await crossWrite(archivePath, arrayBuffer);
33828
35352
  await extractZip(archivePath, cacheDir);
33829
- if (existsSync9(archivePath)) {
35353
+ if (existsSync10(archivePath)) {
33830
35354
  unlinkSync5(archivePath);
33831
35355
  }
33832
- if (process.platform !== "win32" && existsSync9(binaryPath)) {
35356
+ if (process.platform !== "win32" && existsSync10(binaryPath)) {
33833
35357
  chmodSync2(binaryPath, 493);
33834
35358
  }
33835
35359
  console.log(`[oh-my-opencode-slim] ast-grep binary ready.`);
@@ -33912,7 +35436,7 @@ function findSgCliPathSync() {
33912
35436
  const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
33913
35437
  const cliDir = dirname10(cliPkgPath);
33914
35438
  const sgPath = join15(cliDir, binaryName);
33915
- if (existsSync10(sgPath) && isValidBinary(sgPath)) {
35439
+ if (existsSync11(sgPath) && isValidBinary(sgPath)) {
33916
35440
  return sgPath;
33917
35441
  }
33918
35442
  } catch {}
@@ -33924,7 +35448,7 @@ function findSgCliPathSync() {
33924
35448
  const pkgDir = dirname10(pkgPath);
33925
35449
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
33926
35450
  const binaryPath = join15(pkgDir, astGrepName);
33927
- if (existsSync10(binaryPath) && isValidBinary(binaryPath)) {
35451
+ if (existsSync11(binaryPath) && isValidBinary(binaryPath)) {
33928
35452
  return binaryPath;
33929
35453
  }
33930
35454
  } catch {}
@@ -33932,7 +35456,7 @@ function findSgCliPathSync() {
33932
35456
  if (process.platform === "darwin") {
33933
35457
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
33934
35458
  for (const path18 of homebrewPaths) {
33935
- if (existsSync10(path18) && isValidBinary(path18)) {
35459
+ if (existsSync11(path18) && isValidBinary(path18)) {
33936
35460
  return path18;
33937
35461
  }
33938
35462
  }
@@ -33961,7 +35485,7 @@ var DEFAULT_MAX_MATCHES = 500;
33961
35485
  var initPromise = null;
33962
35486
  async function getAstGrepPath() {
33963
35487
  const currentPath = getSgCliPath();
33964
- if (currentPath !== "sg" && existsSync11(currentPath)) {
35488
+ if (currentPath !== "sg" && existsSync12(currentPath)) {
33965
35489
  return currentPath;
33966
35490
  }
33967
35491
  if (initPromise) {
@@ -33969,7 +35493,7 @@ async function getAstGrepPath() {
33969
35493
  }
33970
35494
  initPromise = (async () => {
33971
35495
  const syncPath = findSgCliPathSync();
33972
- if (syncPath && existsSync11(syncPath)) {
35496
+ if (syncPath && existsSync12(syncPath)) {
33973
35497
  setSgCliPath(syncPath);
33974
35498
  return syncPath;
33975
35499
  }
@@ -34008,7 +35532,7 @@ async function runSg(options) {
34008
35532
  const paths2 = options.paths && options.paths.length > 0 ? options.paths : ["."];
34009
35533
  args.push(...paths2);
34010
35534
  let cliPath = getSgCliPath();
34011
- if (!existsSync11(cliPath) && cliPath !== "sg") {
35535
+ if (!existsSync12(cliPath) && cliPath !== "sg") {
34012
35536
  const downloadedPath = await getAstGrepPath();
34013
35537
  if (downloadedPath) {
34014
35538
  cliPath = downloadedPath;
@@ -34620,7 +36144,7 @@ async function getSessionStatus(client, taskID) {
34620
36144
  try {
34621
36145
  const result = await client.session.status();
34622
36146
  const data = result.data;
34623
- if (!isRecord(data)) {
36147
+ if (!isRecord2(data)) {
34624
36148
  return { status: undefined, source: "invalid-data", keys: [] };
34625
36149
  }
34626
36150
  const keys = Object.keys(data).slice(0, 20);
@@ -34628,14 +36152,14 @@ async function getSessionStatus(client, taskID) {
34628
36152
  if (item === undefined) {
34629
36153
  return { status: "idle", source: "missing-from-map", keys };
34630
36154
  }
34631
- if (isRecord(item) && typeof item.type === "string") {
36155
+ if (isRecord2(item) && typeof item.type === "string") {
34632
36156
  return { status: item.type, source: "task-map-entry", keys };
34633
36157
  }
34634
36158
  if (typeof data.type === "string") {
34635
36159
  return { status: data.type, source: "legacy-data-type", keys };
34636
36160
  }
34637
36161
  const nested = data.status;
34638
- if (isRecord(nested) && typeof nested.type === "string") {
36162
+ if (isRecord2(nested) && typeof nested.type === "string") {
34639
36163
  return { status: nested.type, source: "legacy-data-status", keys };
34640
36164
  }
34641
36165
  return { status: undefined, source: "unknown-shape", keys };
@@ -34664,7 +36188,7 @@ async function getSessionParentID(client, taskID) {
34664
36188
  try {
34665
36189
  const response = await session2.get({ path: { id: taskID } });
34666
36190
  const data = response.data;
34667
- if (!isRecord(data))
36191
+ if (!isRecord2(data))
34668
36192
  return;
34669
36193
  const parentID = data.parentID;
34670
36194
  return typeof parentID === "string" ? parentID : undefined;
@@ -34757,6 +36281,7 @@ Returns the councillor responses with a summary footer.`,
34757
36281
  import * as fs10 from "node:fs";
34758
36282
 
34759
36283
  // src/tui-state.ts
36284
+ import { createHash as createHash4 } from "node:crypto";
34760
36285
  import * as fs9 from "node:fs";
34761
36286
  import * as os6 from "node:os";
34762
36287
  import * as path18 from "node:path";
@@ -34765,8 +36290,11 @@ var STATE_FILE = "tui-state.json";
34765
36290
  function dataDir() {
34766
36291
  return process.env.XDG_DATA_HOME ?? path18.join(os6.homedir(), ".local", "share");
34767
36292
  }
34768
- function getTuiStatePath() {
34769
- return path18.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
36293
+ function projectScope(projectDir) {
36294
+ return createHash4("sha256").update(path18.resolve(projectDir)).digest("hex").slice(0, 12);
36295
+ }
36296
+ function getTuiStatePath(projectDir) {
36297
+ return path18.join(dataDir(), "opencode", "storage", STATE_DIR, projectScope(projectDir), STATE_FILE);
34770
36298
  }
34771
36299
  function emptySnapshot() {
34772
36300
  return {
@@ -34787,42 +36315,42 @@ function parseSnapshot(value) {
34787
36315
  agentVariants: parsed.agentVariants ?? {}
34788
36316
  };
34789
36317
  }
34790
- function readTuiSnapshot() {
36318
+ function readTuiSnapshot(projectDir) {
34791
36319
  try {
34792
- return parseSnapshot(fs9.readFileSync(getTuiStatePath(), "utf8"));
36320
+ return parseSnapshot(fs9.readFileSync(getTuiStatePath(projectDir), "utf8"));
34793
36321
  } catch {
34794
36322
  return emptySnapshot();
34795
36323
  }
34796
36324
  }
34797
- async function readTuiSnapshotAsync() {
36325
+ async function readTuiSnapshotAsync(projectDir) {
34798
36326
  try {
34799
- return parseSnapshot(await fs9.promises.readFile(getTuiStatePath(), "utf8"));
36327
+ return parseSnapshot(await fs9.promises.readFile(getTuiStatePath(projectDir), "utf8"));
34800
36328
  } catch {
34801
36329
  return emptySnapshot();
34802
36330
  }
34803
36331
  }
34804
- function writeTuiSnapshot(snapshot) {
36332
+ function writeTuiSnapshot(snapshot, projectDir) {
34805
36333
  try {
34806
- const filePath = getTuiStatePath();
36334
+ const filePath = getTuiStatePath(projectDir);
34807
36335
  fs9.mkdirSync(path18.dirname(filePath), { recursive: true });
34808
36336
  fs9.writeFileSync(filePath, `${JSON.stringify(snapshot)}
34809
36337
  `);
34810
36338
  } catch {}
34811
36339
  }
34812
- function updateSnapshot(mutator) {
34813
- const snapshot = readTuiSnapshot();
36340
+ function updateSnapshot(projectDir, mutator) {
36341
+ const snapshot = readTuiSnapshot(projectDir);
34814
36342
  mutator(snapshot);
34815
36343
  snapshot.updatedAt = Date.now();
34816
- writeTuiSnapshot(snapshot);
36344
+ writeTuiSnapshot(snapshot, projectDir);
34817
36345
  }
34818
- function recordTuiAgentModels(input) {
34819
- updateSnapshot((snapshot) => {
36346
+ function recordTuiAgentModels(input, projectDir) {
36347
+ updateSnapshot(projectDir, (snapshot) => {
34820
36348
  snapshot.agentModels = { ...input.agentModels };
34821
36349
  snapshot.agentVariants = { ...input.agentVariants ?? {} };
34822
36350
  });
34823
36351
  }
34824
- function recordTuiAgentModel(input) {
34825
- updateSnapshot((snapshot) => {
36352
+ function recordTuiAgentModel(input, projectDir) {
36353
+ updateSnapshot(projectDir, (snapshot) => {
34826
36354
  snapshot.agentModels[input.agentName] = input.model;
34827
36355
  if (input.variant !== undefined) {
34828
36356
  if (input.variant === null) {
@@ -34900,7 +36428,7 @@ function createPresetManager(ctx, config) {
34900
36428
  `);
34901
36429
  }
34902
36430
  } catch {}
34903
- const snapshot = readTuiSnapshot();
36431
+ const snapshot = readTuiSnapshot(ctx.directory);
34904
36432
  const agentModels = { ...snapshot.agentModels };
34905
36433
  const agentVariants = { ...snapshot.agentVariants };
34906
36434
  for (const [agentName, agentConfig] of Object.entries(agentUpdates)) {
@@ -34913,7 +36441,7 @@ function createPresetManager(ctx, config) {
34913
36441
  delete agentVariants[agentName];
34914
36442
  }
34915
36443
  }
34916
- recordTuiAgentModels({ agentModels, agentVariants });
36444
+ recordTuiAgentModels({ agentModels, agentVariants }, ctx.directory);
34917
36445
  activePreset = presetName;
34918
36446
  const summaryParts = [];
34919
36447
  for (const [name, cfg] of Object.entries(agentUpdates)) {
@@ -35878,9 +37406,6 @@ function cleanFetchedMarkdown(input) {
35878
37406
  function cleanFetchedText(input) {
35879
37407
  return trimBlankRuns(input);
35880
37408
  }
35881
- function escapeHtml2(input) {
35882
- return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
35883
- }
35884
37409
  function withTruncationMarker(content, format, truncated) {
35885
37410
  if (!truncated)
35886
37411
  return content;
@@ -35919,7 +37444,7 @@ ${content}`;
35919
37444
  }
35920
37445
  function renderMessageForFormat(content, format) {
35921
37446
  if (format === "html")
35922
- return `<pre>${escapeHtml2(content)}</pre>`;
37447
+ return `<pre>${escapeHtml(content)}</pre>`;
35923
37448
  return content;
35924
37449
  }
35925
37450
  function buildRedirectResultMessage(originalUrl, redirectUrl, statusCode) {
@@ -36016,22 +37541,9 @@ async function extractFromHtml(html, finalUrl, extractMain) {
36016
37541
  headings
36017
37542
  };
36018
37543
  }
36019
- function parseFrontmatterBlock(content) {
36020
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
36021
- if (!match)
36022
- return;
36023
- const result = {};
36024
- for (const line of match[1].split(/\r?\n/)) {
36025
- const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.+?)\s*$/);
36026
- if (!kv)
36027
- continue;
36028
- result[kv[1]] = kv[2].replace(/^(['"])(.*)\1$/, "$2");
36029
- }
36030
- return result;
36031
- }
36032
37544
  function inferCanonicalUrlFromText(content, finalUrl) {
36033
- const frontmatter2 = parseFrontmatterBlock(content);
36034
- const raw = frontmatter2?.url;
37545
+ const frontmatterData = parseFrontmatter(content);
37546
+ const raw = frontmatterData?.url;
36035
37547
  if (!raw)
36036
37548
  return;
36037
37549
  try {
@@ -36598,7 +38110,7 @@ function isInvalidLlmsResult(fetchResult) {
36598
38110
  }
36599
38111
 
36600
38112
  // src/tools/smartfetch/secondary-model.ts
36601
- import { existsSync as existsSync12 } from "node:fs";
38113
+ import { existsSync as existsSync13 } from "node:fs";
36602
38114
  import { readFile as readFile4 } from "node:fs/promises";
36603
38115
  import path21 from "node:path";
36604
38116
  function parseModelRef(value) {
@@ -36627,7 +38139,7 @@ function pickAgentModelRef(value) {
36627
38139
  function findPreferredOpenCodeConfigPath(baseDir) {
36628
38140
  for (const file of ["opencode.jsonc", "opencode.json"]) {
36629
38141
  const fullPath = path21.join(baseDir, file);
36630
- if (existsSync12(fullPath))
38142
+ if (existsSync13(fullPath))
36631
38143
  return fullPath;
36632
38144
  }
36633
38145
  return;
@@ -36743,7 +38255,7 @@ async function deleteSessionSafely(client, sessionId, directory) {
36743
38255
  console.warn(`[smartfetch] Failed to clean up secondary session ${sessionId} ` + `after ${SESSION_DELETE_RETRIES} attempts: ` + (error instanceof Error ? error.message : String(error)));
36744
38256
  return;
36745
38257
  }
36746
- await new Promise((resolve4) => setTimeout(resolve4, _testConfig.deleteRetryDelayMs));
38258
+ await new Promise((resolve5) => setTimeout(resolve5, _testConfig.deleteRetryDelayMs));
36747
38259
  }
36748
38260
  }
36749
38261
  }
@@ -37297,7 +38809,7 @@ function createWebfetchTool(pluginCtx, options = {}) {
37297
38809
  secondary_model: `${secondaryRun.model.providerID}/${secondaryRun.model.modelID}`
37298
38810
  }) : "";
37299
38811
  const secondaryRaw = secondaryRun.text || "No response from secondary model.";
37300
- const secondaryContent = args.format === "html" ? withTruncationMarker(`<pre>${escapeHtml2(secondaryRaw)}</pre>`, "html", false) : withTruncationMarker(secondaryRaw, args.format, false);
38812
+ const secondaryContent = args.format === "html" ? withTruncationMarker(`<pre>${escapeHtml(secondaryRaw)}</pre>`, "html", false) : withTruncationMarker(secondaryRaw, args.format, false);
37301
38813
  return joinRenderedContent(metadataWithSecondary, secondaryContent, args.format);
37302
38814
  } finally {
37303
38815
  clearTimeout(timeout);
@@ -37416,6 +38928,7 @@ var OhMyOpenCodeLite = async (ctx) => {
37416
38928
  let agents;
37417
38929
  let mcps;
37418
38930
  let modelArrayMap;
38931
+ let everModelSwitched;
37419
38932
  let runtimeChains;
37420
38933
  let multiplexerConfig;
37421
38934
  let multiplexerEnabled;
@@ -37423,6 +38936,7 @@ var OhMyOpenCodeLite = async (ctx) => {
37423
38936
  let multiplexerSessionManager;
37424
38937
  let autoUpdateChecker;
37425
38938
  let sessionAgentMap;
38939
+ const sessionDirectories = new Map;
37426
38940
  let sessionLifecycle;
37427
38941
  let chatHeadersHook;
37428
38942
  let foregroundFallback;
@@ -37466,6 +38980,7 @@ var OhMyOpenCodeLite = async (ctx) => {
37466
38980
  agentDefs = createAgents(config, { projectDirectory: ctx.directory });
37467
38981
  agents = getAgentConfigs(config, { projectDirectory: ctx.directory });
37468
38982
  modelArrayMap = {};
38983
+ everModelSwitched = new Set;
37469
38984
  runtimeChains = {};
37470
38985
  for (const agentDef of agentDefs) {
37471
38986
  if (agentDef._modelArray?.length) {
@@ -37495,9 +39010,9 @@ var OhMyOpenCodeLite = async (ctx) => {
37495
39010
  acpRunTools = Object.keys(config.acpAgents ?? {}).length > 0 ? { acp_run: createAcpRunTool(config.acpAgents) } : {};
37496
39011
  webfetch = createWebfetchTool(ctx);
37497
39012
  backgroundJobBoard = new BackgroundJobBoard({
37498
- maxReusablePerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
37499
- readContextMinLines: config.backgroundJobs?.readContextMinLines ?? 10,
37500
- readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ?? 8
39013
+ maxReusablePerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? DEFAULT_MAX_SESSIONS_PER_AGENT,
39014
+ readContextMinLines: config.backgroundJobs?.readContextMinLines ?? DEFAULT_READ_CONTEXT_MIN_LINES,
39015
+ readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ?? DEFAULT_READ_CONTEXT_MAX_FILES
37501
39016
  });
37502
39017
  const backgroundJobCoordinator = new BackgroundJobCoordinator(backgroundJobBoard);
37503
39018
  multiplexerSessionManager = new MultiplexerSessionManager(ctx, multiplexerConfig, backgroundJobCoordinator);
@@ -37511,14 +39026,14 @@ var OhMyOpenCodeLite = async (ctx) => {
37511
39026
  });
37512
39027
  sessionAgentMap = new Map;
37513
39028
  chatHeadersHook = createChatHeadersHook(ctx);
37514
- foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false, config.fallback?.maxRetries ?? 3, sessionLifecycle, config.fallback?.runtimeOverride ?? true);
39029
+ foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false, config.fallback?.maxRetries ?? 3, sessionLifecycle);
37515
39030
  deepworkCommandHook = createDeepworkCommandHook();
37516
39031
  reflectCommandHook = createReflectCommandHook();
37517
39032
  loopCommandHook = createLoopCommandHook();
37518
39033
  taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
37519
- maxSessionsPerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
37520
- readContextMinLines: config.backgroundJobs?.readContextMinLines ?? 10,
37521
- readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ?? 8,
39034
+ maxSessionsPerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? DEFAULT_MAX_SESSIONS_PER_AGENT,
39035
+ readContextMinLines: config.backgroundJobs?.readContextMinLines ?? DEFAULT_READ_CONTEXT_MIN_LINES,
39036
+ readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ?? DEFAULT_READ_CONTEXT_MAX_FILES,
37522
39037
  backgroundJobBoard: backgroundJobCoordinator,
37523
39038
  shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator",
37524
39039
  registerSessionAsOrchestrator: (sessionID) => {
@@ -37658,6 +39173,15 @@ var OhMyOpenCodeLite = async (ctx) => {
37658
39173
  } else {
37659
39174
  for (const [name, pluginAgent] of Object.entries(agents)) {
37660
39175
  const existing = opencodeConfig.agent[name];
39176
+ if (existing && typeof existing.model === "string") {
39177
+ const primary = modelArrayMap[name]?.[0]?.id;
39178
+ if (primary && existing.model !== primary) {
39179
+ everModelSwitched.add(name);
39180
+ }
39181
+ if (everModelSwitched.has(name)) {
39182
+ foregroundFallback.disableChain(name);
39183
+ }
39184
+ }
37661
39185
  if (existing) {
37662
39186
  opencodeConfig.agent[name] = {
37663
39187
  ...pluginAgent,
@@ -37790,6 +39314,13 @@ var OhMyOpenCodeLite = async (ctx) => {
37790
39314
  recordTuiAgentModels({
37791
39315
  agentModels: tuiAgentModels,
37792
39316
  agentVariants: tuiAgentVariants
39317
+ }, ctx.directory);
39318
+ applyOrchestratorModelConfig({
39319
+ agents: configAgent,
39320
+ enabled: config.stripOrchestratorModel,
39321
+ presets: config.presets,
39322
+ configPreset: config.preset,
39323
+ runtimePreset: runtimePresetName
37793
39324
  });
37794
39325
  const configMcp = opencodeConfig.mcp;
37795
39326
  if (!configMcp) {
@@ -37839,7 +39370,7 @@ var OhMyOpenCodeLite = async (ctx) => {
37839
39370
  agentName,
37840
39371
  model,
37841
39372
  variant: variant ?? null
37842
- });
39373
+ }, (info?.sessionID && sessionDirectories.get(info.sessionID)) ?? ctx.directory);
37843
39374
  }
37844
39375
  }
37845
39376
  if (event.type === "session.created") {
@@ -37848,10 +39379,18 @@ var OhMyOpenCodeLite = async (ctx) => {
37848
39379
  if (depthTracker && childSessionId && parentSessionId) {
37849
39380
  depthTracker.registerChild(parentSessionId, childSessionId);
37850
39381
  }
39382
+ const createdSessionId = event.properties?.info?.id;
39383
+ const createdSessionDir = event.properties?.info?.directory;
39384
+ if (createdSessionId && createdSessionDir) {
39385
+ sessionDirectories.set(createdSessionId, createdSessionDir);
39386
+ }
37851
39387
  }
37852
39388
  await multiplexerSessionManager.onSessionCreated(event);
37853
39389
  await multiplexerSessionManager.onSessionStatus(event);
37854
39390
  await multiplexerSessionManager.onSessionDeleted(event);
39391
+ if (event.type === "server.instance.disposed") {
39392
+ await multiplexerSessionManager.cleanupOnInstanceDisposed();
39393
+ }
37855
39394
  await foregroundFallback.handleEvent(input.event);
37856
39395
  await autoUpdateChecker.event(input);
37857
39396
  await interviewManager.handleEvent(input);
@@ -37883,6 +39422,7 @@ var OhMyOpenCodeLite = async (ctx) => {
37883
39422
  }
37884
39423
  if (sessionID) {
37885
39424
  sessionAgentMap.delete(sessionID);
39425
+ sessionDirectories.delete(sessionID);
37886
39426
  }
37887
39427
  }
37888
39428
  },
@@ -37905,6 +39445,7 @@ var OhMyOpenCodeLite = async (ctx) => {
37905
39445
  output.message.agent = agent;
37906
39446
  }
37907
39447
  if (agent) {
39448
+ foregroundFallback.registerSessionAgent(input.sessionID, agent);
37908
39449
  sessionAgentMap.set(input.sessionID, agent);
37909
39450
  companionManager.onSessionStatus({
37910
39451
  sessionId: input.sessionID,
@@ -37931,6 +39472,9 @@ ${output.system[0]}` : "");
37931
39472
  "experimental.chat.messages.transform": async (input, output) => {
37932
39473
  const typedOutput = output;
37933
39474
  for (const message of typedOutput.messages) {
39475
+ if (!isMessageWithParts(message)) {
39476
+ continue;
39477
+ }
37934
39478
  if (message.info.role !== "user") {
37935
39479
  continue;
37936
39480
  }
@@ -37944,6 +39488,7 @@ ${output.system[0]}` : "");
37944
39488
  processImageAttachments({
37945
39489
  messages: typedOutput.messages,
37946
39490
  workDir: ctx.directory,
39491
+ imageRouting: resolveImageRouting(config.image_routing),
37947
39492
  disabledAgents,
37948
39493
  log
37949
39494
  });