What's wrong
band installs no signal handler and runs cobra without a cancellable context:
// cmd/root.go
func Execute() error {
api.Version = version
return rootCmd.Execute() // no context
}
grep -rn "signal.Notify\|NotifyContext" cmd/ internal/ returns nothing. So cmd.Context() is context.Background() everywhere, and Ctrl-C kills the process via the Go default — no cleanup, no output.
Why it matters more than it looks
internal/cmdutil/poll.go grew a PollConfig.Context field specifically so a cancelled poll could exit gracefully, and cmd/tendlc/async.go's awaitTerminal has a cancellation branch whose entire job is to emit the partial-result receipt. Neither can ever fire from a real SIGINT, because nothing upstream produces a cancellable context. The plumbing exists with no producer.
For the 10DLC commands that matters concretely. Once POST /brands returns its 202 the write may already have succeeded, and bandwidthId is the only thing that cannot be recovered afterward — it's why awaitTerminal owns stdout on every failure path. Press Ctrl-C during band tendlc brand create --wait after the 202 lands and the process dies with nothing on stdout. The brand exists, is billable, and the caller has no ID for it.
Same shape on number order --wait, call create --wait, and transcription create --wait.
Reproduce
band tendlc brand create --customer-profile-id <id> --brand-type PRIVATE_PROFIT ... --wait
# wait for the 202, then Ctrl-C
# → no receipt, no bandwidthId; the brand exists and is billed
Suggested fix
Wire signal.NotifyContext in Execute() and pass it through ExecuteContext, so cmd.Context() is cancelled on SIGINT/SIGTERM:
func Execute() error {
api.Version = version
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
return rootCmd.ExecuteContext(ctx)
}
Two things to get right beyond that:
internal/api/client.go:137 builds requests with http.NewRequest, not NewRequestWithContext. Without that change an in-flight HTTP call still won't abort — cancellation would only be observed between polls, not during one.
- A second Ctrl-C should hard-exit.
NotifyContext stops trapping after the first signal, which gives that for free, but it's worth an explicit test.
Interim state
AGENTS.md now documents this honestly — the receipt guarantee is scoped to paths the command itself takes, and the recovery for an interrupted create is band tendlc brand list --customer-profile-id <id>. That's a stopgap, not a fix.
Found by an adversarial review pass on #33.
What's wrong
bandinstalls no signal handler and runs cobra without a cancellable context:grep -rn "signal.Notify\|NotifyContext" cmd/ internal/returns nothing. Socmd.Context()iscontext.Background()everywhere, and Ctrl-C kills the process via the Go default — no cleanup, no output.Why it matters more than it looks
internal/cmdutil/poll.gogrew aPollConfig.Contextfield specifically so a cancelled poll could exit gracefully, andcmd/tendlc/async.go'sawaitTerminalhas a cancellation branch whose entire job is to emit the partial-result receipt. Neither can ever fire from a real SIGINT, because nothing upstream produces a cancellable context. The plumbing exists with no producer.For the 10DLC commands that matters concretely. Once
POST /brandsreturns its 202 the write may already have succeeded, andbandwidthIdis the only thing that cannot be recovered afterward — it's whyawaitTerminalowns stdout on every failure path. Press Ctrl-C duringband tendlc brand create --waitafter the 202 lands and the process dies with nothing on stdout. The brand exists, is billable, and the caller has no ID for it.Same shape on
number order --wait,call create --wait, andtranscription create --wait.Reproduce
Suggested fix
Wire
signal.NotifyContextinExecute()and pass it throughExecuteContext, socmd.Context()is cancelled on SIGINT/SIGTERM:Two things to get right beyond that:
internal/api/client.go:137builds requests withhttp.NewRequest, notNewRequestWithContext. Without that change an in-flight HTTP call still won't abort — cancellation would only be observed between polls, not during one.NotifyContextstops trapping after the first signal, which gives that for free, but it's worth an explicit test.Interim state
AGENTS.mdnow documents this honestly — the receipt guarantee is scoped to paths the command itself takes, and the recovery for an interrupted create isband tendlc brand list --customer-profile-id <id>. That's a stopgap, not a fix.Found by an adversarial review pass on #33.