Skip to content

feat(tui): Phase 1 — Observer foundation, scheduler slots, history - #73

Merged
Exelord merged 1 commit into
mainfrom
claude/tui-phase-1-observer
May 13, 2026
Merged

feat(tui): Phase 1 — Observer foundation, scheduler slots, history#73
Exelord merged 1 commit into
mainfrom
claude/tui-phase-1-observer

Conversation

@Exelord

@Exelord Exelord commented May 13, 2026

Copy link
Copy Markdown
Member

Summary

Phase 1 of the TUI roadmap (specced in PR #72): orchestrator-side
scaffolding. No renderer in this PR — the surface is
renderer-agnostic so the same Observer can feed a future vx ui
historical browser and embedder use cases. Phase 2 (OpenTUI
components + --tui flag) builds on top.

No behavior change for non-TUI runs. Tests: 414 → 434.

What ships

  1. src/orchestrator/observer.ts — tagged-union ObserverEvent
    (runStart | taskStart | taskStdout | taskStderr |
    cacheProbe | taskComplete | remoteCache | runEnd), an
    Observer interface, and makeSafeObserver(inner) that swallows
    throws from inner.emit so a buggy consumer never crashes the run.
    Logger stays parallel — it owns terminal framed-block output;
    Observer is the structural sink.

  2. RunOptions.observer wired through orchestrator.run().
    Emit sites match the design doc (docs/design/tui-design.md §3):

    • runStart after header writes.
    • taskStart from the scheduler's onStart(node, slot).
    • cacheProbe from execute-task.ts after cache.get(hash)
      (hit-local | hit-remote | miss | no-cache).
    • taskComplete from scheduler's onFinish.
    • runEnd after formatRunSummary, before --summarize /
      --profile writes.
  3. Scheduler worker-slot allocation. runGraph now allocates
    lowest-free-index slots from [0, concurrency) and passes
    slot: number to execute() + onStart(). Stable allocation so
    a future Workers view renders [1] always-busy / [N] idle-gap
    visibly across runs.

  4. Cache.getTaskHistory(taskIds) — one SQL CTE with
    ROW_NUMBER() OVER (PARTITION BY project, task) capped at 50 rows
    per pair. Returns TaskHistoryMap of runs / avg / p50 / p99 / successRate / hitRate / recent[10]. Threaded through prepareRun
    so the runStart event carries historyTable populated for every
    node in the graph (cheap; one batched read).

  5. LayeredCacheOptions.onRemoteRequest callback fires for every
    remote GET/PUT with { op, hash, bytes?, latencyMs, ok }.
    wrapWithRemoteCache(local, log, observer) forwards it into
    observer.emit({ kind: 'remoteCache', ... }).

Test plan

  • bun src/bin.ts run format — clean
  • bun src/bin.ts run lint — 0 warnings, 0 errors
  • bun src/bin.ts run test — 434 pass, 0 fail
  • New: 5 observer (tests/observer.test.ts), 5 scheduler-slot
    (tests/scheduler.test.ts), 4 cache-history
    (tests/cache.test.ts), 3 layered-cache callback
    (tests/layered-cache.test.ts), 3 end-to-end observer wiring
    (tests/orchestrator.test.ts)
  • Existing 414 still green (unchanged behavior)

https://claude.ai/code/session_016HXj6HW6bxSn8EYuKcxTD9


Generated by Claude Code

Orchestrator-side scaffolding for the TUI. No renderer yet (Phase 2);
the surface is renderer-agnostic so the same Observer can feed a
future `vx ui` historical browser and embedder use cases.

Five additions, no behavior change for non-TUI runs:

1. `src/orchestrator/observer.ts` — tagged-union ObserverEvent
   (runStart, taskStart, taskStdout/Stderr, cacheProbe, taskComplete,
   remoteCache, runEnd), Observer interface, makeSafeObserver wrapper
   that swallows throws so a buggy consumer never crashes the run.
   Logger stays parallel (terminal framed-block output); Observer is
   the structural sink.

2. RunOptions.observer wired through orchestrator.run(). Emit sites:
   runStart after header; taskStart from scheduler onStart(node, slot);
   cacheProbe in execute-task after cache.get(hash); taskComplete from
   scheduler onFinish; runEnd after formatRunSummary.

3. Scheduler now allocates lowest-free-index worker slots and passes
   `slot: number` to execute() + onStart(). Stable allocation so a
   future Workers view renders [1] always-busy / [N] idle-gap visibly.

4. Cache.getTaskHistory(taskIds) — one batched SQL CTE with
   ROW_NUMBER() OVER (PARTITION BY project, task) capped at 50 per
   pair. Threaded through prepareRun so runStart carries a
   HistoryTable for every node (cheap; sub-ms in practice).

5. LayeredCacheOptions.onRemoteRequest fires for every GET/PUT with
   { op, hash, bytes?, latencyMs, ok }. wrapWithRemoteCache forwards
   it into observer.emit({ kind: 'remoteCache', ... }).

Tests: 414 → 434. New: 5 observer, 5 scheduler-slot, 4 cache-history,
3 layered-cache callback, 3 orchestrator end-to-end observer.

https://claude.ai/code/session_016HXj6HW6bxSn8EYuKcxTD9
@Exelord
Exelord merged commit 72e98c2 into main May 13, 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.

2 participants