From b73704e79bbc888c4b6bcc356d8616b22ffe7c98 Mon Sep 17 00:00:00 2001 From: FourWindff Date: Mon, 25 May 2026 10:40:05 +0800 Subject: [PATCH] fix(coordinator): default coordinator tasks to human control --- src/store/persistence.test.ts | 42 +++++++++++++++++++++++++++++++++++ src/store/persistence.ts | 6 +++-- src/store/tasks.ts | 2 +- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/store/persistence.test.ts b/src/store/persistence.test.ts index 734e8da0..6b0c03eb 100644 --- a/src/store/persistence.test.ts +++ b/src/store/persistence.test.ts @@ -369,6 +369,48 @@ describe('coordinator control hint persistence', () => { }); }); +describe('coordinator control defaults', () => { + it('restores coordinator tasks to human control when persisted state omits controlledBy', async () => { + mockInvoke.mockResolvedValueOnce( + basePayload({ + taskOrder: ['coord-1'], + tasks: { + 'coord-1': { + ...persistedTask(agentDef()), + id: 'coord-1', + coordinatorMode: true, + }, + }, + activeTaskId: 'coord-1', + }), + ); + + await loadState(); + + expect(store.tasks['coord-1']?.controlledBy).toBe('human'); + }); + + it('restores coordinated child tasks to coordinator control when persisted state omits controlledBy', async () => { + mockInvoke.mockResolvedValueOnce( + basePayload({ + taskOrder: ['child-1'], + tasks: { + 'child-1': { + ...persistedTask(agentDef()), + id: 'child-1', + coordinatedBy: 'coord-1', + }, + }, + activeTaskId: 'child-1', + }), + ); + + await loadState(); + + expect(store.tasks['child-1']?.controlledBy).toBe('coordinator'); + }); +}); + describe('projects section collapsed persistence', () => { it('defaults to expanded when not in saved state', async () => { setStore('projectsCollapsed', true); diff --git a/src/store/persistence.ts b/src/store/persistence.ts index 3212b7ce..46e8ee61 100644 --- a/src/store/persistence.ts +++ b/src/store/persistence.ts @@ -676,7 +676,8 @@ export async function loadState(): Promise { propagateSkipPermissions: pt.propagateSkipPermissions, coordinatedBy: pt.coordinatedBy, controlledBy: - pt.controlledBy ?? (pt.coordinatorMode || pt.coordinatedBy ? 'coordinator' : undefined), + pt.controlledBy ?? + (pt.coordinatedBy ? 'coordinator' : pt.coordinatorMode ? 'human' : undefined), // Defer TerminalView spawn until StartMCPServer/hydrateTask complete — // the config file has a stale token from the previous session until then. mcpStartupStatus: @@ -777,7 +778,8 @@ export async function loadState(): Promise { propagateSkipPermissions: pt.propagateSkipPermissions, coordinatedBy: pt.coordinatedBy, controlledBy: - pt.controlledBy ?? (pt.coordinatorMode || pt.coordinatedBy ? 'coordinator' : undefined), + pt.controlledBy ?? + (pt.coordinatedBy ? 'coordinator' : pt.coordinatorMode ? 'human' : undefined), mcpStartupStatus: pt.coordinatorMode || pt.coordinatedBy ? ('pending' as const) : undefined, mcpConfigPath: pt.mcpConfigPath, diff --git a/src/store/tasks.ts b/src/store/tasks.ts index 66ddf733..09386509 100644 --- a/src/store/tasks.ts +++ b/src/store/tasks.ts @@ -281,7 +281,7 @@ export async function createTask(opts: CreateTaskOptions): Promise { propagateSkipPermissions: opts.coordinatorMode ? (opts.propagateSkipPermissions ?? false) : undefined, - controlledBy: opts.coordinatorMode ? 'coordinator' : undefined, + controlledBy: opts.coordinatorMode ? 'human' : undefined, mcpConfigPath, mcpLaunchArgs, // Coordinator tasks call StartMCPServer before entering the store, so MCP is ready immediately.