From 8db30f1f128b80e23326a2ee8ea629d003165635 Mon Sep 17 00:00:00 2001 From: Luciano Castillo <2213102+luccast@users.noreply.github.com> Date: Thu, 29 Jan 2026 21:22:47 -0500 Subject: [PATCH] fix(monitor): improve subagent spawn inference and minimap sizing (#22) * refactor(clawdbot): update spawn inference logic - Replaced parent session activity tracking with a pending spawns array to record Task tool calls. - Adjusted the spawn inference window to 10 seconds, ensuring accurate tracking of recent Task calls. - Enhanced the inferSpawnedBy function to utilize the new pending spawns structure for improved session mapping. - Removed obsolete parent session activity tracking code for cleaner implementation. * fix(clawdbot): normalize tool name handling in action tracking - Updated the action tracking logic to convert tool names to lowercase for consistent comparison, ensuring accurate detection of 'Task' tool calls in both addAction and hydrateFromServer functions. - Enhanced event parsing to process tool events more effectively, improving overall event handling and session state management. * refactor(clawdbot): enhance spawn inference with parent session activity tracking - Introduced a new Map to track recent parent session activity, providing a fallback for spawn inference when Task tool calls are not available. - Updated the recordPendingSpawn and inferSpawnedBy functions to utilize this new tracking mechanism, improving the accuracy of session mapping. - Enhanced the addAction and hydrateFromServer functions to incorporate parent session activity tracking, ensuring comprehensive spawn history management. - Cleared parent session activity during collection resets to maintain data integrity. * refactor(clawdbot): improve parent session action tracking for spawn inference - Replaced the previous parent session activity tracking with a new structure to maintain a history of recent actions, allowing for more precise spawn inference. - Updated the trackParentAction function to log actions with timestamps and manage a maximum history length. - Modified the inferSpawnedBy function to utilize the new action history for better accuracy in linking subagents to their parent sessions. - Adjusted the addAction and hydrateFromServer functions to reflect the new tracking mechanism, ensuring comprehensive session mapping. - Cleared the action history during collection resets to maintain data integrity. * style(monitor): adjust ActionGraph component dimensions - Updated the ActionGraphInner component to include width and height styles, enhancing the layout and visual consistency of the graph representation. --- src/components/monitor/ActionGraph.tsx | 2 +- src/integrations/clawdbot/collections.ts | 96 ++++++++++++++---------- src/integrations/clawdbot/parser.ts | 12 +++ 3 files changed, 70 insertions(+), 40 deletions(-) diff --git a/src/components/monitor/ActionGraph.tsx b/src/components/monitor/ActionGraph.tsx index 0652594..3e26b30 100644 --- a/src/components/monitor/ActionGraph.tsx +++ b/src/components/monitor/ActionGraph.tsx @@ -791,7 +791,7 @@ function ActionGraphInner({ }} maskColor="rgba(10, 10, 15, 0.8)" className="bg-shell-900! border-shell-700!" - style={{ backgroundColor: '#0a0a0f' }} + style={{ backgroundColor: '#0a0a0f', width: 100, height: 75 }} /> diff --git a/src/integrations/clawdbot/collections.ts b/src/integrations/clawdbot/collections.ts index 84f0a25..6ed7ca4 100644 --- a/src/integrations/clawdbot/collections.ts +++ b/src/integrations/clawdbot/collections.ts @@ -12,12 +12,13 @@ import { // Track runId → sessionKey mapping (learned from chat events) const runSessionMap = new Map() -// Track recent activity on parent (non-subagent) sessions for spawn inference -// Maps sessionKey → lastActivityTimestamp -const parentSessionActivity = new Map() +// Track recent parent session actions with precise timestamps +// Stores the last few action timestamps per parent session +const parentActionHistory = new Map() +const MAX_ACTION_HISTORY = 10 -// Time window for spawn inference - parent must have been active within this window -const SPAWN_INFERENCE_WINDOW_MS = 5000 +// Time window for spawn inference +const SPAWN_INFERENCE_WINDOW_MS = 10000 function isSubagentSession(key: string): boolean { return key.includes('subagent') @@ -27,31 +28,58 @@ function isParentSession(key: string): boolean { return !isSubagentSession(key) && !key.includes('lifecycle') } -// Infer which parent session spawned this subagent based on recent activity +// Track an action on a parent session with its timestamp +function trackParentAction(sessionKey: string, timestamp?: number) { + if (!isParentSession(sessionKey)) return + const ts = timestamp ?? Date.now() + + let history = parentActionHistory.get(sessionKey) + if (!history) { + history = [] + parentActionHistory.set(sessionKey, history) + } + + history.push(ts) + + // Keep only recent entries + if (history.length > MAX_ACTION_HISTORY) { + history.shift() + } +} + +// Infer which parent session spawned this subagent +// Finds the parent with the most recent action before the subagent's timestamp function inferSpawnedBy(subagentKey: string, timestamp?: number): string | undefined { if (!isSubagentSession(subagentKey)) return undefined - const now = timestamp ?? Date.now() + const subagentTime = timestamp ?? Date.now() + const cutoff = subagentTime - SPAWN_INFERENCE_WINDOW_MS + let bestParent: string | undefined let bestTime = 0 - for (const [parentKey, activityTime] of parentSessionActivity) { - // Must be within inference window - if (now - activityTime > SPAWN_INFERENCE_WINDOW_MS) continue - // Pick most recently active parent - if (activityTime > bestTime) { - bestTime = activityTime - bestParent = parentKey + for (const [parentKey, history] of parentActionHistory) { + // Find the most recent action from this parent that's before the subagent time + for (let i = history.length - 1; i >= 0; i--) { + const actionTime = history[i]! + // Must be before subagent appeared and within window + if (actionTime <= subagentTime && actionTime >= cutoff) { + if (actionTime > bestTime) { + bestTime = actionTime + bestParent = parentKey + } + break // Found the most recent valid action for this parent + } } } - return bestParent -} + if (bestParent) { + console.log(`[spawn] linked ${subagentKey} to ${bestParent} (action ${subagentTime - bestTime}ms before)`) + } else { + console.log(`[spawn] could not infer parent for ${subagentKey}`) + } -// Track activity on a parent session -function trackParentActivity(sessionKey: string, timestamp?: number) { - if (!isParentSession(sessionKey)) return - parentSessionActivity.set(sessionKey, timestamp ?? Date.now()) + return bestParent } export const sessionsCollection = createCollection( @@ -164,11 +192,6 @@ function createPlaceholderExec(event: MonitorExecEvent, sessionKey?: string): Mo // Helper to update or insert session export function upsertSession(session: MonitorSession) { - // Track activity on parent sessions - if (isParentSession(session.key)) { - trackParentActivity(session.key, session.lastActivityAt) - } - const existing = sessionsCollection.state.get(session.key) if (existing) { @@ -207,9 +230,9 @@ export function addAction(action: MonitorAction) { backfillExecSessionKey(action.runId, action.sessionKey) } - // Track activity on parent sessions for spawn inference + // Track parent session actions for spawn inference if (isParentSession(action.sessionKey)) { - trackParentActivity(action.sessionKey, action.timestamp) + trackParentAction(action.sessionKey, action.timestamp) } } @@ -370,12 +393,6 @@ export function updateSessionStatus( status: MonitorSession['status'] ) { const now = Date.now() - - // Track activity on parent sessions - if (isParentSession(key)) { - trackParentActivity(key, now) - } - const session = sessionsCollection.state.get(key) if (session) { sessionsCollection.update(key, (draft) => { @@ -412,7 +429,7 @@ export function updateSession(key: string, update: Partial) { // Clear all data export function clearCollections() { runSessionMap.clear() - parentSessionActivity.clear() + parentActionHistory.clear() for (const session of sessionsCollection.state.values()) { sessionsCollection.delete(session.key) } @@ -478,23 +495,24 @@ export function hydrateFromServer( // First clear existing data clearCollections() - // Replay actions first to build parent activity history + // Sort actions by timestamp for replay const sortedActions = [...actions].sort((a, b) => a.timestamp - b.timestamp) + + // First pass: build parent action history for spawn inference for (const action of sortedActions) { - // Track parent activity without inserting actions yet if (action.sessionKey && isParentSession(action.sessionKey)) { - trackParentActivity(action.sessionKey, action.timestamp) + trackParentAction(action.sessionKey, action.timestamp) } } // Also track parent sessions by their lastActivityAt for (const session of sessions) { if (isParentSession(session.key)) { - trackParentActivity(session.key, session.lastActivityAt) + trackParentAction(session.key, session.lastActivityAt) } } - // Now insert all sessions - subagents will get inferred spawnedBy + // Insert all sessions - subagents will get inferred spawnedBy from Task tool calls for (const session of sessions) { if (isSubagentSession(session.key)) { const spawnedBy = session.spawnedBy || inferSpawnedBy(session.key, session.lastActivityAt) diff --git a/src/integrations/clawdbot/parser.ts b/src/integrations/clawdbot/parser.ts index 4fd333d..c138b4a 100644 --- a/src/integrations/clawdbot/parser.ts +++ b/src/integrations/clawdbot/parser.ts @@ -203,6 +203,18 @@ export function parseEventFrame( } } + // Process tool events (tool_use, tool_result) + if (agentEvent.data?.type === 'tool_use' || agentEvent.data?.type === 'tool_result') { + return { + action: agentEventToAction(agentEvent), + session: agentEvent.sessionKey ? { + key: agentEvent.sessionKey, + status: 'thinking', + lastActivityAt: Date.now(), + } : undefined, + } + } + return null }