Skip to content

fix(#6212): reconstruct Anchor outcomes from run journals#6295

Open
webtecnica wants to merge 2 commits into
nesquena:masterfrom
webtecnica:feat/6212-anchor-outcomes
Open

fix(#6212): reconstruct Anchor outcomes from run journals#6295
webtecnica wants to merge 2 commits into
nesquena:masterfrom
webtecnica:feat/6212-anchor-outcomes

Conversation

@webtecnica

Copy link
Copy Markdown
Contributor

Summary

Teach _run_journal_live_snapshot() to reconstruct artifact_reference and state_saved journal events into artifacts[] and side_effects[] on the anchor activity scene, closing the consumer-side gap after #6204 and #6207.

Changes

  • Accumulators (api/routes.py ~L3161): Add artifacts: list[dict] = [] and side_effects: list[dict] = [] alongside existing accumulators.
  • Event handling (api/routes.py ~L3305): Handle artifact_reference events (extracting kind, path, event_id) and state_saved events (extracting kind, name, action, event_id) in the journal event loop. Malformed/empty payloads are safely ignored.
  • Scene hydration (api/routes.py ~L3703): Include artifacts and side_effects in the returned anchor_activity_scene dict.

Acceptance Criteria

  • Hard refresh, reconnect, and session switch recover turn-owned artifact and side-effect references from the run journal.
  • Malformed or unrelated journal events do not become artifacts or side effects (guarded by isinstance(payload, dict) and payload).
  • Reconstructed outcomes do not create Worklog rows or change final-answer rendering (they live in the scene's artifacts/side_effects fields only).
  • No new producer events, no shell-command/execute_code inference, no UI changes.

Refs: #6212, #6204, #6207, #3400

Closes common issue where clicking MEDIA: links for .md files could
download a JSON auth error on desktop. Three-layer fix:

1. MIME mapping (api/config.py): add .md/.mkd/.mkdn → text/markdown
   so the server returns the correct Content-Type instead of
   application/octet-stream

2. Session allow-list (api/routes.py): add text/markdown to
   _SESSION_MEDIA_TOKEN_TYPES so .md files are accepted by the
   session-token path check

3. Frontend preview (static/ui.js + static/style.css): detect .md
   files in _inlineMediaHtmlForRef, fetch content via api/media,
   render through the existing renderMd() pipeline, and display
   inline in a styled container — same pattern as HTML/CSV previews
In _run_journal_live_snapshot(), accumulate artifact_reference and
state_saved journal events into artifacts[] and side_effects[]
lists, then include them in the returned anchor_activity_scene dict.

This preserves turn-owned artifact and side-effect references during
hard-refresh/reconnect/session-switch reconstruction from the durable
run journal, closing the remaining consumer-side gap after nesquena#6204 and
nesquena#6207.

- Add artifacts=[] and side_effects=[] accumulators
- Handle artifact_reference events (kind, path, event_id)
- Handle state_saved events (kind, name, action, event_id)
- Include artifacts and side_effects in anchor_activity_scene
- Ignore malformed/empty payloads (no-op for safety)
@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR teaches _run_journal_live_snapshot() to reconstruct artifact_reference and state_saved journal events into artifacts[] and side_effects[] on the anchor activity scene, and simultaneously adds first-class inline preview support for .md/.mkd/.mkdn files across the stack (MIME types, token allowlist, CSS, and a new loadMarkdownInline renderer).

  • Journal reconstruction (api/routes.py): Two new accumulators are populated during the event loop — artifact_reference payloads produce artifact entries with kind, path, and event_id; state_saved payloads produce side-effect entries with kind, name, action, and event_id. Both lists are attached to the returned anchor_activity_scene.
  • Markdown inline preview (static/ui.js + static/style.css): A new async loadMarkdownInline() function fetches a .md file, passes the text through the existing renderMd() sanitizer (256 KB cap), and replaces the placeholder element with the rendered HTML, matching the patterns used by the HTML/CSV/PDF inline loaders.
  • MIME/token allowlisting (api/config.py + api/routes.py): text/markdown is added to MIME_MAP and _SESSION_MEDIA_TOKEN_TYPES so markdown attachments are served inline and counted against the session media quota.

Confidence Score: 4/5

Safe to merge; changes are additive and don't touch existing event-processing paths.

The journal reconstruction logic is straightforward accumulation with proper guards for malformed payloads. The markdown inline preview follows established async-loader patterns and reuses the well-tested renderMd sanitizer. Two minor issues stand out: the raw payload dict is echoed verbatim into each reconstructed artifact/side-effect entry (widening what the snapshot serialises to the client), and the event_id truthiness guard would silently drop a zero-valued ID. Neither causes incorrect behaviour today but both are worth a follow-up.

api/routes.py — the payload passthrough and event_id guard in the new event handlers.

Important Files Changed

Filename Overview
api/routes.py Adds artifact/side_effect accumulators and event handlers in _run_journal_live_snapshot; hydrates the anchor_activity_scene with both lists. The full raw payload dict is stored verbatim alongside extracted fields; event_id uses a truthiness guard that would silently drop event_id=0.
static/ui.js Adds _MD_EXTS regex, loadMarkdownInline() async fetcher, and wires it into postProcessRenderedMessages. Rendered markdown is sanitized by the existing renderMd() pipeline; Mermaid/KaTeX placeholders inside markdown file previews won't receive follow-up post-processing because renderMermaidBlocks/renderKatexBlocks run synchronously before the fetch resolves.
api/config.py Adds .md, .mkd, .mkdn to MIME_MAP. Consistent with the JS extension regex and the session token allowlist change.
static/style.css Adds markdown inline preview CSS classes (loading state, header bar, content area). Mirrors the existing HTML preview style conventions.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant C as Client (Browser)
    participant S as Server (routes.py)
    participant J as Run Journal

    Note over C,J: Reconnect / hard-refresh / session switch
    C->>S: GET /snapshot (stream_id)
    S->>J: read_run_events(session_id, stream_id)
    J-->>S: events[]
    S->>S: Loop events
    S->>S: artifact_reference to artifacts[]
    S->>S: state_saved to side_effects[]
    S-->>C: "anchor_activity_scene { artifacts, side_effects, activity_rows }"

    Note over C,J: Markdown inline preview (new)
    C->>C: postProcessRenderedMessages()
    C->>C: loadMarkdownInline() async
    C->>S: "fetch api/media?path=file.md"
    S-->>C: text/markdown content
    C->>C: renderMd(text) sanitize and render
    C->>C: "el.outerHTML = rendered HTML"
    Note right of C: renderMermaidBlocks and renderKatexBlocks already ran
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant C as Client (Browser)
    participant S as Server (routes.py)
    participant J as Run Journal

    Note over C,J: Reconnect / hard-refresh / session switch
    C->>S: GET /snapshot (stream_id)
    S->>J: read_run_events(session_id, stream_id)
    J-->>S: events[]
    S->>S: Loop events
    S->>S: artifact_reference to artifacts[]
    S->>S: state_saved to side_effects[]
    S-->>C: "anchor_activity_scene { artifacts, side_effects, activity_rows }"

    Note over C,J: Markdown inline preview (new)
    C->>C: postProcessRenderedMessages()
    C->>C: loadMarkdownInline() async
    C->>S: "fetch api/media?path=file.md"
    S-->>C: text/markdown content
    C->>C: renderMd(text) sanitize and render
    C->>C: "el.outerHTML = rendered HTML"
    Note right of C: renderMermaidBlocks and renderKatexBlocks already ran
Loading

Reviews (1): Last reviewed commit: "fix(#6212): reconstruct Anchor outcomes ..." | Re-trigger Greptile

Comment thread api/routes.py
Comment on lines +3307 to +3318
entry: dict = {
"source_event_type": "artifact_reference",
"payload": dict(payload),
}
for key in ("kind", "path"):
value = payload.get(key)
if value is not None:
entry[key] = str(value)
event_id = event.get("event_id")
if event_id:
entry["event_id"] = str(event_id)
artifacts.append(entry)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Raw payload stored alongside extracted fields

Each entry includes "payload": dict(payload) — a shallow copy of the entire original journal event payload — in addition to the explicitly extracted kind/path/event_id fields. If any future payload field is large or sensitive, this copy will silently widen what's sent to the frontend on every snapshot reconnect. The same pattern applies to state_saved entries.

Consider whether the raw payload passthrough is intentional; if it's only needed as a debugging fallback, restricting it to a development flag or omitting it entirely would reduce surface area.

Comment thread api/routes.py
Comment on lines +3315 to +3317
event_id = event.get("event_id")
if event_id:
entry["event_id"] = str(event_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Falsy guard silently drops event_id = 0

if event_id: evaluates to False when event_id is the integer 0 or an empty string. If the journal backend ever uses zero-based auto-increment IDs or emits an empty-string sentinel, the entry would be returned without event_id, which could break consumer-side deduplication. A stricter guard like if event_id is not None: would be safer.

Comment thread static/ui.js
Comment on lines 18659 to 18692
});
}

const MD_MAX_SIZE = 256 * 1024;

function loadMarkdownInline(container){
const root = container || document;
root.querySelectorAll('.md-inline-load:not([data-loaded])').forEach(el => {
el.setAttribute('data-loaded', '1');
const path = el.dataset.path;
const fname = path.split('/').pop() || path;
const mediaSessionId = (typeof S !== 'undefined' && S && S.session && S.session.session_id) ? String(S.session.session_id) : '';
const publicMediaUrl = 'api/media?path=' + encodeURIComponent(path);
const mediaUrl = publicMediaUrl + (mediaSessionId ? '&session_id=' + encodeURIComponent(mediaSessionId) : '');
const downloadUrl = mediaUrl + '&download=1';
fetch(mediaUrl)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(text => {
if (text.length > MD_MAX_SIZE) {
el.outerHTML = `<div class="md-inline-fallback"><a class="msg-media-link" href="${esc(downloadUrl)}" download="${esc(fname)}">📎 ${esc(fname)}</a><br><span style="color:var(--muted);font-size:12px">${esc(typeof t === 'function' ? t('md_too_large') : 'File too large for preview')}</span></div>`;
return;
}
const rendered = renderMd(text);
el.outerHTML = `<div class="md-inline-wrap"><div class="md-inline-header"><span class="md-preview-title">${esc(fname)}</span><a class="msg-media-link" href="${esc(downloadUrl)}" download="${esc(fname)}">📎 ${esc(fname)}</a></div><div class="md-inline-content">${rendered}</div></div>`;
})
.catch(() => {
el.outerHTML = `<div class="md-inline-fallback"><a class="msg-media-link" href="${esc(downloadUrl)}" download="${esc(fname)}">📎 ${esc(fname)}</a><br><span style="color:var(--muted);font-size:12px">${esc(typeof t === 'function' ? t('md_error') : 'Error loading markdown')}</span></div>`;
});
});
}

function renderMermaidBlocks(container){
const root=container||document;
const blocks=root.querySelectorAll('.mermaid-block:not([data-rendered])');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Mermaid/KaTeX blocks inside markdown file previews will not render

loadMarkdownInline is asynchronous (fetch-based), so by the time the markdown content is fetched and el.outerHTML is set with renderMd(text) output, renderMermaidBlocks and renderKatexBlocks have already run synchronously in postProcessRenderedMessages. Any ```mermaid ``` fence or $$...$$ block inside the previewed file will be left as an inert placeholder div. This is the same limitation shared by the other async loaders, but worth noting since renderMd actively generates those placeholder elements.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M Medium PR (≤10 files, ≤250 LOC)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants