Import a Claude Code session the machine already has
Claude Code keeps every session as JSONL under `~/.claude/projects/`, and the CLI continues one with `--resume <id>`. `claude.rs` already resumes whenever it finds a resume token in the session directory, for crash recovery -- so importing is that same path with the token written before the driver starts, and there is deliberately no second way to begin a session. The seed goes through `launch` with the ordinary spawn, so the driver never learns which kind it got. Two things the machine answers and the phone does not. **Which sessions exist.** One command per setup rather than one per file, for the reason discovery already gives: over ssh each would be its own connection. Titles come from the first few user records rather than the first, because a session opens with records the CLI injected -- slash commands, caveats around local command output -- which are stored as ordinary user records without the meta flag, so titling by "first user record" produced a list where most rows read `<command-name>/clear`. **Which file an id names.** The phone sends an id and never a path; the server looks it up again among the sessions it enumerated. An enrolled token must not be able to turn a spawn into "read me this file", which is the same rule that keeps a provider's command out of `POST /setups`. Only the tail is replayed. The imported conversation is for reading -- continuing it is the CLI's job, and it reads the whole file itself -- so this is a display budget, and it has to be one: the session this was written in is 39 MB, and all of it would otherwise cross a tunnel to a phone. A recorded working directory can outlive itself, which this found immediately: every session from before the checkouts moved to `~/repos` still records `~/host/repos/...`. Resuming into one fails at `cd` before the CLI starts -- a confusing way to meet a feature whose promise is "carry on where you left off" -- so the directory is checked, and a missing one is dropped with a log line naming it rather than being passed on to fail. Verified against this very session: 905 events replayed from the tail (351 tool calls, 350 results, 185 assistant messages, 19 mine), the resume token pointing at its id, and the stale directory reported and dropped. The list was read on the emulator, where the top row is that session under its opening sentence.
This commit is contained in:
1 parent
2a1bc84c1e
commit
6bbc829a3e
9 files changed
+726
-19
No files matched your search
@@ -12,6 +12,7 @@
|
||||
pub mod claude;
|
||||
pub mod driver;
|
||||
pub mod echo;
|
||||
pub mod import;
|
||||
pub mod llama;
|
||||
pub mod transcript;
|
||||
pub mod transport;
|
||||
@@ -225,7 +226,14 @@ impl SessionManager {
|
||||
// shows as exited rather than taking the whole server down
|
||||
// with it, and can still be deleted from the phone.
|
||||
match resolve(&config, meta).and_then(|(setup, provider)| {
|
||||
launch(meta.clone(), &setup, &provider, &data_dir, &models_dir)
|
||||
launch(
|
||||
meta.clone(),
|
||||
&setup,
|
||||
&provider,
|
||||
&data_dir,
|
||||
&models_dir,
|
||||
None,
|
||||
)
|
||||
}) {
|
||||
Ok(session) => {
|
||||
live.insert(meta.id.clone(), session);
|
||||
@@ -475,6 +483,21 @@ impl SessionManager {
|
||||
}
|
||||
|
||||
pub fn spawn_session(&self, spec: SpawnSpec) -> Result<SessionInfo> {
|
||||
self.spawn_seeded(spec, None)
|
||||
}
|
||||
|
||||
/// Spawns a session that continues one the machine already had.
|
||||
///
|
||||
/// The same path as any other spawn, with a [`Seed`] written into the
|
||||
/// session directory before the driver starts -- which is all an
|
||||
/// import is, because `claude.rs` already resumes when it finds a
|
||||
/// resume token. A separate spawn path would be a second way to start
|
||||
/// a session, and the driver would have to learn which one it was.
|
||||
pub fn spawn_imported(&self, spec: SpawnSpec, seed: Seed) -> Result<SessionInfo> {
|
||||
self.spawn_seeded(spec, Some(seed))
|
||||
}
|
||||
|
||||
fn spawn_seeded(&self, spec: SpawnSpec, seed: Option<Seed>) -> Result<SessionInfo> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
let setup = inner
|
||||
.config
|
||||
@@ -525,6 +548,7 @@ impl SessionManager {
|
||||
&provider,
|
||||
&self.data_dir,
|
||||
&self.models_dir,
|
||||
seed,
|
||||
)?;
|
||||
let mut candidate = inner.config.clone();
|
||||
candidate.sessions.push(meta);
|
||||
@@ -642,17 +666,38 @@ fn unique_id(config: &Config) -> String {
|
||||
/// Creates the session directory, opens its transcript (continuing the
|
||||
/// sequence numbering if one exists), starts the driver, and spawns the
|
||||
/// event pump connecting them.
|
||||
/// What an imported session starts life with: the token that makes the CLI
|
||||
/// continue rather than begin, and the conversation so far.
|
||||
pub struct Seed {
|
||||
/// The CLI's own session id, written where `claude.rs` looks for it.
|
||||
pub resume: String,
|
||||
/// Replayed into the transcript so the phone shows the conversation it
|
||||
/// is joining. The CLI reads the real file itself, so this is what the
|
||||
/// reader sees rather than what the model is given.
|
||||
pub events: Vec<Event>,
|
||||
}
|
||||
|
||||
fn launch(
|
||||
meta: SessionConfig,
|
||||
setup: &SetupConfig,
|
||||
provider: &ProviderConfig,
|
||||
data_dir: &Path,
|
||||
models_dir: &Path,
|
||||
seed: Option<Seed>,
|
||||
) -> Result<Arc<LiveSession>> {
|
||||
let dir = data_dir.join(&meta.id);
|
||||
wg_app_link::private::create_dir(&dir)?;
|
||||
let transcript_path = dir.join("transcript.jsonl");
|
||||
let transcript = Transcript::open(&transcript_path)?;
|
||||
let mut transcript = Transcript::open(&transcript_path)?;
|
||||
// Before the driver starts, so the token is there when it looks and
|
||||
// the history is already in the transcript a phone will read.
|
||||
if let Some(seed) = seed {
|
||||
claude::write_resume_token(&dir, &seed.resume);
|
||||
let at = now();
|
||||
for event in seed.events {
|
||||
transcript.append(event, at)?;
|
||||
}
|
||||
}
|
||||
|
||||
let (sink, source) = mpsc::unbounded_channel();
|
||||
let (events, _) = broadcast::channel(EVENT_BUFFER);
|
||||
|
||||
Reference in new issue
Block a user