Expand description
One-time sweep for identity conversation history stranded before
ensure_history_link (SPEC_AGENT_IDENTITY_HISTORY_PERSISTENCE_PROTOCOL_2026_08_16.md,
shipped in 0007c6b64051e8 / PR #2605) existed to keep it linked into
the always-global shared/identities/<id>/<provider>/<subdir>/ tree.
§What went wrong
ensure_history_link already does the hard part — migrate a real,
pre-existing directory’s contents into the global target (never
clobbering an existing entry) and replace it with a junction/symlink —
but it only ever runs at auth.start and at ordinary agent spawn
(identity_auth_dirs::link_history_if_isolated, called from
identity/resolver/inject.rs). A channel that stopped being spawned
into before the fix landed — retired, superseded by a newer dev
branch, or simply abandoned — never spawns again, so that call site
never re-runs for it. Its real identities/<bundle>/<provider>/<subdir>/
directory is left exactly as it was: live conversation history that
ClaudeHistoryAdapter’s per-channel fallback scan can still find and
read (so it isn’t lost), but that will never be linked into the
global tree, and will never benefit from a fix to the linking logic
itself, because nothing ever calls it again for that channel.
Confirmed on a live install: two channels
(local-agentx-fix-lan-discovery-tx-fc5809-ec08a24c,
local-main-b28b7a-0e5d07ad) born on 2026-08-16 — the exact day the
fix shipped — carry four such real, unlinked, non-empty identity
history directories, last written 2026-09-03 and never touched since.
§What this does
Walks every identities/ directory found up to 2 levels under
<home>/channels/* and <home>/dev/* — covering channels/<slug>/,
dev/<branch>/, and the clone-scoped dev/<branch>/<clone_id>/ shape
(see identities_roots, and ClaudeHistoryAdapter’s identical
bounded traversal, which this mirrors on purpose). For every
identities/<bundle_id>/<provider.auth_dir_name>/<history_subdir> path
that exists on disk, calls the live ensure_history_link to merge it
into <home>/shared/identities/<bundle_id>/<provider.auth_dir_name>/<history_subdir>
and replace it with a link — the exact same operation a spawn would
have performed, just run once, retroactively, for every channel that
will never spawn again.
Deliberately calls the LIVE ensure_history_link rather than freezing
a copy (contrast the guidance in this module’s own doc comment about
migrations that produce DB rows): that guidance protects rows whose
shape is meaningful and time-bound. This migration produces no rows
at all — it performs a filesystem merge-and-link operation that
ensure_history_link already defines as idempotent and safe
(“never deletes or overwrites data”). Freezing a copy would mean a
future safety fix to that function (it has already had one real bug
fixed under it, per PR #2605’s review) never applies to this
migration, on any machine that runs it after that fix ships — the
opposite of what freezing is meant to protect against.
Scoped Global: it only ever writes under <home>/shared/, and must
run at most once regardless of which channel happens to run it first.
Never invents new structure: only acts where a real
identities/<bundle>/<provider>/<subdir> path already exists on disk.
Does not create a link (or the target directory) for a bundle/provider
combination that was never present, which ensure_history_link would
otherwise happily do (it unconditionally create_dir_alls the
target). Best-effort per entry, matching link_history_if_isolated’s
own philosophy: one bundle’s I/O error (e.g. a genuine name collision
ensure_history_link refuses to resolve automatically) is logged and
skipped, never aborts the sweep for every other bundle.
Structs§
Functions§
- collect_
identities_ 🔒roots - identities_
roots 🔒 - Every
identities/directory up to 2 levels under<home>/channels/and<home>/dev/. Deliberately mirrorsClaudeHistoryAdapter::scan_isolated_identities_under’s bounded traversal (crates/srv/src/backend/history/claude_adapter.rs) rather than assuming a fixed one-level shape:DataPaths::resolveplaces a dev instance atdev/<branch>/when there is no clone id, but atdev/<branch>/<clone_id>/whenever one is derived — whichagentmux_common::runtime_mode::derive_clone_iddoes for essentially every real dev checkout (anywhere a.git/Cargo.toml/Taskfile.ymlis found up the tree), not an edge case. An earlier revision of this function only checked one level underdev/, which silently found nothing for that layout and would have marked the migration applied without ever backfilling clone-scoped dev instances — caught in review (muxreview + Codex, independently, on the same commit) before merge. Depth 2 covers every known layout:channels/<slug>/identities/,dev/<branch>/identities/, anddev/<branch>/<clone_id>/identities/. Kept as its own copy rather than calling the adapter’s function directly: that one is Claude-specific (hardcodesclaude/projects) and private to its module; this migration needs theidentities/directory itself, for every provider. - is_
safe_ 🔒path_ segment - Mirrors
agentmux_common::data_paths’s privatesanitize_path_segmentas a boolean check only (that function isn’tpub, and this call site only ever needs to know whether a name discovered on disk is safe to re-join into a path — never the sanitized value itself, since the originalbundle_entrypath is already known-safe on this filesystem and is what actually gets used). Kept as its own small, stable copy rather than exporting the original: this predicate is simple path hygiene, not the isolation-sensitive logicensure_history_linkand the migration framework’s own “don’t freeze what might change” note are about.