Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e7a96eb
fix: fence daemon lifetime to active wrapper requests
ejgallego Aug 23, 2026
184ed18
refactor: make wrapper daemon ownership explicit
ejgallego Aug 25, 2026
b1e008a
fix: harden explicit daemon ownership
ejgallego Aug 25, 2026
5371efe
refactor: keep daemon startup failures typed
ejgallego Aug 25, 2026
c8750d1
refactor: centralize broker runtime shutdown
ejgallego Aug 26, 2026
3d3b343
refactor: make broker authoritative for MCP workspaces
ejgallego Aug 26, 2026
8d42ff7
fix: preserve cancellation during runtime shutdown
ejgallego Aug 26, 2026
1a08a27
refactor: detach workspace sessions before shutdown
ejgallego Aug 26, 2026
138e492
fix: unpublish owner generations before drain
ejgallego Aug 26, 2026
6755a49
refactor: type client failures and own MCP runtime control
ejgallego Aug 26, 2026
cbf874d
fix: harden MCP completion and client failures
ejgallego Aug 26, 2026
a901119
fix: harden MCP transport teardown
ejgallego Aug 26, 2026
d79871a
fix: authenticate and harden daemon shutdown
ejgallego Aug 26, 2026
5cfe095
fix: make daemon shutdown observations exact
ejgallego Aug 26, 2026
2f78199
test: restore daemon ownership safety coverage
ejgallego Aug 26, 2026
50411fa
fix: bound daemon identity probes
ejgallego Aug 26, 2026
194f9b9
fix: bound daemon shutdown responses
ejgallego Aug 26, 2026
c0a2d44
refactor: tighten daemon and MCP resource ownership
ejgallego Aug 27, 2026
09bdbe5
fix: close remaining daemon lifetime gaps
ejgallego Aug 27, 2026
3c135b0
refactor: tighten daemon and MCP resource scopes
ejgallego Aug 27, 2026
dfe3853
refactor: enforce explicit wrapper daemon ownership
ejgallego Aug 27, 2026
7b01567
fix: harden daemon capability boundaries
ejgallego Aug 27, 2026
6bbf86b
refactor: freeze wrapper session contract
ejgallego Aug 28, 2026
6741164
test: align session contract checks
ejgallego Aug 28, 2026
4170c09
refactor: enforce explicit wrapper session ownership
ejgallego Aug 28, 2026
32bf3e3
fix: harden wrapper recovery and descriptor publication
ejgallego Aug 28, 2026
e18e983
fix: refuse unsafe control directories
ejgallego Aug 29, 2026
0660193
fix: keep Beam-created project state private
ejgallego Aug 29, 2026
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
147 changes: 131 additions & 16 deletions Beam/Broker/Client.lean
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,48 @@ structure StreamCallbacks where

abbrev Endpoint := Transport.Endpoint

/-- The transport operation that produced a typed broker client failure. -/
inductive BrokerTransportOperation where
| connect
| send
| receive
deriving Repr, BEq

private def BrokerTransportOperation.label : BrokerTransportOperation → String
| .connect => "connect"
| .send => "send"
| .receive => "receive"

/-- Keep broker client failures typed until a CLI or transport presentation boundary. -/
inductive BrokerClientFailure where
| transport (operation : BrokerTransportOperation) (error : IO.Error)
| invalidResponse (detail : String)
| streamCallback (error : IO.Error)
| responseTimeout (timeoutMs : Nat)

def BrokerClientFailure.detail : BrokerClientFailure → String
| .transport operation error =>
s!"Beam daemon {operation.label} failed: {error}"
| .streamCallback error => error.toString
| .invalidResponse detail => detail
| .responseTimeout timeoutMs =>
s!"Beam daemon response timed out after {timeoutMs} ms"

instance : Repr BrokerClientFailure where
reprPrec failure _ := Std.Format.text <|
match failure with
| .transport operation error => s!"BrokerClientFailure.transport {repr operation} {error}"
| .invalidResponse detail => s!"BrokerClientFailure.invalidResponse {detail}"
| .streamCallback error => s!"BrokerClientFailure.streamCallback {error}"
| .responseTimeout timeoutMs => s!"BrokerClientFailure.responseTimeout {timeoutMs}"

private def BrokerClientFailure.toIOError : BrokerClientFailure → IO.Error
| failure@(.transport ..) => IO.userError failure.detail
| .streamCallback error => error
| .invalidResponse detail => IO.userError detail
| .responseTimeout timeoutMs =>
IO.userError s!"Beam daemon response timed out after {timeoutMs} ms"

def parsePortText (name value : String) : Except String UInt16 := do
let some n := value.toNat?
| throw s!"invalid {name} '{value}'"
Expand All @@ -34,13 +76,21 @@ def parseEndpointOption (args : List String) : Except String (Endpoint × List S
| _ =>
pure (.tcp 8765, args)

private def decodeStreamMessage (msg : String) : IO StreamMessage := do
private def decodeStreamMessage (msg : String) : Except String StreamMessage := do
match Json.parse msg with
| .error err => throw <| IO.userError s!"invalid Beam daemon response json: {err}"
| .error err => throw s!"invalid Beam daemon response json: {err}"
| .ok json =>
match fromJson? (α := StreamMessage) json with
| .ok stream => pure stream
| .error err => throw <| IO.userError s!"invalid Beam daemon response payload: {err}"
| .error err => throw s!"invalid Beam daemon response payload: {err}"

private def captureClientFailure
(failure : IO.Error → BrokerClientFailure)
(action : IO α) : IO (Except BrokerClientFailure α) := do
try
pure <| .ok (← action)
catch e =>
pure <| .error (failure e)

private def diagnosticSeverityLabel : Option Lsp.DiagnosticSeverity → String
| some .error => "error"
Expand All @@ -67,41 +117,106 @@ def formatStreamDiagnostic (diagnostic : StreamDiagnostic) : String :=
""
s!"beam: diagnostic {severity}{blocking} {diagnostic.path}:{line}:{character}: {message}"

partial def sendRequestWithStream
private structure ResponseDeadline where
timeoutMs : Nat
deadlineNanos : Nat

/-- Send one request while preserving transport, response, callback, and timeout failures. -/
private partial def sendRequestWithStreamResultCore
(endpoint : Endpoint)
(req : Request)
(onStream : StreamMessage → IO Unit) : IO Response := do
let client ← Transport.connect endpoint
(onStream : StreamMessage → IO Unit)
(responseTimeoutMs? : Option Nat) : IO (Except BrokerClientFailure Response) := do
let client ←
match ← captureClientFailure (.transport .connect) (Transport.connect endpoint) with
| .ok client => pure client
| .error failure => return .error failure
try
Transport.sendMsg client (toJson req).compress
let rec loop : IO Response := do
let msg ← Transport.recvMsg client
let stream ← decodeStreamMessage msg
match ← captureClientFailure (.transport .send) <|
Transport.sendMsg client (toJson req).compress with
| .ok () => pure ()
| .error failure => return .error failure
let deadline? : Option ResponseDeadline ← responseTimeoutMs?.mapM fun timeoutMs => do
pure {
timeoutMs
deadlineNanos := (← IO.monoNanosNow) + timeoutMs * 1000000
}
let rec loop : IO (Except BrokerClientFailure Response) := do
let msg ←
match deadline? with
| none =>
match ← captureClientFailure (.transport .receive) (Transport.recvMsg client) with
| .ok msg => pure msg
| .error failure => return .error failure
| some deadline =>
match ← captureClientFailure (.transport .receive) <|
Transport.recvMsgUntil client deadline.deadlineNanos with
| .ok (some msg) => pure msg
| .ok none => return .error (.responseTimeout deadline.timeoutMs)
| .error failure => return .error failure
let stream ←
match decodeStreamMessage msg with
| .ok stream => pure stream
| .error detail => return .error (.invalidResponse detail)
unless stream.clientRequestId? == req.clientRequestId? do
throw <| IO.userError
return .error <| .invalidResponse <|
s!"Beam daemon stream request id {stream.clientRequestId?} does not match request id {req.clientRequestId?}"
onStream stream
match ← captureClientFailure .streamCallback (onStream stream) with
| .ok () => pure ()
| .error failure => return .error failure
match stream with
| .response _ response =>
pure response
pure <| .ok response
| .fileProgress .. | .diagnostic .. =>
loop
loop
finally
Transport.closeConnection client

partial def sendRequestWithCallbacks
/-- Send one request while preserving transport, response, and callback failures as typed data. -/
partial def sendRequestWithStreamResult
(endpoint : Endpoint)
(req : Request)
(callbacks : StreamCallbacks := {}) : IO Response := do
sendRequestWithStream endpoint req fun stream => do
(onStream : StreamMessage → IO Unit) : IO (Except BrokerClientFailure Response) := do
sendRequestWithStreamResultCore endpoint req onStream none

/-- Send one request with an absolute timeout for receiving its complete response stream. -/
partial def sendRequestWithStreamTimeoutResult
(endpoint : Endpoint)
(req : Request)
(timeoutMs : Nat)
(onStream : StreamMessage → IO Unit) : IO (Except BrokerClientFailure Response) := do
sendRequestWithStreamResultCore endpoint req onStream (some timeoutMs)

partial def sendRequestWithStream
(endpoint : Endpoint)
(req : Request)
(onStream : StreamMessage → IO Unit) : IO Response := do
match ← sendRequestWithStreamResult endpoint req onStream with
| .ok response => pure response
| .error failure => throw failure.toIOError

/-- Send one request with typed client failures and structured progress callbacks. -/
partial def sendRequestWithCallbacksResult
(endpoint : Endpoint)
(req : Request)
(callbacks : StreamCallbacks := {}) : IO (Except BrokerClientFailure Response) := do
sendRequestWithStreamResult endpoint req fun stream => do
match stream with
| .response .. =>
pure ()
| .fileProgress clientRequestId? progress =>
callbacks.onFileProgress clientRequestId? progress
| .diagnostic clientRequestId? diagnostic =>
callbacks.onDiagnostic clientRequestId? diagnostic

partial def sendRequestWithCallbacks
(endpoint : Endpoint)
(req : Request)
(callbacks : StreamCallbacks := {}) : IO Response := do
match ← sendRequestWithCallbacksResult endpoint req callbacks with
| .ok response => pure response
| .error failure => throw failure.toIOError
def sendRequest (endpoint : Endpoint) (req : Request) : IO Response :=
sendRequestWithCallbacks endpoint req

Expand Down
Loading