Skip to content

fix(addie): run wg-slack-context hourly with daily dedup so business-hours gate cannot starve it#5831

Merged
bokelley merged 1 commit into
mainfrom
wg-slack-context
Jul 7, 2026
Merged

fix(addie): run wg-slack-context hourly with daily dedup so business-hours gate cannot starve it#5831
bokelley merged 1 commit into
mainfrom
wg-slack-context

Conversation

@bokelley

@bokelley bokelley commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #5811, found while verifying the first prod run after the GITHUB_TOKEN upgrade. The scheduler's business-hours gate silently skips ticks (scheduler.ts runJob), and setInterval anchors tick time to whenever the app booted. A 24h-interval job registered with a 9–17 ET window therefore never runs if the machines boot outside the window — which is exactly what happened: tonight's secret-set restart anchored the daily tick at ~23:45 ET, permanently outside the gate. (Existing 24h+window jobs like profile-completion-nudge survive this only because frequent deploys keep re-anchoring their ticks during working hours.)

Fix

  • Interval 24h → 1h (the shape wg-digest uses: hourly tick, internal cadence logic).
  • The job dedups itself to one refresh per day: if a digest with today's Generated: date already exists on the PR branch or on main, the tick is a no-op (skipped: 'already-ran-today') before any Slack or LLM call.
  • Registration comment documents the trap so the next 24h+window job doesn't reintroduce it.

Test plan

  • 2 new unit tests: same-day dedup short-circuits before Slack/LLM calls; prior-day digest proceeds to a refresh.
  • All 16 tests in the two suites pass; tsc --noEmit clean; precommit on commit.
  • Prod: first digest PR appears on addie/wg-slack-context during today's 9–17 ET window after this deploys.

No changeset — Addie server work.

🤖 Generated with Claude Code

…ess-hours gate cannot starve it

The scheduler's business-hours gate silently skips ticks and
setInterval anchors tick time to boot time, so a 24h-interval job that
boots outside 9-17 ET never runs (tonight's secret-set restart anchored
it at 23:45 ET). Hourly interval + one-refresh-per-day dedup keyed on
the Generated date already stamped in the digest file, checked on both
the PR branch and main. Same shape wg-digest uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bokelley bokelley force-pushed the wg-slack-context branch from ef60014 to 5757882 Compare July 7, 2026 03:38

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct fix for a real starvation bug. Hourly tick + daily dedup is the right shape: it decouples the run cadence from setInterval's boot-anchored tick so the silent business-hours gate can no longer permanently skip a 24h job.

The root-cause story checks out — code-reviewer confirmed against scheduler.ts that runJob skips out-of-window ticks without rescheduling and that the interval anchors to boot time, so a 24h job booted at 23:45 ET never re-enters the 9–17 window. wg-digest's hourly-tick-with-internal-cadence pattern is the established remedy; this follows it.

Things I checked

  • Dedup is race-safe against the real cases. wg-slack-context.ts:419-431 reads today's Generated: date from PR_BRANCH then main; first in-window tick writes it, every later tick that day short-circuits before any Slack or LLM call. No same-day double-run: the dedup date (L419, UTC) and the written date (L387, UTC) are consistent, and the 9–17 ET window never straddles UTC midnight in EST or EDT, so the UTC calendar date is stable across the whole run window.
  • No cross-day starvation of an unmerged PR. Yesterday's open PR carries yesterday's date on PR_BRANCH, so today's tick proceeds, force-resets the branch to base head (github-pr.ts:112-131), re-commits with today's date, and reuses the open PR via the list-then-create path (github-pr.ts:162-173). Daily refresh survives an open PR.
  • Fail-closed on privacy. wg-slack-context.ts:447-451 sweeps a channel only when getChannelInfo returns is_private === false; null/unknown/private are all excluded. security-reviewer confirmed getChannelInfo returns null on any API error, so an unresolvable channel is never swept. No private-channel leak into the public repo.
  • Injection containment holds. Untrusted Slack text → LLM → .agents/wg/slack-context.md never reaches an approving desk without a human. The digest PR trips the .agents/* sensitive-path gate; security-reviewer verified Argus posts a comment and does not run the LLM review on the digest, and desks read it only from the merged main checkout. cleanSlackText (L310) and stripResidualMentions (L324) are secondary hygiene — the load-bearing control is the human-merge gate.
  • Credentials. GITHUB_TOKEN/ANTHROPIC_API_KEY are env-only, token sent as Bearer and never logged; ref is encodeURIComponent-wrapped, path is the hardcoded CONTEXT_FILE_PATH, host is always api.github.com. No SSRF, no secret in the error-body logs.
  • Test-plan honesty. The two new unit tests are behavior tests, not implementation tests: the same-day case asserts the short-circuit fires before getChannelHistory/messagesCreate (wg-slack-context-job.test.ts:754-756), the prior-day case asserts it proceeds to a PR. The unchecked prod checkbox ("first digest PR appears during today's window") is inherently post-deploy — it can't be validated pre-merge, so leaving it unchecked is honest, not a skipped verification. No changeset needed: Addie server, not published wire.

Follow-ups (non-blocking — file as issues)

  • Pin CONTEXT_FILE_PATH under .agents/. security-reviewer Low 1: the whole injection-containment guarantee rests on the digest file matching the .agents/* sensitive-path glob in ai-review.yml. A future refactor that moved it (e.g. to docs/) would silently let Argus auto-review Slack-derived content. Add a unit assertion that CONTEXT_FILE_PATH.startsWith('.agents/') so the invariant fails loudly, not silently.
  • no-material-change compares against main only (wg-slack-context.ts:507), not PR_BRANCH. Narrow edge: content equal to main but an open PR carrying a stale body skips and leaves that PR open. The daily force-reset normally supersedes it; a one-line comment on the asymmetry would help.

Minor nits (non-blocking)

  1. Silent per-ref catch in the dedup loop. wg-slack-context.ts:427-430 swallows all read errors and falls through to a full Slack+LLM run. On a transient GitHub 5xx during the window that means a full sweep every hour until GitHub recovers, invisibly. A logger.debug in the catch would make starving-vs-thrashing distinguishable in prod.
  2. Scheduler re-entrancy (not this PR). job.executing is set but never checked in runJob, so the 14-minute initialDelay setTimeout and the hourly interval can fire runJob concurrently. The daily dedup makes a concurrent second run mostly a no-op and the PR-list reuse prevents a duplicate PR, so worst case is one redundant sweep. Flag for the scheduler owner.

Interesting choice to have the job re-anchor via daily dedup rather than fix setInterval's boot anchoring at the scheduler — but given wg-digest already lives with the same trap, matching the established pattern is the smaller change that closes the loop.

Approving on the strength of both expert verdicts (code-reviewer: sound-with-caveats; security-reviewer: ship, no High/Medium) plus the fail-closed injection and privacy design. Follow-ups noted.

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean fix for a real starvation bug. Right shape: hourly tick with internal cadence logic is the pattern wg-digest already uses, and it defeats the boot-time setInterval anchoring that the 24h+window registration couldn't survive.

Things I checked

  • Premise holds. getFileContent (github-pr.ts:63-82) returns null on 404 and throws only on non-404 non-ok — so the try/catch at wg-slack-context.ts:172-186 correctly falls through on a genuine outage, and the existing?.includes(...) guard is null-safe on a missing file. Fail-open here is acceptable: the no-material-change gate at L243-246 still prevents duplicate PR content.
  • Gate ordering. Dedup guard sits after the missing-env check (L163-167), so it only fires GitHub reads once GITHUB_TOKEN is confirmed present. Correct.
  • UTC-date vs the window. Generated: uses toISOString().slice(0,10) (UTC). The UTC date rolls at ~19:00-20:00 ET, after the 9-17 ET window closes — so the date is constant across a single business-hours window and the guard can't split a business day. Right call.
  • No runaway PRs. Hourly ticks reuse the open PR / force-reset the branch (github-pr.ts), so the interval bump doesn't spawn duplicate PRs.
  • Tests: same-day dedup short-circuits before getChannelHistory/messages.create; prior-day digest proceeds to upsertFilePr. already-ran-today added to the skipped union (L44). 16/16 pass per the description.
  • code-reviewer: sound-with-caveats, no blockers.

Follow-ups (non-blocking — file as issues)

  • Quiet-day LLM/Slack re-run. The guard's comment claims it dedups "before any Slack or LLM call," but that guarantee only holds on days that produce a PR. Generated: is written solely inside composeFile, which runs only when a PR commits. On a genuinely-quiet day the body equals main's and the job returns no-material-change (L243-246) without persisting today's date — so every subsequent hourly tick re-runs the full Slack sweep + Anthropic messages.create (~8×/quiet day). Not runaway PRs, but runaway cost. Persist a per-day marker independent of content change, or document the quiet-day cost. (code-reviewer flagged this; verified against L234-246.)
  • Add a test for the guard × no-material-change interaction — the untested path is the one most likely to bite.

Minor nits (non-blocking)

  1. Log the swallowed error. The catch {} at wg-slack-context.ts:181 is safe by design, but a logger.debug inside it would surface a real GitHub auth/outage that silently forces a re-run.

Test plan

The unchecked box — first prod digest PR during today's 9-17 ET window — is inherently post-deploy and can't be verified pre-merge; the unit tests cover the code path this PR changes. Follow-up, not a hold.

No changeset needed: Addie server work, no static/schemas/source/** or wire surface touched.

LGTM. Follow-ups noted below.

@bokelley bokelley merged commit afe3854 into main Jul 7, 2026
14 of 15 checks passed
@bokelley bokelley deleted the wg-slack-context branch July 7, 2026 03:48
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