detached 69cc15b

feat(control): return explicit kill outcomes

commit
69cc15b18414c6a1fb13ee8e8a9eb0ecf964d714
author
brice · · 2026-09-20 07:45 UTC
parent
68c0c3b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
30 files · +1657 -247
M docs/architecture.md +1 -1
@@ -197,7 +197,7 @@ Use cases exchange `protocol.ClientMessage` and `protocol.ServerMessage` values.
197197 `go tool buf generate`, and handle the new variant in `sessionwire`.
198198 Never edit generated `*.pb.go`; never add manual IDs or dispatch tables.
199199 - Implement I/O, queues, workers, environment integration, or technology selection in an adapter or `internal/app`.
200-- Bump `internal/protocol.Version` for negotiated wire layout changes (currently `56`). The preamble epoch (`wire.ProtocolEpoch`, QUIC ALPN `vev/1`) bumps only for an intentional clean break.
200+- Bump `internal/protocol.Version` for negotiated wire layout changes (currently `57`). The preamble epoch (`wire.ProtocolEpoch`, QUIC ALPN `vev/1`) bumps only for an intentional clean break.
201201
202202 ## Broker wire core (Plan 001 P3.1, not activated)
203203
@@ -197,7 +197,7 @@ Use cases exchange `protocol.ClientMessage` and `protocol.ServerMessage` values.
197 `go tool buf generate`, and handle the new variant in `sessionwire`.197 `go tool buf generate`, and handle the new variant in `sessionwire`.
198 Never edit generated `*.pb.go`; never add manual IDs or dispatch tables.198 Never edit generated `*.pb.go`; never add manual IDs or dispatch tables.
199 - Implement I/O, queues, workers, environment integration, or technology selection in an adapter or `internal/app`.199 - Implement I/O, queues, workers, environment integration, or technology selection in an adapter or `internal/app`.
200-- Bump `internal/protocol.Version` for negotiated wire layout changes (currently `56`). The preamble epoch (`wire.ProtocolEpoch`, QUIC ALPN `vev/1`) bumps only for an intentional clean break.200+- Bump `internal/protocol.Version` for negotiated wire layout changes (currently `57`). The preamble epoch (`wire.ProtocolEpoch`, QUIC ALPN `vev/1`) bumps only for an intentional clean break.
201 201
202 ## Broker wire core (Plan 001 P3.1, not activated)202 ## Broker wire core (Plan 001 P3.1, not activated)
203 203
M docs/protocol.md +20 -10
@@ -28,7 +28,7 @@ Server → client: `Welcome`, `ErrorMsg`, `Output`, `Detached`, `Pong`,
2828 `NavigationInventoryResponse`, `NavigationInventoryDemand`,
2929 `NavigationInventorySelection`, `PickerOffer`, `PickerSnapshot`,
3030 `PickerClosed`, `PickerResult`, `PickerFailure`, `PickerPreview`,
31-`PickerControlResponse`.
31+`PickerControlResponse`, `KillResult`.
3232
3333 `RouteNavigationFailure` is the one payload that appears in both unions.
3434 `sessionwire` conversion switches (`decodeProtoClient`/`decodeProtoServer`,
@@ -78,7 +78,7 @@ mutation:
7878 | `DecodeMessageHello`, version mismatch | `ErrorMsg{ErrVersionMismatch}` | no |
7979 | `DecodeMessageHello`, malformed | `ErrorMsg{ErrInternal}` | no |
8080 | `DecodeMessageCommand` | `CommandResult{Code, Text}` | yes, else silent close |
81-| `DecodeMessageKill` | `ErrorMsg{ErrInternal}` | no |
81+| `DecodeMessageKill` | `ErrorMsg{ErrInternal}` | no — a malformed kill has no reliable RequestID to correlate a `KillResult` to |
8282 | `DecodeMessageRemotePreview` | `RemotePreview{Status: Malformed}` | no |
8383 | `DecodeMessageNavigationInventory` | `NavigationInventoryResponse{Invalid / VersionMismatch}` | yes, else silent close |
8484 | other / unknown | `ErrorMsg{ErrInternal, "expected hello"}` | no |
@@ -188,14 +188,24 @@ while `pickerOpen`; only typed picker messages act.
188188 and carry no async byte-budget charge; async sends are charged on
189189 admission and released exactly once on dequeue.
190190
191-Current state: a one-shot control client uses the daemon closing the
192-connection (EOF) as its success signal, so a control failure whose error
193-response cannot be delivered would otherwise be indistinguishable from
194-success; the daemon only logs such a failed send
195-(`daemon.logControlSendFailure`) before closing. The wire-level decision for that
196-residual belongs to the P7 explicit result protocol, which replaces close-as-
197-success with an explicit per-request result; until that cutover the close-based
198-convention is kept unchanged (GO-005, deferred to P7).
191+Current state: the one-shot kill control paths have moved to the explicit
192+result protocol. `Kill` carries a unique nonzero `RequestID`, and the daemon
193+answers every normally decoded kill request with exactly one correlated
194+`KillResult` before the control connection closes: `KillSucceeded`,
195+`KillFailed` (with a bounded structured `Failures` list), or a definite
196+`KillOutcomeUnknown`. The client consumes that result; a close without a
197+result, a mismatched `RequestID`, or a wrong-typed reply is `outcome unknown`
198+and is never replayed, so a control failure whose response cannot be delivered
199+is never mistaken for success. The remaining one-shot controls (`List`,
200+`CommandRequest`, remote preview, inventory, and picker control) have no
201+explicit per-request result. They do not treat close as success: the daemon
202+attempts one response and then closes, and a client that receives no valid
203+response reports a receive error rather than a silent success. Because no
204+typed result exists, an undeliverable response is logged daemon-side only where
205+the handler has a send-failure path (`CommandRequest`, remote preview) and is
206+otherwise unrecorded, so the client is left without any outcome at all; these
207+paths stay weaker than the explicit `KillResult`, and replacing them with
208+explicit per-request results belongs to the P7 cutover (GO-005).
199209
200210 ## Limits
201211
@@ -28,7 +28,7 @@ Server → client: `Welcome`, `ErrorMsg`, `Output`, `Detached`, `Pong`,
28 `NavigationInventoryResponse`, `NavigationInventoryDemand`,28 `NavigationInventoryResponse`, `NavigationInventoryDemand`,
29 `NavigationInventorySelection`, `PickerOffer`, `PickerSnapshot`,29 `NavigationInventorySelection`, `PickerOffer`, `PickerSnapshot`,
30 `PickerClosed`, `PickerResult`, `PickerFailure`, `PickerPreview`,30 `PickerClosed`, `PickerResult`, `PickerFailure`, `PickerPreview`,
31-`PickerControlResponse`.31+`PickerControlResponse`, `KillResult`.
32 32
33 `RouteNavigationFailure` is the one payload that appears in both unions.33 `RouteNavigationFailure` is the one payload that appears in both unions.
34 `sessionwire` conversion switches (`decodeProtoClient`/`decodeProtoServer`,34 `sessionwire` conversion switches (`decodeProtoClient`/`decodeProtoServer`,
@@ -78,7 +78,7 @@ mutation:
78 | `DecodeMessageHello`, version mismatch | `ErrorMsg{ErrVersionMismatch}` | no |78 | `DecodeMessageHello`, version mismatch | `ErrorMsg{ErrVersionMismatch}` | no |
79 | `DecodeMessageHello`, malformed | `ErrorMsg{ErrInternal}` | no |79 | `DecodeMessageHello`, malformed | `ErrorMsg{ErrInternal}` | no |
80 | `DecodeMessageCommand` | `CommandResult{Code, Text}` | yes, else silent close |80 | `DecodeMessageCommand` | `CommandResult{Code, Text}` | yes, else silent close |
81-| `DecodeMessageKill` | `ErrorMsg{ErrInternal}` | no |81+| `DecodeMessageKill` | `ErrorMsg{ErrInternal}` | no — a malformed kill has no reliable RequestID to correlate a `KillResult` to |
82 | `DecodeMessageRemotePreview` | `RemotePreview{Status: Malformed}` | no |82 | `DecodeMessageRemotePreview` | `RemotePreview{Status: Malformed}` | no |
83 | `DecodeMessageNavigationInventory` | `NavigationInventoryResponse{Invalid / VersionMismatch}` | yes, else silent close |83 | `DecodeMessageNavigationInventory` | `NavigationInventoryResponse{Invalid / VersionMismatch}` | yes, else silent close |
84 | other / unknown | `ErrorMsg{ErrInternal, "expected hello"}` | no |84 | other / unknown | `ErrorMsg{ErrInternal, "expected hello"}` | no |
@@ -188,14 +188,24 @@ while `pickerOpen`; only typed picker messages act.
188 and carry no async byte-budget charge; async sends are charged on188 and carry no async byte-budget charge; async sends are charged on
189 admission and released exactly once on dequeue.189 admission and released exactly once on dequeue.
190 190
191-Current state: a one-shot control client uses the daemon closing the191+Current state: the one-shot kill control paths have moved to the explicit
192-connection (EOF) as its success signal, so a control failure whose error192+result protocol. `Kill` carries a unique nonzero `RequestID`, and the daemon
193-response cannot be delivered would otherwise be indistinguishable from193+answers every normally decoded kill request with exactly one correlated
194-success; the daemon only logs such a failed send194+`KillResult` before the control connection closes: `KillSucceeded`,
195-(`daemon.logControlSendFailure`) before closing. The wire-level decision for that195+`KillFailed` (with a bounded structured `Failures` list), or a definite
196-residual belongs to the P7 explicit result protocol, which replaces close-as-196+`KillOutcomeUnknown`. The client consumes that result; a close without a
197-success with an explicit per-request result; until that cutover the close-based197+result, a mismatched `RequestID`, or a wrong-typed reply is `outcome unknown`
198-convention is kept unchanged (GO-005, deferred to P7).198+and is never replayed, so a control failure whose response cannot be delivered
199+is never mistaken for success. The remaining one-shot controls (`List`,
200+`CommandRequest`, remote preview, inventory, and picker control) have no
201+explicit per-request result. They do not treat close as success: the daemon
202+attempts one response and then closes, and a client that receives no valid
203+response reports a receive error rather than a silent success. Because no
204+typed result exists, an undeliverable response is logged daemon-side only where
205+the handler has a send-failure path (`CommandRequest`, remote preview) and is
206+otherwise unrecorded, so the client is left without any outcome at all; these
207+paths stay weaker than the explicit `KillResult`, and replacing them with
208+explicit per-request results belongs to the P7 cutover (GO-005).
199 209
200 ## Limits210 ## Limits
201 211
M internal/adapters/sessionwire/client_test.go +3 -1
@@ -25,7 +25,7 @@ func TestClientConnectionEncodesEveryClientMessage(t *testing.T) {
2525 {name: "detach", message: protocol.Detach{}},
2626 {name: "ping", message: protocol.Ping{}},
2727 {name: "list", message: protocol.List{}},
28- {name: "kill", message: protocol.Kill{Name: "work"}},
28+ {name: "kill", message: protocol.Kill{RequestID: 1, Name: "work"}},
2929 {name: "theme", message: protocol.Theme{TrueColor: true}},
3030 {name: "ack", message: protocol.Ack{Epoch: 1, State: 1}},
3131 {name: "image", message: protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: []byte{1}}},
@@ -73,6 +73,8 @@ func TestClientConnectionDecodesEveryServerMessage(t *testing.T) {
7373 {name: "pong", message: protocol.Pong{}},
7474 {name: "sessions", message: protocol.Sessions{}},
7575 {name: "command result", message: protocol.CommandResult{RequestID: 1, OK: true}},
76+ {name: "kill result", message: protocol.KillResult{RequestID: 1, Outcome: protocol.KillSucceeded}},
77+ {name: "kill result partial failures", message: protocol.KillResult{RequestID: 2, Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: "partial", Failures: []protocol.KillFailure{{Class: "stopped", Name: "work", Text: "boom"}}}},
7678 {name: "attach target", message: protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach}},
7779 {name: "preview", message: protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable}},
7880 {name: "identity", message: protocol.CommittedRouteIdentity{Target: exact}},
@@ -25,7 +25,7 @@ func TestClientConnectionEncodesEveryClientMessage(t *testing.T) {
25 {name: "detach", message: protocol.Detach{}},25 {name: "detach", message: protocol.Detach{}},
26 {name: "ping", message: protocol.Ping{}},26 {name: "ping", message: protocol.Ping{}},
27 {name: "list", message: protocol.List{}},27 {name: "list", message: protocol.List{}},
28- {name: "kill", message: protocol.Kill{Name: "work"}},28+ {name: "kill", message: protocol.Kill{RequestID: 1, Name: "work"}},
29 {name: "theme", message: protocol.Theme{TrueColor: true}},29 {name: "theme", message: protocol.Theme{TrueColor: true}},
30 {name: "ack", message: protocol.Ack{Epoch: 1, State: 1}},30 {name: "ack", message: protocol.Ack{Epoch: 1, State: 1}},
31 {name: "image", message: protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: []byte{1}}},31 {name: "image", message: protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: []byte{1}}},
@@ -73,6 +73,8 @@ func TestClientConnectionDecodesEveryServerMessage(t *testing.T) {
73 {name: "pong", message: protocol.Pong{}},73 {name: "pong", message: protocol.Pong{}},
74 {name: "sessions", message: protocol.Sessions{}},74 {name: "sessions", message: protocol.Sessions{}},
75 {name: "command result", message: protocol.CommandResult{RequestID: 1, OK: true}},75 {name: "command result", message: protocol.CommandResult{RequestID: 1, OK: true}},
76+ {name: "kill result", message: protocol.KillResult{RequestID: 1, Outcome: protocol.KillSucceeded}},
77+ {name: "kill result partial failures", message: protocol.KillResult{RequestID: 2, Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: "partial", Failures: []protocol.KillFailure{{Class: "stopped", Name: "work", Text: "boom"}}}},
76 {name: "attach target", message: protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach}},78 {name: "attach target", message: protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach}},
77 {name: "preview", message: protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable}},79 {name: "preview", message: protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable}},
78 {name: "identity", message: protocol.CommittedRouteIdentity{Target: exact}},80 {name: "identity", message: protocol.CommittedRouteIdentity{Target: exact}},
A internal/adapters/sessionwire/kill_result_test.go +250 -0
@@ -0,0 +1,250 @@
1+package sessionwire
2+
3+// Kill/KillResult wire contract: a Kill request carries a unique nonzero
4+// RequestID, exactly one correlated KillResult answers it, and both directions
5+// fail closed on malformed, wrong-direction, truncated, trailing, or oversize
6+// bytes. The result is a bounded control envelope: it never rides the
7+// Output/bulk ceiling exemption.
8+
9+import (
10+ "testing"
11+
12+ "google.golang.org/protobuf/proto"
13+
14+ "github.com/bnema/vev/internal/protocol"
15+ "github.com/bnema/vev/internal/protocol/wire"
16+ "github.com/stretchr/testify/require"
17+)
18+
19+func testKillResult() protocol.KillResult {
20+ return protocol.KillResult{
21+ RequestID: 0x1122334455,
22+ Outcome: protocol.KillFailed,
23+ Code: protocol.ErrNoSuchSession,
24+ Text: "no such session: work",
25+ Failures: []protocol.KillFailure{
26+ {Class: "live", Name: "one", Text: "boom"},
27+ {Class: "stopped", Name: "two", Text: "gone"},
28+ },
29+ }
30+}
31+
32+// TestKillResultByteRoundTrip proves one semantic KillResult re-encodes to
33+// byte-identical bytes after a scan + generated unmarshal + semantic decode.
34+func TestKillResultByteRoundTrip(t *testing.T) {
35+ message := testKillResult()
36+ raw, err := EncodeServerMessage(message)
37+ require.NoError(t, err)
38+ require.NoError(t, wire.ScanEnvelope(&wire.ServerEnvelope{}, raw))
39+
40+ decoded, err := DecodeServerEnvelope(raw)
41+ require.NoError(t, err)
42+ require.Equal(t, message, decoded)
43+
44+ reencoded, err := EncodeServerMessage(decoded)
45+ require.NoError(t, err)
46+ require.Equal(t, raw, reencoded, "KillResult must re-encode byte-for-byte")
47+}
48+
49+// TestKillRequestByteRoundTrip proves one Kill request with a unique nonzero
50+// RequestID survives the client direction byte-for-byte.
51+func TestKillRequestByteRoundTrip(t *testing.T) {
52+ message := protocol.Kill{RequestID: 0xDEADBEEF, Name: "work", Scope: protocol.KillSession}
53+ raw, err := EncodeClientMessage(message)
54+ require.NoError(t, err)
55+ require.NoError(t, wire.ScanEnvelope(&wire.ClientEnvelope{}, raw))
56+
57+ decoded, err := DecodeClientEnvelope(raw)
58+ require.NoError(t, err)
59+ require.Equal(t, message, decoded)
60+
61+ reencoded, err := EncodeClientMessage(decoded)
62+ require.NoError(t, err)
63+ require.Equal(t, raw, reencoded, "Kill must re-encode byte-for-byte")
64+}
65+
66+// TestKillResultWrongDirectionFailsClosed proves a server-only KillResult
67+// payload is refused on the client direction and the server decoder rejects a
68+// nil envelope, so a result can never be consumed as a request.
69+func TestKillResultWrongDirectionFailsClosed(t *testing.T) {
70+ raw, err := proto.Marshal(&wire.ServerEnvelope{Payload: &wire.ServerEnvelope_KillResult{KillResult: killResultToWire(testKillResult())}})
71+ require.NoError(t, err)
72+
73+ var clientEnvelope wire.ClientEnvelope
74+ err = wire.ScanEnvelope(&clientEnvelope, raw)
75+ require.Error(t, err, "a server KillResult must not scan as a client envelope")
76+
77+ _, err = decodeProtoClient(&wire.ClientEnvelope{Payload: nil})
78+ require.ErrorIs(t, err, ErrWrongDirection)
79+ _, err = decodeProtoServer(nil)
80+ require.ErrorIs(t, err, ErrInvalidMessage)
81+}
82+
83+// TestKillAndKillResultRejectMissingOrOutOfRangeIdentity proves the decode
84+// boundary refuses a zero RequestID, an unknown outcome, an out-of-range code,
85+// and a nil failure entry instead of aliasing them into valid values.
86+func TestKillAndKillResultRejectMissingOrOutOfRangeIdentity(t *testing.T) {
87+ tests := []struct {
88+ name string
89+ run func(t *testing.T) error
90+ wantErr error
91+ }{
92+ {
93+ name: "kill zero request id",
94+ run: func(t *testing.T) error {
95+ _, err := killFromWire(&wire.Kill{Name: "work", Scope: uint32(protocol.KillSession)})
96+ return err
97+ },
98+ wantErr: errProtoConvertRange,
99+ },
100+ {
101+ name: "kill nil",
102+ run: func(t *testing.T) error { _, err := killFromWire(nil); return err },
103+ wantErr: errProtoConvertRange,
104+ },
105+ {
106+ name: "kill result nil",
107+ run: func(t *testing.T) error { _, err := killResultFromWire(nil); return err },
108+ wantErr: errProtoConvertRange,
109+ },
110+ {
111+ name: "kill result zero request id",
112+ run: func(t *testing.T) error {
113+ _, err := killResultFromWire(&wire.KillResult{RequestId: 0, Outcome: uint32(protocol.KillSucceeded)})
114+ return err
115+ },
116+ wantErr: errProtoConvertRange,
117+ },
118+ {
119+ name: "kill result unknown outcome below range",
120+ run: func(t *testing.T) error {
121+ _, err := killResultFromWire(&wire.KillResult{RequestId: 1, Outcome: 0})
122+ return err
123+ },
124+ wantErr: errProtoConvertRange,
125+ },
126+ {
127+ name: "kill result unknown outcome above range",
128+ run: func(t *testing.T) error {
129+ _, err := killResultFromWire(&wire.KillResult{RequestId: 1, Outcome: uint32(protocol.KillOutcomeUnknown) + 1})
130+ return err
131+ },
132+ wantErr: errProtoConvertRange,
133+ },
134+ {
135+ name: "kill result code overflow",
136+ run: func(t *testing.T) error {
137+ _, err := killResultFromWire(&wire.KillResult{RequestId: 1, Outcome: uint32(protocol.KillFailed), Code: 1 << 16})
138+ return err
139+ },
140+ wantErr: errProtoConvertRange,
141+ },
142+ {
143+ name: "kill result nil failure entry",
144+ run: func(t *testing.T) error {
145+ _, err := killResultFromWire(&wire.KillResult{RequestId: 1, Outcome: uint32(protocol.KillFailed), Failures: []*wire.KillFailure{nil}})
146+ return err
147+ },
148+ wantErr: errProtoConvertRange,
149+ },
150+ }
151+ for _, tt := range tests {
152+ t.Run(tt.name, func(t *testing.T) {
153+ require.ErrorIs(t, tt.run(t), tt.wantErr)
154+ })
155+ }
156+}
157+
158+// TestKillResultConversionArmInventory is the sessionwire arm inventory for the
159+// explicit kill result: every arm of the kill encoders is exercised (server
160+// result value/pointer, client request value/pointer), each arm shares the
161+// value arm's bytes, and a typed nil pointer arm fails closed with
162+// ErrInvalidMessage instead of panicking. A new arm added without coverage fails
163+// here.
164+func TestKillResultConversionArmInventory(t *testing.T) {
165+ result := testKillResult()
166+ resultRaw, err := EncodeServerMessage(result)
167+ require.NoError(t, err)
168+ request := protocol.Kill{RequestID: 0xDEADBEEF, Name: "work", Scope: protocol.KillSession}
169+ requestRaw, err := EncodeClientMessage(request)
170+ require.NoError(t, err)
171+
172+ tests := []struct {
173+ name string
174+ // run encodes one arm and returns the bytes and any error.
175+ run func() ([]byte, error)
176+ wantRaw []byte
177+ // wantErr, when non-nil, is the required failure; otherwise run must
178+ // succeed with wantRaw.
179+ wantErr error
180+ }{
181+ {name: "result value arm", run: func() ([]byte, error) { return EncodeServerMessage(result) }, wantRaw: resultRaw},
182+ {name: "result pointer arm", run: func() ([]byte, error) { return EncodeServerMessage(&result) }, wantRaw: resultRaw},
183+ {name: "result typed nil pointer arm", run: func() ([]byte, error) { return EncodeServerMessage((*protocol.KillResult)(nil)) }, wantErr: ErrInvalidMessage},
184+ {name: "request value arm", run: func() ([]byte, error) { return EncodeClientMessage(request) }, wantRaw: requestRaw},
185+ {name: "request pointer arm", run: func() ([]byte, error) { return EncodeClientMessage(&request) }, wantRaw: requestRaw},
186+ {name: "request typed nil pointer arm", run: func() ([]byte, error) { return EncodeClientMessage((*protocol.Kill)(nil)) }, wantErr: ErrInvalidMessage},
187+ }
188+ for _, tt := range tests {
189+ t.Run(tt.name, func(t *testing.T) {
190+ raw, err := tt.run()
191+ if tt.wantErr != nil {
192+ require.ErrorIs(t, err, tt.wantErr)
193+ return
194+ }
195+ require.NoError(t, err)
196+ require.Equal(t, tt.wantRaw, raw, "every arm must share the value arm's bytes")
197+ })
198+ }
199+
200+ // The pointer arm decodes back to the same semantic value, so it is not a
201+ // side channel around validation.
202+ decoded, err := DecodeServerEnvelope(resultRaw)
203+ require.NoError(t, err)
204+ require.Equal(t, result, decoded)
205+}
206+
207+// TestKillResultEnvelopeStrictScan proves the scan boundary rejects a
208+// truncated payload, trailing bytes after a complete variant, and a
209+// concatenated duplicate before any semantic decode.
210+func TestKillResultEnvelopeStrictScan(t *testing.T) {
211+ valid, err := EncodeServerMessage(testKillResult())
212+ require.NoError(t, err)
213+ require.NoError(t, wire.ScanEnvelope(&wire.ServerEnvelope{}, valid))
214+
215+ require.Error(t, wire.ScanEnvelope(&wire.ServerEnvelope{}, valid[:len(valid)-1]))
216+ require.Error(t, wire.ScanEnvelope(&wire.ServerEnvelope{}, append(append([]byte(nil), valid...), 0xFF)))
217+ require.Error(t, wire.ScanEnvelope(&wire.ServerEnvelope{}, append(append([]byte(nil), valid...), valid...)))
218+
219+ _, err = DecodeServerEnvelope(valid[:len(valid)-1])
220+ require.Error(t, err)
221+ var failure *protocol.DecodeFailure
222+ require.ErrorAs(t, err, &failure)
223+ require.Equal(t, protocol.DecodeMalformed, failure.Category)
224+}
225+
226+// TestKillResultIsBoundedControlEnvelope proves a KillResult rides the strict
227+// control ceiling, never the bulk exemption, on both send and receive.
228+func TestKillResultIsBoundedControlEnvelope(t *testing.T) {
229+ message := testKillResult()
230+
231+ raw, err := EncodeServerMessage(message)
232+ require.NoError(t, err)
233+ require.Equal(t, categoryControl, serverEnvelopeCategory(raw), "a KillResult is a control envelope")
234+
235+ // A text sized so the serialized control envelope crosses the 1 MiB
236+ // ceiling while the semantic value itself is only the declared cap.
237+ oversize, err := EncodeServerMessage(protocol.KillResult{RequestID: 1, Outcome: protocol.KillSucceeded, Text: string(make([]byte, wire.ControlEnvelopeLimit))})
238+ require.NoError(t, err)
239+ require.ErrorIs(t, checkCategoryCeiling(oversize, categoryControl, wire.AbsoluteEnvelopeLimit), wire.ErrScanLength,
240+ "a KillResult above the control ceiling must fail even under an absolute negotiated ceiling")
241+
242+ send := &serverConnection{raw: &scriptedTransport{}, ceilings: defaultProtoCeilings()}
243+ require.NoError(t, send.SendServer(message))
244+ require.ErrorIs(t, send.SendServer(protocol.KillResult{RequestID: 1, Outcome: protocol.KillSucceeded, Text: string(make([]byte, wire.ControlEnvelopeLimit))}), wire.ErrScanLength)
245+
246+ recv := &clientConnection{raw: &scriptedTransport{recv: []wire.Envelope{{Payload: oversize}}}, ceilings: defaultProtoCeilings()}
247+ recv.preambleOnce.Do(func() {})
248+ _, err = recv.ReceiveServer()
249+ require.Error(t, err)
250+}
@@ -0,0 +1,250 @@
1+package sessionwire
2+
3+// Kill/KillResult wire contract: a Kill request carries a unique nonzero
4+// RequestID, exactly one correlated KillResult answers it, and both directions
5+// fail closed on malformed, wrong-direction, truncated, trailing, or oversize
6+// bytes. The result is a bounded control envelope: it never rides the
7+// Output/bulk ceiling exemption.
8+
9+import (
10+ "testing"
11+
12+ "google.golang.org/protobuf/proto"
13+
14+ "github.com/bnema/vev/internal/protocol"
15+ "github.com/bnema/vev/internal/protocol/wire"
16+ "github.com/stretchr/testify/require"
17+)
18+
19+func testKillResult() protocol.KillResult {
20+ return protocol.KillResult{
21+ RequestID: 0x1122334455,
22+ Outcome: protocol.KillFailed,
23+ Code: protocol.ErrNoSuchSession,
24+ Text: "no such session: work",
25+ Failures: []protocol.KillFailure{
26+ {Class: "live", Name: "one", Text: "boom"},
27+ {Class: "stopped", Name: "two", Text: "gone"},
28+ },
29+ }
30+}
31+
32+// TestKillResultByteRoundTrip proves one semantic KillResult re-encodes to
33+// byte-identical bytes after a scan + generated unmarshal + semantic decode.
34+func TestKillResultByteRoundTrip(t *testing.T) {
35+ message := testKillResult()
36+ raw, err := EncodeServerMessage(message)
37+ require.NoError(t, err)
38+ require.NoError(t, wire.ScanEnvelope(&wire.ServerEnvelope{}, raw))
39+
40+ decoded, err := DecodeServerEnvelope(raw)
41+ require.NoError(t, err)
42+ require.Equal(t, message, decoded)
43+
44+ reencoded, err := EncodeServerMessage(decoded)
45+ require.NoError(t, err)
46+ require.Equal(t, raw, reencoded, "KillResult must re-encode byte-for-byte")
47+}
48+
49+// TestKillRequestByteRoundTrip proves one Kill request with a unique nonzero
50+// RequestID survives the client direction byte-for-byte.
51+func TestKillRequestByteRoundTrip(t *testing.T) {
52+ message := protocol.Kill{RequestID: 0xDEADBEEF, Name: "work", Scope: protocol.KillSession}
53+ raw, err := EncodeClientMessage(message)
54+ require.NoError(t, err)
55+ require.NoError(t, wire.ScanEnvelope(&wire.ClientEnvelope{}, raw))
56+
57+ decoded, err := DecodeClientEnvelope(raw)
58+ require.NoError(t, err)
59+ require.Equal(t, message, decoded)
60+
61+ reencoded, err := EncodeClientMessage(decoded)
62+ require.NoError(t, err)
63+ require.Equal(t, raw, reencoded, "Kill must re-encode byte-for-byte")
64+}
65+
66+// TestKillResultWrongDirectionFailsClosed proves a server-only KillResult
67+// payload is refused on the client direction and the server decoder rejects a
68+// nil envelope, so a result can never be consumed as a request.
69+func TestKillResultWrongDirectionFailsClosed(t *testing.T) {
70+ raw, err := proto.Marshal(&wire.ServerEnvelope{Payload: &wire.ServerEnvelope_KillResult{KillResult: killResultToWire(testKillResult())}})
71+ require.NoError(t, err)
72+
73+ var clientEnvelope wire.ClientEnvelope
74+ err = wire.ScanEnvelope(&clientEnvelope, raw)
75+ require.Error(t, err, "a server KillResult must not scan as a client envelope")
76+
77+ _, err = decodeProtoClient(&wire.ClientEnvelope{Payload: nil})
78+ require.ErrorIs(t, err, ErrWrongDirection)
79+ _, err = decodeProtoServer(nil)
80+ require.ErrorIs(t, err, ErrInvalidMessage)
81+}
82+
83+// TestKillAndKillResultRejectMissingOrOutOfRangeIdentity proves the decode
84+// boundary refuses a zero RequestID, an unknown outcome, an out-of-range code,
85+// and a nil failure entry instead of aliasing them into valid values.
86+func TestKillAndKillResultRejectMissingOrOutOfRangeIdentity(t *testing.T) {
87+ tests := []struct {
88+ name string
89+ run func(t *testing.T) error
90+ wantErr error
91+ }{
92+ {
93+ name: "kill zero request id",
94+ run: func(t *testing.T) error {
95+ _, err := killFromWire(&wire.Kill{Name: "work", Scope: uint32(protocol.KillSession)})
96+ return err
97+ },
98+ wantErr: errProtoConvertRange,
99+ },
100+ {
101+ name: "kill nil",
102+ run: func(t *testing.T) error { _, err := killFromWire(nil); return err },
103+ wantErr: errProtoConvertRange,
104+ },
105+ {
106+ name: "kill result nil",
107+ run: func(t *testing.T) error { _, err := killResultFromWire(nil); return err },
108+ wantErr: errProtoConvertRange,
109+ },
110+ {
111+ name: "kill result zero request id",
112+ run: func(t *testing.T) error {
113+ _, err := killResultFromWire(&wire.KillResult{RequestId: 0, Outcome: uint32(protocol.KillSucceeded)})
114+ return err
115+ },
116+ wantErr: errProtoConvertRange,
117+ },
118+ {
119+ name: "kill result unknown outcome below range",
120+ run: func(t *testing.T) error {
121+ _, err := killResultFromWire(&wire.KillResult{RequestId: 1, Outcome: 0})
122+ return err
123+ },
124+ wantErr: errProtoConvertRange,
125+ },
126+ {
127+ name: "kill result unknown outcome above range",
128+ run: func(t *testing.T) error {
129+ _, err := killResultFromWire(&wire.KillResult{RequestId: 1, Outcome: uint32(protocol.KillOutcomeUnknown) + 1})
130+ return err
131+ },
132+ wantErr: errProtoConvertRange,
133+ },
134+ {
135+ name: "kill result code overflow",
136+ run: func(t *testing.T) error {
137+ _, err := killResultFromWire(&wire.KillResult{RequestId: 1, Outcome: uint32(protocol.KillFailed), Code: 1 << 16})
138+ return err
139+ },
140+ wantErr: errProtoConvertRange,
141+ },
142+ {
143+ name: "kill result nil failure entry",
144+ run: func(t *testing.T) error {
145+ _, err := killResultFromWire(&wire.KillResult{RequestId: 1, Outcome: uint32(protocol.KillFailed), Failures: []*wire.KillFailure{nil}})
146+ return err
147+ },
148+ wantErr: errProtoConvertRange,
149+ },
150+ }
151+ for _, tt := range tests {
152+ t.Run(tt.name, func(t *testing.T) {
153+ require.ErrorIs(t, tt.run(t), tt.wantErr)
154+ })
155+ }
156+}
157+
158+// TestKillResultConversionArmInventory is the sessionwire arm inventory for the
159+// explicit kill result: every arm of the kill encoders is exercised (server
160+// result value/pointer, client request value/pointer), each arm shares the
161+// value arm's bytes, and a typed nil pointer arm fails closed with
162+// ErrInvalidMessage instead of panicking. A new arm added without coverage fails
163+// here.
164+func TestKillResultConversionArmInventory(t *testing.T) {
165+ result := testKillResult()
166+ resultRaw, err := EncodeServerMessage(result)
167+ require.NoError(t, err)
168+ request := protocol.Kill{RequestID: 0xDEADBEEF, Name: "work", Scope: protocol.KillSession}
169+ requestRaw, err := EncodeClientMessage(request)
170+ require.NoError(t, err)
171+
172+ tests := []struct {
173+ name string
174+ // run encodes one arm and returns the bytes and any error.
175+ run func() ([]byte, error)
176+ wantRaw []byte
177+ // wantErr, when non-nil, is the required failure; otherwise run must
178+ // succeed with wantRaw.
179+ wantErr error
180+ }{
181+ {name: "result value arm", run: func() ([]byte, error) { return EncodeServerMessage(result) }, wantRaw: resultRaw},
182+ {name: "result pointer arm", run: func() ([]byte, error) { return EncodeServerMessage(&result) }, wantRaw: resultRaw},
183+ {name: "result typed nil pointer arm", run: func() ([]byte, error) { return EncodeServerMessage((*protocol.KillResult)(nil)) }, wantErr: ErrInvalidMessage},
184+ {name: "request value arm", run: func() ([]byte, error) { return EncodeClientMessage(request) }, wantRaw: requestRaw},
185+ {name: "request pointer arm", run: func() ([]byte, error) { return EncodeClientMessage(&request) }, wantRaw: requestRaw},
186+ {name: "request typed nil pointer arm", run: func() ([]byte, error) { return EncodeClientMessage((*protocol.Kill)(nil)) }, wantErr: ErrInvalidMessage},
187+ }
188+ for _, tt := range tests {
189+ t.Run(tt.name, func(t *testing.T) {
190+ raw, err := tt.run()
191+ if tt.wantErr != nil {
192+ require.ErrorIs(t, err, tt.wantErr)
193+ return
194+ }
195+ require.NoError(t, err)
196+ require.Equal(t, tt.wantRaw, raw, "every arm must share the value arm's bytes")
197+ })
198+ }
199+
200+ // The pointer arm decodes back to the same semantic value, so it is not a
201+ // side channel around validation.
202+ decoded, err := DecodeServerEnvelope(resultRaw)
203+ require.NoError(t, err)
204+ require.Equal(t, result, decoded)
205+}
206+
207+// TestKillResultEnvelopeStrictScan proves the scan boundary rejects a
208+// truncated payload, trailing bytes after a complete variant, and a
209+// concatenated duplicate before any semantic decode.
210+func TestKillResultEnvelopeStrictScan(t *testing.T) {
211+ valid, err := EncodeServerMessage(testKillResult())
212+ require.NoError(t, err)
213+ require.NoError(t, wire.ScanEnvelope(&wire.ServerEnvelope{}, valid))
214+
215+ require.Error(t, wire.ScanEnvelope(&wire.ServerEnvelope{}, valid[:len(valid)-1]))
216+ require.Error(t, wire.ScanEnvelope(&wire.ServerEnvelope{}, append(append([]byte(nil), valid...), 0xFF)))
217+ require.Error(t, wire.ScanEnvelope(&wire.ServerEnvelope{}, append(append([]byte(nil), valid...), valid...)))
218+
219+ _, err = DecodeServerEnvelope(valid[:len(valid)-1])
220+ require.Error(t, err)
221+ var failure *protocol.DecodeFailure
222+ require.ErrorAs(t, err, &failure)
223+ require.Equal(t, protocol.DecodeMalformed, failure.Category)
224+}
225+
226+// TestKillResultIsBoundedControlEnvelope proves a KillResult rides the strict
227+// control ceiling, never the bulk exemption, on both send and receive.
228+func TestKillResultIsBoundedControlEnvelope(t *testing.T) {
229+ message := testKillResult()
230+
231+ raw, err := EncodeServerMessage(message)
232+ require.NoError(t, err)
233+ require.Equal(t, categoryControl, serverEnvelopeCategory(raw), "a KillResult is a control envelope")
234+
235+ // A text sized so the serialized control envelope crosses the 1 MiB
236+ // ceiling while the semantic value itself is only the declared cap.
237+ oversize, err := EncodeServerMessage(protocol.KillResult{RequestID: 1, Outcome: protocol.KillSucceeded, Text: string(make([]byte, wire.ControlEnvelopeLimit))})
238+ require.NoError(t, err)
239+ require.ErrorIs(t, checkCategoryCeiling(oversize, categoryControl, wire.AbsoluteEnvelopeLimit), wire.ErrScanLength,
240+ "a KillResult above the control ceiling must fail even under an absolute negotiated ceiling")
241+
242+ send := &serverConnection{raw: &scriptedTransport{}, ceilings: defaultProtoCeilings()}
243+ require.NoError(t, send.SendServer(message))
244+ require.ErrorIs(t, send.SendServer(protocol.KillResult{RequestID: 1, Outcome: protocol.KillSucceeded, Text: string(make([]byte, wire.ControlEnvelopeLimit))}), wire.ErrScanLength)
245+
246+ recv := &clientConnection{raw: &scriptedTransport{recv: []wire.Envelope{{Payload: oversize}}}, ceilings: defaultProtoCeilings()}
247+ recv.preambleOnce.Do(func() {})
248+ _, err = recv.ReceiveServer()
249+ require.Error(t, err)
250+}
M internal/adapters/sessionwire/proto_convert_misc.go +31 -2
@@ -187,7 +187,7 @@ func detachedFromWire(message *wire.Detached) (protocol.Detached, error) {
187187 }
188188
189189 func killToWire(message protocol.Kill) *wire.Kill {
190- return &wire.Kill{Name: message.Name, Scope: uint32(message.Scope)}
190+ return &wire.Kill{Name: message.Name, Scope: uint32(message.Scope), RequestId: message.RequestID}
191191 }
192192
193193 func killFromWire(message *wire.Kill) (protocol.Kill, error) {
@@ -201,7 +201,36 @@ func killFromWire(message *wire.Kill) (protocol.Kill, error) {
201201 if scope != protocol.KillSession && scope != protocol.KillDaemon && scope != protocol.KillAll {
202202 return protocol.Kill{}, errProtoConvertRange
203203 }
204- return protocol.Kill{Name: message.GetName(), Scope: scope}, nil
204+ if message.GetRequestId() == 0 {
205+ return protocol.Kill{}, errProtoConvertRange
206+ }
207+ return protocol.Kill{RequestID: message.GetRequestId(), Name: message.GetName(), Scope: scope}, nil
208+}
209+
210+func killResultToWire(message protocol.KillResult) *wire.KillResult {
211+ out := &wire.KillResult{RequestId: message.RequestID, Outcome: uint32(message.Outcome), Code: uint32(message.Code), Text: message.Text}
212+ for _, failure := range message.Failures {
213+ out.Failures = append(out.Failures, &wire.KillFailure{Class: failure.Class, Name: failure.Name, Text: failure.Text})
214+ }
215+ return out
216+}
217+
218+func killResultFromWire(message *wire.KillResult) (protocol.KillResult, error) {
219+ if message == nil || message.GetRequestId() == 0 || message.GetOutcome() < uint32(protocol.KillSucceeded) || message.GetOutcome() > uint32(protocol.KillOutcomeUnknown) {
220+ return protocol.KillResult{}, errProtoConvertRange
221+ }
222+ code, err := mustUint16(message.GetCode())
223+ if err != nil {
224+ return protocol.KillResult{}, err
225+ }
226+ out := protocol.KillResult{RequestID: message.GetRequestId(), Outcome: protocol.KillOutcome(message.GetOutcome()), Code: code, Text: message.GetText()}
227+ for _, failure := range message.GetFailures() {
228+ if failure == nil {
229+ return protocol.KillResult{}, errProtoConvertRange
230+ }
231+ out.Failures = append(out.Failures, protocol.KillFailure{Class: failure.GetClass(), Name: failure.GetName(), Text: failure.GetText()})
232+ }
233+ return out, nil
205234 }
206235
207236 func sessionsToWire(message protocol.Sessions) *wire.Sessions {
@@ -187,7 +187,7 @@ func detachedFromWire(message *wire.Detached) (protocol.Detached, error) {
187 }187 }
188 188
189 func killToWire(message protocol.Kill) *wire.Kill {189 func killToWire(message protocol.Kill) *wire.Kill {
190- return &wire.Kill{Name: message.Name, Scope: uint32(message.Scope)}190+ return &wire.Kill{Name: message.Name, Scope: uint32(message.Scope), RequestId: message.RequestID}
191 }191 }
192 192
193 func killFromWire(message *wire.Kill) (protocol.Kill, error) {193 func killFromWire(message *wire.Kill) (protocol.Kill, error) {
@@ -201,7 +201,36 @@ func killFromWire(message *wire.Kill) (protocol.Kill, error) {
201 if scope != protocol.KillSession && scope != protocol.KillDaemon && scope != protocol.KillAll {201 if scope != protocol.KillSession && scope != protocol.KillDaemon && scope != protocol.KillAll {
202 return protocol.Kill{}, errProtoConvertRange202 return protocol.Kill{}, errProtoConvertRange
203 }203 }
204- return protocol.Kill{Name: message.GetName(), Scope: scope}, nil204+ if message.GetRequestId() == 0 {
205+ return protocol.Kill{}, errProtoConvertRange
206+ }
207+ return protocol.Kill{RequestID: message.GetRequestId(), Name: message.GetName(), Scope: scope}, nil
208+}
209+
210+func killResultToWire(message protocol.KillResult) *wire.KillResult {
211+ out := &wire.KillResult{RequestId: message.RequestID, Outcome: uint32(message.Outcome), Code: uint32(message.Code), Text: message.Text}
212+ for _, failure := range message.Failures {
213+ out.Failures = append(out.Failures, &wire.KillFailure{Class: failure.Class, Name: failure.Name, Text: failure.Text})
214+ }
215+ return out
216+}
217+
218+func killResultFromWire(message *wire.KillResult) (protocol.KillResult, error) {
219+ if message == nil || message.GetRequestId() == 0 || message.GetOutcome() < uint32(protocol.KillSucceeded) || message.GetOutcome() > uint32(protocol.KillOutcomeUnknown) {
220+ return protocol.KillResult{}, errProtoConvertRange
221+ }
222+ code, err := mustUint16(message.GetCode())
223+ if err != nil {
224+ return protocol.KillResult{}, err
225+ }
226+ out := protocol.KillResult{RequestID: message.GetRequestId(), Outcome: protocol.KillOutcome(message.GetOutcome()), Code: code, Text: message.GetText()}
227+ for _, failure := range message.GetFailures() {
228+ if failure == nil {
229+ return protocol.KillResult{}, errProtoConvertRange
230+ }
231+ out.Failures = append(out.Failures, protocol.KillFailure{Class: failure.GetClass(), Name: failure.GetName(), Text: failure.GetText()})
232+ }
233+ return out, nil
205 }234 }
206 235
207 func sessionsToWire(message protocol.Sessions) *wire.Sessions {236 func sessionsToWire(message protocol.Sessions) *wire.Sessions {
M internal/adapters/sessionwire/proto_convert_test.go +3 -1
@@ -42,7 +42,7 @@ func TestProtoClientRoundTrips(t *testing.T) {
4242 protocol.Detach{},
4343 protocol.Ping{},
4444 protocol.List{},
45- protocol.Kill{Name: "work"},
45+ protocol.Kill{RequestID: 1, Name: "work"},
4646 protocol.Theme{TrueColor: true},
4747 protocol.Ack{Epoch: 1, State: 1},
4848 protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: []byte{1}},
@@ -94,6 +94,8 @@ func TestProtoServerRoundTrips(t *testing.T) {
9494 protocol.Pong{},
9595 protocol.Sessions{},
9696 protocol.CommandResult{RequestID: 1, OK: true},
97+ protocol.KillResult{RequestID: 1, Outcome: protocol.KillSucceeded},
98+ protocol.KillResult{RequestID: 2, Outcome: protocol.KillFailed, Code: protocol.ErrNoSuchSession, Text: "no such session: work", Failures: []protocol.KillFailure{{Class: "stopped", Name: "work", Text: "delete failed"}}},
9799 protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach},
98100 protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable},
99101 protocol.CommittedRouteIdentity{Target: exact},
@@ -42,7 +42,7 @@ func TestProtoClientRoundTrips(t *testing.T) {
42 protocol.Detach{},42 protocol.Detach{},
43 protocol.Ping{},43 protocol.Ping{},
44 protocol.List{},44 protocol.List{},
45- protocol.Kill{Name: "work"},45+ protocol.Kill{RequestID: 1, Name: "work"},
46 protocol.Theme{TrueColor: true},46 protocol.Theme{TrueColor: true},
47 protocol.Ack{Epoch: 1, State: 1},47 protocol.Ack{Epoch: 1, State: 1},
48 protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: []byte{1}},48 protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: []byte{1}},
@@ -94,6 +94,8 @@ func TestProtoServerRoundTrips(t *testing.T) {
94 protocol.Pong{},94 protocol.Pong{},
95 protocol.Sessions{},95 protocol.Sessions{},
96 protocol.CommandResult{RequestID: 1, OK: true},96 protocol.CommandResult{RequestID: 1, OK: true},
97+ protocol.KillResult{RequestID: 1, Outcome: protocol.KillSucceeded},
98+ protocol.KillResult{RequestID: 2, Outcome: protocol.KillFailed, Code: protocol.ErrNoSuchSession, Text: "no such session: work", Failures: []protocol.KillFailure{{Class: "stopped", Name: "work", Text: "delete failed"}}},
97 protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach},99 protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach},
98 protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable},100 protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable},
99 protocol.CommittedRouteIdentity{Target: exact},101 protocol.CommittedRouteIdentity{Target: exact},
M internal/adapters/sessionwire/proto_envelope.go +9 -0
@@ -394,6 +394,13 @@ func encodeProtoServer(message protocol.ServerMessage) (*wire.ServerEnvelope, er
394394 return encodeProtoServer(*m)
395395 case protocol.CommandResult:
396396 return &wire.ServerEnvelope{Payload: &wire.ServerEnvelope_CommandResult{CommandResult: commandResultToWire(m)}}, nil
397+ case protocol.KillResult:
398+ return &wire.ServerEnvelope{Payload: &wire.ServerEnvelope_KillResult{KillResult: killResultToWire(m)}}, nil
399+ case *protocol.KillResult:
400+ if m == nil {
401+ return nil, ErrInvalidMessage
402+ }
403+ return encodeProtoServer(*m)
397404 case *protocol.CommandResult:
398405 if m == nil {
399406 return nil, ErrInvalidMessage
@@ -732,6 +739,8 @@ func decodeProtoServer(envelope *wire.ServerEnvelope) (protocol.ServerMessage, e
732739 return sessionsFromWire(payload.Sessions)
733740 case *wire.ServerEnvelope_CommandResult:
734741 return commandResultFromWire(payload.CommandResult)
742+ case *wire.ServerEnvelope_KillResult:
743+ return killResultFromWire(payload.KillResult)
735744 case *wire.ServerEnvelope_AttachTarget:
736745 return attachTargetFromWire(payload.AttachTarget)
737746 case *wire.ServerEnvelope_RemotePreview:
@@ -394,6 +394,13 @@ func encodeProtoServer(message protocol.ServerMessage) (*wire.ServerEnvelope, er
394 return encodeProtoServer(*m)394 return encodeProtoServer(*m)
395 case protocol.CommandResult:395 case protocol.CommandResult:
396 return &wire.ServerEnvelope{Payload: &wire.ServerEnvelope_CommandResult{CommandResult: commandResultToWire(m)}}, nil396 return &wire.ServerEnvelope{Payload: &wire.ServerEnvelope_CommandResult{CommandResult: commandResultToWire(m)}}, nil
397+ case protocol.KillResult:
398+ return &wire.ServerEnvelope{Payload: &wire.ServerEnvelope_KillResult{KillResult: killResultToWire(m)}}, nil
399+ case *protocol.KillResult:
400+ if m == nil {
401+ return nil, ErrInvalidMessage
402+ }
403+ return encodeProtoServer(*m)
397 case *protocol.CommandResult:404 case *protocol.CommandResult:
398 if m == nil {405 if m == nil {
399 return nil, ErrInvalidMessage406 return nil, ErrInvalidMessage
@@ -732,6 +739,8 @@ func decodeProtoServer(envelope *wire.ServerEnvelope) (protocol.ServerMessage, e
732 return sessionsFromWire(payload.Sessions)739 return sessionsFromWire(payload.Sessions)
733 case *wire.ServerEnvelope_CommandResult:740 case *wire.ServerEnvelope_CommandResult:
734 return commandResultFromWire(payload.CommandResult)741 return commandResultFromWire(payload.CommandResult)
742+ case *wire.ServerEnvelope_KillResult:
743+ return killResultFromWire(payload.KillResult)
735 case *wire.ServerEnvelope_AttachTarget:744 case *wire.ServerEnvelope_AttachTarget:
736 return attachTargetFromWire(payload.AttachTarget)745 return attachTargetFromWire(payload.AttachTarget)
737 case *wire.ServerEnvelope_RemotePreview:746 case *wire.ServerEnvelope_RemotePreview:
M internal/adapters/sessionwire/server_test.go +5 -1
@@ -27,7 +27,7 @@ func TestServerConnectionDecodesEveryClientMessage(t *testing.T) {
2727 {name: "detach", want: protocol.Detach{}},
2828 {name: "ping", want: protocol.Ping{}},
2929 {name: "list", want: protocol.List{}},
30- {name: "kill", want: protocol.Kill{Name: "work"}},
30+ {name: "kill", want: protocol.Kill{RequestID: 1, Name: "work"}},
3131 {name: "theme", want: protocol.Theme{TrueColor: true}},
3232 {name: "ack", want: protocol.Ack{Epoch: 1, State: 2}},
3333 {name: "image", want: protocol.ImagePush{InputSeq: 2, Mime: "image/png", Data: []byte{1}}},
@@ -62,6 +62,8 @@ func TestServerConnectionEncodesEveryServerMessage(t *testing.T) {
6262 pong := protocol.Pong{}
6363 sessions := protocol.Sessions{}
6464 commandResult := protocol.CommandResult{RequestID: 1, OK: true}
65+ killResult := protocol.KillResult{RequestID: 1, Outcome: protocol.KillSucceeded}
66+ killFailure := protocol.KillResult{RequestID: 2, Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: "partial", Failures: []protocol.KillFailure{{Class: "stopped", Name: "work", Text: "boom"}}}
6567 attachTarget := protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach}
6668 preview := protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable}
6769 identity := protocol.CommittedRouteIdentity{Target: exact}
@@ -86,6 +88,8 @@ func TestServerConnectionEncodesEveryServerMessage(t *testing.T) {
8688 {name: "pong", message: pong},
8789 {name: "sessions", message: sessions},
8890 {name: "command result", message: commandResult},
91+ {name: "kill result", message: killResult},
92+ {name: "kill result partial failures", message: killFailure},
8993 {name: "attach target", message: attachTarget},
9094 {name: "preview", message: preview},
9195 {name: "identity", message: identity},
@@ -27,7 +27,7 @@ func TestServerConnectionDecodesEveryClientMessage(t *testing.T) {
27 {name: "detach", want: protocol.Detach{}},27 {name: "detach", want: protocol.Detach{}},
28 {name: "ping", want: protocol.Ping{}},28 {name: "ping", want: protocol.Ping{}},
29 {name: "list", want: protocol.List{}},29 {name: "list", want: protocol.List{}},
30- {name: "kill", want: protocol.Kill{Name: "work"}},30+ {name: "kill", want: protocol.Kill{RequestID: 1, Name: "work"}},
31 {name: "theme", want: protocol.Theme{TrueColor: true}},31 {name: "theme", want: protocol.Theme{TrueColor: true}},
32 {name: "ack", want: protocol.Ack{Epoch: 1, State: 2}},32 {name: "ack", want: protocol.Ack{Epoch: 1, State: 2}},
33 {name: "image", want: protocol.ImagePush{InputSeq: 2, Mime: "image/png", Data: []byte{1}}},33 {name: "image", want: protocol.ImagePush{InputSeq: 2, Mime: "image/png", Data: []byte{1}}},
@@ -62,6 +62,8 @@ func TestServerConnectionEncodesEveryServerMessage(t *testing.T) {
62 pong := protocol.Pong{}62 pong := protocol.Pong{}
63 sessions := protocol.Sessions{}63 sessions := protocol.Sessions{}
64 commandResult := protocol.CommandResult{RequestID: 1, OK: true}64 commandResult := protocol.CommandResult{RequestID: 1, OK: true}
65+ killResult := protocol.KillResult{RequestID: 1, Outcome: protocol.KillSucceeded}
66+ killFailure := protocol.KillResult{RequestID: 2, Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: "partial", Failures: []protocol.KillFailure{{Class: "stopped", Name: "work", Text: "boom"}}}
65 attachTarget := protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach}67 attachTarget := protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach}
66 preview := protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable}68 preview := protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable}
67 identity := protocol.CommittedRouteIdentity{Target: exact}69 identity := protocol.CommittedRouteIdentity{Target: exact}
@@ -86,6 +88,8 @@ func TestServerConnectionEncodesEveryServerMessage(t *testing.T) {
86 {name: "pong", message: pong},88 {name: "pong", message: pong},
87 {name: "sessions", message: sessions},89 {name: "sessions", message: sessions},
88 {name: "command result", message: commandResult},90 {name: "command result", message: commandResult},
91+ {name: "kill result", message: killResult},
92+ {name: "kill result partial failures", message: killFailure},
89 {name: "attach target", message: attachTarget},93 {name: "attach target", message: attachTarget},
90 {name: "preview", message: preview},94 {name: "preview", message: preview},
91 {name: "identity", message: identity},95 {name: "identity", message: identity},
M internal/app/connectivity_matrix.go +1 -1
@@ -117,7 +117,7 @@ var connectivityMatrix = []connectivityEntry{
117117 "forceStopDaemonFallback (force_stop.go)",
118118 },
119119 PersistMutation: true,
120- Notes: "Offline runOfflineNamedKill mutation becomes daemon-owned via broker. Kill-all purges sessions and leaves the daemon running; daemon-stop is the distinct explicit stop.",
120+ Notes: "Offline runOfflineNamedKill mutation becomes daemon-owned via broker. Kill-all purges sessions and leaves the daemon running; daemon-stop is the distinct explicit stop. Direct force-stop is offered only when the typed endpoint is unreachable, never after a request may have been delivered.",
121121 },
122122 {
123123 Kind: "kindCmd",
@@ -117,7 +117,7 @@ var connectivityMatrix = []connectivityEntry{
117 "forceStopDaemonFallback (force_stop.go)",117 "forceStopDaemonFallback (force_stop.go)",
118 },118 },
119 PersistMutation: true,119 PersistMutation: true,
120- Notes: "Offline runOfflineNamedKill mutation becomes daemon-owned via broker. Kill-all purges sessions and leaves the daemon running; daemon-stop is the distinct explicit stop.",120+ Notes: "Offline runOfflineNamedKill mutation becomes daemon-owned via broker. Kill-all purges sessions and leaves the daemon running; daemon-stop is the distinct explicit stop. Direct force-stop is offered only when the typed endpoint is unreachable, never after a request may have been delivered.",
121 },121 },
122 {122 {
123 Kind: "kindCmd",123 Kind: "kindCmd",
M internal/app/daemon_integration_test.go +92 -41
@@ -150,62 +150,53 @@ func killDaemon(dir string) error {
150150 return requestKill(dir, protocol.KillDaemon)
151151 }
152152
153+// nextKillRequestID allocates a unique nonzero correlation ID for one control
154+// request, matching the production caller contract.
155+var killRequestSeq atomic.Uint64
156+
157+func nextKillRequestID() uint64 { return killRequestSeq.Add(1) }
158+
153159 // killNamedSession deletes one named live or stopped session over the control
154160 // connection, using the same wire scope the client sends for `kill -s NAME`.
161+// It sends a unique RequestID and consumes the matching KillResult: a failed
162+// outcome is an error, and a close without a result is never read as success.
155163 func killNamedSession(dir, name string) error {
156- raw, err := ipc.DialContext(context.Background(), dir)
157- if err != nil {
158- return err
159- }
160- conn := sessionwire.NewClientConnection(raw)
161- defer func() { _ = conn.Close() }()
162- if err := conn.SendClient(protocol.Kill{Scope: protocol.KillSession, Name: name}); err != nil {
163- return err
164- }
165- reply, err := receiveKillReply(conn, daemonStopTimeout)
166- if errors.Is(err, context.DeadlineExceeded) {
167- return err
168- }
169- if errors.Is(err, io.EOF) {
170- return nil
171- }
172- if err != nil {
173- return err
174- }
175- if em, ok := reply.(protocol.ErrorMsg); ok {
176- return fmt.Errorf("kill failed: %s", em.Text)
177- }
178- return nil
164+ return controlKill(dir, protocol.KillScope(protocol.KillSession), name)
179165 }
180166
181167 func requestKill(dir string, scope protocol.KillScope) error {
168+ return controlKill(dir, scope, "")
169+}
170+
171+func controlKill(dir string, scope protocol.KillScope, name string) error {
182172 raw, err := ipc.DialContext(context.Background(), dir)
183173 if err != nil {
184174 return err
185175 }
186176 conn := sessionwire.NewClientConnection(raw)
187177 defer func() { _ = conn.Close() }()
188- if err := conn.SendClient(protocol.Kill{Scope: scope}); err != nil {
189- return err
190- }
191- reply, err := receiveKillReply(conn, daemonStopTimeout)
192- if errors.Is(err, context.DeadlineExceeded) {
178+ requestID := nextKillRequestID()
179+ if err := conn.SendClient(protocol.Kill{RequestID: requestID, Scope: scope, Name: name}); err != nil {
193180 return err
194181 }
195- if errors.Is(err, io.EOF) {
196- return nil
197- }
182+ result, err := awaitKillResult(conn, requestID, daemonStopTimeout)
198183 if err != nil {
199184 return err
200185 }
201- em, ok := reply.(protocol.ErrorMsg)
202- if !ok {
186+ switch result.Outcome {
187+ case protocol.KillSucceeded:
203188 return nil
189+ case protocol.KillFailed:
190+ return fmt.Errorf("kill failed: %s", result.Text)
191+ default:
192+ return fmt.Errorf("kill outcome unknown: %s", result.Text)
204193 }
205- return fmt.Errorf("kill failed: %s", em.Text)
206194 }
207195
208-func receiveKillReply(conn ports.ClientConnection, timeout time.Duration) (protocol.ServerMessage, error) {
196+// awaitKillResult reads one correlated KillResult with a bounded timeout. A
197+// close, a wrong type, or a mismatched RequestID is a definite error rather
198+// than an inferred success, so a lost result is never reported as a kill.
199+func awaitKillResult(conn ports.ClientConnection, requestID uint64, timeout time.Duration) (protocol.KillResult, error) {
209200 type result struct {
210201 message protocol.ServerMessage
211202 err error
@@ -218,16 +209,29 @@ func receiveKillReply(conn ports.ClientConnection, timeout time.Duration) (proto
218209
219210 timer := time.NewTimer(timeout)
220211 defer timer.Stop()
212+ var reply result
221213 select {
222- case reply := <-received:
223- return reply.message, reply.err
214+ case reply = <-received:
224215 case <-timer.C:
225216 _ = conn.Close()
226- reply := <-received
227- return reply.message, errors.Join(context.DeadlineExceeded, reply.err)
217+ reply = <-received
218+ return protocol.KillResult{}, errors.Join(context.DeadlineExceeded, reply.err)
228219 }
220+ if reply.err != nil {
221+ return protocol.KillResult{}, reply.err
222+ }
223+ killResult, ok := reply.message.(protocol.KillResult)
224+ if !ok {
225+ return protocol.KillResult{}, fmt.Errorf("unexpected reply %T to kill", reply.message)
226+ }
227+ if killResult.RequestID != requestID {
228+ return protocol.KillResult{}, fmt.Errorf("kill result request id %d, want %d", killResult.RequestID, requestID)
229+ }
230+ return killResult, nil
229231 }
230232
233+// blockingClientConnection parks ReceiveServer until Close, so a test can prove
234+// the bounded await returns on timeout rather than hanging.
231235 type blockingClientConnection struct {
232236 closed chan struct{}
233237 once sync.Once
@@ -252,13 +256,60 @@ func (c *blockingClientConnection) Close() error {
252256 return nil
253257 }
254258
255-func TestReceiveKillReplyTimesOut(t *testing.T) {
259+// TestAwaitKillResultTimesOut proves the bounded result await reports a timeout
260+// (never a silent success) and closes the exact connection when no
261+// KillResult arrives.
262+func TestAwaitKillResultTimesOut(t *testing.T) {
256263 conn := &blockingClientConnection{closed: make(chan struct{})}
257- _, err := receiveKillReply(conn, 10*time.Millisecond)
264+ _, err := awaitKillResult(conn, 1, 10*time.Millisecond)
258265 require.ErrorIs(t, err, context.DeadlineExceeded)
259266 require.ErrorIs(t, err, io.ErrClosedPipe)
260267 }
261268
269+// TestAwaitKillResultRejectsUncorrelatedReply proves a reply of the wrong type
270+// or with a mismatched RequestID is a definite error, so a lost or stale result
271+// is never mistaken for this request's success.
272+func TestAwaitKillResultRejectsUncorrelatedReply(t *testing.T) {
273+ tests := []struct {
274+ name string
275+ reply protocol.ServerMessage
276+ }{
277+ {name: "wrong type", reply: protocol.Pong{}},
278+ {name: "wrong request id", reply: protocol.KillResult{RequestID: 9, Outcome: protocol.KillSucceeded}},
279+ }
280+ for _, tt := range tests {
281+ t.Run(tt.name, func(t *testing.T) {
282+ conn := &scriptedKillReplyConnection{reply: tt.reply}
283+ _, err := awaitKillResult(conn, 1, time.Second)
284+ require.Error(t, err)
285+ })
286+ }
287+}
288+
289+// scriptedKillReplyConnection returns exactly one canned server message.
290+type scriptedKillReplyConnection struct {
291+ reply protocol.ServerMessage
292+}
293+
294+func (*scriptedKillReplyConnection) SendClient(protocol.ClientMessage) error { return nil }
295+
296+func (c *scriptedKillReplyConnection) ReceiveServer() (protocol.ServerMessage, error) {
297+ if c.reply == nil {
298+ return nil, io.EOF
299+ }
300+ reply := c.reply
301+ c.reply = nil
302+ return reply, nil
303+}
304+
305+func (*scriptedKillReplyConnection) Capabilities() protocol.ConnectionCapabilities {
306+ return protocol.ConnectionCapabilities{}
307+}
308+
309+func (*scriptedKillReplyConnection) LinkState() ports.LinkState { return ports.LinkState(0) }
310+func (*scriptedKillReplyConnection) LinkEvents() <-chan ports.LinkEvent { return nil }
311+func (*scriptedKillReplyConnection) Close() error { return nil }
312+
262313 // mustHelloBytes encodes a minimal valid Hello for first-frame shape probes.
263314 func mustHelloBytes() []byte {
264315 raw, err := sessionwire.EncodeClientMessage(protocol.Hello{
@@ -150,62 +150,53 @@ func killDaemon(dir string) error {
150 return requestKill(dir, protocol.KillDaemon)150 return requestKill(dir, protocol.KillDaemon)
151 }151 }
152 152
153+// nextKillRequestID allocates a unique nonzero correlation ID for one control
154+// request, matching the production caller contract.
155+var killRequestSeq atomic.Uint64
156+
157+func nextKillRequestID() uint64 { return killRequestSeq.Add(1) }
158+
153 // killNamedSession deletes one named live or stopped session over the control159 // killNamedSession deletes one named live or stopped session over the control
154 // connection, using the same wire scope the client sends for `kill -s NAME`.160 // connection, using the same wire scope the client sends for `kill -s NAME`.
161+// It sends a unique RequestID and consumes the matching KillResult: a failed
162+// outcome is an error, and a close without a result is never read as success.
155 func killNamedSession(dir, name string) error {163 func killNamedSession(dir, name string) error {
156- raw, err := ipc.DialContext(context.Background(), dir)164+ return controlKill(dir, protocol.KillScope(protocol.KillSession), name)
157- if err != nil {
158- return err
159- }
160- conn := sessionwire.NewClientConnection(raw)
161- defer func() { _ = conn.Close() }()
162- if err := conn.SendClient(protocol.Kill{Scope: protocol.KillSession, Name: name}); err != nil {
163- return err
164- }
165- reply, err := receiveKillReply(conn, daemonStopTimeout)
166- if errors.Is(err, context.DeadlineExceeded) {
167- return err
168- }
169- if errors.Is(err, io.EOF) {
170- return nil
171- }
172- if err != nil {
173- return err
174- }
175- if em, ok := reply.(protocol.ErrorMsg); ok {
176- return fmt.Errorf("kill failed: %s", em.Text)
177- }
178- return nil
179 }165 }
180 166
181 func requestKill(dir string, scope protocol.KillScope) error {167 func requestKill(dir string, scope protocol.KillScope) error {
168+ return controlKill(dir, scope, "")
169+}
170+
171+func controlKill(dir string, scope protocol.KillScope, name string) error {
182 raw, err := ipc.DialContext(context.Background(), dir)172 raw, err := ipc.DialContext(context.Background(), dir)
183 if err != nil {173 if err != nil {
184 return err174 return err
185 }175 }
186 conn := sessionwire.NewClientConnection(raw)176 conn := sessionwire.NewClientConnection(raw)
187 defer func() { _ = conn.Close() }()177 defer func() { _ = conn.Close() }()
188- if err := conn.SendClient(protocol.Kill{Scope: scope}); err != nil {178+ requestID := nextKillRequestID()
189- return err179+ if err := conn.SendClient(protocol.Kill{RequestID: requestID, Scope: scope, Name: name}); err != nil {
190- }
191- reply, err := receiveKillReply(conn, daemonStopTimeout)
192- if errors.Is(err, context.DeadlineExceeded) {
193 return err180 return err
194 }181 }
195- if errors.Is(err, io.EOF) {182+ result, err := awaitKillResult(conn, requestID, daemonStopTimeout)
196- return nil
197- }
198 if err != nil {183 if err != nil {
199 return err184 return err
200 }185 }
201- em, ok := reply.(protocol.ErrorMsg)186+ switch result.Outcome {
202- if !ok {187+ case protocol.KillSucceeded:
203 return nil188 return nil
189+ case protocol.KillFailed:
190+ return fmt.Errorf("kill failed: %s", result.Text)
191+ default:
192+ return fmt.Errorf("kill outcome unknown: %s", result.Text)
204 }193 }
205- return fmt.Errorf("kill failed: %s", em.Text)
206 }194 }
207 195
208-func receiveKillReply(conn ports.ClientConnection, timeout time.Duration) (protocol.ServerMessage, error) {196+// awaitKillResult reads one correlated KillResult with a bounded timeout. A
197+// close, a wrong type, or a mismatched RequestID is a definite error rather
198+// than an inferred success, so a lost result is never reported as a kill.
199+func awaitKillResult(conn ports.ClientConnection, requestID uint64, timeout time.Duration) (protocol.KillResult, error) {
209 type result struct {200 type result struct {
210 message protocol.ServerMessage201 message protocol.ServerMessage
211 err error202 err error
@@ -218,16 +209,29 @@ func receiveKillReply(conn ports.ClientConnection, timeout time.Duration) (proto
218 209
219 timer := time.NewTimer(timeout)210 timer := time.NewTimer(timeout)
220 defer timer.Stop()211 defer timer.Stop()
212+ var reply result
221 select {213 select {
222- case reply := <-received:214+ case reply = <-received:
223- return reply.message, reply.err
224 case <-timer.C:215 case <-timer.C:
225 _ = conn.Close()216 _ = conn.Close()
226- reply := <-received217+ reply = <-received
227- return reply.message, errors.Join(context.DeadlineExceeded, reply.err)218+ return protocol.KillResult{}, errors.Join(context.DeadlineExceeded, reply.err)
228 }219 }
220+ if reply.err != nil {
221+ return protocol.KillResult{}, reply.err
222+ }
223+ killResult, ok := reply.message.(protocol.KillResult)
224+ if !ok {
225+ return protocol.KillResult{}, fmt.Errorf("unexpected reply %T to kill", reply.message)
226+ }
227+ if killResult.RequestID != requestID {
228+ return protocol.KillResult{}, fmt.Errorf("kill result request id %d, want %d", killResult.RequestID, requestID)
229+ }
230+ return killResult, nil
229 }231 }
230 232
233+// blockingClientConnection parks ReceiveServer until Close, so a test can prove
234+// the bounded await returns on timeout rather than hanging.
231 type blockingClientConnection struct {235 type blockingClientConnection struct {
232 closed chan struct{}236 closed chan struct{}
233 once sync.Once237 once sync.Once
@@ -252,13 +256,60 @@ func (c *blockingClientConnection) Close() error {
252 return nil256 return nil
253 }257 }
254 258
255-func TestReceiveKillReplyTimesOut(t *testing.T) {259+// TestAwaitKillResultTimesOut proves the bounded result await reports a timeout
260+// (never a silent success) and closes the exact connection when no
261+// KillResult arrives.
262+func TestAwaitKillResultTimesOut(t *testing.T) {
256 conn := &blockingClientConnection{closed: make(chan struct{})}263 conn := &blockingClientConnection{closed: make(chan struct{})}
257- _, err := receiveKillReply(conn, 10*time.Millisecond)264+ _, err := awaitKillResult(conn, 1, 10*time.Millisecond)
258 require.ErrorIs(t, err, context.DeadlineExceeded)265 require.ErrorIs(t, err, context.DeadlineExceeded)
259 require.ErrorIs(t, err, io.ErrClosedPipe)266 require.ErrorIs(t, err, io.ErrClosedPipe)
260 }267 }
261 268
269+// TestAwaitKillResultRejectsUncorrelatedReply proves a reply of the wrong type
270+// or with a mismatched RequestID is a definite error, so a lost or stale result
271+// is never mistaken for this request's success.
272+func TestAwaitKillResultRejectsUncorrelatedReply(t *testing.T) {
273+ tests := []struct {
274+ name string
275+ reply protocol.ServerMessage
276+ }{
277+ {name: "wrong type", reply: protocol.Pong{}},
278+ {name: "wrong request id", reply: protocol.KillResult{RequestID: 9, Outcome: protocol.KillSucceeded}},
279+ }
280+ for _, tt := range tests {
281+ t.Run(tt.name, func(t *testing.T) {
282+ conn := &scriptedKillReplyConnection{reply: tt.reply}
283+ _, err := awaitKillResult(conn, 1, time.Second)
284+ require.Error(t, err)
285+ })
286+ }
287+}
288+
289+// scriptedKillReplyConnection returns exactly one canned server message.
290+type scriptedKillReplyConnection struct {
291+ reply protocol.ServerMessage
292+}
293+
294+func (*scriptedKillReplyConnection) SendClient(protocol.ClientMessage) error { return nil }
295+
296+func (c *scriptedKillReplyConnection) ReceiveServer() (protocol.ServerMessage, error) {
297+ if c.reply == nil {
298+ return nil, io.EOF
299+ }
300+ reply := c.reply
301+ c.reply = nil
302+ return reply, nil
303+}
304+
305+func (*scriptedKillReplyConnection) Capabilities() protocol.ConnectionCapabilities {
306+ return protocol.ConnectionCapabilities{}
307+}
308+
309+func (*scriptedKillReplyConnection) LinkState() ports.LinkState { return ports.LinkState(0) }
310+func (*scriptedKillReplyConnection) LinkEvents() <-chan ports.LinkEvent { return nil }
311+func (*scriptedKillReplyConnection) Close() error { return nil }
312+
262 // mustHelloBytes encodes a minimal valid Hello for first-frame shape probes.313 // mustHelloBytes encodes a minimal valid Hello for first-frame shape probes.
263 func mustHelloBytes() []byte {314 func mustHelloBytes() []byte {
264 raw, err := sessionwire.EncodeClientMessage(protocol.Hello{315 raw, err := sessionwire.EncodeClientMessage(protocol.Hello{
M internal/app/force_stop.go +7 -5
@@ -138,7 +138,7 @@ func forceStopDaemon(ctx context.Context, runtimeDir string, hooks forceStopHook
138138
139139 confirmed, err := hooks.confirm(fmt.Sprintf(
140140 "vev: daemon process %d (%s) still owns %s after the typed kill request failed.\n"+
141- "vev: it may be incompatible or unreachable. Send SIGTERM to process %d?",
141+ "vev: it is unreachable through the typed control endpoint. Send SIGTERM to process %d?",
142142 candidate.PID, candidate.Command, lifecycle.Path(runtimeDir), candidate.PID))
143143 if err != nil {
144144 return err
@@ -284,10 +284,12 @@ var forceStopDaemonFn = func(ctx context.Context) error {
284284 return forceStopDaemon(ctx, ipc.SocketDir(), defaultForceStopHooks())
285285 }
286286
287-// forceStopDaemonFallback offers the interactive force-stop after the typed
288-// KillDaemon protocol failed. When no verified daemon process holds lifecycle
289-// ownership, the original failure is returned unchanged so the caller keeps its
290-// own diagnosis.
287+// forceStopDaemonFallback offers the interactive force-stop when the daemon
288+// cannot be reached through the typed protocol. Once a request may have been
289+// delivered, the caller preserves its explicit or outcome-unknown result rather
290+// than bypassing the protocol with a direct process signal. When no verified
291+// daemon process holds lifecycle ownership, the original failure is returned
292+// unchanged so the caller keeps its own diagnosis.
291293 func forceStopDaemonFallback(ctx context.Context, cause error) error {
292294 err := forceStopDaemonFn(ctx)
293295 if errors.Is(err, errForceStopNotApplicable) {
@@ -138,7 +138,7 @@ func forceStopDaemon(ctx context.Context, runtimeDir string, hooks forceStopHook
138 138
139 confirmed, err := hooks.confirm(fmt.Sprintf(139 confirmed, err := hooks.confirm(fmt.Sprintf(
140 "vev: daemon process %d (%s) still owns %s after the typed kill request failed.\n"+140 "vev: daemon process %d (%s) still owns %s after the typed kill request failed.\n"+
141- "vev: it may be incompatible or unreachable. Send SIGTERM to process %d?",141+ "vev: it is unreachable through the typed control endpoint. Send SIGTERM to process %d?",
142 candidate.PID, candidate.Command, lifecycle.Path(runtimeDir), candidate.PID))142 candidate.PID, candidate.Command, lifecycle.Path(runtimeDir), candidate.PID))
143 if err != nil {143 if err != nil {
144 return err144 return err
@@ -284,10 +284,12 @@ var forceStopDaemonFn = func(ctx context.Context) error {
284 return forceStopDaemon(ctx, ipc.SocketDir(), defaultForceStopHooks())284 return forceStopDaemon(ctx, ipc.SocketDir(), defaultForceStopHooks())
285 }285 }
286 286
287-// forceStopDaemonFallback offers the interactive force-stop after the typed287+// forceStopDaemonFallback offers the interactive force-stop when the daemon
288-// KillDaemon protocol failed. When no verified daemon process holds lifecycle288+// cannot be reached through the typed protocol. Once a request may have been
289-// ownership, the original failure is returned unchanged so the caller keeps its289+// delivered, the caller preserves its explicit or outcome-unknown result rather
290-// own diagnosis.290+// than bypassing the protocol with a direct process signal. When no verified
291+// daemon process holds lifecycle ownership, the original failure is returned
292+// unchanged so the caller keeps its own diagnosis.
291 func forceStopDaemonFallback(ctx context.Context, cause error) error {293 func forceStopDaemonFallback(ctx context.Context, cause error) error {
292 err := forceStopDaemonFn(ctx)294 err := forceStopDaemonFn(ctx)
293 if errors.Is(err, errForceStopNotApplicable) {295 if errors.Is(err, errForceStopNotApplicable) {
A internal/app/kill_result_test.go +429 -0
@@ -0,0 +1,429 @@
1+package app
2+
3+// KillResult caller contract (explicit result slice). runKill and
4+// requestDaemonStop send one Kill with a unique nonzero RequestID and consume
5+// exactly one matching KillResult; a lost, uncorrelated, or wrong-typed reply is
6+// outcome-unknown and is never replayed, while a caller canceled before the
7+// send is a definite not-sent outcome. These tests drive the two helpers those
8+// callers share with a controllable in-memory client connection, so every case
9+// is bounded and uses no sleeps.
10+
11+import (
12+ "context"
13+ "errors"
14+ "io"
15+ "sync"
16+ "sync/atomic"
17+ "testing"
18+ "time"
19+
20+ "github.com/stretchr/testify/require"
21+
22+ "github.com/bnema/vev/internal/adapters/ipc"
23+ "github.com/bnema/vev/internal/adapters/sessionwire"
24+ "github.com/bnema/vev/internal/ports"
25+ "github.com/bnema/vev/internal/protocol"
26+)
27+
28+// fakeKillConnection is a scripted ports.ClientConnection. Sends are recorded
29+// and counted; ReceiveServer serves queued replies in order, then either a
30+// reply derived from the last sent Kill (replyForKill), a terminal error, or a
31+// block until Close.
32+type fakeKillConnection struct {
33+ mu sync.Mutex
34+ sends []protocol.ClientMessage
35+ recv []protocol.ServerMessage
36+ replyForKill func(protocol.Kill) protocol.ServerMessage
37+ sendErr error
38+ recvErr error
39+ // block: once no reply is available, ReceiveServer parks until Close.
40+ block bool
41+ closed chan struct{}
42+ once sync.Once
43+ // recvEntered reports that a blocked ReceiveServer was actually entered.
44+ recvEntered chan struct{}
45+ enterOnce sync.Once
46+}
47+
48+func newFakeKillConnection() *fakeKillConnection {
49+ return &fakeKillConnection{closed: make(chan struct{}), recvEntered: make(chan struct{})}
50+}
51+
52+func (c *fakeKillConnection) SendClient(message protocol.ClientMessage) error {
53+ c.mu.Lock()
54+ defer c.mu.Unlock()
55+ if c.sendErr != nil {
56+ return c.sendErr
57+ }
58+ c.sends = append(c.sends, message)
59+ return nil
60+}
61+
62+func (c *fakeKillConnection) ReceiveServer() (protocol.ServerMessage, error) {
63+ c.mu.Lock()
64+ if len(c.recv) > 0 {
65+ next := c.recv[0]
66+ c.recv = c.recv[1:]
67+ c.mu.Unlock()
68+ return next, nil
69+ }
70+ // replyForKill derives a matching result from the exact request that was
71+ // sent, so a test never has to predict the fresh random RequestID.
72+ if c.replyForKill != nil && len(c.sends) > 0 {
73+ if kill, ok := c.sends[len(c.sends)-1].(protocol.Kill); ok {
74+ reply := c.replyForKill(kill)
75+ c.mu.Unlock()
76+ return reply, nil
77+ }
78+ }
79+ block, recvErr := c.block, c.recvErr
80+ c.mu.Unlock()
81+ if !block {
82+ return nil, recvErr
83+ }
84+ c.enterOnce.Do(func() { close(c.recvEntered) })
85+ <-c.closed
86+ return nil, io.ErrClosedPipe
87+}
88+
89+func (c *fakeKillConnection) Capabilities() protocol.ConnectionCapabilities {
90+ return protocol.ConnectionCapabilities{}
91+}
92+
93+func (*fakeKillConnection) LinkState() ports.LinkState { return ports.LinkState(0) }
94+func (*fakeKillConnection) LinkEvents() <-chan ports.LinkEvent { return nil }
95+
96+func (c *fakeKillConnection) Close() error {
97+ c.once.Do(func() { close(c.closed) })
98+ return nil
99+}
100+
101+func (c *fakeKillConnection) sentCount() int {
102+ c.mu.Lock()
103+ defer c.mu.Unlock()
104+ return len(c.sends)
105+}
106+
107+func (c *fakeKillConnection) sentKill(t *testing.T) protocol.Kill {
108+ t.Helper()
109+ c.mu.Lock()
110+ defer c.mu.Unlock()
111+ require.Len(t, c.sends, 1, "exactly one Kill must be sent (no replay)")
112+ kill, ok := c.sends[0].(protocol.Kill)
113+ require.True(t, ok, "sent message %T is not a Kill", c.sends[0])
114+ return kill
115+}
116+
117+// sendAndAwait drives the shared send/receive pair once, as runKill and
118+// requestDaemonStop do.
119+func sendAndAwait(ctx context.Context, conn *fakeKillConnection, request protocol.Kill) error {
120+ requestID, err := sendKillRequest(ctx, conn, request)
121+ if err != nil {
122+ return err
123+ }
124+ return receiveKillResult(ctx, conn, requestID)
125+}
126+
127+// matchingResult mirrors one Kill's RequestID into a successful result.
128+func matchingResult(outcome protocol.KillOutcome, code uint16, text string) func(protocol.Kill) protocol.ServerMessage {
129+ return func(kill protocol.Kill) protocol.ServerMessage {
130+ return protocol.KillResult{RequestID: kill.RequestID, Outcome: outcome, Code: code, Text: text}
131+ }
132+}
133+
134+func TestKillResultMatchingSuccessConsumesOneCorrelatedResult(t *testing.T) {
135+ conn := newFakeKillConnection()
136+ conn.replyForKill = matchingResult(protocol.KillSucceeded, 0, "")
137+
138+ require.NoError(t, sendAndAwait(context.Background(), conn, protocol.Kill{Name: "work", Scope: protocol.KillSession}))
139+
140+ kill := conn.sentKill(t)
141+ require.NotZero(t, kill.RequestID, "a Kill must carry a nonzero RequestID")
142+ require.Equal(t, "work", kill.Name)
143+ require.Equal(t, protocol.KillSession, kill.Scope)
144+ require.Equal(t, 1, conn.sentCount())
145+}
146+
147+func TestKillResultFailedDefiniteIsNotUnknown(t *testing.T) {
148+ tests := []struct {
149+ name string
150+ text string
151+ }{
152+ {name: "no such session", text: "no such session: work"},
153+ {name: "participants changed", text: "session changed during the operation; try again"},
154+ }
155+ for _, tt := range tests {
156+ t.Run(tt.name, func(t *testing.T) {
157+ conn := newFakeKillConnection()
158+ conn.replyForKill = matchingResult(protocol.KillFailed, protocol.ErrInternal, tt.text)
159+
160+ err := sendAndAwait(context.Background(), conn, protocol.Kill{Name: "work", Scope: protocol.KillSession})
161+ require.Error(t, err)
162+ require.ErrorContains(t, err, tt.text)
163+ require.NotErrorIs(t, err, errKillOutcomeUnknown, "a definite failure must not be reported as unknown")
164+ require.Equal(t, 1, conn.sentCount(), "a definite failure must not be replayed")
165+ })
166+ }
167+}
168+
169+func TestKillResultUnknownOutcomeStaysUnknown(t *testing.T) {
170+ conn := newFakeKillConnection()
171+ conn.replyForKill = matchingResult(protocol.KillOutcomeUnknown, protocol.ErrServerShutdown, "daemon is shutting down")
172+
173+ err := sendAndAwait(context.Background(), conn, protocol.Kill{Scope: protocol.KillAll})
174+ require.ErrorIs(t, err, errKillOutcomeUnknown)
175+ require.Equal(t, 1, conn.sentCount(), "an unknown outcome must never be replayed")
176+}
177+
178+func TestKillResultWrongTypeOrIDIsOutcomeUnknown(t *testing.T) {
179+ tests := []struct {
180+ name string
181+ reply protocol.ServerMessage
182+ }{
183+ {name: "wrong type", reply: protocol.Pong{}},
184+ {name: "wrong type error", reply: protocol.ErrorMsg{Code: protocol.ErrInternal, Text: "boom"}},
185+ {name: "wrong id", reply: protocol.KillResult{RequestID: 99, Outcome: protocol.KillSucceeded}},
186+ {name: "zero id", reply: protocol.KillResult{RequestID: 0, Outcome: protocol.KillSucceeded}},
187+ }
188+ for _, tt := range tests {
189+ t.Run(tt.name, func(t *testing.T) {
190+ conn := newFakeKillConnection()
191+ conn.recv = []protocol.ServerMessage{tt.reply}
192+
193+ err := sendAndAwait(context.Background(), conn, protocol.Kill{Scope: protocol.KillSession})
194+ require.ErrorIs(t, err, errKillOutcomeUnknown)
195+ require.Equal(t, 1, conn.sentCount(), "an uncorrelated reply must not trigger a replay")
196+ })
197+ }
198+}
199+
200+func TestKillResultClosedBeforeReplyIsOutcomeUnknown(t *testing.T) {
201+ conn := newFakeKillConnection()
202+ conn.recvErr = io.EOF
203+
204+ err := sendAndAwait(context.Background(), conn, protocol.Kill{Scope: protocol.KillSession})
205+ require.ErrorIs(t, err, errKillOutcomeUnknown)
206+ require.Equal(t, 1, conn.sentCount(), "a close before the reply must never be read as success")
207+}
208+
209+// TestKillResultCancelAfterSendIsOutcomeUnknownWithoutReplay proves a caller
210+// canceled while awaiting the result closes the exact connection, reports an
211+// unknown outcome (never EOF-as-success), and never resends the request.
212+func TestKillResultCancelAfterSendIsOutcomeUnknownWithoutReplay(t *testing.T) {
213+ conn := newFakeKillConnection()
214+ conn.block = true
215+
216+ ctx, cancel := context.WithCancel(context.Background())
217+ defer cancel()
218+
219+ done := make(chan error, 1)
220+ go func() { done <- sendAndAwait(ctx, conn, protocol.Kill{Name: "work", Scope: protocol.KillSession}) }()
221+
222+ // Wait until the receive is actually parked, then cancel. No sleep: the
223+ // fake signals entry deterministically.
224+ <-conn.recvEntered
225+ cancel()
226+
227+ require.ErrorIs(t, <-done, errKillOutcomeUnknown)
228+ require.Equal(t, 1, conn.sentCount(), "a canceled kill must never be replayed")
229+
230+ select {
231+ case <-conn.closed:
232+ default:
233+ t.Fatal("cancellation after send must close the exact connection")
234+ }
235+}
236+
237+// TestKillResultTimeoutAfterSendIsOutcomeUnknownWithoutReplay proves a caller
238+// whose result deadline expires while awaiting the reply closes the exact
239+// connection, reports an unknown outcome, and never resends the request.
240+func TestKillResultTimeoutAfterSendIsOutcomeUnknownWithoutReplay(t *testing.T) {
241+ conn := newFakeKillConnection()
242+ conn.block = true
243+
244+ ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
245+ defer cancel()
246+
247+ done := make(chan error, 1)
248+ go func() { done <- sendAndAwait(ctx, conn, protocol.Kill{Name: "work", Scope: protocol.KillSession}) }()
249+
250+ <-conn.recvEntered
251+ require.ErrorIs(t, <-done, errKillOutcomeUnknown)
252+ require.Equal(t, 1, conn.sentCount(), "a timed-out kill must never be replayed")
253+
254+ select {
255+ case <-conn.closed:
256+ default:
257+ t.Fatal("a result timeout must close the exact connection")
258+ }
259+}
260+
261+// TestKillResultCancelBeforeSendIsDefiniteNotSent proves a caller canceled
262+// before the request is sent reports a definite not-sent outcome and puts no
263+// bytes on the wire.
264+func TestKillResultCancelBeforeSendIsDefiniteNotSent(t *testing.T) {
265+ conn := newFakeKillConnection()
266+
267+ ctx, cancel := context.WithCancel(context.Background())
268+ cancel()
269+
270+ err := sendAndAwait(ctx, conn, protocol.Kill{Name: "work", Scope: protocol.KillSession})
271+ require.ErrorIs(t, err, errKillNotSent)
272+ require.NotErrorIs(t, err, errKillOutcomeUnknown, "a not-sent request has a definite outcome")
273+ require.Zero(t, conn.sentCount(), "a canceled-before-send kill must not be sent")
274+}
275+
276+func TestKillRequestSendFailureIsReported(t *testing.T) {
277+ conn := newFakeKillConnection()
278+ conn.sendErr = errors.New("write failed")
279+
280+ err := sendAndAwait(context.Background(), conn, protocol.Kill{Name: "work", Scope: protocol.KillSession})
281+ require.ErrorContains(t, err, "write failed")
282+ require.Zero(t, conn.sentCount())
283+}
284+
285+// TestKillRequestIDsAreUniqueAndNonzero proves each request allocates a fresh,
286+// nonzero correlation ID so a reply for one request can never match another.
287+func TestKillRequestIDsAreUniqueAndNonzero(t *testing.T) {
288+ seen := make(map[uint64]struct{})
289+ for range 64 {
290+ conn := newFakeKillConnection()
291+ conn.replyForKill = matchingResult(protocol.KillSucceeded, 0, "")
292+ require.NoError(t, sendAndAwait(context.Background(), conn, protocol.Kill{Scope: protocol.KillAll}))
293+ id := conn.sentKill(t).RequestID
294+ require.NotZero(t, id)
295+ _, duplicate := seen[id]
296+ require.False(t, duplicate, "duplicate RequestID %d", id)
297+ seen[id] = struct{}{}
298+ }
299+}
300+
301+// countingLifecycleProbe records every lifecycle-acquire attempt and returns
302+// the configured owner or error, so a test can prove both whether the transfer
303+// wait was entered and how it resolved.
304+type countingLifecycleProbe struct {
305+ attempts atomic.Int64
306+ owner lifecycleOwnership
307+ err error
308+}
309+
310+func (p *countingLifecycleProbe) TryAcquire(string) (lifecycleOwnership, error) {
311+ p.attempts.Add(1)
312+ return p.owner, p.err
313+}
314+
315+// startServingOneKill binds ipc.SocketDir() under a temp runtime root, points
316+// the lifecycle probe at a counting fake, and serves exactly one control
317+// connection in the background. reply==nil closes the connection without
318+// answering, which models a lost reply; otherwise the reply is correlated to the
319+// exact request read from the wire. The returned channel closes once the
320+// scripted server has finished.
321+func startServingOneKill(t *testing.T, probe *countingLifecycleProbe, reply func(protocol.Kill) protocol.ServerMessage) <-chan struct{} {
322+ t.Helper()
323+ t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
324+ t.Setenv("XDG_STATE_HOME", t.TempDir())
325+
326+ // Bound the transfer wait so a regression that wrongly waits fails fast
327+ // instead of burning the production retry budget.
328+ originalBackoff := defaultBackoff
329+ t.Cleanup(func() { defaultBackoff = originalBackoff })
330+ defaultBackoff = backoffConfig{initial: time.Millisecond, max: time.Millisecond, total: 5 * time.Millisecond}
331+
332+ originalProbe := daemonLifecycleProbe
333+ t.Cleanup(func() { daemonLifecycleProbe = originalProbe })
334+ daemonLifecycleProbe = probe
335+
336+ listener, err := ipc.Listen(ipc.SocketDir())
337+ require.NoError(t, err)
338+ t.Cleanup(func() { _ = listener.Close() })
339+
340+ done := make(chan struct{})
341+ go func() {
342+ defer close(done)
343+ raw, err := listener.Accept()
344+ if err != nil {
345+ return
346+ }
347+ defer func() { _ = raw.Close() }()
348+ connection := sessionwire.NewServerConnection(raw)
349+ message, err := connection.ReceiveClient()
350+ if err != nil {
351+ return
352+ }
353+ kill, ok := message.(protocol.Kill)
354+ if !ok || reply == nil {
355+ return
356+ }
357+ _ = connection.SendServer(reply(kill))
358+ }()
359+ return done
360+}
361+
362+func awaitServingOneKill(t *testing.T, done <-chan struct{}) {
363+ t.Helper()
364+ select {
365+ case <-done:
366+ case <-time.After(2 * time.Second):
367+ t.Fatal("the scripted control server did not finish")
368+ }
369+}
370+
371+// TestDaemonStopLostReplyConfirmedByLifecycleTransfer proves a daemon-stop whose
372+// explicit result is lost is still reported as success once the caller directly
373+// acquires lifecycle ownership: the authoritative transfer confirms the accepted
374+// shutdown completed.
375+func TestDaemonStopLostReplyConfirmedByLifecycleTransfer(t *testing.T) {
376+ probe := &countingLifecycleProbe{owner: fakeLifecycleOwnership{release: func() error { return nil }}}
377+ done := startServingOneKill(t, probe, nil)
378+
379+ require.NoError(t, requestDaemonStop(context.Background()))
380+ awaitServingOneKill(t, done)
381+ require.EqualValues(t, 1, probe.attempts.Load(), "a lost reply must be resolved by exactly one ownership probe")
382+}
383+
384+// TestDaemonStopLostReplyAndTransferFailurePreservesUnknownOutcome proves a
385+// daemon-stop whose result is lost AND whose lifecycle confirmation fails keeps
386+// errors.Is(err, errKillOutcomeUnknown) and carries both diagnostics, so the
387+// caller never reads a lost reply as a definite success or a definite failure.
388+func TestDaemonStopLostReplyAndTransferFailurePreservesUnknownOutcome(t *testing.T) {
389+ probe := &countingLifecycleProbe{err: errors.New("lifecycle still busy")}
390+ done := startServingOneKill(t, probe, nil)
391+
392+ err := requestDaemonStop(context.Background())
393+ require.ErrorIs(t, err, errKillOutcomeUnknown)
394+ require.ErrorContains(t, err, "awaiting explicit result", "the lost-reply diagnostic must survive")
395+ require.ErrorContains(t, err, "lifecycle confirmation also failed")
396+ awaitServingOneKill(t, done)
397+}
398+
399+// TestDaemonStopDefiniteFailureReturnsWithoutLifecycleWait proves a definite
400+// KillFailed daemon-stop reply is returned immediately and never enters the
401+// lifecycle-transfer wait, so a rejected stop cannot be retried as if the
402+// outcome were still unknown.
403+func TestDaemonStopDefiniteFailureReturnsWithoutLifecycleWait(t *testing.T) {
404+ probe := &countingLifecycleProbe{err: errors.New("lifecycle must not be probed")}
405+ done := startServingOneKill(t, probe, func(kill protocol.Kill) protocol.ServerMessage {
406+ return protocol.KillResult{RequestID: kill.RequestID, Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: "daemon refused stop"}
407+ })
408+
409+ err := requestDaemonStop(context.Background())
410+ require.Error(t, err)
411+ require.ErrorContains(t, err, "daemon refused stop")
412+ require.NotErrorIs(t, err, errKillOutcomeUnknown, "a definite failure must not be reported as unknown")
413+ require.Zero(t, probe.attempts.Load(), "a definite failure must return without waiting for ownership transfer")
414+ awaitServingOneKill(t, done)
415+}
416+
417+// TestDaemonStopUnknownResultKeepsUnknownWhenOwnershipReleaseFails pins the
418+// ownership-release branch: even when the probe grants ownership, a release
419+// failure keeps the lost-reply diagnostic instead of discarding it.
420+func TestDaemonStopUnknownResultKeepsUnknownWhenOwnershipReleaseFails(t *testing.T) {
421+ probe := &countingLifecycleProbe{owner: fakeLifecycleOwnership{release: func() error { return errors.New("unlock failed") }}}
422+ done := startServingOneKill(t, probe, nil)
423+
424+ err := requestDaemonStop(context.Background())
425+ require.ErrorIs(t, err, errKillOutcomeUnknown)
426+ require.ErrorContains(t, err, "confirmed ownership release failed")
427+ require.ErrorContains(t, err, "unlock failed")
428+ awaitServingOneKill(t, done)
429+}
@@ -0,0 +1,429 @@
1+package app
2+
3+// KillResult caller contract (explicit result slice). runKill and
4+// requestDaemonStop send one Kill with a unique nonzero RequestID and consume
5+// exactly one matching KillResult; a lost, uncorrelated, or wrong-typed reply is
6+// outcome-unknown and is never replayed, while a caller canceled before the
7+// send is a definite not-sent outcome. These tests drive the two helpers those
8+// callers share with a controllable in-memory client connection, so every case
9+// is bounded and uses no sleeps.
10+
11+import (
12+ "context"
13+ "errors"
14+ "io"
15+ "sync"
16+ "sync/atomic"
17+ "testing"
18+ "time"
19+
20+ "github.com/stretchr/testify/require"
21+
22+ "github.com/bnema/vev/internal/adapters/ipc"
23+ "github.com/bnema/vev/internal/adapters/sessionwire"
24+ "github.com/bnema/vev/internal/ports"
25+ "github.com/bnema/vev/internal/protocol"
26+)
27+
28+// fakeKillConnection is a scripted ports.ClientConnection. Sends are recorded
29+// and counted; ReceiveServer serves queued replies in order, then either a
30+// reply derived from the last sent Kill (replyForKill), a terminal error, or a
31+// block until Close.
32+type fakeKillConnection struct {
33+ mu sync.Mutex
34+ sends []protocol.ClientMessage
35+ recv []protocol.ServerMessage
36+ replyForKill func(protocol.Kill) protocol.ServerMessage
37+ sendErr error
38+ recvErr error
39+ // block: once no reply is available, ReceiveServer parks until Close.
40+ block bool
41+ closed chan struct{}
42+ once sync.Once
43+ // recvEntered reports that a blocked ReceiveServer was actually entered.
44+ recvEntered chan struct{}
45+ enterOnce sync.Once
46+}
47+
48+func newFakeKillConnection() *fakeKillConnection {
49+ return &fakeKillConnection{closed: make(chan struct{}), recvEntered: make(chan struct{})}
50+}
51+
52+func (c *fakeKillConnection) SendClient(message protocol.ClientMessage) error {
53+ c.mu.Lock()
54+ defer c.mu.Unlock()
55+ if c.sendErr != nil {
56+ return c.sendErr
57+ }
58+ c.sends = append(c.sends, message)
59+ return nil
60+}
61+
62+func (c *fakeKillConnection) ReceiveServer() (protocol.ServerMessage, error) {
63+ c.mu.Lock()
64+ if len(c.recv) > 0 {
65+ next := c.recv[0]
66+ c.recv = c.recv[1:]
67+ c.mu.Unlock()
68+ return next, nil
69+ }
70+ // replyForKill derives a matching result from the exact request that was
71+ // sent, so a test never has to predict the fresh random RequestID.
72+ if c.replyForKill != nil && len(c.sends) > 0 {
73+ if kill, ok := c.sends[len(c.sends)-1].(protocol.Kill); ok {
74+ reply := c.replyForKill(kill)
75+ c.mu.Unlock()
76+ return reply, nil
77+ }
78+ }
79+ block, recvErr := c.block, c.recvErr
80+ c.mu.Unlock()
81+ if !block {
82+ return nil, recvErr
83+ }
84+ c.enterOnce.Do(func() { close(c.recvEntered) })
85+ <-c.closed
86+ return nil, io.ErrClosedPipe
87+}
88+
89+func (c *fakeKillConnection) Capabilities() protocol.ConnectionCapabilities {
90+ return protocol.ConnectionCapabilities{}
91+}
92+
93+func (*fakeKillConnection) LinkState() ports.LinkState { return ports.LinkState(0) }
94+func (*fakeKillConnection) LinkEvents() <-chan ports.LinkEvent { return nil }
95+
96+func (c *fakeKillConnection) Close() error {
97+ c.once.Do(func() { close(c.closed) })
98+ return nil
99+}
100+
101+func (c *fakeKillConnection) sentCount() int {
102+ c.mu.Lock()
103+ defer c.mu.Unlock()
104+ return len(c.sends)
105+}
106+
107+func (c *fakeKillConnection) sentKill(t *testing.T) protocol.Kill {
108+ t.Helper()
109+ c.mu.Lock()
110+ defer c.mu.Unlock()
111+ require.Len(t, c.sends, 1, "exactly one Kill must be sent (no replay)")
112+ kill, ok := c.sends[0].(protocol.Kill)
113+ require.True(t, ok, "sent message %T is not a Kill", c.sends[0])
114+ return kill
115+}
116+
117+// sendAndAwait drives the shared send/receive pair once, as runKill and
118+// requestDaemonStop do.
119+func sendAndAwait(ctx context.Context, conn *fakeKillConnection, request protocol.Kill) error {
120+ requestID, err := sendKillRequest(ctx, conn, request)
121+ if err != nil {
122+ return err
123+ }
124+ return receiveKillResult(ctx, conn, requestID)
125+}
126+
127+// matchingResult mirrors one Kill's RequestID into a successful result.
128+func matchingResult(outcome protocol.KillOutcome, code uint16, text string) func(protocol.Kill) protocol.ServerMessage {
129+ return func(kill protocol.Kill) protocol.ServerMessage {
130+ return protocol.KillResult{RequestID: kill.RequestID, Outcome: outcome, Code: code, Text: text}
131+ }
132+}
133+
134+func TestKillResultMatchingSuccessConsumesOneCorrelatedResult(t *testing.T) {
135+ conn := newFakeKillConnection()
136+ conn.replyForKill = matchingResult(protocol.KillSucceeded, 0, "")
137+
138+ require.NoError(t, sendAndAwait(context.Background(), conn, protocol.Kill{Name: "work", Scope: protocol.KillSession}))
139+
140+ kill := conn.sentKill(t)
141+ require.NotZero(t, kill.RequestID, "a Kill must carry a nonzero RequestID")
142+ require.Equal(t, "work", kill.Name)
143+ require.Equal(t, protocol.KillSession, kill.Scope)
144+ require.Equal(t, 1, conn.sentCount())
145+}
146+
147+func TestKillResultFailedDefiniteIsNotUnknown(t *testing.T) {
148+ tests := []struct {
149+ name string
150+ text string
151+ }{
152+ {name: "no such session", text: "no such session: work"},
153+ {name: "participants changed", text: "session changed during the operation; try again"},
154+ }
155+ for _, tt := range tests {
156+ t.Run(tt.name, func(t *testing.T) {
157+ conn := newFakeKillConnection()
158+ conn.replyForKill = matchingResult(protocol.KillFailed, protocol.ErrInternal, tt.text)
159+
160+ err := sendAndAwait(context.Background(), conn, protocol.Kill{Name: "work", Scope: protocol.KillSession})
161+ require.Error(t, err)
162+ require.ErrorContains(t, err, tt.text)
163+ require.NotErrorIs(t, err, errKillOutcomeUnknown, "a definite failure must not be reported as unknown")
164+ require.Equal(t, 1, conn.sentCount(), "a definite failure must not be replayed")
165+ })
166+ }
167+}
168+
169+func TestKillResultUnknownOutcomeStaysUnknown(t *testing.T) {
170+ conn := newFakeKillConnection()
171+ conn.replyForKill = matchingResult(protocol.KillOutcomeUnknown, protocol.ErrServerShutdown, "daemon is shutting down")
172+
173+ err := sendAndAwait(context.Background(), conn, protocol.Kill{Scope: protocol.KillAll})
174+ require.ErrorIs(t, err, errKillOutcomeUnknown)
175+ require.Equal(t, 1, conn.sentCount(), "an unknown outcome must never be replayed")
176+}
177+
178+func TestKillResultWrongTypeOrIDIsOutcomeUnknown(t *testing.T) {
179+ tests := []struct {
180+ name string
181+ reply protocol.ServerMessage
182+ }{
183+ {name: "wrong type", reply: protocol.Pong{}},
184+ {name: "wrong type error", reply: protocol.ErrorMsg{Code: protocol.ErrInternal, Text: "boom"}},
185+ {name: "wrong id", reply: protocol.KillResult{RequestID: 99, Outcome: protocol.KillSucceeded}},
186+ {name: "zero id", reply: protocol.KillResult{RequestID: 0, Outcome: protocol.KillSucceeded}},
187+ }
188+ for _, tt := range tests {
189+ t.Run(tt.name, func(t *testing.T) {
190+ conn := newFakeKillConnection()
191+ conn.recv = []protocol.ServerMessage{tt.reply}
192+
193+ err := sendAndAwait(context.Background(), conn, protocol.Kill{Scope: protocol.KillSession})
194+ require.ErrorIs(t, err, errKillOutcomeUnknown)
195+ require.Equal(t, 1, conn.sentCount(), "an uncorrelated reply must not trigger a replay")
196+ })
197+ }
198+}
199+
200+func TestKillResultClosedBeforeReplyIsOutcomeUnknown(t *testing.T) {
201+ conn := newFakeKillConnection()
202+ conn.recvErr = io.EOF
203+
204+ err := sendAndAwait(context.Background(), conn, protocol.Kill{Scope: protocol.KillSession})
205+ require.ErrorIs(t, err, errKillOutcomeUnknown)
206+ require.Equal(t, 1, conn.sentCount(), "a close before the reply must never be read as success")
207+}
208+
209+// TestKillResultCancelAfterSendIsOutcomeUnknownWithoutReplay proves a caller
210+// canceled while awaiting the result closes the exact connection, reports an
211+// unknown outcome (never EOF-as-success), and never resends the request.
212+func TestKillResultCancelAfterSendIsOutcomeUnknownWithoutReplay(t *testing.T) {
213+ conn := newFakeKillConnection()
214+ conn.block = true
215+
216+ ctx, cancel := context.WithCancel(context.Background())
217+ defer cancel()
218+
219+ done := make(chan error, 1)
220+ go func() { done <- sendAndAwait(ctx, conn, protocol.Kill{Name: "work", Scope: protocol.KillSession}) }()
221+
222+ // Wait until the receive is actually parked, then cancel. No sleep: the
223+ // fake signals entry deterministically.
224+ <-conn.recvEntered
225+ cancel()
226+
227+ require.ErrorIs(t, <-done, errKillOutcomeUnknown)
228+ require.Equal(t, 1, conn.sentCount(), "a canceled kill must never be replayed")
229+
230+ select {
231+ case <-conn.closed:
232+ default:
233+ t.Fatal("cancellation after send must close the exact connection")
234+ }
235+}
236+
237+// TestKillResultTimeoutAfterSendIsOutcomeUnknownWithoutReplay proves a caller
238+// whose result deadline expires while awaiting the reply closes the exact
239+// connection, reports an unknown outcome, and never resends the request.
240+func TestKillResultTimeoutAfterSendIsOutcomeUnknownWithoutReplay(t *testing.T) {
241+ conn := newFakeKillConnection()
242+ conn.block = true
243+
244+ ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
245+ defer cancel()
246+
247+ done := make(chan error, 1)
248+ go func() { done <- sendAndAwait(ctx, conn, protocol.Kill{Name: "work", Scope: protocol.KillSession}) }()
249+
250+ <-conn.recvEntered
251+ require.ErrorIs(t, <-done, errKillOutcomeUnknown)
252+ require.Equal(t, 1, conn.sentCount(), "a timed-out kill must never be replayed")
253+
254+ select {
255+ case <-conn.closed:
256+ default:
257+ t.Fatal("a result timeout must close the exact connection")
258+ }
259+}
260+
261+// TestKillResultCancelBeforeSendIsDefiniteNotSent proves a caller canceled
262+// before the request is sent reports a definite not-sent outcome and puts no
263+// bytes on the wire.
264+func TestKillResultCancelBeforeSendIsDefiniteNotSent(t *testing.T) {
265+ conn := newFakeKillConnection()
266+
267+ ctx, cancel := context.WithCancel(context.Background())
268+ cancel()
269+
270+ err := sendAndAwait(ctx, conn, protocol.Kill{Name: "work", Scope: protocol.KillSession})
271+ require.ErrorIs(t, err, errKillNotSent)
272+ require.NotErrorIs(t, err, errKillOutcomeUnknown, "a not-sent request has a definite outcome")
273+ require.Zero(t, conn.sentCount(), "a canceled-before-send kill must not be sent")
274+}
275+
276+func TestKillRequestSendFailureIsReported(t *testing.T) {
277+ conn := newFakeKillConnection()
278+ conn.sendErr = errors.New("write failed")
279+
280+ err := sendAndAwait(context.Background(), conn, protocol.Kill{Name: "work", Scope: protocol.KillSession})
281+ require.ErrorContains(t, err, "write failed")
282+ require.Zero(t, conn.sentCount())
283+}
284+
285+// TestKillRequestIDsAreUniqueAndNonzero proves each request allocates a fresh,
286+// nonzero correlation ID so a reply for one request can never match another.
287+func TestKillRequestIDsAreUniqueAndNonzero(t *testing.T) {
288+ seen := make(map[uint64]struct{})
289+ for range 64 {
290+ conn := newFakeKillConnection()
291+ conn.replyForKill = matchingResult(protocol.KillSucceeded, 0, "")
292+ require.NoError(t, sendAndAwait(context.Background(), conn, protocol.Kill{Scope: protocol.KillAll}))
293+ id := conn.sentKill(t).RequestID
294+ require.NotZero(t, id)
295+ _, duplicate := seen[id]
296+ require.False(t, duplicate, "duplicate RequestID %d", id)
297+ seen[id] = struct{}{}
298+ }
299+}
300+
301+// countingLifecycleProbe records every lifecycle-acquire attempt and returns
302+// the configured owner or error, so a test can prove both whether the transfer
303+// wait was entered and how it resolved.
304+type countingLifecycleProbe struct {
305+ attempts atomic.Int64
306+ owner lifecycleOwnership
307+ err error
308+}
309+
310+func (p *countingLifecycleProbe) TryAcquire(string) (lifecycleOwnership, error) {
311+ p.attempts.Add(1)
312+ return p.owner, p.err
313+}
314+
315+// startServingOneKill binds ipc.SocketDir() under a temp runtime root, points
316+// the lifecycle probe at a counting fake, and serves exactly one control
317+// connection in the background. reply==nil closes the connection without
318+// answering, which models a lost reply; otherwise the reply is correlated to the
319+// exact request read from the wire. The returned channel closes once the
320+// scripted server has finished.
321+func startServingOneKill(t *testing.T, probe *countingLifecycleProbe, reply func(protocol.Kill) protocol.ServerMessage) <-chan struct{} {
322+ t.Helper()
323+ t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
324+ t.Setenv("XDG_STATE_HOME", t.TempDir())
325+
326+ // Bound the transfer wait so a regression that wrongly waits fails fast
327+ // instead of burning the production retry budget.
328+ originalBackoff := defaultBackoff
329+ t.Cleanup(func() { defaultBackoff = originalBackoff })
330+ defaultBackoff = backoffConfig{initial: time.Millisecond, max: time.Millisecond, total: 5 * time.Millisecond}
331+
332+ originalProbe := daemonLifecycleProbe
333+ t.Cleanup(func() { daemonLifecycleProbe = originalProbe })
334+ daemonLifecycleProbe = probe
335+
336+ listener, err := ipc.Listen(ipc.SocketDir())
337+ require.NoError(t, err)
338+ t.Cleanup(func() { _ = listener.Close() })
339+
340+ done := make(chan struct{})
341+ go func() {
342+ defer close(done)
343+ raw, err := listener.Accept()
344+ if err != nil {
345+ return
346+ }
347+ defer func() { _ = raw.Close() }()
348+ connection := sessionwire.NewServerConnection(raw)
349+ message, err := connection.ReceiveClient()
350+ if err != nil {
351+ return
352+ }
353+ kill, ok := message.(protocol.Kill)
354+ if !ok || reply == nil {
355+ return
356+ }
357+ _ = connection.SendServer(reply(kill))
358+ }()
359+ return done
360+}
361+
362+func awaitServingOneKill(t *testing.T, done <-chan struct{}) {
363+ t.Helper()
364+ select {
365+ case <-done:
366+ case <-time.After(2 * time.Second):
367+ t.Fatal("the scripted control server did not finish")
368+ }
369+}
370+
371+// TestDaemonStopLostReplyConfirmedByLifecycleTransfer proves a daemon-stop whose
372+// explicit result is lost is still reported as success once the caller directly
373+// acquires lifecycle ownership: the authoritative transfer confirms the accepted
374+// shutdown completed.
375+func TestDaemonStopLostReplyConfirmedByLifecycleTransfer(t *testing.T) {
376+ probe := &countingLifecycleProbe{owner: fakeLifecycleOwnership{release: func() error { return nil }}}
377+ done := startServingOneKill(t, probe, nil)
378+
379+ require.NoError(t, requestDaemonStop(context.Background()))
380+ awaitServingOneKill(t, done)
381+ require.EqualValues(t, 1, probe.attempts.Load(), "a lost reply must be resolved by exactly one ownership probe")
382+}
383+
384+// TestDaemonStopLostReplyAndTransferFailurePreservesUnknownOutcome proves a
385+// daemon-stop whose result is lost AND whose lifecycle confirmation fails keeps
386+// errors.Is(err, errKillOutcomeUnknown) and carries both diagnostics, so the
387+// caller never reads a lost reply as a definite success or a definite failure.
388+func TestDaemonStopLostReplyAndTransferFailurePreservesUnknownOutcome(t *testing.T) {
389+ probe := &countingLifecycleProbe{err: errors.New("lifecycle still busy")}
390+ done := startServingOneKill(t, probe, nil)
391+
392+ err := requestDaemonStop(context.Background())
393+ require.ErrorIs(t, err, errKillOutcomeUnknown)
394+ require.ErrorContains(t, err, "awaiting explicit result", "the lost-reply diagnostic must survive")
395+ require.ErrorContains(t, err, "lifecycle confirmation also failed")
396+ awaitServingOneKill(t, done)
397+}
398+
399+// TestDaemonStopDefiniteFailureReturnsWithoutLifecycleWait proves a definite
400+// KillFailed daemon-stop reply is returned immediately and never enters the
401+// lifecycle-transfer wait, so a rejected stop cannot be retried as if the
402+// outcome were still unknown.
403+func TestDaemonStopDefiniteFailureReturnsWithoutLifecycleWait(t *testing.T) {
404+ probe := &countingLifecycleProbe{err: errors.New("lifecycle must not be probed")}
405+ done := startServingOneKill(t, probe, func(kill protocol.Kill) protocol.ServerMessage {
406+ return protocol.KillResult{RequestID: kill.RequestID, Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: "daemon refused stop"}
407+ })
408+
409+ err := requestDaemonStop(context.Background())
410+ require.Error(t, err)
411+ require.ErrorContains(t, err, "daemon refused stop")
412+ require.NotErrorIs(t, err, errKillOutcomeUnknown, "a definite failure must not be reported as unknown")
413+ require.Zero(t, probe.attempts.Load(), "a definite failure must return without waiting for ownership transfer")
414+ awaitServingOneKill(t, done)
415+}
416+
417+// TestDaemonStopUnknownResultKeepsUnknownWhenOwnershipReleaseFails pins the
418+// ownership-release branch: even when the probe grants ownership, a release
419+// failure keeps the lost-reply diagnostic instead of discarding it.
420+func TestDaemonStopUnknownResultKeepsUnknownWhenOwnershipReleaseFails(t *testing.T) {
421+ probe := &countingLifecycleProbe{owner: fakeLifecycleOwnership{release: func() error { return errors.New("unlock failed") }}}
422+ done := startServingOneKill(t, probe, nil)
423+
424+ err := requestDaemonStop(context.Background())
425+ require.ErrorIs(t, err, errKillOutcomeUnknown)
426+ require.ErrorContains(t, err, "confirmed ownership release failed")
427+ require.ErrorContains(t, err, "unlock failed")
428+ awaitServingOneKill(t, done)
429+}
M internal/app/run.go +97 -27
@@ -1178,7 +1178,81 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
11781178
11791179 const daemonStopTimeout = 2 * time.Second
11801180
1181-var errDaemonNotRunning = errors.New("vev: no daemon running")
1181+var (
1182+ errDaemonNotRunning = errors.New("vev: no daemon running")
1183+ errKillOutcomeUnknown = errors.New("vev: kill outcome unknown")
1184+ // errKillNotSent reports a Kill that was never placed on the wire because
1185+ // its caller was already canceled. The outcome is definite: the daemon
1186+ // cannot have acted, so it must not be reported as an unknown outcome.
1187+ errKillNotSent = errors.New("vev: kill request not sent")
1188+)
1189+
1190+func newControlRequestID() (uint64, error) {
1191+ var raw [8]byte
1192+ if _, err := rand.Read(raw[:]); err != nil {
1193+ return 0, err
1194+ }
1195+ id := uint64(0)
1196+ for _, b := range raw {
1197+ id = id<<8 | uint64(b)
1198+ }
1199+ if id == 0 {
1200+ id = 1
1201+ }
1202+ return id, nil
1203+}
1204+
1205+// sendKillRequest allocates a unique nonzero RequestID and sends one Kill
1206+// request, returning the ID its matching KillResult must carry. A caller
1207+// already canceled before the send returns errKillNotSent (a definite
1208+// not-sent outcome) instead of sending a request whose reply could no longer be
1209+// awaited; the request is never replayed.
1210+func sendKillRequest(ctx context.Context, connection ports.ClientConnection, request protocol.Kill) (uint64, error) {
1211+ requestID, err := newControlRequestID()
1212+ if err != nil {
1213+ return 0, fmt.Errorf("preparing request: %w", err)
1214+ }
1215+ request.RequestID = requestID
1216+ if err := ctx.Err(); err != nil {
1217+ return 0, fmt.Errorf("%w: %w", errKillNotSent, err)
1218+ }
1219+ if err := connection.SendClient(request); err != nil {
1220+ return 0, err
1221+ }
1222+ return requestID, nil
1223+}
1224+
1225+// receiveKillResult consumes exactly one correlated KillResult from a control
1226+// connection. A definite failed result becomes a plain error; a close,
1227+// cancellation, or an uncorrelated reply becomes errKillOutcomeUnknown, so a
1228+// caller never treats a lost result as success and never replays the kill.
1229+func receiveKillResult(ctx context.Context, connection ports.ClientConnection, requestID uint64) error {
1230+ // Cancellation after the request was sent closes the exact connection so a
1231+ // blocked receive unwinds; the lost reply stays outcome-unknown rather than
1232+ // becoming a silent success or a blind retry.
1233+ stop := context.AfterFunc(ctx, func() { _ = connection.Close() })
1234+ defer stop()
1235+
1236+ reply, err := connection.ReceiveServer()
1237+ if err != nil {
1238+ if ctxErr := ctx.Err(); ctxErr != nil {
1239+ return fmt.Errorf("%w: awaiting explicit result: %v", errKillOutcomeUnknown, ctxErr)
1240+ }
1241+ return fmt.Errorf("%w: awaiting explicit result: %v", errKillOutcomeUnknown, err)
1242+ }
1243+ result, ok := reply.(protocol.KillResult)
1244+ if !ok || result.RequestID != requestID {
1245+ return fmt.Errorf("%w: unexpected reply %T", errKillOutcomeUnknown, reply)
1246+ }
1247+ switch result.Outcome {
1248+ case protocol.KillSucceeded:
1249+ return nil
1250+ case protocol.KillFailed:
1251+ return fmt.Errorf("vev: %s", result.Text)
1252+ default:
1253+ return fmt.Errorf("%w: %s", errKillOutcomeUnknown, result.Text)
1254+ }
1255+}
11821256
11831257 type localDaemonDialer struct {
11841258 dir string
@@ -1305,18 +1379,31 @@ func requestDaemonStop(ctx context.Context) error {
13051379 }
13061380 defer func() { _ = transport.Close() }()
13071381 connection := sessionwire.NewClientConnection(transport)
1308- if err := connection.SendClient(protocol.Kill{Scope: protocol.KillDaemon}); err != nil {
1382+ requestID, err := sendKillRequest(ctx, connection, protocol.Kill{Scope: protocol.KillDaemon})
1383+ if err != nil {
13091384 return fmt.Errorf("vev: requesting daemon stop: %w", err)
13101385 }
1311- if _, err := connection.ReceiveServer(); err != nil && !errors.Is(err, io.EOF) {
1312- return fmt.Errorf("vev: reading daemon stop reply: %w", err)
1386+ resultErr := receiveKillResult(ctx, connection, requestID)
1387+ if resultErr != nil && !errors.Is(resultErr, errKillOutcomeUnknown) {
1388+ return fmt.Errorf("vev: reading daemon stop reply: %w", resultErr)
13131389 }
13141390
13151391 owner, err = waitForLifecycleAvailability(ctx, ipc.SocketDir(), defaultBackoff)
13161392 if err != nil {
1393+ if resultErr != nil {
1394+ return fmt.Errorf("vev: reading daemon stop reply: %w (lifecycle confirmation also failed: %v)", resultErr, err)
1395+ }
13171396 return fmt.Errorf("vev: waiting for daemon ownership transfer: %w", err)
13181397 }
1319- return owner.Release()
1398+ if err := owner.Release(); err != nil {
1399+ if resultErr != nil {
1400+ return fmt.Errorf("vev: reading daemon stop reply: %w (confirmed ownership release failed: %v)", resultErr, err)
1401+ }
1402+ return err
1403+ }
1404+ // When the explicit reply was lost, acquiring daemon lifecycle ownership is
1405+ // direct authoritative confirmation that the accepted shutdown completed.
1406+ return nil
13201407 }
13211408
13221409 // runStdio is the hidden remote-side mode used by `ssh host vev _stdio`: it
@@ -1753,29 +1840,12 @@ func runKill(ctx context.Context, name string, all, daemon bool) (retErr error)
17531840 } else if daemon {
17541841 scope = protocol.KillDaemon
17551842 }
1756- if err := connection.SendClient(protocol.Kill{Name: name, Scope: scope}); err != nil {
1757- cause := fmt.Errorf("vev: requesting kill: %w", err)
1758- if daemon {
1759- return forceStopDaemonFallback(ctx, cause)
1760- }
1761- return cause
1843+ requestID, err := sendKillRequest(ctx, connection, protocol.Kill{Name: name, Scope: scope})
1844+ if err != nil {
1845+ return fmt.Errorf("vev: requesting kill: %w", err)
17621846 }
1763- reply, err := connection.ReceiveServer()
1764- if err != nil && !errors.Is(err, io.EOF) {
1765- cause := fmt.Errorf("vev: reading kill reply: %w", err)
1766- if daemon {
1767- return forceStopDaemonFallback(ctx, cause)
1768- }
1769- return cause
1770- }
1771- if err == nil {
1772- if em, ok := reply.(protocol.ErrorMsg); ok {
1773- cause := fmt.Errorf("vev: %s", em.Text)
1774- if daemon {
1775- return forceStopDaemonFallback(ctx, cause)
1776- }
1777- return cause
1778- }
1847+ if err := receiveKillResult(ctx, connection, requestID); err != nil {
1848+ return fmt.Errorf("vev: reading kill reply: %w", err)
17791849 }
17801850 // Only an explicit daemon stop ends the daemon; kill-all purges sessions and
17811851 // leaves it running, so it must not wait for an ownership transfer.
@@ -1178,7 +1178,81 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
1178 1178
1179 const daemonStopTimeout = 2 * time.Second1179 const daemonStopTimeout = 2 * time.Second
1180 1180
1181-var errDaemonNotRunning = errors.New("vev: no daemon running")1181+var (
1182+ errDaemonNotRunning = errors.New("vev: no daemon running")
1183+ errKillOutcomeUnknown = errors.New("vev: kill outcome unknown")
1184+ // errKillNotSent reports a Kill that was never placed on the wire because
1185+ // its caller was already canceled. The outcome is definite: the daemon
1186+ // cannot have acted, so it must not be reported as an unknown outcome.
1187+ errKillNotSent = errors.New("vev: kill request not sent")
1188+)
1189+
1190+func newControlRequestID() (uint64, error) {
1191+ var raw [8]byte
1192+ if _, err := rand.Read(raw[:]); err != nil {
1193+ return 0, err
1194+ }
1195+ id := uint64(0)
1196+ for _, b := range raw {
1197+ id = id<<8 | uint64(b)
1198+ }
1199+ if id == 0 {
1200+ id = 1
1201+ }
1202+ return id, nil
1203+}
1204+
1205+// sendKillRequest allocates a unique nonzero RequestID and sends one Kill
1206+// request, returning the ID its matching KillResult must carry. A caller
1207+// already canceled before the send returns errKillNotSent (a definite
1208+// not-sent outcome) instead of sending a request whose reply could no longer be
1209+// awaited; the request is never replayed.
1210+func sendKillRequest(ctx context.Context, connection ports.ClientConnection, request protocol.Kill) (uint64, error) {
1211+ requestID, err := newControlRequestID()
1212+ if err != nil {
1213+ return 0, fmt.Errorf("preparing request: %w", err)
1214+ }
1215+ request.RequestID = requestID
1216+ if err := ctx.Err(); err != nil {
1217+ return 0, fmt.Errorf("%w: %w", errKillNotSent, err)
1218+ }
1219+ if err := connection.SendClient(request); err != nil {
1220+ return 0, err
1221+ }
1222+ return requestID, nil
1223+}
1224+
1225+// receiveKillResult consumes exactly one correlated KillResult from a control
1226+// connection. A definite failed result becomes a plain error; a close,
1227+// cancellation, or an uncorrelated reply becomes errKillOutcomeUnknown, so a
1228+// caller never treats a lost result as success and never replays the kill.
1229+func receiveKillResult(ctx context.Context, connection ports.ClientConnection, requestID uint64) error {
1230+ // Cancellation after the request was sent closes the exact connection so a
1231+ // blocked receive unwinds; the lost reply stays outcome-unknown rather than
1232+ // becoming a silent success or a blind retry.
1233+ stop := context.AfterFunc(ctx, func() { _ = connection.Close() })
1234+ defer stop()
1235+
1236+ reply, err := connection.ReceiveServer()
1237+ if err != nil {
1238+ if ctxErr := ctx.Err(); ctxErr != nil {
1239+ return fmt.Errorf("%w: awaiting explicit result: %v", errKillOutcomeUnknown, ctxErr)
1240+ }
1241+ return fmt.Errorf("%w: awaiting explicit result: %v", errKillOutcomeUnknown, err)
1242+ }
1243+ result, ok := reply.(protocol.KillResult)
1244+ if !ok || result.RequestID != requestID {
1245+ return fmt.Errorf("%w: unexpected reply %T", errKillOutcomeUnknown, reply)
1246+ }
1247+ switch result.Outcome {
1248+ case protocol.KillSucceeded:
1249+ return nil
1250+ case protocol.KillFailed:
1251+ return fmt.Errorf("vev: %s", result.Text)
1252+ default:
1253+ return fmt.Errorf("%w: %s", errKillOutcomeUnknown, result.Text)
1254+ }
1255+}
1182 1256
1183 type localDaemonDialer struct {1257 type localDaemonDialer struct {
1184 dir string1258 dir string
@@ -1305,18 +1379,31 @@ func requestDaemonStop(ctx context.Context) error {
1305 }1379 }
1306 defer func() { _ = transport.Close() }()1380 defer func() { _ = transport.Close() }()
1307 connection := sessionwire.NewClientConnection(transport)1381 connection := sessionwire.NewClientConnection(transport)
1308- if err := connection.SendClient(protocol.Kill{Scope: protocol.KillDaemon}); err != nil {1382+ requestID, err := sendKillRequest(ctx, connection, protocol.Kill{Scope: protocol.KillDaemon})
1383+ if err != nil {
1309 return fmt.Errorf("vev: requesting daemon stop: %w", err)1384 return fmt.Errorf("vev: requesting daemon stop: %w", err)
1310 }1385 }
1311- if _, err := connection.ReceiveServer(); err != nil && !errors.Is(err, io.EOF) {1386+ resultErr := receiveKillResult(ctx, connection, requestID)
1312- return fmt.Errorf("vev: reading daemon stop reply: %w", err)1387+ if resultErr != nil && !errors.Is(resultErr, errKillOutcomeUnknown) {
1388+ return fmt.Errorf("vev: reading daemon stop reply: %w", resultErr)
1313 }1389 }
1314 1390
1315 owner, err = waitForLifecycleAvailability(ctx, ipc.SocketDir(), defaultBackoff)1391 owner, err = waitForLifecycleAvailability(ctx, ipc.SocketDir(), defaultBackoff)
1316 if err != nil {1392 if err != nil {
1393+ if resultErr != nil {
1394+ return fmt.Errorf("vev: reading daemon stop reply: %w (lifecycle confirmation also failed: %v)", resultErr, err)
1395+ }
1317 return fmt.Errorf("vev: waiting for daemon ownership transfer: %w", err)1396 return fmt.Errorf("vev: waiting for daemon ownership transfer: %w", err)
1318 }1397 }
1319- return owner.Release()1398+ if err := owner.Release(); err != nil {
1399+ if resultErr != nil {
1400+ return fmt.Errorf("vev: reading daemon stop reply: %w (confirmed ownership release failed: %v)", resultErr, err)
1401+ }
1402+ return err
1403+ }
1404+ // When the explicit reply was lost, acquiring daemon lifecycle ownership is
1405+ // direct authoritative confirmation that the accepted shutdown completed.
1406+ return nil
1320 }1407 }
1321 1408
1322 // runStdio is the hidden remote-side mode used by `ssh host vev _stdio`: it1409 // runStdio is the hidden remote-side mode used by `ssh host vev _stdio`: it
@@ -1753,29 +1840,12 @@ func runKill(ctx context.Context, name string, all, daemon bool) (retErr error)
1753 } else if daemon {1840 } else if daemon {
1754 scope = protocol.KillDaemon1841 scope = protocol.KillDaemon
1755 }1842 }
1756- if err := connection.SendClient(protocol.Kill{Name: name, Scope: scope}); err != nil {1843+ requestID, err := sendKillRequest(ctx, connection, protocol.Kill{Name: name, Scope: scope})
1757- cause := fmt.Errorf("vev: requesting kill: %w", err)1844+ if err != nil {
1758- if daemon {1845+ return fmt.Errorf("vev: requesting kill: %w", err)
1759- return forceStopDaemonFallback(ctx, cause)
1760- }
1761- return cause
1762 }1846 }
1763- reply, err := connection.ReceiveServer()1847+ if err := receiveKillResult(ctx, connection, requestID); err != nil {
1764- if err != nil && !errors.Is(err, io.EOF) {1848+ return fmt.Errorf("vev: reading kill reply: %w", err)
1765- cause := fmt.Errorf("vev: reading kill reply: %w", err)
1766- if daemon {
1767- return forceStopDaemonFallback(ctx, cause)
1768- }
1769- return cause
1770- }
1771- if err == nil {
1772- if em, ok := reply.(protocol.ErrorMsg); ok {
1773- cause := fmt.Errorf("vev: %s", em.Text)
1774- if daemon {
1775- return forceStopDaemonFallback(ctx, cause)
1776- }
1777- return cause
1778- }
1779 }1849 }
1780 // Only an explicit daemon stop ends the daemon; kill-all purges sessions and1850 // Only an explicit daemon stop ends the daemon; kill-all purges sessions and
1781 // leaves it running, so it must not wait for an ownership transfer.1851 // leaves it running, so it must not wait for an ownership transfer.
M internal/protocol/messages.go +1 -0
@@ -42,6 +42,7 @@ func (Detached) serverMessage() {}
4242 func (Pong) serverMessage() {}
4343 func (Sessions) serverMessage() {}
4444 func (CommandResult) serverMessage() {}
45+func (KillResult) serverMessage() {}
4546 func (AttachTarget) serverMessage() {}
4647 func (RemotePreview) serverMessage() {}
4748 func (CommittedRouteIdentity) serverMessage() {}
@@ -42,6 +42,7 @@ func (Detached) serverMessage() {}
42 func (Pong) serverMessage() {}42 func (Pong) serverMessage() {}
43 func (Sessions) serverMessage() {}43 func (Sessions) serverMessage() {}
44 func (CommandResult) serverMessage() {}44 func (CommandResult) serverMessage() {}
45+func (KillResult) serverMessage() {}
45 func (AttachTarget) serverMessage() {}46 func (AttachTarget) serverMessage() {}
46 func (RemotePreview) serverMessage() {}47 func (RemotePreview) serverMessage() {}
47 func (CommittedRouteIdentity) serverMessage() {}48 func (CommittedRouteIdentity) serverMessage() {}
M internal/protocol/session.go +25 -2
@@ -16,7 +16,7 @@ import (
1616 // daemonmux physical preamble conversation (multiplex.proto), whose
1717 // negotiated ceilings and accepted daemon binding ride the same wire
1818 // version as the session and broker conversations.
19-const Version uint16 = 56
19+const Version uint16 = 57
2020
2121 // HandshakeTimeout bounds every transport handshake from connect through the
2222 // first committed publication. It excludes the preceding client-local
@@ -295,8 +295,31 @@ const (
295295 )
296296
297297 type Kill struct {
298+ RequestID uint64
299+ Name string
300+ Scope KillScope
301+}
302+
303+type KillOutcome uint8
304+
305+const (
306+ KillSucceeded KillOutcome = iota + 1
307+ KillFailed
308+ KillOutcomeUnknown
309+)
310+
311+type KillFailure struct {
312+ Class string
298313 Name string
299- Scope KillScope
314+ Text string
315+}
316+
317+type KillResult struct {
318+ RequestID uint64
319+ Outcome KillOutcome
320+ Code uint16
321+ Text string
322+ Failures []KillFailure
300323 }
301324
302325 type SessionState uint8
@@ -16,7 +16,7 @@ import (
16 // daemonmux physical preamble conversation (multiplex.proto), whose16 // daemonmux physical preamble conversation (multiplex.proto), whose
17 // negotiated ceilings and accepted daemon binding ride the same wire17 // negotiated ceilings and accepted daemon binding ride the same wire
18 // version as the session and broker conversations.18 // version as the session and broker conversations.
19-const Version uint16 = 5619+const Version uint16 = 57
20 20
21 // HandshakeTimeout bounds every transport handshake from connect through the21 // HandshakeTimeout bounds every transport handshake from connect through the
22 // first committed publication. It excludes the preceding client-local22 // first committed publication. It excludes the preceding client-local
@@ -295,8 +295,31 @@ const (
295 )295 )
296 296
297 type Kill struct {297 type Kill struct {
298+ RequestID uint64
299+ Name string
300+ Scope KillScope
301+}
302+
303+type KillOutcome uint8
304+
305+const (
306+ KillSucceeded KillOutcome = iota + 1
307+ KillFailed
308+ KillOutcomeUnknown
309+)
310+
311+type KillFailure struct {
312+ Class string
298 Name string313 Name string
299- Scope KillScope314+ Text string
315+}
316+
317+type KillResult struct {
318+ RequestID uint64
319+ Outcome KillOutcome
320+ Code uint16
321+ Text string
322+ Failures []KillFailure
300 }323 }
301 324
302 type SessionState uint8325 type SessionState uint8
M internal/protocol/session_test.go +1 -1
@@ -10,7 +10,7 @@ import (
1010 )
1111
1212 func TestVersionRemainsWireCompatible(t *testing.T) {
13- require.Equal(t, uint16(56), Version)
13+ require.Equal(t, uint16(57), Version)
1414 require.Equal(t, (16<<20)-407, MaxOutputDataLen)
1515 }
1616
@@ -10,7 +10,7 @@ import (
10 )10 )
11 11
12 func TestVersionRemainsWireCompatible(t *testing.T) {12 func TestVersionRemainsWireCompatible(t *testing.T) {
13- require.Equal(t, uint16(56), Version)13+ require.Equal(t, uint16(57), Version)
14 require.Equal(t, (16<<20)-407, MaxOutputDataLen)14 require.Equal(t, (16<<20)-407, MaxOutputDataLen)
15 }15 }
16 16
M internal/protocol/wire/envelope.pb.go +28 -7 generated file: collapsed
M internal/protocol/wire/envelope_inventory_test.go +3 -2
@@ -15,8 +15,8 @@ import (
1515 // shares a payload type except route navigation failure, and the preamble
1616 // constants agree with the schema authority.
1717 func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) {
18- if protocol.Version != 56 {
19- t.Fatalf("protocol.Version = %d, want 56", protocol.Version)
18+ if protocol.Version != 57 {
19+ t.Fatalf("protocol.Version = %d, want 57", protocol.Version)
2020 }
2121 if ProtocolEpoch != 1 {
2222 t.Fatalf("ProtocolEpoch = %d, want 1", ProtocolEpoch)
@@ -54,6 +54,7 @@ func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) {
5454 "navigation_inventory_selection": true, "picker_offer": true,
5555 "picker_snapshot": true, "picker_closed": true, "picker_result": true,
5656 "picker_failure": true, "picker_preview": true, "picker_control_response": true,
57+ "kill_result": true,
5758 }
5859
5960 if len(clientVariants) != len(clientSemantic) {
@@ -15,8 +15,8 @@ import (
15 // shares a payload type except route navigation failure, and the preamble15 // shares a payload type except route navigation failure, and the preamble
16 // constants agree with the schema authority.16 // constants agree with the schema authority.
17 func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) {17 func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) {
18- if protocol.Version != 56 {18+ if protocol.Version != 57 {
19- t.Fatalf("protocol.Version = %d, want 56", protocol.Version)19+ t.Fatalf("protocol.Version = %d, want 57", protocol.Version)
20 }20 }
21 if ProtocolEpoch != 1 {21 if ProtocolEpoch != 1 {
22 t.Fatalf("ProtocolEpoch = %d, want 1", ProtocolEpoch)22 t.Fatalf("ProtocolEpoch = %d, want 1", ProtocolEpoch)
@@ -54,6 +54,7 @@ func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) {
54 "navigation_inventory_selection": true, "picker_offer": true,54 "navigation_inventory_selection": true, "picker_offer": true,
55 "picker_snapshot": true, "picker_closed": true, "picker_result": true,55 "picker_snapshot": true, "picker_closed": true, "picker_result": true,
56 "picker_failure": true, "picker_preview": true, "picker_control_response": true,56 "picker_failure": true, "picker_preview": true, "picker_control_response": true,
57+ "kill_result": true,
57 }58 }
58 59
59 if len(clientVariants) != len(clientSemantic) {60 if len(clientVariants) != len(clientSemantic) {
M internal/protocol/wire/schema/envelope.proto +1 -0
@@ -138,5 +138,6 @@ message ServerEnvelope {
138138 PickerControlResponse picker_control_response = 28;
139139 AttachmentSuspended attachment_suspended = 31;
140140 AttachmentActivated attachment_activated = 32;
141+ KillResult kill_result = 33;
141142 }
142143 }
@@ -138,5 +138,6 @@ message ServerEnvelope {
138 PickerControlResponse picker_control_response = 28;138 PickerControlResponse picker_control_response = 28;
139 AttachmentSuspended attachment_suspended = 31;139 AttachmentSuspended attachment_suspended = 31;
140 AttachmentActivated attachment_activated = 32;140 AttachmentActivated attachment_activated = 32;
141+ KillResult kill_result = 33;
141 }142 }
142 }143 }
M internal/protocol/wire/schema/session.proto +16 -0
@@ -90,6 +90,22 @@ message Kill {
9090 string name = 1;
9191 // Closed scope taxonomy: 0 = session, 1 = daemon, 2 = all.
9292 uint32 scope = 2;
93+ uint64 request_id = 3;
94+}
95+
96+message KillFailure {
97+ string class = 1;
98+ string name = 2;
99+ string text = 3;
100+}
101+
102+message KillResult {
103+ uint64 request_id = 1;
104+ // Closed outcome taxonomy: 1 = succeeded, 2 = failed, 3 = outcome unknown.
105+ uint32 outcome = 2;
106+ uint32 code = 3;
107+ string text = 4;
108+ repeated KillFailure failures = 5;
93109 }
94110
95111 // Detached reports why an attachment ended.
@@ -90,6 +90,22 @@ message Kill {
90 string name = 1;90 string name = 1;
91 // Closed scope taxonomy: 0 = session, 1 = daemon, 2 = all.91 // Closed scope taxonomy: 0 = session, 1 = daemon, 2 = all.
92 uint32 scope = 2;92 uint32 scope = 2;
93+ uint64 request_id = 3;
94+}
95+
96+message KillFailure {
97+ string class = 1;
98+ string name = 2;
99+ string text = 3;
100+}
101+
102+message KillResult {
103+ uint64 request_id = 1;
104+ // Closed outcome taxonomy: 1 = succeeded, 2 = failed, 3 = outcome unknown.
105+ uint32 outcome = 2;
106+ uint32 code = 3;
107+ string text = 4;
108+ repeated KillFailure failures = 5;
93 }109 }
94 110
95 // Detached reports why an attachment ended.111 // Detached reports why an attachment ended.
M internal/protocol/wire/session.pb.go +282 -120
@@ -674,6 +674,7 @@ type Kill struct {
674674 Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
675675 // Closed scope taxonomy: 0 = session, 1 = daemon, 2 = all.
676676 Scope uint32 `protobuf:"varint,2,opt,name=scope,proto3" json:"scope,omitempty"`
677+ RequestId uint64 `protobuf:"varint,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
677678 unknownFields protoimpl.UnknownFields
678679 sizeCache protoimpl.SizeCache
679680 }
@@ -722,6 +723,150 @@ func (x *Kill) GetScope() uint32 {
722723 return 0
723724 }
724725
726+func (x *Kill) GetRequestId() uint64 {
727+ if x != nil {
728+ return x.RequestId
729+ }
730+ return 0
731+}
732+
733+type KillFailure struct {
734+ state protoimpl.MessageState `protogen:"open.v1"`
735+ Class string `protobuf:"bytes,1,opt,name=class,proto3" json:"class,omitempty"`
736+ Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
737+ Text string `protobuf:"bytes,3,opt,name=text,proto3" json:"text,omitempty"`
738+ unknownFields protoimpl.UnknownFields
739+ sizeCache protoimpl.SizeCache
740+}
741+
742+func (x *KillFailure) Reset() {
743+ *x = KillFailure{}
744+ mi := &file_session_proto_msgTypes[8]
745+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
746+ ms.StoreMessageInfo(mi)
747+}
748+
749+func (x *KillFailure) String() string {
750+ return protoimpl.X.MessageStringOf(x)
751+}
752+
753+func (*KillFailure) ProtoMessage() {}
754+
755+func (x *KillFailure) ProtoReflect() protoreflect.Message {
756+ mi := &file_session_proto_msgTypes[8]
757+ if x != nil {
758+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
759+ if ms.LoadMessageInfo() == nil {
760+ ms.StoreMessageInfo(mi)
761+ }
762+ return ms
763+ }
764+ return mi.MessageOf(x)
765+}
766+
767+// Deprecated: Use KillFailure.ProtoReflect.Descriptor instead.
768+func (*KillFailure) Descriptor() ([]byte, []int) {
769+ return file_session_proto_rawDescGZIP(), []int{8}
770+}
771+
772+func (x *KillFailure) GetClass() string {
773+ if x != nil {
774+ return x.Class
775+ }
776+ return ""
777+}
778+
779+func (x *KillFailure) GetName() string {
780+ if x != nil {
781+ return x.Name
782+ }
783+ return ""
784+}
785+
786+func (x *KillFailure) GetText() string {
787+ if x != nil {
788+ return x.Text
789+ }
790+ return ""
791+}
792+
793+type KillResult struct {
794+ state protoimpl.MessageState `protogen:"open.v1"`
795+ RequestId uint64 `protobuf:"varint,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
796+ // Closed outcome taxonomy: 1 = succeeded, 2 = failed, 3 = outcome unknown.
797+ Outcome uint32 `protobuf:"varint,2,opt,name=outcome,proto3" json:"outcome,omitempty"`
798+ Code uint32 `protobuf:"varint,3,opt,name=code,proto3" json:"code,omitempty"`
799+ Text string `protobuf:"bytes,4,opt,name=text,proto3" json:"text,omitempty"`
800+ Failures []*KillFailure `protobuf:"bytes,5,rep,name=failures,proto3" json:"failures,omitempty"`
801+ unknownFields protoimpl.UnknownFields
802+ sizeCache protoimpl.SizeCache
803+}
804+
805+func (x *KillResult) Reset() {
806+ *x = KillResult{}
807+ mi := &file_session_proto_msgTypes[9]
808+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
809+ ms.StoreMessageInfo(mi)
810+}
811+
812+func (x *KillResult) String() string {
813+ return protoimpl.X.MessageStringOf(x)
814+}
815+
816+func (*KillResult) ProtoMessage() {}
817+
818+func (x *KillResult) ProtoReflect() protoreflect.Message {
819+ mi := &file_session_proto_msgTypes[9]
820+ if x != nil {
821+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
822+ if ms.LoadMessageInfo() == nil {
823+ ms.StoreMessageInfo(mi)
824+ }
825+ return ms
826+ }
827+ return mi.MessageOf(x)
828+}
829+
830+// Deprecated: Use KillResult.ProtoReflect.Descriptor instead.
831+func (*KillResult) Descriptor() ([]byte, []int) {
832+ return file_session_proto_rawDescGZIP(), []int{9}
833+}
834+
835+func (x *KillResult) GetRequestId() uint64 {
836+ if x != nil {
837+ return x.RequestId
838+ }
839+ return 0
840+}
841+
842+func (x *KillResult) GetOutcome() uint32 {
843+ if x != nil {
844+ return x.Outcome
845+ }
846+ return 0
847+}
848+
849+func (x *KillResult) GetCode() uint32 {
850+ if x != nil {
851+ return x.Code
852+ }
853+ return 0
854+}
855+
856+func (x *KillResult) GetText() string {
857+ if x != nil {
858+ return x.Text
859+ }
860+ return ""
861+}
862+
863+func (x *KillResult) GetFailures() []*KillFailure {
864+ if x != nil {
865+ return x.Failures
866+ }
867+ return nil
868+}
869+
725870 // Detached reports why an attachment ended.
726871 type Detached struct {
727872 state protoimpl.MessageState `protogen:"open.v1"`
@@ -733,7 +878,7 @@ type Detached struct {
733878
734879 func (x *Detached) Reset() {
735880 *x = Detached{}
736- mi := &file_session_proto_msgTypes[8]
881+ mi := &file_session_proto_msgTypes[10]
737882 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
738883 ms.StoreMessageInfo(mi)
739884 }
@@ -745,7 +890,7 @@ func (x *Detached) String() string {
745890 func (*Detached) ProtoMessage() {}
746891
747892 func (x *Detached) ProtoReflect() protoreflect.Message {
748- mi := &file_session_proto_msgTypes[8]
893+ mi := &file_session_proto_msgTypes[10]
749894 if x != nil {
750895 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
751896 if ms.LoadMessageInfo() == nil {
@@ -758,7 +903,7 @@ func (x *Detached) ProtoReflect() protoreflect.Message {
758903
759904 // Deprecated: Use Detached.ProtoReflect.Descriptor instead.
760905 func (*Detached) Descriptor() ([]byte, []int) {
761- return file_session_proto_rawDescGZIP(), []int{8}
906+ return file_session_proto_rawDescGZIP(), []int{10}
762907 }
763908
764909 func (x *Detached) GetReason() uint32 {
@@ -777,7 +922,7 @@ type Detach struct {
777922
778923 func (x *Detach) Reset() {
779924 *x = Detach{}
780- mi := &file_session_proto_msgTypes[9]
925+ mi := &file_session_proto_msgTypes[11]
781926 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
782927 ms.StoreMessageInfo(mi)
783928 }
@@ -789,7 +934,7 @@ func (x *Detach) String() string {
789934 func (*Detach) ProtoMessage() {}
790935
791936 func (x *Detach) ProtoReflect() protoreflect.Message {
792- mi := &file_session_proto_msgTypes[9]
937+ mi := &file_session_proto_msgTypes[11]
793938 if x != nil {
794939 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
795940 if ms.LoadMessageInfo() == nil {
@@ -802,7 +947,7 @@ func (x *Detach) ProtoReflect() protoreflect.Message {
802947
803948 // Deprecated: Use Detach.ProtoReflect.Descriptor instead.
804949 func (*Detach) Descriptor() ([]byte, []int) {
805- return file_session_proto_rawDescGZIP(), []int{9}
950+ return file_session_proto_rawDescGZIP(), []int{11}
806951 }
807952
808953 // Ping / Pong are empty keepalive markers.
@@ -814,7 +959,7 @@ type Ping struct {
814959
815960 func (x *Ping) Reset() {
816961 *x = Ping{}
817- mi := &file_session_proto_msgTypes[10]
962+ mi := &file_session_proto_msgTypes[12]
818963 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
819964 ms.StoreMessageInfo(mi)
820965 }
@@ -826,7 +971,7 @@ func (x *Ping) String() string {
826971 func (*Ping) ProtoMessage() {}
827972
828973 func (x *Ping) ProtoReflect() protoreflect.Message {
829- mi := &file_session_proto_msgTypes[10]
974+ mi := &file_session_proto_msgTypes[12]
830975 if x != nil {
831976 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
832977 if ms.LoadMessageInfo() == nil {
@@ -839,7 +984,7 @@ func (x *Ping) ProtoReflect() protoreflect.Message {
839984
840985 // Deprecated: Use Ping.ProtoReflect.Descriptor instead.
841986 func (*Ping) Descriptor() ([]byte, []int) {
842- return file_session_proto_rawDescGZIP(), []int{10}
987+ return file_session_proto_rawDescGZIP(), []int{12}
843988 }
844989
845990 type Pong struct {
@@ -850,7 +995,7 @@ type Pong struct {
850995
851996 func (x *Pong) Reset() {
852997 *x = Pong{}
853- mi := &file_session_proto_msgTypes[11]
998+ mi := &file_session_proto_msgTypes[13]
854999 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
8551000 ms.StoreMessageInfo(mi)
8561001 }
@@ -862,7 +1007,7 @@ func (x *Pong) String() string {
8621007 func (*Pong) ProtoMessage() {}
8631008
8641009 func (x *Pong) ProtoReflect() protoreflect.Message {
865- mi := &file_session_proto_msgTypes[11]
1010+ mi := &file_session_proto_msgTypes[13]
8661011 if x != nil {
8671012 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
8681013 if ms.LoadMessageInfo() == nil {
@@ -875,7 +1020,7 @@ func (x *Pong) ProtoReflect() protoreflect.Message {
8751020
8761021 // Deprecated: Use Pong.ProtoReflect.Descriptor instead.
8771022 func (*Pong) Descriptor() ([]byte, []int) {
878- return file_session_proto_rawDescGZIP(), []int{11}
1023+ return file_session_proto_rawDescGZIP(), []int{13}
8791024 }
8801025
8811026 // List asks for the one-shot session listing. Empty marker.
@@ -887,7 +1032,7 @@ type List struct {
8871032
8881033 func (x *List) Reset() {
8891034 *x = List{}
890- mi := &file_session_proto_msgTypes[12]
1035+ mi := &file_session_proto_msgTypes[14]
8911036 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
8921037 ms.StoreMessageInfo(mi)
8931038 }
@@ -899,7 +1044,7 @@ func (x *List) String() string {
8991044 func (*List) ProtoMessage() {}
9001045
9011046 func (x *List) ProtoReflect() protoreflect.Message {
902- mi := &file_session_proto_msgTypes[12]
1047+ mi := &file_session_proto_msgTypes[14]
9031048 if x != nil {
9041049 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
9051050 if ms.LoadMessageInfo() == nil {
@@ -912,7 +1057,7 @@ func (x *List) ProtoReflect() protoreflect.Message {
9121057
9131058 // Deprecated: Use List.ProtoReflect.Descriptor instead.
9141059 func (*List) Descriptor() ([]byte, []int) {
915- return file_session_proto_rawDescGZIP(), []int{12}
1060+ return file_session_proto_rawDescGZIP(), []int{14}
9161061 }
9171062
9181063 // RouteRef is an opaque client-ledger reference.
@@ -926,7 +1071,7 @@ type RouteRef struct {
9261071
9271072 func (x *RouteRef) Reset() {
9281073 *x = RouteRef{}
929- mi := &file_session_proto_msgTypes[13]
1074+ mi := &file_session_proto_msgTypes[15]
9301075 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
9311076 ms.StoreMessageInfo(mi)
9321077 }
@@ -938,7 +1083,7 @@ func (x *RouteRef) String() string {
9381083 func (*RouteRef) ProtoMessage() {}
9391084
9401085 func (x *RouteRef) ProtoReflect() protoreflect.Message {
941- mi := &file_session_proto_msgTypes[13]
1086+ mi := &file_session_proto_msgTypes[15]
9421087 if x != nil {
9431088 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
9441089 if ms.LoadMessageInfo() == nil {
@@ -951,7 +1096,7 @@ func (x *RouteRef) ProtoReflect() protoreflect.Message {
9511096
9521097 // Deprecated: Use RouteRef.ProtoReflect.Descriptor instead.
9531098 func (*RouteRef) Descriptor() ([]byte, []int) {
954- return file_session_proto_rawDescGZIP(), []int{13}
1099+ return file_session_proto_rawDescGZIP(), []int{15}
9551100 }
9561101
9571102 func (x *RouteRef) GetKey() uint64 {
@@ -980,7 +1125,7 @@ type RouteAttentionTarget struct {
9801125
9811126 func (x *RouteAttentionTarget) Reset() {
9821127 *x = RouteAttentionTarget{}
983- mi := &file_session_proto_msgTypes[14]
1128+ mi := &file_session_proto_msgTypes[16]
9841129 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
9851130 ms.StoreMessageInfo(mi)
9861131 }
@@ -992,7 +1137,7 @@ func (x *RouteAttentionTarget) String() string {
9921137 func (*RouteAttentionTarget) ProtoMessage() {}
9931138
9941139 func (x *RouteAttentionTarget) ProtoReflect() protoreflect.Message {
995- mi := &file_session_proto_msgTypes[14]
1140+ mi := &file_session_proto_msgTypes[16]
9961141 if x != nil {
9971142 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
9981143 if ms.LoadMessageInfo() == nil {
@@ -1005,7 +1150,7 @@ func (x *RouteAttentionTarget) ProtoReflect() protoreflect.Message {
10051150
10061151 // Deprecated: Use RouteAttentionTarget.ProtoReflect.Descriptor instead.
10071152 func (*RouteAttentionTarget) Descriptor() ([]byte, []int) {
1008- return file_session_proto_rawDescGZIP(), []int{14}
1153+ return file_session_proto_rawDescGZIP(), []int{16}
10091154 }
10101155
10111156 func (x *RouteAttentionTarget) GetRef() *RouteRef {
@@ -1039,7 +1184,7 @@ type RouteAttentionSubscription struct {
10391184
10401185 func (x *RouteAttentionSubscription) Reset() {
10411186 *x = RouteAttentionSubscription{}
1042- mi := &file_session_proto_msgTypes[15]
1187+ mi := &file_session_proto_msgTypes[17]
10431188 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
10441189 ms.StoreMessageInfo(mi)
10451190 }
@@ -1051,7 +1196,7 @@ func (x *RouteAttentionSubscription) String() string {
10511196 func (*RouteAttentionSubscription) ProtoMessage() {}
10521197
10531198 func (x *RouteAttentionSubscription) ProtoReflect() protoreflect.Message {
1054- mi := &file_session_proto_msgTypes[15]
1199+ mi := &file_session_proto_msgTypes[17]
10551200 if x != nil {
10561201 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
10571202 if ms.LoadMessageInfo() == nil {
@@ -1064,7 +1209,7 @@ func (x *RouteAttentionSubscription) ProtoReflect() protoreflect.Message {
10641209
10651210 // Deprecated: Use RouteAttentionSubscription.ProtoReflect.Descriptor instead.
10661211 func (*RouteAttentionSubscription) Descriptor() ([]byte, []int) {
1067- return file_session_proto_rawDescGZIP(), []int{15}
1212+ return file_session_proto_rawDescGZIP(), []int{17}
10681213 }
10691214
10701215 func (x *RouteAttentionSubscription) GetTargets() []*RouteAttentionTarget {
@@ -1094,7 +1239,7 @@ type RecentRouteEntry struct {
10941239
10951240 func (x *RecentRouteEntry) Reset() {
10961241 *x = RecentRouteEntry{}
1097- mi := &file_session_proto_msgTypes[16]
1242+ mi := &file_session_proto_msgTypes[18]
10981243 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
10991244 ms.StoreMessageInfo(mi)
11001245 }
@@ -1106,7 +1251,7 @@ func (x *RecentRouteEntry) String() string {
11061251 func (*RecentRouteEntry) ProtoMessage() {}
11071252
11081253 func (x *RecentRouteEntry) ProtoReflect() protoreflect.Message {
1109- mi := &file_session_proto_msgTypes[16]
1254+ mi := &file_session_proto_msgTypes[18]
11101255 if x != nil {
11111256 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
11121257 if ms.LoadMessageInfo() == nil {
@@ -1119,7 +1264,7 @@ func (x *RecentRouteEntry) ProtoReflect() protoreflect.Message {
11191264
11201265 // Deprecated: Use RecentRouteEntry.ProtoReflect.Descriptor instead.
11211266 func (*RecentRouteEntry) Descriptor() ([]byte, []int) {
1122- return file_session_proto_rawDescGZIP(), []int{16}
1267+ return file_session_proto_rawDescGZIP(), []int{18}
11231268 }
11241269
11251270 func (x *RecentRouteEntry) GetKey() uint64 {
@@ -1200,7 +1345,7 @@ type RecentRouteSnapshot struct {
12001345
12011346 func (x *RecentRouteSnapshot) Reset() {
12021347 *x = RecentRouteSnapshot{}
1203- mi := &file_session_proto_msgTypes[17]
1348+ mi := &file_session_proto_msgTypes[19]
12041349 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
12051350 ms.StoreMessageInfo(mi)
12061351 }
@@ -1212,7 +1357,7 @@ func (x *RecentRouteSnapshot) String() string {
12121357 func (*RecentRouteSnapshot) ProtoMessage() {}
12131358
12141359 func (x *RecentRouteSnapshot) ProtoReflect() protoreflect.Message {
1215- mi := &file_session_proto_msgTypes[17]
1360+ mi := &file_session_proto_msgTypes[19]
12161361 if x != nil {
12171362 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
12181363 if ms.LoadMessageInfo() == nil {
@@ -1225,7 +1370,7 @@ func (x *RecentRouteSnapshot) ProtoReflect() protoreflect.Message {
12251370
12261371 // Deprecated: Use RecentRouteSnapshot.ProtoReflect.Descriptor instead.
12271372 func (*RecentRouteSnapshot) Descriptor() ([]byte, []int) {
1228- return file_session_proto_rawDescGZIP(), []int{17}
1373+ return file_session_proto_rawDescGZIP(), []int{19}
12291374 }
12301375
12311376 func (x *RecentRouteSnapshot) GetGeneration() uint64 {
@@ -1283,7 +1428,7 @@ type RouteNavigationAction struct {
12831428
12841429 func (x *RouteNavigationAction) Reset() {
12851430 *x = RouteNavigationAction{}
1286- mi := &file_session_proto_msgTypes[18]
1431+ mi := &file_session_proto_msgTypes[20]
12871432 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
12881433 ms.StoreMessageInfo(mi)
12891434 }
@@ -1295,7 +1440,7 @@ func (x *RouteNavigationAction) String() string {
12951440 func (*RouteNavigationAction) ProtoMessage() {}
12961441
12971442 func (x *RouteNavigationAction) ProtoReflect() protoreflect.Message {
1298- mi := &file_session_proto_msgTypes[18]
1443+ mi := &file_session_proto_msgTypes[20]
12991444 if x != nil {
13001445 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
13011446 if ms.LoadMessageInfo() == nil {
@@ -1308,7 +1453,7 @@ func (x *RouteNavigationAction) ProtoReflect() protoreflect.Message {
13081453
13091454 // Deprecated: Use RouteNavigationAction.ProtoReflect.Descriptor instead.
13101455 func (*RouteNavigationAction) Descriptor() ([]byte, []int) {
1311- return file_session_proto_rawDescGZIP(), []int{18}
1456+ return file_session_proto_rawDescGZIP(), []int{20}
13121457 }
13131458
13141459 func (x *RouteNavigationAction) GetCauseActionId() uint64 {
@@ -1354,7 +1499,7 @@ type RouteCreateSessionAction struct {
13541499
13551500 func (x *RouteCreateSessionAction) Reset() {
13561501 *x = RouteCreateSessionAction{}
1357- mi := &file_session_proto_msgTypes[19]
1502+ mi := &file_session_proto_msgTypes[21]
13581503 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
13591504 ms.StoreMessageInfo(mi)
13601505 }
@@ -1366,7 +1511,7 @@ func (x *RouteCreateSessionAction) String() string {
13661511 func (*RouteCreateSessionAction) ProtoMessage() {}
13671512
13681513 func (x *RouteCreateSessionAction) ProtoReflect() protoreflect.Message {
1369- mi := &file_session_proto_msgTypes[19]
1514+ mi := &file_session_proto_msgTypes[21]
13701515 if x != nil {
13711516 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
13721517 if ms.LoadMessageInfo() == nil {
@@ -1379,7 +1524,7 @@ func (x *RouteCreateSessionAction) ProtoReflect() protoreflect.Message {
13791524
13801525 // Deprecated: Use RouteCreateSessionAction.ProtoReflect.Descriptor instead.
13811526 func (*RouteCreateSessionAction) Descriptor() ([]byte, []int) {
1382- return file_session_proto_rawDescGZIP(), []int{19}
1527+ return file_session_proto_rawDescGZIP(), []int{21}
13831528 }
13841529
13851530 func (x *RouteCreateSessionAction) GetCauseActionId() uint64 {
@@ -1439,7 +1584,7 @@ type RouteNavigationFailure struct {
14391584
14401585 func (x *RouteNavigationFailure) Reset() {
14411586 *x = RouteNavigationFailure{}
1442- mi := &file_session_proto_msgTypes[20]
1587+ mi := &file_session_proto_msgTypes[22]
14431588 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
14441589 ms.StoreMessageInfo(mi)
14451590 }
@@ -1451,7 +1596,7 @@ func (x *RouteNavigationFailure) String() string {
14511596 func (*RouteNavigationFailure) ProtoMessage() {}
14521597
14531598 func (x *RouteNavigationFailure) ProtoReflect() protoreflect.Message {
1454- mi := &file_session_proto_msgTypes[20]
1599+ mi := &file_session_proto_msgTypes[22]
14551600 if x != nil {
14561601 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
14571602 if ms.LoadMessageInfo() == nil {
@@ -1464,7 +1609,7 @@ func (x *RouteNavigationFailure) ProtoReflect() protoreflect.Message {
14641609
14651610 // Deprecated: Use RouteNavigationFailure.ProtoReflect.Descriptor instead.
14661611 func (*RouteNavigationFailure) Descriptor() ([]byte, []int) {
1467- return file_session_proto_rawDescGZIP(), []int{20}
1612+ return file_session_proto_rawDescGZIP(), []int{22}
14681613 }
14691614
14701615 func (x *RouteNavigationFailure) GetKey() uint64 {
@@ -1499,7 +1644,7 @@ type SessionCreationFailure struct {
14991644
15001645 func (x *SessionCreationFailure) Reset() {
15011646 *x = SessionCreationFailure{}
1502- mi := &file_session_proto_msgTypes[21]
1647+ mi := &file_session_proto_msgTypes[23]
15031648 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
15041649 ms.StoreMessageInfo(mi)
15051650 }
@@ -1511,7 +1656,7 @@ func (x *SessionCreationFailure) String() string {
15111656 func (*SessionCreationFailure) ProtoMessage() {}
15121657
15131658 func (x *SessionCreationFailure) ProtoReflect() protoreflect.Message {
1514- mi := &file_session_proto_msgTypes[21]
1659+ mi := &file_session_proto_msgTypes[23]
15151660 if x != nil {
15161661 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
15171662 if ms.LoadMessageInfo() == nil {
@@ -1524,7 +1669,7 @@ func (x *SessionCreationFailure) ProtoReflect() protoreflect.Message {
15241669
15251670 // Deprecated: Use SessionCreationFailure.ProtoReflect.Descriptor instead.
15261671 func (*SessionCreationFailure) Descriptor() ([]byte, []int) {
1527- return file_session_proto_rawDescGZIP(), []int{21}
1672+ return file_session_proto_rawDescGZIP(), []int{23}
15281673 }
15291674
15301675 func (x *SessionCreationFailure) GetRequestId() uint64 {
@@ -1552,7 +1697,7 @@ type RoutePosition struct {
15521697
15531698 func (x *RoutePosition) Reset() {
15541699 *x = RoutePosition{}
1555- mi := &file_session_proto_msgTypes[22]
1700+ mi := &file_session_proto_msgTypes[24]
15561701 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
15571702 ms.StoreMessageInfo(mi)
15581703 }
@@ -1564,7 +1709,7 @@ func (x *RoutePosition) String() string {
15641709 func (*RoutePosition) ProtoMessage() {}
15651710
15661711 func (x *RoutePosition) ProtoReflect() protoreflect.Message {
1567- mi := &file_session_proto_msgTypes[22]
1712+ mi := &file_session_proto_msgTypes[24]
15681713 if x != nil {
15691714 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
15701715 if ms.LoadMessageInfo() == nil {
@@ -1577,7 +1722,7 @@ func (x *RoutePosition) ProtoReflect() protoreflect.Message {
15771722
15781723 // Deprecated: Use RoutePosition.ProtoReflect.Descriptor instead.
15791724 func (*RoutePosition) Descriptor() ([]byte, []int) {
1580- return file_session_proto_rawDescGZIP(), []int{22}
1725+ return file_session_proto_rawDescGZIP(), []int{24}
15811726 }
15821727
15831728 func (x *RoutePosition) GetTarget() *ExactTarget {
@@ -1605,7 +1750,7 @@ type RouteRetired struct {
16051750
16061751 func (x *RouteRetired) Reset() {
16071752 *x = RouteRetired{}
1608- mi := &file_session_proto_msgTypes[23]
1753+ mi := &file_session_proto_msgTypes[25]
16091754 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
16101755 ms.StoreMessageInfo(mi)
16111756 }
@@ -1617,7 +1762,7 @@ func (x *RouteRetired) String() string {
16171762 func (*RouteRetired) ProtoMessage() {}
16181763
16191764 func (x *RouteRetired) ProtoReflect() protoreflect.Message {
1620- mi := &file_session_proto_msgTypes[23]
1765+ mi := &file_session_proto_msgTypes[25]
16211766 if x != nil {
16221767 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
16231768 if ms.LoadMessageInfo() == nil {
@@ -1630,7 +1775,7 @@ func (x *RouteRetired) ProtoReflect() protoreflect.Message {
16301775
16311776 // Deprecated: Use RouteRetired.ProtoReflect.Descriptor instead.
16321777 func (*RouteRetired) Descriptor() ([]byte, []int) {
1633- return file_session_proto_rawDescGZIP(), []int{23}
1778+ return file_session_proto_rawDescGZIP(), []int{25}
16341779 }
16351780
16361781 func (x *RouteRetired) GetRef() *RouteRef {
@@ -1659,7 +1804,7 @@ type SamePeerSwitchRequest struct {
16591804
16601805 func (x *SamePeerSwitchRequest) Reset() {
16611806 *x = SamePeerSwitchRequest{}
1662- mi := &file_session_proto_msgTypes[24]
1807+ mi := &file_session_proto_msgTypes[26]
16631808 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
16641809 ms.StoreMessageInfo(mi)
16651810 }
@@ -1671,7 +1816,7 @@ func (x *SamePeerSwitchRequest) String() string {
16711816 func (*SamePeerSwitchRequest) ProtoMessage() {}
16721817
16731818 func (x *SamePeerSwitchRequest) ProtoReflect() protoreflect.Message {
1674- mi := &file_session_proto_msgTypes[24]
1819+ mi := &file_session_proto_msgTypes[26]
16751820 if x != nil {
16761821 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
16771822 if ms.LoadMessageInfo() == nil {
@@ -1684,7 +1829,7 @@ func (x *SamePeerSwitchRequest) ProtoReflect() protoreflect.Message {
16841829
16851830 // Deprecated: Use SamePeerSwitchRequest.ProtoReflect.Descriptor instead.
16861831 func (*SamePeerSwitchRequest) Descriptor() ([]byte, []int) {
1687- return file_session_proto_rawDescGZIP(), []int{24}
1832+ return file_session_proto_rawDescGZIP(), []int{26}
16881833 }
16891834
16901835 func (x *SamePeerSwitchRequest) GetRequestId() uint64 {
@@ -1720,7 +1865,7 @@ type SamePeerSwitchFailure struct {
17201865
17211866 func (x *SamePeerSwitchFailure) Reset() {
17221867 *x = SamePeerSwitchFailure{}
1723- mi := &file_session_proto_msgTypes[25]
1868+ mi := &file_session_proto_msgTypes[27]
17241869 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
17251870 ms.StoreMessageInfo(mi)
17261871 }
@@ -1732,7 +1877,7 @@ func (x *SamePeerSwitchFailure) String() string {
17321877 func (*SamePeerSwitchFailure) ProtoMessage() {}
17331878
17341879 func (x *SamePeerSwitchFailure) ProtoReflect() protoreflect.Message {
1735- mi := &file_session_proto_msgTypes[25]
1880+ mi := &file_session_proto_msgTypes[27]
17361881 if x != nil {
17371882 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
17381883 if ms.LoadMessageInfo() == nil {
@@ -1745,7 +1890,7 @@ func (x *SamePeerSwitchFailure) ProtoReflect() protoreflect.Message {
17451890
17461891 // Deprecated: Use SamePeerSwitchFailure.ProtoReflect.Descriptor instead.
17471892 func (*SamePeerSwitchFailure) Descriptor() ([]byte, []int) {
1748- return file_session_proto_rawDescGZIP(), []int{25}
1893+ return file_session_proto_rawDescGZIP(), []int{27}
17491894 }
17501895
17511896 func (x *SamePeerSwitchFailure) GetRequestId() uint64 {
@@ -1772,7 +1917,7 @@ type SuspendAttachment struct {
17721917
17731918 func (x *SuspendAttachment) Reset() {
17741919 *x = SuspendAttachment{}
1775- mi := &file_session_proto_msgTypes[26]
1920+ mi := &file_session_proto_msgTypes[28]
17761921 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
17771922 ms.StoreMessageInfo(mi)
17781923 }
@@ -1784,7 +1929,7 @@ func (x *SuspendAttachment) String() string {
17841929 func (*SuspendAttachment) ProtoMessage() {}
17851930
17861931 func (x *SuspendAttachment) ProtoReflect() protoreflect.Message {
1787- mi := &file_session_proto_msgTypes[26]
1932+ mi := &file_session_proto_msgTypes[28]
17881933 if x != nil {
17891934 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
17901935 if ms.LoadMessageInfo() == nil {
@@ -1797,7 +1942,7 @@ func (x *SuspendAttachment) ProtoReflect() protoreflect.Message {
17971942
17981943 // Deprecated: Use SuspendAttachment.ProtoReflect.Descriptor instead.
17991944 func (*SuspendAttachment) Descriptor() ([]byte, []int) {
1800- return file_session_proto_rawDescGZIP(), []int{26}
1945+ return file_session_proto_rawDescGZIP(), []int{28}
18011946 }
18021947
18031948 func (x *SuspendAttachment) GetRequestId() uint64 {
@@ -1818,7 +1963,7 @@ type AttachmentSuspended struct {
18181963
18191964 func (x *AttachmentSuspended) Reset() {
18201965 *x = AttachmentSuspended{}
1821- mi := &file_session_proto_msgTypes[27]
1966+ mi := &file_session_proto_msgTypes[29]
18221967 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
18231968 ms.StoreMessageInfo(mi)
18241969 }
@@ -1830,7 +1975,7 @@ func (x *AttachmentSuspended) String() string {
18301975 func (*AttachmentSuspended) ProtoMessage() {}
18311976
18321977 func (x *AttachmentSuspended) ProtoReflect() protoreflect.Message {
1833- mi := &file_session_proto_msgTypes[27]
1978+ mi := &file_session_proto_msgTypes[29]
18341979 if x != nil {
18351980 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
18361981 if ms.LoadMessageInfo() == nil {
@@ -1843,7 +1988,7 @@ func (x *AttachmentSuspended) ProtoReflect() protoreflect.Message {
18431988
18441989 // Deprecated: Use AttachmentSuspended.ProtoReflect.Descriptor instead.
18451990 func (*AttachmentSuspended) Descriptor() ([]byte, []int) {
1846- return file_session_proto_rawDescGZIP(), []int{27}
1991+ return file_session_proto_rawDescGZIP(), []int{29}
18471992 }
18481993
18491994 func (x *AttachmentSuspended) GetRequestId() uint64 {
@@ -1875,7 +2020,7 @@ type ActivateAttachment struct {
18752020
18762021 func (x *ActivateAttachment) Reset() {
18772022 *x = ActivateAttachment{}
1878- mi := &file_session_proto_msgTypes[28]
2023+ mi := &file_session_proto_msgTypes[30]
18792024 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
18802025 ms.StoreMessageInfo(mi)
18812026 }
@@ -1887,7 +2032,7 @@ func (x *ActivateAttachment) String() string {
18872032 func (*ActivateAttachment) ProtoMessage() {}
18882033
18892034 func (x *ActivateAttachment) ProtoReflect() protoreflect.Message {
1890- mi := &file_session_proto_msgTypes[28]
2035+ mi := &file_session_proto_msgTypes[30]
18912036 if x != nil {
18922037 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
18932038 if ms.LoadMessageInfo() == nil {
@@ -1900,7 +2045,7 @@ func (x *ActivateAttachment) ProtoReflect() protoreflect.Message {
19002045
19012046 // Deprecated: Use ActivateAttachment.ProtoReflect.Descriptor instead.
19022047 func (*ActivateAttachment) Descriptor() ([]byte, []int) {
1903- return file_session_proto_rawDescGZIP(), []int{28}
2048+ return file_session_proto_rawDescGZIP(), []int{30}
19042049 }
19052050
19062051 func (x *ActivateAttachment) GetRequestId() uint64 {
@@ -1961,7 +2106,7 @@ type AttachmentActivated struct {
19612106
19622107 func (x *AttachmentActivated) Reset() {
19632108 *x = AttachmentActivated{}
1964- mi := &file_session_proto_msgTypes[29]
2109+ mi := &file_session_proto_msgTypes[31]
19652110 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
19662111 ms.StoreMessageInfo(mi)
19672112 }
@@ -1973,7 +2118,7 @@ func (x *AttachmentActivated) String() string {
19732118 func (*AttachmentActivated) ProtoMessage() {}
19742119
19752120 func (x *AttachmentActivated) ProtoReflect() protoreflect.Message {
1976- mi := &file_session_proto_msgTypes[29]
2121+ mi := &file_session_proto_msgTypes[31]
19772122 if x != nil {
19782123 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
19792124 if ms.LoadMessageInfo() == nil {
@@ -1986,7 +2131,7 @@ func (x *AttachmentActivated) ProtoReflect() protoreflect.Message {
19862131
19872132 // Deprecated: Use AttachmentActivated.ProtoReflect.Descriptor instead.
19882133 func (*AttachmentActivated) Descriptor() ([]byte, []int) {
1989- return file_session_proto_rawDescGZIP(), []int{29}
2134+ return file_session_proto_rawDescGZIP(), []int{31}
19902135 }
19912136
19922137 func (x *AttachmentActivated) GetRequestId() uint64 {
@@ -2090,10 +2235,24 @@ const file_session_proto_rawDesc = "" +
20902235 "\x04tabs\x18\x05 \x01(\rR\x04tabs\x12\x1a\n" +
20912236 "\battached\x18\x06 \x01(\bR\battached\"@\n" +
20922237 "\bSessions\x124\n" +
2093- "\bsessions\x18\x01 \x03(\v2\x18.vev.wire.v1.SessionInfoR\bsessions\"0\n" +
2238+ "\bsessions\x18\x01 \x03(\v2\x18.vev.wire.v1.SessionInfoR\bsessions\"O\n" +
20942239 "\x04Kill\x12\x12\n" +
20952240 "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
2096- "\x05scope\x18\x02 \x01(\rR\x05scope\"\"\n" +
2241+ "\x05scope\x18\x02 \x01(\rR\x05scope\x12\x1d\n" +
2242+ "\n" +
2243+ "request_id\x18\x03 \x01(\x04R\trequestId\"K\n" +
2244+ "\vKillFailure\x12\x14\n" +
2245+ "\x05class\x18\x01 \x01(\tR\x05class\x12\x12\n" +
2246+ "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" +
2247+ "\x04text\x18\x03 \x01(\tR\x04text\"\xa3\x01\n" +
2248+ "\n" +
2249+ "KillResult\x12\x1d\n" +
2250+ "\n" +
2251+ "request_id\x18\x01 \x01(\x04R\trequestId\x12\x18\n" +
2252+ "\aoutcome\x18\x02 \x01(\rR\aoutcome\x12\x12\n" +
2253+ "\x04code\x18\x03 \x01(\rR\x04code\x12\x12\n" +
2254+ "\x04text\x18\x04 \x01(\tR\x04text\x124\n" +
2255+ "\bfailures\x18\x05 \x03(\v2\x18.vev.wire.v1.KillFailureR\bfailures\"\"\n" +
20972256 "\bDetached\x12\x16\n" +
20982257 "\x06reason\x18\x01 \x01(\rR\x06reason\"\b\n" +
20992258 "\x06Detach\"\x06\n" +
@@ -2212,7 +2371,7 @@ func file_session_proto_rawDescGZIP() []byte {
22122371 return file_session_proto_rawDescData
22132372 }
22142373
2215-var file_session_proto_msgTypes = make([]protoimpl.MessageInfo, 30)
2374+var file_session_proto_msgTypes = make([]protoimpl.MessageInfo, 32)
22162375 var file_session_proto_goTypes = []any{
22172376 (*Hello)(nil), // 0: vev.wire.v1.Hello
22182377 (*Welcome)(nil), // 1: vev.wire.v1.Welcome
@@ -2222,60 +2381,63 @@ var file_session_proto_goTypes = []any{
22222381 (*SessionInfo)(nil), // 5: vev.wire.v1.SessionInfo
22232382 (*Sessions)(nil), // 6: vev.wire.v1.Sessions
22242383 (*Kill)(nil), // 7: vev.wire.v1.Kill
2225- (*Detached)(nil), // 8: vev.wire.v1.Detached
2226- (*Detach)(nil), // 9: vev.wire.v1.Detach
2227- (*Ping)(nil), // 10: vev.wire.v1.Ping
2228- (*Pong)(nil), // 11: vev.wire.v1.Pong
2229- (*List)(nil), // 12: vev.wire.v1.List
2230- (*RouteRef)(nil), // 13: vev.wire.v1.RouteRef
2231- (*RouteAttentionTarget)(nil), // 14: vev.wire.v1.RouteAttentionTarget
2232- (*RouteAttentionSubscription)(nil), // 15: vev.wire.v1.RouteAttentionSubscription
2233- (*RecentRouteEntry)(nil), // 16: vev.wire.v1.RecentRouteEntry
2234- (*RecentRouteSnapshot)(nil), // 17: vev.wire.v1.RecentRouteSnapshot
2235- (*RouteNavigationAction)(nil), // 18: vev.wire.v1.RouteNavigationAction
2236- (*RouteCreateSessionAction)(nil), // 19: vev.wire.v1.RouteCreateSessionAction
2237- (*RouteNavigationFailure)(nil), // 20: vev.wire.v1.RouteNavigationFailure
2238- (*SessionCreationFailure)(nil), // 21: vev.wire.v1.SessionCreationFailure
2239- (*RoutePosition)(nil), // 22: vev.wire.v1.RoutePosition
2240- (*RouteRetired)(nil), // 23: vev.wire.v1.RouteRetired
2241- (*SamePeerSwitchRequest)(nil), // 24: vev.wire.v1.SamePeerSwitchRequest
2242- (*SamePeerSwitchFailure)(nil), // 25: vev.wire.v1.SamePeerSwitchFailure
2243- (*SuspendAttachment)(nil), // 26: vev.wire.v1.SuspendAttachment
2244- (*AttachmentSuspended)(nil), // 27: vev.wire.v1.AttachmentSuspended
2245- (*ActivateAttachment)(nil), // 28: vev.wire.v1.ActivateAttachment
2246- (*AttachmentActivated)(nil), // 29: vev.wire.v1.AttachmentActivated
2247- (*RemoteTarget)(nil), // 30: vev.wire.v1.RemoteTarget
2248- (*ExactTarget)(nil), // 31: vev.wire.v1.ExactTarget
2384+ (*KillFailure)(nil), // 8: vev.wire.v1.KillFailure
2385+ (*KillResult)(nil), // 9: vev.wire.v1.KillResult
2386+ (*Detached)(nil), // 10: vev.wire.v1.Detached
2387+ (*Detach)(nil), // 11: vev.wire.v1.Detach
2388+ (*Ping)(nil), // 12: vev.wire.v1.Ping
2389+ (*Pong)(nil), // 13: vev.wire.v1.Pong
2390+ (*List)(nil), // 14: vev.wire.v1.List
2391+ (*RouteRef)(nil), // 15: vev.wire.v1.RouteRef
2392+ (*RouteAttentionTarget)(nil), // 16: vev.wire.v1.RouteAttentionTarget
2393+ (*RouteAttentionSubscription)(nil), // 17: vev.wire.v1.RouteAttentionSubscription
2394+ (*RecentRouteEntry)(nil), // 18: vev.wire.v1.RecentRouteEntry
2395+ (*RecentRouteSnapshot)(nil), // 19: vev.wire.v1.RecentRouteSnapshot
2396+ (*RouteNavigationAction)(nil), // 20: vev.wire.v1.RouteNavigationAction
2397+ (*RouteCreateSessionAction)(nil), // 21: vev.wire.v1.RouteCreateSessionAction
2398+ (*RouteNavigationFailure)(nil), // 22: vev.wire.v1.RouteNavigationFailure
2399+ (*SessionCreationFailure)(nil), // 23: vev.wire.v1.SessionCreationFailure
2400+ (*RoutePosition)(nil), // 24: vev.wire.v1.RoutePosition
2401+ (*RouteRetired)(nil), // 25: vev.wire.v1.RouteRetired
2402+ (*SamePeerSwitchRequest)(nil), // 26: vev.wire.v1.SamePeerSwitchRequest
2403+ (*SamePeerSwitchFailure)(nil), // 27: vev.wire.v1.SamePeerSwitchFailure
2404+ (*SuspendAttachment)(nil), // 28: vev.wire.v1.SuspendAttachment
2405+ (*AttachmentSuspended)(nil), // 29: vev.wire.v1.AttachmentSuspended
2406+ (*ActivateAttachment)(nil), // 30: vev.wire.v1.ActivateAttachment
2407+ (*AttachmentActivated)(nil), // 31: vev.wire.v1.AttachmentActivated
2408+ (*RemoteTarget)(nil), // 32: vev.wire.v1.RemoteTarget
2409+ (*ExactTarget)(nil), // 33: vev.wire.v1.ExactTarget
22492410 }
22502411 var file_session_proto_depIdxs = []int32{
2251- 30, // 0: vev.wire.v1.Hello.remote_target:type_name -> vev.wire.v1.RemoteTarget
2252- 31, // 1: vev.wire.v1.Hello.exact_target:type_name -> vev.wire.v1.ExactTarget
2412+ 32, // 0: vev.wire.v1.Hello.remote_target:type_name -> vev.wire.v1.RemoteTarget
2413+ 33, // 1: vev.wire.v1.Hello.exact_target:type_name -> vev.wire.v1.ExactTarget
22532414 3, // 2: vev.wire.v1.Welcome.committed_identity:type_name -> vev.wire.v1.CommittedRouteIdentity
2254- 31, // 3: vev.wire.v1.CommittedRouteIdentity.target:type_name -> vev.wire.v1.ExactTarget
2255- 30, // 4: vev.wire.v1.AttachTarget.remote_target:type_name -> vev.wire.v1.RemoteTarget
2256- 31, // 5: vev.wire.v1.AttachTarget.exact_target:type_name -> vev.wire.v1.ExactTarget
2415+ 33, // 3: vev.wire.v1.CommittedRouteIdentity.target:type_name -> vev.wire.v1.ExactTarget
2416+ 32, // 4: vev.wire.v1.AttachTarget.remote_target:type_name -> vev.wire.v1.RemoteTarget
2417+ 33, // 5: vev.wire.v1.AttachTarget.exact_target:type_name -> vev.wire.v1.ExactTarget
22572418 5, // 6: vev.wire.v1.Sessions.sessions:type_name -> vev.wire.v1.SessionInfo
2258- 13, // 7: vev.wire.v1.RouteAttentionTarget.ref:type_name -> vev.wire.v1.RouteRef
2259- 31, // 8: vev.wire.v1.RouteAttentionTarget.target:type_name -> vev.wire.v1.ExactTarget
2260- 14, // 9: vev.wire.v1.RouteAttentionSubscription.targets:type_name -> vev.wire.v1.RouteAttentionTarget
2261- 31, // 10: vev.wire.v1.RecentRouteEntry.target:type_name -> vev.wire.v1.ExactTarget
2262- 13, // 11: vev.wire.v1.RecentRouteSnapshot.active:type_name -> vev.wire.v1.RouteRef
2263- 16, // 12: vev.wire.v1.RecentRouteSnapshot.active_entry:type_name -> vev.wire.v1.RecentRouteEntry
2264- 13, // 13: vev.wire.v1.RecentRouteSnapshot.previous:type_name -> vev.wire.v1.RouteRef
2265- 13, // 14: vev.wire.v1.RecentRouteSnapshot.home:type_name -> vev.wire.v1.RouteRef
2266- 16, // 15: vev.wire.v1.RecentRouteSnapshot.entries:type_name -> vev.wire.v1.RecentRouteEntry
2267- 31, // 16: vev.wire.v1.RoutePosition.target:type_name -> vev.wire.v1.ExactTarget
2268- 13, // 17: vev.wire.v1.RouteRetired.ref:type_name -> vev.wire.v1.RouteRef
2269- 31, // 18: vev.wire.v1.RouteRetired.target:type_name -> vev.wire.v1.ExactTarget
2270- 31, // 19: vev.wire.v1.SamePeerSwitchRequest.target:type_name -> vev.wire.v1.ExactTarget
2271- 31, // 20: vev.wire.v1.AttachmentSuspended.target:type_name -> vev.wire.v1.ExactTarget
2272- 31, // 21: vev.wire.v1.ActivateAttachment.target:type_name -> vev.wire.v1.ExactTarget
2273- 3, // 22: vev.wire.v1.AttachmentActivated.identity:type_name -> vev.wire.v1.CommittedRouteIdentity
2274- 23, // [23:23] is the sub-list for method output_type
2275- 23, // [23:23] is the sub-list for method input_type
2276- 23, // [23:23] is the sub-list for extension type_name
2277- 23, // [23:23] is the sub-list for extension extendee
2278- 0, // [0:23] is the sub-list for field type_name
2419+ 8, // 7: vev.wire.v1.KillResult.failures:type_name -> vev.wire.v1.KillFailure
2420+ 15, // 8: vev.wire.v1.RouteAttentionTarget.ref:type_name -> vev.wire.v1.RouteRef
2421+ 33, // 9: vev.wire.v1.RouteAttentionTarget.target:type_name -> vev.wire.v1.ExactTarget
2422+ 16, // 10: vev.wire.v1.RouteAttentionSubscription.targets:type_name -> vev.wire.v1.RouteAttentionTarget
2423+ 33, // 11: vev.wire.v1.RecentRouteEntry.target:type_name -> vev.wire.v1.ExactTarget
2424+ 15, // 12: vev.wire.v1.RecentRouteSnapshot.active:type_name -> vev.wire.v1.RouteRef
2425+ 18, // 13: vev.wire.v1.RecentRouteSnapshot.active_entry:type_name -> vev.wire.v1.RecentRouteEntry
2426+ 15, // 14: vev.wire.v1.RecentRouteSnapshot.previous:type_name -> vev.wire.v1.RouteRef
2427+ 15, // 15: vev.wire.v1.RecentRouteSnapshot.home:type_name -> vev.wire.v1.RouteRef
2428+ 18, // 16: vev.wire.v1.RecentRouteSnapshot.entries:type_name -> vev.wire.v1.RecentRouteEntry
2429+ 33, // 17: vev.wire.v1.RoutePosition.target:type_name -> vev.wire.v1.ExactTarget
2430+ 15, // 18: vev.wire.v1.RouteRetired.ref:type_name -> vev.wire.v1.RouteRef
2431+ 33, // 19: vev.wire.v1.RouteRetired.target:type_name -> vev.wire.v1.ExactTarget
2432+ 33, // 20: vev.wire.v1.SamePeerSwitchRequest.target:type_name -> vev.wire.v1.ExactTarget
2433+ 33, // 21: vev.wire.v1.AttachmentSuspended.target:type_name -> vev.wire.v1.ExactTarget
2434+ 33, // 22: vev.wire.v1.ActivateAttachment.target:type_name -> vev.wire.v1.ExactTarget
2435+ 3, // 23: vev.wire.v1.AttachmentActivated.identity:type_name -> vev.wire.v1.CommittedRouteIdentity
2436+ 24, // [24:24] is the sub-list for method output_type
2437+ 24, // [24:24] is the sub-list for method input_type
2438+ 24, // [24:24] is the sub-list for extension type_name
2439+ 24, // [24:24] is the sub-list for extension extendee
2440+ 0, // [0:24] is the sub-list for field type_name
22792441 }
22802442
22812443 func init() { file_session_proto_init() }
@@ -2290,7 +2452,7 @@ func file_session_proto_init() {
22902452 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
22912453 RawDescriptor: unsafe.Slice(unsafe.StringData(file_session_proto_rawDesc), len(file_session_proto_rawDesc)),
22922454 NumEnums: 0,
2293- NumMessages: 30,
2455+ NumMessages: 32,
22942456 NumExtensions: 0,
22952457 NumServices: 0,
22962458 },
@@ -674,6 +674,7 @@ type Kill struct {
674 Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`674 Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
675 // Closed scope taxonomy: 0 = session, 1 = daemon, 2 = all.675 // Closed scope taxonomy: 0 = session, 1 = daemon, 2 = all.
676 Scope uint32 `protobuf:"varint,2,opt,name=scope,proto3" json:"scope,omitempty"`676 Scope uint32 `protobuf:"varint,2,opt,name=scope,proto3" json:"scope,omitempty"`
677+ RequestId uint64 `protobuf:"varint,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
677 unknownFields protoimpl.UnknownFields678 unknownFields protoimpl.UnknownFields
678 sizeCache protoimpl.SizeCache679 sizeCache protoimpl.SizeCache
679 }680 }
@@ -722,6 +723,150 @@ func (x *Kill) GetScope() uint32 {
722 return 0723 return 0
723 }724 }
724 725
726+func (x *Kill) GetRequestId() uint64 {
727+ if x != nil {
728+ return x.RequestId
729+ }
730+ return 0
731+}
732+
733+type KillFailure struct {
734+ state protoimpl.MessageState `protogen:"open.v1"`
735+ Class string `protobuf:"bytes,1,opt,name=class,proto3" json:"class,omitempty"`
736+ Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
737+ Text string `protobuf:"bytes,3,opt,name=text,proto3" json:"text,omitempty"`
738+ unknownFields protoimpl.UnknownFields
739+ sizeCache protoimpl.SizeCache
740+}
741+
742+func (x *KillFailure) Reset() {
743+ *x = KillFailure{}
744+ mi := &file_session_proto_msgTypes[8]
745+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
746+ ms.StoreMessageInfo(mi)
747+}
748+
749+func (x *KillFailure) String() string {
750+ return protoimpl.X.MessageStringOf(x)
751+}
752+
753+func (*KillFailure) ProtoMessage() {}
754+
755+func (x *KillFailure) ProtoReflect() protoreflect.Message {
756+ mi := &file_session_proto_msgTypes[8]
757+ if x != nil {
758+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
759+ if ms.LoadMessageInfo() == nil {
760+ ms.StoreMessageInfo(mi)
761+ }
762+ return ms
763+ }
764+ return mi.MessageOf(x)
765+}
766+
767+// Deprecated: Use KillFailure.ProtoReflect.Descriptor instead.
768+func (*KillFailure) Descriptor() ([]byte, []int) {
769+ return file_session_proto_rawDescGZIP(), []int{8}
770+}
771+
772+func (x *KillFailure) GetClass() string {
773+ if x != nil {
774+ return x.Class
775+ }
776+ return ""
777+}
778+
779+func (x *KillFailure) GetName() string {
780+ if x != nil {
781+ return x.Name
782+ }
783+ return ""
784+}
785+
786+func (x *KillFailure) GetText() string {
787+ if x != nil {
788+ return x.Text
789+ }
790+ return ""
791+}
792+
793+type KillResult struct {
794+ state protoimpl.MessageState `protogen:"open.v1"`
795+ RequestId uint64 `protobuf:"varint,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
796+ // Closed outcome taxonomy: 1 = succeeded, 2 = failed, 3 = outcome unknown.
797+ Outcome uint32 `protobuf:"varint,2,opt,name=outcome,proto3" json:"outcome,omitempty"`
798+ Code uint32 `protobuf:"varint,3,opt,name=code,proto3" json:"code,omitempty"`
799+ Text string `protobuf:"bytes,4,opt,name=text,proto3" json:"text,omitempty"`
800+ Failures []*KillFailure `protobuf:"bytes,5,rep,name=failures,proto3" json:"failures,omitempty"`
801+ unknownFields protoimpl.UnknownFields
802+ sizeCache protoimpl.SizeCache
803+}
804+
805+func (x *KillResult) Reset() {
806+ *x = KillResult{}
807+ mi := &file_session_proto_msgTypes[9]
808+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
809+ ms.StoreMessageInfo(mi)
810+}
811+
812+func (x *KillResult) String() string {
813+ return protoimpl.X.MessageStringOf(x)
814+}
815+
816+func (*KillResult) ProtoMessage() {}
817+
818+func (x *KillResult) ProtoReflect() protoreflect.Message {
819+ mi := &file_session_proto_msgTypes[9]
820+ if x != nil {
821+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
822+ if ms.LoadMessageInfo() == nil {
823+ ms.StoreMessageInfo(mi)
824+ }
825+ return ms
826+ }
827+ return mi.MessageOf(x)
828+}
829+
830+// Deprecated: Use KillResult.ProtoReflect.Descriptor instead.
831+func (*KillResult) Descriptor() ([]byte, []int) {
832+ return file_session_proto_rawDescGZIP(), []int{9}
833+}
834+
835+func (x *KillResult) GetRequestId() uint64 {
836+ if x != nil {
837+ return x.RequestId
838+ }
839+ return 0
840+}
841+
842+func (x *KillResult) GetOutcome() uint32 {
843+ if x != nil {
844+ return x.Outcome
845+ }
846+ return 0
847+}
848+
849+func (x *KillResult) GetCode() uint32 {
850+ if x != nil {
851+ return x.Code
852+ }
853+ return 0
854+}
855+
856+func (x *KillResult) GetText() string {
857+ if x != nil {
858+ return x.Text
859+ }
860+ return ""
861+}
862+
863+func (x *KillResult) GetFailures() []*KillFailure {
864+ if x != nil {
865+ return x.Failures
866+ }
867+ return nil
868+}
869+
725 // Detached reports why an attachment ended.870 // Detached reports why an attachment ended.
726 type Detached struct {871 type Detached struct {
727 state protoimpl.MessageState `protogen:"open.v1"`872 state protoimpl.MessageState `protogen:"open.v1"`
@@ -733,7 +878,7 @@ type Detached struct {
733 878
734 func (x *Detached) Reset() {879 func (x *Detached) Reset() {
735 *x = Detached{}880 *x = Detached{}
736- mi := &file_session_proto_msgTypes[8]881+ mi := &file_session_proto_msgTypes[10]
737 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))882 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
738 ms.StoreMessageInfo(mi)883 ms.StoreMessageInfo(mi)
739 }884 }
@@ -745,7 +890,7 @@ func (x *Detached) String() string {
745 func (*Detached) ProtoMessage() {}890 func (*Detached) ProtoMessage() {}
746 891
747 func (x *Detached) ProtoReflect() protoreflect.Message {892 func (x *Detached) ProtoReflect() protoreflect.Message {
748- mi := &file_session_proto_msgTypes[8]893+ mi := &file_session_proto_msgTypes[10]
749 if x != nil {894 if x != nil {
750 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))895 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
751 if ms.LoadMessageInfo() == nil {896 if ms.LoadMessageInfo() == nil {
@@ -758,7 +903,7 @@ func (x *Detached) ProtoReflect() protoreflect.Message {
758 903
759 // Deprecated: Use Detached.ProtoReflect.Descriptor instead.904 // Deprecated: Use Detached.ProtoReflect.Descriptor instead.
760 func (*Detached) Descriptor() ([]byte, []int) {905 func (*Detached) Descriptor() ([]byte, []int) {
761- return file_session_proto_rawDescGZIP(), []int{8}906+ return file_session_proto_rawDescGZIP(), []int{10}
762 }907 }
763 908
764 func (x *Detached) GetReason() uint32 {909 func (x *Detached) GetReason() uint32 {
@@ -777,7 +922,7 @@ type Detach struct {
777 922
778 func (x *Detach) Reset() {923 func (x *Detach) Reset() {
779 *x = Detach{}924 *x = Detach{}
780- mi := &file_session_proto_msgTypes[9]925+ mi := &file_session_proto_msgTypes[11]
781 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))926 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
782 ms.StoreMessageInfo(mi)927 ms.StoreMessageInfo(mi)
783 }928 }
@@ -789,7 +934,7 @@ func (x *Detach) String() string {
789 func (*Detach) ProtoMessage() {}934 func (*Detach) ProtoMessage() {}
790 935
791 func (x *Detach) ProtoReflect() protoreflect.Message {936 func (x *Detach) ProtoReflect() protoreflect.Message {
792- mi := &file_session_proto_msgTypes[9]937+ mi := &file_session_proto_msgTypes[11]
793 if x != nil {938 if x != nil {
794 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))939 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
795 if ms.LoadMessageInfo() == nil {940 if ms.LoadMessageInfo() == nil {
@@ -802,7 +947,7 @@ func (x *Detach) ProtoReflect() protoreflect.Message {
802 947
803 // Deprecated: Use Detach.ProtoReflect.Descriptor instead.948 // Deprecated: Use Detach.ProtoReflect.Descriptor instead.
804 func (*Detach) Descriptor() ([]byte, []int) {949 func (*Detach) Descriptor() ([]byte, []int) {
805- return file_session_proto_rawDescGZIP(), []int{9}950+ return file_session_proto_rawDescGZIP(), []int{11}
806 }951 }
807 952
808 // Ping / Pong are empty keepalive markers.953 // Ping / Pong are empty keepalive markers.
@@ -814,7 +959,7 @@ type Ping struct {
814 959
815 func (x *Ping) Reset() {960 func (x *Ping) Reset() {
816 *x = Ping{}961 *x = Ping{}
817- mi := &file_session_proto_msgTypes[10]962+ mi := &file_session_proto_msgTypes[12]
818 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))963 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
819 ms.StoreMessageInfo(mi)964 ms.StoreMessageInfo(mi)
820 }965 }
@@ -826,7 +971,7 @@ func (x *Ping) String() string {
826 func (*Ping) ProtoMessage() {}971 func (*Ping) ProtoMessage() {}
827 972
828 func (x *Ping) ProtoReflect() protoreflect.Message {973 func (x *Ping) ProtoReflect() protoreflect.Message {
829- mi := &file_session_proto_msgTypes[10]974+ mi := &file_session_proto_msgTypes[12]
830 if x != nil {975 if x != nil {
831 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))976 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
832 if ms.LoadMessageInfo() == nil {977 if ms.LoadMessageInfo() == nil {
@@ -839,7 +984,7 @@ func (x *Ping) ProtoReflect() protoreflect.Message {
839 984
840 // Deprecated: Use Ping.ProtoReflect.Descriptor instead.985 // Deprecated: Use Ping.ProtoReflect.Descriptor instead.
841 func (*Ping) Descriptor() ([]byte, []int) {986 func (*Ping) Descriptor() ([]byte, []int) {
842- return file_session_proto_rawDescGZIP(), []int{10}987+ return file_session_proto_rawDescGZIP(), []int{12}
843 }988 }
844 989
845 type Pong struct {990 type Pong struct {
@@ -850,7 +995,7 @@ type Pong struct {
850 995
851 func (x *Pong) Reset() {996 func (x *Pong) Reset() {
852 *x = Pong{}997 *x = Pong{}
853- mi := &file_session_proto_msgTypes[11]998+ mi := &file_session_proto_msgTypes[13]
854 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))999 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
855 ms.StoreMessageInfo(mi)1000 ms.StoreMessageInfo(mi)
856 }1001 }
@@ -862,7 +1007,7 @@ func (x *Pong) String() string {
862 func (*Pong) ProtoMessage() {}1007 func (*Pong) ProtoMessage() {}
863 1008
864 func (x *Pong) ProtoReflect() protoreflect.Message {1009 func (x *Pong) ProtoReflect() protoreflect.Message {
865- mi := &file_session_proto_msgTypes[11]1010+ mi := &file_session_proto_msgTypes[13]
866 if x != nil {1011 if x != nil {
867 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1012 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
868 if ms.LoadMessageInfo() == nil {1013 if ms.LoadMessageInfo() == nil {
@@ -875,7 +1020,7 @@ func (x *Pong) ProtoReflect() protoreflect.Message {
875 1020
876 // Deprecated: Use Pong.ProtoReflect.Descriptor instead.1021 // Deprecated: Use Pong.ProtoReflect.Descriptor instead.
877 func (*Pong) Descriptor() ([]byte, []int) {1022 func (*Pong) Descriptor() ([]byte, []int) {
878- return file_session_proto_rawDescGZIP(), []int{11}1023+ return file_session_proto_rawDescGZIP(), []int{13}
879 }1024 }
880 1025
881 // List asks for the one-shot session listing. Empty marker.1026 // List asks for the one-shot session listing. Empty marker.
@@ -887,7 +1032,7 @@ type List struct {
887 1032
888 func (x *List) Reset() {1033 func (x *List) Reset() {
889 *x = List{}1034 *x = List{}
890- mi := &file_session_proto_msgTypes[12]1035+ mi := &file_session_proto_msgTypes[14]
891 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1036 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
892 ms.StoreMessageInfo(mi)1037 ms.StoreMessageInfo(mi)
893 }1038 }
@@ -899,7 +1044,7 @@ func (x *List) String() string {
899 func (*List) ProtoMessage() {}1044 func (*List) ProtoMessage() {}
900 1045
901 func (x *List) ProtoReflect() protoreflect.Message {1046 func (x *List) ProtoReflect() protoreflect.Message {
902- mi := &file_session_proto_msgTypes[12]1047+ mi := &file_session_proto_msgTypes[14]
903 if x != nil {1048 if x != nil {
904 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1049 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
905 if ms.LoadMessageInfo() == nil {1050 if ms.LoadMessageInfo() == nil {
@@ -912,7 +1057,7 @@ func (x *List) ProtoReflect() protoreflect.Message {
912 1057
913 // Deprecated: Use List.ProtoReflect.Descriptor instead.1058 // Deprecated: Use List.ProtoReflect.Descriptor instead.
914 func (*List) Descriptor() ([]byte, []int) {1059 func (*List) Descriptor() ([]byte, []int) {
915- return file_session_proto_rawDescGZIP(), []int{12}1060+ return file_session_proto_rawDescGZIP(), []int{14}
916 }1061 }
917 1062
918 // RouteRef is an opaque client-ledger reference.1063 // RouteRef is an opaque client-ledger reference.
@@ -926,7 +1071,7 @@ type RouteRef struct {
926 1071
927 func (x *RouteRef) Reset() {1072 func (x *RouteRef) Reset() {
928 *x = RouteRef{}1073 *x = RouteRef{}
929- mi := &file_session_proto_msgTypes[13]1074+ mi := &file_session_proto_msgTypes[15]
930 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1075 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
931 ms.StoreMessageInfo(mi)1076 ms.StoreMessageInfo(mi)
932 }1077 }
@@ -938,7 +1083,7 @@ func (x *RouteRef) String() string {
938 func (*RouteRef) ProtoMessage() {}1083 func (*RouteRef) ProtoMessage() {}
939 1084
940 func (x *RouteRef) ProtoReflect() protoreflect.Message {1085 func (x *RouteRef) ProtoReflect() protoreflect.Message {
941- mi := &file_session_proto_msgTypes[13]1086+ mi := &file_session_proto_msgTypes[15]
942 if x != nil {1087 if x != nil {
943 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1088 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
944 if ms.LoadMessageInfo() == nil {1089 if ms.LoadMessageInfo() == nil {
@@ -951,7 +1096,7 @@ func (x *RouteRef) ProtoReflect() protoreflect.Message {
951 1096
952 // Deprecated: Use RouteRef.ProtoReflect.Descriptor instead.1097 // Deprecated: Use RouteRef.ProtoReflect.Descriptor instead.
953 func (*RouteRef) Descriptor() ([]byte, []int) {1098 func (*RouteRef) Descriptor() ([]byte, []int) {
954- return file_session_proto_rawDescGZIP(), []int{13}1099+ return file_session_proto_rawDescGZIP(), []int{15}
955 }1100 }
956 1101
957 func (x *RouteRef) GetKey() uint64 {1102 func (x *RouteRef) GetKey() uint64 {
@@ -980,7 +1125,7 @@ type RouteAttentionTarget struct {
980 1125
981 func (x *RouteAttentionTarget) Reset() {1126 func (x *RouteAttentionTarget) Reset() {
982 *x = RouteAttentionTarget{}1127 *x = RouteAttentionTarget{}
983- mi := &file_session_proto_msgTypes[14]1128+ mi := &file_session_proto_msgTypes[16]
984 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1129 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
985 ms.StoreMessageInfo(mi)1130 ms.StoreMessageInfo(mi)
986 }1131 }
@@ -992,7 +1137,7 @@ func (x *RouteAttentionTarget) String() string {
992 func (*RouteAttentionTarget) ProtoMessage() {}1137 func (*RouteAttentionTarget) ProtoMessage() {}
993 1138
994 func (x *RouteAttentionTarget) ProtoReflect() protoreflect.Message {1139 func (x *RouteAttentionTarget) ProtoReflect() protoreflect.Message {
995- mi := &file_session_proto_msgTypes[14]1140+ mi := &file_session_proto_msgTypes[16]
996 if x != nil {1141 if x != nil {
997 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1142 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
998 if ms.LoadMessageInfo() == nil {1143 if ms.LoadMessageInfo() == nil {
@@ -1005,7 +1150,7 @@ func (x *RouteAttentionTarget) ProtoReflect() protoreflect.Message {
1005 1150
1006 // Deprecated: Use RouteAttentionTarget.ProtoReflect.Descriptor instead.1151 // Deprecated: Use RouteAttentionTarget.ProtoReflect.Descriptor instead.
1007 func (*RouteAttentionTarget) Descriptor() ([]byte, []int) {1152 func (*RouteAttentionTarget) Descriptor() ([]byte, []int) {
1008- return file_session_proto_rawDescGZIP(), []int{14}1153+ return file_session_proto_rawDescGZIP(), []int{16}
1009 }1154 }
1010 1155
1011 func (x *RouteAttentionTarget) GetRef() *RouteRef {1156 func (x *RouteAttentionTarget) GetRef() *RouteRef {
@@ -1039,7 +1184,7 @@ type RouteAttentionSubscription struct {
1039 1184
1040 func (x *RouteAttentionSubscription) Reset() {1185 func (x *RouteAttentionSubscription) Reset() {
1041 *x = RouteAttentionSubscription{}1186 *x = RouteAttentionSubscription{}
1042- mi := &file_session_proto_msgTypes[15]1187+ mi := &file_session_proto_msgTypes[17]
1043 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1188 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1044 ms.StoreMessageInfo(mi)1189 ms.StoreMessageInfo(mi)
1045 }1190 }
@@ -1051,7 +1196,7 @@ func (x *RouteAttentionSubscription) String() string {
1051 func (*RouteAttentionSubscription) ProtoMessage() {}1196 func (*RouteAttentionSubscription) ProtoMessage() {}
1052 1197
1053 func (x *RouteAttentionSubscription) ProtoReflect() protoreflect.Message {1198 func (x *RouteAttentionSubscription) ProtoReflect() protoreflect.Message {
1054- mi := &file_session_proto_msgTypes[15]1199+ mi := &file_session_proto_msgTypes[17]
1055 if x != nil {1200 if x != nil {
1056 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1201 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1057 if ms.LoadMessageInfo() == nil {1202 if ms.LoadMessageInfo() == nil {
@@ -1064,7 +1209,7 @@ func (x *RouteAttentionSubscription) ProtoReflect() protoreflect.Message {
1064 1209
1065 // Deprecated: Use RouteAttentionSubscription.ProtoReflect.Descriptor instead.1210 // Deprecated: Use RouteAttentionSubscription.ProtoReflect.Descriptor instead.
1066 func (*RouteAttentionSubscription) Descriptor() ([]byte, []int) {1211 func (*RouteAttentionSubscription) Descriptor() ([]byte, []int) {
1067- return file_session_proto_rawDescGZIP(), []int{15}1212+ return file_session_proto_rawDescGZIP(), []int{17}
1068 }1213 }
1069 1214
1070 func (x *RouteAttentionSubscription) GetTargets() []*RouteAttentionTarget {1215 func (x *RouteAttentionSubscription) GetTargets() []*RouteAttentionTarget {
@@ -1094,7 +1239,7 @@ type RecentRouteEntry struct {
1094 1239
1095 func (x *RecentRouteEntry) Reset() {1240 func (x *RecentRouteEntry) Reset() {
1096 *x = RecentRouteEntry{}1241 *x = RecentRouteEntry{}
1097- mi := &file_session_proto_msgTypes[16]1242+ mi := &file_session_proto_msgTypes[18]
1098 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1243 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1099 ms.StoreMessageInfo(mi)1244 ms.StoreMessageInfo(mi)
1100 }1245 }
@@ -1106,7 +1251,7 @@ func (x *RecentRouteEntry) String() string {
1106 func (*RecentRouteEntry) ProtoMessage() {}1251 func (*RecentRouteEntry) ProtoMessage() {}
1107 1252
1108 func (x *RecentRouteEntry) ProtoReflect() protoreflect.Message {1253 func (x *RecentRouteEntry) ProtoReflect() protoreflect.Message {
1109- mi := &file_session_proto_msgTypes[16]1254+ mi := &file_session_proto_msgTypes[18]
1110 if x != nil {1255 if x != nil {
1111 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1256 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1112 if ms.LoadMessageInfo() == nil {1257 if ms.LoadMessageInfo() == nil {
@@ -1119,7 +1264,7 @@ func (x *RecentRouteEntry) ProtoReflect() protoreflect.Message {
1119 1264
1120 // Deprecated: Use RecentRouteEntry.ProtoReflect.Descriptor instead.1265 // Deprecated: Use RecentRouteEntry.ProtoReflect.Descriptor instead.
1121 func (*RecentRouteEntry) Descriptor() ([]byte, []int) {1266 func (*RecentRouteEntry) Descriptor() ([]byte, []int) {
1122- return file_session_proto_rawDescGZIP(), []int{16}1267+ return file_session_proto_rawDescGZIP(), []int{18}
1123 }1268 }
1124 1269
1125 func (x *RecentRouteEntry) GetKey() uint64 {1270 func (x *RecentRouteEntry) GetKey() uint64 {
@@ -1200,7 +1345,7 @@ type RecentRouteSnapshot struct {
1200 1345
1201 func (x *RecentRouteSnapshot) Reset() {1346 func (x *RecentRouteSnapshot) Reset() {
1202 *x = RecentRouteSnapshot{}1347 *x = RecentRouteSnapshot{}
1203- mi := &file_session_proto_msgTypes[17]1348+ mi := &file_session_proto_msgTypes[19]
1204 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1349 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1205 ms.StoreMessageInfo(mi)1350 ms.StoreMessageInfo(mi)
1206 }1351 }
@@ -1212,7 +1357,7 @@ func (x *RecentRouteSnapshot) String() string {
1212 func (*RecentRouteSnapshot) ProtoMessage() {}1357 func (*RecentRouteSnapshot) ProtoMessage() {}
1213 1358
1214 func (x *RecentRouteSnapshot) ProtoReflect() protoreflect.Message {1359 func (x *RecentRouteSnapshot) ProtoReflect() protoreflect.Message {
1215- mi := &file_session_proto_msgTypes[17]1360+ mi := &file_session_proto_msgTypes[19]
1216 if x != nil {1361 if x != nil {
1217 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1362 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1218 if ms.LoadMessageInfo() == nil {1363 if ms.LoadMessageInfo() == nil {
@@ -1225,7 +1370,7 @@ func (x *RecentRouteSnapshot) ProtoReflect() protoreflect.Message {
1225 1370
1226 // Deprecated: Use RecentRouteSnapshot.ProtoReflect.Descriptor instead.1371 // Deprecated: Use RecentRouteSnapshot.ProtoReflect.Descriptor instead.
1227 func (*RecentRouteSnapshot) Descriptor() ([]byte, []int) {1372 func (*RecentRouteSnapshot) Descriptor() ([]byte, []int) {
1228- return file_session_proto_rawDescGZIP(), []int{17}1373+ return file_session_proto_rawDescGZIP(), []int{19}
1229 }1374 }
1230 1375
1231 func (x *RecentRouteSnapshot) GetGeneration() uint64 {1376 func (x *RecentRouteSnapshot) GetGeneration() uint64 {
@@ -1283,7 +1428,7 @@ type RouteNavigationAction struct {
1283 1428
1284 func (x *RouteNavigationAction) Reset() {1429 func (x *RouteNavigationAction) Reset() {
1285 *x = RouteNavigationAction{}1430 *x = RouteNavigationAction{}
1286- mi := &file_session_proto_msgTypes[18]1431+ mi := &file_session_proto_msgTypes[20]
1287 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1432 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1288 ms.StoreMessageInfo(mi)1433 ms.StoreMessageInfo(mi)
1289 }1434 }
@@ -1295,7 +1440,7 @@ func (x *RouteNavigationAction) String() string {
1295 func (*RouteNavigationAction) ProtoMessage() {}1440 func (*RouteNavigationAction) ProtoMessage() {}
1296 1441
1297 func (x *RouteNavigationAction) ProtoReflect() protoreflect.Message {1442 func (x *RouteNavigationAction) ProtoReflect() protoreflect.Message {
1298- mi := &file_session_proto_msgTypes[18]1443+ mi := &file_session_proto_msgTypes[20]
1299 if x != nil {1444 if x != nil {
1300 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1445 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1301 if ms.LoadMessageInfo() == nil {1446 if ms.LoadMessageInfo() == nil {
@@ -1308,7 +1453,7 @@ func (x *RouteNavigationAction) ProtoReflect() protoreflect.Message {
1308 1453
1309 // Deprecated: Use RouteNavigationAction.ProtoReflect.Descriptor instead.1454 // Deprecated: Use RouteNavigationAction.ProtoReflect.Descriptor instead.
1310 func (*RouteNavigationAction) Descriptor() ([]byte, []int) {1455 func (*RouteNavigationAction) Descriptor() ([]byte, []int) {
1311- return file_session_proto_rawDescGZIP(), []int{18}1456+ return file_session_proto_rawDescGZIP(), []int{20}
1312 }1457 }
1313 1458
1314 func (x *RouteNavigationAction) GetCauseActionId() uint64 {1459 func (x *RouteNavigationAction) GetCauseActionId() uint64 {
@@ -1354,7 +1499,7 @@ type RouteCreateSessionAction struct {
1354 1499
1355 func (x *RouteCreateSessionAction) Reset() {1500 func (x *RouteCreateSessionAction) Reset() {
1356 *x = RouteCreateSessionAction{}1501 *x = RouteCreateSessionAction{}
1357- mi := &file_session_proto_msgTypes[19]1502+ mi := &file_session_proto_msgTypes[21]
1358 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1503 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1359 ms.StoreMessageInfo(mi)1504 ms.StoreMessageInfo(mi)
1360 }1505 }
@@ -1366,7 +1511,7 @@ func (x *RouteCreateSessionAction) String() string {
1366 func (*RouteCreateSessionAction) ProtoMessage() {}1511 func (*RouteCreateSessionAction) ProtoMessage() {}
1367 1512
1368 func (x *RouteCreateSessionAction) ProtoReflect() protoreflect.Message {1513 func (x *RouteCreateSessionAction) ProtoReflect() protoreflect.Message {
1369- mi := &file_session_proto_msgTypes[19]1514+ mi := &file_session_proto_msgTypes[21]
1370 if x != nil {1515 if x != nil {
1371 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1516 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1372 if ms.LoadMessageInfo() == nil {1517 if ms.LoadMessageInfo() == nil {
@@ -1379,7 +1524,7 @@ func (x *RouteCreateSessionAction) ProtoReflect() protoreflect.Message {
1379 1524
1380 // Deprecated: Use RouteCreateSessionAction.ProtoReflect.Descriptor instead.1525 // Deprecated: Use RouteCreateSessionAction.ProtoReflect.Descriptor instead.
1381 func (*RouteCreateSessionAction) Descriptor() ([]byte, []int) {1526 func (*RouteCreateSessionAction) Descriptor() ([]byte, []int) {
1382- return file_session_proto_rawDescGZIP(), []int{19}1527+ return file_session_proto_rawDescGZIP(), []int{21}
1383 }1528 }
1384 1529
1385 func (x *RouteCreateSessionAction) GetCauseActionId() uint64 {1530 func (x *RouteCreateSessionAction) GetCauseActionId() uint64 {
@@ -1439,7 +1584,7 @@ type RouteNavigationFailure struct {
1439 1584
1440 func (x *RouteNavigationFailure) Reset() {1585 func (x *RouteNavigationFailure) Reset() {
1441 *x = RouteNavigationFailure{}1586 *x = RouteNavigationFailure{}
1442- mi := &file_session_proto_msgTypes[20]1587+ mi := &file_session_proto_msgTypes[22]
1443 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1588 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1444 ms.StoreMessageInfo(mi)1589 ms.StoreMessageInfo(mi)
1445 }1590 }
@@ -1451,7 +1596,7 @@ func (x *RouteNavigationFailure) String() string {
1451 func (*RouteNavigationFailure) ProtoMessage() {}1596 func (*RouteNavigationFailure) ProtoMessage() {}
1452 1597
1453 func (x *RouteNavigationFailure) ProtoReflect() protoreflect.Message {1598 func (x *RouteNavigationFailure) ProtoReflect() protoreflect.Message {
1454- mi := &file_session_proto_msgTypes[20]1599+ mi := &file_session_proto_msgTypes[22]
1455 if x != nil {1600 if x != nil {
1456 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1601 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1457 if ms.LoadMessageInfo() == nil {1602 if ms.LoadMessageInfo() == nil {
@@ -1464,7 +1609,7 @@ func (x *RouteNavigationFailure) ProtoReflect() protoreflect.Message {
1464 1609
1465 // Deprecated: Use RouteNavigationFailure.ProtoReflect.Descriptor instead.1610 // Deprecated: Use RouteNavigationFailure.ProtoReflect.Descriptor instead.
1466 func (*RouteNavigationFailure) Descriptor() ([]byte, []int) {1611 func (*RouteNavigationFailure) Descriptor() ([]byte, []int) {
1467- return file_session_proto_rawDescGZIP(), []int{20}1612+ return file_session_proto_rawDescGZIP(), []int{22}
1468 }1613 }
1469 1614
1470 func (x *RouteNavigationFailure) GetKey() uint64 {1615 func (x *RouteNavigationFailure) GetKey() uint64 {
@@ -1499,7 +1644,7 @@ type SessionCreationFailure struct {
1499 1644
1500 func (x *SessionCreationFailure) Reset() {1645 func (x *SessionCreationFailure) Reset() {
1501 *x = SessionCreationFailure{}1646 *x = SessionCreationFailure{}
1502- mi := &file_session_proto_msgTypes[21]1647+ mi := &file_session_proto_msgTypes[23]
1503 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1648 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1504 ms.StoreMessageInfo(mi)1649 ms.StoreMessageInfo(mi)
1505 }1650 }
@@ -1511,7 +1656,7 @@ func (x *SessionCreationFailure) String() string {
1511 func (*SessionCreationFailure) ProtoMessage() {}1656 func (*SessionCreationFailure) ProtoMessage() {}
1512 1657
1513 func (x *SessionCreationFailure) ProtoReflect() protoreflect.Message {1658 func (x *SessionCreationFailure) ProtoReflect() protoreflect.Message {
1514- mi := &file_session_proto_msgTypes[21]1659+ mi := &file_session_proto_msgTypes[23]
1515 if x != nil {1660 if x != nil {
1516 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1661 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1517 if ms.LoadMessageInfo() == nil {1662 if ms.LoadMessageInfo() == nil {
@@ -1524,7 +1669,7 @@ func (x *SessionCreationFailure) ProtoReflect() protoreflect.Message {
1524 1669
1525 // Deprecated: Use SessionCreationFailure.ProtoReflect.Descriptor instead.1670 // Deprecated: Use SessionCreationFailure.ProtoReflect.Descriptor instead.
1526 func (*SessionCreationFailure) Descriptor() ([]byte, []int) {1671 func (*SessionCreationFailure) Descriptor() ([]byte, []int) {
1527- return file_session_proto_rawDescGZIP(), []int{21}1672+ return file_session_proto_rawDescGZIP(), []int{23}
1528 }1673 }
1529 1674
1530 func (x *SessionCreationFailure) GetRequestId() uint64 {1675 func (x *SessionCreationFailure) GetRequestId() uint64 {
@@ -1552,7 +1697,7 @@ type RoutePosition struct {
1552 1697
1553 func (x *RoutePosition) Reset() {1698 func (x *RoutePosition) Reset() {
1554 *x = RoutePosition{}1699 *x = RoutePosition{}
1555- mi := &file_session_proto_msgTypes[22]1700+ mi := &file_session_proto_msgTypes[24]
1556 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1701 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1557 ms.StoreMessageInfo(mi)1702 ms.StoreMessageInfo(mi)
1558 }1703 }
@@ -1564,7 +1709,7 @@ func (x *RoutePosition) String() string {
1564 func (*RoutePosition) ProtoMessage() {}1709 func (*RoutePosition) ProtoMessage() {}
1565 1710
1566 func (x *RoutePosition) ProtoReflect() protoreflect.Message {1711 func (x *RoutePosition) ProtoReflect() protoreflect.Message {
1567- mi := &file_session_proto_msgTypes[22]1712+ mi := &file_session_proto_msgTypes[24]
1568 if x != nil {1713 if x != nil {
1569 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1714 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1570 if ms.LoadMessageInfo() == nil {1715 if ms.LoadMessageInfo() == nil {
@@ -1577,7 +1722,7 @@ func (x *RoutePosition) ProtoReflect() protoreflect.Message {
1577 1722
1578 // Deprecated: Use RoutePosition.ProtoReflect.Descriptor instead.1723 // Deprecated: Use RoutePosition.ProtoReflect.Descriptor instead.
1579 func (*RoutePosition) Descriptor() ([]byte, []int) {1724 func (*RoutePosition) Descriptor() ([]byte, []int) {
1580- return file_session_proto_rawDescGZIP(), []int{22}1725+ return file_session_proto_rawDescGZIP(), []int{24}
1581 }1726 }
1582 1727
1583 func (x *RoutePosition) GetTarget() *ExactTarget {1728 func (x *RoutePosition) GetTarget() *ExactTarget {
@@ -1605,7 +1750,7 @@ type RouteRetired struct {
1605 1750
1606 func (x *RouteRetired) Reset() {1751 func (x *RouteRetired) Reset() {
1607 *x = RouteRetired{}1752 *x = RouteRetired{}
1608- mi := &file_session_proto_msgTypes[23]1753+ mi := &file_session_proto_msgTypes[25]
1609 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1754 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1610 ms.StoreMessageInfo(mi)1755 ms.StoreMessageInfo(mi)
1611 }1756 }
@@ -1617,7 +1762,7 @@ func (x *RouteRetired) String() string {
1617 func (*RouteRetired) ProtoMessage() {}1762 func (*RouteRetired) ProtoMessage() {}
1618 1763
1619 func (x *RouteRetired) ProtoReflect() protoreflect.Message {1764 func (x *RouteRetired) ProtoReflect() protoreflect.Message {
1620- mi := &file_session_proto_msgTypes[23]1765+ mi := &file_session_proto_msgTypes[25]
1621 if x != nil {1766 if x != nil {
1622 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1767 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1623 if ms.LoadMessageInfo() == nil {1768 if ms.LoadMessageInfo() == nil {
@@ -1630,7 +1775,7 @@ func (x *RouteRetired) ProtoReflect() protoreflect.Message {
1630 1775
1631 // Deprecated: Use RouteRetired.ProtoReflect.Descriptor instead.1776 // Deprecated: Use RouteRetired.ProtoReflect.Descriptor instead.
1632 func (*RouteRetired) Descriptor() ([]byte, []int) {1777 func (*RouteRetired) Descriptor() ([]byte, []int) {
1633- return file_session_proto_rawDescGZIP(), []int{23}1778+ return file_session_proto_rawDescGZIP(), []int{25}
1634 }1779 }
1635 1780
1636 func (x *RouteRetired) GetRef() *RouteRef {1781 func (x *RouteRetired) GetRef() *RouteRef {
@@ -1659,7 +1804,7 @@ type SamePeerSwitchRequest struct {
1659 1804
1660 func (x *SamePeerSwitchRequest) Reset() {1805 func (x *SamePeerSwitchRequest) Reset() {
1661 *x = SamePeerSwitchRequest{}1806 *x = SamePeerSwitchRequest{}
1662- mi := &file_session_proto_msgTypes[24]1807+ mi := &file_session_proto_msgTypes[26]
1663 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1808 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1664 ms.StoreMessageInfo(mi)1809 ms.StoreMessageInfo(mi)
1665 }1810 }
@@ -1671,7 +1816,7 @@ func (x *SamePeerSwitchRequest) String() string {
1671 func (*SamePeerSwitchRequest) ProtoMessage() {}1816 func (*SamePeerSwitchRequest) ProtoMessage() {}
1672 1817
1673 func (x *SamePeerSwitchRequest) ProtoReflect() protoreflect.Message {1818 func (x *SamePeerSwitchRequest) ProtoReflect() protoreflect.Message {
1674- mi := &file_session_proto_msgTypes[24]1819+ mi := &file_session_proto_msgTypes[26]
1675 if x != nil {1820 if x != nil {
1676 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1821 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1677 if ms.LoadMessageInfo() == nil {1822 if ms.LoadMessageInfo() == nil {
@@ -1684,7 +1829,7 @@ func (x *SamePeerSwitchRequest) ProtoReflect() protoreflect.Message {
1684 1829
1685 // Deprecated: Use SamePeerSwitchRequest.ProtoReflect.Descriptor instead.1830 // Deprecated: Use SamePeerSwitchRequest.ProtoReflect.Descriptor instead.
1686 func (*SamePeerSwitchRequest) Descriptor() ([]byte, []int) {1831 func (*SamePeerSwitchRequest) Descriptor() ([]byte, []int) {
1687- return file_session_proto_rawDescGZIP(), []int{24}1832+ return file_session_proto_rawDescGZIP(), []int{26}
1688 }1833 }
1689 1834
1690 func (x *SamePeerSwitchRequest) GetRequestId() uint64 {1835 func (x *SamePeerSwitchRequest) GetRequestId() uint64 {
@@ -1720,7 +1865,7 @@ type SamePeerSwitchFailure struct {
1720 1865
1721 func (x *SamePeerSwitchFailure) Reset() {1866 func (x *SamePeerSwitchFailure) Reset() {
1722 *x = SamePeerSwitchFailure{}1867 *x = SamePeerSwitchFailure{}
1723- mi := &file_session_proto_msgTypes[25]1868+ mi := &file_session_proto_msgTypes[27]
1724 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1869 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1725 ms.StoreMessageInfo(mi)1870 ms.StoreMessageInfo(mi)
1726 }1871 }
@@ -1732,7 +1877,7 @@ func (x *SamePeerSwitchFailure) String() string {
1732 func (*SamePeerSwitchFailure) ProtoMessage() {}1877 func (*SamePeerSwitchFailure) ProtoMessage() {}
1733 1878
1734 func (x *SamePeerSwitchFailure) ProtoReflect() protoreflect.Message {1879 func (x *SamePeerSwitchFailure) ProtoReflect() protoreflect.Message {
1735- mi := &file_session_proto_msgTypes[25]1880+ mi := &file_session_proto_msgTypes[27]
1736 if x != nil {1881 if x != nil {
1737 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1882 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1738 if ms.LoadMessageInfo() == nil {1883 if ms.LoadMessageInfo() == nil {
@@ -1745,7 +1890,7 @@ func (x *SamePeerSwitchFailure) ProtoReflect() protoreflect.Message {
1745 1890
1746 // Deprecated: Use SamePeerSwitchFailure.ProtoReflect.Descriptor instead.1891 // Deprecated: Use SamePeerSwitchFailure.ProtoReflect.Descriptor instead.
1747 func (*SamePeerSwitchFailure) Descriptor() ([]byte, []int) {1892 func (*SamePeerSwitchFailure) Descriptor() ([]byte, []int) {
1748- return file_session_proto_rawDescGZIP(), []int{25}1893+ return file_session_proto_rawDescGZIP(), []int{27}
1749 }1894 }
1750 1895
1751 func (x *SamePeerSwitchFailure) GetRequestId() uint64 {1896 func (x *SamePeerSwitchFailure) GetRequestId() uint64 {
@@ -1772,7 +1917,7 @@ type SuspendAttachment struct {
1772 1917
1773 func (x *SuspendAttachment) Reset() {1918 func (x *SuspendAttachment) Reset() {
1774 *x = SuspendAttachment{}1919 *x = SuspendAttachment{}
1775- mi := &file_session_proto_msgTypes[26]1920+ mi := &file_session_proto_msgTypes[28]
1776 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1921 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1777 ms.StoreMessageInfo(mi)1922 ms.StoreMessageInfo(mi)
1778 }1923 }
@@ -1784,7 +1929,7 @@ func (x *SuspendAttachment) String() string {
1784 func (*SuspendAttachment) ProtoMessage() {}1929 func (*SuspendAttachment) ProtoMessage() {}
1785 1930
1786 func (x *SuspendAttachment) ProtoReflect() protoreflect.Message {1931 func (x *SuspendAttachment) ProtoReflect() protoreflect.Message {
1787- mi := &file_session_proto_msgTypes[26]1932+ mi := &file_session_proto_msgTypes[28]
1788 if x != nil {1933 if x != nil {
1789 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1934 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1790 if ms.LoadMessageInfo() == nil {1935 if ms.LoadMessageInfo() == nil {
@@ -1797,7 +1942,7 @@ func (x *SuspendAttachment) ProtoReflect() protoreflect.Message {
1797 1942
1798 // Deprecated: Use SuspendAttachment.ProtoReflect.Descriptor instead.1943 // Deprecated: Use SuspendAttachment.ProtoReflect.Descriptor instead.
1799 func (*SuspendAttachment) Descriptor() ([]byte, []int) {1944 func (*SuspendAttachment) Descriptor() ([]byte, []int) {
1800- return file_session_proto_rawDescGZIP(), []int{26}1945+ return file_session_proto_rawDescGZIP(), []int{28}
1801 }1946 }
1802 1947
1803 func (x *SuspendAttachment) GetRequestId() uint64 {1948 func (x *SuspendAttachment) GetRequestId() uint64 {
@@ -1818,7 +1963,7 @@ type AttachmentSuspended struct {
1818 1963
1819 func (x *AttachmentSuspended) Reset() {1964 func (x *AttachmentSuspended) Reset() {
1820 *x = AttachmentSuspended{}1965 *x = AttachmentSuspended{}
1821- mi := &file_session_proto_msgTypes[27]1966+ mi := &file_session_proto_msgTypes[29]
1822 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1967 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1823 ms.StoreMessageInfo(mi)1968 ms.StoreMessageInfo(mi)
1824 }1969 }
@@ -1830,7 +1975,7 @@ func (x *AttachmentSuspended) String() string {
1830 func (*AttachmentSuspended) ProtoMessage() {}1975 func (*AttachmentSuspended) ProtoMessage() {}
1831 1976
1832 func (x *AttachmentSuspended) ProtoReflect() protoreflect.Message {1977 func (x *AttachmentSuspended) ProtoReflect() protoreflect.Message {
1833- mi := &file_session_proto_msgTypes[27]1978+ mi := &file_session_proto_msgTypes[29]
1834 if x != nil {1979 if x != nil {
1835 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1980 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1836 if ms.LoadMessageInfo() == nil {1981 if ms.LoadMessageInfo() == nil {
@@ -1843,7 +1988,7 @@ func (x *AttachmentSuspended) ProtoReflect() protoreflect.Message {
1843 1988
1844 // Deprecated: Use AttachmentSuspended.ProtoReflect.Descriptor instead.1989 // Deprecated: Use AttachmentSuspended.ProtoReflect.Descriptor instead.
1845 func (*AttachmentSuspended) Descriptor() ([]byte, []int) {1990 func (*AttachmentSuspended) Descriptor() ([]byte, []int) {
1846- return file_session_proto_rawDescGZIP(), []int{27}1991+ return file_session_proto_rawDescGZIP(), []int{29}
1847 }1992 }
1848 1993
1849 func (x *AttachmentSuspended) GetRequestId() uint64 {1994 func (x *AttachmentSuspended) GetRequestId() uint64 {
@@ -1875,7 +2020,7 @@ type ActivateAttachment struct {
1875 2020
1876 func (x *ActivateAttachment) Reset() {2021 func (x *ActivateAttachment) Reset() {
1877 *x = ActivateAttachment{}2022 *x = ActivateAttachment{}
1878- mi := &file_session_proto_msgTypes[28]2023+ mi := &file_session_proto_msgTypes[30]
1879 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))2024 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1880 ms.StoreMessageInfo(mi)2025 ms.StoreMessageInfo(mi)
1881 }2026 }
@@ -1887,7 +2032,7 @@ func (x *ActivateAttachment) String() string {
1887 func (*ActivateAttachment) ProtoMessage() {}2032 func (*ActivateAttachment) ProtoMessage() {}
1888 2033
1889 func (x *ActivateAttachment) ProtoReflect() protoreflect.Message {2034 func (x *ActivateAttachment) ProtoReflect() protoreflect.Message {
1890- mi := &file_session_proto_msgTypes[28]2035+ mi := &file_session_proto_msgTypes[30]
1891 if x != nil {2036 if x != nil {
1892 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))2037 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1893 if ms.LoadMessageInfo() == nil {2038 if ms.LoadMessageInfo() == nil {
@@ -1900,7 +2045,7 @@ func (x *ActivateAttachment) ProtoReflect() protoreflect.Message {
1900 2045
1901 // Deprecated: Use ActivateAttachment.ProtoReflect.Descriptor instead.2046 // Deprecated: Use ActivateAttachment.ProtoReflect.Descriptor instead.
1902 func (*ActivateAttachment) Descriptor() ([]byte, []int) {2047 func (*ActivateAttachment) Descriptor() ([]byte, []int) {
1903- return file_session_proto_rawDescGZIP(), []int{28}2048+ return file_session_proto_rawDescGZIP(), []int{30}
1904 }2049 }
1905 2050
1906 func (x *ActivateAttachment) GetRequestId() uint64 {2051 func (x *ActivateAttachment) GetRequestId() uint64 {
@@ -1961,7 +2106,7 @@ type AttachmentActivated struct {
1961 2106
1962 func (x *AttachmentActivated) Reset() {2107 func (x *AttachmentActivated) Reset() {
1963 *x = AttachmentActivated{}2108 *x = AttachmentActivated{}
1964- mi := &file_session_proto_msgTypes[29]2109+ mi := &file_session_proto_msgTypes[31]
1965 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))2110 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1966 ms.StoreMessageInfo(mi)2111 ms.StoreMessageInfo(mi)
1967 }2112 }
@@ -1973,7 +2118,7 @@ func (x *AttachmentActivated) String() string {
1973 func (*AttachmentActivated) ProtoMessage() {}2118 func (*AttachmentActivated) ProtoMessage() {}
1974 2119
1975 func (x *AttachmentActivated) ProtoReflect() protoreflect.Message {2120 func (x *AttachmentActivated) ProtoReflect() protoreflect.Message {
1976- mi := &file_session_proto_msgTypes[29]2121+ mi := &file_session_proto_msgTypes[31]
1977 if x != nil {2122 if x != nil {
1978 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))2123 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1979 if ms.LoadMessageInfo() == nil {2124 if ms.LoadMessageInfo() == nil {
@@ -1986,7 +2131,7 @@ func (x *AttachmentActivated) ProtoReflect() protoreflect.Message {
1986 2131
1987 // Deprecated: Use AttachmentActivated.ProtoReflect.Descriptor instead.2132 // Deprecated: Use AttachmentActivated.ProtoReflect.Descriptor instead.
1988 func (*AttachmentActivated) Descriptor() ([]byte, []int) {2133 func (*AttachmentActivated) Descriptor() ([]byte, []int) {
1989- return file_session_proto_rawDescGZIP(), []int{29}2134+ return file_session_proto_rawDescGZIP(), []int{31}
1990 }2135 }
1991 2136
1992 func (x *AttachmentActivated) GetRequestId() uint64 {2137 func (x *AttachmentActivated) GetRequestId() uint64 {
@@ -2090,10 +2235,24 @@ const file_session_proto_rawDesc = "" +
2090 "\x04tabs\x18\x05 \x01(\rR\x04tabs\x12\x1a\n" +2235 "\x04tabs\x18\x05 \x01(\rR\x04tabs\x12\x1a\n" +
2091 "\battached\x18\x06 \x01(\bR\battached\"@\n" +2236 "\battached\x18\x06 \x01(\bR\battached\"@\n" +
2092 "\bSessions\x124\n" +2237 "\bSessions\x124\n" +
2093- "\bsessions\x18\x01 \x03(\v2\x18.vev.wire.v1.SessionInfoR\bsessions\"0\n" +2238+ "\bsessions\x18\x01 \x03(\v2\x18.vev.wire.v1.SessionInfoR\bsessions\"O\n" +
2094 "\x04Kill\x12\x12\n" +2239 "\x04Kill\x12\x12\n" +
2095 "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +2240 "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
2096- "\x05scope\x18\x02 \x01(\rR\x05scope\"\"\n" +2241+ "\x05scope\x18\x02 \x01(\rR\x05scope\x12\x1d\n" +
2242+ "\n" +
2243+ "request_id\x18\x03 \x01(\x04R\trequestId\"K\n" +
2244+ "\vKillFailure\x12\x14\n" +
2245+ "\x05class\x18\x01 \x01(\tR\x05class\x12\x12\n" +
2246+ "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" +
2247+ "\x04text\x18\x03 \x01(\tR\x04text\"\xa3\x01\n" +
2248+ "\n" +
2249+ "KillResult\x12\x1d\n" +
2250+ "\n" +
2251+ "request_id\x18\x01 \x01(\x04R\trequestId\x12\x18\n" +
2252+ "\aoutcome\x18\x02 \x01(\rR\aoutcome\x12\x12\n" +
2253+ "\x04code\x18\x03 \x01(\rR\x04code\x12\x12\n" +
2254+ "\x04text\x18\x04 \x01(\tR\x04text\x124\n" +
2255+ "\bfailures\x18\x05 \x03(\v2\x18.vev.wire.v1.KillFailureR\bfailures\"\"\n" +
2097 "\bDetached\x12\x16\n" +2256 "\bDetached\x12\x16\n" +
2098 "\x06reason\x18\x01 \x01(\rR\x06reason\"\b\n" +2257 "\x06reason\x18\x01 \x01(\rR\x06reason\"\b\n" +
2099 "\x06Detach\"\x06\n" +2258 "\x06Detach\"\x06\n" +
@@ -2212,7 +2371,7 @@ func file_session_proto_rawDescGZIP() []byte {
2212 return file_session_proto_rawDescData2371 return file_session_proto_rawDescData
2213 }2372 }
2214 2373
2215-var file_session_proto_msgTypes = make([]protoimpl.MessageInfo, 30)2374+var file_session_proto_msgTypes = make([]protoimpl.MessageInfo, 32)
2216 var file_session_proto_goTypes = []any{2375 var file_session_proto_goTypes = []any{
2217 (*Hello)(nil), // 0: vev.wire.v1.Hello2376 (*Hello)(nil), // 0: vev.wire.v1.Hello
2218 (*Welcome)(nil), // 1: vev.wire.v1.Welcome2377 (*Welcome)(nil), // 1: vev.wire.v1.Welcome
@@ -2222,60 +2381,63 @@ var file_session_proto_goTypes = []any{
2222 (*SessionInfo)(nil), // 5: vev.wire.v1.SessionInfo2381 (*SessionInfo)(nil), // 5: vev.wire.v1.SessionInfo
2223 (*Sessions)(nil), // 6: vev.wire.v1.Sessions2382 (*Sessions)(nil), // 6: vev.wire.v1.Sessions
2224 (*Kill)(nil), // 7: vev.wire.v1.Kill2383 (*Kill)(nil), // 7: vev.wire.v1.Kill
2225- (*Detached)(nil), // 8: vev.wire.v1.Detached2384+ (*KillFailure)(nil), // 8: vev.wire.v1.KillFailure
2226- (*Detach)(nil), // 9: vev.wire.v1.Detach2385+ (*KillResult)(nil), // 9: vev.wire.v1.KillResult
2227- (*Ping)(nil), // 10: vev.wire.v1.Ping2386+ (*Detached)(nil), // 10: vev.wire.v1.Detached
2228- (*Pong)(nil), // 11: vev.wire.v1.Pong2387+ (*Detach)(nil), // 11: vev.wire.v1.Detach
2229- (*List)(nil), // 12: vev.wire.v1.List2388+ (*Ping)(nil), // 12: vev.wire.v1.Ping
2230- (*RouteRef)(nil), // 13: vev.wire.v1.RouteRef2389+ (*Pong)(nil), // 13: vev.wire.v1.Pong
2231- (*RouteAttentionTarget)(nil), // 14: vev.wire.v1.RouteAttentionTarget2390+ (*List)(nil), // 14: vev.wire.v1.List
2232- (*RouteAttentionSubscription)(nil), // 15: vev.wire.v1.RouteAttentionSubscription2391+ (*RouteRef)(nil), // 15: vev.wire.v1.RouteRef
2233- (*RecentRouteEntry)(nil), // 16: vev.wire.v1.RecentRouteEntry2392+ (*RouteAttentionTarget)(nil), // 16: vev.wire.v1.RouteAttentionTarget
2234- (*RecentRouteSnapshot)(nil), // 17: vev.wire.v1.RecentRouteSnapshot2393+ (*RouteAttentionSubscription)(nil), // 17: vev.wire.v1.RouteAttentionSubscription
2235- (*RouteNavigationAction)(nil), // 18: vev.wire.v1.RouteNavigationAction2394+ (*RecentRouteEntry)(nil), // 18: vev.wire.v1.RecentRouteEntry
2236- (*RouteCreateSessionAction)(nil), // 19: vev.wire.v1.RouteCreateSessionAction2395+ (*RecentRouteSnapshot)(nil), // 19: vev.wire.v1.RecentRouteSnapshot
2237- (*RouteNavigationFailure)(nil), // 20: vev.wire.v1.RouteNavigationFailure2396+ (*RouteNavigationAction)(nil), // 20: vev.wire.v1.RouteNavigationAction
2238- (*SessionCreationFailure)(nil), // 21: vev.wire.v1.SessionCreationFailure2397+ (*RouteCreateSessionAction)(nil), // 21: vev.wire.v1.RouteCreateSessionAction
2239- (*RoutePosition)(nil), // 22: vev.wire.v1.RoutePosition2398+ (*RouteNavigationFailure)(nil), // 22: vev.wire.v1.RouteNavigationFailure
2240- (*RouteRetired)(nil), // 23: vev.wire.v1.RouteRetired2399+ (*SessionCreationFailure)(nil), // 23: vev.wire.v1.SessionCreationFailure
2241- (*SamePeerSwitchRequest)(nil), // 24: vev.wire.v1.SamePeerSwitchRequest2400+ (*RoutePosition)(nil), // 24: vev.wire.v1.RoutePosition
2242- (*SamePeerSwitchFailure)(nil), // 25: vev.wire.v1.SamePeerSwitchFailure2401+ (*RouteRetired)(nil), // 25: vev.wire.v1.RouteRetired
2243- (*SuspendAttachment)(nil), // 26: vev.wire.v1.SuspendAttachment2402+ (*SamePeerSwitchRequest)(nil), // 26: vev.wire.v1.SamePeerSwitchRequest
2244- (*AttachmentSuspended)(nil), // 27: vev.wire.v1.AttachmentSuspended2403+ (*SamePeerSwitchFailure)(nil), // 27: vev.wire.v1.SamePeerSwitchFailure
2245- (*ActivateAttachment)(nil), // 28: vev.wire.v1.ActivateAttachment2404+ (*SuspendAttachment)(nil), // 28: vev.wire.v1.SuspendAttachment
2246- (*AttachmentActivated)(nil), // 29: vev.wire.v1.AttachmentActivated2405+ (*AttachmentSuspended)(nil), // 29: vev.wire.v1.AttachmentSuspended
2247- (*RemoteTarget)(nil), // 30: vev.wire.v1.RemoteTarget2406+ (*ActivateAttachment)(nil), // 30: vev.wire.v1.ActivateAttachment
2248- (*ExactTarget)(nil), // 31: vev.wire.v1.ExactTarget2407+ (*AttachmentActivated)(nil), // 31: vev.wire.v1.AttachmentActivated
2408+ (*RemoteTarget)(nil), // 32: vev.wire.v1.RemoteTarget
2409+ (*ExactTarget)(nil), // 33: vev.wire.v1.ExactTarget
2249 }2410 }
2250 var file_session_proto_depIdxs = []int32{2411 var file_session_proto_depIdxs = []int32{
2251- 30, // 0: vev.wire.v1.Hello.remote_target:type_name -> vev.wire.v1.RemoteTarget2412+ 32, // 0: vev.wire.v1.Hello.remote_target:type_name -> vev.wire.v1.RemoteTarget
2252- 31, // 1: vev.wire.v1.Hello.exact_target:type_name -> vev.wire.v1.ExactTarget2413+ 33, // 1: vev.wire.v1.Hello.exact_target:type_name -> vev.wire.v1.ExactTarget
2253 3, // 2: vev.wire.v1.Welcome.committed_identity:type_name -> vev.wire.v1.CommittedRouteIdentity2414 3, // 2: vev.wire.v1.Welcome.committed_identity:type_name -> vev.wire.v1.CommittedRouteIdentity
2254- 31, // 3: vev.wire.v1.CommittedRouteIdentity.target:type_name -> vev.wire.v1.ExactTarget2415+ 33, // 3: vev.wire.v1.CommittedRouteIdentity.target:type_name -> vev.wire.v1.ExactTarget
2255- 30, // 4: vev.wire.v1.AttachTarget.remote_target:type_name -> vev.wire.v1.RemoteTarget2416+ 32, // 4: vev.wire.v1.AttachTarget.remote_target:type_name -> vev.wire.v1.RemoteTarget
2256- 31, // 5: vev.wire.v1.AttachTarget.exact_target:type_name -> vev.wire.v1.ExactTarget2417+ 33, // 5: vev.wire.v1.AttachTarget.exact_target:type_name -> vev.wire.v1.ExactTarget
2257 5, // 6: vev.wire.v1.Sessions.sessions:type_name -> vev.wire.v1.SessionInfo2418 5, // 6: vev.wire.v1.Sessions.sessions:type_name -> vev.wire.v1.SessionInfo
2258- 13, // 7: vev.wire.v1.RouteAttentionTarget.ref:type_name -> vev.wire.v1.RouteRef2419+ 8, // 7: vev.wire.v1.KillResult.failures:type_name -> vev.wire.v1.KillFailure
2259- 31, // 8: vev.wire.v1.RouteAttentionTarget.target:type_name -> vev.wire.v1.ExactTarget2420+ 15, // 8: vev.wire.v1.RouteAttentionTarget.ref:type_name -> vev.wire.v1.RouteRef
2260- 14, // 9: vev.wire.v1.RouteAttentionSubscription.targets:type_name -> vev.wire.v1.RouteAttentionTarget2421+ 33, // 9: vev.wire.v1.RouteAttentionTarget.target:type_name -> vev.wire.v1.ExactTarget
2261- 31, // 10: vev.wire.v1.RecentRouteEntry.target:type_name -> vev.wire.v1.ExactTarget2422+ 16, // 10: vev.wire.v1.RouteAttentionSubscription.targets:type_name -> vev.wire.v1.RouteAttentionTarget
2262- 13, // 11: vev.wire.v1.RecentRouteSnapshot.active:type_name -> vev.wire.v1.RouteRef2423+ 33, // 11: vev.wire.v1.RecentRouteEntry.target:type_name -> vev.wire.v1.ExactTarget
2263- 16, // 12: vev.wire.v1.RecentRouteSnapshot.active_entry:type_name -> vev.wire.v1.RecentRouteEntry2424+ 15, // 12: vev.wire.v1.RecentRouteSnapshot.active:type_name -> vev.wire.v1.RouteRef
2264- 13, // 13: vev.wire.v1.RecentRouteSnapshot.previous:type_name -> vev.wire.v1.RouteRef2425+ 18, // 13: vev.wire.v1.RecentRouteSnapshot.active_entry:type_name -> vev.wire.v1.RecentRouteEntry
2265- 13, // 14: vev.wire.v1.RecentRouteSnapshot.home:type_name -> vev.wire.v1.RouteRef2426+ 15, // 14: vev.wire.v1.RecentRouteSnapshot.previous:type_name -> vev.wire.v1.RouteRef
2266- 16, // 15: vev.wire.v1.RecentRouteSnapshot.entries:type_name -> vev.wire.v1.RecentRouteEntry2427+ 15, // 15: vev.wire.v1.RecentRouteSnapshot.home:type_name -> vev.wire.v1.RouteRef
2267- 31, // 16: vev.wire.v1.RoutePosition.target:type_name -> vev.wire.v1.ExactTarget2428+ 18, // 16: vev.wire.v1.RecentRouteSnapshot.entries:type_name -> vev.wire.v1.RecentRouteEntry
2268- 13, // 17: vev.wire.v1.RouteRetired.ref:type_name -> vev.wire.v1.RouteRef2429+ 33, // 17: vev.wire.v1.RoutePosition.target:type_name -> vev.wire.v1.ExactTarget
2269- 31, // 18: vev.wire.v1.RouteRetired.target:type_name -> vev.wire.v1.ExactTarget2430+ 15, // 18: vev.wire.v1.RouteRetired.ref:type_name -> vev.wire.v1.RouteRef
2270- 31, // 19: vev.wire.v1.SamePeerSwitchRequest.target:type_name -> vev.wire.v1.ExactTarget2431+ 33, // 19: vev.wire.v1.RouteRetired.target:type_name -> vev.wire.v1.ExactTarget
2271- 31, // 20: vev.wire.v1.AttachmentSuspended.target:type_name -> vev.wire.v1.ExactTarget2432+ 33, // 20: vev.wire.v1.SamePeerSwitchRequest.target:type_name -> vev.wire.v1.ExactTarget
2272- 31, // 21: vev.wire.v1.ActivateAttachment.target:type_name -> vev.wire.v1.ExactTarget2433+ 33, // 21: vev.wire.v1.AttachmentSuspended.target:type_name -> vev.wire.v1.ExactTarget
2273- 3, // 22: vev.wire.v1.AttachmentActivated.identity:type_name -> vev.wire.v1.CommittedRouteIdentity2434+ 33, // 22: vev.wire.v1.ActivateAttachment.target:type_name -> vev.wire.v1.ExactTarget
2274- 23, // [23:23] is the sub-list for method output_type2435+ 3, // 23: vev.wire.v1.AttachmentActivated.identity:type_name -> vev.wire.v1.CommittedRouteIdentity
2275- 23, // [23:23] is the sub-list for method input_type2436+ 24, // [24:24] is the sub-list for method output_type
2276- 23, // [23:23] is the sub-list for extension type_name2437+ 24, // [24:24] is the sub-list for method input_type
2277- 23, // [23:23] is the sub-list for extension extendee2438+ 24, // [24:24] is the sub-list for extension type_name
2278- 0, // [0:23] is the sub-list for field type_name2439+ 24, // [24:24] is the sub-list for extension extendee
2440+ 0, // [0:24] is the sub-list for field type_name
2279 }2441 }
2280 2442
2281 func init() { file_session_proto_init() }2443 func init() { file_session_proto_init() }
@@ -2290,7 +2452,7 @@ func file_session_proto_init() {
2290 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),2452 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
2291 RawDescriptor: unsafe.Slice(unsafe.StringData(file_session_proto_rawDesc), len(file_session_proto_rawDesc)),2453 RawDescriptor: unsafe.Slice(unsafe.StringData(file_session_proto_rawDesc), len(file_session_proto_rawDesc)),
2292 NumEnums: 0,2454 NumEnums: 0,
2293- NumMessages: 30,2455+ NumMessages: 32,
2294 NumExtensions: 0,2456 NumExtensions: 0,
2295 NumServices: 0,2457 NumServices: 0,
2296 },2458 },
M internal/usecase/client/typed_connection_test.go +2 -0
@@ -349,6 +349,8 @@ func serverMessageNameForTest(payload []byte) string {
349349 return "Sessions"
350350 case protocol.CommandResult:
351351 return "CommandResult"
352+ case protocol.KillResult:
353+ return "KillResult"
352354 case protocol.AttachTarget:
353355 return "AttachTarget"
354356 case protocol.RemotePreview:
@@ -349,6 +349,8 @@ func serverMessageNameForTest(payload []byte) string {
349 return "Sessions"349 return "Sessions"
350 case protocol.CommandResult:350 case protocol.CommandResult:
351 return "CommandResult"351 return "CommandResult"
352+ case protocol.KillResult:
353+ return "KillResult"
352 case protocol.AttachTarget:354 case protocol.AttachTarget:
353 return "AttachTarget"355 return "AttachTarget"
354 case protocol.RemotePreview:356 case protocol.RemotePreview:
M internal/usecase/client/wire_external_test_helpers_test.go +2 -0
@@ -147,6 +147,8 @@ func serverMessageNameForTest(payload []byte) string {
147147 return "Sessions"
148148 case protocol.CommandResult:
149149 return "CommandResult"
150+ case protocol.KillResult:
151+ return "KillResult"
150152 case protocol.AttachTarget:
151153 return "AttachTarget"
152154 case protocol.RemotePreview:
@@ -147,6 +147,8 @@ func serverMessageNameForTest(payload []byte) string {
147 return "Sessions"147 return "Sessions"
148 case protocol.CommandResult:148 case protocol.CommandResult:
149 return "CommandResult"149 return "CommandResult"
150+ case protocol.KillResult:
151+ return "KillResult"
150 case protocol.AttachTarget:152 case protocol.AttachTarget:
151 return "AttachTarget"153 return "AttachTarget"
152 case protocol.RemotePreview:154 case protocol.RemotePreview:
M internal/usecase/daemon/control.go +7 -5
@@ -87,12 +87,14 @@ func (d *Daemon) boundedControlSend(tr ports.ServerConnection, message protocol.
8787 return err
8888 }
8989
90-// logControlSendFailure records that an error response could not be delivered.
91-// A one-shot control client treats EOF as success, so a failed error send would
92-// otherwise let it infer success from a silent close.
93-func (d *Daemon) logControlSendFailure(operation string, err error) {
90+// logKillResultSendFailure records that one one-shot kill result could not be
91+// delivered. The kill path answers with an explicit correlated KillResult, so a
92+// failed result send leaves the client at outcome-unknown rather than letting it
93+// infer success from a silent close; the outcome and code fields preserve what
94+// the client never observed.
95+func (d *Daemon) logKillResultSendFailure(result protocol.KillResult, err error) {
9496 if err != nil {
95- d.log.Warn("control error response send failed", "operation", operation, "err", err)
97+ d.log.Warn("control error response send failed", "operation", "kill result", "outcome", result.Outcome, "code", result.Code, "err", err)
9698 }
9799 }
98100
@@ -87,12 +87,14 @@ func (d *Daemon) boundedControlSend(tr ports.ServerConnection, message protocol.
87 return err87 return err
88 }88 }
89 89
90-// logControlSendFailure records that an error response could not be delivered.90+// logKillResultSendFailure records that one one-shot kill result could not be
91-// A one-shot control client treats EOF as success, so a failed error send would91+// delivered. The kill path answers with an explicit correlated KillResult, so a
92-// otherwise let it infer success from a silent close.92+// failed result send leaves the client at outcome-unknown rather than letting it
93-func (d *Daemon) logControlSendFailure(operation string, err error) {93+// infer success from a silent close; the outcome and code fields preserve what
94+// the client never observed.
95+func (d *Daemon) logKillResultSendFailure(result protocol.KillResult, err error) {
94 if err != nil {96 if err != nil {
95- d.log.Warn("control error response send failed", "operation", operation, "err", err)97+ d.log.Warn("control error response send failed", "operation", "kill result", "outcome", result.Outcome, "code", result.Code, "err", err)
96 }98 }
97 }99 }
98 100
M internal/usecase/daemon/control_test.go +7 -5
@@ -1482,13 +1482,15 @@ func TestHandleKillSessionParticipantsChangedReportsRetryable(t *testing.T) {
14821482 t.Cleanup(func() { d.afterAttachmentEffectParticipantsSnapshotted = nil })
14831483
14841484 tr, sends := newCapturingTransport(t)
1485- d.handleKill(tr, protocol.Kill{Scope: protocol.KillSession, Name: "work"})
1485+ d.handleKill(tr, protocol.Kill{RequestID: 1, Scope: protocol.KillSession, Name: "work"})
14861486
1487- frame := awaitFrame(t, sends, "Error")
1488- msg, ok := decodeServerMessage(t, frame).(protocol.ErrorMsg)
1487+ frame := awaitFrame(t, sends, "KillResult")
1488+ result, ok := decodeServerMessage(t, frame).(protocol.KillResult)
14891489 require.True(t, ok)
1490- require.Equal(t, protocol.ErrInternal, msg.Code)
1491- require.Equal(t, sessionKillRetryMessage, msg.Text)
1490+ require.Equal(t, uint64(1), result.RequestID)
1491+ require.Equal(t, protocol.KillFailed, result.Outcome)
1492+ require.Equal(t, protocol.ErrInternal, result.Code)
1493+ require.Equal(t, sessionKillRetryMessage, result.Text)
14921494 require.Equal(t, 1, sessionCount(d), "an aborted direct kill must leave the session live")
14931495 require.Same(t, sess, firstSession(d))
14941496 }
@@ -1482,13 +1482,15 @@ func TestHandleKillSessionParticipantsChangedReportsRetryable(t *testing.T) {
1482 t.Cleanup(func() { d.afterAttachmentEffectParticipantsSnapshotted = nil })1482 t.Cleanup(func() { d.afterAttachmentEffectParticipantsSnapshotted = nil })
1483 1483
1484 tr, sends := newCapturingTransport(t)1484 tr, sends := newCapturingTransport(t)
1485- d.handleKill(tr, protocol.Kill{Scope: protocol.KillSession, Name: "work"})1485+ d.handleKill(tr, protocol.Kill{RequestID: 1, Scope: protocol.KillSession, Name: "work"})
1486 1486
1487- frame := awaitFrame(t, sends, "Error")1487+ frame := awaitFrame(t, sends, "KillResult")
1488- msg, ok := decodeServerMessage(t, frame).(protocol.ErrorMsg)1488+ result, ok := decodeServerMessage(t, frame).(protocol.KillResult)
1489 require.True(t, ok)1489 require.True(t, ok)
1490- require.Equal(t, protocol.ErrInternal, msg.Code)1490+ require.Equal(t, uint64(1), result.RequestID)
1491- require.Equal(t, sessionKillRetryMessage, msg.Text)1491+ require.Equal(t, protocol.KillFailed, result.Outcome)
1492+ require.Equal(t, protocol.ErrInternal, result.Code)
1493+ require.Equal(t, sessionKillRetryMessage, result.Text)
1492 require.Equal(t, 1, sessionCount(d), "an aborted direct kill must leave the session live")1494 require.Equal(t, 1, sessionCount(d), "an aborted direct kill must leave the session live")
1493 require.Same(t, sess, firstSession(d))1495 require.Same(t, sess, firstSession(d))
1494 }1496 }
M internal/usecase/daemon/daemon.go +25 -10
@@ -1326,29 +1326,40 @@ func (d *Daemon) handleList(tr ports.ServerConnection) {
13261326 }
13271327
13281328 // handleKill terminates the requested live session or stopped named session,
1329-// purges every session (KillAll), or stops the daemon (KillDaemon), and closes
1330-// the control connection; the resulting EOF is the client's success signal.
1331-// KillAll leaves the daemon active; KillDaemon is the explicit stop.
1329+// purges every session (KillAll), or accepts daemon shutdown (KillDaemon).
1330+// Every normally decoded request receives exactly one correlated result before
1331+// the control connection closes. KillAll leaves the daemon active.
13321332 func (d *Daemon) handleKill(tr ports.ServerConnection, request protocol.Kill) {
13331333 defer func() { _ = tr.Close() }()
1334+ send := func(result protocol.KillResult) {
1335+ result.RequestID = request.RequestID
1336+ d.logKillResultSendFailure(result, d.boundedControlSend(tr, result))
1337+ }
13341338
13351339 switch request.Scope {
13361340 case protocol.KillAll:
13371341 result := d.purgeAllSessions(protocol.ReasonSessionKilled)
13381342 if result.Superseded() {
1339- d.logControlSendFailure("kill all superseded response", d.boundedControlSend(tr, serverError(protocol.ErrServerShutdown, "daemon is shutting down")))
1343+ send(protocol.KillResult{Outcome: protocol.KillOutcomeUnknown, Code: protocol.ErrServerShutdown, Text: "daemon is shutting down"})
13401344 return
13411345 }
13421346 if result.Failed() {
1343- d.logControlSendFailure("kill all failure response", d.boundedControlSend(tr, serverError(protocol.ErrInternal, result.summary())))
1347+ failures := make([]protocol.KillFailure, 0, min(len(result.Failures), purgeSummaryFailureLimit))
1348+ for _, failure := range result.Failures[:min(len(result.Failures), purgeSummaryFailureLimit)] {
1349+ failures = append(failures, protocol.KillFailure{Class: failure.Class.String(), Name: failure.Name, Text: failure.Err.Error()})
1350+ }
1351+ send(protocol.KillResult{Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: result.summary(), Failures: failures})
1352+ return
13441353 }
1354+ send(protocol.KillResult{Outcome: protocol.KillSucceeded})
13451355 return
13461356 case protocol.KillDaemon:
1357+ send(protocol.KillResult{Outcome: protocol.KillSucceeded})
13471358 d.shutdownAll(protocol.ReasonServerShutdown)
13481359 return
13491360 case protocol.KillSession:
13501361 default:
1351- d.logControlSendFailure("invalid kill scope response", d.boundedControlSend(tr, serverError(protocol.ErrInternal, "invalid kill scope")))
1362+ send(protocol.KillResult{Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: "invalid kill scope"})
13521363 return
13531364 }
13541365
@@ -1361,15 +1372,17 @@ func (d *Daemon) handleKill(tr ports.ServerConnection, request protocol.Kill) {
13611372 // deletion order as live and offline purges.
13621373 if err := d.retryStoppedPurge(request.Name); err != nil {
13631374 d.log.Warn("deleting stopped session failed", "err", err, "session", request.Name)
1364- d.logControlSendFailure("stopped delete failure response", d.boundedControlSend(tr, serverError(protocol.ErrInternal, "deleting stopped session failed")))
1375+ send(protocol.KillResult{Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: "deleting stopped session failed"})
1376+ return
13651377 }
1378+ send(protocol.KillResult{Outcome: protocol.KillSucceeded})
13661379 return
13671380 }
13681381 }
13691382 d.mu.Unlock()
13701383
13711384 if target == nil {
1372- d.logControlSendFailure("no such session response", d.boundedControlSend(tr, serverError(protocol.ErrNoSuchSession, "no such session: "+request.Name)))
1385+ send(protocol.KillResult{Outcome: protocol.KillFailed, Code: protocol.ErrNoSuchSession, Text: "no such session: " + request.Name})
13731386 return
13741387 }
13751388 if err := d.killSession(target, protocol.ReasonSessionKilled, true); err != nil {
@@ -1377,11 +1390,13 @@ func (d *Daemon) handleKill(tr ports.ServerConnection, request protocol.Kill) {
13771390 // The exact session was not removed; a concurrent detach or token
13781391 // resume invalidated the teardown snapshot. Report the retryable
13791392 // outcome instead of a permanent delete failure.
1380- d.logControlSendFailure("session delete retry response", d.boundedControlSend(tr, serverError(protocol.ErrInternal, sessionKillRetryMessage)))
1393+ send(protocol.KillResult{Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: sessionKillRetryMessage})
13811394 return
13821395 }
1383- d.logControlSendFailure("session delete failure response", d.boundedControlSend(tr, serverError(protocol.ErrInternal, "deleting persisted session failed")))
1396+ send(protocol.KillResult{Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: "deleting persisted session failed"})
1397+ return
13841398 }
1399+ send(protocol.KillResult{Outcome: protocol.KillSucceeded})
13851400 }
13861401
13871402 // handleHello runs the attach handshake for direct package callers. Accepted
@@ -1326,29 +1326,40 @@ func (d *Daemon) handleList(tr ports.ServerConnection) {
1326 }1326 }
1327 1327
1328 // handleKill terminates the requested live session or stopped named session,1328 // handleKill terminates the requested live session or stopped named session,
1329-// purges every session (KillAll), or stops the daemon (KillDaemon), and closes1329+// purges every session (KillAll), or accepts daemon shutdown (KillDaemon).
1330-// the control connection; the resulting EOF is the client's success signal.1330+// Every normally decoded request receives exactly one correlated result before
1331-// KillAll leaves the daemon active; KillDaemon is the explicit stop.1331+// the control connection closes. KillAll leaves the daemon active.
1332 func (d *Daemon) handleKill(tr ports.ServerConnection, request protocol.Kill) {1332 func (d *Daemon) handleKill(tr ports.ServerConnection, request protocol.Kill) {
1333 defer func() { _ = tr.Close() }()1333 defer func() { _ = tr.Close() }()
1334+ send := func(result protocol.KillResult) {
1335+ result.RequestID = request.RequestID
1336+ d.logKillResultSendFailure(result, d.boundedControlSend(tr, result))
1337+ }
1334 1338
1335 switch request.Scope {1339 switch request.Scope {
1336 case protocol.KillAll:1340 case protocol.KillAll:
1337 result := d.purgeAllSessions(protocol.ReasonSessionKilled)1341 result := d.purgeAllSessions(protocol.ReasonSessionKilled)
1338 if result.Superseded() {1342 if result.Superseded() {
1339- d.logControlSendFailure("kill all superseded response", d.boundedControlSend(tr, serverError(protocol.ErrServerShutdown, "daemon is shutting down")))1343+ send(protocol.KillResult{Outcome: protocol.KillOutcomeUnknown, Code: protocol.ErrServerShutdown, Text: "daemon is shutting down"})
1340 return1344 return
1341 }1345 }
1342 if result.Failed() {1346 if result.Failed() {
1343- d.logControlSendFailure("kill all failure response", d.boundedControlSend(tr, serverError(protocol.ErrInternal, result.summary())))1347+ failures := make([]protocol.KillFailure, 0, min(len(result.Failures), purgeSummaryFailureLimit))
1348+ for _, failure := range result.Failures[:min(len(result.Failures), purgeSummaryFailureLimit)] {
1349+ failures = append(failures, protocol.KillFailure{Class: failure.Class.String(), Name: failure.Name, Text: failure.Err.Error()})
1350+ }
1351+ send(protocol.KillResult{Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: result.summary(), Failures: failures})
1352+ return
1344 }1353 }
1354+ send(protocol.KillResult{Outcome: protocol.KillSucceeded})
1345 return1355 return
1346 case protocol.KillDaemon:1356 case protocol.KillDaemon:
1357+ send(protocol.KillResult{Outcome: protocol.KillSucceeded})
1347 d.shutdownAll(protocol.ReasonServerShutdown)1358 d.shutdownAll(protocol.ReasonServerShutdown)
1348 return1359 return
1349 case protocol.KillSession:1360 case protocol.KillSession:
1350 default:1361 default:
1351- d.logControlSendFailure("invalid kill scope response", d.boundedControlSend(tr, serverError(protocol.ErrInternal, "invalid kill scope")))1362+ send(protocol.KillResult{Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: "invalid kill scope"})
1352 return1363 return
1353 }1364 }
1354 1365
@@ -1361,15 +1372,17 @@ func (d *Daemon) handleKill(tr ports.ServerConnection, request protocol.Kill) {
1361 // deletion order as live and offline purges.1372 // deletion order as live and offline purges.
1362 if err := d.retryStoppedPurge(request.Name); err != nil {1373 if err := d.retryStoppedPurge(request.Name); err != nil {
1363 d.log.Warn("deleting stopped session failed", "err", err, "session", request.Name)1374 d.log.Warn("deleting stopped session failed", "err", err, "session", request.Name)
1364- d.logControlSendFailure("stopped delete failure response", d.boundedControlSend(tr, serverError(protocol.ErrInternal, "deleting stopped session failed")))1375+ send(protocol.KillResult{Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: "deleting stopped session failed"})
1376+ return
1365 }1377 }
1378+ send(protocol.KillResult{Outcome: protocol.KillSucceeded})
1366 return1379 return
1367 }1380 }
1368 }1381 }
1369 d.mu.Unlock()1382 d.mu.Unlock()
1370 1383
1371 if target == nil {1384 if target == nil {
1372- d.logControlSendFailure("no such session response", d.boundedControlSend(tr, serverError(protocol.ErrNoSuchSession, "no such session: "+request.Name)))1385+ send(protocol.KillResult{Outcome: protocol.KillFailed, Code: protocol.ErrNoSuchSession, Text: "no such session: " + request.Name})
1373 return1386 return
1374 }1387 }
1375 if err := d.killSession(target, protocol.ReasonSessionKilled, true); err != nil {1388 if err := d.killSession(target, protocol.ReasonSessionKilled, true); err != nil {
@@ -1377,11 +1390,13 @@ func (d *Daemon) handleKill(tr ports.ServerConnection, request protocol.Kill) {
1377 // The exact session was not removed; a concurrent detach or token1390 // The exact session was not removed; a concurrent detach or token
1378 // resume invalidated the teardown snapshot. Report the retryable1391 // resume invalidated the teardown snapshot. Report the retryable
1379 // outcome instead of a permanent delete failure.1392 // outcome instead of a permanent delete failure.
1380- d.logControlSendFailure("session delete retry response", d.boundedControlSend(tr, serverError(protocol.ErrInternal, sessionKillRetryMessage)))1393+ send(protocol.KillResult{Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: sessionKillRetryMessage})
1381 return1394 return
1382 }1395 }
1383- d.logControlSendFailure("session delete failure response", d.boundedControlSend(tr, serverError(protocol.ErrInternal, "deleting persisted session failed")))1396+ send(protocol.KillResult{Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: "deleting persisted session failed"})
1397+ return
1384 }1398 }
1399+ send(protocol.KillResult{Outcome: protocol.KillSucceeded})
1385 }1400 }
1386 1401
1387 // handleHello runs the attach handshake for direct package callers. Accepted1402 // handleHello runs the attach handshake for direct package callers. Accepted
A internal/usecase/daemon/kill_result_test.go +298 -0
@@ -0,0 +1,298 @@
1+package daemon
2+
3+// Explicit KillResult daemon contract. handleKill answers every normally
4+// decoded request with exactly one correlated KillResult before the control
5+// connection closes; a KillDaemon result is delivered before teardown begins.
6+// These tests are deterministic (no sleeps) and bounded by awaitFrame timeouts.
7+
8+import (
9+ "errors"
10+ "fmt"
11+ "io"
12+ "log/slog"
13+ "strconv"
14+ "sync"
15+ "testing"
16+
17+ "github.com/stretchr/testify/mock"
18+ "github.com/stretchr/testify/require"
19+
20+ "github.com/bnema/vev/internal/adapters/sessionwire"
21+ "github.com/bnema/vev/internal/domain"
22+ "github.com/bnema/vev/internal/protocol"
23+ "github.com/bnema/vev/internal/protocol/wire"
24+)
25+
26+func awaitKillResult(t *testing.T, sends chan wire.Envelope, requestID uint64) protocol.KillResult {
27+ t.Helper()
28+ frame := awaitFrame(t, sends, "KillResult")
29+ result, ok := decodeServerMessage(t, frame).(protocol.KillResult)
30+ require.True(t, ok, "expected a KillResult, got %T", decodeServerMessage(t, frame))
31+ require.Equal(t, requestID, result.RequestID, "result must correlate to its request")
32+ return result
33+}
34+
35+// requireNoFurtherSends proves the handler produced exactly one result.
36+func requireNoFurtherSends(t *testing.T, sends chan wire.Envelope) {
37+ t.Helper()
38+ select {
39+ case extra := <-sends:
40+ t.Fatalf("unexpected extra send %s", envelopeMessageName(t, extra.Payload))
41+ default:
42+ }
43+}
44+
45+// TestHandleKillSessionSendsExactlyOneSucceededResult proves a direct kill of a
46+// live session answers with one KillSucceeded result correlated to the request
47+// and removes the session.
48+func TestHandleKillSessionSendsExactlyOneSucceededResult(t *testing.T) {
49+ d := newTestDaemon(t, nil, stubClock{})
50+ addControlSession(d, "work", "t_work", "p_work")
51+
52+ tr, sends := newCapturingTransport(t)
53+ d.handleKill(tr, protocol.Kill{RequestID: 7, Scope: protocol.KillSession, Name: "work"})
54+
55+ result := awaitKillResult(t, sends, 7)
56+ require.Equal(t, protocol.KillResult{RequestID: 7, Outcome: protocol.KillSucceeded}, result)
57+ require.Zero(t, sessionCount(d), "a succeeded kill must remove the session")
58+ requireNoFurtherSends(t, sends)
59+}
60+
61+// TestHandleKillSessionSendsDefiniteFailureResult proves a kill of a missing
62+// session answers with one definite KillFailed result, not a close-as-success,
63+// and leaves the registry untouched.
64+func TestHandleKillSessionSendsDefiniteFailureResult(t *testing.T) {
65+ d := newTestDaemon(t, nil, stubClock{})
66+
67+ tr, sends := newCapturingTransport(t)
68+ d.handleKill(tr, protocol.Kill{RequestID: 9, Scope: protocol.KillSession, Name: "ghost"})
69+
70+ result := awaitKillResult(t, sends, 9)
71+ require.Equal(t, protocol.KillFailed, result.Outcome)
72+ require.Equal(t, protocol.ErrNoSuchSession, result.Code)
73+ require.Equal(t, "no such session: ghost", result.Text)
74+ requireNoFurtherSends(t, sends)
75+}
76+
77+// TestHandleKillInvalidScopeSendsDefiniteFailureResult proves an invalid scope
78+// still receives exactly one definite failure instead of a silent close.
79+func TestHandleKillInvalidScopeSendsDefiniteFailureResult(t *testing.T) {
80+ d := newTestDaemon(t, nil, stubClock{})
81+
82+ tr, sends := newCapturingTransport(t)
83+ d.handleKill(tr, protocol.Kill{RequestID: 3, Scope: protocol.KillScope(9)})
84+
85+ result := awaitKillResult(t, sends, 3)
86+ require.Equal(t, protocol.KillFailed, result.Outcome)
87+ require.Equal(t, protocol.ErrInternal, result.Code)
88+ require.Equal(t, "invalid kill scope", result.Text)
89+}
90+
91+// TestHandleKillAllEmptyDaemonSendsOneSucceededResult proves a successful
92+// kill-all answers with one KillSucceeded result and leaves the daemon serving.
93+func TestHandleKillAllEmptyDaemonSendsOneSucceededResult(t *testing.T) {
94+ d := newTestDaemon(t, nil, stubClock{})
95+
96+ tr, sends := newCapturingTransport(t)
97+ d.handleKill(tr, protocol.Kill{RequestID: 11, Scope: protocol.KillAll})
98+
99+ result := awaitKillResult(t, sends, 11)
100+ require.Equal(t, protocol.KillSucceeded, result.Outcome)
101+ select {
102+ case <-d.done:
103+ t.Fatal("kill-all must not end the daemon")
104+ default:
105+ }
106+ requireNoFurtherSends(t, sends)
107+}
108+
109+// TestHandleKillAllReportsBoundedStructuredPartialFailures proves a kill-all
110+// whose stopped records cannot be purged answers with one definite KillFailed
111+// result carrying the total count plus at most purgeSummaryFailureLimit
112+// structured failures, so a control response stays bounded.
113+func TestHandleKillAllReportsBoundedStructuredPartialFailures(t *testing.T) {
114+ const total = purgeSummaryFailureLimit + 3
115+
116+ fail := make(map[domain.IncarnationID]error, total)
117+ repository := newSelectivePurgeRepository(fail)
118+ d := newTestDaemon(t, nil, stubClock{})
119+ WithSnapshotRepository(repository)(d)
120+ store, _ := newMockStore(t)
121+ WithStore(t, store)(d)
122+
123+ for i := 0; i < total; i++ {
124+ incarnation := domain.IncarnationID{byte(i + 1)}
125+ fail[incarnation] = errors.New("delete failed")
126+ record := domain.CatalogueRecord{Name: fmt.Sprintf("s%02d", i), IncarnationID: incarnation, CreatedAt: int64(i + 1)}
127+ require.NoError(t, d.catalogue.Create(record))
128+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
129+ }
130+
131+ tr, sends := newCapturingTransport(t)
132+ d.handleKill(tr, protocol.Kill{RequestID: 13, Scope: protocol.KillAll})
133+
134+ result := awaitKillResult(t, sends, 13)
135+ require.Equal(t, protocol.KillFailed, result.Outcome)
136+ require.Equal(t, protocol.ErrInternal, result.Code)
137+ require.Len(t, result.Failures, purgeSummaryFailureLimit, "wire detail must be bounded")
138+ require.Contains(t, result.Text, fmt.Sprintf("%d record(s) could not be purged", total))
139+ require.Contains(t, result.Text, "more omitted")
140+ requireNoFurtherSends(t, sends)
141+}
142+
143+// killOrderTransport records the order of the KillResult send and the control
144+// close, and whether the daemon's shutdown signal was already closed when the
145+// result was written.
146+type killOrderTransport struct {
147+ mu sync.Mutex
148+ order []string
149+ resultBeforeTeardown bool
150+}
151+
152+func (r *killOrderTransport) record(name string) {
153+ r.mu.Lock()
154+ r.order = append(r.order, name)
155+ r.mu.Unlock()
156+}
157+
158+func (r *killOrderTransport) snapshot() ([]string, bool) {
159+ r.mu.Lock()
160+ defer r.mu.Unlock()
161+ return append([]string(nil), r.order...), r.resultBeforeTeardown
162+}
163+
164+func newKillOrderTransport(t *testing.T, teardown <-chan struct{}) (*mockServerConnection, *killOrderTransport) {
165+ t.Helper()
166+ recorder := &killOrderTransport{}
167+ tr := newMockServerConnection(t)
168+ tr.EXPECT().Send(mock.Anything).RunAndReturn(func(f wire.Envelope) error {
169+ name := envelopeMessageName(t, f.Payload)
170+ if name == "KillResult" {
171+ recorder.mu.Lock()
172+ recorder.order = append(recorder.order, "result")
173+ select {
174+ case <-teardown:
175+ default:
176+ recorder.resultBeforeTeardown = true
177+ }
178+ recorder.mu.Unlock()
179+ return nil
180+ }
181+ recorder.record(name)
182+ return nil
183+ }).Maybe()
184+ tr.EXPECT().Close().RunAndReturn(func() error {
185+ recorder.record("close")
186+ return nil
187+ }).Maybe()
188+ return tr, recorder
189+}
190+
191+// TestHandleKillDaemonSendsResultBeforeTeardownAndClose proves the explicit
192+// daemon-stop result is delivered before shutdown begins and before the control
193+// connection closes, so a client can observe success instead of inferring it
194+// from a close.
195+func TestHandleKillDaemonSendsResultBeforeTeardownAndClose(t *testing.T) {
196+ d := newTestDaemon(t, nil, stubClock{})
197+
198+ tr, recorder := newKillOrderTransport(t, d.done)
199+ d.handleKill(tr, protocol.Kill{RequestID: 42, Scope: protocol.KillDaemon})
200+
201+ order, beforeTeardown := recorder.snapshot()
202+ require.Equal(t, []string{"result", "close"}, order)
203+ require.True(t, beforeTeardown, "the daemon-stop result must be sent before teardown begins")
204+ select {
205+ case <-d.done:
206+ default:
207+ t.Fatal("KillDaemon must end the daemon")
208+ }
209+}
210+
211+// recordFailTransport records every sent envelope then fails, so a test can
212+// inspect the exact result an undeliverable control send attempted.
213+type recordFailTransport struct {
214+ mu sync.Mutex
215+ sent []wire.Envelope
216+ err error
217+}
218+
219+func (t *recordFailTransport) Send(envelope wire.Envelope) error {
220+ t.mu.Lock()
221+ t.sent = append(t.sent, envelope)
222+ t.mu.Unlock()
223+ return t.err
224+}
225+
226+func (*recordFailTransport) Recv() (wire.Envelope, error) { return wire.Envelope{}, io.EOF }
227+func (*recordFailTransport) Close() error { return nil }
228+
229+func (t *recordFailTransport) payloads() []wire.Envelope {
230+ t.mu.Lock()
231+ defer t.mu.Unlock()
232+ return append([]wire.Envelope(nil), t.sent...)
233+}
234+
235+// TestHandleKillAllSupersededByDaemonShutdownSendsOneUnknownOutcome proves a
236+// KillAll that an explicit daemon shutdown superseded answers with exactly one
237+// correlated KillOutcomeUnknown carrying ErrServerShutdown, records the outcome
238+// and code in the send-failure log, and sends nothing further (no success and no
239+// second result).
240+func TestHandleKillAllSupersededByDaemonShutdownSendsOneUnknownOutcome(t *testing.T) {
241+ d := newTestDaemon(t, nil, stubClock{})
242+ var logged syncedLogBuffer
243+ d.log = slog.New(slog.NewTextHandler(&logged, nil))
244+
245+ // An explicit stop already owns the lifecycle set, so the purge defers.
246+ d.mu.Lock()
247+ d.closing = true
248+ d.mu.Unlock()
249+
250+ recorder := &recordFailTransport{err: errors.New("client gone")}
251+ d.handleKill(&rawServerConnection{raw: recorder}, protocol.Kill{RequestID: 5, Scope: protocol.KillAll})
252+
253+ payloads := recorder.payloads()
254+ require.Len(t, payloads, 1, "a superseded kill-all must attempt exactly one result")
255+ message, err := sessionwire.DecodeServerEnvelope(payloads[0].Payload)
256+ require.NoError(t, err)
257+ result, ok := message.(protocol.KillResult)
258+ require.True(t, ok, "expected KillResult, got %T", message)
259+ require.Equal(t, protocol.KillResult{RequestID: 5, Outcome: protocol.KillOutcomeUnknown, Code: protocol.ErrServerShutdown, Text: "daemon is shutting down"}, result)
260+
261+ require.Contains(t, logged.String(), "kill result")
262+ require.Contains(t, logged.String(), "client gone")
263+ require.Contains(t, logged.String(), "outcome="+strconv.Itoa(int(protocol.KillOutcomeUnknown)), "the send-failure log must name the unobserved outcome")
264+ require.Contains(t, logged.String(), "code="+strconv.Itoa(int(protocol.ErrServerShutdown)), "the send-failure log must name the unobserved code")
265+}
266+
267+// TestKillAllUndeliverableResultIsLogged proves an explicit partial-failure
268+// result that cannot be delivered is logged (never silently closing as
269+// success) and still carries the bounded structured failures.
270+func TestKillAllUndeliverableResultIsLogged(t *testing.T) {
271+ repository := newSelectivePurgeRepository(map[domain.IncarnationID]error{{2}: errors.New("delete failed")})
272+ d := newTestDaemon(t, nil, stubClock{})
273+ var logged syncedLogBuffer
274+ d.log = slog.New(slog.NewTextHandler(&logged, nil))
275+ WithSnapshotRepository(repository)(d)
276+ store, _ := newMockStore(t)
277+ WithStore(t, store)(d)
278+
279+ record := domain.CatalogueRecord{Name: "stopped", IncarnationID: domain.IncarnationID{2}, CreatedAt: 7}
280+ require.NoError(t, d.catalogue.Create(record))
281+ d.inactive["stopped"] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
282+
283+ recorder := &recordFailTransport{err: errors.New("client gone")}
284+ d.handleKill(&rawServerConnection{raw: recorder}, protocol.Kill{RequestID: 1, Scope: protocol.KillAll})
285+
286+ require.Contains(t, logged.String(), "kill result")
287+ require.Contains(t, logged.String(), "client gone")
288+
289+ payloads := recorder.payloads()
290+ require.Len(t, payloads, 1, "exactly one result is attempted")
291+ message, err := sessionwire.DecodeServerEnvelope(payloads[0].Payload)
292+ require.NoError(t, err)
293+ result, ok := message.(protocol.KillResult)
294+ require.True(t, ok, "expected KillResult, got %T", message)
295+ require.Equal(t, protocol.KillFailed, result.Outcome)
296+ require.Len(t, result.Failures, 1)
297+ require.Equal(t, "stopped", result.Failures[0].Name)
298+}
@@ -0,0 +1,298 @@
1+package daemon
2+
3+// Explicit KillResult daemon contract. handleKill answers every normally
4+// decoded request with exactly one correlated KillResult before the control
5+// connection closes; a KillDaemon result is delivered before teardown begins.
6+// These tests are deterministic (no sleeps) and bounded by awaitFrame timeouts.
7+
8+import (
9+ "errors"
10+ "fmt"
11+ "io"
12+ "log/slog"
13+ "strconv"
14+ "sync"
15+ "testing"
16+
17+ "github.com/stretchr/testify/mock"
18+ "github.com/stretchr/testify/require"
19+
20+ "github.com/bnema/vev/internal/adapters/sessionwire"
21+ "github.com/bnema/vev/internal/domain"
22+ "github.com/bnema/vev/internal/protocol"
23+ "github.com/bnema/vev/internal/protocol/wire"
24+)
25+
26+func awaitKillResult(t *testing.T, sends chan wire.Envelope, requestID uint64) protocol.KillResult {
27+ t.Helper()
28+ frame := awaitFrame(t, sends, "KillResult")
29+ result, ok := decodeServerMessage(t, frame).(protocol.KillResult)
30+ require.True(t, ok, "expected a KillResult, got %T", decodeServerMessage(t, frame))
31+ require.Equal(t, requestID, result.RequestID, "result must correlate to its request")
32+ return result
33+}
34+
35+// requireNoFurtherSends proves the handler produced exactly one result.
36+func requireNoFurtherSends(t *testing.T, sends chan wire.Envelope) {
37+ t.Helper()
38+ select {
39+ case extra := <-sends:
40+ t.Fatalf("unexpected extra send %s", envelopeMessageName(t, extra.Payload))
41+ default:
42+ }
43+}
44+
45+// TestHandleKillSessionSendsExactlyOneSucceededResult proves a direct kill of a
46+// live session answers with one KillSucceeded result correlated to the request
47+// and removes the session.
48+func TestHandleKillSessionSendsExactlyOneSucceededResult(t *testing.T) {
49+ d := newTestDaemon(t, nil, stubClock{})
50+ addControlSession(d, "work", "t_work", "p_work")
51+
52+ tr, sends := newCapturingTransport(t)
53+ d.handleKill(tr, protocol.Kill{RequestID: 7, Scope: protocol.KillSession, Name: "work"})
54+
55+ result := awaitKillResult(t, sends, 7)
56+ require.Equal(t, protocol.KillResult{RequestID: 7, Outcome: protocol.KillSucceeded}, result)
57+ require.Zero(t, sessionCount(d), "a succeeded kill must remove the session")
58+ requireNoFurtherSends(t, sends)
59+}
60+
61+// TestHandleKillSessionSendsDefiniteFailureResult proves a kill of a missing
62+// session answers with one definite KillFailed result, not a close-as-success,
63+// and leaves the registry untouched.
64+func TestHandleKillSessionSendsDefiniteFailureResult(t *testing.T) {
65+ d := newTestDaemon(t, nil, stubClock{})
66+
67+ tr, sends := newCapturingTransport(t)
68+ d.handleKill(tr, protocol.Kill{RequestID: 9, Scope: protocol.KillSession, Name: "ghost"})
69+
70+ result := awaitKillResult(t, sends, 9)
71+ require.Equal(t, protocol.KillFailed, result.Outcome)
72+ require.Equal(t, protocol.ErrNoSuchSession, result.Code)
73+ require.Equal(t, "no such session: ghost", result.Text)
74+ requireNoFurtherSends(t, sends)
75+}
76+
77+// TestHandleKillInvalidScopeSendsDefiniteFailureResult proves an invalid scope
78+// still receives exactly one definite failure instead of a silent close.
79+func TestHandleKillInvalidScopeSendsDefiniteFailureResult(t *testing.T) {
80+ d := newTestDaemon(t, nil, stubClock{})
81+
82+ tr, sends := newCapturingTransport(t)
83+ d.handleKill(tr, protocol.Kill{RequestID: 3, Scope: protocol.KillScope(9)})
84+
85+ result := awaitKillResult(t, sends, 3)
86+ require.Equal(t, protocol.KillFailed, result.Outcome)
87+ require.Equal(t, protocol.ErrInternal, result.Code)
88+ require.Equal(t, "invalid kill scope", result.Text)
89+}
90+
91+// TestHandleKillAllEmptyDaemonSendsOneSucceededResult proves a successful
92+// kill-all answers with one KillSucceeded result and leaves the daemon serving.
93+func TestHandleKillAllEmptyDaemonSendsOneSucceededResult(t *testing.T) {
94+ d := newTestDaemon(t, nil, stubClock{})
95+
96+ tr, sends := newCapturingTransport(t)
97+ d.handleKill(tr, protocol.Kill{RequestID: 11, Scope: protocol.KillAll})
98+
99+ result := awaitKillResult(t, sends, 11)
100+ require.Equal(t, protocol.KillSucceeded, result.Outcome)
101+ select {
102+ case <-d.done:
103+ t.Fatal("kill-all must not end the daemon")
104+ default:
105+ }
106+ requireNoFurtherSends(t, sends)
107+}
108+
109+// TestHandleKillAllReportsBoundedStructuredPartialFailures proves a kill-all
110+// whose stopped records cannot be purged answers with one definite KillFailed
111+// result carrying the total count plus at most purgeSummaryFailureLimit
112+// structured failures, so a control response stays bounded.
113+func TestHandleKillAllReportsBoundedStructuredPartialFailures(t *testing.T) {
114+ const total = purgeSummaryFailureLimit + 3
115+
116+ fail := make(map[domain.IncarnationID]error, total)
117+ repository := newSelectivePurgeRepository(fail)
118+ d := newTestDaemon(t, nil, stubClock{})
119+ WithSnapshotRepository(repository)(d)
120+ store, _ := newMockStore(t)
121+ WithStore(t, store)(d)
122+
123+ for i := 0; i < total; i++ {
124+ incarnation := domain.IncarnationID{byte(i + 1)}
125+ fail[incarnation] = errors.New("delete failed")
126+ record := domain.CatalogueRecord{Name: fmt.Sprintf("s%02d", i), IncarnationID: incarnation, CreatedAt: int64(i + 1)}
127+ require.NoError(t, d.catalogue.Create(record))
128+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
129+ }
130+
131+ tr, sends := newCapturingTransport(t)
132+ d.handleKill(tr, protocol.Kill{RequestID: 13, Scope: protocol.KillAll})
133+
134+ result := awaitKillResult(t, sends, 13)
135+ require.Equal(t, protocol.KillFailed, result.Outcome)
136+ require.Equal(t, protocol.ErrInternal, result.Code)
137+ require.Len(t, result.Failures, purgeSummaryFailureLimit, "wire detail must be bounded")
138+ require.Contains(t, result.Text, fmt.Sprintf("%d record(s) could not be purged", total))
139+ require.Contains(t, result.Text, "more omitted")
140+ requireNoFurtherSends(t, sends)
141+}
142+
143+// killOrderTransport records the order of the KillResult send and the control
144+// close, and whether the daemon's shutdown signal was already closed when the
145+// result was written.
146+type killOrderTransport struct {
147+ mu sync.Mutex
148+ order []string
149+ resultBeforeTeardown bool
150+}
151+
152+func (r *killOrderTransport) record(name string) {
153+ r.mu.Lock()
154+ r.order = append(r.order, name)
155+ r.mu.Unlock()
156+}
157+
158+func (r *killOrderTransport) snapshot() ([]string, bool) {
159+ r.mu.Lock()
160+ defer r.mu.Unlock()
161+ return append([]string(nil), r.order...), r.resultBeforeTeardown
162+}
163+
164+func newKillOrderTransport(t *testing.T, teardown <-chan struct{}) (*mockServerConnection, *killOrderTransport) {
165+ t.Helper()
166+ recorder := &killOrderTransport{}
167+ tr := newMockServerConnection(t)
168+ tr.EXPECT().Send(mock.Anything).RunAndReturn(func(f wire.Envelope) error {
169+ name := envelopeMessageName(t, f.Payload)
170+ if name == "KillResult" {
171+ recorder.mu.Lock()
172+ recorder.order = append(recorder.order, "result")
173+ select {
174+ case <-teardown:
175+ default:
176+ recorder.resultBeforeTeardown = true
177+ }
178+ recorder.mu.Unlock()
179+ return nil
180+ }
181+ recorder.record(name)
182+ return nil
183+ }).Maybe()
184+ tr.EXPECT().Close().RunAndReturn(func() error {
185+ recorder.record("close")
186+ return nil
187+ }).Maybe()
188+ return tr, recorder
189+}
190+
191+// TestHandleKillDaemonSendsResultBeforeTeardownAndClose proves the explicit
192+// daemon-stop result is delivered before shutdown begins and before the control
193+// connection closes, so a client can observe success instead of inferring it
194+// from a close.
195+func TestHandleKillDaemonSendsResultBeforeTeardownAndClose(t *testing.T) {
196+ d := newTestDaemon(t, nil, stubClock{})
197+
198+ tr, recorder := newKillOrderTransport(t, d.done)
199+ d.handleKill(tr, protocol.Kill{RequestID: 42, Scope: protocol.KillDaemon})
200+
201+ order, beforeTeardown := recorder.snapshot()
202+ require.Equal(t, []string{"result", "close"}, order)
203+ require.True(t, beforeTeardown, "the daemon-stop result must be sent before teardown begins")
204+ select {
205+ case <-d.done:
206+ default:
207+ t.Fatal("KillDaemon must end the daemon")
208+ }
209+}
210+
211+// recordFailTransport records every sent envelope then fails, so a test can
212+// inspect the exact result an undeliverable control send attempted.
213+type recordFailTransport struct {
214+ mu sync.Mutex
215+ sent []wire.Envelope
216+ err error
217+}
218+
219+func (t *recordFailTransport) Send(envelope wire.Envelope) error {
220+ t.mu.Lock()
221+ t.sent = append(t.sent, envelope)
222+ t.mu.Unlock()
223+ return t.err
224+}
225+
226+func (*recordFailTransport) Recv() (wire.Envelope, error) { return wire.Envelope{}, io.EOF }
227+func (*recordFailTransport) Close() error { return nil }
228+
229+func (t *recordFailTransport) payloads() []wire.Envelope {
230+ t.mu.Lock()
231+ defer t.mu.Unlock()
232+ return append([]wire.Envelope(nil), t.sent...)
233+}
234+
235+// TestHandleKillAllSupersededByDaemonShutdownSendsOneUnknownOutcome proves a
236+// KillAll that an explicit daemon shutdown superseded answers with exactly one
237+// correlated KillOutcomeUnknown carrying ErrServerShutdown, records the outcome
238+// and code in the send-failure log, and sends nothing further (no success and no
239+// second result).
240+func TestHandleKillAllSupersededByDaemonShutdownSendsOneUnknownOutcome(t *testing.T) {
241+ d := newTestDaemon(t, nil, stubClock{})
242+ var logged syncedLogBuffer
243+ d.log = slog.New(slog.NewTextHandler(&logged, nil))
244+
245+ // An explicit stop already owns the lifecycle set, so the purge defers.
246+ d.mu.Lock()
247+ d.closing = true
248+ d.mu.Unlock()
249+
250+ recorder := &recordFailTransport{err: errors.New("client gone")}
251+ d.handleKill(&rawServerConnection{raw: recorder}, protocol.Kill{RequestID: 5, Scope: protocol.KillAll})
252+
253+ payloads := recorder.payloads()
254+ require.Len(t, payloads, 1, "a superseded kill-all must attempt exactly one result")
255+ message, err := sessionwire.DecodeServerEnvelope(payloads[0].Payload)
256+ require.NoError(t, err)
257+ result, ok := message.(protocol.KillResult)
258+ require.True(t, ok, "expected KillResult, got %T", message)
259+ require.Equal(t, protocol.KillResult{RequestID: 5, Outcome: protocol.KillOutcomeUnknown, Code: protocol.ErrServerShutdown, Text: "daemon is shutting down"}, result)
260+
261+ require.Contains(t, logged.String(), "kill result")
262+ require.Contains(t, logged.String(), "client gone")
263+ require.Contains(t, logged.String(), "outcome="+strconv.Itoa(int(protocol.KillOutcomeUnknown)), "the send-failure log must name the unobserved outcome")
264+ require.Contains(t, logged.String(), "code="+strconv.Itoa(int(protocol.ErrServerShutdown)), "the send-failure log must name the unobserved code")
265+}
266+
267+// TestKillAllUndeliverableResultIsLogged proves an explicit partial-failure
268+// result that cannot be delivered is logged (never silently closing as
269+// success) and still carries the bounded structured failures.
270+func TestKillAllUndeliverableResultIsLogged(t *testing.T) {
271+ repository := newSelectivePurgeRepository(map[domain.IncarnationID]error{{2}: errors.New("delete failed")})
272+ d := newTestDaemon(t, nil, stubClock{})
273+ var logged syncedLogBuffer
274+ d.log = slog.New(slog.NewTextHandler(&logged, nil))
275+ WithSnapshotRepository(repository)(d)
276+ store, _ := newMockStore(t)
277+ WithStore(t, store)(d)
278+
279+ record := domain.CatalogueRecord{Name: "stopped", IncarnationID: domain.IncarnationID{2}, CreatedAt: 7}
280+ require.NoError(t, d.catalogue.Create(record))
281+ d.inactive["stopped"] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
282+
283+ recorder := &recordFailTransport{err: errors.New("client gone")}
284+ d.handleKill(&rawServerConnection{raw: recorder}, protocol.Kill{RequestID: 1, Scope: protocol.KillAll})
285+
286+ require.Contains(t, logged.String(), "kill result")
287+ require.Contains(t, logged.String(), "client gone")
288+
289+ payloads := recorder.payloads()
290+ require.Len(t, payloads, 1, "exactly one result is attempted")
291+ message, err := sessionwire.DecodeServerEnvelope(payloads[0].Payload)
292+ require.NoError(t, err)
293+ result, ok := message.(protocol.KillResult)
294+ require.True(t, ok, "expected KillResult, got %T", message)
295+ require.Equal(t, protocol.KillFailed, result.Outcome)
296+ require.Len(t, result.Failures, 1)
297+ require.Equal(t, "stopped", result.Failures[0].Name)
298+}
M internal/usecase/daemon/purge_test.go +8 -4
@@ -6,6 +6,7 @@ import (
66 "errors"
77 "fmt"
88 "log/slog"
9+ "strconv"
910 "strings"
1011 "sync"
1112 "sync/atomic"
@@ -791,8 +792,9 @@ func TestPurgeAllSupersededByExplicitShutdownPreservesStopAuthority(t *testing.T
791792 d.waitNotifies()
792793 }
793794
794-// TestPurgeAllLogsFailedControlSend proves a purge failure that cannot be
795-// delivered is logged, so a client observing EOF cannot silently infer success.
795+// TestPurgeAllLogsFailedControlSend proves a purge failure result that cannot be
796+// delivered is logged, so a client that never observes the explicit correlated
797+// KillResult cannot silently infer success from a close.
796798 func TestPurgeAllLogsFailedControlSend(t *testing.T) {
797799 repository := newSelectivePurgeRepository(map[domain.IncarnationID]error{{2}: errors.New("delete failed")})
798800 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
@@ -809,10 +811,12 @@ func TestPurgeAllLogsFailedControlSend(t *testing.T) {
809811 tr := newMockServerConnection(t)
810812 tr.EXPECT().Send(mock.Anything).Return(errors.New("client gone")).Maybe()
811813 tr.EXPECT().Close().Return(nil).Maybe()
812- d.handleKill(tr, protocol.Kill{Scope: protocol.KillAll})
814+ d.handleKill(tr, protocol.Kill{RequestID: 1, Scope: protocol.KillAll})
813815
814- require.Contains(t, logged.String(), "kill all failure response")
816+ require.Contains(t, logged.String(), "kill result")
815817 require.Contains(t, logged.String(), "client gone")
818+ require.Contains(t, logged.String(), "outcome="+strconv.Itoa(int(protocol.KillFailed)), "the send-failure log must name the unobserved outcome")
819+ require.Contains(t, logged.String(), "code="+strconv.Itoa(int(protocol.ErrInternal)), "the send-failure log must name the unobserved code")
816820 }
817821
818822 // TestPurgeResultSummaryBoundsWireDetail proves the partial-failure summary is
@@ -6,6 +6,7 @@ import (
6 "errors"6 "errors"
7 "fmt"7 "fmt"
8 "log/slog"8 "log/slog"
9+ "strconv"
9 "strings"10 "strings"
10 "sync"11 "sync"
11 "sync/atomic"12 "sync/atomic"
@@ -791,8 +792,9 @@ func TestPurgeAllSupersededByExplicitShutdownPreservesStopAuthority(t *testing.T
791 d.waitNotifies()792 d.waitNotifies()
792 }793 }
793 794
794-// TestPurgeAllLogsFailedControlSend proves a purge failure that cannot be795+// TestPurgeAllLogsFailedControlSend proves a purge failure result that cannot be
795-// delivered is logged, so a client observing EOF cannot silently infer success.796+// delivered is logged, so a client that never observes the explicit correlated
797+// KillResult cannot silently infer success from a close.
796 func TestPurgeAllLogsFailedControlSend(t *testing.T) {798 func TestPurgeAllLogsFailedControlSend(t *testing.T) {
797 repository := newSelectivePurgeRepository(map[domain.IncarnationID]error{{2}: errors.New("delete failed")})799 repository := newSelectivePurgeRepository(map[domain.IncarnationID]error{{2}: errors.New("delete failed")})
798 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})800 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
@@ -809,10 +811,12 @@ func TestPurgeAllLogsFailedControlSend(t *testing.T) {
809 tr := newMockServerConnection(t)811 tr := newMockServerConnection(t)
810 tr.EXPECT().Send(mock.Anything).Return(errors.New("client gone")).Maybe()812 tr.EXPECT().Send(mock.Anything).Return(errors.New("client gone")).Maybe()
811 tr.EXPECT().Close().Return(nil).Maybe()813 tr.EXPECT().Close().Return(nil).Maybe()
812- d.handleKill(tr, protocol.Kill{Scope: protocol.KillAll})814+ d.handleKill(tr, protocol.Kill{RequestID: 1, Scope: protocol.KillAll})
813 815
814- require.Contains(t, logged.String(), "kill all failure response")816+ require.Contains(t, logged.String(), "kill result")
815 require.Contains(t, logged.String(), "client gone")817 require.Contains(t, logged.String(), "client gone")
818+ require.Contains(t, logged.String(), "outcome="+strconv.Itoa(int(protocol.KillFailed)), "the send-failure log must name the unobserved outcome")
819+ require.Contains(t, logged.String(), "code="+strconv.Itoa(int(protocol.ErrInternal)), "the send-failure log must name the unobserved code")
816 }820 }
817 821
818 // TestPurgeResultSummaryBoundsWireDetail proves the partial-failure summary is822 // TestPurgeResultSummaryBoundsWireDetail proves the partial-failure summary is
M internal/usecase/daemon/session_test.go +1 -1
@@ -144,7 +144,7 @@ func TestKillAllEmptyDaemonKeepsServing(t *testing.T) {
144144 pty, release := newBlockingPTY(t)
145145 defer release()
146146 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
147- tr, _, _ := newConn(t, mustClientEnvelope(protocol.Kill{Scope: protocol.KillAll}))
147+ tr, _, _ := newConn(t, mustClientEnvelope(protocol.Kill{RequestID: 1, Scope: protocol.KillAll}))
148148 d.handleConn(tr)
149149
150150 select {
@@ -144,7 +144,7 @@ func TestKillAllEmptyDaemonKeepsServing(t *testing.T) {
144 pty, release := newBlockingPTY(t)144 pty, release := newBlockingPTY(t)
145 defer release()145 defer release()
146 d := newTestDaemon(t, newFactory(t, pty), stubClock{})146 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
147- tr, _, _ := newConn(t, mustClientEnvelope(protocol.Kill{Scope: protocol.KillAll}))147+ tr, _, _ := newConn(t, mustClientEnvelope(protocol.Kill{RequestID: 1, Scope: protocol.KillAll}))
148 d.handleConn(tr)148 d.handleConn(tr)
149 149
150 select {150 select {
M internal/usecase/daemon/typed_connection_test.go +2 -0
@@ -159,6 +159,8 @@ func serverMessageName(message protocol.ServerMessage) string {
159159 return "Sessions"
160160 case protocol.CommandResult:
161161 return "CommandResult"
162+ case protocol.KillResult:
163+ return "KillResult"
162164 case protocol.AttachTarget:
163165 return "AttachTarget"
164166 case protocol.RemotePreview:
@@ -159,6 +159,8 @@ func serverMessageName(message protocol.ServerMessage) string {
159 return "Sessions"159 return "Sessions"
160 case protocol.CommandResult:160 case protocol.CommandResult:
161 return "CommandResult"161 return "CommandResult"
162+ case protocol.KillResult:
163+ return "KillResult"
162 case protocol.AttachTarget:164 case protocol.AttachTarget:
163 return "AttachTarget"165 return "AttachTarget"
164 case protocol.RemotePreview:166 case protocol.RemotePreview:

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close