oh-my-opencode-slim 1.0.2 → 1.0.4

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
@@ -6246,33 +6246,33 @@ var require_URL = __commonJS((exports, module) => {
6246
6246
  else
6247
6247
  return basepath.substring(0, lastslash + 1) + refpath;
6248
6248
  }
6249
- function remove_dot_segments(path13) {
6250
- if (!path13)
6251
- return path13;
6249
+ function remove_dot_segments(path14) {
6250
+ if (!path14)
6251
+ return path14;
6252
6252
  var output = "";
6253
- while (path13.length > 0) {
6254
- if (path13 === "." || path13 === "..") {
6255
- path13 = "";
6253
+ while (path14.length > 0) {
6254
+ if (path14 === "." || path14 === "..") {
6255
+ path14 = "";
6256
6256
  break;
6257
6257
  }
6258
- var twochars = path13.substring(0, 2);
6259
- var threechars = path13.substring(0, 3);
6260
- var fourchars = path13.substring(0, 4);
6258
+ var twochars = path14.substring(0, 2);
6259
+ var threechars = path14.substring(0, 3);
6260
+ var fourchars = path14.substring(0, 4);
6261
6261
  if (threechars === "../") {
6262
- path13 = path13.substring(3);
6262
+ path14 = path14.substring(3);
6263
6263
  } else if (twochars === "./") {
6264
- path13 = path13.substring(2);
6264
+ path14 = path14.substring(2);
6265
6265
  } else if (threechars === "/./") {
6266
- path13 = "/" + path13.substring(3);
6267
- } else if (twochars === "/." && path13.length === 2) {
6268
- path13 = "/";
6269
- } else if (fourchars === "/../" || threechars === "/.." && path13.length === 3) {
6270
- path13 = "/" + path13.substring(4);
6266
+ path14 = "/" + path14.substring(3);
6267
+ } else if (twochars === "/." && path14.length === 2) {
6268
+ path14 = "/";
6269
+ } else if (fourchars === "/../" || threechars === "/.." && path14.length === 3) {
6270
+ path14 = "/" + path14.substring(4);
6271
6271
  output = output.replace(/\/?[^\/]*$/, "");
6272
6272
  } else {
6273
- var segment = path13.match(/(\/?([^\/]*))/)[0];
6273
+ var segment = path14.match(/(\/?([^\/]*))/)[0];
6274
6274
  output += segment;
6275
- path13 = path13.substring(segment.length);
6275
+ path14 = path14.substring(segment.length);
6276
6276
  }
6277
6277
  }
6278
6278
  return output;
@@ -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;
@@ -18526,7 +18526,9 @@ var InterviewConfigSchema = z2.object({
18526
18526
  dashboard: z2.boolean().default(false)
18527
18527
  });
18528
18528
  var SessionManagerConfigSchema = z2.object({
18529
- maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2)
18529
+ maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2),
18530
+ readContextMinLines: z2.number().int().min(0).max(1000).default(10),
18531
+ readContextMaxFiles: z2.number().int().min(0).max(50).default(8)
18530
18532
  });
18531
18533
  var TodoContinuationConfigSchema = z2.object({
18532
18534
  maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
@@ -18852,14 +18854,15 @@ var AGENT_DESCRIPTIONS = {
18852
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
18853
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.`,
18854
18856
  council: `@council
18855
- - 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.
18856
18858
  - Permissions: Read files
18857
18859
  - Stats: 3x slower than orchestrator, 3x or more cost of orchestrator
18858
- - Capabilities: Runs multiple models in parallel, synthesizes their responses into a consensus answer
18859
- - **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
18860
- - **Don't delegate when:** Straightforward tasks you're confident about • Speed matters more than confidence • Single-model answer is sufficientRoutine implementation work
18861
- - **Result handling:** Present the council's synthesized response verbatim. Do not re-summarize or condense.
18862
- - **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.`,
18863
18866
  observer: `@observer
18864
18867
  - Role: Visual analysis specialist for images, PDFs, and diagrams
18865
18868
  - Permissions: Read files
@@ -18951,10 +18954,10 @@ Balance: respect dependencies, avoid parallelizing what must be sequential.
18951
18954
  5. Adjust if needed
18952
18955
 
18953
18956
  ### Session Reuse
18954
- - Reuse an available specialist session only for clear follow-up work on the same thread.
18955
- - 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
18956
18959
  - If multiple remembered sessions fit, prefer the most recently used matching session.
18957
- - If reuse is unclear, start a fresh session.
18960
+ - Prefer re-uses over creating new sessions all the time
18958
18961
 
18959
18962
  ### Auto-Continue
18960
18963
  When working through multi-step tasks, consider enabling auto-continue to avoid stopping between batches:
@@ -19042,27 +19045,47 @@ var COUNCIL_AGENT_PROMPT = `You are the Council agent — a multi-LLM orchestrat
19042
19045
  1. Call the \`council_session\` tool with the user's prompt
19043
19046
  2. Optionally specify a preset (default: "default")
19044
19047
  3. Receive the councillor responses formatted for synthesis
19045
- 4. Synthesize the optimal final answer from the councillor responses
19046
- 5. Present the synthesized result to the user
19047
-
19048
- **Synthesis Guidelines**:
19049
- When you receive councillor responses, synthesize them into the optimal final answer:
19050
- - Review all councillor responses thoroughly and create the best possible answer
19051
- - Credit specific insights from individual councillors by name (e.g., "alpha noted that...", "beta suggested...")
19052
- - Clearly explain your reasoning for the chosen approach
19053
- - Be transparent about trade-offs when different approaches have valid pros/cons
19054
- - Note any remaining uncertainties or areas where further investigation is needed
19055
- - If councillors disagree, explain the resolution and your reasoning
19056
- - Acknowledge if consensus was impossible and explain why
19057
- - Don't just average responses — choose the best approach and improve upon it
19058
- - 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
19059
19058
 
19060
19059
  **Behavior**:
19061
19060
  - Delegate requests directly to council_session
19062
19061
  - Don't pre-analyze or filter the prompt before calling council_session
19063
- - Synthesize the councillor results into a comprehensive, coherent answer
19064
- - Include attribution for valuable insights from specific councillors
19065
- - 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.`;
19066
19089
  function createCouncilAgent(model, customPrompt, customAppendPrompt) {
19067
19090
  const prompt = resolvePrompt(COUNCIL_AGENT_PROMPT, customPrompt, customAppendPrompt);
19068
19091
  const definition = {
@@ -19135,7 +19158,7 @@ ${failedSection}`;
19135
19158
 
19136
19159
  ---
19137
19160
 
19138
- 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).`;
19139
19162
  return prompt;
19140
19163
  }
19141
19164
 
@@ -19767,14 +19790,37 @@ function getDisabledAgents(config) {
19767
19790
  return disabled;
19768
19791
  }
19769
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
+
19770
19814
  // src/utils/logger.ts
19771
19815
  import * as fs2 from "node:fs";
19816
+ import { appendFile } from "node:fs/promises";
19772
19817
  import * as os from "node:os";
19773
19818
  import * as path2 from "node:path";
19774
19819
  var LOG_PREFIX = "oh-my-opencode-slim.";
19775
19820
  var LOG_SUFFIX = ".log";
19776
19821
  var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
19777
19822
  var logFile = null;
19823
+ var writeChain = Promise.resolve();
19778
19824
  function getLogDir() {
19779
19825
  return process.env.OPENCODE_LOG_DIR ?? path2.join(os.homedir(), ".local/share/opencode");
19780
19826
  }
@@ -19817,10 +19863,14 @@ function initLogger(sessionId) {
19817
19863
  fs2.mkdirSync(dir, { recursive: true });
19818
19864
  } catch {}
19819
19865
  logFile = path2.join(dir, `${LOG_PREFIX}${sessionId}${LOG_SUFFIX}`);
19866
+ try {
19867
+ fs2.closeSync(fs2.openSync(logFile, "a"));
19868
+ } catch {}
19820
19869
  cleanupOldLogs(dir);
19821
19870
  }
19822
19871
  function log(message, data) {
19823
- if (!logFile)
19872
+ const target = logFile;
19873
+ if (!target)
19824
19874
  return;
19825
19875
  try {
19826
19876
  const timestamp = new Date().toISOString();
@@ -19834,7 +19884,7 @@ function log(message, data) {
19834
19884
  }
19835
19885
  const logEntry = `[${timestamp}] ${message} ${dataStr}
19836
19886
  `;
19837
- fs2.appendFileSync(logFile, logEntry);
19887
+ writeChain = writeChain.then(() => appendFile(target, logEntry)).catch(() => {});
19838
19888
  } catch {}
19839
19889
  }
19840
19890
 
@@ -20020,7 +20070,7 @@ class CouncilManager {
20020
20070
  }
20021
20071
  } else {
20022
20072
  const promises = entries.map(([name, config], index) => (async () => {
20023
- if (index > 0) {
20073
+ if (this.tmuxEnabled && index > 0) {
20024
20074
  await new Promise((r) => setTimeout(r, index * COUNCILLOR_STAGGER_MS));
20025
20075
  }
20026
20076
  return this.runCouncillorWithRetry(name, config, prompt, parentSessionId, timeout, maxRetries);
@@ -20206,6 +20256,16 @@ function unexpectedPatchLine(context, line) {
20206
20256
  const rendered = line.length === 0 ? "<empty>" : line;
20207
20257
  throw new Error(`Invalid patch format: unexpected line ${context}: ${rendered}`);
20208
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
+ }
20209
20269
  function parseChunks(lines, index, mode) {
20210
20270
  const chunks = [];
20211
20271
  let at = index;
@@ -20217,7 +20277,7 @@ function parseChunks(lines, index, mode) {
20217
20277
  at += 1;
20218
20278
  continue;
20219
20279
  }
20220
- const context = lines[at].slice(2).trim() || undefined;
20280
+ const context = parseChangeContext(lines[at]);
20221
20281
  at += 1;
20222
20282
  const old_lines = [];
20223
20283
  const new_lines = [];
@@ -20260,12 +20320,11 @@ function parseChunks(lines, index, mode) {
20260
20320
  return { chunks, next: at };
20261
20321
  }
20262
20322
  function parseAdd(lines, index, mode) {
20263
- let contents = "";
20323
+ const contents = [];
20264
20324
  let at = index;
20265
20325
  while (at < lines.length && !lines[at].startsWith("***")) {
20266
20326
  if (lines[at].startsWith("+")) {
20267
- contents += `${lines[at].slice(1)}
20268
- `;
20327
+ contents.push(lines[at].slice(1));
20269
20328
  at += 1;
20270
20329
  continue;
20271
20330
  }
@@ -20274,18 +20333,15 @@ function parseAdd(lines, index, mode) {
20274
20333
  }
20275
20334
  at += 1;
20276
20335
  }
20277
- if (contents.endsWith(`
20278
- `)) {
20279
- contents = contents.slice(0, -1);
20280
- }
20281
- return { content: contents, next: at };
20336
+ return { content: contents.join(`
20337
+ `), next: at };
20282
20338
  }
20283
20339
  function parsePatchInternal(patchText, mode) {
20284
20340
  const clean = normalizePatchText(patchText);
20285
20341
  const lines = clean.split(`
20286
20342
  `);
20287
- const begin = lines.findIndex((line) => line.trim() === "*** Begin Patch");
20288
- 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"));
20289
20345
  if (begin === -1 || end === -1 || begin >= end) {
20290
20346
  throw new Error("Invalid patch format: missing Begin/End markers");
20291
20347
  }
@@ -20417,12 +20473,6 @@ function formatPatch(patch) {
20417
20473
  }
20418
20474
 
20419
20475
  // src/hooks/apply-patch/matching.ts
20420
- var AUTO_RESCUE_COMPARATOR_NAMES = new Set([
20421
- "exact",
20422
- "unicode",
20423
- "trim-end",
20424
- "unicode-trim-end"
20425
- ]);
20426
20476
  function equalExact(a, b) {
20427
20477
  return a === b;
20428
20478
  }
@@ -20441,7 +20491,7 @@ function equalTrim(a, b) {
20441
20491
  function equalUnicodeTrim(a, b) {
20442
20492
  return normalizeUnicode(a.trim()) === normalizeUnicode(b.trim());
20443
20493
  }
20444
- var comparatorEntries = [
20494
+ var autoRescueComparatorEntries = [
20445
20495
  { name: "exact", exact: true, same: equalExact },
20446
20496
  { name: "unicode", exact: false, same: equalUnicodeExact },
20447
20497
  { name: "trim-end", exact: false, same: equalTrimEnd },
@@ -20449,14 +20499,44 @@ var comparatorEntries = [
20449
20499
  name: "unicode-trim-end",
20450
20500
  exact: false,
20451
20501
  same: equalUnicodeTrimEnd
20452
- },
20502
+ }
20503
+ ];
20504
+ var comparatorEntries = [
20505
+ ...autoRescueComparatorEntries,
20453
20506
  { name: "trim", exact: false, same: equalTrim },
20454
20507
  { name: "unicode-trim", exact: false, same: equalUnicodeTrim }
20455
20508
  ];
20456
- var autoRescueComparatorEntries = comparatorEntries.filter((entry) => AUTO_RESCUE_COMPARATOR_NAMES.has(entry.name));
20457
20509
  var MAX_LCS_CHUNK_LINES = 48;
20458
20510
  var MAX_LCS_CANDIDATES = 64;
20459
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
+ }
20460
20540
  var permissiveComparators = comparatorEntries.map((entry) => entry.same);
20461
20541
  function tryMatch(lines, pattern, start, comparator, eof) {
20462
20542
  if (eof) {
@@ -20530,6 +20610,19 @@ function list(lines, pattern, start, same) {
20530
20610
  }
20531
20611
  return out;
20532
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
+ }
20533
20626
  function sameRescueLine(a, b) {
20534
20627
  return equalExact(a, b) || equalUnicodeExact(a, b);
20535
20628
  }
@@ -20556,36 +20649,102 @@ function rescueByPrefixSuffix(lines, old_lines, new_lines, start) {
20556
20649
  const left = old_lines.slice(0, prefixLength);
20557
20650
  const right = old_lines.slice(old_lines.length - suffixLength);
20558
20651
  const middle = new_lines.slice(prefixLength, new_lines.length - suffixLength);
20559
- 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;
20560
20658
  for (const same of autoRescueComparators) {
20561
- 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) {
20562
20668
  const from = leftIndex + left.length;
20563
- 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];
20564
20671
  const key = `${from}:${rightIndex}`;
20565
- 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 = {
20566
20721
  start: from,
20567
20722
  del: rightIndex - from,
20568
20723
  add: [...middle]
20569
- });
20724
+ };
20725
+ }
20726
+ if (hits.size > 1) {
20727
+ return { kind: "ambiguous", phase: "prefix_suffix" };
20570
20728
  }
20571
20729
  }
20572
20730
  }
20573
- if (hits.size === 0) {
20731
+ if (!hit) {
20574
20732
  return { kind: "miss" };
20575
20733
  }
20576
- if (hits.size > 1) {
20577
- return { kind: "ambiguous", phase: "prefix_suffix" };
20578
- }
20579
- return { kind: "match", hit: [...hits.values()][0] };
20734
+ return { kind: "match", hit };
20580
20735
  }
20581
20736
  function score(a, b) {
20582
- 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);
20583
20740
  for (let i = 1;i <= a.length; i += 1) {
20741
+ const current = Array(b.length + 1).fill(0);
20584
20742
  for (let j = 1;j <= b.length; j += 1) {
20585
- 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]);
20586
20744
  }
20745
+ previous = current;
20587
20746
  }
20588
- return dp[a.length][b.length];
20747
+ return previous[b.length];
20589
20748
  }
20590
20749
  function normalizeLcsLine(line) {
20591
20750
  return normalizeUnicode(line).trim();
@@ -20612,45 +20771,23 @@ function countLcsUpperBound(a, b) {
20612
20771
  }
20613
20772
  return shared;
20614
20773
  }
20615
- function hasStableBorders(oldLines, candidate) {
20616
- if (oldLines.length === 0 || candidate.length !== oldLines.length) {
20617
- return false;
20618
- }
20619
- const same = autoRescueComparators.some((compare) => compare(oldLines[0], candidate[0]));
20620
- if (!same) {
20621
- return false;
20622
- }
20623
- if (oldLines.length === 1) {
20624
- return true;
20625
- }
20626
- return autoRescueComparators.some((compare) => compare(oldLines[oldLines.length - 1], candidate[candidate.length - 1]));
20627
- }
20628
20774
  function collectBorderAnchoredStarts(lines, oldLines, start) {
20629
20775
  if (oldLines.length === 0) {
20630
20776
  return [];
20631
20777
  }
20632
- const firstHits = new Set;
20633
- const lastHits = new Set;
20634
- const lastLine = oldLines[oldLines.length - 1];
20635
- for (const same of autoRescueComparators) {
20636
- for (const index of list(lines, [oldLines[0]], start, same)) {
20637
- firstHits.add(index);
20638
- }
20639
- for (const index of list(lines, [lastLine], start, same)) {
20640
- lastHits.add(index);
20641
- }
20642
- }
20643
20778
  const candidates = [];
20644
- for (const index of [...firstHits].sort((a, b) => a - b)) {
20645
- const end = index + oldLines.length - 1;
20646
- 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) {
20647
20786
  continue;
20648
20787
  }
20649
- const candidate = lines.slice(index, index + oldLines.length);
20650
- if (!hasStableBorders(oldLines, candidate)) {
20651
- continue;
20788
+ if (oldLines.length === 1 || matchPreparedAutoRescueComparator(lines[end], lastLine) !== undefined) {
20789
+ candidates.push(index);
20652
20790
  }
20653
- candidates.push(index);
20654
20791
  }
20655
20792
  return candidates;
20656
20793
  }
@@ -20658,11 +20795,6 @@ function rescueByLcs(lines, old_lines, new_lines, start) {
20658
20795
  if (old_lines.length === 0 || lines.length === 0) {
20659
20796
  return { kind: "miss" };
20660
20797
  }
20661
- const from = start;
20662
- const to = lines.length - old_lines.length;
20663
- if (to < from) {
20664
- return { kind: "miss" };
20665
- }
20666
20798
  if (old_lines.length > MAX_LCS_CHUNK_LINES) {
20667
20799
  return { kind: "miss" };
20668
20800
  }
@@ -20675,9 +20807,6 @@ function rescueByLcs(lines, old_lines, new_lines, start) {
20675
20807
  let bestScore = 0;
20676
20808
  let ties = 0;
20677
20809
  for (const index of candidates) {
20678
- if (index < from || index > to) {
20679
- continue;
20680
- }
20681
20810
  const window2 = lines.slice(index, index + old_lines.length);
20682
20811
  if (countLcsUpperBound(old_lines, window2) < needed) {
20683
20812
  continue;
@@ -20732,26 +20861,29 @@ function resolveChunkStart(lines, chunk, start) {
20732
20861
  return at === -1 ? start : at + 1;
20733
20862
  }
20734
20863
  function resolveUniqueAnchor(lines, changeContext, start) {
20735
- const hits = new Set;
20736
- for (const same of autoRescueComparators) {
20737
- for (const index2 of list(lines, [changeContext], start, same)) {
20738
- 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;
20871
+ }
20872
+ if (matchedIndex !== undefined) {
20873
+ return { kind: "ambiguous" };
20739
20874
  }
20875
+ matchedIndex = index;
20876
+ matchedComparator = comparator;
20740
20877
  }
20741
- if (hits.size === 0) {
20878
+ if (matchedIndex === undefined) {
20742
20879
  return { kind: "missing" };
20743
20880
  }
20744
- if (hits.size > 1) {
20745
- return { kind: "ambiguous" };
20746
- }
20747
- const index = [...hits][0];
20748
- const canonicalLine = lines[index];
20749
- const comparator = seekMatch(lines, [changeContext], index)?.comparator;
20881
+ const canonicalLine = lines[matchedIndex];
20750
20882
  return {
20751
20883
  kind: "match",
20752
- index,
20884
+ index: matchedIndex,
20753
20885
  exact: canonicalLine === changeContext,
20754
- comparator: comparator ?? "exact",
20886
+ comparator: matchedComparator ?? "exact",
20755
20887
  canonicalLine
20756
20888
  };
20757
20889
  }
@@ -20902,10 +21034,11 @@ ${chunk.change_context}`);
20902
21034
  old_lines: [],
20903
21035
  canonical_old_lines: [anchor],
20904
21036
  canonical_new_lines: [...chunk.new_lines, anchor],
21037
+ canonical_change_context: anchorMatch.exact ? undefined : anchorMatch.canonicalLine,
20905
21038
  resolved_is_end_of_file: insertAt + 1 === lines.length,
20906
21039
  rewritten: true,
20907
21040
  strategy,
20908
- matchComparator: "exact"
21041
+ matchComparator: anchorMatch.comparator
20909
21042
  });
20910
21043
  start = insertAt;
20911
21044
  continue;
@@ -21279,15 +21412,6 @@ function renderRewriteDependencyGroup(group, cfg) {
21279
21412
  }
21280
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);
21281
21414
  }
21282
- function rewriteModeForDependentUpdate(group) {
21283
- if (group.kind === "add") {
21284
- return "collapse:add-followed-by-update";
21285
- }
21286
- if (group.group.outputPath !== group.group.sourcePath) {
21287
- return "collapse:move-followed-by-update";
21288
- }
21289
- return "merge:same-file-updates";
21290
- }
21291
21415
  function combineDependentUpdateGroup(filePath, group, nextChunks, finalText, nextOutputPath, nextOutputFilePath, cfg) {
21292
21416
  if (group.kind === "add") {
21293
21417
  return {
@@ -21327,12 +21451,6 @@ async function rewritePatch(root, patchText, cfg, worktree) {
21327
21451
  const normalizedPatchText = normalizePatchText(patchText);
21328
21452
  const rewritten = [];
21329
21453
  let changed = false;
21330
- let rewrittenChunks = 0;
21331
- const rewriteModes = new Set;
21332
- const totalChunks = hunks.reduce((count, hunk) => count + (hunk.type === "update" ? hunk.chunks.length : 0), 0);
21333
- if (pathsNormalized) {
21334
- rewriteModes.add("normalize:patch-paths");
21335
- }
21336
21454
  const dependencyGroups = new Map;
21337
21455
  for (const hunk of hunks) {
21338
21456
  if (hunk.type === "add") {
@@ -21387,14 +21505,6 @@ async function rewritePatch(root, patchText, cfg, worktree) {
21387
21505
  continue;
21388
21506
  }
21389
21507
  changed = true;
21390
- rewrittenChunks += 1;
21391
- if (chunk.strategy) {
21392
- rewriteModes.add(chunk.strategy);
21393
- continue;
21394
- }
21395
- if (chunk.matchComparator && chunk.matchComparator !== "exact") {
21396
- rewriteModes.add(`match:${chunk.matchComparator}`);
21397
- }
21398
21508
  }
21399
21509
  const nextOutputPath = hunk.move_path ?? hunk.path;
21400
21510
  const nextOutputFilePath = movePath ?? filePath;
@@ -21402,7 +21512,6 @@ async function rewritePatch(root, patchText, cfg, worktree) {
21402
21512
  const nextGroup = combineDependentUpdateGroup(filePath, currentDependency, next, nextText, nextOutputPath, nextOutputFilePath, cfg);
21403
21513
  rewritten[currentDependency.group.index] = renderRewriteDependencyGroup(nextGroup, cfg);
21404
21514
  changed = true;
21405
- rewriteModes.add(rewriteModeForDependentUpdate(currentDependency));
21406
21515
  clearDependencyGroup(filePath);
21407
21516
  if (movePath && movePath !== filePath) {
21408
21517
  clearDependencyGroup(movePath);
@@ -21449,35 +21558,23 @@ async function rewritePatch(root, patchText, cfg, worktree) {
21449
21558
  if (pathsNormalized) {
21450
21559
  return {
21451
21560
  patchText: formatPatch({ hunks }),
21452
- changed: true,
21453
- rewrittenChunks: 0,
21454
- totalChunks,
21455
- rewriteModes: [...rewriteModes].sort()
21561
+ changed: true
21456
21562
  };
21457
21563
  }
21458
21564
  if (normalizedPatchText !== patchText) {
21459
21565
  return {
21460
21566
  patchText: normalizedPatchText,
21461
- changed: true,
21462
- rewrittenChunks: 0,
21463
- totalChunks,
21464
- rewriteModes: ["normalize:patch-text"]
21567
+ changed: true
21465
21568
  };
21466
21569
  }
21467
21570
  return {
21468
21571
  patchText,
21469
- changed: false,
21470
- rewrittenChunks: 0,
21471
- totalChunks,
21472
- rewriteModes: []
21572
+ changed: false
21473
21573
  };
21474
21574
  }
21475
21575
  return {
21476
21576
  patchText: formatPatch({ hunks: rewritten }),
21477
- changed: true,
21478
- rewrittenChunks,
21479
- totalChunks,
21480
- rewriteModes: [...rewriteModes].sort()
21577
+ changed: true
21481
21578
  };
21482
21579
  } catch (error) {
21483
21580
  throw ensureApplyPatchError(error, "Unexpected rewrite failure");
@@ -21497,30 +21594,36 @@ function createApplyPatchHook(ctx) {
21497
21594
  if (input.tool !== "apply_patch") {
21498
21595
  return;
21499
21596
  }
21500
- if (typeof output.args?.patchText !== "string") {
21597
+ const args = output.args;
21598
+ if (!args || typeof args.patchText !== "string") {
21501
21599
  return;
21502
21600
  }
21601
+ const patchText = args.patchText;
21503
21602
  const root = input.directory || ctx.directory || process.cwd();
21504
21603
  const worktree = ctx.worktree || root;
21505
21604
  try {
21506
- 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);
21507
21606
  if (result.changed) {
21508
- output.args.patchText = result.patchText;
21509
- logHookStatus("rewrite", {
21510
- rewrittenChunks: result.rewrittenChunks,
21511
- totalChunks: result.totalChunks,
21512
- strategies: result.rewriteModes
21513
- });
21607
+ args.patchText = result.patchText;
21608
+ logHookStatus("rewrite");
21514
21609
  return;
21515
21610
  }
21516
- logHookStatus("unchanged", {
21517
- rewrittenChunks: 0,
21518
- totalChunks: result.totalChunks
21519
- });
21611
+ logHookStatus("unchanged");
21520
21612
  return;
21521
21613
  } catch (error) {
21522
21614
  const normalizedError = isApplyPatchError(error) ? error : createApplyPatchInternalError(`Unexpected hook failure before native apply: ${error instanceof Error ? error.message : String(error)}`, error);
21523
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
+ }
21524
21627
  logHookStatus(isApplyPatchVerificationError(normalizedError) ? "verification" : normalizedError.kind === "validation" ? "validation" : normalizedError.kind === "internal" ? "internal" : "blocked", {
21525
21628
  kind: details?.kind ?? "internal",
21526
21629
  code: details?.code ?? "internal_unexpected",
@@ -22036,11 +22139,8 @@ function resolveRuntimeAgentName(config, agentName) {
22036
22139
  function escapeRegExp2(value) {
22037
22140
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22038
22141
  }
22039
- function rewriteDisplayNameMentions(config, text) {
22040
- if (!text.includes("@")) {
22041
- return text;
22042
- }
22043
- let rewritten = text;
22142
+ function createDisplayNameMentionRewriter(config) {
22143
+ const replacements = [];
22044
22144
  for (const internalName of getRuntimeAgentNames(config)) {
22045
22145
  const displayName = getAgentOverride(config, internalName)?.displayName;
22046
22146
  if (!displayName) {
@@ -22050,9 +22150,24 @@ function rewriteDisplayNameMentions(config, text) {
22050
22150
  if (!normalizedDisplayName || normalizedDisplayName === internalName) {
22051
22151
  continue;
22052
22152
  }
22053
- rewritten = rewritten.replace(new RegExp(`(^|[^\\w.])@${escapeRegExp2(normalizedDisplayName)}\\b`, "g"), `$1@${internalName}`);
22153
+ replacements.push({
22154
+ regex: new RegExp(`(^|[^\\w.])@${escapeRegExp2(normalizedDisplayName)}\\b`, "g"),
22155
+ internalName
22156
+ });
22054
22157
  }
22055
- return rewritten;
22158
+ if (replacements.length === 0) {
22159
+ return (text) => text;
22160
+ }
22161
+ return (text) => {
22162
+ if (!text.includes("@")) {
22163
+ return text;
22164
+ }
22165
+ let rewritten = text;
22166
+ for (const replacement of replacements) {
22167
+ rewritten = rewritten.replace(replacement.regex, `$1@${replacement.internalName}`);
22168
+ }
22169
+ return rewritten;
22170
+ };
22056
22171
  }
22057
22172
  // src/utils/internal-initiator.ts
22058
22173
  var SLIM_INTERNAL_INITIATOR_MARKER = "<!-- SLIM_INTERNAL_INITIATOR -->";
@@ -22076,6 +22191,8 @@ function hasInternalInitiatorMarker(part) {
22076
22191
  return part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER);
22077
22192
  }
22078
22193
  // src/utils/session-manager.ts
22194
+ var MIN_CONTEXT_FILE_LINES = 10;
22195
+ var MAX_CONTEXT_FILES_PER_SESSION = 8;
22079
22196
  function aliasPrefix(agentType) {
22080
22197
  switch (agentType) {
22081
22198
  case "explorer":
@@ -22115,11 +22232,15 @@ function deriveTaskSessionLabel(input) {
22115
22232
 
22116
22233
  class SessionManager {
22117
22234
  maxSessionsPerAgent;
22235
+ readContextMinLines;
22236
+ readContextMaxFiles;
22118
22237
  sessionsByParent = new Map;
22119
22238
  nextAliasIndexByParent = new Map;
22120
22239
  orderCounter = 0;
22121
- constructor(maxSessionsPerAgent) {
22240
+ constructor(maxSessionsPerAgent, options = {}) {
22122
22241
  this.maxSessionsPerAgent = maxSessionsPerAgent;
22242
+ this.readContextMinLines = options.readContextMinLines ?? MIN_CONTEXT_FILE_LINES;
22243
+ this.readContextMaxFiles = options.readContextMaxFiles ?? MAX_CONTEXT_FILES_PER_SESSION;
22123
22244
  }
22124
22245
  remember(input) {
22125
22246
  const now = this.nextOrder();
@@ -22138,6 +22259,7 @@ class SessionManager {
22138
22259
  taskId: input.taskId,
22139
22260
  agentType: input.agentType,
22140
22261
  label: input.label,
22262
+ contextFiles: [],
22141
22263
  createdAt: now,
22142
22264
  lastUsedAt: now
22143
22265
  };
@@ -22171,6 +22293,39 @@ class SessionManager {
22171
22293
  }
22172
22294
  }
22173
22295
  }
22296
+ taskIds() {
22297
+ const ids = new Set;
22298
+ for (const groups of this.sessionsByParent.values()) {
22299
+ for (const group of groups.values()) {
22300
+ for (const entry of group) {
22301
+ ids.add(entry.taskId);
22302
+ }
22303
+ }
22304
+ }
22305
+ return ids;
22306
+ }
22307
+ addContext(taskId, files) {
22308
+ if (files.length === 0)
22309
+ return;
22310
+ for (const groups of this.sessionsByParent.values()) {
22311
+ for (const group of groups.values()) {
22312
+ const match = group.find((entry) => entry.taskId === taskId);
22313
+ if (!match)
22314
+ continue;
22315
+ const existing = new Map(match.contextFiles.map((file) => [file.path, file]));
22316
+ for (const file of files) {
22317
+ const previous = existing.get(file.path);
22318
+ if (previous) {
22319
+ previous.lineCount = Math.max(previous.lineCount, file.lineCount);
22320
+ previous.lastReadAt = Math.max(previous.lastReadAt, file.lastReadAt);
22321
+ continue;
22322
+ }
22323
+ match.contextFiles.push({ ...file });
22324
+ }
22325
+ this.trimContextFiles(match);
22326
+ }
22327
+ }
22328
+ }
22174
22329
  clearParent(parentSessionId) {
22175
22330
  this.sessionsByParent.delete(parentSessionId);
22176
22331
  this.nextAliasIndexByParent.delete(parentSessionId);
@@ -22182,7 +22337,17 @@ class SessionManager {
22182
22337
  const lines = [...groups.entries()].map(([agentType, entries]) => [
22183
22338
  agentType,
22184
22339
  [...entries].sort((a, b) => b.lastUsedAt - a.lastUsedAt)
22185
- ]).filter(([, entries]) => entries.length > 0).sort((a, b) => b[1][0].lastUsedAt - a[1][0].lastUsedAt).map(([agentType, entries]) => `- ${agentType}: ${entries.map((entry) => `${entry.alias} ${entry.label}`).join("; ")}`);
22340
+ ]).filter(([, entries]) => entries.length > 0).sort((a, b) => b[1][0].lastUsedAt - a[1][0].lastUsedAt).map(([agentType, entries]) => [
22341
+ `- ${agentType}: ${entries.map((entry) => `${entry.alias} ${entry.label}`).join("; ")}`,
22342
+ ...entries.map((entry) => [
22343
+ entry,
22344
+ formatContextFiles(entry.contextFiles, {
22345
+ minLines: this.readContextMinLines,
22346
+ maxFiles: this.readContextMaxFiles
22347
+ })
22348
+ ]).filter(([, context]) => context.length > 0).map(([entry, context]) => ` Context read by ${entry.alias}: ${context}`)
22349
+ ].join(`
22350
+ `));
22186
22351
  if (lines.length === 0)
22187
22352
  return;
22188
22353
  return [
@@ -22236,11 +22401,25 @@ class SessionManager {
22236
22401
  group.length = this.maxSessionsPerAgent;
22237
22402
  }
22238
22403
  }
22404
+ trimContextFiles(entry) {
22405
+ if (this.readContextMaxFiles === 0) {
22406
+ entry.contextFiles = [];
22407
+ return;
22408
+ }
22409
+ entry.contextFiles = entry.contextFiles.filter((file) => file.lineCount >= this.readContextMinLines).sort((a, b) => b.lastReadAt - a.lastReadAt).slice(0, this.readContextMaxFiles + 1);
22410
+ }
22239
22411
  nextOrder() {
22240
22412
  this.orderCounter += 1;
22241
22413
  return this.orderCounter;
22242
22414
  }
22243
22415
  }
22416
+ function formatContextFiles(files, options) {
22417
+ const eligible = files.filter((file) => file.lineCount >= options.minLines).sort((a, b) => b.lastReadAt - a.lastReadAt);
22418
+ const shown = eligible.slice(0, options.maxFiles);
22419
+ const rest = eligible.length - shown.length;
22420
+ const rendered = shown.map((file) => `${file.path} (${file.lineCount} lines)`);
22421
+ return `${rendered.join(", ")}${rest > 0 ? ` (+${rest} more)` : ""}`;
22422
+ }
22244
22423
  // src/utils/task.ts
22245
22424
  function parseTaskIdFromTaskOutput(output) {
22246
22425
  const lines = output.split(/\r?\n/);
@@ -22542,6 +22721,17 @@ ${allowedEntries.map((entry) => entry.block).join(`
22542
22721
  });
22543
22722
  }
22544
22723
  function createFilterAvailableSkillsHook(_ctx, config) {
22724
+ const permissionRulesByAgent = new Map;
22725
+ const getPermissionRules = (agentName) => {
22726
+ const cached = permissionRulesByAgent.get(agentName);
22727
+ if (cached) {
22728
+ return cached;
22729
+ }
22730
+ const configuredSkills = getAgentOverride(config, agentName)?.skills;
22731
+ const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills);
22732
+ permissionRulesByAgent.set(agentName, permissionRules);
22733
+ return permissionRules;
22734
+ };
22545
22735
  return {
22546
22736
  "experimental.chat.messages.transform": async (_input, output) => {
22547
22737
  const { messages } = output;
@@ -22549,8 +22739,7 @@ function createFilterAvailableSkillsHook(_ctx, config) {
22549
22739
  return;
22550
22740
  }
22551
22741
  const agentName = getCurrentAgent(messages);
22552
- const configuredSkills = getAgentOverride(config, agentName)?.skills;
22553
- const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills);
22742
+ const permissionRules = getPermissionRules(agentName);
22554
22743
  for (const message of messages) {
22555
22744
  for (const part of message.parts) {
22556
22745
  if (part.type !== "text" || !part.text || !part.text.includes("<available_skills>")) {
@@ -22903,7 +23092,21 @@ function processImageAttachments(args) {
22903
23092
  const observerEnabled = !disabledAgents.has("observer");
22904
23093
  if (!observerEnabled)
22905
23094
  return;
23095
+ const messagesWithImages = [];
23096
+ for (const msg of messages) {
23097
+ if (msg.info.role !== "user")
23098
+ continue;
23099
+ const imageParts = msg.parts.filter(isImagePart);
23100
+ if (imageParts.length > 0) {
23101
+ messagesWithImages.push({ msg, imageParts });
23102
+ }
23103
+ }
22906
23104
  const saveDir = join7(workDir, ".opencode", "images");
23105
+ if (messagesWithImages.length === 0) {
23106
+ if (existsSync4(saveDir))
23107
+ cleanupAllSessions(saveDir);
23108
+ return;
23109
+ }
22907
23110
  const gitignorePath = join7(workDir, ".opencode", ".gitignore");
22908
23111
  try {
22909
23112
  mkdirSync2(saveDir, { recursive: true });
@@ -22914,12 +23117,7 @@ function processImageAttachments(args) {
22914
23117
  log2(`[image-hook] failed to create image directory: ${e}`);
22915
23118
  }
22916
23119
  cleanupAllSessions(saveDir);
22917
- for (const msg of messages) {
22918
- if (msg.info.role !== "user")
22919
- continue;
22920
- const imageParts = msg.parts.filter(isImagePart);
22921
- if (imageParts.length === 0)
22922
- continue;
23120
+ for (const { msg, imageParts } of messagesWithImages) {
22923
23121
  const sessionSubdir = msg.info.sessionID ? sanitizeFilename(msg.info.sessionID) : undefined;
22924
23122
  const targetDir = sessionSubdir ? join7(saveDir, sessionSubdir) : saveDir;
22925
23123
  try {
@@ -23007,7 +23205,7 @@ ${JSON_ERROR_REMINDER}`;
23007
23205
  };
23008
23206
  }
23009
23207
  // src/hooks/phase-reminder/index.ts
23010
- var PHASE_REMINDER = `<reminder>${PHASE_REMINDER_TEXT}</reminder>`;
23208
+ var PHASE_REMINDER = `<internal_reminder>${PHASE_REMINDER_TEXT}</internal_reminder>`;
23011
23209
  function createPhaseReminderHook() {
23012
23210
  return {
23013
23211
  "experimental.chat.messages.transform": async (_input, output) => {
@@ -23038,11 +23236,14 @@ function createPhaseReminderHook() {
23038
23236
  if (originalText.includes(SLIM_INTERNAL_INITIATOR_MARKER)) {
23039
23237
  return;
23040
23238
  }
23041
- lastUserMessage.parts[textPartIndex].text = `${PHASE_REMINDER}
23239
+ if (originalText.includes(PHASE_REMINDER)) {
23240
+ return;
23241
+ }
23242
+ lastUserMessage.parts[textPartIndex].text = `${originalText}
23042
23243
 
23043
23244
  ---
23044
23245
 
23045
- ${originalText}`;
23246
+ ${PHASE_REMINDER}`;
23046
23247
  }
23047
23248
  };
23048
23249
  }
@@ -23050,36 +23251,36 @@ ${originalText}`;
23050
23251
  var POST_FILE_TOOL_NUDGE = PHASE_REMINDER_TEXT;
23051
23252
  var FILE_TOOLS = new Set(["Read", "read", "Write", "write"]);
23052
23253
  function createPostFileToolNudgeHook(options = {}) {
23053
- 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
+ }
23054
23270
  return {
23055
- "tool.execute.after": async (input, _output) => {
23271
+ "tool.execute.after": async (input, output) => {
23056
23272
  if (!FILE_TOOLS.has(input.tool) || !input.sessionID) {
23057
23273
  return;
23058
23274
  }
23059
- pendingSessionIds.add(input.sessionID);
23060
- },
23061
- "experimental.chat.system.transform": async (input, output) => {
23062
- if (!input.sessionID || !pendingSessionIds.delete(input.sessionID)) {
23063
- return;
23064
- }
23065
23275
  if (options.shouldInject && !options.shouldInject(input.sessionID)) {
23066
23276
  return;
23067
23277
  }
23068
- output.system.push(POST_FILE_TOOL_NUDGE);
23069
- },
23070
- event: async (input) => {
23071
- if (input.event.type !== "session.deleted") {
23072
- return;
23073
- }
23074
- const sessionID = input.event.properties?.sessionID ?? input.event.properties?.info?.id;
23075
- if (!sessionID) {
23076
- return;
23077
- }
23078
- pendingSessionIds.delete(sessionID);
23278
+ appendReminder(output);
23079
23279
  }
23080
23280
  };
23081
23281
  }
23082
23282
  // src/hooks/task-session-manager/index.ts
23283
+ import path8 from "node:path";
23083
23284
  var AGENT_NAME_SET = new Set([
23084
23285
  "orchestrator",
23085
23286
  "oracle",
@@ -23092,16 +23293,97 @@ var AGENT_NAME_SET = new Set([
23092
23293
  "councillor"
23093
23294
  ]);
23094
23295
  var MAX_PENDING_TASK_CALLS = 100;
23296
+ var RESUMABLE_SESSIONS_START = "<resumable_sessions>";
23297
+ var RESUMABLE_SESSIONS_END = "</resumable_sessions>";
23095
23298
  function isAgentName(value) {
23096
23299
  return typeof value === "string" && AGENT_NAME_SET.has(value);
23097
23300
  }
23098
23301
  function isObjectRecord(value) {
23099
23302
  return typeof value === "object" && value !== null;
23100
23303
  }
23304
+ function extractPath(output) {
23305
+ return /<path>([^<]+)<\/path>/.exec(output)?.[1];
23306
+ }
23307
+ function normalizePath(root, file) {
23308
+ const relative = path8.relative(root, file);
23309
+ if (!relative || relative.startsWith("..") || path8.isAbsolute(relative)) {
23310
+ return file;
23311
+ }
23312
+ return relative;
23313
+ }
23314
+ function extractReadFiles(root, output) {
23315
+ if (typeof output.output !== "string")
23316
+ return [];
23317
+ const file = extractPath(output.output);
23318
+ if (!file)
23319
+ return [];
23320
+ return [
23321
+ {
23322
+ path: normalizePath(root, file),
23323
+ lineCount: countReadLines(output.output).length,
23324
+ lineNumbers: countReadLines(output.output),
23325
+ lastReadAt: Date.now()
23326
+ }
23327
+ ];
23328
+ }
23329
+ function countReadLines(output) {
23330
+ const lines = new Set;
23331
+ for (const match of output.matchAll(/^([0-9]+):/gm)) {
23332
+ lines.add(Number(match[1]));
23333
+ }
23334
+ return [...lines];
23335
+ }
23101
23336
  function createTaskSessionManagerHook(_ctx, options) {
23102
- const sessionManager = new SessionManager(options.maxSessionsPerAgent);
23337
+ const sessionManager = new SessionManager(options.maxSessionsPerAgent, {
23338
+ readContextMinLines: options.readContextMinLines,
23339
+ readContextMaxFiles: options.readContextMaxFiles
23340
+ });
23103
23341
  const pendingCalls = new Map;
23104
23342
  const pendingCallOrder = [];
23343
+ const contextByTask = new Map;
23344
+ const pendingManagedTaskIds = new Set;
23345
+ function addTaskContext(taskId, files) {
23346
+ if (files.length === 0)
23347
+ return;
23348
+ let context = contextByTask.get(taskId);
23349
+ if (!context) {
23350
+ context = new Map;
23351
+ contextByTask.set(taskId, context);
23352
+ }
23353
+ for (const file of files) {
23354
+ const pending = context.get(file.path) ?? {
23355
+ path: file.path,
23356
+ lines: new Set,
23357
+ lastReadAt: file.lastReadAt
23358
+ };
23359
+ for (const line of file.lineNumbers ?? []) {
23360
+ pending.lines.add(line);
23361
+ }
23362
+ pending.lastReadAt = Math.max(pending.lastReadAt, file.lastReadAt);
23363
+ context.set(file.path, pending);
23364
+ }
23365
+ sessionManager.addContext(taskId, contextFilesForPrompt(context));
23366
+ }
23367
+ function contextFilesForPrompt(context) {
23368
+ if (!context)
23369
+ return [];
23370
+ return [...context.values()].map((file) => ({
23371
+ path: file.path,
23372
+ lineCount: file.lines.size,
23373
+ lastReadAt: file.lastReadAt
23374
+ }));
23375
+ }
23376
+ function canTrackTaskContext(taskId) {
23377
+ return pendingManagedTaskIds.has(taskId) || sessionManager.taskIds().has(taskId);
23378
+ }
23379
+ function pruneContext() {
23380
+ const remembered = sessionManager.taskIds();
23381
+ for (const taskId of contextByTask.keys()) {
23382
+ if (!pendingManagedTaskIds.has(taskId) && !remembered.has(taskId)) {
23383
+ contextByTask.delete(taskId);
23384
+ }
23385
+ }
23386
+ }
23105
23387
  function isMissingRememberedSessionError(output) {
23106
23388
  const firstLine = output.split(/\r?\n/, 1)[0]?.trim().toLowerCase() ?? "";
23107
23389
  return firstLine.startsWith("[error]") && firstLine.includes("session") && (firstLine.includes("not found") || firstLine.includes("no session"));
@@ -23167,6 +23449,7 @@ function createTaskSessionManagerHook(_ctx, options) {
23167
23449
  return;
23168
23450
  }
23169
23451
  args.task_id = remembered.taskId;
23452
+ pendingManagedTaskIds.add(remembered.taskId);
23170
23453
  sessionManager.markUsed(input.sessionID, args.subagent_type, remembered.taskId);
23171
23454
  if (input.callID) {
23172
23455
  rememberPendingCall({
@@ -23179,6 +23462,12 @@ function createTaskSessionManagerHook(_ctx, options) {
23179
23462
  }
23180
23463
  },
23181
23464
  "tool.execute.after": async (input, output) => {
23465
+ if (input.tool.toLowerCase() === "read") {
23466
+ if (input.sessionID && canTrackTaskContext(input.sessionID)) {
23467
+ addTaskContext(input.sessionID, extractReadFiles(_ctx.directory, output));
23468
+ }
23469
+ return;
23470
+ }
23182
23471
  if (input.tool.toLowerCase() !== "task")
23183
23472
  return;
23184
23473
  const pending = takePendingCall(input.callID);
@@ -23200,17 +23489,50 @@ function createTaskSessionManagerHook(_ctx, options) {
23200
23489
  agentType: pending.agentType,
23201
23490
  label: pending.label
23202
23491
  });
23492
+ pendingManagedTaskIds.delete(taskId);
23493
+ const contextFiles = contextFilesForPrompt(contextByTask.get(taskId));
23494
+ sessionManager.addContext(taskId, contextFiles);
23495
+ pruneContext();
23203
23496
  },
23204
- "experimental.chat.system.transform": async (input, output) => {
23205
- 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
+ `);
23206
23525
  return;
23207
23526
  }
23208
- const reminder = sessionManager.formatForPrompt(input.sessionID);
23209
- if (!reminder)
23210
- return;
23211
- output.system.push(reminder);
23212
23527
  },
23213
23528
  event: async (input) => {
23529
+ if (input.event.type === "session.created") {
23530
+ const info = input.event.properties?.info;
23531
+ if (info?.id && info.parentID && options.shouldManageSession(info.parentID)) {
23532
+ pendingManagedTaskIds.add(info.id);
23533
+ }
23534
+ return;
23535
+ }
23214
23536
  if (input.event.type !== "session.deleted")
23215
23537
  return;
23216
23538
  const sessionId = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
@@ -23218,6 +23540,9 @@ function createTaskSessionManagerHook(_ctx, options) {
23218
23540
  return;
23219
23541
  sessionManager.clearParent(sessionId);
23220
23542
  sessionManager.dropTask(sessionId);
23543
+ contextByTask.delete(sessionId);
23544
+ pendingManagedTaskIds.delete(sessionId);
23545
+ pruneContext();
23221
23546
  for (const [callId, pending] of pendingCalls.entries()) {
23222
23547
  if (pending.parentSessionId !== sessionId) {
23223
23548
  continue;
@@ -23263,7 +23588,7 @@ function createTodoHygiene(options) {
23263
23588
  handleRequestStart(input) {
23264
23589
  clear(input.sessionID);
23265
23590
  },
23266
- async handleToolExecuteAfter(input) {
23591
+ async handleToolExecuteAfter(input, _output) {
23267
23592
  if (!input.sessionID) {
23268
23593
  return;
23269
23594
  }
@@ -23273,6 +23598,10 @@ function createTodoHygiene(options) {
23273
23598
  }
23274
23599
  try {
23275
23600
  if (RESET.has(tool)) {
23601
+ if (options.shouldInject && !options.shouldInject(input.sessionID)) {
23602
+ clear(input.sessionID);
23603
+ return;
23604
+ }
23276
23605
  active.add(input.sessionID);
23277
23606
  clearCycle(input.sessionID);
23278
23607
  const state2 = await options.getTodoState(input.sessionID);
@@ -23331,39 +23660,22 @@ function createTodoHygiene(options) {
23331
23660
  });
23332
23661
  }
23333
23662
  },
23334
- async handleChatSystemTransform(input, output) {
23335
- if (!input.sessionID) {
23336
- return;
23337
- }
23338
- const reasons = pending.get(input.sessionID);
23663
+ getPendingReminder(sessionID) {
23664
+ const reasons = pending.get(sessionID);
23339
23665
  if (!reasons || reasons.size === 0) {
23340
- return;
23341
- }
23342
- const reminder = pick(reasons);
23343
- if (options.shouldInject && !options.shouldInject(input.sessionID)) {
23344
- clear(input.sessionID);
23345
- return;
23666
+ return null;
23346
23667
  }
23347
- try {
23348
- const state = await options.getTodoState(input.sessionID);
23349
- if (!state.hasOpenTodos) {
23350
- clear(input.sessionID);
23351
- return;
23352
- }
23353
- pending.delete(input.sessionID);
23354
- output.system.push(reminder);
23355
- options.log?.("Injected todo hygiene reminder", {
23356
- sessionID: input.sessionID,
23357
- reminder,
23358
- reasons: Array.from(reasons)
23359
- });
23360
- } catch (error) {
23361
- pending.delete(input.sessionID);
23362
- options.log?.("Skipped todo hygiene reminder: failed to inspect todos", {
23363
- sessionID: input.sessionID,
23364
- error: error instanceof Error ? error.message : String(error)
23365
- });
23668
+ if (options.shouldInject && !options.shouldInject(sessionID)) {
23669
+ clear(sessionID);
23670
+ return null;
23366
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;
23367
23679
  },
23368
23680
  handleEvent(event) {
23369
23681
  if (event.type !== "session.deleted") {
@@ -23382,6 +23694,8 @@ function createTodoHygiene(options) {
23382
23694
  var HOOK_NAME = "todo-continuation";
23383
23695
  var COMMAND_NAME = "auto-continue";
23384
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>";
23385
23699
  var SUPPRESS_AFTER_ABORT_MS = 5000;
23386
23700
  var NOTIFICATION_BUSY_GRACE_MS = 250;
23387
23701
  var QUESTION_PHRASES = [
@@ -23419,6 +23733,35 @@ function resetState(state) {
23419
23733
  state.notifyingSessionIds.clear();
23420
23734
  state.notificationBusyUntilBySession.clear();
23421
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
+ }
23422
23765
  function createTodoContinuationHook(ctx, config) {
23423
23766
  const maxContinuations = config?.maxContinuations ?? 5;
23424
23767
  const cooldownMs = config?.cooldownMs ?? 3000;
@@ -23494,7 +23837,8 @@ function createTodoContinuationHook(ctx, config) {
23494
23837
  const sessionID = inferSessionID(messages, i);
23495
23838
  const partSignature = message.parts.map((part) => {
23496
23839
  if (part.type === "text" && typeof part.text === "string") {
23497
- 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()}`;
23498
23842
  }
23499
23843
  return part.type ?? "unknown";
23500
23844
  }).join("|");
@@ -23502,6 +23846,7 @@ function createTodoContinuationHook(ctx, config) {
23502
23846
  return {
23503
23847
  sessionID,
23504
23848
  agent: message.info.agent,
23849
+ message,
23505
23850
  signature: message.info.id ? `${message.info.id}:${partSignature}` : `${ordinal}:${partSignature}`
23506
23851
  };
23507
23852
  }
@@ -23529,9 +23874,16 @@ function createTodoContinuationHook(ctx, config) {
23529
23874
  return;
23530
23875
  }
23531
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
+ }
23532
23883
  return;
23533
23884
  }
23534
23885
  requestSignatureBySession.set(lastUserMessage.sessionID, lastUserMessage.signature);
23886
+ stripTodoHygieneInstructionFromMessage(lastUserMessage.message);
23535
23887
  hygiene.handleRequestStart({ sessionID: lastUserMessage.sessionID });
23536
23888
  }
23537
23889
  function markNotificationStarted(sessionID) {
@@ -23881,7 +24233,6 @@ function createTodoContinuationHook(ctx, config) {
23881
24233
  return {
23882
24234
  tool: { auto_continue: autoContinue },
23883
24235
  handleToolExecuteAfter: hygiene.handleToolExecuteAfter,
23884
- handleChatSystemTransform: hygiene.handleChatSystemTransform,
23885
24236
  handleMessagesTransform,
23886
24237
  handleEvent,
23887
24238
  handleChatMessage,
@@ -23889,7 +24240,7 @@ function createTodoContinuationHook(ctx, config) {
23889
24240
  };
23890
24241
  }
23891
24242
  // src/interview/manager.ts
23892
- import path11 from "node:path";
24243
+ import path12 from "node:path";
23893
24244
 
23894
24245
  // src/interview/dashboard.ts
23895
24246
  import crypto from "node:crypto";
@@ -23899,27 +24250,27 @@ import {
23899
24250
  createServer
23900
24251
  } from "node:http";
23901
24252
  import os3 from "node:os";
23902
- import path9 from "node:path";
24253
+ import path10 from "node:path";
23903
24254
  import { URL as URL2 } from "node:url";
23904
24255
 
23905
24256
  // src/interview/document.ts
23906
24257
  import * as fsSync from "node:fs";
23907
24258
  import * as fs6 from "node:fs/promises";
23908
- import * as path8 from "node:path";
24259
+ import * as path9 from "node:path";
23909
24260
  var DEFAULT_OUTPUT_FOLDER = "interview";
23910
24261
  function normalizeOutputFolder(outputFolder) {
23911
24262
  const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
23912
24263
  return normalized || DEFAULT_OUTPUT_FOLDER;
23913
24264
  }
23914
24265
  function createInterviewDirectoryPath(directory, outputFolder) {
23915
- return path8.join(directory, normalizeOutputFolder(outputFolder));
24266
+ return path9.join(directory, normalizeOutputFolder(outputFolder));
23916
24267
  }
23917
24268
  function createInterviewFilePath(directory, outputFolder, idea) {
23918
24269
  const fileName = `${slugify(idea) || "interview"}.md`;
23919
- return path8.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
24270
+ return path9.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
23920
24271
  }
23921
24272
  function relativeInterviewPath(directory, filePath) {
23922
- return path8.relative(directory, filePath) || path8.basename(filePath);
24273
+ return path9.relative(directory, filePath) || path9.basename(filePath);
23923
24274
  }
23924
24275
  function resolveExistingInterviewPath(directory, outputFolder, value) {
23925
24276
  const trimmed = value.trim();
@@ -23928,22 +24279,22 @@ function resolveExistingInterviewPath(directory, outputFolder, value) {
23928
24279
  }
23929
24280
  const outputDir = createInterviewDirectoryPath(directory, outputFolder);
23930
24281
  const candidates = new Set;
23931
- const resolvedRoot = path8.resolve(directory);
23932
- if (path8.isAbsolute(trimmed)) {
24282
+ const resolvedRoot = path9.resolve(directory);
24283
+ if (path9.isAbsolute(trimmed)) {
23933
24284
  candidates.add(trimmed);
23934
24285
  } else {
23935
- candidates.add(path8.resolve(directory, trimmed));
23936
- candidates.add(path8.join(outputDir, trimmed));
24286
+ candidates.add(path9.resolve(directory, trimmed));
24287
+ candidates.add(path9.join(outputDir, trimmed));
23937
24288
  if (!trimmed.endsWith(".md")) {
23938
- candidates.add(path8.join(outputDir, `${trimmed}.md`));
24289
+ candidates.add(path9.join(outputDir, `${trimmed}.md`));
23939
24290
  }
23940
24291
  }
23941
24292
  for (const candidate of candidates) {
23942
- if (path8.extname(candidate) !== ".md") {
24293
+ if (path9.extname(candidate) !== ".md") {
23943
24294
  continue;
23944
24295
  }
23945
- const resolved = path8.resolve(candidate);
23946
- if (!resolved.startsWith(resolvedRoot + path8.sep) && resolved !== resolvedRoot) {
24296
+ const resolved = path9.resolve(candidate);
24297
+ if (!resolved.startsWith(resolvedRoot + path9.sep) && resolved !== resolvedRoot) {
23947
24298
  continue;
23948
24299
  }
23949
24300
  if (fsSync.existsSync(candidate)) {
@@ -24023,7 +24374,7 @@ function parseFrontmatter(content) {
24023
24374
  return result;
24024
24375
  }
24025
24376
  async function ensureInterviewFile(record) {
24026
- await fs6.mkdir(path8.dirname(record.markdownPath), { recursive: true });
24377
+ await fs6.mkdir(path9.dirname(record.markdownPath), { recursive: true });
24027
24378
  try {
24028
24379
  await fs6.access(record.markdownPath);
24029
24380
  } catch {
@@ -25693,12 +26044,12 @@ function renderInterviewPage(interviewId, resumeSlug) {
25693
26044
 
25694
26045
  // src/interview/dashboard.ts
25695
26046
  function getAuthFilePath(port) {
25696
- const dataHome = process.env.XDG_DATA_HOME || path9.join(os3.homedir(), ".local", "share");
25697
- return path9.join(dataHome, "opencode", `.dashboard-${port}.json`);
26047
+ const dataHome = process.env.XDG_DATA_HOME || path10.join(os3.homedir(), ".local", "share");
26048
+ return path10.join(dataHome, "opencode", `.dashboard-${port}.json`);
25698
26049
  }
25699
26050
  function writeAuthFile(port, token) {
25700
26051
  const filePath = getAuthFilePath(port);
25701
- const dir = path9.dirname(filePath);
26052
+ const dir = path10.dirname(filePath);
25702
26053
  try {
25703
26054
  fsSync2.mkdirSync(dir, { recursive: true });
25704
26055
  } catch {}
@@ -25835,7 +26186,7 @@ function createDashboardServer(config) {
25835
26186
  const directories = getKnownDirectories();
25836
26187
  const items = [];
25837
26188
  for (const dir of directories) {
25838
- const interviewDir = path9.join(dir, config.outputFolder);
26189
+ const interviewDir = path10.join(dir, config.outputFolder);
25839
26190
  let entries;
25840
26191
  try {
25841
26192
  entries = await fs7.readdir(interviewDir);
@@ -25847,7 +26198,7 @@ function createDashboardServer(config) {
25847
26198
  continue;
25848
26199
  let content;
25849
26200
  try {
25850
- content = await fs7.readFile(path9.join(interviewDir, entry), "utf8");
26201
+ content = await fs7.readFile(path10.join(interviewDir, entry), "utf8");
25851
26202
  } catch {
25852
26203
  continue;
25853
26204
  }
@@ -25873,7 +26224,7 @@ function createDashboardServer(config) {
25873
26224
  const directories = getKnownDirectories();
25874
26225
  let rebuilt = 0;
25875
26226
  for (const dir of directories) {
25876
- const interviewDir = path9.join(dir, config.outputFolder);
26227
+ const interviewDir = path10.join(dir, config.outputFolder);
25877
26228
  let entries;
25878
26229
  try {
25879
26230
  entries = await fs7.readdir(interviewDir);
@@ -25885,7 +26236,7 @@ function createDashboardServer(config) {
25885
26236
  continue;
25886
26237
  let content;
25887
26238
  try {
25888
- content = await fs7.readFile(path9.join(interviewDir, entry), "utf8");
26239
+ content = await fs7.readFile(path10.join(interviewDir, entry), "utf8");
25889
26240
  } catch {
25890
26241
  continue;
25891
26242
  }
@@ -25911,7 +26262,7 @@ function createDashboardServer(config) {
25911
26262
  questions: [],
25912
26263
  pendingAnswers: null,
25913
26264
  lastUpdatedAt: fm.updatedAt ? new Date(fm.updatedAt).getTime() : Date.now(),
25914
- filePath: path9.join(interviewDir, entry),
26265
+ filePath: path10.join(interviewDir, entry),
25915
26266
  nudgeAction: null
25916
26267
  });
25917
26268
  if (!sessions.has(fm.sessionID)) {
@@ -26175,7 +26526,7 @@ function createDashboardServer(config) {
26175
26526
  const dirs = getKnownDirectories();
26176
26527
  for (const dir of dirs) {
26177
26528
  const slug = extractResumeSlug(interviewId);
26178
- const candidate = path9.join(dir, config.outputFolder, `${slug}.md`);
26529
+ const candidate = path10.join(dir, config.outputFolder, `${slug}.md`);
26179
26530
  try {
26180
26531
  document = await fs7.readFile(candidate, "utf8");
26181
26532
  markdownPath = candidate;
@@ -26703,7 +27054,7 @@ function createInterviewServer(deps) {
26703
27054
  // src/interview/service.ts
26704
27055
  import { spawn } from "node:child_process";
26705
27056
  import * as fs8 from "node:fs/promises";
26706
- import * as path10 from "node:path";
27057
+ import * as path11 from "node:path";
26707
27058
 
26708
27059
  // src/interview/types.ts
26709
27060
  import { z as z3 } from "zod";
@@ -26970,12 +27321,12 @@ function createInterviewService(ctx, config, deps) {
26970
27321
  if (!newSlug) {
26971
27322
  return;
26972
27323
  }
26973
- const currentFileName = path10.basename(interview.markdownPath, ".md");
27324
+ const currentFileName = path11.basename(interview.markdownPath, ".md");
26974
27325
  if (currentFileName === newSlug) {
26975
27326
  return;
26976
27327
  }
26977
- const dir = path10.dirname(interview.markdownPath);
26978
- const newPath = path10.join(dir, `${newSlug}.md`);
27328
+ const dir = path11.dirname(interview.markdownPath);
27329
+ const newPath = path11.join(dir, `${newSlug}.md`);
26979
27330
  try {
26980
27331
  await fs8.access(newPath);
26981
27332
  return;
@@ -27051,9 +27402,9 @@ function createInterviewService(ctx, config, deps) {
27051
27402
  const messages = await loadMessages(sessionID);
27052
27403
  const title = extractTitle(document);
27053
27404
  const record = {
27054
- id: `${Date.now()}-${++idCounter}-${slugify(path10.basename(markdownPath, ".md")) || "interview"}`,
27405
+ id: `${Date.now()}-${++idCounter}-${slugify(path11.basename(markdownPath, ".md")) || "interview"}`,
27055
27406
  sessionID,
27056
- idea: title || path10.basename(markdownPath, ".md"),
27407
+ idea: title || path11.basename(markdownPath, ".md"),
27057
27408
  markdownPath,
27058
27409
  createdAt: nowIso(),
27059
27410
  status: "active",
@@ -27280,7 +27631,7 @@ function createInterviewService(ctx, config, deps) {
27280
27631
  return fileCache.items;
27281
27632
  }
27282
27633
  const outputDir = createInterviewDirectoryPath(ctx.directory, outputFolder);
27283
- const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) => path10.resolve(i.markdownPath)));
27634
+ const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) => path11.resolve(i.markdownPath)));
27284
27635
  let entries;
27285
27636
  try {
27286
27637
  entries = await fs8.readdir(outputDir);
@@ -27291,8 +27642,8 @@ function createInterviewService(ctx, config, deps) {
27291
27642
  for (const entry of entries) {
27292
27643
  if (!entry.endsWith(".md"))
27293
27644
  continue;
27294
- const fullPath = path10.join(outputDir, entry);
27295
- if (activePaths.has(path10.resolve(fullPath)))
27645
+ const fullPath = path11.join(outputDir, entry);
27646
+ if (activePaths.has(path11.resolve(fullPath)))
27296
27647
  continue;
27297
27648
  let content;
27298
27649
  try {
@@ -27391,7 +27742,7 @@ function createInterviewManager(ctx, config) {
27391
27742
  const outputFolder = interviewConfig?.outputFolder ?? "interview";
27392
27743
  if (!dashboardEnabled) {
27393
27744
  const service2 = createInterviewService(ctx, interviewConfig);
27394
- const resolvedOutputPath = path11.join(ctx.directory, outputFolder);
27745
+ const resolvedOutputPath = path12.join(ctx.directory, outputFolder);
27395
27746
  const server = createInterviewServer({
27396
27747
  getState: async (interviewId) => service2.getInterviewState(interviewId),
27397
27748
  listInterviewFiles: async () => service2.listInterviewFiles(),
@@ -27496,7 +27847,7 @@ function createInterviewManager(ctx, config) {
27496
27847
  listInterviews: () => service.listInterviews(),
27497
27848
  submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers),
27498
27849
  handleNudgeAction: async (interviewId, action) => service.handleNudgeAction(interviewId, action),
27499
- outputFolder: path11.join(ctx.directory, outputFolder),
27850
+ outputFolder: path12.join(ctx.directory, outputFolder),
27500
27851
  port: 0
27501
27852
  });
27502
27853
  service.setBaseUrlResolver(() => perSessionServer.ensureStarted());
@@ -27936,23 +28287,23 @@ class TmuxMultiplexer {
27936
28287
  return null;
27937
28288
  }
27938
28289
  const stdout = await proc.stdout();
27939
- const path12 = stdout.trim().split(`
28290
+ const path13 = stdout.trim().split(`
27940
28291
  `)[0];
27941
- if (!path12) {
28292
+ if (!path13) {
27942
28293
  log("[tmux] findBinary: no path in output");
27943
28294
  return null;
27944
28295
  }
27945
- const verifyProc = crossSpawn([path12, "-V"], {
28296
+ const verifyProc = crossSpawn([path13, "-V"], {
27946
28297
  stdout: "pipe",
27947
28298
  stderr: "pipe"
27948
28299
  });
27949
28300
  const verifyExit = await verifyProc.exited;
27950
28301
  if (verifyExit !== 0) {
27951
- log("[tmux] findBinary: tmux -V failed", { path: path12, verifyExit });
28302
+ log("[tmux] findBinary: tmux -V failed", { path: path13, verifyExit });
27952
28303
  return null;
27953
28304
  }
27954
- log("[tmux] findBinary: found", { path: path12 });
27955
- return path12;
28305
+ log("[tmux] findBinary: found", { path: path13 });
28306
+ return path13;
27956
28307
  } catch (err) {
27957
28308
  log("[tmux] findBinary: exception", { error: String(err) });
27958
28309
  return null;
@@ -28333,6 +28684,7 @@ class MultiplexerSessionManager {
28333
28684
  sessions = new Map;
28334
28685
  knownSessions = new Map;
28335
28686
  spawningSessions = new Set;
28687
+ closingSessions = new Map;
28336
28688
  pollInterval;
28337
28689
  enabled = false;
28338
28690
  constructor(ctx, config) {
@@ -28361,17 +28713,22 @@ class MultiplexerSessionManager {
28361
28713
  const parentId = info.parentID;
28362
28714
  const title = info.title ?? "Subagent";
28363
28715
  const directory = info.directory ?? this.directory;
28364
- this.knownSessions.set(sessionId, {
28365
- parentId,
28366
- title,
28367
- directory
28368
- });
28369
28716
  if (this.isTrackedOrSpawning(sessionId)) {
28370
28717
  log("[multiplexer-session-manager] session already tracked or spawning", {
28371
28718
  sessionId
28372
28719
  });
28373
28720
  return;
28374
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
+ });
28375
28732
  this.spawningSessions.add(sessionId);
28376
28733
  try {
28377
28734
  const serverRunning = await isServerRunning(this.serverUrl);
@@ -28381,7 +28738,7 @@ class MultiplexerSessionManager {
28381
28738
  });
28382
28739
  return;
28383
28740
  }
28384
- if (this.sessions.has(sessionId)) {
28741
+ if (this.closingSessions.has(sessionId) || this.sessions.has(sessionId)) {
28385
28742
  return;
28386
28743
  }
28387
28744
  log("[multiplexer-session-manager] child session created, spawning pane", {
@@ -28395,23 +28752,31 @@ class MultiplexerSessionManager {
28395
28752
  });
28396
28753
  return { success: false, paneId: undefined };
28397
28754
  });
28398
- if (paneResult.success && paneResult.paneId) {
28399
- const now = Date.now();
28400
- 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", {
28401
28759
  sessionId,
28402
28760
  paneId: paneResult.paneId,
28403
- parentId,
28404
- title,
28405
- directory,
28406
- createdAt: now,
28407
- lastSeenAt: now
28408
- });
28409
- log("[multiplexer-session-manager] pane spawned", {
28410
- sessionId,
28411
- paneId: paneResult.paneId
28412
- });
28413
- this.startPolling();
28761
+ error: String(err)
28762
+ }));
28763
+ return;
28414
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();
28415
28780
  } finally {
28416
28781
  this.spawningSessions.delete(sessionId);
28417
28782
  }
@@ -28425,7 +28790,7 @@ class MultiplexerSessionManager {
28425
28790
  if (!sessionId)
28426
28791
  return;
28427
28792
  if (event.properties?.status?.type === "idle") {
28428
- await this.closeSession(sessionId);
28793
+ await this.closeSession(sessionId, "idle");
28429
28794
  return;
28430
28795
  }
28431
28796
  if (event.properties?.status?.type === "busy") {
@@ -28437,14 +28802,13 @@ class MultiplexerSessionManager {
28437
28802
  return;
28438
28803
  if (event.type !== "session.deleted")
28439
28804
  return;
28440
- const sessionId = event.properties?.sessionID;
28805
+ const sessionId = this.getSessionId(event);
28441
28806
  if (!sessionId)
28442
28807
  return;
28443
28808
  log("[multiplexer-session-manager] session deleted, closing pane", {
28444
28809
  sessionId
28445
28810
  });
28446
- await this.closeSession(sessionId);
28447
- this.knownSessions.delete(sessionId);
28811
+ await this.closeSession(sessionId, "deleted");
28448
28812
  }
28449
28813
  startPolling() {
28450
28814
  if (this.pollInterval)
@@ -28481,35 +28845,58 @@ class MultiplexerSessionManager {
28481
28845
  const missingTooLong = !!tracked.missingSince && now - tracked.missingSince >= SESSION_MISSING_GRACE_MS;
28482
28846
  const isTimedOut = now - tracked.createdAt > SESSION_TIMEOUT_MS;
28483
28847
  if (isIdle || missingTooLong || isTimedOut) {
28484
- sessionsToClose.push(sessionId);
28848
+ sessionsToClose.push({
28849
+ sessionId,
28850
+ reason: isIdle ? "idle" : isTimedOut ? "timeout" : "missing"
28851
+ });
28485
28852
  }
28486
28853
  }
28487
- for (const sessionId of sessionsToClose) {
28488
- await this.closeSession(sessionId);
28854
+ for (const { sessionId, reason } of sessionsToClose) {
28855
+ await this.closeSession(sessionId, reason);
28489
28856
  }
28490
28857
  } catch (err) {
28491
28858
  log("[multiplexer-session-manager] poll error", { error: String(err) });
28492
28859
  }
28493
28860
  }
28494
- 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;
28495
28868
  const tracked = this.sessions.get(sessionId);
28496
28869
  if (!tracked || !this.multiplexer)
28497
28870
  return;
28871
+ this.sessions.delete(sessionId);
28498
28872
  log("[multiplexer-session-manager] closing session pane", {
28499
28873
  sessionId,
28500
- paneId: tracked.paneId
28874
+ paneId: tracked.paneId,
28875
+ reason
28501
28876
  });
28502
- await this.multiplexer.closePane(tracked.paneId);
28503
- this.sessions.delete(sessionId);
28504
- if (this.sessions.size === 0) {
28505
- this.stopPolling();
28506
- }
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;
28507
28890
  }
28508
28891
  async respawnIfKnown(sessionId) {
28509
28892
  if (!this.enabled || !this.multiplexer)
28510
28893
  return;
28511
- if (this.isTrackedOrSpawning(sessionId))
28894
+ const closing = this.closingSessions.get(sessionId);
28895
+ if (closing)
28896
+ await closing;
28897
+ if (this.isTrackedOrSpawning(sessionId)) {
28512
28898
  return;
28899
+ }
28513
28900
  const known = this.knownSessions.get(sessionId);
28514
28901
  if (!known)
28515
28902
  return;
@@ -28523,8 +28910,9 @@ class MultiplexerSessionManager {
28523
28910
  });
28524
28911
  return;
28525
28912
  }
28526
- if (this.sessions.has(sessionId))
28913
+ if (this.sessions.has(sessionId) || this.closingSessions.has(sessionId)) {
28527
28914
  return;
28915
+ }
28528
28916
  log("[multiplexer-session-manager] child session busy again, respawning pane", {
28529
28917
  sessionId,
28530
28918
  parentId: known.parentId,
@@ -28538,6 +28926,14 @@ class MultiplexerSessionManager {
28538
28926
  });
28539
28927
  if (!paneResult.success || !paneResult.paneId)
28540
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
+ }
28541
28937
  const now = Date.now();
28542
28938
  this.sessions.set(sessionId, {
28543
28939
  sessionId,
@@ -28560,8 +28956,21 @@ class MultiplexerSessionManager {
28560
28956
  isTrackedOrSpawning(sessionId) {
28561
28957
  return this.sessions.has(sessionId) || this.spawningSessions.has(sessionId);
28562
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
+ }
28563
28969
  async cleanup() {
28564
28970
  this.stopPolling();
28971
+ if (this.closingSessions.size > 0) {
28972
+ await Promise.all(this.closingSessions.values());
28973
+ }
28565
28974
  if (this.sessions.size > 0 && this.multiplexer) {
28566
28975
  log("[multiplexer-session-manager] closing all panes", {
28567
28976
  count: this.sessions.size
@@ -28576,6 +28985,7 @@ class MultiplexerSessionManager {
28576
28985
  }
28577
28986
  this.knownSessions.clear();
28578
28987
  this.spawningSessions.clear();
28988
+ this.closingSessions.clear();
28579
28989
  log("[multiplexer-session-manager] cleanup complete");
28580
28990
  }
28581
28991
  }
@@ -28787,9 +29197,9 @@ function findSgCliPathSync() {
28787
29197
  }
28788
29198
  if (process.platform === "darwin") {
28789
29199
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
28790
- for (const path12 of homebrewPaths) {
28791
- if (existsSync7(path12) && isValidBinary(path12)) {
28792
- return path12;
29200
+ for (const path13 of homebrewPaths) {
29201
+ if (existsSync7(path13) && isValidBinary(path13)) {
29202
+ return path13;
28793
29203
  }
28794
29204
  }
28795
29205
  }
@@ -28806,8 +29216,8 @@ function getSgCliPath() {
28806
29216
  }
28807
29217
  return "sg";
28808
29218
  }
28809
- function setSgCliPath(path12) {
28810
- resolvedCliPath = path12;
29219
+ function setSgCliPath(path13) {
29220
+ resolvedCliPath = path13;
28811
29221
  }
28812
29222
  var DEFAULT_TIMEOUT_MS2 = 300000;
28813
29223
  var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
@@ -29215,7 +29625,7 @@ Returns the councillor responses with a summary footer.`,
29215
29625
  // src/tools/preset-manager.ts
29216
29626
  var COMMAND_NAME3 = "preset";
29217
29627
  function createPresetManager(ctx, config) {
29218
- let activePreset = config.preset ?? null;
29628
+ let activePreset = getActiveRuntimePreset() ?? config.preset ?? null;
29219
29629
  async function handleCommandExecuteBefore(input, output) {
29220
29630
  if (input.command !== COMMAND_NAME3) {
29221
29631
  return;
@@ -29256,21 +29666,41 @@ function createPresetManager(ctx, config) {
29256
29666
  }
29257
29667
  const agentUpdates = {};
29258
29668
  for (const [agentName, override] of Object.entries(preset)) {
29669
+ const resolvedName = AGENT_ALIASES[agentName] ?? agentName;
29259
29670
  const agentConfig = mapOverrideToAgentConfig(override);
29260
29671
  if (Object.keys(agentConfig).length > 0) {
29261
- agentUpdates[agentName] = agentConfig;
29672
+ agentUpdates[resolvedName] = agentConfig;
29262
29673
  }
29263
29674
  }
29264
- 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) {
29265
29692
  output.parts.push(createInternalAgentTextPart(`Preset "${presetName}" is empty (no agent overrides defined).`));
29266
29693
  return;
29267
29694
  }
29695
+ const previousPreset = activePreset;
29696
+ setActiveRuntimePresetWithPrevious(presetName);
29268
29697
  try {
29269
29698
  await ctx.client.config.update({
29270
- body: { agent: agentUpdates }
29699
+ body: { agent: allUpdates }
29271
29700
  });
29272
29701
  activePreset = presetName;
29273
- const summary = Object.entries(agentUpdates).map(([name, cfg]) => {
29702
+ const summaryParts = [];
29703
+ for (const [name, cfg] of Object.entries(agentUpdates)) {
29274
29704
  const parts = [name];
29275
29705
  if (cfg.model)
29276
29706
  parts.push(`model: ${cfg.model}`);
@@ -29280,12 +29710,16 @@ function createPresetManager(ctx, config) {
29280
29710
  parts.push(`temp: ${cfg.temperature}`);
29281
29711
  if (cfg.options)
29282
29712
  parts.push("options: yes");
29283
- return parts.join(" → ");
29284
- }).join(`
29285
- `);
29713
+ summaryParts.push(parts.join(" → "));
29714
+ }
29715
+ if (Object.keys(resetUpdates).length > 0) {
29716
+ summaryParts.push(`Reset to baseline: ${Object.keys(resetUpdates).join(", ")}`);
29717
+ }
29286
29718
  output.parts.push(createInternalAgentTextPart(`Switched to preset "${presetName}":
29287
- ${summary}`));
29719
+ ${summaryParts.join(`
29720
+ `)}`));
29288
29721
  } catch (err) {
29722
+ rollbackRuntimePreset(previousPreset);
29289
29723
  output.parts.push(createInternalAgentTextPart(`Failed to switch preset "${presetName}": ${String(err)}`));
29290
29724
  }
29291
29725
  }
@@ -29375,14 +29809,14 @@ var BINARY_PREFIXES = [
29375
29809
  var WEBFETCH_DESCRIPTION = "Fetch a URL with better extraction for static/docs pages. Supports llms.txt probing, content-focused HTML extraction, metadata, redirects, and an optional prompt processed by a cheap secondary model.";
29376
29810
  // src/tools/smartfetch/tool.ts
29377
29811
  import os4 from "node:os";
29378
- import path15 from "node:path";
29812
+ import path16 from "node:path";
29379
29813
  import {
29380
29814
  tool as tool4
29381
29815
  } from "@opencode-ai/plugin";
29382
29816
 
29383
29817
  // src/tools/smartfetch/binary.ts
29384
29818
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
29385
- import path12 from "node:path";
29819
+ import path13 from "node:path";
29386
29820
  function extensionForMime(contentType) {
29387
29821
  const mime = contentType.split(";")[0]?.trim().toLowerCase();
29388
29822
  const map = {
@@ -29403,10 +29837,10 @@ function buildBinaryResultMessage(fetchResult, savedPath) {
29403
29837
  async function saveBinary(binaryDir, data, contentType, filename) {
29404
29838
  await mkdir2(binaryDir, { recursive: true });
29405
29839
  const initialName = filename || `webfetch-${Date.now()}.${extensionForMime(contentType)}`;
29406
- const parsed = path12.parse(initialName);
29840
+ const parsed = path13.parse(initialName);
29407
29841
  for (let attempt = 0;attempt < 1000; attempt++) {
29408
29842
  const candidateName = attempt === 0 ? initialName : `${parsed.name}-${attempt}${parsed.ext || `.${extensionForMime(contentType)}`}`;
29409
- const file = path12.join(binaryDir, candidateName);
29843
+ const file = path13.join(binaryDir, candidateName);
29410
29844
  try {
29411
29845
  await writeFile2(file, data, { flag: "wx" });
29412
29846
  return file;
@@ -30060,7 +30494,7 @@ var L = class u2 {
30060
30494
  };
30061
30495
 
30062
30496
  // src/tools/smartfetch/network.ts
30063
- import path13 from "node:path";
30497
+ import path14 from "node:path";
30064
30498
 
30065
30499
  // src/tools/smartfetch/utils.ts
30066
30500
  var import_readability = __toESM(require_readability(), 1);
@@ -30785,7 +31219,7 @@ function inferFilenameFromUrl(url) {
30785
31219
  function truncateFilename(name, maxLength = 180) {
30786
31220
  if (name.length <= maxLength)
30787
31221
  return name;
30788
- const parsed = path13.parse(name);
31222
+ const parsed = path14.parse(name);
30789
31223
  const ext = parsed.ext || "";
30790
31224
  const baseLimit = Math.max(1, maxLength - ext.length);
30791
31225
  return `${parsed.name.slice(0, baseLimit)}${ext}`;
@@ -30957,7 +31391,7 @@ function isInvalidLlmsResult(fetchResult) {
30957
31391
  // src/tools/smartfetch/secondary-model.ts
30958
31392
  import { existsSync as existsSync9 } from "node:fs";
30959
31393
  import { readFile as readFile4 } from "node:fs/promises";
30960
- import path14 from "node:path";
31394
+ import path15 from "node:path";
30961
31395
  function parseModelRef(value) {
30962
31396
  if (!value)
30963
31397
  return;
@@ -30983,7 +31417,7 @@ function pickAgentModelRef(value) {
30983
31417
  }
30984
31418
  function findPreferredOpenCodeConfigPath(baseDir) {
30985
31419
  for (const file of ["opencode.jsonc", "opencode.json"]) {
30986
- const fullPath = path14.join(baseDir, file);
31420
+ const fullPath = path15.join(baseDir, file);
30987
31421
  if (existsSync9(fullPath))
30988
31422
  return fullPath;
30989
31423
  }
@@ -31000,7 +31434,7 @@ async function readOpenCodeConfigFile(configPath) {
31000
31434
  }
31001
31435
  }
31002
31436
  async function readEffectiveOpenCodeConfig(directory) {
31003
- const projectDir = path14.join(directory, ".opencode");
31437
+ const projectDir = path15.join(directory, ".opencode");
31004
31438
  const userDirs = getConfigSearchDirs();
31005
31439
  const projectPath = findPreferredOpenCodeConfigPath(projectDir);
31006
31440
  const userPath = userDirs.map((configDir) => findPreferredOpenCodeConfigPath(configDir)).find(Boolean);
@@ -31161,7 +31595,7 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
31161
31595
  // src/tools/smartfetch/tool.ts
31162
31596
  var z5 = tool4.schema;
31163
31597
  function createWebfetchTool(pluginCtx, options = {}) {
31164
- const binaryDir = options.binaryDir || path15.join(os4.tmpdir(), "opencode-smartfetch");
31598
+ const binaryDir = options.binaryDir || path16.join(os4.tmpdir(), "opencode-smartfetch");
31165
31599
  return tool4({
31166
31600
  description: WEBFETCH_DESCRIPTION,
31167
31601
  args: {
@@ -31685,6 +32119,16 @@ class SubagentDepthTracker {
31685
32119
 
31686
32120
  // src/utils/system-collapse.ts
31687
32121
  function collapseSystemInPlace(system2) {
32122
+ if (system2.length === 0) {
32123
+ return;
32124
+ }
32125
+ if (system2.length === 1) {
32126
+ if (system2[0]) {
32127
+ return;
32128
+ }
32129
+ system2.length = 0;
32130
+ return;
32131
+ }
31688
32132
  const joined = system2.join(`
31689
32133
 
31690
32134
  `);
@@ -31723,6 +32167,7 @@ var OhMyOpenCodeLite = async (ctx) => {
31723
32167
  const sessionId = new Date().toISOString().replace(/[-:]/g, "").slice(0, 15);
31724
32168
  initLogger(sessionId);
31725
32169
  let config;
32170
+ let disabledAgents;
31726
32171
  let agentDefs;
31727
32172
  let agents;
31728
32173
  let mcps;
@@ -31748,9 +32193,20 @@ var OhMyOpenCodeLite = async (ctx) => {
31748
32193
  let presetManager;
31749
32194
  let councilTools;
31750
32195
  let webfetch;
32196
+ let rewriteDisplayNameMentions;
31751
32197
  let toolCount = 0;
31752
32198
  try {
31753
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
+ }
32208
+ disabledAgents = getDisabledAgents(config);
32209
+ rewriteDisplayNameMentions = createDisplayNameMentionRewriter(config);
31754
32210
  agentDefs = createAgents(config);
31755
32211
  agents = getAgentConfigs(config);
31756
32212
  modelArrayMap = {};
@@ -31787,7 +32243,7 @@ var OhMyOpenCodeLite = async (ctx) => {
31787
32243
  main_pane_size: config.multiplexer?.main_pane_size ?? 60
31788
32244
  };
31789
32245
  const multiplexer = getMultiplexer(multiplexerConfig);
31790
- multiplexerEnabled = multiplexerConfig.type !== "none" && multiplexer !== null;
32246
+ multiplexerEnabled = multiplexerConfig.type !== "none" && multiplexer !== null && multiplexer.isInsideSession();
31791
32247
  log("[plugin] initialized with multiplexer config", {
31792
32248
  multiplexerConfig,
31793
32249
  enabled: multiplexerEnabled,
@@ -31824,6 +32280,8 @@ var OhMyOpenCodeLite = async (ctx) => {
31824
32280
  });
31825
32281
  taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
31826
32282
  maxSessionsPerAgent: config.sessionManager?.maxSessionsPerAgent ?? 2,
32283
+ readContextMinLines: config.sessionManager?.readContextMinLines ?? 10,
32284
+ readContextMaxFiles: config.sessionManager?.readContextMaxFiles ?? 8,
31827
32285
  shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
31828
32286
  });
31829
32287
  interviewManager = createInterviewManager(ctx, config);
@@ -31943,6 +32401,83 @@ var OhMyOpenCodeLite = async (ctx) => {
31943
32401
  });
31944
32402
  }
31945
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
+ }
31946
32481
  const configMcp = opencodeConfig.mcp;
31947
32482
  if (!configMcp) {
31948
32483
  opencodeConfig.mcp = { ...mcps };
@@ -32000,7 +32535,6 @@ var OhMyOpenCodeLite = async (ctx) => {
32000
32535
  await multiplexerSessionManager.onSessionStatus(event);
32001
32536
  await multiplexerSessionManager.onSessionDeleted(event);
32002
32537
  await interviewManager.handleEvent(input);
32003
- await postFileToolNudgeHook.event(input);
32004
32538
  await taskSessionManagerHook.event(input);
32005
32539
  if (input.event.type === "session.deleted") {
32006
32540
  const props = input.event.properties;
@@ -32043,15 +32577,12 @@ var OhMyOpenCodeLite = async (ctx) => {
32043
32577
  const alreadyInjected = output.system.some((s) => typeof s === "string" && s.includes("<Role>") && s.includes("orchestrator"));
32044
32578
  if (!alreadyInjected) {
32045
32579
  const orchestratorDef = agentDefs.find((a) => a.name === "orchestrator");
32046
- const orchestratorPrompt = typeof orchestratorDef?.config?.prompt === "string" ? orchestratorDef.config.prompt : buildOrchestratorPrompt(getDisabledAgents(config));
32580
+ const orchestratorPrompt = typeof orchestratorDef?.config?.prompt === "string" ? orchestratorDef.config.prompt : buildOrchestratorPrompt(disabledAgents);
32047
32581
  output.system[0] = orchestratorPrompt + (output.system[0] ? `
32048
32582
 
32049
32583
  ${output.system[0]}` : "");
32050
32584
  }
32051
32585
  }
32052
- await todoContinuationHook.handleChatSystemTransform(input, output);
32053
- await postFileToolNudgeHook["experimental.chat.system.transform"](input, output);
32054
- await taskSessionManagerHook["experimental.chat.system.transform"](input, output);
32055
32586
  collapseSystemInPlace(output.system);
32056
32587
  },
32057
32588
  "experimental.chat.messages.transform": async (input, output) => {
@@ -32064,25 +32595,26 @@ ${output.system[0]}` : "");
32064
32595
  if (part.type !== "text" || typeof part.text !== "string") {
32065
32596
  continue;
32066
32597
  }
32067
- part.text = rewriteDisplayNameMentions(config, part.text);
32598
+ part.text = rewriteDisplayNameMentions(part.text);
32068
32599
  }
32069
32600
  }
32070
32601
  processImageAttachments({
32071
32602
  messages: typedOutput.messages,
32072
32603
  workDir: ctx.directory,
32073
- disabledAgents: getDisabledAgents(config),
32604
+ disabledAgents,
32074
32605
  log
32075
32606
  });
32076
32607
  await todoContinuationHook.handleMessagesTransform({
32077
32608
  messages: typedOutput.messages
32078
32609
  });
32610
+ await taskSessionManagerHook["experimental.chat.messages.transform"](input, typedOutput);
32079
32611
  await phaseReminderHook["experimental.chat.messages.transform"](input, typedOutput);
32080
32612
  await filterAvailableSkillsHook["experimental.chat.messages.transform"](input, typedOutput);
32081
32613
  },
32082
32614
  "tool.execute.after": async (input, output) => {
32083
32615
  await delegateTaskRetryHook["tool.execute.after"](input, output);
32084
32616
  await jsonErrorRecoveryHook["tool.execute.after"](input, output);
32085
- await todoContinuationHook.handleToolExecuteAfter(input);
32617
+ await todoContinuationHook.handleToolExecuteAfter(input, output);
32086
32618
  await postFileToolNudgeHook["tool.execute.after"](input, output);
32087
32619
  await taskSessionManagerHook["tool.execute.after"](input, output);
32088
32620
  }