Changelog
2026-10-04 — Re-pin the e2e runtime to v0.8.8
docker-compose.e2e.yml now defaults to ghcr.io/multiagentcoordinationprotocol/macp-runtime:0.8.8
(was the v0.8.0 SHA tag f97fd15). Release builds now publish semver image tags, so the SHA workaround is
gone. macp-control-plane / macp-playground pin 0.8.6; this stack is two patch releases ahead of
them. Override with MACP_RUNTIME_IMAGE. This resolves the "Known drift" item in the entry below.
2026-10-04 — Docs sync: post-0.8.0 fixes, dependency majors, de-duplication against sibling docs
Covers the work merged after the 2026-09-23 entry (#39, #43, #46–#50) and trims console docs that restated sibling-repo content. No app-code change in this entry.
Since the last entry
- #46 — live-run reconnect no longer targets a stale run.
attemptReconnectreconnects throughconnectSSERef, so navigating run A → B without a remount no longer re-subscribes to A after a heartbeat timeout or SSE error. Details:api-integration.md § SSE integration. - #47 — dependency majors. mermaid 12 (the Mermaid 11 look is pinned in
components/docs/mermaid-block.tsx), jsdom 30, vitest 5, jest-dom 7; CI runs on Node 22 (and #39 bumpedactions/setup-nodeto v7);engines.nodeis>=22.22.2. - #48 — policy
schemaVersiondefault is 3 at the control plane (fail-closed on an empty decisive tally) and unknown rule keys are rejected (macp-control-plane#96). No console behaviour change — the registration form already sent an explicit value. - #49 — docs loader marks its dynamic
fscallsturbopackIgnoreso the build stops tracing the whole project (lib/docs/loader.ts). - #43 / #50 —
docs-content/macp-playground/re-synced from macp-playground (#43: policy authoring, direct-agent-auth, scenario authoring, worker bootstrap contract; #50: deployment). Those files are automated copies; edit them upstream, not here.
De-duplication
Rule: console docs describe what the console does and link to the owning repo for everything else.
api-integration.md: runtime notes,POST /runtime/policiesvalidation rules, the control-plane error envelopes and the SSE resume rationale are now short console-side summaries linking tomacp-runtime/docs,macp-control-plane/docs/API.mdandINTEGRATION.md.backend-repo-notes.md: the credential-scoping (FORBIDDEN:) walkthrough is reduced to the console triage rule and where to read the reason; the mechanism lives inmacp-control-plane/docs/INTEGRATION.md.
Known drift (not changed here)
- Sibling repos have moved on since the e2e pin: macp-runtime is at v0.8.8, macp-control-plane absorbed
runtime v0.8.1–v0.8.6 (#96), and both SDKs are at 0.14.x.
docker-compose.e2e.ymlstill pins runtimef97fd15(v0.8.0), and the docs describe that pin accurately. Re-pinning is an infra change that needs a stack run, so it is left as a follow-up.
2026-09-23 — Absorb macp-control-plane / macp-playground (Jul–Sep 2026), runtime v0.8.0
Absorbs the control-plane and playground work landed since the v0.5.0 window, and repoints the
dev/e2e stack at runtime v0.8.0 (ghcr.io/multiagentcoordinationprotocol/macp-runtime:f97fd15,
the tag both sibling repos pin). Mostly correctness and fidelity work in existing surfaces —
no data migration and no wire-contract change on the console side.
Correctness
CommitmentAuthoritywire value correcteddesignated_roles→designated_role. This repo was the only one in the stack with the plural; the runtime matcher, the runtime validator, the control plane's rejection check, the playground contract mirror and a shipped policy file all use the singular. The playground now rejects the mistake explicitly withdesignated_role authority requires a non-empty designated_roles array. The sibling array fielddesignated_roles: string[]keeps its plural — it is a different thing.- SSE resume cursor no longer skips events.
lastSeq— what goes out as?afterSeq=on every reconnect — was derived fromtimeline.latestSeq, the server's head, in three places. The load-bearing one was thesnapshothandler: the control plane republishes a snapshot on every commit batch, so the cursor was dragged to the head continuously during normal streaming and anything not yet delivered was skipped permanently. The cursor is now derived only from events actually received. NoteafterSeqis exclusive and replay is gated onafterSeq > 0, so0means "snapshot plus live tail", not "send everything". - The live event rail no longer collapses to a single row. The hook reads its seed events at mount only, and the live route mounts before the history query settles, so the fetched history was shown until the first live event arrived and then discarded. The two sources are now merged by id and ordered by seq.
- That merge no longer orders on a comparator that can return
NaN.seqis typednumberbut reaches the console throughJSON.parseof an SSE frame, so the type is a claim about the wire, not a guarantee — which is why the resume cursor above isNumber.isFinite-guarded.a.seq - b.seqreturnsNaNagainst a missing or non-numericseq, and aNaN-returning comparator is not a valid ordering: the engine may leave the array in any order at all, so a single malformed frame could scramble the whole rail rather than merely misplace itself. Unusable values now sort to the end, in arrival order.
Fidelity & errors
decision.current.supersedessurfaces the non-canonical/canonical distinction.- Structured
errorCodeonApiError, with the control plane's three error envelopes handled distinctly: anAppExceptioncarries a real code; a Nest exception with an object body is emitted verbatim and usually has noerrorCode(this is what everyPOST /runtime/policiesvalidation rejection looks like); and a string body or unhandled error is rewritten with a hardcodedINTERNAL_ERROR. That last one is why a throttled 429 arrives labelledINTERNAL_ERROR— a presenterrorCodeis not always a real classification. ReadmessageviadescribeApiErrorrather than branching on a code. - Runtime session drift (
GET /admin/runtime/sessions) surfaces on the Infrastructure tab: sessions the runtime holds with no control-plane run, and runs whose session the runtime has lost. A sweep cut short by its page or time budget reportscomplete: false, so a partial answer is never read as "no drift". - Policy
schemaVersionconstrained to{1, 2, 3}in the request type and the registration form. historyGap— the control plane's own signal that a run's history is incomplete, set when it could not resume the runtime session because that history had been compacted away — is now surfaced as a fidelity notice in the event feed. Its contract doc had asked for exactly this and the console had ignored it. The notice offers no retry, because those envelopes were never recorded: after emitting the gap the control plane degrades to poll-only, and polling returns session state, never the missing events.- Event summaries no longer render half-built strings.
formatEventSubjectreturned a danglingkind:when a subject carried a kind but an empty id, and the outcome suffix onpolicy.resolved/policy.violated/policy.commitment.evaluatedcarried its own separator, producing a doubled· ·. Both fixed. policy.deniednow shows its reasons. The inline denial path populates none of the fields the old summary read, so a denial rendered as the bare words "Policy denied" with the reasons unread in the payload.session.stream.gapgained a label and a/logsfilter entry.- No client-side seq-gap detector, deliberately. Canonical
seqvalues are not contiguous per run — for each runtime envelope persisted, the control plane allocates one sequence span, gives the first value to the raw row and the rest to the canonical events derived from it, and publishes only the canonical ones, so each such batch burns a seq no subscriber ever sees. When a raw envelope yields exactly one canonical event, the published seqs therefore run2, 4, 6, 8— but that ratio is not fixed either: normalization can emit two canonical events for one envelope, or none at all, so the stride varies as well as skipping. (Control-plane-authored lifecycle events take a different path that allocates exactly what it publishes and burns nothing — which is precisely why a client cannot infer loss from a delta: the two paths interleave in one counter.) A detector on "seq jumped, therefore an event was lost" was built and would have warned once per batch on every healthy run; it was removed, and a regression test pins the hook's return surface so it cannot return under another name.
Launch flow
- A failed bootstrap now says so. Previously the spinner simply stopped; the page now surfaces the failure and does not navigate. Both launch triggers — the main submit and the quick-run button — were byte-identical duplicate mutations, so a fix to one would have left the other on the old behaviour; they now share a single mutation.
/examples/runresponses carrycontrolPlaneRun, and the new-run page redirects to the live view usingcontrolPlaneRun.runId. That id is not the session id:POST /runsmints a fresh run id and stores the session id separately, and the live route resolves by run id. When the field is absent the page explains what it does and does not know rather than redirecting somewhere broken.
Upstream behaviour worth knowing (no console change needed)
- Event counts on
/runs/:id/eventscan legitimately go down. The control plane now dropsStreamSessionenvelopes whosesessionIdis empty or belongs to another session; previously a short-circuit let empty ones through. Dropped envelopes are never persisted. - Runs fail faster and more honestly. A stream-loop crash now finalizes the run as
failedimmediately, bounded by a last-resort timeout, instead of leaving it hanging — narrowed so an intentional shutdown is not misreported as a failure. - Large envelopes now arrive. The gRPC receive limit is explicit and raised to 16 MiB; it was previously grpc-js's implicit 4 MB default, which surfaced as mysterious gaps or 503s. A send limit was added alongside it.
- Proto moved 0.1.9 → 0.1.10 — audited, and it touches nothing the console reads. The whole
delta across every
.protofile is a four-line comment change onPolicyDescriptor.schema_versioninpolicy.proto, documenting that RFC-MACP-0012 now defines version 3. No field was added, removed, renamed, re-nested or retyped.policy.protois loaded only for the gRPC policy RPCs and is not among the descriptors the control plane decodes intodecodedPayload, so it cannot reach the event stream at all. The console already implements the semantics that bump documents —POLICY_SCHEMA_VERSIONS = [1, 2, 3], with new registrations defaulting to 3. Worth keeping for next time: mode-payloaddecodedPayloadis a straight pass-through of the proto decode, so a future bump touchingdecision/proposal/task/handoff/quorum/multi_rounddoes need this check — which is one command,git diff proto-v<old> proto-v<new> -- packages/proto-npmagainst the monorepo's tags, not the open-ended audit it was first assumed to be. Demo fixtures remain non-evidence for upstream drift, since they are this repo's own mock data.
Corrections to this repo's own docs
Several long-standing errors were found while verifying the above, and are worth calling out because each had been wrong for some time:
- The proxy service identifiers were documented as
example/control-plane. They aremacp-playground/macp-control-plane. - The documented backend env var names —
EXAMPLE_SERVICE_BASE_URL,CONTROL_PLANE_BASE_URL,EXAMPLE_SERVICE_API_KEY,CONTROL_PLANE_API_KEY— do not exist. The real names all carry theMACP_prefix. GET /runtime/modesreturns five descriptors, not six. The control plane calls the runtime'sListModes, which returns the standards-track set only;ext.multi_round.v1lives behindListExtModes, for which the control plane exposes no route. Demo mode still shows six — a known divergence, now flagged in the mock data rather than silently disagreeing with production.- The runtime advertises
list_changed: falsefor roots, so the console never watches them. The previous claim that "there is no change-notification stream" was wrong about the runtime — aWatchRootsRPC exists and parks idle — even though the console's behaviour was right. - The
Commitment-terminal rule is now enforced at registration, not merely conventional. - The runtime exposes 24 gRPC RPCs; the notes said 22.
- Policy management moved to
/policies; docs still pointed at/settings. - Mock runtime manifest
protocolVersion→0.1.10. It tracks the proto package, not the runtime build. The previous0.5.0came from this changelog's own 2026-07-07 entry, which set it alongside the v0.5.0 image pin and conflated the two. A real runtime returns empty manifest metadata, so the field is illustrative — but not invisible:/modesrenders the whole manifest as raw JSON, so in demo mode a wrong value here is a wrong value on screen. - Source line counts were removed from the docs rather than updated —
client.tswas documented as ~460 lines against an actual 1346, and counts like these are guaranteed to rot.
Infra & docs
docker-compose.e2e.ymlruntime pinned toghcr.io/multiagentcoordinationprotocol/macp-runtime:f97fd15(runtime v0.8.0, overridable viaMACP_RUNTIME_IMAGE), matchingmacp-control-planeandmacp-playgroundbyte for byte. The SHA tag rather than a semver one: the runtime repo moved to per-crate tags, somacp-runtime:0.8.0is not a published image. The threeRUNTIME_LIST_SESSIONS_*knobs are present as commented-out entries, with the recipe for forcing the drift table's incomplete-sweep branch and the CircuitBreaker footgun that comes with it.- Demo timeline counters are derived from the event fixtures instead of restating them. They
were hand-written literals and three of the six runs had drifted, so the demo showed "11 events"
beside a 14-row rail. The cancelled run was the worst case: it claimed three events and had no
event fixture at all, which also left
/logs'srun.cancelledfilter entry matching nothing in demo mode — the default. It now has the three events its projection was already asserting, andmock-data.test.tsfails if any counter is restated rather than derived. - Updated:
docs/api-integration.md,docs/architecture.md,docs/feature-matrix.md,docs/backend-repo-notes.md,README.md, and two code comments that had gone stale alongside them (lib/data/mock-data.ts,app/modes/page.tsx). AlsoCLAUDE.md, which is gitignored and so appears in no diff. The in-app/docssurface needs no separate edit — it rendersdocs/*.mddirectly, so there is no second copy to drift. - Not updated, by design: everything under
docs-content/macp-playground/. Those files are auto-synced from the upstream playground repo, so hand edits would be clobbered by the next sync. They still carry v0.5.0-era statements, including a stale"authority": "designated_roles"that upstream has already corrected; the sync will carry it.
2026-07-07 — Absorb macp-runtime v0.5.0 / macp-proto 0.1.6
Absorbs the runtime v0.5.0 release into the console, consumed via the macp-control-plane and macp-playground (both of which shipped their own v0.5.0 absorptions). All changes are additive display / mock / config work — no data migration or wire-contract change.
Lifecycle & events
- Session canonical-event vocabulary corrected to what the control plane actually emits:
session.bound,session.stream.opened,session.state.changed(transitions carrydata.state, e.g.SESSION_STATE_SUSPENDED). The/logsSession filter group andsummarizeEventwere keyed to phantomsession.opened/resolved/expiredevents; those are retained only for legacy exports. Addedrun.suspended/run.resumedsummarizers. - Demo mode now covers the full lifecycle: added a suspended and a cancelled run
(records, projections, metrics with
SESSION_STATE_SUSPENDED/_CANCELLED) so the suspended badge, Resume action, and Suspended KPI render in demos. - Breadcrumb id detection widened for 36-char base64url session ids.
Handoff implicit accepts (RFC-MACP-0010 §5.1)
- Runtime-synthesized implicit handoff accepts (silent target past the accept window)
now render with an
implicitbadge in the live feed,/logs, and the event dialog (isImplicitAccept()), distinguishing them from accepts a participant actually sent.
Registries
/modesnow displaysterminalMessageTypesper mode. Mock descriptors rewritten to match runtimeall_mode_descriptors(): all six modes (was three), quorum terminal set fixed to[Commitment],ext.multi_round.v1bumped to1.0.0./settingspolicy management detects a file-managed (read-only) runtime registry from the CP's405 REGISTRY_READ_ONLYerror, shows a persistent banner, and disables register/unregister instead of looping dead-end toasts.
Launch flow
InitiatorPayload.sessionStartgainsmaxSuspendMs(proto 0.1.5max_suspend_ms, compiled by the Examples Service) — surfaced as a "Max suspend" badge in the launch review. Removed the deprecated inlineSessionStart.contextfield; launch inputs flow only viascenarioMeta.sessionContext.
Infra & docs
docker-compose.e2e.ymlruntime pinned toghcr.io/multiagentcoordinationprotocol/macp-runtime:0.5.0(overridable viaMACP_RUNTIME_IMAGE),MACP_METRICS_ADDR+ port 9464 exposed,MACP_ALLOW_INSECUREkept (now load-bearing). Mock runtime manifestprotocolVersion→0.5.0.- Runtime Prometheus metrics documented as an ops-only surface (the CP does not re-serve them, so the console's runtime-metrics UI was intentionally not built).
docs/api-integration.md, in-app/docs, andREADME.mdupdated for the above.
2026-04-22 — In-console docs surface + Examples Service docs sync
Moves the UI Console and Examples Service docs out of the marketing website and into the Console app itself at /docs. Operators now read the docs next to the product.
New
/docs— single-page landing with hand-authored themed SVG diagrams (architecture, scenario-pack pipeline, run flow) plus a doc index./docs/macp-ui-console/[slug]— renders this repo'sdocs/*.mdviareact-markdown+remark-gfm. Mermaid code fences render as diagrams via a lazy-loaded client component that re-renders on theme change./docs/macp-playground/[slug]— renders markdown fromdocs-content/macp-playground/, which is synced from the upstream repo.- Sidebar gains a Docs entry (BookOpen icon, immediately after Dashboard).
- New sync workflow
.github/workflows/sync-examples-docs.yml+scripts/sync-examples-docs.sh. Listens forrepository_dispatch: docs-updatedfrom the macp-playground repo (plus a 6-hour fallback schedule), copies the docs, and opens an auto-PR. lib/docs/loader.ts— small server-side helper (title / first-paragraph extraction).components/docs/markdown-renderer.tsxwith link rewriting (relative.md→/docs/<collection>/<slug>, cross-repo paths → GitHub URLs), styled code / tables / blockquotes, andgithub-sluggerheading anchors.
Removed
.github/workflows/notify-website.yml— the console is the docs host now, not a source for the website. Website-side cleanup (removingapp/console/+app/macp-playground/+ their sync blocks) lands in a separate website commit.
Dependencies
- Added
react-markdown,remark-gfm,mermaid,github-slugger.
Operator note
The macp-playground repo's notify-website.yml is retargeted at ui-console in a sibling commit. Once that ships, a push to macp-playground main automatically opens a sync PR here. Merging the PR redeploys the Console with fresh Examples Service docs.
2026-04-22 — Docs refresh: cross-repo references, reduced duplication
Backend docs (macp-playground, macp-control-plane, runtime, python-sdk, typescript-sdk) were all refreshed upstream. The UI console docs are realigned to reference those sources rather than mirror their endpoint tables. No UI code changes.
backend-repo-notes.mdrewritten as a pointer index. Per-service endpoint inventories removed; each service now links to its canonical upstream doc plus a short note on the UI-facing role.api-integration.mdtrimmed: endpoint schemas now point tomacp-control-plane/docs/API.mdandmacp-playground/docs/api-reference.md; the UI-specific pieces (proxy, normalizers, SSE hook, demo mode, launch sequences) remain documented inline.architecture.mdgained an Upstream services section linking to each repo's architecture doc (CP, ES, runtime, SDKs). Client line counts resynced.README.mdgained an Upstream repositories table and updated doc index.- All occurrences of "Example Service" normalized to the upstream spelling Examples Service.
2026-04-22 — Observer-only CP alignment, session-interaction rework, Playwright removal
Consolidates a run of UI ↔ backend integration changes (5adcc20, 1045a59, 65433b6, fdbe99f, 6cd9da6, 4965a63, 6da8a76).
Control Plane contract alignment
- Aligned the client with the observer-only Control Plane authority model: agent-originated traffic (messages, signals, context updates) now flows through the runtime SDKs. CP endpoints
POST /runs/:id/messages,POST /runs/:id/signal, andPOST /runs/:id/contextreturn410 Gone.POST /runs/:id/cancelproxies to the initiator agent's cancel callback (or callsCancelSessiondirectly when policy-delegated). POST /runsis the only run-creation path and accepts a whitelisted-safe compiledRunDescriptoronly — CP rejects any body containingkickoff[],participants[].role,commitments[],policyHints, orinitiatorParticipantId. The UI continues to compile via Examples Service, which is already whitelisted-safe.POST /runs/:id/clonerejects non-emptycontextoverrides.CreateRunResponsenow surfacessessionIdalongsiderunId(equal under observer-only; reserved for future auto-discovery).- Extended
RunStateProjectionwith thellmprojection:{ calls[], totals: { callCount, promptTokens, completionTokens, totalTokens, estimatedCostUsd } }pluscontextIdandextensionKeyson therunblock.NodeInspectorsurfaces the LLM tab from this data.
Runtime policy registry integration (RFC-MACP-0012 pass-through)
- New client functions:
listRuntimePolicies,getRuntimePolicy,registerRuntimePolicy,unregisterRuntimePolicyagainst CP/runtime/policies{?mode}and/runtime/policies/:policyId. - Surface:
/policies(registry browser) and/modes(mode-scoped policy list). PolicyBadgeandPolicyPaneltones updated to reflect the revised governance rules and commitment-evaluation contract.- Mock dataset gains
MOCK_RUNTIME_POLICIESto keep demo mode exercisable.
Session interaction simplification
- Removed UI-side message/signal/context POST forms that targeted the now-410 CP endpoints. The runs workbench is now a read-only consumer of CP state, canonical events, and artifacts; agents drive envelopes via
macp-sdk-python/macp-sdk-typescript. - Artifact creation (
POST /runs/:id/artifacts) and projection rebuild (POST /runs/:id/projection/rebuild) remain CP-local and are kept.
Dashboard chart series
- CP now emits a single
decisionOutcomeseries (positive/negative encoded as +1/-1 per bucket) instead of the earlier splitdecisionOutcomePositive/decisionOutcomeNegativearrays. Client and mock data updated; any callers relying on the split fields were removed.
Response normalization cleanup
normalizeRun()no longer synthesizesarchivedAtfrom tags — CP exposes the column directly; the client passes it through unchanged.normalizeEvent()promoted out of the SSE hook and applied uniformly ingetRunEventsandlistEventsso every CP event consumer gets the nestedsource/subjectshape.listEventsgained a session-cachedeventsEndpointMissingflag — when CP returns404for/events, the fallback per-run fan-out is used for the rest of the browser session without re-probing.
Test infrastructure
- Playwright removed (
4965a63) — the visual-regression and E2E spec suites are retired. All golden-PNG baselines deleted. The Vitest + React Testing Library suite remains canonical. - Integration tests under
test/integration/tightened for the observer-only contracts and the new 410 responses (6da8a76).
UI regression fixes
- Color palette and chart/config warning cleanup that regressed during the CP rewrite work (
fdbe99f). - Lint-only fixes across
decision-panel,policy-panel, andrun-workbench(6cd9da6,ddd8a26). ExecutionGraphregained the four extra auto-layout passes lost in the CP rewrite (part of6da8a76).
Docs
api-integration.md,backend-repo-notes.md, andarchitecture.mdregenerated against the currentlib/api/client.ts. Runtime policy endpoints,/admin/circuit-breaker/history,/readyz,/runs/:id/replay/state?seq=,/runs/batch/export, and the Jaeger proxy route are now documented.README.mdroute map includes/modesand/policies; added thenpm run local:{up,down,status}Docker full-stack section.
2026-04-14 (continued) — Phase F + Phase D remainder
Second push on 2026-04-14: completes Phase F (filters + observability) and the
remaining Phase D items (D3 Prometheus parser, D4 tree waterfall, D5 /traces
filters). All items below ship against the backend substantially-complete
plan; no console / lint warnings; npm run build clean.
Added
API client extensions
listEvents(demoMode, query)(lib/api/client.ts) — wrapper around BE §4.1 cross-run/eventsendpoint. Query shape:{ runId, scenarioRef, type, afterTs, beforeTs, afterSeq, limit }. Returns{ data, total, nextCursor }. Demo mode fans out acrossMOCK_RUN_EVENTSwith the same filters so/logsis exercisable without a backend.getRunEventsacceptsRunEventsQuery— new fieldsafterTs,beforeTs,type(csv) map to BE §4.2. Legacy(runId, demoMode, limit, afterSeq)call signature preserved for back-compat.getDashboardOverview(demoMode, query)— accepts{ window, scenarioRef, environment, from, to }per BE §5.1. Forwards as query params; demo mode filtersMOCK_RUNSclient-side so KPIs respond to the pickers.- Chart series extended:
charts.throughput,queueDepth,latencyP50/P95/P99,cost,successRate,decisionOutcomePositive,decisionOutcomeNegative,perScenario— matches BE §5.2 output. getCircuitBreakerHistory(demoMode, window?)— BE §5.3 endpoint. ReturnsCircuitBreakerHistoryEntry[]({ state, enteredAt, reason? }).computeDashboardKpis(runs?)accepts an optional filtered run list so demo mode can respond to scenario/environment filters.
Pure helpers
lib/utils/prometheus.ts—parsePrometheusText()with full support for counters, gauges, histograms (grouped under base name via_bucket/_sum/_countsuffixes), summaries, and escaped quotes in labels.metricSummaryValue()returns the one-value-per-row summary (sum of labeled counters,_countfor histograms).histogramQuantile(metric, p)interpolates percentiles fromlebuckets — same algorithm as Grafana'shistogram_quantile. 13 unit tests.lib/utils/traces.ts—buildSpanTree()builds depth-annotated tree fromspan.references[CHILD_OF], sorts siblings bystartTime, treats orphans as additional roots, and counts descendants.findCriticalPath()post-order DP finds the longest root-to-leaf chain by cumulative duration. 7 unit tests.
New components
PrometheusMetricsTable(components/observability/prometheus-metrics-table.tsx) — sortable, filterable table over parsed exposition. Type chips (counter/gauge/histogram/summary), name/help search, click-row →EventDetailDialogwith per-series breakdown. 4 integration tests.CircuitBreakerTimeline(components/observability/circuit-breaker-timeline.tsx) — horizontal proportional-width state timeline + current-state badge + per-transition table. Freezesnowat mount for render-purity. 3 tests.
Page rewrites
-
/logs(PR-F1) — rewritten against the cross-run/eventsendpoint:- Shared
<RunSelectorFilters>(scenario + runId + time window) - Event type filter with
EVENT_TYPE_GROUPSincl. newLLMgroup - Cursor-based "Load more" pagination (cursors kept as an append-only
useStatearray; each new cursor concatenates a dedup'd page) - Client-side free-text search over the current page
summarizeEvent()one-line summary in the Payload columnEventDetailDialogopens on row click with full payload + Run / Trace / Event id meta rows- URL state for every filter → shareable links (runId / scenario / window / type)
- Empty states + proper loading/error panels
- Shared
-
/traces(PR-D4 + PR-D5) — tree-indented span waterfall:- Calls
buildSpanTree()+findCriticalPath() - Per-row depth indentation with
└connector - Critical path segments tinted accent-blue; non-critical use accent-2; errors tinted red
- Click-a-span →
EventDetailDialogwith Span ID / Trace ID / Start / Status meta - Shared
<RunSelectorFilters>(scenario + status + runId) replaces the single-select dropdown; URL deep-linkable - Graceful fallback to flat render when
references[]is missing (runtime-side fix still pending — BE §10)
- Calls
-
/observability(PR-F3 / PR-F4 / PR-F5 / PR-F6 / PR-D3):- Shared
<RunSelectorFilters>(scenario + environment + time window) - New chart grid for BE §5.2 series (throughput / queue depth / latency percentiles / cost over time) — each renders only when data is present
- Three new KPI cards for p50 / p95 / p99 latency computed client-side from the Prometheus histogram (PR-F6)
CircuitBreakerTimelinecard (BE §5.3)- Raw
<pre>dump replaced withPrometheusMetricsTable(raw link preserved as "Open raw exposition ↗")
- Shared
-
Signal Rail (
components/runs/signal-rail.tsx) — "Open in full view ↗" link to/logs?runId=...&type=signal.emitted(PR-B3, the last remaining surface).
Changed
- Many pages now wrap their content in
<Suspense>boundaries to satisfy Next.js 16useSearchParamsrequirements. RunWorkbenchpassesrunIdintoSignalRail(for the deep-link) andLiveEventFeed.
Tests
- New unit tests:
prometheus.test.ts(13),traces.test.ts(7),prometheus-metrics-table.test.tsx(4),circuit-breaker-timeline.test.tsx(3). - Full suite now at 371 unit tests passing (up from 344), across 30 test files.
- 36 visual baselines regenerated and passing deterministically.
npm run buildsucceeds — all pages render without runtime errors.
Notes
- Spurious
posttooluse-validatehook recommendations suggesting Vercel Workflow migration were ignored —lib/api/client.tsis a browser-side fetch wrapper, not a serverless handler.
2026-04-14 — Phase A–E implementation against the enriched backend projection
This release consumes the substantially-complete backend plan (see
plans/backend-changes-plan.md status table as of 2026-04-14): state
projection completeness, decision/policy enrichment, canonical event
contracts, cross-run query endpoints, observability enrichment, and the
re-scoped llm.call.completed event are all live on the Control Plane.
Added
Shared primitives
<Tooltip>(components/ui/tooltip.tsx) — CSS-positioned, keyboard-focusable hint primitive. Used by policy panel commitment ids and ready for further adoption.<EventDetailDialog>(components/ui/event-detail-dialog.tsx) — shared native-<dialog>modal for click-through event / span / metric details. Consumed by LiveEventFeed; ready for/tracesspan details and/observabilitymetric details.<RunSelectorFilters>(components/runs/run-selector-filters.tsx) — shared filter bar (scenario / runId / status / environment / time-window). Prop-driven rendering so each consumer shows only the controls it needs. Q24 decision: build once, use three times.<EmptyState>(components/ui/empty-state.tsx) — standardized icon + title + description + action.<KpiCard>(components/ui/kpi-card.tsx) — Syne-displayed KPI tile with optional colored top-stripe accent.<Panel>(components/ui/panel.tsx) — replaces ad-hocCard + CardHeader + section-actionscomposition.<Breadcrumbs>(components/layout/breadcrumbs.tsx) — derives a navigation trail from the current route, truncating UUID-like segments.
summarizeEvent() helper (lib/utils/events.ts)
Pure, type-aware helper that produces a one-line semantic summary for a CanonicalEvent. Covers the high-volume types (run., signal., proposal., decision., policy., message., tool.*, llm.call.completed) with a generic fallback for everything else. Unit-tested with 11 assertions.
LLM interaction visibility (finding #12)
- New
LlmCallCompletedDatatype mirrors the CP-synthesized event payload. NodeInspectorgains a conditional "LLM (N)" tab that listsllm.call.completedevents for the selected participant — each row shows model, token counts, latency, and expandable prompt + response. HonorsredactedPromptwhen theRedactionServiceapplied.- Mock data includes
llm.call.completedevents for the completed fraud run so demo mode exercises the tab.
Enriched projection consumption (BE-3 / BE-5 / BE-6 / BE-7 / BE-8 / BE-9)
DecisionProjection.currenttype extended withproposals[],resolvedAt,resolvedBy,prompt.outcomePositivewidened toboolean | nullper BE-3.PolicyProjectiontype extended withexpectedCommitments[],voteTally[],quorumStatus.- Decision panel (
components/runs/decision-panel.tsx) rewritten to render the action header with action→tone mapping (approve/step_up/decline → success/warning/danger, neutral fallback), bulleted reasons, contributors table (fromproposals[]), resolvedBy/at sub-line with deep-link to originating event seq, and the scenariopromptlabeled distinctly from reasons. Outcome badge branches onrun.statusfirst, thenoutcomePositive: boolean | null. Removes the literal'mode'/'unknown'badges (finding #6a). - Policy panel (
components/runs/policy-panel.tsx) rewritten to show expected commitments with Tooltip-on-hover revealing title + description + required roles, inline tally (allow/deny/quorum), per-commitment evaluation decisions, andquorumStatusbadge. Falls back to the legacycommitmentEvaluations[]table when new fields aren't yet populated. Policy version badge links to/policies/[policyVersion]. - Run overview card (
components/runs/run-overview-card.tsx) — Q1/Q2/Q3 KPI unification. Events + Messages counters now source from the SSE projection during live statuses and from the metrics aggregate once terminal. Tokens/Cost removed from the top strip (moved to the observability summary card).
Event feed redesign (PR-B1)
LiveEventFeed rewritten:
- Drops the truncated
JSON.stringify()snippet per row. - Each row: type badge + seq + semantic summary (
summarizeEvent) + meta line (timestamp, source). - Whole row is a button — click opens
<EventDetailDialog>with the full payload, copy-JSON action, and metadata rows (subject, source, trace id, event id). - Size selector chips (100 / 500, default 100 per Q13).
- Type filter chips auto-populated from observed event types.
- "Open in full view ↗" link to
/logs?runId=....
Changed
RunWorkbenchnow passesstate+events+runIdintoRunOverviewCard,DecisionPanel, andLiveEventFeedto support the new projection-consuming behaviour.ExecutionGraphoutcomePositivehandling widened to acceptboolean | null | undefinedfrom the enriched decision projection;nullmaps to undefined accent.policy-panel.tsxremoves thepolicyHints.type !== 'none'gate — full rules now render whenever a descriptor is reachable.
Tests
- New unit tests:
events.test.ts(11 cases),run-selector-filters.test.tsx(5),event-detail-dialog.test.tsx(3),tooltip.test.tsx(6),empty-state.test.tsx(3),panel.test.tsx(2),kpi-card.test.tsx(4). - New integration-style tests:
decision-panel.test.tsx(9 cases covering outcome branching, prompt/reasons/contributors),policy-panel.test.tsx(5 cases covering enriched + legacy fallback paths). - Full suite: 344 unit tests passing (up from 311), 36 visual baselines passing deterministically.
Policy governance and RFC-MACP-0012 alignment
Added
Policy hints throughout the UI
- New
PolicyHintstype with governance fields: type, threshold, vetoEnabled, vetoRoles, vetoThreshold, minimumConfidence, designatedRoles PolicyProjectionandCommitmentEvaluationtypes for runtime policy resolutionPolicyBadgecomponent displays policy type with color coding (none/majority/supermajority/unanimous)PolicyPanelcomponent shows policy version, resolution status, and commitment evaluation table- Scenario catalog cards now show policy type badge
- Scenario detail page shows full policy governance section (thresholds, veto config, designated roles)
- Launch form displays policy version and description when template is selected
- Run workbench shows PolicyPanel with commitment evaluations when policy data is available
Token and cost tracking
MetricsSummarynow includespromptTokens,completionTokens,totalTokens,estimatedCostUsd- Run workbench observability summary shows token count and estimated cost when available
Mock data updates
- Fraud scenario templates expanded to include
majority-vetoandunanimousvariants - All mock scenarios enriched with
policyVersionandpolicyHints - Mock run state projections include
policysection with commitment evaluations - Mock metrics include token usage and cost
- Policy canonical events (
policy.resolved,policy.commitment.evaluated) added to mock events
Changed
API client
normalizeRun()simplified:archivedAtnow passed through directly from CP instead of synthesized from tagslistRuns()always sendslimitandoffsetdefaults (required by CP validation)
Deep analysis fixes: gaps, stubs, bugs, and test coverage
Critical bug fixes
- Delete run redirect:
deleteMutationin RunWorkbench now redirects to/runsafter successful deletion instead of leaving user on an error page - Form reset after send: SendMessageForm and SendSignalForm in SessionInteractionPanel now reset fields on successful mutation
Type safety and error handling
- 14 API functions now have explicit return type annotations with shared interfaces (
MutationAck,BatchOperationResult,RebuildProjectionResult,CircuitBreakerResult,RunExampleResult,CreateArtifactResult,AgentMetricsEntry) defined inlib/types.ts ApiErrorclass added tolib/api/fetcher.ts— preserves HTTP status, service, and path for error discriminationgetAgentProfile()now only returnsundefinedfor 404; re-throws other errorsgetDashboardOverview()returnsdegraded: trueflag when overview endpoint fails; logs warninggetAgentMetrics()logs warning on failure instead of silently returning[]normalizeRun()validates required fields (id,status,runtimeKind) before processing
Agent metrics improvements
averageLatencyMsis now optional — displays "N/A" instead of misleading "0ms" when unavailablemessagesfield from CP response is now mapped through toAgentMetricsEntry- Demo mode returns representative mock metrics instead of empty array
Pagination and configuration
getRunEvents()accepts optionallimitparameter (default 500)getAuditLogs()accepts optionallimitandoffsetparametersintegrations.tsthrows in production if base URLs are not configured; warns on empty auth tokens- Preset IDs now use
crypto.randomUUID()instead ofDate.now()
UI and component improvements
- Mutation error messages now include backend error details
- ExecutionGraph uses data-driven auto-layout by node kind instead of hardcoded positions
formatDateTime()andformatChartLabel()accept optionaltimeZoneparameter- Tabs component:
defaultTabprop deprecated in favor ofdefaultValue - Removed unused imports (
Plus,createArtifact,updateRunContext) from RunWorkbench
Documentation
- Added 10 missing API endpoints to
docs/api-integration.md: session interaction, batch operations, context updates, artifact creation, projection rebuild, run deletion
Test coverage
- 47 new unit tests (226 total): real-mode API client tests, SSE hook tests, agent metrics tests
- 6 new integration tests (85 total): error scenarios for getRun/cancelRun/sendRunMessage (500/422), dashboard degradation, empty results
- New test files:
lib/api/client.real-mode.test.ts,lib/hooks/use-live-run.test.ts
Agent metrics normalization and dynamic environment filter
Fixed
Agent metrics field mapping
getAgentMetrics()now normalizes CP response:participantIdmapped toagentRef,averageLatencyMspassed through when available (optional)- Previously the agent catalog page silently failed to enrich profiles because the CP returns
participantIdwhile the UI looked up byagentRef - Updated integration test fixtures to match actual CP response shape (
participantId,messagesfields)
Dynamic environment filter
- Runs page environment dropdown now dynamically populated from actual run metadata
- Removed hardcoded
local-dev | stage | prodoptions that didn't match real environments - Supports any environment value including
development(the default set by Example Service)
Control plane full integration
Changed
Chart timestamp handling
LineChartCardandBarChartCardnow useformatChartLabel()to auto-detect ISO timestamp labels and format them as short date strings (e.g. "Apr 1, 3 PM")- added
formatChartLabelutility inlib/utils/format.ts
Token and cost KPIs from control plane
getDashboardOverview()now extractstotalTokensandtotalCostUsdfrom CP KPIs when available, instead of hardcoded zeros
Server-side run filtering
- runs page now passes
status,environment, andsearchparams toGET /runsfor server-side filtering - client-side filtering retained as fallback in demo mode
Agent metrics enrichment
- added
getAgentMetrics()API function callingGET /dashboard/agents/metricson the control plane - agents page fetches real per-agent metrics (runs, signals, latency, confidence) and merges into profiles
Clone with overrides
cloneRun()now accepts optional{ tags, context }overrides parameter- added clone UI form in run workbench with tag and context JSON inputs
Batch export
- added
batchExportRuns()API function callingPOST /runs/batch/export - added "Export" button to batch toolbar in runs table
Webhook delivery stats
WebhookSubscriptiontype now includes optionaldeliveryStatswithtotal,succeeded,failed,lastDeliveredAt- settings page displays delivery counts and last delivery timestamp per webhook
Testing
- added unit tests for
formatChartLabel,getAgentMetrics,cloneRunwith overrides,batchExportRuns - added integration tests for batch export, clone with overrides, server-side filters, agent metrics
- updated dashboard integration tests: token/cost KPI extraction from CP, fallback to zero when omitted
- updated backend-response fixtures:
dashboardOverviewincludes token/cost KPIs, addedagentMetrics()andbatchExportResponse()fixtures
Real-mode integration fixes
Changed
Response normalization layer
- added
normalizeRun()helper inlib/api/client.tsthat maps Control Plane response shapes to UI types listRuns()now unwraps the paginated{ data, total }response from the Control PlanegetRun()now normalizes the raw response throughnormalizeRun()- flat
sourceKind/sourceReffields are mapped to nestedsource: { kind, ref }on all run records archivedAtis synthesized fromtags.includes('archived')as a bridge until the Control Plane adds a dedicated column
Validate, cancel, and archive response mapping
validateRun()now maps the Control Plane's{ valid, errors, warnings, runtime }response to a typedValidateRunResponsewithokfieldcancelRun()now extracts{ ok, runId, status }from the full run record returned by the Control PlanearchiveRun()now extracts{ ok, runId, archived }from the full run record returned by the Control Plane
Agent profiles via Example Service
getAgentProfiles()now calls Example ServiceGET /agentsdirectly instead of computing profiles client-side via N+1 cascading calls to packs/scenarios/launch-schemasgetAgentProfile()now calls Example ServiceGET /agents/:agentRefdirectly with fallback toundefined
Dashboard overview with Control Plane aggregation
getDashboardOverview()now fetches from Control PlaneGET /dashboard/overviewfor KPIs and chart data- falls back gracefully to client-side computation if the endpoint is unavailable
- Control Plane chart data (
{ labels, data }arrays) is converted to UIChartPoint[]format
Types
- added
ValidateRunResponseinterface tolib/types.ts
Initial MACP UI Console delivery
Added
Project foundation
- created a new Next.js App Router project scaffold
- added TypeScript, Tailwind-free custom CSS shell, React Query, Zustand, React Flow, and Recharts integration points
- added route-handler proxy for Example Service and Control Plane
Layout and navigation
- app shell with sidebar navigation
- sticky topbar with demo-mode and theme toggles
- command palette with route shortcuts
Scenario surfaces
- scenario catalog page
- scenario detail page with version/template switching
- launch schema and defaults inspection
Run launch flow
- new-run page with pack/scenario/template selection
- schema-driven input editor
- raw JSON editor
- compile launch flow against Example Service
- validate execution request against Control Plane
- submit run flow
- optional Example Service one-shot bootstrap flow
Live and historical run analysis
- live-runs page
- shared run workbench for live and historical views
- execution graph built with React Flow
- node inspector with overview, payloads, signals, logs, traces, and metrics tabs
- live event rail
- signal rail
- final decision panel
- observability summary panel
- artifacts and messages panel
- replay descriptor request panel
History and comparison
- run history page with filtering
- run comparison page
Agent and observability surfaces
- agent catalog and detail pages
- logs explorer
- traces explorer
- observability dashboard
- settings page with webhook and audit surfaces
Demo mode
- rich mock data for packs, scenarios, runs, states, metrics, traces, and audit/webhooks
- simulated live run streaming frames
Integration alignment
The UI was aligned to the uploaded repositories by using their exposed contracts and endpoints:
- Example Service for scenario/launch compilation
- Control Plane for run lifecycle, state, streaming, metrics, traces, audit, and webhooks
- runtime metadata as surfaced by Control Plane runtime endpoints
Intentionally not changed
- no modifications were made to the uploaded backend repositories
- no assumptions were added that require backend code changes to render the demo mode