From 565362a911018657cc8a158a7db4fa707e69ea17 Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 14 May 2026 03:05:54 +0300 Subject: [PATCH] fix(team): refresh task logs from opencode session evidence --- .../services/team/TeamProvisioningService.ts | 28 +++++ .../stream/BoardTaskLogStreamService.ts | 13 ++- .../team/BoardTaskLogStreamService.test.ts | 104 ++++++++++++++++++ .../team/TeamProvisioningService.test.ts | 104 ++++++++++++++++++ 4 files changed, 245 insertions(+), 4 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index b3312349..e41cca39 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -8827,6 +8827,30 @@ export class TeamProvisioningService { } } + private emitOpenCodePromptDeliveryTaskLogChange( + record: OpenCodePromptDeliveryLedgerRecord, + detail: string + ): void { + if (!record.runtimeSessionId?.trim() || record.taskRefs.length === 0) { + return; + } + const taskIds = new Set( + record.taskRefs + .map((taskRef) => taskRef.taskId?.trim() || taskRef.displayId?.trim()) + .filter((taskId): taskId is string => Boolean(taskId)) + ); + for (const taskId of taskIds) { + this.teamChangeEmitter?.({ + type: 'task-log-change', + teamName: record.teamName, + ...(record.runId ? { runId: record.runId } : {}), + taskId, + detail, + taskSignalKind: 'log', + }); + } + } + private async handleOpenCodeRuntimeDeliveryUserFacingSideEffects( record: OpenCodePromptDeliveryLedgerRecord ): Promise { @@ -9964,6 +9988,10 @@ export class TeamProvisioningService { reason: promptAccepted ? responseObservation?.reason : result.diagnostics[0], now: nowIso(), }); + this.emitOpenCodePromptDeliveryTaskLogChange( + ledgerRecord, + 'opencode-prompt-delivery-session-evidence' + ); let proof = await this.applyOpenCodeVisibleDestinationProof({ ledger, ledgerRecord, diff --git a/src/main/services/team/taskLogs/stream/BoardTaskLogStreamService.ts b/src/main/services/team/taskLogs/stream/BoardTaskLogStreamService.ts index 7c180fea..3c8976b1 100644 --- a/src/main/services/team/taskLogs/stream/BoardTaskLogStreamService.ts +++ b/src/main/services/team/taskLogs/stream/BoardTaskLogStreamService.ts @@ -2201,10 +2201,6 @@ export class BoardTaskLogStreamService { taskId: string, records: BoardTaskActivityRecord[] ): Promise { - if (records.some((record) => record.linkKind === 'execution')) { - return false; - } - try { const [activeTasks, deletedTasks, metaMembers, config] = await Promise.all([ this.taskReader.getTasks(teamName).catch(() => []), @@ -2219,6 +2215,15 @@ export class BoardTaskLogStreamService { } const normalizedOwner = normalizeMemberName(ownerName); + const hasOwnerExecution = records.some( + (record) => + record.linkKind === 'execution' && + normalizeMemberName(record.actor.memberName ?? '') === normalizedOwner + ); + if (hasOwnerExecution) { + return false; + } + const member = [...metaMembers, ...(config?.members ?? [])].find( (candidate) => normalizeMemberName(candidate.name) === normalizedOwner ); diff --git a/test/main/services/team/BoardTaskLogStreamService.test.ts b/test/main/services/team/BoardTaskLogStreamService.test.ts index 8f758be8..510db92b 100644 --- a/test/main/services/team/BoardTaskLogStreamService.test.ts +++ b/test/main/services/team/BoardTaskLogStreamService.test.ts @@ -273,6 +273,110 @@ describe('BoardTaskLogStreamService', () => { }); }); + it('does not suppress exact OpenCode fallback because of unrelated execution records', async () => { + const lead = { + role: 'lead' as const, + sessionId: 'session-lead', + isSidechain: false, + }; + const baseCandidate = makeCandidate( + 'c1', + '2026-04-12T16:00:00.000Z', + lead, + 'tool-board' + ); + const executionRecord: BoardTaskActivityRecord = { + ...baseCandidate.records[0]!, + linkKind: 'execution', + }; + const candidate: BoardTaskExactLogBundleCandidate = { + ...baseCandidate, + records: [executionRecord], + linkKinds: ['execution'], + }; + const runtimeFallbackSource = { + getTaskLogStream: vi.fn(async () => ({ + participants: [ + { + key: 'member:jack', + label: 'jack', + role: 'member' as const, + isLead: false, + isSidechain: true, + }, + ], + defaultFilter: 'member:jack', + segments: [ + { + id: 'opencode:demo:task-a:jack:session-opencode', + participantKey: 'member:jack', + actor: { + memberName: 'jack', + role: 'member' as const, + sessionId: 'session-opencode', + isSidechain: true, + }, + startTimestamp: '2026-04-12T16:01:00.000Z', + endTimestamp: '2026-04-12T16:02:00.000Z', + chunks: [{ id: 'chunk-exact-opencode' }], + }, + ], + source: 'opencode_runtime_attribution' as const, + runtimeProjection: { + provider: 'opencode' as const, + mode: 'attribution' as const, + attributionRecordCount: 1, + projectedMessageCount: 2, + }, + })), + }; + const service = new BoardTaskLogStreamService( + { + getTaskRecords: vi.fn(async () => candidate.records), + } as never, + { + selectSummaries: vi.fn(() => [candidate]), + } as never, + { + parseFiles: vi.fn(async () => new Map([['/tmp/task.jsonl', []]])), + } as never, + { + selectDetail: vi.fn(() => ({ + id: 'c1', + timestamp: '2026-04-12T16:00:00.000Z', + actor: lead, + source: candidate.source, + records: candidate.records, + filteredMessages: [makeMessage('c1', '2026-04-12T16:00:00.000Z', 'lead execution')], + })), + } as never, + { + buildBundleChunks: vi.fn((messages: ParsedMessage[]) => [{ id: messages[0]?.uuid }]), + } as never, + { + getTasks: vi.fn(async () => [{ id: 'task-a', owner: 'jack' }]), + getDeletedTasks: vi.fn(async () => []), + } as never, + undefined as never, + runtimeFallbackSource as never, + { + getMembers: vi.fn(async () => [{ name: 'jack', providerId: 'opencode' }]), + } as never, + { + getConfig: vi.fn(async () => null), + } as never + ); + + const response = await service.getTaskLogStream('demo', 'task-a'); + + expect(runtimeFallbackSource.getTaskLogStream).toHaveBeenCalledWith('demo', 'task-a'); + expect(response.source).toBe('mixed_transcript_opencode_runtime'); + expect(response.segments.map((segment) => segment.id)).toEqual([ + 'lead:c1:c1', + 'opencode:demo:task-a:jack:session-opencode', + ]); + }); + it('does not probe OpenCode runtime for non-OpenCode task owners', async () => { const lead = { role: 'lead' as const, diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index 33b9c5ea..1463f425 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -6050,6 +6050,110 @@ describe('TeamProvisioningService', () => { ); }); + it('emits a narrow task-log signal when OpenCode prompt delivery records exact session evidence', async () => { + const svc = new TeamProvisioningService(); + const emitter = vi.fn(); + svc.setTeamChangeEmitter(emitter); + const sendMessageToMember = vi.fn(async (input: Record) => ({ + ok: true, + providerId: 'opencode', + memberName: String(input.memberName), + sessionId: 'oc-session-bob', + prePromptCursor: 'cursor-before', + responseObservation: { + state: 'pending', + deliveredUserMessageId: 'oc-user-1', + assistantMessageId: null, + toolCallNames: [], + visibleMessageToolCallId: null, + visibleReplyMessageId: null, + visibleReplyCorrelation: null, + latestAssistantPreview: null, + reason: 'assistant_response_pending', + }, + diagnostics: [], + })); + const registry = new TeamRuntimeAdapterRegistry([ + { + providerId: 'opencode', + prepare: vi.fn(), + launch: vi.fn(), + reconcile: vi.fn(), + stop: vi.fn(), + sendMessageToMember, + } as any, + ]); + svc.setRuntimeAdapterRegistry(registry); + + (svc as any).getTrackedRunId = vi.fn(() => 'run-1'); + (svc as any).provisioningRunByTeam.set('team-a', 'run-1'); + (svc as any).setSecondaryRuntimeRun({ + teamName: 'team-a', + runId: 'opencode-run-bob', + providerId: 'opencode', + laneId: 'secondary:opencode:bob', + memberName: 'bob', + cwd: '/repo', + }); + await writeDefaultBobOpenCodeBootstrapEvidence(); + (svc as any).configReader = { + getConfig: vi.fn(async () => ({ + projectPath: '/repo', + members: [ + { name: 'team-lead', providerId: 'codex', model: 'gpt-5.4' }, + { name: 'bob', providerId: 'opencode', model: 'minimax-m2.5-free' }, + ], + })), + }; + (svc as any).teamMetaStore = { + getMeta: vi.fn(async () => ({ + launchIdentity: { providerId: 'codex' }, + providerId: 'codex', + })), + }; + (svc as any).membersMetaStore = { + getMembers: vi.fn(async () => [ + { + name: 'bob', + providerId: 'opencode', + model: 'opencode/minimax-m2.5-free', + }, + ]), + }; + + await expect( + svc.deliverOpenCodeMemberMessage('team-a', { + memberName: 'bob', + text: 'hello bob', + messageId: 'msg-ledger-session', + source: 'watcher', + inboxTimestamp: '2026-04-25T10:00:00.000Z', + taskRefs: [ + { + taskId: 'task-a', + displayId: 'task-a', + teamName: 'team-a', + }, + ], + }) + ).resolves.toMatchObject({ + delivered: true, + responsePending: true, + responseState: 'pending', + }); + + expect(emitter).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'task-log-change', + teamName: 'team-a', + runId: 'opencode-run-bob', + taskId: 'task-a', + detail: 'opencode-prompt-delivery-session-evidence', + taskSignalKind: 'log', + }) + ); + }); + it('retries due stale OpenCode sessions instead of observing forever', async () => { const svc = new TeamProvisioningService(); const sendMessageToMember = vi.fn(async (input: Record) => ({