Skip to content

Commit c70b833

Browse files
DylanMerigaudclaude
andcommitted
#6 Fix replayed runs showing the unresolved default workflow (read the LAST stage event)
Opening a stored run drew the full, unresolved workflow with every condition visible (like edit mode), not the resolved lit path the run actually took. Root cause: a live run upserts each stage node in place (one event), but the STORED trace keeps BOTH the "running" event (empty data) and the "done" event (which carries the workflow + steps + verdict) under the same stepId. `readRunGraph`/`readMatchContext` used Array.find, which returned the FIRST (running, empty) approval/matching event → no workflow, no verdict → the graph fell back to the generic default and never resolved. Read the LAST matching event instead (a small findLastEvent helper; Array.findLast needs a newer lib target). Applied to the approval, matching, and investigation reads. Now a replayed run resolves the same path + statuses a live run shows. Verified against the real stored trace on prod: the done approval event (seq 8) carries the workflow; find grabbed the empty seq-3 one, findLast grabs seq 8. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 05febe3 commit c70b833

1 file changed

Lines changed: 33 additions & 4 deletions

File tree

components/dashboard.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,21 @@ import { usePipelineRun } from "@/lib/use-pipeline-run";
6262
* an append-only audit row (the Recent runs panel below the queue lists them,
6363
* replayable). A nightly reset clears those, so every morning starts pristine.
6464
*/
65+
/** The LAST trace event matching a predicate. A live run upserts a stage in place (one
66+
event), but a stored/replayed trace keeps the running (often empty) AND the done
67+
(data-carrying) event for a stage, in order, so we must read the last, not the first.
68+
(Local helper rather than Array.findLast, which needs a newer lib target.) */
69+
const findLastEvent = (
70+
trace: TraceEvent[],
71+
pred: (e: TraceEvent) => boolean,
72+
): TraceEvent | undefined => {
73+
for (let i = trace.length - 1; i >= 0; i--) {
74+
const e = trace[i];
75+
if (e && pred(e)) return e;
76+
}
77+
return undefined;
78+
};
79+
6580
/**
6681
* Pull the intake (extraction) node out of the trace and shape it for the reveal.
6782
* The intake step carries `{ document }` while reading and `{ extracted, matches }`
@@ -101,7 +116,13 @@ const readIntake = (
101116
const readRunGraph = (
102117
trace: TraceEvent[],
103118
): { workflow: TApprovalWorkflow; statuses: StepStatuses } | null => {
104-
const approval = trace.find(
119+
// findLast, not find: a live run upserts the approval node in place (one event), but
120+
// a STORED/replayed trace keeps both the "running" event (empty data) AND the "done"
121+
// event (which carries the workflow + steps) under the same stepId. `find` grabbed the
122+
// running one → no workflow → the graph fell back to the full unresolved default. Take
123+
// the LAST approval event so replay resolves the same lit path a live run shows.
124+
const approval = findLastEvent(
125+
trace,
105126
(e) => e.stage === "approval" && e.kind === "step",
106127
);
107128
if (!approval || !isRecord(approval.data)) return null;
@@ -149,7 +170,10 @@ const isBlockedRun = (
149170
approvalData: Record<string, unknown>,
150171
): boolean => {
151172
if (approvalData["outcome"] === "blocked") return true;
152-
const matching = trace.find(
173+
// findLast: the stored trace keeps the running (empty) + done matching events; the
174+
// verdict is on the done one.
175+
const matching = findLastEvent(
176+
trace,
153177
(e) => e.stage === "matching" && e.kind === "step",
154178
);
155179
return isRecord(matching?.data) && matching.data["verdict"] === "duplicate";
@@ -164,7 +188,10 @@ type Recommendation = {
164188
rationale: string | null;
165189
};
166190
const readRecommendation = (trace: TraceEvent[]): Recommendation | null => {
167-
const inv = trace.find((e) => e.stage === "investigation" && e.data != null);
191+
const inv = findLastEvent(
192+
trace,
193+
(e) => e.stage === "investigation" && e.data != null,
194+
);
168195
if (!inv || !isRecord(inv.data)) return null;
169196
const rec = inv.data["recommendation"];
170197
const verdict =
@@ -187,7 +214,9 @@ const readRecommendation = (trace: TraceEvent[]): Recommendation | null => {
187214
the extras, rather than strict-parsing the whole event (which would reject it). */
188215
const MatchResultLoose = MatchResult.passthrough();
189216
const readMatchContext = (trace: TraceEvent[]): InvoiceContext | undefined => {
190-
const matching = trace.find(
217+
// findLast: the done matching event (with the MatchResult) follows the running one.
218+
const matching = findLastEvent(
219+
trace,
191220
(e) => e.stage === "matching" && e.kind === "step",
192221
);
193222
if (!matching || !isRecord(matching.data)) return undefined;

0 commit comments

Comments
 (0)