feat: add worktree archiving with context menu and improved logging
This commit is contained in:
@@ -801,6 +801,15 @@ export class AgentManager {
|
||||
|
||||
async closeAgent(agentId: string): Promise<void> {
|
||||
const agent = this.requireAgent(agentId);
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
lifecycle: agent.lifecycle,
|
||||
hasPendingRun: Boolean(agent.pendingRun),
|
||||
pendingPermissions: agent.pendingPermissions.size,
|
||||
},
|
||||
"closeAgent: start"
|
||||
);
|
||||
this.agents.delete(agentId);
|
||||
this.liveEventPumps.delete(agentId);
|
||||
this.liveEventBacklog.delete(agentId);
|
||||
@@ -816,6 +825,7 @@ export class AgentManager {
|
||||
};
|
||||
await session.close();
|
||||
this.emitState(closedAgent);
|
||||
this.logger.trace({ agentId }, "closeAgent: completed");
|
||||
}
|
||||
|
||||
async setAgentMode(agentId: string, modeId: string): Promise<void> {
|
||||
@@ -1005,7 +1015,24 @@ export class AgentManager {
|
||||
options?: AgentRunOptions
|
||||
): AsyncGenerator<AgentStreamEvent> {
|
||||
const existingAgent = this.requireAgent(agentId);
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
lifecycle: existingAgent.lifecycle,
|
||||
hasPendingRun: Boolean(existingAgent.pendingRun),
|
||||
promptType: typeof prompt === "string" ? "string" : "structured",
|
||||
hasRunOptions: Boolean(options),
|
||||
},
|
||||
"streamAgent: requested"
|
||||
);
|
||||
if (existingAgent.pendingRun) {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
lifecycle: existingAgent.lifecycle,
|
||||
},
|
||||
"streamAgent: rejected because pendingRun already exists"
|
||||
);
|
||||
throw new Error(`Agent ${agentId} already has an active run`);
|
||||
}
|
||||
|
||||
@@ -1015,12 +1042,31 @@ export class AgentManager {
|
||||
|
||||
let finalized = false;
|
||||
const finalize = (error?: string) => {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
error,
|
||||
alreadyFinalized: finalized,
|
||||
lifecycle: agent.lifecycle,
|
||||
hasPendingRun: Boolean(agent.pendingRun),
|
||||
},
|
||||
"streamAgent.finalize: invoked"
|
||||
);
|
||||
if (finalized) {
|
||||
return;
|
||||
}
|
||||
finalized = true;
|
||||
|
||||
if (agent.pendingRun !== streamForwarder) {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
error,
|
||||
lifecycle: agent.lifecycle,
|
||||
hasPendingRun: Boolean(agent.pendingRun),
|
||||
},
|
||||
"streamAgent.finalize: skipped because pendingRun no longer points to streamForwarder"
|
||||
);
|
||||
if (error) {
|
||||
agent.lastError = error;
|
||||
}
|
||||
@@ -1043,6 +1089,15 @@ export class AgentManager {
|
||||
mutableAgent.cwd
|
||||
);
|
||||
}
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
lifecycle: mutableAgent.lifecycle,
|
||||
hasPendingRun: Boolean(mutableAgent.pendingRun),
|
||||
terminalError,
|
||||
},
|
||||
"streamAgent.finalize: applying terminal state"
|
||||
);
|
||||
this.emitState(mutableAgent);
|
||||
this.flushLiveEventBacklog(mutableAgent);
|
||||
};
|
||||
@@ -1054,6 +1109,22 @@ export class AgentManager {
|
||||
try {
|
||||
for await (const event of iterator) {
|
||||
self.handleStreamEvent(agent, event);
|
||||
if (
|
||||
event.type === "turn_started" ||
|
||||
event.type === "turn_completed" ||
|
||||
event.type === "turn_failed" ||
|
||||
event.type === "turn_canceled"
|
||||
) {
|
||||
self.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
eventType: event.type,
|
||||
lifecycle: agent.lifecycle,
|
||||
hasPendingRun: Boolean(agent.pendingRun),
|
||||
},
|
||||
"streamAgent: forwarded terminal/turn event"
|
||||
);
|
||||
}
|
||||
yield event;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1079,6 +1150,14 @@ export class AgentManager {
|
||||
// deterministically order idle->running transitions.
|
||||
this.touchUpdatedAt(agent);
|
||||
self.emitState(agent);
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
lifecycle: agent.lifecycle,
|
||||
hasPendingRun: Boolean(agent.pendingRun),
|
||||
},
|
||||
"streamAgent: started"
|
||||
);
|
||||
|
||||
return streamForwarder;
|
||||
}
|
||||
@@ -1743,6 +1822,14 @@ export class AgentManager {
|
||||
}
|
||||
break;
|
||||
case "turn_completed":
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: agent.id,
|
||||
lifecycle: agent.lifecycle,
|
||||
hasPendingRun: Boolean(agent.pendingRun),
|
||||
},
|
||||
"handleStreamEvent: turn_completed"
|
||||
);
|
||||
agent.lastUsage = event.usage;
|
||||
agent.lastError = undefined;
|
||||
if (!agent.pendingRun && agent.lifecycle !== "idle") {
|
||||
@@ -1774,6 +1861,14 @@ export class AgentManager {
|
||||
this.emitState(agent);
|
||||
break;
|
||||
case "turn_canceled":
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: agent.id,
|
||||
lifecycle: agent.lifecycle,
|
||||
hasPendingRun: Boolean(agent.pendingRun),
|
||||
},
|
||||
"handleStreamEvent: turn_canceled"
|
||||
);
|
||||
if (!agent.pendingRun) {
|
||||
(agent as ActiveManagedAgent).lifecycle = "idle";
|
||||
}
|
||||
@@ -1792,6 +1887,14 @@ export class AgentManager {
|
||||
this.emitState(agent);
|
||||
break;
|
||||
case "turn_started":
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: agent.id,
|
||||
lifecycle: agent.lifecycle,
|
||||
hasPendingRun: Boolean(agent.pendingRun),
|
||||
},
|
||||
"handleStreamEvent: turn_started"
|
||||
);
|
||||
if (!agent.pendingRun) {
|
||||
(agent as ActiveManagedAgent).lifecycle = "running";
|
||||
this.emitState(agent);
|
||||
@@ -2143,6 +2246,14 @@ export class AgentManager {
|
||||
// Keep consuming provider events even during an active foreground run,
|
||||
// then replay them immediately once that run settles.
|
||||
if (latest.pendingRun) {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: latest.id,
|
||||
eventType: event.type,
|
||||
backlogSize: (this.liveEventBacklog.get(latest.id)?.length ?? 0) + 1,
|
||||
},
|
||||
"Live event pump: queued event because pendingRun is active"
|
||||
);
|
||||
this.enqueueLiveEvent(latest.id, event);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1906,6 +1906,16 @@ class ClaudeAgentSession implements AgentSession {
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
this.logger.trace(
|
||||
{
|
||||
claudeSessionId: this.claudeSessionId,
|
||||
turnState: this.turnState,
|
||||
hasQuery: Boolean(this.query),
|
||||
hasInput: Boolean(this.input),
|
||||
hasActiveForegroundTurn: Boolean(this.activeForegroundTurn),
|
||||
},
|
||||
"Claude session close: start"
|
||||
);
|
||||
this.closed = true;
|
||||
this.rejectAllPendingPermissions(new Error("Claude session closed"));
|
||||
this.cancelCurrentTurn?.();
|
||||
@@ -1922,6 +1932,10 @@ class ClaudeAgentSession implements AgentSession {
|
||||
await this.awaitWithTimeout(this.query?.return?.(), "close query return");
|
||||
this.query = null;
|
||||
this.input = null;
|
||||
this.logger.trace(
|
||||
{ claudeSessionId: this.claudeSessionId, turnState: this.turnState },
|
||||
"Claude session close: completed"
|
||||
);
|
||||
}
|
||||
|
||||
async listCommands(): Promise<AgentSlashCommand[]> {
|
||||
@@ -2235,8 +2249,11 @@ class ClaudeAgentSession implements AgentSession {
|
||||
label: string
|
||||
): Promise<void> {
|
||||
if (!promise) {
|
||||
this.logger.trace({ label }, "Claude query operation skipped (no promise)");
|
||||
return;
|
||||
}
|
||||
const startedAt = Date.now();
|
||||
this.logger.trace({ label }, "Claude query operation wait start");
|
||||
try {
|
||||
await Promise.race([
|
||||
promise,
|
||||
@@ -2244,6 +2261,10 @@ class ClaudeAgentSession implements AgentSession {
|
||||
setTimeout(() => reject(new Error("timeout")), 3_000);
|
||||
}),
|
||||
]);
|
||||
this.logger.trace(
|
||||
{ label, durationMs: Date.now() - startedAt },
|
||||
"Claude query operation settled"
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.warn({ err: error, label }, "Claude query operation did not settle cleanly");
|
||||
}
|
||||
@@ -2464,6 +2485,23 @@ class ClaudeAgentSession implements AgentSession {
|
||||
}
|
||||
|
||||
private emitRunEvent(run: RunRecord, event: AgentStreamEvent): void {
|
||||
if (
|
||||
event.type === "turn_started" ||
|
||||
event.type === "turn_completed" ||
|
||||
event.type === "turn_failed" ||
|
||||
event.type === "turn_canceled"
|
||||
) {
|
||||
this.logger.trace(
|
||||
{
|
||||
runId: run.id,
|
||||
owner: run.owner,
|
||||
runState: run.state,
|
||||
eventType: event.type,
|
||||
routedTo: run.owner === "foreground" && run.queue ? "foreground_queue" : "live_queue",
|
||||
},
|
||||
"Claude run event emitted"
|
||||
);
|
||||
}
|
||||
if (run.owner === "foreground" && run.queue) {
|
||||
run.queue.push(event);
|
||||
if (
|
||||
@@ -2494,6 +2532,16 @@ class ClaudeAgentSession implements AgentSession {
|
||||
this.activeForegroundTurn = null;
|
||||
this.preReplayMetadataSeen = false;
|
||||
}
|
||||
this.logger.trace(
|
||||
{
|
||||
runId: run.id,
|
||||
owner: run.owner,
|
||||
eventType: event.type,
|
||||
runState: run.state,
|
||||
hasActiveForegroundTurn: Boolean(this.activeForegroundTurn),
|
||||
},
|
||||
"Claude run terminal event handled"
|
||||
);
|
||||
this.transitionTurnStateFromActiveRuns(`run ${run.id} terminal`);
|
||||
}
|
||||
|
||||
@@ -2789,6 +2837,13 @@ class ClaudeAgentSession implements AgentSession {
|
||||
}
|
||||
|
||||
if (next.done) {
|
||||
this.logger.trace(
|
||||
{
|
||||
claudeSessionId: this.claudeSessionId,
|
||||
activeRunCount: this.runTracker.listActiveRuns().length,
|
||||
},
|
||||
"Claude query pump next() returned done"
|
||||
);
|
||||
this.input?.end();
|
||||
await this.awaitWithTimeout(q.return?.(), "query pump return on done");
|
||||
if (this.query === q) {
|
||||
@@ -2834,6 +2889,18 @@ class ClaudeAgentSession implements AgentSession {
|
||||
run: route.run,
|
||||
message,
|
||||
});
|
||||
this.logger.trace(
|
||||
{
|
||||
claudeSessionId: this.claudeSessionId,
|
||||
messageType: message.type,
|
||||
routeReason: route.reason,
|
||||
runId: route.run?.id ?? null,
|
||||
runOwner: route.run?.owner ?? null,
|
||||
suppressTerminalEvents,
|
||||
metadataOnly,
|
||||
},
|
||||
"Claude query pump routed SDK message"
|
||||
);
|
||||
if (route.run) {
|
||||
this.transitionTurnStateFromActiveRuns(`routed via ${route.reason}`);
|
||||
this.runTracker.bindIdentifiers(route.run, identifiers);
|
||||
@@ -2898,6 +2965,16 @@ class ClaudeAgentSession implements AgentSession {
|
||||
// Pre-replay success results are stale in practice (leftover from an
|
||||
// earlier query segment) and must not end the current foreground run.
|
||||
if (resultSubtype === "success") {
|
||||
this.logger.trace(
|
||||
{
|
||||
runId: run.id,
|
||||
runOwner: run.owner,
|
||||
runState: run.state,
|
||||
promptReplaySeen: run.promptReplaySeen,
|
||||
resultSubtype,
|
||||
},
|
||||
"Suppressing pre-replay foreground success result terminal event"
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2917,6 +2994,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
message: SDKMessage,
|
||||
identifiers: EventIdentifiers
|
||||
): void {
|
||||
const previousState = run.state;
|
||||
if (
|
||||
message.type === "user" &&
|
||||
identifiers.messageId &&
|
||||
@@ -2941,8 +3019,25 @@ class ClaudeAgentSession implements AgentSession {
|
||||
|
||||
if (message.type === "result") {
|
||||
this.runTracker.transition(run, "finalizing");
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (run.state !== previousState) {
|
||||
this.logger.trace(
|
||||
{
|
||||
runId: run.id,
|
||||
owner: run.owner,
|
||||
messageType: message.type,
|
||||
previousState,
|
||||
nextState: run.state,
|
||||
taskId: identifiers.taskId,
|
||||
parentMessageId: identifiers.parentMessageId,
|
||||
messageId: identifiers.messageId,
|
||||
},
|
||||
"Updated Claude run lifecycle from SDK message"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private shouldSuppressLocalReplayActivity(message: SDKMessage): boolean {
|
||||
|
||||
@@ -736,10 +736,15 @@ export class Session {
|
||||
private async interruptAgentIfRunning(agentId: string): Promise<void> {
|
||||
const snapshot = this.agentManager.getAgent(agentId)
|
||||
if (!snapshot) {
|
||||
this.sessionLogger.trace({ agentId }, 'interruptAgentIfRunning: agent not found')
|
||||
throw new Error(`Agent ${agentId} not found`)
|
||||
}
|
||||
|
||||
if (snapshot.lifecycle !== 'running' && !snapshot.pendingRun) {
|
||||
this.sessionLogger.trace(
|
||||
{ agentId, lifecycle: snapshot.lifecycle, pendingRun: Boolean(snapshot.pendingRun) },
|
||||
'interruptAgentIfRunning: skipping because agent is not running'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -787,9 +792,18 @@ export class Session {
|
||||
prompt: AgentPromptInput,
|
||||
runOptions?: AgentRunOptions
|
||||
): { ok: true } | { ok: false; error: string } {
|
||||
this.sessionLogger.trace(
|
||||
{
|
||||
agentId,
|
||||
promptType: typeof prompt === 'string' ? 'string' : 'structured',
|
||||
hasRunOptions: Boolean(runOptions),
|
||||
},
|
||||
'startAgentStream: requested'
|
||||
)
|
||||
let iterator: AsyncGenerator<AgentStreamEvent>
|
||||
try {
|
||||
iterator = this.agentManager.streamAgent(agentId, prompt, runOptions)
|
||||
this.sessionLogger.trace({ agentId }, 'startAgentStream: streamAgent returned iterator')
|
||||
} catch (error) {
|
||||
this.handleAgentRunError(agentId, error, 'Failed to start agent run')
|
||||
const message =
|
||||
@@ -802,7 +816,9 @@ export class Session {
|
||||
for await (const _ of iterator) {
|
||||
// Events are forwarded via the session's AgentManager subscription.
|
||||
}
|
||||
this.sessionLogger.trace({ agentId }, 'startAgentStream: iterator drained')
|
||||
} catch (error) {
|
||||
this.sessionLogger.trace({ agentId, err: error }, 'startAgentStream: iterator threw')
|
||||
this.handleAgentRunError(agentId, error, 'Agent stream failed')
|
||||
}
|
||||
})()
|
||||
|
||||
Reference in New Issue
Block a user