You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Implement authenticated attach authorization, host dispatch, owner-authoritative non-optimistic queues, runtime and attachment leases, replay, and default final-viewer shutdown with five-second unexpected-loss grace; persisted Continue in background can retain the runtime.
Scope
Implement only commit 4 of the approved design represented by commit 0c6578641befad31d5a38003baf6c0f0fe64748a. Preserve the contracts below
exactly; local and remote behavior must remain compatible.
This is the host/runtime request-API implementation item. The complete audited design contains 28 request/options types: 22 public and 6 internal. This child owns the five non-wire internal
request types used by authorization, host dispatch, runtime termination/retention, and attachment.
It consumes, but does not redefine ownership of, the property-based protocol and persisted-intent
records from its dependencies.
API shape convention
APIs with multiple independent values use one property-based *Request or *Options record plus
an optional CancellationToken. Semantically required members are required init; optional
members have explicit defaults.
Constructors remain compact value objects or cohesive dependency sets. Existing framework
overrides retain inherited signatures.
Protocol DTO/property records retain strict serializers, fixed discriminators, kebab-case JSON,
unknown-member rejection, and all existing wire semantics.
There is no InterruptAgentSessionRequest. The client API remains exactly Task InterruptAsync(Guid commandId, CancellationToken ct = default).
Shared protocol state forms
The host snapshot/event path uses the exact property-based state forms below:
Usage permits null for a metric the provider did not report; otherwise counts and USD cost are
nonnegative. AgentInformation requires nonnull, nonblank identity/display strings, a null-or-
nonblank model id, and a complete nonnull definition. Authorization completes before runtime lookup,
snapshot construction, or definition serialization. Authorized viewers receive the same complete
definition; unauthorized peers receive no definition bytes, metadata, or existence signal.
Files
Files: peer identity provider, attach authorizer, listener, host, runtime registry, runtime and
attachment leases, replay buffer, transport composition registration. Tests: listener, authorizer, host, registry, replay limits, multiple viewers, sanitized errors, and
no existence disclosure, including default final-viewer stop, persisted background retention,
five-second reconnect grace, attach/stop races, explicit-terminate precedence, complete graceful
cleanup, queue convergence, and reconnect snapshot/replay.
OnChannelOpenAsync returns null for a discriminator other than attach-agent-session. For a match,
it uses strict deserialization, obtains authenticated peer identity, and calls the host. Validation
or authorization failures write one sanitized terminal error and close; authorization occurs before
runtime lookup. OnStreamOpenAsync always returns null. Disposal stops accepting channels and
releases listener-owned active attachment leases. It does not directly dispose runtime leases, but
final attachment release applies the normal background policy; owner-host application shutdown then
calls RemoteAgentSessionHost.DisposeAsync, which fences and stops every remaining runtime
regardless of that policy.
Attach identity and authorization - New
Namespace/project/files: transport-authenticated TransportPeerIdentity.cs and ITransportPeerIdentityProvider.cs remain in Phantom.Workspaces.Transport; IAgentSessionAttachAuthorizer.cs is in Phantom.Workspaces/Services/AgentSessions.
Visibility/kind:public sealed record TransportPeerIdentity; two internal interfaces.
Ownership: each authenticated transport adapter records identity in a channel-keyed feature
provider. Anonymous or ambiguous channels have no identity and fail closed.
TransportPeerIdentity rejects blank authentication scheme/stable peer id. Optional entity ids are
either null or nonblank canonical entity ids; these values are authenticated claims and are not
serialized by the attach protocol.
GetRequiredIdentity returns only transport-authenticated claims, never request fields, and throws a
sanitized unauthenticated error when absent. The concrete internal AgentSessionAttachAuthorizer(IDataAccessLayer) loads the session's owning user-computer-profile, resolves that profile's user, and allows attach/mutation only when:
(a) request owner and generation exactly match persistence; (b) peer.UserEntityId equals the
owning profile's user entity id; and (c) the authenticated transport's UserComputerProfileEntityId, when present, resolves to that same user. Takeover additionally
requires the proposed new profile to resolve to the same user. Child attach additionally requires
the child id to be in the owning runtime's subagent registry. There is no caller-authored ACL or
request-supplied identity. Authorization runs for open/reconnect and every mutation. Denial is
indistinguishable from nonexistent session on the wire. Cancellation performs no mutation. The
authorizer is stateless and process-scoped.
Consumed protocol request values
The host consumes the exact strict property-based protocol values below from #1486:
These retain version-1 kebab-case serialization, strict unknown-member rejection, and fixed protocol
semantics. They carry stable identity and intent only, never authenticated peer claims, compiled
policy, local paths, process handles, or credentials.
Responsibility/lifetime/threading: application-layer coordinator between authorization,
persistence/runtime hydration, the runtime registry, and a transport channel.
GetStatusAsync accepts only OpenIntent.Status, authorizes before lookup, and exposes no other
runtime state. OpenAsync rejects Status, authorizes before lookup, validates owner/generation,
then atomically gets or starts the
runtime according to OpenIntent; Attach never starts, Start fails if already running, and StartOrAttach/Resume reuse the matching runtime. It creates the attachment before taking a
snapshot so no delta is lost, then emits snapshot-or-replay. TakeOverAsync authorizes takeover,
fences mutations, awaits old runtime disposal, compare/exchanges owner/generation, and starts nothing
until persistence succeeds. A live unconfirmed lease returns takeover-blocked. Host disposal drains
attachments then runtime leases. No method accepts a compiled policy.
The ownership lease is a persisted compare/exchange record keyed by session and generation. It uses
the data service's authoritative timestamp, is renewed every 10 seconds, and expires after 30 seconds.
Each successful renewal returns the authoritative expiry instant. If renewal fails or is uncertain,
the owner retries once per second but fences the runtime no later than five seconds before the last
confirmed expiry; after fencing it accepts no mutation and performs graceful stop. It never assumes
renewal from a local clock or transient response. Orderly stop releases the lease only after
terminal/stopped persistence; a crashed host cannot renew it.
After expiry, restart/takeover first marks the abandoned epoch stopped, then creates a new epoch. continue-in-background survives that recovery but is not an auto-resume instruction: only an
ordinary open or the existing auto-resume entity setting starts the replacement runtime.
Visibility/kind: registry interface/implementation and leases are internal; epoch/cursor value
types are public protocol values.
Ownership: the process-scoped registry owns one runtime lease per (AgentSessionId, OwnershipGeneration). The lease owns execution resources; its lifecycle gate
also tracks logical attachment count and the persisted background preference.
GetOrStartAsync is single-flight and returns the existing matching lease; a failed/cancelled factory
is removed. TryTerminateAsync succeeds only on exact generation+epoch, marks the registry entry
fenced in place first, then
performs graceful stop once; explicit terminate always wins over attach, reconnect, grace, or
preference changes. Runtime stop rejects new mutations, interrupts an active turn when necessary,
unsubscribes the local event bridge, disposes AgentChat, component transports, MXC/process-executor
leases, wrappers, stdio MCP transports, and contained children, persists terminal/stopped state,
emits terminal once before writable channels close, and only then removes the registry entry. A
fenced entry rejects attach/start so no replacement epoch can appear before cleanup completes.
Attach subscribes before snapshot capture and increments the logical viewer count under the same
lifecycle gate. Explicit attachment disposal decrements immediately. Unexpected channel loss marks
the attachment token disconnected and starts one TimeProvider-driven five-second timer; reconnect
with that token cancels the timer and keeps the count unchanged. Timer expiry releases the viewer.
When a release changes the count to zero and ContinueInBackground is false, graceful stop starts. SetContinueInBackgroundAsync validates the exact generation/epoch, persists first, then updates the
runtime and emits SessionRetentionChangedEvent; setting false at zero viewers starts stop in the
same serialized transition. Attach, release, grace expiry, and preference updates emit an ordered SessionRetentionChangedEvent whenever either authoritative value changes; snapshots carry both
values. A later attach cannot cancel a fenced stop.
Named initializer examples
The child-owned request records are constructed by name, never with transposable positional values:
AgentSessionReplayBuffer retains the newest 4,096 events subject to an 8 MiB serialized-size cap
and a 15-minute age cap. Append and cursor reads are locked and sequence-monotonic. A reconnect gets
replay only when epoch matches and every sequence after its cursor is retained; otherwise it gets one
new snapshot whose sequence is the current high-water mark. Snapshot generation and event append use
the runtime's serialized scheduler, preventing snapshot/delta gaps.
Lifecycle
An owning runtime has a host-owned RemoteAgentSessionLease; each attached GUI has an independent RemoteAgentAttachmentLease. The runtime serializes attachment-count, retention-preference, and termination transitions under one lifecycle gate.
continue-in-background is an explicit persisted per-session preference, defaults to false, and is copied into every runtime snapshot. When the last viewer detaches and the value is false, the owner gracefully stops the runtime. When it is true, the runtime may continue with zero viewers until explicit termination, takeover, owner-host shutdown, or runtime-lease expiry.
Explicit detach, proxy disposal, UI tab close, and graceful viewer-application shutdown release that viewer immediately. Unexpected channel/transport loss instead reserves that logical attachment for a five-second reconnect grace period; reconnect with the same attachment token cancels expiry without changing the logical viewer count. When the grace expires, the attachment is released and the last-viewer rule runs.
A new attach racing with last-viewer stop is serialized by the lifecycle gate. If attachment reservation wins, stop is cancelled and attach receives the existing epoch. If fencing wins, that epoch accepts no attach or mutation; Attach returns the indistinguishable not-found result and StartOrAttach may create a fresh epoch only after terminal persistence and registry removal complete.
Multiple viewers are independent: removing any non-final viewer never stops the runtime. Setting continue-in-background to true preserves a zero-viewer runtime; setting it to false while viewers remain changes persistence and the snapshot but does not interrupt the run; setting it to false when the viewer count is already zero starts graceful stop immediately.
Owner-host application shutdown and runtime-lease expiry stop every runtime regardless of the preference. Viewer-application shutdown is only a graceful detach. A host crash cannot emit a terminal frame, but process containment kills owned children and recovery records the interrupted epoch as stopped before any new epoch starts; the persisted preference remains unchanged and does not itself imply auto-resume.
Graceful stop first fences the epoch so it accepts no new mutations, cancels/interrupts an active turn if needed, disposes AgentChat, component transports, MXC/process-executor leases, wrappers, stdio MCP transports, and contained child sessions/trees, persists the terminal completion and stopped state, then removes the registry entry. It emits exactly one session-terminal event after persistence and before closing attachment channels where the transport remains writable.
Interrupt cancels only the active run; terminate ends the owning runtime. These are distinct protocol verbs and UI actions.
Security and failure semantics
attach-agent-session authenticates the transport peer and authorizes that peer for the requested owning profile and session before revealing whether the session exists.
Attach authorization is evaluated on every initial attach, reconnect, child-subagent attach, mutation verb, and takeover request. Existing execution-target reachability alone is not sufficient proof of session access.
MXC does not authorize attach, and transport authorization does not sandbox processes.
A required containment compile, handoff, wrapper, or launch failure fails closed. No layer retries with a null policy or direct uncontained launch.
Host-local details remain in protected logs. Wire errors contain only a stable error code, safe operation category, retryability, user-safe message, and correlation id. They exclude policy JSON, grants, local paths, environment values, command arguments, stderr containing secrets, native handles, and credentials.
operation-error terminates the affected operation without necessarily terminating the owning session. A fatal runtime error additionally emits session-terminal.
MXC is a preview dependency and must not be described as a production-grade security boundary.
Add session attach, tools, subagents, modals, lifecycle, and replay to the existing chat-client
protocol.
Pros: reuses an existing adapter and framing.
Cons: overloads IChatClient, mixes per-run and whole-session lifetimes, and makes reconnect and
independent owning-runtime lifetime difficult.
Option B — Dedicated attach-agent-session protocol (chosen)
Add a session-oriented listener/client pair over existing message channels. Keep ChatClientOverTransport focused on the lower-level remote chat-client topology.
Pros: clean ownership, authorization, replay, lifecycle, and error boundaries; supports multiple
viewers and subagent channels.
Cons: adds a second protocol vocabulary and new proxy abstraction.
Option C — Entity-store mirror plus thin control channel
Read persisted history/tools/subagents through entity observation and use a channel only for live
state and control.
Pros: less data on the control channel.
Cons: creates two ordering domains and cannot reliably reconstruct unpersisted streaming, queue,
modal, and lifecycle state.
The dedicated protocol keeps per-run IChatClient lifetime distinct from persistent AgentChat
lifetime. Separate runtime and attachment leases make default final-viewer shutdown compatible with
a five-second reconnect grace. Reconnect reuses attachment token, epoch, and cursor and never
reconstructs the runtime. Takeover transfers persisted intent only; host-local confinement state is
recompiled. Wire failures remain sanitized while detailed diagnostics stay in protected host logs.
Implementation commit mapping
This issue is Commit 4 — Add authorization, host, and viewer lifecycle of the approved ten-commit
plan.
Files: property-based peer identity/authorization/host/runtime request records, peer identity
provider, attach authorizer, listener, host, runtime registry, runtime and attachment leases,
replay buffer, and transport composition registration.
Tests: required-member/default/named-initializer mapping for all five non-wire internal request
types, listener, authorizer, host, registry, replay limits, multiple viewers, sanitized errors, and
no existence disclosure, including default final-viewer stop, persisted background retention,
five-second reconnect grace, attach/stop races, explicit-terminate precedence, complete graceful
cleanup, queue convergence, and reconnect snapshot/replay.
Part of #1483
Summary
Implement authenticated attach authorization, host dispatch, owner-authoritative non-optimistic queues, runtime and attachment leases, replay, and default final-viewer shutdown with five-second unexpected-loss grace; persisted Continue in background can retain the runtime.
Scope
Implement only commit 4 of the approved design represented by commit
0c6578641befad31d5a38003baf6c0f0fe64748a. Preserve the contracts belowexactly; local and remote behavior must remain compatible.
This is the host/runtime request-API implementation item. The complete audited design contains
28 request/options types: 22 public and 6 internal. This child owns the five non-wire internal
request types used by authorization, host dispatch, runtime termination/retention, and attachment.
It consumes, but does not redefine ownership of, the property-based protocol and persisted-intent
records from its dependencies.
API shape convention
*Requestor*Optionsrecord plusan optional
CancellationToken. Semantically required members arerequired init; optionalmembers have explicit defaults.
overrides retain inherited signatures.
unknown-member rejection, and all existing wire semantics.
InterruptAgentSessionRequest. The client API remains exactlyTask InterruptAsync(Guid commandId, CancellationToken ct = default).Shared protocol state forms
The host snapshot/event path uses the exact property-based state forms below:
Usagepermits null for a metric the provider did not report; otherwise counts and USD cost arenonnegative.
AgentInformationrequires nonnull, nonblank identity/display strings, a null-or-nonblank model id, and a complete nonnull definition. Authorization completes before runtime lookup,
snapshot construction, or definition serialization. Authorized viewers receive the same complete
definition; unauthorized peers receive no definition bytes, metadata, or existence signal.
Files
Files: peer identity provider, attach authorizer, listener, host, runtime registry, runtime and
attachment leases, replay buffer, transport composition registration.
Tests: listener, authorizer, host, registry, replay limits, multiple viewers, sanitized errors, and
no existence disclosure, including default final-viewer stop, persisted background retention,
five-second reconnect grace, attach/stop races, explicit-terminate precedence, complete graceful
cleanup, queue convergence, and reconnect snapshot/replay.
Detailed design
AgentSessionTransportListener- NewPhantom.Workspaces.Services.AgentSessions;Phantom.Workspaces/Services/AgentSessions/AgentSessionTransportListener.cs.public sealed class : ITransportListener.the attachment lease supplied by the host.
OnChannelOpenAsyncreturns null for a discriminator other thanattach-agent-session. For a match,it uses strict deserialization, obtains authenticated peer identity, and calls the host. Validation
or authorization failures write one sanitized terminal error and close; authorization occurs before
runtime lookup.
OnStreamOpenAsyncalways returns null. Disposal stops accepting channels andreleases listener-owned active attachment leases. It does not directly dispose runtime leases, but
final attachment release applies the normal background policy; owner-host application shutdown then
calls
RemoteAgentSessionHost.DisposeAsync, which fences and stops every remaining runtimeregardless of that policy.
Attach identity and authorization - New
TransportPeerIdentity.csandITransportPeerIdentityProvider.csremain inPhantom.Workspaces.Transport;IAgentSessionAttachAuthorizer.csis inPhantom.Workspaces/Services/AgentSessions.public sealed record TransportPeerIdentity; twointernal interfaces.provider. Anonymous or ambiguous channels have no identity and fail closed.
TransportPeerIdentityrejects blank authentication scheme/stable peer id. Optional entity ids areeither null or nonblank canonical entity ids; these values are authenticated claims and are not
serialized by the attach protocol.
GetRequiredIdentityreturns only transport-authenticated claims, never request fields, and throws asanitized unauthenticated error when absent. The concrete internal
AgentSessionAttachAuthorizer(IDataAccessLayer)loads the session's owninguser-computer-profile, resolves that profile's user, and allows attach/mutation only when:(a) request owner and generation exactly match persistence; (b)
peer.UserEntityIdequals theowning profile's user entity id; and (c) the authenticated transport's
UserComputerProfileEntityId, when present, resolves to that same user. Takeover additionallyrequires the proposed new profile to resolve to the same user. Child attach additionally requires
the child id to be in the owning runtime's subagent registry. There is no caller-authored ACL or
request-supplied identity. Authorization runs for open/reconnect and every mutation. Denial is
indistinguishable from nonexistent session on the wire. Cancellation performs no mutation. The
authorizer is stateless and process-scoped.
Consumed protocol request values
The host consumes the exact strict property-based protocol values below from #1486:
These retain version-1 kebab-case serialization, strict unknown-member rejection, and fixed protocol
semantics. They carry stable identity and intent only, never authenticated peer claims, compiled
policy, local paths, process handles, or credentials.
RemoteAgentSessionHost- NewPhantom.Workspaces.Services.AgentSessions;Phantom.Workspaces/Services/AgentSessions/RemoteAgentSessionHost.cs.internal sealed class.persistence/runtime hydration, the runtime registry, and a transport channel.
GetStatusAsyncaccepts onlyOpenIntent.Status, authorizes before lookup, and exposes no otherruntime state.
OpenAsyncrejectsStatus, authorizes before lookup, validates owner/generation,then atomically gets or starts the
runtime according to
OpenIntent;Attachnever starts,Startfails if already running, andStartOrAttach/Resumereuse the matching runtime. It creates the attachment before taking asnapshot so no delta is lost, then emits snapshot-or-replay.
TakeOverAsyncauthorizes takeover,fences mutations, awaits old runtime disposal, compare/exchanges owner/generation, and starts nothing
until persistence succeeds. A live unconfirmed lease returns
takeover-blocked. Host disposal drainsattachments then runtime leases. No method accepts a compiled policy.
The ownership lease is a persisted compare/exchange record keyed by session and generation. It uses
the data service's authoritative timestamp, is renewed every 10 seconds, and expires after 30 seconds.
Each successful renewal returns the authoritative expiry instant. If renewal fails or is uncertain,
the owner retries once per second but fences the runtime no later than five seconds before the last
confirmed expiry; after fencing it accepts no mutation and performs graceful stop. It never assumes
renewal from a local clock or transient response. Orderly stop releases the lease only after
terminal/stopped persistence; a crashed host cannot renew it.
After expiry, restart/takeover first marks the abandoned epoch stopped, then creates a new epoch.
continue-in-backgroundsurvives that recovery but is not an auto-resume instruction: only anordinary open or the existing
auto-resumeentity setting starts the replacement runtime.Runtime and attachment storage - New
Phantom.Workspaces.Services.AgentSessions;IRemoteAgentSessionRuntimeRegistry.cs,RemoteAgentSessionRuntimeRegistry.cs,RemoteAgentSessionLease.cs,RemoteAgentAttachmentLease.cs,AgentSessionReplayBuffer.cs.internal; epoch/cursor valuetypes are public protocol values.
(AgentSessionId, OwnershipGeneration). The lease owns execution resources; its lifecycle gatealso tracks logical attachment count and the persisted background preference.
GetOrStartAsyncis single-flight and returns the existing matching lease; a failed/cancelled factoryis removed.
TryTerminateAsyncsucceeds only on exact generation+epoch, marks the registry entryfenced in place first, then
performs graceful stop once; explicit terminate always wins over attach, reconnect, grace, or
preference changes. Runtime stop rejects new mutations, interrupts an active turn when necessary,
unsubscribes the local event bridge, disposes
AgentChat, component transports, MXC/process-executorleases, wrappers, stdio MCP transports, and contained children, persists terminal/stopped state,
emits terminal once before writable channels close, and only then removes the registry entry. A
fenced entry rejects attach/start so no replacement epoch can appear before cleanup completes.
Attachsubscribes before snapshot capture and increments the logical viewer count under the samelifecycle gate. Explicit attachment disposal decrements immediately. Unexpected channel loss marks
the attachment token disconnected and starts one
TimeProvider-driven five-second timer; reconnectwith that token cancels the timer and keeps the count unchanged. Timer expiry releases the viewer.
When a release changes the count to zero and
ContinueInBackgroundis false, graceful stop starts.SetContinueInBackgroundAsyncvalidates the exact generation/epoch, persists first, then updates theruntime and emits
SessionRetentionChangedEvent; setting false at zero viewers starts stop in thesame serialized transition. Attach, release, grace expiry, and preference updates emit an ordered
SessionRetentionChangedEventwhenever either authoritative value changes; snapshots carry bothvalues. A later attach cannot cancel a fenced stop.
Named initializer examples
The child-owned request records are constructed by name, never with transposable positional values:
The strict protocol open descriptor consumed here is likewise property-based:
AgentSessionReplayBufferretains the newest 4,096 events subject to an 8 MiB serialized-size capand a 15-minute age cap. Append and cursor reads are locked and sequence-monotonic. A reconnect gets
replay only when epoch matches and every sequence after its cursor is retained; otherwise it gets one
new snapshot whose sequence is the current high-water mark. Snapshot generation and event append use
the runtime's serialized scheduler, preventing snapshot/delta gaps.
Lifecycle
RemoteAgentSessionLease; each attached GUI has an independentRemoteAgentAttachmentLease. The runtime serializes attachment-count, retention-preference, and termination transitions under one lifecycle gate.continue-in-backgroundis an explicit persisted per-session preference, defaults tofalse, and is copied into every runtime snapshot. When the last viewer detaches and the value isfalse, the owner gracefully stops the runtime. When it istrue, the runtime may continue with zero viewers until explicit termination, takeover, owner-host shutdown, or runtime-lease expiry.Attachreturns the indistinguishable not-found result andStartOrAttachmay create a fresh epoch only after terminal persistence and registry removal complete.continue-in-backgroundtotruepreserves a zero-viewer runtime; setting it tofalsewhile viewers remain changes persistence and the snapshot but does not interrupt the run; setting it tofalsewhen the viewer count is already zero starts graceful stop immediately.AgentChat, component transports, MXC/process-executor leases, wrappers, stdio MCP transports, and contained child sessions/trees, persists the terminal completion and stopped state, then removes the registry entry. It emits exactly onesession-terminalevent after persistence and before closing attachment channels where the transport remains writable.Security and failure semantics
attach-agent-sessionauthenticates the transport peer and authorizes that peer for the requested owning profile and session before revealing whether the session exists.operation-errorterminates the affected operation without necessarily terminating the owning session. A fatal runtime error additionally emitssession-terminal.Tests
AgentSessionTransportListenerTests(Phantom.Workspaces.Tests)OnChannelOpenAsync_OtherType_ReturnsNull.OnChannelOpenAsync_ValidAttach_ReturnsAttachmentLease.OnChannelOpenAsync_MalformedRequest_WritesSanitizedTerminalError.OnChannelOpenAsync_UnauthenticatedChannel_DoesNotLookupSession.OnChannelOpenAsync_UnauthorizedPeer_DoesNotRevealSessionExistence.OnStreamOpenAsync_AnyRequest_ReturnsNull.DisposeAsync_ActiveAttachments_ReleasesViewersThenHostStopsAllRuntimes.Property/request-shape coverage:
Usage_DefaultInitialization_AllOptionalMetricsAreNull.Usage_NamedInitializer_PreservesExactMetricTypes.AgentInformation_RequiredInitProperties_AreMarkedRequired.AgentInformation_NamedInitializer_PreservesAllFields.TransportPeerIdentity_BlankAuthenticatedIdentity_RejectsInitialization.OpenAgentSessionHostRequest_NamedInitializer_MapsPeerOpenRequestAndChannel.RuntimeRequestTypes_RequiredMembersDefaultsAndNamedInitializers_MapToLifecycleOperations.AgentSessionAuthorizationRequest_RequiredMembersDefaultsAndNamedInitializer_MapToAuthorization.RemoteAgentSessionHostTests:OpenAsync_Start_CreatesOneRuntimeAndSnapshot.OpenAsync_AttachMissingRuntime_ReturnsIndistinguishableNotFound.OpenAsync_StartOrAttachConcurrent_CreatesOneRuntime.GetStatusAsync_AuthorizedPeer_ReturnsRunningOrNotRunningWithoutAttaching.GetStatusAsync_UnauthorizedPeer_ReturnsUnavailableWithoutRuntimeLookup.OpenAsync_ReconnectCoveredCursor_ReplaysWithoutSnapshotOrRelaunch.OpenAsync_ReconnectExpiredCursor_SendsSnapshotWithoutRelaunch.OpenAsync_ChildSubagent_ReauthorizesMembership.Command_DuplicateIdSamePayload_ReturnsCachedResultWithoutMutation.Command_DuplicateIdDifferentPayload_ReturnsConflict.QueueCommand_StaleExpectedRevision_ReturnsConflictWithoutMutation.QueueCommand_InvalidQueueOrItem_ReturnsRejectedWithoutDelta.SetToolEnabledCommand_EachMutation_ReauthorizesAndBroadcastsToolsEvent.QueueCommand_Applied_BroadcastsOneOrderedDeltaToEveryViewer.QueueCommand_Applied_TaskCompletesAfterAuthoritativeRevisionApplied.QueueConsumption_CurrentRun_BroadcastsOwnerRevisionAfterEnqueue.Command_EachMutation_ReauthorizesPeer.TakeOverAsync_ConfirmedOldTermination_AdvancesGenerationThenStartsNewEpoch.TakeOverAsync_UnconfirmedLiveLease_FailsClosed.OwnershipLease_RenewalEveryTenSeconds_ExtendsThirtySecondExpiry.OwnershipLease_RenewalUncertain_FencesBeforeSafetyDeadline.OwnershipLease_HostCrashExpires_RecoveryMarksOldEpochStoppedBeforeRestart.DisposeAsync_HostShutdown_DisposesAllRuntimeTrees.OpenAsync_AttachRacesLastViewerStop_WinnerDeterminesExistingOrFreshEpoch.OpenAsync_ReconnectAfterGrace_ReturnsNotFoundWithoutStartingRuntime.OpenAsync_UnauthorizedPeer_DoesNotSerializeDefinitionOrSessionMetadata.SetContinueInBackgroundAsync_ZeroViewersFalse_StopsImmediately.RemoteAgentSessionRuntimeRegistryTests:GetOrStartAsync_ConcurrentCallers_StartsFactoryOnce.GetOrStartAsync_CancelledFactory_RemovesFailedEntry.TryGetAsync_WrongGeneration_ReturnsNull.TryTerminateAsync_ExactEpoch_FencesThenDisposesOnce.TryTerminateAsync_StaleEpoch_ReturnsFalse.Attach_MultipleViewers_UsesIndependentAttachmentLeases.AttachmentDispose_NonFinalViewer_DoesNotDisposeRuntime.AttachmentDispose_LastViewer_DefaultPolicy_DisposesRuntime.AttachmentDispose_LastViewer_BackgroundEnabled_PreservesRuntime.TransportLoss_ReconnectWithinFiveSeconds_ReusesAttachmentAndEpoch.TransportLoss_GraceExpiresAsLastViewer_DefaultPolicy_DisposesRuntimeAndChildren.TransportLoss_GraceExpiresAsLastViewer_BackgroundEnabled_PreservesRuntimeAndChildren.SetContinueInBackground_TrueWithViewers_PersistsWithoutStopping.SetContinueInBackground_FalseWithViewers_PersistsWithoutStopping.SetContinueInBackground_FalseAtZeroViewers_StopsImmediately.TryTerminateAsync_ConcurrentAttachOrPreferenceChange_TerminateWins.RuntimeDispose_ActiveAttachments_DisposesChildrenPersistsThenEmitsOneTerminal.RuntimeDispose_ActiveTurn_FencesInterruptsPersistsTerminalThenClosesChannels.HostCrash_Restart_RecordsInterruptedEpochStoppedAndPreservesPreference.ReplayBuffer_Over4096Events_DropsOldest.ReplayBuffer_Over8MiB_DropsOldest.ReplayBuffer_Over15Minutes_ForcesSnapshotFallback.AgentSessionAttachAuthorizerTests:AuthorizeAsync_OwnerPeerAllowed_ReturnsAllow.AuthorizeAsync_UnrelatedPeerDenied_ReturnsIndistinguishableDenial.AuthorizeAsync_MutationAfterAclChange_DeniesPreviouslyAttachedPeer.AuthorizeAsync_TakeoverWithoutOwnerPermission_Denies.AuthorizeAsync_Cancelled_PerformsNoRuntimeLookupOrMutation.Considered / Background
Option A — Extend
ChatClientOverTransportAdd session attach, tools, subagents, modals, lifecycle, and replay to the existing
chat-clientprotocol.
Pros: reuses an existing adapter and framing.
Cons: overloads
IChatClient, mixes per-run and whole-session lifetimes, and makes reconnect andindependent owning-runtime lifetime difficult.
Option B — Dedicated
attach-agent-sessionprotocol (chosen)Add a session-oriented listener/client pair over existing message channels. Keep
ChatClientOverTransportfocused on the lower-level remote chat-client topology.Pros: clean ownership, authorization, replay, lifecycle, and error boundaries; supports multiple
viewers and subagent channels.
Cons: adds a second protocol vocabulary and new proxy abstraction.
Option C — Entity-store mirror plus thin control channel
Read persisted history/tools/subagents through entity observation and use a channel only for live
state and control.
Pros: less data on the control channel.
Cons: creates two ordering domains and cannot reliably reconstruct unpersisted streaming, queue,
modal, and lifecycle state.
The dedicated protocol keeps per-run
IChatClientlifetime distinct from persistentAgentChatlifetime. Separate runtime and attachment leases make default final-viewer shutdown compatible with
a five-second reconnect grace. Reconnect reuses attachment token, epoch, and cursor and never
reconstructs the runtime. Takeover transfers persisted intent only; host-local confinement state is
recompiled. Wire failures remain sanitized while detailed diagnostics stay in protected host logs.
Implementation commit mapping
This issue is Commit 4 — Add authorization, host, and viewer lifecycle of the approved ten-commit
plan.
provider, attach authorizer, listener, host, runtime registry, runtime and attachment leases,
replay buffer, and transport composition registration.
types, listener, authorizer, host, registry, replay limits, multiple viewers, sanitized errors, and
no existence disclosure, including default final-viewer stop, persisted background retention,
five-second reconnect grace, attach/stop races, explicit-terminate precedence, complete graceful
cleanup, queue convergence, and reconnect snapshot/replay.
Hierarchy and dependencies
[RemoteChat] - Design: remote agent sessionschanging those issues' dependency metadata.