From a93675cbe41dfaf543c70cad4dd4bfb607ff9a12 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:07:44 +0530 Subject: [PATCH 1/8] refactor(agents): standardize on effort --- src/cli.test.ts | 6 ++-- src/cli.ts | 18 ++++++------ src/db/migrations.ts | 22 ++++++++++++-- src/db/schema.ts | 2 +- src/local-agent-acp.test.ts | 10 +++---- src/local-agent-acp.ts | 16 +++++------ src/local-agent-adapters.test.ts | 24 ++++++++-------- src/local-agent-adapters.ts | 4 +-- src/local-agent-claude.test.ts | 8 +++--- src/local-agent-claude.ts | 8 +++--- src/local-agent-codex.test.ts | 2 +- src/local-agent-codex.ts | 2 +- src/local-agent-daemon-lifecycle.ts | 2 +- src/local-agent-daemon-protocol.test.ts | 10 +++---- src/local-agent-daemon-protocol.ts | 6 ++-- src/local-agent-daemon.test.ts | 8 +++--- src/local-agent-manager.ts | 18 ++++++------ src/local-agent-opencode.test.ts | 6 ++-- src/local-agent-opencode.ts | 12 ++++---- src/local-agent-pi.test.ts | 8 +++--- src/local-agent-pi.ts | 8 +++--- src/local-agent-profiles.test.ts | 6 ++-- src/local-agent-profiles.ts | 8 +++--- src/local-agent-runtime.ts | 6 ++-- src/local-agent-store.test.ts | 14 +++++---- src/local-agent-store.ts | 18 ++++++------ src/local-agent-targets.test.ts | 28 +++++++++--------- src/local-agent-targets.ts | 38 ++++++++++++------------- src/server.ts | 8 +++--- src/ui/card-types.ts | 2 +- src/ui/workspace-app.tsx | 2 +- 31 files changed, 175 insertions(+), 155 deletions(-) diff --git a/src/cli.test.ts b/src/cli.test.ts index 534574b1c..5be303095 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -41,7 +41,7 @@ try { "description: Read-only reviewer.", "provider: codex", "model: gpt-5.4", - "thinking: high", + "effort: high", "---", "", "Review only.", @@ -56,7 +56,7 @@ try { profileName: "reviewer", provider: "codex", model: "gpt-5.4", - thinking: "high", + effort: "high", }).id, { status: "idle" }, ); @@ -137,7 +137,7 @@ try { }, }); - assert.match(output, new RegExp(`${current.id} idle reviewer codex gpt-5\\.4 thinking=high`)); + assert.match(output, new RegExp(`${current.id} idle reviewer codex gpt-5\\.4 effort=high`)); assert.doesNotMatch(output, /profile reviewer/); assert.doesNotMatch(output, new RegExp(other.id)); diff --git a/src/cli.ts b/src/cli.ts index 900680d1c..8e023d433 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -299,8 +299,8 @@ function printHelp(): void { " devspace config get Print persisted config", " devspace config set publicBaseUrl ", " devspace agents ls List subagent sessions", - " devspace agents run [--model ] [--thinking ] ", - " devspace agents continue [--model ] [--thinking ] ", + " devspace agents run [--model ] [--effort ] ", + " devspace agents continue [--model ] [--effort ] ", " devspace agents show ", " devspace agents daemon ", " devspace -v, --version Print the installed version", @@ -376,7 +376,7 @@ async function runAgentsRun(args: string[], json: boolean): Promise { workspaceRoot: scope.workspaceRoot, workspaceId: scope.workspaceId, model: parsed.model, - thinking: parsed.thinking, + effort: parsed.effort, }); const record = presentAgentResult(result, json); if (!record) return; @@ -394,7 +394,7 @@ async function runAgentsContinue(args: string[], json: boolean): Promise { const scope = resolveCurrentWorkspaceScope(); const result = await client.continue(parsed.agentId, parsed.prompt, { model: parsed.model, - thinking: parsed.thinking, + effort: parsed.effort, }, scope); const record = presentAgentResult(result, json); if (!record) return; @@ -488,11 +488,11 @@ function resolveCurrentWorkspaceScope(): { workspaceId: string; workspaceRoot: s function formatAgentLine(agent: Pick< LocalAgentRecord, - "id" | "status" | "profileName" | "provider" | "model" | "thinking" + "id" | "status" | "profileName" | "provider" | "model" | "effort" >): string { const model = agent.model ? ` ${agent.model}` : ""; - const thinking = agent.thinking ? ` thinking=${agent.thinking}` : ""; - return `${agent.id} ${agent.status} ${agent.profileName} ${agent.provider}${model}${thinking}`; + const effort = agent.effort ? ` effort=${agent.effort}` : ""; + return `${agent.id} ${agent.status} ${agent.profileName} ${agent.provider}${model}${effort}`; } function presentAgentResult( @@ -519,8 +519,8 @@ function printAgentsHelp(): void { "", "Usage:", " devspace agents ls [--json]", - " devspace agents run [--model ] [--thinking ] [--json] ", - " devspace agents continue [--model ] [--thinking ] [--json] ", + " devspace agents run [--model ] [--effort ] [--json] ", + " devspace agents continue [--model ] [--effort ] [--json] ", " devspace agents show [--json]", " devspace agents daemon [--json]", ].join("\n"), diff --git a/src/db/migrations.ts b/src/db/migrations.ts index ce2722562..3f547ffee 100644 --- a/src/db/migrations.ts +++ b/src/db/migrations.ts @@ -32,6 +32,11 @@ const migrations: Migration[] = [ name: "local-agent-structured-errors", up: migrateLocalAgentStructuredErrors, }, + { + version: 6, + name: "local-agent-effort-rename", + up: migrateLocalAgentEffortRename, + }, ]; export function migrateDatabase(sqlite: Database.Database): void { @@ -162,7 +167,7 @@ function migrateLocalAgentSessions(sqlite: Database.Database): void { profile_name text not null, provider text not null, model text, - thinking text, + effort text, provider_session_id text, status text not null, latest_response text, @@ -181,7 +186,7 @@ function migrateLocalAgentSessions(sqlite: Database.Database): void { on local_agent_sessions(provider_session_id); `); - addColumnIfMissing(sqlite, "local_agent_sessions", "thinking", "text"); + addColumnIfMissing(sqlite, "local_agent_sessions", "effort", "text"); } function migrateWorkspaceConversationBindings(sqlite: Database.Database): void { @@ -208,6 +213,19 @@ function migrateLocalAgentStructuredErrors(sqlite: Database.Database): void { addColumnIfMissing(sqlite, "local_agent_sessions", "error_retryable", "text"); } +function migrateLocalAgentEffortRename(sqlite: Database.Database): void { + const columns = sqlite.prepare("pragma table_info(local_agent_sessions)").all() as Array<{ + name: string; + }>; + const names = new Set(columns.map((column) => column.name)); + if (names.has("effort")) return; + if (!names.has("thinking")) { + addColumnIfMissing(sqlite, "local_agent_sessions", "effort", "text"); + return; + } + sqlite.exec("alter table local_agent_sessions rename column thinking to effort"); +} + function addColumnIfMissing( sqlite: Database.Database, table: "workspace_sessions" | "local_agent_sessions", diff --git a/src/db/schema.ts b/src/db/schema.ts index 495f7169c..c16da8925 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -99,7 +99,7 @@ export const localAgentSessions = sqliteTable( profileName: text("profile_name").notNull(), provider: text("provider").notNull(), model: text("model"), - thinking: text("thinking"), + effort: text("effort"), providerSessionId: text("provider_session_id"), status: text("status").notNull(), latestResponse: text("latest_response"), diff --git a/src/local-agent-acp.test.ts b/src/local-agent-acp.test.ts index bc6f335e5..6b5fd9e84 100644 --- a/src/local-agent-acp.test.ts +++ b/src/local-agent-acp.test.ts @@ -25,7 +25,7 @@ const connection = { sessionId, configOptions: [ { type: "select", category: "model", id: "model", options: [{ value: "model-a" }] }, - { type: "select", category: "thought_level", id: "thinking", options: [{ value: "high" }] }, + { type: "select", category: "thought_level", id: "effort", options: [{ value: "high" }] }, ], }; } @@ -65,7 +65,7 @@ const firstResult = await runtime.run({ prompt: "first", workspaceRoot: "/tmp/project", model: "model-a", - thinking: "high", + effort: "high", writeMode: "read_only", }, { onSessionId: (sessionId) => { sessionIds.push(sessionId); }, @@ -78,7 +78,7 @@ const warmResult = await runtime.run({ workspaceRoot: "/tmp/project", providerSessionId: first.providerSessionId ?? undefined, model: "model-a", - thinking: "high", + effort: "high", writeMode: "full_access", }, { onSessionId: (sessionId) => { sessionIds.push(sessionId); }, @@ -116,7 +116,7 @@ const resumedPersistedResult = await resumedRuntime.run({ workspaceRoot: "/tmp/project", providerSessionId: first.providerSessionId ?? undefined, model: "model-a", - thinking: "high", + effort: "high", }); assert.equal(resumedPersistedResult.isOk(), true); if (resumedPersistedResult.isErr()) throw resumedPersistedResult.error; @@ -125,7 +125,7 @@ assert.equal(resumedPersisted.finalResponse, "ACP response"); assert.equal( requests.filter(({ method }) => method === "session/set_config_option").length, 4, - "cold resume must not require config metadata just to preserve prior model/thinking state", + "cold resume must not require config metadata just to preserve prior model/effort state", ); const resumeFailure = await resumedRuntime.run({ prompt: "resumed", diff --git a/src/local-agent-acp.ts b/src/local-agent-acp.ts index 1ac3ecb2a..9bb675367 100644 --- a/src/local-agent-acp.ts +++ b/src/local-agent-acp.ts @@ -265,7 +265,7 @@ export class AcpRuntime implements LocalAgentRuntime { if (!canConfigure) { const requested = [ input.model && input.modelOverrideRequested ? "model" : undefined, - input.thinking && input.thinkingOverrideRequested ? "thinking" : undefined, + input.effort && input.effortOverrideRequested ? "effort" : undefined, ] .filter(Boolean) .join(" and "); @@ -280,15 +280,15 @@ export class AcpRuntime implements LocalAgentRuntime { } // A durable resumed session keeps its previously selected provider // configuration. If resume does not re-advertise config options, do not - // force a redundant set operation for persisted model/thinking values. + // force a redundant set operation for persisted model/effort values. return; } if (input.model) { const config = resolveAcpModelConfigUpdate(metadata, input.model, this.provider, sessionId); await this.connection.agent.request("session/set_config_option", config); } - if (input.thinking) { - const config = resolveAcpThinkingConfigUpdate(metadata, input.thinking, this.provider, sessionId); + if (input.effort) { + const config = resolveAcpEffortConfigUpdate(metadata, input.effort, this.provider, sessionId); await this.connection.agent.request("session/set_config_option", config); } } @@ -547,17 +547,17 @@ export function resolveAcpModelConfigUpdate( }); } -export function resolveAcpThinkingConfigUpdate( +export function resolveAcpEffortConfigUpdate( session: unknown, - thinking: string, + effort: string, provider: string, sessionIdOverride?: string, ): { sessionId: string; configId: string; value: string } { return resolveAcpSelectConfigUpdate(session, { category: "thought_level", - label: "thinking option", + label: "reasoning effort option", provider, - value: thinking, + value: effort, sessionIdOverride, }); } diff --git a/src/local-agent-adapters.test.ts b/src/local-agent-adapters.test.ts index 10bb9769d..c628ab685 100644 --- a/src/local-agent-adapters.test.ts +++ b/src/local-agent-adapters.test.ts @@ -7,7 +7,7 @@ import { extractPiFinalResponse, extractPiProviderError, resolveAcpModelConfigUpdate, - resolveAcpThinkingConfigUpdate, + resolveAcpEffortConfigUpdate, } from "./local-agent-adapters.js"; import { removeDevspaceNodeModulesBinFromPath } from "./local-agent-path.js"; import type { LocalAgentProvider } from "./local-agent-profiles.js"; @@ -109,7 +109,7 @@ assert.throws( ); assert.deepEqual( - resolveAcpThinkingConfigUpdate({ + resolveAcpEffortConfigUpdate({ sessionId: "session_1", newSessionResponse: { configOptions: [ @@ -129,7 +129,7 @@ assert.deepEqual( ); assert.deepEqual( - resolveAcpThinkingConfigUpdate({ + resolveAcpEffortConfigUpdate({ sessionId: "session_2", newSessionResponse: { configOptions: [ @@ -155,7 +155,7 @@ assert.deepEqual( ); assert.throws( - () => resolveAcpThinkingConfigUpdate({ + () => resolveAcpEffortConfigUpdate({ sessionId: "session_3", newSessionResponse: { configOptions: [ @@ -172,21 +172,21 @@ assert.throws( ); assert.throws( - () => resolveAcpThinkingConfigUpdate(undefined, "high", "copilot"), + () => resolveAcpEffortConfigUpdate(undefined, "high", "copilot"), /session metadata/, ); assert.throws( - () => resolveAcpThinkingConfigUpdate({ newSessionResponse: { configOptions: [] } }, "high", "copilot"), + () => resolveAcpEffortConfigUpdate({ newSessionResponse: { configOptions: [] } }, "high", "copilot"), /session id/, ); assert.throws( - () => resolveAcpThinkingConfigUpdate({ + () => resolveAcpEffortConfigUpdate({ sessionId: "session_4", newSessionResponse: { configOptions: [] }, }, "high", "copilot"), - /does not expose a thinking option/, + /does not expose a reasoning effort option/, ); { @@ -215,7 +215,7 @@ assert.equal( { info: { id: "msg_assistant", role: "assistant" }, parts: [ - { type: "reasoning", text: "thinking" }, + { type: "reasoning", text: "effort" }, { type: "tool", tool: "grep", input: { pattern: "secret" }, output: "src/foo.ts" }, { type: "text", text: "Final OpenCode response." }, ], @@ -237,7 +237,7 @@ assert.equal( id: "msg_assistant", type: "assistant", content: [ - { type: "reasoning", text: "thinking" }, + { type: "reasoning", text: "effort" }, { type: "tool", name: "grep", state: { status: "completed", result: "src/foo.ts" } }, { type: "text", text: "Final OpenCode v2 response." }, ], @@ -255,7 +255,7 @@ assert.equal( role: "assistant", structured: { summary: "structured answer" }, }, - parts: [{ type: "reasoning", text: "thinking" }], + parts: [{ type: "reasoning", text: "effort" }], }, }), '{"summary":"structured answer"}', @@ -266,7 +266,7 @@ assert.equal( data: { info: { id: "msg_tool_only", role: "assistant" }, parts: [ - { type: "reasoning", text: "thinking" }, + { type: "reasoning", text: "effort" }, { type: "tool", tool: "bash", input: { command: "cat src/secret.ts" }, output: "secret" }, ], }, diff --git a/src/local-agent-adapters.ts b/src/local-agent-adapters.ts index 475390e5b..c1b558ede 100644 --- a/src/local-agent-adapters.ts +++ b/src/local-agent-adapters.ts @@ -3,7 +3,7 @@ import { AcpLocalAgentDriver, resolveAcpCommand, resolveAcpModelConfigUpdate, - resolveAcpThinkingConfigUpdate, + resolveAcpEffortConfigUpdate, } from "./local-agent-acp.js"; import { ClaudeLocalAgentDriver, @@ -72,5 +72,5 @@ export { extractPiProviderError, resolveAcpCommand, resolveAcpModelConfigUpdate, - resolveAcpThinkingConfigUpdate, + resolveAcpEffortConfigUpdate, }; diff --git a/src/local-agent-claude.test.ts b/src/local-agent-claude.test.ts index 971342195..183d83ef1 100644 --- a/src/local-agent-claude.test.ts +++ b/src/local-agent-claude.test.ts @@ -57,7 +57,7 @@ const context: LocalAgentRuntimeContext = { provider: "claude", workspaceRoot: "/tmp/project", model: "sonnet", - thinking: "high", + effort: "high", writeMode: "read_only", }; let factoryCalls = 0; @@ -90,7 +90,7 @@ const firstResult = await runtime.run({ prompt: "first", workspaceRoot: "/tmp/project", model: "sonnet", - thinking: "high", + effort: "high", writeMode: "read_only", }, { onSessionId: (sessionId) => { sessionIds.push(sessionId); }, @@ -101,7 +101,7 @@ const first = firstResult.value; const secondResult = await runtime.run({ prompt: "second", workspaceRoot: "/tmp/project", - thinking: "low", + effort: "low", writeMode: "allowed", }); assert.equal(secondResult.isOk(), true); @@ -110,7 +110,7 @@ const second = secondResult.value; const third = await runtime.run({ prompt: "third", workspaceRoot: "/tmp/project", - thinking: "high", + effort: "high", writeMode: "full_access", }); assert.equal(third.isOk(), true); diff --git a/src/local-agent-claude.ts b/src/local-agent-claude.ts index d6f30d7b0..16feed692 100644 --- a/src/local-agent-claude.ts +++ b/src/local-agent-claude.ts @@ -103,10 +103,10 @@ export class ClaudeQueryRuntime implements LocalAgentRuntime { } if (this.providerSessionId) await callbacks?.onSessionId?.(this.providerSessionId); const flagSettings = claudeAuthoritySettings(input.workspaceRoot, input.writeMode); - if (input.thinking) { + if (input.effort) { Object.assign(flagSettings, { alwaysThinkingEnabled: true, - effortLevel: input.thinking, + effortLevel: input.effort, }); } await this.query.applyFlagSettings(flagSettings); @@ -233,7 +233,7 @@ export class ClaudeLocalAgentDriver implements LocalAgentDriver { providerSessionId: context.providerSessionId, writeMode: context.writeMode, model: context.model, - thinking: context.thinking, + effort: context.effort, }; const query = await this.factory({ context, @@ -268,7 +268,7 @@ export function claudeQueryOptions( return { cwd: input.workspaceRoot, ...(input.model ? { model: input.model } : {}), - ...(input.thinking ? { thinking: { type: "adaptive" }, effort: input.thinking } : {}), + ...(input.effort ? { thinking: { type: "adaptive" }, effort: input.effort } : {}), ...(context.providerSessionId ? { resume: context.providerSessionId } : {}), permissionMode, sandbox: authority.sandbox, diff --git a/src/local-agent-codex.test.ts b/src/local-agent-codex.test.ts index 9efd7be14..15a510f53 100644 --- a/src/local-agent-codex.test.ts +++ b/src/local-agent-codex.test.ts @@ -103,7 +103,7 @@ readline.createInterface({ input: process.stdin }).on("line", (line) => { workspaceRoot: "/tmp/project", writeMode: "read_only", model: "gpt-5.4", - thinking: "high", + effort: "high", }, { onSessionId: (id) => { callbackSessionId = id; } }); assert.equal(firstResult.isOk(), true); if (firstResult.isErr()) throw firstResult.error; diff --git a/src/local-agent-codex.ts b/src/local-agent-codex.ts index 0ebb0dd55..dac27cc35 100644 --- a/src/local-agent-codex.ts +++ b/src/local-agent-codex.ts @@ -468,7 +468,7 @@ function turnParams(input: LocalAgentRunInput, threadId: string): Record decodeLocalAgentDaemonRequest({ requestId: "req_2", - protocolVersion: 1, + protocolVersion: 2, authToken: "test-secret", method: "agent.start", params: { target: "reviewer", prompt: "" }, @@ -68,7 +68,7 @@ assert.equal(record.latestResponse, " response whitespace \n"); const response = decodeLocalAgentDaemonResponse({ requestId: "req_1", - protocolVersion: 1, + protocolVersion: 2, ok: true, result: record, }); @@ -76,7 +76,7 @@ assert.equal(response.ok, true); const errorResponse = decodeLocalAgentDaemonResponse(JSON.parse(encodeLocalAgentDaemonResponse({ requestId: "req_error", - protocolVersion: 1, + protocolVersion: 2, ok: false, error: { code: "PROVIDER_UNAVAILABLE", diff --git a/src/local-agent-daemon-protocol.ts b/src/local-agent-daemon-protocol.ts index f5824c668..b79773613 100644 --- a/src/local-agent-daemon-protocol.ts +++ b/src/local-agent-daemon-protocol.ts @@ -185,7 +185,7 @@ export function decodeAgentRecord(value: unknown): LocalAgentRecord { profileName: requiredString(record?.profileName, "profileName"), provider: requiredString(record?.provider, "provider"), model: optionalString(record?.model), - thinking: optionalString(record?.thinking), + effort: optionalString(record?.effort), providerSessionId: optionalString(record?.providerSessionId), status, latestResponse: optionalContentString(record?.latestResponse), @@ -249,7 +249,7 @@ function decodeStartInput(value: unknown): StartLocalAgentInput { workspaceRoot: requiredString(record?.workspaceRoot, "workspaceRoot"), workspaceId: requiredString(record?.workspaceId, "workspaceId"), model: optionalString(record?.model), - thinking: optionalString(record?.thinking), + effort: optionalString(record?.effort), writeMode: decodeWriteMode(record?.writeMode), }; } @@ -263,7 +263,7 @@ function decodeContinueInput(value: unknown): { id: string; prompt: string; scop scope: decodeWorkspaceScope(record?.scope), ...(overrides ? { overrides: { model: optionalString(overrides.model), - thinking: optionalString(overrides.thinking), + effort: optionalString(overrides.effort), writeMode: decodeWriteMode(overrides.writeMode), } } : {}), }; diff --git a/src/local-agent-daemon.test.ts b/src/local-agent-daemon.test.ts index 3e9fd11da..895caf083 100644 --- a/src/local-agent-daemon.test.ts +++ b/src/local-agent-daemon.test.ts @@ -223,11 +223,11 @@ const timeoutServer = createNetServer((socket) => { if (request.method !== "hello") return; socket.end(encodeLocalAgentDaemonResponse({ requestId: request.requestId, - protocolVersion: 1, + protocolVersion: 2, ok: true, result: { state: "ready", - protocolVersion: 1, + protocolVersion: 2, pid: process.pid, endpoint: timeoutPaths.endpoint, startedAt: "now", @@ -269,7 +269,7 @@ const invalidServer = createNetServer((socket) => { if (!buffer.includes("\n")) return; socket.end(encodeLocalAgentDaemonResponse({ requestId: "wrong_request_id", - protocolVersion: 1, + protocolVersion: 2, ok: true, result: {}, })); @@ -323,7 +323,7 @@ try { const unauthorized = await sendRawRequest(socketDaemon.paths.endpoint, JSON.stringify({ requestId: "unauthorized", - protocolVersion: 1, + protocolVersion: 2, authToken: "wrong-secret", method: "hello", params: {}, diff --git a/src/local-agent-manager.ts b/src/local-agent-manager.ts index 85a0e6ea4..16de67aa8 100644 --- a/src/local-agent-manager.ts +++ b/src/local-agent-manager.ts @@ -38,13 +38,13 @@ export interface StartLocalAgentInput { workspaceRoot: string; workspaceId: string; model?: string; - thinking?: string; + effort?: string; writeMode?: LocalAgentWriteMode; } export interface RunOverrides { model?: string; - thinking?: string; + effort?: string; writeMode?: LocalAgentWriteMode; } @@ -104,7 +104,7 @@ export class LocalAgentManager { yield* manager.acceptingResult("start"); const workspaceRoot = yield* manager.authorizeWorkspace(input.workspaceRoot, "start"); const profiles = yield* Result.await(manager.loadProfilesResult(workspaceRoot, input.target)); - const target = resolveLocalAgentTarget(input.target, profiles, input.model, input.thinking); + const target = resolveLocalAgentTarget(input.target, profiles, input.model, input.effort); if (!target) { return Result.err(new AgentTargetError({ code: "UNKNOWN_TARGET", @@ -129,11 +129,11 @@ export class LocalAgentManager { profileName: target.name, provider: target.provider, model: target.model, - thinking: target.thinking, + effort: target.effort, }); return manager.begin(record, input.prompt, { model: target.model, - thinking: target.thinking, + effort: target.effort, writeMode: input.writeMode, }); }); @@ -229,7 +229,7 @@ export class LocalAgentManager { const updated = this.store.updateResult(record.id, { status: "running", model: overrides.model ?? record.model, - thinking: overrides.thinking ?? record.thinking, + effort: overrides.effort ?? record.effort, latestResponse: undefined, error: undefined, errorCode: undefined, @@ -292,7 +292,7 @@ export class LocalAgentManager { providerSessionId: record.providerSessionId, writeMode: input.value.writeMode, model: input.value.model, - thinking: input.value.thinking, + effort: input.value.effort, agentDir: this.agentDir, }; const callbacks: LocalAgentRunCallbacks = { @@ -401,9 +401,9 @@ export class LocalAgentManager { providerSessionId: record.providerSessionId, writeMode: overrides.writeMode ?? "allowed", model: record.model ?? profile?.model, - thinking: record.thinking ?? profile?.thinking, + effort: record.effort ?? profile?.effort, modelOverrideRequested: overrides.model !== undefined, - thinkingOverrideRequested: overrides.thinking !== undefined, + effortOverrideRequested: overrides.effort !== undefined, }); } diff --git a/src/local-agent-opencode.test.ts b/src/local-agent-opencode.test.ts index 90c222146..7798d1d07 100644 --- a/src/local-agent-opencode.test.ts +++ b/src/local-agent-opencode.test.ts @@ -69,7 +69,7 @@ const first = await pool.run(driver, { prompt: "first", workspaceRoot: "/tmp/project", model: "anthropic/sonnet", - thinking: "high", + effort: "high", }); const second = await pool.run(driver, { agentId: "agt_two", @@ -106,10 +106,10 @@ await pool.run(driver, { provider: "opencode", workspaceRoot: "/tmp/project", }, { - prompt: "thinking override", + prompt: "effort override", workspaceRoot: "/tmp/project", providerSessionId: firstRecord.providerSessionId ?? undefined, - thinking: "low", + effort: "low", }, { onSessionId: (id) => { callbackSessionId = id; }, }); diff --git a/src/local-agent-opencode.ts b/src/local-agent-opencode.ts index f7424c659..b2f86faa0 100644 --- a/src/local-agent-opencode.ts +++ b/src/local-agent-opencode.ts @@ -58,7 +58,7 @@ export class OpencodeRuntime implements LocalAgentRuntime { try { await assertOpencodeHealthy(this.client); const resumed = Boolean(input.providerSessionId); - const initialModel = input.model ? parseOpencodeModel(input.model, input.thinking) : undefined; + const initialModel = input.model ? parseOpencodeModel(input.model, input.effort) : undefined; const sessionId = input.providerSessionId ?? await createOpencodeSession(this.client, input, initialModel); await callbacks?.onSessionId?.(sessionId); await this.client.v2.session.switchAgent({ @@ -66,7 +66,7 @@ export class OpencodeRuntime implements LocalAgentRuntime { agent: opencodeAgentFor(input.writeMode), }, { throwOnError: true }); - const model = initialModel ?? (input.thinking ? await modelWithThinking(this.client, sessionId, input.thinking) : undefined); + const model = initialModel ?? (input.effort ? await modelWithEffort(this.client, sessionId, input.effort) : undefined); if (model && (resumed || !initialModel)) { await this.client.v2.session.switchModel({ sessionID: sessionId, model }, { throwOnError: true }); } @@ -235,10 +235,10 @@ class OpencodeHealthError extends Error { } } -async function modelWithThinking( +async function modelWithEffort( client: OpencodeClientLike, sessionId: string, - thinking: string, + effort: string, ): Promise { const result = await client.v2.session.get({ sessionID: sessionId }, { throwOnError: true }); const model = result.data.data.model; @@ -248,10 +248,10 @@ async function modelWithThinking( provider: "opencode", operation: "resolve_model", retryable: false, - message: "OpenCode did not return the current session model for a thinking override.", + message: "OpenCode did not return the current session model for an effort override.", }); } - return { ...model, variant: thinking }; + return { ...model, variant: effort }; } async function promptOpencodeSession( diff --git a/src/local-agent-pi.test.ts b/src/local-agent-pi.test.ts index f43ded9f9..f404105e9 100644 --- a/src/local-agent-pi.test.ts +++ b/src/local-agent-pi.test.ts @@ -18,7 +18,7 @@ class FakePiSession implements PiSessionLike { private readonly listeners = new Set(); disposeCount = 0; model?: unknown; - thinking?: unknown; + effort?: unknown; activeTools: string[] = []; toolHistory: string[][] = []; @@ -46,7 +46,7 @@ class FakePiSession implements PiSessionLike { } setThinkingLevel(level: any): void { - this.thinking = level; + this.effort = level; } dispose(): void { @@ -75,7 +75,7 @@ const first = await pool.run(driver, context, { prompt: "first", workspaceRoot: "/tmp/project", model: "provider/model", - thinking: "high", + effort: "high", writeMode: "read_only", }, { onSessionId: (sessionId) => { sessionIds.push(sessionId); }, @@ -103,7 +103,7 @@ if (second.isErr()) throw second.error; assert.equal(first.value.providerSessionId, "pi_session_1"); assert.equal(second.value.finalResponse, "response:second"); assert.deepEqual(sessions[0]?.model, { id: "model" }); -assert.equal(sessions[0]?.thinking, "high"); +assert.equal(sessions[0]?.effort, "high"); assert.deepEqual(sessionIds, ["pi_session_1"]); assert.deepEqual(piToolsForWriteMode("allowed"), ["read", "grep", "find", "ls", "edit", "write", "bash"]); assert.ok( diff --git a/src/local-agent-pi.ts b/src/local-agent-pi.ts index cf9b4b72c..8880983b1 100644 --- a/src/local-agent-pi.ts +++ b/src/local-agent-pi.ts @@ -155,8 +155,8 @@ export class PiSessionRuntime implements LocalAgentRuntime { } await this.session.setModel(model as never); } - if (input.thinking) { - this.session.setThinkingLevel(input.thinking as never); + if (input.effort) { + this.session.setThinkingLevel(input.effort as never); } } } @@ -183,7 +183,7 @@ export class PiLocalAgentDriver implements LocalAgentDriver { providerSessionId: context.providerSessionId, writeMode: context.writeMode, model: context.model, - thinking: context.thinking, + effort: context.effort, }; const session = await this.factory(context, input); return new PiSessionRuntime(session); @@ -237,7 +237,7 @@ async function defaultPiSessionFactory( sessionManager: sessionManager as never, resourceLoader, ...(model ? { model: model as never } : {}), - ...(input.thinking ? { thinkingLevel: input.thinking as never } : {}), + ...(input.effort ? { thinkingLevel: input.effort as never } : {}), // Keep the full built-in registry available so warm turns can narrow or // broaden active tools without recreating the session. tools: [...PI_FULL_ACCESS_TOOLS], diff --git a/src/local-agent-profiles.test.ts b/src/local-agent-profiles.test.ts index 7665b9f8a..6868e140e 100644 --- a/src/local-agent-profiles.test.ts +++ b/src/local-agent-profiles.test.ts @@ -35,7 +35,7 @@ try { 'description: "Project reviewer #1."', "provider: claude", "model: sonnet", - "thinking: high", + "effort: high", "---", "", "Project body.", @@ -70,14 +70,14 @@ try { assert.equal(profiles[0]?.description, "Project reviewer #1."); assert.equal(profiles[0]?.provider, "claude"); assert.equal(profiles[0]?.model, "sonnet"); - assert.equal(profiles[0]?.thinking, "high"); + assert.equal(profiles[0]?.effort, "high"); assert.equal(profiles[0]?.body, "Project body."); assert.deepEqual(summarizeLocalAgentProfile(profiles[0]!), { name: "reviewer", description: "Project reviewer #1.", provider: "claude", model: "sonnet", - thinking: "high", + effort: "high", }); await writeFile( diff --git a/src/local-agent-profiles.ts b/src/local-agent-profiles.ts index d41a7a64d..327028491 100644 --- a/src/local-agent-profiles.ts +++ b/src/local-agent-profiles.ts @@ -20,7 +20,7 @@ export interface LocalAgentProfile { description: string; provider: LocalAgentProvider; model?: string; - thinking?: string; + effort?: string; filePath: string; body: string; disabled: boolean; @@ -31,7 +31,7 @@ export interface LocalAgentProfileSummary { description: string; provider: LocalAgentProvider; model?: string; - thinking?: string; + effort?: string; } interface ParsedFrontmatter { @@ -74,7 +74,7 @@ export function summarizeLocalAgentProfile( description: profile.description, provider: profile.provider, model: profile.model, - thinking: profile.thinking, + effort: profile.effort, }; } @@ -158,7 +158,7 @@ function profileFromFrontmatter( description, provider, model: readString(frontmatter, "model"), - thinking: readString(frontmatter, "thinking"), + effort: readString(frontmatter, "effort"), filePath, body, disabled: frontmatter.disabled === true, diff --git a/src/local-agent-runtime.ts b/src/local-agent-runtime.ts index 696682c9f..ecc40f545 100644 --- a/src/local-agent-runtime.ts +++ b/src/local-agent-runtime.ts @@ -10,9 +10,9 @@ export interface LocalAgentRunInput { providerSessionId?: string; writeMode?: LocalAgentWriteMode; model?: string; - thinking?: string; + effort?: string; modelOverrideRequested?: boolean; - thinkingOverrideRequested?: boolean; + effortOverrideRequested?: boolean; } export interface LocalAgentRunResult { @@ -38,7 +38,7 @@ export interface LocalAgentRuntimeContext { providerSessionId?: string; writeMode?: LocalAgentWriteMode; model?: string; - thinking?: string; + effort?: string; agentDir?: string; } diff --git a/src/local-agent-store.test.ts b/src/local-agent-store.test.ts index 131b25f2e..eaa3be30a 100644 --- a/src/local-agent-store.test.ts +++ b/src/local-agent-store.test.ts @@ -18,12 +18,12 @@ try { profileName: "reviewer", provider: "codex", model: "gpt-5.4", - thinking: "high", + effort: "high", }); assert.match(created.id, /^agt_[a-f0-9]{8}$/); assert.equal(created.status, "starting"); - assert.equal(store.getById(created.id)?.thinking, "high"); + assert.equal(store.getById(created.id)?.effort, "high"); assert.equal(store.getById(created.id)?.profileName, "reviewer"); assert.equal(store.getById(created.id.slice(0, 7)), undefined); @@ -31,14 +31,14 @@ try { status: "error", latestResponse: "done", providerSessionId: "thread_123", - thinking: "medium", + effort: "medium", error: "Codex executable was not found.", errorCode: "PROVIDER_UNAVAILABLE", errorRetryable: false, }); assert.equal(updated.status, "error"); - assert.equal(updated.thinking, "medium"); + assert.equal(updated.effort, "medium"); assert.equal(updated.errorCode, "PROVIDER_UNAVAILABLE"); assert.equal(updated.errorRetryable, false); assert.equal(store.getById("thread_123"), undefined); @@ -103,13 +103,14 @@ assert.deepEqual(store.list({ workspaceRoot: join(root, "other") }), []); } legacy.prepare(` insert into local_agent_sessions ( - id, workspace_root, profile_name, provider, status, error, created_at, updated_at - ) values (?, ?, ?, ?, ?, ?, ?, ?) + id, workspace_root, profile_name, provider, thinking, status, error, created_at, updated_at + ) values (?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( "agt_legacy", join(root, "legacy-project"), "reviewer", "codex", + "high", "error", "old error", "2026-08-01T00:00:00.000Z", @@ -121,6 +122,7 @@ assert.deepEqual(store.list({ workspaceRoot: join(root, "other") }), []); stores.push(upgradedStore); const legacyRecord = upgradedStore.getById("agt_legacy"); assert.equal(legacyRecord?.error, "old error"); + assert.equal(legacyRecord?.effort, "high"); assert.equal(legacyRecord?.errorCode, undefined); assert.equal(legacyRecord?.errorRetryable, undefined); const upgradedRecord = upgradedStore.update("agt_legacy", { diff --git a/src/local-agent-store.ts b/src/local-agent-store.ts index 16e5275c5..ef987a8e9 100644 --- a/src/local-agent-store.ts +++ b/src/local-agent-store.ts @@ -13,7 +13,7 @@ export interface LocalAgentRecord { profileName: string; provider: string; model?: string; - thinking?: string; + effort?: string; providerSessionId?: string; status: LocalAgentStatus; latestResponse?: string; @@ -30,7 +30,7 @@ export interface CreateLocalAgentRecordInput { profileName: string; provider: string; model?: string; - thinking?: string; + effort?: string; } export interface LocalAgentWorkspaceScope { @@ -50,7 +50,7 @@ interface LocalAgentRow { profile_name: string; provider: string; model: string | null; - thinking: string | null; + effort: string | null; provider_session_id: string | null; status: string; latest_response: string | null; @@ -116,7 +116,7 @@ export class LocalAgentStore { profileName: input.profileName, provider: input.provider, model: input.model, - thinking: input.thinking, + effort: input.effort, status: "starting", createdAt: now, updatedAt: now, @@ -131,7 +131,7 @@ export class LocalAgentStore { profile_name, provider, model, - thinking, + effort, status, created_at, updated_at @@ -144,7 +144,7 @@ export class LocalAgentStore { record.profileName, record.provider, record.model ?? null, - record.thinking ?? null, + record.effort ?? null, record.status, record.createdAt, record.updatedAt, @@ -198,7 +198,7 @@ export class LocalAgentStore { profile_name = ?, provider = ?, model = ?, - thinking = ?, + effort = ?, provider_session_id = ?, status = ?, latest_response = ?, @@ -214,7 +214,7 @@ export class LocalAgentStore { updated.profileName, updated.provider, updated.model ?? null, - updated.thinking ?? null, + updated.effort ?? null, updated.providerSessionId ?? null, updated.status, updated.latestResponse ?? null, @@ -271,7 +271,7 @@ function rowToLocalAgentRecord(row: LocalAgentRow): LocalAgentRecord { profileName: row.profile_name, provider: row.provider, model: row.model ?? undefined, - thinking: row.thinking ?? undefined, + effort: row.effort ?? undefined, providerSessionId: row.provider_session_id ?? undefined, status: readStatus(row.status), latestResponse: row.latest_response ?? undefined, diff --git a/src/local-agent-targets.test.ts b/src/local-agent-targets.test.ts index 3f1ae08f0..e243579dd 100644 --- a/src/local-agent-targets.test.ts +++ b/src/local-agent-targets.test.ts @@ -12,7 +12,7 @@ const profiles: LocalAgentProfile[] = [ description: "Review changes.", provider: "codex", model: "gpt-5-codex", - thinking: "high", + effort: "high", filePath: "/workspace/.devspace/agents/reviewer.md", body: "Review carefully.", disabled: false, @@ -32,35 +32,35 @@ assert.deepEqual(parseLocalAgentRunArgs(["codex", "hello", "world"]), { target: "codex", prompt: "hello world", model: undefined, - thinking: undefined, + effort: undefined, }); assert.deepEqual(parseLocalAgentRunArgs(["codex", "--model", "gpt-5.1", "hello"]), { target: "codex", prompt: "hello", model: "gpt-5.1", - thinking: undefined, + effort: undefined, }); assert.deepEqual(parseLocalAgentRunArgs(["codex", "--model=gpt-5.1", "hello"]), { target: "codex", prompt: "hello", model: "gpt-5.1", - thinking: undefined, + effort: undefined, }); -assert.deepEqual(parseLocalAgentRunArgs(["codex", "--thinking", "high", "hello"]), { +assert.deepEqual(parseLocalAgentRunArgs(["codex", "--effort", "high", "hello"]), { target: "codex", prompt: "hello", model: undefined, - thinking: "high", + effort: "high", }); -assert.deepEqual(parseLocalAgentRunArgs(["codex", "--thinking=high", "hello"]), { +assert.deepEqual(parseLocalAgentRunArgs(["codex", "--effort=high", "hello"]), { target: "codex", prompt: "hello", model: undefined, - thinking: "high", + effort: "high", }); assert.throws( @@ -69,8 +69,8 @@ assert.throws( ); assert.throws( - () => parseLocalAgentRunArgs(["codex", "--thinking"]), - /Missing value for --thinking/, + () => parseLocalAgentRunArgs(["codex", "--effort"]), + /Missing value for --effort/, ); { @@ -79,14 +79,14 @@ assert.throws( assert.equal(target?.name, "reviewer"); assert.equal(target?.provider, "codex"); assert.equal(target?.model, "gpt-5-codex"); - assert.equal(target?.thinking, "high"); + assert.equal(target?.effort, "high"); } { const target = resolveLocalAgentTarget("reviewer", profiles, "gpt-5.2", "xhigh"); assert.equal(target?.kind, "profile"); assert.equal(target?.model, "gpt-5.2"); - assert.equal(target?.thinking, "xhigh"); + assert.equal(target?.effort, "xhigh"); } { @@ -95,14 +95,14 @@ assert.throws( assert.equal(target?.name, "opencode"); assert.equal(target?.provider, "opencode"); assert.equal(target?.model, undefined); - assert.equal(target?.thinking, undefined); + assert.equal(target?.effort, undefined); } { const target = resolveLocalAgentTarget("opencode", profiles, "kimi-k2", "deep"); assert.equal(target?.kind, "provider"); assert.equal(target?.model, "kimi-k2"); - assert.equal(target?.thinking, "deep"); + assert.equal(target?.effort, "deep"); } { diff --git a/src/local-agent-targets.ts b/src/local-agent-targets.ts index ab6faaf05..868aad309 100644 --- a/src/local-agent-targets.ts +++ b/src/local-agent-targets.ts @@ -9,14 +9,14 @@ export interface ParsedLocalAgentRunArgs { target: string; prompt: string; model?: string; - thinking?: string; + effort?: string; } export interface ParsedLocalAgentContinueArgs { agentId: string; prompt: string; model?: string; - thinking?: string; + effort?: string; } export type LocalAgentTarget = @@ -25,7 +25,7 @@ export type LocalAgentTarget = name: string; provider: LocalAgentProvider; model?: string; - thinking?: string; + effort?: string; profile: LocalAgentProfile; } | { @@ -33,13 +33,13 @@ export type LocalAgentTarget = name: LocalAgentProvider; provider: LocalAgentProvider; model?: string; - thinking?: string; + effort?: string; }; export function parseLocalAgentRunArgs(args: string[]): ParsedLocalAgentRunArgs { const parsed = parseAgentPromptArgs( args, - 'Usage: devspace agents run [--model ] [--thinking ] ""', + 'Usage: devspace agents run [--model ] [--effort ] ""', ); return parsed; } @@ -47,9 +47,9 @@ export function parseLocalAgentRunArgs(args: string[]): ParsedLocalAgentRunArgs export function parseLocalAgentContinueArgs(args: string[]): ParsedLocalAgentContinueArgs { const parsed = parseAgentPromptArgs( args, - 'Usage: devspace agents continue [--model ] [--thinking ] ""', + 'Usage: devspace agents continue [--model ] [--effort ] ""', ); - return { agentId: parsed.target, prompt: parsed.prompt, model: parsed.model, thinking: parsed.thinking }; + return { agentId: parsed.target, prompt: parsed.prompt, model: parsed.model, effort: parsed.effort }; } function parseAgentPromptArgs( @@ -62,7 +62,7 @@ function parseAgentPromptArgs( } let model: string | undefined; - let thinking: string | undefined; + let effort: string | undefined; const promptParts: string[] = []; for (let index = 0; index < rest.length; index += 1) { const part = rest[index]; @@ -79,17 +79,17 @@ function parseAgentPromptArgs( model = value; continue; } - if (part === "--thinking") { + if (part === "--effort") { const value = rest[index + 1]?.trim(); - if (!value) throw new Error("Missing value for --thinking."); - thinking = value; + if (!value) throw new Error("Missing value for --effort."); + effort = value; index += 1; continue; } - if (part?.startsWith("--thinking=")) { - const value = part.slice("--thinking=".length).trim(); - if (!value) throw new Error("Missing value for --thinking."); - thinking = value; + if (part?.startsWith("--effort=")) { + const value = part.slice("--effort=".length).trim(); + if (!value) throw new Error("Missing value for --effort."); + effort = value; continue; } promptParts.push(part ?? ""); @@ -100,14 +100,14 @@ function parseAgentPromptArgs( throw new Error(usage); } - return { target, prompt, model, thinking }; + return { target, prompt, model, effort }; } export function resolveLocalAgentTarget( target: string, profiles: LocalAgentProfile[], modelOverride?: string, - thinkingOverride?: string, + effortOverride?: string, ): LocalAgentTarget | undefined { const profile = profiles.find((candidate) => candidate.name === target); if (profile) { @@ -116,7 +116,7 @@ export function resolveLocalAgentTarget( name: profile.name, provider: profile.provider, model: modelOverride ?? profile.model, - thinking: thinkingOverride ?? profile.thinking, + effort: effortOverride ?? profile.effort, profile, }; } @@ -127,7 +127,7 @@ export function resolveLocalAgentTarget( name: target, provider: target, model: modelOverride, - thinking: thinkingOverride, + effort: effortOverride, }; } diff --git a/src/server.ts b/src/server.ts index 477986b72..a29cbc365 100644 --- a/src/server.ts +++ b/src/server.ts @@ -220,16 +220,16 @@ function formatVisibleAgent(agent: { name: string; provider: string; model?: string; - thinking?: string; + effort?: string; providerAvailable?: boolean; providerUnavailableReason?: string; }): string { const model = agent.model ? `, model ${agent.model}` : ""; - const thinking = agent.thinking ? `, thinking ${agent.thinking}` : ""; + const effort = agent.effort ? `, effort ${agent.effort}` : ""; const availability = agent.providerAvailable === false ? `, unavailable: ${agent.providerUnavailableReason ?? "provider unavailable"}` : ""; - return `${agent.name} (${agent.provider}${model}${thinking}${availability})`; + return `${agent.name} (${agent.provider}${model}${effort}${availability})`; } function formatUnavailableAgentProvider(provider: LocalAgentProviderAvailability): string { @@ -267,7 +267,7 @@ const workspaceLocalAgentOutputSchema = z.object({ description: z.string(), provider: z.string(), model: z.string().optional(), - thinking: z.string().optional(), + effort: z.string().optional(), providerAvailable: z.boolean().optional(), providerUnavailableReason: z.string().optional(), }); diff --git a/src/ui/card-types.ts b/src/ui/card-types.ts index 0da0db2b1..77c928158 100644 --- a/src/ui/card-types.ts +++ b/src/ui/card-types.ts @@ -75,7 +75,7 @@ export interface ToolResultCard { description?: string; provider?: string; model?: string; - thinking?: string; + effort?: string; providerAvailable?: boolean; providerUnavailableReason?: string; }>; diff --git a/src/ui/workspace-app.tsx b/src/ui/workspace-app.tsx index 46d653765..0cb393734 100644 --- a/src/ui/workspace-app.tsx +++ b/src/ui/workspace-app.tsx @@ -531,7 +531,7 @@ function renderWorkspacePayload(container: HTMLElement, card: ToolResultCard): v agent.description, providerName ? `Provider: ${providerName}` : undefined, agent.model ? `Model: ${agent.model}` : undefined, - agent.thinking ? `Thinking: ${agent.thinking}` : undefined, + agent.effort ? `Effort: ${agent.effort}` : undefined, unavailable ? agent.providerUnavailableReason ?? "Provider unavailable" : undefined, From da4cea8a37470c76ffc2b5f196d19e3e94183718 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:07:48 +0530 Subject: [PATCH 2/8] docs(agents): document effort configuration --- docs/agent-profile-schema.md | 12 ++++++------ docs/chatgpt-coding-workflow.md | 2 +- docs/configuration.md | 2 +- examples/agents/claude-implementer.md | 2 +- examples/agents/codex-explorer.md | 2 +- examples/agents/codex-qa-tester.md | 2 +- examples/agents/opencode-explorer.md | 2 +- examples/agents/pi-reviewer.md | 2 +- skills/subagent-delegation/SKILL.md | 16 ++++++++-------- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/agent-profile-schema.md b/docs/agent-profile-schema.md index 0f28e300b..4e2ca11af 100644 --- a/docs/agent-profile-schema.md +++ b/docs/agent-profile-schema.md @@ -27,7 +27,7 @@ name: reviewer description: Read-only reviewer for bugs, security risks, and missing tests. provider: codex model: gpt-5.4 -thinking: high +effort: high disabled: false --- @@ -100,16 +100,16 @@ model: gpt-5.4 model: sonnet ``` -### `thinking` +### `effort` Optional provider reasoning effort, thinking level, or model variant. If omitted, DevSpace lets the provider default apply. Values are provider-specific passthrough strings; DevSpace does not translate names between harnesses. ```yaml -thinking: low -thinking: high -thinking: xhigh +effort: low +effort: high +effort: xhigh ``` DevSpace passes this through to providers that expose a matching control: @@ -159,7 +159,7 @@ devspace agents show "description": "Read-only reviewer for bugs, security risks, and missing tests.", "provider": "codex", "model": "gpt-5.4", - "thinking": "high" + "effort": "high" } ``` diff --git a/docs/chatgpt-coding-workflow.md b/docs/chatgpt-coding-workflow.md index 20f263aad..1ec7fe57f 100644 --- a/docs/chatgpt-coding-workflow.md +++ b/docs/chatgpt-coding-workflow.md @@ -123,7 +123,7 @@ It also keeps compatibility with: When Subagents are enabled, DevSpace discovers agent profiles from `~/.devspace/agents/*.md` and project `.devspace/agents/*.md`. `open_workspace` exposes a compact catalog with profile names, descriptions, -providers, and optional models/thinking levels so the model can choose a configured agent +providers, and optional models/effort levels so the model can choose a configured agent without seeing provider-specific launch details. Example profiles are packaged under `examples/agents/` for users who want diff --git a/docs/configuration.md b/docs/configuration.md index 2a22ea036..ff4fe8135 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -147,7 +147,7 @@ from: - project `.devspace/agents/*.md` `open_workspace` returns a compact catalog containing profile names, -descriptions, providers, and optional models/thinking levels so the host model can choose an +descriptions, providers, and optional models/effort levels so the host model can choose an agent without reading provider-specific launch details. `devspace agents ls` lists existing subagent sessions for the current workspace, scoped by the workspace environment injected into shell commands. The `subagent-delegation` diff --git a/examples/agents/claude-implementer.md b/examples/agents/claude-implementer.md index b907659fe..6f967b5f7 100644 --- a/examples/agents/claude-implementer.md +++ b/examples/agents/claude-implementer.md @@ -4,7 +4,7 @@ name: claude-implementer description: Implementation profile for multi-file changes, careful refactors, and failing test repair. provider: claude model: sonnet -thinking: high +effort: high --- Take ownership of the requested implementation while keeping the change narrow. diff --git a/examples/agents/codex-explorer.md b/examples/agents/codex-explorer.md index 93a92db6c..3645f2091 100644 --- a/examples/agents/codex-explorer.md +++ b/examples/agents/codex-explorer.md @@ -4,7 +4,7 @@ name: codex-explorer description: Read-only profile for bounded codebase questions, architecture tracing, and risk discovery. provider: codex model: gpt-5.4-mini -thinking: high +effort: high --- Investigate without editing. Use this profile to answer bounded questions such diff --git a/examples/agents/codex-qa-tester.md b/examples/agents/codex-qa-tester.md index f85706197..ae24b1a24 100644 --- a/examples/agents/codex-qa-tester.md +++ b/examples/agents/codex-qa-tester.md @@ -4,7 +4,7 @@ name: codex-qa-tester description: Manual QA profile for browser testing, workflow verification, and regression checks. provider: codex model: gpt-5.4-mini -thinking: high +effort: high --- Verify the requested user workflow from the outside, like a QA pass before diff --git a/examples/agents/opencode-explorer.md b/examples/agents/opencode-explorer.md index 250a4d84a..884be72db 100644 --- a/examples/agents/opencode-explorer.md +++ b/examples/agents/opencode-explorer.md @@ -4,7 +4,7 @@ name: opencode-explorer description: Read-only profile for fast relevant-file discovery and small architecture questions. provider: opencode model: opencode/deepseek-v4-flash-free -thinking: high +effort: high --- Find the answer quickly without editing. Use this profile when the main need is diff --git a/examples/agents/pi-reviewer.md b/examples/agents/pi-reviewer.md index 4ed2b9ded..e5a4a8a62 100644 --- a/examples/agents/pi-reviewer.md +++ b/examples/agents/pi-reviewer.md @@ -4,7 +4,7 @@ name: pi-reviewer description: Read-only review profile for quick risk checks and targeted implementation questions. provider: pi model: openai-codex/gpt-5.5 -thinking: high +effort: high --- Review or investigate only the area requested. This profile is best for quick diff --git a/skills/subagent-delegation/SKILL.md b/skills/subagent-delegation/SKILL.md index 7bba04c7f..24fc72668 100644 --- a/skills/subagent-delegation/SKILL.md +++ b/skills/subagent-delegation/SKILL.md @@ -37,11 +37,11 @@ profile is needed. Built-in providers are listed by `open_workspace`. `continue ""` sends a follow-up to an existing agent. Do not use `run ` for continuation. -Continuation supports the same per-turn model and thinking overrides: +Continuation supports the same per-turn model and effort overrides: ```bash devspace agents continue --model "" -devspace agents continue --thinking "" +devspace agents continue --effort "" ``` `show ` prints status and the latest response. If the agent is still @@ -64,18 +64,18 @@ Choose profiles from the compact subagent profile catalog returned by profile fits and delegation is still appropriate, use a built-in provider name from `open_workspace`. -Profiles may declare a model and optional thinking level. To override the -configured/default provider model or thinking level for a run, pass `--model` -or `--thinking`: +Profiles may declare a model and optional effort level. To override the +configured/default provider model or effort level for a run, pass `--model` +or `--effort`: ```bash devspace agents run --model "" -devspace agents run --thinking "" +devspace agents run --effort "" ``` -Use `--thinking` only when the user asks for a specific reasoning depth or when +Use `--effort` only when the user asks for a specific reasoning depth or when the task clearly needs a different effort than the configured profile default. -Thinking values are provider-specific passthrough values. Use names supported by +Effort values are provider-specific passthrough values. Use names supported by the selected local agent harness; DevSpace does not translate values between providers. From bdae5b954d1ef317d4aa5f09994ea7b5bb740eb4 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:09:59 +0530 Subject: [PATCH 3/8] test(db): track the effort migration --- src/oauth-store.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/oauth-store.test.ts b/src/oauth-store.test.ts index d3ecacb36..225f9fdf5 100644 --- a/src/oauth-store.test.ts +++ b/src/oauth-store.test.ts @@ -46,6 +46,7 @@ async function testDatabaseConfiguration(stateDir: string): Promise { { version: 3, name: "local-agent-sessions" }, { version: 4, name: "workspace-conversation-bindings" }, { version: 5, name: "local-agent-structured-errors" }, + { version: 6, name: "local-agent-effort-rename" }, ]); } finally { database.close(); From 5525b143f9fde7dbdfb6623bcbc20e89e35bac97 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:09:59 +0530 Subject: [PATCH 4/8] fix(agents): replace idle legacy daemons --- src/local-agent-client.ts | 64 +++++++++++++++++++++++++++ src/local-agent-daemon.test.ts | 80 ++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) diff --git a/src/local-agent-client.ts b/src/local-agent-client.ts index eeae963f6..17e1ff1ae 100644 --- a/src/local-agent-client.ts +++ b/src/local-agent-client.ts @@ -8,6 +8,7 @@ import type { ServerConfig } from "./config.js"; import { AgentDaemonInvalidRequestError, AgentDaemonInvalidResponseError, + AgentDaemonProtocolMismatchError, AgentDaemonStartupError, AgentDaemonTimeoutError, AgentDaemonUnauthorizedError, @@ -219,12 +220,75 @@ export class LocalAgentClient { message: "Local agent daemon returned an invalid hello error.", })); } + if ( + error.code === "DAEMON_PROTOCOL_MISMATCH" + && response.value.protocolVersion < LOCAL_AGENT_DAEMON_PROTOCOL_VERSION + ) { + return this.replaceIdleOlderDaemon(authToken.value, response.value.protocolVersion, error); + } return error.code === "DAEMON_UNAVAILABLE" ? Result.ok(undefined) : Result.err(error); } const decoded = decodeValue(response.value.result, "hello", decodeDaemonStatus); return decoded.map((status) => status.state === "ready" ? status : undefined); } + private async replaceIdleOlderDaemon( + authToken: string, + protocolVersion: number, + mismatch: AgentDaemonProtocolMismatchError, + ): Promise> { + const statusResponse = await sendRequest(this.endpoint, { + requestId: randomUUID(), + protocolVersion, + authToken, + method: "hello", + params: {}, + }, this.requestTimeoutMs); + if (statusResponse.isErr() || !statusResponse.value.ok) return Result.err(mismatch); + const status = decodeValue(statusResponse.value.result, "hello", decodeDaemonStatus); + if (status.isErr()) return status; + if (status.value.activeTurns > 0) { + return Result.err(new AgentDaemonProtocolMismatchError({ + code: "DAEMON_PROTOCOL_MISMATCH", + operation: "startup", + retryable: true, + cause: mismatch, + message: "An older local agent daemon is still running active turns. Retry after they finish.", + })); + } + + const stopResponse = await sendRequest(this.endpoint, { + requestId: randomUUID(), + protocolVersion, + authToken, + method: "daemon.stop", + params: {}, + }, this.requestTimeoutMs); + if (stopResponse.isErr() || !stopResponse.value.ok) return Result.err(mismatch); + + const deadline = Date.now() + this.startupTimeoutMs; + while (Date.now() < deadline) { + await delay(RETRY_DELAY_MS); + const probe = await sendRequest(this.endpoint, { + requestId: randomUUID(), + protocolVersion, + authToken, + method: "hello", + params: {}, + }, Math.min(this.requestTimeoutMs, 250)); + if (probe.isErr() && probe.error.code === "DAEMON_UNAVAILABLE") { + return Result.ok(undefined); + } + } + return Result.err(new AgentDaemonStartupError({ + code: "DAEMON_STARTUP_FAILURE", + operation: "startup", + retryable: true, + cause: mismatch, + message: "The older local agent daemon did not stop in time for the upgrade.", + })); + } + private async request( method: M, params: Extract['params'], diff --git a/src/local-agent-daemon.test.ts b/src/local-agent-daemon.test.ts index 895caf083..d72a09c63 100644 --- a/src/local-agent-daemon.test.ts +++ b/src/local-agent-daemon.test.ts @@ -208,6 +208,86 @@ const startupFailure = await startupFailureClient.ensureReady(); assert.equal(startupFailure.isErr(), true); if (startupFailure.isErr()) assert.equal(startupFailure.error.code, "DAEMON_STARTUP_FAILURE"); +const upgradeStateDir = join(root, "upgrade-state"); +await mkdir(upgradeStateDir, { recursive: true }); +const upgradePaths = localAgentDaemonPaths(upgradeStateDir); +ensureLocalAgentDaemonSecret(upgradePaths); +const legacyMethods: string[] = []; +const legacyServer = createNetServer((socket) => { + let buffer = ""; + socket.setEncoding("utf8"); + socket.on("data", (chunk: string | Buffer) => { + buffer += chunk.toString(); + const newline = buffer.indexOf("\n"); + if (newline === -1) return; + const request = JSON.parse(buffer.slice(0, newline)) as { + requestId: string; + protocolVersion: number; + method: string; + }; + legacyMethods.push(`${request.method}:${request.protocolVersion}`); + if (request.protocolVersion !== 1) { + socket.end(encodeLocalAgentDaemonResponse({ + requestId: request.requestId, + protocolVersion: 1, + ok: false, + error: { + code: "DAEMON_PROTOCOL_MISMATCH", + message: "Unsupported daemon protocol version 2; expected 1.", + retryable: false, + }, + })); + return; + } + const stopping = request.method === "daemon.stop"; + socket.end(encodeLocalAgentDaemonResponse({ + requestId: request.requestId, + protocolVersion: 1, + ok: true, + result: { + state: stopping ? "stopping" : "ready", + protocolVersion: 1, + pid: process.pid, + endpoint: upgradePaths.endpoint, + startedAt: "now", + activeTurns: 0, + runtimeCount: 0, + clientConnections: 1, + }, + }), () => { + if (stopping) legacyServer.close(); + }); + }); +}); +await new Promise((resolveListen, rejectListen) => { + legacyServer.once("error", rejectListen); + legacyServer.listen(upgradePaths.endpoint, resolveListen); +}); +const replacementManager = new FakeManager(); +replacementManager.activeTurnCount = 0; +const replacementDaemon = new LocalAgentDaemon({ + stateDir: upgradeStateDir, + manager: replacementManager, + idleShutdownMs: 60_000, +}); +let replacementSpawns = 0; +const upgradeClient = new LocalAgentClient({ + stateDir: upgradeStateDir, + startupTimeoutMs: 2_000, + requestTimeoutMs: 500, + spawnDaemon: () => { + replacementSpawns += 1; + void replacementDaemon.start(); + }, +}); +try { + assert.equal(unwrap(await upgradeClient.ensureReady()).protocolVersion, 2); + assert.equal(replacementSpawns, 1); + assert.deepEqual(legacyMethods.slice(0, 3), ["hello:2", "hello:1", "daemon.stop:1"]); +} finally { + await replacementDaemon.close(); +} + const timeoutStateDir = join(root, "request-timeout-state"); await mkdir(timeoutStateDir, { recursive: true }); const timeoutPaths = localAgentDaemonPaths(timeoutStateDir); From dd0f8fd20ae19b47ad2baed8a7d14d40b7eca8a5 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:41:41 +0530 Subject: [PATCH 5/8] fix(db): preserve mixed-schema effort values --- src/db/migrations.ts | 11 ++++++++++- src/local-agent-store.test.ts | 4 +++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/db/migrations.ts b/src/db/migrations.ts index 3f547ffee..df192caa0 100644 --- a/src/db/migrations.ts +++ b/src/db/migrations.ts @@ -218,7 +218,16 @@ function migrateLocalAgentEffortRename(sqlite: Database.Database): void { name: string; }>; const names = new Set(columns.map((column) => column.name)); - if (names.has("effort")) return; + if (names.has("effort")) { + if (names.has("thinking")) { + sqlite.exec(` + update local_agent_sessions + set effort = thinking + where effort is null and thinking is not null + `); + } + return; + } if (!names.has("thinking")) { addColumnIfMissing(sqlite, "local_agent_sessions", "effort", "text"); return; diff --git a/src/local-agent-store.test.ts b/src/local-agent-store.test.ts index eaa3be30a..829940f92 100644 --- a/src/local-agent-store.test.ts +++ b/src/local-agent-store.test.ts @@ -98,7 +98,9 @@ assert.deepEqual(store.list({ workspaceRoot: join(root, "other") }), []); const migration = legacy.prepare( "insert into devspace_schema_migrations (version, name, applied_at) values (?, ?, ?)", ); - for (const [version, name] of [[1, "workspace-state"], [2, "oauth-state"], [3, "local-agent-sessions"], [4, "workspace-conversation-bindings"]] as const) { + // Leave migration 3 unapplied to exercise an interrupted legacy upgrade: + // it adds an empty effort column before migration 6 copies thinking values. + for (const [version, name] of [[1, "workspace-state"], [2, "oauth-state"], [4, "workspace-conversation-bindings"]] as const) { migration.run(version, name, "2026-08-01T00:00:00.000Z"); } legacy.prepare(` From e26517a76aa7c4648597819d7427b69c8dd409e0 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:41:41 +0530 Subject: [PATCH 6/8] fix(agents): tolerate concurrent daemon upgrades --- src/local-agent-client.ts | 10 ++++- src/local-agent-daemon.test.ts | 67 ++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/local-agent-client.ts b/src/local-agent-client.ts index 17e1ff1ae..f9858c0ae 100644 --- a/src/local-agent-client.ts +++ b/src/local-agent-client.ts @@ -236,7 +236,7 @@ export class LocalAgentClient { authToken: string, protocolVersion: number, mismatch: AgentDaemonProtocolMismatchError, - ): Promise> { + ): Promise> { const statusResponse = await sendRequest(this.endpoint, { requestId: randomUUID(), protocolVersion, @@ -279,6 +279,14 @@ export class LocalAgentClient { if (probe.isErr() && probe.error.code === "DAEMON_UNAVAILABLE") { return Result.ok(undefined); } + if ( + probe.isOk() + && probe.value.protocolVersion >= LOCAL_AGENT_DAEMON_PROTOCOL_VERSION + ) { + // Another client completed the replacement while this client was + // waiting for the old endpoint to disappear. + return this.tryHello(); + } } return Result.err(new AgentDaemonStartupError({ code: "DAEMON_STARTUP_FAILURE", diff --git a/src/local-agent-daemon.test.ts b/src/local-agent-daemon.test.ts index d72a09c63..6b2e86d9a 100644 --- a/src/local-agent-daemon.test.ts +++ b/src/local-agent-daemon.test.ts @@ -288,6 +288,73 @@ try { await replacementDaemon.close(); } +const replacementRaceStateDir = join(root, "upgrade-race-state"); +await mkdir(replacementRaceStateDir, { recursive: true }); +const replacementRacePaths = localAgentDaemonPaths(replacementRaceStateDir); +ensureLocalAgentDaemonSecret(replacementRacePaths); +let replacementRaceProtocol = 1; +const replacementRaceServer = createNetServer((socket) => { + let buffer = ""; + socket.setEncoding("utf8"); + socket.on("data", (chunk: string | Buffer) => { + buffer += chunk.toString(); + const newline = buffer.indexOf("\n"); + if (newline === -1) return; + const request = JSON.parse(buffer.slice(0, newline)) as { + requestId: string; + protocolVersion: number; + method: string; + }; + if (request.protocolVersion !== replacementRaceProtocol) { + socket.end(encodeLocalAgentDaemonResponse({ + requestId: request.requestId, + protocolVersion: replacementRaceProtocol, + ok: false, + error: { + code: "DAEMON_PROTOCOL_MISMATCH", + message: `Unsupported daemon protocol version ${request.protocolVersion}.`, + retryable: false, + }, + })); + return; + } + socket.end(encodeLocalAgentDaemonResponse({ + requestId: request.requestId, + protocolVersion: replacementRaceProtocol, + ok: true, + result: { + state: request.method === "daemon.stop" ? "stopping" : "ready", + protocolVersion: replacementRaceProtocol, + pid: process.pid, + endpoint: replacementRacePaths.endpoint, + startedAt: "now", + activeTurns: 0, + runtimeCount: 0, + clientConnections: 1, + }, + }), () => { + if (request.method === "daemon.stop") replacementRaceProtocol = 2; + }); + }); +}); +await new Promise((resolveListen, rejectListen) => { + replacementRaceServer.once("error", rejectListen); + replacementRaceServer.listen(replacementRacePaths.endpoint, resolveListen); +}); +const replacementRaceClient = new LocalAgentClient({ + stateDir: replacementRaceStateDir, + startupTimeoutMs: 500, + requestTimeoutMs: 100, + spawnDaemon: () => { + throw new Error("the replacement daemon is already running"); + }, +}); +try { + assert.equal(unwrap(await replacementRaceClient.ensureReady()).protocolVersion, 2); +} finally { + await new Promise((resolveClose) => replacementRaceServer.close(() => resolveClose())); +} + const timeoutStateDir = join(root, "request-timeout-state"); await mkdir(timeoutStateDir, { recursive: true }); const timeoutPaths = localAgentDaemonPaths(timeoutStateDir); From e0dc3e5747d51478ce7e55276a8ffa4ba851398f Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:44:23 +0530 Subject: [PATCH 7/8] test(agents): follow the current daemon protocol --- src/local-agent-daemon.test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/local-agent-daemon.test.ts b/src/local-agent-daemon.test.ts index 6b2e86d9a..da4002766 100644 --- a/src/local-agent-daemon.test.ts +++ b/src/local-agent-daemon.test.ts @@ -9,6 +9,7 @@ import { daemonExecArgv, LocalAgentClient } from "./local-agent-client.js"; import { LocalAgentDaemon, type LocalAgentDaemonManager } from "./local-agent-daemon.js"; import { ensureLocalAgentDaemonSecret, + LOCAL_AGENT_DAEMON_PROTOCOL_VERSION, localAgentDaemonPaths, } from "./local-agent-daemon-lifecycle.js"; import { @@ -333,7 +334,9 @@ const replacementRaceServer = createNetServer((socket) => { clientConnections: 1, }, }), () => { - if (request.method === "daemon.stop") replacementRaceProtocol = 2; + if (request.method === "daemon.stop") { + replacementRaceProtocol = LOCAL_AGENT_DAEMON_PROTOCOL_VERSION; + } }); }); }); @@ -350,7 +353,10 @@ const replacementRaceClient = new LocalAgentClient({ }, }); try { - assert.equal(unwrap(await replacementRaceClient.ensureReady()).protocolVersion, 2); + assert.equal( + unwrap(await replacementRaceClient.ensureReady()).protocolVersion, + LOCAL_AGENT_DAEMON_PROTOCOL_VERSION, + ); } finally { await new Promise((resolveClose) => replacementRaceServer.close(() => resolveClose())); } From e7e0bb47ebfad74ea44e9375e7a157da27ace63b Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:48:31 +0530 Subject: [PATCH 8/8] fix(agents): wait for daemon ownership handoff --- src/local-agent-client.ts | 6 +++++- src/local-agent-daemon.test.ts | 13 ++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/local-agent-client.ts b/src/local-agent-client.ts index f9858c0ae..eb47ebafa 100644 --- a/src/local-agent-client.ts +++ b/src/local-agent-client.ts @@ -35,6 +35,7 @@ import { import { LOCAL_AGENT_DAEMON_PROTOCOL_VERSION, ensureLocalAgentDaemonSecret, + isProcessAlive, localAgentDaemonPaths, readLocalAgentDaemonSecret, type LocalAgentDaemonPaths, @@ -277,7 +278,10 @@ export class LocalAgentClient { params: {}, }, Math.min(this.requestTimeoutMs, 250)); if (probe.isErr() && probe.error.code === "DAEMON_UNAVAILABLE") { - return Result.ok(undefined); + if (!existsSync(this.paths.lockPath) || !isProcessAlive(status.value.pid)) { + return Result.ok(undefined); + } + continue; } if ( probe.isOk() diff --git a/src/local-agent-daemon.test.ts b/src/local-agent-daemon.test.ts index da4002766..610241900 100644 --- a/src/local-agent-daemon.test.ts +++ b/src/local-agent-daemon.test.ts @@ -10,6 +10,7 @@ import { LocalAgentDaemon, type LocalAgentDaemonManager } from "./local-agent-da import { ensureLocalAgentDaemonSecret, LOCAL_AGENT_DAEMON_PROTOCOL_VERSION, + LocalAgentDaemonLock, localAgentDaemonPaths, } from "./local-agent-daemon-lifecycle.js"; import { @@ -213,6 +214,8 @@ const upgradeStateDir = join(root, "upgrade-state"); await mkdir(upgradeStateDir, { recursive: true }); const upgradePaths = localAgentDaemonPaths(upgradeStateDir); ensureLocalAgentDaemonSecret(upgradePaths); +const legacyLock = new LocalAgentDaemonLock(upgradePaths); +legacyLock.acquire(); const legacyMethods: string[] = []; const legacyServer = createNetServer((socket) => { let buffer = ""; @@ -256,7 +259,11 @@ const legacyServer = createNetServer((socket) => { clientConnections: 1, }, }), () => { - if (stopping) legacyServer.close(); + if (stopping) { + legacyServer.close(() => { + setTimeout(() => legacyLock.release(), 50); + }); + } }); }); }); @@ -272,20 +279,24 @@ const replacementDaemon = new LocalAgentDaemon({ idleShutdownMs: 60_000, }); let replacementSpawns = 0; +let spawnedBeforeLegacyLockReleased = false; const upgradeClient = new LocalAgentClient({ stateDir: upgradeStateDir, startupTimeoutMs: 2_000, requestTimeoutMs: 500, spawnDaemon: () => { replacementSpawns += 1; + spawnedBeforeLegacyLockReleased = existsSync(upgradePaths.lockPath); void replacementDaemon.start(); }, }); try { assert.equal(unwrap(await upgradeClient.ensureReady()).protocolVersion, 2); assert.equal(replacementSpawns, 1); + assert.equal(spawnedBeforeLegacyLockReleased, false); assert.deepEqual(legacyMethods.slice(0, 3), ["hello:2", "hello:1", "daemon.stop:1"]); } finally { + legacyLock.release(); await replacementDaemon.close(); }