Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
on the session (previously always accepted and silently dropped its own output) — and its
expanded prompt text is now persisted/replayed as part of the session's conversation history
like a normal turn, rather than never being recorded anywhere.
- `zeph` (binary): fixed local trace files being left as unterminated JSON when the process was
killed via `pkill`'s default `SIGTERM` (issue #6683). Plain CLI/TUI mode had no `SIGTERM`
handling at all — only `Ctrl-C`/`SIGINT` — so the OS default disposition terminated the process
before any `Drop` impl ran, skipping the Chrome trace `FlushGuard`'s closing `]` write.
`init_tracing` now hands the guard to both `TracingGuards` and a new `SIGTERM` listener task
via a shared take-once cell, so whichever path runs first flushes the trace file and the other
safely no-ops. The listener is installed only when nothing else on the invocation's path
already owns `SIGTERM` — `--daemon`, `zeph serve-sessions`, and `zeph scheduler serve` each
install their own graceful-shutdown `SIGTERM` handler, and an earlier version of this fix
gated on `!daemon_mode` alone, which raced and beat those two non-daemon paths' graceful drain
with a hard exit (caught in review). Once installed, the listener flushes and then calls
`signal_hook::low_level::emulate_default_handler` to reset `SIGTERM` to its default disposition
and re-raise it, so the process still dies *by the signal* (`WIFSIGNALED`) as external
supervisors (systemd, launchd, container runtimes, `zeph scheduler stop`'s wait loop) expect,
rather than a plain `exit(143)` that reads identically via `$?` but is distinguishable via
`wait`/`waitpid`. New dependency: `signal-hook` 0.4.4 (Unix-only, mirrors the existing `nix`
scoping in `zeph-tools`).
- `zeph` (binary): switched `build_chrome_layer` to `tracing_chrome::TraceStyle::Async` (issue
#6682), fixing wall-clock-inaccurate local trace spans. The previous `Threaded` default fires
`on_enter`/`on_exit` on every poll of an async span, fragmenting any span with an internal
`.await` into many short on-CPU slices instead of one continuous duration — confirmed on a
127s session where the top-level `core.agent.run` span reconstructed as 209 fragments
totalling 234ms instead of ~127,236ms wall time (originally surfaced, but left unresolved, by
the #6676 jq-recipe fix above). `Async` style fires `on_new_span`/`on_close` once per span
lifetime instead, verified against the `tracing-chrome` 0.7.2 source. The jq recipes in
`.claude/rules/continuous-improvement.md` are reworked to pair the resulting `ph:"b"`/`"e"`
events by `id` (the span tree's root, shared across all spans under it) with a LIFO stack
instead of `B`/`E` by thread id; that file is untracked (global gitignore) so the change does
not appear in this PR's diff.

## [0.22.3] - 2026-07-22
### Fixed
Expand Down
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 Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ serde_norway = "0.9.42"
serial_test = "3.5.0"
sha2 = "0.11"
shell-words = "1.1.1"
signal-hook = "0.4.4"
similar = "3.1.1"
sqlx = { version = "0.9.0", default-features = false }
subtle = "2.6"
Expand Down Expand Up @@ -422,6 +423,12 @@ zeph-tui = { workspace = true, optional = true }
zeph-worktree.workspace = true
zeroize.workspace = true

# signal-hook is only meaningful on Unix targets — the local-trace SIGTERM flush handler
# (`src/tracing_init.rs::spawn_sigterm_flush_task`) is `#[cfg(unix)]`-gated, same scoping as
# zeph-tools' `nix` dependency.
[target.'cfg(unix)'.dependencies]
signal-hook.workspace = true

[dev-dependencies]
agent-client-protocol.workspace = true
bytes.workspace = true
Expand Down
24 changes: 24 additions & 0 deletions src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -841,13 +841,37 @@ pub(crate) async fn run(mut cli: Cli) -> anyhow::Result<()> {
// guaranteeing no human-readable text interleaves with the JSONL stdout stream.
let json_mode_early = cli.json || base_config.cli.json;

// Whether some other code path reached by this invocation will install its own SIGTERM
// handler and own graceful shutdown once dispatch proceeds below — `init_tracing`'s local
// trace flush task must not compete with those for the same signal (critic finding C1,
// #6683: a naive `!daemon_mode` gate let the flush task's hard exit race and beat
// `serve-sessions`'/`scheduler serve`'s graceful drain). `cli.command` is already fully
// parsed at this point, decidable without executing any of the async dispatch logic below.
#[cfg(feature = "profiling")]
let owns_sigterm_elsewhere = {
#[cfg(feature = "session")]
let owns_serve_sessions =
matches!(cli.command.as_ref(), Some(Command::ServeSessions { .. }));
#[cfg(not(feature = "session"))]
let owns_serve_sessions = false;

#[cfg(all(unix, feature = "scheduler"))]
let owns_scheduler_serve = matches!(cli.command.as_ref(), Some(Command::Serve { .. }));
#[cfg(not(all(unix, feature = "scheduler")))]
let owns_scheduler_serve = false;

runtime_ctx.daemon_mode || owns_serve_sessions || owns_scheduler_serve
};

let _tracing_guards = init_tracing(
&logging_config,
runtime_ctx,
telemetry_config,
redact_secrets,
json_mode_early,
#[cfg(feature = "profiling")]
owns_sigterm_elsewhere,
#[cfg(feature = "profiling")]
Some(std::sync::Arc::clone(&metrics_collector_arc)),
);

Expand Down
Loading
Loading