oh-my-opencode-slim 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -18302,7 +18302,7 @@ var ALL_AGENT_NAMES = [ORCHESTRATOR_NAME, ...SUBAGENT_NAMES];
18302
18302
  var PROTECTED_AGENTS = new Set(["orchestrator", "councillor"]);
18303
18303
  var DEFAULT_MODELS = {
18304
18304
  orchestrator: undefined,
18305
- oracle: "openai/gpt-5.4",
18305
+ oracle: "openai/gpt-5.5",
18306
18306
  librarian: "openai/gpt-5.4-mini",
18307
18307
  explorer: "openai/gpt-5.4-mini",
18308
18308
  designer: "openai/gpt-5.4-mini",
@@ -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 agents delegation rules → don't prefer solo → execute → verify.
18319
+ Understand → choose the best parallelized path based on your capabilities and agents delegation 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;
@@ -18378,13 +18378,15 @@ var CouncilConfigSchema = z.object({
18378
18378
  deprecated.push("master_timeout");
18379
18379
  if (data.master_fallback !== undefined)
18380
18380
  deprecated.push("master_fallback");
18381
+ const legacyMasterModel = typeof data.master === "object" && data.master !== null && "model" in data.master && typeof data.master.model === "string" ? data.master.model : undefined;
18381
18382
  return {
18382
18383
  presets: data.presets,
18383
18384
  timeout: data.timeout,
18384
18385
  default_preset: data.default_preset,
18385
18386
  councillor_execution_mode: data.councillor_execution_mode,
18386
18387
  councillor_retries: data.councillor_retries,
18387
- _deprecated: deprecated.length > 0 ? deprecated : undefined
18388
+ _deprecated: deprecated.length > 0 ? deprecated : undefined,
18389
+ _legacyMasterModel: legacyMasterModel
18388
18390
  };
18389
18391
  });
18390
18392
  // src/config/loader.ts
@@ -18523,6 +18525,9 @@ var InterviewConfigSchema = z2.object({
18523
18525
  port: z2.number().int().min(0).max(65535).default(0),
18524
18526
  dashboard: z2.boolean().default(false)
18525
18527
  });
18528
+ var SessionManagerConfigSchema = z2.object({
18529
+ maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2)
18530
+ });
18526
18531
  var TodoContinuationConfigSchema = z2.object({
18527
18532
  maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
18528
18533
  cooldownMs: z2.number().int().min(0).max(30000).default(3000).describe("Delay in ms before auto-continuing (gives user time to abort)"),
@@ -18564,6 +18569,7 @@ var PluginConfigSchema = z2.object({
18564
18569
  scoringEngineVersion: z2.enum(["v1", "v2-shadow", "v2"]).optional(),
18565
18570
  balanceProviderUsage: z2.boolean().optional(),
18566
18571
  showStartupToast: z2.boolean().optional().describe("Show the startup activation toast when OpenCode starts. Defaults to true."),
18572
+ autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
18567
18573
  manualPlan: ManualPlanSchema.optional(),
18568
18574
  presets: z2.record(z2.string(), PresetSchema).optional(),
18569
18575
  agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
@@ -18573,6 +18579,7 @@ var PluginConfigSchema = z2.object({
18573
18579
  tmux: TmuxConfigSchema.optional(),
18574
18580
  websearch: WebsearchConfigSchema.optional(),
18575
18581
  interview: InterviewConfigSchema.optional(),
18582
+ sessionManager: SessionManagerConfigSchema.optional(),
18576
18583
  todoContinuation: TodoContinuationConfigSchema.optional(),
18577
18584
  fallback: FailoverConfigSchema.optional(),
18578
18585
  council: CouncilConfigSchema.optional()
@@ -18658,6 +18665,7 @@ function loadPluginConfig(directory) {
18658
18665
  tmux: deepMerge(config.tmux, projectConfig.tmux),
18659
18666
  multiplexer: deepMerge(config.multiplexer, projectConfig.multiplexer),
18660
18667
  interview: deepMerge(config.interview, projectConfig.interview),
18668
+ sessionManager: deepMerge(config.sessionManager, projectConfig.sessionManager),
18661
18669
  fallback: deepMerge(config.fallback, projectConfig.fallback),
18662
18670
  council: deepMerge(config.council, projectConfig.council)
18663
18671
  };
@@ -18929,6 +18937,12 @@ ${enabledParallelExamples}
18929
18937
 
18930
18938
  Balance: respect dependencies, avoid parallelizing what must be sequential.
18931
18939
 
18940
+ ### OpenCode subagent execution model
18941
+ - A delegated specialist runs in a separate child session.
18942
+ - Delegation is blocking for the parent at that point: send work out, then continue that line after results return.
18943
+ - Parallel delegation means launching multiple independent child-session branches.
18944
+ - Only parallelize branches that are truly independent; reconcile dependent steps after delegated results come back.
18945
+
18932
18946
  ## 5. Execute
18933
18947
  1. Break complex tasks into todos
18934
18948
  2. Fire parallel research/implementation
@@ -18936,6 +18950,12 @@ Balance: respect dependencies, avoid parallelizing what must be sequential.
18936
18950
  4. Integrate results
18937
18951
  5. Adjust if needed
18938
18952
 
18953
+ ### 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.
18956
+ - If multiple remembered sessions fit, prefer the most recently used matching session.
18957
+ - If reuse is unclear, start a fresh session.
18958
+
18939
18959
  ### Auto-Continue
18940
18960
  When working through multi-step tasks, consider enabling auto-continue to avoid stopping between batches:
18941
18961
  - **Enable when:** User requests autonomous/batch work, or you create 4+ todos in a session
@@ -19630,6 +19650,13 @@ function createAgents(config) {
19630
19650
  applyDefaultPermissions(agent, override?.skills);
19631
19651
  return agent;
19632
19652
  });
19653
+ const legacyMasterModel = config?.council?._legacyMasterModel;
19654
+ if (legacyMasterModel) {
19655
+ const councilAgent = builtInSubAgents.find((a) => a.name === "council");
19656
+ if (councilAgent && !getAgentOverride(config, "council")?.model && councilAgent.config.model === DEFAULT_MODELS.council) {
19657
+ councilAgent.config.model = legacyMasterModel;
19658
+ }
19659
+ }
19633
19660
  const customSubAgents = protoCustomAgents.map((agent) => {
19634
19661
  const override = getAgentOverride(config, agent.name);
19635
19662
  if (override) {
@@ -19819,17 +19846,22 @@ class CouncilManager {
19819
19846
  depthTracker;
19820
19847
  tmuxEnabled;
19821
19848
  deprecatedFields;
19849
+ legacyMasterModel;
19822
19850
  constructor(ctx, config, depthTracker, tmuxEnabled = false) {
19823
19851
  this.client = ctx.client;
19824
19852
  this.directory = ctx.directory;
19825
19853
  this.config = config;
19826
19854
  this.deprecatedFields = config?.council?._deprecated;
19855
+ this.legacyMasterModel = config?.council?._legacyMasterModel;
19827
19856
  this.depthTracker = depthTracker;
19828
19857
  this.tmuxEnabled = tmuxEnabled;
19829
19858
  }
19830
19859
  getDeprecatedFields() {
19831
19860
  return this.deprecatedFields;
19832
19861
  }
19862
+ getLegacyMasterModel() {
19863
+ return this.legacyMasterModel;
19864
+ }
19833
19865
  async runCouncil(prompt, presetName, parentSessionId) {
19834
19866
  if (this.depthTracker) {
19835
19867
  const parentDepth = this.depthTracker.getDepth(parentSessionId);
@@ -22043,6 +22075,185 @@ function hasInternalInitiatorMarker(part) {
22043
22075
  }
22044
22076
  return part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER);
22045
22077
  }
22078
+ // src/utils/session-manager.ts
22079
+ function aliasPrefix(agentType) {
22080
+ switch (agentType) {
22081
+ case "explorer":
22082
+ return "exp";
22083
+ case "librarian":
22084
+ return "lib";
22085
+ case "oracle":
22086
+ return "ora";
22087
+ case "designer":
22088
+ return "des";
22089
+ case "fixer":
22090
+ return "fix";
22091
+ case "observer":
22092
+ return "obs";
22093
+ case "council":
22094
+ return "cnc";
22095
+ case "councillor":
22096
+ return "clr";
22097
+ case "orchestrator":
22098
+ return "orc";
22099
+ }
22100
+ }
22101
+ function normalizeWhitespace(value) {
22102
+ return value.replace(/\s+/g, " ").trim();
22103
+ }
22104
+ function deriveTaskSessionLabel(input) {
22105
+ const preferred = normalizeWhitespace(input.description ?? "");
22106
+ if (preferred) {
22107
+ return preferred.slice(0, 48);
22108
+ }
22109
+ const firstPromptLine = (input.prompt ?? "").split(/\r?\n/).map((line) => normalizeWhitespace(line)).find(Boolean);
22110
+ if (firstPromptLine) {
22111
+ return firstPromptLine.slice(0, 48);
22112
+ }
22113
+ return `recent ${input.agentType} task`;
22114
+ }
22115
+
22116
+ class SessionManager {
22117
+ maxSessionsPerAgent;
22118
+ sessionsByParent = new Map;
22119
+ nextAliasIndexByParent = new Map;
22120
+ orderCounter = 0;
22121
+ constructor(maxSessionsPerAgent) {
22122
+ this.maxSessionsPerAgent = maxSessionsPerAgent;
22123
+ }
22124
+ remember(input) {
22125
+ const now = this.nextOrder();
22126
+ const group = this.getAgentGroup(input.parentSessionId, input.agentType, true);
22127
+ if (!group) {
22128
+ throw new Error("Failed to initialize session group");
22129
+ }
22130
+ const existing = group.find((entry) => entry.taskId === input.taskId);
22131
+ if (existing) {
22132
+ existing.label = input.label;
22133
+ existing.lastUsedAt = this.nextOrder();
22134
+ return existing;
22135
+ }
22136
+ const remembered = {
22137
+ alias: this.nextAlias(input.parentSessionId, input.agentType),
22138
+ taskId: input.taskId,
22139
+ agentType: input.agentType,
22140
+ label: input.label,
22141
+ createdAt: now,
22142
+ lastUsedAt: now
22143
+ };
22144
+ group.push(remembered);
22145
+ this.trimGroup(group);
22146
+ return remembered;
22147
+ }
22148
+ markUsed(parentSessionId, agentType, key) {
22149
+ const group = this.getAgentGroup(parentSessionId, agentType, false);
22150
+ const match = group?.find((entry) => entry.alias === key || entry.taskId === key);
22151
+ if (match) {
22152
+ match.lastUsedAt = this.nextOrder();
22153
+ }
22154
+ }
22155
+ resolve(parentSessionId, agentType, key) {
22156
+ const group = this.getAgentGroup(parentSessionId, agentType, false);
22157
+ return group?.find((entry) => entry.alias === key || entry.taskId === key);
22158
+ }
22159
+ drop(parentSessionId, agentType, key) {
22160
+ const group = this.getAgentGroup(parentSessionId, agentType, false);
22161
+ if (!group)
22162
+ return;
22163
+ const next = group.filter((entry) => entry.alias !== key && entry.taskId !== key);
22164
+ this.setAgentGroup(parentSessionId, agentType, next);
22165
+ }
22166
+ dropTask(taskId) {
22167
+ for (const [parentSessionId, groups] of this.sessionsByParent.entries()) {
22168
+ for (const [agentType, group] of groups.entries()) {
22169
+ const next = group.filter((entry) => entry.taskId !== taskId);
22170
+ this.setAgentGroup(parentSessionId, agentType, next);
22171
+ }
22172
+ }
22173
+ }
22174
+ clearParent(parentSessionId) {
22175
+ this.sessionsByParent.delete(parentSessionId);
22176
+ this.nextAliasIndexByParent.delete(parentSessionId);
22177
+ }
22178
+ formatForPrompt(parentSessionId) {
22179
+ const groups = this.sessionsByParent.get(parentSessionId);
22180
+ if (!groups || groups.size === 0)
22181
+ return;
22182
+ const lines = [...groups.entries()].map(([agentType, entries]) => [
22183
+ agentType,
22184
+ [...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("; ")}`);
22186
+ if (lines.length === 0)
22187
+ return;
22188
+ return [
22189
+ "### Resumable Sessions",
22190
+ "Reuse only for clear continuation of the same thread. Otherwise start fresh.",
22191
+ "",
22192
+ ...lines
22193
+ ].join(`
22194
+ `);
22195
+ }
22196
+ getAgentGroup(parentSessionId, agentType, create) {
22197
+ let groups = this.sessionsByParent.get(parentSessionId);
22198
+ if (!groups && create) {
22199
+ groups = new Map;
22200
+ this.sessionsByParent.set(parentSessionId, groups);
22201
+ }
22202
+ let group = groups?.get(agentType);
22203
+ if (!group && create && groups) {
22204
+ group = [];
22205
+ groups.set(agentType, group);
22206
+ }
22207
+ return group;
22208
+ }
22209
+ setAgentGroup(parentSessionId, agentType, entries) {
22210
+ const groups = this.sessionsByParent.get(parentSessionId);
22211
+ if (!groups)
22212
+ return;
22213
+ if (entries.length === 0) {
22214
+ groups.delete(agentType);
22215
+ if (groups.size === 0) {
22216
+ this.sessionsByParent.delete(parentSessionId);
22217
+ this.nextAliasIndexByParent.delete(parentSessionId);
22218
+ }
22219
+ return;
22220
+ }
22221
+ groups.set(agentType, entries);
22222
+ }
22223
+ nextAlias(parentSessionId, agentType) {
22224
+ let counters = this.nextAliasIndexByParent.get(parentSessionId);
22225
+ if (!counters) {
22226
+ counters = new Map;
22227
+ this.nextAliasIndexByParent.set(parentSessionId, counters);
22228
+ }
22229
+ const next = (counters.get(agentType) ?? 0) + 1;
22230
+ counters.set(agentType, next);
22231
+ return `${aliasPrefix(agentType)}-${next}`;
22232
+ }
22233
+ trimGroup(group) {
22234
+ group.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
22235
+ if (group.length > this.maxSessionsPerAgent) {
22236
+ group.length = this.maxSessionsPerAgent;
22237
+ }
22238
+ }
22239
+ nextOrder() {
22240
+ this.orderCounter += 1;
22241
+ return this.orderCounter;
22242
+ }
22243
+ }
22244
+ // src/utils/task.ts
22245
+ function parseTaskIdFromTaskOutput(output) {
22246
+ const lines = output.split(/\r?\n/);
22247
+ for (const line of lines) {
22248
+ const trimmed = line.trim();
22249
+ const match = /^task_id:\s*([^\s()]+)(?:\s*\(.*)?$/.exec(trimmed);
22250
+ if (!match) {
22251
+ continue;
22252
+ }
22253
+ return match[1];
22254
+ }
22255
+ return;
22256
+ }
22046
22257
  // src/utils/zip-extractor.ts
22047
22258
  import { spawnSync } from "node:child_process";
22048
22259
  import { release } from "node:os";
@@ -22358,6 +22569,8 @@ var RATE_LIMIT_PATTERNS = [
22358
22569
  /too many requests/i,
22359
22570
  /quota.?exceeded/i,
22360
22571
  /usage.?exceeded/i,
22572
+ /ExceededBudget/i,
22573
+ /over.?budget/i,
22361
22574
  /usage limit/i,
22362
22575
  /overloaded/i,
22363
22576
  /resource.?exhausted/i,
@@ -22436,7 +22649,7 @@ class ForegroundFallbackManager {
22436
22649
  if (!props?.sessionID || props.status?.type !== "retry")
22437
22650
  break;
22438
22651
  const msg = props.status.message?.toLowerCase() ?? "";
22439
- if (msg.includes("rate limit") || msg.includes("usage limit") || msg.includes("usage exceeded") || msg.includes("quota exceeded") || msg.includes("high concurrency") || msg.includes("reduce concurrency")) {
22652
+ if (msg.includes("rate limit") || msg.includes("usage limit") || msg.includes("usage exceeded") || msg.includes("quota exceeded") || msg.includes("exceededbudget") || msg.includes("over budget") || msg.includes("high concurrency") || msg.includes("reduce concurrency")) {
22440
22653
  await this.tryFallback(props.sessionID);
22441
22654
  }
22442
22655
  break;
@@ -22613,31 +22826,51 @@ function extFromMime(mime) {
22613
22826
  function sanitizeFilename(name) {
22614
22827
  return name.replace(/[^a-zA-Z0-9._-]/g, "_");
22615
22828
  }
22616
- function cleanupOldImages(dir, saveDir) {
22829
+ function cleanupAllSessions(saveDir) {
22617
22830
  const now = Date.now();
22618
- if (!lastCleanupByDir.has(dir) && existsSync4(dir)) {
22619
- lastCleanupByDir.set(dir, now);
22620
- }
22621
- const lastCleanup = lastCleanupByDir.get(dir) ?? 0;
22831
+ const lastCleanup = lastCleanupByDir.get(saveDir) ?? 0;
22622
22832
  if (now - lastCleanup < CLEANUP_INTERVAL)
22623
22833
  return;
22624
- lastCleanupByDir.set(dir, now);
22834
+ lastCleanupByDir.set(saveDir, now);
22835
+ const maxAge = 60 * 60 * 1000;
22836
+ const dirsToScan = [];
22625
22837
  try {
22626
- const maxAge = 60 * 60 * 1000;
22627
- for (const f of readdirSync2(dir)) {
22628
- const fp = join7(dir, f);
22629
- try {
22630
- if (now - statSync3(fp).mtimeMs > maxAge)
22631
- unlinkSync2(fp);
22632
- } catch {}
22633
- }
22634
- if (dir !== saveDir) {
22635
- try {
22636
- rmdirSync(dir);
22637
- lastCleanupByDir.delete(dir);
22638
- } catch {}
22838
+ for (const entry of readdirSync2(saveDir, { withFileTypes: true })) {
22839
+ const fp = join7(saveDir, entry.name);
22840
+ if (entry.isDirectory()) {
22841
+ dirsToScan.push(fp);
22842
+ } else {
22843
+ try {
22844
+ if (now - statSync3(fp).mtimeMs > maxAge)
22845
+ unlinkSync2(fp);
22846
+ } catch {}
22847
+ }
22639
22848
  }
22640
22849
  } catch {}
22850
+ for (const dir of dirsToScan) {
22851
+ try {
22852
+ let isEmpty = true;
22853
+ let allRemoved = true;
22854
+ for (const f of readdirSync2(dir)) {
22855
+ isEmpty = false;
22856
+ const fp = join7(dir, f);
22857
+ try {
22858
+ if (now - statSync3(fp).mtimeMs > maxAge) {
22859
+ unlinkSync2(fp);
22860
+ } else {
22861
+ allRemoved = false;
22862
+ }
22863
+ } catch {
22864
+ allRemoved = false;
22865
+ }
22866
+ }
22867
+ if (!isEmpty && allRemoved) {
22868
+ try {
22869
+ rmdirSync(dir);
22870
+ } catch {}
22871
+ }
22872
+ } catch {}
22873
+ }
22641
22874
  }
22642
22875
  function writeUniqueFile(dir, name, data, log2) {
22643
22876
  const ext = extname(name);
@@ -22680,6 +22913,7 @@ function processImageAttachments(args) {
22680
22913
  } catch (e) {
22681
22914
  log2(`[image-hook] failed to create image directory: ${e}`);
22682
22915
  }
22916
+ cleanupAllSessions(saveDir);
22683
22917
  for (const msg of messages) {
22684
22918
  if (msg.info.role !== "user")
22685
22919
  continue;
@@ -22693,7 +22927,6 @@ function processImageAttachments(args) {
22693
22927
  } catch (e) {
22694
22928
  log2(`[image-hook] failed to create target image directory: ${e}`);
22695
22929
  }
22696
- cleanupOldImages(targetDir, saveDir);
22697
22930
  const savedPaths = [];
22698
22931
  for (const p of imageParts) {
22699
22932
  const url = p.url;
@@ -22846,6 +23079,154 @@ function createPostFileToolNudgeHook(options = {}) {
22846
23079
  }
22847
23080
  };
22848
23081
  }
23082
+ // src/hooks/task-session-manager/index.ts
23083
+ var AGENT_NAME_SET = new Set([
23084
+ "orchestrator",
23085
+ "oracle",
23086
+ "designer",
23087
+ "explorer",
23088
+ "librarian",
23089
+ "fixer",
23090
+ "observer",
23091
+ "council",
23092
+ "councillor"
23093
+ ]);
23094
+ var MAX_PENDING_TASK_CALLS = 100;
23095
+ function isAgentName(value) {
23096
+ return typeof value === "string" && AGENT_NAME_SET.has(value);
23097
+ }
23098
+ function isObjectRecord(value) {
23099
+ return typeof value === "object" && value !== null;
23100
+ }
23101
+ function createTaskSessionManagerHook(_ctx, options) {
23102
+ const sessionManager = new SessionManager(options.maxSessionsPerAgent);
23103
+ const pendingCalls = new Map;
23104
+ const pendingCallOrder = [];
23105
+ function isMissingRememberedSessionError(output) {
23106
+ const firstLine = output.split(/\r?\n/, 1)[0]?.trim().toLowerCase() ?? "";
23107
+ return firstLine.startsWith("[error]") && firstLine.includes("session") && (firstLine.includes("not found") || firstLine.includes("no session"));
23108
+ }
23109
+ function rememberPendingCall(call) {
23110
+ const existingIndex = pendingCallOrder.indexOf(call.callId);
23111
+ if (existingIndex >= 0) {
23112
+ pendingCallOrder.splice(existingIndex, 1);
23113
+ }
23114
+ pendingCalls.set(call.callId, call);
23115
+ pendingCallOrder.push(call.callId);
23116
+ while (pendingCallOrder.length > MAX_PENDING_TASK_CALLS) {
23117
+ const evictedCallId = pendingCallOrder.shift();
23118
+ if (!evictedCallId) {
23119
+ break;
23120
+ }
23121
+ pendingCalls.delete(evictedCallId);
23122
+ }
23123
+ }
23124
+ function takePendingCall(callId) {
23125
+ if (!callId)
23126
+ return;
23127
+ const pending = pendingCalls.get(callId);
23128
+ pendingCalls.delete(callId);
23129
+ const orderIndex = pendingCallOrder.indexOf(callId);
23130
+ if (orderIndex >= 0) {
23131
+ pendingCallOrder.splice(orderIndex, 1);
23132
+ }
23133
+ return pending;
23134
+ }
23135
+ return {
23136
+ "tool.execute.before": async (input, output) => {
23137
+ if (input.tool.toLowerCase() !== "task")
23138
+ return;
23139
+ if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
23140
+ return;
23141
+ }
23142
+ if (!isObjectRecord(output.args))
23143
+ return;
23144
+ const args = output.args;
23145
+ if (!isAgentName(args.subagent_type))
23146
+ return;
23147
+ const label = deriveTaskSessionLabel({
23148
+ description: typeof args.description === "string" ? args.description : undefined,
23149
+ prompt: typeof args.prompt === "string" ? args.prompt : undefined,
23150
+ agentType: args.subagent_type
23151
+ });
23152
+ if (input.callID) {
23153
+ rememberPendingCall({
23154
+ callId: input.callID,
23155
+ parentSessionId: input.sessionID,
23156
+ agentType: args.subagent_type,
23157
+ label
23158
+ });
23159
+ }
23160
+ if (typeof args.task_id !== "string" || args.task_id.trim() === "") {
23161
+ return;
23162
+ }
23163
+ const requested = args.task_id.trim();
23164
+ const remembered = sessionManager.resolve(input.sessionID, args.subagent_type, requested);
23165
+ if (!remembered) {
23166
+ delete args.task_id;
23167
+ return;
23168
+ }
23169
+ args.task_id = remembered.taskId;
23170
+ sessionManager.markUsed(input.sessionID, args.subagent_type, remembered.taskId);
23171
+ if (input.callID) {
23172
+ rememberPendingCall({
23173
+ callId: input.callID,
23174
+ parentSessionId: input.sessionID,
23175
+ agentType: args.subagent_type,
23176
+ label,
23177
+ resumedTaskId: remembered.taskId
23178
+ });
23179
+ }
23180
+ },
23181
+ "tool.execute.after": async (input, output) => {
23182
+ if (input.tool.toLowerCase() !== "task")
23183
+ return;
23184
+ const pending = takePendingCall(input.callID);
23185
+ if (!pending || typeof output.output !== "string")
23186
+ return;
23187
+ const taskId = parseTaskIdFromTaskOutput(output.output);
23188
+ if (!taskId) {
23189
+ if (pending.resumedTaskId && isMissingRememberedSessionError(output.output)) {
23190
+ sessionManager.drop(pending.parentSessionId, pending.agentType, pending.resumedTaskId);
23191
+ }
23192
+ return;
23193
+ }
23194
+ if (pending.resumedTaskId && pending.resumedTaskId !== taskId) {
23195
+ sessionManager.drop(pending.parentSessionId, pending.agentType, pending.resumedTaskId);
23196
+ }
23197
+ sessionManager.remember({
23198
+ parentSessionId: pending.parentSessionId,
23199
+ taskId,
23200
+ agentType: pending.agentType,
23201
+ label: pending.label
23202
+ });
23203
+ },
23204
+ "experimental.chat.system.transform": async (input, output) => {
23205
+ if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
23206
+ return;
23207
+ }
23208
+ const reminder = sessionManager.formatForPrompt(input.sessionID);
23209
+ if (!reminder)
23210
+ return;
23211
+ output.system.push(reminder);
23212
+ },
23213
+ event: async (input) => {
23214
+ if (input.event.type !== "session.deleted")
23215
+ return;
23216
+ const sessionId = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
23217
+ if (!sessionId)
23218
+ return;
23219
+ sessionManager.clearParent(sessionId);
23220
+ sessionManager.dropTask(sessionId);
23221
+ for (const [callId, pending] of pendingCalls.entries()) {
23222
+ if (pending.parentSessionId !== sessionId) {
23223
+ continue;
23224
+ }
23225
+ takePendingCall(callId);
23226
+ }
23227
+ }
23228
+ };
23229
+ }
22849
23230
  // src/hooks/todo-continuation/index.ts
22850
23231
  import { tool } from "@opencode-ai/plugin/tool";
22851
23232
 
@@ -23730,6 +24111,7 @@ async function readJsonBody(request) {
23730
24111
  }
23731
24112
 
23732
24113
  // src/interview/ui.ts
24114
+ var BRAND_LOGO_URL = "https://ohmyopencodeslim.com/android-chrome-512x512.png";
23733
24115
  function escapeHtml(value) {
23734
24116
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
23735
24117
  }
@@ -23827,12 +24209,8 @@ function sharedStyles() {
23827
24209
  }
23828
24210
  .footer { margin-top: 32px; text-align: center; font-size: 13px; color: rgba(255,255,255,0.4); }`;
23829
24211
  }
23830
- function brandSvg(size) {
23831
- return `<svg class="brand-mark" viewBox="0 0 144 144" role="img" aria-label="Oh My Opencode Slim" style="width:${size}px;height:${size}px">
23832
- <rect x="12" y="12" width="120" height="120" rx="32" fill="rgba(255,255,255,0.08)" stroke="rgba(255,255,255,0.18)" stroke-width="2"/>
23833
- <path d="M50 48h18c16 0 26 10 26 24s-10 24-26 24H50z" fill="none" stroke="white" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
23834
- <path d="M74 48h20c10 0 18 8 18 18v12c0 10-8 18-18 18H74" fill="none" stroke="white" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" opacity="0.65"/>
23835
- </svg>`;
24212
+ function brandImage(size) {
24213
+ return `<img class="brand-mark" src="${BRAND_LOGO_URL}" alt="Oh My Opencode Slim" width="${size}" height="${size}" />`;
23836
24214
  }
23837
24215
  function renderDashboardPage(interviews, files, outputFolder) {
23838
24216
  const activeHtml = interviews.length === 0 ? "" : interviews.map((item) => {
@@ -24041,7 +24419,7 @@ function renderDashboardPage(interviews, files, outputFolder) {
24041
24419
  <body>
24042
24420
  <div class="wrap">
24043
24421
  <div class="brand-header">
24044
- ${brandSvg(96)}
24422
+ ${brandImage(96)}
24045
24423
  <h1>Interviews</h1>
24046
24424
  <p class="muted">${totalCount} item${totalCount === 1 ? "" : "s"}</p>
24047
24425
  </div>
@@ -24242,6 +24620,37 @@ function renderInterviewPage(interviewId, resumeSlug) {
24242
24620
  }
24243
24621
 
24244
24622
  .options { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; }
24623
+ .question-hint {
24624
+ display: flex;
24625
+ flex-wrap: wrap;
24626
+ gap: 8px;
24627
+ margin: -4px 0 16px;
24628
+ color: rgba(255,255,255,0.5);
24629
+ font-size: 13px;
24630
+ line-height: 1.5;
24631
+ transition: color 0.2s ease;
24632
+ }
24633
+ .active-question .question-hint {
24634
+ color: rgba(255,255,255,0.78);
24635
+ }
24636
+ .hint-chip {
24637
+ display: inline-flex;
24638
+ align-items: center;
24639
+ gap: 6px;
24640
+ padding: 4px 10px;
24641
+ border-radius: 999px;
24642
+ background: rgba(255,255,255,0.06);
24643
+ border: 1px solid rgba(255,255,255,0.08);
24644
+ }
24645
+ .hint-chip kbd {
24646
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
24647
+ font-size: 12px;
24648
+ padding: 2px 6px;
24649
+ border-radius: 6px;
24650
+ background: rgba(255,255,255,0.12);
24651
+ border: 1px solid rgba(255,255,255,0.08);
24652
+ color: rgba(255,255,255,0.95);
24653
+ }
24245
24654
 
24246
24655
  .option {
24247
24656
  border: 1px solid rgba(255,255,255,0.1);
@@ -24521,7 +24930,7 @@ function renderInterviewPage(interviewId, resumeSlug) {
24521
24930
  <div class="wrap">
24522
24931
  <a href="/" class="back-link">← All Interviews</a>
24523
24932
  <div class="brand-header">
24524
- ${brandSvg(144)}
24933
+ ${brandImage(144)}
24525
24934
  </div>
24526
24935
  <h1 id="idea">Connecting...</h1>
24527
24936
  <p class="muted" id="summary">Preparing interview session</p>
@@ -24562,7 +24971,15 @@ function renderInterviewPage(interviewId, resumeSlug) {
24562
24971
  ${clipboardHelperJs()}
24563
24972
  const interviewId = ${JSON.stringify(interviewId).replace(/</g, "\\u003c")};
24564
24973
  const resumeSlug = ${JSON.stringify(resumeSlug).replace(/</g, "\\u003c")};
24565
- const state = { data: null, answers: {}, activeQuestionIndex: 0, lastSig: null, customMode: {} };
24974
+ const state = {
24975
+ data: null,
24976
+ answers: {},
24977
+ activeQuestionIndex: 0,
24978
+ lastQuestionIds: [],
24979
+ lastSig: null,
24980
+ customMode: {},
24981
+ isSubmitting: false,
24982
+ };
24566
24983
 
24567
24984
  function updateSubmitButton() {
24568
24985
  const button = document.getElementById('submitButton');
@@ -24575,7 +24992,11 @@ function renderInterviewPage(interviewId, resumeSlug) {
24575
24992
  const allAnswered = questions.every((question) =>
24576
24993
  (state.answers[question.id] || '').trim().length > 0,
24577
24994
  );
24578
- button.disabled = state.data.isBusy || !questions.length || !allAnswered;
24995
+ button.disabled =
24996
+ state.data.isBusy ||
24997
+ state.isSubmitting ||
24998
+ !questions.length ||
24999
+ !allAnswered;
24579
25000
  const hideSubmit = ['completed', 'session-disconnected'];
24580
25001
  button.style.display = hideSubmit.includes(state.data.mode) ? 'none' : '';
24581
25002
 
@@ -24726,6 +25147,54 @@ function renderInterviewPage(interviewId, resumeSlug) {
24726
25147
  });
24727
25148
  }
24728
25149
 
25150
+ function scrollToActiveQuestion(behavior) {
25151
+ const questions = state.data?.questions || [];
25152
+ const activeQ = questions[state.activeQuestionIndex];
25153
+ if (!activeQ) return;
25154
+
25155
+ const wrapper = document.getElementById('question-' + activeQ.id);
25156
+ if (wrapper) {
25157
+ wrapper.scrollIntoView({ behavior, block: 'center' });
25158
+ }
25159
+ }
25160
+
25161
+ function syncActiveQuestionIndex(questions) {
25162
+ if (!questions.length) {
25163
+ state.activeQuestionIndex = 0;
25164
+ state.lastQuestionIds = [];
25165
+ return;
25166
+ }
25167
+
25168
+ const nextQuestionIds = questions.map((question) => question.id);
25169
+ const previousQuestionIds = state.lastQuestionIds || [];
25170
+ const activeQuestionId = previousQuestionIds[state.activeQuestionIndex];
25171
+ const nextActiveIndex = activeQuestionId
25172
+ ? nextQuestionIds.indexOf(activeQuestionId)
25173
+ : -1;
25174
+
25175
+ if (nextActiveIndex >= 0) {
25176
+ state.activeQuestionIndex = nextActiveIndex;
25177
+ } else {
25178
+ state.activeQuestionIndex = 0;
25179
+ }
25180
+
25181
+ state.lastQuestionIds = nextQuestionIds;
25182
+ }
25183
+
25184
+ function isTextEntryTarget(target) {
25185
+ return target &&
25186
+ (target.tagName === 'TEXTAREA' ||
25187
+ target.tagName === 'INPUT' ||
25188
+ target.isContentEditable);
25189
+ }
25190
+
25191
+ function isShortcutBlockedTarget(target) {
25192
+ if (!target) return false;
25193
+ return !!target.closest(
25194
+ 'button, a, select, summary, textarea, input, [contenteditable="true"]',
25195
+ );
25196
+ }
25197
+
24729
25198
  document.addEventListener('keydown', (e) => {
24730
25199
  const isSubmitShortcut =
24731
25200
  (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) ||
@@ -24739,12 +25208,41 @@ function renderInterviewPage(interviewId, resumeSlug) {
24739
25208
  return;
24740
25209
  }
24741
25210
 
24742
- if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') return;
25211
+ if (isTextEntryTarget(e.target)) return;
24743
25212
  if (e.ctrlKey || e.metaKey || e.altKey) return;
24744
25213
 
24745
25214
  const questions = state.data?.questions || [];
24746
25215
  if (!questions.length) return;
24747
25216
 
25217
+ if (e.key === 'Enter') {
25218
+ if (e.repeat) {
25219
+ e.preventDefault();
25220
+ return;
25221
+ }
25222
+ if (isShortcutBlockedTarget(e.target)) return;
25223
+ if (state.data.isBusy || state.isSubmitting) return;
25224
+
25225
+ const activeQ = questions[state.activeQuestionIndex];
25226
+ if (!activeQ) return;
25227
+
25228
+ const answer = (state.answers[activeQ.id] || '').trim();
25229
+ if (!answer) return;
25230
+
25231
+ const isLastQuestion =
25232
+ state.activeQuestionIndex === questions.length - 1;
25233
+ if (isLastQuestion) {
25234
+ const submitBtn = document.getElementById('submitButton');
25235
+ if (submitBtn && !submitBtn.disabled) {
25236
+ submitBtn.click();
25237
+ }
25238
+ } else {
25239
+ advanceToNextQuestion(activeQ.id);
25240
+ }
25241
+
25242
+ e.preventDefault();
25243
+ return;
25244
+ }
25245
+
24748
25246
  const num = parseInt(e.key, 10);
24749
25247
  if (num >= 1 && num <= 9) {
24750
25248
  const activeQ = questions[state.activeQuestionIndex];
@@ -24912,6 +25410,10 @@ function renderInterviewPage(interviewId, resumeSlug) {
24912
25410
  function renderQuestions(questions) {
24913
25411
  const sig = JSON.stringify([questions, state.data?.mode]);
24914
25412
  const container = document.getElementById('questions');
25413
+ const previousActiveQuestionId =
25414
+ state.lastQuestionIds[state.activeQuestionIndex];
25415
+
25416
+ syncActiveQuestionIndex(questions);
24915
25417
 
24916
25418
  if (state.lastSig === sig) {
24917
25419
  questions.forEach((q) => updateOptionsDOM(q.id));
@@ -24952,6 +25454,15 @@ function renderInterviewPage(interviewId, resumeSlug) {
24952
25454
  wrapper.appendChild(title);
24953
25455
 
24954
25456
  const predefined = question.options || [];
25457
+ if (predefined.length) {
25458
+ const hint = document.createElement('div');
25459
+ hint.className = 'question-hint';
25460
+ hint.innerHTML =
25461
+ '<span class="hint-chip"><kbd>1-9</kbd><span>Choose an option</span></span>' +
25462
+ '<span class="hint-chip"><kbd>Enter</kbd><span>Accept selected answer</span></span>';
25463
+ wrapper.appendChild(hint);
25464
+ }
25465
+
24955
25466
  if (predefined.length) {
24956
25467
  const options = document.createElement('div');
24957
25468
  options.className = 'options';
@@ -24987,6 +25498,13 @@ function renderInterviewPage(interviewId, resumeSlug) {
24987
25498
 
24988
25499
  updateActiveQuestionFocus();
24989
25500
  questions.forEach(q => updateOptionsDOM(q.id));
25501
+ const currentActiveQuestionId = questions[state.activeQuestionIndex]?.id;
25502
+ if (
25503
+ questions.length > 0 &&
25504
+ previousActiveQuestionId !== currentActiveQuestionId
25505
+ ) {
25506
+ scrollToActiveQuestion('smooth');
25507
+ }
24990
25508
  }
24991
25509
 
24992
25510
  function render(data) {
@@ -25055,8 +25573,13 @@ function renderInterviewPage(interviewId, resumeSlug) {
25055
25573
  render(data);
25056
25574
  }
25057
25575
 
25576
+ function scrollToTop() {
25577
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
25578
+ }
25579
+
25058
25580
  document.getElementById('submitButton').addEventListener('click', async () => {
25059
- if (!state.data) return;
25581
+ if (!state.data || state.isSubmitting) return;
25582
+ document.getElementById('submitButton').blur();
25060
25583
  const answers = (state.data.questions || []).map((question) => {
25061
25584
  return {
25062
25585
  questionId: question.id,
@@ -25064,10 +25587,14 @@ function renderInterviewPage(interviewId, resumeSlug) {
25064
25587
  };
25065
25588
  });
25066
25589
 
25590
+ state.isSubmitting = true;
25591
+ updateSubmitButton();
25592
+
25067
25593
  const overlay = document.getElementById('loadingOverlay');
25068
25594
  const overlayText = document.getElementById('loadingText');
25069
25595
  overlay.classList.add('active');
25070
25596
  overlayText.textContent = "Submitting Answers...";
25597
+ scrollToTop();
25071
25598
 
25072
25599
  try {
25073
25600
  const response = await fetch('/api/interviews/' + encodeURIComponent(interviewId) + '/answers', {
@@ -25080,6 +25607,8 @@ function renderInterviewPage(interviewId, resumeSlug) {
25080
25607
  } catch (err) {
25081
25608
  document.getElementById('submitStatus').textContent = 'Error submitting answers.';
25082
25609
  }
25610
+ state.isSubmitting = false;
25611
+ updateSubmitButton();
25083
25612
  try {
25084
25613
  await refresh();
25085
25614
  } catch (_error) {
@@ -26399,6 +26928,7 @@ function createInterviewService(ctx, config, deps) {
26399
26928
  const activeInterviewIds = new Map;
26400
26929
  const interviewsById = new Map;
26401
26930
  const sessionBusy = new Map;
26931
+ const sessionModel = new Map;
26402
26932
  const browserOpened = new Set;
26403
26933
  let resolveBaseUrl = null;
26404
26934
  let onStateChange = null;
@@ -26654,10 +27184,12 @@ function createInterviewService(ctx, config, deps) {
26654
27184
  }
26655
27185
  await appendInterviewAnswers(interview, state.questions, answers);
26656
27186
  const prompt = buildAnswerPrompt(answers, state.questions, maxQuestions);
27187
+ const model = sessionModel.get(interview.sessionID);
26657
27188
  await ctx.client.session.promptAsync({
26658
27189
  path: { id: interview.sessionID },
26659
27190
  body: {
26660
- parts: [createInternalAgentTextPart(prompt)]
27191
+ parts: [createInternalAgentTextPart(prompt)],
27192
+ ...model ? { model: parseModelReference(model) ?? undefined } : {}
26661
27193
  }
26662
27194
  });
26663
27195
  promptSent = true;
@@ -26707,12 +27239,23 @@ function createInterviewService(ctx, config, deps) {
26707
27239
  }
26708
27240
  return;
26709
27241
  }
27242
+ if (event.type === "message.updated") {
27243
+ const info = properties;
27244
+ const sessionID = info?.info?.sessionID;
27245
+ const providerID = info?.info?.providerID;
27246
+ const modelID = info?.info?.modelID;
27247
+ if (sessionID && providerID && modelID) {
27248
+ sessionModel.set(sessionID, `${providerID}/${modelID}`);
27249
+ }
27250
+ return;
27251
+ }
26710
27252
  if (event.type === "session.deleted") {
26711
27253
  const deletedSessionId = (properties.info?.id ?? properties.sessionID) || null;
26712
27254
  if (!deletedSessionId) {
26713
27255
  return;
26714
27256
  }
26715
27257
  sessionBusy.delete(deletedSessionId);
27258
+ sessionModel.delete(deletedSessionId);
26716
27259
  const interviewId = activeInterviewIds.get(deletedSessionId);
26717
27260
  if (!interviewId) {
26718
27261
  return;
@@ -26809,10 +27352,12 @@ function createInterviewService(ctx, config, deps) {
26809
27352
  ].join(`
26810
27353
  `);
26811
27354
  }
27355
+ const model = sessionModel.get(interview.sessionID);
26812
27356
  await ctx.client.session.promptAsync({
26813
27357
  path: { id: interview.sessionID },
26814
27358
  body: {
26815
- parts: [createInternalAgentTextPart(prompt)]
27359
+ parts: [createInternalAgentTextPart(prompt)],
27360
+ ...model ? { model: parseModelReference(model) ?? undefined } : {}
26816
27361
  }
26817
27362
  });
26818
27363
  promptSent = true;
@@ -27786,6 +28331,8 @@ class MultiplexerSessionManager {
27786
28331
  directory;
27787
28332
  multiplexer = null;
27788
28333
  sessions = new Map;
28334
+ knownSessions = new Map;
28335
+ spawningSessions = new Set;
27789
28336
  pollInterval;
27790
28337
  enabled = false;
27791
28338
  constructor(ctx, config) {
@@ -27814,45 +28361,59 @@ class MultiplexerSessionManager {
27814
28361
  const parentId = info.parentID;
27815
28362
  const title = info.title ?? "Subagent";
27816
28363
  const directory = info.directory ?? this.directory;
27817
- if (this.sessions.has(sessionId)) {
27818
- log("[multiplexer-session-manager] session already tracked", {
28364
+ this.knownSessions.set(sessionId, {
28365
+ parentId,
28366
+ title,
28367
+ directory
28368
+ });
28369
+ if (this.isTrackedOrSpawning(sessionId)) {
28370
+ log("[multiplexer-session-manager] session already tracked or spawning", {
27819
28371
  sessionId
27820
28372
  });
27821
28373
  return;
27822
28374
  }
27823
- const serverRunning = await isServerRunning(this.serverUrl);
27824
- if (!serverRunning) {
27825
- log("[multiplexer-session-manager] server not running, skipping", {
27826
- serverUrl: this.serverUrl
27827
- });
27828
- return;
27829
- }
27830
- log("[multiplexer-session-manager] child session created, spawning pane", {
27831
- sessionId,
27832
- parentId,
27833
- title
27834
- });
27835
- const paneResult = await this.multiplexer.spawnPane(sessionId, title, this.serverUrl, directory).catch((err) => {
27836
- log("[multiplexer-session-manager] failed to spawn pane", {
27837
- error: String(err)
27838
- });
27839
- return { success: false, paneId: undefined };
27840
- });
27841
- if (paneResult.success && paneResult.paneId) {
27842
- const now = Date.now();
27843
- this.sessions.set(sessionId, {
28375
+ this.spawningSessions.add(sessionId);
28376
+ try {
28377
+ const serverRunning = await isServerRunning(this.serverUrl);
28378
+ if (!serverRunning) {
28379
+ log("[multiplexer-session-manager] server not running, skipping", {
28380
+ serverUrl: this.serverUrl
28381
+ });
28382
+ return;
28383
+ }
28384
+ if (this.sessions.has(sessionId)) {
28385
+ return;
28386
+ }
28387
+ log("[multiplexer-session-manager] child session created, spawning pane", {
27844
28388
  sessionId,
27845
- paneId: paneResult.paneId,
27846
28389
  parentId,
27847
- title,
27848
- createdAt: now,
27849
- lastSeenAt: now
28390
+ title
27850
28391
  });
27851
- log("[multiplexer-session-manager] pane spawned", {
27852
- sessionId,
27853
- paneId: paneResult.paneId
28392
+ const paneResult = await this.multiplexer.spawnPane(sessionId, title, this.serverUrl, directory).catch((err) => {
28393
+ log("[multiplexer-session-manager] failed to spawn pane", {
28394
+ error: String(err)
28395
+ });
28396
+ return { success: false, paneId: undefined };
27854
28397
  });
27855
- this.startPolling();
28398
+ if (paneResult.success && paneResult.paneId) {
28399
+ const now = Date.now();
28400
+ this.sessions.set(sessionId, {
28401
+ sessionId,
28402
+ 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();
28414
+ }
28415
+ } finally {
28416
+ this.spawningSessions.delete(sessionId);
27856
28417
  }
27857
28418
  }
27858
28419
  async onSessionStatus(event) {
@@ -27865,6 +28426,10 @@ class MultiplexerSessionManager {
27865
28426
  return;
27866
28427
  if (event.properties?.status?.type === "idle") {
27867
28428
  await this.closeSession(sessionId);
28429
+ return;
28430
+ }
28431
+ if (event.properties?.status?.type === "busy") {
28432
+ await this.respawnIfKnown(sessionId);
27868
28433
  }
27869
28434
  }
27870
28435
  async onSessionDeleted(event) {
@@ -27879,6 +28444,7 @@ class MultiplexerSessionManager {
27879
28444
  sessionId
27880
28445
  });
27881
28446
  await this.closeSession(sessionId);
28447
+ this.knownSessions.delete(sessionId);
27882
28448
  }
27883
28449
  startPolling() {
27884
28450
  if (this.pollInterval)
@@ -27939,6 +28505,61 @@ class MultiplexerSessionManager {
27939
28505
  this.stopPolling();
27940
28506
  }
27941
28507
  }
28508
+ async respawnIfKnown(sessionId) {
28509
+ if (!this.enabled || !this.multiplexer)
28510
+ return;
28511
+ if (this.isTrackedOrSpawning(sessionId))
28512
+ return;
28513
+ const known = this.knownSessions.get(sessionId);
28514
+ if (!known)
28515
+ return;
28516
+ this.spawningSessions.add(sessionId);
28517
+ try {
28518
+ const serverRunning = await isServerRunning(this.serverUrl);
28519
+ if (!serverRunning) {
28520
+ log("[multiplexer-session-manager] server not running, skipping busy respawn", {
28521
+ serverUrl: this.serverUrl,
28522
+ sessionId
28523
+ });
28524
+ return;
28525
+ }
28526
+ if (this.sessions.has(sessionId))
28527
+ return;
28528
+ log("[multiplexer-session-manager] child session busy again, respawning pane", {
28529
+ sessionId,
28530
+ parentId: known.parentId,
28531
+ title: known.title
28532
+ });
28533
+ const paneResult = await this.multiplexer.spawnPane(sessionId, known.title, this.serverUrl, known.directory).catch((err) => {
28534
+ log("[multiplexer-session-manager] failed to respawn pane", {
28535
+ error: String(err)
28536
+ });
28537
+ return { success: false, paneId: undefined };
28538
+ });
28539
+ if (!paneResult.success || !paneResult.paneId)
28540
+ return;
28541
+ const now = Date.now();
28542
+ this.sessions.set(sessionId, {
28543
+ sessionId,
28544
+ paneId: paneResult.paneId,
28545
+ parentId: known.parentId,
28546
+ title: known.title,
28547
+ directory: known.directory,
28548
+ createdAt: now,
28549
+ lastSeenAt: now
28550
+ });
28551
+ log("[multiplexer-session-manager] pane respawned on busy", {
28552
+ sessionId,
28553
+ paneId: paneResult.paneId
28554
+ });
28555
+ this.startPolling();
28556
+ } finally {
28557
+ this.spawningSessions.delete(sessionId);
28558
+ }
28559
+ }
28560
+ isTrackedOrSpawning(sessionId) {
28561
+ return this.sessions.has(sessionId) || this.spawningSessions.has(sessionId);
28562
+ }
27942
28563
  async cleanup() {
27943
28564
  this.stopPolling();
27944
28565
  if (this.sessions.size > 0 && this.multiplexer) {
@@ -27953,6 +28574,8 @@ class MultiplexerSessionManager {
27953
28574
  await Promise.all(closePromises);
27954
28575
  this.sessions.clear();
27955
28576
  }
28577
+ this.knownSessions.clear();
28578
+ this.spawningSessions.clear();
27956
28579
  log("[multiplexer-session-manager] cleanup complete");
27957
28580
  }
27958
28581
  }
@@ -28571,14 +29194,158 @@ Returns the councillor responses with a summary footer.`,
28571
29194
  *Council: ${completed}/${total} councillors responded (${composition})*`;
28572
29195
  const deprecated = councilManager.getDeprecatedFields();
28573
29196
  if (deprecated && deprecated.length > 0) {
29197
+ const legacyMasterModel = councilManager.getLegacyMasterModel();
29198
+ const hasMaster = deprecated.includes("master");
29199
+ const trulyIgnored = hasMaster && !legacyMasterModel ? deprecated : deprecated.filter((f) => f !== "master");
29200
+ const parts = [];
29201
+ if (hasMaster && legacyMasterModel) {
29202
+ parts.push(`\`council.master\` is deprecated and will be removed in a future version. Its \`model\` is currently used as a fallback for the council agent — add a \`council\` entry to your preset to make this explicit.`);
29203
+ }
29204
+ if (trulyIgnored.length > 0) {
29205
+ parts.push(`${trulyIgnored.map((f) => `\`council.${f}\``).join(", ")} ${trulyIgnored.length === 1 ? "is" : "are"} deprecated and ignored — remove ${trulyIgnored.length === 1 ? "it" : "them"} from your config.`);
29206
+ }
28574
29207
  output += `
28575
- ⚠ Config warning: ${deprecated.map((f) => `\`council.${f}\``).join(", ")} ${deprecated.length === 1 ? "is" : "are"} deprecated and ignored. The council agent synthesizes directly — remove ${deprecated.length === 1 ? "it" : "them"} from your config.`;
29208
+ ⚠ Config warning: ${parts.join(" ")}`;
28576
29209
  }
28577
29210
  return output;
28578
29211
  }
28579
29212
  });
28580
29213
  return { council_session };
28581
29214
  }
29215
+ // src/tools/preset-manager.ts
29216
+ var COMMAND_NAME3 = "preset";
29217
+ function createPresetManager(ctx, config) {
29218
+ let activePreset = config.preset ?? null;
29219
+ async function handleCommandExecuteBefore(input, output) {
29220
+ if (input.command !== COMMAND_NAME3) {
29221
+ return;
29222
+ }
29223
+ output.parts.length = 0;
29224
+ const arg = input.arguments.trim();
29225
+ const presets = config.presets ?? {};
29226
+ if (!arg) {
29227
+ output.parts.push(createInternalAgentTextPart(formatPresetList(presets)));
29228
+ return;
29229
+ }
29230
+ if (/\s/.test(arg)) {
29231
+ const suggestion = arg.split(/\s+/)[0];
29232
+ output.parts.push(createInternalAgentTextPart(`Preset names cannot contain spaces. Did you mean: /preset ${suggestion}?`));
29233
+ return;
29234
+ }
29235
+ await switchPreset(arg, presets, output);
29236
+ }
29237
+ function registerCommand(opencodeConfig) {
29238
+ const configCommand = opencodeConfig.command;
29239
+ if (!configCommand?.[COMMAND_NAME3]) {
29240
+ if (!opencodeConfig.command) {
29241
+ opencodeConfig.command = {};
29242
+ }
29243
+ opencodeConfig.command[COMMAND_NAME3] = {
29244
+ template: "List available presets and switch between them",
29245
+ description: "Switch agent presets at runtime (e.g., /preset cheap, /preset powerful)"
29246
+ };
29247
+ }
29248
+ }
29249
+ async function switchPreset(presetName, presets, output) {
29250
+ const preset = presets[presetName];
29251
+ if (!preset) {
29252
+ const available = Object.keys(presets);
29253
+ const hint = available.length > 0 ? `Available presets: ${available.join(", ")}` : "No presets configured. Define presets in oh-my-opencode-slim.jsonc.";
29254
+ output.parts.push(createInternalAgentTextPart(`Preset "${presetName}" not found. ${hint}`));
29255
+ return;
29256
+ }
29257
+ const agentUpdates = {};
29258
+ for (const [agentName, override] of Object.entries(preset)) {
29259
+ const agentConfig = mapOverrideToAgentConfig(override);
29260
+ if (Object.keys(agentConfig).length > 0) {
29261
+ agentUpdates[agentName] = agentConfig;
29262
+ }
29263
+ }
29264
+ if (Object.keys(agentUpdates).length === 0) {
29265
+ output.parts.push(createInternalAgentTextPart(`Preset "${presetName}" is empty (no agent overrides defined).`));
29266
+ return;
29267
+ }
29268
+ try {
29269
+ await ctx.client.config.update({
29270
+ body: { agent: agentUpdates }
29271
+ });
29272
+ activePreset = presetName;
29273
+ const summary = Object.entries(agentUpdates).map(([name, cfg]) => {
29274
+ const parts = [name];
29275
+ if (cfg.model)
29276
+ parts.push(`model: ${cfg.model}`);
29277
+ if (cfg.variant)
29278
+ parts.push(`variant: ${cfg.variant}`);
29279
+ if (cfg.temperature !== undefined)
29280
+ parts.push(`temp: ${cfg.temperature}`);
29281
+ if (cfg.options)
29282
+ parts.push("options: yes");
29283
+ return parts.join(" → ");
29284
+ }).join(`
29285
+ `);
29286
+ output.parts.push(createInternalAgentTextPart(`Switched to preset "${presetName}":
29287
+ ${summary}`));
29288
+ } catch (err) {
29289
+ output.parts.push(createInternalAgentTextPart(`Failed to switch preset "${presetName}": ${String(err)}`));
29290
+ }
29291
+ }
29292
+ function mapOverrideToAgentConfig(override) {
29293
+ const agentConfig = {};
29294
+ if (typeof override.model === "string") {
29295
+ agentConfig.model = override.model;
29296
+ } else if (Array.isArray(override.model) && override.model.length > 0) {
29297
+ const first = override.model[0];
29298
+ agentConfig.model = typeof first === "string" ? first : first.id;
29299
+ if (typeof first !== "string" && first.variant) {
29300
+ agentConfig.variant = first.variant;
29301
+ }
29302
+ }
29303
+ if (typeof override.temperature === "number") {
29304
+ agentConfig.temperature = override.temperature;
29305
+ }
29306
+ if (typeof override.variant === "string") {
29307
+ agentConfig.variant = override.variant;
29308
+ }
29309
+ if (override.options && typeof override.options === "object" && !Array.isArray(override.options)) {
29310
+ agentConfig.options = override.options;
29311
+ }
29312
+ return agentConfig;
29313
+ }
29314
+ function formatPresetList(presets) {
29315
+ const names = Object.keys(presets);
29316
+ if (names.length === 0) {
29317
+ return 'No presets configured. Define presets in oh-my-opencode-slim.jsonc under the "presets" field.';
29318
+ }
29319
+ const lines = ["Available presets:"];
29320
+ for (const name of names) {
29321
+ const marker = name === activePreset ? " ← active" : "";
29322
+ const preset = presets[name];
29323
+ const agentNames = Object.keys(preset);
29324
+ const models = agentNames.map((a) => {
29325
+ const cfg = preset[a];
29326
+ const modelStr = typeof cfg.model === "string" ? cfg.model : Array.isArray(cfg.model) && cfg.model.length > 0 ? resolveFirstModel(cfg.model) : undefined;
29327
+ return modelStr ? ` ${a} → ${modelStr}` : ` ${a}`;
29328
+ }).join(`
29329
+ `);
29330
+ lines.push(` ${name}${marker}`);
29331
+ lines.push(models);
29332
+ }
29333
+ lines.push(`
29334
+ Usage: /preset <name> to switch.`);
29335
+ return lines.join(`
29336
+ `);
29337
+ }
29338
+ function resolveFirstModel(models) {
29339
+ if (models.length === 0)
29340
+ return;
29341
+ const first = models[0];
29342
+ return typeof first === "string" ? first : first.id;
29343
+ }
29344
+ return {
29345
+ handleCommandExecuteBefore,
29346
+ registerCommand
29347
+ };
29348
+ }
28582
29349
  // src/tools/smartfetch/constants.ts
28583
29350
  var DOCS_HOST_SUFFIXES = [
28584
29351
  ".readthedocs.io",
@@ -30916,6 +31683,17 @@ class SubagentDepthTracker {
30916
31683
  }
30917
31684
  }
30918
31685
 
31686
+ // src/utils/system-collapse.ts
31687
+ function collapseSystemInPlace(system2) {
31688
+ const joined = system2.join(`
31689
+
31690
+ `);
31691
+ system2.length = 0;
31692
+ if (joined) {
31693
+ system2.push(joined);
31694
+ }
31695
+ }
31696
+
30919
31697
  // src/index.ts
30920
31698
  async function appLog(ctx, level, message) {
30921
31699
  try {
@@ -30965,7 +31743,9 @@ var OhMyOpenCodeLite = async (ctx) => {
30965
31743
  let jsonErrorRecoveryHook;
30966
31744
  let foregroundFallback;
30967
31745
  let todoContinuationHook;
31746
+ let taskSessionManagerHook;
30968
31747
  let interviewManager;
31748
+ let presetManager;
30969
31749
  let councilTools;
30970
31750
  let webfetch;
30971
31751
  let toolCount = 0;
@@ -31023,7 +31803,7 @@ var OhMyOpenCodeLite = async (ctx) => {
31023
31803
  multiplexerSessionManager = new MultiplexerSessionManager(ctx, multiplexerConfig);
31024
31804
  autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
31025
31805
  showStartupToast: config.showStartupToast ?? true,
31026
- autoUpdate: true
31806
+ autoUpdate: config.autoUpdate ?? true
31027
31807
  });
31028
31808
  phaseReminderHook = createPhaseReminderHook();
31029
31809
  filterAvailableSkillsHook = createFilterAvailableSkillsHook(ctx, config);
@@ -31042,7 +31822,12 @@ var OhMyOpenCodeLite = async (ctx) => {
31042
31822
  autoEnable: config.todoContinuation?.autoEnable ?? false,
31043
31823
  autoEnableThreshold: config.todoContinuation?.autoEnableThreshold ?? 4
31044
31824
  });
31825
+ taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
31826
+ maxSessionsPerAgent: config.sessionManager?.maxSessionsPerAgent ?? 2,
31827
+ shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
31828
+ });
31045
31829
  interviewManager = createInterviewManager(ctx, config);
31830
+ presetManager = createPresetManager(ctx, config);
31046
31831
  toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2;
31047
31832
  } catch (err) {
31048
31833
  log("[plugin] FATAL: init failed", String(err));
@@ -31197,6 +31982,7 @@ var OhMyOpenCodeLite = async (ctx) => {
31197
31982
  };
31198
31983
  }
31199
31984
  interviewManager.registerCommand(opencodeConfig);
31985
+ presetManager.registerCommand(opencodeConfig);
31200
31986
  },
31201
31987
  event: async (input) => {
31202
31988
  const event = input.event;
@@ -31215,6 +32001,7 @@ var OhMyOpenCodeLite = async (ctx) => {
31215
32001
  await multiplexerSessionManager.onSessionDeleted(event);
31216
32002
  await interviewManager.handleEvent(input);
31217
32003
  await postFileToolNudgeHook.event(input);
32004
+ await taskSessionManagerHook.event(input);
31218
32005
  if (input.event.type === "session.deleted") {
31219
32006
  const props = input.event.properties;
31220
32007
  const sessionID = props?.info?.id ?? props?.sessionID;
@@ -31228,10 +32015,12 @@ var OhMyOpenCodeLite = async (ctx) => {
31228
32015
  },
31229
32016
  "tool.execute.before": async (input, output) => {
31230
32017
  await applyPatchHook["tool.execute.before"](input, output);
32018
+ await taskSessionManagerHook["tool.execute.before"](input, output);
31231
32019
  },
31232
32020
  "command.execute.before": async (input, output) => {
31233
32021
  await todoContinuationHook.handleCommandExecuteBefore(input, output);
31234
32022
  await interviewManager.handleCommandExecuteBefore(input, output);
32023
+ await presetManager.handleCommandExecuteBefore(input, output);
31235
32024
  },
31236
32025
  "chat.headers": chatHeadersHook["chat.headers"],
31237
32026
  "chat.message": async (input, output) => {
@@ -31262,10 +32051,8 @@ ${output.system[0]}` : "");
31262
32051
  }
31263
32052
  await todoContinuationHook.handleChatSystemTransform(input, output);
31264
32053
  await postFileToolNudgeHook["experimental.chat.system.transform"](input, output);
31265
- const joined = output.system.join(`
31266
-
31267
- `);
31268
- output.system = joined ? [joined] : [];
32054
+ await taskSessionManagerHook["experimental.chat.system.transform"](input, output);
32055
+ collapseSystemInPlace(output.system);
31269
32056
  },
31270
32057
  "experimental.chat.messages.transform": async (input, output) => {
31271
32058
  const typedOutput = output;
@@ -31297,6 +32084,7 @@ ${output.system[0]}` : "");
31297
32084
  await jsonErrorRecoveryHook["tool.execute.after"](input, output);
31298
32085
  await todoContinuationHook.handleToolExecuteAfter(input);
31299
32086
  await postFileToolNudgeHook["tool.execute.after"](input, output);
32087
+ await taskSessionManagerHook["tool.execute.after"](input, output);
31300
32088
  }
31301
32089
  };
31302
32090
  };