fix: Resolve signal.json path mismatch for standalone agents
Standalone agents (no initiative or 0 linked projects) run in a workspace/ subdirectory, but signal.json lookups used the parent directory. This caused all standalone agents to be marked "crashed" despite successful completion. Track the actual agent cwd at spawn time via ActiveAgent.agentCwd and probe for the workspace/ subdirectory during reconciliation and crash detection paths.
This commit is contained in:
@@ -43,6 +43,19 @@ export class CleanupManager {
|
|||||||
return join(this.workspaceRoot, 'agent-workdirs', alias);
|
return join(this.workspaceRoot, 'agent-workdirs', alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the actual working directory for an agent, probing for the
|
||||||
|
* workspace/ subdirectory used by standalone agents.
|
||||||
|
*/
|
||||||
|
private resolveAgentCwd(worktreeId: string): string {
|
||||||
|
const base = this.getAgentWorkdir(worktreeId);
|
||||||
|
const workspaceSub = join(base, 'workspace');
|
||||||
|
if (!existsSync(join(base, '.cw', 'output')) && existsSync(join(workspaceSub, '.cw'))) {
|
||||||
|
return workspaceSub;
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove git worktrees for an agent.
|
* Remove git worktrees for an agent.
|
||||||
* Handles both initiative-linked (multi-project) and standalone agents.
|
* Handles both initiative-linked (multi-project) and standalone agents.
|
||||||
@@ -341,6 +354,7 @@ export class CleanupManager {
|
|||||||
pid: number;
|
pid: number;
|
||||||
tailer: FileTailer;
|
tailer: FileTailer;
|
||||||
outputFilePath: string;
|
outputFilePath: string;
|
||||||
|
agentCwd?: string;
|
||||||
}>,
|
}>,
|
||||||
onStreamEvent: (agentId: string, event: StreamEvent) => void,
|
onStreamEvent: (agentId: string, event: StreamEvent) => void,
|
||||||
onAgentOutput: (agentId: string, rawOutput: string, provider: NonNullable<ReturnType<typeof getProvider>>) => Promise<void>,
|
onAgentOutput: (agentId: string, rawOutput: string, provider: NonNullable<ReturnType<typeof getProvider>>) => Promise<void>,
|
||||||
@@ -375,17 +389,22 @@ export class CleanupManager {
|
|||||||
|
|
||||||
const pid = agent.pid!;
|
const pid = agent.pid!;
|
||||||
|
|
||||||
|
// Resolve actual agent cwd — standalone agents run in workspace/ subdir
|
||||||
|
const resolvedCwd = this.resolveAgentCwd(agent.worktreeId);
|
||||||
|
|
||||||
activeAgents.set(agent.id, {
|
activeAgents.set(agent.id, {
|
||||||
agentId: agent.id,
|
agentId: agent.id,
|
||||||
pid,
|
pid,
|
||||||
tailer,
|
tailer,
|
||||||
outputFilePath: agent.outputFilePath,
|
outputFilePath: agent.outputFilePath,
|
||||||
|
agentCwd: resolvedCwd,
|
||||||
});
|
});
|
||||||
|
|
||||||
pollForCompletion(agent.id, pid);
|
pollForCompletion(agent.id, pid);
|
||||||
} else if (agent.outputFilePath) {
|
} else if (agent.outputFilePath) {
|
||||||
// CRITICAL FIX: Check for signal.json completion FIRST before parsing raw output
|
// CRITICAL FIX: Check for signal.json completion FIRST before parsing raw output
|
||||||
const agentWorkdir = this.getAgentWorkdir(agent.worktreeId);
|
// Resolve actual agent cwd — standalone agents run in workspace/ subdir
|
||||||
|
const agentWorkdir = this.resolveAgentCwd(agent.worktreeId);
|
||||||
const hasValidSignal = this.signalManager ? await this.signalManager.readSignal(agentWorkdir) : null;
|
const hasValidSignal = this.signalManager ? await this.signalManager.readSignal(agentWorkdir) : null;
|
||||||
|
|
||||||
if (hasValidSignal) {
|
if (hasValidSignal) {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import { writeInputFiles } from './file-io.js';
|
|||||||
import { buildWorkspaceLayout, buildInterAgentCommunication } from './prompts/index.js';
|
import { buildWorkspaceLayout, buildInterAgentCommunication } from './prompts/index.js';
|
||||||
import { getProvider } from './providers/registry.js';
|
import { getProvider } from './providers/registry.js';
|
||||||
import { createModuleLogger } from '../logger/index.js';
|
import { createModuleLogger } from '../logger/index.js';
|
||||||
import { join, dirname } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { unlink, readFile } from 'node:fs/promises';
|
import { unlink, readFile } from 'node:fs/promises';
|
||||||
import { existsSync, writeFileSync } from 'node:fs';
|
import { existsSync, writeFileSync } from 'node:fs';
|
||||||
import type { AccountCredentialManager } from './credentials/types.js';
|
import type { AccountCredentialManager } from './credentials/types.js';
|
||||||
@@ -340,7 +340,7 @@ export class MultiProviderAgentManager implements AgentManager {
|
|||||||
'utf-8'
|
'utf-8'
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeEntry: ActiveAgent = { agentId, pid, tailer, outputFilePath };
|
const activeEntry: ActiveAgent = { agentId, pid, tailer, outputFilePath, agentCwd: finalCwd };
|
||||||
this.activeAgents.set(agentId, activeEntry);
|
this.activeAgents.set(agentId, activeEntry);
|
||||||
log.info({ agentId, alias, pid, diagnosticWritten: true }, 'detached subprocess started with diagnostic');
|
log.info({ agentId, alias, pid, diagnosticWritten: true }, 'detached subprocess started with diagnostic');
|
||||||
|
|
||||||
@@ -820,7 +820,7 @@ export class MultiProviderAgentManager implements AgentManager {
|
|||||||
|
|
||||||
// Check if the agent has output that indicates successful completion
|
// Check if the agent has output that indicates successful completion
|
||||||
if (agent.outputFilePath) {
|
if (agent.outputFilePath) {
|
||||||
const hasCompletion = await this.checkAgentCompletionResult(agent.outputFilePath);
|
const hasCompletion = await this.checkAgentCompletionResult(agent.worktreeId);
|
||||||
if (hasCompletion) {
|
if (hasCompletion) {
|
||||||
log.info({
|
log.info({
|
||||||
agentId: processId,
|
agentId: processId,
|
||||||
@@ -872,14 +872,21 @@ export class MultiProviderAgentManager implements AgentManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if agent completed successfully by reading signal.json file.
|
* Check if agent completed successfully by reading signal.json file.
|
||||||
|
* Probes the workspace/ subdirectory for standalone agents.
|
||||||
*/
|
*/
|
||||||
private async checkAgentCompletionResult(outputFilePath: string): Promise<boolean> {
|
private async checkAgentCompletionResult(worktreeId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const agentDir = dirname(outputFilePath);
|
// Resolve actual agent workdir — standalone agents have .cw inside workspace/ subdir
|
||||||
const signalPath = join(agentDir, '.cw/output/signal.json');
|
let agentWorkdir = this.processManager.getAgentWorkdir(worktreeId);
|
||||||
|
const workspaceSub = join(agentWorkdir, 'workspace');
|
||||||
|
if (!existsSync(join(agentWorkdir, '.cw', 'output')) && existsSync(join(workspaceSub, '.cw'))) {
|
||||||
|
agentWorkdir = workspaceSub;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signalPath = join(agentWorkdir, '.cw/output/signal.json');
|
||||||
|
|
||||||
if (!existsSync(signalPath)) {
|
if (!existsSync(signalPath)) {
|
||||||
log.debug({ outputFilePath, signalPath }, 'no signal.json found - agent not completed');
|
log.debug({ worktreeId, signalPath }, 'no signal.json found - agent not completed');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -890,15 +897,15 @@ export class MultiProviderAgentManager implements AgentManager {
|
|||||||
const completed = signal.status === 'done' || signal.status === 'questions' || signal.status === 'error';
|
const completed = signal.status === 'done' || signal.status === 'questions' || signal.status === 'error';
|
||||||
|
|
||||||
if (completed) {
|
if (completed) {
|
||||||
log.debug({ outputFilePath, signal }, 'agent completion detected via signal.json');
|
log.debug({ worktreeId, signal }, 'agent completion detected via signal.json');
|
||||||
} else {
|
} else {
|
||||||
log.debug({ outputFilePath, signal }, 'signal.json found but status indicates incomplete');
|
log.debug({ worktreeId, signal }, 'signal.json found but status indicates incomplete');
|
||||||
}
|
}
|
||||||
|
|
||||||
return completed;
|
return completed;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.warn({ outputFilePath, err: err instanceof Error ? err.message : String(err) }, 'failed to read or parse signal.json');
|
log.warn({ worktreeId, err: err instanceof Error ? err.message : String(err) }, 'failed to read or parse signal.json');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export interface ActiveAgent {
|
|||||||
pid: number;
|
pid: number;
|
||||||
tailer: import('./file-tailer.js').FileTailer;
|
tailer: import('./file-tailer.js').FileTailer;
|
||||||
outputFilePath: string;
|
outputFilePath: string;
|
||||||
|
/** Actual working directory the agent process runs in (may differ from getAgentWorkdir for standalone agents) */
|
||||||
|
agentCwd?: string;
|
||||||
result?: AgentResult;
|
result?: AgentResult;
|
||||||
pendingQuestions?: PendingQuestions;
|
pendingQuestions?: PendingQuestions;
|
||||||
streamResultText?: string;
|
streamResultText?: string;
|
||||||
@@ -226,8 +228,10 @@ export class OutputHandler {
|
|||||||
|
|
||||||
log.debug({ agentId }, 'detached agent completed');
|
log.debug({ agentId }, 'detached agent completed');
|
||||||
|
|
||||||
// Verify agent worked in correct location by checking for output files
|
// Resolve actual agent working directory — standalone agents run in a
|
||||||
const agentWorkdir = getAgentWorkdir(agent.worktreeId);
|
// "workspace/" subdirectory inside getAgentWorkdir, so prefer agentCwd
|
||||||
|
// recorded at spawn time when available.
|
||||||
|
const agentWorkdir = active?.agentCwd ?? getAgentWorkdir(agent.worktreeId);
|
||||||
const outputDir = join(agentWorkdir, '.cw', 'output');
|
const outputDir = join(agentWorkdir, '.cw', 'output');
|
||||||
const expectedPwdFile = join(agentWorkdir, '.cw', 'expected-pwd.txt');
|
const expectedPwdFile = join(agentWorkdir, '.cw', 'expected-pwd.txt');
|
||||||
const diagnosticFile = join(agentWorkdir, '.cw', 'spawn-diagnostic.json');
|
const diagnosticFile = join(agentWorkdir, '.cw', 'spawn-diagnostic.json');
|
||||||
@@ -267,7 +271,6 @@ export class OutputHandler {
|
|||||||
const outputFilePath = active?.outputFilePath ?? '';
|
const outputFilePath = active?.outputFilePath ?? '';
|
||||||
if (outputFilePath) {
|
if (outputFilePath) {
|
||||||
// First, check for robust signal.json completion before attempting incremental reading
|
// First, check for robust signal.json completion before attempting incremental reading
|
||||||
const agentWorkdir = getAgentWorkdir(agent.worktreeId); // FIX: Use worktreeId, not agentId!
|
|
||||||
log.debug({ agentId, worktreeId: agent.worktreeId, agentWorkdir }, 'checking signal completion');
|
log.debug({ agentId, worktreeId: agent.worktreeId, agentWorkdir }, 'checking signal completion');
|
||||||
|
|
||||||
const hasSignalCompletion = await this.readSignalCompletion(agentWorkdir);
|
const hasSignalCompletion = await this.readSignalCompletion(agentWorkdir);
|
||||||
@@ -311,9 +314,8 @@ export class OutputHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for signal.json file first, then fall back to stream text
|
// Check for signal.json file first, then fall back to stream text
|
||||||
const completionWorkdir = getAgentWorkdir(agent.worktreeId);
|
if (await this.readSignalCompletion(agentWorkdir)) {
|
||||||
if (await this.readSignalCompletion(completionWorkdir)) {
|
const signalPath = join(agentWorkdir, '.cw/output/signal.json');
|
||||||
const signalPath = join(completionWorkdir, '.cw/output/signal.json');
|
|
||||||
const signalContent = await readFile(signalPath, 'utf-8');
|
const signalContent = await readFile(signalPath, 'utf-8');
|
||||||
log.debug({ agentId, signalPath }, 'using signal.json content for completion');
|
log.debug({ agentId, signalPath }, 'using signal.json content for completion');
|
||||||
await this.processSignalAndFiles(agentId, signalContent, agent.mode as AgentMode, getAgentWorkdir, active?.streamSessionId);
|
await this.processSignalAndFiles(agentId, signalContent, agent.mode as AgentMode, getAgentWorkdir, active?.streamSessionId);
|
||||||
|
|||||||
Reference in New Issue
Block a user