# Circe PR #5 review, round 3

APPROVED at cdbb1af4b905ea797774c5e10358ce26b5b51e55. F61-F64 fixed; no open findings. PR remains open.

Ready to merge within the read-only Jev scope. The runner now contains decoder exceptions where it owns call counts, traces and latency. Removing the synthetic fallback simplifies the evaluation loop and gives failure accounting one owner. This is a focused fix; no new abstraction or duplicate counter system was added. Real-model semantic quality remains outside this review.

All 380 repository tests pass: 323 evals, 40 journal-file and 17 perception-jev. Repository and independent-probe typechecks pass, as does git diff --check. The 139-case independent rerun has 137 passes and two inherited status-getter failures. A new later-stage preservation test also passes, for 138 passes and two inherited failures across 140 cases. F64 and the formerly failing inherited N4 throwing-getter case now pass. No live model call was made.

Round 3: N4 is now fixed by the runner decoder boundary. N2 and N3 still reproduce repeated status-getter reads and an AMBIGUOUS-to-RESOLVED upgrade with a custom in-process backend. These behaviors predate PR #5 and remain excluded from new actionable findings. The normal Jev wrapper snapshots status and its controls pass.

## F61 Send the materialized context to the model client [fixed]

packages/perception-jev/src/backend.ts:92-104

The backend reduces ContextFrame to four identity fields and coverage flags before calling its only injected client. The utterance, candidate attributes, relations, events, interaction state and capability semantics never reach that interface. Decision specs describe the bounded questions; they do not necessarily contain the evidence needed to answer them.

N1 assembles a real frame for “continue the payment worker” with two materialized candidates. The request contains the generic question “Which candidate does the phrase refer to?” and option IDs A, B and none, but neither the user's phrase nor either candidate's title. This is the same valid spec shape the retained integration tests use. An SDK wrapper implementing only complete(request) cannot infer the intended answer from those IDs and hashes.

Leaving a real interpreter out of scope is reasonable, but the model seam must carry its inputs. Requiring each host to secretly fetch frames by ID or embed all context into every question creates an undocumented second path. Pass an immutable, detached, bounded frame projection with the evidence already assembled by Circe.

Fix: Include the utterance and the assembled semantic context in JevDecisionRequest, preserving frame identity and retrieval completeness. Detach and freeze the full request. Keep the client read-only; do not add store access or language rules.

## F62 Count backend attempts and explicit error results accurately [fixed]

packages/perception-jev/src/evaluation.ts:88-99

The new evaluation metrics copy run.calls and classify failures only from run.failures. run.calls increments after a successful backend return, so a client invocation that throws is not counted. A well-formed result whose status is ERROR also creates no DecisionFailure, so backendFailures stays zero.

N5 invokes the Jev client twice, with one transport failure and one success. The evaluation returns two runs and one backend failure but reports only one call. N6 returns an explicit model ERROR result; errorRuns becomes one while backendFailures is zero. These are ordinary failure paths, not malformed JavaScript objects.

The harness is meant to support model evaluation before enabling execution. Its call counts and backend failure totals currently understate failures and usage. Count attempted calls at the invocation boundary, and represent explicit backend ERROR statuses consistently without double counting thrown or malformed responses.

Fix: Count every attempted backend call, including exceptions, and attribute explicit backend ERROR results as backend failures. Keep run counts separate from failure-event counts and document latency semantics for unsuccessful calls.

## F63 Preserve evaluation results when a workflow callback fails [fixed]

packages/perception-jev/src/evaluation.ts:78-86

The evaluation loop awaits each workflow without an exception boundary. The workflow runner records some structural faults, but exceptions from specs, reduce, stop or resolveStatus propagate. A callback error therefore escapes the new batch evaluation API instead of contributing a workflow failure.

N7 evaluates two frames with a reducer that throws only for the first. runReadOnlyEvaluation rejects immediately, returns no metrics or runs, and never evaluates the second frame. A successful backend response is available, so this is a workflow fault rather than a model failure.

For a read-only corpus evaluation, one faulty frame should remain visible in the report instead of discarding the batch. Record a failed run with the correct attribution and retain completed runs while continuing the remaining frames. If a deliberate fail-fast mode is wanted, make it explicit and preserve the accumulated diagnostic output.

Fix: Convert workflow callback exceptions into structured per-run workflow failures, then continue the evaluation batch. Preserve frame/run identity and distinguish workflow errors from backend errors.

## F64 Preserve accounting when the evaluation fallback catches a runner exception [fixed]

packages/perception-jev/src/evaluation.ts:91-116

The new outer catch creates a fresh WorkflowRun with calls and latency set to zero, empty traces, and a workflow-error reason. An exception escaping the runner does not establish that no backend call occurred or that the workflow caused it. This fallback now returns invented accounting as a completed evaluation result.

The independent reproduction uses the public PerceptionBackend interface. Its first response has an answers getter that throws during decoding; the second frame succeeds. Both backend calls happen, but the evaluation reports calls=1, backendFailures=0 and workflowFailures=1. The getter escaping the shared decoder is an inherited limitation already documented in round 1. The new defect is the fallback turning that exception into an inaccurate, apparently complete metrics report.

This is a nonblocking P2 for PR #5. The normal Jev wrapper catches this response-access failure at its backend boundary, and the four ordinary workflow callback paths now retain their accounting correctly. Preserve the available run state at the runner boundary and attribute decoder exceptions there, or represent unknown accounting explicitly instead of supplying zero. Avoid adding a second counter system in the evaluator.

Fix: Keep exception containment where the runner owns accumulated state. Record a decoder failure at the backend-output boundary and preserve counts, traces and known latency. Do not silently classify every escaping exception as a workflow failure with zero calls.

## Original inherited limitation record

N2-N4 expose limitations in the claimed one-read decoder: status is read repeatedly, an accessor can change AMBIGUOUS to RESOLVED, and a throwing answer getter escapes workflow failure recording. Separate tests against the merged PR4 code reproduce the same workflow failures. They are inherited limitations, not new PR5 regressions, and are excluded from actionable counts. The new analyzePerceptionResult API still does not establish the advertised full-envelope one-read guarantee. The Jev wrapper itself snapshots status once, so its ordinary status path passes N8. These are custom in-process accessor cases, not ordinary parsed model JSON.

Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

Sidecar: /home/abstergo/Documents/reviews/Circe-perception-jev-abbyview.json
HTML: /home/abstergo/Documents/reviews/Circe-perception-jev-abbyview.html

No open findings at cdbb1af4b905ea797774c5e10358ce26b5b51e55. F61-F64 are verified fixed. Preserve the recorded inherited N2/N3 limitations separately.
