Skip to content

Add read + action tracking#33

Merged
cmeans merged 3 commits into
mainfrom
read-action-tracking
Mar 23, 2026
Merged

Add read + action tracking#33
cmeans merged 3 commits into
mainfrom
read-action-tracking

Conversation

@cmeans
Copy link
Copy Markdown
Owner

@cmeans cmeans commented Mar 23, 2026

Summary

Two new tables and five new tools for tracking how knowledge is consumed and acted upon.

Read tracking — auto-logs when entries are accessed by get_knowledge, get_alerts, and other read tools. Fire-and-forget (never blocks responses). Query with get_reads.

Action trackingacted_on(entry_id, action, platform?, detail?, tags?) records concrete actions agents take because of entries. Tags default to the referenced entry's tags. Query with get_actions.

Unread entriesget_unread(since?) returns entries with zero reads. Cleanup candidates.

Activity feedget_activity(since?, platform?, limit?) returns combined reads + actions chronologically.

List mode enrichmentget_knowledge(mode="list") now includes read_count and last_read on each entry.

New tools (5)

Tool Type Description
acted_on write Record an action taken because of an entry
get_reads read Read history (entry_id?, since?, platform?, limit?)
get_actions read Action history (entry_id?, since?, platform?, tags?, limit?)
get_unread read Entries with zero reads — cleanup candidates
get_activity read Combined read + action feed

Schema changes

  • reads table: id, entry_id, timestamp, platform, tool_used
  • actions table: id, entry_id, timestamp, platform, action, detail, tags (JSONB + GIN)
  • Alembic migration included
  • ON DELETE CASCADE — reads/actions auto-clean when entries are deleted

QA

Prerequisites

pip install -e ".[dev]"

# Option A: Run automated tests (requires Docker for testcontainers)
pytest tests/ -v --cov=mcp_awareness

# Option B: Run a local server for manual MCP tool testing
docker compose -f docker-compose.demo.yaml up postgres -d
AWARENESS_DATABASE_URL=postgresql://awareness:mcp-awareness-demo@localhost:5432/awareness \
  mcp-awareness

Automated tests

  • 211 tests pass (15 new for read/action tracking)
  • ruff + mypy clean

Manual tests (via MCP tools)

    • Auto-logging: get_knowledge logs reads
    remember(source="qa-test", tags=["qa"], description="Read tracking test entry")
    get_knowledge(tags=["qa"])
    get_reads(limit=5)
    

    Expected: get_reads returns at least 1 read with tool_used="get_knowledge"

    • acted_on records an action with tags from entry
    # Use the entry ID from step 1
    acted_on(entry_id="<id>", action="verified during QA", platform="claude-code")
    get_actions(limit=5)
    

    Expected: action recorded with tags=["qa"] (copied from entry)

    • acted_on with custom tags
    acted_on(entry_id="<id>", action="tagged action", tags=["qa", "custom"])
    get_actions(tags=["custom"])
    

    Expected: returns the action with custom tags, filtered correctly

    • get_unread returns entries with zero reads
    remember(source="qa-test", tags=["qa"], description="Never-read entry")
    get_unread()
    

    Expected: the new entry appears (it hasn't been fetched via get_knowledge yet)

    • get_activity shows combined feed
    get_activity(limit=10)
    

    Expected: interleaved read + action events, most recent first

    • List mode includes read_count and last_read
    get_knowledge(mode="list", tags=["qa"])
    

    Expected: entries include read_count (number) and last_read (timestamp or null)

    • Cleanup
    delete_entry(tags=["qa"], confirm=true)
    

    Expected: QA entries removed. Reads/actions auto-cleaned via CASCADE.

🤖 Generated with Claude Code

Two new tables: reads (auto-logged, prunable) and actions (agent-reported,
permanent, tagged). Five new tools: acted_on, get_reads, get_actions,
get_unread, get_activity. Auto-logging in get_knowledge, get_alerts.
List mode enriched with read_count and last_read per entry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov-commenter
Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

ℹ️ You can also turn on project coverage checks and project coverage reporting on Pull Request comment

Thanks for integrating Codecov - We've got you covered ☂️

Copy link
Copy Markdown
Owner Author

@cmeans cmeans left a comment

Choose a reason for hiding this comment

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

QA Review — PR #33: Add read + action tracking

Automated tests

  • 211/211 pass (15 new for read/action tracking)
  • ruff: clean
  • mypy: clean

Manual tests (7/7 pass)

All executed via isolated MCP instance (claude -p --mcp-config --strict-mcp-config --allowedTools).

# Test Result
1 Auto-logging: get_knowledge logs reads tool_used="get_knowledge" recorded
2 acted_on inherits tags from entry tags=["qa"] copied from entry
3 acted_on with custom tags + GIN filter ✅ Custom ["qa","custom"] applied, filtered by ["custom"]
4 get_unread returns zero-read entries ✅ Only never-read entry returned
5 get_activity combined feed ✅ Interleaved reads + actions, descending timestamp
6 List mode read_count/last_read enrichment ✅ Both fields present with correct values
7 Cleanup via delete_entry ✅ Entries soft-deleted

Code review

Architecture: Clean separation — protocol in store.py, implementation in postgres_store.py, MCP wiring in server.py. Fire-and-forget _log_reads wrapper ensures read logging never breaks tool responses. Good.

Schema: Two new tables with proper FK (ON DELETE CASCADE), indexes (entry_id, timestamp, GIN on tags), and a matching Alembic migration. Migration matches the inline DDL in postgres_store.py. Good.

Docs: CHANGELOG, README (tool count 18→23, test count 196→211), data-dictionary, deployment-guide all updated. Good.

Findings

1. CASCADE vs soft delete mismatch (documentation issue)

PR body and QA step 7 both state: "Reads/actions auto-cleaned via CASCADE."

But delete_entry is a soft delete (sets deleted timestamp). ON DELETE CASCADE only fires on actual SQL DELETE. During manual testing, reads and actions persisted after delete_entry.

The CASCADE is correct for the eventual hard delete path (_cleanup_expired), but the PR description is misleading. Suggest updating:

  • PR body line 28: ON DELETE CASCADE note should mention it applies on hard delete (trash purge), not delete_entry
  • QA step 7 expected: remove "Reads/actions auto-cleaned via CASCADE" or clarify it happens at purge time

2. acted_on with invalid entry_id — unhandled FK violation

log_action in postgres_store.py does not validate that entry_id exists before INSERT. If called with a nonexistent ID, the FK constraint produces a raw psycopg exception that bubbles up through acted_on as an unstructured error. Unlike log_read (which is fire-and-forget with try/except), acted_on is an explicit user action that should return a clear error.

Suggest: catch psycopg.errors.ForeignKeyViolation (or check get_entry_by_id result) and return a structured {"status": "error", "message": "entry not found"}.

3. "and other read tools" — only 2 instrumented (minor, docs)

PR body says: "auto-logs when entries are accessed by get_knowledge, get_alerts, and other read tools."

Only get_knowledge and get_alerts call _log_reads. Other read paths (get_status, get_briefing, get_suppressions, get_patterns) do not. This is likely intentional (those are operational, not knowledge consumption), but the phrasing "and other read tools" implies broader coverage. Suggest removing that phrase or enumerating the two tools explicitly.

Verdict

Finding #1 is a documentation fix (misleading expected behavior in PR body and summary). Finding #2 is a minor robustness issue. Finding #3 is a wording nit.

None are architectural concerns — the feature is well-implemented. Requesting changes for the FK handling (#2) since it produces unstructured exceptions for a foreseeable user error. The doc fixes (#1, #3) can be addressed in the same pass.

- log_action returns structured error for nonexistent entry_id
  (prevents FK violation bubbling as unstructured exception)
- acted_on tool checks for error status before returning ok
- Added tests: store-level and server-level invalid entry_id cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cmeans
Copy link
Copy Markdown
Owner Author

cmeans commented Mar 23, 2026

Response to QA findings

All three addressed in commit 645eba3:

1. CASCADE vs soft delete mismatch (docs) — You're right, CASCADE only fires on hard delete (trash purge), not delete_entry. The PR description was misleading. I haven't edited the PR body (GitHub doesn't re-notify on body edits), but this is noted for the final docs.

2. acted_on with invalid entry_id — Fixed. log_action now validates entry_id exists via get_entry_by_id before INSERT. Returns {"status": "error", "message": "Entry not found: <id>"} for nonexistent entries. Added tests at both store and server level.

3. "and other read tools" phrasing — Acknowledged. Only get_knowledge and get_alerts are instrumented (intentional — operational reads like get_briefing and get_suppressions are not knowledge consumption). Will fix phrasing in final docs.

213 tests, all green.

Updated QA steps for finding #2

acted_on(entry_id="nonexistent-id", action="should fail")

Expected: {"status": "error", "message": "Entry not found: nonexistent-id"}

No unstructured exception, no FK violation traceback.

Copy link
Copy Markdown
Owner Author

@cmeans cmeans left a comment

Choose a reason for hiding this comment

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

QA Re-review — PR #33 (round 2)

Fix verification (commit 645eba3)

All three findings from round 1 addressed:

  1. CASCADE vs soft delete — Acknowledged. Docs note pending.
  2. acted_on invalid entry_id — ✅ Verified. Returns {"status": "error", "message": "Entry not found: nonexistent-id"}. No FK violation, no unstructured exception. Tests added at both store and server level.
  3. "and other read tools" phrasing — Acknowledged. Docs fix pending.

Automated tests

  • 213/213 pass (+2 from fix: test_acted_on_invalid_entry, test_log_action_invalid_entry_id)
  • ruff + mypy clean

Manual retest

  • acted_on(entry_id="nonexistent-id", action="should fail") → structured error ✅

Verdict

Feature is solid. All findings resolved. Ready for QA Approved label.

- CHANGELOG/data-dictionary: specify get_knowledge and get_alerts (not
  generic "read tools") as the instrumented tools
- Data dictionary: clarify CASCADE fires on hard delete (purge), not
  soft delete (delete_entry)
- Test counts: 211 → 213

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cmeans
Copy link
Copy Markdown
Owner Author

cmeans commented Mar 23, 2026

Doc fixes pushed (commit 16a47d2)

All three findings now fully addressed:

  1. "and other read tools" → CHANGELOG and data dictionary now explicitly name get_knowledge and get_alerts as the two instrumented tools
  2. Invalid entry_id → Already fixed in 645eba3 (structured error + tests)
  3. CASCADE clarification → Data dictionary lifecycle section now explains: CASCADE fires on hard delete (auto-purge), not on delete_entry (soft delete). Reads and actions persist for trashed entries until the 30-day purge.

Test counts updated to 213 across CHANGELOG and README.

213 tests, all green.

@cmeans cmeans added the QA Approved Manual QA testing completed and passed label Mar 23, 2026
@cmeans cmeans merged commit 8124fbe into main Mar 23, 2026
8 checks passed
@cmeans cmeans deleted the read-action-tracking branch March 23, 2026 18:01
@cmeans cmeans mentioned this pull request Mar 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

QA Approved Manual QA testing completed and passed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants