Skip to content

Stage 2.5: make SessionService self-contained (dissolve the deps bag) #67

Description

@ashwin-pc

Stage 2.5: make SessionService self-contained (dissolve the deps bag)

Context

PR #60 extracted a transport-ready session-service shape from server.ts (Stage 2 of the plan in #43 (comment)). It deliberately left LocalSessionService as a facade: the real lifecycle/operations logic still lives in server.ts and is handed to the service as an 18-closure dependency bag. A runner cannot instantiate that — which blocks Stage 3 (runner as a thin stdio transport over the same service).

This issue is the follow-up explicitly deferred in the PR #60 review: move lifecycle and operations behind the service so it is a self-contained module, with zero behavior change.

Current state (line refs = main @ 90807f6, server.ts 1,676 lines)

Still in server.ts, to be moved into server/session/:

Piece Location
Live-session cache, LiveSessionEntry, viewer/work leases, cleanup scheduling, disposeLiveSession server.ts:722 area
registerLiveSession — subscribes to pi events and calls broadcast() directly server.ts:927 (pi_event at :940)
makeAgentSession, createNewLiveSession server.ts:983, :1059
getOrCreateLiveSessionById, deleteSessionById, listSessionInfos server.ts:668, :594, :382
Operations: executeSlashCommand, switchEmptySessionCwd, navigateSession, startSessionPrompt, startSessionRetry server.ts:468, :1100, :1108, :1133, :1154
The deps-bag wiring server.ts:1182

Already done (do not redo): shared git/fs/artifacts modules, projections + DTOs, host-side activity decoration, realtime hub, extension web-UI module, route→service migration, parity/status-matrix tests, route guard, navigate finalizer.

Work items

1. Move lifecycle + operations into the service

Relocate everything in the table above into server/session/ (e.g. lifecycle.ts + service.ts, or one module — implementer's choice) so LocalSessionService owns them. Shrink LocalSessionServiceDependencies from 18 closures to true externals only:

  • authStorage / modelRegistry
  • session factory (this is where the mock harness injects — createMockSession must keep working)
  • extension-paths provider (additionalExtensionPaths)
  • settings-defaults provider (applyDefaultSessionSettings / applyDefaultSessionBucket read the host settings store — inject a defaultsFor(cwd) callback, do not import the store)
  • global cwd provider
  • event sink (see item 2)

Host-side things that must stay in server.ts (per the Stage 2 boundary rule):

  • activity/tool-timing decoration (server/session/activity.ts usage), decorateState / decorateMessageContent composition
  • realtime broadcast, unread state, session-UI-state store, transferCurrentTabUiState
  • current-session singleton and resolveSessionId-style route defaults

2. Invert events (the core of this issue)

registerLiveSession currently calls broadcast() directly. Instead:

  • The service exposes subscribe(listener: (e: SessionServiceEvent) => void): () => void and emits a typed union (reintroduce it in dto.ts, now for real — it was correctly removed in 012c447 while speculative):
    • pi (verbatim pi event + sessionId/sessionFile)
    • state (emitted where the old code re-broadcast on session_info_changed)
    • stats (on message_end/agent_end/compaction_end)
    • models (blocked-model detection — model_not_supported/model_not_available handling moves into the service; note blockedModelIds thereby becomes per-service state, which is the accepted semantics)
    • error, shutdown
  • server.ts subscribes once: enrich via activity.tsbroadcast — producing byte-identical wire messages in the same order as today (pi_event, session_runtime_changed, optional state_changed, optional session_stats_changed, optional models_updated).
  • server/extensions/webUi.ts keeps working; if practical, route its emit through the same service event sink rather than broadcast directly (its wire shapes must not change).

Every emitted event payload must be JSON-serializable (this becomes the stdio event stream in Stage 3).

3. Strengthen service DTO types where projections already exist

  • state/rename/setModel/create/open → a decorated-state type based on BaseSessionStateDto (host decoration fields typed as an intersection added in server.ts)
  • messages() → a real message DTO instead of unknown[]
  • No new speculative types: only type what the implementation actually returns (the 012c447 lesson).

4. Keep the finalizer serving-side

NavigationResult.finish(): void is a callback and not JSON-serializable. It must remain on the serving side of any future transport (a remote runner finalizes after writing its own response). Document this on the type; do not attempt to serialize it.

Constraints

  • Zero behavior change. The PR Extract a transport-ready session service #60 parity suite (status matrix, rename semantics, create fallback, ordering source-check, route guard) is the regression net — it must pass unmodified except where a test asserts implementation location rather than behavior.
  • Same wire messages, same order, same status codes, same lease/dispose timing.
  • Keep changes reviewable: suggested slices — (a) lifecycle move, (b) operations move, (c) event inversion, (d) DTO strengthening. Each slice green on full npm test.

Acceptance criteria

  1. npm run typecheck, npm run test:unit, full npm test, npm run build, git diff --check — all green.
  2. The entry ticket for Stage 3: a new test constructs a working LocalSessionService (create session → prompt via mock → state/messages/events) without importing server.ts. If the service can't be built in a test without the server, this issue is not done.
  3. server.ts contains no session lifecycle/operation logic — only config, HTTP plumbing, routes (parse → service → decorate → send), realtime/activity/unread, host stores, and wiring.
  4. All service method results and all emitted SessionServiceEvents pass strict-deep-equal JSON round-trip (extend the existing fixture test to the event stream).
  5. The deps interface has ≤ ~6 members, each a true external.
  6. docs/runtime-binding-design.md refreshed to describe the staged architecture (service module → Stage 3 runner transport → Stage 4 binding-authoritative router → Stage 5 providers/broker/UI, per the PR Add runtime-bound sessions and runtime UI #43 plan comment), so the durable design lives in the repo instead of PR comments.

What comes after (not this issue)

Stage 3: runner.ts = readline loop + this service + event forwarding; typed SessionApi derived from the interface; the parity suite run a second time over spawned stdio (describeSessionService(makeService)); bundled single-file runner with version-hash health handshake. Then Stage 4 (SessionRouter + persisted bindings, fail-closed) and Stage 5 (cherry-pick model broker, stdio client, network isolation, providers, runtimes panel from PR #43).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions