diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx
index 15b5d78a3..15d919a17 100644
--- a/packages/app/src/components/agent-list.tsx
+++ b/packages/app/src/components/agent-list.tsx
@@ -98,6 +98,7 @@ function AgentListRow({
const timeAgo = formatTimeAgo(agent.lastActivityAt);
const agentKey = `${agent.serverId}:${agent.id}`;
const isSelected = selectedAgentId === agentKey;
+ const archivedLabel = agent.archivedAt ? "Archived" : null;
const checkoutQuery = useCheckoutStatusCacheOnly({
serverId: agent.serverId,
cwd: agent.cwd,
@@ -137,7 +138,8 @@ function AgentListRow({
{shortenPath(projectPath)}
- {branchLabel ? ` · ${branchLabel}` : ""} · {timeAgo}
+ {branchLabel ? ` · ${branchLabel}` : ""}
+ {archivedLabel ? ` · ${archivedLabel}` : ""} · {timeAgo}
)}
diff --git a/packages/app/src/components/sidebar-agent-list.tsx b/packages/app/src/components/sidebar-agent-list.tsx
index e1e13f3fc..3579b788a 100644
--- a/packages/app/src/components/sidebar-agent-list.tsx
+++ b/packages/app/src/components/sidebar-agent-list.tsx
@@ -1,4 +1,4 @@
-import { View, Text, Pressable, Image, Platform } from 'react-native'
+import { View, Text, Pressable, Image, Platform, Alert, ActivityIndicator, StatusBar } from 'react-native'
import { useQueries } from '@tanstack/react-query'
import {
useCallback,
@@ -11,8 +11,8 @@ import {
} from 'react'
import { router, usePathname, useSegments } from 'expo-router'
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
-import { type GestureType } from 'react-native-gesture-handler'
-import { ChevronDown, ChevronRight } from 'lucide-react-native'
+import { Gesture, GestureDetector, type GestureType } from 'react-native-gesture-handler'
+import { Archive, ChevronDown, ChevronRight } from 'lucide-react-native'
import { DraggableList, type DraggableRenderItemInfo } from './draggable-list'
import { getHostRuntimeStore, isHostRuntimeConnected } from '@/runtime/host-runtime'
import { projectIconQueryKey } from '@/hooks/use-project-icon-query'
@@ -27,8 +27,17 @@ import {
type SidebarWorkspaceEntry,
} from '@/hooks/use-sidebar-agents-list'
import { useSidebarOrderStore } from '@/stores/sidebar-order-store'
+import { useCheckoutGitActionsStore } from '@/stores/checkout-git-actions-store'
import { formatTimeAgo } from '@/utils/time'
import type { SidebarStateBucket } from '@/utils/sidebar-agent-state'
+import { confirmDialog } from '@/utils/confirm-dialog'
+import {
+ ContextMenu,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuTrigger,
+ useContextMenu,
+} from '@/components/ui/context-menu'
type SidebarTreeRow =
| {
@@ -76,7 +85,7 @@ interface WorkspaceRowProps {
workspace: SidebarWorkspaceEntry
selected: boolean
onPress: () => void
- onLongPress: () => void
+ drag: () => void
}
function deriveProjectDisplayName(input: { projectKey: string; projectName: string }): string {
@@ -135,10 +144,25 @@ function resolveStatusDotColor(input: { theme: ReturnType['
: theme.colors.border
}
-function WorkspaceStatusDot({ bucket }: { bucket: SidebarWorkspaceEntry['statusBucket'] }) {
+function WorkspaceStatusIndicator({
+ bucket,
+ loading = false,
+}: {
+ bucket: SidebarWorkspaceEntry['statusBucket']
+ loading?: boolean
+}) {
const { theme } = useUnistyles()
const color = resolveStatusDotColor({ theme, bucket })
- return
+
+ return (
+
+ {loading ? (
+
+ ) : (
+
+ )}
+
+ )
}
function ProjectRow({
@@ -201,10 +225,71 @@ function ProjectRow({
)
}
-function WorkspaceRow({ workspace, selected, onPress, onLongPress }: WorkspaceRowProps) {
- const didLongPressRef = useRef(false)
+function WorkspaceRow({ workspace, selected, onPress, drag }: WorkspaceRowProps) {
+ const { theme } = useUnistyles()
const createdAtLabel = resolveWorkspaceCreatedAtLabel(workspace)
+ const { setAnchorRect, setOpen } = useContextMenu()
+ const didLongPressRef = useRef(false)
+ const didLongPressCleanupTimerRef = useRef | null>(null)
+ const longPressArmedRef = useRef(false)
+ const longPressCancelledRef = useRef(false)
+ const didStartDragRef = useRef(false)
+ const touchStartRef = useRef<{ x: number; y: number } | null>(null)
+
+ const archiveStatus = useCheckoutGitActionsStore((state) =>
+ state.getStatus({
+ serverId: workspace.serverId,
+ cwd: workspace.cwd,
+ actionId: 'archive-worktree',
+ })
+ )
+ const runArchiveWorktree = useCheckoutGitActionsStore((state) => state.archiveWorktree)
+ const isArchiving = archiveStatus === 'pending'
+
+ const openContextMenuAtTouchStart = useCallback(() => {
+ const point = touchStartRef.current
+ if (!point) {
+ return
+ }
+ const statusBarHeight = Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0
+ setAnchorRect({
+ x: point.x,
+ y: point.y + statusBarHeight,
+ width: 0,
+ height: 0,
+ })
+ setOpen(true)
+ }, [setAnchorRect, setOpen])
+
+ const handleArchiveWorktree = useCallback(() => {
+ if (!workspace.isPaseoOwnedWorktree) {
+ return
+ }
+
+ void confirmDialog({
+ title: 'Archive worktree?',
+ message: `Archive this worktree?\n\n${workspace.cwd}`,
+ confirmLabel: 'Archive',
+ cancelLabel: 'Cancel',
+ destructive: true,
+ })
+ .then((confirmed) => {
+ if (!confirmed) {
+ return
+ }
+ return runArchiveWorktree({
+ serverId: workspace.serverId,
+ cwd: workspace.cwd,
+ worktreePath: workspace.cwd,
+ })
+ })
+ .catch((err) => {
+ const message = err instanceof Error ? err.message : 'Failed to archive worktree'
+ Alert.alert('Error', message)
+ })
+ }, [runArchiveWorktree, workspace.cwd, workspace.isPaseoOwnedWorktree, workspace.serverId])
+
const handlePress = useCallback(() => {
if (didLongPressRef.current) {
didLongPressRef.current = false
@@ -214,25 +299,115 @@ function WorkspaceRow({ workspace, selected, onPress, onLongPress }: WorkspaceRo
}, [onPress])
const handleLongPress = useCallback(() => {
+ if (longPressCancelledRef.current) {
+ return
+ }
didLongPressRef.current = true
- onLongPress()
- }, [onLongPress])
+ longPressArmedRef.current = true
+ }, [])
- return (
- {
+ if (Platform.OS === 'web') {
+ return null
+ }
+
+ const CANCEL_SLOP_PX = 10
+ const DRAG_SLOP_PX = 8
+
+ return Gesture.Pan()
+ .manualActivation(true)
+ .runOnJS(true)
+ .onTouchesDown((event) => {
+ const touch = event.changedTouches[0]
+ if (!touch) {
+ return
+ }
+ touchStartRef.current = { x: touch.absoluteX, y: touch.absoluteY }
+ })
+ .onTouchesMove((event, stateManager) => {
+ const touch = event.changedTouches[0]
+ if (!touch || event.numberOfTouches !== 1) {
+ stateManager.fail()
+ return
+ }
+
+ const start = touchStartRef.current
+ if (!start) {
+ touchStartRef.current = { x: touch.absoluteX, y: touch.absoluteY }
+ return
+ }
+
+ const dx = touch.absoluteX - start.x
+ const dy = touch.absoluteY - start.y
+ const distance = Math.sqrt(dx * dx + dy * dy)
+
+ if (!longPressArmedRef.current) {
+ if (distance > CANCEL_SLOP_PX) {
+ longPressCancelledRef.current = true
+ stateManager.fail()
+ }
+ return
+ }
+
+ if (didStartDragRef.current) {
+ return
+ }
+
+ if (distance > DRAG_SLOP_PX) {
+ didStartDragRef.current = true
+ drag()
+ stateManager.fail()
+ }
+ })
+ }, [drag])
+
+ const trigger = (
+ [
styles.workspaceRow,
selected && styles.workspaceRowSelected,
hovered && styles.workspaceRowHovered,
pressed && styles.workspaceRowPressed,
]}
+ onPressIn={(event) => {
+ if (didLongPressCleanupTimerRef.current) {
+ clearTimeout(didLongPressCleanupTimerRef.current)
+ didLongPressCleanupTimerRef.current = null
+ }
+ longPressCancelledRef.current = false
+ longPressArmedRef.current = false
+ didStartDragRef.current = false
+ touchStartRef.current = { x: event.nativeEvent.pageX, y: event.nativeEvent.pageY }
+ }}
+ onPressOut={() => {
+ if (Platform.OS === 'web') {
+ return
+ }
+ if (!longPressArmedRef.current || didStartDragRef.current) {
+ longPressCancelledRef.current = false
+ longPressArmedRef.current = false
+ didStartDragRef.current = false
+ touchStartRef.current = null
+ return
+ }
+ openContextMenuAtTouchStart()
+ didLongPressCleanupTimerRef.current = setTimeout(() => {
+ didLongPressRef.current = false
+ didLongPressCleanupTimerRef.current = null
+ }, 0)
+ longPressCancelledRef.current = false
+ longPressArmedRef.current = false
+ didStartDragRef.current = false
+ touchStartRef.current = null
+ }}
onPress={handlePress}
onLongPress={handleLongPress}
delayLongPress={200}
testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
>
-
+
{resolveWorkspaceBranchLabel(workspace)}
@@ -242,7 +417,54 @@ function WorkspaceRow({ workspace, selected, onPress, onLongPress }: WorkspaceRo
{createdAtLabel}
) : null}
-
+
+ )
+
+ return (
+ <>
+ {moveMonitorGesture ? (
+ {trigger}
+ ) : (
+ trigger
+ )}
+
+
+ }
+ destructive
+ disabled={!workspace.isPaseoOwnedWorktree}
+ status={workspace.isPaseoOwnedWorktree ? archiveStatus : 'idle'}
+ pendingLabel="Archiving…"
+ successLabel="Archived"
+ onSelect={handleArchiveWorktree}
+ testID={`sidebar-workspace-context-${workspace.workspaceKey}-archive`}
+ >
+ Archive worktree
+
+
+ >
+ )
+}
+
+function WorkspaceRowWithMenu({
+ workspace,
+ selected,
+ onPress,
+ drag,
+}: {
+ workspace: SidebarWorkspaceEntry
+ selected: boolean
+ onPress: () => void
+ drag: () => void
+}) {
+ return (
+
+
+
)
}
@@ -452,7 +674,7 @@ export function SidebarAgentList({
activeWorkspaceSelection.workspaceId === item.workspace.cwd
return (
- {
@@ -462,7 +684,7 @@ export function SidebarAgentList({
onWorkspacePress?.()
navigate(workspaceRoute as any)
}}
- onLongPress={drag}
+ drag={drag}
/>
)
},
@@ -671,6 +893,14 @@ const styles = StyleSheet.create((theme) => ({
height: 8,
borderRadius: theme.borderRadius.full,
flexShrink: 0,
+ alignItems: 'center',
+ justifyContent: 'center',
+ overflow: 'hidden',
+ },
+ workspaceStatusDotFill: {
+ width: 8,
+ height: 8,
+ borderRadius: theme.borderRadius.full,
},
workspaceBranchText: {
color: theme.colors.foreground,
diff --git a/packages/app/src/components/ui/context-menu.tsx b/packages/app/src/components/ui/context-menu.tsx
index eea75d193..ce5326396 100644
--- a/packages/app/src/components/ui/context-menu.tsx
+++ b/packages/app/src/components/ui/context-menu.tsx
@@ -58,6 +58,10 @@ function useContextMenuContext(componentName: string): ContextMenuContextValue {
return ctx;
}
+export function useContextMenu(): ContextMenuContextValue {
+ return useContextMenuContext("useContextMenu");
+}
+
function useControllableOpenState({
open,
defaultOpen,
diff --git a/packages/app/src/hooks/use-aggregated-agents.ts b/packages/app/src/hooks/use-aggregated-agents.ts
index f7b4a170a..30324af3b 100644
--- a/packages/app/src/hooks/use-aggregated-agents.ts
+++ b/packages/app/src/hooks/use-aggregated-agents.ts
@@ -55,6 +55,9 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
}
const serverLabel = serverLabelById.get(serverId) ?? serverId;
for (const agent of agents.values()) {
+ if (agent.archivedAt) {
+ continue;
+ }
const nextAgent: AggregatedAgent = {
id: agent.id,
serverId,
diff --git a/packages/app/src/hooks/use-all-agents-list.ts b/packages/app/src/hooks/use-all-agents-list.ts
index c9508a244..319b889cd 100644
--- a/packages/app/src/hooks/use-all-agents-list.ts
+++ b/packages/app/src/hooks/use-all-agents-list.ts
@@ -74,9 +74,6 @@ export function useAllAgentsList(options?: {
serverId,
serverLabel,
});
- if (aggregated.archivedAt) {
- continue;
- }
if (aggregated.labels.ui !== "true") {
continue;
}
diff --git a/packages/app/src/runtime/host-runtime.ts b/packages/app/src/runtime/host-runtime.ts
index 2f3ac7eb6..252b012d0 100644
--- a/packages/app/src/runtime/host-runtime.ts
+++ b/packages/app/src/runtime/host-runtime.ts
@@ -1113,7 +1113,7 @@ export class HostRuntimeStore {
controller.markAgentDirectorySyncLoading();
try {
const payload = await client.fetchAgents({
- filter: input.filter ?? { labels: { ui: "true" } },
+ filter: input.filter ?? { labels: { ui: "true" }, includeArchived: true },
...(input.subscribe ? { subscribe: input.subscribe } : {}),
...(input.page ? { page: input.page } : {}),
});
diff --git a/packages/app/src/stores/checkout-git-actions-store.ts b/packages/app/src/stores/checkout-git-actions-store.ts
index 7e9cd85ed..7db0c3084 100644
--- a/packages/app/src/stores/checkout-git-actions-store.ts
+++ b/packages/app/src/stores/checkout-git-actions-store.ts
@@ -86,6 +86,49 @@ function invalidateWorktreeList() {
predicate: (query) =>
Array.isArray(query.queryKey) && query.queryKey[0] === "paseoWorktreeList",
});
+ void queryClient.invalidateQueries({
+ predicate: (query) =>
+ Array.isArray(query.queryKey) && query.queryKey[0] === "sidebarPaseoWorktreeList",
+ });
+}
+
+function removeWorktreeFromCachedLists(input: {
+ serverId: string;
+ worktreePath: string;
+}): void {
+ const serverId = input.serverId.trim();
+ const worktreePath = input.worktreePath.trim();
+ if (!serverId || !worktreePath) {
+ return;
+ }
+
+ const removeFromList = (current: unknown) => {
+ if (!Array.isArray(current)) {
+ return current;
+ }
+ const filtered = current.filter((entry) => entry?.worktreePath !== worktreePath);
+ return filtered.length === current.length ? current : filtered;
+ };
+
+ queryClient.setQueriesData(
+ {
+ predicate: (query) =>
+ Array.isArray(query.queryKey) &&
+ query.queryKey[0] === "paseoWorktreeList" &&
+ query.queryKey[1] === serverId,
+ },
+ removeFromList
+ );
+
+ queryClient.setQueriesData(
+ {
+ predicate: (query) =>
+ Array.isArray(query.queryKey) &&
+ query.queryKey[0] === "sidebarPaseoWorktreeList" &&
+ query.queryKey[1] === serverId,
+ },
+ removeFromList
+ );
}
const successTimers = new Map>();
@@ -267,6 +310,7 @@ export const useCheckoutGitActionsStore = create()
if (payload.error) {
throw new Error(payload.error.message);
}
+ removeWorktreeFromCachedLists({ serverId, worktreePath });
invalidateWorktreeList();
},
});
diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts
index 0a38a41c3..faa08dfe2 100644
--- a/packages/server/src/server/agent/agent-manager.ts
+++ b/packages/server/src/server/agent/agent-manager.ts
@@ -801,6 +801,15 @@ export class AgentManager {
async closeAgent(agentId: string): Promise {
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 {
@@ -1005,7 +1015,24 @@ export class AgentManager {
options?: AgentRunOptions
): AsyncGenerator {
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;
}
diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts
index d26d3ba1d..65745249e 100644
--- a/packages/server/src/server/agent/providers/claude-agent.ts
+++ b/packages/server/src/server/agent/providers/claude-agent.ts
@@ -1906,6 +1906,16 @@ class ClaudeAgentSession implements AgentSession {
}
async close(): Promise {
+ 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 {
@@ -2235,8 +2249,11 @@ class ClaudeAgentSession implements AgentSession {
label: string
): Promise {
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 {
diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts
index 813a9a79e..c18132166 100644
--- a/packages/server/src/server/session.ts
+++ b/packages/server/src/server/session.ts
@@ -736,10 +736,15 @@ export class Session {
private async interruptAgentIfRunning(agentId: string): Promise {
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
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')
}
})()
diff --git a/scripts/dev.sh b/scripts/dev.sh
index 1b435a0f4..07137fa00 100755
--- a/scripts/dev.sh
+++ b/scripts/dev.sh
@@ -10,8 +10,11 @@ DAEMON_PORT=$(get-port 6767 6768 6769 6770 6771 6772 6773)
METRO_PORT=$(get-port 8081 8082 8083 8084 8085 8086 8087)
# Use a temporary PASEO_HOME to avoid conflicts between dev instances
-# export PASEO_HOME=$(mktemp -d "${TMPDIR:-/tmp}/paseo-dev.XXXXXX")
-# trap "rm -rf '$PASEO_HOME'" EXIT
+if [ -z "${PASEO_HOME}" ]; then
+ export PASEO_HOME
+ PASEO_HOME="$(mktemp -d "${TMPDIR:-/tmp}/paseo-dev.XXXXXX")"
+ trap "rm -rf '$PASEO_HOME'" EXIT
+fi
# Build CORS origins for this Expo instance
CORS_ORIGINS="http://localhost:${METRO_PORT},http://127.0.0.1:${METRO_PORT}"
@@ -24,7 +27,7 @@ echo " Paseo Dev"
echo "══════════════════════════════════════════════════════"
echo " Daemon: http://localhost:${DAEMON_PORT}"
echo " Metro: http://localhost:${METRO_PORT}"
-# echo " Home: ${PASEO_HOME}"
+echo " Home: ${PASEO_HOME}"
echo "══════════════════════════════════════════════════════"
# Export for child processes (overrides .env values)