You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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/:
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)
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):
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.ts → broadcast — 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
npm run typecheck, npm run test:unit, full npm test, npm run build, git diff --check — all green.
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.
server.ts contains no session lifecycle/operation logic — only config, HTTP plumbing, routes (parse → service → decorate → send), realtime/activity/unread, host stores, and wiring.
All service method results and all emitted SessionServiceEvents pass strict-deep-equal JSON round-trip (extend the existing fixture test to the event stream).
The deps interface has ≤ ~6 members, each a true external.
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).
Stage 2.5: make
SessionServiceself-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 leftLocalSessionServiceas a facade: the real lifecycle/operations logic still lives inserver.tsand 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 intoserver/session/:LiveSessionEntry, viewer/work leases, cleanup scheduling,disposeLiveSessionregisterLiveSession— subscribes to pi events and callsbroadcast()directlymakeAgentSession,createNewLiveSessiongetOrCreateLiveSessionById,deleteSessionById,listSessionInfosexecuteSlashCommand,switchEmptySessionCwd,navigateSession,startSessionPrompt,startSessionRetryAlready 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) soLocalSessionServiceowns them. ShrinkLocalSessionServiceDependenciesfrom 18 closures to true externals only:authStorage/modelRegistrycreateMockSessionmust keep working)additionalExtensionPaths)applyDefaultSessionSettings/applyDefaultSessionBucketread the host settings store — inject adefaultsFor(cwd)callback, do not import the store)Host-side things that must stay in server.ts (per the Stage 2 boundary rule):
server/session/activity.tsusage),decorateState/decorateMessageContentcompositiontransferCurrentTabUiStateresolveSessionId-style route defaults2. Invert events (the core of this issue)
registerLiveSessioncurrently callsbroadcast()directly. Instead:subscribe(listener: (e: SessionServiceEvent) => void): () => voidand emits a typed union (reintroduce it indto.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 onsession_info_changed)stats(onmessage_end/agent_end/compaction_end)models(blocked-model detection —model_not_supported/model_not_availablehandling moves into the service; noteblockedModelIdsthereby becomes per-service state, which is the accepted semantics)error,shutdownserver.tssubscribes once: enrich viaactivity.ts→broadcast— producing byte-identical wire messages in the same order as today (pi_event,session_runtime_changed, optionalstate_changed, optionalsession_stats_changed, optionalmodels_updated).server/extensions/webUi.tskeeps working; if practical, route itsemitthrough the same service event sink rather thanbroadcastdirectly (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 onBaseSessionStateDto(host decoration fields typed as an intersection added in server.ts)messages()→ a real message DTO instead ofunknown[]4. Keep the finalizer serving-side
NavigationResult.finish(): voidis 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
npm test.Acceptance criteria
npm run typecheck,npm run test:unit, fullnpm test,npm run build,git diff --check— all green.LocalSessionService(create session → prompt via mock → state/messages/events) without importingserver.ts. If the service can't be built in a test without the server, this issue is not done.server.tscontains no session lifecycle/operation logic — only config, HTTP plumbing, routes (parse → service → decorate → send), realtime/activity/unread, host stores, and wiring.SessionServiceEvents pass strict-deep-equal JSON round-trip (extend the existing fixture test to the event stream).docs/runtime-binding-design.mdrefreshed 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; typedSessionApiderived 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).