Skip to content

feat(backend): wire Session Manager into the daemon (real tmux + gitworktree, stub Agent)#52

Merged
harshitsinghbhandari merged 3 commits into
mainfrom
feat/wire-session-manager
May 31, 2026
Merged

feat(backend): wire Session Manager into the daemon (real tmux + gitworktree, stub Agent)#52
harshitsinghbhandari merged 3 commits into
mainfrom
feat/wire-session-manager

Conversation

@harshitsinghbhandari
Copy link
Copy Markdown
Collaborator

Summary

Construct a live *session.Manager inside backend/main.go over real tmux.Runtime, real gitworktree.Workspace, the SQLite-backed SessionStore/PRWriter, and the same *lifecycle.Manager the LCM already holds. ports.Agent is a loud stub (no production adapter exists on main); Notifier/AgentMessenger remain stubbed alongside the LCM. No HTTP routes are wired in this PR — that's the daemon lane's territory.

The refactor extracts the old storeAdapter from package main into a new internal package, internal/storage/sqlite/wiring, so the daemon's composition root and any in-process integration tests can share one canonical bridge instead of duplicating it.

File-by-file

File Change
backend/internal/storage/sqlite/wiring/adapter.go (NEW, 107 lines) New package wiring. Exports Adapter struct{ *sqlite.Store }, methods PRFactsForSession + WritePR, helper prState, and both var _ ports.SessionStore = Adapter{} / var _ ports.PRWriter = Adapter{} assertions — moved verbatim from lifecycle_wiring.go:45-128.
backend/lifecycle_wiring.go Removes storeAdapter + the two var assertions + PRFactsForSession + WritePR + prState (all moved). Switches startLifecycle to wiring.Adapter{Store: store} and exposes the adapter on lifecycleStack.Adapter. Adds sessionStack, startSession, and the loud-stub *noopAgent (sentinel AO_AGENT_HARNESS_NOT_WIRED, sync.Once warning on first call). Reuses existing noopNotifier / noopMessenger.
backend/main.go Calls startSession(ctx, cfg, lcStack, log) right after startLifecycle. Holds the *sessionStack in a local (no HTTP route uses it yet). Rewrites the NOT wired here yet block at lines 73-86 to reflect the new reality: Runtime + Workspace are real, only ports.Agent is a loud stub, Notifier/Messenger stubs remain pending their multiplexers, HTTP routes still deferred.
backend/wiring_test.go Updates the existing TestWiring_WriteFlowsToBroadcaster to use wiring.Adapter. Adds TestWiring_SessionManagerSharesLifecycleStoreAndLCM: asserts startSession returns non-nil, then verifies (via reflect + unsafe scoped to one helper) that the SM's store field is the exact same wiring.Adapter and its lcm field is the exact same *lifecycle.Manager the lifecycle stack owns — proving a single canonical pair, not two parallel stacks. The unsafe access is the smallest path that respects the brief's no-modify-session/manager.go constraint.

git diff --stat origin/main...HEAD:

backend/internal/storage/sqlite/wiring/adapter.go | 107 +++++++++++++
backend/lifecycle_wiring.go                       | 181 ++++++++++++----------
backend/main.go                                   |  26 ++--
backend/wiring_test.go                            |  89 ++++++++++-
4 files changed, 305 insertions(+), 98 deletions(-)

NOT in this PR (intentional)

  • Real ports.Agent adapter — none exists on main; the loud stub gives the SM a working Spawn shape that fails clearly when invoked. A per-harness Agent adapter (Claude Code / Codex / Cursor) is the next lane.
  • HTTP routes for SessionManager — owned by the daemon lane (Tracking: Go HTTP Daemon — REST #10). httpd.New keeps its current signature; SM is held in a local so wiring routes later is a one-line plumb-through.
  • Integration / Spawn tests against this wiring — PR test(integration): LCM+SM live-fire against real SQLite store #49 (session/aa-31) already covers SM+LCM+SQLite live-fire under its own stub plugins.
  • Real RepoResolverstartSession passes an empty gitworktree.StaticRepoResolver{}. Any future Spawn for any project fails with gitworktree: no repo configured for project %q — the right loud failure until a projects table feeds repo paths in (hard-coding one repo here would silently misroute spawns).
  • Notifier / AgentMessenger multiplexers — still stubbed (noopNotifier / noopMessenger), tracked separately.
  • Behavior changes to lifecycle/manager.go, session/manager.go, ports/*, domain/*, or migrations — explicitly forbidden by the brief; verified untouched.

Follow-up for PR #49

backend/internal/integration/lifecycle_sqlite_test.go on PR #49's branch (session/aa-31) currently carries its own duplicate storeAdapter. Once both this PR and #49 land, that duplicate should switch to wiring.Adapter. I deliberately did not modify #49's branch (per the brief).

Test plan

  • go build ./...
  • go vet ./...
  • go test -race -count=1 ./... (215 pass across 18 packages — was 214; the +1 is the new wiring test)
  • go test -race -count=1 -run TestWiring ./. (both TestWiring_WriteFlowsToBroadcaster and TestWiring_SessionManagerSharesLifecycleStoreAndLCM pass under -race)
  • Branch verified off baseline 0672dbb (git log -1 origin/main)

🤖 Generated with Claude Code

harshitsinghbhandari and others added 2 commits May 31, 2026 20:12
…orktree, stub Agent)

Constructs a live *session.Manager in main alongside the LCM, sharing the
exact same SessionStore + LCM dependencies the lifecycle stack already
holds.

Refactor: storeAdapter moves from package main to a new internal
package, wiring.Adapter, so the daemon's composition root and any
in-process integration tests can share a single bridge.

Stubbed for now: ports.Agent has no production adapter on main; a loud
*noopAgent returns sentinel AO_AGENT_HARNESS_NOT_WIRED and logs a
warning once on first call, so a future Spawn through this lane fails
at the runtime layer with a clear breadcrumb rather than starting a
broken session quietly. ports.Notifier and ports.AgentMessenger remain
stubbed alongside the LCM.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Renames the unused context.Context parameter from `_` to `ctx` so the
parameter name is already in place when a future plugin constructor
needs to honor cancellation (tmux/gitworktree are synchronous today).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 31, 2026

Greptile Summary

This PR wires a live *session.Manager into the daemon by composing it over real tmux.Runtime and gitworktree.Workspace plugins, the same wiring.Adapter (SQLite store bridge) and *lifecycle.Manager the LCM already holds, and a loud-stub noopAgent that returns a sentinel command causing any Spawn to fail clearly at the runtime layer. It also extracts storeAdapter from package main into a new internal/storage/sqlite/wiring package so the composition root and integration tests share one canonical bridge.

  • wiring/adapter.go (new): moves storeAdapter verbatim into its own package with compile-time interface assertions; both ports.SessionStore and ports.PRWriter are satisfied on the value type with no behaviour change.
  • lifecycle_wiring.go: exposes Adapter on lifecycleStack, adds startSession / sessionStack, and introduces noopAgent with a sync.Once warning that fires once on first Spawn/Restore.
  • main.go: calls startSession after startLifecycle with a correct explicit-drain error path (stop → lcStack.Stop → cdcPipe.Stop) so no background goroutine touches the store after the deferred store.Close fires.

Confidence Score: 5/5

Safe to merge — the shutdown ordering on the new startSession error path is correct, the shared store and LCM pointers are verified by the new test, and all new code paths are guarded by loud failures rather than silent no-ops.

The core wiring is a clean move: storeAdapter is lifted verbatim into the new package, the SM is composed over the exact same Adapter and LCM pointer the lifecycle stack holds, and the error path in main.go correctly drains the reaper and CDC pipeline before the deferred store.Close fires. The only fragility is the reflect+unsafe field inspection in the test, which relies on unexported field names staying stable — a compile-time-invisible assumption that would surface as a runtime test failure if session.Manager is ever refactored.

backend/wiring_test.go — the reflect+unsafe field inspection in inspectSessionDeps silently assumes the store and lcm field names of session.Manager never change; a rename would produce a runtime test failure rather than a compile error.

Important Files Changed

Filename Overview
backend/internal/storage/sqlite/wiring/adapter.go New package that lifts storeAdapter from package main into a shared wiring package; compile-time interface assertions, correct value-receiver pattern, clean move with no behaviour changes.
backend/lifecycle_wiring.go Removes inlined storeAdapter, wires startSession over real tmux/gitworktree with a loud-stub Agent and correctly exposes Adapter on lifecycleStack so SM and LCM share one canonical store instance.
backend/main.go Adds startSession call with a correct explicit-drain error path (stop → lcStack.Stop → cdcPipe.Stop) before the deferred store.Close fires; SM held as a blank local pending HTTP route wiring.
backend/wiring_test.go Updates existing broadcast test to use wiring.Adapter; adds a new structural test using reflect+unsafe to verify SM shares the same store and LCM pointer as the lifecycle stack — deliberate trade-off but the field-name coupling is fragile.

Sequence Diagram

sequenceDiagram
    participant main
    participant startLifecycle
    participant startSession
    participant wiring.Adapter
    participant lifecycle.Manager
    participant session.Manager
    participant tmux.Runtime
    participant gitworktree.Workspace
    participant noopAgent

    main->>startLifecycle: startLifecycle(ctx, store, log)
    startLifecycle->>wiring.Adapter: "Adapter{Store: store}"
    startLifecycle->>lifecycle.Manager: lifecycle.New(a, a, noopNotifier, noopMessenger)
    startLifecycle-->>main: "lifecycleStack{LCM, Adapter, reaperDone}"

    main->>startSession: startSession(ctx, cfg, lcStack, log)
    startSession->>tmux.Runtime: "tmux.New(Options{})"
    startSession->>gitworktree.Workspace: "gitworktree.New(Options{ManagedRoot, StaticRepoResolver{}})"
    startSession->>noopAgent: newNoopAgent(log)
    startSession->>session.Manager: "session.New(Deps{Runtime, Agent, Workspace, lcStack.Adapter, noopMessenger, lcStack.LCM})"
    note over session.Manager,wiring.Adapter: SM.store == lcStack.Adapter (same *sqlite.Store)
    note over session.Manager,lifecycle.Manager: SM.lcm == lcStack.LCM (same pointer)
    startSession-->>main: "sessionStack{SM}"

    main->>main: srv.Run(ctx)
    main->>main: stop() → lcStack.Stop() → cdcPipe.Stop()
Loading

Reviews (2): Last reviewed commit: "fix(backend): drain reaper + cdc poller ..." | Re-trigger Greptile

Comment thread backend/main.go
If startSession returned an error, run() returned immediately and the
reaper + cdc poller goroutines kept running while defer store.Close()
fired — a data race against the SQLite handle. Mirror the bottom-of-run
shutdown sequence on the error path (cancel ctx, drain reaper, drain
poller) so both goroutines have exited before the store is closed. The
explicit-not-defer ordering is the same the existing
post-srv.Run shutdown block uses; piling on more defers would hit the
LIFO trap the same comment already warns about.

Reported by Greptile on PR #52.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@harshitsinghbhandari harshitsinghbhandari merged commit f8611de into main May 31, 2026
2 checks 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.

1 participant