From d61884520780094ceab368637b6424b29b7b6c23 Mon Sep 17 00:00:00 2001 From: Dante Date: Mon, 14 Sep 2026 04:22:53 +0800 Subject: [PATCH] fix(cli): resolve imported custom session launch Model: gpt-5.6-sol Signed-off-by: Dante --- ...26-09-14-imported-custom-session-launch.md | 32 +++++ ...09-14-imported-custom-session-launch.zh.md | 32 +++++ .../session-launch-config-resolver.test.ts | 113 ++++++++++++++++++ .../session/session-launch-config-resolver.ts | 36 ++++-- 4 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 .agents/notes/proposed/bug-fix/2026-09-14-imported-custom-session-launch.md create mode 100644 .agents/notes/proposed/bug-fix/2026-09-14-imported-custom-session-launch.zh.md diff --git a/.agents/notes/proposed/bug-fix/2026-09-14-imported-custom-session-launch.md b/.agents/notes/proposed/bug-fix/2026-09-14-imported-custom-session-launch.md new file mode 100644 index 000000000..33e01617f --- /dev/null +++ b/.agents/notes/proposed/bug-fix/2026-09-14-imported-custom-session-launch.md @@ -0,0 +1,32 @@ +# Resolve imported custom sessions from machine-owned configuration + +Status: proposed +Translation: current +PR: https://github.com/LodyAI/Lody/pull/689 + +[中文](2026-09-14-imported-custom-session-launch.zh.md) + +## Abstract + +Imported custom ACP sessions retain their provider identity but not the agent configuration id that originally launched the provider. Session resume therefore cannot recover the custom command even when the same machine still has exactly one matching configuration. Resolve that unique match from the already-open target-machine Flock, while treating zero or multiple matches as unresolved so no arbitrary executable can be selected. + +## Problem + +External history import materializes a local session with `cliType`, `agentType`, and `externalHistory`, but no `agentConfigId`. Builtin and registry providers can reconstruct their executable from static provider metadata; a custom provider needs the machine-owned `customAcp`, environment, and runtime override fields. The launch resolver currently returns before consulting any agent configuration when the id is absent, so opening the imported session reaches resume without a custom command and fails with `session_restore_failed`. + +History synchronization resolves its launch on the daemon under the target machine's authority. Session resume must preserve that boundary: imported metadata and control-plane callers must not supply an executable path. + +## Decision + +When an externally imported session has no `agentConfigId` and `cliType` is `custom`, scan the `agentConfig` family in the target machine's already-open Flock and match the session's `cliType` plus `agentType`: + +- exactly one match supplies the current `customAcp`, environment, and runtime overrides; +- no match preserves the existing unresolved result; +- multiple matches are ambiguous and also remain unresolved; +- non-imported sessions, sessions with an explicit id, builtin and registry providers, and legacy per-session fields retain their existing resolution paths. + +This intentionally follows current machine configuration, so updating the custom command updates later resumes without rewriting imported session metadata. It also avoids widening the session or control-plane schema with executable fields. + +## Verification + +The focused resolver suite covers a unique match, current-command updates, no match, ambiguity, non-imported legacy preservation, and the existing explicit-id behavior. A live third-party custom ACP process is not launched by this test. diff --git a/.agents/notes/proposed/bug-fix/2026-09-14-imported-custom-session-launch.zh.md b/.agents/notes/proposed/bug-fix/2026-09-14-imported-custom-session-launch.zh.md new file mode 100644 index 000000000..12ab7514e --- /dev/null +++ b/.agents/notes/proposed/bug-fix/2026-09-14-imported-custom-session-launch.zh.md @@ -0,0 +1,32 @@ +# 从机器自有配置解析导入的自定义会话启动参数 + +Status: proposed +Translation: current +PR: https://github.com/LodyAI/Lody/pull/689 + +[English](2026-09-14-imported-custom-session-launch.md) + +## 摘要 + +导入的自定义 ACP 会话保留了 provider 身份,但没有保留最初启动 provider 的 agent 配置 ID。因此,即使同一台机器仍有且仅有一个匹配配置,会话恢复也无法取回自定义命令。解决方案是从已经打开的目标机器 Flock 中解析唯一匹配;若零个或多个配置匹配,则保持未解析状态,避免任意选择可执行程序。 + +## 问题 + +外部历史导入会创建一个带有 `cliType`、`agentType` 和 `externalHistory`,但没有 `agentConfigId` 的本地会话。Builtin 和 registry provider 可以从静态 provider 元数据重建可执行程序;custom provider 则需要机器自有的 `customAcp`、环境变量和 runtime override。当前启动解析器在缺少 ID 时直接返回,不会读取任何 agent 配置,因此打开导入会话时,恢复流程拿不到自定义命令并报 `session_restore_failed`。 + +历史同步在 daemon 中以目标机器为授权边界解析启动配置。会话恢复也必须保持该边界:导入元数据和 control-plane 调用方都不能提供可执行程序路径。 + +## 决策 + +当外部导入的会话没有 `agentConfigId` 且 `cliType` 为 `custom` 时,扫描已经打开的目标机器 Flock 中的 `agentConfig` family,并用会话的 `cliType` 与 `agentType` 匹配: + +- 恰好一个匹配时,使用其当前 `customAcp`、环境变量与 runtime override; +- 没有匹配时,保留既有的未解析结果; +- 多个匹配时视为有歧义,同样保持未解析; +- 非导入会话、带明确 ID 的会话、builtin 与 registry provider,以及旧的逐会话字段继续沿用现有解析路径。 + +该方案有意读取当前机器配置,因此修改自定义命令后,后续恢复会直接使用新值,无需改写导入会话元数据。同时无需给会话或 control-plane schema 增加可执行程序字段。 + +## 验证 + +定向 resolver 测试覆盖唯一匹配、当前命令更新、无匹配、歧义、非导入会话的旧字段保留,以及既有的明确 ID 行为。测试没有启动真实的第三方 custom ACP 进程。 diff --git a/apps/cli/src/session/session-launch-config-resolver.test.ts b/apps/cli/src/session/session-launch-config-resolver.test.ts index 0da877af4..441f3343a 100644 --- a/apps/cli/src/session/session-launch-config-resolver.test.ts +++ b/apps/cli/src/session/session-launch-config-resolver.test.ts @@ -39,7 +39,120 @@ const sessionMeta = (): SessionMeta => agentConfigId, }) as SessionMeta; +const importedCustomSessionMeta = (): SessionMeta => + ({ + id: sessionId, + machineId, + cliType: 'custom', + agentType: 'history-agent', + origin: 'external-acp', + }) as SessionMeta; + +function customAgentConfig(input: { + id: string; + agentType?: string; + command: string; +}): Parameters[0] { + return { + key: machineFlockKeys.agentConfig(input.id as AgentConfigId), + value: { + id: input.id, + machineId, + name: input.id, + cliType: 'custom', + agentType: input.agentType ?? 'history-agent', + customAcp: { command: input.command }, + env: { HISTORY_TOKEN: input.id }, + prompt: '', + }, + } as Parameters[0]; +} + describe('resolveSessionLaunchConfig', () => { + it('uses the unique current custom config for an imported session without a config id', () => { + const flock = new FakeMachineFlock(); + const config = customAgentConfig({ id: 'custom-1', command: 'history-agent-v1' }); + flock.rows.push(config); + + expect( + readMachineSessionLaunchSnapshotFromFlock({ + flock, + sessionId, + sessionMeta: importedCustomSessionMeta(), + }).resolution + ).toEqual({ + source: 'agent-config', + config: { + customAcp: { command: 'history-agent-v1' }, + env: { HISTORY_TOKEN: 'custom-1' }, + }, + }); + + config.value = { + ...(config.value as object), + customAcp: { command: 'history-agent-v2' }, + }; + expect( + readMachineSessionLaunchSnapshotFromFlock({ + flock, + sessionId, + sessionMeta: importedCustomSessionMeta(), + }).resolution.config?.customAcp + ).toEqual({ command: 'history-agent-v2' }); + }); + + it('fails closed when an imported custom session has no matching config', () => { + const flock = new FakeMachineFlock(); + flock.rows.push( + customAgentConfig({ id: 'other-agent', agentType: 'other', command: 'other-agent' }) + ); + + expect( + readMachineSessionLaunchSnapshotFromFlock({ + flock, + sessionId, + sessionMeta: importedCustomSessionMeta(), + }).resolution + ).toEqual({ source: 'none', config: undefined }); + }); + + it('fails closed when multiple configs match an imported custom session', () => { + const flock = new FakeMachineFlock(); + flock.rows.push( + customAgentConfig({ id: 'custom-1', command: 'history-agent-v1' }), + customAgentConfig({ id: 'custom-2', command: 'history-agent-v2' }) + ); + + expect( + readMachineSessionLaunchSnapshotFromFlock({ + flock, + sessionId, + sessionMeta: importedCustomSessionMeta(), + }).resolution + ).toEqual({ source: 'none', config: undefined }); + }); + + it('preserves legacy launch fields for a non-imported custom session without a config id', () => { + const flock = new FakeMachineFlock(); + flock.rows.push(customAgentConfig({ id: 'custom-1', command: 'current-command' })); + const meta = { + ...importedCustomSessionMeta(), + origin: 'lody', + customAcp: { command: 'legacy-command' }, + env: { HISTORY_TOKEN: 'legacy' }, + } as SessionMeta; + + expect( + readMachineSessionLaunchSnapshotFromFlock({ flock, sessionId, sessionMeta: meta }).resolution + ).toEqual({ + source: 'legacy-session', + config: { + customAcp: { command: 'legacy-command' }, + env: { HISTORY_TOKEN: 'legacy' }, + }, + }); + }); + it('reads the current launch config synchronously from an existing Flock handle', () => { const flock = new FakeMachineFlock(); flock.rows.push({ diff --git a/apps/cli/src/session/session-launch-config-resolver.ts b/apps/cli/src/session/session-launch-config-resolver.ts index 5a6aabf6e..9f6bd6c32 100644 --- a/apps/cli/src/session/session-launch-config-resolver.ts +++ b/apps/cli/src/session/session-launch-config-resolver.ts @@ -50,6 +50,26 @@ export type MachineSessionLaunchSnapshot = { resolution: SessionLaunchConfigResolution; }; +function resolveImportedCustomAgentConfig( + sessionMeta: SessionMeta | undefined, + agentConfigs: Record +): AgentConfigMeta | null { + if ( + sessionMeta?.agentConfigId || + sessionMeta?.origin !== 'external-acp' || + sessionMeta.cliType !== 'custom' + ) { + return null; + } + const matches = Object.values(agentConfigs).filter( + (config) => + config.machineId === sessionMeta.machineId && + config.cliType === sessionMeta.cliType && + config.agentType === sessionMeta.agentType + ); + return matches.length === 1 ? (matches[0] ?? null) : null; +} + function resolveSessionLaunchConfigFromSources(input: { legacy: SessionLaunchConfig | undefined; agentConfig: AgentConfigLaunchFields | null | undefined; @@ -84,14 +104,20 @@ export function readMachineSessionLaunchSnapshotFromFlock(input: { machineFlockKeys.sessionLaunchConfig(input.sessionId), ...(agentConfigId ? [machineFlockKeys.agentConfig(agentConfigId)] : []), ], + ...(!agentConfigId && + input.sessionMeta?.origin === 'external-acp' && + input.sessionMeta.cliType === 'custom' + ? { families: ['agentConfig'] as const } + : {}), }); const legacy = mergeSessionLaunchConfig( getMachineFlockSessionLaunchConfig(rows, input.sessionId), metaLegacy ); + const agentConfigs = getMachineFlockAgentConfigs(rows); const agentConfig = agentConfigId - ? (getMachineFlockAgentConfigs(rows)[agentConfigId] ?? null) - : null; + ? (agentConfigs[agentConfigId] ?? null) + : resolveImportedCustomAgentConfig(input.sessionMeta, agentConfigs); return { legacy, agentConfig, @@ -177,11 +203,7 @@ export async function resolveSessionLaunchConfig(input: { }; } - if (!input.sessionMeta?.agentConfigId) { - return snapshot.resolution; - } - - if (snapshot.agentConfig) { + if (!input.sessionMeta?.agentConfigId || snapshot.agentConfig) { return snapshot.resolution; }