@dsh-enhanced/assistant-delivery 0.1.32 → 0.1.34

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/lib/service.js CHANGED
@@ -3,21 +3,23 @@ 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
+ import { acceptanceCanonicalJson, goalDefinitionSituation } from '@dsh-enhanced/task-acceptance-contract';
7
7
  import { approvalReviewerOf, isAutoReviewEscalation, } from '@dsh-enhanced/assistant-policy';
8
8
  import { TRUSTED_EVALUATION_PRODUCER_PROTOCOL, } from '@dsh-enhanced/assistant-evaluation';
9
9
  import { ASSISTANT_GROWTH_CONTRACT_VERSION, growthObjectDigest, validateResolvedWorkflowAutomationTemplate, validateWorkflowAutomationTemplate, validateWorkflowScope, validateWorkflowTraceProjectionReceipt, workflowArgumentShapeDigest, } from '@dsh-enhanced/assistant-growth-contract';
10
10
  import { DeliveryAdapterRegistry, DeliveryCoordinator, InboundCoordinator, } from './coordinator.js';
11
- import { DeliveryStore, DeliveryStoreError } from './store.js';
11
+ import { DeliveryStore, DeliveryStoreError, } from './store.js';
12
12
  import { DshDeliveryRuntime } from './agent-runtime.js';
13
13
  import { DeliverySessionLeases } from './session-lease-runtime.js';
14
14
  import { NativeWebOwner } from './native-web-owner.js';
15
15
  import { validateOwnerGoalOutcomeFeedbackLocator, validateOwnerGoalOutcomeFeedbackProof, } from './goal-wake-types.js';
16
+ import { isSynchronousForegroundObservationHandle, validForegroundObservationRegistration } from './foreground-observation.js';
16
17
  import { InboundImageMaterializer } from './inbound-images.js';
17
18
  import { registerDeliveryTools } from './tools.js';
18
19
  import { bindingMatchesOwnerRoute, canonicalBackgroundSourceId as canonicalHostBackgroundSourceId, canonicalConversation, canonicalLocalOperatorId, canonicalOwnerRouteAuthority, externalPrincipalId, ownerRouteAuthorityHash, ownerRouteBindingSnapshot, } from './canonical.js';
19
20
  import { isExactDeliveryCommand, parseDeliveryCommand } from './session-commands.js';
20
21
  import { deliveryT1PreferenceKeys, deliveryT1PreferenceValues } from './learning-command.js';
22
+ import { assertReplyReplayAllowed, blockAgentRepliesForReplay as createReplyReplayBlock, } from './reply-replay.js';
21
23
  import { feedbackSignalInput, parseFeedbackCommand, classifyNaturalPreferenceDirective, observedResponseLanguage, } from './feedback-command.js';
22
24
  import { DELIVERY_PREFERENCE_PROJECTION_PROTOCOL, TRUSTED_DELIVERY_PRESENTATION_PRODUCER_PROTOCOL, } from './types.js';
23
25
  const trustedDeliveryPreferenceProducers = new WeakSet();
@@ -242,6 +244,23 @@ export class AssistantDeliveryError extends Error {
242
244
  this.name = 'AssistantDeliveryError';
243
245
  }
244
246
  }
247
+ function isOwnerAnchoredGoalsCapability(value) {
248
+ return typeof value === 'object' && value !== null
249
+ && typeof value.inspectOwnerVerifiedWorkflowSource === 'function'
250
+ && typeof value.inspectOwnerGoalRunProof === 'function';
251
+ }
252
+ function isOwnerGoalSnapshotLocator(value) {
253
+ if (typeof value !== 'object' || value === null)
254
+ return false;
255
+ const locator = value;
256
+ return Object.keys(locator).sort().join(',') === 'goalId,ownerRouteId,preset,principalId,sessionId,workspace'
257
+ && typeof locator['ownerRouteId'] === 'string' && locator['ownerRouteId'] !== ''
258
+ && typeof locator['principalId'] === 'string' && locator['principalId'] !== ''
259
+ && typeof locator['workspace'] === 'string' && locator['workspace'] !== ''
260
+ && typeof locator['preset'] === 'string' && locator['preset'] !== ''
261
+ && typeof locator['sessionId'] === 'string' && locator['sessionId'] !== ''
262
+ && typeof locator['goalId'] === 'string' && locator['goalId'] !== '';
263
+ }
245
264
  const configSchema = Schema.object({
246
265
  databasePath: Schema.string().required(),
247
266
  spoolPath: Schema.string().required(),
@@ -524,6 +543,7 @@ export class AssistantDeliveryService extends Service {
524
543
  preferenceProducerGeneration = `assistant-delivery-preference:${randomUUID()}`;
525
544
  acceptanceProducerGeneration = `assistant-delivery-acceptance:${randomUUID()}`;
526
545
  acceptanceSink;
546
+ foregroundObserver;
527
547
  /** Once a live verifier required acceptance, loss of that verifier must not silently bypass it. */
528
548
  acceptanceRequired = false;
529
549
  preferenceTurns = new WeakMap();
@@ -651,6 +671,10 @@ export class AssistantDeliveryService extends Service {
651
671
  maxTextBytes: config.maxTextBytes,
652
672
  prepareForegroundTaskAcceptance: (binding, envelope) => this.prepareForegroundTaskAcceptance(binding, envelope),
653
673
  completeForegroundTaskAcceptance: (handle, input) => this.completeForegroundTaskAcceptance(handle, input),
674
+ recordForegroundTaskModelSelection: (handle, input) => this.deliveryStore.recordForegroundTaskModelSelection({ contractId: handle.contractId, ...input }),
675
+ beginForegroundTaskExecution: (binding, envelope) => this.beginForegroundTaskExecution(binding, envelope),
676
+ recordForegroundExecutionModelSelection: (inboxId, input) => this.deliveryStore.recordForegroundExecutionModelSelection({ inboxId, ...input }),
677
+ completeForegroundTaskExecution: (inboxId, input) => this.completeForegroundTaskExecution(inboxId, input),
654
678
  modelPickerTtlMs: config.modelPickerTtlMs,
655
679
  permissionPickerTtlMs: config.permissionPickerTtlMs,
656
680
  getModelSelection: conversation => this.deliveryStore.getModelSelection(conversation),
@@ -731,6 +755,7 @@ export class AssistantDeliveryService extends Service {
731
755
  trustedDeliveryPreferenceProducers.delete(this);
732
756
  this.preferenceFeedbackSink = undefined;
733
757
  this.acceptanceSink = undefined;
758
+ this.foregroundObserver = undefined;
734
759
  this.evaluationSink = undefined;
735
760
  this.workflowTraceSink = undefined;
736
761
  this.automationPresentationBinding?.dispose();
@@ -1344,7 +1369,7 @@ export class AssistantDeliveryService extends Service {
1344
1369
  if (Buffer.byteLength(rendered, 'utf8') > this.config.maxTextBytes) {
1345
1370
  throw new AssistantDeliveryError('runtime-conflict', 'scheduled goal result exceeds the delivery text limit');
1346
1371
  }
1347
- const situation = `goal:${locator.goalId}:definition:${proof.goal.definitionVersion}`;
1372
+ const situation = goalDefinitionSituation(proof.goal.definitionDigest);
1348
1373
  return this.deliveryStore.enqueueGoalOutcomeTarget({
1349
1374
  locator, proof,
1350
1375
  intent: {
@@ -1353,7 +1378,7 @@ export class AssistantDeliveryService extends Service {
1353
1378
  target: { conversation: finalBinding.conversation, principal: finalBinding.principal },
1354
1379
  text: rendered, format: 'markdown',
1355
1380
  metadata: Object.freeze({
1356
- 'dsh.learning.schemaVersion': '3', 'dsh.learning.kind': 'goal-outcome',
1381
+ 'dsh.learning.schemaVersion': '4', 'dsh.learning.kind': 'goal-outcome',
1357
1382
  'dsh.learning.goalId': locator.goalId, 'dsh.learning.assessmentId': locator.assessmentId,
1358
1383
  'dsh.learning.runId': proof.runId, 'dsh.learning.situation': situation,
1359
1384
  'dsh.learning.occurredAt': String(proof.receipt.completedAt),
@@ -1475,6 +1500,91 @@ export class AssistantDeliveryService extends Service {
1475
1500
  this.acceptanceSink = undefined;
1476
1501
  };
1477
1502
  }
1503
+ beginForegroundTaskExecution(binding, envelope) {
1504
+ const owner = this.ownerLineageForBinding(binding);
1505
+ if (owner === undefined)
1506
+ return undefined;
1507
+ const inbox = this.deliveryStore.getInboxByProviderEvent(envelope.channel, envelope.account, envelope.eventId);
1508
+ if (!inbox || inbox.status !== 'claimed' || inbox.bindingId !== binding.id
1509
+ || JSON.stringify(inbox.envelope) !== JSON.stringify(envelope)) {
1510
+ throw new AssistantDeliveryError('missing-binding', 'foreground execution lacks an authenticated owner inbox');
1511
+ }
1512
+ const dispatchedAt = Date.now();
1513
+ this.deliveryStore.bindForegroundTaskExecution({ inboxId: inbox.id,
1514
+ scope: { workspace: binding.workspace, preset: binding.agentPreset }, owner, binding, dispatchedAt });
1515
+ this.observeForegroundBegin(Object.freeze({ protocol: 'assistant-delivery/foreground-task/v1', inboxId: inbox.id,
1516
+ sessionId: binding.sessionId, scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
1517
+ owner: Object.freeze({ principalRecordId: owner.principalRecordId, principalVersion: owner.principalVersion }),
1518
+ binding: Object.freeze({ id: binding.id, version: binding.version, generation: binding.generation }), dispatchedAt }));
1519
+ return inbox.id;
1520
+ }
1521
+ registerForegroundTaskObserver(registration) {
1522
+ this.assertActive();
1523
+ const control = this.context.get('pluginControlPlane', false);
1524
+ if (!validForegroundObservationRegistration(registration) || control === undefined
1525
+ || control.ownsForegroundTaskObservationRegistration?.(registration) !== true) {
1526
+ throw new AssistantDeliveryError('runtime-conflict', 'foreground observation registration is invalid');
1527
+ }
1528
+ if (this.foregroundObserver !== undefined)
1529
+ throw new AssistantDeliveryError('runtime-conflict', 'foreground observer is already registered');
1530
+ const token = Symbol('assistant-delivery.foreground-observer');
1531
+ const sink = Object.freeze({ token, generation: registration.generation, registration,
1532
+ handles: new Map() });
1533
+ this.foregroundObserver = sink;
1534
+ let live = true;
1535
+ return () => { if (live) {
1536
+ live = false;
1537
+ if (this.foregroundObserver?.token === token)
1538
+ this.foregroundObserver = undefined;
1539
+ } };
1540
+ }
1541
+ observerCurrent(sink) {
1542
+ const control = this.context.get('pluginControlPlane', false);
1543
+ return this.active && this.foregroundObserver?.token === sink.token
1544
+ && control?.ownsForegroundTaskObservationRegistration?.(sink.registration) === true
1545
+ && sink.generation === sink.registration.generation;
1546
+ }
1547
+ observeForegroundBegin(task) {
1548
+ const sink = this.foregroundObserver;
1549
+ if (sink === undefined || !this.observerCurrent(sink) || sink.handles.size >= Math.max(16, this.config.maxConcurrency * 4))
1550
+ return;
1551
+ try {
1552
+ const handle = sink.registration.begin(task);
1553
+ if (!isSynchronousForegroundObservationHandle(handle)) {
1554
+ void Promise.resolve(handle).catch(error => this.context.logger.warn(`assistant-delivery: foreground observer begin must be synchronous: ${String(error)}`));
1555
+ return;
1556
+ }
1557
+ if (!this.observerCurrent(sink))
1558
+ return;
1559
+ sink.handles.set(task.inboxId, { task, handle });
1560
+ }
1561
+ catch (error) {
1562
+ this.context.logger.warn(`assistant-delivery: foreground observer begin failed: ${String(error)}`);
1563
+ }
1564
+ }
1565
+ completeForegroundTaskExecution(inboxId, input) {
1566
+ this.deliveryStore.finishForegroundTaskExecution({ inboxId, ...input, completedAt: Date.now() });
1567
+ const sink = this.foregroundObserver;
1568
+ const observed = sink?.handles.get(inboxId);
1569
+ if (sink === undefined || observed === undefined)
1570
+ return;
1571
+ sink.handles.delete(inboxId);
1572
+ if (!this.observerCurrent(sink))
1573
+ return;
1574
+ const actual = this.deliveryStore.inspectForegroundExecutionForOwner({ inboxId, scope: observed.task.scope, owner: observed.task.owner,
1575
+ bindingId: observed.task.binding.id, bindingVersion: observed.task.binding.version, bindingGeneration: observed.task.binding.generation });
1576
+ if (actual === null)
1577
+ return;
1578
+ try {
1579
+ const result = sink.registration.completed(observed.handle, observed.task, actual);
1580
+ if (!isSynchronousForegroundObservationHandle(result)) {
1581
+ void Promise.resolve(result).catch(error => this.context.logger.warn(`assistant-delivery: foreground observer completion must be synchronous: ${String(error)}`));
1582
+ }
1583
+ }
1584
+ catch (error) {
1585
+ this.context.logger.warn(`assistant-delivery: foreground observer completion failed: ${String(error)}`);
1586
+ }
1587
+ }
1478
1588
  prepareForegroundTaskAcceptance(binding, envelope) {
1479
1589
  const sink = this.acceptanceSink;
1480
1590
  if (sink === undefined) {
@@ -1979,6 +2089,106 @@ export class AssistantDeliveryService extends Service {
1979
2089
  }
1980
2090
  return route;
1981
2091
  }
2092
+ /**
2093
+ * Host-only owner-anchored workflow learning bridge. This method is not
2094
+ * registered as an Agent tool (same Host seam as prepareWorkflowApproval):
2095
+ * only the opt-in growth driver invokes it, and only with a locator plus one
2096
+ * bounded-wake authority. Delivery itself re-fetches both the verified Goals
2097
+ * source and the owner-run-trace proof, so the caller can never supply the
2098
+ * objective, steps, acceptance verdict or proof digest — a spoofed driver
2099
+ * learns nothing. A goal that does not honestly reduce to the single
2100
+ * no-tool `assistant.agent-turn` step is returned as `abstained` with zero
2101
+ * writes; the trace is projected through the existing outbox/drain chain.
2102
+ */
2103
+ async commitOwnerAnchoredWorkflowTrace(input) {
2104
+ this.assertActive();
2105
+ if (typeof input !== 'object' || input === null
2106
+ || !isOwnerGoalSnapshotLocator(input.locator)
2107
+ || typeof input.authority !== 'object' || input.authority === null
2108
+ || typeof input.authority.assertCurrent !== 'function') {
2109
+ throw new AssistantDeliveryError('runtime-conflict', 'owner-anchored workflow commit request is invalid');
2110
+ }
2111
+ const { locator, authority } = input;
2112
+ authority.assertCurrent();
2113
+ if (authority.ownerRouteId !== locator.ownerRouteId
2114
+ || authority.scope.principalId !== locator.principalId
2115
+ || authority.scope.workspace !== locator.workspace
2116
+ || authority.scope.preset !== locator.preset) {
2117
+ throw new AssistantDeliveryError('policy-denied', 'owner-anchored workflow authority scope does not match its locator');
2118
+ }
2119
+ const goals = this.ctx.get('assistantGoals', false);
2120
+ if (!isOwnerAnchoredGoalsCapability(goals)) {
2121
+ throw new AssistantDeliveryError('runtime-unavailable', 'owner-anchored workflow goals evidence is unavailable');
2122
+ }
2123
+ let source;
2124
+ try {
2125
+ source = await goals.inspectOwnerVerifiedWorkflowSource(locator);
2126
+ }
2127
+ catch (error) {
2128
+ if (error?.code === 'unavailable') {
2129
+ throw new AssistantDeliveryError('runtime-unavailable', 'owner-anchored workflow goals observation is unavailable');
2130
+ }
2131
+ throw new AssistantDeliveryError('missing-binding', 'owner-anchored workflow verified source is not available');
2132
+ }
2133
+ let proof;
2134
+ try {
2135
+ proof = await goals.inspectOwnerGoalRunProof({ ...locator, runId: source.runId });
2136
+ }
2137
+ catch (error) {
2138
+ if (/unavailable/u.test(error instanceof Error ? error.message : '')) {
2139
+ throw new AssistantDeliveryError('runtime-unavailable', 'owner-anchored workflow run proof is unavailable');
2140
+ }
2141
+ throw new AssistantDeliveryError('missing-binding', 'owner-anchored workflow run proof is not available');
2142
+ }
2143
+ // Re-anchor after every await: the bounded-wake authority and the live
2144
+ // owner route must still be the same generation the driver was minted for.
2145
+ authority.assertCurrent();
2146
+ const route = this.resolveOwnerRoute(locator.ownerRouteId);
2147
+ if (externalPrincipalId(route.binding.principal) !== locator.principalId
2148
+ || route.binding.workspace !== locator.workspace
2149
+ || route.binding.agentPreset !== locator.preset) {
2150
+ throw new AssistantDeliveryError('missing-binding', 'owner-anchored workflow locator does not match the live owner route');
2151
+ }
2152
+ const failedObservations = source.failedObservations ?? [];
2153
+ const proofSucceeded = proof.steps.filter(step => step.outcome === 'succeeded');
2154
+ const proofFailed = proof.steps.filter(step => step.outcome === 'failed');
2155
+ // The reduction decision below must cover exactly the settled calls the
2156
+ // trusted run-proof digest covers; a mismatch fails closed.
2157
+ if (source.scope.principalId !== locator.principalId
2158
+ || source.scope.workspace !== locator.workspace || source.scope.preset !== locator.preset
2159
+ || source.goal.id !== locator.goalId || source.goal.sessionId !== locator.sessionId
2160
+ || proof.runId !== source.runId || proof.turn !== source.turn
2161
+ || proof.definitionDigest !== source.goal.definition.digest
2162
+ || !/^[a-f0-9]{64}$/u.test(proof.traceDigest)
2163
+ || proofSucceeded.length !== source.steps.length
2164
+ || proofFailed.length !== failedObservations.length
2165
+ || JSON.stringify(proofSucceeded.map(step => `${step.id}:${step.name}`).sort())
2166
+ !== JSON.stringify(source.steps.map(step => `${step.id}:${step.toolName}`).sort())
2167
+ || JSON.stringify(proofFailed.map(step => step.id).sort())
2168
+ !== JSON.stringify(failedObservations.map(step => step.id).sort())) {
2169
+ throw new AssistantDeliveryError('missing-binding', 'owner-anchored workflow evidence does not match its locator');
2170
+ }
2171
+ const committed = this.deliveryStore.commitOwnerAnchoredWorkflowTrace({
2172
+ binding: route.binding,
2173
+ ownerRouteId: route.authorityId,
2174
+ scope: { workspace: locator.workspace, preset: locator.preset },
2175
+ goal: source.goal,
2176
+ runId: source.runId,
2177
+ turn: source.turn,
2178
+ acceptance: {
2179
+ contractId: source.acceptance.contractId,
2180
+ receiptDigest: source.acceptance.receiptDigest,
2181
+ verifiedAt: source.acceptance.verifiedAt,
2182
+ validUntil: source.acceptance.validUntil,
2183
+ },
2184
+ steps: source.steps.map(step => Object.freeze({ id: step.id, toolName: step.toolName })),
2185
+ ...(failedObservations.length === 0 ? {} : { failedObservations }),
2186
+ ownerRunTraceDigest: proof.traceDigest,
2187
+ });
2188
+ if (committed.outcome === 'trace-recorded')
2189
+ await this.drainWorkflowTraces();
2190
+ return committed;
2191
+ }
1982
2192
  /**
1983
2193
  * Host-only, content-free route minting seam for Preference promotion.
1984
2194
  * No binding id is accepted from the caller; Delivery resolves the unique
@@ -2635,12 +2845,11 @@ export class AssistantDeliveryService extends Service {
2635
2845
  return 'invalid-target';
2636
2846
  return await this.dispatchGoalOutcomeFeedback(binding, inbox, target, parsed, goalOutcomeTarget);
2637
2847
  }
2638
- // Ordinary Agent replies have no learning metadata. They are eligible
2639
- // only for Delivery's local, atomic verified-workflow receipt; the Store
2640
- // rebuilds the full Inbox/Outbox fence and abstains unless the source text
2641
- // selects a closed deterministic-deidentification template. Do not route
2642
- // these judgements through Evaluation: they are task proof for workflow
2643
- // repetition, not a cross-plugin quality vote.
2848
+ // Ordinary Agent replies have no learning metadata. A completed Host
2849
+ // execution permits an authenticated owner revision in Evaluation. The
2850
+ // separate local workflow path rebuilds its Inbox/Outbox fence and abstains
2851
+ // unless the source selects a closed deterministic-deidentification template;
2852
+ // legacy replies without an execution receipt retain that local path.
2644
2853
  if (metadata?.['dsh.learning.kind'] !== 'automation-run') {
2645
2854
  const sourceEventId = target.intent.replyToEventId;
2646
2855
  if (typeof sourceEventId !== 'string')
@@ -2655,7 +2864,17 @@ export class AssistantDeliveryService extends Service {
2655
2864
  owner: lineage,
2656
2865
  bindingId: binding.id,
2657
2866
  });
2658
- const canonicalForeground = accepted !== null;
2867
+ const foreground = lineage === undefined ? null : this.deliveryStore.inspectForegroundExecutionForOwner({
2868
+ inboxId: sourceInbox.id, scope: { workspace: binding.workspace, preset: binding.agentPreset },
2869
+ owner: lineage, bindingId: binding.id, bindingVersion: binding.version, bindingGeneration: binding.generation,
2870
+ });
2871
+ // The owner labels the objective, not whether Host work has stopped.
2872
+ // Do not let Evaluation's delivery producer turn an unknown execution
2873
+ // into a succeeded one merely because its reply reached the user.
2874
+ if ([foreground, accepted].some(execution => execution !== null
2875
+ && (execution.status !== 'succeeded' || !execution.quiescent)))
2876
+ return 'invalid-target';
2877
+ const canonicalForeground = foreground !== null || accepted !== null;
2659
2878
  const projectForegroundOwner = async (inspectOnly) => {
2660
2879
  const sink = this.evaluationSink;
2661
2880
  if (sink === undefined || lineage === undefined)
@@ -2707,7 +2926,7 @@ export class AssistantDeliveryService extends Service {
2707
2926
  if (canonicalForeground && this.evaluationSink === undefined)
2708
2927
  return 'unavailable';
2709
2928
  try {
2710
- // Evaluation commits first for accepted foreground tasks. If the sink
2929
+ // Evaluation commits first for recorded foreground tasks. If the sink
2711
2930
  // is unavailable, never acknowledge a Delivery-only success; a replay
2712
2931
  // can safely repeat the opaque command and then repair local workflow
2713
2932
  // evidence after the canonical owner revision is durable.
@@ -2855,8 +3074,8 @@ export class AssistantDeliveryService extends Service {
2855
3074
  const metadata = target.intent.metadata;
2856
3075
  const locator = durable.locator;
2857
3076
  const immutableProof = durable.proof;
2858
- const situation = `goal:${locator.goalId}:definition:${immutableProof.goal.definitionVersion}`;
2859
- if (metadata?.['dsh.learning.schemaVersion'] !== '3'
3077
+ const situation = goalDefinitionSituation(immutableProof.goal.definitionDigest);
3078
+ if (metadata?.['dsh.learning.schemaVersion'] !== '4'
2860
3079
  || metadata['dsh.learning.kind'] !== 'goal-outcome'
2861
3080
  || metadata['dsh.learning.goalId'] !== locator.goalId
2862
3081
  || metadata['dsh.learning.assessmentId'] !== locator.assessmentId
@@ -3143,38 +3362,14 @@ export class AssistantDeliveryService extends Service {
3143
3362
  return reviewer === 'auto-review' ? next() : 'unavailable';
3144
3363
  if (initialRoute.state !== 'bound')
3145
3364
  return 'unavailable';
3146
- if (signalAborted(request.signal))
3147
- return 'cancelled';
3148
- let authority;
3149
- try {
3150
- authority = this.resolveToolApprovalAuthority(ctx, request);
3151
- }
3152
- catch {
3153
- return 'unavailable';
3154
- }
3155
- if (authority === undefined || authority.binding.id !== initialRoute.binding.id)
3156
- return 'unavailable';
3157
- const sessions = ctx.get('sessions');
3158
- if (sessions === undefined)
3159
- return 'unavailable';
3160
- try {
3161
- if (!await sessions.flush(request.agent.session))
3162
- return 'unavailable';
3163
- }
3164
- catch {
3165
- return 'unavailable';
3166
- }
3167
- if (signalAborted(request.signal))
3168
- return 'cancelled';
3169
- let persisted;
3170
- try {
3171
- persisted = this.resolveToolApprovalAuthority(ctx, request);
3172
- }
3173
- catch {
3174
- return 'unavailable';
3175
- }
3176
- if (!sameToolApprovalAuthority(authority, persisted))
3177
- return 'unavailable';
3365
+ // The TTL controller is installed BEFORE every pre-card wait, not just
3366
+ // before the adapter call. resolveToolApprovalAuthority is synchronous, but
3367
+ // the durability `sessions.flush()` between the two authority reads has no
3368
+ // cancellation of its own: a stuck flush listener used to hang the whole
3369
+ // approval waterfall for hours with no card dispatched and no expiry (real
3370
+ // headless-Lark observation). Everything from here on is bounded by the
3371
+ // same fail-closed deadline; abort races resolve 'cancelled' only when the
3372
+ // owning request itself was aborted, otherwise 'unavailable'.
3178
3373
  const operationId = `tool-approval:${randomUUID()}`;
3179
3374
  const expiresAt = Date.now() + this.config.toolApprovalTtlMs;
3180
3375
  const controller = new AbortController();
@@ -3184,7 +3379,11 @@ export class AssistantDeliveryService extends Service {
3184
3379
  controller.abort(request.signal?.reason);
3185
3380
  };
3186
3381
  request.signal?.addEventListener('abort', onRequestAbort, { once: true });
3187
- const aborted = new Promise(resolve => {
3382
+ if (signalAborted(request.signal)) {
3383
+ request.signal?.removeEventListener('abort', onRequestAbort);
3384
+ return 'cancelled';
3385
+ }
3386
+ const deadlineAbort = new Promise(resolve => {
3188
3387
  controller.signal.addEventListener('abort', () => {
3189
3388
  resolve(requestCancelled ? 'cancelled' : 'unavailable');
3190
3389
  }, { once: true });
@@ -3194,26 +3393,69 @@ export class AssistantDeliveryService extends Service {
3194
3393
  }, this.config.toolApprovalTtlMs);
3195
3394
  timeout.unref?.();
3196
3395
  this.toolApprovalControllers.add(controller);
3197
- const adapterRequest = Object.freeze({
3198
- operationId,
3199
- bindingId: authority.binding.id,
3200
- target: Object.freeze({
3201
- conversation: Object.freeze({ ...authority.binding.conversation }),
3202
- principal: Object.freeze({ ...authority.binding.principal }),
3203
- }),
3204
- expiresAt,
3205
- actionHash: authority.actionHash,
3206
- toolName: request.toolName,
3207
- callId: authority.callId,
3208
- ...(request.reason === undefined ? {} : { reason: request.reason }),
3209
- arguments: authority.arguments,
3210
- });
3211
3396
  let outcome;
3397
+ // Declared outside the try block: the post-race authority re-check below
3398
+ // must compare against the exact authority that dispatched the card.
3399
+ let authority;
3212
3400
  try {
3401
+ const preCard = await Promise.race([
3402
+ (async () => {
3403
+ let authority;
3404
+ try {
3405
+ authority = this.resolveToolApprovalAuthority(ctx, request);
3406
+ }
3407
+ catch {
3408
+ return 'unavailable';
3409
+ }
3410
+ if (authority === undefined || authority.binding.id !== initialRoute.binding.id)
3411
+ return 'unavailable';
3412
+ const sessions = ctx.get('sessions');
3413
+ if (sessions === undefined)
3414
+ return 'unavailable';
3415
+ try {
3416
+ if (!await sessions.flush(request.agent.session))
3417
+ return 'unavailable';
3418
+ }
3419
+ catch {
3420
+ return 'unavailable';
3421
+ }
3422
+ if (signalAborted(request.signal))
3423
+ return 'cancelled';
3424
+ let persisted;
3425
+ try {
3426
+ persisted = this.resolveToolApprovalAuthority(ctx, request);
3427
+ }
3428
+ catch {
3429
+ return 'unavailable';
3430
+ }
3431
+ if (!sameToolApprovalAuthority(authority, persisted))
3432
+ return 'unavailable';
3433
+ return authority;
3434
+ })(),
3435
+ deadlineAbort,
3436
+ ]);
3437
+ if (typeof preCard === 'string')
3438
+ return preCard;
3439
+ const granted = preCard;
3440
+ authority = granted;
3441
+ const adapterRequest = Object.freeze({
3442
+ operationId,
3443
+ bindingId: granted.binding.id,
3444
+ target: Object.freeze({
3445
+ conversation: Object.freeze({ ...granted.binding.conversation }),
3446
+ principal: Object.freeze({ ...granted.binding.principal }),
3447
+ }),
3448
+ expiresAt,
3449
+ actionHash: granted.actionHash,
3450
+ toolName: request.toolName,
3451
+ callId: granted.callId,
3452
+ ...(request.reason === undefined ? {} : { reason: request.reason }),
3453
+ arguments: granted.arguments,
3454
+ });
3213
3455
  const answer = Promise.resolve()
3214
- .then(() => authority.adapter.requestToolApproval(adapterRequest, controller.signal))
3456
+ .then(() => granted.adapter.requestToolApproval(adapterRequest, controller.signal))
3215
3457
  .then(value => value, () => 'unavailable');
3216
- outcome = await Promise.race([answer, aborted]);
3458
+ outcome = await Promise.race([answer, deadlineAbort]);
3217
3459
  }
3218
3460
  finally {
3219
3461
  clearTimeout(timeout);
@@ -3231,7 +3473,7 @@ export class AssistantDeliveryService extends Service {
3231
3473
  catch {
3232
3474
  return 'unavailable';
3233
3475
  }
3234
- if (current === undefined || !sameToolApprovalAuthority(authority, current))
3476
+ if (authority === undefined || current === undefined || !sameToolApprovalAuthority(authority, current))
3235
3477
  return 'unavailable';
3236
3478
  if (outcome === 'allowed-once') {
3237
3479
  try {
@@ -3655,6 +3897,123 @@ export class AssistantDeliveryService extends Service {
3655
3897
  generation: resolved.snapshot.generation,
3656
3898
  });
3657
3899
  }
3900
+ /** Host-only snapshot of the model selected for this owner's external conversation. */
3901
+ inspectOwnerModelSelection(input) {
3902
+ this.validateOwnerRoute(input);
3903
+ const { binding } = this.resolveOwnerRoute(input.authorityId);
3904
+ if (input.sourceAgent !== undefined) {
3905
+ const session = input.sourceAgent.session;
3906
+ if (String(session.id) !== binding.sessionId || session.header.cwd !== binding.workspace
3907
+ || session.header.agentPreset !== binding.agentPreset) {
3908
+ throw new AssistantDeliveryError('missing-binding', 'growth source session does not match the owner route');
3909
+ }
3910
+ // Request headers reflect native model-selection hooks; agent.options
3911
+ // can still contain the initial route after the user changes models.
3912
+ const request = session.snapshotEvents().findLast(event => event.type === 'request/header');
3913
+ const selected = request?.type === 'request/header' ? request.data.header.config : undefined;
3914
+ if (!selected?.provider || !selected.model) {
3915
+ throw new AssistantDeliveryError('missing-binding', 'growth source session has no resolved model request');
3916
+ }
3917
+ return Object.freeze({ provider: selected.provider, model: selected.model,
3918
+ ...(selected.reasoningEffort === undefined ? {} : { reasoningEffort: selected.reasoningEffort }) });
3919
+ }
3920
+ // Native Web sessions own their model selection in DSH, not this external
3921
+ // conversation ledger. Never substitute Delivery defaults for those sessions.
3922
+ if (binding.conversation.channel === 'web') {
3923
+ throw new AssistantDeliveryError('missing-binding', 'native Web model selection requires a source session');
3924
+ }
3925
+ const selected = this.deliveryStore.getModelSelection(binding.conversation)
3926
+ ?? { provider: this.config.agentProvider, model: this.config.agentModel };
3927
+ return Object.freeze({ provider: selected.provider, model: selected.model,
3928
+ ...(selected.reasoningEffort === undefined ? {} : { reasoningEffort: selected.reasoningEffort }) });
3929
+ }
3930
+ /**
3931
+ * Resolve one canonical foreground result against its actual owner lineage.
3932
+ * Evaluation's workspace/preset scope alone does not establish ownership.
3933
+ * Historical Sessions remain eligible after /new, but not after owner ABA.
3934
+ */
3935
+ inspectOwnerForegroundLearningTask(input) {
3936
+ this.assertActive();
3937
+ const routeInput = { authorityId: input.authorityId, principalId: input.principalId,
3938
+ workspace: input.workspace, agentPreset: input.agentPreset };
3939
+ const owner = this.validateOwnerRoute(routeInput);
3940
+ const evaluation = this.context.get('assistantEvaluation', false);
3941
+ if (!evaluation || typeof evaluation.inspectTrustedTaskOwnerRevision !== 'function')
3942
+ return undefined;
3943
+ const scope = evaluation.canonicalHostScope({ workspace: owner.workspace, preset: owner.agentPreset });
3944
+ const canonical = evaluation.getTrustedTaskLearningProjection({ scope, outcomeId: input.outcomeId });
3945
+ if (!canonical || canonical.projection.subjectKind !== 'foreground-turn')
3946
+ return undefined;
3947
+ const inbox = this.deliveryStore.getInbox(canonical.projection.subjectRef);
3948
+ const binding = inbox?.bindingId === undefined ? undefined : this.deliveryStore.getBinding(inbox.bindingId);
3949
+ if (!inbox || !binding || binding.workspace !== owner.workspace || binding.agentPreset !== owner.agentPreset
3950
+ || externalPrincipalId(binding.principal) !== owner.principalId
3951
+ || JSON.stringify(inbox.envelope.principal) !== JSON.stringify(binding.principal)
3952
+ || JSON.stringify(inbox.envelope.conversation) !== JSON.stringify(binding.conversation))
3953
+ return undefined;
3954
+ const accepted = this.deliveryStore.inspectForegroundAcceptedExecutionForOwner({ inboxId: inbox.id,
3955
+ scope: { workspace: owner.workspace, preset: owner.agentPreset }, bindingId: binding.id,
3956
+ owner: { principalRecordId: owner.principalRecordId, principalVersion: owner.principalVersion } });
3957
+ const execution = this.deliveryStore.inspectForegroundExecutionForOwner({ inboxId: inbox.id,
3958
+ scope: { workspace: owner.workspace, preset: owner.agentPreset }, bindingId: binding.id,
3959
+ bindingVersion: binding.version, bindingGeneration: binding.generation,
3960
+ owner: { principalRecordId: owner.principalRecordId, principalVersion: owner.principalVersion } }) ?? accepted;
3961
+ if (execution === null)
3962
+ return undefined;
3963
+ const verifierComponent = (component) => accepted !== null && component.source.kind === 'evaluator' && component.source.id === 'assistant-verifier'
3964
+ && component.evaluator.id === 'assistant-verifier' && component.evaluator.version === '1'
3965
+ && component.evidence.some(ref => ref.kind === 'foreground-turn' && ref.ref === inbox.id)
3966
+ && component.evidence.some(ref => ref.kind === 'execution' && ref.ref === accepted.executionRef)
3967
+ && component.evidence.some(ref => ref.kind === 'acceptance-contract'
3968
+ && ref.ref === accepted.contractId && ref.digest === accepted.contractDigest);
3969
+ if (canonical.execution !== undefined && (!verifierComponent(canonical.execution)
3970
+ || canonical.execution.status !== accepted?.status))
3971
+ return undefined;
3972
+ let judgement = 'unresolved';
3973
+ let ownerRevision;
3974
+ const objective = canonical.objective;
3975
+ if (objective?.source.kind === 'user-feedback') {
3976
+ if (objective.source.id !== 'assistant-delivery/typed-owner-feedback')
3977
+ return undefined;
3978
+ const revision = evaluation.inspectTrustedTaskOwnerRevision({ scope, outcomeId: input.outcomeId,
3979
+ principalRecordId: owner.principalRecordId, principalVersion: owner.principalVersion });
3980
+ if (!revision || revision.outcomeId !== objective.outcomeId || revision.objectiveStatus !== objective.status)
3981
+ return undefined;
3982
+ const outboxRefs = objective.evidence.filter(ref => ref.kind === 'delivery-outbox');
3983
+ if (outboxRefs.length !== 1)
3984
+ return undefined;
3985
+ const outbox = this.deliveryStore.getOutbox(outboxRefs[0].ref);
3986
+ if (!outbox || outbox.intent.bindingId !== binding.id
3987
+ || outbox.intent.idempotencyKey !== `inbound:${inbox.id}:reply`
3988
+ || outbox.intent.replyToEventId !== inbox.envelope.eventId
3989
+ || JSON.stringify(outbox.intent.target.principal) !== JSON.stringify(binding.principal)
3990
+ || JSON.stringify(outbox.intent.target.conversation) !== JSON.stringify(binding.conversation)
3991
+ || !['accepted', 'delivered', 'read'].includes(outbox.status))
3992
+ return undefined;
3993
+ judgement = 'owner-feedback';
3994
+ ownerRevision = Object.freeze({ version: revision.version, action: revision.action });
3995
+ }
3996
+ else if (objective !== undefined) {
3997
+ if (!verifierComponent(objective))
3998
+ return undefined;
3999
+ judgement = 'independent-verifier';
4000
+ }
4001
+ // Generic execution receipts establish task provenance, never goal success.
4002
+ // Without an acceptance contract, only authenticated typed owner feedback
4003
+ // may turn a completed ordinary conversation into a learning source.
4004
+ if (accepted === null && judgement !== 'owner-feedback')
4005
+ return undefined;
4006
+ // Fence cross-ledger reads before handing any task text to the consumer.
4007
+ if (!evaluation.isTrustedTaskLearningProjectionReceipt(canonical)
4008
+ || acceptanceCanonicalJson(owner) !== acceptanceCanonicalJson(this.validateOwnerRoute(routeInput)))
4009
+ return undefined;
4010
+ const objectiveText = inbox.envelope.text;
4011
+ return Object.freeze({ protocol: 'assistant-delivery/owner-foreground-learning/v1', owner, canonical, judgement,
4012
+ ...(ownerRevision === undefined ? {} : { ownerRevision }),
4013
+ source: Object.freeze({ sessionId: binding.sessionId, inboxId: inbox.id,
4014
+ objective: objectiveText.slice(0, 4096), truncated: objectiveText.length > 4096, quiescent: execution.quiescent,
4015
+ modelSelectionState: execution.modelSelectionState, ...(execution.modelSelection === undefined ? {} : { modelSelection: execution.modelSelection }) }) });
4016
+ }
3658
4017
  /**
3659
4018
  * Enqueue against a stable Host-owned route rather than a session binding.
3660
4019
  * Policy is evaluated against the exact `route:<authorityId>` resource. The
@@ -3794,6 +4153,11 @@ export class AssistantDeliveryService extends Service {
3794
4153
  const prepared = this.prepareAgentReply(agent, input);
3795
4154
  return this.deliveryStore.enqueue(prepared.intent);
3796
4155
  }
4156
+ /** Host-only replay boundary. The returned tombstone fences this exact Agent object permanently. */
4157
+ blockAgentRepliesForReplay(agent, config) {
4158
+ this.assertActive();
4159
+ return createReplyReplayBlock(agent, config);
4160
+ }
3797
4161
  async replyCompletedPreferenceTurn(agent, expectedBinding, envelope, input) {
3798
4162
  const prepared = this.prepareAgentReply(agent, {
3799
4163
  idempotencyKey: `inbound:${envelope.eventId}:reply`,
@@ -3818,6 +4182,7 @@ export class AssistantDeliveryService extends Service {
3818
4182
  return projected ? 'recorded' : 'unavailable';
3819
4183
  }
3820
4184
  prepareAgentReply(agent, input) {
4185
+ assertReplyReplayAllowed(agent, input);
3821
4186
  this.assertActive();
3822
4187
  const binding = agent === undefined ? undefined : this.deliveryStore.getBindingBySession(String(agent.session.id));
3823
4188
  if (agent === undefined || binding === undefined || binding.status !== 'active'