oh-my-opencode-slim 2.1.1 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.ja-JP.md +113 -118
  2. package/README.ko-KR.md +141 -123
  3. package/README.md +102 -145
  4. package/README.zh-CN.md +112 -118
  5. package/dist/agents/index.d.ts +0 -4
  6. package/dist/cli/index.js +57 -20
  7. package/dist/cli/providers.d.ts +11 -4
  8. package/dist/config/constants.d.ts +11 -1
  9. package/dist/config/schema.d.ts +680 -2
  10. package/dist/config/strip-orchestrator-model.d.ts +9 -0
  11. package/dist/hooks/command-hook-utils.d.ts +5 -0
  12. package/dist/hooks/foreground-fallback/index.d.ts +40 -28
  13. package/dist/hooks/image-hook.d.ts +1 -0
  14. package/dist/hooks/index.d.ts +1 -1
  15. package/dist/index.js +1992 -455
  16. package/dist/interview/document.d.ts +2 -1
  17. package/dist/interview/ui.d.ts +0 -1
  18. package/dist/multiplexer/cmux/close-policy.d.ts +20 -0
  19. package/dist/multiplexer/cmux/index.d.ts +102 -0
  20. package/dist/multiplexer/cmux/session-lifecycle.d.ts +92 -0
  21. package/dist/multiplexer/cmux/session-state.d.ts +45 -0
  22. package/dist/multiplexer/factory.d.ts +0 -9
  23. package/dist/multiplexer/herdr/index.d.ts +2 -0
  24. package/dist/multiplexer/index.d.ts +3 -1
  25. package/dist/multiplexer/session-manager.d.ts +10 -2
  26. package/dist/multiplexer/shared.d.ts +8 -1
  27. package/dist/multiplexer/types.d.ts +5 -2
  28. package/dist/multiplexer/zellij/index.d.ts +1 -1
  29. package/dist/tools/smartfetch/utils.d.ts +3 -1
  30. package/dist/tui-state.d.ts +5 -5
  31. package/dist/tui.js +87 -27
  32. package/dist/utils/escape-html.d.ts +1 -0
  33. package/dist/utils/frontmatter.d.ts +6 -0
  34. package/dist/utils/index.d.ts +1 -1
  35. package/dist/utils/logger.d.ts +0 -2
  36. package/oh-my-opencode-slim.schema.json +717 -2
  37. package/package.json +1 -1
  38. package/src/skills/clonedeps/SKILL.md +32 -29
  39. package/src/skills/codemap.md +5 -3
  40. package/src/skills/deepwork/SKILL.md +17 -6
  41. package/src/skills/verification-planning/SKILL.md +102 -0
  42. package/src/skills/worktrees/SKILL.md +14 -7
  43. package/src/skills/release-smoke-test/SKILL.md +0 -159
package/dist/index.js CHANGED
@@ -18363,6 +18363,12 @@ var CUSTOM_SKILLS = [
18363
18363
  allowedAgents: ["orchestrator"],
18364
18364
  sourcePath: "src/skills/deepwork"
18365
18365
  },
18366
+ {
18367
+ name: "verification-planning",
18368
+ description: "Plan credible, proportionate evidence before non-trivial implementation",
18369
+ allowedAgents: ["orchestrator"],
18370
+ sourcePath: "src/skills/verification-planning"
18371
+ },
18366
18372
  {
18367
18373
  name: "reflect",
18368
18374
  description: "Review repeated work and suggest reusable workflow improvements",
@@ -18375,12 +18381,6 @@ var CUSTOM_SKILLS = [
18375
18381
  allowedAgents: ["orchestrator"],
18376
18382
  sourcePath: "src/skills/oh-my-opencode-slim"
18377
18383
  },
18378
- {
18379
- name: "release-smoke-test",
18380
- description: "Validate packed release candidates and bugfixes before public publish",
18381
- allowedAgents: ["orchestrator"],
18382
- sourcePath: "src/skills/release-smoke-test"
18383
- },
18384
18384
  {
18385
18385
  name: "worktrees",
18386
18386
  description: "Manage Git worktrees as OMO safe isolated coding lanes for complex/risky/parallel work",
@@ -18496,7 +18496,7 @@ var DEFAULT_MODELS = {
18496
18496
  var POLL_INTERVAL_BACKGROUND_MS = 2000;
18497
18497
  var MAX_POLL_TIME_MS = 5 * 60 * 1000;
18498
18498
  var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
18499
- var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: plan lanes/dependencies → dispatch background specialists → track task IDs → wait for hook-driven completion → reconcile terminal results → verify. Do not poll running jobs, consume running-job output, or advance dependent work. !END!`;
18499
+ var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: First choose the lightest workflow that fits the work. If direct execution is justified, complete it and verify proportionately. Otherwise: plan lanes/dependencies → dispatch background specialists → track task IDs → wait for hook-driven completion → reconcile terminal results → verify. Do not poll running jobs, consume running-job output, or advance dependent work. !END!`;
18500
18500
  function formatSystemReminder(text) {
18501
18501
  return `<system-reminder>
18502
18502
  ${text}
@@ -18521,6 +18521,13 @@ var NO_SHELL_READONLY_FILE_OPERATIONS_RULES = `**File Operations Rules**:
18521
18521
  var TMUX_SPAWN_DELAY_MS = 500;
18522
18522
  var COUNCILLOR_STAGGER_MS = 250;
18523
18523
  var DEFAULT_DISABLED_AGENTS = ["observer"];
18524
+ var DEFAULT_MAX_SESSIONS_PER_AGENT = 2;
18525
+ var DEFAULT_READ_CONTEXT_MIN_LINES = 10;
18526
+ var DEFAULT_READ_CONTEXT_MAX_FILES = 8;
18527
+ var DEFAULT_IMAGE_ROUTING = "auto";
18528
+ function resolveImageRouting(imageRouting) {
18529
+ return imageRouting ?? DEFAULT_IMAGE_ROUTING;
18530
+ }
18524
18531
  // src/config/council-schema.ts
18525
18532
  import { z } from "zod";
18526
18533
 
@@ -18686,6 +18693,33 @@ var ManualPlanSchema = z2.object({
18686
18693
  librarian: ManualAgentPlanSchema,
18687
18694
  fixer: ManualAgentPlanSchema
18688
18695
  }).strict();
18696
+ var PermissionActionSchema = z2.enum(["ask", "allow", "deny"]);
18697
+ var PermissionRuleSchema = z2.union([
18698
+ PermissionActionSchema,
18699
+ z2.record(z2.string(), PermissionActionSchema)
18700
+ ]);
18701
+ var PermissionObjectSchema = z2.object({
18702
+ read: PermissionRuleSchema.optional(),
18703
+ edit: PermissionRuleSchema.optional(),
18704
+ glob: PermissionRuleSchema.optional(),
18705
+ grep: PermissionRuleSchema.optional(),
18706
+ list: PermissionRuleSchema.optional(),
18707
+ bash: PermissionRuleSchema.optional(),
18708
+ task: PermissionRuleSchema.optional(),
18709
+ external_directory: PermissionRuleSchema.optional(),
18710
+ lsp: PermissionRuleSchema.optional(),
18711
+ skill: PermissionRuleSchema.optional(),
18712
+ todowrite: PermissionActionSchema.optional(),
18713
+ question: PermissionActionSchema.optional(),
18714
+ webfetch: PermissionActionSchema.optional(),
18715
+ websearch: PermissionActionSchema.optional(),
18716
+ codesearch: PermissionActionSchema.optional(),
18717
+ doom_loop: PermissionActionSchema.optional()
18718
+ }).catchall(PermissionRuleSchema);
18719
+ var PermissionConfigSchema = z2.union([
18720
+ PermissionActionSchema,
18721
+ PermissionObjectSchema
18722
+ ]);
18689
18723
  var AgentOverrideConfigSchema = z2.object({
18690
18724
  model: z2.union([
18691
18725
  z2.string(),
@@ -18704,13 +18738,15 @@ var AgentOverrideConfigSchema = z2.object({
18704
18738
  prompt: z2.string().min(1).optional(),
18705
18739
  orchestratorPrompt: z2.string().min(1).optional(),
18706
18740
  options: z2.record(z2.string(), z2.unknown()).optional(),
18707
- displayName: z2.string().min(1).optional()
18741
+ displayName: z2.string().min(1).optional(),
18742
+ permission: PermissionConfigSchema.optional()
18708
18743
  }).strict();
18709
18744
  var MultiplexerTypeSchema = z2.enum([
18710
18745
  "auto",
18711
18746
  "tmux",
18712
18747
  "zellij",
18713
18748
  "herdr",
18749
+ "cmux",
18714
18750
  "none"
18715
18751
  ]);
18716
18752
  var MultiplexerLayoutSchema = z2.enum([
@@ -18756,7 +18792,7 @@ var FailoverConfigSchema = z2.object({
18756
18792
  retryDelayMs: z2.number().min(0).default(500),
18757
18793
  maxRetries: z2.number().int().min(0).default(3).describe("Number of consecutive 429/rate-limit responses tolerated on the " + "same model before aborting (or swapping to the next fallback " + "model when a chain is configured)."),
18758
18794
  retry_on_empty: z2.boolean().default(true).describe("When true (default), empty provider responses are treated as failures, " + "triggering fallback/retry. Set to false to treat them as successes."),
18759
- runtimeOverride: z2.boolean().default(true).describe("When true (default), a runtime model selected via /model that is " + "outside the configured fallback chain will still trigger the chain " + "on rate-limit errors. When false, out-of-chain runtime picks are " + "respected and the error surfaces instead of silently falling back " + "to the chain. Models that are members of the chain always fall back " + "regardless of this setting.")
18795
+ runtimeOverride: z2.boolean().optional().describe("DEPRECATED: no longer used. Previously controlled whether out-of-chain " + "runtime model picks triggered fallback. Fallback is now always " + "disabled when a user explicitly selects a model via /model.")
18760
18796
  }).strict();
18761
18797
  var CompanionConfigSchema = z2.object({
18762
18798
  enabled: z2.boolean().optional(),
@@ -18798,11 +18834,13 @@ var PluginConfigSchema = z2.object({
18798
18834
  preset: z2.string().optional(),
18799
18835
  setDefaultAgent: z2.boolean().optional(),
18800
18836
  compactSidebar: z2.boolean().optional().describe("Use the compact TUI sidebar layout. Defaults to true; set false to use the expanded layout."),
18837
+ stripOrchestratorModel: z2.boolean().optional().describe("When true, omit orchestrator.model and orchestrator.variant from the SDK config so OpenCode uses the session model selected with /model after subagent dispatch. An explicitly selected preset that sets orchestrator.model is preserved. Defaults to false."),
18801
18838
  autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
18802
18839
  presets: z2.record(z2.string(), PresetSchema).optional(),
18803
18840
  agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
18804
18841
  disabled_agents: z2.array(z2.string()).optional().describe("Agent names to disable completely. " + "Disabled agents are not instantiated and cannot be delegated to. " + "Orchestrator and council internal agents (councillor) cannot be disabled. " + "By default, 'observer' is disabled. Remove it from this list and configure a vision-capable model to enable."),
18805
- disabled_mcps: z2.array(z2.string()).optional(),
18842
+ image_routing: z2.enum(["auto", "direct"]).optional().describe("How image attachments are handled. " + "When omitted, preserves legacy conditional behavior: intercept " + 'attachments only when observer is enabled. "auto": requires ' + "observer to be enabled and saves attachments to disk before " + 'nudging delegation to @observer. "direct": always passes ' + "attachments to the orchestrator untouched."),
18843
+ disabled_mcps: z2.array(z2.string()).optional().describe("MCP server names to disable completely. Disabled servers are not " + "started and cannot be used by agents."),
18806
18844
  disabled_tools: z2.array(z2.string()).optional().describe("Tool names to disable completely. Disabled tools are not registered with OpenCode and cannot be used by agents."),
18807
18845
  disabled_skills: z2.array(z2.string()).optional().describe("Skill names to disable completely. Disabled skills are not granted to agents, even when referenced by presets or agent overrides."),
18808
18846
  multiplexer: MultiplexerConfigSchema.optional(),
@@ -18899,6 +18937,23 @@ function findConfigPathInDirs(configDirs, baseName) {
18899
18937
  }
18900
18938
  return null;
18901
18939
  }
18940
+ function validateFinalImageRouting(config, configPath, options) {
18941
+ if (config.image_routing !== "auto")
18942
+ return true;
18943
+ const disabledAgents = config.disabled_agents ?? DEFAULT_DISABLED_AGENTS;
18944
+ if (!disabledAgents.includes("observer"))
18945
+ return true;
18946
+ const message = 'image_routing "auto" requires observer to be enabled. ' + 'Remove "observer" from disabled_agents.';
18947
+ options?.onWarning?.({
18948
+ path: configPath,
18949
+ kind: "invalid-schema",
18950
+ message
18951
+ });
18952
+ if (!options?.silent) {
18953
+ console.warn(`[oh-my-opencode-slim] Invalid config: ${message}`);
18954
+ }
18955
+ return false;
18956
+ }
18902
18957
  function findPluginConfigPaths(directory) {
18903
18958
  const userConfigPath = findConfigPathInDirs(getConfigSearchDirs(), "oh-my-opencode-slim");
18904
18959
  const projectConfigBasePath = path.join(directory, ".opencode", "oh-my-opencode-slim");
@@ -18980,6 +19035,7 @@ function loadPluginConfig(directory, options) {
18980
19035
  debug: config.companion.debug ?? false
18981
19036
  };
18982
19037
  }
19038
+ validateFinalImageRouting(config, projectConfigPath ?? userConfigPath ?? "", options);
18983
19039
  return config;
18984
19040
  }
18985
19041
  function loadAgentPrompt(agentName, optionsOrPreset) {
@@ -19195,7 +19251,8 @@ var AGENT_DESCRIPTIONS = {
19195
19251
  - Permissions: read_files
19196
19252
  - Stats: 5x better decision maker, problem solver, investigator than orchestrator, 0.8x speed of orchestrator, same cost.
19197
19253
  - Capabilities: Deep architectural reasoning, system-level trade-offs, complex debugging, code review, simplification, maintainability review
19198
- - **Delegate when:** Major architectural decisions with long-term impact • Problems persisting after 2+ fix attempts • High-risk multi-system refactors • Costly trade-offs (performance vs maintainability) • Complex debugging with unclear root cause • Security/scalability/data integrity decisions • Genuinely uncertain and cost of wrong choice is high • When a workflow calls for a **reviewer** subagent • Code needs simplification or YAGNI scrutiny
19254
+ - **Delegate when:** Major architectural decisions with long-term impact • Problems persisting after 2+ fix attempts • High-risk multi-system refactors • Costly trade-offs (performance vs maintainability) • Complex debugging with unclear root cause • Security/scalability/data integrity decisions • Genuinely uncertain and cost of wrong choice is high • Code needs simplification or YAGNI scrutiny
19255
+ - **Review use:** Oracle is an escalation, not a default verification step. Request independent Oracle review only when its analysis is expected to materially reduce risk or uncertainty.
19199
19256
  - **Don't delegate when:** Routine decisions you're confident about • First bug fix attempt • Straightforward trade-offs • Tactical "how" vs strategic "should" • Time-sensitive good-enough decisions • Quick research/testing can answer
19200
19257
  - **Rule of thumb:** Need senior architect review? → @oracle. Need code review or simplification? → @oracle. Routine coordination or final synthesis? → handle directly.`,
19201
19258
  designer: `@designer
@@ -19241,13 +19298,6 @@ var AGENT_DESCRIPTIONS = {
19241
19298
  - **Rule of thumb:** Even if your model supports vision, delegate visual analysis to @observer - it isolates large image/PDF bytes from your context window, returning only concise structured text. Need exact file contents for routing? → Read only the minimal context yourself.
19242
19299
  - **IMPORTANT:** When delegating to @observer, always include the **full file path** in the prompt so it can read the file. Example: "Analyze the screenshot at /path/to/file.png - describe the UI elements and error messages."`
19243
19300
  };
19244
- var VALIDATION_ROUTING = [
19245
- "- Route UI/UX validation and review to @designer",
19246
- "- Route code review, code simplification and maintainability review checks to @oracle",
19247
- "- Route implementation to @fixer or multiple @fixer instances for maximum parallel execution",
19248
- "- Route visual/media analysis and interpretation to @observer",
19249
- "- If a request spans multiple lanes, delegate only the lanes that add clear value"
19250
- ];
19251
19301
  var PARALLEL_DELEGATION_EXAMPLES = [
19252
19302
  "- Multiple @explorer searches across different domains?",
19253
19303
  "- @explorer + @librarian research in parallel?",
@@ -19257,13 +19307,6 @@ var PARALLEL_DELEGATION_EXAMPLES = [
19257
19307
  function buildOrchestratorPrompt(disabledAgents) {
19258
19308
  const enabledAgents = Object.entries(AGENT_DESCRIPTIONS).filter(([name]) => !disabledAgents?.has(name)).map(([, desc]) => desc).join(`
19259
19309
 
19260
- `);
19261
- const enabledValidationRouting = VALIDATION_ROUTING.filter((line) => {
19262
- const mentions = [...line.matchAll(/@(\w+)/g)].map((m) => m[1]);
19263
- if (mentions.length === 0)
19264
- return true;
19265
- return mentions.every((name) => !disabledAgents?.has(name));
19266
- }).join(`
19267
19310
  `);
19268
19311
  const enabledParallelExamples = PARALLEL_DELEGATION_EXAMPLES.filter((line) => {
19269
19312
  const mentions = [...line.matchAll(/@(\w+)/g)].map((m) => m[1]);
@@ -19275,6 +19318,10 @@ function buildOrchestratorPrompt(disabledAgents) {
19275
19318
  return `<Role>
19276
19319
  You are a workflow manager for coding work. Your job is to plan, schedule, delegate, monitor, reconcile, and verify specialist-agent work. You are not the default implementation worker.
19277
19320
 
19321
+ For non-trivial coding work, identify separable lanes first and delegate bounded work to the appropriate specialist. Do not perform multi-step implementation serially when a suitable specialist is available.
19322
+
19323
+ Handle work directly only when it is one isolated, clear, low-risk action and delegation overhead exceeds doing it yourself.
19324
+
19278
19325
  Optimize for quality, speed, cost, and reliability by dispatching the right specialist lanes, tracking background task state, and integrating terminal results into one coherent outcome.
19279
19326
  You have perfect understanding of agent's context management, understand well the cost of building content and reusing context of existing agents when it's best or when it's best to spawn a new agent.
19280
19327
  </Role>
@@ -19295,12 +19342,17 @@ Evaluate approach by: quality, speed and cost.
19295
19342
  Choose the path that optimizes all four.
19296
19343
 
19297
19344
  ## 3. Delegation Check
19298
- Review available agents and lane rules.
19345
+ Review available agents and lane rules. Before beginning non-trivial work, identify which parts can proceed independently.
19346
+
19347
+ **Routing threshold:**
19348
+ - Handle directly only for one isolated, clear, low-risk action where delegation would cost more than execution.
19349
+ - For multi-step implementation, broad discovery, external research, visual work, or complex debugging, delegate to the suitable specialist.
19350
+ - If two or more parts can proceed independently, dispatch them in parallel before starting dependent work.
19351
+ - Do not delegate merely because an agent exists. Do not keep substantive work entirely in the orchestrator merely because each individual step seems easy.
19299
19352
 
19300
19353
  **Dispatch efficiency:**
19301
19354
  - Reference paths/lines, don't paste files (\`src/app.ts:42\` not full contents)
19302
19355
  - Brief user on delegation goal before each call
19303
- - For trivial conversational answers or tiny mechanical edits, direct execution is allowed when scheduling overhead would clearly dominate
19304
19356
  - Record task IDs, state, and advisory ownership/dependency labels
19305
19357
  - Do not immediately wait after spawning independent background tasks unless the next step truly depends on their result
19306
19358
  - Reconcile results, resolve conflicts, and gate dependent lanes
@@ -19308,7 +19360,7 @@ Review available agents and lane rules.
19308
19360
  ${WRITABLE_FILE_OPERATIONS_RULES}
19309
19361
 
19310
19362
  ## 4. Plan and Parallelize
19311
- Build a short work graph before dispatching:
19363
+ When the routing threshold calls for delegation, build a short work graph before dispatching:
19312
19364
  - Independent lanes that can run now
19313
19365
  - Dependency-ordered lanes that must wait
19314
19366
  - Advisory ownership for write-capable lanes
@@ -19326,7 +19378,7 @@ Balance: respect dependencies, avoid parallelizing what must be sequential, and
19326
19378
 
19327
19379
  ### Background Task Discipline
19328
19380
  - Prefer \`task(..., background: true)\` for delegated work that can run independently.
19329
- - Launch specialist agents in the background by default so the orchestrator stays unblocked and can reconcile results when they return.
19381
+ - For work already chosen for delegation, launch independent specialist lanes in the background so the orchestrator stays unblocked and can reconcile results when they return.
19330
19382
  - Track each task's specialist, objective, task/session ID, and file/topic ownership.
19331
19383
  - Continue orchestration only on non-overlapping work; otherwise briefly report what was launched and stop.
19332
19384
  - Before local edits or another writer task, compare against running task scopes.
@@ -19351,16 +19403,14 @@ Balance: respect dependencies, avoid parallelizing what must be sequential, and
19351
19403
  - If the Background Job Board lists \`fix-1 / ses_abc / fixer\`, call task with \`subagent_type: "fixer"\` and \`task_id: "fix-1"\` or \`task_id: "ses_abc"\`.
19352
19404
  - Do not leave \`task_id\` empty when intending to reuse; omitted or empty \`task_id\` creates a new specialist session.
19353
19405
 
19354
- ### Validation routing
19355
- - Validation is a workflow stage owned by the Orchestrator, not a separate specialist
19356
- ${enabledValidationRouting}
19357
-
19358
19406
  ## 6. Verify
19359
- - Run relevant checks/diagnostics for the change
19360
- - Use validation routing when applicable instead of doing all review work yourself
19361
- - If test files are involved, prefer @fixer for bounded test changes and @oracle only for test strategy or quality review
19362
- - Confirm specialists completed successfully
19363
- - Verify solution meets requirements
19407
+ - Define the observable success criteria from the user's request.
19408
+ - Choose the minimum verification that produces meaningful evidence for the change's scope, risk, uncertainty, and potential impact.
19409
+ - Start with the narrowest relevant validation. Broaden verification only when integration scope, uncertainty, risk, or a failed focused check justifies it.
19410
+ - Do not run project-wide checks by habit or merely because files changed.
19411
+ - Do not treat verification as a fixed checklist; select evidence that can actually confirm the requested behavior.
19412
+ - Request independent review only when its expected risk reduction justifies its coordination cost.
19413
+ - Report what was verified and any material remaining uncertainty.
19364
19414
 
19365
19415
  </Workflow>
19366
19416
 
@@ -19376,6 +19426,8 @@ ${enabledValidationRouting}
19376
19426
  - Don't summarize what you did unless asked
19377
19427
  - Don't explain code unless asked
19378
19428
  - One-word answers are fine when appropriate
19429
+ - Default to the minimum response that fully resolves the user's request; expand only when detail is necessary or the user asks for it.
19430
+ - Do not restate the user's request or narrate routine work.
19379
19431
  - Brief delegation notices: "Checking docs via @librarian..." not "I'm going to delegate to @librarian because..."
19380
19432
 
19381
19433
  ## No Flattery
@@ -20045,6 +20097,9 @@ function applyOverrides(agent, override) {
20045
20097
  if (override.displayName) {
20046
20098
  agent.displayName = override.displayName;
20047
20099
  }
20100
+ if (override.permission) {
20101
+ agent.config.permission = override.permission;
20102
+ }
20048
20103
  }
20049
20104
  function isKnownAgentName(name) {
20050
20105
  return ALL_AGENT_NAMES.includes(name);
@@ -20085,6 +20140,9 @@ function injectDisplayNames(orchestrator, nameMap) {
20085
20140
  orchestrator.config.prompt = prompt;
20086
20141
  }
20087
20142
  function applyDefaultPermissions(agent, configuredSkills, disabledSkills) {
20143
+ if (typeof agent.config.permission === "string") {
20144
+ return;
20145
+ }
20088
20146
  const existing = agent.config.permission ?? {};
20089
20147
  const skillPermissions = getSkillPermissionsForAgent(agent.name, configuredSkills, disabledSkills);
20090
20148
  const questionPerm = existing.question === "deny" ? "deny" : "allow";
@@ -20231,10 +20289,10 @@ function createAgents(config, options) {
20231
20289
  const defaultOrchestratorPrompt = orchestrator.config.prompt ?? "";
20232
20290
  const baseOrchestratorPrompt = inlineOrchestratorPrompt !== undefined ? inlineOrchestratorPrompt : defaultOrchestratorPrompt;
20233
20291
  orchestrator.config.prompt = resolvePrompt(baseOrchestratorPrompt, orchestratorPrompts.prompt, orchestratorPrompts.appendPrompt);
20234
- applyDefaultPermissions(orchestrator, orchestratorOverride?.skills, config?.disabled_skills);
20235
20292
  if (orchestratorOverride) {
20236
20293
  applyOverrides(orchestrator, orchestratorOverride);
20237
20294
  }
20295
+ applyDefaultPermissions(orchestrator, orchestratorOverride?.skills, config?.disabled_skills);
20238
20296
  const displayNameMap = new Map;
20239
20297
  if (orchestrator.displayName) {
20240
20298
  displayNameMap.set("orchestrator", orchestrator.displayName);
@@ -20488,7 +20546,9 @@ function acquirePidFileLock(file) {
20488
20546
  return () => {
20489
20547
  try {
20490
20548
  rmSync(lock, { recursive: true, force: true });
20491
- } catch {}
20549
+ } catch (err) {
20550
+ log("[companion] lock release failed", String(err));
20551
+ }
20492
20552
  };
20493
20553
  } catch (err) {
20494
20554
  const code = err.code;
@@ -20516,10 +20576,13 @@ function pidFileLockHasLiveOwner(lock) {
20516
20576
  const owner = parsePidFile(readFileSync2(path3.join(lock, "owner"), "utf8"));
20517
20577
  if (owner !== null)
20518
20578
  return isProcessAlive(owner);
20519
- } catch {
20579
+ } catch (err) {
20580
+ log("[companion] lock owner check failed", String(err));
20520
20581
  try {
20521
20582
  return Date.now() - statSync2(lock).mtimeMs < 5000;
20522
- } catch {}
20583
+ } catch (err2) {
20584
+ log("[companion] lock owner check failed", String(err2));
20585
+ }
20523
20586
  }
20524
20587
  return false;
20525
20588
  }
@@ -20541,7 +20604,9 @@ function readState() {
20541
20604
  if (parsed?.version === 1 && Array.isArray(parsed.sessions)) {
20542
20605
  return parsed;
20543
20606
  }
20544
- } catch {}
20607
+ } catch (err) {
20608
+ log("[companion] state load failed", String(err));
20609
+ }
20545
20610
  return { version: 1, sessions: [] };
20546
20611
  }
20547
20612
  function writeState(mutator) {
@@ -20570,7 +20635,9 @@ function acquireStateLock(file) {
20570
20635
  return () => {
20571
20636
  try {
20572
20637
  rmSync(lock, { recursive: true, force: true });
20573
- } catch {}
20638
+ } catch (err) {
20639
+ log("[companion] lock release failed", String(err));
20640
+ }
20574
20641
  };
20575
20642
  } catch (err) {
20576
20643
  const code = err.code;
@@ -20605,7 +20672,9 @@ class CompanionManager {
20605
20672
  writeState((state) => {
20606
20673
  state.sessions = state.sessions.filter((s) => s.session_id !== this.id);
20607
20674
  });
20608
- } catch {}
20675
+ } catch (err) {
20676
+ log("[companion] status update failed", String(err));
20677
+ }
20609
20678
  return;
20610
20679
  }
20611
20680
  this.registerActiveManager();
@@ -20643,9 +20712,7 @@ class CompanionManager {
20643
20712
  return;
20644
20713
  }
20645
20714
  if (status === "busy") {
20646
- if (!agent)
20647
- return;
20648
- this.busyAgentSessions.set(sessionId, agent);
20715
+ this.busyAgentSessions.set(sessionId, agent ?? sessionId);
20649
20716
  } else {
20650
20717
  this.busyAgentSessions.delete(sessionId);
20651
20718
  }
@@ -20677,7 +20744,9 @@ class CompanionManager {
20677
20744
  if (activeManagers.size === 0 && activeExitListener) {
20678
20745
  try {
20679
20746
  process.removeListener("exit", activeExitListener);
20680
- } catch {}
20747
+ } catch (err) {
20748
+ log("[companion] exit listener removal failed", String(err));
20749
+ }
20681
20750
  activeExitListener = null;
20682
20751
  }
20683
20752
  if (this.config?.enabled !== true)
@@ -20835,7 +20904,9 @@ class CompanionManager {
20835
20904
  if (spawnedChild && !this.wasSpawner) {
20836
20905
  try {
20837
20906
  spawnedChild.kill();
20838
- } catch {}
20907
+ } catch (killErr) {
20908
+ log("[companion] spawn failed", String(killErr));
20909
+ }
20839
20910
  }
20840
20911
  log("[companion] spawn guard failed", String(err));
20841
20912
  } finally {
@@ -20914,7 +20985,9 @@ function loadCompanionManifestFromPackageRoot(packageRoot) {
20914
20985
  checksums: parsed.checksums
20915
20986
  };
20916
20987
  }
20917
- } catch {}
20988
+ } catch (err) {
20989
+ log("[updater] manifest read failed", String(err));
20990
+ }
20918
20991
  return null;
20919
20992
  }
20920
20993
  async function ensureCompanionVersion(options) {
@@ -21082,7 +21155,9 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
21082
21155
  if (tempDir) {
21083
21156
  try {
21084
21157
  rmSync2(tempDir, { recursive: true, force: true });
21085
- } catch {}
21158
+ } catch (err) {
21159
+ log("[updater] install cleanup failed", String(err));
21160
+ }
21086
21161
  }
21087
21162
  }
21088
21163
  }
@@ -21092,7 +21167,9 @@ function readInstallMetadata(binaryPath) {
21092
21167
  if (parsed?.version && parsed.tag && parsed.target) {
21093
21168
  return parsed;
21094
21169
  }
21095
- } catch {}
21170
+ } catch (err) {
21171
+ log("[updater] metadata read failed", String(err));
21172
+ }
21096
21173
  return null;
21097
21174
  }
21098
21175
  function writeInstallMetadata(binaryPath, metadata) {
@@ -21114,7 +21191,9 @@ async function withCompanionInstallLock(binaryPath, timeoutMs, staleMs, run) {
21114
21191
  } finally {
21115
21192
  try {
21116
21193
  rmSync2(lock, { recursive: true, force: true });
21117
- } catch {}
21194
+ } catch (err) {
21195
+ log("[updater] lock release failed", String(err));
21196
+ }
21118
21197
  }
21119
21198
  } catch (err) {
21120
21199
  const code = err.code;
@@ -21185,6 +21264,24 @@ function setActiveRuntimePresetWithPrevious(name) {
21185
21264
  activeRuntimePreset = name;
21186
21265
  }
21187
21266
 
21267
+ // src/config/strip-orchestrator-model.ts
21268
+ function isRecord(value) {
21269
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21270
+ }
21271
+ function stripOrchestratorModel(agents, enabled, preset) {
21272
+ if (enabled !== true || preset?.orchestrator?.model !== undefined)
21273
+ return;
21274
+ const orchestrator = agents.orchestrator;
21275
+ if (!isRecord(orchestrator))
21276
+ return;
21277
+ delete orchestrator.model;
21278
+ delete orchestrator.variant;
21279
+ }
21280
+ function applyOrchestratorModelConfig(input) {
21281
+ const presetName = input.runtimePreset ?? input.configPreset;
21282
+ stripOrchestratorModel(input.agents, input.enabled, presetName ? input.presets?.[presetName] : undefined);
21283
+ }
21284
+
21188
21285
  // src/council/council-manager.ts
21189
21286
  class CouncilManager {
21190
21287
  client;
@@ -24696,9 +24793,9 @@ class BackgroundJobBoard {
24696
24793
  readContextMinLines;
24697
24794
  readContextMaxFiles;
24698
24795
  constructor(options = {}) {
24699
- this.maxReusablePerAgent = options.maxReusablePerAgent ?? 2;
24700
- this.readContextMinLines = options.readContextMinLines ?? 10;
24701
- this.readContextMaxFiles = options.readContextMaxFiles ?? 8;
24796
+ this.maxReusablePerAgent = options.maxReusablePerAgent ?? DEFAULT_MAX_SESSIONS_PER_AGENT;
24797
+ this.readContextMinLines = options.readContextMinLines ?? DEFAULT_READ_CONTEXT_MIN_LINES;
24798
+ this.readContextMaxFiles = options.readContextMaxFiles ?? DEFAULT_READ_CONTEXT_MAX_FILES;
24702
24799
  }
24703
24800
  addTerminalStateListener(listener) {
24704
24801
  this.terminalStateListeners.push(listener);
@@ -25239,7 +25336,7 @@ class BackgroundJobCoordinator {
25239
25336
  }
25240
25337
  }
25241
25338
  // src/utils/guards.ts
25242
- function isRecord(value) {
25339
+ function isRecord2(value) {
25243
25340
  return typeof value === "object" && value !== null;
25244
25341
  }
25245
25342
 
@@ -25256,10 +25353,10 @@ ${SLIM_INTERNAL_INITIATOR_MARKER}`,
25256
25353
  };
25257
25354
  }
25258
25355
  function isInternalInitiatorPart(part) {
25259
- if (!isRecord(part) || part.type !== "text") {
25356
+ if (!isRecord2(part) || part.type !== "text") {
25260
25357
  return false;
25261
25358
  }
25262
- if (part.synthetic !== true || !isRecord(part.metadata)) {
25359
+ if (part.synthetic !== true || !isRecord2(part.metadata)) {
25263
25360
  return false;
25264
25361
  }
25265
25362
  return part.metadata[INTERNAL_INITIATOR_METADATA_KEY] === true;
@@ -25318,6 +25415,17 @@ function createChatHeadersHook(ctx) {
25318
25415
  }
25319
25416
  };
25320
25417
  }
25418
+ // src/hooks/command-hook-utils.ts
25419
+ function registerCommandHook(opencodeConfig, commandName, template, description) {
25420
+ const cmdConfig = opencodeConfig.command;
25421
+ if (cmdConfig?.[commandName])
25422
+ return false;
25423
+ if (!opencodeConfig.command)
25424
+ opencodeConfig.command = {};
25425
+ opencodeConfig.command[commandName] = { template, description };
25426
+ return true;
25427
+ }
25428
+
25321
25429
  // src/hooks/deepwork/index.ts
25322
25430
  var COMMAND_NAME = "deepwork";
25323
25431
  function activationPrompt(task2) {
@@ -25325,6 +25433,7 @@ function activationPrompt(task2) {
25325
25433
  "Use the deepwork skill for this task. Treat it as a heavy coding session.",
25326
25434
  "",
25327
25435
  "Deepwork requirements:",
25436
+ "- before planning, delegation, or creating state, inspect existing `.gitignore` and `.ignore`; add only missing entries without duplicates: `.gitignore` must contain `.slim/deepwork/`, and `.ignore` must contain `!.slim/deepwork/` and `!.slim/deepwork/**`; this keeps state git-local yet OpenCode-readable;",
25328
25437
  "- create/update a `.slim/deepwork/` progress file;",
25329
25438
  "- keep OpenCode todos synced with the current phase;",
25330
25439
  "- draft a plan and get `@oracle` review before implementation;",
@@ -25342,15 +25451,7 @@ function activationPrompt(task2) {
25342
25451
  function createDeepworkCommandHook() {
25343
25452
  return {
25344
25453
  registerCommand: (opencodeConfig) => {
25345
- const commandConfig = opencodeConfig.command;
25346
- if (commandConfig?.[COMMAND_NAME])
25347
- return;
25348
- if (!opencodeConfig.command)
25349
- opencodeConfig.command = {};
25350
- opencodeConfig.command[COMMAND_NAME] = {
25351
- template: "Start a deepwork session for a complex coding task",
25352
- description: "Use the deepwork workflow for heavy multi-phase coding work"
25353
- };
25454
+ registerCommandHook(opencodeConfig, COMMAND_NAME, "Start a deepwork session for a complex coding task", "Use the deepwork workflow for heavy multi-phase coding work");
25354
25455
  },
25355
25456
  handleCommandExecuteBefore: async (input, output) => {
25356
25457
  if (input.command !== COMMAND_NAME)
@@ -25565,7 +25666,7 @@ function createFilterAvailableSkillsHook(_ctx, config) {
25565
25666
  };
25566
25667
  }
25567
25668
  // src/hooks/foreground-fallback/index.ts
25568
- var RATE_LIMIT_PATTERNS = [
25669
+ var RETRYABLE_ERROR_PATTERNS = [
25569
25670
  /\b429\b/,
25570
25671
  /rate.?limit/i,
25571
25672
  /too many requests/i,
@@ -25579,22 +25680,76 @@ var RATE_LIMIT_PATTERNS = [
25579
25680
  /insufficient.?(quota|balance)/i,
25580
25681
  /high concurrency/i,
25581
25682
  /reduce concurrency/i,
25582
- /service unavailable/i,
25583
25683
  /monthly usage limit/i,
25584
25684
  /5-hour usage limit/i,
25585
- /weekly usage limit/i
25685
+ /weekly usage limit/i,
25686
+ /\b403\b/,
25687
+ /forbidden/i,
25688
+ /blocked by gateway/i
25689
+ ];
25690
+ var OUTAGE_STATUS_CODES = new Set([500, 502, 503, 504]);
25691
+ var TRANSPORT_CODES = new Set([
25692
+ "ECONNREFUSED",
25693
+ "ECONNRESET",
25694
+ "ENOTFOUND",
25695
+ "ETIMEDOUT",
25696
+ "EAI_AGAIN"
25697
+ ]);
25698
+ var TRANSPORT_MESSAGE_PATTERNS = [
25699
+ /^fetch failed$/i,
25700
+ /^socket hang up$/i,
25701
+ /^provider request timeout$/i,
25702
+ /^request timeout$/i,
25703
+ /^connect ECONNREFUSED\b/i,
25704
+ /^getaddrinfo ENOTFOUND\b/i
25586
25705
  ];
25587
- function isRateLimitError(error) {
25588
- if (!error || typeof error !== "object")
25706
+ var PROVIDER_OUTAGE_PATTERNS = [
25707
+ /\binternal server error\b/i,
25708
+ /\bbad gateway\b/i,
25709
+ /\bgateway timeout\b/i,
25710
+ /\bservice unavailable\b/i,
25711
+ /\bupstream outage\b/i,
25712
+ /\bprovider outage\b/i,
25713
+ /\bprovider unavailable\b/i
25714
+ ];
25715
+ function extractStatusCode(error) {
25716
+ const value = error.statusCode ?? error.data?.statusCode;
25717
+ return typeof value === "number" ? value : undefined;
25718
+ }
25719
+ function eventSessionID(props) {
25720
+ return props.sessionID ?? props.info?.id;
25721
+ }
25722
+ function isFailoverError(error) {
25723
+ if (!error)
25724
+ return false;
25725
+ if (typeof error === "string") {
25726
+ return RETRYABLE_ERROR_PATTERNS.some((pattern) => pattern.test(error)) || PROVIDER_OUTAGE_PATTERNS.some((pattern) => pattern.test(error)) || TRANSPORT_MESSAGE_PATTERNS.some((pattern) => pattern.test(error));
25727
+ }
25728
+ if (typeof error !== "object")
25589
25729
  return false;
25590
25730
  const err = error;
25731
+ const statusCode = extractStatusCode(err);
25732
+ if (statusCode === 429 || statusCode === 403 || statusCode !== undefined && OUTAGE_STATUS_CODES.has(statusCode)) {
25733
+ return true;
25734
+ }
25735
+ if ([err.code, err.cause?.code, err.data?.code].some((code) => typeof code === "string" && TRANSPORT_CODES.has(code))) {
25736
+ return true;
25737
+ }
25738
+ const messages = [
25739
+ err.message ?? "",
25740
+ err.data?.message ?? "",
25741
+ err.data?.responseBody ?? ""
25742
+ ];
25743
+ if (messages.some((message) => TRANSPORT_MESSAGE_PATTERNS.some((p) => p.test(message)))) {
25744
+ return true;
25745
+ }
25591
25746
  const text = [
25592
25747
  err.message ?? "",
25593
- String(err.data?.statusCode ?? ""),
25594
25748
  err.data?.message ?? "",
25595
25749
  err.data?.responseBody ?? ""
25596
25750
  ].join(" ");
25597
- return RATE_LIMIT_PATTERNS.some((p) => p.test(text));
25751
+ const hasFailoverReason = RETRYABLE_ERROR_PATTERNS.some((p) => p.test(text)) || PROVIDER_OUTAGE_PATTERNS.some((p) => p.test(text));
25752
+ return hasFailoverReason;
25598
25753
  }
25599
25754
  var DEDUP_WINDOW_MS = 5000;
25600
25755
  var REPROMPT_DELAY_MS = 500;
@@ -25604,7 +25759,6 @@ class ForegroundFallbackManager {
25604
25759
  chains;
25605
25760
  enabled;
25606
25761
  maxRetries;
25607
- runtimeOverride;
25608
25762
  sessionModel = new Map;
25609
25763
  sessionAgent = new Map;
25610
25764
  sessionTried = new Map;
@@ -25615,18 +25769,26 @@ class ForegroundFallbackManager {
25615
25769
  isFallbackInProgress(sessionID) {
25616
25770
  return this.inProgress.has(sessionID);
25617
25771
  }
25618
- constructor(client, chains, enabled, maxRetries = 3, coordinator, runtimeOverride = true) {
25772
+ disableChain(agentName) {
25773
+ this.chains[agentName] = [];
25774
+ }
25775
+ registerSessionAgent(sessionID, agentName) {
25776
+ const normalizedAgentName = agentName.trim();
25777
+ if (!sessionID || !normalizedAgentName || this.sessionAgent.has(sessionID)) {
25778
+ return;
25779
+ }
25780
+ this.sessionAgent.set(sessionID, normalizedAgentName);
25781
+ }
25782
+ constructor(client, chains, enabled, maxRetries = 3, coordinator) {
25619
25783
  this.client = client;
25620
25784
  this.chains = chains;
25621
25785
  this.enabled = enabled;
25622
25786
  this.maxRetries = maxRetries;
25623
- this.runtimeOverride = runtimeOverride;
25624
25787
  if (coordinator) {
25625
25788
  coordinator.onSessionDeleted((id) => {
25626
25789
  this.sessionModel.delete(id);
25627
25790
  this.sessionAgent.delete(id);
25628
25791
  this.sessionTried.delete(id);
25629
- this.inProgress.delete(id);
25630
25792
  this.lastTrigger.delete(id);
25631
25793
  this.lastTriggerModel.delete(id);
25632
25794
  this.sessionRetries.delete(id);
@@ -25648,13 +25810,13 @@ class ForegroundFallbackManager {
25648
25810
  if (!sessionID)
25649
25811
  break;
25650
25812
  if (typeof info.agent === "string") {
25651
- this.sessionAgent.set(sessionID, info.agent);
25813
+ this.registerSessionAgent(sessionID, info.agent);
25652
25814
  }
25653
25815
  if (typeof info.providerID === "string" && typeof info.modelID === "string") {
25654
25816
  this.sessionModel.set(sessionID, `${info.providerID}/${info.modelID}`);
25655
25817
  }
25656
- if (info.error && isRateLimitError(info.error)) {
25657
- if (this.shouldIntervene(sessionID)) {
25818
+ if (info.error && isFailoverError(info.error)) {
25819
+ if (this.shouldTriggerFallback(sessionID)) {
25658
25820
  await this.tryFallback(sessionID);
25659
25821
  }
25660
25822
  } else {
@@ -25664,29 +25826,46 @@ class ForegroundFallbackManager {
25664
25826
  }
25665
25827
  case "session.error": {
25666
25828
  const props = event.properties;
25667
- if (props?.sessionID && props.error && isRateLimitError(props.error) && this.shouldIntervene(props.sessionID)) {
25668
- await this.tryFallback(props.sessionID);
25829
+ if (!props)
25830
+ break;
25831
+ const sessionID = eventSessionID(props);
25832
+ if (sessionID && props.error && isFailoverError(props.error) && this.shouldTriggerFallback(sessionID)) {
25833
+ await this.tryFallback(sessionID);
25669
25834
  }
25670
25835
  break;
25671
25836
  }
25672
25837
  case "session.status": {
25673
25838
  const props = event.properties;
25674
- if (!props?.sessionID || !props.status?.message)
25839
+ if (!props)
25675
25840
  break;
25676
- const msg = props.status.message.toLowerCase();
25677
- if (msg.includes("rate limit") || msg.includes("usage limit") || msg.includes("usage exceeded") || msg.includes("quota exceeded") || msg.includes("exceededbudget") || msg.includes("over budget") || msg.includes("insufficient") || msg.includes("high concurrency") || msg.includes("reduce concurrency")) {
25678
- if (this.checkRetryBudget(props.sessionID)) {
25679
- await this.tryFallback(props.sessionID);
25841
+ const sessionID = eventSessionID(props);
25842
+ if (!sessionID)
25843
+ break;
25844
+ const isFailoverRetry = props.status?.type === "retry" && (isFailoverError(props.error) || props.status.message !== undefined && isFailoverError({ message: props.status.message }));
25845
+ if (isFailoverRetry) {
25846
+ const prevModel = this.lastTriggerModel.get(sessionID);
25847
+ const curModel = this.sessionModel.get(sessionID);
25848
+ const lastTriggerTime = this.lastTrigger.get(sessionID) ?? 0;
25849
+ const attempt = props.status?.attempt ?? 1;
25850
+ const modelChanged = prevModel !== undefined && curModel !== undefined && prevModel !== curModel;
25851
+ const withinDedupWindow = Date.now() - lastTriggerTime < DEDUP_WINDOW_MS;
25852
+ if (modelChanged && withinDedupWindow && attempt > 1) {
25853
+ break;
25680
25854
  }
25681
- } else {
25682
- this.sessionRetries.delete(props.sessionID);
25855
+ if (this.shouldTriggerFallback(sessionID)) {
25856
+ await this.tryFallbackWithAbort(sessionID);
25857
+ }
25858
+ break;
25859
+ }
25860
+ if (this.isRecoveredStatus(props.status?.type)) {
25861
+ this.sessionRetries.delete(sessionID);
25683
25862
  }
25684
25863
  break;
25685
25864
  }
25686
25865
  case "subagent.session.created": {
25687
25866
  const props = event.properties;
25688
25867
  if (props?.sessionID && typeof props.agentName === "string") {
25689
- this.sessionAgent.set(props.sessionID, props.agentName);
25868
+ this.registerSessionAgent(props.sessionID, props.agentName);
25690
25869
  }
25691
25870
  break;
25692
25871
  }
@@ -25702,7 +25881,7 @@ class ForegroundFallbackManager {
25702
25881
  }
25703
25882
  }
25704
25883
  }
25705
- checkRetryBudget(sessionID) {
25884
+ consumeRetryBudget(sessionID) {
25706
25885
  const tried = this.sessionRetries.get(sessionID) ?? 0;
25707
25886
  if (tried < this.maxRetries - 1) {
25708
25887
  this.sessionRetries.set(sessionID, tried + 1);
@@ -25716,27 +25895,57 @@ class ForegroundFallbackManager {
25716
25895
  this.sessionRetries.delete(sessionID);
25717
25896
  return true;
25718
25897
  }
25719
- shouldIntervene(sessionID) {
25898
+ shouldTriggerFallback(sessionID) {
25720
25899
  const tried = this.sessionRetries.get(sessionID) ?? 0;
25721
25900
  if (tried === 0)
25722
25901
  return true;
25723
- return this.checkRetryBudget(sessionID);
25902
+ return this.consumeRetryBudget(sessionID);
25903
+ }
25904
+ isRecoveredStatus(statusType) {
25905
+ return statusType === "idle" || statusType === "complete" || statusType === "completed" || statusType === "success" || statusType === "terminal";
25724
25906
  }
25725
25907
  async tryFallback(sessionID) {
25726
25908
  if (!sessionID)
25727
25909
  return;
25728
25910
  if (this.inProgress.has(sessionID))
25729
25911
  return;
25912
+ if (this.isDeduped(sessionID))
25913
+ return;
25914
+ this.inProgress.add(sessionID);
25915
+ try {
25916
+ await this.execFallback(sessionID);
25917
+ } finally {
25918
+ this.inProgress.delete(sessionID);
25919
+ }
25920
+ }
25921
+ async tryFallbackWithAbort(sessionID) {
25922
+ if (!sessionID)
25923
+ return;
25924
+ if (this.inProgress.has(sessionID))
25925
+ return;
25926
+ if (this.isDeduped(sessionID))
25927
+ return;
25928
+ this.inProgress.add(sessionID);
25929
+ try {
25930
+ await abortSessionWithTimeout(this.client, sessionID);
25931
+ await this.execFallback(sessionID);
25932
+ } finally {
25933
+ this.inProgress.delete(sessionID);
25934
+ }
25935
+ }
25936
+ isDeduped(sessionID) {
25730
25937
  const now = Date.now();
25731
25938
  const curModel = this.sessionModel.get(sessionID);
25732
25939
  const modelChanged = this.lastTriggerModel.has(sessionID) && this.lastTriggerModel.get(sessionID) !== curModel;
25733
25940
  if (!modelChanged && now - (this.lastTrigger.get(sessionID) ?? 0) < DEDUP_WINDOW_MS)
25734
- return;
25941
+ return true;
25735
25942
  this.lastTrigger.set(sessionID, now);
25736
25943
  if (curModel !== undefined) {
25737
25944
  this.lastTriggerModel.set(sessionID, curModel);
25738
25945
  }
25739
- this.inProgress.add(sessionID);
25946
+ return false;
25947
+ }
25948
+ async execFallback(sessionID) {
25740
25949
  try {
25741
25950
  let currentModel = this.sessionModel.get(sessionID);
25742
25951
  const agentName = this.sessionAgent.get(sessionID);
@@ -25751,16 +25960,6 @@ class ForegroundFallbackManager {
25751
25960
  if (!currentModel && agentName && chain.length > 0) {
25752
25961
  currentModel = chain[0];
25753
25962
  }
25754
- if (!this.runtimeOverride && currentModel && !chain.includes(currentModel)) {
25755
- log("[foreground-fallback] current model not in chain, skipping fallback (runtimeOverride=false)", {
25756
- sessionID,
25757
- agentName,
25758
- currentModel,
25759
- chain
25760
- });
25761
- await abortSessionWithTimeout(this.client, sessionID);
25762
- return;
25763
- }
25764
25963
  if (!this.sessionTried.has(sessionID)) {
25765
25964
  this.sessionTried.set(sessionID, new Set);
25766
25965
  }
@@ -25820,10 +26019,15 @@ class ForegroundFallbackManager {
25820
26019
  log("[foreground-fallback] promptAsync unavailable", { sessionID });
25821
26020
  return;
25822
26021
  }
26022
+ const promptBody = {
26023
+ parts: lastUser.parts,
26024
+ model: ref,
26025
+ ...agentName ? { agent: agentName } : {}
26026
+ };
25823
26027
  try {
25824
26028
  await sessionClient.promptAsync({
25825
26029
  path: { id: sessionID },
25826
- body: { parts: lastUser.parts, model: ref }
26030
+ body: promptBody
25827
26031
  });
25828
26032
  } catch (_promptErr) {
25829
26033
  log("[foreground-fallback] promptAsync on busy session, aborting", {
@@ -25833,7 +26037,7 @@ class ForegroundFallbackManager {
25833
26037
  await new Promise((r) => setTimeout(r, REPROMPT_DELAY_MS));
25834
26038
  await sessionClient.promptAsync({
25835
26039
  path: { id: sessionID },
25836
- body: { parts: lastUser.parts, model: ref }
26040
+ body: promptBody
25837
26041
  });
25838
26042
  }
25839
26043
  this.sessionModel.set(sessionID, nextModel);
@@ -25848,8 +26052,6 @@ class ForegroundFallbackManager {
25848
26052
  sessionID,
25849
26053
  error: err instanceof Error ? err.message : String(err)
25850
26054
  });
25851
- } finally {
25852
- this.inProgress.delete(sessionID);
25853
26055
  }
25854
26056
  }
25855
26057
  resolveChain(agentName, currentModel) {
@@ -25857,8 +26059,7 @@ class ForegroundFallbackManager {
25857
26059
  const chain = this.chains[agentName];
25858
26060
  if (chain)
25859
26061
  return chain;
25860
- if (ALL_AGENT_NAMES.includes(agentName))
25861
- return [];
26062
+ return [];
25862
26063
  }
25863
26064
  if (currentModel) {
25864
26065
  for (const chain of Object.values(this.chains)) {
@@ -25945,10 +26146,14 @@ function cleanupAllSessions(saveDir) {
25945
26146
  try {
25946
26147
  if (now - statSync5(fp).mtimeMs > maxAge)
25947
26148
  unlinkSync3(fp);
25948
- } catch {}
26149
+ } catch (err) {
26150
+ log("[image-hook] file cleanup failed", String(err));
26151
+ }
25949
26152
  }
25950
26153
  }
25951
- } catch {}
26154
+ } catch (err) {
26155
+ log("[image-hook] directory scan failed", String(err));
26156
+ }
25952
26157
  for (const dir of dirsToScan) {
25953
26158
  try {
25954
26159
  let isEmpty = true;
@@ -25962,16 +26167,21 @@ function cleanupAllSessions(saveDir) {
25962
26167
  } else {
25963
26168
  allRemoved = false;
25964
26169
  }
25965
- } catch {
26170
+ } catch (err) {
26171
+ log("[image-hook] file cleanup failed", String(err));
25966
26172
  allRemoved = false;
25967
26173
  }
25968
26174
  }
25969
26175
  if (!isEmpty && allRemoved) {
25970
26176
  try {
25971
26177
  rmdirSync(dir);
25972
- } catch {}
26178
+ } catch (err) {
26179
+ log("[image-hook] directory removal failed", String(err));
26180
+ }
25973
26181
  }
25974
- } catch {}
26182
+ } catch (err) {
26183
+ log("[image-hook] session cleanup failed", String(err));
26184
+ }
25975
26185
  }
25976
26186
  }
25977
26187
  function writeUniqueFile(dir, name, data, log2) {
@@ -26001,7 +26211,9 @@ function writeUniqueFile(dir, name, data, log2) {
26001
26211
  return null;
26002
26212
  }
26003
26213
  function processImageAttachments(args) {
26004
- const { messages, workDir, disabledAgents, log: log2 } = args;
26214
+ const { messages, workDir, imageRouting, disabledAgents, log: log2 } = args;
26215
+ if (imageRouting === "direct")
26216
+ return;
26005
26217
  const observerEnabled = !disabledAgents.has("observer");
26006
26218
  if (!observerEnabled)
26007
26219
  return;
@@ -26039,6 +26251,7 @@ function processImageAttachments(args) {
26039
26251
  log2(`[image-hook] failed to create target image directory: ${e}`);
26040
26252
  }
26041
26253
  const savedPaths = [];
26254
+ const savedImageParts = new Set;
26042
26255
  for (const p of imageParts) {
26043
26256
  const url = p.url;
26044
26257
  const filename = p.filename ?? p.name;
@@ -26051,14 +26264,21 @@ function processImageAttachments(args) {
26051
26264
  const ext = sanitizedFilename ? extname(sanitizedFilename) || extFromMime(decoded.mime) : extFromMime(decoded.mime);
26052
26265
  const name = `${baseName}-${hash}${ext}`;
26053
26266
  const filePath = writeUniqueFile(targetDir, name, decoded.data, log2);
26054
- if (filePath)
26267
+ if (filePath) {
26055
26268
  savedPaths.push(filePath);
26269
+ savedImageParts.add(p);
26270
+ }
26056
26271
  }
26057
26272
  }
26058
26273
  }
26059
- const pathsText = savedPaths.length > 0 ? ` Saved to: ${savedPaths.join(", ")}` : "";
26060
- log2(`[image-hook] stripping image/file parts, saving to disk${pathsText}`);
26061
- msg.parts = msg.parts.filter((p) => !isImagePart(p)).concat([
26274
+ if (savedPaths.length === 0) {
26275
+ log2("[image-hook] no images saved; leaving original parts in message");
26276
+ continue;
26277
+ }
26278
+ const pathsText = ` Saved to: ${savedPaths.join(", ")}`;
26279
+ log2(`[image-hook] saved image/file parts to disk${pathsText}`);
26280
+ log2(`[image-routing] auto mode: intercepted ${savedImageParts.size} image(s), delegating to @observer`);
26281
+ msg.parts = msg.parts.filter((p) => !savedImageParts.has(p)).concat([
26062
26282
  {
26063
26283
  type: "text",
26064
26284
  text: `[Image attachment detected.${pathsText} Your model may not support image input. Delegate to @observer with the file path(s) above so it can read the file with its read tool.]`
@@ -26161,15 +26381,7 @@ function helpPrompt() {
26161
26381
  function createLoopCommandHook() {
26162
26382
  return {
26163
26383
  registerCommand: (opencodeConfig) => {
26164
- const cfg = opencodeConfig.command;
26165
- if (cfg?.[COMMAND_NAME2])
26166
- return;
26167
- if (!opencodeConfig.command)
26168
- opencodeConfig.command = {};
26169
- opencodeConfig.command[COMMAND_NAME2] = {
26170
- template: "Run an automated execute-verify loop",
26171
- description: "Dispatch fixer, verify, iterate with file-based history on disk."
26172
- };
26384
+ registerCommandHook(opencodeConfig, COMMAND_NAME2, "Run an automated execute-verify loop", "Dispatch fixer, verify, iterate with file-based history on disk.");
26173
26385
  },
26174
26386
  handleCommandExecuteBefore: async (input, output) => {
26175
26387
  if (input.command !== COMMAND_NAME2)
@@ -26223,7 +26435,7 @@ function createPhaseReminderHook(coordinator) {
26223
26435
  if (isInternalInitiatorPart(originalPart)) {
26224
26436
  return;
26225
26437
  }
26226
- if (lastUserMessage.parts.some((part) => part.synthetic === true && isRecord(part.metadata) && part.metadata[PHASE_REMINDER_METADATA_KEY] === true)) {
26438
+ if (lastUserMessage.parts.some((part) => part.synthetic === true && isRecord2(part.metadata) && part.metadata[PHASE_REMINDER_METADATA_KEY] === true)) {
26227
26439
  return;
26228
26440
  }
26229
26441
  lastUserMessage.parts.push({
@@ -26297,18 +26509,7 @@ function createReflectCommandHook() {
26297
26509
  let shouldHandleCommand = false;
26298
26510
  return {
26299
26511
  registerCommand: (opencodeConfig) => {
26300
- const commandConfig = opencodeConfig.command;
26301
- if (commandConfig?.[COMMAND_NAME3]) {
26302
- shouldHandleCommand = false;
26303
- return;
26304
- }
26305
- if (!opencodeConfig.command)
26306
- opencodeConfig.command = {};
26307
- opencodeConfig.command[COMMAND_NAME3] = {
26308
- template: "Review repeated work and suggest workflow improvements",
26309
- description: "Use reflect to learn from repeated workflows and suggest reusable improvements"
26310
- };
26311
- shouldHandleCommand = true;
26512
+ shouldHandleCommand ||= registerCommandHook(opencodeConfig, COMMAND_NAME3, "Review repeated work and suggest workflow improvements", "Use reflect to learn from repeated workflows and suggest reusable improvements");
26312
26513
  },
26313
26514
  handleCommandExecuteBefore: async (input, output) => {
26314
26515
  if (input.command !== COMMAND_NAME3 || !shouldHandleCommand)
@@ -26534,8 +26735,10 @@ function createTaskSessionManagerHook(_ctx, options) {
26534
26735
  const terminalJobsInjectedByParent = new Map;
26535
26736
  if (options.coordinator) {
26536
26737
  options.coordinator.onSessionDeleted((sessionId) => {
26537
- backgroundJobBoard.drop(sessionId);
26538
- backgroundJobBoard.clearParent(sessionId);
26738
+ if (!options.isFallbackInProgress?.(sessionId)) {
26739
+ backgroundJobBoard.drop(sessionId);
26740
+ backgroundJobBoard.clearParent(sessionId);
26741
+ }
26539
26742
  terminalJobsInjectedByParent.delete(sessionId);
26540
26743
  taskContextTracker.clearSession(sessionId);
26541
26744
  taskContextTracker.prune(backgroundJobBoard);
@@ -26705,7 +26908,7 @@ function createTaskSessionManagerHook(_ctx, options) {
26705
26908
  sessionID: input.sessionID
26706
26909
  });
26707
26910
  }
26708
- if (!isRecord(output.args))
26911
+ if (!isRecord2(output.args))
26709
26912
  return;
26710
26913
  const args = output.args;
26711
26914
  if (typeof args.subagent_type !== "string" || args.subagent_type.trim() === "") {
@@ -26890,7 +27093,7 @@ function createTaskSessionManagerHook(_ctx, options) {
26890
27093
  if (isInternalInitiatorPart(textPart)) {
26891
27094
  return;
26892
27095
  }
26893
- if (message.parts.some((part) => part.synthetic === true && isRecord(part.metadata) && part.metadata[BACKGROUND_JOB_BOARD_METADATA_KEY] === true)) {
27096
+ if (message.parts.some((part) => part.synthetic === true && isRecord2(part.metadata) && part.metadata[BACKGROUND_JOB_BOARD_METADATA_KEY] === true)) {
26894
27097
  return;
26895
27098
  }
26896
27099
  rememberInjectedTerminalJobs(message.info.sessionID);
@@ -26964,7 +27167,7 @@ function createTaskSessionManagerHook(_ctx, options) {
26964
27167
  }
26965
27168
  if (sessionId2 && options.shouldManageSession(sessionId2)) {
26966
27169
  const props = input.event.properties;
26967
- if (!props?.error || !isRateLimitError(props.error)) {
27170
+ if (!props?.error || !isFailoverError(props.error)) {
26968
27171
  terminalJobsInjectedByParent.delete(sessionId2);
26969
27172
  }
26970
27173
  }
@@ -27035,7 +27238,7 @@ function createTaskSessionManagerHook(_ctx, options) {
27035
27238
  result: status.result
27036
27239
  });
27037
27240
  output.output = formatCancelledTaskStatusOutput(status.taskID, backgroundJobBoard.getResultSummary(status.taskID));
27038
- if (isRecord(output) && isRecord(output.metadata)) {
27241
+ if (isRecord2(output) && isRecord2(output.metadata)) {
27039
27242
  output.metadata.state = "cancelled";
27040
27243
  }
27041
27244
  }
@@ -27073,6 +27276,23 @@ import { URL as URL2 } from "node:url";
27073
27276
  import * as fsSync from "node:fs";
27074
27277
  import * as fs6 from "node:fs/promises";
27075
27278
  import * as path13 from "node:path";
27279
+
27280
+ // src/utils/frontmatter.ts
27281
+ function parseFrontmatter(content) {
27282
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
27283
+ if (!match)
27284
+ return null;
27285
+ const result = {};
27286
+ for (const line of match[1].split(/\r?\n/)) {
27287
+ const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.+?)\s*$/);
27288
+ if (!kv)
27289
+ continue;
27290
+ result[kv[1]] = kv[2].replace(/^(['"])(.*)\1$/, "$2");
27291
+ }
27292
+ return result;
27293
+ }
27294
+
27295
+ // src/interview/document.ts
27076
27296
  var DEFAULT_OUTPUT_FOLDER = "interview";
27077
27297
  function normalizeOutputFolder(outputFolder) {
27078
27298
  const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
@@ -27182,20 +27402,7 @@ function buildInterviewDocument(idea, summary, history, meta) {
27182
27402
  ].join(`
27183
27403
  `);
27184
27404
  }
27185
- function parseFrontmatter(content) {
27186
- const match = content.match(/^---\n([\s\S]*?)\n---\n/);
27187
- if (!match)
27188
- return null;
27189
- const result = {};
27190
- for (const line of match[1].split(`
27191
- `)) {
27192
- const colonIdx = line.indexOf(":");
27193
- if (colonIdx > 0) {
27194
- result[line.slice(0, colonIdx).trim()] = line.slice(colonIdx + 1).trim();
27195
- }
27196
- }
27197
- return result;
27198
- }
27405
+ var parseFrontmatter2 = parseFrontmatter;
27199
27406
  async function ensureInterviewFile(record) {
27200
27407
  await fs6.mkdir(path13.dirname(record.markdownPath), { recursive: true });
27201
27408
  try {
@@ -27333,11 +27540,13 @@ async function readJsonBody(request) {
27333
27540
  return raw ? JSON.parse(raw) : {};
27334
27541
  }
27335
27542
 
27336
- // src/interview/ui.ts
27337
- var BRAND_LOGO_URL = "https://ohmyopencodeslim.com/android-chrome-512x512.png";
27543
+ // src/utils/escape-html.ts
27338
27544
  function escapeHtml(value) {
27339
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
27545
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
27340
27546
  }
27547
+
27548
+ // src/interview/ui.ts
27549
+ var BRAND_LOGO_URL = "https://ohmyopencodeslim.com/android-chrome-512x512.png";
27341
27550
  function clipboardHelperJs() {
27342
27551
  return `
27343
27552
  function copyCommand(text, btn) {
@@ -29513,7 +29722,7 @@ data: ${JSON.stringify(formatSseState(entry))}
29513
29722
  const title = extractTitle(content) || entry.replace(/\.md$/, "");
29514
29723
  const summary = extractSummarySection(content);
29515
29724
  const baseName = entry.replace(/\.md$/, "");
29516
- const fm = parseFrontmatter(content);
29725
+ const fm = parseFrontmatter2(content);
29517
29726
  items.push({
29518
29727
  fileName: entry,
29519
29728
  resumeCommand: `/interview ${baseName}`,
@@ -29548,7 +29757,7 @@ data: ${JSON.stringify(formatSseState(entry))}
29548
29757
  } catch {
29549
29758
  continue;
29550
29759
  }
29551
- const fm = parseFrontmatter(content);
29760
+ const fm = parseFrontmatter2(content);
29552
29761
  if (!fm?.sessionID)
29553
29762
  continue;
29554
29763
  const title = extractTitle(content) || entry.replace(/\.md$/, "");
@@ -31301,6 +31510,14 @@ function createInterviewService(ctx, config, deps) {
31301
31510
  const interview = await createInterview(input.sessionID, idea);
31302
31511
  await notifyInterviewUrl(input.sessionID, interview);
31303
31512
  output.parts.push(createInternalAgentTextPart(buildKickoffPrompt(idea, maxQuestions)));
31513
+ let sessionTitle = `Interview: ${idea}`;
31514
+ if (sessionTitle.length > 50) {
31515
+ sessionTitle = `${sessionTitle.slice(0, 49)}…`;
31516
+ }
31517
+ ctx.client.session.update?.({
31518
+ path: { id: input.sessionID },
31519
+ body: { title: sessionTitle }
31520
+ })?.catch(() => {});
31304
31521
  }
31305
31522
  async function handleEvent(input) {
31306
31523
  const { event } = input;
@@ -32061,21 +32278,23 @@ function createBuiltinMcps(disabledMcps = [], websearchConfig) {
32061
32278
  return mcps;
32062
32279
  }
32063
32280
 
32064
- // src/multiplexer/herdr/index.ts
32281
+ // src/multiplexer/cmux/index.ts
32065
32282
  init_compat();
32066
32283
 
32067
32284
  // src/multiplexer/shared.ts
32068
32285
  init_compat();
32286
+ import { existsSync as existsSync9 } from "node:fs";
32287
+ import { basename as basename6, isAbsolute as isAbsolute5 } from "node:path";
32069
32288
  function quoteShellArg(value) {
32070
32289
  return `'${value.replace(/'/g, `'\\''`)}'`;
32071
32290
  }
32072
32291
  function normalizePathForShell(directory) {
32073
32292
  return process.platform === "win32" ? directory.replace(/\\/g, "/") : directory;
32074
32293
  }
32075
- function buildOpencodeAttachCommand(sessionId, serverUrl, directory) {
32294
+ function buildOpencodeAttachCommand(sessionId, serverUrl, directory, executable = "opencode") {
32076
32295
  const attachDir = normalizePathForShell(directory);
32077
32296
  return [
32078
- "opencode",
32297
+ executable === "opencode" ? executable : quoteShellArg(executable),
32079
32298
  "attach",
32080
32299
  quoteShellArg(serverUrl),
32081
32300
  "--session",
@@ -32084,6 +32303,20 @@ function buildOpencodeAttachCommand(sessionId, serverUrl, directory) {
32084
32303
  quoteShellArg(attachDir)
32085
32304
  ].join(" ");
32086
32305
  }
32306
+ function resolveHostOpencodeBinary(options = {}) {
32307
+ const pathExists = options.pathExists ?? existsSync9;
32308
+ for (const candidate of [
32309
+ options.override,
32310
+ options.envOverride ?? process.env.OPENCODE_BIN,
32311
+ options.execPath ?? process.execPath,
32312
+ options.argv0 ?? process.argv[0]
32313
+ ]) {
32314
+ if (candidate && isAbsolute5(candidate) && /^opencode(?:\.exe)?$/i.test(basename6(candidate)) && pathExists(candidate)) {
32315
+ return candidate;
32316
+ }
32317
+ }
32318
+ return null;
32319
+ }
32087
32320
  async function findBinary(binaryName, options = {}) {
32088
32321
  const isWindows = process.platform === "win32";
32089
32322
  const cmd = isWindows ? "where" : "which";
@@ -32165,50 +32398,591 @@ async function gracefulClosePane(binary, paneId, options) {
32165
32398
  }
32166
32399
  }
32167
32400
 
32168
- // src/multiplexer/herdr/index.ts
32169
- class HerdrMultiplexer {
32170
- type = "herdr";
32171
- binaryPath = null;
32172
- hasChecked = false;
32173
- parentPaneId = process.env.HERDR_PANE_ID;
32174
- layout;
32175
- paneDirection;
32176
- agentAreaPaneId = null;
32177
- constructor(layout = "main-vertical", mainPaneSize = 60) {
32178
- this.layout = layout;
32179
- this.paneDirection = getPaneDirection(layout);
32401
+ // src/multiplexer/cmux/index.ts
32402
+ var MINIMUM_VERSION = "0.64.14";
32403
+ var READINESS_DELAYS_MS = [50, 100, 200, 400, 500, 500, 250];
32404
+ var registries = new Map;
32405
+ var mutationQueue = Promise.resolve();
32406
+ var mutationSequence = 0;
32407
+
32408
+ class CmuxMultiplexer {
32409
+ client;
32410
+ type = "cmux";
32411
+ versionAvailable = false;
32412
+ availabilityError = "unavailable";
32413
+ checkSessionReady;
32414
+ delay;
32415
+ readinessAttemptTimeoutMs;
32416
+ opencodeBinary;
32417
+ constructor(client = new CliCmuxClient, options = {}) {
32418
+ this.client = client;
32419
+ this.checkSessionReady = options.checkSessionReady ?? defaultSessionReady;
32420
+ this.delay = options.delay ?? defaultDelay;
32421
+ this.readinessAttemptTimeoutMs = options.readinessAttemptTimeoutMs ?? 1000;
32422
+ this.opencodeBinary = resolveHostOpencodeBinary({
32423
+ override: options.opencodeBinary,
32424
+ pathExists: options.pathExists
32425
+ });
32180
32426
  }
32181
32427
  async isAvailable() {
32182
- if (this.hasChecked) {
32183
- return this.binaryPath !== null;
32428
+ if (this.versionAvailable)
32429
+ return true;
32430
+ const version = await this.client.version().catch(() => null);
32431
+ if (version && compareVersions2(version, MINIMUM_VERSION) >= 0) {
32432
+ this.versionAvailable = true;
32433
+ return true;
32184
32434
  }
32185
- this.binaryPath = await findBinary("herdr");
32186
- this.hasChecked = true;
32187
- return this.binaryPath !== null;
32435
+ this.availabilityError = version ? "hard" : this.client.getVersionError?.() ?? "unavailable";
32436
+ return false;
32188
32437
  }
32189
32438
  isInsideSession() {
32190
- return !!(process.env.HERDR_ENV || process.env.HERDR_PANE_ID);
32191
- }
32192
- async spawnPane(sessionId, description, serverUrl, directory) {
32193
- const herdr = await this.getBinary();
32194
- if (!herdr) {
32195
- log("[herdr] spawnPane: herdr binary not found");
32196
- return { success: false };
32439
+ return Boolean(process.env.CMUX_SOCKET_PATH && process.env.CMUX_WORKSPACE_ID && process.env.CMUX_SURFACE_ID);
32440
+ }
32441
+ async spawnPane(sessionId, _description, serverUrl, directory) {
32442
+ if (!this.opencodeBinary)
32443
+ return { success: false, error: "hard" };
32444
+ const opencodeBinary = this.opencodeBinary;
32445
+ const statusUrl = new URL("/session/status", serverUrl);
32446
+ if (!await this.waitForSession(statusUrl, sessionId)) {
32447
+ log("[cmux] spawnPane failed", {
32448
+ stage: "readinessTimeout",
32449
+ sessionId
32450
+ });
32451
+ return { success: false, error: "unavailable" };
32197
32452
  }
32198
- try {
32199
- const attachDir = normalizePathForShell(directory);
32200
- let paneId = null;
32201
- let lastRawOutput = "";
32202
- if (this.layout === "main-vertical" && this.agentAreaPaneId) {
32203
- const result = await this.runSplit([this.agentAreaPaneId], "down", attachDir);
32204
- paneId = result.paneId;
32205
- if (!paneId) {
32206
- log("[herdr] agent area split failed, falling back to parent", {
32207
- agentAreaPaneId: this.agentAreaPaneId
32208
- });
32209
- this.agentAreaPaneId = null;
32210
- }
32211
- }
32453
+ return enqueueMutation("spawn", async (sequence) => {
32454
+ if (!await this.isAvailable()) {
32455
+ log("[cmux] spawnPane failed", {
32456
+ sequence,
32457
+ stage: "version",
32458
+ sessionId
32459
+ });
32460
+ return {
32461
+ success: false,
32462
+ error: this.availabilityError
32463
+ };
32464
+ }
32465
+ const root = await this.client.identify();
32466
+ if (!root) {
32467
+ log("[cmux] spawnPane failed", {
32468
+ sequence,
32469
+ stage: "identify",
32470
+ sessionId
32471
+ });
32472
+ return {
32473
+ success: false,
32474
+ error: this.client.getIdentifyError?.() ?? "unavailable"
32475
+ };
32476
+ }
32477
+ const key = registryKey(root.socketPath, root.workspaceId);
32478
+ const registry = registries.get(key) ?? { root, agents: [] };
32479
+ registries.set(key, registry);
32480
+ const previous = registry.agents.at(-1);
32481
+ const targetSurfaceId = previous?.surfaceId ?? registry.root.surfaceId;
32482
+ const direction = previous ? "down" : "right";
32483
+ const created = await this.client.createSurface({
32484
+ workspaceId: root.workspaceId,
32485
+ targetSurfaceId,
32486
+ direction,
32487
+ focus: false
32488
+ }, root.socketPath);
32489
+ if (!created) {
32490
+ log("[cmux] spawnPane failed", {
32491
+ sequence,
32492
+ stage: "createSurface",
32493
+ sessionId,
32494
+ workspaceId: root.workspaceId,
32495
+ targetSurfaceId,
32496
+ direction
32497
+ });
32498
+ return {
32499
+ success: false,
32500
+ error: this.client.getCreateError?.() ?? "hard"
32501
+ };
32502
+ }
32503
+ const handle = {
32504
+ v: 1,
32505
+ socketPath: root.socketPath,
32506
+ workspaceId: root.workspaceId,
32507
+ paneId: created.paneId,
32508
+ surfaceId: created.surfaceId
32509
+ };
32510
+ const encodedHandle = encodeHandle(handle);
32511
+ const command = buildOpencodeAttachCommand(sessionId, serverUrl, directory, opencodeBinary);
32512
+ try {
32513
+ const started = await this.client.respawnSurface(root.workspaceId, created.surfaceId, command, root.socketPath);
32514
+ if (!started) {
32515
+ log("[cmux] spawnPane failed", {
32516
+ sequence,
32517
+ stage: "respawn",
32518
+ sessionId,
32519
+ workspaceId: root.workspaceId,
32520
+ targetSurfaceId,
32521
+ direction
32522
+ });
32523
+ throw new Error("cmux respawn-pane failed");
32524
+ }
32525
+ } catch (error) {
32526
+ log("[cmux] spawnPane respawn exception", {
32527
+ sequence,
32528
+ stage: "respawn",
32529
+ sessionId,
32530
+ workspaceId: root.workspaceId,
32531
+ targetSurfaceId,
32532
+ direction,
32533
+ errorType: errorName(error)
32534
+ });
32535
+ const cleaned = await this.cleanupPane(root.workspaceId, created.surfaceId, root.socketPath);
32536
+ return cleaned ? { success: false, error: "unavailable" } : {
32537
+ success: false,
32538
+ error: "unavailable",
32539
+ orphanPaneId: encodedHandle
32540
+ };
32541
+ }
32542
+ registry.agents.push(handle);
32543
+ await this.equalize(root.workspaceId, root.socketPath);
32544
+ return { success: true, paneId: encodeHandle(handle) };
32545
+ });
32546
+ }
32547
+ closePane(paneId) {
32548
+ const handle = decodeHandle(paneId);
32549
+ if (!handle)
32550
+ return Promise.resolve(false);
32551
+ return enqueueMutation("close", async () => {
32552
+ if (!await this.isAvailable())
32553
+ return false;
32554
+ const result = await this.client.closeSurface(handle.workspaceId, handle.surfaceId, handle.socketPath);
32555
+ if (result === "failed")
32556
+ return false;
32557
+ const key = registryKey(handle.socketPath, handle.workspaceId);
32558
+ const registry = registries.get(key);
32559
+ if (registry) {
32560
+ const index = registry.agents.findIndex((agent) => agent.surfaceId === handle.surfaceId);
32561
+ if (index >= 0)
32562
+ registry.agents.splice(index, 1);
32563
+ if (registry.agents.length === 0)
32564
+ registries.delete(key);
32565
+ }
32566
+ await this.equalize(handle.workspaceId, handle.socketPath);
32567
+ return true;
32568
+ });
32569
+ }
32570
+ async applyLayout(_layout, _mainPaneSize) {}
32571
+ async cleanupPane(workspaceId, surfaceId, socketPath) {
32572
+ try {
32573
+ const result = await this.client.closeSurface(workspaceId, surfaceId, socketPath);
32574
+ if (result === "failed") {
32575
+ log("[cmux] failed to close pre-respawn surface", {
32576
+ workspaceId,
32577
+ surfaceId
32578
+ });
32579
+ return false;
32580
+ }
32581
+ return true;
32582
+ } catch (error) {
32583
+ log("[cmux] failed to close pre-respawn surface", {
32584
+ workspaceId,
32585
+ surfaceId,
32586
+ errorType: errorName(error)
32587
+ });
32588
+ return false;
32589
+ }
32590
+ }
32591
+ async waitForSession(url, sessionId) {
32592
+ for (let attempt = 0;attempt <= READINESS_DELAYS_MS.length; attempt++) {
32593
+ const controller = new AbortController;
32594
+ const timeout = setTimeout(() => controller.abort(), this.readinessAttemptTimeoutMs);
32595
+ timeout.unref?.();
32596
+ try {
32597
+ if (await Promise.race([
32598
+ this.checkSessionReady(url, sessionId, controller.signal),
32599
+ new Promise((resolve4) => controller.signal.addEventListener("abort", () => resolve4(false), {
32600
+ once: true
32601
+ }))
32602
+ ])) {
32603
+ return true;
32604
+ }
32605
+ } catch {} finally {
32606
+ clearTimeout(timeout);
32607
+ }
32608
+ const delay2 = READINESS_DELAYS_MS[attempt];
32609
+ if (delay2 === undefined)
32610
+ return false;
32611
+ await this.delay(delay2);
32612
+ }
32613
+ return false;
32614
+ }
32615
+ async equalize(workspaceId, socketPath) {
32616
+ try {
32617
+ const success = await this.client.equalizeSplits({
32618
+ workspace_id: workspaceId,
32619
+ orientation: "vertical"
32620
+ }, socketPath);
32621
+ if (!success)
32622
+ log("[cmux] workspace.equalize_splits failed", { workspaceId });
32623
+ } catch (error) {
32624
+ log("[cmux] workspace.equalize_splits failed", {
32625
+ workspaceId,
32626
+ error: String(error)
32627
+ });
32628
+ }
32629
+ }
32630
+ }
32631
+
32632
+ class SpawnCommandRunner {
32633
+ timeoutMs;
32634
+ spawn;
32635
+ constructor(timeoutMs = 5000, spawn3 = crossSpawn) {
32636
+ this.timeoutMs = timeoutMs;
32637
+ this.spawn = spawn3;
32638
+ }
32639
+ async run(argv) {
32640
+ const proc = this.spawn(argv, { stdout: "pipe", stderr: "pipe" });
32641
+ let timeout;
32642
+ try {
32643
+ return await Promise.race([
32644
+ Promise.all([proc.exited, proc.stdout(), proc.stderr()]).then(([exitCode, stdout, stderr]) => ({ exitCode, stdout, stderr })),
32645
+ new Promise((resolve4) => {
32646
+ timeout = setTimeout(() => {
32647
+ proc.kill("SIGTERM");
32648
+ resolve4({
32649
+ exitCode: 124,
32650
+ stdout: "",
32651
+ stderr: "unavailable: cmux command timed out"
32652
+ });
32653
+ }, this.timeoutMs);
32654
+ timeout.unref?.();
32655
+ })
32656
+ ]);
32657
+ } finally {
32658
+ if (timeout)
32659
+ clearTimeout(timeout);
32660
+ }
32661
+ }
32662
+ }
32663
+
32664
+ class CliCmuxClient {
32665
+ runner;
32666
+ binary = null;
32667
+ versionError = "unavailable";
32668
+ identifyError = "unavailable";
32669
+ lastRunThrew = false;
32670
+ createError = "hard";
32671
+ constructor(runner = new SpawnCommandRunner, binary) {
32672
+ this.runner = runner;
32673
+ this.binary = binary ?? null;
32674
+ }
32675
+ async version() {
32676
+ this.versionError = "unavailable";
32677
+ const result = await this.run(["--version"]);
32678
+ if (!result || result.exitCode !== 0) {
32679
+ this.versionError = "unavailable";
32680
+ return null;
32681
+ }
32682
+ const version = result.stdout.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
32683
+ this.versionError = "hard";
32684
+ return version;
32685
+ }
32686
+ getVersionError() {
32687
+ return this.versionError;
32688
+ }
32689
+ async identify() {
32690
+ this.identifyError = "unavailable";
32691
+ const result = await this.run(["--id-format", "uuids", "identify"]);
32692
+ if (!result || result.exitCode !== 0) {
32693
+ this.identifyError = "unavailable";
32694
+ return null;
32695
+ }
32696
+ const value = asRecord(parseJson(result.stdout));
32697
+ const caller = asRecord(value?.caller);
32698
+ const focused = asRecord(value?.focused);
32699
+ const workspaceId = stringField(caller, "workspace_id") ?? stringField(focused, "workspace_id");
32700
+ const paneId = stringField(caller, "pane_id") ?? stringField(focused, "pane_id");
32701
+ const surfaceId = stringField(caller, "surface_id") ?? stringField(focused, "surface_id");
32702
+ const socketPath = stringField(value, "socket_path");
32703
+ if (workspaceId && paneId && surfaceId && socketPath) {
32704
+ return { workspaceId, paneId, surfaceId, socketPath };
32705
+ }
32706
+ log("[cmux] response parse failed", {
32707
+ operation: "identify",
32708
+ reason: value ? "missing_fields" : "invalid_json",
32709
+ stdoutLength: result.stdout.length
32710
+ });
32711
+ this.identifyError = "hard";
32712
+ return null;
32713
+ }
32714
+ getIdentifyError() {
32715
+ return this.identifyError;
32716
+ }
32717
+ async createSurface(input, socketPath) {
32718
+ this.createError = "hard";
32719
+ const result = await this.run(withSocket(socketPath, [
32720
+ "--json",
32721
+ "--id-format",
32722
+ "uuids",
32723
+ "new-split",
32724
+ input.direction,
32725
+ "--workspace",
32726
+ input.workspaceId,
32727
+ "--surface",
32728
+ input.targetSurfaceId,
32729
+ "--focus",
32730
+ "false"
32731
+ ]));
32732
+ if (!result || result.exitCode !== 0) {
32733
+ this.createError = this.lastRunThrew ? "unavailable" : classifyCreateError(result?.stderr ?? "");
32734
+ return null;
32735
+ }
32736
+ const value = asRecord(parseJson(result.stdout));
32737
+ const resultValue = asRecord(value?.result);
32738
+ const pane = asRecord(resultValue?.pane);
32739
+ const paneId = stringField(value, "pane_id") ?? stringField(resultValue, "pane_id") ?? stringField(pane, "pane_id");
32740
+ const surfaceId = stringField(value, "surface_id") ?? stringField(resultValue, "surface_id") ?? stringField(pane, "surface_id");
32741
+ if (paneId && surfaceId)
32742
+ return { paneId, surfaceId };
32743
+ log("[cmux] response parse failed", {
32744
+ operation: "new-split",
32745
+ reason: value ? "missing_fields" : "invalid_json",
32746
+ stdoutLength: result.stdout.length
32747
+ });
32748
+ return null;
32749
+ }
32750
+ getCreateError() {
32751
+ return this.createError;
32752
+ }
32753
+ async respawnSurface(workspaceId, surfaceId, command, socketPath) {
32754
+ const result = await this.run(withSocket(socketPath, [
32755
+ "respawn-pane",
32756
+ "--workspace",
32757
+ workspaceId,
32758
+ "--surface",
32759
+ surfaceId,
32760
+ "--command",
32761
+ command
32762
+ ]));
32763
+ return result?.exitCode === 0;
32764
+ }
32765
+ async closeSurface(workspaceId, surfaceId, socketPath) {
32766
+ const result = await this.run(withSocket(socketPath, [
32767
+ "close-surface",
32768
+ "--workspace",
32769
+ workspaceId,
32770
+ "--surface",
32771
+ surfaceId
32772
+ ]));
32773
+ if (result?.exitCode === 0)
32774
+ return "closed";
32775
+ return result?.stderr.toLowerCase().includes("not_found") || result?.stderr.toLowerCase().includes("not found") ? "not_found" : "failed";
32776
+ }
32777
+ async equalizeSplits(params, socketPath) {
32778
+ const result = await this.run(withSocket(socketPath, [
32779
+ "rpc",
32780
+ "workspace.equalize_splits",
32781
+ JSON.stringify(params)
32782
+ ]));
32783
+ return result?.exitCode === 0;
32784
+ }
32785
+ async run(args) {
32786
+ this.lastRunThrew = false;
32787
+ this.binary ??= await findBinary("cmux");
32788
+ if (!this.binary)
32789
+ return null;
32790
+ let result;
32791
+ try {
32792
+ result = await this.runner.run([this.binary, ...args]);
32793
+ } catch (error) {
32794
+ this.lastRunThrew = true;
32795
+ log("[cmux] command threw", {
32796
+ operation: commandOperation(args),
32797
+ errorType: errorName(error)
32798
+ });
32799
+ return null;
32800
+ }
32801
+ if (result.exitCode !== 0) {
32802
+ const operation = commandOperation(args);
32803
+ log("[cmux] command failed", {
32804
+ operation,
32805
+ exitCode: result.exitCode,
32806
+ stderr: operation === "respawn-pane" ? "[redacted: may contain attach command]" : safeSummary(result.stderr)
32807
+ });
32808
+ }
32809
+ return result;
32810
+ }
32811
+ }
32812
+ function enqueueMutation(operation, mutation) {
32813
+ const sequence = ++mutationSequence;
32814
+ log("[cmux] mutation enqueue", {
32815
+ sequence,
32816
+ operation,
32817
+ agentCount: registryAgentCount()
32818
+ });
32819
+ const run = async () => {
32820
+ log("[cmux] mutation start", {
32821
+ sequence,
32822
+ operation,
32823
+ agentCount: registryAgentCount()
32824
+ });
32825
+ try {
32826
+ return await mutation(sequence);
32827
+ } finally {
32828
+ log("[cmux] mutation end", {
32829
+ sequence,
32830
+ operation,
32831
+ agentCount: registryAgentCount()
32832
+ });
32833
+ }
32834
+ };
32835
+ const result = mutationQueue.then(run, run);
32836
+ mutationQueue = result.then(() => {
32837
+ return;
32838
+ }, () => {
32839
+ return;
32840
+ });
32841
+ return result;
32842
+ }
32843
+ function registryAgentCount() {
32844
+ let count = 0;
32845
+ for (const registry of registries.values())
32846
+ count += registry.agents.length;
32847
+ return count;
32848
+ }
32849
+ function commandOperation(args) {
32850
+ return args.find((arg) => [
32851
+ "identify",
32852
+ "new-split",
32853
+ "respawn-pane",
32854
+ "close-surface",
32855
+ "rpc"
32856
+ ].includes(arg)) ?? "version";
32857
+ }
32858
+ function withSocket(socketPath, args) {
32859
+ return socketPath ? ["--socket", socketPath, ...args] : args;
32860
+ }
32861
+ function safeSummary(value) {
32862
+ const trimmed = value.trim();
32863
+ return trimmed.length > 300 ? `${trimmed.slice(0, 300)}…` : trimmed;
32864
+ }
32865
+ function classifyCreateError(stderr) {
32866
+ const normalized = stderr.toLowerCase();
32867
+ if (normalized.includes("not_found") || normalized.includes("not found")) {
32868
+ return "not_found";
32869
+ }
32870
+ if (normalized.includes("unavailable"))
32871
+ return "unavailable";
32872
+ if (normalized.includes("invalid_state") || normalized.includes("invalid state")) {
32873
+ return "invalid_state";
32874
+ }
32875
+ return "hard";
32876
+ }
32877
+ function errorName(error) {
32878
+ return error instanceof Error ? error.name : typeof error;
32879
+ }
32880
+ async function defaultSessionReady(url, sessionId, signal) {
32881
+ const response = await fetch(url, { signal });
32882
+ if (!response.ok)
32883
+ return false;
32884
+ const statuses = await response.json();
32885
+ return ["idle", "running", "busy", "retry"].includes(statuses[sessionId]?.type ?? "");
32886
+ }
32887
+ function defaultDelay(milliseconds) {
32888
+ return new Promise((resolve4) => setTimeout(resolve4, milliseconds));
32889
+ }
32890
+ function registryKey(socketPath, workspaceId) {
32891
+ return `${socketPath}\x00${workspaceId}`;
32892
+ }
32893
+ function encodeHandle(handle) {
32894
+ return `cmux:v1:${Buffer.from(JSON.stringify(handle)).toString("base64url")}`;
32895
+ }
32896
+ function decodeHandle(value) {
32897
+ if (!value.startsWith("cmux:v1:"))
32898
+ return null;
32899
+ try {
32900
+ const parsed = JSON.parse(Buffer.from(value.slice("cmux:v1:".length), "base64url").toString());
32901
+ return parsed.v === 1 && typeof parsed.socketPath === "string" && typeof parsed.workspaceId === "string" && typeof parsed.paneId === "string" && typeof parsed.surfaceId === "string" ? parsed : null;
32902
+ } catch {
32903
+ return null;
32904
+ }
32905
+ }
32906
+ function compareVersions2(left, right) {
32907
+ const a = left.split(".").map(Number);
32908
+ const b = right.split(".").map(Number);
32909
+ for (let index = 0;index < 3; index++) {
32910
+ if (a[index] !== b[index])
32911
+ return (a[index] ?? 0) - (b[index] ?? 0);
32912
+ }
32913
+ return 0;
32914
+ }
32915
+ function parseJson(stdout) {
32916
+ try {
32917
+ return JSON.parse(stdout);
32918
+ } catch {
32919
+ return null;
32920
+ }
32921
+ }
32922
+ function asRecord(value) {
32923
+ return value && typeof value === "object" ? value : null;
32924
+ }
32925
+ function stringField(value, field) {
32926
+ const candidate = value?.[field];
32927
+ return typeof candidate === "string" ? candidate : null;
32928
+ }
32929
+ // src/multiplexer/herdr/index.ts
32930
+ init_compat();
32931
+ class HerdrMultiplexer {
32932
+ type = "herdr";
32933
+ binaryPath = null;
32934
+ hasChecked = false;
32935
+ parentPaneId = process.env.HERDR_PANE_ID;
32936
+ layout;
32937
+ paneDirection;
32938
+ agentAreaPaneId = null;
32939
+ spawnMutex = Promise.resolve();
32940
+ constructor(layout = "main-vertical", mainPaneSize = 60) {
32941
+ this.layout = layout;
32942
+ this.paneDirection = getPaneDirection(layout);
32943
+ }
32944
+ async isAvailable() {
32945
+ if (this.hasChecked) {
32946
+ return this.binaryPath !== null;
32947
+ }
32948
+ this.binaryPath = await findBinary("herdr");
32949
+ this.hasChecked = true;
32950
+ return this.binaryPath !== null;
32951
+ }
32952
+ isInsideSession() {
32953
+ return !!(process.env.HERDR_ENV || process.env.HERDR_PANE_ID);
32954
+ }
32955
+ async spawnPane(sessionId, description, serverUrl, directory) {
32956
+ const prev = this.spawnMutex;
32957
+ let release2;
32958
+ this.spawnMutex = new Promise((r) => release2 = r);
32959
+ await prev;
32960
+ try {
32961
+ return await this.doSpawn(sessionId, description, serverUrl, directory);
32962
+ } finally {
32963
+ release2();
32964
+ }
32965
+ }
32966
+ async doSpawn(sessionId, description, serverUrl, directory) {
32967
+ const herdr = await this.getBinary();
32968
+ if (!herdr) {
32969
+ log("[herdr] spawnPane: herdr binary not found");
32970
+ return { success: false };
32971
+ }
32972
+ try {
32973
+ const attachDir = normalizePathForShell(directory);
32974
+ let paneId = null;
32975
+ let lastRawOutput = "";
32976
+ if (this.layout === "main-vertical" && this.agentAreaPaneId) {
32977
+ const result = await this.runSplit([this.agentAreaPaneId], "down", attachDir);
32978
+ paneId = result.paneId;
32979
+ if (!paneId) {
32980
+ log("[herdr] agent area split failed, falling back to parent", {
32981
+ agentAreaPaneId: this.agentAreaPaneId
32982
+ });
32983
+ this.agentAreaPaneId = null;
32984
+ }
32985
+ }
32212
32986
  if (!this.agentAreaPaneId) {
32213
32987
  const result = await this.runSplit(this.targetPaneArg(), this.paneDirection, attachDir);
32214
32988
  paneId = result.paneId;
@@ -32560,6 +33334,7 @@ class ZellijMultiplexer {
32560
33334
  this.firstPaneUsed = true;
32561
33335
  return { success: true, paneId: this.firstPaneId };
32562
33336
  }
33337
+ this.firstPaneUsed = true;
32563
33338
  }
32564
33339
  return await this.createPaneInAgentTab(zellij, sessionId, serverUrl, directory, description);
32565
33340
  } catch {
@@ -32689,13 +33464,12 @@ class ZellijMultiplexer {
32689
33464
  try {
32690
33465
  const existingTab = await this.findTabByName(zellij, "opencode-agents");
32691
33466
  if (existingTab) {
32692
- const firstPane = await this.getFirstPaneInTab(zellij, existingTab.tabId);
33467
+ const firstPane2 = await this.getFirstPaneInTab(zellij, existingTab.tabId);
32693
33468
  return {
32694
33469
  tabId: existingTab.tabId,
32695
- firstPaneId: firstPane || "terminal_0"
33470
+ firstPaneId: firstPane2
32696
33471
  };
32697
33472
  }
32698
- const beforePanes = await this.listPanes(zellij);
32699
33473
  const createProc = crossSpawn([zellij, "action", "new-tab", "--name", "opencode-agents"], { stdout: "pipe", stderr: "pipe" });
32700
33474
  const createExit = await createProc.exited;
32701
33475
  if (createExit !== 0)
@@ -32703,27 +33477,33 @@ class ZellijMultiplexer {
32703
33477
  const newTab = await this.findTabByName(zellij, "opencode-agents");
32704
33478
  if (!newTab)
32705
33479
  return null;
32706
- const afterPanes = await this.listPanes(zellij);
32707
- const newPane = afterPanes.find((p) => !beforePanes.includes(p));
32708
- return { tabId: newTab.tabId, firstPaneId: newPane || "terminal_0" };
33480
+ const firstPane = await this.getFirstPaneInTab(zellij, newTab.tabId);
33481
+ return { tabId: newTab.tabId, firstPaneId: firstPane };
33482
+ } catch {
33483
+ return null;
33484
+ }
33485
+ }
33486
+ async listPanesJson(zellij) {
33487
+ try {
33488
+ const proc = crossSpawn([zellij, "action", "list-panes", "--json", "--tab", "--all"], { stdout: "pipe", stderr: "pipe" });
33489
+ if (await proc.exited !== 0)
33490
+ return null;
33491
+ const stdout = await proc.stdout();
33492
+ return JSON.parse(stdout);
32709
33493
  } catch {
32710
33494
  return null;
32711
33495
  }
32712
33496
  }
32713
33497
  async getFirstPaneInTab(zellij, tabId) {
32714
- const originalTab = await this.getCurrentTabId(zellij);
32715
- await crossSpawn([zellij, "action", "go-to-tab-by-id", tabId], {
32716
- stdout: "ignore",
32717
- stderr: "ignore"
32718
- }).exited;
32719
- const panes = await this.listPanes(zellij);
32720
- if (originalTab) {
32721
- await crossSpawn([zellij, "action", "go-to-tab-by-id", String(originalTab)], {
32722
- stdout: "ignore",
32723
- stderr: "ignore"
32724
- }).exited;
33498
+ try {
33499
+ const panes = await this.listPanesJson(zellij);
33500
+ if (!panes)
33501
+ return null;
33502
+ const pane = panes.find((candidate) => !candidate.is_plugin && candidate.tab_id === Number(tabId));
33503
+ return pane ? `terminal_${pane.id}` : null;
33504
+ } catch {
33505
+ return null;
32725
33506
  }
32726
- return panes[0] || null;
32727
33507
  }
32728
33508
  async findTabByName(zellij, name) {
32729
33509
  try {
@@ -32793,142 +33573,814 @@ class ZellijMultiplexer {
32793
33573
  return null;
32794
33574
  }
32795
33575
  }
32796
- async listPanes(zellij) {
33576
+ async closePane(paneId) {
33577
+ const zellij = await this.getBinary();
33578
+ return gracefulClosePane(zellij, paneId, {
33579
+ ctrlC: ["action", "write", "--pane-id", paneId, "\x03"],
33580
+ close: ["action", "close-pane", "--pane-id", paneId],
33581
+ acceptExitCode1: true,
33582
+ emptyPaneReturnsTrue: true
33583
+ });
33584
+ }
33585
+ async applyLayout(_layout, _mainPaneSize) {}
33586
+ directionArgs() {
33587
+ return this.paneDirection ? ["--direction", this.paneDirection] : [];
33588
+ }
33589
+ tabIdArgs(tabId) {
33590
+ return tabId ? ["--tab-id", tabId] : [];
33591
+ }
33592
+ async getParentTabId(zellij) {
33593
+ if (this.parentTabId)
33594
+ return this.parentTabId;
33595
+ if (this.parentPaneId) {
33596
+ const tabId = await this.findTabIdForPane(zellij, this.parentPaneId);
33597
+ if (tabId) {
33598
+ this.parentTabId = tabId;
33599
+ return tabId;
33600
+ }
33601
+ }
33602
+ this.parentTabId = await this.getCurrentTabId(zellij);
33603
+ return this.parentTabId;
33604
+ }
33605
+ async findTabIdForPane(zellij, paneId) {
33606
+ try {
33607
+ const panes = await this.listPanesJson(zellij);
33608
+ if (!panes)
33609
+ return null;
33610
+ const normalizedPaneId = normalizePaneId(paneId);
33611
+ const pane = panes.find((candidate) => !candidate.is_plugin && String(candidate.id) === normalizedPaneId);
33612
+ return pane?.tab_id === undefined ? null : String(pane.tab_id);
33613
+ } catch {
33614
+ return null;
33615
+ }
33616
+ }
33617
+ async getBinary() {
33618
+ await this.isAvailable();
33619
+ return this.binaryPath;
33620
+ }
33621
+ }
33622
+ function normalizePaneId(paneId) {
33623
+ return paneId.replace(/^terminal_/, "");
33624
+ }
33625
+ function getPaneDirection2(layout) {
33626
+ switch (layout) {
33627
+ case "main-vertical":
33628
+ return "right";
33629
+ case "main-horizontal":
33630
+ return "down";
33631
+ case "even-horizontal":
33632
+ case "even-vertical":
33633
+ case "tiled":
33634
+ return null;
33635
+ }
33636
+ }
33637
+ function buildShellLaunchCommand(command) {
33638
+ return ["sh", "-lc", quoteShellArg(command)].join(" ");
33639
+ }
33640
+
33641
+ // src/multiplexer/factory.ts
33642
+ function getMultiplexer(config) {
33643
+ const { type } = config;
33644
+ if (type === "none") {
33645
+ return null;
33646
+ }
33647
+ let multiplexer;
33648
+ let actualType;
33649
+ switch (type) {
33650
+ case "tmux":
33651
+ multiplexer = new TmuxMultiplexer(config.layout, config.main_pane_size);
33652
+ actualType = "tmux";
33653
+ break;
33654
+ case "zellij":
33655
+ multiplexer = new ZellijMultiplexer(config.layout, config.main_pane_size, config.zellij_pane_mode);
33656
+ actualType = "zellij";
33657
+ break;
33658
+ case "herdr":
33659
+ multiplexer = new HerdrMultiplexer(config.layout, config.main_pane_size);
33660
+ actualType = "herdr";
33661
+ break;
33662
+ case "cmux":
33663
+ multiplexer = new CmuxMultiplexer;
33664
+ actualType = "cmux";
33665
+ break;
33666
+ case "auto": {
33667
+ if (process.env.CMUX_SOCKET_PATH && process.env.CMUX_WORKSPACE_ID && process.env.CMUX_SURFACE_ID) {
33668
+ multiplexer = new CmuxMultiplexer;
33669
+ actualType = "cmux";
33670
+ } else if (process.env.TMUX) {
33671
+ multiplexer = new TmuxMultiplexer(config.layout, config.main_pane_size);
33672
+ actualType = "tmux";
33673
+ } else if (process.env.ZELLIJ) {
33674
+ multiplexer = new ZellijMultiplexer(config.layout, config.main_pane_size, config.zellij_pane_mode);
33675
+ actualType = "zellij";
33676
+ } else if (process.env.HERDR_ENV || process.env.HERDR_PANE_ID) {
33677
+ multiplexer = new HerdrMultiplexer(config.layout, config.main_pane_size);
33678
+ actualType = "herdr";
33679
+ } else {
33680
+ log("[multiplexer] auto: not inside any session, disabling");
33681
+ return null;
33682
+ }
33683
+ break;
33684
+ }
33685
+ default:
33686
+ log(`[multiplexer] Unknown type: ${type}`);
33687
+ return null;
33688
+ }
33689
+ log(`[multiplexer] Created ${actualType} instance`);
33690
+ return multiplexer;
33691
+ }
33692
+ function startAvailabilityCheck(config) {
33693
+ const multiplexer = getMultiplexer(config);
33694
+ if (multiplexer) {
33695
+ multiplexer.isAvailable().catch(() => {});
33696
+ }
33697
+ }
33698
+ // src/multiplexer/types.ts
33699
+ async function isServerRunning(serverUrl, timeoutMs = 3000, maxAttempts = 2) {
33700
+ const healthUrl = new URL("/health", serverUrl).toString();
33701
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
33702
+ const controller = new AbortController;
33703
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
33704
+ let response = null;
33705
+ try {
33706
+ response = await fetch(healthUrl, { signal: controller.signal }).catch(() => null);
33707
+ } finally {
33708
+ clearTimeout(timeout);
33709
+ }
33710
+ if (response?.ok) {
33711
+ return true;
33712
+ }
33713
+ if (attempt < maxAttempts) {
33714
+ await new Promise((r) => setTimeout(r, 250));
33715
+ }
33716
+ }
33717
+ return false;
33718
+ }
33719
+
33720
+ // src/multiplexer/cmux/close-policy.ts
33721
+ class CmuxClosePolicy {
33722
+ budgetMs;
33723
+ maxAttempts;
33724
+ constructor(budgetMs = 30000, maxAttempts = 4) {
33725
+ this.budgetMs = budgetMs;
33726
+ this.maxAttempts = maxAttempts;
33727
+ }
33728
+ request(reason, version, now, current) {
33729
+ if (!current || reason === "deleted" && current.reason === "idle") {
33730
+ return {
33731
+ reason,
33732
+ expectedActivityVersion: version,
33733
+ attempts: 0,
33734
+ deadline: now + this.budgetMs,
33735
+ phase: "pending",
33736
+ nextAttemptAt: now,
33737
+ cooldowns: 0
33738
+ };
33739
+ }
33740
+ return current;
33741
+ }
33742
+ activity(intent) {
33743
+ return intent?.reason === "idle" ? undefined : intent;
33744
+ }
33745
+ failed(intent, now) {
33746
+ const attempts = intent.attempts + 1;
33747
+ if (attempts >= this.maxAttempts || now >= intent.deadline) {
33748
+ const cooldowns = intent.cooldowns + 1;
33749
+ const delay2 = cooldowns === 1 ? 30000 : cooldowns === 2 ? 60000 : Infinity;
33750
+ return {
33751
+ ...intent,
33752
+ attempts,
33753
+ phase: "cooldown",
33754
+ nextAttemptAt: now + delay2,
33755
+ cooldowns
33756
+ };
33757
+ }
33758
+ return { ...intent, attempts, nextAttemptAt: now + 1000 };
33759
+ }
33760
+ resume(intent, now) {
33761
+ if (intent.phase !== "cooldown" || now < intent.nextAttemptAt)
33762
+ return intent;
33763
+ return {
33764
+ ...intent,
33765
+ attempts: 0,
33766
+ deadline: now + this.budgetMs,
33767
+ phase: "pending",
33768
+ nextAttemptAt: now
33769
+ };
33770
+ }
33771
+ complete() {
33772
+ return;
33773
+ }
33774
+ }
33775
+
33776
+ // src/multiplexer/cmux/session-state.ts
33777
+ var STORE_KEY = Symbol.for("oh-my-opencode-slim.cmux-session-store");
33778
+ function records() {
33779
+ const globalStore = globalThis;
33780
+ globalStore[STORE_KEY] ??= new Map;
33781
+ return globalStore[STORE_KEY];
33782
+ }
33783
+
33784
+ class CmuxSessionStore {
33785
+ claimCreated(record) {
33786
+ const existing = records().get(record.session);
33787
+ if (existing) {
33788
+ if (existing.directory !== record.directory || existing.lifecycle !== "orphaned" && existing.lifecycle !== "deleted")
33789
+ return false;
33790
+ existing.closeTimer?.cancel();
33791
+ Object.assign(record, existing, {
33792
+ owner: record.owner,
33793
+ closeTimer: undefined
33794
+ });
33795
+ }
33796
+ records().set(record.session, record);
33797
+ return true;
33798
+ }
33799
+ get(session2) {
33800
+ return records().get(session2);
33801
+ }
33802
+ ownedBy(owner) {
33803
+ return [...records().values()].filter((record) => record.owner === owner);
33804
+ }
33805
+ claimOrphans(owner, directory) {
33806
+ const claimed = [...records().values()].filter((record) => record.directory === directory && Boolean(record.paneId) && (record.lifecycle === "orphaned" || record.lifecycle === "deleted"));
33807
+ for (const record of claimed) {
33808
+ record.closeTimer?.cancel();
33809
+ record.closeTimer = undefined;
33810
+ record.owner = owner;
33811
+ }
33812
+ return claimed;
33813
+ }
33814
+ markAttached(session2, paneId, now) {
33815
+ const record = records().get(session2);
33816
+ if (!record)
33817
+ return;
33818
+ Object.assign(record, {
33819
+ paneId,
33820
+ attachedAt: now,
33821
+ lastActivityAt: now,
33822
+ spawnState: "attached",
33823
+ deferredSpawn: undefined
33824
+ });
33825
+ }
33826
+ markActivity(session2, now) {
33827
+ const record = records().get(session2);
33828
+ if (!record)
33829
+ return;
33830
+ record.lastActivityAt = now;
33831
+ record.activityVersion += 1;
33832
+ record.idleConsecutive = 0;
33833
+ record.statusMissingSince = undefined;
33834
+ }
33835
+ markDeleted(session2) {
33836
+ const record = records().get(session2);
33837
+ if (record)
33838
+ record.lifecycle = "deleted";
33839
+ }
33840
+ markOrphaned(session2) {
33841
+ const record = records().get(session2);
33842
+ if (record)
33843
+ record.lifecycle = "orphaned";
33844
+ }
33845
+ removeAfterConfirmedClose(session2) {
33846
+ const record = records().get(session2);
33847
+ return record?.paneId ? records().delete(session2) : false;
33848
+ }
33849
+ removeWithoutPane(session2) {
33850
+ const record = records().get(session2);
33851
+ return record && !record.paneId ? records().delete(session2) : false;
33852
+ }
33853
+ resetForTests() {
33854
+ for (const record of records().values()) {
33855
+ record.deferredSpawn?.timer?.cancel();
33856
+ record.closeTimer?.cancel();
33857
+ }
33858
+ records().clear();
33859
+ }
33860
+ }
33861
+
33862
+ // src/multiplexer/cmux/session-lifecycle.ts
33863
+ var ACTIVITY_EVENTS = new Set([
33864
+ "message.updated",
33865
+ "message.removed",
33866
+ "message.part.updated",
33867
+ "message.part.delta",
33868
+ "message.part.removed"
33869
+ ]);
33870
+ var MIN_LIFETIME_MS = 1e4;
33871
+ var IDLE_CONFIRMATIONS = 3;
33872
+
33873
+ class ServerUrlUnavailableError extends Error {
33874
+ constructor() {
33875
+ super("OpenCode server URL is unavailable");
33876
+ this.name = "ServerUrlUnavailableError";
33877
+ }
33878
+ }
33879
+
33880
+ class CmuxSessionLifecycle {
33881
+ owner;
33882
+ multiplexer;
33883
+ resolveServerUrl;
33884
+ defaultDirectory;
33885
+ backgroundJobs;
33886
+ store = new CmuxSessionStore;
33887
+ policy;
33888
+ now;
33889
+ delay;
33890
+ injectedDelay;
33891
+ deferredRetryMs;
33892
+ deferredTtlMs;
33893
+ missingGraceMs;
33894
+ closeRetryMs;
33895
+ shutdownTimeoutMs;
33896
+ serverCheck;
33897
+ fetchStatuses;
33898
+ pollTimer;
33899
+ polling = false;
33900
+ cleanupPromise;
33901
+ disposed = false;
33902
+ spawnGeneration = 0;
33903
+ constructor(owner, multiplexer, resolveServerUrl, defaultDirectory, backgroundJobs, options = {}) {
33904
+ this.owner = owner;
33905
+ this.multiplexer = multiplexer;
33906
+ this.resolveServerUrl = resolveServerUrl;
33907
+ this.defaultDirectory = defaultDirectory;
33908
+ this.backgroundJobs = backgroundJobs;
33909
+ this.now = options.now ?? Date.now;
33910
+ this.injectedDelay = Boolean(options.delay);
33911
+ this.delay = options.delay ?? ((milliseconds) => new Promise((resolve4) => setTimeout(resolve4, milliseconds)));
33912
+ this.deferredRetryMs = options.deferredRetryMs ?? 2000;
33913
+ this.deferredTtlMs = options.deferredTtlMs ?? 300000;
33914
+ this.missingGraceMs = options.missingGraceMs ?? 30000;
33915
+ this.closeRetryMs = options.closeRetryMs ?? 1000;
33916
+ this.shutdownTimeoutMs = options.shutdownTimeoutMs ?? 5000;
33917
+ this.policy = new CmuxClosePolicy(options.closeRetryTtlMs, options.closeRetryMaxAttempts);
33918
+ this.serverCheck = options.isServerRunning ?? isServerRunning;
33919
+ this.fetchStatuses = options.fetchStatuses ?? (() => this.loadStatuses());
33920
+ for (const orphan of this.store.claimOrphans(owner, defaultDirectory)) {
33921
+ if (orphan.closeIntent?.phase === "cooldown" && Number.isFinite(orphan.closeIntent.nextAttemptAt)) {
33922
+ this.scheduleCooldown(orphan);
33923
+ } else {
33924
+ orphan.closeIntent = undefined;
33925
+ this.requestClose(orphan, "cleanup");
33926
+ }
33927
+ }
33928
+ }
33929
+ async onSessionCreated(event) {
33930
+ if (this.disposed)
33931
+ return;
33932
+ if (event.type !== "session.created")
33933
+ return;
33934
+ const info = event.properties?.info;
33935
+ if (!info?.id || !info.parentID)
33936
+ return;
33937
+ const now = this.now();
33938
+ const record = {
33939
+ session: info.id,
33940
+ owner: this.owner,
33941
+ parent: info.parentID,
33942
+ title: info.title ?? "Subagent",
33943
+ directory: info.directory ?? this.defaultDirectory,
33944
+ spawnState: "known",
33945
+ lifecycle: "active",
33946
+ lastActivityAt: now,
33947
+ activityVersion: 0,
33948
+ idleConsecutive: 0
33949
+ };
33950
+ if (!this.store.claimCreated(record))
33951
+ return;
33952
+ if (record.paneId && record.lifecycle !== "active") {
33953
+ record.closeIntent = undefined;
33954
+ await this.requestClose(record, "cleanup");
33955
+ return;
33956
+ }
33957
+ await this.spawn(record);
33958
+ }
33959
+ async onSessionStatus(event) {
33960
+ if (this.disposed)
33961
+ return;
33962
+ const session2 = this.eventSession(event);
33963
+ if (!session2)
33964
+ return;
33965
+ const owned = this.store.get(session2);
33966
+ if (!owned || owned.owner !== this.owner)
33967
+ return;
33968
+ if (ACTIVITY_EVENTS.has(event.type)) {
33969
+ this.activity(session2);
33970
+ return;
33971
+ }
33972
+ const status = event.type === "session.idle" ? "idle" : event.type === "session.status" ? event.properties?.status?.type : undefined;
33973
+ if (!status)
33974
+ return;
33975
+ if (status !== "idle") {
33976
+ this.activity(session2);
33977
+ this.backgroundJobs?.clearDeferredClose(session2);
33978
+ const record = this.store.get(session2);
33979
+ if (status === "busy" && record && !record.paneId)
33980
+ await this.spawn(record);
33981
+ }
33982
+ if (owned.paneId)
33983
+ this.startPolling();
33984
+ }
33985
+ async onSessionDeleted(event) {
33986
+ if (event.type !== "session.deleted")
33987
+ return;
33988
+ const session2 = this.eventSession(event);
33989
+ if (!session2)
33990
+ return;
33991
+ const record = this.store.get(session2);
33992
+ if (!record)
33993
+ return;
33994
+ if (record.owner !== this.owner)
33995
+ return;
33996
+ this.store.markDeleted(session2);
33997
+ this.cancelDeferred(record);
33998
+ this.backgroundJobs?.clearDeferredClose(session2);
33999
+ if (!record.paneId) {
34000
+ if (!record.spawnPromise)
34001
+ this.store.removeWithoutPane(session2);
34002
+ return;
34003
+ }
34004
+ await this.requestClose(record, "deleted");
34005
+ }
34006
+ async closeSessionFromCoordinator(session2) {
34007
+ if (this.disposed)
34008
+ return;
34009
+ const record = this.store.get(session2);
34010
+ if (record?.paneId && record.owner === this.owner)
34011
+ this.startPolling();
34012
+ }
34013
+ cleanup() {
34014
+ this.cleanupPromise ??= this.runCleanup();
34015
+ return this.cleanupPromise;
34016
+ }
34017
+ pollOnce() {
34018
+ return this.poll();
34019
+ }
34020
+ async spawn(record, deferred = false) {
34021
+ if (this.disposed || record.owner !== this.owner)
34022
+ return;
34023
+ if (record.spawnState === "spawning" || record.paneId)
34024
+ return;
34025
+ const generation = this.spawnGeneration;
34026
+ const token = record.deferredSpawn?.generation;
34027
+ record.spawnState = "spawning";
34028
+ const operation = this.spawnOperation(record);
34029
+ record.spawnPromise = operation;
34030
+ const result = await operation;
34031
+ if (record.spawnPromise === operation)
34032
+ record.spawnPromise = undefined;
34033
+ const current = this.store.get(record.session);
34034
+ if (this.disposed || generation !== this.spawnGeneration) {
34035
+ const latePane = result.paneId ?? result.orphanPaneId;
34036
+ if (latePane)
34037
+ await this.closeLatePane(record, latePane);
34038
+ else if (current && !current.paneId)
34039
+ this.store.removeWithoutPane(record.session);
34040
+ return;
34041
+ }
34042
+ if (!current) {
34043
+ if (result.success && result.paneId)
34044
+ await this.adoptAndClose(record, result.paneId);
34045
+ if (result.orphanPaneId)
34046
+ await this.adoptAndClose(record, result.orphanPaneId);
34047
+ return;
34048
+ }
34049
+ if (deferred && token !== undefined && current.deferredSpawn?.generation !== token) {
34050
+ const stalePane = result.paneId ?? result.orphanPaneId;
34051
+ if (stalePane)
34052
+ await this.adoptAndClose(current, stalePane);
34053
+ return;
34054
+ }
34055
+ const paneId = result.paneId ?? result.orphanPaneId;
34056
+ if (paneId) {
34057
+ this.store.markAttached(record.session, paneId, this.now());
34058
+ if (result.orphanPaneId)
34059
+ this.store.markOrphaned(record.session);
34060
+ if (current.lifecycle !== "active" || result.orphanPaneId) {
34061
+ await this.requestClose(current, current.lifecycle === "active" ? "cleanup" : "deleted");
34062
+ } else
34063
+ this.startPolling();
34064
+ return;
34065
+ }
34066
+ current.spawnState = "failed";
34067
+ if (current.lifecycle !== "active") {
34068
+ this.store.removeWithoutPane(current.session);
34069
+ } else if (result.error === "unavailable" || result.error === "not_found" || result.error === "invalid_state") {
34070
+ this.deferSpawn(current);
34071
+ }
34072
+ }
34073
+ async spawnOperation(record) {
34074
+ const serverUrl = this.resolveServerUrl();
34075
+ if (!serverUrl) {
34076
+ log("[cmux-session-lifecycle] no valid server URL; skipping spawn");
34077
+ return { success: false, error: "unavailable" };
34078
+ }
34079
+ if (!await this.serverCheck(serverUrl)) {
34080
+ return { success: false, error: "unavailable" };
34081
+ }
34082
+ try {
34083
+ return await this.multiplexer.spawnPane(record.session, record.title, serverUrl, record.directory);
34084
+ } catch {
34085
+ return { success: false, error: "hard" };
34086
+ }
34087
+ }
34088
+ deferSpawn(record) {
34089
+ if (this.disposed || record.owner !== this.owner)
34090
+ return;
34091
+ const existing = record.deferredSpawn;
34092
+ const deferred = existing ?? {
34093
+ deadline: this.now() + this.deferredTtlMs,
34094
+ generation: 0
34095
+ };
34096
+ deferred.generation += 1;
34097
+ deferred.timer?.cancel();
34098
+ record.deferredSpawn = deferred;
34099
+ if (this.now() >= deferred.deadline) {
34100
+ this.cancelDeferred(record);
34101
+ return;
34102
+ }
34103
+ deferred.timer = this.timer(async () => {
34104
+ deferred.timer = undefined;
34105
+ if (this.disposed || this.store.get(record.session) !== record || record.lifecycle !== "active" || record.owner !== this.owner)
34106
+ return;
34107
+ await this.spawn(record, true);
34108
+ }, this.deferredRetryMs);
34109
+ }
34110
+ cancelDeferred(record) {
34111
+ record.deferredSpawn?.timer?.cancel();
34112
+ record.deferredSpawn = undefined;
34113
+ }
34114
+ activity(session2) {
34115
+ const record = this.store.get(session2);
34116
+ if (!record || record.owner !== this.owner || this.disposed)
34117
+ return;
34118
+ this.store.markActivity(session2, this.now());
34119
+ const next = this.policy.activity(record.closeIntent);
34120
+ if (!next && record.closeIntent)
34121
+ record.closeTimer?.cancel();
34122
+ record.closeIntent = next;
34123
+ if (!next)
34124
+ record.closeTimer = undefined;
34125
+ }
34126
+ async requestClose(record, reason) {
34127
+ if (!record.paneId || record.owner !== this.owner)
34128
+ return;
34129
+ if (reason === "idle" && !(this.backgroundJobs?.deferIfRunning(record.session) ?? true))
34130
+ return;
34131
+ const previous = record.closeIntent;
34132
+ record.closeIntent = this.policy.request(reason, record.activityVersion, this.now(), previous);
34133
+ if (previous !== record.closeIntent)
34134
+ record.closeTimer?.cancel();
34135
+ await this.attemptClose(record);
34136
+ }
34137
+ async attemptClose(record) {
34138
+ const intent = record.closeIntent;
34139
+ if (!intent || !record.paneId || record.owner !== this.owner || this.store.get(record.session) !== record)
34140
+ return;
34141
+ if (intent.phase === "cooldown" && this.now() < intent.nextAttemptAt) {
34142
+ this.scheduleCooldown(record);
34143
+ return;
34144
+ }
34145
+ record.closeIntent = this.policy.resume(intent, this.now());
34146
+ if (record.closeIntent !== intent)
34147
+ return this.attemptClose(record);
34148
+ if (intent.reason === "idle" && intent.expectedActivityVersion !== record.activityVersion) {
34149
+ record.closeIntent = this.policy.activity(intent);
34150
+ return;
34151
+ }
34152
+ let closed = false;
32797
34153
  try {
32798
- const proc = crossSpawn([zellij, "action", "list-panes"], {
32799
- stdout: "pipe",
32800
- stderr: "pipe"
32801
- });
32802
- const exitCode = await proc.exited;
32803
- if (exitCode !== 0)
32804
- return [];
32805
- const stdout = await proc.stdout();
32806
- return stdout.split(`
32807
- `).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter((id) => id?.startsWith("terminal_"));
32808
- } catch {
32809
- return [];
34154
+ closed = await this.multiplexer.closePane(record.paneId);
34155
+ } catch {}
34156
+ if (this.disposed || this.store.get(record.session) !== record || record.owner !== this.owner || record.closeIntent !== intent)
34157
+ return;
34158
+ const intentStillCurrent = record.closeIntent === intent;
34159
+ const idleStillCurrent = intent.reason !== "idle" || intent.expectedActivityVersion === record.activityVersion;
34160
+ if (closed) {
34161
+ record.closeTimer?.cancel();
34162
+ record.closeTimer = undefined;
34163
+ if (record.lifecycle !== "active") {
34164
+ this.store.removeAfterConfirmedClose(record.session);
34165
+ } else {
34166
+ record.paneId = undefined;
34167
+ record.spawnState = "known";
34168
+ if (intentStillCurrent)
34169
+ record.closeIntent = this.policy.complete();
34170
+ if (intent.reason === "idle" && record.owner === this.owner && !this.disposed && record.activityVersion !== intent.expectedActivityVersion) {
34171
+ await this.spawn(record);
34172
+ }
34173
+ }
34174
+ this.updatePolling();
34175
+ return;
32810
34176
  }
34177
+ if (!intentStillCurrent || !idleStillCurrent)
34178
+ return;
34179
+ record.closeIntent = this.policy.failed(intent, this.now());
34180
+ if (record.closeIntent.phase === "cooldown") {
34181
+ if (!Number.isFinite(record.closeIntent.nextAttemptAt))
34182
+ this.store.markOrphaned(record.session);
34183
+ this.scheduleCooldown(record);
34184
+ return;
34185
+ }
34186
+ record.closeTimer?.cancel();
34187
+ record.closeTimer = this.timer(() => this.attemptClose(record), this.closeRetryMs);
32811
34188
  }
32812
- async closePane(paneId) {
32813
- const zellij = await this.getBinary();
32814
- return gracefulClosePane(zellij, paneId, {
32815
- ctrlC: ["action", "write", "--pane-id", paneId, "\x03"],
32816
- close: ["action", "close-pane", "--pane-id", paneId],
32817
- acceptExitCode1: true,
32818
- emptyPaneReturnsTrue: true
32819
- });
34189
+ scheduleCooldown(record) {
34190
+ record.closeTimer?.cancel();
34191
+ record.closeTimer = undefined;
34192
+ const intent = record.closeIntent;
34193
+ if (!intent || !Number.isFinite(intent.nextAttemptAt) || this.disposed)
34194
+ return;
34195
+ record.closeTimer = this.timer(() => this.attemptClose(record), Math.max(0, intent.nextAttemptAt - this.now()));
32820
34196
  }
32821
- async applyLayout(_layout, _mainPaneSize) {}
32822
- directionArgs() {
32823
- return this.paneDirection ? ["--direction", this.paneDirection] : [];
34197
+ async poll() {
34198
+ if (this.polling || this.disposed)
34199
+ return;
34200
+ this.polling = true;
34201
+ try {
34202
+ const statuses = await this.fetchStatuses();
34203
+ for (const record of this.store.ownedBy(this.owner)) {
34204
+ if (!record.paneId || record.lifecycle !== "active")
34205
+ continue;
34206
+ const status = statuses[record.session];
34207
+ if (!status) {
34208
+ record.statusMissingSince ??= this.now();
34209
+ if (this.now() - record.statusMissingSince < this.missingGraceMs) {
34210
+ record.idleConsecutive = 0;
34211
+ continue;
34212
+ }
34213
+ }
34214
+ if (status)
34215
+ record.statusMissingSince = undefined;
34216
+ if (status && status.type !== "idle") {
34217
+ this.activity(record.session);
34218
+ continue;
34219
+ }
34220
+ if (this.now() - (record.attachedAt ?? this.now()) < MIN_LIFETIME_MS || this.now() - record.lastActivityAt < MIN_LIFETIME_MS) {
34221
+ record.idleConsecutive = 0;
34222
+ continue;
34223
+ }
34224
+ record.idleConsecutive += 1;
34225
+ if (record.idleConsecutive < IDLE_CONFIRMATIONS)
34226
+ continue;
34227
+ const version = record.activityVersion;
34228
+ const final = await this.fetchStatuses();
34229
+ if ((final[record.session]?.type === "idle" || !final[record.session] && record.statusMissingSince !== undefined && this.now() - record.statusMissingSince >= this.missingGraceMs) && version === record.activityVersion)
34230
+ await this.requestClose(record, "idle");
34231
+ else
34232
+ this.activity(record.session);
34233
+ }
34234
+ } catch {} finally {
34235
+ this.polling = false;
34236
+ }
32824
34237
  }
32825
- tabIdArgs(tabId) {
32826
- return tabId ? ["--tab-id", tabId] : [];
34238
+ startPolling() {
34239
+ if (this.pollTimer || this.disposed)
34240
+ return;
34241
+ this.pollTimer = setInterval(() => void this.poll().catch(() => {
34242
+ return;
34243
+ }), POLL_INTERVAL_BACKGROUND_MS);
34244
+ this.pollTimer.unref?.();
32827
34245
  }
32828
- async getParentTabId(zellij) {
32829
- if (this.parentTabId)
32830
- return this.parentTabId;
32831
- if (this.parentPaneId) {
32832
- const tabId = await this.findTabIdForPane(zellij, this.parentPaneId);
32833
- if (tabId) {
32834
- this.parentTabId = tabId;
32835
- return tabId;
34246
+ updatePolling() {
34247
+ if (this.store.ownedBy(this.owner).some((record) => record.paneId))
34248
+ this.startPolling();
34249
+ else if (this.pollTimer) {
34250
+ clearInterval(this.pollTimer);
34251
+ this.pollTimer = undefined;
34252
+ }
34253
+ }
34254
+ async runCleanup() {
34255
+ this.disposed = true;
34256
+ this.spawnGeneration += 1;
34257
+ if (this.pollTimer)
34258
+ clearInterval(this.pollTimer);
34259
+ this.pollTimer = undefined;
34260
+ const records2 = this.store.ownedBy(this.owner);
34261
+ for (const record of records2)
34262
+ this.cancelDeferred(record);
34263
+ const pending = records2.flatMap((record) => record.spawnPromise ? [record.spawnPromise] : []);
34264
+ if (pending.length) {
34265
+ await Promise.race([
34266
+ Promise.allSettled(pending),
34267
+ this.delay(this.shutdownTimeoutMs)
34268
+ ]);
34269
+ }
34270
+ for (const record of this.store.ownedBy(this.owner)) {
34271
+ if (!record.paneId) {
34272
+ if (!record.spawnPromise)
34273
+ this.store.removeWithoutPane(record.session);
34274
+ continue;
34275
+ }
34276
+ record.closeTimer?.cancel();
34277
+ record.closeTimer = undefined;
34278
+ record.closeIntent = this.policy.request("cleanup", record.activityVersion, this.now());
34279
+ while (record.closeIntent?.phase === "pending" && this.store.get(record.session) === record && record.owner === this.owner) {
34280
+ await this.attemptCloseWithoutTimer(record);
34281
+ if (record.closeIntent?.phase === "pending")
34282
+ await this.delay(this.closeRetryMs);
32836
34283
  }
34284
+ if (record.closeIntent && this.store.get(record.session) === record && record.owner === this.owner)
34285
+ this.store.markOrphaned(record.session);
32837
34286
  }
32838
- this.parentTabId = await this.getCurrentTabId(zellij);
32839
- return this.parentTabId;
32840
34287
  }
32841
- async findTabIdForPane(zellij, paneId) {
34288
+ async attemptCloseWithoutTimer(record) {
34289
+ const intent = record.closeIntent;
34290
+ if (!intent || !record.paneId)
34291
+ return;
34292
+ const paneId = record.paneId;
34293
+ let closed = false;
32842
34294
  try {
32843
- const proc = crossSpawn([zellij, "action", "list-panes", "--json", "--tab", "--all"], {
32844
- stdout: "pipe",
32845
- stderr: "pipe"
32846
- });
32847
- if (await proc.exited !== 0)
32848
- return null;
32849
- const stdout = await proc.stdout();
32850
- const panes = JSON.parse(stdout);
32851
- const normalizedPaneId = normalizePaneId(paneId);
32852
- const pane = panes.find((candidate) => !candidate.is_plugin && String(candidate.id) === normalizedPaneId);
32853
- return pane?.tab_id === undefined ? null : String(pane.tab_id);
32854
- } catch {
32855
- return null;
34295
+ closed = await this.multiplexer.closePane(paneId);
34296
+ } catch {}
34297
+ if (this.store.get(record.session) !== record || record.owner !== this.owner || record.closeIntent !== intent || record.paneId !== paneId)
34298
+ return;
34299
+ if (closed) {
34300
+ record.closeIntent = undefined;
34301
+ this.store.removeAfterConfirmedClose(record.session);
34302
+ } else
34303
+ record.closeIntent = this.policy.failed(intent, this.now());
34304
+ }
34305
+ async adoptAndClose(record, paneId) {
34306
+ if (!this.store.get(record.session))
34307
+ this.store.claimCreated(record);
34308
+ this.store.markAttached(record.session, paneId, this.now());
34309
+ this.store.markOrphaned(record.session);
34310
+ await this.requestClose(record, "cleanup");
34311
+ }
34312
+ async closeLatePane(source, paneId) {
34313
+ const existing = this.store.get(source.session);
34314
+ if (existing && existing.owner !== this.owner) {
34315
+ let closed = false;
34316
+ try {
34317
+ closed = await this.multiplexer.closePane(paneId);
34318
+ } catch {}
34319
+ if (!closed)
34320
+ this.trackStalePane(source, paneId);
34321
+ return;
32856
34322
  }
34323
+ const record = existing ?? source;
34324
+ if (!existing)
34325
+ this.store.claimCreated(record);
34326
+ record.paneId = paneId;
34327
+ record.spawnState = "attached";
34328
+ record.lifecycle = "orphaned";
34329
+ record.closeIntent = this.policy.request("cleanup", record.activityVersion, this.now());
34330
+ while (record.closeIntent?.phase === "pending") {
34331
+ await this.attemptCloseWithoutTimer(record);
34332
+ if (record.closeIntent?.phase === "pending")
34333
+ await this.delay(this.closeRetryMs);
34334
+ }
34335
+ }
34336
+ trackStalePane(source, paneId) {
34337
+ const session2 = `${source.session}\x00late\x00${paneId}`;
34338
+ this.store.claimCreated({
34339
+ session: session2,
34340
+ owner: this.owner,
34341
+ parent: source.parent,
34342
+ title: source.title,
34343
+ directory: source.directory,
34344
+ paneId,
34345
+ spawnState: "attached",
34346
+ lifecycle: "orphaned",
34347
+ attachedAt: this.now(),
34348
+ lastActivityAt: source.lastActivityAt,
34349
+ activityVersion: source.activityVersion,
34350
+ idleConsecutive: 0
34351
+ });
32857
34352
  }
32858
- async getBinary() {
32859
- await this.isAvailable();
32860
- return this.binaryPath;
32861
- }
32862
- }
32863
- function normalizePaneId(paneId) {
32864
- return paneId.replace(/^terminal_/, "");
32865
- }
32866
- function getPaneDirection2(layout) {
32867
- switch (layout) {
32868
- case "main-vertical":
32869
- return "right";
32870
- case "main-horizontal":
32871
- return "down";
32872
- case "even-horizontal":
32873
- case "even-vertical":
32874
- case "tiled":
32875
- return null;
32876
- }
32877
- }
32878
- function buildShellLaunchCommand(command) {
32879
- return ["sh", "-lc", quoteShellArg(command)].join(" ");
32880
- }
32881
-
32882
- // src/multiplexer/factory.ts
32883
- function getMultiplexer(config) {
32884
- const { type } = config;
32885
- if (type === "none") {
32886
- return null;
34353
+ eventSession(event) {
34354
+ return event.properties?.sessionID ?? event.properties?.info?.sessionID ?? event.properties?.part?.sessionID ?? event.properties?.info?.id;
32887
34355
  }
32888
- let multiplexer;
32889
- let actualType;
32890
- switch (type) {
32891
- case "tmux":
32892
- multiplexer = new TmuxMultiplexer(config.layout, config.main_pane_size);
32893
- actualType = "tmux";
32894
- break;
32895
- case "zellij":
32896
- multiplexer = new ZellijMultiplexer(config.layout, config.main_pane_size, config.zellij_pane_mode);
32897
- actualType = "zellij";
32898
- break;
32899
- case "herdr":
32900
- multiplexer = new HerdrMultiplexer(config.layout, config.main_pane_size);
32901
- actualType = "herdr";
32902
- break;
32903
- case "auto": {
32904
- if (process.env.TMUX) {
32905
- multiplexer = new TmuxMultiplexer(config.layout, config.main_pane_size);
32906
- actualType = "tmux";
32907
- } else if (process.env.ZELLIJ) {
32908
- multiplexer = new ZellijMultiplexer(config.layout, config.main_pane_size, config.zellij_pane_mode);
32909
- actualType = "zellij";
32910
- } else if (process.env.HERDR_ENV || process.env.HERDR_PANE_ID) {
32911
- multiplexer = new HerdrMultiplexer(config.layout, config.main_pane_size);
32912
- actualType = "herdr";
32913
- } else {
32914
- log("[multiplexer] auto: not inside any session, disabling");
32915
- return null;
32916
- }
32917
- break;
34356
+ timer(callback, milliseconds) {
34357
+ let cancelled = false;
34358
+ if (this.injectedDelay) {
34359
+ this.delay(milliseconds).then(() => {
34360
+ if (!cancelled)
34361
+ callback();
34362
+ });
34363
+ return { cancel: () => cancelled = true };
32918
34364
  }
32919
- default:
32920
- log(`[multiplexer] Unknown type: ${type}`);
32921
- return null;
34365
+ const timer = setTimeout(() => void callback(), milliseconds);
34366
+ timer.unref?.();
34367
+ return { cancel: () => clearTimeout(timer) };
32922
34368
  }
32923
- log(`[multiplexer] Created ${actualType} instance`);
32924
- return multiplexer;
32925
- }
32926
- function startAvailabilityCheck(config) {
32927
- const multiplexer = getMultiplexer(config);
32928
- if (multiplexer) {
32929
- multiplexer.isAvailable().catch(() => {});
34369
+ async loadStatuses() {
34370
+ const serverUrl = this.resolveServerUrl();
34371
+ if (!serverUrl) {
34372
+ log("[cmux-session-lifecycle] no valid server URL; skipping poll");
34373
+ throw new ServerUrlUnavailableError;
34374
+ }
34375
+ const response = await fetch(new URL("/session/status", serverUrl), {
34376
+ signal: AbortSignal.timeout(2000)
34377
+ });
34378
+ if (!response.ok)
34379
+ throw new Error(`session status failed: ${response.status}`);
34380
+ return await response.json();
32930
34381
  }
32931
34382
  }
34383
+
32932
34384
  // src/multiplexer/session-manager.ts
32933
34385
  var SHARED_STATE_KEY = Symbol.for("oh-my-opencode-slim.multiplexer-session-manager.state");
32934
34386
  function getSharedState() {
@@ -32941,10 +34393,53 @@ function getSharedState() {
32941
34393
  };
32942
34394
  return globalWithState[SHARED_STATE_KEY];
32943
34395
  }
34396
+ function validServerUrl(value) {
34397
+ if (typeof value !== "string" && !(value instanceof URL))
34398
+ return null;
34399
+ try {
34400
+ const url = new URL(value.toString());
34401
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : null;
34402
+ } catch {
34403
+ return null;
34404
+ }
34405
+ }
34406
+ function clientBaseUrl(client) {
34407
+ try {
34408
+ if (!client || typeof client !== "object" || !("_client" in client))
34409
+ return null;
34410
+ const internal = client._client;
34411
+ if (!internal || typeof internal !== "object" || !("getConfig" in internal))
34412
+ return null;
34413
+ const getConfig = internal.getConfig;
34414
+ if (typeof getConfig !== "function")
34415
+ return null;
34416
+ const config = getConfig.call(internal);
34417
+ if (!config || typeof config !== "object" || !("baseUrl" in config))
34418
+ return null;
34419
+ return validServerUrl(config.baseUrl);
34420
+ } catch {
34421
+ return null;
34422
+ }
34423
+ }
34424
+ function createServerUrlResolver(ctx) {
34425
+ return () => {
34426
+ try {
34427
+ const serverUrl = validServerUrl(ctx.serverUrl);
34428
+ if (serverUrl)
34429
+ return serverUrl;
34430
+ } catch {}
34431
+ try {
34432
+ return clientBaseUrl(ctx.client);
34433
+ } catch {
34434
+ return null;
34435
+ }
34436
+ };
34437
+ }
34438
+
32944
34439
  class MultiplexerSessionManager {
32945
34440
  backgroundJobBoard;
32946
34441
  instanceId = Math.random().toString(36).slice(2, 8);
32947
- serverUrl;
34442
+ resolveServerUrl;
32948
34443
  directory;
32949
34444
  multiplexer = null;
32950
34445
  sessions;
@@ -32953,7 +34448,8 @@ class MultiplexerSessionManager {
32953
34448
  closingSessions;
32954
34449
  pollInterval;
32955
34450
  enabled = false;
32956
- constructor(ctx, config, backgroundJobBoard) {
34451
+ cmuxLifecycle;
34452
+ constructor(ctx, config, backgroundJobBoard, options = {}) {
32957
34453
  this.backgroundJobBoard = backgroundJobBoard;
32958
34454
  const sharedState = getSharedState();
32959
34455
  this.sessions = sharedState.sessions;
@@ -32961,20 +34457,24 @@ class MultiplexerSessionManager {
32961
34457
  this.spawningSessions = sharedState.spawningSessions;
32962
34458
  this.closingSessions = sharedState.closingSessions;
32963
34459
  this.directory = ctx.directory;
32964
- const defaultPort = process.env.OPENCODE_PORT ?? "4096";
32965
- this.serverUrl = ctx.serverUrl?.toString() ?? `http://localhost:${defaultPort}`;
34460
+ this.resolveServerUrl = createServerUrlResolver(ctx);
32966
34461
  this.multiplexer = getMultiplexer(config);
32967
34462
  this.enabled = config.type !== "none" && this.multiplexer !== null && this.multiplexer.isInsideSession();
34463
+ if (this.enabled && this.multiplexer?.type === "cmux") {
34464
+ this.cmuxLifecycle = new CmuxSessionLifecycle(this.instanceId, this.multiplexer, this.resolveServerUrl, this.directory, this.backgroundJobBoard, options);
34465
+ }
32968
34466
  log("[multiplexer-session-manager] initialized", {
32969
34467
  instanceId: this.instanceId,
32970
34468
  enabled: this.enabled,
32971
34469
  type: config.type,
32972
- serverUrl: this.serverUrl,
34470
+ serverUrl: "dynamic",
32973
34471
  trackedSessions: this.sessions.size,
32974
34472
  knownSessions: this.knownSessions.size
32975
34473
  });
32976
34474
  }
32977
34475
  async onSessionCreated(event) {
34476
+ if (this.cmuxLifecycle)
34477
+ return this.cmuxLifecycle.onSessionCreated(event);
32978
34478
  if (!this.enabled || !this.multiplexer)
32979
34479
  return;
32980
34480
  if (event.type !== "session.created")
@@ -33006,11 +34506,19 @@ class MultiplexerSessionManager {
33006
34506
  });
33007
34507
  this.spawningSessions.add(sessionId);
33008
34508
  try {
33009
- const serverRunning = await isServerRunning(this.serverUrl);
34509
+ const serverUrl = this.resolveServerUrl();
34510
+ if (!serverUrl) {
34511
+ log("[multiplexer-session-manager] no valid server URL, skipping spawn", {
34512
+ instanceId: this.instanceId,
34513
+ sessionId
34514
+ });
34515
+ return;
34516
+ }
34517
+ const serverRunning = await isServerRunning(serverUrl);
33010
34518
  if (!serverRunning) {
33011
34519
  log("[multiplexer-session-manager] server not running, skipping", {
33012
34520
  instanceId: this.instanceId,
33013
- serverUrl: this.serverUrl
34521
+ serverUrl
33014
34522
  });
33015
34523
  return;
33016
34524
  }
@@ -33023,7 +34531,7 @@ class MultiplexerSessionManager {
33023
34531
  title,
33024
34532
  instanceId: this.instanceId
33025
34533
  });
33026
- const paneResult = await this.multiplexer.spawnPane(sessionId, title, this.serverUrl, directory).catch((err) => {
34534
+ const paneResult = await this.multiplexer.spawnPane(sessionId, title, serverUrl, directory).catch((err) => {
33027
34535
  log("[multiplexer-session-manager] failed to spawn pane", {
33028
34536
  instanceId: this.instanceId,
33029
34537
  error: String(err)
@@ -33060,6 +34568,8 @@ class MultiplexerSessionManager {
33060
34568
  }
33061
34569
  }
33062
34570
  async onSessionStatus(event) {
34571
+ if (this.cmuxLifecycle)
34572
+ return this.cmuxLifecycle.onSessionStatus(event);
33063
34573
  if (!this.enabled)
33064
34574
  return;
33065
34575
  if (event.type === "session.idle") {
@@ -33112,6 +34622,8 @@ class MultiplexerSessionManager {
33112
34622
  }
33113
34623
  }
33114
34624
  async onSessionDeleted(event) {
34625
+ if (this.cmuxLifecycle)
34626
+ return this.cmuxLifecycle.onSessionDeleted(event);
33115
34627
  if (!this.enabled)
33116
34628
  return;
33117
34629
  if (event.type !== "session.deleted")
@@ -33147,6 +34659,8 @@ class MultiplexerSessionManager {
33147
34659
  }
33148
34660
  }
33149
34661
  async pollSessions() {
34662
+ if (this.cmuxLifecycle)
34663
+ return this.cmuxLifecycle.pollOnce();
33150
34664
  if (this.sessions.size === 0) {
33151
34665
  this.stopPolling();
33152
34666
  return;
@@ -33181,7 +34695,14 @@ class MultiplexerSessionManager {
33181
34695
  }
33182
34696
  }
33183
34697
  async fetchSessionStatuses() {
33184
- const url = new URL("/session/status", this.serverUrl);
34698
+ const serverUrl = this.resolveServerUrl();
34699
+ if (!serverUrl) {
34700
+ log("[multiplexer-session-manager] no valid server URL, skipping poll", {
34701
+ instanceId: this.instanceId
34702
+ });
34703
+ return {};
34704
+ }
34705
+ const url = new URL("/session/status", serverUrl);
33185
34706
  const response = await fetch(url, { signal: AbortSignal.timeout(2000) });
33186
34707
  if (!response.ok) {
33187
34708
  throw new Error(`session status request failed: ${response.status} ${response.statusText}`);
@@ -33283,11 +34804,19 @@ class MultiplexerSessionManager {
33283
34804
  return;
33284
34805
  this.spawningSessions.add(sessionId);
33285
34806
  try {
33286
- const serverRunning = await isServerRunning(this.serverUrl);
34807
+ const serverUrl = this.resolveServerUrl();
34808
+ if (!serverUrl) {
34809
+ log("[multiplexer-session-manager] no valid server URL, skipping respawn", {
34810
+ instanceId: this.instanceId,
34811
+ sessionId
34812
+ });
34813
+ return;
34814
+ }
34815
+ const serverRunning = await isServerRunning(serverUrl);
33287
34816
  if (!serverRunning) {
33288
34817
  log("[multiplexer-session-manager] server not running, skipping busy respawn", {
33289
34818
  instanceId: this.instanceId,
33290
- serverUrl: this.serverUrl,
34819
+ serverUrl,
33291
34820
  sessionId
33292
34821
  });
33293
34822
  return;
@@ -33301,7 +34830,7 @@ class MultiplexerSessionManager {
33301
34830
  parentId: known.parentId,
33302
34831
  title: known.title
33303
34832
  });
33304
- const paneResult = await this.multiplexer.spawnPane(sessionId, known.title, this.serverUrl, known.directory).catch((err) => {
34833
+ const paneResult = await this.multiplexer.spawnPane(sessionId, known.title, serverUrl, known.directory).catch((err) => {
33305
34834
  log("[multiplexer-session-manager] failed to respawn pane", {
33306
34835
  instanceId: this.instanceId,
33307
34836
  error: String(err)
@@ -33358,11 +34887,15 @@ class MultiplexerSessionManager {
33358
34887
  return this.backgroundJobBoard?.deferIfRunning(sessionId) ?? true;
33359
34888
  }
33360
34889
  async closeSessionFromCoordinator(sessionId) {
34890
+ if (this.cmuxLifecycle)
34891
+ return this.cmuxLifecycle.closeSessionFromCoordinator(sessionId);
33361
34892
  if (!this.enabled)
33362
34893
  return;
33363
34894
  await this.closeSession(sessionId, "idle", true);
33364
34895
  }
33365
34896
  async cleanup() {
34897
+ if (this.cmuxLifecycle)
34898
+ return this.cmuxLifecycle.cleanup();
33366
34899
  this.stopPolling();
33367
34900
  if (this.closingSessions.size > 0) {
33368
34901
  await Promise.all(this.closingSessions.values());
@@ -33384,27 +34917,10 @@ class MultiplexerSessionManager {
33384
34917
  this.closingSessions.clear();
33385
34918
  log("[multiplexer-session-manager] cleanup complete");
33386
34919
  }
33387
- }
33388
- // src/multiplexer/types.ts
33389
- async function isServerRunning(serverUrl, timeoutMs = 3000, maxAttempts = 2) {
33390
- const healthUrl = new URL("/health", serverUrl).toString();
33391
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
33392
- const controller = new AbortController;
33393
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
33394
- let response = null;
33395
- try {
33396
- response = await fetch(healthUrl, { signal: controller.signal }).catch(() => null);
33397
- } finally {
33398
- clearTimeout(timeout);
33399
- }
33400
- if (response?.ok) {
33401
- return true;
33402
- }
33403
- if (attempt < maxAttempts) {
33404
- await new Promise((r) => setTimeout(r, 250));
33405
- }
34920
+ async cleanupOnInstanceDisposed() {
34921
+ if (this.cmuxLifecycle)
34922
+ await this.cmuxLifecycle.cleanup();
33406
34923
  }
33407
- return false;
33408
34924
  }
33409
34925
  // src/tools/acp-run.ts
33410
34926
  import { spawn as spawn3 } from "node:child_process";
@@ -33605,7 +35121,7 @@ class AcpClient {
33605
35121
  return;
33606
35122
  this.lastUpdate = Date.now();
33607
35123
  const update = message.params?.update;
33608
- if (!isRecord2(update))
35124
+ if (!isRecord3(update))
33609
35125
  return;
33610
35126
  collectText(update, this.chunks);
33611
35127
  }
@@ -33685,16 +35201,16 @@ function createAcpRunTool(agents = {}) {
33685
35201
  });
33686
35202
  }
33687
35203
  function readSessionId(value) {
33688
- if (!isRecord2(value) || typeof value.sessionId !== "string") {
35204
+ if (!isRecord3(value) || typeof value.sessionId !== "string") {
33689
35205
  throw new Error("ACP agent did not return a sessionId");
33690
35206
  }
33691
35207
  return value.sessionId;
33692
35208
  }
33693
35209
  function readAuthMethods(value) {
33694
- if (!isRecord2(value) || !Array.isArray(value.authMethods))
35210
+ if (!isRecord3(value) || !Array.isArray(value.authMethods))
33695
35211
  return [];
33696
35212
  const methods = value.authMethods;
33697
- return methods.filter(isRecord2);
35213
+ return methods.filter(isRecord3);
33698
35214
  }
33699
35215
  function rpcError(error) {
33700
35216
  const err = new Error(error.message ?? "ACP request failed");
@@ -33708,11 +35224,11 @@ function isAuthError(error) {
33708
35224
  const meta = error;
33709
35225
  return meta.code === -32001 || error.message.toLowerCase().includes("auth_required") || error.message.toLowerCase().includes("auth required");
33710
35226
  }
33711
- function isRecord2(value) {
35227
+ function isRecord3(value) {
33712
35228
  return typeof value === "object" && value !== null && !Array.isArray(value);
33713
35229
  }
33714
35230
  function readPermissionTitle(params) {
33715
- const tool2 = isRecord2(params?.toolCall) ? params.toolCall : undefined;
35231
+ const tool2 = isRecord3(params?.toolCall) ? params.toolCall : undefined;
33716
35232
  if (typeof tool2?.title === "string")
33717
35233
  return tool2.title;
33718
35234
  if (typeof params?.permission === "string")
@@ -33721,7 +35237,7 @@ function readPermissionTitle(params) {
33721
35237
  }
33722
35238
  function selectPermissionOption(params, mode) {
33723
35239
  const options = Array.isArray(params?.options) ? params.options : [];
33724
- const choices = options.filter(isRecord2).filter((item) => typeof item.optionId === "string");
35240
+ const choices = options.filter(isRecord3).filter((item) => typeof item.optionId === "string");
33725
35241
  const reject = choices.find((item) => typeof item.kind === "string" && item.kind.startsWith("reject"));
33726
35242
  if (mode === "reject")
33727
35243
  return reject?.optionId;
@@ -33738,7 +35254,7 @@ function collectText(update, chunks) {
33738
35254
  function readText(value) {
33739
35255
  if (typeof value === "string")
33740
35256
  return value;
33741
- if (isRecord2(value) && typeof value.text === "string")
35257
+ if (isRecord3(value) && typeof value.text === "string")
33742
35258
  return value.text;
33743
35259
  return;
33744
35260
  }
@@ -33747,15 +35263,15 @@ import { tool as tool2 } from "@opencode-ai/plugin";
33747
35263
 
33748
35264
  // src/tools/ast-grep/cli.ts
33749
35265
  init_compat();
33750
- import { existsSync as existsSync11 } from "node:fs";
35266
+ import { existsSync as existsSync12 } from "node:fs";
33751
35267
 
33752
35268
  // src/tools/ast-grep/constants.ts
33753
- import { existsSync as existsSync10, statSync as statSync6 } from "node:fs";
35269
+ import { existsSync as existsSync11, statSync as statSync6 } from "node:fs";
33754
35270
  import { createRequire as createRequire3 } from "node:module";
33755
35271
  import { dirname as dirname10, join as join15 } from "node:path";
33756
35272
 
33757
35273
  // src/tools/ast-grep/downloader.ts
33758
- import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "node:fs";
35274
+ import { chmodSync as chmodSync2, existsSync as existsSync10, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "node:fs";
33759
35275
  import { createRequire as createRequire2 } from "node:module";
33760
35276
  import { homedir as homedir6 } from "node:os";
33761
35277
  import { join as join14 } from "node:path";
@@ -33795,7 +35311,7 @@ function getBinaryName() {
33795
35311
  }
33796
35312
  function getCachedBinaryPath() {
33797
35313
  const binaryPath = join14(getCacheDir2(), getBinaryName());
33798
- return existsSync9(binaryPath) ? binaryPath : null;
35314
+ return existsSync10(binaryPath) ? binaryPath : null;
33799
35315
  }
33800
35316
  async function downloadAstGrep(version = DEFAULT_VERSION) {
33801
35317
  const platformKey = `${process.platform}-${process.arch}`;
@@ -33807,7 +35323,7 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
33807
35323
  const cacheDir = getCacheDir2();
33808
35324
  const binaryName = getBinaryName();
33809
35325
  const binaryPath = join14(cacheDir, binaryName);
33810
- if (existsSync9(binaryPath)) {
35326
+ if (existsSync10(binaryPath)) {
33811
35327
  return binaryPath;
33812
35328
  }
33813
35329
  const { arch, os: os6 } = platformInfo;
@@ -33815,7 +35331,7 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
33815
35331
  const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`;
33816
35332
  console.log(`[oh-my-opencode-slim] Downloading ast-grep binary...`);
33817
35333
  try {
33818
- if (!existsSync9(cacheDir)) {
35334
+ if (!existsSync10(cacheDir)) {
33819
35335
  mkdirSync7(cacheDir, { recursive: true });
33820
35336
  }
33821
35337
  const response = await fetch(downloadUrl, { redirect: "follow" });
@@ -33826,10 +35342,10 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
33826
35342
  const arrayBuffer = await response.arrayBuffer();
33827
35343
  await crossWrite(archivePath, arrayBuffer);
33828
35344
  await extractZip(archivePath, cacheDir);
33829
- if (existsSync9(archivePath)) {
35345
+ if (existsSync10(archivePath)) {
33830
35346
  unlinkSync5(archivePath);
33831
35347
  }
33832
- if (process.platform !== "win32" && existsSync9(binaryPath)) {
35348
+ if (process.platform !== "win32" && existsSync10(binaryPath)) {
33833
35349
  chmodSync2(binaryPath, 493);
33834
35350
  }
33835
35351
  console.log(`[oh-my-opencode-slim] ast-grep binary ready.`);
@@ -33912,7 +35428,7 @@ function findSgCliPathSync() {
33912
35428
  const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
33913
35429
  const cliDir = dirname10(cliPkgPath);
33914
35430
  const sgPath = join15(cliDir, binaryName);
33915
- if (existsSync10(sgPath) && isValidBinary(sgPath)) {
35431
+ if (existsSync11(sgPath) && isValidBinary(sgPath)) {
33916
35432
  return sgPath;
33917
35433
  }
33918
35434
  } catch {}
@@ -33924,7 +35440,7 @@ function findSgCliPathSync() {
33924
35440
  const pkgDir = dirname10(pkgPath);
33925
35441
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
33926
35442
  const binaryPath = join15(pkgDir, astGrepName);
33927
- if (existsSync10(binaryPath) && isValidBinary(binaryPath)) {
35443
+ if (existsSync11(binaryPath) && isValidBinary(binaryPath)) {
33928
35444
  return binaryPath;
33929
35445
  }
33930
35446
  } catch {}
@@ -33932,7 +35448,7 @@ function findSgCliPathSync() {
33932
35448
  if (process.platform === "darwin") {
33933
35449
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
33934
35450
  for (const path18 of homebrewPaths) {
33935
- if (existsSync10(path18) && isValidBinary(path18)) {
35451
+ if (existsSync11(path18) && isValidBinary(path18)) {
33936
35452
  return path18;
33937
35453
  }
33938
35454
  }
@@ -33961,7 +35477,7 @@ var DEFAULT_MAX_MATCHES = 500;
33961
35477
  var initPromise = null;
33962
35478
  async function getAstGrepPath() {
33963
35479
  const currentPath = getSgCliPath();
33964
- if (currentPath !== "sg" && existsSync11(currentPath)) {
35480
+ if (currentPath !== "sg" && existsSync12(currentPath)) {
33965
35481
  return currentPath;
33966
35482
  }
33967
35483
  if (initPromise) {
@@ -33969,7 +35485,7 @@ async function getAstGrepPath() {
33969
35485
  }
33970
35486
  initPromise = (async () => {
33971
35487
  const syncPath = findSgCliPathSync();
33972
- if (syncPath && existsSync11(syncPath)) {
35488
+ if (syncPath && existsSync12(syncPath)) {
33973
35489
  setSgCliPath(syncPath);
33974
35490
  return syncPath;
33975
35491
  }
@@ -34008,7 +35524,7 @@ async function runSg(options) {
34008
35524
  const paths2 = options.paths && options.paths.length > 0 ? options.paths : ["."];
34009
35525
  args.push(...paths2);
34010
35526
  let cliPath = getSgCliPath();
34011
- if (!existsSync11(cliPath) && cliPath !== "sg") {
35527
+ if (!existsSync12(cliPath) && cliPath !== "sg") {
34012
35528
  const downloadedPath = await getAstGrepPath();
34013
35529
  if (downloadedPath) {
34014
35530
  cliPath = downloadedPath;
@@ -34620,7 +36136,7 @@ async function getSessionStatus(client, taskID) {
34620
36136
  try {
34621
36137
  const result = await client.session.status();
34622
36138
  const data = result.data;
34623
- if (!isRecord(data)) {
36139
+ if (!isRecord2(data)) {
34624
36140
  return { status: undefined, source: "invalid-data", keys: [] };
34625
36141
  }
34626
36142
  const keys = Object.keys(data).slice(0, 20);
@@ -34628,14 +36144,14 @@ async function getSessionStatus(client, taskID) {
34628
36144
  if (item === undefined) {
34629
36145
  return { status: "idle", source: "missing-from-map", keys };
34630
36146
  }
34631
- if (isRecord(item) && typeof item.type === "string") {
36147
+ if (isRecord2(item) && typeof item.type === "string") {
34632
36148
  return { status: item.type, source: "task-map-entry", keys };
34633
36149
  }
34634
36150
  if (typeof data.type === "string") {
34635
36151
  return { status: data.type, source: "legacy-data-type", keys };
34636
36152
  }
34637
36153
  const nested = data.status;
34638
- if (isRecord(nested) && typeof nested.type === "string") {
36154
+ if (isRecord2(nested) && typeof nested.type === "string") {
34639
36155
  return { status: nested.type, source: "legacy-data-status", keys };
34640
36156
  }
34641
36157
  return { status: undefined, source: "unknown-shape", keys };
@@ -34664,7 +36180,7 @@ async function getSessionParentID(client, taskID) {
34664
36180
  try {
34665
36181
  const response = await session2.get({ path: { id: taskID } });
34666
36182
  const data = response.data;
34667
- if (!isRecord(data))
36183
+ if (!isRecord2(data))
34668
36184
  return;
34669
36185
  const parentID = data.parentID;
34670
36186
  return typeof parentID === "string" ? parentID : undefined;
@@ -34757,6 +36273,7 @@ Returns the councillor responses with a summary footer.`,
34757
36273
  import * as fs10 from "node:fs";
34758
36274
 
34759
36275
  // src/tui-state.ts
36276
+ import { createHash as createHash4 } from "node:crypto";
34760
36277
  import * as fs9 from "node:fs";
34761
36278
  import * as os6 from "node:os";
34762
36279
  import * as path18 from "node:path";
@@ -34765,8 +36282,11 @@ var STATE_FILE = "tui-state.json";
34765
36282
  function dataDir() {
34766
36283
  return process.env.XDG_DATA_HOME ?? path18.join(os6.homedir(), ".local", "share");
34767
36284
  }
34768
- function getTuiStatePath() {
34769
- return path18.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
36285
+ function projectScope(projectDir) {
36286
+ return createHash4("sha256").update(path18.resolve(projectDir)).digest("hex").slice(0, 12);
36287
+ }
36288
+ function getTuiStatePath(projectDir) {
36289
+ return path18.join(dataDir(), "opencode", "storage", STATE_DIR, projectScope(projectDir), STATE_FILE);
34770
36290
  }
34771
36291
  function emptySnapshot() {
34772
36292
  return {
@@ -34787,42 +36307,42 @@ function parseSnapshot(value) {
34787
36307
  agentVariants: parsed.agentVariants ?? {}
34788
36308
  };
34789
36309
  }
34790
- function readTuiSnapshot() {
36310
+ function readTuiSnapshot(projectDir) {
34791
36311
  try {
34792
- return parseSnapshot(fs9.readFileSync(getTuiStatePath(), "utf8"));
36312
+ return parseSnapshot(fs9.readFileSync(getTuiStatePath(projectDir), "utf8"));
34793
36313
  } catch {
34794
36314
  return emptySnapshot();
34795
36315
  }
34796
36316
  }
34797
- async function readTuiSnapshotAsync() {
36317
+ async function readTuiSnapshotAsync(projectDir) {
34798
36318
  try {
34799
- return parseSnapshot(await fs9.promises.readFile(getTuiStatePath(), "utf8"));
36319
+ return parseSnapshot(await fs9.promises.readFile(getTuiStatePath(projectDir), "utf8"));
34800
36320
  } catch {
34801
36321
  return emptySnapshot();
34802
36322
  }
34803
36323
  }
34804
- function writeTuiSnapshot(snapshot) {
36324
+ function writeTuiSnapshot(snapshot, projectDir) {
34805
36325
  try {
34806
- const filePath = getTuiStatePath();
36326
+ const filePath = getTuiStatePath(projectDir);
34807
36327
  fs9.mkdirSync(path18.dirname(filePath), { recursive: true });
34808
36328
  fs9.writeFileSync(filePath, `${JSON.stringify(snapshot)}
34809
36329
  `);
34810
36330
  } catch {}
34811
36331
  }
34812
- function updateSnapshot(mutator) {
34813
- const snapshot = readTuiSnapshot();
36332
+ function updateSnapshot(projectDir, mutator) {
36333
+ const snapshot = readTuiSnapshot(projectDir);
34814
36334
  mutator(snapshot);
34815
36335
  snapshot.updatedAt = Date.now();
34816
- writeTuiSnapshot(snapshot);
36336
+ writeTuiSnapshot(snapshot, projectDir);
34817
36337
  }
34818
- function recordTuiAgentModels(input) {
34819
- updateSnapshot((snapshot) => {
36338
+ function recordTuiAgentModels(input, projectDir) {
36339
+ updateSnapshot(projectDir, (snapshot) => {
34820
36340
  snapshot.agentModels = { ...input.agentModels };
34821
36341
  snapshot.agentVariants = { ...input.agentVariants ?? {} };
34822
36342
  });
34823
36343
  }
34824
- function recordTuiAgentModel(input) {
34825
- updateSnapshot((snapshot) => {
36344
+ function recordTuiAgentModel(input, projectDir) {
36345
+ updateSnapshot(projectDir, (snapshot) => {
34826
36346
  snapshot.agentModels[input.agentName] = input.model;
34827
36347
  if (input.variant !== undefined) {
34828
36348
  if (input.variant === null) {
@@ -34900,7 +36420,7 @@ function createPresetManager(ctx, config) {
34900
36420
  `);
34901
36421
  }
34902
36422
  } catch {}
34903
- const snapshot = readTuiSnapshot();
36423
+ const snapshot = readTuiSnapshot(ctx.directory);
34904
36424
  const agentModels = { ...snapshot.agentModels };
34905
36425
  const agentVariants = { ...snapshot.agentVariants };
34906
36426
  for (const [agentName, agentConfig] of Object.entries(agentUpdates)) {
@@ -34913,7 +36433,7 @@ function createPresetManager(ctx, config) {
34913
36433
  delete agentVariants[agentName];
34914
36434
  }
34915
36435
  }
34916
- recordTuiAgentModels({ agentModels, agentVariants });
36436
+ recordTuiAgentModels({ agentModels, agentVariants }, ctx.directory);
34917
36437
  activePreset = presetName;
34918
36438
  const summaryParts = [];
34919
36439
  for (const [name, cfg] of Object.entries(agentUpdates)) {
@@ -35878,9 +37398,6 @@ function cleanFetchedMarkdown(input) {
35878
37398
  function cleanFetchedText(input) {
35879
37399
  return trimBlankRuns(input);
35880
37400
  }
35881
- function escapeHtml2(input) {
35882
- return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
35883
- }
35884
37401
  function withTruncationMarker(content, format, truncated) {
35885
37402
  if (!truncated)
35886
37403
  return content;
@@ -35919,7 +37436,7 @@ ${content}`;
35919
37436
  }
35920
37437
  function renderMessageForFormat(content, format) {
35921
37438
  if (format === "html")
35922
- return `<pre>${escapeHtml2(content)}</pre>`;
37439
+ return `<pre>${escapeHtml(content)}</pre>`;
35923
37440
  return content;
35924
37441
  }
35925
37442
  function buildRedirectResultMessage(originalUrl, redirectUrl, statusCode) {
@@ -36016,22 +37533,9 @@ async function extractFromHtml(html, finalUrl, extractMain) {
36016
37533
  headings
36017
37534
  };
36018
37535
  }
36019
- function parseFrontmatterBlock(content) {
36020
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
36021
- if (!match)
36022
- return;
36023
- const result = {};
36024
- for (const line of match[1].split(/\r?\n/)) {
36025
- const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.+?)\s*$/);
36026
- if (!kv)
36027
- continue;
36028
- result[kv[1]] = kv[2].replace(/^(['"])(.*)\1$/, "$2");
36029
- }
36030
- return result;
36031
- }
36032
37536
  function inferCanonicalUrlFromText(content, finalUrl) {
36033
- const frontmatter2 = parseFrontmatterBlock(content);
36034
- const raw = frontmatter2?.url;
37537
+ const frontmatterData = parseFrontmatter(content);
37538
+ const raw = frontmatterData?.url;
36035
37539
  if (!raw)
36036
37540
  return;
36037
37541
  try {
@@ -36598,7 +38102,7 @@ function isInvalidLlmsResult(fetchResult) {
36598
38102
  }
36599
38103
 
36600
38104
  // src/tools/smartfetch/secondary-model.ts
36601
- import { existsSync as existsSync12 } from "node:fs";
38105
+ import { existsSync as existsSync13 } from "node:fs";
36602
38106
  import { readFile as readFile4 } from "node:fs/promises";
36603
38107
  import path21 from "node:path";
36604
38108
  function parseModelRef(value) {
@@ -36627,7 +38131,7 @@ function pickAgentModelRef(value) {
36627
38131
  function findPreferredOpenCodeConfigPath(baseDir) {
36628
38132
  for (const file of ["opencode.jsonc", "opencode.json"]) {
36629
38133
  const fullPath = path21.join(baseDir, file);
36630
- if (existsSync12(fullPath))
38134
+ if (existsSync13(fullPath))
36631
38135
  return fullPath;
36632
38136
  }
36633
38137
  return;
@@ -36743,7 +38247,7 @@ async function deleteSessionSafely(client, sessionId, directory) {
36743
38247
  console.warn(`[smartfetch] Failed to clean up secondary session ${sessionId} ` + `after ${SESSION_DELETE_RETRIES} attempts: ` + (error instanceof Error ? error.message : String(error)));
36744
38248
  return;
36745
38249
  }
36746
- await new Promise((resolve4) => setTimeout(resolve4, _testConfig.deleteRetryDelayMs));
38250
+ await new Promise((resolve5) => setTimeout(resolve5, _testConfig.deleteRetryDelayMs));
36747
38251
  }
36748
38252
  }
36749
38253
  }
@@ -37297,7 +38801,7 @@ function createWebfetchTool(pluginCtx, options = {}) {
37297
38801
  secondary_model: `${secondaryRun.model.providerID}/${secondaryRun.model.modelID}`
37298
38802
  }) : "";
37299
38803
  const secondaryRaw = secondaryRun.text || "No response from secondary model.";
37300
- const secondaryContent = args.format === "html" ? withTruncationMarker(`<pre>${escapeHtml2(secondaryRaw)}</pre>`, "html", false) : withTruncationMarker(secondaryRaw, args.format, false);
38804
+ const secondaryContent = args.format === "html" ? withTruncationMarker(`<pre>${escapeHtml(secondaryRaw)}</pre>`, "html", false) : withTruncationMarker(secondaryRaw, args.format, false);
37301
38805
  return joinRenderedContent(metadataWithSecondary, secondaryContent, args.format);
37302
38806
  } finally {
37303
38807
  clearTimeout(timeout);
@@ -37416,6 +38920,7 @@ var OhMyOpenCodeLite = async (ctx) => {
37416
38920
  let agents;
37417
38921
  let mcps;
37418
38922
  let modelArrayMap;
38923
+ let everModelSwitched;
37419
38924
  let runtimeChains;
37420
38925
  let multiplexerConfig;
37421
38926
  let multiplexerEnabled;
@@ -37423,6 +38928,7 @@ var OhMyOpenCodeLite = async (ctx) => {
37423
38928
  let multiplexerSessionManager;
37424
38929
  let autoUpdateChecker;
37425
38930
  let sessionAgentMap;
38931
+ const sessionDirectories = new Map;
37426
38932
  let sessionLifecycle;
37427
38933
  let chatHeadersHook;
37428
38934
  let foregroundFallback;
@@ -37466,6 +38972,7 @@ var OhMyOpenCodeLite = async (ctx) => {
37466
38972
  agentDefs = createAgents(config, { projectDirectory: ctx.directory });
37467
38973
  agents = getAgentConfigs(config, { projectDirectory: ctx.directory });
37468
38974
  modelArrayMap = {};
38975
+ everModelSwitched = new Set;
37469
38976
  runtimeChains = {};
37470
38977
  for (const agentDef of agentDefs) {
37471
38978
  if (agentDef._modelArray?.length) {
@@ -37495,9 +39002,9 @@ var OhMyOpenCodeLite = async (ctx) => {
37495
39002
  acpRunTools = Object.keys(config.acpAgents ?? {}).length > 0 ? { acp_run: createAcpRunTool(config.acpAgents) } : {};
37496
39003
  webfetch = createWebfetchTool(ctx);
37497
39004
  backgroundJobBoard = new BackgroundJobBoard({
37498
- maxReusablePerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
37499
- readContextMinLines: config.backgroundJobs?.readContextMinLines ?? 10,
37500
- readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ?? 8
39005
+ maxReusablePerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? DEFAULT_MAX_SESSIONS_PER_AGENT,
39006
+ readContextMinLines: config.backgroundJobs?.readContextMinLines ?? DEFAULT_READ_CONTEXT_MIN_LINES,
39007
+ readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ?? DEFAULT_READ_CONTEXT_MAX_FILES
37501
39008
  });
37502
39009
  const backgroundJobCoordinator = new BackgroundJobCoordinator(backgroundJobBoard);
37503
39010
  multiplexerSessionManager = new MultiplexerSessionManager(ctx, multiplexerConfig, backgroundJobCoordinator);
@@ -37511,14 +39018,14 @@ var OhMyOpenCodeLite = async (ctx) => {
37511
39018
  });
37512
39019
  sessionAgentMap = new Map;
37513
39020
  chatHeadersHook = createChatHeadersHook(ctx);
37514
- foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false, config.fallback?.maxRetries ?? 3, sessionLifecycle, config.fallback?.runtimeOverride ?? true);
39021
+ foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false, config.fallback?.maxRetries ?? 3, sessionLifecycle);
37515
39022
  deepworkCommandHook = createDeepworkCommandHook();
37516
39023
  reflectCommandHook = createReflectCommandHook();
37517
39024
  loopCommandHook = createLoopCommandHook();
37518
39025
  taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
37519
- maxSessionsPerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
37520
- readContextMinLines: config.backgroundJobs?.readContextMinLines ?? 10,
37521
- readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ?? 8,
39026
+ maxSessionsPerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? DEFAULT_MAX_SESSIONS_PER_AGENT,
39027
+ readContextMinLines: config.backgroundJobs?.readContextMinLines ?? DEFAULT_READ_CONTEXT_MIN_LINES,
39028
+ readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ?? DEFAULT_READ_CONTEXT_MAX_FILES,
37522
39029
  backgroundJobBoard: backgroundJobCoordinator,
37523
39030
  shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator",
37524
39031
  registerSessionAsOrchestrator: (sessionID) => {
@@ -37658,6 +39165,15 @@ var OhMyOpenCodeLite = async (ctx) => {
37658
39165
  } else {
37659
39166
  for (const [name, pluginAgent] of Object.entries(agents)) {
37660
39167
  const existing = opencodeConfig.agent[name];
39168
+ if (existing && typeof existing.model === "string") {
39169
+ const primary = modelArrayMap[name]?.[0]?.id;
39170
+ if (primary && existing.model !== primary) {
39171
+ everModelSwitched.add(name);
39172
+ }
39173
+ if (everModelSwitched.has(name)) {
39174
+ foregroundFallback.disableChain(name);
39175
+ }
39176
+ }
37661
39177
  if (existing) {
37662
39178
  opencodeConfig.agent[name] = {
37663
39179
  ...pluginAgent,
@@ -37790,6 +39306,13 @@ var OhMyOpenCodeLite = async (ctx) => {
37790
39306
  recordTuiAgentModels({
37791
39307
  agentModels: tuiAgentModels,
37792
39308
  agentVariants: tuiAgentVariants
39309
+ }, ctx.directory);
39310
+ applyOrchestratorModelConfig({
39311
+ agents: configAgent,
39312
+ enabled: config.stripOrchestratorModel,
39313
+ presets: config.presets,
39314
+ configPreset: config.preset,
39315
+ runtimePreset: runtimePresetName
37793
39316
  });
37794
39317
  const configMcp = opencodeConfig.mcp;
37795
39318
  if (!configMcp) {
@@ -37839,7 +39362,7 @@ var OhMyOpenCodeLite = async (ctx) => {
37839
39362
  agentName,
37840
39363
  model,
37841
39364
  variant: variant ?? null
37842
- });
39365
+ }, (info?.sessionID && sessionDirectories.get(info.sessionID)) ?? ctx.directory);
37843
39366
  }
37844
39367
  }
37845
39368
  if (event.type === "session.created") {
@@ -37848,10 +39371,18 @@ var OhMyOpenCodeLite = async (ctx) => {
37848
39371
  if (depthTracker && childSessionId && parentSessionId) {
37849
39372
  depthTracker.registerChild(parentSessionId, childSessionId);
37850
39373
  }
39374
+ const createdSessionId = event.properties?.info?.id;
39375
+ const createdSessionDir = event.properties?.info?.directory;
39376
+ if (createdSessionId && createdSessionDir) {
39377
+ sessionDirectories.set(createdSessionId, createdSessionDir);
39378
+ }
37851
39379
  }
37852
39380
  await multiplexerSessionManager.onSessionCreated(event);
37853
39381
  await multiplexerSessionManager.onSessionStatus(event);
37854
39382
  await multiplexerSessionManager.onSessionDeleted(event);
39383
+ if (event.type === "server.instance.disposed") {
39384
+ await multiplexerSessionManager.cleanupOnInstanceDisposed();
39385
+ }
37855
39386
  await foregroundFallback.handleEvent(input.event);
37856
39387
  await autoUpdateChecker.event(input);
37857
39388
  await interviewManager.handleEvent(input);
@@ -37883,6 +39414,7 @@ var OhMyOpenCodeLite = async (ctx) => {
37883
39414
  }
37884
39415
  if (sessionID) {
37885
39416
  sessionAgentMap.delete(sessionID);
39417
+ sessionDirectories.delete(sessionID);
37886
39418
  }
37887
39419
  }
37888
39420
  },
@@ -37905,6 +39437,7 @@ var OhMyOpenCodeLite = async (ctx) => {
37905
39437
  output.message.agent = agent;
37906
39438
  }
37907
39439
  if (agent) {
39440
+ foregroundFallback.registerSessionAgent(input.sessionID, agent);
37908
39441
  sessionAgentMap.set(input.sessionID, agent);
37909
39442
  companionManager.onSessionStatus({
37910
39443
  sessionId: input.sessionID,
@@ -37931,6 +39464,9 @@ ${output.system[0]}` : "");
37931
39464
  "experimental.chat.messages.transform": async (input, output) => {
37932
39465
  const typedOutput = output;
37933
39466
  for (const message of typedOutput.messages) {
39467
+ if (!isMessageWithParts(message)) {
39468
+ continue;
39469
+ }
37934
39470
  if (message.info.role !== "user") {
37935
39471
  continue;
37936
39472
  }
@@ -37944,6 +39480,7 @@ ${output.system[0]}` : "");
37944
39480
  processImageAttachments({
37945
39481
  messages: typedOutput.messages,
37946
39482
  workDir: ctx.directory,
39483
+ imageRouting: resolveImageRouting(config.image_routing),
37947
39484
  disabledAgents,
37948
39485
  log
37949
39486
  });