@@ -16,6 +16,12 @@ import {
1616import type { PlatformRequestScope } from '@agent-device/contracts/platform-runtime-host' ;
1717import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations' ;
1818import { ensureDeviceReady , type DeviceReadyOptions } from './device-ready.ts' ;
19+ import type {
20+ ManagedRequestAdmission ,
21+ ResolveManagedRequestLease ,
22+ } from './managed-device-allocation/request-admission.ts' ;
23+
24+ const managedReadiness = new WeakMap < BoundDeviceIdentity , ( ) => Promise < void > > ( ) ;
1925
2026export type BindDeviceRuntime = <
2127 const Required extends readonly RuntimeOperationKey < PlatformRuntimeOperations > [ ] ,
@@ -75,20 +81,26 @@ export type BoundDeviceIdentity = Readonly<{
7581 owner : RuntimeOwnerRef ;
7682} > ;
7783
78- /** Runs legacy local readiness only after the request has crossed the binding/ claim fence . */
84+ /** Confirms managed authority or runs local readiness after binding and claim admission . */
7985export async function ensureBoundDeviceReady (
8086 bound : BoundDeviceIdentity ,
8187 options : DeviceReadyOptions = { } ,
8288) : Promise < void > {
8389 switch ( bound . owner . kind ) {
8490 case 'provider-runtime' :
8591 return ;
86- case 'managed-local' :
92+ case 'managed-local' : {
93+ const ready = managedReadiness . get ( bound ) ;
94+ if ( ready ) {
95+ await ready ( ) ;
96+ return ;
97+ }
8798 throw new AppError (
8899 'UNSUPPORTED_OPERATION' ,
89100 'Managed-device readiness is unavailable until allocator confirmation.' ,
90101 { reason : 'managed-readiness-unavailable' } ,
91102 ) ;
103+ }
92104 case 'local-family' :
93105 await ensureDeviceReady ( bound . device , options ) ;
94106 }
@@ -101,29 +113,19 @@ export type RequestRuntimeBindings = AsyncDisposable &
101113 bindExactDevice : BindExactDeviceRuntime ;
102114 } > ;
103115
104- /**
105- * Private broad-binding cache; handlers receive only the selected projection.
106- *
107- * `admitDeviceClaim` is the #1320 claim gate, and it runs as part of creating a
108- * binding, so the per-device cache below is also what makes it run once per
109- * device. Binding performs no device mutation — it composes the operation
110- * catalog — so a binding that has not been admitted is the last state before any
111- * device operation exists, and admitting here covers every handler by
112- * construction. A refusal rejects the cached promise, so a second `bindDevice`
113- * for the same device re-attempts rather than inheriting a rejected binding.
114- * The gate receives the very intent the gateway bound, so an exact-owner fence
115- * reaches claim admission unchanged.
116- */
116+ /** Owns request runtime bindings while exposing only the requested operation projection. */
117117export function createRequestRuntimeBindings ( params : {
118118 gateway : DeviceRuntimeGateway < PlatformRuntimeOperations > ;
119119 scope : PlatformRequestScope ;
120+ resolveManagedLease ?: ResolveManagedRequestLease ;
120121 admitDeviceClaim : (
121122 device : DeviceInfo ,
122123 owner : RuntimeOwnerRef ,
123124 intent : DeviceBindingIntent ,
124125 ) => Promise < void > ;
125126} ) : RequestRuntimeBindings {
126127 const cleanups = new AsyncCleanupStack ( ) ;
128+ const managedLifetime = new AbortController ( ) ;
127129 const bindings = new Map < string , Promise < DeviceBinding < PlatformRuntimeOperations > > > ( ) ;
128130
129131 const admitBinding = async (
@@ -151,19 +153,40 @@ export function createRequestRuntimeBindings(params: {
151153 return narrowDeviceBinding ( await bindingPromise , use ) ;
152154 } ;
153155
154- // Exact-owner bindings deliberately bypass the cache, so they admit their own.
155156 const bindExactDevice : BindExactDeviceRuntime = async ( device , owner , fence , use , scope ) => {
156157 const intent : DeviceBindingIntent = { kind : 'exact-owner' , owner, fence } ;
157- const published = await params . gateway . bind ( { device, intent, scope } ) ;
158- const binding = await admitBinding ( await adoptExactBinding ( cleanups , published , scope ) , intent ) ;
159- return narrowDeviceBinding ( binding , use ) ;
158+ let managed : ManagedRequestAdmission | undefined ;
159+ if ( owner . kind === 'managed-local' ) {
160+ const { createManagedRequestAdmission } =
161+ await import ( './managed-device-allocation/request-admission.ts' ) ;
162+ managed = createManagedRequestAdmission ( {
163+ device,
164+ intent,
165+ scope,
166+ lifetime : managedLifetime . signal ,
167+ resolve : params . resolveManagedLease ,
168+ } ) ;
169+ }
170+ if ( managed ) await params . admitDeviceClaim ( device , owner , intent ) ;
171+ const published = managed
172+ ? await managed . bind ( ( ) => params . gateway . bind ( { device, intent, scope : managed . scope } ) )
173+ : await params . gateway . bind ( { device, intent, scope } ) ;
174+ const adopted = await adoptExactBinding ( cleanups , published , scope ) ;
175+ const binding = managed ? adopted : await admitBinding ( adopted , intent ) ;
176+ const bound = narrowDeviceBinding ( binding , use ) ;
177+ managed ?. activate ( ) ;
178+ if ( managed ) managedReadiness . set ( bound , managed . ensureReady ) ;
179+ return bound ;
160180 } ;
161181
162182 return {
163183 inspectFacts : async ( device ) => await params . gateway . inspectFacts ( device ) ,
164184 bindDevice,
165185 bindExactDevice,
166- [ Symbol . asyncDispose ] : async ( ) => await cleanups [ Symbol . asyncDispose ] ( ) ,
186+ [ Symbol . asyncDispose ] : async ( ) => {
187+ managedLifetime . abort ( ) ;
188+ await cleanups [ Symbol . asyncDispose ] ( ) ;
189+ } ,
167190 } ;
168191}
169192
0 commit comments