oh-my-opencode-slim 1.0.2 → 1.0.3

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/README.md CHANGED
@@ -8,7 +8,6 @@
8
8
  <a href="https://boringdystopia.ai/"><img src="https://img.shields.io/badge/boringdystopia.ai-111111?style=for-the-badge&logo=vercel&logoColor=white" alt="boringdystopia.ai"></a>&nbsp;
9
9
  <a href="https://x.com/alvinunreal"><img src="https://img.shields.io/badge/X-@alvinunreal-000000?style=for-the-badge&logo=x&logoColor=white" alt="X @alvinunreal"></a>&nbsp;
10
10
  <a href="https://t.me/boringdystopiadevelopment"><img src="https://img.shields.io/badge/Telegram-Join%20channel-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white" alt="Telegram Join channel"></a>&nbsp;
11
- <a href="https://deepwiki.com/alvinunreal/oh-my-opencode-slim"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
12
11
  </p>
13
12
  </div>
14
13
 
@@ -69,7 +68,7 @@ The default generated configuration looks like this:
69
68
  "preset": "openai",
70
69
  "presets": {
71
70
  "openai": {
72
- "orchestrator": { "model": "openai/gpt-5.5", "variant": "high", "skills": ["*"], "mcps": ["*", "!context7"] },
71
+ "orchestrator": { "model": "openai/gpt-5.5", "skills": ["*"], "mcps": ["*", "!context7"] },
73
72
  "oracle": { "model": "openai/gpt-5.5", "variant": "high", "skills": ["simplify"], "mcps": [] },
74
73
  "librarian": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
75
74
  "explorer": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": [], "mcps": [] },
package/dist/cli/index.js CHANGED
@@ -260,7 +260,9 @@ var InterviewConfigSchema = z2.object({
260
260
  dashboard: z2.boolean().default(false)
261
261
  });
262
262
  var SessionManagerConfigSchema = z2.object({
263
- maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2)
263
+ maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2),
264
+ readContextMinLines: z2.number().int().min(0).max(1000).default(10),
265
+ readContextMaxFiles: z2.number().int().min(0).max(50).default(8)
264
266
  });
265
267
  var TodoContinuationConfigSchema = z2.object({
266
268
  maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
@@ -160,6 +160,8 @@ export declare const InterviewConfigSchema: z.ZodObject<{
160
160
  export type InterviewConfig = z.infer<typeof InterviewConfigSchema>;
161
161
  export declare const SessionManagerConfigSchema: z.ZodObject<{
162
162
  maxSessionsPerAgent: z.ZodDefault<z.ZodNumber>;
163
+ readContextMinLines: z.ZodDefault<z.ZodNumber>;
164
+ readContextMaxFiles: z.ZodDefault<z.ZodNumber>;
163
165
  }, z.core.$strip>;
164
166
  export type SessionManagerConfig = z.infer<typeof SessionManagerConfigSchema>;
165
167
  export declare const TodoContinuationConfigSchema: z.ZodObject<{
@@ -305,6 +307,8 @@ export declare const PluginConfigSchema: z.ZodObject<{
305
307
  }, z.core.$strip>>;
306
308
  sessionManager: z.ZodOptional<z.ZodObject<{
307
309
  maxSessionsPerAgent: z.ZodDefault<z.ZodNumber>;
310
+ readContextMinLines: z.ZodDefault<z.ZodNumber>;
311
+ readContextMaxFiles: z.ZodDefault<z.ZodNumber>;
308
312
  }, z.core.$strip>>;
309
313
  todoContinuation: z.ZodOptional<z.ZodObject<{
310
314
  maxContinuations: z.ZodDefault<z.ZodNumber>;
@@ -1,6 +1,8 @@
1
1
  import type { PluginInput } from '@opencode-ai/plugin';
2
2
  export declare function createTaskSessionManagerHook(_ctx: PluginInput, options: {
3
3
  maxSessionsPerAgent: number;
4
+ readContextMinLines?: number;
5
+ readContextMaxFiles?: number;
4
6
  shouldManageSession: (sessionID: string) => boolean;
5
7
  }): {
6
8
  'tool.execute.before': (input: {
@@ -16,6 +18,7 @@ export declare function createTaskSessionManagerHook(_ctx: PluginInput, options:
16
18
  callID?: string;
17
19
  }, output: {
18
20
  output: unknown;
21
+ metadata?: unknown;
19
22
  }) => Promise<void>;
20
23
  'experimental.chat.system.transform': (input: {
21
24
  sessionID?: string;
@@ -28,6 +31,7 @@ export declare function createTaskSessionManagerHook(_ctx: PluginInput, options:
28
31
  properties?: {
29
32
  info?: {
30
33
  id?: string;
34
+ parentID?: string;
31
35
  };
32
36
  sessionID?: string;
33
37
  };
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;
@@ -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"),
@@ -19769,12 +19771,14 @@ function getDisabledAgents(config) {
19769
19771
 
19770
19772
  // src/utils/logger.ts
19771
19773
  import * as fs2 from "node:fs";
19774
+ import { appendFile } from "node:fs/promises";
19772
19775
  import * as os from "node:os";
19773
19776
  import * as path2 from "node:path";
19774
19777
  var LOG_PREFIX = "oh-my-opencode-slim.";
19775
19778
  var LOG_SUFFIX = ".log";
19776
19779
  var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
19777
19780
  var logFile = null;
19781
+ var writeChain = Promise.resolve();
19778
19782
  function getLogDir() {
19779
19783
  return process.env.OPENCODE_LOG_DIR ?? path2.join(os.homedir(), ".local/share/opencode");
19780
19784
  }
@@ -19817,10 +19821,14 @@ function initLogger(sessionId) {
19817
19821
  fs2.mkdirSync(dir, { recursive: true });
19818
19822
  } catch {}
19819
19823
  logFile = path2.join(dir, `${LOG_PREFIX}${sessionId}${LOG_SUFFIX}`);
19824
+ try {
19825
+ fs2.closeSync(fs2.openSync(logFile, "a"));
19826
+ } catch {}
19820
19827
  cleanupOldLogs(dir);
19821
19828
  }
19822
19829
  function log(message, data) {
19823
- if (!logFile)
19830
+ const target = logFile;
19831
+ if (!target)
19824
19832
  return;
19825
19833
  try {
19826
19834
  const timestamp = new Date().toISOString();
@@ -19834,7 +19842,7 @@ function log(message, data) {
19834
19842
  }
19835
19843
  const logEntry = `[${timestamp}] ${message} ${dataStr}
19836
19844
  `;
19837
- fs2.appendFileSync(logFile, logEntry);
19845
+ writeChain = writeChain.then(() => appendFile(target, logEntry)).catch(() => {});
19838
19846
  } catch {}
19839
19847
  }
19840
19848
 
@@ -20020,7 +20028,7 @@ class CouncilManager {
20020
20028
  }
20021
20029
  } else {
20022
20030
  const promises = entries.map(([name, config], index) => (async () => {
20023
- if (index > 0) {
20031
+ if (this.tmuxEnabled && index > 0) {
20024
20032
  await new Promise((r) => setTimeout(r, index * COUNCILLOR_STAGGER_MS));
20025
20033
  }
20026
20034
  return this.runCouncillorWithRetry(name, config, prompt, parentSessionId, timeout, maxRetries);
@@ -22036,11 +22044,8 @@ function resolveRuntimeAgentName(config, agentName) {
22036
22044
  function escapeRegExp2(value) {
22037
22045
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22038
22046
  }
22039
- function rewriteDisplayNameMentions(config, text) {
22040
- if (!text.includes("@")) {
22041
- return text;
22042
- }
22043
- let rewritten = text;
22047
+ function createDisplayNameMentionRewriter(config) {
22048
+ const replacements = [];
22044
22049
  for (const internalName of getRuntimeAgentNames(config)) {
22045
22050
  const displayName = getAgentOverride(config, internalName)?.displayName;
22046
22051
  if (!displayName) {
@@ -22050,9 +22055,24 @@ function rewriteDisplayNameMentions(config, text) {
22050
22055
  if (!normalizedDisplayName || normalizedDisplayName === internalName) {
22051
22056
  continue;
22052
22057
  }
22053
- rewritten = rewritten.replace(new RegExp(`(^|[^\\w.])@${escapeRegExp2(normalizedDisplayName)}\\b`, "g"), `$1@${internalName}`);
22058
+ replacements.push({
22059
+ regex: new RegExp(`(^|[^\\w.])@${escapeRegExp2(normalizedDisplayName)}\\b`, "g"),
22060
+ internalName
22061
+ });
22062
+ }
22063
+ if (replacements.length === 0) {
22064
+ return (text) => text;
22054
22065
  }
22055
- return rewritten;
22066
+ return (text) => {
22067
+ if (!text.includes("@")) {
22068
+ return text;
22069
+ }
22070
+ let rewritten = text;
22071
+ for (const replacement of replacements) {
22072
+ rewritten = rewritten.replace(replacement.regex, `$1@${replacement.internalName}`);
22073
+ }
22074
+ return rewritten;
22075
+ };
22056
22076
  }
22057
22077
  // src/utils/internal-initiator.ts
22058
22078
  var SLIM_INTERNAL_INITIATOR_MARKER = "<!-- SLIM_INTERNAL_INITIATOR -->";
@@ -22076,6 +22096,8 @@ function hasInternalInitiatorMarker(part) {
22076
22096
  return part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER);
22077
22097
  }
22078
22098
  // src/utils/session-manager.ts
22099
+ var MIN_CONTEXT_FILE_LINES = 10;
22100
+ var MAX_CONTEXT_FILES_PER_SESSION = 8;
22079
22101
  function aliasPrefix(agentType) {
22080
22102
  switch (agentType) {
22081
22103
  case "explorer":
@@ -22115,11 +22137,15 @@ function deriveTaskSessionLabel(input) {
22115
22137
 
22116
22138
  class SessionManager {
22117
22139
  maxSessionsPerAgent;
22140
+ readContextMinLines;
22141
+ readContextMaxFiles;
22118
22142
  sessionsByParent = new Map;
22119
22143
  nextAliasIndexByParent = new Map;
22120
22144
  orderCounter = 0;
22121
- constructor(maxSessionsPerAgent) {
22145
+ constructor(maxSessionsPerAgent, options = {}) {
22122
22146
  this.maxSessionsPerAgent = maxSessionsPerAgent;
22147
+ this.readContextMinLines = options.readContextMinLines ?? MIN_CONTEXT_FILE_LINES;
22148
+ this.readContextMaxFiles = options.readContextMaxFiles ?? MAX_CONTEXT_FILES_PER_SESSION;
22123
22149
  }
22124
22150
  remember(input) {
22125
22151
  const now = this.nextOrder();
@@ -22138,6 +22164,7 @@ class SessionManager {
22138
22164
  taskId: input.taskId,
22139
22165
  agentType: input.agentType,
22140
22166
  label: input.label,
22167
+ contextFiles: [],
22141
22168
  createdAt: now,
22142
22169
  lastUsedAt: now
22143
22170
  };
@@ -22171,6 +22198,39 @@ class SessionManager {
22171
22198
  }
22172
22199
  }
22173
22200
  }
22201
+ taskIds() {
22202
+ const ids = new Set;
22203
+ for (const groups of this.sessionsByParent.values()) {
22204
+ for (const group of groups.values()) {
22205
+ for (const entry of group) {
22206
+ ids.add(entry.taskId);
22207
+ }
22208
+ }
22209
+ }
22210
+ return ids;
22211
+ }
22212
+ addContext(taskId, files) {
22213
+ if (files.length === 0)
22214
+ return;
22215
+ for (const groups of this.sessionsByParent.values()) {
22216
+ for (const group of groups.values()) {
22217
+ const match = group.find((entry) => entry.taskId === taskId);
22218
+ if (!match)
22219
+ continue;
22220
+ const existing = new Map(match.contextFiles.map((file) => [file.path, file]));
22221
+ for (const file of files) {
22222
+ const previous = existing.get(file.path);
22223
+ if (previous) {
22224
+ previous.lineCount = Math.max(previous.lineCount, file.lineCount);
22225
+ previous.lastReadAt = Math.max(previous.lastReadAt, file.lastReadAt);
22226
+ continue;
22227
+ }
22228
+ match.contextFiles.push({ ...file });
22229
+ }
22230
+ this.trimContextFiles(match);
22231
+ }
22232
+ }
22233
+ }
22174
22234
  clearParent(parentSessionId) {
22175
22235
  this.sessionsByParent.delete(parentSessionId);
22176
22236
  this.nextAliasIndexByParent.delete(parentSessionId);
@@ -22182,7 +22242,17 @@ class SessionManager {
22182
22242
  const lines = [...groups.entries()].map(([agentType, entries]) => [
22183
22243
  agentType,
22184
22244
  [...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("; ")}`);
22245
+ ]).filter(([, entries]) => entries.length > 0).sort((a, b) => b[1][0].lastUsedAt - a[1][0].lastUsedAt).map(([agentType, entries]) => [
22246
+ `- ${agentType}: ${entries.map((entry) => `${entry.alias} ${entry.label}`).join("; ")}`,
22247
+ ...entries.map((entry) => [
22248
+ entry,
22249
+ formatContextFiles(entry.contextFiles, {
22250
+ minLines: this.readContextMinLines,
22251
+ maxFiles: this.readContextMaxFiles
22252
+ })
22253
+ ]).filter(([, context]) => context.length > 0).map(([entry, context]) => ` Context read by ${entry.alias}: ${context}`)
22254
+ ].join(`
22255
+ `));
22186
22256
  if (lines.length === 0)
22187
22257
  return;
22188
22258
  return [
@@ -22236,11 +22306,25 @@ class SessionManager {
22236
22306
  group.length = this.maxSessionsPerAgent;
22237
22307
  }
22238
22308
  }
22309
+ trimContextFiles(entry) {
22310
+ if (this.readContextMaxFiles === 0) {
22311
+ entry.contextFiles = [];
22312
+ return;
22313
+ }
22314
+ entry.contextFiles = entry.contextFiles.filter((file) => file.lineCount >= this.readContextMinLines).sort((a, b) => b.lastReadAt - a.lastReadAt).slice(0, this.readContextMaxFiles + 1);
22315
+ }
22239
22316
  nextOrder() {
22240
22317
  this.orderCounter += 1;
22241
22318
  return this.orderCounter;
22242
22319
  }
22243
22320
  }
22321
+ function formatContextFiles(files, options) {
22322
+ const eligible = files.filter((file) => file.lineCount >= options.minLines).sort((a, b) => b.lastReadAt - a.lastReadAt);
22323
+ const shown = eligible.slice(0, options.maxFiles);
22324
+ const rest = eligible.length - shown.length;
22325
+ const rendered = shown.map((file) => `${file.path} (${file.lineCount} lines)`);
22326
+ return `${rendered.join(", ")}${rest > 0 ? ` (+${rest} more)` : ""}`;
22327
+ }
22244
22328
  // src/utils/task.ts
22245
22329
  function parseTaskIdFromTaskOutput(output) {
22246
22330
  const lines = output.split(/\r?\n/);
@@ -22542,6 +22626,17 @@ ${allowedEntries.map((entry) => entry.block).join(`
22542
22626
  });
22543
22627
  }
22544
22628
  function createFilterAvailableSkillsHook(_ctx, config) {
22629
+ const permissionRulesByAgent = new Map;
22630
+ const getPermissionRules = (agentName) => {
22631
+ const cached = permissionRulesByAgent.get(agentName);
22632
+ if (cached) {
22633
+ return cached;
22634
+ }
22635
+ const configuredSkills = getAgentOverride(config, agentName)?.skills;
22636
+ const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills);
22637
+ permissionRulesByAgent.set(agentName, permissionRules);
22638
+ return permissionRules;
22639
+ };
22545
22640
  return {
22546
22641
  "experimental.chat.messages.transform": async (_input, output) => {
22547
22642
  const { messages } = output;
@@ -22549,8 +22644,7 @@ function createFilterAvailableSkillsHook(_ctx, config) {
22549
22644
  return;
22550
22645
  }
22551
22646
  const agentName = getCurrentAgent(messages);
22552
- const configuredSkills = getAgentOverride(config, agentName)?.skills;
22553
- const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills);
22647
+ const permissionRules = getPermissionRules(agentName);
22554
22648
  for (const message of messages) {
22555
22649
  for (const part of message.parts) {
22556
22650
  if (part.type !== "text" || !part.text || !part.text.includes("<available_skills>")) {
@@ -22903,7 +22997,21 @@ function processImageAttachments(args) {
22903
22997
  const observerEnabled = !disabledAgents.has("observer");
22904
22998
  if (!observerEnabled)
22905
22999
  return;
23000
+ const messagesWithImages = [];
23001
+ for (const msg of messages) {
23002
+ if (msg.info.role !== "user")
23003
+ continue;
23004
+ const imageParts = msg.parts.filter(isImagePart);
23005
+ if (imageParts.length > 0) {
23006
+ messagesWithImages.push({ msg, imageParts });
23007
+ }
23008
+ }
22906
23009
  const saveDir = join7(workDir, ".opencode", "images");
23010
+ if (messagesWithImages.length === 0) {
23011
+ if (existsSync4(saveDir))
23012
+ cleanupAllSessions(saveDir);
23013
+ return;
23014
+ }
22907
23015
  const gitignorePath = join7(workDir, ".opencode", ".gitignore");
22908
23016
  try {
22909
23017
  mkdirSync2(saveDir, { recursive: true });
@@ -22914,12 +23022,7 @@ function processImageAttachments(args) {
22914
23022
  log2(`[image-hook] failed to create image directory: ${e}`);
22915
23023
  }
22916
23024
  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;
23025
+ for (const { msg, imageParts } of messagesWithImages) {
22923
23026
  const sessionSubdir = msg.info.sessionID ? sanitizeFilename(msg.info.sessionID) : undefined;
22924
23027
  const targetDir = sessionSubdir ? join7(saveDir, sessionSubdir) : saveDir;
22925
23028
  try {
@@ -23080,6 +23183,7 @@ function createPostFileToolNudgeHook(options = {}) {
23080
23183
  };
23081
23184
  }
23082
23185
  // src/hooks/task-session-manager/index.ts
23186
+ import path8 from "node:path";
23083
23187
  var AGENT_NAME_SET = new Set([
23084
23188
  "orchestrator",
23085
23189
  "oracle",
@@ -23098,10 +23202,89 @@ function isAgentName(value) {
23098
23202
  function isObjectRecord(value) {
23099
23203
  return typeof value === "object" && value !== null;
23100
23204
  }
23205
+ function extractPath(output) {
23206
+ return /<path>([^<]+)<\/path>/.exec(output)?.[1];
23207
+ }
23208
+ function normalizePath(root, file) {
23209
+ const relative = path8.relative(root, file);
23210
+ if (!relative || relative.startsWith("..") || path8.isAbsolute(relative)) {
23211
+ return file;
23212
+ }
23213
+ return relative;
23214
+ }
23215
+ function extractReadFiles(root, output) {
23216
+ if (typeof output.output !== "string")
23217
+ return [];
23218
+ const file = extractPath(output.output);
23219
+ if (!file)
23220
+ return [];
23221
+ return [
23222
+ {
23223
+ path: normalizePath(root, file),
23224
+ lineCount: countReadLines(output.output).length,
23225
+ lineNumbers: countReadLines(output.output),
23226
+ lastReadAt: Date.now()
23227
+ }
23228
+ ];
23229
+ }
23230
+ function countReadLines(output) {
23231
+ const lines = new Set;
23232
+ for (const match of output.matchAll(/^([0-9]+):/gm)) {
23233
+ lines.add(Number(match[1]));
23234
+ }
23235
+ return [...lines];
23236
+ }
23101
23237
  function createTaskSessionManagerHook(_ctx, options) {
23102
- const sessionManager = new SessionManager(options.maxSessionsPerAgent);
23238
+ const sessionManager = new SessionManager(options.maxSessionsPerAgent, {
23239
+ readContextMinLines: options.readContextMinLines,
23240
+ readContextMaxFiles: options.readContextMaxFiles
23241
+ });
23103
23242
  const pendingCalls = new Map;
23104
23243
  const pendingCallOrder = [];
23244
+ const contextByTask = new Map;
23245
+ const pendingManagedTaskIds = new Set;
23246
+ function addTaskContext(taskId, files) {
23247
+ if (files.length === 0)
23248
+ return;
23249
+ let context = contextByTask.get(taskId);
23250
+ if (!context) {
23251
+ context = new Map;
23252
+ contextByTask.set(taskId, context);
23253
+ }
23254
+ for (const file of files) {
23255
+ const pending = context.get(file.path) ?? {
23256
+ path: file.path,
23257
+ lines: new Set,
23258
+ lastReadAt: file.lastReadAt
23259
+ };
23260
+ for (const line of file.lineNumbers ?? []) {
23261
+ pending.lines.add(line);
23262
+ }
23263
+ pending.lastReadAt = Math.max(pending.lastReadAt, file.lastReadAt);
23264
+ context.set(file.path, pending);
23265
+ }
23266
+ sessionManager.addContext(taskId, contextFilesForPrompt(context));
23267
+ }
23268
+ function contextFilesForPrompt(context) {
23269
+ if (!context)
23270
+ return [];
23271
+ return [...context.values()].map((file) => ({
23272
+ path: file.path,
23273
+ lineCount: file.lines.size,
23274
+ lastReadAt: file.lastReadAt
23275
+ }));
23276
+ }
23277
+ function canTrackTaskContext(taskId) {
23278
+ return pendingManagedTaskIds.has(taskId) || sessionManager.taskIds().has(taskId);
23279
+ }
23280
+ function pruneContext() {
23281
+ const remembered = sessionManager.taskIds();
23282
+ for (const taskId of contextByTask.keys()) {
23283
+ if (!pendingManagedTaskIds.has(taskId) && !remembered.has(taskId)) {
23284
+ contextByTask.delete(taskId);
23285
+ }
23286
+ }
23287
+ }
23105
23288
  function isMissingRememberedSessionError(output) {
23106
23289
  const firstLine = output.split(/\r?\n/, 1)[0]?.trim().toLowerCase() ?? "";
23107
23290
  return firstLine.startsWith("[error]") && firstLine.includes("session") && (firstLine.includes("not found") || firstLine.includes("no session"));
@@ -23167,6 +23350,7 @@ function createTaskSessionManagerHook(_ctx, options) {
23167
23350
  return;
23168
23351
  }
23169
23352
  args.task_id = remembered.taskId;
23353
+ pendingManagedTaskIds.add(remembered.taskId);
23170
23354
  sessionManager.markUsed(input.sessionID, args.subagent_type, remembered.taskId);
23171
23355
  if (input.callID) {
23172
23356
  rememberPendingCall({
@@ -23179,6 +23363,12 @@ function createTaskSessionManagerHook(_ctx, options) {
23179
23363
  }
23180
23364
  },
23181
23365
  "tool.execute.after": async (input, output) => {
23366
+ if (input.tool.toLowerCase() === "read") {
23367
+ if (input.sessionID && canTrackTaskContext(input.sessionID)) {
23368
+ addTaskContext(input.sessionID, extractReadFiles(_ctx.directory, output));
23369
+ }
23370
+ return;
23371
+ }
23182
23372
  if (input.tool.toLowerCase() !== "task")
23183
23373
  return;
23184
23374
  const pending = takePendingCall(input.callID);
@@ -23200,6 +23390,10 @@ function createTaskSessionManagerHook(_ctx, options) {
23200
23390
  agentType: pending.agentType,
23201
23391
  label: pending.label
23202
23392
  });
23393
+ pendingManagedTaskIds.delete(taskId);
23394
+ const contextFiles = contextFilesForPrompt(contextByTask.get(taskId));
23395
+ sessionManager.addContext(taskId, contextFiles);
23396
+ pruneContext();
23203
23397
  },
23204
23398
  "experimental.chat.system.transform": async (input, output) => {
23205
23399
  if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
@@ -23211,6 +23405,13 @@ function createTaskSessionManagerHook(_ctx, options) {
23211
23405
  output.system.push(reminder);
23212
23406
  },
23213
23407
  event: async (input) => {
23408
+ if (input.event.type === "session.created") {
23409
+ const info = input.event.properties?.info;
23410
+ if (info?.id && info.parentID && options.shouldManageSession(info.parentID)) {
23411
+ pendingManagedTaskIds.add(info.id);
23412
+ }
23413
+ return;
23414
+ }
23214
23415
  if (input.event.type !== "session.deleted")
23215
23416
  return;
23216
23417
  const sessionId = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
@@ -23218,6 +23419,9 @@ function createTaskSessionManagerHook(_ctx, options) {
23218
23419
  return;
23219
23420
  sessionManager.clearParent(sessionId);
23220
23421
  sessionManager.dropTask(sessionId);
23422
+ contextByTask.delete(sessionId);
23423
+ pendingManagedTaskIds.delete(sessionId);
23424
+ pruneContext();
23221
23425
  for (const [callId, pending] of pendingCalls.entries()) {
23222
23426
  if (pending.parentSessionId !== sessionId) {
23223
23427
  continue;
@@ -23889,7 +24093,7 @@ function createTodoContinuationHook(ctx, config) {
23889
24093
  };
23890
24094
  }
23891
24095
  // src/interview/manager.ts
23892
- import path11 from "node:path";
24096
+ import path12 from "node:path";
23893
24097
 
23894
24098
  // src/interview/dashboard.ts
23895
24099
  import crypto from "node:crypto";
@@ -23899,27 +24103,27 @@ import {
23899
24103
  createServer
23900
24104
  } from "node:http";
23901
24105
  import os3 from "node:os";
23902
- import path9 from "node:path";
24106
+ import path10 from "node:path";
23903
24107
  import { URL as URL2 } from "node:url";
23904
24108
 
23905
24109
  // src/interview/document.ts
23906
24110
  import * as fsSync from "node:fs";
23907
24111
  import * as fs6 from "node:fs/promises";
23908
- import * as path8 from "node:path";
24112
+ import * as path9 from "node:path";
23909
24113
  var DEFAULT_OUTPUT_FOLDER = "interview";
23910
24114
  function normalizeOutputFolder(outputFolder) {
23911
24115
  const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
23912
24116
  return normalized || DEFAULT_OUTPUT_FOLDER;
23913
24117
  }
23914
24118
  function createInterviewDirectoryPath(directory, outputFolder) {
23915
- return path8.join(directory, normalizeOutputFolder(outputFolder));
24119
+ return path9.join(directory, normalizeOutputFolder(outputFolder));
23916
24120
  }
23917
24121
  function createInterviewFilePath(directory, outputFolder, idea) {
23918
24122
  const fileName = `${slugify(idea) || "interview"}.md`;
23919
- return path8.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
24123
+ return path9.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
23920
24124
  }
23921
24125
  function relativeInterviewPath(directory, filePath) {
23922
- return path8.relative(directory, filePath) || path8.basename(filePath);
24126
+ return path9.relative(directory, filePath) || path9.basename(filePath);
23923
24127
  }
23924
24128
  function resolveExistingInterviewPath(directory, outputFolder, value) {
23925
24129
  const trimmed = value.trim();
@@ -23928,22 +24132,22 @@ function resolveExistingInterviewPath(directory, outputFolder, value) {
23928
24132
  }
23929
24133
  const outputDir = createInterviewDirectoryPath(directory, outputFolder);
23930
24134
  const candidates = new Set;
23931
- const resolvedRoot = path8.resolve(directory);
23932
- if (path8.isAbsolute(trimmed)) {
24135
+ const resolvedRoot = path9.resolve(directory);
24136
+ if (path9.isAbsolute(trimmed)) {
23933
24137
  candidates.add(trimmed);
23934
24138
  } else {
23935
- candidates.add(path8.resolve(directory, trimmed));
23936
- candidates.add(path8.join(outputDir, trimmed));
24139
+ candidates.add(path9.resolve(directory, trimmed));
24140
+ candidates.add(path9.join(outputDir, trimmed));
23937
24141
  if (!trimmed.endsWith(".md")) {
23938
- candidates.add(path8.join(outputDir, `${trimmed}.md`));
24142
+ candidates.add(path9.join(outputDir, `${trimmed}.md`));
23939
24143
  }
23940
24144
  }
23941
24145
  for (const candidate of candidates) {
23942
- if (path8.extname(candidate) !== ".md") {
24146
+ if (path9.extname(candidate) !== ".md") {
23943
24147
  continue;
23944
24148
  }
23945
- const resolved = path8.resolve(candidate);
23946
- if (!resolved.startsWith(resolvedRoot + path8.sep) && resolved !== resolvedRoot) {
24149
+ const resolved = path9.resolve(candidate);
24150
+ if (!resolved.startsWith(resolvedRoot + path9.sep) && resolved !== resolvedRoot) {
23947
24151
  continue;
23948
24152
  }
23949
24153
  if (fsSync.existsSync(candidate)) {
@@ -24023,7 +24227,7 @@ function parseFrontmatter(content) {
24023
24227
  return result;
24024
24228
  }
24025
24229
  async function ensureInterviewFile(record) {
24026
- await fs6.mkdir(path8.dirname(record.markdownPath), { recursive: true });
24230
+ await fs6.mkdir(path9.dirname(record.markdownPath), { recursive: true });
24027
24231
  try {
24028
24232
  await fs6.access(record.markdownPath);
24029
24233
  } catch {
@@ -25693,12 +25897,12 @@ function renderInterviewPage(interviewId, resumeSlug) {
25693
25897
 
25694
25898
  // src/interview/dashboard.ts
25695
25899
  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`);
25900
+ const dataHome = process.env.XDG_DATA_HOME || path10.join(os3.homedir(), ".local", "share");
25901
+ return path10.join(dataHome, "opencode", `.dashboard-${port}.json`);
25698
25902
  }
25699
25903
  function writeAuthFile(port, token) {
25700
25904
  const filePath = getAuthFilePath(port);
25701
- const dir = path9.dirname(filePath);
25905
+ const dir = path10.dirname(filePath);
25702
25906
  try {
25703
25907
  fsSync2.mkdirSync(dir, { recursive: true });
25704
25908
  } catch {}
@@ -25835,7 +26039,7 @@ function createDashboardServer(config) {
25835
26039
  const directories = getKnownDirectories();
25836
26040
  const items = [];
25837
26041
  for (const dir of directories) {
25838
- const interviewDir = path9.join(dir, config.outputFolder);
26042
+ const interviewDir = path10.join(dir, config.outputFolder);
25839
26043
  let entries;
25840
26044
  try {
25841
26045
  entries = await fs7.readdir(interviewDir);
@@ -25847,7 +26051,7 @@ function createDashboardServer(config) {
25847
26051
  continue;
25848
26052
  let content;
25849
26053
  try {
25850
- content = await fs7.readFile(path9.join(interviewDir, entry), "utf8");
26054
+ content = await fs7.readFile(path10.join(interviewDir, entry), "utf8");
25851
26055
  } catch {
25852
26056
  continue;
25853
26057
  }
@@ -25873,7 +26077,7 @@ function createDashboardServer(config) {
25873
26077
  const directories = getKnownDirectories();
25874
26078
  let rebuilt = 0;
25875
26079
  for (const dir of directories) {
25876
- const interviewDir = path9.join(dir, config.outputFolder);
26080
+ const interviewDir = path10.join(dir, config.outputFolder);
25877
26081
  let entries;
25878
26082
  try {
25879
26083
  entries = await fs7.readdir(interviewDir);
@@ -25885,7 +26089,7 @@ function createDashboardServer(config) {
25885
26089
  continue;
25886
26090
  let content;
25887
26091
  try {
25888
- content = await fs7.readFile(path9.join(interviewDir, entry), "utf8");
26092
+ content = await fs7.readFile(path10.join(interviewDir, entry), "utf8");
25889
26093
  } catch {
25890
26094
  continue;
25891
26095
  }
@@ -25911,7 +26115,7 @@ function createDashboardServer(config) {
25911
26115
  questions: [],
25912
26116
  pendingAnswers: null,
25913
26117
  lastUpdatedAt: fm.updatedAt ? new Date(fm.updatedAt).getTime() : Date.now(),
25914
- filePath: path9.join(interviewDir, entry),
26118
+ filePath: path10.join(interviewDir, entry),
25915
26119
  nudgeAction: null
25916
26120
  });
25917
26121
  if (!sessions.has(fm.sessionID)) {
@@ -26175,7 +26379,7 @@ function createDashboardServer(config) {
26175
26379
  const dirs = getKnownDirectories();
26176
26380
  for (const dir of dirs) {
26177
26381
  const slug = extractResumeSlug(interviewId);
26178
- const candidate = path9.join(dir, config.outputFolder, `${slug}.md`);
26382
+ const candidate = path10.join(dir, config.outputFolder, `${slug}.md`);
26179
26383
  try {
26180
26384
  document = await fs7.readFile(candidate, "utf8");
26181
26385
  markdownPath = candidate;
@@ -26703,7 +26907,7 @@ function createInterviewServer(deps) {
26703
26907
  // src/interview/service.ts
26704
26908
  import { spawn } from "node:child_process";
26705
26909
  import * as fs8 from "node:fs/promises";
26706
- import * as path10 from "node:path";
26910
+ import * as path11 from "node:path";
26707
26911
 
26708
26912
  // src/interview/types.ts
26709
26913
  import { z as z3 } from "zod";
@@ -26970,12 +27174,12 @@ function createInterviewService(ctx, config, deps) {
26970
27174
  if (!newSlug) {
26971
27175
  return;
26972
27176
  }
26973
- const currentFileName = path10.basename(interview.markdownPath, ".md");
27177
+ const currentFileName = path11.basename(interview.markdownPath, ".md");
26974
27178
  if (currentFileName === newSlug) {
26975
27179
  return;
26976
27180
  }
26977
- const dir = path10.dirname(interview.markdownPath);
26978
- const newPath = path10.join(dir, `${newSlug}.md`);
27181
+ const dir = path11.dirname(interview.markdownPath);
27182
+ const newPath = path11.join(dir, `${newSlug}.md`);
26979
27183
  try {
26980
27184
  await fs8.access(newPath);
26981
27185
  return;
@@ -27051,9 +27255,9 @@ function createInterviewService(ctx, config, deps) {
27051
27255
  const messages = await loadMessages(sessionID);
27052
27256
  const title = extractTitle(document);
27053
27257
  const record = {
27054
- id: `${Date.now()}-${++idCounter}-${slugify(path10.basename(markdownPath, ".md")) || "interview"}`,
27258
+ id: `${Date.now()}-${++idCounter}-${slugify(path11.basename(markdownPath, ".md")) || "interview"}`,
27055
27259
  sessionID,
27056
- idea: title || path10.basename(markdownPath, ".md"),
27260
+ idea: title || path11.basename(markdownPath, ".md"),
27057
27261
  markdownPath,
27058
27262
  createdAt: nowIso(),
27059
27263
  status: "active",
@@ -27280,7 +27484,7 @@ function createInterviewService(ctx, config, deps) {
27280
27484
  return fileCache.items;
27281
27485
  }
27282
27486
  const outputDir = createInterviewDirectoryPath(ctx.directory, outputFolder);
27283
- const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) => path10.resolve(i.markdownPath)));
27487
+ const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) => path11.resolve(i.markdownPath)));
27284
27488
  let entries;
27285
27489
  try {
27286
27490
  entries = await fs8.readdir(outputDir);
@@ -27291,8 +27495,8 @@ function createInterviewService(ctx, config, deps) {
27291
27495
  for (const entry of entries) {
27292
27496
  if (!entry.endsWith(".md"))
27293
27497
  continue;
27294
- const fullPath = path10.join(outputDir, entry);
27295
- if (activePaths.has(path10.resolve(fullPath)))
27498
+ const fullPath = path11.join(outputDir, entry);
27499
+ if (activePaths.has(path11.resolve(fullPath)))
27296
27500
  continue;
27297
27501
  let content;
27298
27502
  try {
@@ -27391,7 +27595,7 @@ function createInterviewManager(ctx, config) {
27391
27595
  const outputFolder = interviewConfig?.outputFolder ?? "interview";
27392
27596
  if (!dashboardEnabled) {
27393
27597
  const service2 = createInterviewService(ctx, interviewConfig);
27394
- const resolvedOutputPath = path11.join(ctx.directory, outputFolder);
27598
+ const resolvedOutputPath = path12.join(ctx.directory, outputFolder);
27395
27599
  const server = createInterviewServer({
27396
27600
  getState: async (interviewId) => service2.getInterviewState(interviewId),
27397
27601
  listInterviewFiles: async () => service2.listInterviewFiles(),
@@ -27496,7 +27700,7 @@ function createInterviewManager(ctx, config) {
27496
27700
  listInterviews: () => service.listInterviews(),
27497
27701
  submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers),
27498
27702
  handleNudgeAction: async (interviewId, action) => service.handleNudgeAction(interviewId, action),
27499
- outputFolder: path11.join(ctx.directory, outputFolder),
27703
+ outputFolder: path12.join(ctx.directory, outputFolder),
27500
27704
  port: 0
27501
27705
  });
27502
27706
  service.setBaseUrlResolver(() => perSessionServer.ensureStarted());
@@ -27936,23 +28140,23 @@ class TmuxMultiplexer {
27936
28140
  return null;
27937
28141
  }
27938
28142
  const stdout = await proc.stdout();
27939
- const path12 = stdout.trim().split(`
28143
+ const path13 = stdout.trim().split(`
27940
28144
  `)[0];
27941
- if (!path12) {
28145
+ if (!path13) {
27942
28146
  log("[tmux] findBinary: no path in output");
27943
28147
  return null;
27944
28148
  }
27945
- const verifyProc = crossSpawn([path12, "-V"], {
28149
+ const verifyProc = crossSpawn([path13, "-V"], {
27946
28150
  stdout: "pipe",
27947
28151
  stderr: "pipe"
27948
28152
  });
27949
28153
  const verifyExit = await verifyProc.exited;
27950
28154
  if (verifyExit !== 0) {
27951
- log("[tmux] findBinary: tmux -V failed", { path: path12, verifyExit });
28155
+ log("[tmux] findBinary: tmux -V failed", { path: path13, verifyExit });
27952
28156
  return null;
27953
28157
  }
27954
- log("[tmux] findBinary: found", { path: path12 });
27955
- return path12;
28158
+ log("[tmux] findBinary: found", { path: path13 });
28159
+ return path13;
27956
28160
  } catch (err) {
27957
28161
  log("[tmux] findBinary: exception", { error: String(err) });
27958
28162
  return null;
@@ -28787,9 +28991,9 @@ function findSgCliPathSync() {
28787
28991
  }
28788
28992
  if (process.platform === "darwin") {
28789
28993
  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;
28994
+ for (const path13 of homebrewPaths) {
28995
+ if (existsSync7(path13) && isValidBinary(path13)) {
28996
+ return path13;
28793
28997
  }
28794
28998
  }
28795
28999
  }
@@ -28806,8 +29010,8 @@ function getSgCliPath() {
28806
29010
  }
28807
29011
  return "sg";
28808
29012
  }
28809
- function setSgCliPath(path12) {
28810
- resolvedCliPath = path12;
29013
+ function setSgCliPath(path13) {
29014
+ resolvedCliPath = path13;
28811
29015
  }
28812
29016
  var DEFAULT_TIMEOUT_MS2 = 300000;
28813
29017
  var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
@@ -29375,14 +29579,14 @@ var BINARY_PREFIXES = [
29375
29579
  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
29580
  // src/tools/smartfetch/tool.ts
29377
29581
  import os4 from "node:os";
29378
- import path15 from "node:path";
29582
+ import path16 from "node:path";
29379
29583
  import {
29380
29584
  tool as tool4
29381
29585
  } from "@opencode-ai/plugin";
29382
29586
 
29383
29587
  // src/tools/smartfetch/binary.ts
29384
29588
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
29385
- import path12 from "node:path";
29589
+ import path13 from "node:path";
29386
29590
  function extensionForMime(contentType) {
29387
29591
  const mime = contentType.split(";")[0]?.trim().toLowerCase();
29388
29592
  const map = {
@@ -29403,10 +29607,10 @@ function buildBinaryResultMessage(fetchResult, savedPath) {
29403
29607
  async function saveBinary(binaryDir, data, contentType, filename) {
29404
29608
  await mkdir2(binaryDir, { recursive: true });
29405
29609
  const initialName = filename || `webfetch-${Date.now()}.${extensionForMime(contentType)}`;
29406
- const parsed = path12.parse(initialName);
29610
+ const parsed = path13.parse(initialName);
29407
29611
  for (let attempt = 0;attempt < 1000; attempt++) {
29408
29612
  const candidateName = attempt === 0 ? initialName : `${parsed.name}-${attempt}${parsed.ext || `.${extensionForMime(contentType)}`}`;
29409
- const file = path12.join(binaryDir, candidateName);
29613
+ const file = path13.join(binaryDir, candidateName);
29410
29614
  try {
29411
29615
  await writeFile2(file, data, { flag: "wx" });
29412
29616
  return file;
@@ -30060,7 +30264,7 @@ var L = class u2 {
30060
30264
  };
30061
30265
 
30062
30266
  // src/tools/smartfetch/network.ts
30063
- import path13 from "node:path";
30267
+ import path14 from "node:path";
30064
30268
 
30065
30269
  // src/tools/smartfetch/utils.ts
30066
30270
  var import_readability = __toESM(require_readability(), 1);
@@ -30785,7 +30989,7 @@ function inferFilenameFromUrl(url) {
30785
30989
  function truncateFilename(name, maxLength = 180) {
30786
30990
  if (name.length <= maxLength)
30787
30991
  return name;
30788
- const parsed = path13.parse(name);
30992
+ const parsed = path14.parse(name);
30789
30993
  const ext = parsed.ext || "";
30790
30994
  const baseLimit = Math.max(1, maxLength - ext.length);
30791
30995
  return `${parsed.name.slice(0, baseLimit)}${ext}`;
@@ -30957,7 +31161,7 @@ function isInvalidLlmsResult(fetchResult) {
30957
31161
  // src/tools/smartfetch/secondary-model.ts
30958
31162
  import { existsSync as existsSync9 } from "node:fs";
30959
31163
  import { readFile as readFile4 } from "node:fs/promises";
30960
- import path14 from "node:path";
31164
+ import path15 from "node:path";
30961
31165
  function parseModelRef(value) {
30962
31166
  if (!value)
30963
31167
  return;
@@ -30983,7 +31187,7 @@ function pickAgentModelRef(value) {
30983
31187
  }
30984
31188
  function findPreferredOpenCodeConfigPath(baseDir) {
30985
31189
  for (const file of ["opencode.jsonc", "opencode.json"]) {
30986
- const fullPath = path14.join(baseDir, file);
31190
+ const fullPath = path15.join(baseDir, file);
30987
31191
  if (existsSync9(fullPath))
30988
31192
  return fullPath;
30989
31193
  }
@@ -31000,7 +31204,7 @@ async function readOpenCodeConfigFile(configPath) {
31000
31204
  }
31001
31205
  }
31002
31206
  async function readEffectiveOpenCodeConfig(directory) {
31003
- const projectDir = path14.join(directory, ".opencode");
31207
+ const projectDir = path15.join(directory, ".opencode");
31004
31208
  const userDirs = getConfigSearchDirs();
31005
31209
  const projectPath = findPreferredOpenCodeConfigPath(projectDir);
31006
31210
  const userPath = userDirs.map((configDir) => findPreferredOpenCodeConfigPath(configDir)).find(Boolean);
@@ -31161,7 +31365,7 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
31161
31365
  // src/tools/smartfetch/tool.ts
31162
31366
  var z5 = tool4.schema;
31163
31367
  function createWebfetchTool(pluginCtx, options = {}) {
31164
- const binaryDir = options.binaryDir || path15.join(os4.tmpdir(), "opencode-smartfetch");
31368
+ const binaryDir = options.binaryDir || path16.join(os4.tmpdir(), "opencode-smartfetch");
31165
31369
  return tool4({
31166
31370
  description: WEBFETCH_DESCRIPTION,
31167
31371
  args: {
@@ -31685,6 +31889,16 @@ class SubagentDepthTracker {
31685
31889
 
31686
31890
  // src/utils/system-collapse.ts
31687
31891
  function collapseSystemInPlace(system2) {
31892
+ if (system2.length === 0) {
31893
+ return;
31894
+ }
31895
+ if (system2.length === 1) {
31896
+ if (system2[0]) {
31897
+ return;
31898
+ }
31899
+ system2.length = 0;
31900
+ return;
31901
+ }
31688
31902
  const joined = system2.join(`
31689
31903
 
31690
31904
  `);
@@ -31723,6 +31937,7 @@ var OhMyOpenCodeLite = async (ctx) => {
31723
31937
  const sessionId = new Date().toISOString().replace(/[-:]/g, "").slice(0, 15);
31724
31938
  initLogger(sessionId);
31725
31939
  let config;
31940
+ let disabledAgents;
31726
31941
  let agentDefs;
31727
31942
  let agents;
31728
31943
  let mcps;
@@ -31748,9 +31963,12 @@ var OhMyOpenCodeLite = async (ctx) => {
31748
31963
  let presetManager;
31749
31964
  let councilTools;
31750
31965
  let webfetch;
31966
+ let rewriteDisplayNameMentions;
31751
31967
  let toolCount = 0;
31752
31968
  try {
31753
31969
  config = loadPluginConfig(ctx.directory);
31970
+ disabledAgents = getDisabledAgents(config);
31971
+ rewriteDisplayNameMentions = createDisplayNameMentionRewriter(config);
31754
31972
  agentDefs = createAgents(config);
31755
31973
  agents = getAgentConfigs(config);
31756
31974
  modelArrayMap = {};
@@ -31787,7 +32005,7 @@ var OhMyOpenCodeLite = async (ctx) => {
31787
32005
  main_pane_size: config.multiplexer?.main_pane_size ?? 60
31788
32006
  };
31789
32007
  const multiplexer = getMultiplexer(multiplexerConfig);
31790
- multiplexerEnabled = multiplexerConfig.type !== "none" && multiplexer !== null;
32008
+ multiplexerEnabled = multiplexerConfig.type !== "none" && multiplexer !== null && multiplexer.isInsideSession();
31791
32009
  log("[plugin] initialized with multiplexer config", {
31792
32010
  multiplexerConfig,
31793
32011
  enabled: multiplexerEnabled,
@@ -31824,6 +32042,8 @@ var OhMyOpenCodeLite = async (ctx) => {
31824
32042
  });
31825
32043
  taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
31826
32044
  maxSessionsPerAgent: config.sessionManager?.maxSessionsPerAgent ?? 2,
32045
+ readContextMinLines: config.sessionManager?.readContextMinLines ?? 10,
32046
+ readContextMaxFiles: config.sessionManager?.readContextMaxFiles ?? 8,
31827
32047
  shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
31828
32048
  });
31829
32049
  interviewManager = createInterviewManager(ctx, config);
@@ -32043,7 +32263,7 @@ var OhMyOpenCodeLite = async (ctx) => {
32043
32263
  const alreadyInjected = output.system.some((s) => typeof s === "string" && s.includes("<Role>") && s.includes("orchestrator"));
32044
32264
  if (!alreadyInjected) {
32045
32265
  const orchestratorDef = agentDefs.find((a) => a.name === "orchestrator");
32046
- const orchestratorPrompt = typeof orchestratorDef?.config?.prompt === "string" ? orchestratorDef.config.prompt : buildOrchestratorPrompt(getDisabledAgents(config));
32266
+ const orchestratorPrompt = typeof orchestratorDef?.config?.prompt === "string" ? orchestratorDef.config.prompt : buildOrchestratorPrompt(disabledAgents);
32047
32267
  output.system[0] = orchestratorPrompt + (output.system[0] ? `
32048
32268
 
32049
32269
  ${output.system[0]}` : "");
@@ -32064,13 +32284,13 @@ ${output.system[0]}` : "");
32064
32284
  if (part.type !== "text" || typeof part.text !== "string") {
32065
32285
  continue;
32066
32286
  }
32067
- part.text = rewriteDisplayNameMentions(config, part.text);
32287
+ part.text = rewriteDisplayNameMentions(part.text);
32068
32288
  }
32069
32289
  }
32070
32290
  processImageAttachments({
32071
32291
  messages: typedOutput.messages,
32072
32292
  workDir: ctx.directory,
32073
- disabledAgents: getDisabledAgents(config),
32293
+ disabledAgents,
32074
32294
  log
32075
32295
  });
32076
32296
  await todoContinuationHook.handleMessagesTransform({
@@ -36,6 +36,8 @@ export declare function resolveAgentVariant(config: PluginConfig | undefined, ag
36
36
  * - displayName aliases (e.g. "advisor" -> "oracle")
37
37
  */
38
38
  export declare function resolveRuntimeAgentName(config: PluginConfig | undefined, agentName: string): string;
39
+ export type DisplayNameMentionRewriter = (text: string) => string;
40
+ export declare function createDisplayNameMentionRewriter(config: PluginConfig | undefined): DisplayNameMentionRewriter;
39
41
  /**
40
42
  * Rewrites user-facing display-name mentions (e.g. @advisor) into internal
41
43
  * agent mentions (e.g. @oracle) for runtime routing.
@@ -2,5 +2,7 @@ declare function getLogDir(): string;
2
2
  export declare function initLogger(sessionId: string): void;
3
3
  /** @internal Reset logger state for testing */
4
4
  export declare function resetLogger(): void;
5
+ /** @internal Wait for queued log writes in tests. */
6
+ export declare function flushLoggerForTesting(): Promise<void>;
5
7
  export { getLogDir };
6
8
  export declare function log(message: string, data?: unknown): void;
@@ -1,12 +1,23 @@
1
1
  import type { AgentName } from '../config';
2
+ export interface ContextFile {
3
+ path: string;
4
+ lineCount: number;
5
+ lineNumbers?: number[];
6
+ lastReadAt: number;
7
+ }
2
8
  export interface RememberedTaskSession {
3
9
  alias: string;
4
10
  taskId: string;
5
11
  agentType: AgentName;
6
12
  label: string;
13
+ contextFiles: ContextFile[];
7
14
  createdAt: number;
8
15
  lastUsedAt: number;
9
16
  }
17
+ interface SessionManagerOptions {
18
+ readContextMinLines?: number;
19
+ readContextMaxFiles?: number;
20
+ }
10
21
  export declare function deriveTaskSessionLabel(input: {
11
22
  description?: string;
12
23
  prompt?: string;
@@ -14,10 +25,12 @@ export declare function deriveTaskSessionLabel(input: {
14
25
  }): string;
15
26
  export declare class SessionManager {
16
27
  private readonly maxSessionsPerAgent;
28
+ private readonly readContextMinLines;
29
+ private readonly readContextMaxFiles;
17
30
  private readonly sessionsByParent;
18
31
  private readonly nextAliasIndexByParent;
19
32
  private orderCounter;
20
- constructor(maxSessionsPerAgent: number);
33
+ constructor(maxSessionsPerAgent: number, options?: SessionManagerOptions);
21
34
  remember(input: {
22
35
  parentSessionId: string;
23
36
  taskId: string;
@@ -28,11 +41,15 @@ export declare class SessionManager {
28
41
  resolve(parentSessionId: string, agentType: AgentName, key: string): RememberedTaskSession | undefined;
29
42
  drop(parentSessionId: string, agentType: AgentName, key: string): void;
30
43
  dropTask(taskId: string): void;
44
+ taskIds(): Set<string>;
45
+ addContext(taskId: string, files: ContextFile[]): void;
31
46
  clearParent(parentSessionId: string): void;
32
47
  formatForPrompt(parentSessionId: string): string | undefined;
33
48
  private getAgentGroup;
34
49
  private setAgentGroup;
35
50
  private nextAlias;
36
51
  private trimGroup;
52
+ private trimContextFiles;
37
53
  private nextOrder;
38
54
  }
55
+ export {};
@@ -498,6 +498,18 @@
498
498
  "type": "integer",
499
499
  "minimum": 1,
500
500
  "maximum": 10
501
+ },
502
+ "readContextMinLines": {
503
+ "default": 10,
504
+ "type": "integer",
505
+ "minimum": 0,
506
+ "maximum": 1000
507
+ },
508
+ "readContextMaxFiles": {
509
+ "default": 8,
510
+ "type": "integer",
511
+ "minimum": 0,
512
+ "maximum": 50
501
513
  }
502
514
  }
503
515
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode-slim",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Lightweight agent orchestration plugin for OpenCode - a slimmed-down fork of oh-my-opencode",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",