fix(changes): key opencode backfill by context hash

This commit is contained in:
777genius 2026-04-28 23:01:42 +03:00
parent 53012ed623
commit 33a8a5fabc

View file

@ -81,6 +81,11 @@ interface OpenCodeDeliveryContextTempFile {
cleanup: () => Promise<void>; cleanup: () => Promise<void>;
} }
interface OpenCodeDeliveryContextPayload {
rawContext: string;
hash: string;
}
export class ChangeExtractorService { export class ChangeExtractorService {
private cache = new Map<string, CacheEntry>(); private cache = new Map<string, CacheEntry>();
private taskChangeSummaryCache = new Map<string, TaskChangeSummaryCacheEntry>(); private taskChangeSummaryCache = new Map<string, TaskChangeSummaryCacheEntry>();
@ -422,12 +427,16 @@ export class ChangeExtractorService {
input.teamName, input.teamName,
input.taskId input.taskId
); );
const deliveryContextPayload = this.buildOpenCodeDeliveryContextPayload(
input.teamName,
input.taskId,
deliveryContextRecords
);
const backfillMemberName = this.resolveOpenCodeBackfillMemberName( const backfillMemberName = this.resolveOpenCodeBackfillMemberName(
input.effectiveOptions.owner, input.effectiveOptions.owner,
deliveryContextRecords deliveryContextRecords
); );
const deliveryContextFingerprint = const deliveryContextFingerprint = deliveryContextPayload.hash;
this.hashOpenCodeDeliveryContextRecords(deliveryContextRecords);
const cacheKey = this.buildOpenCodeBackfillCacheKey({ const cacheKey = this.buildOpenCodeBackfillCacheKey({
teamName: input.teamName, teamName: input.teamName,
@ -479,6 +488,7 @@ export class ChangeExtractorService {
workspaceRoot, workspaceRoot,
cacheKey, cacheKey,
deliveryContextRecords, deliveryContextRecords,
deliveryContextPayload,
sourceGeneration, sourceGeneration,
backfillMemberName backfillMemberName
).finally(() => { ).finally(() => {
@ -496,13 +506,15 @@ export class ChangeExtractorService {
deliveryContextRecords: Awaited< deliveryContextRecords: Awaited<
ReturnType<ChangeExtractorService['readOpenCodeDeliveryContextRecords']> ReturnType<ChangeExtractorService['readOpenCodeDeliveryContextRecords']>
>, >,
deliveryContextPayload: OpenCodeDeliveryContextPayload,
sourceGeneration: string | null, sourceGeneration: string | null,
backfillMemberName?: string backfillMemberName?: string
): Promise<OpenCodeBackfillAttempt> { ): Promise<OpenCodeBackfillAttempt> {
const deliveryContext = await this.createOpenCodeDeliveryContextTempFile( const deliveryContext = await this.createOpenCodeDeliveryContextTempFile(
input.teamName, input.teamName,
input.taskId, input.taskId,
deliveryContextRecords deliveryContextRecords,
deliveryContextPayload
); );
try { try {
const result = await this.openCodeLedgerBackfillPort!.backfillOpenCodeTaskLedger({ const result = await this.openCodeLedgerBackfillPort!.backfillOpenCodeTaskLedger({
@ -532,7 +544,7 @@ export class ChangeExtractorService {
workspaceRoot, workspaceRoot,
sourceGeneration, sourceGeneration,
deliveryRecordCount: deliveryContextRecords.length, deliveryRecordCount: deliveryContextRecords.length,
deliveryContextFingerprint: this.hashOpenCodeDeliveryContextRecords(deliveryContextRecords), deliveryContextFingerprint: deliveryContextPayload.hash,
result: { result: {
attributionMode: result.attributionMode ?? OPEN_CODE_AUTO_BACKFILL_ATTRIBUTION_MODE, attributionMode: result.attributionMode ?? OPEN_CODE_AUTO_BACKFILL_ATTRIBUTION_MODE,
outcome: result.outcome, outcome: result.outcome,
@ -586,7 +598,7 @@ export class ChangeExtractorService {
projectDir, projectDir,
workspaceRoot, workspaceRoot,
deliveryRecordCount: deliveryContextRecords.length, deliveryRecordCount: deliveryContextRecords.length,
deliveryContextFingerprint: this.hashOpenCodeDeliveryContextRecords(deliveryContextRecords), deliveryContextFingerprint: deliveryContextPayload.hash,
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
}).catch(() => undefined); }).catch(() => undefined);
if (deliveryContextRecords.length === 0) { if (deliveryContextRecords.length === 0) {
@ -664,7 +676,8 @@ export class ChangeExtractorService {
private async createOpenCodeDeliveryContextTempFile( private async createOpenCodeDeliveryContextTempFile(
teamName: string, teamName: string,
taskId: string, taskId: string,
records: Awaited<ReturnType<ChangeExtractorService['readOpenCodeDeliveryContextRecords']>> records: Awaited<ReturnType<ChangeExtractorService['readOpenCodeDeliveryContextRecords']>>,
payload = this.buildOpenCodeDeliveryContextPayload(teamName, taskId, records)
): Promise<OpenCodeDeliveryContextTempFile> { ): Promise<OpenCodeDeliveryContextTempFile> {
if (records.length === 0) { if (records.length === 0) {
return { filePath: null, hash: null, cleanup: async () => undefined }; return { filePath: null, hash: null, cleanup: async () => undefined };
@ -673,6 +686,21 @@ export class ChangeExtractorService {
const dir = await mkdtemp(path.join(os.tmpdir(), 'claude-team-opencode-ledger-context-')); const dir = await mkdtemp(path.join(os.tmpdir(), 'claude-team-opencode-ledger-context-'));
await chmod(dir, 0o700).catch(() => undefined); await chmod(dir, 0o700).catch(() => undefined);
const filePath = path.join(dir, 'delivery-context.json'); const filePath = path.join(dir, 'delivery-context.json');
await writeFile(filePath, payload.rawContext, { encoding: 'utf8', mode: 0o600 });
return {
filePath,
hash: payload.hash,
cleanup: async () => {
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
},
};
}
private buildOpenCodeDeliveryContextPayload(
teamName: string,
taskId: string,
records: Awaited<ReturnType<ChangeExtractorService['readOpenCodeDeliveryContextRecords']>>
): OpenCodeDeliveryContextPayload {
const rawContext = `${JSON.stringify( const rawContext = `${JSON.stringify(
{ {
schemaVersion: 1, schemaVersion: 1,
@ -683,13 +711,9 @@ export class ChangeExtractorService {
null, null,
2 2
)}\n`; )}\n`;
await writeFile(filePath, rawContext, { encoding: 'utf8', mode: 0o600 });
return { return {
filePath,
hash: createHash('sha256').update(rawContext).digest('hex'), hash: createHash('sha256').update(rawContext).digest('hex'),
cleanup: async () => { rawContext,
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
},
}; };
} }
@ -798,50 +822,6 @@ export class ChangeExtractorService {
return laneIds.sort((left, right) => left.localeCompare(right)); return laneIds.sort((left, right) => left.localeCompare(right));
} }
private hashOpenCodeDeliveryContextRecords(
records: Awaited<ReturnType<ChangeExtractorService['readOpenCodeDeliveryContextRecords']>>
): string {
const stableRecords = records
.map((record) => ({
memberName: record.memberName,
laneId: record.laneId ?? '',
runtimeSessionId: record.runtimeSessionId ?? '',
inboxMessageId: record.inboxMessageId ?? '',
deliveredUserMessageId: record.deliveredUserMessageId ?? '',
taskRefs: record.taskRefs
.map((taskRef) => ({
taskId: taskRef.taskId,
displayId: taskRef.displayId,
teamName: taskRef.teamName,
}))
.sort((left, right) =>
`${left.teamName}\0${left.taskId}\0${left.displayId}`.localeCompare(
`${right.teamName}\0${right.taskId}\0${right.displayId}`
)
),
}))
.sort((left, right) =>
[
left.laneId,
left.memberName,
left.runtimeSessionId,
left.inboxMessageId,
left.deliveredUserMessageId,
]
.join('\0')
.localeCompare(
[
right.laneId,
right.memberName,
right.runtimeSessionId,
right.inboxMessageId,
right.deliveredUserMessageId,
].join('\0')
)
);
return createHash('sha256').update(JSON.stringify(stableRecords)).digest('hex');
}
private async readOpenCodePromptDeliveryLedgerRecords( private async readOpenCodePromptDeliveryLedgerRecords(
filePath: string filePath: string
): Promise<OpenCodePromptDeliveryLedgerRecord[]> { ): Promise<OpenCodePromptDeliveryLedgerRecord[]> {