feat(backend): wire terminal manager into the daemon at /mux

Construct the tmux runtime and a terminal.Manager fed by the CDC
broadcaster, and hand it to httpd.New so the /mux WebSocket surface goes
live. httpd.New now runs after the CDC substrate so the broadcaster exists
when the manager subscribes; the listener still binds before running.json
is written, preserving fail-fast on port conflict. The manager is closed on
shutdown alongside the CDC pipeline and lifecycle stack.
This commit is contained in:
Pritom14 2026-05-31 19:12:05 +05:30
parent edcc631037
commit 4d90d59bf0
5 changed files with 22 additions and 12 deletions

View File

@ -284,9 +284,9 @@ func (c *connState) handleSubscribe() {
Type: msgSnapshot,
Session: &sessionUpdate{
Seq: e.Seq,
ProjectID: e.ProjectID,
SessionID: e.SessionID,
EventType: e.EventType,
Revision: e.Revision,
EventType: string(e.Type),
},
})
})

View File

@ -127,7 +127,7 @@ func TestServeForwardsSessionChannelFromCDC(t *testing.T) {
conn.in <- clientMsg{Ch: chSubscribe, Type: msgSubscribe}
// Give the subscription time to register before publishing.
eventually(t, time.Second, func() bool {
bc.Publish(cdc.Event{Seq: 9, SessionID: "s1", EventType: "session_updated", Revision: 4})
bc.Publish(cdc.Event{Seq: 9, ProjectID: "p1", SessionID: "s1", Type: cdc.EventSessionUpdated})
select {
case m := <-conn.out:
return m.Ch == chSessions && m.Session != nil && m.Session.Seq == 9

View File

@ -65,7 +65,7 @@ type serverMsg struct {
// change_log payload blob; the client refetches detail over the REST surface.
type sessionUpdate struct {
Seq int64 `json:"seq"`
SessionID string `json:"sessionId"`
ProjectID string `json:"projectId"`
SessionID string `json:"sessionId,omitempty"`
EventType string `json:"eventType"`
Revision int64 `json:"revision"`
}

View File

@ -33,7 +33,7 @@ func TestServerMsgSessionFrameWireShape(t *testing.T) {
Ch: chSessions,
Type: msgSnapshot,
Session: &sessionUpdate{
Seq: 7, SessionID: "s1", EventType: "session_updated", Revision: 3,
Seq: 7, ProjectID: "p1", SessionID: "s1", EventType: "session_updated",
},
}
raw, err := json.Marshal(msg)
@ -41,7 +41,7 @@ func TestServerMsgSessionFrameWireShape(t *testing.T) {
t.Fatalf("marshal: %v", err)
}
// Golden wire shape the client depends on.
want := `{"ch":"sessions","type":"snapshot","session":{"seq":7,"sessionId":"s1","eventType":"session_updated","revision":3}}`
want := `{"ch":"sessions","type":"snapshot","session":{"seq":7,"projectId":"p1","sessionId":"s1","eventType":"session_updated"}}`
if string(raw) != want {
t.Fatalf("wire shape:\n got %s\nwant %s", raw, want)
}

View File

@ -12,10 +12,12 @@ import (
"os/signal"
"syscall"
"github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/tmux"
"github.com/aoagents/agent-orchestrator/backend/internal/config"
"github.com/aoagents/agent-orchestrator/backend/internal/httpd"
"github.com/aoagents/agent-orchestrator/backend/internal/runfile"
"github.com/aoagents/agent-orchestrator/backend/internal/storage/sqlite"
"github.com/aoagents/agent-orchestrator/backend/internal/terminal"
)
func main() {
@ -42,11 +44,6 @@ func run() error {
return fmt.Errorf("daemon already running (pid %d, port %d); refusing to start", live.PID, live.Port)
}
srv, err := httpd.New(cfg, log, nil)
if err != nil {
return err
}
// Open the durable store and bring up the CDC substrate: the DB triggers
// capture changes into change_log, the poller tails it, and the broadcaster
// fans events out to the SSE transport. The LCM/Session Manager and the HTTP
@ -70,6 +67,19 @@ func run() error {
return err
}
// Terminal streaming: the tmux runtime supplies the PTY-attach command and
// liveness; the CDC broadcaster feeds the session-state channel. The manager
// is handed to httpd, which mounts it at /mux. Raw PTY bytes never flow
// through the CDC change_log — only session-state events do.
runtimeAdapter := tmux.New(tmux.Options{})
termMgr := terminal.NewManager(runtimeAdapter, cdcPipe.Broadcaster, log)
defer termMgr.Close()
srv, err := httpd.New(cfg, log, termMgr)
if err != nil {
return err
}
// Bring up the Lifecycle Manager (sole store writer) and the reaper (OBSERVE
// timer). This makes the write path live end-to-end: LCM write -> store -> DB
// trigger -> change_log -> poller -> broadcaster. The collaborators it needs