diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 57fbdd7..245689b 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,3 +1,4 @@ +{"_type":"issue","id":"islandflow-yza","title":"Persist historical flow packets for alert detail replay","description":"## Why\nAlert details can show a missing persisted flow packet when the packet is no longer present in the Redis hot cache, even though the associated historical alert and evidence were loaded from ClickHouse.\n\n## What needs to be done\nTrace the API path that resolves alert detail flow packets, compare Redis hot-cache lookups with ClickHouse historical fetches, and ensure historical flow packet payloads are treated as first-class persisted data with context preserved when replaying or loading older alerts.\n\n## Acceptance Criteria\n- Alert detail flow packets load for historical alerts even when the packet is absent from Redis hot cache\n- Historical ClickHouse-backed flow packet responses preserve the context required by the UI\n- Relevant automated tests cover the regression or the gap is explicitly documented","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-20T06:52:04Z","created_by":"dirtydishes","updated_at":"2026-05-20T06:59:26Z","started_at":"2026-05-20T06:52:09Z","closed_at":"2026-05-20T06:59:26Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-jor","title":"Support Forgejo pull request status in desktop git panel","description":"The desktop app currently reports pull request status unavailable when a repository only has a Forgejo remote. Add native Forgejo/Gitea-style remote detection and pull request status lookup so Forgejo-only repositories can show PR state in the Codex app git panel.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T20:55:15Z","created_by":"dirtydishes","updated_at":"2026-05-19T20:59:46Z","started_at":"2026-05-19T20:55:25Z","closed_at":"2026-05-19T20:59:46Z","close_reason":"Patched the installed Codex desktop app bundle with a Forgejo PR status fallback and documented the local change.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-g3a","title":"Reconcile PR merge conflicts","description":"Resolve the current pull request conflicts for the nextjs-upgrade branch, validate the result, document the turn, and push the reconciled branch.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T18:44:51Z","created_by":"dirtydishes","updated_at":"2026-05-19T18:47:35Z","started_at":"2026-05-19T18:44:56Z","closed_at":"2026-05-19T18:47:35Z","close_reason":"Merged forgejo/main into nextjs-upgrade, resolved README and Beads conflicts, updated JetStream retention tests, validated deploy help, Docker workspace sync, API/bus tests, and web build, and added turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-jbi","title":"Hydrate alert evidence details from ClickHouse","description":"Alert detail drawers need to fetch persisted alert context from ClickHouse by trace id, including linked flow packets, option prints, preserved execution context, and explicit missing refs for UI diagnostics.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-17T14:55:43Z","created_by":"dirtydishes","updated_at":"2026-05-17T15:01:58Z","started_at":"2026-05-17T14:55:53Z","closed_at":"2026-05-17T15:01:58Z","close_reason":"Implemented ClickHouse-backed alert context hydration across storage, API, terminal drawer, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/apps/web/app/terminal.test.ts b/apps/web/app/terminal.test.ts index 63918f2..92a9904 100644 --- a/apps/web/app/terminal.test.ts +++ b/apps/web/app/terminal.test.ts @@ -43,6 +43,8 @@ import { shouldClearOptionFocusSeed, smartMoneyProfileLabel, smartMoneyToneForProfile, + getAlertFlowPacketRefs, + resolveAlertFlowPacket, statusLabel, toggleFilterValue } from "./terminal"; @@ -133,6 +135,33 @@ describe("alert context hydration helpers", () => { expect(evidence.prints.get("print:1")?.execution_nbbo_bid).toBe(1.2); expect(evidence.prints.get("print:1")?.execution_underlying_spot).toBe(450.05); }); + + it("finds flow-packet refs even when they are not first in alert evidence", () => { + const alert = makeAlert({ + evidence_refs: ["smartmoney:single_leg_event:flowpacket:1", "flowpacket:1", "print:1"] + }); + + expect(getAlertFlowPacketRefs(alert)).toEqual(["flowpacket:1"]); + }); + + it("resolves the primary alert flow packet from hydrated historical context", () => { + const packet = { + trace_id: "flowpacket:1", + id: "flowpacket:1", + members: ["print:1"], + source_ts: 1, + ingest_ts: 2, + seq: 1, + features: {}, + join_quality: {} + } as any; + const alert = makeAlert({ + evidence_refs: ["smartmoney:single_leg_event:flowpacket:1", "flowpacket:1", "print:1"] + }); + const packets = new Map([[packet.id, packet]]); + + expect(resolveAlertFlowPacket(alert, packets)).toBe(packet); + }); }); describe("live manifest", () => { diff --git a/apps/web/app/terminal.tsx b/apps/web/app/terminal.tsx index 3bec184..3057f58 100644 --- a/apps/web/app/terminal.tsx +++ b/apps/web/app/terminal.tsx @@ -4753,6 +4753,26 @@ export const collectAlertContextEvidence = ( return { packets, prints }; }; +export const getAlertFlowPacketRefs = ( + alert: Pick +): string[] => { + return alert.evidence_refs.filter((ref) => ref.startsWith("flowpacket:")); +}; + +export const resolveAlertFlowPacket = ( + alert: Pick, + packets: Map +): FlowPacket | null => { + for (const ref of getAlertFlowPacketRefs(alert)) { + const packet = packets.get(ref); + if (packet) { + return packet; + } + } + + return null; +}; + type DarkEvidenceItem = | { kind: "join"; id: string; join: EquityPrintJoin } | { kind: "unknown"; id: string }; @@ -6014,8 +6034,7 @@ const useTerminalState = () => { if (!selectedAlert) { return null; } - const packetId = selectedAlert.evidence_refs[0]; - return packetId ? resolvedFlowPacketMap.get(packetId) ?? null : null; + return resolveAlertFlowPacket(selectedAlert, resolvedFlowPacketMap); }, [selectedAlert, resolvedFlowPacketMap]); const selectedDarkEvidence = useMemo((): DarkEvidenceItem[] => { @@ -6427,12 +6446,9 @@ const useTerminalState = () => { return fromTrace; } - const packetId = alert.evidence_refs[0]; - if (packetId) { - const packet = resolvedFlowPacketMap.get(packetId); - if (packet) { - return extractUnderlying(extractPacketContract(packet)); - } + const packet = resolveAlertFlowPacket(alert, resolvedFlowPacketMap); + if (packet) { + return extractUnderlying(extractPacketContract(packet)); } for (const ref of alert.evidence_refs) { @@ -6704,9 +6720,7 @@ const useTerminalState = () => { return; } - const visiblePacketIds = visibleAlerts - .map((alert) => alert.evidence_refs[0] ?? null) - .filter((id): id is string => Boolean(id) && id.startsWith("flowpacket:")); + const visiblePacketIds = visibleAlerts.flatMap((alert) => getAlertFlowPacketRefs(alert)); const missingPacketIds = Array.from(new Set(visiblePacketIds)).filter( (id) => !resolvedFlowPacketMap.has(id) ); @@ -6788,9 +6802,10 @@ const useTerminalState = () => { const activePinnedFlowKeys = useMemo(() => { const keys = new Set(); - const selectedAlertPacketId = selectedAlert?.evidence_refs[0]; - if (selectedAlertPacketId) { - keys.add(selectedAlertPacketId); + if (selectedAlert) { + for (const packetId of getAlertFlowPacketRefs(selectedAlert)) { + keys.add(packetId); + } } if (selectedClassifierPacketId) { keys.add(selectedClassifierPacketId); @@ -6799,8 +6814,7 @@ const useTerminalState = () => { keys.add(packetId); } for (const alert of visibleAlerts) { - const packetId = alert.evidence_refs[0]; - if (packetId) { + for (const packetId of getAlertFlowPacketRefs(alert)) { keys.add(packetId); } } @@ -6945,7 +6959,7 @@ const useTerminalState = () => { const desiredTrace = `alert:${packetId}`; return ( alertsFeed.items.find( - (item) => item.trace_id === desiredTrace || item.evidence_refs[0] === packetId + (item) => item.trace_id === desiredTrace || getAlertFlowPacketRefs(item).includes(packetId) ) ?? null ); }, diff --git a/docs/turns/2026-05-20-fix-alert-flow-packet-history.html b/docs/turns/2026-05-20-fix-alert-flow-packet-history.html new file mode 100644 index 0000000..d7e2b30 --- /dev/null +++ b/docs/turns/2026-05-20-fix-alert-flow-packet-history.html @@ -0,0 +1,412 @@ + + + + + + Fix historical alert flow packet persistence in the web terminal + + + + + + +
+
+

Turn Document · 2026-05-20 02:56 EDT

+

Historical Alert Flow Packets Persist Again

+

Alert detail drawers now resolve persisted flow packets from ClickHouse-backed historical context instead of assuming the first evidence reference is the packet. This restores packet visibility for replayed and older alerts after their Redis hot-cache entries have aged out.

+
+ Beads: islandflow-yza + Surface: apps/web terminal + Validation: tests + prod build +
+
+ +
+
+

Summary

+

The web terminal was assuming alert.evidence_refs[0] always pointed at a flow packet. For compute-generated alerts, the first evidence ref is often the smart-money event id, with the actual packet id later in the list. That made persisted historical packets look missing even when ClickHouse context had already hydrated them successfully.

+
+ +
+

Changes Made

+
    +
  • Added shared alert helpers in apps/web/app/terminal.tsx to extract all flow-packet refs from an alert and resolve the first hydrated packet semantically.
  • +
  • Switched the alert drawer's selected packet lookup to use the shared resolver instead of the first evidence ref.
  • +
  • Updated alert-underlying inference, visible-alert prefetch, pinned-flow retention keys, and classifier-hit-to-alert matching to use the same alert packet semantics.
  • +
  • Added focused regression coverage in apps/web/app/terminal.test.ts for alerts whose packet ref is not the first evidence entry.
  • +
+
+ +
+

Context

+

Islandflow alert detail views combine live Redis retention with ClickHouse historical hydration. Once a packet leaves the hot cache, the UI must treat ClickHouse-loaded evidence as first-class persisted context, not as a degraded fallback. The bug was in the web client’s interpretation of alert evidence ordering, not in the persistence of the packet itself.

+
+ Historical packet context was already present. The terminal simply was not selecting it unless the packet id happened to be the first evidence ref. +
+
+ +
+

Important Implementation Details

+
    +
  • The fix is backward-compatible with already-persisted alerts because it tolerates existing evidence ordering instead of rewriting stored records.
  • +
  • The shared resolver centralizes the packet-selection rule so replay, pinning, and alert navigation do not drift apart again.
  • +
  • The classifier-hit alert matching path now finds alerts by any embedded packet ref, which improves consistency when opening related alert context from signal panes.
  • +
+
+ +
+

Relevant Diff Snippets

+
+
+

apps/web/app/terminal.tsx · alert packet resolution

+
+
-const packetId = selectedAlert.evidence_refs[0];
+-return packetId ? resolvedFlowPacketMap.get(packetId) ?? null : null;
++return resolveAlertFlowPacket(selectedAlert, resolvedFlowPacketMap);
+
+ +
+

apps/web/app/terminal.tsx · prefetch and alert matching

+
+
-const visiblePacketIds = visibleAlerts
+-  .map((alert) => alert.evidence_refs[0] ?? null)
+-  .filter((id): id is string => Boolean(id) && id.startsWith("flowpacket:"));
++const visiblePacketIds = visibleAlerts.flatMap((alert) => getAlertFlowPacketRefs(alert));
+
+-alertsFeed.items.find((item) => item.trace_id === desiredTrace || item.evidence_refs[0] === packetId)
++alertsFeed.items.find(
++  (item) => item.trace_id === desiredTrace || getAlertFlowPacketRefs(item).includes(packetId)
++)
+
+
+

These snippets are rendered client-side with Diffs using the same old/new code blocks shown in the fallback text if the library cannot load.

+
+ +
+

Expected Impact for End-Users

+

Older or replayed alerts should now show their persisted flow packet summary in the detail drawer even after the Redis hot cache no longer has that packet. Users investigating signal history should keep the same evidence continuity they get from live data: packet summary, print context, and related alert linkage stay intact.

+
+ +
+

Validation

+
    +
  • bun test apps/web/app/terminal.test.ts passed with 72 tests.
  • +
  • bun --cwd=apps/web run build passed on Next.js 16.2.6.
  • +
  • The new tests specifically cover alerts where a smart-money event id precedes the packet id in evidence_refs.
  • +
+
+ +
+

Issues, Limitations, and Mitigations

+
    +
  • This change does not alter how compute persists alert evidence ordering. Instead, it makes the terminal resilient to existing and future mixed evidence lists.
  • +
  • The Diffs rendering in this document loads from the published package at view time. A plain-text fallback is included directly in the HTML so the document remains readable offline.
  • +
  • No full monorepo test sweep was run because the change was isolated to the web terminal alert-context path.
  • +
+
+ +
+

Follow-up Work

+
    +
  • No additional Beads issue was required for this fix.
  • +
  • Optional: audit whether compute should emit packet ids before higher-level event ids in evidence_refs for simpler downstream consumers.
  • +
  • Optional: add a small integration test around alert drawer selection if the web app gains component-level interaction tests later.
  • +
+
+
+
+ + + +