feat(pi): exit 4 (EXIT_CODE_EMPTY_OUTPUT) when --mode json produces no MessageEnd events (terraphim-agents#80, D4) - #4
Conversation
…ces no MessageEnd events (terraphim-agents#80, D4) Cooperative upstream signal for the orchestrator-side fix in terraphim-agents PR Dicklesworthstone#106. When --mode json runs complete without producing any AgentEvent::MessageEnd events, pi-rust now: 1. Emits a final JSON marker event: {"event":"empty_output","reason":"no MessageEnd events observed during run","exit_code":4} 2. Flushes stdout 3. Calls std::process::exit(EXIT_CODE_EMPTY_OUTPUT) (= 4) The orchestrator classifies exit_code == 4 as ExitClass::EmptySuccess (via its stdout-empty detection in ExitClassifier::classify_with_budget), making the silent-run failure mode observable at reconcile time rather than only via downstream drain-file analysis. Detection logic: AtomicBool flipped true on any AgentEvent::MessageEnd event regardless of payload. Per design doc Q1, even MessageEnd events with empty text content count as 'produced something observable' -- tool-only runs are NOT flagged as empty. The orchestrator side decides EmptySuccess vs Success based on stdout emptiness separately. Only --mode json is affected. --mode text and interactive mode are unchanged. Tests (3 new): - empty_output_exit_code_constant_is_4: pins EXIT_CODE_EMPTY_OUTPUT = 4 and verifies it's distinct from EXIT_CODE_FAILURE/USAGE/0 - empty_output_event_json_is_well_formed: pins the marker JSON shape (event, reason, exit_code fields) for downstream consumers - empty_output_message_end_observation_contract: pins the AtomicBool observation semantics so a future refactor doesn't accidentally match MessageStart instead of MessageEnd All 40 pre-existing main.rs unit tests still pass (43 total now). Companion orchestrator PR: terraphim-agents#106 (merged 2026-08-14). Round-1 review posted (3/5 confidence, author-self-review cap).
SummaryImplements the pi-rust side of the end-to-end fix for Gitea terraphim-agents#80. When Key changes (1 file, +103):
What was done well:
What remains problematic:
Design decisions / scope boundaries:
Confidence Score: 3/5
Important Files Changed
Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Caller as orchestrator<br/>(terraphim-agents#106)
participant Pi as pi-rust<br/>(this PR)
participant Provider as LLM Provider
Caller->>Pi: spawn --mode json -p "..."
activate Pi
Pi->>Provider: API request
Provider-->>Pi: response (possibly empty)
Pi->>Pi: track AgentEvent::MessageEnd
alt message_end_observed = false
Pi-->>Caller: {"event":"empty_output",<br/>"reason":"...","exit_code":4}
Pi->>Pi: process::exit(4)
Note over Pi,Caller: orchestrator classifies<br/>as ExitClass::EmptySuccess
else message_end_observed = true
Pi-->>Caller: final assistant message event(s)
Pi->>Pi: process::exit(0)
Note over Pi,Caller: orchestrator classifies<br/>as ExitClass::Success (or EmptySuccess<br/>if stdout is empty downstream)
end
Inline FindingsP2 The flag name is Suggested follow-up (not blocking): rename to No action required — the naming is unambiguous given the comment block above. P2 The producer thread is the event handler (running on a spawned tokio task via Suggested follow-up (not blocking): downgrade to No action required — Comments Outside Diff (1)
|
SummaryIndependent round-2 review of Round-2 probing (independent):
What was done well:
What remains problematic:
Coordination follow-up: Once PR-A is merged (it IS merged as Confidence Score: 5/5
Important Files Changed
DiagramSame as round 1 (architecture unchanged). Round-2 verified the actor ordering matches reality. Inline FindingsP2 (round-1 P2, still valid): Same as round 1. Worth a comment cross-reference to P2 (round-1 P2, still valid): Same as round 1. P2 (round-1 P2, still valid): No e2e test against a real provider Same as round 1. Existing P1 coordination follow-up (out of PR scope): Orchestrator-side This is a PR-A follow-up, not PR-B. The orchestrator's Comments Outside Diff (1)
|
SummaryIndependent round-2 review of Round-2 probing (independent):
Critical round-2 finding (P1 — uncovered by empirical testing on bigbox): The empty-output exit-code path is effectively unreachable in practice with a healthy provider. Empirical smoke-test on bigbox (post-install of this PR) confirmed:
Implications:
What was done well:
What remains problematic:
Coordination follow-up: Once PR-A is merged (it IS merged as Smoke-test evidence (post-install on bigbox): Strings inspection shows Confidence Score: 5/5
Important Files Changed
DiagramSame as round 1 (architecture unchanged). Round-2 verified the actor ordering matches reality. Inline FindingsP2 (round-1 P2, still valid): Same as round 1. Worth a comment cross-reference to P2 (round-1 P2, still valid): Same as round 1. P2 (round-1 P2, still valid): No e2e test against a real provider Same as round 1. Existing P1 coordination follow-up (out of PR scope): Orchestrator-side This is a PR-A follow-up, not PR-B. The orchestrator's Comments Outside Diff (1)
|
Summary
Implements the pi-rust side of the end-to-end fix for Gitea terraphim-agents#80. When
--mode jsonruns complete without producing anyMessageEndevents, pi-rust now exits with code 4 (EXIT_CODE_EMPTY_OUTPUT) and emits a final JSON marker event.Companion PR
The orchestrator-side fix is on
terraphim/terraphim-agents(PR Dicklesworthstone#106, merged 2026-08-14 asf46c9c4). This is the cooperative upstream signal.Changes (1 file, +103)
src/main.rs:80-87— newconst EXIT_CODE_EMPTY_OUTPUT: i32 = 4;with documentationsrc/main.rs:14-17—use std::sync::atomic::{AtomicBool, Ordering};src/main.rs:6559-6593—AtomicBooltrackingMessageEndobservation inrun_print_mode's event handlersrc/main.rs:6693-6712— exit branch: emits JSON marker +exit(4)when zeroMessageEndeventssrc/main.rs:7222-7278— 3 new regression testsDetection logic
AtomicBoolflipped true on anyAgentEvent::MessageEndevent regardless of payload. Per design doc Q1, evenMessageEndevents with empty text content count as "produced something observable" — tool-only runs are NOT flagged as empty. The orchestrator side decidesEmptySuccessvsSuccessbased on stdout emptiness separately.Behaviour changes
--mode jsonruns with zeroMessageEndevents → exit 4 (was: exit 0){"event":"empty_output","reason":"no MessageEnd events observed during run","exit_code":4}--mode textand interactive mode: unchangedVerification
cargo check --bin picargo clippy --bin picargo test --bin pi empty_outputcargo test --bin piCompanion docs
cto-executive-system/.docs/research-issue-80-empty-output-classification-2026-08-14.md(4.83/5 KLS)cto-executive-system/.docs/design-issue-80-empty-output-classification-2026-08-14.md(5.00/5 KLS)