Skip to content

Harden Google self-heal follow-ups: atomic session saves, fast cookie-save retry, paced-repair observability#151

Merged
MaxGhenis merged 1 commit into
mainfrom
harden/google-selfheal-followups
Jul 22, 2026
Merged

Harden Google self-heal follow-ups: atomic session saves, fast cookie-save retry, paced-repair observability#151
MaxGhenis merged 1 commit into
mainfrom
harden/google-selfheal-followups

Conversation

@MaxGhenis

Copy link
Copy Markdown
Owner

Non-blocking polish from PR #148's cross-family review (sol findings). #148 made maybePersistRotatedCookies rewrite session.json every ~5 minutes, which turned two latent nits into real exposure; the third item makes the fast-revocation detector visible in production.

1. Atomic SaveSession (internal/client/session.go)

SaveSession did a direct os.WriteFile of session.json — a crash mid-write could truncate the file holding the paired Google/WhatsApp/Signal credentials, and losing it costs a manual re-pair. It now writes a unique same-dir temp file (0600), fsyncs, and renames over the target, mirroring googlecookies.UpdateSessionCookies (plus fsync, so a power loss can't commit the rename ahead of the contents). Dir 0700 / file 0600 perms preserved; a unique temp name (not a fixed .tmp) keeps concurrent savers from installing each other's partial writes.

Test: concurrent writers + torn-read detector, verified to FAIL against the old os.WriteFile implementation (unmarshal: unexpected end of JSON input); plus a failed-save-keeps-previous-session test asserting no debris and intact prior contents.

2. Failed cookie-save retry (internal/client/events.go)

The throttle timestamp advanced before the save attempt, so a transient SaveSession failure wasn't retried for a full ~5-min interval. Resolution (the "OR" branch from the review, made explicit): the throttle still advances up front — these events arrive per message, so an unthrottled failure path would rewrite the session on each one — but a failed save pulls the next attempt in to interval/10 (~30s), and lastCookieHash stays untouched on failure so the retry still sees the pending rotation. The deterministic marshal-failure path deliberately keeps the full interval (commented).

Test: blocked-session-path harness proving one attempt per window, retry at interval/10 (not per event, not a full interval), and that the eventual successful save persists the rotation the failures skipped — verified to FAIL against the old advance-before-save semantics.

3. Paced-repair observability (cmd/google_supervisor.go → status surfaces)

pacedCount — the "cookies are being revoked within minutes" detector from the durability adjudication (theory A) — was test-only. Now:

  • Log: each delayed repair emits a warn with wait, since_last_repair, min_interval, paced_total.
  • /api/status: google.repairs_paced (omitempty — absent in the healthy 0 case). Wired via app.SetGoogleRepairPaceCounter; only the transport-owning daemon builds a repairer, so client/demo modes report 0. MCP client mode inherits it for free through RawStatus daemon-truth passthrough.
  • MCP get_status: Repairs paced: N line, only when non-zero.
  • Runbook: "Is revocation running fast?" section with the jq one-liner and the healthy-is-zero/climbing-means-churn interpretation, next to the heal-cycle description.

Testing

🤖 Generated with Claude Code

…-save retry, paced-repair observability (#148 review)

Non-blocking polish from PR #148's cross-family review (sol). #148 made
maybePersistRotatedCookies rewrite session.json every ~5 minutes, which
turned two latent nits into real exposure:

- internal/client/session.go: SaveSession now writes a same-dir 0600 temp
  file, fsyncs, and renames over session.json instead of os.WriteFile in
  place. A crash mid-save can no longer truncate the only copy of the
  paired Google/WhatsApp/Signal credentials (which costs a manual
  re-pair). Mirrors googlecookies.UpdateSessionCookies, plus fsync.
  Regression-tested with a concurrent writers+reader test that fails
  against the old implementation (torn JSON read).

- internal/client/events.go: a failed rotated-cookie save now retries
  after interval/10 (~30s) instead of silently waiting the full ~5-min
  throttle, still bounded so a persistent failure can't write per event.
  lastCookieHash stays untouched on failure (now asserted), so the retry
  still sees the pending rotation. Test proven against the old
  advance-before-save semantics.

- Paced-repair observability: the fast-cookie-revocation signal from the
  durability adjudication (theory A) was test-only. A delayed repair now
  logs a warn with wait/since_last_repair/paced_total, and the count is
  published as google.repairs_paced in /api/status and MCP get_status
  (omitted when 0; client/demo modes report 0 — only the
  transport-owning daemon builds a repairer). Runbook documents the
  check and the healthy-is-zero expectation.

All touched packages pass with -race.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@MaxGhenis
MaxGhenis merged commit d7fc1d4 into main Jul 22, 2026
5 checks passed
@MaxGhenis
MaxGhenis deleted the harden/google-selfheal-followups branch July 22, 2026 16:16
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.

1 participant