fix: prevent managed runtime stalls during file changes
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Automatic native Git snapshot diffs could block concurrent OpenCode sessions after broad rewrites or line-ending conversions. Managed desktop and Agent profiles now skip snapshots that GoodBuddy does not use, while preserving file tools, native subagents, workspace Git operations, and Ask/Execute rules. Drain otherwise-unused Continue utility-host stdout to prevent console output from filling the pipe and blocking requests. Correct the lifecycle regression's body observer to watch the underlying reader rather than a buffered wrapper. Prepare Desktop 0.12.10 and Agent 0.11.22 with aligned bilingual notes and feature documentation. Existing runtimes require restart or Agent update. Full desktop UI before/after and Linux Agent production-path checks passed; 3626 tests passed, with 61 existing platform/manual skips. Release note: 修复批量文件改写或换行转换后的托管 OpenCode 并行停顿,以及 Continue 控制台输出阻塞;本机升级重启后生效,远程项目请配套更新 Agent。
This commit is contained in:
@@ -48,6 +48,7 @@ export const MODEL_BRIDGE_SDK_AUTH_SENTINEL =
|
||||
|
||||
export type OpenCodeModelBridgeProviderConfig = {
|
||||
model: string
|
||||
snapshot: false
|
||||
permission: 'ask' | 'allow'
|
||||
agent: {
|
||||
title: {
|
||||
@@ -521,6 +522,7 @@ export function createOpenCodeModelBridgeProviderConfig(input: {
|
||||
const baseURL = `${origin}/v1`
|
||||
return {
|
||||
model: openCodeModelBridgeModelId(input.protocol, model),
|
||||
snapshot: false,
|
||||
permission: input.workMode === 'ask' ? 'ask' : 'allow',
|
||||
// GoodBuddy owns conversation titles. A first-prompt title request can
|
||||
// race the real ACP prompt, abandon its Provider response, and correctly
|
||||
|
||||
@@ -134,6 +134,7 @@ describe('model bridge loopback helper', () => {
|
||||
expect(options.env.NODE_OPTIONS).toBeUndefined()
|
||||
const config = JSON.parse(options.env.OPENCODE_CONFIG_CONTENT!)
|
||||
expect(config.model).toBe('goodbuddy-anthropic/private-model')
|
||||
expect(config.snapshot).toBe(false)
|
||||
expect(config.permission).toBe(workMode === 'ask' ? 'ask' : 'allow')
|
||||
expect(config.plugin).toHaveLength(1)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,19 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('OpenCode direct launch profile', () => {
|
||||
it.each(['ask', 'execute'] as const)(
|
||||
'disables unused automatic Git snapshots for %s',
|
||||
(workMode) => {
|
||||
const profile = createOpenCodeLaunchProfile({
|
||||
...createFixture(),
|
||||
workMode
|
||||
})
|
||||
const config = JSON.parse(profile.env.OPENCODE_CONFIG_CONTENT!)
|
||||
expect(config.snapshot).toBe(false)
|
||||
expect(config.permission).toBe(workMode === 'ask' ? 'ask' : 'allow')
|
||||
}
|
||||
)
|
||||
|
||||
it('runs Ask directly in the Workspace', () => {
|
||||
const fixture = createFixture()
|
||||
const profile = createOpenCodeLaunchProfile({
|
||||
|
||||
@@ -146,6 +146,8 @@ export function createOpenCodeLaunchProfile(input: {
|
||||
const environment = {
|
||||
...environmentBase,
|
||||
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
||||
// Native snapshot summaries are not consumed by GoodBuddy.
|
||||
snapshot: false,
|
||||
permission,
|
||||
agent: { build: { permission } },
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { PassThrough } from 'node:stream'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
createContinueUtilityProcessChild,
|
||||
@@ -6,11 +7,12 @@ import {
|
||||
} from './continue-utility-process-adapter'
|
||||
import { waitForProcessExit } from './child-process-termination'
|
||||
|
||||
function createSource(): ContinueUtilityProcessSource & EventEmitter {
|
||||
function createSource(stdout?: PassThrough): ContinueUtilityProcessSource & EventEmitter {
|
||||
const emitter =
|
||||
new EventEmitter() as ContinueUtilityProcessSource & EventEmitter
|
||||
Object.defineProperties(emitter, {
|
||||
pid: { value: 42 },
|
||||
stdout: { value: stdout },
|
||||
stderr: { value: undefined }
|
||||
})
|
||||
emitter.kill = vi.fn(() => true)
|
||||
@@ -33,6 +35,17 @@ function createSource(): ContinueUtilityProcessSource & EventEmitter {
|
||||
}
|
||||
|
||||
describe('Continue utility process adapter', () => {
|
||||
it('drains unused stdout so native output cannot block the host', async () => {
|
||||
const stdout = new PassThrough({ highWaterMark: 1024 })
|
||||
createContinueUtilityProcessChild(createSource(stdout))
|
||||
|
||||
expect(stdout.readableFlowing).toBe(true)
|
||||
await new Promise<void>((resolve) => {
|
||||
stdout.end(Buffer.alloc(128 * 1024), resolve)
|
||||
})
|
||||
expect(stdout.readableLength).toBe(0)
|
||||
})
|
||||
|
||||
it('maps Electron exit to close and completes helper waits immediately', async () => {
|
||||
const source = createSource()
|
||||
const child = createContinueUtilityProcessChild(source)
|
||||
|
||||
@@ -8,6 +8,7 @@ type UtilityErrorListener = (
|
||||
|
||||
export type ContinueUtilityProcessSource = {
|
||||
readonly pid?: number
|
||||
readonly stdout?: Pick<NodeJS.ReadableStream, 'resume'> | null
|
||||
readonly stderr?: ContinueHostChild['stderr']
|
||||
kill(): boolean
|
||||
onExit(listener: (code: number) => void): void
|
||||
@@ -20,6 +21,8 @@ export type ContinueUtilityProcessSource = {
|
||||
export function createContinueUtilityProcessChild(
|
||||
utility: ContinueUtilityProcessSource
|
||||
): ContinueHostChild {
|
||||
// Responses arrive over HTTP, but an unread console pipe can block the host.
|
||||
utility.stdout?.resume()
|
||||
let exitCode: number | null = null
|
||||
let killed = false
|
||||
const closeListeners = new Map<
|
||||
|
||||
@@ -71,6 +71,8 @@ it.skipIf(!existsSync(binaryPath))(
|
||||
if (!closed) openEventBodies--
|
||||
closed = true
|
||||
}
|
||||
// The wrapper may stay full after fetch aborts, with no further pull.
|
||||
void reader.closed.then(close, close)
|
||||
return new Response(new ReadableStream({
|
||||
async pull(controller) {
|
||||
try {
|
||||
|
||||
@@ -288,6 +288,41 @@ async function collectRun(
|
||||
}
|
||||
|
||||
describe("OpenCodeRuntime embedded launcher", () => {
|
||||
it.each(["native", "profile"] as const)(
|
||||
"disables unused automatic Git snapshots with %s model configuration",
|
||||
async (configuration) => {
|
||||
const { deps, spawnMock } = dependencies(fakeChild());
|
||||
const runtime = new OpenCodeRuntime(
|
||||
options({
|
||||
modelProfile: configuration === "profile"
|
||||
? {
|
||||
id: "00000000-0000-4000-8000-000000000011",
|
||||
name: "Test model",
|
||||
baseUrl: "https://model.example/v1",
|
||||
modelName: "test-model",
|
||||
protocol: "openai-responses",
|
||||
authentication: "none",
|
||||
supportsImageInput: false,
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
deps,
|
||||
);
|
||||
try {
|
||||
await expect(runtime.testConnection()).resolves.toMatchObject({
|
||||
available: true,
|
||||
});
|
||||
const config = JSON.parse(
|
||||
spawnMock.mock.calls[0]?.[2]?.env?.OPENCODE_CONFIG_CONTENT ?? "{}",
|
||||
);
|
||||
expect(config.snapshot).toBe(false);
|
||||
expect(config.permission["*"]).toBe("allow");
|
||||
} finally {
|
||||
await runtime.dispose();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("forwards retry scheduling and resumes only on a matching native busy event", async () => {
|
||||
const next = Date.now() + 120_000;
|
||||
const setup = runClient([
|
||||
@@ -640,6 +675,7 @@ describe("OpenCodeRuntime embedded launcher", () => {
|
||||
spawnOptions?.env?.OPENCODE_CONFIG_CONTENT ?? "{}",
|
||||
) as Record<string, unknown>;
|
||||
expect(config).toEqual({
|
||||
snapshot: false,
|
||||
skills: {
|
||||
paths: [join(configDirectory!, "skills")],
|
||||
urls: [],
|
||||
|
||||
@@ -1089,14 +1089,12 @@ export class OpenCodeRuntime implements AgentRuntime {
|
||||
env.XDG_DATA_HOME = join(registration.root, "xdg-data");
|
||||
env.XDG_STATE_HOME = join(registration.root, "xdg-state");
|
||||
const skillConfig = createOpenCodeSkillConfig(registration, skillIds);
|
||||
env.OPENCODE_CONFIG_CONTENT = JSON.stringify(
|
||||
profile
|
||||
? {
|
||||
...createOpenCodeProviderConfig(profile),
|
||||
...skillConfig,
|
||||
}
|
||||
: skillConfig,
|
||||
);
|
||||
// GoodBuddy does not use native Git snapshots or their synchronous diffs.
|
||||
env.OPENCODE_CONFIG_CONTENT = JSON.stringify({
|
||||
...(profile ? createOpenCodeProviderConfig(profile) : {}),
|
||||
...skillConfig,
|
||||
snapshot: false,
|
||||
});
|
||||
if (!profile && this.options.configPath.trim()) {
|
||||
env.OPENCODE_CONFIG = resolve(this.options.configPath);
|
||||
}
|
||||
|
||||
@@ -389,6 +389,7 @@ const launchContinueHost: ContinueHostLauncher = (
|
||||
get pid() {
|
||||
return utilityChild.pid
|
||||
},
|
||||
stdout: utilityChild.stdout,
|
||||
stderr: utilityChild.stderr,
|
||||
kill: () => utilityChild.kill(),
|
||||
onExit: (listener) => {
|
||||
|
||||
Reference in New Issue
Block a user