Skip to content

fix(#5361): set span status from run outcome, not runErr alone - #5944

Merged
waynesun09 merged 12 commits into
fullsend-ai:mainfrom
dhshah13:fix/5361-span-status
Aug 12, 2026
Merged

fix(#5361): set span status from run outcome, not runErr alone#5944
waynesun09 merged 12 commits into
fullsend-ai:mainfrom
dhshah13:fix/5361-span-status

Conversation

@dhshah13

@dhshah13 dhshah13 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Agent and root spans previously exported Status Ok for failed runs: both status writes keyed solely on runErr == nil, and the agent span ended before the Agent API errors silently swallowed when Claude Code exits 0 with is_error:true #2786 is_error transcript check could flip the outcome. Any consumer keying on span/trace status (MLflow trace state, dashboards, filters) counted failed iterations as successes — one leg of the file-vs-MLflow discrepancy gating the tracing rollout.
  • The agent span is now finalized after the transcript check via finalizeAgentSpan: a runtime error, a transcript-reported error, or a non-zero exit is Status Error, with RecordError attaching the exception event on both error paths (runtime error and transcript error), message UTF-8-repaired. The root span keys on the run outcome: validation passed is Ok (validation, not the last agent exit, is the success gate); otherwise a non-zero telemetryExitCode is Error even when runErr is nil (harnesses without a validation_loop).
  • Two deliberate calls, stated up front: (1) agent-span exit_code stays the raw process exitfullsend.transcript_error marks the Agent API errors silently swallowed when Claude Code exits 0 with is_error:true #2786 override, status is the failure signal, and the root span's exit_code remains the effective telemetryExitCode, so run-level filtering is unaffected; (2) status descriptions are bounded at 2,000 bytes total — prefix and truncation ellipsis included — on a UTF-8 rune boundary. No OTel, collector, or backend limit mandates a value; 2,000 reuses maxTranscriptErrorLength, the repo's bound for the same class of error text (raised from this PR's earlier 256 after review). The transcript payload is budgeted for its transcript error: prefix: messages within the 1,982-byte headroom pass through untouched, while parser-truncated messages (which arrive at 2,012+ bytes) are re-truncated, with the full text preserved on the exception event. The same bounding applies to runErr messages and to the sandbox-create status (which embeds raw container logs — previously exported unbounded and unrepaired), because an invalid-UTF-8 or oversized status fails proto marshaling of the whole export batch. Exception events are bounded too — 8,192 bytes, UTF-8-repaired, cut on a rune boundary (added after review: the sandbox-create error embeds supervisor/gateway logs collected with no line limit, and the SDK never truncates event attribute values). The event still carries the fuller copy of every truncated status, and a parser-truncated transcript message stays whole on it even after sanitization growth. The transcript console line prints the same bounded, sanitized string the span records, through the rendering the GHA annotation path uses (TranscriptError.DisplayMessage), whose subtype fallback is bounded with the same truncateError treatment ErrorMessage gets at parse time (the parser never truncates Subtype). Span attribute values get a provider-level SDK limit of 8,192 characters — the SDK counts characters, not bytes, so multibyte content can reach four bytes per character on the wire, and it repairs invalid UTF-8 only when it truncates, so every dynamic string attribute is UTF-8-repaired at the source (stringAttr), with both SDK properties pinned by a canary test — the first non-empty of OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT / OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT decides alone, matching the SDK's own resolution: a parseable value there (including -1, unlimited) is honored as-is, and an override the SDK discards falls back to the 8,192 default — so free-text attributes — a transcript-derived model name, a pre-script skip reason — are bounded without per-site caps; the SDK applies this limit to span attributes only, which is why events are bounded at their call site instead. Classification (ADR 0050): error text on statuses and exception events — including the container/supervisor log excerpt a failed sandbox create embeds — is Level 1/2 "errors" metadata, not Level 3 content; the same excerpt was exported unbounded on the sandbox status before this PR, and both channels are now bounded. The ADR's Level 1 wording states this explicitly.

Relates to #5361 (fixes the span-status leg only — the enrichment and cost-fidelity legs remain open, so this deliberately does not close it). The diff does not touch the root-span attribute block that #5788 edits.

Evidence (live runs, minimal-explore, local sandbox)

Run Transcript Agent spans before Agent spans after
auth failure (invalid_grant) is_error=true, exit 1, $0 Status OK, exit_code=1 Status ERROR "agent exited with code 1", exit_code=1
green control success, validation passed Status OK, exit_code=0 Status OK, exit_code=0 (unchanged)

Root span on the failed run: Status ERROR "validation failed after 2 iteration(s)" with an exception event.

Test plan

  • TestAgentSpanStatus / TestRootSpanStatus — table tests pinning both outcome→status mappings, including the Agent API errors silently swallowed when Claude Code exits 0 with is_error:true #2786 exit-0 override, the no-validation-loop root case, and validation-passed-with-non-zero-last-exit staying Ok
  • TestFinalizeAgentSpantracetest.SpanRecorder tests pinning the exported span: status, fullsend.transcript_error marker, raw exit_code, exception event, ended exactly once
  • TestTruncateStatusMsg / TestAgentSpanStatus_TranscriptBoundary — cap includes the ellipsis; multi-byte rune straddling the cap stays valid UTF-8; invalid UTF-8 repaired at any length; prefixed transcript status never exceeds the cap and a fitting message is not re-truncated
  • TestSetup_SpanAttributeValueLengthLimit / TestSpanLimits — a 100KB span attribute exports truncated to 8,192; operator env setting wins over the default
  • TestRecordSanitizedError / TestTranscriptErrorMessage / TestTranscriptError_DisplayMessage — exception events bounded (ellipsis included, rune boundary, UTF-8 repaired); the transcript message is sanitized, bounded against an untruncated Subtype, and worst-case sanitization growth (4,014 bytes at the colon-run fixed point) stays whole on the event
  • go test ./internal/cli/ -race full suite passes (183s)
  • Live validation: failed run (fake GCP credentials → invalid_grant) exports agent spans ERROR + root ERROR with exception event; green control run unchanged, all Ok

🤖 Generated with Claude Code

@dhshah13
dhshah13 requested a review from a team as a code owner August 5, 2026 17:57
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

E2E tests did not run

The ok-to-test label was removed because new commits landed after it was applied. A maintainer must re-apply ok-to-test after reviewing the latest changes.

Note: ok-to-test was cleared due to new commits.

See E2E testing guide for details.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix OTel span status to reflect run outcome (incl. transcript errors)

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Set agent span status from iteration outcome, including transcript-reported failures.
• Set root span status from run outcome (validation gate), not runErr alone.
• Cap span status descriptions to 256 bytes on UTF-8 rune boundaries.
Diagram

graph TD
A["runAgent"] --> B["Agent iteration"] --> C[("output.jsonl")] --> D["Transcript parse"] --> E{"Outcome?"} --> F["finalizeAgentSpan"] --> G["OTLP exporter"]
A --> H["rootSpanStatus"] --> G
subgraph Legend
  direction LR
  _p["Process"] ~~~ _d{"Decision"} ~~~ _db[("File/Store")]
end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Centralize outcome into a single RunOutcome struct
  • ➕ Reduces parameter lists (runErr/exitCode/validationPassed/transcriptErr) passed around
  • ➕ Makes it harder for agent/root status logic to diverge over time
  • ➖ More refactor surface area in already-sensitive run/telemetry code
  • ➖ Bigger change than necessary for a correctness fix
2. Keep existing code shape and defer span.End() later
  • ➕ Minimizes new helper functions and keeps logic inline
  • ➕ Still fixes the ordering bug (ending agent span before transcript check)
  • ➖ Harder to test span finalization behavior in isolation
  • ➖ Risk of repeating status/attribute logic across multiple early-return paths

Recommendation: Current approach is the best fit: finalizeAgentSpan + explicit status-mapping helpers fix the ordering issue, make the agent/root semantics clear (iteration vs validation gate), and are directly unit-tested (including UTF-8-safe truncation and transcript override signaling). The alternatives either expand refactor scope or reduce testability.

Files changed (3) +237 / -17

Bug fix (1) +95 / -17
run.goFinalize agent spans after transcript check and derive span statuses from outcome +95/-17

Finalize agent spans after transcript check and derive span statuses from outcome

• Moves agent span finalization to occur after transcript parsing so transcript-reported failures can set span status. Introduces 'finalizeAgentSpan', 'agentSpanStatus', and 'rootSpanStatus', and switches root span status to be based on validation outcome plus telemetry exit code; also caps status descriptions at 256 bytes on UTF-8 rune boundaries.

internal/cli/run.go

Tests (1) +141 / -0
telemetry_run_test.goAdd tests for span status mapping and finalized span contents +141/-0

Add tests for span status mapping and finalized span contents

• Adds table tests for agent/root span status mapping, including transcript override and validation-gated success. Adds tests for UTF-8-safe truncation and for 'finalizeAgentSpan' behavior using a 'tracetest.SpanRecorder' (status, attributes, and exception event).

internal/cli/telemetry_run_test.go

Documentation (1) +1 / -0
distributed-tracing.mdDocument transcript-error marker on agent spans +1/-0

Document transcript-error marker on agent spans

• Adds the 'fullsend.transcript_error' attribute to distributed tracing docs. Clarifies that agent spans can be Status Error even when 'exit_code' remains 0 due to transcript-reported failures.

docs/guides/infrastructure/distributed-tracing.md

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Site preview

Preview: https://f677ef58-site.fullsend-ai.workers.dev

Commit: 3e1ac84af84c818292ceae1737a0f4368c2cd780

@qodo-code-review

qodo-code-review Bot commented Aug 5, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Status cap not strict ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
truncateStatusMsg slices to maxSpanStatusMsgLen bytes and then appends a UTF-8 ellipsis, so the
returned status description can exceed maxSpanStatusMsgLen despite comments implying a hard cap.
This makes the helper’s contract ambiguous and can break any internal assumption that 256 is an
absolute maximum for status descriptions.
Code

internal/cli/run.go[R2372-2375]

+	for len(truncated) > 0 && !utf8.Valid([]byte(truncated)) {
+		truncated = truncated[:len(truncated)-1]
+	}
+	return truncated + "…"
Relevance

●● Moderate

Team accepts UTF-8-safe truncation patterns, but no clear precedent requiring strict max after
ellipsis.

PR-#816

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The implementation slices to maxSpanStatusMsgLen and then appends an ellipsis, which can exceed
the configured maximum. The added unit test explicitly allows maxSpanStatusMsgLen + len("…"),
confirming this is not currently a strict cap.

internal/cli/run.go[2359-2376]
internal/cli/telemetry_run_test.go[479-486]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`truncateStatusMsg` is intended to cap span status descriptions to `maxSpanStatusMsgLen`, but it currently truncates to 256 bytes and then appends `"…"` (3 bytes), producing a result longer than the configured cap.

### Issue Context
The helper is used for OTel span status descriptions in both agent and root spans.

### Fix Focus Areas
- internal/cli/run.go[2359-2376]
- internal/cli/telemetry_run_test.go[479-486]

### Suggested fix
- Decide whether the cap is a *hard* maximum including the ellipsis.
 - If hard: truncate to `maxSpanStatusMsgLen - len("…")` before UTF-8 boundary adjustment and then append the ellipsis.
 - If not hard: rename/update comments/constant to reflect that the ellipsis is added on top (e.g., `maxSpanStatusPrefixLen`) and adjust docs accordingly.
- Update `TestTruncateStatusMsg` to assert the chosen contract (e.g., `len(got) <= maxSpanStatusMsgLen`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Guide not under admin/user ⊘ Outdated 📜 Skill insight ⌂ Architecture
Description
docs/guides/infrastructure/distributed-tracing.md is a guide but it is not placed under
docs/guides/admin/ or docs/guides/user/ as required. This breaks the documented guide
directory/audience structure and risks mixing audiences or misplacing documentation.
Code

docs/guides/infrastructure/distributed-tracing.md[217]

+| `fullsend.transcript_error` | `agent` spans | Present (`true`) when the agent exited 0 but its transcript reported an error — the span's status is Error while `exit_code` keeps the raw process exit |
Relevance

● Weak

Similar guide-placement violation was previously flagged and explicitly rejected; repo keeps
dev/infrastructure guides as-is.

PR-#5502
PR-#5454
PR-#4901

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1062077 requires all guide files under docs/guides/ to be placed in either
admin/ or user/ subdirectories. The modified guide is in docs/guides/infrastructure/, which
violates this placement rule.

docs/guides/infrastructure/distributed-tracing.md[1-6]
Skill: writing-user-docs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The guide file is located under `docs/guides/infrastructure/`, but guides must live under either `docs/guides/admin/` or `docs/guides/user/`.

## Issue Context
This PR modifies `docs/guides/infrastructure/distributed-tracing.md`, making the guide subject to the directory/audience placement requirement.

## Fix Focus Areas
- docs/guides/infrastructure/distributed-tracing.md[214-220]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context
✅ Compliance rules (platform): 54 rules

Grey Divider

Tip of the day
💡 Did you know, you can enable the Remediation agent and Qodo fixes findings in a dedicated fix PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread internal/cli/run.go Outdated
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Automated review pass (3 findings).

Comment thread internal/cli/run.go Outdated
Comment thread internal/cli/run.go
Comment thread internal/cli/run.go
@dhshah13

dhshah13 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

All three confirmed and fixed in 92505a6.

Sanitization: correct — the transcript fields reach a new export sink here and had none. sanitizeOutput is unexported, so this adds a thin exported SanitizeOutput wrapper in internal/runtime and routes both te.ErrorMessage and the te.Subtype fallback through it, matching emitTranscriptErrors. Pinned by TestSanitizeOutput_ExportedWrapper.

Missing event on the transcript path: correct, and the PR description's "nothing is lost" was wrong as written. finalizeAgentSpan now records the untruncated message as an exception event on that branch too, so the claim holds uniformly rather than being narrowed. TestFinalizeAgentSpan asserts a >256-byte transcript message survives on the event while the status description is capped.

UTF-8 on the short path: correct — the guard was conditional on length, which defeats its stated purpose. truncateStatusMsg now applies strings.ToValidUTF8 to every input before the length check, with a short-invalid-input test case.

Note TestDummyRuntime_Bootstrap and TestDummyRuntime_ClearIterationArtifacts fail on my machine independently of this branch (verified with the changes stashed) — they look environment-dependent, not related to this PR.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Automated review pass (1 finding).

Comment thread internal/cli/run.go Outdated
Comment thread internal/cli/run.go Outdated
@dhshah13

dhshah13 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Confirmed and fixed. 256 was an invented conservative number — no OTel, collector, or MLflow limit motivates it, and the PR shouldn't imply one exists. Adopted your alignment suggestion: maxSpanStatusMsgLen is now 2000, matching maxTranscriptErrorLength's bound for the same class of consumer-facing error text, with the comment stating explicitly that no external constraint mandates the value. A side benefit of the alignment: transcript messages are parse-time-truncated to 2000 already, so they are never truncated a second time at the status layer.

@dhshah13
dhshah13 force-pushed the fix/5361-span-status branch from ac19a07 to 029a88b Compare August 6, 2026 18:33

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review-only pass: 2 findings verified against the current head commit (029a88b).

Comment thread internal/cli/run.go Outdated
Comment thread internal/cli/run.go
@dhshah13

dhshah13 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Both findings fixed in de43047, and the PR description is updated to describe the shipped head.

Double truncation: confirmed — your arithmetic is right (up to 2,015 parser bytes + 18-byte prefix > 2,000), and the comment stated an invariant that was false on exactly the motivating path. The cap is now a true total bound: truncateStatusMsgTo budgets the transcript payload for its prefix and counts the ellipsis against the limit, so the prefixed status never exceeds 2,000 bytes; messages within the 1,982-byte headroom pass through untouched, and parser-truncated messages (which arrive at 2,012+ bytes) are re-truncated with the full text preserved on the exception event — stated plainly now, in the comment and the description, rather than implied away. TestAgentSpanStatus_TranscriptBoundary pins both boundary directions, including the parser's maximum.

Stale description: updated — the deliberate-decisions bullet now states the 2,000-byte bound and its rationale.

Auditing the claims for this round also surfaced a sibling gap now fixed in the same commit: the sandbox-create span exported an unbounded, unrepaired status built from an error that embeds raw supervisor/gateway/container logs — by this PR's own argument, one invalid byte there fails proto marshaling of the batch carrying every other span. It now gets the same treatment (recordSanitizedError + bounded status), which also makes the cap comment's "every status built from an error" claim true for the whole file. The rune-straddle test moved to the actual cut point (cap minus ellipsis) — at the old offset the walk-back loop was no longer exercised.

Comment thread internal/cli/run.go Outdated
Comment thread internal/cli/run.go

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Automated review pass (2 findings).

Comment thread internal/telemetry/telemetry.go Outdated
Comment thread internal/runtime/sanitize.go Outdated
Comment thread internal/telemetry/telemetry.go
Comment thread internal/cli/run.go Outdated
@waynesun09 waynesun09 added the ok-to-test Allow e2e CI to run after maintainer review (must be re-applied after each push) label Aug 12, 2026
@waynesun09
waynesun09 added this pull request to the merge queue Aug 12, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 12, 2026
…lone

Agent and root spans previously reported Status Ok whenever runErr was
nil, so failed runs exported as successes: the agent span ended before
the fullsend-ai#2786 is_error transcript check could flip the outcome, and a
harness without a validation loop returned nil alongside a non-zero
agent exit.

- Finalize the agent span after the transcript check via
  finalizeAgentSpan: a runtime error, transcript-reported error, or
  non-zero exit is Status Error. exit_code keeps the raw process exit;
  fullsend.transcript_error marks the override; RecordError attaches
  the exception event.
- Key the root span status on the run outcome: validation passed is Ok
  (validation, not the last agent exit, is the success gate); otherwise
  a non-zero telemetryExitCode is Error even when runErr is nil.
- Cap status descriptions at 256 bytes on a UTF-8 rune boundary — an
  invalid-UTF-8 status fails proto marshaling of the entire OTLP batch.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
…TF-8 unconditionally

Addresses review findings on the span-status fix:

- Transcript error text is agent-controlled and now reaches a new export
  sink (span status). Route it through the same sanitization the GHA
  annotation path applies, via a new exported SanitizeOutput wrapper, so
  ANSI escapes and workflow-command markers cannot reach telemetry
  backends.
- Record the untruncated transcript error as a span event, matching the
  runtime-error path — the 256-byte status cap no longer loses verbose
  API error payloads.
- Repair invalid UTF-8 for every status description, not only those long
  enough to trigger truncation: a short malformed message would otherwise
  reach the exporter and fail proto marshaling of the whole batch.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
…tatus descriptions

RecordError feeds the same OTLP proto marshal path as SetStatus, so an
invalid-UTF-8 error message fails export of the whole batch — the exact
failure mode the previous commit fixed for status descriptions, left open
on the exception-event path. Route both RecordError call sites through a
recordSanitizedError helper that repairs the message first.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
…error precedent

256 was an invented number with no cited constraint. No OTel, collector,
or backend limit mandates a specific value, so adopt the repo's existing
bound for the same class of consumer-facing error text —
maxTranscriptErrorLength's 2000 bytes — which also means a
parse-time-truncated transcript message is never truncated twice. The
comment now states the constraint honestly.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
… close the sandbox-status gap

Review round: the cap comment claimed a parse-time-truncated transcript
message is never truncated twice — false by arithmetic (truncateError
emits up to 2,015 bytes, the status prefix adds 18). The cap is now a
true total bound: truncateStatusMsgTo budgets the transcript payload for
its prefix, the ellipsis counts against the limit, and messages within
the 1,982-byte headroom pass through untouched. Boundary test added at
the parser's maximum.

Audit fallout, same defect class: the sandbox-create span exported an
unbounded, unrepaired status built from an error embedding raw
supervisor/gateway/container logs — one invalid byte there fails proto
marshaling of the batch carrying every other span. It now gets the
sibling treatment (recordSanitizedError + bounded status), which also
makes the cap comment's every-error-status claim true repo-wide. The
straddle test moved to the actual cut point (cap minus ellipsis) — at
the old offset the walk-back was no longer exercised.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
…ript console line

Two findings from the round-5 review pass:

- recordSanitizedError bounds the exception message at maxSpanEventMsgLen
  (8192) on top of UTF-8 repair. The sandbox-create error embeds raw
  supervisor/gateway logs collected with no line limit, the SDK never
  truncates event attribute values, and the same text rides the root span
  via the wrapped runErr — an oversized batch can be rejected by the
  collector whole.

- The transcript-failure console line prints the same bounded, sanitized
  string the span event records, via TranscriptError.DisplayMessage — one
  rendering shared with the GHA annotation path. Console behavior changes:
  an empty ErrorMessage prints the subtype fallback instead of a bare
  trailing colon, multi-line messages are flattened, and :: markers are
  broken — a raw transcript payload could otherwise start a
  ::workflow-command:: line in the CI job log. The bound also covers
  Subtype, which unlike ErrorMessage is not parser-truncated.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
…ute values

Closes the two remaining unbounded telemetry inputs adjacent to the
round-5 fixes:

- TranscriptError.DisplayMessage bounds the subtype fallback with the
  same truncateError treatment ErrorMessage gets at parse time — the
  transcript parser never truncates Subtype and accepts 1MB lines, so
  the fallback could otherwise reach the console line, the span sinks,
  and the GHA annotation path unbounded.

- telemetry.Setup configures provider span limits: attribute values are
  SDK-truncated (UTF-8-safely) at 8192 bytes, matching the
  exception-event bound, unless the operator sets
  OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT or
  OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT. Free-text attributes (a
  transcript-derived model name, a pre-script skip reason) were
  otherwise unbounded. The limit does not apply to event attributes,
  which recordSanitizedError already bounds.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
…d drop the unused wrapper

- spanLimits treated the SDK's -1 as 'operator did not configure', but -1
  is also the OTel sentinel an operator sets explicitly for unlimited —
  the two collapse to the same struct value in NewSpanLimits. The env
  vars are now consulted directly (parsed the way the SDK parses them),
  so any parseable operator setting, including -1, is honored as-is and
  the 8192 default applies only when neither variable is set.

- The exported runtime.SanitizeOutput wrapper lost its last production
  caller when the console line moved to TranscriptError.DisplayMessage;
  removed until a real external consumer lands.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
… pin every span finalizer

- attrValueLenConfigured returned true if either env var parsed, but the
  SDK's firstInt short-circuits on the first non-empty variable — an
  unparseable specific var plus a valid generic var left attribute
  values unbounded. The helper now mirrors firstInt exactly (first
  non-empty key decides alone), with the SDK's own NewSpanLimits output
  asserted as ground truth in the test so the two cannot drift silently.

- The sandbox-create and root spans finalize through extracted, tested
  helpers (finalizeSandboxSpan, finalizeRootSpan), matching the agent
  span: all three spans now pin the exception event, bounded repaired
  status, and end-exactly-once on the wire via SpanRecorder tests.

- Recorder tests pin the OTEL span-limit env vars so an ambient
  OTEL_SPAN_EVENT_COUNT_LIMIT on a CI runner cannot drop the exception
  events they assert on.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
…e 8192 bounds

- sanitize()'s single ReplaceAll pass reconstituted a literal "::" at
  the seam of two replacements ("::::" -> ": :: :"), so a crafted colon
  run survived the GHA command-injection guard. Colon pairs now break to
  a fixed point — no output contains "::", re-sanitizing is a no-op, and
  at most two passes run. Pinned by exact-output tests for 3-6 colon
  runs and an idempotence property test over both sanitize variants.
  Worst-case sanitization growth becomes just under 2x (4,014 bytes for
  the parser-max transcript message), still well inside the event bound.

- maxSpanEventMsgLen is now defined from the exported
  telemetry.MaxSpanAttrValueLen, so the two 8,192 defaults cannot drift;
  the comments on both sides state the shared-default relationship and
  the deliberate non-tracking of runtime attribute overrides.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
…an-limit tests hermetic

- ADR 0050's Level 1 'errors' bullet now states what error text covers:
  bounded, repaired, sanitized-where-agent-controlled operational error
  text on statuses and exception events, including the container/
  supervisor log excerpt a failed sandbox create embeds. The Level 3
  gate governs prompt/completion capture, which is separate from error
  reporting. The same excerpt was exported unbounded on the sandbox
  status before this branch bounded both channels; finalizeSandboxSpan's
  comment records the classification.

- pinOTELEnv clears OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT and
  OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, which spanLimits made load-bearing,
  so every telemetry test is hermetic against ambient runner env —
  TestSetup_SpanAttributeValueLengthLimit failed under an ambient
  512-byte override before this.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
…e and state the SDK's units

- The SDK's attribute limit repairs invalid UTF-8 only when it truncates
  — a value at or under the limit passes through untouched, and one
  invalid byte fails proto marshaling of the whole OTLP batch, the exact
  failure mode statuses and events already guard against. Every dynamic
  string attribute (agent name, work-item id, model, skip reason, system,
  security trace id) now goes through stringAttr, which repairs to valid
  UTF-8; literal values keep attribute.String.

- The SDK counts attribute characters, not bytes: a multibyte value can
  reach four bytes per character on the wire, and the event-message
  bound counts bytes. Comments on both constants now state the units and
  that the coupling is a shared numeric default, each side applying it
  in its own unit.

- TestAttrLimit_SDKBehaviorCanary pins both SDK properties (character
  counting, no under-limit repair) against the live SDK, so an upgrade
  that changes either fails the suite instead of silently shifting the
  export contract.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
@waynesun09
waynesun09 force-pushed the fix/5361-span-status branch from cd273a6 to 3e1ac84 Compare August 12, 2026 17:21
@github-actions github-actions Bot removed the ok-to-test Allow e2e CI to run after maintainer review (must be re-applied after each push) label Aug 12, 2026
@waynesun09
waynesun09 enabled auto-merge August 12, 2026 17:22
@waynesun09
waynesun09 added this pull request to the merge queue Aug 12, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 12, 2026
@waynesun09
waynesun09 added this pull request to the merge queue Aug 12, 2026
Merged via the queue into fullsend-ai:main with commit 7253ee3 Aug 12, 2026
12 of 13 checks passed
@fullsend-ai-retro

fullsend-ai-retro Bot commented Aug 12, 2026

Copy link
Copy Markdown

🤖 Finished Retro · ✅ Success · Started 6:15 PM UTC · Completed 6:34 PM UTC

Commit: 3e1ac84 · View workflow run →

@fullsend-ai-retro

Copy link
Copy Markdown

Retro Analysis: PR #5944

Workflow type: Human-authored fork PR with agent-assisted manual review. No automated fullsend review/code/fix agents dispatched (fork PR security gate); only retro dispatched on merge.

Timeline

  • Aug 5: dhshah13 opened PR fixing OTel span status derivation (issue telemetry: MLflow trace columns (Name/Session/User/Source) blank; token & cost omit cache tokens #5361). Fork PR from dhshah13/fullsend — 11 files, +954/−45.
  • Aug 5–12: waynesun09 conducted 10 rounds of agent-assisted review, producing 20 findings (0 false positives), including 2 HIGH-severity security bugs (GHA command injection via non-idempotent colon sanitization; unbounded exception events from sandbox-create).
  • Aug 12: PR approved after all findings addressed and merged.

Key Observations

Review quality was exceptional. 0% false positive rate across 20 findings. Every finding was accepted and resulted in a code change. The reviewer traced through vendored OTel SDK source, wrote standalone test programs, and fuzzed the colon-break fix with 20,000+ adversarial inputs.

Review ratchet consumed 40% of findings. 8 of 20 findings targeted code written to fix earlier findings, creating a 5-deep causal chain (UTF-8 repair → RecordError asymmetry → unbounded exception events → span limit semantics → env-var precedence). This chain consumed 5 of the PR's 7 calendar days.

29 wasted CI routing runs. All fullsend.yaml runs except the final retro dispatch hit "No stage matched" — largely from pull_request_review events on non-changes_requested states.

Evidence for existing issues

Proposals filed

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