feat: complete Windows client control and recovery paths

This commit is contained in:
2026-09-06 13:37:32 +00:00
parent 56b15c7f4f
commit 486894557d
38 changed files with 2188 additions and 106 deletions
+22 -3
View File
@@ -267,14 +267,25 @@ func (store *Store) AssignSendWindow(ctx context.Context, issueUUID domain.UUID,
return nil, err
}
defer tx.Rollback()
var next uint64
err = tx.QueryRowContext(ctx, `SELECT next_event_seq FROM commands WHERE issue_uuid = ?`, issueUUID[:]).Scan(&next)
var next, lastAck, assignedBytes uint64
err = tx.QueryRowContext(ctx, `SELECT next_event_seq, last_server_ack,
COALESCE((SELECT sum(length(payload)) FROM events
WHERE issue_uuid = commands.issue_uuid AND event_seq IS NOT NULL AND event_seq > commands.last_server_ack), 0)
FROM commands WHERE issue_uuid = ?`, issueUUID[:]).Scan(&next, &lastAck, &assignedBytes)
if err == sql.ErrNoRows {
return nil, ErrUnknownCommand
}
if err != nil {
return nil, err
}
// Assigned rows remain pinned until cumulative acknowledgement. Never
// assign another row while that durable send window is full; otherwise a
// burst of local events could grow the pinned set without bound even though
// callers pass a per-command byte limit on every invocation.
if assignedBytes >= maximumStoredBytes {
return eventsBySequence(ctx, tx, issueUUID, 0)
}
maximumStoredBytes -= assignedBytes
rows, err := tx.QueryContext(ctx, `SELECT local_ordinal, length(payload) FROM events WHERE issue_uuid = ? AND event_seq IS NULL ORDER BY local_ordinal LIMIT ?`, issueUUID[:], maximumEvents)
if err != nil {
return nil, err
@@ -475,6 +486,14 @@ func (store *Store) RecoverLaunchUncertainty(ctx context.Context, now time.Time)
// crash between a terminal marker and its public event from creating a state
// that can be replayed as a second execution.
func (store *Store) AppendLifecycle(ctx context.Context, issueUUID domain.UUID, phase uint32, revision uint64, detail string, observedAt time.Time) (Event, error) {
return store.AppendLifecycleWithIdentity(ctx, issueUUID, phase, revision, detail, observedAt, nil)
}
// AppendLifecycleWithIdentity is the lifecycle boundary used by the client
// runtime when the Windows supervisor has captured an immutable selection
// record. Keeping the legacy wrapper above preserves the small store API for
// recovery and tests that have no platform identity to attach.
func (store *Store) AppendLifecycleWithIdentity(ctx context.Context, issueUUID domain.UUID, phase uint32, revision uint64, detail string, observedAt time.Time, identity *rvboxv1.WindowsExecutionIdentity) (Event, error) {
if !validUUID(issueUUID) || phase == 0 || phase > 11 || observedAt.IsZero() || revision == 0 {
return Event{}, errors.New("invalid lifecycle event")
}
@@ -503,7 +522,7 @@ func (store *Store) AppendLifecycle(ctx context.Context, issueUUID domain.UUID,
if !domain.CanTransition(rvboxv1.CommandLifecycle(current), rvboxv1.CommandLifecycle(phase)) {
return Event{}, fmt.Errorf("invalid lifecycle transition %s -> %s", rvboxv1.CommandLifecycle(current), rvboxv1.CommandLifecycle(phase))
}
lifecycle := &rvboxv1.LifecycleChange{Lifecycle: rvboxv1.CommandLifecycle(phase), CommandRevision: revision, Detail: detail}
lifecycle := &rvboxv1.LifecycleChange{Lifecycle: rvboxv1.CommandLifecycle(phase), CommandRevision: revision, Detail: detail, WindowsExecutionIdentity: identity}
payload, err := proto.MarshalOptions{Deterministic: true}.Marshal(&rvboxv1.CommandEvent{IssueUuid: issueUUID.String(), ObservedAt: timestamppb.New(observedAt), Payload: &rvboxv1.CommandEvent_Lifecycle{Lifecycle: lifecycle}})
if err != nil {
return Event{}, err