Skip to content

fix(memory): scope TrafficLearner's SQLite reads to its user_id - #4850

Open
chopratejas wants to merge 4 commits into
mainfrom
fix/traffic-learner-user-scope
Open

chopratejas wants to merge 4 commits into
mainfrom
fix/traffic-learner-user-scope

Conversation

@chopratejas

@chopratejas chopratejas commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

Description

TrafficLearner saves its rows under its configured user_id, but its two direct SQLite reads ignored user_id:

  • _hydrate_persisted_state (startup) loaded every user's traffic_learner rows into the dedup maps, then applied LIMIT dedup_window. Bob's learner could bind a pattern hash to Alice's row and later bump her evidence instead of creating his own row. Other users' rows also used up his window.
  • _load_persisted_patterns_from_sqlite (used by _collect_all_patterns when writing learned context files) returned every user's patterns, so Alice's learned text ended up in Bob's files.

Both queries now filter on user_id = ?. In hydration the filter runs before the LIMIT. Only matters when several user IDs share one memory database; a single-user setup (user_id="default") is unchanged.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Changes Made

  • headroom/memory/traffic_learner.py: WHERE user_id = ? in both reads. _load_persisted_patterns_from_sqlite(db_path, user_id) takes the user explicitly (private helper; its only caller is updated).
  • tests/test_memory/test_traffic_learner.py: new TestUserScopedPersistence, which checks that hydration leaves another user's row alone and collection excludes other users. Hand-built test schemas gain the user_id column that the real memories table has, and direct helper calls pass "default".

Testing

  • Unit tests pass (pytest)
  • Linting passes (ruff check .)
  • Type checking passes (mypy headroom)
  • New tests added for new functionality
  • Manual testing performed

Test Output

# new tests against origin/main (c21b0813)
$ pytest tests/test_memory/test_traffic_learner.py -k TestUserScopedPersistence
E   assert ['Alice-only ...ge workflow."] == ["Bob's package workflow."]
FAILED ...::TestUserScopedPersistence::test_hydration_ignores_other_users_rows
FAILED ...::TestUserScopedPersistence::test_collected_patterns_exclude_other_users
====================== 2 failed, 236 deselected in 2.76s =======================

# this branch
$ pytest tests/test_memory/test_traffic_learner.py
============================= 238 passed in 18.28s =============================
$ pytest tests/test_openai_responses_traffic_learner.py tests/test_learn/test_worktree_merge.py tests/test_learn/test_section_lifecycle.py tests/test_memory/test_learn_flag.py
============================== 47 passed in 4.81s ==============================
$ ruff check / ruff format --check (0.17.3): All checks passed!
$ mypy headroom/memory/traffic_learner.py --ignore-missing-imports
Success: no issues found in 1 source file

Real Behavior Proof

  • Environment: macOS, Python 3.12. One SQLite memory.db holding a traffic_learner row for alice and one for bob.
  • Exact command / steps: TrafficLearner(backend=<backend pointing at that db>, user_id="bob")._collect_all_patterns().
  • Observed result:
origin/main:  bob collects: ['Alice-only procedure.', "Bob's preference."]
this branch:  bob collects: ["Bob's preference."]
  • Not tested: a full proxy run with --learn --memory and several users.

Runtime Rollout Safety

  • Rollout-managed feature(s): none
  • Minimum rollout channel: n/a
  • Stable/default behavior changed: see Description
  • Kill switch / disable path: none needed
  • Unsafe override required: no
  • Qualification impact: none
  • Rollback path: revert this PR

Review Readiness

  • I have performed a self-review
  • This PR is ready for human review

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I did not edit CHANGELOG.md — it is generated by release-please from my Conventional Commit PR title (a CI guard enforces this)

Screenshots (if applicable)

N/A

Additional Notes

Rows written before this change already carry the correct user_id (the save path always set it), so no migration is needed.

🤖 Generated with Claude Code


Devin Review

Reported by @JoaoMarcos44. Thank you for your contribution! 🩵

TrafficLearner saves rows under its configured user_id, but both direct
SQLite reads (startup hydration and the persisted-pattern collector) read
every user's traffic_learner rows. With several user IDs sharing one
memory.db, one user's learned patterns ended up in another user's learned
context, and hydration could bind a hash to another user's row and bump
its evidence. Both queries now filter by user_id, before the hydration
LIMIT.

Assay-Kind: bugfix
Assay-Ask: TrafficLearner read and updated other users' learned patterns when several user IDs
 shared one memory database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chopratejas chopratejas added bug Something isn't working size: small At most 100 added/deleted lines. risk: low Scope estimate: documentation/tests-only change; verification still required. area: memory Touches memory files; triage routing, not a CODEOWNERS assignment. labels Nov 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR governance

This PR does not yet satisfy the required template fields:

  • Fill in Real Behavior Proof → Observed result.

Please update the PR body, or move the PR back to draft while it is still in progress.

@github-actions github-actions Bot added status: needs author action Pull request body or readiness checklist still needs author updates status: ci failing Required or reported CI checks are failing labels Nov 13, 2026

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@JerrettDavis JerrettDavis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No defect found in the user-scoping change. Independent SQLite checks with rows for Alice, Bob and default confirm hydration respects the configured user before LIMIT 1 and collection returns only that user’s patterns; a quoted unknown user returns none. These checks pass on this head and reproduce the leakage on recorded main. This was isolated method validation, not a full pytest run. Qualification remains pending: the shared workflow audit retains an inherited gitleaks integrity hold, and the staged Security run reports Rust advisory and pip-audit failures whose logs were not available for diagnosis. These are separate from the reviewed code change.

Picks up the dependency updates from #4835 so the audit checks pass.

Assay-Kind: deps
Assay-Ask: The dependency audit checks failed on this PR because the branch predates the dependency
 updates on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot removed the status: ci failing Required or reported CI checks are failing label Nov 13, 2026

@JerrettDavis JerrettDavis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The updated head passes four independently executed isolated SQLite checks: hydration filters by user before LIMIT, collection excludes other users, quoted user IDs remain bound parameters, and Bob creates his own evidence row for content already saved by Alice. The same checks fail on recorded main. No defect found in the code change; these checks do not constitute the full pytest suite or a live proxy run. Security now passes. CI is red only on commitlint (run 43519004770, job 132208203465); please provide its failing annotations/log and resolve any reported violation before qualification. The cause is not established by the staged check metadata. The inherited gitleaks installer integrity hold remains separate from this PR.

@JerrettDavis JerrettDavis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The remaining CI failure is now diagnosed: commitlint rejects merge commit 457cceb because its Assay-Ask footer is 116 characters, over the 100-character limit (footer-max-line-length). See failed commitlint job. Please wrap that footer and ensure commitlint passes for the PR commit range. For example:

Assay-Ask: The dependency audit checks failed because this branch predates
 the dependency updates on main.

No code defect found in the user-scoping change. The prior independent SQLite checks pass at this identical head and reproduce the leakage on recorded main; those results and source coverage were reused. Full pytest and a live proxy run were not executed locally. The inherited workflow provenance hold remains separate and does not explain this PR-specific commit-message failure.

@JerrettDavis JerrettDavis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The previously reported commitlint failure remains after merging main: the current run still rejects commit 457cceb because its Assay-Ask footer is 116 characters (limit 100). That commit remains in the PR range. Please wrap the footer and obtain passing commitlint for the complete PR commit range.

No additional defect found in the current user-scoping delta. Current hosted Python checks and Security report success; no new local runtime tests were executed. The inherited workflow integrity hold remains separate from this PR-specific failure.

@JerrettDavis JerrettDavis added size: medium 101–500 added/deleted lines. status: ci failing Required or reported CI checks are failing status: code changes requested and removed size: small At most 100 added/deleted lines. labels Nov 14, 2026
@chopratejas

Copy link
Copy Markdown
Collaborator Author

Jerrett, the commitlint blocker is cleared. The footer-length rule fix (#4868) is merged into main, and I merged main into this branch, so commitlint now passes on this head. No code changes in this update.

@JerrettDavis
JerrettDavis force-pushed the fix/traffic-learner-user-scope branch from 7070d36 to 7d7b676 Compare November 14, 2026 05:22
@github-actions github-actions Bot removed the status: ci failing Required or reported CI checks are failing label Nov 14, 2026
@JerrettDavis

Copy link
Copy Markdown
Collaborator

Applied the requested mechanical fix: rewrote the PR-range commits to wrap the oversized Assay-Ask footer(s) to under the 100-character commitlint limit (content preserved, wrapped at word boundaries). Branch force-pushed; commitlint and CI are re-running at the new head.

@github-actions github-actions Bot added the status: ci failing Required or reported CI checks are failing label Nov 14, 2026

This branch was successfully deployed

1 active deployment
Preview — 7d7b6766 Deployed Nov 14, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: memory Touches memory files; triage routing, not a CODEOWNERS assignment. bug Something isn't working risk: low Scope estimate: documentation/tests-only change; verification still required. size: medium 101–500 added/deleted lines. status: ci failing Required or reported CI checks are failing status: code changes requested status: needs author action Pull request body or readiness checklist still needs author updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants