Skip to content

feat: ATIF post-processor worker with event-driven dispatch, portal UI, and resilience - #948

Closed
Cedric Vidal (cedricvidal) wants to merge 43 commits into
mainfrom
cedricvidal-microsoft/refactored-umbrella
Closed

Cedric Vidal (cedricvidal) wants to merge 43 commits into
mainfrom
cedricvidal-microsoft/refactored-umbrella

Conversation

@cedricvidal

Copy link
Copy Markdown
Collaborator

Original author: Cedric Vidal (@cedricvidal)

Summary

Implements #780 — ATIF (AI Tool Interaction Format) file generation via a new post-processor worker, along with Portal UI, API refactoring, resilience utilities, and developer skills.

Key Changes

ATIF Post-Processor Worker (apps/workers/post-processor/)

  • New queue-based worker that generates ATIF trajectory files from completed runs
  • Uses the atifact package to convert HAR recordings into the standardized ATIF v1.7 format
  • Uploads atif.trajectory.json to blob storage per iteration
  • Stores the URL in the request document for API serving
  • Triggers report generation after post-processing succeeds (with retry)
  • Proper temp directory cleanup with rm(recursive) instead of unlink
  • Clarifying comments on container coupling and concurrency model

Event-Driven Post-Processing Dispatch

  • Coder workers enqueue post-processing immediately on run completion instead of relying solely on the scheduler polling loop
  • Sets run.postProcessorStatus: "queued" atomically in the same DB write
  • Existing PostProcessorDispatcher polling loop repurposed as backfill-only (interval increased from 2s to 30s, batch 10 → 30)
  • Added onRunTerminal hook in BaseQueueProcessor for error-path dispatch
  • Queue-send failure rollback: resets postProcessorStatus via $unset if sendMessage fails after all SDK retries

Portal UI

  • Enrichment status shown in RunsList and RunDetail pages (renamed from "post-processing")
  • ATIF download button on each iteration card
  • Enrichment card in run details tab with version info on badge hover
  • <EnrichmentBadge> component — extracted reusable badge with consistent labels across all usages (Pending / Enriching… / Enriched / Failed)
  • Auto-refresh fix — RunDetail continues polling while post-processing is in progress (previously stopped when run status reached "done")

API Endpoint Refactoring

  • registerArtifactRoutes() helper — generic route-pair registration for request-level and per-run artifact downloads (ATIF, HAR, tool-calls, snapshots)
  • downloadBlobToResponse() helper — shared blob download logic with Content-Type/Disposition/Length handling
  • Eliminates ~60 lines of boilerplate per endpoint
  • Null check for resource.run in artifact-route (prevents crash on requests without run data)
  • Content-Disposition filename escaping (defensive for non-UUID filenames)

ATIF API Endpoints

  • GET /api/v1/requests/:id/atif?iteration=N — download ATIF for a request
  • GET /api/v1/requests/:id/runs/:runId/atif?iteration=N — download ATIF for a specific run
  • iteration parameter is mandatory (returns 400 if missing)
  • ATIF included in archive export/import

Resilience Utilities

  • @Retry decorator — exponential backoff with configurable maxRetries, baseDelayMs, and isRetryable predicate (packages/shared/src/resilience/)
  • withRetry() function — standalone retry wrapper for non-class contexts
  • Applied to report trigger call in post-processor
  • Architecture doc: docs/architecture/retry.md

Report Generator

  • Added get_atif_trajectory tool so reports can reference ATIF data
  • Typed report tool API responses with RequestDocument
  • Fixed bug: report tools reading turns from wrong path in API response

Infrastructure

  • Database migration 017: compound index for post-processor dispatch queries
  • Docker Compose: register-version-post-processor init service for version registration
  • Worker profiles added to dev scripts
  • required: false on mongodb dependency for shared infra mode

Documentation

  • docs/architecture/post-processing.md — pipeline architecture with Mermaid diagrams, handler extensibility, version management
  • docs/architecture/retry.md — retry utilities reference

Developer Skills

  • Resilience — retry, backoff, circuit breaking patterns (.agents/skills/resilience/)
  • TypeScript — best practices for the monorepo (.agents/skills/typescript/)
  • CosmosDB for MongoDB — best practices (.agents/skills/cosmosdb-mongodb/)
  • Documentation — Mermaid diagram guidelines (.agents/skills/documentation/)

Testing

  • 14 dedicated ATIF endpoint tests (atif.test.ts)
  • 5 post-processor dispatch tests (happy path, no-op, error tolerance, stale-heartbeat, queue-send rollback)
  • All 1785 tests pass

Closes #780

Implement automatic ATIF (Agent Trajectory Interchange Format) file
generation after coding runs complete. The system uses a new extensible
post-processor worker with a handler registry pattern.

Key components:
- Post-processor worker (apps/workers/post-processor/) with AtifHandler
  that converts HAR captures to ATIF v1.7 JSON using the atifact package
- PostProcessorDispatcher in the scheduler with atomic claim pattern
  to detect completed runs needing processing (real-time + backfill)
- Version tracking via K8s Job writing to MongoDB services collection
- API download endpoints: GET /requests/:id/atif and /runs/:runId/atif
- ATIF included in archive exports
- K8s deployment + KEDA ScaledObject + FluxCD version registration Job

Architecture:
- Scheduler-driven dispatch (single path for real-time and backfill)
- postProcessorVersion + postProcessorStatus fields on RunState
- atifUrl field on ConversationTurn
- MongoDB-only version storage (no Redis key needed)
- Extensible: add new handlers, bump version, auto-backfill

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add postProcessorStatus and postProcessorVersion to portal RunState type
- Show post-processing badge in RunDetail header (queued/processing/failed)
- Add Post-Processing column to RunsList table with status badges
- Badge is hidden when status is 'done' on RunDetail (no noise)
- Column visible by default; users can hide via column picker

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Show the badge for all states including 'done' (green 'Post-processed')
so users always see the post-processing status in the header area.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously the badge was hidden when postProcessorStatus was undefined,
meaning completed runs that haven't been dispatched yet showed nothing.
Now shows 'pending' for done runs with no status or queued status.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use shorter, cleaner terminology throughout the UI:
- RunDetail: 'Enrichment pending', 'Enriching…', 'Enriched', 'Enrichment failed'
- RunsList column header: 'Enrichment'
- RunsList cells: 'Pending', 'Enriching', 'Enriched', 'Failed'

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Shows a 'Download ATIF' button next to 'Download Archive' when the run
has been enriched (postProcessorStatus === 'done') and turns have atifUrl.
Also adds atifUrl to the portal ConversationTurn type and api helper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Single iteration: shows one 'ATIF' button.
Multiple iterations: shows 'ATIF #1', 'ATIF #2', etc. as individual buttons.
Each downloads the specific iteration's trajectory.json file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds an 'ATIF' button next to 'Snapshot' and 'HAR' on each iteration
card in the Turns tab, consistent with existing per-iteration downloads.
Removes the separate header-level ATIF buttons in favor of this approach.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Shows enrichment status, version, and artifact count (ATIF trajectories)
in the Details tab alongside Scenario, Persona, and Version Info cards.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds title attribute to enrichment badges showing 'Enrichment v1' (or
whatever version) on hover, in both RunDetail header and RunsList table.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move the report trigger from coding workers (fire-and-forget on run
completion) to the post-processor worker (after enrichment succeeds).
This ensures reports can leverage enriched data like ATIF files.

Changes:
- Remove triggerReportGeneration() call from queue-processor.ts
- Add triggerReportGeneration() to PostProcessor after status=done
- Wire SCOPE_MT_API_URL config into post-processor
- Add api dependency in docker-compose for post-processor

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
3 attempts with exponential backoff (1s, 2s, 4s). Client errors
(4xx) are not retried since they indicate a permanent issue.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Documents withRetry function and @Retry decorator in
docs/architecture/retry.md. Links from AGENTS.md doc table.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Provides a decision checklist, retry/no-retry table, configuration
defaults, and anti-patterns. Triggered when writing or reviewing
code that makes HTTP/REST calls or service-to-service interactions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The portal sends null for systemPrompt, model, and timeoutMs when
clearing these fields. The API handler already interprets null as
'remove field', but Zod rejected null at validation time. Added
.nullable() to these optional fields.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Allows the reporting agent to fetch and analyze ATIF trajectory data
for any iteration, enabling richer analysis of agent step-by-step behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The tools were reading `response.turns` but the API returns turns
nested under `response.run.turns`. This caused the report agent to
always see 0 turns, producing useless reports.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prevents future data-path bugs by annotating response.json() with the
shared RequestDocument type. TypeScript now catches incorrect property
access at compile time.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Covers typing response.json(), avoiding any, preferring optional
chaining over fallback objects, and using shared types from packages/shared.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Creates compound index {run.postProcessorStatus: 1, updatedAt: -1} on
requests collection to optimize the PostProcessorDispatcher polling query.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Covers: indexing strategy (single-field for filters, 2-field compounds for
ORDER BY), silent index drops, migration patterns, and RU cost awareness.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Aligns with the shared-infra pattern used by all other services — mongodb
container is not started when SCOPE_SHARED_INFRA=1 (using Azure CosmosDB).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The scheduler's PostProcessorDispatcher requires a version entry in the
services collection. In K8s this is a separate Job, but in local dev
(and with fresh databases like shared CosmosDB) the entry didn't exist,
causing the dispatcher to skip all dispatch (targetVersion=0).

Now the post-processor registers its version on startup before entering
the poll loop, making it work reliably regardless of environment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This reverts commit 128ec83f33f126de2bbca8f5fa35782e1a9f9c6a.
Follows the same pattern as the register-agents service and K8s Jobs.
Runs the register-version.ts script before the post-processor starts,
ensuring the scheduler can dispatch post-processing work.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The iteration query parameter is now required for both /atif routes.
Returns 400 if missing, instead of silently defaulting to the last
iteration with an ATIF file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…points

Deduplicate blob download logic shared across ATIF, HAR, tool-calls, and
snapshots endpoints into a single downloadBlobToResponse() helper in
blob-helpers.ts. Each endpoint retains its own URL-resolution logic
(iteration parsing, field extraction, fallback behavior) but delegates
the blob-fetch-and-pipe step to the shared helper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirrors the pattern of har.test.ts, tool-calls.test.ts, and
snapshots.test.ts with coverage for: missing iteration (400),
invalid iteration (400), zero iteration (400), no ATIF available (404),
successful proxy download, and blob-not-found (404).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ration

Introduces artifact-route.ts with a generic registerArtifactRoutes()
that registers both the request-level and per-run variants of an
artifact download endpoint with shared findOne/resolveRun/RestError
handling. Refactors atif, har, tool-calls, and snapshots to use it,
reducing each file from ~90 to ~35 lines.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Renames the blob storage path, archive entry name, and download
Content-Disposition filename from trajectory.json to
atif.trajectory.json so the file purpose is immediately obvious.

Existing blobs are unaffected — atifUrl in the DB is a full URL so
old runs continue to serve correctly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Coder workers now enqueue post-processing directly when a run reaches
terminal state, eliminating the 2s polling delay. The existing
PostProcessorDispatcher polling loop remains as a backfill/catch-up
mechanism for version upgrades and missed events.

Changes:
- Add postProcessorQueueName to QueueProcessorConfig
- CodingAgentQueueProcessor creates a QueueClient for the post-processor
  queue and calls enqueuePostProcessing after finalWrite succeeds
- Set run.postProcessorStatus="queued" atomically in finalWrite to
  prevent the polling dispatcher from double-enqueuing
- Add onRunTerminal hook in BaseQueueProcessor for error-path dispatch
- All three coder workers pass QUEUE_NAME_POST_PROCESSOR config
- 4 new tests covering happy path, no-op, error tolerance, and
  stale-heartbeat redelivery dispatch

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… to 30

Now that coder workers dispatch post-processing event-driven on run
completion, the polling loop only serves as backfill/catch-up. Reduce
RU consumption by polling every 30s instead of 2s (separate env var
SCHEDULER_PP_POLL_INTERVAL_MS). Increase batch size to 30 so backfills
still make good progress per tick.

Also documents SCHEDULER_POLL_INTERVAL_MS, SCHEDULER_PP_POLL_INTERVAL_MS,
and QUEUE_NAME_POST_PROCESSOR in ENV_VARIABLES.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…/refactored-umbrella

# Conflicts:
#	pnpm-lock.yaml
Covers ATIF generation flow, handler extensibility, version management,
status lifecycle, configuration, and infrastructure details.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix unlink → rm(recursive) for temp directory cleanup in atif-handler
- Add queue-send failure rollback in post-processor dispatcher
- Fix artifact-route null check for resource.run
- Escape quotes/backslashes in Content-Disposition filename
- Fix migration 017 comment (reference migration 014, not 009)
- Extract EnrichmentBadge component (dedup labels across portal)
- Replace turns! non-null assertion with optional chaining
- Add clarifying comments (container coupling, optimistic locking)
- Update atif-handler test mock (unlink → rm)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously, polling stopped as soon as run.status reached 'done'.
Post-processing happens after run completion, so the enrichment badge
never updated without a manual page refresh. Now polling continues
until postProcessorStatus is also terminal (done/failed).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@cedricvidal

Copy link
Copy Markdown
Collaborator Author

Original author: @github-actions[bot]

Test Results (Node.js 22)

test: Run #1626

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
1763 1763 0 0 0 0 0 1m11s

🎉 All tests passed!

Github Test Reporter

@cedricvidal

Copy link
Copy Markdown
Collaborator Author

Original author: @github-actions[bot]

Test Results (Node.js 22)

test: Run #1627

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
1763 1763 0 0 0 0 0 1m6s

🎉 All tests passed!

Github Test Reporter

@cedricvidal

Copy link
Copy Markdown
Collaborator Author

Original author: @github-actions[bot]

Test Results (Node.js 22)

test: Run #1628

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
1766 1766 0 0 0 0 0 1m8s

🎉 All tests passed!

Github Test Reporter

@cedricvidal

Copy link
Copy Markdown
Collaborator Author

Original author: @github-actions[bot]

Test Results (Node.js 22)

test: Run #1633

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
1784 1784 0 0 0 0 0 1m9s

🎉 All tests passed!

Github Test Reporter

@cedricvidal

Copy link
Copy Markdown
Collaborator Author

Original author: @github-actions[bot]

Test Results (Node.js 22)

test: Run #1635

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
1784 1784 0 0 0 0 0 1m11s

🎉 All tests passed!

Github Test Reporter

@cedricvidal

Copy link
Copy Markdown
Collaborator Author

Original author: @github-actions[bot]

Test Results (Node.js 22)

test: Run #1637

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
1784 1784 0 0 0 0 0 1m10s

🎉 All tests passed!

Github Test Reporter

@cedricvidal

Copy link
Copy Markdown
Collaborator Author

Original author: @github-actions[bot]

Test Results (Node.js 22)

test: Run #1649

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
1785 1785 0 0 0 0 0 1m9s

🎉 All tests passed!

Github Test Reporter

@cedricvidal

Copy link
Copy Markdown
Collaborator Author

Original author: @github-actions[bot]

Test Results (Node.js 22)

test: Run #1650

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
1785 1785 0 0 0 0 0 1m9s

🎉 All tests passed!

Github Test Reporter

@cedricvidal

Copy link
Copy Markdown
Collaborator Author

Original author: Wassim Chegham (@manekinekko)

Portal UI Review

✅ What looks good

  • Types are well-defined — postProcessorStatus is a proper union type, atifUrl is optional
  • Consistent badge styling — variant logic (success/destructive/secondary) is applied consistently
  • Column visibility — new "Enrichment" column follows the existing isCol() pattern and is visible by default (since only hidden columns are stored)
  • Enrichment card on RunDetail is a clean addition with status, version, and artifact count

Issues to flag

1. Duplicated badge rendering logic (3 places)
The status→label and status→variant mapping is copy-pasted across RunDetail.tsx (header badge, enrichment card) and RunsList.tsx (row cell). If a new status is added or labels change, all three must be updated in sync.

Suggestion: Extract an <EnrichmentBadge status={…} version={…} /> component.

2. Inconsistent labels between badge and card
The header badge says "Enrichment pending" / "Enriching…" (with ellipsis), but the card below says "Pending" / "Enriching" (no ellipsis, no prefix). Minor, but could confuse users seeing both on the same page.

3. turns! non-null assertion in RunDetail (~line 1022)

activeRun.turns!.filter(t => t.atifUrl).length

This is called twice with !. If turns is somehow undefined despite the ?.some() guard above, this crashes. Safer to use optional chaining or store the filtered result in a variable.

4. No error/retry UX for failed enrichment
When postProcessorStatus === "failed", the user sees a red "Failed" badge but has no way to retry or see the error. Consider adding a retry button or linking to logs.

5. ATIF download opens raw blob URL in new tab

window.open(api.atifUrl(runId, turn.iteration), "_blank");

This opens JSON in a blank tab. A proper file download (via Content-Disposition: attachment) would be a better UX.

6. Grouped rows show dash for enrichment
Group-level rows always render – for the enrichment column. This is fine for now, but if groups aggregate other stats (e.g., outcome), it might be worth showing an aggregate enrichment status too.


Post-Processor Worker Review

# Severity Finding
1 🔴 High unlink() on a directory leaks temp dirs — atif-handler.ts:98 calls unlink(tempDir) but unlink is for files only. Will fail with EISDIR, silently caught by .catch(() => {}). Use rm(tempDir, { recursive: true }) instead.
2 🔴 High Hidden container coupling in blob download — atif-handler.ts:66 calls downloadBlobToBuffer(blobName) assuming the "snapshots" container. No validation that the extracted container matches.
3 🟡 Medium No optimistic locking on status updates — post-processor.ts:68-93 updates status without verifying it hasn't been modified concurrently. Could conflict if a message is processed twice.
4 🟡 Medium Silent error suppression in cleanup — atif-handler.ts:97-98 swallows all cleanup errors. Should log at debug/warn level instead.

Event-Driven Dispatcher Review

# Severity Finding
1 🔴 High Queue send failure permanently orphans runs — post-processor-dispatcher.ts:82: if sendMessage() fails, the document stays "queued" in MongoDB but the message never reaches the queue. The polling filter (line 56) excludes "queued" docs, so it's never retried. Needs status rollback on queue send failure.
2 🟡 Medium No test for queue send failure — The test suite only covers the happy path. The critical failure/rollback scenario above has no coverage.

API Refactoring Review

# Severity Finding
1 🔴 Critical Null pointer crash on HAR endpoint — har.ts:27 accesses targetRun.turns without null check. artifact-route.ts:89 passes resource.run! without validating it exists. If a request has no run (it's optional per the schema), the server crashes with TypeError.
2 🟡 Medium Content-Disposition header not escaped — blob-helpers.ts:64 interpolates filename without escaping quotes/backslashes. Safe today (UUIDs only), but the helper is reusable and could be called with unsafe input later.

Shared Package & Migrations Review

# Severity Finding
1 🟡 Medium Migration comment references wrong migration — 017-add-post-processor-dispatch-index.ts:12 says run.status index comes from migration 009, but 009 created status (top-level). The run.status index was created in migration 014.

No breaking changes to shared types, queue processor, retry logic, or blob storage. ✅


Infrastructure & Deployment Review

✅ No issues found. Env vars, resource limits, queue names, Docker, kustomize, and service dependencies all follow established patterns correctly.


Summary

Component 🔴 Critical/High 🟡 Medium ✅ Clean
Portal UI — 6 —
Post-Processor Worker 2 2 —
Event-Driven Dispatcher 1 1 —
API Refactoring 1 1 —
Shared Package & Migrations — 1 —
Infrastructure & Deployment — — ✅

Top priority fixes: the HAR null pointer crash (API), the queue send orphaning runs (dispatcher), and the unlink() on a directory (worker).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ATIF file generation

1 participant