Skip to content

feat(store,server,claude-code): bound the SessionStart context injection - #685

Open
neuralabmarketing wants to merge 4 commits into
Gentleman-Programming:mainfrom
neuralabmarketing:feat/context-size-cap
Open

feat(store,server,claude-code): bound the SessionStart context injection#685
neuralabmarketing wants to merge 4 commits into
Gentleman-Programming:mainfrom
neuralabmarketing:feat/context-size-cap

Conversation

@neuralabmarketing

@neuralabmarketing neuralabmarketing commented Jul 30, 2026

Copy link
Copy Markdown

🔗 Linked Issue

Closes #163


🏷️ PR Type

  • type:feature — New feature

I can't apply labels from outside the repo (needs triage permission), so "Check PR Has type: Label" will fail until a maintainer adds type:feature*. Flagging it so it doesn't read as a broken PR.


📝 Summary

  • Makes every section of the context blob boundable: ContextOptions{Observations, Prompts, Sessions, Pinned, Compact}, one convention for all four ints — 0 keeps the legacy default · >0 caps · <0 omits the section and its header.
  • Caps the pinned section, which has had no LIMIT at all since fix(memory): pin critical observations in context #484 and was the one part of the blob that could grow without any ceiling.
  • Defaults are byte-identical. FormatContext keeps its signature and output; a zero-value ContextOptions reproduces today's bytes exactly, and a test asserts precisely that.

📂 Changes

File What changed
internal/store/store.go ContextOptions + FormatContextWithOptions; FormatContext becomes a thin wrapper; unexported pinned query with LIMIT (exported PinnedObservations signature untouched)
internal/store/store_test.go TestFormatContextWithOptions — 11 subtests: byte-identity for zero value, cap/omit per section with cross-checks that siblings are untouched, compact, pinned cap
internal/server/server.go GET /context accepts observations, prompts, sessions, pinned, compact, reusing the existing queryInt/queryBool helpers
internal/server/server_e2e_test.go TestContextQueryParamsE2E — no params ≡ legacy, caps shrink, negative omits, garbage input still 200
plugin/claude-code/scripts/session-start.sh, post-compaction.sh (separable last commit) hooks request compact=1&pinned=20

📊 Measured on a 2,976-observation project

Real install, project scope, ~4 months of daily use — measured against a snapshot of that database, not synthetic data:

Request Bytes vs today
today (no params) 9,252
prompts=-1 7,946 −14%
observations=10 5,395 −42%
compact=1 3,152 −66%
compact=1&prompts=-1 1,846 −80%
compact=1&prompts=-1&observations=10 1,039 −89%

End to end, running the hook scripts themselves against that database: 9,404 B → 3,179 B per session start.

That last combo lands at −89%, which reproduces @todie's −88% from #161/#162 on an install roughly 15× larger — the win holds as history grows, because every section is a fixed row count regardless of database size.

🤔 The default I chose, and why it's conservative

The hook commit asks only for compact=1&pinned=20:

  • compact=1 drops the 300-char body preview from observation bullets. No row disappears — every type and title still renders, and the body is one mem_get_observation away when the agent actually wants it. That alone is −66%.
  • pinned=20 gives the blob a real ceiling. Without it there is still no hard cap, since a user who pins 40 observations to make context better silently makes the injection several times larger.

I deliberately did not default prompts=-1, even though it's another −14%: your v1.19.0 notes describe the context block as "load-bearing", so which sections earn their bytes is your call, not mine. The table above is there so you can pick a stronger default in one line if you want it.

🙏 Credit

The ContextOptions + thin-wrapper shape is @todie's design from #162, rebased onto current main and credited with Co-authored-by: on every commit. #161/#162 were closed on process, not merit; @todie filed first, did the original measurement, and hasn't been active since April. This picks it up and extends it to what #162 predates: the hardcoded RecentPrompts(project, 10) and the unbounded pinned section from #484. If @todie wants to take it back, I'll close this.

✅ Verification

Run locally on this branch, plus a clean upstream/main worktree as a baseline for comparison:

  • go test ./... — 22 packages ok, 0 failures, identical to the baseline run
  • go test -tags e2e ./internal/server/... — ok
  • gofmt -l clean · go vet -tags e2e ./internal/server/... clean
  • Hook scripts checked with bash -n and executed against an isolated daemon (ENGRAM_DATA_DIR pointed at a snapshot, never the live database)

📋 Note for review

The three commits are meant to be separable: store (mechanism), server (transport), hooks (the only behavior change). Happy to reshape any of it — including switching to a hard server-side ceiling that no caller can exceed, if you'd rather the bound not be up to the caller at all.

Summary by CodeRabbit

  • New Features
    • Enhanced GET /context to support query-driven, per-section caps for observations, prompts, sessions, and pinned items, plus a compact mode to reduce inline preview content.
    • Numeric values follow sign-based behavior: 0/invalid values use legacy defaults, >0 caps results, and <0 omits the section (including its header).
  • Improvements
    • Session startup and post-compaction context retrieval now request compact context with up to 20 pinned items.
  • Documentation
    • Updated GET /context API documentation for the expanded query interface and defaulting/error behavior.
  • Tests
    • Added end-to-end and unit test coverage for the new query parameters and formatting behavior.

Neuralab Marketing and others added 3 commits July 30, 2026 10:31
FormatContext renders four fixed-size sections into the SessionStart blob:
sessions (5), prompts (10, hardcoded), unpinned observations
(cfg.MaxContextResults) and pinned observations, which have had no LIMIT at
all since Gentleman-Programming#484. None of it is reachable from outside the store.

Adds ContextOptions{Observations, Prompts, Sessions, Pinned, Compact} and
FormatContextWithOptions. Each int field follows one convention: 0 keeps that
section's legacy default, >0 caps it, <0 omits the section and its header.
Compact drops the 300-char body preview from observation-shaped bullets.

FormatContext keeps its signature and becomes a thin wrapper over a zero-value
ContextOptions, so its four callers (cmd, server, mcp) are untouched and the
output is byte-identical -- asserted by a dedicated test. PinnedObservations
also keeps its exported signature and delegates to an unexported helper that
adds SQL LIMIT only when a positive cap is requested.

Refs Gentleman-Programming#163

Co-authored-by: todie <7841714+todie@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtshktvJgt8fVkBr5ZjT1J
handleContext now builds a store.ContextOptions from observations, prompts,
sessions, pinned and compact, reusing the existing queryInt/queryBool helpers:
an absent, empty or unparseable param falls back to the zero value, so garbage
input keeps returning 200 with the legacy blob instead of a 4xx.

Negative values are passed through deliberately -- they are how a caller omits
a section entirely -- unlike the 'err == nil && n > 0' guard in Gentleman-Programming#162, which
would have silently dropped them.

Refs Gentleman-Programming#163

Co-authored-by: todie <7841714+todie@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtshktvJgt8fVkBr5ZjT1J
Both session-start.sh and post-compaction.sh now fetch the context with
compact=1&pinned=20. compact drops the 300-char body preview from observation
bullets while keeping every row and title -- the body is one
mem_get_observation away -- and pinned=20 puts a ceiling on the only section
that never had one.

Measured end to end by running the hooks against a 2,976-observation project:
9,404 B -> 3,179 B injected per session start (-66%), with no row dropped.

This is the only commit in the series that changes observable behavior; the
store and server commits are mechanism with byte-identical defaults. Drop this
one if you would rather choose the numbers yourself.

Refs Gentleman-Programming#163

Co-authored-by: todie <7841714+todie@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtshktvJgt8fVkBr5ZjT1J
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9cf896e2-7b2f-41c4-9810-abd7cdfddfeb

📥 Commits

Reviewing files that changed from the base of the PR and between 4cb4978 and 7b3a09d.

📒 Files selected for processing (3)
  • DOCS.md
  • internal/store/store.go
  • internal/store/store_test.go

📝 Walkthrough

Walkthrough

The /context endpoint now accepts per-section limits, omission values, and compact rendering. Store formatting preserves legacy defaults, while Claude hooks request compact context with up to 20 pinned items. Tests cover endpoint and formatting behavior.

Changes

Context options and retrieval

Layer / File(s) Summary
Store context formatting
internal/store/store.go, internal/store/store_test.go
Adds ContextOptions, configurable section retrieval, pinned limits, compact observation rendering, and compatibility tests for defaults, caps, omissions, errors, and output size.
Context endpoint query handling
internal/server/server.go, internal/server/server_e2e_test.go, DOCS.md
Maps /context query parameters to store options, documents defaulting and omission semantics, and tests default, compact, capped, omitted, and invalid-parameter behavior.
Claude hook context requests
plugin/claude-code/scripts/post-compaction.sh, plugin/claude-code/scripts/session-start.sh
Requests compact context with pinned=20 while preserving existing context extraction behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ClaudeHook
  participant ContextEndpoint
  participant Store
  ClaudeHook->>ContextEndpoint: GET /context?compact=1&pinned=20
  ContextEndpoint->>Store: FormatContextWithOptions(ContextOptions)
  Store-->>ContextEndpoint: Compact bounded context
  ContextEndpoint-->>ClaudeHook: .context response
Loading

Suggested labels: type:feature

Suggested reviewers: alan-thegentleman, gentleman-programming

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Core context bounding and compact hook updates are implemented, but #163 also required protocol text replacement and mixed-version fallback, which are missing. Add the concise SessionStart protocol text, hook-only fallback for older server/plugin versions, and the environment-tunable limit path described in #163.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: bounding SessionStart context injection.
Out of Scope Changes check ✅ Passed The changes stay focused on /context formatting, hook injection, tests, and docs, with no unrelated feature work.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/server/server.go`:
- Around line 784-790: Update the API specification for the GET /context
endpoint to document the observations, prompts, sessions, pinned, and compact
query parameters, matching the names and default behavior used by the
ContextOptions initialization. Keep the handler and tests unchanged.

In `@internal/store/store_test.go`:
- Around line 8846-9089: Add error-path subtests for FormatContextWithOptions
that force each section fetch—Sessions, Prompts, Observations, and Pinned—to
fail, such as by closing or otherwise invalidating the store database before
invocation. Verify the method returns a non-nil error for every failure path,
while keeping the existing happy-path and option-isolation tests unchanged.

In `@internal/store/store.go`:
- Around line 3436-3460: Extract the duplicated observation bullet formatting
from the pinned and recent-observation loops into a shared helper or local
rendering function, preserving the existing Compact and full output—including
truncate(obs.Content, 300). Update both loops to reuse that single rendering
path while keeping their section-specific slices and headings unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: eb1ef138-05e2-4141-906b-e938013dfbe8

📥 Commits

Reviewing files that changed from the base of the PR and between 509e676 and 4cb4978.

📒 Files selected for processing (6)
  • internal/server/server.go
  • internal/server/server_e2e_test.go
  • internal/store/store.go
  • internal/store/store_test.go
  • plugin/claude-code/scripts/post-compaction.sh
  • plugin/claude-code/scripts/session-start.sh

Comment thread internal/server/server.go
Comment on lines +784 to +790
opts := store.ContextOptions{
Observations: queryInt(r, "observations", 0),
Prompts: queryInt(r, "prompts", 0),
Sessions: queryInt(r, "sessions", 0),
Pinned: queryInt(r, "pinned", 0),
Compact: queryBool(r, "compact", false),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: check whether docs mention the new /context query params.
rg -n -i 'context.*(observations|pinned|compact)=' --glob '*.md'
fd -i 'api|http' --extension md

Repository: Gentleman-Programming/engram

Length of output: 166


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | sed -n '1,200p' | rg -i '(^README|api|http|server|route|handler|test)' || true

echo
echo "== internal/server/server.go relevant lines =="
sed -n '740,830p' internal/server/server.go | nl -ba -v740

echo
echo "== route registration mentioning context =="
rg -n 'context|Context' internal/server -g '*.go'

echo
echo "== markdown files =="
git ls-files '*.md'

echo
echo "== broad /context mentions =="
rg -n -i '/context|\/context|context\s+queries|context.*query|query.*context|ContextOptions|observations|prompts|sessions|pinned|compact' -g '*.md' -g '*.go' .

Repository: Gentleman-Programming/engram

Length of output: 2551


🏁 Script executed:

#!/bin/bash
set -u

echo "== context query handling and route registration =="
rg -n 'observations|prompts|sessions|pinned|compact|ContextOptions|/context|context' internal/server internal -g '*.go' | head -200

echo
echo "== markdown files in repo =="
find . -maxdepth 4 -type f -iname '*.md' -print | sed 's#^\./##' | sort

echo
echo "== docs/search for context query parameters in markdown files only =="
find . -maxdepth 4 -type f -iname '*.md' -print | xargs -r rg -n -i 'observations|prompts|sessions|pinned|compact|context' || true

Repository: Gentleman-Programming/engram

Length of output: 50385


Document the new /context query parameters.

GET /context now accepts observations, prompts, sessions, pinned, and compact; the API spec needs the same update as the handler/test changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/server/server.go` around lines 784 - 790, Update the API
specification for the GET /context endpoint to document the observations,
prompts, sessions, pinned, and compact query parameters, matching the names and
default behavior used by the ContextOptions initialization. Keep the handler and
tests unchanged.

Source: Path instructions

Comment thread internal/store/store_test.go
Comment thread internal/store/store.go
… paths, document params

- Extract the Compact/full bullet rendering shared by the Pinned and Recent
  Observations loops into writeObservationBullet, so a future formatting change
  (truncate length, redaction) lands in one place instead of two copies that
  can drift apart.
- Add TestFormatContextWithOptionsErrorBranches: four subtests that close the
  store before invoking, each using the option convention to skip the earlier
  sections so every 'return "", err' branch is reached (sessions, pinned,
  observations, prompts).
- Document the five query params on GET /context in DOCS.md, including the
  0 / >0 / <0 convention and that invalid values fall back to the default
  rather than returning 400.

Output is unchanged: the byte-identity test still passes untouched.

Refs Gentleman-Programming#163

Co-authored-by: todie <7841714+todie@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtshktvJgt8fVkBr5ZjT1J
@neuralabmarketing

Copy link
Copy Markdown
Author

Addressed all three review comments in 7b3a09d:

  • Duplicated bullet rendering — extracted to writeObservationBullet, shared by both loops. Fair catch: the two copies were byte-identical, so a future change to truncate(obs.Content, 300) or any redaction would have landed in one section only.
  • Error paths untested — added TestFormatContextWithOptionsErrorBranches, four subtests that close the store first and use the option convention to skip the earlier sections ({Sessions: -1, Pinned: -1} to reach the observations branch, and so on), so each of the four return "", err is actually exercised.
  • Undocumented paramsDOCS.md now lists all five on GET /context, with the 0 / >0 / <0 convention and the fact that invalid values fall back to the default instead of returning 400.

Output is unchanged — the byte-identity test still passes without being touched. go test ./... (22 packages) and go test -tags e2e ./internal/server/... both green.

On the inconclusive Linked Issues check: it's right that this PR doesn't implement the env-tunable limits described in #163's original writeup. That's deliberate — @Alan-TheGentleman's approval narrowed the scope to "a bounded-size injection. The scope is the size cap", and I offered in that thread to build a hard server-side ceiling instead of (or on top of) per-caller params if that's the shape he prefers. I'd rather wait for that call than widen the diff on a guess.

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.

perf: SessionStart hook injects ~10 KB (~2.5k tokens) into every Claude Code session

1 participant