fix(#6936): persist token telemetry when agents are cancelled - #6938
Conversation
GitHub Actions cancellation sends SIGINT followed by SIGTERM ~7.5 s later. signal.NotifyContext stops listening after the first signal, so the second signal hit the default "terminate" handler and killed the process before metrics.json and the agent telemetry span could be written — silently losing API credit usage data on every cancelled Review run. Two fixes: 1. Replace signal.NotifyContext with a persistent signal channel that absorbs all subsequent SIGINT/SIGTERM deliveries. The first signal still cancels the context; subsequent signals no longer terminate the process, giving the cleanup path time to flush metrics and telemetry before the process exits normally. 2. Add a cancellation short-circuit in the agent iteration loop: when ctx.Err() is non-nil after rt.Run returns, immediately aggregate metrics, finalize the agent span, and write metrics.json — before extraction and validation that would be pointless on a dead sandbox. This ensures the artifact upload step (if: always()) captures the partial usage data even if the process is subsequently killed. Dollar cost (TotalCostUSD) remains zero on cancellation because it is only available from the terminal ResultEvent, which a killed run never emits. Token counts (input, output, cache_read, cache_creation) are captured via the deferred TokensEvent from PR #6908. Closes #6936
|
🤖 Review · Commit: |
|
🤖 Finished Review · ✅ Success · Started 10:50 PM UTC · Completed 11:06 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $4.66 |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Risk Assessment: moderate (2/5) DetailsRe-review anchoring: Tier 1 metadata unchanged from prior run (small-medium diff, no protected/security/CI/dependency changes, strong 0.67 test ratio, bot author) at ~1.25; Tier 2 confirms run.go/run_test.go/telemetry_run_test.go remain hot, multi-author, fix/revert-prone files, offset by very recent code age, at ~2.5; Tier 3 unchanged in character (no risk labels, small scoped follow-up) at ~2.25. Composite 1.825 rounds to 2, matching the prior moderate score with no signal providing a specific reason to diverge. Previous runRisk Assessment: moderate (2/5) DetailsTier 1 metadata is low risk (small-medium diff, no protected/security paths, no CI/dependency changes, strong 0.60 test ratio, bot author), but Tier 2 git history shows run.go and run_test.go are hot, multi-author, fix/revert-prone files with coupling to files not touched in this PR; Tier 3 is moderate given 5/6 acceptance criteria addressed and no risk labels. The weighted composite (Tier1=1, Tier2=3, Tier3=2) rounds to 2 (moderate). Previous run (2)Risk Assessment: moderate (2/5) DetailsRe-review confirms prior score: Tier 1 signals are unchanged (1.25) — bot author, 0 protected paths, no dependency/CI changes, 60% test ratio; internal/cli/run.go remains a high-churn hotspot but this is characteristic of the file (Tier 2: 2.8); issue scope matches well with most acceptance criteria covered and no unresolved discussions, rollback safety slightly elevated due to signal-handling changes (Tier 3: 2.0). Composite 1.87 rounds to 2. Previous run (3)Risk Assessment: moderate (2/5) DetailsRe-review confirms prior score: Tier 1 signals are unchanged (1.25) — bot author, 0 protected paths, no dependency/CI changes, 60% test ratio; internal/cli/run.go remains a high-churn hotspot but this is characteristic of the file (Tier 2: 2.8); issue scope matches well with clear acceptance criteria covered and no unresolved discussions, rollback safety slightly elevated due to signal-handling changes (Tier 3: 2.0). Composite 1.87 rounds to 2. Previous run (4)Risk Assessment: moderate (2/5) DetailsRe-review confirms prior score: small bug fix (4 files, 206 lines, 50% test ratio) by bot author with no protected paths or dependency/CI changes (Tier 1: 1.25); internal/cli/run.go remains an extreme churn hotspot but this is characteristic of the file (Tier 2: 3.0); issue scope matches with clear acceptance criteria mostly covered, though rollback safety is elevated since the change modifies process signal handling (Tier 3: 2.0). Composite 1.93 rounds to 2. Previous run (5)Risk Assessment: moderate (2/5) DetailsSmall bug fix (4 files, 206 lines, 50% test ratio) by bot author with no protected paths, no dependency or CI changes — Tier 1 is low (1.25). Tier 2 is elevated (3.25) due to high churn on internal/cli/run.go (100 commits in 30d, 18 authors). Tier 3 moderate (2.75) — issue scope matches but change modifies process lifecycle without a feature flag. Composite 2.15 rounds to 2. |
|
Looks good to me Previous runReviewFindingsMedium
Low
Other dimensions reviewed with no findings above the reporting threshold: style-conventions (the previously-flagged Next steps:
Previous run (2)ReviewFindingsLow
Previous run (3)ReviewFindingsMedium
Low
Next steps:
Previous run (4)Looks good to me Previous run (5)ReviewFindingsLow
Next steps:
|
|
🤖 Finished Fix · ✅ Success · Started 11:08 PM UTC · Completed 11:15 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $0.93 |
…nvention Use "Failed to write metrics.json: " instead of "Cancellation: failed to write metrics.json: " to match the established error-message pattern used elsewhere in runAgent. Addresses #6938
🔧 Fix agent — iteration 1 (bot-triggered)Aligned the cancellation-path error message with the existing convention used elsewhere in runAgent. Single-line change from novel "Cancellation: failed to ..." prefix to the established "Failed to ..." pattern. Fixed (1):
Tests: passedNext steps:
|
|
🤖 Finished Review · ✅ Success · Started 11:18 PM UTC · Completed 11:31 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $3.62 |
Superseded by updated review
|
/fs-fix increase the code coverage |
|
🤖 Finished Fix · ✅ Success · Started 3:06 PM UTC · Completed 3:18 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $2.23 |
…ion metrics Extract signalContext() from main() to make the signal-absorption logic (#6936) independently testable. Add three tests verifying the first-signal-cancels, subsequent-signals-absorbed, and cleanup-stops-forwarding invariants. cmd/fullsend coverage rises from 0% to 52.9% (signalContext: 100%). Add two aggregate-metrics tests for the multi-iteration cancellation scenario: one for aggregateRunMetrics and one for the writeMetricsJSON round-trip, covering the data path the cancellation short-circuit relies on. Addresses #6938
🔧 Fix agent — iteration 2 (human-triggered)Increased code coverage by extracting signalContext() from main() (3 new tests, 100% function coverage) and adding 2 multi-iteration cancellation scenario tests for aggregateRunMetrics and writeMetricsJSON round-trip. cmd/fullsend coverage rose from 0% to 52.9%. Fixed (2):
Tests: passed Decision points
Next steps:
|
|
🤖 Finished Review · ✅ Success · Started 3:20 PM UTC · Completed 3:37 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $4.91 |
Superseded by updated review
|
🤖 Finished Fix · ✅ Success · Started 3:39 PM UTC · Completed 3:47 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $1.21 |
|
🤖 Finished Review · ✅ Success · Started 3:48 PM UTC · Completed 4:05 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $5.14 |
Superseded by updated review
…ncel-metrics Signed-off-by: Adam Scerra <ascerra@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
🤖 Finished Review · ✅ Success · Started 3:42 PM UTC · Completed 4:02 PM UTC Commit: Runtime: pi · Model: sonnet → claude-sonnet-5 · Effort: high · Cost: $4.94 |
Superseded by updated review
|
🤖 Finished Fix · ✅ Success · Started 4:04 PM UTC · Completed 4:20 PM UTC Commit: Runtime: claude · Model: sonnet → claude-sonnet-5 · Effort: high · Cost: $1.91 |
…anup test Extracts the cancellation short-circuit in runAgent's per-iteration loop into handleRunCancellation, a directly unit-testable function, and adds TestHandleRunCancellation to drive it: cancelled ctx with non-zero RunMetrics asserts metrics.json is written with partial token counts, the returned error wraps context.Canceled, and content/span finalization runs while downstream extraction/validation is bypassed. A live-context case confirms the short-circuit doesn't fire on normal iterations. Addresses the missing-test finding on PR #6938 (this was previously untested load-bearing production code for #6936). Also tightens TestSignalContext_CleanupStopsForwarding: it now sends a SIGINT after cleanup() and asserts on an independently registered probe channel that ctx is not cancelled, rather than only checking ctx state before any signal was ever sent, which passed regardless of whether signal.Stop actually worked. Addresses #6938
🔧 Fix agent — iteration 4 (bot-triggered)Addressed both review findings on PR #6938. Extracted the untested cancellation short-circuit in runAgent into handleRunCancellation and added TestHandleRunCancellation exercising the cancelled-context, non-cancelled-context, and pre-existing-runErr cases, verifying metrics.json persistence, error wrapping of context.Canceled, span finalization, and that downstream extraction/validation is skipped. Tightened TestSignalContext_CleanupStopsForwarding to actually send a signal after cleanup() and verify via an independent probe channel that signal.Stop disabled forwarding, rather than asserting on a state that was true regardless of whether cleanup worked. This is iteration 4, so per strategy escalation guidance I moved from the prior iterations' approach (adding tests only for already-existing helpers like aggregateRunMetrics/writeMetricsJSON) to extracting and directly testing the actual load-bearing branch. Fixed (2):
Tests: passed
Decision points
Next steps:
|
|
🤖 Finished Review · ✅ Success · Started 4:22 PM UTC · Completed 4:35 PM UTC Commit: Runtime: pi · Model: sonnet → claude-sonnet-5 · Effort: high · Cost: $3.06 |
Superseded by updated review
|
This is important enough and it looks good to me, merging. |
|
🤖 Finished Retro · ✅ Success · Started 7:32 AM UTC · Completed 7:44 AM UTC Commit: Runtime: claude · Model: sonnet → claude-sonnet-5 · Effort: high · Cost: $1.67 |
|
PR #6938 (fullsend-ai/fullsend, merged 2026-09-10) fixed issue #6936: a double-signal (SIGINT then SIGTERM) killing cancelled Review runs before telemetry/metrics.json could be written. The rework loop itself was clean — 4 fix iterations, each mapping 1:1 to a real prior review finding, no thrashing or reverted work, and iteration 4 shows good self-correction (the code agent escalated from testing only helpers to extracting and directly unit-testing the actual However, this run reproduces three failure patterns that already have open tracking issues, and I'm treating them as corroborating evidence rather than new proposals per the duplicate-check policy:
No new proposals filed — all identified gaps are already covered by open issues in fullsend-ai/agents and fullsend-ai/fullsend. Recommend the humans triaging those issues use this PR's timeline as a fresh, concrete data point (#6938). |
|
Follow-up PR #7208 addresses the remaining USD cost gap: when a run is cancelled before the |
GitHub Actions cancellation sends SIGINT followed by SIGTERM ~7.5 s later. signal.NotifyContext stops listening after the first signal, so the second signal hit the default "terminate" handler and killed the process before metrics.json and the agent telemetry span could be written — silently losing API credit usage data on every cancelled Review run.
Two fixes:
Replace signal.NotifyContext with a persistent signal channel that
absorbs all subsequent SIGINT/SIGTERM deliveries. The first signal
still cancels the context; subsequent signals no longer terminate
the process, giving the cleanup path time to flush metrics and
telemetry before the process exits normally.
Add a cancellation short-circuit in the agent iteration loop: when
ctx.Err() is non-nil after rt.Run returns, immediately aggregate
metrics, finalize the agent span, and write metrics.json — before
extraction and validation that would be pointless on a dead sandbox.
This ensures the artifact upload step (if: always()) captures the
partial usage data even if the process is subsequently killed.
Dollar cost (TotalCostUSD) remains zero on cancellation because it is only available from the terminal ResultEvent, which a killed run never emits. Token counts (input, output, cache_read, cache_creation) are captured via the deferred TokensEvent from PR #6908.
Closes #6936
Post-script verification
agent/6936-persist-cancel-metrics)318e04c56f6457aa88b368b9731305d1f9659adb..HEAD)