Skip to content

fix: don't pin dashboard to a non-orchestrator session#12

Merged
tlongwell-block merged 2 commits into
aaif-goose:mainfrom
jgnagy:fix/dashboard-parent-session-resolution
Jun 18, 2026
Merged

fix: don't pin dashboard to a non-orchestrator session#12
tlongwell-block merged 2 commits into
aaif-goose:mainfrom
jgnagy:fix/dashboard-parent-session-resolution

Conversation

@jgnagy

@jgnagy jgnagy commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

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:

  • The wall (messages) is read from the flat GOOSE_GTWALL_FILE — works regardless of session resolution.
  • The delegate registry is built by scanning the parent (orchestrator) session's messages for 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:

  1. dashboardresolve_session() trusted AGENT_SESSION_ID unconditionally and passed it as --session, which makes goosetown-ui skip 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, a session_type='terminal' session with zero "started in background" messages, while the real orchestrator was 20260601_3.)
  2. scripts/goosetown-uisessions_tree_watcher() reads parent_id once and never re-resolves, so any wrong/early pin is permanent.
  3. Multi-instance: every goosetown on the machine shares one 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 trust AGENT_SESSION_ID when it actually has delegate activity; otherwise discover a delegate-bearing session. Adds session_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)

  • The wall is the per-instance anchor: an instance's wall lists its delegates' gtwall IDs (senders), and the orchestrator's spawn messages declare those same IDs (already parsed by build_sender_session_map). Matching them uniquely identifies this instance's orchestrator.
  • goosetown-ui: adds scoped_parent_session(cur, senders) (most sender-overlap wins, ties → most recent, else None). 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-ui is authoritative.

Verification

Real sessions.db that exhibited the bug:

resolve_session  AGENT_SESSION_ID=20260525_4 (terminal), WALL_ID set/unset  -> 20260601_3
self-heal: pinned terminal session (0 children) -> re-resolves to orchestrator
no-thrash: a correct parent with children is left untouched

Synthetic two-instance sessions.db (orchestrator A → researcher-alpha; orchestrator B → researcher-beta, more recently updated):

scoped(A wall senders)              -> orchestrator A   (not B, despite B newer)
scoped(B wall senders)              -> orchestrator B
find_parent_session(A wall file)    -> orchestrator A
find_parent_session(B wall file)    -> orchestrator B
scoped(no delegate senders)         -> None             (never a cross-instance guess)
bash resolve_session WALL_ID=BBB, AGENT_SESSION_ID=A_orch -> B   (wall overrides env)
  • bash -n dashboard and python3 -m py_compile scripts/goosetown-ui pass.
  • tests/test_dashboard.sh: 4 passed, 0 failed (the pre-existing tmpfile: unbound variable teardown 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

jgnagy and others added 2 commits June 1, 2026 13:53
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>
@zazer0

zazer0 commented Jun 18, 2026

Copy link
Copy Markdown

Hi, is this project abandoned? (just confirming before I spend time contributing)

@tlongwell-block
@maxamillion
@balcsida

@balcsida

Copy link
Copy Markdown
Contributor

@zazer0 I'm an outside contributor

@tlongwell-block

Copy link
Copy Markdown
Collaborator

Hi, @zazer0 👋

The goose team was working on getting some of the nice idea in goosetown directly into goose, like the nest and better subagent communication. cc @DOsinga here

Let me take a look at this PR in a bit

@tlongwell-block tlongwell-block 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.

lgtm. Thanks @jgnagy 👍

@tlongwell-block tlongwell-block merged commit d1f62b0 into aaif-goose:main Jun 18, 2026
1 check passed
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.

4 participants