From 1aee59585bf8d752f5d29100a2b08df53f51c4f9 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sat, 12 Sep 2026 12:29:36 -0600 Subject: [PATCH 1/6] Pin pebble to mcp-embedder-events and accept startup_ms on MCP outcomes Pebble's McpServerReady and McpServerFailed events now carry startup_ms. The workflow event sink destructured both variants by name, so it stops listing every field. The pin is temporary: it moves to pebble main once lithoscomputer/pebble merges the branch. Co-Authored-By: Claude Fable 5.1 --- Cargo.lock | 28 +++++++++---------- Cargo.toml | 6 ++-- .../fabro-workflow/src/handler/llm/pebble.rs | 4 +-- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef6490fd1..afc83282a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2063,7 +2063,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2190,7 +2190,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4364,7 +4364,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.61.2", ] [[package]] @@ -5310,7 +5310,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5875,7 +5875,7 @@ dependencies = [ [[package]] name = "pebble-agent" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/pebble?rev=222d17f17d7384545d3782f3b315210ad2cb0cfe#222d17f17d7384545d3782f3b315210ad2cb0cfe" +source = "git+https://github.com/lithoscomputer/pebble?rev=330495748fa8c76a98ca7af31c70d0850523759d#330495748fa8c76a98ca7af31c70d0850523759d" dependencies = [ "async-trait", "futures-util", @@ -5892,7 +5892,7 @@ dependencies = [ [[package]] name = "pebble-cli-core" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/pebble?rev=222d17f17d7384545d3782f3b315210ad2cb0cfe#222d17f17d7384545d3782f3b315210ad2cb0cfe" +source = "git+https://github.com/lithoscomputer/pebble?rev=330495748fa8c76a98ca7af31c70d0850523759d#330495748fa8c76a98ca7af31c70d0850523759d" dependencies = [ "anyhow", "async-trait", @@ -5921,7 +5921,7 @@ dependencies = [ [[package]] name = "pebble-coding-agent" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/pebble?rev=222d17f17d7384545d3782f3b315210ad2cb0cfe#222d17f17d7384545d3782f3b315210ad2cb0cfe" +source = "git+https://github.com/lithoscomputer/pebble?rev=330495748fa8c76a98ca7af31c70d0850523759d#330495748fa8c76a98ca7af31c70d0850523759d" dependencies = [ "async-trait", "futures-util", @@ -6291,7 +6291,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -6768,7 +6768,7 @@ dependencies = [ "errno 0.3.14", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6827,7 +6827,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7474,7 +7474,7 @@ version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ - "errno 0.3.14", + "errno 0.2.8", "libc", ] @@ -8039,7 +8039,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8074,7 +8074,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9141,7 +9141,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 50dc9d593..7a5d09cc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,9 +123,9 @@ sandbox-driver-testing = { git = "https://github.com/lithoscomputer/sandbox-driv # sandbox-driver revision, so move the two pins together. Pebble pins the # same lithos-llm rev as fabro, and its lockfile policy is that every shared # crate resolves to the version lithos-llm locks. -pebble-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "222d17f17d7384545d3782f3b315210ad2cb0cfe" } -pebble-coding-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "222d17f17d7384545d3782f3b315210ad2cb0cfe", features = ["mcp", "search-providers"] } -pebble-cli-core = { git = "https://github.com/lithoscomputer/pebble", rev = "222d17f17d7384545d3782f3b315210ad2cb0cfe" } +pebble-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "330495748fa8c76a98ca7af31c70d0850523759d" } +pebble-coding-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "330495748fa8c76a98ca7af31c70d0850523759d", features = ["mcp", "search-providers"] } +pebble-cli-core = { git = "https://github.com/lithoscomputer/pebble", rev = "330495748fa8c76a98ca7af31c70d0850523759d" } sentry = { version = "0.35", default-features = false, features = ["backtrace", "contexts", "ureq", "rustls"] } fork = "0.2" exec = "0.3" diff --git a/lib/components/fabro-workflow/src/handler/llm/pebble.rs b/lib/components/fabro-workflow/src/handler/llm/pebble.rs index 15a6d7007..a0dc178f1 100644 --- a/lib/components/fabro-workflow/src/handler/llm/pebble.rs +++ b/lib/components/fabro-workflow/src/handler/llm/pebble.rs @@ -198,7 +198,7 @@ impl EventSink for WorkflowEventSink { ); return Ok(()); } - CodingEvent::McpServerReady { server, tools } => { + CodingEvent::McpServerReady { server, tools, .. } => { self.emitter.emit_scoped( &Event::AgentMcpReady { node_id: self.node_id.clone(), @@ -217,7 +217,7 @@ impl EventSink for WorkflowEventSink { ); return Ok(()); } - CodingEvent::McpServerFailed { server, error } => { + CodingEvent::McpServerFailed { server, error, .. } => { self.emitter.emit_scoped( &Event::AgentMcpFailed { node_id: self.node_id.clone(), From ff7821dcd27ace3f90742e893b27c81d92910605 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sat, 12 Sep 2026 12:39:31 -0600 Subject: [PATCH 2/6] Adopt pebble's MCP disconnect event and startup timing Pebble reports an MCP server whose connection closed mid-session once, as McpServerDisconnected, and carries startup_ms on McpServerReady and McpServerFailed. The workflow event sink mirrors the disconnect onto a new agent.mcp.disconnected run event shaped like agent.mcp.failed, and passes startup_ms through on agent.mcp.ready and agent.mcp.failed. The raw pebble event is not stored for these, so the timing would otherwise be dropped at the boundary. The stage projection's McpServerStatus gains a `disconnected` kind next to `ready` and `failed`. The fold keeps the server's tool count and sticky invoked flag and only moves the status. The OpenAPI McpServerStatus oneOf gains McpServerStatusDisconnected, and the fabro-api round-trip test covers its JSON shape. A new session_projection_parity test folds the same MCP events through pebble's SessionProjection and fabro's stage projection and compares them, including pebble's `disconnected`. ToolErrorKind::Timeout needs no fabro change: the kind is stored as pebble serializes it and never matched. Co-Authored-By: Claude Fable 5.1 --- docs/internal/events.md | 37 ++- .../fabro-event-schema-v2-concrete-shape.md | 1 + docs/public/api-reference/fabro-api.yaml | 16 ++ lib/components/fabro-store/src/run_state.rs | 220 +++++++++++++++++- .../fabro-workflow/src/event/convert.rs | 14 ++ .../fabro-workflow/src/event/events.rs | 37 ++- .../fabro-workflow/src/event/names.rs | 10 + .../fabro-workflow/src/event/stored_fields.rs | 3 +- .../fabro-workflow/src/handler/llm/pebble.rs | 33 ++- .../tests/stage_projection_round_trip.rs | 25 ++ .../fabro-types/src/run_event/agent.rs | 26 +++ .../fabro-types/src/run_event/mod.rs | 64 +++++ .../fabro-types/src/run_projection.rs | 13 +- 13 files changed, 481 insertions(+), 18 deletions(-) diff --git a/docs/internal/events.md b/docs/internal/events.md index c58f041d2..abc4134ab 100644 --- a/docs/internal/events.md +++ b/docs/internal/events.md @@ -1421,6 +1421,7 @@ Emitted when a sub-agent is spawned. "original_name": "list_issues" } ], + "startup_ms": 842, "visit": 1 } } @@ -1431,6 +1432,7 @@ Emitted when a sub-agent is spawned. | `server_name` | string | MCP server name | | `tool_count` | number | Number of tools available | | `tools` | array | Names-only tool summaries for the ready server, sorted by qualified `name`. Each entry has `name` (Fabro-qualified `mcp__{server}__{tool}` identifier) and `original_name` (server-provided tool name). Descriptions and input schemas are intentionally omitted. The field is omitted from serialized JSON for legacy parity when empty. | +| `startup_ms` | number | Whole milliseconds from the server's launch to its tools being listed. Events written before the field existed read as `0`. | | `visit` | number | Stage visit count when the server became ready | ### `agent.mcp.failed` @@ -1443,7 +1445,9 @@ Emitted when a sub-agent is spawned. "session_id": "ses_abc", "properties": { "server_name": "filesystem", - "error": "Connection refused" + "error": "Connection refused", + "startup_ms": 4, + "visit": 1 } } ``` @@ -1452,6 +1456,37 @@ Emitted when a sub-agent is spawned. |----------|------|-------------| | `server_name` | string | MCP server name | | `error` | string | Error message | +| `startup_ms` | number | Whole milliseconds from the server's launch to the failure. Events written before the field existed read as `0`. | +| `visit` | number | Stage visit count when the server failed | + +### `agent.mcp.disconnected` + +An MCP server that was ready lost its connection during the stage. Pebble +publishes the disconnect once per server, from whichever session's tool call +first observed the closed connection, so the event can originate in a +sub-agent. Every later call to that server's tools fails until the session +ends. The stage projection moves the server's status from `ready` to +`disconnected`; its `tool_count` and `invoked` flag are kept. + +```json +{ + "id": "...", "ts": "...", "run_id": "...", + "event": "agent.mcp.disconnected", + "node_id": "code", "node_label": "code", + "session_id": "ses_abc", + "properties": { + "server_name": "github", + "error": "transport closed", + "visit": 1 + } +} +``` + +| Property | Type | Description | +|----------|------|-------------| +| `server_name` | string | MCP server name | +| `error` | string | What closed the connection, as the client observed it | +| `visit` | number | Stage visit count when the disconnect was observed | ### `agent.memory.loaded` diff --git a/docs/internal/fabro-event-schema-v2-concrete-shape.md b/docs/internal/fabro-event-schema-v2-concrete-shape.md index 6d9a3c28d..81d3eab15 100644 --- a/docs/internal/fabro-event-schema-v2-concrete-shape.md +++ b/docs/internal/fabro-event-schema-v2-concrete-shape.md @@ -386,6 +386,7 @@ V2 keeps the current durable family surface broadly intact. - `agent.sub.closed` - `agent.mcp.ready` - `agent.mcp.failed` +- `agent.mcp.disconnected` - `agent.failover` ### Git diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 96ca10147..fc3e0555c 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -11528,11 +11528,13 @@ components: oneOf: - $ref: "#/components/schemas/McpServerStatusReady" - $ref: "#/components/schemas/McpServerStatusFailed" + - $ref: "#/components/schemas/McpServerStatusDisconnected" discriminator: propertyName: kind mapping: ready: "#/components/schemas/McpServerStatusReady" failed: "#/components/schemas/McpServerStatusFailed" + disconnected: "#/components/schemas/McpServerStatusDisconnected" McpServerStatusReady: type: object @@ -11560,6 +11562,20 @@ components: error: type: string + McpServerStatusDisconnected: + description: The server was ready and then its connection closed during the stage; its tools fail until the session ends. + type: object + required: + - kind + - error + properties: + kind: + type: string + enum: [disconnected] + error: + type: string + description: What closed the connection, as the client observed it. + AgentMcpToolSummary: description: Summary of one tool exposed by an MCP server. type: object diff --git a/lib/components/fabro-store/src/run_state.rs b/lib/components/fabro-store/src/run_state.rs index 8f0f641a0..e5ec36bb5 100644 --- a/lib/components/fabro-store/src/run_state.rs +++ b/lib/components/fabro-store/src/run_state.rs @@ -729,6 +729,13 @@ impl RunProjectionReducer for RunProjection { invoked: false, }); } + EventBody::AgentMcpDisconnected(props) => { + let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq) + else { + return Ok(()); + }; + mark_mcp_server_disconnected(stage, &props.server_name, &props.error); + } _ => {} } @@ -1110,6 +1117,30 @@ fn upsert_mcp_server(stage: &mut StageProjection, mut server: McpServerProjectio } } +/// Move a server the stage saw come up to `Disconnected`. Its tool count and +/// sticky `invoked` flag stay: the tools existed and may have been used, they +/// only fail from here on. A disconnect for a server the stage never saw come +/// up is still recorded, without tools. +fn mark_mcp_server_disconnected(stage: &mut StageProjection, server_name: &str, error: &str) { + let status = McpServerStatus::Disconnected { + error: error.to_string(), + }; + if let Some(existing) = stage + .mcp_servers + .iter_mut() + .find(|existing| existing.server_name == server_name) + { + existing.status = status; + } else { + stage.mcp_servers.push(McpServerProjection { + server_name: server_name.to_string(), + tool_count: 0, + status, + invoked: false, + }); + } +} + /// Extract the `` segment from an `mcp____` qualified /// tool name. Returns `None` for non-MCP tools or malformed names. fn mcp_server_from_tool_name(tool_name: &str) -> Option<&str> { @@ -1796,12 +1827,13 @@ mod tests { use fabro_types::run_event::run::RunFailedProps; use fabro_types::run_event::{ AgentAcpCancelledProps, AgentAcpCompletedProps, AgentAcpStartedProps, - AgentAcpTimedOutProps, AgentEventProps, AgentMcpFailedProps, AgentMcpReadyProps, - AgentMcpToolSummary, AgentSessionActivatedProps, AgentSessionDeactivatedProps, - AgentToolsAvailableProps, CheckpointCompletedProps, InterviewCompletedProps, - InterviewOption, InterviewStartedProps, ParallelBranchCompletedProps, - ParallelBranchStartedProps, RunCompletedProps, RunControlEffectProps, StageCompletedProps, - StageFailedProps, StagePromptProps, StageRetryingProps, StageStartedProps, + AgentAcpTimedOutProps, AgentEventProps, AgentMcpDisconnectedProps, AgentMcpFailedProps, + AgentMcpReadyProps, AgentMcpToolSummary, AgentSessionActivatedProps, + AgentSessionDeactivatedProps, AgentToolsAvailableProps, CheckpointCompletedProps, + InterviewCompletedProps, InterviewOption, InterviewStartedProps, + ParallelBranchCompletedProps, ParallelBranchStartedProps, RunCompletedProps, + RunControlEffectProps, StageCompletedProps, StageFailedProps, StagePromptProps, + StageRetryingProps, StageStartedProps, }; use fabro_types::settings::run::DockerfileSource; use fabro_types::{ @@ -7324,6 +7356,7 @@ mod tests { original_name: "write_file".to_string(), }, ], + startup_ms: 0, visit: 1, }), stage_id.clone(), @@ -7335,6 +7368,7 @@ mod tests { EventBody::AgentMcpFailed(AgentMcpFailedProps { server_name: "github".to_string(), error: "missing token".to_string(), + startup_ms: 0, visit: 1, }), stage_id.clone(), @@ -7350,6 +7384,7 @@ mod tests { name: "read_file".to_string(), original_name: "read_file".to_string(), }], + startup_ms: 0, visit: 1, }), stage_id.clone(), @@ -7375,6 +7410,87 @@ mod tests { assert!(!stage.mcp_servers[1].invoked); } + #[test] + fn mcp_server_disconnect_keeps_tool_count_and_invoked() { + let mut state = initialized_projection(); + let stage_id = stage_id(); + + state + .apply_event(&test_stage_event( + 1, + EventBody::AgentMcpReady(AgentMcpReadyProps { + server_name: "github".to_string(), + tool_count: 1, + tools: vec![AgentMcpToolSummary { + name: "mcp__github__list_issues".to_string(), + original_name: "list_issues".to_string(), + }], + startup_ms: 842, + visit: 1, + }), + stage_id.clone(), + )) + .unwrap(); + state + .apply_event(&test_stage_event( + 2, + agent_body(CodingEvent::ToolCallStarted { + tool_name: "mcp__github__list_issues".to_string(), + tool_call_id: "call_gh".to_string(), + arguments: serde_json::json!({}), + }), + stage_id.clone(), + )) + .unwrap(); + state + .apply_event(&test_stage_event( + 3, + EventBody::AgentMcpDisconnected(AgentMcpDisconnectedProps { + server_name: "github".to_string(), + error: "transport closed".to_string(), + visit: 1, + }), + stage_id.clone(), + )) + .unwrap(); + + let stage = state.stage(&stage_id).unwrap(); + assert_eq!(stage.mcp_servers.len(), 1); + let github = &stage.mcp_servers[0]; + assert_eq!(github.server_name, "github"); + assert_eq!(github.status, McpServerStatus::Disconnected { + error: "transport closed".to_string(), + }); + assert_eq!(github.tool_count, 1, "the tools existed; they now fail"); + assert!(github.invoked, "the server was used before it dropped"); + } + + #[test] + fn mcp_server_disconnect_without_a_ready_is_recorded_without_tools() { + let mut state = initialized_projection(); + let stage_id = stage_id(); + + state + .apply_event(&test_stage_event( + 1, + EventBody::AgentMcpDisconnected(AgentMcpDisconnectedProps { + server_name: "github".to_string(), + error: "transport closed".to_string(), + visit: 1, + }), + stage_id.clone(), + )) + .unwrap(); + + let stage = state.stage(&stage_id).unwrap(); + assert_eq!(stage.mcp_servers.len(), 1); + assert_eq!(stage.mcp_servers[0].tool_count, 0); + assert!(!stage.mcp_servers[0].invoked); + assert_eq!(stage.mcp_servers[0].status, McpServerStatus::Disconnected { + error: "transport closed".to_string(), + }); + } + #[test] fn agent_tool_started_marks_matching_mcp_server_as_invoked() { let mut state = initialized_projection(); @@ -7390,6 +7506,7 @@ mod tests { name: "read_file".to_string(), original_name: "read_file".to_string(), }], + startup_ms: 0, visit: 1, }), stage_id.clone(), @@ -7402,6 +7519,7 @@ mod tests { server_name: "other".to_string(), tool_count: 0, tools: vec![], + startup_ms: 0, visit: 1, }), stage_id.clone(), @@ -7462,6 +7580,7 @@ mod tests { name: "read_file".to_string(), original_name: "read_file".to_string(), }], + startup_ms: 0, visit: 1, }), stage_id.clone(), @@ -7496,6 +7615,7 @@ mod tests { original_name: "stat".to_string(), }, ], + startup_ms: 0, visit: 1, }), stage_id.clone(), @@ -7851,7 +7971,7 @@ mod tests { /// other for a retained session that spans two stages, so a stage's live /// account is the prompt delta pebble reports and the two never drift. mod session_projection_parity { - use pebble_coding_agent::events::InputSource; + use pebble_coding_agent::events::{InputSource, McpToolSummary}; use pebble_coding_agent::projection::{ SessionActivity, SessionProjection, SubagentStatus as PebbleSubagentStatus, }; @@ -8066,5 +8186,91 @@ mod tests { assert_eq!(resumed, replayed); } + + /// Pebble folds its own `McpServer*` events; fabro folds the + /// `agent.mcp.*` events the workflow sink mirrors them onto, since + /// the raw pebble event is not stored. The mirrored events are built + /// here the way the sink builds them. + #[test] + fn mcp_servers_agree_across_the_two_folds() { + let code = StageId::new("code", 1); + let tools = vec![McpToolSummary { + name: "mcp__github__list_issues".to_string(), + original_name: "list_issues".to_string(), + }]; + let ready = root(CodingEvent::McpServerReady { + server: "github".to_string(), + tools: tools.clone(), + startup_ms: 842, + }); + let call = root(CodingEvent::ToolCallStarted { + tool_name: "mcp__github__list_issues".to_string(), + tool_call_id: "call_1".to_string(), + arguments: json!({}), + }); + // The child's call is the one that sees the connection close. + let disconnected = child(CodingEvent::McpServerDisconnected { + server: "github".to_string(), + error: "transport closed".to_string(), + }); + + let mut projection = SessionProjection::new(); + projection.apply(&ready); + projection.apply(&call); + // A projection stored between the two carries the disconnect on + // resume like the replayed one. + let stored_bytes = serde_json::to_vec(&projection).unwrap(); + projection.apply(&disconnected); + let mut resumed: SessionProjection = serde_json::from_slice(&stored_bytes).unwrap(); + resumed.apply(&disconnected); + assert_eq!(resumed, projection); + + let mut run = initialized_projection(); + run.apply_event(&test_stage_event( + 1, + EventBody::AgentMcpReady(AgentMcpReadyProps { + server_name: "github".to_string(), + tool_count: tools.len(), + tools: tools + .iter() + .map(|tool| AgentMcpToolSummary { + name: tool.name.clone(), + original_name: tool.original_name.clone(), + }) + .collect(), + startup_ms: 842, + visit: 1, + }), + code.clone(), + )) + .unwrap(); + run.apply_event(&stored(2, &code, call)).unwrap(); + run.apply_event(&test_stage_event( + 3, + EventBody::AgentMcpDisconnected(AgentMcpDisconnectedProps { + server_name: "github".to_string(), + error: "transport closed".to_string(), + visit: 1, + }), + code.clone(), + )) + .unwrap(); + + let stage = run.stage(&code).unwrap(); + assert_eq!(stage.mcp_servers.len(), projection.mcp_servers.len()); + let server = &stage.mcp_servers[0]; + let pebble = &projection.mcp_servers["github"]; + assert_eq!(server.server_name, "github"); + assert_eq!(server.tool_count, pebble.tools.len()); + assert_eq!(server.invoked, pebble.invoked); + assert!(server.invoked); + assert_eq!(pebble.error, None, "a disconnect is not a failed start"); + assert_eq!(server.status, McpServerStatus::Disconnected { + error: pebble + .disconnected + .clone() + .expect("pebble recorded the disconnect"), + }); + } } } diff --git a/lib/components/fabro-workflow/src/event/convert.rs b/lib/components/fabro-workflow/src/event/convert.rs index 012f2485e..3af99f621 100644 --- a/lib/components/fabro-workflow/src/event/convert.rs +++ b/lib/components/fabro-workflow/src/event/convert.rs @@ -844,19 +844,33 @@ fn event_body_from_event(event: &Event) -> EventBody { server_name, tool_count, tools, + startup_ms, .. } => EventBody::AgentMcpReady(fabro_types::AgentMcpReadyProps { server_name: server_name.clone(), tool_count: *tool_count, tools: tools.clone(), + startup_ms: *startup_ms, visit: *visit, }), Event::AgentMcpFailed { visit, server_name, error, + startup_ms, .. } => EventBody::AgentMcpFailed(fabro_types::AgentMcpFailedProps { + server_name: server_name.clone(), + error: error.clone(), + startup_ms: *startup_ms, + visit: *visit, + }), + Event::AgentMcpDisconnected { + visit, + server_name, + error, + .. + } => EventBody::AgentMcpDisconnected(fabro_types::AgentMcpDisconnectedProps { server_name: server_name.clone(), error: error.clone(), visit: *visit, diff --git a/lib/components/fabro-workflow/src/event/events.rs b/lib/components/fabro-workflow/src/event/events.rs index c486953ee..fe28742b7 100644 --- a/lib/components/fabro-workflow/src/event/events.rs +++ b/lib/components/fabro-workflow/src/event/events.rs @@ -651,6 +651,9 @@ pub enum Event { server_name: String, tool_count: usize, tools: Vec, + /// Whole milliseconds from launch to the tools being listed. + #[serde(default)] + startup_ms: u64, }, /// An MCP server configured for a stage failed to start or connect. AgentMcpFailed { @@ -658,6 +661,17 @@ pub enum Event { visit: u32, server_name: String, error: String, + /// Whole milliseconds from launch to the failure. + #[serde(default)] + startup_ms: u64, + }, + /// An MCP server that was ready lost its connection during the stage; + /// its tools fail until the session ends. + AgentMcpDisconnected { + node_id: String, + visit: u32, + server_name: String, + error: String, }, /// A run-level interrupt was delivered to a concrete steerable agent /// session/stage. @@ -1534,17 +1548,36 @@ impl Event { visit, server_name, tool_count, + startup_ms, .. } => { - debug!(node_id, visit, server_name, tool_count, "MCP server ready"); + debug!( + node_id, + visit, server_name, tool_count, startup_ms, "MCP server ready" + ); } Self::AgentMcpFailed { node_id, visit, server_name, error, + startup_ms, } => { - warn!(node_id, visit, server_name, error, "MCP server failed"); + warn!( + node_id, + visit, server_name, error, startup_ms, "MCP server failed" + ); + } + Self::AgentMcpDisconnected { + node_id, + visit, + server_name, + error, + } => { + warn!( + node_id, + visit, server_name, error, "MCP server disconnected" + ); } Self::AgentInterruptInjected { node_id, diff --git a/lib/components/fabro-workflow/src/event/names.rs b/lib/components/fabro-workflow/src/event/names.rs index a6ba1d4eb..3bdb95cea 100644 --- a/lib/components/fabro-workflow/src/event/names.rs +++ b/lib/components/fabro-workflow/src/event/names.rs @@ -90,6 +90,7 @@ pub fn event_name(event: &Event) -> Cow<'static, str> { Event::AgentSessionDeactivated { .. } => "agent.session.deactivated", Event::AgentMcpReady { .. } => "agent.mcp.ready", Event::AgentMcpFailed { .. } => "agent.mcp.failed", + Event::AgentMcpDisconnected { .. } => "agent.mcp.disconnected", Event::AgentInterruptInjected { .. } => "agent.interrupt.injected", Event::AgentPairUserMessage { .. } => "agent.pair.user_message", Event::AgentPairSystemMessage { .. } => "agent.pair.system_message", @@ -130,6 +131,15 @@ mod tests { }), "parallel.branch.started" ); + assert_eq!( + event_name(&Event::AgentMcpDisconnected { + node_id: "code".to_string(), + visit: 1, + server_name: "github".to_string(), + error: "transport closed".to_string(), + }), + "agent.mcp.disconnected" + ); assert_eq!( event_name(&Event::Agent { stage: "code".to_string(), diff --git a/lib/components/fabro-workflow/src/event/stored_fields.rs b/lib/components/fabro-workflow/src/event/stored_fields.rs index 05c6e83fd..2bb17f6b0 100644 --- a/lib/components/fabro-workflow/src/event/stored_fields.rs +++ b/lib/components/fabro-workflow/src/event/stored_fields.rs @@ -134,7 +134,8 @@ fn stored_event_fields_for_variant(event: &Event) -> StoredEventFields { | Event::AgentAcpTimedOut { node_id, .. } => node_stored_fields(Some(node_id.clone())), Event::AgentAcpStarted { node_id, visit, .. } | Event::AgentMcpReady { node_id, visit, .. } - | Event::AgentMcpFailed { node_id, visit, .. } => { + | Event::AgentMcpFailed { node_id, visit, .. } + | Event::AgentMcpDisconnected { node_id, visit, .. } => { let node_id_str = node_id.clone(); let node_label = default_node_label(Some(&node_id_str), None); StoredEventFields { diff --git a/lib/components/fabro-workflow/src/handler/llm/pebble.rs b/lib/components/fabro-workflow/src/handler/llm/pebble.rs index a0dc178f1..52ba54737 100644 --- a/lib/components/fabro-workflow/src/handler/llm/pebble.rs +++ b/lib/components/fabro-workflow/src/handler/llm/pebble.rs @@ -164,9 +164,10 @@ fn classify_agent_error(error: pebble_coding_agent::Error) -> AgentErrorDisposit /// Pebble's durable event sink for one stage: every agent event becomes a /// run event in the run's log before the agent goes on. A route failover and -/// an MCP server's outcome are facts the run already has events for, so -/// those are mirrored onto the run's own `agent.failover`, `agent.mcp.ready`, -/// and `agent.mcp.failed` events instead of being stored twice. +/// an MCP server's outcome or disconnect are facts the run already has +/// events for, so those are mirrored onto the run's own `agent.failover`, +/// `agent.mcp.ready`, `agent.mcp.failed`, and `agent.mcp.disconnected` +/// events instead of being stored twice. struct WorkflowEventSink { emitter: Arc, node_id: String, @@ -198,7 +199,11 @@ impl EventSink for WorkflowEventSink { ); return Ok(()); } - CodingEvent::McpServerReady { server, tools, .. } => { + CodingEvent::McpServerReady { + server, + tools, + startup_ms, + } => { self.emitter.emit_scoped( &Event::AgentMcpReady { node_id: self.node_id.clone(), @@ -212,18 +217,36 @@ impl EventSink for WorkflowEventSink { original_name: tool.original_name.clone(), }) .collect(), + startup_ms: *startup_ms, }, &self.scope, ); return Ok(()); } - CodingEvent::McpServerFailed { server, error, .. } => { + CodingEvent::McpServerFailed { + server, + error, + startup_ms, + } => { self.emitter.emit_scoped( &Event::AgentMcpFailed { node_id: self.node_id.clone(), visit: self.scope.visit, server_name: server.clone(), error: error.clone(), + startup_ms: *startup_ms, + }, + &self.scope, + ); + return Ok(()); + } + CodingEvent::McpServerDisconnected { server, error } => { + self.emitter.emit_scoped( + &Event::AgentMcpDisconnected { + node_id: self.node_id.clone(), + visit: self.scope.visit, + server_name: server.clone(), + error: error.clone(), }, &self.scope, ); diff --git a/lib/foundation/fabro-api/tests/stage_projection_round_trip.rs b/lib/foundation/fabro-api/tests/stage_projection_round_trip.rs index e9ad0aea3..416c27b56 100644 --- a/lib/foundation/fabro-api/tests/stage_projection_round_trip.rs +++ b/lib/foundation/fabro-api/tests/stage_projection_round_trip.rs @@ -528,6 +528,31 @@ fn nested_agent_state_types_match_openapi_json_shape() { let api_mcp: ApiMcpServerProjection = serde_json::from_value(mcp_json).unwrap(); assert_eq!(api_mcp, mcp_server); assert_eq!(mcp_server.tool_count, 1); + + let disconnected = McpServerProjection { + server_name: "filesystem".to_string(), + tool_count: 1, + status: McpServerStatus::Disconnected { + error: "transport closed".to_string(), + }, + invoked: true, + }; + let disconnected_json = serde_json::to_value(&disconnected).unwrap(); + assert_eq!( + disconnected_json, + json!({ + "server_name": "filesystem", + "tool_count": 1, + "status": { + "kind": "disconnected", + "error": "transport closed" + }, + "invoked": true, + }) + ); + let api_disconnected: ApiMcpServerProjection = + serde_json::from_value(disconnected_json).unwrap(); + assert_eq!(api_disconnected, disconnected); } #[test] diff --git a/lib/foundation/fabro-types/src/run_event/agent.rs b/lib/foundation/fabro-types/src/run_event/agent.rs index 005c17d17..91504f375 100644 --- a/lib/foundation/fabro-types/src/run_event/agent.rs +++ b/lib/foundation/fabro-types/src/run_event/agent.rs @@ -79,6 +79,7 @@ pub fn coding_event_name(event: &CodingEvent) -> &'static str { CodingEvent::RouteFailover { .. } => "agent.route.failover", CodingEvent::McpServerReady { .. } => "agent.mcp.server.ready", CodingEvent::McpServerFailed { .. } => "agent.mcp.server.failed", + CodingEvent::McpServerDisconnected { .. } => "agent.mcp.server.disconnected", CodingEvent::SteeringInjected { .. } => "agent.steering.injected", CodingEvent::RoundInterrupted { .. } => "agent.round.interrupted", CodingEvent::CompactionStarted { .. } => "agent.compaction.started", @@ -126,6 +127,7 @@ pub const CODING_EVENT_NAMES: &[&str] = &[ "agent.route.failover", "agent.mcp.server.ready", "agent.mcp.server.failed", + "agent.mcp.server.disconnected", "agent.steering.injected", "agent.round.interrupted", "agent.compaction.started", @@ -238,6 +240,10 @@ pub struct AgentMcpReadyProps { pub tool_count: usize, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub tools: Vec, + /// Whole milliseconds from the server's launch to its tools being + /// listed. Events written before the field existed read as `0`. + #[serde(default)] + pub startup_ms: u64, pub visit: u32, } @@ -251,6 +257,22 @@ pub struct AgentMcpToolSummary { pub struct AgentMcpFailedProps { pub server_name: String, pub error: String, + /// Whole milliseconds from the server's launch to the failure. Events + /// written before the field existed read as `0`. + #[serde(default)] + pub startup_ms: u64, + pub visit: u32, +} + +/// An MCP server that was ready lost its connection during the stage; every +/// later call to its tools fails until the session ends. Pebble reports the +/// disconnect once per server, from whichever session's tool call first +/// observed the closed connection. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AgentMcpDisconnectedProps { + pub server_name: String, + /// What closed the connection, as the client observed it. + pub error: String, pub visit: u32, } @@ -313,6 +335,10 @@ mod tests { CodingEvent::SessionEnded, CodingEvent::ProcessingEnd, CodingEvent::LoopDetected, + CodingEvent::McpServerDisconnected { + server: "github".to_string(), + error: "transport closed".to_string(), + }, CodingEvent::AssistantMessage { text: String::new(), model: "gpt-5.4".to_string(), diff --git a/lib/foundation/fabro-types/src/run_event/mod.rs b/lib/foundation/fabro-types/src/run_event/mod.rs index 1d4ec2c32..9757215d8 100644 --- a/lib/foundation/fabro-types/src/run_event/mod.rs +++ b/lib/foundation/fabro-types/src/run_event/mod.rs @@ -221,6 +221,8 @@ pub enum EventBody { AgentMcpReady(AgentMcpReadyProps), #[serde(rename = "agent.mcp.failed")] AgentMcpFailed(AgentMcpFailedProps), + #[serde(rename = "agent.mcp.disconnected")] + AgentMcpDisconnected(AgentMcpDisconnectedProps), #[serde(rename = "subgraph.started")] SubgraphStarted(SubgraphStartedProps), #[serde(rename = "subgraph.completed")] @@ -508,6 +510,7 @@ impl EventBody { Self::AgentSteerDropped(_) => "agent.steer.dropped", Self::AgentMcpReady(_) => "agent.mcp.ready", Self::AgentMcpFailed(_) => "agent.mcp.failed", + Self::AgentMcpDisconnected(_) => "agent.mcp.disconnected", Self::SubgraphStarted(_) => "subgraph.started", Self::SubgraphCompleted(_) => "subgraph.completed", Self::SandboxInitializing(_) => "sandbox.initializing", @@ -640,6 +643,7 @@ fn is_known_event_name(event: &str) -> bool { | "agent.steer.dropped" | "agent.mcp.ready" | "agent.mcp.failed" + | "agent.mcp.disconnected" | "subgraph.started" | "subgraph.completed" | "sandbox.initializing" @@ -2258,6 +2262,7 @@ mod tests { name: "mcp__github__create_issue".to_string(), original_name: "create_issue".to_string(), }], + startup_ms: 0, visit: 1, }); let value = serde_json::to_value(&body).unwrap(); @@ -2272,12 +2277,71 @@ mod tests { ); } + #[test] + fn agent_mcp_ready_and_failed_carry_startup_ms_and_default_it_when_absent() { + let ready = EventBody::AgentMcpReady(AgentMcpReadyProps { + server_name: "github".to_string(), + tool_count: 0, + tools: Vec::new(), + startup_ms: 842, + visit: 1, + }); + let value = serde_json::to_value(&ready).unwrap(); + assert_eq!(value["properties"]["startup_ms"], 842); + + let failed = EventBody::AgentMcpFailed(AgentMcpFailedProps { + server_name: "filesystem".to_string(), + error: "could not launch `npx`".to_string(), + startup_ms: 4, + visit: 1, + }); + let value = serde_json::to_value(&failed).unwrap(); + assert_eq!(value["properties"]["startup_ms"], 4); + + // Events written before pebble reported startup time. + let legacy: EventBody = serde_json::from_value(json!({ + "event": "agent.mcp.failed", + "properties": { + "server_name": "filesystem", + "error": "Connection refused", + "visit": 1 + } + })) + .unwrap(); + match legacy { + EventBody::AgentMcpFailed(props) => assert_eq!(props.startup_ms, 0), + other => panic!("unexpected body: {other:?}"), + } + } + + #[test] + fn agent_mcp_disconnected_round_trips() { + let body = EventBody::AgentMcpDisconnected(AgentMcpDisconnectedProps { + server_name: "github".to_string(), + error: "transport closed".to_string(), + visit: 1, + }); + let value = serde_json::to_value(&body).unwrap(); + assert_eq!(value["event"], "agent.mcp.disconnected"); + assert_eq!( + value["properties"], + json!({ + "server_name": "github", + "error": "transport closed", + "visit": 1 + }) + ); + let parsed: EventBody = serde_json::from_value(value).unwrap(); + assert_eq!(parsed, body); + } + #[test] fn agent_mcp_ready_omits_tools_when_empty() { let body = EventBody::AgentMcpReady(AgentMcpReadyProps { server_name: "github".to_string(), tool_count: 0, tools: Vec::new(), + startup_ms: 0, visit: 1, }); let value = serde_json::to_value(&body).unwrap(); diff --git a/lib/foundation/fabro-types/src/run_projection.rs b/lib/foundation/fabro-types/src/run_projection.rs index e3f256683..39ee53932 100644 --- a/lib/foundation/fabro-types/src/run_projection.rs +++ b/lib/foundation/fabro-types/src/run_projection.rs @@ -450,8 +450,17 @@ pub struct McpServerProjection { #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum McpServerStatus { - Ready { tools: Vec }, - Failed { error: String }, + Ready { + tools: Vec, + }, + Failed { + error: String, + }, + /// The server was ready and then its connection closed during the + /// stage; its tools fail until the session ends. + Disconnected { + error: String, + }, } /// Convert a 1-based event sequence number into the `NonZeroU32` form used for From 3b33730bac3db396ddef0a0f9345dda4aaed46ef Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sat, 12 Sep 2026 12:39:31 -0600 Subject: [PATCH 3/6] Regenerate the TypeScript API client for McpServerStatusDisconnected Co-Authored-By: Claude Fable 5.1 --- .../src/.openapi-generator/FILES | 1 + .../fabro-api-client/src/models/index.ts | 1 + .../models/mcp-server-status-disconnected.ts | 32 +++++++++++++++++++ .../src/models/mcp-server-status.ts | 5 ++- 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 lib/packages/fabro-api-client/src/models/mcp-server-status-disconnected.ts diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index e57ae9997..86ea048e3 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -215,6 +215,7 @@ models/mcp-server-list-meta.ts models/mcp-server-list-response.ts models/mcp-server-projection.ts models/mcp-server-settings.ts +models/mcp-server-status-disconnected.ts models/mcp-server-status-failed.ts models/mcp-server-status-ready.ts models/mcp-server-status.ts diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index d199bb683..5cbdb7601 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -187,6 +187,7 @@ export * from './mcp-server-list-response'; export * from './mcp-server-projection'; export * from './mcp-server-settings'; export * from './mcp-server-status'; +export * from './mcp-server-status-disconnected'; export * from './mcp-server-status-failed'; export * from './mcp-server-status-ready'; export * from './mcp-transport'; diff --git a/lib/packages/fabro-api-client/src/models/mcp-server-status-disconnected.ts b/lib/packages/fabro-api-client/src/models/mcp-server-status-disconnected.ts new file mode 100644 index 000000000..e684c9d01 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/mcp-server-status-disconnected.ts @@ -0,0 +1,32 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.2.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * The server was ready and then its connection closed during the stage; its tools fail until the session ends. + */ +export interface McpServerStatusDisconnected { + 'kind': McpServerStatusDisconnectedKindEnum; + /** + * What closed the connection, as the client observed it. + */ + 'error': string; +} + +export const McpServerStatusDisconnectedKindEnum = { + DISCONNECTED: 'disconnected' +} as const; + +export type McpServerStatusDisconnectedKindEnum = typeof McpServerStatusDisconnectedKindEnum[keyof typeof McpServerStatusDisconnectedKindEnum]; diff --git a/lib/packages/fabro-api-client/src/models/mcp-server-status.ts b/lib/packages/fabro-api-client/src/models/mcp-server-status.ts index eb898822a..45e63f255 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-server-status.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-server-status.ts @@ -18,6 +18,9 @@ import type { AgentMcpToolSummary } from './agent-mcp-tool-summary'; // May contain unused imports in some cases // @ts-ignore +import type { McpServerStatusDisconnected } from './mcp-server-status-disconnected'; +// May contain unused imports in some cases +// @ts-ignore import type { McpServerStatusFailed } from './mcp-server-status-failed'; // May contain unused imports in some cases // @ts-ignore @@ -27,4 +30,4 @@ import type { McpServerStatusReady } from './mcp-server-status-ready'; * @type McpServerStatus * Projected MCP server readiness status. */ -export type McpServerStatus = { kind: 'failed' } & McpServerStatusFailed | { kind: 'ready' } & McpServerStatusReady; +export type McpServerStatus = { kind: 'disconnected' } & McpServerStatusDisconnected | { kind: 'failed' } & McpServerStatusFailed | { kind: 'ready' } & McpServerStatusReady; From 49afaa9025d80bb020b618ad49c5d62c05657868 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sat, 12 Sep 2026 12:39:31 -0600 Subject: [PATCH 4/6] Render disconnected MCP servers in the stage insights sidebar A server whose connection closed mid-stage shows an amber warning icon and a "Disconnected" badge, distinct from a server that failed to start. Co-Authored-By: Claude Fable 5.1 --- .../stage-insights-sidebar.test.tsx | 20 +++++++ .../app/components/stage-insights-sidebar.tsx | 54 +++++++++++++------ 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/apps/fabro-web/app/components/stage-insights-sidebar.test.tsx b/apps/fabro-web/app/components/stage-insights-sidebar.test.tsx index 4aa368c14..fff871494 100644 --- a/apps/fabro-web/app/components/stage-insights-sidebar.test.tsx +++ b/apps/fabro-web/app/components/stage-insights-sidebar.test.tsx @@ -215,6 +215,26 @@ describe("StageInsightsSidebar", () => { expect(dom).toContain("Failed"); }); + test("renders a disconnected mcp server as disconnected, still counted as used", () => { + const dom = render( + makeStage({ + mcp_servers: [ + { + server_name: "github", + tool_count: 4, + status: { kind: "disconnected", error: "transport closed" }, + invoked: true, + }, + ], + }), + null, + ); + expect(dom).toContain("1/1"); + expect(dom).toContain("github"); + expect(dom).toContain("Disconnected"); + expect(dom).not.toContain("Failed"); + }); + test("shows skill activated/available ratio with source label", () => { const dom = render( makeStage({ diff --git a/apps/fabro-web/app/components/stage-insights-sidebar.tsx b/apps/fabro-web/app/components/stage-insights-sidebar.tsx index 3aa053c4b..65323135e 100644 --- a/apps/fabro-web/app/components/stage-insights-sidebar.tsx +++ b/apps/fabro-web/app/components/stage-insights-sidebar.tsx @@ -7,6 +7,7 @@ import { import { ArrowPathIcon, CheckCircleIcon, + ExclamationTriangleIcon, XCircleIcon, } from "@heroicons/react/24/solid"; import { @@ -554,27 +555,15 @@ function McpSection({ servers }: { servers: McpServerProjection[] }) {
    {servers.map((server) => { // Dim unused servers so the eye lands on the invoked ones first; - // failed servers stay coral regardless. + // failed and disconnected servers keep their tone regardless. const nameClass = server.status.kind === "ready" && !server.invoked ? "min-w-0 flex-1 truncate text-xs text-fg-muted" : "min-w-0 flex-1 truncate text-xs text-fg-2"; return (
  • - {server.status.kind === "ready" ? ( - - ) : ( - - )} + {server.server_name} - {server.status.kind === "ready" ? ( - - {server.invoked - ? "used" - : `${server.tool_count} ${server.tool_count === 1 ? "tool" : "tools"}`} - - ) : ( - Failed - )} +
  • ); })} @@ -582,6 +571,41 @@ function McpSection({ servers }: { servers: McpServerProjection[] }) { ); } +function McpStatusIcon({ status }: { status: McpServerProjection["status"] }) { + switch (status.kind) { + case "ready": + return ; + case "disconnected": + return ( + + ); + case "failed": + return ; + } +} + +function McpStatusBadge({ server }: { server: McpServerProjection }) { + switch (server.status.kind) { + case "ready": + return ( + + {server.invoked + ? "used" + : `${server.tool_count} ${server.tool_count === 1 ? "tool" : "tools"}`} + + ); + case "disconnected": + return ( + Disconnected + ); + case "failed": + return Failed; + } +} + // ---------- helpers ---------- type IconType = (props: { className?: string }) => ReactNode; From 3ac9c213d88e8c7dbd472e8d367492ba0433730d Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sat, 12 Sep 2026 12:48:16 -0600 Subject: [PATCH 5/6] Move the temporary pebble pin to e494e8e Pebble's mcp-embedder-events branch gained a commit that waits for an HTTP-placed MCP server to start answering, up to its startup timeout, before the handshake. No fabro types changed. No fabro test runs a pebble agent against an HTTP MCP server nothing listens on, and none pins the old handshake text, so no fixture needs a shorter timeout. Co-Authored-By: Claude Fable 5.1 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 6 +++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index afc83282a..55d4515a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2063,7 +2063,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2190,7 +2190,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4364,7 +4364,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -5310,7 +5310,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5875,7 +5875,7 @@ dependencies = [ [[package]] name = "pebble-agent" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/pebble?rev=330495748fa8c76a98ca7af31c70d0850523759d#330495748fa8c76a98ca7af31c70d0850523759d" +source = "git+https://github.com/lithoscomputer/pebble?rev=e494e8e2818576b521f295c1426036e2e576028d#e494e8e2818576b521f295c1426036e2e576028d" dependencies = [ "async-trait", "futures-util", @@ -5892,7 +5892,7 @@ dependencies = [ [[package]] name = "pebble-cli-core" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/pebble?rev=330495748fa8c76a98ca7af31c70d0850523759d#330495748fa8c76a98ca7af31c70d0850523759d" +source = "git+https://github.com/lithoscomputer/pebble?rev=e494e8e2818576b521f295c1426036e2e576028d#e494e8e2818576b521f295c1426036e2e576028d" dependencies = [ "anyhow", "async-trait", @@ -5921,7 +5921,7 @@ dependencies = [ [[package]] name = "pebble-coding-agent" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/pebble?rev=330495748fa8c76a98ca7af31c70d0850523759d#330495748fa8c76a98ca7af31c70d0850523759d" +source = "git+https://github.com/lithoscomputer/pebble?rev=e494e8e2818576b521f295c1426036e2e576028d#e494e8e2818576b521f295c1426036e2e576028d" dependencies = [ "async-trait", "futures-util", @@ -6291,7 +6291,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -6768,7 +6768,7 @@ dependencies = [ "errno 0.3.14", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6827,7 +6827,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -7474,7 +7474,7 @@ version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ - "errno 0.2.8", + "errno 0.3.14", "libc", ] @@ -8039,7 +8039,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -8074,7 +8074,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -9141,7 +9141,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7a5d09cc3..cb7a1763a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,9 +123,9 @@ sandbox-driver-testing = { git = "https://github.com/lithoscomputer/sandbox-driv # sandbox-driver revision, so move the two pins together. Pebble pins the # same lithos-llm rev as fabro, and its lockfile policy is that every shared # crate resolves to the version lithos-llm locks. -pebble-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "330495748fa8c76a98ca7af31c70d0850523759d" } -pebble-coding-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "330495748fa8c76a98ca7af31c70d0850523759d", features = ["mcp", "search-providers"] } -pebble-cli-core = { git = "https://github.com/lithoscomputer/pebble", rev = "330495748fa8c76a98ca7af31c70d0850523759d" } +pebble-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "e494e8e2818576b521f295c1426036e2e576028d" } +pebble-coding-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "e494e8e2818576b521f295c1426036e2e576028d", features = ["mcp", "search-providers"] } +pebble-cli-core = { git = "https://github.com/lithoscomputer/pebble", rev = "e494e8e2818576b521f295c1426036e2e576028d" } sentry = { version = "0.35", default-features = false, features = ["backtrace", "contexts", "ureq", "rustls"] } fork = "0.2" exec = "0.3" From b2cf0514a49267cf0a8f72eda2ab22ea4660258a Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sat, 12 Sep 2026 12:57:37 -0600 Subject: [PATCH 6/6] Pin pebble to main after lithoscomputer/pebble#11 Same tree as the development pin e494e8e; only the three pebble source lines in the lockfile move. Co-Authored-By: Claude Fable 5.1 --- Cargo.lock | 6 +++--- Cargo.toml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55d4515a4..d8d74e512 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5875,7 +5875,7 @@ dependencies = [ [[package]] name = "pebble-agent" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/pebble?rev=e494e8e2818576b521f295c1426036e2e576028d#e494e8e2818576b521f295c1426036e2e576028d" +source = "git+https://github.com/lithoscomputer/pebble?rev=4c0063327394cd0f1e9fee2c24541b829b93a8f7#4c0063327394cd0f1e9fee2c24541b829b93a8f7" dependencies = [ "async-trait", "futures-util", @@ -5892,7 +5892,7 @@ dependencies = [ [[package]] name = "pebble-cli-core" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/pebble?rev=e494e8e2818576b521f295c1426036e2e576028d#e494e8e2818576b521f295c1426036e2e576028d" +source = "git+https://github.com/lithoscomputer/pebble?rev=4c0063327394cd0f1e9fee2c24541b829b93a8f7#4c0063327394cd0f1e9fee2c24541b829b93a8f7" dependencies = [ "anyhow", "async-trait", @@ -5921,7 +5921,7 @@ dependencies = [ [[package]] name = "pebble-coding-agent" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/pebble?rev=e494e8e2818576b521f295c1426036e2e576028d#e494e8e2818576b521f295c1426036e2e576028d" +source = "git+https://github.com/lithoscomputer/pebble?rev=4c0063327394cd0f1e9fee2c24541b829b93a8f7#4c0063327394cd0f1e9fee2c24541b829b93a8f7" dependencies = [ "async-trait", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index cb7a1763a..afb360523 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,9 +123,9 @@ sandbox-driver-testing = { git = "https://github.com/lithoscomputer/sandbox-driv # sandbox-driver revision, so move the two pins together. Pebble pins the # same lithos-llm rev as fabro, and its lockfile policy is that every shared # crate resolves to the version lithos-llm locks. -pebble-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "e494e8e2818576b521f295c1426036e2e576028d" } -pebble-coding-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "e494e8e2818576b521f295c1426036e2e576028d", features = ["mcp", "search-providers"] } -pebble-cli-core = { git = "https://github.com/lithoscomputer/pebble", rev = "e494e8e2818576b521f295c1426036e2e576028d" } +pebble-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "4c0063327394cd0f1e9fee2c24541b829b93a8f7" } +pebble-coding-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "4c0063327394cd0f1e9fee2c24541b829b93a8f7", features = ["mcp", "search-providers"] } +pebble-cli-core = { git = "https://github.com/lithoscomputer/pebble", rev = "4c0063327394cd0f1e9fee2c24541b829b93a8f7" } sentry = { version = "0.35", default-features = false, features = ["backtrace", "contexts", "ureq", "rustls"] } fork = "0.2" exec = "0.3"