Skip to content

fix(mcp): resolve implicit session_id to active hook-created session#400

Open
SacrilegeTx wants to merge 5 commits into
Gentleman-Programming:mainfrom
SacrilegeTx:fix/session-id-propagation
Open

fix(mcp): resolve implicit session_id to active hook-created session#400
SacrilegeTx wants to merge 5 commits into
Gentleman-Programming:mainfrom
SacrilegeTx:fix/session-id-propagation

Conversation

@SacrilegeTx
Copy link
Copy Markdown

Closes #386

Summary

When the SessionStart hook registers a session via POST /sessions, subsequent mem_save calls without an explicit session_id were falling back to manual-save-{project} instead of attaching observations to the active UUID session. The result: every Claude Code session left an empty UUID row, while one ever-growing manual-save-* bucket accumulated all observations.

This PR implements direction (3) from the issue (store-side resolution), with directory matching to avoid cross-cwd collisions inside the same project.

Approach

  • New store.LookupActiveSession(project, directory) returns the most recent open session (ended_at IS NULL) for the resolved project, preferring the row whose directory matches the caller's cwd.
  • New resolveImplicitSessionID(s, project) in internal/mcp is the single bridge used by every handler that previously called defaultSessionID(project) directly. It falls back to manual-save-{project} only when no active session is found.
  • SessionActivity is now keyed by the resolved session ID rather than the raw argument, so activity tracking stays consistent whether the caller passes session_id explicitly or relies on implicit resolution.

Handlers updated

  • handleSave (mem_save)
  • handleSavePrompt (mem_save_prompt)
  • handleSessionSummary
  • handleSessionStart / handleSessionEnd
  • handleCapturePassive

Behavior preserved

  • Explicit session_id argument still wins; resolution only kicks in when the caller omits it.
  • manual-save-{project} remains the final fallback when there is no active session row, so existing flows that never opened a session are unaffected.
  • Saves still auto-fill project for authoring; the resolver only affects which session row the observation lands on.

Tests

  • internal/store/sessions_lookup_test.go (new) covers LookupActiveSession: directory match, project scoping, ended_at filtering, ordering by most recent.
  • internal/mcp/mcp_test.go adds coverage across every affected handler: implicit resolution to an active session, fallback to manual-save-* when none is open, activity tracker keyed by the resolved ID, and the capture_passive and save_prompt / session_summary paths.

Test plan

  • go test ./internal/store/... -run LookupActiveSession
  • go test ./internal/mcp/...
  • go test ./...
  • Optional manual: start a Claude Code session, let the hook open a session, call mem_save without session_id, verify the UUID session's observation_count increments instead of the manual-save-* row.

Copilot AI review requested due to automatic review settings May 20, 2026 20:24
mem_save and four sibling handlers fell back to manual-save-{project}
whenever session_id was not provided in the tool args, leaving the UUID
sessions created by the Claude Code SessionStart hook with zero
observations and accumulating all writes into one bucket per project.

New Store.LookupActiveSession(project, directory) resolves the most
recent open session matching the MCP process cwd; the manual-save
fallback is preserved when no active session is found.

Fixes Gentleman-Programming#386
The Gentleman-Programming#386 data-attachment fix routes observations to the active
hook-created UUID session, but four handlers still recorded/read
activity under defaultSessionID(project). After this change all
four handlers key activity against the same session ID they use
for writes: handleSessionSummary reads under the resolved ID,
handleCapturePassive records under the resolved ID (resolution
moved before the activity call), handleSessionStart/End use the
explicit id from the request — which is the correct bucket
regardless of Gentleman-Programming#386.
…the active-session index

handleCapturePassive reorder in the previous commit moved
ensureImplicitSessionWithCWD and RecordToolCall above the content
guard, so empty-content calls were creating a session row in the DB
and incrementing the activity counter before returning the error.
Restore the content guard above the side-effecting block; only the
read-only resolveImplicitSessionID remains relevant ahead of the
activity call, which now lives after the guard alongside session
ensurement.

LookupActiveSession's index lacked the sort column, so
ORDER BY started_at DESC triggered a TEMP B-TREE sort despite the
covering WHERE. Recreate the index with started_at DESC appended so
the planner can serve the ordered LIMIT 1 directly from the index;
DROP IF EXISTS first to supersede the prior shape.

Adds a regression test that empty content does not mutate the
session table or the activity tracker.
- resolveImplicitSessionID now returns (id, found) so handlers skip
  ensureImplicitSessionWithCWD when the lookup resolved to an existing
  UUID session, preventing spurious SyncOpUpsert mutations on every
  mem_save when the hook-registered session is active. Caller-provided
  session IDs preserve the pre-existing auto-create behavior.

- Surface DB errors from LookupActiveSession via stderr instead of
  swallowing them; a transient lookup failure was silently routing
  writes back to manual-save-{project} with no signal.

- Rename TestCapturePassiveRecordsToolCall to clarify it exercises
  the fallback path; UUID resolution is covered separately.

- Drop the 1100ms sleep in TestLookupActiveSessionReturnsMostRecent...
  by writing explicit started_at values, eliminating clock-step flake
  risk and trimming suite time.
…overage gap

Item 1: drop rotting line-number reference in resolveImplicitSessionID's
error-log comment; replace with a stable prose reference to handleSave.

Item 2: add TestMemSavePromptSkipsSessionUpsertWhenResolvedToExistingUUID
and TestMemSessionSummarySkipsSessionUpsertWhenResolvedToExistingUUID,
mirroring the existing handleSave test from 939cc2c. Both handlers already
carry the !sessionFound guard, but had no test pinning it — a regression
could reintroduce the spurious SyncOpUpsert flood silently. Each new test
was verified: FAIL with the unconditional ensure, PASS with the guard.
@SacrilegeTx SacrilegeTx force-pushed the fix/session-id-propagation branch from 1a693e9 to 084ce71 Compare May 20, 2026 20:27
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes MCP write tools (e.g., mem_save) incorrectly defaulting to manual-save-{project} when a real UUID session was already opened by the Claude Code SessionStart hook, ensuring observations/prompts/summaries attach to the active open session instead.

Changes:

  • Added Store.LookupActiveSession(project, directory) and a supporting SQLite index to find the most recent open session for a (project, directory) pair.
  • Routed implicit session resolution in MCP through resolveImplicitSessionID, updating multiple handlers and aligning SessionActivity to be keyed by the resolved/explicit session ID.
  • Added store- and MCP-level tests covering resolution behavior, fallbacks, and activity keying.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
internal/store/store.go Adds a new session lookup API and a migration-time index intended to speed up open-session resolution.
internal/store/sessions_lookup_test.go New unit tests for LookupActiveSession behavior (ended sessions, ordering, blank inputs, etc.).
internal/mcp/mcp.go Uses store-side lookup to resolve implicit session IDs and fixes activity tracking to use the resolved/explicit session.
internal/mcp/mcp_test.go Adds/updates tests validating implicit session resolution across affected MCP handlers and activity tracking.
Comments suppressed due to low confidence (1)

internal/store/store.go:1925

  • LookupActiveSession matches sessions by exact project string, but sessions are stored with NormalizeProject() in CreateSession (and other query APIs normalize too, e.g. RecentSessions). Without normalizing here, callers that pass an unnormalized project will silently miss the active session and fall back to manual-save. Normalize/trim the project (and document/consider normalizing directory if you want stable matching).
			title,
			content,
			tool_name,
			type,
			project,
			topic_key,
			content='observations',
			content_rowid='id'
		);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/store/store.go
Comment on lines +1060 to +1071
// Phase: session-id-propagation (#386) — index for LookupActiveSession.
// Includes started_at DESC so the planner can satisfy ORDER BY started_at DESC
// directly from the index without a TEMP B-TREE sort step. DROP first to
// supersede any pre-existing index that lacked the sort column.
if _, err := s.execHook(s.db, `DROP INDEX IF EXISTS idx_sessions_active_lookup;`); err != nil {
return err
}
if _, err := s.execHook(s.db, `
CREATE INDEX IF NOT EXISTS idx_sessions_active_lookup
ON sessions(project, directory, ended_at, started_at DESC);
`); err != nil {
return err
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.

fix(mcp): mem_save falls back to manual-save-{project} despite active session, leaving UUID sessions empty

2 participants