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
@@ -17,6 +17,11 @@
17
17
  * under the License.
18
18
  */
19
19
  import { openai } from '@ai-sdk/openai';
20
+ import { CODEX_PATCH_DESCRIPTION, CODEX_PATCH_GRAMMAR } from './codex-v4a-patch.js';
21
+ export const codexApplyPatchProviderTool = openai.tools.customTool({
22
+ description: `${CODEX_PATCH_DESCRIPTION}\nPass raw patch text, not JSON.`,
23
+ format: { type: 'grammar', syntax: 'lark', definition: CODEX_PATCH_GRAMMAR },
24
+ });
20
25
  export const openAiApplyPatchProviderTool = openai.tools.applyPatch({});
21
26
  const inputSchema = openAiApplyPatchProviderTool.inputSchema;
22
27
  export const openAiApplyPatchInputSchema = typeof inputSchema === 'function' ? inputSchema() : inputSchema;
@@ -24,6 +24,8 @@ export class PluginExecutorBackend {
24
24
  sessionId;
25
25
  #cwd;
26
26
  #instructions;
27
+ #model;
28
+ #thinkingLevel;
27
29
  #binding;
28
30
  #newId;
29
31
  #now;
@@ -33,6 +35,8 @@ export class PluginExecutorBackend {
33
35
  this.sessionId = input.sessionId;
34
36
  this.#cwd = input.cwd;
35
37
  this.#instructions = input.instructions;
38
+ this.#model = input.model;
39
+ this.#thinkingLevel = input.thinkingLevel;
36
40
  this.#binding = input.binding;
37
41
  this.#newId = input.newId ?? randomUUID;
38
42
  this.#now = input.now ?? Date.now;
@@ -93,6 +97,8 @@ export class PluginExecutorBackend {
93
97
  conversationKey: this.sessionId,
94
98
  text: input.text,
95
99
  cwd: this.#cwd,
100
+ ...(this.#model ? { model: this.#model } : {}),
101
+ reasoningEffort: this.#thinkingLevel ?? null,
96
102
  ...(this.#instructions ? { instructions: this.#instructions } : {}),
97
103
  ...(input.attachments ? { attachments: input.attachments } : {}),
98
104
  ...(input.directoryReferences ? { directoryReferences: input.directoryReferences } : {}),
@@ -547,14 +547,27 @@ export class SessionManager {
547
547
  async recoverInterruptedSessions() {
548
548
  return this.recoverInterruptedSessionsWithPolicy({ kind: 'best_effort' });
549
549
  }
550
+ /**
551
+ * Recover only the Sessions named by an already-held quiescent operation.
552
+ *
553
+ * Bundle export uses this after fencing a Session subtree. A persisted open
554
+ * invocation can survive a Host restart even though no live execution claim
555
+ * remains; settling that invocation is safe under the fence and lets export
556
+ * distinguish an interrupted run from a live one without scanning or
557
+ * mutating unrelated Sessions.
558
+ */
559
+ async recoverInterruptedSessionsForSessions(sessionIds) {
560
+ const scope = new Set(sessionIds);
561
+ return this.recoverInterruptedSessionsWithPolicy({ kind: 'best_effort' }, scope);
562
+ }
550
563
  async recoverInterruptedSessionsStrict(stores) {
551
564
  if (stores.sessionStore !== this.deps.store || stores.agentRunStore !== this.deps.runStore) {
552
565
  throw new Error('Strict recovery stores must match the SessionManager composition');
553
566
  }
554
567
  return this.recoverInterruptedSessionsWithPolicy({ kind: 'strict', stores });
555
568
  }
556
- async recoverInterruptedSessionsWithPolicy(policy) {
557
- const interrupted = (await listSessionsForRecovery(this.deps.store, policy)).filter((session) => !session.isArchived);
569
+ async recoverInterruptedSessionsWithPolicy(policy, scope) {
570
+ const interrupted = (await listSessionsForRecovery(this.deps.store, policy)).filter((session) => !session.isArchived && (scope === undefined || scope.has(session.id)));
558
571
  const recovered = new Set();
559
572
  for (const session of interrupted) {
560
573
  if (this.runtimeKernel.hasActiveRuns(session.id))
@@ -140,12 +140,14 @@ function snapshotProvider(provider) {
140
140
  const call = provider.call?.bind(provider);
141
141
  const callService = provider.callService?.bind(provider);
142
142
  const close = provider.close?.bind(provider);
143
+ const currentRegistrationRetired = provider.currentRegistrationRetired?.bind(provider);
143
144
  return {
144
145
  offers: () => canonical.offers,
145
146
  ...(canonical.services === undefined ? {} : { services: () => canonical.services ?? [] }),
146
147
  ...(call ? { call } : {}),
147
148
  ...(callService ? { callService } : {}),
148
149
  ...(close ? { close } : {}),
150
+ ...(currentRegistrationRetired ? { currentRegistrationRetired } : {}),
149
151
  };
150
152
  }
151
153
  async function closeProvider(provider) {
@@ -24,74 +24,82 @@ export class ClientCapabilityChannel {
24
24
  #registrations = new Map();
25
25
  #invocations = new Map();
26
26
  #releasedRegistrationIds = new Set();
27
- #currentRegistrationId;
28
- #mutationPending = false;
27
+ #currentRegistrationIds = new Map();
28
+ #pendingMutations = new Set();
29
29
  #closedError;
30
30
  constructor(options) {
31
31
  this.#options = options;
32
32
  }
33
- async replace(provider, timeoutMs) {
33
+ async replace(provider, timeoutMs, sessionId) {
34
34
  this.#assertOpen();
35
- if (this.#mutationPending) {
35
+ if (this.#pendingMutations.has(sessionId)) {
36
36
  throw new Error('A Client Capability registration mutation is already pending');
37
37
  }
38
- this.#mutationPending = true;
38
+ this.#pendingMutations.add(sessionId);
39
39
  const registrationId = randomUUID();
40
40
  let registration;
41
41
  try {
42
42
  const services = provider.services?.() ?? [];
43
43
  const canonical = decodeClientCapabilityReplaceInput({
44
44
  registrationId,
45
+ ...(sessionId === undefined ? {} : { sessionId }),
45
46
  offers: provider.offers(),
46
47
  ...(services.length === 0 ? {} : { services }),
47
48
  });
48
49
  registration = {
49
50
  registrationId,
51
+ ...(canonical.sessionId === undefined ? {} : { sessionId: canonical.sessionId }),
50
52
  provider,
51
53
  offers: canonical.offers,
52
54
  services: canonical.services ?? [],
53
55
  };
54
56
  this.#registrations.set(registrationId, registration);
55
57
  const result = await this.#options.replace(canonical, timeoutMs);
58
+ this.#assertOpen();
56
59
  if (result.registrationId !== registrationId) {
57
60
  throw new Error('Runtime Host replaced a different Client Capability registration');
58
61
  }
59
- this.#currentRegistrationId = registrationId;
62
+ this.#currentRegistrationIds.set(sessionId, registrationId);
60
63
  this.#collectReleasedRegistrations();
61
64
  return result;
62
65
  }
63
66
  catch (error) {
64
- if (registration && this.#currentRegistrationId !== registrationId) {
67
+ if (registration && this.#currentRegistrationIds.get(sessionId) !== registrationId) {
65
68
  this.#registrations.delete(registrationId);
66
69
  }
67
70
  throw error;
68
71
  }
69
72
  finally {
70
- this.#mutationPending = false;
73
+ this.#pendingMutations.delete(sessionId);
74
+ this.#settleReleasedCurrentRegistration(sessionId);
75
+ this.#collectReleasedRegistrations();
71
76
  }
72
77
  }
73
- async unregister(timeoutMs) {
78
+ async unregister(timeoutMs, sessionId) {
74
79
  this.#assertOpen();
75
- if (this.#mutationPending) {
80
+ if (this.#pendingMutations.has(sessionId)) {
76
81
  throw new Error('A Client Capability registration mutation is already pending');
77
82
  }
78
- const registrationId = this.#currentRegistrationId;
83
+ const registrationId = this.#currentRegistrationIds.get(sessionId);
79
84
  if (!registrationId)
80
85
  throw new Error('No Client Capability registration is active');
81
- this.#mutationPending = true;
86
+ this.#pendingMutations.add(sessionId);
82
87
  try {
83
88
  const result = await this.#options.unregister({ registrationId }, timeoutMs);
89
+ this.#assertOpen();
84
90
  if (result.registrationId !== registrationId) {
85
91
  throw new Error('Runtime Host unregistered a different Client Capability registration');
86
92
  }
87
- if (this.#currentRegistrationId === registrationId) {
88
- this.#currentRegistrationId = undefined;
93
+ if (this.#currentRegistrationIds.get(sessionId) === registrationId) {
94
+ this.#currentRegistrationIds.delete(sessionId);
89
95
  }
90
96
  this.#collectReleasedRegistrations();
91
97
  return result;
92
98
  }
93
99
  finally {
94
- this.#mutationPending = false;
100
+ this.#pendingMutations.delete(sessionId);
101
+ this.#settleReleasedCurrentRegistration(sessionId);
102
+ this.#collectReleasedRegistrations();
95
103
  }
96
104
  }
97
105
  accept(frame) {
@@ -124,10 +132,17 @@ export class ClientCapabilityChannel {
124
132
  this.#invocations.delete(frame.invocationId);
125
133
  return;
126
134
  }
127
- case 'client.capability.registration_release':
135
+ case 'client.capability.registration_release': {
136
+ const registration = this.#registrations.get(frame.registrationId);
128
137
  this.#releasedRegistrationIds.add(frame.registrationId);
138
+ // A successful replacement can enqueue the old release before its response. Defer
139
+ // current-slot retirement until the in-flight mutation establishes which ID won.
140
+ if (registration && !this.#pendingMutations.has(registration.sessionId)) {
141
+ this.#settleReleasedCurrentRegistration(registration.sessionId);
142
+ }
129
143
  this.#collectReleasedRegistrations();
130
144
  return;
145
+ }
131
146
  case 'client.capability.admitted': {
132
147
  const invocation = this.#invocations.get(frame.invocationId);
133
148
  if (!invocation?.admission || !invocation.admission.resolve()) {
@@ -161,7 +176,7 @@ export class ClientCapabilityChannel {
161
176
  const providers = new Set([...this.#registrations.values()].map((registration) => registration.provider));
162
177
  this.#registrations.clear();
163
178
  this.#releasedRegistrationIds.clear();
164
- this.#currentRegistrationId = undefined;
179
+ this.#currentRegistrationIds.clear();
165
180
  for (const provider of providers)
166
181
  this.#closeProvider(provider);
167
182
  }
@@ -172,7 +187,11 @@ export class ClientCapabilityChannel {
172
187
  const registration = this.#registrations.get(frame.registrationId);
173
188
  const offer = registration?.offers.find((candidate) => candidate.offerId === frame.offerId);
174
189
  const offered = offer?.tools.some((tool) => tool.serverId === frame.serverId && tool.name === frame.toolName);
175
- if (!registration || !offer || !offered || !registration.provider.call) {
190
+ if (!registration ||
191
+ (registration.sessionId !== undefined && registration.sessionId !== frame.sessionId) ||
192
+ !offer ||
193
+ !offered ||
194
+ !registration.provider.call) {
176
195
  void this.#options
177
196
  .write({
178
197
  kind: 'client.capability.rejected',
@@ -403,10 +422,12 @@ export class ClientCapabilityChannel {
403
422
  }
404
423
  #collectReleasedRegistrations() {
405
424
  for (const registrationId of this.#releasedRegistrationIds) {
406
- if (registrationId === this.#currentRegistrationId)
425
+ if ([...this.#currentRegistrationIds.values()].includes(registrationId))
407
426
  continue;
408
- this.#releasedRegistrationIds.delete(registrationId);
409
427
  const registration = this.#registrations.get(registrationId);
428
+ if (registration && this.#pendingMutations.has(registration.sessionId))
429
+ continue;
430
+ this.#releasedRegistrationIds.delete(registrationId);
410
431
  if (!registration)
411
432
  continue;
412
433
  this.#registrations.delete(registrationId);
@@ -415,6 +436,16 @@ export class ClientCapabilityChannel {
415
436
  }
416
437
  }
417
438
  }
439
+ #settleReleasedCurrentRegistration(sessionId) {
440
+ const registrationId = this.#currentRegistrationIds.get(sessionId);
441
+ if (!registrationId || !this.#releasedRegistrationIds.has(registrationId))
442
+ return;
443
+ const registration = this.#registrations.get(registrationId);
444
+ this.#currentRegistrationIds.delete(sessionId);
445
+ this.#collectReleasedRegistrations();
446
+ if (registration)
447
+ this.#notifyCurrentRegistrationRetired(registration.provider);
448
+ }
418
449
  #closeProvider(provider) {
419
450
  try {
420
451
  void Promise.resolve(provider.close?.()).catch((error) => this.#options.onFailure(asError(error)));
@@ -423,6 +454,14 @@ export class ClientCapabilityChannel {
423
454
  this.#options.onFailure(asError(error));
424
455
  }
425
456
  }
457
+ #notifyCurrentRegistrationRetired(provider) {
458
+ try {
459
+ void Promise.resolve(provider.currentRegistrationRetired?.()).catch((error) => this.#options.onFailure(asError(error)));
460
+ }
461
+ catch (error) {
462
+ this.#options.onFailure(asError(error));
463
+ }
464
+ }
426
465
  #assertOpen() {
427
466
  if (this.#closedError)
428
467
  throw this.#closedError;
@@ -299,11 +299,13 @@ class RuntimeHostConnectionImpl {
299
299
  this.#fail(new RuntimeHostTransportError('closed', 'Runtime Host connection closed by Client'), true);
300
300
  await this.closed;
301
301
  }
302
- async replaceClientCapabilities(provider, timeoutMs = DEFAULT_HANDSHAKE_TIMEOUT_MS) {
303
- return this.#clientCapabilities.replace(provider, timeoutMs);
302
+ async replaceClientCapabilities(provider, options) {
303
+ const { timeoutMs = DEFAULT_HANDSHAKE_TIMEOUT_MS, sessionId } = typeof options === 'number' ? { timeoutMs: options } : (options ?? {});
304
+ return this.#clientCapabilities.replace(provider, timeoutMs, sessionId);
304
305
  }
305
- async unregisterClientCapabilities(timeoutMs = DEFAULT_HANDSHAKE_TIMEOUT_MS) {
306
- return this.#clientCapabilities.unregister(timeoutMs);
306
+ async unregisterClientCapabilities(options) {
307
+ const { timeoutMs = DEFAULT_HANDSHAKE_TIMEOUT_MS, sessionId } = typeof options === 'number' ? { timeoutMs: options } : (options ?? {});
308
+ return this.#clientCapabilities.unregister(timeoutMs, sessionId);
307
309
  }
308
310
  subscribeConfigurationChanges(listener) {
309
311
  this.#configurationChangeListeners.add(listener);
@@ -142,13 +142,13 @@ class RuntimeHostReconnectingConnectionImpl {
142
142
  openSessionSubscriptionOnce(input, timeoutMs) {
143
143
  return this.#requireCurrent('subscription.open').openSessionSubscription(input, timeoutMs);
144
144
  }
145
- async replaceClientCapabilities(provider, timeoutMs) {
145
+ async replaceClientCapabilities(provider, options) {
146
146
  const connection = this.#requireCurrent('client.capability.replace');
147
- return connection.replaceClientCapabilities(provider, timeoutMs);
147
+ return connection.replaceClientCapabilities(provider, options);
148
148
  }
149
- async unregisterClientCapabilities(timeoutMs) {
149
+ async unregisterClientCapabilities(options) {
150
150
  const connection = this.#requireCurrent('client.capability.unregister');
151
- return connection.unregisterClientCapabilities(timeoutMs);
151
+ return connection.unregisterClientCapabilities(options);
152
152
  }
153
153
  subscribeConfigurationChanges(listener) {
154
154
  this.#configurationListeners.add(listener);
@@ -59,7 +59,7 @@ export const CLIENT_CAPABILITY_OPERATION_SPECS = {
59
59
  };
60
60
  export function decodeClientCapabilityReplaceInput(value) {
61
61
  const record = requireRecord(value, 'Client Capability replacement');
62
- assertOptionalExactKeys(record, 'Client Capability replacement', ['registrationId', 'offers'], ['services']);
62
+ assertOptionalExactKeys(record, 'Client Capability replacement', ['registrationId', 'offers'], ['services', 'sessionId']);
63
63
  if (!Array.isArray(record.offers) || record.offers.length > CLIENT_CAPABILITY_MAX_OFFERS) {
64
64
  throw invalidProtocolFrame('Invalid Client Capability offers');
65
65
  }
@@ -67,11 +67,23 @@ export function decodeClientCapabilityReplaceInput(value) {
67
67
  if (!Array.isArray(serviceValues) || serviceValues.length > CLIENT_CAPABILITY_MAX_SERVICES) {
68
68
  throw invalidProtocolFrame('Invalid Client Capability services');
69
69
  }
70
- if (record.offers.length === 0 && serviceValues.length === 0) {
70
+ // An empty Session snapshot clears lost contracts after reconnect and keeps
71
+ // a bounded registration through which the Host can signal Session retirement.
72
+ // Connection-wide publications still use unregister for withdrawal.
73
+ if (record.offers.length === 0 && serviceValues.length === 0 && record.sessionId === undefined) {
71
74
  throw invalidProtocolFrame('Client Capability registration is empty');
72
75
  }
73
76
  const offers = record.offers.map((offer) => decodeClientCapabilityOffer(offer));
74
77
  const services = serviceValues.map((service) => decodeClientCapabilityServiceOffer(service));
78
+ const sessionId = record.sessionId === undefined ? undefined : requireEntityId(record.sessionId, 'sessionId');
79
+ if (sessionId !== undefined &&
80
+ (services.length > 0 ||
81
+ offers.some((offer) => offer.affinity !== 'session' || offer.hostPathAccess !== 'none'))) {
82
+ throw invalidProtocolFrame('Session capabilities must be path-independent session tools');
83
+ }
84
+ if (offers.some((offer) => offer.admission === 'mcp') && sessionId === undefined) {
85
+ throw invalidProtocolFrame('MCP admission requires a target Session');
86
+ }
75
87
  const offerIds = new Set();
76
88
  const serviceContracts = new Set();
77
89
  const toolIdentities = new Set();
@@ -102,6 +114,7 @@ export function decodeClientCapabilityReplaceInput(value) {
102
114
  }
103
115
  const decoded = {
104
116
  registrationId: requireEntityId(record.registrationId, 'registrationId'),
117
+ ...(sessionId === undefined ? {} : { sessionId }),
105
118
  offers,
106
119
  ...(record.services === undefined ? {} : { services }),
107
120
  };
@@ -427,17 +440,21 @@ function decodeClientCapabilityFormResult(value) {
427
440
  }
428
441
  function decodeClientCapabilityOffer(value) {
429
442
  const record = requireRecord(value, 'Client Capability offer');
430
- assertOptionalExactKeys(record, 'Client Capability offer', ['offerId', 'version', 'affinity', 'hostPathAccess', 'label', 'tools'], ['description']);
443
+ assertOptionalExactKeys(record, 'Client Capability offer', ['offerId', 'version', 'affinity', 'hostPathAccess', 'label', 'tools'], ['description', 'admission']);
431
444
  if (!Array.isArray(record.tools) ||
432
445
  record.tools.length === 0 ||
433
446
  record.tools.length > CLIENT_CAPABILITY_MAX_TOOLS_PER_OFFER) {
434
447
  throw invalidProtocolFrame('Invalid Client Capability offer tools');
435
448
  }
449
+ if (record.admission !== undefined && record.admission !== 'mcp') {
450
+ throw invalidProtocolFrame('Invalid Client Capability admission');
451
+ }
436
452
  return {
437
453
  offerId: requireEntityId(record.offerId, 'offerId'),
438
454
  version: requireString(record.version, 'version', 64),
439
455
  affinity: decodeClientCapabilityAffinity(record.affinity),
440
456
  hostPathAccess: decodeClientCapabilityHostPathAccess(record.hostPathAccess),
457
+ ...(record.admission === 'mcp' ? { admission: 'mcp' } : {}),
441
458
  label: requireString(record.label, 'label', 128),
442
459
  ...(record.description === undefined
443
460
  ? {}
@@ -120,15 +120,18 @@ export function decodeExternalSessionCatalogQueryResult(value) {
120
120
  return decoded;
121
121
  }
122
122
  export function decodeExternalSessionImportInput(value) {
123
- const input = requireExactRecord(value, 'external Session import input', [
124
- 'adapterId',
125
- 'sourceSessionId',
126
- ]);
123
+ const input = requireShapedRecord(value, 'external Session import input', ['adapterId', 'sourceSessionId'], ['workspace']);
127
124
  const sourceSessionId = requireUtf8String(input.sourceSessionId, 'external source Session id', EXTERNAL_SESSION_SOURCE_SESSION_ID_MAX_BYTES);
128
125
  if (/[\u0000-\u001f\u007f]/.test(sourceSessionId)) {
129
126
  throw invalidProtocolFrame('Invalid external source Session id');
130
127
  }
131
- return { adapterId: adapterId(input.adapterId), sourceSessionId };
128
+ return {
129
+ adapterId: adapterId(input.adapterId),
130
+ sourceSessionId,
131
+ ...(Object.hasOwn(input, 'workspace')
132
+ ? { workspace: decodeWorkspaceTarget(input.workspace) }
133
+ : {}),
134
+ };
132
135
  }
133
136
  export function decodeExternalSessionImportResult(value) {
134
137
  const result = requireRecord(value, 'external Session import result');
@@ -62,7 +62,17 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1;
62
62
  export const RUNTIME_HOST_PROTOCOL_VERSION = 0;
63
63
  // Increment when the same protocol version no longer guarantees safe Client-Host
64
64
  // interoperability. Mismatches are rejected before domain commands are admitted.
65
- export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 172;
65
+ export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 177;
66
+ // 177: External Session import input may carry an optional Host-resolved
67
+ // workspace target. Epoch-176 peers reject the unknown `workspace` key.
68
+ // 176: Session-scoped capability publication and MCP admission require compatible
69
+ // Client and Host builds; older peers do not enforce their isolation contract.
70
+ // 175: WorkHub coordinator model configuration again accepts only native
71
+ // explicit targets. Epoch-174 peers may send an executor target it must reject.
72
+ // 174: WorkHub new-Work defaults may select an executor-specific model and
73
+ // thinking level. Epoch-173 peers reject these fields on strict action shapes.
74
+ // 173: Plugin executor Sessions may select an executor-specific model at
75
+ // creation and configuration time, and propagate its reasoning effort.
66
76
  // 172: Usage screen requests and results accept fractional timestamps in the
67
77
  // persisted domain. Older peers reject these otherwise valid wire values.
68
78
  // 171: Removed the `connection.usage.read` operation along with the Command
@@ -20,7 +20,7 @@ import { isCollaborationMode } from '@maka/core/collaboration';
20
20
  import { isOrchestrationMode } from '@maka/core/orchestration';
21
21
  import { isPermissionMode } from '@maka/core/permission';
22
22
  import { isSessionStartMode } from '@maka/core/session-start-mode';
23
- import { isSessionBlockedReason, isSessionToolProfile, } from '@maka/core/session';
23
+ import { isSessionBlockedReason, isSessionToolProfile, SESSION_MODEL_ID_MAX_BYTES, } from '@maka/core/session';
24
24
  import { isThinkingLevel } from '@maka/core/model-thinking';
25
25
  import { isExecutorId } from '@maka/core/executor-id';
26
26
  import { assertAllowedKeys, requireCount, requireEncodedByteLimit, requireEntityId, requireExactRecord, requireRecord, requireShapedRecord, requireUtf8String, } from './codec.js';
@@ -36,7 +36,7 @@ export const SESSION_CATALOG_NAME_MAX_BYTES = 320;
36
36
  export const SESSION_CATALOG_LABEL_MAX_ITEMS = 32;
37
37
  export const SESSION_CATALOG_LABEL_MAX_BYTES = 128;
38
38
  export const SESSION_CATALOG_PREVIEW_MAX_BYTES = 4 * 1024;
39
- export const SESSION_CATALOG_MODEL_MAX_BYTES = 512;
39
+ export const SESSION_CATALOG_MODEL_MAX_BYTES = SESSION_MODEL_ID_MAX_BYTES;
40
40
  export const SESSION_CATALOG_CONNECTION_SLUG_MAX_BYTES = 256;
41
41
  export const SESSION_CATALOG_LIVE_RUN_STATE_SCHEMA_VERSION = 1;
42
42
  export const SESSION_CATALOG_RUNNING_TURN_MAX_ITEMS = 64;
@@ -251,6 +251,7 @@ export function decodeSessionCreateInput(value) {
251
251
  'labels',
252
252
  'modelTarget',
253
253
  'executorId',
254
+ 'executorModel',
254
255
  'thinkingLevel',
255
256
  'toolProfile',
256
257
  'permissionMode',
@@ -261,9 +262,15 @@ export function decodeSessionCreateInput(value) {
261
262
  ? executorIdValue(input.executorId)
262
263
  : undefined;
263
264
  const target = Object.hasOwn(input, 'modelTarget') ? modelTarget(input.modelTarget) : undefined;
265
+ const executorModel = Object.hasOwn(input, 'executorModel')
266
+ ? requireUtf8String(input.executorModel, 'Executor model', SESSION_CATALOG_MODEL_MAX_BYTES)
267
+ : undefined;
264
268
  if ((executorId === undefined) === (target === undefined)) {
265
269
  throw invalidProtocolFrame('Session creation requires exactly one model target or executor id');
266
270
  }
271
+ if (executorModel !== undefined && executorId === undefined) {
272
+ throw invalidProtocolFrame('Executor model requires an executor id');
273
+ }
267
274
  return {
268
275
  sessionId: requireEntityId(input.sessionId, 'sessionId'),
269
276
  workspace: decodeWorkspaceTarget(input.workspace),
@@ -272,6 +279,7 @@ export function decodeSessionCreateInput(value) {
272
279
  ...(Object.hasOwn(input, 'labels') ? { labels: labels(input.labels) } : {}),
273
280
  ...(target ? { modelTarget: target } : {}),
274
281
  ...(executorId ? { executorId } : {}),
282
+ ...(executorModel ? { executorModel } : {}),
275
283
  ...(Object.hasOwn(input, 'thinkingLevel')
276
284
  ? { thinkingLevel: input.thinkingLevel === null ? null : thinkingLevel(input.thinkingLevel) }
277
285
  : {}),
@@ -327,10 +335,20 @@ export function decodeSessionConfigurationUpdateInput(value) {
327
335
  'expectedRevision',
328
336
  'patch',
329
337
  ]);
330
- const patch = requireShapedRecord(input.patch, 'Session configuration patch', [], ['modelTarget', 'thinkingLevel', 'permissionMode', 'collaborationMode', 'orchestrationMode']);
338
+ const patch = requireShapedRecord(input.patch, 'Session configuration patch', [], [
339
+ 'modelTarget',
340
+ 'executorTarget',
341
+ 'thinkingLevel',
342
+ 'permissionMode',
343
+ 'collaborationMode',
344
+ 'orchestrationMode',
345
+ ]);
331
346
  if (Object.keys(patch).length === 0) {
332
347
  throw invalidProtocolFrame('Session configuration patch is empty');
333
348
  }
349
+ if (Object.hasOwn(patch, 'modelTarget') && Object.hasOwn(patch, 'executorTarget')) {
350
+ throw invalidProtocolFrame('Session configuration cannot select two execution targets');
351
+ }
334
352
  return {
335
353
  sessionId: requireEntityId(input.sessionId, 'sessionId'),
336
354
  expectedRevision: positiveRevision(input.expectedRevision, 'expected Session revision'),
@@ -338,6 +356,9 @@ export function decodeSessionConfigurationUpdateInput(value) {
338
356
  ...(Object.hasOwn(patch, 'modelTarget')
339
357
  ? { modelTarget: explicitModelTarget(patch.modelTarget) }
340
358
  : {}),
359
+ ...(Object.hasOwn(patch, 'executorTarget')
360
+ ? { executorTarget: executorTarget(patch.executorTarget) }
361
+ : {}),
341
362
  ...(Object.hasOwn(patch, 'thinkingLevel')
342
363
  ? {
343
364
  thinkingLevel: patch.thinkingLevel === null ? null : thinkingLevel(patch.thinkingLevel),
@@ -659,6 +680,17 @@ function executorIdValue(value) {
659
680
  }
660
681
  return id;
661
682
  }
683
+ function executorTarget(value) {
684
+ const exact = requireShapedRecord(value, 'Session executor target', ['executorId'], ['model']);
685
+ return {
686
+ executorId: executorIdValue(exact.executorId),
687
+ ...(Object.hasOwn(exact, 'model')
688
+ ? {
689
+ model: requireUtf8String(exact.model, 'Executor model', SESSION_CATALOG_MODEL_MAX_BYTES),
690
+ }
691
+ : {}),
692
+ };
693
+ }
662
694
  function optionalExecutorId(record) {
663
695
  if (record.executorId === undefined)
664
696
  return {};
@@ -25,11 +25,7 @@ import { defineOperation } from './operation-spec.js';
25
25
  import { decodeWorkspaceProjection, decodeWorkspaceTarget, } from './workspace.js';
26
26
  import { decodeSessionConfigurationUpdateInput, decodeSessionUpdateResult, decodeSessionCatalogItem, SESSION_CATALOG_OPERATION_SPECS, } from './session-catalog.js';
27
27
  export function decodeWorkHubCoordinationConfigureModelInput(value) {
28
- const input = requireExactRecord(value, 'WorkHub model configuration', [
29
- 'expectedRevision',
30
- 'modelTarget',
31
- 'thinkingLevel',
32
- ]);
28
+ const input = requireShapedRecord(value, 'WorkHub model configuration', ['expectedRevision', 'modelTarget', 'thinkingLevel'], []);
33
29
  const decoded = decodeSessionConfigurationUpdateInput({
34
30
  sessionId: WORKHUB_COORDINATION_SESSION_ID,
35
31
  expectedRevision: input.expectedRevision,