@dsh-enhanced/assistant-delivery 0.1.23 → 0.1.30
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 +81 -10
- package/lib/acceptance.d.ts +46 -0
- package/lib/acceptance.d.ts.map +1 -0
- package/lib/acceptance.js +2 -0
- package/lib/acceptance.js.map +1 -0
- package/lib/agent-runtime.d.ts +33 -3
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +835 -62
- package/lib/agent-runtime.js.map +1 -1
- package/lib/coordinator.d.ts +4 -1
- package/lib/coordinator.d.ts.map +1 -1
- package/lib/coordinator.js +9 -0
- package/lib/coordinator.js.map +1 -1
- package/lib/feedback-command.d.ts +21 -1
- package/lib/feedback-command.d.ts.map +1 -1
- package/lib/feedback-command.js +18 -0
- package/lib/feedback-command.js.map +1 -1
- package/lib/goal-wake-types.d.ts +93 -0
- package/lib/goal-wake-types.d.ts.map +1 -0
- package/lib/goal-wake-types.js +70 -0
- package/lib/goal-wake-types.js.map +1 -0
- package/lib/inbound-images.d.ts.map +1 -1
- package/lib/inbound-images.js +11 -9
- package/lib/inbound-images.js.map +1 -1
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/native-web-owner.d.ts +78 -0
- package/lib/native-web-owner.d.ts.map +1 -0
- package/lib/native-web-owner.js +291 -0
- package/lib/native-web-owner.js.map +1 -0
- package/lib/operator-snapshot.d.ts +80 -0
- package/lib/operator-snapshot.d.ts.map +1 -0
- package/lib/operator-snapshot.js +935 -0
- package/lib/operator-snapshot.js.map +1 -0
- package/lib/operator.d.ts +2 -0
- package/lib/operator.d.ts.map +1 -1
- package/lib/operator.js +10 -0
- package/lib/operator.js.map +1 -1
- package/lib/service.d.ts +63 -13
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +917 -349
- package/lib/service.js.map +1 -1
- package/lib/session-lease-runtime.d.ts +69 -0
- package/lib/session-lease-runtime.d.ts.map +1 -0
- package/lib/session-lease-runtime.js +395 -0
- package/lib/session-lease-runtime.js.map +1 -0
- package/lib/session-lease-types.d.ts +27 -0
- package/lib/session-lease-types.d.ts.map +1 -0
- package/lib/session-lease-types.js +2 -0
- package/lib/session-lease-types.js.map +1 -0
- package/lib/sqlite.d.ts +1 -1
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +201 -3
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +133 -4
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +771 -43
- package/lib/store.js.map +1 -1
- package/lib/types.d.ts +9 -2
- 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 +61 -44
package/lib/service.js
CHANGED
|
@@ -3,18 +3,22 @@ import { isAbsolute } from 'node:path';
|
|
|
3
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
4
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
5
5
|
import Schema from '@deepseek-ai/schemastery';
|
|
6
|
+
import { acceptanceCanonicalJson } from '@dsh-enhanced/task-acceptance-contract';
|
|
6
7
|
import { approvalReviewerOf, isAutoReviewEscalation, } from '@dsh-enhanced/assistant-policy';
|
|
7
8
|
import { TRUSTED_EVALUATION_PRODUCER_PROTOCOL, } from '@dsh-enhanced/assistant-evaluation';
|
|
8
9
|
import { ASSISTANT_GROWTH_CONTRACT_VERSION, growthObjectDigest, validateResolvedWorkflowAutomationTemplate, validateWorkflowAutomationTemplate, validateWorkflowScope, validateWorkflowTraceProjectionReceipt, workflowArgumentShapeDigest, } from '@dsh-enhanced/assistant-growth-contract';
|
|
9
10
|
import { DeliveryAdapterRegistry, DeliveryCoordinator, InboundCoordinator, } from './coordinator.js';
|
|
10
11
|
import { DeliveryStore, DeliveryStoreError } from './store.js';
|
|
11
12
|
import { DshDeliveryRuntime } from './agent-runtime.js';
|
|
13
|
+
import { DeliverySessionLeases } from './session-lease-runtime.js';
|
|
14
|
+
import { NativeWebOwner } from './native-web-owner.js';
|
|
15
|
+
import { validateOwnerGoalOutcomeFeedbackLocator, validateOwnerGoalOutcomeFeedbackProof, } from './goal-wake-types.js';
|
|
12
16
|
import { InboundImageMaterializer } from './inbound-images.js';
|
|
13
17
|
import { registerDeliveryTools } from './tools.js';
|
|
14
18
|
import { bindingMatchesOwnerRoute, canonicalBackgroundSourceId as canonicalHostBackgroundSourceId, canonicalConversation, canonicalLocalOperatorId, canonicalOwnerRouteAuthority, externalPrincipalId, ownerRouteAuthorityHash, ownerRouteBindingSnapshot, } from './canonical.js';
|
|
15
19
|
import { isExactDeliveryCommand, parseDeliveryCommand } from './session-commands.js';
|
|
16
20
|
import { deliveryT1PreferenceKeys, deliveryT1PreferenceValues } from './learning-command.js';
|
|
17
|
-
import { feedbackSignalInput, classifyNaturalPreferenceDirective, observedResponseLanguage, } from './feedback-command.js';
|
|
21
|
+
import { feedbackSignalInput, parseFeedbackCommand, classifyNaturalPreferenceDirective, observedResponseLanguage, } from './feedback-command.js';
|
|
18
22
|
import { DELIVERY_PREFERENCE_PROJECTION_PROTOCOL, TRUSTED_DELIVERY_PRESENTATION_PRODUCER_PROTOCOL, } from './types.js';
|
|
19
23
|
const trustedDeliveryPreferenceProducers = new WeakSet();
|
|
20
24
|
const deliveryPreferenceProducerProbe = Symbol('assistant-delivery.preference-producer-probe');
|
|
@@ -46,6 +50,17 @@ const automationObjectiveFeedbackFooter = [
|
|
|
46
50
|
'任务结果反馈:直接回复本消息并发送 `/feedback achieved`、`/feedback partial` 或 `/feedback not-achieved`。',
|
|
47
51
|
'`helpful` 等只记录偏好,不会被当成任务成败。',
|
|
48
52
|
].join('\n');
|
|
53
|
+
const goalOutcomeFeedbackFooter = [
|
|
54
|
+
'',
|
|
55
|
+
'---',
|
|
56
|
+
'目标结果反馈:直接回复本消息并发送 `/feedback achieved`、`/feedback partial` 或 `/feedback not-achieved`。',
|
|
57
|
+
'`helpful` 等只记录偏好,不会被当成目标成败。',
|
|
58
|
+
].join('\n');
|
|
59
|
+
function isGoalOutcomeFeedbackAuthority(value) {
|
|
60
|
+
return typeof value === 'object' && value !== null
|
|
61
|
+
&& typeof value.issueOwnerGoalOutcomeFeedbackTarget === 'function'
|
|
62
|
+
&& typeof value.resolveOwnerGoalOutcomeFeedbackTarget === 'function';
|
|
63
|
+
}
|
|
49
64
|
function isAutomationDeliveryEvidenceResolver(value) {
|
|
50
65
|
return typeof value === 'object' && value !== null
|
|
51
66
|
&& typeof value.resolveDeliveryEvidence === 'function';
|
|
@@ -246,6 +261,7 @@ const configSchema = Schema.object({
|
|
|
246
261
|
agentModel: Schema.string().min(1).default('deepseek-v4-flash'),
|
|
247
262
|
agentMaxOutputTokens: Schema.number().step(1).min(1).default(8_192),
|
|
248
263
|
agentMaxAutoContinuationTurns: Schema.number().step(1).min(0).max(8).default(2),
|
|
264
|
+
agentGoalContinuationTimeoutMs: Schema.number().step(1).min(0).max(300_000).default(0),
|
|
249
265
|
modelPickerTtlMs: Schema.number().step(1).min(60_000).max(86_400_000).default(900_000),
|
|
250
266
|
permissionPickerTtlMs: Schema.number().step(1).min(60_000).max(86_400_000).default(900_000),
|
|
251
267
|
toolApprovalTtlMs: Schema.number().step(1).min(1_000).max(300_000).default(300_000),
|
|
@@ -351,7 +367,7 @@ const permissionEventTypes = [
|
|
|
351
367
|
const approvalOutcomes = ['allowed-once', 'rejected', 'cancelled', 'unavailable'];
|
|
352
368
|
function currentPermissionEvents(agent) {
|
|
353
369
|
const latest = new Map();
|
|
354
|
-
for (const event of agent.session.
|
|
370
|
+
for (const event of agent.session.snapshotEvents()) {
|
|
355
371
|
const type = String(event.type);
|
|
356
372
|
if (!permissionEventTypes.includes(type))
|
|
357
373
|
continue;
|
|
@@ -362,70 +378,6 @@ function currentPermissionEvents(agent) {
|
|
|
362
378
|
function signalAborted(signal) {
|
|
363
379
|
return signal?.aborted === true;
|
|
364
380
|
}
|
|
365
|
-
const userQuestionTombstoneLimit = 1_024;
|
|
366
|
-
function isUserQuestionApiProxy(value) {
|
|
367
|
-
if (value === null || typeof value !== 'object')
|
|
368
|
-
return false;
|
|
369
|
-
const candidate = value;
|
|
370
|
-
return candidate.events !== null && typeof candidate.events === 'object'
|
|
371
|
-
&& typeof candidate.events.mux === 'function'
|
|
372
|
-
&& typeof candidate.respond === 'function';
|
|
373
|
-
}
|
|
374
|
-
function requestedUserQuestionFrame(value) {
|
|
375
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
376
|
-
return undefined;
|
|
377
|
-
const envelope = value;
|
|
378
|
-
if (typeof envelope.rpcId !== 'string' || envelope.rpcId.length === 0
|
|
379
|
-
|| envelope.payload === null || typeof envelope.payload !== 'object'
|
|
380
|
-
|| Array.isArray(envelope.payload))
|
|
381
|
-
return undefined;
|
|
382
|
-
const payload = envelope.payload;
|
|
383
|
-
if (payload.type !== 'question/requested' || typeof payload.sessionId !== 'string'
|
|
384
|
-
|| payload.sessionId.length === 0 || !Array.isArray(payload.questions))
|
|
385
|
-
return undefined;
|
|
386
|
-
return {
|
|
387
|
-
rpcId: envelope.rpcId,
|
|
388
|
-
sessionId: payload.sessionId,
|
|
389
|
-
questions: Object.freeze([...payload.questions]),
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
function resolvedUserQuestionFrame(value) {
|
|
393
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
394
|
-
return undefined;
|
|
395
|
-
const envelope = value;
|
|
396
|
-
if (envelope.payload === null || typeof envelope.payload !== 'object' || Array.isArray(envelope.payload)) {
|
|
397
|
-
return undefined;
|
|
398
|
-
}
|
|
399
|
-
const payload = envelope.payload;
|
|
400
|
-
if (payload.type !== 'question/resolved' || typeof payload.questionRpcId !== 'string'
|
|
401
|
-
|| payload.questionRpcId.length === 0
|
|
402
|
-
|| (payload.outcome !== 'answered' && payload.outcome !== 'cancelled'))
|
|
403
|
-
return undefined;
|
|
404
|
-
return { questionRpcId: payload.questionRpcId };
|
|
405
|
-
}
|
|
406
|
-
function userQuestionRequestHash(sessionId, questions) {
|
|
407
|
-
try {
|
|
408
|
-
const encoded = JSON.stringify([sessionId, questions]);
|
|
409
|
-
return typeof encoded === 'string' ? createHash('sha256').update(encoded).digest('hex') : undefined;
|
|
410
|
-
}
|
|
411
|
-
catch {
|
|
412
|
-
return undefined;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
function cancelledUserQuestionResponse(rpcId) {
|
|
416
|
-
return {
|
|
417
|
-
type: 'client-response',
|
|
418
|
-
rpcId,
|
|
419
|
-
result: {
|
|
420
|
-
ok: false,
|
|
421
|
-
error: {
|
|
422
|
-
code: 'cancelled',
|
|
423
|
-
message: 'the user cancelled ask_user_question',
|
|
424
|
-
details: {},
|
|
425
|
-
},
|
|
426
|
-
},
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
381
|
function sameUserQuestionAuthority(left, right) {
|
|
430
382
|
return right !== undefined
|
|
431
383
|
&& right.adapter === left.adapter
|
|
@@ -438,6 +390,40 @@ function sameUserQuestionAuthority(left, right) {
|
|
|
438
390
|
&& JSON.stringify(right.binding.conversation) === JSON.stringify(left.binding.conversation)
|
|
439
391
|
&& JSON.stringify(right.binding.principal) === JSON.stringify(left.binding.principal);
|
|
440
392
|
}
|
|
393
|
+
function isRecord(value) {
|
|
394
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
395
|
+
}
|
|
396
|
+
function validUserQuestionAnswer(questions, value) {
|
|
397
|
+
if (!isRecord(value) || Object.getPrototypeOf(value) !== Object.prototype
|
|
398
|
+
|| Object.keys(value).some(key => key !== 'answers')
|
|
399
|
+
|| !Array.isArray(value.answers) || value.answers.length !== questions.length)
|
|
400
|
+
return false;
|
|
401
|
+
return value.answers.every((answer, index) => {
|
|
402
|
+
const question = questions[index];
|
|
403
|
+
if (question === undefined || !isRecord(answer) || Object.getPrototypeOf(answer) !== Object.prototype
|
|
404
|
+
|| Object.keys(answer).some(key => !['id', 'selected', 'custom'].includes(key))
|
|
405
|
+
|| answer.id !== question.id || !Array.isArray(answer.selected)
|
|
406
|
+
|| answer.selected.some(item => typeof item !== 'string')
|
|
407
|
+
|| new Set(answer.selected).size !== answer.selected.length)
|
|
408
|
+
return false;
|
|
409
|
+
const optionLabels = new Set((question.options ?? []).map(option => option.label));
|
|
410
|
+
if (answer.selected.some(item => !optionLabels.has(item)))
|
|
411
|
+
return false;
|
|
412
|
+
const custom = answer.custom;
|
|
413
|
+
if (custom !== undefined && (typeof custom !== 'string' || custom.trim() === ''))
|
|
414
|
+
return false;
|
|
415
|
+
if (question.multiSelect !== true && (answer.selected.length > 1
|
|
416
|
+
|| (answer.selected.length !== 0 && custom !== undefined)))
|
|
417
|
+
return false;
|
|
418
|
+
return true;
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
function userQuestionError(message, code, cause) {
|
|
422
|
+
return Object.assign(new Error(message, cause === undefined ? undefined : { cause }), {
|
|
423
|
+
name: 'UserQuestionError',
|
|
424
|
+
code,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
441
427
|
function sameToolApprovalAuthority(left, right) {
|
|
442
428
|
return right !== undefined
|
|
443
429
|
&& right.adapter === left.adapter
|
|
@@ -516,13 +502,16 @@ export class AssistantDeliveryService extends Service {
|
|
|
516
502
|
config;
|
|
517
503
|
ownerRoutes;
|
|
518
504
|
ownerRouteGuard;
|
|
505
|
+
nativeWebBound = false;
|
|
506
|
+
nativeWebOwner;
|
|
507
|
+
nativeWebRuntime;
|
|
519
508
|
ownerId = `assistant-delivery-${randomUUID()}`;
|
|
520
509
|
bindingFlights = new Map();
|
|
521
510
|
conversationTransitions = new Map();
|
|
522
511
|
agentApprovalBindings = new WeakMap();
|
|
523
512
|
toolApprovalControllers = new Set();
|
|
524
513
|
drainingUserQuestionAdapters = new WeakSet();
|
|
525
|
-
|
|
514
|
+
userQuestionAttempts = new Set();
|
|
526
515
|
modelSelectionWaiters = new Map();
|
|
527
516
|
preferenceFeedbackSink;
|
|
528
517
|
evaluationSink;
|
|
@@ -532,11 +521,19 @@ export class AssistantDeliveryService extends Service {
|
|
|
532
521
|
activePresentationRegistrations = new WeakSet();
|
|
533
522
|
evaluationProducerGeneration = `assistant-delivery:${randomUUID()}`;
|
|
534
523
|
preferenceProducerGeneration = `assistant-delivery-preference:${randomUUID()}`;
|
|
524
|
+
acceptanceProducerGeneration = `assistant-delivery-acceptance:${randomUUID()}`;
|
|
525
|
+
acceptanceSink;
|
|
526
|
+
/** Once a live verifier required acceptance, loss of that verifier must not silently bypass it. */
|
|
527
|
+
acceptanceRequired = false;
|
|
535
528
|
preferenceTurns = new WeakMap();
|
|
529
|
+
goalWakeResults = new WeakMap();
|
|
536
530
|
modelSelectionFlight;
|
|
537
531
|
presentationFlight;
|
|
538
532
|
workflowTraceFlight;
|
|
539
533
|
preferenceProjectionFlight;
|
|
534
|
+
inboundAdmissionRecoveryFlight;
|
|
535
|
+
inboundAdmissionRecoveryAfterSequence = 0;
|
|
536
|
+
inboundAdmissionRecoveryController = new AbortController();
|
|
540
537
|
workflowTraceRetryTimer;
|
|
541
538
|
preferenceProjectionRetryTimer;
|
|
542
539
|
modelSelectionRetryTimer;
|
|
@@ -623,18 +620,36 @@ export class AssistantDeliveryService extends Service {
|
|
|
623
620
|
ownerId: this.ownerId, leaseMs: config.leaseMs, maxAttempts: config.maxAttempts,
|
|
624
621
|
maxConcurrency: config.maxConcurrency, retryBaseMs: config.retryBaseMs, retryMaxMs: config.retryMaxMs,
|
|
625
622
|
ownerRouteGuard: this.ownerRouteGuard,
|
|
623
|
+
notificationGuard: record => this.#notificationGuard(record),
|
|
626
624
|
});
|
|
627
625
|
this.inbound = new InboundCoordinator({ store: this.deliveryStore, processor: () => this.runtime,
|
|
628
626
|
ownerId: this.ownerId, leaseMs: config.leaseMs, maxAttempts: config.maxAttempts,
|
|
629
627
|
maxConcurrency: config.maxConcurrency, retryBaseMs: config.retryBaseMs, retryMaxMs: config.retryMaxMs });
|
|
630
628
|
ctx.inject(['agents', 'sessions', 'llm'], runtimeCtx => {
|
|
629
|
+
const sessionLeases = new DeliverySessionLeases(runtimeCtx, {
|
|
630
|
+
requiresLease: sessionId => this.deliveryStore.requiresSessionLease(sessionId),
|
|
631
|
+
leaseMs: Math.min(config.leaseMs, 300_000),
|
|
632
|
+
claim: (target, holderId, leaseMs) => this.deliveryStore.claimSessionLease(target, holderId, leaseMs),
|
|
633
|
+
dispatch: lease => this.deliveryStore.markSessionLeaseDispatched(lease),
|
|
634
|
+
valid: lease => this.deliveryStore.hasSessionLease(lease),
|
|
635
|
+
renew: (lease, leaseMs) => this.deliveryStore.renewSessionLease(lease, leaseMs),
|
|
636
|
+
finish: (lease, input) => this.deliveryStore.finishSessionLease(lease, input),
|
|
637
|
+
});
|
|
638
|
+
const webRuntime = { ctx: runtimeCtx, leases: sessionLeases };
|
|
639
|
+
this.nativeWebRuntime = webRuntime;
|
|
640
|
+
runtimeCtx.effect(() => () => { if (this.nativeWebRuntime === webRuntime)
|
|
641
|
+
this.nativeWebRuntime = undefined; });
|
|
631
642
|
const unregister = this.registerInboundRuntime(new DshDeliveryRuntime(runtimeCtx, policy, {
|
|
643
|
+
sessionLeases,
|
|
632
644
|
sessionNamespace: this.deliveryStore.instanceId(),
|
|
633
645
|
workspace: config.defaultWorkspace, agentPreset: config.defaultAgentPreset, policyRef: config.policyRef,
|
|
634
646
|
getAgentPresets: () => runtimeCtx.get('agentPresets'),
|
|
635
647
|
provider: config.agentProvider, model: config.agentModel, maxOutputTokens: config.agentMaxOutputTokens,
|
|
636
648
|
maxAutoContinuationTurns: config.agentMaxAutoContinuationTurns,
|
|
649
|
+
goalContinuationTimeoutMs: config.agentGoalContinuationTimeoutMs,
|
|
637
650
|
maxTextBytes: config.maxTextBytes,
|
|
651
|
+
prepareForegroundTaskAcceptance: (binding, envelope) => this.prepareForegroundTaskAcceptance(binding, envelope),
|
|
652
|
+
completeForegroundTaskAcceptance: (handle, input) => this.completeForegroundTaskAcceptance(handle, input),
|
|
638
653
|
modelPickerTtlMs: config.modelPickerTtlMs,
|
|
639
654
|
permissionPickerTtlMs: config.permissionPickerTtlMs,
|
|
640
655
|
getModelSelection: conversation => this.deliveryStore.getModelSelection(conversation),
|
|
@@ -677,15 +692,10 @@ export class AssistantDeliveryService extends Service {
|
|
|
677
692
|
});
|
|
678
693
|
ctx.inject(['approval'], approvalCtx => approvalCtx.on('approval/request', (request, next) => this.requestToolApproval(approvalCtx, request, next)));
|
|
679
694
|
ctx.inject(['tools'], toolsCtx => registerDeliveryTools(toolsCtx, this));
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
ctx.inject(['
|
|
684
|
-
const apiProxy = apiCtx.apiProxy;
|
|
685
|
-
if (!isUserQuestionApiProxy(apiProxy))
|
|
686
|
-
return;
|
|
687
|
-
return this.attachUserQuestionApiProxy(apiProxy);
|
|
688
|
-
});
|
|
695
|
+
// rc.1 exposes questions as an Agent-scoped waterfall. Delivery is one
|
|
696
|
+
// answerer in that chain, so Web and other frontends may call `next()` and
|
|
697
|
+
// coexist without a competing service provider or a private ApiProxy seam.
|
|
698
|
+
ctx.inject(['userQuestions'], questionCtx => questionCtx.on('user-questions/request', (request, next) => this.answerUserQuestion(request, next), { prepend: true }));
|
|
689
699
|
// Presentation writes are intentionally reverse-bound. Delivery creates
|
|
690
700
|
// the publisher closure and hands it only to the exact live domain service;
|
|
691
701
|
// a public service-slot caller can no longer forge a terminal projection.
|
|
@@ -713,14 +723,19 @@ export class AssistantDeliveryService extends Service {
|
|
|
713
723
|
this.start();
|
|
714
724
|
ctx.effect(() => async () => {
|
|
715
725
|
this.active = false;
|
|
726
|
+
// The isolated Web owner may already be unwinding concurrently. Await
|
|
727
|
+
// its memoized drain while the delivery Store is still available so a
|
|
728
|
+
// successfully disposed Agent can record its quiescent lease release.
|
|
729
|
+
await this.nativeWebOwner?.dispose();
|
|
716
730
|
trustedDeliveryPreferenceProducers.delete(this);
|
|
717
731
|
this.preferenceFeedbackSink = undefined;
|
|
732
|
+
this.acceptanceSink = undefined;
|
|
718
733
|
this.evaluationSink = undefined;
|
|
719
734
|
this.workflowTraceSink = undefined;
|
|
720
735
|
this.automationPresentationBinding?.dispose();
|
|
721
736
|
this.evolutionPresentationBinding?.dispose();
|
|
722
737
|
this.cancelModelSelectionWaiters();
|
|
723
|
-
this.
|
|
738
|
+
this.abortUserQuestionAttempts(new Error('assistant-delivery is stopping'));
|
|
724
739
|
for (const controller of this.toolApprovalControllers) {
|
|
725
740
|
controller.abort(new Error('assistant-delivery is stopping'));
|
|
726
741
|
}
|
|
@@ -1220,11 +1235,272 @@ export class AssistantDeliveryService extends Service {
|
|
|
1220
1235
|
this.runtime = undefined;
|
|
1221
1236
|
};
|
|
1222
1237
|
}
|
|
1238
|
+
/** Versioned capability: idle wake Agents remain alive through exact goal settlement. */
|
|
1239
|
+
goalWakeSettlementVersion = () => { this.assertActive(); return 1; };
|
|
1240
|
+
goalWakeResultVersion = () => { this.assertActive(); return 1; };
|
|
1241
|
+
/** Only the live Goals service can mint this process-local wake capability. */
|
|
1242
|
+
async resumeScheduledGoal(input) {
|
|
1243
|
+
this.assertActive();
|
|
1244
|
+
const runtime = this.runtime;
|
|
1245
|
+
const denied = () => { throw new AssistantDeliveryError('policy-denied', 'scheduled goal authority is no longer current'); };
|
|
1246
|
+
const current = () => {
|
|
1247
|
+
this.assertActive();
|
|
1248
|
+
// Cordis creates contextual service proxies on read; proxy identity is
|
|
1249
|
+
// not a generation proof. The currently installed service must own the
|
|
1250
|
+
// exact process-local capability on every boundary.
|
|
1251
|
+
const producer = this.ctx.get('assistantGoals', false);
|
|
1252
|
+
if (this.runtime !== runtime || !(runtime instanceof DshDeliveryRuntime)
|
|
1253
|
+
|| producer?.ownsWakeExecution?.(input) !== true)
|
|
1254
|
+
return denied();
|
|
1255
|
+
input.signal.throwIfAborted();
|
|
1256
|
+
if (!Number.isSafeInteger(input.deadlineAt) || Date.now() >= input.deadlineAt)
|
|
1257
|
+
return denied();
|
|
1258
|
+
const expected = input.attestation;
|
|
1259
|
+
const resolved = this.deliveryStore.getPreferencePrincipalForSession({ sessionId: expected.sessionId,
|
|
1260
|
+
workspace: expected.scope.workspace, preset: expected.scope.preset });
|
|
1261
|
+
if (resolved === undefined)
|
|
1262
|
+
return denied();
|
|
1263
|
+
const { binding, principal } = resolved;
|
|
1264
|
+
if (binding.id !== expected.bindingId || binding.version !== expected.bindingVersion
|
|
1265
|
+
|| binding.generation !== expected.bindingGeneration || binding.sessionId !== expected.sessionId
|
|
1266
|
+
|| externalPrincipalId(binding.principal) !== expected.principalId
|
|
1267
|
+
|| principal.id !== expected.principalLineage.principalRecordId
|
|
1268
|
+
|| principal.version !== expected.principalLineage.principalVersion)
|
|
1269
|
+
return denied();
|
|
1270
|
+
const decision = this.policy.authorize({ subject: { kind: 'background', id: 'assistant-goals-wake/v1',
|
|
1271
|
+
workspace: binding.workspace, principal: expected.principalId }, action: 'wake',
|
|
1272
|
+
resource: { kind: 'goal', id: 'business-context' }, context: { initiator: 'background' } });
|
|
1273
|
+
if (decision.effect !== 'allow')
|
|
1274
|
+
return denied();
|
|
1275
|
+
return binding;
|
|
1276
|
+
};
|
|
1277
|
+
const binding = current();
|
|
1278
|
+
if (typeof input.settle !== 'function')
|
|
1279
|
+
return denied();
|
|
1280
|
+
const result = await runtime.resumeScheduledGoal(binding, Object.freeze({ ...input,
|
|
1281
|
+
assertCurrent: (agent, phase) => {
|
|
1282
|
+
current();
|
|
1283
|
+
if (String(agent.session.id) !== binding.sessionId || agent.session.header.cwd !== binding.workspace
|
|
1284
|
+
|| agent.session.header.agentPreset !== binding.agentPreset)
|
|
1285
|
+
return denied();
|
|
1286
|
+
input.assertCurrent(agent, phase);
|
|
1287
|
+
current();
|
|
1288
|
+
},
|
|
1289
|
+
beforeResume: (agent) => { current(); input.beforeResume(agent); current(); },
|
|
1290
|
+
settle: async (agent, signal) => { current(); signal.throwIfAborted(); await input.settle(agent, signal); signal.throwIfAborted(); current(); },
|
|
1291
|
+
}));
|
|
1292
|
+
if (result.outcome === 'succeeded') {
|
|
1293
|
+
current();
|
|
1294
|
+
if (input.includeOutput === true && result.quiescent && result.output?.trim()) {
|
|
1295
|
+
const text = result.output;
|
|
1296
|
+
this.goalWakeResults.set(input, () => {
|
|
1297
|
+
// Re-read the exact owner, binding generation and Session at enqueue,
|
|
1298
|
+
// after the caller has rechecked its Goal/source authority.
|
|
1299
|
+
const target = current();
|
|
1300
|
+
if (target.conversation.channel === 'web') {
|
|
1301
|
+
const owner = this.nativeWebOwner;
|
|
1302
|
+
// This finalizes the existing, flushed native Session reply. It does
|
|
1303
|
+
// not create a second send or claim that the reply was read.
|
|
1304
|
+
if (!this.nativeWebBound || owner === undefined)
|
|
1305
|
+
return denied();
|
|
1306
|
+
try {
|
|
1307
|
+
owner.assertSession(target.sessionId);
|
|
1308
|
+
}
|
|
1309
|
+
catch {
|
|
1310
|
+
return denied();
|
|
1311
|
+
}
|
|
1312
|
+
return Object.freeze({ kind: 'native-session', sessionId: target.sessionId });
|
|
1313
|
+
}
|
|
1314
|
+
return this.publishScheduledGoalResult(input, text, current, denied);
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
return result;
|
|
1319
|
+
}
|
|
1320
|
+
/** Finalize the captured native reply, or publish it only for a still-owned external channel. */
|
|
1321
|
+
enqueueScheduledGoalResult(input) {
|
|
1322
|
+
this.assertActive();
|
|
1323
|
+
const publish = this.goalWakeResults.get(input);
|
|
1324
|
+
if (publish === undefined)
|
|
1325
|
+
throw new AssistantDeliveryError('policy-denied', 'scheduled goal result is unavailable');
|
|
1326
|
+
return publish();
|
|
1327
|
+
}
|
|
1328
|
+
publishScheduledGoalResult(input, text, current, denied) {
|
|
1329
|
+
current();
|
|
1330
|
+
let resolved;
|
|
1331
|
+
try {
|
|
1332
|
+
resolved = input.resolveOutcomeFeedbackTarget();
|
|
1333
|
+
}
|
|
1334
|
+
catch {
|
|
1335
|
+
return denied();
|
|
1336
|
+
}
|
|
1337
|
+
const afterResolve = current();
|
|
1338
|
+
let locator;
|
|
1339
|
+
let proof;
|
|
1340
|
+
try {
|
|
1341
|
+
locator = validateOwnerGoalOutcomeFeedbackLocator(resolved.locator);
|
|
1342
|
+
proof = validateOwnerGoalOutcomeFeedbackProof(resolved.proof);
|
|
1343
|
+
}
|
|
1344
|
+
catch {
|
|
1345
|
+
return denied();
|
|
1346
|
+
}
|
|
1347
|
+
const goals = this.ctx.get('assistantGoals', false);
|
|
1348
|
+
if (!isGoalOutcomeFeedbackAuthority(goals))
|
|
1349
|
+
return denied();
|
|
1350
|
+
let capabilityProof;
|
|
1351
|
+
try {
|
|
1352
|
+
capabilityProof = validateOwnerGoalOutcomeFeedbackProof(goals.resolveOwnerGoalOutcomeFeedbackTarget(resolved.capability));
|
|
1353
|
+
}
|
|
1354
|
+
catch {
|
|
1355
|
+
return denied();
|
|
1356
|
+
}
|
|
1357
|
+
const finalBinding = current();
|
|
1358
|
+
const owner = this.deliveryStore.getPrincipal(finalBinding.principal);
|
|
1359
|
+
if (acceptanceCanonicalJson(locator) !== acceptanceCanonicalJson(proof.locator)
|
|
1360
|
+
|| acceptanceCanonicalJson(proof) !== acceptanceCanonicalJson(capabilityProof)
|
|
1361
|
+
|| finalBinding.id !== afterResolve.id || locator.bindingId !== finalBinding.id
|
|
1362
|
+
|| locator.bindingVersion !== finalBinding.version
|
|
1363
|
+
|| locator.bindingGeneration !== finalBinding.generation
|
|
1364
|
+
|| locator.sessionId !== finalBinding.sessionId || locator.workspace !== finalBinding.workspace
|
|
1365
|
+
|| locator.preset !== finalBinding.agentPreset
|
|
1366
|
+
|| locator.principalId !== externalPrincipalId(finalBinding.principal)
|
|
1367
|
+
|| owner?.status !== 'active' || owner.role !== 'owner'
|
|
1368
|
+
|| locator.principalRecordId !== owner.id || locator.principalVersion !== owner.version
|
|
1369
|
+
|| proof.goal.nativeGoalId !== input.native.goalId
|
|
1370
|
+
|| proof.receipt.completedAt > Date.now() || proof.receipt.validUntil <= Date.now())
|
|
1371
|
+
return denied();
|
|
1372
|
+
let route;
|
|
1373
|
+
try {
|
|
1374
|
+
route = this.resolveOwnerRoute(locator.ownerRouteId);
|
|
1375
|
+
}
|
|
1376
|
+
catch {
|
|
1377
|
+
return denied();
|
|
1378
|
+
}
|
|
1379
|
+
if (route.binding.id !== finalBinding.id || route.binding.version !== finalBinding.version
|
|
1380
|
+
|| route.binding.generation !== finalBinding.generation
|
|
1381
|
+
|| route.binding.sessionId !== finalBinding.sessionId)
|
|
1382
|
+
return denied();
|
|
1383
|
+
const decision = this.policy.authorize({
|
|
1384
|
+
subject: { kind: 'background', id: 'assistant-goals-wake/v1',
|
|
1385
|
+
workspace: finalBinding.workspace, principal: locator.principalId },
|
|
1386
|
+
action: 'send', resource: { kind: 'message', id: finalBinding.id }, context: { initiator: 'background' },
|
|
1387
|
+
}, { idempotencyKey: `message-send:goal-outcome:${proof.proofDigest}:${finalBinding.id}` });
|
|
1388
|
+
if (decision.effect !== 'allow')
|
|
1389
|
+
throw policyDenied(decision);
|
|
1390
|
+
const rendered = `${text}${goalOutcomeFeedbackFooter}`;
|
|
1391
|
+
if (Buffer.byteLength(rendered, 'utf8') > this.config.maxTextBytes) {
|
|
1392
|
+
throw new AssistantDeliveryError('runtime-conflict', 'scheduled goal result exceeds the delivery text limit');
|
|
1393
|
+
}
|
|
1394
|
+
const situation = `goal:${locator.goalId}:definition:${proof.goal.definitionVersion}`;
|
|
1395
|
+
return this.deliveryStore.enqueueGoalOutcomeTarget({
|
|
1396
|
+
locator, proof,
|
|
1397
|
+
intent: {
|
|
1398
|
+
idempotencyKey: `goal-outcome:${proof.proofDigest}:${finalBinding.id}`,
|
|
1399
|
+
bindingId: finalBinding.id,
|
|
1400
|
+
target: { conversation: finalBinding.conversation, principal: finalBinding.principal },
|
|
1401
|
+
text: rendered, format: 'markdown',
|
|
1402
|
+
metadata: Object.freeze({
|
|
1403
|
+
'dsh.learning.schemaVersion': '3', 'dsh.learning.kind': 'goal-outcome',
|
|
1404
|
+
'dsh.learning.goalId': locator.goalId, 'dsh.learning.assessmentId': locator.assessmentId,
|
|
1405
|
+
'dsh.learning.runId': proof.runId, 'dsh.learning.situation': situation,
|
|
1406
|
+
'dsh.learning.occurredAt': String(proof.receipt.completedAt),
|
|
1407
|
+
'dsh.learning.objectiveStatus': proof.receipt.objectiveStatus,
|
|
1408
|
+
'dsh.learning.proofDigest': proof.proofDigest,
|
|
1409
|
+
}),
|
|
1410
|
+
},
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
/** Private verifier producer generation; invalidated with this service. */
|
|
1414
|
+
trustedAcceptanceProducerGeneration = () => {
|
|
1415
|
+
this.assertActive();
|
|
1416
|
+
return this.acceptanceProducerGeneration;
|
|
1417
|
+
};
|
|
1418
|
+
registerTaskAcceptanceSink = (registration) => {
|
|
1419
|
+
this.assertActive();
|
|
1420
|
+
const verifier = this.ctx.get('assistantVerifier', false);
|
|
1421
|
+
if (registration.protocol !== 'assistant-verifier/host-producer/v1'
|
|
1422
|
+
|| registration.generation !== this.acceptanceProducerGeneration
|
|
1423
|
+
|| typeof registration.prepare !== 'function' || typeof registration.completed !== 'function'
|
|
1424
|
+
|| typeof registration.requiresAcceptance !== 'boolean'
|
|
1425
|
+
|| typeof verifier?.ownsTaskAcceptanceRegistration !== 'function'
|
|
1426
|
+
|| verifier.ownsTaskAcceptanceRegistration(registration) !== true) {
|
|
1427
|
+
throw new AssistantDeliveryError('runtime-conflict', 'task acceptance registration is invalid');
|
|
1428
|
+
}
|
|
1429
|
+
if (this.acceptanceSink !== undefined) {
|
|
1430
|
+
if (this.acceptanceSink.registration === registration)
|
|
1431
|
+
return this.removeTaskAcceptanceSink(this.acceptanceSink.token);
|
|
1432
|
+
throw new AssistantDeliveryError('runtime-conflict', 'task acceptance sink is already registered');
|
|
1433
|
+
}
|
|
1434
|
+
const token = Symbol('assistant-delivery.task-acceptance');
|
|
1435
|
+
if (registration.requiresAcceptance)
|
|
1436
|
+
this.acceptanceRequired = true;
|
|
1437
|
+
this.acceptanceSink = Object.freeze({ token, registration });
|
|
1438
|
+
return this.removeTaskAcceptanceSink(token);
|
|
1439
|
+
};
|
|
1440
|
+
inspectAcceptedExecution = async (contract) => {
|
|
1441
|
+
this.assertActive();
|
|
1442
|
+
return this.deliveryStore.inspectForegroundAcceptedExecution(contract);
|
|
1443
|
+
};
|
|
1444
|
+
removeTaskAcceptanceSink(token) {
|
|
1445
|
+
let live = true;
|
|
1446
|
+
return () => {
|
|
1447
|
+
if (!live)
|
|
1448
|
+
return;
|
|
1449
|
+
live = false;
|
|
1450
|
+
if (this.acceptanceSink?.token === token)
|
|
1451
|
+
this.acceptanceSink = undefined;
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
prepareForegroundTaskAcceptance(binding, envelope) {
|
|
1455
|
+
const sink = this.acceptanceSink;
|
|
1456
|
+
if (sink === undefined) {
|
|
1457
|
+
if (this.acceptanceRequired) {
|
|
1458
|
+
throw new AssistantDeliveryError('runtime-conflict', 'required task acceptance verifier is unavailable');
|
|
1459
|
+
}
|
|
1460
|
+
return undefined;
|
|
1461
|
+
}
|
|
1462
|
+
const owner = this.ownerLineageForBinding(binding);
|
|
1463
|
+
const inbox = this.deliveryStore.getInboxByProviderEvent(envelope.channel, envelope.account, envelope.eventId);
|
|
1464
|
+
const exactInbox = inbox !== undefined && inbox.status === 'claimed' && inbox.bindingId === binding.id
|
|
1465
|
+
&& JSON.stringify(inbox.envelope) === JSON.stringify(envelope);
|
|
1466
|
+
if (owner === undefined || !exactInbox) {
|
|
1467
|
+
if (this.acceptanceRequired) {
|
|
1468
|
+
throw new AssistantDeliveryError('missing-binding', 'required task acceptance lacks an authenticated owner inbox');
|
|
1469
|
+
}
|
|
1470
|
+
return undefined;
|
|
1471
|
+
}
|
|
1472
|
+
const task = Object.freeze({ kind: 'foreground-turn', ref: inbox.id });
|
|
1473
|
+
const prepared = sink.registration.prepare(Object.freeze({
|
|
1474
|
+
scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
|
|
1475
|
+
owner: Object.freeze({ principalRecordId: owner.principalRecordId, principalVersion: owner.principalVersion }),
|
|
1476
|
+
task,
|
|
1477
|
+
// Preserve the original inbound objective byte-for-byte.
|
|
1478
|
+
objective: envelope.text,
|
|
1479
|
+
}));
|
|
1480
|
+
if (prepared === null) {
|
|
1481
|
+
if (this.acceptanceRequired) {
|
|
1482
|
+
throw new AssistantDeliveryError('runtime-conflict', 'required task acceptance profile did not produce a contract');
|
|
1483
|
+
}
|
|
1484
|
+
return undefined;
|
|
1485
|
+
}
|
|
1486
|
+
this.deliveryStore.bindForegroundTaskAcceptance({ inboxId: inbox.id, contractId: prepared.contractId,
|
|
1487
|
+
contractDigest: prepared.contractDigest, scope: { workspace: binding.workspace, preset: binding.agentPreset },
|
|
1488
|
+
owner, binding, dispatchedAt: Date.now() });
|
|
1489
|
+
return prepared;
|
|
1490
|
+
}
|
|
1491
|
+
async completeForegroundTaskAcceptance(handle, input) {
|
|
1492
|
+
const sink = this.acceptanceSink;
|
|
1493
|
+
this.deliveryStore.finishForegroundTaskAcceptance({ contractId: handle.contractId, ...input, completedAt: Date.now() });
|
|
1494
|
+
if (sink === undefined && this.acceptanceRequired) {
|
|
1495
|
+
throw new AssistantDeliveryError('runtime-conflict', 'required task acceptance verifier is unavailable after Host completion');
|
|
1496
|
+
}
|
|
1497
|
+
if (sink !== undefined)
|
|
1498
|
+
await sink.registration.completed(handle);
|
|
1499
|
+
}
|
|
1223
1500
|
registerAdapter(adapter) {
|
|
1224
1501
|
this.assertActive();
|
|
1225
1502
|
return this.registry.register(adapter).then(unregister => {
|
|
1226
1503
|
this.drainingUserQuestionAdapters.delete(adapter);
|
|
1227
|
-
this.dispatchPendingUserQuestions();
|
|
1228
1504
|
let registered = true;
|
|
1229
1505
|
return async () => {
|
|
1230
1506
|
if (!registered)
|
|
@@ -1233,283 +1509,109 @@ export class AssistantDeliveryService extends Service {
|
|
|
1233
1509
|
this.drainingUserQuestionAdapters.add(adapter);
|
|
1234
1510
|
this.abortUserQuestionAttemptsForAdapter(adapter, new Error('delivery adapter is unregistering'));
|
|
1235
1511
|
await unregister();
|
|
1236
|
-
this.dispatchPendingUserQuestions();
|
|
1237
1512
|
};
|
|
1238
1513
|
});
|
|
1239
1514
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
const bridge = {
|
|
1245
|
-
proxy,
|
|
1246
|
-
controller: new AbortController(),
|
|
1247
|
-
pending: new Map(),
|
|
1248
|
-
completed: new Set(),
|
|
1249
|
-
};
|
|
1250
|
-
this.userQuestionBridge = bridge;
|
|
1251
|
-
void this.consumeUserQuestionMux(bridge);
|
|
1252
|
-
let attached = true;
|
|
1253
|
-
return () => {
|
|
1254
|
-
if (!attached)
|
|
1255
|
-
return;
|
|
1256
|
-
attached = false;
|
|
1257
|
-
this.stopUserQuestionBridge(new Error('ApiProxy question bridge disposed'), bridge);
|
|
1258
|
-
};
|
|
1259
|
-
}
|
|
1260
|
-
stopUserQuestionBridge(reason, expected) {
|
|
1261
|
-
const bridge = expected ?? this.userQuestionBridge;
|
|
1262
|
-
if (bridge === undefined || this.userQuestionBridge !== bridge)
|
|
1263
|
-
return;
|
|
1264
|
-
this.userQuestionBridge = undefined;
|
|
1265
|
-
bridge.controller.abort(reason);
|
|
1266
|
-
for (const pending of bridge.pending.values()) {
|
|
1267
|
-
const attempt = pending.attempt;
|
|
1268
|
-
attempt?.detachBridgeAbort();
|
|
1269
|
-
attempt?.controller.abort(reason);
|
|
1270
|
-
delete pending.attempt;
|
|
1271
|
-
}
|
|
1272
|
-
bridge.pending.clear();
|
|
1273
|
-
bridge.completed.clear();
|
|
1274
|
-
}
|
|
1275
|
-
async consumeUserQuestionMux(bridge) {
|
|
1276
|
-
try {
|
|
1277
|
-
const stream = bridge.proxy.events.mux({
|
|
1278
|
-
rpcId: randomUUID(),
|
|
1279
|
-
payload: {},
|
|
1280
|
-
}, bridge.controller.signal);
|
|
1281
|
-
if (stream === null || typeof stream !== 'object'
|
|
1282
|
-
|| typeof stream[Symbol.asyncIterator] !== 'function')
|
|
1283
|
-
return;
|
|
1284
|
-
for await (const frame of stream) {
|
|
1285
|
-
if (!this.active || this.userQuestionBridge !== bridge || bridge.controller.signal.aborted)
|
|
1286
|
-
return;
|
|
1287
|
-
const resolved = resolvedUserQuestionFrame(frame);
|
|
1288
|
-
if (resolved !== undefined) {
|
|
1289
|
-
this.resolvePendingUserQuestion(bridge, resolved.questionRpcId);
|
|
1290
|
-
continue;
|
|
1291
|
-
}
|
|
1292
|
-
const requested = requestedUserQuestionFrame(frame);
|
|
1293
|
-
if (requested !== undefined)
|
|
1294
|
-
this.requestUserQuestionFromMux(bridge, requested);
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
catch {
|
|
1298
|
-
// ApiProxy is an in-process service. If its stream is torn down, its
|
|
1299
|
-
// Cordis injection disposer will replace this bridge; never leak a
|
|
1300
|
-
// rejected iterator into the Host lifecycle.
|
|
1301
|
-
}
|
|
1302
|
-
finally {
|
|
1303
|
-
if (this.userQuestionBridge === bridge && !bridge.controller.signal.aborted) {
|
|
1304
|
-
this.stopUserQuestionBridge(new Error('ApiProxy question mux ended'), bridge);
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
requestUserQuestionFromMux(bridge, frame) {
|
|
1309
|
-
if (bridge.completed.has(frame.rpcId))
|
|
1310
|
-
return;
|
|
1311
|
-
const requestHash = userQuestionRequestHash(frame.sessionId, frame.questions);
|
|
1312
|
-
if (requestHash === undefined)
|
|
1313
|
-
return;
|
|
1314
|
-
const existing = bridge.pending.get(frame.rpcId);
|
|
1315
|
-
if (existing !== undefined) {
|
|
1316
|
-
if (existing.sessionId !== frame.sessionId || existing.requestHash !== requestHash) {
|
|
1317
|
-
this.queueUserQuestionResponse(bridge, existing, cancelledUserQuestionResponse(existing.rpcId));
|
|
1318
|
-
}
|
|
1319
|
-
else {
|
|
1320
|
-
this.dispatchUserQuestion(bridge, existing);
|
|
1321
|
-
}
|
|
1322
|
-
return;
|
|
1515
|
+
abortUserQuestionAttemptsForAdapter(adapter, reason) {
|
|
1516
|
+
for (const attempt of this.userQuestionAttempts) {
|
|
1517
|
+
if (attempt.authority.adapter === adapter)
|
|
1518
|
+
attempt.controller.abort(reason);
|
|
1323
1519
|
}
|
|
1324
|
-
const pending = {
|
|
1325
|
-
rpcId: frame.rpcId,
|
|
1326
|
-
sessionId: frame.sessionId,
|
|
1327
|
-
questions: frame.questions,
|
|
1328
|
-
requestHash,
|
|
1329
|
-
state: 'waiting',
|
|
1330
|
-
responseAttempts: 0,
|
|
1331
|
-
};
|
|
1332
|
-
bridge.pending.set(pending.rpcId, pending);
|
|
1333
|
-
this.dispatchUserQuestion(bridge, pending);
|
|
1334
1520
|
}
|
|
1335
|
-
|
|
1336
|
-
this.
|
|
1337
|
-
|
|
1338
|
-
if (pending === undefined)
|
|
1339
|
-
return;
|
|
1340
|
-
bridge.pending.delete(rpcId);
|
|
1341
|
-
const attempt = pending.attempt;
|
|
1342
|
-
attempt?.detachBridgeAbort();
|
|
1343
|
-
attempt?.controller.abort(new Error('ApiProxy question request resolved'));
|
|
1344
|
-
delete pending.attempt;
|
|
1345
|
-
}
|
|
1346
|
-
markUserQuestionCompleted(bridge, rpcId) {
|
|
1347
|
-
if (bridge.completed.size >= userQuestionTombstoneLimit) {
|
|
1348
|
-
const oldest = bridge.completed.values().next().value;
|
|
1349
|
-
if (oldest !== undefined)
|
|
1350
|
-
bridge.completed.delete(oldest);
|
|
1351
|
-
}
|
|
1352
|
-
bridge.completed.add(rpcId);
|
|
1353
|
-
}
|
|
1354
|
-
dispatchPendingUserQuestions() {
|
|
1355
|
-
const bridge = this.userQuestionBridge;
|
|
1356
|
-
if (!this.active || bridge === undefined || bridge.controller.signal.aborted)
|
|
1357
|
-
return;
|
|
1358
|
-
for (const pending of bridge.pending.values())
|
|
1359
|
-
this.dispatchUserQuestion(bridge, pending);
|
|
1521
|
+
abortUserQuestionAttempts(reason) {
|
|
1522
|
+
for (const attempt of this.userQuestionAttempts)
|
|
1523
|
+
attempt.controller.abort(reason);
|
|
1360
1524
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
return;
|
|
1365
|
-
|
|
1366
|
-
this.sendUserQuestionResponse(bridge, pending);
|
|
1367
|
-
return;
|
|
1368
|
-
}
|
|
1369
|
-
const route = this.resolveUserQuestionRoute(pending.sessionId);
|
|
1525
|
+
async answerUserQuestion(request, next) {
|
|
1526
|
+
const agent = request.agent;
|
|
1527
|
+
if (!this.active || agent === undefined)
|
|
1528
|
+
return next();
|
|
1529
|
+
const route = this.resolveUserQuestionRoute(String(agent.session.id), agent);
|
|
1370
1530
|
if (route.state === 'deferred')
|
|
1371
|
-
return;
|
|
1531
|
+
return next();
|
|
1372
1532
|
if (route.state === 'invalid') {
|
|
1373
|
-
|
|
1374
|
-
return;
|
|
1533
|
+
throw userQuestionError('delivery user-question authority is no longer valid', 'ASK_ABORTED');
|
|
1375
1534
|
}
|
|
1376
1535
|
const controller = new AbortController();
|
|
1377
|
-
const
|
|
1378
|
-
if (
|
|
1379
|
-
|
|
1536
|
+
const abortForRequest = () => controller.abort(request.signal?.reason);
|
|
1537
|
+
if (request.signal?.aborted)
|
|
1538
|
+
abortForRequest();
|
|
1380
1539
|
else
|
|
1381
|
-
|
|
1540
|
+
request.signal?.addEventListener('abort', abortForRequest, { once: true });
|
|
1382
1541
|
const attempt = {
|
|
1383
1542
|
controller,
|
|
1384
1543
|
authority: route.authority,
|
|
1385
|
-
|
|
1544
|
+
detachRequestAbort: () => request.signal?.removeEventListener('abort', abortForRequest),
|
|
1386
1545
|
};
|
|
1387
|
-
|
|
1388
|
-
pending.attempt = attempt;
|
|
1546
|
+
this.userQuestionAttempts.add(attempt);
|
|
1389
1547
|
const input = Object.freeze({
|
|
1390
|
-
operationId:
|
|
1548
|
+
operationId: randomUUID(),
|
|
1391
1549
|
bindingId: route.authority.binding.id,
|
|
1392
1550
|
bindingVersion: route.authority.binding.version,
|
|
1393
1551
|
bindingGeneration: route.authority.binding.generation,
|
|
1394
|
-
sessionId:
|
|
1552
|
+
sessionId: String(agent.session.id),
|
|
1395
1553
|
target: Object.freeze({
|
|
1396
1554
|
conversation: Object.freeze({ ...route.authority.binding.conversation }),
|
|
1397
1555
|
principal: Object.freeze({ ...route.authority.binding.principal }),
|
|
1398
1556
|
}),
|
|
1399
|
-
questions:
|
|
1557
|
+
questions: Object.freeze([...request.questions]),
|
|
1400
1558
|
});
|
|
1401
|
-
void Promise.resolve()
|
|
1402
|
-
.then(() => route.authority.adapter.requestUserQuestion(input, controller.signal))
|
|
1403
|
-
.then(outcome => this.completeUserQuestionAttempt(bridge, pending, attempt, outcome), () => this.completeUserQuestionAttempt(bridge, pending, attempt, { outcome: 'unavailable' }));
|
|
1404
|
-
}
|
|
1405
|
-
completeUserQuestionAttempt(bridge, pending, attempt, outcome) {
|
|
1406
|
-
if (this.userQuestionBridge !== bridge || bridge.pending.get(pending.rpcId) !== pending
|
|
1407
|
-
|| pending.attempt !== attempt)
|
|
1408
|
-
return;
|
|
1409
|
-
delete pending.attempt;
|
|
1410
|
-
attempt.detachBridgeAbort();
|
|
1411
|
-
if (attempt.controller.signal.aborted) {
|
|
1412
|
-
pending.state = 'waiting';
|
|
1413
|
-
this.dispatchUserQuestion(bridge, pending);
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
if (!this.active || bridge.controller.signal.aborted)
|
|
1417
|
-
return;
|
|
1418
|
-
const route = this.resolveUserQuestionRoute(pending.sessionId);
|
|
1419
|
-
if (route.state === 'deferred') {
|
|
1420
|
-
pending.state = 'waiting';
|
|
1421
|
-
return;
|
|
1422
|
-
}
|
|
1423
|
-
if (route.state === 'invalid' || !sameUserQuestionAuthority(attempt.authority, route.authority)) {
|
|
1424
|
-
this.queueUserQuestionResponse(bridge, pending, cancelledUserQuestionResponse(pending.rpcId));
|
|
1425
|
-
return;
|
|
1426
|
-
}
|
|
1427
|
-
if (outcome !== null && typeof outcome === 'object'
|
|
1428
|
-
&& outcome.outcome === 'answered'
|
|
1429
|
-
&& typeof outcome.answer === 'object'
|
|
1430
|
-
&& outcome.answer !== null) {
|
|
1431
|
-
const answer = outcome.answer;
|
|
1432
|
-
this.queueUserQuestionResponse(bridge, pending, {
|
|
1433
|
-
type: 'client-response',
|
|
1434
|
-
rpcId: pending.rpcId,
|
|
1435
|
-
result: { ok: true, value: { sessionId: pending.sessionId, answer } },
|
|
1436
|
-
});
|
|
1437
|
-
return;
|
|
1438
|
-
}
|
|
1439
|
-
this.queueUserQuestionResponse(bridge, pending, cancelledUserQuestionResponse(pending.rpcId));
|
|
1440
|
-
}
|
|
1441
|
-
queueUserQuestionResponse(bridge, pending, response) {
|
|
1442
|
-
if (this.userQuestionBridge !== bridge || bridge.pending.get(pending.rpcId) !== pending)
|
|
1443
|
-
return;
|
|
1444
|
-
const attempt = pending.attempt;
|
|
1445
|
-
attempt?.detachBridgeAbort();
|
|
1446
|
-
attempt?.controller.abort(new Error('ApiProxy question response is being sent'));
|
|
1447
|
-
delete pending.attempt;
|
|
1448
|
-
pending.response = response;
|
|
1449
|
-
pending.responseAttempts = 0;
|
|
1450
|
-
pending.state = 'responding';
|
|
1451
|
-
this.sendUserQuestionResponse(bridge, pending);
|
|
1452
|
-
}
|
|
1453
|
-
async sendUserQuestionResponse(bridge, pending) {
|
|
1454
|
-
const response = pending.response;
|
|
1455
|
-
if (response === undefined || this.userQuestionBridge !== bridge
|
|
1456
|
-
|| bridge.pending.get(pending.rpcId) !== pending || pending.state !== 'responding')
|
|
1457
|
-
return;
|
|
1458
|
-
let receipt;
|
|
1459
1559
|
try {
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
if (this.userQuestionBridge === bridge && bridge.pending.get(pending.rpcId) === pending) {
|
|
1464
|
-
pending.responseAttempts += 1;
|
|
1465
|
-
const retry = setTimeout(() => {
|
|
1466
|
-
void this.sendUserQuestionResponse(bridge, pending);
|
|
1467
|
-
}, Math.min(250 * 2 ** Math.min(pending.responseAttempts - 1, 5), 5_000));
|
|
1468
|
-
retry.unref();
|
|
1560
|
+
let outcome;
|
|
1561
|
+
try {
|
|
1562
|
+
outcome = await route.authority.adapter.requestUserQuestion(input, controller.signal);
|
|
1469
1563
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
if (!receipt.accepted && receipt.reason === 'bad-response') {
|
|
1475
|
-
if (response.result.ok) {
|
|
1476
|
-
pending.response = cancelledUserQuestionResponse(pending.rpcId);
|
|
1477
|
-
pending.responseAttempts = 0;
|
|
1478
|
-
void this.sendUserQuestionResponse(bridge, pending);
|
|
1564
|
+
catch (error) {
|
|
1565
|
+
if (!controller.signal.aborted)
|
|
1566
|
+
return next();
|
|
1567
|
+
throw userQuestionError('ask_user_question was aborted before the user answered', 'ASK_ABORTED', controller.signal.reason ?? error);
|
|
1479
1568
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
pending.state = 'waiting';
|
|
1569
|
+
if (controller.signal.aborted) {
|
|
1570
|
+
throw userQuestionError('ask_user_question was aborted before the user answered', 'ASK_ABORTED', controller.signal.reason);
|
|
1483
1571
|
}
|
|
1484
|
-
|
|
1572
|
+
const current = this.resolveUserQuestionRoute(String(agent.session.id), agent);
|
|
1573
|
+
if (current.state !== 'ready' || !sameUserQuestionAuthority(route.authority, current.authority)) {
|
|
1574
|
+
throw userQuestionError('delivery user-question authority changed before the answer returned', 'ASK_ABORTED');
|
|
1575
|
+
}
|
|
1576
|
+
if (isRecord(outcome) && outcome.outcome === 'answered') {
|
|
1577
|
+
return validUserQuestionAnswer(request.questions, outcome.answer) ? outcome.answer : next();
|
|
1578
|
+
}
|
|
1579
|
+
if (isRecord(outcome) && outcome.outcome === 'unavailable')
|
|
1580
|
+
return next();
|
|
1581
|
+
if (!isRecord(outcome) || outcome.outcome !== 'cancelled')
|
|
1582
|
+
return next();
|
|
1583
|
+
throw userQuestionError('the user cancelled ask_user_question', 'ASK_CANCELLED');
|
|
1485
1584
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
abortUserQuestionAttemptsForAdapter(adapter, reason) {
|
|
1490
|
-
const bridge = this.userQuestionBridge;
|
|
1491
|
-
if (bridge === undefined)
|
|
1492
|
-
return;
|
|
1493
|
-
for (const pending of bridge.pending.values()) {
|
|
1494
|
-
if (pending.attempt?.authority.adapter === adapter)
|
|
1495
|
-
pending.attempt.controller.abort(reason);
|
|
1585
|
+
finally {
|
|
1586
|
+
this.userQuestionAttempts.delete(attempt);
|
|
1587
|
+
attempt.detachRequestAbort();
|
|
1496
1588
|
}
|
|
1497
1589
|
}
|
|
1498
|
-
resolveUserQuestionRoute(sessionId) {
|
|
1590
|
+
resolveUserQuestionRoute(sessionId, agent) {
|
|
1499
1591
|
try {
|
|
1500
|
-
return this.resolveUserQuestionRouteFromStore(sessionId);
|
|
1592
|
+
return this.resolveUserQuestionRouteFromStore(sessionId, agent);
|
|
1501
1593
|
}
|
|
1502
1594
|
catch {
|
|
1503
1595
|
// A transient local store failure is not proof that the owner or binding
|
|
1504
|
-
// was revoked.
|
|
1596
|
+
// was revoked. Decline this answerer and let the waterfall continue.
|
|
1505
1597
|
return { state: 'deferred' };
|
|
1506
1598
|
}
|
|
1507
1599
|
}
|
|
1508
|
-
resolveUserQuestionRouteFromStore(sessionId) {
|
|
1600
|
+
resolveUserQuestionRouteFromStore(sessionId, agent) {
|
|
1601
|
+
if (agent !== undefined) {
|
|
1602
|
+
const agents = this.context.get('agents');
|
|
1603
|
+
if (agents === undefined)
|
|
1604
|
+
return { state: 'invalid' };
|
|
1605
|
+
const live = agents.get(agent.id);
|
|
1606
|
+
if (live !== agent || live.session !== agent.session || String(agent.session.id) !== sessionId) {
|
|
1607
|
+
return { state: 'invalid' };
|
|
1608
|
+
}
|
|
1609
|
+
if (!agents.roots().includes(agent))
|
|
1610
|
+
return { state: 'invalid' };
|
|
1611
|
+
}
|
|
1509
1612
|
const binding = this.deliveryStore.getBindingBySession(sessionId);
|
|
1510
|
-
//
|
|
1511
|
-
//
|
|
1512
|
-
// question owned only by the Web UI.
|
|
1613
|
+
// Absence from Delivery means this question may belong to another UI
|
|
1614
|
+
// answerer; decline without converting it into a cancellation.
|
|
1513
1615
|
if (binding === undefined)
|
|
1514
1616
|
return { state: 'deferred' };
|
|
1515
1617
|
const active = this.deliveryStore.getActiveBinding(binding.conversation);
|
|
@@ -1535,6 +1637,61 @@ export class AssistantDeliveryService extends Service {
|
|
|
1535
1637
|
ownerVersion: owner.version,
|
|
1536
1638
|
} };
|
|
1537
1639
|
}
|
|
1640
|
+
/** Trusted Host configuration only; the returned capability fixes a pre-paired Web owner and scope. */
|
|
1641
|
+
bindNativeWebOwner = (ctx, config) => {
|
|
1642
|
+
this.assertActive();
|
|
1643
|
+
const runtime = this.nativeWebRuntime;
|
|
1644
|
+
if (runtime === undefined)
|
|
1645
|
+
throw new AssistantDeliveryError('runtime-conflict', 'native Web runtime is unavailable');
|
|
1646
|
+
if (this.nativeWebBound)
|
|
1647
|
+
throw new AssistantDeliveryError('runtime-conflict', 'native Web owner Controller is already bound');
|
|
1648
|
+
let access;
|
|
1649
|
+
access = new NativeWebOwner(ctx, this.deliveryStore, runtime.leases, this.policy, {
|
|
1650
|
+
assertActive: () => {
|
|
1651
|
+
this.assertActive();
|
|
1652
|
+
if (this.nativeWebRuntime !== runtime)
|
|
1653
|
+
throw new AssistantDeliveryError('runtime-conflict', 'native Web runtime changed');
|
|
1654
|
+
},
|
|
1655
|
+
released: () => { if (this.nativeWebOwner === access) {
|
|
1656
|
+
this.nativeWebOwner = undefined;
|
|
1657
|
+
this.nativeWebBound = false;
|
|
1658
|
+
} },
|
|
1659
|
+
policyRef: this.config.policyRef,
|
|
1660
|
+
leaseMs: this.config.leaseMs,
|
|
1661
|
+
prepare: (binding, envelope) => this.prepareForegroundTaskAcceptance(binding, envelope),
|
|
1662
|
+
complete: (handle, input) => this.completeForegroundTaskAcceptance(handle, input),
|
|
1663
|
+
claimed: (agent, envelope, turn) => this.capturePreferenceTurn(agent, { kind: 'delivery', channel: envelope.channel, account: envelope.account, eventId: envelope.eventId }, turn),
|
|
1664
|
+
notifications: binding => this.deliveryStore.listOutbox({ bindingId: binding.id, limit: 50 }).filter(record => record.status === 'accepted'
|
|
1665
|
+
&& record.intent.metadata?.['dsh.native-notice'] === 'v1' && this.#notificationGuard(record) === undefined)
|
|
1666
|
+
.map(record => ({ id: record.id, text: record.intent.text, createdAt: record.createdAt })),
|
|
1667
|
+
}, config);
|
|
1668
|
+
this.nativeWebOwner = access;
|
|
1669
|
+
this.nativeWebBound = true;
|
|
1670
|
+
const noticeAdapter = {
|
|
1671
|
+
channel: 'web', account: config.principal.account,
|
|
1672
|
+
capabilities: Object.freeze({ reconcileUnknownSend: true, receipts: [], formats: ['plain'] }),
|
|
1673
|
+
async start() { },
|
|
1674
|
+
send: async (intent) => {
|
|
1675
|
+
if (this.#nativeNoticeGuard(intent) !== undefined)
|
|
1676
|
+
return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
|
|
1677
|
+
return { outcome: 'accepted', providerMessageId: this.#notificationProviderId(intent) };
|
|
1678
|
+
},
|
|
1679
|
+
reconcileUnknownSend: async (record) => { return record.intent.metadata?.['dsh.native-notice'] === 'v1' && this.#notificationGuard(record) === undefined ? { outcome: 'accepted', providerMessageId: this.#notificationProviderId(record.intent) } : { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false }; },
|
|
1680
|
+
};
|
|
1681
|
+
ctx.effect(() => {
|
|
1682
|
+
const registration = this.registry.register(noticeAdapter);
|
|
1683
|
+
void registration.catch(() => { ctx.logger.warn('assistant-delivery: native Web notification adapter registration failed'); });
|
|
1684
|
+
return async () => {
|
|
1685
|
+
try {
|
|
1686
|
+
const remove = await registration;
|
|
1687
|
+
await remove();
|
|
1688
|
+
}
|
|
1689
|
+
catch { /* Registration failure is already reported; there is no adapter to remove. */ }
|
|
1690
|
+
};
|
|
1691
|
+
}, 'assistant-delivery.native-web-notices');
|
|
1692
|
+
runtime.ctx.effect(() => () => access.dispose(), 'assistant-delivery.native-web-runtime');
|
|
1693
|
+
return access;
|
|
1694
|
+
};
|
|
1538
1695
|
currentPreferenceTurn(agent) {
|
|
1539
1696
|
this.assertActive();
|
|
1540
1697
|
const attestation = this.preferenceTurns.get(agent);
|
|
@@ -1554,8 +1711,8 @@ export class AssistantDeliveryService extends Service {
|
|
|
1554
1711
|
|| lineage?.principalVersion !== attestation.principalLineage.principalVersion
|
|
1555
1712
|
|| !this.isOwnerFeedbackController(binding, inbox.envelope))
|
|
1556
1713
|
return undefined;
|
|
1557
|
-
const started = agent.session.
|
|
1558
|
-
const ended = agent.session.
|
|
1714
|
+
const started = agent.session.snapshotEvents().some(event => event.type === 'turn/start' && event.data.turn === attestation.turn);
|
|
1715
|
+
const ended = agent.session.snapshotEvents().some(event => event.type === 'turn/end' && event.data.turn === attestation.turn);
|
|
1559
1716
|
return started && !ended ? attestation : undefined;
|
|
1560
1717
|
}
|
|
1561
1718
|
preferencePrincipalForAgent(agent) {
|
|
@@ -2420,7 +2577,9 @@ export class AssistantDeliveryService extends Service {
|
|
|
2420
2577
|
* Evolution re-proves the immutable production run; the later Inbox is a
|
|
2421
2578
|
* separate evidence reference, never copied as prose.
|
|
2422
2579
|
*/
|
|
2423
|
-
async dispatchObjectiveFeedback(binding, envelope,
|
|
2580
|
+
async dispatchObjectiveFeedback(binding, envelope, command) {
|
|
2581
|
+
const parsed = typeof command === 'string' ? { kind: 'objective', objectiveStatus: command } : command;
|
|
2582
|
+
const objectiveStatus = parsed.kind === 'objective-status' ? 'achieved' : parsed.objectiveStatus;
|
|
2424
2583
|
const replyTarget = envelope.metadata?.replyToProviderMessageId;
|
|
2425
2584
|
if (typeof replyTarget !== 'string')
|
|
2426
2585
|
return 'invalid-target';
|
|
@@ -2435,7 +2594,23 @@ export class AssistantDeliveryService extends Service {
|
|
|
2435
2594
|
|| JSON.stringify(target.intent.target.principal) !== JSON.stringify(binding.principal)) {
|
|
2436
2595
|
return 'invalid-target';
|
|
2437
2596
|
}
|
|
2597
|
+
const hostCommand = parseDeliveryCommand(inbox.envelope);
|
|
2598
|
+
if (hostCommand?.name !== 'feedback' || (inbox.envelope.attachments?.length ?? 0) !== 0
|
|
2599
|
+
|| JSON.stringify(parseFeedbackCommand(hostCommand.rawInput)) !== JSON.stringify(parsed))
|
|
2600
|
+
return 'invalid-target';
|
|
2438
2601
|
const metadata = target.intent.metadata;
|
|
2602
|
+
let goalOutcomeTarget;
|
|
2603
|
+
try {
|
|
2604
|
+
goalOutcomeTarget = this.deliveryStore.getGoalOutcomeTarget(target.id);
|
|
2605
|
+
}
|
|
2606
|
+
catch {
|
|
2607
|
+
return 'invalid-target';
|
|
2608
|
+
}
|
|
2609
|
+
if (goalOutcomeTarget !== undefined || metadata?.['dsh.learning.kind'] === 'goal-outcome') {
|
|
2610
|
+
if (goalOutcomeTarget === undefined)
|
|
2611
|
+
return 'invalid-target';
|
|
2612
|
+
return await this.dispatchGoalOutcomeFeedback(binding, inbox, target, parsed, goalOutcomeTarget);
|
|
2613
|
+
}
|
|
2439
2614
|
// Ordinary Agent replies have no learning metadata. They are eligible
|
|
2440
2615
|
// only for Delivery's local, atomic verified-workflow receipt; the Store
|
|
2441
2616
|
// rebuilds the full Inbox/Outbox fence and abstains unless the source text
|
|
@@ -2449,17 +2624,82 @@ export class AssistantDeliveryService extends Service {
|
|
|
2449
2624
|
const sourceInbox = this.deliveryStore.getInboxByProviderEvent(envelope.channel, envelope.account, sourceEventId);
|
|
2450
2625
|
if (sourceInbox === undefined)
|
|
2451
2626
|
return 'invalid-target';
|
|
2627
|
+
const lineage = this.ownerLineageForBinding(binding);
|
|
2628
|
+
const accepted = lineage === undefined ? null : this.deliveryStore.inspectForegroundAcceptedExecutionForOwner({
|
|
2629
|
+
inboxId: sourceInbox.id,
|
|
2630
|
+
scope: { workspace: binding.workspace, preset: binding.agentPreset },
|
|
2631
|
+
owner: lineage,
|
|
2632
|
+
bindingId: binding.id,
|
|
2633
|
+
});
|
|
2634
|
+
const canonicalForeground = accepted !== null;
|
|
2635
|
+
const projectForegroundOwner = async (inspectOnly) => {
|
|
2636
|
+
const sink = this.evaluationSink;
|
|
2637
|
+
if (sink === undefined || lineage === undefined)
|
|
2638
|
+
return undefined;
|
|
2639
|
+
if ((parsed.kind !== 'objective' || inspectOnly) && (!['owner-objective-revision/v1', 'owner-objective-revision/v2']
|
|
2640
|
+
.includes(String(sink.registration.ownerRevisionProtocol))
|
|
2641
|
+
|| typeof sink.registration.inspect !== 'function'))
|
|
2642
|
+
return undefined;
|
|
2643
|
+
const operationId = `owner-feedback:${createHash('sha256').update(JSON.stringify([inbox.id, inbox.envelopeHash])).digest('hex')}`;
|
|
2644
|
+
const digest = createHash('sha256').update('assistant-delivery-foreground-objective-feedback-v1\0')
|
|
2645
|
+
.update(JSON.stringify([binding.workspace, binding.agentPreset, sourceInbox.id, target.id]))
|
|
2646
|
+
.digest('hex');
|
|
2647
|
+
const claims = Object.freeze({
|
|
2648
|
+
scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
|
|
2649
|
+
situation: `foreground:${sourceInbox.id}`,
|
|
2650
|
+
subjectKind: 'foreground-turn', subjectRef: sourceInbox.id, runId: sourceInbox.id, outboxId: target.id,
|
|
2651
|
+
chatId: binding.conversation.chat, principalId: externalPrincipalId(binding.principal), bindingId: binding.id,
|
|
2652
|
+
objectiveStatus, ownerCommand: { ...lineage, operationId,
|
|
2653
|
+
action: parsed.kind === 'objective-revision' ? parsed.action : 'initial',
|
|
2654
|
+
...(parsed.kind === 'objective-revision' ? { expectedVersion: parsed.expectedVersion, previousStatus: parsed.previousStatus } : {}), },
|
|
2655
|
+
occurredAt: inbox.receivedAt,
|
|
2656
|
+
initialIdempotencyKey: `assistant-delivery:foreground-objective-feedback-v1:${digest}`,
|
|
2657
|
+
idempotencyKey: parsed.kind === 'objective-revision' ? operationId : `assistant-delivery:foreground-objective-feedback-v1:${digest}`,
|
|
2658
|
+
});
|
|
2659
|
+
const capabilityReceipt = sink.registration.issueCapability(claims);
|
|
2660
|
+
if (this.evaluationSink?.token !== sink.token)
|
|
2661
|
+
return undefined;
|
|
2662
|
+
if (inspectOnly)
|
|
2663
|
+
return sink.registration.inspect?.(capabilityReceipt);
|
|
2664
|
+
const receipt = await Promise.resolve(sink.registration.append({
|
|
2665
|
+
capabilityReceipt, runId: sourceInbox.id, outboxId: target.id, chatId: binding.conversation.chat,
|
|
2666
|
+
principalId: externalPrincipalId(binding.principal), bindingId: binding.id, idempotencyKey: claims.idempotencyKey,
|
|
2667
|
+
}));
|
|
2668
|
+
return typeof receipt === 'object' && receipt !== null
|
|
2669
|
+
&& typeof receipt.ownerFeedbackState === 'object'
|
|
2670
|
+
? receipt.ownerFeedbackState
|
|
2671
|
+
: undefined;
|
|
2672
|
+
};
|
|
2673
|
+
if (parsed.kind === 'objective-status') {
|
|
2674
|
+
if (!canonicalForeground)
|
|
2675
|
+
return this.deliveryStore.verifiedWorkflowObjectiveState(target.id, lineage) ?? 'recorded';
|
|
2676
|
+
try {
|
|
2677
|
+
return await projectForegroundOwner(true) ?? 'unavailable';
|
|
2678
|
+
}
|
|
2679
|
+
catch {
|
|
2680
|
+
return 'unknown';
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
if (canonicalForeground && this.evaluationSink === undefined)
|
|
2684
|
+
return 'unavailable';
|
|
2452
2685
|
try {
|
|
2453
|
-
|
|
2686
|
+
// Evaluation commits first for accepted foreground tasks. If the sink
|
|
2687
|
+
// is unavailable, never acknowledge a Delivery-only success; a replay
|
|
2688
|
+
// can safely repeat the opaque command and then repair local workflow
|
|
2689
|
+
// evidence after the canonical owner revision is durable.
|
|
2690
|
+
const canonical = canonicalForeground ? await projectForegroundOwner(false) : undefined;
|
|
2691
|
+
if (canonicalForeground && canonical === undefined)
|
|
2692
|
+
return 'unavailable';
|
|
2693
|
+
const result = this.deliveryStore.commitVerifiedWorkflowTraceFeedback({
|
|
2454
2694
|
binding,
|
|
2455
2695
|
feedbackInboxId: inbox.id,
|
|
2456
2696
|
sourceInboxId: sourceInbox.id,
|
|
2457
2697
|
sourceOutboxId: target.id,
|
|
2458
2698
|
objectiveStatus,
|
|
2699
|
+
...(parsed.kind === 'objective-revision' ? { command: parsed } : {}),
|
|
2459
2700
|
});
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
return 'recorded';
|
|
2701
|
+
await this.drainWorkflowTraces();
|
|
2702
|
+
return canonical ?? result.ownerFeedbackState ?? 'recorded';
|
|
2463
2703
|
}
|
|
2464
2704
|
catch (error) {
|
|
2465
2705
|
if (error instanceof DeliveryStoreError) {
|
|
@@ -2471,6 +2711,9 @@ export class AssistantDeliveryService extends Service {
|
|
|
2471
2711
|
|| error.code === 'not-found')
|
|
2472
2712
|
return 'invalid-target';
|
|
2473
2713
|
}
|
|
2714
|
+
if (typeof error === 'object' && error !== null && 'code' in error
|
|
2715
|
+
&& ['idempotency-conflict', 'version-conflict'].includes(String(error.code)))
|
|
2716
|
+
return 'conflict';
|
|
2474
2717
|
return 'invalid-target';
|
|
2475
2718
|
}
|
|
2476
2719
|
}
|
|
@@ -2525,6 +2768,10 @@ export class AssistantDeliveryService extends Service {
|
|
|
2525
2768
|
const sink = this.evaluationSink;
|
|
2526
2769
|
if (sink === undefined)
|
|
2527
2770
|
return 'unavailable';
|
|
2771
|
+
if (parsed.kind !== 'objective' && (!['owner-objective-revision/v1', 'owner-objective-revision/v2']
|
|
2772
|
+
.includes(String(sink.registration.ownerRevisionProtocol))
|
|
2773
|
+
|| typeof sink.registration.inspect !== 'function'))
|
|
2774
|
+
return 'unavailable';
|
|
2528
2775
|
// One immutable Automation run is one learning subject. The objective
|
|
2529
2776
|
// status and Inbox event deliberately do not enter the key: replays and
|
|
2530
2777
|
// repeated equal judgements collapse, while an opposite judgement becomes
|
|
@@ -2532,22 +2779,32 @@ export class AssistantDeliveryService extends Service {
|
|
|
2532
2779
|
const digest = createHash('sha256').update('assistant-delivery-objective-feedback-v2\0')
|
|
2533
2780
|
.update(JSON.stringify([binding.workspace, binding.agentPreset, target.id, runId]))
|
|
2534
2781
|
.digest('hex');
|
|
2782
|
+
const lineage = this.ownerLineageForBinding(binding);
|
|
2783
|
+
if (lineage === undefined)
|
|
2784
|
+
return 'invalid-target';
|
|
2785
|
+
const operationId = `owner-feedback:${createHash('sha256').update(JSON.stringify([inbox.id, inbox.envelopeHash])).digest('hex')}`;
|
|
2535
2786
|
const claims = Object.freeze({
|
|
2536
2787
|
scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
|
|
2537
2788
|
situation,
|
|
2538
2789
|
objectiveStatus,
|
|
2790
|
+
ownerCommand: { ...lineage, operationId,
|
|
2791
|
+
action: parsed.kind === 'objective-revision' ? parsed.action : 'initial',
|
|
2792
|
+
...(parsed.kind === 'objective-revision' ? { expectedVersion: parsed.expectedVersion, previousStatus: parsed.previousStatus } : {}), },
|
|
2539
2793
|
runId,
|
|
2540
2794
|
outboxId: target.id,
|
|
2541
2795
|
chatId: binding.conversation.chat,
|
|
2542
2796
|
principalId: externalPrincipalId(binding.principal),
|
|
2543
2797
|
bindingId: binding.id,
|
|
2544
2798
|
occurredAt,
|
|
2545
|
-
|
|
2799
|
+
initialIdempotencyKey: `assistant-delivery:objective-feedback-v2:${digest}`,
|
|
2800
|
+
idempotencyKey: parsed.kind === 'objective-revision' ? operationId : `assistant-delivery:objective-feedback-v2:${digest}`,
|
|
2546
2801
|
});
|
|
2547
2802
|
try {
|
|
2548
2803
|
const capabilityReceipt = sink.registration.issueCapability(claims);
|
|
2549
2804
|
if (this.evaluationSink?.token !== sink.token)
|
|
2550
2805
|
return 'unknown';
|
|
2806
|
+
if (parsed.kind === 'objective-status')
|
|
2807
|
+
return sink.registration.inspect?.(capabilityReceipt) ?? 'recorded';
|
|
2551
2808
|
const receipt = await Promise.resolve(sink.registration.append({
|
|
2552
2809
|
capabilityReceipt,
|
|
2553
2810
|
runId,
|
|
@@ -2558,15 +2815,126 @@ export class AssistantDeliveryService extends Service {
|
|
|
2558
2815
|
idempotencyKey: claims.idempotencyKey,
|
|
2559
2816
|
}));
|
|
2560
2817
|
if (typeof receipt !== 'object' || receipt === null
|
|
2561
|
-
|| receipt.idempotencyKey !==
|
|
2818
|
+
|| typeof receipt.idempotencyKey !== 'string') {
|
|
2562
2819
|
return 'unknown';
|
|
2563
2820
|
}
|
|
2564
|
-
return 'recorded';
|
|
2821
|
+
return receipt.ownerFeedbackState ?? 'recorded';
|
|
2822
|
+
}
|
|
2823
|
+
catch (error) {
|
|
2824
|
+
if (typeof error === 'object' && error !== null && 'code' in error
|
|
2825
|
+
&& ['idempotency-conflict', 'version-conflict'].includes(String(error.code)))
|
|
2826
|
+
return 'conflict';
|
|
2827
|
+
return 'unknown';
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
async dispatchGoalOutcomeFeedback(binding, inbox, target, parsed, durable) {
|
|
2831
|
+
const metadata = target.intent.metadata;
|
|
2832
|
+
const locator = durable.locator;
|
|
2833
|
+
const immutableProof = durable.proof;
|
|
2834
|
+
const situation = `goal:${locator.goalId}:definition:${immutableProof.goal.definitionVersion}`;
|
|
2835
|
+
if (metadata?.['dsh.learning.schemaVersion'] !== '3'
|
|
2836
|
+
|| metadata['dsh.learning.kind'] !== 'goal-outcome'
|
|
2837
|
+
|| metadata['dsh.learning.goalId'] !== locator.goalId
|
|
2838
|
+
|| metadata['dsh.learning.assessmentId'] !== locator.assessmentId
|
|
2839
|
+
|| metadata['dsh.learning.runId'] !== immutableProof.runId
|
|
2840
|
+
|| metadata['dsh.learning.situation'] !== situation
|
|
2841
|
+
|| metadata['dsh.learning.occurredAt'] !== String(immutableProof.receipt.completedAt)
|
|
2842
|
+
|| metadata['dsh.learning.objectiveStatus'] !== immutableProof.receipt.objectiveStatus
|
|
2843
|
+
|| metadata['dsh.learning.proofDigest'] !== immutableProof.proofDigest
|
|
2844
|
+
|| !target.intent.text.endsWith(goalOutcomeFeedbackFooter))
|
|
2845
|
+
return 'invalid-target';
|
|
2846
|
+
const owner = this.deliveryStore.getPrincipal(binding.principal);
|
|
2847
|
+
if (locator.bindingId !== binding.id || locator.bindingVersion !== binding.version
|
|
2848
|
+
|| locator.bindingGeneration !== binding.generation || locator.sessionId !== binding.sessionId
|
|
2849
|
+
|| locator.workspace !== binding.workspace || locator.preset !== binding.agentPreset
|
|
2850
|
+
|| locator.principalId !== externalPrincipalId(binding.principal)
|
|
2851
|
+
|| owner?.status !== 'active' || owner.role !== 'owner'
|
|
2852
|
+
|| locator.principalRecordId !== owner.id || locator.principalVersion !== owner.version)
|
|
2853
|
+
return 'invalid-target';
|
|
2854
|
+
let route;
|
|
2855
|
+
try {
|
|
2856
|
+
route = this.resolveOwnerRoute(locator.ownerRouteId);
|
|
2857
|
+
}
|
|
2858
|
+
catch {
|
|
2859
|
+
return 'invalid-target';
|
|
2860
|
+
}
|
|
2861
|
+
if (route.binding.id !== binding.id || route.binding.version !== binding.version
|
|
2862
|
+
|| route.binding.generation !== binding.generation || route.binding.sessionId !== binding.sessionId) {
|
|
2863
|
+
return 'invalid-target';
|
|
2864
|
+
}
|
|
2865
|
+
const issuer = this.context.get('assistantGoals', false);
|
|
2866
|
+
if (!isGoalOutcomeFeedbackAuthority(issuer))
|
|
2867
|
+
return 'unavailable';
|
|
2868
|
+
let goalOutcomeCapability;
|
|
2869
|
+
try {
|
|
2870
|
+
goalOutcomeCapability = issuer.issueOwnerGoalOutcomeFeedbackTarget(locator);
|
|
2871
|
+
}
|
|
2872
|
+
catch {
|
|
2873
|
+
return 'invalid-target';
|
|
2874
|
+
}
|
|
2875
|
+
const resolver = this.context.get('assistantGoals', false);
|
|
2876
|
+
if (!isGoalOutcomeFeedbackAuthority(resolver))
|
|
2877
|
+
return 'unavailable';
|
|
2878
|
+
let currentProof;
|
|
2879
|
+
try {
|
|
2880
|
+
currentProof = validateOwnerGoalOutcomeFeedbackProof(resolver.resolveOwnerGoalOutcomeFeedbackTarget(goalOutcomeCapability));
|
|
2881
|
+
}
|
|
2882
|
+
catch {
|
|
2883
|
+
return 'invalid-target';
|
|
2884
|
+
}
|
|
2885
|
+
// Expiry is not an identity change: the owner is allowed to label the
|
|
2886
|
+
// immutable result after its verifier freshness window has elapsed.
|
|
2887
|
+
if (acceptanceCanonicalJson(currentProof) !== acceptanceCanonicalJson(immutableProof))
|
|
2888
|
+
return 'invalid-target';
|
|
2889
|
+
const sink = this.evaluationSink;
|
|
2890
|
+
const registration = sink?.registration;
|
|
2891
|
+
if (sink === undefined || registration?.ownerRevisionProtocol !== 'owner-objective-revision/v2'
|
|
2892
|
+
|| typeof registration.inspect !== 'function')
|
|
2893
|
+
return 'unavailable';
|
|
2894
|
+
const objectiveStatus = parsed.kind === 'objective-status' ? 'achieved' : parsed.objectiveStatus;
|
|
2895
|
+
const operationId = `owner-feedback:${createHash('sha256')
|
|
2896
|
+
.update(JSON.stringify([inbox.id, inbox.envelopeHash])).digest('hex')}`;
|
|
2897
|
+
const digest = createHash('sha256').update('assistant-delivery-goal-outcome-feedback-v1\0')
|
|
2898
|
+
.update(JSON.stringify([binding.workspace, binding.agentPreset, locator.goalId, locator.assessmentId, target.id]))
|
|
2899
|
+
.digest('hex');
|
|
2900
|
+
const idempotencyKey = parsed.kind === 'objective-revision'
|
|
2901
|
+
? operationId : `assistant-delivery:goal-outcome-feedback-v1:${digest}`;
|
|
2902
|
+
const claims = Object.freeze({
|
|
2903
|
+
scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
|
|
2904
|
+
situation, subjectKind: 'goal-outcome', subjectRef: locator.assessmentId,
|
|
2905
|
+
runId: locator.assessmentId, outboxId: target.id, chatId: binding.conversation.chat,
|
|
2906
|
+
principalId: locator.principalId, bindingId: binding.id, objectiveStatus, goalOutcomeCapability,
|
|
2907
|
+
ownerCommand: { principalRecordId: locator.principalRecordId, principalVersion: locator.principalVersion,
|
|
2908
|
+
operationId, action: parsed.kind === 'objective-revision' ? parsed.action : 'initial',
|
|
2909
|
+
...(parsed.kind === 'objective-revision'
|
|
2910
|
+
? { expectedVersion: parsed.expectedVersion, previousStatus: parsed.previousStatus }
|
|
2911
|
+
: {}), },
|
|
2912
|
+
occurredAt: immutableProof.receipt.completedAt,
|
|
2913
|
+
initialIdempotencyKey: `assistant-delivery:goal-outcome-feedback-v1:${digest}`,
|
|
2914
|
+
idempotencyKey,
|
|
2915
|
+
});
|
|
2916
|
+
try {
|
|
2917
|
+
const capabilityReceipt = registration.issueCapability(claims);
|
|
2918
|
+
if (this.evaluationSink?.token !== sink.token)
|
|
2919
|
+
return 'unknown';
|
|
2920
|
+
if (parsed.kind === 'objective-status')
|
|
2921
|
+
return registration.inspect(capabilityReceipt) ?? 'recorded';
|
|
2922
|
+
const receipt = await Promise.resolve(registration.append({
|
|
2923
|
+
capabilityReceipt, runId: locator.assessmentId, outboxId: target.id,
|
|
2924
|
+
chatId: binding.conversation.chat, principalId: locator.principalId,
|
|
2925
|
+
bindingId: binding.id, idempotencyKey,
|
|
2926
|
+
}));
|
|
2927
|
+
if (typeof receipt !== 'object' || receipt === null
|
|
2928
|
+
|| typeof receipt.idempotencyKey !== 'string')
|
|
2929
|
+
return 'unknown';
|
|
2930
|
+
return receipt
|
|
2931
|
+
.ownerFeedbackState ?? 'recorded';
|
|
2565
2932
|
}
|
|
2566
2933
|
catch (error) {
|
|
2567
2934
|
if (typeof error === 'object' && error !== null && 'code' in error
|
|
2568
|
-
&&
|
|
2935
|
+
&& ['idempotency-conflict', 'version-conflict'].includes(String(error.code))) {
|
|
2569
2936
|
return 'conflict';
|
|
2937
|
+
}
|
|
2570
2938
|
return 'unknown';
|
|
2571
2939
|
}
|
|
2572
2940
|
}
|
|
@@ -2632,7 +3000,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
2632
3000
|
const agents = ctx.get('agents');
|
|
2633
3001
|
const sessions = ctx.get('sessions');
|
|
2634
3002
|
const agent = request.agent;
|
|
2635
|
-
const events = agent.session.
|
|
3003
|
+
const events = agent.session.snapshotEvents();
|
|
2636
3004
|
const reviewer = approvalReviewerOf(events);
|
|
2637
3005
|
const reviewRoute = reviewer === 'user'
|
|
2638
3006
|
? 'user'
|
|
@@ -2706,7 +3074,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
2706
3074
|
['owner', owner.id, owner.version, owner.role, owner.status, owner.linkedToId ?? null,
|
|
2707
3075
|
[owner.principal.channel, owner.principal.account, owner.principal.tenant, owner.principal.user]],
|
|
2708
3076
|
['session', header.version, sessionId, header.createdAt, header.cwd ?? null,
|
|
2709
|
-
header.parentSession ?? null, header.
|
|
3077
|
+
header.parentSession ?? null, header.isSeeded, agent.session.inheritedEventCount, header.origin ?? null,
|
|
2710
3078
|
header.delegationDepth ?? null, header.agentPreset ?? null],
|
|
2711
3079
|
['route-kind', route.kind],
|
|
2712
3080
|
['review-route', reviewRoute],
|
|
@@ -2737,7 +3105,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
2737
3105
|
return next();
|
|
2738
3106
|
let reviewer;
|
|
2739
3107
|
try {
|
|
2740
|
-
reviewer = approvalReviewerOf(request.agent.session.
|
|
3108
|
+
reviewer = approvalReviewerOf(request.agent.session.snapshotEvents());
|
|
2741
3109
|
}
|
|
2742
3110
|
catch {
|
|
2743
3111
|
return 'unavailable';
|
|
@@ -2861,7 +3229,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
2861
3229
|
}
|
|
2862
3230
|
return approvalOutcomes.includes(outcome) ? outcome : 'unavailable';
|
|
2863
3231
|
}
|
|
2864
|
-
async acceptInbound(envelope) {
|
|
3232
|
+
async acceptInbound(envelope, recoverySignal) {
|
|
2865
3233
|
this.assertActive();
|
|
2866
3234
|
const accepted = this.deliveryStore.acceptInbound(envelope);
|
|
2867
3235
|
let record = accepted.record;
|
|
@@ -2882,7 +3250,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
2882
3250
|
const sessionCommand = parseDeliveryCommand(envelope);
|
|
2883
3251
|
if (isExactDeliveryCommand(sessionCommand, 'new', 'clear')) {
|
|
2884
3252
|
return await this.runConversationTransition(envelope.conversation, async () => {
|
|
2885
|
-
await this.recoverPendingInbound(envelope, record.id);
|
|
3253
|
+
await this.recoverPendingInbound(envelope, record.id, recoverySignal);
|
|
2886
3254
|
// A provider retry can arrive while the first copy is waiting for the
|
|
2887
3255
|
// old generation to drain. Re-read after acquiring the transition:
|
|
2888
3256
|
// once that copy queued the command, its rotation is already complete
|
|
@@ -2896,7 +3264,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
2896
3264
|
};
|
|
2897
3265
|
}
|
|
2898
3266
|
record = currentRecord;
|
|
2899
|
-
const binding = await this.bindingForInbound(envelope, record.id);
|
|
3267
|
+
const binding = await this.bindingForInbound(envelope, record.id, recoverySignal);
|
|
2900
3268
|
if (binding === undefined) {
|
|
2901
3269
|
const failed = this.deliveryStore.getInbox(record.id);
|
|
2902
3270
|
return { duplicate: accepted.duplicate, inboxId: failed.id, status: failed.status };
|
|
@@ -2911,7 +3279,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
2911
3279
|
});
|
|
2912
3280
|
await this.runtime?.cancelActive?.(binding, 'new');
|
|
2913
3281
|
await this.inbound.cancelUndispatchedClaims(fence.claimedInboxIds, 'new');
|
|
2914
|
-
const rotated = await this.rotateBinding(envelope, binding, record.id);
|
|
3282
|
+
const rotated = await this.rotateBinding(envelope, binding, record.id, recoverySignal);
|
|
2915
3283
|
record = this.deliveryStore.getInbox(record.id);
|
|
2916
3284
|
if (record.status !== 'queued' || record.bindingId !== rotated.id) {
|
|
2917
3285
|
throw new AssistantDeliveryError('runtime-conflict', 'new-session binding committed without its command Inbox');
|
|
@@ -2921,7 +3289,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
2921
3289
|
}
|
|
2922
3290
|
if (isExactDeliveryCommand(sessionCommand, 'stop')) {
|
|
2923
3291
|
return await this.runConversationTransition(envelope.conversation, async () => {
|
|
2924
|
-
await this.recoverPendingInbound(envelope, record.id);
|
|
3292
|
+
await this.recoverPendingInbound(envelope, record.id, recoverySignal);
|
|
2925
3293
|
const currentRecord = this.deliveryStore.getInbox(record.id);
|
|
2926
3294
|
if (!['received', 'authorized'].includes(currentRecord.status)) {
|
|
2927
3295
|
return {
|
|
@@ -2931,7 +3299,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
2931
3299
|
};
|
|
2932
3300
|
}
|
|
2933
3301
|
record = currentRecord;
|
|
2934
|
-
const binding = await this.bindingForInbound(envelope, record.id);
|
|
3302
|
+
const binding = await this.bindingForInbound(envelope, record.id, recoverySignal);
|
|
2935
3303
|
if (binding === undefined) {
|
|
2936
3304
|
const failed = this.deliveryStore.getInbox(record.id);
|
|
2937
3305
|
return { duplicate: accepted.duplicate, inboxId: failed.id, status: failed.status };
|
|
@@ -2953,7 +3321,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
2953
3321
|
// It serializes only durable admission (not Agent execution), and makes a
|
|
2954
3322
|
// queued `/stop` or `/new` an unambiguous boundary for later arrivals.
|
|
2955
3323
|
return await this.runConversationTransition(envelope.conversation, async () => {
|
|
2956
|
-
await this.recoverPendingInbound(envelope, record.id);
|
|
3324
|
+
await this.recoverPendingInbound(envelope, record.id, recoverySignal);
|
|
2957
3325
|
const currentRecord = this.deliveryStore.getInbox(record.id);
|
|
2958
3326
|
if (!['received', 'authorized'].includes(currentRecord.status)) {
|
|
2959
3327
|
return {
|
|
@@ -2963,7 +3331,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
2963
3331
|
};
|
|
2964
3332
|
}
|
|
2965
3333
|
record = currentRecord;
|
|
2966
|
-
const binding = await this.bindingForInbound(envelope, record.id);
|
|
3334
|
+
const binding = await this.bindingForInbound(envelope, record.id, recoverySignal);
|
|
2967
3335
|
if (binding === undefined) {
|
|
2968
3336
|
const failed = this.deliveryStore.getInbox(record.id);
|
|
2969
3337
|
return { duplicate: accepted.duplicate, inboxId: failed.id, status: failed.status };
|
|
@@ -2974,9 +3342,12 @@ export class AssistantDeliveryService extends Service {
|
|
|
2974
3342
|
}
|
|
2975
3343
|
enqueueBackground(input) {
|
|
2976
3344
|
this.assertActive();
|
|
2977
|
-
if (containsReservedLearningMetadata(input.metadata)) {
|
|
2978
|
-
throw new AssistantDeliveryError('runtime-conflict', 'reserved
|
|
3345
|
+
if (containsReservedLearningMetadata(input.metadata) || Object.keys(input.metadata ?? {}).some(key => key.startsWith('dsh.native-notice'))) {
|
|
3346
|
+
throw new AssistantDeliveryError('runtime-conflict', 'reserved metadata requires its typed delivery interface');
|
|
2979
3347
|
}
|
|
3348
|
+
return this.enqueueBackgroundAuthorized(input);
|
|
3349
|
+
}
|
|
3350
|
+
enqueueBackgroundAuthorized(input) {
|
|
2980
3351
|
const binding = this.deliveryStore.getBinding(input.bindingId);
|
|
2981
3352
|
if (binding === undefined || binding.status !== 'active' || !this.hasActivePrincipal(binding)) {
|
|
2982
3353
|
throw new AssistantDeliveryError('missing-binding', 'delivery binding does not exist or is revoked');
|
|
@@ -2997,6 +3368,126 @@ export class AssistantDeliveryService extends Service {
|
|
|
2997
3368
|
format: input.format ?? 'plain',
|
|
2998
3369
|
...(input.metadata === undefined ? {} : { metadata: input.metadata }) });
|
|
2999
3370
|
}
|
|
3371
|
+
#notificationGuard(record) {
|
|
3372
|
+
const metadata = record.intent.metadata;
|
|
3373
|
+
const nativeKeys = Object.keys(metadata ?? {}).filter(key => key.startsWith('dsh.native-notice'));
|
|
3374
|
+
if (nativeKeys.length > 0) {
|
|
3375
|
+
// Native adapter sends receive only an intent, so never bind a fabricated
|
|
3376
|
+
// outbox id into the goal-outcome sidecar lookup. A persisted record still
|
|
3377
|
+
// rejects any sidecar collision before native authority is considered.
|
|
3378
|
+
if (containsReservedLearningMetadata(metadata))
|
|
3379
|
+
return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
|
|
3380
|
+
if (typeof record.id === 'string') {
|
|
3381
|
+
try {
|
|
3382
|
+
if (this.deliveryStore.getGoalOutcomeTarget(record.id) !== undefined)
|
|
3383
|
+
return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
|
|
3384
|
+
}
|
|
3385
|
+
catch {
|
|
3386
|
+
return { outcome: 'not-sent', failureCode: 'goal-outcome-target-invalid', retryable: false };
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
return this.#nativeNoticeGuard(record.intent);
|
|
3390
|
+
}
|
|
3391
|
+
let goalTarget;
|
|
3392
|
+
try {
|
|
3393
|
+
goalTarget = this.deliveryStore.getGoalOutcomeTarget(record.id);
|
|
3394
|
+
}
|
|
3395
|
+
catch {
|
|
3396
|
+
return { outcome: 'not-sent', failureCode: 'goal-outcome-target-invalid', retryable: false };
|
|
3397
|
+
}
|
|
3398
|
+
if (goalTarget !== undefined || metadata?.['dsh.learning.kind'] === 'goal-outcome') {
|
|
3399
|
+
if (goalTarget === undefined) {
|
|
3400
|
+
return { outcome: 'not-sent', failureCode: 'goal-outcome-target-missing', retryable: false };
|
|
3401
|
+
}
|
|
3402
|
+
const locator = goalTarget.locator;
|
|
3403
|
+
const proof = goalTarget.proof;
|
|
3404
|
+
const binding = this.deliveryStore.getBinding(record.intent.bindingId);
|
|
3405
|
+
const owner = binding === undefined ? undefined : this.deliveryStore.getPrincipal(binding.principal);
|
|
3406
|
+
try {
|
|
3407
|
+
if (proof.receipt.completedAt > Date.now() || proof.receipt.validUntil <= Date.now()) {
|
|
3408
|
+
throw new Error('goal outcome receipt expired');
|
|
3409
|
+
}
|
|
3410
|
+
// One captured service reference must both issue and resolve the
|
|
3411
|
+
// process-local capability. A replacement cannot inherit authority.
|
|
3412
|
+
const goals = this.context.get('assistantGoals', false);
|
|
3413
|
+
if (!isGoalOutcomeFeedbackAuthority(goals))
|
|
3414
|
+
throw new Error('Goals unavailable');
|
|
3415
|
+
const capability = goals.issueOwnerGoalOutcomeFeedbackTarget(locator);
|
|
3416
|
+
const currentGoals = this.context.get('assistantGoals', false);
|
|
3417
|
+
if (!isGoalOutcomeFeedbackAuthority(currentGoals))
|
|
3418
|
+
throw new Error('current Goals unavailable');
|
|
3419
|
+
const currentProof = validateOwnerGoalOutcomeFeedbackProof(currentGoals.resolveOwnerGoalOutcomeFeedbackTarget(capability));
|
|
3420
|
+
if (acceptanceCanonicalJson(currentProof) !== acceptanceCanonicalJson(proof)) {
|
|
3421
|
+
throw new Error('goal outcome proof changed');
|
|
3422
|
+
}
|
|
3423
|
+
const route = this.resolveOwnerRoute(locator.ownerRouteId);
|
|
3424
|
+
if (binding?.status !== 'active' || route.binding.id !== binding.id
|
|
3425
|
+
|| route.binding.version !== locator.bindingVersion
|
|
3426
|
+
|| route.binding.generation !== locator.bindingGeneration
|
|
3427
|
+
|| route.binding.sessionId !== locator.sessionId || binding.workspace !== locator.workspace
|
|
3428
|
+
|| binding.agentPreset !== locator.preset || externalPrincipalId(binding.principal) !== locator.principalId
|
|
3429
|
+
|| owner?.status !== 'active' || owner.role !== 'owner'
|
|
3430
|
+
|| owner.id !== locator.principalRecordId || owner.version !== locator.principalVersion) {
|
|
3431
|
+
throw new Error('goal outcome route changed');
|
|
3432
|
+
}
|
|
3433
|
+
const permission = this.policy.evaluate({ subject: { kind: 'background', id: 'assistant-goals-wake/v1',
|
|
3434
|
+
workspace: locator.workspace, principal: locator.principalId }, action: 'send',
|
|
3435
|
+
resource: { kind: 'message', id: binding.id }, context: { initiator: 'background' } });
|
|
3436
|
+
if (permission.effect !== 'allow')
|
|
3437
|
+
throw new Error('goal outcome send denied');
|
|
3438
|
+
}
|
|
3439
|
+
catch {
|
|
3440
|
+
return { outcome: 'not-sent', failureCode: 'goal-outcome-authority-revoked', retryable: false };
|
|
3441
|
+
}
|
|
3442
|
+
return undefined;
|
|
3443
|
+
}
|
|
3444
|
+
return undefined;
|
|
3445
|
+
}
|
|
3446
|
+
#nativeNoticeGuard(intent) {
|
|
3447
|
+
const metadata = intent.metadata;
|
|
3448
|
+
const nativeKeys = Object.keys(metadata ?? {}).filter(key => key.startsWith('dsh.native-notice'));
|
|
3449
|
+
if (nativeKeys.length === 0 || containsReservedLearningMetadata(metadata))
|
|
3450
|
+
return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
|
|
3451
|
+
if (metadata?.['dsh.native-notice'] !== 'v1')
|
|
3452
|
+
return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
|
|
3453
|
+
const expiresAt = Number(metadata['dsh.native-notice.expiresAt']);
|
|
3454
|
+
const binding = this.deliveryStore.getBinding(intent.bindingId);
|
|
3455
|
+
const owner = binding === undefined ? undefined : this.deliveryStore.getPrincipal(binding.principal);
|
|
3456
|
+
if (!Number.isSafeInteger(expiresAt) || Date.now() >= expiresAt || binding?.status !== 'active' || owner?.status !== 'active' || owner.role !== 'owner'
|
|
3457
|
+
|| owner.id !== metadata['dsh.native-notice.ownerRecordId'] || String(owner.version) !== metadata['dsh.native-notice.ownerVersion']
|
|
3458
|
+
|| String(binding.version) !== metadata['dsh.native-notice.bindingVersion'] || String(binding.generation) !== metadata['dsh.native-notice.bindingGeneration'] || binding.sessionId !== metadata['dsh.native-notice.sessionId'])
|
|
3459
|
+
return { outcome: 'not-sent', failureCode: 'native-notice-authority-expired', retryable: false };
|
|
3460
|
+
try {
|
|
3461
|
+
const sourceId = canonicalBackgroundSourceId(metadata['dsh.native-notice.sourceId']);
|
|
3462
|
+
const current = this.resolveOwnerRoute(metadata['dsh.native-notice.ownerRouteId']).binding;
|
|
3463
|
+
if (current.id !== binding.id || current.version !== binding.version || current.generation !== binding.generation || current.sessionId !== binding.sessionId)
|
|
3464
|
+
throw new Error('route changed');
|
|
3465
|
+
const permission = this.policy.evaluate({ subject: { kind: 'background', id: sourceId, workspace: binding.workspace, principal: externalPrincipalId(binding.principal) }, action: 'send', resource: { kind: 'message', id: binding.id }, context: { initiator: 'background' } });
|
|
3466
|
+
if (permission.effect !== 'allow')
|
|
3467
|
+
throw new Error('send denied');
|
|
3468
|
+
}
|
|
3469
|
+
catch {
|
|
3470
|
+
return { outcome: 'not-sent', failureCode: 'native-notice-policy-revoked', retryable: false };
|
|
3471
|
+
}
|
|
3472
|
+
return undefined;
|
|
3473
|
+
}
|
|
3474
|
+
#notificationProviderId(intent) { return `native-notice:${createHash('sha256').update(intent.idempotencyKey).update('\u0000').update(intent.bindingId).update('\u0000').update(intent.text).digest('hex')}`; }
|
|
3475
|
+
/** Queue a typed owner notice. Accepted means queued; it never asserts user read. */
|
|
3476
|
+
enqueueOwnerNotification(input) {
|
|
3477
|
+
this.assertActive();
|
|
3478
|
+
if (!Number.isSafeInteger(input.expiresAt) || input.expiresAt <= Date.now() || typeof input.text !== 'string' || input.text.length === 0 || input.text.length > 16_384)
|
|
3479
|
+
throw new AssistantDeliveryError('policy-denied', 'owner notification is invalid or expired');
|
|
3480
|
+
const resolved = this.resolveOwnerRoute(input.ownerRouteId);
|
|
3481
|
+
const binding = resolved.binding;
|
|
3482
|
+
const owner = this.deliveryStore.getPrincipal(binding.principal);
|
|
3483
|
+
if (binding.sessionId !== input.sessionId || binding.workspace !== input.scope.workspace || binding.agentPreset !== input.scope.preset
|
|
3484
|
+
|| externalPrincipalId(binding.principal) !== input.scope.principalId || owner?.id !== input.scope.principalRecordId || owner.version !== input.scope.principalVersion)
|
|
3485
|
+
throw new AssistantDeliveryError('policy-denied', 'owner notification authority is no longer current');
|
|
3486
|
+
return this.enqueueBackgroundAuthorized({ sourceId: input.sourceId, workspace: binding.workspace, bindingId: binding.id, idempotencyKey: input.idempotencyKey, text: input.text, format: 'plain', metadata: {
|
|
3487
|
+
'dsh.native-notice': 'v1', 'dsh.native-notice.sourceId': input.sourceId, 'dsh.native-notice.ownerRouteId': input.ownerRouteId, 'dsh.native-notice.expiresAt': String(input.expiresAt), 'dsh.native-notice.ownerRecordId': owner.id,
|
|
3488
|
+
'dsh.native-notice.ownerVersion': String(owner.version), 'dsh.native-notice.bindingVersion': String(binding.version), 'dsh.native-notice.bindingGeneration': String(binding.generation), 'dsh.native-notice.sessionId': binding.sessionId,
|
|
3489
|
+
} });
|
|
3490
|
+
}
|
|
3000
3491
|
/**
|
|
3001
3492
|
* Dedicated Automation result lane. Every learning field is derived from an
|
|
3002
3493
|
* authoritative Automations receipt; callers cannot supply metadata,
|
|
@@ -3402,6 +3893,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
3402
3893
|
}
|
|
3403
3894
|
async tick() {
|
|
3404
3895
|
this.assertActive();
|
|
3896
|
+
await this.drainInboundAdmissions();
|
|
3405
3897
|
await this.inbound.tick();
|
|
3406
3898
|
await this.drainModelSelectionSettlements();
|
|
3407
3899
|
this.drainApprovalDispatches();
|
|
@@ -3419,6 +3911,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
3419
3911
|
this.presentationFlight,
|
|
3420
3912
|
this.preferenceProjectionFlight,
|
|
3421
3913
|
this.workflowTraceFlight,
|
|
3914
|
+
this.inboundAdmissionRecoveryFlight,
|
|
3422
3915
|
]);
|
|
3423
3916
|
}
|
|
3424
3917
|
start() {
|
|
@@ -3433,7 +3926,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
3433
3926
|
this.assertActive();
|
|
3434
3927
|
await this.stopInternal();
|
|
3435
3928
|
}
|
|
3436
|
-
async ensureBinding(envelope) {
|
|
3929
|
+
async ensureBinding(envelope, signal) {
|
|
3437
3930
|
const key = JSON.stringify(envelope.conversation);
|
|
3438
3931
|
const existing = this.bindingFlights.get(key);
|
|
3439
3932
|
if (existing !== undefined)
|
|
@@ -3442,11 +3935,15 @@ export class AssistantDeliveryService extends Service {
|
|
|
3442
3935
|
if (runtime === undefined)
|
|
3443
3936
|
throw new AssistantDeliveryError('runtime-unavailable', 'inbound runtime is unavailable');
|
|
3444
3937
|
const promise = (async () => {
|
|
3938
|
+
signal?.throwIfAborted();
|
|
3445
3939
|
const current = this.deliveryStore.getActiveBinding(envelope.conversation);
|
|
3446
3940
|
if (current !== undefined)
|
|
3447
3941
|
return current;
|
|
3448
3942
|
const generation = this.deliveryStore.nextBindingGeneration(envelope.conversation);
|
|
3449
|
-
const created = await runtime.createSession({
|
|
3943
|
+
const created = await runtime.createSession({
|
|
3944
|
+
envelope, generation, signal: signal ?? new AbortController().signal,
|
|
3945
|
+
});
|
|
3946
|
+
signal?.throwIfAborted();
|
|
3450
3947
|
this.assertActive();
|
|
3451
3948
|
return this.deliveryStore.createBinding({ conversation: envelope.conversation, principal: envelope.principal,
|
|
3452
3949
|
workspace: created.workspace, agentPreset: created.agentPreset, sessionId: created.sessionId,
|
|
@@ -3455,14 +3952,68 @@ export class AssistantDeliveryService extends Service {
|
|
|
3455
3952
|
this.bindingFlights.set(key, promise);
|
|
3456
3953
|
return promise;
|
|
3457
3954
|
}
|
|
3458
|
-
|
|
3955
|
+
drainInboundAdmissions() {
|
|
3956
|
+
if (this.inboundAdmissionRecoveryFlight !== undefined)
|
|
3957
|
+
return this.inboundAdmissionRecoveryFlight;
|
|
3958
|
+
const flight = this.runInboundAdmissionRecovery()
|
|
3959
|
+
.finally(() => { if (this.inboundAdmissionRecoveryFlight === flight)
|
|
3960
|
+
this.inboundAdmissionRecoveryFlight = undefined; });
|
|
3961
|
+
this.inboundAdmissionRecoveryFlight = flight;
|
|
3962
|
+
return flight;
|
|
3963
|
+
}
|
|
3964
|
+
async runInboundAdmissionRecovery() {
|
|
3965
|
+
if (this.runtime === undefined)
|
|
3966
|
+
return;
|
|
3967
|
+
let pending = this.deliveryStore.listRecoverableInboundAdmissions({
|
|
3968
|
+
afterSequence: this.inboundAdmissionRecoveryAfterSequence,
|
|
3969
|
+
limit: 100,
|
|
3970
|
+
});
|
|
3971
|
+
if (pending.length === 0 && this.inboundAdmissionRecoveryAfterSequence !== 0) {
|
|
3972
|
+
this.inboundAdmissionRecoveryAfterSequence = 0;
|
|
3973
|
+
pending = this.deliveryStore.listRecoverableInboundAdmissions({ limit: 100 });
|
|
3974
|
+
}
|
|
3975
|
+
for (const admission of pending) {
|
|
3976
|
+
if (!this.active || this.runtime === undefined)
|
|
3977
|
+
return;
|
|
3978
|
+
this.inboundAdmissionRecoveryController.signal.throwIfAborted();
|
|
3979
|
+
this.inboundAdmissionRecoveryAfterSequence = admission.admissionCursor.sequence;
|
|
3980
|
+
try {
|
|
3981
|
+
const current = this.deliveryStore.getInbox(admission.id);
|
|
3982
|
+
if (current === undefined || !['received', 'authorized'].includes(current.status))
|
|
3983
|
+
continue;
|
|
3984
|
+
await this.acceptInbound(current.envelope, this.inboundAdmissionRecoveryController.signal);
|
|
3985
|
+
}
|
|
3986
|
+
catch (error) {
|
|
3987
|
+
if (!this.active)
|
|
3988
|
+
return;
|
|
3989
|
+
this.context.logger.warn(`assistant-delivery: deferred recovery of Inbox ${admission.id}: ${String(error)}`);
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
async bindingForInbound(envelope, inboxId, signal) {
|
|
3994
|
+
const admission = this.deliveryStore.getInbox(inboxId);
|
|
3995
|
+
if (admission === undefined || !['received', 'authorized'].includes(admission.status)) {
|
|
3996
|
+
return undefined;
|
|
3997
|
+
}
|
|
3459
3998
|
let binding = this.deliveryStore.getActiveBinding(envelope.conversation);
|
|
3999
|
+
if (admission.status === 'authorized') {
|
|
4000
|
+
const recorded = admission.bindingId === undefined
|
|
4001
|
+
? undefined
|
|
4002
|
+
: this.deliveryStore.getBinding(admission.bindingId);
|
|
4003
|
+
if (recorded === undefined || binding?.id !== recorded.id
|
|
4004
|
+
|| binding.version !== recorded.version || binding.generation !== recorded.generation
|
|
4005
|
+
|| binding.sessionId !== recorded.sessionId) {
|
|
4006
|
+
this.deliveryStore.deadLetterInbox(inboxId, 'inbound-binding-superseded');
|
|
4007
|
+
return undefined;
|
|
4008
|
+
}
|
|
4009
|
+
binding = recorded;
|
|
4010
|
+
}
|
|
3460
4011
|
if (binding === undefined) {
|
|
3461
4012
|
if (this.runtime === undefined) {
|
|
3462
4013
|
this.deliveryStore.deadLetterInbox(inboxId, 'runtime-unavailable');
|
|
3463
4014
|
return undefined;
|
|
3464
4015
|
}
|
|
3465
|
-
binding = await this.ensureBinding(envelope);
|
|
4016
|
+
binding = await this.ensureBinding(envelope, signal);
|
|
3466
4017
|
}
|
|
3467
4018
|
if (externalPrincipalId(binding.principal) !== externalPrincipalId(envelope.principal)) {
|
|
3468
4019
|
this.deliveryStore.deadLetterInbox(inboxId, 'binding-principal-mismatch');
|
|
@@ -3477,8 +4028,9 @@ export class AssistantDeliveryService extends Service {
|
|
|
3477
4028
|
* original durable Inbox order. This includes ordinary messages that were
|
|
3478
4029
|
* already persisted behind a blocked `/stop` or `/new` transition.
|
|
3479
4030
|
*/
|
|
3480
|
-
async recoverPendingInbound(boundary, beforeInboxId) {
|
|
4031
|
+
async recoverPendingInbound(boundary, beforeInboxId, signal) {
|
|
3481
4032
|
while (true) {
|
|
4033
|
+
signal?.throwIfAborted();
|
|
3482
4034
|
const pending = this.deliveryStore.findPendingInboundBefore({
|
|
3483
4035
|
conversation: boundary.conversation,
|
|
3484
4036
|
principal: boundary.principal,
|
|
@@ -3503,7 +4055,8 @@ export class AssistantDeliveryService extends Service {
|
|
|
3503
4055
|
const command = parseDeliveryCommand(pending.envelope);
|
|
3504
4056
|
const reset = isExactDeliveryCommand(command, 'new', 'clear');
|
|
3505
4057
|
const stop = isExactDeliveryCommand(command, 'stop');
|
|
3506
|
-
const binding = await this.bindingForInbound(pending.envelope, pending.id);
|
|
4058
|
+
const binding = await this.bindingForInbound(pending.envelope, pending.id, signal);
|
|
4059
|
+
signal?.throwIfAborted();
|
|
3507
4060
|
if (binding === undefined)
|
|
3508
4061
|
continue;
|
|
3509
4062
|
if (!reset && !stop) {
|
|
@@ -3517,9 +4070,11 @@ export class AssistantDeliveryService extends Service {
|
|
|
3517
4070
|
});
|
|
3518
4071
|
const cancellationCommand = reset ? 'new' : 'stop';
|
|
3519
4072
|
await this.runtime?.cancelActive?.(binding, cancellationCommand);
|
|
4073
|
+
signal?.throwIfAborted();
|
|
3520
4074
|
await this.inbound.cancelUndispatchedClaims(fence.claimedInboxIds, cancellationCommand);
|
|
4075
|
+
signal?.throwIfAborted();
|
|
3521
4076
|
if (reset) {
|
|
3522
|
-
await this.rotateBinding(pending.envelope, binding, pending.id);
|
|
4077
|
+
await this.rotateBinding(pending.envelope, binding, pending.id, signal);
|
|
3523
4078
|
}
|
|
3524
4079
|
else {
|
|
3525
4080
|
this.deliveryStore.queueInbox(pending.id, binding.id);
|
|
@@ -3543,7 +4098,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
3543
4098
|
this.conversationTransitions.delete(key);
|
|
3544
4099
|
}
|
|
3545
4100
|
}
|
|
3546
|
-
async rotateBinding(envelope, previous, commandInboxId) {
|
|
4101
|
+
async rotateBinding(envelope, previous, commandInboxId, signal) {
|
|
3547
4102
|
const key = `${JSON.stringify(envelope.conversation)}:/new:${envelope.eventId}`;
|
|
3548
4103
|
const existing = this.bindingFlights.get(key);
|
|
3549
4104
|
if (existing !== undefined)
|
|
@@ -3552,6 +4107,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
3552
4107
|
if (runtime === undefined)
|
|
3553
4108
|
throw new AssistantDeliveryError('runtime-unavailable', 'inbound runtime is unavailable');
|
|
3554
4109
|
const promise = (async () => {
|
|
4110
|
+
signal?.throwIfAborted();
|
|
3555
4111
|
const current = this.deliveryStore.getActiveBinding(envelope.conversation);
|
|
3556
4112
|
if (current === undefined)
|
|
3557
4113
|
throw new AssistantDeliveryError('missing-binding', 'active binding disappeared during /new');
|
|
@@ -3562,7 +4118,8 @@ export class AssistantDeliveryService extends Service {
|
|
|
3562
4118
|
throw new AssistantDeliveryError('runtime-conflict', 'binding generation changed before /new session creation');
|
|
3563
4119
|
}
|
|
3564
4120
|
const created = await runtime.createSession({ envelope, generation,
|
|
3565
|
-
previous: current, signal: new AbortController().signal });
|
|
4121
|
+
previous: current, signal: signal ?? new AbortController().signal });
|
|
4122
|
+
signal?.throwIfAborted();
|
|
3566
4123
|
this.assertActive();
|
|
3567
4124
|
return this.deliveryStore.rotateBindingAndQueueCommand({
|
|
3568
4125
|
bindingId: current.id,
|
|
@@ -3588,9 +4145,20 @@ export class AssistantDeliveryService extends Service {
|
|
|
3588
4145
|
this.workflowTraceRetryTimer = undefined;
|
|
3589
4146
|
this.preferenceProjectionRetryTimer = undefined;
|
|
3590
4147
|
this.cancelModelSelectionWaiters();
|
|
4148
|
+
this.inboundAdmissionRecoveryController.abort(new Error('assistant-delivery is stopping'));
|
|
4149
|
+
const admissionRecovery = this.inboundAdmissionRecoveryFlight === undefined
|
|
4150
|
+
? undefined
|
|
4151
|
+
: Promise.race([
|
|
4152
|
+
this.inboundAdmissionRecoveryFlight,
|
|
4153
|
+
new Promise(resolve => {
|
|
4154
|
+
const timeout = setTimeout(resolve, Math.max(1, Math.floor(this.config.leaseMs / 3)));
|
|
4155
|
+
timeout.unref?.();
|
|
4156
|
+
}),
|
|
4157
|
+
]);
|
|
3591
4158
|
await Promise.all([
|
|
3592
4159
|
this.inbound.stop(),
|
|
3593
4160
|
this.outbound.stop(),
|
|
4161
|
+
admissionRecovery,
|
|
3594
4162
|
this.modelSelectionFlight,
|
|
3595
4163
|
this.presentationFlight,
|
|
3596
4164
|
this.preferenceProjectionFlight,
|