detached b21386d

refactor(protocol): extract session control contracts (#233)

* refactor(protocol): extract session control contracts

* docs(architecture): record session contract ownership
commit
b21386ddeb77d3332261a35a70adec59ce534281
author
bnema · · 2026-08-28 11:44 UTC
committer
GitHub · · 2026-08-28 11:44 UTC
parent
be1cf92
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
122 files · +2209 -2141
M AGENTS.md +1 -1
@@ -61,7 +61,7 @@ Before touching daemon teardown paths, read the lock-ordering notes at the top o
6161
6262 ## Wire protocol
6363
64-Semantic presentation and route values live in `internal/protocol`; session-control values live in `internal/ports/session_control.go`. Frame IDs and codecs live in `internal/ports/frame.go`, `internal/ports/wire.go`, `internal/ports/routes_wire.go`, and `internal/ports/wire_command.go`. Connection framing lives in `internal/adapters/ipc/transport.go`.
64+Semantic presentation, route, and session-control values live in `internal/protocol`. Frame IDs and codecs live in `internal/ports/frame.go`, `internal/ports/wire.go`, `internal/ports/routes_wire.go`, and `internal/ports/wire_command.go`. Connection framing lives in `internal/adapters/ipc/transport.go`.
6565
6666 - IPC frames on a connection are 4-byte big-endian length, 1 type byte, then payload.
6767 - Client message types occupy `1–13`, `15`, `32–33`, and `35` (`MsgParkedRouteRequest`); server types occupy `16–23`, `25–31`, `34`, and `36` (`MsgParkedRouteResponse`). Types `14` and `24` remain reserved.
M internal/adapters/dgram/proxy_test.go +4 -4
@@ -89,7 +89,7 @@ func TestProxyRuntimeClampsDatagramHelloOutputWindowAndPreservesControl(t *testi
8989 daemon := newFakeTransport()
9090 errCh := make(chan error, 1)
9191 go func() { errCh <- ProxyRuntime{Client: client, Daemon: daemon, IdleTTL: time.Hour}.Run(t.Context()) }()
92- hello := ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, MaxOutputInFlight: requested}
92+ hello := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, MaxOutputInFlight: requested}
9393 client.recv <- recvResult{frame: ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}}
9494
9595 select {
@@ -490,7 +490,7 @@ func TestProxyCopierStopsRetryingWhenContextCanceled(t *testing.T) {
490490
491491 func TestClampDatagramHelloOutputWindowPreservesProtocolValidation(t *testing.T) {
492492 t.Run("malformed payload remains malformed", func(t *testing.T) {
493- payload := append(ports.MarshalHello(ports.Hello{Version: ports.ProtocolVersion, Size: domain.Size{Cols: 80, Rows: 24}, MaxOutputInFlight: 8}), 0xff)
493+ payload := append(ports.MarshalHello(protocol.Hello{Version: protocol.Version, Size: domain.Size{Cols: 80, Rows: 24}, MaxOutputInFlight: 8}), 0xff)
494494 frame := ports.Frame{Type: ports.MsgHello, Payload: payload}
495495 got := clampDatagramHelloOutputWindow(frame)
496496 if !reflect.DeepEqual(got, frame) {
@@ -502,8 +502,8 @@ func TestClampDatagramHelloOutputWindowPreservesProtocolValidation(t *testing.T)
502502 })
503503
504504 t.Run("mismatched version remains mismatched", func(t *testing.T) {
505- wantVersion := ports.ProtocolVersion + 1
506- frame := ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(ports.Hello{
505+ wantVersion := protocol.Version + 1
506+ frame := ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(protocol.Hello{
507507 Version: wantVersion, Size: domain.Size{Cols: 80, Rows: 24}, MaxOutputInFlight: 8,
508508 })}
509509 got, err := ports.UnmarshalHello(clampDatagramHelloOutputWindow(frame).Payload)
M internal/adapters/ipc/transport_test.go +3 -3
@@ -24,8 +24,8 @@ func TestTransportSendRecvBothDirections(t *testing.T) {
2424 client := NewTransport(c1)
2525 server := NewTransport(c2)
2626
27- clientToServer := ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(ports.Hello{Version: 1, Intent: ports.IntentNew, Name: "w0"})}
28- serverToClient := ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(ports.Welcome{SessionID: "s1", SessionName: "main"})}
27+ clientToServer := ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(protocol.Hello{Version: 1, Intent: protocol.IntentNew, Name: "w0"})}
28+ serverToClient := ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(protocol.Welcome{SessionID: "s1", SessionName: "main"})}
2929
3030 var wg sync.WaitGroup
3131 wg.Go(func() {
@@ -140,7 +140,7 @@ func TestTransportManySmallFramesBackToBack(t *testing.T) {
140140 frames[i] = ports.Frame{Type: ports.MsgPing, Payload: nil}
141141 }
142142 // Vary a couple to prove ordering and content are both preserved.
143- frames[7] = ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("hop")})}
143+ frames[7] = ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("hop")})}
144144 frames[150] = ports.Frame{Type: ports.MsgResize, Payload: mustMarshalResize(protocol.Resize{Size: domain.Size{Cols: 5, Rows: 6}})}
145145
146146 var wg sync.WaitGroup
M internal/adapters/remote/catalog.go +3 -2
@@ -17,6 +17,7 @@ import (
1717 "github.com/bnema/vev/internal/adapters/sshstdio"
1818 "github.com/bnema/vev/internal/domain"
1919 "github.com/bnema/vev/internal/ports"
20+ "github.com/bnema/vev/internal/protocol"
2021 )
2122
2223 const (
@@ -158,10 +159,10 @@ func decodeRemoteCatalog(raw []byte) (ports.RemoteCatalog, error) {
158159 Kind: "catalog",
159160 }
160161 }
161- if *version.ProtocolVersion != ports.ProtocolVersion {
162+ if *version.ProtocolVersion != protocol.Version {
162163 return ports.RemoteCatalog{}, &ports.RemoteCatalogVersionMismatchError{
163164 Got: *version.ProtocolVersion,
164- Want: ports.ProtocolVersion,
165+ Want: protocol.Version,
165166 Kind: "protocol",
166167 }
167168 }
M internal/adapters/remote/catalog_test.go +15 -14
@@ -13,6 +13,7 @@ import (
1313
1414 "github.com/bnema/vev/internal/adapters/sshstdio"
1515 "github.com/bnema/vev/internal/ports"
16+ "github.com/bnema/vev/internal/protocol"
1617 )
1718
1819 func TestRemoteCatalogClientCommandConstruction(t *testing.T) {
@@ -37,7 +38,7 @@ func TestRemoteCatalogClientCommandConstruction(t *testing.T) {
3738 gotPath = name
3839 gotArgs = append([]string(nil), args...)
3940 return stdoutCmd(ctx, mustCatalogJSON(t, ports.RemoteCatalog{
40- ProtocolVersion: ports.ProtocolVersion,
41+ ProtocolVersion: protocol.Version,
4142 SchemaVersion: ports.RemoteCatalogSchemaVersion,
4243 Sessions: []ports.RemoteCatalogSession{},
4344 }))
@@ -83,7 +84,7 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
8384 t.Parallel()
8485
8586 valid := ports.RemoteCatalog{
86- ProtocolVersion: ports.ProtocolVersion,
87+ ProtocolVersion: protocol.Version,
8788 SchemaVersion: ports.RemoteCatalogSchemaVersion,
8889 Sessions: []ports.RemoteCatalogSession{
8990 {LifecycleID: [16]byte{1}, Name: "work", State: "up", Ephemeral: false, Tabs: []ports.RemoteCatalogTab{{ID: "tab-1"}, {ID: "tab-2", Index: 1}}, Attached: true},
@@ -112,14 +113,14 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
112113 },
113114 {
114115 name: "mismatching protocol version",
115- stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion + 1, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{}}),
116+ stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: protocol.Version + 1, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{}}),
116117 check: func(t *testing.T, err error) {
117118 t.Helper()
118119 var mismatch *ports.RemoteCatalogVersionMismatchError
119120 if !errors.As(err, &mismatch) {
120121 t.Fatalf("error = %v, want RemoteCatalogVersionMismatchError", err)
121122 }
122- if mismatch.Kind != "protocol" || mismatch.Got != ports.ProtocolVersion+1 || mismatch.Want != ports.ProtocolVersion {
123+ if mismatch.Kind != "protocol" || mismatch.Got != protocol.Version+1 || mismatch.Want != protocol.Version {
123124 t.Fatalf("mismatch = %#v", mismatch)
124125 }
125126 },
@@ -127,7 +128,7 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
127128 {
128129 name: "obsolete count-only schema is classified before session decoding",
129130 stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":2,"sessions":[{"tabs":1}]}`,
130- ports.ProtocolVersion),
131+ protocol.Version),
131132 check: func(t *testing.T, err error) {
132133 t.Helper()
133134 var mismatch *ports.RemoteCatalogVersionMismatchError
@@ -138,7 +139,7 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
138139 },
139140 {
140141 name: "unknown catalogue schema",
141- stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":4,"sessions":[]}`, ports.ProtocolVersion),
142+ stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":4,"sessions":[]}`, protocol.Version),
142143 check: func(t *testing.T, err error) {
143144 t.Helper()
144145 var mismatch *ports.RemoteCatalogVersionMismatchError
@@ -160,19 +161,19 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
160161 {
161162 name: "count-only tabs rejected",
162163 stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d,"sessions":[{"lifecycle_id":"01000000000000000000000000000000","name":"work","state":"up","ephemeral":false,"tabs":1,"attached":false}]}`,
163- ports.ProtocolVersion, ports.RemoteCatalogSchemaVersion),
164+ protocol.Version, ports.RemoteCatalogSchemaVersion),
164165 wantErr: errCatalogDecode,
165166 },
166167 {
167168 name: "unknown fields rejected",
168169 stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d,"sessions":[],"future":true}`,
169- ports.ProtocolVersion, ports.RemoteCatalogSchemaVersion),
170+ protocol.Version, ports.RemoteCatalogSchemaVersion),
170171 wantErr: errCatalogDecode,
171172 },
172173 {
173174 name: "missing required session field",
174175 stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d,"sessions":[{"lifecycle_id":"01000000000000000000000000000000","name":"work","state":"up","tabs":[],"attached":false}]}`,
175- ports.ProtocolVersion, ports.RemoteCatalogSchemaVersion),
176+ protocol.Version, ports.RemoteCatalogSchemaVersion),
176177 wantErr: errCatalogRequired,
177178 },
178179 {
@@ -187,7 +188,7 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
187188 },
188189 {
189190 name: "missing catalogue schema",
190- stdout: fmt.Sprintf(`{"protocol_version":%d,"sessions":[]}`+"\n", ports.ProtocolVersion),
191+ stdout: fmt.Sprintf(`{"protocol_version":%d,"sessions":[]}`+"\n", protocol.Version),
191192 check: func(t *testing.T, err error) {
192193 t.Helper()
193194 var mismatch *ports.RemoteCatalogVersionMismatchError
@@ -198,19 +199,19 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
198199 },
199200 {
200201 name: "missing sessions",
201- stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d}`+"\n", ports.ProtocolVersion, ports.RemoteCatalogSchemaVersion),
202+ stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d}`+"\n", protocol.Version, ports.RemoteCatalogSchemaVersion),
202203 wantErr: errCatalogRequired,
203204 },
204205 {
205206 name: "invalid session name",
206- stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{{
207+ stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: protocol.Version, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{{
207208 LifecycleID: [16]byte{1}, Name: "bad name", State: "up", Tabs: []ports.RemoteCatalogTab{},
208209 }}}),
209210 wantErr: ports.ErrInvalidRemoteCatalog,
210211 },
211212 {
212213 name: "invalid session state",
213- stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{{
214+ stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: protocol.Version, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{{
214215 LifecycleID: [16]byte{1}, Name: "work", State: "weird", Tabs: []ports.RemoteCatalogTab{},
215216 }}}),
216217 wantErr: ports.ErrRemoteCatalogUnknownState,
@@ -292,7 +293,7 @@ func TestRemoteCatalogClientAppliesLifetimeAndWaitDelay(t *testing.T) {
292293 command: func(ctx context.Context, name string, args ...string) *exec.Cmd {
293294 commandCtx = ctx
294295 cmd = stdoutCmd(ctx, mustCatalogJSON(t, ports.RemoteCatalog{
295- ProtocolVersion: ports.ProtocolVersion,
296+ ProtocolVersion: protocol.Version,
296297 SchemaVersion: ports.RemoteCatalogSchemaVersion,
297298 Sessions: []ports.RemoteCatalogSession{},
298299 }))
M internal/adapters/sshstdio/transport_test.go +5 -4
@@ -15,6 +15,7 @@ import (
1515
1616 "github.com/bnema/vev/internal/domain"
1717 "github.com/bnema/vev/internal/ports"
18+ "github.com/bnema/vev/internal/protocol"
1819 )
1920
2021 func TestBuildCommandUsesExecArgs(t *testing.T) {
@@ -158,7 +159,7 @@ func TestTransportRoundTripAndVersionMismatchFrame(t *testing.T) {
158159 defer func() { _ = client.Close() }()
159160 defer func() { _ = server.Close() }()
160161
161- hello := ports.Hello{Version: ports.ProtocolVersion + 1, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}}
162+ hello := protocol.Hello{Version: protocol.Version + 1, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}}
162163 go func() {
163164 f, err := server.Recv()
164165 if err != nil {
@@ -170,10 +171,10 @@ func TestTransportRoundTripAndVersionMismatchFrame(t *testing.T) {
170171 t.Errorf("UnmarshalHello: %v", err)
171172 return
172173 }
173- if got.Version == ports.ProtocolVersion {
174+ if got.Version == protocol.Version {
174175 t.Errorf("test did not send a mismatched version")
175176 }
176- _ = server.Send(ports.Frame{Type: ports.MsgError, Payload: ports.MarshalErrorMsg(ports.ErrorMsg{Code: ports.ErrVersionMismatch, Text: "protocol version mismatch"})})
177+ _ = server.Send(ports.Frame{Type: ports.MsgError, Payload: ports.MarshalErrorMsg(protocol.ErrorMsg{Code: protocol.ErrVersionMismatch, Text: "protocol version mismatch"})})
177178 }()
178179
179180 if err := client.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}); err != nil {
@@ -190,7 +191,7 @@ func TestTransportRoundTripAndVersionMismatchFrame(t *testing.T) {
190191 if err != nil {
191192 t.Fatalf("UnmarshalErrorMsg: %v", err)
192193 }
193- if em.Code != ports.ErrVersionMismatch {
194+ if em.Code != protocol.ErrVersionMismatch {
194195 t.Fatalf("error code = %d, want ErrVersionMismatch", em.Code)
195196 }
196197 }
M internal/app/cmd.go +4 -3
@@ -13,6 +13,7 @@ import (
1313 "github.com/bnema/vev/internal/adapters/clock"
1414 "github.com/bnema/vev/internal/adapters/ipc"
1515 "github.com/bnema/vev/internal/ports"
16+ "github.com/bnema/vev/internal/protocol"
1617 commandusecase "github.com/bnema/vev/internal/usecase/command"
1718 "github.com/bnema/vev/internal/usecase/daemon"
1819 )
@@ -149,8 +150,8 @@ func runCmdWithDeps(ctx context.Context, invocation cmdInvocation, deps cmdDeps)
149150 _, err := fmt.Fprintln(deps.stdout, cmdHelp(invocation))
150151 return err
151152 }
152- request := ports.CommandRequest{
153- Version: ports.ProtocolVersion,
153+ request := protocol.CommandRequest{
154+ Version: protocol.Version,
154155 Self: invocation.self,
155156 Slug: invocation.slug,
156157 Args: invocation.args,
@@ -225,7 +226,7 @@ func runCmdWithDeps(ctx context.Context, invocation cmdInvocation, deps cmdDeps)
225226 return &exitCoded{code: 3, err: err}
226227 }
227228 if !result.OK {
228- if result.Code == ports.ErrInvalidCommandArgs {
229+ if result.Code == protocol.ErrInvalidCommandArgs {
229230 return &exitCoded{code: 2, err: errors.New(result.Text)}
230231 }
231232 return errors.New(result.Text)
M internal/app/cmd_test.go +17 -16
@@ -13,6 +13,7 @@ import (
1313
1414 "github.com/bnema/vev/internal/ports"
1515 portsmocks "github.com/bnema/vev/internal/ports/mocks"
16+ "github.com/bnema/vev/internal/protocol"
1617 "github.com/bnema/vev/internal/usecase/daemon"
1718 "github.com/stretchr/testify/mock"
1819 "github.com/stretchr/testify/require"
@@ -140,7 +141,7 @@ func TestMoveCmdPreservesPositionalArguments(t *testing.T) {
140141 t.Run(tt.slug, func(t *testing.T) {
141142 parsed, err := parseArgs(append([]string{"cmd", tt.slug}, tt.args...))
142143 require.NoError(t, err)
143- transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{OK: true})}}
144+ transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(protocol.CommandResult{OK: true})}}
144145 require.NoError(t, runCmdWithDeps(context.Background(), parsed.cmd, cmdDeps{
145146 stdout: io.Discard,
146147 getenv: func(string) string { return "" },
@@ -151,13 +152,13 @@ func TestMoveCmdPreservesPositionalArguments(t *testing.T) {
151152 require.NoError(t, err)
152153 require.Equal(t, tt.slug, request.Slug)
153154 require.Equal(t, tt.args, request.Args)
154- require.Equal(t, ports.ProtocolVersion, request.Version)
155+ require.Equal(t, protocol.Version, request.Version)
155156 })
156157 }
157158 }
158159
159160 func TestRemoteCatalogCommandEnsuresDaemon(t *testing.T) {
160- transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{OK: true})}}
161+ transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(protocol.CommandResult{OK: true})}}
161162 ensureCalls := 0
162163 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "remote-catalog", jsonOut: true}, cmdDeps{
163164 stdout: io.Discard,
@@ -183,8 +184,8 @@ func TestMoveCmdInvalidArgumentResultExitsTwo(t *testing.T) {
183184 {slug: "move-tab", args: []string{"work", "extra"}},
184185 } {
185186 t.Run(invocation.slug+"/"+strconv.Itoa(len(invocation.args)), func(t *testing.T) {
186- transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{
187- Code: ports.ErrInvalidCommandArgs, Text: "invalid command arguments",
187+ transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(protocol.CommandResult{
188+ Code: protocol.ErrInvalidCommandArgs, Text: "invalid command arguments",
188189 })}}
189190 err := runCmdWithDeps(context.Background(), invocation, cmdDeps{
190191 stdout: io.Discard,
@@ -206,7 +207,7 @@ func TestTargetPaneCmdsParseAndUseSelfTarget(t *testing.T) {
206207 t.Run(slug, func(t *testing.T) {
207208 invocation, err := parseArgs([]string{"cmd", "--self", slug})
208209 require.NoError(t, err)
209- transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{OK: true})}}
210+ transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(protocol.CommandResult{OK: true})}}
210211 require.NoError(t, runCmdWithDeps(context.Background(), invocation.cmd, cmdDeps{
211212 stdout: io.Discard,
212213 getenv: func(string) string { return "session=work,tab=t_abc,pane=p_def" },
@@ -225,7 +226,7 @@ func TestTargetPaneCmdsParseAndUseSelfTarget(t *testing.T) {
225226 }
226227
227228 func TestRunCmdBuildsOneShotTargetedRequest(t *testing.T) {
228- transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{OK: true, Output: "done"})}}
229+ transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(protocol.CommandResult{OK: true, Output: "done"})}}
229230 out := new(strings.Builder)
230231 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right", self: true}, cmdDeps{
231232 stdout: out,
@@ -251,7 +252,7 @@ func TestRunCmdBuildsOneShotTargetedRequest(t *testing.T) {
251252 }
252253
253254 func TestRunCmdVEVWithoutSelfUsesIDsOnlyAsSessionLocator(t *testing.T) {
254- transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{OK: true})}}
255+ transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(protocol.CommandResult{OK: true})}}
255256 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{
256257 stdout: io.Discard,
257258 getenv: func(string) string { return "session=old,tab=t_abc,pane=p_def" },
@@ -270,7 +271,7 @@ func TestRunCmdVEVWithoutSelfUsesIDsOnlyAsSessionLocator(t *testing.T) {
270271 }
271272
272273 func TestRunCmdExplicitSessionDoesNotUseEnvIDsWithoutSelf(t *testing.T) {
273- transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{OK: true})}}
274+ transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(protocol.CommandResult{OK: true})}}
274275 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "new-tab", session: "current"}, cmdDeps{
275276 stdout: io.Discard,
276277 getenv: func(string) string { return "session=old,tab=t_abc,pane=p_def" },
@@ -350,8 +351,8 @@ func TestRunCmdTimesOutPendingRequest(t *testing.T) {
350351
351352 func TestRunCmdZeroRequestIDReplyReturnsDaemonError(t *testing.T) {
352353 const want = "daemon error"
353- transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{
354- Code: ports.ErrInternal, Text: want,
354+ transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(protocol.CommandResult{
355+ Code: protocol.ErrInternal, Text: want,
355356 })}}
356357 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{
357358 stdout: io.Discard,
@@ -388,27 +389,27 @@ func TestRunCmdDoesNotAutostartAndClassifiesDialFailure(t *testing.T) {
388389 func TestRunCmdClassifiesDaemonCommandErrors(t *testing.T) {
389390 tests := []struct {
390391 name string
391- result ports.CommandResult
392+ result protocol.CommandResult
392393 wantCode int
393394 }{
394395 {
395396 name: "invalid command arguments are usage errors",
396- result: ports.CommandResult{Code: ports.ErrInvalidCommandArgs, Text: "usage: toast [-l level] <message>"},
397+ result: protocol.CommandResult{Code: protocol.ErrInvalidCommandArgs, Text: "usage: toast [-l level] <message>"},
397398 wantCode: 2,
398399 },
399400 {
400401 name: "missing runtime target is a command failure",
401- result: ports.CommandResult{Code: ports.ErrNoSuchTarget, Text: "no live sessions"},
402+ result: protocol.CommandResult{Code: protocol.ErrNoSuchTarget, Text: "no live sessions"},
402403 wantCode: 1,
403404 },
404405 {
405406 name: "resize not in split remains an exit one failure",
406- result: ports.CommandResult{Code: ports.ErrNoSuchTarget, Text: "pane is not in a split"},
407+ result: protocol.CommandResult{Code: protocol.ErrNoSuchTarget, Text: "pane is not in a split"},
407408 wantCode: 1,
408409 },
409410 {
410411 name: "resize minimum remains an exit one failure",
411- result: ports.CommandResult{Code: ports.ErrNoSuchTarget, Text: "pane cannot be resized further"},
412+ result: protocol.CommandResult{Code: protocol.ErrNoSuchTarget, Text: "pane cannot be resized further"},
412413 wantCode: 1,
413414 },
414415 }
M internal/app/daemon_integration_test.go +41 -40
@@ -28,6 +28,7 @@ import (
2828 "github.com/bnema/vev/internal/domain"
2929 "github.com/bnema/vev/internal/persist"
3030 "github.com/bnema/vev/internal/ports"
31+ "github.com/bnema/vev/internal/protocol"
3132 "github.com/bnema/vev/internal/usecase/daemon"
3233 "github.com/bnema/vev/internal/usecase/recovery"
3334 )
@@ -90,7 +91,7 @@ func attachWithEnvironment(t *testing.T, dir string, intent uint8, name string,
9091 t.Helper()
9192 tr, err := ipc.DialContext(context.Background(), dir)
9293 require.NoError(t, err)
93- hello := ports.Hello{Version: ports.ProtocolVersion, Intent: intent, Name: name, Size: sz, TermEnv: "xterm-256color", TrueColor: true, Env: env}
94+ hello := protocol.Hello{Version: protocol.Version, Intent: intent, Name: name, Size: sz, TermEnv: "xterm-256color", TrueColor: true, Env: env}
9495 require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}))
9596 p := recvPump(tr)
9697 select {
@@ -103,12 +104,12 @@ func attachWithEnvironment(t *testing.T, dir string, intent uint8, name string,
103104 return tr, p
104105 }
105106
106-func listRemoteSessions(t *testing.T, dir string) ports.Sessions {
107+func listRemoteSessions(t *testing.T, dir string) protocol.Sessions {
107108 t.Helper()
108109 tr, err := ipc.DialContext(context.Background(), dir)
109110 require.NoError(t, err)
110111 defer func() { _ = tr.Close() }()
111- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(ports.List{})}))
112+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(protocol.List{})}))
112113 f, err := tr.Recv()
113114 require.NoError(t, err)
114115 require.Equal(t, ports.MsgSessions, f.Type)
@@ -123,7 +124,7 @@ func killAll(dir string) error {
123124 return err
124125 }
125126 defer func() { _ = tr.Close() }()
126- if err := tr.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(ports.Kill{All: true})}); err != nil {
127+ if err := tr.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(protocol.Kill{All: true})}); err != nil {
127128 return err
128129 }
129130 _, err = tr.Recv()
@@ -213,7 +214,7 @@ func shellFixture(t *testing.T, label string) string {
213214 func assertChildEnvironment(t *testing.T, tr ports.Transport, p *pump, sz domain.Size, wantTestEnv, wantShell, wantRuntimeDir, wantWayland string) {
214215 t.Helper()
215216 command := "printf '\\033[2J\\033[H'; printf 'VEV_TEST_ENV=%s SHELL=%s XDG_RUNTIME_DIR=%s WAYLAND_DISPLAY=%s TERM=%s COLORTERM=%s TERM_PROGRAM=%s VEV_PREFIX=%.24s\\n' \"$VEV_TEST_ENV\" \"${SHELL##*/}\" \"$XDG_RUNTIME_DIR\" \"$WAYLAND_DISPLAY\" \"$TERM\" \"$COLORTERM\" \"$TERM_PROGRAM\" \"$VEV\"\n"
216- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte(command)})}))
217+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte(command)})}))
217218 text := awaitScreenText(t, p, sz, "TERM_PROGRAM=vev")
218219 for _, want := range []string{
219220 "VEV_TEST_ENV=" + wantTestEnv,
@@ -239,7 +240,7 @@ func TestIntegration_AttachEnvironmentRefreshesFuturePTYChildren(t *testing.T) {
239240 "TERM=client", "COLORTERM=client", "TERM_PROGRAM=client", "VEV=client",
240241 }
241242
242- tr1, p1 := attachWithEnvironment(t, dir, ports.IntentNew, "environment", sz, firstEnv)
243+ tr1, p1 := attachWithEnvironment(t, dir, protocol.IntentNew, "environment", sz, firstEnv)
243244 defer func() { _ = tr1.Close() }()
244245 awaitText(t, p1, sz, "SHELL_COMMAND=first")
245246 assertChildEnvironment(t, tr1, p1, sz, "first", firstShell, "/run/first", "wayland-first")
@@ -248,15 +249,15 @@ func TestIntegration_AttachEnvironmentRefreshesFuturePTYChildren(t *testing.T) {
248249 "VEV_TEST_ENV=second", "SHELL=" + secondShell, "XDG_RUNTIME_DIR=/run/second", "WAYLAND_DISPLAY=wayland-second",
249250 "TERM=client", "COLORTERM=client", "TERM_PROGRAM=client", "VEV=client",
250251 }
251- tr2, p2 := attachWithEnvironment(t, dir, ports.IntentAttach, "environment", sz, secondEnv)
252+ tr2, p2 := attachWithEnvironment(t, dir, protocol.IntentAttach, "environment", sz, secondEnv)
252253 defer func() { _ = tr2.Close() }()
253254
254255 // The first shell was already running, so it retains its original environment.
255256 assertChildEnvironment(t, tr2, p2, sz, "first", firstShell, "/run/first", "wayland-first")
256257
257- require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1b ")})}))
258+ require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("\x1b ")})}))
258259 awaitText(t, p2, sz, "Commands")
259- require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("CNT\r")})}))
260+ require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("CNT\r")})}))
260261 awaitText(t, p2, sz, "SHELL_COMMAND=second")
261262 assertChildEnvironment(t, tr2, p2, sz, "second", secondShell, "/run/second", "wayland-second")
262263 }
@@ -272,7 +273,7 @@ func TestIntegration_TwoAttachmentsReceiveSharedMutationPTYOutput(t *testing.T)
272273 "VEV_TEST_ENV=first", "SHELL=" + firstShell, "XDG_RUNTIME_DIR=/run/first", "WAYLAND_DISPLAY=wayland-first",
273274 "TERM=client", "COLORTERM=client", "TERM_PROGRAM=client", "VEV=client",
274275 }
275- tr1, p1 := attachWithEnvironment(t, dir, ports.IntentNew, "environment", sz, firstEnv)
276+ tr1, p1 := attachWithEnvironment(t, dir, protocol.IntentNew, "environment", sz, firstEnv)
276277 defer func() { _ = tr1.Close() }()
277278 awaitText(t, p1, sz, "SHELL_COMMAND=first")
278279 assertChildEnvironment(t, tr1, p1, sz, "first", firstShell, "/run/first", "wayland-first")
@@ -281,23 +282,23 @@ func TestIntegration_TwoAttachmentsReceiveSharedMutationPTYOutput(t *testing.T)
281282 "VEV_TEST_ENV=second", "SHELL=" + secondShell, "XDG_RUNTIME_DIR=/run/second", "WAYLAND_DISPLAY=wayland-second",
282283 "TERM=client", "COLORTERM=client", "TERM_PROGRAM=client", "VEV=client",
283284 }
284- tr2, p2 := attachWithEnvironment(t, dir, ports.IntentAttach, "environment", sz, secondEnv)
285+ tr2, p2 := attachWithEnvironment(t, dir, protocol.IntentAttach, "environment", sz, secondEnv)
285286 defer func() { _ = tr2.Close() }()
286287 assertChildEnvironment(t, tr2, p2, sz, "first", firstShell, "/run/first", "wayland-first")
287288
288289 // The second attachment mutates shared tab state. Its newly opened PTY must
289290 // also be rendered to that attachment, not only to the coordinator primary.
290- require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1b ")})}))
291+ require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("\x1b ")})}))
291292 awaitText(t, p2, sz, "Commands")
292- require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("CNT\r")})}))
293+ require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("CNT\r")})}))
293294 awaitText(t, p2, sz, "SHELL_COMMAND=second")
294295
295296 // Each attachment owns its selected tab. Move the first attachment to the
296297 // new PTY before releasing its gate, so the fresh PTY output must reach both
297298 // views rather than only being recovered by a later first paint.
298- require.NoError(t, tr1.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1b2")})}))
299+ require.NoError(t, tr1.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("\x1b2")})}))
299300 awaitText(t, p1, sz, "SHELL_COMMAND=second")
300- require.NoError(t, tr1.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("go\n")})}))
301+ require.NoError(t, tr1.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("go\n")})}))
301302 awaitText(t, p2, sz, sharedOutput)
302303 awaitText(t, p1, sz, sharedOutput)
303304 }
@@ -306,7 +307,7 @@ func TestIntegration_AttachFirstOutput(t *testing.T) {
306307 sz := domain.Size{Cols: 80, Rows: 24}
307308 dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf HELLO; sleep 30"}))
308309
309- tr, p := attach(t, dir, ports.IntentEphemeral, "", sz)
310+ tr, p := attach(t, dir, protocol.IntentEphemeral, "", sz)
310311 defer func() { _ = tr.Close() }()
311312
312313 awaitText(t, p, sz, "HELLO")
@@ -316,10 +317,10 @@ func TestIntegration_InputRoundtrip(t *testing.T) {
316317 sz := domain.Size{Cols: 80, Rows: 24}
317318 dir, _ := startDaemon(t, daemon.WithShell("/bin/cat", nil))
318319
319- tr, p := attach(t, dir, ports.IntentEphemeral, "", sz)
320+ tr, p := attach(t, dir, protocol.IntentEphemeral, "", sz)
320321 defer func() { _ = tr.Close() }()
321322
322- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("PINGPONG\n")})}))
323+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("PINGPONG\n")})}))
323324 awaitText(t, p, sz, "PINGPONG")
324325 }
325326
@@ -327,14 +328,14 @@ func TestIntegration_CommandPaletteCreatesTab(t *testing.T) {
327328 sz := domain.Size{Cols: 80, Rows: 24}
328329 dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"}))
329330
330- tr, p := attach(t, dir, ports.IntentEphemeral, "", sz)
331+ tr, p := attach(t, dir, protocol.IntentEphemeral, "", sz)
331332 defer func() { _ = tr.Close() }()
332333 awaitText(t, p, sz, "READY")
333334
334- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1b ")})}))
335+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("\x1b ")})}))
335336 awaitText(t, p, sz, "Commands")
336337
337- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("CNT\r")})}))
338+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("CNT\r")})}))
338339 // Tab labels are enriched with the focused pane's title; with no process
339340 // inspector wired in this test, that title falls back to the shell's
340341 // basename ("sh").
@@ -346,25 +347,25 @@ func TestIntegration_CommandPaletteRenamesEphemeralSession(t *testing.T) {
346347 sz := domain.Size{Cols: 80, Rows: 24}
347348 dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"}))
348349
349- tr, p := attach(t, dir, ports.IntentEphemeral, "", sz)
350+ tr, p := attach(t, dir, protocol.IntentEphemeral, "", sz)
350351 defer func() { _ = tr.Close() }()
351352 text := awaitScreenText(t, p, sz, "READY")
352353 require.Contains(t, text, " 0* ")
353354
354- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1b ")})}))
355+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("\x1b ")})}))
355356 awaitText(t, p, sz, "Commands")
356357
357- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("RNS\r")})}))
358+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("RNS\r")})}))
358359 awaitText(t, p, sz, "Rename session")
359360
360- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x7fwork\r")})}))
361+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("\x7fwork\r")})}))
361362 text = awaitScreenText(t, p, sz, " work ")
362363 require.NotContains(t, text, "work*")
363364
364365 listTr, err := ipc.DialContext(context.Background(), dir)
365366 require.NoError(t, err)
366367 defer func() { _ = listTr.Close() }()
367- require.NoError(t, listTr.Send(ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(ports.List{})}))
368+ require.NoError(t, listTr.Send(ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(protocol.List{})}))
368369 f, err := listTr.Recv()
369370 require.NoError(t, err)
370371 require.Equal(t, ports.MsgSessions, f.Type)
@@ -379,12 +380,12 @@ func TestIntegration_AltCWithoutPaletteDoesNotCreateTab(t *testing.T) {
379380 sz := domain.Size{Cols: 80, Rows: 24}
380381 dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"}))
381382
382- tr, p := attach(t, dir, ports.IntentEphemeral, "", sz)
383+ tr, p := attach(t, dir, protocol.IntentEphemeral, "", sz)
383384 defer func() { _ = tr.Close() }()
384385 text := awaitScreenText(t, p, sz, "READY")
385386 require.NotContains(t, text, " 1 2 ")
386387
387- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1bc")})}))
388+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("\x1bc")})}))
388389 assertNoTextAfterInput(t, p, sz, " 1 2 ")
389390 }
390391
@@ -392,7 +393,7 @@ func TestIntegration_EphemeralSurvivesDetachAndReattaches(t *testing.T) {
392393 sz := domain.Size{Cols: 80, Rows: 24}
393394 dir, served := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf MARKER; sleep 30"}))
394395
395- tr1, p1 := attach(t, dir, ports.IntentEphemeral, "", sz)
396+ tr1, p1 := attach(t, dir, protocol.IntentEphemeral, "", sz)
396397 awaitText(t, p1, sz, "MARKER")
397398 require.NoError(t, tr1.Close())
398399
@@ -402,7 +403,7 @@ func TestIntegration_EphemeralSurvivesDetachAndReattaches(t *testing.T) {
402403 case <-time.After(300 * time.Millisecond):
403404 }
404405
405- tr2, p2 := attach(t, dir, ports.IntentAttach, "0", sz)
406+ tr2, p2 := attach(t, dir, protocol.IntentAttach, "0", sz)
406407 defer func() { _ = tr2.Close() }()
407408 awaitText(t, p2, sz, "MARKER")
408409
@@ -423,7 +424,7 @@ func TestIntegration_EphemeralNotListedAfterDaemonRestart(t *testing.T) {
423424 dir := ipc.SocketDir()
424425
425426 _, served := startDaemonInDir(t, dir, daemon.WithShell("/bin/sh", []string{"-c", "printf TEMP; sleep 30"}))
426- tr, p := attach(t, dir, ports.IntentEphemeral, "", sz)
427+ tr, p := attach(t, dir, protocol.IntentEphemeral, "", sz)
427428 awaitText(t, p, sz, "TEMP")
428429 require.NoError(t, tr.Close())
429430
@@ -452,7 +453,7 @@ func TestIntegration_NamedSurvivesReattach(t *testing.T) {
452453 sz := domain.Size{Cols: 80, Rows: 24}
453454 dir, served := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf MARKER; sleep 30"}))
454455
455- tr1, p1 := attach(t, dir, ports.IntentNew, "work", sz)
456+ tr1, p1 := attach(t, dir, protocol.IntentNew, "work", sz)
456457 awaitText(t, p1, sz, "MARKER")
457458
458459 // Detach: a named session must survive.
@@ -466,7 +467,7 @@ func TestIntegration_NamedSurvivesReattach(t *testing.T) {
466467 }
467468
468469 // Re-attach: the first paint must reproduce the retained screen state.
469- tr2, p2 := attach(t, dir, ports.IntentAttach, "work", sz)
470+ tr2, p2 := attach(t, dir, protocol.IntentAttach, "work", sz)
470471 defer func() { _ = tr2.Close() }()
471472 awaitText(t, p2, sz, "MARKER")
472473 }
@@ -800,8 +801,8 @@ func publishRestorableCheckpoint(t *testing.T, stateDir string, repository *snap
800801 served := make(chan error, 1)
801802 go func() { served <- d.Serve(ctx, listener) }()
802803
803- tr, _ := attach(t, runtimeDir, ports.IntentNew, name, domain.Size{Cols: 80, Rows: 24})
804- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("checkpoint me\n")})}))
804+ tr, _ := attach(t, runtimeDir, protocol.IntentNew, name, domain.Size{Cols: 80, Rows: 24})
805+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("checkpoint me\n")})}))
805806 require.Eventually(t, func() bool {
806807 record, ok, _ := opened.Catalogue.Record(name)
807808 return ok && record.Committed != nil && record.DegradedReason == ""
@@ -886,8 +887,8 @@ func runBlockedSnapshotWriterShutdown(t *testing.T) blockedSnapshotWriterShutdow
886887 }()
887888
888889 awaitLifecycleStage(t, listenerReady, "daemon listener")
889- tr, _ := attach(t, runtimeDir, ports.IntentNew, "work", domain.Size{Cols: 80, Rows: 24})
890- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("dirty state\n")})}))
890+ tr, _ := attach(t, runtimeDir, protocol.IntentNew, "work", domain.Size{Cols: 80, Rows: 24})
891+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("dirty state\n")})}))
891892 awaitLifecycleStage(t, repository.entered, "snapshot publication")
892893 require.NoError(t, tr.Close())
893894 require.Eventually(t, func() bool {
@@ -1249,15 +1250,15 @@ func TestIntegration_KillAllShutsDownDaemon(t *testing.T) {
12491250 sz := domain.Size{Cols: 80, Rows: 24}
12501251 dir, served := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "sleep 30"}))
12511252
1252- tr1, _ := attach(t, dir, ports.IntentNew, "one", sz)
1253+ tr1, _ := attach(t, dir, protocol.IntentNew, "one", sz)
12531254 require.NoError(t, tr1.Close())
1254- tr2, _ := attach(t, dir, ports.IntentNew, "two", sz)
1255+ tr2, _ := attach(t, dir, protocol.IntentNew, "two", sz)
12551256 require.NoError(t, tr2.Close())
12561257
12571258 killTr, err := ipc.DialContext(context.Background(), dir)
12581259 require.NoError(t, err)
12591260 defer func() { _ = killTr.Close() }()
1260- require.NoError(t, killTr.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(ports.Kill{All: true})}))
1261+ require.NoError(t, killTr.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(protocol.Kill{All: true})}))
12611262
12621263 _, err = killTr.Recv()
12631264 require.ErrorIs(t, err, io.EOF)
M internal/app/final_acceptance_test.go +12 -12
@@ -15,7 +15,7 @@ import (
1515
1616 func sendAcceptanceInput(t *testing.T, tr ports.Transport, data string) {
1717 t.Helper()
18- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte(data)})}))
18+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte(data)})}))
1919 }
2020
2121 func awaitAcceptanceFrame(t *testing.T, p *pump, typ ports.MsgType, predicate func(ports.Frame) bool) ports.Frame {
@@ -35,7 +35,7 @@ func awaitAcceptanceFrame(t *testing.T, p *pump, typ ports.MsgType, predicate fu
3535 }
3636 }
3737
38-func awaitAcceptanceCommand(t *testing.T, p *pump, requestID uint64) ports.CommandResult {
38+func awaitAcceptanceCommand(t *testing.T, p *pump, requestID uint64) protocol.CommandResult {
3939 t.Helper()
4040 frame := awaitAcceptanceFrame(t, p, ports.MsgCommandResult, func(frame ports.Frame) bool {
4141 result, err := ports.UnmarshalCommandResult(frame.Payload)
@@ -59,10 +59,10 @@ func awaitAcceptanceOutput(t *testing.T, p *pump, predicate func(protocol.Output
5959 return output
6060 }
6161
62-func awaitAcceptanceCommandResult(t *testing.T, tr ports.Transport, p *pump, requestID uint64, slug string) ports.CommandResult {
62+func awaitAcceptanceCommandResult(t *testing.T, tr ports.Transport, p *pump, requestID uint64, slug string) protocol.CommandResult {
6363 t.Helper()
64- payload, err := ports.MarshalCommandRequest(ports.CommandRequest{
65- Version: ports.ProtocolVersion, RequestID: requestID, Attached: true, Slug: slug,
64+ payload, err := ports.MarshalCommandRequest(protocol.CommandRequest{
65+ Version: protocol.Version, RequestID: requestID, Attached: true, Slug: slug,
6666 })
6767 require.NoError(t, err)
6868 require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgCommand, Payload: payload}))
@@ -72,8 +72,8 @@ func awaitAcceptanceCommandResult(t *testing.T, tr ports.Transport, p *pump, req
7272 func TestAcceptanceTwoLocalAttachmentsKeepViewsOverTransports(t *testing.T) {
7373 size := domain.Size{Cols: 80, Rows: 24}
7474 dir, _ := startDaemon(t, daemon.WithShell("/bin/cat", nil))
75- first, firstPump := attach(t, dir, ports.IntentNew, "shared", size)
76- second, secondPump := attach(t, dir, ports.IntentAttach, "shared", size)
75+ first, firstPump := attach(t, dir, protocol.IntentNew, "shared", size)
76+ second, secondPump := attach(t, dir, protocol.IntentAttach, "shared", size)
7777 t.Cleanup(func() { _ = first.Close() })
7878 t.Cleanup(func() { _ = second.Close() })
7979
@@ -121,12 +121,12 @@ func TestAcceptanceTwoLocalAttachmentsKeepViewsOverTransports(t *testing.T) {
121121 require.Equal(t, domain.Size{Cols: 100, Rows: 30}, resized.Size)
122122 assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT")
123123
124- require.NoError(t, first.Send(ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{})}))
124+ require.NoError(t, first.Send(ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(protocol.OutputResetRequest{})}))
125125 reset := awaitAcceptanceOutput(t, firstPump, func(output protocol.Output) bool { return output.Base == 0 && output.Full })
126126 require.True(t, reset.Full)
127127 assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT")
128128
129- require.NoError(t, first.Send(ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(ports.Detach{})}))
129+ require.NoError(t, first.Send(ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(protocol.Detach{})}))
130130 deadline := time.NewTimer(5 * time.Second)
131131 defer deadline.Stop()
132132 for {
@@ -149,8 +149,8 @@ detached:
149149 func TestAcceptanceAttachedCommandUsesItsConnectionOnly(t *testing.T) {
150150 size := domain.Size{Cols: 80, Rows: 24}
151151 dir, _ := startDaemon(t, daemon.WithShell("/bin/cat", nil))
152- first, firstPump := attach(t, dir, ports.IntentNew, "commands", size)
153- second, secondPump := attach(t, dir, ports.IntentAttach, "commands", size)
152+ first, firstPump := attach(t, dir, protocol.IntentNew, "commands", size)
153+ second, secondPump := attach(t, dir, protocol.IntentAttach, "commands", size)
154154 t.Cleanup(func() { _ = first.Close() })
155155 t.Cleanup(func() { _ = second.Close() })
156156
@@ -173,7 +173,7 @@ func TestAcceptanceAttachedCommandUsesItsConnectionOnly(t *testing.T) {
173173 }
174174
175175 drained:
176- require.NoError(t, first.Send(ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{})}))
176+ require.NoError(t, first.Send(ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(protocol.OutputResetRequest{})}))
177177 awaitAcceptanceOutput(t, firstPump, func(output protocol.Output) bool { return output.Full && output.Base == 0 })
178178 assertNoTextAfterInput(t, secondPump, size, "Commands")
179179 }
M internal/app/remote_final_acceptance_test.go +11 -11
@@ -23,7 +23,7 @@ import (
2323 type acceptanceRemoteFactory struct {
2424 mu sync.Mutex
2525 calls []string
26- handoff map[string]ports.AttachTarget
26+ handoff map[string]protocol.AttachTarget
2727 outputs chan string
2828 }
2929
@@ -37,7 +37,7 @@ func (f *acceptanceRemoteFactory) DialerForRemote(target string, _ string, _ por
3737
3838 type acceptanceRemoteDialer struct {
3939 target string
40- handoff ports.AttachTarget
40+ handoff protocol.AttachTarget
4141 outputs chan<- string
4242 }
4343
@@ -49,7 +49,7 @@ func (d acceptanceRemoteDialer) Dial(context.Context) (ports.Transport, error) {
4949 return clientTransport, nil
5050 }
5151
52-func serveAcceptanceRemote(tr ports.Transport, target string, handoff ports.AttachTarget, outputs chan<- string) {
52+func serveAcceptanceRemote(tr ports.Transport, target string, handoff protocol.AttachTarget, outputs chan<- string) {
5353 defer func() { _ = tr.Close() }()
5454 welcomed := false
5555 for {
@@ -61,7 +61,7 @@ func serveAcceptanceRemote(tr ports.Transport, target string, handoff ports.Atta
6161 if frame.Type != ports.MsgHello {
6262 continue
6363 }
64- if err := tr.Send(ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(ports.Welcome{SessionID: target})}); err != nil {
64+ if err := tr.Send(ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(protocol.Welcome{SessionID: target})}); err != nil {
6565 return
6666 }
6767 welcomed = true
@@ -88,7 +88,7 @@ func serveAcceptanceRemote(tr ports.Transport, target string, handoff ports.Atta
8888 if err := tr.Send(ports.Frame{Type: ports.MsgOutput, Payload: payload}); err != nil {
8989 return
9090 }
91- _ = tr.Send(ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach})})
91+ _ = tr.Send(ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach})})
9292 for {
9393 if _, err := tr.Recv(); err != nil {
9494 return
@@ -139,7 +139,7 @@ func TestAcceptanceRemoteDirectAndPickerUseOnlyRemoteTransports(t *testing.T) {
139139 result := client.NewRunner(deps).Run(ctx, request)
140140 if request.SessionName == "work" {
141141 require.NoError(t, result, "the client handoff completes before the test injects the composition-root handoff")
142- return &client.AttachTargetError{Target: ports.AttachTarget{Endpoint: "picked.example", Session: "picked", Intent: ports.IntentAttach}}
142+ return &client.AttachTargetError{Target: protocol.AttachTarget{Endpoint: "picked.example", Session: "picked", Intent: protocol.IntentAttach}}
143143 }
144144 return result
145145 },
@@ -147,11 +147,11 @@ func TestAcceptanceRemoteDirectAndPickerUseOnlyRemoteTransports(t *testing.T) {
147147
148148 wireTransport, err := (acceptanceRemoteDialer{
149149 target: "picker-wire.example",
150- handoff: ports.AttachTarget{Endpoint: "picked.example", Session: "picked", Intent: ports.IntentAttach},
150+ handoff: protocol.AttachTarget{Endpoint: "picked.example", Session: "picked", Intent: protocol.IntentAttach},
151151 }).Dial(context.Background())
152152 require.NoError(t, err)
153- require.NoError(t, wireTransport.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(ports.Hello{
154- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24},
153+ require.NoError(t, wireTransport.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(protocol.Hello{
154+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24},
155155 })}))
156156 welcome, err := wireTransport.Recv()
157157 require.NoError(t, err)
@@ -165,8 +165,8 @@ func TestAcceptanceRemoteDirectAndPickerUseOnlyRemoteTransports(t *testing.T) {
165165 require.Equal(t, "picked.example", target.Endpoint)
166166 require.NoError(t, wireTransport.Close())
167167
168- require.NoError(t, runAttachWithDeps(context.Background(), ports.IntentAttach, "direct", "direct.example", "", nil, deps))
169- require.NoError(t, runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "picker.example", "", nil, deps))
168+ require.NoError(t, runAttachWithDeps(context.Background(), protocol.IntentAttach, "direct", "direct.example", "", nil, deps))
169+ require.NoError(t, runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "picker.example", "", nil, deps))
170170 factory.mu.Lock()
171171 calls := append([]string(nil), factory.calls...)
172172 factory.mu.Unlock()
M internal/app/remote_hosts.go +10 -9
@@ -14,6 +14,7 @@ import (
1414 "github.com/bnema/vev/internal/domain"
1515 "github.com/bnema/vev/internal/platform"
1616 "github.com/bnema/vev/internal/ports"
17+ "github.com/bnema/vev/internal/protocol"
1718 )
1819
1920 const (
@@ -27,7 +28,7 @@ type remoteHostDeps struct {
2728 stateDir func() string
2829 store ports.RemoteHostStore
2930 catalog ports.RemoteCatalogClient
30- localList func(context.Context) ([]ports.SessionInfo, error)
31+ localList func(context.Context) ([]protocol.SessionInfo, error)
3132 stdout io.Writer
3233 }
3334
@@ -174,7 +175,7 @@ func listAllSessions(ctx context.Context, deps remoteHostDeps, hosts []domain.Re
174175 }
175176 return err
176177 }
177- all := append([]ports.SessionInfo(nil), local...)
178+ all := append([]protocol.SessionInfo(nil), local...)
178179 var hostErrs []error
179180 for _, host := range hosts {
180181 if err := ctx.Err(); err != nil {
@@ -195,10 +196,10 @@ func listAllSessions(ctx context.Context, deps remoteHostDeps, hosts []domain.Re
195196 return nil
196197 }
197198
198-func catalogSessionsAsInfo(host string, sessions []ports.RemoteCatalogSession) []ports.SessionInfo {
199- out := make([]ports.SessionInfo, 0, len(sessions))
199+func catalogSessionsAsInfo(host string, sessions []ports.RemoteCatalogSession) []protocol.SessionInfo {
200+ out := make([]protocol.SessionInfo, 0, len(sessions))
200201 for _, session := range sessions {
201- info := ports.SessionInfo{
202+ info := protocol.SessionInfo{
202203 Name: domain.RemoteSessionDisplay(session.Name, host),
203204 Ephemeral: session.Ephemeral,
204205 Tabs: ports.SaturateUint16(ports.CatalogTabCount(session)),
@@ -206,14 +207,14 @@ func catalogSessionsAsInfo(host string, sessions []ports.RemoteCatalogSession) [
206207 }
207208 switch session.State {
208209 case ports.RemoteCatalogSessionUp:
209- info.State = ports.SessionUp
210+ info.State = protocol.SessionUp
210211 case ports.RemoteCatalogSessionDown:
211- info.State = ports.SessionDown
212+ info.State = protocol.SessionDown
212213 case ports.RemoteCatalogSessionBroken:
213- info.State = ports.SessionBroken
214+ info.State = protocol.SessionBroken
214215 default:
215216 slog.Debug("remote catalog session has unknown state", "host", host, "session", session.Name, "state", session.State)
216- info.State = ports.SessionBroken
217+ info.State = protocol.SessionBroken
217218 }
218219 out = append(out, info)
219220 }
M internal/app/remote_test.go +15 -14
@@ -11,6 +11,7 @@ import (
1111 "github.com/bnema/vev/internal/domain"
1212 "github.com/bnema/vev/internal/ports"
1313 portsmocks "github.com/bnema/vev/internal/ports/mocks"
14+ "github.com/bnema/vev/internal/protocol"
1415 "github.com/bnema/vev/internal/usecase/client"
1516 "github.com/stretchr/testify/mock"
1617 "github.com/stretchr/testify/require"
@@ -57,7 +58,7 @@ func TestDecodeSessionListErrorReply(t *testing.T) {
5758 t.Run("valid", func(t *testing.T) {
5859 _, err := decodeSessionListReply(ports.Frame{
5960 Type: ports.MsgError,
60- Payload: ports.MarshalErrorMsg(ports.ErrorMsg{Text: "denied"}),
61+ Payload: ports.MarshalErrorMsg(protocol.ErrorMsg{Text: "denied"}),
6162 })
6263 require.EqualError(t, err, "vev: denied")
6364 })
@@ -193,8 +194,8 @@ func TestListAllSessionsInvariants(t *testing.T) {
193194 localErr := errors.New("local release failed")
194195 var out bytes.Buffer
195196 err := listAllSessions(context.Background(), remoteHostDeps{
196- localList: func(context.Context) ([]ports.SessionInfo, error) {
197- return []ports.SessionInfo{{Name: "local", State: ports.SessionUp}}, localErr
197+ localList: func(context.Context) ([]protocol.SessionInfo, error) {
198+ return []protocol.SessionInfo{{Name: "local", State: protocol.SessionUp}}, localErr
198199 },
199200 stdout: &out,
200201 }, nil)
@@ -215,8 +216,8 @@ func TestListAllSessionsInvariants(t *testing.T) {
215216 var out bytes.Buffer
216217 err := listAllSessions(ctx, remoteHostDeps{
217218 catalog: catalog,
218- localList: func(context.Context) ([]ports.SessionInfo, error) {
219- return []ports.SessionInfo{{Name: "local", State: ports.SessionUp}}, nil
219+ localList: func(context.Context) ([]protocol.SessionInfo, error) {
220+ return []protocol.SessionInfo{{Name: "local", State: protocol.SessionUp}}, nil
220221 },
221222 stdout: &out,
222223 }, []domain.RemoteHost{{Target: "arch"}, {Target: "mule"}})
@@ -240,8 +241,8 @@ func TestListAllSessionsInvariants(t *testing.T) {
240241 var out bytes.Buffer
241242 err := listAllSessions(context.Background(), remoteHostDeps{
242243 catalog: catalog,
243- localList: func(context.Context) ([]ports.SessionInfo, error) {
244- return []ports.SessionInfo{{Name: "local", State: ports.SessionUp}}, nil
244+ localList: func(context.Context) ([]protocol.SessionInfo, error) {
245+ return []protocol.SessionInfo{{Name: "local", State: protocol.SessionUp}}, nil
245246 },
246247 stdout: &out,
247248 }, []domain.RemoteHost{{Target: "arch"}, {Target: "mule"}})
@@ -262,13 +263,13 @@ func TestCatalogSessionsAsInfoInvariants(t *testing.T) {
262263 tests := []struct {
263264 name string
264265 session ports.RemoteCatalogSession
265- wantState ports.SessionState
266+ wantState protocol.SessionState
266267 wantTabs uint16
267268 }{
268- {name: "up", session: ports.RemoteCatalogSession{Name: "dev", State: "up", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: ports.SessionUp, wantTabs: 2},
269- {name: "down", session: ports.RemoteCatalogSession{Name: "dev", State: "down", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: ports.SessionDown, wantTabs: 2},
270- {name: "broken", session: ports.RemoteCatalogSession{Name: "dev", State: "broken", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: ports.SessionBroken, wantTabs: 2},
271- {name: "unknown fails closed", session: ports.RemoteCatalogSession{Name: "dev", State: "unknown", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: ports.SessionBroken, wantTabs: 2},
269+ {name: "up", session: ports.RemoteCatalogSession{Name: "dev", State: "up", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: protocol.SessionUp, wantTabs: 2},
270+ {name: "down", session: ports.RemoteCatalogSession{Name: "dev", State: "down", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: protocol.SessionDown, wantTabs: 2},
271+ {name: "broken", session: ports.RemoteCatalogSession{Name: "dev", State: "broken", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: protocol.SessionBroken, wantTabs: 2},
272+ {name: "unknown fails closed", session: ports.RemoteCatalogSession{Name: "dev", State: "unknown", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: protocol.SessionBroken, wantTabs: 2},
272273 }
273274 for _, tt := range tests {
274275 t.Run(tt.name, func(t *testing.T) {
@@ -288,7 +289,7 @@ func TestRunAttachWithDepsRemoteLearning(t *testing.T) {
288289 factory.EXPECT().DialerForRemote("build@mule", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once()
289290
290291 var learner ports.RemoteHostLearner
291- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "build@mule", "", nil, runAttachDeps{
292+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "build@mule", "", nil, runAttachDeps{
292293 remoteDialerFactory: factory,
293294 hostStore: store,
294295 runClient: func(_ context.Context, deps client.Dependencies, _ client.AttachRequest) error {
@@ -307,7 +308,7 @@ func TestRunAttachWithDepsAlwaysLearnsRemoteHost(t *testing.T) {
307308 factory := portsmocks.NewMockRemoteDialerFactory(t)
308309 factory.EXPECT().DialerForRemote("arch", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once()
309310 var learner ports.RemoteHostLearner
310- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "arch", "", nil, runAttachDeps{
311+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "arch", "", nil, runAttachDeps{
311312 remoteDialerFactory: factory,
312313 hostStore: store,
313314 runClient: func(_ context.Context, deps client.Dependencies, _ client.AttachRequest) error {
M internal/app/run.go +28 -28
@@ -155,7 +155,7 @@ func Run(args []string) error {
155155 // I/O.
156156 func parseArgs(args []string) (command, error) {
157157 if len(args) == 0 {
158- return command{kind: kindAttach, intent: ports.IntentEphemeral}, nil
158+ return command{kind: kindAttach, intent: protocol.IntentEphemeral}, nil
159159 }
160160
161161 switch args[0] {
@@ -193,7 +193,7 @@ func parseArgs(args []string) (command, error) {
193193 if err := domain.ValidateSessionName(args[1]); err != nil {
194194 return command{}, err
195195 }
196- return command{kind: kindAttach, intent: ports.IntentNew, name: args[1]}, nil
196+ return command{kind: kindAttach, intent: protocol.IntentNew, name: args[1]}, nil
197197 case "attach", "a":
198198 if len(args) < 2 || args[1] == "" {
199199 return command{}, usagef("`attach` requires a session name")
@@ -201,7 +201,7 @@ func parseArgs(args []string) (command, error) {
201201 if len(args) > 2 {
202202 return command{}, usagef("`attach` accepts exactly one session name or remote target")
203203 }
204- cmd := command{kind: kindAttach, intent: ports.IntentAttach, name: args[1]}
204+ cmd := command{kind: kindAttach, intent: protocol.IntentAttach, name: args[1]}
205205 if target, session, ok := parseRemoteAttachTarget(args[1]); ok {
206206 if err := domain.ValidateRemoteHostTarget(target); err != nil {
207207 return command{}, err
@@ -214,7 +214,7 @@ func parseArgs(args []string) (command, error) {
214214 cmd.remoteTarget = target
215215 cmd.name = session
216216 if session == "" {
217- cmd.intent = ports.IntentEphemeral
217+ cmd.intent = protocol.IntentEphemeral
218218 }
219219 }
220220 return cmd, nil
@@ -828,8 +828,8 @@ func remoteTransportModeFromEnv(value string) (ports.RemoteTransportMode, error)
828828 }
829829 }
830830
831-func validateRemoteAttachHandoff(target ports.AttachTarget) error {
832- if err := ports.ValidateAttachTarget(target); err != nil {
831+func validateRemoteAttachHandoff(target protocol.AttachTarget) error {
832+ if err := protocol.ValidateAttachTarget(target); err != nil {
833833 return err
834834 }
835835 if err := domain.ValidateRemoteHostTarget(target.Endpoint); err != nil {
@@ -839,7 +839,7 @@ func validateRemoteAttachHandoff(target ports.AttachTarget) error {
839839 return err
840840 }
841841 if target.RemoteTarget != nil {
842- if target.EnvironmentPolicy != ports.EnvironmentPolicyDaemonOwned {
842+ if target.EnvironmentPolicy != protocol.EnvironmentPolicyDaemonOwned {
843843 return errors.New("remote picker handoff must use daemon-owned environment")
844844 }
845845 if target.RemoteTarget.Endpoint != target.Endpoint || target.RemoteTarget.SessionName != target.Session {
@@ -874,7 +874,7 @@ const maxAttachTargetHandoffs = 32
874874
875875 func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, activeSession string, log *slog.Logger, deps runAttachDeps) error {
876876 if activeSession != "" {
877- if remoteTarget == "" && intent == ports.IntentNew {
877+ if remoteTarget == "" && intent == protocol.IntentNew {
878878 return deps.createDetached(ctx, name)
879879 }
880880 return errors.New("vev: sessions should be nested with care; unset VEV to force")
@@ -891,7 +891,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
891891 mode, modeErr := remoteTransportModeFromEnv(deps.selectedRemoteTransport)
892892 factory := deps.remoteDialerFactory
893893 var remoteSelection *domain.RemoteSessionTarget
894- var remoteEnvironmentPolicy ports.EnvironmentPolicy
894+ var remoteEnvironmentPolicy protocol.EnvironmentPolicy
895895 remoteDisplayOrigin := domain.RemoteDisplayOrigin(remoteTarget)
896896 routeOrigin := protocol.RouteOriginLocal
897897 routeOriginKey := "local"
@@ -906,13 +906,13 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
906906 }
907907 }
908908 pickerHandoff := remoteTarget == ""
909- pickerEnvironmentPolicy := func(target *domain.RemoteSessionTarget, policy ports.EnvironmentPolicy) ports.EnvironmentPolicy {
909+ pickerEnvironmentPolicy := func(target *domain.RemoteSessionTarget, policy protocol.EnvironmentPolicy) protocol.EnvironmentPolicy {
910910 if pickerHandoff && target == nil {
911- return ports.EnvironmentPolicyDaemonOwned
911+ return protocol.EnvironmentPolicyDaemonOwned
912912 }
913913 return policy
914914 }
915- handoff := func(target ports.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
915+ handoff := func(target protocol.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
916916 if err := validateRemoteAttachHandoff(target); err != nil {
917917 return nil, client.AttachRequest{}, fmt.Errorf("vev: invalid remote attach handoff: %w", err)
918918 }
@@ -1051,11 +1051,11 @@ func (d localDaemonDialer) Dial(ctx context.Context) (ports.Transport, error) {
10511051 return ensureDaemonWithLifecycle(ctx, d.dir, dial, realSpawn, defaultBackoff)
10521052 }
10531053
1054-func detachedLocalHello(name, cwd string) ports.Hello {
1054+func detachedLocalHello(name, cwd string) protocol.Hello {
10551055 termEnv := os.Getenv("TERM")
1056- return ports.Hello{
1057- Version: ports.ProtocolVersion,
1058- Intent: ports.IntentNew,
1056+ return protocol.Hello{
1057+ Version: protocol.Version,
1058+ Intent: protocol.IntentNew,
10591059 Name: name,
10601060 Size: domain.Size{Cols: 80, Rows: 24},
10611061 TermEnv: termEnv,
@@ -1126,7 +1126,7 @@ func requestDaemonStop(ctx context.Context) error {
11261126 return errors.Join(errors.New("vev: no daemon running"), owner.Release())
11271127 }
11281128 defer func() { _ = transport.Close() }()
1129- if err := transport.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(ports.Kill{All: true})}); err != nil {
1129+ if err := transport.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(protocol.Kill{All: true})}); err != nil {
11301130 return fmt.Errorf("vev: requesting daemon stop: %w", err)
11311131 }
11321132 if _, err := transport.Recv(); err != nil && !errors.Is(err, io.EOF) {
@@ -1456,7 +1456,7 @@ func runList(ctx context.Context, cmd command) (retErr error) {
14561456 return nil
14571457 }
14581458
1459-func listLocalSessions(ctx context.Context) (_ []ports.SessionInfo, retErr error) {
1459+func listLocalSessions(ctx context.Context) (_ []protocol.SessionInfo, retErr error) {
14601460 transport, owner, err := waitForDaemonOrLifecycle(ctx, ipc.SocketDir(), realDial, defaultBackoff)
14611461 if err != nil {
14621462 return nil, fmt.Errorf("vev: waiting for durable session state: %w", err)
@@ -1470,19 +1470,19 @@ func listLocalSessions(ctx context.Context) (_ []ports.SessionInfo, retErr error
14701470 }
14711471 return nil, fmt.Errorf("vev: reading stored sessions: %w", loadErr)
14721472 }
1473- infos := make([]ports.SessionInfo, 0, len(records))
1473+ infos := make([]protocol.SessionInfo, 0, len(records))
14741474 for _, r := range records {
1475- state := ports.SessionDown
1475+ state := protocol.SessionDown
14761476 if r.DegradedReason != "" {
1477- state = ports.SessionBroken
1477+ state = protocol.SessionBroken
14781478 }
1479- infos = append(infos, ports.SessionInfo{Name: r.Name, State: state})
1479+ infos = append(infos, protocol.SessionInfo{Name: r.Name, State: state})
14801480 }
14811481 return infos, nil
14821482 }
14831483 defer func() { _ = transport.Close() }()
14841484
1485- if err := transport.Send(ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(ports.List{})}); err != nil {
1485+ if err := transport.Send(ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(protocol.List{})}); err != nil {
14861486 return nil, fmt.Errorf("vev: requesting session list: %w", err)
14871487 }
14881488 reply, err := transport.Recv()
@@ -1492,7 +1492,7 @@ func listLocalSessions(ctx context.Context) (_ []ports.SessionInfo, retErr error
14921492 return decodeSessionListReply(reply)
14931493 }
14941494
1495-func decodeSessionListReply(reply ports.Frame) ([]ports.SessionInfo, error) {
1495+func decodeSessionListReply(reply ports.Frame) ([]protocol.SessionInfo, error) {
14961496 if reply.Type == ports.MsgError {
14971497 em, err := ports.UnmarshalErrorMsg(reply.Payload)
14981498 if err != nil {
@@ -1511,7 +1511,7 @@ func decodeSessionListReply(reply ports.Frame) ([]ports.SessionInfo, error) {
15111511 }
15121512
15131513 // printSessions renders a session table (or a friendly note when empty).
1514-func printSessions(w io.Writer, sessions []ports.SessionInfo) {
1514+func printSessions(w io.Writer, sessions []protocol.SessionInfo) {
15151515 if len(sessions) == 0 {
15161516 _, _ = fmt.Fprintln(w, "no sessions")
15171517 return
@@ -1523,10 +1523,10 @@ func printSessions(w io.Writer, sessions []ports.SessionInfo) {
15231523 tabs := fmt.Sprintf("%d", s.Tabs)
15241524 attached := "no"
15251525 switch s.State {
1526- case ports.SessionDown:
1526+ case protocol.SessionDown:
15271527 state = "down"
15281528 tabs = "-"
1529- case ports.SessionBroken:
1529+ case protocol.SessionBroken:
15301530 state = "broken"
15311531 tabs = "-"
15321532 default:
@@ -1596,7 +1596,7 @@ func runKill(ctx context.Context, name string, all, daemon bool) (retErr error)
15961596 }
15971597 defer func() { _ = transport.Close() }()
15981598
1599- if err := transport.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(ports.Kill{Name: name, All: all || daemon})}); err != nil {
1599+ if err := transport.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(protocol.Kill{Name: name, All: all || daemon})}); err != nil {
16001600 return fmt.Errorf("vev: requesting kill: %w", err)
16011601 }
16021602 reply, err := transport.Recv()
M internal/app/run_test.go +62 -62
@@ -437,20 +437,20 @@ func TestParseArgs(t *testing.T) {
437437 wantErr bool
438438 nonUsageErr bool
439439 }{
440- {name: "no args -> ephemeral attach", args: nil, wantKind: kindAttach, wantIntent: ports.IntentEphemeral},
441- {name: "empty slice -> ephemeral attach", args: []string{}, wantKind: kindAttach, wantIntent: ports.IntentEphemeral},
442- {name: "new named", args: []string{"new", "work"}, wantKind: kindAttach, wantIntent: ports.IntentNew, wantName: "work"},
440+ {name: "no args -> ephemeral attach", args: nil, wantKind: kindAttach, wantIntent: protocol.IntentEphemeral},
441+ {name: "empty slice -> ephemeral attach", args: []string{}, wantKind: kindAttach, wantIntent: protocol.IntentEphemeral},
442+ {name: "new named", args: []string{"new", "work"}, wantKind: kindAttach, wantIntent: protocol.IntentNew, wantName: "work"},
443443 {name: "new without name", args: []string{"new"}, wantErr: true},
444444 {name: "new empty name", args: []string{"new", ""}, wantErr: true},
445445 {name: "new command override unsupported", args: []string{"new", "work", "--", "sh"}, wantErr: true},
446- {name: "attach named", args: []string{"attach", "work"}, wantKind: kindAttach, wantIntent: ports.IntentAttach, wantName: "work"},
447- {name: "attach preserves legacy unsafe name", args: []string{"attach", "my work"}, wantKind: kindAttach, wantIntent: ports.IntentAttach, wantName: "my work"},
448- {name: "attach alias a", args: []string{"a", "work"}, wantKind: kindAttach, wantIntent: ports.IntentAttach, wantName: "work"},
449- {name: "attach remote host uses ephemeral", args: []string{"attach", "user@example.com"}, wantKind: kindAttach, wantIntent: ports.IntentEphemeral, wantRemote: "user@example.com"},
450- {name: "attach remote host with empty session uses ephemeral", args: []string{"attach", "user@example.com:"}, wantKind: kindAttach, wantIntent: ports.IntentEphemeral, wantRemote: "user@example.com"},
451- {name: "attach remote host with session", args: []string{"attach", "user@example.com:work"}, wantKind: kindAttach, wantIntent: ports.IntentAttach, wantName: "work", wantRemote: "user@example.com"},
452- {name: "attach remote ipv4 with session", args: []string{"attach", "user@192.0.2.10:work"}, wantKind: kindAttach, wantIntent: ports.IntentAttach, wantName: "work", wantRemote: "user@192.0.2.10"},
453- {name: "attach remote ipv6 with session", args: []string{"attach", "user@[2001:db8::1]:work"}, wantKind: kindAttach, wantIntent: ports.IntentAttach, wantName: "work", wantRemote: "user@[2001:db8::1]"},
446+ {name: "attach named", args: []string{"attach", "work"}, wantKind: kindAttach, wantIntent: protocol.IntentAttach, wantName: "work"},
447+ {name: "attach preserves legacy unsafe name", args: []string{"attach", "my work"}, wantKind: kindAttach, wantIntent: protocol.IntentAttach, wantName: "my work"},
448+ {name: "attach alias a", args: []string{"a", "work"}, wantKind: kindAttach, wantIntent: protocol.IntentAttach, wantName: "work"},
449+ {name: "attach remote host uses ephemeral", args: []string{"attach", "user@example.com"}, wantKind: kindAttach, wantIntent: protocol.IntentEphemeral, wantRemote: "user@example.com"},
450+ {name: "attach remote host with empty session uses ephemeral", args: []string{"attach", "user@example.com:"}, wantKind: kindAttach, wantIntent: protocol.IntentEphemeral, wantRemote: "user@example.com"},
451+ {name: "attach remote host with session", args: []string{"attach", "user@example.com:work"}, wantKind: kindAttach, wantIntent: protocol.IntentAttach, wantName: "work", wantRemote: "user@example.com"},
452+ {name: "attach remote ipv4 with session", args: []string{"attach", "user@192.0.2.10:work"}, wantKind: kindAttach, wantIntent: protocol.IntentAttach, wantName: "work", wantRemote: "user@192.0.2.10"},
453+ {name: "attach remote ipv6 with session", args: []string{"attach", "user@[2001:db8::1]:work"}, wantKind: kindAttach, wantIntent: protocol.IntentAttach, wantName: "work", wantRemote: "user@[2001:db8::1]"},
454454 {name: "attach remote invalid session", args: []string{"attach", "user@example.com:my work"}, wantErr: true, nonUsageErr: true},
455455 {name: "attach extra arg", args: []string{"attach", "work", "extra"}, wantErr: true},
456456 {name: "attach without name", args: []string{"attach"}, wantErr: true},
@@ -566,10 +566,10 @@ func TestAttachTargetsCreateCommonSessionRequest(t *testing.T) {
566566 arg string
567567 want client.AttachRequest
568568 }{
569- {name: "local host", arg: "work", want: client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work"}},
570- {name: "remote host", arg: "user@example.com:work", want: client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work"}},
571- {name: "remote ipv4", arg: "user@192.0.2.10:work", want: client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work"}},
572- {name: "remote ipv6", arg: "user@[2001:db8::1]:work", want: client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work"}},
569+ {name: "local host", arg: "work", want: client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work"}},
570+ {name: "remote host", arg: "user@example.com:work", want: client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work"}},
571+ {name: "remote ipv4", arg: "user@192.0.2.10:work", want: client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work"}},
572+ {name: "remote ipv6", arg: "user@[2001:db8::1]:work", want: client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work"}},
573573 }
574574 for _, tt := range tests {
575575 t.Run(tt.name, func(t *testing.T) {
@@ -587,10 +587,10 @@ func TestAttachTargetsCreateCommonSessionRequest(t *testing.T) {
587587
588588 func TestListShowsBroken(t *testing.T) {
589589 var out bytes.Buffer
590- printSessions(&out, []ports.SessionInfo{
591- {Name: "fresh", State: ports.SessionDown},
592- {Name: "loading", State: ports.SessionDown},
593- {Name: "broken", State: ports.SessionBroken},
590+ printSessions(&out, []protocol.SessionInfo{
591+ {Name: "fresh", State: protocol.SessionDown},
592+ {Name: "loading", State: protocol.SessionDown},
593+ {Name: "broken", State: protocol.SessionBroken},
594594 })
595595 require.Contains(t, out.String(), "fresh")
596596 require.Contains(t, out.String(), "down")
@@ -602,9 +602,9 @@ func TestListShowsBroken(t *testing.T) {
602602
603603 func TestPrintSessionsShowsDownState(t *testing.T) {
604604 var out bytes.Buffer
605- printSessions(&out, []ports.SessionInfo{
606- {Name: "main", State: ports.SessionUp, Tabs: 2, Attached: true},
607- {Name: "old", State: ports.SessionDown},
605+ printSessions(&out, []protocol.SessionInfo{
606+ {Name: "main", State: protocol.SessionUp, Tabs: 2, Attached: true},
607+ {Name: "old", State: protocol.SessionDown},
608608 })
609609 got := out.String()
610610 for _, want := range []string{"NAME", "STATE", "main", "up", "2", "yes", "old", "down", "-"} {
@@ -616,10 +616,10 @@ func TestPrintSessionsShowsDownState(t *testing.T) {
616616
617617 func TestPrintSessionsMarksEphemeral(t *testing.T) {
618618 var buf bytes.Buffer
619- printSessions(&buf, []ports.SessionInfo{
620- {Name: "0", State: ports.SessionUp, Ephemeral: true, Tabs: 1, Attached: false},
621- {Name: "work", State: ports.SessionUp, Tabs: 2, Attached: true},
622- {Name: "old", State: ports.SessionDown},
619+ printSessions(&buf, []protocol.SessionInfo{
620+ {Name: "0", State: protocol.SessionUp, Ephemeral: true, Tabs: 1, Attached: false},
621+ {Name: "work", State: protocol.SessionUp, Tabs: 2, Attached: true},
622+ {Name: "old", State: protocol.SessionDown},
623623 })
624624 out := buf.String()
625625 for _, want := range []string{"0", "temporary", "work", "up", "old", "down"} {
@@ -757,7 +757,7 @@ func captureStdout(t *testing.T, fn func()) string {
757757
758758 func TestRunAttachRejectsNestedVEVBeforeDial(t *testing.T) {
759759 called := false
760- err := runAttachWithDeps(context.Background(), ports.IntentEphemeral, "", "", "outer", nil, runAttachDeps{
760+ err := runAttachWithDeps(context.Background(), protocol.IntentEphemeral, "", "", "outer", nil, runAttachDeps{
761761 createDetached: func(context.Context, string) error {
762762 called = true
763763 return nil
@@ -776,7 +776,7 @@ func TestRunAttachRejectsNestedVEVBeforeDial(t *testing.T) {
776776
777777 func TestRunAttachNestedNewCreatesDetachedSession(t *testing.T) {
778778 var gotName string
779- err := runAttachWithDeps(context.Background(), ports.IntentNew, "scratch", "", "outer", nil, runAttachDeps{
779+ err := runAttachWithDeps(context.Background(), protocol.IntentNew, "scratch", "", "outer", nil, runAttachDeps{
780780 createDetached: func(_ context.Context, name string) error {
781781 gotName = name
782782 return nil
@@ -796,7 +796,7 @@ func TestDetachedLocalHelloIncludesTrueColor(t *testing.T) {
796796
797797 hello := detachedLocalHello("scratch", "/tmp/work")
798798
799- require.Equal(t, ports.IntentNew, hello.Intent)
799+ require.Equal(t, protocol.IntentNew, hello.Intent)
800800 require.Equal(t, "scratch", hello.Name)
801801 require.Equal(t, "xterm-direct", hello.TermEnv)
802802 require.Equal(t, "/tmp/work", hello.Cwd)
@@ -834,7 +834,7 @@ func TestRunAttachWithDepsSelectsRemoteTransport(t *testing.T) {
834834 factory := portsmocks.NewMockRemoteDialerFactory(t)
835835 factory.EXPECT().DialerForRemote("remote.example", "work", tt.wantMode, mock.Anything).Return(namedDialer{name: "remote"}, nil)
836836
837- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
837+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
838838 remoteDialerFactory: factory,
839839 selectedRemoteTransport: tt.selectedTransport,
840840 clipboard: clip,
@@ -845,11 +845,11 @@ func TestRunAttachWithDepsSelectsRemoteTransport(t *testing.T) {
845845 }
846846 gotDialer = nd.name
847847 gotRemote = deps.Remote
848- if !request.Remote || request.EnvironmentPolicy != ports.EnvironmentPolicyClientOwned {
848+ if !request.Remote || request.EnvironmentPolicy != protocol.EnvironmentPolicyClientOwned {
849849 t.Fatal("direct CLI remote attach must preserve client-owned environment policy")
850850 }
851851 gotClipboard = deps.Clipboard
852- if request.Intent != ports.IntentAttach || request.SessionName != "work" {
852+ if request.Intent != protocol.IntentAttach || request.SessionName != "work" {
853853 t.Fatalf("intent/name = %d/%q, want attach/work", request.Intent, request.SessionName)
854854 }
855855 return nil
@@ -876,18 +876,18 @@ func TestRunAttachWithDepsDirectLegacyCallbackPreservesClientOwnership(t *testin
876876 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-1"}, nil).Once()
877877 factory.EXPECT().DialerForRemote("selected.example", "picked", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-2"}, nil).Once()
878878
879- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
879+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
880880 remoteDialerFactory: factory,
881881 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {
882882 require.True(t, request.Remote)
883- require.Equal(t, ports.EnvironmentPolicyClientOwned, request.EnvironmentPolicy)
884- nextDialer, nextRequest, err := deps.AttachHandoff(ports.AttachTarget{
885- Endpoint: "selected.example", Session: "picked", Intent: ports.IntentAttach,
886- EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
883+ require.Equal(t, protocol.EnvironmentPolicyClientOwned, request.EnvironmentPolicy)
884+ nextDialer, nextRequest, err := deps.AttachHandoff(protocol.AttachTarget{
885+ Endpoint: "selected.example", Session: "picked", Intent: protocol.IntentAttach,
886+ EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
887887 })
888888 require.NoError(t, err)
889889 require.NotNil(t, nextDialer)
890- require.Equal(t, ports.EnvironmentPolicyClientOwned, nextRequest.EnvironmentPolicy)
890+ require.Equal(t, protocol.EnvironmentPolicyClientOwned, nextRequest.EnvironmentPolicy)
891891 return nil
892892 },
893893 })
@@ -899,15 +899,15 @@ func TestRunAttachWithDepsRemotePickerHandoffReopensDirectConnection(t *testing.
899899 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-1"}, nil).Once()
900900 factory.EXPECT().DialerForRemote("selected.example", "picked", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-2"}, nil).Once()
901901 var calls int
902- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
902+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
903903 remoteDialerFactory: factory,
904904 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {
905905 calls++
906906 require.NotNil(t, deps.Dialer)
907907 require.True(t, request.Remote)
908- require.Equal(t, ports.EnvironmentPolicyClientOwned, request.EnvironmentPolicy, "direct CLI remote ownership must survive a handoff")
908+ require.Equal(t, protocol.EnvironmentPolicyClientOwned, request.EnvironmentPolicy, "direct CLI remote ownership must survive a handoff")
909909 if calls == 1 {
910- return &client.AttachTargetError{Target: ports.AttachTarget{Endpoint: "selected.example", Session: "picked", Intent: ports.IntentAttach}}
910+ return &client.AttachTargetError{Target: protocol.AttachTarget{Endpoint: "selected.example", Session: "picked", Intent: protocol.IntentAttach}}
911911 }
912912 return nil
913913 },
@@ -921,11 +921,11 @@ func TestRunAttachWithDepsAllowsRevisitingRemoteHandoff(t *testing.T) {
921921 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once()
922922 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-revisit"}, nil).Once()
923923
924- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
924+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
925925 remoteDialerFactory: factory,
926926 runClient: func(_ context.Context, deps client.Dependencies, _ client.AttachRequest) error {
927- dialer, request, err := deps.AttachHandoff(ports.AttachTarget{
928- Endpoint: "remote.example", Session: "work", Intent: ports.IntentAttach,
927+ dialer, request, err := deps.AttachHandoff(protocol.AttachTarget{
928+ Endpoint: "remote.example", Session: "work", Intent: protocol.IntentAttach,
929929 })
930930 require.NoError(t, err)
931931 require.NotNil(t, dialer)
@@ -941,11 +941,11 @@ func TestRunAttachWithDepsBoundsRepeatedAttachTargetHandoffs(t *testing.T) {
941941 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Times(maxAttachTargetHandoffs + 1)
942942
943943 calls := 0
944- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
944+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
945945 remoteDialerFactory: factory,
946946 runClient: func(_ context.Context, _ client.Dependencies, _ client.AttachRequest) error {
947947 calls++
948- return &client.AttachTargetError{Target: ports.AttachTarget{Endpoint: "remote.example", Session: "work", Intent: ports.IntentAttach}}
948+ return &client.AttachTargetError{Target: protocol.AttachTarget{Endpoint: "remote.example", Session: "work", Intent: protocol.IntentAttach}}
949949 },
950950 })
951951 require.ErrorContains(t, err, "attach handoff exceeded maximum")
@@ -957,18 +957,18 @@ func TestRunAttachWithDepsLocalPickerHandoffAttachesSelectedRemote(t *testing.T)
957957 factory.EXPECT().DialerForRemote("selected.example", "picked", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once()
958958
959959 localCalls, remoteCalls := 0, 0
960- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "", "", nil, runAttachDeps{
960+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "", "", nil, runAttachDeps{
961961 localDialer: func() ports.Dialer { return namedDialer{name: "local"} },
962962 remoteDialerFactory: factory,
963963 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {
964964 if deps.Remote {
965965 remoteCalls++
966- require.Equal(t, client.AttachRequest{Intent: ports.IntentAttach, SessionName: "picked", Remote: true, Origin: protocol.RouteOriginDiscovery, OriginKey: "selected.example", HostLabel: "selected.example", EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}, request)
966+ require.Equal(t, client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "picked", Remote: true, Origin: protocol.RouteOriginDiscovery, OriginKey: "selected.example", HostLabel: "selected.example", EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}, request)
967967 return nil
968968 }
969969 localCalls++
970- require.Equal(t, client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work", Origin: protocol.RouteOriginLocal, OriginKey: "local"}, request)
971- return &client.AttachTargetError{Target: ports.AttachTarget{Endpoint: "selected.example", Session: "picked", Intent: ports.IntentAttach}}
970+ require.Equal(t, client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work", Origin: protocol.RouteOriginLocal, OriginKey: "local"}, request)
971+ return &client.AttachTargetError{Target: protocol.AttachTarget{Endpoint: "selected.example", Session: "picked", Intent: protocol.IntentAttach}}
972972 },
973973 })
974974 require.NoError(t, err)
@@ -979,20 +979,20 @@ func TestRunAttachWithDepsLocalPickerHandoffAttachesSelectedRemote(t *testing.T)
979979 func TestRunAttachWithDepsRejectsInvalidHandoffBeforeDialing(t *testing.T) {
980980 tests := []struct {
981981 name string
982- target ports.AttachTarget
982+ target protocol.AttachTarget
983983 }{
984- {name: "invalid host", target: ports.AttachTarget{Endpoint: "remote host", Session: "work", Intent: ports.IntentAttach}},
985- {name: "invalid session", target: ports.AttachTarget{Endpoint: "remote.example", Session: "bad name", Intent: ports.IntentAttach}},
986- {name: "tokenless resume", target: ports.AttachTarget{Endpoint: "remote.example", Session: "work", Intent: ports.IntentResume}},
984+ {name: "invalid host", target: protocol.AttachTarget{Endpoint: "remote host", Session: "work", Intent: protocol.IntentAttach}},
985+ {name: "invalid session", target: protocol.AttachTarget{Endpoint: "remote.example", Session: "bad name", Intent: protocol.IntentAttach}},
986+ {name: "tokenless resume", target: protocol.AttachTarget{Endpoint: "remote.example", Session: "work", Intent: protocol.IntentResume}},
987987 {
988988 name: "tokenless resume with remote target",
989- target: ports.AttachTarget{
990- Endpoint: "remote.example", Session: "work", Intent: ports.IntentResume,
989+ target: protocol.AttachTarget{
990+ Endpoint: "remote.example", Session: "work", Intent: protocol.IntentResume,
991991 RemoteTarget: &domain.RemoteSessionTarget{
992992 Endpoint: "remote.example", DisplayOrigin: "remote.example",
993993 LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work", LiveTabID: "tab-1",
994994 },
995- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
995+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
996996 },
997997 },
998998 }
@@ -1001,7 +1001,7 @@ func TestRunAttachWithDepsRejectsInvalidHandoffBeforeDialing(t *testing.T) {
10011001 factory := portsmocks.NewMockRemoteDialerFactory(t)
10021002 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once()
10031003 calls := 0
1004- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
1004+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
10051005 remoteDialerFactory: factory,
10061006 runClient: func(_ context.Context, _ client.Dependencies, _ client.AttachRequest) error {
10071007 calls++
@@ -1018,7 +1018,7 @@ func TestRunAttachWithDepsRejectsInvalidRemoteTransportBeforeDialing(t *testing.
10181018 factory := portsmocks.NewMockRemoteDialerFactory(t)
10191019 runClientCalled := false
10201020
1021- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
1021+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
10221022 remoteDialerFactory: factory,
10231023 selectedRemoteTransport: "serial",
10241024 runClient: func(context.Context, client.Dependencies, client.AttachRequest) error {
@@ -1040,7 +1040,7 @@ func TestRunAttachWithDepsReturnsFactoryErrorBeforeRunClient(t *testing.T) {
10401040 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(nil, factoryErr)
10411041 runClientCalled := false
10421042
1043- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
1043+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
10441044 remoteDialerFactory: factory,
10451045 runClient: func(context.Context, client.Dependencies, client.AttachRequest) error {
10461046 runClientCalled = true
@@ -1060,7 +1060,7 @@ func TestRunAttachWithDepsBuildsLocalDialer(t *testing.T) {
10601060 gotRemote := true
10611061 gotClipboard := ports.ClipboardReader(&fakeClipboardReader{})
10621062 factory := portsmocks.NewMockRemoteDialerFactory(t)
1063- err := runAttachWithDeps(context.Background(), ports.IntentEphemeral, "", "", "", nil, runAttachDeps{
1063+ err := runAttachWithDeps(context.Background(), protocol.IntentEphemeral, "", "", "", nil, runAttachDeps{
10641064 localDialer: func() ports.Dialer { return namedDialer{name: "local"} },
10651065 remoteDialerFactory: factory,
10661066 clipboard: &fakeClipboardReader{}, // must NOT reach runClient for a local attach
@@ -1075,7 +1075,7 @@ func TestRunAttachWithDepsBuildsLocalDialer(t *testing.T) {
10751075 t.Fatal("local carriage metadata must not be in the attach request")
10761076 }
10771077 gotClipboard = deps.Clipboard
1078- if request.Intent != ports.IntentEphemeral || request.SessionName != "" {
1078+ if request.Intent != protocol.IntentEphemeral || request.SessionName != "" {
10791079 t.Fatalf("intent/name = %d/%q, want ephemeral/empty", request.Intent, request.SessionName)
10801080 }
10811081 return nil
M internal/app/transport_observability_test.go +3 -2
@@ -15,6 +15,7 @@ import (
1515
1616 "github.com/bnema/vev/internal/ports"
1717 portsmocks "github.com/bnema/vev/internal/ports/mocks"
18+ "github.com/bnema/vev/internal/protocol"
1819 "github.com/bnema/vev/internal/usecase/client"
1920 "github.com/stretchr/testify/mock"
2021 )
@@ -189,7 +190,7 @@ func TestRunAttachJoinsTraceCloseError(t *testing.T) {
189190 t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
190191 t.Setenv(envRemoteTransport, "invalid")
191192
192- err := runAttach(context.Background(), ports.IntentEphemeral, "", "host")
193+ err := runAttach(context.Background(), protocol.IntentEphemeral, "", "host")
193194 if !strings.Contains(err.Error(), "invalid remote transport") {
194195 t.Fatalf("runAttach() error = %v, want remote transport validation error", err)
195196 }
@@ -246,7 +247,7 @@ func TestRunAttachPropagatesOneObserverToRemoteTransportFactory(t *testing.T) {
246247 }
247248 t.Cleanup(func() { runClientWithDeps = originalRunClient })
248249
249- if err := runAttach(context.Background(), ports.IntentAttach, "work", "remote.example"); err != nil {
250+ if err := runAttach(context.Background(), protocol.IntentAttach, "work", "remote.example"); err != nil {
250251 t.Fatal(err)
251252 }
252253 if traceCalls != 1 || factoryCalls != 1 {
M internal/persist/catalogue_test.go +2 -2
@@ -9,7 +9,7 @@ import (
99 "github.com/stretchr/testify/require"
1010
1111 "github.com/bnema/vev/internal/domain"
12- "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
1313 "github.com/bnema/vev/pkg/kv"
1414 "github.com/bnema/vev/pkg/safedir"
1515 )
@@ -57,7 +57,7 @@ func TestOpenOrCreateReportsRecordsFromOlderProtocol(t *testing.T) {
5757 TabNames: []string{"shell", "logs"},
5858 Committed: &domain.CheckpointRef{Generation: 3, ManifestDigest: [32]byte{3}},
5959 }
60- olderProtocol, err := encodeRecordValueForProtocol(record, ports.ProtocolVersion-1)
60+ olderProtocol, err := encodeRecordValueForProtocol(record, protocol.Version-1)
6161 require.NoError(t, err)
6262 protocolLess, err := encodeRecordValue(record)
6363 require.NoError(t, err)
M internal/persist/persist.go +2 -1
@@ -10,6 +10,7 @@ import (
1010
1111 "github.com/bnema/vev/internal/domain"
1212 "github.com/bnema/vev/internal/ports"
13+ "github.com/bnema/vev/internal/protocol"
1314 "github.com/bnema/vev/pkg/kv"
1415 )
1516
@@ -454,7 +455,7 @@ func (p *Persister) loadIncompatibleRecords() ([]domain.CatalogueRecord, error)
454455 decodeErr = err
455456 return false
456457 }
457- if stored.protocolVersion != ports.ProtocolVersion {
458+ if stored.protocolVersion != protocol.Version {
458459 records = append(records, stored.record)
459460 }
460461 return true
M internal/persist/record.go +2 -2
@@ -7,7 +7,7 @@ import (
77 "math"
88
99 "github.com/bnema/vev/internal/domain"
10- "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
1111 )
1212
1313 const (
@@ -24,7 +24,7 @@ var catalogueMagic = [4]byte{'V', 'E', 'V', 'C'}
2424 type Record = domain.CatalogueRecord
2525
2626 func encodeRecordValue(record domain.CatalogueRecord) ([]byte, error) {
27- return encodeRecordValueForProtocol(record, ports.ProtocolVersion)
27+ return encodeRecordValueForProtocol(record, protocol.Version)
2828 }
2929
3030 func encodeRecordValueForProtocol(record domain.CatalogueRecord, protocolVersion uint16) ([]byte, error) {
M internal/ports/frame_test.go +4 -2
@@ -2,11 +2,13 @@ package ports
22
33 import (
44 "testing"
5+
6+ "github.com/bnema/vev/internal/protocol"
57 )
68
79 func TestProtocolVersion(t *testing.T) {
8- if ProtocolVersion != 37 {
9- t.Fatalf("ProtocolVersion = %d, want 37", ProtocolVersion)
10+ if protocol.Version != 37 {
11+ t.Fatalf("ProtocolVersion = %d, want 37", protocol.Version)
1012 }
1113 }
1214
M internal/ports/navigation_test.go +14 -14
@@ -133,36 +133,36 @@ func TestParkedRouteWireRoundTripsStrictly(t *testing.T) {
133133 }
134134
135135 func TestHelloNavigationValidationTable(t *testing.T) {
136- base := Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}
136+ base := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}
137137 remoteTarget := &domain.RemoteSessionTarget{
138138 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: domain.SessionLifecycleID{1},
139139 SessionName: "work", LiveTabID: "tab-1",
140140 }
141141 tests := []struct {
142142 name string
143- hello Hello
143+ hello protocol.Hello
144144 valid bool
145145 capability protocol.NavigationCapabilities
146146 overlay protocol.StartupOverlay
147147 wantPayload string
148148 }{
149149 {name: "ordinary", hello: base, valid: true},
150- {name: "resume route", hello: Hello{Version: ProtocolVersion, Intent: IntentResume, Size: domain.Size{Cols: 80, Rows: 24}}, valid: true},
151- {name: "daemon-owned home picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityHomePicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: true, capability: protocol.NavigationCapabilityHomePicker},
152- {name: "remote-target home picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: protocol.NavigationCapabilityHomePicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: true, capability: protocol.NavigationCapabilityHomePicker},
153- {name: "client-owned home picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityHomePicker}, valid: false},
154- {name: "daemon-owned back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false},
155- {name: "remote-target back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false},
156- {name: "back with startup picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: true, capability: protocol.NavigationCapabilityBack, overlay: protocol.StartupOverlaySessionPicker, wantPayload: "002502" + "00000000000000000000000000000000" + "0000000000000000" + "0000" + "00500018" + "00000000" + "0000" + "0000" + "00" + "00" + "00000000" + "00" + "00" + "00" + "0000" + "02010000"},
157- {name: "new rejects navigation", hello: Hello{Version: ProtocolVersion, Intent: IntentNew, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: false},
158- {name: "unknown capability", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: 4}, valid: false},
159- {name: "back without picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: false},
160- {name: "picker without back", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: false},
150+ {name: "resume route", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentResume, Size: domain.Size{Cols: 80, Rows: 24}}, valid: true},
151+ {name: "daemon-owned home picker", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityHomePicker, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}, valid: true, capability: protocol.NavigationCapabilityHomePicker},
152+ {name: "remote-target home picker", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: protocol.NavigationCapabilityHomePicker, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}, valid: true, capability: protocol.NavigationCapabilityHomePicker},
153+ {name: "client-owned home picker rejected", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityHomePicker}, valid: false},
154+ {name: "daemon-owned back picker rejected", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}, valid: false},
155+ {name: "remote-target back picker rejected", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}, valid: false},
156+ {name: "back with startup picker", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: true, capability: protocol.NavigationCapabilityBack, overlay: protocol.StartupOverlaySessionPicker, wantPayload: "002502" + "00000000000000000000000000000000" + "0000000000000000" + "0000" + "00500018" + "00000000" + "0000" + "0000" + "00" + "00" + "00000000" + "00" + "00" + "00" + "0000" + "02010000"},
157+ {name: "new rejects navigation", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: false},
158+ {name: "unknown capability", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: 4}, valid: false},
159+ {name: "back without picker", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: false},
160+ {name: "picker without back", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: false},
161161 }
162162
163163 for _, tt := range tests {
164164 t.Run(tt.name, func(t *testing.T) {
165- err := ValidateHello(tt.hello)
165+ err := protocol.ValidateHello(tt.hello)
166166 if tt.valid {
167167 require.NoError(t, err)
168168 payload := MarshalHello(tt.hello)
M internal/ports/remote.go +3 -3
@@ -193,8 +193,8 @@ var (
193193 // ValidateRemoteCatalog applies the exact current schema and all bounds before
194194 // a catalogue is cached or rendered.
195195 func ValidateRemoteCatalog(c RemoteCatalog) error {
196- if c.ProtocolVersion != ProtocolVersion {
197- return &RemoteCatalogVersionMismatchError{Got: c.ProtocolVersion, Want: ProtocolVersion, Kind: "protocol"}
196+ if c.ProtocolVersion != protocol.Version {
197+ return &RemoteCatalogVersionMismatchError{Got: c.ProtocolVersion, Want: protocol.Version, Kind: "protocol"}
198198 }
199199 if c.SchemaVersion != RemoteCatalogSchemaVersion {
200200 return &RemoteCatalogVersionMismatchError{Got: c.SchemaVersion, Want: RemoteCatalogSchemaVersion, Kind: "catalog"}
@@ -309,7 +309,7 @@ func ValidateRemoteCatalogCacheEntries(entries []RemoteCatalogCacheEntry) error
309309 return fmt.Errorf("%w: duplicate host", ErrInvalidRemoteCatalog)
310310 }
311311 seenHosts[entry.Host] = struct{}{}
312- catalog := RemoteCatalog{ProtocolVersion: ProtocolVersion, SchemaVersion: RemoteCatalogSchemaVersion, Sessions: entry.Sessions}
312+ catalog := RemoteCatalog{ProtocolVersion: protocol.Version, SchemaVersion: RemoteCatalogSchemaVersion, Sessions: entry.Sessions}
313313 if err := ValidateRemoteCatalog(catalog); err != nil {
314314 return fmt.Errorf("%w: host %q: %v", ErrInvalidRemoteCatalog, entry.Host, err)
315315 }
M internal/ports/remote_preview_hardening_test.go +6 -6
@@ -178,7 +178,7 @@ func TestRemoteTargetWireRejectsInvalidSelectorCombinations(t *testing.T) {
178178 t.Run(test.name, func(t *testing.T) {
179179 target := *testRemoteTarget(false)
180180 test.mutate(&target)
181- message := AttachTarget{Endpoint: target.Endpoint, Session: target.SessionName, Intent: IntentAttach, RemoteTarget: &target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}
181+ message := protocol.AttachTarget{Endpoint: target.Endpoint, Session: target.SessionName, Intent: protocol.IntentAttach, RemoteTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}
182182 if MarshalAttachTarget(message) != nil {
183183 t.Fatal("invalid target was marshaled")
184184 }
@@ -188,13 +188,13 @@ func TestRemoteTargetWireRejectsInvalidSelectorCombinations(t *testing.T) {
188188
189189 func TestRemoteTargetWireRichGoldenRoundTrip(t *testing.T) {
190190 target := testRemoteTarget(false)
191- got := MarshalAttachTarget(AttachTarget{
192- Endpoint: target.Endpoint, Session: target.SessionName, Intent: IntentAttach,
193- RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned,
191+ got := MarshalAttachTarget(protocol.AttachTarget{
192+ Endpoint: target.Endpoint, Session: target.SessionName, Intent: protocol.IntentAttach,
193+ RemoteTarget: target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
194194 })
195195 want := []byte{
196196 0x00, 0x0f, 'b', 'u', 'i', 'l', 'd', '@', 'm', 'u', 'l', 'e', ':', '2', '2', '2', '2',
197- 0x00, 0x04, 'w', 'o', 'r', 'k', IntentAttach,
197+ 0x00, 0x04, 'w', 'o', 'r', 'k', protocol.IntentAttach,
198198 0x01,
199199 0x00, 0x0f, 'b', 'u', 'i', 'l', 'd', '@', 'm', 'u', 'l', 'e', ':', '2', '2', '2', '2',
200200 0x00, 0x09, 'm', 'u', 'l', 'e', ':', '2', '2', '2', '2',
@@ -210,7 +210,7 @@ func TestRemoteTargetWireRichGoldenRoundTrip(t *testing.T) {
210210 if err != nil {
211211 t.Fatalf("UnmarshalAttachTarget() error = %v", err)
212212 }
213- if decoded.RemoteTarget == nil || *decoded.RemoteTarget != *target || decoded.EnvironmentPolicy != EnvironmentPolicyDaemonOwned {
213+ if decoded.RemoteTarget == nil || *decoded.RemoteTarget != *target || decoded.EnvironmentPolicy != protocol.EnvironmentPolicyDaemonOwned {
214214 t.Fatalf("decoded target = %#v, policy = %d", decoded.RemoteTarget, decoded.EnvironmentPolicy)
215215 }
216216 }
M internal/ports/remote_state_test.go +5 -4
@@ -3,6 +3,7 @@ package ports
33 import (
44 "testing"
55
6+ "github.com/bnema/vev/internal/protocol"
67 "github.com/stretchr/testify/require"
78 )
89
@@ -12,7 +13,7 @@ func TestRemoteCatalogStateContract(t *testing.T) {
1213 for _, state := range []RemoteCatalogSessionState{RemoteCatalogSessionUp, RemoteCatalogSessionDown, RemoteCatalogSessionBroken} {
1314 t.Run(string(state), func(t *testing.T) {
1415 err := ValidateRemoteCatalog(RemoteCatalog{
15- ProtocolVersion: ProtocolVersion,
16+ ProtocolVersion: protocol.Version,
1617 SchemaVersion: RemoteCatalogSchemaVersion,
1718 Sessions: []RemoteCatalogSession{{
1819 LifecycleID: [16]byte{1}, Name: "work", State: state,
@@ -24,7 +25,7 @@ func TestRemoteCatalogStateContract(t *testing.T) {
2425 }
2526
2627 err := ValidateRemoteCatalog(RemoteCatalog{
27- ProtocolVersion: ProtocolVersion,
28+ ProtocolVersion: protocol.Version,
2829 SchemaVersion: RemoteCatalogSchemaVersion,
2930 Sessions: []RemoteCatalogSession{{
3031 LifecycleID: [16]byte{1}, Name: "work", State: RemoteCatalogSessionState("running"),
@@ -33,7 +34,7 @@ func TestRemoteCatalogStateContract(t *testing.T) {
3334 })
3435 require.ErrorIs(t, err, ErrRemoteCatalogUnknownState)
3536
36- err = ValidateRemoteCatalog(RemoteCatalog{ProtocolVersion: ProtocolVersion, Sessions: []RemoteCatalogSession{}})
37+ err = ValidateRemoteCatalog(RemoteCatalog{ProtocolVersion: protocol.Version, Sessions: []RemoteCatalogSession{}})
3738 var mismatch *RemoteCatalogVersionMismatchError
3839 require.ErrorAs(t, err, &mismatch)
3940 require.Equal(t, "catalog", mismatch.Kind)
@@ -42,7 +43,7 @@ func TestRemoteCatalogStateContract(t *testing.T) {
4243
4344 func TestValidateRemoteCatalogRejectsDuplicateSessionNames(t *testing.T) {
4445 err := ValidateRemoteCatalog(RemoteCatalog{
45- ProtocolVersion: ProtocolVersion,
46+ ProtocolVersion: protocol.Version,
4647 SchemaVersion: RemoteCatalogSchemaVersion,
4748 Sessions: []RemoteCatalogSession{
4849 {LifecycleID: [16]byte{1}, Name: "work", State: RemoteCatalogSessionUp, Tabs: []RemoteCatalogTab{}},
M internal/ports/remote_target_wire_test.go +38 -37
@@ -6,6 +6,7 @@ import (
66 "testing"
77
88 "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/internal/protocol"
910 )
1011
1112 func testRemoteTarget(stopped bool) *domain.RemoteSessionTarget {
@@ -36,13 +37,13 @@ func TestRemoteTargetWireRoundTripPreservesExactSelector(t *testing.T) {
3637 }
3738 t.Run(name, func(t *testing.T) {
3839 target := testRemoteTarget(stopped)
39- msg := Hello{
40- Version: ProtocolVersion,
41- Intent: IntentAttach,
40+ msg := protocol.Hello{
41+ Version: protocol.Version,
42+ Intent: protocol.IntentAttach,
4243 Name: target.SessionName,
4344 Size: domain.Size{Cols: 80, Rows: 24},
4445 RemoteTarget: target,
45- EnvironmentPolicy: EnvironmentPolicyDaemonOwned,
46+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
4647 }
4748 payload := MarshalHello(msg)
4849 if payload == nil {
@@ -55,16 +56,16 @@ func TestRemoteTargetWireRoundTripPreservesExactSelector(t *testing.T) {
5556 if got.RemoteTarget == nil || *got.RemoteTarget != *target {
5657 t.Fatalf("remote target = %#v, want %#v", got.RemoteTarget, target)
5758 }
58- if got.EnvironmentPolicy != EnvironmentPolicyDaemonOwned {
59+ if got.EnvironmentPolicy != protocol.EnvironmentPolicyDaemonOwned {
5960 t.Fatalf("environment policy = %d, want daemon-owned", got.EnvironmentPolicy)
6061 }
6162
62- attachPayload := MarshalAttachTarget(AttachTarget{
63+ attachPayload := MarshalAttachTarget(protocol.AttachTarget{
6364 Endpoint: target.Endpoint,
6465 Session: target.SessionName,
65- Intent: IntentAttach,
66+ Intent: protocol.IntentAttach,
6667 RemoteTarget: target,
67- EnvironmentPolicy: EnvironmentPolicyDaemonOwned,
68+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
6869 })
6970 if attachPayload == nil {
7071 t.Fatal("MarshalAttachTarget returned nil")
@@ -94,53 +95,53 @@ func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) {
9495 {
9596 name: "hello",
9697 validate: func() error {
97- return ValidateHello(Hello{
98- Version: ProtocolVersion, Intent: IntentAttach,
98+ return protocol.ValidateHello(protocol.Hello{
99+ Version: protocol.Version, Intent: protocol.IntentAttach,
99100 Name: target.SessionName, Size: domain.Size{Cols: 80, Rows: 24},
100- RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyClientOwned,
101+ RemoteTarget: target, EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
101102 })
102103 },
103104 marshalBad: func() []byte {
104- return MarshalHello(Hello{
105- Version: ProtocolVersion, Intent: IntentAttach,
105+ return MarshalHello(protocol.Hello{
106+ Version: protocol.Version, Intent: protocol.IntentAttach,
106107 Name: target.SessionName, Size: domain.Size{Cols: 80, Rows: 24},
107- RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyClientOwned,
108+ RemoteTarget: target, EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
108109 })
109110 },
110111 marshalGood: func() []byte {
111- return MarshalHello(Hello{
112- Version: ProtocolVersion, Intent: IntentAttach,
112+ return MarshalHello(protocol.Hello{
113+ Version: protocol.Version, Intent: protocol.IntentAttach,
113114 Name: target.SessionName, Size: domain.Size{Cols: 80, Rows: 24},
114- RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned,
115+ RemoteTarget: target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
115116 })
116117 },
117118 unmarshal: func(payload []byte) error { _, err := UnmarshalHello(payload); return err },
118119 policyOffset: 10,
119- wantErr: ErrInvalidHello,
120+ wantErr: protocol.ErrInvalidHello,
120121 },
121122 {
122123 name: "attach target",
123124 validate: func() error {
124- return ValidateAttachTarget(AttachTarget{
125- Endpoint: target.Endpoint, Session: target.SessionName, Intent: IntentAttach,
126- RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyClientOwned,
125+ return protocol.ValidateAttachTarget(protocol.AttachTarget{
126+ Endpoint: target.Endpoint, Session: target.SessionName, Intent: protocol.IntentAttach,
127+ RemoteTarget: target, EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
127128 })
128129 },
129130 marshalBad: func() []byte {
130- return MarshalAttachTarget(AttachTarget{
131- Endpoint: target.Endpoint, Session: target.SessionName, Intent: IntentAttach,
132- RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyClientOwned,
131+ return MarshalAttachTarget(protocol.AttachTarget{
132+ Endpoint: target.Endpoint, Session: target.SessionName, Intent: protocol.IntentAttach,
133+ RemoteTarget: target, EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
133134 })
134135 },
135136 marshalGood: func() []byte {
136- return MarshalAttachTarget(AttachTarget{
137- Endpoint: target.Endpoint, Session: target.SessionName, Intent: IntentAttach,
138- RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned,
137+ return MarshalAttachTarget(protocol.AttachTarget{
138+ Endpoint: target.Endpoint, Session: target.SessionName, Intent: protocol.IntentAttach,
139+ RemoteTarget: target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
139140 })
140141 },
141142 unmarshal: func(payload []byte) error { _, err := UnmarshalAttachTarget(payload); return err },
142143 policyOffset: 3,
143- wantErr: ErrInvalidAttachTarget,
144+ wantErr: protocol.ErrInvalidAttachTarget,
144145 },
145146 }
146147
@@ -165,7 +166,7 @@ func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) {
165166 if err := tt.unmarshal(trailing); err == nil {
166167 t.Fatal("trailing garbage unexpectedly decoded")
167168 }
168- payload[len(payload)-tt.policyOffset] = byte(EnvironmentPolicyClientOwned)
169+ payload[len(payload)-tt.policyOffset] = byte(protocol.EnvironmentPolicyClientOwned)
169170 if err := tt.unmarshal(payload); !errors.Is(err, tt.wantErr) {
170171 t.Fatalf("mutated policy error = %v, want %v", err, tt.wantErr)
171172 }
@@ -174,19 +175,19 @@ func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) {
174175 }
175176
176177 func TestAttachTargetRejectsResumeIntent(t *testing.T) {
177- tests := []AttachTarget{
178- {Endpoint: "host", Session: "work", Intent: IntentResume},
178+ tests := []protocol.AttachTarget{
179+ {Endpoint: "host", Session: "work", Intent: protocol.IntentResume},
179180 {
180181 Endpoint: "build@mule:2222",
181182 Session: "work",
182- Intent: IntentResume,
183+ Intent: protocol.IntentResume,
183184 RemoteTarget: testRemoteTarget(false),
184- EnvironmentPolicy: EnvironmentPolicyDaemonOwned,
185+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
185186 },
186187 }
187188 for _, target := range tests {
188- if err := ValidateAttachTarget(target); !errors.Is(err, ErrInvalidAttachTarget) {
189- t.Fatalf("ValidateAttachTarget(%#v) error = %v, want %v", target, err, ErrInvalidAttachTarget)
189+ if err := protocol.ValidateAttachTarget(target); !errors.Is(err, protocol.ErrInvalidAttachTarget) {
190+ t.Fatalf("ValidateAttachTarget(%#v) error = %v, want %v", target, err, protocol.ErrInvalidAttachTarget)
190191 }
191192 if got := MarshalAttachTarget(target); got != nil {
192193 t.Fatalf("MarshalAttachTarget(%#v) = %x, want nil", target, got)
@@ -195,8 +196,8 @@ func TestAttachTargetRejectsResumeIntent(t *testing.T) {
195196 }
196197
197198 func TestTargetWireIncludesRequiredV27Section(t *testing.T) {
198- want := []byte{0, 4, 'h', 'o', 's', 't', 0, 4, 'w', 'o', 'r', 'k', IntentAttach, 0, 0, 0, 0}
199- got := MarshalAttachTarget(AttachTarget{Endpoint: "host", Session: "work", Intent: IntentAttach})
199+ want := []byte{0, 4, 'h', 'o', 's', 't', 0, 4, 'w', 'o', 'r', 'k', protocol.IntentAttach, 0, 0, 0, 0}
200+ got := MarshalAttachTarget(protocol.AttachTarget{Endpoint: "host", Session: "work", Intent: protocol.IntentAttach})
200201 if !bytes.Equal(got, want) {
201202 t.Fatalf("target bytes = %x, want %x", got, want)
202203 }
M internal/ports/routes_test.go +8 -8
@@ -33,9 +33,9 @@ func TestExactSessionTargetCodecIsStrict(t *testing.T) {
3333
3434 func TestHelloExactTargetRoundTrip(t *testing.T) {
3535 target := testExactTarget()
36- msg := Hello{
37- Version: ProtocolVersion,
38- Intent: IntentAttach,
36+ msg := protocol.Hello{
37+ Version: protocol.Version,
38+ Intent: protocol.IntentAttach,
3939 Name: target.SessionName,
4040 Size: domain.Size{Cols: 80, Rows: 24},
4141 ExactTarget: &target,
@@ -49,12 +49,12 @@ func TestHelloExactTargetRoundTrip(t *testing.T) {
4949 require.Equal(t, msg, got)
5050
5151 invalid := msg
52- invalid.Intent = IntentNew
53- require.Error(t, ValidateHello(invalid))
52+ invalid.Intent = protocol.IntentNew
53+ require.Error(t, protocol.ValidateHello(invalid))
5454
5555 invalid = msg
5656 invalid.PreferredTabID = "bad tab"
57- require.Error(t, ValidateHello(invalid))
57+ require.Error(t, protocol.ValidateHello(invalid))
5858 }
5959
6060 func TestRoutePositionCodecIsStrict(t *testing.T) {
@@ -78,7 +78,7 @@ func TestRoutePositionCodecIsStrict(t *testing.T) {
7878
7979 func TestWelcomeCarriesCommittedRouteIdentity(t *testing.T) {
8080 identity := &protocol.CommittedRouteIdentity{Target: testExactTarget(), Ephemeral: true}
81- want := Welcome{SessionID: "daemon-session", SessionName: "work", Ephemeral: true, CommittedIdentity: identity}
81+ want := protocol.Welcome{SessionID: "daemon-session", SessionName: "work", Ephemeral: true, CommittedIdentity: identity}
8282
8383 got, err := UnmarshalWelcome(MarshalWelcome(want))
8484 require.NoError(t, err)
@@ -87,7 +87,7 @@ func TestWelcomeCarriesCommittedRouteIdentity(t *testing.T) {
8787
8888 func TestWelcomeRejectsMismatchedCommittedIdentity(t *testing.T) {
8989 identity := &protocol.CommittedRouteIdentity{Target: testExactTarget()}
90- payload := MarshalWelcome(Welcome{SessionID: "daemon-session", SessionName: "other", CommittedIdentity: identity})
90+ payload := MarshalWelcome(protocol.Welcome{SessionID: "daemon-session", SessionName: "other", CommittedIdentity: identity})
9191
9292 require.Nil(t, payload)
9393 }
D internal/ports/session_control.go +0 -323
@@ -1,323 +0,0 @@
1-package ports
2-
3-import (
4- "errors"
5- "fmt"
6- "math"
7- "time"
8-
9- "github.com/bnema/vev/internal/domain"
10- "github.com/bnema/vev/internal/protocol"
11-)
12-
13-// HandshakeTimeout bounds every transport handshake from connect through the
14-// first committed publication.
15-const HandshakeTimeout = 15 * time.Second
16-
17-// ProtocolVersion is the current vev IPC wire protocol version.
18-const ProtocolVersion uint16 = 37
19-
20-// ConnectionCapabilities describes transport behavior relevant to session
21-// flow without naming a concrete carriage.
22-type ConnectionCapabilities struct {
23- OutputDataLimit int
24- PreferredOutputWindow uint8
25- AsyncSend bool
26- OwnedSynchronousSend bool
27- LinkState bool
28-}
29-
30-var (
31- ErrInvalidHello = errors.New("ports: invalid hello")
32- ErrInvalidAttachTarget = errors.New("ports: invalid attach target")
33- ErrInvalidEnvironmentPolicy = errors.New("ports: invalid environment policy")
34- ErrInvalidClientNotice = errors.New("ports: invalid client notice")
35- ErrInvalidDetached = errors.New("ports: invalid detached reason")
36-)
37-
38-const (
39- IntentEphemeral uint8 = 0
40- IntentNew uint8 = 1
41- IntentAttach uint8 = 2
42- IntentResume uint8 = 3
43-)
44-
45-type EnvironmentPolicy uint8
46-
47-const (
48- EnvironmentPolicyClientOwned EnvironmentPolicy = iota
49- EnvironmentPolicyDaemonOwned
50-)
51-
52-const (
53- CapabilityResume uint32 = 1 << 0
54- CapabilityUDP uint32 = 1 << 1
55- CapabilityPredict uint32 = 1 << 2
56-)
57-
58-const (
59- ErrVersionMismatch uint16 = 1
60- ErrNoSuchSession uint16 = 2
61- ErrNameTaken uint16 = 3
62- ErrServerShutdown uint16 = 4
63- ErrInvalidSessionName uint16 = 5
64- ErrUnknownCommand uint16 = 6
65- ErrNotScriptable uint16 = 7
66- ErrInvalidCommandArgs uint16 = 8
67- ErrNoSuchTarget uint16 = 9
68- ErrAmbiguousTarget uint16 = 10
69- ErrInternal uint16 = 255
70-)
71-
72-const (
73- ReasonDetach uint8 = 0
74- ReasonSessionKilled uint8 = 1
75- ReasonServerShutdown uint8 = 2
76- ReasonReplaced uint8 = 3
77-)
78-
79-// Hello is sent by the client immediately after connecting.
80-type Hello struct {
81- Version uint16
82- Intent uint8
83- ClientID [16]byte
84- ResumeToken uint64
85- Name string
86- Size domain.Size
87- PixelWidth int
88- PixelHeight int
89- TermEnv string
90- Cwd string
91- TrueColor bool
92- // KittyDirectGraphics is an explicit declaration that the client has
93- // probed its direct outer terminal and it accepts Kitty graphics output.
94- KittyDirectGraphics bool
95- MaxOutputInFlight uint8
96- Env []string
97- RemoteTarget *domain.RemoteSessionTarget
98- ExactTarget *protocol.ExactSessionTarget
99- PreferredTabID domain.TabStableID
100- EnvironmentPolicy EnvironmentPolicy
101- NavigationCapabilities protocol.NavigationCapabilities
102- StartupOverlay protocol.StartupOverlay
103- Remote bool
104-}
105-
106-func (h Hello) Geometry() domain.Geometry {
107- return domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}.NormalizePixels()
108-}
109-
110-type Input struct {
111- InputSeq uint64
112- Data []byte
113-}
114-
115-type ClientNotice struct{ Action uint8 }
116-
117-const (
118- ClientNoticeClipboardFallback uint8 = 1
119- ClientNoticeClipboardTooLarge uint8 = 2
120- ClientNoticeLinkDegraded uint8 = 3
121- ClientNoticeLinkConnected uint8 = 4
122-)
123-
124-type Detach struct{}
125-type Ping struct{}
126-type Pong struct{}
127-
128-type Welcome struct {
129- SessionID string
130- SessionName string
131- Ephemeral bool
132- ResumeToken uint64
133- Capabilities uint32
134- CommittedIdentity *protocol.CommittedRouteIdentity
135-}
136-
137-type ErrorMsg struct {
138- Code uint16
139- Text string
140-}
141-
142-type AttachTarget struct {
143- Endpoint string
144- Session string
145- Intent uint8
146- RemoteTarget *domain.RemoteSessionTarget
147- ExactTarget *protocol.ExactSessionTarget
148- EnvironmentPolicy EnvironmentPolicy
149- SamePeer bool
150-}
151-
152-type Detached struct{ Reason uint8 }
153-type List struct{}
154-
155-type Kill struct {
156- Name string
157- All bool
158-}
159-
160-type SessionState uint8
161-
162-const (
163- SessionUp SessionState = iota
164- SessionDown
165- SessionBroken
166-)
167-
168-type SessionInfo struct {
169- SessionID string
170- Name string
171- State SessionState
172- Ephemeral bool
173- Tabs uint16
174- Attached bool
175-}
176-
177-type Sessions struct{ Sessions []SessionInfo }
178-type OutputResetRequest struct{}
179-
180-func validEnvironmentPolicy(policy EnvironmentPolicy) bool {
181- return policy == EnvironmentPolicyClientOwned || policy == EnvironmentPolicyDaemonOwned
182-}
183-
184-func validClientNoticeAction(action uint8) bool {
185- switch action {
186- case ClientNoticeClipboardFallback, ClientNoticeClipboardTooLarge, ClientNoticeLinkDegraded, ClientNoticeLinkConnected:
187- return true
188- default:
189- return false
190- }
191-}
192-
193-func validDetachedReason(reason uint8) bool {
194- switch reason {
195- case ReasonDetach, ReasonSessionKilled, ReasonServerShutdown, ReasonReplaced:
196- return true
197- default:
198- return false
199- }
200-}
201-
202-func ValidateEnvironmentPolicy(policy EnvironmentPolicy) error {
203- if !validEnvironmentPolicy(policy) {
204- return ErrInvalidEnvironmentPolicy
205- }
206- return nil
207-}
208-
209-func ValidateClientNotice(m ClientNotice) error {
210- if !validClientNoticeAction(m.Action) {
211- return ErrInvalidClientNotice
212- }
213- return nil
214-}
215-
216-func ValidateDetached(m Detached) error {
217- if !validDetachedReason(m.Reason) {
218- return ErrInvalidDetached
219- }
220- return nil
221-}
222-
223-func validateHelloNavigation(h Hello) error {
224- if h.Intent != IntentAttach && h.Intent != IntentResume {
225- if h.NavigationCapabilities != 0 || h.StartupOverlay != protocol.StartupOverlayNone {
226- return protocol.ErrInvalidNavigation
227- }
228- return nil
229- }
230- return protocol.ValidateNavigation(h.NavigationCapabilities, h.StartupOverlay, h.RemoteTarget != nil || h.EnvironmentPolicy == EnvironmentPolicyDaemonOwned)
231-}
232-
233-func ValidateHello(h Hello) error {
234- if h.Intent != IntentEphemeral && h.Intent != IntentNew && h.Intent != IntentAttach && h.Intent != IntentResume {
235- return ErrInvalidHello
236- }
237- if err := protocol.ValidateGeometry(domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}); err != nil {
238- return fmt.Errorf("%w: geometry", ErrInvalidHello)
239- }
240- if !validEnvironmentPolicy(h.EnvironmentPolicy) {
241- return ErrInvalidHello
242- }
243- if err := validateHelloNavigation(h); err != nil {
244- return fmt.Errorf("%w: navigation: %w", ErrInvalidHello, err)
245- }
246- if len(h.Name) > math.MaxUint16 || len(h.TermEnv) > math.MaxUint16 || len(h.Cwd) > math.MaxUint16 || len(h.Env) > math.MaxUint32 {
247- return ErrInvalidHello
248- }
249- for _, entry := range h.Env {
250- if uint64(len(entry)) > math.MaxUint32 {
251- return ErrInvalidHello
252- }
253- }
254- if h.PreferredTabID != "" {
255- if h.Intent != IntentAttach && h.Intent != IntentResume {
256- return ErrInvalidHello
257- }
258- if err := domain.ValidateTabStableID(h.PreferredTabID); err != nil {
259- return fmt.Errorf("%w: preferred tab: %v", ErrInvalidHello, err)
260- }
261- }
262- if h.ExactTarget != nil {
263- if h.Intent != IntentAttach && h.Intent != IntentResume {
264- return ErrInvalidHello
265- }
266- if err := h.ExactTarget.Validate(); err != nil {
267- return fmt.Errorf("%w: exact target: %v", ErrInvalidHello, err)
268- }
269- if h.Name != h.ExactTarget.SessionName {
270- return ErrInvalidHello
271- }
272- if h.RemoteTarget != nil && (h.RemoteTarget.LifecycleID != h.ExactTarget.LifecycleID || h.RemoteTarget.SessionName != h.ExactTarget.SessionName) {
273- return ErrInvalidHello
274- }
275- }
276- if h.RemoteTarget == nil {
277- return nil
278- }
279- if h.EnvironmentPolicy != EnvironmentPolicyDaemonOwned || (h.Intent != IntentAttach && h.Intent != IntentResume) {
280- return ErrInvalidHello
281- }
282- if err := validateWireRemoteTarget(*h.RemoteTarget); err != nil {
283- return fmt.Errorf("%w: remote target: %v", ErrInvalidHello, err)
284- }
285- if h.Name != h.RemoteTarget.SessionName {
286- return ErrInvalidHello
287- }
288- return nil
289-}
290-
291-func ValidateAttachTarget(m AttachTarget) error {
292- if m.Session == "" || len(m.Endpoint) > math.MaxUint16 || len(m.Session) > math.MaxUint16 {
293- return ErrInvalidAttachTarget
294- }
295- if err := domain.ValidateSessionName(m.Session); err != nil {
296- return ErrInvalidAttachTarget
297- }
298- if m.Intent != IntentEphemeral && m.Intent != IntentNew && m.Intent != IntentAttach && m.Intent != IntentResume {
299- return ErrInvalidAttachTarget
300- }
301- if m.Intent == IntentResume || !validEnvironmentPolicy(m.EnvironmentPolicy) {
302- return ErrInvalidAttachTarget
303- }
304- if m.ExactTarget != nil && (m.ExactTarget.SessionName != m.Session || m.ExactTarget.Validate() != nil) {
305- return ErrInvalidAttachTarget
306- }
307- if m.SamePeer && (m.Endpoint != "" || m.RemoteTarget != nil || m.ExactTarget == nil) {
308- return ErrInvalidAttachTarget
309- }
310- if m.RemoteTarget == nil {
311- return nil
312- }
313- if m.EnvironmentPolicy != EnvironmentPolicyDaemonOwned || m.Intent != IntentAttach {
314- return ErrInvalidAttachTarget
315- }
316- if err := validateWireRemoteTarget(*m.RemoteTarget); err != nil {
317- return fmt.Errorf("%w: remote target: %v", ErrInvalidAttachTarget, err)
318- }
319- if m.Endpoint != m.RemoteTarget.Endpoint || m.Session != m.RemoteTarget.SessionName {
320- return ErrInvalidAttachTarget
321- }
322- return nil
323-}
M internal/ports/wire.go +138 -138
@@ -114,7 +114,7 @@ func (w *payloadWriter) putLongBytes(b []byte) {
114114 // A target may be absent for direct local attaches, but its presence marker and
115115 // environment policy are never optional: v27 has no extension or legacy tail
116116 // inside this section; the exact-target section follows it.
117-func marshalRemoteTargetSection(w *payloadWriter, target *domain.RemoteSessionTarget, policy EnvironmentPolicy) {
117+func marshalRemoteTargetSection(w *payloadWriter, target *domain.RemoteSessionTarget, policy protocol.EnvironmentPolicy) {
118118 if target == nil {
119119 w.putBool(false)
120120 } else {
@@ -190,7 +190,7 @@ func skipRemoteTargetSection(r *payloadReader) error {
190190 if err != nil {
191191 return err
192192 }
193- if ValidateEnvironmentPolicy(EnvironmentPolicy(policy)) != nil {
193+ if protocol.ValidateEnvironmentPolicy(protocol.EnvironmentPolicy(policy)) != nil {
194194 return errInvalidEnum
195195 }
196196 return nil
@@ -256,7 +256,7 @@ func unmarshalRemoteTarget(r *payloadReader) (*domain.RemoteSessionTarget, error
256256 return target, nil
257257 }
258258
259-func unmarshalRemoteTargetSection(r *payloadReader) (*domain.RemoteSessionTarget, EnvironmentPolicy, error) {
259+func unmarshalRemoteTargetSection(r *payloadReader) (*domain.RemoteSessionTarget, protocol.EnvironmentPolicy, error) {
260260 target, err := unmarshalRemoteTarget(r)
261261 if err != nil {
262262 return nil, 0, err
@@ -265,10 +265,10 @@ func unmarshalRemoteTargetSection(r *payloadReader) (*domain.RemoteSessionTarget
265265 if err != nil {
266266 return nil, 0, err
267267 }
268- if ValidateEnvironmentPolicy(EnvironmentPolicy(policy)) != nil {
268+ if protocol.ValidateEnvironmentPolicy(protocol.EnvironmentPolicy(policy)) != nil {
269269 return nil, 0, errInvalidEnum
270270 }
271- return target, EnvironmentPolicy(policy), nil
271+ return target, protocol.EnvironmentPolicy(policy), nil
272272 }
273273
274274 // payloadReader consumes a message payload field by field in wire order,
@@ -439,8 +439,8 @@ func validateWireRemoteTarget(target domain.RemoteSessionTarget) error {
439439 }
440440
441441 // MarshalHello encodes h into a Hello message payload.
442-func MarshalHello(h Hello) []byte {
443- if err := ValidateHello(h); err != nil {
442+func MarshalHello(h protocol.Hello) []byte {
443+ if err := protocol.ValidateHello(h); err != nil {
444444 return nil
445445 }
446446 w := payloadWriter{}
@@ -478,7 +478,7 @@ func preflightHello(b []byte) error {
478478 return err
479479 }
480480 if version == 21 {
481- return ErrInvalidHello
481+ return protocol.ErrInvalidHello
482482 }
483483 if _, err := r.getUint8(); err != nil {
484484 return err
@@ -554,122 +554,122 @@ func preflightHello(b []byte) error {
554554 }
555555
556556 // UnmarshalHello decodes a Hello message payload.
557-func UnmarshalHello(b []byte) (Hello, error) {
557+func UnmarshalHello(b []byte) (protocol.Hello, error) {
558558 if len(b) > MaxFrameLen-1 {
559- return Hello{}, ErrInvalidHello
559+ return protocol.Hello{}, protocol.ErrInvalidHello
560560 }
561561 if err := preflightHello(b); err != nil {
562- return Hello{}, err
562+ return protocol.Hello{}, err
563563 }
564564 r := payloadReader{b: b}
565- var h Hello
565+ var h protocol.Hello
566566 var err error
567567
568568 if h.Version, err = r.getUint16(); err != nil {
569- return Hello{}, err
569+ return protocol.Hello{}, err
570570 }
571571 if h.Intent, err = r.getUint8(); err != nil {
572- return Hello{}, err
572+ return protocol.Hello{}, err
573573 }
574574 clientID, err := r.getBytes(len(h.ClientID))
575575 if err != nil {
576- return Hello{}, err
576+ return protocol.Hello{}, err
577577 }
578578 copy(h.ClientID[:], clientID)
579579 if h.ResumeToken, err = r.getUint64(); err != nil {
580- return Hello{}, err
580+ return protocol.Hello{}, err
581581 }
582582 if h.Name, err = r.getString(); err != nil {
583- return Hello{}, err
583+ return protocol.Hello{}, err
584584 }
585585 cols, err := r.getUint16()
586586 if err != nil {
587- return Hello{}, err
587+ return protocol.Hello{}, err
588588 }
589589 rows, err := r.getUint16()
590590 if err != nil {
591- return Hello{}, err
591+ return protocol.Hello{}, err
592592 }
593593 pixelWidth, err := r.getUint16()
594594 if err != nil {
595- return Hello{}, err
595+ return protocol.Hello{}, err
596596 }
597597 pixelHeight, err := r.getUint16()
598598 if err != nil {
599- return Hello{}, err
599+ return protocol.Hello{}, err
600600 }
601601 h.Size = domain.Size{Cols: int(cols), Rows: int(rows)}
602602 h.PixelWidth = int(pixelWidth)
603603 h.PixelHeight = int(pixelHeight)
604604 if h.TermEnv, err = r.getString(); err != nil {
605- return Hello{}, err
605+ return protocol.Hello{}, err
606606 }
607607 if h.Cwd, err = r.getString(); err != nil {
608- return Hello{}, err
608+ return protocol.Hello{}, err
609609 }
610610 if h.TrueColor, err = r.getBool(); err != nil {
611- return Hello{}, err
611+ return protocol.Hello{}, err
612612 }
613613 if h.MaxOutputInFlight, err = r.getUint8(); err != nil {
614- return Hello{}, err
614+ return protocol.Hello{}, err
615615 }
616616 envCount, err := r.getUint32()
617617 if err != nil {
618- return Hello{}, err
618+ return protocol.Hello{}, err
619619 }
620620 // Each entry has at least its uint32 byte length. Check that before
621621 // allocating so a malformed count cannot force an excessive allocation.
622622 if uint64(envCount) > uint64(len(r.b)/4) {
623- return Hello{}, errShortPayload
623+ return protocol.Hello{}, errShortPayload
624624 }
625625 if envCount != 0 {
626626 h.Env = make([]string, 0, int(envCount))
627627 for range int(envCount) {
628628 entry, err := r.getLongString()
629629 if err != nil {
630- return Hello{}, err
630+ return protocol.Hello{}, err
631631 }
632632 h.Env = append(h.Env, entry)
633633 }
634634 }
635635 if h.RemoteTarget, h.EnvironmentPolicy, err = unmarshalRemoteTargetSection(&r); err != nil {
636- return Hello{}, err
636+ return protocol.Hello{}, err
637637 }
638638 if h.ExactTarget, err = unmarshalExactTargetSection(&r); err != nil {
639- return Hello{}, err
639+ return protocol.Hello{}, err
640640 }
641641 preferredTabID, err := r.getString()
642642 if err != nil {
643- return Hello{}, err
643+ return protocol.Hello{}, err
644644 }
645645 h.PreferredTabID = domain.TabStableID(preferredTabID)
646646 capabilities, err := r.getUint8()
647647 if err != nil {
648- return Hello{}, err
648+ return protocol.Hello{}, err
649649 }
650650 h.NavigationCapabilities = protocol.NavigationCapabilities(capabilities)
651651 overlay, err := r.getUint8()
652652 if err != nil {
653- return Hello{}, err
653+ return protocol.Hello{}, err
654654 }
655655 h.StartupOverlay = protocol.StartupOverlay(overlay)
656656 if h.Remote, err = r.getBool(); err != nil {
657- return Hello{}, err
657+ return protocol.Hello{}, err
658658 }
659659 if h.KittyDirectGraphics, err = r.getBool(); err != nil {
660- return Hello{}, err
660+ return protocol.Hello{}, err
661661 }
662662 if err := r.done(); err != nil {
663- return Hello{}, err
663+ return protocol.Hello{}, err
664664 }
665- if err := ValidateHello(h); err != nil {
666- return Hello{}, err
665+ if err := protocol.ValidateHello(h); err != nil {
666+ return protocol.Hello{}, err
667667 }
668668 return h, nil
669669 }
670670
671671 // MarshalInput encodes m into an Input message payload.
672-func MarshalInput(m Input) []byte {
672+func MarshalInput(m protocol.Input) []byte {
673673 w := payloadWriter{}
674674 w.putUint64(m.InputSeq)
675675 w.putBytes(m.Data)
@@ -678,13 +678,13 @@ func MarshalInput(m Input) []byte {
678678
679679 // UnmarshalInput decodes an Input message payload. After the fixed input
680680 // sequence, the rest of the payload is data; there is no length prefix.
681-func UnmarshalInput(b []byte) (Input, error) {
681+func UnmarshalInput(b []byte) (protocol.Input, error) {
682682 r := payloadReader{b: b}
683683 seq, err := r.getUint64()
684684 if err != nil {
685- return Input{}, err
685+ return protocol.Input{}, err
686686 }
687- return Input{InputSeq: seq, Data: r.rest()}, nil
687+ return protocol.Input{InputSeq: seq, Data: r.rest()}, nil
688688 }
689689
690690 // MarshalImagePush encodes m into an ImagePush message payload.
@@ -713,25 +713,25 @@ func UnmarshalImagePush(b []byte) (protocol.ImagePush, error) {
713713 }
714714
715715 // MarshalClientNotice encodes a fixed one-byte client notice action.
716-func MarshalClientNotice(m ClientNotice) []byte {
716+func MarshalClientNotice(m protocol.ClientNotice) []byte {
717717 return []byte{m.Action}
718718 }
719719
720720 // UnmarshalClientNotice decodes a fixed client notice action and rejects both
721721 // unknown values and any trailing bytes.
722-func UnmarshalClientNotice(b []byte) (ClientNotice, error) {
722+func UnmarshalClientNotice(b []byte) (protocol.ClientNotice, error) {
723723 r := payloadReader{b: b}
724724 action, err := r.getUint8()
725725 if err != nil {
726- return ClientNotice{}, err
726+ return protocol.ClientNotice{}, err
727727 }
728- if ValidateClientNotice(ClientNotice{Action: action}) != nil {
729- return ClientNotice{}, errors.New("ports: unknown client notice action")
728+ if protocol.ValidateClientNotice(protocol.ClientNotice{Action: action}) != nil {
729+ return protocol.ClientNotice{}, errors.New("ports: unknown client notice action")
730730 }
731731 if err := r.done(); err != nil {
732- return ClientNotice{}, err
732+ return protocol.ClientNotice{}, err
733733 }
734- return ClientNotice{Action: action}, nil
734+ return protocol.ClientNotice{Action: action}, nil
735735 }
736736
737737 // MarshalResize encodes m into a Resize message payload.
@@ -883,45 +883,45 @@ func UnmarshalResize(b []byte) (protocol.Resize, error) {
883883 }
884884
885885 // MarshalDetach encodes a Detach message payload (always empty).
886-func MarshalDetach(Detach) []byte {
886+func MarshalDetach(protocol.Detach) []byte {
887887 return nil
888888 }
889889
890890 // UnmarshalDetach decodes a Detach message payload.
891-func UnmarshalDetach(b []byte) (Detach, error) {
891+func UnmarshalDetach(b []byte) (protocol.Detach, error) {
892892 r := payloadReader{b: b}
893893 if err := r.done(); err != nil {
894- return Detach{}, err
894+ return protocol.Detach{}, err
895895 }
896- return Detach{}, nil
896+ return protocol.Detach{}, nil
897897 }
898898
899899 // MarshalPing encodes a Ping message payload (always empty).
900-func MarshalPing(Ping) []byte {
900+func MarshalPing(protocol.Ping) []byte {
901901 return nil
902902 }
903903
904904 // UnmarshalPing decodes a Ping message payload.
905-func UnmarshalPing(b []byte) (Ping, error) {
905+func UnmarshalPing(b []byte) (protocol.Ping, error) {
906906 r := payloadReader{b: b}
907907 if err := r.done(); err != nil {
908- return Ping{}, err
908+ return protocol.Ping{}, err
909909 }
910- return Ping{}, nil
910+ return protocol.Ping{}, nil
911911 }
912912
913913 // MarshalPong encodes a Pong message payload (always empty).
914-func MarshalPong(Pong) []byte {
914+func MarshalPong(protocol.Pong) []byte {
915915 return nil
916916 }
917917
918918 // UnmarshalPong decodes a Pong message payload.
919-func UnmarshalPong(b []byte) (Pong, error) {
919+func UnmarshalPong(b []byte) (protocol.Pong, error) {
920920 r := payloadReader{b: b}
921921 if err := r.done(); err != nil {
922- return Pong{}, err
922+ return protocol.Pong{}, err
923923 }
924- return Pong{}, nil
924+ return protocol.Pong{}, nil
925925 }
926926
927927 // MarshalAck encodes m into an epoch/state Ack payload.
@@ -956,7 +956,7 @@ func UnmarshalAck(b []byte) (protocol.Ack, error) {
956956 }
957957
958958 // MarshalWelcome encodes m into a Welcome message payload.
959-func MarshalWelcome(m Welcome) []byte {
959+func MarshalWelcome(m protocol.Welcome) []byte {
960960 if m.CommittedIdentity != nil && (m.SessionName != m.CommittedIdentity.Target.SessionName || m.Ephemeral != m.CommittedIdentity.Ephemeral) {
961961 return nil
962962 }
@@ -977,42 +977,42 @@ func MarshalWelcome(m Welcome) []byte {
977977 }
978978
979979 // UnmarshalWelcome decodes a Welcome message payload.
980-func UnmarshalWelcome(b []byte) (Welcome, error) {
980+func UnmarshalWelcome(b []byte) (protocol.Welcome, error) {
981981 r := payloadReader{b: b}
982- var m Welcome
982+ var m protocol.Welcome
983983 var err error
984984
985985 if m.SessionID, err = r.getString(); err != nil {
986- return Welcome{}, err
986+ return protocol.Welcome{}, err
987987 }
988988 if m.SessionName, err = r.getString(); err != nil {
989- return Welcome{}, err
989+ return protocol.Welcome{}, err
990990 }
991991 var ephemeral bool
992992 if ephemeral, err = r.getBool(); err != nil {
993- return Welcome{}, err
993+ return protocol.Welcome{}, err
994994 }
995995 m.Ephemeral = ephemeral
996996 if m.ResumeToken, err = r.getUint64(); err != nil {
997- return Welcome{}, err
997+ return protocol.Welcome{}, err
998998 }
999999 if m.Capabilities, err = r.getUint32(); err != nil {
1000- return Welcome{}, err
1000+ return protocol.Welcome{}, err
10011001 }
10021002 if m.CommittedIdentity, err = unmarshalCommittedIdentitySection(&r); err != nil {
1003- return Welcome{}, err
1003+ return protocol.Welcome{}, err
10041004 }
10051005 if m.CommittedIdentity != nil && (m.CommittedIdentity.Target.SessionName != m.SessionName || m.CommittedIdentity.Ephemeral != m.Ephemeral) {
1006- return Welcome{}, fmt.Errorf("%w: committed identity does not match welcome", protocol.ErrInvalidRouteWire)
1006+ return protocol.Welcome{}, fmt.Errorf("%w: committed identity does not match welcome", protocol.ErrInvalidRouteWire)
10071007 }
10081008 if err := r.done(); err != nil {
1009- return Welcome{}, err
1009+ return protocol.Welcome{}, err
10101010 }
10111011 return m, nil
10121012 }
10131013
10141014 // MarshalErrorMsg encodes m into an ErrorMsg message payload.
1015-func MarshalErrorMsg(m ErrorMsg) []byte {
1015+func MarshalErrorMsg(m protocol.ErrorMsg) []byte {
10161016 w := payloadWriter{}
10171017 w.putUint16(m.Code)
10181018 w.putString(m.Text)
@@ -1020,19 +1020,19 @@ func MarshalErrorMsg(m ErrorMsg) []byte {
10201020 }
10211021
10221022 // UnmarshalErrorMsg decodes an ErrorMsg message payload.
1023-func UnmarshalErrorMsg(b []byte) (ErrorMsg, error) {
1023+func UnmarshalErrorMsg(b []byte) (protocol.ErrorMsg, error) {
10241024 r := payloadReader{b: b}
1025- var m ErrorMsg
1025+ var m protocol.ErrorMsg
10261026 var err error
10271027
10281028 if m.Code, err = r.getUint16(); err != nil {
1029- return ErrorMsg{}, err
1029+ return protocol.ErrorMsg{}, err
10301030 }
10311031 if m.Text, err = r.getString(); err != nil {
1032- return ErrorMsg{}, err
1032+ return protocol.ErrorMsg{}, err
10331033 }
10341034 if err := r.done(); err != nil {
1035- return ErrorMsg{}, err
1035+ return protocol.ErrorMsg{}, err
10361036 }
10371037 return m, nil
10381038 }
@@ -1198,8 +1198,8 @@ func decompressOutputData(data []byte, decodedLen int) ([]byte, error) {
11981198 }
11991199
12001200 // MarshalAttachTarget encodes a strict server attach-target payload.
1201-func MarshalAttachTarget(m AttachTarget) []byte {
1202- if err := ValidateAttachTarget(m); err != nil {
1201+func MarshalAttachTarget(m protocol.AttachTarget) []byte {
1202+ if err := protocol.ValidateAttachTarget(m); err != nil {
12031203 return nil
12041204 }
12051205 w := payloadWriter{}
@@ -1213,118 +1213,118 @@ func MarshalAttachTarget(m AttachTarget) []byte {
12131213 }
12141214
12151215 // UnmarshalAttachTarget decodes a strict server attach-target payload.
1216-func UnmarshalAttachTarget(b []byte) (AttachTarget, error) {
1216+func UnmarshalAttachTarget(b []byte) (protocol.AttachTarget, error) {
12171217 // Preflight lengths and intent before getString can allocate either value.
12181218 probe := payloadReader{b: b}
12191219 endpointLen, err := probe.getUint16()
12201220 if err != nil || int(endpointLen) > len(probe.b) {
1221- return AttachTarget{}, ErrInvalidAttachTarget
1221+ return protocol.AttachTarget{}, protocol.ErrInvalidAttachTarget
12221222 }
12231223 probe.b = probe.b[endpointLen:]
12241224 sessionLen, err := probe.getUint16()
12251225 if err != nil || sessionLen == 0 || int(sessionLen) > len(probe.b) {
1226- return AttachTarget{}, ErrInvalidAttachTarget
1226+ return protocol.AttachTarget{}, protocol.ErrInvalidAttachTarget
12271227 }
12281228 probe.b = probe.b[sessionLen:]
12291229 intent, err := probe.getUint8()
1230- if err != nil || (intent != IntentEphemeral && intent != IntentNew && intent != IntentAttach && intent != IntentResume) {
1231- return AttachTarget{}, ErrInvalidAttachTarget
1230+ if err != nil || (intent != protocol.IntentEphemeral && intent != protocol.IntentNew && intent != protocol.IntentAttach && intent != protocol.IntentResume) {
1231+ return protocol.AttachTarget{}, protocol.ErrInvalidAttachTarget
12321232 }
12331233 if err := skipRemoteTargetSection(&probe); err != nil {
1234- return AttachTarget{}, ErrInvalidAttachTarget
1234+ return protocol.AttachTarget{}, protocol.ErrInvalidAttachTarget
12351235 }
12361236 if err := skipExactTargetSection(&probe); err != nil {
1237- return AttachTarget{}, ErrInvalidAttachTarget
1237+ return protocol.AttachTarget{}, protocol.ErrInvalidAttachTarget
12381238 }
12391239 if _, err := probe.getBool(); err != nil {
1240- return AttachTarget{}, ErrInvalidAttachTarget
1240+ return protocol.AttachTarget{}, protocol.ErrInvalidAttachTarget
12411241 }
12421242 if err := probe.done(); err != nil {
1243- return AttachTarget{}, ErrInvalidAttachTarget
1243+ return protocol.AttachTarget{}, protocol.ErrInvalidAttachTarget
12441244 }
12451245
12461246 r := payloadReader{b: b}
1247- var m AttachTarget
1247+ var m protocol.AttachTarget
12481248 if m.Endpoint, err = r.getString(); err != nil {
1249- return AttachTarget{}, err
1249+ return protocol.AttachTarget{}, err
12501250 }
12511251 if m.Session, err = r.getString(); err != nil {
1252- return AttachTarget{}, err
1252+ return protocol.AttachTarget{}, err
12531253 }
12541254 if m.Intent, err = r.getUint8(); err != nil {
1255- return AttachTarget{}, err
1255+ return protocol.AttachTarget{}, err
12561256 }
12571257 if m.RemoteTarget, m.EnvironmentPolicy, err = unmarshalRemoteTargetSection(&r); err != nil {
1258- return AttachTarget{}, err
1258+ return protocol.AttachTarget{}, err
12591259 }
12601260 if m.ExactTarget, err = unmarshalExactTargetSection(&r); err != nil {
1261- return AttachTarget{}, err
1261+ return protocol.AttachTarget{}, err
12621262 }
12631263 if m.SamePeer, err = r.getBool(); err != nil {
1264- return AttachTarget{}, err
1264+ return protocol.AttachTarget{}, err
12651265 }
12661266 if err := r.done(); err != nil {
1267- return AttachTarget{}, err
1267+ return protocol.AttachTarget{}, err
12681268 }
1269- if err := ValidateAttachTarget(m); err != nil {
1270- return AttachTarget{}, err
1269+ if err := protocol.ValidateAttachTarget(m); err != nil {
1270+ return protocol.AttachTarget{}, err
12711271 }
12721272 return m, nil
12731273 }
12741274
12751275 // MarshalDetached encodes m into a Detached message payload.
1276-func MarshalDetached(m Detached) []byte {
1276+func MarshalDetached(m protocol.Detached) []byte {
12771277 return []byte{m.Reason}
12781278 }
12791279
12801280 // UnmarshalDetached decodes a Detached message payload.
1281-func UnmarshalDetached(b []byte) (Detached, error) {
1281+func UnmarshalDetached(b []byte) (protocol.Detached, error) {
12821282 r := payloadReader{b: b}
12831283 reason, err := r.getUint8()
12841284 if err != nil {
1285- return Detached{}, err
1285+ return protocol.Detached{}, err
12861286 }
1287- if ValidateDetached(Detached{Reason: reason}) != nil {
1288- return Detached{}, errInvalidEnum
1287+ if protocol.ValidateDetached(protocol.Detached{Reason: reason}) != nil {
1288+ return protocol.Detached{}, errInvalidEnum
12891289 }
12901290 if err := r.done(); err != nil {
1291- return Detached{}, err
1291+ return protocol.Detached{}, err
12921292 }
1293- return Detached{Reason: reason}, nil
1293+ return protocol.Detached{Reason: reason}, nil
12941294 }
12951295
12961296 // MarshalList encodes a List message payload (always empty).
1297-func MarshalList(List) []byte {
1297+func MarshalList(protocol.List) []byte {
12981298 return nil
12991299 }
13001300
13011301 // UnmarshalList decodes a List message payload.
1302-func UnmarshalList(b []byte) (List, error) {
1302+func UnmarshalList(b []byte) (protocol.List, error) {
13031303 r := payloadReader{b: b}
13041304 if err := r.done(); err != nil {
1305- return List{}, err
1305+ return protocol.List{}, err
13061306 }
1307- return List{}, nil
1307+ return protocol.List{}, nil
13081308 }
13091309
13101310 // MarshalOutputResetRequest encodes an OutputResetRequest payload (always
13111311 // empty).
1312-func MarshalOutputResetRequest(OutputResetRequest) []byte {
1312+func MarshalOutputResetRequest(protocol.OutputResetRequest) []byte {
13131313 return nil
13141314 }
13151315
13161316 // UnmarshalOutputResetRequest decodes a strict empty OutputResetRequest
13171317 // payload.
1318-func UnmarshalOutputResetRequest(b []byte) (OutputResetRequest, error) {
1318+func UnmarshalOutputResetRequest(b []byte) (protocol.OutputResetRequest, error) {
13191319 r := payloadReader{b: b}
13201320 if err := r.done(); err != nil {
1321- return OutputResetRequest{}, err
1321+ return protocol.OutputResetRequest{}, err
13221322 }
1323- return OutputResetRequest{}, nil
1323+ return protocol.OutputResetRequest{}, nil
13241324 }
13251325
13261326 // MarshalKill encodes m into a Kill message payload.
1327-func MarshalKill(m Kill) []byte {
1327+func MarshalKill(m protocol.Kill) []byte {
13281328 w := payloadWriter{}
13291329 w.putString(m.Name)
13301330 if m.All {
@@ -1334,28 +1334,28 @@ func MarshalKill(m Kill) []byte {
13341334 }
13351335
13361336 // UnmarshalKill decodes a Kill message payload.
1337-func UnmarshalKill(b []byte) (Kill, error) {
1337+func UnmarshalKill(b []byte) (protocol.Kill, error) {
13381338 r := payloadReader{b: b}
13391339 name, err := r.getString()
13401340 if err != nil {
1341- return Kill{}, err
1341+ return protocol.Kill{}, err
13421342 }
13431343 var all bool
13441344 if len(r.b) > 0 {
13451345 all, err = r.getBool()
13461346 if err != nil {
1347- return Kill{}, err
1347+ return protocol.Kill{}, err
13481348 }
13491349 }
13501350 if err := r.done(); err != nil {
1351- return Kill{}, err
1351+ return protocol.Kill{}, err
13521352 }
1353- return Kill{Name: name, All: all}, nil
1353+ return protocol.Kill{Name: name, All: all}, nil
13541354 }
13551355
13561356 // MarshalSessions encodes m into a Sessions message payload: a uint16 count
13571357 // followed by that many session records.
1358-func MarshalSessions(m Sessions) []byte {
1358+func MarshalSessions(m protocol.Sessions) []byte {
13591359 w := payloadWriter{}
13601360 w.putUint16(uint16(len(m.Sessions)))
13611361 for _, s := range m.Sessions {
@@ -1378,48 +1378,48 @@ func MarshalSessions(m Sessions) []byte {
13781378 }
13791379
13801380 // UnmarshalSessions decodes a Sessions message payload.
1381-func UnmarshalSessions(b []byte) (Sessions, error) {
1381+func UnmarshalSessions(b []byte) (protocol.Sessions, error) {
13821382 r := payloadReader{b: b}
13831383 count, err := r.getUint16()
13841384 if err != nil {
1385- return Sessions{}, err
1385+ return protocol.Sessions{}, err
13861386 }
1387- sessions := make([]SessionInfo, 0, count)
1387+ sessions := make([]protocol.SessionInfo, 0, count)
13881388 for range int(count) {
1389- var s SessionInfo
1389+ var s protocol.SessionInfo
13901390 if s.SessionID, err = r.getString(); err != nil {
1391- return Sessions{}, err
1391+ return protocol.Sessions{}, err
13921392 }
13931393 if s.Name, err = r.getString(); err != nil {
1394- return Sessions{}, err
1394+ return protocol.Sessions{}, err
13951395 }
13961396 eph, err := r.getBool()
13971397 if err != nil {
1398- return Sessions{}, err
1398+ return protocol.Sessions{}, err
13991399 }
14001400 s.Ephemeral = eph
14011401 if s.Tabs, err = r.getUint16(); err != nil {
1402- return Sessions{}, err
1402+ return protocol.Sessions{}, err
14031403 }
14041404 att, err := r.getBool()
14051405 if err != nil {
1406- return Sessions{}, err
1406+ return protocol.Sessions{}, err
14071407 }
14081408 s.Attached = att
14091409 state, err := r.getUint8()
14101410 if err != nil {
1411- return Sessions{}, err
1411+ return protocol.Sessions{}, err
14121412 }
1413- s.State = SessionState(state)
1414- if s.State > SessionBroken {
1415- return Sessions{}, errors.New("ports: invalid session state")
1413+ s.State = protocol.SessionState(state)
1414+ if s.State > protocol.SessionBroken {
1415+ return protocol.Sessions{}, errors.New("ports: invalid session state")
14161416 }
14171417 sessions = append(sessions, s)
14181418 }
14191419 if err := r.done(); err != nil {
1420- return Sessions{}, err
1420+ return protocol.Sessions{}, err
14211421 }
1422- return Sessions{Sessions: sessions}, nil
1422+ return protocol.Sessions{Sessions: sessions}, nil
14231423 }
14241424 func putInt16(w *payloadWriter, n int) { w.putUint16(uint16(int16(n))) }
14251425
@@ -1737,7 +1737,7 @@ func MarshalParkedRouteRequest(request protocol.ParkedRouteRequest) []byte {
17371737 w.putUint64(request.RequestID)
17381738 w.putBytes(request.LeaseID[:])
17391739 w.putUint8(uint8(request.Action))
1740- marshalRemoteTargetSection(&w, request.Target, EnvironmentPolicyDaemonOwned)
1740+ marshalRemoteTargetSection(&w, request.Target, protocol.EnvironmentPolicyDaemonOwned)
17411741 return w.b
17421742 }
17431743
@@ -1757,7 +1757,7 @@ func UnmarshalParkedRouteRequest(b []byte) (protocol.ParkedRouteRequest, error)
17571757 return protocol.ParkedRouteRequest{}, protocol.ErrInvalidNavigation
17581758 }
17591759 target, policy, err := unmarshalRemoteTargetSection(&r)
1760- if err != nil || policy != EnvironmentPolicyDaemonOwned || r.done() != nil {
1760+ if err != nil || policy != protocol.EnvironmentPolicyDaemonOwned || r.done() != nil {
17611761 return protocol.ParkedRouteRequest{}, protocol.ErrInvalidNavigation
17621762 }
17631763 request := protocol.ParkedRouteRequest{RequestID: requestID, Action: protocol.ParkedRouteAction(action), Target: target}
M internal/ports/wire_command.go +27 -49
@@ -4,35 +4,13 @@ import (
44 "encoding/binary"
55 "errors"
66 "math"
7+
8+ "github.com/bnema/vev/internal/protocol"
79 )
810
911 // ErrTooManyCommandArgs reports that a request exceeds the wire count limit.
1012 var ErrTooManyCommandArgs = errors.New("ports: too many command arguments")
1113
12-// CommandRequest asks the daemon to run one control command. Version must
13-// stay first so a future payload layout can still be rejected cleanly.
14-type CommandRequest struct {
15- Version uint16
16- RequestID uint64
17- Attached bool
18- Self bool
19- Slug string
20- Args []string
21- TargetSession string
22- TargetTab string
23- TargetPane string
24- JSON bool
25-}
26-
27-// CommandResult reports a control command's outcome.
28-type CommandResult struct {
29- RequestID uint64
30- OK bool
31- Code uint16
32- Text string
33- Output string
34-}
35-
3614 // PeekCommandVersion returns the leading protocol version from a
3715 // CommandRequest payload.
3816 func PeekCommandVersion(b []byte) (uint16, bool) {
@@ -43,7 +21,7 @@ func PeekCommandVersion(b []byte) (uint16, bool) {
4321 }
4422
4523 // MarshalCommandRequest encodes m into a CommandRequest payload.
46-func MarshalCommandRequest(m CommandRequest) ([]byte, error) {
24+func MarshalCommandRequest(m protocol.CommandRequest) ([]byte, error) {
4725 if len(m.Args) > math.MaxUint16 {
4826 return nil, ErrTooManyCommandArgs
4927 }
@@ -66,65 +44,65 @@ func MarshalCommandRequest(m CommandRequest) ([]byte, error) {
6644 }
6745
6846 // UnmarshalCommandRequest decodes a strict CommandRequest payload.
69-func UnmarshalCommandRequest(b []byte) (CommandRequest, error) {
47+func UnmarshalCommandRequest(b []byte) (protocol.CommandRequest, error) {
7048 r := payloadReader{b: b}
71- var m CommandRequest
49+ var m protocol.CommandRequest
7250 var err error
7351
7452 if m.Version, err = r.getUint16(); err != nil {
75- return CommandRequest{}, err
53+ return protocol.CommandRequest{}, err
7654 }
7755 if m.RequestID, err = r.getUint64(); err != nil {
78- return CommandRequest{}, err
56+ return protocol.CommandRequest{}, err
7957 }
8058 if m.Attached, err = r.getBool(); err != nil {
81- return CommandRequest{}, err
59+ return protocol.CommandRequest{}, err
8260 }
8361 if m.Self, err = r.getBool(); err != nil {
84- return CommandRequest{}, err
62+ return protocol.CommandRequest{}, err
8563 }
8664 if m.Slug, err = r.getString(); err != nil {
87- return CommandRequest{}, err
65+ return protocol.CommandRequest{}, err
8866 }
8967 argCount, err := r.getUint16()
9068 if err != nil {
91- return CommandRequest{}, err
69+ return protocol.CommandRequest{}, err
9270 }
9371 // Every argument requires at least a uint32 length prefix. Reject an
9472 // impossible count before allocating from untrusted input.
9573 if uint64(argCount) > uint64(len(r.b)/4) {
96- return CommandRequest{}, errShortPayload
74+ return protocol.CommandRequest{}, errShortPayload
9775 }
9876 if argCount != 0 {
9977 m.Args = make([]string, 0, int(argCount))
10078 for range int(argCount) {
10179 arg, err := r.getLongString()
10280 if err != nil {
103- return CommandRequest{}, err
81+ return protocol.CommandRequest{}, err
10482 }
10583 m.Args = append(m.Args, arg)
10684 }
10785 }
10886 if m.TargetSession, err = r.getString(); err != nil {
109- return CommandRequest{}, err
87+ return protocol.CommandRequest{}, err
11088 }
11189 if m.TargetTab, err = r.getString(); err != nil {
112- return CommandRequest{}, err
90+ return protocol.CommandRequest{}, err
11391 }
11492 if m.TargetPane, err = r.getString(); err != nil {
115- return CommandRequest{}, err
93+ return protocol.CommandRequest{}, err
11694 }
11795 if m.JSON, err = r.getBool(); err != nil {
118- return CommandRequest{}, err
96+ return protocol.CommandRequest{}, err
11997 }
12098 if err := r.done(); err != nil {
121- return CommandRequest{}, err
99+ return protocol.CommandRequest{}, err
122100 }
123101 return m, nil
124102 }
125103
126104 // MarshalCommandResult encodes m into a CommandResult payload.
127-func MarshalCommandResult(m CommandResult) []byte {
105+func MarshalCommandResult(m protocol.CommandResult) []byte {
128106 w := payloadWriter{}
129107 w.putUint64(m.RequestID)
130108 w.putBool(m.OK)
@@ -135,28 +113,28 @@ func MarshalCommandResult(m CommandResult) []byte {
135113 }
136114
137115 // UnmarshalCommandResult decodes a strict CommandResult payload.
138-func UnmarshalCommandResult(b []byte) (CommandResult, error) {
116+func UnmarshalCommandResult(b []byte) (protocol.CommandResult, error) {
139117 r := payloadReader{b: b}
140- var m CommandResult
118+ var m protocol.CommandResult
141119 var err error
142120
143121 if m.RequestID, err = r.getUint64(); err != nil {
144- return CommandResult{}, err
122+ return protocol.CommandResult{}, err
145123 }
146124 if m.OK, err = r.getBool(); err != nil {
147- return CommandResult{}, err
125+ return protocol.CommandResult{}, err
148126 }
149127 if m.Code, err = r.getUint16(); err != nil {
150- return CommandResult{}, err
128+ return protocol.CommandResult{}, err
151129 }
152130 if m.Text, err = r.getString(); err != nil {
153- return CommandResult{}, err
131+ return protocol.CommandResult{}, err
154132 }
155133 if m.Output, err = r.getLongString(); err != nil {
156- return CommandResult{}, err
134+ return protocol.CommandResult{}, err
157135 }
158136 if err := r.done(); err != nil {
159- return CommandResult{}, err
137+ return protocol.CommandResult{}, err
160138 }
161139 return m, nil
162140 }
M internal/ports/wire_final_test.go +27 -27
@@ -12,21 +12,21 @@ import (
1212 )
1313
1414 func TestFinalProtocolVersionAndNoV21Hello(t *testing.T) {
15- if ProtocolVersion != 37 {
16- t.Fatalf("ProtocolVersion = %d, want 37", ProtocolVersion)
15+ if protocol.Version != 37 {
16+ t.Fatalf("ProtocolVersion = %d, want 37", protocol.Version)
1717 }
18- payload := MarshalHello(Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}})
18+ payload := MarshalHello(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}})
1919 if len(payload) < 2 {
2020 t.Fatal("MarshalHello produced a truncated valid payload")
2121 }
2222 binary.BigEndian.PutUint16(payload, 21)
23- if _, err := UnmarshalHello(payload); !errors.Is(err, ErrInvalidHello) {
23+ if _, err := UnmarshalHello(payload); !errors.Is(err, protocol.ErrInvalidHello) {
2424 t.Fatalf("UnmarshalHello(v21) error = %v, want ErrInvalidHello", err)
2525 }
2626 }
2727
2828 func TestFinalHelloGoldenStrict(t *testing.T) {
29- msg := Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}
29+ msg := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}
3030 want := append([]byte{0, 37, 2}, make([]byte, 16+8)...)
3131 want = append(want, 0, 0, 0, 80, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
3232 want = append(want, 0, 0)
@@ -46,7 +46,7 @@ func TestFinalHelloGoldenStrict(t *testing.T) {
4646 }
4747
4848 func TestHelloDeclaresKittyDirectGraphicsAtWireTail(t *testing.T) {
49- msg := Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, KittyDirectGraphics: true}
49+ msg := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, KittyDirectGraphics: true}
5050 payload := MarshalHello(msg)
5151 if len(payload) == 0 || payload[len(payload)-1] != 1 {
5252 t.Fatalf("Hello capability tail = %x, want trailing true declaration", payload)
@@ -61,20 +61,20 @@ func TestHelloDeclaresKittyDirectGraphicsAtWireTail(t *testing.T) {
6161 }
6262
6363 func TestFinalHelloSemanticValidation(t *testing.T) {
64- valid := Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}
64+ valid := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}
6565 for _, tt := range []struct {
6666 name string
67- mutate func(*Hello)
67+ mutate func(*protocol.Hello)
6868 }{
69- {name: "intent", mutate: func(h *Hello) { h.Intent = 99 }},
70- {name: "zero columns", mutate: func(h *Hello) { h.Size.Cols = 0 }},
71- {name: "zero rows", mutate: func(h *Hello) { h.Size.Rows = 0 }},
72- {name: "area", mutate: func(h *Hello) { h.Size = domain.Size{Cols: 513, Rows: 512} }},
69+ {name: "intent", mutate: func(h *protocol.Hello) { h.Intent = 99 }},
70+ {name: "zero columns", mutate: func(h *protocol.Hello) { h.Size.Cols = 0 }},
71+ {name: "zero rows", mutate: func(h *protocol.Hello) { h.Size.Rows = 0 }},
72+ {name: "area", mutate: func(h *protocol.Hello) { h.Size = domain.Size{Cols: 513, Rows: 512} }},
7373 } {
7474 t.Run(tt.name, func(t *testing.T) {
7575 msg := valid
7676 tt.mutate(&msg)
77- if err := ValidateHello(msg); err == nil {
77+ if err := protocol.ValidateHello(msg); err == nil {
7878 t.Fatal("ValidateHello accepted malformed semantics")
7979 }
8080 })
@@ -195,7 +195,7 @@ func TestFinalAckGoldenStrict(t *testing.T) {
195195 }
196196
197197 func TestFinalAttachTargetGoldenStrict(t *testing.T) {
198- msg := AttachTarget{Endpoint: "host", Session: "work", Intent: IntentAttach}
198+ msg := protocol.AttachTarget{Endpoint: "host", Session: "work", Intent: protocol.IntentAttach}
199199 want := []byte{0, 4, 'h', 'o', 's', 't', 0, 4, 'w', 'o', 'r', 'k', 2, 0, 0, 0, 0}
200200 got := MarshalAttachTarget(msg)
201201 if !bytes.Equal(got, want) {
@@ -207,12 +207,12 @@ func TestFinalAttachTargetGoldenStrict(t *testing.T) {
207207 }
208208 assertAllPrefixesFail(t, got, UnmarshalAttachTarget)
209209 assertTrailingGarbageFails(t, got, UnmarshalAttachTarget)
210- local := AttachTarget{Session: "work", Intent: IntentAttach}
210+ local := protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach}
211211 localPayload := MarshalAttachTarget(local)
212212 if localPayload == nil {
213213 t.Fatal("MarshalAttachTarget rejected same-peer route handoff")
214214 }
215- wantLocal := []byte{0, 0, 0, 4, 'w', 'o', 'r', 'k', IntentAttach, 0, 0, 0, 0}
215+ wantLocal := []byte{0, 0, 0, 4, 'w', 'o', 'r', 'k', protocol.IntentAttach, 0, 0, 0, 0}
216216 if !bytes.Equal(localPayload, wantLocal) {
217217 t.Fatalf("same-peer bytes = %x, want %x", localPayload, wantLocal)
218218 }
@@ -222,7 +222,7 @@ func TestFinalAttachTargetGoldenStrict(t *testing.T) {
222222 }
223223 assertAllPrefixesFail(t, localPayload, UnmarshalAttachTarget)
224224 assertTrailingGarbageFails(t, localPayload, UnmarshalAttachTarget)
225- for _, bad := range []AttachTarget{{Endpoint: "host", Intent: IntentAttach}, {Endpoint: "host", Session: "work", Intent: 99}} {
225+ for _, bad := range []protocol.AttachTarget{{Endpoint: "host", Intent: protocol.IntentAttach}, {Endpoint: "host", Session: "work", Intent: 99}} {
226226 if got := MarshalAttachTarget(bad); got != nil {
227227 t.Fatalf("MarshalAttachTarget(%+v) = %x, want nil", bad, got)
228228 }
@@ -232,9 +232,9 @@ func TestFinalAttachTargetGoldenStrict(t *testing.T) {
232232 func TestAttachTargetExactTargetWireStrict(t *testing.T) {
233233 var lifecycle domain.SessionLifecycleID
234234 lifecycle[0] = 1
235- target := AttachTarget{Session: "work", Intent: IntentAttach, ExactTarget: &protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}, SamePeer: true}
235+ target := protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach, ExactTarget: &protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}, SamePeer: true}
236236 payload := MarshalAttachTarget(target)
237- want := append([]byte{0, 0, 0, 4, 'w', 'o', 'r', 'k', IntentAttach, 0, 0, 1, 1}, make([]byte, 15)...)
237+ want := append([]byte{0, 0, 0, 4, 'w', 'o', 'r', 'k', protocol.IntentAttach, 0, 0, 1, 1}, make([]byte, 15)...)
238238 want = append(want, 0, 4, 'w', 'o', 'r', 'k', 1)
239239 if !bytes.Equal(payload, want) {
240240 t.Fatalf("exact target bytes = %x, want %x", payload, want)
@@ -261,7 +261,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
261261 {
262262 name: "hello boolean",
263263 payload: func() []byte {
264- b := MarshalHello(Hello{Version: ProtocolVersion, Size: domain.Size{Cols: 1, Rows: 1}})
264+ b := MarshalHello(protocol.Hello{Version: protocol.Version, Size: domain.Size{Cols: 1, Rows: 1}})
265265 b[len(b)-8] = 2
266266 return b
267267 }(),
@@ -270,7 +270,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
270270 {
271271 name: "hello remote boolean",
272272 payload: func() []byte {
273- b := MarshalHello(Hello{Version: ProtocolVersion, Size: domain.Size{Cols: 1, Rows: 1}})
273+ b := MarshalHello(protocol.Hello{Version: protocol.Version, Size: domain.Size{Cols: 1, Rows: 1}})
274274 b[len(b)-2] = 2
275275 return b
276276 }(),
@@ -279,7 +279,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
279279 {
280280 name: "hello Kitty graphics boolean",
281281 payload: func() []byte {
282- b := MarshalHello(Hello{Version: ProtocolVersion, Size: domain.Size{Cols: 1, Rows: 1}})
282+ b := MarshalHello(protocol.Hello{Version: protocol.Version, Size: domain.Size{Cols: 1, Rows: 1}})
283283 b[len(b)-1] = 2
284284 return b
285285 }(),
@@ -288,7 +288,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
288288 {
289289 name: "attach target same-peer boolean",
290290 payload: func() []byte {
291- b := MarshalAttachTarget(AttachTarget{Endpoint: "host", Session: "work", Intent: IntentAttach})
291+ b := MarshalAttachTarget(protocol.AttachTarget{Endpoint: "host", Session: "work", Intent: protocol.IntentAttach})
292292 b[len(b)-1] = 2
293293 return b
294294 }(),
@@ -297,7 +297,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
297297 {
298298 name: "welcome boolean",
299299 payload: func() []byte {
300- b := MarshalWelcome(Welcome{SessionID: "id"})
300+ b := MarshalWelcome(protocol.Welcome{SessionID: "id"})
301301 b[6] = 2
302302 return b
303303 }(),
@@ -307,7 +307,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
307307 {
308308 name: "kill boolean",
309309 payload: func() []byte {
310- b := MarshalKill(Kill{All: true})
310+ b := MarshalKill(protocol.Kill{All: true})
311311 b[len(b)-1] = 2
312312 return b
313313 }(),
@@ -316,7 +316,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
316316 {
317317 name: "sessions ephemeral boolean",
318318 payload: func() []byte {
319- b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionUp}}})
319+ b := MarshalSessions(protocol.Sessions{Sessions: []protocol.SessionInfo{{State: protocol.SessionUp}}})
320320 b[6] = 2
321321 return b
322322 }(),
@@ -325,7 +325,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
325325 {
326326 name: "sessions attached boolean",
327327 payload: func() []byte {
328- b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionUp}}})
328+ b := MarshalSessions(protocol.Sessions{Sessions: []protocol.SessionInfo{{State: protocol.SessionUp}}})
329329 b[9] = 2
330330 return b
331331 }(),
M internal/ports/wire_test.go +92 -92
@@ -63,14 +63,14 @@ func appendNoNavigationTail(payload []byte) []byte {
6363 func TestHelloGoldenAndRoundTrip(t *testing.T) {
6464 tests := []struct {
6565 name string
66- msg Hello
66+ msg protocol.Hello
6767 want []byte
6868 }{
6969 {
7070 name: "typical",
71- msg: Hello{
71+ msg: protocol.Hello{
7272 Version: 1,
73- Intent: IntentNew,
73+ Intent: protocol.IntentNew,
7474 ClientID: [16]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10},
7575 ResumeToken: 0x0102030405060708,
7676 Name: "w0",
@@ -84,9 +84,9 @@ func TestHelloGoldenAndRoundTrip(t *testing.T) {
8484 },
8585 {
8686 name: "empty strings",
87- msg: Hello{
87+ msg: protocol.Hello{
8888 Version: 1,
89- Intent: IntentEphemeral,
89+ Intent: protocol.IntentEphemeral,
9090 Name: "",
9191 Size: domain.Size{Cols: 1, Rows: 1},
9292 TermEnv: "",
@@ -129,8 +129,8 @@ func TestHelloGoldenAndRoundTrip(t *testing.T) {
129129
130130 func TestHelloEnvironmentCodec(t *testing.T) {
131131 t.Run("literal wire layout", func(t *testing.T) {
132- msg := Hello{
133- Version: ProtocolVersion,
132+ msg := protocol.Hello{
133+ Version: protocol.Version,
134134 Size: domain.Size{Cols: 1, Rows: 1},
135135 Env: []string{"A=B", "XY=123"},
136136 }
@@ -163,20 +163,20 @@ func TestHelloEnvironmentCodec(t *testing.T) {
163163 t.Run("lossless order and values", func(t *testing.T) {
164164 entryOverUint16 := "LARGE=" + string(bytes.Repeat([]byte("x"), 65536))
165165 want := []string{"A=first", "TOKEN=a=b=c", "EMPTY=", entryOverUint16, "A=second"}
166- payload := MarshalHello(Hello{Version: ProtocolVersion, Size: domain.Size{Cols: 1, Rows: 1}, Env: want})
166+ payload := MarshalHello(protocol.Hello{Version: protocol.Version, Size: domain.Size{Cols: 1, Rows: 1}, Env: want})
167167 got, err := UnmarshalHello(payload)
168168 require.NoError(t, err)
169169 require.Equal(t, want, got.Env)
170170 })
171171
172172 t.Run("empty", func(t *testing.T) {
173- payload := MarshalHello(Hello{Version: ProtocolVersion, Size: domain.Size{Cols: 1, Rows: 1}, Env: []string{}})
173+ payload := MarshalHello(protocol.Hello{Version: protocol.Version, Size: domain.Size{Cols: 1, Rows: 1}, Env: []string{}})
174174 got, err := UnmarshalHello(payload)
175175 require.NoError(t, err)
176176 require.Empty(t, got.Env)
177177 })
178178
179- base := MarshalHello(Hello{Version: ProtocolVersion, Size: domain.Size{Cols: 1, Rows: 1}})
179+ base := MarshalHello(protocol.Hello{Version: protocol.Version, Size: domain.Size{Cols: 1, Rows: 1}})
180180 withCount := func(count byte) []byte {
181181 payload := append([]byte(nil), base...)
182182 // The uint32 environment count occupies bytes len-13 through len-10.
@@ -214,11 +214,11 @@ func TestHelloEnvironmentCodec(t *testing.T) {
214214 func TestInputGoldenAndRoundTrip(t *testing.T) {
215215 tests := []struct {
216216 name string
217- msg Input
217+ msg protocol.Input
218218 want []byte
219219 }{
220- {name: "data", msg: Input{InputSeq: 0x0102030405060708, Data: []byte("hi")}, want: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x68, 0x69}},
221- {name: "empty", msg: Input{InputSeq: 0, Data: nil}, want: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}},
220+ {name: "data", msg: protocol.Input{InputSeq: 0x0102030405060708, Data: []byte("hi")}, want: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x68, 0x69}},
221+ {name: "empty", msg: protocol.Input{InputSeq: 0, Data: nil}, want: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}},
222222 }
223223
224224 for _, tt := range tests {
@@ -367,7 +367,7 @@ func TestThemeGenerationClearedWireGoldenPreservesProtocolVersion(t *testing.T)
367367 }
368368 want := append([]byte{0x0f, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x00}, make([]byte, 48)...)
369369 require.Equal(t, want, MarshalTheme(cleared))
370- require.Equal(t, uint16(37), ProtocolVersion)
370+ require.Equal(t, uint16(37), protocol.Version)
371371 }
372372
373373 func TestResizeGoldenAndRoundTrip(t *testing.T) {
@@ -394,7 +394,7 @@ func TestResizeGoldenAndRoundTrip(t *testing.T) {
394394 }
395395
396396 func TestDetachGoldenAndRoundTrip(t *testing.T) {
397- got := MarshalDetach(Detach{})
397+ got := MarshalDetach(protocol.Detach{})
398398 if len(got) != 0 {
399399 t.Fatalf("MarshalDetach() = %#v, want empty", got)
400400 }
@@ -402,14 +402,14 @@ func TestDetachGoldenAndRoundTrip(t *testing.T) {
402402 if err != nil {
403403 t.Fatalf("UnmarshalDetach() error = %v", err)
404404 }
405- if back != (Detach{}) {
406- t.Fatalf("round trip = %#v, want %#v", back, Detach{})
405+ if back != (protocol.Detach{}) {
406+ t.Fatalf("round trip = %#v, want %#v", back, protocol.Detach{})
407407 }
408408 assertTrailingGarbageFails(t, got, UnmarshalDetach)
409409 }
410410
411411 func TestPingGoldenAndRoundTrip(t *testing.T) {
412- got := MarshalPing(Ping{})
412+ got := MarshalPing(protocol.Ping{})
413413 if len(got) != 0 {
414414 t.Fatalf("MarshalPing() = %#v, want empty", got)
415415 }
@@ -417,14 +417,14 @@ func TestPingGoldenAndRoundTrip(t *testing.T) {
417417 if err != nil {
418418 t.Fatalf("UnmarshalPing() error = %v", err)
419419 }
420- if back != (Ping{}) {
421- t.Fatalf("round trip = %#v, want %#v", back, Ping{})
420+ if back != (protocol.Ping{}) {
421+ t.Fatalf("round trip = %#v, want %#v", back, protocol.Ping{})
422422 }
423423 assertTrailingGarbageFails(t, got, UnmarshalPing)
424424 }
425425
426426 func TestPongGoldenAndRoundTrip(t *testing.T) {
427- got := MarshalPong(Pong{})
427+ got := MarshalPong(protocol.Pong{})
428428 if len(got) != 0 {
429429 t.Fatalf("MarshalPong() = %#v, want empty", got)
430430 }
@@ -432,14 +432,14 @@ func TestPongGoldenAndRoundTrip(t *testing.T) {
432432 if err != nil {
433433 t.Fatalf("UnmarshalPong() error = %v", err)
434434 }
435- if back != (Pong{}) {
436- t.Fatalf("round trip = %#v, want %#v", back, Pong{})
435+ if back != (protocol.Pong{}) {
436+ t.Fatalf("round trip = %#v, want %#v", back, protocol.Pong{})
437437 }
438438 assertTrailingGarbageFails(t, got, UnmarshalPong)
439439 }
440440
441441 func TestMarshalWelcomeRejectsInvalidCommittedIdentity(t *testing.T) {
442- got := MarshalWelcome(Welcome{
442+ got := MarshalWelcome(protocol.Welcome{
443443 SessionName: "work",
444444 CommittedIdentity: &protocol.CommittedRouteIdentity{
445445 Target: protocol.ExactSessionTarget{SessionName: "different"},
@@ -449,7 +449,7 @@ func TestMarshalWelcomeRejectsInvalidCommittedIdentity(t *testing.T) {
449449 }
450450
451451 func TestMarshalWelcomeRejectsCommittedIdentityMetadataMismatch(t *testing.T) {
452- base := Welcome{
452+ base := protocol.Welcome{
453453 SessionID: "session",
454454 SessionName: "work",
455455 Ephemeral: true,
@@ -458,9 +458,9 @@ func TestMarshalWelcomeRejectsCommittedIdentityMetadataMismatch(t *testing.T) {
458458 Ephemeral: true,
459459 },
460460 }
461- for name, mutate := range map[string]func(*Welcome){
462- "session name": func(w *Welcome) { w.SessionName = "other" },
463- "ephemeral": func(w *Welcome) { w.Ephemeral = false },
461+ for name, mutate := range map[string]func(*protocol.Welcome){
462+ "session name": func(w *protocol.Welcome) { w.SessionName = "other" },
463+ "ephemeral": func(w *protocol.Welcome) { w.Ephemeral = false },
464464 } {
465465 t.Run(name, func(t *testing.T) {
466466 got := base
@@ -474,17 +474,17 @@ func TestMarshalWelcomeRejectsCommittedIdentityMetadataMismatch(t *testing.T) {
474474 func TestWelcomeGoldenAndRoundTrip(t *testing.T) {
475475 tests := []struct {
476476 name string
477- msg Welcome
477+ msg protocol.Welcome
478478 want []byte
479479 }{
480480 {
481481 name: "ephemeral",
482- msg: Welcome{SessionID: "sess-1", SessionName: "main", Ephemeral: true, ResumeToken: 0x0102030405060708, Capabilities: CapabilityResume | CapabilityPredict},
482+ msg: protocol.Welcome{SessionID: "sess-1", SessionName: "main", Ephemeral: true, ResumeToken: 0x0102030405060708, Capabilities: protocol.CapabilityResume | protocol.CapabilityPredict},
483483 want: []byte{0x00, 0x06, 0x73, 0x65, 0x73, 0x73, 0x2d, 0x31, 0x00, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x00, 0x00, 0x05, 0x00},
484484 },
485485 {
486486 name: "non-ephemeral empty name",
487- msg: Welcome{SessionID: "abc", SessionName: "", Ephemeral: false},
487+ msg: protocol.Welcome{SessionID: "abc", SessionName: "", Ephemeral: false},
488488 want: []byte{0x00, 0x03, 0x61, 0x62, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
489489 },
490490 }
@@ -511,22 +511,22 @@ func TestWelcomeGoldenAndRoundTrip(t *testing.T) {
511511 }
512512
513513 func TestCommandErrorCodes(t *testing.T) {
514- require.Equal(t, uint16(6), ErrUnknownCommand)
515- require.Equal(t, uint16(7), ErrNotScriptable)
516- require.Equal(t, uint16(8), ErrInvalidCommandArgs)
517- require.Equal(t, uint16(9), ErrNoSuchTarget)
518- require.Equal(t, uint16(10), ErrAmbiguousTarget)
514+ require.Equal(t, uint16(6), protocol.ErrUnknownCommand)
515+ require.Equal(t, uint16(7), protocol.ErrNotScriptable)
516+ require.Equal(t, uint16(8), protocol.ErrInvalidCommandArgs)
517+ require.Equal(t, uint16(9), protocol.ErrNoSuchTarget)
518+ require.Equal(t, uint16(10), protocol.ErrAmbiguousTarget)
519519 }
520520
521521 func TestCommandRequestGoldenAndRoundTrip(t *testing.T) {
522522 tests := []struct {
523523 name string
524- msg CommandRequest
524+ msg protocol.CommandRequest
525525 want []byte
526526 }{
527527 {
528528 name: "minimal",
529- msg: CommandRequest{Version: ProtocolVersion, Slug: "split-right"},
529+ msg: protocol.CommandRequest{Version: protocol.Version, Slug: "split-right"},
530530 want: []byte{
531531 0x00, 0x25,
532532 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // request ID
@@ -540,8 +540,8 @@ func TestCommandRequestGoldenAndRoundTrip(t *testing.T) {
540540 },
541541 {
542542 name: "full",
543- msg: CommandRequest{
544- Version: ProtocolVersion,
543+ msg: protocol.CommandRequest{
544+ Version: protocol.Version,
545545 Self: true,
546546 Slug: "toast",
547547 Args: []string{"-l", "warn", "tests KO"},
@@ -590,8 +590,8 @@ func TestCommandRequestGoldenAndRoundTrip(t *testing.T) {
590590 }
591591
592592 func TestMarshalCommandRequestRejectsTooManyArguments(t *testing.T) {
593- payload, err := MarshalCommandRequest(CommandRequest{
594- Version: ProtocolVersion,
593+ payload, err := MarshalCommandRequest(protocol.CommandRequest{
594+ Version: protocol.Version,
595595 Slug: "toast",
596596 Args: make([]string, math.MaxUint16+1),
597597 })
@@ -601,7 +601,7 @@ func TestMarshalCommandRequestRejectsTooManyArguments(t *testing.T) {
601601 }
602602
603603 func TestCommandRequestRejectsImpossibleArgumentCount(t *testing.T) {
604- payload, err := MarshalCommandRequest(CommandRequest{Version: ProtocolVersion, Slug: "toast"})
604+ payload, err := MarshalCommandRequest(protocol.CommandRequest{Version: protocol.Version, Slug: "toast"})
605605 require.NoError(t, err)
606606 argCountOffset := 2 + 8 + 1 + 1 + 2 + len("toast")
607607 payload[argCountOffset] = 0xff
@@ -615,12 +615,12 @@ func TestCommandRequestRejectsImpossibleArgumentCount(t *testing.T) {
615615 func TestCommandResultGoldenAndRoundTrip(t *testing.T) {
616616 tests := []struct {
617617 name string
618- msg CommandResult
618+ msg protocol.CommandResult
619619 want []byte
620620 }{
621621 {
622622 name: "error",
623- msg: CommandResult{Code: ErrNoSuchTarget, Text: "no such session"},
623+ msg: protocol.CommandResult{Code: protocol.ErrNoSuchTarget, Text: "no such session"},
624624 want: []byte{
625625 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // request ID
626626 0x00,
@@ -631,7 +631,7 @@ func TestCommandResultGoldenAndRoundTrip(t *testing.T) {
631631 },
632632 {
633633 name: "success with output",
634- msg: CommandResult{OK: true, Text: "listed", Output: "ID\tFOCUSED\n"},
634+ msg: protocol.CommandResult{OK: true, Text: "listed", Output: "ID\tFOCUSED\n"},
635635 want: []byte{
636636 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // request ID
637637 0x01,
@@ -656,7 +656,7 @@ func TestCommandResultGoldenAndRoundTrip(t *testing.T) {
656656 }
657657
658658 func TestErrorMsgGoldenAndRoundTrip(t *testing.T) {
659- msg := ErrorMsg{Code: ErrNameTaken, Text: "name taken"}
659+ msg := protocol.ErrorMsg{Code: protocol.ErrNameTaken, Text: "name taken"}
660660 want := []byte{0x00, 0x03, 0x00, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x74, 0x61, 0x6b, 0x65, 0x6e}
661661
662662 got := MarshalErrorMsg(msg)
@@ -722,7 +722,7 @@ func TestAckGoldenAndRoundTrip(t *testing.T) {
722722 }
723723
724724 func TestDetachedGoldenAndRoundTrip(t *testing.T) {
725- msg := Detached{Reason: ReasonReplaced}
725+ msg := protocol.Detached{Reason: protocol.ReasonReplaced}
726726 want := []byte{0x03}
727727
728728 got := MarshalDetached(msg)
@@ -744,13 +744,13 @@ func TestDetachedGoldenAndRoundTrip(t *testing.T) {
744744 func TestClientNoticeGoldenAndStrict(t *testing.T) {
745745 tests := []struct {
746746 name string
747- msg ClientNotice
747+ msg protocol.ClientNotice
748748 want []byte
749749 }{
750- {name: "clipboard fallback", msg: ClientNotice{Action: ClientNoticeClipboardFallback}, want: []byte{0x01}},
751- {name: "clipboard too large", msg: ClientNotice{Action: ClientNoticeClipboardTooLarge}, want: []byte{0x02}},
752- {name: "link degraded", msg: ClientNotice{Action: ClientNoticeLinkDegraded}, want: []byte{0x03}},
753- {name: "link connected", msg: ClientNotice{Action: ClientNoticeLinkConnected}, want: []byte{0x04}},
750+ {name: "clipboard fallback", msg: protocol.ClientNotice{Action: protocol.ClientNoticeClipboardFallback}, want: []byte{0x01}},
751+ {name: "clipboard too large", msg: protocol.ClientNotice{Action: protocol.ClientNoticeClipboardTooLarge}, want: []byte{0x02}},
752+ {name: "link degraded", msg: protocol.ClientNotice{Action: protocol.ClientNoticeLinkDegraded}, want: []byte{0x03}},
753+ {name: "link connected", msg: protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}, want: []byte{0x04}},
754754 }
755755 for _, tt := range tests {
756756 t.Run(tt.name, func(t *testing.T) {
@@ -768,7 +768,7 @@ func TestClientNoticeGoldenAndStrict(t *testing.T) {
768768 }
769769
770770 func TestListGoldenAndRoundTrip(t *testing.T) {
771- got := MarshalList(List{})
771+ got := MarshalList(protocol.List{})
772772 if len(got) != 0 {
773773 t.Fatalf("MarshalList() = %#v, want empty", got)
774774 }
@@ -776,8 +776,8 @@ func TestListGoldenAndRoundTrip(t *testing.T) {
776776 if err != nil {
777777 t.Fatalf("UnmarshalList() error = %v", err)
778778 }
779- if back != (List{}) {
780- t.Fatalf("round trip = %#v, want %#v", back, List{})
779+ if back != (protocol.List{}) {
780+ t.Fatalf("round trip = %#v, want %#v", back, protocol.List{})
781781 }
782782 assertTrailingGarbageFails(t, got, UnmarshalList)
783783 }
@@ -785,12 +785,12 @@ func TestListGoldenAndRoundTrip(t *testing.T) {
785785 func TestKillGoldenAndRoundTrip(t *testing.T) {
786786 tests := []struct {
787787 name string
788- msg Kill
788+ msg protocol.Kill
789789 want []byte
790790 }{
791- {name: "named", msg: Kill{Name: "main"}, want: []byte{0x00, 0x04, 0x6d, 0x61, 0x69, 0x6e}},
792- {name: "empty", msg: Kill{Name: ""}, want: []byte{0x00, 0x00}},
793- {name: "all", msg: Kill{All: true}, want: []byte{0x00, 0x00, 0x01}},
791+ {name: "named", msg: protocol.Kill{Name: "main"}, want: []byte{0x00, 0x04, 0x6d, 0x61, 0x69, 0x6e}},
792+ {name: "empty", msg: protocol.Kill{Name: ""}, want: []byte{0x00, 0x00}},
793+ {name: "all", msg: protocol.Kill{All: true}, want: []byte{0x00, 0x00, 0x01}},
794794 }
795795
796796 for _, tt := range tests {
@@ -814,7 +814,7 @@ func TestKillGoldenAndRoundTrip(t *testing.T) {
814814 if err != nil {
815815 t.Fatalf("UnmarshalKill(legacy) error = %v", err)
816816 }
817- if !reflect.DeepEqual(back, Kill{Name: "main"}) {
817+ if !reflect.DeepEqual(back, protocol.Kill{Name: "main"}) {
818818 t.Fatalf("legacy round trip = %#v, want named kill", back)
819819 }
820820 assertAllPrefixesFail(t, legacy, UnmarshalKill)
@@ -824,16 +824,16 @@ func TestKillGoldenAndRoundTrip(t *testing.T) {
824824 func TestSessionInfoRecoveryState(t *testing.T) {
825825 tests := []struct {
826826 name string
827- state SessionState
827+ state protocol.SessionState
828828 want []byte
829829 }{
830- {name: "up", state: SessionUp, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
831- {name: "down", state: SessionDown, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1}},
832- {name: "broken", state: SessionBroken, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2}},
830+ {name: "up", state: protocol.SessionUp, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
831+ {name: "down", state: protocol.SessionDown, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1}},
832+ {name: "broken", state: protocol.SessionBroken, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2}},
833833 }
834834 for _, tt := range tests {
835835 t.Run(tt.name, func(t *testing.T) {
836- payload := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: tt.state}}})
836+ payload := MarshalSessions(protocol.Sessions{Sessions: []protocol.SessionInfo{{State: tt.state}}})
837837 require.Equal(t, tt.want, payload)
838838 got, err := UnmarshalSessions(payload)
839839 require.NoError(t, err)
@@ -853,19 +853,19 @@ func TestSessionInfoRecoveryState(t *testing.T) {
853853 func TestSessionsGoldenAndRoundTrip(t *testing.T) {
854854 tests := []struct {
855855 name string
856- msg Sessions
856+ msg protocol.Sessions
857857 want []byte
858858 }{
859859 {
860860 name: "empty",
861- msg: Sessions{},
861+ msg: protocol.Sessions{},
862862 want: []byte{0x00, 0x00},
863863 },
864864 {
865865 name: "two",
866- msg: Sessions{Sessions: []SessionInfo{
867- {SessionID: "0", Name: "0", State: SessionUp, Ephemeral: true, Tabs: 1, Attached: false},
868- {SessionID: "work", Name: "proj", State: SessionDown, Ephemeral: false, Tabs: 5, Attached: true},
866+ msg: protocol.Sessions{Sessions: []protocol.SessionInfo{
867+ {SessionID: "0", Name: "0", State: protocol.SessionUp, Ephemeral: true, Tabs: 1, Attached: false},
868+ {SessionID: "work", Name: "proj", State: protocol.SessionDown, Ephemeral: false, Tabs: 5, Attached: true},
869869 }},
870870 want: []byte{
871871 0x00, 0x02,
@@ -907,10 +907,10 @@ func TestSessionsGoldenAndRoundTrip(t *testing.T) {
907907 func TestMsgTypeConstantsDistinct(t *testing.T) {
908908 t.Run("intents", func(t *testing.T) {
909909 vals := map[uint8]string{
910- IntentEphemeral: "IntentEphemeral",
911- IntentNew: "IntentNew",
912- IntentAttach: "IntentAttach",
913- IntentResume: "IntentResume",
910+ protocol.IntentEphemeral: "IntentEphemeral",
911+ protocol.IntentNew: "IntentNew",
912+ protocol.IntentAttach: "IntentAttach",
913+ protocol.IntentResume: "IntentResume",
914914 }
915915 if len(vals) != 4 {
916916 t.Fatalf("expected 4 distinct intent values, got %d", len(vals))
@@ -919,12 +919,12 @@ func TestMsgTypeConstantsDistinct(t *testing.T) {
919919
920920 t.Run("error codes", func(t *testing.T) {
921921 vals := map[uint16]string{
922- ErrVersionMismatch: "ErrVersionMismatch",
923- ErrNoSuchSession: "ErrNoSuchSession",
924- ErrNameTaken: "ErrNameTaken",
925- ErrServerShutdown: "ErrServerShutdown",
926- ErrInvalidSessionName: "ErrInvalidSessionName",
927- ErrInternal: "ErrInternal",
922+ protocol.ErrVersionMismatch: "ErrVersionMismatch",
923+ protocol.ErrNoSuchSession: "ErrNoSuchSession",
924+ protocol.ErrNameTaken: "ErrNameTaken",
925+ protocol.ErrServerShutdown: "ErrServerShutdown",
926+ protocol.ErrInvalidSessionName: "ErrInvalidSessionName",
927+ protocol.ErrInternal: "ErrInternal",
928928 }
929929 if len(vals) != 6 {
930930 t.Fatalf("expected 6 distinct error codes, got %d", len(vals))
@@ -933,10 +933,10 @@ func TestMsgTypeConstantsDistinct(t *testing.T) {
933933
934934 t.Run("detached reasons", func(t *testing.T) {
935935 vals := map[uint8]string{
936- ReasonDetach: "ReasonDetach",
937- ReasonSessionKilled: "ReasonSessionKilled",
938- ReasonServerShutdown: "ReasonServerShutdown",
939- ReasonReplaced: "ReasonReplaced",
936+ protocol.ReasonDetach: "ReasonDetach",
937+ protocol.ReasonSessionKilled: "ReasonSessionKilled",
938+ protocol.ReasonServerShutdown: "ReasonServerShutdown",
939+ protocol.ReasonReplaced: "ReasonReplaced",
940940 }
941941 if len(vals) != 4 {
942942 t.Fatalf("expected 4 distinct detached reasons, got %d", len(vals))
@@ -947,7 +947,7 @@ func TestMsgTypeConstantsDistinct(t *testing.T) {
947947 func TestHelloOutputWindowByteExactValues(t *testing.T) {
948948 for _, window := range []uint8{0, 1, 8} {
949949 t.Run(fmt.Sprintf("window_%d", window), func(t *testing.T) {
950- hello := Hello{Version: 14, Size: domain.Size{Cols: 1, Rows: 1}, MaxOutputInFlight: window}
950+ hello := protocol.Hello{Version: 14, Size: domain.Size{Cols: 1, Rows: 1}, MaxOutputInFlight: window}
951951 // The empty Hello has a fixed 1x1 size and zero pixel geometry before the negotiated output window.
952952 want := append(make([]byte, 42), window, 0, 0, 0, 0, 0, 0)
953953 want = appendNoNavigationTail(want)
@@ -973,12 +973,12 @@ func requireBytesEqual(t *testing.T, want, got []byte) {
973973 func TestCommandCorrelationGoldenAndStrict(t *testing.T) {
974974 tests := []struct {
975975 name string
976- msg CommandRequest
976+ msg protocol.CommandRequest
977977 want []byte
978978 }{
979979 {
980980 name: "attached",
981- msg: CommandRequest{Version: ProtocolVersion, RequestID: 0x0102030405060708, Attached: true, Self: true, Slug: "split-right", Args: []string{"--vertical"}},
981+ msg: protocol.CommandRequest{Version: protocol.Version, RequestID: 0x0102030405060708, Attached: true, Self: true, Slug: "split-right", Args: []string{"--vertical"}},
982982 want: []byte{
983983 0x00, 0x25, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x01,
984984 0x00, 0x0b, 's', 'p', 'l', 'i', 't', '-', 'r', 'i', 'g', 'h', 't',
@@ -988,7 +988,7 @@ func TestCommandCorrelationGoldenAndStrict(t *testing.T) {
988988 },
989989 {
990990 name: "control",
991- msg: CommandRequest{Version: ProtocolVersion, Slug: "ls"},
991+ msg: protocol.CommandRequest{Version: protocol.Version, Slug: "ls"},
992992 want: []byte{
993993 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
994994 0x00, 0x02, 'l', 's', 0x00, 0x00,
@@ -1020,7 +1020,7 @@ func TestCommandCorrelationGoldenAndStrict(t *testing.T) {
10201020 }
10211021
10221022 func TestCommandResultCorrelationGoldenAndStrict(t *testing.T) {
1023- msg := CommandResult{RequestID: 0x0102030405060708, OK: true, Code: 7, Text: "ok", Output: "result"}
1023+ msg := protocol.CommandResult{RequestID: 0x0102030405060708, OK: true, Code: 7, Text: "ok", Output: "result"}
10241024 want := []byte{
10251025 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
10261026 0x01, 0x00, 0x07, 0x00, 0x02, 'o', 'k', 0x00, 0x00, 0x00, 0x06, 'r', 'e', 's', 'u', 'l', 't',
@@ -1040,10 +1040,10 @@ func TestCommandResultCorrelationGoldenAndStrict(t *testing.T) {
10401040 }
10411041
10421042 func TestOutputResetRequestStrict(t *testing.T) {
1043- got := MarshalOutputResetRequest(OutputResetRequest{})
1043+ got := MarshalOutputResetRequest(protocol.OutputResetRequest{})
10441044 require.Empty(t, got)
10451045 back, err := UnmarshalOutputResetRequest(got)
10461046 require.NoError(t, err)
1047- require.Equal(t, OutputResetRequest{}, back)
1047+ require.Equal(t, protocol.OutputResetRequest{}, back)
10481048 assertTrailingGarbageFails(t, got, UnmarshalOutputResetRequest)
10491049 }
A internal/protocol/command.go +25 -0
@@ -0,0 +1,25 @@
1+package protocol
2+
3+// CommandRequest asks the daemon to run one control command. Version must stay
4+// first so a future payload layout can still be rejected cleanly.
5+type CommandRequest struct {
6+ Version uint16
7+ RequestID uint64
8+ Attached bool
9+ Self bool
10+ Slug string
11+ Args []string
12+ TargetSession string
13+ TargetTab string
14+ TargetPane string
15+ JSON bool
16+}
17+
18+// CommandResult reports a control command's outcome.
19+type CommandResult struct {
20+ RequestID uint64
21+ OK bool
22+ Code uint16
23+ Text string
24+ Output string
25+}
M internal/protocol/navigation.go +10 -0
@@ -86,6 +86,16 @@ func ValidateNavigation(capabilities NavigationCapabilities, overlay StartupOver
8686 return nil
8787 }
8888
89+func validateHelloNavigation(h Hello) error {
90+ if h.Intent != IntentAttach && h.Intent != IntentResume {
91+ if h.NavigationCapabilities != 0 || h.StartupOverlay != StartupOverlayNone {
92+ return ErrInvalidNavigation
93+ }
94+ return nil
95+ }
96+ return ValidateNavigation(h.NavigationCapabilities, h.StartupOverlay, h.RemoteTarget != nil || h.EnvironmentPolicy == EnvironmentPolicyDaemonOwned)
97+}
98+
8999 func ValidateParkedRouteRequest(request ParkedRouteRequest) error {
90100 if request.RequestID == 0 || request.LeaseID.IsZero() {
91101 return ErrInvalidNavigation
M internal/protocol/session.go +304 -4
@@ -6,22 +6,122 @@ import (
66 "errors"
77 "fmt"
88 "math"
9+ "time"
910
1011 renderer "github.com/bnema/vev-vt"
1112 "github.com/bnema/vev/internal/domain"
1213 )
1314
15+// Version is the negotiated vev session protocol version.
16+const Version uint16 = 37
17+
18+// HandshakeTimeout bounds every transport handshake from connect through the
19+// first committed publication.
20+const HandshakeTimeout = 15 * time.Second
21+
1422 // MaxOutputDataLen is the largest decoded terminal byte stream accepted by one
1523 // output publication in protocol version 37.
1624 const MaxOutputDataLen = (16 << 20) - 55
1725
26+// ConnectionCapabilities describes transport behavior relevant to session
27+// flow without naming a concrete carriage.
28+type ConnectionCapabilities struct {
29+ OutputDataLimit int
30+ PreferredOutputWindow uint8
31+ AsyncSend bool
32+ OwnedSynchronousSend bool
33+ LinkState bool
34+}
35+
1836 var (
19- ErrInvalidSize = errors.New("ports: invalid terminal size")
20- ErrInvalidOutput = errors.New("ports: invalid output")
21- ErrInvalidAck = errors.New("ports: invalid ack")
22- ErrInvalidNavigation = errors.New("ports: invalid navigation")
37+ ErrInvalidSize = errors.New("ports: invalid terminal size")
38+ ErrInvalidHello = errors.New("ports: invalid hello")
39+ ErrInvalidOutput = errors.New("ports: invalid output")
40+ ErrInvalidAck = errors.New("ports: invalid ack")
41+ ErrInvalidAttachTarget = errors.New("ports: invalid attach target")
42+ ErrInvalidNavigation = errors.New("ports: invalid navigation")
43+ ErrInvalidEnvironmentPolicy = errors.New("ports: invalid environment policy")
44+ ErrInvalidClientNotice = errors.New("ports: invalid client notice")
45+ ErrInvalidDetached = errors.New("ports: invalid detached reason")
46+)
47+
48+const (
49+ IntentEphemeral uint8 = 0
50+ IntentNew uint8 = 1
51+ IntentAttach uint8 = 2
52+ IntentResume uint8 = 3
53+)
54+
55+type EnvironmentPolicy uint8
56+
57+const (
58+ EnvironmentPolicyClientOwned EnvironmentPolicy = iota
59+ EnvironmentPolicyDaemonOwned
60+)
61+
62+const (
63+ CapabilityResume uint32 = 1 << 0
64+ CapabilityUDP uint32 = 1 << 1
65+ CapabilityPredict uint32 = 1 << 2
66+)
67+
68+const (
69+ ErrVersionMismatch uint16 = 1
70+ ErrNoSuchSession uint16 = 2
71+ ErrNameTaken uint16 = 3
72+ ErrServerShutdown uint16 = 4
73+ ErrInvalidSessionName uint16 = 5
74+ ErrUnknownCommand uint16 = 6
75+ ErrNotScriptable uint16 = 7
76+ ErrInvalidCommandArgs uint16 = 8
77+ ErrNoSuchTarget uint16 = 9
78+ ErrAmbiguousTarget uint16 = 10
79+ ErrInternal uint16 = 255
2380 )
2481
82+const (
83+ ReasonDetach uint8 = 0
84+ ReasonSessionKilled uint8 = 1
85+ ReasonServerShutdown uint8 = 2
86+ ReasonReplaced uint8 = 3
87+)
88+
89+// Hello is sent by the client immediately after connecting.
90+type Hello struct {
91+ Version uint16
92+ Intent uint8
93+ ClientID [16]byte
94+ ResumeToken uint64
95+ Name string
96+ Size domain.Size
97+ PixelWidth int
98+ PixelHeight int
99+ TermEnv string
100+ Cwd string
101+ TrueColor bool
102+ // KittyDirectGraphics is an explicit declaration that the client has
103+ // probed its direct outer terminal and it accepts Kitty graphics output.
104+ KittyDirectGraphics bool
105+ MaxOutputInFlight uint8
106+ Env []string
107+ RemoteTarget *domain.RemoteSessionTarget
108+ ExactTarget *ExactSessionTarget
109+ PreferredTabID domain.TabStableID
110+ EnvironmentPolicy EnvironmentPolicy
111+ NavigationCapabilities NavigationCapabilities
112+ StartupOverlay StartupOverlay
113+ Remote bool
114+}
115+
116+func (h Hello) Geometry() domain.Geometry {
117+ return domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}.NormalizePixels()
118+}
119+
120+type Input struct {
121+ InputSeq uint64
122+ Data []byte
123+}
124+
25125 type Resize struct {
26126 Size domain.Size
27127 PixelWidth int
@@ -50,11 +150,38 @@ type ImagePush struct {
50150 Data []byte
51151 }
52152
153+type ClientNotice struct{ Action uint8 }
154+
155+const (
156+ ClientNoticeClipboardFallback uint8 = 1
157+ ClientNoticeClipboardTooLarge uint8 = 2
158+ ClientNoticeLinkDegraded uint8 = 3
159+ ClientNoticeLinkConnected uint8 = 4
160+)
161+
162+type Detach struct{}
163+type Ping struct{}
164+type Pong struct{}
165+
53166 type Ack struct {
54167 Epoch uint64
55168 State uint64
56169 }
57170
171+type Welcome struct {
172+ SessionID string
173+ SessionName string
174+ Ephemeral bool
175+ ResumeToken uint64
176+ Capabilities uint32
177+ CommittedIdentity *CommittedRouteIdentity
178+}
179+
180+type ErrorMsg struct {
181+ Code uint16
182+ Text string
183+}
184+
58185 type Output struct {
59186 Epoch uint64
60187 Base uint64
@@ -66,6 +193,145 @@ type Output struct {
66193 Data []byte
67194 }
68195
196+type AttachTarget struct {
197+ Endpoint string
198+ Session string
199+ Intent uint8
200+ RemoteTarget *domain.RemoteSessionTarget
201+ ExactTarget *ExactSessionTarget
202+ EnvironmentPolicy EnvironmentPolicy
203+ SamePeer bool
204+}
205+
206+type Detached struct{ Reason uint8 }
207+type List struct{}
208+
209+type Kill struct {
210+ Name string
211+ All bool
212+}
213+
214+type SessionState uint8
215+
216+const (
217+ SessionUp SessionState = iota
218+ SessionDown
219+ SessionBroken
220+)
221+
222+type SessionInfo struct {
223+ SessionID string
224+ Name string
225+ State SessionState
226+ Ephemeral bool
227+ Tabs uint16
228+ Attached bool
229+}
230+
231+type Sessions struct{ Sessions []SessionInfo }
232+type OutputResetRequest struct{}
233+
234+func validEnvironmentPolicy(policy EnvironmentPolicy) bool {
235+ return policy == EnvironmentPolicyClientOwned || policy == EnvironmentPolicyDaemonOwned
236+}
237+
238+func ValidateEnvironmentPolicy(policy EnvironmentPolicy) error {
239+ if !validEnvironmentPolicy(policy) {
240+ return ErrInvalidEnvironmentPolicy
241+ }
242+ return nil
243+}
244+
245+func validClientNoticeAction(action uint8) bool {
246+ switch action {
247+ case ClientNoticeClipboardFallback, ClientNoticeClipboardTooLarge, ClientNoticeLinkDegraded, ClientNoticeLinkConnected:
248+ return true
249+ default:
250+ return false
251+ }
252+}
253+
254+func validDetachedReason(reason uint8) bool {
255+ switch reason {
256+ case ReasonDetach, ReasonSessionKilled, ReasonServerShutdown, ReasonReplaced:
257+ return true
258+ default:
259+ return false
260+ }
261+}
262+
263+func ValidateClientNotice(m ClientNotice) error {
264+ if !validClientNoticeAction(m.Action) {
265+ return ErrInvalidClientNotice
266+ }
267+ return nil
268+}
269+
270+func ValidateDetached(m Detached) error {
271+ if !validDetachedReason(m.Reason) {
272+ return ErrInvalidDetached
273+ }
274+ return nil
275+}
276+
277+func ValidateHello(h Hello) error {
278+ if h.Intent != IntentEphemeral && h.Intent != IntentNew && h.Intent != IntentAttach && h.Intent != IntentResume {
279+ return ErrInvalidHello
280+ }
281+ if err := ValidateGeometry(domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}); err != nil {
282+ return fmt.Errorf("%w: geometry", ErrInvalidHello)
283+ }
284+ if !validEnvironmentPolicy(h.EnvironmentPolicy) {
285+ return ErrInvalidHello
286+ }
287+ if err := validateHelloNavigation(h); err != nil {
288+ return fmt.Errorf("%w: navigation: %w", ErrInvalidHello, err)
289+ }
290+ if len(h.Name) > math.MaxUint16 || len(h.TermEnv) > math.MaxUint16 || len(h.Cwd) > math.MaxUint16 || len(h.Env) > math.MaxUint32 {
291+ return ErrInvalidHello
292+ }
293+ for _, entry := range h.Env {
294+ if uint64(len(entry)) > math.MaxUint32 {
295+ return ErrInvalidHello
296+ }
297+ }
298+ if h.PreferredTabID != "" {
299+ if h.Intent != IntentAttach && h.Intent != IntentResume {
300+ return ErrInvalidHello
301+ }
302+ if err := domain.ValidateTabStableID(h.PreferredTabID); err != nil {
303+ return fmt.Errorf("%w: preferred tab: %v", ErrInvalidHello, err)
304+ }
305+ }
306+ if h.ExactTarget != nil {
307+ if h.Intent != IntentAttach && h.Intent != IntentResume {
308+ return ErrInvalidHello
309+ }
310+ if err := h.ExactTarget.Validate(); err != nil {
311+ return fmt.Errorf("%w: exact target: %v", ErrInvalidHello, err)
312+ }
313+ if h.Name != h.ExactTarget.SessionName {
314+ return ErrInvalidHello
315+ }
316+ if h.RemoteTarget != nil && (h.RemoteTarget.LifecycleID != h.ExactTarget.LifecycleID || h.RemoteTarget.SessionName != h.ExactTarget.SessionName) {
317+ return ErrInvalidHello
318+ }
319+ }
320+ if h.RemoteTarget == nil {
321+ return nil
322+ }
323+ if h.EnvironmentPolicy != EnvironmentPolicyDaemonOwned || (h.Intent != IntentAttach && h.Intent != IntentResume) {
324+ return ErrInvalidHello
325+ }
326+ if err := validateRemoteTarget(*h.RemoteTarget); err != nil {
327+ return fmt.Errorf("%w: remote target: %v", ErrInvalidHello, err)
328+ }
329+ if h.Name != h.RemoteTarget.SessionName {
330+ return ErrInvalidHello
331+ }
332+ return nil
333+}
334+
69335 func ValidateOutput(m Output) error {
70336 if m.Epoch == 0 {
71337 return ErrInvalidOutput
@@ -93,6 +359,40 @@ func ValidateAck(m Ack) error {
93359 return nil
94360 }
95361
362+func ValidateAttachTarget(m AttachTarget) error {
363+ if m.Session == "" || len(m.Endpoint) > math.MaxUint16 || len(m.Session) > math.MaxUint16 {
364+ return ErrInvalidAttachTarget
365+ }
366+ if err := domain.ValidateSessionName(m.Session); err != nil {
367+ return ErrInvalidAttachTarget
368+ }
369+ if m.Intent != IntentEphemeral && m.Intent != IntentNew && m.Intent != IntentAttach && m.Intent != IntentResume {
370+ return ErrInvalidAttachTarget
371+ }
372+ if m.Intent == IntentResume || !validEnvironmentPolicy(m.EnvironmentPolicy) {
373+ return ErrInvalidAttachTarget
374+ }
375+ if m.ExactTarget != nil && (m.ExactTarget.SessionName != m.Session || m.ExactTarget.Validate() != nil) {
376+ return ErrInvalidAttachTarget
377+ }
378+ if m.SamePeer && (m.Endpoint != "" || m.RemoteTarget != nil || m.ExactTarget == nil) {
379+ return ErrInvalidAttachTarget
380+ }
381+ if m.RemoteTarget == nil {
382+ return nil
383+ }
384+ if m.EnvironmentPolicy != EnvironmentPolicyDaemonOwned || m.Intent != IntentAttach {
385+ return ErrInvalidAttachTarget
386+ }
387+ if err := validateRemoteTarget(*m.RemoteTarget); err != nil {
388+ return fmt.Errorf("%w: remote target: %v", ErrInvalidAttachTarget, err)
389+ }
390+ if m.Endpoint != m.RemoteTarget.Endpoint || m.Session != m.RemoteTarget.SessionName {
391+ return ErrInvalidAttachTarget
392+ }
393+ return nil
394+}
395+
96396 func validateRemoteTarget(target domain.RemoteSessionTarget) error {
97397 if len(target.Endpoint) > math.MaxUint16 || len(target.DisplayOrigin) > math.MaxUint16 || len(target.SessionName) > math.MaxUint16 || len(target.LiveTabID) > math.MaxUint16 || len(target.StoppedTab.RawName) > math.MaxUint16 {
98398 return errors.New("remote target string too long")
M internal/protocol/session_test.go +36 -1
@@ -9,6 +9,40 @@ import (
99 "github.com/stretchr/testify/require"
1010 )
1111
12+func TestVersionRemainsWireCompatible(t *testing.T) {
13+ require.Equal(t, uint16(37), Version)
14+ require.Equal(t, (16<<20)-55, MaxOutputDataLen)
15+}
16+
17+func TestHelloSemanticValidation(t *testing.T) {
18+ base := Hello{Version: Version, Intent: IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}}
19+ tests := []struct {
20+ name string
21+ mutate func(*Hello)
22+ want error
23+ }{
24+ {name: "valid", mutate: func(*Hello) {}},
25+ {name: "unknown intent", mutate: func(h *Hello) { h.Intent = 99 }, want: ErrInvalidHello},
26+ {name: "invalid geometry", mutate: func(h *Hello) { h.Size.Rows = 0 }, want: ErrInvalidHello},
27+ {name: "unsafe navigation", mutate: func(h *Hello) { h.NavigationCapabilities = NavigationCapabilityBack }, want: ErrInvalidHello},
28+ {name: "exact target name mismatch", mutate: func(h *Hello) {
29+ h.ExactTarget = &ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "other"}
30+ }, want: ErrInvalidHello},
31+ }
32+ for _, tt := range tests {
33+ t.Run(tt.name, func(t *testing.T) {
34+ hello := base
35+ tt.mutate(&hello)
36+ err := ValidateHello(hello)
37+ if tt.want == nil {
38+ require.NoError(t, err)
39+ return
40+ }
41+ require.ErrorIs(t, err, tt.want)
42+ })
43+ }
44+}
45+
1246 func TestOutputSemanticValidation(t *testing.T) {
1347 valid := Output{Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}}
1448 require.NoError(t, ValidateOutput(valid))
@@ -43,7 +77,8 @@ func TestRouteAndPreviewSemanticValidation(t *testing.T) {
4377 require.ErrorIs(t, ValidateRemotePreview(preview), ErrInvalidRemotePreview)
4478 }
4579
46-func TestPresentationErrorsPreserveClassification(t *testing.T) {
80+func TestSemanticErrorsPreserveClassification(t *testing.T) {
4781 require.True(t, errors.Is(ValidateAck(Ack{}), ErrInvalidAck))
82+ require.True(t, errors.Is(ValidateAttachTarget(AttachTarget{}), ErrInvalidAttachTarget))
4883 require.True(t, errors.Is(ValidateParkedRouteRequest(ParkedRouteRequest{}), ErrInvalidNavigation))
4984 }
M internal/usecase/client/client.go +41 -41
@@ -133,7 +133,7 @@ const sendQueueDepth = 64
133133
134134 // preWelcomeTimeout remains the focused-test name for the shared handshake
135135 // budget, which now also covers connect and initial publication.
136-const preWelcomeTimeout = ports.HandshakeTimeout
136+const preWelcomeTimeout = protocol.HandshakeTimeout
137137
138138 // remoteHostLearnerShutdownTimeout bounds how long Run waits for async remote
139139 // host learning after terminal restoration. Learning is best-effort; a stalled
@@ -151,7 +151,7 @@ const (
151151 )
152152
153153 // Dependencies supplies the collaborators required by a Runner.
154-type AttachHandoffFunc func(ports.AttachTarget) (ports.Dialer, AttachRequest, error)
154+type AttachHandoffFunc func(protocol.AttachTarget) (ports.Dialer, AttachRequest, error)
155155
156156 type Dependencies struct {
157157 Dialer ports.Dialer
@@ -189,7 +189,7 @@ type AttachRequest struct {
189189 ExactTarget *protocol.ExactSessionTarget
190190 PreferredTabID domain.TabStableID
191191 HostLabel string
192- EnvironmentPolicy ports.EnvironmentPolicy
192+ EnvironmentPolicy protocol.EnvironmentPolicy
193193 NavigationCapabilities protocol.NavigationCapabilities
194194 StartupOverlay protocol.StartupOverlay
195195 }
@@ -379,14 +379,14 @@ func validateAttachRequest(request AttachRequest) error {
379379 if err := (SessionTarget{Intent: request.Intent, SessionName: request.SessionName}).validate(); err != nil {
380380 return fmt.Errorf("vev: invalid session target: %w", err)
381381 }
382- if err := protocol.ValidateNavigation(request.NavigationCapabilities, request.StartupOverlay, request.RemoteTarget != nil || request.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned); err != nil {
382+ if err := protocol.ValidateNavigation(request.NavigationCapabilities, request.StartupOverlay, request.RemoteTarget != nil || request.EnvironmentPolicy == protocol.EnvironmentPolicyDaemonOwned); err != nil {
383383 return fmt.Errorf("vev: invalid navigation route: %w", err)
384384 }
385385 if request.RemoteTarget == nil {
386- if request.EnvironmentPolicy == ports.EnvironmentPolicyClientOwned {
386+ if request.EnvironmentPolicy == protocol.EnvironmentPolicyClientOwned {
387387 return nil
388388 }
389- if request.Remote && request.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned {
389+ if request.Remote && request.EnvironmentPolicy == protocol.EnvironmentPolicyDaemonOwned {
390390 return nil
391391 }
392392 return errors.New("vev: attach without remote target requires a matching environment policy")
@@ -397,10 +397,10 @@ func validateAttachRequest(request AttachRequest) error {
397397 if request.SessionName != request.RemoteTarget.SessionName {
398398 return errors.New("vev: remote attach target session mismatch")
399399 }
400- if request.EnvironmentPolicy != ports.EnvironmentPolicyDaemonOwned {
400+ if request.EnvironmentPolicy != protocol.EnvironmentPolicyDaemonOwned {
401401 return errors.New("vev: remote attach target requires daemon-owned environment")
402402 }
403- if request.Intent != ports.IntentAttach && request.Intent != ports.IntentResume {
403+ if request.Intent != protocol.IntentAttach && request.Intent != protocol.IntentResume {
404404 return errors.New("vev: remote attach target requires attach or resume")
405405 }
406406 return nil
@@ -510,9 +510,9 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
510510 dialer = route.dialer
511511 attemptRequest = route.request
512512 if route.resumeToken != 0 {
513- attemptRequest.Intent = ports.IntentResume
513+ attemptRequest.Intent = protocol.IntentResume
514514 } else {
515- attemptRequest.Intent = ports.IntentAttach
515+ attemptRequest.Intent = protocol.IntentAttach
516516 }
517517 attemptRequest.StartupOverlay = protocol.StartupOverlayNone
518518 attemptRequest.NavigationCapabilities &^= protocol.NavigationCapabilityBack | protocol.NavigationCapabilityHomePicker
@@ -524,11 +524,11 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
524524 enterHomePicker := func() {
525525 dialer = homeRoute.dialer
526526 attemptRequest = homeRoute.request
527- attemptRequest.Intent = ports.IntentAttach
527+ attemptRequest.Intent = protocol.IntentAttach
528528 attemptRequest.NavigationCapabilities = protocol.NavigationCapabilityBack
529529 attemptRequest.StartupOverlay = protocol.StartupOverlaySessionPicker
530530 attemptRequest.RemoteTarget = nil
531- attemptRequest.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned
531+ attemptRequest.EnvironmentPolicy = protocol.EnvironmentPolicyClientOwned
532532 attemptRequest.Remote = homeRoute.request.Remote || r.remote
533533 resumeToken = 0
534534 remote = syncReconnectRemote(reconnect, homeRoute.request.Remote || r.remote)
@@ -556,11 +556,11 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
556556 return attachResult{err: errors.New("vev: home route unavailable")}
557557 }
558558 request := cloneAttachRequest(homeRoute.request)
559- request.Intent = ports.IntentAttach
559+ request.Intent = protocol.IntentAttach
560560 request.NavigationCapabilities = protocol.NavigationCapabilityBack
561561 request.StartupOverlay = protocol.StartupOverlaySessionPicker
562562 request.RemoteTarget = nil
563- request.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned
563+ request.EnvironmentPolicy = protocol.EnvironmentPolicyClientOwned
564564 request.Remote = false
565565 if err := validateAttachRequest(request); err != nil {
566566 return attachResult{err: err}
@@ -627,7 +627,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
627627 return ctx.Err()
628628 }
629629 backoff = nextReconnectBackoff(backoff, defaultReconnectBackoff.max)
630- attemptRequest.Intent = ports.IntentResume
630+ attemptRequest.Intent = protocol.IntentResume
631631 continue
632632 }
633633 ms.dialed = true
@@ -733,9 +733,9 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
733733 dialer = selection.selected.dialer
734734 attemptRequest = cloneAttachRequest(selection.selected.request)
735735 if selection.selected.resumeToken != 0 {
736- attemptRequest.Intent = ports.IntentResume
736+ attemptRequest.Intent = protocol.IntentResume
737737 } else {
738- attemptRequest.Intent = ports.IntentAttach
738+ attemptRequest.Intent = protocol.IntentAttach
739739 }
740740 // Back is transient to the picker overlay. Re-derive the home-picker
741741 // capability from the selected route instead of trusting request
@@ -795,7 +795,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
795795 if result.sessionName != "" {
796796 routeRequest.SessionName = result.sessionName
797797 }
798- routeRequest.Intent = ports.IntentAttach
798+ routeRequest.Intent = protocol.IntentAttach
799799 routeRequest.NavigationCapabilities = 0
800800 routeRequest.StartupOverlay = protocol.StartupOverlayNone
801801 homeRoute = &attachRoute{dialer: dialer, request: routeRequest}
@@ -832,7 +832,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
832832 if result.target.ExactTarget != nil {
833833 nextRequest.ExactTarget = result.target.ExactTarget
834834 }
835- if homeRoute != nil && (nextRequest.RemoteTarget != nil || nextRequest.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned) {
835+ if homeRoute != nil && (nextRequest.RemoteTarget != nil || nextRequest.EnvironmentPolicy == protocol.EnvironmentPolicyDaemonOwned) {
836836 nextRequest.NavigationCapabilities |= protocol.NavigationCapabilityHomePicker
837837 }
838838 if attemptRequest.StartupOverlay == protocol.StartupOverlaySessionPicker {
@@ -875,7 +875,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
875875 continue
876876 }
877877 if routeNavigationPending && routeNavigationResumeFallback && resumeNeedsExactAttach(result.err) {
878- attemptRequest.Intent = ports.IntentAttach
878+ attemptRequest.Intent = protocol.IntentAttach
879879 resumeToken = 0
880880 routeNavigationResumeFallback = false
881881 continue
@@ -896,7 +896,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
896896 continue
897897 }
898898 if returnResumeFallback && resumeNeedsExactAttach(result.err) {
899- attemptRequest.Intent = ports.IntentAttach
899+ attemptRequest.Intent = protocol.IntentAttach
900900 resumeToken = 0
901901 returnResumeFallback = false
902902 continue
@@ -928,7 +928,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
928928 return ctx.Err()
929929 }
930930 backoff = nextReconnectBackoff(backoff, defaultReconnectBackoff.max)
931- attemptRequest.Intent = ports.IntentResume
931+ attemptRequest.Intent = protocol.IntentResume
932932 }
933933 }
934934
@@ -1089,7 +1089,7 @@ type attachResult struct {
10891089 routePosition *protocol.RoutePosition
10901090 welcomed bool
10911091 transportClosed bool
1092- target *ports.AttachTarget
1092+ target *protocol.AttachTarget
10931093 action protocol.NavigationAction
10941094 homePickerTarget bool
10951095 routeAction *protocol.RouteNavigationAction
@@ -1099,7 +1099,7 @@ type attachResult struct {
10991099 // AttachTargetError requests that the composition root replace the current
11001100 // transport with the selected endpoint. The daemon never opens that endpoint.
11011101 type AttachTargetError struct {
1102- Target ports.AttachTarget
1102+ Target protocol.AttachTarget
11031103 }
11041104
11051105 func (e *AttachTargetError) Error() string {
@@ -1219,8 +1219,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
12191219 SessionName: request.RemoteTarget.SessionName,
12201220 }
12211221 }
1222- hello := ports.Hello{
1223- Version: ports.ProtocolVersion,
1222+ hello := protocol.Hello{
1223+ Version: protocol.Version,
12241224 Intent: intent,
12251225 ClientID: clientID,
12261226 ResumeToken: resumeToken,
@@ -1490,7 +1490,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
14901490 requestID uint64
14911491 action protocol.ParkedRouteAction
14921492 leaseID protocol.ParkedRouteLeaseID
1493- fallbackTarget *ports.AttachTarget
1493+ fallbackTarget *protocol.AttachTarget
14941494 timer ports.Timer
14951495 }
14961496 var parkedPending *parkedRoutePending
@@ -1600,7 +1600,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
16001600 awaitParkedFull := func() {
16011601 clearParkedFull()
16021602 parkedWaitingFull = true
1603- parkedFullTimer = clk.NewTimer(ports.HandshakeTimeout)
1603+ parkedFullTimer = clk.NewTimer(protocol.HandshakeTimeout)
16041604 }
16051605 parkedFullC := func() <-chan time.Time {
16061606 if parkedFullTimer == nil {
@@ -1654,7 +1654,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
16541654 case controlCh <- ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: payload}:
16551655 parkedPending = &parkedRoutePending{
16561656 requestID: request.RequestID, action: action, leaseID: leaseID,
1657- timer: clk.NewTimer(ports.HandshakeTimeout),
1657+ timer: clk.NewTimer(protocol.HandshakeTimeout),
16581658 }
16591659 return nil
16601660 case <-loopCtx.Done():
@@ -1827,7 +1827,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
18271827 return welcomedResult(fmt.Errorf("dismissing reconnect toast: %w", err))
18281828 }
18291829 select {
1830- case sendCh <- ports.Frame{Type: ports.MsgClientNotice, Payload: ports.MarshalClientNotice(ports.ClientNotice{Action: ports.ClientNoticeLinkConnected})}:
1830+ case sendCh <- ports.Frame{Type: ports.MsgClientNotice, Payload: ports.MarshalClientNotice(protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected})}:
18311831 case <-loopCtx.Done():
18321832 return welcomedResult(nil)
18331833 }
@@ -1836,7 +1836,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
18361836 if ev.State == ports.LinkStateDegraded {
18371837 log.Warn("UDP link degraded")
18381838 select {
1839- case sendCh <- ports.Frame{Type: ports.MsgClientNotice, Payload: ports.MarshalClientNotice(ports.ClientNotice{Action: ports.ClientNoticeLinkDegraded})}:
1839+ case sendCh <- ports.Frame{Type: ports.MsgClientNotice, Payload: ports.MarshalClientNotice(protocol.ClientNotice{Action: protocol.ClientNoticeLinkDegraded})}:
18401840 case <-loopCtx.Done():
18411841 return welcomedResult(nil)
18421842 }
@@ -1896,7 +1896,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
18961896 // repeated gaps while that reset is in flight.
18971897 if !outputResetRequested {
18981898 select {
1899- case sendCh <- ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{})}:
1899+ case sendCh <- ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(protocol.OutputResetRequest{})}:
19001900 outputResetRequested = true
19011901 case <-loopCtx.Done():
19021902 return loopCanceledResult()
@@ -2111,7 +2111,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
21112111 transitionWaitingFull = true
21122112 if !outputResetRequested {
21132113 select {
2114- case sendCh <- ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{})}:
2114+ case sendCh <- ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(protocol.OutputResetRequest{})}:
21152115 outputResetRequested = true
21162116 case <-loopCtx.Done():
21172117 return loopCanceledResult()
@@ -2145,7 +2145,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
21452145 return welcomedResult(fmt.Errorf("vev: decoding detached: %w", derr))
21462146 }
21472147 ms.detached = true
2148- if d.Reason == ports.ReasonDetach {
2148+ if d.Reason == protocol.ReasonDetach {
21492149 log.Info("detached cleanly", "reason", d.Reason)
21502150 } else {
21512151 log.Warn("detached by daemon", "reason", d.Reason)
@@ -2785,7 +2785,7 @@ func (p *stdinPump) run() {
27852785 return
27862786 }
27872787 inputSeq++
2788- if !send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{InputSeq: inputSeq, Data: append([]byte(nil), data...)})}) {
2788+ if !send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{InputSeq: inputSeq, Data: append([]byte(nil), data...)})}) {
27892789 undeliveredMu.Lock()
27902790 undelivered = append(undelivered, data...)
27912791 undeliveredMu.Unlock()
@@ -2799,7 +2799,7 @@ func (p *stdinPump) run() {
27992799 reader: p.clipboard,
28002800 log: p.logger,
28012801 sendNotice: func(action uint8) {
2802- send(ports.Frame{Type: ports.MsgClientNotice, Payload: ports.MarshalClientNotice(ports.ClientNotice{Action: action})})
2802+ send(ports.Frame{Type: ports.MsgClientNotice, Payload: ports.MarshalClientNotice(protocol.ClientNotice{Action: action})})
28032803 },
28042804 sendImage: func(mime string, data []byte) {
28052805 inputSeq++
@@ -2945,7 +2945,7 @@ func (p *stdinPump) run() {
29452945 disarmMarkerDeadline()
29462946 markers.flush(sink)
29472947 select {
2948- case p.out <- ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(ports.Detach{})}:
2948+ case p.out <- ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(protocol.Detach{})}:
29492949 case <-p.ctx.Done():
29502950 }
29512951 p.cancel()
@@ -3002,13 +3002,13 @@ func runResize(ctx context.Context, events <-chan domain.Geometry, out chan<- po
30023002 // the user learns why the session went away.
30033003 func detachedResult(reason uint8) error {
30043004 switch reason {
3005- case ports.ReasonDetach:
3005+ case protocol.ReasonDetach:
30063006 return nil
3007- case ports.ReasonSessionKilled:
3007+ case protocol.ReasonSessionKilled:
30083008 return &DetachedError{Reason: reason, Text: "session was killed"}
3009- case ports.ReasonServerShutdown:
3009+ case protocol.ReasonServerShutdown:
30103010 return &DetachedError{Reason: reason, Text: "daemon shut down"}
3011- case ports.ReasonReplaced:
3011+ case protocol.ReasonReplaced:
30123012 return &DetachedError{Reason: reason, Text: "session taken over by another client"}
30133013 default:
30143014 return &DetachedError{Reason: reason, Text: "detached by daemon"}
M internal/usecase/client/client_test.go +153 -153
@@ -265,7 +265,7 @@ func attachTestDependencies(transport ports.Transport, terminal ports.Terminal,
265265 return testDependencies(transportDialer{transport: transport}, terminal, clock, nil, nil)
266266 }
267267
268-const testPreWelcomeTimeout = ports.HandshakeTimeout
268+const testPreWelcomeTimeout = protocol.HandshakeTimeout
269269
270270 func newHandshakeClock(t *testing.T, timerCount int) (*portsmocks.MockClock, <-chan chan time.Time) {
271271 t.Helper()
@@ -294,7 +294,7 @@ func TestRunBoundsBlockedDial(t *testing.T) {
294294
295295 errCh := make(chan error, 1)
296296 go func() {
297- errCh <- runTestClient(context.Background(), testDependencies(dialer, nil, clk, nil, nil), client.AttachRequest{Intent: ports.IntentAttach, SessionName: "main"})
297+ errCh <- runTestClient(context.Background(), testDependencies(dialer, nil, clk, nil, nil), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "main"})
298298 }()
299299 timerC := <-createdTimers
300300 <-started
@@ -353,7 +353,7 @@ func TestRunBoundsPreWelcomeOperations(t *testing.T) {
353353
354354 errCh := make(chan error, 1)
355355 go func() {
356- errCh <- runTestClient(ctx, testDependencies(dialer, term, clk, nil, nil), client.AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: false})
356+ errCh <- runTestClient(ctx, testDependencies(dialer, term, clk, nil, nil), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: false})
357357 }()
358358
359359 var timerC chan time.Time
@@ -399,7 +399,7 @@ func TestAttachHelloIncludesTrueColor(t *testing.T) {
399399 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)
400400 defer in.unblock()
401401
402- gotHello := make(chan ports.Hello, 1)
402+ gotHello := make(chan protocol.Hello, 1)
403403 tr := portsmocks.NewMockTransport(t)
404404 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
405405 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
@@ -410,13 +410,13 @@ func TestAttachHelloIncludesTrueColor(t *testing.T) {
410410 return nil
411411 }).Once()
412412 unblock := scriptRecv(tr,
413- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
414- recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
413+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
414+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
415415 )
416416 defer unblock()
417417 tr.EXPECT().Close().Return(nil).Once()
418418
419- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""})
419+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: ""})
420420 require.NoError(t, err)
421421
422422 select {
@@ -434,18 +434,18 @@ func TestAttachPublishesCommittedRouteSnapshot(t *testing.T) {
434434 defer term.in.unblock()
435435 lifecycle := domain.SessionLifecycleID{9, 8, 7}
436436 tr := &recordingTransport{recvs: []recvItem{
437- {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
437+ {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{
438438 SessionID: "s1",
439439 SessionName: "main",
440440 CommittedIdentity: &protocol.CommittedRouteIdentity{
441441 Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "main"},
442442 },
443443 }))},
444- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
444+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
445445 }}
446446
447447 err := runTestClient(context.Background(), attachTestDependencies(tr, term, realClock{}), client.AttachRequest{
448- Intent: ports.IntentAttach,
448+ Intent: protocol.IntentAttach,
449449 SessionName: "main",
450450 Origin: protocol.RouteOriginLocal,
451451 OriginKey: "local",
@@ -485,8 +485,8 @@ func TestLocalOnlyHandoffBetweenLocalSessionsKeepsClientRunning(t *testing.T) {
485485 firstLifecycle := domain.SessionLifecycleID{1}
486486 secondLifecycle := domain.SessionLifecycleID{2}
487487 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {
488- return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
489- SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,
488+ return frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{
489+ SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: protocol.CapabilityResume,
490490 CommittedIdentity: &protocol.CommittedRouteIdentity{
491491 Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
492492 },
@@ -495,19 +495,19 @@ func TestLocalOnlyHandoffBetweenLocalSessionsKeepsClientRunning(t *testing.T) {
495495 secondTarget := protocol.ExactSessionTarget{LifecycleID: secondLifecycle, SessionName: "second"}
496496 first := &recordingTransport{recvs: []recvItem{
497497 {f: welcome("first", firstLifecycle)},
498- {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{
499- Session: "second", Intent: ports.IntentAttach, ExactTarget: &secondTarget,
500- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, SamePeer: true,
498+ {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(protocol.AttachTarget{
499+ Session: "second", Intent: protocol.IntentAttach, ExactTarget: &secondTarget,
500+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, SamePeer: true,
501501 }))},
502502 {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{Target: secondTarget}))},
503- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
503+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
504504 }}
505505 dialer := &sequenceDialer{trs: []ports.Transport{first}}
506506
507507 err := runTestClient(context.Background(), testDependencies(dialer, term, realClock{}, nil, nil), client.AttachRequest{
508- Intent: ports.IntentAttach, SessionName: "first",
508+ Intent: protocol.IntentAttach, SessionName: "first",
509509 Origin: protocol.RouteOriginLocal, OriginKey: "local",
510- EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
510+ EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
511511 })
512512
513513 require.NoError(t, err)
@@ -523,14 +523,14 @@ func TestAttachHelloPreservesCompleteAttachRequest(t *testing.T) {
523523 SessionName: "work", LiveTabID: "tab-1",
524524 }
525525 request := client.AttachRequest{
526- Intent: ports.IntentAttach, SessionName: "work", Remote: true,
527- RemoteTarget: target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
526+ Intent: protocol.IntentAttach, SessionName: "work", Remote: true,
527+ RemoteTarget: target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
528528 NavigationCapabilities: protocol.NavigationCapabilityHomePicker,
529529 StartupOverlay: protocol.StartupOverlayNone,
530530 }
531531 tr := &recordingTransport{recvs: []recvItem{
532532 {f: welcomeFrame(0)},
533- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
533+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
534534 }}
535535
536536 require.NoError(t, runTestClient(context.Background(), attachTestDependencies(tr, term, realClock{}), request))
@@ -551,8 +551,8 @@ func TestStoppedLocalHandoffDialsReplacementTransport(t *testing.T) {
551551 targetLifecycle := domain.SessionLifecycleID{2}
552552 target := protocol.ExactSessionTarget{LifecycleID: targetLifecycle, SessionName: "stopped"}
553553 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {
554- return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
555- SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,
554+ return frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{
555+ SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: protocol.CapabilityResume,
556556 CommittedIdentity: &protocol.CommittedRouteIdentity{
557557 Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
558558 },
@@ -560,21 +560,21 @@ func TestStoppedLocalHandoffDialsReplacementTransport(t *testing.T) {
560560 }
561561 first := &recordingTransport{recvs: []recvItem{
562562 {f: welcome("source", sourceLifecycle)},
563- {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{
564- Session: "stopped", Intent: ports.IntentAttach, ExactTarget: &target,
565- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
563+ {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(protocol.AttachTarget{
564+ Session: "stopped", Intent: protocol.IntentAttach, ExactTarget: &target,
565+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
566566 }))},
567567 }}
568568 second := &recordingTransport{recvs: []recvItem{
569569 {f: welcome("stopped", targetLifecycle)},
570- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
570+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
571571 }}
572572 dialer := &sequenceDialer{trs: []ports.Transport{first, second}}
573573
574574 err := runTestClient(context.Background(), testDependencies(dialer, term, realClock{}, nil, nil), client.AttachRequest{
575- Intent: ports.IntentAttach, SessionName: "source",
575+ Intent: protocol.IntentAttach, SessionName: "source",
576576 Origin: protocol.RouteOriginLocal, OriginKey: "local",
577- EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
577+ EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
578578 })
579579
580580 require.NoError(t, err)
@@ -588,8 +588,8 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin
588588 localLifecycle := domain.SessionLifecycleID{1}
589589 remoteLifecycle := domain.SessionLifecycleID{2}
590590 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {
591- return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
592- SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,
591+ return frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{
592+ SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: protocol.CapabilityResume,
593593 CommittedIdentity: &protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name}},
594594 }))
595595 }
@@ -599,9 +599,9 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin
599599 }
600600 local1 := &recordingTransport{recvs: []recvItem{
601601 {f: welcome("local", localLifecycle)},
602- {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{
603- Endpoint: "remote", Session: "work", Intent: ports.IntentAttach,
604- RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
602+ {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(protocol.AttachTarget{
603+ Endpoint: "remote", Session: "work", Intent: protocol.IntentAttach,
604+ RemoteTarget: &remoteTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
605605 }))},
606606 }}
607607 remote := &recordingTransport{recvs: []recvItem{
@@ -610,31 +610,31 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin
610610 }}
611611 local2 := &recordingTransport{recvs: []recvItem{
612612 {f: welcome("local", localLifecycle)},
613- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
613+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
614614 }}
615615 localDialer := &sequenceDialer{trs: []ports.Transport{local1, local2}}
616616 remoteDialer := &sequenceDialer{trs: []ports.Transport{remote}}
617617 deps := testDependencies(localDialer, term, realClock{}, nil, nil)
618- deps.AttachHandoff = func(ports.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
618+ deps.AttachHandoff = func(protocol.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
619619 target := protocol.ExactSessionTarget{LifecycleID: remoteLifecycle, SessionName: "work"}
620620 return remoteDialer, client.AttachRequest{
621- Intent: ports.IntentAttach, SessionName: "work", Remote: true,
621+ Intent: protocol.IntentAttach, SessionName: "work", Remote: true,
622622 Origin: protocol.RouteOriginRemote, OriginKey: "remote", ExactTarget: &target,
623- EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
623+ EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
624624 // Deliberately omit the copied Home capability. The accepted server
625625 // action remains valid because Runner retained the concrete home route.
626626 }, nil
627627 }
628628
629629 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
630- Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
630+ Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
631631 }))
632632 require.Equal(t, protocol.StartupOverlaySessionPicker, helloFromSend(t, local2).StartupOverlay)
633633 }
634634
635635 func hybridWelcomeFrame(name string, lifecycle domain.SessionLifecycleID) ports.Frame {
636- return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
637- SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,
636+ return frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{
637+ SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: protocol.CapabilityResume,
638638 CommittedIdentity: &protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name}},
639639 }))
640640 }
@@ -642,16 +642,16 @@ func hybridWelcomeFrame(name string, lifecycle domain.SessionLifecycleID) ports.
642642 func hybridLocalBootstrap(lifecycle domain.SessionLifecycleID, target domain.RemoteSessionTarget) *recordingTransport {
643643 return &recordingTransport{recvs: []recvItem{
644644 {f: hybridWelcomeFrame("local", lifecycle)},
645- {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{
646- Endpoint: target.Endpoint, Session: target.SessionName, Intent: ports.IntentAttach,
647- RemoteTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
645+ {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(protocol.AttachTarget{
646+ Endpoint: target.Endpoint, Session: target.SessionName, Intent: protocol.IntentAttach,
647+ RemoteTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
648648 }))},
649649 }}
650650 }
651651
652652 func hybridPickerDependencies(localDialer ports.Dialer, term ports.Terminal, clock ports.Clock, remoteByEndpoint map[string]ports.Dialer) client.Dependencies {
653653 deps := testDependencies(localDialer, term, clock, nil, nil)
654- deps.AttachHandoff = func(target ports.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
654+ deps.AttachHandoff = func(target protocol.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
655655 if target.RemoteTarget == nil {
656656 return nil, client.AttachRequest{}, errors.New("hybrid picker target is not remote")
657657 }
@@ -661,9 +661,9 @@ func hybridPickerDependencies(localDialer ports.Dialer, term ports.Terminal, clo
661661 }
662662 selection := *target.RemoteTarget
663663 return dialer, client.AttachRequest{
664- Intent: ports.IntentAttach, SessionName: target.Session, Remote: true,
664+ Intent: protocol.IntentAttach, SessionName: target.Session, Remote: true,
665665 Origin: protocol.RouteOriginDiscovery, OriginKey: target.Endpoint,
666- RemoteTarget: &selection, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
666+ RemoteTarget: &selection, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
667667 }, nil
668668 }
669669 return deps
@@ -721,7 +721,7 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
721721 {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: remoteTargetLifecycle, SessionName: "target"}})), wait: switchSent},
722722 {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteSwitched}))},
723723 {f: frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 2, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("target frame")}))},
724- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
724+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
725725 }}
726726 remote.onSend = hybridParkedRequestHandler(parkedRequests, parkedRequestErrors, map[protocol.ParkedRouteAction]chan struct{}{
727727 protocol.ParkedRoutePrepare: parkSent,
@@ -729,9 +729,9 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
729729 })
730730 localPicker := &recordingTransport{recvs: []recvItem{
731731 {f: hybridWelcomeFrame("local", localLifecycle)},
732- {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{
733- Endpoint: "remote", Session: "target", Intent: ports.IntentAttach,
734- RemoteTarget: &targetTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
732+ {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(protocol.AttachTarget{
733+ Endpoint: "remote", Session: "target", Intent: protocol.IntentAttach,
734+ RemoteTarget: &targetTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
735735 }))},
736736 }}
737737 var resizeOnce sync.Once
@@ -745,7 +745,7 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
745745 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})
746746
747747 err := runTestClient(context.Background(), deps, client.AttachRequest{
748- Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
748+ Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
749749 })
750750
751751 require.NoError(t, err)
@@ -812,14 +812,14 @@ func TestHybridPickerExpiredSwitchFallsBackToNewDial(t *testing.T) {
812812 })
813813 localPicker := &recordingTransport{recvs: []recvItem{
814814 {f: hybridWelcomeFrame("local", localLifecycle)},
815- {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{
816- Endpoint: "remote", Session: "target", Intent: ports.IntentAttach,
817- RemoteTarget: &targetTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
815+ {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(protocol.AttachTarget{
816+ Endpoint: "remote", Session: "target", Intent: protocol.IntentAttach,
817+ RemoteTarget: &targetTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
818818 }))},
819819 }}
820820 targetRemote := &recordingTransport{recvs: []recvItem{
821821 {f: hybridWelcomeFrame("target", targetLifecycle)},
822- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
822+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
823823 }}
824824 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}
825825 remoteDialer := &sequenceDialer{trs: []ports.Transport{
@@ -828,7 +828,7 @@ func TestHybridPickerExpiredSwitchFallsBackToNewDial(t *testing.T) {
828828 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})
829829
830830 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
831- Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
831+ Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
832832 }))
833833 select {
834834 case requestErr := <-parkedRequestErrors:
@@ -855,7 +855,7 @@ func TestHybridPickerPrepareResponseTimeoutClosesRetainedTransport(t *testing.T)
855855 ch := make(chan time.Time, 1)
856856 timer.EXPECT().C().Return(ch).Maybe()
857857 timer.EXPECT().Reset(mock.Anything).Return(true).Maybe()
858- if duration == ports.HandshakeTimeout {
858+ if duration == protocol.HandshakeTimeout {
859859 record := &deadlineTimer{ch: ch}
860860 timer.EXPECT().Stop().Run(func() { record.stopped.Store(true) }).Return(true).Maybe()
861861 deadlineMu.Lock()
@@ -900,7 +900,7 @@ func TestHybridPickerPrepareResponseTimeoutClosesRetainedTransport(t *testing.T)
900900 result := make(chan error, 1)
901901 go func() {
902902 result <- runTestClient(ctx, deps, client.AttachRequest{
903- Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
903+ Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
904904 })
905905 }()
906906 select {
@@ -954,7 +954,7 @@ func TestHybridPickerBackResumesRetainedRemoteTransport(t *testing.T) {
954954 {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady})), wait: parkSent},
955955 {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteResumed})), wait: resumeSent},
956956 {f: frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("source frame")}))},
957- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
957+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
958958 }}
959959 remote.onSend = hybridParkedRequestHandler(nil, parkedRequestErrors, map[protocol.ParkedRouteAction]chan struct{}{
960960 protocol.ParkedRoutePrepare: parkSent,
@@ -969,7 +969,7 @@ func TestHybridPickerBackResumesRetainedRemoteTransport(t *testing.T) {
969969 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})
970970
971971 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
972- Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
972+ Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
973973 }))
974974 select {
975975 case requestErr := <-parkedRequestErrors:
@@ -1010,14 +1010,14 @@ func TestHybridPickerDifferentHostFallsBackToNewRemoteDial(t *testing.T) {
10101010 })
10111011 localPicker := &recordingTransport{recvs: []recvItem{
10121012 {f: hybridWelcomeFrame("local", localLifecycle)},
1013- {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{
1014- Endpoint: targetTarget.Endpoint, Session: targetTarget.SessionName, Intent: ports.IntentAttach,
1015- RemoteTarget: &targetTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
1013+ {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(protocol.AttachTarget{
1014+ Endpoint: targetTarget.Endpoint, Session: targetTarget.SessionName, Intent: protocol.IntentAttach,
1015+ RemoteTarget: &targetTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
10161016 }))},
10171017 }}
10181018 targetRemote := &recordingTransport{recvs: []recvItem{
10191019 {f: hybridWelcomeFrame("target", targetLifecycle)},
1020- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
1020+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
10211021 }}
10221022 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}
10231023 sourceDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: sourceRemote}}}
@@ -1028,7 +1028,7 @@ func TestHybridPickerDifferentHostFallsBackToNewRemoteDial(t *testing.T) {
10281028 })
10291029
10301030 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
1031- Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
1031+ Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
10321032 }))
10331033 select {
10341034 case requestErr := <-parkedRequestErrors:
@@ -1049,11 +1049,11 @@ func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T)
10491049 newLifecycle := domain.SessionLifecycleID{3, 3, 3}
10501050 remoteNewLifecycle := domain.SessionLifecycleID{4, 4, 4}
10511051 welcome := func(name string, lifecycle domain.SessionLifecycleID, token uint64) ports.Frame {
1052- return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
1052+ return frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{
10531053 SessionID: name + "-id",
10541054 SessionName: name,
10551055 ResumeToken: token,
1056- Capabilities: ports.CapabilityResume,
1056+ Capabilities: protocol.CapabilityResume,
10571057 CommittedIdentity: &protocol.CommittedRouteIdentity{
10581058 Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
10591059 },
@@ -1066,9 +1066,9 @@ func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T)
10661066 SessionName: "remote-manual",
10671067 LiveTabID: "tab-1",
10681068 }
1069- remoteHandoff := ports.AttachTarget{
1070- Endpoint: "remote", Session: "remote-manual", Intent: ports.IntentAttach,
1071- RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
1069+ remoteHandoff := protocol.AttachTarget{
1070+ Endpoint: "remote", Session: "remote-manual", Intent: protocol.IntentAttach,
1071+ RemoteTarget: &remoteTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
10721072 }
10731073
10741074 local1 := &recordingTransport{recvs: []recvItem{
@@ -1102,23 +1102,23 @@ func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T)
11021102 }}
11031103 remote3 := &recordingTransport{recvs: []recvItem{
11041104 {f: welcome("remote-new", remoteNewLifecycle, 22)},
1105- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
1105+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
11061106 }}
11071107 localDialer := &sequenceDialer{trs: []ports.Transport{local1, local2, local3}}
11081108 remoteDialer := &sequenceDialer{trs: []ports.Transport{remote1, remote2, remote3}}
11091109
11101110 deps := testDependencies(localDialer, term, realClock{}, nil, nil)
1111- deps.AttachHandoff = func(target ports.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
1111+ deps.AttachHandoff = func(target protocol.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
11121112 require.Equal(t, remoteHandoff, target)
11131113 return remoteDialer, client.AttachRequest{
1114- Intent: ports.IntentAttach, SessionName: target.Session, Remote: true,
1114+ Intent: protocol.IntentAttach, SessionName: target.Session, Remote: true,
11151115 Origin: protocol.RouteOriginDiscovery, OriginKey: target.Endpoint,
1116- RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
1116+ RemoteTarget: &remoteTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
11171117 }, nil
11181118 }
11191119
11201120 err := runTestClient(context.Background(), deps, client.AttachRequest{
1121- Intent: ports.IntentAttach, SessionName: "test", Origin: protocol.RouteOriginLocal, OriginKey: "local",
1121+ Intent: protocol.IntentAttach, SessionName: "test", Origin: protocol.RouteOriginLocal, OriginKey: "local",
11221122 })
11231123 remoteHello := helloFromSend(t, remote2)
11241124 require.NoError(t, err)
@@ -1168,18 +1168,18 @@ func TestAttachTargetHandoffReturnsValidatedTargetAndClosesTransport(t *testing.
11681168 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)
11691169 defer in.unblock()
11701170
1171- target := ports.AttachTarget{Endpoint: "remote.example", Session: "work", Intent: ports.IntentAttach}
1171+ target := protocol.AttachTarget{Endpoint: "remote.example", Session: "work", Intent: protocol.IntentAttach}
11721172 tr := portsmocks.NewMockTransport(t)
11731173 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
11741174 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
11751175 unblock := scriptRecv(tr,
1176- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "local"}))},
1176+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "local"}))},
11771177 recvItem{f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(target))},
11781178 )
11791179 defer unblock()
11801180 tr.EXPECT().Close().Return(nil).Once()
11811181
1182- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work"})
1182+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work"})
11831183 var handoff *client.AttachTargetError
11841184 require.ErrorAs(t, err, &handoff)
11851185 require.Equal(t, target, handoff.Target)
@@ -1195,7 +1195,7 @@ func TestAttachHelloIncludesCompleteLocalEnvironment(t *testing.T) {
11951195 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)
11961196 defer in.unblock()
11971197
1198- gotHello := make(chan ports.Hello, 1)
1198+ gotHello := make(chan protocol.Hello, 1)
11991199 tr := portsmocks.NewMockTransport(t)
12001200 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
12011201 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
@@ -1208,13 +1208,13 @@ func TestAttachHelloIncludesCompleteLocalEnvironment(t *testing.T) {
12081208 return nil
12091209 }).Once()
12101210 unblock := scriptRecv(tr,
1211- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
1212- recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
1211+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
1212+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
12131213 )
12141214 defer unblock()
12151215 tr.EXPECT().Close().Return(nil).Once()
12161216
1217- require.NoError(t, runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral}))
1217+ require.NoError(t, runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral}))
12181218 require.Equal(t, os.Environ(), (<-gotHello).Env)
12191219 }
12201220
@@ -1235,13 +1235,13 @@ func TestAttachHelloRequestsSingleOutputForDatagramTransport(t *testing.T) {
12351235 return nil
12361236 }).Once()
12371237 unblock := scriptRecv(tr,
1238- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
1239- recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
1238+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
1239+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
12401240 )
12411241 defer unblock()
12421242 tr.EXPECT().Close().Return(nil).Once()
12431243
1244- require.NoError(t, runTestClient(context.Background(), attachTestDependencies(markedDatagramTransport{tr}, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral}))
1244+ require.NoError(t, runTestClient(context.Background(), attachTestDependencies(markedDatagramTransport{tr}, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral}))
12451245 }
12461246
12471247 func TestAttachHappyPath(t *testing.T) {
@@ -1256,14 +1256,14 @@ func TestAttachHappyPath(t *testing.T) {
12561256 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
12571257 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
12581258 unblock := scriptRecv(tr,
1259- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1", SessionName: "main"}))},
1259+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1", SessionName: "main"}))},
12601260 recvItem{f: frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Data: []byte("hello world")}))},
1261- recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
1261+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
12621262 )
12631263 defer unblock()
12641264 tr.EXPECT().Close().Return(nil).Once()
12651265
1266- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""})
1266+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: ""})
12671267 require.NoError(t, err)
12681268 require.Equal(t, "\x1b]10;?\x07\x1b]11;?\x07\x1b]4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?\x07\x1b[?2031$phello world", out.String())
12691269 require.Equal(t, int32(1), restoreCount.Load(), "restore must run exactly once")
@@ -1277,16 +1277,16 @@ func TestAttachVersionMismatch(t *testing.T) {
12771277 tr := portsmocks.NewMockTransport(t)
12781278 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
12791279 tr.EXPECT().Recv().Return(
1280- frameOf(ports.MsgError, ports.MarshalErrorMsg(ports.ErrorMsg{Code: ports.ErrVersionMismatch, Text: "version mismatch"})),
1280+ frameOf(ports.MsgError, ports.MarshalErrorMsg(protocol.ErrorMsg{Code: protocol.ErrVersionMismatch, Text: "version mismatch"})),
12811281 nil,
12821282 ).Once()
12831283 tr.EXPECT().Close().Return(nil).Once()
12841284
1285- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentAttach, SessionName: "main"})
1285+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "main"})
12861286 require.Error(t, err)
12871287 var pe *client.ProtocolError
12881288 require.True(t, errors.As(err, &pe), "want *client.ProtocolError, got %T", err)
1289- require.Equal(t, ports.ErrVersionMismatch, pe.Code)
1289+ require.Equal(t, protocol.ErrVersionMismatch, pe.Code)
12901290 tr.AssertNotCalled(t, "Send", isType(ports.MsgTheme))
12911291 }
12921292
@@ -1303,13 +1303,13 @@ func TestAttachRestoredOnRecvErrorMidStream(t *testing.T) {
13031303 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
13041304 boom := errors.New("connection reset")
13051305 unblock := scriptRecv(tr,
1306- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
1306+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
13071307 recvItem{err: boom},
13081308 )
13091309 defer unblock()
13101310 tr.EXPECT().Close().Return(nil).Once()
13111311
1312- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""})
1312+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: ""})
13131313 require.Error(t, err)
13141314 require.Equal(t, int32(1), restoreCount.Load(), "restore must run after mid-stream Recv error")
13151315 }
@@ -1326,13 +1326,13 @@ func TestAttachDaemonVanishedOnEOF(t *testing.T) {
13261326 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
13271327 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
13281328 unblock := scriptRecv(tr,
1329- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
1329+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
13301330 recvItem{err: io.EOF},
13311331 )
13321332 defer unblock()
13331333 tr.EXPECT().Close().Return(nil).Once()
13341334
1335- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""})
1335+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: ""})
13361336 require.Error(t, err)
13371337 require.ErrorIs(t, err, io.EOF)
13381338 require.Equal(t, int32(1), restoreCount.Load())
@@ -1366,8 +1366,8 @@ func TestAttachStdinForwardsSGRMouseReportAsSingleFrame(t *testing.T) {
13661366 close(allowDetach)
13671367 return nil
13681368 }).Once()
1369- welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))
1370- detached := frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))
1369+ welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))
1370+ detached := frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))
13711371 recvCh := make(chan recvItem, 1)
13721372 recvCh <- recvItem{f: welcome}
13731373 closed := make(chan struct{})
@@ -1389,7 +1389,7 @@ func TestAttachStdinForwardsSGRMouseReportAsSingleFrame(t *testing.T) {
13891389 }).Maybe()
13901390 tr.EXPECT().Close().Return(nil).Once()
13911391
1392- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""})
1392+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: ""})
13931393 require.NoError(t, err)
13941394 select {
13951395 case got := <-gotInput:
@@ -1428,8 +1428,8 @@ func TestAttachStdinCoalescesSplitBracketedPaste(t *testing.T) {
14281428 close(allowDetach)
14291429 return nil
14301430 }).Once()
1431- welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))
1432- detached := frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))
1431+ welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))
1432+ detached := frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))
14331433 recvCh := make(chan recvItem, 1)
14341434 recvCh <- recvItem{f: welcome}
14351435 closed := make(chan struct{})
@@ -1451,7 +1451,7 @@ func TestAttachStdinCoalescesSplitBracketedPaste(t *testing.T) {
14511451 }).Maybe()
14521452 tr.EXPECT().Close().Return(nil).Once()
14531453
1454- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""})
1454+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: ""})
14551455 require.NoError(t, err)
14561456 select {
14571457 case got := <-gotInput:
@@ -1485,8 +1485,8 @@ func TestAttachForwardsResize(t *testing.T) {
14851485 return nil
14861486 }).Once()
14871487
1488- welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))
1489- detached := frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))
1488+ welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))
1489+ detached := frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))
14901490 recvCh := make(chan recvItem, 2)
14911491 recvCh <- recvItem{f: welcome}
14921492 done := make(chan struct{})
@@ -1516,7 +1516,7 @@ func TestAttachForwardsResize(t *testing.T) {
15161516 resizeCh <- domain.Geometry{Size: domain.Size{Cols: 120, Rows: 40}}
15171517 }()
15181518
1519- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""})
1519+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: ""})
15201520 require.NoError(t, err)
15211521 select {
15221522 case r := <-gotResize:
@@ -1539,7 +1539,7 @@ func TestRunRestoresRawModeAfterAttachError(t *testing.T) {
15391539 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
15401540 boom := errors.New("connection reset")
15411541 unblock := scriptRecv(tr,
1542- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
1542+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
15431543 recvItem{err: boom},
15441544 )
15451545 defer unblock()
@@ -1547,7 +1547,7 @@ func TestRunRestoresRawModeAfterAttachError(t *testing.T) {
15471547 d := portsmocks.NewMockDialer(t)
15481548 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
15491549
1550- err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: "", Remote: false})
1550+ err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: "", Remote: false})
15511551 require.Error(t, err)
15521552 require.Equal(t, int32(1), restoreCount.Load(), "Run must restore raw mode after attach attempt errors")
15531553 }
@@ -1560,14 +1560,14 @@ func TestRunDoesNotEnterRawBeforePreWelcomeError(t *testing.T) {
15601560 tr := portsmocks.NewMockTransport(t)
15611561 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
15621562 tr.EXPECT().Recv().Return(
1563- frameOf(ports.MsgError, ports.MarshalErrorMsg(ports.ErrorMsg{Code: ports.ErrVersionMismatch, Text: "version mismatch"})),
1563+ frameOf(ports.MsgError, ports.MarshalErrorMsg(protocol.ErrorMsg{Code: protocol.ErrVersionMismatch, Text: "version mismatch"})),
15641564 nil,
15651565 ).Once()
15661566 tr.EXPECT().Close().Return(nil).Once()
15671567 d := portsmocks.NewMockDialer(t)
15681568 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
15691569
1570- err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: false})
1570+ err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: false})
15711571 require.Error(t, err)
15721572 var pe *client.ProtocolError
15731573 require.True(t, errors.As(err, &pe), "want *client.ProtocolError, got %T", err)
@@ -1580,7 +1580,7 @@ func TestRunPhaseASingleAttempt(t *testing.T) {
15801580 d.EXPECT().Dial(mock.Anything).Return(nil, dialErr).Once()
15811581 tm := portsmocks.NewMockTerminal(t)
15821582
1583- err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: "", Remote: false})
1583+ err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: "", Remote: false})
15841584 require.ErrorIs(t, err, dialErr)
15851585 }
15861586
@@ -1688,7 +1688,7 @@ func (t *runTerminal) In() io.Reader { return t.in }
16881688 func (t *runTerminal) Out() io.Writer { return &t.out }
16891689 func (t *runTerminal) Flush() error { return nil }
16901690
1691-func helloFromSend(t *testing.T, tr *recordingTransport) ports.Hello {
1691+func helloFromSend(t *testing.T, tr *recordingTransport) protocol.Hello {
16921692 t.Helper()
16931693 sends := tr.Sends()
16941694 require.NotEmpty(t, sends)
@@ -1699,7 +1699,7 @@ func helloFromSend(t *testing.T, tr *recordingTransport) ports.Hello {
16991699 }
17001700
17011701 func welcomeFrame(token uint64) ports.Frame {
1702- return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1", SessionName: "main", ResumeToken: token, Capabilities: ports.CapabilityResume}))
1702+ return frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1", SessionName: "main", ResumeToken: token, Capabilities: protocol.CapabilityResume}))
17031703 }
17041704
17051705 func TestRunReconnectsWithRotatedTokenAndSameClientID(t *testing.T) {
@@ -1707,12 +1707,12 @@ func TestRunReconnectsWithRotatedTokenAndSameClientID(t *testing.T) {
17071707 defer term.in.unblock()
17081708 tr1 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(11)}, {err: io.EOF}}}
17091709 tr2 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(22)}, {err: io.EOF}}}
1710- tr3 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(33)}, {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))}}}
1710+ tr3 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(33)}, {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))}}}
17111711 d := &sequenceDialer{trs: []ports.Transport{tr1, tr2, tr3}}
17121712 clock := &reconnectTestClock{}
17131713 result := make(chan error, 1)
17141714 go func() {
1715- result <- runTestClient(context.Background(), testDependencies(d, term, clock, nil, nil), client.AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: false})
1715+ result <- runTestClient(context.Background(), testDependencies(d, term, clock, nil, nil), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: false})
17161716 }()
17171717 clock.fireReconnect(t)
17181718 clock.fireReconnect(t)
@@ -1724,11 +1724,11 @@ func TestRunReconnectsWithRotatedTokenAndSameClientID(t *testing.T) {
17241724 h1 := helloFromSend(t, tr1)
17251725 h2 := helloFromSend(t, tr2)
17261726 h3 := helloFromSend(t, tr3)
1727- require.Equal(t, ports.IntentAttach, h1.Intent)
1727+ require.Equal(t, protocol.IntentAttach, h1.Intent)
17281728 require.Zero(t, h1.ResumeToken)
1729- require.Equal(t, ports.IntentResume, h2.Intent)
1729+ require.Equal(t, protocol.IntentResume, h2.Intent)
17301730 require.Equal(t, uint64(11), h2.ResumeToken)
1731- require.Equal(t, ports.IntentResume, h3.Intent)
1731+ require.Equal(t, protocol.IntentResume, h3.Intent)
17321732 require.Equal(t, uint64(22), h3.ResumeToken)
17331733 require.Equal(t, h1.ClientID, h2.ClientID)
17341734 require.Equal(t, h1.ClientID, h3.ClientID)
@@ -1742,8 +1742,8 @@ func TestReconnectRestoresLastPublishedRouteTab(t *testing.T) {
17421742 lifecycle := domain.SessionLifecycleID{7, 8, 9}
17431743 target := protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}
17441744 welcome := func(token uint64) ports.Frame {
1745- return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
1746- SessionID: "work", SessionName: "work", ResumeToken: token, Capabilities: ports.CapabilityResume,
1745+ return frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{
1746+ SessionID: "work", SessionName: "work", ResumeToken: token, Capabilities: protocol.CapabilityResume,
17471747 CommittedIdentity: &protocol.CommittedRouteIdentity{Target: target},
17481748 }))
17491749 }
@@ -1754,14 +1754,14 @@ func TestReconnectRestoresLastPublishedRouteTab(t *testing.T) {
17541754 }}
17551755 tr2 := &recordingTransport{recvs: []recvItem{
17561756 {f: welcome(22)},
1757- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
1757+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
17581758 }}
17591759 dialer := &sequenceDialer{trs: []ports.Transport{tr1, tr2}}
17601760 clock := &reconnectTestClock{}
17611761 result := make(chan error, 1)
17621762 go func() {
17631763 result <- runTestClient(context.Background(), testDependencies(dialer, term, clock, nil, nil), client.AttachRequest{
1764- Intent: ports.IntentAttach, SessionName: "work", Origin: protocol.RouteOriginLocal, OriginKey: "local",
1764+ Intent: protocol.IntentAttach, SessionName: "work", Origin: protocol.RouteOriginLocal, OriginKey: "local",
17651765 })
17661766 }()
17671767 clock.fireReconnect(t)
@@ -1781,8 +1781,8 @@ func TestRemoteReconnectDropsStalePickerTargetAfterDaemonSessionSwitch(t *testin
17811781 SessionName: "old", LiveTabID: "tab-1",
17821782 }
17831783 welcome := func(name string, lifecycle domain.SessionLifecycleID, token uint64) ports.Frame {
1784- return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
1785- SessionID: name, SessionName: name, ResumeToken: token, Capabilities: ports.CapabilityResume,
1784+ return frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{
1785+ SessionID: name, SessionName: name, ResumeToken: token, Capabilities: protocol.CapabilityResume,
17861786 CommittedIdentity: &protocol.CommittedRouteIdentity{
17871787 Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
17881788 },
@@ -1797,23 +1797,23 @@ func TestRemoteReconnectDropsStalePickerTargetAfterDaemonSessionSwitch(t *testin
17971797 }}
17981798 tr2 := &recordingTransport{recvs: []recvItem{
17991799 {f: welcome("new", newLifecycle, 22)},
1800- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
1800+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
18011801 }}
18021802 dialer := &sequenceDialer{trs: []ports.Transport{tr1, tr2}}
18031803 clock := &reconnectTestClock{}
18041804 result := make(chan error, 1)
18051805 go func() {
18061806 result <- runTestClient(context.Background(), testDependencies(dialer, term, clock, nil, nil), client.AttachRequest{
1807- Intent: ports.IntentAttach, SessionName: "old", Remote: true,
1807+ Intent: protocol.IntentAttach, SessionName: "old", Remote: true,
18081808 Origin: protocol.RouteOriginDiscovery, OriginKey: "remote",
1809- RemoteTarget: remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
1809+ RemoteTarget: remoteTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
18101810 })
18111811 }()
18121812 clock.fireReconnect(t)
18131813 require.NoError(t, <-result)
18141814
18151815 reconnectHello := helloFromSend(t, tr2)
1816- require.Equal(t, ports.IntentResume, reconnectHello.Intent)
1816+ require.Equal(t, protocol.IntentResume, reconnectHello.Intent)
18171817 require.Equal(t, "new", reconnectHello.Name)
18181818 require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"}, reconnectHello.ExactTarget)
18191819 require.Nil(t, reconnectHello.RemoteTarget)
@@ -1832,35 +1832,35 @@ func TestAttachRememberRemoteHost(t *testing.T) {
18321832 {
18331833 name: "remote success",
18341834 request: client.AttachRequest{
1835- Intent: ports.IntentAttach,
1835+ Intent: protocol.IntentAttach,
18361836 SessionName: "main",
18371837 Remote: true,
18381838 },
18391839 remember: func() error { return nil },
18401840 recv: []recvItem{
1841- {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
1842- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
1841+ {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
1842+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
18431843 },
18441844 wantCalled: true,
18451845 },
18461846 {
18471847 name: "callback failure does not fail attach",
18481848 request: client.AttachRequest{
1849- Intent: ports.IntentAttach,
1849+ Intent: protocol.IntentAttach,
18501850 SessionName: "main",
18511851 Remote: true,
18521852 },
18531853 remember: func() error { return errors.New("disk full") },
18541854 recv: []recvItem{
1855- {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
1856- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
1855+ {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
1856+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
18571857 },
18581858 wantCalled: true,
18591859 },
18601860 {
18611861 name: "local attach does not call callback",
18621862 request: client.AttachRequest{
1863- Intent: ports.IntentAttach,
1863+ Intent: protocol.IntentAttach,
18641864 SessionName: "main",
18651865 Remote: false,
18661866 },
@@ -1869,14 +1869,14 @@ func TestAttachRememberRemoteHost(t *testing.T) {
18691869 return nil
18701870 },
18711871 recv: []recvItem{
1872- {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
1873- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
1872+ {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
1873+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
18741874 },
18751875 },
18761876 {
18771877 name: "rejected before welcome does not call callback",
18781878 request: client.AttachRequest{
1879- Intent: ports.IntentAttach,
1879+ Intent: protocol.IntentAttach,
18801880 SessionName: "main",
18811881 Remote: true,
18821882 },
@@ -1885,7 +1885,7 @@ func TestAttachRememberRemoteHost(t *testing.T) {
18851885 return nil
18861886 },
18871887 recv: []recvItem{
1888- {f: frameOf(ports.MsgError, ports.MarshalErrorMsg(ports.ErrorMsg{Code: ports.ErrVersionMismatch, Text: "version mismatch"}))},
1888+ {f: frameOf(ports.MsgError, ports.MarshalErrorMsg(protocol.ErrorMsg{Code: protocol.ErrVersionMismatch, Text: "version mismatch"}))},
18891889 },
18901890 preWelcomeReject: true,
18911891 wantErr: true,
@@ -1893,13 +1893,13 @@ func TestAttachRememberRemoteHost(t *testing.T) {
18931893 {
18941894 name: "post-welcome error still uses happy terminal fixture",
18951895 request: client.AttachRequest{
1896- Intent: ports.IntentAttach,
1896+ Intent: protocol.IntentAttach,
18971897 SessionName: "main",
18981898 Remote: true,
18991899 },
19001900 remember: func() error { return nil },
19011901 recv: []recvItem{
1902- {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
1902+ {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
19031903 {err: errors.New("connection reset")},
19041904 },
19051905 wantCalled: true,
@@ -1984,7 +1984,7 @@ func TestRunRememberRemoteHostAtMostOnceAcrossReconnects(t *testing.T) {
19841984 learnerDone := make(chan struct{})
19851985
19861986 tr1 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(11)}, {err: io.EOF}}}
1987- tr2 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(22)}, {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))}}}
1987+ tr2 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(22)}, {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))}}}
19881988 d := &sequenceDialer{trs: []ports.Transport{tr1, tr2}}
19891989
19901990 deps := testDependencies(d, term, realClock{}, nil, nil)
@@ -1996,7 +1996,7 @@ func TestRunRememberRemoteHostAtMostOnceAcrossReconnects(t *testing.T) {
19961996 }).Once()
19971997 deps.RemoteHostLearner = learner
19981998 err := runTestClient(context.Background(), deps, client.AttachRequest{
1999- Intent: ports.IntentAttach,
1999+ Intent: protocol.IntentAttach,
20002000 SessionName: "main",
20012001 Remote: true,
20022002 })
@@ -2036,8 +2036,8 @@ func TestAttachRememberRemoteHostDoesNotBlockAfterWelcome(t *testing.T) {
20362036 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
20372037 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
20382038 unblock := scriptRecv(tr,
2039- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
2040- recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
2039+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
2040+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
20412041 )
20422042 defer unblock()
20432043 tr.EXPECT().Close().Return(nil).Once()
@@ -2055,7 +2055,7 @@ func TestAttachRememberRemoteHostDoesNotBlockAfterWelcome(t *testing.T) {
20552055 errCh := make(chan error, 1)
20562056 go func() {
20572057 errCh <- runTestClient(context.Background(), deps, client.AttachRequest{
2058- Intent: ports.IntentAttach,
2058+ Intent: protocol.IntentAttach,
20592059 SessionName: "main",
20602060 Remote: true,
20612061 })
@@ -2117,8 +2117,8 @@ func TestRunRestoresTerminalBeforeWaitingForLearner(t *testing.T) {
21172117 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
21182118 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
21192119 unblock := scriptRecv(tr,
2120- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
2121- recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
2120+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
2121+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
21222122 )
21232123 defer unblock()
21242124 tr.EXPECT().Close().Return(nil).Once()
@@ -2136,7 +2136,7 @@ func TestRunRestoresTerminalBeforeWaitingForLearner(t *testing.T) {
21362136 errCh := make(chan error, 1)
21372137 go func() {
21382138 errCh <- runTestClient(context.Background(), deps, client.AttachRequest{
2139- Intent: ports.IntentAttach,
2139+ Intent: protocol.IntentAttach,
21402140 SessionName: "main",
21412141 Remote: true,
21422142 })
@@ -2214,8 +2214,8 @@ func TestRunReturnsWhenRemoteHostLearnerStalls(t *testing.T) {
22142214 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
22152215 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
22162216 unblock := scriptRecv(tr,
2217- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
2218- recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
2217+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
2218+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
22192219 )
22202220 defer unblock()
22212221 tr.EXPECT().Close().Return(nil).Once()
@@ -2232,7 +2232,7 @@ func TestRunReturnsWhenRemoteHostLearnerStalls(t *testing.T) {
22322232 errCh := make(chan error, 1)
22332233 go func() {
22342234 errCh <- runTestClient(context.Background(), deps, client.AttachRequest{
2235- Intent: ports.IntentAttach,
2235+ Intent: protocol.IntentAttach,
22362236 SessionName: "main",
22372237 Remote: true,
22382238 })
@@ -2267,10 +2267,10 @@ func TestRunReturnsWhenRemoteHostLearnerStalls(t *testing.T) {
22672267 func TestRunDoesNotRetryTerminalDetachedError(t *testing.T) {
22682268 term := newRunTerminal()
22692269 defer term.in.unblock()
2270- tr := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(11)}, {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonSessionKilled}))}}}
2270+ tr := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(11)}, {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonSessionKilled}))}}}
22712271 d := &sequenceDialer{trs: []ports.Transport{tr}}
22722272
2273- err := runTestClient(context.Background(), testDependencies(d, term, realClock{}, nil, nil), client.AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: false})
2273+ err := runTestClient(context.Background(), testDependencies(d, term, realClock{}, nil, nil), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: false})
22742274 require.Error(t, err)
22752275 var de *client.DetachedError
22762276 require.True(t, errors.As(err, &de))
M internal/usecase/client/clipboard_intercept.go +3 -2
@@ -7,6 +7,7 @@ import (
77 "log/slog"
88
99 "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
1011 )
1112
1213 // ctrlV is the byte a Ctrl+V keypress sends on the wire.
@@ -101,7 +102,7 @@ func (c *clipboardIntercept) handleCtrlV() {
101102 if !errors.Is(err, ports.ErrNoClipboardImage) {
102103 c.log.Warn("clipboard image read failed", "err", err)
103104 if c.sendNotice != nil {
104- c.sendNotice(ports.ClientNoticeClipboardFallback)
105+ c.sendNotice(protocol.ClientNoticeClipboardFallback)
105106 }
106107 }
107108 c.next([]byte{ctrlV})
@@ -110,7 +111,7 @@ func (c *clipboardIntercept) handleCtrlV() {
110111 if len(data) > maxClipboardImagePush {
111112 c.log.Warn("clipboard image too large to send, forwarding Ctrl+V instead", "size", len(data), "cap", maxClipboardImagePush)
112113 if c.sendNotice != nil {
113- c.sendNotice(ports.ClientNoticeClipboardTooLarge)
114+ c.sendNotice(protocol.ClientNoticeClipboardTooLarge)
114115 }
115116 c.next([]byte{ctrlV})
116117 return
M internal/usecase/client/clipboard_intercept_internal_test.go +2 -2
@@ -86,8 +86,8 @@ func TestClipboardInterceptFailureNotifiesDaemonAndForwardsCtrlV(t *testing.T) {
8686 reader ports.ClipboardReader
8787 want uint8
8888 }{
89- {name: "image read failure", reader: &ciFakeReader{err: errors.New("boom")}, want: ports.ClientNoticeClipboardFallback},
90- {name: "oversized image", reader: &ciFakeReader{mime: "image/png", data: make([]byte, maxClipboardImagePush+1)}, want: ports.ClientNoticeClipboardTooLarge},
89+ {name: "image read failure", reader: &ciFakeReader{err: errors.New("boom")}, want: protocol.ClientNoticeClipboardFallback},
90+ {name: "oversized image", reader: &ciFakeReader{mime: "image/png", data: make([]byte, maxClipboardImagePush+1)}, want: protocol.ClientNoticeClipboardTooLarge},
9191 }
9292 for _, tt := range tests {
9393 t.Run(tt.name, func(t *testing.T) {
M internal/usecase/client/clipboard_intercept_test.go +7 -7
@@ -64,8 +64,8 @@ func runRemoteClipboardTest(t *testing.T, remote bool, clip ports.ClipboardReade
6464 }).Maybe()
6565 tr.EXPECT().Send(isType(ports.MsgClientNotice)).Return(nil).Maybe()
6666
67- welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))
68- detached := frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))
67+ welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))
68+ detached := frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))
6969 recvCh := make(chan recvItem, 1)
7070 recvCh <- recvItem{f: welcome}
7171 closed := make(chan struct{})
@@ -90,7 +90,7 @@ func runRemoteClipboardTest(t *testing.T, remote bool, clip ports.ClipboardReade
9090 d := portsmocks.NewMockDialer(t)
9191 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
9292
93- err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, clip, nil), client.AttachRequest{Intent: ports.IntentEphemeral, Remote: remote})
93+ err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, clip, nil), client.AttachRequest{Intent: protocol.IntentEphemeral, Remote: remote})
9494 require.NoError(t, err)
9595 return gotInput, gotImage
9696 }
@@ -176,11 +176,11 @@ func TestRunRemoteClipboardFailureNotifiesDaemonAndWritesOutputVerbatim(t *testi
176176 clipboard := portsmocks.NewMockClipboardReader(t)
177177 clipboard.EXPECT().ReadImage(mock.Anything).Return("", nil, errors.New("read failed")).Once()
178178 transport := &clipboardToastLifecycleTransport{recv: make(chan ports.Frame, 8), sends: make(chan ports.Frame, 16)}
179- transport.recv <- frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))
179+ transport.recv <- frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))
180180
181181 result := make(chan error, 1)
182182 go func() {
183- result <- client.NewRunner(client.Dependencies{Dialer: clipboardToastLifecycleDialer{transport: transport}, Terminal: term, Clock: realClock{}, DisableCapabilityProbe: true, Clipboard: clipboard}).Run(context.Background(), client.AttachRequest{Intent: ports.IntentEphemeral, Remote: true})
183+ result <- client.NewRunner(client.Dependencies{Dialer: clipboardToastLifecycleDialer{transport: transport}, Terminal: term, Clock: realClock{}, DisableCapabilityProbe: true, Clipboard: clipboard}).Run(context.Background(), client.AttachRequest{Intent: protocol.IntentEphemeral, Remote: true})
184184 }()
185185
186186 var gotNotice bool
@@ -192,7 +192,7 @@ func TestRunRemoteClipboardFailureNotifiesDaemonAndWritesOutputVerbatim(t *testi
192192 }
193193 notice, err := ports.UnmarshalClientNotice(sent.Payload)
194194 require.NoError(t, err)
195- require.Equal(t, ports.ClientNoticeClipboardFallback, notice.Action)
195+ require.Equal(t, protocol.ClientNoticeClipboardFallback, notice.Action)
196196 gotNotice = true
197197 case <-time.After(time.Second):
198198 t.Fatal("client did not notify daemon of clipboard failure")
@@ -200,7 +200,7 @@ func TestRunRemoteClipboardFailureNotifiesDaemonAndWritesOutputVerbatim(t *testi
200200 }
201201 beforeOutput := out.String()
202202 transport.recv <- frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("incremental")}))
203- transport.recv <- frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))
203+ transport.recv <- frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))
204204
205205 select {
206206 case err := <-result:
M internal/usecase/client/handshake.go +3 -2
@@ -7,6 +7,7 @@ import (
77 "time"
88
99 "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
1011 )
1112
1213 var errHandshakeTimeout = errors.New("handshake timed out")
@@ -33,9 +34,9 @@ func newHandshakeContext(parent context.Context, clock ports.Clock) (context.Con
3334 if clock == nil {
3435 clock = systemClock{}
3536 }
36- timer := clock.NewTimer(ports.HandshakeTimeout)
37+ timer := clock.NewTimer(protocol.HandshakeTimeout)
3738 if timer == nil {
38- timer = systemClock{}.NewTimer(ports.HandshakeTimeout)
39+ timer = systemClock{}.NewTimer(protocol.HandshakeTimeout)
3940 }
4041 stop := make(chan struct{})
4142 done := make(chan struct{})
M internal/usecase/client/navigation_test.go +3 -4
@@ -4,7 +4,6 @@ import (
44 "testing"
55
66 "github.com/bnema/vev/internal/domain"
7- "github.com/bnema/vev/internal/ports"
87 "github.com/bnema/vev/internal/protocol"
98 "github.com/stretchr/testify/require"
109 )
@@ -38,10 +37,10 @@ func TestValidateAttachRequestNavigationTable(t *testing.T) {
3837 valid bool
3938 }{
4039 {name: "ordinary route", request: AttachRequest{}, valid: true},
41- {name: "home picker without remote route", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "work", NavigationCapabilities: protocol.NavigationCapabilityHomePicker}, valid: false},
42- {name: "home picker on daemon-owned remote route", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "work", Remote: true, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, NavigationCapabilities: protocol.NavigationCapabilityHomePicker}, valid: true},
40+ {name: "home picker without remote route", request: AttachRequest{Intent: protocol.IntentAttach, SessionName: "work", NavigationCapabilities: protocol.NavigationCapabilityHomePicker}, valid: false},
41+ {name: "home picker on daemon-owned remote route", request: AttachRequest{Intent: protocol.IntentAttach, SessionName: "work", Remote: true, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, NavigationCapabilities: protocol.NavigationCapabilityHomePicker}, valid: true},
4342 {name: "back on client-owned route", request: AttachRequest{StartupOverlay: protocol.StartupOverlaySessionPicker, NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: true},
44- {name: "back on remote-target route", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "work", RemoteTarget: remoteRoute, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, StartupOverlay: protocol.StartupOverlaySessionPicker, NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: false},
43+ {name: "back on remote-target route", request: AttachRequest{Intent: protocol.IntentAttach, SessionName: "work", RemoteTarget: remoteRoute, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, StartupOverlay: protocol.StartupOverlaySessionPicker, NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: false},
4544 {name: "unknown capability", request: AttachRequest{NavigationCapabilities: 4}, valid: false},
4645 {name: "back without startup picker", request: AttachRequest{NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: false},
4746 {name: "startup picker without back", request: AttachRequest{StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: false},
M internal/usecase/client/palette_attach_generation_test.go +7 -7
@@ -43,7 +43,7 @@ func newAttachPaletteClock() *attachPaletteClock {
4343 func (*attachPaletteClock) Now() time.Time { return time.Time{} }
4444 func (c *attachPaletteClock) NewTimer(delay time.Duration) ports.Timer {
4545 timer := &attachPaletteTimer{ch: make(chan time.Time, 1), stopped: make(chan struct{}), duration: delay}
46- if delay == ports.HandshakeTimeout {
46+ if delay == protocol.HandshakeTimeout {
4747 select {
4848 case c.handshakeTimers <- timer:
4949 default:
@@ -118,7 +118,7 @@ type attachPaletteTransport struct {
118118 frames []ports.Frame
119119 themes []protocol.Theme
120120 themeCh chan protocol.Theme
121- inputCh chan ports.Input
121+ inputCh chan protocol.Input
122122 detached chan ports.Frame
123123 themeSendErr error
124124 welcomed bool
@@ -127,7 +127,7 @@ type attachPaletteTransport struct {
127127 func newAttachPaletteTransport() *attachPaletteTransport {
128128 return &attachPaletteTransport{
129129 themeCh: make(chan protocol.Theme, 16),
130- inputCh: make(chan ports.Input, 16),
130+ inputCh: make(chan protocol.Input, 16),
131131 detached: make(chan ports.Frame, 1),
132132 }
133133 }
@@ -163,7 +163,7 @@ func (t *attachPaletteTransport) Recv() (ports.Frame, error) {
163163 if !t.welcomed {
164164 t.welcomed = true
165165 t.mu.Unlock()
166- return ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(ports.Welcome{SessionID: "s"})}, nil
166+ return ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(protocol.Welcome{SessionID: "s"})}, nil
167167 }
168168 t.mu.Unlock()
169169 return <-t.detached, nil
@@ -205,7 +205,7 @@ func startAttachPaletteHarnessWithInputAndWriteError(state *terminalThemeState,
205205 ms := &milestones{}
206206 runner := &Runner{term: term, clock: clock, logger: slog.New(slog.DiscardHandler)}
207207 attempt := &attachAttempt{
208- runner: runner, transport: transport, request: AttachRequest{Intent: ports.IntentAttach},
208+ runner: runner, transport: transport, request: AttachRequest{Intent: protocol.IntentAttach},
209209 milestones: ms, themeState: state,
210210 enterRaw: func() error { ms.rawEntered = true; return nil },
211211 reconnect: &reconnectUI{term: term, rawEntered: new(bool)},
@@ -239,7 +239,7 @@ func (h *attachPaletteHarness) nextTimer(t *testing.T) *attachPaletteTimer {
239239 }
240240 func (h *attachPaletteHarness) detach(t *testing.T) {
241241 t.Helper()
242- h.transport.detached <- ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach})}
242+ h.transport.detached <- ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach})}
243243 result := <-h.done
244244 require.NoError(t, result.err)
245245 select {
@@ -325,7 +325,7 @@ func TestAttachReconnectPreservesStandaloneEscapeBeforeClaimRevocation(t *testin
325325
326326 // Detach starts reconnect teardown. The residual must already be owned by
327327 // the lifecycle reader at the precise point the old claim is revoked.
328- first.transport.detached <- ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach})}
328+ first.transport.detached <- ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach})}
329329 require.Equal(t, []byte("\x1b"), <-revoked)
330330 release()
331331 require.NoError(t, (<-first.done).err)
M internal/usecase/client/palette_attach_test.go +3 -3
@@ -109,10 +109,10 @@ func (t *paletteAttachTransport) Recv() (ports.Frame, error) {
109109 first := len(t.sent) == 1
110110 t.mu.Unlock()
111111 if first {
112- return ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(ports.Welcome{SessionID: "s"})}, nil
112+ return ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(protocol.Welcome{SessionID: "s"})}, nil
113113 }
114114 <-t.finalSet
115- return ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach})}, nil
115+ return ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach})}, nil
116116 }
117117 func (*paletteAttachTransport) Close() error { return nil }
118118
@@ -142,7 +142,7 @@ func TestAttachPublishesOnlyClearedAndDefinitiveInitialPalette(t *testing.T) {
142142 ms := &milestones{}
143143 runner := &Runner{term: term, clock: paletteAttachClock{}, logger: slog.New(slog.DiscardHandler)}
144144 attempt := &attachAttempt{
145- runner: runner, transport: transport, request: AttachRequest{Intent: ports.IntentAttach},
145+ runner: runner, transport: transport, request: AttachRequest{Intent: protocol.IntentAttach},
146146 milestones: ms, themeState: &terminalThemeState{},
147147 enterRaw: func() error { ms.rawEntered = true; return nil },
148148 reconnect: &reconnectUI{term: term, rawEntered: new(bool)},
M internal/usecase/client/reconnect.go +2 -1
@@ -5,6 +5,7 @@ import (
55 "time"
66
77 "github.com/bnema/vev/internal/ports"
8+ "github.com/bnema/vev/internal/protocol"
89 )
910
1011 type reconnectBackoff struct {
@@ -54,7 +55,7 @@ func resumeNeedsExactAttach(err error) bool {
5455 return true
5556 }
5657 protocolErr, ok := errors.AsType[*ProtocolError](err)
57- return ok && (protocolErr.Code == ports.ErrNoSuchSession || protocolErr.Code == ports.ErrNoSuchTarget)
58+ return ok && (protocolErr.Code == protocol.ErrNoSuchSession || protocolErr.Code == protocol.ErrNoSuchTarget)
5859 }
5960
6061 func shouldReconnect(err error) bool {
M internal/usecase/client/reconnect_toast_test.go +45 -45
@@ -134,11 +134,11 @@ func reconnectToastWelcome(token uint64) ports.Frame {
134134 }
135135
136136 func reconnectToastWelcomeNamed(name string, token uint64) ports.Frame {
137- return ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(ports.Welcome{SessionID: "s1", SessionName: name, ResumeToken: token, Capabilities: ports.CapabilityResume})}
137+ return ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(protocol.Welcome{SessionID: "s1", SessionName: name, ResumeToken: token, Capabilities: protocol.CapabilityResume})}
138138 }
139139
140140 func reconnectToastDetach(reason uint8) ports.Frame {
141- return ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(ports.Detached{Reason: reason})}
141+ return ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(protocol.Detached{Reason: reason})}
142142 }
143143
144144 type reconnectToastRecv struct {
@@ -205,7 +205,7 @@ func (s *reconnectToastSentFrames) find(match func(ports.Frame) bool) (ports.Fra
205205 type reconnectToastLinkTransport struct {
206206 recvCh chan reconnectToastRecv
207207 sends *reconnectToastSentFrames
208- clientNotices chan ports.ClientNotice
208+ clientNotices chan protocol.ClientNotice
209209 events chan ports.LinkEvent
210210 state ports.LinkState
211211 }
@@ -214,7 +214,7 @@ func newReconnectToastLinkTransport() *reconnectToastLinkTransport {
214214 return &reconnectToastLinkTransport{
215215 recvCh: make(chan reconnectToastRecv, 4),
216216 sends: newReconnectToastSentFrames(),
217- clientNotices: make(chan ports.ClientNotice, 2),
217+ clientNotices: make(chan protocol.ClientNotice, 2),
218218 events: make(chan ports.LinkEvent, 4),
219219 state: ports.LinkStateConnected,
220220 }
@@ -298,7 +298,7 @@ func assertReconnectToastAttemptPublishesOnlyClearedTheme(t *testing.T, tr *reco
298298 require.True(t, tr.wasClosed())
299299 }
300300
301-func reconnectToastHelloFromSend(t *testing.T, tr *reconnectToastRecordingTransport) ports.Hello {
301+func reconnectToastHelloFromSend(t *testing.T, tr *reconnectToastRecordingTransport) protocol.Hello {
302302 t.Helper()
303303 frames := tr.sentFrames()
304304 require.NotEmpty(t, frames)
@@ -362,9 +362,9 @@ func TestResumeNeedsExactAttach(t *testing.T) {
362362 want bool
363363 }{
364364 {name: "moved resume token", err: errRouteTargetChanged, want: true},
365- {name: "missing target", err: &ProtocolError{Code: ports.ErrNoSuchTarget}, want: true},
366- {name: "missing session", err: &ProtocolError{Code: ports.ErrNoSuchSession}, want: true},
367- {name: "internal", err: &ProtocolError{Code: ports.ErrInternal}},
365+ {name: "missing target", err: &ProtocolError{Code: protocol.ErrNoSuchTarget}, want: true},
366+ {name: "missing session", err: &ProtocolError{Code: protocol.ErrNoSuchSession}, want: true},
367+ {name: "internal", err: &ProtocolError{Code: protocol.ErrInternal}},
368368 {name: "transport", err: io.EOF},
369369 } {
370370 t.Run(test.name, func(t *testing.T) {
@@ -386,7 +386,7 @@ func TestProbingToastReconcilesDaemonOutputAndDismissal(t *testing.T) {
386386
387387 resultCh := make(chan attachResult, 1)
388388 ms := milestones{}
389- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
389+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
390390 go func() { resultCh <- attempt.run(context.Background()) }()
391391
392392 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}
@@ -408,7 +408,7 @@ func TestProbingToastReconcilesDaemonOutputAndDismissal(t *testing.T) {
408408 require.Contains(t, redrawn, "probing UDP path")
409409
410410 tr.events <- ports.LinkEvent{State: ports.LinkStateConnected}
411- require.Equal(t, ports.ClientNoticeLinkConnected, (<-tr.clientNotices).Action)
411+ require.Equal(t, protocol.ClientNoticeLinkConnected, (<-tr.clientNotices).Action)
412412 require.Equal(t, term.size, requireResize(t, tr.sends).Size)
413413
414414 beforeAwaitingReset := out.String()
@@ -461,7 +461,7 @@ func TestProbingToastReconcilesDaemonOutputAndDismissal(t *testing.T) {
461461 }, time.Second, time.Millisecond)
462462 requireAckedState(t, tr.sends, 2, 5)
463463
464- tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonDetach)}
464+ tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonDetach)}
465465 result := requireAttachResult(t, resultCh)
466466 require.NoError(t, result.err)
467467 }
@@ -479,7 +479,7 @@ func TestActiveReconnectToastStageTransitionReconcilesBeforeRedraw(t *testing.T)
479479
480480 resultCh := make(chan attachResult, 1)
481481 ms := milestones{}
482- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
482+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
483483 go func() { resultCh <- attempt.run(context.Background()) }()
484484
485485 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}
@@ -547,7 +547,7 @@ func TestActiveReconnectToastStageTransitionReconcilesBeforeRedraw(t *testing.T)
547547 require.Eventually(t, func() bool { return flushes.Load() > beforeIncrementFlushes }, time.Second, time.Millisecond)
548548 requireAckedState(t, tr.sends, 2, 5)
549549
550- tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonDetach)}
550+ tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonDetach)}
551551 result := requireAttachResult(t, resultCh)
552552 require.NoError(t, result.err)
553553 }
@@ -693,7 +693,7 @@ func TestLocalReconnectStatusFlushesDaemonOutputBeforeObservationAndAck(t *testi
693693
694694 resultCh := make(chan attachResult, 1)
695695 ms := milestones{}
696- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main"}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
696+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main"}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
697697 attempt.runner.runtimeObserver = observer
698698 go func() { resultCh <- attempt.run(context.Background()) }()
699699
@@ -725,7 +725,7 @@ func TestLocalReconnectStatusFlushesDaemonOutputBeforeObservationAndAck(t *testi
725725 requireAckedState(t, tr.sends, 1, 2)
726726 require.Greater(t, flushes.Load(), beforeOutput, "daemon output must be flushed before ACK")
727727
728- tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonDetach)}
728+ tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonDetach)}
729729 require.NoError(t, requireAttachResult(t, resultCh).err)
730730 }
731731
@@ -748,14 +748,14 @@ func TestLocalReconnectStatusTransitionsDoNotRequestAuthoritativeReset(t *testin
748748
749749 resultCh := make(chan attachResult, 1)
750750 ms := milestones{}
751- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main"}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
751+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main"}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
752752 go func() { resultCh <- attempt.run(context.Background()) }()
753753
754754 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}
755755 require.Eventually(t, func() bool { return strings.Contains(out.String(), statusReconnect) }, time.Second, time.Millisecond)
756756 tr.events <- ports.LinkEvent{State: tt.transition}
757757 if tt.transition == ports.LinkStateConnected {
758- require.Equal(t, ports.ClientNoticeLinkConnected, (<-tr.clientNotices).Action)
758+ require.Equal(t, protocol.ClientNoticeLinkConnected, (<-tr.clientNotices).Action)
759759 }
760760
761761 const output = "local status output remains visible"
@@ -772,7 +772,7 @@ func TestLocalReconnectStatusTransitionsDoNotRequestAuthoritativeReset(t *testin
772772 _, sentResize := tr.sends.find(func(frame ports.Frame) bool { return frame.Type == ports.MsgResize })
773773 require.False(t, sentResize, "local reconnect status must not request a daemon reset")
774774
775- tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonDetach)}
775+ tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonDetach)}
776776 require.NoError(t, requireAttachResult(t, resultCh).err)
777777 })
778778 }
@@ -820,7 +820,7 @@ func TestReconnectOverlayRedrawFailureDoesNotObserveOrAckOutput(t *testing.T) {
820820
821821 resultCh := make(chan attachResult, 1)
822822 ms := milestones{}
823- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
823+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
824824 attempt.runner.runtimeObserver = observer
825825 go func() { resultCh <- attempt.run(context.Background()) }()
826826
@@ -852,20 +852,20 @@ func TestReconnectLinkEventsNotifyDaemonWithoutLocalTerminalWrites(t *testing.T)
852852
853853 resultCh := make(chan attachResult, 1)
854854 ms := milestones{}
855- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
855+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
856856 go func() { resultCh <- attempt.run(context.Background()) }()
857857
858858 tr.events <- ports.LinkEvent{State: ports.LinkStateDegraded}
859859 tr.events <- ports.LinkEvent{State: ports.LinkStateConnected}
860- require.Equal(t, ports.ClientNoticeLinkDegraded, (<-tr.clientNotices).Action)
861- require.Equal(t, ports.ClientNoticeLinkConnected, (<-tr.clientNotices).Action)
860+ require.Equal(t, protocol.ClientNoticeLinkDegraded, (<-tr.clientNotices).Action)
861+ require.Equal(t, protocol.ClientNoticeLinkConnected, (<-tr.clientNotices).Action)
862862 select {
863863 case bytes := <-out.completed:
864864 t.Fatalf("link notices wrote client-local terminal bytes: %q", bytes)
865865 default:
866866 }
867867
868- tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonDetach)}
868+ tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonDetach)}
869869 result := requireAttachResult(t, resultCh)
870870 require.NoError(t, result.err)
871871 require.True(t, result.welcomed)
@@ -879,7 +879,7 @@ func TestAttachAttemptOfflineLinkEventReturnsReconnectableError(t *testing.T) {
879879
880880 resultCh := make(chan attachResult, 1)
881881 ms := milestones{}
882- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
882+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
883883 go func() { resultCh <- attempt.run(context.Background()) }()
884884
885885 tr.events <- ports.LinkEvent{State: ports.LinkStateOffline}
@@ -988,7 +988,7 @@ func TestPreSenderReconnectResetIsBounded(t *testing.T) {
988988 ctx, cancel := context.WithCancel(context.Background())
989989 defer cancel()
990990 ms := milestones{}
991- attempt := newReconnectAttachAttempt(term.term, tr, clock, AttachRequest{Intent: ports.IntentResume, SessionName: "requested", Remote: true}, 12, &terminalThemeState{}, nil, &ms)
991+ attempt := newReconnectAttachAttempt(term.term, tr, clock, AttachRequest{Intent: protocol.IntentResume, SessionName: "requested", Remote: true}, 12, &terminalThemeState{}, nil, &ms)
992992 attempt.reconnect.showing = true
993993 resultCh := make(chan attachResult, 1)
994994 go func() { resultCh <- attempt.run(ctx) }()
@@ -1078,7 +1078,7 @@ func TestReconnectResetEnqueueCancellationExitsCleanly(t *testing.T) {
10781078
10791079 resultCh := make(chan attachResult, 1)
10801080 ms := milestones{}
1081- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
1081+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
10821082 go func() { resultCh <- attempt.run(ctx) }()
10831083
10841084 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}
@@ -1118,7 +1118,7 @@ func TestReconnectResetCancellationPreservesQueuedSenderError(t *testing.T) {
11181118
11191119 resultCh := make(chan attachResult, 1)
11201120 ms := milestones{}
1121- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
1121+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
11221122 go func() { resultCh <- attempt.run(context.Background()) }()
11231123
11241124 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}
@@ -1157,7 +1157,7 @@ func TestAttachAttemptReturnsWhileSenderSendIsBlockedAfterCancellation(t *testin
11571157
11581158 resultCh := make(chan attachResult, 1)
11591159 ms := milestones{}
1160- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
1160+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
11611161 go func() { resultCh <- attempt.run(context.Background()) }()
11621162
11631163 tr.armed.Store(true)
@@ -1188,8 +1188,8 @@ func TestDetachedResultLegacyReplacementCompatibilityAndCleanDetach(t *testing.T
11881188 reason uint8
11891189 wantText string
11901190 }{
1191- {name: "clean detach", reason: ports.ReasonDetach},
1192- {name: "legacy replacement", reason: ports.ReasonReplaced, wantText: "session taken over by another client"},
1191+ {name: "clean detach", reason: protocol.ReasonDetach},
1192+ {name: "legacy replacement", reason: protocol.ReasonReplaced, wantText: "session taken over by another client"},
11931193 } {
11941194 t.Run(tt.name, func(t *testing.T) {
11951195 err := detachedResult(tt.reason)
@@ -1376,7 +1376,7 @@ func TestReconnectRetryFailuresPreserveAttachError(t *testing.T) {
13761376 )
13771377 dialer := &reconnectToastSequenceDialer{transports: []ports.Transport{tr}}
13781378
1379- err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true})
1379+ err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true})
13801380 require.ErrorIs(t, err, attachErr)
13811381 require.ErrorIs(t, err, retryErr)
13821382 })
@@ -1433,7 +1433,7 @@ func TestReconnectRetrySleepFailuresPreserveDialError(t *testing.T) {
14331433 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
14341434 dialer.EXPECT().Dial(mock.Anything).Run(func(context.Context) { armed.Store(true) }).Return(nil, dialErr).Once()
14351435
1436- err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true})
1436+ err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true})
14371437 require.ErrorIs(t, err, dialErr)
14381438 require.ErrorIs(t, err, retryErr)
14391439 })
@@ -1463,7 +1463,7 @@ func TestRemoteReconnectToastFailedDrawDoesNotBlankBounds(t *testing.T) {
14631463 }}
14641464 dialer := &reconnectToastSequenceDialer{transports: []ports.Transport{tr}}
14651465
1466- err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(ctx, AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true})
1466+ err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(ctx, AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true})
14671467 require.ErrorContains(t, err, "reconnect toast write failed")
14681468 require.True(t, out.failed)
14691469 require.NotContains(t, out.String(), strings.Repeat(" ", reconnectToastBoundsFor(term.size, reconnectStageMessage(reconnectStageSSH)).Width))
@@ -1540,7 +1540,7 @@ func TestReconnectCancellationPreservesContextWhenStatusClearFails(t *testing.T)
15401540 dialer := portsmocks.NewMockDialer(t)
15411541 tt.configure(dialer, tr)
15421542
1543- err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(ctx, AttachRequest{Intent: ports.IntentAttach, SessionName: "main"})
1543+ err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(ctx, AttachRequest{Intent: protocol.IntentAttach, SessionName: "main"})
15441544 require.ErrorIs(t, err, context.Canceled)
15451545 require.ErrorIs(t, err, clearErr)
15461546 })
@@ -1572,11 +1572,11 @@ func TestRemoteReconnectToastLifecycleWithWrappedTransportError(t *testing.T) {
15721572 }}
15731573 tr2 := &reconnectToastRecordingTransport{recvs: []reconnectToastRecv{
15741574 {frame: reconnectToastWelcome(55)},
1575- {frame: reconnectToastDetach(ports.ReasonDetach)},
1575+ {frame: reconnectToastDetach(protocol.ReasonDetach)},
15761576 }}
15771577 dialer := &reconnectToastSequenceDialer{transports: []ports.Transport{tr1, tr2}}
15781578
1579- err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true})
1579+ err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true})
15801580 require.NoError(t, err)
15811581 require.True(t, tr1.wasClosed())
15821582 require.True(t, tr2.wasClosed())
@@ -1584,9 +1584,9 @@ func TestRemoteReconnectToastLifecycleWithWrappedTransportError(t *testing.T) {
15841584
15851585 firstHello := reconnectToastHelloFromSend(t, tr1)
15861586 resumeHello := reconnectToastHelloFromSend(t, tr2)
1587- require.Equal(t, ports.IntentAttach, firstHello.Intent)
1587+ require.Equal(t, protocol.IntentAttach, firstHello.Intent)
15881588 require.Zero(t, firstHello.ResumeToken)
1589- require.Equal(t, ports.IntentResume, resumeHello.Intent)
1589+ require.Equal(t, protocol.IntentResume, resumeHello.Intent)
15901590 require.Equal(t, uint64(44), resumeHello.ResumeToken)
15911591 require.Equal(t, firstHello.ClientID, resumeHello.ClientID)
15921592
@@ -1616,20 +1616,20 @@ func TestRemoteEphemeralReconnectUsesAssignedSessionName(t *testing.T) {
16161616 }}
16171617 tr2 := &reconnectToastRecordingTransport{recvs: []reconnectToastRecv{
16181618 {frame: reconnectToastWelcomeNamed("0", 55)},
1619- {frame: reconnectToastDetach(ports.ReasonDetach)},
1619+ {frame: reconnectToastDetach(protocol.ReasonDetach)},
16201620 }}
16211621 dialer := &reconnectToastSequenceDialer{transports: []ports.Transport{tr1, tr2}}
16221622
1623- err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: ports.IntentEphemeral, SessionName: "", Remote: true})
1623+ err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: protocol.IntentEphemeral, SessionName: "", Remote: true})
16241624 require.NoError(t, err)
16251625 require.Equal(t, 2, dialer.calls)
16261626
16271627 firstHello := reconnectToastHelloFromSend(t, tr1)
16281628 resumeHello := reconnectToastHelloFromSend(t, tr2)
1629- require.Equal(t, ports.IntentEphemeral, firstHello.Intent)
1629+ require.Equal(t, protocol.IntentEphemeral, firstHello.Intent)
16301630 require.Empty(t, firstHello.Name)
16311631 require.Zero(t, firstHello.ResumeToken)
1632- require.Equal(t, ports.IntentResume, resumeHello.Intent)
1632+ require.Equal(t, protocol.IntentResume, resumeHello.Intent)
16331633 require.Equal(t, "0", resumeHello.Name)
16341634 require.Equal(t, uint64(44), resumeHello.ResumeToken)
16351635 require.Equal(t, firstHello.ClientID, resumeHello.ClientID)
@@ -1647,7 +1647,7 @@ func TestRemoteReconnectToastLifecycle(t *testing.T) {
16471647 name: "clears on successful reconnect",
16481648 configure: func(t *testing.T, dialer *portsmocks.MockDialer) []*reconnectToastRecordingTransport {
16491649 tr1 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(11)}, reconnectToastRecv{err: io.EOF})
1650- tr2 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(22)}, reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonDetach)})
1650+ tr2 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(22)}, reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonDetach)})
16511651 dialer.EXPECT().Dial(mock.Anything).Return(tr1, nil).Once()
16521652 dialer.EXPECT().Dial(mock.Anything).Return(tr2, nil).Once()
16531653 return []*reconnectToastRecordingTransport{tr1, tr2}
@@ -1672,7 +1672,7 @@ func TestRemoteReconnectToastLifecycle(t *testing.T) {
16721672 name: "clears on final exit",
16731673 configure: func(t *testing.T, dialer *portsmocks.MockDialer) []*reconnectToastRecordingTransport {
16741674 tr1 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(11)}, reconnectToastRecv{err: io.EOF})
1675- tr2 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(22)}, reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonSessionKilled)})
1675+ tr2 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(22)}, reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonSessionKilled)})
16761676 dialer.EXPECT().Dial(mock.Anything).Return(tr1, nil).Once()
16771677 dialer.EXPECT().Dial(mock.Anything).Return(tr2, nil).Once()
16781678 return []*reconnectToastRecordingTransport{tr1, tr2}
@@ -1704,7 +1704,7 @@ func TestRemoteReconnectToastLifecycle(t *testing.T) {
17041704 dialer := portsmocks.NewMockDialer(t)
17051705 transports := tt.configure(t, dialer)
17061706
1707- err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(ctx, AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true})
1707+ err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(ctx, AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true})
17081708 tt.wantErr(t, err)
17091709 for _, transport := range transports {
17101710 assertReconnectToastAttemptPublishesOnlyClearedTheme(t, transport)
M internal/usecase/client/remote_attach_validation_test.go +9 -10
@@ -7,7 +7,6 @@ import (
77 "github.com/stretchr/testify/require"
88
99 "github.com/bnema/vev/internal/domain"
10- "github.com/bnema/vev/internal/ports"
1110 portsmocks "github.com/bnema/vev/internal/ports/mocks"
1211 "github.com/bnema/vev/internal/protocol"
1312 )
@@ -17,10 +16,10 @@ func TestMalformedAttachRequestsNeverDial(t *testing.T) {
1716 name string
1817 request AttachRequest
1918 }{
20- {name: "missing session name", request: AttachRequest{Intent: ports.IntentAttach}},
21- {name: "unsafe session name", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "bad name"}},
22- {name: "daemon-owned without remote", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "main", EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}},
23- {name: "ephemeral with navigation capability", request: AttachRequest{Intent: ports.IntentEphemeral, NavigationCapabilities: protocol.NavigationCapabilityHomePicker}},
19+ {name: "missing session name", request: AttachRequest{Intent: protocol.IntentAttach}},
20+ {name: "unsafe session name", request: AttachRequest{Intent: protocol.IntentAttach, SessionName: "bad name"}},
21+ {name: "daemon-owned without remote", request: AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}},
22+ {name: "ephemeral with navigation capability", request: AttachRequest{Intent: protocol.IntentEphemeral, NavigationCapabilities: protocol.NavigationCapabilityHomePicker}},
2423 }
2524 for _, tt := range tests {
2625 t.Run(tt.name, func(t *testing.T) {
@@ -42,17 +41,17 @@ func TestValidateAttachRequestRequiresDaemonOwnedEnvironmentForRemoteTarget(t *t
4241
4342 tests := []struct {
4443 name string
45- policy ports.EnvironmentPolicy
44+ policy protocol.EnvironmentPolicy
4645 wantOK bool
4746 }{
48- {name: "daemon owned", policy: ports.EnvironmentPolicyDaemonOwned, wantOK: true},
49- {name: "client owned", policy: ports.EnvironmentPolicyClientOwned},
50- {name: "unknown", policy: ports.EnvironmentPolicy(99)},
47+ {name: "daemon owned", policy: protocol.EnvironmentPolicyDaemonOwned, wantOK: true},
48+ {name: "client owned", policy: protocol.EnvironmentPolicyClientOwned},
49+ {name: "unknown", policy: protocol.EnvironmentPolicy(99)},
5150 }
5251 for _, test := range tests {
5352 t.Run(test.name, func(t *testing.T) {
5453 err := validateAttachRequest(AttachRequest{
55- Intent: ports.IntentAttach, SessionName: target.SessionName,
54+ Intent: protocol.IntentAttach, SessionName: target.SessionName,
5655 RemoteTarget: target, EnvironmentPolicy: test.policy,
5756 })
5857 if test.wantOK {
M internal/usecase/client/routes.go +5 -5
@@ -199,7 +199,7 @@ func routeFailureCode(err error) protocol.RouteFailureCode {
199199 var protocolErr *ProtocolError
200200 if errors.As(err, &protocolErr) {
201201 switch protocolErr.Code {
202- case ports.ErrNoSuchSession, ports.ErrNoSuchTarget:
202+ case protocol.ErrNoSuchSession, protocol.ErrNoSuchTarget:
203203 return protocol.RouteFailureNoSuchRoute
204204 }
205205 }
@@ -545,12 +545,12 @@ func (l *routeLedger) snapshot() protocol.RecentRouteSnapshot {
545545 // samePeerHandoff restores a previously committed route on the daemon already
546546 // connected to by the client. The daemon deliberately sends no tab state: the
547547 // ledger is the sole owner of per-route view memory.
548-func (l *routeLedger) samePeerHandoff(active AttachRequest, target ports.AttachTarget) AttachRequest {
548+func (l *routeLedger) samePeerHandoff(active AttachRequest, target protocol.AttachTarget) AttachRequest {
549549 origin := normalizeRouteOrigin(active.Origin, active.Remote)
550550 originKey := normalizeRouteOriginKey(active.OriginKey, origin)
551551 environmentPolicy := target.EnvironmentPolicy
552552 if origin == protocol.RouteOriginLocal {
553- environmentPolicy = ports.EnvironmentPolicyClientOwned
553+ environmentPolicy = protocol.EnvironmentPolicyClientOwned
554554 }
555555
556556 l.mu.RLock()
@@ -568,7 +568,7 @@ func (l *routeLedger) samePeerHandoff(active AttachRequest, target ports.AttachT
568568 // original discovery target would make its Hello request a second,
569569 // incorrect remote handoff instead of a same-peer session switch.
570570 request.RemoteTarget = nil
571- if request.EnvironmentPolicy != environmentPolicy && environmentPolicy == ports.EnvironmentPolicyClientOwned {
571+ if request.EnvironmentPolicy != environmentPolicy && environmentPolicy == protocol.EnvironmentPolicyClientOwned {
572572 request.NavigationCapabilities = 0
573573 request.StartupOverlay = protocol.StartupOverlayNone
574574 }
@@ -586,7 +586,7 @@ func (l *routeLedger) samePeerHandoff(active AttachRequest, target ports.AttachT
586586 request.ExactTarget = target.ExactTarget
587587 request.PreferredTabID = ""
588588 request.RemoteTarget = nil
589- if request.EnvironmentPolicy != environmentPolicy && environmentPolicy == ports.EnvironmentPolicyClientOwned {
589+ if request.EnvironmentPolicy != environmentPolicy && environmentPolicy == protocol.EnvironmentPolicyClientOwned {
590590 request.NavigationCapabilities = 0
591591 request.StartupOverlay = protocol.StartupOverlayNone
592592 }
M internal/usecase/client/routes_test.go +17 -18
@@ -7,7 +7,6 @@ import (
77 "testing"
88
99 "github.com/bnema/vev/internal/domain"
10- "github.com/bnema/vev/internal/ports"
1110 "github.com/bnema/vev/internal/protocol"
1211 "github.com/stretchr/testify/require"
1312 )
@@ -27,7 +26,7 @@ func routeTestCandidate(index byte, origin protocol.RouteOrigin) routeCandidate
2726 reachability: protocol.RouteReachabilityReachable,
2827 },
2928 request: AttachRequest{
30- Intent: ports.IntentAttach,
29+ Intent: protocol.IntentAttach,
3130 SessionName: target.SessionName,
3231 Origin: origin,
3332 },
@@ -37,7 +36,7 @@ func routeTestCandidate(index byte, origin protocol.RouteOrigin) routeCandidate
3736 func TestRouteCandidateRetainsHostLabelWithoutDiscoveryTarget(t *testing.T) {
3837 target := routeTestTarget(1)
3938 candidate := routeCandidateForAttach(AttachRequest{
40- Intent: ports.IntentAttach, SessionName: target.SessionName, Remote: true,
39+ Intent: protocol.IntentAttach, SessionName: target.SessionName, Remote: true,
4140 Origin: protocol.RouteOriginRemote, OriginKey: "user@remote", HostLabel: "remote",
4241 }, protocol.CommittedRouteIdentity{Target: target}, nil, 0)
4342
@@ -139,16 +138,16 @@ func TestRouteLedgerSamePeerHandoffRestoresRouteTab(t *testing.T) {
139138 {name: "recreated session does not reuse remembered tab", target: recreated, wantExactTarget: &recreated},
140139 } {
141140 t.Run(tt.name, func(t *testing.T) {
142- request := ledger.samePeerHandoff(second.request, ports.AttachTarget{
141+ request := ledger.samePeerHandoff(second.request, protocol.AttachTarget{
143142 Session: first.target.SessionName,
144- Intent: ports.IntentAttach,
143+ Intent: protocol.IntentAttach,
145144 ExactTarget: &tt.target,
146- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
145+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
147146 })
148147 require.Equal(t, first.target.SessionName, request.SessionName)
149148 require.Equal(t, tt.wantExactTarget, request.ExactTarget)
150149 require.Equal(t, tt.wantTab, request.PreferredTabID)
151- require.Equal(t, ports.EnvironmentPolicyClientOwned, request.EnvironmentPolicy)
150+ require.Equal(t, protocol.EnvironmentPolicyClientOwned, request.EnvironmentPolicy)
152151 })
153152 }
154153 }
@@ -156,7 +155,7 @@ func TestRouteLedgerSamePeerHandoffRestoresRouteTab(t *testing.T) {
156155 func TestRouteLedgerSamePeerHandoffClearsDaemonOwnedNavigationForLocalRoutes(t *testing.T) {
157156 active := routeTestCandidate(2, protocol.RouteOriginLocal)
158157 active.originKey = "local"
159- active.request.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned
158+ active.request.EnvironmentPolicy = protocol.EnvironmentPolicyDaemonOwned
160159 active.request.NavigationCapabilities = protocol.NavigationCapabilityHomePicker
161160 active.request.StartupOverlay = protocol.StartupOverlaySessionPicker
162161
@@ -173,19 +172,19 @@ func TestRouteLedgerSamePeerHandoffClearsDaemonOwnedNavigationForLocalRoutes(t *
173172 if tt.matched {
174173 stored := routeTestCandidate(1, protocol.RouteOriginLocal)
175174 stored.originKey = "local"
176- stored.request.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned
175+ stored.request.EnvironmentPolicy = protocol.EnvironmentPolicyDaemonOwned
177176 stored.request.NavigationCapabilities = protocol.NavigationCapabilityHomePicker
178177 stored.request.StartupOverlay = protocol.StartupOverlaySessionPicker
179178 _, err := ledger.commit(stored)
180179 require.NoError(t, err)
181180 }
182181
183- request := ledger.samePeerHandoff(active.request, ports.AttachTarget{
184- Session: target.SessionName, Intent: ports.IntentAttach, ExactTarget: &target,
185- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
182+ request := ledger.samePeerHandoff(active.request, protocol.AttachTarget{
183+ Session: target.SessionName, Intent: protocol.IntentAttach, ExactTarget: &target,
184+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
186185 })
187186
188- require.Equal(t, ports.EnvironmentPolicyClientOwned, request.EnvironmentPolicy)
187+ require.Equal(t, protocol.EnvironmentPolicyClientOwned, request.EnvironmentPolicy)
189188 require.Zero(t, request.NavigationCapabilities)
190189 require.Equal(t, protocol.StartupOverlayNone, request.StartupOverlay)
191190 require.NoError(t, validateAttachRequest(request))
@@ -199,7 +198,7 @@ func TestRouteLedgerSamePeerHandoffDropsOriginalRemoteTarget(t *testing.T) {
199198 work.originKey = "remote"
200199 work.request.Remote = true
201200 work.request.OriginKey = "remote"
202- work.request.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned
201+ work.request.EnvironmentPolicy = protocol.EnvironmentPolicyDaemonOwned
203202 work.request.RemoteTarget = &domain.RemoteSessionTarget{
204203 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: work.target.LifecycleID,
205204 SessionName: work.target.SessionName, LiveTabID: "tab-work",
@@ -211,13 +210,13 @@ func TestRouteLedgerSamePeerHandoffDropsOriginalRemoteTarget(t *testing.T) {
211210 agents.originKey = "remote"
212211 agents.request.Remote = true
213212 agents.request.OriginKey = "remote"
214- agents.request.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned
213+ agents.request.EnvironmentPolicy = protocol.EnvironmentPolicyDaemonOwned
215214
216- request := ledger.samePeerHandoff(agents.request, ports.AttachTarget{
215+ request := ledger.samePeerHandoff(agents.request, protocol.AttachTarget{
217216 Session: work.target.SessionName,
218- Intent: ports.IntentAttach,
217+ Intent: protocol.IntentAttach,
219218 ExactTarget: &work.target,
220- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
219+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
221220 })
222221
223222 require.True(t, request.Remote)
M internal/usecase/client/same_peer_switch_test.go +4 -4
@@ -24,7 +24,7 @@ func TestCloseAndDialAttachTargetSkipsSamePeerSwitch(t *testing.T) {
2424 term.term,
2525 transport,
2626 newReconnectHandshakeClock(t),
27- AttachRequest{Intent: ports.IntentAttach, SessionName: "source"},
27+ AttachRequest{Intent: protocol.IntentAttach, SessionName: "source"},
2828 0,
2929 &terminalThemeState{},
3030 nil,
@@ -35,9 +35,9 @@ func TestCloseAndDialAttachTargetSkipsSamePeerSwitch(t *testing.T) {
3535 go func() { resultCh <- attempt.run(ctx) }()
3636
3737 exact := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{7}, SessionName: "stopped"}
38- target := ports.AttachTarget{
39- Session: "stopped", Intent: ports.IntentAttach, ExactTarget: &exact,
40- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
38+ target := protocol.AttachTarget{
39+ Session: "stopped", Intent: protocol.IntentAttach, ExactTarget: &exact,
40+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
4141 }
4242 transport.recvCh <- reconnectToastRecv{frame: ports.Frame{
4343 Type: ports.MsgAttachTarget, Payload: ports.MarshalAttachTarget(target),
M internal/usecase/client/session_connection.go +3 -2
@@ -6,6 +6,7 @@ import (
66
77 "github.com/bnema/vev/internal/domain"
88 "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
910 )
1011
1112 var (
@@ -22,12 +23,12 @@ type SessionTarget struct {
2223
2324 func (t SessionTarget) validate() error {
2425 switch t.Intent {
25- case ports.IntentEphemeral:
26+ case protocol.IntentEphemeral:
2627 if t.SessionName != "" {
2728 return ErrEphemeralSessionName
2829 }
2930 return nil
30- case ports.IntentNew, ports.IntentAttach, ports.IntentResume:
31+ case protocol.IntentNew, protocol.IntentAttach, protocol.IntentResume:
3132 if t.SessionName == "" {
3233 return domain.ErrInvalidSessionName
3334 }
M internal/usecase/client/session_connection_test.go +11 -11
@@ -3,14 +3,14 @@ package client_test
33 import (
44 "testing"
55
6- "github.com/bnema/vev/internal/ports"
76 portsmocks "github.com/bnema/vev/internal/ports/mocks"
7+ "github.com/bnema/vev/internal/protocol"
88 "github.com/bnema/vev/internal/usecase/client"
99 "github.com/stretchr/testify/require"
1010 )
1111
1212 func TestSessionConnectionUsesOneAttachShapeForLocalAndRemoteTransports(t *testing.T) {
13- target := client.SessionTarget{Intent: ports.IntentAttach, SessionName: "work"}
13+ target := client.SessionTarget{Intent: protocol.IntentAttach, SessionName: "work"}
1414 local := portsmocks.NewMockTransport(t)
1515 remote := portsmocks.NewMockTransport(t)
1616
@@ -20,7 +20,7 @@ func TestSessionConnectionUsesOneAttachShapeForLocalAndRemoteTransports(t *testi
2020 require.NoError(t, err)
2121
2222 require.Equal(t, localConnection.AttachRequest(), remoteConnection.AttachRequest())
23- require.Equal(t, client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work"}, localConnection.AttachRequest())
23+ require.Equal(t, client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work"}, localConnection.AttachRequest())
2424 require.Same(t, local, localConnection.Transport())
2525 require.Same(t, remote, remoteConnection.Transport())
2626 }
@@ -32,13 +32,13 @@ func TestSessionTargetValidation(t *testing.T) {
3232 wantErr bool
3333 wantErrIs error
3434 }{
35- {name: "ephemeral", target: client.SessionTarget{Intent: ports.IntentEphemeral}},
36- {name: "named ephemeral", target: client.SessionTarget{Intent: ports.IntentEphemeral, SessionName: "work"}, wantErr: true, wantErrIs: client.ErrEphemeralSessionName},
37- {name: "new", target: client.SessionTarget{Intent: ports.IntentNew, SessionName: "work"}},
38- {name: "attach", target: client.SessionTarget{Intent: ports.IntentAttach, SessionName: "work"}},
39- {name: "resume", target: client.SessionTarget{Intent: ports.IntentResume, SessionName: "work"}},
40- {name: "missing named session", target: client.SessionTarget{Intent: ports.IntentAttach}, wantErr: true},
41- {name: "unsafe session", target: client.SessionTarget{Intent: ports.IntentAttach, SessionName: "my work"}, wantErr: true},
35+ {name: "ephemeral", target: client.SessionTarget{Intent: protocol.IntentEphemeral}},
36+ {name: "named ephemeral", target: client.SessionTarget{Intent: protocol.IntentEphemeral, SessionName: "work"}, wantErr: true, wantErrIs: client.ErrEphemeralSessionName},
37+ {name: "new", target: client.SessionTarget{Intent: protocol.IntentNew, SessionName: "work"}},
38+ {name: "attach", target: client.SessionTarget{Intent: protocol.IntentAttach, SessionName: "work"}},
39+ {name: "resume", target: client.SessionTarget{Intent: protocol.IntentResume, SessionName: "work"}},
40+ {name: "missing named session", target: client.SessionTarget{Intent: protocol.IntentAttach}, wantErr: true},
41+ {name: "unsafe session", target: client.SessionTarget{Intent: protocol.IntentAttach, SessionName: "my work"}, wantErr: true},
4242 {name: "unknown intent", target: client.SessionTarget{Intent: 99, SessionName: "work"}, wantErr: true},
4343 }
4444 for _, tt := range tests {
@@ -58,6 +58,6 @@ func TestSessionTargetValidation(t *testing.T) {
5858 }
5959
6060 func TestSessionConnectionRequiresTransport(t *testing.T) {
61- _, err := client.NewSessionConnection(nil, client.SessionTarget{Intent: ports.IntentEphemeral})
61+ _, err := client.NewSessionConnection(nil, client.SessionTarget{Intent: protocol.IntentEphemeral})
6262 require.Error(t, err)
6363 }
M internal/usecase/client/transition_ui.go +3 -2
@@ -7,6 +7,7 @@ import (
77
88 "github.com/bnema/vev/internal/domain"
99 "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
1011 )
1112
1213 const (
@@ -16,7 +17,7 @@ const (
1617
1718 var transitionSpinnerFrames = [...]rune{'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'}
1819
19-func transitionMessage(target ports.AttachTarget) string {
20+func transitionMessage(target protocol.AttachTarget) string {
2021 origin := target.Endpoint
2122 if target.RemoteTarget != nil && target.RemoteTarget.DisplayOrigin != "" {
2223 origin = target.RemoteTarget.DisplayOrigin
@@ -55,7 +56,7 @@ func newTransitionUI(term ports.Terminal, clock ports.Clock, rawEntered *bool) *
5556 return &transitionUI{term: term, clock: clock, rawEntered: rawEntered}
5657 }
5758
58-func (u *transitionUI) start(target ports.AttachTarget) {
59+func (u *transitionUI) start(target protocol.AttachTarget) {
5960 if u == nil {
6061 return
6162 }
M internal/usecase/client/transition_ui_test.go +4 -3
@@ -11,15 +11,16 @@ import (
1111
1212 "github.com/bnema/vev/internal/domain"
1313 "github.com/bnema/vev/internal/ports"
14+ "github.com/bnema/vev/internal/protocol"
1415 )
1516
1617 func TestTransitionMessageDescribesSwitchAndStoppedRestore(t *testing.T) {
17- live := ports.AttachTarget{Endpoint: "remote", Session: "work"}
18+ live := protocol.AttachTarget{Endpoint: "remote", Session: "work"}
1819 stoppedTarget := domain.RemoteSessionTarget{
1920 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: domain.SessionLifecycleID{1},
2021 SessionName: "work", Stopped: true, StoppedTab: domain.NewStableTabSelector("tab-1"),
2122 }
22- stopped := ports.AttachTarget{Endpoint: "remote", Session: "work", RemoteTarget: &stoppedTarget}
23+ stopped := protocol.AttachTarget{Endpoint: "remote", Session: "work", RemoteTarget: &stoppedTarget}
2324
2425 require.Equal(t, "Switching to work@remote…", transitionMessage(live))
2526 require.Equal(t, "Starting work@remote…", transitionMessage(stopped))
@@ -94,7 +95,7 @@ func TestTransitionUIStartsAfterDelayTicksResizesAndStops(t *testing.T) {
9495 term := &transitionTestTerminal{}
9596 raw := tt.raw
9697 ui := newTransitionUI(term, clock, &raw)
97- ui.start(ports.AttachTarget{Endpoint: "remote", Session: "work"})
98+ ui.start(protocol.AttachTarget{Endpoint: "remote", Session: "work"})
9899
99100 require.Equal(t, []time.Duration{transitionSpinnerDelay}, clock.delays)
100101 require.NotNil(t, ui.tickC())
M internal/usecase/client/transport_observability_test.go +5 -5
@@ -50,7 +50,7 @@ func TestBlockingRuntimeObserverDoesNotDelayTerminalFlushOrACK(t *testing.T) {
5050 transport.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
5151 transport.EXPECT().Send(isType(ports.MsgAck)).Run(func(ports.Frame) { close(acked) }).Return(nil).Once()
5252 unblock := scriptRecv(transport,
53- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
53+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
5454 recvItem{f: frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("flush-before-observe"), New: 3}))},
5555 )
5656 defer unblock()
@@ -59,7 +59,7 @@ func TestBlockingRuntimeObserverDoesNotDelayTerminalFlushOrACK(t *testing.T) {
5959 ctx, cancel := context.WithCancel(context.Background())
6060 result := make(chan error, 1)
6161 go func() {
62- result <- runTestClient(ctx, testDependencies(transportDialer{transport: transport}, term, realClock{}, nil, reporter), client.AttachRequest{Intent: ports.IntentEphemeral})
62+ result <- runTestClient(ctx, testDependencies(transportDialer{transport: transport}, term, realClock{}, nil, reporter), client.AttachRequest{Intent: protocol.IntentEphemeral})
6363 }()
6464 awaitClientRuntime(t, observer.entered, "blocking observer")
6565 awaitClientRuntime(t, flushed, "terminal flush")
@@ -90,15 +90,15 @@ func TestTerminalFlushBoundaryTransportObservability(t *testing.T) {
9090 transport.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
9191 transport.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
9292 unblock := scriptRecv(transport,
93- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
93+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
9494 recvItem{f: frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("unchanged-by-observer"), New: 3}))},
95- recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
95+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
9696 )
9797 defer unblock()
9898 transport.EXPECT().Send(isType(ports.MsgAck)).Return(nil).Maybe()
9999 transport.EXPECT().Close().Return(nil).Once()
100100
101- err := runTestClient(context.Background(), testDependencies(transportDialer{transport: transport}, term, realClock{}, nil, reporter), client.AttachRequest{Intent: ports.IntentEphemeral})
101+ err := runTestClient(context.Background(), testDependencies(transportDialer{transport: transport}, term, realClock{}, nil, reporter), client.AttachRequest{Intent: protocol.IntentEphemeral})
102102 require.NoError(t, err)
103103 reporter.Flush()
104104 require.Equal(t, "\x1b]10;?\x07\x1b]11;?\x07\x1b]4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?\x07\x1b[?2031$punchanged-by-observer", out.String(), "observer must not alter terminal bytes")
M internal/usecase/daemon/attachment_effect_gate_test.go +8 -8
@@ -250,7 +250,7 @@ func TestKillSessionAcquisitionTimeoutDoesNotPublishPartialInvalidation(t *testi
250250 frozenByKill := make(chan *attachedClient, 3)
251251 d.afterAttachmentEffectGateFrozen = func(_ string, ac *attachedClient) { frozenByKill <- ac }
252252 killDone := make(chan error, 1)
253- go func() { killDone <- d.killSession(sess, ports.ReasonSessionKilled, false) }()
253+ go func() { killDone <- d.killSession(sess, protocol.ReasonSessionKilled, false) }()
254254
255255 require.Same(t, first, awaitTestValue(t, frozenByKill, "teardown did not acquire the first attachment effect gate"))
256256 deadline := awaitTestValue(t, clock.timers, "teardown did not arm the gate acquisition deadline")
@@ -343,7 +343,7 @@ func TestKillSessionInterruptsOnlyExactParticipantBlockedSends(t *testing.T) {
343343 awaitTestCompletion(t, activeTransport.sendEntered, "active send did not block")
344344
345345 killDone := make(chan error, 1)
346- go func() { killDone <- d.killSession(sess, ports.ReasonSessionKilled, false) }()
346+ go func() { killDone <- d.killSession(sess, protocol.ReasonSessionKilled, false) }()
347347 select {
348348 case err := <-killDone:
349349 require.NoError(t, err)
@@ -380,7 +380,7 @@ func TestKillSessionInterruptsCapturedSendWithoutClosingNewerIncarnation(t *test
380380 ac.replaceTransport(fresh)
381381 }
382382
383- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))
383+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
384384 awaitTestCompletion(t, sendDone, "captured incarnation attachment effect ticket did not retire")
385385 require.True(t, stale.Closed(), "exact captured in-flight transport was not interrupted")
386386 require.False(t, fresh.Closed(), "newer transport incarnation was interrupted")
@@ -408,7 +408,7 @@ func TestShutdownSignalsServeWhenParticipantGateAcquisitionTimesOut(t *testing.T
408408 clock := &signalClock{timers: make(chan *signalTimer, 1)}
409409 d.clock = clock
410410 shutdownDone := make(chan bool, 1)
411- go func() { shutdownDone <- d.shutdownAll(ports.ReasonServerShutdown) }()
411+ go func() { shutdownDone <- d.shutdownAll(protocol.ReasonServerShutdown) }()
412412
413413 deadline := awaitTestValue(t, clock.timers, "shutdown did not arm the gate acquisition deadline")
414414 deadline.ch <- clock.Now()
@@ -437,7 +437,7 @@ func TestShutdownInterruptsBlockedParticipantSend(t *testing.T) {
437437 awaitTestCompletion(t, blocked.sendEntered, "active send did not block")
438438
439439 shutdownDone := make(chan bool, 1)
440- go func() { shutdownDone <- d.shutdownAll(ports.ReasonServerShutdown) }()
440+ go func() { shutdownDone <- d.shutdownAll(protocol.ReasonServerShutdown) }()
441441 select {
442442 case incomplete := <-shutdownDone:
443443 require.False(t, incomplete)
@@ -726,8 +726,8 @@ func TestAttachedNavigationCommandSendsResultAfterLocalTransition(t *testing.T)
726726 token := source.captureAttachmentCapability(ac, transport)
727727 token.lease = rc.attachmentLease(ac)
728728 ac.installTestAttachmentCapability(token)
729- payload, err := ports.MarshalCommandRequest(ports.CommandRequest{
730- Version: ports.ProtocolVersion, RequestID: 1, Slug: "back-session", Attached: true,
729+ payload, err := ports.MarshalCommandRequest(protocol.CommandRequest{
730+ Version: protocol.Version, RequestID: 1, Slug: "back-session", Attached: true,
731731 })
732732 require.NoError(t, err)
733733
@@ -735,7 +735,7 @@ func TestAttachedNavigationCommandSendsResultAfterLocalTransition(t *testing.T)
735735 require.Same(t, source, ac.currentAttachmentSession())
736736 frames := transport.Sends()
737737 require.NotEmpty(t, frames)
738- var result ports.CommandResult
738+ var result protocol.CommandResult
739739 for _, frame := range frames {
740740 if frame.Type != ports.MsgCommandResult {
741741 continue
M internal/usecase/daemon/attachment_output.go +1 -1
@@ -42,7 +42,7 @@ type attachmentOutput struct {
4242
4343 // reconfigureAttachmentOutput applies a replacement terminal's declared
4444 // capabilities to attachment-owned output state. Caller holds d.mu.
45-func (d *Daemon) reconfigureAttachmentOutput(sess *session, ac *attachedClient, h ports.Hello) {
45+func (d *Daemon) reconfigureAttachmentOutput(sess *session, ac *attachedClient, h protocol.Hello) {
4646 if d == nil || ac == nil || ac.output == nil {
4747 return
4848 }
M internal/usecase/daemon/attachment_parking_test.go +24 -24
@@ -24,10 +24,10 @@ func TestAttachmentLossParksOnlyThatAttachment(t *testing.T) {
2424 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
2525
2626 firstTransport := &closeTrackingTransport{}
27- sess, first, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), firstTransport)
27+ sess, first, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), firstTransport)
2828 require.NoError(t, err)
2929 secondTransport := &closeTrackingTransport{}
30- _, second, err := d.route(helloResumeCapable(ports.IntentAttach, "work", 0), secondTransport)
30+ _, second, err := d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), secondTransport)
3131 require.NoError(t, err)
3232
3333 d.clientGone(sess, first, firstTransport, false)
@@ -51,10 +51,10 @@ func TestTwoAttachmentsParkIndependently(t *testing.T) {
5151 defer release()
5252 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
5353 firstTransport := &closeTrackingTransport{}
54- sess, first, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), firstTransport)
54+ sess, first, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), firstTransport)
5555 require.NoError(t, err)
5656 secondTransport := &closeTrackingTransport{}
57- _, second, err := d.route(helloResumeCapable(ports.IntentAttach, "work", 0), secondTransport)
57+ _, second, err := d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), secondTransport)
5858 require.NoError(t, err)
5959 firstToken, secondToken := first.resumeToken, second.resumeToken
6060
@@ -76,10 +76,10 @@ func TestParkExpiryRemovesOnlyOneAttachment(t *testing.T) {
7676 defer release()
7777 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
7878 firstTransport := &closeTrackingTransport{}
79- sess, first, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), firstTransport)
79+ sess, first, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), firstTransport)
8080 require.NoError(t, err)
8181 secondTransport := &closeTrackingTransport{}
82- _, second, err := d.route(helloResumeCapable(ports.IntentAttach, "work", 0), secondTransport)
82+ _, second, err := d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), secondTransport)
8383 require.NoError(t, err)
8484 d.clientGone(sess, first, firstTransport, false)
8585 d.clientGone(sess, second, secondTransport, false)
@@ -101,14 +101,14 @@ func TestResumeRotatesCredentialAndRejectsStaleTransport(t *testing.T) {
101101 defer release()
102102 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
103103 oldTransport := &closeTrackingTransport{}
104- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
104+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
105105 require.NoError(t, err)
106106 oldToken := ac.resumeToken
107107 oldGeneration := ac.lifecycle.generationValue()
108108 d.clientGone(sess, ac, oldTransport, false)
109109
110110 newTransport := &closeTrackingTransport{}
111- resumed, same, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", oldToken), newTransport, defaultSize)
111+ resumed, same, ok, err := d.resumeParked(helloResumeCapable(protocol.IntentResume, "work", oldToken), newTransport, defaultSize)
112112 require.NoError(t, err)
113113 require.True(t, ok)
114114 require.Same(t, sess, resumed)
@@ -135,14 +135,14 @@ func TestSuccessfulResumeRejectsOldCredentialAfterWelcome(t *testing.T) {
135135 defer release()
136136 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
137137 oldTransport := &closeTrackingTransport{}
138- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
138+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
139139 require.NoError(t, err)
140140 oldToken := ac.resumeToken
141141 ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
142142 d.clientGone(sess, ac, oldTransport, false)
143143
144144 resumedTransport := &closeTrackingTransport{}
145- d.handleHello(resumedTransport, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(helloResumeCapable(ports.IntentResume, "work", oldToken))})
145+ d.handleHello(resumedTransport, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(helloResumeCapable(protocol.IntentResume, "work", oldToken))})
146146 newToken := ac.resumeToken
147147 require.NotEqual(t, oldToken, newToken)
148148 d.mu.Lock()
@@ -160,13 +160,13 @@ func TestFailedResumeHandshakeKeepsParkedCredential(t *testing.T) {
160160 defer release()
161161 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
162162 oldTransport := &closeTrackingTransport{}
163- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
163+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
164164 require.NoError(t, err)
165165 token := ac.resumeToken
166166 d.clientGone(sess, ac, oldTransport, false)
167167
168168 failed := &resumeWelcomeFailureTransport{}
169- d.handleHello(failed, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(helloResumeCapable(ports.IntentResume, "work", token))})
169+ d.handleHello(failed, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(helloResumeCapable(protocol.IntentResume, "work", token))})
170170 require.Nil(t, ac.transport())
171171 d.mu.Lock()
172172 parked := d.parked[token]
@@ -176,7 +176,7 @@ func TestFailedResumeHandshakeKeepsParkedCredential(t *testing.T) {
176176 require.False(t, claimed)
177177 require.Equal(t, token, ac.resumeToken)
178178
179- _, resumed, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{}, defaultSize)
179+ _, resumed, ok, err := d.resumeParked(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{}, defaultSize)
180180 require.NoError(t, err)
181181 require.True(t, ok)
182182 require.Same(t, ac, resumed)
@@ -187,7 +187,7 @@ func TestConcurrentResumeHasExactlyOneWinner(t *testing.T) {
187187 defer release()
188188 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
189189 oldTransport := &closeTrackingTransport{}
190- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
190+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
191191 require.NoError(t, err)
192192 token := ac.resumeToken
193193 d.clientGone(sess, ac, oldTransport, false)
@@ -199,7 +199,7 @@ func TestConcurrentResumeHasExactlyOneWinner(t *testing.T) {
199199 results := make(chan result, 2)
200200 for range 2 {
201201 go func() {
202- _, _, ok, resumeErr := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{}, defaultSize)
202+ _, _, ok, resumeErr := d.resumeParked(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{}, defaultSize)
203203 results <- result{ok: ok, err: resumeErr}
204204 }()
205205 }
@@ -219,18 +219,18 @@ func TestKillSessionClosesEveryAttachmentAndParkedCredential(t *testing.T) {
219219 defer release()
220220 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
221221 firstTransport := &closeTrackingTransport{}
222- sess, _, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), firstTransport)
222+ sess, _, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), firstTransport)
223223 require.NoError(t, err)
224224 secondTransport := &closeTrackingTransport{}
225- _, _, err = d.route(helloResumeCapable(ports.IntentAttach, "work", 0), secondTransport)
225+ _, _, err = d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), secondTransport)
226226 require.NoError(t, err)
227227 parkedTransport := &closeTrackingTransport{}
228- _, parkedAttachment, err := d.route(helloResumeCapable(ports.IntentAttach, "work", 0), parkedTransport)
228+ _, parkedAttachment, err := d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), parkedTransport)
229229 require.NoError(t, err)
230230 d.clientGone(sess, parkedAttachment, parkedTransport, false)
231231 parkedToken := parkedAttachment.resumeToken
232232
233- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))
233+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
234234 d.waitNotifies()
235235 require.True(t, firstTransport.Closed())
236236 require.True(t, secondTransport.Closed())
@@ -246,14 +246,14 @@ func TestResumeCredentialIsInvalidAfterDaemonRestart(t *testing.T) {
246246 defer release()
247247 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
248248 transport := &closeTrackingTransport{}
249- _, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), transport)
249+ _, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), transport)
250250 require.NoError(t, err)
251251 token := ac.resumeToken
252252 restarted := newTestDaemon(t, newFactory(t, pty), stubClock{})
253- _, _, err = restarted.route(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{})
253+ _, _, err = restarted.route(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{})
254254 var protocolErr *protoErr
255255 require.ErrorAs(t, err, &protocolErr)
256- require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)
256+ require.Equal(t, protocol.ErrNoSuchSession, protocolErr.code)
257257 }
258258
259259 func TestHandleHelloAllowsMultipleAttachments(t *testing.T) {
@@ -261,10 +261,10 @@ func TestHandleHelloAllowsMultipleAttachments(t *testing.T) {
261261 defer release()
262262 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
263263 firstTransport := &closeTrackingTransport{}
264- sess, first, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), firstTransport)
264+ sess, first, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), firstTransport)
265265 require.NoError(t, err)
266266 secondTransport := &closeTrackingTransport{}
267- _, second, err := d.route(helloResumeCapable(ports.IntentAttach, "work", 0), secondTransport)
267+ _, second, err := d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), secondTransport)
268268 require.NoError(t, err)
269269 require.Equal(t, true, sess.attachmentRegistered(first))
270270 require.Equal(t, true, sess.attachmentRegistered(second))
M internal/usecase/daemon/attachment_render_test.go +14 -14
@@ -103,14 +103,14 @@ func TestAttachmentResizeKeepsPeerWindowAndExpandsSharedContent(t *testing.T) {
103103 defer releasePTY()
104104 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
105105 firstTransport, _ := newCapturingTransport(t)
106- sess, first, err := d.route(ports.Hello{
107- Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work",
106+ sess, first, err := d.route(protocol.Hello{
107+ Version: protocol.Version, Intent: protocol.IntentNew, Name: "work",
108108 Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1},
109109 }, firstTransport)
110110 require.NoError(t, err)
111111 secondTransport, _ := newCapturingTransport(t)
112- _, second, err := d.route(ports.Hello{
113- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work",
112+ _, second, err := d.route(protocol.Hello{
113+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work",
114114 Size: domain.Size{Cols: 100, Rows: 40}, ClientID: [16]byte{2},
115115 }, secondTransport)
116116 require.NoError(t, err)
@@ -147,10 +147,10 @@ func TestAttachmentFirstPaintDoesNotWaitForBlockedPeer(t *testing.T) {
147147 clock := &signalClock{timers: make(chan *signalTimer, 16)}
148148 d := newTestDaemon(t, newFactory(t, pty), clock)
149149 oldTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)}
150- sess, old, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1}}, oldTransport)
150+ sess, old, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1}}, oldTransport)
151151 require.NoError(t, err)
152152 newTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)}
153- _, fresh, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{2}}, newTransport)
153+ _, fresh, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{2}}, newTransport)
154154 require.NoError(t, err)
155155
156156 rc := sess.renderCoordinator()
@@ -183,7 +183,7 @@ func TestMultiAttachmentHandshakeFirstPaintNotGatedByBlockedPeer(t *testing.T) {
183183 clock := &signalClock{timers: make(chan *signalTimer, 16)}
184184 d := newTestDaemon(t, newFactory(t, pty), clock)
185185 oldTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)}
186- sess, old, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1}}, oldTransport)
186+ sess, old, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1}}, oldTransport)
187187 require.NoError(t, err)
188188 rc := sess.renderCoordinator()
189189 require.True(t, rc.markAttachmentReady(rc.attachmentLease(old)))
@@ -201,7 +201,7 @@ func TestMultiAttachmentHandshakeFirstPaintNotGatedByBlockedPeer(t *testing.T) {
201201 }()
202202 awaitTestCompletion(t, oldTransport.entered, "slow attachment did not begin its blocked output")
203203
204- hello := ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{2}}
204+ hello := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{2}}
205205 tr, sends, releaseConn := newConn(t, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)})
206206 defer releaseConn()
207207 handshakeDone := make(chan struct{})
@@ -223,10 +223,10 @@ func TestAttachmentPaintFanoutDoesNotWaitForBlockedPeer(t *testing.T) {
223223 defer releasePTY()
224224 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
225225 firstTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)}
226- sess, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1}}, firstTransport)
226+ sess, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1}}, firstTransport)
227227 require.NoError(t, err)
228228 secondTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)}
229- _, _, err = d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{2}}, secondTransport)
229+ _, _, err = d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{2}}, secondTransport)
230230 require.NoError(t, err)
231231 rc := sess.renderCoordinator()
232232 for _, ac := range sess.snapshotAttachments() {
@@ -259,14 +259,14 @@ func TestAttachmentResizeUsesLatestClaimedSessionGeometry(t *testing.T) {
259259 defer releasePTY()
260260 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
261261 firstTransport, _ := newCapturingTransport(t)
262- sess, first, err := d.route(ports.Hello{
263- Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work",
262+ sess, first, err := d.route(protocol.Hello{
263+ Version: protocol.Version, Intent: protocol.IntentNew, Name: "work",
264264 Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1},
265265 }, firstTransport)
266266 require.NoError(t, err)
267267 secondTransport, _ := newCapturingTransport(t)
268- _, second, err := d.route(ports.Hello{
269- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work",
268+ _, second, err := d.route(protocol.Hello{
269+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work",
270270 Size: domain.Size{Cols: 100, Rows: 40}, ClientID: [16]byte{2},
271271 }, secondTransport)
272272 require.NoError(t, err)
M internal/usecase/daemon/client.go +8 -8
@@ -1016,7 +1016,7 @@ func (d *Daemon) resizeAttachmentGeometryForLease(effect *attachmentEffect, geom
10161016 // attachment-routing operation: attachment publication also takes routingMu
10171017 // before changing membership. Never retain sess.mu while touching notice or overlay
10181018 // state, and retain the routingMu -> sess.mu order used by attachment paths.
1019-func (d *Daemon) handleClientNotice(sess *session, ac *attachedClient, notice ports.ClientNotice) {
1019+func (d *Daemon) handleClientNotice(sess *session, ac *attachedClient, notice protocol.ClientNotice) {
10201020 if sess == nil || ac == nil {
10211021 return
10221022 }
@@ -1036,7 +1036,7 @@ func (d *Daemon) handleClientNotice(sess *session, ac *attachedClient, notice po
10361036 d.handleClientNoticeForAttachment(effect, notice)
10371037 }
10381038
1039-func (d *Daemon) handleClientNoticeForAttachment(effect *attachmentEffect, notice ports.ClientNotice) {
1039+func (d *Daemon) handleClientNoticeForAttachment(effect *attachmentEffect, notice protocol.ClientNotice) {
10401040 d.notices.routingMu.Lock()
10411041 if !effect.current() {
10421042 d.notices.routingMu.Unlock()
@@ -1060,7 +1060,7 @@ func (d *Daemon) handleClientNoticeForAttachment(effect *attachmentEffect, notic
10601060 }
10611061 }
10621062
1063-func (d *Daemon) handleClientNoticeDirect(sess *session, ac *attachedClient, notice ports.ClientNotice) {
1063+func (d *Daemon) handleClientNoticeDirect(sess *session, ac *attachedClient, notice protocol.ClientNotice) {
10641064 d.notices.routingMu.Lock()
10651065 sess.mu.Lock()
10661066 _, current := sess.attachments[ac]
@@ -1079,15 +1079,15 @@ func (d *Daemon) handleClientNoticeDirect(sess *session, ac *attachedClient, not
10791079 }
10801080 }
10811081
1082-func (d *Daemon) mutateClientNotice(sess *session, ac *attachedClient, notice ports.ClientNotice) bool {
1082+func (d *Daemon) mutateClientNotice(sess *session, ac *attachedClient, notice protocol.ClientNotice) bool {
10831083 switch notice.Action {
1084- case ports.ClientNoticeClipboardFallback:
1084+ case protocol.ClientNoticeClipboardFallback:
10851085 return d.recordClientNotice(sess, ac, domain.NoticeError, domain.NoticeClipboard, "image paste failed; sent Ctrl+V")
1086- case ports.ClientNoticeClipboardTooLarge:
1086+ case protocol.ClientNoticeClipboardTooLarge:
10871087 return d.recordClientNotice(sess, ac, domain.NoticeWarn, domain.NoticeClipboardTooLarge, "image too large to paste")
1088- case ports.ClientNoticeLinkDegraded:
1088+ case protocol.ClientNoticeLinkDegraded:
10891089 return d.recordClientNotice(sess, ac, domain.NoticeWarn, domain.NoticeConnection, "connection degraded")
1090- case ports.ClientNoticeLinkConnected:
1090+ case protocol.ClientNoticeLinkConnected:
10911091 return d.dismissToastWithoutRepaint(ac, domain.NoticeConnection, sess.id)
10921092 default:
10931093 return false
M internal/usecase/daemon/client_frame_routing.go +14 -14
@@ -234,52 +234,52 @@ func (d *Daemon) commandResultEffect(effect *attachmentEffect) (*attachmentEffec
234234 return effect.ac.beginAttachmentEffect(capability)
235235 }
236236
237-func (d *Daemon) executeAttachedCommand(effect *attachmentEffect, request ports.CommandRequest) ports.CommandResult {
238- result := ports.CommandResult{RequestID: request.RequestID}
239- if request.Version != ports.ProtocolVersion {
240- result.Code = ports.ErrInvalidCommandArgs
237+func (d *Daemon) executeAttachedCommand(effect *attachmentEffect, request protocol.CommandRequest) protocol.CommandResult {
238+ result := protocol.CommandResult{RequestID: request.RequestID}
239+ if request.Version != protocol.Version {
240+ result.Code = protocol.ErrInvalidCommandArgs
241241 result.Text = "unsupported command protocol version"
242242 return result
243243 }
244244 if !request.Attached {
245- result.Code = ports.ErrInvalidCommandArgs
245+ result.Code = protocol.ErrInvalidCommandArgs
246246 result.Text = "attached command flag is required"
247247 return result
248248 }
249249 if request.RequestID == 0 {
250- result.Code = ports.ErrInvalidCommandArgs
250+ result.Code = protocol.ErrInvalidCommandArgs
251251 result.Text = "command request id is required"
252252 return result
253253 }
254254 if request.Self {
255- result.Code = ports.ErrInvalidCommandArgs
255+ result.Code = protocol.ErrInvalidCommandArgs
256256 result.Text = "attached commands cannot target themselves"
257257 return result
258258 }
259259 if request.TargetSession != "" || request.TargetTab != "" || request.TargetPane != "" {
260- result.Code = ports.ErrInvalidCommandArgs
260+ result.Code = protocol.ErrInvalidCommandArgs
261261 result.Text = "attached commands cannot override their active session target"
262262 return result
263263 }
264264 sess := effect.sess
265265 if sess == nil || !effect.current() {
266- result.Code = ports.ErrNoSuchTarget
266+ result.Code = protocol.ErrNoSuchTarget
267267 result.Text = "attached session is no longer active"
268268 return result
269269 }
270270 cmd, ok := command.BySlug(request.Slug)
271271 if !ok {
272- result.Code = ports.ErrUnknownCommand
272+ result.Code = protocol.ErrUnknownCommand
273273 result.Text = "unknown command: " + request.Slug
274274 return result
275275 }
276276 if !cmd.PaletteVisible {
277- result.Code = ports.ErrNotScriptable
277+ result.Code = protocol.ErrNotScriptable
278278 result.Text = request.Slug + " is not available in the palette"
279279 return result
280280 }
281281 if cmd.Run == nil {
282- result.Code = ports.ErrNotScriptable
282+ result.Code = protocol.ErrNotScriptable
283283 result.Text = request.Slug + " is not scriptable"
284284 return result
285285 }
@@ -293,11 +293,11 @@ func (d *Daemon) executeAttachedCommand(effect *attachmentEffect, request ports.
293293 return result
294294 }
295295 if errors.Is(err, command.ErrInvalidArguments) {
296- result.Code = ports.ErrInvalidCommandArgs
296+ result.Code = protocol.ErrInvalidCommandArgs
297297 result.Text = "usage: " + cmd.Usage
298298 return result
299299 }
300- result.Code = ports.ErrInternal
300+ result.Code = protocol.ErrInternal
301301 result.Text = err.Error()
302302 return result
303303 }
M internal/usecase/daemon/client_transport_errors.go +2 -1
@@ -2,6 +2,7 @@ package daemon
22
33 import (
44 "github.com/bnema/vev/internal/ports"
5+ "github.com/bnema/vev/internal/protocol"
56 )
67
78 // clientGone detaches ac if it is still the session's current client. The
@@ -110,7 +111,7 @@ func (d *Daemon) finishClientGone(sess *session, ac *attachedClient, failed port
110111 // (the client is actively awaiting it), but deadline-bounded so a
111112 // wedged client cannot pin this conn handler and hang Serve's
112113 // connWg.Wait.
113- d.boundedSend(ac, frameDetached(ports.ReasonDetach))
114+ d.boundedSend(ac, frameDetached(protocol.ReasonDetach))
114115 }
115116 _ = ac.closeCapturedTransport(oldTr)
116117 d.log.Info("client detached", "session", name, "explicit", explicit)
M internal/usecase/daemon/clipboard_test.go +1 -1
@@ -176,7 +176,7 @@ func TestKillSessionRemovesClipboardTempFiles(t *testing.T) {
176176 path := string(injected)
177177 require.FileExists(t, path)
178178
179- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))
179+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
180180 require.NoFileExists(t, path)
181181 }
182182
M internal/usecase/daemon/coderabbit_regression_test.go +24 -23
@@ -14,6 +14,7 @@ import (
1414 "github.com/bnema/vev/internal/domain"
1515 "github.com/bnema/vev/internal/ports"
1616 portsmocks "github.com/bnema/vev/internal/ports/mocks"
17+ "github.com/bnema/vev/internal/protocol"
1718 "github.com/bnema/vev/internal/usecase/picker"
1819 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"
1920 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"
@@ -40,7 +41,7 @@ func TestRestoreAmbiguousBadVersionLoadFailurePreservesCheckpoint(t *testing.T)
4041 d.mu.Lock()
4142 entry := d.inactive[record.Name]
4243 d.mu.Unlock()
43- require.Equal(t, ports.SessionBroken, entry.state)
44+ require.Equal(t, protocol.SessionBroken, entry.state)
4445 require.Equal(t, persisted, entry.record)
4546 }
4647
@@ -62,9 +63,9 @@ func TestRestoreTransientLoadFailureBecomesDiscardable(t *testing.T) {
6263 d.mu.Lock()
6364 entry := d.inactive[record.Name]
6465 d.mu.Unlock()
65- require.Equal(t, ports.SessionBroken, entry.state)
66+ require.Equal(t, protocol.SessionBroken, entry.state)
6667 require.Equal(t, persisted, entry.record)
67- require.Equal(t, ports.SessionBroken, listSessions(t, d).Sessions[0].State)
68+ require.Equal(t, protocol.SessionBroken, listSessions(t, d).Sessions[0].State)
6869 select {
6970 case <-entry.restoreDone:
7071 default:
@@ -122,14 +123,14 @@ func TestRestoreFailureCannotOverwriteDiscardReplacement(t *testing.T) {
122123 entry := d.inactive[record.Name]
123124 d.mu.Unlock()
124125 require.Equal(t, persisted, entry.record)
125- require.Equal(t, ports.SessionDown, entry.state)
126+ require.Equal(t, protocol.SessionDown, entry.state)
126127 }
127128
128129 func TestStaleIncompatibleRestoreCannotOverwriteNewerAuthority(t *testing.T) {
129130 tests := []struct {
130131 name string
131132 replacement func(domain.CatalogueRecord) domain.CatalogueRecord
132- state ports.SessionState
133+ state protocol.SessionState
133134 }{
134135 {
135136 name: "newer checkpoint",
@@ -140,7 +141,7 @@ func TestStaleIncompatibleRestoreCannotOverwriteNewerAuthority(t *testing.T) {
140141 record.Committed = &ref
141142 return record
142143 },
143- state: ports.SessionDown,
144+ state: protocol.SessionDown,
144145 },
145146 {
146147 name: "newer incarnation",
@@ -148,7 +149,7 @@ func TestStaleIncompatibleRestoreCannotOverwriteNewerAuthority(t *testing.T) {
148149 record.IncarnationID = domain.IncarnationID{9}
149150 return record
150151 },
151- state: ports.SessionDown,
152+ state: protocol.SessionDown,
152153 },
153154 {
154155 name: "degraded authority",
@@ -156,7 +157,7 @@ func TestStaleIncompatibleRestoreCannotOverwriteNewerAuthority(t *testing.T) {
156157 record.DegradedReason = "newer restore decision"
157158 return record
158159 },
159- state: ports.SessionBroken,
160+ state: protocol.SessionBroken,
160161 },
161162 }
162163 for _, tt := range tests {
@@ -225,7 +226,7 @@ func TestCreateSessionRechecksShutdownAfterCatalogueRead(t *testing.T) {
225226 d := newTestDaemon(t, nil, stubClock{})
226227 d.catalogue = catalogue
227228 d.persistEnabled = true
228- d.inactive[record.Name] = inactiveSessionFromRecord(record, ports.SessionDown, make(chan struct{}))
229+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionDown, make(chan struct{}))
229230
230231 result := make(chan error, 1)
231232 go func() {
@@ -276,7 +277,7 @@ func TestResumeStoppedSessionRejectsReplacementDuringCatalogueRead(t *testing.T)
276277 d.catalogue = catalogue
277278 d.persistEnabled = true
278279 d.recovery = recoveryusecase.NewCoordinator(catalogue, noOpSnapshotRepository{}, nil)
279- d.inactive[record.Name] = inactiveSessionFromRecord(record, ports.SessionDown, nil)
280+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
280281
281282 target := picker.Target{
282283 Name: record.Name,
@@ -294,7 +295,7 @@ func TestResumeStoppedSessionRejectsReplacementDuringCatalogueRead(t *testing.T)
294295 replacement.UpdatedAt = replacement.CreatedAt
295296 require.NoError(t, catalogue.Replace(replacement.Name, replacement))
296297 d.mu.Lock()
297- d.inactive[replacement.Name] = inactiveSessionFromRecord(replacement, ports.SessionDown, nil)
298+ d.inactive[replacement.Name] = inactiveSessionFromRecord(replacement, protocol.SessionDown, nil)
298299 d.mu.Unlock()
299300 close(catalogue.release)
300301
@@ -322,7 +323,7 @@ func TestRemoteInactiveRouteRejectsReplacementDuringCatalogueRead(t *testing.T)
322323 d.catalogue = catalogue
323324 d.persistEnabled = true
324325 d.recovery = recoveryusecase.NewCoordinator(catalogue, noOpSnapshotRepository{}, nil)
325- d.inactive[record.Name] = inactiveSessionFromRecord(record, ports.SessionDown, nil)
326+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
326327
327328 target := domain.RemoteSessionTarget{
328329 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: record.IncarnationID,
@@ -331,10 +332,10 @@ func TestRemoteInactiveRouteRejectsReplacementDuringCatalogueRead(t *testing.T)
331332 transport, _ := newCapturingTransport(t)
332333 result := make(chan error, 1)
333334 go func() {
334- _, _, err := d.routeWithContext(context.Background(), ports.Hello{
335- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: record.Name,
335+ _, _, err := d.routeWithContext(context.Background(), protocol.Hello{
336+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: record.Name,
336337 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,
337- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
338+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
338339 }, transport)
339340 result <- err
340341 }()
@@ -346,13 +347,13 @@ func TestRemoteInactiveRouteRejectsReplacementDuringCatalogueRead(t *testing.T)
346347 replacement.UpdatedAt = replacement.CreatedAt
347348 require.NoError(t, catalogue.Replace(replacement.Name, replacement))
348349 d.mu.Lock()
349- d.inactive[replacement.Name] = inactiveSessionFromRecord(replacement, ports.SessionDown, nil)
350+ d.inactive[replacement.Name] = inactiveSessionFromRecord(replacement, protocol.SessionDown, nil)
350351 d.mu.Unlock()
351352 close(catalogue.release)
352353
353354 var protocolErr *protoErr
354355 require.ErrorAs(t, <-result, &protocolErr)
355- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
356+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
356357 require.Empty(t, catalogue.MetadataUpdates())
357358 d.mu.Lock()
358359 defer d.mu.Unlock()
@@ -411,7 +412,7 @@ func TestRemoteInactiveRouteRejectsTabAuthorityChangeDuringCatalogueRead(t *test
411412 d.catalogue = catalogue
412413 d.persistEnabled = true
413414 d.recovery = recoveryusecase.NewCoordinator(catalogue, noOpSnapshotRepository{}, nil)
414- d.inactive[record.Name] = inactiveSessionFromRecord(record, ports.SessionDown, nil)
415+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
415416
416417 target := test.target
417418 target.Endpoint = "remote"
@@ -421,10 +422,10 @@ func TestRemoteInactiveRouteRejectsTabAuthorityChangeDuringCatalogueRead(t *test
421422 transport, _ := newCapturingTransport(t)
422423 result := make(chan error, 1)
423424 go func() {
424- _, _, err := d.routeWithContext(context.Background(), ports.Hello{
425- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: record.Name,
425+ _, _, err := d.routeWithContext(context.Background(), protocol.Hello{
426+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: record.Name,
426427 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,
427- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
428+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
428429 }, transport)
429430 result <- err
430431 }()
@@ -438,14 +439,14 @@ func TestRemoteInactiveRouteRejectsTabAuthorityChangeDuringCatalogueRead(t *test
438439 require.NoError(t, catalogue.Replace(replacement.Name, replacement))
439440 if test.updateMap {
440441 d.mu.Lock()
441- d.inactive[replacement.Name] = inactiveSessionFromRecord(replacement, ports.SessionDown, nil)
442+ d.inactive[replacement.Name] = inactiveSessionFromRecord(replacement, protocol.SessionDown, nil)
442443 d.mu.Unlock()
443444 }
444445 close(catalogue.release)
445446
446447 var protocolErr *protoErr
447448 require.ErrorAs(t, <-result, &protocolErr)
448- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
449+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
449450 require.Empty(t, catalogue.MetadataUpdates())
450451 d.mu.Lock()
451452 defer d.mu.Unlock()
M internal/usecase/daemon/command_tracker.go +9 -8
@@ -7,6 +7,7 @@ import (
77 "time"
88
99 "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
1011 )
1112
1213 const CommandRequestTimeout = 10 * time.Second
@@ -18,7 +19,7 @@ var (
1819
1920 // CommandRequestOutcome is the result delivered for one tracked request.
2021 type CommandRequestOutcome struct {
21- Result ports.CommandResult
22+ Result protocol.CommandResult
2223 Err error
2324 }
2425
@@ -82,21 +83,21 @@ func (t *CommandRequestTracker) trackLocked(requestID, generation uint64) <-chan
8283
8384 // Wait waits without holding the tracker or connection sender lock. Timeout
8485 // and cancellation remove only the matching request.
85-func (t *CommandRequestTracker) Wait(ctx context.Context, clock ports.Clock, requestID, generation uint64, outcome <-chan CommandRequestOutcome) (ports.CommandResult, error) {
86+func (t *CommandRequestTracker) Wait(ctx context.Context, clock ports.Clock, requestID, generation uint64, outcome <-chan CommandRequestOutcome) (protocol.CommandResult, error) {
8687 if t == nil || clock == nil {
87- return ports.CommandResult{}, ErrCommandRequestUnavailable
88+ return protocol.CommandResult{}, ErrCommandRequestUnavailable
8889 }
8990 if ctx == nil {
9091 ctx = context.Background()
9192 }
9293 if err := ctx.Err(); err != nil {
9394 t.Remove(requestID, generation)
94- return ports.CommandResult{}, err
95+ return protocol.CommandResult{}, err
9596 }
9697 timer := clock.NewTimer(CommandRequestTimeout)
9798 if timer == nil {
9899 t.Remove(requestID, generation)
99- return ports.CommandResult{}, ErrCommandRequestTimeout
100+ return protocol.CommandResult{}, ErrCommandRequestTimeout
100101 }
101102 defer timer.Stop()
102103 select {
@@ -104,10 +105,10 @@ func (t *CommandRequestTracker) Wait(ctx context.Context, clock ports.Clock, req
104105 return completed.Result, completed.Err
105106 case <-ctx.Done():
106107 t.Remove(requestID, generation)
107- return ports.CommandResult{}, ctx.Err()
108+ return protocol.CommandResult{}, ctx.Err()
108109 case <-timer.C():
109110 t.Remove(requestID, generation)
110- return ports.CommandResult{}, ErrCommandRequestTimeout
111+ return protocol.CommandResult{}, ErrCommandRequestTimeout
111112 }
112113 }
113114
@@ -126,7 +127,7 @@ func (t *CommandRequestTracker) Remove(requestID, generation uint64) {
126127
127128 // Complete accepts only the exact request generation. Unknown, late, and old
128129 // generation results are safe no-ops.
129-func (t *CommandRequestTracker) Complete(generation uint64, result ports.CommandResult) {
130+func (t *CommandRequestTracker) Complete(generation uint64, result protocol.CommandResult) {
130131 t.finish(result.RequestID, generation, CommandRequestOutcome{Result: result})
131132 }
132133
M internal/usecase/daemon/command_tracker_test.go +3 -2
@@ -10,6 +10,7 @@ import (
1010 "github.com/stretchr/testify/require"
1111
1212 "github.com/bnema/vev/internal/ports"
13+ "github.com/bnema/vev/internal/protocol"
1314 )
1415
1516 type commandTrackerTestTimer struct {
@@ -60,13 +61,13 @@ func TestCommandRequestTrackerTimeoutAndGenerationIsolation(t *testing.T) {
6061 // old connection must not complete the new request.
6162 secondOutcome, ok := tracker.Track(requestID, 8)
6263 require.True(t, ok)
63- tracker.Complete(4, ports.CommandResult{RequestID: requestID, OK: true})
64+ tracker.Complete(4, protocol.CommandResult{RequestID: requestID, OK: true})
6465 select {
6566 case <-secondOutcome:
6667 t.Fatal("late result from the old generation completed a newer request")
6768 default:
6869 }
69- tracker.Complete(8, ports.CommandResult{RequestID: requestID, OK: true})
70+ tracker.Complete(8, protocol.CommandResult{RequestID: requestID, OK: true})
7071 result := <-secondOutcome
7172 require.True(t, result.Result.OK)
7273
M internal/usecase/daemon/control.go +38 -37
@@ -12,6 +12,7 @@ import (
1212
1313 "github.com/bnema/vev/internal/domain"
1414 "github.com/bnema/vev/internal/ports"
15+ "github.com/bnema/vev/internal/protocol"
1516 "github.com/bnema/vev/internal/usecase/command"
1617 "github.com/bnema/vev/internal/usecase/layout"
1718 )
@@ -21,17 +22,17 @@ import (
2122 func (d *Daemon) handleCommand(tr ports.Transport, f ports.Frame) error {
2223 defer func() { _ = tr.Close() }()
2324
24- if version, ok := ports.PeekCommandVersion(f.Payload); !ok || version != ports.ProtocolVersion {
25- return d.sendCommandResult(tr, ports.CommandResult{Code: ports.ErrVersionMismatch, Text: "protocol version mismatch"})
25+ if version, ok := ports.PeekCommandVersion(f.Payload); !ok || version != protocol.Version {
26+ return d.sendCommandResult(tr, protocol.CommandResult{Code: protocol.ErrVersionMismatch, Text: "protocol version mismatch"})
2627 }
2728 request, err := ports.UnmarshalCommandRequest(f.Payload)
2829 if err != nil {
29- return d.sendCommandResult(tr, ports.CommandResult{Code: ports.ErrInternal, Text: "malformed command request"})
30+ return d.sendCommandResult(tr, protocol.CommandResult{Code: protocol.ErrInternal, Text: "malformed command request"})
3031 }
3132 if request.Attached {
32- return d.sendCommandResult(tr, ports.CommandResult{
33+ return d.sendCommandResult(tr, protocol.CommandResult{
3334 RequestID: request.RequestID,
34- Code: ports.ErrNotScriptable,
35+ Code: protocol.ErrNotScriptable,
3536 Text: "attached command relay is not enabled",
3637 })
3738 }
@@ -56,9 +57,9 @@ func (d *Daemon) handleCommand(tr ports.Transport, f ports.Frame) error {
5657 }
5758 result, waitErr := tracker.Wait(commandCtx, commandClock, request.RequestID, generation, outcome)
5859 if waitErr != nil {
59- return d.sendCommandResult(tr, ports.CommandResult{
60+ return d.sendCommandResult(tr, protocol.CommandResult{
6061 RequestID: request.RequestID,
61- Code: ports.ErrInternal,
62+ Code: protocol.ErrInternal,
6263 Text: waitErr.Error(),
6364 })
6465 }
@@ -67,7 +68,7 @@ func (d *Daemon) handleCommand(tr ports.Transport, f ports.Frame) error {
6768
6869 // sendCommandResult gives one-shot control responses the same observable
6970 // transport-failure behavior regardless of which validation path produced it.
70-func (d *Daemon) sendCommandResult(tr ports.Transport, result ports.CommandResult) error {
71+func (d *Daemon) sendCommandResult(tr ports.Transport, result protocol.CommandResult) error {
7172 if err := d.boundedControlSend(tr, frameCommandResult(result)); err != nil {
7273 d.log.Warn("command response send failed", "err", err)
7374 return err
@@ -86,17 +87,17 @@ func (d *Daemon) boundedControlSend(tr ports.Transport, frame ports.Frame) error
8687 return err
8788 }
8889
89-func frameCommandResult(result ports.CommandResult) ports.Frame {
90+func frameCommandResult(result protocol.CommandResult) ports.Frame {
9091 return ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(result)}
9192 }
9293
93-func (d *Daemon) dispatchCommand(ctx context.Context, request ports.CommandRequest) ports.CommandResult {
94+func (d *Daemon) dispatchCommand(ctx context.Context, request protocol.CommandRequest) protocol.CommandResult {
9495 cmd, ok := command.BySlug(request.Slug)
9596 if !ok {
96- return commandFailure(ports.ErrUnknownCommand, "unknown command: "+request.Slug)
97+ return commandFailure(protocol.ErrUnknownCommand, "unknown command: "+request.Slug)
9798 }
9899 if !cmd.Scriptable || cmd.Control == nil {
99- return commandFailure(ports.ErrNotScriptable, request.Slug+" requires an attached client")
100+ return commandFailure(protocol.ErrNotScriptable, request.Slug+" requires an attached client")
100101 }
101102 if cmd.Target == command.TargetNone && !request.Self {
102103 return d.runControl(cmd, controlExec{ctx: ctx, d: d, recoveryName: request.TargetSession}, request)
@@ -122,7 +123,7 @@ func (d *Daemon) dispatchCommand(ctx context.Context, request ports.CommandReque
122123 for {
123124 select {
124125 case <-ctx.Done():
125- return commandFailure(ports.ErrInternal, ctx.Err().Error())
126+ return commandFailure(protocol.ErrInternal, ctx.Err().Error())
126127 default:
127128 }
128129 if sess.dispatchMu.TryLock() {
@@ -139,51 +140,51 @@ func (d *Daemon) dispatchCommand(ctx context.Context, request ports.CommandReque
139140 return d.runControl(cmd, controlExec{ctx: ctx, d: d, sess: sess, tab: tb, target: daemonActionTarget{session: sess, tab: tb, pane: pane}}, request)
140141 }
141142
142-func (d *Daemon) runControl(cmd command.Command, exec controlExec, request ports.CommandRequest) ports.CommandResult {
143+func (d *Daemon) runControl(cmd command.Command, exec controlExec, request protocol.CommandRequest) protocol.CommandResult {
143144 result, err := cmd.Control(exec, request.Args, command.ControlOptions{JSON: request.JSON})
144145 if err == nil {
145- return ports.CommandResult{OK: true, Output: result.Output}
146+ return protocol.CommandResult{OK: true, Output: result.Output}
146147 }
147148 switch {
148149 case errors.Is(err, command.ErrInvalidArguments), errors.Is(err, errSessionNameRequired), errors.Is(err, domain.ErrInvalidSessionName):
149- return commandFailure(ports.ErrInvalidCommandArgs, "usage: "+cmd.Usage)
150+ return commandFailure(protocol.ErrInvalidCommandArgs, "usage: "+cmd.Usage)
150151 case errors.Is(err, errSessionNameInUse):
151- return commandFailure(ports.ErrNameTaken, err.Error())
152+ return commandFailure(protocol.ErrNameTaken, err.Error())
152153 case isMoveCommandError(err):
153154 return moveCommandFailure(err)
154155 case errors.Is(err, layout.ErrNotInSplit):
155- return commandFailure(ports.ErrNoSuchTarget, "pane is not in a split")
156+ return commandFailure(protocol.ErrNoSuchTarget, "pane is not in a split")
156157 case errors.Is(err, layout.ErrTooSmall):
157- return commandFailure(ports.ErrNoSuchTarget, "pane cannot be resized further")
158+ return commandFailure(protocol.ErrNoSuchTarget, "pane cannot be resized further")
158159 default:
159- return commandFailure(ports.ErrInternal, err.Error())
160+ return commandFailure(protocol.ErrInternal, err.Error())
160161 }
161162 }
162163
163-func commandFailure(code uint16, text string) ports.CommandResult {
164- return ports.CommandResult{Code: code, Text: text}
164+func commandFailure(code uint16, text string) protocol.CommandResult {
165+ return protocol.CommandResult{Code: code, Text: text}
165166 }
166167
167168 // resolveTargetSession applies explicit-name, stable-ID, then unique-session
168169 // resolution. A name paired with complete stable IDs is always advisory because
169170 // a process keeps its original VEV session component after relocation.
170-func (d *Daemon) resolveTargetSession(request ports.CommandRequest) (*session, uint16, string) {
171+func (d *Daemon) resolveTargetSession(request protocol.CommandRequest) (*session, uint16, string) {
171172 d.mu.Lock()
172173 defer d.mu.Unlock()
173174
174175 named := d.findByNameLocked(request.TargetSession)
175176 if request.Self && (request.TargetTab == "" || request.TargetPane == "") {
176- return nil, ports.ErrNoSuchTarget, "--self requires target tab and pane IDs"
177+ return nil, protocol.ErrNoSuchTarget, "--self requires target tab and pane IDs"
177178 }
178179 if request.TargetSession != "" && request.TargetTab == "" && request.TargetPane == "" {
179180 if named == nil {
180- return nil, ports.ErrNoSuchTarget, "no such session: " + request.TargetSession
181+ return nil, protocol.ErrNoSuchTarget, "no such session: " + request.TargetSession
181182 }
182183 return named, 0, ""
183184 }
184185 if request.TargetTab != "" || request.TargetPane != "" {
185186 if request.TargetTab == "" || request.TargetPane == "" {
186- return nil, ports.ErrNoSuchTarget, "target tab and pane IDs must be provided together"
187+ return nil, protocol.ErrNoSuchTarget, "target tab and pane IDs must be provided together"
187188 }
188189 var match *session
189190 for _, sess := range d.sessions {
@@ -191,21 +192,21 @@ func (d *Daemon) resolveTargetSession(request ports.CommandRequest) (*session, u
191192 continue
192193 }
193194 if match != nil {
194- return nil, ports.ErrAmbiguousTarget, "target tab and pane IDs are ambiguous"
195+ return nil, protocol.ErrAmbiguousTarget, "target tab and pane IDs are ambiguous"
195196 }
196197 match = sess
197198 }
198199 if match != nil {
199200 return match, 0, ""
200201 }
201- return nil, ports.ErrNoSuchTarget, "no live session contains the target tab/pane"
202+ return nil, protocol.ErrNoSuchTarget, "no live session contains the target tab/pane"
202203 }
203204 locals := sessionsSnapshot(d.sessions)
204205 if len(locals) == 0 {
205- return nil, ports.ErrNoSuchTarget, "no live sessions"
206+ return nil, protocol.ErrNoSuchTarget, "no live sessions"
206207 }
207208 if len(locals) != 1 {
208- return nil, ports.ErrAmbiguousTarget, "several sessions are live; use -s <session> or run from inside a pane"
209+ return nil, protocol.ErrAmbiguousTarget, "several sessions are live; use -s <session> or run from inside a pane"
209210 }
210211 return locals[0], 0, ""
211212 }
@@ -226,7 +227,7 @@ func (s *session) containsStableIDs(tabID, paneID string) bool {
226227 return false
227228 }
228229
229-func resolveControlTarget(sess *session, kind command.TargetKind, request ports.CommandRequest) (*tab, *pane, uint16, string) {
230+func resolveControlTarget(sess *session, kind command.TargetKind, request protocol.CommandRequest) (*tab, *pane, uint16, string) {
230231 sess.mu.Lock()
231232 tabs := append([]*tab(nil), sess.tabs...)
232233 sess.mu.Unlock()
@@ -240,14 +241,14 @@ func resolveControlTarget(sess *session, kind command.TargetKind, request ports.
240241 target := paneByStableIDLocked(tb, request.TargetPane)
241242 tb.mu.Unlock()
242243 if target == nil {
243- return nil, nil, ports.ErrNoSuchTarget, "target pane does not belong to target tab"
244+ return nil, nil, protocol.ErrNoSuchTarget, "target pane does not belong to target tab"
244245 }
245246 return tb, target, 0, ""
246247 }
247- return nil, nil, ports.ErrNoSuchTarget, "no such target tab"
248+ return nil, nil, protocol.ErrNoSuchTarget, "no such target tab"
248249 }
249250 if len(tabs) == 0 {
250- return nil, nil, ports.ErrNoSuchTarget, "target session has no tabs"
251+ return nil, nil, protocol.ErrNoSuchTarget, "target session has no tabs"
251252 }
252253 tb := tabs[0]
253254 if kind != command.TargetPane {
@@ -257,7 +258,7 @@ func resolveControlTarget(sess *session, kind command.TargetKind, request ports.
257258 target := tb.focusedPane()
258259 tb.mu.Unlock()
259260 if target == nil {
260- return nil, nil, ports.ErrNoSuchTarget, "no such target pane"
261+ return nil, nil, protocol.ErrNoSuchTarget, "no such target pane"
261262 }
262263 return tb, target, 0, ""
263264 }
@@ -657,7 +658,7 @@ func (e controlExec) SessionRecovery(action string) (string, error) {
657658 return "", err
658659 }
659660 if ok {
660- e.d.setStoppedRecovery(record, ports.SessionDown)
661+ e.d.setStoppedRecovery(record, protocol.SessionDown)
661662 }
662663 return "", nil
663664 }
@@ -772,7 +773,7 @@ func (e controlExec) RemoteCatalog(asJSON bool) (string, error) {
772773 return rows[i].LifecycleID.String() < rows[j].LifecycleID.String()
773774 })
774775 catalog := ports.RemoteCatalog{
775- ProtocolVersion: ports.ProtocolVersion,
776+ ProtocolVersion: protocol.Version,
776777 SchemaVersion: ports.RemoteCatalogSchemaVersion,
777778 Sessions: rows,
778779 }
M internal/usecase/daemon/control_test.go +113 -112
@@ -16,6 +16,7 @@ import (
1616 "github.com/bnema/vev/internal/domain"
1717 "github.com/bnema/vev/internal/ports"
1818 portsmocks "github.com/bnema/vev/internal/ports/mocks"
19+ "github.com/bnema/vev/internal/protocol"
1920 "github.com/bnema/vev/internal/usecase/command"
2021 "github.com/bnema/vev/internal/usecase/layout"
2122 "github.com/stretchr/testify/require"
@@ -69,7 +70,7 @@ func TestConsumeOrExpelControlSelfTargetsNonFocusedPane(t *testing.T) {
6970 targetStableID := h.panes["pane-3"].stableID
7071 h.tab.mu.Unlock()
7172
72- result := sendCommand(t, h.daemon, ports.CommandRequest{
73+ result := sendCommand(t, h.daemon, protocol.CommandRequest{
7374 Slug: "consume-or-expel-pane-left",
7475 Self: true,
7576 TargetSession: "work",
@@ -100,7 +101,7 @@ func TestConsumeOrExpelControlEdgeNoopReturnsOKAndPreservesFocus(t *testing.T) {
100101 h.session.installRenderCoordinator(rc)
101102 before := h.snapshot()
102103
103- result := sendCommand(t, h.daemon, ports.CommandRequest{
104+ result := sendCommand(t, h.daemon, protocol.CommandRequest{
104105 Slug: "consume-or-expel-pane-left",
105106 Self: true,
106107 TargetSession: "work",
@@ -130,7 +131,7 @@ func TestHandleCommandTimesOutWithRequestGeneration(t *testing.T) {
130131 clock := &signalClock{timers: make(chan *signalTimer, 1)}
131132 d := newTestDaemon(t, factory, clock)
132133 addControlSession(d, "work", "t_work", "p_work")
133- frame := commandFrame(t, ports.CommandRequest{
134+ frame := commandFrame(t, protocol.CommandRequest{
134135 RequestID: 17, Slug: "new-tab", TargetSession: "work",
135136 })
136137 tr, sends, releaseConn := newConn(t, frame)
@@ -145,7 +146,7 @@ func TestHandleCommandTimesOutWithRequestGeneration(t *testing.T) {
145146 timer.ch <- time.Time{}
146147 result := awaitCommandResult(t, sends)
147148 require.Equal(t, uint64(17), result.RequestID)
148- require.Equal(t, ports.ErrInternal, result.Code)
149+ require.Equal(t, protocol.ErrInternal, result.Code)
149150 require.Equal(t, ErrCommandRequestTimeout.Error(), result.Text)
150151 require.NoError(t, <-done)
151152 close(factory.release)
@@ -155,7 +156,7 @@ func TestHandleCommandResponseSendTimeoutClosesTransport(t *testing.T) {
155156 clock := &signalClock{timers: make(chan *signalTimer, 1)}
156157 d := newTestDaemon(t, nil, clock)
157158 tr := newBlockingControlSendTransport()
158- frame := commandFrame(t, ports.CommandRequest{RequestID: 41, Attached: true})
159+ frame := commandFrame(t, protocol.CommandRequest{RequestID: 41, Attached: true})
159160 done := make(chan error, 1)
160161 go func() { done <- d.handleCommand(tr, frame) }()
161162
@@ -182,7 +183,7 @@ func TestHandleCommandAttachedRejectionReturnsSendFailure(t *testing.T) {
182183 sendErr := errors.New("command result send failed")
183184 tr := &commandSendErrorTransport{err: sendErr}
184185
185- err := d.handleCommand(tr, commandFrame(t, ports.CommandRequest{
186+ err := d.handleCommand(tr, commandFrame(t, protocol.CommandRequest{
186187 RequestID: 41,
187188 Attached: true,
188189 }))
@@ -195,7 +196,7 @@ func TestHandleCommandAttachedRejectionReturnsSendFailure(t *testing.T) {
195196 func TestHandleCommandRejectsVersionBeforeDecodeOrDispatch(t *testing.T) {
196197 // The version prefix is valid and mismatched, while the remainder is not a
197198 // decodable request. The daemon must still return ErrVersionMismatch.
198- frame := ports.Frame{Type: ports.MsgCommand, Payload: []byte{0, byte(ports.ProtocolVersion + 1)}}
199+ frame := ports.Frame{Type: ports.MsgCommand, Payload: []byte{0, byte(protocol.Version + 1)}}
199200 tr, sends, _ := newConn(t, frame)
200201
201202 d := newTestDaemon(t, nil, stubClock{})
@@ -203,12 +204,12 @@ func TestHandleCommandRejectsVersionBeforeDecodeOrDispatch(t *testing.T) {
203204
204205 result := awaitCommandResult(t, sends)
205206 require.False(t, result.OK)
206- require.Equal(t, ports.ErrVersionMismatch, result.Code)
207+ require.Equal(t, protocol.ErrVersionMismatch, result.Code)
207208 }
208209
209210 func TestHandleConnRoutesCommand(t *testing.T) {
210211 d := newTestDaemon(t, nil, stubClock{})
211- request := ports.CommandRequest{Version: ports.ProtocolVersion, Slug: "list-sessions"}
212+ request := protocol.CommandRequest{Version: protocol.Version, Slug: "list-sessions"}
212213 payload, err := ports.MarshalCommandRequest(request)
213214 require.NoError(t, err)
214215 frame := ports.Frame{Type: ports.MsgCommand, Payload: payload}
@@ -224,35 +225,35 @@ func TestHandleCommandDispatchAndTargetErrors(t *testing.T) {
224225 tests := []struct {
225226 name string
226227 arrange func(*Daemon)
227- request ports.CommandRequest
228+ request protocol.CommandRequest
228229 code uint16
229230 }{
230- {name: "unknown slug", request: ports.CommandRequest{Slug: "no-such"}, code: ports.ErrUnknownCommand},
231- {name: "non-scriptable command", request: ports.CommandRequest{Slug: "session-picker"}, code: ports.ErrNotScriptable},
232- {name: "no sessions", request: ports.CommandRequest{Slug: "split-right"}, code: ports.ErrNoSuchTarget},
231+ {name: "unknown slug", request: protocol.CommandRequest{Slug: "no-such"}, code: protocol.ErrUnknownCommand},
232+ {name: "non-scriptable command", request: protocol.CommandRequest{Slug: "session-picker"}, code: protocol.ErrNotScriptable},
233+ {name: "no sessions", request: protocol.CommandRequest{Slug: "split-right"}, code: protocol.ErrNoSuchTarget},
233234 {name: "ambiguous sessions", arrange: func(d *Daemon) {
234235 addControlSession(d, "one", "t_one", "p_one")
235236 addControlSession(d, "two", "t_two", "p_two")
236- }, request: ports.CommandRequest{Slug: "split-right"}, code: ports.ErrAmbiguousTarget},
237+ }, request: protocol.CommandRequest{Slug: "split-right"}, code: protocol.ErrAmbiguousTarget},
237238 {name: "missing explicit session", arrange: func(d *Daemon) {
238239 addControlSession(d, "work", "t_work", "p_work")
239- }, request: ports.CommandRequest{Slug: "new-tab", TargetSession: "missing"}, code: ports.ErrNoSuchTarget},
240+ }, request: protocol.CommandRequest{Slug: "new-tab", TargetSession: "missing"}, code: protocol.ErrNoSuchTarget},
240241 {name: "missing stable IDs", arrange: func(d *Daemon) {
241242 addControlSession(d, "work", "t_work", "p_work")
242- }, request: ports.CommandRequest{Slug: "split-right", TargetTab: "t_nope", TargetPane: "p_nope"}, code: ports.ErrNoSuchTarget},
243+ }, request: protocol.CommandRequest{Slug: "split-right", TargetTab: "t_nope", TargetPane: "p_nope"}, code: protocol.ErrNoSuchTarget},
243244 {name: "duplicate stable IDs are ambiguous", arrange: func(d *Daemon) {
244245 addControlSession(d, "one", "t_shared", "p_shared")
245246 addControlSession(d, "two", "t_shared", "p_shared")
246- }, request: ports.CommandRequest{Slug: "toast", Args: []string{"hello"}, TargetTab: "t_shared", TargetPane: "p_shared"}, code: ports.ErrAmbiguousTarget},
247+ }, request: protocol.CommandRequest{Slug: "toast", Args: []string{"hello"}, TargetTab: "t_shared", TargetPane: "p_shared"}, code: protocol.ErrAmbiguousTarget},
247248 {name: "self requires both IDs", arrange: func(d *Daemon) {
248249 addControlSession(d, "work", "t_work", "p_work")
249- }, request: ports.CommandRequest{Slug: "split-right", Self: true, TargetSession: "work", TargetTab: "t_work"}, code: ports.ErrNoSuchTarget},
250+ }, request: protocol.CommandRequest{Slug: "split-right", Self: true, TargetSession: "work", TargetTab: "t_work"}, code: protocol.ErrNoSuchTarget},
250251 {name: "self rejects pane from another tab", arrange: func(d *Daemon) {
251252 sess := addControlSession(d, "work", "t_work", "p_work")
252253 other := newTabWithStableID("t_other", "p_other", newQuietPTY(), domain.Size{Cols: 80, Rows: 22})
253254 other.ctx, other.cancel = context.WithCancel(d.serveCtx)
254255 sess.tabs = append(sess.tabs, other)
255- }, request: ports.CommandRequest{Slug: "split-right", Self: true, TargetSession: "work", TargetTab: "t_work", TargetPane: "p_other"}, code: ports.ErrNoSuchTarget},
256+ }, request: protocol.CommandRequest{Slug: "split-right", Self: true, TargetSession: "work", TargetTab: "t_work", TargetPane: "p_other"}, code: protocol.ErrNoSuchTarget},
256257 }
257258
258259 for _, tt := range tests {
@@ -272,7 +273,7 @@ func TestHandleCommandResolvesStaleSessionNameByStableIDs(t *testing.T) {
272273 d := newTestDaemon(t, nil, stubClock{})
273274 sess := addControlSession(d, "renamed", "t_work", "p_work")
274275
275- result := sendCommand(t, d, ports.CommandRequest{
276+ result := sendCommand(t, d, protocol.CommandRequest{
276277 Slug: "toast", Args: []string{"hello"},
277278 TargetSession: "old-name", TargetTab: "t_work", TargetPane: "p_work",
278279 })
@@ -292,7 +293,7 @@ func TestHandleCommandStableIDsResolveSessionWithoutSelectingTab(t *testing.T) {
292293 selectTestAttachmentTabLocked(sess, 0)
293294 sess.mu.Unlock()
294295
295- result := sendCommand(t, d, ports.CommandRequest{
296+ result := sendCommand(t, d, protocol.CommandRequest{
296297 Slug: "rename-tab", Args: []string{"targeted"}, TargetSession: "work",
297298 TargetTab: "t_second", TargetPane: "p_second",
298299 })
@@ -324,7 +325,7 @@ func TestHandleCommandStableIDsDoNotRedirectSplitFromCurrentFocus(t *testing.T)
324325 selectTestAttachmentTabLocked(sess, 0)
325326 sess.mu.Unlock()
326327
327- result := sendCommand(t, d, ports.CommandRequest{
328+ result := sendCommand(t, d, protocol.CommandRequest{
328329 Slug: "split-right", TargetSession: "work",
329330 TargetTab: "t_origin", TargetPane: "p_origin",
330331 })
@@ -361,7 +362,7 @@ func TestHandleCommandSelfTargetsNonActiveTabAndPane(t *testing.T) {
361362 selectTestAttachmentTabLocked(sess, 0)
362363 sess.mu.Unlock()
363364
364- rename := sendCommand(t, d, ports.CommandRequest{
365+ rename := sendCommand(t, d, protocol.CommandRequest{
365366 Slug: "rename-tab", Args: []string{"invoking"}, Self: true,
366367 TargetSession: "work", TargetTab: "t_invoking", TargetPane: "p_invoking",
367368 })
@@ -373,7 +374,7 @@ func TestHandleCommandSelfTargetsNonActiveTabAndPane(t *testing.T) {
373374 require.Equal(t, "invoking", invoking.name)
374375 invoking.mu.Unlock()
375376
376- split := sendCommand(t, d, ports.CommandRequest{
377+ split := sendCommand(t, d, protocol.CommandRequest{
377378 Slug: "split-right", Self: true,
378379 TargetSession: "work", TargetTab: "t_invoking", TargetPane: "p_invoking",
379380 })
@@ -387,7 +388,7 @@ func TestHandleCommandSelfTargetsNonActiveTabAndPane(t *testing.T) {
387388 beforeGeneration := invoking.layoutGeneration
388389 invoking.mu.Unlock()
389390
390- grow := sendCommand(t, d, ports.CommandRequest{
391+ grow := sendCommand(t, d, protocol.CommandRequest{
391392 Slug: "grow-pane-width", Self: true,
392393 TargetSession: "work", TargetTab: "t_invoking", TargetPane: "p_invoking",
393394 })
@@ -401,7 +402,7 @@ func TestHandleCommandSelfTargetsNonActiveTabAndPane(t *testing.T) {
401402 require.Len(t, active.panes, 1, "--self resize must not mutate the active tab")
402403 active.mu.Unlock()
403404
404- equalize := sendCommand(t, d, ports.CommandRequest{
405+ equalize := sendCommand(t, d, protocol.CommandRequest{
405406 Slug: "equalize-panes", Self: true,
406407 TargetSession: "work", TargetTab: "t_invoking", TargetPane: "p_invoking",
407408 })
@@ -417,10 +418,10 @@ func TestHandleCommandSelfTargetsNonActiveTabAndPane(t *testing.T) {
417418 func TestResizeControlOneShotsTargetDetachedSessions(t *testing.T) {
418419 targets := []struct {
419420 name string
420- request func() ports.CommandRequest
421+ request func() protocol.CommandRequest
421422 }{
422- {"named", func() ports.CommandRequest { return ports.CommandRequest{TargetSession: "work"} }},
423- {"unique", func() ports.CommandRequest { return ports.CommandRequest{} }},
423+ {"named", func() protocol.CommandRequest { return protocol.CommandRequest{TargetSession: "work"} }},
424+ {"unique", func() protocol.CommandRequest { return protocol.CommandRequest{} }},
424425 }
425426 for _, target := range targets {
426427 t.Run(target.name, func(t *testing.T) {
@@ -430,8 +431,8 @@ func TestResizeControlOneShotsTargetDetachedSessions(t *testing.T) {
430431 d := newTestDaemon(t, factory, stubClock{})
431432 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })
432433 sess := addControlSession(d, "work", "t_work", "p_work")
433- require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
434- require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-down", TargetSession: "work"}).OK)
434+ require.True(t, sendCommand(t, d, protocol.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
435+ require.True(t, sendCommand(t, d, protocol.CommandRequest{Slug: "split-down", TargetSession: "work"}).OK)
435436 tb := testAttachmentTab(sess)
436437 tb.mu.Lock()
437438 generation := tb.layoutGeneration
@@ -465,7 +466,7 @@ func TestResizeControlNonSelfVEVLocatorUsesActiveTarget(t *testing.T) {
465466 d := newTestDaemon(t, factory, stubClock{})
466467 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })
467468 sess := addControlSession(d, "work", "t_active", "p_active")
468- require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
469+ require.True(t, sendCommand(t, d, protocol.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
469470 active := sess.tabs[0]
470471 locator := newTabWithStableID("t_locator", "p_locator", newQuietPTY(), domain.Size{Cols: 80, Rows: 22})
471472 locator.ctx, locator.cancel = context.WithCancel(d.serveCtx)
@@ -477,7 +478,7 @@ func TestResizeControlNonSelfVEVLocatorUsesActiveTarget(t *testing.T) {
477478 before := active.layoutGeneration
478479 active.mu.Unlock()
479480
480- result := sendCommand(t, d, ports.CommandRequest{
481+ result := sendCommand(t, d, protocol.CommandRequest{
481482 Slug: "grow-pane-width", TargetSession: "work", TargetTab: "t_locator", TargetPane: "p_locator",
482483 })
483484 require.True(t, result.OK, result.Text)
@@ -497,16 +498,16 @@ func TestResizeControlTooSmallUsesStableFailure(t *testing.T) {
497498 d := newTestDaemon(t, factory, stubClock{})
498499 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })
499500 sess := addControlSession(d, "work", "t_work", "p_work")
500- require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
501+ require.True(t, sendCommand(t, d, protocol.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
501502 tb := testAttachmentTab(sess)
502503 tb.mu.Lock()
503504 tb.size.Cols = 41
504505 tb.bumpLayoutGenerationLocked()
505506 tb.mu.Unlock()
506507
507- result := sendCommand(t, d, ports.CommandRequest{Slug: "grow-pane-width", TargetSession: "work"})
508+ result := sendCommand(t, d, protocol.CommandRequest{Slug: "grow-pane-width", TargetSession: "work"})
508509 require.False(t, result.OK)
509- require.Equal(t, ports.ErrNoSuchTarget, result.Code)
510+ require.Equal(t, protocol.ErrNoSuchTarget, result.Code)
510511 require.Equal(t, "pane cannot be resized further", result.Text)
511512 }
512513
@@ -520,7 +521,7 @@ func TestHandleCommandSelfListPanesUsesInvokingTab(t *testing.T) {
520521 selectTestAttachmentTabLocked(sess, 0)
521522 sess.mu.Unlock()
522523
523- result := sendCommand(t, d, ports.CommandRequest{
524+ result := sendCommand(t, d, protocol.CommandRequest{
524525 Slug: "list-panes", Self: true, JSON: true,
525526 TargetSession: "work", TargetTab: "t_invoking", TargetPane: "p_invoking",
526527 })
@@ -533,12 +534,12 @@ func TestHandleCommandRenameSessionRejectsInvalidNameAsCommandArgs(t *testing.T)
533534 d := newTestDaemon(t, nil, stubClock{})
534535 addControlSession(d, "work", "t_work", "p_work")
535536
536- result := sendCommand(t, d, ports.CommandRequest{
537+ result := sendCommand(t, d, protocol.CommandRequest{
537538 Slug: "rename-session", Args: []string{"invalid name"}, TargetSession: "work",
538539 })
539540
540541 require.False(t, result.OK)
541- require.Equal(t, ports.ErrInvalidCommandArgs, result.Code)
542+ require.Equal(t, protocol.ErrInvalidCommandArgs, result.Code)
542543 }
543544
544545 func TestCloseCommandsReportMutationOutcome(t *testing.T) {
@@ -552,10 +553,10 @@ func TestCloseCommandsReportMutationOutcome(t *testing.T) {
552553 result := d.runControl(cmd, controlExec{
553554 d: d, sess: sess, tab: stale,
554555 target: daemonActionTarget{session: sess, tab: stale},
555- }, ports.CommandRequest{Slug: "close-tab"})
556+ }, protocol.CommandRequest{Slug: "close-tab"})
556557
557558 require.False(t, result.OK)
558- require.Equal(t, ports.ErrInternal, result.Code)
559+ require.Equal(t, protocol.ErrInternal, result.Code)
559560 require.Len(t, sess.tabs, 1, "a stale close must retain the live tab")
560561 })
561562
@@ -569,10 +570,10 @@ func TestCloseCommandsReportMutationOutcome(t *testing.T) {
569570 sess.mu.Unlock()
570571 sess.snapEligible.Store(true)
571572
572- result := sendCommand(t, d, ports.CommandRequest{Slug: "close-pane", TargetSession: "work"})
573+ result := sendCommand(t, d, protocol.CommandRequest{Slug: "close-pane", TargetSession: "work"})
573574
574575 require.False(t, result.OK)
575- require.Equal(t, ports.ErrInternal, result.Code)
576+ require.Equal(t, protocol.ErrInternal, result.Code)
576577 d.mu.Lock()
577578 require.Same(t, sess, d.sessions[sess.id], "failed final-pane close must retain the session")
578579 d.mu.Unlock()
@@ -591,7 +592,7 @@ func TestCloseCommandsReportMutationOutcome(t *testing.T) {
591592 selectTestAttachmentTabLocked(sess, 1)
592593 sess.mu.Unlock()
593594
594- result := sendCommand(t, d, ports.CommandRequest{Slug: "close-tab", TargetSession: "work"})
595+ result := sendCommand(t, d, protocol.CommandRequest{Slug: "close-tab", TargetSession: "work"})
595596
596597 require.True(t, result.OK, result.Text)
597598 sess.mu.Lock()
@@ -634,7 +635,7 @@ func TestHandleCommandHeadlessMutations(t *testing.T) {
634635 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })
635636 sess := addControlSession(d, "work", "t_work", "p_work")
636637
637- result := sendCommand(t, d, ports.CommandRequest{Slug: tt.slug, Args: tt.args, TargetSession: "work"})
638+ result := sendCommand(t, d, protocol.CommandRequest{Slug: tt.slug, Args: tt.args, TargetSession: "work"})
638639
639640 require.True(t, result.OK, result.Text)
640641 tt.verify(t, d, sess)
@@ -653,7 +654,7 @@ func TestHandleCommandNewSessionInheritsHeadlessIdentityAndViewport(t *testing.T
653654 source.tabs[0].size = domain.Size{Cols: 118, Rows: 38}
654655 source.mu.Unlock()
655656
656- result := sendCommand(t, d, ports.CommandRequest{Slug: "new-session", Args: []string{"scripted"}, TargetSession: "work"})
657+ result := sendCommand(t, d, protocol.CommandRequest{Slug: "new-session", Args: []string{"scripted"}, TargetSession: "work"})
657658
658659 require.True(t, result.OK, result.Text)
659660 d.mu.Lock()
@@ -671,9 +672,9 @@ func TestHandleCommandNewSessionInheritsHeadlessIdentityAndViewport(t *testing.T
671672 tb.mu.Unlock()
672673 require.Empty(t, source.snapshotAttachments())
673674
674- taken := sendCommand(t, d, ports.CommandRequest{Slug: "new-session", Args: []string{"scripted"}, TargetSession: "work"})
675+ taken := sendCommand(t, d, protocol.CommandRequest{Slug: "new-session", Args: []string{"scripted"}, TargetSession: "work"})
675676 require.False(t, taken.OK)
676- require.Equal(t, ports.ErrNameTaken, taken.Code)
677+ require.Equal(t, protocol.ErrNameTaken, taken.Code)
677678 }
678679
679680 func TestHandleCommandNewSessionPreservesSourceGeometry(t *testing.T) {
@@ -687,7 +688,7 @@ func TestHandleCommandNewSessionPreservesSourceGeometry(t *testing.T) {
687688 })
688689 require.True(t, source.registerAttachment(client))
689690
690- result := sendCommand(t, d, ports.CommandRequest{Slug: "new-session", Args: []string{"scripted"}, TargetSession: "work"})
691+ result := sendCommand(t, d, protocol.CommandRequest{Slug: "new-session", Args: []string{"scripted"}, TargetSession: "work"})
691692
692693 require.True(t, result.OK, result.Text)
693694 d.mu.Lock()
@@ -710,11 +711,11 @@ func TestHandleCommandValidatesToastAndQueuesForDetachedSession(t *testing.T) {
710711 d := newTestDaemon(t, nil, stubClock{})
711712 sess := addControlSession(d, "work", "t_work", "p_work")
712713
713- bad := sendCommand(t, d, ports.CommandRequest{Slug: "toast", Args: []string{"-l", "loud", "hello"}, TargetSession: "work"})
714+ bad := sendCommand(t, d, protocol.CommandRequest{Slug: "toast", Args: []string{"-l", "loud", "hello"}, TargetSession: "work"})
714715 require.False(t, bad.OK)
715- require.Equal(t, ports.ErrInvalidCommandArgs, bad.Code)
716+ require.Equal(t, protocol.ErrInvalidCommandArgs, bad.Code)
716717
717- good := sendCommand(t, d, ports.CommandRequest{Slug: "toast", Args: []string{"-l", "warn", "hello"}, TargetSession: "work"})
718+ good := sendCommand(t, d, protocol.CommandRequest{Slug: "toast", Args: []string{"-l", "warn", "hello"}, TargetSession: "work"})
718719 require.True(t, good.OK, good.Text)
719720 d.notices.mu.Lock()
720721 require.Len(t, d.notices.pending, 1)
@@ -728,16 +729,16 @@ func TestHandleCommandListingsContainStableIDsMarkersAndCWD(t *testing.T) {
728729 sess := addControlSession(d, "work", "t_work", "p_work")
729730 sess.tabs[0].name = "shell"
730731
731- sessions := sendCommand(t, d, ports.CommandRequest{Slug: "list-sessions"})
732+ sessions := sendCommand(t, d, protocol.CommandRequest{Slug: "list-sessions"})
732733 require.True(t, sessions.OK, sessions.Text)
733734 require.Contains(t, sessions.Output, "NAME\tSTATE\tTABS\tATTACHED\tACTIVE")
734735 require.Contains(t, sessions.Output, "work\tephemeral\t1\tfalse\ttrue")
735736
736- tabs := sendCommand(t, d, ports.CommandRequest{Slug: "list-tabs", TargetSession: "work"})
737+ tabs := sendCommand(t, d, protocol.CommandRequest{Slug: "list-tabs", TargetSession: "work"})
737738 require.True(t, tabs.OK, tabs.Text)
738739 require.Contains(t, tabs.Output, "0\tt_work\tshell\t1\ttrue")
739740
740- panes := sendCommand(t, d, ports.CommandRequest{Slug: "list-panes", TargetSession: "work", JSON: true})
741+ panes := sendCommand(t, d, protocol.CommandRequest{Slug: "list-panes", TargetSession: "work", JSON: true})
741742 require.True(t, panes.OK, panes.Text)
742743 var decoded []map[string]any
743744 require.NoError(t, json.Unmarshal([]byte(panes.Output), &decoded))
@@ -766,29 +767,29 @@ func TestRemoteCatalogJSONOutput(t *testing.T) {
766767 build.mruAt.Store(1)
767768
768769 d.mu.Lock()
769- d.inactive["old"] = inactiveSession{name: "old", cwd: "/tmp/old", createdAt: 1, incarnation: domain.IncarnationID{3}, state: ports.SessionDown}
770+ d.inactive["old"] = inactiveSession{name: "old", cwd: "/tmp/old", createdAt: 1, incarnation: domain.IncarnationID{3}, state: protocol.SessionDown}
770771 // Live runtime authority wins if both registries contain the same name.
771- d.inactive["work"] = inactiveSession{name: "work", cwd: "/tmp/work", createdAt: 1, incarnation: domain.IncarnationID{4}, state: ports.SessionDown}
772+ d.inactive["work"] = inactiveSession{name: "work", cwd: "/tmp/work", createdAt: 1, incarnation: domain.IncarnationID{4}, state: protocol.SessionDown}
772773 d.mu.Unlock()
773774
774- listBefore := sendCommand(t, d, ports.CommandRequest{Slug: "list-sessions"})
775+ listBefore := sendCommand(t, d, protocol.CommandRequest{Slug: "list-sessions"})
775776 require.True(t, listBefore.OK, listBefore.Text)
776777
777- missingJSON := sendCommand(t, d, ports.CommandRequest{Slug: "remote-catalog"})
778+ missingJSON := sendCommand(t, d, protocol.CommandRequest{Slug: "remote-catalog"})
778779 require.False(t, missingJSON.OK)
779- require.Equal(t, ports.ErrInvalidCommandArgs, missingJSON.Code)
780+ require.Equal(t, protocol.ErrInvalidCommandArgs, missingJSON.Code)
780781
781- withArgs := sendCommand(t, d, ports.CommandRequest{Slug: "remote-catalog", Args: []string{"extra"}, JSON: true})
782+ withArgs := sendCommand(t, d, protocol.CommandRequest{Slug: "remote-catalog", Args: []string{"extra"}, JSON: true})
782783 require.False(t, withArgs.OK)
783- require.Equal(t, ports.ErrInvalidCommandArgs, withArgs.Code)
784+ require.Equal(t, protocol.ErrInvalidCommandArgs, withArgs.Code)
784785
785- result := sendCommand(t, d, ports.CommandRequest{Slug: "remote-catalog", JSON: true})
786+ result := sendCommand(t, d, protocol.CommandRequest{Slug: "remote-catalog", JSON: true})
786787 require.True(t, result.OK, result.Text)
787788 require.True(t, strings.HasSuffix(result.Output, "\n"), "catalog output must be newline-terminated")
788789
789790 var catalog ports.RemoteCatalog
790791 require.NoError(t, json.Unmarshal([]byte(result.Output), &catalog))
791- require.Equal(t, ports.ProtocolVersion, catalog.ProtocolVersion)
792+ require.Equal(t, protocol.Version, catalog.ProtocolVersion)
792793
793794 raw := map[string]json.RawMessage{}
794795 require.NoError(t, json.Unmarshal([]byte(result.Output), &raw))
@@ -806,7 +807,7 @@ func TestRemoteCatalogJSONOutput(t *testing.T) {
806807
807808 require.Equal(t, ports.RemoteCatalogSessionDown, catalog.Sessions[1].State)
808809
809- listAfter := sendCommand(t, d, ports.CommandRequest{Slug: "list-sessions"})
810+ listAfter := sendCommand(t, d, protocol.CommandRequest{Slug: "list-sessions"})
810811 require.True(t, listAfter.OK, listAfter.Text)
811812 require.Equal(t, listBefore.Output, listAfter.Output)
812813 require.Contains(t, listAfter.Output, "work\tnamed\t2\ttrue\t")
@@ -828,7 +829,7 @@ func TestRemoteCatalogRefreshesFocusedTabTitle(t *testing.T) {
828829 return "fish", nil
829830 }
830831
831- result := sendCommand(t, d, ports.CommandRequest{Slug: "remote-catalog", JSON: true})
832+ result := sendCommand(t, d, protocol.CommandRequest{Slug: "remote-catalog", JSON: true})
832833 require.True(t, result.OK, result.Text)
833834 var catalog ports.RemoteCatalog
834835 require.NoError(t, json.Unmarshal([]byte(result.Output), &catalog))
@@ -860,7 +861,7 @@ func TestRemoteCatalogRejectsTooManyTabs(t *testing.T) {
860861 }
861862 sess.mu.Unlock()
862863
863- result := sendCommand(t, d, ports.CommandRequest{Slug: "remote-catalog", JSON: true})
864+ result := sendCommand(t, d, protocol.CommandRequest{Slug: "remote-catalog", JSON: true})
864865 require.False(t, result.OK)
865866 require.Contains(t, result.Text, "too many tabs")
866867 }
@@ -878,7 +879,7 @@ func TestHandleCommandSerializesSelfTargetOnNonActiveTab(t *testing.T) {
878879 sess.mu.Unlock()
879880 original := target.tree.Focus
880881
881- firstFrame := commandFrame(t, ports.CommandRequest{
882+ firstFrame := commandFrame(t, protocol.CommandRequest{
882883 Slug: "split-right", Self: true, TargetSession: "work",
883884 TargetTab: "t_invoking", TargetPane: "p_invoking",
884885 })
@@ -896,7 +897,7 @@ func TestHandleCommandSerializesSelfTargetOnNonActiveTab(t *testing.T) {
896897 t.Fatal("first command did not enter its blocked action")
897898 }
898899
899- secondFrame := commandFrame(t, ports.CommandRequest{
900+ secondFrame := commandFrame(t, protocol.CommandRequest{
900901 Slug: "focus-pane-right", Self: true, TargetSession: "work",
901902 TargetTab: "t_invoking", TargetPane: "p_invoking",
902903 })
@@ -961,7 +962,7 @@ func TestHandleCommandMovePaneUsesActiveFocusedSourceWithSessionFlag(t *testing.
961962 source.mu.Unlock()
962963 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
963964
964- result := sendCommand(t, d, ports.CommandRequest{
965+ result := sendCommand(t, d, protocol.CommandRequest{
965966 Slug: "move-pane", Args: []string{"dest", "t_dest"}, TargetSession: "work",
966967 })
967968 require.True(t, result.OK, result.Text)
@@ -996,7 +997,7 @@ func TestHandleCommandMovePaneSelfUsesStableSourceIDs(t *testing.T) {
996997 source.mu.Unlock()
997998 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
998999
999- result := sendCommand(t, d, ports.CommandRequest{
1000+ result := sendCommand(t, d, protocol.CommandRequest{
10001001 Slug: "move-pane", Args: []string{"dest", "t_dest"}, Self: true,
10011002 TargetSession: "work", TargetTab: "t_inactive", TargetPane: "p_inactive",
10021003 })
@@ -1034,7 +1035,7 @@ func TestHandleCommandMoveTabUsesActiveTabWithoutSelf(t *testing.T) {
10341035 source.mu.Unlock()
10351036 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
10361037
1037- result := sendCommand(t, d, ports.CommandRequest{
1038+ result := sendCommand(t, d, protocol.CommandRequest{
10381039 Slug: "move-tab", Args: []string{"dest"}, TargetSession: "work", Self: false,
10391040 TargetTab: "t_second", TargetPane: "p_second",
10401041 })
@@ -1062,7 +1063,7 @@ func TestHandleCommandMoveTabSelfUsesStableTab(t *testing.T) {
10621063 source.mu.Unlock()
10631064 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
10641065
1065- result := sendCommand(t, d, ports.CommandRequest{
1066+ result := sendCommand(t, d, protocol.CommandRequest{
10661067 Slug: "move-tab", Args: []string{"dest"}, Self: true,
10671068 TargetSession: "work", TargetTab: "t_second", TargetPane: "p_second",
10681069 })
@@ -1081,101 +1082,101 @@ func TestHandleCommandMoveTabSelfUsesStableTab(t *testing.T) {
10811082 func TestHandleCommandMoveCommandsRejectInvalidArgsAndTargets(t *testing.T) {
10821083 tests := []struct {
10831084 name string
1084- arrange func(*Daemon) ports.CommandRequest
1085+ arrange func(*Daemon) protocol.CommandRequest
10851086 code uint16
10861087 text string
10871088 }{
10881089 {
10891090 name: "move-pane missing destination session",
1090- arrange: func(d *Daemon) ports.CommandRequest {
1091+ arrange: func(d *Daemon) protocol.CommandRequest {
10911092 addControlSession(d, "work", "t_work", "p_work")
1092- return ports.CommandRequest{Slug: "move-pane", Args: []string{"work", "t_work"}, TargetSession: "work"}
1093+ return protocol.CommandRequest{Slug: "move-pane", Args: []string{"work", "t_work"}, TargetSession: "work"}
10931094 },
1094- code: ports.ErrNoSuchTarget,
1095+ code: protocol.ErrNoSuchTarget,
10951096 text: "move request is invalid",
10961097 },
10971098 {
10981099 name: "move-pane unknown destination session",
1099- arrange: func(d *Daemon) ports.CommandRequest {
1100+ arrange: func(d *Daemon) protocol.CommandRequest {
11001101 addControlSession(d, "work", "t_work", "p_work")
1101- return ports.CommandRequest{Slug: "move-pane", Args: []string{"missing", "t_dest"}, TargetSession: "work"}
1102+ return protocol.CommandRequest{Slug: "move-pane", Args: []string{"missing", "t_dest"}, TargetSession: "work"}
11021103 },
1103- code: ports.ErrNoSuchTarget,
1104+ code: protocol.ErrNoSuchTarget,
11041105 text: "move target is no longer available",
11051106 },
11061107 {
11071108 name: "move-pane destination tab not in session",
1108- arrange: func(d *Daemon) ports.CommandRequest {
1109+ arrange: func(d *Daemon) protocol.CommandRequest {
11091110 addControlSession(d, "work", "t_work", "p_work")
11101111 addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
1111- return ports.CommandRequest{Slug: "move-pane", Args: []string{"dest", "t_other"}, TargetSession: "work"}
1112+ return protocol.CommandRequest{Slug: "move-pane", Args: []string{"dest", "t_other"}, TargetSession: "work"}
11121113 },
1113- code: ports.ErrNoSuchTarget,
1114+ code: protocol.ErrNoSuchTarget,
11141115 text: "move target is no longer available",
11151116 },
11161117 {
11171118 name: "move-pane same source tab",
1118- arrange: func(d *Daemon) ports.CommandRequest {
1119+ arrange: func(d *Daemon) protocol.CommandRequest {
11191120 addControlSession(d, "work", "t_work", "p_work")
1120- return ports.CommandRequest{Slug: "move-pane", Args: []string{"work", "t_work"}, TargetSession: "work"}
1121+ return protocol.CommandRequest{Slug: "move-pane", Args: []string{"work", "t_work"}, TargetSession: "work"}
11211122 },
1122- code: ports.ErrNoSuchTarget,
1123+ code: protocol.ErrNoSuchTarget,
11231124 text: "move request is invalid",
11241125 },
11251126 {
11261127 name: "move-tab same session",
1127- arrange: func(d *Daemon) ports.CommandRequest {
1128+ arrange: func(d *Daemon) protocol.CommandRequest {
11281129 addControlSession(d, "work", "t_work", "p_work")
1129- return ports.CommandRequest{Slug: "move-tab", Args: []string{"work"}, TargetSession: "work"}
1130+ return protocol.CommandRequest{Slug: "move-tab", Args: []string{"work"}, TargetSession: "work"}
11301131 },
1131- code: ports.ErrNoSuchTarget,
1132+ code: protocol.ErrNoSuchTarget,
11321133 text: "move request is invalid",
11331134 },
11341135 {
11351136 name: "move-tab stopping destination",
1136- arrange: func(d *Daemon) ports.CommandRequest {
1137+ arrange: func(d *Daemon) protocol.CommandRequest {
11371138 addControlSession(d, "work", "t_work", "p_work")
11381139 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
11391140 destination.teardownMu.Lock()
11401141 destination.teardownActive = true
11411142 destination.teardownMu.Unlock()
1142- return ports.CommandRequest{Slug: "move-tab", Args: []string{"dest"}, TargetSession: "work"}
1143+ return protocol.CommandRequest{Slug: "move-tab", Args: []string{"dest"}, TargetSession: "work"}
11431144 },
1144- code: ports.ErrNoSuchTarget,
1145+ code: protocol.ErrNoSuchTarget,
11451146 text: "move target is no longer available",
11461147 },
11471148 {
11481149 name: "move-pane too few args",
1149- arrange: func(d *Daemon) ports.CommandRequest {
1150+ arrange: func(d *Daemon) protocol.CommandRequest {
11501151 addControlSession(d, "work", "t_work", "p_work")
1151- return ports.CommandRequest{Slug: "move-pane", Args: []string{"dest"}, TargetSession: "work"}
1152+ return protocol.CommandRequest{Slug: "move-pane", Args: []string{"dest"}, TargetSession: "work"}
11521153 },
1153- code: ports.ErrInvalidCommandArgs,
1154+ code: protocol.ErrInvalidCommandArgs,
11541155 },
11551156 {
11561157 name: "move-pane too many args",
1157- arrange: func(d *Daemon) ports.CommandRequest {
1158+ arrange: func(d *Daemon) protocol.CommandRequest {
11581159 addControlSession(d, "work", "t_work", "p_work")
1159- return ports.CommandRequest{Slug: "move-pane", Args: []string{"dest", "t_dest", "extra"}, TargetSession: "work"}
1160+ return protocol.CommandRequest{Slug: "move-pane", Args: []string{"dest", "t_dest", "extra"}, TargetSession: "work"}
11601161 },
1161- code: ports.ErrInvalidCommandArgs,
1162+ code: protocol.ErrInvalidCommandArgs,
11621163 },
11631164 {
11641165 name: "move-tab missing destination",
1165- arrange: func(d *Daemon) ports.CommandRequest {
1166+ arrange: func(d *Daemon) protocol.CommandRequest {
11661167 addControlSession(d, "work", "t_work", "p_work")
1167- return ports.CommandRequest{Slug: "move-tab", TargetSession: "work"}
1168+ return protocol.CommandRequest{Slug: "move-tab", TargetSession: "work"}
11681169 },
1169- code: ports.ErrInvalidCommandArgs,
1170+ code: protocol.ErrInvalidCommandArgs,
11701171 },
11711172 {
11721173 name: "explicit session name without IDs remains authoritative",
1173- arrange: func(d *Daemon) ports.CommandRequest {
1174+ arrange: func(d *Daemon) protocol.CommandRequest {
11741175 addControlSession(d, "renamed", "t_work", "p_work")
11751176 addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
1176- return ports.CommandRequest{Slug: "move-pane", Args: []string{"dest", "t_dest"}, TargetSession: "old-name"}
1177+ return protocol.CommandRequest{Slug: "move-pane", Args: []string{"dest", "t_dest"}, TargetSession: "old-name"}
11771178 },
1178- code: ports.ErrNoSuchTarget,
1179+ code: protocol.ErrNoSuchTarget,
11791180 text: "no such session: old-name",
11801181 },
11811182 }
@@ -1200,7 +1201,7 @@ func TestHandleCommandMovePaneRelocatedStableIDsOverrideAdvisorySessionName(t *t
12001201 source := addControlSession(d, "renamed", "t_work", "p_work")
12011202 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
12021203
1203- result := sendCommand(t, d, ports.CommandRequest{
1204+ result := sendCommand(t, d, protocol.CommandRequest{
12041205 Slug: "move-pane", Args: []string{"dest", "t_dest"}, Self: true,
12051206 TargetSession: "old-name", TargetTab: "t_work", TargetPane: "p_work",
12061207 })
@@ -1232,7 +1233,7 @@ func TestHandleCommandMovePaneStableIDsLocateSessionWithoutSelfRedirect(t *testi
12321233 source.mu.Unlock()
12331234 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
12341235
1235- result := sendCommand(t, d, ports.CommandRequest{
1236+ result := sendCommand(t, d, protocol.CommandRequest{
12361237 Slug: "move-pane", Args: []string{"dest", "t_dest"}, Self: true,
12371238 TargetSession: "old-name", TargetTab: "t_inactive", TargetPane: "p_inactive",
12381239 })
@@ -1273,10 +1274,10 @@ func TestHandleCommandOppositeMoveCommandsDoNotDeadlock(t *testing.T) {
12731274 selectTestAttachmentTabLocked(right, 1)
12741275 right.mu.Unlock()
12751276
1276- leftFrame := commandFrame(t, ports.CommandRequest{
1277+ leftFrame := commandFrame(t, protocol.CommandRequest{
12771278 Slug: "move-tab", Args: []string{"right"}, TargetSession: "left",
12781279 })
1279- rightFrame := commandFrame(t, ports.CommandRequest{
1280+ rightFrame := commandFrame(t, protocol.CommandRequest{
12801281 Slug: "move-tab", Args: []string{"left"}, TargetSession: "right",
12811282 })
12821283 leftTransport, leftSends, _ := newConn(t, leftFrame)
@@ -1323,17 +1324,17 @@ func addNamedMoveDestination(d *Daemon, name, tabID, paneID string) *session {
13231324 return sess
13241325 }
13251326
1326-func commandFrame(t *testing.T, request ports.CommandRequest) ports.Frame {
1327+func commandFrame(t *testing.T, request protocol.CommandRequest) ports.Frame {
13271328 t.Helper()
13281329 if request.Version == 0 {
1329- request.Version = ports.ProtocolVersion
1330+ request.Version = protocol.Version
13301331 }
13311332 payload, err := ports.MarshalCommandRequest(request)
13321333 require.NoError(t, err)
13331334 return ports.Frame{Type: ports.MsgCommand, Payload: payload}
13341335 }
13351336
1336-func sendCommand(t *testing.T, d *Daemon, request ports.CommandRequest) ports.CommandResult {
1337+func sendCommand(t *testing.T, d *Daemon, request protocol.CommandRequest) protocol.CommandResult {
13371338 t.Helper()
13381339 frame := commandFrame(t, request)
13391340 tr, sends, _ := newConn(t, frame)
@@ -1341,7 +1342,7 @@ func sendCommand(t *testing.T, d *Daemon, request ports.CommandRequest) ports.Co
13411342 return awaitCommandResult(t, sends)
13421343 }
13431344
1344-func awaitCommandResult(t *testing.T, sends chan ports.Frame) ports.CommandResult {
1345+func awaitCommandResult(t *testing.T, sends chan ports.Frame) protocol.CommandResult {
13451346 t.Helper()
13461347 reply := awaitFrame(t, sends, ports.MsgCommandResult)
13471348 result, err := ports.UnmarshalCommandResult(reply.Payload)
M internal/usecase/daemon/copymode_test.go +3 -2
@@ -18,6 +18,7 @@ import (
1818 "github.com/bnema/vev/internal/domain"
1919 "github.com/bnema/vev/internal/ports"
2020 portsmocks "github.com/bnema/vev/internal/ports/mocks"
21+ "github.com/bnema/vev/internal/protocol"
2122 scopy "github.com/bnema/vev/internal/usecase/copy"
2223 "github.com/bnema/vev/internal/usecase/layout"
2324 "github.com/bnema/vev/internal/usecase/ui"
@@ -416,7 +417,7 @@ func TestScrollbackEvictionFeedsCopyModeYank(t *testing.T) {
416417 // relying on wall-clock debounce delivery.
417418 clk := newCoordinatorMockClock(t, 16)
418419 d := newTestDaemon(t, newFactory(t, p), clk.clock)
419- tr, sends, releaseConn := newConn(t, mustHello(ports.IntentNew, "work", domain.Size{Cols: 16, Rows: 5}))
420+ tr, sends, releaseConn := newConn(t, mustHello(protocol.IntentNew, "work", domain.Size{Cols: 16, Rows: 5}))
420421 advanceRender := func() {
421422 deadline := time.NewTimer(2 * time.Second)
422423 defer deadline.Stop()
@@ -483,7 +484,7 @@ func TestScrollbackEvictionFeedsCopyModeYank(t *testing.T) {
483484 releaseConn()
484485 closeOnce.Do(func() { close(readDone) })
485486 hg.Wait()
486- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))
487+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
487488 d.sessWg.Wait()
488489 }
489490
M internal/usecase/daemon/daemon.go +59 -59
@@ -374,7 +374,7 @@ type inactiveSession struct {
374374 tabRecords []domain.CatalogueTabRecord
375375 purging bool
376376 record domain.CatalogueRecord
377- state ports.SessionState
377+ state protocol.SessionState
378378 restoreDone chan struct{}
379379 }
380380
@@ -391,7 +391,7 @@ func (s inactiveSession) restorePending() bool {
391391 }
392392
393393 func (s inactiveSession) broken() bool {
394- return s.state == ports.SessionBroken || s.record.DegradedReason != ""
394+ return s.state == protocol.SessionBroken || s.record.DegradedReason != ""
395395 }
396396
397397 func (s inactiveSession) visible() bool {
@@ -399,7 +399,7 @@ func (s inactiveSession) visible() bool {
399399 }
400400
401401 func (s inactiveSession) canResume() bool {
402- return s.visible() && s.state == ports.SessionDown && !s.broken()
402+ return s.visible() && s.state == protocol.SessionDown && !s.broken()
403403 }
404404
405405 func (s inactiveSession) sameLifecycle(other inactiveSession) bool {
@@ -717,7 +717,7 @@ func New(ptys ports.PTYFactory, clock ports.Clock, log *slog.Logger, opts ...Opt
717717 state, done := initialSessionState(r)
718718 d.inactive[r.Name] = inactiveSessionFromRecord(r, state, done)
719719 } else {
720- d.inactive[r.Name] = inactiveSessionFromRecord(r, ports.SessionDown, nil)
720+ d.inactive[r.Name] = inactiveSessionFromRecord(r, protocol.SessionDown, nil)
721721 }
722722 if !hasCreatedAt || r.CreatedAt > maxCreatedAt {
723723 maxCreatedAt = r.CreatedAt
@@ -806,7 +806,7 @@ func (d *Daemon) Serve(ctx context.Context, l ports.Listener) error {
806806 snapshotDeadline = newSnapshotShutdownDeadline(d.clock)
807807 defer snapshotDeadline.stop()
808808 }
809- d.shutdownAllWithSnapshotDeadline(ports.ReasonServerShutdown, snapshotDeadline)
809+ d.shutdownAllWithSnapshotDeadline(protocol.ReasonServerShutdown, snapshotDeadline)
810810 d.waitNotifies()
811811 d.hardCancel()
812812 d.serveCancel()
@@ -826,7 +826,7 @@ func (d *Daemon) Serve(ctx context.Context, l ports.Listener) error {
826826 d.persistShutdownSnapshotFailure(name, context.DeadlineExceeded)
827827 }
828828 d.WaitDurableWriters()
829- d.shutdownAllWithSnapshotDeadline(ports.ReasonServerShutdown, snapshotDeadline)
829+ d.shutdownAllWithSnapshotDeadline(protocol.ReasonServerShutdown, snapshotDeadline)
830830 d.waitSessionWorkersWithSnapshotDeadline(snapshotDeadline)
831831 d.waitNotifies()
832832 if err := d.flushCatalogue(); err != nil {
@@ -1058,7 +1058,7 @@ func (d *Daemon) handleConn(tr ports.Transport) {
10581058 default:
10591059 d.log.Warn("hello rejected", "err", "expected hello", "type", first.Type)
10601060 _ = boundedHandshakeOperation(handshakeCtx, tr, func() error {
1061- return tr.Send(frameError(ports.ErrInternal, "expected hello"))
1061+ return tr.Send(frameError(protocol.ErrInternal, "expected hello"))
10621062 })
10631063 stopTransport()
10641064 finishHandshake()
@@ -1072,17 +1072,17 @@ func (d *Daemon) handleList(tr ports.Transport) {
10721072 defer func() { _ = tr.Close() }()
10731073
10741074 d.mu.Lock()
1075- infos := make([]ports.SessionInfo, 0, len(d.sessions)+len(d.inactive))
1075+ infos := make([]protocol.SessionInfo, 0, len(d.sessions)+len(d.inactive))
10761076 liveNames := make(map[string]struct{}, len(d.sessions))
10771077 for _, s := range d.sessions {
10781078 if s == nil {
10791079 continue
10801080 }
10811081 s.mu.Lock()
1082- info := ports.SessionInfo{
1082+ info := protocol.SessionInfo{
10831083 SessionID: string(s.id),
10841084 Name: s.name,
1085- State: ports.SessionUp,
1085+ State: protocol.SessionUp,
10861086 Ephemeral: s.ephemeral,
10871087 Tabs: uint16(len(s.tabs)),
10881088 Attached: len(s.attachments) != 0,
@@ -1095,11 +1095,11 @@ func (d *Daemon) handleList(tr ports.Transport) {
10951095 if _, live := liveNames[name]; live || !inactive.visible() {
10961096 continue
10971097 }
1098- state := ports.SessionDown
1098+ state := protocol.SessionDown
10991099 if inactive.broken() {
1100- state = ports.SessionBroken
1100+ state = protocol.SessionBroken
11011101 }
1102- infos = append(infos, ports.SessionInfo{Name: name, State: state})
1102+ infos = append(infos, protocol.SessionInfo{Name: name, State: state})
11031103 }
11041104 d.mu.Unlock()
11051105
@@ -1115,11 +1115,11 @@ func (d *Daemon) handleKill(tr ports.Transport, f ports.Frame) {
11151115
11161116 k, err := ports.UnmarshalKill(f.Payload)
11171117 if err != nil {
1118- _ = d.boundedControlSend(tr, frameError(ports.ErrInternal, "malformed kill request"))
1118+ _ = d.boundedControlSend(tr, frameError(protocol.ErrInternal, "malformed kill request"))
11191119 return
11201120 }
11211121 if k.All {
1122- d.shutdownAll(ports.ReasonServerShutdown)
1122+ d.shutdownAll(protocol.ReasonServerShutdown)
11231123 return
11241124 }
11251125
@@ -1132,7 +1132,7 @@ func (d *Daemon) handleKill(tr ports.Transport, f ports.Frame) {
11321132 // deletion order as live and offline purges.
11331133 if err := d.retryStoppedPurge(k.Name); err != nil {
11341134 d.log.Warn("deleting stopped session failed", "err", err, "session", k.Name)
1135- _ = d.boundedControlSend(tr, frameError(ports.ErrInternal, "deleting stopped session failed"))
1135+ _ = d.boundedControlSend(tr, frameError(protocol.ErrInternal, "deleting stopped session failed"))
11361136 }
11371137 return
11381138 }
@@ -1140,11 +1140,11 @@ func (d *Daemon) handleKill(tr ports.Transport, f ports.Frame) {
11401140 d.mu.Unlock()
11411141
11421142 if target == nil {
1143- _ = d.boundedControlSend(tr, frameError(ports.ErrNoSuchSession, "no such session: "+k.Name))
1143+ _ = d.boundedControlSend(tr, frameError(protocol.ErrNoSuchSession, "no such session: "+k.Name))
11441144 return
11451145 }
1146- if err := d.killSession(target, ports.ReasonSessionKilled, true); err != nil {
1147- _ = d.boundedControlSend(tr, frameError(ports.ErrInternal, "deleting persisted session failed"))
1146+ if err := d.killSession(target, protocol.ReasonSessionKilled, true); err != nil {
1147+ _ = d.boundedControlSend(tr, frameError(protocol.ErrInternal, "deleting persisted session failed"))
11481148 }
11491149 }
11501150
@@ -1175,19 +1175,19 @@ func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut <
11751175 }
11761176 h, err := ports.UnmarshalHello(f.Payload)
11771177 if err != nil {
1178- if version, ok := ports.PeekHelloVersion(f.Payload); ok && version != ports.ProtocolVersion {
1179- d.log.Warn("hello rejected", "err", "protocol version mismatch", "version", version, "expected", ports.ProtocolVersion)
1180- _ = sendHandshake(frameError(ports.ErrVersionMismatch, "protocol version mismatch"))
1178+ if version, ok := ports.PeekHelloVersion(f.Payload); ok && version != protocol.Version {
1179+ d.log.Warn("hello rejected", "err", "protocol version mismatch", "version", version, "expected", protocol.Version)
1180+ _ = sendHandshake(frameError(protocol.ErrVersionMismatch, "protocol version mismatch"))
11811181 } else {
11821182 d.log.Warn("hello rejected", "err", err)
1183- _ = sendHandshake(frameError(ports.ErrInternal, "malformed hello"))
1183+ _ = sendHandshake(frameError(protocol.ErrInternal, "malformed hello"))
11841184 }
11851185 _ = tr.Close()
11861186 return
11871187 }
1188- if h.Version != ports.ProtocolVersion {
1189- d.log.Warn("hello rejected", "err", "protocol version mismatch", "version", h.Version, "expected", ports.ProtocolVersion, "intent", h.Intent, "session", h.Name)
1190- _ = sendHandshake(frameError(ports.ErrVersionMismatch, "protocol version mismatch"))
1188+ if h.Version != protocol.Version {
1189+ d.log.Warn("hello rejected", "err", "protocol version mismatch", "version", h.Version, "expected", protocol.Version, "intent", h.Intent, "session", h.Name)
1190+ _ = sendHandshake(frameError(protocol.ErrVersionMismatch, "protocol version mismatch"))
11911191 _ = tr.Close()
11921192 return
11931193 }
@@ -1198,7 +1198,7 @@ func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut <
11981198 if pe, ok := errors.AsType[*protoErr](rerr); ok {
11991199 _ = sendHandshake(frameError(pe.code, pe.text))
12001200 } else {
1201- _ = sendHandshake(frameError(ports.ErrInternal, rerr.Error()))
1201+ _ = sendHandshake(frameError(protocol.ErrInternal, rerr.Error()))
12021202 }
12031203 _ = tr.Close()
12041204 return
@@ -1310,7 +1310,7 @@ func (e *protoErr) Error() string { return e.text }
13101310 // unlocking d.mu before returning, including every error path. It publishes
13111311 // terminal and role state before releasing d.mu, then defers coordinator
13121312 // cleanup so obsolete workers never delay the new handshake.
1313-func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size, h ports.Hello) (*attachedClient, error) {
1313+func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size, h protocol.Hello) (*attachedClient, error) {
13141314 initialTabIndex := -1
13151315 if h.RemoteTarget != nil {
13161316 var ok bool
@@ -1320,7 +1320,7 @@ func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size,
13201320 _ = tr.Close()
13211321 }
13221322 d.mu.Unlock()
1323- return nil, &protoErr{ports.ErrNoSuchTarget, "remote tab no longer exists"}
1323+ return nil, &protoErr{protocol.ErrNoSuchTarget, "remote tab no longer exists"}
13241324 }
13251325 }
13261326 if h.PreferredTabID != "" {
@@ -1334,7 +1334,7 @@ func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size,
13341334 // A picker handoff deliberately leaves the daemon-owned environment and CWD
13351335 // untouched, even though Hello retains those fields for direct CLI clients.
13361336 sess.mu.Lock()
1337- if h.EnvironmentPolicy != ports.EnvironmentPolicyDaemonOwned {
1337+ if h.EnvironmentPolicy != protocol.EnvironmentPolicyDaemonOwned {
13381338 sess.env = copyEnvironment(h.Env)
13391339 }
13401340 sess.mu.Unlock()
@@ -1439,12 +1439,12 @@ func (d *Daemon) waitForTargetRestore(ctx context.Context, name string) error {
14391439 return nil
14401440 }
14411441 if stopped.broken() {
1442- return &protoErr{ports.ErrInternal, "session durable state is broken: " + name}
1442+ return &protoErr{protocol.ErrInternal, "session durable state is broken: " + name}
14431443 }
14441444 if stopped.record.Committed == nil {
14451445 return nil
14461446 }
1447- return &protoErr{ports.ErrInternal, "session was not restored into this daemon: " + name}
1447+ return &protoErr{protocol.ErrInternal, "session was not restored into this daemon: " + name}
14481448 }
14491449
14501450 // route resolves a Hello to a session and a freshly attached client, creating
@@ -1453,20 +1453,20 @@ func (d *Daemon) waitForTargetRestore(ctx context.Context, name string) error {
14531453 func (d *Daemon) validateExactSessionTargetLocked(target protocol.ExactSessionTarget) error {
14541454 if sess := d.findByNameLocked(target.SessionName); sess != nil {
14551455 if sess.incarnation != target.LifecycleID {
1456- return &protoErr{ports.ErrNoSuchSession, "session lifecycle has changed: " + target.SessionName}
1456+ return &protoErr{protocol.ErrNoSuchSession, "session lifecycle has changed: " + target.SessionName}
14571457 }
14581458 return nil
14591459 }
14601460 stopped, ok := d.inactive[target.SessionName]
14611461 if !ok || !stopped.visible() || stopped.incarnation != target.LifecycleID {
1462- return &protoErr{ports.ErrNoSuchSession, "no such session lifecycle: " + target.SessionName}
1462+ return &protoErr{protocol.ErrNoSuchSession, "no such session lifecycle: " + target.SessionName}
14631463 }
14641464 return nil
14651465 }
14661466
14671467 func (d *Daemon) validateExactSessionTarget(ctx context.Context, target protocol.ExactSessionTarget) error {
14681468 if err := target.Validate(); err != nil {
1469- return &protoErr{ports.ErrNoSuchSession, "invalid exact session target"}
1469+ return &protoErr{protocol.ErrNoSuchSession, "invalid exact session target"}
14701470 }
14711471 if err := d.waitForTargetRestore(ctx, target.SessionName); err != nil {
14721472 return err
@@ -1477,7 +1477,7 @@ func (d *Daemon) validateExactSessionTarget(ctx context.Context, target protocol
14771477 return d.validateExactSessionTargetLocked(target)
14781478 }
14791479
1480-func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedClient, error) {
1480+func (d *Daemon) route(h protocol.Hello, tr ports.Transport) (*session, *attachedClient, error) {
14811481 ctx := d.serveCtx
14821482 if ctx == nil {
14831483 ctx = context.Background()
@@ -1485,7 +1485,7 @@ func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedCl
14851485 return d.routeWithContext(ctx, h, tr)
14861486 }
14871487
1488-func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.Transport) (*session, *attachedClient, error) {
1488+func (d *Daemon) routeWithContext(ctx context.Context, h protocol.Hello, tr ports.Transport) (*session, *attachedClient, error) {
14891489 if ctx == nil {
14901490 ctx = context.Background()
14911491 }
@@ -1494,13 +1494,13 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T
14941494 }
14951495 sz := h.Size
14961496 if !sz.Valid() {
1497- return nil, nil, &protoErr{ports.ErrInternal, "invalid terminal size"}
1497+ return nil, nil, &protoErr{protocol.ErrInternal, "invalid terminal size"}
14981498 }
1499- if h.Intent == ports.IntentResume && h.ResumeToken == 0 {
1500- return nil, nil, &protoErr{ports.ErrNoSuchSession, "resume token is required"}
1499+ if h.Intent == protocol.IntentResume && h.ResumeToken == 0 {
1500+ return nil, nil, &protoErr{protocol.ErrNoSuchSession, "resume token is required"}
15011501 }
15021502 if h.ExactTarget != nil && h.ResumeToken == 0 && h.Name != h.ExactTarget.SessionName {
1503- return nil, nil, &protoErr{ports.ErrNoSuchSession, "exact session target name mismatch"}
1503+ return nil, nil, &protoErr{protocol.ErrNoSuchSession, "exact session target name mismatch"}
15041504 }
15051505 if h.RemoteTarget != nil && h.ResumeToken == 0 {
15061506 return d.routeRemoteTargetWithContext(ctx, h, tr)
@@ -1526,36 +1526,36 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T
15261526 // Live recovery parked then lost a competing resumeParked
15271527 // race; keep the fail-closed wire response instead of
15281528 // leaking the internal sentinel to handleHello as ErrInternal.
1529- return nil, nil, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}
1529+ return nil, nil, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
15301530 }
15311531 return nil, nil, err
15321532 } else if ok {
15331533 return sess, ac, nil
15341534 }
1535- return nil, nil, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}
1535+ return nil, nil, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
15361536 }
15371537 if sess, ac, ok, err := d.resumeParked(h, tr, sz); err == nil {
15381538 if ok {
15391539 return sess, ac, nil
15401540 }
1541- return nil, nil, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}
1541+ return nil, nil, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
15421542 } else if errors.Is(err, errResumeTokenLifecycleRace) {
15431543 // The parked entry was replaced while this handshake waited for
15441544 // its send lock. Fail closed on the original credential rather
15451545 // than falling through to ordinary attach/create routing.
1546- return nil, nil, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}
1546+ return nil, nil, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
15471547 } else {
15481548 return nil, nil, err
15491549 }
15501550 }
1551- if h.Intent == ports.IntentResume || h.Intent == ports.IntentAttach {
1551+ if h.Intent == protocol.IntentResume || h.Intent == protocol.IntentAttach {
15521552 if err := d.waitForTargetRestore(ctx, h.Name); err != nil {
15531553 return nil, nil, err
15541554 }
15551555 }
1556- if h.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned && h.RemoteTarget == nil &&
1557- (h.Intent == ports.IntentNew || h.Intent == ports.IntentEphemeral) {
1558- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "daemon-owned environment requires an exact remote target"}
1556+ if h.EnvironmentPolicy == protocol.EnvironmentPolicyDaemonOwned && h.RemoteTarget == nil &&
1557+ (h.Intent == protocol.IntentNew || h.Intent == protocol.IntentEphemeral) {
1558+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "daemon-owned environment requires an exact remote target"}
15591559 }
15601560 d.mu.Lock()
15611561 // Shutdown/create interlock: once shutdown has begun (last session removed,
@@ -1564,7 +1564,7 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T
15641564 // anything inserted now would leak its PTY and hang Serve.
15651565 if d.closing {
15661566 d.mu.Unlock()
1567- return nil, nil, &protoErr{ports.ErrServerShutdown, "daemon is shutting down"}
1567+ return nil, nil, &protoErr{protocol.ErrServerShutdown, "daemon is shutting down"}
15681568 }
15691569 // The first exact-target check happens before restore I/O. Recheck while
15701570 // holding d.mu so a same-name lifecycle replacement cannot slip between
@@ -1576,7 +1576,7 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T
15761576 }
15771577 }
15781578 switch h.Intent {
1579- case ports.IntentEphemeral:
1579+ case protocol.IntentEphemeral:
15801580 name := d.allocEphemeralNameLocked()
15811581 sess, err := d.createSessionLockedWithMode(name, true, h.Cwd, h.Geometry(), h.Env)
15821582 if err != nil {
@@ -1586,18 +1586,18 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T
15861586 ac, err := d.finishRouteAttach(sess, tr, sz, h, true, true)
15871587 return sess, ac, err
15881588
1589- case ports.IntentNew:
1589+ case protocol.IntentNew:
15901590 if h.Name == "" {
15911591 d.mu.Unlock()
1592- return nil, nil, &protoErr{ports.ErrInvalidSessionName, "empty session name"}
1592+ return nil, nil, &protoErr{protocol.ErrInvalidSessionName, "empty session name"}
15931593 }
15941594 if err := domain.ValidateSessionName(h.Name); err != nil {
15951595 d.mu.Unlock()
1596- return nil, nil, &protoErr{ports.ErrInvalidSessionName, err.Error()}
1596+ return nil, nil, &protoErr{protocol.ErrInvalidSessionName, err.Error()}
15971597 }
15981598 if d.nameLiveOrStoppedLocked(h.Name) {
15991599 d.mu.Unlock()
1600- return nil, nil, &protoErr{ports.ErrNameTaken, "session name already in use: " + h.Name}
1600+ return nil, nil, &protoErr{protocol.ErrNameTaken, "session name already in use: " + h.Name}
16011601 }
16021602 sess, err := d.createSessionLockedWithMode(h.Name, false, h.Cwd, h.Geometry(), h.Env)
16031603 if err != nil {
@@ -1607,18 +1607,18 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T
16071607 ac, err := d.finishRouteAttach(sess, tr, sz, h, true, true)
16081608 return sess, ac, err
16091609
1610- case ports.IntentAttach:
1610+ case protocol.IntentAttach:
16111611 sess := d.findByNameLocked(h.Name)
16121612 created := false
16131613 if sess == nil {
16141614 stopped, ok := d.inactive[h.Name]
16151615 if !ok || !stopped.canResume() {
16161616 d.mu.Unlock()
1617- return nil, nil, &protoErr{ports.ErrNoSuchSession, "no such resumable session: " + h.Name}
1617+ return nil, nil, &protoErr{protocol.ErrNoSuchSession, "no such resumable session: " + h.Name}
16181618 }
16191619 cwd := d.dirOrHome(stopped.cwd)
16201620 env := h.Env
1621- if h.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned {
1621+ if h.EnvironmentPolicy == protocol.EnvironmentPolicyDaemonOwned {
16221622 env = copyEnvironment(d.baseEnv)
16231623 }
16241624 var err error
@@ -1634,6 +1634,6 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T
16341634
16351635 default:
16361636 d.mu.Unlock()
1637- return nil, nil, &protoErr{ports.ErrInternal, "unknown intent"}
1637+ return nil, nil, &protoErr{protocol.ErrInternal, "unknown intent"}
16381638 }
16391639 }
M internal/usecase/daemon/daemon_test.go +10 -9
@@ -16,6 +16,7 @@ import (
1616 "github.com/bnema/vev/internal/domain"
1717 "github.com/bnema/vev/internal/ports"
1818 portsmocks "github.com/bnema/vev/internal/ports/mocks"
19+ "github.com/bnema/vev/internal/protocol"
1920 "github.com/bnema/vev/internal/usecase/keys"
2021 )
2122
@@ -265,13 +266,13 @@ func newConn(t *testing.T, first ports.Frame, more ...ports.Frame) (*portsmocks.
265266 }
266267
267268 func mustHello(intent uint8, name string, sz domain.Size) ports.Frame {
268- return ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(ports.Hello{
269- Version: ports.ProtocolVersion, Intent: intent, Name: name, Size: sz, TermEnv: "xterm-256color",
269+ return ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(protocol.Hello{
270+ Version: protocol.Version, Intent: intent, Name: name, Size: sz, TermEnv: "xterm-256color",
270271 })}
271272 }
272273
273274 func frameInput(data []byte) ports.Frame {
274- return ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: data})}
275+ return ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: data})}
275276 }
276277
277278 func screenLineText(s *vt.Screen, y int) string {
@@ -315,9 +316,9 @@ func sessionCount(d *Daemon) int {
315316 return len(d.sessions)
316317 }
317318
318-func listSessions(t *testing.T, d *Daemon) ports.Sessions {
319+func listSessions(t *testing.T, d *Daemon) protocol.Sessions {
319320 t.Helper()
320- tr, sends, _ := newConn(t, ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(ports.List{})})
321+ tr, sends, _ := newConn(t, ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(protocol.List{})})
321322 d.handleList(tr)
322323 f := awaitFrame(t, sends, ports.MsgSessions)
323324 sessions, err := ports.UnmarshalSessions(f.Payload)
@@ -594,7 +595,7 @@ func TestRouteRejectsInvalidTerminalSizeBeforeResumeMutation(t *testing.T) {
594595 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
595596
596597 transport := &closeTrackingTransport{}
597- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), transport)
598+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), transport)
598599 require.NoError(t, err)
599600 token := ac.resumeToken
600601 d.clientGone(sess, ac, transport, false)
@@ -604,12 +605,12 @@ func TestRouteRejectsInvalidTerminalSizeBeforeResumeMutation(t *testing.T) {
604605 d.mu.Unlock()
605606 require.NotNil(t, parked)
606607
607- invalid := helloResumeCapable(ports.IntentResume, "work", token)
608+ invalid := helloResumeCapable(protocol.IntentResume, "work", token)
608609 invalid.Size = tt.size
609610 _, _, err = d.route(invalid, &closeTrackingTransport{})
610611 var protocolErr *protoErr
611612 require.ErrorAs(t, err, &protocolErr)
612- require.Equal(t, ports.ErrInternal, protocolErr.code)
613+ require.Equal(t, protocol.ErrInternal, protocolErr.code)
613614
614615 d.mu.Lock()
615616 require.Same(t, parked, d.parked[token], "invalid resume must not consume the parked attachment")
@@ -664,7 +665,7 @@ func TestHandleHelloDefersFreshOutputUntilWelcome(t *testing.T) {
664665 tr := newWelcomeBlockingTransport(t)
665666 done := make(chan struct{})
666667 go func() {
667- d.handleHello(tr.tr, mustHello(ports.IntentNew, "welcome-gate", domain.Size{Cols: 80, Rows: 24}))
668+ d.handleHello(tr.tr, mustHello(protocol.IntentNew, "welcome-gate", domain.Size{Cols: 80, Rows: 24}))
668669 close(done)
669670 }()
670671
M internal/usecase/daemon/durable_recovery_test.go +33 -32
@@ -20,6 +20,7 @@ import (
2020 "github.com/bnema/vev/internal/domain"
2121 "github.com/bnema/vev/internal/ports"
2222 portsmocks "github.com/bnema/vev/internal/ports/mocks"
23+ "github.com/bnema/vev/internal/protocol"
2324 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"
2425 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"
2526 )
@@ -304,8 +305,8 @@ func TestCatalogueRestoreIndependent(t *testing.T) {
304305 healthyEntry := d.inactive[healthy.Name]
305306 brokenEntry := d.inactive[broken.Name]
306307 d.mu.Unlock()
307- require.Equal(t, ports.SessionDown, healthyEntry.state)
308- require.Equal(t, ports.SessionBroken, brokenEntry.state)
308+ require.Equal(t, protocol.SessionDown, healthyEntry.state)
309+ require.Equal(t, protocol.SessionBroken, brokenEntry.state)
309310 for name, done := range map[string]<-chan struct{}{healthy.Name: healthyEntry.restoreDone, broken.Name: brokenEntry.restoreDone} {
310311 select {
311312 case <-done:
@@ -344,14 +345,14 @@ func TestCatalogueRestoreResetsOnlyIncompatibleCheckpoint(t *testing.T) {
344345 require.Empty(t, fresh.DegradedReason)
345346 require.Empty(t, fresh.TabNames)
346347 require.Equal(t, []domain.IncarnationID{incompatible.IncarnationID}, repository.deleted)
347- require.Equal(t, ports.SessionDown, d.inactive[incompatible.Name].state)
348+ require.Equal(t, protocol.SessionDown, d.inactive[incompatible.Name].state)
348349 require.Equal(t, fresh, d.inactive[incompatible.Name].record)
349350
350351 restoredSibling, ok, err := catalogue.Record(healthy.Name)
351352 require.NoError(t, err)
352353 require.True(t, ok)
353354 require.Equal(t, healthy, restoredSibling)
354- require.Equal(t, ports.SessionDown, d.inactive[healthy.Name].state)
355+ require.Equal(t, protocol.SessionDown, d.inactive[healthy.Name].state)
355356 require.Equal(t, 1, repository.loads[healthy.Name])
356357 }
357358
@@ -381,7 +382,7 @@ func TestIncompatibleCheckpointDeleteFailurePublishesFreshStoppedAuthority(t *te
381382 d.mu.Lock()
382383 entry := d.inactive[record.Name]
383384 d.mu.Unlock()
384- require.Equal(t, ports.SessionDown, entry.state)
385+ require.Equal(t, protocol.SessionDown, entry.state)
385386 require.Equal(t, fresh, entry.record)
386387 require.Equal(t, restoreDone, entry.restoreDone)
387388 select {
@@ -394,7 +395,7 @@ func TestIncompatibleCheckpointDeleteFailurePublishesFreshStoppedAuthority(t *te
394395 d.mu.Lock()
395396 entry = d.inactive[record.Name]
396397 d.mu.Unlock()
397- require.Equal(t, ports.SessionDown, entry.state)
398+ require.Equal(t, protocol.SessionDown, entry.state)
398399 require.Equal(t, fresh, entry.record)
399400 select {
400401 case <-entry.restoreDone:
@@ -452,7 +453,7 @@ func TestRestoreLoadFailuresRetainCheckpointWithoutDeletion(t *testing.T) {
452453 require.Equal(t, record.IncarnationID, persisted.IncarnationID)
453454 require.Equal(t, record.Committed, persisted.Committed)
454455 require.Equal(t, "checkpoint validation failed", persisted.DegradedReason)
455- require.Equal(t, ports.SessionBroken, d.inactive[record.Name].state)
456+ require.Equal(t, protocol.SessionBroken, d.inactive[record.Name].state)
456457 require.Empty(t, repository.deleted)
457458 })
458459 }
@@ -472,7 +473,7 @@ func TestCatalogueRestoreAggregateOver4096(t *testing.T) {
472473
473474 require.Len(t, d.inactive, count)
474475 for _, record := range records {
475- require.Equal(t, ports.SessionDown, d.inactive[record.Name].state)
476+ require.Equal(t, protocol.SessionDown, d.inactive[record.Name].state)
476477 require.Equal(t, 1, repository.loads[record.Name])
477478 }
478479 }
@@ -486,7 +487,7 @@ func TestCatalogueRestoreSingleSessionOver4096(t *testing.T) {
486487 // discovery API and therefore performs exactly one direct catalogue lookup.
487488 d.restoreCatalogue(context.Background(), mustDurableRecords(t, catalogue))
488489
489- require.Equal(t, ports.SessionDown, d.inactive[record.Name].state)
490+ require.Equal(t, protocol.SessionDown, d.inactive[record.Name].state)
490491 require.Equal(t, 1, repository.loads[record.Name])
491492 }
492493
@@ -499,7 +500,7 @@ func TestCatalogueRestoreIncarnationMismatch(t *testing.T) {
499500
500501 d.restoreCatalogue(context.Background(), mustDurableRecords(t, catalogue))
501502
502- require.Equal(t, ports.SessionBroken, d.inactive[record.Name].state)
503+ require.Equal(t, protocol.SessionBroken, d.inactive[record.Name].state)
503504 got, _, _ := catalogue.Record(record.Name)
504505 require.Equal(t, "checkpoint validation failed", got.DegradedReason)
505506 }
@@ -709,7 +710,7 @@ func TestCreateNamedSessionRegistrationFailureRollsBackDurableState(t *testing.T
709710 name: record.Name, cwd: record.Cwd, createdAt: record.CreatedAt,
710711 incarnation: record.IncarnationID, lastUsedSeq: record.LastUsedSeq,
711712 tabNames: append([]string(nil), record.TabNames...), record: record,
712- state: ports.SessionDown, restoreDone: make(chan struct{}),
713+ state: protocol.SessionDown, restoreDone: make(chan struct{}),
713714 }
714715 }
715716 beforeStopped, hadStopped := d.inactive[tc.sessName]
@@ -769,7 +770,7 @@ func TestSessionRecoveryCommand(t *testing.T) {
769770
770771 _, err := (controlExec{d: d, recoveryName: degraded.Name}).SessionRecovery("discard")
771772 require.NoError(t, err)
772- require.Equal(t, ports.SessionDown, d.inactive[degraded.Name].state)
773+ require.Equal(t, protocol.SessionDown, d.inactive[degraded.Name].state)
773774 require.Equal(t, fresh, d.inactive[degraded.Name].record)
774775 }
775776
@@ -782,10 +783,10 @@ func TestListShowsDegraded(t *testing.T) {
782783 d, _ := newDurableRecoveryDaemon(t, []domain.CatalogueRecord{fresh, restoring, degraded}, &durableRecoveryRepository{})
783784
784785 listed := listSessions(t, d)
785- require.Equal(t, []ports.SessionInfo{
786- {Name: fresh.Name, State: ports.SessionDown},
787- {Name: restoring.Name, State: ports.SessionDown},
788- {Name: degraded.Name, State: ports.SessionBroken},
786+ require.Equal(t, []protocol.SessionInfo{
787+ {Name: fresh.Name, State: protocol.SessionDown},
788+ {Name: restoring.Name, State: protocol.SessionDown},
789+ {Name: degraded.Name, State: protocol.SessionBroken},
789790 }, listed.Sessions)
790791 }
791792
@@ -795,7 +796,7 @@ func TestListHidesPurgingInactiveSession(t *testing.T) {
795796 d.mu.Lock()
796797 entry := d.inactive[record.Name]
797798 entry.purging = true
798- entry.state = ports.SessionDown
799+ entry.state = protocol.SessionDown
799800 d.inactive[record.Name] = entry
800801 d.mu.Unlock()
801802
@@ -812,7 +813,7 @@ func TestAttachWaitsForRestore(t *testing.T) {
812813
813814 result := make(chan error, 1)
814815 go func() {
815- _, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: record.Name, Size: domain.Size{Cols: 80, Rows: 24}}, nil)
816+ _, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: record.Name, Size: domain.Size{Cols: 80, Rows: 24}}, nil)
816817 result <- err
817818 }()
818819
@@ -823,13 +824,13 @@ func TestAttachWaitsForRestore(t *testing.T) {
823824 }
824825 require.Zero(t, factory.calls.Load())
825826
826- d.setStoppedRecovery(record, ports.SessionBroken)
827+ d.setStoppedRecovery(record, protocol.SessionBroken)
827828 d.mu.Lock()
828829 closeRuntimeRestoreDoneLocked(d.inactive[record.Name].restoreDone)
829830 d.mu.Unlock()
830831 var protocolError *protoErr
831832 require.ErrorAs(t, <-result, &protocolError)
832- require.Equal(t, ports.ErrInternal, protocolError.code)
833+ require.Equal(t, protocol.ErrInternal, protocolError.code)
833834 require.Zero(t, factory.calls.Load())
834835 }
835836
@@ -885,17 +886,17 @@ func TestRestoreCancellationTransitionsBeforeAttachCompletion(t *testing.T) {
885886
886887 attached := make(chan error, 1)
887888 go func() {
888- _, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: record.Name}, nil)
889+ _, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: record.Name}, nil)
889890 attached <- err
890891 }()
891892 cancelRestore()
892893
893894 var protocolError *protoErr
894895 require.ErrorAs(t, <-attached, &protocolError)
895- require.Equal(t, ports.ErrInternal, protocolError.code)
896+ require.Equal(t, protocol.ErrInternal, protocolError.code)
896897 <-restored
897898 entry := d.inactive[record.Name]
898- require.Equal(t, ports.SessionBroken, entry.state)
899+ require.Equal(t, protocol.SessionBroken, entry.state)
899900 require.Equal(t, "restore interrupted", entry.record.DegradedReason)
900901 select {
901902 case <-entry.restoreDone:
@@ -920,7 +921,7 @@ func TestWaitForTargetRestoreRejectsClosedRestoringChannel(t *testing.T) {
920921 case err := <-result:
921922 var protocolError *protoErr
922923 require.ErrorAs(t, err, &protocolError)
923- require.Equal(t, ports.ErrInternal, protocolError.code)
924+ require.Equal(t, protocol.ErrInternal, protocolError.code)
924925 case <-time.After(time.Second):
925926 t.Fatal("waiter spun on a closed restore channel")
926927 }
@@ -932,11 +933,11 @@ func TestWaitForTargetRestoreDistinguishesHealthyRuntime(t *testing.T) {
932933 record := durableRecoveryRecord(0)
933934 tests := []struct {
934935 name string
935- state ports.SessionState
936+ state protocol.SessionState
936937 message string
937938 }{
938- {name: "healthy record without runtime", state: ports.SessionDown, message: "session was not restored into this daemon: " + record.Name},
939- {name: "broken durable state", state: ports.SessionBroken, message: "session durable state is broken: " + record.Name},
939+ {name: "healthy record without runtime", state: protocol.SessionDown, message: "session was not restored into this daemon: " + record.Name},
940+ {name: "broken durable state", state: protocol.SessionBroken, message: "session durable state is broken: " + record.Name},
940941 }
941942 for _, tt := range tests {
942943 t.Run(tt.name, func(t *testing.T) {
@@ -948,7 +949,7 @@ func TestWaitForTargetRestoreDistinguishesHealthyRuntime(t *testing.T) {
948949
949950 var protocolError *protoErr
950951 require.ErrorAs(t, d.waitForTargetRestore(context.Background(), record.Name), &protocolError)
951- require.Equal(t, ports.ErrInternal, protocolError.code)
952+ require.Equal(t, protocol.ErrInternal, protocolError.code)
952953 require.Equal(t, tt.message, protocolError.Error())
953954 })
954955 }
@@ -962,10 +963,10 @@ func TestAttachRejectsDegraded(t *testing.T) {
962963 d.serveCtx, d.serveCancel = context.WithCancel(context.Background())
963964 defer d.serveCancel()
964965
965- _, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: record.Name}, nil)
966+ _, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: record.Name}, nil)
966967 var protocolError *protoErr
967968 require.ErrorAs(t, err, &protocolError)
968- require.Equal(t, ports.ErrInternal, protocolError.code)
969+ require.Equal(t, protocol.ErrInternal, protocolError.code)
969970 require.Zero(t, factory.calls.Load())
970971 }
971972
@@ -977,7 +978,7 @@ func TestRestoreUnavailableReleasesPerRecordWaiters(t *testing.T) {
977978 d.mu.Lock()
978979 entry := d.inactive[record.Name]
979980 d.mu.Unlock()
980- require.Equal(t, ports.SessionBroken, entry.state)
981+ require.Equal(t, protocol.SessionBroken, entry.state)
981982 select {
982983 case <-entry.restoreDone:
983984 default:
@@ -993,6 +994,6 @@ func TestCatalogueRestoreDegradedVisible(t *testing.T) {
993994
994995 d.restoreCatalogue(context.Background(), mustDurableRecords(t, catalogue))
995996
996- require.Equal(t, ports.SessionBroken, d.inactive[record.Name].state)
997+ require.Equal(t, protocol.SessionBroken, d.inactive[record.Name].state)
997998 require.Zero(t, repository.loads[record.Name])
998999 }
M internal/usecase/daemon/exact_route_test.go +12 -13
@@ -5,7 +5,6 @@ import (
55 "testing"
66
77 "github.com/bnema/vev/internal/domain"
8- "github.com/bnema/vev/internal/ports"
98 "github.com/bnema/vev/internal/protocol"
109 "github.com/stretchr/testify/require"
1110 )
@@ -17,9 +16,9 @@ func TestRouteExactSessionTargetSelectsLifecycle(t *testing.T) {
1716 sess.incarnation = remoteLifecycleForTest()
1817 tr, _ := newCapturingTransport(t)
1918 target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
20- hello := ports.Hello{
21- Version: ports.ProtocolVersion,
22- Intent: ports.IntentAttach,
19+ hello := protocol.Hello{
20+ Version: protocol.Version,
21+ Intent: protocol.IntentAttach,
2322 Name: target.SessionName,
2423 Size: domain.Size{Cols: 80, Rows: 24},
2524 ExactTarget: &target,
@@ -38,9 +37,9 @@ func TestRouteExactSessionTargetRejectsNameMismatch(t *testing.T) {
3837 sess.ephemeral = false
3938 sess.incarnation = remoteLifecycleForTest()
4039 tr, _ := newCapturingTransport(t)
41- hello := ports.Hello{
42- Version: ports.ProtocolVersion,
43- Intent: ports.IntentAttach,
40+ hello := protocol.Hello{
41+ Version: protocol.Version,
42+ Intent: protocol.IntentAttach,
4443 Name: "other",
4544 Size: domain.Size{Cols: 80, Rows: 24},
4645 ExactTarget: &protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name},
@@ -49,7 +48,7 @@ func TestRouteExactSessionTargetRejectsNameMismatch(t *testing.T) {
4948 _, _, err := d.routeWithContext(context.Background(), hello, tr)
5049 var protocolErr *protoErr
5150 require.ErrorAs(t, err, &protocolErr)
52- require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)
51+ require.Equal(t, protocol.ErrNoSuchSession, protocolErr.code)
5352 }
5453
5554 func TestLockedExactSessionTargetRejectsReplacement(t *testing.T) {
@@ -68,7 +67,7 @@ func TestLockedExactSessionTargetRejectsReplacement(t *testing.T) {
6867
6968 var protocolErr *protoErr
7069 require.ErrorAs(t, err, &protocolErr)
71- require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)
70+ require.Equal(t, protocol.ErrNoSuchSession, protocolErr.code)
7271 }
7372
7473 func TestRouteExactSessionTargetRejectsLifecycleReplacement(t *testing.T) {
@@ -79,9 +78,9 @@ func TestRouteExactSessionTargetRejectsLifecycleReplacement(t *testing.T) {
7978 tr, _ := newCapturingTransport(t)
8079 wrong := sess.incarnation
8180 wrong[0]++
82- hello := ports.Hello{
83- Version: ports.ProtocolVersion,
84- Intent: ports.IntentAttach,
81+ hello := protocol.Hello{
82+ Version: protocol.Version,
83+ Intent: protocol.IntentAttach,
8584 Name: sess.name,
8685 Size: domain.Size{Cols: 80, Rows: 24},
8786 ExactTarget: &protocol.ExactSessionTarget{LifecycleID: wrong, SessionName: sess.name},
@@ -90,5 +89,5 @@ func TestRouteExactSessionTargetRejectsLifecycleReplacement(t *testing.T) {
9089 _, _, err := d.routeWithContext(context.Background(), hello, tr)
9190 var protocolErr *protoErr
9291 require.ErrorAs(t, err, &protocolErr)
93- require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)
92+ require.Equal(t, protocol.ErrNoSuchSession, protocolErr.code)
9493 }
M internal/usecase/daemon/final_acceptance_test.go +2 -1
@@ -6,6 +6,7 @@ import (
66 vt "github.com/bnema/vev-vt"
77 "github.com/bnema/vev/internal/domain"
88 "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
910 "github.com/bnema/vev/internal/usecase/layout"
1011 "github.com/stretchr/testify/require"
1112 )
@@ -150,7 +151,7 @@ func TestAcceptanceAttachmentStateIsolationAcrossResetResizeAndDetach(t *testing
150151
151152 d.handleAttachmentClientFrame(secondToken, ports.Frame{
152153 Type: ports.MsgOutputResetRequest,
153- Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{}),
154+ Payload: ports.MarshalOutputResetRequest(protocol.OutputResetRequest{}),
154155 })
155156 require.Greater(t, second.output.currentEpoch(), secondEpoch)
156157 require.Equal(t, firstEpoch, first.output.currentEpoch(), "peer output reset crossed attachment boundary")
M internal/usecase/daemon/floating_open_test.go +3 -2
@@ -14,6 +14,7 @@ import (
1414 "github.com/bnema/vev/internal/domain"
1515 "github.com/bnema/vev/internal/ports"
1616 portsmocks "github.com/bnema/vev/internal/ports/mocks"
17+ "github.com/bnema/vev/internal/protocol"
1718 )
1819
1920 type contextAwareFloatingPTY struct {
@@ -94,7 +95,7 @@ func TestFloatingSuccessfulOpenTransfersContextOwnershipToPane(t *testing.T) {
9495 name: "visible session",
9596 desiredVisible: true,
9697 teardown: func(d *Daemon, sess *session, _ *tab) {
97- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))
98+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
9899 },
99100 },
100101 }
@@ -412,7 +413,7 @@ func TestFloatingOpenCancellationBoundsSessionTeardownAndClosesLatePTY(t *testin
412413 }
413414
414415 killed := make(chan error, 1)
415- go func() { killed <- d.killSession(sess, ports.ReasonSessionKilled, false) }()
416+ go func() { killed <- d.killSession(sess, protocol.ReasonSessionKilled, false) }()
416417 select {
417418 case err := <-killed:
418419 require.NoError(t, err)
M internal/usecase/daemon/floating_test.go +2 -1
@@ -17,6 +17,7 @@ import (
1717 "github.com/bnema/vev/internal/domain"
1818 "github.com/bnema/vev/internal/ports"
1919 portsmocks "github.com/bnema/vev/internal/ports/mocks"
20+ "github.com/bnema/vev/internal/protocol"
2021 scopy "github.com/bnema/vev/internal/usecase/copy"
2122 "github.com/bnema/vev/internal/usecase/layout"
2223 themeui "github.com/bnema/vev/internal/usecase/theme"
@@ -324,7 +325,7 @@ func TestFloatingLaunchOwnershipJoinsShutdown(t *testing.T) {
324325 case <-time.After(time.Second):
325326 t.Fatal("floating reader did not start")
326327 }
327- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))
328+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
328329 select {
329330 case <-waitGroupDone(&d.sessWg):
330331 case <-time.After(time.Second):
M internal/usecase/daemon/geometry_propagation_test.go +3 -2
@@ -10,6 +10,7 @@ import (
1010 vt "github.com/bnema/vev-vt"
1111 "github.com/bnema/vev/internal/domain"
1212 "github.com/bnema/vev/internal/ports"
13+ "github.com/bnema/vev/internal/protocol"
1314 "github.com/stretchr/testify/require"
1415 )
1516
@@ -200,14 +201,14 @@ func TestResumeReappliesClaimingPixelGeometryBeforeReturn(t *testing.T) {
200201 pty := &blockingRecordingGeometryPTY{done: make(chan struct{})}
201202 t.Cleanup(func() { _ = pty.Close() })
202203 d := newTestDaemon(t, &recordingPTYFactory{pty: pty}, stubClock{})
203- firstHello := helloResumeCapable(ports.IntentNew, "pixels", 0)
204+ firstHello := helloResumeCapable(protocol.IntentNew, "pixels", 0)
204205 firstHello.PixelWidth, firstHello.PixelHeight = 800, 480
205206 firstTransport := &closeTrackingTransport{}
206207 sess, ac, err := d.route(firstHello, firstTransport)
207208 require.NoError(t, err)
208209 d.clientGone(sess, ac, firstTransport, false)
209210
210- resumeHello := helloResumeCapable(ports.IntentResume, "pixels", ac.resumeToken)
211+ resumeHello := helloResumeCapable(protocol.IntentResume, "pixels", ac.resumeToken)
211212 resumeHello.PixelWidth, resumeHello.PixelHeight = 1000, 600
212213 resumed, same, ok, err := d.resumeParked(resumeHello, &closeTrackingTransport{}, resumeHello.Size)
213214 require.NoError(t, err)
M internal/usecase/daemon/graphics_output_test.go +3 -2
@@ -14,6 +14,7 @@ import (
1414 "github.com/bnema/vev-vt/graphics"
1515 "github.com/bnema/vev/internal/domain"
1616 "github.com/bnema/vev/internal/ports"
17+ "github.com/bnema/vev/internal/protocol"
1718 "github.com/bnema/vev/internal/usecase/layout"
1819 "github.com/stretchr/testify/require"
1920 )
@@ -149,7 +150,7 @@ func TestFreshCapabilityDowngradePreservesForeignTerminalGraphics(t *testing.T)
149150 ac.output.graphicsOutput = newGraphicsOutputState()
150151 ac.output.graphicsUnsupportedWarned.Store(true)
151152 d.mu.Lock()
152- d.reconfigureAttachmentOutput(sess, ac, ports.Hello{KittyDirectGraphics: false})
153+ d.reconfigureAttachmentOutput(sess, ac, protocol.Hello{KittyDirectGraphics: false})
153154 d.mu.Unlock()
154155 require.False(t, ac.terminalCapabilities.SupportsKittyGraphics())
155156 require.Nil(t, ac.output.graphicsOutput)
@@ -184,7 +185,7 @@ func TestReplacementKittyTerminalGetsFreshGraphicsState(t *testing.T) {
184185 oldState.mayHaveEmitted = true
185186 ac.output.graphicsOutput = oldState
186187 ac.output.graphicsUnsupportedWarned.Store(true)
187- d.reconfigureAttachmentOutput(sess, ac, ports.Hello{KittyDirectGraphics: true})
188+ d.reconfigureAttachmentOutput(sess, ac, protocol.Hello{KittyDirectGraphics: true})
188189 d.mu.Unlock()
189190
190191 require.True(t, ac.terminalCapabilities.SupportsKittyGraphics())
M internal/usecase/daemon/handshake.go +4 -3
@@ -6,6 +6,7 @@ import (
66 "sync"
77
88 "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
910 )
1011
1112 var errHandshakeTimeout = errors.New("handshake timed out")
@@ -22,9 +23,9 @@ func (d *Daemon) newHandshakeContext(parent context.Context) (context.Context, <
2223 if d != nil && d.clock != nil {
2324 clock = d.clock
2425 }
25- timer := clock.NewTimer(ports.HandshakeTimeout)
26+ timer := clock.NewTimer(protocol.HandshakeTimeout)
2627 if timer == nil {
27- timer = systemClock{}.NewTimer(ports.HandshakeTimeout)
28+ timer = systemClock{}.NewTimer(protocol.HandshakeTimeout)
2829 }
2930 stop := make(chan struct{})
3031 done := make(chan struct{})
@@ -153,6 +154,6 @@ func (d *Daemon) failHandshakeAttachment(sess *session, ac *attachedClient, tr p
153154 _ = tr.Close()
154155 }
155156 if ac.routeCreatedSession {
156- _ = d.killSessionIfEmpty(sess, ports.ReasonSessionKilled, ac.routeSessionPurge)
157+ _ = d.killSessionIfEmpty(sess, protocol.ReasonSessionKilled, ac.routeSessionPurge)
157158 }
158159 }
M internal/usecase/daemon/handshake_test.go +19 -18
@@ -11,6 +11,7 @@ import (
1111
1212 "github.com/bnema/vev/internal/domain"
1313 "github.com/bnema/vev/internal/ports"
14+ "github.com/bnema/vev/internal/protocol"
1415 )
1516
1617 type handshakeBlockingTransport struct {
@@ -128,7 +129,7 @@ func TestHandshakeFirstPaintCancellationDoesNotRacePaintResult(t *testing.T) {
128129 handshakeDone := make(chan struct{})
129130 go func() {
130131 d.handleHelloWithContext(ctx, make(chan struct{}), func() {}, func() {}, tr,
131- mustHello(ports.IntentNew, "paint-cancel", domain.Size{Cols: 80, Rows: 24}))
132+ mustHello(protocol.IntentNew, "paint-cancel", domain.Size{Cols: 80, Rows: 24}))
132133 close(handshakeDone)
133134 }()
134135
@@ -156,7 +157,7 @@ func TestHandshakeTimeoutClosesBlockedReceive(t *testing.T) {
156157 }()
157158
158159 timer := <-clock.timers
159- require.Equal(t, ports.HandshakeTimeout, timer.duration)
160+ require.Equal(t, protocol.HandshakeTimeout, timer.duration)
160161 timer.ch <- time.Time{}
161162 awaitTestCompletion(t, done, "handshake receive did not stop after timeout")
162163 requireClosedHandshakeTransport(t, tr)
@@ -171,7 +172,7 @@ func TestFailedResumeHandshakeRestoresParkedCredential(t *testing.T) {
171172 defer release()
172173 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
173174 oldTransport := newHandshakeBlockingTransport(false)
174- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "resume-timeout", 0), oldTransport)
175+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "resume-timeout", 0), oldTransport)
175176 require.NoError(t, err)
176177 token := ac.resumeToken
177178 d.clientGone(sess, ac, oldTransport, false)
@@ -182,12 +183,12 @@ func TestFailedResumeHandshakeRestoresParkedCredential(t *testing.T) {
182183 resumeTransport := newHandshakeBlockingTransport(true)
183184 done := make(chan struct{})
184185 go func() {
185- d.handleHello(resumeTransport, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(helloResumeCapable(ports.IntentResume, sess.name, token))})
186+ d.handleHello(resumeTransport, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(helloResumeCapable(protocol.IntentResume, sess.name, token))})
186187 close(done)
187188 }()
188189
189190 timer := <-clock.timers
190- require.Equal(t, ports.HandshakeTimeout, timer.duration)
191+ require.Equal(t, protocol.HandshakeTimeout, timer.duration)
191192 <-resumeTransport.welcome
192193 timer.ch <- time.Time{}
193194 awaitTestCompletion(t, done, "failed resume handshake did not finish")
@@ -210,12 +211,12 @@ func TestHandshakeTimeoutClosesBlockedWelcomeSend(t *testing.T) {
210211 tr := newHandshakeBlockingTransport(true)
211212 done := make(chan struct{})
212213 go func() {
213- d.handleHello(tr, mustHello(ports.IntentNew, "timeout-send", domain.Size{Cols: 80, Rows: 24}))
214+ d.handleHello(tr, mustHello(protocol.IntentNew, "timeout-send", domain.Size{Cols: 80, Rows: 24}))
214215 close(done)
215216 }()
216217
217218 timer := <-clock.timers
218- require.Equal(t, ports.HandshakeTimeout, timer.duration)
219+ require.Equal(t, protocol.HandshakeTimeout, timer.duration)
219220 <-tr.welcome
220221 timer.ch <- time.Time{}
221222 awaitTestCompletion(t, done, "handshake send did not stop after timeout")
@@ -246,12 +247,12 @@ func TestHandshakeTimeoutCancelsRouteRestoreWait(t *testing.T) {
246247 tr := newHandshakeBlockingTransport(false)
247248 done := make(chan struct{})
248249 go func() {
249- d.handleHello(tr, mustHello(ports.IntentAttach, "restoring", domain.Size{Cols: 80, Rows: 24}))
250+ d.handleHello(tr, mustHello(protocol.IntentAttach, "restoring", domain.Size{Cols: 80, Rows: 24}))
250251 close(done)
251252 }()
252253
253254 timer := <-clock.timers
254- require.Equal(t, ports.HandshakeTimeout, timer.duration)
255+ require.Equal(t, protocol.HandshakeTimeout, timer.duration)
255256 timer.ch <- time.Time{}
256257 awaitTestCompletion(t, done, "route restore wait did not stop after handshake timeout")
257258 requireClosedHandshakeTransport(t, tr)
@@ -266,7 +267,7 @@ func TestHandshakeTimeoutRemovesRestoredEmptySession(t *testing.T) {
266267 defer release()
267268 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
268269 d.mu.Lock()
269- d.inactive["restored"] = inactiveSession{name: "restored", cwd: "/tmp", state: ports.SessionDown}
270+ d.inactive["restored"] = inactiveSession{name: "restored", cwd: "/tmp", state: protocol.SessionDown}
270271 d.mu.Unlock()
271272
272273 clock := &signalClock{timers: make(chan *signalTimer, 4)}
@@ -274,12 +275,12 @@ func TestHandshakeTimeoutRemovesRestoredEmptySession(t *testing.T) {
274275 tr := newHandshakeBlockingTransport(true)
275276 done := make(chan struct{})
276277 go func() {
277- d.handleHello(tr, mustHello(ports.IntentAttach, "restored", domain.Size{Cols: 80, Rows: 24}))
278+ d.handleHello(tr, mustHello(protocol.IntentAttach, "restored", domain.Size{Cols: 80, Rows: 24}))
278279 close(done)
279280 }()
280281
281282 timer := <-clock.timers
282- require.Equal(t, ports.HandshakeTimeout, timer.duration)
283+ require.Equal(t, protocol.HandshakeTimeout, timer.duration)
283284 <-tr.welcome
284285 timer.ch <- time.Time{}
285286 awaitTestCompletion(t, done, "timed-out restored attachment cleanup did not finish")
@@ -296,7 +297,7 @@ func TestHandshakeTimeoutPreservesUnrelatedAttachment(t *testing.T) {
296297 defer release()
297298 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
298299 oldTransport := &closeTrackingTransport{}
299- sess, old, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "shared", Size: domain.Size{Cols: 80, Rows: 24}}, oldTransport)
300+ sess, old, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "shared", Size: domain.Size{Cols: 80, Rows: 24}}, oldTransport)
300301 require.NoError(t, err)
301302
302303 clock := &signalClock{timers: make(chan *signalTimer, 1)}
@@ -304,12 +305,12 @@ func TestHandshakeTimeoutPreservesUnrelatedAttachment(t *testing.T) {
304305 tr := newHandshakeBlockingTransport(true)
305306 done := make(chan struct{})
306307 go func() {
307- d.handleHello(tr, mustHello(ports.IntentAttach, "shared", domain.Size{Cols: 80, Rows: 24}))
308+ d.handleHello(tr, mustHello(protocol.IntentAttach, "shared", domain.Size{Cols: 80, Rows: 24}))
308309 close(done)
309310 }()
310311
311312 timer := <-clock.timers
312- require.Equal(t, ports.HandshakeTimeout, timer.duration)
313+ require.Equal(t, protocol.HandshakeTimeout, timer.duration)
313314 <-tr.welcome
314315 timer.ch <- time.Time{}
315316 awaitTestCompletion(t, done, "timed-out attachment cleanup did not finish")
@@ -321,7 +322,7 @@ func TestHandshakeTimeoutPreservesUnrelatedAttachment(t *testing.T) {
321322 require.Len(t, d.sessions, 1)
322323 require.Empty(t, d.parked)
323324 d.mu.Unlock()
324- _ = d.killSession(sess, ports.ReasonServerShutdown, false)
325+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
325326 }
326327
327328 func TestSuccessfulRouteOutlivesStoppedHandshakeContext(t *testing.T) {
@@ -329,7 +330,7 @@ func TestSuccessfulRouteOutlivesStoppedHandshakeContext(t *testing.T) {
329330 defer release()
330331 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
331332 ctx, cancel := context.WithCancel(context.Background())
332- sess, ac, err := d.routeWithContext(ctx, ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "lifetime", Size: domain.Size{Cols: 80, Rows: 24}}, &closeTrackingTransport{})
333+ sess, ac, err := d.routeWithContext(ctx, protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "lifetime", Size: domain.Size{Cols: 80, Rows: 24}}, &closeTrackingTransport{})
333334 require.NoError(t, err)
334335 cancel()
335336 select {
@@ -338,5 +339,5 @@ func TestSuccessfulRouteOutlivesStoppedHandshakeContext(t *testing.T) {
338339 default:
339340 }
340341 require.NotNil(t, ac)
341- _ = d.killSession(sess, ports.ReasonServerShutdown, false)
342+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
342343 }
M internal/usecase/daemon/inactive_session_test.go +7 -7
@@ -6,7 +6,7 @@ import (
66 "github.com/stretchr/testify/require"
77
88 "github.com/bnema/vev/internal/domain"
9- "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
1010 )
1111
1212 func TestInactiveSessionPredicates(t *testing.T) {
@@ -22,12 +22,12 @@ func TestInactiveSessionPredicates(t *testing.T) {
2222 canResume bool
2323 restorePending bool
2424 }{
25- {name: "healthy down", entry: inactiveSession{state: ports.SessionDown}, visible: true, canResume: true},
26- {name: "restore pending remains resumable after wait", entry: inactiveSession{state: ports.SessionDown, restoreDone: pending}, visible: true, canResume: true, restorePending: true},
27- {name: "settled restore", entry: inactiveSession{state: ports.SessionDown, restoreDone: settled}, visible: true, canResume: true},
28- {name: "broken", entry: inactiveSession{state: ports.SessionBroken}, visible: true, broken: true},
29- {name: "degraded", entry: inactiveSession{state: ports.SessionDown, record: domain.CatalogueRecord{DegradedReason: "checkpoint unavailable"}}, visible: true, broken: true},
30- {name: "purging", entry: inactiveSession{state: ports.SessionDown, purging: true}},
25+ {name: "healthy down", entry: inactiveSession{state: protocol.SessionDown}, visible: true, canResume: true},
26+ {name: "restore pending remains resumable after wait", entry: inactiveSession{state: protocol.SessionDown, restoreDone: pending}, visible: true, canResume: true, restorePending: true},
27+ {name: "settled restore", entry: inactiveSession{state: protocol.SessionDown, restoreDone: settled}, visible: true, canResume: true},
28+ {name: "broken", entry: inactiveSession{state: protocol.SessionBroken}, visible: true, broken: true},
29+ {name: "degraded", entry: inactiveSession{state: protocol.SessionDown, record: domain.CatalogueRecord{DegradedReason: "checkpoint unavailable"}}, visible: true, broken: true},
30+ {name: "purging", entry: inactiveSession{state: protocol.SessionDown, purging: true}},
3131 }
3232
3333 for _, test := range tests {
M internal/usecase/daemon/input_test.go +1 -1
@@ -884,7 +884,7 @@ func TestAltDDetachesCurrentClient(t *testing.T) {
884884 f := awaitFrame(t, sends, ports.MsgDetached)
885885 det, err := ports.UnmarshalDetached(f.Payload)
886886 require.NoError(t, err)
887- require.Equal(t, ports.ReasonDetach, det.Reason)
887+ require.Equal(t, protocol.ReasonDetach, det.Reason)
888888 }
889889
890890 func TestRNSOpensPromptAndEnterPromotesEphemeralSession(t *testing.T) {
M internal/usecase/daemon/move_gate_race_test.go +3 -3
@@ -8,7 +8,7 @@ import (
88 "github.com/stretchr/testify/require"
99
1010 "github.com/bnema/vev/internal/domain"
11- "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
1212 )
1313
1414 func newMoveGateRaceFixture(t *testing.T) (*Daemon, *session, *tab, *pane, *session, *tab, func()) {
@@ -93,7 +93,7 @@ func TestMovePaneGateReservationRaceKillWinsWithoutDeadlock(t *testing.T) {
9393 waitMoveRace(t, moveReserved, "move reservation")
9494
9595 killDone := make(chan error, 1)
96- go func() { killDone <- d.killSession(source, ports.ReasonSessionKilled, false) }()
96+ go func() { killDone <- d.killSession(source, protocol.ReasonSessionKilled, false) }()
9797 waitMoveRace(t, killFrozen, "kill gate freeze")
9898 close(releaseMoveAdmission)
9999 moveErr := waitMoveRace(t, moveDone, "move abort after kill gate acquisition")
@@ -277,7 +277,7 @@ func TestMovePaneGateReservationRaceMoveWinsWithoutDeadlock(t *testing.T) {
277277 waitMoveRace(t, moveGateFrozen, "move gate freeze")
278278
279279 killDone := make(chan error, 1)
280- go func() { killDone <- d.killSession(source, ports.ReasonSessionKilled, false) }()
280+ go func() { killDone <- d.killSession(source, protocol.ReasonSessionKilled, false) }()
281281 waitMoveRace(t, killSnapshotted, "kill participant snapshot")
282282 close(releaseMove)
283283
M internal/usecase/daemon/move_lifecycle_test.go +2 -2
@@ -4,7 +4,7 @@ import (
44 "testing"
55 "time"
66
7- "github.com/bnema/vev/internal/ports"
7+ "github.com/bnema/vev/internal/protocol"
88 "github.com/stretchr/testify/require"
99 )
1010
@@ -230,7 +230,7 @@ func TestDaemonShutdownDrainsMoveBeforePaneLifetimeCancellation(t *testing.T) {
230230 require.NotNil(t, paneLifetime)
231231
232232 shutdown := make(chan bool, 1)
233- go func() { shutdown <- d.shutdownAll(ports.ReasonServerShutdown) }()
233+ go func() { shutdown <- d.shutdownAll(protocol.ReasonServerShutdown) }()
234234 awaitDaemonMoveClosing(t, d)
235235 select {
236236 case <-paneLifetime.Done():
M internal/usecase/daemon/move_postcommit.go +2 -2
@@ -6,7 +6,7 @@ import (
66 "os"
77
88 "github.com/bnema/vev/internal/domain"
9- "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
1010 )
1111
1212 // movePostcommitPlan is captured while the move's architecture and owner
@@ -148,7 +148,7 @@ func (p movePostcommitPlan) execute(d *Daemon) {
148148 if err := d.cleanupAttachmentOutput(attachment.ac); err != nil {
149149 d.log.Warn("move attachment output cleanup failed", "err", err, "operation", p.operation, "source_session", p.sourceName)
150150 }
151- d.notifyDetachedSnapshotAsync(attachment, ports.ReasonSessionKilled)
151+ d.notifyDetachedSnapshotAsync(attachment, protocol.ReasonSessionKilled)
152152 }
153153 for _, retirement := range p.retiredParked {
154154 if retirement.parked == nil || retirement.parked.ac == nil {
M internal/usecase/daemon/move_rejection.go +3 -3
@@ -4,7 +4,7 @@ import (
44 "errors"
55
66 "github.com/bnema/vev/internal/domain"
7- "github.com/bnema/vev/internal/ports"
7+ "github.com/bnema/vev/internal/protocol"
88 "github.com/bnema/vev/internal/usecase/layout"
99 )
1010
@@ -70,7 +70,7 @@ func moveRejectionDescriptorByReason(reason moveRejectionReason) moveRejectionDe
7070 PaletteCode: domain.NoticeInternal,
7171 PaletteSeverity: domain.NoticeError,
7272 PaletteText: "Move failed.",
73- CommandCode: ports.ErrNoSuchTarget,
73+ CommandCode: protocol.ErrNoSuchTarget,
7474 CommandText: "move request is invalid",
7575 }
7676 switch reason {
@@ -129,7 +129,7 @@ func isMoveCommandError(err error) bool {
129129 return ok
130130 }
131131
132-func moveCommandFailure(err error) ports.CommandResult {
132+func moveCommandFailure(err error) protocol.CommandResult {
133133 descriptor, ok := moveRejectionDescriptorFor(err)
134134 if !ok {
135135 descriptor = moveRejectionDescriptorByReason(moveRejectionInvalid)
M internal/usecase/daemon/move_rejection_test.go +2 -2
@@ -7,7 +7,7 @@ import (
77 "github.com/stretchr/testify/require"
88
99 "github.com/bnema/vev/internal/domain"
10- "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
1111 "github.com/bnema/vev/internal/usecase/layout"
1212 )
1313
@@ -89,7 +89,7 @@ func TestMoveRejectionPresentationParity(t *testing.T) {
8989 require.False(t, result.OK)
9090 require.Equal(t, descriptor.CommandCode, result.Code)
9191 require.Equal(t, descriptor.CommandText, result.Text)
92- require.Equal(t, ports.ErrNoSuchTarget, result.Code)
92+ require.Equal(t, protocol.ErrNoSuchTarget, result.Code)
9393 require.Equal(t, tt.commandText, result.Text)
9494 })
9595 }
M internal/usecase/daemon/navigation_attachment_handoff_test.go +3 -3
@@ -6,7 +6,7 @@ import (
66 "time"
77
88 "github.com/bnema/vev/internal/domain"
9- "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
1010 "github.com/bnema/vev/internal/usecase/layout"
1111 "github.com/bnema/vev/internal/usecase/picker"
1212 "github.com/stretchr/testify/require"
@@ -51,7 +51,7 @@ func TestNavigationHandoffsDropReplacedInitiatorWithoutMutation(t *testing.T) {
5151 run: func(d *Daemon, _ *session, target *session, _ *attachedClient, effect *attachmentEffect) error {
5252 d.mu.Lock()
5353 delete(d.sessions, target.id)
54- d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 9, incarnation: domain.IncarnationID{2}, state: ports.SessionDown}
54+ d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 9, incarnation: domain.IncarnationID{2}, state: protocol.SessionDown}
5555 d.mu.Unlock()
5656 expectedCreatedAt := int64(9)
5757 return d.switchToTargetForAttachment(effect, picker.Target{Name: "stopped", Stopped: true, ExpectedCreatedAt: &expectedCreatedAt}, sessionHandoffGuard{}, "stopped-session")
@@ -149,7 +149,7 @@ func TestNavigationHandoffsDropReplacedInitiatorWithoutMutation(t *testing.T) {
149149 func TestStoppedSessionHandoffDoesNotResumeAfterInitiatorReplacement(t *testing.T) {
150150 d, source, old, _ := newManualSessionWithPTYs(t, nil)
151151 d.mu.Lock()
152- d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 7, incarnation: domain.IncarnationID{3}, state: ports.SessionDown}
152+ d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 7, incarnation: domain.IncarnationID{3}, state: protocol.SessionDown}
153153 d.mu.Unlock()
154154
155155 rc := d.attachCoordinator(source, nil, old, true)
M internal/usecase/daemon/nil_pty_test.go +2 -2
@@ -9,7 +9,7 @@ import (
99 "github.com/stretchr/testify/require"
1010
1111 "github.com/bnema/vev/internal/domain"
12- "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
1313 "github.com/bnema/vev/internal/usecase/layout"
1414 )
1515
@@ -51,7 +51,7 @@ func TestKillSessionIgnoresNilPanePTYs(t *testing.T) {
5151 d.sessions[sess.id] = sess
5252
5353 require.NotPanics(t, func() {
54- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))
54+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
5555 })
5656 require.NotContains(t, d.sessions, sess.id)
5757 }
M internal/usecase/daemon/notice_persist_test.go +4 -4
@@ -6,8 +6,8 @@ import (
66 "github.com/stretchr/testify/require"
77
88 "github.com/bnema/vev/internal/domain"
9- "github.com/bnema/vev/internal/ports"
109 portsmocks "github.com/bnema/vev/internal/ports/mocks"
10+ "github.com/bnema/vev/internal/protocol"
1111 )
1212
1313 // TestKillSessionServerShutdownSaturatedAppendsNotice proves that when the
@@ -37,9 +37,9 @@ func TestStartupClaimRecordsAndToastsPendingNotices(t *testing.T) {
3737 require.Len(t, d.notices.history(), 2, "each drained notice is recorded individually")
3838
3939 tr, _ := newCapturingTransport(t)
40- sess, ac, err := d.route(ports.Hello{
41- Version: ports.ProtocolVersion,
42- Intent: ports.IntentEphemeral,
40+ sess, ac, err := d.route(protocol.Hello{
41+ Version: protocol.Version,
42+ Intent: protocol.IntentEphemeral,
4343 Size: domain.Size{Cols: 80, Rows: 24},
4444 }, tr)
4545 require.NoError(t, err)
M internal/usecase/daemon/notify_test.go +6 -5
@@ -16,6 +16,7 @@ import (
1616 "github.com/bnema/vev/internal/domain"
1717 "github.com/bnema/vev/internal/ports"
1818 portsmocks "github.com/bnema/vev/internal/ports/mocks"
19+ "github.com/bnema/vev/internal/protocol"
1920 )
2021
2122 func TestReportAttachmentErrorPreservesLocalOrNilScope(t *testing.T) {
@@ -30,9 +31,9 @@ func TestReportAttachmentErrorPreservesLocalOrNilScope(t *testing.T) {
3031
3132 func TestClientNoticeMapsFixedActionsAndDismissesOnlyConnectionToast(t *testing.T) {
3233 d, sess, ac, _ := newNoticeFixture(t, &noticeClock{})
33- d.handleClientNotice(sess, ac, ports.ClientNotice{Action: ports.ClientNoticeClipboardFallback})
34- d.handleClientNotice(sess, ac, ports.ClientNotice{Action: ports.ClientNoticeClipboardTooLarge})
35- d.handleClientNotice(sess, ac, ports.ClientNotice{Action: ports.ClientNoticeLinkDegraded})
34+ d.handleClientNotice(sess, ac, protocol.ClientNotice{Action: protocol.ClientNoticeClipboardFallback})
35+ d.handleClientNotice(sess, ac, protocol.ClientNotice{Action: protocol.ClientNoticeClipboardTooLarge})
36+ d.handleClientNotice(sess, ac, protocol.ClientNotice{Action: protocol.ClientNoticeLinkDegraded})
3637
3738 toasts, _ := visibleToasts(ac)
3839 require.Len(t, toasts, 3)
@@ -42,7 +43,7 @@ func TestClientNoticeMapsFixedActionsAndDismissesOnlyConnectionToast(t *testing.
4243 {Code: domain.NoticeClipboard, Severity: domain.NoticeError, Message: "image paste failed; sent Ctrl+V", SessionID: sess.id, Count: 1},
4344 }, stripNoticeTimes(toasts))
4445
45- d.handleClientNotice(sess, ac, ports.ClientNotice{Action: ports.ClientNoticeLinkConnected})
46+ d.handleClientNotice(sess, ac, protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected})
4647 toasts, _ = visibleToasts(ac)
4748 require.Len(t, toasts, 2)
4849 for _, toast := range toasts {
@@ -57,7 +58,7 @@ func TestMalformedClientNoticeIsIgnored(t *testing.T) {
5758 require.True(t, ok, "attached client transport must be a MockTransport")
5859 frames := []ports.Frame{
5960 {Type: ports.MsgClientNotice, Payload: []byte{0xff}},
60- {Type: ports.MsgDetach, Payload: ports.MarshalDetach(ports.Detach{})},
61+ {Type: ports.MsgDetach, Payload: ports.MarshalDetach(protocol.Detach{})},
6162 }
6263 tr.EXPECT().Recv().RunAndReturn(func() (ports.Frame, error) {
6364 frame := frames[0]
M internal/usecase/daemon/output_state_sync_test.go +6 -6
@@ -72,9 +72,9 @@ func TestDatagramAttachPipelinesRendererBeforeAck(t *testing.T) {
7272 defer releasePTY()
7373 d, sess, _, _ := newManualSessionWithPTYs(t, p)
7474 tr := newDatagramTestTransport()
75- routed, ac, err := d.route(ports.Hello{
76- Version: ports.ProtocolVersion,
77- Intent: ports.IntentAttach,
75+ routed, ac, err := d.route(protocol.Hello{
76+ Version: protocol.Version,
77+ Intent: protocol.IntentAttach,
7878 Name: sess.name,
7979 Size: domain.Size{Cols: 80, Rows: 24},
8080 MaxOutputInFlight: 8,
@@ -136,9 +136,9 @@ func TestDatagramMultipleUnackedScrollPaintsMatchLatestFrame(t *testing.T) {
136136 defer releasePTY()
137137 d, sess, _, _ := newManualSessionWithPTYs(t, p)
138138 tr := newDatagramTestTransport()
139- _, ac, err := d.route(ports.Hello{
140- Version: ports.ProtocolVersion,
141- Intent: ports.IntentAttach,
139+ _, ac, err := d.route(protocol.Hello{
140+ Version: protocol.Version,
141+ Intent: protocol.IntentAttach,
142142 Name: sess.name,
143143 Size: domain.Size{Cols: 80, Rows: 24},
144144 MaxOutputInFlight: 8,
M internal/usecase/daemon/palette_test.go +9 -9
@@ -229,10 +229,10 @@ func TestPaletteEntryPublishesEligibleNamedSessionResults(t *testing.T) {
229229 d, current, ac, _ := newManualSessionWithPTYs(t, p)
230230 d.sessions["active"] = &session{sessionCore: sessionCore{id: "active", name: "active", createdAt: 10}}
231231 d.sessions["ephemeral"] = &session{sessionCore: sessionCore{id: "ephemeral", name: "ephemeral", ephemeral: true, createdAt: 11}}
232- d.inactive["stopped"] = inactiveSession{name: "stopped", createdAt: 12, incarnation: domain.IncarnationID{3}, state: ports.SessionDown}
233- d.inactive["purging"] = inactiveSession{name: "purging", createdAt: 13, incarnation: domain.IncarnationID{4}, state: ports.SessionDown, purging: true}
234- d.inactive["broken"] = inactiveSession{name: "broken", createdAt: 14, incarnation: domain.IncarnationID{5}, state: ports.SessionBroken}
235- d.inactive["degraded"] = inactiveSession{name: "degraded", createdAt: 15, incarnation: domain.IncarnationID{6}, state: ports.SessionDown, record: domain.CatalogueRecord{DegradedReason: "checkpoint unavailable"}}
232+ d.inactive["stopped"] = inactiveSession{name: "stopped", createdAt: 12, incarnation: domain.IncarnationID{3}, state: protocol.SessionDown}
233+ d.inactive["purging"] = inactiveSession{name: "purging", createdAt: 13, incarnation: domain.IncarnationID{4}, state: protocol.SessionDown, purging: true}
234+ d.inactive["broken"] = inactiveSession{name: "broken", createdAt: 14, incarnation: domain.IncarnationID{5}, state: protocol.SessionBroken}
235+ d.inactive["degraded"] = inactiveSession{name: "degraded", createdAt: 15, incarnation: domain.IncarnationID{6}, state: protocol.SessionDown, record: domain.CatalogueRecord{DegradedReason: "checkpoint unavailable"}}
236236 ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1, Entries: []protocol.RecentRouteEntry{
237237 testRouteEntry(1, 1, "purging", 4, protocol.RouteKindLocal),
238238 testRouteEntry(2, 1, "broken", 5, protocol.RouteKindLocal),
@@ -322,7 +322,7 @@ func TestPaletteStoppedSessionResumeFailureKeepsPaletteAndSourceAttachment(t *te
322322 p, release := newBlockingPTY(t)
323323 defer release()
324324 d, current, ac, sends := newManualSessionWithPTYs(t, p)
325- stopped := inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 42, lastUsedSeq: 7, tabNames: []string{"shell", "logs"}, state: ports.SessionDown}
325+ stopped := inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 42, lastUsedSeq: 7, tabNames: []string{"shell", "logs"}, state: protocol.SessionDown}
326326 d.inactive[stopped.name] = stopped
327327 ptys := portsmocks.NewMockPTYFactory(t)
328328 ptys.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("open failed")).Once()
@@ -351,7 +351,7 @@ func TestPaletteSelectedStoppedSessionResumesAndSwitches(t *testing.T) {
351351 defer release2()
352352 d, current, ac, sends := newManualSessionWithPTYs(t, p1)
353353 d.ptys = newFactory(t, p2)
354- d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 42, state: ports.SessionDown}
354+ d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 42, state: protocol.SessionDown}
355355
356356 d.handleInput(current, ac, []byte("\x1b "))
357357 awaitFrame(t, sends, ports.MsgOutput)
@@ -598,9 +598,9 @@ func TestPaletteRemoteCatalogSelectionSendsExactAttachTarget(t *testing.T) {
598598 Endpoint: "user@arch", DisplayOrigin: "arch", LifecycleID: remoteLifecycle,
599599 SessionName: "work", LiveTabID: "tab-work",
600600 }
601- require.Equal(t, ports.AttachTarget{
602- Endpoint: "user@arch", Session: "work", Intent: ports.IntentAttach,
603- RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
601+ require.Equal(t, protocol.AttachTarget{
602+ Endpoint: "user@arch", Session: "work", Intent: protocol.IntentAttach,
603+ RemoteTarget: &remoteTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
604604 }, target)
605605 require.False(t, ac.overlays.paletteActive())
606606 d.remoteCatalog.mu.Lock()
M internal/usecase/daemon/parked_route.go +2 -2
@@ -417,7 +417,7 @@ func (d *Daemon) switchParkedRoute(effect *attachmentEffect, request protocol.Pa
417417
418418 func (d *Daemon) waitForParkedTargetRestore(parent context.Context, name string) error {
419419 ctx, cancel := context.WithCancel(parent)
420- timer := d.clock.NewTimer(ports.HandshakeTimeout)
420+ timer := d.clock.NewTimer(protocol.HandshakeTimeout)
421421 stop := make(chan struct{})
422422 go func() {
423423 select {
@@ -501,7 +501,7 @@ func (d *Daemon) transitionParkedRouteTarget(effect *attachmentEffect, target do
501501 },
502502 })
503503 if err != nil && created != nil {
504- _ = d.killSession(created, ports.ReasonSessionKilled, true)
504+ _ = d.killSession(created, protocol.ReasonSessionKilled, true)
505505 }
506506 if err != nil {
507507 return attachmentTransitionResult{}, created, err
M internal/usecase/daemon/parked_route_test.go +1 -1
@@ -168,7 +168,7 @@ func TestParkedRouteRestoresExactStoppedTargetWithDaemonEnvironment(t *testing.T
168168 lifecycle := domain.SessionLifecycleID{8}
169169 d.mu.Lock()
170170 d.inactive["stopped"] = inactiveSession{
171- name: "stopped", cwd: "/remote/work", incarnation: lifecycle, state: ports.SessionDown,
171+ name: "stopped", cwd: "/remote/work", incarnation: lifecycle, state: protocol.SessionDown,
172172 tabNames: []string{"alpha", "beta"},
173173 tabRecords: []domain.CatalogueTabRecord{
174174 {StableID: "tab-a", Name: "alpha"},
M internal/usecase/daemon/performance_benchmark_test.go +2 -1
@@ -15,6 +15,7 @@ import (
1515 renderer "github.com/bnema/vev-vt"
1616 "github.com/bnema/vev/internal/domain"
1717 "github.com/bnema/vev/internal/ports"
18+ "github.com/bnema/vev/internal/protocol"
1819 "github.com/bnema/vev/internal/usecase/layout"
1920 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"
2021 )
@@ -1100,7 +1101,7 @@ func (f *performanceFixture) close() error {
11001101 sess.mu.Lock()
11011102 sess.ephemeral = true
11021103 sess.mu.Unlock()
1103- killErr := f.killSession(sess, ports.ReasonServerShutdown, false)
1104+ killErr := f.killSession(sess, protocol.ReasonServerShutdown, false)
11041105 d.stopSnapshotEncodeWorker()
11051106 d.serveCancel()
11061107 d.hardCancel()
M internal/usecase/daemon/picker.go +10 -10
@@ -923,11 +923,11 @@ func (d *Daemon) sendLocalAttachTargetForAttachment(effect *attachmentEffect, ta
923923 // Explicit tab rows already take the direct transition path above, while
924924 // stopped sessions have no active target session and retain the fallback.
925925 samePeerEligible := guard.allowSamePeer && targetSess != nil && target.TabIndex <= 0
926- payload := ports.MarshalAttachTarget(ports.AttachTarget{
926+ payload := ports.MarshalAttachTarget(protocol.AttachTarget{
927927 Session: sessionName,
928- Intent: ports.IntentAttach,
928+ Intent: protocol.IntentAttach,
929929 ExactTarget: exactTarget,
930- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
930+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
931931 SamePeer: samePeerEligible,
932932 })
933933 if payload == nil || exactTarget == nil {
@@ -982,12 +982,12 @@ func (d *Daemon) sendRemoteAttachTargetForAttachment(effect *attachmentEffect, t
982982 if err := remoteTarget.Validate(); err != nil || key.Validate() != nil || target.Session != key.ID() || key.Host != remoteTarget.Endpoint || key.Name != remoteTarget.SessionName || key.LifecycleID != remoteTarget.LifecycleID || !d.remoteCatalogTargetReady(remoteTarget) {
983983 return failUnavailable()
984984 }
985- handoff := ports.AttachTarget{
985+ handoff := protocol.AttachTarget{
986986 Endpoint: remoteTarget.Endpoint,
987987 Session: remoteTarget.SessionName,
988- Intent: ports.IntentAttach,
988+ Intent: protocol.IntentAttach,
989989 RemoteTarget: &remoteTarget,
990- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
990+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
991991 }
992992 payload := ports.MarshalAttachTarget(handoff)
993993 if payload == nil {
@@ -1252,7 +1252,7 @@ func (d *Daemon) switchToActiveTargetLocked(from *session, ac *attachedClient, t
12521252 // and stopped record untouched.
12531253 func (d *Daemon) resumeStoppedAndSwitchLocked(from *session, ac *attachedClient, target picker.Target, stopped inactiveSession, sourceEffect *attachmentEffect, guard sessionHandoffGuard, action string) (*session, attachmentTransitionResult, bool, error) {
12541254 if stopped.broken() {
1255- return nil, attachmentTransitionResult{}, false, &protoErr{ports.ErrInternal, "session durable state is broken: " + target.Name}
1255+ return nil, attachmentTransitionResult{}, false, &protoErr{protocol.ErrInternal, "session durable state is broken: " + target.Name}
12561256 }
12571257 if !stopped.canResume() {
12581258 return nil, attachmentTransitionResult{}, false, errAttachmentTransition
@@ -1283,7 +1283,7 @@ func (d *Daemon) resumeStoppedAndSwitchLocked(from *session, ac *attachedClient,
12831283 if err != nil {
12841284 if targetSess != nil {
12851285 d.mu.Unlock()
1286- _ = d.killSession(targetSess, ports.ReasonSessionKilled, true)
1286+ _ = d.killSession(targetSess, protocol.ReasonSessionKilled, true)
12871287 d.mu.Lock()
12881288 }
12891289 return targetSess, attachmentTransitionResult{}, false, err
@@ -1390,7 +1390,7 @@ func (d *Daemon) killPickerTargetForAttachment(target picker.Target, effect *att
13901390 if targetSess == nil {
13911391 return nil
13921392 }
1393- return d.killSessionForAttachment(targetSess, ports.ReasonSessionKilled, true, effect, "picker-delete")
1393+ return d.killSessionForAttachment(targetSess, protocol.ReasonSessionKilled, true, effect, "picker-delete")
13941394 }
13951395
13961396 func (d *Daemon) killPickerTarget(target picker.Target) error {
@@ -1405,7 +1405,7 @@ func (d *Daemon) killPickerTarget(target picker.Target) error {
14051405 targetSess := d.sessions[target.Session]
14061406 d.mu.Unlock()
14071407 if targetSess != nil {
1408- return d.killSession(targetSess, ports.ReasonSessionKilled, true)
1408+ return d.killSession(targetSess, protocol.ReasonSessionKilled, true)
14091409 }
14101410 return nil
14111411 }
M internal/usecase/daemon/picker_test.go +10 -10
@@ -734,7 +734,7 @@ func TestRestoreCancellationTransitionsBeforePickerCompletion(t *testing.T) {
734734 d.mu.Lock()
735735 entry := d.inactive[record.Name]
736736 d.mu.Unlock()
737- require.Equal(t, ports.SessionBroken, entry.state)
737+ require.Equal(t, protocol.SessionBroken, entry.state)
738738 require.Equal(t, "restore interrupted", entry.record.DegradedReason)
739739 select {
740740 case <-entry.restoreDone:
@@ -747,10 +747,10 @@ func TestPickerRejectsCatalogueTargetsWithoutFreshRuntime(t *testing.T) {
747747 tests := []struct {
748748 name string
749749 broken bool
750- runtimeState ports.SessionState
750+ runtimeState protocol.SessionState
751751 }{
752- {name: "broken", broken: true, runtimeState: ports.SessionBroken},
753- {name: "healthy without restored runtime", runtimeState: ports.SessionDown},
752+ {name: "broken", broken: true, runtimeState: protocol.SessionBroken},
753+ {name: "healthy without restored runtime", runtimeState: protocol.SessionDown},
754754 }
755755
756756 for _, tt := range tests {
@@ -783,7 +783,7 @@ func TestPickerRejectsCatalogueTargetsWithoutFreshRuntime(t *testing.T) {
783783 require.Equal(t, domain.NoticeSessionUnavailable, userError.Code)
784784 var protocolError *protoErr
785785 require.ErrorAs(t, err, &protocolError)
786- require.Equal(t, ports.ErrInternal, protocolError.code)
786+ require.Equal(t, protocol.ErrInternal, protocolError.code)
787787 require.Zero(t, factory.calls.Load(), "unsafe target must not open a PTY")
788788 require.Same(t, from, ac.currentSession())
789789
@@ -809,7 +809,7 @@ func TestPickerResumesStoppedSessionWithPersistedTabNames(t *testing.T) {
809809 defer release3()
810810 d, from, ac, sends := newManualSessionWithPTYs(t, p1)
811811 d.ptys = newFactorySeq(t, p2, p3)
812- d.inactive["work"] = inactiveSession{name: "work", cwd: "/tmp/work", createdAt: 7, tabNames: []string{"shell", "logs"}, record: domain.CatalogueRecord{Name: "work"}, state: ports.SessionDown}
812+ d.inactive["work"] = inactiveSession{name: "work", cwd: "/tmp/work", createdAt: 7, tabNames: []string{"shell", "logs"}, record: domain.CatalogueRecord{Name: "work"}, state: protocol.SessionDown}
813813
814814 d.resumeStoppedAndSwitch(from, ac, picker.Target{Name: "work", Stopped: true})
815815 awaitFrame(t, sends, ports.MsgOutput)
@@ -1406,11 +1406,11 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) {
14061406 return got.Size != normalSize && got.Size.Valid()
14071407 })).Return(floating, nil).Once()
14081408 d := newTestDaemon(t, f, stubClock{})
1409- d.inactive["old"] = inactiveSession{name: "old", cwd: t.TempDir(), createdAt: 1, state: ports.SessionDown}
1409+ d.inactive["old"] = inactiveSession{name: "old", cwd: t.TempDir(), createdAt: 1, state: protocol.SessionDown}
14101410 tr := portsmocks.NewMockTransport(t)
14111411 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
14121412 tr.EXPECT().Close().Return(nil).Maybe()
1413- sess, ac, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)
1413+ sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)
14141414 require.NoError(t, err)
14151415 d.resumeStoppedAndSwitch(sess, ac, picker.Target{Name: "old", Stopped: true})
14161416 got := ac.currentSession()
@@ -1428,7 +1428,7 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) {
14281428 defer tb.mu.Unlock()
14291429 return tb.floating.pane != nil && tb.floating.pane.pty == floating
14301430 }, time.Second, 5*time.Millisecond)
1431- _ = d.killSession(got, ports.ReasonSessionKilled, false)
1431+ _ = d.killSession(got, protocol.ReasonSessionKilled, false)
14321432 release1()
14331433 release2()
14341434 d.sessWg.Wait()
@@ -1448,7 +1448,7 @@ func TestPickerEnterOnStoppedSessionRestoreFailureSurfacesNoticeAndStaysPut(t *t
14481448 p, release := newBlockingPTY(t)
14491449 defer release()
14501450 d, from, ac, sends := newManualSessionWithPTYs(t, p)
1451- d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp/stopped", createdAt: 7, state: ports.SessionDown}
1451+ d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp/stopped", createdAt: 7, state: protocol.SessionDown}
14521452 cause := errors.New("open failed")
14531453 ptys := portsmocks.NewMockPTYFactory(t)
14541454 ptys.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, cause).Once()
M internal/usecase/daemon/remote_attach.go +19 -19
@@ -13,24 +13,24 @@ import (
1313 // It is deliberately separate from legacy name routing: a lifecycle mismatch,
1414 // missing tab, broken record, or stopped-record race is a no-such-target error,
1515 // never an invitation to attach a same-name replacement.
16-func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h ports.Hello, tr ports.Transport) (*session, *attachedClient, error) {
16+func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h protocol.Hello, tr ports.Transport) (*session, *attachedClient, error) {
1717 if h.RemoteTarget == nil {
1818 return nil, nil, errors.New("daemon: missing remote target")
1919 }
2020 target := *h.RemoteTarget
2121 if err := target.Validate(); err != nil {
22- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "invalid remote target"}
22+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "invalid remote target"}
2323 }
24- if h.EnvironmentPolicy != ports.EnvironmentPolicyDaemonOwned {
25- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote target requires daemon-owned environment"}
24+ if h.EnvironmentPolicy != protocol.EnvironmentPolicyDaemonOwned {
25+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "remote target requires daemon-owned environment"}
2626 }
27- if h.Intent != ports.IntentAttach && h.Intent != ports.IntentResume {
28- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote target requires attach or resume"}
27+ if h.Intent != protocol.IntentAttach && h.Intent != protocol.IntentResume {
28+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "remote target requires attach or resume"}
2929 }
3030 if err := d.waitForTargetRestore(ctx, target.SessionName); err != nil {
3131 var protocolErr *protoErr
32- if errors.As(err, &protocolErr) && protocolErr.code == ports.ErrInternal {
33- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote session is unavailable"}
32+ if errors.As(err, &protocolErr) && protocolErr.code == protocol.ErrInternal {
33+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "remote session is unavailable"}
3434 }
3535 return nil, nil, remoteTargetError(err)
3636 }
@@ -38,20 +38,20 @@ func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h ports.Hello
3838 d.mu.Lock()
3939 if d.closing {
4040 d.mu.Unlock()
41- return nil, nil, &protoErr{ports.ErrServerShutdown, "daemon is shutting down"}
41+ return nil, nil, &protoErr{protocol.ErrServerShutdown, "daemon is shutting down"}
4242 }
4343 if live := d.findByNameLocked(target.SessionName); live != nil {
4444 if live.incarnation != target.LifecycleID {
4545 d.mu.Unlock()
46- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote session lifecycle has changed"}
46+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "remote session lifecycle has changed"}
4747 }
4848 if target.Stopped && target.LiveTabID != "" {
4949 d.mu.Unlock()
50- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "stopped target has a live tab selector"}
50+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "stopped target has a live tab selector"}
5151 }
5252 if _, ok := remoteTargetTabIndexLocked(live, target); !ok {
5353 d.mu.Unlock()
54- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote tab no longer exists"}
54+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "remote tab no longer exists"}
5555 }
5656 ac, err := d.finishRouteAttach(live, tr, h.Size, h, false, false)
5757 return live, ac, err
@@ -59,20 +59,20 @@ func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h ports.Hello
5959
6060 if !target.Stopped {
6161 d.mu.Unlock()
62- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "live remote target has no active runtime"}
62+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "live remote target has no active runtime"}
6363 }
6464 inactive, ok := d.inactive[target.SessionName]
6565 if !ok || inactive.incarnation != target.LifecycleID {
6666 d.mu.Unlock()
67- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote session lifecycle no longer exists"}
67+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "remote session lifecycle no longer exists"}
6868 }
6969 if !inactive.canResume() {
7070 d.mu.Unlock()
71- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote session is unavailable"}
71+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "remote session is unavailable"}
7272 }
7373 if _, ok := remoteTargetTabIndexInactive(inactive, target); !ok {
7474 d.mu.Unlock()
75- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote stopped tab no longer exists"}
75+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "remote stopped tab no longer exists"}
7676 }
7777
7878 // Picker handoffs use the daemon's own environment and persisted CWD. The
@@ -156,10 +156,10 @@ func (d *Daemon) sendCommittedRouteIdentityForAttachment(effect *attachmentEffec
156156 return effect.sendControl(ports.Frame{Type: ports.MsgCommittedRouteIdentity, Payload: payload})
157157 }
158158
159-func (d *Daemon) finishRouteAttach(sess *session, tr ports.Transport, sz domain.Size, h ports.Hello, routeCreated, purge bool) (*attachedClient, error) {
159+func (d *Daemon) finishRouteAttach(sess *session, tr ports.Transport, sz domain.Size, h protocol.Hello, routeCreated, purge bool) (*attachedClient, error) {
160160 ac, err := d.finishAttach(sess, tr, sz, h)
161161 if err != nil && routeCreated {
162- if cleanupErr := d.killSessionIfEmpty(sess, ports.ReasonSessionKilled, purge); cleanupErr != nil {
162+ if cleanupErr := d.killSessionIfEmpty(sess, protocol.ReasonSessionKilled, purge); cleanupErr != nil {
163163 err = errors.Join(err, cleanupErr)
164164 }
165165 return nil, err
@@ -196,7 +196,7 @@ func remoteTargetError(err error) error {
196196 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
197197 return err
198198 }
199- return &protoErr{ports.ErrNoSuchTarget, "remote target is unavailable"}
199+ return &protoErr{protocol.ErrNoSuchTarget, "remote target is unavailable"}
200200 }
201201
202202 func stoppedTabMetadata(stopped inactiveSession) []domain.TabSelectorTab {
M internal/usecase/daemon/remote_attach_test.go +3 -3
@@ -33,7 +33,7 @@ func TestLocalPickerOfferCarriesExactLifecycle(t *testing.T) {
3333 got, err := ports.UnmarshalAttachTarget(frame.Payload)
3434 require.NoError(t, err)
3535 require.Empty(t, got.Endpoint)
36- require.Equal(t, ports.IntentAttach, got.Intent)
36+ require.Equal(t, protocol.IntentAttach, got.Intent)
3737 require.True(t, got.SamePeer)
3838 require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: target.incarnation, SessionName: target.name}, got.ExactTarget)
3939 require.Same(t, source, ac.currentAttachmentSession(), "the source remains attached until the client confirms the switch")
@@ -44,7 +44,7 @@ func TestStoppedLocalPickerHandoffWaitsForClientClose(t *testing.T) {
4444 source, ac, sends := addRemoteRefreshPickerOwner(t, d, "source")
4545 lifecycle := remoteLifecycleForTest()
4646 d.inactive["stopped"] = inactiveSession{
47- name: "stopped", state: ports.SessionDown, incarnation: lifecycle,
47+ name: "stopped", state: protocol.SessionDown, incarnation: lifecycle,
4848 }
4949 graphics := newGraphicsOutputState()
5050 graphics.assets["asset"] = graphicsOutputAsset{id: graphics.namespaceBase}
@@ -97,7 +97,7 @@ func TestRemotePickerRichHandoffCarriesLifecycleTabAndPolicy(t *testing.T) {
9797 require.NoError(t, err)
9898 require.NotNil(t, got.RemoteTarget)
9999 require.Equal(t, remoteTarget, *got.RemoteTarget)
100- require.Equal(t, ports.EnvironmentPolicyDaemonOwned, got.EnvironmentPolicy)
100+ require.Equal(t, protocol.EnvironmentPolicyDaemonOwned, got.EnvironmentPolicy)
101101 }
102102
103103 func TestRemotePickerRichHandoffRejectsMismatchedRouteKey(t *testing.T) {
M internal/usecase/daemon/remote_picker_test.go +14 -14
@@ -458,7 +458,7 @@ func requireNoRemoteLockViolations(t *testing.T, catalog *channelRemoteCatalog,
458458
459459 func remoteCatalogForTest(sessions ...ports.RemoteCatalogSession) ports.RemoteCatalog {
460460 return ports.RemoteCatalog{
461- ProtocolVersion: ports.ProtocolVersion,
461+ ProtocolVersion: protocol.Version,
462462 SchemaVersion: ports.RemoteCatalogSchemaVersion,
463463 Sessions: append([]ports.RemoteCatalogSession{}, sessions...),
464464 }
@@ -648,7 +648,7 @@ func TestRemoteRefreshValidationSentinelsRemainMalformed(t *testing.T) {
648648 hosts := &remoteRefreshHostStore{hosts: []string{"arch"}}
649649 d, _, _ := newRemoteRefreshDaemon(t, hosts, time.Unix(400, 0))
650650 d.remoteCatalog.refresh = 1
651- catalog := ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{test.session}}
651+ catalog := ports.RemoteCatalog{ProtocolVersion: protocol.Version, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{test.session}}
652652 require.False(t, d.applyRemoteRefreshResult(1, "arch", catalog, nil))
653653 d.remoteCatalog.mu.Lock()
654654 require.Equal(t, remoteHostMalformed, d.remoteCatalog.status["arch"])
@@ -666,7 +666,7 @@ func TestRemoteRefreshVersionMismatchPreservesStaleRows(t *testing.T) {
666666 d.remoteCatalog.refresh = 3
667667 d.remoteCatalog.mu.Unlock()
668668
669- require.False(t, d.applyRemoteRefreshResult(3, "arch", ports.RemoteCatalog{}, &ports.RemoteCatalogVersionMismatchError{Got: 19, Want: ports.ProtocolVersion}))
669+ require.False(t, d.applyRemoteRefreshResult(3, "arch", ports.RemoteCatalog{}, &ports.RemoteCatalogVersionMismatchError{Got: 19, Want: protocol.Version}))
670670 d.remoteCatalog.mu.Lock()
671671 require.Equal(t, old, d.remoteCatalog.cache["arch"])
672672 require.Equal(t, remoteHostVersionMismatch, d.remoteCatalog.status["arch"])
@@ -699,7 +699,7 @@ func TestRemotePickerHandoffSendsTargetAndLeavesNoShadowSession(t *testing.T) {
699699 require.Equal(t, ports.MsgAttachTarget, frame.Type)
700700 got, err := ports.UnmarshalAttachTarget(frame.Payload)
701701 require.NoError(t, err)
702- require.Equal(t, ports.AttachTarget{Endpoint: "arch", Session: "work", Intent: ports.IntentAttach, RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}, got)
702+ require.Equal(t, protocol.AttachTarget{Endpoint: "arch", Session: "work", Intent: protocol.IntentAttach, RemoteTarget: &remoteTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}, got)
703703 require.Nil(t, ac.currentAttachmentSession())
704704 d.mu.Lock()
705705 require.NotContains(t, d.sessions, key.ID(), "remote picker handoff must not create a local session shadow")
@@ -759,16 +759,16 @@ func TestRemotePickerSelectsStoppedRemoteTabAndRestoresIt(t *testing.T) {
759759 remote := newTestDaemon(t, newFactory(t, newQuietPTY()), stubClock{})
760760 remote.mu.Lock()
761761 remote.inactive["work"] = inactiveSession{
762- name: "work", cwd: "/remote/work", incarnation: lifecycle, state: ports.SessionDown,
762+ name: "work", cwd: "/remote/work", incarnation: lifecycle, state: protocol.SessionDown,
763763 tabNames: []string{"alpha", "beta"},
764764 tabRecords: []domain.CatalogueTabRecord{{StableID: "tab-a", Name: "alpha"}, {StableID: "tab-b", Name: "beta"}},
765765 }
766766 remote.mu.Unlock()
767767 transport, _ := newCapturingTransport(t)
768- restored, attachment, err := remote.routeWithContext(context.Background(), ports.Hello{
769- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: handoff.Session,
768+ restored, attachment, err := remote.routeWithContext(context.Background(), protocol.Hello{
769+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: handoff.Session,
770770 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: handoff.RemoteTarget,
771- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
771+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
772772 }, transport)
773773 require.NoError(t, err)
774774 t.Cleanup(func() { remote.clientGone(restored, attachment, transport, false) })
@@ -825,13 +825,13 @@ func TestRemotePickerResurrectsStoppedRemoteSessionWithoutTabMetadata(t *testing
825825
826826 remote := newTestDaemon(t, newFactory(t, newQuietPTY()), stubClock{})
827827 remote.mu.Lock()
828- remote.inactive["work"] = inactiveSession{name: "work", cwd: "/remote/work", incarnation: lifecycle, state: ports.SessionDown}
828+ remote.inactive["work"] = inactiveSession{name: "work", cwd: "/remote/work", incarnation: lifecycle, state: protocol.SessionDown}
829829 remote.mu.Unlock()
830830 transport, _ := newCapturingTransport(t)
831- restored, attachment, err := remote.routeWithContext(context.Background(), ports.Hello{
832- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: handoff.Session,
831+ restored, attachment, err := remote.routeWithContext(context.Background(), protocol.Hello{
832+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: handoff.Session,
833833 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: handoff.RemoteTarget,
834- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
834+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
835835 }, transport)
836836 require.NoError(t, err)
837837 t.Cleanup(func() { remote.clientGone(restored, attachment, transport, false) })
@@ -962,7 +962,7 @@ func TestRemoteRefreshUpdatesAllOpenPickersPreservingSelection(t *testing.T) {
962962 d.remoteCatalog.mu.Unlock()
963963
964964 require.True(t, d.applyRemoteRefreshResult(4, "arch", ports.RemoteCatalog{
965- ProtocolVersion: ports.ProtocolVersion,
965+ ProtocolVersion: protocol.Version,
966966 SchemaVersion: ports.RemoteCatalogSchemaVersion,
967967 Sessions: []ports.RemoteCatalogSession{{
968968 LifecycleID: remoteLifecycleForTest(), Name: "work", State: "up",
@@ -1117,7 +1117,7 @@ func TestParkedPickerResumePreservesGeneration(t *testing.T) {
11171117 d.clientGone(sess, ac, ac.transport(), false)
11181118 token := ac.resumeToken
11191119 tr := &closeTrackingTransport{}
1120- resumedSess, resumedAC, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, sess.name, token), tr, domain.Size{Cols: 80, Rows: 24})
1120+ resumedSess, resumedAC, ok, err := d.resumeParked(helloResumeCapable(protocol.IntentResume, sess.name, token), tr, domain.Size{Cols: 80, Rows: 24})
11211121 require.NoError(t, err)
11221122 require.True(t, ok)
11231123 require.Same(t, sess, resumedSess)
M internal/usecase/daemon/remote_route_test.go +27 -26
@@ -9,6 +9,7 @@ import (
99
1010 "github.com/bnema/vev/internal/domain"
1111 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
1213 )
1314
1415 func TestMixedAttachmentCapabilitiesKeepGraphicsAttachmentLocal(t *testing.T) {
@@ -16,8 +17,8 @@ func TestMixedAttachmentCapabilitiesKeepGraphicsAttachmentLocal(t *testing.T) {
1617 sess := addControlSession(d, "work", "tab-1", "pane-1")
1718 sess.ephemeral = false
1819 kittyTransport, _ := newCapturingTransport(t)
19- _, kitty, err := d.route(ports.Hello{
20- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: defaultSize,
20+ _, kitty, err := d.route(protocol.Hello{
21+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: defaultSize,
2122 ClientID: [16]byte{1}, KittyDirectGraphics: true,
2223 }, kittyTransport)
2324 require.NoError(t, err)
@@ -31,8 +32,8 @@ func TestMixedAttachmentCapabilitiesKeepGraphicsAttachmentLocal(t *testing.T) {
3132 pane.mu.Unlock()
3233
3334 textTransport, _ := newCapturingTransport(t)
34- _, text, err := d.route(ports.Hello{
35- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: defaultSize,
35+ _, text, err := d.route(protocol.Hello{
36+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: defaultSize,
3637 ClientID: [16]byte{2},
3738 }, textTransport)
3839 require.NoError(t, err)
@@ -53,8 +54,8 @@ func TestDirectRemoteAttachSuppressesUndeclaredGraphicsBackend(t *testing.T) {
5354 sess := addControlSession(d, "work", "tab-1", "pane-1")
5455 sess.ephemeral = false
5556 tr, _ := newCapturingTransport(t)
56- _, ac, err := d.routeWithContext(context.Background(), ports.Hello{
57- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: defaultSize,
57+ _, ac, err := d.routeWithContext(context.Background(), protocol.Hello{
58+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: defaultSize,
5859 Env: []string{"TERM=xterm-kitty", "KITTY_WINDOW_ID=1"}, Remote: true,
5960 }, tr)
6061 require.NoError(t, err)
@@ -67,8 +68,8 @@ func TestResumeRemoteRoutePreservesDeclaredGraphicsCapability(t *testing.T) {
6768 d := newTestDaemon(t, nil, stubClock{})
6869 sess := addControlSession(d, "work", "tab-1", "pane-1")
6970 sess.ephemeral = false
70- local := ports.Hello{
71- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: defaultSize,
71+ local := protocol.Hello{
72+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: defaultSize,
7273 ClientID: [16]byte{1, 2, 3, 4}, Env: []string{"TERM=xterm-kitty", "KITTY_WINDOW_ID=1"}, KittyDirectGraphics: true,
7374 }
7475 oldTransport, _ := newCapturingTransport(t)
@@ -78,7 +79,7 @@ func TestResumeRemoteRoutePreservesDeclaredGraphicsCapability(t *testing.T) {
7879 token := ac.resumeToken
7980 d.clientGone(sess, ac, oldTransport, false)
8081
81- remote := helloResumeCapable(ports.IntentResume, "work", token)
82+ remote := helloResumeCapable(protocol.IntentResume, "work", token)
8283 remote.Remote = true
8384 remote.KittyDirectGraphics = true
8485 replacement, _ := newCapturingTransport(t)
@@ -98,7 +99,7 @@ func TestRouteRemoteTargetSelectsExactLiveTab(t *testing.T) {
9899 sess.incarnation = remoteLifecycleForTest()
99100 tr, _ := newCapturingTransport(t)
100101 target := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation, SessionName: "work", LiveTabID: "tab-1"}
101- hello := ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}
102+ hello := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}
102103 _, ac, err := d.routeWithContext(context.Background(), hello, tr)
103104 require.NoError(t, err)
104105 require.NotNil(t, ac)
@@ -114,9 +115,9 @@ func TestFinishRouteAttachRollsBackCreatedSession(t *testing.T) {
114115 sess, err := createSessionForTest(d, "work", false, "/tmp/work", defaultSize, terminalEnv{}, d.baseEnv)
115116 require.NoError(t, err)
116117 target := domain.RemoteSessionTarget{LifecycleID: sess.incarnation, SessionName: "work", LiveTabID: "missing-tab"}
117- hello := ports.Hello{
118- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: defaultSize,
119- RemoteTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
118+ hello := protocol.Hello{
119+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: defaultSize,
120+ RemoteTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
120121 }
121122 // The caller must hold d.mu on entry; finishRouteAttach releases it on
122123 // both success and error paths before returning.
@@ -124,7 +125,7 @@ func TestFinishRouteAttachRollsBackCreatedSession(t *testing.T) {
124125 _, err = d.finishRouteAttach(sess, &closeTrackingTransport{}, defaultSize, hello, true, true)
125126 var protocolErr *protoErr
126127 require.ErrorAs(t, err, &protocolErr)
127- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
128+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
128129 d.mu.Lock()
129130 _, retained := d.sessions[sess.id]
130131 d.mu.Unlock()
@@ -141,14 +142,14 @@ func TestFinishRouteAttachPreservesConcurrentAttachment(t *testing.T) {
141142
142143 missing := domain.RemoteSessionTarget{LifecycleID: sess.incarnation, SessionName: "work", LiveTabID: "missing-tab"}
143144 d.mu.Lock()
144- _, err = d.finishRouteAttach(sess, &closeTrackingTransport{}, defaultSize, ports.Hello{
145- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: defaultSize,
146- RemoteTarget: &missing, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
145+ _, err = d.finishRouteAttach(sess, &closeTrackingTransport{}, defaultSize, protocol.Hello{
146+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: defaultSize,
147+ RemoteTarget: &missing, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
147148 }, true, true)
148149
149150 var protocolErr *protoErr
150151 require.ErrorAs(t, err, &protocolErr)
151- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
152+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
152153 require.Same(t, sess, winner().currentAttachmentSession())
153154 d.mu.Lock()
154155 require.Same(t, sess, d.sessions[sess.id])
@@ -164,9 +165,9 @@ func TestFailedHandshakeCleanupPreservesConcurrentAttachment(t *testing.T) {
164165 require.NoError(t, err)
165166 initialTransport, _ := newCapturingTransport(t)
166167 target := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation, SessionName: "work", LiveTabID: domain.TabStableID(sess.tabs[0].stableID)}
167- _, initial, err := d.routeWithContext(context.Background(), ports.Hello{
168- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: defaultSize,
169- RemoteTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
168+ _, initial, err := d.routeWithContext(context.Background(), protocol.Hello{
169+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: defaultSize,
170+ RemoteTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
170171 }, initialTransport)
171172 require.NoError(t, err)
172173 initial.routeCreatedSession = true
@@ -193,9 +194,9 @@ func attachWhenRouteCleanupSnapshots(t *testing.T, d *Daemon, sess *session) (fu
193194 SessionName: sess.name, LiveTabID: domain.TabStableID(sess.tabs[0].stableID),
194195 }
195196 var err error
196- _, winner, err = d.routeWithContext(context.Background(), ports.Hello{
197- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: sess.name, Size: defaultSize,
198- RemoteTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
197+ _, winner, err = d.routeWithContext(context.Background(), protocol.Hello{
198+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: sess.name, Size: defaultSize,
199+ RemoteTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
199200 }, winnerTransport)
200201 require.NoError(t, err)
201202 }
@@ -215,9 +216,9 @@ func TestRouteRemoteTargetRejectsSameNameReplacement(t *testing.T) {
215216 var old domain.SessionLifecycleID
216217 old[0] = 8
217218 target := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: old, SessionName: "work", LiveTabID: "tab-new"}
218- hello := ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}
219+ hello := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}
219220 _, _, err := d.routeWithContext(context.Background(), hello, tr)
220221 var protocolErr *protoErr
221222 require.ErrorAs(t, err, &protocolErr)
222- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
223+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
223224 }
M internal/usecase/daemon/remote_stopped_route_test.go +42 -42
@@ -9,8 +9,8 @@ import (
99 "github.com/stretchr/testify/require"
1010
1111 "github.com/bnema/vev/internal/domain"
12- "github.com/bnema/vev/internal/ports"
1312 portsmocks "github.com/bnema/vev/internal/ports/mocks"
13+ "github.com/bnema/vev/internal/protocol"
1414 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"
1515 )
1616
@@ -19,20 +19,20 @@ func TestDaemonOwnedNoExactTargetRejectsNewAndEphemeral(t *testing.T) {
1919 name string
2020 intent uint8
2121 }{
22- {name: "new", intent: ports.IntentNew},
23- {name: "ephemeral", intent: ports.IntentEphemeral},
22+ {name: "new", intent: protocol.IntentNew},
23+ {name: "ephemeral", intent: protocol.IntentEphemeral},
2424 } {
2525 t.Run(tt.name, func(t *testing.T) {
2626 d := newTestDaemon(t, nil, stubClock{})
27- hello := ports.Hello{
28- Version: ports.ProtocolVersion, Intent: tt.intent, Name: "work",
27+ hello := protocol.Hello{
28+ Version: protocol.Version, Intent: tt.intent, Name: "work",
2929 Size: domain.Size{Cols: 80, Rows: 24}, Cwd: "/untrusted/cwd",
30- Env: []string{"UNTRUSTED=client"}, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
30+ Env: []string{"UNTRUSTED=client"}, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
3131 }
3232 _, _, err := d.route(hello, &closeTrackingTransport{})
3333 var protocolErr *protoErr
3434 require.ErrorAs(t, err, &protocolErr)
35- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
35+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
3636 d.mu.Lock()
3737 sessions := len(d.sessions)
3838 d.mu.Unlock()
@@ -45,13 +45,13 @@ func TestDaemonOwnedStoppedAttachUsesDaemonEnvironment(t *testing.T) {
4545 d := newTestDaemon(t, newFactory(t, newQuietPTY()), stubClock{})
4646 d.baseEnv = []string{"DAEMON=owned"}
4747 d.mu.Lock()
48- d.inactive["work"] = inactiveSession{name: "work", cwd: "/remote/work", tabNames: []string{"main"}, state: ports.SessionDown}
48+ d.inactive["work"] = inactiveSession{name: "work", cwd: "/remote/work", tabNames: []string{"main"}, state: protocol.SessionDown}
4949 d.mu.Unlock()
5050
51- hello := ports.Hello{
52- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work",
51+ hello := protocol.Hello{
52+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work",
5353 Size: domain.Size{Cols: 80, Rows: 24}, Cwd: "/untrusted/cwd",
54- Env: []string{"UNTRUSTED=client"}, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
54+ Env: []string{"UNTRUSTED=client"}, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
5555 }
5656 sess, ac, err := d.routeWithContext(context.Background(), hello, &closeTrackingTransport{})
5757 require.NoError(t, err)
@@ -59,7 +59,7 @@ func TestDaemonOwnedStoppedAttachUsesDaemonEnvironment(t *testing.T) {
5959 sess.mu.Lock()
6060 require.Equal(t, []string{"DAEMON=owned"}, sess.env)
6161 sess.mu.Unlock()
62- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))
62+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
6363 }
6464
6565 func TestRouteRemoteTargetRejectsLiveIntentForInactiveSession(t *testing.T) {
@@ -67,7 +67,7 @@ func TestRouteRemoteTargetRejectsLiveIntentForInactiveSession(t *testing.T) {
6767 lifecycle := remoteLifecycleForTest()
6868 d.mu.Lock()
6969 d.inactive["work"] = inactiveSession{
70- name: "work", incarnation: lifecycle, state: ports.SessionDown,
70+ name: "work", incarnation: lifecycle, state: protocol.SessionDown,
7171 tabNames: []string{"main"},
7272 tabRecords: []domain.CatalogueTabRecord{{StableID: "tab-1", Name: "main"}},
7373 }
@@ -77,15 +77,15 @@ func TestRouteRemoteTargetRejectsLiveIntentForInactiveSession(t *testing.T) {
7777 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle,
7878 SessionName: "work", LiveTabID: "tab-1",
7979 }
80- _, _, err := d.routeWithContext(context.Background(), ports.Hello{
81- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work",
80+ _, _, err := d.routeWithContext(context.Background(), protocol.Hello{
81+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work",
8282 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,
83- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
83+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
8484 }, &closeTrackingTransport{})
8585
8686 var protocolErr *protoErr
8787 require.ErrorAs(t, err, &protocolErr)
88- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
88+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
8989 d.mu.Lock()
9090 defer d.mu.Unlock()
9191 require.Empty(t, d.sessions)
@@ -98,9 +98,9 @@ func TestRouteRemoteTargetMapsUnavailableInactiveStatesToNoSuchTarget(t *testing
9898 name string
9999 entry inactiveSession
100100 }{
101- {name: "broken", entry: inactiveSession{name: "work", incarnation: lifecycle, state: ports.SessionBroken}},
102- {name: "degraded", entry: inactiveSession{name: "work", incarnation: lifecycle, state: ports.SessionDown, record: domain.CatalogueRecord{Name: "work", DegradedReason: "checkpoint unavailable"}}},
103- {name: "purging", entry: inactiveSession{name: "work", incarnation: lifecycle, state: ports.SessionDown, purging: true}},
101+ {name: "broken", entry: inactiveSession{name: "work", incarnation: lifecycle, state: protocol.SessionBroken}},
102+ {name: "degraded", entry: inactiveSession{name: "work", incarnation: lifecycle, state: protocol.SessionDown, record: domain.CatalogueRecord{Name: "work", DegradedReason: "checkpoint unavailable"}}},
103+ {name: "purging", entry: inactiveSession{name: "work", incarnation: lifecycle, state: protocol.SessionDown, purging: true}},
104104 } {
105105 t.Run(test.name, func(t *testing.T) {
106106 d := newTestDaemon(t, nil, stubClock{})
@@ -109,15 +109,15 @@ func TestRouteRemoteTargetMapsUnavailableInactiveStatesToNoSuchTarget(t *testing
109109 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle,
110110 SessionName: "work", Stopped: true,
111111 }
112- _, _, err := d.routeWithContext(context.Background(), ports.Hello{
113- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work",
112+ _, _, err := d.routeWithContext(context.Background(), protocol.Hello{
113+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work",
114114 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,
115- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
115+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
116116 }, &closeTrackingTransport{})
117117
118118 var protocolErr *protoErr
119119 require.ErrorAs(t, err, &protocolErr)
120- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
120+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
121121 })
122122 }
123123 }
@@ -125,22 +125,22 @@ func TestRouteRemoteTargetMapsUnavailableInactiveStatesToNoSuchTarget(t *testing
125125 func TestRouteRemoteTargetMapsConcurrentInactiveResumeToNoSuchTarget(t *testing.T) {
126126 d := newTestDaemon(t, nil, stubClock{})
127127 lifecycle := remoteLifecycleForTest()
128- d.inactive["work"] = inactiveSession{name: "work", incarnation: lifecycle, state: ports.SessionDown}
128+ d.inactive["work"] = inactiveSession{name: "work", incarnation: lifecycle, state: protocol.SessionDown}
129129 d.creating["work"] = struct{}{}
130130 target := domain.RemoteSessionTarget{
131131 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle,
132132 SessionName: "work", Stopped: true,
133133 }
134134
135- _, _, err := d.routeWithContext(context.Background(), ports.Hello{
136- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work",
135+ _, _, err := d.routeWithContext(context.Background(), protocol.Hello{
136+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work",
137137 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,
138- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
138+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
139139 }, &closeTrackingTransport{})
140140
141141 var protocolErr *protoErr
142142 require.ErrorAs(t, err, &protocolErr)
143- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
143+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
144144 }
145145
146146 func TestRouteRemoteTargetPreservesInactiveResumeFailures(t *testing.T) {
@@ -151,10 +151,10 @@ func TestRouteRemoteTargetPreservesInactiveResumeFailures(t *testing.T) {
151151 }
152152 }
153153 route := func(d *Daemon, target domain.RemoteSessionTarget) error {
154- _, _, err := d.routeWithContext(context.Background(), ports.Hello{
155- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: target.SessionName,
154+ _, _, err := d.routeWithContext(context.Background(), protocol.Hello{
155+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: target.SessionName,
156156 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,
157- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
157+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
158158 }, &closeTrackingTransport{})
159159 return err
160160 }
@@ -168,7 +168,7 @@ func TestRouteRemoteTargetPreservesInactiveResumeFailures(t *testing.T) {
168168 record.Committed = nil
169169 record.TabNames = nil
170170 record.TabRecords = nil
171- d.inactive[record.Name] = inactiveSessionFromRecord(record, ports.SessionDown, nil)
171+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
172172
173173 err := route(d, newTarget(record))
174174 require.ErrorIs(t, err, cause)
@@ -187,7 +187,7 @@ func TestRouteRemoteTargetPreservesInactiveResumeFailures(t *testing.T) {
187187 d.catalogue = recordErrorCatalogue{durableRecoveryCatalogue: catalogue, err: cause}
188188 d.persistEnabled = true
189189 d.recovery = recoveryusecase.NewCoordinator(d.catalogue, noOpSnapshotRepository{}, nil)
190- d.inactive[record.Name] = inactiveSessionFromRecord(record, ports.SessionDown, nil)
190+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
191191
192192 err := route(d, newTarget(record))
193193 require.ErrorIs(t, err, cause)
@@ -224,17 +224,17 @@ func TestRouteRemoteTargetResumesCanonicalPersistedTabMetadata(t *testing.T) {
224224 d.catalogue = catalogue
225225 d.persistEnabled = true
226226 d.recovery = recoveryusecase.NewCoordinator(catalogue, noOpSnapshotRepository{}, nil)
227- d.inactive[record.Name] = inactiveSessionFromRecord(record, ports.SessionDown, nil)
227+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
228228
229229 target := domain.RemoteSessionTarget{
230230 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: record.IncarnationID,
231231 SessionName: record.Name, Stopped: true, StoppedTab: test.selector,
232232 }
233233 transport, _ := newCapturingTransport(t)
234- sess, attachment, err := d.routeWithContext(context.Background(), ports.Hello{
235- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: record.Name,
234+ sess, attachment, err := d.routeWithContext(context.Background(), protocol.Hello{
235+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: record.Name,
236236 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,
237- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
237+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
238238 }, transport)
239239 require.NoError(t, err)
240240 t.Cleanup(func() { d.clientGone(sess, attachment, transport, false) })
@@ -249,7 +249,7 @@ func TestRouteRemoteTargetRestoresStoppedStableTabAndOwnsEnvironment(t *testing.
249249 lifecycle := remoteLifecycleForTest()
250250 d.mu.Lock()
251251 d.inactive["work"] = inactiveSession{
252- name: "work", cwd: "/remote/work", incarnation: lifecycle, state: ports.SessionDown,
252+ name: "work", cwd: "/remote/work", incarnation: lifecycle, state: protocol.SessionDown,
253253 tabNames: []string{"alpha", "beta"},
254254 tabRecords: []domain.CatalogueTabRecord{
255255 {StableID: "tab-a", Name: "alpha"},
@@ -264,10 +264,10 @@ func TestRouteRemoteTargetRestoresStoppedStableTabAndOwnsEnvironment(t *testing.
264264 StoppedTab: domain.NewStableTabSelector("tab-b"),
265265 }
266266 tr, _ := newCapturingTransport(t)
267- hello := ports.Hello{
268- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work",
267+ hello := protocol.Hello{
268+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work",
269269 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,
270- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
270+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
271271 Env: []string{"VEV_REMOTE_PICKER_SENTINEL=untrusted"},
272272 }
273273 sess, ac, err := d.routeWithContext(context.Background(), hello, tr)
M internal/usecase/daemon/render_coordinator_test.go +2 -2
@@ -46,7 +46,7 @@ func newCoordinatorMockClock(t *testing.T, capacity int) *coordinatorMockClock {
4646 }
4747 clk.clock.EXPECT().Now().Return(time.Time{}).Maybe()
4848 clk.clock.EXPECT().NewTimer(mock.MatchedBy(func(d time.Duration) bool {
49- return d == ports.HandshakeTimeout ||
49+ return d == protocol.HandshakeTimeout ||
5050 d == urgentRenderDeadline ||
5151 (d >= minOutputRenderDeadline && d <= maxOutputRenderDeadline) ||
5252 d == maxSyncUpdateDuration ||
@@ -66,7 +66,7 @@ func newCoordinatorMockClock(t *testing.T, capacity int) *coordinatorMockClock {
6666 timer.mock.EXPECT().Stop().Run(func() {
6767 timer.stopOnce.Do(func() { close(timer.stopped) })
6868 }).Maybe().Return(true)
69- if d == ports.HandshakeTimeout {
69+ if d == protocol.HandshakeTimeout {
7070 clk.handshakeTimers <- timer
7171 } else {
7272 clk.timers <- timer
M internal/usecase/daemon/render_test.go +4 -3
@@ -21,6 +21,7 @@ import (
2121 "github.com/bnema/vev/internal/domain"
2222 "github.com/bnema/vev/internal/ports"
2323 portsmocks "github.com/bnema/vev/internal/ports/mocks"
24+ "github.com/bnema/vev/internal/protocol"
2425 scopy "github.com/bnema/vev/internal/usecase/copy"
2526 "github.com/bnema/vev/internal/usecase/layout"
2627 themeui "github.com/bnema/vev/internal/usecase/theme"
@@ -591,7 +592,7 @@ func TestAltXClosesFinalTabAndDetaches(t *testing.T) {
591592 f := awaitFrame(t, sends, ports.MsgDetached)
592593 det, err := ports.UnmarshalDetached(f.Payload)
593594 require.NoError(t, err)
594- require.Equal(t, ports.ReasonSessionKilled, det.Reason)
595+ require.Equal(t, protocol.ReasonSessionKilled, det.Reason)
595596 }
596597
597598 func TestPTYEOFClosesActiveNonFinalTabAndRepaintsRemaining(t *testing.T) {
@@ -657,7 +658,7 @@ func TestPTYEOFFinalTabKillsSessionAndDetaches(t *testing.T) {
657658 f := awaitFrame(t, sends, ports.MsgDetached)
658659 det, err := ports.UnmarshalDetached(f.Payload)
659660 require.NoError(t, err)
660- require.Equal(t, ports.ReasonSessionKilled, det.Reason)
661+ require.Equal(t, protocol.ReasonSessionKilled, det.Reason)
661662
662663 d.sessWg.Wait()
663664 }
@@ -986,7 +987,7 @@ func TestSendErrorKeepsEphemeralHeadless(t *testing.T) {
986987 require.Empty(t, sess.snapshotAttachmentsLocked())
987988 sess.mu.Unlock()
988989
989- _ = d.killSession(sess, ports.ReasonServerShutdown, false)
990+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
990991 cancel()
991992 d.waitNotifies()
992993 }
M internal/usecase/daemon/resize_mode_test.go +5 -5
@@ -5,7 +5,7 @@ import (
55 "testing"
66
77 "github.com/bnema/vev/internal/domain"
8- "github.com/bnema/vev/internal/ports"
8+ "github.com/bnema/vev/internal/protocol"
99 "github.com/bnema/vev/internal/usecase/layout"
1010 "github.com/stretchr/testify/require"
1111 )
@@ -56,12 +56,12 @@ func TestResizeControlHeadlessAndErrors(t *testing.T) {
5656 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })
5757 sess := addControlSession(d, "work", "t_work", "p_work")
5858
59- tooEarly := sendCommand(t, d, ports.CommandRequest{Slug: "grow-pane-width", TargetSession: "work"})
59+ tooEarly := sendCommand(t, d, protocol.CommandRequest{Slug: "grow-pane-width", TargetSession: "work"})
6060 require.False(t, tooEarly.OK)
61- require.Equal(t, ports.ErrNoSuchTarget, tooEarly.Code)
61+ require.Equal(t, protocol.ErrNoSuchTarget, tooEarly.Code)
6262 require.Equal(t, "pane is not in a split", tooEarly.Text)
6363
64- require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
64+ require.True(t, sendCommand(t, d, protocol.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
6565 tb := testAttachmentTab(sess)
6666 tb.mu.Lock()
6767 beforeFocus := tb.tree.Focus
@@ -87,7 +87,7 @@ func TestResizeControlHeadlessAndErrors(t *testing.T) {
8787 require.Equal(t, beforeGeneration+1, tb.layoutGeneration)
8888 tb.mu.Unlock()
8989
90- equalized := sendCommand(t, d, ports.CommandRequest{Slug: "equalize-panes", TargetSession: "work"})
90+ equalized := sendCommand(t, d, protocol.CommandRequest{Slug: "equalize-panes", TargetSession: "work"})
9191 require.True(t, equalized.OK, equalized.Text)
9292 require.Empty(t, equalized.Output)
9393 }
M internal/usecase/daemon/resume.go +17 -16
@@ -7,6 +7,7 @@ import (
77
88 "github.com/bnema/vev/internal/domain"
99 "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
1011 )
1112
1213 var errResumeTokenLifecycleRace = errors.New("resume token lifecycle race")
@@ -164,7 +165,7 @@ func (d *Daemon) purgeParkingForSessionLocked(sess *session) {
164165 // waitParkingInFlight waits for an identified same-client parking publication
165166 // to finish. Returns true when a matching in-flight entry was observed.
166167 // Unknown and mismatched credentials return false without waiting.
167-func (d *Daemon) waitParkingInFlight(h ports.Hello) bool {
168+func (d *Daemon) waitParkingInFlight(h protocol.Hello) bool {
168169 if h.ResumeToken == 0 {
169170 return false
170171 }
@@ -479,11 +480,11 @@ func (d *Daemon) finishParkedAttachmentRetirements(retirements []parkedAttachmen
479480 // to the named session's active attachment because transport teardown has not
480481 // parked it yet. Only the exact live owner (same session, token, and client ID)
481482 // is accepted; arbitrary unknown tokens stay fail-closed.
482-func (d *Daemon) resumeLiveAttachment(h ports.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {
483+func (d *Daemon) resumeLiveAttachment(h protocol.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {
483484 d.mu.Lock()
484485 if d.closing {
485486 d.mu.Unlock()
486- return nil, nil, false, &protoErr{ports.ErrServerShutdown, "daemon is shutting down"}
487+ return nil, nil, false, &protoErr{protocol.ErrServerShutdown, "daemon is shutting down"}
487488 }
488489 // Teardown may have parked the credential already.
489490 if d.parked[h.ResumeToken] != nil {
@@ -521,14 +522,14 @@ func (d *Daemon) resumeLiveAttachment(h ports.Hello, tr ports.Transport, sz doma
521522 }
522523 if credentialMatch && h.RemoteTarget != nil && !d.remoteTargetMatchesSessionLocked(sess, *h.RemoteTarget) {
523524 d.mu.Unlock()
524- return nil, nil, false, &protoErr{ports.ErrNoSuchTarget, "remote target no longer matches resumed session"}
525+ return nil, nil, false, &protoErr{protocol.ErrNoSuchTarget, "remote target no longer matches resumed session"}
525526 }
526527 d.mu.Unlock()
527528
528529 if clientMismatch {
529530 reason := "client id mismatch"
530531 d.log.Warn("resume rejected", "session", sess.nameSnapshot(), "err", reason)
531- return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}
532+ return nil, nil, false, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
532533 }
533534 if !credentialMatch {
534535 // Detach may have won and not published parked yet; wait out that gap
@@ -569,9 +570,9 @@ func (d *Daemon) resumeLiveAttachment(h ports.Hello, tr ports.Transport, sz doma
569570 closing := d.closing
570571 d.mu.Unlock()
571572 if closing {
572- return nil, nil, false, &protoErr{ports.ErrServerShutdown, "daemon is shutting down"}
573+ return nil, nil, false, &protoErr{protocol.ErrServerShutdown, "daemon is shutting down"}
573574 }
574- return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}
575+ return nil, nil, false, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
575576 }
576577 _ = ac.closeCapturedTransport(oldSnap.transport)
577578 d.log.Info("live resume credential parked for reconnect", "session", sess.nameSnapshot())
@@ -582,7 +583,7 @@ func (d *Daemon) resumeLiveAttachment(h ports.Hello, tr ports.Transport, sz doma
582583 // registry lock, preserving the global sendMu > Daemon.mu ordering. The parked
583584 // entry is revalidated after both locks are held because it may expire between
584585 // the initial lookup and lock acquisition.
585-func (d *Daemon) resumeParked(h ports.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {
586+func (d *Daemon) resumeParked(h protocol.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {
586587 d.mu.Lock()
587588 parked := d.parked[h.ResumeToken]
588589 d.mu.Unlock()
@@ -599,7 +600,7 @@ func (d *Daemon) resumeParked(h ports.Hello, tr ports.Transport, sz domain.Size)
599600 if d.closing {
600601 d.mu.Unlock()
601602 ac.sendMu.Unlock()
602- return nil, nil, false, &protoErr{ports.ErrServerShutdown, "daemon is shutting down"}
603+ return nil, nil, false, &protoErr{protocol.ErrServerShutdown, "daemon is shutting down"}
603604 }
604605 if d.parked[h.ResumeToken] != parked || parked.claimed {
605606 if d.sessions[parked.sess.id] == parked.sess {
@@ -609,7 +610,7 @@ func (d *Daemon) resumeParked(h ports.Hello, tr ports.Transport, sz domain.Size)
609610 }
610611 d.mu.Unlock()
611612 ac.sendMu.Unlock()
612- return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}
613+ return nil, nil, false, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
613614 }
614615 sess, resumed, ok, err := d.resumeParkedLocked(h, tr, sz)
615616 d.mu.Unlock()
@@ -631,7 +632,7 @@ func (d *Daemon) resumeParked(h ports.Hello, tr ports.Transport, sz domain.Size)
631632
632633 // resumeParkedLocked completes a validated resume. Caller holds both ac.sendMu
633634 // and d.mu in that order.
634-func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {
635+func (d *Daemon) resumeParkedLocked(h protocol.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {
635636 parked := d.parked[h.ResumeToken]
636637 if parked == nil || h.ResumeToken == 0 {
637638 return nil, nil, false, nil
@@ -641,7 +642,7 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain
641642 if !registered {
642643 d.removeParkedLocked(h.ResumeToken, parked)
643644 d.log.Warn("resume rejected", "session", sess.nameSnapshot(), "registered", false)
644- return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}
645+ return nil, nil, false, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
645646 }
646647 ac := parked.ac
647648 if parked.claimed {
@@ -650,10 +651,10 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain
650651 if ac.clientID != h.ClientID {
651652 reason := "client id mismatch"
652653 d.log.Warn("resume rejected", "session", sess.nameSnapshot(), "err", reason)
653- return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}
654+ return nil, nil, false, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
654655 }
655656 if h.RemoteTarget != nil && !d.remoteTargetMatchesSessionLocked(sess, *h.RemoteTarget) {
656- return nil, nil, false, &protoErr{ports.ErrNoSuchTarget, "remote target no longer matches resumed session"}
657+ return nil, nil, false, &protoErr{protocol.ErrNoSuchTarget, "remote target no longer matches resumed session"}
657658 }
658659 // Claim the attachment while retaining the old credential until Welcome is
659660 // accepted. A failed handshake can therefore restore this exact parked
@@ -680,7 +681,7 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain
680681 // The resumed session's snapshot is the sole source for future PTY children.
681682 // Existing PTYs retain the environment they were started with.
682683 sess.mu.Lock()
683- if h.EnvironmentPolicy != ports.EnvironmentPolicyDaemonOwned {
684+ if h.EnvironmentPolicy != protocol.EnvironmentPolicyDaemonOwned {
684685 sess.env = copyEnvironment(h.Env)
685686 }
686687 sess.mu.Unlock()
@@ -702,7 +703,7 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain
702703 ac.sendMu.Lock()
703704 d.mu.Lock()
704705 if err != nil {
705- return sess, ac, false, &protoErr{ports.ErrInternal, "resume attachment transition failed"}
706+ return sess, ac, false, &protoErr{protocol.ErrInternal, "resume attachment transition failed"}
706707 }
707708 d.deferAttachmentTransitionCleanups(transition)
708709 d.touchMRU(sess)
M internal/usecase/daemon/resume_test.go +118 -117
@@ -15,6 +15,7 @@ import (
1515 "github.com/bnema/vev/internal/domain"
1616 "github.com/bnema/vev/internal/ports"
1717 portsmocks "github.com/bnema/vev/internal/ports/mocks"
18+ "github.com/bnema/vev/internal/protocol"
1819 "github.com/bnema/vev/internal/usecase/layout"
1920 )
2021
@@ -85,9 +86,9 @@ func (t *closeCountingBlockedTransport) CloseCount() int {
8586 return t.count
8687 }
8788
88-func helloResumeCapable(intent uint8, name string, token uint64) ports.Hello {
89- return ports.Hello{
90- Version: ports.ProtocolVersion,
89+func helloResumeCapable(intent uint8, name string, token uint64) protocol.Hello {
90+ return protocol.Hello{
91+ Version: protocol.Version,
9192 Intent: intent,
9293 ClientID: [16]byte{1, 2, 3, 4},
9394 ResumeToken: token,
@@ -100,10 +101,10 @@ func helloResumeCapable(intent uint8, name string, token uint64) ports.Hello {
100101 func TestTokenlessResumeIsRejectedBeforeNameRouting(t *testing.T) {
101102 d := newTestDaemon(t, nil, stubClock{})
102103 tr, _ := newCapturingTransport(t)
103- _, _, err := d.route(helloResumeCapable(ports.IntentResume, "work", 0), tr)
104+ _, _, err := d.route(helloResumeCapable(protocol.IntentResume, "work", 0), tr)
104105 var protocolErr *protoErr
105106 require.ErrorAs(t, err, &protocolErr)
106- require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)
107+ require.Equal(t, protocol.ErrNoSuchSession, protocolErr.code)
107108 d.mu.Lock()
108109 sessionCount := len(d.sessions)
109110 d.mu.Unlock()
@@ -115,27 +116,27 @@ func TestResumeRejectsMismatchedRemoteTargetBeforeOwnershipMutation(t *testing.T
115116 defer release()
116117 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
117118 oldTransport := &closeTrackingTransport{}
118- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
119+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
119120 require.NoError(t, err)
120121 token := ac.resumeToken
121122
122123 target := domain.RemoteSessionTarget{
123124 LifecycleID: sess.incarnation, SessionName: sess.name, LiveTabID: "missing-tab",
124125 }
125- resume := helloResumeCapable(ports.IntentResume, "work", token)
126+ resume := helloResumeCapable(protocol.IntentResume, "work", token)
126127 resume.RemoteTarget = &target
127- resume.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned
128+ resume.EnvironmentPolicy = protocol.EnvironmentPolicyDaemonOwned
128129 _, _, err = d.route(resume, &closeTrackingTransport{})
129130 var protocolErr *protoErr
130131 require.ErrorAs(t, err, &protocolErr)
131- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
132+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
132133 require.Same(t, oldTransport, ac.transport())
133134 d.mu.Lock()
134135 _, parked := d.parked[token]
135136 d.mu.Unlock()
136137 require.False(t, parked, "mismatched target must not park the live attachment")
137138
138- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))
139+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
139140 }
140141
141142 func TestParkedResumeRejectsMismatchedRemoteTargetBeforeClaim(t *testing.T) {
@@ -143,7 +144,7 @@ func TestParkedResumeRejectsMismatchedRemoteTargetBeforeClaim(t *testing.T) {
143144 defer release()
144145 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
145146 oldTransport := &closeTrackingTransport{}
146- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
147+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
147148 require.NoError(t, err)
148149 token := ac.resumeToken
149150 d.clientGone(sess, ac, oldTransport, false)
@@ -151,13 +152,13 @@ func TestParkedResumeRejectsMismatchedRemoteTargetBeforeClaim(t *testing.T) {
151152 target := domain.RemoteSessionTarget{
152153 LifecycleID: sess.incarnation, SessionName: sess.name, LiveTabID: "missing-tab",
153154 }
154- resume := helloResumeCapable(ports.IntentResume, "work", token)
155+ resume := helloResumeCapable(protocol.IntentResume, "work", token)
155156 resume.RemoteTarget = &target
156- resume.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned
157+ resume.EnvironmentPolicy = protocol.EnvironmentPolicyDaemonOwned
157158 _, _, err = d.route(resume, &closeTrackingTransport{})
158159 var protocolErr *protoErr
159160 require.ErrorAs(t, err, &protocolErr)
160- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
161+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
161162 d.mu.Lock()
162163 parked := d.parked[token]
163164 claimed := parked != nil && parked.claimed
@@ -165,7 +166,7 @@ func TestParkedResumeRejectsMismatchedRemoteTargetBeforeClaim(t *testing.T) {
165166 require.NotNil(t, parked)
166167 require.False(t, claimed, "mismatched target must not claim the parked attachment")
167168
168- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))
169+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
169170 }
170171
171172 func TestFailedResumeRearmsParkExpiryAfterClaimTimerFires(t *testing.T) {
@@ -175,7 +176,7 @@ func TestFailedResumeRearmsParkExpiryAfterClaimTimerFires(t *testing.T) {
175176 d := newTestDaemon(t, newFactory(t, pty), clock)
176177
177178 oldTransport := &closeTrackingTransport{}
178- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
179+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
179180 require.NoError(t, err)
180181 token := ac.resumeToken
181182 d.clientGone(sess, ac, oldTransport, false)
@@ -185,7 +186,7 @@ func TestFailedResumeRearmsParkExpiryAfterClaimTimerFires(t *testing.T) {
185186 d.mu.Unlock()
186187 require.NotNil(t, parked)
187188
188- resumed, resumedAC, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{}, defaultSize)
189+ resumed, resumedAC, ok, err := d.resumeParked(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{}, defaultSize)
189190 require.NoError(t, err)
190191 require.True(t, ok)
191192 require.Same(t, sess, resumed)
@@ -212,7 +213,7 @@ func TestFailedResumeRearmsParkExpiryAfterClaimTimerFires(t *testing.T) {
212213 d.mu.Unlock()
213214 require.False(t, retained, "rearmed credential must still expire")
214215
215- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))
216+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
216217 d.sessWg.Wait()
217218 }
218219
@@ -221,8 +222,8 @@ func TestNamedLinkLossParks(t *testing.T) {
221222 defer release()
222223 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
223224
224- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))
225- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)
225+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
226+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
226227 require.NoError(t, err)
227228 token := ac.resumeToken
228229 require.NotZero(t, token)
@@ -241,7 +242,7 @@ func TestBoundedSendTimeoutCannotTargetResumedTransport(t *testing.T) {
241242 defer release()
242243 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
243244 oldTransport := &closeTrackingTransport{}
244- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "bounded-send-resume", 0), oldTransport)
245+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "bounded-send-resume", 0), oldTransport)
245246 require.NoError(t, err)
246247 token := ac.resumeToken
247248 d.clientGone(sess, ac, oldTransport, false)
@@ -259,7 +260,7 @@ func TestBoundedSendTimeoutCannotTargetResumedTransport(t *testing.T) {
259260 go func() {
260261 transport, sendErr := d.boundedSendWithTimeout(time.Second, expected.transport, func() error {
261262 defer close(orphanDone)
262- return ac.sendExpectedTransport(expected, frameDetached(ports.ReasonDetach))
263+ return ac.sendExpectedTransport(expected, frameDetached(protocol.ReasonDetach))
263264 })
264265 result <- boundedResult{transport: transport, err: sendErr}
265266 }()
@@ -276,7 +277,7 @@ func TestBoundedSendTimeoutCannotTargetResumedTransport(t *testing.T) {
276277 newTransport := &closeTrackingTransport{}
277278 d.mu.Lock()
278279 resumedSess, resumedAC, ok, err := d.resumeParkedLocked(
279- helloResumeCapable(ports.IntentResume, sess.name, token),
280+ helloResumeCapable(protocol.IntentResume, sess.name, token),
280281 newTransport,
281282 domain.Size{Cols: 80, Rows: 24},
282283 )
@@ -301,7 +302,7 @@ func TestDetachedNotificationTimeoutClosesCapturedTransportOnce(t *testing.T) {
301302 d.notifyDetachedSnapshotAsync(detachedAttachmentSnapshot{
302303 ac: ac,
303304 transport: ac.transportSnapshot(),
304- }, ports.ReasonDetach)
305+ }, protocol.ReasonDetach)
305306 timer := <-clock.timers
306307 timer.ch <- time.Time{}
307308 d.waitNotifies()
@@ -316,14 +317,14 @@ func TestHandleHelloResumeDefersFreshOutputUntilWelcome(t *testing.T) {
316317 clock := newCoordinatorMockClock(t, 4)
317318 d := newTestDaemon(t, newFactorySeq(t, pty), clock.clock)
318319 oldTransport := &closeTrackingTransport{}
319- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "resume-welcome-gate", 0), oldTransport)
320+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "resume-welcome-gate", 0), oldTransport)
320321 require.NoError(t, err)
321322 token := ac.resumeToken
322323 d.clientGone(sess, ac, oldTransport, false)
323324
324325 tr := newWelcomeBlockingTransport(t)
325326 done := make(chan struct{})
326- resumeHello := helloResumeCapable(ports.IntentResume, sess.name, token)
327+ resumeHello := helloResumeCapable(protocol.IntentResume, sess.name, token)
327328 go func() {
328329 d.handleHello(tr.tr, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(resumeHello)})
329330 close(done)
@@ -366,7 +367,7 @@ func TestEphemeralLinkLossParksAndResumes(t *testing.T) {
366367 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})
367368
368369 tr := &closeTrackingTransport{}
369- initial := helloResumeCapable(ports.IntentEphemeral, "", 0)
370+ initial := helloResumeCapable(protocol.IntentEphemeral, "", 0)
370371 initial.Size = domain.Size{Cols: 120, Rows: 40}
371372 sess, ac, err := d.route(initial, tr)
372373 require.NoError(t, err)
@@ -374,7 +375,7 @@ func TestEphemeralLinkLossParksAndResumes(t *testing.T) {
374375 token := ac.resumeToken
375376 require.NotZero(t, token, "ephemeral sessions receive resume tokens")
376377
377- peerHello := helloResumeCapable(ports.IntentAttach, sess.name, 0)
378+ peerHello := helloResumeCapable(protocol.IntentAttach, sess.name, 0)
378379 peerHello.ClientID = [16]byte{9}
379380 peerHello.Size = domain.Size{Cols: 100, Rows: 30}
380381 peer := &closeTrackingTransport{}
@@ -404,7 +405,7 @@ func TestEphemeralLinkLossParksAndResumes(t *testing.T) {
404405 d.mu.Unlock()
405406 require.True(t, parked, "ephemeral resume-capable link loss is parked")
406407
407- resumeHello := helloResumeCapable(ports.IntentResume, sess.name, token)
408+ resumeHello := helloResumeCapable(protocol.IntentResume, sess.name, token)
408409 resumeHello.Size = domain.Size{Cols: 80, Rows: 24}
409410 newTr := &closeTrackingTransport{}
410411 resumedSess, resumedAC, err := d.route(resumeHello, newTr)
@@ -431,7 +432,7 @@ func TestResumeParkedTokenReplacedDuringWaitFailsClosed(t *testing.T) {
431432 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
432433
433434 oldTr := &closeTrackingTransport{}
434- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)
435+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
435436 require.NoError(t, err)
436437 token := ac.resumeToken
437438 require.NotZero(t, token)
@@ -460,7 +461,7 @@ func TestResumeParkedTokenReplacedDuringWaitFailsClosed(t *testing.T) {
460461 }
461462 result := make(chan routeResult, 1)
462463 go func() {
463- routedSess, routedAC, routeErr := d.route(helloResumeCapable(ports.IntentResume, "work", token), resumeTr)
464+ routedSess, routedAC, routeErr := d.route(helloResumeCapable(protocol.IntentResume, "work", token), resumeTr)
464465 result <- routeResult{sess: routedSess, ac: routedAC, err: routeErr}
465466 }()
466467 awaitTestCompletion(t, reachedLookup, "resumeParked did not pause after the parked lookup")
@@ -480,7 +481,7 @@ func TestResumeParkedTokenReplacedDuringWaitFailsClosed(t *testing.T) {
480481 require.Nil(t, got.ac)
481482 var pe *protoErr
482483 require.ErrorAs(t, got.err, &pe)
483- require.Equal(t, ports.ErrNoSuchSession, pe.code)
484+ require.Equal(t, protocol.ErrNoSuchSession, pe.code)
484485 require.Contains(t, pe.Error(), "resume token is no longer valid")
485486 require.Empty(t, sess.snapshotAttachments(), "lifecycle-race resume must not take over the named session")
486487 require.Empty(t, resumeTr.Sends(), "failed resume must not complete a Welcome handshake")
@@ -503,7 +504,7 @@ func TestResumeLiveAttachmentParkedResumeRaceFailsClosed(t *testing.T) {
503504 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
504505
505506 oldTr := &closeTrackingTransport{}
506- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)
507+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
507508 require.NoError(t, err)
508509 token := ac.resumeToken
509510 require.NotZero(t, token)
@@ -531,7 +532,7 @@ func TestResumeLiveAttachmentParkedResumeRaceFailsClosed(t *testing.T) {
531532 }
532533 result := make(chan routeResult, 1)
533534 go func() {
534- routedSess, routedAC, routeErr := d.route(helloResumeCapable(ports.IntentResume, "work", token), resumeTr)
535+ routedSess, routedAC, routeErr := d.route(helloResumeCapable(protocol.IntentResume, "work", token), resumeTr)
535536 result <- routeResult{sess: routedSess, ac: routedAC, err: routeErr}
536537 }()
537538 awaitTestCompletion(t, reachedLookup, "live resume did not pause inside resumeParked after parking")
@@ -554,7 +555,7 @@ func TestResumeLiveAttachmentParkedResumeRaceFailsClosed(t *testing.T) {
554555 require.False(t, errors.Is(got.err, errResumeTokenLifecycleRace), "raw lifecycle sentinel must not reach route callers")
555556 var pe *protoErr
556557 require.ErrorAs(t, got.err, &pe)
557- require.Equal(t, ports.ErrNoSuchSession, pe.code)
558+ require.Equal(t, protocol.ErrNoSuchSession, pe.code)
558559 require.Contains(t, pe.Error(), "resume token is no longer valid")
559560 require.Empty(t, sess.snapshotAttachments(), "losing live resume must not take over the named session")
560561 require.Empty(t, resumeTr.Sends(), "failed live resume must not complete a Welcome handshake")
@@ -576,7 +577,7 @@ func TestResumeDuringTeardownBeforeParkRecoversSameAttachment(t *testing.T) {
576577 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
577578
578579 oldTr := &closeTrackingTransport{}
579- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)
580+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
580581 require.NoError(t, err)
581582 token := ac.resumeToken
582583 require.NotZero(t, token)
@@ -608,20 +609,20 @@ func TestResumeDuringTeardownBeforeParkRecoversSameAttachment(t *testing.T) {
608609 require.Same(t, sess, parkingInGap.sess)
609610 require.False(t, parkedInGap, "fixture: park must not have published yet")
610611
611- _, _, err = d.route(helloResumeCapable(ports.IntentResume, "work", 0xdecafbad), &closeTrackingTransport{})
612+ _, _, err = d.route(helloResumeCapable(protocol.IntentResume, "work", 0xdecafbad), &closeTrackingTransport{})
612613 require.Error(t, err)
613614 var unknownErr *protoErr
614615 require.ErrorAs(t, err, &unknownErr)
615- require.Equal(t, ports.ErrNoSuchSession, unknownErr.code)
616+ require.Equal(t, protocol.ErrNoSuchSession, unknownErr.code)
616617 require.Contains(t, unknownErr.Error(), "resume token is no longer valid")
617618
618- wrongClient := helloResumeCapable(ports.IntentResume, "work", token)
619+ wrongClient := helloResumeCapable(protocol.IntentResume, "work", token)
619620 wrongClient.ClientID = [16]byte{9, 9, 9, 9}
620621 _, _, err = d.route(wrongClient, &closeTrackingTransport{})
621622 require.Error(t, err)
622623 var mismatchErr *protoErr
623624 require.ErrorAs(t, err, &mismatchErr)
624- require.Equal(t, ports.ErrNoSuchSession, mismatchErr.code)
625+ require.Equal(t, protocol.ErrNoSuchSession, mismatchErr.code)
625626 require.Contains(t, mismatchErr.Error(), "resume token is no longer valid")
626627
627628 newTr := &closeTrackingTransport{}
@@ -634,7 +635,7 @@ func TestResumeDuringTeardownBeforeParkRecoversSameAttachment(t *testing.T) {
634635 d.afterParkingWaitArmed = func() { close(waiterArmed) }
635636 result := make(chan routeResult, 1)
636637 go func() {
637- routedSess, routedAC, routeErr := d.route(helloResumeCapable(ports.IntentResume, "work", token), newTr)
638+ routedSess, routedAC, routeErr := d.route(helloResumeCapable(protocol.IntentResume, "work", token), newTr)
638639 result <- routeResult{sess: routedSess, ac: routedAC, err: routeErr}
639640 }()
640641 awaitTestCompletion(t, waiterArmed, "resume waiter did not arm on parking marker before park")
@@ -675,7 +676,7 @@ func TestConcurrentLiveResumesWaitParkingMarkerBeforePark(t *testing.T) {
675676 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
676677
677678 oldTr := &closeTrackingTransport{}
678- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)
679+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
679680 require.NoError(t, err)
680681 token := ac.resumeToken
681682 require.NotZero(t, token)
@@ -698,7 +699,7 @@ func TestConcurrentLiveResumesWaitParkingMarkerBeforePark(t *testing.T) {
698699 firstTr := &closeTrackingTransport{}
699700 firstResult := make(chan routeResult, 1)
700701 go func() {
701- routedSess, routedAC, routeErr := d.route(helloResumeCapable(ports.IntentResume, "work", token), firstTr)
702+ routedSess, routedAC, routeErr := d.route(helloResumeCapable(protocol.IntentResume, "work", token), firstTr)
702703 firstResult <- routeResult{sess: routedSess, ac: routedAC, err: routeErr}
703704 }()
704705 awaitTestCompletion(t, reachedGap, "first live resume did not pause after detach before park")
@@ -712,11 +713,11 @@ func TestConcurrentLiveResumesWaitParkingMarkerBeforePark(t *testing.T) {
712713 require.Same(t, ac, parkingInGap.ac)
713714 require.False(t, parkedInGap, "fixture: park must not have published yet")
714715
715- _, _, err = d.route(helloResumeCapable(ports.IntentResume, "work", 0xdecafbad), &closeTrackingTransport{})
716+ _, _, err = d.route(helloResumeCapable(protocol.IntentResume, "work", 0xdecafbad), &closeTrackingTransport{})
716717 require.Error(t, err)
717718 var unknownErr *protoErr
718719 require.ErrorAs(t, err, &unknownErr)
719- require.Equal(t, ports.ErrNoSuchSession, unknownErr.code)
720+ require.Equal(t, protocol.ErrNoSuchSession, unknownErr.code)
720721
721722 secondTr := &closeTrackingTransport{}
722723 secondWaiting := make(chan struct{})
@@ -725,7 +726,7 @@ func TestConcurrentLiveResumesWaitParkingMarkerBeforePark(t *testing.T) {
725726 }
726727 secondResult := make(chan routeResult, 1)
727728 go func() {
728- routedSess, routedAC, routeErr := d.route(helloResumeCapable(ports.IntentResume, "work", token), secondTr)
729+ routedSess, routedAC, routeErr := d.route(helloResumeCapable(protocol.IntentResume, "work", token), secondTr)
729730 secondResult <- routeResult{sess: routedSess, ac: routedAC, err: routeErr}
730731 }()
731732 awaitTestCompletion(t, secondWaiting, "second live resume did not arm parking wait")
@@ -757,7 +758,7 @@ func TestConcurrentLiveResumesWaitParkingMarkerBeforePark(t *testing.T) {
757758
758759 var loserErr *protoErr
759760 require.ErrorAs(t, loser.err, &loserErr)
760- require.Equal(t, ports.ErrNoSuchSession, loserErr.code)
761+ require.Equal(t, protocol.ErrNoSuchSession, loserErr.code)
761762 require.Contains(t, loserErr.Error(), "resume token is no longer valid")
762763 require.True(t, loserTr.Closed() || len(loserTr.Sends()) == 0, "losing resume must not complete Welcome")
763764
@@ -784,13 +785,13 @@ func TestExplicitDetachClearsOrphanedSameAttachmentParkingMarker(t *testing.T) {
784785 d := newTestDaemon(t, newFactorySeq(t, workPTY, otherPTY), stubClock{})
785786
786787 oldTr := &closeTrackingTransport{}
787- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)
788+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
788789 require.NoError(t, err)
789790 token := ac.resumeToken
790791 require.NotZero(t, token)
791792
792793 otherTr := &closeTrackingTransport{}
793- otherSess, otherAC, err := d.route(helloResumeCapable(ports.IntentNew, "other", 0), otherTr)
794+ otherSess, otherAC, err := d.route(helloResumeCapable(protocol.IntentNew, "other", 0), otherTr)
794795 require.NoError(t, err)
795796 otherToken := d.markParkingInFlight(otherSess, otherAC)
796797 require.NotZero(t, otherToken)
@@ -811,7 +812,7 @@ func TestExplicitDetachClearsOrphanedSameAttachmentParkingMarker(t *testing.T) {
811812 d.afterParkingWaitArmed = func() { close(waiterArmed) }
812813 waiterDone := make(chan bool, 1)
813814 go func() {
814- waiterDone <- d.waitParkingInFlight(helloResumeCapable(ports.IntentResume, "work", token))
815+ waiterDone <- d.waitParkingInFlight(helloResumeCapable(protocol.IntentResume, "work", token))
815816 }()
816817 awaitTestCompletion(t, waiterArmed, "parking waiter did not arm on the orphaned marker")
817818
@@ -829,18 +830,18 @@ func TestExplicitDetachClearsOrphanedSameAttachmentParkingMarker(t *testing.T) {
829830 require.NotNil(t, otherAfter, "explicit winner must not clear another attachment's marker")
830831 require.Same(t, otherAC, otherAfter.ac)
831832
832- _, _, err = d.route(helloResumeCapable(ports.IntentResume, "work", 0xdecafbad), &closeTrackingTransport{})
833+ _, _, err = d.route(helloResumeCapable(protocol.IntentResume, "work", 0xdecafbad), &closeTrackingTransport{})
833834 require.Error(t, err)
834835 var unknownErr *protoErr
835836 require.ErrorAs(t, err, &unknownErr)
836- require.Equal(t, ports.ErrNoSuchSession, unknownErr.code)
837+ require.Equal(t, protocol.ErrNoSuchSession, unknownErr.code)
837838 require.Contains(t, unknownErr.Error(), "resume token is no longer valid")
838839
839- _, _, err = d.route(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{})
840+ _, _, err = d.route(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{})
840841 require.Error(t, err)
841842 var retiredErr *protoErr
842843 require.ErrorAs(t, err, &retiredErr)
843- require.Equal(t, ports.ErrNoSuchSession, retiredErr.code)
844+ require.Equal(t, protocol.ErrNoSuchSession, retiredErr.code)
844845 require.Contains(t, retiredErr.Error(), "resume token is no longer valid")
845846
846847 // A later parked lifecycle for another attachment must survive a clear
@@ -882,7 +883,7 @@ func TestLiveResumeRejectsLateMarkerAfterTerminalCleanupWins(t *testing.T) {
882883 name: "session_removal",
883884 win: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient, oldTr *closeTrackingTransport, token uint64) {
884885 t.Helper()
885- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))
886+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
886887 d.mu.Lock()
887888 registered := d.sessions[sess.id] == sess
888889 d.mu.Unlock()
@@ -897,7 +898,7 @@ func TestLiveResumeRejectsLateMarkerAfterTerminalCleanupWins(t *testing.T) {
897898 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
898899
899900 oldTr := &closeTrackingTransport{}
900- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)
901+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
901902 require.NoError(t, err)
902903 token := ac.resumeToken
903904 require.NotZero(t, token)
@@ -917,7 +918,7 @@ func TestLiveResumeRejectsLateMarkerAfterTerminalCleanupWins(t *testing.T) {
917918 }
918919 result := make(chan routeResult, 1)
919920 go func() {
920- _, _, routeErr := d.route(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{})
921+ _, _, routeErr := d.route(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{})
921922 result <- routeResult{err: routeErr}
922923 }()
923924 awaitTestCompletion(t, validated, "live resume did not pause after credential validation before mark")
@@ -934,7 +935,7 @@ func TestLiveResumeRejectsLateMarkerAfterTerminalCleanupWins(t *testing.T) {
934935 require.Error(t, got.err)
935936 var pe *protoErr
936937 require.ErrorAs(t, got.err, &pe)
937- require.Equal(t, ports.ErrNoSuchSession, pe.code)
938+ require.Equal(t, protocol.ErrNoSuchSession, pe.code)
938939 require.Contains(t, pe.Error(), "resume token is no longer valid")
939940
940941 d.mu.Lock()
@@ -952,7 +953,7 @@ func TestLiveResumeRejectsLateMarkerAfterTerminalCleanupWins(t *testing.T) {
952953 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
953954
954955 oldTr := &closeTrackingTransport{}
955- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)
956+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
956957 require.NoError(t, err)
957958 token := ac.resumeToken
958959 require.NotZero(t, token)
@@ -986,7 +987,7 @@ func TestStaleClientGoneAfterTransportCheckDoesNotDetachReboundAttachment(t *tes
986987 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
987988
988989 oldTr := &closeTrackingTransport{}
989- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)
990+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
990991 require.NoError(t, err)
991992 token := ac.resumeToken
992993 require.NotZero(t, token)
@@ -1008,7 +1009,7 @@ func TestStaleClientGoneAfterTransportCheckDoesNotDetachReboundAttachment(t *tes
10081009 awaitTestCompletion(t, passedPrecheck, "stale clientGone did not reach the post-precheck seam")
10091010
10101011 newTr := &closeTrackingTransport{}
1011- resumedSess, resumedAC, err := d.route(helloResumeCapable(ports.IntentResume, "work", token), newTr)
1012+ resumedSess, resumedAC, err := d.route(helloResumeCapable(protocol.IntentResume, "work", token), newTr)
10121013 require.NoError(t, err)
10131014 require.Same(t, sess, resumedSess)
10141015 require.Same(t, ac, resumedAC)
@@ -1032,7 +1033,7 @@ func TestResumeLiveAttachmentParkFailureRetiresOldTransport(t *testing.T) {
10321033 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
10331034
10341035 oldTr := &closeTrackingTransport{}
1035- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)
1036+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
10361037 require.NoError(t, err)
10371038 token := ac.resumeToken
10381039 require.NotZero(t, token)
@@ -1043,11 +1044,11 @@ func TestResumeLiveAttachmentParkFailureRetiresOldTransport(t *testing.T) {
10431044 d.mu.Unlock()
10441045 }
10451046
1046- _, _, err = d.route(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{})
1047+ _, _, err = d.route(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{})
10471048 require.Error(t, err)
10481049 var pe *protoErr
10491050 require.ErrorAs(t, err, &pe)
1050- require.Equal(t, ports.ErrServerShutdown, pe.code)
1051+ require.Equal(t, protocol.ErrServerShutdown, pe.code)
10511052 require.Empty(t, sess.snapshotAttachments(), "failed live park must leave the session without an owner")
10521053 require.Nil(t, ac.transport(), "failed live park must revoke the captured old transport")
10531054 require.True(t, oldTr.Closed(), "failed live park must close the captured old transport")
@@ -1062,14 +1063,14 @@ func TestResumeRebindsRotatesAndDoesNotOpenPTY(t *testing.T) {
10621063 defer release()
10631064 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})
10641065
1065- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))
1066- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)
1066+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1067+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
10671068 require.NoError(t, err)
10681069 oldToken := ac.resumeToken
10691070 d.clientGone(sess, ac, ac.transport(), false)
10701071
1071- tr2, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))
1072- resumedSess, resumedAC, err := d.route(helloResumeCapable(ports.IntentResume, "work", oldToken), tr2)
1072+ tr2, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1073+ resumedSess, resumedAC, err := d.route(helloResumeCapable(protocol.IntentResume, "work", oldToken), tr2)
10731074 require.NoError(t, err)
10741075 require.Same(t, sess, resumedSess)
10751076 require.Same(t, ac, resumedAC)
@@ -1114,13 +1115,13 @@ func TestResumeClientIDMismatchDoesNotConsumeParkedToken(t *testing.T) {
11141115 defer release()
11151116 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})
11161117
1117- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))
1118- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)
1118+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1119+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
11191120 require.NoError(t, err)
11201121 token := ac.resumeToken
11211122 d.clientGone(sess, ac, ac.transport(), false)
11221123
1123- wrongClient := helloResumeCapable(ports.IntentResume, "work", token)
1124+ wrongClient := helloResumeCapable(protocol.IntentResume, "work", token)
11241125 wrongClient.ClientID = [16]byte{9, 9, 9, 9}
11251126 _, _, ok, err := d.resumeParked(wrongClient, &closeTrackingTransport{}, domain.Size{Cols: 80, Rows: 24})
11261127 d.mu.Lock()
@@ -1132,8 +1133,8 @@ func TestResumeClientIDMismatchDoesNotConsumeParkedToken(t *testing.T) {
11321133 require.Equal(t, token, ac.resumeToken)
11331134 require.True(t, ac.parked)
11341135
1135- tr2, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))
1136- resumedSess, resumedAC, err := d.route(helloResumeCapable(ports.IntentResume, "work", token), tr2)
1136+ tr2, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1137+ resumedSess, resumedAC, err := d.route(helloResumeCapable(protocol.IntentResume, "work", token), tr2)
11371138 require.NoError(t, err)
11381139 require.Same(t, sess, resumedSess)
11391140 require.Same(t, ac, resumedAC)
@@ -1145,7 +1146,7 @@ func TestResumeCloseCapturedOldTransportDoesNotCloseReboundTransport(t *testing.
11451146 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})
11461147
11471148 oldTr := &closeTrackingTransport{}
1148- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)
1149+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
11491150 require.NoError(t, err)
11501151 token := ac.resumeToken
11511152 require.True(t, sess.detachIfCurrent(ac))
@@ -1153,7 +1154,7 @@ func TestResumeCloseCapturedOldTransportDoesNotCloseReboundTransport(t *testing.
11531154 generation := ac.lifecycle.generationValue()
11541155
11551156 newTr := &closeTrackingTransport{}
1156- resumedSess, resumedAC, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", token), newTr, domain.Size{Cols: 80, Rows: 24})
1157+ resumedSess, resumedAC, ok, err := d.resumeParked(helloResumeCapable(protocol.IntentResume, "work", token), newTr, domain.Size{Cols: 80, Rows: 24})
11571158 require.NoError(t, err)
11581159 require.True(t, ok)
11591160 require.Same(t, sess, resumedSess)
@@ -1171,7 +1172,7 @@ func TestResumeRebasesFullOutputWindowBeforeFirstPaint(t *testing.T) {
11711172 defer release()
11721173 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})
11731174 oldTr := &closeTrackingTransport{}
1174- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)
1175+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
11751176 require.NoError(t, err)
11761177 ac.output.next = maxUnackedOutputStates
11771178 token := ac.resumeToken
@@ -1179,7 +1180,7 @@ func TestResumeRebasesFullOutputWindowBeforeFirstPaint(t *testing.T) {
11791180 require.True(t, d.parkAttachment(sess, ac))
11801181
11811182 newTr := &closeTrackingTransport{}
1182- resumedSess, resumedAC, err := d.route(helloResumeCapable(ports.IntentResume, "work", token), newTr)
1183+ resumedSess, resumedAC, err := d.route(helloResumeCapable(protocol.IntentResume, "work", token), newTr)
11831184 require.NoError(t, err)
11841185 require.Same(t, ac, resumedAC)
11851186 d.paint(resumedSess, resumedAC, true, nil)
@@ -1206,7 +1207,7 @@ func TestParkingReleasesPaneCapturesBeforeHeadlessCloseAndResume(t *testing.T) {
12061207 defer release()
12071208 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})
12081209 oldTransport := &closeTrackingTransport{}
1209- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
1210+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
12101211 require.NoError(t, err)
12111212
12121213 tb := sess.tabs[0]
@@ -1237,7 +1238,7 @@ func TestParkingReleasesPaneCapturesBeforeHeadlessCloseAndResume(t *testing.T) {
12371238 ac.sendMu.Unlock()
12381239
12391240 newTransport := &closeTrackingTransport{}
1240- resumedSess, resumedAC, err := d.route(helloResumeCapable(ports.IntentResume, sess.name, token), newTransport)
1241+ resumedSess, resumedAC, err := d.route(helloResumeCapable(protocol.IntentResume, sess.name, token), newTransport)
12411242 require.NoError(t, err)
12421243 require.Same(t, sess, resumedSess)
12431244 require.Same(t, ac, resumedAC)
@@ -1260,8 +1261,8 @@ func TestExplicitDetachDoesNotPark(t *testing.T) {
12601261 pty, release := newBlockingPTY(t)
12611262 defer release()
12621263 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
1263- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))
1264- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)
1264+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1265+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
12651266 require.NoError(t, err)
12661267 d.clientGone(sess, ac, ac.transport(), true)
12671268 d.mu.Lock()
@@ -1276,8 +1277,8 @@ func TestResumeParkUsesConfiguredGraceAndExpiresOnlyAfterGrace(t *testing.T) {
12761277 defer release()
12771278 d := newTestDaemon(t, newFactory(t, pty), clk)
12781279 WithResumeParkGrace(20 * time.Minute)(d)
1279- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))
1280- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)
1280+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1281+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
12811282 require.NoError(t, err)
12821283 token := ac.resumeToken
12831284
@@ -1303,8 +1304,8 @@ func TestParkExpiryAndShutdownCleanup(t *testing.T) {
13031304 pty, release := newBlockingPTY(t)
13041305 defer release()
13051306 d := newTestDaemon(t, newFactory(t, pty), clk)
1306- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))
1307- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)
1307+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1308+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
13081309 require.NoError(t, err)
13091310 token := ac.resumeToken
13101311 d.clientGone(sess, ac, ac.transport(), false)
@@ -1320,11 +1321,11 @@ func TestParkExpiryAndShutdownCleanup(t *testing.T) {
13201321 pty2, release2 := newBlockingPTY(t)
13211322 defer release2()
13221323 d2 := newTestDaemon(t, newFactory(t, pty2), stubClock{})
1323- tr2, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))
1324- sess2, ac2, err := d2.route(helloResumeCapable(ports.IntentNew, "other", 0), tr2)
1324+ tr2, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1325+ sess2, ac2, err := d2.route(helloResumeCapable(protocol.IntentNew, "other", 0), tr2)
13251326 require.NoError(t, err)
13261327 d2.clientGone(sess2, ac2, ac2.transport(), false)
1327- d2.shutdownAll(ports.ReasonServerShutdown)
1328+ d2.shutdownAll(protocol.ReasonServerShutdown)
13281329 d2.mu.Lock()
13291330 parked := len(d2.parked)
13301331 d2.mu.Unlock()
@@ -1335,8 +1336,8 @@ func TestLiveParkAndResumeRetainsAttachmentWithoutDaemonHistory(t *testing.T) {
13351336 pty, release := newBlockingPTY(t)
13361337 defer release()
13371338 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
1338- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))
1339- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)
1339+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1340+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
13401341 require.NoError(t, err)
13411342
13421343 d.clientGone(sess, ac, ac.transport(), false)
@@ -1347,8 +1348,8 @@ func TestLiveParkAndResumeRetainsAttachmentWithoutDaemonHistory(t *testing.T) {
13471348 d.mu.Unlock()
13481349 require.NotNil(t, parked)
13491350
1350- tr2, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))
1351- resumedSess, resumedAC, err := d.route(helloResumeCapable(ports.IntentResume, "work", token), tr2)
1351+ tr2, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1352+ resumedSess, resumedAC, err := d.route(helloResumeCapable(protocol.IntentResume, "work", token), tr2)
13521353 require.NoError(t, err)
13531354 require.Same(t, sess, resumedSess)
13541355 require.Same(t, ac, resumedAC)
@@ -1377,8 +1378,8 @@ func TestDiscardingParkedAttachmentDoesNotOwnPreviousSession(t *testing.T) {
13771378 pty, release := newBlockingPTY(t)
13781379 defer release()
13791380 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
1380- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))
1381- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)
1381+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1382+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
13821383 require.NoError(t, err)
13831384
13841385 d.clientGone(sess, ac, ac.transport(), false)
@@ -1401,7 +1402,7 @@ func TestEphemeralParkExpiryKeepsSession(t *testing.T) {
14011402 d := newTestDaemon(t, newFactory(t, pty), clk)
14021403
14031404 tr := &closeTrackingTransport{}
1404- sess, ac, err := d.route(helloResumeCapable(ports.IntentEphemeral, "", 0), tr)
1405+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentEphemeral, "", 0), tr)
14051406 require.NoError(t, err)
14061407 token := ac.resumeToken
14071408 require.NotZero(t, token)
@@ -1427,17 +1428,17 @@ func TestKilledSessionPurgesParkedResumeToken(t *testing.T) {
14271428 pty, release := newBlockingPTY(t)
14281429 defer release()
14291430 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
1430- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))
1431- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)
1431+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1432+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
14321433 require.NoError(t, err)
14331434 token := ac.resumeToken
14341435 d.clientGone(sess, ac, ac.transport(), false)
14351436
1436- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))
1437+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
14371438 d.mu.Lock()
14381439 _, parked := d.parked[token]
14391440 d.mu.Unlock()
1440- _, _, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{}, domain.Size{Cols: 80, Rows: 24})
1441+ _, _, ok, err := d.resumeParked(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{}, domain.Size{Cols: 80, Rows: 24})
14411442 require.False(t, parked, "killSession purges parked token")
14421443 require.NoError(t, err)
14431444 require.False(t, ok, "killed session cannot be resumed")
@@ -1458,13 +1459,13 @@ func TestKilledSessionUnblocksParkingWaiterFailsClosed(t *testing.T) {
14581459 d := newTestDaemon(t, newFactorySeq(t, workPTY, otherPTY), stubClock{})
14591460
14601461 oldTr := &closeTrackingTransport{}
1461- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)
1462+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
14621463 require.NoError(t, err)
14631464 token := ac.resumeToken
14641465 require.NotZero(t, token)
14651466
14661467 otherTr := &closeTrackingTransport{}
1467- otherSess, otherAC, err := d.route(helloResumeCapable(ports.IntentNew, "other", 0), otherTr)
1468+ otherSess, otherAC, err := d.route(helloResumeCapable(protocol.IntentNew, "other", 0), otherTr)
14681469 require.NoError(t, err)
14691470 otherToken := d.markParkingInFlight(otherSess, otherAC)
14701471 require.NotZero(t, otherToken)
@@ -1484,11 +1485,11 @@ func TestKilledSessionUnblocksParkingWaiterFailsClosed(t *testing.T) {
14841485 d.afterParkingWaitArmed = func() { close(waiterArmed) }
14851486 waiterDone := make(chan bool, 1)
14861487 go func() {
1487- waiterDone <- d.waitParkingInFlight(helloResumeCapable(ports.IntentResume, "work", token))
1488+ waiterDone <- d.waitParkingInFlight(helloResumeCapable(protocol.IntentResume, "work", token))
14881489 }()
14891490 awaitTestCompletion(t, waiterArmed, "parking waiter did not arm before session kill")
14901491
1491- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))
1492+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
14921493
14931494 waited := awaitTestValue(t, waiterDone, "parking waiter stayed blocked after session kill")
14941495 require.True(t, waited, "waiter must observe the session marker before it is purged")
@@ -1505,11 +1506,11 @@ func TestKilledSessionUnblocksParkingWaiterFailsClosed(t *testing.T) {
15051506 require.True(t, otherRegistered, "other session must survive work kill when PTYs are isolated")
15061507 require.False(t, closing, "daemon must not shut down while the other session remains")
15071508
1508- _, _, err = d.route(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{})
1509+ _, _, err = d.route(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{})
15091510 require.Error(t, err)
15101511 var killedErr *protoErr
15111512 require.ErrorAs(t, err, &killedErr)
1512- require.Equal(t, ports.ErrNoSuchSession, killedErr.code)
1513+ require.Equal(t, protocol.ErrNoSuchSession, killedErr.code)
15131514 require.Contains(t, killedErr.Error(), "resume token is no longer valid")
15141515 }
15151516
@@ -1519,7 +1520,7 @@ func TestStaleClientGoneDoesNotDetachOrCloseFreshTransport(t *testing.T) {
15191520 d := newTestDaemon(t, newFactory(t, p), stubClock{})
15201521
15211522 oldTr := &closeTrackingTransport{}
1522- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)
1523+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
15231524 require.NoError(t, err)
15241525 freshTr := &closeTrackingTransport{}
15251526 ac.replaceTransport(freshTr)
@@ -1557,7 +1558,7 @@ func TestSequencedInputDoesNotPrematurelyEcho(t *testing.T) {
15571558 defer release()
15581559 d := newTestDaemon(t, newFactory(t, p), stubClock{})
15591560 tr := &closeTrackingTransport{}
1560- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)
1561+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
15611562 require.NoError(t, err)
15621563
15631564 d.handleSequencedInput(sess, ac, 42, []byte("x"))
@@ -1587,8 +1588,8 @@ func TestResumeParkedReplacesFuturePTYEnvironment(t *testing.T) {
15871588 },
15881589 ).Maybe()
15891590 d := newTestDaemon(t, factory, stubClock{})
1590- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))
1591- hello := helloResumeCapable(ports.IntentNew, "work", 0)
1591+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1592+ hello := helloResumeCapable(protocol.IntentNew, "work", 0)
15921593 hello.Env = []string{"SECRET=before", "SHELL=/bin/sh", "TERM=old", "VEV=old"}
15931594 sess, ac, err := d.route(hello, tr)
15941595 require.NoError(t, err)
@@ -1596,7 +1597,7 @@ func TestResumeParkedReplacesFuturePTYEnvironment(t *testing.T) {
15961597 require.True(t, sess.detachIfCurrent(ac))
15971598 require.True(t, d.parkAttachment(sess, ac))
15981599
1599- resumeHello := helloResumeCapable(ports.IntentResume, "work", token)
1600+ resumeHello := helloResumeCapable(protocol.IntentResume, "work", token)
16001601 resumeHello.Env = []string{"SECRET=after", "PAIR=a=b", "SHELL=/usr/bin/fish", "TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old"}
16011602 resumeHello.TrueColor = true
16021603 _, _, ok, err := d.resumeParked(resumeHello, &closeTrackingTransport{}, domain.Size{Cols: 80, Rows: 24})
@@ -1619,7 +1620,7 @@ func TestResumeParkedDaemonOwnedPreservesSessionEnvironment(t *testing.T) {
16191620 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
16201621 d.baseEnv = []string{"DAEMON=owned"}
16211622 oldTransport := &closeTrackingTransport{}
1622- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
1623+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
16231624 require.NoError(t, err)
16241625 sess.mu.Lock()
16251626 sess.env = copyEnvironment(d.baseEnv)
@@ -1628,8 +1629,8 @@ func TestResumeParkedDaemonOwnedPreservesSessionEnvironment(t *testing.T) {
16281629 require.True(t, sess.detachIfCurrent(ac))
16291630 require.True(t, d.parkAttachment(sess, ac))
16301631
1631- resume := helloResumeCapable(ports.IntentResume, "work", token)
1632- resume.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned
1632+ resume := helloResumeCapable(protocol.IntentResume, "work", token)
1633+ resume.EnvironmentPolicy = protocol.EnvironmentPolicyDaemonOwned
16331634 resume.Env = []string{"UNTRUSTED=client"}
16341635 _, _, ok, err := d.resumeParked(resume, &closeTrackingTransport{}, defaultSize)
16351636 require.NoError(t, err)
@@ -1638,14 +1639,14 @@ func TestResumeParkedDaemonOwnedPreservesSessionEnvironment(t *testing.T) {
16381639 require.Equal(t, []string{"DAEMON=owned"}, sess.env)
16391640 sess.mu.Unlock()
16401641
1641- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))
1642+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
16421643 }
16431644
16441645 func TestResumeRenegotiatesOutputWindowOnReusedStream(t *testing.T) {
16451646 pty, release := newBlockingPTY(t)
16461647 defer release()
16471648 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})
1648- firstHello := helloResumeCapable(ports.IntentNew, "work", 0)
1649+ firstHello := helloResumeCapable(protocol.IntentNew, "work", 0)
16491650 firstHello.MaxOutputInFlight = 8
16501651 sess, ac, err := d.route(firstHello, &closeTrackingTransport{})
16511652 require.NoError(t, err)
@@ -1655,7 +1656,7 @@ func TestResumeRenegotiatesOutputWindowOnReusedStream(t *testing.T) {
16551656 require.True(t, sess.detachIfCurrent(ac))
16561657 require.True(t, d.parkAttachment(sess, ac))
16571658
1658- resumeOne := helloResumeCapable(ports.IntentResume, "work", token)
1659+ resumeOne := helloResumeCapable(protocol.IntentResume, "work", token)
16591660 resumeOne.MaxOutputInFlight = 1
16601661 _, resumed, err := d.route(resumeOne, &closeTrackingTransport{})
16611662 require.NoError(t, err)
@@ -1665,7 +1666,7 @@ func TestResumeRenegotiatesOutputWindowOnReusedStream(t *testing.T) {
16651666 token = resumed.resumeToken
16661667 require.True(t, sess.detachIfCurrent(resumed))
16671668 require.True(t, d.parkAttachment(sess, resumed))
1668- resumeEight := helloResumeCapable(ports.IntentResume, "work", token)
1669+ resumeEight := helloResumeCapable(protocol.IntentResume, "work", token)
16691670 resumeEight.MaxOutputInFlight = 8
16701671 _, resumed, err = d.route(resumeEight, &closeTrackingTransport{})
16711672 require.NoError(t, err)
M internal/usecase/daemon/resume_wake_incarnation_test.go +3 -3
@@ -6,7 +6,7 @@ import (
66 "github.com/stretchr/testify/require"
77
88 "github.com/bnema/vev/internal/domain"
9- "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
1010 )
1111
1212 // A wake can have passed coordinator scheduling before a link loss parks its
@@ -19,7 +19,7 @@ func TestParkedResumeRejectsDispatchedWakeFromPriorAttachment(t *testing.T) {
1919 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})
2020
2121 oldTransport := &closeTrackingTransport{}
22- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
22+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
2323 require.NoError(t, err)
2424 token := ac.resumeToken
2525 rc := sess.renderCoordinator()
@@ -45,7 +45,7 @@ func TestParkedResumeRejectsDispatchedWakeFromPriorAttachment(t *testing.T) {
4545
4646 d.clientGone(sess, ac, oldTransport, false)
4747 newTransport := &closeTrackingTransport{}
48- resumedSess, resumedAC, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, sess.name, token), newTransport, domain.Size{Cols: 80, Rows: 24})
48+ resumedSess, resumedAC, ok, err := d.resumeParked(helloResumeCapable(protocol.IntentResume, sess.name, token), newTransport, domain.Size{Cols: 80, Rows: 24})
4949 require.NoError(t, err)
5050 require.True(t, ok)
5151 require.Same(t, sess, resumedSess)
M internal/usecase/daemon/route_position_test.go +8 -8
@@ -30,10 +30,10 @@ func TestRouteAppliesPreferredTabPerAttachment(t *testing.T) {
3030 sess.mu.Unlock()
3131 tr := &closeTrackingTransport{}
3232 target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
33- hello := ports.Hello{
34- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: sess.name,
33+ hello := protocol.Hello{
34+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: sess.name,
3535 Size: defaultSize, ExactTarget: &target, PreferredTabID: test.preferred,
36- EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
36+ EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
3737 }
3838
3939 _, ac, err := d.routeWithContext(context.Background(), hello, tr)
@@ -50,7 +50,7 @@ func TestResumeAppliesPreferredTabToParkedAttachment(t *testing.T) {
5050 defer release()
5151 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
5252 oldTransport := &closeTrackingTransport{}
53- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
53+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
5454 require.NoError(t, err)
5555 second := newTabWithStableID("tab-2", "pane-2", newQuietPTY(), domain.Size{Cols: 80, Rows: 22})
5656 sess.mu.Lock()
@@ -60,7 +60,7 @@ func TestResumeAppliesPreferredTabToParkedAttachment(t *testing.T) {
6060 token := ac.resumeToken
6161 d.clientGone(sess, ac, oldTransport, false)
6262
63- hello := helloResumeCapable(ports.IntentResume, "work", token)
63+ hello := helloResumeCapable(protocol.IntentResume, "work", token)
6464 hello.PreferredTabID = "tab-2"
6565 newTransport := &closeTrackingTransport{}
6666 resumedSession, resumed, err := d.route(hello, newTransport)
@@ -80,9 +80,9 @@ func TestPaintPublishesChangedAttachmentRoutePosition(t *testing.T) {
8080 sess.mu.Unlock()
8181 tr := &closeTrackingTransport{}
8282 target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
83- hello := ports.Hello{
84- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: sess.name,
85- Size: defaultSize, ExactTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
83+ hello := protocol.Hello{
84+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: sess.name,
85+ Size: defaultSize, ExactTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
8686 }
8787 _, ac, err := d.routeWithContext(context.Background(), hello, tr)
8888 require.NoError(t, err)
M internal/usecase/daemon/session.go +10 -9
@@ -17,6 +17,7 @@ import (
1717
1818 "github.com/bnema/vev/internal/domain"
1919 "github.com/bnema/vev/internal/ports"
20+ "github.com/bnema/vev/internal/protocol"
2021 "github.com/bnema/vev/internal/usecase/layout"
2122 )
2223
@@ -243,7 +244,7 @@ func (d *Daemon) createSessionLockedWithModeAndInactiveFence(name string, epheme
243244 return nil, domain.UserErr(domain.NoticeSessionSpawn, "couldn't read session catalogue", err)
244245 }
245246 if d.closing {
246- return nil, &protoErr{ports.ErrServerShutdown, "daemon is shutting down"}
247+ return nil, &protoErr{protocol.ErrServerShutdown, "daemon is shutting down"}
247248 }
248249 stopped, resuming = d.inactive[name]
249250 if !inactiveResumeFenceAllows(stopped, resuming, expectedInactive, authoritative, authoritativeExists, validateInactive) {
@@ -498,7 +499,7 @@ func (d *Daemon) createSessionAndSwitch(from *session, ac *attachedClient, name
498499 ready: true,
499500 })
500501 if err != nil {
501- _ = d.killSession(newSess, ports.ReasonSessionKilled, true)
502+ _ = d.killSession(newSess, protocol.ReasonSessionKilled, true)
502503 return err
503504 }
504505
@@ -552,7 +553,7 @@ func (d *Daemon) createSessionAndSwitchForAttachment(effect *attachmentEffect, n
552553 })
553554 if err != nil {
554555 if created != nil {
555- _ = d.killSession(created, ports.ReasonSessionKilled, true)
556+ _ = d.killSession(created, protocol.ReasonSessionKilled, true)
556557 }
557558 return err
558559 }
@@ -590,7 +591,7 @@ func (d *Daemon) createEphemeralSessionAndSwitch(from *session, ac *attachedClie
590591 expectedTransport: ac.transportSnapshot(), ready: true,
591592 })
592593 if err != nil {
593- _ = d.killSession(newSess, ports.ReasonSessionKilled, true)
594+ _ = d.killSession(newSess, protocol.ReasonSessionKilled, true)
594595 return err
595596 }
596597 d.touchMRU(newSess)
@@ -629,7 +630,7 @@ func (d *Daemon) createEphemeralSessionAndSwitchForAttachment(effect *attachment
629630 })
630631 if err != nil {
631632 if created != nil {
632- _ = d.killSession(created, ports.ReasonSessionKilled, true)
633+ _ = d.killSession(created, protocol.ReasonSessionKilled, true)
633634 }
634635 return err
635636 }
@@ -1164,9 +1165,9 @@ func (d *Daemon) closeTabLockedWithEffect(sess *session, tb *tab, repaint bool,
11641165 var err error
11651166 if effect != nil {
11661167 effect.bindActionEnd(d, "close-tab")
1167- err = d.killSessionForAttachment(sess, ports.ReasonSessionKilled, false, effect, "close-tab")
1168+ err = d.killSessionForAttachment(sess, protocol.ReasonSessionKilled, false, effect, "close-tab")
11681169 } else {
1169- err = d.killSession(sess, ports.ReasonSessionKilled, false)
1170+ err = d.killSession(sess, protocol.ReasonSessionKilled, false)
11701171 }
11711172 if err != nil {
11721173 d.log.Warn("closing last tab failed", "session", name, "err", err)
@@ -1645,7 +1646,7 @@ func (d *Daemon) killSessionWithSnapshotDeadlineAndCondition(sess *session, reas
16451646 name := sess.name
16461647 sess.mu.Unlock()
16471648 terminalSnapshotErr = fmt.Errorf("retain final snapshot for session %q: snapshot worker unavailable, saturated, or timed out", name)
1648- if reason != ports.ReasonServerShutdown {
1649+ if reason != protocol.ReasonServerShutdown {
16491650 return terminalSnapshotErr
16501651 }
16511652 // Preserve any already-queued routine capture until the blocked
@@ -1741,7 +1742,7 @@ func (d *Daemon) killSessionWithSnapshotDeadlineAndCondition(sess *session, reas
17411742 unlockSessions()
17421743 d.notices.routingMu.Unlock()
17431744 if !ephemeral {
1744- stopped := inactiveSessionFromRecord(stoppedRecord, ports.SessionDown, nil)
1745+ stopped := inactiveSessionFromRecord(stoppedRecord, protocol.SessionDown, nil)
17451746 stopped.purging = purge
17461747 d.inactive[stoppedName] = stopped
17471748 }
M internal/usecase/daemon/session_core_test.go +2 -2
@@ -5,7 +5,7 @@ import (
55 "time"
66
77 "github.com/bnema/vev/internal/domain"
8- "github.com/bnema/vev/internal/ports"
8+ "github.com/bnema/vev/internal/protocol"
99 "github.com/stretchr/testify/require"
1010 )
1111
@@ -103,7 +103,7 @@ func TestLocalCreateThenKillRemovesLiveRegistryEntry(t *testing.T) {
103103 d.mu.Unlock()
104104 require.Same(t, sess, registered)
105105
106- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))
106+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
107107 d.mu.Lock()
108108 _, live := d.sessions[sess.id]
109109 d.mu.Unlock()
M internal/usecase/daemon/session_purge_retry_test.go +2 -2
@@ -8,8 +8,8 @@ import (
88 "github.com/stretchr/testify/require"
99
1010 "github.com/bnema/vev/internal/domain"
11- "github.com/bnema/vev/internal/ports"
1211 portsmocks "github.com/bnema/vev/internal/ports/mocks"
12+ "github.com/bnema/vev/internal/protocol"
1313 )
1414
1515 type retryablePurgeRepository struct {
@@ -40,7 +40,7 @@ func TestLivePurgeLeavesFailedDirectoryDeletionForStartupGarbageCollection(t *te
4040 pty.EXPECT().Close().Return(nil).Once()
4141 d.sessions = map[domain.SessionID]*session{sess.id: sess}
4242
43- require.Error(t, d.killSession(sess, ports.ReasonSessionKilled, true))
43+ require.Error(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
4444 require.Equal(t, []string{"delete incarnation"}, repository.calls)
4545 _, exists, err := d.catalogue.Record("work")
4646 require.NoError(t, err)
M internal/usecase/daemon/session_test.go +73 -72
@@ -19,6 +19,7 @@ import (
1919 "github.com/bnema/vev/internal/persist"
2020 "github.com/bnema/vev/internal/ports"
2121 portsmocks "github.com/bnema/vev/internal/ports/mocks"
22+ "github.com/bnema/vev/internal/protocol"
2223 "github.com/bnema/vev/internal/usecase/picker"
2324 themeui "github.com/bnema/vev/internal/usecase/theme"
2425 )
@@ -36,7 +37,7 @@ func expectFloatingPrewarmOpen(factory *portsmocks.MockPTYFactory, normalSize do
3637 func TestHandshakeEphemeralHappy(t *testing.T) {
3738 p, releasePTY := newBlockingPTY(t)
3839 d := newTestDaemon(t, newFactory(t, p), stubClock{})
39- tr, sends, releaseConn := newConn(t, mustHello(ports.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))
40+ tr, sends, releaseConn := newConn(t, mustHello(protocol.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))
4041
4142 var hg sync.WaitGroup
4243 hg.Go(func() { d.handleConn(tr) })
@@ -58,7 +59,7 @@ func TestHandshakeEphemeralHappy(t *testing.T) {
5859 func TestHandshakeNewHappy(t *testing.T) {
5960 p, releasePTY := newBlockingPTY(t)
6061 d := newTestDaemon(t, newFactory(t, p), stubClock{})
61- tr, sends, releaseConn := newConn(t, mustHello(ports.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
62+ tr, sends, releaseConn := newConn(t, mustHello(protocol.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
6263
6364 var hg sync.WaitGroup
6465 hg.Go(func() { d.handleConn(tr) })
@@ -73,7 +74,7 @@ func TestHandshakeNewHappy(t *testing.T) {
7374 require.NotNil(t, sess)
7475
7576 releaseConn()
76- _ = d.killSession(sess, ports.ReasonServerShutdown, false)
77+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
7778 releasePTY()
7879 hg.Wait()
7980 d.sessWg.Wait()
@@ -83,8 +84,8 @@ func TestHandshakeNewHappy(t *testing.T) {
8384 func TestHandshakeVersionMismatch(t *testing.T) {
8485 // No Open expectation: the factory must never be asked to spawn a PTY.
8586 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
86- bad := ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(ports.Hello{
87- Version: ports.ProtocolVersion + 99, Intent: ports.IntentEphemeral, Size: domain.Size{Cols: 80, Rows: 24},
87+ bad := ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(protocol.Hello{
88+ Version: protocol.Version + 99, Intent: protocol.IntentEphemeral, Size: domain.Size{Cols: 80, Rows: 24},
8889 })}
8990 tr, sends, _ := newConn(t, bad)
9091
@@ -93,15 +94,15 @@ func TestHandshakeVersionMismatch(t *testing.T) {
9394 e := awaitFrame(t, sends, ports.MsgError)
9495 em, err := ports.UnmarshalErrorMsg(e.Payload)
9596 require.NoError(t, err)
96- require.Equal(t, ports.ErrVersionMismatch, em.Code)
97+ require.Equal(t, protocol.ErrVersionMismatch, em.Code)
9798 require.Equal(t, 0, sessionCount(d))
9899 }
99100
100101 func TestHandshakeOldHelloLayoutReportsVersionMismatch(t *testing.T) {
101102 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
102103 oldLayout := []byte{
103- 0x00, byte(ports.ProtocolVersion - 1),
104- ports.IntentEphemeral,
104+ 0x00, byte(protocol.Version - 1),
105+ protocol.IntentEphemeral,
105106 0x00, 0x00,
106107 0x00, 0x50,
107108 0x00, 0x18,
@@ -113,7 +114,7 @@ func TestHandshakeOldHelloLayoutReportsVersionMismatch(t *testing.T) {
113114 e := awaitFrame(t, sends, ports.MsgError)
114115 em, err := ports.UnmarshalErrorMsg(e.Payload)
115116 require.NoError(t, err)
116- require.Equal(t, ports.ErrVersionMismatch, em.Code)
117+ require.Equal(t, protocol.ErrVersionMismatch, em.Code)
117118 require.Equal(t, 0, sessionCount(d))
118119 }
119120
@@ -122,27 +123,27 @@ func TestHandshakeNameTaken(t *testing.T) {
122123 // Pre-populate a bare session (no goroutines) to collide with.
123124 d.sessions[domain.SessionID("x")] = &session{sessionCore: sessionCore{id: "x", name: "work"}}
124125
125- tr, sends, _ := newConn(t, mustHello(ports.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
126+ tr, sends, _ := newConn(t, mustHello(protocol.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
126127 d.handleConn(tr)
127128
128129 e := awaitFrame(t, sends, ports.MsgError)
129130 em, _ := ports.UnmarshalErrorMsg(e.Payload)
130- require.Equal(t, ports.ErrNameTaken, em.Code)
131+ require.Equal(t, protocol.ErrNameTaken, em.Code)
131132 }
132133
133134 func TestHandshakeNoSuchSession(t *testing.T) {
134135 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
135- tr, sends, _ := newConn(t, mustHello(ports.IntentAttach, "ghost", domain.Size{Cols: 80, Rows: 24}))
136+ tr, sends, _ := newConn(t, mustHello(protocol.IntentAttach, "ghost", domain.Size{Cols: 80, Rows: 24}))
136137 d.handleConn(tr)
137138
138139 e := awaitFrame(t, sends, ports.MsgError)
139140 em, _ := ports.UnmarshalErrorMsg(e.Payload)
140- require.Equal(t, ports.ErrNoSuchSession, em.Code)
141+ require.Equal(t, protocol.ErrNoSuchSession, em.Code)
141142 }
142143
143144 func TestKillAllEmptyDaemonSignalsShutdown(t *testing.T) {
144145 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
145- tr, _, _ := newConn(t, ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(ports.Kill{All: true})})
146+ tr, _, _ := newConn(t, ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(protocol.Kill{All: true})})
146147 d.handleConn(tr)
147148
148149 select {
@@ -157,18 +158,18 @@ func TestCreateTabForAttachmentUsesRequestingAttachmentViewportAndTheme(t *testi
157158 defer release()
158159 d := newTestDaemon(t, newFactory(t, p), stubClock{})
159160 firstTransport := &closeTrackingTransport{}
160- sess, first, err := d.route(ports.Hello{
161- Version: ports.ProtocolVersion,
162- Intent: ports.IntentNew,
161+ sess, first, err := d.route(protocol.Hello{
162+ Version: protocol.Version,
163+ Intent: protocol.IntentNew,
163164 Name: "work",
164165 ClientID: [16]byte{1},
165166 Size: domain.Size{Cols: 80, Rows: 24},
166167 }, firstTransport)
167168 require.NoError(t, err)
168169 secondTransport := &closeTrackingTransport{}
169- _, second, err := d.route(ports.Hello{
170- Version: ports.ProtocolVersion,
171- Intent: ports.IntentAttach,
170+ _, second, err := d.route(protocol.Hello{
171+ Version: protocol.Version,
172+ Intent: protocol.IntentAttach,
172173 Name: "work",
173174 ClientID: [16]byte{2},
174175 Size: domain.Size{Cols: 80, Rows: 24},
@@ -194,7 +195,7 @@ func TestCreateTabForAttachmentUsesRequestingAttachmentViewportAndTheme(t *testi
194195 require.Equal(t, contentSize(domain.Size{Cols: 80, Rows: 24}), gotSize, "sizing must come from the requesting attachment")
195196 assertPaneDefaultColors(t, pane, requesterTheme.Foreground, requesterTheme.Background)
196197
197- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))
198+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
198199 d.sessWg.Wait()
199200 }
200201
@@ -228,7 +229,7 @@ func TestCreateTabClosesPTYIfSessionKilledDuringOpen(t *testing.T) {
228229 ).Once()
229230
230231 d := newTestDaemon(t, f, stubClock{})
231- tr, sends, releaseConn := newConn(t, mustHello(ports.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
232+ tr, sends, releaseConn := newConn(t, mustHello(protocol.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
232233 var hg sync.WaitGroup
233234 hg.Go(func() { d.handleConn(tr) })
234235 awaitFrame(t, sends, ports.MsgWelcome)
@@ -239,7 +240,7 @@ func TestCreateTabClosesPTYIfSessionKilledDuringOpen(t *testing.T) {
239240 errCh := make(chan error, 1)
240241 go func() { errCh <- d.createTab(sess, domain.Size{Cols: 80, Rows: 24}) }()
241242 ctx := <-openCtx
242- _ = d.killSession(sess, ports.ReasonSessionKilled, false)
243+ _ = d.killSession(sess, protocol.ReasonSessionKilled, false)
243244
244245 cancelled := true
245246 select {
@@ -287,8 +288,8 @@ func TestDetachKeepsEphemeralHeadless(t *testing.T) {
287288 p, releasePTY := newBlockingPTY(t)
288289 d := newTestDaemon(t, newFactory(t, p), stubClock{})
289290 tr, sends, _ := newConn(t,
290- mustHello(ports.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}),
291- ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(ports.Detach{})},
291+ mustHello(protocol.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}),
292+ ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(protocol.Detach{})},
292293 )
293294
294295 var hg sync.WaitGroup
@@ -305,7 +306,7 @@ func TestDetachKeepsEphemeralHeadless(t *testing.T) {
305306 require.Empty(t, sess.snapshotAttachmentsLocked(), "ephemeral session is headless after detach")
306307 sess.mu.Unlock()
307308
308- _ = d.killSession(sess, ports.ReasonServerShutdown, false)
309+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
309310 releasePTY()
310311 d.sessWg.Wait()
311312 d.waitNotifies()
@@ -317,7 +318,7 @@ func TestListShowsDetachedEphemeralSession(t *testing.T) {
317318 d := newTestDaemon(t, newFactory(t, p), stubClock{})
318319
319320 tr := &closeTrackingTransport{}
320- sess, ac, err := d.route(helloResumeCapable(ports.IntentEphemeral, "", 0), tr)
321+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentEphemeral, "", 0), tr)
321322 require.NoError(t, err)
322323 d.clientGone(sess, ac, ac.transport(), true)
323324
@@ -327,7 +328,7 @@ func TestListShowsDetachedEphemeralSession(t *testing.T) {
327328 require.True(t, got.Sessions[0].Ephemeral)
328329 require.False(t, got.Sessions[0].Attached)
329330
330- _ = d.killSession(sess, ports.ReasonServerShutdown, false)
331+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
331332 d.sessWg.Wait()
332333 d.waitNotifies()
333334 }
@@ -336,8 +337,8 @@ func TestDetachKeepsNamed(t *testing.T) {
336337 p, releasePTY := newBlockingPTY(t)
337338 d := newTestDaemon(t, newFactory(t, p), stubClock{})
338339 tr, sends, _ := newConn(t,
339- mustHello(ports.IntentNew, "keep", domain.Size{Cols: 80, Rows: 24}),
340- ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(ports.Detach{})},
340+ mustHello(protocol.IntentNew, "keep", domain.Size{Cols: 80, Rows: 24}),
341+ ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(protocol.Detach{})},
341342 )
342343
343344 var hg sync.WaitGroup
@@ -352,7 +353,7 @@ func TestDetachKeepsNamed(t *testing.T) {
352353 require.Empty(t, sess.snapshotAttachmentsLocked(), "named session is headless after detach")
353354 sess.mu.Unlock()
354355
355- _ = d.killSession(sess, ports.ReasonServerShutdown, false)
356+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
356357 releasePTY()
357358 d.sessWg.Wait()
358359 d.waitNotifies()
@@ -368,7 +369,7 @@ func TestReaderEOFRemovesSessionAndSignalsShutdown(t *testing.T) {
368369 p.EXPECT().Pid().Return(1).Maybe()
369370
370371 d := newTestDaemon(t, newFactory(t, p), stubClock{})
371- tr, sends, _ := newConn(t, mustHello(ports.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))
372+ tr, sends, _ := newConn(t, mustHello(protocol.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))
372373
373374 var hg sync.WaitGroup
374375 hg.Go(func() { d.handleConn(tr) })
@@ -376,7 +377,7 @@ func TestReaderEOFRemovesSessionAndSignalsShutdown(t *testing.T) {
376377 // The client is detached with ReasonSessionKilled when the child exits.
377378 det := awaitFrame(t, sends, ports.MsgDetached)
378379 dm, _ := ports.UnmarshalDetached(det.Payload)
379- require.Equal(t, ports.ReasonSessionKilled, dm.Reason)
380+ require.Equal(t, protocol.ReasonSessionKilled, dm.Reason)
380381
381382 select {
382383 case <-d.done:
@@ -394,7 +395,7 @@ func TestReaderEOFRemovesSessionAndSignalsShutdown(t *testing.T) {
394395
395396 func TestServeReturnsWhenLastSessionExits(t *testing.T) {
396397 p, releasePTY := newBlockingPTY(t)
397- tr, sends, _ := newConn(t, mustHello(ports.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))
398+ tr, sends, _ := newConn(t, mustHello(protocol.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))
398399
399400 l := portsmocks.NewMockListener(t)
400401 connCh := make(chan ports.Transport, 1)
@@ -434,7 +435,7 @@ func TestServeReturnsWhenLastSessionExits(t *testing.T) {
434435 func TestServeGracefulShutdownOnContextCancel(t *testing.T) {
435436 p, releasePTY := newBlockingPTY(t)
436437 defer releasePTY()
437- tr, sends, _ := newConn(t, mustHello(ports.IntentNew, "long", domain.Size{Cols: 80, Rows: 24}))
438+ tr, sends, _ := newConn(t, mustHello(protocol.IntentNew, "long", domain.Size{Cols: 80, Rows: 24}))
438439
439440 l := portsmocks.NewMockListener(t)
440441 connCh := make(chan ports.Transport, 1)
@@ -465,7 +466,7 @@ func TestServeGracefulShutdownOnContextCancel(t *testing.T) {
465466
466467 det := awaitFrame(t, sends, ports.MsgDetached)
467468 dm, _ := ports.UnmarshalDetached(det.Payload)
468- require.Equal(t, ports.ReasonServerShutdown, dm.Reason)
469+ require.Equal(t, protocol.ReasonServerShutdown, dm.Reason)
469470
470471 select {
471472 case err := <-served:
@@ -511,7 +512,7 @@ func TestHelloRacingShutdownIsRejected(t *testing.T) {
511512 },
512513 ).Maybe()
513514
514- tr1, sends1, release1 := newConn(t, mustHello(ports.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))
515+ tr1, sends1, release1 := newConn(t, mustHello(protocol.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))
515516 var hg sync.WaitGroup
516517 hg.Go(func() { d.handleConn(tr1) })
517518 awaitFrame(t, sends1, ports.MsgWelcome)
@@ -528,7 +529,7 @@ func TestHelloRacingShutdownIsRejected(t *testing.T) {
528529 require.NotNil(t, sess)
529530
530531 // The last session dies: shutdown begins irreversibly.
531- _ = d.killSession(sess, ports.ReasonSessionKilled, false)
532+ _ = d.killSession(sess, protocol.ReasonSessionKilled, false)
532533 select {
533534 case <-d.done:
534535 default:
@@ -542,15 +543,15 @@ func TestHelloRacingShutdownIsRejected(t *testing.T) {
542543 intent uint8
543544 sess string
544545 }{
545- {"ephemeral", ports.IntentEphemeral, ""},
546- {"named", ports.IntentNew, "latecomer"},
546+ {"ephemeral", protocol.IntentEphemeral, ""},
547+ {"named", protocol.IntentNew, "latecomer"},
547548 } {
548549 tr2, sends2, _ := newConn(t, mustHello(intent.intent, intent.sess, domain.Size{Cols: 80, Rows: 24}))
549550 d.handleConn(tr2)
550551 e := awaitFrame(t, sends2, ports.MsgError)
551552 em, err := ports.UnmarshalErrorMsg(e.Payload)
552553 require.NoError(t, err)
553- require.Equal(t, ports.ErrServerShutdown, em.Code, "%s hello racing shutdown must be rejected", intent.name)
554+ require.Equal(t, protocol.ErrServerShutdown, em.Code, "%s hello racing shutdown must be rejected", intent.name)
554555 }
555556 require.Equal(t, 0, sessionCount(d), "no session may be inserted after shutdown began")
556557 require.Zero(t, opensAfterShutdown.Load(), "no PTYFactory.Open may start after d.done/shutdown completion")
@@ -574,7 +575,7 @@ func TestServeReturnsDespiteWedgedClientOnShutdown(t *testing.T) {
574575 tr := portsmocks.NewMockTransport(t)
575576 sends := make(chan ports.Frame, 64)
576577 recvCh := make(chan ports.Frame, 1)
577- recvCh <- mustHello(ports.IntentNew, "wedge", domain.Size{Cols: 80, Rows: 24})
578+ recvCh <- mustHello(protocol.IntentNew, "wedge", domain.Size{Cols: 80, Rows: 24})
578579 connDone := make(chan struct{})
579580 var connOnce sync.Once
580581 closeConn := func() { connOnce.Do(func() { close(connDone) }) }
@@ -779,7 +780,7 @@ func TestDaemonLoadsPersistedSessionsAsStopped(t *testing.T) {
779780 require.Equal(t, int64(7), stopped.createdAt)
780781 require.Equal(t, incarnation, stopped.incarnation)
781782 require.Equal(t, uint64(9), stopped.lastUsedSeq)
782- require.Equal(t, ports.SessionDown, stopped.state)
783+ require.Equal(t, protocol.SessionDown, stopped.state)
783784 require.NotNil(t, stopped.restoreDone)
784785 require.Equal(t, uint64(9), d.mruSeq.Load())
785786 }
@@ -905,7 +906,7 @@ func TestCreateRenameKillPersistenceLifecycle(t *testing.T) {
905906 require.Equal(t, created.IncarnationID, renamed.IncarnationID)
906907 require.NoError(t, renamed.Validate())
907908
908- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))
909+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
909910 require.False(t, state.has("renamed"))
910911 }
911912
@@ -1018,12 +1019,12 @@ func TestAttachRestoresPersistedTabNames(t *testing.T) {
10181019 record, ok, err := d.catalogueRecord("work")
10191020 require.NoError(t, err)
10201021 require.True(t, ok)
1021- d.inactive["work"] = inactiveSessionFromRecord(record, ports.SessionDown, nil)
1022+ d.inactive["work"] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
10221023 tr := portsmocks.NewMockTransport(t)
10231024 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
10241025 tr.EXPECT().Close().Return(nil).Maybe()
10251026
1026- sess, ac, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: sz}, tr)
1027+ sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: sz}, tr)
10271028 require.NoError(t, err)
10281029 require.NotNil(t, ac)
10291030 require.Len(t, sess.tabs, 2)
@@ -1088,7 +1089,7 @@ func TestEphemeralPromotionLifecyclePreventsStaleSameNamePaletteTarget(t *testin
10881089 require.NoError(t, d.renameSession(first, "named"))
10891090 staleCreatedAt := first.createdAt
10901091 require.NotZero(t, staleCreatedAt)
1091- require.NoError(t, d.killSession(first, ports.ReasonSessionKilled, true))
1092+ require.NoError(t, d.killSession(first, protocol.ReasonSessionKilled, true))
10921093 d.sessWg.Wait()
10931094
10941095 second, err := createSessionForTest(d, "0", true, "/tmp", ac.size, terminalEnv{}, d.baseEnv)
@@ -1192,12 +1193,12 @@ func TestAttachResumesStoppedSessionFromStoredCwd(t *testing.T) {
11921193 d := newTestDaemon(t, newFactory(t, p), stubClock{})
11931194 WithStore(t, store)(d)
11941195 cwd := t.TempDir()
1195- d.inactive["work"] = inactiveSession{name: "work", cwd: cwd, createdAt: 1, state: ports.SessionDown}
1196+ d.inactive["work"] = inactiveSession{name: "work", cwd: cwd, createdAt: 1, state: protocol.SessionDown}
11961197 tr := portsmocks.NewMockTransport(t)
11971198 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
11981199 tr.EXPECT().Close().Return(nil).Maybe()
11991200
1200- sess, ac, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: sz}, tr)
1201+ sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: sz}, tr)
12011202 require.NoError(t, err)
12021203 require.NotNil(t, ac)
12031204 require.Equal(t, "work", sess.name)
@@ -1215,12 +1216,12 @@ func TestAttachStoppedMissingCwdFallsBackToHome(t *testing.T) {
12151216 store, _ := newMockStore(t)
12161217 d := newTestDaemon(t, newFactory(t, p), stubClock{})
12171218 WithStore(t, store)(d)
1218- d.inactive["work"] = inactiveSession{name: "work", cwd: "/definitely/missing/vev", createdAt: 1, state: ports.SessionDown}
1219+ d.inactive["work"] = inactiveSession{name: "work", cwd: "/definitely/missing/vev", createdAt: 1, state: protocol.SessionDown}
12191220 tr := portsmocks.NewMockTransport(t)
12201221 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
12211222 tr.EXPECT().Close().Return(nil).Maybe()
12221223
1223- sess, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: sz}, tr)
1224+ sess, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: sz}, tr)
12241225 require.NoError(t, err)
12251226 require.NotEqual(t, "/definitely/missing/vev", sess.cwd)
12261227 }
@@ -1266,7 +1267,7 @@ func TestNewSessionAssignsStableIDsAndChildEnv(t *testing.T) {
12661267 sess, err := createSessionForTest(d, "work", false, "/tmp/work", sz, terminalEnv{TrueColor: true}, d.baseEnv)
12671268 require.NoError(t, err)
12681269 defer func() {
1269- _ = d.killSession(sess, ports.ReasonServerShutdown, false)
1270+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
12701271 releasePTY()
12711272 d.sessWg.Wait()
12721273 }()
@@ -1298,14 +1299,14 @@ func TestIntentNewStoppedNameRejected(t *testing.T) {
12981299 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
12991300 d.inactive["taken"] = inactiveSession{name: "taken", cwd: "/tmp", createdAt: 1}
13001301 tr := portsmocks.NewMockTransport(t)
1301- _, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "taken", Size: domain.Size{Cols: 80, Rows: 24}}, tr)
1302+ _, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "taken", Size: domain.Size{Cols: 80, Rows: 24}}, tr)
13021303 require.ErrorContains(t, err, "name already in use")
13031304 }
13041305
13051306 func TestIntentNewUnsafeNameRejected(t *testing.T) {
13061307 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
13071308 tr := portsmocks.NewMockTransport(t)
1308- _, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "my work", Size: domain.Size{Cols: 80, Rows: 24}}, tr)
1309+ _, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "my work", Size: domain.Size{Cols: 80, Rows: 24}}, tr)
13091310 require.ErrorContains(t, err, domain.ErrInvalidSessionName.Error())
13101311 }
13111312
@@ -1363,14 +1364,14 @@ func TestNaturalExitStoppedButExplicitKillPurges(t *testing.T) {
13631364 require.NoError(t, err)
13641365 other, err := createSessionForTest(d, "other", false, "/tmp/other", sz, terminalEnv{}, d.baseEnv)
13651366 require.NoError(t, err)
1366- _ = d.killSession(natural, ports.ReasonSessionKilled, false)
1367+ _ = d.killSession(natural, protocol.ReasonSessionKilled, false)
13671368 require.True(t, state.has("natural"))
13681369 d.mu.Lock()
13691370 stopped := d.inactive["natural"]
13701371 d.mu.Unlock()
13711372 require.Equal(t, "/tmp/latest", stopped.cwd)
13721373
1373- _ = d.killSession(other, ports.ReasonSessionKilled, true)
1374+ _ = d.killSession(other, protocol.ReasonSessionKilled, true)
13741375 require.False(t, state.has("other"))
13751376 }
13761377
@@ -1447,10 +1448,10 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) {
14471448 tr := portsmocks.NewMockTransport(t)
14481449 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
14491450 tr.EXPECT().Close().Return(nil).Maybe()
1450- sess, ac, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)
1451+ sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)
14511452 require.NoError(t, err)
14521453 defer func() {
1453- _ = d.killSession(sess, ports.ReasonServerShutdown, false)
1454+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
14541455 d.sessWg.Wait()
14551456 }()
14561457
@@ -1492,12 +1493,12 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) {
14921493 tr2 := portsmocks.NewMockTransport(t)
14931494 tr2.EXPECT().Send(mock.Anything).Return(nil).Maybe()
14941495 tr2.EXPECT().Close().Return(nil).Maybe()
1495- sess, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: sz, TrueColor: false}, tr1)
1496+ sess, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: false}, tr1)
14961497 require.NoError(t, err)
1497- _, ac, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: sz, TrueColor: true}, tr2)
1498+ _, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: sz, TrueColor: true}, tr2)
14981499 require.NoError(t, err)
14991500 defer func() {
1500- _ = d.killSession(sess, ports.ReasonServerShutdown, false)
1501+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
15011502 d.sessWg.Wait()
15021503 }()
15031504
@@ -1533,7 +1534,7 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) {
15331534 tr := portsmocks.NewMockTransport(t)
15341535 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
15351536 tr.EXPECT().Close().Return(nil).Maybe()
1536- sess, ac, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)
1537+ sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)
15371538 require.NoError(t, err)
15381539 // The source coordinator is deliberately made pending so the switch must
15391540 // invalidate it rather than letting its stale callback render for ac.
@@ -1569,7 +1570,7 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) {
15691570 require.Contains(t, opens[1], "TERM=xterm-256color")
15701571 require.Contains(t, opens[1], "COLORTERM=truecolor")
15711572 require.Contains(t, opens[1], "TERM_PROGRAM=vev")
1572- _ = d.killSession(got, ports.ReasonSessionKilled, false)
1573+ _ = d.killSession(got, protocol.ReasonSessionKilled, false)
15731574 release1()
15741575 release2()
15751576 d.sessWg.Wait()
@@ -1607,13 +1608,13 @@ func TestAttachEnvironmentReplacesFuturePTYInputs(t *testing.T) {
16071608 tr2.EXPECT().Close().Return(nil).Maybe()
16081609
16091610 first := []string{"SECRET=first", "TERM=bad", "TERM_PROGRAM_extra=keep", "SHELL=/usr/bin/fish", "A=a=b"}
1610- sess, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: sz, Env: first}, tr1)
1611+ sess, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, Env: first}, tr1)
16111612 require.NoError(t, err)
16121613 second := []string{"SECRET=second", "TERM=bad", "TERM_PROGRAM_extra=keep", "SHELL=/bin/bash", "A=a=b"}
1613- _, ac, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: sz, Env: second}, tr2)
1614+ _, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: sz, Env: second}, tr2)
16141615 require.NoError(t, err)
16151616 defer func() {
1616- _ = d.killSession(sess, ports.ReasonServerShutdown, false)
1617+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
16171618 d.sessWg.Wait()
16181619 }()
16191620
@@ -1702,7 +1703,7 @@ func TestNamedSessionLifecycleExhaustionDoesNotMutateSessionState(t *testing.T)
17021703 require.Equal(t, int64(math.MaxInt64), d.lastAllocatedCreatedAt)
17031704 require.Equal(t, inactiveSession{name: "retained", cwd: "/tmp", createdAt: 9}, d.inactive["retained"])
17041705
1705- _, _, err = d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "routed", Size: domain.Size{Cols: 80, Rows: 24}}, nil)
1706+ _, _, err = d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "routed", Size: domain.Size{Cols: 80, Rows: 24}}, nil)
17061707 require.ErrorContains(t, err, "lifecycle identities exhausted")
17071708 require.Empty(t, d.sessions)
17081709 require.Equal(t, uint64(17), d.nextID)
@@ -1721,7 +1722,7 @@ func TestCatalogueRecordsConstructExpectedSessionRegistry(t *testing.T) {
17211722
17221723 alpha := d.inactive["alpha"]
17231724 require.Equal(t, records[0], alpha.record)
1724- require.Equal(t, ports.SessionDown, alpha.state)
1725+ require.Equal(t, protocol.SessionDown, alpha.state)
17251726 require.NotNil(t, alpha.restoreDone)
17261727 select {
17271728 case <-alpha.restoreDone:
@@ -1730,7 +1731,7 @@ func TestCatalogueRecordsConstructExpectedSessionRegistry(t *testing.T) {
17301731 }
17311732 work := d.inactive["work"]
17321733 require.Equal(t, records[1], work.record)
1733- require.Equal(t, ports.SessionBroken, work.state)
1734+ require.Equal(t, protocol.SessionBroken, work.state)
17341735 require.NotNil(t, work.restoreDone)
17351736 select {
17361737 case <-work.restoreDone:
@@ -1768,7 +1769,7 @@ func TestResumingStoppedSessionPreservesLifecycleIdentityInPersistence(t *testin
17681769 store, state := newMockStore(t)
17691770 WithStore(t, store)(d)
17701771 d.ptys = newFactory(t, targetPTY)
1771- d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 77, state: ports.SessionDown}
1772+ d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 77, state: protocol.SessionDown}
17721773
17731774 require.True(t, d.resumeStoppedAndSwitch(from, ac, picker.Target{Name: "stopped", Stopped: true}))
17741775 resumed := ac.currentSession()
@@ -1796,7 +1797,7 @@ func TestLifecycleExpectedTargetChecksAreAtomicAcrossStateTransitions(t *testing
17961797 d, from, ac, _ := newManualSessionWithPTYs(t, fromPTY)
17971798 d.ptys = newFactory(t, targetPTY)
17981799 expected := int64(31)
1799- d.inactive["target"] = inactiveSession{name: "target", cwd: "/tmp", createdAt: expected, state: ports.SessionDown}
1800+ d.inactive["target"] = inactiveSession{name: "target", cwd: "/tmp", createdAt: expected, state: protocol.SessionDown}
18001801
18011802 require.NoError(t, d.switchToTarget(from, ac, picker.Target{Session: "old-active-id", Name: "target", TabIndex: 0, ExpectedCreatedAt: &expected}))
18021803 require.Equal(t, "target", ac.currentSession().name)
@@ -1818,7 +1819,7 @@ func TestLifecycleExpectedTargetChecksAreAtomicAcrossStateTransitions(t *testing
18181819 fromPTY, releaseFrom := newBlockingPTY(t)
18191820 defer releaseFrom()
18201821 d, from, ac, _ := newManualSessionWithPTYs(t, fromPTY)
1821- d.inactive["target"] = inactiveSession{name: "target", cwd: "/tmp", createdAt: 52, state: ports.SessionDown}
1822+ d.inactive["target"] = inactiveSession{name: "target", cwd: "/tmp", createdAt: 52, state: protocol.SessionDown}
18221823 expected := int64(51)
18231824
18241825 require.Error(t, d.switchToTarget(from, ac, picker.Target{Name: "target", TabIndex: 0, Stopped: true, ExpectedCreatedAt: &expected}))
M internal/usecase/daemon/shared_pty_geometry_test.go +5 -5
@@ -4,7 +4,7 @@ import (
44 "testing"
55
66 "github.com/bnema/vev/internal/domain"
7- "github.com/bnema/vev/internal/ports"
7+ "github.com/bnema/vev/internal/protocol"
88 "github.com/stretchr/testify/require"
99 )
1010
@@ -45,14 +45,14 @@ func TestSharedPTYGeometryCharacterization(t *testing.T) {
4545 t.Cleanup(releasePTY)
4646 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
4747 firstTransport, _ := newCapturingTransport(t)
48- sess, first, err := d.route(ports.Hello{
49- Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work",
48+ sess, first, err := d.route(protocol.Hello{
49+ Version: protocol.Version, Intent: protocol.IntentNew, Name: "work",
5050 Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1},
5151 }, firstTransport)
5252 require.NoError(t, err)
5353 secondTransport, _ := newCapturingTransport(t)
54- _, second, err := d.route(ports.Hello{
55- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work",
54+ _, second, err := d.route(protocol.Hello{
55+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work",
5656 Size: domain.Size{Cols: 100, Rows: 40}, ClientID: [16]byte{2},
5757 }, secondTransport)
5858 require.NoError(t, err)
M internal/usecase/daemon/snapshot_restore.go +2 -2
@@ -9,7 +9,7 @@ import (
99
1010 vt "github.com/bnema/vev-vt"
1111 "github.com/bnema/vev/internal/domain"
12- "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
1313 "github.com/bnema/vev/internal/usecase/layout"
1414 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"
1515 )
@@ -25,7 +25,7 @@ func (d *Daemon) closeRestoreDone() {
2525 continue
2626 default:
2727 }
28- entry.state = ports.SessionBroken
28+ entry.state = protocol.SessionBroken
2929 if entry.record.DegradedReason == "" {
3030 entry.record.DegradedReason = "restore unavailable"
3131 }
M internal/usecase/daemon/snapshot_restore_incremental.go +11 -10
@@ -12,6 +12,7 @@ import (
1212
1313 "github.com/bnema/vev/internal/domain"
1414 "github.com/bnema/vev/internal/ports"
15+ "github.com/bnema/vev/internal/protocol"
1516 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"
1617 )
1718
@@ -19,11 +20,11 @@ const catalogueRestoreConcurrency = 8
1920
2021 var errRetryableRestoreLoad = errors.New("snapshot: retryable checkpoint load")
2122
22-func initialSessionState(record domain.CatalogueRecord) (ports.SessionState, chan struct{}) {
23+func initialSessionState(record domain.CatalogueRecord) (protocol.SessionState, chan struct{}) {
2324 done := make(chan struct{})
24- state := ports.SessionDown
25+ state := protocol.SessionDown
2526 if record.DegradedReason != "" {
26- state = ports.SessionBroken
27+ state = protocol.SessionBroken
2728 close(done)
2829 } else if record.Committed == nil {
2930 close(done)
@@ -68,7 +69,7 @@ func (d *Daemon) restoreCatalogue(ctx context.Context, records []domain.Catalogu
6869 wg.Wait()
6970 }
7071
71-func inactiveSessionFromRecord(record domain.CatalogueRecord, state ports.SessionState, done chan struct{}) inactiveSession {
72+func inactiveSessionFromRecord(record domain.CatalogueRecord, state protocol.SessionState, done chan struct{}) inactiveSession {
7273 tabRecords := append([]domain.CatalogueTabRecord(nil), record.TabRecords...)
7374 tabNames := append([]string(nil), record.TabNames...)
7475 if len(tabRecords) != 0 {
@@ -123,7 +124,7 @@ func closeRuntimeRestoreDoneLocked(done chan struct{}) {
123124 }
124125 }
125126
126-func (d *Daemon) setStoppedRecovery(record domain.CatalogueRecord, state ports.SessionState) {
127+func (d *Daemon) setStoppedRecovery(record domain.CatalogueRecord, state protocol.SessionState) {
127128 d.mu.Lock()
128129 defer d.mu.Unlock()
129130 entry, ok := d.inactive[record.Name]
@@ -156,7 +157,7 @@ func (d *Daemon) finishRecordRestore(record domain.CatalogueRecord, restoreErr e
156157
157158 d.mu.Lock()
158159 if entry, ok := d.inactive[record.Name]; ok && degraded {
159- d.inactive[record.Name] = inactiveSessionFromRecord(record, ports.SessionBroken, entry.restoreDone)
160+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionBroken, entry.restoreDone)
160161 }
161162 closeRuntimeRestoreDoneLocked(done)
162163 d.mu.Unlock()
@@ -174,12 +175,12 @@ func (d *Daemon) finishRecordRestore(record domain.CatalogueRecord, restoreErr e
174175
175176 func (d *Daemon) restoreRecord(ctx context.Context, record domain.CatalogueRecord) error {
176177 if record.DegradedReason != "" {
177- d.setStoppedRecovery(record, ports.SessionBroken)
178+ d.setStoppedRecovery(record, protocol.SessionBroken)
178179 d.logSessionDegraded(record, "persisted-broken")
179180 return nil
180181 }
181182 if record.Committed == nil {
182- d.setStoppedRecovery(record, ports.SessionDown)
183+ d.setStoppedRecovery(record, protocol.SessionDown)
183184 d.logSessionRestoreComplete(record, 0, false)
184185 return nil
185186 }
@@ -231,7 +232,7 @@ func (d *Daemon) restoreRecord(ctx context.Context, record domain.CatalogueRecor
231232 if err := d.restoreSession(ctx, selectedSnapshot, selectedGeneration.Generation, selected); err != nil {
232233 return err
233234 }
234- d.setStoppedRecovery(record, ports.SessionDown)
235+ d.setStoppedRecovery(record, protocol.SessionDown)
235236 d.logSessionRestoreComplete(record, selected.Generation, false)
236237 return nil
237238 }
@@ -252,7 +253,7 @@ func (d *Daemon) resetIncompatibleCheckpoint(ctx context.Context, record domain.
252253
253254 d.mu.Lock()
254255 if entry, ok := d.inactive[record.Name]; ok {
255- d.inactive[record.Name] = inactiveSessionFromRecord(fresh, ports.SessionDown, entry.restoreDone)
256+ d.inactive[record.Name] = inactiveSessionFromRecord(fresh, protocol.SessionDown, entry.restoreDone)
256257 }
257258 d.mu.Unlock()
258259
M internal/usecase/daemon/snapshot_resume_publication_test.go +8 -7
@@ -12,6 +12,7 @@ import (
1212
1313 "github.com/bnema/vev/internal/domain"
1414 "github.com/bnema/vev/internal/ports"
15+ "github.com/bnema/vev/internal/protocol"
1516 )
1617
1718 type snapshotLifecycleRepository struct {
@@ -159,9 +160,9 @@ func TestStoppedNamedSessionResumePublishesNextCheckpointInSameDaemon(t *testing
159160 d.sessWg.Wait()
160161 })
161162
162- _, _, err := d.route(snapshotLifecycleHello(ports.IntentEphemeral, ""), &closeTrackingTransport{})
163+ _, _, err := d.route(snapshotLifecycleHello(protocol.IntentEphemeral, ""), &closeTrackingTransport{})
163164 require.NoError(t, err, "keeper session setup failed")
164- sess, _, err := d.route(snapshotLifecycleHello(ports.IntentNew, "work"), &closeTrackingTransport{})
165+ sess, _, err := d.route(snapshotLifecycleHello(protocol.IntentNew, "work"), &closeTrackingTransport{})
165166 require.NoError(t, err)
166167
167168 testAttachmentTab(sess).focusedPane().screen.Write([]byte("first checkpoint"))
@@ -172,7 +173,7 @@ func TestStoppedNamedSessionResumePublishesNextCheckpointInSameDaemon(t *testing
172173 require.NotNil(t, firstRecord.Committed)
173174 require.Equal(t, uint64(1), firstRecord.Committed.Generation)
174175
175- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))
176+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
176177 d.mu.Lock()
177178 stopped, retained := d.inactive[sess.name]
178179 d.mu.Unlock()
@@ -183,7 +184,7 @@ func TestStoppedNamedSessionResumePublishesNextCheckpointInSameDaemon(t *testing
183184 require.Equal(t, firstRecord.Committed.Generation+1, closedRecord.Committed.Generation,
184185 "terminal checkpoint must be authoritative before same-daemon resume")
185186
186- resumed, _, err := d.route(snapshotLifecycleHello(ports.IntentAttach, sess.name), &closeTrackingTransport{})
187+ resumed, _, err := d.route(snapshotLifecycleHello(protocol.IntentAttach, sess.name), &closeTrackingTransport{})
187188 require.NoError(t, err)
188189 require.NotSame(t, sess, resumed, "stopped-session attach must create a new live runtime")
189190 require.Equal(t, sess.incarnation, resumed.incarnation, "resume must retain durable incarnation authority")
@@ -211,9 +212,9 @@ func TestStoppedNamedSessionResumePublishesNextCheckpointInSameDaemon(t *testing
211212 "same-daemon resume must use the committed terminal checkpoint as parent")
212213 }
213214
214-func snapshotLifecycleHello(intent uint8, name string) ports.Hello {
215- return ports.Hello{
216- Version: ports.ProtocolVersion,
215+func snapshotLifecycleHello(intent uint8, name string) protocol.Hello {
216+ return protocol.Hello{
217+ Version: protocol.Version,
217218 Intent: intent,
218219 Name: name,
219220 Cwd: "/tmp",
M internal/usecase/daemon/snapshot_scheduler_test.go +7 -6
@@ -14,6 +14,7 @@ import (
1414 "github.com/bnema/vev/internal/domain"
1515 "github.com/bnema/vev/internal/ports"
1616 portsmocks "github.com/bnema/vev/internal/ports/mocks"
17+ "github.com/bnema/vev/internal/protocol"
1718 )
1819
1920 func TestSnapshotCoordinatorQuarantineCancelsPublicationBeforeDelete(t *testing.T) {
@@ -247,7 +248,7 @@ func TestForcedSnapshotShutdownTimeoutRetainsRetryableStateAndNotice(t *testing.
247248 require.True(t, d.scheduleSnapshot(sess), "routine capture should occupy the bounded queue")
248249
249250 killed := make(chan error, 1)
250- go func() { killed <- d.killSession(sess, ports.ReasonServerShutdown, false) }()
251+ go func() { killed <- d.killSession(sess, protocol.ReasonServerShutdown, false) }()
251252 timer := clock.nextTimer(t)
252253 timer.fire()
253254 require.Error(t, <-killed)
@@ -303,7 +304,7 @@ func TestServeShutdownDeadlineStillJoinsPaneExitWriter(t *testing.T) {
303304 return nil
304305 }).Once()
305306
306- tr, sends, _ := newConn(t, mustHello(ports.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
307+ tr, sends, _ := newConn(t, mustHello(protocol.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
307308 listener := serveSnapshotListener(t, tr)
308309 ctx, cancel := context.WithCancel(t.Context())
309310 defer cancel()
@@ -402,7 +403,7 @@ func TestConcurrentSessionTeardownPublishesOneFinalSnapshot(t *testing.T) {
402403 d.sessions[sess.id] = sess
403404
404405 first := make(chan error, 1)
405- go func() { first <- d.killSession(sess, ports.ReasonSessionKilled, false) }()
406+ go func() { first <- d.killSession(sess, protocol.ReasonSessionKilled, false) }()
406407 select {
407408 case <-publicationStarted:
408409 case <-time.After(testWaitTimeout):
@@ -418,7 +419,7 @@ func TestConcurrentSessionTeardownPublishesOneFinalSnapshot(t *testing.T) {
418419 duplicates := make(chan error, duplicateCallers)
419420 for range duplicateCallers {
420421 go func() {
421- duplicates <- d.killSession(sess, ports.ReasonSessionKilled, false)
422+ duplicates <- d.killSession(sess, protocol.ReasonSessionKilled, false)
422423 }()
423424 }
424425 // Synchronize on the ownership state itself: every duplicate is waiting
@@ -468,7 +469,7 @@ func TestServeShutdownCheckpointsBeforeStoppingSnapshotWorker(t *testing.T) {
468469 return nil
469470 }).Twice()
470471
471- tr, sends, _ := newConn(t, mustHello(ports.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
472+ tr, sends, _ := newConn(t, mustHello(protocol.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
472473 l := serveSnapshotListener(t, tr)
473474 ctx, cancel := context.WithCancel(t.Context())
474475 defer cancel()
@@ -525,7 +526,7 @@ func TestServeShutdownDeadlineStillJoinsUncooperativeSnapshotRepository(t *testi
525526 return nil
526527 }).Twice()
527528
528- tr, sends, _ := newConn(t, mustHello(ports.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
529+ tr, sends, _ := newConn(t, mustHello(protocol.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
529530 l := serveSnapshotListener(t, tr)
530531 ctx, cancel := context.WithCancel(t.Context())
531532 defer cancel()
M internal/usecase/daemon/status_test.go +1 -1
@@ -1441,7 +1441,7 @@ func TestStatusCoalescesCreateSwitchAndResize(t *testing.T) {
14411441 clock := newCoordinatorMockClock(t, 64)
14421442 d := newTestDaemon(t, newFactorySeq(t, p1, p2), clock.clock)
14431443 tr, sends, releaseConn := newConn(t,
1444- mustHello(ports.IntentNew, "work", domain.Size{Cols: 20, Rows: 5}),
1444+ mustHello(protocol.IntentNew, "work", domain.Size{Cols: 20, Rows: 5}),
14451445 frameInput([]byte("\x1b ")),
14461446 frameInput([]byte("CNT\r")),
14471447 frameInput([]byte("\x1b1")),
M internal/usecase/daemon/wire.go +7 -7
@@ -16,11 +16,11 @@ func frameWelcome(s *session, resumeToken uint64) (ports.Frame, error) {
1616 Ephemeral: s.ephemeral,
1717 }
1818 }
19- w := ports.Welcome{
19+ w := protocol.Welcome{
2020 SessionID: string(s.id),
2121 SessionName: s.name,
2222 Ephemeral: s.ephemeral,
23- Capabilities: ports.CapabilityResume,
23+ Capabilities: protocol.CapabilityResume,
2424 CommittedIdentity: identity,
2525 }
2626 s.mu.Unlock()
@@ -33,7 +33,7 @@ func frameWelcome(s *session, resumeToken uint64) (ports.Frame, error) {
3333 }
3434
3535 func frameError(code uint16, text string) ports.Frame {
36- return ports.Frame{Type: ports.MsgError, Payload: ports.MarshalErrorMsg(ports.ErrorMsg{Code: code, Text: text})}
36+ return ports.Frame{Type: ports.MsgError, Payload: ports.MarshalErrorMsg(protocol.ErrorMsg{Code: code, Text: text})}
3737 }
3838
3939 func frameOutputState(b []byte, baseState uint64, state uint64, echoAck uint64) (ports.Frame, error) {
@@ -48,13 +48,13 @@ func frameOutputState(b []byte, baseState uint64, state uint64, echoAck uint64)
4848 }
4949
5050 func frameDetached(reason uint8) ports.Frame {
51- return ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(ports.Detached{Reason: reason})}
51+ return ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(protocol.Detached{Reason: reason})}
5252 }
5353
5454 func framePong() ports.Frame {
55- return ports.Frame{Type: ports.MsgPong, Payload: ports.MarshalPong(ports.Pong{})}
55+ return ports.Frame{Type: ports.MsgPong, Payload: ports.MarshalPong(protocol.Pong{})}
5656 }
5757
58-func frameSessions(infos []ports.SessionInfo) ports.Frame {
59- return ports.Frame{Type: ports.MsgSessions, Payload: ports.MarshalSessions(ports.Sessions{Sessions: infos})}
58+func frameSessions(infos []protocol.SessionInfo) ports.Frame {
59+ return ports.Frame{Type: ports.MsgSessions, Payload: ports.MarshalSessions(protocol.Sessions{Sessions: infos})}
6060 }
M scripts/remote-picker-harness/main.go +25 -25
@@ -86,8 +86,8 @@ func runLiveStdioPhase(ctx context.Context, target string, catalog ports.RemoteC
8686 return fmt.Errorf("open stdio transport: %w", err)
8787 }
8888 defer func() { _ = stdio.Close() }()
89- if err := attachAndCheck(ctx, stdio, ports.Hello{
90- Intent: ports.IntentNew, Name: sessionName, Env: localEnvironment(), Cwd: "/tmp/local-cwd",
89+ if err := attachAndCheck(ctx, stdio, protocol.Hello{
90+ Intent: protocol.IntentNew, Name: sessionName, Env: localEnvironment(), Cwd: "/tmp/local-cwd",
9191 TermEnv: "xterm-256color", TrueColor: true,
9292 }); err != nil {
9393 return fmt.Errorf("stdio live attach: %w", err)
@@ -177,8 +177,8 @@ func runLocalPickerUnitedPhase(ctx context.Context, target string, catalog ports
177177 err = errors.Join(err, fmt.Errorf("destroy remote picker session: %w", cleanupErr))
178178 }
179179 }()
180- if err := attachAndCheck(ctx, remote, ports.Hello{
181- Intent: ports.IntentNew, Name: remoteSession, Env: localEnvironment(), Cwd: "/tmp/remote-cwd",
180+ if err := attachAndCheck(ctx, remote, protocol.Hello{
181+ Intent: protocol.IntentNew, Name: remoteSession, Env: localEnvironment(), Cwd: "/tmp/remote-cwd",
182182 TermEnv: "xterm-256color", TrueColor: true,
183183 }); err != nil {
184184 return fmt.Errorf("attach remote picker source: %w", err)
@@ -247,8 +247,8 @@ func runLocalPickerUnitedPhase(ctx context.Context, target string, catalog ports
247247 // or MsgAttachTarget to this transport.
248248 local.probe.configure("local-picker")
249249 defer func() { _ = local.Close() }()
250- if err := attachAndCheck(ctx, local, ports.Hello{
251- Intent: ports.IntentNew, Name: localSession, Env: localEnvironment(), Cwd: "/tmp/local-cwd",
250+ if err := attachAndCheck(ctx, local, protocol.Hello{
251+ Intent: protocol.IntentNew, Name: localSession, Env: localEnvironment(), Cwd: "/tmp/local-cwd",
252252 TermEnv: "xterm-256color", TrueColor: true,
253253 }); err != nil {
254254 return fmt.Errorf("attach local picker session: %w", err)
@@ -351,10 +351,10 @@ func runRestartResumePhase(ctx context.Context, target string, catalog ports.Rem
351351 return domain.RemoteSessionTarget{}, fmt.Errorf("open restart stdio transport: %w", err)
352352 }
353353 defer func() { _ = resumed.Close() }()
354- if err := attachAndCheck(ctx, resumed, ports.Hello{
355- Intent: ports.IntentResume, Name: sessionName, Env: localEnvironment(), Cwd: "/tmp/local-cwd",
354+ if err := attachAndCheck(ctx, resumed, protocol.Hello{
355+ Intent: protocol.IntentResume, Name: sessionName, Env: localEnvironment(), Cwd: "/tmp/local-cwd",
356356 TermEnv: "xterm-256color", TrueColor: true, RemoteTarget: &resumedTarget,
357- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
357+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
358358 }); err != nil {
359359 return domain.RemoteSessionTarget{}, fmt.Errorf("exact restart resume: %w", err)
360360 }
@@ -385,8 +385,8 @@ func runUDPPhase(ctx context.Context, target string) error {
385385 return fmt.Errorf("open UDP transport: %w", err)
386386 }
387387 defer func() { _ = udp.Close() }()
388- if err := attachAndCheck(ctx, udp, ports.Hello{
389- Intent: ports.IntentNew, Name: "udp-harness", Env: localEnvironment(), Cwd: "/tmp/local-cwd",
388+ if err := attachAndCheck(ctx, udp, protocol.Hello{
389+ Intent: protocol.IntentNew, Name: "udp-harness", Env: localEnvironment(), Cwd: "/tmp/local-cwd",
390390 TermEnv: "xterm-256color", TrueColor: true,
391391 }); err != nil {
392392 return fmt.Errorf("UDP attach: %w", err)
@@ -636,11 +636,11 @@ func openLocalTransport(ctx context.Context) (*harnessTransport, error) {
636636 return h, nil
637637 }
638638
639-func attachAndCheck(ctx context.Context, tr *harnessTransport, hello ports.Hello) error {
640- hello.Version = ports.ProtocolVersion
639+func attachAndCheck(ctx context.Context, tr *harnessTransport, hello protocol.Hello) error {
640+ hello.Version = protocol.Version
641641 hello.Size = domain.Size{Cols: 80, Rows: 24}
642642 if hello.EnvironmentPolicy == 0 {
643- hello.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned
643+ hello.EnvironmentPolicy = protocol.EnvironmentPolicyClientOwned
644644 }
645645 payload := ports.MarshalHello(hello)
646646 if err := tr.Send(ports.Frame{Type: ports.MsgHello, Payload: payload}); err != nil {
@@ -668,7 +668,7 @@ func attachAndCheck(ctx context.Context, tr *harnessTransport, hello ports.Hello
668668 }
669669
670670 func sendRawInput(tr *harnessTransport, data string) error {
671- return tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte(data)})})
671+ return tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte(data)})})
672672 }
673673
674674 func processOutputFrame(tr *harnessTransport, frame ports.Frame) error {
@@ -679,7 +679,7 @@ func processOutputFrame(tr *harnessTransport, frame ports.Frame) error {
679679 result := tr.probe.apply(output)
680680 if !result.Accepted {
681681 if !tr.probe.resetPending {
682- if err := tr.Send(ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{})}); err != nil {
682+ if err := tr.Send(ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(protocol.OutputResetRequest{})}); err != nil {
683683 return err
684684 }
685685 tr.probe.resetPending = true
@@ -768,7 +768,7 @@ func remotePreviewContains(preview protocol.RemotePreview, want string) bool {
768768 }
769769
770770 func sendInputAndAwait(ctx context.Context, tr *harnessTransport, command, want string) error {
771- if err := tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte(command + "\n")})}); err != nil {
771+ if err := tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte(command + "\n")})}); err != nil {
772772 return err
773773 }
774774 deadline, cancel := context.WithTimeout(ctx, 5*time.Second)
@@ -795,8 +795,8 @@ func sendInputAndAwait(ctx context.Context, tr *harnessTransport, command, want
795795
796796 func sendCommand(ctx context.Context, tr *harnessTransport, slug string) error {
797797 id := requestID.Add(1)
798- request := ports.CommandRequest{
799- Version: ports.ProtocolVersion,
798+ request := protocol.CommandRequest{
799+ Version: protocol.Version,
800800 RequestID: id,
801801 Attached: true,
802802 Slug: slug,
@@ -836,7 +836,7 @@ func sendCommand(ctx context.Context, tr *harnessTransport, slug string) error {
836836 }
837837
838838 func detach(ctx context.Context, tr *harnessTransport) error {
839- if err := tr.Send(ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(ports.Detach{})}); err != nil {
839+ if err := tr.Send(ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(protocol.Detach{})}); err != nil {
840840 return err
841841 }
842842 deadline, cancel := context.WithTimeout(ctx, 5*time.Second)
@@ -859,9 +859,9 @@ func expectNoSuchTarget(ctx context.Context, tr *harnessTransport, target domain
859859 if err := target.Validate(); err != nil {
860860 return err
861861 }
862- hello := ports.Hello{
863- Version: ports.ProtocolVersion,
864- Intent: ports.IntentResume,
862+ hello := protocol.Hello{
863+ Version: protocol.Version,
864+ Intent: protocol.IntentResume,
865865 Name: sessionName,
866866 Size: domain.Size{Cols: 80, Rows: 24},
867867 TermEnv: "xterm-256color",
@@ -869,7 +869,7 @@ func expectNoSuchTarget(ctx context.Context, tr *harnessTransport, target domain
869869 Env: localEnvironment(),
870870 Cwd: "/tmp/local-cwd",
871871 RemoteTarget: &target,
872- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
872+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
873873 }
874874 payload := ports.MarshalHello(hello)
875875 if err := tr.Send(ports.Frame{Type: ports.MsgHello, Payload: payload}); err != nil {
@@ -886,7 +886,7 @@ func expectNoSuchTarget(ctx context.Context, tr *harnessTransport, target domain
886886 if err != nil {
887887 return err
888888 }
889- if message.Code != ports.ErrNoSuchTarget {
889+ if message.Code != protocol.ErrNoSuchTarget {
890890 return fmt.Errorf("stale target returned error code %d", message.Code)
891891 }
892892 return nil

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