feat: add worktree archiving with context menu and improved logging

This commit is contained in:
Mohamed Boudra
2026-03-03 17:35:48 +07:00
parent f418f3ab02
commit 888af29485
11 changed files with 529 additions and 24 deletions
@@ -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 {
+16
View File
@@ -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')
}
})()