Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ own mapping as an extension in its own module.
│ │ │ ├── SessionModels.swift # SessionProvider, SessionSummary, transcripts, delete plans
│ │ │ ├── SessionParsing.swift # Provider-agnostic, total parsing primitives
│ │ │ ├── SessionProviderAdapter.swift # The protocol + the registry
│ │ │ ├── {Claude,ClaudeCowork,Codex,Grok,Cursor,Gemini,Antigravity,GrokBot,Muse,Devin,MistralVibe}SessionAdapter.swift
│ │ │ ├── {Claude,ClaudeCowork,Codex,Grok,Cursor,Gemini,Antigravity,GrokBot,Muse,Devin,MistralVibe,MuseAgent}SessionAdapter.swift
│ │ │ ├── CodexTitleHydrator.swift # Titles from Codex's side index
│ │ │ ├── AntigravityConversationIndex.swift
│ │ │ ├── LiveSQLiteReader.swift # Read a store another process holds open
Expand Down Expand Up @@ -165,7 +165,10 @@ sockets inside the session directory. Devin keeps every session as rows in one
database the CLI holds open, so a delete would be SQL against another
process's live store — that is `devin rm`'s job. Mistral Vibe caches its
listing, leases running sessions, nests sub-agent sessions inside the parent's
directory, and sweeps worktrees no listed session resumes into. Removing from
directory, and sweeps worktrees no listed session resumes into. Muse (the
desktop app, not Muse Code) is Grok Bot's case again: its conversation cache
is owned and rewritten by the running app, and the conversations themselves
live on Meta's servers. Removing from
underneath a live SQLite handle is how a store gets corrupted rather than
emptied. Do not "fix" that.

Expand Down
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,35 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.11.0] - 2026-09-23

Meta's Muse app (muse.ai's Mac client) joins the session list as a read-only
provider distinct from Muse Code. New provider and harness raw value
(`museAgent`) lands in host caches.

### Added

- **`MuseAgentSessionAdapter`** — Meta's desktop agent app, `Muse.app`
(muse.ai's Mac client, bundle `com.meta.endo`), is a listed, readable,
searchable provider: **`SessionProvider.museAgent`** / **`Harness.museAgent`**
(raw value `museAgent`, display name "Muse", `HarnessCatalog.museAgent`). It
is not Muse Code — the `muse` CLI keeps its own provider and harness. The app
caches each conversation at `~/Library/Caches/ConversationCache/` as one JSON
array of messages (`storeRelativePath`); the directory is shared with the
consumer `Meta AI.app`, so discovery lists only `hatch-<id>.json` files, flat,
without following links, and never the UUID-named Meta AI chats beside them.
The session id is the file stem (`hatch-main`). Messages are ordered by
`sortSeq`; roles come from `isUser`; text is `content`, or the message's
markdown blocks when that is empty. Timestamps are seconds since the Apple
reference date and are converted as such. An `isUser` that is not a JSON
boolean marks an entry as foreign and it is skipped. The title is the first prompt,
since the cache has none. Read-only (`supportsDeletion == false`), no resume
command, and no model, token counts, or project directory: the agent runs in
a VM on Meta's servers and the cache may hold only part of the history.
- The Rust lane knows the provider: `SessionProvider::MuseAgent` (raw value
`museAgent`, default harness "Muse"), read-only, no resume, and the index
reader's harness filter accepts "Muse" and `museAgent`.

## [0.10.0] - 2026-09-17

Cognition's Devin and Mistral AI's Mistral Vibe join the session list. Devin
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ point takes the home directory it should read.
| Muse Code | `~/.local/share/muse/sessions/YYYY/MM/DD/<id>/session.jsonl` | ✅ | ✅ | ❌ the CLI indexes and locks it |
| Devin | `~/.local/share/devin/cli/sessions.db` — one database, a row per session | ✅ | ✅ | ❌ rows in a live database |
| Mistral Vibe | `~/.vibe/logs/session/session_*/{meta.json,messages.jsonl}` | ✅ | ✅ | ❌ the CLI indexes and leases it |
| Muse | `~/Library/Caches/ConversationCache/hatch-*.json` | ⚠️ only what the client cached | ❌ the run happens server-side | ❌ a cloud cache |

Where a ⚠️ appears the log genuinely does not carry the value — an aborted
Cursor conversation records no model name at all, and old Gemini CLI chats
Expand All @@ -115,8 +116,19 @@ an empty one. Grok Bot's ❌ is the stronger statement: the conversation runs
on xAI's servers and the client's cache records no model, no token counts,
and no cost for anyone to read.

Seven providers are listed and readable but never deletable, because another
running app — or, for Grok Bot, a server — owns the store.
Muse — Meta's desktop agent app, `Muse.app`, not the `muse` CLI that is Muse
Code — is the same kind of store. Its agent runs in a VM on Meta's servers, and
the app caches each conversation as one JSON array of messages. The cache
holds no model, no token counts, no title (the list row's title is the first
prompt), and no local project directory, and it may hold only part of the
history; the running app rewrites it at will. Its timestamps are seconds since
2001-01-01 (Apple's reference date), not Unix seconds. The directory is shared
with the consumer `Meta AI.app`, which is built from the same code: only the
`hatch-*.json` files are Muse's, and the UUID-named ones beside them are never
listed.

Eight providers are listed and readable but never deletable, because another
running app — or, for Grok Bot and Muse, a server — owns the store.
`SessionProvider.supportsDeletion` says so up front, and the adapters fail
closed with `SessionDeleteError.providerIsReadOnly`.

Expand Down Expand Up @@ -307,6 +319,7 @@ snapshot.quota?.usedPercent // 43.2, from Codex's own rate_limits
| Grok Build | `signals.json`: `contextTokensUsed` and `contextWindowTokens` | `measured` |
| Cursor, AntiGravity, Grok Bot, Gemini CLI, Muse Code | nothing on disk answers it | `nil` |
| Devin, Mistral Vibe | no live adapter yet; Devin records per-reply `metrics`, Vibe `stats.context_tokens` | `nil` |
| Muse | nothing on disk answers it; the run happens server-side | `nil` |

Claude Code computes both its window size *and* its category breakdown
(messages, system tools, skills, MCP tools, memory files) in-process and writes
Expand Down Expand Up @@ -360,6 +373,7 @@ table goes through `ArgvSanitizer`.
| Muse Code | — | `~/.local/share/muse/sessions/**/session.jsonl` |
| Devin | — | `~/.local/share/devin/cli/sessions.db` (WAL) |
| Mistral Vibe | — | `~/.vibe/logs/session/session_*/messages.jsonl` + `meta.json` |
| Muse | — | `~/Library/Caches/ConversationCache/hatch-*.json` |
| AntiGravity | ✅ `AntigravityLiveAdapter` | `~/.gemini/antigravity{-cli,}/conversations/*.db`; state from the SQL columns plus a shallow `step_payload` decode; liveness via `presence/<id>.lock` |
| Cursor | ✅ `CursorLiveAdapter` | `~/.cursor/chats/**/store.db` + `~/.cursor/projects/<slug>/agent-transcripts`; liveness via `cursor-agent-worker-*.pid` and the store's WAL |
| Grok Bot | ✅ `GrokBotLiveAdapter` | `~/Library/Application Support/Grok Bot/sand-client-persistence/<base32(key)>.blob`; the roster slice supplies the name and the needs-you flag; liveness via the `Grok Bot` process and `~/.grokbot/local-exec-supervisor.json` |
Expand Down
2 changes: 1 addition & 1 deletion implementations/rust/crates/agent-session-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "agent-session-core"
version = "0.10.0"
version = "0.11.0"
edition = "2021"
license = "AGPL-3.0-only"
description = "Cross-platform Rust implementation of agent-session-kit's session reading: index queries, lightweight discovery, transcripts, and resume commands."
Expand Down
6 changes: 3 additions & 3 deletions implementations/rust/crates/agent-session-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ let recent = reader.list(&SessionListFilter { limit: 20, ..Default::default() })
| Contract | Value |
| --- | --- |
| Index schema version | `PRAGMA user_version = 5` (any other value ⇒ refuse) |
| Provider raw values | `claude`, `claudeCowork`, `codex`, `grok`, `cursor`, `gemini`, `antigravity`, `grokBot`, `muse`, `devin`, `mistralVibe` |
| Harness display names | `Codex`, `ChatGPT Work`, `Claude Code`, `Claude Cowork`, `Gemini CLI`, `AntiGravity`, `Grok Build`, `Cursor`, `Grok Bot`, `Muse Code`, `Devin`, `Mistral Vibe` |
| Provider raw values | `claude`, `claudeCowork`, `codex`, `grok`, `cursor`, `gemini`, `antigravity`, `grokBot`, `muse`, `devin`, `mistralVibe`, `museAgent` |
| Harness display names | `Codex`, `ChatGPT Work`, `Claude Code`, `Claude Cowork`, `Gemini CLI`, `AntiGravity`, `Grok Build`, `Cursor`, `Grok Bot`, `Muse Code`, `Devin`, `Mistral Vibe`, `Muse` |
| Index timestamps | Unix epoch seconds (INTEGER) |
| FTS query | one double-quoted phrase, `""` escaping; needles < 3 chars use escaped `LIKE` |
| Resume validation | uuid charset for claude/codex/cursor/antigravity/grokBot/muse/mistralVibe ids; `[A-Za-z0-9._-]` for grok/gemini/devin; max 200 chars |
| Resume validation | uuid charset for claude/codex/cursor/antigravity/grokBot/muse/mistralVibe ids; `[A-Za-z0-9._-]` for grok/gemini/devin/museAgent; max 200 chars |
41 changes: 40 additions & 1 deletion implementations/rust/crates/agent-session-core/src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,7 @@ impl SessionIndexReader {
"Muse Code" => "museCode",
"Devin" => "devin",
"Mistral Vibe" => "mistralVibe",
"Muse" => "museAgent",
raw => raw,
}
}
Expand All @@ -777,7 +778,8 @@ impl SessionIndexReader {
WHEN 'grokBot' THEN 'grokBot' \
WHEN 'muse' THEN 'museCode' \
WHEN 'devin' THEN 'devin' \
WHEN 'mistralVibe' THEN 'mistralVibe' END)"
WHEN 'mistralVibe' THEN 'mistralVibe' \
WHEN 'museAgent' THEN 'museAgent' END)"
}

fn dedup_nonempty_strings(
Expand Down Expand Up @@ -1049,6 +1051,43 @@ mod tests {
}
}

#[test]
fn muse_agent_rows_stay_apart_from_muse_code() {
let dir = tempfile::tempdir().unwrap();
let path = fixture_index(dir.path());
let conn = Connection::open(&path).unwrap();
conn.execute_batch(
"INSERT INTO sessions(id, provider, session_id, harness, title, created_at, last_active_at, source_path) \
VALUES(6, 'museAgent', 'hatch-main', NULL, 'muse app row', 1700006000, 1700006000, \
'/Users/example/Library/Caches/ConversationCache/hatch-main.json'); \
INSERT INTO sessions(id, provider, session_id, harness, title, created_at, last_active_at, source_path) \
VALUES(7, 'muse', '01a0ac0d-5355-7c41-bc77-3b55f0e77ea1', 'museCode', 'muse cli row', \
1700007000, 1700007000, \
'/Users/example/.local/share/muse/sessions/2026/01/01/01a0ac0d-5355-7c41-bc77-3b55f0e77ea1/session.jsonl');",
)
.unwrap();
drop(conn);

let reader = SessionIndexReader::open(&path).unwrap();
for (harness, provider, title) in [
("Muse", SessionProvider::MuseAgent, "muse app row"),
("museAgent", SessionProvider::MuseAgent, "muse app row"),
("Muse Code", SessionProvider::Muse, "muse cli row"),
("museCode", SessionProvider::Muse, "muse cli row"),
] {
let rows = reader
.list(&SessionListFilter {
harnesses: Some(vec![harness.to_string()]),
limit: 10,
..Default::default()
})
.unwrap();
assert_eq!(rows.len(), 1, "{harness}");
assert_eq!(rows[0].provider, provider);
assert_eq!(rows[0].title.as_deref(), Some(title));
}
}

#[test]
fn null_harness_uses_provider_default_before_filtering_and_offset() {
let dir = tempfile::tempdir().unwrap();
Expand Down
24 changes: 23 additions & 1 deletion implementations/rust/crates/agent-session-core/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ pub enum SessionProvider {
Devin,
#[serde(rename = "mistralVibe")]
MistralVibe,
#[serde(rename = "museAgent")]
MuseAgent,
}

impl SessionProvider {
pub const ALL: [SessionProvider; 11] = [
pub const ALL: [SessionProvider; 12] = [
SessionProvider::Claude,
SessionProvider::ClaudeCowork,
SessionProvider::Codex,
Expand All @@ -42,6 +44,7 @@ impl SessionProvider {
SessionProvider::Muse,
SessionProvider::Devin,
SessionProvider::MistralVibe,
SessionProvider::MuseAgent,
];

/// Storage raw value — identical to Swift's `rawValue`.
Expand All @@ -58,6 +61,7 @@ impl SessionProvider {
SessionProvider::Muse => "muse",
SessionProvider::Devin => "devin",
SessionProvider::MistralVibe => "mistralVibe",
SessionProvider::MuseAgent => "museAgent",
}
}

Expand All @@ -82,6 +86,7 @@ impl SessionProvider {
SessionProvider::Muse => "Muse Code",
SessionProvider::Devin => "Devin",
SessionProvider::MistralVibe => "Mistral Vibe",
SessionProvider::MuseAgent => "Muse",
}
}

Expand Down Expand Up @@ -122,6 +127,22 @@ mod tests {
SessionProvider::MistralVibe.default_harness(),
"Mistral Vibe"
);
assert_eq!(SessionProvider::MuseAgent.raw_value(), "museAgent");
assert_eq!(SessionProvider::MuseAgent.default_harness(), "Muse");
assert_eq!(
serde_json::from_str::<SessionProvider>("\"museAgent\"").unwrap(),
SessionProvider::MuseAgent
);
// Muse (the desktop agent app) and Muse Code (the `muse` CLI) are
// different stores and must never collapse onto one key.
assert_ne!(
SessionProvider::Muse.raw_value(),
SessionProvider::MuseAgent.raw_value()
);
assert_ne!(
SessionProvider::Muse.default_harness(),
SessionProvider::MuseAgent.default_harness()
);
}

#[test]
Expand All @@ -134,6 +155,7 @@ mod tests {
SessionProvider::Muse,
SessionProvider::Devin,
SessionProvider::MistralVibe,
SessionProvider::MuseAgent,
] {
assert!(!provider.supports_deletion(), "{provider:?}");
}
Expand Down
24 changes: 20 additions & 4 deletions implementations/rust/crates/agent-session-core/src/resume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,14 @@ pub fn command(
SessionProvider::Muse => Ok(format!("muse resume {id}")),
SessionProvider::Devin => Ok(format!("devin --resume {id}")),
SessionProvider::MistralVibe => Ok(format!("vibe --resume {id}")),
SessionProvider::ClaudeCowork | SessionProvider::Cursor | SessionProvider::GrokBot => {
SessionProvider::ClaudeCowork
| SessionProvider::Cursor
| SessionProvider::GrokBot
| SessionProvider::MuseAgent => {
// Cowork runs inside Claude.app and Cursor's agents inside Cursor;
// neither publishes a "reopen this conversation" command. Grok Bot
// has no CLI at all.
// has no CLI at all, and Muse's agent runs in a VM on Meta's
// servers.
Err(SessionCoreError::ResumeUnavailable)
}
}
Expand Down Expand Up @@ -85,8 +89,12 @@ fn is_valid(id: &str, provider: SessionProvider) -> bool {
| SessionProvider::GrokBot
| SessionProvider::Muse
| SessionProvider::MistralVibe => id.chars().all(|c| c.is_ascii_hexdigit() || c == '-'),
// Devin names a session with a word pair (`quiet-harbor`), not a UUID.
SessionProvider::Grok | SessionProvider::Gemini | SessionProvider::Devin => id
// Devin names a session with a word pair (`quiet-harbor`), not a UUID;
// Muse's is its cache file's stem (`hatch-main`).
SessionProvider::Grok
| SessionProvider::Gemini
| SessionProvider::Devin
| SessionProvider::MuseAgent => id
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')),
}
Expand Down Expand Up @@ -170,6 +178,14 @@ mod tests {
command(SessionProvider::Devin, "quiet harbor; rm -rf /", None),
Err(SessionCoreError::InvalidSessionId)
));
assert!(matches!(
command(SessionProvider::MuseAgent, "hatch-main", None),
Err(SessionCoreError::ResumeUnavailable)
));
assert!(matches!(
command(SessionProvider::MuseAgent, "hatch main; rm -rf /", None),
Err(SessionCoreError::InvalidSessionId)
));
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public enum AgentSessionKitInfo {
///
/// Semver, bare — no `v` prefix, matching the tags. Bumping it is part
/// of the release commit; see `RELEASING.md`.
public static let version = "0.10.0"
public static let version = "0.11.0"

/// Where the package lives. Public because a host that shows the
/// version usually wants somewhere to send the reader.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Foundation
/// | Cursor | Anysphere | `~/.cursor/chats/**/store.db` |
/// | Grok Bot | xAI | `~/Library/Application Support/Grok Bot/sand-client-persistence` — a cloud cache: conversations only, no model |
/// | Muse Code | Meta AI | `~/.local/share/muse/sessions/YYYY/MM/DD/<id>/session.jsonl` |
/// | Muse | Meta AI | `~/Library/Caches/ConversationCache/hatch-*.json` — a cloud cache: conversations only, no model |
/// | Devin | Cognition | `~/.local/share/devin/cli/sessions.db` — one database, every session a row |
/// | Mistral Vibe | Mistral AI | `~/.vibe/logs/session/session_*/{meta.json,messages.jsonl}` |
///
Expand All @@ -41,6 +42,10 @@ public enum HarnessCatalog {
public static let grokBot = "Grok Bot"
/// Meta's terminal coding agent, the `muse` CLI.
public static let museCode = "Muse Code"
/// Meta's desktop agent app, `Muse.app` — muse.ai's Mac client, not the
/// `muse` CLI. Its agent runs in a VM on Meta's servers, so the local
/// store is a conversation cache and nothing a token counter could read.
public static let museAgent = "Muse"
/// Cognition's local agent: the `devin` CLI and the Devin desktop app,
/// whose "Devin Local" agent drives that CLI over ACP. Both write the one
/// session database, so they are one harness.
Expand Down Expand Up @@ -69,6 +74,7 @@ public enum Harness: String, CaseIterable, Codable, Sendable, Hashable {
case cursor
case grokBot
case museCode
case museAgent
case devin
case mistralVibe

Expand All @@ -84,6 +90,7 @@ public enum Harness: String, CaseIterable, Codable, Sendable, Hashable {
case .cursor: HarnessCatalog.cursor
case .grokBot: HarnessCatalog.grokBot
case .museCode: HarnessCatalog.museCode
case .museAgent: HarnessCatalog.museAgent
case .devin: HarnessCatalog.devin
case .mistralVibe: HarnessCatalog.mistralVibe
}
Expand Down
Loading
Loading