Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
c53248f
docs: add implementation plan for amplifier-integration-fixes
danshapiro Aug 3, 2026
8e57b7e
docs: plan amendment — systemd unit sets PATH/FRESHELL_CLAUDE_NODE, E…
danshapiro Aug 3, 2026
dba059c
docs: harden plan with load-bearing validation findings (10 verified …
danshapiro Aug 3, 2026
2a749f3
docs(plan): fix fresheyes blocking issues — self-excluding --apply ga…
danshapiro Aug 3, 2026
e49c330
feat(sessions): resolve amplifier bundle.active from merged settings …
danshapiro Aug 3, 2026
c62b619
feat(sessions): stamp user's active bundle into new amplifier session…
danshapiro Aug 3, 2026
1b83897
test(ws): amplifier stub contract now asserts stamped bundle end-to-end
danshapiro Aug 3, 2026
35798aa
test(contract): mirror bundle-stamped stub shape in real-CLI adoption…
danshapiro Aug 3, 2026
300ccc7
fix(scripts): detach freshell-server into its own session (setsid) so…
danshapiro Aug 3, 2026
c0a6155
feat(installers): optional systemd user unit for the rust server; doc…
danshapiro Aug 3, 2026
c6ff552
fix(terminal): hard-cap agent-mode idle reaping at 24h (spare busy-qu…
danshapiro Aug 3, 2026
62e2eaa
test(ws): pin idle-reaper agent hard cap and re-point reap-reconcile …
danshapiro Aug 3, 2026
f920264
feat(scripts): one-time amplifier bundle backfill (dry-run default, l…
danshapiro Aug 3, 2026
41e1b66
fix(terminal): include gemini/kimi in the idle-reaper agent-mode hard…
danshapiro Aug 3, 2026
1dfe4bf
Merge fix/amplifier-integration-fixes: Amplifier integration fixes (b…
danshapiro Aug 3, 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
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ Key facts:
- The server loads `.env` from its cwd (env vars win over the file) and refuses to start without `AUTH_TOKEN`. Note `.env`'s `PORT` may differ from the live port — the launcher passes `PORT` explicitly.
- The startup log line includes the commit the binary was built from: `freshell-server listening on http://0.0.0.0:<port> (ws://...) [commit <sha>]`. Use it (or `~/.freshell/logs/rust-server-3002.log`) to check what the running server was built from when asking "are we running change X?".
- Health check: `curl http://127.0.0.1:<port>/api/health` (unauthenticated, rate-limit exempt).
- **Detached launch:** `scripts/launch-rust.sh` starts the server in its own
session (`setsid`, stdin from `/dev/null`). Closing the launching shell
does NOT stop the server or its child agent terminals (this fixed the
SIGTERM/SIGHUP cascades visible as `shutdown_forensics` events). Stop it
only via `scripts/launch-rust.sh --stop [--port N]`. CAVEAT: WSL2 shuts
the whole VM down shortly after the LAST console/interop handle closes —
no launcher-side detachment survives that.
- **systemd user unit (recommended for unattended operation):** install
`installers/systemd/freshell-rust.service` (see its header for the
`/etc/wsl.conf` requirement and `loginctl enable-linger`). It cannot keep
the WSL VM alive either, but it restores the server at the next distro
boot and supervises restarts. systemd is NOT required — the setsid
launcher remains the default, dependency-free path.

## Codex Agent in CMD Instructions (Codex agents only; only when running in CMD on windows; all other agents must ignore)
- Prefer bash/WSL over PowerShell; Windows paths map like `D:\\...` -> `/mnt/d/...`.
Expand Down
52 changes: 51 additions & 1 deletion Cargo.lock

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

6 changes: 6 additions & 0 deletions crates/freshell-sessions/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ serde_json = { workspace = true }
serde = { workspace = true }
# Timestamp serialization for amplifier stub metadata (ISO-8601 RFC-3339).
chrono = { workspace = true }
# ITEM-1 bundle stamping (`bundle_config.rs`): read amplifier's settings.yaml
# `bundle.active` when pre-writing session stubs. saphyr is the maintained
# pure-Rust YAML 1.2 successor to the archived serde_yaml (release 2026-07;
# yaml-rust2's successor project). default-features off: we only need
# untyped `Yaml` access to one nested string key, no serde.
saphyr = { version = "0.0.11", default-features = false }
# chokidar -> notify: the session-indexer file watcher (ADR Decision 1.1, deps=notify).
notify = "6"
# node:sqlite -> rusqlite: the opencode.db read-only listing parser. `bundled`
Expand Down
96 changes: 92 additions & 4 deletions crates/freshell-sessions/src/amplifier_stub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,14 @@ pub struct EnsuredSession {
/// `working_dir` (canonical cwd), custom `freshell_terminal_id` (best-effort
/// durable-linkage bonus — validation observed a real turn's save REWRITE
/// metadata.json and add `*.backup` files, so the field may not survive use;
/// Freshell's own registry stays primary and nothing keys off it), NO `bundle`; plus empty `transcript.jsonl` and empty
/// Freshell's own registry stays primary and nothing keys off it), plus a
/// best-effort `bundle` (bare name from the user's merged settings
/// `bundle.active` — see [`crate::bundle_config::resolve_active_bundle`];
/// stamped because the CLI's resume path never consults settings and would
/// otherwise run its hardcoded default bundle (`anchors`) and persist a
/// self-perpetuating `"bundle": "unknown"`; the CLI normalizes a bare stamp
/// to `bundle:<name>` on its first save; omitted entirely when nothing
/// resolves safely); plus empty `transcript.jsonl` and empty
/// `events.jsonl` (the latter so the activity hub's create-time resolver
/// attach finds a file — see the module design note).
pub fn ensure_session(
Expand Down Expand Up @@ -190,12 +197,29 @@ pub fn ensure_session(
.join("sessions")
.join(session_id);
std::fs::create_dir_all(&dir)?;
let metadata = serde_json::json!({
let mut metadata = serde_json::json!({
"session_id": session_id,
"created": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
"working_dir": resolved.to_string_lossy(),
"freshell_terminal_id": terminal_id,
});
// ITEM-1: stamp the user's active bundle (bare name, e.g. "foundation";
// the CLI accepts bare on read and normalizes it to "bundle:<name>" on
// its first save — never assume the literal survives a real turn).
// The CLI's `resume` path never consults settings `bundle.active`; an
// unstamped stub silently runs the CLI's hardcoded default bundle
// (`anchors`) and then persists a self-perpetuating "bundle": "unknown".
// Best-effort by
// design: `resolve_active_bundle` collapses every surprise to `None`
// (HARD SAFETY RULE — a wrong stamp is trusted forever, a missing one
// is healed), so stub creation can never fail or slow down here.
// `amplifier_home` doubles as the global settings root: callers pass
// `resolve_amplifier_home()` = `$HOME/.amplifier` in production (with
// the FRESHELL_AMPLIFIER_HOME test override), which is exactly where
// the CLI reads its global settings.yaml.
if let Some(bundle) = crate::bundle_config::resolve_active_bundle(amplifier_home, &resolved) {
metadata["bundle"] = serde_json::Value::String(bundle);
}
// The three stub-file writes below can fail partway through (ENOSPC,
// permissions, ...) after create_dir_all already succeeded. On that
// path, best-effort roll back the directory rather than leaving a
Expand Down Expand Up @@ -526,6 +550,14 @@ mod tests {
let cwd_dir = home.join("workdir");
std::fs::create_dir_all(&cwd_dir).unwrap();
let canonical = std::fs::canonicalize(&cwd_dir).unwrap();
// ITEM-1: a configured global bundle must be stamped into the stub.
std::fs::write(
home.join("settings.yaml"),
"bundle:
active: foundation
",
)
.unwrap();

let ensured = ensure_session(
&home,
Expand Down Expand Up @@ -554,8 +586,11 @@ mod tests {
assert_eq!(meta["freshell_terminal_id"], "term-1");
// ISO-8601 with tz — must parse through the crate's own parser.
assert!(crate::time::parse_timestamp_ms(&meta["created"]).is_some());
// Omit `bundle` so the user's default bundle resolves.
assert!(meta.get("bundle").is_none());
// ITEM-1: stamp the user's configured bundle (bare name). The CLI's
// resume path never consults settings.yaml — an unstamped stub runs
// the CLI's hardcoded default bundle and then persists a
// self-perpetuating "bundle": "unknown".
assert_eq!(meta["bundle"], "foundation");
// No turn_count on a fresh stub (the GC "unused" signature).
assert!(meta.get("turn_count").is_none());
// Empty transcript + empty events (events.jsonl is load-bearing for
Expand Down Expand Up @@ -902,6 +937,59 @@ mod tests {
}
);
}

#[test]
fn ensure_session_omits_bundle_when_no_settings_resolve() {
// No settings file at any layer -> no stamp. Amplifier's own
// default-bundle resolution heals a missing key; only a WRONG key
// is unrecoverable (HARD SAFETY RULE).
let home = unique_temp_home("no-bundle");
let cwd_dir = home.join("workdir");
std::fs::create_dir_all(&cwd_dir).unwrap();

let ensured = ensure_session(
&home,
"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
cwd_dir.to_str().unwrap(),
"term-nb",
)
.unwrap();
assert!(ensured.created);
let meta: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(ensured.session_dir.join("metadata.json")).unwrap(),
)
.unwrap();
assert!(meta.get("bundle").is_none());
let _ = std::fs::remove_dir_all(&home);
}

#[test]
fn ensure_session_still_creates_stub_when_settings_are_garbage() {
// Surprise settings must degrade to omission — NEVER fail or delay
// stub creation.
let home = unique_temp_home("garbage-bundle");
let cwd_dir = home.join("workdir");
std::fs::create_dir_all(&cwd_dir).unwrap();
std::fs::write(home.join("settings.yaml"), "bundle: [unclosed").unwrap();

let ensured = ensure_session(
&home,
"bbbbbbbb-cccc-dddd-eeee-ffffffffffff",
cwd_dir.to_str().unwrap(),
"term-gb",
)
.unwrap();
assert!(
ensured.created,
"stub creation must never fail because settings were unreadable"
);
let meta: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(ensured.session_dir.join("metadata.json")).unwrap(),
)
.unwrap();
assert!(meta.get("bundle").is_none());
let _ = std::fs::remove_dir_all(&home);
}
}

#[cfg(test)]
Expand Down
Loading
Loading