Gateway Module — `sessions/`
1. Purpose
The sessions module is the cross-channel session registry (Dispatch 399/650) — it tracks live VS Code and Genkan (Hermes) sessions, their heartbeats, working state, active dispatches, and conversation turns, and provides the cross-channel directive-injection mechanism. It carries the module's own SSE event stream (GET /events), a heartbeat update path, a stale detector that auto-ends sessions silent >30min, and three transcript-read carve-outs (vscode, hermes, :id/turns) that leak operator conversation state and are therefore explicitly Bearer-gated post-AK-415/AK-334.
2. File Inventory
| File | Lines | Responsibility |
|---|---|---|
routes.ts | 447 | 9 endpoints incl. SSE /events + 3 transcript readers |
repository.ts | 296 | active_sessions + session_turns prepared statements; merge-on-heartbeat; stale sweep |
types.ts | 114 | Zod schemas, ActiveSession, SessionTurn, repository + stale-detector interfaces |
index.ts | 70 | createSessionLayer + createStaleSessionDetector (5-min interval, 30-min threshold) |
events.ts | 40 | SessionEventBus singleton — directive + session_state events |
| Total | 967 |
3. Public API Surface
REST Endpoints (mount: index.ts:752 → /api/sessions)
| Method | Path | Auth | Body Schema | Response | Side Effects |
|---|---|---|---|---|---|
| POST | /register | Bearer (write) | RegisterSessionInputSchema | 201 {data: ActiveSession} | INSERT OR REPLACE active_sessions; emits session_state |
| PUT | /:id/heartbeat | Bearer (write) | HeartbeatInputSchema | {data: ActiveSession} | Merge-update (incoming ?? existing); emits session_state; 404 if absent |
| PUT | /:id/end | Bearer (write) | EndSessionInputSchema | {data: ActiveSession} | status → ended; emits session_state |
| GET | /active?channel= | Bearer | — | {data: ActiveSession[]} | Reads active/idle; operator-state leak → gated |
| POST | /directive | Bearer (write) | PostDirectiveInputSchema | 201 {cache_entry_id, ...} | Writes a directive psychic-cache entry via WriteCacheInputSchema; emits directive |
| GET | /events?channel= | Bearer | — | text/event-stream | Streams SessionEventBus; 30s keepalive |
| POST | /:id/turn | Bearer (write) | WriteTurnInputSchema | 201 {data: SessionTurn} | INSERT session_turns; content truncated to 2000 at repo |
| GET | /:id/turns?limit=&since= | Bearer | — | {data, count} | Reads turns; full conversation → gated (gray-fox D1021 exfil demo) |
| GET | /vscode/transcript?limit= | Bearer | — | {data, session_id, count} | Reads Claude Code JSONL (env-gated: 404 when CLAUDE_CODE_JSONL_DIR unset) |
| GET | /hermes/transcript?limit= | Bearer | — | {data, session_id, count} | Reads Hermes state.db read-only (env-gated: 404 when HERMES_STATE_DB_PATH unset) |
Several session endpoints that would otherwise be broadly readable are explicitly Bearer-gated because they expose session metadata, transcripts, or full conversation turns. The event stream is the exception: it uses a read-only-token handshake instead of an Authorization header, because EventSource cannot set request headers.
MCP Tools
list_sessions (RO → /active); post_session_directive_raw (RAW-ONLY → /directive). No MCP tool targets the transcript readers or SSE stream.
4. Internal API
createSessionLayer(db, psychicCacheRepo) → {repo, router, staleDetector}. SessionRepository: register, heartbeat, end, findActive, findById, cleanStale, writeTurn, readTurns. Re-exports sessionEventBus + event types from events.ts. The /directive route depends on PsychicCacheRepository.write — sessions has no cache table of its own; directives are ordinary psychic-cache directive entries.
5. Background Services
| Service | Trigger / Interval | Side Effects |
|---|---|---|
Stale session detector (index.ts) | 5min (300_000) + an initial sweep on start() | cleanStale(30) → UPDATE active/idle sessions with last_heartbeat < now-30min to ended; logs count to stderr. Started/stopped from server/index.ts:938/949. |
6. Data Contracts
RegisterSessionInputSchema:id,channel(vscode|hermes),current_mission_namespaces?,active_dispatches?(positive int[]),working_state_prose?,metadata?.HeartbeatInputSchema: same optional fields +last_exchange_summary?(≤500),status?(active|idle).PostDirectiveInputSchema:content,directive_target,directive_priority(defaultmedium),mission_id,tags(min 1), plus targeting/source attribution optionals.WriteTurnInputSchema:role(user|assistant|system),content(min 1, max 10000 validated; truncated to 2000 at repo).SessionEvent=DirectiveEvent | SessionStateEvent(events.ts). Row JSON columns (current_mission_namespaces,active_dispatches,metadata) parse with fail-safe defaults (class-(b) silent-catch, intentional).
7. Dependencies
- Gateway modules:
psychic-cache/types.js(PsychicCacheRepository,WriteCacheInputSchema),shared/zod-error.js. - External:
better-sqlite3(incl. a second read-onlyDatabasehandle opened per-request forHERMES_STATE_DB),express,zod, Nodefs/os/path. - Environment variables:
CLAUDE_CODE_JSONL_DIR,CLAUDE_CODE_SESSION_ID_PATH(default~/.claude/session-env/.active-session-id),HERMES_STATE_DB_PATH.
8. Test Coverage
| Layer | File | Notes |
|---|---|---|
| Routes | test/sessions-routes.test.ts | 9 endpoints, SSE, directive→cache, transcript env-gates |
| Repository | test/sessions-repository.test.ts | Merge-heartbeat, turn truncation, stale sweep |
| Layer/detector | test/sessions-index.test.ts | Stale detector lifecycle |
9. Known Limitations
registerusesINSERT OR REPLACE— re-registering an existing session id silently overwritesstarted_at/turn linkage rather than erroring. Intentional for reconnect, but destructive if an id collides across channels.- The
hermes/transcriptreader opens a fresh read-only SQLite handle per request and closes it inline; no pooling. Low-frequency endpoint, acceptable, but not free under burst. - Two commented-but-absent route markers remain in
routes.ts(a strayGET /hermes/transcriptheader comment mid-file and aPOST /:id/turnmarker) — cosmetic drift, no functional impact. /eventschannel filter logic is inverted-looking: itreturns (skips) whenevent.*_channel === channelFilter, i.e. thechannel=param excludes that channel rather than including it. Worth confirming against dashboard intent. [DIVERGENCE — LOW, verify]
10. Change History
| Date | Dispatch | Summary |
|---|---|---|
| 2026-07-04 | 2296 | Module spec authored (Phase 4 §5.2), smoke-clone-2, HEAD 5c9a304 |
| — | 399 | Cross-channel session registry, SSE, directive injection, stale detector |
| — | 650/652 | Session turns; VS Code + Hermes transcript readers |
| — | 1027 | AK-334/FUP-4 Bearer carve-outs for session-metadata endpoints |
| — | 1689 | AK-415 — /events moved to read-only-token handshake gate |