@borgee/agents-host 0.2.97 → 0.2.110

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 (104) hide show
  1. package/README.md +77 -5
  2. package/dist/agents-host.d.ts +16 -0
  3. package/dist/agents-host.js +608 -82
  4. package/dist/catalog-worker.js +21 -0
  5. package/dist/catalog-worker.js.map +7 -0
  6. package/dist/chat/chat-control-plane.d.ts +19 -4
  7. package/dist/chat/normalized-input.d.ts +9 -0
  8. package/dist/chat/normalized-input.js +24 -0
  9. package/dist/chat/sdk-chat-control-plane.d.ts +46 -6
  10. package/dist/chat/sdk-chat-control-plane.js +169 -6
  11. package/dist/cli-args.d.ts +1 -1
  12. package/dist/cli-args.js +13 -4
  13. package/dist/config.d.ts +3 -1
  14. package/dist/config.js +25 -0
  15. package/dist/context/injection.d.ts +16 -0
  16. package/dist/context/injection.js +37 -21
  17. package/dist/context/prompt.d.ts +12 -0
  18. package/dist/context/prompt.js +25 -0
  19. package/dist/context/skill-manual.js +1 -1
  20. package/dist/context/turn-preparation.d.ts +2 -1
  21. package/dist/context/turn-preparation.js +15 -0
  22. package/dist/execution-telemetry.d.ts +96 -0
  23. package/dist/execution-telemetry.js +583 -0
  24. package/dist/gateway/channel-file-workspace.d.ts +15 -0
  25. package/dist/gateway/channel-file-workspace.js +130 -0
  26. package/dist/gateway/localhost-gateway.js +167 -2
  27. package/dist/hosted-turn-content.js +3 -2
  28. package/dist/local-config.js +34 -1
  29. package/dist/managed-daemon.js +15 -0
  30. package/dist/native-environment.d.ts +2 -0
  31. package/dist/native-environment.js +9 -0
  32. package/dist/native-protocol-types/codec.d.ts +49 -0
  33. package/dist/native-protocol-types/commands.d.ts +71 -0
  34. package/dist/native-protocol-types/compatibility.d.ts +5 -0
  35. package/dist/native-protocol-types/cursor.d.ts +7 -0
  36. package/dist/native-protocol-types/envelope.d.ts +1105 -0
  37. package/dist/native-protocol-types/file-changes.d.ts +20 -0
  38. package/dist/native-protocol-types/history.d.ts +487 -0
  39. package/dist/native-protocol-types/index.d.ts +18 -0
  40. package/dist/native-protocol-types/interactions.d.ts +619 -0
  41. package/dist/native-protocol-types/messages.d.ts +2482 -0
  42. package/dist/native-protocol-types/public-validation.d.ts +10 -0
  43. package/dist/native-protocol-types/remote-host-uplink.d.ts +67 -0
  44. package/dist/native-protocol-types/resources.d.ts +126 -0
  45. package/dist/native-protocol-types/session-settings.d.ts +22 -0
  46. package/dist/native-protocol-types/snapshot.d.ts +575 -0
  47. package/dist/native-protocol-types/timeline.d.ts +1503 -0
  48. package/dist/native-protocol-types/tool-result.d.ts +26 -0
  49. package/dist/native-protocol-types/uplink.d.ts +56 -0
  50. package/dist/native-protocol-types/version.d.ts +5 -0
  51. package/dist/native-provider-types/commands.d.ts +51 -0
  52. package/dist/native-provider-types/control.d.ts +192 -0
  53. package/dist/native-provider-types/file-changes.d.ts +9 -0
  54. package/dist/native-provider-types/index.d.ts +8 -0
  55. package/dist/native-provider-types/interactions.d.ts +7 -0
  56. package/dist/native-provider-types/observation.d.ts +209 -0
  57. package/dist/native-provider-types/provider.d.ts +124 -0
  58. package/dist/native-provider-types/session-settings.d.ts +17 -0
  59. package/dist/native-provider-types/testing.d.ts +27 -0
  60. package/dist/native-provider-types/tool-result.d.ts +21 -0
  61. package/dist/native-providers.d.ts +21 -0
  62. package/dist/native-providers.js +18402 -0
  63. package/dist/native-providers.js.map +7 -0
  64. package/dist/plugin-sdk.js +10713 -229
  65. package/dist/plugin-sdk.js.map +4 -4
  66. package/dist/policy/copilot-permission.js +1 -8
  67. package/dist/policy/gateway-authorization.d.ts +1 -1
  68. package/dist/policy/gateway-authorization.js +22 -2
  69. package/dist/providers/claude/adapter.js +1 -0
  70. package/dist/providers/claude/cli-client.js +4 -0
  71. package/dist/providers/codex/adapter.js +1 -0
  72. package/dist/providers/codex/cli-client.js +5 -0
  73. package/dist/providers/copilot/adapter.js +4 -0
  74. package/dist/providers/copilot/cli-client.js +11 -1
  75. package/dist/providers/copilot/sdk-session.d.ts +12 -3
  76. package/dist/providers/copilot/sdk-session.js +230 -6
  77. package/dist/providers/create-provider.js +24 -0
  78. package/dist/providers/normalized/adapter.d.ts +107 -0
  79. package/dist/providers/normalized/adapter.js +1650 -0
  80. package/dist/providers/normalized/control-policy.d.ts +5 -0
  81. package/dist/providers/normalized/control-policy.js +12 -0
  82. package/dist/providers/normalized/interactions.d.ts +3 -0
  83. package/dist/providers/normalized/interactions.js +39 -0
  84. package/dist/providers/normalized/legacy-input-ownership.d.ts +34 -0
  85. package/dist/providers/normalized/legacy-input-ownership.js +123 -0
  86. package/dist/providers/normalized/session-management.d.ts +67 -0
  87. package/dist/providers/normalized/session-management.js +334 -0
  88. package/dist/providers/normalized/session-ownership.d.ts +3 -0
  89. package/dist/providers/normalized/session-ownership.js +90 -0
  90. package/dist/providers/prompt-usage.d.ts +8 -0
  91. package/dist/providers/prompt-usage.js +52 -0
  92. package/dist/providers/provider-adapter.d.ts +43 -1
  93. package/dist/providers/provider-adapter.js +8 -0
  94. package/dist/state-paths.d.ts +2 -0
  95. package/dist/state-paths.js +6 -0
  96. package/dist/types.d.ts +66 -0
  97. package/dist/typing-lease.d.ts +14 -0
  98. package/dist/typing-lease.js +31 -0
  99. package/dist/vendor/agent-provider-codex/LICENSE +211 -0
  100. package/dist/vendor/agent-provider-codex/NOTICE +13 -0
  101. package/package.json +22 -13
  102. package/skills/borgee-agent/SKILL.md +12 -4
  103. package/skills/borgee-agent/scripts/borgee-agent.mjs +110 -0
  104. package/skills/borgee-agent/scripts/borgee-agent.py +116 -0
@@ -0,0 +1,1650 @@
1
+ import { questionnaireAnswerText, SessionTimeline, SessionObservationProjection, publishSessionTimeline, InputOwnership, NormalizedActivityPublisher, formatTimelineReplacement, createSessionDirectory } from '../../plugin-sdk.js';
2
+ import { recordProtocolTrace, NormalizedObservationGate, sessionControlResponse } from '../../plugin-sdk.js';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { realpath } from 'node:fs/promises';
5
+ import { dirname } from 'node:path';
6
+ import { AgentCommandRejectedError, AgentInputRejectedError, AgentSessionInUseError, PROTOCOL_VERSION, decodeAgentSnapshot, validateNativeInteractionResponse, redactInteractionRequest, redactInteractionResponse } from '../../native-providers.js';
7
+ import { assertHostedInteractionScope, nativeSessionFailureMessage } from './control-policy.js';
8
+ import { InputOwnership as LegacyInputOwnership } from './legacy-input-ownership.js';
9
+ import { parseProviderReply } from '../awaiting-user.js';
10
+ import { buildCodexTurnContext } from '../../context/prompt.js';
11
+ import { buildHostedIncomingContentText } from '../../hosted-turn-content.js';
12
+ import { createHostedProviderCapabilities, ProviderInputRejectedError, ProviderTurnCancelledError } from '../provider-adapter.js';
13
+ import { HostSessionManagement } from './session-management.js';
14
+ const SELECTED_SESSION_NOT_IDLE = 'A native turn is already active in the selected Session.';
15
+ function questionnaireReplyText(item) {
16
+ return item.text || item.questions.map(question => [question.header, question.prompt, ...question.options.map(option => option.label)].join('\n')).join('\n\n');
17
+ }
18
+ function deferred() {
19
+ let resolve;
20
+ let reject;
21
+ const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
22
+ void promise.catch(() => { });
23
+ const value = { fulfilled: false, promise, resolve: (result) => { value.fulfilled = true; resolve(result); }, reject };
24
+ return value;
25
+ }
26
+ function publicHostInteraction(value) {
27
+ const request = redactInteractionRequest(value);
28
+ return request.kind === 'tool_approval'
29
+ ? { ...request, allowScopes: request.allowScopes.filter(scope => scope === 'once'), policies: undefined }
30
+ : request.kind === 'permission_approval' ? { ...request, allowScopes: request.allowScopes.filter(scope => scope === 'turn') } : request;
31
+ }
32
+ /** Owns Host binding and business replies; native event interpretation stays in providers. */
33
+ export class NormalizedProviderAdapter {
34
+ options;
35
+ outputMode = 'normalized';
36
+ capabilities = createHostedProviderCapabilities({
37
+ imageInputTransport: { source: 'transport-derived', support: 'not-supported', reason: 'host-policy-blocked', delivery: [] },
38
+ progressUpdates: { support: 'supported' },
39
+ });
40
+ bindings = new Map();
41
+ bindingRecords = new Map();
42
+ relationPasses = new Map();
43
+ deferredRelations = new Map();
44
+ busy = new Set();
45
+ stores = new Map();
46
+ saving = Promise.resolve();
47
+ defaultAdmission = Promise.resolve();
48
+ disposed = false;
49
+ management;
50
+ constructor(options) {
51
+ this.options = options;
52
+ if (options.sessionManagement && options.provider.descriptor.providerId === 'codex' && 'listSessions' in options.provider && 'inspectSession' in options.provider) {
53
+ const configuration = options.sessionManagement;
54
+ this.management = new HostSessionManagement({ ...configuration, provider: options.provider,
55
+ resolveAgentId: options.resolveAgentId, serverUrl: options.serverUrl,
56
+ loadProtectedNativeIds: async () => {
57
+ const protectedIds = await configuration.loadProtectedNativeIds();
58
+ for (const binding of this.bindingRecords.values())
59
+ if (!this.hasIndependentOwner(binding))
60
+ protectedIds.add(binding.nativeId);
61
+ return protectedIds;
62
+ },
63
+ find: async (nativeId, channelId, generation) => {
64
+ const binding = [...this.bindingRecords.values()].find(row => this.hasIndependentOwner(row) && row.nativeId === nativeId && row.channelId === channelId && this.bindingCurrent(row));
65
+ if (!binding)
66
+ return undefined;
67
+ const info = await binding.session.runtimeInfo();
68
+ if (info.sessionId !== nativeId || info.providerId !== 'codex' || !info.cwd || await realpath(info.cwd) !== binding.cwd)
69
+ throw new Error('session_native_identity_changed');
70
+ const target = this.describeSession({ version: 1, action: 'describe_session', agent_id: binding.agentId, channel_id: channelId, binding_id: binding.bindingId, native_session_id: nativeId, connection_generation: generation }, { signal: new AbortController().signal });
71
+ return target ? { target, workspace_path: binding.cwd, persistence: info.persistence } : undefined;
72
+ },
73
+ register: (session, channelId, cwd, generation, current) => this.registerIndependentSession(session, channelId, cwd, generation, current), });
74
+ }
75
+ }
76
+ hasIndependentOwner(binding) {
77
+ for (let owner = binding; owner; owner = owner.source)
78
+ if (owner.independent)
79
+ return true;
80
+ return false;
81
+ }
82
+ get supportsDefaultOpening() { return !!this.options.prepareSession; }
83
+ get supportsSessionManagement() { return !!this.management || this.supportsDefaultOpening; }
84
+ async describeSessionManagement() {
85
+ const operations = await this.management?.operations() ?? [];
86
+ if (this.supportsDefaultOpening && !operations.includes('open_session'))
87
+ operations.push('open_session');
88
+ return { operations, ...(operations.includes('open_session') ? { open_targets: [...(this.management ? ['native'] : []), ...(this.supportsDefaultOpening ? ['channel_default'] : [])] } : {}) };
89
+ }
90
+ async manageSession(request, context) {
91
+ if (request.action === 'open_session' && 'target' in request && request.target === 'channel_default')
92
+ return this.openDefaultSession(request, context);
93
+ if (this.disposed || !this.management)
94
+ throw new Error('session_management_not_supported');
95
+ return request.action === 'discover_sessions' ? this.management.discover(request, context) : this.management.mutate(request, context);
96
+ }
97
+ async openDefaultSession(request, context) {
98
+ const base = { agent_id: request.agent_id, channel_id: request.channel_id, connection_generation: request.connection_generation, operation_id: request.operation_id };
99
+ if (!this.supportsDefaultOpening)
100
+ return { ...base, status: 'unsupported', reason: 'session_default_open_unsupported' };
101
+ const current = () => !this.disposed && request.agent_id === this.options.resolveAgentId() && !context.signal.aborted
102
+ && !context.connectionSignal?.aborted && context.isConnectionCurrent?.() !== false && Date.now() < request.deadline_at;
103
+ const assertCurrent = () => { if (!current())
104
+ throw new Error('stale_session_binding'); };
105
+ try {
106
+ return await this.withDefaultAdmission(async () => {
107
+ assertCurrent();
108
+ let binding = this.bindings.get(request.channel_id);
109
+ if (binding && (binding.routingKey !== request.channel_id || !binding.persistent || binding.independent || binding.source))
110
+ throw new Error('session_default_identity_changed');
111
+ if (binding?.observation.status === 'stopped') {
112
+ await this.closeBinding(binding);
113
+ assertCurrent();
114
+ binding = undefined;
115
+ }
116
+ if (!binding) {
117
+ const records = this.stores.get(request.agent_id) ?? await this.options.store.load(request.agent_id);
118
+ assertCurrent();
119
+ this.stores.set(request.agent_id, records);
120
+ if (!Object.hasOwn(records, request.channel_id))
121
+ throw new Error('session_default_not_found');
122
+ const promptContext = await this.options.prepareSession(request.channel_id);
123
+ assertCurrent();
124
+ binding = await this.bindingForDefault({ channelId: request.channel_id, promptContext }, { mode: 'resume-existing', assertCurrent });
125
+ }
126
+ assertCurrent();
127
+ const target = this.describeSession({ version: 1, action: 'describe_session', ...base, binding_id: binding.bindingId, native_session_id: binding.nativeId }, context);
128
+ if (!target)
129
+ throw new Error('stale_session_binding');
130
+ return { ...base, status: 'confirmed', target, workspace_path: binding.cwd };
131
+ });
132
+ }
133
+ catch (error) {
134
+ const reason = error instanceof AgentSessionInUseError ? 'session_in_use'
135
+ : error instanceof Error && ['stale_session_binding', 'session_default_not_found', 'session_default_identity_changed', 'session_default_invalid', 'session_owned_elsewhere'].includes(error.message)
136
+ ? error.message : 'session_default_unavailable';
137
+ return { ...base, status: 'rejected', reason };
138
+ }
139
+ }
140
+ async registerIndependentSession(session, channelId, cwd, generation, current) {
141
+ const agentId = this.options.resolveAgentId();
142
+ const info = await session.runtimeInfo();
143
+ if (!current() || !agentId || !info.sessionId || this.disposed || [...this.bindingRecords.values()].some(row => row.nativeId === info.sessionId && !row.closed))
144
+ throw new Error('session_owned_elsewhere');
145
+ const visibilityKey = createHash('sha256').update(JSON.stringify({ independent: true, server: this.options.serverUrl, agentId, channelId, nativeId: info.sessionId })).digest('hex');
146
+ const now = new Date().toISOString();
147
+ const timeline = new SessionTimeline({ providerId: info.providerId, nativeSessionId: info.sessionId, visibilityKey }, agentId, channelId);
148
+ const ownership = new InputOwnership(timeline);
149
+ const binding = { timeline, projection: new SessionObservationProjection(timeline, ownership, owner => binding.executions.get(owner)?.taskId),
150
+ bindingId: randomUUID(), routingKey: `independent:${info.sessionId}`, session, nativeId: info.sessionId, channelId, cwd, visibilityKey, agentId,
151
+ persistent: false, independent: true, retained: true, gate: new NormalizedObservationGate(), ownership, executions: new Map(), ready: deferred(),
152
+ observation: { status: 'recovering', revision: 1, reason: 'initializing' }, observationQueue: [], observationBytes: 0, pump: Promise.resolve(),
153
+ requests: new Map(), receipts: new Map(), closed: false, createdAt: now, updatedAt: now, status: info.status };
154
+ const { persistence: _persistence, ...runtimeInfo } = info;
155
+ binding.runtime = { type: 'observation', delivery: 'live', sourceKey: `runtime:${randomUUID()}`, occurredAt: Date.now(), event: { type: 'runtime_updated', provider: info.providerId, runtimeInfo } };
156
+ this.bindingRecords.set(binding.bindingId, binding);
157
+ binding.pump = this.observe(binding);
158
+ this.publishBinding(binding, false);
159
+ const target = this.describeSession({ version: 1, action: 'describe_session', agent_id: agentId, channel_id: channelId, binding_id: binding.bindingId, native_session_id: info.sessionId, connection_generation: generation }, { signal: new AbortController().signal });
160
+ return { target, workspace_path: cwd };
161
+ }
162
+ get sessionInputOperations() { return this.options.provider.descriptor.sessionInputOperations ?? []; }
163
+ get supportsSessionInput() { return this.sessionInputOperations.length > 0; }
164
+ sessionPublication;
165
+ setSessionPublication(publication) { this.sessionPublication = publication; }
166
+ publishSessionBindings() {
167
+ if (!this.sessionPublication?.supported())
168
+ return;
169
+ for (const binding of this.bindings.values())
170
+ this.publishBinding(binding, true);
171
+ for (const binding of this.bindingRecords.values())
172
+ if (this.bindings.get(binding.channelId) !== binding)
173
+ this.publishBinding(binding, false);
174
+ }
175
+ publishAvailability(binding) {
176
+ try {
177
+ return this.publishActivity(binding, { shape: 'normalized_session_availability', sessionControlBindingId: binding.bindingId,
178
+ nativeSessionId: binding.nativeId, ...binding.observation });
179
+ }
180
+ catch {
181
+ return false;
182
+ }
183
+ }
184
+ setAvailability(binding, status, reason, advance = false) {
185
+ const safeReason = reason && /^[a-z][a-z0-9_]{0,127}$/.test(reason) ? reason : undefined;
186
+ if (advance || binding.observation.status !== status || binding.observation.reason !== safeReason) {
187
+ binding.observation = { status, revision: binding.observation.revision + 1, ...(safeReason ? { reason: safeReason } : {}) };
188
+ }
189
+ if (status === 'stopped' && !binding.ready.fulfilled)
190
+ binding.ready.reject(new Error('Native Session observation is unavailable.'));
191
+ this.publishAvailability(binding);
192
+ }
193
+ visibleRuntime(binding, runtimeInfo) {
194
+ if (!runtimeInfo.childSessions)
195
+ return runtimeInfo;
196
+ return { ...runtimeInfo, childSessions: runtimeInfo.childSessions.filter(child => {
197
+ if (this.management?.canExpose(child.nativeSessionId, binding.agentId, binding.channelId) === false)
198
+ return false;
199
+ return ![...this.bindingRecords.values()].some(record => !record.closed && record.nativeId === child.nativeSessionId && record.channelId !== binding.channelId);
200
+ }) };
201
+ }
202
+ publishBinding(binding, select) {
203
+ if (!this.sessionPublication?.supported() || binding.closed || this.disposed || this.bindingRecords.get(binding.bindingId) !== binding || binding.agentId !== this.options.resolveAgentId() || binding.runtime?.event.type !== 'runtime_updated')
204
+ return false;
205
+ const availabilityPublished = this.publishAvailability(binding);
206
+ if (availabilityPublished)
207
+ binding.retained = true;
208
+ const runtimeInfo = this.visibleRuntime(binding, structuredClone(binding.runtime.event.runtimeInfo));
209
+ binding.runtime = { ...binding.runtime, event: { ...binding.runtime.event, runtimeInfo } };
210
+ if (runtimeInfo.sessionId !== binding.nativeId)
211
+ return false;
212
+ const capabilities = structuredClone(this.bindingCapabilities(binding));
213
+ if (select && this.bindings.get(binding.channelId) !== binding)
214
+ return false;
215
+ try {
216
+ const published = this.sessionPublication.publish(binding.channelId, { shape: 'normalized', protocolVersion: PROTOCOL_VERSION,
217
+ sessionControlBindingId: binding.bindingId, timelineEpoch: binding.timeline.epoch, ...(select ? { sessionControlDefault: true } : {}), sessionCapabilities: capabilities,
218
+ event: { type: 'runtime_updated', providerId: this.options.provider.descriptor.providerId, runtimeInfo } });
219
+ if (published)
220
+ binding.retained = true;
221
+ return published;
222
+ }
223
+ catch {
224
+ return false;
225
+ }
226
+ }
227
+ publishActivity(binding, activity) {
228
+ if (!this.bindingCurrent(binding))
229
+ return false;
230
+ if (binding.stateSink?.sessionActivity?.(activity))
231
+ return true;
232
+ return this.sessionPublication?.supported() ? this.sessionPublication.publish(binding.channelId, activity) : false;
233
+ }
234
+ publisher(binding) {
235
+ return binding.publisher ??= new NormalizedActivityPublisher({ channelId: binding.channelId, agentId: binding.agentId, get taskId() { return binding.publicationTaskId; },
236
+ client: { reportActivity: ({ activity }) => { this.publishActivity(binding, activity); } } });
237
+ }
238
+ publishPreTurnError(error, options, binding, current, publicMessage) {
239
+ const agentId = this.options.resolveAgentId();
240
+ if (!agentId || current && !current())
241
+ return;
242
+ const event = {
243
+ type: 'timeline',
244
+ provider: this.options.provider.descriptor.providerId,
245
+ item: { type: 'error', message: publicMessage ?? nativeSessionFailureMessage(error) },
246
+ };
247
+ const observation = {
248
+ type: 'observation',
249
+ delivery: 'live',
250
+ sourceKey: `session-error:${randomUUID()}`,
251
+ occurredAt: Date.now(),
252
+ event,
253
+ };
254
+ try {
255
+ if (binding && this.bindingCurrent(binding)) {
256
+ binding.timeline.append(observation);
257
+ this.publisher(binding).observeTimeline(observation, { scope: binding.timeline.scope, inputs: binding.timeline.inputs, agentId: binding.agentId, channelId: binding.channelId }, binding.bindingId, binding.timeline.epoch);
258
+ }
259
+ else
260
+ options?.onNormalizedObservation?.(event, agentId, observation);
261
+ }
262
+ catch {
263
+ // Error presentation cannot change native admission.
264
+ }
265
+ }
266
+ publishTimeline(binding) {
267
+ if (!this.bindingCurrent(binding) || !binding.ready.fulfilled || binding.observation.status !== 'available')
268
+ return false;
269
+ return publishSessionTimeline({ timeline: binding.timeline, publisher: this.publisher(binding), bindingId: binding.bindingId,
270
+ agentId: binding.agentId, channelId: binding.channelId, send: activity => this.publishActivity(binding, activity) });
271
+ }
272
+ listSessions(request, context) {
273
+ if (this.disposed || request.agent_id !== this.options.resolveAgentId() || context.signal.aborted || context.isConnectionCurrent?.() === false)
274
+ throw new Error('stale_session_binding');
275
+ const entries = [...this.bindingRecords.values()].filter(binding => this.controlBinding(request.channel_id, binding.bindingId) === binding)
276
+ .map(binding => ({ binding_id: binding.bindingId, native_session_id: binding.nativeId, is_default: this.bindings.get(request.channel_id) === binding }));
277
+ return createSessionDirectory(request, entries);
278
+ }
279
+ describeSession(request, context) {
280
+ if (request.agent_id !== this.options.resolveAgentId() || context.signal.aborted || context.isConnectionCurrent?.() === false)
281
+ return undefined;
282
+ const candidates = request.native_session_id !== undefined && request.binding_id === undefined
283
+ ? [...this.bindingRecords.values()].filter(record => record.nativeId === request.native_session_id && this.controlBinding(request.channel_id, record.bindingId) === record) : undefined;
284
+ if (candidates && candidates.length > 1)
285
+ throw new Error('session_ambiguous');
286
+ const binding = candidates ? candidates[0] : this.controlBinding(request.channel_id, request.binding_id);
287
+ if (!binding || request.native_session_id !== undefined && binding.nativeId !== request.native_session_id || request.agent_id !== binding.agentId || context.signal.aborted || context.isConnectionCurrent && !context.isConnectionCurrent())
288
+ return undefined;
289
+ const capabilities = this.bindingCapabilities(binding);
290
+ const input_operations = this.sessionInputOperations.filter(operation => operation === 'start_turn' ? capabilities.sendMessage : operation === 'steer' ? capabilities.steer : capabilities.queueMessage);
291
+ return { version: 1, agent_id: binding.agentId, channel_id: binding.channelId, binding_id: binding.bindingId, native_session_id: binding.nativeId,
292
+ connection_generation: request.connection_generation, capabilities, ...(input_operations.length ? { input_operations } : {}) };
293
+ }
294
+ async observeSession(request, context) {
295
+ const input = request.input;
296
+ const result = (status, reason) => ({ operation_id: input.operation_id, status, ...(reason ? { reason } : {}) });
297
+ const binding = this.controlBinding(input.channel_id, input.binding_id);
298
+ const current = () => !!binding && this.bindingCurrent(binding) && !(context.connectionSignal ?? context.signal).aborted && (context.isConnectionCurrent?.() ?? true)
299
+ && binding.agentId === input.agent_id && binding.bindingId === input.binding_id && binding.nativeId === input.native_session_id;
300
+ if (!binding || !current())
301
+ return result('rejected', 'stale_session_binding');
302
+ if (input.operation !== 'sync_timeline')
303
+ return result('unsupported');
304
+ if (binding.observation.status !== 'available') {
305
+ this.publishAvailability(binding);
306
+ return result('unconfirmed', `native_observation_${binding.observation.status}`);
307
+ }
308
+ const previous = binding.timeline.publication;
309
+ this.syncNormalizedActivity(input.channel_id, input.binding_id);
310
+ const published = binding.timeline.publication;
311
+ return current() && published && published !== previous ? { ...result('confirmed'), timeline_epoch: published.epoch, replacement_id: published.replacementId } : result('unconfirmed');
312
+ }
313
+ async generateReply(input, options) {
314
+ if (this.disposed)
315
+ throw new Error('The native provider is disposed.');
316
+ if (input.incomingParts?.some((part) => part.type === 'image')) {
317
+ throw new ProviderInputRejectedError('Image input requires providerOutputMode: legacy; no native message was submitted.');
318
+ }
319
+ const codex = this.options.provider.descriptor.providerId === 'codex';
320
+ const internalInput = input.collaboration?.turnMode === 'protocol-managed' || input.collaboration?.turnMode === 'silent-kickoff';
321
+ const unavailableBody = codex && !internalInput && (options?.normalizedInputText === null || options?.normalizedInputMessageId && options.normalizedInputText === undefined);
322
+ const nativeInput = codex ? (options?.normalizedInputText ?? input.incomingContent) : undefined;
323
+ const selected = input.selectedSession;
324
+ const selectedBinding = selected ? this.controlBinding(input.channelId, selected.bindingId) : undefined;
325
+ const selectedCurrent = () => !!selected && !!selectedBinding && this.bindingCurrent(selectedBinding)
326
+ && selectedBinding.agentId === selected.agentId && selectedBinding.bindingId === selected.bindingId
327
+ && selectedBinding.nativeId === selected.nativeSessionId && (selected.sourceCurrent?.() ?? true);
328
+ if (selected && !selectedCurrent())
329
+ throw new ProviderInputRejectedError('The selected Session is stale; no native message was submitted.');
330
+ if (selected) {
331
+ if (selectedBinding.status !== 'idle' || selectedBinding.inputSubmitting || selectedBinding.controlBusy || selectedBinding.active || selectedBinding.executions.size) {
332
+ const error = new ProviderInputRejectedError(SELECTED_SESSION_NOT_IDLE);
333
+ this.publishPreTurnError(error, options, selectedBinding, selectedCurrent, error.publicReplyText);
334
+ throw error;
335
+ }
336
+ selectedBinding.inputSubmitting = true;
337
+ }
338
+ else {
339
+ if (this.busy.has(input.channelId))
340
+ throw new Error('A native turn is already active in this channel.');
341
+ this.busy.add(input.channelId);
342
+ }
343
+ let binding;
344
+ let turn;
345
+ try {
346
+ if (unavailableBody)
347
+ throw new ProviderInputRejectedError('The original message body is unavailable; no native message was submitted.');
348
+ if (codex && !nativeInput?.trim())
349
+ throw new ProviderInputRejectedError('Add message text before sending attachments to this Session; no native message was submitted.');
350
+ const prepared = await this.options.prepare(selectedBinding && this.hasIndependentOwner(selectedBinding)
351
+ ? { ...input, independentSessionWorkspace: { bindingId: selectedBinding.bindingId, cwd: selectedBinding.cwd } } : input);
352
+ if (codex && prepared.contextPreparationFailed)
353
+ throw new ProviderInputRejectedError('Required turn context is unavailable; no native message was submitted.');
354
+ const additionalContext = codex ? buildCodexTurnContext({ ...input, promptContext: prepared.promptContext }) : undefined;
355
+ if (additionalContext) {
356
+ let deliveryContext = '';
357
+ if ((input.incomingMessageType === 'task_assignment' || input.incomingMessageType === 'task_heartbeat') && input.incomingContent !== nativeInput) {
358
+ // BPP task delivery prefixes the exact durable body with AgentPreamble and two newlines.
359
+ const suffix = `\n\n${nativeInput}`;
360
+ if (!input.incomingContent.endsWith(suffix))
361
+ throw new ProviderInputRejectedError('The task delivery context boundary is unavailable; no native message was submitted.');
362
+ deliveryContext = input.incomingContent.slice(0, -suffix.length);
363
+ }
364
+ additionalContext.borgee_delivery = { kind: 'untrusted', value: deliveryContext };
365
+ additionalContext.borgee_attachments = { kind: 'untrusted', value: buildHostedIncomingContentText(prepared.incomingParts.filter(part => part.type !== 'text')) };
366
+ }
367
+ if (prepared.promptContext?.resolvedWorkingFolder?.authority === 'task-thread-scratch') {
368
+ throw new ProviderInputRejectedError('This task has no authorized execution workspace. Select legacy mode for its discussion-only tool policy; no native message was submitted.');
369
+ }
370
+ binding = selectedBinding ?? await this.bindingFor(prepared);
371
+ await binding.ready.promise;
372
+ if (binding.observation.status !== 'available')
373
+ throw new ProviderInputRejectedError('Native Session observation is unavailable; no native message was submitted.');
374
+ if (selected && !selectedCurrent())
375
+ throw new ProviderInputRejectedError('The selected Session source expired; no native message was submitted.');
376
+ if (selected && binding.status !== 'idle')
377
+ throw new ProviderInputRejectedError('A native turn is already active in the selected Session.');
378
+ if (binding.closed || binding.controlBusy || binding.active || binding.executions.size)
379
+ throw new Error('A native turn is already active in this Session.');
380
+ const questionnaireResponse = selected?.questionnaireResponse;
381
+ if (questionnaireResponse) {
382
+ const questionnaire = binding.timeline.questionnaire(questionnaireResponse.messageId);
383
+ if (!questionnaire || questionnaire.questions.some(question => question.sensitive)) {
384
+ throw new ProviderInputRejectedError('The questionnaire is unavailable in the selected Session; no native message was submitted.');
385
+ }
386
+ try {
387
+ validateNativeInteractionResponse({ kind: 'question', requestId: questionnaire.messageId, questions: questionnaire.questions }, { kind: 'question', answers: questionnaireResponse.answers });
388
+ }
389
+ catch {
390
+ throw new ProviderInputRejectedError('The questionnaire answers are invalid; no native message was submitted.');
391
+ }
392
+ if (options?.normalizedInputText !== questionnaireAnswerText(questionnaire, questionnaireResponse.answers, binding.agentId).trim()) {
393
+ throw new ProviderInputRejectedError('The questionnaire answer body does not match its answers; no native message was submitted.');
394
+ }
395
+ }
396
+ turn = { ...(questionnaireResponse ? { questionnaireResponse: structuredClone(questionnaireResponse) } : {}), taskId: options?.normalizedTaskId ?? input.collaboration?.turnExecutionId ?? randomUUID(), result: deferred(), options, started: false, terminal: false, messages: new Map(), ...(selected ? { isCurrent: selectedCurrent } : {}) };
397
+ if (binding.legacyOwnership)
398
+ turn.legacyInput = { text: options?.normalizedInputText === undefined ? input.incomingContent : options.normalizedInputText, prompt: prepared.prompt };
399
+ binding.stateSink?.close();
400
+ binding.stateSink = options?.normalizedStateSink;
401
+ binding.active = turn;
402
+ turn.inputToken = randomUUID();
403
+ this.registerInput(binding, turn, turn.inputToken, options?.normalizedInputMessageId, turn.inputToken);
404
+ try {
405
+ if (binding.runtime)
406
+ binding.stateSink?.observe(binding.runtime, binding.agentId, structuredClone(this.bindingCapabilities(binding)), binding.bindingId, binding.timeline.epoch);
407
+ this.publishTimeline(binding);
408
+ }
409
+ catch { /* State synchronization cannot prevent native execution. */ }
410
+ recordProtocolTrace({ stage: 'provider.input', kind: 'sendMessage',
411
+ correlation: { channelId: binding.channelId, agentId: binding.agentId, taskId: binding.active?.taskId, sessionId: binding.nativeId },
412
+ payload: { prompt: codex ? { text: nativeInput, additionalContext } : prepared.prompt } });
413
+ if (selected && !selectedCurrent())
414
+ throw new ProviderInputRejectedError('The selected Session source expired; no native message was submitted.');
415
+ const receipt = await binding.session.sendMessage(nativeInput ?? prepared.prompt, {
416
+ ...(selected || codex ? { requireIdle: true } : {}), clientMessageId: turn.inputToken,
417
+ ...(additionalContext ? { additionalContext } : {}),
418
+ });
419
+ turn.turnId = receipt?.turnId;
420
+ await this.applyAssociation(binding, this.confirmInput(binding, turn, turn.inputToken, receipt));
421
+ if (selected && !selectedCurrent())
422
+ throw new ProviderInputRejectedError('The selected Session source expired after native admission.');
423
+ return await turn.result.promise;
424
+ }
425
+ catch (error) {
426
+ const failure = !turn?.started && error instanceof AgentInputRejectedError && error.reason === 'context_unavailable'
427
+ ? new ProviderInputRejectedError(error.message)
428
+ : selected && !turn?.started && error instanceof AgentInputRejectedError
429
+ ? new ProviderInputRejectedError(SELECTED_SESSION_NOT_IDLE)
430
+ : error;
431
+ if (!turn?.started) {
432
+ this.publishPreTurnError(failure, options, selected ? selectedBinding : undefined, turn?.isCurrent ?? (selected ? selectedCurrent : undefined), failure instanceof ProviderInputRejectedError ? failure.publicReplyText : undefined);
433
+ }
434
+ if (binding && turn?.started && !turn.terminal)
435
+ this.observeEvent(binding, { type: 'turn_failed', provider: this.options.provider.descriptor.providerId, turnId: turn.turnId, error: error instanceof Error ? error.message : 'Native turn failed.' });
436
+ if (binding && turn?.inputToken && error instanceof AgentInputRejectedError)
437
+ binding.ownership.rejectInput(turn.inputToken);
438
+ turn?.result.reject(failure);
439
+ throw failure;
440
+ }
441
+ finally {
442
+ if (binding && turn)
443
+ this.releaseExecution(binding, turn);
444
+ if (binding && binding.active === turn) {
445
+ binding.active = undefined;
446
+ }
447
+ if (selectedBinding)
448
+ selectedBinding.inputSubmitting = false;
449
+ else
450
+ this.busy.delete(input.channelId);
451
+ if (binding && !binding.retained && !binding.persistent && !binding.executions.size)
452
+ await this.closeBinding(binding);
453
+ }
454
+ }
455
+ bindingCurrent(binding) {
456
+ return !this.disposed && !binding.closed && binding.agentId === this.options.resolveAgentId()
457
+ && (binding.retained || this.sessionPublication?.supported() ? this.bindingRecords.get(binding.bindingId) === binding : this.bindings.get(binding.channelId) === binding);
458
+ }
459
+ controlBinding(channelId, bindingId) {
460
+ const retained = bindingId ? this.bindingRecords.get(bindingId) : undefined;
461
+ const binding = bindingId && (retained?.retained || this.sessionPublication?.supported()) ? retained : this.bindings.get(channelId);
462
+ return binding && binding.channelId === channelId && (!bindingId || binding.bindingId === bindingId) && this.bindingCurrent(binding) ? binding : undefined;
463
+ }
464
+ async controlSession(request, context) {
465
+ const respond = (status, reason) => ({ response: sessionControlResponse(request, status, reason) });
466
+ const binding = this.controlBinding(request.channel_id, request.binding_id);
467
+ const isCurrent = () => !!binding && this.bindingCurrent(binding) && (context.isConnectionCurrent?.() ?? !context.connectionSignal?.aborted);
468
+ const revision = binding?.observation.revision;
469
+ const current = () => isCurrent() && binding?.observation.status === 'available' && binding.observation.revision === revision && !context.signal.aborted && Date.now() < request.deadline_at;
470
+ if (!binding || binding.bindingId !== request.binding_id || binding.nativeId !== request.native_session_id || binding.agentId !== request.agent_id)
471
+ return respond('rejected', 'stale_session_binding');
472
+ if (!isCurrent() || context.signal.aborted || Date.now() >= request.deadline_at)
473
+ return respond('unconfirmed', 'session_control_expired');
474
+ if (binding.observation.status !== 'available')
475
+ return respond('rejected', `native_observation_${binding.observation.status}`);
476
+ if (request.operation.kind === 'send_input')
477
+ return this.sendInput(binding, request, context);
478
+ if (request.operation.kind === 'list_pending_inputs') {
479
+ if (!this.supportsSessionInput || !this.bindingCapabilities(binding).queueMessage || !binding.session.listPendingInputs)
480
+ return respond('unsupported', 'native_queue_inventory_not_supported');
481
+ try {
482
+ const pending = await binding.session.listPendingInputs();
483
+ if (!current() || context.isConnectionCurrent && !context.isConnectionCurrent())
484
+ return respond('unconfirmed', 'session_control_expired');
485
+ return { response: { ...sessionControlResponse(request, 'confirmed'), result: { pending_native_message_ids: pending } } };
486
+ }
487
+ catch {
488
+ return respond('unconfirmed', 'native_queue_inventory_unavailable');
489
+ }
490
+ }
491
+ if (request.operation.kind === 'list_commands') {
492
+ if (!this.bindingCapabilities(binding).commands || !binding.session.listCommands)
493
+ return respond('unsupported', 'commands_not_supported');
494
+ try {
495
+ const commands = await binding.session.listCommands();
496
+ if (!current())
497
+ return respond('unconfirmed', 'session_control_expired');
498
+ return { response: { ...sessionControlResponse(request, 'confirmed'), result: { commands } } };
499
+ }
500
+ catch {
501
+ return respond('unconfirmed', 'command_directory_unavailable');
502
+ }
503
+ }
504
+ if (binding.controlBusy || this.bindings.get(binding.channelId) === binding && this.busy.has(binding.channelId) || binding.active || binding.executions.size || binding.requests.size)
505
+ return respond('rejected', 'session_busy');
506
+ binding.controlBusy = true;
507
+ let turn;
508
+ let invoked = false;
509
+ let completion;
510
+ const release = () => {
511
+ if (turn)
512
+ this.releaseExecution(binding, turn);
513
+ if (turn && binding.active === turn)
514
+ binding.active = undefined;
515
+ binding.controlBusy = false;
516
+ };
517
+ try {
518
+ const info = await binding.session.runtimeInfo();
519
+ if (!current())
520
+ return respond('unconfirmed', 'session_control_expired');
521
+ if (info.status !== 'idle')
522
+ return respond('rejected', 'session_busy');
523
+ const operation = request.operation;
524
+ if (operation.kind === 'set_setting') {
525
+ if (!this.bindingCapabilities(binding).sessionSettings || !binding.session.setSessionSetting)
526
+ return respond('unsupported', 'settings_not_supported');
527
+ const setting = info.settings?.find(value => value.id === operation.setting_id);
528
+ if (!setting?.mutable || setting.category === 'permissions' || !setting.options.some(option => option.value === operation.value))
529
+ return respond('rejected', 'setting_not_allowed');
530
+ invoked = true;
531
+ await binding.session.setSessionSetting(operation.setting_id, operation.value);
532
+ }
533
+ else if (operation.kind === 'set_planning') {
534
+ if (!this.bindingCapabilities(binding).planning || !binding.session.setPlanning)
535
+ return respond('unsupported', 'planning_not_supported');
536
+ invoked = true;
537
+ await binding.session.setPlanning(operation.active);
538
+ }
539
+ else {
540
+ if (!this.bindingCapabilities(binding).commands || !binding.session.executeCommand || !request.admission || !context.execution && !context.activateExecution)
541
+ return respond('unsupported', 'command_execution_not_supported');
542
+ const execution = context.execution ?? await context.activateExecution(isCurrent);
543
+ turn = { taskId: request.admission.input_message_id, result: deferred(), options: execution, started: false, terminal: false, messages: new Map(), commandSettled: false, isCurrent };
544
+ if (binding.legacyOwnership)
545
+ turn.legacyInput = { text: execution.normalizedInputText ?? null, prompt: '' };
546
+ binding.stateSink?.close();
547
+ binding.stateSink = execution.normalizedStateSink;
548
+ binding.active = turn;
549
+ turn.inputToken = randomUUID();
550
+ this.registerInput(binding, turn, turn.inputToken, request.admission.input_message_id);
551
+ // Legacy native commands claim their first observed turn before the command receipt.
552
+ binding.legacyOwnership?.settle(turn, undefined);
553
+ completion = turn.result.promise.then(reply => reply.text ? reply : undefined).finally(release);
554
+ void completion.catch(() => { });
555
+ context.onCompletion?.(completion);
556
+ invoked = true;
557
+ const result = await binding.session.executeCommand(operation.command_id, operation.args);
558
+ turn.commandSettled = true;
559
+ turn.expectsNativeTurn = result.execution === 'turn';
560
+ turn.commandTurnId = result.turnId;
561
+ if (result.turnId && turn.turnId && turn.turnId !== result.turnId) {
562
+ const error = new Error('The native command returned a different turn from the observed command execution.');
563
+ turn.terminal = true;
564
+ turn.result.reject(error);
565
+ throw error;
566
+ }
567
+ if (result.turnId && !turn.turnId)
568
+ turn.turnId = result.turnId;
569
+ const receipt = result.messageId ? { messageId: result.messageId, turnId: result.turnId } : result.turnId ? { turnId: result.turnId } : undefined;
570
+ await this.applyAssociation(binding, this.confirmInput(binding, turn, turn.inputToken, receipt));
571
+ await this.finishLocalCommand(binding, turn);
572
+ const response = sessionControlResponse(request, current() ? 'confirmed' : 'unconfirmed', current() ? undefined : 'session_control_expired');
573
+ if (response.status === 'confirmed' && result.text !== undefined)
574
+ response.result = { text: result.text };
575
+ return { response, completion };
576
+ }
577
+ const confirmed = await binding.session.runtimeInfo();
578
+ if (!current())
579
+ return respond('unconfirmed', 'session_control_expired');
580
+ if (operation.kind === 'set_setting' ? confirmed.settings?.find(setting => setting.id === operation.setting_id)?.value !== operation.value : confirmed.planning?.active !== operation.active)
581
+ return respond('unconfirmed', 'native_confirmation_missing');
582
+ await this.persistConfirmedBinding(binding, confirmed, current);
583
+ if (!current())
584
+ return respond('unconfirmed', 'session_control_expired');
585
+ this.syncNormalizedActivity(binding.channelId, binding.bindingId);
586
+ return respond('confirmed');
587
+ }
588
+ catch (error) {
589
+ if (error instanceof AgentCommandRejectedError && turn && !turn.started && !binding.requests.size) {
590
+ turn.terminal = true;
591
+ turn.result.resolve({ text: '' });
592
+ return { ...respond('rejected', 'native_command_rejected'), completion };
593
+ }
594
+ return { ...respond(invoked ? 'unconfirmed' : 'rejected', invoked ? 'native_confirmation_unavailable' : 'session_control_unavailable'), ...(completion ? { completion } : {}) };
595
+ }
596
+ finally {
597
+ if (!completion)
598
+ release();
599
+ }
600
+ }
601
+ registerInput(binding, turn, key, clientMessageId, issuedToken, expectedTurnId) {
602
+ turn.ownerId ??= randomUUID();
603
+ if (binding.legacyOwnership) {
604
+ binding.legacyOwnership.register(turn, turn.legacyInput && { ...turn.legacyInput, clientMessageId });
605
+ binding.executions.set(turn.ownerId, turn);
606
+ return;
607
+ }
608
+ const result = binding.ownership.register({ key, owner: turn.ownerId, clientMessageId, taskId: turn.taskId, issuedToken, expectedTurnId, questionnaireResponse: turn.questionnaireResponse });
609
+ if (result.status !== 'changed' && result.status !== 'unchanged')
610
+ throw new Error(`Native input admission ${result.status}.`);
611
+ binding.executions.set(turn.ownerId, turn);
612
+ }
613
+ confirmInput(binding, turn, key, receipt) {
614
+ if (!binding.legacyOwnership)
615
+ return binding.ownership.confirm(key, receipt);
616
+ return { status: 'unchanged', correlations: [], unavailableOwners: [], owned: binding.legacyOwnership.settle(turn, receipt).map(value => ({ owner: value.owner.ownerId, observation: value.observation })) };
617
+ }
618
+ releaseExecution(binding, turn) {
619
+ binding.legacyOwnership?.remove(turn);
620
+ if (turn.ownerId) {
621
+ binding.ownership.releaseOwner(turn.ownerId);
622
+ binding.executions.delete(turn.ownerId);
623
+ }
624
+ }
625
+ reconcileAssociationMetadata(binding, result) {
626
+ for (const owner of result.unavailableOwners)
627
+ binding.executions.get(owner)?.result.reject(new Error('Native input execution evidence unavailable.'));
628
+ return result.correlations.some(value => value.status === 'changed');
629
+ }
630
+ async applyAssociation(binding, result) {
631
+ const revision = binding.observation.revision;
632
+ let changed = this.reconcileAssociationMetadata(binding, result);
633
+ for (const owned of result.owned) {
634
+ const turn = binding.executions.get(owned.owner);
635
+ if (!turn)
636
+ continue;
637
+ changed = binding.projection.attributeOwned(owned).changed || changed;
638
+ await this.observeOwned(binding, turn, owned.observation);
639
+ if (binding.observation.revision !== revision || !this.bindingCurrent(binding))
640
+ return;
641
+ }
642
+ if (changed)
643
+ this.publishTimeline(binding);
644
+ }
645
+ observeOwned(binding, turn, observation) {
646
+ const revision = binding.observation.revision;
647
+ const work = (binding.ownedObservationWork ?? Promise.resolve()).then(() => this.processOwnedObservation(binding, turn, observation, revision));
648
+ binding.ownedObservationWork = work;
649
+ return work;
650
+ }
651
+ async processOwnedObservation(binding, turn, observation, revision) {
652
+ if (binding.observation.revision !== revision || !this.bindingCurrent(binding))
653
+ return;
654
+ if (turn.terminal)
655
+ return;
656
+ if (turn.isCurrent && !turn.isCurrent()) {
657
+ turn.result.reject(new ProviderTurnCancelledError('Native input connection ownership ended.'));
658
+ return;
659
+ }
660
+ if (binding.active !== turn) {
661
+ if (binding.active && !binding.active.terminal)
662
+ throw new Error('Native input observations overlap distinct owned turns.');
663
+ binding.active = turn;
664
+ }
665
+ if (observation.event.type === 'turn_started' && turn.activate) {
666
+ try {
667
+ turn.options = await turn.activate();
668
+ }
669
+ catch (error) {
670
+ turn.terminal = true;
671
+ turn.result.reject(error);
672
+ if (binding.active === turn)
673
+ binding.active = undefined;
674
+ return;
675
+ }
676
+ turn.activate = undefined;
677
+ if (binding.observation.revision !== revision || !this.bindingCurrent(binding)) {
678
+ turn.options.normalizedStateSink?.close();
679
+ return;
680
+ }
681
+ if (turn.isCurrent && !turn.isCurrent()) {
682
+ turn.options.normalizedStateSink?.close();
683
+ const error = new ProviderTurnCancelledError('Native input connection ownership ended during execution admission.');
684
+ turn.terminal = true;
685
+ turn.result.reject(error);
686
+ if (binding.active === turn)
687
+ binding.active = undefined;
688
+ return;
689
+ }
690
+ binding.stateSink?.close();
691
+ binding.stateSink = turn.options.normalizedStateSink;
692
+ }
693
+ if (binding.legacyOwnership && observation.event.type === 'timeline' && observation.event.item.type === 'user_message') {
694
+ observation = binding.legacyOwnership.publicObservation(observation, turn);
695
+ binding.timeline.append(observation);
696
+ const event = observation.event;
697
+ if (event.type === 'timeline' && event.item.type === 'user_message' && event.item.messageId && event.item.clientMessageId) {
698
+ binding.timeline.inputs.accept(binding.timeline.scope, event.item.messageId, { clientMessageId: event.item.clientMessageId, taskId: turn.taskId });
699
+ }
700
+ if (turn.legacyInput?.text === null)
701
+ return;
702
+ this.observeSessionState(binding, observation, turn.taskId);
703
+ }
704
+ this.observeEvent(binding, observation.event, observation);
705
+ }
706
+ async sendInput(binding, request, context) {
707
+ const operation = request.operation;
708
+ const respond = (status, reason) => ({ response: sessionControlResponse(request, status, reason) });
709
+ if (operation.kind !== 'send_input' || !request.admission || !this.supportsSessionInput)
710
+ return respond('unsupported', 'native_input_not_supported');
711
+ if (!this.sessionInputOperations.includes(operation.delivery))
712
+ return respond('unsupported', 'native_input_delivery_not_supported');
713
+ if (binding.inputSubmitting)
714
+ return respond('rejected', 'native_input_submission_busy');
715
+ const isCurrent = () => this.bindingCurrent(binding) && (context.isConnectionCurrent?.() ?? !context.signal.aborted);
716
+ const revision = binding?.observation.revision;
717
+ const current = () => isCurrent() && binding?.observation.status === 'available' && binding.observation.revision === revision && !context.signal.aborted && Date.now() < request.deadline_at;
718
+ if (!current())
719
+ return respond('unconfirmed', 'session_control_expired');
720
+ const active = binding.active;
721
+ if (operation.delivery === 'steer' && (!this.bindingCapabilities(binding).steer || !binding.session.steer))
722
+ return respond('unsupported', 'native_steering_not_supported');
723
+ if (operation.delivery === 'steer' && (!active?.started || active.terminal || active.turnId !== operation.expected_turn_id))
724
+ return respond('rejected', 'native_turn_changed');
725
+ if (operation.delivery === 'next_turn' && (!this.bindingCapabilities(binding).queueMessage || !context.activateExecution))
726
+ return respond('unsupported', 'native_queue_not_supported');
727
+ binding.inputSubmitting = true;
728
+ let queued;
729
+ let completion;
730
+ let invoked = false;
731
+ const inputKey = randomUUID();
732
+ try {
733
+ if (operation.delivery === 'next_turn') {
734
+ queued = { taskId: request.admission.input_message_id, started: false, terminal: false, messages: new Map(), result: deferred(), isCurrent, activate: () => context.activateExecution(isCurrent) };
735
+ if (binding.legacyOwnership)
736
+ queued.legacyInput = { text: operation.text, prompt: operation.text };
737
+ queued.inputToken = inputKey;
738
+ this.registerInput(binding, queued, inputKey, request.admission.input_message_id, inputKey);
739
+ const owned = queued;
740
+ const disconnected = () => owned.result.reject(new ProviderTurnCancelledError('Native input connection ownership ended.'));
741
+ context.connectionSignal?.addEventListener('abort', disconnected, { once: true });
742
+ completion = owned.result.promise.then(reply => reply.text ? reply : undefined).finally(() => {
743
+ context.connectionSignal?.removeEventListener('abort', disconnected);
744
+ this.releaseExecution(binding, owned);
745
+ if (binding.active === owned)
746
+ binding.active = undefined;
747
+ });
748
+ void completion.catch(() => { });
749
+ context.onCompletion?.(completion);
750
+ if (context.connectionSignal?.aborted || !isCurrent()) {
751
+ disconnected();
752
+ return { ...respond('unconfirmed', 'session_control_expired'), completion };
753
+ }
754
+ }
755
+ else {
756
+ this.registerInput(binding, active, inputKey, request.admission.input_message_id, inputKey, operation.expected_turn_id);
757
+ }
758
+ invoked = true;
759
+ const receipt = operation.delivery === 'next_turn'
760
+ ? await binding.session.sendMessage(operation.text, { delivery: 'next_turn', clientMessageId: inputKey })
761
+ : await binding.session.steer(operation.text, { expectedTurnId: operation.expected_turn_id, clientMessageId: inputKey });
762
+ if (!receipt || !receipt.messageId && !receipt.turnId)
763
+ return { ...respond('unconfirmed', 'native_input_identity_missing'), completion };
764
+ if (queued) {
765
+ queued.turnId = receipt.turnId;
766
+ }
767
+ const association = this.confirmInput(binding, queued ?? active, inputKey, receipt);
768
+ await this.applyAssociation(binding, association);
769
+ const turnId = binding.ownership.turnId((queued ?? active).ownerId);
770
+ if (association.status === 'conflict' || operation.delivery === 'steer' && turnId !== operation.expected_turn_id)
771
+ return { ...respond('unconfirmed', 'native_turn_changed'), completion };
772
+ if (!current())
773
+ return { ...respond('unconfirmed', 'session_control_expired'), completion };
774
+ return { response: { ...sessionControlResponse(request, 'confirmed'), result: { input_receipt: { delivery: operation.delivery, ...(receipt.messageId ? { native_message_id: receipt.messageId } : {}), ...(turnId ? { turn_id: turnId } : {}) } } }, completion };
775
+ }
776
+ catch (error) {
777
+ if (error instanceof AgentInputRejectedError || !invoked)
778
+ binding.ownership.rejectInput(inputKey);
779
+ if (error instanceof AgentInputRejectedError && queued) {
780
+ queued.terminal = true;
781
+ queued.result.resolve({ text: '' });
782
+ }
783
+ return { ...respond(error instanceof AgentInputRejectedError || !invoked ? 'rejected' : 'unconfirmed', error instanceof AgentInputRejectedError ? 'native_input_rejected' : error instanceof Error && error.message === 'Native input admission capacity.' ? 'native_input_capacity' : 'native_confirmation_unavailable'), completion };
784
+ }
785
+ finally {
786
+ binding.inputSubmitting = false;
787
+ }
788
+ }
789
+ async finishLocalCommand(binding, turn) {
790
+ const current = () => this.bindingCurrent(binding) && (turn.isCurrent?.() ?? true) && binding.active === turn && !turn.terminal;
791
+ if (!current() || !turn.commandSettled || turn.commandFinishing || turn.started || turn.expectsNativeTurn || binding.requests.size)
792
+ return;
793
+ turn.commandFinishing = true;
794
+ try {
795
+ const info = await binding.session.runtimeInfo();
796
+ if (!current() || turn.started || binding.requests.size || info.status !== 'idle')
797
+ return;
798
+ await this.persistConfirmedBinding(binding, info, current);
799
+ if (!current() || turn.started || binding.requests.size)
800
+ return;
801
+ turn.terminal = true;
802
+ turn.result.resolve({ text: '' });
803
+ this.syncNormalizedActivity(binding.channelId, binding.bindingId);
804
+ }
805
+ catch {
806
+ // An unconfirmed local command retains ownership until native state is known.
807
+ }
808
+ finally {
809
+ turn.commandFinishing = false;
810
+ }
811
+ }
812
+ async persistConfirmedBinding(binding, info, current, selecting = false) {
813
+ if (!binding.persistent)
814
+ return;
815
+ if (!info.persistence || info.persistence.sessionId !== binding.nativeId || info.persistence.providerId !== this.options.provider.descriptor.providerId)
816
+ throw new Error('Native session persistence identity changed.');
817
+ const record = JSON.stringify({ handle: info.persistence, cwd: binding.cwd, visibilityKey: binding.visibilityKey, additionalDirectories: binding.additionalDirectories });
818
+ this.saving = this.saving.catch(() => { }).then(async () => {
819
+ if (!current())
820
+ throw new Error('Native session confirmation expired before persistence.');
821
+ if (!selecting && this.bindings.get(binding.channelId) !== binding)
822
+ return;
823
+ const records = this.stores.get(binding.agentId);
824
+ const next = { ...records, [binding.routingKey]: record };
825
+ await this.options.store.save(binding.agentId, next);
826
+ if (current())
827
+ records[binding.routingKey] = record;
828
+ });
829
+ await this.saving;
830
+ }
831
+ withDefaultAdmission(work) {
832
+ if (this.management)
833
+ return this.management.withAdmission(work);
834
+ if (!this.supportsDefaultOpening)
835
+ return work();
836
+ const result = this.defaultAdmission.catch(() => { }).then(work);
837
+ this.defaultAdmission = result;
838
+ return result;
839
+ }
840
+ bindingFor(turn) {
841
+ return this.withDefaultAdmission(() => this.bindingForDefault(turn));
842
+ }
843
+ async bindingForDefault(turn, admission) {
844
+ const agentId = this.options.resolveAgentId();
845
+ const resumeOnly = admission?.mode === 'resume-existing';
846
+ if (!agentId)
847
+ throw new Error('A verified Borgee agent identity is required for native sessions.');
848
+ const assertCurrent = () => {
849
+ if (this.disposed || agentId !== this.options.resolveAgentId())
850
+ throw new Error('stale_session_binding');
851
+ admission?.assertCurrent();
852
+ };
853
+ assertCurrent();
854
+ const cwd = await realpath(turn.promptContext?.resolvedWorkingFolder?.rootPath ?? process.cwd());
855
+ assertCurrent();
856
+ const additionalDirectories = this.options.provider.descriptor.providerId === 'claude'
857
+ ? resolveAdditionalDirectories(turn) : undefined;
858
+ const routingKey = turn.providerSessionRouting?.key ?? turn.channelId;
859
+ const persistent = turn.providerSessionRouting?.persistence !== 'ephemeral';
860
+ const visibilityKey = createHash('sha256').update(JSON.stringify({
861
+ agentId, server: this.options.serverUrl, channelId: turn.channelId, routingKey,
862
+ context: turn.promptContext?.channelContextPayloadPath ?? null,
863
+ credentials: turn.promptContext?.gatewayCredentialPath ?? null,
864
+ brief: turn.promptContext?.claudeProjectedBriefPath ?? null,
865
+ })).digest('hex');
866
+ const existing = [...this.bindingRecords.values()].find(binding => !binding.source && !binding.closed && binding.agentId === agentId
867
+ && binding.channelId === turn.channelId && binding.cwd === cwd && binding.visibilityKey === visibilityKey && binding.persistent === persistent
868
+ && JSON.stringify(binding.additionalDirectories) === JSON.stringify(additionalDirectories));
869
+ if (existing) {
870
+ const previous = this.bindings.get(turn.channelId);
871
+ if (previous !== existing && existing.persistent) {
872
+ await this.persistConfirmedBinding(existing, await existing.session.runtimeInfo(), () => !this.disposed && !existing.closed
873
+ && this.bindings.get(turn.channelId) === previous && this.bindingRecords.get(existing.bindingId) === existing, true);
874
+ }
875
+ assertCurrent();
876
+ await this.selectBinding(existing);
877
+ assertCurrent();
878
+ return existing;
879
+ }
880
+ const records = this.stores.get(agentId) ?? await this.options.store.load(agentId);
881
+ assertCurrent();
882
+ this.stores.set(agentId, records);
883
+ let stored;
884
+ if (persistent && records[routingKey]) {
885
+ try {
886
+ stored = JSON.parse(records[routingKey]);
887
+ }
888
+ catch {
889
+ throw new Error(resumeOnly ? 'session_default_invalid' : 'Invalid native session binding.');
890
+ }
891
+ }
892
+ if (resumeOnly) {
893
+ if (!stored)
894
+ throw new Error('session_default_invalid');
895
+ if (!stored.handle || stored.handle.providerId !== this.options.provider.descriptor.providerId || typeof stored.handle.sessionId !== 'string'
896
+ || !stored.handle.sessionId || typeof stored.handle.opaque !== 'string' || typeof stored.cwd !== 'string' || typeof stored.visibilityKey !== 'string')
897
+ throw new Error('session_default_invalid');
898
+ if (stored.cwd !== cwd || stored.visibilityKey !== visibilityKey)
899
+ throw new Error('session_default_identity_changed');
900
+ }
901
+ const handle = stored?.cwd === cwd && stored.visibilityKey === visibilityKey ? stored.handle : undefined;
902
+ if (handle) {
903
+ await this.management?.assertUnclaimed(handle.sessionId);
904
+ assertCurrent();
905
+ const loaded = [...this.bindingRecords.values()].filter(binding => binding.nativeId === handle.sessionId);
906
+ if (loaded.some(binding => !binding.closed))
907
+ throw new Error('Native session is already loaded with different Host configuration.');
908
+ for (const binding of loaded) {
909
+ await this.closeBinding(binding);
910
+ assertCurrent();
911
+ }
912
+ }
913
+ assertCurrent();
914
+ const session = handle
915
+ ? await this.options.provider.resumeSession(handle, ...(additionalDirectories === undefined ? [] : [{ additionalDirectories }]))
916
+ : await this.options.provider.createSession({ sessionId: routingKey, cwd, systemPrompt: turn.claudeSessionPromptAppend,
917
+ ...(additionalDirectories === undefined ? {} : { additionalDirectories }) });
918
+ if ([...this.bindingRecords.values()].some(binding => binding.session === session))
919
+ throw new Error('Native session is already owned by another Host binding.');
920
+ let createdBinding;
921
+ try {
922
+ assertCurrent();
923
+ await this.options.configureSession(session);
924
+ assertCurrent();
925
+ const info = await session.runtimeInfo();
926
+ assertCurrent();
927
+ if (resumeOnly && info.providerId !== this.options.provider.descriptor.providerId)
928
+ throw new Error('session_default_identity_changed');
929
+ if (!info.sessionId)
930
+ throw new Error('Native provider did not return a session identity.');
931
+ await this.management?.assertUnclaimed(info.sessionId);
932
+ assertCurrent();
933
+ if (this.management && [...this.bindingRecords.values()].some(binding => binding.nativeId === info.sessionId && !binding.closed))
934
+ throw new Error('Native session is already owned by another Host binding.');
935
+ if (handle && info.sessionId !== handle.sessionId)
936
+ throw new Error(resumeOnly ? 'session_default_identity_changed' : 'Resumed native identity differs from the stored Host binding.');
937
+ if (info.cwd && await realpath(info.cwd) !== cwd)
938
+ throw new Error(resumeOnly ? 'session_default_identity_changed' : 'Native session working directory differs from the authorized Host binding.');
939
+ assertCurrent();
940
+ if (persistent) {
941
+ if (!info.persistence || info.persistence.sessionId !== info.sessionId || info.persistence.providerId !== this.options.provider.descriptor.providerId)
942
+ throw new Error(resumeOnly ? 'session_default_identity_changed' : 'Native provider did not return a matching persistence handle.');
943
+ if (!resumeOnly) {
944
+ records[routingKey] = JSON.stringify({ handle: info.persistence, cwd, visibilityKey, additionalDirectories });
945
+ this.saving = this.saving.catch(() => { }).then(() => this.options.store.save(agentId, { ...records }));
946
+ await this.saving;
947
+ assertCurrent();
948
+ }
949
+ }
950
+ const now = new Date().toISOString();
951
+ const timeline = new SessionTimeline({ providerId: this.options.provider.descriptor.providerId, nativeSessionId: info.sessionId, visibilityKey }, agentId, turn.channelId);
952
+ const ownership = new InputOwnership(timeline);
953
+ const binding = { timeline, projection: new SessionObservationProjection(timeline, ownership, owner => binding.executions.get(owner)?.taskId), bindingId: randomUUID(), routingKey, session, nativeId: info.sessionId, channelId: turn.channelId, cwd, visibilityKey, additionalDirectories, agentId, persistent, retained: false, gate: new NormalizedObservationGate(), ownership, executions: new Map(), ready: deferred(), observation: { status: 'recovering', revision: 1, reason: 'initializing' }, observationQueue: [], observationBytes: 0, pump: Promise.resolve(), requests: new Map(), receipts: new Map(), closed: false, createdAt: now, updatedAt: now, status: info.status };
954
+ const { persistence: _persistence, ...runtimeInfo } = info;
955
+ binding.runtime = { type: 'observation', delivery: 'live', sourceKey: `runtime:${randomUUID()}`, occurredAt: Date.now(), event: { type: 'runtime_updated', provider: this.options.provider.descriptor.providerId, runtimeInfo } };
956
+ assertCurrent();
957
+ createdBinding = binding;
958
+ this.bindingRecords.set(binding.bindingId, binding);
959
+ if (this.options.provider.descriptor.providerId !== 'codex')
960
+ binding.legacyOwnership = new LegacyInputOwnership(true);
961
+ binding.pump = this.observe(binding);
962
+ await this.selectBinding(binding);
963
+ assertCurrent();
964
+ return binding;
965
+ }
966
+ catch (error) {
967
+ if (createdBinding)
968
+ await this.closeBinding(createdBinding);
969
+ else
970
+ await session.dispose();
971
+ throw error;
972
+ }
973
+ }
974
+ admitChildren(source, relationsChanged = false) {
975
+ if (!this.sessionPublication?.supported() || !this.options.provider.openChildSession || !this.bindingCurrent(source))
976
+ return;
977
+ const running = this.relationPasses.get(source.channelId);
978
+ if (running) {
979
+ if (!running.inspected.has(source.nativeId) && !running.queue.some(binding => binding.nativeId === source.nativeId))
980
+ running.queue.push(source);
981
+ else if (relationsChanged)
982
+ void running.promise.then(() => this.admitChildren(source));
983
+ return;
984
+ }
985
+ const queue = [...(this.deferredRelations.get(source.channelId) ?? []), source];
986
+ const inspected = new Set();
987
+ const expanded = new Set();
988
+ const retry = [];
989
+ const pass = Promise.resolve().then(async () => {
990
+ while (queue.length) {
991
+ const binding = queue.shift();
992
+ if (!this.bindingCurrent(binding) || expanded.has(binding.nativeId))
993
+ continue;
994
+ if (!inspected.has(binding.nativeId)) {
995
+ if (inspected.size >= 64) {
996
+ queue.unshift(binding);
997
+ break;
998
+ }
999
+ inspected.add(binding.nativeId);
1000
+ try {
1001
+ const info = await binding.session.runtimeInfo();
1002
+ if (!this.bindingCurrent(binding) || info.sessionId !== binding.nativeId || info.providerId !== this.options.provider.descriptor.providerId)
1003
+ continue;
1004
+ const previous = binding.runtime?.event.type === 'runtime_updated' ? binding.runtime.event.runtimeInfo.childSessions ?? [] : [];
1005
+ const children = new Map(previous.map(child => [child.nativeSessionId, child]));
1006
+ for (const child of info.childSessions ?? [])
1007
+ children.set(child.nativeSessionId, child);
1008
+ const { persistence: _persistence, ...runtimeInfo } = info;
1009
+ binding.runtime = { type: 'observation', delivery: 'live', sourceKey: `runtime:${randomUUID()}`, occurredAt: Date.now(), event: { type: 'runtime_updated', provider: info.providerId, runtimeInfo: { ...runtimeInfo, childSessions: [...children.values()] } } };
1010
+ this.publishBinding(binding, false);
1011
+ }
1012
+ catch {
1013
+ continue;
1014
+ }
1015
+ }
1016
+ expanded.add(binding.nativeId);
1017
+ await this.adoptKnownChildren(binding, inspected, queue, retry);
1018
+ }
1019
+ }).finally(() => {
1020
+ this.deferredRelations.set(source.channelId, [...new Set([...queue, ...retry])]);
1021
+ this.relationPasses.delete(source.channelId);
1022
+ source.admitting = undefined;
1023
+ });
1024
+ this.relationPasses.set(source.channelId, { promise: pass, queue, inspected });
1025
+ source.admitting = pass;
1026
+ }
1027
+ async adoptKnownChildren(source, inspected, queue, retry) {
1028
+ const descriptors = source.runtime?.event.type === 'runtime_updated' ? source.runtime.event.runtimeInfo.childSessions ?? [] : [];
1029
+ for (const descriptor of descriptors) {
1030
+ if (!this.bindingCurrent(source))
1031
+ return;
1032
+ const known = [...this.bindingRecords.values()].find(record => record.channelId === source.channelId && record.nativeId === descriptor.nativeSessionId && this.bindingCurrent(record));
1033
+ if (known) {
1034
+ queue.push(known);
1035
+ continue;
1036
+ }
1037
+ if (inspected.has(descriptor.nativeSessionId))
1038
+ continue;
1039
+ if (inspected.size >= 64) {
1040
+ retry.push(source);
1041
+ continue;
1042
+ }
1043
+ inspected.add(descriptor.nativeSessionId);
1044
+ const admit = async () => {
1045
+ let session;
1046
+ try {
1047
+ session = await this.options.provider.openChildSession(source.nativeId, descriptor.nativeSessionId);
1048
+ if (!this.bindingCurrent(source))
1049
+ return;
1050
+ const info = await session.runtimeInfo();
1051
+ if (!this.bindingCurrent(source) || info.sessionId !== descriptor.nativeSessionId || info.providerId !== this.options.provider.descriptor.providerId)
1052
+ return;
1053
+ if (info.cwd && await realpath(info.cwd) !== source.cwd)
1054
+ return;
1055
+ if ([...this.bindingRecords.values()].some(record => record.session === session || !record.closed && record.nativeId === info.sessionId))
1056
+ return;
1057
+ const now = new Date().toISOString();
1058
+ const timeline = new SessionTimeline({ providerId: info.providerId, nativeSessionId: info.sessionId, visibilityKey: source.visibilityKey }, source.agentId, source.channelId);
1059
+ const ownership = new InputOwnership(timeline);
1060
+ const binding = { bindingId: randomUUID(), routingKey: source.routingKey, session, nativeId: info.sessionId, channelId: source.channelId,
1061
+ cwd: source.cwd, visibilityKey: source.visibilityKey, additionalDirectories: source.additionalDirectories, agentId: source.agentId, persistent: false, retained: false, source,
1062
+ timeline, projection: new SessionObservationProjection(timeline, ownership, owner => binding.executions.get(owner)?.taskId),
1063
+ gate: new NormalizedObservationGate(), ownership, executions: new Map(), ready: deferred(), observation: { status: 'recovering', revision: 1, reason: 'initializing' }, observationQueue: [], observationBytes: 0, pump: Promise.resolve(), requests: new Map(), receipts: new Map(),
1064
+ closed: false, createdAt: now, updatedAt: now, status: info.status };
1065
+ const { persistence: _persistence, ...runtimeInfo } = info;
1066
+ binding.runtime = { type: 'observation', delivery: 'live', sourceKey: `runtime:${randomUUID()}`, occurredAt: Date.now(), event: { type: 'runtime_updated', provider: info.providerId, runtimeInfo } };
1067
+ this.bindingRecords.set(binding.bindingId, binding);
1068
+ queue.push(binding);
1069
+ session = undefined;
1070
+ if (this.options.provider.descriptor.providerId !== 'codex')
1071
+ binding.legacyOwnership = new LegacyInputOwnership(true);
1072
+ binding.pump = this.observe(binding);
1073
+ this.publishBinding(binding, false);
1074
+ }
1075
+ catch {
1076
+ // A source may advertise a child before its native handle is available.
1077
+ }
1078
+ finally {
1079
+ if (session && ![...this.bindingRecords.values()].some(record => record.session === session))
1080
+ await session.dispose();
1081
+ }
1082
+ };
1083
+ if (this.management)
1084
+ await this.management.admitUnclaimed(descriptor.nativeSessionId, admit).catch(() => { });
1085
+ else
1086
+ await admit();
1087
+ }
1088
+ }
1089
+ observeSessionState(binding, observation, taskId) {
1090
+ if (!this.bindingCurrent(binding))
1091
+ return;
1092
+ let event = observation.event;
1093
+ if (event.type === 'runtime_updated')
1094
+ return;
1095
+ binding.publicationTaskId = taskId;
1096
+ if (event.type === 'interaction_requested') {
1097
+ binding.requestTasks ??= new Map();
1098
+ binding.requestTasks.set(event.request.requestId, taskId);
1099
+ binding.requests.set(event.request.requestId, event.request);
1100
+ event = { ...event, request: publicHostInteraction(event.request) };
1101
+ }
1102
+ else if (event.type === 'interaction_resolved') {
1103
+ const request = binding.requests.get(event.requestId);
1104
+ if (!request || request.kind !== event.response.kind)
1105
+ return;
1106
+ const response = redactInteractionResponse(request, event.response);
1107
+ binding.publicationTaskId = binding.requestTasks?.get(event.requestId);
1108
+ binding.requestTasks?.delete(event.requestId);
1109
+ binding.requests.delete(event.requestId);
1110
+ binding.receipts.get(event.requestId)?.resolve(response);
1111
+ event = { ...event, response };
1112
+ }
1113
+ if (!this.sessionPublication?.supported() && !binding.publisher?.hasTimelineSource(observation))
1114
+ return;
1115
+ if (event.type === 'timeline')
1116
+ this.publisher(binding).observeTimeline({ ...observation, event }, { scope: binding.timeline.scope, inputs: binding.timeline.inputs, agentId: binding.agentId, channelId: binding.channelId }, binding.bindingId, binding.timeline.epoch);
1117
+ else
1118
+ this.publisher(binding).observe({ ...observation, event }, undefined, binding.bindingId, binding.timeline.epoch);
1119
+ }
1120
+ async selectBinding(binding) {
1121
+ const previous = this.bindings.get(binding.channelId);
1122
+ if (previous === binding)
1123
+ return;
1124
+ this.bindings.set(binding.channelId, binding);
1125
+ this.publishBinding(binding, true);
1126
+ if (previous) {
1127
+ if (!this.sessionPublication?.supported() || !previous.active && !previous.executions.size) {
1128
+ previous.stateSink?.close();
1129
+ previous.stateSink = undefined;
1130
+ }
1131
+ }
1132
+ }
1133
+ async observe(binding) {
1134
+ let ready = false;
1135
+ try {
1136
+ for await (const item of binding.session.observe()) {
1137
+ recordProtocolTrace({ stage: 'provider.observation', kind: item.type,
1138
+ correlation: { channelId: binding.channelId, agentId: binding.agentId, sessionId: binding.nativeId,
1139
+ taskId: binding.active?.taskId, turnId: binding.active?.turnId }, payload: item,
1140
+ outcome: item.type === 'history_boundary' ? 'boundary' : !ready ? 'history' : item.type !== 'observation' || item.delivery !== 'live' ? 'not_live' : !binding.active ? 'idle' : 'active' });
1141
+ if (item.type === 'history_boundary') {
1142
+ ready = true;
1143
+ if (this.options.provider.descriptor.sessionObservation !== 'complete-state') {
1144
+ binding.ready.resolve();
1145
+ this.setAvailability(binding, 'available');
1146
+ this.publishBinding(binding, false);
1147
+ this.publishTimeline(binding);
1148
+ this.admitChildren(binding);
1149
+ }
1150
+ continue;
1151
+ }
1152
+ if (item.type === 'session_invalidated') {
1153
+ this.setAvailability(binding, item.recovery === 'retrying' ? 'recovering' : 'stopped', item.reason, true);
1154
+ binding.observationQueue = [];
1155
+ binding.observationBytes = 0;
1156
+ this.retireInteractions(binding);
1157
+ binding.ownership.retireObservations();
1158
+ binding.legacyOwnership?.retireObservations();
1159
+ continue;
1160
+ }
1161
+ if (item.type === 'observation' && (!ready || item.delivery !== 'live')) {
1162
+ binding.timeline.append(binding.legacyOwnership?.publicObservation(item) ?? item);
1163
+ binding.gate.accept(item);
1164
+ continue;
1165
+ }
1166
+ if (!ready)
1167
+ continue;
1168
+ if (item.type === 'session_restored') {
1169
+ this.setAvailability(binding, 'recovering', 'restoring', true);
1170
+ }
1171
+ this.queueObservation(binding, item);
1172
+ }
1173
+ if (!binding.closed)
1174
+ throw new Error('The native observation stream closed before the session was disposed.');
1175
+ }
1176
+ catch (error) {
1177
+ binding.ready.reject(error);
1178
+ this.setAvailability(binding, 'stopped', 'observation_ended', true);
1179
+ binding.observationQueue = [];
1180
+ binding.observationBytes = 0;
1181
+ this.retireInteractions(binding);
1182
+ binding.active?.result.reject(error);
1183
+ for (const turn of binding.executions.values())
1184
+ turn.result.reject(error);
1185
+ }
1186
+ }
1187
+ queueObservation(binding, item) {
1188
+ const bytes = Buffer.byteLength(JSON.stringify(item));
1189
+ if (binding.observationQueue.length >= 4096 || binding.observationBytes + bytes > 4 * 1024 * 1024) {
1190
+ binding.observationQueue = [];
1191
+ binding.observationBytes = 0;
1192
+ this.setAvailability(binding, 'stopped', 'observation_capacity', true);
1193
+ this.retireInteractions(binding);
1194
+ return;
1195
+ }
1196
+ binding.observationQueue.push({ item: structuredClone(item), revision: binding.observation.revision, bytes });
1197
+ binding.observationBytes += bytes;
1198
+ this.drainObservations(binding);
1199
+ }
1200
+ drainObservations(binding) {
1201
+ if (binding.observationWork)
1202
+ return;
1203
+ binding.observationWork = Promise.resolve().then(async () => {
1204
+ while (binding.observationQueue.length && !this.disposed && !binding.closed) {
1205
+ const next = binding.observationQueue.shift();
1206
+ binding.observationBytes -= next.bytes;
1207
+ if (next.revision !== binding.observation.revision)
1208
+ continue;
1209
+ try {
1210
+ if (next.item.type === 'session_restored')
1211
+ await this.restoreSession(binding, next.item, next.revision);
1212
+ else if (binding.observation.status === 'available')
1213
+ await this.processObservation(binding, next.item);
1214
+ }
1215
+ catch {
1216
+ if (next.revision === binding.observation.revision && this.bindingCurrent(binding)) {
1217
+ this.setAvailability(binding, 'stopped', next.item.type === 'session_restored' ? 'invalid_native_snapshot' : 'observation_failed', true);
1218
+ this.retireInteractions(binding);
1219
+ }
1220
+ }
1221
+ }
1222
+ }).finally(() => {
1223
+ binding.observationWork = undefined;
1224
+ if (binding.observationQueue.length && !this.disposed && !binding.closed)
1225
+ this.drainObservations(binding);
1226
+ });
1227
+ }
1228
+ async processObservation(binding, item) {
1229
+ const revision = binding.observation.revision;
1230
+ if (item.type === 'timeline_replacement') {
1231
+ if (!binding.closed) {
1232
+ const association = binding.projection.replaceHistory(item.observations.map(observation => binding.legacyOwnership?.publicObservation(observation) ?? observation));
1233
+ if (association) {
1234
+ if (this.reconcileAssociationMetadata(binding, association))
1235
+ this.publishTimeline(binding);
1236
+ this.publishTimeline(binding);
1237
+ }
1238
+ }
1239
+ return;
1240
+ }
1241
+ if (['turn_started', 'turn_completed', 'turn_failed', 'turn_canceled'].includes(item.event.type) && 'turnId' in item.event && item.event.turnId && binding.restoredTerminals?.has(item.event.turnId))
1242
+ return;
1243
+ const accepted = binding.gate.accept(item);
1244
+ if (!accepted)
1245
+ return;
1246
+ let observation = accepted;
1247
+ if (accepted.event.type === 'interaction_requested')
1248
+ observation = { ...accepted, event: { ...accepted.event, request: publicHostInteraction(accepted.event.request) } };
1249
+ else if (accepted.event.type === 'interaction_resolved') {
1250
+ const request = binding.requests.get(accepted.event.requestId);
1251
+ if (request?.kind === accepted.event.response.kind)
1252
+ observation = { ...accepted, event: { ...accepted.event, response: redactInteractionResponse(request, accepted.event.response) } };
1253
+ }
1254
+ const legacyUser = binding.legacyOwnership && binding.executions.size > 0 && observation.event.type === 'timeline' && observation.event.item.type === 'user_message';
1255
+ if (!legacyUser) {
1256
+ observation = binding.legacyOwnership?.publicObservation(observation) ?? observation;
1257
+ if (binding.legacyOwnership)
1258
+ binding.timeline.append(observation);
1259
+ }
1260
+ const projected = binding.legacyOwnership ? undefined : binding.projection.observeLive(observation);
1261
+ observation = projected?.publicObservation ?? observation;
1262
+ const nativeTurnChanged = this.observeNativeTurn(binding, observation);
1263
+ if (observation !== item)
1264
+ recordProtocolTrace({ stage: 'host.normalized', kind: 'observation_conflict', correlation: { channelId: binding.channelId, agentId: binding.agentId, taskId: binding.active?.taskId }, outcome: 'diagnosed' });
1265
+ const association = binding.legacyOwnership
1266
+ ? { status: 'unchanged', correlations: [], unavailableOwners: [], owned: binding.legacyOwnership.observe(observation).map(value => ({ owner: value.owner.ownerId, observation: value.observation })) }
1267
+ : projected.association;
1268
+ const currentOwned = association.owned.find(value => value.observation.sourceKey === observation.sourceKey);
1269
+ const currentTask = currentOwned && !legacyUser ? binding.projection.attributeOwned(currentOwned).taskId : undefined;
1270
+ if (!legacyUser)
1271
+ this.observeSessionState(binding, observation, currentTask);
1272
+ await this.applyAssociation(binding, association);
1273
+ if (binding.observation.revision !== revision || !this.bindingCurrent(binding))
1274
+ return;
1275
+ if (observation.event.type === 'runtime_updated')
1276
+ this.observeEvent(binding, observation.event, observation);
1277
+ if (binding.active && observation.event.type === 'interaction_resolved')
1278
+ void this.finishLocalCommand(binding, binding.active);
1279
+ if (!association.owned.length && nativeTurnChanged)
1280
+ this.syncNormalizedActivity(binding.channelId, binding.bindingId);
1281
+ }
1282
+ bindingCapabilities(binding) {
1283
+ return binding.restoredCapabilities ?? binding.session.capabilities;
1284
+ }
1285
+ retireInteractions(binding) {
1286
+ binding.requests.clear();
1287
+ binding.requestTasks?.clear();
1288
+ for (const receipt of binding.receipts.values())
1289
+ receipt.reject(new Error('Native interaction connection expired.'));
1290
+ binding.receipts.clear();
1291
+ }
1292
+ observeNativeTurn(binding, observation) {
1293
+ const event = observation.event;
1294
+ if (event.type === 'turn_started' && event.turnId) {
1295
+ binding.nativeActiveTurn = { turnId: event.turnId, startedAt: new Date(observation.occurredAt).toISOString() };
1296
+ binding.status = 'running';
1297
+ return true;
1298
+ }
1299
+ else if (['turn_completed', 'turn_failed', 'turn_canceled'].includes(event.type) && 'turnId' in event && event.turnId === binding.nativeActiveTurn?.turnId) {
1300
+ binding.nativeActiveTurn = null;
1301
+ binding.status = event.type === 'turn_failed' ? 'failed' : 'idle';
1302
+ return true;
1303
+ }
1304
+ return false;
1305
+ }
1306
+ async restoreSession(binding, restored, revision) {
1307
+ if (restored.runtimeInfo.sessionId !== binding.nativeId || restored.runtimeInfo.providerId !== this.options.provider.descriptor.providerId)
1308
+ throw new Error('Native snapshot identity changed.');
1309
+ const current = () => this.bindingCurrent(binding) && binding.observation.revision === revision;
1310
+ const sources = new Set();
1311
+ if (!Array.isArray(restored.observations) || !Array.isArray(restored.pendingInteractions)
1312
+ || new Set(restored.pendingInteractions.map(request => request.requestId)).size !== restored.pendingInteractions.length)
1313
+ throw new Error('Invalid native snapshot.');
1314
+ for (const row of restored.observations) {
1315
+ if (!row || row.type !== 'observation' || row.event.provider !== restored.runtimeInfo.providerId || !row.sourceKey || sources.has(row.sourceKey))
1316
+ throw new Error('Invalid native snapshot.');
1317
+ sources.add(row.sourceKey);
1318
+ }
1319
+ const { persistence: _snapshotPersistence, ...snapshotRuntime } = restored.runtimeInfo;
1320
+ const snapshot = { protocolVersion: PROTOCOL_VERSION, type: 'agent_snapshot', payload: { id: binding.agentId, providerId: snapshotRuntime.providerId,
1321
+ createdAt: binding.createdAt, updatedAt: new Date().toISOString(), status: snapshotRuntime.status, activeTurn: restored.activeTurn,
1322
+ capabilities: restored.capabilities, runtimeInfo: this.visibleRuntime(binding, snapshotRuntime), pendingInteractions: restored.pendingInteractions.map(publicHostInteraction) } };
1323
+ if (decodeAgentSnapshot(JSON.stringify(snapshot)).status !== 'ok')
1324
+ throw new Error('Invalid native snapshot.');
1325
+ const timeline = restored.observations.filter(observation => observation.event.type === 'timeline').map(observation => binding.legacyOwnership?.publicObservation(observation) ?? observation);
1326
+ formatTimelineReplacement(timeline, { scope: binding.timeline.scope, inputs: binding.timeline.inputs, agentId: binding.agentId, channelId: binding.channelId });
1327
+ if (Buffer.byteLength(JSON.stringify(timeline)) > 1024 * 1024)
1328
+ throw new Error('Native snapshot exceeds capacity.');
1329
+ this.retireInteractions(binding);
1330
+ for (const observation of restored.observations)
1331
+ binding.gate.accept(observation);
1332
+ if (!binding.timeline.replace(timeline))
1333
+ throw new Error('Invalid native history.');
1334
+ binding.ownership.retireObservations();
1335
+ binding.legacyOwnership?.retireObservations();
1336
+ await this.applyAssociation(binding, binding.ownership.restoreHistory());
1337
+ if (!current())
1338
+ return;
1339
+ const owners = [...binding.executions.values()];
1340
+ const command = binding.active;
1341
+ if (command?.commandSettled && command.expectsNativeTurn && command.commandTurnId && !owners.includes(command))
1342
+ owners.push(command);
1343
+ for (const turn of owners) {
1344
+ if (turn.terminal || turn.isCurrent && !turn.isCurrent())
1345
+ continue;
1346
+ const turnId = binding.legacyOwnership?.recover(turn, restored.observations) ?? binding.ownership.turnId(turn.ownerId) ?? turn.commandTurnId;
1347
+ if (!turnId || turn.turnId && turn.turnId !== turnId)
1348
+ continue;
1349
+ const evidence = restored.observations.filter(({ event }) => 'turnId' in event && event.turnId === turnId);
1350
+ if (!evidence.length)
1351
+ continue;
1352
+ const started = evidence.find(({ event }) => event.type === 'turn_started');
1353
+ if (!turn.started && !started)
1354
+ continue;
1355
+ if (binding.active && binding.active !== turn && !binding.active.terminal)
1356
+ continue;
1357
+ const messages = new Map();
1358
+ let completeMessages = true;
1359
+ for (const { event } of evidence) {
1360
+ if (event.type !== 'timeline' || event.item.type !== 'assistant_message' && event.item.type !== 'questionnaire')
1361
+ continue;
1362
+ if (!event.item.messageId) {
1363
+ completeMessages = false;
1364
+ break;
1365
+ }
1366
+ messages.set(event.item.messageId, event.item.type === 'questionnaire' ? questionnaireReplyText(event.item) : event.item.text);
1367
+ }
1368
+ if (!completeMessages)
1369
+ continue;
1370
+ turn.turnId = turnId;
1371
+ turn.messages = messages;
1372
+ if (!turn.started && started)
1373
+ await this.observeOwned(binding, turn, started);
1374
+ if (!current())
1375
+ return;
1376
+ if (turn.terminal)
1377
+ continue;
1378
+ const terminal = [...evidence].reverse().find(({ event }) => ['turn_completed', 'turn_failed', 'turn_canceled'].includes(event.type));
1379
+ if (terminal && restored.activeTurn?.turnId !== turnId) {
1380
+ const event = terminal.event.type === 'turn_completed'
1381
+ ? { type: terminal.event.type, provider: terminal.event.provider, turnId } : terminal.event;
1382
+ await this.observeOwned(binding, turn, { ...terminal, event });
1383
+ if (!current())
1384
+ return;
1385
+ }
1386
+ }
1387
+ if (!current())
1388
+ return;
1389
+ const { persistence: _persistence, ...runtimeInfo } = structuredClone(restored.runtimeInfo);
1390
+ binding.restoredCapabilities = structuredClone(restored.capabilities);
1391
+ binding.restoredTerminals = new Set(restored.observations.flatMap(({ event }) => ['turn_completed', 'turn_failed', 'turn_canceled'].includes(event.type)
1392
+ && 'turnId' in event && event.turnId && event.turnId !== restored.activeTurn?.turnId ? [event.turnId] : []));
1393
+ binding.timeline.replace(restored.observations.filter(observation => observation.event.type === 'timeline').map(observation => binding.legacyOwnership?.publicObservation(observation) ?? observation));
1394
+ binding.runtime = { type: 'observation', delivery: 'live', sourceKey: `runtime:${randomUUID()}`, occurredAt: Date.now(),
1395
+ event: { type: 'runtime_updated', provider: this.options.provider.descriptor.providerId, runtimeInfo } };
1396
+ binding.nativeActiveTurn = structuredClone(restored.activeTurn);
1397
+ binding.status = runtimeInfo.status;
1398
+ binding.updatedAt = new Date().toISOString();
1399
+ binding.requests = new Map(restored.pendingInteractions.map(request => [request.requestId, structuredClone(request)]));
1400
+ binding.requestTasks = new Map(restored.pendingInteractions.map(request => [request.requestId, undefined]));
1401
+ binding.ready.resolve();
1402
+ this.setAvailability(binding, 'available');
1403
+ for (const queued of binding.observationQueue)
1404
+ if (queued.revision === revision)
1405
+ queued.revision = binding.observation.revision;
1406
+ this.syncNormalizedActivity(binding.channelId, binding.bindingId);
1407
+ this.admitChildren(binding);
1408
+ }
1409
+ observeEvent(binding, event, observation) {
1410
+ binding.updatedAt = new Date(observation?.occurredAt ?? Date.now()).toISOString();
1411
+ if (event.type === 'runtime_updated') {
1412
+ binding.restoredCapabilities = undefined;
1413
+ const { persistence: _persistence, ...runtimeInfo } = event.runtimeInfo;
1414
+ event = { ...event, runtimeInfo: this.visibleRuntime(binding, runtimeInfo) };
1415
+ if (observation) {
1416
+ const previous = binding.runtime?.event.type === 'runtime_updated' ? binding.runtime.event.runtimeInfo.childSessions : undefined;
1417
+ const relationsChanged = JSON.stringify(previous) !== JSON.stringify(runtimeInfo.childSessions);
1418
+ observation = { ...observation, event };
1419
+ binding.runtime = observation;
1420
+ this.admitChildren(binding, relationsChanged);
1421
+ }
1422
+ binding.status = runtimeInfo.status;
1423
+ this.publishBinding(binding, false);
1424
+ if ((!binding.active || binding.active.terminal) && observation && !binding.closed && this.bindings.get(binding.channelId) === binding) {
1425
+ recordProtocolTrace({ stage: 'host.normalized', kind: event.type, correlation: { channelId: binding.channelId, agentId: binding.agentId }, payload: event, outcome: 'idle_state' });
1426
+ try {
1427
+ binding.stateSink?.observe(observation, binding.agentId, structuredClone(this.bindingCapabilities(binding)), binding.bindingId, binding.timeline.epoch);
1428
+ this.publishTimeline(binding);
1429
+ }
1430
+ catch { /* Idle publication cannot reopen an execution. */ }
1431
+ return;
1432
+ }
1433
+ }
1434
+ const turn = binding.active;
1435
+ const rejected = (reason) => recordProtocolTrace({ stage: 'host.normalized', kind: event.type,
1436
+ correlation: { channelId: binding.channelId, agentId: binding.agentId, taskId: turn?.taskId, turnId: turn?.turnId },
1437
+ payload: event, outcome: 'rejected', reason });
1438
+ if (!turn || turn.terminal)
1439
+ return;
1440
+ if (turn.isCurrent && !turn.isCurrent()) {
1441
+ turn.result.reject(new ProviderTurnCancelledError('Native Session connection ownership ended.'));
1442
+ return;
1443
+ }
1444
+ if (event.type === 'turn_started') {
1445
+ if (turn.turnId && event.turnId !== turn.turnId) {
1446
+ rejected('turn_mismatch');
1447
+ return;
1448
+ }
1449
+ if (!turn.started)
1450
+ turn.startedAt = new Date(observation?.occurredAt ?? Date.now()).toISOString();
1451
+ turn.started = true;
1452
+ turn.turnId = event.turnId;
1453
+ binding.status = 'running';
1454
+ }
1455
+ if ('turnId' in event && event.turnId && turn.turnId && event.turnId !== turn.turnId) {
1456
+ rejected('turn_mismatch');
1457
+ return;
1458
+ }
1459
+ let publicEvent = event;
1460
+ if (event.type === 'timeline' && event.item.type === 'user_message') {
1461
+ if (observation) {
1462
+ const row = formatTimelineReplacement([observation], { scope: binding.timeline.scope, inputs: binding.timeline.inputs, agentId: binding.agentId, channelId: binding.channelId })[0];
1463
+ publicEvent = { ...event, item: row.event.item };
1464
+ }
1465
+ }
1466
+ else if (event.type === 'interaction_requested') {
1467
+ publicEvent = { ...event, request: publicHostInteraction(event.request) };
1468
+ }
1469
+ else if (event.type === 'runtime_updated') {
1470
+ const { persistence: _persistence, ...runtimeInfo } = event.runtimeInfo;
1471
+ publicEvent = { ...event, runtimeInfo };
1472
+ }
1473
+ if (event.type === 'turn_completed' || event.type === 'turn_failed' || event.type === 'turn_canceled') {
1474
+ turn.terminal = true;
1475
+ binding.status = event.type === 'turn_failed' ? 'failed' : 'idle';
1476
+ }
1477
+ if (observation && event.type === 'timeline')
1478
+ binding.timeline.inputs.attribute(binding.timeline.scope, observation.sourceKey, turn.taskId);
1479
+ recordProtocolTrace({ stage: 'host.normalized', kind: publicEvent.type,
1480
+ correlation: { channelId: binding.channelId, agentId: binding.agentId, taskId: turn.taskId, turnId: turn.turnId }, payload: publicEvent });
1481
+ try {
1482
+ const metadata = observation ? { ...observation, event: publicEvent } : undefined;
1483
+ if (publicEvent.type === 'runtime_updated')
1484
+ turn.options?.onNormalizedObservation?.(publicEvent, binding.agentId, metadata, structuredClone(this.bindingCapabilities(binding)), binding.bindingId, binding.timeline.epoch);
1485
+ else if (observation && (this.sessionPublication?.supported() || binding.publisher?.hasTimelineSource(observation)))
1486
+ turn.options?.onNormalizedObservation?.(publicEvent, binding.agentId, metadata, undefined, binding.bindingId, binding.timeline.epoch, true);
1487
+ else
1488
+ turn.options?.onNormalizedObservation?.(publicEvent, binding.agentId, metadata, undefined, binding.bindingId, binding.timeline.epoch);
1489
+ }
1490
+ catch { /* Presentation cannot settle a durable turn. */ }
1491
+ if (event.type === 'interaction_resolved' || event.type === 'runtime_updated')
1492
+ void this.finishLocalCommand(binding, turn);
1493
+ if (event.type === 'timeline' && (event.item.type === 'assistant_message' || event.item.type === 'questionnaire') && turn.started) {
1494
+ const key = event.item.messageId ?? 'assistant';
1495
+ turn.messages.set(key, event.item.type === 'questionnaire' ? questionnaireReplyText(event.item) : (turn.messages.get(key) ?? '') + event.item.text);
1496
+ try {
1497
+ turn.options?.onProgress?.({ type: 'text', stream: 'answer', text: [...turn.messages.values()].join('\n\n'), messageId: key });
1498
+ }
1499
+ catch { /* Draft delivery is best-effort. */ }
1500
+ }
1501
+ else if (event.type === 'turn_completed' && turn.started) {
1502
+ turn.result.resolve({ ...parseProviderReply([...turn.messages.values()].join('\n\n')), sessionId: binding.nativeId });
1503
+ }
1504
+ else if (event.type === 'turn_failed')
1505
+ turn.result.reject(new Error(event.error));
1506
+ else if (event.type === 'turn_canceled')
1507
+ turn.result.reject(new ProviderTurnCancelledError(event.reason));
1508
+ }
1509
+ async respondInteraction(input, context) {
1510
+ const binding = this.controlBinding(input.channelId, input.binding?.binding_id);
1511
+ const revision = binding?.observation.revision;
1512
+ const current = () => !!binding && this.bindingCurrent(binding) && binding.observation.status === 'available' && binding.observation.revision === revision && (!context || !context.signal.aborted && (context.isConnectionCurrent?.() ?? true));
1513
+ const active = binding?.active && !binding.active.terminal && (binding.nativeActiveTurn === undefined || binding.nativeActiveTurn?.turnId === binding.active.turnId) ? binding.active : undefined;
1514
+ const requestTask = binding?.requestTasks?.get(input.requestId);
1515
+ if (!binding || !current() || binding.agentId !== input.agentId || input.binding && (!this.sessionPublication?.supported() || binding.nativeId !== input.binding.native_session_id)
1516
+ || !binding.requestTasks?.has(input.requestId) || (input.taskId ? requestTask !== input.taskId : !input.binding || !this.sessionPublication?.supported()))
1517
+ throw new Error('The native interaction does not belong to this Host Session request.');
1518
+ const request = binding.requests.get(input.requestId);
1519
+ if (!request || request.kind !== input.response.kind || binding.receipts.has(input.requestId))
1520
+ throw new Error('The native interaction is stale or already being answered.');
1521
+ assertHostedInteractionScope(input.response);
1522
+ const receipt = deferred();
1523
+ binding.receipts.set(input.requestId, receipt);
1524
+ const timeout = setTimeout(() => receipt.reject(new Error('Native interaction receipt timed out.')), 10_000);
1525
+ try {
1526
+ const [, confirmed] = await Promise.all([binding.session.respondToInteraction(input.requestId, input.response), receipt.promise]);
1527
+ if (!current())
1528
+ throw new Error('Native interaction connection ownership ended.');
1529
+ if (active && binding.active === active && !active.terminal)
1530
+ await this.finishLocalCommand(binding, active);
1531
+ return confirmed;
1532
+ }
1533
+ finally {
1534
+ clearTimeout(timeout);
1535
+ if (binding.receipts.get(input.requestId) === receipt)
1536
+ binding.receipts.delete(input.requestId);
1537
+ }
1538
+ }
1539
+ /** Capture and publication share the observation loop's synchronous ordering. */
1540
+ syncNormalizedActivity(channelId, bindingId) {
1541
+ const binding = this.controlBinding(channelId, bindingId);
1542
+ if (!binding || binding.closed || binding.runtime?.event.type !== 'runtime_updated')
1543
+ return false;
1544
+ if (binding.observation.status !== 'available') {
1545
+ this.publishAvailability(binding);
1546
+ return false;
1547
+ }
1548
+ this.admitChildren(binding);
1549
+ if (!bindingId && this.sessionPublication?.supported())
1550
+ for (const record of this.bindingRecords.values())
1551
+ if (record.channelId === channelId && record !== binding)
1552
+ this.publishBinding(record, false);
1553
+ const declared = this.publishBinding(binding, !bindingId);
1554
+ if (!binding.stateSink?.snapshot && !this.sessionPublication?.supported())
1555
+ return declared;
1556
+ const active = binding.active && !binding.active.terminal ? binding.active : undefined;
1557
+ const pendingInteractions = [...binding.requests.values()].map(publicHostInteraction);
1558
+ const nativeActive = binding.nativeActiveTurn;
1559
+ const status = pendingInteractions.length ? 'waiting' : nativeActive !== undefined ? binding.status : active ? (active.started ? 'running' : 'starting') : binding.status;
1560
+ const snapshot = { protocolVersion: PROTOCOL_VERSION, type: 'agent_snapshot', payload: {
1561
+ id: binding.agentId, providerId: this.options.provider.descriptor.providerId,
1562
+ createdAt: binding.createdAt, updatedAt: binding.updatedAt, status,
1563
+ activeTurn: nativeActive !== undefined ? nativeActive : active?.turnId ? { turnId: active.turnId, startedAt: active.startedAt } : null,
1564
+ capabilities: structuredClone(this.bindingCapabilities(binding)), pendingInteractions,
1565
+ runtimeInfo: { ...structuredClone(binding.runtime.event.runtimeInfo), sessionId: binding.nativeId, status },
1566
+ } };
1567
+ const sessionScoped = this.sessionPublication?.supported() === true;
1568
+ const stateSynced = !sessionScoped && (binding.stateSink?.snapshot?.(snapshot, binding.bindingId, binding.timeline.epoch) ?? false);
1569
+ const synced = stateSynced || declared;
1570
+ if (sessionScoped) {
1571
+ const taskId = binding.publicationTaskId;
1572
+ // A complete Session snapshot can contain requests owned by different executions.
1573
+ binding.publicationTaskId = undefined;
1574
+ try {
1575
+ this.publisher(binding).publish(snapshot, Date.now(), this.options.provider.descriptor.providerId, this.bindingCapabilities(binding), binding.bindingId, binding.timeline.epoch);
1576
+ }
1577
+ finally {
1578
+ binding.publicationTaskId = taskId;
1579
+ }
1580
+ }
1581
+ if (synced)
1582
+ this.publishTimeline(binding);
1583
+ return synced;
1584
+ }
1585
+ async cancelTurn(channelId, target, context) {
1586
+ const binding = this.controlBinding(channelId, target?.binding_id);
1587
+ if (target && (!this.sessionPublication?.supported() || binding?.nativeId !== target.native_session_id) || context && (context.signal.aborted || context.isConnectionCurrent && !context.isConnectionCurrent()))
1588
+ return false;
1589
+ if (!binding || binding.closed || binding.observation.status !== 'available' || !this.bindingCapabilities(binding).cancel || !binding.session.cancel || !binding.active && !['running', 'waiting', 'starting'].includes(binding.status))
1590
+ return false;
1591
+ await binding.session.cancel();
1592
+ return true;
1593
+ }
1594
+ async interruptTurn(channelId) { return this.cancelTurn(channelId); }
1595
+ async cancelSession(channelId) { return this.cancelTurn(channelId); }
1596
+ closeBinding(binding) {
1597
+ binding.closing ??= this.disposeBinding(binding);
1598
+ return binding.closing;
1599
+ }
1600
+ async disposeBinding(binding) {
1601
+ this.setAvailability(binding, 'stopped', 'session_closed', true);
1602
+ binding.closed = true;
1603
+ binding.observationQueue = [];
1604
+ binding.observationBytes = 0;
1605
+ await binding.admitting;
1606
+ binding.publisher?.close();
1607
+ binding.ownership.dispose();
1608
+ binding.closed = true;
1609
+ if (binding.active?.started && !binding.active.terminal)
1610
+ this.observeEvent(binding, { type: 'turn_canceled', provider: this.options.provider.descriptor.providerId, turnId: binding.active.turnId, reason: 'Native session closed.' });
1611
+ binding.active?.result.reject(new ProviderTurnCancelledError('Native session closed.'));
1612
+ for (const turn of binding.executions.values())
1613
+ turn.result.reject(new ProviderTurnCancelledError('Native session closed.'));
1614
+ for (const receipt of binding.receipts.values())
1615
+ receipt.reject(new Error('Native session closed.'));
1616
+ binding.stateSink?.close();
1617
+ binding.stateSink = undefined;
1618
+ try {
1619
+ await binding.session.dispose();
1620
+ await binding.pump;
1621
+ }
1622
+ finally {
1623
+ if (this.bindings.get(binding.channelId) === binding)
1624
+ this.bindings.delete(binding.channelId);
1625
+ this.bindingRecords.delete(binding.bindingId);
1626
+ }
1627
+ }
1628
+ async dispose() {
1629
+ if (this.disposed)
1630
+ return;
1631
+ this.disposed = true;
1632
+ const closed = await Promise.allSettled([...this.bindingRecords.values()].map((binding) => this.closeBinding(binding)));
1633
+ this.deferredRelations.clear();
1634
+ await this.options.provider.dispose?.();
1635
+ const failed = closed.find((result) => result.status === 'rejected');
1636
+ if (failed?.status === 'rejected')
1637
+ throw failed.reason;
1638
+ }
1639
+ }
1640
+ function resolveAdditionalDirectories(turn) {
1641
+ const context = turn.promptContext;
1642
+ const directories = new Set();
1643
+ if (context?.channelContextPayloadPath)
1644
+ directories.add(dirname(context.channelContextPayloadPath));
1645
+ if (context?.gatewayCredentialPath)
1646
+ directories.add(dirname(context.gatewayCredentialPath));
1647
+ if (context?.skillRuntime?.skillDirectoryPath)
1648
+ directories.add(context.skillRuntime.skillDirectoryPath);
1649
+ return [...directories].sort((left, right) => left.localeCompare(right));
1650
+ }