fix: don't pin dashboard to a non-orchestrator session#12
Merged
tlongwell-block merged 2 commits intoJun 18, 2026
Merged
Conversation
The dashboard discovers delegates by scanning the *parent* (orchestrator) session's messages for "Task <id> started in background" and never re-resolves that parent once pinned. Two paths could pin the wrong session, leaving the UI stuck on "Spawning delegates..." with 0 delegates forever — even while delegate messages stream onto the wall (the wall is keyed off GOOSE_GTWALL_FILE, so it keeps working independently): 1. dashboard `resolve_session()` trusted `AGENT_SESSION_ID` unconditionally and passed it as `--session`, bypassing goosetown-ui's own discovery. goose exports a *terminal* session id into shell subprocesses, so launching the dashboard from a goose shell pinned that terminal session — which has no delegates. 2. `sessions_tree_watcher()` reads `parent_id` once and never re-resolves, so any wrong/early pin is permanent. Fix: - resolve_session(): only trust AGENT_SESSION_ID when it actually has delegate activity; otherwise prefer SQL discovery of a delegate-bearing session, and fall back to AGENT_SESSION_ID / most-recent user session only as a last resort. Adds a `session_has_delegates()` helper. - sessions_tree_watcher(): when the pinned parent yields 0 children, re-resolve via find_parent_session() and adopt a candidate only if it actually has children — self-healing without thrashing between empty sessions. Verified against a real sessions.db that exhibited the bug: with AGENT_SESSION_ID set to a terminal session, resolve_session now returns the orchestrator session, and the watcher re-resolves a wrongly-pinned parent to the orchestrator (6 delegates) while leaving a correct parent untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All goosetowns on a machine share one sessions.db, and sessions carry no
instance/wall identifier. The previous resolution ("most recent delegate-bearing
user session") and the self-heal added in the prior commit would, with multiple
concurrent instances, resolve or re-pin to *another* instance's orchestrator —
e.g. instance A's dashboard adopting instance B's delegates whenever A's
orchestrator momentarily had 0 children.
Use the wall as the per-instance anchor. An instance's wall lists its delegates'
gtwall IDs (senders), and the orchestrator's delegate-spawn messages declare those
same IDs (already parsed by build_sender_session_map). Matching the two uniquely
identifies the orchestrator for this instance:
- goosetown-ui: add scoped_parent_session(cur, senders) — pick the candidate whose
spawned delegates overlap most with this wall's senders (ties → most recent),
else None. find_parent_session() now prefers it; main() lets a positive
wall-scoped match override an explicit --session (the launcher may pass a
terminal/other-instance id); the self-heal re-resolves on 0 children OR when the
current parent's delegates don't match the wall's senders (wrong instance), using
only the scoped lookup — so it can adopt this instance's orchestrator and never
another's.
- dashboard: resolve_session() prefers a delegate-bearing user session that
references this wall (WALL_ID, already sanitized to [A-Za-z0-9_-]) before the
generic most-recent query. Best-effort hint; goosetown-ui is authoritative.
Verified with a synthetic two-instance sessions.db: wall-A senders resolve to
orchestrator A and wall-B senders to orchestrator B even though B is more recently
updated; a wall with no delegate senders resolves to None (never a cross-instance
guess); and bash resolve_session honors WALL_ID over a conflicting AGENT_SESSION_ID.
Single-instance behavior and tests/test_dashboard.sh (4/4) unchanged.
Known limitation: if two concurrent instances spawn delegates with identical gtwall
IDs, the most-overlap-then-most-recent tiebreak may still pick the wrong one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Hi, is this project abandoned? (just confirming before I spend time contributing) |
Contributor
|
@zazer0 I'm an outside contributor |
Collaborator
tlongwell-block
approved these changes
Jun 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The dashboard's left Registry stays stuck on "Spawning delegates…" showing 0 delegates, even though delegate messages stream onto the wall and delegate sub-agent sessions clearly exist in
sessions.db.The registry and the wall are two independent data sources:
GOOSE_GTWALL_FILE— works regardless of session resolution.Task <id> started in background(CHILD_PATTERN). If the dashboard pins the wrong parent session,find_children()matches nothing → 0 delegates forever.Two bugs, plus a latent multi-instance hazard:
dashboard→resolve_session()trustedAGENT_SESSION_IDunconditionally and passed it as--session, which makesgoosetown-uiskip its own discovery. goose exports a terminal session id into shell subprocesses, so launching the dashboard from a goose shell pins that terminal session — which has no delegates. (Observed: dashboard launched with--session 20260525_4, asession_type='terminal'session with zero "started in background" messages, while the real orchestrator was20260601_3.)scripts/goosetown-ui→sessions_tree_watcher()readsparent_idonce and never re-resolves, so any wrong/early pin is permanent.sessions.db, and sessions carry no instance/wall identifier. "Most recent delegate-bearing user session" picks whichever orchestrator was touched last — so with concurrent instances, dashboard A could resolve (or self-heal) onto orchestrator B.Fix
Commit 1 — don't pin a non-orchestrator session
resolve_session(): only trustAGENT_SESSION_IDwhen it actually has delegate activity; otherwise discover a delegate-bearing session. Addssession_has_delegates().sessions_tree_watcher(): re-resolve when the pinned parent has 0 children, adopting a candidate only if it actually has children (no thrash).Commit 2 — scope resolution to the instance's wall (multi-instance safe)
build_sender_session_map). Matching them uniquely identifies this instance's orchestrator.goosetown-ui: addsscoped_parent_session(cur, senders)(most sender-overlap wins, ties → most recent, elseNone).find_parent_session()prefers it;main()lets a positive wall-scoped match override an explicit--session; the self-heal re-resolves on 0 children or when the current parent's delegates don't match the wall's senders (wrong instance), using only the scoped lookup — so it can adopt this instance's orchestrator and never another's.dashboard:resolve_session()prefers a delegate-bearing user session that references this wall (WALL_ID, already sanitized to[A-Za-z0-9_-]) before the generic query. Best-effort hint;goosetown-uiis authoritative.Verification
Real
sessions.dbthat exhibited the bug:Synthetic two-instance
sessions.db(orchestrator A →researcher-alpha; orchestrator B →researcher-beta, more recently updated):bash -n dashboardandpython3 -m py_compile scripts/goosetown-uipass.tests/test_dashboard.sh: 4 passed, 0 failed (the pre-existingtmpfile: unbound variableteardown wart is unrelated).Known limitation
If two concurrent instances spawn delegates with identical gtwall IDs, the most-overlap-then-most-recent tiebreak may still pick the wrong one. Walls/ports/screens remain fully isolated per instance regardless.
🤖 Generated with Claude Code