---
slug: doyle-fresh-ctx-after-v1-10-16
status: resolved
resolution_commit: pending
applied_fix: |
  User redirected scope: keep wrapper cwd at psyches/tracked/ (Phase 25 will need
  Psyche to write under both tracked/projects/ and tracked/agents). Only psyche.md
  changed — relative path becomes `agents/{{self_id}}/live_context.md`.
  Five `{{self_id}}.md` references updated; cargo build --release clean.
  Recovery of already-corrupted agent live_context.md files via
  `git checkout <last-rich-commit> -- live_context.md` in each agent worktree
  is left as an operator step.
trigger: |
  after switching to the v1.10.16 (phase 24 output) format at `$SPT_HOME/psyches/tracked`, doyle's next `/live` invocation claimed it had fresh context. root cause and propose a fix
created: 2026-05-20
updated: 2026-05-21
---

# Debug: doyle /live reports fresh context after v1.10.16 phase 24 tracked-psyches format switch

## Symptoms

- **Expected**: doyle's `/live` invocation should rehydrate prior Psyche context (gen counter, focus, intentions) — the tracked-psyches format under `$SPT_HOME/psyches/tracked/` is intended to be the persistent context store across the v1.8 → Phase 24 layout migration.
- **Actual**: after switching to the v1.10.16 phase 24 output format at `$SPT_HOME/psyches/tracked`, doyle's next `/live` reported / behaved as if no prior context existed (fresh-init style).
- **Error messages**: none — silent regression.
- **Timeline**: regression appears post-v1.10.16 (Phase 24 introduces `psyches/tracked/` forked-repo layout — bare seed + per-agent + per-project worktrees, migration via `migrate_legacy_if_needed`).
- **Reproduction**: switch active SPT install to v1.10.16+, run `/live doyle` (or equivalent revive/resume) — fresh-init path fires instead of rehydrating prior Psyche context.

## Background context

- Phase 24 introduced `src/common/tracked.rs` with `migrate_legacy_if_needed`, `ensure_seed`, `ensure_agent_worktree`, sessions-log writers, and doctor data-source rows.
- Old layout: `psyche.md` and per-agent state under non-tracked paths.
- New layout (v1.10.16+): `$SPT_HOME/psyches/tracked/` — bare seed at `seed/`, worktrees under `agents/{id}/` and `projects/{name}/`.
- The Psyche LLM's working file is canonically `agents/{id}/live_context.md` post-Phase-24. Rust-side writers (`context_save_result`, `amend_signoff_result`, `run_clear`, etc.) all correctly target this path.

## Current Focus
- hypothesis: REJECTED — initial guess was that `migrate_legacy_if_needed` didn't seed the worktree. Evidence shows migration DID copy rich content into the worktree on the cold-path call. The data loss happens AFTER, on a different axis (see Resolution).
- next_action: (none — root cause confirmed, fix proposal below)

## Evidence

- timestamp: 2026-05-20T20:50Z
  source: `$LOCALAPPDATA/spt/psyches/tracked/agents/doyle/live_context.md` (current on-disk content)
  finding: |
    File exists, 191 bytes, body is fresh-init skeleton:
    ```
    # Psyche Context for doyle
    ## Status
    Fresh spawn. Generation 50 started at 2026-05-20. Prior context: none.
    ## Current Focus
    Awaiting first COMMUNE from Self.
    ## Tracked Intentions
    None yet.
    ```
    Confirms user report — doyle's persistent context has been replaced by a fresh-init template.

- timestamp: 2026-05-20T20:50Z
  source: `git log --oneline` inside `agents/doyle/` worktree
  finding: |
    Migration + context-save timeline reveals the data-loss pattern:
    | commit  | time     | subject                                                   |
    |---------|----------|-----------------------------------------------------------|
    | c25aee0 | 17:15:48 | migrate: doyle — import legacy flat layout                |
    | 582bc69 | 17:15:50 | context-save: doyle live_context — Phase 24 smoke test... |
    | b4e7d66 | 17:19:29 | context-save: doyle daemon_log                            |
    | 663c812 | 17:24:56 | migrate: doyle — import legacy flat layout (orphan rec.)  |
    | 7139745 | 17:24:57 | context-save: doyle daemon_log                            |
    The pattern: cold migrate → smoke save → daemon save → **second migrate (orphan-recovery) that overwrites live_context.md** → daemon save. The orphan-recovery migrate is the data-loss event.

- timestamp: 2026-05-20T20:50Z
  source: `git show c25aee0 -- live_context.md` (cold migrate diff)
  finding: |
    Cold migrate imported a RICH 109-line live_context.md with "Generation 47. Psyche restarted 2026-05-19. Prior context: absorbed from gen47 previous run." plus full Phase 24 planning state and echo-commune brief.
    Conclusion: migration worked correctly on the cold path; the legacy file at `psyches/tracked/doyle.md` had the right data and it landed in the worktree intact.

- timestamp: 2026-05-20T20:50Z
  source: `git show 663c812 -- live_context.md` (orphan-recovery migrate diff)
  finding: |
    Orphan-recovery migrate REPLACED the prior worktree content with the fresh-init skeleton:
    ```
    -# rich gen-47 / smoke-saved content (Phase 23 stamp frontmatter)
    +# Psyche Context for doyle
    +## Status
    +Fresh spawn. Generation 48 started at 2026-05-20. Prior context: none.
    +## Current Focus
    +Awaiting first COMMUNE from Self.
    +## Tracked Intentions
    +None yet.
    ```
    The "orphan" file that the recovery pass picked up at `psyches/tracked/doyle.md` was itself a fresh-init skeleton — meaning SOMETHING wrote a fresh-init skeleton to the legacy flat-layout path between 17:15:50 and 17:24:56.

- timestamp: 2026-05-20T20:50Z
  source: `psyche.md` (embedded LLM-side Psyche prompt — `include_str!`-ed at `src/live/wrapper/claude.rs:13`)
  finding: |
    The embedded Psyche LLM prompt instructs the Claude session to use **RELATIVE PATHS** for its context file:
    - Line 113 (first_invocation step 1): "Read the file at `{{self_id}}.md` if it exists. If the file exists, absorb it as your starting context. If it does not exist, you are starting fresh."
    - Line 117 (first_invocation step 2): "Write to the file `{{self_id}}.md`: ...Fresh spawn. Generation started at..."
    - Line 149: "...update your `{{self_id}}.md` context file..."
    - Line 241 (init_signoff): "Write your accumulated context to `{{self_id}}.md`"
    - Line 267 (context_save): "write your accumulated knowledge to `{{self_id}}.md`"
    The template still uses the v1.7 flat-layout filename (`{self_id}.md`, relative). Phase 24 never updated psyche.md to reference `live_context.md` or any subdirectory.

- timestamp: 2026-05-20T20:50Z
  source: `src/live/wrapper/claude.rs` lines 43, 234, 407
  finding: |
    All three Claude invocations (init/resume/final) set:
    ```rust
    init_cmd.current_dir(owlery::psyche_dir());     // line 43
    resume_cmd.current_dir(owlery::psyche_dir());   // line 234
    final_cmd.current_dir(owlery::psyche_dir());    // line 407
    ```
    `owlery::psyche_dir()` returns `$SPT_HOME/psyches/tracked/` (the flat-layout root, NOT the per-agent worktree). The Psyche LLM resolves its relative `{self_id}.md` reads/writes against this cwd, landing them at `psyches/tracked/doyle.md` — the legacy flat-layout path, NOT `agents/doyle/live_context.md`.

- timestamp: 2026-05-20T20:50Z
  source: Synthesis of above evidence
  finding: |
    End-to-end loop reproducing every fresh-init:
    1. Phase 24 cold-path migration moves `psyches/tracked/doyle.md` → `agents/doyle/live_context.md` and deletes the legacy file (rename, then commit).
    2. Next Psyche `claude -p` spawn runs with `cwd = psyches/tracked/` and reads `doyle.md` (relative).
    3. Per psyche.md line 113 ("if it does not exist, you are starting fresh"), the LLM concludes no prior context exists.
    4. Per psyche.md line 117, the LLM writes a fresh-init skeleton to `doyle.md` — i.e. RE-CREATES the legacy flat-layout file at `psyches/tracked/doyle.md`.
    5. The next `ensure_seed()` (fired by any tracked-layout call site — pulse, commune, signoff, sessions-log append, doctor, etc.) runs `migrate_legacy_if_needed(seed_existed_before=true)`, sees the freshly-created legacy file, renames it into `agents/doyle/live_context.md`, OVERWRITING the canonical worktree file with the fresh-init skeleton.
    6. Loop repeats every Psyche turn. The user sees "Gen N, Fresh spawn" perpetually because step 2 always reads back the skeleton step 4 wrote, while step 5 reliably destroys any rich content the Rust side (`context_save_result`) writes between cycles.

## Eliminated

- "Migration didn't seed the worktree." Cold-path migrate `c25aee0` correctly imported the rich Gen-47 content. Migration code is correct.
- "Resume path reads the wrong location." `context_file_exists` (src/live/context.rs:109) and `context_save_result` (src/live/context.rs:814) both correctly target `agents/{id}/live_context.md`.
- "agent_worktree_path mismatch (gen-suffix vs plain)." Worktree exists at the expected `agents/doyle/` path.
- "Wrapper-state read uses wrong root." Not implicated — wrapper-state is JSON metadata, separate from the Psyche markdown context.

## Resolution

### Root cause

The Phase 24 D-13 migration to `agents/{id}/live_context.md` was applied to the **Rust-side writers and readers** but **NOT** to the **LLM-side Psyche prompt template** (`psyche.md`, embedded via `include_str!` in `src/live/wrapper/claude.rs`).

The Psyche LLM is instructed (psyche.md lines 113, 117, 149, 241, 267) to read and write its context file using the relative path `{{self_id}}.md`. The wrapper spawns the Claude session with `current_dir = owlery::psyche_dir()` (= `$SPT_HOME/psyches/tracked/`, NOT the agent worktree), so the LLM's relative-path reads/writes resolve to the **legacy flat-layout path** `psyches/tracked/{id}.md`.

This creates a self-clobbering loop:
1. LLM resolves `{id}.md` → `psyches/tracked/{id}.md` (legacy path, missing post-migration).
2. LLM concludes "starting fresh" and writes a fresh-init skeleton to that legacy path.
3. Next `ensure_seed()` call runs `migrate_legacy_if_needed`'s orphan-recovery pass.
4. Orphan recovery moves the freshly-written legacy file INTO the worktree, OVERWRITING the canonical `agents/{id}/live_context.md`.
5. Goto 1.

The user sees "Fresh spawn, Generation N" on every `/live` invocation because the legacy-path skeleton is the file that wins every migration round, and the rich Rust-side `context_save_result` writes (which go to the correct worktree path) are stomped on the next pulse.

### Proposed fix

**Minimum-viable fix (recommended):** Align the LLM-side relative paths with the post-Phase-24 cwd by spawning the Claude session with `current_dir = agent_worktree_path(self_id)`, AND update the embedded `psyche.md` template to reference `live_context.md` (the canonical filename) instead of `{self_id}.md`.

Concrete edits:

1. **`src/live/wrapper/claude.rs`** — change all three `current_dir` calls so the Psyche LLM's relative paths resolve into its own worktree:
   - line 43 (init): `init_cmd.current_dir(owlery::agent_worktree_path(&self.self_id));`
   - line 234 (resume): `resume_cmd.current_dir(owlery::agent_worktree_path(&self.self_id));`
   - line 407 (final): `final_cmd.current_dir(owlery::agent_worktree_path(&self.self_id));`
   Also ensure the worktree is materialized before spawn — call `tracked::ensure_agent_worktree(&self.self_id)` once at session-init time (soft-fail to the existing `owlery::psyche_dir()` cwd if creation fails, so a broken seed doesn't kill `/live`).

2. **`psyche.md`** (top-level template, `include_str!`-ed) — replace every `{{self_id}}.md` with the literal filename `live_context.md`:
   - Line 113: `Read the file at \`live_context.md\` if it exists.`
   - Line 117: `Write to the file \`live_context.md\`:`
   - Line 149: `...update your \`live_context.md\` context file...`
   - Line 241: `Write your accumulated context to \`live_context.md\`:`
   - Line 267: `write your accumulated knowledge to \`live_context.md\`:`
   This decouples the LLM-visible filename from `{{self_id}}`, which matches the Rust-side constant `LIVE_CONTEXT_FILE` in `src/live/context.rs:15`.

3. **One-shot cleanup for already-corrupted agents** (operator concern, not strictly a code fix): after deploying the fix, agents whose `live_context.md` is currently a fresh-init skeleton will keep losing context until someone restores the prior rich content. Two mitigations:
   - Add a guard in `migrate_legacy_if_needed`: if the destination `agents/{id}/live_context.md` already exists AND the incoming legacy file is a recognizable fresh-init skeleton (matches the psyche.md step-2 boilerplate by string-prefix), SKIP the rename and just delete the stray legacy file with a stderr warning. This breaks the clobber loop for any in-flight regressions on older deploys.
   - Use `git log` on the worktree to find the last non-fresh-init commit (e.g., `c25aee0` for doyle) and `git checkout` that revision's `live_context.md` into the working tree, then commit a "restore: {id} — recover rich context after psyche.md cwd regression" commit.

**Why this fix is minimal and correct:**

- It doesn't touch the migration code, which is correct and battle-tested.
- It doesn't add a fallback-read in the LLM prompt (e.g., "try `live_context.md` then fall back to `{id}.md`"), which would be fragile and prompt-bloat.
- It preserves the Rust-side D-13 contract — `LIVE_CONTEXT_FILE` is already the canonical filename, so the LLM and the Rust side converge on the same physical file.
- After the fix, the Psyche LLM's `Read live_context.md` reads the worktree file directly (no migration round-trip), and its `Write live_context.md` writes into the worktree (where the next `ensure_seed()` call sees no orphan to recover).
- The migration code's orphan-recovery pass becomes truly redundant for live agents (which was its original Phase 18.4/18.5 binary-handoff intent — not "every Psyche turn"); it's still safe to keep around for genuine multi-version race recovery.

**Test plan:**

- Unit: a new test in `src/live/wrapper/claude.rs` that asserts `init_session` / `resume_session` invoke `Command::current_dir` with the value of `owlery::agent_worktree_path(&self_id)` (use a thin trait or an extracted helper if the current code structure doesn't allow inspecting `Command` directly).
- Unit: psyche-template smoke test — `assert!(PSYCHE_TEMPLATE.contains("live_context.md"))` and `assert!(!PSYCHE_TEMPLATE.contains("{{self_id}}.md"))`.
- Integration: extend `tests/handoff_integration.rs` (or add a new test) that:
  1. Pre-seeds `agents/doyle/live_context.md` with rich content via `context_save_result`.
  2. Spawns a real `live` wrapper (or a thin mock that exercises `init_cmd.current_dir(...)`).
  3. Has the (mocked) LLM read the file relative to its cwd and assert it sees the rich content (NOT a fresh-init skeleton).
  4. Run a fake `ensure_seed()` afterward and assert `live_context.md` still contains the rich content.
- Manual smoke (post-deploy): `$LIVE stop doyle && $LIVE start doyle`, then check `head -5 $LOCALAPPDATA/spt/psyches/tracked/agents/doyle/live_context.md`. Expected: prior rich content survives the spawn. Pre-fix: it gets clobbered with a fresh-init skeleton.

### Specialist hint

`rust` — fix lives in Rust code (wrapper/claude.rs) plus the embedded markdown template. No specialist skill is strictly required; the change is mechanical and well-scoped.

### Structural follow-up (2026-05-21)

The original Phase 24 fix (cwd realignment + psyche.md path update) closed the
migration-loop root cause. A separate failure mode — the operator-callable
`$LIVE context-save` subcommand being invoked as a smoke test — could still
clobber a recovered live_context.md with a 228-byte stub (observed
d708974 → 0ad3ece on agents/doyle in `$LOCALAPPDATA/spt/psyches/tracked`). The
structural fix shipped in quick-260521-myj-PLAN removes the subcommand
entirely. The Psyche LLM remains the sole writer of `agents/{id}/live_context.md`
via the Write tool. Removal commits: `eac0b9e` (refactor: drop subcommand
wiring + dead handler) and `7752cfd` (chore: purge user-visible references,
regenerate golden fixture). Patch-bumped to plugin v1.10.25 in commit
`00f28e2`.

### Status

Root cause confirmed. Phase 24 path-alignment fix shipped (see frontmatter
`applied_fix`). Structural follow-up — full removal of the operator-callable
`$LIVE context-save` shortcut — shipped in quick-260521-myj at plugin v1.10.25.
