Skip to content

Commit cf56ecd

Browse files
V48 Gate 3 (implementation-only): live stall visibility on the synthesis telemetry processing indicator
The processing indicator showed a static "Processing" label with no signal of what was running or whether it had stalled — during the depository-search agent hangs (F25/F28-class), the log goes silent while a call is in flight and there is no way to tell live whether it is progressing or stuck. The indicator now shows the last known Phase→Agent→Step→Failsafe→Thricified context plus elapsed seconds since the last streamed event, ticking every second, and flips to an amber warning once elapsed time reaches the BITCODE_LLM_CALL_TIMEOUT_MS default (90s) — past that point a live call should already have timed out server-side, so continued silence is a genuine-hang signal. Does not add a new formal log-line kind (F19's "exactly LLM calls + Tool uses" row contract is unchanged) — purely additive to the existing processing indicator. buildProcessingStallLabel is a pure, exported helper (unit tested: no prior line, normal elapsed time, past-threshold warning, invalid/missing timestamp).
1 parent 6497c53 commit cf56ecd

3 files changed

Lines changed: 110 additions & 4 deletions

File tree

uapi/components/base/bitcode/execution/pipeline-execution-log.tsx

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,37 @@ export function buildRawLogCopyText(args: {
397397
].join('');
398398
}
399399

400+
// Matches the default BITCODE_LLM_CALL_TIMEOUT_MS (AgentLLMsRegistry /
401+
// PipelineLLMRegistry) — past this many seconds with no new row, an in-flight
402+
// LLM call should already have timed out server-side, so continued silence is
403+
// a genuine-hang signal rather than a merely slow generation.
404+
const LIKELY_STALL_SECONDS = 90;
405+
406+
/**
407+
* Build the live "Running: {hierarchy} · Ns since last update" label for the
408+
* processing indicator, from the last known log line + the current tick. Pure
409+
* + exported for unit testing. Returns the bare fallback label when there is no
410+
* prior line yet (nothing streamed since the run started).
411+
*/
412+
export function buildProcessingStallLabel(
413+
lastLine: Pick<LogLine, 'phase' | 'agent' | 'step' | 'failsafe' | 'generation' | 'timestamp'> | undefined,
414+
nowMs: number,
415+
): { label: string; likelyStalled: boolean } {
416+
if (!lastLine?.timestamp) return { label: 'Processing', likelyStalled: false };
417+
const lastMs = new Date(lastLine.timestamp).getTime();
418+
if (!Number.isFinite(lastMs)) return { label: 'Processing', likelyStalled: false };
419+
420+
const elapsedSeconds = Math.max(0, Math.round((nowMs - lastMs) / 1000));
421+
const hierarchy = [lastLine.phase, lastLine.agent, lastLine.step, lastLine.failsafe, lastLine.generation]
422+
.filter(Boolean)
423+
.join(' → ');
424+
const likelyStalled = elapsedSeconds >= LIKELY_STALL_SECONDS;
425+
const label = hierarchy
426+
? `Running: ${hierarchy} · ${elapsedSeconds}s since last update`
427+
: `Processing · ${elapsedSeconds}s since last update`;
428+
return { label, likelyStalled };
429+
}
430+
400431
/**
401432
* Copy text to the clipboard, returning whether it succeeded. Tries the modern
402433
* `navigator.clipboard` (requires a secure context) and, when that is unavailable or
@@ -446,6 +477,20 @@ export const PipelineExecutionLog = forwardRef<HTMLDivElement, PipelineRunLogPro
446477
const containerRef = useRef<HTMLDivElement | null>(null);
447478
const [autoCompact, setAutoCompact] = useState(false);
448479

480+
// Live "stalled since" signal (QA debug aid, V48 Gate 3): while processing, tick
481+
// once a second so the processing indicator can show elapsed time since the
482+
// last streamed event. This does NOT add a new formal log-line kind (F19's
483+
// "exactly LLM calls + Tool uses" contract is unchanged) — it only makes an
484+
// in-flight call's silence visible in real time, so a genuine hang (e.g. past
485+
// BITCODE_LLM_CALL_TIMEOUT_MS with no new row) is distinguishable from a slow
486+
// but progressing run instead of an unexplained blank gap.
487+
const [nowTick, setNowTick] = useState(() => Date.now());
488+
useEffect(() => {
489+
if (!isProcessing) return;
490+
const id = setInterval(() => setNowTick(Date.now()), 1000);
491+
return () => clearInterval(id);
492+
}, [isProcessing]);
493+
449494
// "Copy raw logs": copy this run's full information (all streamed logs + inputs).
450495
const [copiedRaw, setCopiedRaw] = useState(false);
451496
const handleCopyRaw = async () => {
@@ -876,8 +921,13 @@ export const PipelineExecutionLog = forwardRef<HTMLDivElement, PipelineRunLogPro
876921
),
877922
)}
878923

879-
{/* Processing indicator */}
880-
{isProcessing && <ProcessingIndicator />}
924+
{/* Processing indicator — shows the last known Phase→Agent→Step→Failsafe→
925+
Thricified context + elapsed time since the last streamed event, so a
926+
genuine hang is visible live instead of an unexplained blank gap. */}
927+
{isProcessing && (() => {
928+
const { label, likelyStalled } = buildProcessingStallLabel(flatLines[flatLines.length - 1], nowTick);
929+
return <ProcessingIndicator label={label} stalled={likelyStalled} />;
930+
})()}
881931
</div>
882932
</div>
883933
</div>

uapi/components/base/bitcode/indicators/ProcessingIndicator.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
import React, { useEffect, useRef } from 'react';
44
import '@/styles/processing-indicator.css';
55

6-
export function ProcessingIndicator({ label = 'Processing' }: { label?: string }) {
6+
export function ProcessingIndicator({
7+
label = 'Processing',
8+
stalled = false,
9+
}: {
10+
label?: string;
11+
/** True once elapsed time since the last event suggests a genuine hang (vs. a slow but live call) — swaps the label to an amber warning tone. */
12+
stalled?: boolean;
13+
}) {
714
const orbRef = useRef<HTMLDivElement>(null);
815
const textRef = useRef<HTMLDivElement>(null);
916

@@ -26,7 +33,12 @@ export function ProcessingIndicator({ label = 'Processing' }: { label?: string }
2633
</div>
2734
</div>
2835
<div className="relative">
29-
<div ref={textRef} className="processing-text text-brand-emerald text-neon">{intelligentLabel}</div>
36+
<div
37+
ref={textRef}
38+
className={`processing-text text-neon ${stalled ? 'text-amber-400' : 'text-brand-emerald'}`}
39+
>
40+
{intelligentLabel}
41+
</div>
3042
<div className="processing-underline w-full" />
3143
</div>
3244
</div>

uapi/tests/pipelineExecutionLogCopy.test.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ jest.mock('@/components/base/bitcode/execution/FileDiffViewer', () => ({
99

1010
import {
1111
buildRawLogCopyText,
12+
buildProcessingStallLabel,
1213
copyTextToClipboard,
1314
} from '@/components/base/bitcode/execution/pipeline-execution-log';
1415

@@ -49,6 +50,49 @@ describe('PipelineExecutionLog — Copy raw logs (buildRawLogCopyText)', () => {
4950
});
5051
});
5152

53+
describe('buildProcessingStallLabel — live stall visibility (QA debug aid)', () => {
54+
it('falls back to a bare label when there is no prior line yet', () => {
55+
expect(buildProcessingStallLabel(undefined, Date.now())).toEqual({
56+
label: 'Processing',
57+
likelyStalled: false,
58+
});
59+
});
60+
61+
it('renders the hierarchy + elapsed seconds since the last line, not stalled under the threshold', () => {
62+
const lastLine = {
63+
phase: 'Discovery',
64+
agent: 'DepositDepositorySearchAgent',
65+
step: 'try',
66+
failsafe: 'chunk_then_sum',
67+
generation: 'structured_output',
68+
timestamp: new Date(1_000_000).toISOString(),
69+
};
70+
const { label, likelyStalled } = buildProcessingStallLabel(lastLine, 1_000_000 + 30_000);
71+
expect(label).toBe(
72+
'Running: Discovery → DepositDepositorySearchAgent → try → chunk_then_sum → structured_output · 30s since last update',
73+
);
74+
expect(likelyStalled).toBe(false);
75+
});
76+
77+
it('flags likelyStalled once elapsed time reaches the LLM call timeout default (90s)', () => {
78+
const lastLine = { phase: 'Discovery', agent: 'DepositDepositorySearchAgent', timestamp: new Date(0).toISOString() };
79+
const { likelyStalled, label } = buildProcessingStallLabel(lastLine, 90_000);
80+
expect(likelyStalled).toBe(true);
81+
expect(label).toContain('90s since last update');
82+
});
83+
84+
it('handles a missing/invalid timestamp without throwing', () => {
85+
expect(buildProcessingStallLabel({ phase: 'Discovery', timestamp: undefined } as any, Date.now())).toEqual({
86+
label: 'Processing',
87+
likelyStalled: false,
88+
});
89+
expect(buildProcessingStallLabel({ phase: 'Discovery', timestamp: 'not-a-date' } as any, Date.now())).toEqual({
90+
label: 'Processing',
91+
likelyStalled: false,
92+
});
93+
});
94+
});
95+
5296
describe('copyTextToClipboard — modern + insecure-context fallback', () => {
5397
const originalClipboard = (navigator as any).clipboard;
5498
const originalExec = (document as any).execCommand;

0 commit comments

Comments
 (0)