-
-
Notifications
You must be signed in to change notification settings - Fork 57
Add a workspace git runtime state manager for stable hydrated workspace summaries #212
Description
Problem
Workspace hydration currently returns baseline descriptors while live git-derived fields are computed later and emitted opportunistically. That creates UI instability:
fetch_workspacescan returndiffStat: nulleven after the daemon has already computed a real git summary before.- reconnect / app resume / sidebar refresh can overwrite enriched workspace state with baseline values.
- volatile git/runtime state is scattered across
Sessionmethods instead of having a single daemon-owned home.
We just landed a narrow client-side fix to stop diffStat flashing. This issue tracks the real server-side reshape.
Goal
Split workspace data into:
- Stable workspace/project identity data
- persisted registry now, SQLite-friendly later
- Volatile git runtime state
- in-memory, daemon-owned, continuously refreshed, joined into workspace descriptors on fetch and update
Non-Goals
- Do not move workspace/project identity into an in-memory snapshot manager.
- Do not change websocket schemas in a breaking way.
- Do not fold file-level checkout diff subscriptions into the workspace summary cache.
Stable Data That Should Stay In Registry / Reconciliation
These fields affect identity, grouping, or archival and should remain part of stable workspace/project storage:
workspaceIdprojectIdcwdkindarchivedAtprojectDisplayNameprojectRootPathprojectKind- remote-derived project regrouping (
remoteUrl -> projectKey)
Relevant code today:
packages/server/src/server/session.tspackages/server/src/server/workspace-registry.tspackages/server/src/server/workspace-registry-model.ts
Volatile Data That Should Move Into A Workspace Git State Manager
This is the live per-workspace git/runtime summary state that changes frequently and should be cached separately from stable workspace identity:
export type WorkspaceGitRuntimeState = {
workspaceId: string;
status: "unknown" | "loading" | "ready" | "error" | "not_git";
fetchedAt?: string;
error?: string | null;
currentBranch: string | null;
displayNameOverride: string | null;
diffStat: { additions: number; deletions: number } | null;
isDirty: boolean | null;
baseRef: string | null;
aheadBehind: { ahead: number; behind: number } | null;
aheadOfOrigin: number | null;
behindOfOrigin: number | null;
hasRemote: boolean;
remoteUrl: string | null;
repoRoot: string | null;
mainRepoRoot: string | null;
isPaseoOwnedWorktree: boolean;
pr: {
status: {
url: string;
title: string;
state: string;
baseRefName: string;
headRefName: string;
isMerged: boolean;
} | null;
githubFeaturesEnabled: boolean;
fetchedAt?: string;
error?: string | null;
} | null;
};Notes:
diffStat: nullmeans unknown / unavailable.{ additions: 0, deletions: 0 }means known clean state.- PR state remains summary-only here. Full PR actions stay elsewhere.
Proposed Manager Boundary
Add a daemon-owned WorkspaceGitStateManager under packages/server/src/server/.
Responsibilities
- own the in-memory cache of
WorkspaceGitRuntimeStatekeyed byworkspaceId - warm a missing entry on first read
- refresh entries in the background
- keep last-known-good values while refresh is in flight
- expose change events for subscribers
- own git-watch debounce / fingerprint dedupe / background fetch hooks
- provide joined runtime state to workspace descriptor shaping
Explicit Non-Responsibilities
- do not own persisted workspace/project identity
- do not archive or regroup workspaces/projects
- do not own file-level checkout diff subscriptions
- do not own git write actions (commit / merge / push / create PR)
Proposed Interface
type WorkspaceGitStateChangeListener = (
workspaceId: string,
state: WorkspaceGitRuntimeState,
) => void;
export class WorkspaceGitStateManager {
getState(workspaceId: string): WorkspaceGitRuntimeState | null;
getStates(workspaceIds: Iterable<string>): Map<string, WorkspaceGitRuntimeState>;
ensureWorkspace(input: {
workspaceId: string;
cwd: string;
isGit: boolean;
}): WorkspaceGitRuntimeState | null;
refreshWorkspace(input: {
workspaceId: string;
cwd: string;
reason:
| "bootstrap"
| "watch"
| "background_fetch"
| "explicit_refresh"
| "reconcile";
}): Promise<void>;
removeWorkspace(workspaceId: string): void;
on(event: "change", listener: WorkspaceGitStateChangeListener): this;
off(event: "change", listener: WorkspaceGitStateChangeListener): this;
dispose(): void;
}Optional follow-up if useful:
getOrCreateLoadingState(input: { workspaceId: string; isGit: boolean }): WorkspaceGitRuntimeStateDescriptor Join Seam
Workspace descriptors should become a pure join of:
- stable workspace record
- stable project record
- agent-derived status/activity
- git runtime state (if present)
Pseudo-shape:
function buildWorkspaceDescriptor(input: {
workspace: PersistedWorkspaceRecord;
project: PersistedProjectRecord | null;
activity: WorkspaceActivitySummary;
git: WorkspaceGitRuntimeState | null;
}): WorkspaceDescriptorPayloadRules:
- baseline workspace/project fields always come from stable data
nameusesgit.displayNameOverride ?? workspace.displayNamediffStatusesgit.diffStat- later, if we add more workspace-level git summary UI, it should come from
git
Event / Refresh Sources That Should Feed The Manager
1. First fetch / hydration
fetch_workspaces should:
- read stable workspace/project records
- ask manager for cached git state for those workspaces
- return joined descriptors immediately
- call
ensureWorkspace(...)/ schedule background refresh for git workspaces missing runtime state
Result: second client hydration sees the cached git state instead of baseline nulls.
2. Workspace reconciliation
When reconcileWorkspaceRecord(...) discovers or updates a workspace:
- keep stable identity logic where it is
- call
ensureWorkspace(...)orremoveWorkspace(...)on the manager as needed - if the workspace is git, schedule runtime refresh
3. Git watch events
Move or delegate the current workspace git watch machinery into the manager:
- watch target lifecycle
- debounce
- fingerprint dedupe on branch + diff summary
- refresh scheduling
4. Background git fetch
The current BackgroundGitFetchManager can stay separate, but its subscriptions should be owned by the git state manager rather than Session.
Flow:
- manager subscribes a git workspace/repo root
- fetch completes
- manager refreshes affected workspace git runtime state
- manager emits
change
5. PR status polling / refresh
Workspace-level PR summary should also be cached in the manager instead of fetched ad hoc only from the client query path.
Potential first step:
- keep current RPC for checkout PR status
- let manager optionally refresh/store PR summary on a slower cadence
- join cached PR summary later when we have a workspace-level consumer that needs it
What Should Stay Separate
Checkout diff manager
Keep file-level diff subscriptions separate:
- keyed by
(cwd, compare) - heavier payloads
- different lifecycle from summary git state
Git write operations
Keep these outside the manager:
- commit
- merge
- merge-from-base
- push
- create PR
- branch validation / branch suggestions
Those operations can notify / invalidate the manager after completion.
Session Changes
Session should get simpler.
Session should keep
- request orchestration
- subscription state for clients
- reconciliation orchestration
- emitting websocket messages
Session should stop owning directly
- git watch targets
- git fetch subscriptions
- descriptor fingerprint dedupe for git summary changes
- ad hoc git-enriched descriptor computation during emission
Suggested Implementation Steps
- Introduce
WorkspaceGitRuntimeStatetype andWorkspaceGitStateManager. - Move workspace git watch target management out of
Sessioninto the manager. - Move background fetch subscription ownership into the manager.
- Add a joined descriptor builder that accepts stable + runtime state.
- Change
fetch_workspacesto read manager state instead of computing git summary inline or throwing it away. - Change workspace update emission to use the joined descriptor builder.
- Invalidate / refresh manager state on workspace reconcile, open-project, worktree create/archive, and relevant git write operations.
- Add PR summary caching only if we want workspace-level consumers now; otherwise keep it as a second phase.
Verification Plan
Server tests
- first
fetch_workspacesreturns baseline values when runtime state is missing - later manager refresh emits
workspace_updatewith enriched git summary - second
fetch_workspacesreturns cached enriched summary, not baseline nulls - git watch refresh updates branch / diffStat without duplicate emissions
- background fetch invalidates and refreshes runtime state
- reconcile/archive/remove cleans up runtime state and watchers
App verification
- workspace list does not flash diff stats on reconnect / resume / refresh
- second client hydration receives enriched values immediately once manager is warm
Safety checks
- no websocket schema breakage
- typecheck passes
- targeted workspace + git-watch tests pass
Notes
This should be built as a reshape, not as more special-case logic in Session.
The bug-fix PR/hotfix should stay narrow. This issue tracks the follow-up architecture work.