feat(notify): needs-input trigger — edge-triggered, debounced push at the chat tailer's state edge (#99) #103
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab!103
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/99"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #99
What
When a run's conversational state transitions into
needs_inputorquestion, one Web Push notification is broadcast — the first real trigger plugged into #98's plumbing.internal/chat/notify.go+ the tailer's transition edge):chat.Options.Notify func(Notification)follows theAFKRunEndedfunc-field idiom — nil-safe (nil = trigger absent, zero behavior change), injected bycmd/labas a closure overpush.Sender.Broadcastsointernal/chatnever importsinternal/push. Broadcast is async, so the tick loop never blocks on gateway I/O.notifyGateon the injectedOptions.Nowclock arms on the edge and fires at the end of the first tick past the deadline — after that tick's read, so a revert observed on the deadline tick cancels instead of firing.needs_input↔questionis one episode (newest body wins, deadline unchanged); leaving the set disarms and re-arms the next episode; no periodic re-reminders.<repo>~<label> needs input(session name verbatim, no store lookup), body = dialog prompt → newest visible assistant text (thinking blocks and non-text kinds never reach a lock screen) →needs your input, truncated rune-safely to 150;tag= run ID (lock-screen coalescing); route/runs/<id>, always PWA-internal.CONTEXT.md: new needs-input trigger domain term + relationship bullet.Acceptance criteria → tests (
internal/chat/notify_test.go)TestNotifyGate_edgeFiresOnceAfterWindow,_nonNotifyNeverArms,TestTailer_notifiesOnceOnTransition,_nonNotifyTransitionSilent,_runEndsBeforeDebounceFiresNothingTestNotifyGate_flapFiresNothingTestNotifyGate_adoptionEdgeFires,TestTailer_notifiesOnceOnAdoptionTestNotifyGate_payloadFields,_bodyPrecedenceAndTruncation,_betweenNotifyStatesSameEpisodeTestTailer_nilNotifierIsAbsentBroadcast(async by #98's contract) at the call siteVerification
go test ./...all 35 packages ok;go test -race -count=1 ./internal/chat/ok;golangci-lint run0 issues;go build ./cmd/...ok; gofmt clean. Reviewed twice (line-by-line + independent adversarial pass); the review's findings — thinking-text leak into the body, a one-poll debounce blind spot, dead latch state — are all fixed in this diff.Found out of scope: pre-existing data race in
internal/push(#98 code, untouched here)go test -race ./internal/push/fails on main:Broadcastmarshals the payload once and hands the same[]byteto every dispatch goroutine (sender.go:103-113), and webpush-go v1.4.0 doesbytes.NewBuffer(message)then appends padding into the slice's spare capacity (webpush.go:~179-186) — withjson.Marshalroutinely returning cap > len, N subscriptions = N goroutines writing the same backing array. Fix is one line (s.dispatch(sub, bytes.Clone(body))), but the issue scopes push-package changes out and ticket filing was outside this run's permissions — please file it as a follow-up bug. Repro:go test -race -run TestBroadcastHappyPath ./internal/push/.🤖 Generated with Claude Code
https://claude.ai/code/session_01AMDvLm1axUjmT6VdMW19vY