feat: persist and dispatch client command input

This commit is contained in:
2026-09-06 10:40:23 +00:00
parent 8155e5f81f
commit ad564de23f
16 changed files with 836 additions and 22 deletions
+66 -2
View File
@@ -141,13 +141,14 @@ func (server *AgentServer) serveConnection(parent context.Context, connection *w
capacity := &CapacityShadow{}
var capacityMu sync.Mutex
reservations := make(map[string]DispatchLane)
stdinSent := make(map[string]struct{})
reconciled := make(chan struct{})
var reconcileOnce sync.Once
if !capacity.UpdateAdvertised(0, 0, hello.GetMaxRunningCommands(), hello.GetMaxQueuedCommands()) {
server.close(connection, websocket.StatusPolicyViolation, "invalid initial client capacity")
return
}
go server.dispatchLoop(sessionContext, queue, handle.DispatchWake(), reconciled, &capacityMu, capacity, reservations, hello.GetClientId(), hello.GetPlatform(), encodeSessionID(sessionID), registration.Generation, cancel)
go server.dispatchLoop(sessionContext, queue, handle.DispatchWake(), reconciled, &capacityMu, capacity, reservations, stdinSent, hello.GetClientId(), hello.GetPlatform(), encodeSessionID(sessionID), registration.Generation, cancel)
encodedSessionID := encodeSessionID(sessionID)
welcome, err := proto.Marshal(&rvboxv1.AgentEnvelope{
SessionId: encodedSessionID, SessionGeneration: registration.Generation,
@@ -265,6 +266,13 @@ func (server *AgentServer) serveConnection(parent context.Context, connection *w
server.close(connection, websocket.StatusInternalError, "could not acknowledge command event")
return
}
if stdinAck := event.GetStdinAck(); stdinAck != nil {
issue, _ := domain.ParseUUIDv7(event.GetIssueUuid())
capacityMu.Lock()
delete(stdinSent, stdinIntentKey(issue, stdinAck.GetWriteSeq()))
capacityMu.Unlock()
handle.SignalDispatch()
}
}
}
}
@@ -366,10 +374,58 @@ func (server *AgentServer) enqueueNextDispatch(ctx context.Context, queue *Write
return candidate.IssueUUID, true, nil
}
// enqueueNextStdin exposes one durable input intent on the essential control
// lane. Session-local sent tracking suppresses duplicate frames while a live
// connection remains usable; reconnecting naturally replays unacknowledged
// writes from storage.
func (server *AgentServer) enqueueNextStdin(ctx context.Context, queue *WriterQueue, clientID, sessionID string, generation uint64, sent map[string]struct{}, sentMu *sync.Mutex) (bool, error) {
intents, err := server.Store.PendingStdin(ctx, clientID)
if err != nil {
return false, err
}
for _, intent := range intents {
key := stdinIntentKey(intent.IssueUUID, intent.WriteSeq)
sentMu.Lock()
_, alreadySent := sent[key]
if !alreadySent {
sent[key] = struct{}{}
}
sentMu.Unlock()
if alreadySent {
continue
}
envelope := &rvboxv1.AgentEnvelope{SessionId: sessionID, SessionGeneration: generation}
if intent.Close {
envelope.Payload = &rvboxv1.AgentEnvelope_CloseStdin{CloseStdin: &rvboxv1.CloseStdin{IssueUuid: intent.IssueUUID.String(), WriteSeq: intent.WriteSeq}}
} else {
envelope.Payload = &rvboxv1.AgentEnvelope_StdinWrite{StdinWrite: &rvboxv1.StdinWrite{IssueUuid: intent.IssueUUID.String(), WriteSeq: intent.WriteSeq, Data: append([]byte(nil), intent.Data...), AppendNewline: intent.AppendNewline}}
}
encoded, marshalErr := proto.Marshal(envelope)
if marshalErr != nil {
sentMu.Lock()
delete(sent, key)
sentMu.Unlock()
return false, marshalErr
}
if enqueueErr := queue.EnqueueControl(Frame{Kind: FrameControl, Payload: encoded}); enqueueErr != nil {
sentMu.Lock()
delete(sent, key)
sentMu.Unlock()
return false, enqueueErr
}
return true, nil
}
return false, nil
}
func stdinIntentKey(issue domain.UUID, writeSeq uint64) string {
return issue.String() + ":" + fmt.Sprint(writeSeq)
}
// dispatchLoop is the per-session serialized dispatcher. It waits for a
// complete reconciliation result before consuming queued work, then coalesces
// wakeups from local control RPCs, capacity advertisements, and acceptances.
func (server *AgentServer) dispatchLoop(ctx context.Context, queue *WriterQueue, wake <-chan struct{}, reconciled <-chan struct{}, capacityMu *sync.Mutex, capacity *CapacityShadow, reservations map[string]DispatchLane, clientID string, platform rvboxv1.Platform, sessionID string, generation uint64, cancel context.CancelFunc) {
func (server *AgentServer) dispatchLoop(ctx context.Context, queue *WriterQueue, wake <-chan struct{}, reconciled <-chan struct{}, capacityMu *sync.Mutex, capacity *CapacityShadow, reservations map[string]DispatchLane, stdinSent map[string]struct{}, clientID string, platform rvboxv1.Platform, sessionID string, generation uint64, cancel context.CancelFunc) {
select {
case <-reconciled:
case <-ctx.Done():
@@ -377,6 +433,14 @@ func (server *AgentServer) dispatchLoop(ctx context.Context, queue *WriterQueue,
}
for {
for {
stdinQueued, stdinErr := server.enqueueNextStdin(ctx, queue, clientID, sessionID, generation, stdinSent, capacityMu)
if stdinErr != nil {
server.closeForDispatchFailure(cancel)
return
}
if stdinQueued {
continue
}
capacityMu.Lock()
lane := capacity.Reserve()
capacityMu.Unlock()