fix: improve opencode runtime task logs
This commit is contained in:
parent
376480b84f
commit
531a10e34f
13 changed files with 757 additions and 57 deletions
|
|
@ -16072,6 +16072,11 @@ export class TeamProvisioningService {
|
||||||
hardFailureReason: evidenceEntry.hardFailureReason,
|
hardFailureReason: evidenceEntry.hardFailureReason,
|
||||||
pendingPermissionRequestIds: evidenceEntry.pendingPermissionRequestIds,
|
pendingPermissionRequestIds: evidenceEntry.pendingPermissionRequestIds,
|
||||||
runtimePid: evidenceEntry.runtimePid,
|
runtimePid: evidenceEntry.runtimePid,
|
||||||
|
sessionId: evidenceEntry.sessionId,
|
||||||
|
livenessKind: evidenceEntry.livenessKind,
|
||||||
|
pidSource: evidenceEntry.pidSource,
|
||||||
|
runtimeDiagnostic: evidenceEntry.runtimeDiagnostic,
|
||||||
|
runtimeDiagnosticSeverity: evidenceEntry.runtimeDiagnosticSeverity,
|
||||||
diagnostics: evidenceEntry.diagnostics,
|
diagnostics: evidenceEntry.diagnostics,
|
||||||
}
|
}
|
||||||
: finishedWithoutRuntimeEvidence
|
: finishedWithoutRuntimeEvidence
|
||||||
|
|
@ -16638,6 +16643,12 @@ export class TeamProvisioningService {
|
||||||
hardFailureReason?: string;
|
hardFailureReason?: string;
|
||||||
pendingPermissionRequestIds?: string[];
|
pendingPermissionRequestIds?: string[];
|
||||||
runtimePid?: number;
|
runtimePid?: number;
|
||||||
|
sessionId?: string;
|
||||||
|
runtimeSessionId?: string;
|
||||||
|
livenessKind?: TeamAgentRuntimeLivenessKind;
|
||||||
|
pidSource?: TeamAgentRuntimePidSource;
|
||||||
|
runtimeDiagnostic?: string;
|
||||||
|
runtimeDiagnosticSeverity?: TeamAgentRuntimeDiagnosticSeverity;
|
||||||
diagnostics?: string[];
|
diagnostics?: string[];
|
||||||
};
|
};
|
||||||
pendingReason?: string;
|
pendingReason?: string;
|
||||||
|
|
@ -16685,6 +16696,11 @@ export class TeamProvisioningService {
|
||||||
hardFailureReason: runtimeEvidence.hardFailureReason,
|
hardFailureReason: runtimeEvidence.hardFailureReason,
|
||||||
pendingPermissionRequestIds: runtimeEvidence.pendingPermissionRequestIds,
|
pendingPermissionRequestIds: runtimeEvidence.pendingPermissionRequestIds,
|
||||||
runtimePid: runtimeEvidence.runtimePid,
|
runtimePid: runtimeEvidence.runtimePid,
|
||||||
|
sessionId: runtimeEvidence.sessionId,
|
||||||
|
livenessKind: runtimeEvidence.livenessKind,
|
||||||
|
pidSource: runtimeEvidence.pidSource,
|
||||||
|
runtimeDiagnostic: runtimeEvidence.runtimeDiagnostic,
|
||||||
|
runtimeDiagnosticSeverity: runtimeEvidence.runtimeDiagnosticSeverity,
|
||||||
diagnostics: runtimeEvidence.diagnostics,
|
diagnostics: runtimeEvidence.diagnostics,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,8 @@ export function resolveTeamMemberRuntimeLiveness(
|
||||||
): ResolvedTeamMemberRuntimeLiveness {
|
): ResolvedTeamMemberRuntimeLiveness {
|
||||||
const tracked = input.trackedSpawnStatus;
|
const tracked = input.trackedSpawnStatus;
|
||||||
const runtimeSessionId = input.runtimeSessionId ?? input.persistedRuntimeSessionId;
|
const runtimeSessionId = input.runtimeSessionId ?? input.persistedRuntimeSessionId;
|
||||||
|
const hasConfirmedBootstrap =
|
||||||
|
tracked?.bootstrapConfirmed === true || tracked?.launchState === 'confirmed_alive';
|
||||||
const diagnostics: string[] = [];
|
const diagnostics: string[] = [];
|
||||||
if (!input.processTableAvailable) {
|
if (!input.processTableAvailable) {
|
||||||
diagnostics.push('process table unavailable');
|
diagnostics.push('process table unavailable');
|
||||||
|
|
@ -230,15 +232,38 @@ export function resolveTeamMemberRuntimeLiveness(
|
||||||
if (runtimePidRow && input.providerId === 'opencode') {
|
if (runtimePidRow && input.providerId === 'opencode') {
|
||||||
const processCommand = sanitizeProcessCommandForDiagnostics(runtimePidRow.command);
|
const processCommand = sanitizeProcessCommandForDiagnostics(runtimePidRow.command);
|
||||||
if (isOpenCodeRuntimeProcess(runtimePidRow.command)) {
|
if (isOpenCodeRuntimeProcess(runtimePidRow.command)) {
|
||||||
|
if (hasConfirmedBootstrap) {
|
||||||
|
return result({
|
||||||
|
alive: true,
|
||||||
|
livenessKind: 'runtime_process',
|
||||||
|
pidSource: 'opencode_bridge',
|
||||||
|
pid: runtimePidRow.pid,
|
||||||
|
runtimeSessionId,
|
||||||
|
processCommand,
|
||||||
|
runtimeLastSeenAt: tracked?.lastHeartbeatAt ?? tracked?.updatedAt,
|
||||||
|
runtimeDiagnostic: 'OpenCode runtime process detected after bootstrap confirmation',
|
||||||
|
diagnostics: [
|
||||||
|
...diagnostics,
|
||||||
|
'matched OpenCode runtime pid and process identity',
|
||||||
|
'bootstrap confirmed',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
return result({
|
return result({
|
||||||
alive: true,
|
alive: false,
|
||||||
livenessKind: 'runtime_process',
|
livenessKind: 'runtime_process_candidate',
|
||||||
pidSource: 'opencode_bridge',
|
pidSource: 'opencode_bridge',
|
||||||
pid: runtimePidRow.pid,
|
pid: runtimePidRow.pid,
|
||||||
runtimeSessionId,
|
runtimeSessionId,
|
||||||
processCommand,
|
processCommand,
|
||||||
runtimeDiagnostic: 'OpenCode runtime process detected',
|
runtimeDiagnostic:
|
||||||
diagnostics: [...diagnostics, 'matched OpenCode runtime pid and process identity'],
|
'OpenCode runtime process detected, but teammate bootstrap is not confirmed',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
diagnostics: [
|
||||||
|
...diagnostics,
|
||||||
|
'matched OpenCode runtime pid and process identity',
|
||||||
|
'waiting for teammate bootstrap confirmation',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return result({
|
return result({
|
||||||
|
|
@ -257,7 +282,7 @@ export function resolveTeamMemberRuntimeLiveness(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tracked?.bootstrapConfirmed === true || tracked?.launchState === 'confirmed_alive') {
|
if (hasConfirmedBootstrap) {
|
||||||
return result({
|
return result({
|
||||||
alive: true,
|
alive: true,
|
||||||
livenessKind: 'confirmed_bootstrap',
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
|
|
||||||
|
|
@ -556,6 +556,14 @@ function mapBridgeMemberToRuntimeEvidence(
|
||||||
: runtimeMaterialized || sessionId
|
: runtimeMaterialized || sessionId
|
||||||
? 'OpenCode session exists without verified runtime pid'
|
? 'OpenCode session exists without verified runtime pid'
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const runtimeDiagnosticSeverity = failed
|
||||||
|
? 'error'
|
||||||
|
: pendingRuntimeObserved ||
|
||||||
|
launchState === 'permission_blocked' ||
|
||||||
|
runtimeMaterialized ||
|
||||||
|
sessionId
|
||||||
|
? 'warning'
|
||||||
|
: undefined;
|
||||||
return {
|
return {
|
||||||
memberName,
|
memberName,
|
||||||
providerId: 'opencode',
|
providerId: 'opencode',
|
||||||
|
|
@ -585,6 +593,7 @@ function mapBridgeMemberToRuntimeEvidence(
|
||||||
livenessKind,
|
livenessKind,
|
||||||
...(hasRuntimePid ? { pidSource: 'opencode_bridge' as const } : {}),
|
...(hasRuntimePid ? { pidSource: 'opencode_bridge' as const } : {}),
|
||||||
...(runtimeDiagnostic ? { runtimeDiagnostic } : {}),
|
...(runtimeDiagnostic ? { runtimeDiagnostic } : {}),
|
||||||
|
...(runtimeDiagnosticSeverity ? { runtimeDiagnosticSeverity } : {}),
|
||||||
diagnostics,
|
diagnostics,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type {
|
||||||
PersistedTeamLaunchPhase,
|
PersistedTeamLaunchPhase,
|
||||||
PersistedTeamLaunchSnapshot,
|
PersistedTeamLaunchSnapshot,
|
||||||
TeamAgentRuntimeBackendType,
|
TeamAgentRuntimeBackendType,
|
||||||
|
TeamAgentRuntimeDiagnosticSeverity,
|
||||||
TeamAgentRuntimeLivenessKind,
|
TeamAgentRuntimeLivenessKind,
|
||||||
TeamAgentRuntimePidSource,
|
TeamAgentRuntimePidSource,
|
||||||
TeamLaunchAggregateState,
|
TeamLaunchAggregateState,
|
||||||
|
|
@ -78,6 +79,7 @@ export interface TeamRuntimeMemberLaunchEvidence {
|
||||||
livenessKind?: TeamAgentRuntimeLivenessKind;
|
livenessKind?: TeamAgentRuntimeLivenessKind;
|
||||||
pidSource?: TeamAgentRuntimePidSource;
|
pidSource?: TeamAgentRuntimePidSource;
|
||||||
runtimeDiagnostic?: string;
|
runtimeDiagnostic?: string;
|
||||||
|
runtimeDiagnosticSeverity?: TeamAgentRuntimeDiagnosticSeverity;
|
||||||
diagnostics: string[];
|
diagnostics: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,13 +44,16 @@ function isOpenCodeDeliveryAccepted(delivery: OpenCodeTaskStallDelivery): boolea
|
||||||
if (delivery.queuedBehindMessageId) {
|
if (delivery.queuedBehindMessageId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (delivery.responsePending === true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (delivery.accepted === true) {
|
if (delivery.accepted === true) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (delivery.delivered === true && delivery.responsePending !== true) {
|
if (delivery.delivered === true) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return Boolean(delivery.responsePending === true && delivery.ledgerRecordId);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TeamTaskStallNotifier {
|
export class TeamTaskStallNotifier {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,15 @@ const WINDOW_GRACE_AFTER_MS = 15_000;
|
||||||
const ATTRIBUTION_WINDOW_GRACE_MS = 1_000;
|
const ATTRIBUTION_WINDOW_GRACE_MS = 1_000;
|
||||||
const TASK_MARKER_CONTEXT_BEFORE_MESSAGES = 1;
|
const TASK_MARKER_CONTEXT_BEFORE_MESSAGES = 1;
|
||||||
const TASK_MARKER_CONTEXT_MAX_MS = 5 * 60_000;
|
const TASK_MARKER_CONTEXT_MAX_MS = 5 * 60_000;
|
||||||
|
const NATIVE_TOOL_CONTEXT_BEFORE_MS = 5 * 60_000;
|
||||||
|
const NATIVE_TOOL_CONTEXT_AFTER_MS = 5 * 60_000;
|
||||||
|
|
||||||
|
const AGENT_TEAMS_TOOL_PREFIXES = [
|
||||||
|
'mcp__agent-teams__',
|
||||||
|
'mcp__agent_teams__',
|
||||||
|
'agent-teams_',
|
||||||
|
'agent_teams_',
|
||||||
|
] as const;
|
||||||
|
|
||||||
const TASK_LOG_MARKER_TOOL_NAMES = new Set<string>([
|
const TASK_LOG_MARKER_TOOL_NAMES = new Set<string>([
|
||||||
'task_start',
|
'task_start',
|
||||||
|
|
@ -52,6 +61,22 @@ const TASK_LOG_MARKER_TOOL_NAMES = new Set<string>([
|
||||||
'review_request_changes',
|
'review_request_changes',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const BOARD_MCP_TOOL_NAMES = new Set<string>([
|
||||||
|
...TASK_LOG_MARKER_TOOL_NAMES,
|
||||||
|
'runtime_bootstrap_checkin',
|
||||||
|
'member_briefing',
|
||||||
|
'message_send',
|
||||||
|
'cross_team_send',
|
||||||
|
'task_create',
|
||||||
|
'task_create_from_message',
|
||||||
|
'task_get',
|
||||||
|
'task_get_comment',
|
||||||
|
'task_list',
|
||||||
|
'task_update',
|
||||||
|
'task_delete',
|
||||||
|
'process_list',
|
||||||
|
]);
|
||||||
|
|
||||||
const TERMINAL_TASK_MARKER_TOOL_NAMES = new Set<string>([
|
const TERMINAL_TASK_MARKER_TOOL_NAMES = new Set<string>([
|
||||||
'task_complete',
|
'task_complete',
|
||||||
'review_approve',
|
'review_approve',
|
||||||
|
|
@ -104,6 +129,13 @@ interface TaskMarkerProjection {
|
||||||
messages: ParsedMessage[];
|
messages: ParsedMessage[];
|
||||||
markerMatchCount: number;
|
markerMatchCount: number;
|
||||||
markerSpanCount: number;
|
markerSpanCount: number;
|
||||||
|
boardMcpToolCount: number;
|
||||||
|
nativeToolCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectionToolCounts {
|
||||||
|
boardMcpToolCount: number;
|
||||||
|
nativeToolCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeuristicFallbackReason =
|
type HeuristicFallbackReason =
|
||||||
|
|
@ -284,6 +316,44 @@ function refsIntersect(left: Set<string>, right: Set<string>): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isBoardMcpToolName(rawName: string): boolean {
|
||||||
|
const normalizedRawName = rawName
|
||||||
|
.trim()
|
||||||
|
.replace(/^proxy_/, '')
|
||||||
|
.toLowerCase();
|
||||||
|
const canonicalName = canonicalizeAgentTeamsToolName(rawName).trim().toLowerCase();
|
||||||
|
return (
|
||||||
|
AGENT_TEAMS_TOOL_PREFIXES.some((prefix) => normalizedRawName.startsWith(prefix)) ||
|
||||||
|
BOARD_MCP_TOOL_NAMES.has(canonicalName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNativeOpenCodeToolName(rawName: string): boolean {
|
||||||
|
const normalizedName = rawName.trim();
|
||||||
|
return normalizedName.length > 0 && !isBoardMcpToolName(normalizedName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function messageHasNativeOpenCodeToolCall(message: ParsedMessage): boolean {
|
||||||
|
return message.toolCalls.some((toolCall) => isNativeOpenCodeToolName(toolCall.name ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
function countProjectionToolCalls(messages: ParsedMessage[]): ProjectionToolCounts {
|
||||||
|
let boardMcpToolCount = 0;
|
||||||
|
let nativeToolCount = 0;
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
for (const toolCall of message.toolCalls) {
|
||||||
|
if (isNativeOpenCodeToolName(toolCall.name ?? '')) {
|
||||||
|
nativeToolCount += 1;
|
||||||
|
} else if (isBoardMcpToolName(toolCall.name ?? '')) {
|
||||||
|
boardMcpToolCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { boardMcpToolCount, nativeToolCount };
|
||||||
|
}
|
||||||
|
|
||||||
function markerInputReferencesTaskInTeam(
|
function markerInputReferencesTaskInTeam(
|
||||||
input: unknown,
|
input: unknown,
|
||||||
teamName: string,
|
teamName: string,
|
||||||
|
|
@ -504,11 +574,16 @@ function resolveMarkerSpanStart(messages: ParsedMessage[], markerIndex: number):
|
||||||
function findLastMessageIndexInWindow(
|
function findLastMessageIndexInWindow(
|
||||||
messages: ParsedMessage[],
|
messages: ParsedMessage[],
|
||||||
startIndex: number,
|
startIndex: number,
|
||||||
window: TimeWindow
|
window: TimeWindow,
|
||||||
|
maxEndMs = Number.POSITIVE_INFINITY
|
||||||
): number {
|
): number {
|
||||||
let endIndex = startIndex;
|
let endIndex = startIndex;
|
||||||
for (let index = startIndex + 1; index < messages.length; index += 1) {
|
for (let index = startIndex + 1; index < messages.length; index += 1) {
|
||||||
if (!isWithinSingleTimeWindow(messages[index].timestamp, window)) {
|
const message = messages[index];
|
||||||
|
if (!message || message.timestamp.getTime() > maxEndMs) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!isWithinSingleTimeWindow(message.timestamp, window)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
endIndex = index;
|
endIndex = index;
|
||||||
|
|
@ -562,7 +637,11 @@ function buildMarkerSpan(
|
||||||
lastMarker.windowIndex === null ? undefined : (windows[lastMarker.windowIndex] ?? undefined);
|
lastMarker.windowIndex === null ? undefined : (windows[lastMarker.windowIndex] ?? undefined);
|
||||||
|
|
||||||
if (!isTerminalTaskMarkerMatch(lastMarker) && window) {
|
if (!isTerminalTaskMarkerMatch(lastMarker) && window) {
|
||||||
endIndex = findLastMessageIndexInWindow(messages, lastMarker.index, window);
|
const maxEndMs =
|
||||||
|
window.endMs === null
|
||||||
|
? messages[lastMarker.index].timestamp.getTime() + TASK_MARKER_CONTEXT_MAX_MS
|
||||||
|
: Number.POSITIVE_INFINITY;
|
||||||
|
endIndex = findLastMessageIndexInWindow(messages, lastMarker.index, window, maxEndMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -571,6 +650,107 @@ function buildMarkerSpan(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clampWindowToTaskWindow(
|
||||||
|
window: TimeWindow,
|
||||||
|
taskWindow: TimeWindow | undefined
|
||||||
|
): TimeWindow {
|
||||||
|
if (!taskWindow) {
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskEndMs = taskWindow.endMs ?? Date.now();
|
||||||
|
return {
|
||||||
|
startMs: Math.max(window.startMs, taskWindow.startMs),
|
||||||
|
endMs: Math.min(window.endMs ?? taskEndMs, taskEndMs),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNativeToolWindowForMarkerGroup(
|
||||||
|
messages: ParsedMessage[],
|
||||||
|
markerGroup: TaskMarkerMatch[],
|
||||||
|
span: { startIndex: number; endIndex: number },
|
||||||
|
taskWindows: TimeWindow[]
|
||||||
|
): TimeWindow | null {
|
||||||
|
const firstMarker = markerGroup[0];
|
||||||
|
const lastMarker = markerGroup[markerGroup.length - 1];
|
||||||
|
if (!firstMarker || !lastMarker) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupHasStartMarker = markerGroup.some((match) =>
|
||||||
|
match.markerCalls.some((markerCall) => markerCall.toolName === 'task_start')
|
||||||
|
);
|
||||||
|
const spanStartMessage = messages[span.startIndex];
|
||||||
|
const lastMarkerMessage = messages[lastMarker.index];
|
||||||
|
if (!spanStartMessage || !lastMarkerMessage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startMs = groupHasStartMarker
|
||||||
|
? spanStartMessage.timestamp.getTime()
|
||||||
|
: lastMarkerMessage.timestamp.getTime() - NATIVE_TOOL_CONTEXT_BEFORE_MS;
|
||||||
|
const taskWindow =
|
||||||
|
lastMarker.windowIndex === null
|
||||||
|
? undefined
|
||||||
|
: (taskWindows[lastMarker.windowIndex] ?? undefined);
|
||||||
|
const endMs = isTerminalTaskMarkerMatch(lastMarker)
|
||||||
|
? Math.max(
|
||||||
|
messages[span.endIndex]?.timestamp.getTime() ?? lastMarkerMessage.timestamp.getTime(),
|
||||||
|
lastMarkerMessage.timestamp.getTime()
|
||||||
|
)
|
||||||
|
: (taskWindow?.endMs ?? lastMarkerMessage.timestamp.getTime() + NATIVE_TOOL_CONTEXT_AFTER_MS);
|
||||||
|
const clamped = clampWindowToTaskWindow({ startMs, endMs }, taskWindow);
|
||||||
|
return clamped.startMs <= (clamped.endMs ?? Date.now()) ? clamped : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNativeToolIndexesInWindows(
|
||||||
|
includedIndexes: Set<number>,
|
||||||
|
messages: ParsedMessage[],
|
||||||
|
windows: TimeWindow[]
|
||||||
|
): void {
|
||||||
|
if (windows.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < messages.length; index += 1) {
|
||||||
|
const message = messages[index];
|
||||||
|
if (!messageHasNativeOpenCodeToolCall(message)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isWithinTimeWindows(message.timestamp, windows)) {
|
||||||
|
includedIndexes.add(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addToolResultIndexesForIncludedAssistants(
|
||||||
|
includedIndexes: Set<number>,
|
||||||
|
messages: ParsedMessage[]
|
||||||
|
): void {
|
||||||
|
const includedAssistantUuids = new Set<string>();
|
||||||
|
for (const index of includedIndexes) {
|
||||||
|
const message = messages[index];
|
||||||
|
if (message?.type === 'assistant') {
|
||||||
|
includedAssistantUuids.add(message.uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includedAssistantUuids.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < messages.length; index += 1) {
|
||||||
|
const message = messages[index];
|
||||||
|
if (
|
||||||
|
message?.isMeta &&
|
||||||
|
message.sourceToolAssistantUUID &&
|
||||||
|
includedAssistantUuids.has(message.sourceToolAssistantUUID)
|
||||||
|
) {
|
||||||
|
includedIndexes.add(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildTaskMarkerProjection(
|
function buildTaskMarkerProjection(
|
||||||
projectedMessages: OpenCodeRuntimeTranscriptLogMessage[],
|
projectedMessages: OpenCodeRuntimeTranscriptLogMessage[],
|
||||||
teamName: string,
|
teamName: string,
|
||||||
|
|
@ -595,15 +775,36 @@ function buildTaskMarkerProjection(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const spans = groupMarkerMatches(markerMatches, taskWindows)
|
const markerGroups = groupMarkerMatches(markerMatches, taskWindows);
|
||||||
.map((group) => buildMarkerSpan(parsedMessages, group, taskWindows))
|
const spansWithGroups = markerGroups
|
||||||
.filter((span): span is { startIndex: number; endIndex: number } => span !== null);
|
.map((group) => {
|
||||||
|
const span = buildMarkerSpan(parsedMessages, group, taskWindows);
|
||||||
|
return span ? { group, span } : null;
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
(
|
||||||
|
item
|
||||||
|
): item is { group: TaskMarkerMatch[]; span: { startIndex: number; endIndex: number } } =>
|
||||||
|
item !== null
|
||||||
|
);
|
||||||
const includedIndexes = new Set<number>();
|
const includedIndexes = new Set<number>();
|
||||||
for (const span of spans) {
|
const nativeToolWindows: TimeWindow[] = [];
|
||||||
|
for (const { group, span } of spansWithGroups) {
|
||||||
for (let index = span.startIndex; index <= span.endIndex; index += 1) {
|
for (let index = span.startIndex; index <= span.endIndex; index += 1) {
|
||||||
includedIndexes.add(index);
|
includedIndexes.add(index);
|
||||||
}
|
}
|
||||||
|
const nativeToolWindow = buildNativeToolWindowForMarkerGroup(
|
||||||
|
parsedMessages,
|
||||||
|
group,
|
||||||
|
span,
|
||||||
|
taskWindows
|
||||||
|
);
|
||||||
|
if (nativeToolWindow) {
|
||||||
|
nativeToolWindows.push(nativeToolWindow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
addNativeToolIndexesInWindows(includedIndexes, parsedMessages, nativeToolWindows);
|
||||||
|
addToolResultIndexesForIncludedAssistants(includedIndexes, parsedMessages);
|
||||||
|
|
||||||
const messages = [...includedIndexes]
|
const messages = [...includedIndexes]
|
||||||
.sort((left, right) => left - right)
|
.sort((left, right) => left - right)
|
||||||
|
|
@ -618,7 +819,8 @@ function buildTaskMarkerProjection(
|
||||||
? {
|
? {
|
||||||
messages,
|
messages,
|
||||||
markerMatchCount,
|
markerMatchCount,
|
||||||
markerSpanCount: spans.length,
|
markerSpanCount: spansWithGroups.length,
|
||||||
|
...countProjectionToolCalls(messages),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
@ -989,6 +1191,12 @@ export class OpenCodeTaskLogStreamSource {
|
||||||
.filter((message): message is ParsedMessage => message !== null)
|
.filter((message): message is ParsedMessage => message !== null)
|
||||||
.filter((message) => isWithinTimeWindows(message.timestamp, timeWindows))
|
.filter((message) => isWithinTimeWindows(message.timestamp, timeWindows))
|
||||||
.sort((left, right) => left.timestamp.getTime() - right.timestamp.getTime());
|
.sort((left, right) => left.timestamp.getTime() - right.timestamp.getTime());
|
||||||
|
const toolCounts = markerProjection
|
||||||
|
? {
|
||||||
|
boardMcpToolCount: markerProjection.boardMcpToolCount,
|
||||||
|
nativeToolCount: markerProjection.nativeToolCount,
|
||||||
|
}
|
||||||
|
: countProjectionToolCalls(filteredMessages);
|
||||||
|
|
||||||
if (filteredMessages.length === 0) {
|
if (filteredMessages.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -1017,7 +1225,7 @@ export class OpenCodeTaskLogStreamSource {
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`[${teamName}/${task.id}] using OpenCode runtime fallback for task log stream (${filteredMessages.length} messages, owner=${ownerName})`
|
`[${teamName}/${task.id}] using OpenCode runtime fallback for task log stream (${filteredMessages.length} messages, owner=${ownerName}, boardMcpTools=${toolCounts.boardMcpToolCount}, nativeTools=${toolCounts.nativeToolCount})`
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1030,6 +1238,7 @@ export class OpenCodeTaskLogStreamSource {
|
||||||
mode: 'heuristic',
|
mode: 'heuristic',
|
||||||
attributionRecordCount: projectionContext.attributionRecordCount,
|
attributionRecordCount: projectionContext.attributionRecordCount,
|
||||||
projectedMessageCount: filteredMessages.length,
|
projectedMessageCount: filteredMessages.length,
|
||||||
|
...toolCounts,
|
||||||
fallbackReason: projectionReason,
|
fallbackReason: projectionReason,
|
||||||
...(markerProjection
|
...(markerProjection
|
||||||
? {
|
? {
|
||||||
|
|
@ -1122,6 +1331,8 @@ export class OpenCodeTaskLogStreamSource {
|
||||||
const participants: BoardTaskLogParticipant[] = [];
|
const participants: BoardTaskLogParticipant[] = [];
|
||||||
const segments: BoardTaskLogSegment[] = [];
|
const segments: BoardTaskLogSegment[] = [];
|
||||||
let projectedMessageCount = 0;
|
let projectedMessageCount = 0;
|
||||||
|
let boardMcpToolCount = 0;
|
||||||
|
let nativeToolCount = 0;
|
||||||
for (const member of members.sort((left, right) => {
|
for (const member of members.sort((left, right) => {
|
||||||
const leftStart = left.messages[0]?.timestamp.getTime() ?? 0;
|
const leftStart = left.messages[0]?.timestamp.getTime() ?? 0;
|
||||||
const rightStart = right.messages[0]?.timestamp.getTime() ?? 0;
|
const rightStart = right.messages[0]?.timestamp.getTime() ?? 0;
|
||||||
|
|
@ -1142,7 +1353,10 @@ export class OpenCodeTaskLogStreamSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
const participant = buildParticipant(member.memberName);
|
const participant = buildParticipant(member.memberName);
|
||||||
|
const memberToolCounts = countProjectionToolCalls(member.messages);
|
||||||
projectedMessageCount += member.messages.length;
|
projectedMessageCount += member.messages.length;
|
||||||
|
boardMcpToolCount += memberToolCounts.boardMcpToolCount;
|
||||||
|
nativeToolCount += memberToolCounts.nativeToolCount;
|
||||||
participants.push(participant);
|
participants.push(participant);
|
||||||
segments.push({
|
segments.push({
|
||||||
id: `opencode-attributed:${teamName}:${task.id}:${normalizeMemberName(member.memberName)}`,
|
id: `opencode-attributed:${teamName}:${task.id}:${normalizeMemberName(member.memberName)}`,
|
||||||
|
|
@ -1159,7 +1373,7 @@ export class OpenCodeTaskLogStreamSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`[${teamName}/${task.id}] using OpenCode task-log attribution (${segments.length} segment(s), ${attributionRecords.length} record(s))`
|
`[${teamName}/${task.id}] using OpenCode task-log attribution (${segments.length} segment(s), ${attributionRecords.length} record(s), boardMcpTools=${boardMcpToolCount}, nativeTools=${nativeToolCount})`
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1172,6 +1386,8 @@ export class OpenCodeTaskLogStreamSource {
|
||||||
mode: 'attribution',
|
mode: 'attribution',
|
||||||
attributionRecordCount: attributionRecords.length,
|
attributionRecordCount: attributionRecords.length,
|
||||||
projectedMessageCount,
|
projectedMessageCount,
|
||||||
|
boardMcpToolCount,
|
||||||
|
nativeToolCount,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,8 @@ export interface BoardTaskLogStreamRuntimeProjection {
|
||||||
mode: 'attribution' | 'heuristic';
|
mode: 'attribution' | 'heuristic';
|
||||||
attributionRecordCount: number;
|
attributionRecordCount: number;
|
||||||
projectedMessageCount: number;
|
projectedMessageCount: number;
|
||||||
|
boardMcpToolCount?: number;
|
||||||
|
nativeToolCount?: number;
|
||||||
fallbackReason?:
|
fallbackReason?:
|
||||||
| 'no_attribution_records'
|
| 'no_attribution_records'
|
||||||
| 'attribution_no_projected_messages'
|
| 'attribution_no_projected_messages'
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,21 @@ const RELAY_WORKS_10_TASK: TeamTask = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RELAY_WORKS_10_COORDINATION_TASK: TeamTask = {
|
||||||
|
id: 'b5534868-0901-4c9e-9296-2b6e2059a08f',
|
||||||
|
displayId: 'b5534868',
|
||||||
|
subject: 'Split calculator implementation work',
|
||||||
|
owner: 'jack',
|
||||||
|
status: 'in_progress',
|
||||||
|
createdAt: '2026-04-24T20:28:58.000Z',
|
||||||
|
updatedAt: '2026-04-24T20:31:21.876Z',
|
||||||
|
workIntervals: [
|
||||||
|
{
|
||||||
|
startedAt: '2026-04-24T20:28:58.000Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
async function loadFixtureTranscript(): Promise<
|
async function loadFixtureTranscript(): Promise<
|
||||||
NonNullable<OpenCodeRuntimeTranscriptResponse['transcript']>
|
NonNullable<OpenCodeRuntimeTranscriptResponse['transcript']>
|
||||||
> {
|
> {
|
||||||
|
|
@ -171,6 +186,43 @@ describe('OpenCodeTaskLogStreamSource real OpenCode fixture e2e', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('includes real native OpenCode read/bash tools from a task-scoped runtime projection', async () => {
|
||||||
|
const transcript = await loadFixtureTranscript();
|
||||||
|
const { source } = createSource({
|
||||||
|
transcript,
|
||||||
|
activeTasks: [RELAY_WORKS_10_COORDINATION_TASK],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await source.getTaskLogStream(
|
||||||
|
'relay-works-10',
|
||||||
|
RELAY_WORKS_10_COORDINATION_TASK.id
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).not.toBeNull();
|
||||||
|
expect(response?.source).toBe('opencode_runtime_fallback');
|
||||||
|
expect(response?.runtimeProjection).toMatchObject({
|
||||||
|
provider: 'opencode',
|
||||||
|
mode: 'heuristic',
|
||||||
|
fallbackReason: 'task_tool_markers',
|
||||||
|
});
|
||||||
|
expect(response?.runtimeProjection?.boardMcpToolCount).toBeGreaterThan(0);
|
||||||
|
expect(response?.runtimeProjection?.nativeToolCount).toBeGreaterThanOrEqual(2);
|
||||||
|
|
||||||
|
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(['read', 'bash']));
|
||||||
|
expect(toolNames).toEqual(
|
||||||
|
expect.arrayContaining(['agent-teams_task_start', 'agent-teams_task_add_comment'])
|
||||||
|
);
|
||||||
|
expect(serialized).toContain('package.json');
|
||||||
|
expect(serialized).toContain('Разбил работу на мелкие задачи');
|
||||||
|
expect(toolNames).not.toContain('SendMessage');
|
||||||
|
});
|
||||||
|
|
||||||
it('uses real attribution UUID bounds before heuristic fallback', async () => {
|
it('uses real attribution UUID bounds before heuristic fallback', async () => {
|
||||||
const transcript = await loadFixtureTranscript();
|
const transcript = await loadFixtureTranscript();
|
||||||
const { source, bridge, attributionStore } = createSource({
|
const { source, bridge, attributionStore } = createSource({
|
||||||
|
|
@ -196,6 +248,8 @@ describe('OpenCodeTaskLogStreamSource real OpenCode fixture e2e', () => {
|
||||||
mode: 'attribution',
|
mode: 'attribution',
|
||||||
attributionRecordCount: 1,
|
attributionRecordCount: 1,
|
||||||
projectedMessageCount: 10,
|
projectedMessageCount: 10,
|
||||||
|
boardMcpToolCount: 4,
|
||||||
|
nativeToolCount: 0,
|
||||||
});
|
});
|
||||||
expect(response?.defaultFilter).toBe('member:jack');
|
expect(response?.defaultFilter).toBe('member:jack');
|
||||||
expect(response?.segments).toHaveLength(1);
|
expect(response?.segments).toHaveLength(1);
|
||||||
|
|
@ -353,7 +407,9 @@ describe('OpenCodeTaskLogStreamSource real OpenCode fixture e2e', () => {
|
||||||
const assistantToolIds = new Set(
|
const assistantToolIds = new Set(
|
||||||
projectedMessages.flatMap((message) => message.toolCalls.map((toolCall) => toolCall.id))
|
projectedMessages.flatMap((message) => message.toolCalls.map((toolCall) => toolCall.id))
|
||||||
);
|
);
|
||||||
const toolResultMessages = projectedMessages.filter((message) => message.toolResults.length > 0);
|
const toolResultMessages = projectedMessages.filter(
|
||||||
|
(message) => message.toolResults.length > 0
|
||||||
|
);
|
||||||
|
|
||||||
expect(projectedMessages).toHaveLength(101);
|
expect(projectedMessages).toHaveLength(101);
|
||||||
expect(toolResultMessages.length).toBeGreaterThan(20);
|
expect(toolResultMessages.length).toBeGreaterThan(20);
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,8 @@ describe('OpenCodeTaskLogStreamSource', () => {
|
||||||
mode: 'heuristic',
|
mode: 'heuristic',
|
||||||
attributionRecordCount: 0,
|
attributionRecordCount: 0,
|
||||||
projectedMessageCount: 2,
|
projectedMessageCount: 2,
|
||||||
|
boardMcpToolCount: 0,
|
||||||
|
nativeToolCount: 1,
|
||||||
fallbackReason: 'no_attribution_records',
|
fallbackReason: 'no_attribution_records',
|
||||||
});
|
});
|
||||||
expect(first?.participants).toEqual([
|
expect(first?.participants).toEqual([
|
||||||
|
|
@ -254,7 +256,9 @@ describe('OpenCodeTaskLogStreamSource', () => {
|
||||||
expect(chunkBuilder.buildBundleChunks).toHaveBeenCalledTimes(1);
|
expect(chunkBuilder.buildBundleChunks).toHaveBeenCalledTimes(1);
|
||||||
expect(chunkBuilder.buildBundleChunks.mock.calls[0]?.[0]).toHaveLength(2);
|
expect(chunkBuilder.buildBundleChunks.mock.calls[0]?.[0]).toHaveLength(2);
|
||||||
expect(
|
expect(
|
||||||
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map((message: { uuid: string }) => message.uuid)
|
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map(
|
||||||
|
(message: { uuid: string }) => message.uuid
|
||||||
|
)
|
||||||
).toEqual(['assistant-1', 'assistant-1::tool_results']);
|
).toEqual(['assistant-1', 'assistant-1::tool_results']);
|
||||||
expect(bridge.getOpenCodeTranscript).toHaveBeenCalledTimes(1);
|
expect(bridge.getOpenCodeTranscript).toHaveBeenCalledTimes(1);
|
||||||
expect(second).toEqual(first);
|
expect(second).toEqual(first);
|
||||||
|
|
@ -529,12 +533,16 @@ describe('OpenCodeTaskLogStreamSource', () => {
|
||||||
mode: 'heuristic',
|
mode: 'heuristic',
|
||||||
attributionRecordCount: 0,
|
attributionRecordCount: 0,
|
||||||
projectedMessageCount: 6,
|
projectedMessageCount: 6,
|
||||||
|
boardMcpToolCount: 2,
|
||||||
|
nativeToolCount: 0,
|
||||||
fallbackReason: 'task_tool_markers',
|
fallbackReason: 'task_tool_markers',
|
||||||
markerMatchCount: 2,
|
markerMatchCount: 2,
|
||||||
markerSpanCount: 1,
|
markerSpanCount: 1,
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map((message: { uuid: string }) => message.uuid)
|
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map(
|
||||||
|
(message: { uuid: string }) => message.uuid
|
||||||
|
)
|
||||||
).toEqual([
|
).toEqual([
|
||||||
'user-task-prompt',
|
'user-task-prompt',
|
||||||
'assistant-start',
|
'assistant-start',
|
||||||
|
|
@ -545,6 +553,226 @@ describe('OpenCodeTaskLogStreamSource', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps native OpenCode tools near task markers in the task stream', async () => {
|
||||||
|
const bridge = {
|
||||||
|
getOpenCodeTranscript: vi.fn(async () => ({
|
||||||
|
sessionId: 'session-opencode',
|
||||||
|
logProjection: {
|
||||||
|
messages: [
|
||||||
|
taskMarkerLogMessage({
|
||||||
|
uuid: 'native-before-task',
|
||||||
|
timestamp: '2026-04-21T09:50:00.000Z',
|
||||||
|
toolName: 'read',
|
||||||
|
input: { filePath: '/tmp/unrelated.ts' },
|
||||||
|
}),
|
||||||
|
textLogMessage({
|
||||||
|
uuid: 'user-task-prompt',
|
||||||
|
type: 'user',
|
||||||
|
role: 'user',
|
||||||
|
timestamp: '2026-04-21T10:01:00.000Z',
|
||||||
|
content: [{ type: 'text', text: 'Start task-a now' }],
|
||||||
|
}),
|
||||||
|
taskMarkerLogMessage({
|
||||||
|
uuid: 'assistant-start',
|
||||||
|
parentUuid: 'user-task-prompt',
|
||||||
|
timestamp: '2026-04-21T10:02:00.000Z',
|
||||||
|
toolName: 'mcp__agent-teams__task_start',
|
||||||
|
input: { teamName: 'team-a', taskId: 'task-a' },
|
||||||
|
}),
|
||||||
|
toolResultLogMessage({
|
||||||
|
uuid: 'assistant-start::tool_results',
|
||||||
|
parentUuid: 'assistant-start',
|
||||||
|
timestamp: '2026-04-21T10:02:01.000Z',
|
||||||
|
sourceToolAssistantUUID: 'assistant-start',
|
||||||
|
}),
|
||||||
|
taskMarkerLogMessage({
|
||||||
|
uuid: 'native-read',
|
||||||
|
parentUuid: 'assistant-start::tool_results',
|
||||||
|
timestamp: '2026-04-21T10:03:00.000Z',
|
||||||
|
toolName: 'read',
|
||||||
|
input: { filePath: '/tmp/app.ts' },
|
||||||
|
}),
|
||||||
|
toolResultLogMessage({
|
||||||
|
uuid: 'native-read::tool_results',
|
||||||
|
parentUuid: 'native-read',
|
||||||
|
timestamp: '2026-04-21T10:03:01.000Z',
|
||||||
|
sourceToolAssistantUUID: 'native-read',
|
||||||
|
}),
|
||||||
|
taskMarkerLogMessage({
|
||||||
|
uuid: 'native-bash',
|
||||||
|
parentUuid: 'native-read::tool_results',
|
||||||
|
timestamp: '2026-04-21T10:04:00.000Z',
|
||||||
|
toolName: 'bash',
|
||||||
|
input: { command: 'pnpm test' },
|
||||||
|
}),
|
||||||
|
toolResultLogMessage({
|
||||||
|
uuid: 'native-bash::tool_results',
|
||||||
|
parentUuid: 'native-bash',
|
||||||
|
timestamp: '2026-04-21T10:04:01.000Z',
|
||||||
|
sourceToolAssistantUUID: 'native-bash',
|
||||||
|
}),
|
||||||
|
taskMarkerLogMessage({
|
||||||
|
uuid: 'assistant-comment',
|
||||||
|
parentUuid: 'native-bash::tool_results',
|
||||||
|
timestamp: '2026-04-21T10:05:00.000Z',
|
||||||
|
toolName: 'mcp__agent-teams__task_add_comment',
|
||||||
|
input: { teamName: 'team-a', taskId: 'task-a', text: 'Tests passed' },
|
||||||
|
}),
|
||||||
|
toolResultLogMessage({
|
||||||
|
uuid: 'assistant-comment::tool_results',
|
||||||
|
parentUuid: 'assistant-comment',
|
||||||
|
timestamp: '2026-04-21T10:05:01.000Z',
|
||||||
|
sourceToolAssistantUUID: 'assistant-comment',
|
||||||
|
}),
|
||||||
|
taskMarkerLogMessage({
|
||||||
|
uuid: 'native-after-task',
|
||||||
|
timestamp: '2026-04-21T10:20:00.000Z',
|
||||||
|
toolName: 'bash',
|
||||||
|
input: { command: 'echo unrelated' },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
const chunkBuilder = {
|
||||||
|
buildBundleChunks: vi.fn((messages) => [
|
||||||
|
{
|
||||||
|
id: 'chunk-native-tools',
|
||||||
|
kind: 'assistant',
|
||||||
|
messages,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
const source = new OpenCodeTaskLogStreamSource(
|
||||||
|
bridge as never,
|
||||||
|
{ resolve: async () => '/tmp/claude' },
|
||||||
|
{
|
||||||
|
getTasks: async () => [
|
||||||
|
createTask({
|
||||||
|
workIntervals: [
|
||||||
|
{
|
||||||
|
startedAt: '2026-04-21T10:00:00.000Z',
|
||||||
|
completedAt: '2026-04-21T10:06:00.000Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
getDeletedTasks: async () => [],
|
||||||
|
} as never,
|
||||||
|
chunkBuilder as never,
|
||||||
|
{ readTaskRecords: vi.fn(async () => []) }
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await source.getTaskLogStream('team-a', 'task-a');
|
||||||
|
|
||||||
|
expect(response?.runtimeProjection).toEqual({
|
||||||
|
provider: 'opencode',
|
||||||
|
mode: 'heuristic',
|
||||||
|
attributionRecordCount: 0,
|
||||||
|
projectedMessageCount: 9,
|
||||||
|
boardMcpToolCount: 2,
|
||||||
|
nativeToolCount: 2,
|
||||||
|
fallbackReason: 'task_tool_markers',
|
||||||
|
markerMatchCount: 2,
|
||||||
|
markerSpanCount: 1,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map(
|
||||||
|
(message: { uuid: string }) => message.uuid
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
'user-task-prompt',
|
||||||
|
'assistant-start',
|
||||||
|
'assistant-start::tool_results',
|
||||||
|
'native-read',
|
||||||
|
'native-read::tool_results',
|
||||||
|
'native-bash',
|
||||||
|
'native-bash::tool_results',
|
||||||
|
'assistant-comment',
|
||||||
|
'assistant-comment::tool_results',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can include native OpenCode work shortly before a comment-only task marker', async () => {
|
||||||
|
const bridge = {
|
||||||
|
getOpenCodeTranscript: vi.fn(async () => ({
|
||||||
|
sessionId: 'session-opencode',
|
||||||
|
logProjection: {
|
||||||
|
messages: [
|
||||||
|
taskMarkerLogMessage({
|
||||||
|
uuid: 'native-read',
|
||||||
|
timestamp: '2026-04-21T10:02:00.000Z',
|
||||||
|
toolName: 'read',
|
||||||
|
input: { filePath: '/tmp/app.ts' },
|
||||||
|
}),
|
||||||
|
toolResultLogMessage({
|
||||||
|
uuid: 'native-read::tool_results',
|
||||||
|
parentUuid: 'native-read',
|
||||||
|
timestamp: '2026-04-21T10:02:01.000Z',
|
||||||
|
sourceToolAssistantUUID: 'native-read',
|
||||||
|
}),
|
||||||
|
taskMarkerLogMessage({
|
||||||
|
uuid: 'assistant-comment',
|
||||||
|
parentUuid: 'native-read::tool_results',
|
||||||
|
timestamp: '2026-04-21T10:04:00.000Z',
|
||||||
|
toolName: 'mcp__agent-teams__task_add_comment',
|
||||||
|
input: { teamName: 'team-a', taskId: 'task-a', text: 'Found the issue' },
|
||||||
|
}),
|
||||||
|
toolResultLogMessage({
|
||||||
|
uuid: 'assistant-comment::tool_results',
|
||||||
|
parentUuid: 'assistant-comment',
|
||||||
|
timestamp: '2026-04-21T10:04:01.000Z',
|
||||||
|
sourceToolAssistantUUID: 'assistant-comment',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
const chunkBuilder = {
|
||||||
|
buildBundleChunks: vi.fn((messages) => [
|
||||||
|
{
|
||||||
|
id: 'chunk-comment-only-native',
|
||||||
|
kind: 'assistant',
|
||||||
|
messages,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
const source = new OpenCodeTaskLogStreamSource(
|
||||||
|
bridge as never,
|
||||||
|
{ resolve: async () => '/tmp/claude' },
|
||||||
|
{
|
||||||
|
getTasks: async () => [createTask()],
|
||||||
|
getDeletedTasks: async () => [],
|
||||||
|
} as never,
|
||||||
|
chunkBuilder as never,
|
||||||
|
{ readTaskRecords: vi.fn(async () => []) }
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await source.getTaskLogStream('team-a', 'task-a');
|
||||||
|
|
||||||
|
expect(response?.runtimeProjection).toEqual({
|
||||||
|
provider: 'opencode',
|
||||||
|
mode: 'heuristic',
|
||||||
|
attributionRecordCount: 0,
|
||||||
|
projectedMessageCount: 4,
|
||||||
|
boardMcpToolCount: 1,
|
||||||
|
nativeToolCount: 1,
|
||||||
|
fallbackReason: 'task_tool_markers',
|
||||||
|
markerMatchCount: 1,
|
||||||
|
markerSpanCount: 1,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map(
|
||||||
|
(message: { uuid: string }) => message.uuid
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
'native-read',
|
||||||
|
'native-read::tool_results',
|
||||||
|
'assistant-comment',
|
||||||
|
'assistant-comment::tool_results',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('ignores OpenCode task markers that explicitly belong to another team', async () => {
|
it('ignores OpenCode task markers that explicitly belong to another team', async () => {
|
||||||
const bridge = {
|
const bridge = {
|
||||||
getOpenCodeTranscript: vi.fn(async () => ({
|
getOpenCodeTranscript: vi.fn(async () => ({
|
||||||
|
|
@ -620,12 +848,16 @@ describe('OpenCodeTaskLogStreamSource', () => {
|
||||||
mode: 'heuristic',
|
mode: 'heuristic',
|
||||||
attributionRecordCount: 0,
|
attributionRecordCount: 0,
|
||||||
projectedMessageCount: 3,
|
projectedMessageCount: 3,
|
||||||
|
boardMcpToolCount: 1,
|
||||||
|
nativeToolCount: 0,
|
||||||
fallbackReason: 'task_tool_markers',
|
fallbackReason: 'task_tool_markers',
|
||||||
markerMatchCount: 1,
|
markerMatchCount: 1,
|
||||||
markerSpanCount: 1,
|
markerSpanCount: 1,
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map((message: { uuid: string }) => message.uuid)
|
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map(
|
||||||
|
(message: { uuid: string }) => message.uuid
|
||||||
|
)
|
||||||
).toEqual(['team-a-prompt', 'team-a-start', 'team-a-start::tool_results']);
|
).toEqual(['team-a-prompt', 'team-a-start', 'team-a-start::tool_results']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -743,12 +975,16 @@ describe('OpenCodeTaskLogStreamSource', () => {
|
||||||
mode: 'heuristic',
|
mode: 'heuristic',
|
||||||
attributionRecordCount: 0,
|
attributionRecordCount: 0,
|
||||||
projectedMessageCount: 10,
|
projectedMessageCount: 10,
|
||||||
|
boardMcpToolCount: 3,
|
||||||
|
nativeToolCount: 0,
|
||||||
fallbackReason: 'task_tool_markers',
|
fallbackReason: 'task_tool_markers',
|
||||||
markerMatchCount: 3,
|
markerMatchCount: 3,
|
||||||
markerSpanCount: 2,
|
markerSpanCount: 2,
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map((message: { uuid: string }) => message.uuid)
|
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map(
|
||||||
|
(message: { uuid: string }) => message.uuid
|
||||||
|
)
|
||||||
).toEqual([
|
).toEqual([
|
||||||
'cycle-1-prompt',
|
'cycle-1-prompt',
|
||||||
'cycle-1-start',
|
'cycle-1-start',
|
||||||
|
|
@ -810,10 +1046,14 @@ describe('OpenCodeTaskLogStreamSource', () => {
|
||||||
mode: 'heuristic',
|
mode: 'heuristic',
|
||||||
attributionRecordCount: 0,
|
attributionRecordCount: 0,
|
||||||
projectedMessageCount: 1,
|
projectedMessageCount: 1,
|
||||||
|
boardMcpToolCount: 0,
|
||||||
|
nativeToolCount: 0,
|
||||||
fallbackReason: 'no_attribution_records',
|
fallbackReason: 'no_attribution_records',
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map((message: { uuid: string }) => message.uuid)
|
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map(
|
||||||
|
(message: { uuid: string }) => message.uuid
|
||||||
|
)
|
||||||
).toEqual(['current-window-work']);
|
).toEqual(['current-window-work']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -866,12 +1106,16 @@ describe('OpenCodeTaskLogStreamSource', () => {
|
||||||
mode: 'heuristic',
|
mode: 'heuristic',
|
||||||
attributionRecordCount: 0,
|
attributionRecordCount: 0,
|
||||||
projectedMessageCount: 2,
|
projectedMessageCount: 2,
|
||||||
|
boardMcpToolCount: 1,
|
||||||
|
nativeToolCount: 0,
|
||||||
fallbackReason: 'task_tool_markers',
|
fallbackReason: 'task_tool_markers',
|
||||||
markerMatchCount: 1,
|
markerMatchCount: 1,
|
||||||
markerSpanCount: 1,
|
markerSpanCount: 1,
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map((message: { uuid: string }) => message.uuid)
|
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map(
|
||||||
|
(message: { uuid: string }) => message.uuid
|
||||||
|
)
|
||||||
).toEqual(['display-ref-start', 'display-ref-start::tool_results']);
|
).toEqual(['display-ref-start', 'display-ref-start::tool_results']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -955,6 +1199,8 @@ describe('OpenCodeTaskLogStreamSource', () => {
|
||||||
mode: 'attribution',
|
mode: 'attribution',
|
||||||
attributionRecordCount: 1,
|
attributionRecordCount: 1,
|
||||||
projectedMessageCount: 1,
|
projectedMessageCount: 1,
|
||||||
|
boardMcpToolCount: 0,
|
||||||
|
nativeToolCount: 0,
|
||||||
});
|
});
|
||||||
expect(response?.participants).toEqual([
|
expect(response?.participants).toEqual([
|
||||||
{
|
{
|
||||||
|
|
@ -973,7 +1219,9 @@ describe('OpenCodeTaskLogStreamSource', () => {
|
||||||
isSidechain: true,
|
isSidechain: true,
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map((message: { uuid: string }) => message.uuid)
|
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map(
|
||||||
|
(message: { uuid: string }) => message.uuid
|
||||||
|
)
|
||||||
).toEqual(['bob-inside']);
|
).toEqual(['bob-inside']);
|
||||||
expect(bridge.getOpenCodeTranscript).toHaveBeenCalledWith('/tmp/claude', {
|
expect(bridge.getOpenCodeTranscript).toHaveBeenCalledWith('/tmp/claude', {
|
||||||
teamId: 'team-a',
|
teamId: 'team-a',
|
||||||
|
|
@ -1065,11 +1313,15 @@ describe('OpenCodeTaskLogStreamSource', () => {
|
||||||
mode: 'heuristic',
|
mode: 'heuristic',
|
||||||
attributionRecordCount: 1,
|
attributionRecordCount: 1,
|
||||||
projectedMessageCount: 1,
|
projectedMessageCount: 1,
|
||||||
|
boardMcpToolCount: 0,
|
||||||
|
nativeToolCount: 0,
|
||||||
fallbackReason: 'attribution_no_projected_messages',
|
fallbackReason: 'attribution_no_projected_messages',
|
||||||
});
|
});
|
||||||
expect(response?.participants[0]?.label).toBe('alice');
|
expect(response?.participants[0]?.label).toBe('alice');
|
||||||
expect(
|
expect(
|
||||||
chunkBuilder.buildBundleChunks.mock.calls.at(-1)?.[0].map((message: { uuid: string }) => message.uuid)
|
chunkBuilder.buildBundleChunks.mock.calls
|
||||||
|
.at(-1)?.[0]
|
||||||
|
.map((message: { uuid: string }) => message.uuid)
|
||||||
).toEqual(['alice-inside']);
|
).toEqual(['alice-inside']);
|
||||||
expect(bridge.getOpenCodeTranscript).toHaveBeenNthCalledWith(1, '/tmp/claude', {
|
expect(bridge.getOpenCodeTranscript).toHaveBeenNthCalledWith(1, '/tmp/claude', {
|
||||||
teamId: 'team-a',
|
teamId: 'team-a',
|
||||||
|
|
@ -1111,9 +1363,7 @@ describe('OpenCodeTaskLogStreamSource', () => {
|
||||||
uuid: isBob ? 'bob-new-attribution' : 'alice-old-heuristic',
|
uuid: isBob ? 'bob-new-attribution' : 'alice-old-heuristic',
|
||||||
parentUuid: undefined,
|
parentUuid: undefined,
|
||||||
type: 'assistant',
|
type: 'assistant',
|
||||||
timestamp: isBob
|
timestamp: isBob ? '2026-04-21T12:05:00.000Z' : '2026-04-21T10:05:00.000Z',
|
||||||
? '2026-04-21T12:05:00.000Z'
|
|
||||||
: '2026-04-21T10:05:00.000Z',
|
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: [{ type: 'text', text: isBob ? 'new attribution' : 'old heuristic' }],
|
content: [{ type: 'text', text: isBob ? 'new attribution' : 'old heuristic' }],
|
||||||
isMeta: false,
|
isMeta: false,
|
||||||
|
|
@ -1168,7 +1418,9 @@ describe('OpenCodeTaskLogStreamSource', () => {
|
||||||
limit: 500,
|
limit: 500,
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
chunkBuilder.buildBundleChunks.mock.calls.at(-1)?.[0].map((message: { uuid: string }) => message.uuid)
|
chunkBuilder.buildBundleChunks.mock.calls
|
||||||
|
.at(-1)?.[0]
|
||||||
|
.map((message: { uuid: string }) => message.uuid)
|
||||||
).toEqual(['bob-new-attribution']);
|
).toEqual(['bob-new-attribution']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1222,7 +1222,7 @@ describe('TeamProvisioningService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows RSS for OpenCode secondary lanes through the shared runtime host without exposing a member pid', async () => {
|
it('shows RSS for OpenCode secondary lane host pids without treating pre-bootstrap runtime as alive', async () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
(svc as any).configReader = {
|
(svc as any).configReader = {
|
||||||
getConfig: vi.fn(async () => ({
|
getConfig: vi.fn(async () => ({
|
||||||
|
|
@ -1325,15 +1325,16 @@ describe('TeamProvisioningService', () => {
|
||||||
expect(pidusage).toHaveBeenCalledWith(333, { maxage: 0 });
|
expect(pidusage).toHaveBeenCalledWith(333, { maxage: 0 });
|
||||||
expect(snapshot.members.bob).toMatchObject({
|
expect(snapshot.members.bob).toMatchObject({
|
||||||
memberName: 'bob',
|
memberName: 'bob',
|
||||||
alive: true,
|
alive: false,
|
||||||
restartable: false,
|
restartable: false,
|
||||||
pid: 333,
|
pid: 333,
|
||||||
runtimeModel: 'opencode/minimax-m2.5-free',
|
runtimeModel: 'opencode/minimax-m2.5-free',
|
||||||
rssBytes: 456_000_000,
|
rssBytes: 456_000_000,
|
||||||
|
livenessKind: 'runtime_process_candidate',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows RSS for persisted OpenCode secondary lane runtime pids after the launch run is gone', async () => {
|
it('shows RSS for persisted OpenCode secondary lane host pids without treating historical bootstrap as live', async () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
(svc as any).configReader = {
|
(svc as any).configReader = {
|
||||||
getConfig: vi.fn(async () => ({
|
getConfig: vi.fn(async () => ({
|
||||||
|
|
@ -1399,12 +1400,14 @@ describe('TeamProvisioningService', () => {
|
||||||
expect(pidusage).toHaveBeenCalledWith([333], { maxage: 0 });
|
expect(pidusage).toHaveBeenCalledWith([333], { maxage: 0 });
|
||||||
expect(snapshot.members.bob).toMatchObject({
|
expect(snapshot.members.bob).toMatchObject({
|
||||||
memberName: 'bob',
|
memberName: 'bob',
|
||||||
alive: true,
|
alive: false,
|
||||||
restartable: false,
|
restartable: false,
|
||||||
pid: 333,
|
pid: 333,
|
||||||
providerId: 'opencode',
|
providerId: 'opencode',
|
||||||
runtimeModel: 'opencode/minimax-m2.5-free',
|
runtimeModel: 'opencode/minimax-m2.5-free',
|
||||||
rssBytes: 456_000_000,
|
rssBytes: 456_000_000,
|
||||||
|
historicalBootstrapConfirmed: true,
|
||||||
|
livenessKind: 'runtime_process_candidate',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -9892,7 +9895,7 @@ describe('TeamProvisioningService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clears stale OpenCode bridge launch failure when the runtime process is verified alive', async () => {
|
it('does not clear OpenCode bridge launch failure from process-only liveness', async () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
(svc as any).getLiveTeamAgentRuntimeMetadata = vi.fn(
|
(svc as any).getLiveTeamAgentRuntimeMetadata = vi.fn(
|
||||||
async () =>
|
async () =>
|
||||||
|
|
@ -9900,12 +9903,13 @@ describe('TeamProvisioningService', () => {
|
||||||
[
|
[
|
||||||
'bob',
|
'bob',
|
||||||
{
|
{
|
||||||
alive: true,
|
alive: false,
|
||||||
model: 'openrouter/google/gemini-2.5-flash',
|
model: 'openrouter/google/gemini-2.5-flash',
|
||||||
livenessKind: 'runtime_process',
|
livenessKind: 'runtime_process_candidate',
|
||||||
providerId: 'opencode',
|
providerId: 'opencode',
|
||||||
runtimeDiagnostic: 'OpenCode runtime process detected',
|
runtimeDiagnostic:
|
||||||
runtimeDiagnosticSeverity: 'info',
|
'OpenCode runtime process detected, but teammate bootstrap is not confirmed',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
|
|
@ -9922,17 +9926,18 @@ describe('TeamProvisioningService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.bob).toMatchObject({
|
expect(result.bob).toMatchObject({
|
||||||
status: 'online',
|
status: 'error',
|
||||||
launchState: 'runtime_pending_bootstrap',
|
launchState: 'failed_to_start',
|
||||||
runtimeAlive: true,
|
runtimeAlive: false,
|
||||||
hardFailure: false,
|
hardFailure: true,
|
||||||
hardFailureReason: undefined,
|
hardFailureReason: 'OpenCode bridge reported member launch failure',
|
||||||
error: undefined,
|
error: 'OpenCode bridge reported member launch failure',
|
||||||
runtimeModel: 'openrouter/google/gemini-2.5-flash',
|
runtimeModel: 'openrouter/google/gemini-2.5-flash',
|
||||||
livenessKind: 'runtime_process',
|
livenessKind: 'runtime_process_candidate',
|
||||||
runtimeDiagnostic: 'OpenCode runtime process detected',
|
runtimeDiagnostic:
|
||||||
runtimeDiagnosticSeverity: 'info',
|
'OpenCode runtime process detected, but teammate bootstrap is not confirmed',
|
||||||
livenessSource: 'process',
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
livenessSource: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ describe('resolveTeamMemberRuntimeLiveness', () => {
|
||||||
expect(result.pid).toBe(301);
|
expect(result.pid).toBe(301);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('promotes a live OpenCode runtime pid only when process identity matches', () => {
|
it('keeps a live OpenCode runtime pid as candidate until bootstrap is confirmed', () => {
|
||||||
const result = resolveTeamMemberRuntimeLiveness({
|
const result = resolveTeamMemberRuntimeLiveness({
|
||||||
teamName: 'demo',
|
teamName: 'demo',
|
||||||
memberName: 'bob',
|
memberName: 'bob',
|
||||||
|
|
@ -116,6 +116,36 @@ describe('resolveTeamMemberRuntimeLiveness', () => {
|
||||||
nowIso: NOW,
|
nowIso: NOW,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(result.alive).toBe(false);
|
||||||
|
expect(result.livenessKind).toBe('runtime_process_candidate');
|
||||||
|
expect(result.pidSource).toBe('opencode_bridge');
|
||||||
|
expect(result.pid).toBe(404);
|
||||||
|
expect(result.runtimeDiagnostic).toBe(
|
||||||
|
'OpenCode runtime process detected, but teammate bootstrap is not confirmed'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('promotes a live OpenCode runtime pid after bootstrap confirmation', () => {
|
||||||
|
const result = resolveTeamMemberRuntimeLiveness({
|
||||||
|
teamName: 'demo',
|
||||||
|
memberName: 'bob',
|
||||||
|
providerId: 'opencode',
|
||||||
|
persistedRuntimePid: 404,
|
||||||
|
persistedRuntimeSessionId: 'session-bob',
|
||||||
|
trackedSpawnStatus: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: false,
|
||||||
|
updatedAt: NOW,
|
||||||
|
},
|
||||||
|
processRows: [{ pid: 404, ppid: 1, command: 'opencode runtime host' }],
|
||||||
|
processTableAvailable: true,
|
||||||
|
nowIso: NOW,
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.alive).toBe(true);
|
expect(result.alive).toBe(true);
|
||||||
expect(result.livenessKind).toBe('runtime_process');
|
expect(result.livenessKind).toBe('runtime_process');
|
||||||
expect(result.pidSource).toBe('opencode_bridge');
|
expect(result.pidSource).toBe('opencode_bridge');
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,31 @@ describe('TeamTaskStallNotifier', () => {
|
||||||
await expect(notifier.notifyOpenCodeOwners('demo', [createAlert()])).resolves.toEqual([]);
|
await expect(notifier.notifyOpenCodeOwners('demo', [createAlert()])).resolves.toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not mark response-pending delivery as remediated even after runtime acceptance', async () => {
|
||||||
|
const relay = vi.fn(async () => ({
|
||||||
|
relayed: 1,
|
||||||
|
attempted: 1,
|
||||||
|
delivered: 1,
|
||||||
|
failed: 0,
|
||||||
|
lastDelivery: {
|
||||||
|
delivered: true,
|
||||||
|
accepted: true,
|
||||||
|
responsePending: true,
|
||||||
|
ledgerRecordId: 'active-ledger-record',
|
||||||
|
reason: 'opencode_delivery_response_pending',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
const notifier = new TeamTaskStallNotifier(
|
||||||
|
{ sendSystemNotificationToLead: vi.fn(async () => undefined) } as never,
|
||||||
|
{ relayOpenCodeMemberInboxMessages: relay } as never,
|
||||||
|
{ getMessagesFor: vi.fn(async () => []) } as never,
|
||||||
|
{ sendMessage: vi.fn(async () => ({ deliveredToInbox: true, messageId: 'msg' })) } as never
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(notifier.notifyOpenCodeOwners('demo', [createAlert()])).resolves.toEqual([]);
|
||||||
|
expect(relay).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('does not deliver runtime nudge when inbox write fails', async () => {
|
it('does not deliver runtime nudge when inbox write fails', async () => {
|
||||||
const relay = vi.fn(async () => ({ lastDelivery: { delivered: true } }));
|
const relay = vi.fn(async () => ({ lastDelivery: { delivered: true } }));
|
||||||
const notifier = new TeamTaskStallNotifier(
|
const notifier = new TeamTaskStallNotifier(
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,15 @@ import type { BoardTaskLogStreamResponse, TeamTask } from '../../../../../src/sh
|
||||||
|
|
||||||
const TEAM_NAME = 'relay-works-10';
|
const TEAM_NAME = 'relay-works-10';
|
||||||
const TASK_ID = '0b3a0624-5d66-4067-848e-5a74a1720c0d';
|
const TASK_ID = '0b3a0624-5d66-4067-848e-5a74a1720c0d';
|
||||||
|
const COORDINATION_TASK_ID = 'b5534868-0901-4c9e-9296-2b6e2059a08f';
|
||||||
const FIXTURE_PATH = path.resolve(
|
const FIXTURE_PATH = path.resolve(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'test/fixtures/team/opencode/relay-works-10-jack-projection-transcript.json'
|
'test/fixtures/team/opencode/relay-works-10-jack-projection-transcript.json'
|
||||||
);
|
);
|
||||||
|
|
||||||
const apiState = {
|
const apiState = {
|
||||||
getTaskLogStream: vi.fn<
|
getTaskLogStream:
|
||||||
(teamName: string, taskId: string) => Promise<BoardTaskLogStreamResponse>
|
vi.fn<(teamName: string, taskId: string) => Promise<BoardTaskLogStreamResponse>>(),
|
||||||
>(),
|
|
||||||
onTeamChange: vi.fn<(callback: (event: unknown, data: unknown) => void) => () => void>(),
|
onTeamChange: vi.fn<(callback: (event: unknown, data: unknown) => void) => () => void>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -54,17 +54,36 @@ const RELAY_WORKS_10_TASK: TeamTask = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RELAY_WORKS_10_COORDINATION_TASK: TeamTask = {
|
||||||
|
id: COORDINATION_TASK_ID,
|
||||||
|
displayId: 'b5534868',
|
||||||
|
subject: 'Split calculator implementation work',
|
||||||
|
owner: 'jack',
|
||||||
|
status: 'in_progress',
|
||||||
|
createdAt: '2026-04-24T20:28:58.000Z',
|
||||||
|
updatedAt: '2026-04-24T20:31:21.876Z',
|
||||||
|
workIntervals: [
|
||||||
|
{
|
||||||
|
startedAt: '2026-04-24T20:28:58.000Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
async function loadFixtureTranscript(): Promise<
|
async function loadFixtureTranscript(): Promise<
|
||||||
NonNullable<OpenCodeRuntimeTranscriptResponse['transcript']>
|
NonNullable<OpenCodeRuntimeTranscriptResponse['transcript']>
|
||||||
> {
|
> {
|
||||||
const parsed = JSON.parse(await readFile(FIXTURE_PATH, 'utf8')) as OpenCodeRuntimeTranscriptResponse;
|
const parsed = JSON.parse(
|
||||||
|
await readFile(FIXTURE_PATH, 'utf8')
|
||||||
|
) as OpenCodeRuntimeTranscriptResponse;
|
||||||
if (parsed.providerId !== 'opencode' || !parsed.transcript) {
|
if (parsed.providerId !== 'opencode' || !parsed.transcript) {
|
||||||
throw new Error('Invalid OpenCode transcript fixture');
|
throw new Error('Invalid OpenCode transcript fixture');
|
||||||
}
|
}
|
||||||
return parsed.transcript;
|
return parsed.transcript;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildFixtureStream(): Promise<BoardTaskLogStreamResponse> {
|
async function buildFixtureStream(
|
||||||
|
task: TeamTask = RELAY_WORKS_10_TASK
|
||||||
|
): Promise<BoardTaskLogStreamResponse> {
|
||||||
const transcript = await loadFixtureTranscript();
|
const transcript = await loadFixtureTranscript();
|
||||||
const source = new OpenCodeTaskLogStreamSource(
|
const source = new OpenCodeTaskLogStreamSource(
|
||||||
{
|
{
|
||||||
|
|
@ -72,13 +91,13 @@ async function buildFixtureStream(): Promise<BoardTaskLogStreamResponse> {
|
||||||
} as never,
|
} as never,
|
||||||
{ resolve: async () => '/tmp/agent_teams_orchestrator' },
|
{ resolve: async () => '/tmp/agent_teams_orchestrator' },
|
||||||
{
|
{
|
||||||
getTasks: vi.fn(async () => [RELAY_WORKS_10_TASK]),
|
getTasks: vi.fn(async () => [task]),
|
||||||
getDeletedTasks: vi.fn(async () => []),
|
getDeletedTasks: vi.fn(async () => []),
|
||||||
} as never,
|
} as never,
|
||||||
new BoardTaskExactLogChunkBuilder(),
|
new BoardTaskExactLogChunkBuilder(),
|
||||||
{ readTaskRecords: vi.fn(async () => []) }
|
{ readTaskRecords: vi.fn(async () => []) }
|
||||||
);
|
);
|
||||||
const stream = await source.getTaskLogStream(TEAM_NAME, TASK_ID);
|
const stream = await source.getTaskLogStream(TEAM_NAME, task.id);
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
throw new Error('Expected OpenCode fixture stream');
|
throw new Error('Expected OpenCode fixture stream');
|
||||||
}
|
}
|
||||||
|
|
@ -138,4 +157,44 @@ describe('TaskLogStreamSection OpenCode real fixture e2e', () => {
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders real OpenCode native tool rows when the task stream includes them', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
apiState.onTeamChange.mockImplementation(() => () => undefined);
|
||||||
|
apiState.getTaskLogStream.mockResolvedValueOnce(
|
||||||
|
await buildFixtureStream(RELAY_WORKS_10_COORDINATION_TASK)
|
||||||
|
);
|
||||||
|
|
||||||
|
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: COORDINATION_TASK_ID,
|
||||||
|
liveEnabled: false,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await flushMicrotasks();
|
||||||
|
await flushMicrotasks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = host.textContent ?? '';
|
||||||
|
expect(text).toContain('Task Log Stream');
|
||||||
|
expect(text).toContain('read');
|
||||||
|
expect(text).toContain('bash');
|
||||||
|
expect(text).toContain('Разбил работу на мелкие задачи');
|
||||||
|
expect(text).not.toContain('SendMessage');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await flushMicrotasks();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue