---
phase: 20-session-recall
plan: 02
type: execute
wave: 2
depends_on: ["20-01"]
files_modified:
  - src/ztlctl/services/recall.py
  - src/ztlctl/actions/_session.py
  - src/ztlctl/mcp/resources.py
  - tests/services/test_recall.py
  - tests/mcp/test_resources.py
autonomous: true
requirements:
  - RECL-03
  - RECL-04
  - RECL-05

must_haves:
  truths:
    - "RecallService.recall_topology returns session pairs that share notes or tags"
    - "MCP resource ztlctl://sessions/recent returns last 5 sessions with summaries"
    - "recall_temporal, recall_topic, and recall_topology are registered ActionDefinitions in the session category"
  artifacts:
    - path: "src/ztlctl/services/recall.py"
      provides: "recall_topology method added to RecallService"
      contains: "def recall_topology"
    - path: "src/ztlctl/actions/_session.py"
      provides: "3 recall ActionDefinitions registered alongside existing session actions"
      contains: "recall_temporal"
    - path: "src/ztlctl/mcp/resources.py"
      provides: "sessions_recent_impl + ztlctl://sessions/recent resource registration"
      contains: "ztlctl://sessions/recent"
  key_links:
    - from: "src/ztlctl/actions/_session.py"
      to: "src/ztlctl/controllers/recall.py"
      via: "handler lambda calling RecallController"
      pattern: "RecallController.*recall_temporal"
    - from: "src/ztlctl/mcp/resources.py"
      to: "src/ztlctl/services/recall.py"
      via: "sessions_recent_impl imports RecallService"
      pattern: "RecallService.*recall_temporal"
---

<objective>
Add topology recall, MCP resource, and ActionRegistry registration to complete session recall.

Purpose: Complete RECL-03 (topology), RECL-04 (MCP resource), and RECL-05 (all 3 actions registered) so the full recall feature is usable via CLI, MCP, and programmatic API.
Output: recall_topology method, ztlctl://sessions/recent resource, 3 registered ActionDefinitions.
</objective>

<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/20-session-recall/20-CONTEXT.md
@.planning/phases/20-session-recall/20-01-SUMMARY.md

<interfaces>
<!-- From Plan 01 outputs -->

From src/ztlctl/services/recall.py (created in Plan 01):
```python
class RecallService(BaseService):
    def recall_temporal(self, *, from_date: str | None = None, to_date: str | None = None) -> ServiceResult: ...
    def recall_topic(self, query: str) -> ServiceResult: ...
    # recall_topology to be added in this plan
```

From src/ztlctl/controllers/recall.py (created in Plan 01):
```python
class RecallController(BaseController):
    def recall_temporal(self, *, from_date: str | None = None, to_date: str | None = None) -> ServiceResult: ...
    def recall_topic(self, query: str) -> ServiceResult: ...
    def recall_topology(self, *, limit: int = 10) -> ServiceResult: ...  # stub from Plan 01
```

From src/ztlctl/actions/_session.py (existing pattern):
```python
def _register_session_actions() -> None:
    from ztlctl.actions.definitions import ActionDefinition, ActionParam
    from ztlctl.actions.registry import get_action_registry
    from ztlctl.controllers.session import SessionController
    registry = get_action_registry()
    registry.register(ActionDefinition(
        name="start", category="session", ...
        handler=lambda vault, **kw: SessionController(vault).start(**kw),
    ))
```

From src/ztlctl/mcp/resources.py (existing patterns):
```python
_RESOURCE_CATALOG: tuple[dict[str, str], ...] = (
    {"uri": "ztlctl://context", "description": "..."},
    ...
)

def overview_impl(vault: Any) -> dict[str, Any]:
    from ztlctl.services.query import QueryService
    svc = QueryService(vault)
    ...

def register_resources(server: Any, vault: Any) -> None:
    @server.resource("ztlctl://overview")  # type: ignore[untyped-decorator]
    def overview_resource() -> str:
        import json
        return json.dumps(overview_impl(vault), indent=2)
```

From src/ztlctl/infrastructure/database/schema.py:
```python
nodes = Table("nodes", metadata,
    Column("session", Text),  # LOG-NNNN — links notes to sessions
    ...
)
node_tags = Table("node_tags", metadata,
    Column("node_id", Text, ForeignKey("nodes.id")),
    Column("tag", Text),
    ...
)
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: recall_topology method, MCP resource, and action registration</name>
  <files>src/ztlctl/services/recall.py, src/ztlctl/actions/_session.py, src/ztlctl/mcp/resources.py, tests/services/test_recall.py, tests/mcp/test_resources.py</files>
  <read_first>
    src/ztlctl/services/recall.py
    src/ztlctl/controllers/recall.py
    src/ztlctl/actions/_session.py
    src/ztlctl/mcp/resources.py
    tests/services/test_recall.py
    tests/mcp/test_resources.py
  </read_first>
  <behavior>
    - recall_topology returns pairs of sessions that share notes (notes with session column matching multiple session IDs) with shared_notes list
    - recall_topology returns pairs that share tags (tags used by notes in both sessions) with shared_tags list
    - recall_topology with limit parameter caps the number of pairs returned
    - recall_topology returns empty list when no sessions share content (ok=True, data.pairs=[])
    - sessions_recent_impl returns last 5 sessions with session_id, topic, start/end timestamps, note_count, log_entry_count
    - sessions_recent_impl returns empty list when no sessions exist
    - recall_temporal, recall_topic, recall_topology are discoverable via get_action_registry().get("recall_temporal") etc.
  </behavior>
  <action>
    **1. Add recall_topology to RecallService (src/ztlctl/services/recall.py):**

    `recall_topology(self, *, limit: int = 10) -> ServiceResult`:
    - Query all session IDs from nodes WHERE type='log'
    - For each session, collect note IDs (nodes WHERE session=session_id AND type IN ('note','reference','task'))
    - For each session, collect tags via node_tags JOIN on the session's note IDs
    - Build pairs: for each combination of 2 sessions, compute shared_notes (intersection of note IDs that appear in both sessions via the session column — actually, per CONTEXT.md, shared notes = notes created/modified in multiple sessions, so query nodes WHERE session IN (session_a, session_b) — but a note can only have one session column value; instead, look at notes that were *referenced* across sessions via session_logs.references JSON array)
    - CORRECTION: Per CONTEXT.md "shared note references across sessions (notes created/modified in multiple sessions)". Since nodes.session is a single value, "shared" means notes that are *referenced* in log entries of multiple sessions. Query session_logs.references (JSON array) for each session, parse references, find IDs appearing in both sessions' log references.
    - Also compute shared_tags: tags used by notes in session A intersected with tags used by notes in session B
    - Sort pairs by total shared items (len(shared_notes) + len(shared_tags)) descending, limit to `limit`
    - Return ServiceResult(ok=True, op="recall_topology", data={"pairs": [...], "count": N})
    - Each pair: {session_a, session_b, shared_notes: [...], shared_tags: [...]}
    - Use @traced decorator

    **2. Add sessions_recent_impl to resources.py:**

    `sessions_recent_impl(vault: Any) -> dict[str, Any]`:
    - Lazy import RecallService
    - Call recall_temporal() with no date filters
    - Sort by started descending, take first 5
    - Return compact dict: {"sessions": [...], "count": N}
    - Add "ztlctl://sessions/recent" to _RESOURCE_CATALOG tuple
    - Add register_resources entry with @server.resource decorator

    **3. Register 3 recall ActionDefinitions in _session.py:**

    Add to _register_session_actions() (at the end, after existing session actions):

    - `recall_temporal`: category="session", params=(ActionParam("from_date", str, required=False), ActionParam("to_date", str, required=False)), handler=lambda vault, **kw: RecallController(vault).recall_temporal(**kw), side_effect="read", cli_group="session", cli_name="recall-temporal"
    - `recall_topic`: category="session", params=(ActionParam("query", str, required=True, cli_is_argument=True),), handler=lambda vault, **kw: RecallController(vault).recall_topic(**kw), side_effect="read", cli_group="session", cli_name="recall-topic"
    - `recall_topology`: category="session", params=(ActionParam("limit", int, required=False, default=10),), handler=lambda vault, **kw: RecallController(vault).recall_topology(**kw), side_effect="read", cli_group="session", cli_name="recall-topology"

    Add lazy import for RecallController alongside SessionController import.

    **4. Add tests:**

    Add topology tests to tests/services/test_recall.py:
    - Create 2 sessions, add log entries with references to shared note IDs, verify recall_topology returns the pair
    - Test with shared tags: create notes with tags in different sessions, verify shared_tags
    - Test empty topology (no shared content)

    Add MCP resource test to tests/mcp/test_resources.py (or create if needed):
    - Test sessions_recent_impl returns correct structure
  </action>
  <verify>
    <automated>cd /Users/shparki/Documents/Workspace/thatdev/ztlctl && uv run pytest tests/services/test_recall.py tests/mcp/test_resources.py -x -v -k "recall or sessions_recent"</automated>
  </verify>
  <acceptance_criteria>
    - grep -q "def recall_topology" src/ztlctl/services/recall.py
    - grep -q "sessions_recent_impl" src/ztlctl/mcp/resources.py
    - grep -q "ztlctl://sessions/recent" src/ztlctl/mcp/resources.py
    - grep -q "recall_temporal" src/ztlctl/actions/_session.py
    - grep -q "recall_topic" src/ztlctl/actions/_session.py
    - grep -q "recall_topology" src/ztlctl/actions/_session.py
    - grep -q "RecallController" src/ztlctl/actions/_session.py
  </acceptance_criteria>
  <done>recall_topology discovers session connectivity via shared references and tags. ztlctl://sessions/recent MCP resource returns last 5 sessions. All 3 recall actions registered in ActionRegistry under session category. All tests pass.</done>
</task>

<task type="auto">
  <name>Task 2: Full validation and REQUIREMENTS.md update</name>
  <files>.planning/REQUIREMENTS.md</files>
  <read_first>
    .planning/REQUIREMENTS.md
    src/ztlctl/services/recall.py
    src/ztlctl/controllers/recall.py
    src/ztlctl/actions/_session.py
    src/ztlctl/mcp/resources.py
  </read_first>
  <action>
    1. Run full test suite: `uv run pytest` — all tests must pass
    2. Run linting: `uv run ruff check .` — must be clean
    3. Run formatting: `uv run ruff format --check .` — must be clean
    4. Run type checking: `uv run mypy src/` — must pass

    5. Verify all 3 recall actions are registered:
       - `uv run python -c "from ztlctl.actions import get_action_registry; r = get_action_registry(); print([a.name for a in r.list_actions() if 'recall' in a.name])"`
       - Must print recall_temporal, recall_topic, recall_topology

    6. Update .planning/REQUIREMENTS.md:
       - Mark RECL-01 through RECL-05 as complete (change `- [ ]` to `- [x]`)
       - Update traceability table: change Status from "Pending" to "Complete" for RECL-01 through RECL-05
  </action>
  <verify>
    <automated>cd /Users/shparki/Documents/Workspace/thatdev/ztlctl && uv run pytest -x && uv run ruff check . && uv run mypy src/</automated>
  </verify>
  <acceptance_criteria>
    - grep -q "\[x\] \*\*RECL-01\*\*" .planning/REQUIREMENTS.md
    - grep -q "\[x\] \*\*RECL-02\*\*" .planning/REQUIREMENTS.md
    - grep -q "\[x\] \*\*RECL-03\*\*" .planning/REQUIREMENTS.md
    - grep -q "\[x\] \*\*RECL-04\*\*" .planning/REQUIREMENTS.md
    - grep -q "\[x\] \*\*RECL-05\*\*" .planning/REQUIREMENTS.md
  </acceptance_criteria>
  <done>All RECL requirements marked complete. Full test suite passes. Linting, formatting, and type checking all clean.</done>
</task>

</tasks>

<verification>
```bash
cd /Users/shparki/Documents/Workspace/thatdev/ztlctl
uv run pytest -x
uv run ruff check .
uv run ruff format --check .
uv run mypy src/
uv run python -c "from ztlctl.actions import get_action_registry; r = get_action_registry(); print([a.name for a in r.list_actions() if 'recall' in a.name])"
```
</verification>

<success_criteria>
- recall_topology returns session pairs with shared notes and tags
- ztlctl://sessions/recent MCP resource returns last 5 sessions
- All 3 recall actions registered in ActionRegistry
- Full test suite passes, ruff clean, mypy clean
- RECL-01 through RECL-05 marked complete in REQUIREMENTS.md
</success_criteria>

<output>
After completion, create `.planning/phases/20-session-recall/20-02-SUMMARY.md`
</output>
