# Feature 17: Error Termination Metadata **Implementation approach:** Enrich ACP notifications and session info ## Summary v1 captured `exit_code`, structured `StderrOutput` (head/tail/truncated) when a session ended due to error. v1 loses this metadata. Need to capture and expose process termination details. ## Current v1 State - Agent process lifecycle is managed in `acp_runtime/mod.rs` - Process exit is detected but error metadata (exit code, stderr) is not captured or forwarded - The `_sandboxagent/agent/unparsed` notification exists for parse errors, but not for process crashes - No structured error termination data is emitted to clients ## v1 Reference (source commit) Port behavior and payload shape from commit `8ecd27bc24e62505d7aa4c50cbdd1c9dbb09f836`. ## v1 Types (exact, from `universal-agent-schema/src/lib.rs`) ```rust #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct SessionEndedData { pub reason: SessionEndReason, pub terminated_by: TerminatedBy, /// Error message when reason is Error #[serde(default, skip_serializing_if = "Option::is_none")] pub message: Option, /// Process exit code when reason is Error #[serde(default, skip_serializing_if = "Option::is_none")] pub exit_code: Option, /// Agent stderr output when reason is Error #[serde(default, skip_serializing_if = "Option::is_none")] pub stderr: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct StderrOutput { /// First N lines of stderr (if truncated) or full stderr (if not truncated) #[serde(default, skip_serializing_if = "Option::is_none")] pub head: Option, /// Last N lines of stderr (only present if truncated) #[serde(default, skip_serializing_if = "Option::is_none")] pub tail: Option, /// Whether the output was truncated pub truncated: bool, /// Total number of lines in stderr #[serde(default, skip_serializing_if = "Option::is_none")] pub total_lines: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, ToSchema)] #[serde(rename_all = "snake_case")] pub enum SessionEndReason { Completed, Error, Terminated, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] #[serde(rename_all = "snake_case")] pub enum TerminatedBy { Agent, Daemon, } ``` ## v1 Implementation (exact) ### `mark_session_ended` (SessionManager) ```rust async fn mark_session_ended( &self, session_id: &str, exit_code: Option, message: &str, reason: SessionEndReason, terminated_by: TerminatedBy, stderr: Option, ) { let mut sessions = self.sessions.lock().await; if let Some(session) = Self::session_mut(&mut sessions, session_id) { if session.ended { return; } session.mark_ended(exit_code, message.to_string(), reason.clone(), terminated_by.clone()); let (error_message, error_exit_code, error_stderr) = if reason == SessionEndReason::Error { (Some(message.to_string()), exit_code, stderr) } else { (None, None, None) }; let ended = EventConversion::new( UniversalEventType::SessionEnded, UniversalEventData::SessionEnded(SessionEndedData { reason, terminated_by, message: error_message, exit_code: error_exit_code, stderr: error_stderr, }), ).synthetic().with_native_session(session.native_session_id.clone()); session.record_conversions(vec![ended]); } } ``` ### Stderr capture on error exit ```rust // Called from consume_spawn when agent process exits with error: Ok(Ok(status)) => { let message = format!("agent exited with status {:?}", status); if !terminate_early { self.record_error(&session_id, message.clone(), Some("process_exit".to_string()), None).await; } let logs = self.read_agent_stderr(agent); self.mark_session_ended( &session_id, status.code(), &message, SessionEndReason::Error, TerminatedBy::Agent, logs, ).await; } ``` ### Stderr reading ```rust fn read_agent_stderr(&self, agent: AgentId) -> Option { let logs = AgentServerLogs::new(self.server_manager.log_base_dir.clone(), agent.as_str()); logs.read_stderr() } ``` ## Implementation Plan ### Stderr Capture in ACP Runtime When an agent process exits (especially abnormally): 1. **Capture stderr**: Buffer the agent process's stderr stream with head/tail logic (~50 lines each) 2. **Capture exit code**: Get the process exit status 3. **Store in session**: Record termination info in the session registry 4. **Emit notification**: Send error notification to all connected clients ### ACP Notification Shape When an agent process terminates with an error: ```json { "jsonrpc": "2.0", "method": "_sandboxagent/session/ended", "params": { "session_id": "session-uuid", "data": { "reason": "error", "terminated_by": "agent", "message": "agent exited with status ExitStatus(unix_wait_status(256))", "exit_code": 1, "stderr": { "head": "Error: module not found\n at ...", "tail": " at process.exit\nnode exited", "truncated": true, "total_lines": 250 } } } } ``` ### Session Info Integration Termination metadata should be accessible via: - `GET /v1/sessions/{id}` (Feature #16) — include `terminationInfo` in response when session has ended - `session/list` ACP response — include termination status in session entries ### Files to Modify | File | Change | |------|--------| | `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Add stderr capture (head/tail buffer) on agent process; capture exit code; emit `_sandboxagent/session/ended`; store v1-shaped termination info in `MetaSession` | | `server/packages/sandbox-agent/src/acp_runtime/mock.rs` | Add mock error termination scenario (e.g., when prompt contains "crash") | | `sdks/typescript/src/client.ts` | Add `TerminationInfo` type; expose on session events and session info | | `server/packages/sandbox-agent/tests/v1_api.rs` | Add error termination metadata test | ### Docs to Update | Doc | Change | |-----|--------| | `docs/sdks/typescript.mdx` | Document `TerminationInfo` type and how to handle error termination | | `research/acp/spec.md` | Document `_sandboxagent/session/ended` extension and payload |