diff --git a/src/features/member-log-stream/core/domain/policies/__tests__/memberLogPreviewExtractor.test.ts b/src/features/member-log-stream/core/domain/policies/__tests__/memberLogPreviewExtractor.test.ts index 10d7b344..7501fd9a 100644 --- a/src/features/member-log-stream/core/domain/policies/__tests__/memberLogPreviewExtractor.test.ts +++ b/src/features/member-log-stream/core/domain/policies/__tests__/memberLogPreviewExtractor.test.ts @@ -1257,6 +1257,149 @@ Reply to this comment using MCP tool task_add_comment. preview: '2 processes - vite dev running; pnpm test exited', }); expect(processResult.items[0]?.preview).not.toContain('[{'); + + const taskUpdateResult = extractMemberLogPreviewItems({ + provider: 'opencode_runtime', + maxItems: 3, + textLimit: 160, + messages: [ + message({ + uuid: 'task-update-call', + timestamp: '2026-04-01T10:00:00.000Z', + content: [ + { + type: 'tool_use', + id: 'tool-task-update', + name: 'agent-teams_task_update', + input: { + taskId: 'abc12345-0000-0000-0000-000000000000', + status: 'in_progress', + }, + }, + ], + }), + message({ + uuid: 'task-update-result', + type: 'user', + role: 'user', + timestamp: '2026-04-01T10:01:00.000Z', + content: [ + { + type: 'tool_result', + tool_use_id: 'tool-task-update', + content: { + taskId: 'abc12345-0000-0000-0000-000000000000', + status: 'in_progress', + }, + }, + ], + }), + ], + }); + + expect(taskUpdateResult.items[0]).toMatchObject({ + kind: 'tool_result', + title: 'Task updated', + preview: '#abc12345 -> in_progress', + }); + + const remainingOperationalTools = [ + { + toolName: 'agent-teams_lead_briefing', + input: { teamName: 'relay-works-10' }, + result: 'Lead briefing for team relay-works-10. CRITICAL: hidden rules', + expectedTitle: 'Lead briefing', + expectedPreview: 'Loaded lead briefing for relay-works-10', + }, + { + toolName: 'agent-teams_runtime_deliver_message', + input: { to: 'bob', text: 'Follow-up ready', runtimeSessionId: 'ses-secret' }, + result: 'ok', + expectedTitle: 'Runtime delivery', + expectedPreview: 'Delivered to bob - Follow-up ready', + }, + { + toolName: 'agent-teams_cross_team_list_targets', + input: { teamName: 'relay-works-10' }, + result: JSON.stringify([{ teamName: 'qa-team' }, { name: 'design-team' }]), + expectedTitle: 'Cross-team targets', + expectedPreview: '2 teams - qa-team; design-team', + }, + { + toolName: 'agent-teams_cross_team_get_outbox', + input: { teamName: 'relay-works-10' }, + result: { + messages: [{ toTeam: 'qa-team', summary: 'Need smoke-test help' }], + }, + expectedTitle: 'Cross-team outbox', + expectedPreview: '1 message - to qa-team: Need smoke-test help', + }, + { + toolName: 'agent-teams_process_register', + input: { label: 'vite dev', command: 'pnpm dev' }, + result: { process: { label: 'vite dev', status: 'running' } }, + expectedTitle: 'Process registered', + expectedPreview: 'Registered vite dev running', + }, + { + toolName: 'agent-teams_process_stop', + input: { label: 'vite dev' }, + result: 'ok', + expectedTitle: 'Process stopped', + expectedPreview: 'Stopped vite dev', + }, + { + toolName: 'agent-teams_process_unregister', + input: { pid: 123 }, + result: 'ok', + expectedTitle: 'Process unregistered', + expectedPreview: 'Unregistered 123', + }, + ] as const; + + for (const [index, tool] of remainingOperationalTools.entries()) { + const result = extractMemberLogPreviewItems({ + provider: 'opencode_runtime', + maxItems: 3, + textLimit: 160, + messages: [ + message({ + uuid: `remaining-tool-call-${index}`, + timestamp: '2026-04-01T10:00:00.000Z', + content: [ + { + type: 'tool_use', + id: `tool-remaining-${index}`, + name: tool.toolName, + input: tool.input, + }, + ], + }), + message({ + uuid: `remaining-tool-result-${index}`, + type: 'user', + role: 'user', + timestamp: '2026-04-01T10:01:00.000Z', + content: [ + { + type: 'tool_result', + tool_use_id: `tool-remaining-${index}`, + content: tool.result, + }, + ], + }), + ], + }); + + expect(result.items[0]).toMatchObject({ + kind: 'tool_result', + title: tool.expectedTitle, + preview: tool.expectedPreview, + }); + expect(`${result.items[0]?.title ?? ''} ${result.items[0]?.preview ?? ''}`).not.toMatch( + /CRITICAL|runtimeSessionId|agent_teams_|process_register|cross_team_/i + ); + } }); it('uses concrete names for generic runtime tool results', () => { diff --git a/src/features/member-log-stream/core/domain/policies/memberLogPreviewExtractor.ts b/src/features/member-log-stream/core/domain/policies/memberLogPreviewExtractor.ts index 754f43fb..1f6e3381 100644 --- a/src/features/member-log-stream/core/domain/policies/memberLogPreviewExtractor.ts +++ b/src/features/member-log-stream/core/domain/policies/memberLogPreviewExtractor.ts @@ -136,20 +136,24 @@ function timestampIso(value: Date | string): string { function stripAngleTags(value: string): string { let result = ''; let insideTag = false; - for (const char of value) { - if (char === '<') { - insideTag = true; - result += ' '; + for (let index = 0; index < value.length; index += 1) { + const char = value[index]; + if (!insideTag && char === '<') { + const next = value[index + 1] ?? ''; + if (/[A-Za-z/!]/.test(next)) { + insideTag = true; + result += ' '; + continue; + } + } + if (insideTag) { + if (char === '>') { + insideTag = false; + result += ' '; + } continue; } - if (char === '>') { - insideTag = false; - result += ' '; - continue; - } - if (!insideTag) { - result += char; - } + result += char; } return result; } @@ -468,6 +472,8 @@ function formatToolTitle(toolName: string): string { const canonical = canonicalToolName(toolName); if (canonical === 'sendmessage' || canonical === 'message_send') return 'Send message'; if (canonical === 'cross_team_send') return 'Cross-team message'; + if (canonical === 'cross_team_list_targets') return 'List teams'; + if (canonical === 'cross_team_get_outbox') return 'Cross-team outbox'; if (canonical === 'runtime_deliver_message') return 'Runtime delivery'; if (canonical === 'runtime_task_event') return 'Runtime task event'; if (canonical === 'runtime_heartbeat') return 'Runtime heartbeat'; @@ -492,6 +498,7 @@ function formatToolTitle(toolName: string): string { if (canonical === 'review_approve') return 'Approve review'; if (canonical === 'review_request_changes') return 'Request changes'; if (canonical === 'runtime_bootstrap_checkin') return 'Runtime check-in'; + if (canonical === 'lead_briefing') return 'Lead briefing'; if (canonical === 'member_briefing') return 'Member briefing'; if (canonical === 'member_work_sync_status') return 'Work sync status'; if (canonical === 'member_work_sync_report') return 'Work sync report'; @@ -499,6 +506,9 @@ function formatToolTitle(toolName: string): string { if (canonical === 'task_update') return 'Update task'; if (canonical === 'task_delete') return 'Delete task'; if (canonical === 'process_list') return 'List processes'; + if (canonical === 'process_register') return 'Register process'; + if (canonical === 'process_stop') return 'Stop process'; + if (canonical === 'process_unregister') return 'Unregister process'; return humanizeFallbackToolName(toolName); } @@ -525,14 +535,16 @@ function isToolUseSupersededBySuccessResult(toolName: string): boolean { return ( canonical === 'sendmessage' || canonical === 'message_send' || - canonical === 'cross_team_send' || + canonical.startsWith('cross_team_') || canonical === 'runtime_deliver_message' || canonical === 'runtime_bootstrap_checkin' || canonical === 'runtime_heartbeat' || canonical === 'runtime_task_event' || + canonical === 'lead_briefing' || canonical === 'member_briefing' || canonical === 'member_work_sync_status' || canonical === 'member_work_sync_report' || + canonical.startsWith('process_') || canonical.startsWith('task_') || canonical.startsWith('review_') ); @@ -765,6 +777,172 @@ function formatProcessCollectionArrayPayload(items: readonly unknown[]): KnownPa }; } +function processLabelFromPayload( + payload: Record, + fallbackInput?: Record | null +): string | null { + const process = asRecord(payload.process) ?? asRecord(fallbackInput?.process) ?? undefined; + return ( + [ + stringField(payload, 'label'), + stringField(payload, 'name'), + stringField(payload, 'command'), + stringField(payload, 'processId'), + stringField(payload, 'id'), + stringifyPrimitive(payload.pid), + stringField(process, 'label'), + stringField(process, 'name'), + stringField(process, 'command'), + stringField(process, 'processId'), + stringField(process, 'id'), + stringifyPrimitive(process?.pid), + stringField(fallbackInput ?? undefined, 'label'), + stringField(fallbackInput ?? undefined, 'name'), + stringField(fallbackInput ?? undefined, 'command'), + stringField(fallbackInput ?? undefined, 'processId'), + stringField(fallbackInput ?? undefined, 'id'), + stringifyPrimitive(fallbackInput?.pid), + ].find((value) => typeof value === 'string' && value.trim().length > 0) ?? null + ); +} + +function formatProcessLifecyclePayload( + payload: Record, + canonicalToolNameValue: string | null, + fallbackInput?: Record | null +): KnownPayloadPreview | null { + const canonical = canonicalToolNameValue ?? ''; + if ( + canonical !== 'process_register' && + canonical !== 'process_stop' && + canonical !== 'process_unregister' + ) { + return null; + } + + const label = processLabelFromPayload(payload, fallbackInput); + const status = + stringField(payload, 'status') ?? + stringField(asRecord(payload.process), 'status') ?? + stringField(fallbackInput ?? undefined, 'status'); + + if (canonical === 'process_register') { + const suffix = status && label ? ` ${status}` : ''; + return { + title: 'Process registered', + text: label ? `Registered ${label}${suffix}` : 'Registered process', + }; + } + if (canonical === 'process_stop') { + return { + title: 'Process stopped', + text: label ? `Stopped ${label}` : 'Stopped process', + }; + } + return { + title: 'Process unregistered', + text: label ? `Unregistered ${label}` : 'Unregistered process', + }; +} + +function formatCrossTeamTargetItem(item: Record): string | null { + return ( + stringField(item, 'teamName') ?? + stringField(item, 'name') ?? + stringField(item, 'id') ?? + stringField(item, 'slug') + ); +} + +function formatCrossTeamOutboxItem(item: Record): string | null { + const target = + stringField(item, 'toTeam') ?? + stringField(item, 'targetTeam') ?? + stringField(item, 'teamName') ?? + stringField(item, 'target'); + const summary = + stringField(item, 'summary') ?? + stringField(item, 'text') ?? + stringField(item, 'message') ?? + stringField(item, 'content'); + if (target && summary) return `to ${target}: ${summary}`; + return target ?? summary; +} + +function formatCrossTeamCollectionArrayPayload( + items: readonly unknown[], + canonicalToolNameValue: string | null +): KnownPayloadPreview | null { + const canonical = canonicalToolNameValue ?? ''; + const title = + canonical === 'cross_team_list_targets' + ? 'Cross-team targets' + : canonical === 'cross_team_get_outbox' + ? 'Cross-team outbox' + : null; + if (!title) return null; + + const records = items + .map((item) => asRecord(item)) + .filter((item): item is Record => Boolean(item)); + const summaries = records + .slice(0, 3) + .map((item) => + canonical === 'cross_team_list_targets' + ? formatCrossTeamTargetItem(item) + : formatCrossTeamOutboxItem(item) + ) + .filter(Boolean); + const remainingCount = Math.max(0, records.length - summaries.length); + const moreText = remainingCount > 0 ? `; +${remainingCount} more` : ''; + + if (canonical === 'cross_team_list_targets') { + const countText = `${records.length} ${records.length === 1 ? 'team' : 'teams'}`; + return { + title, + text: summaries.length > 0 ? `${countText} - ${summaries.join('; ')}${moreText}` : countText, + }; + } + + const countText = `${records.length} ${records.length === 1 ? 'message' : 'messages'}`; + return { + title, + text: summaries.length > 0 ? `${countText} - ${summaries.join('; ')}${moreText}` : countText, + }; +} + +function formatCrossTeamCollectionPayload( + payload: Record, + canonicalToolNameValue: string | null +): KnownPayloadPreview | null { + const canonical = canonicalToolNameValue ?? ''; + if (canonical !== 'cross_team_list_targets' && canonical !== 'cross_team_get_outbox') { + return null; + } + + const rawItems = + (Array.isArray(payload.targets) ? payload.targets : null) ?? + (Array.isArray(payload.teams) ? payload.teams : null) ?? + (Array.isArray(payload.messages) ? payload.messages : null) ?? + (Array.isArray(payload.outbox) ? payload.outbox : null) ?? + (Array.isArray(payload.items) ? payload.items : null); + if (rawItems) { + return formatCrossTeamCollectionArrayPayload(rawItems, canonical); + } + + const summary = + stringField(payload, 'summary') ?? + stringField(payload, 'message') ?? + stringField(payload, 'text'); + if (summary) { + return { + title: canonical === 'cross_team_list_targets' ? 'Cross-team targets' : 'Cross-team outbox', + text: summary, + }; + } + return null; +} + function formatRelationshipPayload( payload: Record, fallbackInput?: Record | null @@ -959,6 +1137,26 @@ function formatRuntimePayload( text: memberName ? `${memberName} checked in` : 'Runtime checked in', }; } + if (canonical === 'runtime_deliver_message') { + const target = + stringField(payload, 'to') ?? + stringField(payload, 'target') ?? + stringField(fallbackInput ?? undefined, 'to') ?? + stringField(fallbackInput ?? undefined, 'target'); + const summary = + stringField(payload, 'summary') ?? + stringField(payload, 'message') ?? + stringField(payload, 'text') ?? + stringField(fallbackInput ?? undefined, 'summary') ?? + stringField(fallbackInput ?? undefined, 'message') ?? + stringField(fallbackInput ?? undefined, 'text'); + if (target && summary) { + return { title: 'Runtime delivery', text: `Delivered to ${target} - ${summary}` }; + } + if (target) return { title: 'Runtime delivery', text: `Delivered to ${target}` }; + if (summary) return { title: 'Runtime delivery', text: summary }; + return { title: 'Runtime delivery', text: 'Delivered runtime message' }; + } if (canonical === 'runtime_heartbeat') { return { title: 'Runtime heartbeat', @@ -985,6 +1183,14 @@ function formatRuntimePayload( text: memberName ? `Loaded briefing for ${memberName}` : 'Loaded member briefing', }; } + if (canonical === 'lead_briefing') { + const teamName = + stringField(payload, 'teamName') ?? stringField(fallbackInput ?? undefined, 'teamName'); + return { + title: 'Lead briefing', + text: teamName ? `Loaded lead briefing for ${teamName}` : 'Loaded lead briefing', + }; + } return null; } @@ -1125,14 +1331,25 @@ function formatPlainToolResultStatus( if (!toolContext) { return null; } - if (toolContext.canonicalName === 'member_briefing') { + if ( + toolContext.canonicalName === 'member_briefing' || + toolContext.canonicalName === 'lead_briefing' + ) { const memberMatch = /^member briefing for\s+([^\s]+)\s+on team\b/i.exec( compactWhitespace(value) ); + const teamMatch = /^lead briefing for team\s+([^\s.]+)/i.exec(compactWhitespace(value)); const memberName = memberMatch?.[1] ?? stringField(asRecord(toolContext.input), 'memberName') ?? stringField(asRecord(toolContext.input), 'member'); + if (toolContext.canonicalName === 'lead_briefing') { + const teamName = teamMatch?.[1] ?? stringField(asRecord(toolContext.input), 'teamName'); + return { + title: 'Lead briefing', + text: teamName ? `Loaded lead briefing for ${teamName}` : 'Loaded lead briefing', + }; + } return { title: 'Member briefing', text: memberName ? `Loaded briefing for ${memberName}` : 'Loaded member briefing', @@ -1151,12 +1368,30 @@ function formatPlainToolResultStatus( const text = fallbackInput ? formatCrossTeamPayload(fallbackInput) : null; return text ? { title: 'Cross-team message', text } : null; } + if (toolContext.canonicalName === 'cross_team_list_targets') { + const teamName = stringField(fallbackInput ?? undefined, 'teamName'); + return { + title: 'Cross-team targets', + text: teamName ? `Listed teams for ${teamName}` : 'Listed cross-team targets', + }; + } + if (toolContext.canonicalName === 'cross_team_get_outbox') { + const teamName = stringField(fallbackInput ?? undefined, 'teamName'); + return { + title: 'Cross-team outbox', + text: teamName ? `Loaded outbox for ${teamName}` : 'Loaded cross-team outbox', + }; + } if ( toolContext.canonicalName === 'member_work_sync_status' || toolContext.canonicalName === 'member_work_sync_report' ) { return formatWorkSyncPayload({}, toolContext.canonicalName, fallbackInput); } + const processText = formatProcessLifecyclePayload({}, toolContext.canonicalName, fallbackInput); + if (processText) { + return processText; + } return ( formatTaskToolPayload({}, toolContext.canonicalName, fallbackInput) ?? formatRuntimePayload({}, toolContext.canonicalName, fallbackInput) @@ -1268,6 +1503,14 @@ function formatKnownPayloadPreview( if (workSyncText) { return workSyncText; } + const processLifecycleText = formatProcessLifecyclePayload(payload, canonical, fallbackInput); + if (processLifecycleText) { + return processLifecycleText; + } + const crossTeamCollectionText = formatCrossTeamCollectionPayload(payload, canonical); + if (crossTeamCollectionText) { + return crossTeamCollectionText; + } if (canonical === 'process_list') { const processText = formatProcessCollectionPayload(payload); if (processText) { @@ -1340,6 +1583,16 @@ function previewUnknownValue( if (knownCollection) { return { ...truncatePreview(knownCollection.text, limit), title: knownCollection.title }; } + const crossTeamCollection = formatCrossTeamCollectionArrayPayload( + value, + toolContext?.canonicalName ?? null + ); + if (crossTeamCollection) { + return { + ...truncatePreview(crossTeamCollection.text, limit), + title: crossTeamCollection.title, + }; + } if (toolContext?.canonicalName === 'process_list') { const processCollection = formatProcessCollectionArrayPayload(value); return { ...truncatePreview(processCollection.text, limit), title: processCollection.title }; @@ -1396,6 +1649,19 @@ function previewToolInputValue(toolName: string, value: unknown, limit: number): return truncatePreview(formatted, limit); } } + if (canonical === 'cross_team_list_targets' || canonical === 'cross_team_get_outbox') { + const payload = recordFromUnknown(value); + const teamName = payload ? stringField(payload, 'teamName') : null; + const text = + canonical === 'cross_team_list_targets' + ? teamName + ? `for ${teamName}` + : 'List cross-team targets' + : teamName + ? `for ${teamName}` + : 'Read cross-team outbox'; + return truncatePreview(text, limit); + } const payload = recordFromUnknown(value); if (payload) { const runtimeFormatted = formatRuntimePayload(payload, canonical, payload); @@ -1406,6 +1672,10 @@ function previewToolInputValue(toolName: string, value: unknown, limit: number): if (workSyncFormatted) { return truncatePreview(workSyncFormatted.text, limit); } + const processFormatted = formatProcessLifecyclePayload(payload, canonical, payload); + if (processFormatted) { + return truncatePreview(processFormatted.text, limit); + } const taskFormatted = formatTaskToolInputPayload(canonical, payload); if (taskFormatted) { return truncatePreview(taskFormatted, limit);