fix(team): refresh task logs from opencode session evidence
This commit is contained in:
parent
dd412355d2
commit
565362a911
4 changed files with 245 additions and 4 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>) => ({
|
||||
|
|
|
|||
Loading…
Reference in a new issue