fix(changes): avoid caching stale opencode contracts
This commit is contained in:
parent
7b5924c8bd
commit
ef82755b17
2 changed files with 99 additions and 7 deletions
|
|
@ -14,6 +14,7 @@ import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { JsonTaskChangeSummaryCacheRepository } from './cache/JsonTaskChangeSummaryCacheRepository';
|
import { JsonTaskChangeSummaryCacheRepository } from './cache/JsonTaskChangeSummaryCacheRepository';
|
||||||
|
import { OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION } from './opencode/bridge/OpenCodeBridgeCommandContract';
|
||||||
import {
|
import {
|
||||||
getOpenCodeLaneScopedRuntimeFilePath,
|
getOpenCodeLaneScopedRuntimeFilePath,
|
||||||
getOpenCodeTeamRuntimeDirectory,
|
getOpenCodeTeamRuntimeDirectory,
|
||||||
|
|
@ -533,9 +534,25 @@ export class ChangeExtractorService {
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
});
|
});
|
||||||
|
const evidenceContractVersion =
|
||||||
|
typeof result.opencodeTaskLedgerEvidenceContractVersion === 'number' &&
|
||||||
|
Number.isInteger(result.opencodeTaskLedgerEvidenceContractVersion)
|
||||||
|
? result.opencodeTaskLedgerEvidenceContractVersion
|
||||||
|
: 0;
|
||||||
|
const hasExpectedEvidenceContract =
|
||||||
|
evidenceContractVersion >= OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION;
|
||||||
|
const diagnostics = hasExpectedEvidenceContract
|
||||||
|
? (result.diagnostics ?? [])
|
||||||
|
: [
|
||||||
|
`OpenCode task ledger evidence contract is unsupported or missing: ${evidenceContractVersion}.`,
|
||||||
|
...(result.diagnostics ?? []),
|
||||||
|
];
|
||||||
void appendOpenCodeTaskChangeDiag({
|
void appendOpenCodeTaskChangeDiag({
|
||||||
event: 'backfill_result',
|
event: 'backfill_result',
|
||||||
reason: this.classifyOpenCodeBackfillResult(result),
|
reason:
|
||||||
|
!hasExpectedEvidenceContract && result.importedEvents <= 0
|
||||||
|
? 'unsupported-evidence-contract'
|
||||||
|
: this.classifyOpenCodeBackfillResult(result),
|
||||||
teamName: input.teamName,
|
teamName: input.teamName,
|
||||||
taskId: input.taskId,
|
taskId: input.taskId,
|
||||||
displayId: input.taskMeta?.displayId ?? null,
|
displayId: input.taskMeta?.displayId ?? null,
|
||||||
|
|
@ -546,6 +563,7 @@ export class ChangeExtractorService {
|
||||||
deliveryRecordCount: deliveryContextRecords.length,
|
deliveryRecordCount: deliveryContextRecords.length,
|
||||||
deliveryContextFingerprint: deliveryContextPayload.hash,
|
deliveryContextFingerprint: deliveryContextPayload.hash,
|
||||||
result: {
|
result: {
|
||||||
|
opencodeTaskLedgerEvidenceContractVersion: evidenceContractVersion,
|
||||||
attributionMode: result.attributionMode ?? OPEN_CODE_AUTO_BACKFILL_ATTRIBUTION_MODE,
|
attributionMode: result.attributionMode ?? OPEN_CODE_AUTO_BACKFILL_ATTRIBUTION_MODE,
|
||||||
outcome: result.outcome,
|
outcome: result.outcome,
|
||||||
dryRun: result.dryRun,
|
dryRun: result.dryRun,
|
||||||
|
|
@ -555,13 +573,14 @@ export class ChangeExtractorService {
|
||||||
importedEvents: result.importedEvents,
|
importedEvents: result.importedEvents,
|
||||||
skippedEvents: result.skippedEvents,
|
skippedEvents: result.skippedEvents,
|
||||||
},
|
},
|
||||||
diagnostics: (result.diagnostics ?? []).slice(0, 25),
|
diagnostics: diagnostics.slice(0, 25),
|
||||||
notices: (result.notices ?? []).slice(0, 25),
|
notices: (result.notices ?? []).slice(0, 25),
|
||||||
}).catch(() => undefined);
|
}).catch(() => undefined);
|
||||||
const backfilled =
|
const backfilled =
|
||||||
result.importedEvents > 0 ||
|
result.importedEvents > 0 ||
|
||||||
result.outcome === 'imported' ||
|
(hasExpectedEvidenceContract &&
|
||||||
(result.outcome === 'duplicates-only' && result.candidateEvents > 0);
|
(result.outcome === 'imported' ||
|
||||||
|
(result.outcome === 'duplicates-only' && result.candidateEvents > 0)));
|
||||||
|
|
||||||
if (result.importedEvents > 0) {
|
if (result.importedEvents > 0) {
|
||||||
await this.invalidateTaskChangeSummaries(input.teamName, [input.taskId], {
|
await this.invalidateTaskChangeSummaries(input.teamName, [input.taskId], {
|
||||||
|
|
@ -569,7 +588,7 @@ export class ChangeExtractorService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (backfilled || deliveryContextRecords.length === 0) {
|
if ((hasExpectedEvidenceContract && backfilled) || deliveryContextRecords.length === 0) {
|
||||||
this.openCodeBackfillCache.set(cacheKey, {
|
this.openCodeBackfillCache.set(cacheKey, {
|
||||||
backfilledAt: backfilled ? Date.now() : 0,
|
backfilledAt: backfilled ? Date.now() : 0,
|
||||||
expiresAt: Date.now() + this.openCodeBackfillCacheTtl,
|
expiresAt: Date.now() + this.openCodeBackfillCacheTtl,
|
||||||
|
|
@ -578,9 +597,9 @@ export class ChangeExtractorService {
|
||||||
this.openCodeBackfillCache.delete(cacheKey);
|
this.openCodeBackfillCache.delete(cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.diagnostics.length > 0 && result.outcome !== 'no-history') {
|
if (diagnostics.length > 0 && result.outcome !== 'no-history') {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`OpenCode ledger backfill for ${input.teamName}/${input.taskId}: ${result.outcome}; ${result.diagnostics.join('; ')}`
|
`OpenCode ledger backfill for ${input.teamName}/${input.taskId}: ${result.outcome}; ${diagnostics.join('; ')}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return { attempted: true, backfilled };
|
return { attempted: true, backfilled };
|
||||||
|
|
|
||||||
|
|
@ -1752,4 +1752,77 @@ describe('ChangeExtractorService', () => {
|
||||||
);
|
);
|
||||||
expect(backfillOpenCodeTaskLedger.mock.calls[1]?.[0]?.deliveryContextHash).toMatch(/^[a-f0-9]{64}$/);
|
expect(backfillOpenCodeTaskLedger.mock.calls[1]?.[0]?.deliveryContextHash).toMatch(/^[a-f0-9]{64}$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not cache duplicates-only OpenCode backfill from an old evidence contract', async () => {
|
||||||
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'change-extractor-service-'));
|
||||||
|
setClaudeBasePathOverride(tmpDir);
|
||||||
|
await writeTaskFile(tmpDir, { displayId: 'abc12345', owner: 'bob' });
|
||||||
|
const projectDir = path.join(tmpDir, 'project-dir');
|
||||||
|
const projectPath = path.join(tmpDir, 'repo');
|
||||||
|
await fs.mkdir(projectDir, { recursive: true });
|
||||||
|
await fs.mkdir(projectPath, { recursive: true });
|
||||||
|
await writeOpenCodeDeliveryLedger(tmpDir);
|
||||||
|
|
||||||
|
const backfillOpenCodeTaskLedger = vi.fn(async (input: any) => ({
|
||||||
|
schemaVersion: 1,
|
||||||
|
providerId: 'opencode',
|
||||||
|
teamName: input.teamName,
|
||||||
|
taskId: input.taskId,
|
||||||
|
projectDir: input.projectDir,
|
||||||
|
workspaceRoot: input.workspaceRoot,
|
||||||
|
dryRun: false,
|
||||||
|
attributionMode: input.attributionMode,
|
||||||
|
scannedSessions: 1,
|
||||||
|
scannedToolparts: 1,
|
||||||
|
candidateEvents: 1,
|
||||||
|
importedEvents: 0,
|
||||||
|
skippedEvents: 1,
|
||||||
|
outcome: 'duplicates-only',
|
||||||
|
notices: [],
|
||||||
|
diagnostics: [],
|
||||||
|
}));
|
||||||
|
const workerClient = {
|
||||||
|
isAvailable: vi.fn(() => true),
|
||||||
|
computeTaskChanges: vi.fn(async () =>
|
||||||
|
makeTaskChangeResult(TASK_ID, { content: '', confidence: 'fallback' })
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const service = new ChangeExtractorService(
|
||||||
|
{
|
||||||
|
getLogSourceWatchContext: vi.fn(async () => ({
|
||||||
|
projectDir,
|
||||||
|
projectPath,
|
||||||
|
sessionIds: [],
|
||||||
|
})),
|
||||||
|
findLogFileRefsForTask: vi.fn(async () => []),
|
||||||
|
findMemberLogPaths: vi.fn(async () => []),
|
||||||
|
} as any,
|
||||||
|
{
|
||||||
|
parseBoundaries: vi.fn(async () => ({
|
||||||
|
boundaries: [],
|
||||||
|
scopes: [],
|
||||||
|
isSingleTaskSession: true,
|
||||||
|
detectedMechanism: 'none' as const,
|
||||||
|
})),
|
||||||
|
} as any,
|
||||||
|
{ getConfig: vi.fn(async () => ({ projectPath })) } as any,
|
||||||
|
undefined,
|
||||||
|
workerClient as any,
|
||||||
|
{ backfillOpenCodeTaskLedger } as any,
|
||||||
|
{ getMeta: vi.fn(async () => ({ providerId: 'opencode' })) } as any
|
||||||
|
);
|
||||||
|
|
||||||
|
await service.getTaskChanges(TEAM_NAME, TASK_ID, {
|
||||||
|
owner: 'bob',
|
||||||
|
status: 'completed',
|
||||||
|
});
|
||||||
|
await service.getTaskChanges(TEAM_NAME, TASK_ID, {
|
||||||
|
owner: 'bob',
|
||||||
|
status: 'completed',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(backfillOpenCodeTaskLedger).toHaveBeenCalledTimes(2);
|
||||||
|
expect(workerClient.computeTaskChanges).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue