fix(intelligence): per-run span exports verbatim attribute keys#432
Conversation
The intelligence run span reused the loop-topology span builder, which namespaces every payload key under loop.* — so contract keys the plane exact-matches (tangle.sessionId, gen_ai.request.model, tangle.usage.*) arrived prefixed and were invisible to session/model/cost readers. - flatOtelSpan: flat single-span builder with verbatim keys (otel-export) - exportTrace emits tangle.intelligence.run via flatOtelSpan, stamps tangle.runId - OTLP scope version pin corrected to the package version
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 9e696cd3
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-07-01T23:31:48Z
tangletools
left a comment
There was a problem hiding this comment.
🟢 Value Audit — sound
| Verdict | sound |
| Concerns | 0 (none) |
| Heuristic | 0.0s |
| Duplication | 0.0s |
| Interrogation | 79.0s (2 bridge agents) |
| Total | 79.0s |
💰 Value — sound
Fixes intelligence per-run spans by exporting contract keys verbatim instead of loop.*-namespaced, plus corrects the OTLP scope version to match the package; a clean, targeted fix that ships.
- What it does: Adds
flatOtelSpaninsrc/otel-export.ts:204— a single-span builder that emits attribute keys verbatim — and switches the intelligence per-run export insrc/intelligence/index.ts:375fromloopEventToOtelSpan(which prefixes every payload key withloop.) toflatOtelSpan. It also updates the OTLP scope version pin from0.33.0to0.79.3(src/otel-export.ts:62, matching `package.json - Goals it achieves: Makes intelligence per-run telemetry readable by downstream plane readers that exact-match unprefixed keys (
gen_ai.request.model,tangle.usage.*,tangle.runId). Before the change, those keys arrived asloop.gen_ai.request.model,loop.tangle.usage.*, etc., so session/model/cost derivation saw nothing. After the change, the OFF-tier billing proof (tangle.usage.intelligence_usd: 0) and mo - Assessment: Good change. It fixes a concrete ingestion bug, keeps loop-topology spans namespaced by design (free-form payloads need it), and gives contract-key spans their own explicit builder. The separation is documented and the code follows the existing style of
src/otel-export.ts. The version pin correction is a small but real accuracy win. - Better / existing approach: none — this is the right approach. I searched
src/otel-export.ts,src/intelligence/index.ts, and the public exports insrc/index.tsfor existing span builders.loopEventToOtelSpanalways namespaces payload keys, andbuildLoopOtelSpansreconstructs loop topology trees — neither fits a single flat span with contract keys. A boolean or namespace option onloopEventToOtelSpanwould muddy i - Model: opencode/kimi-for-coding/k2p7
- Bridge attempts: 1
🎯 Usefulness — sound
Fixes a real contract bug: the intelligence per-run span was namespacing every key under loop.*, so the plane's exact-match readers could never derive session/model/cost; a new flat verbatim-key builder aligns the export with the OTel GenAI contract the plane already speaks.
- Integration: Reachable and live.
flatOtelSpan(src/otel-export.ts:204) has exactly one caller —exportTracein src/intelligence/index.ts:375 — which sits on everytraceRunandwithTangleIntelligencecall. It is NOT re-exported from the package barrel (src/index.ts exports onlybuildLoopOtelSpans+loopEventToOtelSpan), so it is internal-only surface with zero dead public API. TherecordTraceloop - Fit with existing patterns: Fits the codebase grain. The exporter already had two distinct builders —
loopEventToOtelSpan(flat,loop.-namespaced, for free-form loop payloads) andbuildLoopOtelSpans(nested tree).flatOtelSpanis a coherent third: same shape, but verbatim keys for spans whose attributes ARE the downstream contract. The rationale (loop payload = free-form → namespace; intelligence span = contract keys - Real-world viability: Robust by construction. The new function is a pure builder with no I/O or concurrency; it shares the exact same serialization helpers as the other two builders, so no new edge-input or error-path surface. The call site remains wrapped in the existing try/catch best-effort guard (src/intelligence/index.ts:382), so export failures still never break the agent's turn. Verbatim keys (`gen_ai.request.mo
- 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.
…pt-only delivery lane Product chat routes assemble their own system prompt, so the delivery surface they need is a cached certified-prompt composer, not the agent wrapper. createCertifiedPromptSource owns the refresh-window + coalesced pull + keep-last-known cache once; withCertifiedDelivery now rides it.
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 22352016
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-07-01T23:46:17Z
tangletools
left a comment
There was a problem hiding this comment.
🟢 Value Audit — sound
| Verdict | sound |
| Concerns | 0 (none) |
| Heuristic | 0.0s |
| Duplication | 0.1s |
| Interrogation | 1002.4s (2 bridge agents) |
| Total | 1002.5s |
💰 Value — sound
Fixes intelligence span attribute keys to be verbatim (matching the plane's exact-match contract) via a new flatOtelSpan builder, and extracts the cached-prompt-refresh logic into createCertifiedPromptSource for reuse by product chat routes — both in the codebase's grain with no better alternati
- What it does: Adds
flatOtelSpan()in otel-export.ts — an OTLP span builder that emits attribute keys verbatim (noloop.*prefix). The intelligence per-run span now uses this instead ofloopEventToOtelSpan, so the Intelligence plane's readers can exact-match keys liketangle.sessionId,gen_ai.request.model, andtangle.usage.*. Also fixes the OTLP scope version from0.33.0to0.79.3. Separately, e - Goals it achieves: 1) Fix a data-contract mismatch: the plane's readers exact-match unprefixed keys but were receiving
loop.tangle.sessionIdetc., so sessions/models/cost were never derived. 2) Enable a prompt-only delivery lane — callers can reuse the same cache/refresh/coalesce logic aswithCertifiedDeliverybut for their own prompt assembly. - Assessment: Both changes are coherent and well-scoped.
flatOtelSpanis the right design — a separate function with a clearly different contract rather than adding a booleanprefixflag toloopEventToOtelSpan(the flag approach would muddle the latter's intentional namespacing). The extraction ofcreateCertifiedPromptSourceproperly deduplicates:withCertifiedDeliverynow delegates to it, and standal - Better / existing approach: none — this is the right approach. Checked: no existing verbatim-span builder in otel-export.ts or elsewhere (
loopEventToOtelSpanalways prefixes;buildLoopOtelSpansbuilds topology trees with explicit keys). No existing cached-prompt-source pattern outsidewithCertifiedDeliveryitself — the extraction IS the reuse. The boolean-flag alternative forloopEventToOtelSpanwould be worse (two d - Model: opencode/deepseek/deepseek-v4-pro
- Bridge attempts: 3
- Bridge warning: opencode/kimi-for-coding/k2p7: bridge stream ended without value-audit content; opencode/zai-coding-plan/glm-5.2: bridge stream ended without value-audit content
🎯 Usefulness — sound
The change fixes the intelligence per-run span contract by emitting verbatim attribute keys, and it refactors certified-prompt delivery into a reusable cached source that is already consumed by withCertifiedDelivery and exported for product chat routes.
- Integration: flatOtelSpan is called from createIntelligenceClient.exportTrace at src/intelligence/index.ts:381-388, which is reached by the intelligence-drop-in example (examples/intelligence-drop-in/intelligence-drop-in.ts:19,94) and by tests. createCertifiedPromptSource is consumed internally by withCertifiedDelivery at src/intelligence/delivery.ts:285 and is exported publicly from src/intelligence/index.ts:
- Fit with existing patterns: It matches the codebase's grain. The OTEL module already had loopEventToOtelSpan for namespaced loop events and buildLoopOtelSpans for loop topology; flatOtelSpan fills the missing single-span builder for contract-key spans. The delivery module already cached/refresh certified profiles inside withCertifiedDelivery; createCertifiedPromptSource simply exposes the prompt-only lane without duplicating
- Real-world viability: The new source handles concurrency (coalesces concurrent refreshes, caches within the refresh window), error paths (404 keeps the base prompt, later failures keep the last-known profile, never throws), and timeouts via pullCertified's AbortSignal.timeout. Tests at src/intelligence/delivery.test.ts:236-280 cover caching, 404, stale-profile retention, and concurrency. flatOtelSpan is best-effort and
- Model: opencode/kimi-for-coding/k2p7
- Bridge attempts: 2
- Bridge warning: opencode/zai-coding-plan/glm-5.2: bridge stream ended without value-audit content
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.
The loop-prefix removal (#432) left attrs['project'] where biome's useLiteralKeys wants attrs.project — dot access, no behavior change.
Problem
The intelligence per-run span (
tangle.intelligence.run) was built with the loop-topology span builder, which namespaces every payload key underloop.*. The intelligence plane's readers exact-match unprefixed keys (tangle.sessionId,gen_ai.request.model,tangle.usage.*), so exported product turns would ingest but never derive sessions, model, or cost.Change
flatOtelSpan(name, attributes, traceId, timestampMs, parentSpanId?)in otel-export: a flat single-span builder that emits attribute keys VERBATIM.loopEventToOtelSpankeeps its namespacing — loop payload fields are free-form; spans whose keys ARE the contract use the flat builder.exportTraceemits viaflatOtelSpan, addstangle.runId.0.33.0→0.79.3(was stamping wrong SDK provenance on every span).recordTrace(loop-topology export) is unchanged — loop spans stayloop.*-namespaced by design; adc reader tolerance for those is a separate PR.Verification
tests/loops/strategy-evolution.test.ts— pre-existing load-sensitive flake (fails on unchanged main under concurrent load, passes solo 19/19 in this worktree).Compatibility
New spans carry unprefixed keys the plane already expects. Historical spans keep
loop.*keys; reader-side tolerance for those ships in agent-dev-container.