agent-ecosystem/src/features/member-log-stream/main/adapters/output/sources/OpenCodeMemberRuntimePreviewSource.ts
2026-05-07 15:18:21 +03:00

188 lines
5.9 KiB
TypeScript

import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
import { mapOpenCodeRuntimeTranscriptMessagesToParsedMessages } from '@main/services/team/taskLogs/stream/OpenCodeRuntimeProjectionMapper';
import { extractMemberLogPreviewItems } from '../../../../core/domain/policies/memberLogPreviewExtractor';
import { normalizeMemberName } from './memberLogStreamSourceUtils';
import type { MemberLogStreamWarning } from '../../../../contracts';
import type {
MemberLogPreviewSource,
MemberLogPreviewSourceInput,
MemberLogPreviewSourceResult,
} from '../../../../core/application/ports/MemberLogPreviewSource';
import type { ClaudeMultimodelBridgeService } from '@main/services/runtime/ClaudeMultimodelBridgeService';
interface BinaryResolverLike {
resolve(): Promise<string | null>;
}
function classifyOpenCodePreviewError(error: unknown): MemberLogStreamWarning {
const message = error instanceof Error ? error.message : String(error);
const normalized = message.toLowerCase();
if (normalized.includes('timed out') || normalized.includes('timeout')) {
return {
code: 'opencode_runtime_timeout',
message: 'OpenCode runtime preview timed out; graph preview will use other sources.',
};
}
if (
normalized.includes('--lane') ||
normalized.includes('multiple') ||
normalized.includes('ambiguous')
) {
return {
code: 'opencode_ambiguous_lane',
message: 'OpenCode runtime session is ambiguous without a safe lane id.',
};
}
return {
code: 'opencode_runtime_unavailable',
message: `OpenCode runtime preview is unavailable: ${message}`,
};
}
export class OpenCodeMemberRuntimePreviewSource implements MemberLogPreviewSource {
readonly provider = 'opencode_runtime' as const;
private readonly cache = new Map<
string,
{ expiresAt: number; result: MemberLogPreviewSourceResult }
>();
private readonly inFlight = new Map<string, Promise<MemberLogPreviewSourceResult>>();
constructor(
private readonly runtimeBridge: ClaudeMultimodelBridgeService,
private readonly binaryResolver: BinaryResolverLike = ClaudeBinaryResolver
) {}
async loadPreview(input: MemberLogPreviewSourceInput): Promise<MemberLogPreviewSourceResult> {
const cacheKey = [
input.teamName,
normalizeMemberName(input.memberName),
input.laneId ?? '',
input.maxItems,
input.textLimit,
input.budget.openCodeMessageLimit,
].join('::');
if (!input.forceRefresh) {
const cached = this.cache.get(cacheKey);
if (cached && cached.expiresAt > Date.now()) {
return cached.result;
}
}
const inFlightKey = input.forceRefresh ? `${cacheKey}::force` : cacheKey;
const existing = this.inFlight.get(inFlightKey);
if (existing) {
return existing;
}
const promise = this.buildResult(input)
.then((result) => {
this.cache.set(cacheKey, {
expiresAt: Date.now() + input.budget.cacheTtlMs,
result,
});
return result;
})
.finally(() => {
this.inFlight.delete(inFlightKey);
});
this.inFlight.set(inFlightKey, promise);
return promise;
}
private async buildResult(
input: MemberLogPreviewSourceInput
): Promise<MemberLogPreviewSourceResult> {
if (!input.laneId) {
return {
provider: this.provider,
status: 'skipped',
reason: 'opencode_safe_lane_unavailable',
items: [],
warnings: [],
truncated: false,
overflowCount: 0,
};
}
const binaryPath = await this.binaryResolver.resolve();
if (!binaryPath) {
return this.skipped(
'opencode_runtime_unavailable',
'OpenCode runtime bridge is unavailable.'
);
}
try {
const transcript = await this.runtimeBridge.getOpenCodeTranscript(binaryPath, {
teamId: input.teamName,
memberName: input.memberName,
limit: input.budget.openCodeMessageLimit,
laneId: input.laneId,
timeoutMs: input.budget.openCodeTimeoutMs,
});
const projectedMessages = transcript?.logProjection?.messages ?? [];
const parsedMessages = mapOpenCodeRuntimeTranscriptMessagesToParsedMessages(projectedMessages)
.sort((left, right) => left.timestamp.getTime() - right.timestamp.getTime())
.slice(-input.budget.maxSourceMessagesPerProvider);
if (parsedMessages.length === 0) {
return {
provider: this.provider,
status: 'skipped',
reason: 'opencode_missing_runtime_session',
items: [],
warnings: [],
truncated: false,
overflowCount: 0,
};
}
const sessionId =
transcript?.sessionId ??
parsedMessages[0]?.sessionId ??
`opencode:${normalizeMemberName(input.memberName)}`;
const extracted = extractMemberLogPreviewItems({
messages: parsedMessages,
provider: this.provider,
maxItems: input.maxItems,
textLimit: input.textLimit,
sourceId: sessionId,
sourceLabel: 'OpenCode runtime',
sessionId,
laneId: input.laneId,
});
return {
provider: this.provider,
status: extracted.items.length > 0 ? 'included' : 'skipped',
reason: extracted.items.length > 0 ? undefined : 'opencode_no_renderable_preview',
items: extracted.items,
warnings: [],
truncated: extracted.truncated,
overflowCount: extracted.overflowCount,
};
} catch (error) {
const warning = classifyOpenCodePreviewError(error);
return this.skipped(warning.code, warning.message, warning);
}
}
private skipped(
code: MemberLogStreamWarning['code'],
reason: string,
warning: MemberLogStreamWarning = { code, message: reason }
): MemberLogPreviewSourceResult {
return {
provider: this.provider,
status: 'skipped',
reason,
items: [],
warnings: [warning],
truncated: false,
overflowCount: 0,
};
}
}