oh-my-opencode-slim 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -18316,7 +18316,7 @@ var DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
18316
18316
  var MAX_POLL_TIME_MS = 5 * 60 * 1000;
18317
18317
  var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
18318
18318
  var PHASE_REMINDER_TEXT = `!IMPORTANT! Recall the workflow rules:
18319
- Understand → choose the best parallelized path based on your capabilities and agents delegation rules → execute → verify.
18319
+ Understand → choose the best parallelized path based on your capabilities and agents delegation rules → recall session reuse rules → execute → verify.
18320
18320
  If delegating, launch the specialist in the same turn you mention it !END!`;
18321
18321
  var TMUX_SPAWN_DELAY_MS = 500;
18322
18322
  var COUNCILLOR_STAGGER_MS = 250;
@@ -18854,14 +18854,15 @@ var AGENT_DESCRIPTIONS = {
18854
18854
  - **Don't delegate when:** Needs discovery/research/decisions • Single small change (<20 lines, one file) • Unclear requirements needing iteration • Explaining to fixer > doing • Tight integration with your current work • Sequential dependencies
18855
18855
  - **Rule of thumb:** Explaining > doing? → yourself. Test file modifications and bounded implementation work usually go to @fixer. Bigger or lots of edits, splitting makes sense, parallelized by spawning @fixers per certain scope.`,
18856
18856
  council: `@council
18857
- - Role: Multi-LLM consensus engine for high-confidence answers
18857
+ - Role: Multi-LLM consensus engine that runs several councillors, synthesizes their views, and returns a structured council report.
18858
18858
  - Permissions: Read files
18859
18859
  - Stats: 3x slower than orchestrator, 3x or more cost of orchestrator
18860
- - Capabilities: Runs multiple models in parallel, synthesizes their responses into a consensus answer
18861
- - **Delegate when:** Critical decisions needing diverse model perspectives • High-stakes architectural choices where consensus reduces riskAmbiguous problems where multi-model disagreement is informativeSecurity-sensitive design reviews
18862
- - **Don't delegate when:** Straightforward tasks you're confident about • Speed matters more than confidence • Single-model answer is sufficientRoutine implementation work
18863
- - **Result handling:** Present the council's synthesized response verbatim. Do not re-summarize or condense.
18864
- - **Rule of thumb:** Need second/third opinions from different models? @council. One good answer enough? yourself.`,
18860
+ - Capabilities: Runs multiple models in parallel, compares their answers, resolves disagreements, and produces a final synthesized answer plus councillor details and consensus summary.
18861
+ - **Delegate when:** Critical decisions need multiple independent perspectives • High-stakes architectural/security/data-integrity choices • Ambiguous problems where disagreement is useful signal You want confidence beyond a single modelThe user explicitly asks for council/consensus/multiple opinions.
18862
+ - **Don't delegate when:** Straightforward tasks you're confident about • Speed matters more than confidence • Routine implementation/debugging • A single specialist is clearly the right tool You only need current docs/search/code review rather than multi-model consensus.
18863
+ - **How to call:** Send the full question/task and relevant context. Be explicit about what decision, trade-off, or answer the council should resolve. Do not ask council to do routine code edits.
18864
+ - **Result handling:** Council returns a structured response that may include: synthesized Council Response, individual Councillor Details, and Council Summary/confidence. Preserve that structure when the user asked for council output. Do not pretend the council only returned a final answer. If you need to act on the council result, first briefly state the council's recommendation, then proceed.
18865
+ - **Rule of thumb:** Need second/third opinions from different models? → @council. Need one expert agent or direct execution? → use the specialist or yourself.`,
18865
18866
  observer: `@observer
18866
18867
  - Role: Visual analysis specialist for images, PDFs, and diagrams
18867
18868
  - Permissions: Read files
@@ -18953,10 +18954,10 @@ Balance: respect dependencies, avoid parallelizing what must be sequential.
18953
18954
  5. Adjust if needed
18954
18955
 
18955
18956
  ### Session Reuse
18956
- - Reuse an available specialist session only for clear follow-up work on the same thread.
18957
- - Prefer a fresh session for unrelated work, even with the same specialist.
18957
+ - Smartly reuse an available specialist session - constext reuse saves time and tokens
18958
+ - When too much unrelated, and really needed, start a fresh session with the specialist
18958
18959
  - If multiple remembered sessions fit, prefer the most recently used matching session.
18959
- - If reuse is unclear, start a fresh session.
18960
+ - Prefer re-uses over creating new sessions all the time
18960
18961
 
18961
18962
  ### Auto-Continue
18962
18963
  When working through multi-step tasks, consider enabling auto-continue to avoid stopping between batches:
@@ -19044,27 +19045,47 @@ var COUNCIL_AGENT_PROMPT = `You are the Council agent — a multi-LLM orchestrat
19044
19045
  1. Call the \`council_session\` tool with the user's prompt
19045
19046
  2. Optionally specify a preset (default: "default")
19046
19047
  3. Receive the councillor responses formatted for synthesis
19047
- 4. Synthesize the optimal final answer from the councillor responses
19048
- 5. Present the synthesized result to the user
19049
-
19050
- **Synthesis Guidelines**:
19051
- When you receive councillor responses, synthesize them into the optimal final answer:
19052
- - Review all councillor responses thoroughly and create the best possible answer
19053
- - Credit specific insights from individual councillors by name (e.g., "alpha noted that...", "beta suggested...")
19054
- - Clearly explain your reasoning for the chosen approach
19055
- - Be transparent about trade-offs when different approaches have valid pros/cons
19056
- - Note any remaining uncertainties or areas where further investigation is needed
19057
- - If councillors disagree, explain the resolution and your reasoning
19058
- - Acknowledge if consensus was impossible and explain why
19059
- - Don't just average responses — choose the best approach and improve upon it
19060
- - Present the synthesized solution with relevant code examples, concrete details, and clear explanations
19048
+ 4. Follow the Synthesis Process below
19049
+ 5. Present the result to the user
19050
+
19051
+ **Synthesis Process** (MANDATORY — follow in order):
19052
+ 1. Read the original user prompt
19053
+ 2. Review each councillor's response individually — note each councillor's key insight and unique contribution by name
19054
+ 3. Identify agreements and contradictions between councillors
19055
+ 4. Resolve contradictions with explicit reasoning
19056
+ 5. Synthesize the optimal final answer
19057
+ 6. Format output per the Required Output Format below
19061
19058
 
19062
19059
  **Behavior**:
19063
19060
  - Delegate requests directly to council_session
19064
19061
  - Don't pre-analyze or filter the prompt before calling council_session
19065
- - Synthesize the councillor results into a comprehensive, coherent answer
19066
- - Include attribution for valuable insights from specific councillors
19067
- - If councillors disagree, explain why you chose one approach over another`;
19062
+ - Credit specific insights from individual councillors using their names
19063
+ - If councillors disagree, explain why you chose one approach over another
19064
+ - Do not omit per-councillor details from the final response
19065
+ - Do not collapse the output into only a final summary
19066
+ - Be transparent about trade-offs when different approaches have valid pros/cons
19067
+ - Don't just average responses — choose the best approach and improve upon it
19068
+
19069
+ **Required Output Format**:
19070
+ Always include these sections in your final response:
19071
+
19072
+ ## Council Response
19073
+ Provide the best synthesized answer. Integrate the strongest points from the councillors, resolve disagreements, and give the user a clear final recommendation or answer. Include relevant code examples and concrete details.
19074
+
19075
+ ## Councillor Details
19076
+ Include each councillor's response separately.
19077
+
19078
+ Use each councillor name exactly as provided in the tool result.
19079
+
19080
+ Format each councillor like:
19081
+
19082
+ ### <councillor name>
19083
+ <that councillor's response>
19084
+
19085
+ If a councillor failed or timed out, include that status briefly.
19086
+
19087
+ ## Council Summary
19088
+ Summarize where councillors agreed, where they disagreed, why you chose the final answer, and any remaining uncertainty. Include a consensus confidence rating: unanimous, majority, or split.`;
19068
19089
  function createCouncilAgent(model, customPrompt, customAppendPrompt) {
19069
19090
  const prompt = resolvePrompt(COUNCIL_AGENT_PROMPT, customPrompt, customAppendPrompt);
19070
19091
  const definition = {
@@ -19137,7 +19158,7 @@ ${failedSection}`;
19137
19158
 
19138
19159
  ---
19139
19160
 
19140
- Synthesize the optimal response based on the above.`;
19161
+ You MUST follow the Synthesis Process steps before producing output: review each councillor response individually, then produce the required output with a synthesized Council Response, per-councillor details using their exact names, and a Council Summary with consensus confidence rating (unanimous, majority, or split).`;
19141
19162
  return prompt;
19142
19163
  }
19143
19164
 
@@ -19769,6 +19790,27 @@ function getDisabledAgents(config) {
19769
19790
  return disabled;
19770
19791
  }
19771
19792
 
19793
+ // src/config/runtime-preset.ts
19794
+ var activeRuntimePreset = null;
19795
+ function setActiveRuntimePreset(name) {
19796
+ activeRuntimePreset = name;
19797
+ }
19798
+ function getActiveRuntimePreset() {
19799
+ return activeRuntimePreset;
19800
+ }
19801
+ var previousRuntimePreset = null;
19802
+ function getPreviousRuntimePreset() {
19803
+ return previousRuntimePreset;
19804
+ }
19805
+ function setActiveRuntimePresetWithPrevious(name) {
19806
+ previousRuntimePreset = activeRuntimePreset;
19807
+ activeRuntimePreset = name;
19808
+ }
19809
+ function rollbackRuntimePreset(previous) {
19810
+ activeRuntimePreset = previous;
19811
+ previousRuntimePreset = null;
19812
+ }
19813
+
19772
19814
  // src/utils/logger.ts
19773
19815
  import * as fs2 from "node:fs";
19774
19816
  import { appendFile } from "node:fs/promises";
@@ -20214,6 +20256,16 @@ function unexpectedPatchLine(context, line) {
20214
20256
  const rendered = line.length === 0 ? "<empty>" : line;
20215
20257
  throw new Error(`Invalid patch format: unexpected line ${context}: ${rendered}`);
20216
20258
  }
20259
+ function parseChangeContext(line) {
20260
+ const context = line.slice(2);
20261
+ if (context.length === 0) {
20262
+ return;
20263
+ }
20264
+ return context.startsWith(" ") ? context.slice(1) || undefined : context;
20265
+ }
20266
+ function isPatchBoundary(line, marker) {
20267
+ return line.trimEnd() === marker;
20268
+ }
20217
20269
  function parseChunks(lines, index, mode) {
20218
20270
  const chunks = [];
20219
20271
  let at = index;
@@ -20225,7 +20277,7 @@ function parseChunks(lines, index, mode) {
20225
20277
  at += 1;
20226
20278
  continue;
20227
20279
  }
20228
- const context = lines[at].slice(2).trim() || undefined;
20280
+ const context = parseChangeContext(lines[at]);
20229
20281
  at += 1;
20230
20282
  const old_lines = [];
20231
20283
  const new_lines = [];
@@ -20268,12 +20320,11 @@ function parseChunks(lines, index, mode) {
20268
20320
  return { chunks, next: at };
20269
20321
  }
20270
20322
  function parseAdd(lines, index, mode) {
20271
- let contents = "";
20323
+ const contents = [];
20272
20324
  let at = index;
20273
20325
  while (at < lines.length && !lines[at].startsWith("***")) {
20274
20326
  if (lines[at].startsWith("+")) {
20275
- contents += `${lines[at].slice(1)}
20276
- `;
20327
+ contents.push(lines[at].slice(1));
20277
20328
  at += 1;
20278
20329
  continue;
20279
20330
  }
@@ -20282,18 +20333,15 @@ function parseAdd(lines, index, mode) {
20282
20333
  }
20283
20334
  at += 1;
20284
20335
  }
20285
- if (contents.endsWith(`
20286
- `)) {
20287
- contents = contents.slice(0, -1);
20288
- }
20289
- return { content: contents, next: at };
20336
+ return { content: contents.join(`
20337
+ `), next: at };
20290
20338
  }
20291
20339
  function parsePatchInternal(patchText, mode) {
20292
20340
  const clean = normalizePatchText(patchText);
20293
20341
  const lines = clean.split(`
20294
20342
  `);
20295
- const begin = lines.findIndex((line) => line.trim() === "*** Begin Patch");
20296
- const end = lines.findIndex((line) => line.trim() === "*** End Patch");
20343
+ const begin = lines.findIndex((line) => isPatchBoundary(line, "*** Begin Patch"));
20344
+ const end = lines.findIndex((line, index2) => index2 > begin && isPatchBoundary(line, "*** End Patch"));
20297
20345
  if (begin === -1 || end === -1 || begin >= end) {
20298
20346
  throw new Error("Invalid patch format: missing Begin/End markers");
20299
20347
  }
@@ -20425,12 +20473,6 @@ function formatPatch(patch) {
20425
20473
  }
20426
20474
 
20427
20475
  // src/hooks/apply-patch/matching.ts
20428
- var AUTO_RESCUE_COMPARATOR_NAMES = new Set([
20429
- "exact",
20430
- "unicode",
20431
- "trim-end",
20432
- "unicode-trim-end"
20433
- ]);
20434
20476
  function equalExact(a, b) {
20435
20477
  return a === b;
20436
20478
  }
@@ -20449,7 +20491,7 @@ function equalTrim(a, b) {
20449
20491
  function equalUnicodeTrim(a, b) {
20450
20492
  return normalizeUnicode(a.trim()) === normalizeUnicode(b.trim());
20451
20493
  }
20452
- var comparatorEntries = [
20494
+ var autoRescueComparatorEntries = [
20453
20495
  { name: "exact", exact: true, same: equalExact },
20454
20496
  { name: "unicode", exact: false, same: equalUnicodeExact },
20455
20497
  { name: "trim-end", exact: false, same: equalTrimEnd },
@@ -20457,14 +20499,44 @@ var comparatorEntries = [
20457
20499
  name: "unicode-trim-end",
20458
20500
  exact: false,
20459
20501
  same: equalUnicodeTrimEnd
20460
- },
20502
+ }
20503
+ ];
20504
+ var comparatorEntries = [
20505
+ ...autoRescueComparatorEntries,
20461
20506
  { name: "trim", exact: false, same: equalTrim },
20462
20507
  { name: "unicode-trim", exact: false, same: equalUnicodeTrim }
20463
20508
  ];
20464
- var autoRescueComparatorEntries = comparatorEntries.filter((entry) => AUTO_RESCUE_COMPARATOR_NAMES.has(entry.name));
20465
20509
  var MAX_LCS_CHUNK_LINES = 48;
20466
20510
  var MAX_LCS_CANDIDATES = 64;
20467
20511
  var autoRescueComparators = autoRescueComparatorEntries.map((entry) => entry.same);
20512
+ function prepareAutoRescueTarget(target) {
20513
+ const trimEnd = target.trimEnd();
20514
+ const unicode = normalizeUnicode(target);
20515
+ return {
20516
+ exact: target,
20517
+ unicode,
20518
+ trimEnd,
20519
+ unicodeTrimEnd: trimEnd === target ? unicode : normalizeUnicode(trimEnd)
20520
+ };
20521
+ }
20522
+ function matchPreparedAutoRescueComparator(candidate, target) {
20523
+ if (candidate === target.exact) {
20524
+ return "exact";
20525
+ }
20526
+ const unicode = normalizeUnicode(candidate);
20527
+ if (unicode === target.unicode) {
20528
+ return "unicode";
20529
+ }
20530
+ const trimEnd = candidate.trimEnd();
20531
+ if (trimEnd === target.trimEnd) {
20532
+ return "trim-end";
20533
+ }
20534
+ const unicodeTrimEnd = trimEnd === candidate ? unicode : normalizeUnicode(trimEnd);
20535
+ if (unicodeTrimEnd === target.unicodeTrimEnd) {
20536
+ return "unicode-trim-end";
20537
+ }
20538
+ return;
20539
+ }
20468
20540
  var permissiveComparators = comparatorEntries.map((entry) => entry.same);
20469
20541
  function tryMatch(lines, pattern, start, comparator, eof) {
20470
20542
  if (eof) {
@@ -20538,6 +20610,19 @@ function list(lines, pattern, start, same) {
20538
20610
  }
20539
20611
  return out;
20540
20612
  }
20613
+ function lowerBound(values, target) {
20614
+ let low = 0;
20615
+ let high = values.length;
20616
+ while (low < high) {
20617
+ const middle = Math.floor((low + high) / 2);
20618
+ if (values[middle] < target) {
20619
+ low = middle + 1;
20620
+ continue;
20621
+ }
20622
+ high = middle;
20623
+ }
20624
+ return low;
20625
+ }
20541
20626
  function sameRescueLine(a, b) {
20542
20627
  return equalExact(a, b) || equalUnicodeExact(a, b);
20543
20628
  }
@@ -20564,36 +20649,102 @@ function rescueByPrefixSuffix(lines, old_lines, new_lines, start) {
20564
20649
  const left = old_lines.slice(0, prefixLength);
20565
20650
  const right = old_lines.slice(old_lines.length - suffixLength);
20566
20651
  const middle = new_lines.slice(prefixLength, new_lines.length - suffixLength);
20567
- const hits = new Map;
20652
+ if (left.length === 1 && right.length === 1) {
20653
+ const { leftHits, rightHits } = collectOneLinePrefixSuffixHits(lines, left[0], right[0], start);
20654
+ return resolvePrefixSuffixHits(leftHits, rightHits, left.length, middle);
20655
+ }
20656
+ const hits = new Set;
20657
+ let hit;
20568
20658
  for (const same of autoRescueComparators) {
20569
- for (const leftIndex of list(lines, left, start, same)) {
20659
+ const leftHits = list(lines, left, start, same);
20660
+ if (leftHits.length === 0) {
20661
+ continue;
20662
+ }
20663
+ const rightHits = list(lines, right, leftHits[0] + left.length, same);
20664
+ if (rightHits.length === 0) {
20665
+ continue;
20666
+ }
20667
+ for (const leftIndex of leftHits) {
20570
20668
  const from = leftIndex + left.length;
20571
- for (const rightIndex of list(lines, right, from, same)) {
20669
+ for (let index = lowerBound(rightHits, from);index < rightHits.length; index += 1) {
20670
+ const rightIndex = rightHits[index];
20572
20671
  const key = `${from}:${rightIndex}`;
20573
- hits.set(key, {
20672
+ if (!hits.has(key)) {
20673
+ hits.add(key);
20674
+ hit = {
20675
+ start: from,
20676
+ del: rightIndex - from,
20677
+ add: [...middle]
20678
+ };
20679
+ }
20680
+ if (hits.size > 1) {
20681
+ return { kind: "ambiguous", phase: "prefix_suffix" };
20682
+ }
20683
+ }
20684
+ }
20685
+ }
20686
+ if (!hit) {
20687
+ return { kind: "miss" };
20688
+ }
20689
+ return { kind: "match", hit };
20690
+ }
20691
+ function collectOneLinePrefixSuffixHits(lines, left, right, start) {
20692
+ const leftTarget = prepareAutoRescueTarget(left);
20693
+ const rightTarget = prepareAutoRescueTarget(right);
20694
+ const leftHits = [];
20695
+ const rightHits = [];
20696
+ for (let index = start;index < lines.length; index += 1) {
20697
+ const line = prepareAutoRescueTarget(lines[index]);
20698
+ if (line.unicodeTrimEnd === leftTarget.unicodeTrimEnd) {
20699
+ leftHits.push(index);
20700
+ }
20701
+ if (index > start && line.unicodeTrimEnd === rightTarget.unicodeTrimEnd) {
20702
+ rightHits.push(index);
20703
+ }
20704
+ }
20705
+ return { leftHits, rightHits };
20706
+ }
20707
+ function resolvePrefixSuffixHits(leftHits, rightHits, leftLength, middle) {
20708
+ if (leftHits.length === 0 || rightHits.length === 0) {
20709
+ return { kind: "miss" };
20710
+ }
20711
+ const hits = new Set;
20712
+ let hit;
20713
+ for (const leftIndex of leftHits) {
20714
+ const from = leftIndex + leftLength;
20715
+ for (let index = lowerBound(rightHits, from);index < rightHits.length; index += 1) {
20716
+ const rightIndex = rightHits[index];
20717
+ const key = `${from}:${rightIndex}`;
20718
+ if (!hits.has(key)) {
20719
+ hits.add(key);
20720
+ hit = {
20574
20721
  start: from,
20575
20722
  del: rightIndex - from,
20576
20723
  add: [...middle]
20577
- });
20724
+ };
20725
+ }
20726
+ if (hits.size > 1) {
20727
+ return { kind: "ambiguous", phase: "prefix_suffix" };
20578
20728
  }
20579
20729
  }
20580
20730
  }
20581
- if (hits.size === 0) {
20731
+ if (!hit) {
20582
20732
  return { kind: "miss" };
20583
20733
  }
20584
- if (hits.size > 1) {
20585
- return { kind: "ambiguous", phase: "prefix_suffix" };
20586
- }
20587
- return { kind: "match", hit: [...hits.values()][0] };
20734
+ return { kind: "match", hit };
20588
20735
  }
20589
20736
  function score(a, b) {
20590
- const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
20737
+ const normalizedA = a.map(normalizeLcsLine);
20738
+ const normalizedB = b.map(normalizeLcsLine);
20739
+ let previous = Array(b.length + 1).fill(0);
20591
20740
  for (let i = 1;i <= a.length; i += 1) {
20741
+ const current = Array(b.length + 1).fill(0);
20592
20742
  for (let j = 1;j <= b.length; j += 1) {
20593
- dp[i][j] = normalizeUnicode(a[i - 1].trim()) === normalizeUnicode(b[j - 1].trim()) ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
20743
+ current[j] = normalizedA[i - 1] === normalizedB[j - 1] ? previous[j - 1] + 1 : Math.max(previous[j], current[j - 1]);
20594
20744
  }
20745
+ previous = current;
20595
20746
  }
20596
- return dp[a.length][b.length];
20747
+ return previous[b.length];
20597
20748
  }
20598
20749
  function normalizeLcsLine(line) {
20599
20750
  return normalizeUnicode(line).trim();
@@ -20620,45 +20771,23 @@ function countLcsUpperBound(a, b) {
20620
20771
  }
20621
20772
  return shared;
20622
20773
  }
20623
- function hasStableBorders(oldLines, candidate) {
20624
- if (oldLines.length === 0 || candidate.length !== oldLines.length) {
20625
- return false;
20626
- }
20627
- const same = autoRescueComparators.some((compare) => compare(oldLines[0], candidate[0]));
20628
- if (!same) {
20629
- return false;
20630
- }
20631
- if (oldLines.length === 1) {
20632
- return true;
20633
- }
20634
- return autoRescueComparators.some((compare) => compare(oldLines[oldLines.length - 1], candidate[candidate.length - 1]));
20635
- }
20636
20774
  function collectBorderAnchoredStarts(lines, oldLines, start) {
20637
20775
  if (oldLines.length === 0) {
20638
20776
  return [];
20639
20777
  }
20640
- const firstHits = new Set;
20641
- const lastHits = new Set;
20642
- const lastLine = oldLines[oldLines.length - 1];
20643
- for (const same of autoRescueComparators) {
20644
- for (const index of list(lines, [oldLines[0]], start, same)) {
20645
- firstHits.add(index);
20646
- }
20647
- for (const index of list(lines, [lastLine], start, same)) {
20648
- lastHits.add(index);
20649
- }
20650
- }
20651
20778
  const candidates = [];
20652
- for (const index of [...firstHits].sort((a, b) => a - b)) {
20653
- const end = index + oldLines.length - 1;
20654
- if (end >= lines.length || !lastHits.has(end)) {
20779
+ const firstLine = prepareAutoRescueTarget(oldLines[0]);
20780
+ const lastLine = prepareAutoRescueTarget(oldLines[oldLines.length - 1]);
20781
+ const lastOffset = oldLines.length - 1;
20782
+ const maxStart = lines.length - oldLines.length;
20783
+ for (let index = start;index <= maxStart; index += 1) {
20784
+ const end = index + lastOffset;
20785
+ if (matchPreparedAutoRescueComparator(lines[index], firstLine) === undefined) {
20655
20786
  continue;
20656
20787
  }
20657
- const candidate = lines.slice(index, index + oldLines.length);
20658
- if (!hasStableBorders(oldLines, candidate)) {
20659
- continue;
20788
+ if (oldLines.length === 1 || matchPreparedAutoRescueComparator(lines[end], lastLine) !== undefined) {
20789
+ candidates.push(index);
20660
20790
  }
20661
- candidates.push(index);
20662
20791
  }
20663
20792
  return candidates;
20664
20793
  }
@@ -20666,11 +20795,6 @@ function rescueByLcs(lines, old_lines, new_lines, start) {
20666
20795
  if (old_lines.length === 0 || lines.length === 0) {
20667
20796
  return { kind: "miss" };
20668
20797
  }
20669
- const from = start;
20670
- const to = lines.length - old_lines.length;
20671
- if (to < from) {
20672
- return { kind: "miss" };
20673
- }
20674
20798
  if (old_lines.length > MAX_LCS_CHUNK_LINES) {
20675
20799
  return { kind: "miss" };
20676
20800
  }
@@ -20683,9 +20807,6 @@ function rescueByLcs(lines, old_lines, new_lines, start) {
20683
20807
  let bestScore = 0;
20684
20808
  let ties = 0;
20685
20809
  for (const index of candidates) {
20686
- if (index < from || index > to) {
20687
- continue;
20688
- }
20689
20810
  const window2 = lines.slice(index, index + old_lines.length);
20690
20811
  if (countLcsUpperBound(old_lines, window2) < needed) {
20691
20812
  continue;
@@ -20740,26 +20861,29 @@ function resolveChunkStart(lines, chunk, start) {
20740
20861
  return at === -1 ? start : at + 1;
20741
20862
  }
20742
20863
  function resolveUniqueAnchor(lines, changeContext, start) {
20743
- const hits = new Set;
20744
- for (const same of autoRescueComparators) {
20745
- for (const index2 of list(lines, [changeContext], start, same)) {
20746
- hits.add(index2);
20864
+ let matchedIndex;
20865
+ let matchedComparator;
20866
+ const anchorTarget = prepareAutoRescueTarget(changeContext);
20867
+ for (let index = start;index < lines.length; index += 1) {
20868
+ const comparator = matchPreparedAutoRescueComparator(lines[index], anchorTarget);
20869
+ if (!comparator) {
20870
+ continue;
20747
20871
  }
20872
+ if (matchedIndex !== undefined) {
20873
+ return { kind: "ambiguous" };
20874
+ }
20875
+ matchedIndex = index;
20876
+ matchedComparator = comparator;
20748
20877
  }
20749
- if (hits.size === 0) {
20878
+ if (matchedIndex === undefined) {
20750
20879
  return { kind: "missing" };
20751
20880
  }
20752
- if (hits.size > 1) {
20753
- return { kind: "ambiguous" };
20754
- }
20755
- const index = [...hits][0];
20756
- const canonicalLine = lines[index];
20757
- const comparator = seekMatch(lines, [changeContext], index)?.comparator;
20881
+ const canonicalLine = lines[matchedIndex];
20758
20882
  return {
20759
20883
  kind: "match",
20760
- index,
20884
+ index: matchedIndex,
20761
20885
  exact: canonicalLine === changeContext,
20762
- comparator: comparator ?? "exact",
20886
+ comparator: matchedComparator ?? "exact",
20763
20887
  canonicalLine
20764
20888
  };
20765
20889
  }
@@ -20910,10 +21034,11 @@ ${chunk.change_context}`);
20910
21034
  old_lines: [],
20911
21035
  canonical_old_lines: [anchor],
20912
21036
  canonical_new_lines: [...chunk.new_lines, anchor],
21037
+ canonical_change_context: anchorMatch.exact ? undefined : anchorMatch.canonicalLine,
20913
21038
  resolved_is_end_of_file: insertAt + 1 === lines.length,
20914
21039
  rewritten: true,
20915
21040
  strategy,
20916
- matchComparator: "exact"
21041
+ matchComparator: anchorMatch.comparator
20917
21042
  });
20918
21043
  start = insertAt;
20919
21044
  continue;
@@ -21287,15 +21412,6 @@ function renderRewriteDependencyGroup(group, cfg) {
21287
21412
  }
21288
21413
  return group.group.chunks ? createUpdateHunk(group.group.sourcePath, group.group.chunks, group.group.outputPath !== group.group.sourcePath ? group.group.outputPath : undefined) : createCollapsedUpdateHunk(group.group.sourcePath, group.group.sourceFilePath, group.group.baseText, group.group.finalText, cfg, group.group.outputPath !== group.group.sourcePath ? group.group.outputPath : undefined);
21289
21414
  }
21290
- function rewriteModeForDependentUpdate(group) {
21291
- if (group.kind === "add") {
21292
- return "collapse:add-followed-by-update";
21293
- }
21294
- if (group.group.outputPath !== group.group.sourcePath) {
21295
- return "collapse:move-followed-by-update";
21296
- }
21297
- return "merge:same-file-updates";
21298
- }
21299
21415
  function combineDependentUpdateGroup(filePath, group, nextChunks, finalText, nextOutputPath, nextOutputFilePath, cfg) {
21300
21416
  if (group.kind === "add") {
21301
21417
  return {
@@ -21335,12 +21451,6 @@ async function rewritePatch(root, patchText, cfg, worktree) {
21335
21451
  const normalizedPatchText = normalizePatchText(patchText);
21336
21452
  const rewritten = [];
21337
21453
  let changed = false;
21338
- let rewrittenChunks = 0;
21339
- const rewriteModes = new Set;
21340
- const totalChunks = hunks.reduce((count, hunk) => count + (hunk.type === "update" ? hunk.chunks.length : 0), 0);
21341
- if (pathsNormalized) {
21342
- rewriteModes.add("normalize:patch-paths");
21343
- }
21344
21454
  const dependencyGroups = new Map;
21345
21455
  for (const hunk of hunks) {
21346
21456
  if (hunk.type === "add") {
@@ -21395,14 +21505,6 @@ async function rewritePatch(root, patchText, cfg, worktree) {
21395
21505
  continue;
21396
21506
  }
21397
21507
  changed = true;
21398
- rewrittenChunks += 1;
21399
- if (chunk.strategy) {
21400
- rewriteModes.add(chunk.strategy);
21401
- continue;
21402
- }
21403
- if (chunk.matchComparator && chunk.matchComparator !== "exact") {
21404
- rewriteModes.add(`match:${chunk.matchComparator}`);
21405
- }
21406
21508
  }
21407
21509
  const nextOutputPath = hunk.move_path ?? hunk.path;
21408
21510
  const nextOutputFilePath = movePath ?? filePath;
@@ -21410,7 +21512,6 @@ async function rewritePatch(root, patchText, cfg, worktree) {
21410
21512
  const nextGroup = combineDependentUpdateGroup(filePath, currentDependency, next, nextText, nextOutputPath, nextOutputFilePath, cfg);
21411
21513
  rewritten[currentDependency.group.index] = renderRewriteDependencyGroup(nextGroup, cfg);
21412
21514
  changed = true;
21413
- rewriteModes.add(rewriteModeForDependentUpdate(currentDependency));
21414
21515
  clearDependencyGroup(filePath);
21415
21516
  if (movePath && movePath !== filePath) {
21416
21517
  clearDependencyGroup(movePath);
@@ -21457,35 +21558,23 @@ async function rewritePatch(root, patchText, cfg, worktree) {
21457
21558
  if (pathsNormalized) {
21458
21559
  return {
21459
21560
  patchText: formatPatch({ hunks }),
21460
- changed: true,
21461
- rewrittenChunks: 0,
21462
- totalChunks,
21463
- rewriteModes: [...rewriteModes].sort()
21561
+ changed: true
21464
21562
  };
21465
21563
  }
21466
21564
  if (normalizedPatchText !== patchText) {
21467
21565
  return {
21468
21566
  patchText: normalizedPatchText,
21469
- changed: true,
21470
- rewrittenChunks: 0,
21471
- totalChunks,
21472
- rewriteModes: ["normalize:patch-text"]
21567
+ changed: true
21473
21568
  };
21474
21569
  }
21475
21570
  return {
21476
21571
  patchText,
21477
- changed: false,
21478
- rewrittenChunks: 0,
21479
- totalChunks,
21480
- rewriteModes: []
21572
+ changed: false
21481
21573
  };
21482
21574
  }
21483
21575
  return {
21484
21576
  patchText: formatPatch({ hunks: rewritten }),
21485
- changed: true,
21486
- rewrittenChunks,
21487
- totalChunks,
21488
- rewriteModes: [...rewriteModes].sort()
21577
+ changed: true
21489
21578
  };
21490
21579
  } catch (error) {
21491
21580
  throw ensureApplyPatchError(error, "Unexpected rewrite failure");
@@ -21505,30 +21594,36 @@ function createApplyPatchHook(ctx) {
21505
21594
  if (input.tool !== "apply_patch") {
21506
21595
  return;
21507
21596
  }
21508
- if (typeof output.args?.patchText !== "string") {
21597
+ const args = output.args;
21598
+ if (!args || typeof args.patchText !== "string") {
21509
21599
  return;
21510
21600
  }
21601
+ const patchText = args.patchText;
21511
21602
  const root = input.directory || ctx.directory || process.cwd();
21512
21603
  const worktree = ctx.worktree || root;
21513
21604
  try {
21514
- const result = await rewritePatch(root, output.args.patchText, APPLY_PATCH_RESCUE_OPTIONS, worktree);
21605
+ const result = await rewritePatch(root, patchText, APPLY_PATCH_RESCUE_OPTIONS, worktree);
21515
21606
  if (result.changed) {
21516
- output.args.patchText = result.patchText;
21517
- logHookStatus("rewrite", {
21518
- rewrittenChunks: result.rewrittenChunks,
21519
- totalChunks: result.totalChunks,
21520
- strategies: result.rewriteModes
21521
- });
21607
+ args.patchText = result.patchText;
21608
+ logHookStatus("rewrite");
21522
21609
  return;
21523
21610
  }
21524
- logHookStatus("unchanged", {
21525
- rewrittenChunks: 0,
21526
- totalChunks: result.totalChunks
21527
- });
21611
+ logHookStatus("unchanged");
21528
21612
  return;
21529
21613
  } catch (error) {
21530
21614
  const normalizedError = isApplyPatchError(error) ? error : createApplyPatchInternalError(`Unexpected hook failure before native apply: ${error instanceof Error ? error.message : String(error)}`, error);
21531
21615
  const details = getApplyPatchErrorDetails(normalizedError);
21616
+ if (normalizedError.kind === "blocked" && details?.code === "outside_workspace") {
21617
+ logHookStatus("skipped", {
21618
+ kind: details.kind,
21619
+ code: details.code,
21620
+ reason: normalizedError.message,
21621
+ failOpen: true,
21622
+ rescueOptions: APPLY_PATCH_RESCUE_OPTIONS,
21623
+ rewriteStage: "before-native"
21624
+ });
21625
+ return;
21626
+ }
21532
21627
  logHookStatus(isApplyPatchVerificationError(normalizedError) ? "verification" : normalizedError.kind === "validation" ? "validation" : normalizedError.kind === "internal" ? "internal" : "blocked", {
21533
21628
  kind: details?.kind ?? "internal",
21534
21629
  code: details?.code ?? "internal_unexpected",
@@ -23110,7 +23205,7 @@ ${JSON_ERROR_REMINDER}`;
23110
23205
  };
23111
23206
  }
23112
23207
  // src/hooks/phase-reminder/index.ts
23113
- var PHASE_REMINDER = `<reminder>${PHASE_REMINDER_TEXT}</reminder>`;
23208
+ var PHASE_REMINDER = `<internal_reminder>${PHASE_REMINDER_TEXT}</internal_reminder>`;
23114
23209
  function createPhaseReminderHook() {
23115
23210
  return {
23116
23211
  "experimental.chat.messages.transform": async (_input, output) => {
@@ -23141,11 +23236,14 @@ function createPhaseReminderHook() {
23141
23236
  if (originalText.includes(SLIM_INTERNAL_INITIATOR_MARKER)) {
23142
23237
  return;
23143
23238
  }
23144
- lastUserMessage.parts[textPartIndex].text = `${PHASE_REMINDER}
23239
+ if (originalText.includes(PHASE_REMINDER)) {
23240
+ return;
23241
+ }
23242
+ lastUserMessage.parts[textPartIndex].text = `${originalText}
23145
23243
 
23146
23244
  ---
23147
23245
 
23148
- ${originalText}`;
23246
+ ${PHASE_REMINDER}`;
23149
23247
  }
23150
23248
  };
23151
23249
  }
@@ -23153,32 +23251,31 @@ ${originalText}`;
23153
23251
  var POST_FILE_TOOL_NUDGE = PHASE_REMINDER_TEXT;
23154
23252
  var FILE_TOOLS = new Set(["Read", "read", "Write", "write"]);
23155
23253
  function createPostFileToolNudgeHook(options = {}) {
23156
- const pendingSessionIds = new Set;
23254
+ function appendReminder(output) {
23255
+ if (typeof output.output !== "string") {
23256
+ return;
23257
+ }
23258
+ if (output.output.includes(POST_FILE_TOOL_NUDGE)) {
23259
+ return;
23260
+ }
23261
+ output.output = [
23262
+ output.output,
23263
+ "",
23264
+ "<internal_reminder>",
23265
+ POST_FILE_TOOL_NUDGE,
23266
+ "</internal_reminder>"
23267
+ ].join(`
23268
+ `);
23269
+ }
23157
23270
  return {
23158
- "tool.execute.after": async (input, _output) => {
23271
+ "tool.execute.after": async (input, output) => {
23159
23272
  if (!FILE_TOOLS.has(input.tool) || !input.sessionID) {
23160
23273
  return;
23161
23274
  }
23162
- pendingSessionIds.add(input.sessionID);
23163
- },
23164
- "experimental.chat.system.transform": async (input, output) => {
23165
- if (!input.sessionID || !pendingSessionIds.delete(input.sessionID)) {
23166
- return;
23167
- }
23168
23275
  if (options.shouldInject && !options.shouldInject(input.sessionID)) {
23169
23276
  return;
23170
23277
  }
23171
- output.system.push(POST_FILE_TOOL_NUDGE);
23172
- },
23173
- event: async (input) => {
23174
- if (input.event.type !== "session.deleted") {
23175
- return;
23176
- }
23177
- const sessionID = input.event.properties?.sessionID ?? input.event.properties?.info?.id;
23178
- if (!sessionID) {
23179
- return;
23180
- }
23181
- pendingSessionIds.delete(sessionID);
23278
+ appendReminder(output);
23182
23279
  }
23183
23280
  };
23184
23281
  }
@@ -23196,6 +23293,8 @@ var AGENT_NAME_SET = new Set([
23196
23293
  "councillor"
23197
23294
  ]);
23198
23295
  var MAX_PENDING_TASK_CALLS = 100;
23296
+ var RESUMABLE_SESSIONS_START = "<resumable_sessions>";
23297
+ var RESUMABLE_SESSIONS_END = "</resumable_sessions>";
23199
23298
  function isAgentName(value) {
23200
23299
  return typeof value === "string" && AGENT_NAME_SET.has(value);
23201
23300
  }
@@ -23395,14 +23494,36 @@ function createTaskSessionManagerHook(_ctx, options) {
23395
23494
  sessionManager.addContext(taskId, contextFiles);
23396
23495
  pruneContext();
23397
23496
  },
23398
- "experimental.chat.system.transform": async (input, output) => {
23399
- if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
23497
+ "experimental.chat.messages.transform": async (_input, output) => {
23498
+ for (let i = output.messages.length - 1;i >= 0; i -= 1) {
23499
+ const message = output.messages[i];
23500
+ if (message.info.role !== "user")
23501
+ continue;
23502
+ if (message.info.agent && message.info.agent !== "orchestrator")
23503
+ return;
23504
+ if (!message.info.sessionID || !options.shouldManageSession(message.info.sessionID)) {
23505
+ return;
23506
+ }
23507
+ const reminder = sessionManager.formatForPrompt(message.info.sessionID);
23508
+ if (!reminder)
23509
+ return;
23510
+ const textPart = message.parts.find((part) => part.type === "text" && typeof part.text === "string");
23511
+ if (!textPart)
23512
+ return;
23513
+ if (textPart.text?.includes(SLIM_INTERNAL_INITIATOR_MARKER))
23514
+ return;
23515
+ if (textPart.text?.includes(RESUMABLE_SESSIONS_START))
23516
+ return;
23517
+ textPart.text = [
23518
+ textPart.text ?? "",
23519
+ "",
23520
+ RESUMABLE_SESSIONS_START,
23521
+ reminder,
23522
+ RESUMABLE_SESSIONS_END
23523
+ ].join(`
23524
+ `);
23400
23525
  return;
23401
23526
  }
23402
- const reminder = sessionManager.formatForPrompt(input.sessionID);
23403
- if (!reminder)
23404
- return;
23405
- output.system.push(reminder);
23406
23527
  },
23407
23528
  event: async (input) => {
23408
23529
  if (input.event.type === "session.created") {
@@ -23467,7 +23588,7 @@ function createTodoHygiene(options) {
23467
23588
  handleRequestStart(input) {
23468
23589
  clear(input.sessionID);
23469
23590
  },
23470
- async handleToolExecuteAfter(input) {
23591
+ async handleToolExecuteAfter(input, _output) {
23471
23592
  if (!input.sessionID) {
23472
23593
  return;
23473
23594
  }
@@ -23477,6 +23598,10 @@ function createTodoHygiene(options) {
23477
23598
  }
23478
23599
  try {
23479
23600
  if (RESET.has(tool)) {
23601
+ if (options.shouldInject && !options.shouldInject(input.sessionID)) {
23602
+ clear(input.sessionID);
23603
+ return;
23604
+ }
23480
23605
  active.add(input.sessionID);
23481
23606
  clearCycle(input.sessionID);
23482
23607
  const state2 = await options.getTodoState(input.sessionID);
@@ -23535,39 +23660,22 @@ function createTodoHygiene(options) {
23535
23660
  });
23536
23661
  }
23537
23662
  },
23538
- async handleChatSystemTransform(input, output) {
23539
- if (!input.sessionID) {
23540
- return;
23541
- }
23542
- const reasons = pending.get(input.sessionID);
23663
+ getPendingReminder(sessionID) {
23664
+ const reasons = pending.get(sessionID);
23543
23665
  if (!reasons || reasons.size === 0) {
23544
- return;
23545
- }
23546
- const reminder = pick(reasons);
23547
- if (options.shouldInject && !options.shouldInject(input.sessionID)) {
23548
- clear(input.sessionID);
23549
- return;
23666
+ return null;
23550
23667
  }
23551
- try {
23552
- const state = await options.getTodoState(input.sessionID);
23553
- if (!state.hasOpenTodos) {
23554
- clear(input.sessionID);
23555
- return;
23556
- }
23557
- pending.delete(input.sessionID);
23558
- output.system.push(reminder);
23559
- options.log?.("Injected todo hygiene reminder", {
23560
- sessionID: input.sessionID,
23561
- reminder,
23562
- reasons: Array.from(reasons)
23563
- });
23564
- } catch (error) {
23565
- pending.delete(input.sessionID);
23566
- options.log?.("Skipped todo hygiene reminder: failed to inspect todos", {
23567
- sessionID: input.sessionID,
23568
- error: error instanceof Error ? error.message : String(error)
23569
- });
23668
+ if (options.shouldInject && !options.shouldInject(sessionID)) {
23669
+ clear(sessionID);
23670
+ return null;
23570
23671
  }
23672
+ const reminder = pick(reasons);
23673
+ options.log?.("Read todo hygiene reminder", {
23674
+ sessionID,
23675
+ reminder,
23676
+ reasons: Array.from(reasons)
23677
+ });
23678
+ return reminder;
23571
23679
  },
23572
23680
  handleEvent(event) {
23573
23681
  if (event.type !== "session.deleted") {
@@ -23586,6 +23694,8 @@ function createTodoHygiene(options) {
23586
23694
  var HOOK_NAME = "todo-continuation";
23587
23695
  var COMMAND_NAME = "auto-continue";
23588
23696
  var CONTINUATION_PROMPT = "[Auto-continue: enabled - there are incomplete todos remaining. Continue with the next uncompleted item. Press Esc to cancel. If you need user input or review for the next item, ask instead of proceeding.]";
23697
+ var TODO_HYGIENE_INSTRUCTION_OPEN = '<instruction name="todo_hygiene">';
23698
+ var TODO_HYGIENE_INSTRUCTION_CLOSE = "</instruction>";
23589
23699
  var SUPPRESS_AFTER_ABORT_MS = 5000;
23590
23700
  var NOTIFICATION_BUSY_GRACE_MS = 250;
23591
23701
  var QUESTION_PHRASES = [
@@ -23623,6 +23733,35 @@ function resetState(state) {
23623
23733
  state.notifyingSessionIds.clear();
23624
23734
  state.notificationBusyUntilBySession.clear();
23625
23735
  }
23736
+ function stripTodoHygieneInstruction(text) {
23737
+ const trimmed = text.trimEnd();
23738
+ if (!trimmed.endsWith(TODO_HYGIENE_INSTRUCTION_CLOSE)) {
23739
+ return trimmed;
23740
+ }
23741
+ const start = trimmed.lastIndexOf(TODO_HYGIENE_INSTRUCTION_OPEN);
23742
+ if (start === -1) {
23743
+ return trimmed;
23744
+ }
23745
+ return trimmed.slice(0, start).trimEnd();
23746
+ }
23747
+ function appendTodoHygieneInstruction(message, reminder) {
23748
+ const textPart = [...message.parts].reverse().find((part) => part.type === "text" && typeof part.text === "string");
23749
+ if (!textPart)
23750
+ return;
23751
+ const baseText = stripTodoHygieneInstruction(textPart.text ?? "");
23752
+ const instruction = `${TODO_HYGIENE_INSTRUCTION_OPEN}
23753
+ ${reminder}
23754
+ ${TODO_HYGIENE_INSTRUCTION_CLOSE}`;
23755
+ textPart.text = baseText ? `${baseText}
23756
+
23757
+ ${instruction}` : instruction;
23758
+ }
23759
+ function stripTodoHygieneInstructionFromMessage(message) {
23760
+ const textPart = [...message.parts].reverse().find((part) => part.type === "text" && typeof part.text === "string");
23761
+ if (!textPart)
23762
+ return;
23763
+ textPart.text = stripTodoHygieneInstruction(textPart.text ?? "");
23764
+ }
23626
23765
  function createTodoContinuationHook(ctx, config) {
23627
23766
  const maxContinuations = config?.maxContinuations ?? 5;
23628
23767
  const cooldownMs = config?.cooldownMs ?? 3000;
@@ -23698,7 +23837,8 @@ function createTodoContinuationHook(ctx, config) {
23698
23837
  const sessionID = inferSessionID(messages, i);
23699
23838
  const partSignature = message.parts.map((part) => {
23700
23839
  if (part.type === "text" && typeof part.text === "string") {
23701
- return `${part.type}:${part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER) ? "<internal>" : part.text.trim()}`;
23840
+ const text = stripTodoHygieneInstruction(part.text);
23841
+ return `${part.type}:${text.includes(SLIM_INTERNAL_INITIATOR_MARKER) ? "<internal>" : text.trim()}`;
23702
23842
  }
23703
23843
  return part.type ?? "unknown";
23704
23844
  }).join("|");
@@ -23706,6 +23846,7 @@ function createTodoContinuationHook(ctx, config) {
23706
23846
  return {
23707
23847
  sessionID,
23708
23848
  agent: message.info.agent,
23849
+ message,
23709
23850
  signature: message.info.id ? `${message.info.id}:${partSignature}` : `${ordinal}:${partSignature}`
23710
23851
  };
23711
23852
  }
@@ -23733,9 +23874,16 @@ function createTodoContinuationHook(ctx, config) {
23733
23874
  return;
23734
23875
  }
23735
23876
  if (requestSignatureBySession.get(lastUserMessage.sessionID) === lastUserMessage.signature) {
23877
+ const reminder = hygiene.getPendingReminder(lastUserMessage.sessionID);
23878
+ if (reminder) {
23879
+ appendTodoHygieneInstruction(lastUserMessage.message, reminder);
23880
+ } else {
23881
+ stripTodoHygieneInstructionFromMessage(lastUserMessage.message);
23882
+ }
23736
23883
  return;
23737
23884
  }
23738
23885
  requestSignatureBySession.set(lastUserMessage.sessionID, lastUserMessage.signature);
23886
+ stripTodoHygieneInstructionFromMessage(lastUserMessage.message);
23739
23887
  hygiene.handleRequestStart({ sessionID: lastUserMessage.sessionID });
23740
23888
  }
23741
23889
  function markNotificationStarted(sessionID) {
@@ -24085,7 +24233,6 @@ function createTodoContinuationHook(ctx, config) {
24085
24233
  return {
24086
24234
  tool: { auto_continue: autoContinue },
24087
24235
  handleToolExecuteAfter: hygiene.handleToolExecuteAfter,
24088
- handleChatSystemTransform: hygiene.handleChatSystemTransform,
24089
24236
  handleMessagesTransform,
24090
24237
  handleEvent,
24091
24238
  handleChatMessage,
@@ -28537,6 +28684,7 @@ class MultiplexerSessionManager {
28537
28684
  sessions = new Map;
28538
28685
  knownSessions = new Map;
28539
28686
  spawningSessions = new Set;
28687
+ closingSessions = new Map;
28540
28688
  pollInterval;
28541
28689
  enabled = false;
28542
28690
  constructor(ctx, config) {
@@ -28565,17 +28713,22 @@ class MultiplexerSessionManager {
28565
28713
  const parentId = info.parentID;
28566
28714
  const title = info.title ?? "Subagent";
28567
28715
  const directory = info.directory ?? this.directory;
28568
- this.knownSessions.set(sessionId, {
28569
- parentId,
28570
- title,
28571
- directory
28572
- });
28573
28716
  if (this.isTrackedOrSpawning(sessionId)) {
28574
28717
  log("[multiplexer-session-manager] session already tracked or spawning", {
28575
28718
  sessionId
28576
28719
  });
28577
28720
  return;
28578
28721
  }
28722
+ const closing = this.closingSessions.get(sessionId);
28723
+ if (closing)
28724
+ await closing;
28725
+ if (this.isTrackedOrSpawning(sessionId))
28726
+ return;
28727
+ this.knownSessions.set(sessionId, {
28728
+ parentId,
28729
+ title,
28730
+ directory
28731
+ });
28579
28732
  this.spawningSessions.add(sessionId);
28580
28733
  try {
28581
28734
  const serverRunning = await isServerRunning(this.serverUrl);
@@ -28585,7 +28738,7 @@ class MultiplexerSessionManager {
28585
28738
  });
28586
28739
  return;
28587
28740
  }
28588
- if (this.sessions.has(sessionId)) {
28741
+ if (this.closingSessions.has(sessionId) || this.sessions.has(sessionId)) {
28589
28742
  return;
28590
28743
  }
28591
28744
  log("[multiplexer-session-manager] child session created, spawning pane", {
@@ -28599,23 +28752,31 @@ class MultiplexerSessionManager {
28599
28752
  });
28600
28753
  return { success: false, paneId: undefined };
28601
28754
  });
28602
- if (paneResult.success && paneResult.paneId) {
28603
- const now = Date.now();
28604
- this.sessions.set(sessionId, {
28755
+ if (!paneResult.success || !paneResult.paneId)
28756
+ return;
28757
+ if (!this.knownSessions.has(sessionId) || this.closingSessions.has(sessionId)) {
28758
+ await this.multiplexer.closePane(paneResult.paneId).catch((err) => log("[multiplexer-session-manager] closing stale spawned pane failed", {
28605
28759
  sessionId,
28606
28760
  paneId: paneResult.paneId,
28607
- parentId,
28608
- title,
28609
- directory,
28610
- createdAt: now,
28611
- lastSeenAt: now
28612
- });
28613
- log("[multiplexer-session-manager] pane spawned", {
28614
- sessionId,
28615
- paneId: paneResult.paneId
28616
- });
28617
- this.startPolling();
28761
+ error: String(err)
28762
+ }));
28763
+ return;
28618
28764
  }
28765
+ const now = Date.now();
28766
+ this.sessions.set(sessionId, {
28767
+ sessionId,
28768
+ paneId: paneResult.paneId,
28769
+ parentId,
28770
+ title,
28771
+ directory,
28772
+ createdAt: now,
28773
+ lastSeenAt: now
28774
+ });
28775
+ log("[multiplexer-session-manager] pane spawned", {
28776
+ sessionId,
28777
+ paneId: paneResult.paneId
28778
+ });
28779
+ this.startPolling();
28619
28780
  } finally {
28620
28781
  this.spawningSessions.delete(sessionId);
28621
28782
  }
@@ -28629,7 +28790,7 @@ class MultiplexerSessionManager {
28629
28790
  if (!sessionId)
28630
28791
  return;
28631
28792
  if (event.properties?.status?.type === "idle") {
28632
- await this.closeSession(sessionId);
28793
+ await this.closeSession(sessionId, "idle");
28633
28794
  return;
28634
28795
  }
28635
28796
  if (event.properties?.status?.type === "busy") {
@@ -28641,14 +28802,13 @@ class MultiplexerSessionManager {
28641
28802
  return;
28642
28803
  if (event.type !== "session.deleted")
28643
28804
  return;
28644
- const sessionId = event.properties?.sessionID;
28805
+ const sessionId = this.getSessionId(event);
28645
28806
  if (!sessionId)
28646
28807
  return;
28647
28808
  log("[multiplexer-session-manager] session deleted, closing pane", {
28648
28809
  sessionId
28649
28810
  });
28650
- await this.closeSession(sessionId);
28651
- this.knownSessions.delete(sessionId);
28811
+ await this.closeSession(sessionId, "deleted");
28652
28812
  }
28653
28813
  startPolling() {
28654
28814
  if (this.pollInterval)
@@ -28685,35 +28845,58 @@ class MultiplexerSessionManager {
28685
28845
  const missingTooLong = !!tracked.missingSince && now - tracked.missingSince >= SESSION_MISSING_GRACE_MS;
28686
28846
  const isTimedOut = now - tracked.createdAt > SESSION_TIMEOUT_MS;
28687
28847
  if (isIdle || missingTooLong || isTimedOut) {
28688
- sessionsToClose.push(sessionId);
28848
+ sessionsToClose.push({
28849
+ sessionId,
28850
+ reason: isIdle ? "idle" : isTimedOut ? "timeout" : "missing"
28851
+ });
28689
28852
  }
28690
28853
  }
28691
- for (const sessionId of sessionsToClose) {
28692
- await this.closeSession(sessionId);
28854
+ for (const { sessionId, reason } of sessionsToClose) {
28855
+ await this.closeSession(sessionId, reason);
28693
28856
  }
28694
28857
  } catch (err) {
28695
28858
  log("[multiplexer-session-manager] poll error", { error: String(err) });
28696
28859
  }
28697
28860
  }
28698
- async closeSession(sessionId) {
28861
+ async closeSession(sessionId, reason) {
28862
+ if (reason === "deleted") {
28863
+ this.knownSessions.delete(sessionId);
28864
+ }
28865
+ const existingClose = this.closingSessions.get(sessionId);
28866
+ if (existingClose)
28867
+ return existingClose;
28699
28868
  const tracked = this.sessions.get(sessionId);
28700
28869
  if (!tracked || !this.multiplexer)
28701
28870
  return;
28871
+ this.sessions.delete(sessionId);
28702
28872
  log("[multiplexer-session-manager] closing session pane", {
28703
28873
  sessionId,
28704
- paneId: tracked.paneId
28874
+ paneId: tracked.paneId,
28875
+ reason
28705
28876
  });
28706
- await this.multiplexer.closePane(tracked.paneId);
28707
- this.sessions.delete(sessionId);
28708
- if (this.sessions.size === 0) {
28709
- this.stopPolling();
28710
- }
28877
+ const closePromise = this.multiplexer.closePane(tracked.paneId).then(() => {
28878
+ return;
28879
+ }).catch((err) => log("[multiplexer-session-manager] failed to close session pane", {
28880
+ sessionId,
28881
+ paneId: tracked.paneId,
28882
+ reason,
28883
+ error: String(err)
28884
+ })).finally(() => {
28885
+ this.closingSessions.delete(sessionId);
28886
+ this.updatePolling();
28887
+ });
28888
+ this.closingSessions.set(sessionId, closePromise);
28889
+ await closePromise;
28711
28890
  }
28712
28891
  async respawnIfKnown(sessionId) {
28713
28892
  if (!this.enabled || !this.multiplexer)
28714
28893
  return;
28715
- if (this.isTrackedOrSpawning(sessionId))
28894
+ const closing = this.closingSessions.get(sessionId);
28895
+ if (closing)
28896
+ await closing;
28897
+ if (this.isTrackedOrSpawning(sessionId)) {
28716
28898
  return;
28899
+ }
28717
28900
  const known = this.knownSessions.get(sessionId);
28718
28901
  if (!known)
28719
28902
  return;
@@ -28727,8 +28910,9 @@ class MultiplexerSessionManager {
28727
28910
  });
28728
28911
  return;
28729
28912
  }
28730
- if (this.sessions.has(sessionId))
28913
+ if (this.sessions.has(sessionId) || this.closingSessions.has(sessionId)) {
28731
28914
  return;
28915
+ }
28732
28916
  log("[multiplexer-session-manager] child session busy again, respawning pane", {
28733
28917
  sessionId,
28734
28918
  parentId: known.parentId,
@@ -28742,6 +28926,14 @@ class MultiplexerSessionManager {
28742
28926
  });
28743
28927
  if (!paneResult.success || !paneResult.paneId)
28744
28928
  return;
28929
+ if (!this.knownSessions.has(sessionId) || this.closingSessions.has(sessionId)) {
28930
+ await this.multiplexer.closePane(paneResult.paneId).catch((err) => log("[multiplexer-session-manager] closing stale respawned pane failed", {
28931
+ sessionId,
28932
+ paneId: paneResult.paneId,
28933
+ error: String(err)
28934
+ }));
28935
+ return;
28936
+ }
28745
28937
  const now = Date.now();
28746
28938
  this.sessions.set(sessionId, {
28747
28939
  sessionId,
@@ -28764,8 +28956,21 @@ class MultiplexerSessionManager {
28764
28956
  isTrackedOrSpawning(sessionId) {
28765
28957
  return this.sessions.has(sessionId) || this.spawningSessions.has(sessionId);
28766
28958
  }
28959
+ updatePolling() {
28960
+ if (this.sessions.size > 0 || this.closingSessions.size > 0) {
28961
+ this.startPolling();
28962
+ } else {
28963
+ this.stopPolling();
28964
+ }
28965
+ }
28966
+ getSessionId(event) {
28967
+ return event.properties?.info?.id ?? event.properties?.sessionID;
28968
+ }
28767
28969
  async cleanup() {
28768
28970
  this.stopPolling();
28971
+ if (this.closingSessions.size > 0) {
28972
+ await Promise.all(this.closingSessions.values());
28973
+ }
28769
28974
  if (this.sessions.size > 0 && this.multiplexer) {
28770
28975
  log("[multiplexer-session-manager] closing all panes", {
28771
28976
  count: this.sessions.size
@@ -28780,6 +28985,7 @@ class MultiplexerSessionManager {
28780
28985
  }
28781
28986
  this.knownSessions.clear();
28782
28987
  this.spawningSessions.clear();
28988
+ this.closingSessions.clear();
28783
28989
  log("[multiplexer-session-manager] cleanup complete");
28784
28990
  }
28785
28991
  }
@@ -29419,7 +29625,7 @@ Returns the councillor responses with a summary footer.`,
29419
29625
  // src/tools/preset-manager.ts
29420
29626
  var COMMAND_NAME3 = "preset";
29421
29627
  function createPresetManager(ctx, config) {
29422
- let activePreset = config.preset ?? null;
29628
+ let activePreset = getActiveRuntimePreset() ?? config.preset ?? null;
29423
29629
  async function handleCommandExecuteBefore(input, output) {
29424
29630
  if (input.command !== COMMAND_NAME3) {
29425
29631
  return;
@@ -29460,21 +29666,41 @@ function createPresetManager(ctx, config) {
29460
29666
  }
29461
29667
  const agentUpdates = {};
29462
29668
  for (const [agentName, override] of Object.entries(preset)) {
29669
+ const resolvedName = AGENT_ALIASES[agentName] ?? agentName;
29463
29670
  const agentConfig = mapOverrideToAgentConfig(override);
29464
29671
  if (Object.keys(agentConfig).length > 0) {
29465
- agentUpdates[agentName] = agentConfig;
29672
+ agentUpdates[resolvedName] = agentConfig;
29466
29673
  }
29467
29674
  }
29468
- if (Object.keys(agentUpdates).length === 0) {
29675
+ const currentRuntimePreset = getActiveRuntimePreset();
29676
+ const resetUpdates = {};
29677
+ if (currentRuntimePreset && config.presets?.[currentRuntimePreset]) {
29678
+ const oldPreset = config.presets[currentRuntimePreset];
29679
+ for (const rawName of Object.keys(oldPreset)) {
29680
+ const resolvedOld = AGENT_ALIASES[rawName] ?? rawName;
29681
+ if (resolvedOld in agentUpdates)
29682
+ continue;
29683
+ const baseline = config.agents?.[resolvedOld];
29684
+ if (baseline) {
29685
+ resetUpdates[resolvedOld] = mapOverrideToAgentConfig(baseline);
29686
+ }
29687
+ }
29688
+ }
29689
+ const hasAgentUpdates = Object.keys(agentUpdates).length > 0;
29690
+ const allUpdates = { ...resetUpdates, ...agentUpdates };
29691
+ if (!hasAgentUpdates) {
29469
29692
  output.parts.push(createInternalAgentTextPart(`Preset "${presetName}" is empty (no agent overrides defined).`));
29470
29693
  return;
29471
29694
  }
29695
+ const previousPreset = activePreset;
29696
+ setActiveRuntimePresetWithPrevious(presetName);
29472
29697
  try {
29473
29698
  await ctx.client.config.update({
29474
- body: { agent: agentUpdates }
29699
+ body: { agent: allUpdates }
29475
29700
  });
29476
29701
  activePreset = presetName;
29477
- const summary = Object.entries(agentUpdates).map(([name, cfg]) => {
29702
+ const summaryParts = [];
29703
+ for (const [name, cfg] of Object.entries(agentUpdates)) {
29478
29704
  const parts = [name];
29479
29705
  if (cfg.model)
29480
29706
  parts.push(`model: ${cfg.model}`);
@@ -29484,12 +29710,16 @@ function createPresetManager(ctx, config) {
29484
29710
  parts.push(`temp: ${cfg.temperature}`);
29485
29711
  if (cfg.options)
29486
29712
  parts.push("options: yes");
29487
- return parts.join(" → ");
29488
- }).join(`
29489
- `);
29713
+ summaryParts.push(parts.join(" → "));
29714
+ }
29715
+ if (Object.keys(resetUpdates).length > 0) {
29716
+ summaryParts.push(`Reset to baseline: ${Object.keys(resetUpdates).join(", ")}`);
29717
+ }
29490
29718
  output.parts.push(createInternalAgentTextPart(`Switched to preset "${presetName}":
29491
- ${summary}`));
29719
+ ${summaryParts.join(`
29720
+ `)}`));
29492
29721
  } catch (err) {
29722
+ rollbackRuntimePreset(previousPreset);
29493
29723
  output.parts.push(createInternalAgentTextPart(`Failed to switch preset "${presetName}": ${String(err)}`));
29494
29724
  }
29495
29725
  }
@@ -31967,6 +32197,14 @@ var OhMyOpenCodeLite = async (ctx) => {
31967
32197
  let toolCount = 0;
31968
32198
  try {
31969
32199
  config = loadPluginConfig(ctx.directory);
32200
+ const runtimePreset = getActiveRuntimePreset();
32201
+ if (runtimePreset && config.presets?.[runtimePreset]) {
32202
+ config.preset = runtimePreset;
32203
+ const presetAgents = config.presets[runtimePreset];
32204
+ config.agents = deepMerge(config.agents, presetAgents);
32205
+ } else if (runtimePreset) {
32206
+ setActiveRuntimePreset(null);
32207
+ }
31970
32208
  disabledAgents = getDisabledAgents(config);
31971
32209
  rewriteDisplayNameMentions = createDisplayNameMentionRewriter(config);
31972
32210
  agentDefs = createAgents(config);
@@ -32163,6 +32401,83 @@ var OhMyOpenCodeLite = async (ctx) => {
32163
32401
  });
32164
32402
  }
32165
32403
  }
32404
+ const runtimePresetName = getActiveRuntimePreset();
32405
+ if (runtimePresetName && config.presets?.[runtimePresetName]) {
32406
+ const runtimePreset = config.presets[runtimePresetName];
32407
+ for (const [agentName, override] of Object.entries(runtimePreset)) {
32408
+ const resolvedName = AGENT_ALIASES[agentName] ?? agentName;
32409
+ const entry = configAgent[resolvedName];
32410
+ if (!entry)
32411
+ continue;
32412
+ if (typeof override.model === "string") {
32413
+ entry.model = override.model;
32414
+ } else if (Array.isArray(override.model) && override.model.length > 0) {
32415
+ const first = override.model[0];
32416
+ entry.model = typeof first === "string" ? first : first.id;
32417
+ if (typeof first !== "string" && first.variant) {
32418
+ entry.variant = first.variant;
32419
+ }
32420
+ }
32421
+ if (typeof override.variant === "string") {
32422
+ entry.variant = override.variant;
32423
+ } else if ("variant" in override) {
32424
+ delete entry.variant;
32425
+ }
32426
+ if (typeof override.temperature === "number") {
32427
+ entry.temperature = override.temperature;
32428
+ } else if ("temperature" in override) {
32429
+ delete entry.temperature;
32430
+ }
32431
+ if (override.options && typeof override.options === "object" && !Array.isArray(override.options)) {
32432
+ entry.options = override.options;
32433
+ } else if ("options" in override) {
32434
+ delete entry.options;
32435
+ }
32436
+ log("[plugin] runtime preset override", {
32437
+ preset: runtimePresetName,
32438
+ agent: agentName,
32439
+ model: entry.model
32440
+ });
32441
+ }
32442
+ const prevPresetName = getPreviousRuntimePreset();
32443
+ if (prevPresetName && config.presets?.[prevPresetName]) {
32444
+ const prevPreset = config.presets[prevPresetName];
32445
+ const newPresetResolved = new Set(Object.keys(runtimePreset).map((k) => AGENT_ALIASES[k] ?? k));
32446
+ for (const agentName of Object.keys(prevPreset)) {
32447
+ const resolvedName = AGENT_ALIASES[agentName] ?? agentName;
32448
+ if (newPresetResolved.has(resolvedName))
32449
+ continue;
32450
+ const entry = configAgent[resolvedName];
32451
+ if (!entry)
32452
+ continue;
32453
+ const baseline = config.agents?.[resolvedName];
32454
+ const prevOverride = prevPreset[agentName];
32455
+ if (typeof baseline?.model === "string") {
32456
+ entry.model = baseline.model;
32457
+ }
32458
+ if (typeof baseline?.variant === "string") {
32459
+ entry.variant = baseline.variant;
32460
+ } else if (prevOverride && "variant" in prevOverride) {
32461
+ delete entry.variant;
32462
+ }
32463
+ if (typeof baseline?.temperature === "number") {
32464
+ entry.temperature = baseline.temperature;
32465
+ } else if (prevOverride && "temperature" in prevOverride) {
32466
+ delete entry.temperature;
32467
+ }
32468
+ if (baseline?.options && typeof baseline.options === "object" && !Array.isArray(baseline.options)) {
32469
+ entry.options = baseline.options;
32470
+ } else if (prevOverride && "options" in prevOverride) {
32471
+ delete entry.options;
32472
+ }
32473
+ log("[plugin] runtime preset reset from previous", {
32474
+ previousPreset: prevPresetName,
32475
+ agent: resolvedName,
32476
+ model: entry.model
32477
+ });
32478
+ }
32479
+ }
32480
+ }
32166
32481
  const configMcp = opencodeConfig.mcp;
32167
32482
  if (!configMcp) {
32168
32483
  opencodeConfig.mcp = { ...mcps };
@@ -32220,7 +32535,6 @@ var OhMyOpenCodeLite = async (ctx) => {
32220
32535
  await multiplexerSessionManager.onSessionStatus(event);
32221
32536
  await multiplexerSessionManager.onSessionDeleted(event);
32222
32537
  await interviewManager.handleEvent(input);
32223
- await postFileToolNudgeHook.event(input);
32224
32538
  await taskSessionManagerHook.event(input);
32225
32539
  if (input.event.type === "session.deleted") {
32226
32540
  const props = input.event.properties;
@@ -32269,9 +32583,6 @@ var OhMyOpenCodeLite = async (ctx) => {
32269
32583
  ${output.system[0]}` : "");
32270
32584
  }
32271
32585
  }
32272
- await todoContinuationHook.handleChatSystemTransform(input, output);
32273
- await postFileToolNudgeHook["experimental.chat.system.transform"](input, output);
32274
- await taskSessionManagerHook["experimental.chat.system.transform"](input, output);
32275
32586
  collapseSystemInPlace(output.system);
32276
32587
  },
32277
32588
  "experimental.chat.messages.transform": async (input, output) => {
@@ -32296,13 +32607,14 @@ ${output.system[0]}` : "");
32296
32607
  await todoContinuationHook.handleMessagesTransform({
32297
32608
  messages: typedOutput.messages
32298
32609
  });
32610
+ await taskSessionManagerHook["experimental.chat.messages.transform"](input, typedOutput);
32299
32611
  await phaseReminderHook["experimental.chat.messages.transform"](input, typedOutput);
32300
32612
  await filterAvailableSkillsHook["experimental.chat.messages.transform"](input, typedOutput);
32301
32613
  },
32302
32614
  "tool.execute.after": async (input, output) => {
32303
32615
  await delegateTaskRetryHook["tool.execute.after"](input, output);
32304
32616
  await jsonErrorRecoveryHook["tool.execute.after"](input, output);
32305
- await todoContinuationHook.handleToolExecuteAfter(input);
32617
+ await todoContinuationHook.handleToolExecuteAfter(input, output);
32306
32618
  await postFileToolNudgeHook["tool.execute.after"](input, output);
32307
32619
  await taskSessionManagerHook["tool.execute.after"](input, output);
32308
32620
  }