Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
205 commits
Select commit Hold shift + click to select a range
2856267
feat(orchestration): support OpenCode worker model selection
brennanb2025 Sep 22, 2026
751d58a
fix: wait for OpenCode worker composer before first dispatch
nwparker Oct 2, 2026
648da96
feat(opencode): probe execution-host CLI capabilities
nwparker Oct 2, 2026
8be3e1b
fix(opencode): select plugin default for execution host loader
nwparker Oct 2, 2026
f96b46c
fix(opencode): limit prompt prefill capability to verified release
nwparker Oct 2, 2026
de557e0
feat(orchestration): gate OpenCode worker model preferences by host c…
nwparker Oct 2, 2026
32ac363
feat(opencode): probe launch capabilities on the execution host
nwparker Oct 2, 2026
6f8993a
feat(opencode): probe execution-host CLI capabilities
nwparker Oct 2, 2026
7a87a27
feat(opencode): probe launch capabilities on the execution host
nwparker Oct 2, 2026
7ed8f0f
feat(orchestration): resolve explicitly configured command aliases
nwparker Oct 2, 2026
8f06210
fix(opencode): select plugin loader for the launched host binary
nwparker Oct 2, 2026
c4a6af1
fix(opencode): match WSL probe cwd and declared guest environment
nwparker Oct 2, 2026
979c322
fix(orchestration): verify available OpenCode model on execution host
nwparker Oct 2, 2026
d8f11f8
Add host-owned OpenCode and Devin account profiles
nwparker Oct 2, 2026
7c22fa9
fix(orchestration): inspect OpenCode models with selected account env…
nwparker Oct 2, 2026
ae0a9d3
fix(orchestration): bind model validation to direct existing-workspac…
nwparker Oct 2, 2026
9356b67
fix(opencode): preserve launch environment deletion boundaries
nwparker Oct 2, 2026
ef0b55d
fix(orchestration): limit effective model contract to verified OpenCo…
nwparker Oct 2, 2026
7dd9e82
wip(opencode): authorize native startup prompt intent at execution owner
nwparker Oct 2, 2026
fc84887
fix(opencode): atomically replace status plugin entrypoints
AhmedNAgy25 Oct 2, 2026
8bbb511
fix(opencode): retain plugin permissions across restrictive umasks
nwparker Oct 2, 2026
ee2de6a
test(opencode): resolve permission fixture from primary cwd
nwparker Oct 2, 2026
d10b63a
feat(opencode): install startup prompt plugin independently of status…
nwparker Oct 2, 2026
ac48c77
Restore inherited account environment and preserve cleanup retries
nwparker Oct 2, 2026
1b024b9
fix(opencode): select plugin loader for the launched host binary
nwparker Oct 2, 2026
a6516bb
fix(opencode): match WSL probe cwd and declared guest environment
nwparker Oct 2, 2026
fb2b885
fix(opencode): preserve launch environment deletion boundaries
nwparker Oct 2, 2026
1749bfd
wip(opencode): authorize native startup prompt intent at execution owner
nwparker Oct 2, 2026
6115e13
fix(opencode): atomically replace status plugin entrypoints
AhmedNAgy25 Oct 2, 2026
10dda12
fix(opencode): retain plugin permissions across restrictive umasks
nwparker Oct 2, 2026
7cce878
test(opencode): resolve permission fixture from primary cwd
nwparker Oct 2, 2026
d31fd32
feat(opencode): install startup prompt plugin independently of status…
nwparker Oct 2, 2026
b9e17f7
Check relay environment values before merging
nwparker Oct 2, 2026
24ef5e8
fix(opencode): wait for admitted startup intent and preserve failed-l…
nwparker Oct 2, 2026
fffdb21
fix(opencode): confine overlay manifest cleanup to owned directories
nwparker Oct 2, 2026
7f2ba67
fix: wait for OpenCode worker composer before first dispatch
nwparker Oct 2, 2026
7218065
fix(orchestration): validate configured aliases with target shell gra…
nwparker Oct 2, 2026
02a3334
fix(orchestration): use actual shell and refuse assignment-only aliases
nwparker Oct 2, 2026
3839f09
fix(opencode): wait for admitted startup intent and preserve failed-l…
nwparker Oct 2, 2026
527f7f9
Support verified OpenCode v2 model selection in fresh native workers
nwparker Oct 2, 2026
19eafa6
fix(opencode): refuse unsupported startup preferences before model pr…
nwparker Oct 2, 2026
960bb84
feat(opencode): probe execution-host CLI capabilities
nwparker Oct 2, 2026
6b78423
fix(opencode): select plugin default for execution host loader
nwparker Oct 2, 2026
1b5f4c5
fix(opencode): limit prompt prefill capability to verified release
nwparker Oct 2, 2026
b8cc6fc
feat(opencode): probe launch capabilities on the execution host
nwparker Oct 2, 2026
8c3d344
fix(opencode): select plugin loader for the launched host binary
nwparker Oct 2, 2026
944c72c
fix(opencode): match WSL probe cwd and declared guest environment
nwparker Oct 2, 2026
42788e7
fix(opencode): preserve launch environment deletion boundaries
nwparker Oct 2, 2026
087aa5d
wip(opencode): authorize native startup prompt intent at execution owner
nwparker Oct 2, 2026
d2a65a1
fix(opencode): atomically replace status plugin entrypoints
AhmedNAgy25 Oct 2, 2026
8e16df5
fix(opencode): retain plugin permissions across restrictive umasks
nwparker Oct 2, 2026
d9dc629
test(opencode): resolve permission fixture from primary cwd
nwparker Oct 2, 2026
a59e6a7
feat(opencode): install startup prompt plugin independently of status…
nwparker Oct 2, 2026
10a42d7
fix(opencode): wait for admitted startup intent and preserve failed-l…
nwparker Oct 2, 2026
6012c37
fix(opencode): unsubscribe hook settings during async host shutdown
nwparker Oct 2, 2026
ab82bce
fix(opencode): confine overlay manifest cleanup to owned directories
nwparker Oct 2, 2026
6ade642
test: preserve typed calls in configured worker target checks
nwparker Oct 2, 2026
d714b16
test: check typed model host calls and terminal delay state
nwparker Oct 2, 2026
cfae79f
test(readiness): census recorded OpenCode composer boots
nwparker Oct 3, 2026
5668877
fix(opencode): reject redirected overlay parents before cleanup
nwparker Oct 3, 2026
3fc2e91
fix(orcad): retain runtime cleanup when subscribing to hook settings
nwparker Oct 3, 2026
18b4892
refactor(launch): extract OpenCode config and attachment authority
nwparker Oct 3, 2026
1679ae3
fix(opencode): retain host version selection across relay restarts
nwparker Oct 3, 2026
23fc08b
fix(opencode): pass run prompts as positional messages
nwparker Oct 3, 2026
a44345c
fix(opencode): keep wrapped run tasks positional
nwparker Oct 3, 2026
a031e1a
Prepare complete private OpenCode launch validation source
nwparker Oct 3, 2026
a2f9402
Prepare private complete 111-path launch validation on current main
nwparker Oct 3, 2026
350e6b5
Prepare private complete 111-path launch validation on current main
nwparker Oct 3, 2026
deee1bc
Prepare immutable strict worker readiness restack
nwparker Oct 3, 2026
5c690ca
Prepare immutable execution-host OpenCode capabilities restack
nwparker Oct 3, 2026
fef9ce3
Prepare immutable native startup intent restack
nwparker Oct 3, 2026
305d5e1
Prepare immutable owned overlay cleanup restack
nwparker Oct 3, 2026
67e9e70
Prepare immutable positional OpenCode run follow-up
nwparker Oct 3, 2026
dad41e5
Prepare immutable strict worker readiness restack
nwparker Oct 3, 2026
08531ab
Prepare immutable execution-host OpenCode capabilities restack
nwparker Oct 3, 2026
5a74e2c
Prepare immutable native startup intent restack
nwparker Oct 3, 2026
88d3329
Prepare immutable owned overlay cleanup restack
nwparker Oct 3, 2026
849e57a
Prepare immutable positional OpenCode run follow-up
nwparker Oct 3, 2026
9b5fe80
Recognize env options before positional OpenCode run messages
nwparker Oct 3, 2026
40d9038
STRICT launch CI contract correction
nwparker Oct 3, 2026
37f0be5
CAPS launch CI contract correction
nwparker Oct 3, 2026
a6d32ed
INTENT launch CI contract correction
nwparker Oct 3, 2026
7c2ca76
Preserve frozen main and reviewed strict OpenCode layer
nwparker Oct 3, 2026
1b9254e
Preserve frozen main and reviewed caps OpenCode layer
nwparker Oct 3, 2026
f30719e
Preserve frozen main and reviewed intent OpenCode layer
nwparker Oct 3, 2026
d43901e
Preserve frozen main and reviewed overlay OpenCode layer
nwparker Oct 3, 2026
fcaa9e3
Preserve frozen main and reviewed run OpenCode layer
nwparker Oct 3, 2026
2d6636a
fix(opencode): compose reviewed strict launch boundary with current main
nwparker Oct 3, 2026
a158087
fix(opencode): compose reviewed caps launch boundary with current main
nwparker Oct 3, 2026
3824da7
fix(opencode): compose reviewed intent launch boundary with current main
nwparker Oct 3, 2026
234c81e
fix(opencode): compose reviewed overlay launch boundary with current …
nwparker Oct 3, 2026
b47af8d
fix(opencode): compose reviewed run launch boundary with current main
nwparker Oct 3, 2026
a62e9ba
fix(opencode): preserve managed account environment in strict launch …
nwparker Oct 3, 2026
0e46eb4
fix(opencode): preserve managed account environment in caps launch bo…
nwparker Oct 3, 2026
e8fbfc1
fix(opencode): preserve managed account environment in intent launch …
nwparker Oct 3, 2026
b1865aa
fix(opencode): preserve managed account environment in overlay launch…
nwparker Oct 3, 2026
c6acbb6
fix(opencode): preserve managed account environment in run launch bou…
nwparker Oct 3, 2026
4ecbf29
fix(opencode): preserve current Caller main in strict launch boundary
nwparker Oct 3, 2026
c401e91
fix(opencode): preserve current Caller main in caps launch boundary
nwparker Oct 3, 2026
cc9af03
fix(opencode): preserve current Caller main in intent launch boundary
nwparker Oct 3, 2026
a6879b0
fix(opencode): preserve current Caller main in overlay launch boundary
nwparker Oct 3, 2026
df6484a
fix(opencode): preserve current Caller main in run launch boundary
nwparker Oct 3, 2026
59bf5ea
Allow verified OpenCode worker model selection
nwparker Oct 3, 2026
0f05f47
Apply OpenCode models with per-launch configuration
nwparker Oct 3, 2026
ed03a22
test(opencode): wait for malformed claim retries before expiring intent
nwparker Oct 3, 2026
ec6686e
Fix OpenCode caps review boundaries on preserved published source
nwparker Oct 3, 2026
5b429f7
Fix OpenCode intent review boundaries on preserved published source
nwparker Oct 3, 2026
732b301
Fix OpenCode overlay review boundaries on preserved published source
nwparker Oct 3, 2026
fa408f5
Fix OpenCode run review boundaries on preserved published source
nwparker Oct 3, 2026
1dfbd55
Preserve merged STRICT and current main in OpenCode caps correction
nwparker Oct 3, 2026
e5b1548
Preserve merged STRICT and current main in OpenCode intent correction
nwparker Oct 3, 2026
6a67a78
Preserve merged STRICT and current main in OpenCode overlay correction
nwparker Oct 3, 2026
e6b156b
Preserve merged STRICT and current main in OpenCode run correction
nwparker Oct 3, 2026
dcec863
Preserve current main with qualified OpenCode caps source
nwparker Oct 3, 2026
aece2eb
Preserve current main with qualified OpenCode intent source
nwparker Oct 3, 2026
832e740
Preserve current main with qualified OpenCode overlay source
nwparker Oct 3, 2026
b78c1aa
Preserve current main with qualified OpenCode run source
nwparker Oct 3, 2026
be71242
Integrate complete OpenCode catalog and execution-host preflight
nwparker Oct 3, 2026
436f1fa
Integrate complete per-launch OpenCode model selection
nwparker Oct 3, 2026
b94c542
Fix OpenCode caps standalone build boundary
nwparker Oct 3, 2026
3cdf1f4
Fix OpenCode intent standalone build boundary
nwparker Oct 3, 2026
91d00c1
Fix OpenCode overlay standalone build boundary
nwparker Oct 3, 2026
5b895e8
Fix OpenCode run standalone build boundary
nwparker Oct 3, 2026
f1f6feb
Preserve current main and qualified OpenCode caps repairs
nwparker Oct 3, 2026
56b1bc7
Preserve current main and qualified OpenCode intent repairs
nwparker Oct 3, 2026
e10caa9
Preserve current main and qualified OpenCode overlay repairs
nwparker Oct 3, 2026
0e56b44
Preserve current main and qualified OpenCode run repairs
nwparker Oct 3, 2026
6a83da3
test: initialize Claude prompt state in output retention fixture
nwparker Oct 3, 2026
e6440b1
Verify OpenCode model choices on supported launch surfaces
nwparker Oct 3, 2026
0493776
Verify OpenCode catalog model launches and preserve current launch be…
nwparker Oct 3, 2026
107d359
Preserve OpenCode caps integration with the reviewed main test fixture
nwparker Oct 3, 2026
3352397
Preserve OpenCode intent integration with the reviewed main test fixture
nwparker Oct 3, 2026
b43f0c9
Preserve OpenCode overlay integration with the reviewed main test fix…
nwparker Oct 3, 2026
8f3b607
Preserve OpenCode run integration with the reviewed main test fixture
nwparker Oct 3, 2026
7390663
Verify OpenCode catalog model launches and preserve current launch be…
nwparker Oct 3, 2026
3366421
Verify OpenCode catalog model launches and preserve current launch be…
nwparker Oct 3, 2026
0b2e696
Verify OpenCode model model launches and preserve current launch beha…
nwparker Oct 3, 2026
d94a3f2
Preserve OpenCode caps integration and current main relay inventory c…
nwparker Oct 3, 2026
d9285ae
Preserve OpenCode intent integration and current main relay inventory…
nwparker Oct 3, 2026
200eb78
Preserve OpenCode overlay integration and current main relay inventor…
nwparker Oct 3, 2026
d6f8077
Preserve OpenCode run integration and current main relay inventory cache
nwparker Oct 3, 2026
e082971
Preserve current agent option removal while verifying OpenCode catalo…
nwparker Oct 3, 2026
794c1d6
Preserve current agent option removal while verifying OpenCode model …
nwparker Oct 3, 2026
a77cea7
Preserve qualified OpenCode caps with current merged account selection
nwparker Oct 4, 2026
b43c77d
Preserve qualified OpenCode intent with current merged account selection
nwparker Oct 4, 2026
f45ae0b
Preserve qualified OpenCode overlay with current merged account selec…
nwparker Oct 4, 2026
7a24ede
Preserve qualified OpenCode run with current merged account selection
nwparker Oct 4, 2026
25daf75
Preserve current agent option removal while verifying OpenCode catalo…
nwparker Oct 4, 2026
f890c6d
Preserve current agent option removal while verifying OpenCode model …
nwparker Oct 4, 2026
94aa46e
Refuse unverified new-worktree OpenCode models and complete host audits
nwparker Oct 4, 2026
85efe85
Wait for OpenCode location hydration in intent startup
Oct 4, 2026
d12f6b5
Wait for OpenCode location hydration in overlay startup
Oct 4, 2026
9a90462
Wait for OpenCode location hydration in run startup
Oct 4, 2026
ae43d87
Refuse unverified new-worktree OpenCode model launches and record sta…
nwparker Oct 4, 2026
d08fcd7
Preserve native model hydration while verifying OpenCode catalog laun…
nwparker Oct 4, 2026
7c765fd
Preserve native model hydration while verifying OpenCode model launch…
nwparker Oct 4, 2026
931309b
fix(opencode): bind startup readiness to the composer location
nwparker Oct 4, 2026
1dc953c
Bind OpenCode startup readiness to the current location in intent sta…
Oct 4, 2026
0dd7371
Bind OpenCode startup readiness to the current location in overlay st…
Oct 4, 2026
c112f45
Bind OpenCode startup readiness to the current location in run startup
Oct 4, 2026
64c4353
Restore the owning Orca CLI path after shell profiles
codex Oct 4, 2026
2746e62
Use a literal marker for the Bash lookup regression
codex Oct 4, 2026
02a7d39
Preserve current main while retaining qualified native OpenCode start…
nwparker Oct 4, 2026
b759d1e
Retain execution-host Orca CLI selection through shell startup
nwparker Oct 4, 2026
bbfde0c
Retain verified OpenCode catalog selection with native startup and ho…
nwparker Oct 4, 2026
c8fdfdb
Retain verified OpenCode model selection with native startup and host…
nwparker Oct 4, 2026
5a2479b
Preserve plain panes and initialize zsh after prompt hook replacement
nwparker Oct 4, 2026
c6bc30f
Preserve user line-editor dispatchers during deferred startup
nwparker Oct 4, 2026
e213fe1
Merge current main into the OpenCode caps layer
nwparker Oct 4, 2026
89aa00a
Merge current main into the OpenCode intent layer
nwparker Oct 4, 2026
15f3e7a
Merge current main into the OpenCode overlay layer
nwparker Oct 4, 2026
37a22fd
Merge current main into the OpenCode run layer
nwparker Oct 4, 2026
a911328
Merge current main into reviewed Orca CLI shell startup fix
Oct 4, 2026
a8a8361
Merge current main into reviewed Orca CLI shell startup fix
Oct 4, 2026
2b5eca0
Preserve current main native startup and execution-host CLI fixes
nwparker Oct 4, 2026
99b7194
Preserve verified OpenCode catalog selection on current main
nwparker Oct 4, 2026
389fa85
Preserve verified OpenCode model selection on current main
nwparker Oct 4, 2026
f4bd404
fix: retain CLI startup when global Zsh replaces prompt hooks
nwparker Oct 4, 2026
db9634c
test: replay global Zsh hook replacement after host startup
nwparker Oct 4, 2026
5098be9
test: isolate controlled Zsh widgets from distro keyboard setup
nwparker Oct 4, 2026
1f27c13
Integrate merged host capability prerequisite into native prompt intent
nwparker Oct 4, 2026
0716bd6
fix(shell): preserve user hooks during deferred zsh initialization
nwparker Oct 4, 2026
81354ff
Retry interrupted OpenCode startup prompt claims
Oct 4, 2026
cdd4e0c
Preserve qualified OpenCode source on named main: native125-authority
nwparker Oct 4, 2026
35e8c43
Preserve qualified OpenCode source on named main: native125-mainffd23
nwparker Oct 4, 2026
074e743
Preserve qualified OpenCode source on named main: native125-cli30-base
nwparker Oct 4, 2026
6a1cecd
Preserve qualified OpenCode source on named main: catalog
nwparker Oct 4, 2026
404910b
Preserve qualified OpenCode source on named main: model
nwparker Oct 4, 2026
811a359
Keep completed Zsh startup hooks retired when the wrapper is sourced …
Oct 4, 2026
156bcb4
Preserve qualified session launch changes with repeated-source-safe s…
Oct 4, 2026
a1a47a3
Preserve qualified session launch changes with repeated-source-safe s…
Oct 4, 2026
a6f8bb9
Merge current main into the OpenCode run layer
nwparker Oct 4, 2026
35e8927
Merge qualified RUN and current Main into complete OpenCode catalog
nwparker Oct 4, 2026
55f307b
Merge current catalog into complete verified OpenCode model selection
nwparker Oct 4, 2026
d0fa101
Preserve catalog OpenCode scope on merged RUN current Main
Oct 4, 2026
86b61d0
Preserve model OpenCode scope on merged RUN current Main
Oct 4, 2026
8f95032
Preserve validated OpenCode worker model catalog on current main
Oct 4, 2026
255cc87
Preserve isolated OpenCode worker selection after current catalog int…
Oct 4, 2026
7deb759
Reject truncated OpenCode catalogs and explain worktree model limits
Oct 4, 2026
8b2d41e
Use a template literal in truncated-catalog coverage
Oct 4, 2026
a6819b3
Preserve verified OpenCode selection with catalog review corrections
Oct 4, 2026
a3f7e81
Refuse unfinished OpenCode model catalogs
nwparker Oct 4, 2026
9c6e50a
Preserve model selection with complete catalog validation
nwparker Oct 4, 2026
3f4af35
Merge current main after OpenCode worker model gate
nwparker Oct 4, 2026
f153666
Merge current main while preserving qualified OpenCode model selection
codex Oct 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/global-fetch-call-site-audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const AUDITED_GLOBAL_FETCH_LINES = new Map<string, number>([
['main/gitea/client.ts', 1],
// Generated OpenCode claim source consumes JSON or cancels its body in finally.
['main/opencode/opencode-startup-prompt-source.ts', 1],
// Authenticated loopback preflight consumes bounded JSON and cancels every body in finally.
['main/opencode/opencode-launch-model-context.ts', 1],
['main/orca-profiles/profile-cloud-client.ts', 1],
['main/orca-profiles/profile-cloud-org-members-client.ts', 1],
['main/rate-limits/codex-fetcher.ts', 3],
Expand Down
76 changes: 76 additions & 0 deletions src/main/opencode/opencode-launch-model-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, expect, it } from 'vitest'
import { resolveOpenCodeLaunchModelConfig } from './opencode-launch-model-config'

describe('per-launch OpenCode model config', () => {
it('retains credentials, permissions and other agents while overriding the resolved primary model', () => {
const original = {
model: 'provider/old',
default_agent: 'reviewer',
providers: {
custom: { apiKey: 'placeholder-key', models: { preferred: { name: 'Preferred' } } }
},
permissions: [{ tool: 'shell', action: 'ask' }],
agents: {
reviewer: { model: 'provider/agent-old', system: 'Review only', permissions: [] },
build: { model: 'provider/build', steps: 5 },
nested: { mode: 'subagent', model: 'provider/nested' }
}
}
const configContent = JSON.stringify(original)
const merged = resolveOpenCodeLaunchModelConfig({
configContent,
primaryAgent: 'reviewer',
model: 'provider/preferred'
})
expect(merged).not.toBeNull()
expect(JSON.parse(merged ?? 'null')).toEqual({
...original,
model: 'provider/preferred',
agents: {
...original.agents,
reviewer: { ...original.agents.reviewer, model: 'provider/preferred' }
}
})
expect(configContent).toBe(JSON.stringify(original))
})

it('adds an override for the externally resolved built-in primary agent without guessing its name', () => {
expect(
resolveOpenCodeLaunchModelConfig({
configContent: undefined,
primaryAgent: 'plan',
model: 'provider/preferred'
})
).toBe('{"model":"provider/preferred","agents":{"plan":{"model":"provider/preferred"}}}')
})

it.each(['{', 'null', '[]', '{"agents":[]}', '{"agents":{"build":null}}'])(
'refuses malformed inline config %s',
(configContent) => {
expect(
resolveOpenCodeLaunchModelConfig({
configContent,
primaryAgent: 'build',
model: 'provider/a'
})
).toBeNull()
}
)

it('refuses oversized config and absent resolved preferences', () => {
expect(
resolveOpenCodeLaunchModelConfig({
configContent: ' '.repeat(1_048_577),
primaryAgent: 'build',
model: 'provider/a'
})
).toBeNull()
expect(
resolveOpenCodeLaunchModelConfig({
configContent: '{}',
primaryAgent: '',
model: 'provider/a'
})
).toBeNull()
})
})
33 changes: 33 additions & 0 deletions src/main/opencode/opencode-launch-model-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { z } from 'zod'

const launchConfig = z
.object({ agents: z.record(z.string(), z.record(z.string(), z.unknown())).optional() })
.passthrough()

export function resolveOpenCodeLaunchModelConfig(options: {
configContent: string | undefined
primaryAgent: string
model: string
}): string | null {
if (!options.primaryAgent || !options.model || (options.configContent?.length ?? 0) > 1_048_576) {
return null
}
try {
const raw: unknown = options.configContent?.trim() ? JSON.parse(options.configContent) : {}
const parsed = launchConfig.safeParse(raw)
if (!parsed.success) {
return null
}
const config = parsed.data
return JSON.stringify({
...config,
model: options.model,
agents: {
...config.agents,
[options.primaryAgent]: { ...config.agents?.[options.primaryAgent], model: options.model }
}
})
} catch {
return null
}
}
137 changes: 137 additions & 0 deletions src/main/opencode/opencode-launch-model-context-cleanup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { z } from 'zod'
import { ChildProcess } from 'node:child_process'
import { PassThrough } from 'node:stream'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { spawnProcess } from '../../shared/child-process/run-process'
import {
forceTerminateProcessTree,
signalProcessTree
} from '../../shared/child-process/process-tree-termination'
import { probeOpenCodeLaunchModelContext } from './opencode-launch-model-context'
import { readFetchResponseJsonWithinLimit } from '../../shared/fetch-response-body'

vi.mock('../../shared/child-process/run-process', () => ({ spawnProcess: vi.fn() }))
vi.mock('../../shared/child-process/process-tree-termination', () => ({
signalProcessTree: vi.fn(),
forceTerminateProcessTree: vi.fn()
}))

vi.mock('../../shared/fetch-response-body', () => ({ readFetchResponseJsonWithinLimit: vi.fn() }))

class ProbeChild extends ChildProcess {
override stdin = new PassThrough()
override stdout = new PassThrough()
override stderr = new PassThrough()
override pid = 123456
override exitCode: number | null = null
override stdio: [PassThrough, PassThrough, PassThrough, undefined, undefined] = [
this.stdin,
this.stdout,
this.stderr,
undefined,
undefined
]
}

const directory = '/private/project'
const model = { id: 'model-b', providerID: 'private-proof', enabled: true }
const snapshot = {
model: { location: { directory }, data: [model] },
agent: {
location: { directory },
data: [{ id: 'build', mode: 'primary', hidden: false, model }]
},
'model/default': { location: { directory }, data: model },
config: [{ type: 'document', info: { default_agent: 'build' } }]
}
const platform = Object.getOwnPropertyDescriptor(process, 'platform')
let child: ProbeChild
let closeDuringFetch = false
function close() {
child.exitCode = 0
child.emit('exit', 0, null)
child.emit('close', 0, null)
}
const options = { executable: '/private/opencode', cwd: directory, env: {} }

describe('OpenCode model probe termination evidence', () => {
beforeEach(() => {
vi.resetAllMocks()
vi.mocked(readFetchResponseJsonWithinLimit).mockImplementation((response) => response.json())
child = new ProbeChild()
closeDuringFetch = false
vi.mocked(spawnProcess).mockImplementation(() => {
queueMicrotask(() => child.stdout.write('server listening on http://127.0.0.1:45678\n'))
return child
})
vi.stubGlobal(
'fetch',
vi.fn(async (input: URL) => {
const endpoint = z
.enum(['model', 'agent', 'model/default', 'config'])
.parse(input.pathname.slice('/api/'.length))
const response = snapshot[endpoint]
if (closeDuringFetch && endpoint === 'config') {
queueMicrotask(close)
}
return new Response(JSON.stringify(response), { status: 200 })
})
)
vi.mocked(signalProcessTree).mockImplementation(async () => {
close()
return true
})
vi.mocked(forceTerminateProcessTree).mockResolvedValue(false)
})

afterEach(() => {
if (platform) {
Object.defineProperty(process, 'platform', platform)
}
vi.unstubAllGlobals()
})

it('cancels all unread responses if the bounded body reader fails', async () => {
const cancel = vi.fn()
vi.mocked(readFetchResponseJsonWithinLimit).mockRejectedValue(new Error('body_reader_failed'))
vi.stubGlobal(
'fetch',
vi.fn(async () => new Response(new ReadableStream({ cancel }), { status: 200 }))
)
expect(await probeOpenCodeLaunchModelContext(options)).toBeNull()
expect(cancel).toHaveBeenCalledTimes(4)
expect(signalProcessTree).toHaveBeenCalledOnce()
})

it('accepts an already-closed Windows probe without signaling its former pid', async () => {
Object.defineProperty(process, 'platform', { value: 'win32' })
closeDuringFetch = true
vi.mocked(signalProcessTree).mockResolvedValue(false)
expect(await probeOpenCodeLaunchModelContext(options)).toMatchObject({ primaryAgent: 'build' })
expect(signalProcessTree).not.toHaveBeenCalled()
expect(forceTerminateProcessTree).not.toHaveBeenCalled()
})

it('does not force a POSIX group after the graceful close', async () => {
Object.defineProperty(process, 'platform', { value: 'darwin' })
expect(await probeOpenCodeLaunchModelContext(options)).toMatchObject({ primaryAgent: 'build' })
expect(signalProcessTree).toHaveBeenCalledOnce()
expect(forceTerminateProcessTree).not.toHaveBeenCalled()
})

it('keeps a root exit with inherited pipes unverified and avoids its former pid', async () => {
vi.mocked(signalProcessTree).mockImplementation(async () => {
child.exitCode = 0
child.emit('exit', 0, null)
return false
})
expect(await probeOpenCodeLaunchModelContext(options)).toBeNull()
expect(forceTerminateProcessTree).not.toHaveBeenCalled()
})

it('requires verified termination when a still-live probe does not close', async () => {
vi.mocked(signalProcessTree).mockResolvedValue(true)
expect(await probeOpenCodeLaunchModelContext(options)).toBeNull()
expect(forceTerminateProcessTree).toHaveBeenCalledOnce()
})
})
Loading
Loading