fix(opencode): harden runtime projection delivery

This commit is contained in:
777genius 2026-04-25 00:41:27 +03:00
parent dec0eaba18
commit 2f37be4bd0
13 changed files with 3631 additions and 37 deletions

View file

@ -15,8 +15,13 @@ export const agentBlocks = controllerModule.agentBlocks;
export function getController(teamName: string, claudeDir?: string) {
const forcedClaudeDir = process.env[FORCED_CLAUDE_DIR_ENV]?.trim();
let resolvedClaudeDir = claudeDir;
if (forcedClaudeDir) {
resolvedClaudeDir = forcedClaudeDir;
}
return createController({
teamName,
...(forcedClaudeDir ? { claudeDir: forcedClaudeDir } : claudeDir ? { claudeDir } : {}),
...(resolvedClaudeDir ? { claudeDir: resolvedClaudeDir } : {}),
});
}

View file

@ -6,6 +6,9 @@ import {
createLegacyRuntimeFallbackCliExtensionCapabilities,
} from '@shared/utils/providerExtensionCapabilities';
import { filterVisibleProviderRuntimeModels } from '@shared/utils/providerModelVisibility';
import { mkdtemp, readFile, rm } from 'fs/promises';
import { tmpdir } from 'os';
import path from 'path';
import { resolveGeminiRuntimeAuth } from './geminiRuntimeAuth';
import { buildProviderAwareCliEnv } from './providerAwareCliEnv';
@ -843,17 +846,26 @@ export class ClaudeMultimodelBridgeService {
params.teamId,
'--member',
params.memberName,
'--projection-only',
];
if (typeof params.limit === 'number') {
args.push('--limit', String(params.limit));
}
const { stdout } = await execCli(binaryPath, args, {
timeout: PROVIDER_STATUS_TIMEOUT_MS,
env,
});
const parsed = extractJsonObject<OpenCodeRuntimeTranscriptResponse>(stdout);
return parsed.providerId === 'opencode' ? (parsed.transcript ?? null) : null;
const outputDir = await mkdtemp(path.join(tmpdir(), 'opencode-transcript-'));
const outputPath = path.join(outputDir, 'transcript.json');
try {
await execCli(binaryPath, [...args, '--output', outputPath], {
timeout: PROVIDER_STATUS_TIMEOUT_MS,
env,
});
const parsed = extractJsonObject<OpenCodeRuntimeTranscriptResponse>(
await readFile(outputPath, 'utf8')
);
return parsed.providerId === 'opencode' ? (parsed.transcript ?? null) : null;
} finally {
await rm(outputDir, { recursive: true, force: true }).catch(() => undefined);
}
}
private async verifyOpenCodeModel(

View file

@ -290,6 +290,7 @@ import type {
PersistedTeamLaunchSnapshot,
PersistedTeamLaunchSummary,
ProviderModelLaunchIdentity,
TaskRef,
TeamAgentRuntimeBackendType,
TeamAgentRuntimeDiagnosticSeverity,
TeamAgentRuntimeEntry,
@ -314,7 +315,6 @@ import type {
TeamProvisioningState,
TeamRuntimeState,
TeamTask,
TaskRef,
ToolActivityEventPayload,
ToolApprovalAutoResolved,
ToolApprovalEvent,
@ -2035,6 +2035,12 @@ function normalizeTeamMemberProviderId(providerId: unknown): TeamProviderId | un
return normalizeOptionalTeamProviderId(providerId);
}
function normalizeTeamProviderLike(providerId: unknown): TeamProviderId | undefined {
return normalizeOptionalTeamProviderId(
typeof providerId === 'string' ? providerId.trim().toLowerCase() : providerId
);
}
function buildEffectiveTeamMemberSpec(
member: TeamMemberInput,
defaults: {
@ -4508,15 +4514,11 @@ export class TeamProvisioningService {
);
const configProvider = (configMember as { provider?: unknown } | undefined)?.provider;
const metaProvider = (metaMember as { provider?: unknown } | undefined)?.provider;
const normalizeProviderLike = (value: unknown) =>
normalizeOptionalTeamProviderId(
typeof value === 'string' ? value.trim().toLowerCase() : value
);
const providerId =
normalizeProviderLike(metaMember?.providerId) ??
normalizeProviderLike(metaProvider) ??
normalizeProviderLike(configMember?.providerId) ??
normalizeProviderLike(configProvider) ??
normalizeTeamProviderLike(metaMember?.providerId) ??
normalizeTeamProviderLike(metaProvider) ??
normalizeTeamProviderLike(configMember?.providerId) ??
normalizeTeamProviderLike(configProvider) ??
inferTeamProviderIdFromModel(metaMember?.model ?? configMember?.model);
return providerId === 'opencode';
}
@ -4551,15 +4553,11 @@ export class TeamProvisioningService {
);
const configProvider = (configMember as { provider?: unknown } | undefined)?.provider;
const metaProvider = (metaMember as { provider?: unknown } | undefined)?.provider;
const normalizeProviderLike = (value: unknown) =>
normalizeOptionalTeamProviderId(
typeof value === 'string' ? value.trim().toLowerCase() : value
);
const providerId =
normalizeProviderLike(metaMember?.providerId) ??
normalizeProviderLike(metaProvider) ??
normalizeProviderLike(configMember?.providerId) ??
normalizeProviderLike(configProvider) ??
normalizeTeamProviderLike(metaMember?.providerId) ??
normalizeTeamProviderLike(metaProvider) ??
normalizeTeamProviderLike(configMember?.providerId) ??
normalizeTeamProviderLike(configProvider) ??
inferTeamProviderIdFromModel(metaMember?.model ?? configMember?.model);
if (providerId !== 'opencode') {
return { delivered: false, reason: 'recipient_is_not_opencode' };
@ -4605,17 +4603,25 @@ export class TeamProvisioningService {
const trackedRunId = this.resolveDeliverableTrackedRuntimeRunId(teamName);
const trackedRun = trackedRunId ? this.runs.get(trackedRunId) : null;
let liveSecondaryLaneRunId: string | null = null;
let trackedSecondaryLanePresent = false;
let trackedSecondaryLaneSnapshotKnown = false;
if (
trackedRun &&
laneIdentity.laneKind === 'secondary' &&
laneIdentity.laneOwnerProviderId === 'opencode'
) {
const liveLane = trackedRun.mixedSecondaryLanes.find(
const secondaryLanes = trackedRun.mixedSecondaryLanes;
trackedSecondaryLaneSnapshotKnown = secondaryLanes.length > 0;
const liveLane = secondaryLanes.find(
(lane) =>
lane.laneId === laneIdentity.laneId ||
lane.member.name.trim().toLowerCase() === normalizedMemberName.toLowerCase()
);
liveSecondaryLaneRunId = liveLane?.runId?.trim() || null;
trackedSecondaryLanePresent = liveLane != null;
liveSecondaryLaneRunId = liveLane ? trackedRunId : null;
if (!liveLane && trackedSecondaryLaneSnapshotKnown) {
return { delivered: false, reason: 'opencode_runtime_not_active' };
}
}
const runtimeRunId =
laneIdentity.laneKind === 'secondary' && laneIdentity.laneOwnerProviderId === 'opencode'
@ -4623,7 +4629,20 @@ export class TeamProvisioningService {
(await this.resolveCurrentOpenCodeRuntimeRunId(teamName, laneIdentity.laneId)))
: (trackedRunId ??
(await this.resolveCurrentOpenCodeRuntimeRunId(teamName, laneIdentity.laneId)));
if (!runtimeRunId) {
let runtimeActive = Boolean(runtimeRunId);
if (!runtimeActive) {
if (
trackedRun &&
laneIdentity.laneKind === 'secondary' &&
laneIdentity.laneOwnerProviderId === 'opencode' &&
!trackedSecondaryLanePresent &&
trackedSecondaryLaneSnapshotKnown
) {
return { delivered: false, reason: 'opencode_runtime_not_active' };
}
runtimeActive = await this.isOpenCodeRuntimeLaneIndexActive(teamName, laneIdentity.laneId);
}
if (!runtimeActive) {
return { delivered: false, reason: 'opencode_runtime_not_active' };
}
@ -4840,6 +4859,16 @@ export class TeamProvisioningService {
return durableRunId || null;
}
private async isOpenCodeRuntimeLaneIndexActive(
teamName: string,
laneId: string
): Promise<boolean> {
const laneIndex = await readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName).catch(
() => null
);
return laneIndex?.lanes[laneId]?.state === 'active';
}
private async resolveOpenCodeRuntimeLaneId(params: {
teamName: string;
runId: string;

View file

@ -1,7 +1,5 @@
import { randomUUID } from 'crypto';
import type { AgentActionMode, TaskRef } from '@shared/types/team';
import type {
OpenCodeBridgeRuntimeSnapshot,
OpenCodeLaunchTeamCommandBody,
@ -26,6 +24,7 @@ import type {
TeamRuntimeStopInput,
TeamRuntimeStopResult,
} from './TeamRuntimeAdapter';
import type { AgentActionMode, TaskRef } from '@shared/types/team';
export interface OpenCodeTeamRuntimeBridgePort {
checkOpenCodeTeamLaunchReadiness(input: {

View file

@ -2,6 +2,7 @@ import { extractToolCalls, extractToolResults } from '@main/utils/toolExtraction
import { isLeadMember as isLeadMemberCheck } from '@shared/utils/leadDetection';
import { getTaskDisplayId } from '@shared/utils/taskIdentity';
import { canonicalizeAgentTeamsToolName } from '../../agentTeamsToolNames';
import { TeamTaskReader } from '../../TeamTaskReader';
import { BoardTaskActivityRecordSource } from '../activity/BoardTaskActivityRecordSource';
import { TeamTranscriptSourceLocator } from '../discovery/TeamTranscriptSourceLocator';
@ -11,7 +12,6 @@ import { BoardTaskExactLogStrictParser } from '../exact/BoardTaskExactLogStrictP
import { BoardTaskExactLogSummarySelector } from '../exact/BoardTaskExactLogSummarySelector';
import { isBoardTaskExactLogsReadEnabled } from '../exact/featureGates';
import { getBoardTaskExactLogFileVersions } from '../exact/fileVersions';
import { canonicalizeAgentTeamsToolName } from '../../agentTeamsToolNames';
import { OpenCodeTaskLogStreamSource } from './OpenCodeTaskLogStreamSource';

View file

@ -324,6 +324,55 @@ function collectTaskMarkerCalls(
});
}
function markerInputReferencesTaskInDifferentExplicitTeam(
input: unknown,
teamName: string,
taskRefs: Set<string>
): boolean {
if (taskRefs.size === 0) {
return false;
}
const normalizedTeamName = normalizeTaskRef(teamName);
const explicitTeamRefs = collectExplicitRefsForKeys(input, TEAM_REFERENCE_KEYS);
if (
!normalizedTeamName ||
explicitTeamRefs.size === 0 ||
explicitTeamRefs.has(normalizedTeamName)
) {
return false;
}
const explicitTaskRefs = collectExplicitRefsForKeys(input, TASK_REFERENCE_KEYS);
return explicitTaskRefs.size > 0
? refsIntersect(explicitTaskRefs, taskRefs)
: valueReferencesTask(input, taskRefs);
}
function hasForeignTeamTaskMarker(
projectedMessages: OpenCodeRuntimeTranscriptLogMessage[],
teamName: string,
task: TeamTask
): boolean {
const taskRefs = buildTaskRefSet(task);
if (taskRefs.size === 0) {
return false;
}
return projectedMessages
.map(toParsedMessage)
.filter((message): message is ParsedMessage => message !== null)
.some((message) =>
message.toolCalls.some((toolCall) => {
const toolName = canonicalizeAgentTeamsToolName(toolCall.name ?? '').toLowerCase();
return (
TASK_LOG_MARKER_TOOL_NAMES.has(toolName) &&
markerInputReferencesTaskInDifferentExplicitTeam(toolCall.input, teamName, taskRefs)
);
})
);
}
function isTerminalTaskMarkerCall(markerCall: TaskMarkerCall): boolean {
if (TERMINAL_TASK_MARKER_TOOL_NAMES.has(markerCall.toolName)) {
return true;
@ -923,6 +972,9 @@ export class OpenCodeTaskLogStreamSource {
}
const markerProjection = buildTaskMarkerProjection(projectedMessages, teamName, task);
if (!markerProjection && hasForeignTeamTaskMarker(projectedMessages, teamName, task)) {
return null;
}
const timeWindows = markerProjection ? [] : buildTaskTimeWindows(task);
const projectionReason: HeuristicFallbackReason = markerProjection
? 'task_tool_markers'

View file

@ -19,8 +19,8 @@ import {
hasMemberLaunchDiagnosticsError,
} from '@renderer/utils/memberLaunchDiagnostics';
import { getRuntimeMemorySourceLabel } from '@renderer/utils/memberRuntimeSummary';
import { deriveTaskDisplayId } from '@shared/utils/taskIdentity';
import { isLeadMember } from '@shared/utils/leadDetection';
import { deriveTaskDisplayId } from '@shared/utils/taskIdentity';
import {
AlertTriangle,
Ban,

View file

@ -11,6 +11,7 @@
* Backward compatibility:
* - legacy fenced blocks: ```info_for_agent ... ```
* - legacy xml-like blocks: <agent-block> ... </agent-block>
* - OpenCode runtime-only delivery blocks
*/
export const AGENT_BLOCK_TAG = 'info_for_agent';
export const AGENT_BLOCK_OPEN = `<${AGENT_BLOCK_TAG}>`;
@ -22,7 +23,11 @@ export const AGENT_BLOCK_CLOSE = `</${AGENT_BLOCK_TAG}>`;
const CURRENT_AGENT_BLOCK_PATTERN = '\\n?<info_for_agent>\\n?[\\s\\S]*?\\n?<\\/info_for_agent>\\n?';
const LEGACY_FENCED_AGENT_BLOCK_PATTERN = '\\n?```' + AGENT_BLOCK_TAG + '\\n[\\s\\S]*?\\n```\\n?';
const LEGACY_XML_AGENT_BLOCK_PATTERN = '\\n?<agent-block>\\n?[\\s\\S]*?\\n?<\\/agent-block>\\n?';
const AGENT_BLOCK_PATTERN = `(?:${CURRENT_AGENT_BLOCK_PATTERN}|${LEGACY_FENCED_AGENT_BLOCK_PATTERN}|${LEGACY_XML_AGENT_BLOCK_PATTERN})`;
const OPENCODE_RUNTIME_IDENTITY_BLOCK_PATTERN =
'\\n?<opencode_runtime_identity>\\n?[\\s\\S]*?\\n?<\\/opencode_runtime_identity>\\n?';
const OPENCODE_APP_MESSAGE_DELIVERY_BLOCK_PATTERN =
'\\n?<opencode_app_message_delivery>\\n?[\\s\\S]*?\\n?<\\/opencode_app_message_delivery>\\n?';
const AGENT_BLOCK_PATTERN = `(?:${CURRENT_AGENT_BLOCK_PATTERN}|${LEGACY_FENCED_AGENT_BLOCK_PATTERN}|${LEGACY_XML_AGENT_BLOCK_PATTERN}|${OPENCODE_RUNTIME_IDENTITY_BLOCK_PATTERN}|${OPENCODE_APP_MESSAGE_DELIVERY_BLOCK_PATTERN})`;
/**
* Creates a new RegExp for matching agent blocks.
@ -63,6 +68,18 @@ export function unwrapAgentBlock(block: string): string {
return trimmed.slice(legacyXmlOpen.length, -legacyXmlClose.length).trim();
}
const opencodeRuntimeOpen = '<opencode_runtime_identity>';
const opencodeRuntimeClose = '</opencode_runtime_identity>';
if (trimmed.startsWith(opencodeRuntimeOpen) && trimmed.endsWith(opencodeRuntimeClose)) {
return trimmed.slice(opencodeRuntimeOpen.length, -opencodeRuntimeClose.length).trim();
}
const opencodeDeliveryOpen = '<opencode_app_message_delivery>';
const opencodeDeliveryClose = '</opencode_app_message_delivery>';
if (trimmed.startsWith(opencodeDeliveryOpen) && trimmed.endsWith(opencodeDeliveryClose)) {
return trimmed.slice(opencodeDeliveryOpen.length, -opencodeDeliveryClose.length).trim();
}
return trimmed;
}

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,6 @@
// @vitest-environment node
import type { PathLike } from 'fs';
import { readFile as readFileFixture, writeFile } from 'fs/promises';
import * as path from 'path';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
@ -810,15 +811,20 @@ describe('ClaudeMultimodelBridgeService', () => {
});
it('loads projected OpenCode transcript data through the runtime transcript command', async () => {
execCliMock.mockImplementation((_binaryPath, args) => {
execCliMock.mockImplementation(async (_binaryPath, args) => {
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
if (
normalizedArgs
=== 'runtime transcript --json --provider opencode --team team-a --member alice --limit 20'
normalizedArgs.startsWith(
'runtime transcript --json --provider opencode --team team-a --member alice --projection-only --limit 20 --output '
)
) {
return Promise.resolve({
stdout: JSON.stringify({
const outputIndex = Array.isArray(args) ? args.indexOf('--output') : -1;
const outputPath =
outputIndex >= 0 && Array.isArray(args) ? String(args[outputIndex + 1] ?? '') : '';
await writeFile(
outputPath,
JSON.stringify({
schemaVersion: 1,
providerId: 'opencode',
transcript: {
@ -856,6 +862,10 @@ describe('ClaudeMultimodelBridgeService', () => {
},
},
}),
'utf8'
);
return Promise.resolve({
stdout: '',
stderr: '',
exitCode: 0,
});
@ -896,6 +906,71 @@ describe('ClaudeMultimodelBridgeService', () => {
});
});
it('loads a large real OpenCode projection fixture through output-file transcript delivery', async () => {
const fixturePath = path.resolve(
process.cwd(),
'test/fixtures/team/opencode/relay-works-10-jack-projection-transcript.json'
);
const fixtureRaw = await readFileFixture(fixturePath, 'utf8');
execCliMock.mockImplementation(async (_binaryPath, args) => {
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
if (
normalizedArgs.startsWith(
'runtime transcript --json --provider opencode --team relay-works-10 --member jack --projection-only --limit 200 --output '
)
) {
const outputIndex = Array.isArray(args) ? args.indexOf('--output') : -1;
const outputPath =
outputIndex >= 0 && Array.isArray(args) ? String(args[outputIndex + 1] ?? '') : '';
await writeFile(outputPath, fixtureRaw, 'utf8');
return Promise.resolve({
stdout: '',
stderr: '',
exitCode: 0,
});
}
return Promise.reject(new Error(`Unexpected execCli call: ${normalizedArgs}`));
});
const { ClaudeMultimodelBridgeService } =
await import('@main/services/runtime/ClaudeMultimodelBridgeService');
const service = new ClaudeMultimodelBridgeService();
const transcript = await service.getOpenCodeTranscript('/mock/agent_teams_orchestrator', {
teamId: 'relay-works-10',
memberName: 'jack',
limit: 200,
});
const projectedMessages = transcript?.logProjection?.messages ?? [];
const toolNames = projectedMessages.flatMap((message) =>
message.toolCalls.map((toolCall) => toolCall.name)
);
expect(fixtureRaw.length).toBeGreaterThan(64_000);
expect(transcript?.sessionId).toBe('ses_23edf9243ffeSNYPWObDloBJyQ');
expect(transcript?.messageCount).toBe(65);
expect(transcript?.toolCallCount).toBe(36);
expect(transcript?.messages).toEqual([]);
expect(projectedMessages).toHaveLength(101);
expect(toolNames).toEqual(
expect.arrayContaining([
'agent-teams_runtime_bootstrap_checkin',
'agent-teams_member_briefing',
'agent-teams_message_send',
'agent-teams_task_start',
'agent-teams_task_add_comment',
'agent-teams_task_complete',
'bash',
'read',
])
);
expect(toolNames).not.toContain('SendMessage');
});
it('verifies OpenCode models through execution-grade runtime probes', async () => {
execCliMock.mockImplementation((_binaryPath, args) => {
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';

View file

@ -0,0 +1,373 @@
// @vitest-environment node
import { readFile } from 'fs/promises';
import path from 'path';
import { describe, expect, it, vi } from 'vitest';
import { OpenCodeTaskLogStreamSource } from '../../../../src/main/services/team/taskLogs/stream/OpenCodeTaskLogStreamSource';
import { BoardTaskExactLogChunkBuilder } from '../../../../src/main/services/team/taskLogs/exact/BoardTaskExactLogChunkBuilder';
import type { OpenCodeRuntimeTranscriptResponse } from '../../../../src/main/services/runtime/ClaudeMultimodelBridgeService';
import type { OpenCodeTaskLogAttributionRecord } from '../../../../src/main/services/team/taskLogs/stream/OpenCodeTaskLogAttributionStore';
import type { ParsedMessage } from '../../../../src/main/types';
import type { BoardTaskLogStreamResponse, TeamTask } from '../../../../src/shared/types';
const FIXTURE_PATH = path.resolve(
process.cwd(),
'test/fixtures/team/opencode/relay-works-10-jack-projection-transcript.json'
);
const RELAY_WORKS_10_TASK: TeamTask = {
id: '0b3a0624-5d66-4067-848e-5a74a1720c0d',
displayId: '0b3a0624',
subject: 'Define calculator arithmetic behavior',
owner: 'jack',
status: 'completed',
createdAt: '2026-04-24T20:29:03.133Z',
updatedAt: '2026-04-24T20:29:34.157Z',
workIntervals: [
{
startedAt: '2026-04-24T20:29:03.133Z',
completedAt: '2026-04-24T20:29:34.157Z',
},
],
};
async function loadFixtureTranscript(): Promise<
NonNullable<OpenCodeRuntimeTranscriptResponse['transcript']>
> {
const raw = await readFile(FIXTURE_PATH, 'utf8');
const parsed = JSON.parse(raw) as OpenCodeRuntimeTranscriptResponse;
if (parsed.providerId !== 'opencode' || !parsed.transcript) {
throw new Error('Invalid OpenCode transcript fixture');
}
return parsed.transcript;
}
function flattenRawMessages(response: BoardTaskLogStreamResponse): ParsedMessage[] {
return response.segments.flatMap((segment) =>
segment.chunks.flatMap((chunk) => chunk.rawMessages)
);
}
function serializeContent(message: ParsedMessage): string {
return typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
}
function serializeProjectedContent(
transcript: NonNullable<OpenCodeRuntimeTranscriptResponse['transcript']>
): string {
return JSON.stringify(transcript.logProjection?.messages ?? []);
}
function markerTaskIds(messages: ParsedMessage[], markerNames: Set<string>): Set<string> {
const taskIds = new Set<string>();
for (const message of messages) {
for (const toolCall of message.toolCalls) {
if (!markerNames.has(toolCall.name)) {
continue;
}
const input = toolCall.input;
if (input && typeof input === 'object' && !Array.isArray(input)) {
const taskId = (input as Record<string, unknown>).taskId;
if (typeof taskId === 'string') {
taskIds.add(taskId);
}
}
}
}
return taskIds;
}
function createSource(params: {
transcript: NonNullable<OpenCodeRuntimeTranscriptResponse['transcript']>;
activeTasks?: TeamTask[];
deletedTasks?: TeamTask[];
attributionRecords?: OpenCodeTaskLogAttributionRecord[];
}) {
const bridge = {
getOpenCodeTranscript: vi.fn(async () => params.transcript),
};
const taskReader = {
getTasks: vi.fn(async () => params.activeTasks ?? [RELAY_WORKS_10_TASK]),
getDeletedTasks: vi.fn(async () => params.deletedTasks ?? []),
};
const attributionStore = {
readTaskRecords: vi.fn(async () => params.attributionRecords ?? []),
};
const source = new OpenCodeTaskLogStreamSource(
bridge as never,
{ resolve: async () => '/tmp/agent_teams_orchestrator' },
taskReader as never,
new BoardTaskExactLogChunkBuilder(),
attributionStore
);
return { source, bridge, taskReader, attributionStore };
}
describe('OpenCodeTaskLogStreamSource real OpenCode fixture e2e', () => {
it('builds a task log stream from real OpenCode MCP task markers without leaking unrelated tasks', async () => {
const transcript = await loadFixtureTranscript();
const { source, bridge } = createSource({ transcript });
const response = await source.getTaskLogStream('relay-works-10', RELAY_WORKS_10_TASK.id);
expect(response).not.toBeNull();
expect(response?.source).toBe('opencode_runtime_fallback');
expect(response?.runtimeProjection).toMatchObject({
provider: 'opencode',
mode: 'heuristic',
attributionRecordCount: 0,
fallbackReason: 'task_tool_markers',
});
expect(response?.runtimeProjection?.projectedMessageCount).toBeGreaterThanOrEqual(10);
expect(response?.runtimeProjection?.markerMatchCount).toBeGreaterThanOrEqual(4);
expect(response?.runtimeProjection?.markerSpanCount).toBe(1);
expect(response?.participants).toEqual([
{
key: 'member:jack',
label: 'jack',
role: 'member',
isLead: false,
isSidechain: true,
},
]);
expect(response?.segments).toHaveLength(1);
const rawMessages = flattenRawMessages(response as BoardTaskLogStreamResponse);
const toolNames = rawMessages.flatMap((message) =>
message.toolCalls.map((toolCall) => toolCall.name)
);
const serialized = rawMessages.map(serializeContent).join('\n');
expect(toolNames).toEqual(
expect.arrayContaining([
'agent-teams_task_start',
'agent-teams_task_add_comment',
'agent-teams_task_complete',
])
);
expect(toolNames).not.toContain('SendMessage');
expect(serialized).toContain('Calculator behavior: digits 0-9 append to display');
expect(serialized).toContain('Noted');
expect(serialized).toContain('Confirmed');
expect(serialized).not.toContain('Keyboard handlers added');
expect(serialized).not.toContain('Logic smoke check');
expect(serialized).not.toContain('#00000000');
expect(
markerTaskIds(
rawMessages,
new Set([
'agent-teams_task_start',
'agent-teams_task_add_comment',
'agent-teams_task_complete',
])
)
).toEqual(new Set([RELAY_WORKS_10_TASK.id]));
expect(bridge.getOpenCodeTranscript).toHaveBeenCalledWith('/tmp/agent_teams_orchestrator', {
teamId: 'relay-works-10',
memberName: 'jack',
limit: 200,
});
});
it('uses real attribution UUID bounds before heuristic fallback', async () => {
const transcript = await loadFixtureTranscript();
const { source, bridge, attributionStore } = createSource({
transcript,
attributionRecords: [
{
taskId: RELAY_WORKS_10_TASK.id,
memberName: 'jack',
scope: 'member_session_window',
sessionId: 'ses_23edf9243ffeSNYPWObDloBJyQ',
startMessageUuid: 'msg_dc12eb246001iUrCHiLxsvZ3mN',
endMessageUuid: 'msg_dc12ed5ec001OIh5Bh9emN2Utj',
source: 'launch_runtime',
},
],
});
const response = await source.getTaskLogStream('relay-works-10', RELAY_WORKS_10_TASK.id);
expect(response?.source).toBe('opencode_runtime_attribution');
expect(response?.runtimeProjection).toEqual({
provider: 'opencode',
mode: 'attribution',
attributionRecordCount: 1,
projectedMessageCount: 10,
});
expect(response?.defaultFilter).toBe('member:jack');
expect(response?.segments).toHaveLength(1);
const rawMessages = flattenRawMessages(response as BoardTaskLogStreamResponse);
const toolNames = rawMessages.flatMap((message) =>
message.toolCalls.map((toolCall) => toolCall.name)
);
const serialized = rawMessages.map(serializeContent).join('\n');
expect(rawMessages.map((message) => message.uuid)).toEqual([
'msg_dc12eb246001iUrCHiLxsvZ3mN',
'msg_dc12eb261001b8MzfjP5WZGwA1',
'msg_dc12eb261001b8MzfjP5WZGwA1::tool_results',
'msg_dc12ebe27001UFPOASv4SiAr51',
'msg_dc12ebe27001UFPOASv4SiAr51::tool_results',
'msg_dc12ec768001m7G1qMVTexxl2s',
'msg_dc12ec768001m7G1qMVTexxl2s::tool_results',
'msg_dc12ece54001bDAaT7Rt1m6OmN',
'msg_dc12ece54001bDAaT7Rt1m6OmN::tool_results',
'msg_dc12ed5ec001OIh5Bh9emN2Utj',
]);
expect(toolNames).toEqual(
expect.arrayContaining([
'agent-teams_task_start',
'agent-teams_task_add_comment',
'agent-teams_task_complete',
'agent-teams_message_send',
])
);
expect(serialized).toContain('Calculator behavior: digits 0-9 append to display');
expect(serialized).toContain('Задача #0b3a0624 завершена');
expect(serialized).not.toContain('Noted');
expect(serialized).not.toContain('Keyboard handlers added');
expect(attributionStore.readTaskRecords).toHaveBeenCalledWith(
'relay-works-10',
RELAY_WORKS_10_TASK.id
);
expect(bridge.getOpenCodeTranscript).toHaveBeenCalledWith('/tmp/agent_teams_orchestrator', {
teamId: 'relay-works-10',
memberName: 'jack',
limit: 500,
});
});
it('can recover a deleted task stream from real OpenCode markers', async () => {
const transcript = await loadFixtureTranscript();
const deletedTask = {
...RELAY_WORKS_10_TASK,
status: 'deleted',
} satisfies TeamTask;
const { source } = createSource({
transcript,
activeTasks: [],
deletedTasks: [deletedTask],
});
const response = await source.getTaskLogStream('relay-works-10', deletedTask.id);
expect(response?.source).toBe('opencode_runtime_fallback');
expect(response?.participants[0]?.label).toBe('jack');
expect(response?.runtimeProjection?.fallbackReason).toBe('task_tool_markers');
expect(flattenRawMessages(response as BoardTaskLogStreamResponse).length).toBeGreaterThan(0);
});
it('does not leak a real OpenCode task stream across explicit team boundaries', async () => {
const transcript = await loadFixtureTranscript();
const { source, bridge } = createSource({ transcript });
const response = await source.getTaskLogStream('other-team', RELAY_WORKS_10_TASK.id);
expect(response).toBeNull();
expect(bridge.getOpenCodeTranscript).toHaveBeenCalledWith('/tmp/agent_teams_orchestrator', {
teamId: 'other-team',
memberName: 'jack',
limit: 200,
});
});
it('falls back to real marker projection when stale attribution does not match the session', async () => {
const transcript = await loadFixtureTranscript();
const { source, bridge } = createSource({
transcript,
attributionRecords: [
{
taskId: RELAY_WORKS_10_TASK.id,
memberName: 'jack',
scope: 'task_session',
sessionId: 'stale-session-id',
startMessageUuid: 'msg_dc12eb246001iUrCHiLxsvZ3mN',
endMessageUuid: 'msg_dc12ed5ec001OIh5Bh9emN2Utj',
source: 'reconcile',
},
],
});
const response = await source.getTaskLogStream('relay-works-10', RELAY_WORKS_10_TASK.id);
expect(response?.source).toBe('opencode_runtime_fallback');
expect(response?.runtimeProjection).toMatchObject({
provider: 'opencode',
mode: 'heuristic',
attributionRecordCount: 1,
fallbackReason: 'task_tool_markers',
});
expect(bridge.getOpenCodeTranscript).toHaveBeenNthCalledWith(
1,
'/tmp/agent_teams_orchestrator',
{
teamId: 'relay-works-10',
memberName: 'jack',
limit: 500,
}
);
expect(bridge.getOpenCodeTranscript).toHaveBeenNthCalledWith(
2,
'/tmp/agent_teams_orchestrator',
{
teamId: 'relay-works-10',
memberName: 'jack',
limit: 200,
}
);
});
it('captures the OpenCode runtime identity and MCP messaging contract from the real fixture', async () => {
const transcript = await loadFixtureTranscript();
const serialized = serializeProjectedContent(transcript);
const toolNames = (transcript.logProjection?.messages ?? []).flatMap((message) =>
message.toolCalls.map((toolCall) => toolCall.name)
);
expect(serialized).toContain('<opencode_runtime_identity>');
expect(serialized).toContain('runtimeProvider');
expect(serialized).toContain('opencode');
expect(serialized).toContain('agent-teams_runtime_bootstrap_checkin');
expect(serialized).toContain('agent-teams_member_briefing');
expect(serialized).toContain('agent-teams_message_send');
expect(serialized).toContain('Do not use SendMessage');
expect(serialized).toContain('Do not use runtime_deliver_message for ordinary visible replies');
expect(toolNames).toEqual(
expect.arrayContaining([
'agent-teams_runtime_bootstrap_checkin',
'agent-teams_member_briefing',
'agent-teams_message_send',
])
);
expect(toolNames).not.toContain('SendMessage');
expect(toolNames).not.toContain('runtime_deliver_message');
});
it('keeps real OpenCode projected tool results bounded and linked to assistant tool calls', async () => {
const transcript = await loadFixtureTranscript();
const projectedMessages = transcript.logProjection?.messages ?? [];
const assistantToolIds = new Set(
projectedMessages.flatMap((message) => message.toolCalls.map((toolCall) => toolCall.id))
);
const toolResultMessages = projectedMessages.filter((message) => message.toolResults.length > 0);
expect(projectedMessages).toHaveLength(101);
expect(toolResultMessages.length).toBeGreaterThan(20);
for (const message of toolResultMessages) {
expect(message.isMeta).toBe(true);
expect(message.sourceToolAssistantUUID).toBeTruthy();
for (const toolResult of message.toolResults) {
expect(assistantToolIds.has(toolResult.toolUseId)).toBe(true);
const serializedContent =
typeof toolResult.content === 'string'
? toolResult.content
: JSON.stringify(toolResult.content);
expect(serializedContent.length).toBeLessThanOrEqual(8_200);
}
}
});
});

View file

@ -0,0 +1,141 @@
import { readFile } from 'fs/promises';
import path from 'path';
import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { OpenCodeTaskLogStreamSource } from '../../../../../src/main/services/team/taskLogs/stream/OpenCodeTaskLogStreamSource';
import { BoardTaskExactLogChunkBuilder } from '../../../../../src/main/services/team/taskLogs/exact/BoardTaskExactLogChunkBuilder';
import { TooltipProvider } from '../../../../../src/renderer/components/ui/tooltip';
import type { OpenCodeRuntimeTranscriptResponse } from '../../../../../src/main/services/runtime/ClaudeMultimodelBridgeService';
import type { BoardTaskLogStreamResponse, TeamTask } from '../../../../../src/shared/types';
const TEAM_NAME = 'relay-works-10';
const TASK_ID = '0b3a0624-5d66-4067-848e-5a74a1720c0d';
const FIXTURE_PATH = path.resolve(
process.cwd(),
'test/fixtures/team/opencode/relay-works-10-jack-projection-transcript.json'
);
const apiState = {
getTaskLogStream: vi.fn<
(teamName: string, taskId: string) => Promise<BoardTaskLogStreamResponse>
>(),
onTeamChange: vi.fn<(callback: (event: unknown, data: unknown) => void) => () => void>(),
};
vi.mock('@renderer/api', () => ({
api: {
teams: {
getTaskLogStream: (...args: Parameters<typeof apiState.getTaskLogStream>) =>
apiState.getTaskLogStream(...args),
onTeamChange: (...args: Parameters<typeof apiState.onTeamChange>) =>
apiState.onTeamChange(...args),
},
},
}));
import { TaskLogStreamSection } from '@renderer/components/team/taskLogs/TaskLogStreamSection';
const RELAY_WORKS_10_TASK: TeamTask = {
id: TASK_ID,
displayId: '0b3a0624',
subject: 'Define calculator arithmetic behavior',
owner: 'jack',
status: 'completed',
createdAt: '2026-04-24T20:29:03.133Z',
updatedAt: '2026-04-24T20:29:34.157Z',
workIntervals: [
{
startedAt: '2026-04-24T20:29:03.133Z',
completedAt: '2026-04-24T20:29:34.157Z',
},
],
};
async function loadFixtureTranscript(): Promise<
NonNullable<OpenCodeRuntimeTranscriptResponse['transcript']>
> {
const parsed = JSON.parse(await readFile(FIXTURE_PATH, 'utf8')) as OpenCodeRuntimeTranscriptResponse;
if (parsed.providerId !== 'opencode' || !parsed.transcript) {
throw new Error('Invalid OpenCode transcript fixture');
}
return parsed.transcript;
}
async function buildFixtureStream(): Promise<BoardTaskLogStreamResponse> {
const transcript = await loadFixtureTranscript();
const source = new OpenCodeTaskLogStreamSource(
{
getOpenCodeTranscript: vi.fn(async () => transcript),
} as never,
{ resolve: async () => '/tmp/agent_teams_orchestrator' },
{
getTasks: vi.fn(async () => [RELAY_WORKS_10_TASK]),
getDeletedTasks: vi.fn(async () => []),
} as never,
new BoardTaskExactLogChunkBuilder(),
{ readTaskRecords: vi.fn(async () => []) }
);
const stream = await source.getTaskLogStream(TEAM_NAME, TASK_ID);
if (!stream) {
throw new Error('Expected OpenCode fixture stream');
}
return stream;
}
function flushMicrotasks(): Promise<void> {
return Promise.resolve();
}
describe('TaskLogStreamSection OpenCode real fixture e2e', () => {
afterEach(() => {
document.body.innerHTML = '';
apiState.getTaskLogStream.mockReset();
apiState.onTeamChange.mockReset();
vi.unstubAllGlobals();
});
it('renders real OpenCode task activity through the UI log stream', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
apiState.onTeamChange.mockImplementation(() => () => undefined);
apiState.getTaskLogStream.mockResolvedValueOnce(await buildFixtureStream());
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(
React.createElement(
TooltipProvider,
null,
React.createElement(TaskLogStreamSection, {
teamName: TEAM_NAME,
taskId: TASK_ID,
liveEnabled: false,
})
)
);
await flushMicrotasks();
await flushMicrotasks();
});
const text = host.textContent ?? '';
expect(text).toContain('Task Log Stream');
expect(text).toContain('matched task tool markers');
expect(text).toContain('Agent');
expect(text).toContain('Calculator behavior');
expect(text).toContain('Задача #0b3a0624 завершена');
expect(text).not.toContain('Keyboard handlers added');
expect(text).not.toContain('Logic smoke check');
expect(text).not.toContain('#00000000');
expect(text).not.toContain('SendMessage');
await act(async () => {
root.unmount();
await flushMicrotasks();
});
});
});

View file

@ -29,4 +29,22 @@ describe('agentBlocks', () => {
expect(unwrapAgentBlock('```info_for_agent\ninside\n```')).toBe('inside');
expect(unwrapAgentBlock('<agent-block>\ninside\n</agent-block>')).toBe('inside');
});
it('strips OpenCode runtime-only delivery blocks from display text', () => {
const text = [
'<opencode_runtime_identity>',
'Do not use SendMessage.',
'</opencode_runtime_identity>',
'<opencode_app_message_delivery>',
'Use agent-teams_message_send.',
'</opencode_app_message_delivery>',
'Human-visible task text',
].join('\n');
expect(stripAgentBlocks(text)).toBe('Human-visible task text');
expect(extractAgentBlockContents(text)).toEqual([
'Do not use SendMessage.',
'Use agent-teams_message_send.',
]);
});
});