Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3a3ce59
feat(muse): index Muse Code sessions as a first-class source
barryonthecape Sep 24, 2026
6149f37
Index Muse subagent logs as delegated children
barryonthecape Sep 25, 2026
5830d23
Merge origin/main into feat/muse-code-source
barryonthecape Sep 25, 2026
1301451
Merge origin/main into feat/muse-code-source
barryonthecape Sep 25, 2026
800610e
fix(muse): address review — usage pairing, repairs, child logs, CI
barryonthecape Sep 25, 2026
2d07c35
fix(muse): keep remote history on local re-reads; tighten spawn diagn…
barryonthecape Sep 25, 2026
096edc6
fix(muse): own exactly the history a transcript wrote; recurse on rem…
barryonthecape Sep 25, 2026
9d83a9a
Merge origin/main into feat/muse-code-source
barryonthecape Sep 25, 2026
56e8d5d
fix(muse): re-attribute shared prompts, repair erased subagents, chil…
barryonthecape Sep 25, 2026
d0a4b96
fix(muse): a subagent log is never a session on any path
barryonthecape Sep 25, 2026
2b7b58b
fix(muse): delimit model steps by tool results, not by commits
barryonthecape Sep 25, 2026
f93c832
fix(muse): an unstattable subagent log is not a deleted one
barryonthecape Sep 25, 2026
6f5f220
Merge origin/main into feat/muse-code-source
barryonthecape Sep 27, 2026
a7e352c
fix(muse): keep unidentified child logs; share step facts; isolate fi…
barryonthecape Sep 27, 2026
34e1cf5
fix(muse): a subagent read never sets its catalog title
barryonthecape Sep 27, 2026
2fbf37f
fix(muse): read child logs only inside their session directory
barryonthecape Sep 27, 2026
58a546b
Merge origin/main into feat/muse-code-source
barryonthecape Sep 27, 2026
d543532
fix(muse): rebuild each relinked delegation edge
barryonthecape Sep 27, 2026
f09490c
fix(muse): a call that ended without a result keeps its outcome
barryonthecape Sep 27, 2026
0a2e9af
fix(muse): keep a delegation's first-seen time when its details change
barryonthecape Sep 27, 2026
d1d6038
fix(muse): a subagent symlink back into its session is not followed
barryonthecape Sep 27, 2026
1acca70
fix(muse): measure subagent containment against the real session dir
barryonthecape Sep 27, 2026
6291cf0
Merge origin/main into feat/muse-code-source
barryonthecape Sep 27, 2026
ddf8af7
fix(muse): adapt the sync pass to the retired export journal
barryonthecape Sep 27, 2026
5c17f02
Merge origin/main into feat/muse-code-source
barryonthecape Sep 29, 2026
6b0c88b
fix(muse): tool results are standalone outputs, not user-message blocks
barryonthecape Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,23 @@ Notable changes to the native `ai-hist` CLI are documented here.

### Added

- Muse Code (Meta's `muse` CLI) is a first-class source, `muse`. Sessions are
read from `$XDG_DATA_HOME/muse/sessions/YYYY/MM/DD/<id>/session.jsonl`
(`~/.local/share/muse/sessions` by default) by discovery, `sync`,
targeted hydration and live capture: typed prompts become `history` rows at
the microsecond time Muse recorded them, and each session gets its prose,
readable thinking, tool calls and results (status from Muse's own
`tool_batch.effect.terminal` outcome, and a non-zero `bash` exit code),
`write_file` / `edit_file` edits, per-model-step token usage (normalized as
`per-request`), models, CLI version and lifecycle markers (`turn_end`,
`session_start`, `session_resumed`, `session_end`, `model_switch`,
`encrypted_reasoning`). Subagent and reminder logs
(`subagent/<id>/session.jsonl`, at any depth) are indexed under the child's
own session id and linked to their parent as `delegated` children, typed
from the parent's `task_stream_linked` record; they are not catalogued as
sessions and their objectives are not history rows. A Muse hydration
reports `full`. `ai-hist resume` prints `muse resume <id>`, and the sync
service forwards `XDG_DATA_HOME`.
- Read every OpenCode channel database. OpenCode keeps one SQLite store per
release channel — `opencode.db` for `latest`/`beta`, and
`opencode-stable.db`, `opencode-nightly.db`, ... beside it — and only
Expand Down Expand Up @@ -406,6 +423,9 @@ Notable changes to the native `ai-hist` CLI are documented here.

### Rust API

- `Source::Muse` and `ProviderRoots::muse` (the Muse Code sessions directory;
`from_env` honours `XDG_DATA_HOME`). Both types are `#[non_exhaustive]`, so
this is additive.
- `ProviderRoots` gains `opencode_db_pinned: bool`. `from_env` sets it when
`OPENCODE_DB` is set; `from_home` leaves it `false`, so every OpenCode channel
database beside `opencode_db` is read. Set it to read `opencode_db` alone.
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Search, resume, and hand off every coding-agent session — across every harness on your machine.**

Local memory for **Claude Code**, **Codex**, **Cursor**, **Grok**, **OpenCode**, and [**Agent Relay**](https://github.com/AgentWorkforce/relay). Your agent sessions are indexed into one local SQLite database. Search them all at once, jump back into a session with its harness's native resume command, and hand a compact context pack to another agent — or to a teammate's agent — to continue the work.
Local memory for **Claude Code**, **Codex**, **Cursor**, **Grok**, **Muse Code**, **OpenCode**, and [**Agent Relay**](https://github.com/AgentWorkforce/relay). Your agent sessions are indexed into one local SQLite database. Search them all at once, jump back into a session with its harness's native resume command, and hand a compact context pack to another agent — or to a teammate's agent — to continue the work.

```sh
ai-hist search "auth rewrite"
Expand Down Expand Up @@ -31,7 +31,7 @@ ai-hist search "the thing i was working on"

The bare `ai-hist` command bootstraps a searchable database on first use: it discovers your most recent local sessions and indexes their evidence, then tells you what it found. It leaves an already-populated database alone. Run `ai-hist sync` any time you want a full re-ingest rather than the bounded first-run pass.

RelayHistory follows each harness's configured state directory. Set `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, or `GROK_HOME` to relocate Claude Code, Codex, or Grok data; unset or empty values fall back to `~/.claude`, `~/.codex`, and `~/.grok`. OpenCode uses `OPENCODE_DB`, defaulting to `~/.local/share/opencode/opencode.db`.
RelayHistory follows each harness's configured state directory. Set `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, or `GROK_HOME` to relocate Claude Code, Codex, or Grok data; unset or empty values fall back to `~/.claude`, `~/.codex`, and `~/.grok`. Muse Code sessions are read from `$XDG_DATA_HOME/muse/sessions` (`~/.local/share/muse/sessions` when `XDG_DATA_HOME` is unset). OpenCode uses `OPENCODE_DB`, defaulting to `~/.local/share/opencode/opencode.db`.

Node.js 20 or 22 is required. `npm install` pulls a prebuilt native addon for macOS (arm64, x64), Linux glibc ≥ 2.28 and musl (arm64, x64), and Windows x64 — no Rust toolchain, compiler, or separate binary download. The glibc floor covers Debian 12, Ubuntu 22.04, Amazon Linux 2023, and RHEL/Alma 9; releases are smoke-tested on `node:22-bookworm-slim` and `ubuntu:22.04`.

Expand All @@ -52,9 +52,9 @@ ai-hist recent 20 # the last N prompts, newes
ai-hist stats # how much history is indexed, by source and project
```

`<harness>` is the session's source — `claude`, `codex`, `cursor`, `grok`, `opencode`, or `relay` — and is required alongside the ID, because session IDs collide across providers. `ai-hist sessions list` prints both.
`<harness>` is the session's source — `claude`, `codex`, `cursor`, `grok`, `muse`, `opencode`, or `relay` — and is required alongside the ID, because session IDs collide across providers. `ai-hist sessions list` prints both.

`ai-hist resume` prints a native resume command for Claude Code, Codex, Cursor, and Grok sessions. OpenCode and Agent Relay sessions are searchable and packable, but have no native resume command to print, so use `ai-hist pack` to carry that context forward instead.
`ai-hist resume` prints a native resume command for Claude Code, Codex, Cursor, Grok, and Muse Code sessions. OpenCode and Agent Relay sessions are searchable and packable, but have no native resume command to print, so use `ai-hist pack` to carry that context forward instead.

OpenCode is read from whichever of its two stores the machine has: the SQLite
`opencode.db` that current releases write (`OPENCODE_DB`), or the older JSON
Expand Down Expand Up @@ -114,7 +114,7 @@ Team uploads come from the [Agent Relay desktop app](https://agentrelay.com), no

## Why `ai-hist`

- **Every harness, one search.** Claude Code, Codex, Cursor, Grok, OpenCode, Agent Relay — indexed side-by-side. No per-harness silo.
- **Every harness, one search.** Claude Code, Codex, Cursor, Grok, Muse Code, OpenCode, Agent Relay — indexed side-by-side. No per-harness silo.
- **Provider-aware evidence.** Prompts, tool calls, and edits are preserved as raw evidence, not summarized away — as much of it as each harness actually exposes. Hydration reports `full`, `partial`, or `shallow_only` per session, so you can tell thin coverage from a thing that never happened. Where a harness records less than the others, the gap is named. Cursor transcripts carry the assistant's prose and every tool call, but no tool output, model id, token usage or timestamp field — those are reported as unavailable, and a turn whose injected `<timestamp>` tag cannot be read is stamped from the file mtime with `CURSOR_TIMESTAMP_FROM_MTIME`. Older Grok builds log no per-turn billing tokens, so their only token fact is a context-window proxy, and hydration says so; newer builds' per-turn `usage` breakdown is recorded beside it, never added to it. The per-field detail is in [the session catalog](docs/session-catalog.md).
- **Local by default.** SQLite on your machine. Export requires an explicit selection; remote acquisition requires an installed source plugin.
- **Handoff-native.** `pack` and `resume` are first-class commands, not afterthoughts.
Expand Down
10 changes: 6 additions & 4 deletions crates/ai-hist-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ enum SessionsAction {
/// content blocks, agent lifecycle events. Same `(ts_ms IS NULL, ts_ms,
/// id)` keyset as tool calls and file edits; undated markers page last.
Markers {
/// Coding-agent source (claude, codex, cursor, grok, relay, opencode).
/// Coding-agent source (claude, codex, cursor, grok, muse, relay, opencode).
source: String,
/// Native session identifier within that source.
session_id: String,
Expand All @@ -441,7 +441,7 @@ enum SessionsAction {
/// and cost is never computed: `reported_cost_usd` appears only when the
/// source data carried one.
Usage {
/// Coding-agent source (claude, codex, cursor, grok, relay, opencode).
/// Coding-agent source (claude, codex, cursor, grok, muse, relay, opencode).
source: String,
/// Native session identifier within that source.
session_id: String,
Expand Down Expand Up @@ -479,7 +479,7 @@ enum SessionsAction {
enum LearnAction {
/// Distill local session history into decision/finding/reflection events.
Distill {
/// Only distill sessions from this source (claude, codex, cursor, grok, relay, opencode).
/// Only distill sessions from this source (claude, codex, cursor, grok, muse, relay, opencode).
#[arg(long)]
source: Option<String>,
/// Distill one session id.
Expand Down Expand Up @@ -2901,11 +2901,13 @@ fn service_command_args(spec: &ServiceSpec, args: &[String]) -> Vec<String> {
command
}

const PROVIDER_ENV_VARS: [&str; 4] = [
const PROVIDER_ENV_VARS: [&str; 5] = [
"CLAUDE_CONFIG_DIR",
"CODEX_HOME",
"GROK_HOME",
"OPENCODE_DB",
// Muse Code keeps its sessions under `$XDG_DATA_HOME/muse/sessions`.
"XDG_DATA_HOME",
];

fn scheduler_environment_value(name: &str, value: std::ffi::OsString) -> Result<Option<String>> {
Expand Down
4 changes: 3 additions & 1 deletion crates/ai-hist-napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1736,7 +1736,9 @@ pub async fn hydrate_session(options: HydrateSessionOptions) -> napi::Result<Hyd
})
}

const CATALOG_SOURCES: &[&str] = &["claude", "codex", "cursor", "grok", "relay", "opencode"];
const CATALOG_SOURCES: &[&str] = &[
"claude", "codex", "cursor", "grok", "relay", "opencode", "muse",
];

/// Reject an unusable identity before opening anything, so a typo is an
/// argument error rather than an empty result that looks like real data.
Expand Down
2 changes: 2 additions & 0 deletions crates/ai-hist/public-api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ pub ai_hist::Source::Claude
pub ai_hist::Source::Codex
pub ai_hist::Source::Cursor
pub ai_hist::Source::Grok
pub ai_hist::Source::Muse
pub ai_hist::Source::OpenCode
pub ai_hist::Source::Relay
pub ai_hist::Source::Trajectory
Expand Down Expand Up @@ -459,6 +460,7 @@ pub ai_hist::ProviderRoots::claude: std::path::PathBuf
pub ai_hist::ProviderRoots::codex: std::path::PathBuf
pub ai_hist::ProviderRoots::grok: std::path::PathBuf
pub ai_hist::ProviderRoots::home: std::path::PathBuf
pub ai_hist::ProviderRoots::muse: std::path::PathBuf
pub ai_hist::ProviderRoots::opencode_db: std::path::PathBuf
pub ai_hist::ProviderRoots::opencode_db_pinned: bool
pub ai_hist::ProviderRoots::opencode_storage_dir: std::path::PathBuf
Expand Down
147 changes: 147 additions & 0 deletions crates/ai-hist/src/discover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ pub struct DiscoveryEnv<'a> {
pub codex_home: PathBuf,
/// Grok state root.
pub grok_home: PathBuf,
/// Muse Code session logs.
pub muse_sessions: PathBuf,
/// Path to the opencode database.
pub opencode_db: PathBuf,
/// Whether `opencode_db` is the only OpenCode database to read, rather
Expand Down Expand Up @@ -341,6 +343,7 @@ impl<'a> DiscoveryEnv<'a> {
claude_config_dir: roots.claude,
codex_home: roots.codex,
grok_home: roots.grok,
muse_sessions: roots.muse,
opencode_db: roots.opencode_db,
opencode_db_pinned: roots.opencode_db_pinned,
opencode_storage_dir: roots.opencode_storage_dir,
Expand Down Expand Up @@ -369,9 +372,11 @@ impl<'a> DiscoveryEnv<'a> {
.parent()
.map(|parent| parent.join("storage"))
.unwrap_or_else(|| home.join(".local/share/opencode/storage"));
let muse = crate::paths::default_muse_sessions_dir(&home);
Self::with_provider_roots(
conn,
crate::ProviderRoots {
muse,
home,
claude: claude_config_dir,
codex: codex_home,
Expand Down Expand Up @@ -409,6 +414,7 @@ impl<'a> DiscoveryEnv<'a> {
claude_config_dir: &self.claude_config_dir,
codex_home: &self.codex_home,
grok_home: &self.grok_home,
muse_sessions: &self.muse_sessions,
opencode_db: &self.opencode_db,
opencode_db_pinned: self.opencode_db_pinned,
opencode_storage_dir: &self.opencode_storage_dir,
Expand Down Expand Up @@ -451,6 +457,8 @@ pub struct ScanEnv<'a> {
pub codex_home: &'a Path,
/// Grok state root.
pub grok_home: &'a Path,
/// Muse Code session logs.
pub muse_sessions: &'a Path,
/// Path to the opencode database.
pub opencode_db: &'a Path,
/// Whether `opencode_db` is the only OpenCode database to read.
Expand Down Expand Up @@ -500,6 +508,8 @@ pub struct ProviderRoots<'a> {
pub codex: &'a Path,
/// Grok state root.
pub grok: &'a Path,
/// Muse Code session logs.
pub muse: &'a Path,
/// Path to the opencode database.
pub opencode_db: &'a Path,
}
Expand Down Expand Up @@ -1009,6 +1019,7 @@ pub fn shallow_providers() -> Vec<Box<dyn ShallowSessionProvider>> {
Box::new(CodexProvider),
Box::new(CursorProvider),
Box::new(GrokProvider),
Box::new(MuseProvider),
Box::new(OpencodeProvider::default()),
Box::new(RelayProvider),
]
Expand Down Expand Up @@ -2063,6 +2074,141 @@ fn grok_update_bounds(
Ok((first, last))
}

// ---------------------------------------------------------------------------
// muse
// ---------------------------------------------------------------------------

/// Muse Code: one append-only `session.jsonl` per session under
/// `muse/sessions/YYYY/MM/DD/<session-id>/`. Subagent and reminder children
/// write their own transcripts under `subagent/` beside the parent; those are
/// not sessions of their own and are never enumerated.
struct MuseProvider;

impl ShallowSessionProvider for MuseProvider {
fn acquire(
&self,
_home: &Path,
_observation: &crate::observations::SessionObservation,
) -> Result<crate::sources::AcquiredEvidence> {
Ok(crate::sources::AcquiredEvidence::LocalFiles)
}
fn source(&self) -> &'static str {
"muse"
}
/// The transcript parser writes prompts, events, tool calls, file edits
/// and `delegated` edges to the subagent logs beside the session: every
/// kind a full session is made of.
fn evidence_kinds(&self) -> &'static [EvidenceKind] {
FULL_SESSION_KINDS
}

fn watch_roots(&self, roots: &ProviderRoots<'_>) -> Vec<WatchRoot> {
vec![WatchRoot::tree(roots.muse.to_path_buf())]
}

fn enumerate(
&self,
env: &DiscoveryEnv<'_>,
_requested_limit: Option<usize>,
) -> Result<Vec<Candidate>> {
file_candidates(
"muse",
crate::collect_muse_transcripts(&env.muse_sessions)?,
crate::file_stamp_and_modified,
)
Comment thread
barryollama marked this conversation as resolved.
}
Comment thread
barryollama marked this conversation as resolved.

/// Enumeration is top-level transcripts only, but a subagent log is
/// evidence too: sync stamps it, and a background subagent keeps writing
/// after its parent has stopped. Left out of the fold, a tick whose only
/// change was a child log would sit behind an unchanged fingerprint and
/// never reach the sweep that reads it.
fn fingerprint_inputs(&self, env: &DiscoveryEnv<'_>) -> Result<Vec<Candidate>> {
let mut files = Vec::new();
for transcript in crate::collect_muse_transcripts(&env.muse_sessions)? {
match crate::muse_session_files(&transcript) {
Ok(tree) => files.extend(tree),
Err(error) if error.is::<crate::CaptureCancelled>() => return Err(error),
// One session's child log that cannot be statted must not fail
// the whole fold. Its transcript still counts; the sweep reads
// that session, fails it alone, and — having left a source
// unread — does not cache this fingerprint, so it is retried.
Err(_) => files.push(transcript),
}
}
file_candidates("muse", files, crate::file_stamp_and_modified)
}
Comment thread
barryollama marked this conversation as resolved.

fn read_shallow(
&self,
scan: &ScanEnv<'_>,
_catalog: Option<&Connection>,
candidate: &Candidate,
) -> Result<Option<ShallowSession>> {
use crate::ingest::muse;
let path = PathBuf::from(&candidate.locator);
// A subagent's log is never a session of its own, however the
// candidate arrived — enumeration skips them, a watch hit or a
// by-path read need not.
if muse::is_subagent_log(&path) {
return Ok(None);
}
let bounded = read_bounded_jsonl(scan, &path)?;
// A permission frame can precede the metadata record, so the header
// is looked for rather than assumed to be the first line.
let Some(metadata) = bounded
.head_records()
.filter_map(parse_record)
.find_map(|record| muse::parse_metadata(&record))
else {
return Ok(None);
};
let own_stream = |record: &Value| {
record.pointer("/stream/id").and_then(Value::as_str)
== Some(metadata.session_id.as_str())
};
let mut models = Vec::new();
push_unique(&mut models, metadata.model_id.as_deref());
let mut first_prompt = None;
for record in bounded.head_records().filter_map(parse_record) {
if !own_stream(&record) {
continue;
}
match muse::parse_event(&record) {
Some(muse::MuseEvent::Prompt { text }) if first_prompt.is_none() => {
first_prompt = Some(excerpt(&text));
}
Some(muse::MuseEvent::ModelCompleted { model, .. }) => {
push_unique(&mut models, model.as_deref());
}
_ => {}
}
if first_prompt.is_some() && models.len() > 1 {
break;
}
}
let last_activity_ms = bounded
.tail_records_rev()
.filter_map(parse_record)
.filter(own_stream)
.find_map(|record| muse::recorded_ms(&record));
Ok(Some(ShallowSession {
source: "muse".into(),
session_id: metadata.session_id,
cwd: metadata.workspace_root,
first_activity_ms: metadata.recorded_ms,
last_activity_ms: last_activity_ms
.or(metadata.recorded_ms)
.or_else(|| crate::file_modified_ms(&path)),
first_prompt,
models,
agent_version: metadata.cli_version,
raw_path: Some(candidate.locator.clone()),
..Default::default()
}))
}
}

// ---------------------------------------------------------------------------
// opencode
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -3458,6 +3604,7 @@ pub(crate) fn provider_watch_roots(source: &str, roots: &crate::ProviderRoots) -
claude: &roots.claude,
codex: &roots.codex,
grok: &roots.grok,
muse: &roots.muse,
opencode_db: &roots.opencode_db,
},
)
Expand Down
Loading
Loading