# Phase 24.1: Tracked-Agents info.json — Cross-Session Activity Index - Context

**Gathered:** 2026-05-20
**Status:** Ready for planning

<domain>
## Phase Boundary

Every agent gets a persistent `psyches/tracked/agents/{agent_id}/info.json`
recording cross-session activity metadata that survives perch teardown and
machine swaps:

```
{
  "agent_id": "doyle",
  "last_started": "2026-05-18T07:30:00Z",
  "last_machine_name": "<hostname>",
  "last_project_name": "claude_skill_owl",
  "machine_history": [
    { "name": "<hostname>", "first_seen": "...", "last_seen": "..." }
  ],
  "project_history": [
    { "name": "claude_skill_owl", "branch": "main", "first_seen": "...", "last_seen": "..." }
  ]
}
```

This file lives INSIDE Phase 24's `agents/{agent_id}/` worktree (branch
`a-{agent_id}`). The Phase 24 D-13 commit pipeline already covers writes to
that directory — Phase 24.1 adds NO new commit triggers.

Pure-additive phase. New file inside an existing worktree; new field shapes;
no new commands, no new write paths.

</domain>

<decisions>
## Implementation Decisions

### Identity Sources (carried forward)

- **D-01 (carried):** **Machine identity = OS hostname** via
  `crate::common::git::hostname()`. Phase 23 D-03 locked this; Phase 24
  reused it unchanged; Phase 24.1 reuses it again. Per-machine UUID /
  gh-account email alternatives are explicitly Phase 35 territory
  (Phase 24 Deferred Ideas).

- **D-02 (carried):** **Project identity = name string only.** Reuse
  `crate::common::owlery::derive_current_repo_names()` (Phase 32 D-03):
  local `.git`-parent basename + `git remote get-url origin` basename.
  When the two names differ, BOTH are appended as separate
  `project_history` entries — matching current Phase 32 behavior. No
  `root` (abs path) field. No `remote_url` field. The ROADMAP schema
  draft's `root` field is dropped from the locked schema.

- **D-03 (carried):** **Write piggybacks on Phase 24 D-13.** No new
  commit triggers. Every commune/signoff/echo/migrate cycle that hits
  the agent worktree includes `info.json` in the same commit if its
  contents changed. The Phase 23/24 Stamp trailers (D-08) carry the
  machine + branch + head-sha; tracked info.json complements (not
  duplicates) that.

- **D-04 (carried):** **git CLI shell-out** (Phase 24 D-01). No git2-rs.
  Missing-git fallback is D-02 of Phase 24: degrade silently — tracked
  info.json STILL gets written as a raw file in the agent dir, just
  uncommitted.

### Schema Shape (locked)

- **D-05:** **Both perch info.json AND tracked info.json carry the
  richer record shape.** Element types:

  ```rust
  // machine_history elements
  struct MachineHistoryEntry {
      name: String,
      first_seen: String,  // ISO-8601 UTC, format_timestamp()
      last_seen: String,   // ISO-8601 UTC, format_timestamp()
  }

  // project_history elements (carries current-branch context)
  struct ProjectHistoryEntry {
      name: String,
      branch: String,      // last-seen git branch at write time (HEAD)
      first_seen: String,
      last_seen: String,
  }
  ```

  Perch `project_history` is upgraded from `Vec<String>` (Phase 32 D-08)
  to `Vec<ProjectHistoryEntry>`. Both sides identical shape — satisfies
  ROADMAP SC5 ("consistent with perch") via shape-equality, not via
  projection.

  **`branch` semantics:** captured at write time from the cwd repo's
  current HEAD via `git -C {cwd} rev-parse --abbrev-ref HEAD`. Updated
  on every bump (parallels `last_seen` — single-value field overwritten
  on each sighting). When cwd is not in a git repo OR HEAD is detached,
  store empty string `""` (not absent, not null — keeps schema flat).
  Phase 24 D-02 missing-git fallback: `""` again.

  **Migration of the perch shape itself:** Phase 32 D-08 graceful
  deserialize pattern (`#[serde(default)]` + `serde_json::Value`
  round-trip in `owlery::append_project_history`) extends to the new
  element type:
  - Legacy `Vec<String>` entries: each string `s` deserializes to
    `HistoryEntry { name: s, first_seen: <fallback>, last_seen:
    <fallback> }` where fallback = `info.json` mtime per D-11.
  - Mixed arrays (legacy String + new Object) tolerated during rollout.
  - `serde_json::Value` mutate-in-place path (current owlery.rs:566)
    rewrites legacy String entries to Object entries on first write,
    preserving unknown-field round-trip per Phase 32 anti-pattern
    guidance.

### Update Semantics (locked)

- **D-06:** **`last_started` updates on the `boot` trigger only.**
  Phase 24 D-11 defines `boot` as fresh `/spt:live` start OR
  `/spt:revive` (no `--resume`). Pairs with Phase 24 SC7 sessions log
  — every `boot` row in `sessions.log` is matched by a `last_started`
  bump in tracked info.json. `pulse` / `commune` / `signoff` triggers do
  NOT bump `last_started`. Semantic: "when did this agent most recently
  come back to life?" not "when was it last active?"

- **D-07:** **`last_machine_name` updates on every tracked write**
  (commune / signoff / boot — any Phase 24 D-13 write to the agent
  worktree). Matches Phase 23 Stamp's `Machine` trailer cadence (one
  per commit). Read directly from `crate::common::git::hostname()` at
  write time.

- **D-08:** **`machine_history` rules:**
  - On every tracked write: scan `machine_history` for an entry whose
    `name == hostname()`. If found → bump `last_seen` to now. Else →
    append new entry `{ name: hostname(), first_seen: now,
    last_seen: now }`.
  - **last_seen bumps on every write** (one info.json write per
    Phase 24 D-13 commit cycle is acceptable write-amp — bounded by
    the commit cadence, not the pulse cadence).

- **D-09:** **`last_project_name` updates on commune/signoff.**
  Resolves from cwd via `derive_current_repo_names()` at write time.
  When the helper returns 2 names (folder ≠ origin), the **first**
  element wins (folder basename — matches Phase 32 D-03 ordering).
  Both names still get appended to `project_history` via D-10.

- **D-10:** **`project_history` rules** (mirror of D-08, plus branch):
  - On every commune/signoff: capture current branch once via
    `git -C {cwd} rev-parse --abbrev-ref HEAD` (empty string on
    failure / detached HEAD / no-git per D-05). For each name in
    `derive_current_repo_names()`, scan `project_history` for matching
    `name` (dedup key = `name` only — branch is NOT part of the key):
    - Found → bump `last_seen` AND overwrite `branch` with the just-
      captured value.
    - Else → append `{ name, branch, first_seen: now, last_seen: now }`.
  - The existing Phase 32 helper
    `owlery::append_project_history(perch_id, &names)` is **upgraded in
    place** to write the new record shape, and its signature gains a
    `branch: &str` parameter (callers pass the captured HEAD). Tracked
    side gets a sibling helper that writes into the agent worktree's
    info.json on the same call site.
  - Both helpers stay best-effort silent-on-error (Phase 32 precedent).

### Bounds + Order (locked)

- **D-11:** **Both histories unbounded; insertion order preserved.**
  Matches Phase 32 D-08 perch behavior exactly. No LRU. No cap. Order
  = first-seen-first. `first_seen` is set ONCE on insertion;
  `last_seen` bumps on every subsequent sighting. Growth concern is
  bounded in practice — an agent touches single-digit machines and
  tens of repos at most. Future cap is additive (deferred).

### Atomicity (SC6)

- **D-12:** **Atomic write via temp-file + rename.** Pattern lifted
  from `crate::common::owlery::write_atomic` (existing helper for
  perch info.json writes). Same helper reused for tracked info.json
  writes. Crashed mid-write leaves the OLD file intact; Phase 24's
  `git status --porcelain` clean check (Phase 24 D-05) tolerates the
  rename pattern (no partial files left on disk).

### Migration (SC9, locked)

- **D-13:** **Synthesize from perch info.json + mtime fallbacks.**
  On the first boot of the new binary, for each agent already on disk:
  1. Resolve perch info.json path via owlery helper.
  2. Read `project_history` (may be legacy `Vec<String>` or upgraded).
  3. Build tracked info.json:
     - `agent_id` = perch's `owl_id`
     - `last_started` = perch info.json mtime (best available signal;
       sessions log will overwrite on next `boot`)
     - `last_machine_name` = `hostname()` at migration time
     - `last_project_name` = first element of perch's `project_history`
       if non-empty, else `null` (`#[serde(skip_serializing_if =
       "Option::is_none")]`)
     - `machine_history` = `[{ name: hostname(), first_seen: <mtime>,
       last_seen: <mtime> }]` — seeded with current machine only; no
       historical cross-machine record exists, do not fabricate one
     - `project_history` = for every string `s` in perch's history,
       emit `{ name: s, branch: "", first_seen: <mtime>,
       last_seen: <mtime> }`. Order preserved. `branch` is empty
       string at migration (no historical branch signal exists);
       fills in on the next live commune/signoff bump.

  Migration is **idempotent**: presence of tracked info.json on disk
  short-circuits. Tracked info.json write goes through Phase 24's
  D-15 migration commit (subject `migrate: {id} — import legacy flat
  layout`); no separate migration commit for Phase 24.1.

### Doctor Surface (SC8)

- **D-14:** **`$LIVE doctor` surfaces parsed summary + raw path.**
  Per-agent one-line summary:
  `{agent_id}: last_started={ts}, last_machine={name},
  last_project={name}` plus a line for `{path}`. Implementation extends
  Phase 24 D-17 doctor output (`src/owl/doctor.rs`). No new table; same
  per-worktree row gains a sub-line.

### psyche-download Exposure (SC10)

- **D-15:** **Expose tracked info.json path to consumers.**
  `$LIVE psyche-download` and listings gain access to the file. Two
  pieces:
  - Path resolution: new helper
    `owlery::tracked_agent_info_path(agent_id)` (returns the absolute
    path inside the worktree).
  - Listings (`$LIVE list`, `$OWL list`) read tracked info.json
    `last_started` / `last_machine_name` / `last_project_name`
    directly (no perch parse). When tracked info.json is absent (very
    fresh agents pre-first-commit), fall back to perch info.json
    fields. Psyche-download payload shape itself is NOT changed in
    Phase 24.1 — that is Phase 25's territory.

### Claude's Discretion

- Exact struct names for the new types (`HistoryEntry`,
  `TrackedAgentInfo`, etc.) — researcher / planner pick.
- Whether the perch + tracked write paths share a single
  `append_history_entry(&mut Value, name, now)` primitive in
  `common/owlery.rs` or each calls its own — code-organization choice.
- The exact owlery helper name for tracked info.json read/write —
  parallel to `info_file()` / `read_info_json()` precedent.
- Doctor sub-line formatting (column widths, dim/bold) — visual polish.
- Whether `last_project_name` is `String` or `Option<String>` in the
  Rust struct — `Option<String>` recommended to allow migration to
  leave it null when perch history is empty, but planner confirms.
- Test coverage breakdown — perch-shape migration tests should extend
  the existing Phase 32 test cluster in `common/owlery.rs:1182+`.

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Source Surfaces (Touchpoints)
- `src/common/types.rs` — `InfoJson`. Upgrade `project_history` field
  type from `Vec<String>` to `Vec<HistoryEntry>`. Add new struct
  `HistoryEntry { name, first_seen, last_seen }`. Add new struct
  `TrackedAgentInfo` for the tracked info.json shape.
- `src/common/owlery.rs` — `append_project_history` (line 566) is the
  primary upgrade site. Add tracked-side parallel helpers; add
  `tracked_agent_info_path()` resolver; add new
  `append_machine_history` helper.
- `src/common/owlery.rs:447` — `derive_current_repo_names()` is the
  identity source for `last_project_name` / `project_history`. Reuse
  unchanged.
- `src/common/git.rs:326` — `hostname()` is the identity source for
  `last_machine_name` / `machine_history`. Reuse unchanged.
- `src/common/tracked.rs` — Phase 24 worktree primitives
  (`ensure_seed`, `ensure_agent_worktree`, the write-path callers).
  Tracked info.json write happens AFTER the worktree exists; lives in
  the same call sequence as commune/signoff payload writes.
- `src/owl/poll.rs` (line 128 area) + `src/live/start.rs` (line 275
  area) — current `append_project_history(id, &repo_names)` call sites.
  These switch to record-shape append AND add the tracked-side write.
- `src/owl/doctor.rs` — Phase 24 D-17 doctor table. D-14 sub-line lands
  here.

### Phase Continuity
- `.planning/phases/24-tracked-dir-forked-repo-layout-agents-projects-branches-sess/24-CONTEXT.md`
  — **MUST READ**. Phase 24 decisions are the substrate Phase 24.1
  builds on (D-04 branch naming, D-13 write path, D-15 migration,
  D-16 lazy worktree, D-17 doctor surface).
- `.planning/phases/32-list-overhaul-skill-hint-audit/32-CONTEXT.md` —
  Phase 32 D-02 / D-03 / D-05 / D-07 / D-08 define the current
  `project_history` semantics (name derivation, dedupe, graceful
  deserialize). D-05 (locked behavior of D-08) is upgraded here.

### Project Context
- `.planning/PROJECT.md` — v1.8 Psyche Restructure milestone.
- `.planning/ROADMAP.md` §Phase 24.1 — 10 Success Criteria + draft
  schema (note: `root` field is dropped per D-02).

### Codebase Maps
- `.planning/codebase/ARCHITECTURE.md`
- `.planning/codebase/CONVENTIONS.md`
- `.planning/codebase/STRUCTURE.md`

### Forward-Compat
- ROADMAP Phase 25 — psyche-download cwd-awareness will consume
  tracked info.json (D-15 path resolver feeds Phase 25's cwd-project
  matcher).
- ROADMAP Phase 35 — gh remote sync. Tracked info.json roams along
  with the rest of the agent branch payload; no Phase 24.1 code change
  needed to support that.

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- `crate::common::owlery::derive_current_repo_names()` (line 447) —
  identity source for project names. Single canonical derivation;
  Phase 24.1 does not negotiate it.
- `crate::common::owlery::append_project_history` (line 566) — the
  Value-round-trip + write-amp-guarded pattern. Upgrade in place;
  preserves unknown-field tolerance per Phase 32 anti-pattern note.
- `crate::common::owlery::write_atomic` (or equivalent) — temp-file
  + rename helper. Reuse for tracked info.json writes.
- `crate::common::git::hostname()` — Phase 23 D-03 capture.
- `crate::common::time::format_timestamp()` — Phase 24-compatible
  ISO-8601 UTC formatter; reuse for first_seen / last_seen.

### Established Patterns
- **Value round-trip preserves unknown fields** (Phase 32 owlery
  helper) — extends naturally to mixed-shape arrays during the
  String→Object migration.
- **`#[serde(default)]` + `skip_serializing_if`** for legacy-tolerant
  optional fields (Phase 26 D5/Q1 `cwd`, Phase 32 D-08
  `project_history`). Applied to every new field in
  `TrackedAgentInfo`.
- **Best-effort silent-on-error** writes (Phase 32 D-05 mirroring
  `patch_session_id` precedent) — tracked info.json writes match this
  posture. Soft-fail per Phase 24 D-02.

### Integration Points
- Phase 24 D-13 commit pipeline — Phase 24.1 info.json is one more
  file in the same `git add` set. No new commit subject; same
  `commune` / `signoff` / `migrate` subject as the surrounding payload.
- Phase 24 D-15 migration commit — Phase 24.1 migration synth piggybacks
  on the same migrate commit (no separate migrate-info-json commit).
- Phase 24 D-17 doctor — Phase 24.1 adds a sub-line to the existing
  per-worktree row (no new top-level command).

### What Phase 24.1 Does NOT Touch
- The owlery perch info.json call sites are minimal changes (shape
  upgrade only; existing append flow continues).
- Phase 35 sync conflict policy — explicitly deferred (Phase 24 D-14).
- psyche-download payload structure — Phase 25 territory.

</code_context>

<specifics>
## Specific Ideas

- machine_history element shape = `{name, first_seen, last_seen}`.
  project_history element shape = `{name, branch, first_seen,
  last_seen}`. Both sides (perch + tracked) carry the same shape per
  history. No asymmetry.
- project_history dedup key = `name` only. `branch` is a single-value
  field overwritten on every bump (parallels `last_seen` semantics).
  Switching branches in the same repo does NOT create a new entry.
- The ROADMAP draft schema's `root` field is dropped — name-only key
  matches Phase 32 D-03 exactly.
- `last_started` semantic = "fresh claude -p spawn" only. Pairs 1:1
  with `boot` rows in `sessions.log` (Phase 24 D-11).
- `last_machine` / machine_history bump = every Phase 24 D-13 write
  (one info.json write per commit cycle).
- Migration uses perch info.json mtime as the universal fallback for
  first_seen / last_seen — no fabricated history.
- Field order in `TrackedAgentInfo` JSON: `agent_id`, `last_started`,
  `last_machine_name`, `last_project_name`, `machine_history`,
  `project_history`. Matches ROADMAP draft.
- `last_project_name` is `Option<String>` — null for agents whose
  perch project_history was empty at migration time.

</specifics>

<deferred>
## Deferred Ideas

- LRU cap on machine_history / project_history — revisit only if real
  agents grow past tens of entries. Currently unbounded matches Phase
  32 D-08 perch behavior.
- Richer project identity (`root` abs path, git-remote URL hash) —
  Phase 35 may need cross-machine canonical identity; revisit then.
- Per-machine UUID alternative to hostname — Phase 35 territory
  (Phase 24 Deferred carryover).
- Cross-machine concurrent write conflict policy for tracked
  info.json — Phase 35's problem (Phase 24 D-14 carryover).
- Doctor visual polish (column widths, color, dim/bold) — planner's
  call within Phase 24.1; further tuning is future cosmetic work.
- psyche-download payload embedding of last_started / last_machine /
  last_project — Phase 25 territory (cwd-aware concat reshape).
- Sessions log ↔ tracked info.json `last_started` consistency check
  in doctor (cross-validation) — additive future doctor enhancement.

</deferred>

---

*Phase: 24.1-tracked-agents-info-json-cross-session-activity-index*
*Context gathered: 2026-05-20*
