feat: add opencode task change diagnostics
This commit is contained in:
parent
fe3a1a3cb3
commit
9a3e17ce70
4 changed files with 172 additions and 2 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
import { getTasksBasePath, getTeamsBasePath } from '@main/utils/pathDecoder';
|
import { getTasksBasePath, getTeamsBasePath } from '@main/utils/pathDecoder';
|
||||||
|
import { appendOpenCodeTaskChangeDiag } from '@main/utils/openCodeTaskChangeDiagLog';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
import { resolveTaskChangePresenceFromResult } from '@shared/utils/taskChangePresence';
|
import { resolveTaskChangePresenceFromResult } from '@shared/utils/taskChangePresence';
|
||||||
import {
|
import {
|
||||||
|
|
@ -444,7 +445,8 @@ export class ChangeExtractorService {
|
||||||
projectDir,
|
projectDir,
|
||||||
workspaceRoot,
|
workspaceRoot,
|
||||||
cacheKey,
|
cacheKey,
|
||||||
deliveryContextRecords
|
deliveryContextRecords,
|
||||||
|
sourceGeneration
|
||||||
).finally(() => {
|
).finally(() => {
|
||||||
this.openCodeBackfillInFlight.delete(cacheKey);
|
this.openCodeBackfillInFlight.delete(cacheKey);
|
||||||
});
|
});
|
||||||
|
|
@ -467,7 +469,8 @@ export class ChangeExtractorService {
|
||||||
cacheKey: string,
|
cacheKey: string,
|
||||||
deliveryContextRecords: Awaited<
|
deliveryContextRecords: Awaited<
|
||||||
ReturnType<ChangeExtractorService['readOpenCodeDeliveryContextRecords']>
|
ReturnType<ChangeExtractorService['readOpenCodeDeliveryContextRecords']>
|
||||||
>
|
>,
|
||||||
|
sourceGeneration: string | null
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const deliveryContext = await this.createOpenCodeDeliveryContextTempFile(
|
const deliveryContext = await this.createOpenCodeDeliveryContextTempFile(
|
||||||
input.teamName,
|
input.teamName,
|
||||||
|
|
@ -486,6 +489,31 @@ export class ChangeExtractorService {
|
||||||
attributionMode: OPEN_CODE_AUTO_BACKFILL_ATTRIBUTION_MODE,
|
attributionMode: OPEN_CODE_AUTO_BACKFILL_ATTRIBUTION_MODE,
|
||||||
...(deliveryContext.filePath ? { deliveryContextPath: deliveryContext.filePath } : {}),
|
...(deliveryContext.filePath ? { deliveryContextPath: deliveryContext.filePath } : {}),
|
||||||
});
|
});
|
||||||
|
void appendOpenCodeTaskChangeDiag({
|
||||||
|
event: 'backfill_result',
|
||||||
|
reason: this.classifyOpenCodeBackfillResult(result),
|
||||||
|
teamName: input.teamName,
|
||||||
|
taskId: input.taskId,
|
||||||
|
displayId: input.taskMeta?.displayId ?? null,
|
||||||
|
memberName: input.effectiveOptions.owner ?? null,
|
||||||
|
projectDir,
|
||||||
|
workspaceRoot,
|
||||||
|
sourceGeneration,
|
||||||
|
deliveryRecordCount: deliveryContextRecords.length,
|
||||||
|
deliveryContextFingerprint: this.hashOpenCodeDeliveryContextRecords(deliveryContextRecords),
|
||||||
|
result: {
|
||||||
|
attributionMode: result.attributionMode ?? OPEN_CODE_AUTO_BACKFILL_ATTRIBUTION_MODE,
|
||||||
|
outcome: result.outcome,
|
||||||
|
dryRun: result.dryRun,
|
||||||
|
scannedSessions: result.scannedSessions,
|
||||||
|
scannedToolparts: result.scannedToolparts,
|
||||||
|
candidateEvents: result.candidateEvents,
|
||||||
|
importedEvents: result.importedEvents,
|
||||||
|
skippedEvents: result.skippedEvents,
|
||||||
|
},
|
||||||
|
diagnostics: (result.diagnostics ?? []).slice(0, 25),
|
||||||
|
notices: (result.notices ?? []).slice(0, 25),
|
||||||
|
}).catch(() => undefined);
|
||||||
const backfilled =
|
const backfilled =
|
||||||
result.importedEvents > 0 ||
|
result.importedEvents > 0 ||
|
||||||
result.outcome === 'imported' ||
|
result.outcome === 'imported' ||
|
||||||
|
|
@ -516,6 +544,19 @@ export class ChangeExtractorService {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`OpenCode ledger backfill failed for ${input.teamName}/${input.taskId}: ${error instanceof Error ? error.message : String(error)}`
|
`OpenCode ledger backfill failed for ${input.teamName}/${input.taskId}: ${error instanceof Error ? error.message : String(error)}`
|
||||||
);
|
);
|
||||||
|
void appendOpenCodeTaskChangeDiag({
|
||||||
|
event: 'backfill_exception',
|
||||||
|
reason: 'exception',
|
||||||
|
teamName: input.teamName,
|
||||||
|
taskId: input.taskId,
|
||||||
|
displayId: input.taskMeta?.displayId ?? null,
|
||||||
|
memberName: input.effectiveOptions.owner ?? null,
|
||||||
|
projectDir,
|
||||||
|
workspaceRoot,
|
||||||
|
deliveryRecordCount: deliveryContextRecords.length,
|
||||||
|
deliveryContextFingerprint: this.hashOpenCodeDeliveryContextRecords(deliveryContextRecords),
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
}).catch(() => undefined);
|
||||||
if (deliveryContextRecords.length === 0) {
|
if (deliveryContextRecords.length === 0) {
|
||||||
this.openCodeBackfillCache.set(cacheKey, {
|
this.openCodeBackfillCache.set(cacheKey, {
|
||||||
backfilledAt: 0,
|
backfilledAt: 0,
|
||||||
|
|
@ -530,6 +571,27 @@ export class ChangeExtractorService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private classifyOpenCodeBackfillResult(
|
||||||
|
result: Awaited<ReturnType<OpenCodeLedgerBackfillPort['backfillOpenCodeTaskLedger']>>
|
||||||
|
): string {
|
||||||
|
if (result.importedEvents > 0) {
|
||||||
|
return 'imported';
|
||||||
|
}
|
||||||
|
if (result.candidateEvents > 0 && result.outcome === 'duplicates-only') {
|
||||||
|
return 'duplicates-only';
|
||||||
|
}
|
||||||
|
if (result.scannedSessions > 0 && result.scannedToolparts > 0 && result.candidateEvents === 0) {
|
||||||
|
return 'no-write-edit-candidates';
|
||||||
|
}
|
||||||
|
if (result.outcome === 'no-attribution') {
|
||||||
|
return 'no-attribution';
|
||||||
|
}
|
||||||
|
if (result.outcome === 'no-history') {
|
||||||
|
return 'no-history';
|
||||||
|
}
|
||||||
|
return result.outcome;
|
||||||
|
}
|
||||||
|
|
||||||
private async isOpenCodeTeamCandidate(teamName: string): Promise<boolean> {
|
private async isOpenCodeTeamCandidate(teamName: string): Promise<boolean> {
|
||||||
const cached = this.openCodeTeamEligibilityCache.get(teamName);
|
const cached = this.openCodeTeamEligibilityCache.get(teamName);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
|
||||||
|
|
@ -636,9 +636,24 @@ function buildMemberBootstrapPrompt(
|
||||||
function buildOpenCodeRuntimeMessageText(input: OpenCodeTeamRuntimeMessageInput): string {
|
function buildOpenCodeRuntimeMessageText(input: OpenCodeTeamRuntimeMessageInput): string {
|
||||||
const replyRecipient = input.replyRecipient?.trim() || 'user';
|
const replyRecipient = input.replyRecipient?.trim() || 'user';
|
||||||
const taskRefs = input.taskRefs?.length ? JSON.stringify(input.taskRefs) : null;
|
const taskRefs = input.taskRefs?.length ? JSON.stringify(input.taskRefs) : null;
|
||||||
|
const deliveryContext =
|
||||||
|
input.messageId && input.taskRefs?.length
|
||||||
|
? JSON.stringify({
|
||||||
|
schemaVersion: 1,
|
||||||
|
kind: 'opencode-delivery-context',
|
||||||
|
teamName: input.teamName,
|
||||||
|
laneId: input.laneId,
|
||||||
|
memberName: input.memberName,
|
||||||
|
inboundMessageId: input.messageId,
|
||||||
|
taskRefs: input.taskRefs,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'<opencode_app_message_delivery>',
|
'<opencode_app_message_delivery>',
|
||||||
|
deliveryContext
|
||||||
|
? `<opencode_delivery_context>${deliveryContext}</opencode_delivery_context>`
|
||||||
|
: null,
|
||||||
'You are running in OpenCode, not Claude Code or Codex native.',
|
'You are running in OpenCode, not Claude Code or Codex native.',
|
||||||
'To make your reply visible in the app Messages UI, call MCP tool agent-teams_message_send (or mcp__agent-teams__message_send if that is the exposed name).',
|
'To make your reply visible in the app Messages UI, call MCP tool agent-teams_message_send (or mcp__agent-teams__message_send if that is the exposed name).',
|
||||||
`Use teamName="${input.teamName}", to="${replyRecipient}", from="${input.memberName}", text, and summary.`,
|
`Use teamName="${input.teamName}", to="${replyRecipient}", from="${input.memberName}", text, and summary.`,
|
||||||
|
|
|
||||||
90
src/main/utils/openCodeTaskChangeDiagLog.ts
Normal file
90
src/main/utils/openCodeTaskChangeDiagLog.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
/**
|
||||||
|
* Dev-only OpenCode task-change diagnostics.
|
||||||
|
*
|
||||||
|
* The normal logger is intentionally quiet in production. This file writes a
|
||||||
|
* bounded NDJSON trace under Electron's logs dir so local dev sessions can
|
||||||
|
* explain why OpenCode task changes were or were not backfilled.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { appendFile, mkdir, stat, truncate } from 'fs/promises';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
const FILE_NAME = 'opencode-task-change-diag.ndjson';
|
||||||
|
const MAX_DIAG_FILE_BYTES = 1024 * 1024;
|
||||||
|
|
||||||
|
function getElectronApp(): typeof import('electron').app | null {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- lazy for tests / non-Electron.
|
||||||
|
const { app } = require('electron') as typeof import('electron');
|
||||||
|
return app ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEnabled(): boolean {
|
||||||
|
if (process.env.CLAUDE_TEAM_OPENCODE_TASK_CHANGE_DIAG === '1') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const app = getElectronApp();
|
||||||
|
return app !== null && typeof app.isPackaged === 'boolean' && app.isPackaged === false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLogsDirectory(): string | null {
|
||||||
|
const app = getElectronApp();
|
||||||
|
if (!app?.getPath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return app.getPath('logs');
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
return join(app.getPath('userData'), 'logs');
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function appendOpenCodeTaskChangeDiag(
|
||||||
|
entry: Record<string, unknown>
|
||||||
|
): Promise<string | null> {
|
||||||
|
if (!isEnabled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const dir = resolveLogsDirectory();
|
||||||
|
if (!dir) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const filePath = join(dir, FILE_NAME);
|
||||||
|
let line: string;
|
||||||
|
try {
|
||||||
|
line =
|
||||||
|
JSON.stringify({
|
||||||
|
t: new Date().toISOString(),
|
||||||
|
diagFile: filePath,
|
||||||
|
...entry,
|
||||||
|
}) + '\n';
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mkdir(dir, { recursive: true });
|
||||||
|
try {
|
||||||
|
const st = await stat(filePath);
|
||||||
|
if (st.size > MAX_DIAG_FILE_BYTES) {
|
||||||
|
await truncate(filePath, 0);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Missing file is fine.
|
||||||
|
}
|
||||||
|
await appendFile(filePath, line, 'utf8');
|
||||||
|
return filePath;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -361,6 +361,9 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
|
||||||
expect(sentText).toContain('Include source="runtime_delivery"');
|
expect(sentText).toContain('Include source="runtime_delivery"');
|
||||||
expect(sentText).toContain('Include relayOfMessageId="msg-1"');
|
expect(sentText).toContain('Include relayOfMessageId="msg-1"');
|
||||||
expect(sentText).toContain('Action mode for this message: delegate.');
|
expect(sentText).toContain('Action mode for this message: delegate.');
|
||||||
|
expect(sentText).toContain('<opencode_delivery_context>');
|
||||||
|
expect(sentText).toContain('"kind":"opencode-delivery-context"');
|
||||||
|
expect(sentText).toContain('"inboundMessageId":"msg-1"');
|
||||||
expect(sentText).toContain(
|
expect(sentText).toContain(
|
||||||
'If your reply is about these tasks, include taskRefs exactly: [{"taskId":"task-1","displayId":"abcd1234","teamName":"team-a"}]'
|
'If your reply is about these tasks, include taskRefs exactly: [{"taskId":"task-1","displayId":"abcd1234","teamName":"team-a"}]'
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue