Skip to content

Add thread environment status snapshots - #1017

Open
ymichael wants to merge 12 commits into
mainfrom
bb/restore-thread-status-snapshots-thr_emvgbsavwc
Open

Add thread environment status snapshots#1017
ymichael wants to merge 12 commits into
mainfrom
bb/restore-thread-status-snapshots-thr_emvgbsavwc

Conversation

@ymichael

@ymichael ymichael commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Resurrects #291 against current main (the original branch had drifted 530 commits behind and its migration chain was stale), then hardens the restored scheduler based on an adversarial review pass.

What this adds

  • Server-owned git and pull request status snapshots per environment, stored in two new tables (environment_git_status_snapshots, environment_pull_request_status_snapshots, migration 0085) and refreshed by the periodic sweep
  • environmentStatusSummary (git + PR signals) projected into thread responses and thread list entries; a new environment-status-summary-changed thread change kind pushes updates to clients
  • ThreadDetailView and the plugin-SDK sidebar hook (experimental_useSidebarThreadPullRequest) now read the pushed snapshot; all client-side PR polling (hook, query key, invalidations, refetch interval) is deleted
  • Host daemons durably watch workspaces of environments with live threads (watch-set targets derived from tracked environments)

Refresh model

  • Change-driven marks (floor 0): host-reported work-status-changed refreshes git status; git-refs-changed and lifecycle changes also refresh PR state. Local edits never re-poll GitHub.
  • Demand-driven marks (30s floor): thread-list reads and thread attachment only pull schedules earlier, never below refreshedAt + 30s — a list read of fresh data is a no-op, so push-notify → refetch → mark-due cannot loop.
  • Cadences: PR checks pending 5s · open PR 30s · no PR 5min · settled PR 1h · git backstop 5min. Only environments with at least one live thread are ever refreshed; writers preserve due-marks that arrive mid-RPC; the sweep refreshes hosts concurrently, serially per host.

Changes vs #291

  • DB migration regenerated as 0085 (main is at 0084); snapshot status columns enum-typed end to end
  • Adapted to the workspace.pull_request RPC result union (available/absent/unavailable)
  • Folders→sections rename, new thread fixtures, plugin SDK bundled-type/template regeneration
  • Kept the PR's sorted watch-set dedup, dropped main's equivalent unsorted fingerprint (one mechanism)
  • All scheduling fixes above (the restored code polled every visible environment ~5s and every environment forever)

Note: the built-in sidebar (ThreadRow) does not render these signals yet; consumers today are ThreadDetailView and plugin sidebars. No HOST_DAEMON_PROTOCOL_VERSION bump: wire shapes are unchanged, only which watch targets are included.

Validation

  • pnpm exec turbo run build typecheck lint test --force (full workspace, uncached)
  • pnpm exec turbo run test --filter=@bb/integration-tests
  • New regression coverage: demand-mark staleness floor (no refresh loop), per-change-kind due-mark split, snapshot refresh writing + thread notification

Closes #291.

🤖 Generated with Claude Code

ymichael and others added 8 commits June 24, 2026 19:27
Resolves 530-commit drift against main; the stale 0048 migration is
dropped here and regenerated as 0085 in a follow-up commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- workspace.pull_request RPC result is now a discriminated union
  (available/absent/unavailable); map absent to an available snapshot
  with no PR and unavailable into the retryable error path
- pass hub through ThreadRuntimeDisplayDeps in restored tests
- add environmentStatusSummary to thread fixtures added since the PR
- drop snapshot tables in migrate rewind tests that replay 0085

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@SawyerHood

SawyerHood commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

🚨 SLOP COP 🚨 · review

I am SlopCop. I am reviewing this pull request for security, code quality, performance, architecture, and end-to-end behavior.

The stack contains only #1017. I did not skip any pull requests.

I will post a comment after each phase.

ymichael and others added 2 commits August 4, 2026 15:45
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
environmentIds,
now,
});
markEnvironmentStatusSnapshotsDue(deps.db, {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚨 slopcop/review — Repeated thread-list reads force every snapshot due.

This list path sets nextRefreshAt to now for every visible environment.

The snapshot notification invalidates the same list query. An open sidebar can repeat workspace.status and gh pr view calls.

Keep the ensure call, but remove this unconditional dirty mark.

): EnvironmentGitStatusSnapshotRow[] {
return db
.select()
.from(environmentGitStatusSnapshots)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚨 slopcop/review — Snapshot rows remain in the refresh queue after their last live thread disappears.

These due queries select only by time. No code deletes rows when an environment becomes untracked.

Git and pull request RPCs can continue forever and can delay live environments.

Filter by live tracked threads, or delete untracked snapshot rows.

@SawyerHood

Copy link
Copy Markdown
Collaborator

🚨 SLOP COP 🚨 · review

The performance review found two high-impact issues.

Thread-list reads force all visible environments due and can create a refresh loop.

Snapshot rows remain due after their last live thread disappears.

I posted both findings inline.

@SawyerHood

Copy link
Copy Markdown
Collaborator

🚨 SLOP COP 🚨 · review

The security review found one medium denial-of-service risk.

An untrusted web page can call the unauthenticated thread list. Each call makes all visible snapshots due.

This issue has the same cause as the first performance finding.

I found no other security issues.

"idx": 85,
"version": "6",
"when": 1785882543282,
"tag": "0085_environment_status_snapshots",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚨 slopcop/review — The journal names a migration file that this branch does not contain.

Every migrate(db) call throws because 0085_environment_status_snapshots.sql is absent. The server cannot start.

Regenerate the migration and commit the SQL file.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚨 slopcop/review — Fixed in 4a0facc.

The current head now includes 0085_environment_status_snapshots.sql. I checked the pushed delta.

This finding no longer applies.

// depth cap.
export const threadResponseSchema = threadWithRuntimeSchema.extend({
canSpawnChild: z.boolean(),
environmentStatusSummary: threadEnvironmentStatusSummarySchema,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚨 slopcop/review — This required field breaks the plugin SDK test fixture.

makeThreadResponse spreads Partial<ThreadResponse> without a default for this field. The focused Turbo typecheck fails with TS2322.

Add a deterministic environmentStatusSummary before the overrides.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚨 slopcop/review — Fixed in 4a0facc.

The current head now gives makeThreadResponse a deterministic status summary. I checked the pushed delta.

This finding no longer applies.

"environment-status-summary-changed": {
flush: "debounced",
dirty: [
dirtyThreadListQueries, // Sidebar/list rows render environment git and PR signals.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚨 slopcop/review — The existing sidebar pull-request hook does not read this invalidated query.

useSidebarThreadPullRequest still reads useEnvironmentPullRequest. This pull request removes that query's five-second polling.

Pending checks and mergeability can stay stale until focus.

Read the list snapshot, or invalidate the environment pull-request query.

@SawyerHood

Copy link
Copy Markdown
Collaborator

🚨 SLOP COP 🚨 · review

The code-quality review found three direct failures or regressions.

The SQL migration is absent. The plugin SDK fixture does not compile. The sidebar pull-request hook can stay stale.

I posted all three findings inline.

The Git and pull-request storage paths also contain much duplicate code. A shared internal helper could reduce repetition without merging their tables.

@SawyerHood

SawyerHood commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

🚨 SLOP COP 🚨 · review

End-to-end update: the current head 4a0facc passes the startup and UI smoke tests.

The initial head failed because the SQL migration was absent. The author added that file, so I ran the test again.

The server health route returned ok. The thread route returned a valid empty list.

The browser loaded the bb home view with no console errors.

The isolated data had no threads, so I could not exercise a populated snapshot view.

@SawyerHood SawyerHood left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚨 SLOP COP 🚨 · review

Review result: three findings remain on current head 4a0facc.

  • Thread-list reads mark every visible snapshot due. This can create a refresh loop and unauthenticated resource exhaustion.
  • Snapshot rows remain queued after their last live thread disappears.
  • The sidebar pull-request hook can stay stale after this pull request removes polling.

The author fixed the missing SQL migration and SDK fixture during review. I replied to both resolved threads.

Current CI is green. The development server, health route, thread-list route, and browser smoke test passed.

The isolated data had no threads, so I could not test a populated snapshot view.

I did not approve or request changes.

ymichael and others added 2 commits August 5, 2026 09:23
…idebar PRs from snapshots

Review findings from the restored PR, verified adversarially:

- Demand marks (thread-list reads, thread attach) now carry a 30s staleness
  floor and only ever pull schedules earlier, killing the refresh -> notify ->
  list refetch -> mark-due loop that re-polled git and GitHub every sweep tick
- Due listings skip environments with no live thread, so abandoned
  environments stop polling gh forever; absent-PR cadence drops to 5min
- Local work-status changes no longer mark the PR snapshot due (only
  git-refs and lifecycle changes can alter a PR)
- Snapshot writers preserve due-marks that arrive while an RPC is in flight
- The due sweep groups refreshes by host and runs hosts concurrently
- useSidebarThreadPullRequest reads the pushed environmentStatusSummary from
  the thread list entry instead of the removed client-side polling query;
  the whole client PR query surface (hook, key, invalidations) is deleted
- Snapshot status columns are enum-typed end to end; the defensive
  normalizeSnapshotStatus re-parse is gone
- Restore main's 10s sweep interval; dedupe change-kind sets via
  THREAD_CHANGE_KINDS_AFFECTING_TRACKED_ENVIRONMENTS in @bb/db; drop dead
  singular mark-due helpers, pass-through wrappers, and the startup alias;
  optimistic thread inserts seed pending (not not_applicable) summaries

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… once

Adversarial verification of the previous commit found the in-flight-mark
preservation unreachable on the sweep path: a due row's stored schedule
never changed under the only-pull-earlier guard, so the writer's
expectedNextRefreshAt comparison always matched and the computed backstop
overwrote the mark. Change-driven (floor 0) marks now restamp the row at
the mark time, making the divergence observable; demand marks still leave
due rows alone. Due listings also exempt never-refreshed rows from the
live-thread gate so a thread archived before its environment's first
refresh cannot render a pending signal forever. Package-level db tests
cover the floor, the in-flight race, and the gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants