maka-agent 0.2.0-dev.52.20260926 → 0.2.0-dev.53.20260927

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 (71) hide show
  1. package/dist/activation-command.js +11 -3
  2. package/dist/pi-tui-mcp-status.js +73 -5
  3. package/dist/pi-tui-runner.js +1 -1
  4. package/dist/runtime-host-run-command.js +39 -6
  5. package/dist/runtime-host-run-mcp.js +161 -0
  6. package/dist/runtime-host-session-driver.js +22 -2
  7. package/dist/tui-copy-catalog.js +6 -0
  8. package/dist/tui-mcp-control.js +11 -2
  9. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  10. package/node_modules/@maka/core/dist/attachments.js +7 -0
  11. package/node_modules/@maka/core/dist/events.js +11 -0
  12. package/node_modules/@maka/core/dist/provider-registry.js +11 -2
  13. package/node_modules/@maka/core/dist/turn-origin.js +6 -0
  14. package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +1 -5
  15. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +80 -24
  16. package/node_modules/@maka/runtime/dist/bots/proxied-fetch.js +5 -4
  17. package/node_modules/@maka/runtime/dist/computer-use-tools.js +136 -100
  18. package/node_modules/@maka/runtime/dist/context-budget-helpers.js +4 -0
  19. package/node_modules/@maka/runtime/dist/conversation-copy.js +51 -205
  20. package/node_modules/@maka/runtime/dist/edit-replace.js +7 -7
  21. package/node_modules/@maka/runtime/dist/filesystem-executor.js +4 -20
  22. package/node_modules/@maka/runtime/dist/filesystem-worker/operations.js +17 -28
  23. package/node_modules/@maka/runtime/dist/format-json.js +39 -0
  24. package/node_modules/@maka/runtime/dist/history-compact-checkpoint.js +1 -6
  25. package/node_modules/@maka/runtime/dist/image-file.js +22 -14
  26. package/node_modules/@maka/runtime/dist/model-adapter.js +18 -2
  27. package/node_modules/@maka/runtime/dist/model-history.js +201 -3
  28. package/node_modules/@maka/runtime/dist/network/abortable-connector.js +55 -0
  29. package/node_modules/@maka/runtime/dist/network/proxy-dispatcher.js +72 -37
  30. package/node_modules/@maka/runtime/dist/network/proxy-test.js +1 -1
  31. package/node_modules/@maka/runtime/dist/network/scoped-fetch-transport.js +7 -2
  32. package/node_modules/@maka/runtime/dist/openai-codex-history-compactor.js +1 -3
  33. package/node_modules/@maka/runtime/dist/provider-error-classification.js +36 -8
  34. package/node_modules/@maka/runtime/dist/recovery-resolver.js +1 -91
  35. package/node_modules/@maka/runtime/dist/runtime-kernel.js +44 -0
  36. package/node_modules/@maka/runtime/dist/session-manager.js +54 -58
  37. package/node_modules/@maka/runtime/dist/subscription-model-fetch.js +84 -2
  38. package/node_modules/@maka/runtime/dist/workers/filesystem-worker.js +63 -51
  39. package/node_modules/@maka/runtime/dist/workspace-executor.js +5 -6
  40. package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +27 -6
  41. package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +4 -1
  42. package/node_modules/@maka/runtime-host/dist/client/host-handoff.js +9 -9
  43. package/node_modules/@maka/runtime-host/dist/client/session-catalog-summary.js +9 -1
  44. package/node_modules/@maka/runtime-host/dist/protocol/index.js +12 -1
  45. package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +28 -6
  46. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +12 -2
  47. package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +10 -7
  48. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +10 -2
  49. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +2 -0
  50. package/node_modules/@maka/runtime-host/dist/server/interactive-turn-coordinator.js +4 -1
  51. package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +4 -2
  52. package/node_modules/@maka/runtime-host/dist/server/operation-dispatcher.js +4 -0
  53. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +6 -5
  54. package/node_modules/@maka/runtime-host/dist/server/safe-boundary-resume-policy.js +27 -0
  55. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +5 -3
  56. package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +7 -2
  57. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +11 -141
  58. package/node_modules/@maka/runtime-host/dist/server/session-revision-graph-references.js +46 -75
  59. package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +18 -2
  60. package/node_modules/@maka/runtime-host/dist/server/workhub-result-coordinator.js +33 -3
  61. package/node_modules/@maka/runtime-host/dist/transport/framed-byte-stream-transport.js +39 -5
  62. package/node_modules/@maka/storage/dist/agent-run-store-contract.js +7 -1
  63. package/node_modules/@maka/storage/dist/artifact-store.js +1 -5
  64. package/node_modules/@maka/storage/dist/artifact-stores.js +0 -3
  65. package/node_modules/@maka/storage/dist/atomic-file-write.js +1 -0
  66. package/node_modules/@maka/storage/dist/claude-code-session-adapter.js +107 -57
  67. package/node_modules/@maka/storage/dist/codex-session-adapter.js +227 -19
  68. package/node_modules/@maka/storage/dist/external-session-importer.js +1 -1
  69. package/node_modules/@maka/storage/dist/mcp-config-store.js +22 -3
  70. package/node_modules/@maka/storage/dist/settings-store.js +281 -13
  71. package/package.json +1 -1
@@ -321,7 +321,11 @@ export async function runMakaActivationCli(argv, overrides = {}) {
321
321
  }, options.timeoutMs);
322
322
  try {
323
323
  const text = activationStimulusText(request.stimulus);
324
- const stream = await activationStream(context.runtime, session.id, { turnId: deps.newId(), text }, existing !== undefined);
324
+ const stream = await activationStream(context.runtime, session.id, {
325
+ turnId: deps.newId(),
326
+ text,
327
+ origin: { kind: 'cloud_activation', activationId: request.activationId },
328
+ }, existing !== undefined && deps.automatedResumeEnabled());
325
329
  const drain = (async () => {
326
330
  for await (const event of stream) {
327
331
  if (sessionEventSandboxBoundaryFailureReason(event))
@@ -504,8 +508,8 @@ function activationStimulusText(stimulus) {
504
508
  }
505
509
  return JSON.stringify({ type: stimulus.type, payload });
506
510
  }
507
- async function activationStream(runtime, sessionId, input, allowSafeBoundaryResume) {
508
- if (allowSafeBoundaryResume && runtime.resumeLatest) {
511
+ async function activationStream(runtime, sessionId, input, allowAutomatedResume) {
512
+ if (allowAutomatedResume && runtime.resumeLatest) {
509
513
  const resumed = await runtime.resumeLatest(sessionId);
510
514
  if (resumed)
511
515
  return resumed;
@@ -562,6 +566,10 @@ function makaActivateHelpText() {
562
566
  }
563
567
  function defaultMakaActivationDeps() {
564
568
  return {
569
+ automatedResumeEnabled: () => {
570
+ const value = process.env.MAKA_RUNTIME_SAFE_BOUNDARY_RESUME;
571
+ return value === '1' || value === 'true';
572
+ },
565
573
  createContext: createRuntimeHostActivationContext,
566
574
  listSessions: listRuntimeHostActivationSessions,
567
575
  workspaceRoot: () => resolve(process.cwd()),
@@ -16,7 +16,7 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { Editor, Input, Key, matchesKey, truncateToWidth, visibleWidth, } from '@earendil-works/pi-tui';
19
+ import { Editor, Input, Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from '@earendil-works/pi-tui';
20
20
  import { generalizedErrorMessageForLocale } from '@maka/core/redaction';
21
21
  import { defineUiMessageCatalog, formatUiMessage, resolveUiMessageCatalog, } from '@maka/core/ui-locale';
22
22
  import { normalizeMcpConfig } from '@maka/storage/mcp-config-store';
@@ -40,6 +40,7 @@ export class McpManagementOverlay {
40
40
  editor;
41
41
  closed = false;
42
42
  actionAttempt = 0;
43
+ pendingConfigErrorPath;
43
44
  constructor(input) {
44
45
  this.input = input;
45
46
  this.dispose = input.surface?.subscribe(input.onChange) ?? (() => undefined);
@@ -59,11 +60,9 @@ export class McpManagementOverlay {
59
60
  }
60
61
  if (this.phase.kind === 'busy') {
61
62
  if (matchesKey(data, Key.escape)) {
62
- this.actionAttempt += 1;
63
63
  this.backToList();
64
64
  }
65
65
  else if (matchesKey(data, 'q')) {
66
- this.actionAttempt += 1;
67
66
  this.close();
68
67
  }
69
68
  return;
@@ -75,7 +74,9 @@ export class McpManagementOverlay {
75
74
  this.backToList();
76
75
  return;
77
76
  }
78
- if (this.phase.kind === 'list')
77
+ if (this.phase.kind === 'config_error')
78
+ this.handleTextScroll(data);
79
+ else if (this.phase.kind === 'list')
79
80
  this.handleListInput(data);
80
81
  else if (this.phase.kind === 'add_choice')
81
82
  this.handleAddChoice(data);
@@ -134,6 +135,14 @@ export class McpManagementOverlay {
134
135
  handleListInput(data) {
135
136
  const snapshot = this.input.surface?.snapshot();
136
137
  const servers = snapshot?.servers ?? [];
138
+ if (snapshot?.initialization === 'error') {
139
+ this.handleTextScroll(data);
140
+ return;
141
+ }
142
+ // A ready empty list still has explanatory text below the publication
143
+ // status, which must remain reachable in a short terminal.
144
+ if (servers.length === 0 && this.handleTextScroll(data))
145
+ return;
137
146
  if (matchesKey(data, Key.up)) {
138
147
  this.selected = clamp(this.selected - 1, 0, servers.length - 1);
139
148
  }
@@ -187,6 +196,25 @@ export class McpManagementOverlay {
187
196
  }
188
197
  this.input.onChange();
189
198
  }
199
+ handleTextScroll(data) {
200
+ if (matchesKey(data, Key.up))
201
+ this.top -= 1;
202
+ else if (matchesKey(data, Key.down))
203
+ this.top += 1;
204
+ else if (matchesKey(data, Key.pageUp))
205
+ this.top -= Math.max(1, this.bodyRows);
206
+ else if (matchesKey(data, Key.pageDown))
207
+ this.top += Math.max(1, this.bodyRows);
208
+ else if (matchesKey(data, Key.home))
209
+ this.top = 0;
210
+ else if (matchesKey(data, Key.end))
211
+ this.top = this.maxTop();
212
+ else
213
+ return false;
214
+ this.top = clamp(this.top, 0, this.maxTop());
215
+ this.input.onChange();
216
+ return true;
217
+ }
190
218
  handleAddChoice(data) {
191
219
  if (matchesKey(data, 'g'))
192
220
  this.startInput('server_id', { serverId: '' });
@@ -352,8 +380,24 @@ export class McpManagementOverlay {
352
380
  }
353
381
  if (this.closed || attempt !== this.actionAttempt)
354
382
  return;
383
+ if (result.status === 'failed' && result.reason === 'invalid-config-file') {
384
+ // Esc dismisses the busy view, not the operation or its file diagnostic.
385
+ // Defer presentation while the user is editing another form.
386
+ this.pendingConfigErrorPath = result.path;
387
+ this.showPendingConfigError();
388
+ this.input.onChange();
389
+ return;
390
+ }
391
+ if (result.status === 'applied' &&
392
+ ['add', 'edit', 'commit_import', 'set_enabled', 'remove'].includes(action.kind)) {
393
+ this.pendingConfigErrorPath = undefined;
394
+ }
395
+ // Do not restore a dismissed busy view for ordinary completion results.
396
+ if (this.phase.kind !== 'busy')
397
+ return;
355
398
  this.phase = { kind: 'list' };
356
399
  this.notice = actionNotice(result, this.input.locale);
400
+ this.showPendingConfigError();
357
401
  this.input.onChange();
358
402
  }
359
403
  document(width) {
@@ -361,6 +405,9 @@ export class McpManagementOverlay {
361
405
  const snapshot = this.input.surface?.snapshot();
362
406
  if (!snapshot)
363
407
  return unavailableDocument(this.input.locale);
408
+ if (this.phase.kind === 'config_error') {
409
+ return wrapTextWithAnsi(invalidConfigFileCopy(this.input.locale, this.phase.path), width);
410
+ }
364
411
  if (this.phase.kind === 'input')
365
412
  return this.inputDocument(width);
366
413
  const editor = MCP_STATUS_COPY[this.input.locale].editor;
@@ -407,7 +454,11 @@ export class McpManagementOverlay {
407
454
  if (snapshot.initialization === 'loading')
408
455
  return [...lines, loadingCopy(this.input.locale)];
409
456
  if (snapshot.initialization === 'error') {
410
- return [...lines, ansi.red(loadErrorCopy(this.input.locale))];
457
+ lines.push(ansi.red(loadErrorCopy(this.input.locale)));
458
+ if (snapshot.invalidConfigPath) {
459
+ lines.push(...wrapTextWithAnsi(invalidConfigFileCopy(this.input.locale, snapshot.invalidConfigPath), width));
460
+ }
461
+ return lines;
411
462
  }
412
463
  if (snapshot.servers.length === 0)
413
464
  return [...lines, '', emptyCopy(this.input.locale)];
@@ -436,8 +487,12 @@ export class McpManagementOverlay {
436
487
  }
437
488
  footer() {
438
489
  const copy = MCP_STATUS_COPY[this.input.locale].footer;
490
+ if (this.phase.kind === 'config_error')
491
+ return copy.diagnostic;
439
492
  if (this.phase.kind !== 'list')
440
493
  return copy.back;
494
+ if (this.input.surface?.snapshot().initialization === 'error')
495
+ return copy.readOnly;
441
496
  if (!this.management())
442
497
  return copy.readOnly;
443
498
  return this.input.surface?.snapshot().canManagePublicationCredential
@@ -452,8 +507,18 @@ export class McpManagementOverlay {
452
507
  this.phase = { kind: 'list' };
453
508
  if (clearNotice)
454
509
  this.notice = undefined;
510
+ this.showPendingConfigError();
455
511
  this.input.onChange();
456
512
  }
513
+ showPendingConfigError() {
514
+ if (this.pendingConfigErrorPath === undefined ||
515
+ (this.phase.kind !== 'list' && this.phase.kind !== 'busy'))
516
+ return;
517
+ this.phase = { kind: 'config_error', path: this.pendingConfigErrorPath };
518
+ this.pendingConfigErrorPath = undefined;
519
+ this.notice = undefined;
520
+ this.top = 0;
521
+ }
457
522
  clearEditor() {
458
523
  if (!this.editor)
459
524
  return;
@@ -624,6 +689,9 @@ function actionNotice(result, locale) {
624
689
  function resultCopy(locale, code) {
625
690
  return MCP_STATUS_COPY[locale].editor.results[code] ?? code;
626
691
  }
692
+ function invalidConfigFileCopy(locale, path) {
693
+ return formatUiMessage(MCP_STATUS_COPY[locale].invalidConfigFile, { path: path.replace(/[\u0000-\u001f\u007f-\u009f]/gu, '') }, locale);
694
+ }
627
695
  function confirmAddDocument(draft, locale) {
628
696
  const editor = MCP_STATUS_COPY[locale].editor;
629
697
  return [
@@ -111,7 +111,7 @@ export function safeBoundaryResumeParkedCopy(reason) {
111
111
  case 'resume_feature_disabled':
112
112
  return {
113
113
  level: 'info',
114
- text: 'Safe-boundary resume is not enabled on this runtime (set MAKA_RUNTIME_SAFE_BOUNDARY_RESUME=1 to enable).',
114
+ text: 'Safe-boundary resume is disabled by this runtime policy.',
115
115
  };
116
116
  case 'resume_candidate_missing':
117
117
  return {
@@ -18,11 +18,12 @@
18
18
  */
19
19
  import { failureClassFromCompleteStopReason } from '@maka/core/events';
20
20
  import { findProjectByIdentity } from '@maka/core/project';
21
- import { projectSessionCatalogSummary, readRuntimeHostSessions, readRuntimeHostProjects, RuntimeHostOperationError, } from '@maka/runtime-host/client';
21
+ import { projectSessionCatalogSummary, readRuntimeHostSessions, readRuntimeHostProjects, RuntimeHostOperationError, isRuntimeHostReconnectingConnection, } from '@maka/runtime-host/client';
22
22
  import { runtimeHostProfileUsesHostWorkspace } from '@maka/runtime-host/profile-kind';
23
23
  import { runMakaTextCliCore, } from './run-command-core.js';
24
24
  import { connectRuntimeHostCli, resolveRuntimeHostCliTarget, } from './runtime-host-cli-context.js';
25
25
  import { createRuntimeHostMakaSessionDriver, } from './runtime-host-session-driver.js';
26
+ import { RuntimeHostRunMcp } from './runtime-host-run-mcp.js';
26
27
  import { formatRuntimeHostCliTaskBlockers, isRuntimeHostCliTaskBlocked, readRuntimeHostCliTaskReadiness, } from './runtime-host-task-readiness.js';
27
28
  import { resolveMakaClientDataRoot } from './workspace-root.js';
28
29
  const GRAPH_POLL_INTERVAL_MS = 25;
@@ -70,6 +71,10 @@ export function createRuntimeHostRunContext(connection, catalog, input, override
70
71
  createDriver: createRuntimeHostMakaSessionDriver,
71
72
  ...overrides,
72
73
  };
74
+ const mcp = (!input.hostProfileId || input.hostProfileId === 'local') &&
75
+ isRuntimeHostReconnectingConnection(connection)
76
+ ? new RuntimeHostRunMcp(input.workspaceRoot, connection)
77
+ : undefined;
73
78
  const driver = contextDeps.createDriver({
74
79
  connection,
75
80
  cwd: input.cwd,
@@ -80,8 +85,9 @@ export function createRuntimeHostRunContext(connection, catalog, input, override
80
85
  ? { kind: 'client_path' }
81
86
  : { kind: 'host' },
82
87
  ...(input.projectId ? { workspace: { kind: 'project', projectId: input.projectId } } : {}),
88
+ ...(mcp ? { prepareSession: (sessionId) => mcp.prepare(sessionId) } : {}),
83
89
  });
84
- const runtime = new RuntimeHostRunRuntime(connection, driver, input.runOutcomeObserver, input.enableAgentGraph === true, input.sessionCwdOverride, input.maxSteps);
90
+ const runtime = new RuntimeHostRunRuntime(connection, driver, mcp, input.runOutcomeObserver, input.enableAgentGraph === true, input.sessionCwdOverride, input.maxSteps);
85
91
  return {
86
92
  runtime,
87
93
  target: { connection: { slug: target.connection.slug }, model: target.model },
@@ -129,6 +135,7 @@ async function prepareRuntimeHostRunInput(connection, catalog, profile, input, c
129
135
  class RuntimeHostRunRuntime {
130
136
  #connection;
131
137
  #driver;
138
+ #mcp;
132
139
  #observer;
133
140
  #graphEnabled;
134
141
  #sessionCwdOverride;
@@ -142,9 +149,10 @@ class RuntimeHostRunRuntime {
142
149
  #graphAdmissionTurnIds = new Set();
143
150
  #latestTranscriptReplacement;
144
151
  #graphTerminalWaiters = new Map();
145
- constructor(connection, driver, observer, graphEnabled, sessionCwdOverride, maxSteps) {
152
+ constructor(connection, driver, mcp, observer, graphEnabled, sessionCwdOverride, maxSteps) {
146
153
  this.#connection = connection;
147
154
  this.#driver = driver;
155
+ this.#mcp = mcp;
148
156
  this.#observer = observer;
149
157
  this.#graphEnabled = graphEnabled;
150
158
  this.#sessionCwdOverride = sessionCwdOverride;
@@ -166,6 +174,7 @@ class RuntimeHostRunRuntime {
166
174
  }
167
175
  async *sendMessage(sessionId, input) {
168
176
  await this.#attach(sessionId);
177
+ await this.#mcp?.ready();
169
178
  if (this.#stopRequested)
170
179
  throw new Error('Turn was cancelled before start');
171
180
  if (input.turnOrchestration?.mode === 'graph') {
@@ -176,6 +185,7 @@ class RuntimeHostRunRuntime {
176
185
  turnId: input.turnId,
177
186
  ...(input.turnOrchestration ? { turnOrchestration: input.turnOrchestration } : {}),
178
187
  ...(maxSteps !== undefined ? { maxSteps } : {}),
188
+ ...(input.origin !== undefined ? { origin: input.origin } : {}),
179
189
  });
180
190
  if (!turn.runId)
181
191
  throw new Error('Runtime Host did not return a Run identity');
@@ -205,8 +215,30 @@ class RuntimeHostRunRuntime {
205
215
  }
206
216
  async resumeLatest(sessionId) {
207
217
  await this.#attach(sessionId);
218
+ await this.#mcp?.ready();
208
219
  const plan = await this.#connection.request('turn.resume.query', { sessionId });
209
- return plan.disposition === 'ready' ? this.#driver.resumeLatest() : null;
220
+ return plan.disposition === 'ready' ? this.#resumeAndObserve(plan) : null;
221
+ }
222
+ async *#resumeAndObserve(plan) {
223
+ const turn = await this.#driver.resumeLatestTurn(plan);
224
+ if (!turn.runId)
225
+ throw new Error('Runtime Host did not return a Run identity');
226
+ const activeTurn = {
227
+ sessionId: turn.sessionId,
228
+ turnId: turn.turnId,
229
+ runId: turn.runId,
230
+ outcome: new TurnOutcomeClassifier(turn.runId),
231
+ };
232
+ this.#activeTurn = activeTurn;
233
+ try {
234
+ if (this.#stopRequested)
235
+ await this.#stopTurn(activeTurn);
236
+ yield* this.#observeTurn(turn, activeTurn);
237
+ }
238
+ finally {
239
+ if (this.#activeTurn === activeTurn)
240
+ this.#activeTurn = undefined;
241
+ }
210
242
  }
211
243
  async stopSession(sessionId) {
212
244
  this.#stopRequested = true;
@@ -273,16 +305,17 @@ class RuntimeHostRunRuntime {
273
305
  if (outcome)
274
306
  await this.#observer?.(outcome);
275
307
  }
276
- close() {
308
+ async close() {
277
309
  this.#closed = true;
278
310
  this.#interactions.close();
279
311
  this.#unsubscribeTranscriptReplacements();
280
312
  this.#cancelGraphTerminalWaiters(new Error('Runtime Host run context closed'));
281
- return Promise.resolve();
313
+ await this.#mcp?.close();
282
314
  }
283
315
  async #attach(sessionId) {
284
316
  if (this.#sessionId === sessionId)
285
317
  return;
318
+ await this.#mcp?.prepare(sessionId);
286
319
  const switched = await this.#driver.switchSession(sessionId);
287
320
  if (this.#sessionCwdOverride?.sessionId === sessionId &&
288
321
  switched.summary.cwd !== this.#sessionCwdOverride.cwd) {
@@ -0,0 +1,161 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ import { createHash } from 'node:crypto';
20
+ import { stableJsonStringify } from '@maka/core/canonical-json';
21
+ import { createCredentialMcpOAuthStorage, McpClientManager } from '@maka/mcp';
22
+ import { createFileCredentialStore } from '@maka/storage/credential-store';
23
+ import { createMcpConfigStore } from '@maka/storage/mcp-config-store';
24
+ import { RuntimeHostOperationError, } from '@maka/runtime-host/client';
25
+ import { createMcpCapabilityProvider } from './mcp-capability-provider.js';
26
+ import { McpCapabilityPublication } from './mcp-capability-publication.js';
27
+ /** One headless invocation owns one Session's MCP processes and publication. */
28
+ export class RuntimeHostRunMcp {
29
+ #connection;
30
+ #workspaceRoot;
31
+ #manager;
32
+ #publication;
33
+ #unsubscribeManager;
34
+ #unsubscribeConnection;
35
+ #availability;
36
+ #sessionId;
37
+ #sessionConfigurationId;
38
+ #enabledServerIds = [];
39
+ #prepareTask;
40
+ #closed = false;
41
+ #closeTask;
42
+ constructor(workspaceRoot, connection) {
43
+ this.#workspaceRoot = workspaceRoot;
44
+ this.#connection = connection;
45
+ this.#manager = new McpClientManager({
46
+ clientName: 'maka-run',
47
+ excludedStdioEnvironmentKeys: ['MAKA_RUNTIME_HOST_ACCESS_CREDENTIAL'],
48
+ oauthStorage: createCredentialMcpOAuthStorage(createFileCredentialStore(workspaceRoot)),
49
+ });
50
+ this.#publication = new McpCapabilityPublication({
51
+ connectionIdentity: () => this.#availability?.kind === 'connected'
52
+ ? `${this.#availability.hostEpoch}\0${this.#availability.connectionId}`
53
+ : undefined,
54
+ revision: () => this.#manager.toolSnapshot().revision,
55
+ createProvider: () => createMcpCapabilityProvider(this.#manager, {
56
+ admission: 'mcp',
57
+ onCurrentRegistrationRetired: () => this.#retire(),
58
+ }) ?? {
59
+ // Even an empty snapshot must replace a lost Session publication.
60
+ offers: () => [],
61
+ currentRegistrationRetired: () => this.#retire(),
62
+ },
63
+ replace: (provider) => this.#connection.replaceClientCapabilities(provider, {
64
+ sessionId: this.#sessionId,
65
+ sessionConfigurationId: this.#sessionConfigurationId,
66
+ }),
67
+ unregister: () => this.#connection.unregisterClientCapabilities({ sessionId: this.#sessionId }),
68
+ onState: () => undefined,
69
+ });
70
+ this.#unsubscribeManager = this.#manager.onChange(() => {
71
+ if (this.#sessionId && !this.#closed)
72
+ this.#publication.request();
73
+ });
74
+ this.#unsubscribeConnection = connection.subscribeConnectionAvailability((availability) => {
75
+ this.#availability = availability;
76
+ if (availability.kind !== 'connected')
77
+ this.#publication.invalidate();
78
+ if (this.#sessionId && !this.#closed)
79
+ this.#publication.request();
80
+ });
81
+ }
82
+ prepare(sessionId) {
83
+ if (this.#sessionId && this.#sessionId !== sessionId) {
84
+ return Promise.reject(new Error('MCP is already bound to another Session'));
85
+ }
86
+ this.#prepareTask ??= this.#prepare(sessionId);
87
+ return this.#prepareTask;
88
+ }
89
+ async #prepare(sessionId) {
90
+ if (this.#closed)
91
+ throw new Error('MCP publication is closed');
92
+ try {
93
+ const config = await createMcpConfigStore(this.#workspaceRoot).get();
94
+ this.#enabledServerIds = Object.entries(config.mcpServers)
95
+ .filter(([, server]) => server.enabled !== false)
96
+ .map(([serverId]) => serverId);
97
+ try {
98
+ await this.#manager.sync(config);
99
+ }
100
+ catch (error) {
101
+ throw new Error('Session MCP preparation failed; check the workspace MCP configuration', {
102
+ cause: error,
103
+ });
104
+ }
105
+ this.#assertServersConnected();
106
+ if (this.#closed)
107
+ throw new Error('MCP publication is closed');
108
+ this.#sessionConfigurationId = `sha256:${createHash('sha256').update(stableJsonStringify(config)).digest('hex')}`;
109
+ this.#sessionId = sessionId;
110
+ await this.ready();
111
+ }
112
+ catch (error) {
113
+ await this.close().catch(() => undefined);
114
+ throw error;
115
+ }
116
+ }
117
+ async ready() {
118
+ if (this.#closed || !this.#sessionId)
119
+ throw new Error('MCP publication is not prepared');
120
+ this.#assertServersConnected();
121
+ const state = await this.#publication.settle();
122
+ if (state !== 'published') {
123
+ if (this.#publication.lastError instanceof RuntimeHostOperationError &&
124
+ this.#publication.lastError.code === 'session_binding_conflict') {
125
+ throw new Error('Session MCP configuration conflicts with its frozen tools; restore the same configuration before resuming', { cause: this.#publication.lastError });
126
+ }
127
+ throw new Error('Session MCP capability publication is unavailable', {
128
+ cause: this.#publication.lastError,
129
+ });
130
+ }
131
+ this.#assertServersConnected();
132
+ }
133
+ #assertServersConnected() {
134
+ const unavailable = this.#enabledServerIds.flatMap((serverId) => {
135
+ const state = this.#manager.status(serverId)?.state ?? 'missing';
136
+ return state === 'connected' ? [] : [`${serverId} (${state})`];
137
+ });
138
+ if (unavailable.length > 0) {
139
+ throw new Error(`Session MCP server(s) unavailable: ${unavailable.join(', ')}`);
140
+ }
141
+ }
142
+ close() {
143
+ this.#closeTask ??= this.#close(true);
144
+ return this.#closeTask;
145
+ }
146
+ #retire() {
147
+ this.#closeTask ??= this.#close(false);
148
+ return this.#closeTask;
149
+ }
150
+ async #close(unregister) {
151
+ this.#closed = true;
152
+ this.#unsubscribeManager();
153
+ this.#unsubscribeConnection();
154
+ try {
155
+ await (unregister ? this.#publication.close() : this.#publication.retire());
156
+ }
157
+ finally {
158
+ await this.#manager.close();
159
+ }
160
+ }
161
+ }
@@ -57,6 +57,7 @@ export function createRuntimeHostMakaSessionDriver(input) {
57
57
  class RuntimeHostMakaSessionDriverImpl {
58
58
  #connection;
59
59
  #newId;
60
+ #prepareSession;
60
61
  #now;
61
62
  #inspectCwdChanges;
62
63
  #executionLocation;
@@ -103,6 +104,7 @@ class RuntimeHostMakaSessionDriverImpl {
103
104
  constructor(input) {
104
105
  this.#connection = input.connection;
105
106
  this.#newId = input.newId ?? randomUUID;
107
+ this.#prepareSession = input.prepareSession;
106
108
  this.#now = input.now ?? Date.now;
107
109
  this.#inspectCwdChanges = input.inspectCwdChanges ?? inspectGitCwdChanges;
108
110
  this.#executionLocation = input.executionLocation ?? { kind: 'client_path' };
@@ -192,6 +194,9 @@ class RuntimeHostMakaSessionDriverImpl {
192
194
  const events = channel.eventsForTurn(turnId);
193
195
  const modelText = options.modelText ?? prompt;
194
196
  try {
197
+ if (options.origin !== undefined && options.origin.kind !== 'cloud_activation') {
198
+ throw new Error('Runtime Host turn.start only supports cloud activation origins');
199
+ }
195
200
  const startInput = {
196
201
  sessionId,
197
202
  turnId,
@@ -201,6 +206,7 @@ class RuntimeHostMakaSessionDriverImpl {
201
206
  },
202
207
  ...(options.turnOrchestration ? { turnOrchestration: options.turnOrchestration } : {}),
203
208
  ...(options.maxSteps !== undefined ? { maxSteps: options.maxSteps } : {}),
209
+ ...(options.origin !== undefined ? { origin: options.origin } : {}),
204
210
  };
205
211
  const result = await this.#connection.request('turn.start', startInput);
206
212
  if (result.kind === 'blocked') {
@@ -325,6 +331,14 @@ class RuntimeHostMakaSessionDriverImpl {
325
331
  if (plan.disposition !== 'ready') {
326
332
  throw new SafeBoundaryResumeParkedError(plan.reason);
327
333
  }
334
+ const turn = await this.resumeLatestTurn(plan);
335
+ yield* turn.events;
336
+ }
337
+ async resumeLatestTurn(plan) {
338
+ const sessionId = this.#requireSession('resume');
339
+ if (plan.sessionId !== sessionId) {
340
+ throw new Error('Runtime Host resume plan changed Session identity');
341
+ }
328
342
  const channel = await this.#ensureChannel(sessionId);
329
343
  const turnId = this.#newId();
330
344
  this.#claimedTurnIds.add(turnId);
@@ -337,14 +351,19 @@ class RuntimeHostMakaSessionDriverImpl {
337
351
  sourceRuntimeEventHighWater: plan.sourceRuntimeEventHighWater,
338
352
  });
339
353
  if (result.kind !== 'started') {
340
- channel.failTurn(turnId, new SafeBoundaryResumeParkedError(result.plan.reason));
354
+ throw new SafeBoundaryResumeParkedError(result.plan.reason);
341
355
  }
356
+ return {
357
+ sessionId: result.turn.sessionId,
358
+ turnId: result.turn.turnId,
359
+ runId: result.turn.runId,
360
+ events,
361
+ };
342
362
  }
343
363
  catch (error) {
344
364
  channel.failTurn(turnId, error);
345
365
  throw error;
346
366
  }
347
- yield* events;
348
367
  }
349
368
  submitMessage(text, options) {
350
369
  return this.#admit(() => this.#submitMessage(text, options));
@@ -1051,6 +1070,7 @@ class RuntimeHostMakaSessionDriverImpl {
1051
1070
  if (!this.#llmConnectionId) {
1052
1071
  throw new Error('Runtime Host Session creation requires an exact Connection identity');
1053
1072
  }
1073
+ await this.#prepareSession?.(sessionId);
1054
1074
  const session = requireSession(await this.#request('session.create', {
1055
1075
  sessionId,
1056
1076
  workspace,
@@ -246,6 +246,7 @@ export const TUI_COPY_RESOURCES = {
246
246
  title: 'MCP SERVERS',
247
247
  footer: {
248
248
  back: 'Esc back',
249
+ diagnostic: '↑/↓ scroll · Esc back',
249
250
  readOnly: '↑/↓ scroll · q/Esc close',
250
251
  manage: 'a Add · Enter Edit · Space Enable/disable · t Test · r Reconnect · d Remove · Esc Close',
251
252
  managePublication: 'a Add · Enter Edit · Space Enable/disable · t Test · r Reconnect · d Remove · p Set provider credential · x Remove credential · Esc Close',
@@ -254,6 +255,7 @@ export const TUI_COPY_RESOURCES = {
254
255
  unavailableDetail: 'Client MCP tool association for remote Runtime Hosts is planned for a later release.',
255
256
  loading: 'Loading mcp.json and discovering tools…',
256
257
  loadError: 'MCP configuration could not be loaded; no tools were published to the Runtime Host.',
258
+ invalidConfigFile: 'Invalid JSON in {path}. The file is unchanged. Quit maka, back up and repair this file before retrying.',
257
259
  noServers: 'No MCP servers are configured. Press a to add one.',
258
260
  publication: {
259
261
  waiting: 'waiting to publish',
@@ -345,6 +347,7 @@ export const TUI_COPY_RESOURCES = {
345
347
  title: 'MCP 服务器',
346
348
  footer: {
347
349
  back: 'Esc 返回',
350
+ diagnostic: '↑/↓ 滚动 · Esc 返回',
348
351
  readOnly: '↑/↓ 滚动 · q/Esc 关闭',
349
352
  manage: 'a 添加 · Enter 编辑 · Space 启用/停用 · t 测试 · r 重连 · d 删除 · Esc 关闭',
350
353
  managePublication: 'a 添加 · Enter 编辑 · Space 启用/停用 · t 测试 · r 重连 · d 删除 · p 设置 Provider 凭据 · x 删除凭据 · Esc 关闭',
@@ -353,6 +356,7 @@ export const TUI_COPY_RESOURCES = {
353
356
  unavailableDetail: '远程 Runtime Host 的客户端 MCP 工具关联将在后续版本提供。',
354
357
  loading: '正在读取 mcp.json 并发现工具…',
355
358
  loadError: '无法读取或应用 MCP 配置;没有向 Runtime Host 发布工具。',
359
+ invalidConfigFile: '{path} 中的 JSON 无效,文件未被修改。请退出 maka,备份并修复此文件后重试。',
356
360
  noServers: '尚未配置 MCP 服务器。按 a 添加。',
357
361
  publication: {
358
362
  waiting: '等待发布',
@@ -444,6 +448,7 @@ export const TUI_COPY_RESOURCES = {
444
448
  title: 'MCP 伺服器',
445
449
  footer: {
446
450
  back: 'Esc 返回',
451
+ diagnostic: '↑/↓ 捲動 · Esc 返回',
447
452
  readOnly: '↑/↓ 捲動 · q/Esc 關閉',
448
453
  manage: 'a 新增 · Enter 編輯 · Space 啟用/停用 · t 測試 · r 重新連線 · d 刪除 · Esc 關閉',
449
454
  managePublication: 'a 新增 · Enter 編輯 · Space 啟用/停用 · t 測試 · r 重新連線 · d 刪除 · p 設定 Provider 認證資料 · x 刪除認證資料 · Esc 關閉',
@@ -452,6 +457,7 @@ export const TUI_COPY_RESOURCES = {
452
457
  unavailableDetail: '遠端 Runtime Host 的用戶端 MCP 工具關聯將於後續版本提供。',
453
458
  loading: '正在讀取 mcp.json 並探索工具…',
454
459
  loadError: '無法讀取或套用 MCP 設定;未向 Runtime Host 發佈任何工具。',
460
+ invalidConfigFile: '{path} 中的 JSON 無效,檔案未被修改。請退出 maka,備份並修復此檔案後重試。',
455
461
  noServers: '尚未設定 MCP 伺服器。按 a 新增。',
456
462
  publication: {
457
463
  waiting: '等待發佈',
@@ -212,10 +212,16 @@ class TuiMcpControllerImpl {
212
212
  this.#refreshManagerSnapshot('ready', 'ready');
213
213
  this.#requestPublication();
214
214
  }
215
- catch {
215
+ catch (error) {
216
216
  if (this.#closed)
217
217
  return;
218
- this.#updateSnapshot({ initialization: 'error', publication: 'not_published' });
218
+ this.#updateSnapshot({
219
+ initialization: 'error',
220
+ publication: 'not_published',
221
+ ...(error instanceof McpConfigSourceError && error.reason === 'invalid-json' && error.path
222
+ ? { invalidConfigPath: error.path }
223
+ : {}),
224
+ });
219
225
  }
220
226
  }
221
227
  #serializeAction(work) {
@@ -303,6 +309,9 @@ class TuiMcpControllerImpl {
303
309
  return error.result;
304
310
  if (error instanceof McpConfigurationValidationError)
305
311
  return { status: 'failed', reason: 'invalid-config' };
312
+ if (error instanceof McpConfigSourceError && error.reason === 'invalid-json' && error.path) {
313
+ return { status: 'failed', reason: 'invalid-config-file', path: error.path };
314
+ }
306
315
  if (error instanceof AtomicFileWriteCommitUnknownError) {
307
316
  // The transform has already published, including any credential
308
317
  // retirement. Reload its authority; never replay those effects.
@@ -55,6 +55,13 @@ export function parseAttachmentResourceRef(value) {
55
55
  }
56
56
  /** Per-send cap on attachment count, shared by renderer preflight and main resolve. */
57
57
  export const MAX_ATTACHMENT_COUNT = 8;
58
+ /**
59
+ * Per-drop cap on files a composer checks for folders before staging them
60
+ * (#5279). A larger drop or paste is refused whole instead of staging files
61
+ * nobody checked; it is far past MAX_ATTACHMENT_COUNT, so it could never be
62
+ * sent. Preload and main refuse a longer check request outright.
63
+ */
64
+ export const MAX_ATTACHMENT_DROP_COUNT = 1024;
58
65
  /** Per-file byte cap, shared by renderer preflight, preload encode, and main resolve. */
59
66
  export const MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024;
60
67
  /** Raw-byte cap for workspace images returned by Read, leaving room for Base64 transport overhead. */