Skip to content

feat(dashboard): version /api/state and expose leadSessionId for external consumers - #33

Closed
Agony1023 wants to merge 4 commits into
hueyexe:mainfrom
Agony1023:feat/dashboard-state-version-clean
Closed

feat(dashboard): version /api/state and expose leadSessionId for external consumers#33
Agony1023 wants to merge 4 commits into
hueyexe:mainfrom
Agony1023:feat/dashboard-state-version-clean

Conversation

@Agony1023

Copy link
Copy Markdown
Contributor

Summary

Types and versions the /api/state dashboard endpoint's response shape, and exposes each team's lead_session_id (already stored in the DB, never selected before). Both are additive-only changes motivated by an external consumer — an OpenCode sidebar TUI plugin that polls this endpoint to show live team/agent status — needing a stable contract to detect payload drift instead of crashing or rendering stale data, and a way to scope teams to the session that created them rather than showing every team ever tracked.

Changes

  • buildState()'s return type is now the exported, documented EnsembleDashboardState interface instead of an inline { projects: unknown[]; teams: unknown[] } shape.
  • /api/state now returns a bare-integer version field (ENSEMBLE_STATE_VERSION, currently 1). Bump on any breaking change to the payload; additive fields don't require a bump.
  • Each team object now includes leadSessionId alongside the existing leadAgent.
  • No behavior changes to teammate lifecycle, dashboard rendering, or any existing endpoint.

Test Plan

  • Two new tests: version field shape, leadSessionId presence per team
  • Full suite: 695 pass, 0 fail
  • bun run typecheck: clean

Adds a bare-integer version field to the /api/state payload and gives
buildState() a real return type (EnsembleDashboardState) instead of an
inline anonymous shape. Bump ENSEMBLE_STATE_VERSION on any breaking
change; additive fields don't require a bump.

No changes to teammate-lifecycle logic. Motivated by an external
consumer (an OpenCode sidebar TUI plugin) that polls this endpoint and
needs exact-match version detection to degrade gracefully instead of
crashing on shape drift.
Additive field -- team already carries lead_session_id in the DB
schema (NOT NULL since migration 1), buildState() just never selected
or exposed it. Lets an external consumer scope teams to "created by
this specific session" rather than only "this project directory" --
the current sidebar TUI plugin's project-only scoping still mixes
multiple sessions' teams together within one project.

No ENSEMBLE_STATE_VERSION bump -- purely additive per the version
field's own documented policy (field removed/renamed/retyped needs a
bump, new field doesn't).
@Agony1023
Agony1023 requested a review from hueyexe as a code owner August 19, 2026 23:14
…api/state

Depends on the last_nudged_at column added by the watchdog/nudge-staleness
fix (separate PR) — this branch's tests will fail standalone until that
migration is present. Confirmed: only failure mode is 'no such column:
last_nudged_at' (11 fail/10 errors, all traceable to the missing column,
nothing else broken). Merge order: land the watchdog PR first, or rebase
this on top of it.
@Agony1023

Copy link
Copy Markdown
Contributor Author

Pushed an additional commit (cfd79c9) exposing last_nudged_at (added in #31) as lastNudgedAt in /api/state, additive to the versioned response shape — no version bump needed. Note: this branch depends on #31's migration for the underlying column; its own tests will fail standalone until #31 merges (confirmed — the only failure mode is no such column: last_nudged_at, nothing else affected). Happy to rebase or reorder if you'd rather merge these in a different sequence.

Agony1023 added a commit to Agony1023/opencode-ensemble that referenced this pull request Aug 20, 2026
… (backend half)

Based on this PR's branch -- needs no additional dependency beyond what's
already here.

session.status already has a "retry" case in handleSessionStatusEvent
(fires a toast) -- the payload was thrown away immediately after. This
persists it instead:

- Migration 10: four additive, nullable columns on team_member
  (retry_until, retry_attempt, retry_provider, retry_message). No CHECK
  constraint change, no new status/execution_status literal.
- handleSessionStatusEvent gains an optional retryPayload param; the
  existing retry branch persists the four columns in addition to the
  existing toast, not instead of it. status/execution_status untouched.
- No hardcoded rate-limit/throttle language: retry_message prefers
  action.message, falls back to the generic status.message.
- retry_until is a TTL; "currently retrying" is a derived, read-time
  boolean, never a stored enum -- no clear-write anywhere.
- team-status.ts's status line gets an additive annotation for a
  retrying member, same pattern as the existing nudged-timestamp one.

status.next resolved as an absolute epoch-ms timestamp (verified against
the opencode server bundle's session.retry.scheduled handler and the
built-in TUI's own countdown math).

Non-goals: no watchdog/stall-detection changes -- checkStalled()/
ProgressTracker.isTimeStalled() never see this signal, by design.

727/727 tests, typecheck/build clean. Companion change surfacing this via
/api/state and the dashboard UI is a separate commit against hueyexe#33.
… + UI

Depends on the retry_until/retry_attempt/retry_provider/retry_message
columns added by the companion backend PR (against hueyexe#31) -- this branch's
tests will fail standalone until that migration is present. Confirmed:
the only failure mode is the retry_* columns not existing, nothing else
broken.

- dashboard.ts: additive isRetrying (derived, read-time TTL boolean --
  never a stored enum), retryUntil, retryAttempt, retryProvider,
  retryMessage on the member row in /api/state. No version bump needed
  (additive-only, same reasoning as the earlier lastNudgedAt field).
- dashboard-js-render.ts: additive "retrying" chip on agent cards and
  the member drawer, generic label + attempt number, only the actual
  provider message text if present -- no hardcoded rate-limit/throttle
  wording anywhere in this file either.

Merge order: land the backend PR first, or rebase this on top of it.
@Agony1023

Copy link
Copy Markdown
Contributor Author

Pushed another additional commit extending this PR further — exposes the provider-retry signal (added in the backend PR against #31: retry_until, retry_attempt, retry_provider, retry_message on team_member) additively in /api/state as isRetrying/retryUntil/retryAttempt/retryProvider/retryMessage, plus a corresponding chip in the dashboard UI (agent cards + drawer). No version bump needed — additive-only. Same cross-PR dependency shape as the earlier lastNudgedAt commit: this branch's own tests fail standalone until the backend PR's migration lands (confirmed — only failure mode is the missing columns, nothing else affected). Happy to rebase or reorder if you'd rather merge in a different sequence.

hueyexe pushed a commit that referenced this pull request Aug 25, 2026
…32) (#35)

## Summary

This branch is a full review-and-integration pass over **every
currently-open PR** (#29, #31, #33, #34) and **every currently-open
issue** (#26, #32). Each contribution was reviewed against a real
running OpenCode server, reworked where needed, extended where the
review surfaced gaps, and landed as a coherent set of commits. Merging
this supersedes all four open PRs and closes both open issues.

36 files changed: +2111 / −104 across src and test, no external
dependencies added.

---

### Reviewed from open PRs

#### PR #31 — Teammate lifecycle robustness (`4feb15d`, `ab4d3bc`)

Six fixes found by live stress-testing of the lifecycle, plus a
follow-up hardening commit on top of the review:

- `team_message` can now re-engage a teammate stuck in `completed`,
closing the re-engagement gap
- Stall detection covers a member's **first action**: spawn records its
busy-start baseline directly (there is no `ready→busy` status-event for
a fresh spawn), so a member stalled before its first step-finish is
visible to `checkStalled()`
- Orphaned-team reconciliation after crashes, including a deadlock the
naive fix introduced
- `team_merge` no longer reports false success when the merge silently
fails
- `team_broadcast` now wires chatty-detection like other send paths
- Watchdog nudge display-staleness signal + logging parity
(`last_nudged_at` schema, nudges visible in `/api/state`)

#### PR #33 — Versioned `/api/state` for external consumers (`e8be872`,
`4bab276`, `d4fc294`, `95714e8`)

Landed the typed/versioned state contract, then extended it additively
during review:

- `buildState()` returns an exported, documented type; response carries
a `version` field so external consumers can detect payload drift instead
of crashing on it
- Exposes `leadSessionId` (already persisted in the DB, never selected)
so consumers can scope teams to the session that created them
- Additive `lastNudgedAt` and provider-retry signals exposed in both the
API payload and dashboard UI

#### PR #34 — Task status + visualization fixes (`276d2bc`, `f0bc580`)

- Task status is now reliably reflected across the whole teammate
lifecycle: new `src/tasks.ts` centralizes claim/release/reassign, so
tasks return to the board when a member stalls, errors, or is shut down
instead of staying pinned to a dead assignee
- Freshly-spawned teammates no longer stick at `[starting]` with a
frozen timestamp — busy transitions bump execution status and time even
when the member was inserted already-busy

#### PR #29 — Nullish agent normalization ✅ verified

The core fix (`agent: null | undefined` → `"build"` instead of hitting
the NOT NULL constraint) was already merged via #30. This branch retains
and extends its regression coverage in `test/tools/team-spawn.test.ts`;
nothing further required.

---

### Closes open issues

#### Issue #26 — Update agent parameters in-place (`009e868`)

New `src/member-model.ts`: `team_message` accepts `{ model:
"provider/model", to: "<member>" }` and hot-swaps that teammate's model
without respawning or re-assigning tasks. Includes permission-safe
plumbing through `tool.execute.before`, DB persistence of the override,
and tests.

Closes #26

#### Issue #32 — Dashboard shows 08:00:00 for historical timestamps
(`f0bc580`)

OpenCode v1 exposes message time as an object (`{ created: number }`)
rather than a bare number; the activity view parsed the object as a
number, fell back to epoch-0 rendering, and produced identical
`08:00:00` stamps for historical entries. Timestamp extraction now
handles both shapes.

Fixes #32

---

### Also in this branch

- **Notify-the-lead primitive** (`621387f`): new `src/notify.ts` gives
any client a direct channel to wake and message the lead; `team_spawn`'s
failure rollback path now uses it, so lead notification survives cases
where the old in-band message path could not fire
- **Provider-retry observability** (`fb5f7d2`): session.status retry
signals are persisted (backend half) and surfaced in the dashboard, so
provider flapping is distinguishable from genuine stalls
- Test-only scoping fix (`ec6a737`) and doc dedup (`89038ea`) left over
from the integration work

---

### Testing

- `bun run typecheck && bun test && bun run build` — all green
- 783 tests / 38 files passing, including new suites for lifecycle
recovery, watchdog staleness, task reassignment, model updates, notify
paths, and merge-flow accuracy

---------

Co-authored-by: Lennox McKenzie <lennox.mckenzie@gmail.com>
Co-authored-by: Jared Davies <jared.davies@cesicorp.com>
@hueyexe

hueyexe commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Clean addition Lennox. Shipped in v0.17.0 via #35 with a couple additive fields built on top of yours (lastNudgedAt and provider retry signals). Closing since its released. Thanks for your support on this.

@hueyexe hueyexe closed this Aug 25, 2026
@Agony1023
Agony1023 deleted the feat/dashboard-state-version-clean branch August 27, 2026 13:40
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