fix(team): refresh task logs from opencode session evidence

This commit is contained in:
777genius 2026-05-14 03:05:54 +03:00
parent dd412355d2
commit 565362a911
4 changed files with 245 additions and 4 deletions

View file

@ -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<void> {
@ -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,

View file

@ -2201,10 +2201,6 @@ export class BoardTaskLogStreamService {
taskId: string,
records: BoardTaskActivityRecord[]
): Promise<boolean> {
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
);

View file

@ -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,

View file

@ -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<string, unknown>) => ({
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<string, unknown>) => ({