Repository navigation
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 |
🔧 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 |
PR #7208 spent five review cycles on a SIGINT-via-sandbox-exec side-channel so cancelled runs could flush total_cost_usd. Upstream OpenShell cannot deliver that signal: sandbox exec is fire-and-forget (NVIDIA/OpenShell#3159, not planned) and sandbox stop sends SIGKILL, not SIGINT (#2855 / #3036). That knowledge lived only in the closed PR thread. Annotate Accepted ADR 0030 with the exec/stop signal contract, cite the upstream issues, and cross-reference the token-count partial fix (#6936 / PR #6938) and the abandoned dollar-cost attempt (PR #7208). The Commands: SSH decision is unchanged. Note: pre-commit run could not complete (git fetch of hook repos returned HTTP 403). Matching hooks were run directly and passed: lint-adr-status, lint-adr-numbers, lint-adr-frontmatter, lint-docs-links, lychee, gitleaks, trailing-whitespace, end-of-file, and detect-private-key. Closes #7229
PR #7208 spent five review cycles on a SIGINT-via-sandbox-exec side-channel so cancelled runs could flush total_cost_usd. Upstream OpenShell cannot deliver that signal: sandbox exec is fire-and-forget (NVIDIA/OpenShell#3159, not planned) and sandbox stop sends SIGKILL, not SIGINT (#2855 / #3036). That knowledge lived only in the closed PR thread. Annotate Accepted ADR 0030 with the exec/stop signal contract, cite the upstream issues, and cross-reference the token-count partial fix (#6936 / PR #6938) and the abandoned dollar-cost attempt (PR #7208). The Commands: SSH decision is unchanged. Note: pre-commit run could not complete (git fetch of hook repos returned HTTP 403). Matching hooks were run directly and passed: lint-adr-status, lint-adr-numbers, lint-adr-frontmatter, lint-docs-links, lychee, gitleaks, trailing-whitespace, end-of-file, and detect-private-key. Closes #7229
PR #7208 spent five review cycles on a SIGINT-via-sandbox-exec side-channel so cancelled runs could flush total_cost_usd. Upstream OpenShell cannot deliver that signal: sandbox exec is fire-and-forget (NVIDIA/OpenShell#3159, not planned) and sandbox stop sends SIGKILL, not SIGINT (#2855 / #3036). That knowledge lived only in the closed PR thread. Annotate Accepted ADR 0030 with the exec/stop signal contract, cite the upstream issues, and cross-reference the token-count partial fix (#6936 / PR #6938) and the abandoned dollar-cost attempt (PR #7208). The Commands: SSH decision is unchanged. Note: pre-commit run could not complete (git fetch of hook repos returned HTTP 403). Matching hooks were run directly and passed: lint-adr-status, lint-adr-numbers, lint-adr-frontmatter, lint-docs-links, lychee, gitleaks, trailing-whitespace, end-of-file, and detect-private-key. Closes #7229
PR #7208 spent five review cycles on a SIGINT-via-sandbox-exec side-channel so cancelled runs could flush total_cost_usd. Upstream OpenShell cannot deliver that signal: sandbox exec is fire-and-forget (NVIDIA/OpenShell#3159, not planned) and sandbox stop sends SIGKILL, not SIGINT (#2855 / #3036). That knowledge lived only in the closed PR thread. Annotate Accepted ADR 0030 with the exec/stop signal contract, cite the upstream issues, and cross-reference the token-count partial fix (#6936 / PR #6938) and the abandoned dollar-cost attempt (PR #7208). The Commands: SSH decision is unchanged. Note: pre-commit run could not complete (git fetch of hook repos returned HTTP 403). Matching hooks were run directly and passed: lint-adr-status, lint-adr-numbers, lint-adr-frontmatter, lint-docs-links, lychee, gitleaks, trailing-whitespace, end-of-file, and detect-private-key. Closes #7229
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)