@dsh-enhanced/assistant-delivery 0.1.3

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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/cordis.patch.yml +29 -0
  4. package/lib/agent-runtime.d.ts +112 -0
  5. package/lib/agent-runtime.d.ts.map +1 -0
  6. package/lib/agent-runtime.js +1866 -0
  7. package/lib/agent-runtime.js.map +1 -0
  8. package/lib/canonical.d.ts +18 -0
  9. package/lib/canonical.d.ts.map +1 -0
  10. package/lib/canonical.js +83 -0
  11. package/lib/canonical.js.map +1 -0
  12. package/lib/coordinator.d.ts +124 -0
  13. package/lib/coordinator.d.ts.map +1 -0
  14. package/lib/coordinator.js +676 -0
  15. package/lib/coordinator.js.map +1 -0
  16. package/lib/inbound-images.d.ts +38 -0
  17. package/lib/inbound-images.d.ts.map +1 -0
  18. package/lib/inbound-images.js +267 -0
  19. package/lib/inbound-images.js.map +1 -0
  20. package/lib/index.d.ts +12 -0
  21. package/lib/index.d.ts.map +1 -0
  22. package/lib/index.js +12 -0
  23. package/lib/index.js.map +1 -0
  24. package/lib/operator.d.ts +23 -0
  25. package/lib/operator.d.ts.map +1 -0
  26. package/lib/operator.js +38 -0
  27. package/lib/operator.js.map +1 -0
  28. package/lib/service.d.ts +269 -0
  29. package/lib/service.d.ts.map +1 -0
  30. package/lib/service.js +1698 -0
  31. package/lib/service.js.map +1 -0
  32. package/lib/session-commands.d.ts +17 -0
  33. package/lib/session-commands.d.ts.map +1 -0
  34. package/lib/session-commands.js +39 -0
  35. package/lib/session-commands.js.map +1 -0
  36. package/lib/sqlite.d.ts +8 -0
  37. package/lib/sqlite.d.ts.map +1 -0
  38. package/lib/sqlite.js +411 -0
  39. package/lib/sqlite.js.map +1 -0
  40. package/lib/store.d.ts +353 -0
  41. package/lib/store.d.ts.map +1 -0
  42. package/lib/store.js +2614 -0
  43. package/lib/store.js.map +1 -0
  44. package/lib/tools.d.ts +4 -0
  45. package/lib/tools.d.ts.map +1 -0
  46. package/lib/tools.js +54 -0
  47. package/lib/tools.js.map +1 -0
  48. package/lib/types.d.ts +329 -0
  49. package/lib/types.d.ts.map +1 -0
  50. package/lib/types.js +2 -0
  51. package/lib/types.js.map +1 -0
  52. package/lib/version.d.ts +2 -0
  53. package/lib/version.d.ts.map +1 -0
  54. package/lib/version.js +2 -0
  55. package/lib/version.js.map +1 -0
  56. package/package.json +122 -0
package/lib/service.js ADDED
@@ -0,0 +1,1698 @@
1
+ import { chmodSync, mkdirSync } from 'node:fs';
2
+ import { isAbsolute } from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { Context, Service } from '@deepseek-ai/cordis';
5
+ import Schema from '@deepseek-ai/schemastery';
6
+ import { approvalReviewerOf, isAutoReviewEscalation, } from '@dsh-enhanced/assistant-policy';
7
+ import { DeliveryAdapterRegistry, DeliveryCoordinator, InboundCoordinator, } from './coordinator.js';
8
+ import { DeliveryStore, DeliveryStoreError } from './store.js';
9
+ import { DshDeliveryRuntime } from './agent-runtime.js';
10
+ import { InboundImageMaterializer } from './inbound-images.js';
11
+ import { registerDeliveryTools } from './tools.js';
12
+ import { canonicalConversation, externalPrincipalId } from './canonical.js';
13
+ import { isExactDeliveryCommand, parseDeliveryCommand } from './session-commands.js';
14
+ export class AssistantDeliveryError extends Error {
15
+ code;
16
+ constructor(code, message) {
17
+ super(message);
18
+ this.code = code;
19
+ this.name = 'AssistantDeliveryError';
20
+ }
21
+ }
22
+ const configSchema = Schema.object({
23
+ databasePath: Schema.string().required(),
24
+ spoolPath: Schema.string().required(),
25
+ schedulerEnabled: Schema.boolean().default(true),
26
+ tickIntervalMs: Schema.number().step(1).min(100).default(1_000),
27
+ leaseMs: Schema.number().step(1).min(1_000).default(30_000),
28
+ maxAttempts: Schema.number().step(1).min(1).max(100).default(5),
29
+ maxConcurrency: Schema.number().step(1).min(1).max(100).default(4),
30
+ maxTextBytes: Schema.number().step(1).min(1).max(16 * 1024 * 1024).default(65_536),
31
+ retryBaseMs: Schema.number().step(1).min(1).default(1_000),
32
+ retryMaxMs: Schema.number().step(1).min(1).default(300_000),
33
+ pairingTtlMs: Schema.number().step(1).min(1).max(86_400_000).default(600_000),
34
+ pairingMaxAttempts: Schema.number().step(1).min(1).max(10).default(5),
35
+ defaultWorkspace: Schema.string().default(process.cwd()),
36
+ defaultAgentPreset: Schema.string().min(1).default('standard'),
37
+ policyRef: Schema.string().min(1).default('owner-dm'),
38
+ agentProvider: Schema.string().min(1).default('deepseek-official'),
39
+ agentModel: Schema.string().min(1).default('deepseek-v4-flash'),
40
+ toolCapableProviders: Schema.array(Schema.string().pattern(/^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/u)).default(['deepseek-official']),
41
+ unknownRouteToolCalls: Schema.union(['allow', 'deny']).default('allow'),
42
+ agentMaxOutputTokens: Schema.number().step(1).min(1).default(8_192),
43
+ modelPickerTtlMs: Schema.number().step(1).min(60_000).max(86_400_000).default(900_000),
44
+ permissionPickerTtlMs: Schema.number().step(1).min(60_000).max(86_400_000).default(900_000),
45
+ toolApprovalTtlMs: Schema.number().step(1).min(1_000).max(300_000).default(300_000),
46
+ });
47
+ function policyDenied(decision) {
48
+ return new AssistantDeliveryError('policy-denied', `assistant-delivery policy denied operation: ${decision.reasonCode}`);
49
+ }
50
+ function pickerIncludesRoute(picker, route) {
51
+ const model = picker.models.find(candidate => candidate.provider === route.provider && candidate.id === route.model);
52
+ return model !== undefined && (route.reasoningEffort === undefined || model.effortIds.includes(route.reasoningEffort));
53
+ }
54
+ function isModelSelectionPayload(value) {
55
+ if (value === null || typeof value !== 'object')
56
+ return false;
57
+ const payload = value;
58
+ const principal = payload.principal;
59
+ return typeof payload.callbackEventId === 'string'
60
+ && typeof payload.callbackChatId === 'string'
61
+ && typeof payload.bindingId === 'string'
62
+ && principal !== undefined
63
+ && typeof principal.channel === 'string'
64
+ && typeof principal.account === 'string'
65
+ && typeof principal.tenant === 'string'
66
+ && typeof principal.user === 'string'
67
+ && typeof payload.provider === 'string'
68
+ && typeof payload.modelProvider === 'string'
69
+ && typeof payload.model === 'string'
70
+ && (payload.reasoningEffort === null || typeof payload.reasoningEffort === 'string')
71
+ && Number.isSafeInteger(payload.expectedRevision);
72
+ }
73
+ const toolApprovalArgumentBytes = 16 * 1024;
74
+ const toolApprovalIdentityBytes = 512;
75
+ const toolApprovalReasonBytes = 2 * 1024;
76
+ const permissionEventTypes = [
77
+ 'permission/preset',
78
+ 'sandbox/mode',
79
+ 'approval/policy',
80
+ 'assistant-policy/approval-reviewer',
81
+ ];
82
+ const approvalOutcomes = ['allowed-once', 'rejected', 'cancelled', 'unavailable'];
83
+ function currentPermissionEvents(agent) {
84
+ const latest = new Map();
85
+ for (const event of agent.session.events) {
86
+ const type = String(event.type);
87
+ if (!permissionEventTypes.includes(type))
88
+ continue;
89
+ latest.set(type, { seq: event.seq, type, data: event.data });
90
+ }
91
+ return permissionEventTypes.map(type => latest.get(type) ?? null);
92
+ }
93
+ function signalAborted(signal) {
94
+ return signal?.aborted === true;
95
+ }
96
+ function sameToolApprovalAuthority(left, right) {
97
+ return right !== undefined
98
+ && right.adapter === left.adapter
99
+ && right.binding.id === left.binding.id
100
+ && right.binding.version === left.binding.version
101
+ && right.binding.generation === left.binding.generation
102
+ && right.policyEmergencyVersion === left.policyEmergencyVersion
103
+ && right.reviewRoute === left.reviewRoute
104
+ && right.routeKind === left.routeKind
105
+ && right.routeToken === left.routeToken
106
+ && right.actionHash === left.actionHash;
107
+ }
108
+ function serializeCodeDispatchArguments(value) {
109
+ try {
110
+ const serialized = JSON.stringify(value);
111
+ return typeof serialized === 'string' ? serialized : undefined;
112
+ }
113
+ catch {
114
+ return undefined;
115
+ }
116
+ }
117
+ function exactToolApprovalCall(events, openTurn, callId, toolName) {
118
+ const matches = [];
119
+ let settled = false;
120
+ for (const event of events) {
121
+ if (event.seq <= openTurn.seq)
122
+ continue;
123
+ if (event.type === 'tool/call'
124
+ && event.data.turn === openTurn.data.turn
125
+ && String(event.data.callId) === callId) {
126
+ matches.push({
127
+ seq: event.seq,
128
+ name: event.data.name,
129
+ arguments: event.data.arguments,
130
+ hashIdentity: ['tool/call', event.seq, event.data.turn, event.data.step,
131
+ callId, event.data.name, event.data.arguments],
132
+ });
133
+ }
134
+ if (event.type === 'tool/code-dispatch-start' && String(event.data.subCallId) === callId) {
135
+ const rootCallId = String(event.data.rootCallId);
136
+ const parentCallId = String(event.data.parentCallId);
137
+ const argumentsJson = serializeCodeDispatchArguments(event.data.arguments);
138
+ if (argumentsJson === undefined
139
+ || Buffer.byteLength(rootCallId) > toolApprovalIdentityBytes
140
+ || Buffer.byteLength(parentCallId) > toolApprovalIdentityBytes)
141
+ return undefined;
142
+ matches.push({
143
+ seq: event.seq,
144
+ name: event.data.name,
145
+ arguments: argumentsJson,
146
+ hashIdentity: ['tool/code-dispatch-start', event.seq, rootCallId, parentCallId,
147
+ callId, event.data.name, argumentsJson],
148
+ });
149
+ }
150
+ if (event.type === 'tool/result'
151
+ && String(event.data.message.source.callId) === callId)
152
+ settled = true;
153
+ if (event.type === 'tool/code-dispatch'
154
+ && String(event.data.subCallId) === callId)
155
+ settled = true;
156
+ }
157
+ const exact = matches.length === 1 ? matches[0] : undefined;
158
+ if (exact === undefined || settled || exact.name !== toolName
159
+ || Buffer.byteLength(exact.arguments) > toolApprovalArgumentBytes)
160
+ return undefined;
161
+ return exact;
162
+ }
163
+ export class AssistantDeliveryService extends Service {
164
+ static Config = configSchema;
165
+ deliveryStore;
166
+ policy;
167
+ registry;
168
+ outbound;
169
+ inbound;
170
+ config;
171
+ ownerId = `assistant-delivery-${randomUUID()}`;
172
+ bindingFlights = new Map();
173
+ conversationTransitions = new Map();
174
+ agentApprovalBindings = new WeakMap();
175
+ toolApprovalControllers = new Set();
176
+ modelSelectionFlight;
177
+ modelSelectionRetryTimer;
178
+ runtime;
179
+ timer;
180
+ active = true;
181
+ constructor(ctx, input) {
182
+ super(ctx, 'assistantDelivery');
183
+ let config;
184
+ try {
185
+ config = AssistantDeliveryService.Config(input);
186
+ }
187
+ catch (error) {
188
+ throw new Error(`assistant-delivery: invalid configuration: ${String(error)}`, { cause: error });
189
+ }
190
+ if (!isAbsolute(config.databasePath) || !isAbsolute(config.spoolPath) || !isAbsolute(config.defaultWorkspace)) {
191
+ throw new Error('assistant-delivery: databasePath, spoolPath, and defaultWorkspace must be absolute paths');
192
+ }
193
+ const policy = ctx.get('assistantPolicy');
194
+ if (policy === undefined)
195
+ throw new Error('assistant-delivery: assistantPolicy service is required');
196
+ mkdirSync(config.spoolPath, { recursive: true, mode: 0o700 });
197
+ chmodSync(config.spoolPath, 0o700);
198
+ this.config = config;
199
+ this.policy = policy;
200
+ this.deliveryStore = new DeliveryStore({ path: config.databasePath, maxTextBytes: config.maxTextBytes });
201
+ this.registry = new DeliveryAdapterRegistry({
202
+ accept: async (envelope) => {
203
+ const result = await this.acceptInbound(envelope);
204
+ return { duplicate: result.duplicate, inboxId: result.inboxId, status: result.status };
205
+ },
206
+ receipt: async (receipt) => {
207
+ if (!this.active)
208
+ return;
209
+ this.deliveryStore.recordReceipt(receipt);
210
+ },
211
+ });
212
+ const imageMaterializer = new InboundImageMaterializer({
213
+ store: this.deliveryStore,
214
+ registry: this.registry,
215
+ getAttachments: () => ctx.get('attachments'),
216
+ isAuthorized: (binding, envelope) => this.isInboundAuthorized(binding, envelope),
217
+ });
218
+ this.outbound = new DeliveryCoordinator({ store: this.deliveryStore, registry: this.registry,
219
+ ownerId: this.ownerId, leaseMs: config.leaseMs, maxAttempts: config.maxAttempts,
220
+ maxConcurrency: config.maxConcurrency, retryBaseMs: config.retryBaseMs, retryMaxMs: config.retryMaxMs });
221
+ this.inbound = new InboundCoordinator({ store: this.deliveryStore, processor: () => this.runtime,
222
+ ownerId: this.ownerId, leaseMs: config.leaseMs, maxAttempts: config.maxAttempts,
223
+ maxConcurrency: config.maxConcurrency, retryBaseMs: config.retryBaseMs, retryMaxMs: config.retryMaxMs });
224
+ ctx.inject(['agents', 'sessions', 'llm'], runtimeCtx => {
225
+ const unregister = this.registerInboundRuntime(new DshDeliveryRuntime(runtimeCtx, policy, {
226
+ sessionNamespace: this.deliveryStore.instanceId(),
227
+ workspace: config.defaultWorkspace, agentPreset: config.defaultAgentPreset, policyRef: config.policyRef,
228
+ getAgentPresets: () => runtimeCtx.get('agentPresets'),
229
+ provider: config.agentProvider, model: config.agentModel, maxOutputTokens: config.agentMaxOutputTokens,
230
+ toolCapableProviders: new Set(config.toolCapableProviders),
231
+ unknownRouteToolCalls: config.unknownRouteToolCalls ?? 'allow',
232
+ modelPickerTtlMs: config.modelPickerTtlMs,
233
+ permissionPickerTtlMs: config.permissionPickerTtlMs,
234
+ getModelSelection: conversation => this.deliveryStore.getModelSelection(conversation),
235
+ imageMaterializer,
236
+ isInboundAuthorized: (binding, envelope) => this.isInboundAuthorized(binding, envelope),
237
+ isPermissionController: (binding, envelope) => this.isPermissionController(binding, envelope),
238
+ authorizePermissionReply: (binding, envelope) => this.authorizePermissionReply(binding, envelope),
239
+ beginModelCommand: conversation => this.deliveryStore.beginModelCommand(conversation),
240
+ commitModelCommand: input => this.deliveryStore.commitModelCommand(input),
241
+ progress: async (binding, eventId, update) => {
242
+ const adapter = this.registry.get(binding.conversation.channel, binding.conversation.account);
243
+ await adapter?.progress?.({
244
+ bindingId: binding.id,
245
+ target: { conversation: binding.conversation, principal: binding.principal },
246
+ eventId,
247
+ update,
248
+ });
249
+ },
250
+ replyCommand: (binding, eventId, reply, idempotencyKey) => {
251
+ this.replyCommand(binding, {
252
+ idempotencyKey: idempotencyKey ?? this.inboundReplyIdempotencyKey(binding, eventId),
253
+ text: reply.text,
254
+ replyToEventId: eventId,
255
+ ...(reply.format === undefined ? {} : { format: reply.format }),
256
+ ...(reply.modelPicker === undefined ? {} : { modelPicker: reply.modelPicker }),
257
+ ...(reply.permissionPicker === undefined ? {} : { permissionPicker: reply.permissionPicker }),
258
+ });
259
+ },
260
+ reply: (agent, eventId, reply) => {
261
+ this.reply(agent, {
262
+ idempotencyKey: `inbound:${eventId}:reply`,
263
+ text: reply.text,
264
+ replyToEventId: eventId,
265
+ ...(reply.format === undefined ? {} : { format: reply.format }),
266
+ ...(reply.modelPicker === undefined ? {} : { modelPicker: reply.modelPicker }),
267
+ ...(reply.permissionPicker === undefined ? {} : { permissionPicker: reply.permissionPicker }),
268
+ });
269
+ },
270
+ }));
271
+ void this.drainModelSelectionSettlements();
272
+ return unregister;
273
+ });
274
+ ctx.inject(['approval'], approvalCtx => approvalCtx.on('approval/request', (request, next) => this.requestToolApproval(approvalCtx, request, next)));
275
+ ctx.inject(['tools'], toolsCtx => registerDeliveryTools(toolsCtx, this));
276
+ if (config.schedulerEnabled)
277
+ this.start();
278
+ ctx.effect(() => async () => {
279
+ this.active = false;
280
+ for (const controller of this.toolApprovalControllers) {
281
+ controller.abort(new Error('assistant-delivery is stopping'));
282
+ }
283
+ await this.stopInternal();
284
+ await this.registry.stop();
285
+ this.deliveryStore.close();
286
+ }, 'assistant-delivery.runtime');
287
+ }
288
+ issuePairing(operatorId, principal) {
289
+ this.assertActive();
290
+ const decision = this.policy.authorize({ subject: { kind: 'external', id: `local:${operatorId}` },
291
+ action: 'pair.issue', resource: { kind: 'message', id: 'pairing' }, context: { initiator: 'foreground' } });
292
+ if (decision.effect !== 'allow')
293
+ throw policyDenied(decision);
294
+ return this.deliveryStore.issuePairing(principal, { ttlMs: this.config.pairingTtlMs,
295
+ maxAttempts: this.config.pairingMaxAttempts });
296
+ }
297
+ confirmPairing(input) {
298
+ this.assertActive();
299
+ const decision = this.policy.authorize({ subject: { kind: 'external', id: externalPrincipalId(input.principal) },
300
+ action: 'pair.confirm', resource: { kind: 'message', id: 'pairing' }, context: { initiator: 'external' } });
301
+ if (decision.effect !== 'allow')
302
+ throw policyDenied(decision);
303
+ return this.deliveryStore.confirmPairing(input);
304
+ }
305
+ linkPrincipal(input) {
306
+ this.assertActive();
307
+ const decision = this.policy.authorize({ subject: { kind: 'external', id: `local:${input.operatorId}` },
308
+ action: 'pair.link', resource: { kind: 'message', id: 'principal-link' }, context: { initiator: 'foreground' } });
309
+ if (decision.effect !== 'allow')
310
+ throw policyDenied(decision);
311
+ return this.deliveryStore.linkPrincipal(input);
312
+ }
313
+ validateApprovalSettlement(input) {
314
+ const binding = this.deliveryStore.getBinding(input.bindingId);
315
+ const current = this.deliveryStore.getPrincipal(input.principal);
316
+ if (binding?.status !== 'active' || current?.status !== 'active' || current.role !== 'owner'
317
+ || binding.conversation.chat !== input.callbackChatId
318
+ || JSON.stringify(binding.principal) !== JSON.stringify(current.principal)
319
+ || JSON.stringify(binding.principal) !== JSON.stringify(input.principal)) {
320
+ throw new AssistantDeliveryError('missing-binding', 'approval callback principal or chat does not own the active binding');
321
+ }
322
+ const approval = this.deliveryStore.getApprovalIntent(input.operationId, input.bindingId);
323
+ if (approval === undefined
324
+ || approval.proposalId !== input.proposalId
325
+ || approval.expectedVersion !== input.expectedVersion
326
+ || approval.diffHash !== input.diffHash) {
327
+ throw new AssistantDeliveryError('missing-binding', 'approval callback does not match a persisted approval operation');
328
+ }
329
+ return {
330
+ approval,
331
+ principal: externalPrincipalId(current.principal),
332
+ payload: { callbackEventId: input.callbackEventId, callbackChatId: input.callbackChatId,
333
+ bindingId: input.bindingId, principal: current.principal, proposalId: input.proposalId,
334
+ expectedVersion: input.expectedVersion, diffHash: input.diffHash,
335
+ decision: input.decision, reason: input.reason },
336
+ };
337
+ }
338
+ settleApproval(input) {
339
+ this.assertActive();
340
+ const { approval, payload, principal } = this.validateApprovalSettlement(input);
341
+ if (Date.now() >= approval.expiresAt) {
342
+ throw new AssistantDeliveryError('missing-binding', 'approval callback expired before settlement');
343
+ }
344
+ let settlement;
345
+ try {
346
+ settlement = this.deliveryStore.beginApprovalSettlement({ operationId: input.operationId, payload,
347
+ createIfMissing: false });
348
+ }
349
+ catch (error) {
350
+ if (!(error instanceof DeliveryStoreError) || error.code !== 'not-found')
351
+ throw error;
352
+ }
353
+ const proposal = this.policy.getProposal(input.proposalId);
354
+ const immutableProposalMatches = (candidate) => candidate !== undefined
355
+ && candidate.principal === principal
356
+ && candidate.expiresAt === approval.expiresAt
357
+ && candidate.diffHash === approval.diffHash
358
+ && candidate.summary === approval.title;
359
+ const pendingDecision = proposal !== undefined
360
+ && immutableProposalMatches(proposal)
361
+ && proposal.status === 'pending'
362
+ && Date.now() < proposal.expiresAt
363
+ && proposal.version === approval.expectedVersion;
364
+ const terminalReplay = (candidate) => candidate !== undefined
365
+ && immutableProposalMatches(candidate)
366
+ && approval.expectedVersion < Number.MAX_SAFE_INTEGER
367
+ && candidate.version === approval.expectedVersion + 1
368
+ && candidate.status === input.decision
369
+ && candidate.decidedBy === principal
370
+ && candidate.decisionReason === input.reason;
371
+ const terminalDecision = terminalReplay(proposal);
372
+ if (!pendingDecision && !terminalDecision) {
373
+ throw new AssistantDeliveryError('missing-binding', 'approval proposal no longer matches the persisted operation');
374
+ }
375
+ if (settlement === undefined) {
376
+ if (!pendingDecision) {
377
+ throw new AssistantDeliveryError('missing-binding', 'terminal approval has no prior durable settlement');
378
+ }
379
+ settlement = this.deliveryStore.beginApprovalSettlement({ operationId: input.operationId, payload });
380
+ }
381
+ if (settlement.result !== undefined) {
382
+ if (!terminalReplay(this.policy.getProposal(input.proposalId))) {
383
+ throw new AssistantDeliveryError('missing-binding', 'completed approval no longer matches Policy');
384
+ }
385
+ return settlement.result;
386
+ }
387
+ if (pendingDecision) {
388
+ const authorization = this.policy.authorize({ subject: { kind: 'external', id: principal },
389
+ action: 'approval.decide', resource: { kind: 'message', id: input.bindingId },
390
+ context: { initiator: 'external' } }, { idempotencyKey: `approval-callback:${input.operationId}` });
391
+ if (authorization.effect !== 'allow')
392
+ throw policyDenied(authorization);
393
+ }
394
+ const result = this.policy.decideProposal({ proposalId: input.proposalId, principal,
395
+ expectedVersion: input.expectedVersion, decision: input.decision, reason: input.reason });
396
+ return this.deliveryStore.completeApprovalSettlement({ operationId: input.operationId,
397
+ payloadHash: settlement.payloadHash, result });
398
+ }
399
+ recoverApprovalSettlement(input) {
400
+ this.assertActive();
401
+ const { approval, payload, principal } = this.validateApprovalSettlement(input);
402
+ let settlement;
403
+ try {
404
+ settlement = this.deliveryStore.beginApprovalSettlement({ operationId: input.operationId, payload,
405
+ createIfMissing: false });
406
+ }
407
+ catch (error) {
408
+ if (error instanceof DeliveryStoreError && error.code === 'not-found')
409
+ return undefined;
410
+ throw error;
411
+ }
412
+ const proposal = this.policy.getProposal(input.proposalId);
413
+ const exactTerminal = proposal !== undefined
414
+ && proposal.principal === principal
415
+ && proposal.expiresAt === approval.expiresAt
416
+ && proposal.diffHash === approval.diffHash
417
+ && proposal.summary === approval.title
418
+ && approval.expectedVersion < Number.MAX_SAFE_INTEGER
419
+ && proposal.version === approval.expectedVersion + 1
420
+ && proposal.status === input.decision
421
+ && proposal.decidedBy === principal
422
+ && proposal.decisionReason === input.reason;
423
+ if (!exactTerminal)
424
+ return undefined;
425
+ if (settlement.result !== undefined) {
426
+ return settlement.result;
427
+ }
428
+ const result = this.policy.decideProposal({ proposalId: input.proposalId, principal,
429
+ expectedVersion: input.expectedVersion, decision: input.decision, reason: input.reason });
430
+ return this.deliveryStore.completeApprovalSettlement({ operationId: input.operationId,
431
+ payloadHash: settlement.payloadHash, result });
432
+ }
433
+ settleModelSelection(input) {
434
+ this.assertActive();
435
+ const binding = this.deliveryStore.getBinding(input.bindingId);
436
+ const current = this.deliveryStore.getPrincipal(input.principal);
437
+ if (binding?.status !== 'active' || current?.status !== 'active'
438
+ || binding.conversation.chat !== input.callbackChatId
439
+ || JSON.stringify(binding.principal) !== JSON.stringify(current.principal)) {
440
+ throw new AssistantDeliveryError('missing-binding', 'model callback principal or chat does not own the active binding');
441
+ }
442
+ const picker = this.deliveryStore.getModelPicker(input.operationId, input.bindingId);
443
+ const expected = {
444
+ revision: input.expectedRevision,
445
+ provider: input.modelProvider,
446
+ model: input.model,
447
+ ...(input.reasoningEffort === undefined ? {} : { reasoningEffort: input.reasoningEffort }),
448
+ };
449
+ if (picker === undefined || !pickerIncludesRoute(picker, expected)) {
450
+ throw new AssistantDeliveryError('missing-binding', 'model picker confirmation is unavailable or invalid');
451
+ }
452
+ const authorization = this.policy.authorize({ subject: { kind: 'external', id: externalPrincipalId(current.principal) },
453
+ action: 'ingest', resource: { kind: 'message', id: `model-selection:${binding.id}` },
454
+ context: { initiator: 'external' } }, { idempotencyKey: `model-selection:${input.operationId}:${input.callbackEventId}` });
455
+ if (authorization.effect !== 'allow')
456
+ throw policyDenied(authorization);
457
+ const payload = {
458
+ callbackEventId: input.callbackEventId,
459
+ callbackChatId: input.callbackChatId,
460
+ bindingId: input.bindingId,
461
+ principal: current.principal,
462
+ provider: input.provider,
463
+ modelProvider: input.modelProvider,
464
+ model: input.model,
465
+ reasoningEffort: input.reasoningEffort ?? null,
466
+ expectedRevision: input.expectedRevision,
467
+ };
468
+ const settlement = this.deliveryStore.beginModelSelectionSettlement({
469
+ operationId: input.operationId,
470
+ bindingId: input.bindingId,
471
+ expected,
472
+ payload,
473
+ createIfMissing: Date.now() < picker.expiresAt,
474
+ });
475
+ if (settlement.result !== undefined)
476
+ return settlement.result;
477
+ void this.drainModelSelectionSettlements();
478
+ return { status: 'pending' };
479
+ }
480
+ advanceModelPickerForCallback(input) {
481
+ this.assertActive();
482
+ const binding = this.deliveryStore.getBinding(input.bindingId);
483
+ const current = this.deliveryStore.getPrincipal(input.principal);
484
+ if (binding?.status !== 'active' || current?.status !== 'active'
485
+ || binding.conversation.chat !== input.callbackChatId
486
+ || JSON.stringify(binding.principal) !== JSON.stringify(current.principal)) {
487
+ throw new AssistantDeliveryError('missing-binding', 'model callback principal or chat does not own the active binding');
488
+ }
489
+ const picker = this.deliveryStore.getModelPicker(input.operationId, input.bindingId);
490
+ if (picker === undefined || Date.now() >= picker.expiresAt
491
+ || !pickerIncludesRoute(picker, input.expected) || !pickerIncludesRoute(picker, input.next)) {
492
+ throw new AssistantDeliveryError('missing-binding', 'model picker catalog or route is unavailable');
493
+ }
494
+ return this.deliveryStore.advanceModelPicker({
495
+ operationId: input.operationId,
496
+ bindingId: input.bindingId,
497
+ expected: input.expected,
498
+ next: input.next,
499
+ });
500
+ }
501
+ getModelPickerForCallback(input) {
502
+ this.assertActive();
503
+ const binding = this.deliveryStore.getBinding(input.bindingId);
504
+ const current = this.deliveryStore.getPrincipal(input.principal);
505
+ if (binding?.status !== 'active' || current?.status !== 'active'
506
+ || binding.conversation.chat !== input.callbackChatId
507
+ || JSON.stringify(binding.principal) !== JSON.stringify(current.principal)) {
508
+ throw new AssistantDeliveryError('missing-binding', 'model callback principal or chat does not own the active binding');
509
+ }
510
+ const picker = this.deliveryStore.getModelPicker(input.operationId, input.bindingId);
511
+ return picker === undefined || Date.now() >= picker.expiresAt ? undefined : picker;
512
+ }
513
+ async settlePermissionSelection(input) {
514
+ this.assertActive();
515
+ if (!/^[A-Za-z0-9][A-Za-z0-9._@:-]{0,255}$/u.test(input.callbackEventId)
516
+ || !/^[A-Za-z0-9][A-Za-z0-9._@/-]{0,511}$/u.test(input.cardMessageId)
517
+ || !/^[a-f0-9]{64}$/u.test(input.expectedStateHash)
518
+ || !Number.isSafeInteger(input.bindingVersion) || input.bindingVersion < 1
519
+ || !Number.isSafeInteger(input.emergencyStopVersion) || input.emergencyStopVersion < 0
520
+ || !Number.isSafeInteger(input.issuedAt) || input.issuedAt < 1
521
+ || !Number.isSafeInteger(input.expiresAt) || input.expiresAt <= input.issuedAt
522
+ || (input.targetLevel !== 'ask' && input.targetLevel !== 'auto' && input.targetLevel !== 'full')) {
523
+ throw new AssistantDeliveryError('missing-binding', 'permission picker callback shape is invalid');
524
+ }
525
+ const resolveController = () => {
526
+ const emergencyStop = this.policy.getEmergencyStop();
527
+ const binding = this.deliveryStore.getBinding(input.bindingId);
528
+ const principal = this.deliveryStore.getPrincipal(input.principal);
529
+ const active = binding === undefined ? undefined : this.deliveryStore.getActiveBinding(binding.conversation);
530
+ if (binding?.status !== 'active' || active?.id !== binding.id
531
+ || principal?.status !== 'active' || principal.role !== 'owner')
532
+ return undefined;
533
+ const record = this.deliveryStore.getPermissionPickerRecord(input.operationId, input.bindingId);
534
+ const picker = record?.intent.permissionPicker;
535
+ if (record === undefined || picker === undefined
536
+ || binding.version !== input.bindingVersion || picker.bindingVersion !== input.bindingVersion
537
+ || binding.sessionId !== input.sessionId || picker.sessionId !== input.sessionId
538
+ || binding.conversation.kind !== 'dm' || binding.conversation.chat !== input.callbackChatId
539
+ || externalPrincipalId(binding.principal) !== externalPrincipalId(input.principal)
540
+ || externalPrincipalId(record.intent.target.principal) !== externalPrincipalId(input.principal)
541
+ || JSON.stringify(record.intent.target.conversation) !== JSON.stringify(binding.conversation)
542
+ || picker.operationId !== input.operationId
543
+ || picker.issuedAt !== input.issuedAt || picker.expiresAt !== input.expiresAt
544
+ || picker.expectedStateHash !== input.expectedStateHash
545
+ || picker.emergencyStopVersion !== input.emergencyStopVersion
546
+ || emergencyStop.enabled || emergencyStop.version !== picker.emergencyStopVersion
547
+ || Date.now() >= picker.expiresAt
548
+ || !['accepted', 'delivered', 'read'].includes(record.status)
549
+ || record.providerMessageId !== input.cardMessageId)
550
+ return undefined;
551
+ return { binding, picker, principal };
552
+ };
553
+ const controller = resolveController();
554
+ if (controller === undefined) {
555
+ throw new AssistantDeliveryError('missing-binding', 'permission picker no longer owns the active session');
556
+ }
557
+ const { binding, picker, principal } = controller;
558
+ const command = input.targetLevel === 'full'
559
+ ? '/permission full confirm'
560
+ : `/permission ${input.targetLevel}`;
561
+ const envelope = {
562
+ channel: binding.conversation.channel,
563
+ account: binding.conversation.account,
564
+ eventId: input.cardMessageId,
565
+ occurredAt: picker.issuedAt,
566
+ principal: { ...principal.principal },
567
+ conversation: { ...binding.conversation },
568
+ kind: 'command',
569
+ text: command,
570
+ metadata: {
571
+ 'permission-picker-operation': picker.operationId,
572
+ 'permission-picker-state': picker.expectedStateHash,
573
+ 'permission-picker-expires-at': String(picker.expiresAt),
574
+ 'permission-picker-emergency-version': String(picker.emergencyStopVersion),
575
+ 'permission-picker-callback': input.callbackEventId,
576
+ },
577
+ };
578
+ if (!this.isPermissionController(binding, envelope)) {
579
+ throw new AssistantDeliveryError('missing-binding', 'permission picker owner authorization was revoked');
580
+ }
581
+ const accepted = this.deliveryStore.acceptInbound(envelope);
582
+ let inbox = accepted.record;
583
+ if (inbox.status === 'dead_letter') {
584
+ throw new AssistantDeliveryError('runtime-conflict', 'permission picker selection previously failed');
585
+ }
586
+ if (!['received', 'authorized'].includes(inbox.status)) {
587
+ return { duplicate: accepted.duplicate, inboxId: inbox.id, status: inbox.status };
588
+ }
589
+ return await this.runConversationTransition(binding.conversation, async () => {
590
+ await this.recoverPendingInbound(envelope, inbox.id);
591
+ inbox = this.deliveryStore.getInbox(inbox.id);
592
+ if (inbox.status === 'dead_letter') {
593
+ throw new AssistantDeliveryError('runtime-conflict', 'permission picker selection previously failed');
594
+ }
595
+ if (!['received', 'authorized'].includes(inbox.status)) {
596
+ return { duplicate: accepted.duplicate, inboxId: inbox.id, status: inbox.status };
597
+ }
598
+ const current = resolveController();
599
+ if (current === undefined || !this.isPermissionController(current.binding, envelope)) {
600
+ inbox = this.deliveryStore.deadLetterInbox(inbox.id, 'permission-session-superseded');
601
+ throw new AssistantDeliveryError('missing-binding', 'permission picker no longer owns the active session');
602
+ }
603
+ if (!this.authorizePermissionReply(current.binding, envelope)) {
604
+ inbox = this.deliveryStore.deadLetterInbox(inbox.id, 'permission-reply-authorization-denied');
605
+ throw new AssistantDeliveryError('policy-denied', 'permission picker terminal reply authorization was denied');
606
+ }
607
+ const decision = this.policy.authorize({
608
+ subject: { kind: 'external', id: externalPrincipalId(input.principal) },
609
+ action: 'ingest',
610
+ resource: { kind: 'message', id: `inbound:${envelope.channel}/${envelope.account}` },
611
+ context: { initiator: 'external' },
612
+ }, { idempotencyKey: `message-inbound:${inbox.id}` });
613
+ if (decision.effect !== 'allow' || !this.isPermissionController(current.binding, envelope)) {
614
+ inbox = this.deliveryStore.deadLetterInbox(inbox.id, decision.effect !== 'allow' ? `policy-${decision.reasonCode}` : 'permission-authorization-revoked');
615
+ throw new AssistantDeliveryError('policy-denied', 'permission picker owner authorization was revoked');
616
+ }
617
+ inbox = this.deliveryStore.queueInbox(inbox.id, current.binding.id);
618
+ return { duplicate: accepted.duplicate, inboxId: inbox.id, status: inbox.status };
619
+ });
620
+ }
621
+ resolveDeadLetter(input) {
622
+ this.assertActive();
623
+ const decision = this.policy.authorize({ subject: { kind: 'external', id: `local:${input.operatorId}` },
624
+ action: 'delivery.resolve', resource: { kind: 'message', id: `${input.kind}:${input.id}` },
625
+ context: { initiator: 'foreground' } }, { idempotencyKey: `delivery-resolve:${input.kind}:${input.id}:${input.expectedAttemptCount}:${input.resolution}` });
626
+ if (decision.effect !== 'allow')
627
+ throw policyDenied(decision);
628
+ return input.kind === 'inbox'
629
+ ? this.deliveryStore.resolveInbox({ inboxId: input.id, expectedAttemptCount: input.expectedAttemptCount,
630
+ resolution: input.resolution })
631
+ : this.deliveryStore.resolveOutbox({ outboxId: input.id, expectedAttemptCount: input.expectedAttemptCount,
632
+ resolution: input.resolution });
633
+ }
634
+ registerInboundRuntime(runtime) {
635
+ this.assertActive();
636
+ if (this.runtime !== undefined)
637
+ throw new AssistantDeliveryError('runtime-conflict', 'inbound runtime is already registered');
638
+ this.runtime = runtime;
639
+ let active = true;
640
+ return () => {
641
+ if (!active)
642
+ return;
643
+ active = false;
644
+ if (this.runtime === runtime)
645
+ this.runtime = undefined;
646
+ };
647
+ }
648
+ registerAdapter(adapter) {
649
+ this.assertActive();
650
+ return this.registry.register(adapter);
651
+ }
652
+ health() {
653
+ this.assertActive();
654
+ return { ...this.deliveryStore.health(), adapters: this.registry.size() };
655
+ }
656
+ isInboundAuthorized(binding, envelope) {
657
+ if (!this.active)
658
+ return false;
659
+ const current = this.deliveryStore.getBinding(binding.id);
660
+ const active = this.deliveryStore.getActiveBinding(envelope.conversation);
661
+ const principal = this.deliveryStore.getPrincipal(envelope.principal);
662
+ const envelopePrincipal = externalPrincipalId(envelope.principal);
663
+ if (current?.status !== 'active'
664
+ || active?.id !== current.id
665
+ || current.version !== binding.version
666
+ || current.sessionId !== binding.sessionId
667
+ || current.generation !== binding.generation
668
+ || principal?.status !== 'active'
669
+ || externalPrincipalId(current.principal) !== envelopePrincipal
670
+ || externalPrincipalId(binding.principal) !== envelopePrincipal
671
+ || JSON.stringify(current.conversation) !== JSON.stringify(envelope.conversation)
672
+ || envelope.channel !== current.conversation.channel
673
+ || envelope.account !== current.conversation.account) {
674
+ return false;
675
+ }
676
+ return this.policy.evaluate({
677
+ subject: { kind: 'external', id: envelopePrincipal },
678
+ action: 'ingest',
679
+ resource: { kind: 'message', id: `inbound:${envelope.channel}/${envelope.account}` },
680
+ context: { initiator: 'external' },
681
+ }).effect === 'allow';
682
+ }
683
+ isPermissionController(binding, envelope) {
684
+ if (!this.isInboundAuthorized(binding, envelope))
685
+ return false;
686
+ const principal = this.deliveryStore.getPrincipal(binding.principal);
687
+ if (principal?.status !== 'active' || principal.role !== 'owner')
688
+ return false;
689
+ // A permission mutation and its mandatory terminal reply are one control
690
+ // operation. Prove the exact bound Agent can reply before accepting a card
691
+ // callback or allowing the runtime to append any permission event.
692
+ return this.policy.evaluate({
693
+ subject: {
694
+ kind: 'agent',
695
+ id: binding.agentPreset,
696
+ workspace: binding.workspace,
697
+ principal: externalPrincipalId(binding.principal),
698
+ },
699
+ action: 'reply',
700
+ resource: { kind: 'message', id: binding.id },
701
+ context: { initiator: 'external' },
702
+ }).effect === 'allow';
703
+ }
704
+ authorizePermissionReply(binding, envelope) {
705
+ if (!this.isPermissionController(binding, envelope))
706
+ return false;
707
+ const decision = this.policy.authorize({
708
+ subject: {
709
+ kind: 'agent',
710
+ id: binding.agentPreset,
711
+ workspace: binding.workspace,
712
+ principal: externalPrincipalId(binding.principal),
713
+ },
714
+ action: 'reply',
715
+ resource: { kind: 'message', id: binding.id },
716
+ context: { initiator: 'external' },
717
+ }, { idempotencyKey: `message-reply:${this.inboundReplyIdempotencyKey(binding, envelope.eventId)}` });
718
+ return decision.effect === 'allow' && this.isPermissionController(binding, envelope);
719
+ }
720
+ /**
721
+ * Provider event ids are scoped to an adapter account, while Outbox and the
722
+ * Policy ledger share one database-wide idempotency namespace. Anchor every
723
+ * command reply to the accepted Inbox row, whose id is stable across reopen.
724
+ */
725
+ inboundReplyIdempotencyKey(binding, eventId) {
726
+ const inbox = this.deliveryStore.getInboxByProviderEvent(binding.conversation.channel, binding.conversation.account, eventId);
727
+ if (inbox === undefined
728
+ || (inbox.bindingId !== undefined && inbox.bindingId !== binding.id)
729
+ || externalPrincipalId(inbox.envelope.principal) !== externalPrincipalId(binding.principal)
730
+ || JSON.stringify(inbox.envelope.conversation) !== JSON.stringify(binding.conversation)) {
731
+ throw new AssistantDeliveryError('missing-binding', 'control reply does not belong to a durable Inbox on the active binding');
732
+ }
733
+ return `inbound:${inbox.id}:reply`;
734
+ }
735
+ hasActivePrincipal(binding) {
736
+ return this.deliveryStore.getPrincipal(binding.principal)?.status === 'active';
737
+ }
738
+ toolApprovalRoute(agent) {
739
+ const direct = this.deliveryStore.getBindingBySession(String(agent.session.id));
740
+ const delegatedEntry = this.agentApprovalBindings.get(agent);
741
+ const delegated = delegatedEntry === undefined
742
+ ? undefined
743
+ : this.deliveryStore.getBinding(delegatedEntry.bindingId);
744
+ if (direct === undefined && delegatedEntry === undefined)
745
+ return { state: 'none' };
746
+ if ((delegatedEntry !== undefined && delegated === undefined)
747
+ || (direct !== undefined && delegated !== undefined && direct.id !== delegated.id)) {
748
+ return { state: 'invalid' };
749
+ }
750
+ const binding = direct ?? delegated;
751
+ if (binding?.status !== 'active')
752
+ return { state: 'invalid' };
753
+ return direct === undefined
754
+ ? { state: 'bound', binding, kind: 'delegated', token: delegatedEntry.token }
755
+ : { state: 'bound', binding, kind: 'direct' };
756
+ }
757
+ resolveToolApprovalAuthority(ctx, request) {
758
+ if (!this.active || request.callId === undefined)
759
+ return undefined;
760
+ const emergencyStop = this.policy.getEmergencyStop();
761
+ if (emergencyStop.enabled)
762
+ return undefined;
763
+ const agents = ctx.get('agents');
764
+ const sessions = ctx.get('sessions');
765
+ const agent = request.agent;
766
+ const events = agent.session.events;
767
+ const reviewer = approvalReviewerOf(events);
768
+ const reviewRoute = reviewer === 'user'
769
+ ? 'user'
770
+ : reviewer === 'auto-review' && isAutoReviewEscalation(request)
771
+ ? 'auto-escalation'
772
+ : undefined;
773
+ const sessionId = String(agent.session.id);
774
+ const route = this.toolApprovalRoute(agent);
775
+ if (route.state !== 'bound')
776
+ return undefined;
777
+ const { binding } = route;
778
+ if (agents === undefined || sessions === undefined
779
+ || String(agent.id) !== sessionId
780
+ || agents.get(agent.id) !== agent
781
+ || sessions.get(agent.id) !== agent.session
782
+ || (route.kind === 'direct' && binding.sessionId !== sessionId)
783
+ || agent.session.header.cwd !== binding.workspace
784
+ || agent.session.header.agentPreset !== binding.agentPreset
785
+ || binding.conversation.kind !== 'dm'
786
+ || reviewRoute === undefined) {
787
+ return undefined;
788
+ }
789
+ const active = this.deliveryStore.getActiveBinding(binding.conversation);
790
+ const owner = this.deliveryStore.getPrincipal(binding.principal);
791
+ if (active?.id !== binding.id || active.version !== binding.version
792
+ || active.sessionId !== binding.sessionId || active.generation !== binding.generation
793
+ || JSON.stringify(active.principal) !== JSON.stringify(binding.principal)
794
+ || JSON.stringify(active.conversation) !== JSON.stringify(binding.conversation)
795
+ || owner?.status !== 'active' || owner.role !== 'owner'
796
+ || JSON.stringify(owner.principal) !== JSON.stringify(binding.principal)) {
797
+ return undefined;
798
+ }
799
+ const callId = String(request.callId);
800
+ if (Buffer.byteLength(callId) > toolApprovalIdentityBytes
801
+ || Buffer.byteLength(request.toolName) > toolApprovalIdentityBytes
802
+ || (request.reason !== undefined && Buffer.byteLength(request.reason) > toolApprovalReasonBytes)) {
803
+ return undefined;
804
+ }
805
+ const openTurn = events.findLast(event => event.type === 'turn/start' || event.type === 'turn/end');
806
+ if (openTurn?.type !== 'turn/start')
807
+ return undefined;
808
+ const call = exactToolApprovalCall(events, openTurn, callId, request.toolName);
809
+ if (call === undefined)
810
+ return undefined;
811
+ const decided = new Set(events.filter(event => event.type === 'approval/decided')
812
+ .map(event => String(event.data.id)));
813
+ const asked = events.filter((event) => event.type === 'approval/asked'
814
+ && event.seq > call.seq
815
+ && event.seq > openTurn.seq
816
+ && event.data.toolName === request.toolName
817
+ && event.data.callId !== undefined
818
+ && String(event.data.callId) === callId
819
+ && event.data.reason === request.reason
820
+ && !decided.has(String(event.data.id)));
821
+ if (asked.length !== 1)
822
+ return undefined;
823
+ const ask = asked[0];
824
+ const requestHeader = events.findLast(event => event.type === 'request/header');
825
+ const adapter = this.registry.get(binding.conversation.channel, binding.conversation.account);
826
+ if (adapter?.capabilities.toolApprovals !== true || adapter.requestToolApproval === undefined)
827
+ return undefined;
828
+ const header = agent.session.header;
829
+ const actionHash = createHash('sha256').update(JSON.stringify([
830
+ 'assistant-delivery/tool-approval',
831
+ 3,
832
+ ['binding', binding.id, binding.version, binding.generation, binding.sessionId,
833
+ binding.workspace, binding.agentPreset, binding.policyRef,
834
+ [binding.conversation.channel, binding.conversation.account, binding.conversation.tenant,
835
+ binding.conversation.kind, binding.conversation.chat, binding.conversation.thread ?? null],
836
+ [binding.principal.channel, binding.principal.account, binding.principal.tenant, binding.principal.user]],
837
+ ['owner', owner.id, owner.version, owner.role, owner.status, owner.linkedToId ?? null,
838
+ [owner.principal.channel, owner.principal.account, owner.principal.tenant, owner.principal.user]],
839
+ ['session', header.version, sessionId, header.createdAt, header.cwd ?? null,
840
+ header.parentSession ?? null, header.seedLength ?? null, header.origin ?? null,
841
+ header.delegationDepth ?? null, header.agentPreset ?? null],
842
+ ['route-kind', route.kind],
843
+ ['review-route', reviewRoute],
844
+ ['turn', openTurn.seq, openTurn.data.turn],
845
+ ['call', ...call.hashIdentity],
846
+ ['ask', ask.seq, String(ask.data.id), ask.data.toolName, String(ask.data.callId), ask.data.reason ?? null],
847
+ ['request-route', requestHeader === undefined ? null : [requestHeader.seq,
848
+ requestHeader.data.header.config.provider, requestHeader.data.header.config.model,
849
+ requestHeader.data.header.config.reasoningEffort ?? null]],
850
+ ['reviewer', reviewer],
851
+ ['policy-emergency-stop', emergencyStop.version, emergencyStop.enabled],
852
+ ['permission-events', currentPermissionEvents(agent)],
853
+ ])).digest('hex');
854
+ return { adapter, binding, reviewRoute, routeKind: route.kind,
855
+ ...(route.token === undefined ? {} : { routeToken: route.token }),
856
+ policyEmergencyVersion: emergencyStop.version,
857
+ actionHash, arguments: call.arguments, callId };
858
+ }
859
+ async requestToolApproval(ctx, request, next) {
860
+ let initialRoute;
861
+ try {
862
+ initialRoute = this.toolApprovalRoute(request.agent);
863
+ }
864
+ catch {
865
+ return 'unavailable';
866
+ }
867
+ if (initialRoute.state === 'none')
868
+ return next();
869
+ let reviewer;
870
+ try {
871
+ reviewer = approvalReviewerOf(request.agent.session.events);
872
+ }
873
+ catch {
874
+ return 'unavailable';
875
+ }
876
+ const reviewRoute = reviewer === 'user'
877
+ ? 'user'
878
+ : reviewer === 'auto-review' && isAutoReviewEscalation(request)
879
+ ? 'auto-escalation'
880
+ : undefined;
881
+ if (reviewRoute === undefined)
882
+ return reviewer === 'auto-review' ? next() : 'unavailable';
883
+ if (initialRoute.state !== 'bound')
884
+ return 'unavailable';
885
+ if (signalAborted(request.signal))
886
+ return 'cancelled';
887
+ let authority;
888
+ try {
889
+ authority = this.resolveToolApprovalAuthority(ctx, request);
890
+ }
891
+ catch {
892
+ return 'unavailable';
893
+ }
894
+ if (authority === undefined || authority.binding.id !== initialRoute.binding.id)
895
+ return 'unavailable';
896
+ const sessions = ctx.get('sessions');
897
+ if (sessions === undefined)
898
+ return 'unavailable';
899
+ try {
900
+ if (!await sessions.flush(request.agent.session))
901
+ return 'unavailable';
902
+ }
903
+ catch {
904
+ return 'unavailable';
905
+ }
906
+ if (signalAborted(request.signal))
907
+ return 'cancelled';
908
+ let persisted;
909
+ try {
910
+ persisted = this.resolveToolApprovalAuthority(ctx, request);
911
+ }
912
+ catch {
913
+ return 'unavailable';
914
+ }
915
+ if (!sameToolApprovalAuthority(authority, persisted))
916
+ return 'unavailable';
917
+ const operationId = `tool-approval:${randomUUID()}`;
918
+ const expiresAt = Date.now() + this.config.toolApprovalTtlMs;
919
+ const controller = new AbortController();
920
+ let requestCancelled = false;
921
+ const onRequestAbort = () => {
922
+ requestCancelled = true;
923
+ controller.abort(request.signal?.reason);
924
+ };
925
+ request.signal?.addEventListener('abort', onRequestAbort, { once: true });
926
+ const aborted = new Promise(resolve => {
927
+ controller.signal.addEventListener('abort', () => {
928
+ resolve(requestCancelled ? 'cancelled' : 'unavailable');
929
+ }, { once: true });
930
+ });
931
+ const timeout = setTimeout(() => {
932
+ controller.abort(new Error('assistant-delivery tool approval timed out'));
933
+ }, this.config.toolApprovalTtlMs);
934
+ timeout.unref?.();
935
+ this.toolApprovalControllers.add(controller);
936
+ const adapterRequest = Object.freeze({
937
+ operationId,
938
+ bindingId: authority.binding.id,
939
+ target: Object.freeze({
940
+ conversation: Object.freeze({ ...authority.binding.conversation }),
941
+ principal: Object.freeze({ ...authority.binding.principal }),
942
+ }),
943
+ expiresAt,
944
+ actionHash: authority.actionHash,
945
+ toolName: request.toolName,
946
+ callId: authority.callId,
947
+ ...(request.reason === undefined ? {} : { reason: request.reason }),
948
+ arguments: authority.arguments,
949
+ });
950
+ let outcome;
951
+ try {
952
+ const answer = Promise.resolve()
953
+ .then(() => authority.adapter.requestToolApproval(adapterRequest, controller.signal))
954
+ .then(value => value, () => 'unavailable');
955
+ outcome = await Promise.race([answer, aborted]);
956
+ }
957
+ finally {
958
+ clearTimeout(timeout);
959
+ request.signal?.removeEventListener('abort', onRequestAbort);
960
+ this.toolApprovalControllers.delete(controller);
961
+ }
962
+ if (signalAborted(request.signal))
963
+ return 'cancelled';
964
+ if (controller.signal.aborted || !this.active || Date.now() >= expiresAt)
965
+ return 'unavailable';
966
+ let current;
967
+ try {
968
+ current = this.resolveToolApprovalAuthority(ctx, request);
969
+ }
970
+ catch {
971
+ return 'unavailable';
972
+ }
973
+ if (current === undefined || !sameToolApprovalAuthority(authority, current))
974
+ return 'unavailable';
975
+ if (outcome === 'allowed-once') {
976
+ try {
977
+ // The tool guard ran before the native tool raised this approval. Do not
978
+ // consume its budget a second time, but do re-check the mutable hard stop
979
+ // as close as possible to releasing the owner's one-shot grant.
980
+ const emergencyStop = this.policy.getEmergencyStop();
981
+ // The owner granted this call; a later hard-stop is a Policy veto, not
982
+ // a user rejection. `rejected` is reserved for the signed reject action
983
+ // because upstream otherwise renders the false claim "the user rejected".
984
+ if (emergencyStop.enabled)
985
+ return 'unavailable';
986
+ if (emergencyStop.version !== current.policyEmergencyVersion)
987
+ return 'unavailable';
988
+ }
989
+ catch {
990
+ return 'unavailable';
991
+ }
992
+ }
993
+ return approvalOutcomes.includes(outcome) ? outcome : 'unavailable';
994
+ }
995
+ async acceptInbound(envelope) {
996
+ this.assertActive();
997
+ const accepted = this.deliveryStore.acceptInbound(envelope);
998
+ let record = accepted.record;
999
+ if (!['received', 'authorized'].includes(record.status)) {
1000
+ return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
1001
+ }
1002
+ if (!this.deliveryStore.isAuthorizedPrincipal(envelope.principal)) {
1003
+ record = this.deliveryStore.deadLetterInbox(record.id, 'unauthorized-principal');
1004
+ return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
1005
+ }
1006
+ const decision = this.policy.authorize({ subject: { kind: 'external', id: externalPrincipalId(envelope.principal) },
1007
+ action: 'ingest', resource: { kind: 'message', id: `inbound:${envelope.channel}/${envelope.account}` },
1008
+ context: { initiator: 'external' } }, { idempotencyKey: `message-inbound:${record.id}` });
1009
+ if (decision.effect !== 'allow') {
1010
+ record = this.deliveryStore.deadLetterInbox(record.id, `policy-${decision.reasonCode}`);
1011
+ return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
1012
+ }
1013
+ const sessionCommand = parseDeliveryCommand(envelope);
1014
+ if (isExactDeliveryCommand(sessionCommand, 'new', 'clear')) {
1015
+ return await this.runConversationTransition(envelope.conversation, async () => {
1016
+ await this.recoverPendingInbound(envelope, record.id);
1017
+ // A provider retry can arrive while the first copy is waiting for the
1018
+ // old generation to drain. Re-read after acquiring the transition:
1019
+ // once that copy queued the command, its rotation is already complete
1020
+ // and this duplicate must not create yet another generation.
1021
+ const currentRecord = this.deliveryStore.getInbox(record.id);
1022
+ if (!['received', 'authorized'].includes(currentRecord.status)) {
1023
+ return {
1024
+ duplicate: accepted.duplicate,
1025
+ inboxId: currentRecord.id,
1026
+ status: currentRecord.status,
1027
+ };
1028
+ }
1029
+ record = currentRecord;
1030
+ const binding = await this.bindingForInbound(envelope, record.id);
1031
+ if (binding === undefined) {
1032
+ const failed = this.deliveryStore.getInbox(record.id);
1033
+ return { duplicate: accepted.duplicate, inboxId: failed.id, status: failed.status };
1034
+ }
1035
+ // First invalidate all old work that cannot have crossed the Agent
1036
+ // dispatch gate. Then cancel and drain any turn that did cross it.
1037
+ // Rotation is last, so no fresh generation can execute beside the old.
1038
+ const fence = this.deliveryStore.cancelUndispatchedInboxBefore({
1039
+ bindingId: binding.id,
1040
+ beforeInboxId: record.id,
1041
+ failureCode: 'new-session-before-dispatch',
1042
+ });
1043
+ await this.runtime?.cancelActive?.(binding, 'new');
1044
+ await this.inbound.cancelUndispatchedClaims(fence.claimedInboxIds, 'new');
1045
+ const rotated = await this.rotateBinding(envelope, binding, record.id);
1046
+ record = this.deliveryStore.getInbox(record.id);
1047
+ if (record.status !== 'queued' || record.bindingId !== rotated.id) {
1048
+ throw new AssistantDeliveryError('runtime-conflict', 'new-session binding committed without its command Inbox');
1049
+ }
1050
+ return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
1051
+ });
1052
+ }
1053
+ if (isExactDeliveryCommand(sessionCommand, 'stop')) {
1054
+ return await this.runConversationTransition(envelope.conversation, async () => {
1055
+ await this.recoverPendingInbound(envelope, record.id);
1056
+ const currentRecord = this.deliveryStore.getInbox(record.id);
1057
+ if (!['received', 'authorized'].includes(currentRecord.status)) {
1058
+ return {
1059
+ duplicate: accepted.duplicate,
1060
+ inboxId: currentRecord.id,
1061
+ status: currentRecord.status,
1062
+ };
1063
+ }
1064
+ record = currentRecord;
1065
+ const binding = await this.bindingForInbound(envelope, record.id);
1066
+ if (binding === undefined) {
1067
+ const failed = this.deliveryStore.getInbox(record.id);
1068
+ return { duplicate: accepted.duplicate, inboxId: failed.id, status: failed.status };
1069
+ }
1070
+ const fence = this.deliveryStore.cancelUndispatchedInboxBefore({
1071
+ bindingId: binding.id,
1072
+ beforeInboxId: record.id,
1073
+ failureCode: 'user-stopped-before-dispatch',
1074
+ });
1075
+ // Holding the transition while the live turn drains prevents a later
1076
+ // message from overtaking the durable stop acknowledgement.
1077
+ await this.runtime?.cancelActive?.(binding, 'stop');
1078
+ await this.inbound.cancelUndispatchedClaims(fence.claimedInboxIds, 'stop');
1079
+ record = this.deliveryStore.queueInbox(record.id, binding.id);
1080
+ return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
1081
+ });
1082
+ }
1083
+ // All accept-side binding decisions join this short conversation lock.
1084
+ // It serializes only durable admission (not Agent execution), and makes a
1085
+ // queued `/stop` or `/new` an unambiguous boundary for later arrivals.
1086
+ return await this.runConversationTransition(envelope.conversation, async () => {
1087
+ await this.recoverPendingInbound(envelope, record.id);
1088
+ const currentRecord = this.deliveryStore.getInbox(record.id);
1089
+ if (!['received', 'authorized'].includes(currentRecord.status)) {
1090
+ return {
1091
+ duplicate: accepted.duplicate,
1092
+ inboxId: currentRecord.id,
1093
+ status: currentRecord.status,
1094
+ };
1095
+ }
1096
+ record = currentRecord;
1097
+ const binding = await this.bindingForInbound(envelope, record.id);
1098
+ if (binding === undefined) {
1099
+ const failed = this.deliveryStore.getInbox(record.id);
1100
+ return { duplicate: accepted.duplicate, inboxId: failed.id, status: failed.status };
1101
+ }
1102
+ record = this.deliveryStore.queueInbox(record.id, binding.id);
1103
+ return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
1104
+ });
1105
+ }
1106
+ enqueueBackground(input) {
1107
+ this.assertActive();
1108
+ const binding = this.deliveryStore.getBinding(input.bindingId);
1109
+ if (binding === undefined || binding.status !== 'active' || !this.hasActivePrincipal(binding)) {
1110
+ throw new AssistantDeliveryError('missing-binding', 'delivery binding does not exist or is revoked');
1111
+ }
1112
+ if (binding.workspace !== input.workspace) {
1113
+ throw new AssistantDeliveryError('missing-binding', 'delivery workspace does not match the active binding');
1114
+ }
1115
+ const decision = this.policy.authorize({ subject: { kind: 'background', id: input.sourceId,
1116
+ workspace: input.workspace, principal: externalPrincipalId(binding.principal) }, action: 'send',
1117
+ resource: { kind: 'message', id: binding.id }, context: { initiator: 'background' } }, { idempotencyKey: `message-send:${input.idempotencyKey}` });
1118
+ if (decision.effect !== 'allow')
1119
+ throw policyDenied(decision);
1120
+ if (!this.hasActivePrincipal(binding)) {
1121
+ throw new AssistantDeliveryError('missing-binding', 'delivery binding principal is no longer active');
1122
+ }
1123
+ return this.deliveryStore.enqueue({ idempotencyKey: input.idempotencyKey, bindingId: binding.id,
1124
+ target: { conversation: binding.conversation, principal: binding.principal }, text: input.text,
1125
+ format: input.format ?? 'plain' });
1126
+ }
1127
+ enqueueApproval(input) {
1128
+ this.assertActive();
1129
+ const binding = this.deliveryStore.getBinding(input.bindingId);
1130
+ if (binding === undefined || binding.status !== 'active') {
1131
+ throw new AssistantDeliveryError('missing-binding', 'delivery binding does not exist or is revoked');
1132
+ }
1133
+ if (binding.workspace !== input.workspace) {
1134
+ throw new AssistantDeliveryError('missing-binding', 'delivery workspace does not match the active binding');
1135
+ }
1136
+ const owner = this.deliveryStore.getPrincipal(binding.principal);
1137
+ const principal = externalPrincipalId(binding.principal);
1138
+ const proposal = this.policy.getProposal(input.approval.proposalId);
1139
+ if (owner?.status !== 'active' || owner.role !== 'owner'
1140
+ || proposal === undefined || proposal.status !== 'pending' || Date.now() >= proposal.expiresAt
1141
+ || proposal.principal !== principal
1142
+ || proposal.version !== input.approval.expectedVersion
1143
+ || proposal.expiresAt !== input.approval.expiresAt
1144
+ || proposal.summary !== input.approval.title
1145
+ || proposal.diffHash !== input.approval.diffHash
1146
+ || createHash('sha256').update(input.text).digest('hex') !== proposal.diffHash) {
1147
+ throw new AssistantDeliveryError('missing-binding', 'approval card does not match its active owner and Policy proposal');
1148
+ }
1149
+ const decision = this.policy.authorize({ subject: { kind: 'background', id: input.sourceId,
1150
+ workspace: input.workspace, principal }, action: 'approval.send',
1151
+ resource: { kind: 'message', id: binding.id }, context: { initiator: 'background' } }, { idempotencyKey: `message-approval:${input.idempotencyKey}` });
1152
+ if (decision.effect !== 'allow')
1153
+ throw policyDenied(decision);
1154
+ return this.deliveryStore.enqueue({ idempotencyKey: input.idempotencyKey, bindingId: binding.id,
1155
+ target: { conversation: binding.conversation, principal: binding.principal }, text: input.text,
1156
+ format: 'approval', approval: input.approval });
1157
+ }
1158
+ replyCommand(bindingInput, input) {
1159
+ this.assertActive();
1160
+ const binding = this.deliveryStore.getBinding(bindingInput.id);
1161
+ if (binding === undefined || binding.status !== 'active' || binding.sessionId !== bindingInput.sessionId
1162
+ || !this.hasActivePrincipal(binding)) {
1163
+ throw new AssistantDeliveryError('missing-binding', 'control command binding does not exist or is no longer active');
1164
+ }
1165
+ const legacyInboundKey = `inbound:${input.replyToEventId}:reply`;
1166
+ const idempotencyKey = input.idempotencyKey === legacyInboundKey
1167
+ ? this.inboundReplyIdempotencyKey(binding, input.replyToEventId)
1168
+ : input.idempotencyKey;
1169
+ const decision = this.policy.authorize({
1170
+ subject: {
1171
+ kind: 'agent',
1172
+ id: binding.agentPreset,
1173
+ workspace: binding.workspace,
1174
+ principal: externalPrincipalId(binding.principal),
1175
+ },
1176
+ action: 'reply',
1177
+ resource: { kind: 'message', id: binding.id },
1178
+ context: { initiator: 'external' },
1179
+ }, { idempotencyKey: `message-reply:${idempotencyKey}` });
1180
+ if (decision.effect !== 'allow')
1181
+ throw policyDenied(decision);
1182
+ if (!this.hasActivePrincipal(binding)) {
1183
+ throw new AssistantDeliveryError('missing-binding', 'control command binding principal is no longer active');
1184
+ }
1185
+ const format = this.replyFormat(binding.conversation, input.format) ?? 'plain';
1186
+ return this.deliveryStore.enqueue({
1187
+ idempotencyKey,
1188
+ bindingId: binding.id,
1189
+ target: { conversation: binding.conversation, principal: binding.principal },
1190
+ text: input.text,
1191
+ format,
1192
+ ...(input.modelPicker === undefined ? {} : { modelPicker: input.modelPicker }),
1193
+ ...(format !== 'permission-picker' || input.permissionPicker === undefined
1194
+ ? {}
1195
+ : { permissionPicker: input.permissionPicker }),
1196
+ replyToEventId: input.replyToEventId,
1197
+ });
1198
+ }
1199
+ /**
1200
+ * Answers are authored as Markdown, but the coordinator drops an intent whose format the adapter
1201
+ * does not declare (`unsupported-format`). Ask for Markdown only where the channel renders it and
1202
+ * degrade to plain text elsewhere, so a reply is never lost to a capability mismatch.
1203
+ */
1204
+ replyFormat(conversation, requested) {
1205
+ if (requested === 'permission-picker') {
1206
+ const adapter = this.registry.get(conversation.channel, conversation.account);
1207
+ return adapter?.capabilities.formats.includes('permission-picker') === true ? requested : 'plain';
1208
+ }
1209
+ if (requested !== 'markdown')
1210
+ return requested;
1211
+ const adapter = this.registry.get(conversation.channel, conversation.account);
1212
+ return adapter?.capabilities.formats.includes('markdown') === true ? 'markdown' : 'plain';
1213
+ }
1214
+ reply(agent, input) {
1215
+ this.assertActive();
1216
+ const binding = agent === undefined ? undefined : this.deliveryStore.getBindingBySession(String(agent.session.id));
1217
+ if (agent === undefined || binding === undefined || binding.status !== 'active'
1218
+ || agent.session.header.cwd !== binding.workspace
1219
+ || agent.session.header.agentPreset !== binding.agentPreset
1220
+ || !this.hasActivePrincipal(binding)) {
1221
+ throw new AssistantDeliveryError('missing-binding', 'Agent session is not bound to an active delivery route');
1222
+ }
1223
+ let idempotencyKey = input.idempotencyKey;
1224
+ if (input.replyToEventId !== undefined
1225
+ && input.idempotencyKey === `inbound:${input.replyToEventId}:reply`) {
1226
+ idempotencyKey = this.inboundReplyIdempotencyKey(binding, input.replyToEventId);
1227
+ }
1228
+ const decision = this.policy.authorizeAgent(agent, 'reply', { kind: 'message', id: binding.id }, { idempotencyKey: `message-reply:${idempotencyKey}` });
1229
+ if (decision.effect !== 'allow')
1230
+ throw policyDenied(decision);
1231
+ if (!this.hasActivePrincipal(binding)) {
1232
+ throw new AssistantDeliveryError('missing-binding', 'Agent delivery principal is no longer active');
1233
+ }
1234
+ const format = this.replyFormat(binding.conversation, input.format) ?? 'plain';
1235
+ return this.deliveryStore.enqueue({ idempotencyKey, bindingId: binding.id,
1236
+ target: { conversation: binding.conversation, principal: binding.principal }, text: input.text,
1237
+ format,
1238
+ ...(input.modelPicker === undefined ? {} : { modelPicker: input.modelPicker }),
1239
+ ...(format !== 'permission-picker' || input.permissionPicker === undefined
1240
+ ? {}
1241
+ : { permissionPicker: input.permissionPicker }),
1242
+ ...(input.replyToEventId === undefined ? {} : { replyToEventId: input.replyToEventId }) });
1243
+ }
1244
+ history(agent, input) {
1245
+ this.assertActive();
1246
+ const binding = agent === undefined ? undefined : this.deliveryStore.getBindingBySession(String(agent.session.id));
1247
+ if (agent === undefined || binding === undefined || binding.status !== 'active'
1248
+ || agent.session.header.cwd !== binding.workspace
1249
+ || agent.session.header.agentPreset !== binding.agentPreset) {
1250
+ throw new AssistantDeliveryError('missing-binding', 'Agent session has no active matching delivery binding');
1251
+ }
1252
+ const decision = this.policy.authorizeAgent(agent, 'history', { kind: 'message', id: binding.id });
1253
+ if (decision.effect !== 'allow')
1254
+ throw policyDenied(decision);
1255
+ const query = input.limit === undefined ? { bindingId: binding.id } : { bindingId: binding.id, limit: input.limit };
1256
+ return { binding, inbox: this.deliveryStore.listInbox(query), outbox: this.deliveryStore.listOutbox(query) };
1257
+ }
1258
+ bindAgentApprovalRoute(agent, input) {
1259
+ this.assertActive();
1260
+ const binding = this.deliveryStore.getBinding(input.bindingId);
1261
+ const owner = binding === undefined ? undefined : this.deliveryStore.getPrincipal(binding.principal);
1262
+ if (agent === undefined || binding?.status !== 'active' || owner?.status !== 'active' || owner.role !== 'owner'
1263
+ || agent.session.header.cwd !== binding.workspace
1264
+ || agent.session.header.agentPreset !== binding.agentPreset) {
1265
+ throw new AssistantDeliveryError('missing-binding', 'Agent identity does not match an active owner approval route');
1266
+ }
1267
+ const direct = this.deliveryStore.getBindingBySession(String(agent.session.id));
1268
+ if (direct !== undefined && direct.id !== binding.id) {
1269
+ throw new AssistantDeliveryError('missing-binding', 'Agent session is already bound to another approval route');
1270
+ }
1271
+ const token = Symbol('assistant-delivery.approval-binding');
1272
+ this.agentApprovalBindings.set(agent, { bindingId: binding.id, token });
1273
+ let active = true;
1274
+ return () => {
1275
+ if (!active)
1276
+ return;
1277
+ active = false;
1278
+ if (this.agentApprovalBindings.get(agent)?.token === token)
1279
+ this.agentApprovalBindings.delete(agent);
1280
+ };
1281
+ }
1282
+ prepareAgentApproval(agent, input) {
1283
+ this.assertActive();
1284
+ const sourceId = input.sourceId.trim();
1285
+ const direct = agent === undefined ? undefined : this.deliveryStore.getBindingBySession(String(agent.session.id));
1286
+ const delegatedId = agent === undefined ? undefined : this.agentApprovalBindings.get(agent)?.bindingId;
1287
+ const delegated = delegatedId === undefined ? undefined : this.deliveryStore.getBinding(delegatedId);
1288
+ if (direct !== undefined && delegated !== undefined && direct.id !== delegated.id) {
1289
+ throw new AssistantDeliveryError('missing-binding', 'Agent approval routes conflict');
1290
+ }
1291
+ const binding = direct ?? delegated;
1292
+ const owner = binding === undefined ? undefined : this.deliveryStore.getPrincipal(binding.principal);
1293
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u.test(sourceId)
1294
+ || binding?.status !== 'active' || owner?.status !== 'active' || owner.role !== 'owner'
1295
+ || agent?.session.header.cwd !== binding.workspace
1296
+ || agent.session.header.agentPreset !== binding.agentPreset) {
1297
+ throw new AssistantDeliveryError('missing-binding', 'Agent session has no authenticated active owner approval route');
1298
+ }
1299
+ return Object.freeze({ sourceId, bindingId: binding.id, workspace: binding.workspace,
1300
+ principal: externalPrincipalId(binding.principal) });
1301
+ }
1302
+ async tick() {
1303
+ this.assertActive();
1304
+ await this.inbound.tick();
1305
+ await this.drainModelSelectionSettlements();
1306
+ this.drainApprovalDispatches();
1307
+ await this.outbound.tick();
1308
+ }
1309
+ async whenIdle() {
1310
+ this.assertActive();
1311
+ await Promise.all([this.inbound.whenIdle(), this.outbound.whenIdle(), this.modelSelectionFlight]);
1312
+ }
1313
+ start() {
1314
+ this.assertActive();
1315
+ if (this.timer !== undefined)
1316
+ return;
1317
+ this.timer = setInterval(() => void this.tick(), this.config.tickIntervalMs);
1318
+ this.timer.unref?.();
1319
+ void this.tick();
1320
+ }
1321
+ async stop() {
1322
+ this.assertActive();
1323
+ await this.stopInternal();
1324
+ }
1325
+ async ensureBinding(envelope) {
1326
+ const key = JSON.stringify(envelope.conversation);
1327
+ const existing = this.bindingFlights.get(key);
1328
+ if (existing !== undefined)
1329
+ return existing;
1330
+ const runtime = this.runtime;
1331
+ if (runtime === undefined)
1332
+ throw new AssistantDeliveryError('runtime-unavailable', 'inbound runtime is unavailable');
1333
+ const promise = (async () => {
1334
+ const current = this.deliveryStore.getActiveBinding(envelope.conversation);
1335
+ if (current !== undefined)
1336
+ return current;
1337
+ const generation = this.deliveryStore.nextBindingGeneration(envelope.conversation);
1338
+ const created = await runtime.createSession({ envelope, generation, signal: new AbortController().signal });
1339
+ this.assertActive();
1340
+ return this.deliveryStore.createBinding({ conversation: envelope.conversation, principal: envelope.principal,
1341
+ workspace: created.workspace, agentPreset: created.agentPreset, sessionId: created.sessionId,
1342
+ policyRef: created.policyRef, expectedGeneration: generation });
1343
+ })().finally(() => this.bindingFlights.delete(key));
1344
+ this.bindingFlights.set(key, promise);
1345
+ return promise;
1346
+ }
1347
+ async bindingForInbound(envelope, inboxId) {
1348
+ let binding = this.deliveryStore.getActiveBinding(envelope.conversation);
1349
+ if (binding === undefined) {
1350
+ if (this.runtime === undefined) {
1351
+ this.deliveryStore.deadLetterInbox(inboxId, 'runtime-unavailable');
1352
+ return undefined;
1353
+ }
1354
+ binding = await this.ensureBinding(envelope);
1355
+ }
1356
+ if (externalPrincipalId(binding.principal) !== externalPrincipalId(envelope.principal)) {
1357
+ this.deliveryStore.deadLetterInbox(inboxId, 'binding-principal-mismatch');
1358
+ return undefined;
1359
+ }
1360
+ return binding;
1361
+ }
1362
+ /**
1363
+ * Finish admissions that survived a process exit before their queue/rotate
1364
+ * commit. The provider need not retransmit them: any later message in the
1365
+ * same conversation first drives every older received row forward in its
1366
+ * original durable Inbox order. This includes ordinary messages that were
1367
+ * already persisted behind a blocked `/stop` or `/new` transition.
1368
+ */
1369
+ async recoverPendingInbound(boundary, beforeInboxId) {
1370
+ while (true) {
1371
+ const pending = this.deliveryStore.findPendingInboundBefore({
1372
+ conversation: boundary.conversation,
1373
+ principal: boundary.principal,
1374
+ beforeInboxId,
1375
+ });
1376
+ if (pending === undefined)
1377
+ return;
1378
+ if (!this.deliveryStore.isAuthorizedPrincipal(pending.envelope.principal)) {
1379
+ this.deliveryStore.deadLetterInbox(pending.id, 'unauthorized-principal');
1380
+ continue;
1381
+ }
1382
+ const decision = this.policy.authorize({
1383
+ subject: { kind: 'external', id: externalPrincipalId(pending.envelope.principal) },
1384
+ action: 'ingest',
1385
+ resource: { kind: 'message', id: `inbound:${pending.envelope.channel}/${pending.envelope.account}` },
1386
+ context: { initiator: 'external' },
1387
+ }, { idempotencyKey: `message-inbound:${pending.id}` });
1388
+ if (decision.effect !== 'allow') {
1389
+ this.deliveryStore.deadLetterInbox(pending.id, `policy-${decision.reasonCode}`);
1390
+ continue;
1391
+ }
1392
+ const command = parseDeliveryCommand(pending.envelope);
1393
+ const reset = isExactDeliveryCommand(command, 'new', 'clear');
1394
+ const stop = isExactDeliveryCommand(command, 'stop');
1395
+ const binding = await this.bindingForInbound(pending.envelope, pending.id);
1396
+ if (binding === undefined)
1397
+ continue;
1398
+ if (!reset && !stop) {
1399
+ this.deliveryStore.queueInbox(pending.id, binding.id);
1400
+ continue;
1401
+ }
1402
+ const fence = this.deliveryStore.cancelUndispatchedInboxBefore({
1403
+ bindingId: binding.id,
1404
+ beforeInboxId: pending.id,
1405
+ failureCode: reset ? 'new-session-before-dispatch' : 'user-stopped-before-dispatch',
1406
+ });
1407
+ const cancellationCommand = reset ? 'new' : 'stop';
1408
+ await this.runtime?.cancelActive?.(binding, cancellationCommand);
1409
+ await this.inbound.cancelUndispatchedClaims(fence.claimedInboxIds, cancellationCommand);
1410
+ if (reset) {
1411
+ await this.rotateBinding(pending.envelope, binding, pending.id);
1412
+ }
1413
+ else {
1414
+ this.deliveryStore.queueInbox(pending.id, binding.id);
1415
+ }
1416
+ }
1417
+ }
1418
+ async runConversationTransition(conversation, task) {
1419
+ const key = JSON.stringify(canonicalConversation(conversation));
1420
+ const previous = this.conversationTransitions.get(key) ?? Promise.resolve();
1421
+ let release;
1422
+ const own = new Promise(resolve => { release = resolve; });
1423
+ const fence = previous.catch(() => { }).then(() => own);
1424
+ this.conversationTransitions.set(key, fence);
1425
+ await previous.catch(() => { });
1426
+ try {
1427
+ return await task();
1428
+ }
1429
+ finally {
1430
+ release();
1431
+ if (this.conversationTransitions.get(key) === fence)
1432
+ this.conversationTransitions.delete(key);
1433
+ }
1434
+ }
1435
+ async rotateBinding(envelope, previous, commandInboxId) {
1436
+ const key = `${JSON.stringify(envelope.conversation)}:/new:${envelope.eventId}`;
1437
+ const existing = this.bindingFlights.get(key);
1438
+ if (existing !== undefined)
1439
+ return existing;
1440
+ const runtime = this.runtime;
1441
+ if (runtime === undefined)
1442
+ throw new AssistantDeliveryError('runtime-unavailable', 'inbound runtime is unavailable');
1443
+ const promise = (async () => {
1444
+ const current = this.deliveryStore.getActiveBinding(envelope.conversation);
1445
+ if (current === undefined)
1446
+ throw new AssistantDeliveryError('missing-binding', 'active binding disappeared during /new');
1447
+ if (current.id !== previous.id)
1448
+ return current;
1449
+ const generation = this.deliveryStore.nextBindingGeneration(envelope.conversation);
1450
+ if (generation !== current.generation + 1) {
1451
+ throw new AssistantDeliveryError('runtime-conflict', 'binding generation changed before /new session creation');
1452
+ }
1453
+ const created = await runtime.createSession({ envelope, generation,
1454
+ previous: current, signal: new AbortController().signal });
1455
+ this.assertActive();
1456
+ return this.deliveryStore.rotateBindingAndQueueCommand({
1457
+ bindingId: current.id,
1458
+ expectedVersion: current.version,
1459
+ sessionId: created.sessionId,
1460
+ inboxId: commandInboxId,
1461
+ }).binding;
1462
+ })().finally(() => this.bindingFlights.delete(key));
1463
+ this.bindingFlights.set(key, promise);
1464
+ return promise;
1465
+ }
1466
+ async stopInternal() {
1467
+ if (this.timer !== undefined)
1468
+ clearInterval(this.timer);
1469
+ if (this.modelSelectionRetryTimer !== undefined)
1470
+ clearTimeout(this.modelSelectionRetryTimer);
1471
+ this.timer = undefined;
1472
+ this.modelSelectionRetryTimer = undefined;
1473
+ await Promise.all([this.inbound.stop(), this.outbound.stop(), this.modelSelectionFlight]);
1474
+ }
1475
+ drainApprovalDispatches() {
1476
+ const cursor = this.deliveryStore.getApprovalDispatchCursor();
1477
+ const dispatches = this.policy.listPendingApprovalDispatches(100, cursor.after);
1478
+ for (const dispatch of dispatches) {
1479
+ try {
1480
+ this.enqueueApproval({
1481
+ sourceId: dispatch.sourceId,
1482
+ workspace: dispatch.workspace,
1483
+ bindingId: dispatch.bindingId,
1484
+ idempotencyKey: `approval-card:${dispatch.proposalId}`,
1485
+ text: dispatch.diff,
1486
+ approval: {
1487
+ operationId: `approval:${dispatch.proposalId}`,
1488
+ proposalId: dispatch.proposalId,
1489
+ expectedVersion: dispatch.proposalVersion,
1490
+ expiresAt: dispatch.expiresAt,
1491
+ title: dispatch.summary,
1492
+ diffHash: dispatch.diffHash,
1493
+ },
1494
+ });
1495
+ this.policy.markApprovalDispatchEnqueued(dispatch.proposalId, dispatch.version);
1496
+ }
1497
+ catch {
1498
+ // One invalid/revoked route must not starve later durable dispatches.
1499
+ }
1500
+ }
1501
+ const last = dispatches.at(-1);
1502
+ const after = dispatches.length === 100 && last !== undefined
1503
+ ? { createdAt: last.createdAt, proposalId: last.proposalId }
1504
+ : undefined;
1505
+ if (after === undefined && cursor.after === undefined)
1506
+ return;
1507
+ try {
1508
+ this.deliveryStore.advanceApprovalDispatchCursor({ expectedVersion: cursor.version,
1509
+ ...(after === undefined ? {} : { after }) });
1510
+ }
1511
+ catch (error) {
1512
+ // Another Host sharing the same durable Delivery DB won the scan fence.
1513
+ if (error instanceof DeliveryStoreError && error.code === 'stale-fence')
1514
+ return;
1515
+ throw error;
1516
+ }
1517
+ }
1518
+ drainModelSelectionSettlements() {
1519
+ if (this.modelSelectionRetryTimer !== undefined) {
1520
+ clearTimeout(this.modelSelectionRetryTimer);
1521
+ this.modelSelectionRetryTimer = undefined;
1522
+ }
1523
+ if (this.modelSelectionFlight !== undefined) {
1524
+ return this.modelSelectionFlight.then(() => this.drainModelSelectionSettlements());
1525
+ }
1526
+ const flight = this.runModelSelectionSettlements()
1527
+ .finally(() => { if (this.modelSelectionFlight === flight)
1528
+ this.modelSelectionFlight = undefined; });
1529
+ this.modelSelectionFlight = flight;
1530
+ return flight;
1531
+ }
1532
+ async runModelSelectionSettlements() {
1533
+ const llm = this.ctx.get('llm');
1534
+ if (llm === undefined)
1535
+ return;
1536
+ while (this.active) {
1537
+ const claims = this.deliveryStore.claimModelSelectionSettlements({
1538
+ ownerId: this.ownerId,
1539
+ leaseMs: this.config.leaseMs,
1540
+ limit: this.config.maxConcurrency,
1541
+ });
1542
+ if (claims.length === 0) {
1543
+ const next = this.deliveryStore.nextModelSelectionClaimAt();
1544
+ if (next !== undefined && this.modelSelectionRetryTimer === undefined) {
1545
+ const delay = Math.max(1, Math.min(2_147_483_647, next - Date.now()));
1546
+ this.modelSelectionRetryTimer = setTimeout(() => {
1547
+ this.modelSelectionRetryTimer = undefined;
1548
+ if (this.active)
1549
+ void this.drainModelSelectionSettlements();
1550
+ }, delay);
1551
+ this.modelSelectionRetryTimer.unref?.();
1552
+ }
1553
+ return;
1554
+ }
1555
+ await Promise.allSettled(claims.map(async (claim) => {
1556
+ const finishWithoutReply = (result) => this.deliveryStore.completeModelSelectionSettlement({
1557
+ operationId: claim.operationId,
1558
+ payloadHash: claim.payloadHash,
1559
+ result,
1560
+ ownerId: this.ownerId,
1561
+ fencingToken: claim.fencingToken,
1562
+ });
1563
+ const payload = claim.payload;
1564
+ if (!isModelSelectionPayload(payload) || payload.bindingId !== claim.bindingId) {
1565
+ finishWithoutReply({ status: 'rejected', reason: 'authorization-revoked' });
1566
+ return;
1567
+ }
1568
+ const ownsBinding = (binding) => {
1569
+ const principal = this.deliveryStore.getPrincipal(payload.principal);
1570
+ return binding?.status === 'active'
1571
+ && principal?.status === 'active'
1572
+ && binding.conversation.chat === payload.callbackChatId
1573
+ && JSON.stringify(binding.principal) === JSON.stringify(principal.principal);
1574
+ };
1575
+ // The callback claim already consumed any authorization budget. Workers
1576
+ // re-evaluate current rules/emergency-stop without charging it again.
1577
+ const authorized = (binding) => this.policy.evaluate({
1578
+ subject: { kind: 'external', id: externalPrincipalId(payload.principal) },
1579
+ action: 'ingest',
1580
+ resource: { kind: 'message', id: `model-selection:${binding.id}` },
1581
+ context: { initiator: 'external' },
1582
+ }).effect === 'allow';
1583
+ const complete = (binding, result, text, selection) => {
1584
+ const reply = (content) => ({
1585
+ idempotencyKey: `model-selection:${payload.callbackEventId}:reply`,
1586
+ bindingId: binding.id,
1587
+ target: { conversation: binding.conversation, principal: binding.principal },
1588
+ text: content,
1589
+ format: 'plain',
1590
+ });
1591
+ const superseded = { status: 'rejected', reason: 'selection-superseded' };
1592
+ return this.deliveryStore.completeModelSelectionSettlement({
1593
+ operationId: claim.operationId,
1594
+ payloadHash: claim.payloadHash,
1595
+ result,
1596
+ ...(selection === undefined ? {} : { selection: { conversation: binding.conversation, route: selection } }),
1597
+ reply: reply(text),
1598
+ superseded: {
1599
+ result: superseded,
1600
+ reply: reply('该模型选择已被更晚的操作取代,未更改当前模型。'),
1601
+ },
1602
+ ownerId: this.ownerId,
1603
+ fencingToken: claim.fencingToken,
1604
+ });
1605
+ };
1606
+ const initialBinding = this.deliveryStore.getBinding(claim.bindingId);
1607
+ if (!ownsBinding(initialBinding) || !authorized(initialBinding)) {
1608
+ finishWithoutReply({ status: 'rejected', reason: 'authorization-revoked' });
1609
+ return;
1610
+ }
1611
+ if (payload.provider !== payload.modelProvider) {
1612
+ complete(initialBinding, { status: 'rejected', reason: 'provider-model-mismatch' }, `分组 ${payload.provider} 与模型 ${payload.modelProvider}/${payload.model} 不匹配,请重新发送 /model。`);
1613
+ return;
1614
+ }
1615
+ if (!llm.listProviders().some(provider => provider.id === payload.provider)) {
1616
+ complete(initialBinding, { status: 'rejected', reason: 'provider-unavailable' }, `模型分组 ${payload.provider} 当前不可用,请重新发送 /model。`);
1617
+ return;
1618
+ }
1619
+ let leaseLost = false;
1620
+ const renewLease = () => {
1621
+ try {
1622
+ if (!this.deliveryStore.renewModelSelectionSettlement({
1623
+ operationId: claim.operationId,
1624
+ ownerId: this.ownerId,
1625
+ fencingToken: claim.fencingToken,
1626
+ leaseMs: this.config.leaseMs,
1627
+ }))
1628
+ leaseLost = true;
1629
+ }
1630
+ catch {
1631
+ leaseLost = true;
1632
+ }
1633
+ };
1634
+ const heartbeat = setInterval(renewLease, Math.max(1, Math.floor(this.config.leaseMs / 3)));
1635
+ heartbeat.unref?.();
1636
+ const resolveController = new AbortController();
1637
+ let resolveTimeout;
1638
+ const deadline = new Promise((_resolve, reject) => {
1639
+ resolveTimeout = setTimeout(() => {
1640
+ const error = new Error('model selection resolution timed out');
1641
+ resolveController.abort(error);
1642
+ reject(error);
1643
+ }, this.config.leaseMs);
1644
+ });
1645
+ let resolved;
1646
+ try {
1647
+ resolved = await Promise.race([
1648
+ llm.resolveModelInfo(payload.provider, payload.model, resolveController.signal),
1649
+ deadline,
1650
+ ]);
1651
+ }
1652
+ catch {
1653
+ // Model availability is persisted below, after refreshing authority.
1654
+ }
1655
+ finally {
1656
+ if (resolveTimeout !== undefined)
1657
+ clearTimeout(resolveTimeout);
1658
+ clearInterval(heartbeat);
1659
+ }
1660
+ renewLease();
1661
+ if (leaseLost)
1662
+ return;
1663
+ const binding = this.deliveryStore.getBinding(claim.bindingId);
1664
+ if (!ownsBinding(binding)) {
1665
+ finishWithoutReply({ status: 'rejected', reason: 'authorization-revoked' });
1666
+ return;
1667
+ }
1668
+ if (!authorized(binding)) {
1669
+ finishWithoutReply({ status: 'rejected', reason: 'authorization-revoked' });
1670
+ return;
1671
+ }
1672
+ if (resolved === undefined) {
1673
+ complete(binding, { status: 'rejected', reason: 'model-unavailable' }, `模型 ${payload.provider}/${payload.model} 当前不可用,请重新发送 /model。`);
1674
+ return;
1675
+ }
1676
+ if (payload.reasoningEffort !== null
1677
+ && !resolved.reasoning?.efforts.some(effort => String(effort.id) === payload.reasoningEffort)) {
1678
+ complete(binding, { status: 'rejected', reason: 'invalid-effort' }, `模型 ${payload.provider}/${payload.model} 不支持 effort “${payload.reasoningEffort}”,请重新发送 /model。`);
1679
+ return;
1680
+ }
1681
+ const selection = {
1682
+ provider: payload.provider,
1683
+ model: payload.model,
1684
+ ...(payload.reasoningEffort === null ? {} : { reasoningEffort: payload.reasoningEffort }),
1685
+ };
1686
+ complete(binding, { status: 'selected', selection }, `已切换到 ${payload.provider}/${payload.model}${payload.reasoningEffort === null
1687
+ ? ',effort:默认'
1688
+ : `,effort:${payload.reasoningEffort}`}。下一条消息起生效,上下文保留。`, selection);
1689
+ }));
1690
+ }
1691
+ }
1692
+ assertActive() {
1693
+ if (!this.active)
1694
+ throw new AssistantDeliveryError('disposed', 'assistant-delivery service is disposed');
1695
+ }
1696
+ }
1697
+ export const Config = AssistantDeliveryService.Config;
1698
+ //# sourceMappingURL=service.js.map