Skip to content

feat(memory): add --timeline + list_projects to recall CLI#80

Merged
gupsammy merged 3 commits into
mainfrom
feat/recall-timeline-list-projects
Jun 11, 2026
Merged

feat(memory): add --timeline + list_projects to recall CLI#80
gupsammy merged 3 commits into
mainfrom
feat/recall-timeline-list-projects

Conversation

@gupsammy

Copy link
Copy Markdown
Owner

Summary

  • feat(memory): add --timeline + list_projects to recall CLI

Changes

  • .claude-plugin/marketplace.json
  • README.md
  • plugins/claude-memory/.claude-plugin/plugin.json
  • plugins/claude-memory/README.md
  • plugins/claude-memory/skills/recall-conversations/scripts/list_projects.py
  • plugins/claude-memory/skills/recall-conversations/scripts/recent_chats.py

Commits

  • 87bc1e4 feat(memory): add --timeline + list_projects to recall CLI

Diff Stat

.claude-plugin/marketplace.json                    |   2 +-
 README.md                                          |   2 +-
 plugins/claude-memory/.claude-plugin/plugin.json   |   2 +-
 plugins/claude-memory/README.md                    |   2 +-
 .../recall-conversations/scripts/list_projects.py  | 126 ++++++++++++++++++++
 .../recall-conversations/scripts/recent_chats.py   | 128 +++++++++++++++++++++
 6 files changed, 258 insertions(+), 4 deletions(-)

recent_chats.py: --timeline mode — uncapped, body-less, oldest-first session index (per-row project_path, exchange/tool counts, title). Bypasses the 50-result cap so a project's full arc can be reconstructed.

list_projects.py: resolve project name -> path + key + session counts + date span (--match TOKEN). Disambiguates non-unique project names (e.g. two 'EzyCopy').

Both additive and backward-compatible; recall-conversations unaffected. Built for the mine-origin-story skill.
@claude

claude Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @gupsammy's task in 2m 29s —— View job


PR Review: feat(memory): add --timeline + list_projects to recall CLI

Tasks

  • Gather context and read changed files
  • Review Python scripts against stdlib/safety rules
  • Check manifest version sync
  • Check skill/script conventions
  • Post inline comments on issues found
  • Summarize findings

What looks good ✅

  • from __future__ import annotations present in both new files (list_projects.py:19, recent_chats.py:11)
  • Stdlib-only — no third-party imports in either file
  • WAL mode + busy_timeout = 5000 — correctly delegated to open_db_or_exit() (cli_common.py:242-243), no direct sqlite3.connect() calls bypass it
  • No RETURNING clause — all inserts/reads use safe patterns
  • Dynamic SQL IN clause",".join("?" * len(projects)) correctly used in both get_timeline() and the existing get_recent_sessions() (recent_chats.py:206)
  • Sort direction injection-safeORDER BY f-string uses a "DESC"/"ASC" ternary constrained by argparse choices, never user input directly
  • Column introspection safe — optional column names (context_summary_col, tool_counts_col) come from PRAGMA table_info, not user input
  • Version sync correct — both plugins/claude-memory/.claude-plugin/plugin.json and .claude-plugin/marketplace.json bumped 0.8.107 → 0.8.108, README badges updated in both places
  • Architecture: the --timeline early-return pattern cleanly branches before validate_limit (which enforces the 50-cap that timeline intentionally ignores)

Should fix before merge

list_projects.py:58-59 — LIKE metacharacters not escaped in --match

User input is placed directly into a LIKE pattern via f"%{match}%". SQL _ and % in the search term are treated as wildcards, so --match my_project also matches my-project, myXproject, etc. This contradicts the "substring filter" description in the docstring and --help.

Inline comment with a suggested fix posted. Fix this →


Optional / low-priority

recent_chats.py:280-283--verbose / --summary silently ignored in --timeline mode

Both flags are registered via add_common_args and accepted by the parser, but neither affects timeline output. A user running --timeline --verbose gets no error and no extra data. Suggested fix: add a note to the --timeline help string. Inline comment posted.

recent_chats.py:212 — uncapped fetchall() on large stores

The no-LIMIT design is correct and clearly motivated. The risk is that cursor.fetchall() on a DB with thousands of sessions materialises the whole result set at once. The existing --before/--after filters are the practical mitigation, so this is worth documenting rather than blocking on. Inline comment posted.


params: list = []
if match:
sql += " WHERE p.name LIKE ? OR p.path LIKE ?"
params.extend([f"%{match}%", f"%{match}%"])

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.

SQL LIKE metacharacters in --match are unescaped: _ matches any single character, % matches any sequence. A search for --match my_project also hits my-project, myXproject, etc. — not the literal substring the docstring promises.

Suggested change
params.extend([f"%{match}%", f"%{match}%"])
sql += " WHERE p.name LIKE ? ESCAPE '\\' OR p.path LIKE ? ESCAPE '\\'"
escaped = match.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
params.extend([f"%{escaped}%", f"%{escaped}%"])

Fix this →

parser.add_argument("--timeline", action="store_true",
help="Compact UNCAPPED session index (no message bodies, no 50-limit) — "
"the triage primitive for reconstructing a project's full arc. "
"Pair with --sort-order asc for oldest-first.")

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.

--timeline silently discards --verbose and --summary. Both flags are registered by add_common_args, so they're accepted without error, but neither affects timeline output. A user who runs --timeline --verbose will get no tool breakdown despite verbose being listed in args. Either add a warning or explicitly exclude the flags from the timeline argument group.

Suggested change
"Pair with --sort-order asc for oldest-first.")
parser.add_argument("--timeline", action="store_true",
help="Compact UNCAPPED session index (no message bodies, no 50-limit) — "
"the triage primitive for reconstructing a project's full arc. "
"Pair with --sort-order asc for oldest-first. "
"Note: --verbose and --summary are ignored in timeline mode.")

params.extend(projects)

order = "DESC" if sort_order == "desc" else "ASC"
sql += f" ORDER BY b.started_at {order}" # no LIMIT — the whole arc, by design

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.

No LIMIT is intentional and documented, but cursor.fetchall() on a DB with thousands of sessions will materialise the entire result set in memory in one call. For users with large memory stores this could be slow and expensive. Consider streaming with cursor.execute(sql, params) + iterating the cursor directly, or at least documenting a soft self-protection path (e.g. --before/--after to window the range).

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87bc1e4e8d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +206 to +209
if projects:
placeholders = ",".join("?" * len(projects))
sql += f" AND p.name IN ({placeholders})"
params.extend(projects)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Filter timeline by project path, not basename

When the current CWD auto-detects a project whose basename is shared by another project, resolve_scope() returns only the name (for example ['EzyCopy']), and this new timeline query then selects every projects row with that name. The new list_projects.py doc explicitly calls out duplicate project names as expected, so recent_chats.py --timeline can silently merge unrelated project histories instead of returning the single project arc the user asked for; the timeline should carry/use a path or key filter rather than p.name IN (...) for disambiguated scopes.

Useful? React with 👍 / 👎.

@gupsammy

Copy link
Copy Markdown
Owner Author

Addressed review feedback in 77e21ab (v0.8.109):

Fixed

  • list_projects.py — escape SQL LIKE metacharacters (%, _, \) in --match with ESCAPE '\', so the filter is a literal substring match as documented. Verified: --match my_project no longer matches my-project/myXproject.
  • recent_chats.py — note in --timeline help that --verbose/--summary are ignored in that mode.

Acknowledged, not changed (out of scope for this PR)

  • Uncapped fetchall() in get_timeline() — the no-LIMIT design is intentional and documented; --before/--after are the windowing mitigation.
  • Codex P2 (name-vs-path filtering): p.name IN (...) can merge projects that share a basename. This is a pre-existing CLI-wide behavior — resolve_project() disambiguates by path but returns only the name, and the existing get_recent_sessions() uses the identical filter. Worth a dedicated follow-up that threads project keys through resolve_scope rather than a point-fix here.

@claude

claude Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @gupsammy's task in 1m 4s —— View job


Re-review after 77e21ab (v0.8.109)

Tasks

  • Gather context and understand prior review
  • Read updated files (list_projects.py, recent_chats.py)
  • Verify LIKE-escape fix in list_projects.py
  • Verify --timeline help-string fix in recent_chats.py
  • Check version bump to v0.8.109 in both manifests
  • Check for any new issues
  • Post findings

All prior must-fix and optional feedback addressed ✅

LIKE metacharacter escape — list_projects.py:57-60

The fix is correct and complete:

sql += " WHERE p.name LIKE ? ESCAPE '\\' OR p.path LIKE ? ESCAPE '\\'"
escaped = match.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
params.extend([f"%{escaped}%", f"%{escaped}%"])

Escape order is right (\\ first, then %, then _). The SQL ESCAPE '\\' resolves to a single \ character in the SQL statement, consistent with the replacement strings. --match my_project now only matches the literal string my_project, not my-project or myXproject.

--timeline help string — recent_chats.py:280-284

The note is present: "Note: --verbose and --summary are ignored in timeline mode." — exactly as suggested.

Version sync

Both plugins/claude-memory/.claude-plugin/plugin.json and .claude-plugin/marketplace.json are at 0.8.109. In sync.

No new issues introduced. The fix commit touches only the two lines it describes and adds no new code paths, imports, or logic branches.


Acknowledged / tracked separately (not blocking)

  • Uncapped fetchall() (recent_chats.py:215): intentional no-LIMIT design; --before/--after are the windowing mitigation. No change needed here.
  • Codex P2 — name-vs-path filter (recent_chats.py:207): pre-existing p.name IN (...) behavior shared with get_recent_sessions. Correctly deferred to a follow-up that threads project keys through resolve_scope.

This PR is ready to merge.

…eline rows

resolve_scope now returns a ScopeFilter(column, values): auto-detected scope
filters by path (unambiguous) so projects sharing a basename are never merged
into one arc; explicit --project keeps name-based filtering unchanged. Applies
to recent, timeline, and search. get_timeline streams off the cursor instead of
fetchall() to halve peak memory on large stores. Addresses Codex P2 + fetchall
notes on PR #80.
@gupsammy

Copy link
Copy Markdown
Owner Author

Addressed both remaining optionals in 5b9f4ef (v0.8.110):

Codex P2 — name-vs-path merge (root fix)
resolve_scope() now returns a ScopeFilter(column, values). Auto-detected scope (from CWD) filters by path — unambiguous, so two projects sharing a basename are never merged into one arc. Explicit --project NAME keeps name-based filtering, so its behavior (including --project nonexistent → zero results) is unchanged. Fixed consistently across recent, --timeline, and search (all shared the same p.name IN filter via the resolver), not just timeline.

Uncapped fetchall
get_timeline() now streams rows straight off the cursor instead of fetchall(), halving peak memory on large stores. (get_recent_sessions intentionally keeps fetchall — it reuses the cursor for per-session message queries.)

Verification

  • Injected a duplicate-named project at a different path: auto-detect --cwd returned only the path-matched arc (decoy excluded); --project NAME still spanned both; --project nonexistent → 0.
  • Full suite green: 453 passed (34 test call-sites migrated to the ScopeFilter contract).

column is always a code-chosen literal ("name"/"path"), never user input — injection-safe.

@claude

claude Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @gupsammy's task in 2m 54s —— View job


Re-review after 5b9f4ef (v0.8.110)

Tasks

  • Gather context and understand prior reviews
  • Read diff for latest commit (5b9f4ef)
  • Review ScopeFilter implementation in cli_common.py
  • Verify ScopeFilter usage in recent_chats.py and search_conversations.py
  • Verify streaming get_timeline() implementation
  • Check test coverage for ScopeFilter contract
  • Check version sync (v0.8.110 in both manifests)
  • Post findings

What looks good ✅

ScopeFilter NamedTuple — cli_common.py:23-33

Clean, typed abstraction. column is always a code-chosen literal ("name" or "path"), never user input — safe to interpolate into SQL. values are always bound as parameters. The docstring makes the injection-safety contract explicit. All three consumers (recent_chats.py:99-102, get_timeline:207-210, search_conversations.py:94-97 / 118-121) follow the identical pattern:

if scope and scope.values:
    placeholders = ",".join("?" * len(scope.values))
    sql += f" AND p.{scope.column} IN ({placeholders})"
    params.extend(scope.values)

resolve_project — path-keyed everywhere

The path-walk (step 1) returns ScopeFilter("path", [exact_path]) on an exact match — the root fix for Codex P2. The name-fallback (step 2) is also path-keyed: it looks up WHERE name = ? but then collects paths and returns ScopeFilter("path", paths). So even the fallback never re-introduces a name-based filter that could merge multiple same-named projects.

resolve_scope — explicit --project NAME stays name-based

ScopeFilter("name", names) for explicit --project is intentional and correct: the user typed a name, so filter by name. Auto-detect returns the path-keyed result from resolve_project. The two modes are clearly separated and their behavior documented in the docstring.

Streaming get_timelinerecent_chats.py:215-252

for row in cursor: after cursor.execute(sql, params) correctly streams rows without materialising the full result set. The PRAGMA table_info call earlier uses the same cursor but is exhausted via fetchall() before the main query runs — cursor reuse is safe here. No nested cursor operations in the loop (unlike get_recent_sessions, which reuses the cursor for per-session message queries and correctly calls fetchall() to materialise the outer result first). The comment at lines 217-220 clearly explains the memory rationale.

Optional-column index arithmetic

The col-based indexing is correct for all four combinations of has_context_summary × has_tool_counts — consistent with the pre-existing pattern in get_recent_sessions:111-123.

Test migration

All 34 test call-sites correctly use ScopeFilter("name", [...]) for explicit project filters. The scope=None tests continue to verify cross-project behaviour. Version sync confirmed: both plugins/claude-memory/.claude-plugin/plugin.json and .claude-plugin/marketplace.json are at 0.8.110.


No blocking issues

This PR is ready to merge.


Optional (non-blocking)

No automated test for path-vs-name disambiguation in resolve_project

The Codex P2 fix was manually verified (per the PR comment), but there's no unit test that creates two same-named projects at different paths, calls resolve_project(one_path, conn), and asserts the result is ScopeFilter("path", [one_path]) — not both. The manual verification is sufficient for this PR, but adding a test would lock in the contract against future resolve_project edits. Worth a follow-up.


@gupsammy gupsammy merged commit 1c634ac into main Jun 11, 2026
1 check passed
@gupsammy gupsammy deleted the feat/recall-timeline-list-projects branch June 11, 2026 08:02
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.

1 participant