From bd0ac2c703f6ccd74d5cd5c37f3b0f49ce445843 Mon Sep 17 00:00:00 2001 From: Yuhao Chen Date: Wed, 13 May 2026 22:13:18 +0800 Subject: [PATCH] fix(daemon): preserve Orbit no-artifact explanation (#1576) --- apps/daemon/src/orbit-agent-summary.ts | 39 +++++++++++++++ apps/daemon/src/server.ts | 5 +- apps/daemon/tests/orbit-agent-summary.test.ts | 50 +++++++++++++++++++ apps/daemon/tests/orbit.test.ts | 37 ++++++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 apps/daemon/src/orbit-agent-summary.ts create mode 100644 apps/daemon/tests/orbit-agent-summary.test.ts diff --git a/apps/daemon/src/orbit-agent-summary.ts b/apps/daemon/src/orbit-agent-summary.ts new file mode 100644 index 000000000..bf01c8c0a --- /dev/null +++ b/apps/daemon/src/orbit-agent-summary.ts @@ -0,0 +1,39 @@ +const NO_LIVE_ARTIFACT_SUMMARY = + 'Agent succeeded but did not register a live artifact for this Orbit run.'; + +const MAX_FINAL_EXPLANATION_CHARS = 2_000; + +interface RunEventRecord { + event?: unknown; + data?: unknown; +} + +function asObject(value: unknown): Record | null { + if (!value || typeof value !== 'object') return null; + return value as Record; +} + +function textDeltaFromEvent(record: RunEventRecord): string | null { + if (record.event !== 'agent') return null; + const data = asObject(record.data); + if (!data || data.type !== 'text_delta') return null; + return typeof data.delta === 'string' ? data.delta : null; +} + +export function extractOrbitAgentFinalExplanation(events: readonly RunEventRecord[]): string | null { + const text = events + .map(textDeltaFromEvent) + .filter((delta): delta is string => delta !== null) + .join('') + .trim(); + if (!text) return null; + if (text.length <= MAX_FINAL_EXPLANATION_CHARS) return text; + return `${text.slice(0, MAX_FINAL_EXPLANATION_CHARS).trimEnd()}...`; +} + +export function buildOrbitNoLiveArtifactSummary(events: readonly RunEventRecord[]): string { + const explanation = extractOrbitAgentFinalExplanation(events); + return explanation + ? `${NO_LIVE_ARTIFACT_SUMMARY}\n\n${explanation}` + : NO_LIVE_ARTIFACT_SUMMARY; +} diff --git a/apps/daemon/src/server.ts b/apps/daemon/src/server.ts index 04c5bca63..9c6b5a094 100644 --- a/apps/daemon/src/server.ts +++ b/apps/daemon/src/server.ts @@ -155,6 +155,7 @@ import { } from './mcp-tokens.js'; import { agentCliEnvForAgent, readAppConfig, writeAppConfig } from './app-config.js'; import { OrbitService, formatLocalProjectTimestamp, renderOrbitTemplateSystemPrompt } from './orbit.js'; +import { buildOrbitNoLiveArtifactSummary } from './orbit-agent-summary.js'; import { RoutineService, validateSchedule as validateRoutineSchedule, @@ -4657,7 +4658,9 @@ export async function startServer({ ...(artifact?.id ? { artifactId: artifact.id, artifactProjectId: projectId } : {}), summary: artifact?.id ? `Agent ${finalStatus.status} and registered live artifact ${artifact.title}.` - : `Agent ${finalStatus.status} but did not register a live artifact for this Orbit run.`, + : finalStatus.status === 'succeeded' + ? buildOrbitNoLiveArtifactSummary(run.events) + : `Agent ${finalStatus.status} but did not register a live artifact for this Orbit run.`, }; })(); diff --git a/apps/daemon/tests/orbit-agent-summary.test.ts b/apps/daemon/tests/orbit-agent-summary.test.ts new file mode 100644 index 000000000..69199fa86 --- /dev/null +++ b/apps/daemon/tests/orbit-agent-summary.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; + +import { + buildOrbitNoLiveArtifactSummary, + extractOrbitAgentFinalExplanation, +} from '../src/orbit-agent-summary.js'; + +describe('Orbit agent summary helpers', () => { + it('preserves the agent final explanation for no-live-artifact Orbit runs', () => { + const summary = buildOrbitNoLiveArtifactSummary([ + { event: 'agent', data: { type: 'text_delta', delta: 'Data loading failed, ' } }, + { event: 'agent', data: { type: 'text_delta', delta: 'so I did not create a daily digest artifact.' } }, + ]); + + expect(summary).toContain( + 'Agent succeeded but did not register a live artifact for this Orbit run.', + ); + expect(summary).toContain( + 'Data loading failed, so I did not create a daily digest artifact.', + ); + }); + + it('extracts only user-visible text deltas from run events', () => { + expect( + extractOrbitAgentFinalExplanation([ + { event: 'stdout', data: { chunk: 'raw tool output' } }, + { event: 'stderr', data: { chunk: 'OPENAI_API_KEY=sk-raw-secret' } }, + { event: 'tool_result', data: { output: 'token=raw-tool-secret' } }, + { event: 'agent', data: { type: 'thinking_delta', delta: 'private reasoning' } }, + { event: 'agent', data: { type: 'tool_use', name: 'Read' } }, + { event: 'agent', data: { type: 'text_delta', delta: 'GitHub auth failed.' } }, + ]), + ).toBe('GitHub auth failed.'); + }); + + it('falls back to the implementation-level no-artifact marker without final text', () => { + expect(buildOrbitNoLiveArtifactSummary([])).toBe( + 'Agent succeeded but did not register a live artifact for this Orbit run.', + ); + }); + + it('bounds long final explanations before storing them in the Orbit receipt', () => { + const explanation = extractOrbitAgentFinalExplanation([ + { event: 'agent', data: { type: 'text_delta', delta: 'x'.repeat(2_100) } }, + ]); + + expect(explanation).toHaveLength(2_003); + expect(explanation?.endsWith('...')).toBe(true); + }); +}); diff --git a/apps/daemon/tests/orbit.test.ts b/apps/daemon/tests/orbit.test.ts index d50c6ecb1..2dbc21947 100644 --- a/apps/daemon/tests/orbit.test.ts +++ b/apps/daemon/tests/orbit.test.ts @@ -277,6 +277,43 @@ describe('OrbitService', () => { } }); + it('persists failed Orbit agent summaries in the last-run receipt markdown', async () => { + const dataDir = await mkdtemp(path.join(os.tmpdir(), 'orbit-test-')); + try { + const service = new OrbitService(dataDir); + service.setRunHandler(async () => ({ + projectId: 'project-1', + agentRunId: 'agent-1', + completion: Promise.resolve({ + agentRunId: 'agent-1', + status: 'failed', + summary: + 'Agent succeeded but did not register a live artifact for this Orbit run.\n\nGitHub auth failed, so I did not create a daily digest artifact.', + }), + })); + + await service.start('manual'); + let status = await service.status(); + for (let attempt = 0; attempt < 10 && (status.running || !status.lastRun); attempt += 1) { + await new Promise((resolve) => setTimeout(resolve, 0)); + status = await service.status(); + } + + expect(status.lastRun).not.toBeNull(); + expect(status.running).toBe(false); + expect(status.lastRun?.connectorsSucceeded).toBe(0); + expect(status.lastRun?.connectorsFailed).toBe(1); + expect(status.lastRun?.markdown).toContain( + 'Agent succeeded but did not register a live artifact for this Orbit run.', + ); + expect(status.lastRun?.markdown).toContain( + 'GitHub auth failed, so I did not create a daily digest artifact.', + ); + } finally { + await rm(dataDir, { recursive: true, force: true }); + } + }); + it('tracks the most recent run per template alongside the global last run', async () => { vi.useFakeTimers(); const dataDir = await mkdtemp(path.join(os.tmpdir(), 'orbit-test-'));