Skip to content

Commit 098dc47

Browse files
authored
Merge pull request #600 from danshapiro/land/amplifier-integration-fixes
Amplifier integration fixes: bundle stamping, server detach, idle-reap cap, session backfill
2 parents cf5f670 + 1dfe4bf commit 098dc47

17 files changed

Lines changed: 3386 additions & 16 deletions

AGENTS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,19 @@ Key facts:
8686
- 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.
8787
- 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?".
8888
- Health check: `curl http://127.0.0.1:<port>/api/health` (unauthenticated, rate-limit exempt).
89+
- **Detached launch:** `scripts/launch-rust.sh` starts the server in its own
90+
session (`setsid`, stdin from `/dev/null`). Closing the launching shell
91+
does NOT stop the server or its child agent terminals (this fixed the
92+
SIGTERM/SIGHUP cascades visible as `shutdown_forensics` events). Stop it
93+
only via `scripts/launch-rust.sh --stop [--port N]`. CAVEAT: WSL2 shuts
94+
the whole VM down shortly after the LAST console/interop handle closes —
95+
no launcher-side detachment survives that.
96+
- **systemd user unit (recommended for unattended operation):** install
97+
`installers/systemd/freshell-rust.service` (see its header for the
98+
`/etc/wsl.conf` requirement and `loginctl enable-linger`). It cannot keep
99+
the WSL VM alive either, but it restores the server at the next distro
100+
boot and supervises restarts. systemd is NOT required — the setsid
101+
launcher remains the default, dependency-free path.
89102

90103
## Codex Agent in CMD Instructions (Codex agents only; only when running in CMD on windows; all other agents must ignore)
91104
- Prefer bash/WSL over PowerShell; Windows paths map like `D:\\...` -> `/mnt/d/...`.

Cargo.lock

Lines changed: 51 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/freshell-sessions/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ serde_json = { workspace = true }
1616
serde = { workspace = true }
1717
# Timestamp serialization for amplifier stub metadata (ISO-8601 RFC-3339).
1818
chrono = { workspace = true }
19+
# ITEM-1 bundle stamping (`bundle_config.rs`): read amplifier's settings.yaml
20+
# `bundle.active` when pre-writing session stubs. saphyr is the maintained
21+
# pure-Rust YAML 1.2 successor to the archived serde_yaml (release 2026-07;
22+
# yaml-rust2's successor project). default-features off: we only need
23+
# untyped `Yaml` access to one nested string key, no serde.
24+
saphyr = { version = "0.0.11", default-features = false }
1925
# chokidar -> notify: the session-indexer file watcher (ADR Decision 1.1, deps=notify).
2026
notify = "6"
2127
# node:sqlite -> rusqlite: the opencode.db read-only listing parser. `bundled`

crates/freshell-sessions/src/amplifier_stub.rs

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,14 @@ pub struct EnsuredSession {
105105
/// `working_dir` (canonical cwd), custom `freshell_terminal_id` (best-effort
106106
/// durable-linkage bonus — validation observed a real turn's save REWRITE
107107
/// metadata.json and add `*.backup` files, so the field may not survive use;
108-
/// Freshell's own registry stays primary and nothing keys off it), NO `bundle`; plus empty `transcript.jsonl` and empty
108+
/// Freshell's own registry stays primary and nothing keys off it), plus a
109+
/// best-effort `bundle` (bare name from the user's merged settings
110+
/// `bundle.active` — see [`crate::bundle_config::resolve_active_bundle`];
111+
/// stamped because the CLI's resume path never consults settings and would
112+
/// otherwise run its hardcoded default bundle (`anchors`) and persist a
113+
/// self-perpetuating `"bundle": "unknown"`; the CLI normalizes a bare stamp
114+
/// to `bundle:<name>` on its first save; omitted entirely when nothing
115+
/// resolves safely); plus empty `transcript.jsonl` and empty
109116
/// `events.jsonl` (the latter so the activity hub's create-time resolver
110117
/// attach finds a file — see the module design note).
111118
pub fn ensure_session(
@@ -190,12 +197,29 @@ pub fn ensure_session(
190197
.join("sessions")
191198
.join(session_id);
192199
std::fs::create_dir_all(&dir)?;
193-
let metadata = serde_json::json!({
200+
let mut metadata = serde_json::json!({
194201
"session_id": session_id,
195202
"created": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
196203
"working_dir": resolved.to_string_lossy(),
197204
"freshell_terminal_id": terminal_id,
198205
});
206+
// ITEM-1: stamp the user's active bundle (bare name, e.g. "foundation";
207+
// the CLI accepts bare on read and normalizes it to "bundle:<name>" on
208+
// its first save — never assume the literal survives a real turn).
209+
// The CLI's `resume` path never consults settings `bundle.active`; an
210+
// unstamped stub silently runs the CLI's hardcoded default bundle
211+
// (`anchors`) and then persists a self-perpetuating "bundle": "unknown".
212+
// Best-effort by
213+
// design: `resolve_active_bundle` collapses every surprise to `None`
214+
// (HARD SAFETY RULE — a wrong stamp is trusted forever, a missing one
215+
// is healed), so stub creation can never fail or slow down here.
216+
// `amplifier_home` doubles as the global settings root: callers pass
217+
// `resolve_amplifier_home()` = `$HOME/.amplifier` in production (with
218+
// the FRESHELL_AMPLIFIER_HOME test override), which is exactly where
219+
// the CLI reads its global settings.yaml.
220+
if let Some(bundle) = crate::bundle_config::resolve_active_bundle(amplifier_home, &resolved) {
221+
metadata["bundle"] = serde_json::Value::String(bundle);
222+
}
199223
// The three stub-file writes below can fail partway through (ENOSPC,
200224
// permissions, ...) after create_dir_all already succeeded. On that
201225
// path, best-effort roll back the directory rather than leaving a
@@ -526,6 +550,14 @@ mod tests {
526550
let cwd_dir = home.join("workdir");
527551
std::fs::create_dir_all(&cwd_dir).unwrap();
528552
let canonical = std::fs::canonicalize(&cwd_dir).unwrap();
553+
// ITEM-1: a configured global bundle must be stamped into the stub.
554+
std::fs::write(
555+
home.join("settings.yaml"),
556+
"bundle:
557+
active: foundation
558+
",
559+
)
560+
.unwrap();
529561

530562
let ensured = ensure_session(
531563
&home,
@@ -554,8 +586,11 @@ mod tests {
554586
assert_eq!(meta["freshell_terminal_id"], "term-1");
555587
// ISO-8601 with tz — must parse through the crate's own parser.
556588
assert!(crate::time::parse_timestamp_ms(&meta["created"]).is_some());
557-
// Omit `bundle` so the user's default bundle resolves.
558-
assert!(meta.get("bundle").is_none());
589+
// ITEM-1: stamp the user's configured bundle (bare name). The CLI's
590+
// resume path never consults settings.yaml — an unstamped stub runs
591+
// the CLI's hardcoded default bundle and then persists a
592+
// self-perpetuating "bundle": "unknown".
593+
assert_eq!(meta["bundle"], "foundation");
559594
// No turn_count on a fresh stub (the GC "unused" signature).
560595
assert!(meta.get("turn_count").is_none());
561596
// Empty transcript + empty events (events.jsonl is load-bearing for
@@ -902,6 +937,59 @@ mod tests {
902937
}
903938
);
904939
}
940+
941+
#[test]
942+
fn ensure_session_omits_bundle_when_no_settings_resolve() {
943+
// No settings file at any layer -> no stamp. Amplifier's own
944+
// default-bundle resolution heals a missing key; only a WRONG key
945+
// is unrecoverable (HARD SAFETY RULE).
946+
let home = unique_temp_home("no-bundle");
947+
let cwd_dir = home.join("workdir");
948+
std::fs::create_dir_all(&cwd_dir).unwrap();
949+
950+
let ensured = ensure_session(
951+
&home,
952+
"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
953+
cwd_dir.to_str().unwrap(),
954+
"term-nb",
955+
)
956+
.unwrap();
957+
assert!(ensured.created);
958+
let meta: serde_json::Value = serde_json::from_str(
959+
&std::fs::read_to_string(ensured.session_dir.join("metadata.json")).unwrap(),
960+
)
961+
.unwrap();
962+
assert!(meta.get("bundle").is_none());
963+
let _ = std::fs::remove_dir_all(&home);
964+
}
965+
966+
#[test]
967+
fn ensure_session_still_creates_stub_when_settings_are_garbage() {
968+
// Surprise settings must degrade to omission — NEVER fail or delay
969+
// stub creation.
970+
let home = unique_temp_home("garbage-bundle");
971+
let cwd_dir = home.join("workdir");
972+
std::fs::create_dir_all(&cwd_dir).unwrap();
973+
std::fs::write(home.join("settings.yaml"), "bundle: [unclosed").unwrap();
974+
975+
let ensured = ensure_session(
976+
&home,
977+
"bbbbbbbb-cccc-dddd-eeee-ffffffffffff",
978+
cwd_dir.to_str().unwrap(),
979+
"term-gb",
980+
)
981+
.unwrap();
982+
assert!(
983+
ensured.created,
984+
"stub creation must never fail because settings were unreadable"
985+
);
986+
let meta: serde_json::Value = serde_json::from_str(
987+
&std::fs::read_to_string(ensured.session_dir.join("metadata.json")).unwrap(),
988+
)
989+
.unwrap();
990+
assert!(meta.get("bundle").is_none());
991+
let _ = std::fs::remove_dir_all(&home);
992+
}
905993
}
906994

907995
#[cfg(test)]

0 commit comments

Comments
 (0)