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.
- package/README.ja-JP.md +113 -118
- package/README.ko-KR.md +141 -123
- package/README.md +102 -145
- package/README.zh-CN.md +112 -118
- package/dist/agents/index.d.ts +0 -4
- package/dist/cli/index.js +57 -20
- package/dist/cli/providers.d.ts +11 -4
- package/dist/config/constants.d.ts +11 -1
- package/dist/config/schema.d.ts +680 -2
- package/dist/config/strip-orchestrator-model.d.ts +9 -0
- package/dist/hooks/command-hook-utils.d.ts +5 -0
- package/dist/hooks/foreground-fallback/index.d.ts +40 -28
- package/dist/hooks/image-hook.d.ts +1 -0
- package/dist/hooks/index.d.ts +1 -1
- package/dist/index.js +1992 -455
- package/dist/interview/document.d.ts +2 -1
- package/dist/interview/ui.d.ts +0 -1
- package/dist/multiplexer/cmux/close-policy.d.ts +20 -0
- package/dist/multiplexer/cmux/index.d.ts +102 -0
- package/dist/multiplexer/cmux/session-lifecycle.d.ts +92 -0
- package/dist/multiplexer/cmux/session-state.d.ts +45 -0
- package/dist/multiplexer/factory.d.ts +0 -9
- package/dist/multiplexer/herdr/index.d.ts +2 -0
- package/dist/multiplexer/index.d.ts +3 -1
- package/dist/multiplexer/session-manager.d.ts +10 -2
- package/dist/multiplexer/shared.d.ts +8 -1
- package/dist/multiplexer/types.d.ts +5 -2
- package/dist/multiplexer/zellij/index.d.ts +1 -1
- package/dist/tools/smartfetch/utils.d.ts +3 -1
- package/dist/tui-state.d.ts +5 -5
- package/dist/tui.js +87 -27
- package/dist/utils/escape-html.d.ts +1 -0
- package/dist/utils/frontmatter.d.ts +6 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/logger.d.ts +0 -2
- package/oh-my-opencode-slim.schema.json +717 -2
- package/package.json +1 -1
- package/src/skills/clonedeps/SKILL.md +32 -29
- package/src/skills/codemap.md +5 -3
- package/src/skills/deepwork/SKILL.md +17 -6
- package/src/skills/verification-planning/SKILL.md +102 -0
- package/src/skills/worktrees/SKILL.md +14 -7
- 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().
|
|
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
|
-
|
|
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 •
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
19360
|
-
-
|
|
19361
|
-
-
|
|
19362
|
-
-
|
|
19363
|
-
-
|
|
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
|
-
|
|
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 ??
|
|
24700
|
-
this.readContextMinLines = options.readContextMinLines ??
|
|
24701
|
-
this.readContextMaxFiles = options.readContextMaxFiles ??
|
|
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
|
|
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 (!
|
|
25356
|
+
if (!isRecord2(part) || part.type !== "text") {
|
|
25260
25357
|
return false;
|
|
25261
25358
|
}
|
|
25262
|
-
if (part.synthetic !== true || !
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
25588
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 &&
|
|
25657
|
-
if (this.
|
|
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
|
|
25668
|
-
|
|
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
|
|
25839
|
+
if (!props)
|
|
25675
25840
|
break;
|
|
25676
|
-
const
|
|
25677
|
-
if (
|
|
25678
|
-
|
|
25679
|
-
|
|
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
|
-
|
|
25682
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
25898
|
+
shouldTriggerFallback(sessionID) {
|
|
25720
25899
|
const tried = this.sessionRetries.get(sessionID) ?? 0;
|
|
25721
25900
|
if (tried === 0)
|
|
25722
25901
|
return true;
|
|
25723
|
-
return this.
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
26060
|
-
|
|
26061
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
26538
|
-
|
|
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 (!
|
|
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 &&
|
|
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 || !
|
|
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 (
|
|
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
|
-
|
|
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/
|
|
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.
|
|
27545
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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 =
|
|
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 =
|
|
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/
|
|
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/
|
|
32169
|
-
|
|
32170
|
-
|
|
32171
|
-
|
|
32172
|
-
|
|
32173
|
-
|
|
32174
|
-
|
|
32175
|
-
|
|
32176
|
-
|
|
32177
|
-
|
|
32178
|
-
|
|
32179
|
-
|
|
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.
|
|
32183
|
-
return
|
|
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.
|
|
32186
|
-
|
|
32187
|
-
return this.binaryPath !== null;
|
|
32435
|
+
this.availabilityError = version ? "hard" : this.client.getVersionError?.() ?? "unavailable";
|
|
32436
|
+
return false;
|
|
32188
32437
|
}
|
|
32189
32438
|
isInsideSession() {
|
|
32190
|
-
return
|
|
32191
|
-
}
|
|
32192
|
-
async spawnPane(sessionId,
|
|
32193
|
-
|
|
32194
|
-
|
|
32195
|
-
|
|
32196
|
-
|
|
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
|
-
|
|
32199
|
-
|
|
32200
|
-
|
|
32201
|
-
|
|
32202
|
-
|
|
32203
|
-
|
|
32204
|
-
|
|
32205
|
-
|
|
32206
|
-
|
|
32207
|
-
|
|
32208
|
-
|
|
32209
|
-
|
|
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
|
|
33467
|
+
const firstPane2 = await this.getFirstPaneInTab(zellij, existingTab.tabId);
|
|
32693
33468
|
return {
|
|
32694
33469
|
tabId: existingTab.tabId,
|
|
32695
|
-
firstPaneId:
|
|
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
|
|
32707
|
-
|
|
32708
|
-
|
|
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
|
-
|
|
32715
|
-
|
|
32716
|
-
|
|
32717
|
-
|
|
32718
|
-
|
|
32719
|
-
|
|
32720
|
-
|
|
32721
|
-
|
|
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
|
|
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
|
-
|
|
32799
|
-
|
|
32800
|
-
|
|
32801
|
-
|
|
32802
|
-
|
|
32803
|
-
|
|
32804
|
-
|
|
32805
|
-
|
|
32806
|
-
|
|
32807
|
-
|
|
32808
|
-
|
|
32809
|
-
|
|
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
|
-
|
|
32813
|
-
|
|
32814
|
-
|
|
32815
|
-
|
|
32816
|
-
|
|
32817
|
-
|
|
32818
|
-
|
|
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
|
|
32822
|
-
|
|
32823
|
-
|
|
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
|
-
|
|
32826
|
-
|
|
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
|
-
|
|
32829
|
-
if (this.
|
|
32830
|
-
|
|
32831
|
-
if (this.
|
|
32832
|
-
|
|
32833
|
-
|
|
32834
|
-
|
|
32835
|
-
|
|
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
|
|
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
|
-
|
|
32844
|
-
|
|
32845
|
-
|
|
32846
|
-
|
|
32847
|
-
|
|
32848
|
-
|
|
32849
|
-
|
|
32850
|
-
|
|
32851
|
-
|
|
32852
|
-
|
|
32853
|
-
|
|
32854
|
-
|
|
32855
|
-
|
|
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
|
-
|
|
32859
|
-
|
|
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
|
-
|
|
32889
|
-
|
|
32890
|
-
|
|
32891
|
-
|
|
32892
|
-
|
|
32893
|
-
|
|
32894
|
-
|
|
32895
|
-
|
|
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
|
-
|
|
32920
|
-
|
|
32921
|
-
|
|
34365
|
+
const timer = setTimeout(() => void callback(), milliseconds);
|
|
34366
|
+
timer.unref?.();
|
|
34367
|
+
return { cancel: () => clearTimeout(timer) };
|
|
32922
34368
|
}
|
|
32923
|
-
|
|
32924
|
-
|
|
32925
|
-
|
|
32926
|
-
|
|
32927
|
-
|
|
32928
|
-
|
|
32929
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
33389
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
35210
|
+
if (!isRecord3(value) || !Array.isArray(value.authMethods))
|
|
33695
35211
|
return [];
|
|
33696
35212
|
const methods = value.authMethods;
|
|
33697
|
-
return methods.filter(
|
|
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
|
|
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 =
|
|
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(
|
|
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 (
|
|
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
|
|
35266
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
33751
35267
|
|
|
33752
35268
|
// src/tools/ast-grep/constants.ts
|
|
33753
|
-
import { existsSync as
|
|
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
|
|
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
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
35345
|
+
if (existsSync10(archivePath)) {
|
|
33830
35346
|
unlinkSync5(archivePath);
|
|
33831
35347
|
}
|
|
33832
|
-
if (process.platform !== "win32" &&
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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" &&
|
|
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 &&
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
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
|
|
34769
|
-
return path18.
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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>${
|
|
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
|
|
36034
|
-
const raw =
|
|
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
|
|
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 (
|
|
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((
|
|
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>${
|
|
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 ??
|
|
37499
|
-
readContextMinLines: config.backgroundJobs?.readContextMinLines ??
|
|
37500
|
-
readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ??
|
|
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
|
|
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 ??
|
|
37520
|
-
readContextMinLines: config.backgroundJobs?.readContextMinLines ??
|
|
37521
|
-
readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ??
|
|
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
|
});
|