Skip to content

A boot that cannot reach GitHub arms a Discord flood: an empty first poll cannot be told from a failed oneΒ #1623

Description

@suleimansh

πŸ€– agent issue

Found while grounding the spec-vs-code list on #1613. That list names this one, but locates it in the wrong place β€” fixing where it points would change nothing.

keyed-watcher.ts is correct. Its own doc states the rule and its code keeps it:

a failed project scan or projection just yields no new items that cycle, and never counts as a poll: the baseline must come from a real read, or a failed first poll would make the next good one announce everything pre-existing as new

try {
  items = await opts.build(await opts.projects())
} catch {
  return            // <- returns before observe(), so warmedUp stays false
}

The problem is that neither call can throw. Both of the watcher's inputs fail soft, one layer up:

  • listSummaries (daemon-services.ts:130) β€” await listProjects(undefined, env).catch(() => [])
  • buildInterventions (interventions.ts:89) β€” await prs(project.path).catch(() => []), per project

So on a boot where the registry cannot be read, or gh is not yet authenticated, or GitHub is unreachable, the first poll does not fail. It succeeds, empty. SeenTracker.observe([]) then flips warmedUp against an empty baseline, and the first poll that does reach GitHub reports every already-open PR as new.

Both Discord watchers are wired this way β€” the "needs you" queue and the New activity feed β€” so the failure is one message per pre-existing item, at once, to the user's Discord.

Why it stays invisible

Every layer behaves exactly as designed. The watcher refuses to warm up on a failure it never sees; the builders refuse to let one unreachable project sink the whole poll; the registry read refuses to make a missing file fatal. The bug lives in the seam: an empty read and a failed read arrive as the same value, and only the watcher needs to tell them apart.

What a fix has to decide

The watcher cannot distinguish them from T[] alone, so one of:

  • The builders report failure. build returns items and whether the read was whole (or throws, which the watcher already handles). Most honest, and the information exists at the catch that currently discards it.
  • An empty first poll does not warm up. One line in SeenTracker, and it costs the case it is wrong for: a genuinely empty first poll on a quiet machine stays un-warmed until the first item appears, which then announces as new. That is a notification nobody asked for β€” the milder failure of the two, but still a wrong one.

The first is the real fix. The second is what you would ship today if the first were too large.

Worth stating in the same pass: this is the second seam this week where a value that means "we could not read" was flattened into a value that means "there is nothing" (#1619 flattened "no model given" into "no model window in force"). Both were invisible for the same reason β€” the flattening happens at a catch that reads as defensive.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions