maka-agent 0.2.0-dev.46.20260922 → 0.2.0-dev.47.20260922

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 (53) hide show
  1. package/DISCLAIMER-WIP +6 -1
  2. package/README.md +6 -1
  3. package/dist/acp/maka-acp-agent.js +1 -1
  4. package/dist/acp/session-mcp.js +212 -0
  5. package/dist/acp/session-registry.js +52 -8
  6. package/dist/acp/stdio-server.js +3 -0
  7. package/dist/cli-core.js +1 -1
  8. package/dist/mcp-capability-provider.js +6 -1
  9. package/dist/mcp-capability-publication.js +139 -0
  10. package/dist/pi-tui-layout.js +46 -17
  11. package/dist/pi-tui-pickers.js +25 -14
  12. package/dist/pi-tui-runner.js +24 -15
  13. package/dist/runtime-host-service-manager.js +26 -2
  14. package/dist/tui-context-refresh.js +15 -26
  15. package/dist/tui-mcp-control.js +29 -88
  16. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  17. package/node_modules/@maka/core/dist/client-capability-grant.js +2 -2
  18. package/node_modules/@maka/core/dist/model-thinking.js +36 -1
  19. package/node_modules/@maka/core/dist/provider-registry.js +0 -1
  20. package/node_modules/@maka/core/dist/refresh-read-coordinator.js +62 -0
  21. package/node_modules/@maka/core/dist/runtime-policy/connection-catalog-codec.js +4 -0
  22. package/node_modules/@maka/core/dist/session.js +17 -1
  23. package/node_modules/@maka/core/dist/settings.js +2 -0
  24. package/node_modules/@maka/core/package.json +1 -0
  25. package/node_modules/@maka/runtime/dist/ai-sdk-message-projection.js +15 -60
  26. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +18 -0
  27. package/node_modules/@maka/runtime/dist/apply-patch-profile.js +64 -14
  28. package/node_modules/@maka/runtime/dist/builtin-tools.js +3 -2
  29. package/node_modules/@maka/runtime/dist/codex-v4a-patch.js +18 -0
  30. package/node_modules/@maka/runtime/dist/model-adapter.js +80 -3
  31. package/node_modules/@maka/runtime/dist/model-runtime.js +6 -0
  32. package/node_modules/@maka/runtime/dist/openai-apply-patch.js +5 -0
  33. package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +6 -0
  34. package/node_modules/@maka/runtime/dist/session-manager.js +15 -2
  35. package/node_modules/@maka/runtime-host/dist/client/capability-provider-service.js +2 -0
  36. package/node_modules/@maka/runtime-host/dist/client/client-capability-channel.js +59 -20
  37. package/node_modules/@maka/runtime-host/dist/client/connection.js +6 -4
  38. package/node_modules/@maka/runtime-host/dist/client/reconnecting-connection.js +4 -4
  39. package/node_modules/@maka/runtime-host/dist/protocol/client-capability.js +20 -3
  40. package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +8 -5
  41. package/node_modules/@maka/runtime-host/dist/protocol/index.js +11 -1
  42. package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +35 -3
  43. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +1 -5
  44. package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +234 -70
  45. package/node_modules/@maka/runtime-host/dist/server/client-capability-invocation-broker.js +14 -8
  46. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +29 -2
  47. package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +3 -0
  48. package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +29 -7
  49. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +1 -1
  50. package/node_modules/@maka/runtime-host/dist/server/session-bundle-coordinator.js +4 -1
  51. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +32 -4
  52. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +2 -2
  53. package/package.json +1 -1
@@ -20,11 +20,14 @@ import { createHash } from 'node:crypto';
20
20
  import { AsyncLocalStorage } from 'node:async_hooks';
21
21
  import { buildMcpTools, mcpProxyToolName, } from '@maka/runtime/mcp-tools';
22
22
  import { clientCapabilityScopeIdentity, } from '@maka/core/client-capability-grant';
23
+ import { decodeClientCapabilityReplaceInput, } from '../protocol/index.js';
23
24
  import { ClientCapabilityInvocationBroker, ClientCapabilityInvocationError, } from './client-capability-invocation-broker.js';
24
25
  import { clientCapabilityProviderId } from './client-capability-provider-id.js';
25
26
  // Leave the Host deadline outside the provider's bounded action deadline so an
26
27
  // accepted call can return its real terminal result instead of outcome_unknown.
27
28
  const DEFAULT_CALL_TIMEOUT_MS = 150_000;
29
+ // Bound pre-creation Session publications even when a client invents Session IDs.
30
+ export const MAX_SESSION_REGISTRATIONS_PER_PROVIDER = 64;
28
31
  const DESKTOP_BROWSER_SERVER_ID = 'desktop_browser';
29
32
  const DESKTOP_SETTINGS_SERVER_ID = 'desktop_settings';
30
33
  const DESKTOP_MCP_OFFER_PREFIX = 'desktop_mcp';
@@ -48,6 +51,7 @@ export class HostClientCapabilityCoordinator {
48
51
  'client.capability.unregister': (input, context) => this.#unregister(input, context),
49
52
  };
50
53
  #activation;
54
+ #isSessionRetired;
51
55
  #onModelToolsChanged;
52
56
  #interactions;
53
57
  #grants;
@@ -62,16 +66,12 @@ export class HostClientCapabilityCoordinator {
62
66
  #draining = false;
63
67
  constructor(options) {
64
68
  this.#activation = options.activation;
69
+ this.#isSessionRetired = options.isSessionRetired;
65
70
  this.#onModelToolsChanged = options.onModelToolsChanged;
66
71
  this.#interactions = options.interactions;
67
72
  this.#grants = options.grants;
68
73
  this.#invocations = new ClientCapabilityInvocationBroker({
69
- senderFor: (connectionId) => {
70
- const connection = this.#connections.get(connectionId);
71
- return connection && this.#activeConnection(connection.provider) === connection
72
- ? connection.sender
73
- : undefined;
74
- },
74
+ senderFor: (registration) => this.#registrationConnection(registration)?.sender,
75
75
  onRegistrationIdle: (registration) => this.#releaseRegistrationIfUnused(registration),
76
76
  });
77
77
  }
@@ -151,10 +151,14 @@ export class HostClientCapabilityCoordinator {
151
151
  }
152
152
  async bindRecoveredSession(sessionId, binding, toolNames) {
153
153
  return this.#activation.runMutation(() => {
154
- const provider = [...this.#providers.values()].find((candidate) => providerAuthorityDigest(candidate) === binding && this.#activeConnection(candidate));
154
+ const provider = [...this.#providers.values()].find((candidate) => providerAuthorityDigest(candidate) === binding &&
155
+ (this.#activeConnection(candidate) ||
156
+ (candidate.sessionRegistrations.get(sessionId) &&
157
+ this.#registrationConnection(candidate.sessionRegistrations.get(sessionId)))));
155
158
  if (!provider)
156
159
  return false;
157
- const connection = this.#activeConnection(provider);
160
+ const scoped = provider.sessionRegistrations.get(sessionId);
161
+ const connection = (scoped && this.#registrationConnection(scoped)) || this.#activeConnection(provider);
158
162
  const selection = this.#selectSessionState(sessionId, connection.connectionId, 'strict');
159
163
  if (!selection.ok || this.#toolProviderBinding(selection.state, toolNames) !== binding)
160
164
  return false;
@@ -171,8 +175,9 @@ export class HostClientCapabilityCoordinator {
171
175
  if (binding.kind !== 'bound')
172
176
  continue;
173
177
  const provider = this.#providers.get(binding.providerId);
174
- const offer = provider?.current?.offersByContract.get(contractId);
175
- if (!provider || !this.#activeConnection(provider) || !offer)
178
+ const registration = this.#bindingRegistration(binding);
179
+ const offer = registration?.offersByContract.get(contractId);
180
+ if (!provider || !registration || !this.#registrationConnection(registration) || !offer)
176
181
  continue;
177
182
  for (const tool of offer.offer.tools) {
178
183
  if (!missing.delete(mcpProxyToolName(tool.serverId, tool.name)))
@@ -225,7 +230,10 @@ export class HostClientCapabilityCoordinator {
225
230
  }
226
231
  #selectSessionState(sessionId, initiatingConnectionId, mode) {
227
232
  const initiatingProvider = this.#connections.get(initiatingConnectionId)?.provider;
228
- const directProvider = initiatingProvider?.current && this.#activeConnection(initiatingProvider)
233
+ const initiatingScoped = initiatingProvider?.sessionRegistrations.get(sessionId);
234
+ const directProvider = initiatingProvider &&
235
+ ((initiatingProvider.current && this.#activeConnection(initiatingProvider)) ||
236
+ (initiatingScoped && this.#registrationConnection(initiatingScoped)))
229
237
  ? initiatingProvider
230
238
  : undefined;
231
239
  const associatedProviders = initiatingProvider &&
@@ -257,7 +265,7 @@ export class HostClientCapabilityCoordinator {
257
265
  ? initiatingProviderId
258
266
  : undefined);
259
267
  const previous = previousState?.sessionBindings ?? new Map();
260
- const eligible = this.#eligibleOffersByContract();
268
+ const eligible = this.#eligibleOffersByContract(sessionId);
261
269
  const next = new Map();
262
270
  const selected = [];
263
271
  const sessionContractIds = new Set([
@@ -271,10 +279,11 @@ export class HostClientCapabilityCoordinator {
271
279
  const candidates = eligible.get(contractId) ?? [];
272
280
  let candidate;
273
281
  if (previousBinding?.kind === 'bound') {
274
- candidate = candidates.find((entry) => entry.registration.providerId === previousBinding.providerId);
282
+ candidate = candidates.find((entry) => entry.registration.providerId === previousBinding.providerId &&
283
+ entry.registration.sessionId === previousBinding.sessionId);
275
284
  if (!candidate) {
276
285
  if (mode === 'degrade') {
277
- next.set(contractId, { kind: 'lost', providerId: previousBinding.providerId });
286
+ next.set(contractId, { ...previousBinding, kind: 'lost' });
278
287
  continue;
279
288
  }
280
289
  return {
@@ -284,7 +293,8 @@ export class HostClientCapabilityCoordinator {
284
293
  }
285
294
  }
286
295
  else if (previousBinding?.kind === 'lost') {
287
- candidate = candidates.find((entry) => entry.registration.providerId === previousBinding.providerId);
296
+ candidate = candidates.find((entry) => entry.registration.providerId === previousBinding.providerId &&
297
+ entry.registration.sessionId === previousBinding.sessionId);
288
298
  if (!candidate) {
289
299
  if (mode === 'degrade') {
290
300
  next.set(contractId, previousBinding);
@@ -297,8 +307,10 @@ export class HostClientCapabilityCoordinator {
297
307
  }
298
308
  }
299
309
  else {
300
- candidate = selectProviderCandidate(candidates, initiatingProviderId);
301
- if (!candidate && initiatingProviderId === undefined && candidates.length > 1) {
310
+ const selectable = candidates.filter((entry) => entry.registration.sessionId === undefined ||
311
+ entry.registration.providerId === initiatingProviderId);
312
+ candidate = selectProviderCandidate(selectable, initiatingProviderId);
313
+ if (!candidate && initiatingProviderId === undefined && selectable.length > 1) {
302
314
  if (mode === 'degrade')
303
315
  continue;
304
316
  return {
@@ -312,6 +324,9 @@ export class HostClientCapabilityCoordinator {
312
324
  next.set(contractId, {
313
325
  kind: 'bound',
314
326
  providerId: candidate.registration.providerId,
327
+ ...(candidate.registration.sessionId === undefined
328
+ ? {}
329
+ : { sessionId: candidate.registration.sessionId }),
315
330
  });
316
331
  selected.push(candidate);
317
332
  }
@@ -323,6 +338,9 @@ export class HostClientCapabilityCoordinator {
323
338
  next.set(candidate.offer.contractId, {
324
339
  kind: 'lost',
325
340
  providerId: candidate.registration.providerId,
341
+ ...(candidate.registration.sessionId === undefined
342
+ ? {}
343
+ : { sessionId: candidate.registration.sessionId }),
326
344
  });
327
345
  }
328
346
  else {
@@ -375,9 +393,9 @@ export class HostClientCapabilityCoordinator {
375
393
  if (binding.kind === 'lost')
376
394
  continue;
377
395
  const provider = this.#providers.get(binding.providerId);
378
- const registration = provider?.current;
396
+ const registration = this.#bindingRegistration(binding);
379
397
  const offer = registration?.offersByContract.get(contractId);
380
- if (!provider || !this.#activeConnection(provider) || !registration || !offer) {
398
+ if (!provider || !registration || !this.#registrationConnection(registration) || !offer) {
381
399
  throw new ClientCapabilityInvocationError('capability_lost', 'A Session-bound Client Capability provider is unavailable');
382
400
  }
383
401
  selected.push({ registration, offer });
@@ -553,10 +571,28 @@ export class HostClientCapabilityCoordinator {
553
571
  provider.current?.servicesByContract.has(serviceContract(serviceId, version)));
554
572
  }
555
573
  retireSessions(sessionIds) {
556
- for (const sessionId of new Set(sessionIds))
557
- this.#sessions.delete(sessionId);
558
- for (const provider of this.#providers.values())
559
- this.#deleteProviderIfUnused(provider);
574
+ const retiredSessionIds = new Set(sessionIds);
575
+ return this.#activation.runMutation(() => {
576
+ // Invalidate binding previews even when this Session has not committed a
577
+ // stored state or scoped registration yet. Otherwise a preview captured
578
+ // before retirement can commit afterwards and recreate its state.
579
+ for (const sessionId of retiredSessionIds) {
580
+ this.#sessions.delete(sessionId);
581
+ this.#revision += 1;
582
+ }
583
+ for (const provider of this.#providers.values()) {
584
+ for (const sessionId of retiredSessionIds) {
585
+ const registration = provider.sessionRegistrations.get(sessionId);
586
+ if (!registration)
587
+ continue;
588
+ provider.sessionRegistrations.delete(sessionId);
589
+ if (hasModelToolOffers(registration))
590
+ this.#onModelToolsChanged();
591
+ this.#releaseRegistrationIfUnused(registration);
592
+ }
593
+ this.#deleteProviderIfUnused(provider);
594
+ }
595
+ });
560
596
  }
561
597
  releaseConnection(connectionId) {
562
598
  const invocationCleanup = this.#invocations.releaseConnection(connectionId);
@@ -579,16 +615,23 @@ export class HostClientCapabilityCoordinator {
579
615
  return;
580
616
  const { provider } = connection;
581
617
  this.#connections.delete(connection.connectionId);
582
- if (provider.activeConnectionId !== connection.connectionId) {
583
- this.#deleteProviderIfUnused(provider);
584
- return;
618
+ if (provider.activeConnectionId === connection.connectionId) {
619
+ provider.activeConnectionId = undefined;
620
+ if (provider.current) {
621
+ const registration = provider.current;
622
+ provider.current = undefined;
623
+ this.#markBindingsLost(provider.providerId);
624
+ this.#removeTurnBindings(provider.providerId);
625
+ this.#revision += 1;
626
+ if (hasModelToolOffers(registration))
627
+ this.#onModelToolsChanged();
628
+ }
585
629
  }
586
- provider.activeConnectionId = undefined;
587
- if (provider.current) {
588
- const registration = provider.current;
589
- provider.current = undefined;
590
- this.#markBindingsLost(provider.providerId);
591
- this.#removeTurnBindings(provider.providerId);
630
+ for (const [sessionId, registration] of provider.sessionRegistrations) {
631
+ if (registration.connectionId !== connection.connectionId)
632
+ continue;
633
+ provider.sessionRegistrations.delete(sessionId);
634
+ this.#markBindingsLost(provider.providerId, undefined, sessionId);
592
635
  this.#revision += 1;
593
636
  if (hasModelToolOffers(registration))
594
637
  this.#onModelToolsChanged();
@@ -621,6 +664,18 @@ export class HostClientCapabilityCoordinator {
621
664
  },
622
665
  };
623
666
  }
667
+ // The lifecycle owner commits archive/removal before retiring capabilities.
668
+ // Read that authority inside the mutation lane: a queued refresh must not
669
+ // recreate a retired slot. Never-created IDs remain valid for ACP preparation.
670
+ if (input.sessionId !== undefined && (await this.#isSessionRetired(input.sessionId))) {
671
+ return {
672
+ ok: false,
673
+ error: {
674
+ code: 'invalid_request',
675
+ message: 'Client Capability target Session is retired',
676
+ },
677
+ };
678
+ }
624
679
  const connection = this.#connections.get(context.connectionId);
625
680
  if (!connection) {
626
681
  return {
@@ -632,7 +687,18 @@ export class HostClientCapabilityCoordinator {
632
687
  };
633
688
  }
634
689
  const { provider } = connection;
635
- if (connection.superseded) {
690
+ if (input.sessionId !== undefined &&
691
+ !provider.sessionRegistrations.has(input.sessionId) &&
692
+ provider.sessionRegistrations.size >= MAX_SESSION_REGISTRATIONS_PER_PROVIDER) {
693
+ return {
694
+ ok: false,
695
+ error: {
696
+ code: 'invalid_request',
697
+ message: 'Client Capability Session registration limit reached',
698
+ },
699
+ };
700
+ }
701
+ if (connection.superseded && input.sessionId === undefined) {
636
702
  return {
637
703
  ok: false,
638
704
  error: {
@@ -658,6 +724,12 @@ export class HostClientCapabilityCoordinator {
658
724
  throw new Error('A trusted capability provider may publish only path-independent session capabilities');
659
725
  }
660
726
  registration = freezeRegistration(provider.providerId, context.connectionId, input, provider.trustedProvider);
727
+ const overlapping = registration.sessionId === undefined
728
+ ? [...provider.sessionRegistrations.values()]
729
+ : provider.current
730
+ ? [provider.current]
731
+ : [];
732
+ assertRegistrationToolsDoNotOverlap(registration, overlapping);
661
733
  }
662
734
  catch (error) {
663
735
  return {
@@ -668,22 +740,43 @@ export class HostClientCapabilityCoordinator {
668
740
  },
669
741
  };
670
742
  }
671
- const previous = provider.current;
743
+ const previous = this.#currentRegistration(provider, registration.sessionId);
672
744
  const previousConnectionId = provider.activeConnectionId;
673
- if (previousConnectionId && previousConnectionId !== context.connectionId) {
674
- const previousConnection = this.#connections.get(previousConnectionId);
675
- if (previousConnection)
676
- previousConnection.superseded = true;
677
- this.#invocations.releaseConnection(previousConnectionId);
745
+ if (registration.sessionId !== undefined) {
746
+ provider.sessionRegistrations.set(registration.sessionId, registration);
747
+ }
748
+ else {
749
+ if (previousConnectionId && previousConnectionId !== context.connectionId) {
750
+ const previousConnection = this.#connections.get(previousConnectionId);
751
+ if (previousConnection)
752
+ previousConnection.superseded = true;
753
+ for (const retired of provider.registrations.values()) {
754
+ if (retired.sessionId === undefined && retired.connectionId === previousConnectionId) {
755
+ void this.#invocations.releaseRegistration(retired);
756
+ }
757
+ }
758
+ }
759
+ provider.activeConnectionId = context.connectionId;
760
+ provider.current = registration;
678
761
  }
679
- provider.activeConnectionId = context.connectionId;
680
- provider.current = registration;
681
762
  const currentContracts = new Set(registration.offersByContract.keys());
763
+ if (registration.sessionId !== undefined) {
764
+ // Discovery may have changed while this scope's connection was away.
765
+ // Republishing is a complete replacement, including previously lost
766
+ // contracts; keep other Sessions and legacy provider leases untouched.
767
+ const previousBindings = this.#sessions.get(registration.sessionId)?.sessionBindings;
768
+ const removed = new Set([...(previousBindings ?? [])]
769
+ .filter(([contractId, binding]) => binding.providerId === provider.providerId &&
770
+ binding.sessionId === registration.sessionId &&
771
+ !currentContracts.has(contractId))
772
+ .map(([contractId]) => contractId));
773
+ this.#retireBindings(provider.providerId, removed, registration.sessionId);
774
+ }
682
775
  if (previous) {
683
- this.#retireBindings(provider.providerId, new Set([...previous.offersByContract.keys()].filter((contractId) => !currentContracts.has(contractId))));
684
- this.#removeTurnBindings(provider.providerId, new Set([...previous.offersByContract.keys()].filter((contractId) => !currentContracts.has(contractId))));
776
+ this.#retireBindings(provider.providerId, new Set([...previous.offersByContract.keys()].filter((contractId) => !currentContracts.has(contractId))), registration.sessionId);
777
+ this.#removeTurnBindings(provider.providerId, new Set([...previous.offersByContract.keys()].filter((contractId) => !currentContracts.has(contractId))), registration.sessionId);
685
778
  }
686
- this.#restoreBindings(provider.providerId, currentContracts);
779
+ this.#restoreBindings(provider.providerId, currentContracts, registration.sessionId);
687
780
  provider.registrations.set(registration.registrationId, registration);
688
781
  this.#revision += 1;
689
782
  if (hasModelToolOffers(previous) || hasModelToolOffers(registration)) {
@@ -704,9 +797,11 @@ export class HostClientCapabilityCoordinator {
704
797
  #unregister(input, context) {
705
798
  return this.#activation.runMutation(async () => {
706
799
  const provider = this.#connections.get(context.connectionId)?.provider;
707
- if (provider?.activeConnectionId !== context.connectionId ||
708
- !provider.current ||
709
- provider.current.registrationId !== input.registrationId) {
800
+ const registration = provider?.registrations.get(input.registrationId);
801
+ if (!provider ||
802
+ !registration ||
803
+ registration.connectionId !== context.connectionId ||
804
+ this.#currentRegistration(provider, registration.sessionId) !== registration) {
710
805
  return {
711
806
  ok: false,
712
807
  error: {
@@ -715,10 +810,12 @@ export class HostClientCapabilityCoordinator {
715
810
  },
716
811
  };
717
812
  }
718
- const registration = provider.current;
719
- provider.current = undefined;
720
- this.#retireBindings(provider.providerId, new Set(registration.offersByContract.keys()));
721
- this.#removeTurnBindings(provider.providerId, new Set(registration.offersByContract.keys()));
813
+ if (registration.sessionId === undefined)
814
+ provider.current = undefined;
815
+ else
816
+ provider.sessionRegistrations.delete(registration.sessionId);
817
+ this.#retireBindings(provider.providerId, new Set(registration.offersByContract.keys()), registration.sessionId);
818
+ this.#removeTurnBindings(provider.providerId, new Set(registration.offersByContract.keys()), registration.sessionId);
722
819
  this.#revision += 1;
723
820
  if (hasModelToolOffers(registration))
724
821
  this.#onModelToolsChanged();
@@ -772,6 +869,7 @@ export class HostClientCapabilityCoordinator {
772
869
  toolSnapshot: () => snapshot,
773
870
  prepareTool: async (binding, args, options) => {
774
871
  const { selectedBinding, dynamicBinding } = resolveBinding(binding);
872
+ assertRegistrationSession(dynamicBinding.registration, options.context.sessionId);
775
873
  const prepared = this.#invocations.prepare(dynamicBinding.registration, dynamicBinding.tool, args, options.context, options.signal, options.timeoutMs ?? DEFAULT_CALL_TIMEOUT_MS);
776
874
  try {
777
875
  const evidence = await prepared.waitUntilAccepted();
@@ -782,7 +880,7 @@ export class HostClientCapabilityCoordinator {
782
880
  if (options.context.permissionMode !== 'ask' || !options.context.runId) {
783
881
  throw new Error('Client Capability is unavailable in the current permission mode');
784
882
  }
785
- const target = managedClientCapabilityGrantTarget(selectedBinding.contractId, dynamicBinding.registration, dynamicBinding.tool, evidence);
883
+ const target = managedClientCapabilityGrantTarget(selectedBinding.contractId, dynamicBinding.registration, dynamicBinding.tool, evidence, options.context.sessionId);
786
884
  if (!target) {
787
885
  return {
788
886
  execute: ({ emitProgress, requestInteraction } = {}) => prepared.admit(emitProgress, requestInteraction),
@@ -818,6 +916,7 @@ export class HostClientCapabilityCoordinator {
818
916
  },
819
917
  callTool: (binding, args, options) => {
820
918
  const { dynamicBinding } = resolveBinding(binding);
919
+ assertRegistrationSession(dynamicBinding.registration, options.context.sessionId);
821
920
  return this.#invocations.invoke(dynamicBinding.registration, dynamicBinding.tool, args, options.context, options.signal, options.timeoutMs ?? DEFAULT_CALL_TIMEOUT_MS, options.emitProgress, options.requestInteraction);
822
921
  },
823
922
  };
@@ -879,6 +978,7 @@ export class HostClientCapabilityCoordinator {
879
978
  ? { capabilityOwner: Object.freeze({ ...identity.capabilityOwner }) }
880
979
  : {}),
881
980
  registrations: new Map(),
981
+ sessionRegistrations: new Map(),
882
982
  };
883
983
  this.#providers.set(providerId, provider);
884
984
  }
@@ -892,16 +992,21 @@ export class HostClientCapabilityCoordinator {
892
992
  }
893
993
  return provider;
894
994
  }
895
- #eligibleOffersByContract() {
995
+ #eligibleOffersByContract(sessionId) {
896
996
  const eligible = new Map();
897
997
  for (const provider of this.#providers.values()) {
898
- const registration = provider.current;
899
- if (!this.#activeConnection(provider) || !registration)
900
- continue;
901
- for (const offer of registration.offersByContract.values()) {
902
- const candidates = eligible.get(offer.contractId) ?? [];
903
- candidates.push({ registration, offer });
904
- eligible.set(offer.contractId, candidates);
998
+ const registrations = [
999
+ provider.current,
1000
+ sessionId === undefined ? undefined : provider.sessionRegistrations.get(sessionId),
1001
+ ];
1002
+ for (const registration of registrations) {
1003
+ if (!registration || !this.#registrationConnection(registration))
1004
+ continue;
1005
+ for (const offer of registration.offersByContract.values()) {
1006
+ const candidates = eligible.get(offer.contractId) ?? [];
1007
+ candidates.push({ registration, offer });
1008
+ eligible.set(offer.contractId, candidates);
1009
+ }
905
1010
  }
906
1011
  }
907
1012
  for (const candidates of eligible.values()) {
@@ -925,31 +1030,33 @@ export class HostClientCapabilityCoordinator {
925
1030
  }
926
1031
  return { registration: candidate.registration, tool };
927
1032
  }
928
- #markBindingsLost(providerId, contracts) {
1033
+ #markBindingsLost(providerId, contracts, scopeSessionId) {
929
1034
  for (const [sessionId, state] of this.#sessions) {
930
1035
  const bindings = state.sessionBindings;
931
1036
  let next;
932
1037
  for (const [contractId, binding] of bindings) {
933
1038
  if (binding.kind !== 'bound' ||
934
1039
  binding.providerId !== providerId ||
1040
+ binding.sessionId !== scopeSessionId ||
935
1041
  (contracts && !contracts.has(contractId))) {
936
1042
  continue;
937
1043
  }
938
1044
  next ??= new Map(bindings);
939
- next.set(contractId, { kind: 'lost', providerId });
1045
+ next.set(contractId, { ...binding, kind: 'lost' });
940
1046
  }
941
1047
  if (next)
942
1048
  this.#storeSessionState(sessionId, { ...state, sessionBindings: next });
943
1049
  }
944
1050
  }
945
- #restoreBindings(providerId, contracts) {
1051
+ #restoreBindings(providerId, contracts, scopeSessionId) {
946
1052
  for (const [sessionId, state] of this.#sessions) {
947
1053
  const next = new Map(state.sessionBindings);
948
1054
  for (const [contractId, binding] of state.sessionBindings) {
949
1055
  if (binding.kind === 'lost' &&
950
1056
  binding.providerId === providerId &&
1057
+ binding.sessionId === scopeSessionId &&
951
1058
  contracts.has(contractId)) {
952
- next.set(contractId, { kind: 'bound', providerId });
1059
+ next.set(contractId, { ...binding, kind: 'bound' });
953
1060
  }
954
1061
  }
955
1062
  if (bindingMapsEqual(state.sessionBindings, next))
@@ -957,13 +1064,14 @@ export class HostClientCapabilityCoordinator {
957
1064
  this.#storeSessionState(sessionId, { ...state, sessionBindings: next });
958
1065
  }
959
1066
  }
960
- #retireBindings(providerId, contracts) {
1067
+ #retireBindings(providerId, contracts, scopeSessionId) {
961
1068
  for (const [sessionId, state] of this.#sessions) {
962
1069
  const bindings = state.sessionBindings;
963
1070
  const next = new Map(bindings);
964
1071
  for (const [contractId, binding] of bindings) {
965
- if (binding.kind === 'bound' &&
1072
+ if ((binding.kind === 'bound' || scopeSessionId !== undefined) &&
966
1073
  binding.providerId === providerId &&
1074
+ binding.sessionId === scopeSessionId &&
967
1075
  contracts.has(contractId)) {
968
1076
  next.delete(contractId);
969
1077
  }
@@ -973,13 +1081,14 @@ export class HostClientCapabilityCoordinator {
973
1081
  this.#storeSessionState(sessionId, { ...state, sessionBindings: next });
974
1082
  }
975
1083
  }
976
- #removeTurnBindings(providerId, contracts) {
1084
+ #removeTurnBindings(providerId, contracts, scopeSessionId) {
977
1085
  for (const [sessionId, state] of this.#sessions) {
978
1086
  const bindings = state.turnBindings;
979
1087
  const next = new Map(bindings);
980
1088
  for (const [contractId, binding] of bindings) {
981
1089
  if (binding.kind === 'bound' &&
982
1090
  binding.providerId === providerId &&
1091
+ binding.sessionId === scopeSessionId &&
983
1092
  (!contracts || contracts.has(contractId))) {
984
1093
  next.delete(contractId);
985
1094
  }
@@ -1015,7 +1124,7 @@ export class HostClientCapabilityCoordinator {
1015
1124
  }
1016
1125
  #releaseRegistrationIfUnused(registration) {
1017
1126
  const provider = this.#providers.get(registration.providerId);
1018
- if (provider?.current === registration ||
1127
+ if ((provider && this.#currentRegistration(provider, registration.sessionId) === registration) ||
1019
1128
  registration.snapshotRefs !== 0 ||
1020
1129
  this.#invocations.holdsRegistration(registration)) {
1021
1130
  return;
@@ -1037,8 +1146,28 @@ export class HostClientCapabilityCoordinator {
1037
1146
  : undefined;
1038
1147
  return connection?.provider === provider && !connection.superseded ? connection : undefined;
1039
1148
  }
1149
+ #currentRegistration(provider, sessionId) {
1150
+ return sessionId === undefined
1151
+ ? provider.current
1152
+ : provider.sessionRegistrations.get(sessionId);
1153
+ }
1154
+ #registrationConnection(registration) {
1155
+ const connection = this.#connections.get(registration.connectionId);
1156
+ if (!connection || connection.provider.providerId !== registration.providerId)
1157
+ return;
1158
+ // Session publications have their own owning connection. A replacement of
1159
+ // the legacy connection-wide publication cannot supersede those channels.
1160
+ if (registration.sessionId !== undefined)
1161
+ return connection;
1162
+ return this.#activeConnection(connection.provider) === connection ? connection : undefined;
1163
+ }
1164
+ #bindingRegistration(binding) {
1165
+ const provider = this.#providers.get(binding.providerId);
1166
+ return provider ? this.#currentRegistration(provider, binding.sessionId) : undefined;
1167
+ }
1040
1168
  #deleteProviderIfUnused(provider) {
1041
1169
  if (provider.current ||
1170
+ provider.sessionRegistrations.size > 0 ||
1042
1171
  provider.activeConnectionId ||
1043
1172
  provider.registrations.size > 0 ||
1044
1173
  [...this.#connections.values()].some((connection) => connection.provider === provider) ||
@@ -1060,7 +1189,23 @@ export class HostClientCapabilityCoordinator {
1060
1189
  function trustedClientToolActivityKind(descriptor) {
1061
1190
  return descriptor.activityKind;
1062
1191
  }
1192
+ function assertRegistrationSession(registration, sessionId) {
1193
+ if (registration.sessionId !== undefined && registration.sessionId !== sessionId) {
1194
+ throw new ClientCapabilityInvocationError('capability_lost', 'Client Capability belongs to another Session');
1195
+ }
1196
+ }
1197
+ function assertRegistrationToolsDoNotOverlap(registration, overlapping) {
1198
+ const names = new Set([...registration.offersByContract.values()].flatMap(({ offer }) => offer.tools.map((tool) => mcpProxyToolName(tool.serverId, tool.name))));
1199
+ for (const candidate of overlapping) {
1200
+ for (const { offer } of candidate.offersByContract.values()) {
1201
+ if (offer.tools.some((tool) => names.has(mcpProxyToolName(tool.serverId, tool.name)))) {
1202
+ throw new Error('Connection and Session capabilities expose conflicting model tool identities');
1203
+ }
1204
+ }
1205
+ }
1206
+ }
1063
1207
  function freezeRegistration(providerId, connectionId, input, trustedProvider) {
1208
+ input = decodeClientCapabilityReplaceInput(input);
1064
1209
  const offers = input.offers.map((offer) => Object.freeze({
1065
1210
  ...offer,
1066
1211
  tools: Object.freeze(offer.tools.map((tool) => Object.freeze({
@@ -1085,6 +1230,7 @@ function freezeRegistration(providerId, connectionId, input, trustedProvider) {
1085
1230
  toolsByIdentity.set(identity, {
1086
1231
  offerId: offer.offerId,
1087
1232
  hostPathAccess: offer.hostPathAccess,
1233
+ ...(offer.admission === undefined ? {} : { admission: offer.admission }),
1088
1234
  descriptor,
1089
1235
  });
1090
1236
  }
@@ -1105,14 +1251,30 @@ function freezeRegistration(providerId, connectionId, input, trustedProvider) {
1105
1251
  providerId,
1106
1252
  connectionId,
1107
1253
  registrationId: input.registrationId,
1254
+ ...(input.sessionId === undefined ? {} : { sessionId: input.sessionId }),
1108
1255
  trustedProvider,
1109
1256
  offersByContract,
1110
1257
  servicesByContract,
1111
1258
  snapshotRefs: 0,
1112
1259
  };
1113
1260
  }
1114
- function managedClientCapabilityGrantTarget(contractId, registration, tool, evidence) {
1261
+ function managedClientCapabilityGrantTarget(contractId, registration, tool, evidence, sessionId) {
1115
1262
  const { serverId, name: toolName } = tool.descriptor;
1263
+ if (tool.admission === 'mcp') {
1264
+ if (registration.sessionId !== sessionId ||
1265
+ tool.hostPathAccess !== 'none' ||
1266
+ evidence.kind !== 'none') {
1267
+ throw new Error('MCP admission requires its target Session and path-independent evidence');
1268
+ }
1269
+ return Object.freeze({
1270
+ providerId: registration.providerId,
1271
+ contractId,
1272
+ serverId,
1273
+ toolName,
1274
+ capability: 'mcp',
1275
+ scope: Object.freeze({ kind: 'mcp_tool', serverId, toolName }),
1276
+ });
1277
+ }
1116
1278
  if (!registration.trustedProvider) {
1117
1279
  throw new Error('Managed Client Capability requires a trusted Desktop provider');
1118
1280
  }
@@ -1203,6 +1365,7 @@ function capabilityGroupId(offer) {
1203
1365
  version: offer.version,
1204
1366
  affinity: offer.affinity,
1205
1367
  hostPathAccess: offer.hostPathAccess,
1368
+ admission: offer.admission,
1206
1369
  tools,
1207
1370
  }))
1208
1371
  .digest('hex')
@@ -1303,7 +1466,8 @@ function bindingMapsEqual(left, right) {
1303
1466
  const candidate = right.get(contractId);
1304
1467
  if (!candidate ||
1305
1468
  candidate.kind !== binding.kind ||
1306
- candidate.providerId !== binding.providerId) {
1469
+ candidate.providerId !== binding.providerId ||
1470
+ candidate.sessionId !== binding.sessionId) {
1307
1471
  return false;
1308
1472
  }
1309
1473
  }