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
f263ed8
refactor(freshclaude): one launch spec and one establish path (G15)
danshapiro Oct 23, 2026
202dc31
refactor(freshclaude): handle_create returns ClaudeCreateOutcome (I1)
danshapiro Oct 23, 2026
d526a30
test(freshclaude): the default-model e2e spec reads the v2 boot in th…
danshapiro Oct 23, 2026
95b02b8
docs(freshclaude): the create's claim comments name establish_runtime
danshapiro Oct 23, 2026
2e3f433
feat(freshclaude): genesis in the binding row; genesis-aware snapshot…
danshapiro Oct 23, 2026
33df6f5
feat(freshclaude): a pending session boots from its genesis; the gene…
danshapiro Oct 23, 2026
948686d
refactor(freshclaude): the result-time genesis check runs off the sid…
danshapiro Oct 23, 2026
67c965b
test(claude): pin the drops-turn guard on an absorbed send; the simul…
danshapiro Oct 23, 2026
b8a2aaa
fix(freshclaude): rollback steps follow the turns the pane renders (N7)
danshapiro Oct 23, 2026
0e6cfec
test(claude): pin the drops-turn guard on a background agent's notifi…
danshapiro Oct 23, 2026
710af49
fix(freshclaude): undo/redo through a boot-verified fork runtime (G01)
danshapiro Oct 23, 2026
b6d01fd
feat(freshclaude): fork a conversation (G09)
danshapiro Oct 23, 2026
ce423aa
Merge origin/feat/freshclaude-core-lifecycle into feat/freshclaude-co…
danshapiro Oct 23, 2026
25d7573
docs(freshclaude): the genesis round-trip doc sits on its own test (F9)
danshapiro Oct 23, 2026
4680d84
fix(freshclaude): a create that resumes a pending session boots its g…
danshapiro Oct 23, 2026
3af3fc3
fix(freshclaude): an undo's replacement launches with the session's l…
danshapiro Oct 23, 2026
041f475
fix(freshclaude): a fork whose pane closed the parent while the child…
danshapiro Oct 23, 2026
969b665
fix(freshclaude): a configure or /compact parked behind an undo follo…
danshapiro Oct 23, 2026
5e23c1a
feat(freshclaude): the typed boot retries of the boot decision (F3)
danshapiro Oct 23, 2026
be4eaec
test(freshclaude): one ledger-backed identity sink for the ws tests (F6)
danshapiro Oct 23, 2026
db5268d
refactor(freshclaude): drop the pre-v2 rollback's adoption and park m…
danshapiro Oct 23, 2026
d249fdd
refactor(freshclaude): one lease claim and one genesis binding write …
danshapiro Oct 23, 2026
1eafb7c
test(freshclaude): e2e lane for the honest undo refusal (F8, decision 2)
danshapiro Oct 23, 2026
4f2edcf
Merge origin/main into feat/freshclaude-core-history
danshapiro Oct 23, 2026
178401c
test(claude-sidecar): close the exiting sidecar's stdout reader in a …
danshapiro Oct 23, 2026
0ecfb65
test(freshell-ws): wait for a runtime's exit through the shared Proce…
danshapiro Oct 23, 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
3,912 changes: 874 additions & 3,038 deletions crates/freshell-freshagent/src/claude.rs

Large diffs are not rendered by default.

115 changes: 115 additions & 0 deletions crates/freshell-freshagent/src/claude/create_outcome.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//! What a claude create answered (core-runtime design §9.3, I1).
//!
//! [`FreshClaudeState::handle_create`] answers every create on the WS bus
//! (`freshAgent.created` or `freshAgent.create.failed`) and also returns the
//! answer as a [`ClaudeCreateOutcome`], so an in-process caller (the REST/MCP agent
//! API) can act on it without reading the bus. A failed create's frame is built
//! from its outcome, so the two can never disagree; the WS dispatcher ignores the
//! value.

use freshell_protocol::{FreshAgentCreateFailed, ServerMessage, SessionLocator};

use crate::ownership_lane::TerminalOwnerFields;

/// The answer to one claude `freshAgent.create`.
#[derive(Debug, Clone, PartialEq)]
pub enum ClaudeCreateOutcome {
/// A session runs: the one this create started, or the live one it adopted.
Created {
/// The id the pane addresses the session by (`freshAgent.created.sessionId`).
session_id: String,
/// The durable conversation, when known (a resume or an adopted session).
session_ref: Option<SessionLocator>,
},
/// No session runs for this create.
Failed {
code: String,
message: String,
/// `Some(true)` when the client should retry the same create.
retryable: Option<bool>,
/// The runtime that owns the session instead, when the refusal names one.
owner: Option<TerminalOwnerFields>,
},
}

impl ClaudeCreateOutcome {
/// The `freshAgent.create.failed` frame answering `request_id` for a failed
/// create; `None` for a created one.
pub(super) fn create_failed_frame(&self, request_id: &str) -> Option<ServerMessage> {
let Self::Failed {
code,
message,
retryable,
owner,
} = self
else {
return None;
};
Some(ServerMessage::FreshAgentCreateFailed(
FreshAgentCreateFailed {
owner_kind: owner.as_ref().map(|o| o.owner_kind.to_string()),
owner_generation: owner.as_ref().map(|o| o.owner_generation),
owner_epoch: owner.as_ref().map(|o| o.owner_epoch),
code: code.clone(),
message: message.clone(),
request_id: request_id.to_string(),
retryable: *retryable,
},
))
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn a_failed_outcome_is_its_create_failed_frame() {
let refused = ClaudeCreateOutcome::Failed {
code: "SESSION_RESERVED".into(),
message: "Another resume for this session is in flight".into(),
retryable: Some(true),
owner: Some(TerminalOwnerFields {
owner_kind: "terminal",
owner_generation: 4,
owner_epoch: 9,
}),
};
assert_eq!(
serde_json::to_value(refused.create_failed_frame("req-1").unwrap()).unwrap(),
serde_json::json!({
"type": "freshAgent.create.failed",
"code": "SESSION_RESERVED",
"message": "Another resume for this session is in flight",
"requestId": "req-1",
"retryable": true,
"ownerKind": "terminal",
"ownerGeneration": 4,
"ownerEpoch": 9,
})
);

// No retry hint and no owner: neither key is on the wire.
let failed = ClaudeCreateOutcome::Failed {
code: "CLAUDE_CREATE_FAILED".into(),
message: "boom".into(),
retryable: None,
owner: None,
};
assert_eq!(
serde_json::to_value(failed.create_failed_frame("req-2").unwrap()).unwrap(),
serde_json::json!({
"type": "freshAgent.create.failed",
"code": "CLAUDE_CREATE_FAILED",
"message": "boom",
"requestId": "req-2",
})
);

let created = ClaudeCreateOutcome::Created {
session_id: "s".into(),
session_ref: None,
};
assert!(created.create_failed_frame("req-3").is_none());
}
}
Loading
Loading