Skip to content

fix(intelligence): per-run span exports verbatim attribute keys#432

Merged
drewstone merged 2 commits into
mainfrom
fix/intelligence-span-plain-attrs
Jul 2, 2026
Merged

fix(intelligence): per-run span exports verbatim attribute keys#432
drewstone merged 2 commits into
mainfrom
fix/intelligence-span-plain-attrs

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

Problem

The intelligence per-run span (tangle.intelligence.run) was built with the loop-topology span builder, which namespaces every payload key under loop.*. 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. loopEventToOtelSpan keeps its namespacing — loop payload fields are free-form; spans whose keys ARE the contract use the flat builder.
  • exportTrace emits via flatOtelSpan, adds tangle.runId.
  • OTLP scope version pin corrected 0.33.00.79.3 (was stamping wrong SDK provenance on every span).
  • Tests updated to the verbatim contract; the drop-in example already suffix-matched (comment corrected).

recordTrace (loop-topology export) is unchanged — loop spans stay loop.*-namespaced by design; adc reader tolerance for those is a separate PR.

Verification

  • lint, typecheck, docs:check green; intelligence tests 100% pass; full suite 1169/1172 with the only failure 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.

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
tangletools previously approved these changes Jul 1, 2026

@tangletools tangletools left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ 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 tangletools left a comment

Copy link
Copy Markdown
Contributor

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 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 flatOtelSpan in src/otel-export.ts:204 — a single-span builder that emits attribute keys verbatim — and switches the intelligence per-run export in src/intelligence/index.ts:375 from loopEventToOtelSpan (which prefixes every payload key with loop.) to flatOtelSpan. It also updates the OTLP scope version pin from 0.33.0 to 0.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 as loop.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 in src/index.ts for existing span builders. loopEventToOtelSpan always namespaces payload keys, and buildLoopOtelSpans reconstructs loop topology trees — neither fits a single flat span with contract keys. A boolean or namespace option on loopEventToOtelSpan would 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 — exportTrace in src/intelligence/index.ts:375 — which sits on every traceRun and withTangleIntelligence call. It is NOT re-exported from the package barrel (src/index.ts exports only buildLoopOtelSpans + loopEventToOtelSpan), so it is internal-only surface with zero dead public API. The recordTrace loop
  • Fit with existing patterns: Fits the codebase grain. The exporter already had two distinct builders — loopEventToOtelSpan (flat, loop.-namespaced, for free-form loop payloads) and buildLoopOtelSpans (nested tree). flatOtelSpan is 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.

value-audit · 20260701T234316Z

…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 tangletools left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ 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 tangletools left a comment

Copy link
Copy Markdown
Contributor

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.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 (no loop.* prefix). The intelligence per-run span now uses this instead of loopEventToOtelSpan, so the Intelligence plane's readers can exact-match keys like tangle.sessionId, gen_ai.request.model, and tangle.usage.*. Also fixes the OTLP scope version from 0.33.0 to 0.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.sessionId etc., so sessions/models/cost were never derived. 2) Enable a prompt-only delivery lane — callers can reuse the same cache/refresh/coalesce logic as withCertifiedDelivery but for their own prompt assembly.
  • Assessment: Both changes are coherent and well-scoped. flatOtelSpan is the right design — a separate function with a clearly different contract rather than adding a boolean prefix flag to loopEventToOtelSpan (the flag approach would muddle the latter's intentional namespacing). The extraction of createCertifiedPromptSource properly deduplicates: withCertifiedDelivery now 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 (loopEventToOtelSpan always prefixes; buildLoopOtelSpans builds topology trees with explicit keys). No existing cached-prompt-source pattern outside withCertifiedDelivery itself — the extraction IS the reuse. The boolean-flag alternative for loopEventToOtelSpan would 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.

value-audit · 20260702T000758Z

@drewstone drewstone merged commit 7c4dec8 into main Jul 2, 2026
1 check passed
drewstone added a commit that referenced this pull request Jul 2, 2026
The loop-prefix removal (#432) left attrs['project'] where biome's
useLiteralKeys wants attrs.project — dot access, no behavior change.
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