Files
tutti/packages/agent/host/workspace_runtime_gate.go
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

567 lines
18 KiB
Go

package agenthost
import (
"context"
"errors"
"log/slog"
"sort"
"strings"
"sync"
"time"
"github.com/google/uuid"
)
var errWorkspaceRuntimeDisconnectReentrant = errors.New("workspace runtime disconnect requested by an admitted operation")
var errWorkspaceRuntimeDisconnectFenceReleased = errors.New("workspace runtime disconnect fence is released")
// WorkspaceRuntimeOperationInfo identifies one runtime mutation for admission
// diagnostics. OperationID is caller-owned when available; Host generates one
// when the caller does not provide it.
type WorkspaceRuntimeOperationInfo struct {
WorkspaceID string
OperationID string
Kind string
AgentSessionID string
Source string
}
// WorkspaceRuntimeOperationSnapshot describes one mutation currently admitted
// by a Workspace runtime gate.
type WorkspaceRuntimeOperationSnapshot struct {
OperationID string
Kind string
AgentSessionID string
Source string
StartedAt time.Time
}
// WorkspaceRuntimeDisconnectSnapshot describes one acquired disconnect fence.
type WorkspaceRuntimeDisconnectSnapshot struct {
FenceID string
AcquiredAt time.Time
Exclusive bool
}
// WorkspaceRuntimeAdmissionSnapshot is a point-in-time diagnostic view of one
// Workspace runtime gate. It is read-only and does not affect admission.
type WorkspaceRuntimeAdmissionSnapshot struct {
WorkspaceID string
Operations int
Disconnectors int
Disconnecting bool
Exclusive bool
OperationHolders []WorkspaceRuntimeOperationSnapshot
DisconnectHolders []WorkspaceRuntimeDisconnectSnapshot
}
type workspaceRuntimeAdmission struct {
mu sync.Mutex
states map[string]*workspaceRuntimeAdmissionState
}
type workspaceRuntimeAdmissionState struct {
changed chan struct{}
operations int
disconnecting bool
disconnectors int
exclusive bool
refs int
deferred []func(context.Context)
operationHolders map[string]WorkspaceRuntimeOperationSnapshot
disconnectHolders map[string]WorkspaceRuntimeDisconnectSnapshot
}
// WorkspaceRuntimeDisconnectFence closes Workspace runtime admission as soon
// as it is acquired. Wait may be retried with a new context without reopening
// admission; Release is idempotent and reopens admission only after every
// joined owner has released its fence.
type WorkspaceRuntimeDisconnectFence struct {
gate *workspaceRuntimeAdmission
workspaceID string
state *workspaceRuntimeAdmissionState
holderID string
fenceID string
mu sync.Mutex
released bool
exclusive bool
}
type workspaceRuntimeAdmissionContext struct {
gate *workspaceRuntimeAdmission
workspaceID string
exclusive bool
}
type workspaceRuntimeAdmissionContextKey struct{}
type workspaceRuntimeDeferredDisconnectContextKey struct{}
func newWorkspaceRuntimeAdmission() *workspaceRuntimeAdmission {
return &workspaceRuntimeAdmission{states: make(map[string]*workspaceRuntimeAdmissionState)}
}
func normalizeWorkspaceRuntimeOperationInfo(
ctx context.Context,
info WorkspaceRuntimeOperationInfo,
) WorkspaceRuntimeOperationInfo {
info.WorkspaceID = strings.TrimSpace(info.WorkspaceID)
if command := commandTerminalFailureFrom(ctx); command != nil {
command.mu.Lock()
if info.OperationID == "" {
info.OperationID = strings.TrimSpace(command.operationID)
}
if info.Kind == "" {
info.Kind = strings.TrimSpace(command.flow)
}
if info.AgentSessionID == "" {
info.AgentSessionID = strings.TrimSpace(command.agentSessionID)
}
if info.Source == "" && command.flow != "" {
info.Source = "host.command." + strings.TrimSpace(command.flow)
}
command.mu.Unlock()
}
if info.OperationID == "" {
info.OperationID = "runtime-operation:" + uuid.NewString()
}
if info.Kind == "" {
info.Kind = "runtime_operation"
}
if info.Source == "" {
info.Source = "host.workspace_runtime_operation"
}
return info
}
func (g *workspaceRuntimeAdmission) enterOperation(
ctx context.Context,
info WorkspaceRuntimeOperationInfo,
) (context.Context, func(), error) {
info = normalizeWorkspaceRuntimeOperationInfo(ctx, info)
workspaceID := info.WorkspaceID
if g == nil || workspaceID == "" {
return ctx, func() {}, ErrInvalidArgument
}
if err := ctx.Err(); err != nil {
return ctx, func() {}, err
}
if admission, ok := ctx.Value(workspaceRuntimeAdmissionContextKey{}).(workspaceRuntimeAdmissionContext); ok &&
admission.gate == g && admission.workspaceID == workspaceID {
return ctx, func() {}, nil
}
for {
g.mu.Lock()
state := g.stateLocked(workspaceID)
if !state.disconnecting {
holderID := uuid.NewString()
state.operationHolders[holderID] = WorkspaceRuntimeOperationSnapshot{
OperationID: info.OperationID,
Kind: info.Kind,
AgentSessionID: info.AgentSessionID,
Source: info.Source,
StartedAt: time.Now(),
}
state.operations++
g.mu.Unlock()
operationCtx := context.WithValue(ctx, workspaceRuntimeAdmissionContextKey{}, workspaceRuntimeAdmissionContext{
gate: g, workspaceID: workspaceID,
})
var once sync.Once
return operationCtx, func() {
once.Do(func() { g.leaveOperation(workspaceID, state, holderID) })
}, nil
}
changed := state.changed
g.mu.Unlock()
select {
case <-ctx.Done():
g.releaseReference(workspaceID, state)
return ctx, func() {}, ctx.Err()
case <-changed:
g.releaseReference(workspaceID, state)
}
}
}
func (g *workspaceRuntimeAdmission) beginDisconnect(
ctx context.Context,
workspaceID string,
) (context.Context, func(), error) {
workspaceID = strings.TrimSpace(workspaceID)
if g == nil || workspaceID == "" {
return ctx, func() {}, ErrInvalidArgument
}
if admission, ok := ctx.Value(workspaceRuntimeAdmissionContextKey{}).(workspaceRuntimeAdmissionContext); ok &&
admission.gate == g && admission.workspaceID == workspaceID {
if admission.exclusive {
return ctx, func() {}, nil
}
return ctx, func() {}, errWorkspaceRuntimeDisconnectReentrant
}
fence, err := g.acquireDisconnectFence(ctx, workspaceID)
if err != nil {
return ctx, func() {}, err
}
disconnectCtx, err := fence.Wait(ctx)
if err != nil {
fence.Release()
return ctx, func() {}, err
}
return disconnectCtx, fence.Release, nil
}
func (g *workspaceRuntimeAdmission) acquireDisconnectFence(
ctx context.Context,
workspaceID string,
) (*WorkspaceRuntimeDisconnectFence, error) {
workspaceID = strings.TrimSpace(workspaceID)
if g == nil || workspaceID == "" {
return nil, ErrInvalidArgument
}
if err := ctx.Err(); err != nil {
return nil, err
}
if admission, ok := ctx.Value(workspaceRuntimeAdmissionContextKey{}).(workspaceRuntimeAdmissionContext); ok &&
admission.gate == g && admission.workspaceID == workspaceID {
return nil, errWorkspaceRuntimeDisconnectReentrant
}
g.mu.Lock()
state := g.stateLocked(workspaceID)
holderID := uuid.NewString()
fenceID := uuid.NewString()
state.disconnectors++
state.disconnectHolders[holderID] = WorkspaceRuntimeDisconnectSnapshot{
FenceID: fenceID, AcquiredAt: time.Now(), Exclusive: false,
}
if !state.disconnecting {
state.disconnecting = true
g.notifyLocked(state)
}
g.mu.Unlock()
return &WorkspaceRuntimeDisconnectFence{
gate: g, workspaceID: workspaceID, state: state,
holderID: holderID, fenceID: fenceID,
}, nil
}
// Wait drains already-admitted operations and grants one exclusive disconnect
// scope. A canceled wait leaves the fence and admission closure intact so the
// owner can retry without admitting a runtime mutation in between.
func (f *WorkspaceRuntimeDisconnectFence) Wait(ctx context.Context) (context.Context, error) {
if f == nil || f.gate == nil || f.state == nil {
return ctx, ErrInvalidArgument
}
if err := ctx.Err(); err != nil {
f.logWaitFailure(err)
return ctx, err
}
f.mu.Lock()
if f.released {
f.mu.Unlock()
return ctx, errWorkspaceRuntimeDisconnectFenceReleased
}
if f.exclusive {
f.mu.Unlock()
return context.WithValue(ctx, workspaceRuntimeAdmissionContextKey{}, workspaceRuntimeAdmissionContext{
gate: f.gate, workspaceID: f.workspaceID, exclusive: true,
}), nil
}
f.mu.Unlock()
for {
f.gate.mu.Lock()
if f.state.operations == 0 && !f.state.exclusive {
f.mu.Lock()
if f.released {
f.mu.Unlock()
f.gate.mu.Unlock()
return ctx, errWorkspaceRuntimeDisconnectFenceReleased
}
f.state.exclusive = true
f.exclusive = true
if holder, ok := f.state.disconnectHolders[f.holderID]; ok {
holder.Exclusive = true
f.state.disconnectHolders[f.holderID] = holder
}
f.mu.Unlock()
f.gate.mu.Unlock()
return context.WithValue(ctx, workspaceRuntimeAdmissionContextKey{}, workspaceRuntimeAdmissionContext{
gate: f.gate, workspaceID: f.workspaceID, exclusive: true,
}), nil
}
changed := f.state.changed
f.gate.mu.Unlock()
select {
case <-ctx.Done():
err := ctx.Err()
f.logWaitFailure(err)
return ctx, err
case <-changed:
}
}
}
// Release relinquishes this caller's fence ownership. Admission remains closed
// while another joined owner exists.
func (f *WorkspaceRuntimeDisconnectFence) Release() {
if f == nil || f.gate == nil || f.state == nil {
return
}
f.gate.mu.Lock()
f.mu.Lock()
if f.released {
f.mu.Unlock()
f.gate.mu.Unlock()
return
}
f.released = true
if f.exclusive {
f.state.exclusive = false
f.exclusive = false
}
delete(f.state.disconnectHolders, f.holderID)
f.state.disconnectors--
if f.state.disconnectors == 0 && len(f.state.deferred) == 0 {
f.state.disconnecting = false
}
f.gate.notifyLocked(f.state)
f.gate.releaseReferenceLocked(f.workspaceID, f.state)
f.mu.Unlock()
f.gate.mu.Unlock()
}
func (g *workspaceRuntimeAdmission) stateLocked(workspaceID string) *workspaceRuntimeAdmissionState {
state := g.states[workspaceID]
if state == nil {
state = &workspaceRuntimeAdmissionState{
changed: make(chan struct{}),
operationHolders: make(map[string]WorkspaceRuntimeOperationSnapshot),
disconnectHolders: make(map[string]WorkspaceRuntimeDisconnectSnapshot),
}
g.states[workspaceID] = state
}
state.refs++
return state
}
func (g *workspaceRuntimeAdmission) leaveOperation(workspaceID string, state *workspaceRuntimeAdmissionState, holderID string) {
g.mu.Lock()
delete(state.operationHolders, holderID)
state.operations--
g.notifyLocked(state)
var deferred []func(context.Context)
if state.operations == 0 && len(state.deferred) > 0 {
deferred = append(deferred, state.deferred...)
state.deferred = nil
state.exclusive = true
}
if len(deferred) == 0 {
g.releaseReferenceLocked(workspaceID, state)
}
g.mu.Unlock()
if len(deferred) == 0 {
return
}
disconnectCtx := context.WithValue(context.Background(), workspaceRuntimeAdmissionContextKey{}, workspaceRuntimeAdmissionContext{
gate: g, workspaceID: workspaceID, exclusive: true,
})
// The deferred sweep is best-effort and ordered. If a callback panics,
// propagate the panic but always reopen admission; callbacks after the
// panicking callback are not run because their ordering may depend on it.
defer g.finishDeferredDisconnect(workspaceID, state)
for _, disconnect := range deferred {
disconnect(disconnectCtx)
}
}
func (g *workspaceRuntimeAdmission) deferDisconnect(
workspaceID string,
disconnect func(context.Context),
) {
g.mu.Lock()
state := g.states[workspaceID]
if state == nil || state.operations == 0 {
g.mu.Unlock()
return
}
state.disconnecting = true
state.deferred = append(state.deferred, disconnect)
g.notifyLocked(state)
g.mu.Unlock()
}
func (g *workspaceRuntimeAdmission) finishDeferredDisconnect(workspaceID string, state *workspaceRuntimeAdmissionState) {
g.mu.Lock()
state.exclusive = false
if state.disconnectors == 0 {
state.disconnecting = false
}
g.notifyLocked(state)
g.releaseReferenceLocked(workspaceID, state)
g.mu.Unlock()
}
func (g *workspaceRuntimeAdmission) releaseReference(workspaceID string, state *workspaceRuntimeAdmissionState) {
g.mu.Lock()
g.releaseReferenceLocked(workspaceID, state)
g.mu.Unlock()
}
func (g *workspaceRuntimeAdmission) releaseReferenceLocked(workspaceID string, state *workspaceRuntimeAdmissionState) {
state.refs--
if state.refs == 0 && state.operations == 0 && !state.disconnecting && g.states[workspaceID] == state {
delete(g.states, workspaceID)
}
}
func (g *workspaceRuntimeAdmission) snapshot(workspaceID string) WorkspaceRuntimeAdmissionSnapshot {
workspaceID = strings.TrimSpace(workspaceID)
snapshot := WorkspaceRuntimeAdmissionSnapshot{WorkspaceID: workspaceID}
if g == nil || workspaceID == "" {
return snapshot
}
g.mu.Lock()
defer g.mu.Unlock()
state := g.states[workspaceID]
if state == nil {
return snapshot
}
snapshot.Operations = state.operations
snapshot.Disconnectors = state.disconnectors
snapshot.Disconnecting = state.disconnecting
snapshot.Exclusive = state.exclusive
for _, holder := range state.operationHolders {
snapshot.OperationHolders = append(snapshot.OperationHolders, holder)
}
for _, holder := range state.disconnectHolders {
snapshot.DisconnectHolders = append(snapshot.DisconnectHolders, holder)
}
sort.Slice(snapshot.OperationHolders, func(i, j int) bool {
left, right := snapshot.OperationHolders[i], snapshot.OperationHolders[j]
if !left.StartedAt.Equal(right.StartedAt) {
return left.StartedAt.Before(right.StartedAt)
}
return left.OperationID < right.OperationID
})
sort.Slice(snapshot.DisconnectHolders, func(i, j int) bool {
return snapshot.DisconnectHolders[i].FenceID < snapshot.DisconnectHolders[j].FenceID
})
return snapshot
}
func (f *WorkspaceRuntimeDisconnectFence) logWaitFailure(err error) {
if f == nil || f.gate == nil || err == nil {
return
}
snapshot := f.gate.snapshot(f.workspaceID)
slog.Warn("workspace runtime disconnect drain wait failed",
"event", "agent.host.workspace_runtime.disconnect.drain_wait_failed",
"workspace_id", snapshot.WorkspaceID,
"fence_id", f.fenceID,
"error", err,
"operations", snapshot.Operations,
"disconnectors", snapshot.Disconnectors,
"disconnecting", snapshot.Disconnecting,
"exclusive", snapshot.Exclusive,
"operation_holders", snapshot.OperationHolders,
"disconnect_holders", snapshot.DisconnectHolders,
)
}
func (*workspaceRuntimeAdmission) notifyLocked(state *workspaceRuntimeAdmissionState) {
close(state.changed)
state.changed = make(chan struct{})
}
func (h *Host) withWorkspaceRuntimeOperation(
ctx context.Context,
workspaceID string,
fn func(context.Context) error,
) error {
return h.withWorkspaceRuntimeOperationInfo(ctx, WorkspaceRuntimeOperationInfo{WorkspaceID: workspaceID}, fn)
}
func (h *Host) withWorkspaceRuntimeOperationInfo(
ctx context.Context,
info WorkspaceRuntimeOperationInfo,
fn func(context.Context) error,
) error {
if h == nil || h.workspaceRuntimeAdmission == nil || fn == nil {
return ErrInvalidArgument
}
operationCtx, release, err := h.workspaceRuntimeAdmission.enterOperation(ctx, info)
if err != nil {
return err
}
defer release()
return fn(operationCtx)
}
// WithWorkspaceRuntimeOperation runs one caller-owned runtime mutation under
// Host's Workspace admission. The admitted context is reentrant for Host
// lifecycle calls in the callback and must cover the complete mutation,
// including its cleanup.
func (h *Host) WithWorkspaceRuntimeOperation(
ctx context.Context,
workspaceID string,
fn func(context.Context) error,
) error {
return h.withWorkspaceRuntimeOperation(ctx, workspaceID, fn)
}
// WithWorkspaceRuntimeOperationInfo runs one caller-owned runtime mutation
// under Host's Workspace admission with diagnostics supplied by the caller.
// It is an additive variant of WithWorkspaceRuntimeOperation for adapters that
// can name the concrete runtime operation and session.
func (h *Host) WithWorkspaceRuntimeOperationInfo(
ctx context.Context,
info WorkspaceRuntimeOperationInfo,
fn func(context.Context) error,
) error {
return h.withWorkspaceRuntimeOperationInfo(ctx, info, fn)
}
// SnapshotWorkspaceRuntimeAdmission returns the current diagnostic state for
// one Workspace runtime gate. The snapshot is empty when the Workspace has no
// live gate state.
func (h *Host) SnapshotWorkspaceRuntimeAdmission(workspaceID string) WorkspaceRuntimeAdmissionSnapshot {
if h == nil || h.workspaceRuntimeAdmission == nil {
return WorkspaceRuntimeAdmissionSnapshot{WorkspaceID: strings.TrimSpace(workspaceID)}
}
return h.workspaceRuntimeAdmission.snapshot(workspaceID)
}
// AcquireWorkspaceRuntimeDisconnectFence synchronously prevents new runtime
// mutations in one Workspace. Its Wait may be retried after cancellation; the
// caller must always Release the returned fence.
func (h *Host) AcquireWorkspaceRuntimeDisconnectFence(
ctx context.Context,
workspaceID string,
) (*WorkspaceRuntimeDisconnectFence, error) {
if h == nil || h.workspaceRuntimeAdmission == nil {
return nil, ErrInvalidArgument
}
return h.workspaceRuntimeAdmission.acquireDisconnectFence(ctx, workspaceID)
}
// BeginWorkspaceRuntimeDisconnect prevents new runtime mutations in one
// Workspace and waits for mutations already admitted there. The returned
// context must be used for DisconnectWorkspaceRuntime while release is held.
func (h *Host) BeginWorkspaceRuntimeDisconnect(
ctx context.Context,
workspaceID string,
) (context.Context, func(), error) {
if h == nil || h.workspaceRuntimeAdmission == nil {
return ctx, func() {}, ErrInvalidArgument
}
disconnectCtx, release, err := h.workspaceRuntimeAdmission.beginDisconnect(ctx, workspaceID)
if errors.Is(err, errWorkspaceRuntimeDisconnectReentrant) {
// A transport may request attach cleanup while its Host operation already
// owns admission. Let physical cleanup continue on the caller's existing
// attachAxis; DisconnectWorkspaceRuntime will defer only its semantic sweep
// until this operation leaves.
return context.WithValue(ctx, workspaceRuntimeDeferredDisconnectContextKey{}, true), func() {}, nil
}
return disconnectCtx, release, err
}