@dsh-enhanced/assistant-delivery 0.1.14 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +3 -0
- package/lib/agent-runtime.js.map +1 -1
- package/lib/service.d.ts +16 -0
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +400 -1
- package/lib/service.js.map +1 -1
- package/lib/types.d.ts +24 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +17 -7
package/lib/service.js
CHANGED
|
@@ -361,6 +361,82 @@ function currentPermissionEvents(agent) {
|
|
|
361
361
|
function signalAborted(signal) {
|
|
362
362
|
return signal?.aborted === true;
|
|
363
363
|
}
|
|
364
|
+
const userQuestionTombstoneLimit = 1_024;
|
|
365
|
+
function isUserQuestionApiProxy(value) {
|
|
366
|
+
if (value === null || typeof value !== 'object')
|
|
367
|
+
return false;
|
|
368
|
+
const candidate = value;
|
|
369
|
+
return candidate.events !== null && typeof candidate.events === 'object'
|
|
370
|
+
&& typeof candidate.events.mux === 'function'
|
|
371
|
+
&& typeof candidate.respond === 'function';
|
|
372
|
+
}
|
|
373
|
+
function requestedUserQuestionFrame(value) {
|
|
374
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
375
|
+
return undefined;
|
|
376
|
+
const envelope = value;
|
|
377
|
+
if (typeof envelope.rpcId !== 'string' || envelope.rpcId.length === 0
|
|
378
|
+
|| envelope.payload === null || typeof envelope.payload !== 'object'
|
|
379
|
+
|| Array.isArray(envelope.payload))
|
|
380
|
+
return undefined;
|
|
381
|
+
const payload = envelope.payload;
|
|
382
|
+
if (payload.type !== 'question/requested' || typeof payload.sessionId !== 'string'
|
|
383
|
+
|| payload.sessionId.length === 0 || !Array.isArray(payload.questions))
|
|
384
|
+
return undefined;
|
|
385
|
+
return {
|
|
386
|
+
rpcId: envelope.rpcId,
|
|
387
|
+
sessionId: payload.sessionId,
|
|
388
|
+
questions: Object.freeze([...payload.questions]),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function resolvedUserQuestionFrame(value) {
|
|
392
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
393
|
+
return undefined;
|
|
394
|
+
const envelope = value;
|
|
395
|
+
if (envelope.payload === null || typeof envelope.payload !== 'object' || Array.isArray(envelope.payload)) {
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
const payload = envelope.payload;
|
|
399
|
+
if (payload.type !== 'question/resolved' || typeof payload.questionRpcId !== 'string'
|
|
400
|
+
|| payload.questionRpcId.length === 0
|
|
401
|
+
|| (payload.outcome !== 'answered' && payload.outcome !== 'cancelled'))
|
|
402
|
+
return undefined;
|
|
403
|
+
return { questionRpcId: payload.questionRpcId };
|
|
404
|
+
}
|
|
405
|
+
function userQuestionRequestHash(sessionId, questions) {
|
|
406
|
+
try {
|
|
407
|
+
const encoded = JSON.stringify([sessionId, questions]);
|
|
408
|
+
return typeof encoded === 'string' ? createHash('sha256').update(encoded).digest('hex') : undefined;
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function cancelledUserQuestionResponse(rpcId) {
|
|
415
|
+
return {
|
|
416
|
+
type: 'client-response',
|
|
417
|
+
rpcId,
|
|
418
|
+
result: {
|
|
419
|
+
ok: false,
|
|
420
|
+
error: {
|
|
421
|
+
code: 'cancelled',
|
|
422
|
+
message: 'the user cancelled ask_user_question',
|
|
423
|
+
details: {},
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function sameUserQuestionAuthority(left, right) {
|
|
429
|
+
return right !== undefined
|
|
430
|
+
&& right.adapter === left.adapter
|
|
431
|
+
&& right.binding.id === left.binding.id
|
|
432
|
+
&& right.binding.version === left.binding.version
|
|
433
|
+
&& right.binding.generation === left.binding.generation
|
|
434
|
+
&& right.binding.sessionId === left.binding.sessionId
|
|
435
|
+
&& right.ownerId === left.ownerId
|
|
436
|
+
&& right.ownerVersion === left.ownerVersion
|
|
437
|
+
&& JSON.stringify(right.binding.conversation) === JSON.stringify(left.binding.conversation)
|
|
438
|
+
&& JSON.stringify(right.binding.principal) === JSON.stringify(left.binding.principal);
|
|
439
|
+
}
|
|
364
440
|
function sameToolApprovalAuthority(left, right) {
|
|
365
441
|
return right !== undefined
|
|
366
442
|
&& right.adapter === left.adapter
|
|
@@ -444,6 +520,8 @@ export class AssistantDeliveryService extends Service {
|
|
|
444
520
|
conversationTransitions = new Map();
|
|
445
521
|
agentApprovalBindings = new WeakMap();
|
|
446
522
|
toolApprovalControllers = new Set();
|
|
523
|
+
drainingUserQuestionAdapters = new WeakSet();
|
|
524
|
+
userQuestionBridge;
|
|
447
525
|
modelSelectionWaiters = new Map();
|
|
448
526
|
preferenceFeedbackSink;
|
|
449
527
|
evaluationSink;
|
|
@@ -596,6 +674,15 @@ export class AssistantDeliveryService extends Service {
|
|
|
596
674
|
});
|
|
597
675
|
ctx.inject(['approval'], approvalCtx => approvalCtx.on('approval/request', (request, next) => this.requestToolApproval(approvalCtx, request, next)));
|
|
598
676
|
ctx.inject(['tools'], toolsCtx => registerDeliveryTools(toolsCtx, this));
|
|
677
|
+
// ApiProxy owns the only `userQuestions` provider. Delivery merely bridges
|
|
678
|
+
// its server-request mux frames to an authenticated channel adapter and
|
|
679
|
+
// echoes a client response; it must never register a competing provider.
|
|
680
|
+
ctx.inject(['apiProxy'], apiCtx => {
|
|
681
|
+
const apiProxy = apiCtx.apiProxy;
|
|
682
|
+
if (!isUserQuestionApiProxy(apiProxy))
|
|
683
|
+
return;
|
|
684
|
+
return this.attachUserQuestionApiProxy(apiProxy);
|
|
685
|
+
});
|
|
599
686
|
// Presentation writes are intentionally reverse-bound. Delivery creates
|
|
600
687
|
// the publisher closure and hands it only to the exact live domain service;
|
|
601
688
|
// a public service-slot caller can no longer forge a terminal projection.
|
|
@@ -630,6 +717,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
630
717
|
this.automationPresentationBinding?.dispose();
|
|
631
718
|
this.evolutionPresentationBinding?.dispose();
|
|
632
719
|
this.cancelModelSelectionWaiters();
|
|
720
|
+
this.stopUserQuestionBridge(new Error('assistant-delivery is stopping'));
|
|
633
721
|
for (const controller of this.toolApprovalControllers) {
|
|
634
722
|
controller.abort(new Error('assistant-delivery is stopping'));
|
|
635
723
|
}
|
|
@@ -1131,7 +1219,318 @@ export class AssistantDeliveryService extends Service {
|
|
|
1131
1219
|
}
|
|
1132
1220
|
registerAdapter(adapter) {
|
|
1133
1221
|
this.assertActive();
|
|
1134
|
-
return this.registry.register(adapter)
|
|
1222
|
+
return this.registry.register(adapter).then(unregister => {
|
|
1223
|
+
this.drainingUserQuestionAdapters.delete(adapter);
|
|
1224
|
+
this.dispatchPendingUserQuestions();
|
|
1225
|
+
let registered = true;
|
|
1226
|
+
return async () => {
|
|
1227
|
+
if (!registered)
|
|
1228
|
+
return;
|
|
1229
|
+
registered = false;
|
|
1230
|
+
this.drainingUserQuestionAdapters.add(adapter);
|
|
1231
|
+
this.abortUserQuestionAttemptsForAdapter(adapter, new Error('delivery adapter is unregistering'));
|
|
1232
|
+
await unregister();
|
|
1233
|
+
this.dispatchPendingUserQuestions();
|
|
1234
|
+
};
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
attachUserQuestionApiProxy(proxy) {
|
|
1238
|
+
this.stopUserQuestionBridge(new Error('ApiProxy question bridge replaced'));
|
|
1239
|
+
if (!this.active)
|
|
1240
|
+
return () => { };
|
|
1241
|
+
const bridge = {
|
|
1242
|
+
proxy,
|
|
1243
|
+
controller: new AbortController(),
|
|
1244
|
+
pending: new Map(),
|
|
1245
|
+
completed: new Set(),
|
|
1246
|
+
};
|
|
1247
|
+
this.userQuestionBridge = bridge;
|
|
1248
|
+
void this.consumeUserQuestionMux(bridge);
|
|
1249
|
+
let attached = true;
|
|
1250
|
+
return () => {
|
|
1251
|
+
if (!attached)
|
|
1252
|
+
return;
|
|
1253
|
+
attached = false;
|
|
1254
|
+
this.stopUserQuestionBridge(new Error('ApiProxy question bridge disposed'), bridge);
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
stopUserQuestionBridge(reason, expected) {
|
|
1258
|
+
const bridge = expected ?? this.userQuestionBridge;
|
|
1259
|
+
if (bridge === undefined || this.userQuestionBridge !== bridge)
|
|
1260
|
+
return;
|
|
1261
|
+
this.userQuestionBridge = undefined;
|
|
1262
|
+
bridge.controller.abort(reason);
|
|
1263
|
+
for (const pending of bridge.pending.values()) {
|
|
1264
|
+
const attempt = pending.attempt;
|
|
1265
|
+
attempt?.detachBridgeAbort();
|
|
1266
|
+
attempt?.controller.abort(reason);
|
|
1267
|
+
delete pending.attempt;
|
|
1268
|
+
}
|
|
1269
|
+
bridge.pending.clear();
|
|
1270
|
+
bridge.completed.clear();
|
|
1271
|
+
}
|
|
1272
|
+
async consumeUserQuestionMux(bridge) {
|
|
1273
|
+
try {
|
|
1274
|
+
const stream = bridge.proxy.events.mux({
|
|
1275
|
+
rpcId: randomUUID(),
|
|
1276
|
+
payload: {},
|
|
1277
|
+
}, bridge.controller.signal);
|
|
1278
|
+
if (stream === null || typeof stream !== 'object'
|
|
1279
|
+
|| typeof stream[Symbol.asyncIterator] !== 'function')
|
|
1280
|
+
return;
|
|
1281
|
+
for await (const frame of stream) {
|
|
1282
|
+
if (!this.active || this.userQuestionBridge !== bridge || bridge.controller.signal.aborted)
|
|
1283
|
+
return;
|
|
1284
|
+
const resolved = resolvedUserQuestionFrame(frame);
|
|
1285
|
+
if (resolved !== undefined) {
|
|
1286
|
+
this.resolvePendingUserQuestion(bridge, resolved.questionRpcId);
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
const requested = requestedUserQuestionFrame(frame);
|
|
1290
|
+
if (requested !== undefined)
|
|
1291
|
+
this.requestUserQuestionFromMux(bridge, requested);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
catch {
|
|
1295
|
+
// ApiProxy is an in-process service. If its stream is torn down, its
|
|
1296
|
+
// Cordis injection disposer will replace this bridge; never leak a
|
|
1297
|
+
// rejected iterator into the Host lifecycle.
|
|
1298
|
+
}
|
|
1299
|
+
finally {
|
|
1300
|
+
if (this.userQuestionBridge === bridge && !bridge.controller.signal.aborted) {
|
|
1301
|
+
this.stopUserQuestionBridge(new Error('ApiProxy question mux ended'), bridge);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
requestUserQuestionFromMux(bridge, frame) {
|
|
1306
|
+
if (bridge.completed.has(frame.rpcId))
|
|
1307
|
+
return;
|
|
1308
|
+
const requestHash = userQuestionRequestHash(frame.sessionId, frame.questions);
|
|
1309
|
+
if (requestHash === undefined)
|
|
1310
|
+
return;
|
|
1311
|
+
const existing = bridge.pending.get(frame.rpcId);
|
|
1312
|
+
if (existing !== undefined) {
|
|
1313
|
+
if (existing.sessionId !== frame.sessionId || existing.requestHash !== requestHash) {
|
|
1314
|
+
this.queueUserQuestionResponse(bridge, existing, cancelledUserQuestionResponse(existing.rpcId));
|
|
1315
|
+
}
|
|
1316
|
+
else {
|
|
1317
|
+
this.dispatchUserQuestion(bridge, existing);
|
|
1318
|
+
}
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
const pending = {
|
|
1322
|
+
rpcId: frame.rpcId,
|
|
1323
|
+
sessionId: frame.sessionId,
|
|
1324
|
+
questions: frame.questions,
|
|
1325
|
+
requestHash,
|
|
1326
|
+
state: 'waiting',
|
|
1327
|
+
responseAttempts: 0,
|
|
1328
|
+
};
|
|
1329
|
+
bridge.pending.set(pending.rpcId, pending);
|
|
1330
|
+
this.dispatchUserQuestion(bridge, pending);
|
|
1331
|
+
}
|
|
1332
|
+
resolvePendingUserQuestion(bridge, rpcId) {
|
|
1333
|
+
this.markUserQuestionCompleted(bridge, rpcId);
|
|
1334
|
+
const pending = bridge.pending.get(rpcId);
|
|
1335
|
+
if (pending === undefined)
|
|
1336
|
+
return;
|
|
1337
|
+
bridge.pending.delete(rpcId);
|
|
1338
|
+
const attempt = pending.attempt;
|
|
1339
|
+
attempt?.detachBridgeAbort();
|
|
1340
|
+
attempt?.controller.abort(new Error('ApiProxy question request resolved'));
|
|
1341
|
+
delete pending.attempt;
|
|
1342
|
+
}
|
|
1343
|
+
markUserQuestionCompleted(bridge, rpcId) {
|
|
1344
|
+
if (bridge.completed.size >= userQuestionTombstoneLimit) {
|
|
1345
|
+
const oldest = bridge.completed.values().next().value;
|
|
1346
|
+
if (oldest !== undefined)
|
|
1347
|
+
bridge.completed.delete(oldest);
|
|
1348
|
+
}
|
|
1349
|
+
bridge.completed.add(rpcId);
|
|
1350
|
+
}
|
|
1351
|
+
dispatchPendingUserQuestions() {
|
|
1352
|
+
const bridge = this.userQuestionBridge;
|
|
1353
|
+
if (!this.active || bridge === undefined || bridge.controller.signal.aborted)
|
|
1354
|
+
return;
|
|
1355
|
+
for (const pending of bridge.pending.values())
|
|
1356
|
+
this.dispatchUserQuestion(bridge, pending);
|
|
1357
|
+
}
|
|
1358
|
+
dispatchUserQuestion(bridge, pending) {
|
|
1359
|
+
if (!this.active || this.userQuestionBridge !== bridge || bridge.controller.signal.aborted
|
|
1360
|
+
|| bridge.pending.get(pending.rpcId) !== pending || pending.state !== 'waiting')
|
|
1361
|
+
return;
|
|
1362
|
+
if (pending.response !== undefined) {
|
|
1363
|
+
this.sendUserQuestionResponse(bridge, pending);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
const route = this.resolveUserQuestionRoute(pending.sessionId);
|
|
1367
|
+
if (route.state === 'deferred')
|
|
1368
|
+
return;
|
|
1369
|
+
if (route.state === 'invalid') {
|
|
1370
|
+
this.queueUserQuestionResponse(bridge, pending, cancelledUserQuestionResponse(pending.rpcId));
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
const controller = new AbortController();
|
|
1374
|
+
const abortForBridge = () => controller.abort(bridge.controller.signal.reason);
|
|
1375
|
+
if (bridge.controller.signal.aborted)
|
|
1376
|
+
abortForBridge();
|
|
1377
|
+
else
|
|
1378
|
+
bridge.controller.signal.addEventListener('abort', abortForBridge, { once: true });
|
|
1379
|
+
const attempt = {
|
|
1380
|
+
controller,
|
|
1381
|
+
authority: route.authority,
|
|
1382
|
+
detachBridgeAbort: () => bridge.controller.signal.removeEventListener('abort', abortForBridge),
|
|
1383
|
+
};
|
|
1384
|
+
pending.state = 'dispatching';
|
|
1385
|
+
pending.attempt = attempt;
|
|
1386
|
+
const input = Object.freeze({
|
|
1387
|
+
operationId: pending.rpcId,
|
|
1388
|
+
bindingId: route.authority.binding.id,
|
|
1389
|
+
bindingVersion: route.authority.binding.version,
|
|
1390
|
+
bindingGeneration: route.authority.binding.generation,
|
|
1391
|
+
sessionId: pending.sessionId,
|
|
1392
|
+
target: Object.freeze({
|
|
1393
|
+
conversation: Object.freeze({ ...route.authority.binding.conversation }),
|
|
1394
|
+
principal: Object.freeze({ ...route.authority.binding.principal }),
|
|
1395
|
+
}),
|
|
1396
|
+
questions: pending.questions,
|
|
1397
|
+
});
|
|
1398
|
+
void Promise.resolve()
|
|
1399
|
+
.then(() => route.authority.adapter.requestUserQuestion(input, controller.signal))
|
|
1400
|
+
.then(outcome => this.completeUserQuestionAttempt(bridge, pending, attempt, outcome), () => this.completeUserQuestionAttempt(bridge, pending, attempt, { outcome: 'unavailable' }));
|
|
1401
|
+
}
|
|
1402
|
+
completeUserQuestionAttempt(bridge, pending, attempt, outcome) {
|
|
1403
|
+
if (this.userQuestionBridge !== bridge || bridge.pending.get(pending.rpcId) !== pending
|
|
1404
|
+
|| pending.attempt !== attempt)
|
|
1405
|
+
return;
|
|
1406
|
+
delete pending.attempt;
|
|
1407
|
+
attempt.detachBridgeAbort();
|
|
1408
|
+
if (attempt.controller.signal.aborted) {
|
|
1409
|
+
pending.state = 'waiting';
|
|
1410
|
+
this.dispatchUserQuestion(bridge, pending);
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
if (!this.active || bridge.controller.signal.aborted)
|
|
1414
|
+
return;
|
|
1415
|
+
const route = this.resolveUserQuestionRoute(pending.sessionId);
|
|
1416
|
+
if (route.state === 'deferred') {
|
|
1417
|
+
pending.state = 'waiting';
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (route.state === 'invalid' || !sameUserQuestionAuthority(attempt.authority, route.authority)) {
|
|
1421
|
+
this.queueUserQuestionResponse(bridge, pending, cancelledUserQuestionResponse(pending.rpcId));
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
if (outcome !== null && typeof outcome === 'object'
|
|
1425
|
+
&& outcome.outcome === 'answered'
|
|
1426
|
+
&& typeof outcome.answer === 'object'
|
|
1427
|
+
&& outcome.answer !== null) {
|
|
1428
|
+
const answer = outcome.answer;
|
|
1429
|
+
this.queueUserQuestionResponse(bridge, pending, {
|
|
1430
|
+
type: 'client-response',
|
|
1431
|
+
rpcId: pending.rpcId,
|
|
1432
|
+
result: { ok: true, value: { sessionId: pending.sessionId, answer } },
|
|
1433
|
+
});
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
this.queueUserQuestionResponse(bridge, pending, cancelledUserQuestionResponse(pending.rpcId));
|
|
1437
|
+
}
|
|
1438
|
+
queueUserQuestionResponse(bridge, pending, response) {
|
|
1439
|
+
if (this.userQuestionBridge !== bridge || bridge.pending.get(pending.rpcId) !== pending)
|
|
1440
|
+
return;
|
|
1441
|
+
const attempt = pending.attempt;
|
|
1442
|
+
attempt?.detachBridgeAbort();
|
|
1443
|
+
attempt?.controller.abort(new Error('ApiProxy question response is being sent'));
|
|
1444
|
+
delete pending.attempt;
|
|
1445
|
+
pending.response = response;
|
|
1446
|
+
pending.responseAttempts = 0;
|
|
1447
|
+
pending.state = 'responding';
|
|
1448
|
+
this.sendUserQuestionResponse(bridge, pending);
|
|
1449
|
+
}
|
|
1450
|
+
async sendUserQuestionResponse(bridge, pending) {
|
|
1451
|
+
const response = pending.response;
|
|
1452
|
+
if (response === undefined || this.userQuestionBridge !== bridge
|
|
1453
|
+
|| bridge.pending.get(pending.rpcId) !== pending || pending.state !== 'responding')
|
|
1454
|
+
return;
|
|
1455
|
+
let receipt;
|
|
1456
|
+
try {
|
|
1457
|
+
receipt = await bridge.proxy.respond(response);
|
|
1458
|
+
}
|
|
1459
|
+
catch {
|
|
1460
|
+
if (this.userQuestionBridge === bridge && bridge.pending.get(pending.rpcId) === pending) {
|
|
1461
|
+
pending.responseAttempts += 1;
|
|
1462
|
+
const retry = setTimeout(() => {
|
|
1463
|
+
void this.sendUserQuestionResponse(bridge, pending);
|
|
1464
|
+
}, Math.min(250 * 2 ** Math.min(pending.responseAttempts - 1, 5), 5_000));
|
|
1465
|
+
retry.unref();
|
|
1466
|
+
}
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
if (this.userQuestionBridge !== bridge || bridge.pending.get(pending.rpcId) !== pending)
|
|
1470
|
+
return;
|
|
1471
|
+
if (!receipt.accepted && receipt.reason === 'bad-response') {
|
|
1472
|
+
if (response.result.ok) {
|
|
1473
|
+
pending.response = cancelledUserQuestionResponse(pending.rpcId);
|
|
1474
|
+
pending.responseAttempts = 0;
|
|
1475
|
+
void this.sendUserQuestionResponse(bridge, pending);
|
|
1476
|
+
}
|
|
1477
|
+
else {
|
|
1478
|
+
delete pending.response;
|
|
1479
|
+
pending.state = 'waiting';
|
|
1480
|
+
}
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
bridge.pending.delete(pending.rpcId);
|
|
1484
|
+
this.markUserQuestionCompleted(bridge, pending.rpcId);
|
|
1485
|
+
}
|
|
1486
|
+
abortUserQuestionAttemptsForAdapter(adapter, reason) {
|
|
1487
|
+
const bridge = this.userQuestionBridge;
|
|
1488
|
+
if (bridge === undefined)
|
|
1489
|
+
return;
|
|
1490
|
+
for (const pending of bridge.pending.values()) {
|
|
1491
|
+
if (pending.attempt?.authority.adapter === adapter)
|
|
1492
|
+
pending.attempt.controller.abort(reason);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
resolveUserQuestionRoute(sessionId) {
|
|
1496
|
+
try {
|
|
1497
|
+
return this.resolveUserQuestionRouteFromStore(sessionId);
|
|
1498
|
+
}
|
|
1499
|
+
catch {
|
|
1500
|
+
// A transient local store failure is not proof that the owner or binding
|
|
1501
|
+
// was revoked. Keep the ApiProxy request pending for replay/re-registration.
|
|
1502
|
+
return { state: 'deferred' };
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
resolveUserQuestionRouteFromStore(sessionId) {
|
|
1506
|
+
const binding = this.deliveryStore.getBindingBySession(sessionId);
|
|
1507
|
+
// The mux carries every Web session. Absence from Delivery is not a
|
|
1508
|
+
// revocation signal and must never let this optional bridge cancel a
|
|
1509
|
+
// question owned only by the Web UI.
|
|
1510
|
+
if (binding === undefined)
|
|
1511
|
+
return { state: 'deferred' };
|
|
1512
|
+
const active = this.deliveryStore.getActiveBinding(binding.conversation);
|
|
1513
|
+
const owner = this.deliveryStore.getPrincipal(binding.principal);
|
|
1514
|
+
if (binding?.status !== 'active' || binding.sessionId !== sessionId
|
|
1515
|
+
|| active?.id !== binding.id || active.version !== binding.version
|
|
1516
|
+
|| active.generation !== binding.generation || active.sessionId !== binding.sessionId
|
|
1517
|
+
|| JSON.stringify(active.principal) !== JSON.stringify(binding.principal)
|
|
1518
|
+
|| JSON.stringify(active.conversation) !== JSON.stringify(binding.conversation)
|
|
1519
|
+
|| owner?.status !== 'active' || owner.role !== 'owner'
|
|
1520
|
+
|| JSON.stringify(owner.principal) !== JSON.stringify(binding.principal)) {
|
|
1521
|
+
return { state: 'invalid' };
|
|
1522
|
+
}
|
|
1523
|
+
const adapter = this.registry.get(binding.conversation.channel, binding.conversation.account);
|
|
1524
|
+
if (adapter === undefined || this.drainingUserQuestionAdapters.has(adapter)
|
|
1525
|
+
|| adapter.capabilities.userQuestions !== true || adapter.requestUserQuestion === undefined) {
|
|
1526
|
+
return { state: 'deferred' };
|
|
1527
|
+
}
|
|
1528
|
+
return { state: 'ready', authority: {
|
|
1529
|
+
adapter,
|
|
1530
|
+
binding,
|
|
1531
|
+
ownerId: owner.id,
|
|
1532
|
+
ownerVersion: owner.version,
|
|
1533
|
+
} };
|
|
1135
1534
|
}
|
|
1136
1535
|
currentPreferenceTurn(agent) {
|
|
1137
1536
|
this.assertActive();
|