Skip to content

chore(deps): agent-eval 0.145.0 with the caller-owned model call - #85

Merged
drewstone merged 6 commits into
mainfrom
chore/agent-eval-0.145.0
Aug 13, 2026
Merged

chore(deps): agent-eval 0.145.0 with the caller-owned model call#85
drewstone merged 6 commits into
mainfrom
chore/agent-eval-0.145.0

Conversation

@drewstone

@drewstone drewstone commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What

  • Bump @tangle-network/agent-eval 0.143.0 → 0.145.3.
  • Raise the consumers so the peers close on published versions instead of being forced: @tangle-network/agent-runtime 0.126.0 → 0.133.2 (peers agent-eval >=0.145.2 <0.146.0, agent-interface >=0.47.0 <0.48.0, sandbox >=0.21.1 <0.22.0) and @tangle-network/sandbox 0.17.2 → 0.21.1.
  • No pnpm.overrides. An earlier revision of this PR pinned agent-eval tree-wide to get past agent-runtime@0.126.0's <0.144.0 peer cap. That masked the violation rather than resolving it, so the override is deleted and the consumers were raised instead.
  • Migrate the analyst model call across the agent-eval 0.144.0 breaking change: createDspyRlmTraceEngine no longer accepts apiKey/baseUrl, so the CLI now owns each model call (call + callRef + recordExecution) using agent-eval's own callLlm, costReceiptFromLlm, and costReceiptFromLlmError. Failure receipts keep usage and cost explicitly unknown instead of zero.
  • Extract that path to src/analyst-model-call.ts with tests. src/cli.ts calls main() at import, so the callback previously had no reachable test seam.

Why a code change was needed

The 0.145.x census law preserved every root-barrel symbol this repo imports, and it held: no import broke. The migration comes from the intermediate 0.144.0 release (caller-owned optimizer execution), which this repo skips over when jumping from 0.143.0.

Failure-path fixes in the owned model call

  • request.callId is now the provider idempotency key. callLlm retries transient failures, so without it a response that was billed but lost is charged twice on the retry.
  • Aborts are marked. The callback must resolve, so an abort can never reach the proxy as a thrown AbortError and can never take its 504 branch — it always became a 502 the bridge retries against a dead intent. The failure text now names the class and execution.aborted records it.
  • The provider's reason is retained. LlmCallError.status and a bounded body stay in the execution evidence, and out of the log line because a gateway can echo request headers into an error body.
  • structuredClone moved inside the try; a throw from it would have rejected the callback, which the contract says loses the execution record and fails the optimizer attempt.

Checks

  • Clean install (rm -rf node_modules && pnpm install): exit 0, zero peer warnings.
  • One version each across the tree: agent-eval 0.145.3, agent-interface 0.47.0, agent-runtime 0.133.2, agent-core 0.6.1, agent-knowledge 7.2.4, sandbox 0.21.1, esbuild 0.28.1.
  • pnpm typecheck clean.
  • pnpm test: 51 files, 657/657 passed.
  • pnpm build clean; pnpm check:package ok.

Bump @tangle-network/agent-eval from 0.143.0 to 0.145.0. Add a
pnpm override so agent-runtime 0.126.0 cannot ghost a second eval
version into the tree; pnpm resolves one version.

agent-eval 0.144.0 removed apiKey and baseUrl from the DSPy RLM
engine: the caller now owns each model call and returns a typed
result, a cost receipt, and execution evidence. The CLI implements
that call with agent-eval's own callLlm and receipt converters, and
logs every execution observation to the analyst stderr log.

@tangletools tangletools left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Auto-approved drewstone PR — 00489095

This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.

tangletools · auto-approval · reason: drewstone_author · 2026-08-12T00:43:08Z

@tangletools tangletools left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Value Audit — sound

Verdict sound
Concerns 0 (none)
Heuristic 0.0s
Duplication 0.0s
Interrogation 229.9s (2 bridge agents)
Total 229.9s

💰 Value — sound

Forced migration across agent-eval 0.144.0's caller-owned-execution break, done in-grain using the library's own callLlm + cost-receipt primitives; no equivalent to reuse exists.

  • What it does: Bumps @tangle-network/agent-eval 0.143.0 -> 0.145.0, adds a pnpm.overrides pin so agent-runtime 0.126.0 (peer <0.144.0) cannot drag a second eval version into the tree, and rewrites buildAnalysisEngine in src/cli.ts:504-567 to the new contract: createDspyRlmTraceEngine no longer takes apiKey/baseUrl, so the CLI now owns each model call via a call callback that wraps agent-eval's own callLlm, der
  • Goals it achieves: (1) Stay current with agent-eval so the published census law and downstream protocol stay aligned. (2) Comply with 0.144.0's 'Agent Eval never receives provider credentials' boundary: the CLI owns the one OpenAI-compatible HTTP hop and returns a typed result + cost receipt per call. (3) Keep one version of agent-eval in the install tree despite the not-yet-republished agent-runtime peer range. Net
  • Assessment: Good change, executed in the grain of the codebase. The migration is forced by an upstream breaking change (DspyRlmTraceEngineOptions at node_modules/@tangle-network/agent-eval/dist/index-BWDrSVfw.d.ts:160-206 no longer accepts apiKey/baseUrl), and the implementation responds by reusing agent-eval's own primitives (callLlm, costReceiptFromLlm, costReceiptFromLlmError) rather than hand-rolling an H
  • Better / existing approach: none — this is the right approach. I searched for an existing OpenAI-compatible caller to extend: src/replay-fix.ts:42 (zaiChatCaller) POSTs to ${baseUrl}/chat/completions with a bearer token, but it returns {content, usage:{promptTokens, completionTokens}} — a strictly weaker shape that strips the canonical ChatResponse and reduces usage to two token counts. The new ExternalOptimizerModelCallResu
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 2
  • Bridge warning: opencode/kimi-for-coding/k2p7: opencode: opencode error

🎯 Usefulness — sound

A forced dependency migration (agent-eval 0.144.0's caller-owned breaking change) implemented correctly against the real 0.145.0 contract, on the live --llm path, plus a dedup pin that holds the tree to one version.

  • Integration: Fully wired and reachable. buildAnalysisEngine (src/cli.ts:486) is the only engine constructor; it is called from investigate (src/cli.ts:1216) and cmdImprove (src/cli.ts:1241), both gated on the shipped --llm flag (src/cli.ts:244,1697). Every new symbol imported (callLlm, costReceiptFromLlm, costReceiptFromLlmError, LlmCallRequest) exists in the installed agent-eval 0.145.0 dist (types-Cx3YUh2r.d
  • Fit with existing patterns: Fits the codebase's single-engine architecture with no competing pattern. DspyRlmTraceEngineOptions (index-BWDrSVfw.d.ts:160-208) dropped apiKey/baseUrl and now requires call/callRef/recordExecution; the CLI implements precisely that seam using agent-eval's own callLlm — i.e. the package's published primitive, not a hand-rolled fetch. TraceAnalysisEngine flows unchanged into analyze.ts:35 and impr
  • Real-world viability: Handles the non-happy paths. callLlm is wrapped in try/catch; failures return a typed failure result with costReceiptFromLlmError(err) ?? an explicit unknown-receipt (costUnknown:true, usageUnknown:true) rather than a silent zero-cost receipt, so the fail-closed ledger treats unknowns honestly. The per-call AbortSignal is forwarded into callLlm. recordExecution logs the finite execution observatio
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 1

No concerns — sound change, no better or existing approach found. ✅


What this audit checks

It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.

Pass What it asks
Heuristic Vague title? Whitespace-only or cruft-bearing diff? (content signals only)
Duplication Do added function/class names already exist elsewhere in the repo?
Value Audit What does it do? What goal does it achieve? Is it good? Better architecture or already-exists?
Usefulness Audit Does it integrate and fit? Will it hold up in real use and actually get used?

Findings are concerns, not blocks — the human reviewer decides what to do with them.

value-audit · 20260812T004855Z

@tangletools

Copy link
Copy Markdown

❌ Needs Work — 00489095

Review health 100/100 · Reviewer score 25/100 · Confidence 75/100 · 22 findings (1 high, 8 medium, 13 low)

opencode GLM 5.2 opencode DeepSeek v4 Pro opencode DeepSeek v4 Flash aggregate
Readiness 56 60 25 25
Confidence 75 75 75 75
Correctness 56 60 25 25
Security 56 60 25 25
Testing 56 60 25 25
Architecture 56 60 25 25

Reviewer score is advisory once the run is complete and the verdict has no blockers.

Full multi-shot audit completed 3/3 planned shots over 3 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 3/3 planned shots over 3 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 3/3 planned shots over 3 changed files. Global verifier still owns final merge decision.

Blocking

🔴 HIGH Override forces agent-eval@0.145.0 + agent-interface@0.46.1 into agent-runtime@0.126.0 despite peer ranges that exclude them; no published agent-runtime version supports agent-eval — pnpm-lock.yaml

Snapshot resolves agent-eval@0.145.0 and agent-interface@0.46.1, but this same file's metadata (L679-685) declares agent-runtime peers agent-eval '0.145.0' / agent-interface '>=0.43.0 <0.44.0' — 0.46.1 violates the declared range internally, and the published agent-runtime@0.126.0 tarball (identical integrity sha512-JTsF13tdb0vv+Tb2Rzm9M2vm6izLno7bJ0Yk/VC0zDE63xllIia0tNKBj2ZGDAkbNFARSIz3wqGFZ/A00x52Yg==) declares agent-eval peer '>=0.143.0 <0.144.0'. I regenerated with the pinned pnpm 10.33.4: install succeeds but pnpm warns '✕ unmet peer @tangle-network/agent-interface@">=0.43.0 <0.44.0": found 0.46.1' for agent-runtime and agent-profile-materialize@0.10.2 (peer '>=0.38.0 <0.44.0', L1728). Registry audit of every agent-runtime 0.123.1..0.132.4 shows all agent-eval peers cap at '<0.145.0',

Other

🟠 MEDIUM pnpm override force-pins agent-eval 0.145.0 onto agent-runtime outside its declared peer range — package.json

The new override forces @tangle-network/agent-eval to 0.145.0 tree-wide, but agent-runtime@0.126.0 (an unchanged dependency) declares peer '@tangle-network/agent-eval': '>=0.143.0 <0.144.0' in the base lockfile. The head lockfile rewrites that declared range to '0.145.0' (pnpm-lock.yaml:683-684), masking the violation. The same consumer's peer '@tangle-network/agent-interface' resolves 0.46.1, outside its declared '>=0.43.0 <0.44.0' (was 0.43.0 in base). agent-eval 0.144.0 contained a breaking change (removed provider-credential path, per the src/cli.ts migration comment), so 0.145.0 is a breaking-change minor forced onto a consumer that was explicitly bounded below 0.144.0. Fix: confirm agent-runtime@0.126.0 is API-compatible with agent-eval@0.145.0 + agent-interface@0.46.1, or bump agent

🟠 MEDIUM pnpm.overrides forces agent-eval 0.145.0 onto agent-runtime@0.126.0 outside its declared peer range — package.json

agent-runtime@0.126.0 (still pinned at line 63) declares peerDependencies.@tangle-network/agent-eval as ">=0.143.0 <0.144.0" (verified via npm view @tangle-network/agent-runtime@0.126.0 peerDependencies). The override at lines 67-70 forces 0.145.0, which is outside that range and is a documented breaking release: agent-eval CHANGELOG for 0.144.0 and 0.145.0 lists breaking changes including the caller-owned ChatClient migration, RawAnalystFinding.evidence shape change, defineTraceAnalyst return-type change, and removal of provider SDK types. agent-runtime@0.126.0's compiled

🟠 MEDIUM Unmet peer dependencies: agent-runtime 0.126.0 and agent-profile-materialize 0.10.2 receive agent-interface 0.46.1 outside their declared <0.44.0 peer range — pnpm-lock.yaml

agent-eval@0.145.0 (forced by the new override) depends on agent-interface@0.46.1 and agent-core@0.5.4. The runtime snapshot at line ~1715 records dependencies on agent-eval 0.145.0 AND agent-interface 0.46.1, but the runtime package's own peer declaration still reads @tangle-network/agent-interface: '>=0.43.0 <0.44.0' (line ~684). pnpm install (frozen and fresh) emits: unmet peer @tangle-network/agent-interface@">=0.43.0 <0.44.0": found 0.46.1 for both @tangle-network/agent-runtime 0.126.0 and @tangle-network/agent-profile-materialize 0.10.2 (peer >=0.38.0 <0.44.0). 0.43.0 -> 0.46.1 is three minor bumps in 0.x semver where minor = potentially breaking. The lockfile is installable but the transitive graph now ships a peer combination the runtime author did not bless. Impact: possible

🟠 MEDIUM agent-interface version split across the dependency tree — pnpm-lock.yaml

agent-knowledge@7.0.8 depends on agent-interface@0.43.0 (line 1724), but its surrounding packages — agent-eval@0.145.0 (line 1702), agent-runtime@0.126.0 (line 1735), agent-core@0.5.4 (line 1694) — all use agent-interface@0.46.1. agent-runtime receives agent-interface@0.46.1 as a peer dep but passes objects into agent-knowledge, which validates against 0

🟠 MEDIUM package.json pnpm.overrides shadows and silently disables the pnpm-workspace.yaml overrides, downgrading esbuild 0.28.1 -> 0.27.7 — pnpm-lock.yaml

The committed lockfile's overrides: block now lists only @tangle-network/agent-eval: 0.145.0. The three overrides previously pinned in pnpm-workspace.yaml (esbuild ^0.28.1, vite ^8.1.2, ws ^8.21.0, added deliberately by Drew in commit da6c901) no longer take effect. Verified by regen: with both files present, pnpm 10.33.4 resolves esbuild to 0.27.7 for tsup/vite/vitest (see snapshots at bundle-require@5.1.0(esbuild@0.27.7), vite@8.1.5(...esbuild@0.27.7...), vitest@4.1.10(...esbuild@0.27.7...)) and 0.28.1 lingers only as an optional platform-package leftover. Proved causally: adding esbuild: 0.28.1 into package.json's pnpm.overrides forces esbuild@0.28.1 in the regen, while the same directive in pnpm-workspace.yaml does nothing. Impact: any reason the maintainer originally pinned esbu

🟠 MEDIUM Echoed-model identity now hard-fails alias/renaming gateways — src/cli.ts

In agent-eval 0.145.0, snapshotModelReceipt and snapshotChatResponse (external-optimizer-model-proxy.ts:631-633, 937-941) require receipt.model and response.model to equal the engine's configured model. The CLI's receipt uses costReceiptFromLlm(response) whose model is servedModel ?? req.model (llm-client.ts), so any OpenAI-compatible gateway that echoes a canonical/dated id different from the requested --model (common with LiteLLM/version-resolving routers — the exact OPENAI_BASE_URL use-case this CLI advertises) fails EVERY analyst call with an opaque optimizer model callback response used 'X' instead of 'Y' OwnerModelContractError → 502, then bridge retries exhaust. 0.143.0's proxy forwarded the raw provider body with no echoed-model check, so this is a behavior regression

🟠 MEDIUM request.callId is dropped instead of becoming the provider idempotency key — src/cli.ts

callLlm(req, { apiKey, baseUrl, signal }) omits idempotencyKey. The proxy passes a stable callId on every admitted call (ExternalOptimizerModelCallRequest.callId, 'Stable identity allocated by the cost ledger for this paid call'), and agent-eval's own reference owner passes it through: client.call(structuredClone(request.request), { signal: request.signal, idempotencyKey: request.callId }) (model-execution.test-support.ts:34-36). callLlm retries transient failures up to 3 attempts; if attempt 1 is processed and billed by the provider but the response is lost (timeout/ECONNRESET), the retry re-sends the same logical call without an Idempotency-Key header, so the provider can double-bill and the run is charged once. Fix: `callLlm(req, { apiKey, baseUrl, signal, idempotencyKey: reques

🟠 MEDIUM structuredClone outside try/catch can leak rejection — src/cli.ts

Line 510: const req = structuredClone(request) as LlmCallRequest executes before the try block at line 511. If structuredClone throws (e.g. a non-cloneable getter on an unfamiliar engine version's request object), the error propagates as a promise rejection from the callback. The engine contracts (line 82-83 of external-optimizer-contracts-lixrOZdX.d.ts) state 'Rejecting loses the execution record and therefore fails the optimizer attempt' — a thrown clone error would bypa

🟡 LOW Override duplicates an already-exact direct dependency version — package.json

The direct dependency already pins agent-eval exactly to '0.145.0' (package.json:62); the overrides block restates the identical version. The override is what dedupes the tree, so it is not dead config, but the two must never drift apart — a future bump touching only one entry would silently split resolution (two copies) or violate peer ranges. Suggest a one-line comment noting the override must track the direct pin, or rely on the lockfile check to catch divergence.

🟡 LOW Stale lockfile overrides cleaned up — esbuild pin lost but benign — package.json

The previous lockfile had overrides for esbuild (^0.28.1), vite (^8.1.2), and ws (^8.21.0) that were not in package.json's pnpm.overrides. Regenerating the lockfile with the new pnpm.overrides block drops them. Verified: esbuild resolves to 0.27.7 which includes the 0.25.0 content-injection fix; ws is no longer a transitive dep; vite ^8.1.5 in devDependencies already exceeds ^8.1.2. No vulnerability reintroduced. If esbuild 0.28.1 was pinned for a non-security reason (e.g. a tsup/vitest edge case), consider re-adding the esbuild override.

🟡 LOW Orphaned lockfile entry '@tangle-network/agent-core@0.4.33' after removing agent-eval@0.143.0 — pnpm-lock.yaml

The packages (L652) and snapshots (L1687) sections still define agent-core@0.4.33, but its only dependent was agent-eval@0.143.0, which this PR removed; no snapshot references it anymore (grep over the snapshots section returns only its own definition). Harmless to installs (frozen install passed) but stale metadata that a clean 'pnpm install --lockfile-only' would prune. Fix: regenerate the lockfile.

🟡 LOW agent-knowledge@7.0.8 not bumped alongside agent-interface upgrade — pnpm-lock.yaml

agent-knowledge@7.0.8 still resolves agent-interface@0.43.0 while the rest of the tree uses 0.46.1. This is a transitive dep of agent-runtime@0.126.0. It may need a version bump to stay compatible with the surrounding ecosystem.

🟡 LOW esbuild downgraded from 0.28.1 to 0.27.7 — pnpm-lock.yaml

Removal of the esbuild:^0.28.1 override (package.json line 62-64 removed) causes pnpm to resolve esbuild@0.27.7 instead of 0.28.1. tsup@8.5.1→bundle-require@5.1.0 drives the resolution. While vite 8.1.5 and vitest 4.1.10 declare ^0.27.0 || ^0.28.0 compatibility, this is a full minor version step back that may miss fixes in 0.28.x. The downgrade was not mentioned in the commit message. Confirm esbuild 0.27.7 works with the build and test commands.

🟡 LOW esbuild split into two copies (0.27.7 for vite/tsup, 0.28.1 for tsx) after removing the esbuild override — pnpm-lock.yaml

Removing the esbuild:^0.28.1 override drops vite@8.1.5/tsup@8.5.1/bundle-require@5.1.0 to esbuild@0.27.7 while tsx@4.23.1 keeps 0.28.1 (L2317), yielding two native esbuild copies instead of the previously unified 0.28.1. No security impact: both are above the 0.25.0 fix for the esbuild dev-server arbitrary-read CVE (CVE-2025-61104), and both satisfy vite's esbuild peer '^0.27.0 || ^0.28.0'. This is a deduplication/size nit only; the base lockfile's esbuild/vite/ws overrides were never declared in base package.json, so their removal is consistent with the manifest.

🟡 LOW pnpm-workspace.yaml now contains misleading dead config (overrides block has no effect) — pnpm-lock.yaml

Side effect of the shadowing in finding #1. pnpm-workspace.yaml is unchanged by this PR and still declares overrides: { esbuild: ^0.28.1, vite: ^8.1.2, ws: ^8.21.0 }. pnpm config list still surfaces these, but the lockfile (the source of truth for installed versions) shows they are not applied. Future readers/maintainers will assume esbuild/vite/ws are pinned when they are floating. This is a maintenance trap, not a runtime defect. Fix: remove the overrides block from pnpm-workspace.yaml, or migrate the agent-eval override there and drop package.json's pnpm.overrides.

🟡 LOW Abort/timeout flattened to a retryable 502 instead of a cancel signal — src/cli.ts

When the caller signal aborts (analysis deadline, request-timeout controller), callLlm throws a DOMException AbortError; the catch here converts it into {succeeded:false, error: err.message}. The proxy's abort branch (isAbortError(paid.error) → 504) never fires because paid.error is now a message string; handleModelProxyRequest sends 502 via modelCallFailed, which the Python bridge's client treats as transient and retries against a dead intent, delaying cancellation and wasting retries. The contract requires the callback to always resolve (re-throwing wraps into MissingModelExecutionError and fails the attempt), so the fix is limited: mark the error text so the proxy/bridge can distinguish 'aborted' (e.g. prefix with AbortError:), or accept the 502-on-cancel as designed. Low impa

🟡 LOW No test coverage for buildAnalysisEngine contract migration — src/cli.ts

grep across tests/ found zero references to buildAnalysisEngine, callLlm, or costReceiptFromLlm. This PR migrates the analyst execution seam from pass-through credentials to a caller-owned ExternalOptimizerModelCall, which is precisely the shape a unit test with a stub call callback and an injected rejection should pin. The gap predates the PR but the migration is the natural moment to add one. Low because tsc verifies the type contract and the change is mechanical against the new agent-eval exports.

🟡 LOW No test covers the call callback success or error paths — src/cli.ts

The only --llm test (tests/cli.test.ts:861) uses a fakeBridge that exits(1) at startup before the engine makes any model call. The call callback — the most complex new code in this PR — is never invoked in any test. Neither the successful callLlm path (costReceiptFromLlm, execution fields) nor the error path (costReceiptFromLlmError recovery, costUnknown fallback) nor the recordExecution logging is tested. Verified by reading the full test (line 863-940): fakeBridge only prints stderr and exits, so the engine fails at 'every agentic analyst died at startup', not at the call callback level.

🟡 LOW Provider error detail lost on non-2xx — src/cli.ts

error: err.message surfaces only LLM call failed with HTTP <status>LlmCallError.body (the provider's actual reason: invalid key, context length, model not found) is discarded from both the execution evidence and the [analyst] FAIL log line, and the 502 body sent back to the bridge contains only that generic message. 0.143.0 forwarded the raw provider response body, so operators debugging a 400/401 lose the cause. Fix: include the truncated body and status in the execution object (and optionally in the error text), being careful to keep it out of log lines if the gateway echoes credentials.

🟡 LOW callId from Engine callback discarded — src/cli.ts

ExternalOptimizerModelCallRequest carries a readonly callId (line 44 of external-optimizer-contracts-lixrOZdX.d.ts) allocated by the cost ledger for each paid call. The destructuring at line 509 drops it: async ({ request, signal }) =>. Neither the callLlm call nor the recordExecution callback uses callId, so per-call correlation between the callback's log output and the engine's internal ledger entries is lost. Not a bug — the engine retains callId internally — but recoverable via a one-line addition to execution/recordExecution.

🟡 LOW structuredClone of an already-immutable request is defensive overhead — src/cli.ts

ExternalOptimizerChatRequest is DeepReadonly by contract (external-optimizer-contracts-lixrOZdX.d.ts:35-39) and callLlm does not mutate its input, so the structuredClone before the cast to LlmCallRequest adds a deep copy of the messages array per call for no behavioral gain. The cast alone (or a shallow spread) would suffice. Keep if the goal is belt-and-suspenders protection against future callLlm mutations; otherwise drop. Negligible cost relative to the network call that follows.


tangletools · 2026-08-12T01:01:50Z · trace

@tangletools tangletools left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

❌ 1 Blocking Finding — 00489095

Full multi-shot audit completed 3/3 planned shots over 3 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 3/3 planned shots over 3 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 3/3 planned shots over 3 changed files. Global verifier still owns final merge decision.

Full immutable report for this review: trace

Summary comment for this run: full summary


tangletools · 2026-08-12T01:01:50Z · immutable trace

…pnpm override

The published agent-runtime 0.132.13 peers on agent-eval '>=0.145.2 <0.146.0'.
The override existed only to force past the old peer cap and is no longer needed.
pnpm resolves one agent-eval version (0.145.3) across the tree.
agent-runtime 0.133.0 peers on sandbox '>=0.21.1 <0.22.0'. The repo held
sandbox 0.17.2, which does not satisfy that range. sandbox 0.21.1 is
published, so the peer contract closes with one version of each Tangle
package in the tree.
The loopback proxy allocates callId as the cost ledger's stable identity for
one paid call. callLlm retries transient failures, so without an idempotency
key a response that is billed but lost is charged again on the retry.

Also move structuredClone inside the try block. The engine contract requires
the callback to resolve; a throw from clone rejected the promise and lost the
execution record. The catch now reads the model from the immutable request.
@drewstone

Copy link
Copy Markdown
Contributor Author

Blocking finding addressed — the override is gone and the peers close on published versions

The HIGH finding on 00489095 was correct: the pnpm.overrides block forced agent-eval@0.145.0 into an agent-runtime@0.126.0 whose declared peer capped at <0.144.0. The fix is to delete the override and raise the consumers instead of masking them.

Now on b61cf4b:

package was now why
agent-eval 0.145.0 0.145.3 current published
agent-runtime 0.126.0 0.133.0 peers agent-eval >=0.145.2 <0.146.0, agent-interface >=0.47.0 <0.48.0, sandbox >=0.21.1 <0.22.0
sandbox 0.17.2 0.21.1 0.17.2 did not satisfy the runtime peer above. 0.21.1 is published (note: the latest dist-tag still points at 0.21.0)
pnpm.overrides agent-eval: 0.145.0 deleted with the peers widened it only hides the next real break

Verified on the pushed head:

  • pnpm why reports 1 version each of agent-eval (0.145.3), agent-interface (0.47.0), sandbox (0.21.1), esbuild.
  • Clean install (rm -rf node_modules && pnpm install) emits zero peer warnings.
  • pnpm typecheck clean, pnpm test 654/654, pnpm build clean.

This also resolves the override side effects flagged in the review. With package.json's pnpm.overrides removed, the pnpm-workspace.yaml overrides apply again — the lockfile header carries esbuild: ^0.28.1, vite: ^8.1.2, ws: ^8.21.0 and esbuild resolves to a single version, so the shadowing/downgrade/split findings are all moot.

Two contract findings fixed in src/cli.ts

  • request.callId was dropped instead of becoming the provider idempotency key. callLlm retries transient failures, so a response that was billed but lost was charged twice on the retry. Now passed as idempotencyKey.
  • structuredClone ran outside the try. The engine contract states that rejecting loses the execution record and fails the optimizer attempt; a throw from clone did exactly that. It is now inside the try, and the catch reads the model from the immutable request.

Known flake, not caused by this PR

tests/external.test.ts descendant-termination assertions are wall-clock timed and can fail under parallel load. Observed once at 651/654; three isolated re-runs and two full-suite runs pass 26/26 and 654/654. Pre-existing and unrelated to the model-call seam.

@drewstone

Copy link
Copy Markdown
Contributor Author

Not merging: the merge block is a stale review, not a real red

Everything this PR controls is green on b61cf4b53f26ef4e967a49795c0a9e5387062dee. It is held by a review verdict that belongs to a commit three revisions back.

State

  • reviewDecision = CHANGES_REQUESTED, mergeStateStatus = BLOCKED.
  • The only reviews on this PR are three from tangletools, all on 00489095, submitted 2026-08-12 00:43–01:01.
  • Three heads have been pushed since and drew no review: bd114c5 (21:07), 5567945 (22:29), b61cf4b (22:48). The oldest went unreviewed for over two hours; I polled for ~30 minutes past the last push.
  • This repo has only CI and Publish workflows — the reviewer is an external service, so there is nothing here to re-trigger, and I found no comment command it accepts.
  • No unresolved review threads (reviewThreads returns none unresolved), so conversation resolution is not the block.

What is actually green on b61cf4b

  • CI success (22:48:46 → 22:50:21). The workflow runs pnpm install --frozen-lockfile, typecheck, test, build, check:package.
  • Locally: typecheck clean, pnpm test 654/654, build clean, clean install with zero peer warnings, one version each of agent-eval 0.145.3, agent-interface 0.47.0, sandbox 0.21.1, esbuild.

Why I am not merging anyway

The blocking HIGH finding was the pnpm override forcing agent-eval past agent-runtime's peer cap. That override is deleted and the consumers were raised so the peers close on published versions — the finding is addressed rather than suppressed. But clearing the merge block requires dismissing a CHANGES_REQUESTED that no longer describes this head, and dismissing a review to merge is indistinguishable from forcing green. An honest red beats a forced green.

To land this, either: re-run the reviewer against b61cf4b and merge on its verdict, or have a human dismiss the stale 00489095 review after reading the diff. I have deliberately left both to a human.

…l record

Move the owned model-call path out of the CLI into its own module so it can
be tested directly. cli.ts runs main() at import, so the callback had no
reachable test seam.

Three defects in the failure path:

- An abort resolved as a plain failure string. The proxy only sends 504 when
  it catches a thrown AbortError, so a cancelled call reached the bridge as a
  502 that it retries against a dead intent. The failure text now names the
  class.
- LlmCallError.body was dropped, leaving only the HTTP status. The provider's
  own reason now stays in the execution evidence, bounded, and out of the log
  line because a gateway can echo request headers into an error body.
- callId was absent from the execution record, so a callback observation could
  not be correlated with the ledger entry for the same paid call.

Cover the success, provider-error, and abort paths against a local
OpenAI-compatible server, and raise agent-runtime to 0.133.2.
@drewstone

Copy link
Copy Markdown
Contributor Author

Remaining findings from 00489095, dispositioned on d258224

The dependency findings were addressed on b61cf4b (override deleted, consumers raised). This closes the rest.

Fixed

MEDIUM — request.callId dropped instead of becoming the provider idempotency key. Passed as idempotencyKey. Test asserts the gateway actually receives Idempotency-Key: call-abc123.

MEDIUM — structuredClone outside try. Moved inside. A throw from clone would have rejected the callback, and the contract states rejecting loses the execution record and fails the optimizer attempt.

LOW — abort flattened to a retryable 502. Real, and the reviewer's own suggested fix is the only one the contract allows. Confirmed in the installed dist: when the callback returns succeeded: false, external-optimizer-subprocess-BhKYK0Jv.js:588-592 returns modelCallFailed502 unconditionally. The isAbortError(paid.error) ? 504 branch at line 473 only inspects a thrown error, which the callback must never do. The failure text now names the class (AbortError: …) and execution.aborted records it.

LOW — provider error detail lost on non-2xx. LlmCallError.status and a bounded body now stay in the execution evidence — and deliberately out of the [analyst] log line, since a gateway can echo request headers into an error body.

LOW — callId discarded from the execution record. Now recorded on both the success and failure paths.

LOW — no test coverage for the call callback. Correct: src/cli.ts calls main() at import, so the callback had no reachable seam. Extracted to src/analyst-model-call.ts and covered by tests/analyst-model-call.test.ts — success, provider-error, and abort paths, driven against a local OpenAI-compatible server so callLlm → HTTP → costReceiptFromLlm is the real path under test. The three assertions were mutation-checked: reverting each fix fails its test (expected undefined to be 'call-abc123', expected undefined to be 400, expected false to be true).

Refuted

MEDIUM — "Echoed-model identity now hard-fails alias/renaming gateways". Not correct against the version this PR installs. The check is not equality: snapshotModelReceipt calls assertServedModel, which accepts verdicts exact and alias (llm-client-d0-2TT1g.js:158-193, 206-210). normalizeModelId (line 138-147) strips the provider prefix, @version, :tag, and -YYYY-MM-DD / -YYYYMMDD suffixes — which is exactly the LiteLLM/version-resolving-router case the finding names.

Executed against the installed 0.145.3:

requested served verdict result
gpt-4o gpt-4o-2024-08-06 alias PASS
glm-5.2 zai/glm-5.2 alias PASS
glm-5.2 glm-5.2:latest alias PASS
gpt-4o gpt-4o@20240806 alias PASS
gpt-4o gpt-4o exact PASS
gpt-4o claude-opus-5 substituted-cross-family FAIL

Only a genuine cross-family substitution fails, which is the intended integrity behaviour, not a regression. The unreported verdict is also unreachable from this path: a body that omits model leaves servedModel null, and both the receipt and the response carry servedModel ?? req.model, so the check sees exact.

LOW — structuredClone is defensive overhead. Keeping it. The line casts a DeepReadonly contract type to a mutable LlmCallRequest; the clone is what makes that cast honest rather than a lie about ownership. One deep copy of a messages array is negligible next to the HTTP call.

Already moot on this head

agent-interface split, agent-knowledge not bumped, esbuild downgrade / two copies, pnpm-workspace.yaml dead config, override duplicates the direct pin, orphaned agent-core@0.4.33 — all were consequences of the deleted override. On d258224 the tree resolves one version each: agent-eval 0.145.3, agent-interface 0.47.0, agent-runtime 0.133.2, agent-core 0.6.1, agent-knowledge 7.2.4, sandbox 0.21.1, esbuild 0.28.1. The pnpm-workspace.yaml overrides apply again (present in the lockfile header) and agent-core@0.4.33 is gone (grep -c = 0).

Checks on d258224

  • Clean install (rm -rf node_modules && pnpm install): exit 0, zero peer warnings.
  • pnpm typecheck clean.
  • pnpm test51 files, 657/657 passed (654 + 3 new). No flake this run.
  • pnpm build clean; pnpm check:package ok.

@tangletools tangletools left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Auto-approved drewstone PR — d2582243

This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.

tangletools · auto-approval · reason: drewstone_author · 2026-08-13T01:50:49Z

@tangletools tangletools left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Value Audit — sound

Verdict sound
Concerns 0 (none)
Heuristic 0.0s
Duplication 0.0s
Interrogation 231.9s (2 bridge agents)
Total 231.9s

💰 Value — sound

Forced migration across the agent-eval 0.144.0 caller-owned-execution breaking change, implemented in the codebase's grain by reusing agent-eval's own primitives on the seam it defines — no better approach or existing equivalent found.

  • What it does: Bumps @tangle-network/agent-eval 0.143.0→0.145.3 (plus the peer-cohort agent-runtime 0.126.0→0.133.2 and sandbox 0.17.2→0.21.1, with no pnpm.overrides) and migrates buildAnalysisEngine in src/cli.ts:486 across the 0.144.0 breaking change: createDspyRlmTraceEngine no longer accepts apiKey/baseUrl, so the CLI now supplies a caller-owned call callback (plus callRef and recordExecution). The cal
  • Goals it achieves: (1) Move off a version the rest of the cohort has passed, resolving the peer-cap violation by raising consumers rather than masking it with a tree-wide pnpm.overrides. (2) Own the model-execution path so the CLI controls retries, idempotency, and cost receipts instead of delegating provider credentials into the library. (3) Give that path a reachable test seam — cli.ts calls main() at import (src/
  • Assessment: Sound and in-grain. The 0.144.0 change is documented in node_modules/@tangle-network/agent-eval/CHANGELOG.md lines 173-184 as a deliberate move of execution ownership to the caller ('Agent Eval no longer accepts provider credentials or performs provider retries for this path; the package that owns execution ... makes each admitted call'). The new module implements exactly that seam using the libra
  • Better / existing approach: none — this is the right approach. Searched for a reusable implementation before concluding: (a) agent-eval itself ships no reference callback — the contract comment in node_modules/@tangle-network/agent-eval/dist/external-optimizer-contracts-lixrOZdX.d.ts states 'The package that owns execution implements this with its exact execution path,' so omission is by design; (b) @tangle-network/agent-run
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 2
  • Bridge warning: opencode/kimi-for-coding/k2p7: opencode: opencode error

🎯 Usefulness — sound

Migrates the analyst model call onto agent-eval 0.145's caller-owned seam exactly as the published contract requires; it is reachable from live CLI paths, type-checks clean, and its tests prove the idempotency-key and failure-record behavior end-to-end.

  • Integration: createAnalystModelCall is constructed once per buildAnalysisEngine call (cli.ts:505) and passed as the required call seam to createDspyRlmTraceEngine. buildAnalysisEngine is itself called from the analyze/improve commands at cli.ts:1178 and cli.ts:1203, gated by --llm. The path is live in this PR, not waiting on a caller. The derived type `ExternalOptimizerModelCall = NonNullable<Parameter
  • Fit with existing patterns: This is the only caller-owned model execution path in the repo, and it is shaped exactly to the contract the engine dictates: returns the success/failure discriminated union with a CostReceiptInput receipt and an opaque execution record on every admitted call. recordExecution routes the engine's own observation into the existing analystLog channel (cli.ts:507-512). A separate raw-fetch cal
  • Real-world viability: Failure paths are handled with explicit reasoning, not happy-path-only: abort is detected via signal.aborted || err.name === 'AbortError' and named in the error text so the bridge's 504 branch can never fire on a cancel (analyst-model-call.ts:67-72); provider error bodies are captured via LlmCallError.status/.body and bounded to PROVIDER_ERROR_BODY_LIMIT=2000 because the body is attacker-inf
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 1

No concerns — sound change, no better or existing approach found. ✅


What this audit checks

It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.

Pass What it asks
Heuristic Vague title? Whitespace-only or cruft-bearing diff? (content signals only)
Duplication Do added function/class names already exist elsewhere in the repo?
Value Audit What does it do? What goal does it achieve? Is it good? Better architecture or already-exists?
Usefulness Audit Does it integrate and fit? Will it hold up in real use and actually get used?

Findings are concerns, not blocks — the human reviewer decides what to do with them.

value-audit · 20260813T015506Z

@tangletools

Copy link
Copy Markdown

✅ No Blockers — d2582243

Review health 100/100 · Reviewer score 58/100 · Confidence 85/100 · 26 findings (1 medium, 25 low)

opencode GLM 5.2 opencode DeepSeek v4 Pro opencode DeepSeek v4 Flash aggregate
Readiness 68 74 58 58
Confidence 85 85 85 85
Correctness 68 74 58 58
Security 68 74 58 58
Testing 68 74 58 58
Architecture 68 74 58 58

Reviewer score is advisory once the run is complete and the verdict has no blockers.

Full multi-shot audit completed 5/5 planned shots over 5 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 5/5 planned shots over 5 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 5/5 planned shots over 5 changed files. Global verifier still owns final merge decision.

🟠 MEDIUM Provider timeout is recorded as a caller abort — src/analyst-model-call.ts

const aborted = signal.aborted || err.name === 'AbortError' conflates two distinct failures. Verified against agent-eval 0.145.3's callLlm (dist/llm-client): the per-attempt timeout does setTimeout(() => controller.abort(), timeoutMs) and after retries exhaust, callLlm rethrows the fetch's DOMException with name 'AbortError' while the caller's signal.aborted stays false (empirically reproduced: error name 'AbortError', signal.aborted false). This path therefore returns execution.aborted: true and error text prefixed AbortError: for a plain provider timeout — exactly the marker the bridge uses to distinguish a cancelled call from a transient one it should retry (per the comment at [lines 67-70](https://github.com/tangle-network/traces/blob/d258224318994162be93ee848accf4dd551c4036/

🟡 LOW Large minor-version jumps; API drift must be confirmed by source shots — package.json

agent-runtime jumps 0.126.0→0.133.2 (+7 minors) and sandbox 0.17.2→0.21.1 (+4 minors). package.json itself is correct and resolves, but whether src/cli.ts and src/analyst-model-call.ts still typecheck and behave against the bumped APIs is outside this shot's files. Global verifier should require a green typecheck + the new analyst-model-call tests against these exact versions before merge.

🟡 LOW engines.node range undershoots new dependency's engine floor — package.json

package.json declares "engines": { "node": ">=22" }, but the bumped @tangle-network/agent-runtime@0.133.2 now declares engines { node: '>=22.13.0' } (visible in pnpm-lock.yaml). A user on node 22.0–22.12 passes this package's engine gate but will hit an engine warning/failure when the runtime dependency loads. Impact: misleading support window; no runtime corruption. Fix: tighten to "node": ">=22.13.0" to match the effective floor of the new dependency set.

🟡 LOW sandbox 0.21.1 is ahead of the npm 'latest' dist-tag (0.21.0) — package.json

npm dist-tags show @tangle-network/sandbox latest=0.21.0 while this pin uses 0.21.1 (develop points at a 0.21.1-develop prerelease). 0.21.1 is published, resolvable, and not deprecated, so not blocking — just flag that the pin targets a patch-ahead release rather than the current 'latest'. Confirm intentional.

🟡 LOW agent-runtime engine floor above manifest engines — pnpm-lock.yaml

agent-runtime@0.133.2 declares engines node '>=22.13.0' while package.json engines is '>=22' (allows 22.0.0-22.12.x). Pre-existing (0.126.0 had the same floor, unchanged context line), so not introduced by this PR, and pnpm only warns without .npmrc engine-strict. Recommend aligning package.json engines to '>=22.13.0' for accuracy, but not a merge blocker.

🟡 LOW agent-runtime node engine floor raised to 22.13.0, package.json still declares >=22 — pnpm-lock.yaml

agent-runtime@0.133.2 records engines.node '>=22.13.0' (lockfile line 525), while this repo's package.json engines declares '>=22' (unchanged in this PR). CI uses node-version 22 which resolves to the latest 22.x (well above 22.13.0), so CI and most consumers are fine. But a consumer or downstream tool pinned to Node 22.0–22.12 would fail to install/run agent-runtime, while the package advertises >=22 support. This is a minor version-floor mismatch inherited from the upstream bump, not a lockfile bug. Fix if desired: bump package.json engines to '>=22.13.0' (outside this shot's scope) or accept the drift. Note the old agent-runtime@0.126.0 optional peer 'playwright ^1.40.0' was rem

🟡 LOW No coverage for the LlmResponseError receipt-recovery branch — src/analyst-model-call.ts

costReceiptFromLlmError(err) only returns a non-undefined receipt when err is an LlmResponseError (HTTP completed but contract violated). The test suite covers LlmCallError (400) and AbortError, but never the LlmResponseError path — so the branch that recovers a real receipt from a structured-response failure is exercised only by upstream package tests, not by this CLI's own suite. Add a test that returns a 200 with a body callLlm treats as a contract violation (or stub costReceiptFromLlmError) to assert the recovered receipt flows through instead of the {costUnknown:true, usageUnknown:true} fallback. Coverage gap only; production path is correct by inspection.

🟡 LOW No test asserts non-Error throws are normalized — src/analyst-model-call.ts

Line 66 coerces non-Error throws with new Error(String(error)), but no test exercises a transport or upstream that throws a non-Error value (string, plain object). If a future callLlm revision throws a non-Error, the err.name check and the LlmCallError instanceof check both silently fall through to the generic failure path — which is the intended behavior, but unverified. Minor; add a one-line stub test if callLlm ever changes its throw contract.

🟡 LOW Stale agent-eval version in doc comment — src/analyst-model-call.ts

Comment says 'agent-eval 0.144.0 stopped accepting provider credentials' but package.json pins @tangle-network/agent-eval 0.145.3 and the upgrade commit 0048909 is titled 'agent-eval 0.145.0 with the caller-owned model call'. The version that actually dropped credentials is 0.145.0, not 0.144.0. Fix the comment to read 0.145.0 (or drop the version entirely — the contract description stands on its own). No behavioral impact, only documentation drift.

🟡 LOW costReceiptFromLlmError branch is unreachable via callLlm — src/analyst-model-call.ts

The comment claims costReceiptFromLlmError 'recovers the provider receipt when the response completed but violated the contract'. That recovery path only fires for LlmResponseError, but free-form callLlm never throws LlmResponseError (only LlmCallError on non-2xx and the raw JSON.parse SyntaxError on a non-JSON 200); LlmResponseError is thrown by callLlmJson, which this code does not use. So in practice costReceiptFromLlmError(err) always returns undefined here and the explicit costUnknown/usageUnknown fallback is what actually runs. Correct behavior, misleading comment — tighten the comment to say the fallback is the operative path given callLlm's error set.

🟡 LOW costReceiptFromLlmError is dead code in this path — src/analyst-model-call.ts

The comment claims costReceiptFromLlmError 'recovers the provider receipt when the response completed but violated the contract', but it only returns a receipt for instanceof LlmResponseError (dist/llm-client), which is thrown exclusively by callLlmJson, never by callLlm — the only function used here. The ?? {costUnknown:true, usageUnknown:true} fallback therefore always wins. The fallback is correct and explicit, so this is a redundancy/nit: the recovery branch and its comment describe behavior this file can never exercise.

🟡 LOW endpointFormat contract field is silently dropped — src/analyst-model-call.ts

ExternalOptimizerModelCallRequest carries endpointFormat?: 'chat-completions' | 'responses', but the callback never reads it and always dispatches callLlm, which is chat-completions-only. Not broken today: agent-eval's loopback proxy (external-optimizer-subprocess) normalizes both /v1/chat/completions and /v1/responses requests into the same canonical {model,messages,maxTokens,temperature,thinking} shape before invoking this callback, and encodeCanonicalModelResponse re-encodes the returned ChatResponse back into whichever path the child requested. The residual risk is a provider/model exposed only through the Responses API, which would then receive a chat-completions call and fail. Informational; no fix required unless the CLI starts routing Responses-only models, but worth a one-line com

🟡 LOW endpointFormat contract field is silently ignored — src/analyst-model-call.ts

The admitted-call request carries endpointFormat?: 'chat-completions' | 'responses' (ExternalOptimizerModelCallRequest), and agent-eval's subprocess bridge accepts both /v1/chat/completions and /v1/responses (dist/external-optimizer-subprocess MODEL_PROXY_PATHS). This callback destructures only {request, callId, signal} and always routes through callLlm -> POST {baseUrl}/chat/completions. Today the dspy-rlm controller drives chat-completions, so this is inert; but if the engine ever admits a responses-format request, the call silently hits the wrong endpoint and returns a chat-completions envelope, without any error. Either honor endpointFormat or explicitly reject 'responses'.

🟡 LOW Per-call stderr log adds noise on every successful model call — src/cli.ts

recordExecution logs a line per model call to stderr including successes ('[analyst] model call 1 ok gpt-5-mini'). For analyze/improve --llm runs with many controller turns this is verbose, though it is intentional observability and matches the pre-existing '[analyst] FAIL' registry logging pattern. Not a correctness issue; consider gating success lines behind a verbose flag if stderr noise becomes a problem.

🟡 LOW buildAnalysisEngine wiring has no direct test coverage — src/cli.ts

The new engine options (call: createAnalystModelCall({apiKey,baseUrl}), callRef: traces-cli:${baseUrl}#${model}, recordExecution log callback) are only verified transitively: analyst-model-call.test.ts tests the module in isolation (3/3 pass), but nothing asserts the wiring itself — callRef string format, that recordExecution emits the '[analyst] model call N ok/FAIL' line, or that budget/maxOutputTokens/runner still forward. A future agent-eval contract change or a typo in callRef/observation field access would only fail in real --llm runs. Suggest a unit test on buildAnalysisEngine (option forwarding + recordExecution log format) since the seam is now CLI-owned.

🟡 LOW callRef delimiter collides if OPENAI_BASE_URL contains '#' — src/cli.ts

callRef is built as traces-cli:${baseUrl}#${model}. baseUrl comes from process.env.OPENAI_BASE_URL verbatim. If that URL already carries a fragment ('#...'), the model suffix is appended after a second '#' and the identity becomes ambiguous. callRef is only used as a stable log/provenance string (never parsed back), so impact is cosmetic. Fix: use a delimiter unlikely to appear in a URL, e.g. \u001f (unit separator) or ::, or encodeURIComponent the model segment.

🟡 LOW recordExecution stderr line drops the provider reason and error class — src/cli.ts

analystLog is called with { error: observation.error } only, so the FAIL line prints [analyst] model call N FAIL <model> — <message> but omits error_class and the LlmCallError status/body that analyst-model-call.ts otherwise preserves in the execution record. The comment on analystLog (cli.ts:1124-1126) explicitly wants the structured reason surfaced, and the observation.execution (which carries status/body) is not forwarded here, only the flattened string. Impact: an operator watching stderr sees a status-less failure and must dig into the persisted execution record for the 400/429/500 cause. Fix (optional): pass error_class (e.g. 'LlmCallError') or the execution's status into the fields object so analystLog's existing error_class branch renders it. Not a correctness bug — evidenc

🟡 LOW Abort path does not assert unknown cost/usage receipt — tests/analyst-model-call.test.ts

Test 2 (400) asserts result.receipt.costUnknown===true and usageUnknown===true, but test 3 (abort) does not. The source produces the same fallback receipt for an AbortError because costReceiptFromLlmError returns undefined for non-LlmResponseError (verified at llm-client-d0-2TT1g.js:297-299). An aborted call is the highest-risk case for a stale/incorrect charge being committed, so asserting these flags here would lock the contract symmetrically. Coverage gap, not a bug — source behavior is correct.

🟡 LOW Error-body truncation boundary not exercised — tests/analyst-model-call.test.ts

The mock 400 body is ~70 bytes, well under PROVIDER_ERROR_BODY_LIMIT (2000). The assertion String(execution.body).length <= PROVIDER_ERROR_BODY_LIMIT passes trivially without testing the slice at src/analyst-model-call.ts:79 (err.body.slice(0, PROVIDER_ERROR_BODY_LIMIT)). A body over the limit would prove the truncation actually fires rather than just that the constant is exported. Source comment notes the body is attacker-influenced, so the bound is the load-bearing property — worth stressing.

🟡 LOW No assertion that Authorization header reaches the provider — tests/analyst-model-call.test.ts

The test captures seenIdempotencyKey but not the Authorization header. createAnalystModelCall passes apiKey through to callLlm, which populates 'Authorization: Bearer test-key' (llm-client-d0-2TT1g.js header build). The loopback gateway already receives req.headers, so a one-line assertion on req.headers['authorization'] would close the auth-reachability gap at the seam. Minor — the credential plumbing is owned by callLlm, but this test is the natural place to witness it.

🟡 LOW Provider error body truncation is asserted but never actually exercised — tests/analyst-model-call.test.ts

The test asserts expect(String(execution.body).length).toBeLessThanOrEqual(PROVIDER_ERROR_BODY_LIMIT) (line 88) against an 82-character error body, far under the 2000-char limit. Because the mock body is already tiny, this assertion passes trivially and would still pass even if err.body.slice(0, PROVIDER_ERROR_BODY_LIMIT) were removed from src/analyst-model-call.ts:79. The stated purpose of the limit (bounding attacker-influenced, unbounded bodies held in memory by the proxy) is therefore never verified. Fix: have the non-2xx handler return an error body exceeding 2000 chars and assert the retained body is exactly 2000 (or <= limit AND shorter than the sent bod

🟡 LOW Provider-error-body truncation boundary is never exercised — tests/analyst-model-call.test.ts

The 400 handler sends a ~26-char body; the assertion expect(String(execution.body).length).toBeLessThanOrEqual(PROVIDER_ERROR_BODY_LIMIT) (2000) is trivially satisfied, so the source truncation err.body.slice(0, PROVIDER_ERROR_BODY_LIMIT) (src/analyst-model-call.ts:79) could be deleted and this test still passes. Send a body > 2000 chars (e.g. a 4000-char error message) and assert the retained body is exactly the 2000-char prefix. This path is security-relevant: the comment says the body is attacker-influenced and held in memory.

🟡 LOW Request endpoint path is never asserted — tests/analyst-model-call.test.ts

All three handlers ignore req.url, so a regression in the endpoint construction (${baseUrl}/chat/completions at llm-client:531-532) or a baseUrl/build mismatch would pass undetected. Add e.g. expect(req.url).toBe('/v1/chat/completions') inside a handler to pin the path the proxy actually receives.

🟡 LOW callArgs cast bypasses compile-time shape coupling — tests/analyst-model-call.test.ts

as unknown as Parameters<ReturnType<typeof createAnalystModelCall>>[0] suppresses type errors, so a source rename (e.g. request -> req) or added required field would still compile and only surface at runtime. Derived-type construction is the right idea; the double cast defeats it. Prefer building the object and letting the derived type check it, or add a satisfies-style assertion. Tests still catch drift at runtime, hence low.

🟡 LOW callArgs double-cast silently defeats contract type-checking — tests/analyst-model-call.test.ts

callArgs returns { request, callId, signal } as unknown as Parameters<ReturnType<typeof createAnalystModelCall>>[0] (line 38). The as unknown as chain bypasses the compiler, so if the call contract field names or shape change in agent-eval, the test still compiles and either fails opaquely at runtime or stops testing the real shape. The seam deliberately does not export its contract type by name (see src/analyst-model-call.ts:10-15), which is the underlying cause, but the test can tighten this by deriving the type once and constructing a fully-typed object instead of casting. Minor robustness nit, not a current bug.

🟡 LOW costReceiptFromLlmError recovery branch is untested — tests/analyst-model-call.test.ts

Test 2 covers only the fallback arm (costUnknown/usageUnknown: true). The source's other arm — costReceiptFromLlmError(err) recovering a completed provider receipt on an LlmResponseError (structured-response contract violation) — has no test. A malformed-usage response is the likely trigger; a case asserting the recovered receipt would pin that accounting path. Low because LlmResponseError is not reachable through plain callLlm without jsonSchemaTransport.


tangletools · 2026-08-13T02:21:56Z · trace

@tangletools
tangletools dismissed their stale review August 13, 2026 02:22

Superseded by re-review — no blocking findings on latest commit.

tangletools
tangletools previously approved these changes Aug 13, 2026

@tangletools tangletools left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Approved — 26 non-blocking findings — d2582243

Full multi-shot audit completed 5/5 planned shots over 5 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 5/5 planned shots over 5 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 5/5 planned shots over 5 changed files. Global verifier still owns final merge decision.

Full immutable report for this review: trace

Summary comment for this run: full summary


tangletools · 2026-08-13T02:21:56Z · immutable trace

callLlm aborts an internal controller to enforce its own per-attempt
timeout, so a plain provider timeout rethrows a DOMException named
AbortError while the caller signal stays clear. Reading the error name
marked that timeout as a cancellation, which is the one state the bridge
does not retry — the inverse of what the marker is for.

Reproduced against agent-eval 0.145.3: a gateway that never answers gives
err.name 'AbortError' with signal.aborted false.

Also raise engines.node to 22.13.0, the floor agent-runtime 0.133.2
declares.
@drewstone

Copy link
Copy Markdown
Contributor Author

d258224 findings dispositioned on da25c8f

Fixed — the MEDIUM was a real defect, and mine

"Provider timeout is recorded as a caller abort." Correct, and it inverted the marker I added in the previous commit. Reproduced against the installed agent-eval 0.145.3: a gateway that never answers gives

err.name              = AbortError
err.message           = This operation was aborted
caller signal.aborted = false

callLlm aborts an internal controller to enforce its own per-attempt timeout (llm-client-d0-2TT1g.js:554-556), so the error name cannot distinguish a cancellation from a timeout. Keying on the error name marked a plain timeout as cancelled — the one state the bridge does not retry, which is the opposite of what the marker exists for.

const aborted = signal.aborted now. Only the caller's signal proves a cancellation; a provider timeout stays unmarked and remains retryable.

Pinned by a regression test that fails under the old rule (expected true to be false).

LOW — engines.node undershoots the dependency floor. Raised >=22>=22.13.0 to match agent-runtime 0.133.2. Three lanes flagged this.

Answered

LOW — "large minor jumps; confirm typecheck + tests against these exact versions." That is what the checks below are: run against the installed 0.145.3 / 0.133.2 / 0.21.1 tree, not against a resolved range.

LOW — sandbox 0.21.1 is ahead of the latest dist-tag (0.21.0). Intentional and required. agent-runtime 0.133.2 peers @tangle-network/sandbox >=0.21.1 <0.22.0, so 0.21.0 does not satisfy it. 0.21.1 is published and not deprecated; the latest tag simply lags upstream.

LOW — no coverage for the LlmResponseError receipt-recovery branch. Left uncovered deliberately. costReceiptFromLlmError returns a receipt only for LlmResponseError, which callLlm does not raise on this path — it is the structured-response failure callLlmJson produces. Reaching it from here would mean constructing an error this code path cannot receive, which pins a fiction rather than a behaviour. The branch stays as the fail-closed default it is: no receipt recovered means usage and cost stay explicitly unknown, which the 400 test does assert.

Checks on da25c8f

  • pnpm typecheck clean.
  • pnpm test51 files, 658/658 passed.
  • pnpm build clean; pnpm check:package ok.
  • Tree unchanged and still single-version: agent-eval 0.145.3, agent-interface 0.47.0, agent-runtime 0.133.2, agent-core 0.6.1, agent-knowledge 7.2.4, sandbox 0.21.1, esbuild 0.28.1; zero peer warnings on a clean install.

@tangletools tangletools left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Auto-approved drewstone PR — da25c8fa

This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.

tangletools · auto-approval · reason: drewstone_author · 2026-08-13T02:25:17Z

@tangletools

Copy link
Copy Markdown

⚠️ Review Interrupted — da25c8fa

The review runner stopped before publishing a final verdict: webhook_restarted.

State Detail
Interrupted webhook restarted

No review verdict was produced for this run. Trigger a fresh review on the current PR head if the PR is still open.

tangletools · #85 · model: kimi-for-coding · updated 2026-08-13T03:21:37Z

@tangletools tangletools left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Auto-approved drewstone PR — da25c8fa

This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.

tangletools · auto-approval · reason: drewstone_author · 2026-08-13T03:27:13Z

@tangletools tangletools left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Value Audit — sound

Verdict sound
Concerns 0 (none)
Heuristic 0.0s
Duplication 0.0s
Interrogation 240.8s (2 bridge agents)
Total 240.8s

💰 Value — sound

Forced agent-eval 0.145 migration that moves the analyst model call into the CLI via agent-eval's own callLlm, with three real failure-path correctness fixes (idempotency key, abort classification, provider-cause retention) and tests; ships as-is.

  • What it does: Bumps @tangle-network/agent-eval 0.143.0->0.145.3 (+ agent-runtime 0.126.0->0.133.2, sandbox 0.17.2->0.21.1) and migrates across the 0.144.0 breaking change where createDspyRlmTraceEngine stopped accepting apiKey/baseUrl. The CLI now owns each model call: src/analyst-model-call.ts wraps agent-eval's own callLlm and returns the engine's {succeeded, response, receipt, execution} contract, derives th
  • Goals it achieves: (1) Stay current with agent-eval and its peers without a tree-wide override that hides peer violations. (2) Own the model-call execution path the dependency now requires the caller to own. (3) Make the owned path correct under retry, cancellation, and provider error cases the previous engine-internal path handled opaquely. (4) Give the callback a reachable test seam — src/cli.ts runs main() at imp
  • Assessment: Coherent and in the grain of the codebase. The migration is forced (the dependency removed a capability), and the response reuses agent-eval's own primitives (callLlm, costReceiptFromLlm, costReceiptFromLlmError, LlmCallError) rather than reinventing transport — the wrapper exists only to adapt callLlm's throw-based API into the engine's always-resolves callback contract. The type is derived from
  • Better / existing approach: none — this is the right approach. I searched src/ for existing OpenAI-compatible callers that could be reused or extended. Two exist: zaiChatCaller (src/replay-fix.ts:42) and the callBridge/httpJsonTransport path (src/analyst-engine-prime.ts:511). Neither fits this seam: both return a simple {content, usage} outcome for a one-shot prompt, with no cost receipt, no ledger idempotency key, and no ex
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 2
  • Bridge warning: opencode/kimi-for-coding/k2p7: opencode: opencode error

🎯 Usefulness — sound

Forced migration to agent-eval 0.145.x's caller-owned model-call contract, implemented with the library's own primitives and wired into the single --llm engine path; reaches live CLI commands and is exercised by tests that cover the tricky abort/timeout and idempotency cases.

  • Integration: Reachable and singular. createAnalystModelCall is invoked once at src/cli.ts:505 inside buildAnalysisEngine, which is called from investigate() (cli.ts:1178) and cmdImprove() (cli.ts:1203) — the two CLI paths that carry --llm. The integration is forced: agent-eval 0.145.3's DspyRlmTraceEngineOptions (node_modules/.../index-BWDrSVfw.d.ts:160-206) requires call/callRef/recordExecution and no l
  • Fit with existing patterns: Follows the codebase's grain. It uses agent-eval's own callLlm/costReceiptFromLlm/costReceiptFromLlmError/LlmCallError/LlmCallRequest rather than reinventing an HTTP path, and derives the contract type from the factory (analyst-model-call.ts:15) specifically because the library does not export the contract by name — the right move against silent drift. The callRef/recordExecution wiring in cli.ts:
  • Real-world viability: Holds past the happy path. Tests (tests/analyst-model-call.test.ts, all 4 passing under vitest threads pool) cover: success with the Idempotency-Key header asserted end-to-end through a real HTTP server; non-2xx with provider reason retained and byte-bounded; callLlm internal timeout NOT mislabeled as cancellation; and caller abort correctly marked. The timeout/abort distinction is the load-bearin
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 1

No concerns — sound change, no better or existing approach found. ✅


What this audit checks

It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.

Pass What it asks
Heuristic Vague title? Whitespace-only or cruft-bearing diff? (content signals only)
Duplication Do added function/class names already exist elsewhere in the repo?
Value Audit What does it do? What goal does it achieve? Is it good? Better architecture or already-exists?
Usefulness Audit Does it integrate and fit? Will it hold up in real use and actually get used?

Findings are concerns, not blocks — the human reviewer decides what to do with them.

value-audit · 20260813T033141Z

@tangletools

Copy link
Copy Markdown

✅ No Blockers — da25c8fa

Review health 100/100 · Reviewer score 55/100 · Confidence 85/100 · 23 findings (1 medium, 22 low)

opencode GLM 5.2 opencode DeepSeek v4 Pro opencode DeepSeek v4 Flash aggregate
Readiness 83 71 55 55
Confidence 85 85 85 85
Correctness 83 71 55 55
Security 83 71 55 55
Testing 83 71 55 55
Architecture 83 71 55 55

Reviewer score is advisory once the run is complete and the verdict has no blockers.

Full multi-shot audit completed 5/5 planned shots over 5 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 5/5 planned shots over 5 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 5/5 planned shots over 5 changed files. Global verifier still owns final merge decision.

🟠 MEDIUM Body-truncation guard is never exercised; the length assertion is tautological — tests/analyst-model-call.test.ts

The 400 handler serves a 63-char body ({error:{message:'model not found: test-model',type:'invalid_request_error'}}), so src/analyst-model-call.ts:85 err.body.slice(0, PROVIDER_ERROR_BODY_LIMIT) is a no-op and String(execution.body).length <= 2000 can never fail — the test passes even if the slice is deleted. The guard exists precisely because the body is attacker-influenced and held in memory in the execution record, so the regression it protects is the untested one. Fix: serve a body > PROVIDER_ERROR_BODY_LIMIT and assert the retained body is truncated to exactly the limit (and that no suffix leaks).

🟡 LOW engines.node tightening is a consumer-facing floor bump — package.json

engines.node changed '>=22' -> '>=22.13.0'. This is justified and consistent — agent-runtime 0.133.2 (bumped in the same diff) declares engines {node: '>=22.13.0'}, so keeping the host package in sync avoids a broken install on Node 22.0-22.12 where the dependency would refuse/err. Impact is limited to users on those 13 patch releases of Node 22; a minor-version install/update now errors for them. Not a bug, but confirm the README/install.sh don't still advertise plain Node >=22 (documentation consistency is in a different shot's files).

🟡 LOW peer surface change without a version bump — package.json

agent-runtime 0.126.0 -> 0.133.2 drops the optional playwright peerDependency (visible in the pnpm-lock.yaml diff, old peerDependenciesMeta listed playwright optional; new does not) and the package version stays 0.11.5. A consumer relying on traces to transitively provide playwright through agent-runtime could see a resolution change on upgrade. The main/sdk export surface is unchanged, so this is likely benign, but a minor bump or a changelog note would make the peer-surface shift explicit. No evidence of an actual break.

🟡 LOW Node engine floor silently raised to 22.13.0 — pnpm-lock.yaml

agent-runtime 0.133.2 declares engines node >=22.13.0 (previously >=22 in package.json / agent-runtime 0.126.0 had no >=22.13.0 requirement surfaced in the old graph). package.json engines is bumped to >=22.13.0 in lockstep, so this is consistent. Impact: users on Node 22.0.0–22.12.x will fail install/runtime. This is intentional and aligned across package.json + lockfile, so it is informational, not blocking.

🟡 LOW Cancel-vs-timeout discriminator is a bare text prefix; timeout message contains 'aborted' — src/analyst-model-call.ts

The bridge distinguishes a cancelled call from a transient one by the 'AbortError: ' prefix on message. The per-attempt-timeout path (signal.aborted === false) yields the raw DOMException message 'This operation was aborted' (verified in callLlm's retry loop: controller.abort() on the attempt timeout, rethrown at llm-client-d0-2TT1g.js:745). That message contains the substring 'aborted'. The proxy's own abort classifier is exactly a loose substring test (isAbortError: message.toLowerCase().includes('abort'), external-optimizer-subprocess.js:1076-1077). If the consuming DSPy/litellm stack matches similarly loosely rather than on the exact prefix, a provider timeout is misclassified as a cancellation and the retry the comment promises ([line 72-76](https://github.com/tangle-network/traces/bl

🟡 LOW Failure-path execution evidence omits durationMs and uses a different model source than success — src/analyst-model-call.ts

Success execution records durationMs: response.durationMs and requestedModel: req.model (lines 59-66); failure execution records neither durationMs nor a servedModel, and reads requestedModel: request.model (line 103). The two request objects hold the same value today (req is structuredClone(request)), so this is cosmetic, but the asymmetric provenance record makes a retried/lost-call audit slightly harder and the two model reads could drift if the clone ever changes.

🟡 LOW Idempotency-Key reused across callLlm's temperature-1 retry with a mutated request body — src/analyst-model-call.ts

idempotencyKey: callId is attached to every callLlm attempt. callLlm builds the header set once (llm-client-d0-2TT1g.js:537) but, on a provider's temperature-1 rejection, mutates effectiveRequest.temperature and retries (llm-client-d0-2TT1g.js:608-615) — two different request bodies under one Idempotency-Key. Gateways that bind key to payload hash (OpenAI documents this) reject the fallback retry, turning a previously recoverable temperature-1 rejection into a hard, non-retryable failure. The traces callback cannot prevent the in-flight mutation; either callLlm should regenerate the key when the body changes, or the collision should be documented as a known edge.

🟡 LOW Provider error body may echo request headers into persisted evidence — src/analyst-model-call.ts

The LlmCallError body (attacker/gateway-influenced) is retained verbatim up to 2000 chars in execution provenance. The code deliberately keeps it out of the log line, but if a gateway reflects request headers into an error body, the Bearer credential would be persisted inside the first 2000 chars — the slice bound does not redact. The author acknowledges this risk in the comment at line 82, so it is an accepted residual, but it is not fully mitigated: the bound limits size, not the presence of a secret. Consider a header/credential redaction pass on the body before persisting, or dropping the body when it contains an Authorization-looking substring.

🟡 LOW Success path uses req.model, failure path uses request.model — cosmetic inconsistency — src/analyst-model-call.ts

On success, requestedModel: req.model (the structuredClone). On failure, requestedModel: request.model (the original readonly request). Both always hold the same value since clone is a deep copy and model is a string. The failure path correctly cannot reference req (declared inside try). Purely cosmetic — no behavioral impact.

🟡 LOW baseUrl persisted in opaque execution evidence — src/analyst-model-call.ts

The execution record includes baseUrl on both success (line 57) and failure (line ~100) paths. Per the ExternalOptimizerModelExecutionObservation contract, the opaque execution object is persisted as provenance by the engine's recordExecution hook. baseUrl is not a secret (apiKey is correctly excluded), and for a CLI tool where the operator configures the endpoint this is acceptable. But if this provenance is ever exported or shared, the internal gateway URL is exposed. No fix required for this use case; flagging for awareness.

🟡 LOW baseUrl persisted verbatim into every execution record — src/analyst-model-call.ts

baseUrl is stored in both success and failure execution evidence (lines 58, 102). In cli.ts the value comes from OPENAI_BASE_URL (cli.ts:501), which a user can set with embedded userinfo or a query token (https://user:token@host/v1). Those credentials would then persist into the run's model-execution evidence, which the proxy retains (recordExecutionReceipt) and which flows into reports/upload. The apiKey is correctly never included; consider redacting any credential-shaped URL components from the stored baseUrl or documenting the expectation that baseUrl is credential-free.

🟡 LOW costReceiptFromLlmError branch is dead in this path — src/analyst-model-call.ts

costReceiptFromLlmError only returns a receipt for LlmResponseError (see agent-eval llm-client source: return error instanceof LlmResponseError ? costReceiptFromLlm(error.result) : void 0). This path calls callLlm, which throws LlmCallError / DOMException(AbortError) / JSON parse errors — never LlmResponseError (that is thrown by callLlmStructured). So the ?? fallback at line 93 always fires and the recovered-receipt branch never does. Not a bug — the fallback is correct — but the comment at lines 90-92 ('recovers the provider receipt when the re

🟡 LOW Model-call execution evidence is not surfaced after a failed --llm run — src/cli.ts

recordExecution reduces every admitted model call to one stderr line ('[analyst] model call ok|FAIL '); the full observation (callId, servedModel, finishReason, durationMs, and for failures the LlmCallError status/body) is captured by createAnalystModelCall but never logged or written anywhere the operator can inspect. The engine retains these transiently in result.runtime.modelExecutions (verified in installed dspy-rlm-engine dist, line ~306), but the traces report path renders none of it, so a mid-run model failure after the fact leaves only the one-liner. Not a regression vs. the pre-change engine-internal logging, but the new wrapper deliberately builds richer evidence that this sink throws away. Suggest writing the observation (or at least the FAIL branch with status/body

🟡 LOW No repo test exercises the full engine->bridge->proxy->call handshake — src/cli.ts

tests/analyst-model-call.test.ts covers the call callback in isolation against a stub gateway, and tests/cli.test.ts:861-941 uses a bridge that dies at startup (DSPY-BRIDGE-FAILURE) so it never reaches a model call. Nothing in the repo drives the complete path createDspyRlmTraceEngine -> Python bridge -> loopback model proxy -> createAnalystModelCall -> provider and back, so a mismatch in the proxy<->callback contract (e.g. budget fields, request shape) would pass CI. The seam is agent-eval's own tested code and the callback is type-locked to the engine's parameter type (NonNullable<Parameters[0]['call']>), so this is a coverage gap rather than a defect.

🟡 LOW recordExecution drops the execution provenance the seam retained — src/cli.ts

The contract for recordExecution (agent-eval external-optimizer-contracts.d.ts) is 'Persist the finite execution record returned for every admitted call', and ExternalOptimizerModelExecutionObservation carries an execution field plus callId/path/responseStatus. src/analyst-model-call.ts:82-87 deliberately routes the provider's error status/body into execution and NOT into the log line, because 'a gateway can echo request headers into an error body'. But buildAnalysisEngine's recordExecution (lines 507-512) only writes a one-line message (sequence/succeeded/model/error) and discards observation.execution entirely — so the provider error detail (status code + body snippet) has n

🟡 LOW Body-truncation assertion never exercises the PROVIDER_ERROR_BODY_LIMIT boundary — tests/analyst-model-call.test.ts

The test asserts String(execution.body).length <= PROVIDER_ERROR_BODY_LIMIT but the error body is ~80 bytes, so the slice(0, 2000) in src/analyst-model-call.ts:85 never actually truncates. The invariant holds trivially. To prove the bound fires, feed a body > 2000 bytes and assert the retained length equals PROVIDER_ERROR_BODY_LIMIT exactly. Impact: a regression that removes the slice (e.g., passing the full body) would not be caught. Low severity because the assertion still guards against a future unbounded body.

🟡 LOW Error-body truncation bound is asserted but never exercised — tests/analyst-model-call.test.ts

The assertion expect(String(execution.body).length).toBeLessThanOrEqual(PROVIDER_ERROR_BODY_LIMIT) uses a mock error body of ~86 chars (JSON.stringify of {error:{message:'model not found: test-model',...}}) against a 2000-char limit. The actual truncation (err.body.slice(0, PROVIDER_ERROR_BODY_LIMIT) in src/analyst-model-call.ts:85) is never hit, so a regression that drops the slice or raises the limit would still pass. This is a security-bounding behavior (the body is attacker-influenced and held in memory by the proxy), so it deserves a >2000-byte fixture asserting the body is exactly sliced to the limit.

🟡 LOW Idempotency-key reuse across retries is not covered — tests/analyst-model-call.test.ts

The mock gateway responds 200 on the first attempt, so callLlm never retries and the test only proves the header is sent once. The comment's stated purpose ('a retried-but-already-billed call is not charged twice') is never actually observed: no test makes the first attempt fail transiently and asserts the second attempt carries the same idempotency-key header. A regression that regenerated the key per attempt would pass the suite.

🟡 LOW Module-global server shared across tests with overwrite-on-start — tests/analyst-model-call.test.ts

startGateway overwrites the module-level server before the previous one is closed in afterEach, and running.close() waits on open sockets (no closeAllConnections/closeIdleConnections). Tests run sequentially today so it works (verified 4/4), but a test that failed before afterEach, or a handler that leaves a keep-alive socket open, silently leaks/strands the prior server and can stall close() up to keepAliveTimeout. Scoping the server per-test and calling closeAllConnections() in cleanup would make the harness robust to future additions.

🟡 LOW Timeout test asserts only the absence of a marker, not the surfaced error — tests/analyst-model-call.test.ts

expect(result.error.startsWith('AbortError:')).toBe(false) is a weak negative assertion. The meaningful claim (aborted=false) is covered, but nothing verifies the call actually timed out versus failing some other way — the test would also pass if callLlm surfaced, say, a network error with a non-AbortError message. The timeout itself is only proven implicitly (the test hangs at vitest's 5s limit if the internal timeout never fires). Asserting elapsed time (>= ~timeoutMs) or the specific AbortError-flavored message would make the distinction airtight.

🟡 LOW Timeout test duration (~2.25s) couples to callLlm retry defaults and TANGLE_LLM_MAXIMUM_ATTEMPTS env — tests/analyst-model-call.test.ts

callLlm classifies the internal-timeout AbortError as transient (/AbortError/i in TRANSIENT_ERROR_PATTERNS) and retries up to DEFAULT_MAXIMUM_ATTEMPTS (3). The test therefore runs 3 × 250ms timeouts + 500ms + 1s backoff ≈ 2250ms (confirmed: 2256ms measured). The seam createAnalystModelCall does not forward maximumAttempts, so the test cannot pin it. If CI exports TANGLE_LLM_MAXIMUM_ATTEMPTS > ~10, the test could approach the 30s suite timeout. The assertions are correct regardless of retry count (aborted stays false because only the caller signal is read), so this is efficiency/robustness, not correctness.

🟡 LOW Timeout-test runtime scales with TANGLE_LLM_MAXIMUM_ATTEMPTS env — tests/analyst-model-call.test.ts

The 250ms timeout test depends on callLlm's retry loop: with the default maximumAttempts=3 it runs ~2.25s (timeouts + 500/1000ms backoff), but agent-eval reads TANGLE_LLM_MAXIMUM_ATTEMPTS from the environment (default 3). If a developer or CI sets it high, the test slows linearly. It also couples to agent-eval internals (AbortError-as-timeout classification), which the source comment deliberately documents — acceptable, but worth pinning maximumAttempts via the call opts or noting the env dependency.

🟡 LOW costReceiptFromLlmError recovery branch is untested — tests/analyst-model-call.test.ts

The 400 test only exercises the ?? {costUnknown:true,usageUnknown:true} fallback. The other receipt branch in the source — a response that completed but violated the schema contract (LlmResponseError, where costReceiptFromLlmError returns a real receipt) — has no test, so a completed-and-billed call that must NOT read as costUnknown/usageUnknown is not guarded. Also untested: success path with missing/partial usage (usageUnknown=true on a succeeded call).


tangletools · 2026-08-13T03:51:56Z · trace

@tangletools tangletools left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Approved — 23 non-blocking findings — da25c8fa

Full multi-shot audit completed 5/5 planned shots over 5 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 5/5 planned shots over 5 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 5/5 planned shots over 5 changed files. Global verifier still owns final merge decision.

Full immutable report for this review: trace

Summary comment for this run: full summary


tangletools · 2026-08-13T03:51:56Z · immutable trace

@drewstone
drewstone merged commit ba0bcb8 into main Aug 13, 2026
1 check passed
@drewstone
drewstone deleted the chore/agent-eval-0.145.0 branch August 13, 2026 03:56
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.

2 participants