Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
133 commits
Select commit Hold shift + click to select a range
fdc6bbc
refactor(freshagent): share plain-text snapshot capture renderer
danshapiro Oct 23, 2026
28913bc
feat(freshclaude): resolve REST create defaults with picker parity
danshapiro Oct 23, 2026
180fd6e
refactor(freshagent): generalize the REST lane interface
danshapiro Oct 23, 2026
c625956
Merge commit '0a06588e8' into feat/freshclaude-rest-lane
danshapiro Oct 23, 2026
eb1ef64
feat(freshclaude): allow CLI and MCP create split and resume
danshapiro Oct 23, 2026
717ffb2
feat(freshclaude): prepare REST creates with final context and shared…
danshapiro Oct 23, 2026
74150f9
feat(freshclaude): route REST control through the local Claude lane
danshapiro Oct 23, 2026
7fd997a
fix(freshclaude): correlate sends and runtime ends by incarnation
danshapiro Oct 23, 2026
0152e24
test(freshclaude): reserve durable ownership epochs in the sidecar rig
danshapiro Oct 23, 2026
64a5eba
feat(freshclaude): implement local REST runtime control and capture
danshapiro Oct 23, 2026
9b2f7e6
Merge durable create-time session identity foundation
danshapiro Oct 23, 2026
19440f3
feat(freshclaude): establish durable UUID identity before create ackn…
danshapiro Oct 23, 2026
b51bc34
feat(freshclaude): announce conversation resets from the actual next …
danshapiro Oct 23, 2026
eff21d0
Merge typed refusal outcomes and UUID-aware browser fixtures
danshapiro Oct 23, 2026
48bf918
fix(freshclaude): preserve REST pane identity and failed split layout
danshapiro Oct 23, 2026
dcfcf38
fix(freshclaude): retain ownership and retry details from refusals
danshapiro Oct 23, 2026
85a449e
fix(freshclaude): preserve REST refusal metadata and await saved test…
danshapiro Oct 23, 2026
3287ba0
test(freshclaude): cover external MCP control in the browser
danshapiro Oct 23, 2026
0cd45cd
test(freshclaude): require correlated interrupt failure acknowledgements
danshapiro Oct 23, 2026
664ddd2
fix(freshclaude): hydrate legacy panes with their durable identity
danshapiro Oct 23, 2026
e2ae852
feat(freshclaude): follow conversation resets with one live runtime
danshapiro Oct 23, 2026
dd7ab07
fix(freshclaude): keep REST control on durable and accepted identities
danshapiro Oct 23, 2026
a22bae6
Merge branch 'feat/freshclaude-session-identity' into feat/freshclaud…
danshapiro Oct 23, 2026
d96dc63
fix(freshclaude): honor replaced pane content in REST controls
danshapiro Oct 23, 2026
cf758ab
fix(freshclaude): re-resolve kill target after rekey
danshapiro Oct 23, 2026
7a6c527
test(claude): match clear transcript storage to pinned CLI
danshapiro Oct 23, 2026
b8f019f
docs(freshclaude): show fork controls and describe durable reset frames
danshapiro Oct 23, 2026
7dd5ddf
Merge transparent Claude runtime recovery for identity integration
danshapiro Oct 23, 2026
89bac41
Merge local Claude REST control and CLI-conformant reset fixtures
danshapiro Oct 23, 2026
2036101
Merge commit '89bac41a1' into feat/freshclaude-c30-browser-fakes
danshapiro Oct 23, 2026
9b5912e
fix(opencode): retain REST capture after materialized layout sync
danshapiro Oct 23, 2026
0e87486
Merge commit '89bac41a1' into feat/freshclaude-rest-lane
danshapiro Oct 23, 2026
a69cde0
fix(freshclaude): preserve REST recovery refusal semantics
danshapiro Oct 23, 2026
9bef732
test(freshclaude): verify external Kilroy submission and saved history
danshapiro Oct 23, 2026
aa447fd
fix(freshclaude): let native rekeys advance past pending controls
danshapiro Oct 23, 2026
f2ad589
feat(freshclaude): acknowledge canonical attaches to the requesting page
danshapiro Oct 23, 2026
209fbc2
Merge commit 'f2ad58952' into feat/freshclaude-restart-notice
danshapiro Oct 23, 2026
51a92eb
Merge correlated Claude control failures and incarnation-fenced Stop …
danshapiro Oct 23, 2026
f0893f1
Merge Claude quiet detection and capped heartbeat tracking
danshapiro Oct 23, 2026
9fdbd13
Integrate merged attention replay and provider fixture repairs
danshapiro Oct 23, 2026
b294d08
fix(agent-api): preserve pending pane publications across layout sync
danshapiro Oct 23, 2026
5a48a70
Merge commit 'f2ad58952' into feat/freshclaude-rest-lane
danshapiro Oct 23, 2026
5676fd3
feat(freshclaude): report turns interrupted by restart
danshapiro Oct 23, 2026
534e173
fix(freshclaude): retain native name on create replay
danshapiro Oct 23, 2026
3816fdf
Merge commit 'f2ad58952' into feat/freshclaude-c30-browser-fakes
danshapiro Oct 23, 2026
99719fb
test(e2e): run Claude browser scenarios through production sidecar
danshapiro Oct 23, 2026
2c7b8b7
fix(freshclaude): fence abandoned create cleanup by runtime owner
danshapiro Oct 23, 2026
edb054c
docs: describe Rust Claude attention and process recovery
danshapiro Oct 23, 2026
a93c48f
Merge commit 'f88efb095' into feat/freshclaude-session-identity
danshapiro Oct 23, 2026
8d96881
refactor(agent-api): finalize deferred creates before publication
danshapiro Oct 23, 2026
4b92dec
Merge commit '2c7b8b711' into feat/freshclaude-rest-lane
danshapiro Oct 23, 2026
e492eaa
Merge commit 'cbf8bee39' into feat/freshclaude-rest-lane
danshapiro Oct 23, 2026
15d9522
refactor(freshclaude): bind names without placeholder state
danshapiro Oct 23, 2026
cac3a06
Merge commit 'cbf8bee39' into feat/freshclaude-session-identity
danshapiro Oct 23, 2026
d214c80
fix(freshclaude): quiesce turns before restart notice
danshapiro Oct 23, 2026
635fb3c
fix(freshclaude): report cleanup failures and match the owned process
danshapiro Oct 23, 2026
77e8457
Merge commit 'd214c80b6' into feat/freshclaude-session-identity
danshapiro Oct 23, 2026
c80f298
Merge commit 'bea2e0d65' into feat/freshclaude-session-identity
danshapiro Oct 23, 2026
fb6edd6
style(freshclaude): format cleanup owner predicate
danshapiro Oct 24, 2026
6d5eca3
feat(freshclaude): carry create ownership into REST cancellation
danshapiro Oct 24, 2026
9d71b9b
Merge commit '635fb3c7f' into feat/freshclaude-rest-lane
danshapiro Oct 24, 2026
323b0cf
fix(freshclaude): verify native identity after runtime recovery
danshapiro Oct 24, 2026
e9f5241
Merge commit '2c35262fa' into feat/freshclaude-session-identity
danshapiro Oct 24, 2026
6425d37
test(e2e): log fresh Claude cloud failure context
danshapiro Oct 24, 2026
f286d10
test(e2e): align Claude scenarios with SDK behavior
danshapiro Oct 24, 2026
7db5bc2
fix(testing): isolate sandbox Rust builds by host worktree
danshapiro Oct 24, 2026
2780cd0
fix(agent-api): clean abandoned hosted creates by captured intent
danshapiro Oct 24, 2026
551d894
test(freshclaude): match buffered clear history to the pinned CLI
danshapiro Oct 24, 2026
8e05e67
test(freshclaude): verify abandoned create cleanup with real runtimes
danshapiro Oct 24, 2026
8e8d224
Merge commit '323b0cf5e' into feat/freshclaude-rest-lane
danshapiro Oct 24, 2026
e8e4eea
test(e2e): surface Claude approval diagnostics in failures
danshapiro Oct 24, 2026
dd02ea6
Merge commit 'e5273c543' into feat/freshclaude-session-identity
danshapiro Oct 24, 2026
2075811
refactor(agent-api): simplify pending publication close retention
danshapiro Oct 24, 2026
9749cee
fix(agent-api): announce finalized creates before concurrent closes
danshapiro Oct 24, 2026
70adfa4
refactor(freshclaude): persist settings without repeating adoption
danshapiro Oct 24, 2026
76ab17b
Merge commit '9749ceef5' into feat/freshclaude-session-identity
danshapiro Oct 24, 2026
55e4490
test: exercise Kilroy unified-name exclusion
danshapiro Oct 24, 2026
e53a7fd
test(freshclaude): run Rust fixtures through the production sidecar
danshapiro Oct 24, 2026
72d9f9b
test(freshclaude): drive retained controls through real SDK requests
danshapiro Oct 24, 2026
342aceb
test(freshclaude): expose native lineage loss after unwired recovery
danshapiro Oct 24, 2026
f9de976
Merge commit '24dcc1495' into feat/freshclaude-session-identity
danshapiro Oct 24, 2026
b2ef05f
Merge commit '72d9f9b4e' into feat/freshclaude-session-identity
danshapiro Oct 24, 2026
0441eff
refactor(freshclaude): model native runtime identity and close guards
danshapiro Oct 24, 2026
2f094af
test(freshclaude): observe real bootstrap and resume outcomes
danshapiro Oct 24, 2026
d5adc25
test(claude): model transcript-path resume from pinned CLI evidence
danshapiro Oct 24, 2026
eb09e40
test(e2e): align Claude browser scenarios with SDK behavior
danshapiro Oct 24, 2026
261254c
test(freshclaude): exercise WS kill and Stop through the real sidecar
danshapiro Oct 24, 2026
32c42ca
test(freshclaude): crash the real sidecar after an actual assistant f…
danshapiro Oct 24, 2026
456a46d
test(freshclaude): fence checkpointed sends with create-time identity
danshapiro Oct 24, 2026
a5dfafb
test(freshclaude): observe create-time ownership before the first CLI…
danshapiro Oct 24, 2026
3bd6280
test(freshclaude): drive checkpointed sends through real SDK lifecycles
danshapiro Oct 24, 2026
2e6694e
refactor(freshclaude): route native lineage without placeholder machi…
danshapiro Oct 24, 2026
4adabe7
Merge commit '3bd6280e0' into feat/freshclaude-session-identity
danshapiro Oct 24, 2026
eb3b2cf
test(freshclaude): prove REST ownership parity with a real SDK runtime
danshapiro Oct 24, 2026
4416318
Merge commit '4adabe734' into test/freshclaude-c30-runtime-fakes
danshapiro Oct 24, 2026
1263237
refactor(freshclaude): establish every runtime under its native UUID
danshapiro Oct 24, 2026
04d1fe1
refactor(freshclaude): remove obsolete identity comments and attributes
danshapiro Oct 24, 2026
819eeb5
fix(fresh-agent): retain durable identity with restore errors
danshapiro Oct 24, 2026
0f42fb7
test(e2e): assert real Claude recovery and reveal behavior
danshapiro Oct 24, 2026
8ba12d2
refactor(freshclaude): retain a native identity in ended runtime records
danshapiro Oct 24, 2026
efd4248
test(freshclaude): reconcile actual native sidecar sessions
danshapiro Oct 24, 2026
078b976
test(freshclaude): verify signal conflicts with a real sidecar owner
danshapiro Oct 24, 2026
0009932
test(freshclaude): attach and send through native SDK sessions
danshapiro Oct 24, 2026
98564cc
fix(fresh-agent): preserve non-Claude session id on restore error
danshapiro Oct 24, 2026
19d45f9
test(freshclaude): exercise lease races through real SDK initialization
danshapiro Oct 24, 2026
439eaea
test(freshclaude): run cross-kind ownership races with native SDK ses…
danshapiro Oct 24, 2026
b867038
fix(storage): retain Claude identity through boot migration
danshapiro Oct 24, 2026
0ca5466
fix(freshclaude): follow native resets in attach and quiet deadlines
danshapiro Oct 24, 2026
8a7f445
test(e2e): use native compact history and create attention workspace
danshapiro Oct 24, 2026
34aefc8
test(freshclaude): replace obsolete adoption fixtures with native run…
danshapiro Oct 24, 2026
94b4db0
test(e2e): isolate Claude reveal and restore liveness
danshapiro Oct 24, 2026
6fb870b
Merge commit '439eaeac6' into feat/freshclaude-session-identity
danshapiro Oct 24, 2026
ee60620
test(freshclaude): exercise hosted MCP and settings through the real …
danshapiro Oct 24, 2026
633ef6f
test(freshclaude): run live-frame and handoff checks through the real…
danshapiro Oct 24, 2026
f30cca4
Merge commit '94b4db00b' into feat/freshclaude-session-identity
danshapiro Oct 24, 2026
a2cef81
docs(freshclaude): describe native create and naming establishment
danshapiro Oct 24, 2026
4b29eff
test(e2e): run Kilroy harness through the production sidecar
danshapiro Oct 24, 2026
bb81f94
test(e2e): locate Claude restore transcript by ID
danshapiro Oct 24, 2026
2e6bb9d
test(e2e): wait for Claude naming exchange before switch
danshapiro Oct 24, 2026
5e82279
Merge commit 'a2cef8120' into feat/freshclaude-c30-browser-fakes
danshapiro Oct 24, 2026
d8fc122
test(e2e): let scenario env override provider fixture
danshapiro Oct 24, 2026
0bcefa1
fix(freshclaude): close persisted native successors after restart
danshapiro Oct 24, 2026
b81c52e
Merge origin/main recovery while retaining native runtime and SDK tests
danshapiro Oct 24, 2026
65192f1
test: use native Claude identity in cross-tab state fixture
danshapiro Oct 24, 2026
e90e56c
fix: stream cloud verification output without buffering
danshapiro Oct 24, 2026
e754ba5
docs(freshclaude): describe native runtime cleanup in retained tests
danshapiro Oct 24, 2026
5ddfeed
style(freshclaude): format combined native attach handling
danshapiro Oct 24, 2026
b9f771e
fix(test): prepare managed runtime binaries before startup hooks
danshapiro Oct 24, 2026
846ad12
test(freshclaude): align provider contracts with native IPC
danshapiro Oct 24, 2026
2a469dc
fix(opencode): release revival ownership before session waiters
danshapiro Oct 24, 2026
c100715
chore(rust): replace deprecated atomic update calls
danshapiro Oct 24, 2026
1b21bae
docs: explain sandbox selection for agent test runs
danshapiro Oct 24, 2026
b6fb292
test(codex): confirm fixture death before crash recovery
danshapiro Oct 24, 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
5 changes: 3 additions & 2 deletions AGENTS.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.lock

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

7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,13 @@ turning it off removes the server from Freshclaude and Kilroy agents started or
resumed after the change, with no restart. Terminal panes do not follow the
switch yet.

With fresh agents enabled, the CLI and MCP client can create Freshclaude or
Kilroy tabs, split them into panes, send prompts, and capture their conversation
as plain text. These operations also work without an open browser. Use
`agent: "claude"` or `agent: "kilroy"` with MCP `new-tab` and `split-pane`, or
`--agent claude` / `--agent kilroy` with the CLI. Both support resuming a saved
Claude conversation through `resume`.

### Rust server scope

The Rust server supports the browser UI, terminal and session workflows, the
Expand Down
1 change: 1 addition & 0 deletions config/vitest/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['./test/setup/dom.ts'],
globalSetup: ['./test/setup/runtime-build-preflight.ts'],
exclude: [
'test/runtime/**',
'**/node_modules/**',
Expand Down
1 change: 1 addition & 0 deletions crates/freshell-agent-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub use qualification_policy::{
};
pub mod host_actor;
pub mod snapshot_projection;
pub mod snapshot_text;

#[cfg(test)]
#[path = "snapshot_projection_tests.rs"]
Expand Down
95 changes: 95 additions & 0 deletions crates/freshell-agent-runtime/src/snapshot_text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! Plain-text capture shared by local and hosted fresh-agent REST lanes.
//! The byte cap always ends at a UTF-8 boundary.

use serde_json::Value;

/// Render role-prefixed turn text, commands, output and error-only turns.
pub fn render_snapshot_text(snapshot: &Value, max_bytes: usize) -> (String, bool) {
let mut output = String::new();
if let Some(turns) = snapshot.get("turns").and_then(Value::as_array) {
for turn in turns {
let role = turn
.get("role")
.and_then(Value::as_str)
.unwrap_or("assistant");
let mut body = Vec::new();
if let Some(items) = turn.get("items").and_then(Value::as_array) {
for item in items {
if let Some(text) = item.get("text").and_then(Value::as_str) {
body.push(text);
} else if let Some(command) = item.get("command").and_then(Value::as_str) {
body.push(command);
} else if let Some(result) = item.get("output").and_then(Value::as_str) {
body.push(result);
}
}
}
if body.is_empty() {
// An item-less turn carrying only the durable error module
// (a claude API error or failed local command) reads as its
// error; anything else falls back to the summary.
if let Some(message) = turn.pointer("/error/message").and_then(Value::as_str) {
body.push(message);
} else if let Some(summary) = turn.get("summary").and_then(Value::as_str) {
body.push(summary);
}
}
if !body.is_empty() {
output.push_str(role);
output.push_str(": ");
output.push_str(&body.join("\n"));
output.push('\n');
}
}
}
if output.len() <= max_bytes {
return (output, false);
}
let mut boundary = max_bytes;
while boundary > 0 && !output.is_char_boundary(boundary) {
boundary -= 1;
}
output.truncate(boundary);
(output, true)
}

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

#[test]
fn snapshot_text_renderer_is_plain_and_utf8_bounded() {
let snapshot = json!({"turns":[
{"role":"user","summary":"ignored","items":[{"kind":"text","text":"héllo"}]},
{"role":"assistant","summary":"world","items":[]}
]});
let (full, truncated) = render_snapshot_text(&snapshot, 100);
assert_eq!(full, "user: héllo\nassistant: world\n");
assert!(!truncated);
let (short, truncated) = render_snapshot_text(&snapshot, 8);
assert!(short.len() <= 8);
assert!(short.is_char_boundary(short.len()));
assert!(truncated);
}

#[test]
fn snapshot_text_renders_error_only_turns_by_their_error_message() {
// Real claude snapshots (pinned CLI 2.1.237 goldens): a failed /compact
// and an API error are item-less turns carrying only the error module.
for (golden, expected) in [
(
include_str!("../../../test/fixtures/fresh-agent/claude/real-cli-2.1.237/compact-fail/snapshot.golden.json"),
"system: Error during compaction: API Error: 400 compaction refused by fake\n",
),
(
include_str!("../../../test/fixtures/fresh-agent/claude/real-cli-2.1.237/api-error/snapshot.golden.json"),
"assistant: API Error: 400 fixture bad request\n",
),
] {
let snapshot: Value = serde_json::from_str(golden).unwrap();
let (text, _) = render_snapshot_text(&snapshot, 1 << 20);
assert!(text.contains(expected), "{text}");
}
}
}
27 changes: 21 additions & 6 deletions crates/freshell-claude-sidecar/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@
// sidecar → Rust (stdout, one JSON per line):
// { type:'created', requestId, sessionId, protocol:2, cliPid }
// — sent only once the CLI booted (initializationResult() resolved,
// T2); sessionId is the SDK bridge's BARE nanoid placeholder and
// cliPid the CLI's pid. The command catalog follows it.
// T2); sessionId is the bridge's control id (the requested UUID
// when provided), and cliPid the CLI's pid. The command catalog
// follows it. Control messages keep using this bridge id after
// a conversation reset; Rust follows the new durable id below.
// { type:'create.failed', requestId, code, message }
// — the CLI refused the boot (boot.mjs names the codes) or the
// request is not protocol v2. Nothing from the refused boot is
Expand All @@ -46,6 +48,9 @@
// { type:'sdk.configured', sessionId, requestId, ok, settings?, message? }
// { type:'sdk.session.init', sessionId, cliSessionId, model, cwd, tools, capabilities? } // durable Claude UUID;
// capabilities: the CLI's (msg_lifecycle_v1 says it reports every sent command's lifecycle)
// { type:'sdk.session.rekeyed', sessionId, newSessionId, reason:'conversation_reset' }
// — before the post-reset init, names its actual durable UUID.
// conversation_reset.new_conversation_id is only a reset nonce.
// { type:'sdk.assistant', sessionId, content, model, uuid, messageId, parentToolUseId }
// — one per completed content block (blocks of one API message share
// messageId); parentToolUseId is set inside a subagent, else null
Expand Down Expand Up @@ -109,10 +114,9 @@
// ({ severity, component, event, ...ids }, log.mjs). Rust re-emits each
// through `tracing` at its severity (claude_sidecar_io.rs).
//
// The Rust side normalizes `sdk.* -> freshAgent.*` (a port of
// server/fresh-agent/sdk-events.ts) and wraps each in a `freshAgent.event`
// envelope — so this sidecar is the faithful analog of server/sdk-bridge.ts's
// SdkBridge (it emits the SAME `sdk.*` shapes SdkBridge broadcasts).
// Rust maps content facts into `freshAgent.event` envelopes. Its per-session
// turn ledger owns busy status, single-command Stop targets, and attention
// edges; process EOF owns death and recovery. This bridge forwards CLI facts.
//
// This sidecar makes no turn or attention decision (design §3.1, C18): it
// forwards facts, and the server owns busy, the user's Stop, and every
Expand Down Expand Up @@ -286,6 +290,12 @@ function handleSdkMessage(sessionId, msg) {
for (const frame of lifecycleFrames(sessionId, msg, st.lifecycle)) emit(frame)

switch (msg.type) {
case 'conversation_reset': {
// The reset's new_conversation_id is a nonce, not the next session UUID
// (pinned CLI T16). Its next init identifies the conversation to follow.
st.pendingConversationReset = true
break
}
case 'user': {
// The SDK replays actual persisted user messages when this query opts in.
// Only the unique UUID that this sidecar supplied for a checkpointed send
Expand All @@ -296,6 +306,10 @@ function handleSdkMessage(sessionId, msg) {
}
case 'system': {
if (msg.subtype === 'init') {
if (st.pendingConversationReset && isUuid(msg.session_id)) {
st.pendingConversationReset = false
emit({ type: 'sdk.session.rekeyed', sessionId, newSessionId: msg.session_id, reason: 'conversation_reset' })
}
st.cliSessionId = msg.session_id
st.settings.cwd ??= msg.cwd
st.terminalCommandNames = Array.isArray(msg.terminal_slash_commands) ? msg.terminal_slash_commands : []
Expand Down Expand Up @@ -447,6 +461,7 @@ function handleCreate(req) {
abort,
permissionMode: req.permissionMode,
settings: { model: req.model, effort: req.effort, permissionMode: req.permissionMode, cwd: req.cwd },
pendingConversationReset: false,
// False until `created` is sent; until then nothing reaches Rust and
// bootEvidence collects what a refusal is named from (boot.mjs).
booted: false,
Expand Down
Loading
Loading