maka-agent 0.2.0-dev.22.20260905 → 0.2.0-dev.23.20260906

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 (63) hide show
  1. package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
  2. package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
  3. package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
  4. package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
  5. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  6. package/node_modules/@maka/core/dist/bot-events.js +18 -63
  7. package/node_modules/@maka/core/dist/model-call-attempt.js +0 -82
  8. package/node_modules/@maka/core/dist/model-call-usage-projection.js +0 -186
  9. package/node_modules/@maka/core/dist/record-schema.js +13 -0
  10. package/node_modules/@maka/core/dist/redaction.js +41 -58
  11. package/node_modules/@maka/core/dist/session-trace.js +5 -1
  12. package/node_modules/@maka/core/dist/usage-ledger-merge.js +7 -9
  13. package/node_modules/@maka/core/dist/workhub-creation-intent.js +34 -20
  14. package/node_modules/@maka/eval/harbor/egress-proxy/entrypoint.sh +2 -1
  15. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +51 -24
  16. package/node_modules/@maka/runtime/dist/bots/base-adapter.js +17 -0
  17. package/node_modules/@maka/runtime/dist/bots/bot-test.js +38 -22
  18. package/node_modules/@maka/runtime/dist/bots/dingtalk-bridge.js +7 -5
  19. package/node_modules/@maka/runtime/dist/bots/discord-bridge.js +4 -4
  20. package/node_modules/@maka/runtime/dist/bots/feishu-bridge.js +4 -5
  21. package/node_modules/@maka/runtime/dist/bots/qq-bridge.js +6 -4
  22. package/node_modules/@maka/runtime/dist/bots/slack-bridge.js +3 -4
  23. package/node_modules/@maka/runtime/dist/bots/telegram-bridge.js +4 -5
  24. package/node_modules/@maka/runtime/dist/bots/wechat-bridge.js +17 -19
  25. package/node_modules/@maka/runtime/dist/bots/wecom-bridge.js +5 -6
  26. package/node_modules/@maka/runtime/dist/bots/ws-bridge-base.js +2 -3
  27. package/node_modules/@maka/runtime/dist/session-trace-projection.js +3 -30
  28. package/node_modules/@maka/runtime-host/dist/candidate-entry.js +4 -1
  29. package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
  30. package/node_modules/@maka/runtime-host/dist/client/peer-client.js +13 -0
  31. package/node_modules/@maka/runtime-host/dist/client/registered-host-termination.js +0 -59
  32. package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +28 -16
  33. package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +4 -0
  34. package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +8 -0
  35. package/node_modules/@maka/runtime-host/dist/protocol/index.js +13 -1
  36. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
  37. package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +28 -1
  38. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +33 -0
  39. package/node_modules/@maka/runtime-host/dist/server/access-authority.js +26 -1
  40. package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +5 -1
  41. package/node_modules/@maka/runtime-host/dist/server/canonical-usage-reader.js +24 -11
  42. package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +7 -4
  43. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +70 -6
  44. package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +6 -3
  45. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +12 -6
  46. package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +9 -2
  47. package/node_modules/@maka/runtime-host/dist/server/root-admission-owner.js +7 -0
  48. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +21 -0
  49. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +151 -5
  50. package/node_modules/@maka/runtime-host/dist/server/usage-pricing-coordinator.js +4 -11
  51. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +44 -15
  52. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +6 -1
  53. package/node_modules/@maka/storage/dist/agent-run-store.js +44 -0
  54. package/node_modules/@maka/storage/dist/conversation-operational-state.js +4 -0
  55. package/node_modules/@maka/storage/dist/execution-stores.js +2 -0
  56. package/node_modules/@maka/storage/dist/model-call-ledger.js +199 -32
  57. package/node_modules/@maka/storage/dist/model-call-usage-sql.js +136 -0
  58. package/node_modules/@maka/storage/dist/operational-state-store-public.js +1 -1
  59. package/node_modules/@maka/storage/dist/operational-state-store.js +5 -3
  60. package/node_modules/@maka/storage/dist/sqlite-core-execution-schema.js +11 -1
  61. package/node_modules/@maka/storage/dist/sqlite-usage-schema.js +200 -20
  62. package/node_modules/@maka/storage/dist/usage-stores.js +6 -2
  63. package/package.json +1 -1
@@ -19,7 +19,7 @@
19
19
  import { generalizedErrorMessage } from '@maka/core/redaction';
20
20
  import { randomBytes, randomUUID } from 'node:crypto';
21
21
  import { createRequire } from 'node:module';
22
- import { BaseBotAdapter, botReadinessFromSettings } from './base-adapter.js';
22
+ import { BaseBotAdapter, botDiagnosticMessage, botReadinessFromSettings } from './base-adapter.js';
23
23
  import { proxiedFetch } from './proxied-fetch.js';
24
24
  const DEFAULT_WECHAT_BRIDGE_URL = 'http://127.0.0.1:18400';
25
25
  const WECHAT_BRIDGE_TIMEOUT_MS = 5_000;
@@ -90,7 +90,9 @@ export class WechatBridge extends BaseBotAdapter {
90
90
  : await testWechatBridge(this.settings);
91
91
  if (!probe.ok) {
92
92
  this.running = false;
93
- this.reason = probe.error;
93
+ this.reason = probe.errorCode ?? 'connection_failed';
94
+ if (probe.error)
95
+ this.recordFailure(probe.error, this.reason);
94
96
  this.readiness = botReadinessFromSettings(this.settings);
95
97
  this.emitStatusChange();
96
98
  return;
@@ -109,7 +111,7 @@ export class WechatBridge extends BaseBotAdapter {
109
111
  // marked degraded instead of crashing the main process.
110
112
  const fail = (err) => {
111
113
  this.running = false;
112
- this.reason = err instanceof Error ? err.message : 'stream-failed';
114
+ this.recordFailure(err, 'stream-failed');
113
115
  this.readiness = 'degraded';
114
116
  this.emitStatusChange();
115
117
  };
@@ -145,8 +147,7 @@ export class WechatBridge extends BaseBotAdapter {
145
147
  const status = typeof response.status === 'string' ? response.status : '';
146
148
  if (status === 'failed') {
147
149
  this.readiness = 'degraded';
148
- this.reason =
149
- typeof response.diagnostic === 'string' ? response.diagnostic : 'wechat-send-failed';
150
+ this.recordFailure(response.diagnostic ?? 'wechat-send-failed', 'send-failed');
150
151
  this.emitStatusChange();
151
152
  return null;
152
153
  }
@@ -159,7 +160,7 @@ export class WechatBridge extends BaseBotAdapter {
159
160
  }
160
161
  catch (error) {
161
162
  this.readiness = 'degraded';
162
- this.reason = generalizedErrorMessage(error);
163
+ this.recordFailure(error);
163
164
  this.emitStatusChange();
164
165
  return null;
165
166
  }
@@ -205,7 +206,7 @@ export class WechatBridge extends BaseBotAdapter {
205
206
  return;
206
207
  this.readiness =
207
208
  this.readiness === 'operational' ? 'degraded' : botReadinessFromSettings(this.settings);
208
- this.reason = generalizedErrorMessage(error);
209
+ this.recordFailure(error);
209
210
  this.emitStatusChange();
210
211
  await sleep(3_000);
211
212
  }
@@ -230,7 +231,7 @@ export class WechatBridge extends BaseBotAdapter {
230
231
  continue;
231
232
  if (errcode !== 0) {
232
233
  this.readiness = this.readiness === 'operational' ? 'degraded' : 'credentials_valid';
233
- this.reason = stringField(response.errmsg) ?? `ilink-${errcode}`;
234
+ this.recordFailure(stringField(response.errmsg) ?? `ilink-${errcode}`, 'stream-failed');
234
235
  this.emitStatusChange();
235
236
  await sleep(5_000);
236
237
  continue;
@@ -259,7 +260,7 @@ export class WechatBridge extends BaseBotAdapter {
259
260
  return;
260
261
  consecutiveErrors += 1;
261
262
  this.readiness = this.readiness === 'operational' ? 'degraded' : 'credentials_valid';
262
- this.reason = generalizedErrorMessage(error);
263
+ this.recordFailure(error);
263
264
  this.emitStatusChange();
264
265
  await sleep(consecutiveErrors >= 3 ? 30_000 : 2_000);
265
266
  if (consecutiveErrors >= 3)
@@ -298,7 +299,7 @@ export async function getWechatBridgeQrCode(channel) {
298
299
  return {
299
300
  ok: false,
300
301
  error: 'WeChat bridge URL must be http://127.0.0.1 or http://localhost',
301
- hint: '微信扫码登录只允许访问本机 wechat-bridge,不能指向远端 URL。',
302
+ hintCode: 'wechat_bridge_remote_url',
302
303
  };
303
304
  }
304
305
  let lastError;
@@ -316,7 +317,7 @@ export async function getWechatBridgeQrCode(channel) {
316
317
  return {
317
318
  ok: false,
318
319
  error: generalizedErrorMessage(lastError),
319
- hint: '先启动本机 wechat-bridge,并确认它暴露了 iLink 兼容的 /api/weixin/qrcode 或 /qrcode 接口。',
320
+ hintCode: 'wechat_bridge_unreachable',
320
321
  };
321
322
  }
322
323
  export function mapWechatBridgeMessage(raw) {
@@ -425,8 +426,7 @@ export async function testWechatBridge(channel) {
425
426
  if (!baseUrl) {
426
427
  return {
427
428
  ok: false,
428
- error: 'WeChat bridge URL must be http://127.0.0.1 or http://localhost',
429
- hint: '微信本地桥接只允许访问本机 wechat-bridge,不能指向远端 URL。',
429
+ errorCode: 'wechat_bridge_url_invalid',
430
430
  };
431
431
  }
432
432
  try {
@@ -455,8 +455,8 @@ export async function testWechatBridge(channel) {
455
455
  catch (error) {
456
456
  return {
457
457
  ok: false,
458
- error: generalizedErrorMessage(error),
459
- hint: '先在本机启动 wechat-bridge,并确认 WeChat 已登录;发送能力需要 wxp_act_ 激活码。',
458
+ errorCode: 'connection_failed',
459
+ error: botDiagnosticMessage(channel, error),
460
460
  };
461
461
  }
462
462
  }
@@ -486,8 +486,7 @@ export async function testWechatIlinkCredentials(channel) {
486
486
  if (!baseUrl || !token) {
487
487
  return {
488
488
  ok: false,
489
- error: 'WeChat iLink credentials are incomplete',
490
- hint: '请先完成微信扫码登录,保存 iLink bot token 与 base URL。',
489
+ errorCode: 'wechat_ilink_credentials_incomplete',
491
490
  };
492
491
  }
493
492
  return {
@@ -498,11 +497,10 @@ export async function testWechatIlinkCredentials(channel) {
498
497
  displayName: channel.botUserId ? `iLink ${channel.botUserId}` : 'WeChat iLink',
499
498
  },
500
499
  capabilities: { auth: true, send: true },
501
- hint: '扫码登录凭据已保存;运行态会通过 iLink 长轮询接收消息。',
502
500
  };
503
501
  }
504
502
  function isWechatIlinkChannel(channel) {
505
- return Boolean(channel.token.trim() && normalizeWechatIlinkBaseUrl(channel.webhookUrl));
503
+ return Boolean(normalizeWechatIlinkBaseUrl(channel.webhookUrl));
506
504
  }
507
505
  async function wechatIlinkPost(baseUrl, path, body, token, signal) {
508
506
  const response = await proxiedFetch(`${baseUrl}${path}`, {
@@ -17,7 +17,6 @@
17
17
  * under the License.
18
18
  */
19
19
  import { WSClient } from '@wecom/aibot-node-sdk';
20
- import { generalizedErrorMessage } from '@maka/core/redaction';
21
20
  import { BaseBotAdapter, botReadinessFromSettings } from './base-adapter.js';
22
21
  const AUTH_TIMEOUT_MS = 15_000;
23
22
  /** Detach bridge callbacks before disconnect can synchronously emit. */
@@ -73,7 +72,7 @@ export class WeComBotBridge extends BaseBotAdapter {
73
72
  const botId = this.settings.appId?.trim() ?? '';
74
73
  const secret = this.settings.appSecret?.trim() ?? '';
75
74
  if (!botId || !secret) {
76
- this.reason = 'no-credentials';
75
+ this.reason = 'wecom_credentials_missing';
77
76
  this.readiness = 'scaffolded';
78
77
  this.emitStatusChange();
79
78
  return;
@@ -122,7 +121,7 @@ export class WeComBotBridge extends BaseBotAdapter {
122
121
  if (this.explicitlyStopped || this.client !== client)
123
122
  return;
124
123
  this.running = false;
125
- this.reason = generalizedErrorMessage(error);
124
+ this.recordFailure(error);
126
125
  this.readiness = 'configured';
127
126
  this.emitStatusChange();
128
127
  this.client = null;
@@ -157,7 +156,7 @@ export class WeComBotBridge extends BaseBotAdapter {
157
156
  }
158
157
  catch (error) {
159
158
  this.readiness = this.readiness === 'operational' ? 'degraded' : 'credentials_valid';
160
- this.reason = generalizedErrorMessage(error);
159
+ this.recordFailure(error, 'send-failed');
161
160
  this.emitStatusChange();
162
161
  return null;
163
162
  }
@@ -197,7 +196,7 @@ export class WeComBotBridge extends BaseBotAdapter {
197
196
  if (this.client !== client || this.explicitlyStopped)
198
197
  return;
199
198
  this.running = false;
200
- this.reason = reason || 'disconnected';
199
+ this.recordFailure(reason || 'disconnected', 'disconnected');
201
200
  this.readiness = this.readiness === 'operational' ? 'degraded' : 'configured';
202
201
  this.emitStatusChange();
203
202
  });
@@ -210,7 +209,7 @@ export class WeComBotBridge extends BaseBotAdapter {
210
209
  client.on('error', (error) => {
211
210
  if (this.client !== client || this.explicitlyStopped)
212
211
  return;
213
- this.reason = generalizedErrorMessage(error);
212
+ this.recordFailure(error);
214
213
  this.readiness = this.readiness === 'operational' ? 'degraded' : 'configured';
215
214
  this.emitStatusChange();
216
215
  });
@@ -103,7 +103,7 @@ export class WsBridgeBase extends BaseBotAdapter {
103
103
  ws = this.createWebSocket(url);
104
104
  }
105
105
  catch (error) {
106
- this.reason = error instanceof Error ? error.message : String(error);
106
+ this.recordFailure(error);
107
107
  this.readiness = 'configured';
108
108
  this.emitStatusChange();
109
109
  this.scheduleReconnect();
@@ -147,9 +147,9 @@ export class WsBridgeBase extends BaseBotAdapter {
147
147
  const decision = this.decideClose(code, this.explicitlyStopped);
148
148
  if (decision.kind === 'stopped')
149
149
  return;
150
+ this.recordFailure(reason, `${this.closeReasonPrefix}-closed-${code}`);
150
151
  if (decision.kind === 'fatal') {
151
152
  this.readiness = 'configured';
152
- this.reason = `${this.closeReasonPrefix}-closed-${code}`;
153
153
  this.resetSession();
154
154
  this.emitStatusChange();
155
155
  return;
@@ -158,7 +158,6 @@ export class WsBridgeBase extends BaseBotAdapter {
158
158
  this.resetSession();
159
159
  }
160
160
  this.readiness = 'degraded';
161
- this.reason = reason || `${this.closeReasonPrefix}-closed-${code}`;
162
161
  this.emitStatusChange();
163
162
  this.scheduleReconnect();
164
163
  }
@@ -18,7 +18,8 @@
18
18
  */
19
19
  import { dedupeModelCallAttempts, groupModelCallAttempts, } from '@maka/core/model-call-attempt';
20
20
  import { TERMINAL_RUNTIME_EVENT_STATUSES } from '@maka/core/runtime-event';
21
- import { SESSION_TRACE_SCHEMA_VERSION, traceTurnIdentityKey, } from '@maka/core/session-trace';
21
+ import { SESSION_TRACE_SCHEMA_VERSION, MODEL_ATTEMPT_SHAPE, traceTurnIdentityKey, } from '@maka/core/session-trace';
22
+ import { pickShape } from '@maka/core/record-schema';
22
23
  export function projectSessionTrace(input) {
23
24
  const events = input.runtimeEvents.filter((event) => !event.partial);
24
25
  // An aborted attempt and its later settlement are appended under one
@@ -189,35 +190,7 @@ function projectModelCallSteps(attempts) {
189
190
  return steps;
190
191
  }
191
192
  function toTraceAttempt(attempt) {
192
- return {
193
- attemptId: attempt.attemptId,
194
- attempt: attempt.attempt,
195
- status: attempt.status,
196
- startedAt: attempt.startedAt,
197
- completedAt: attempt.completedAt,
198
- latencyMs: attempt.latencyMs,
199
- ...(attempt.timeToFirstTokenMs !== undefined
200
- ? { timeToFirstTokenMs: attempt.timeToFirstTokenMs }
201
- : {}),
202
- ...(attempt.finishReason !== undefined ? { finishReason: attempt.finishReason } : {}),
203
- ...(attempt.errorClass !== undefined ? { errorClass: attempt.errorClass } : {}),
204
- ...(attempt.httpStatus !== undefined ? { httpStatus: attempt.httpStatus } : {}),
205
- ...(attempt.providerCode !== undefined ? { providerCode: attempt.providerCode } : {}),
206
- ...(attempt.providerRequestId !== undefined
207
- ? { providerRequestId: attempt.providerRequestId }
208
- : {}),
209
- ...(attempt.retryable !== undefined ? { retryable: attempt.retryable } : {}),
210
- ...(attempt.inputTokens !== undefined ? { inputTokens: attempt.inputTokens } : {}),
211
- ...(attempt.outputTokens !== undefined ? { outputTokens: attempt.outputTokens } : {}),
212
- ...(attempt.cacheReadInputTokens !== undefined
213
- ? { cacheReadInputTokens: attempt.cacheReadInputTokens }
214
- : {}),
215
- ...(attempt.reasoningTokens !== undefined ? { reasoningTokens: attempt.reasoningTokens } : {}),
216
- ...(attempt.contextWindow !== undefined ? { contextWindow: attempt.contextWindow } : {}),
217
- ...(attempt.costUsd !== undefined ? { costUsd: attempt.costUsd } : {}),
218
- costBasis: attempt.costBasis,
219
- usageBasis: attempt.usageBasis,
220
- };
193
+ return pickShape(attempt, MODEL_ATTEMPT_SHAPE);
221
194
  }
222
195
  /** Prefix the runtime gives a written history-compaction boundary. */
223
196
  const HISTORY_COMPACT_EVENT_PREFIX = 'history-compact:';
@@ -74,7 +74,10 @@ export async function runExecutionCandidateEntry(argv, entrypointUrl, hooks = {}
74
74
  await launchOwnerGuard?.dispose();
75
75
  process.exit(2);
76
76
  }
77
- launchOwnerGuard?.bind(() => result.host.close());
77
+ // A launcher that disappears without releasing this Host (Desktop quit,
78
+ // launcher crash) is an intentional retirement of an owned ephemeral Host,
79
+ // not a crash — closing with the retirement reason keeps the exit truthful.
80
+ launchOwnerGuard?.bind(() => result.host.close({ reason: 'retirement' }));
78
81
  const stopWatch = hooks.onWon?.(result.host);
79
82
  try {
80
83
  await runRuntimeHostProcessLifecycle(result.host);
@@ -21,7 +21,7 @@ export { decodeRuntimeHostWebRtcStunPolicy, resolveRuntimeHostWebRtcStunUrls, }
21
21
  export { openRuntimeHostManagedStdioBridge } from './managed-stdio-bridge.js';
22
22
  export { connectRuntimeHost, connectExistingRuntimeHost, connectRemoteRuntimeHost, RuntimeHostOperationError, RuntimeHostRequestInterruptedError, } from './connection.js';
23
23
  export { prepareConnectedRuntimeHostRetirement, } from './host-retirement.js';
24
- export { forceTerminateObservedRegisteredRuntimeHost, forceTerminateRegisteredRuntimeHost, } from './registered-host-termination.js';
24
+ export { forceTerminateObservedRegisteredRuntimeHost, } from './registered-host-termination.js';
25
25
  export { LOCAL_RUNTIME_HOST_PROFILE, RUNTIME_HOST_ACCESS_CREDENTIAL_MAX_BYTES, createClientRuntimeHostCredentialStore, createClientRuntimeHostProfileCatalog, createFileRuntimeHostProfileCatalog, createRuntimeHostCapabilityProviderCredentialStore, createRuntimeHostProfileCredentialStore, connectRuntimeHostProfile, sameEnvironmentRuntimeHostDeployment, connectRemoteRuntimeHostProfile, decodeEnvironmentRuntimeHostProfile, decodePersistedRuntimeHostProfile, decodeRemoteRuntimeHostProfile, migrateRuntimeHostProfileOperatorCommand, remoteRuntimeHostUnavailableError, runtimeHostProfileAccess, runtimeHostProfileTargetFingerprint, sameRemoteRuntimeHostProfileTarget, sameResolvedRuntimeHostProfileTarget, subscribeClientRuntimeHostProfileCatalogChanges, RuntimeHostProfileConnectionError, } from './host-profile.js';
26
26
  export { createRuntimeHostReconnectingConnection, isRuntimeHostReconnectingConnection, } from './reconnecting-connection.js';
27
27
  export { RuntimeHostSshOperatorActivationError, activateRuntimeHostSshOperator, runtimeHostSshOperatorRemoteCommand, } from './ssh-operator-activation.js';
@@ -126,6 +126,9 @@ class RuntimeHostPeerClientImpl {
126
126
  if (this.#routeResolver === resolver)
127
127
  return () => undefined;
128
128
  this.#routeResolver = resolver;
129
+ for (const peerId of this.#endpoint?.connectivitySnapshot.connectedPeerIds ?? []) {
130
+ this.#notifyPeerConnected(peerId);
131
+ }
129
132
  for (const peerId of this.#routeListeners.keys()) {
130
133
  this.#subscribeResolver(peerId);
131
134
  this.#notifyRouteChange(peerId);
@@ -511,6 +514,8 @@ class RuntimeHostPeerClientImpl {
511
514
  for (const peerId of new Set([...previousPeers, ...nextPeers])) {
512
515
  if (previousPeers.has(peerId) !== nextPeers.has(peerId))
513
516
  this.#notifyRouteChange(peerId);
517
+ if (!previousPeers.has(peerId) && nextPeers.has(peerId))
518
+ this.#notifyPeerConnected(peerId);
514
519
  }
515
520
  }
516
521
  }
@@ -522,6 +527,14 @@ class RuntimeHostPeerClientImpl {
522
527
  this.#finishConsumer('mesh', this.#terminalError);
523
528
  }
524
529
  }
530
+ #notifyPeerConnected(peerId) {
531
+ try {
532
+ this.#routeResolver?.peerConnected?.(peerId);
533
+ }
534
+ catch {
535
+ // Mesh recovery must not interrupt the transport's connectivity watcher.
536
+ }
537
+ }
525
538
  async #drainInbound(endpoint) {
526
539
  try {
527
540
  while (true) {
@@ -16,16 +16,10 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { terminateProcessTree } from '@maka/runtime/process-tree-terminator';
20
19
  import { prepareStorageRootControlDirectory, resolveStorageRoot, } from '@maka/storage/root-authority';
21
20
  import { readHostRegistration } from '../control/registration.js';
22
21
  import { readRuntimeHostProcessIdentity, } from './process-identity.js';
23
22
  const TERMINATION_SETTLE_MS = 2_000;
24
- const defaultDependencies = {
25
- terminateProcess: terminateProcessTree,
26
- isProcessAlive,
27
- settleMs: TERMINATION_SETTLE_MS,
28
- };
29
23
  const defaultObservedDependencies = {
30
24
  isProcessAlive,
31
25
  readProcessIdentity: readRuntimeHostProcessIdentity,
@@ -38,53 +32,6 @@ const defaultObservedDependencies = {
38
32
  * the expected State Root. Callers must reserve this for explicit recovery
39
33
  * and keep their authorization current until the signal is sent.
40
34
  */
41
- export function forceTerminateRegisteredRuntimeHost(identity, stillOwnsProcess) {
42
- return forceTerminateRegisteredRuntimeHostWithDependencies(identity, stillOwnsProcess, defaultDependencies);
43
- }
44
- export async function forceTerminateRegisteredRuntimeHostWithDependencies(identity, stillOwnsProcess, dependencies) {
45
- if (!stillOwnsProcess())
46
- return false;
47
- const capability = await resolveStorageRoot({ path: identity.rootPath, kind: 'interactive' });
48
- if (capability.rootId !== identity.rootId)
49
- return false;
50
- const { controlDirectory } = await prepareStorageRootControlDirectory(capability);
51
- const registered = await readHostRegistration(controlDirectory);
52
- if (!registered)
53
- return true;
54
- if (!matchesIdentity(registered, identity))
55
- return false;
56
- if (!dependencies.isProcessAlive(identity.pid))
57
- return true;
58
- let signalTarget = registered;
59
- const signaled = await dependencies.terminateProcess({
60
- pid: identity.pid,
61
- signal: 'SIGKILL',
62
- hasExited: () => !dependencies.isProcessAlive(identity.pid),
63
- beforeSignal: async () => {
64
- // This runs after asynchronous process-tree discovery and immediately
65
- // before the OS signal, so neither a successor nor a reused PID can
66
- // inherit stale intent.
67
- signalTarget = await readHostRegistration(controlDirectory);
68
- return matchesIdentity(signalTarget, identity) && stillOwnsProcess();
69
- },
70
- fallback: () => {
71
- try {
72
- process.kill(identity.pid, 'SIGKILL');
73
- return true;
74
- }
75
- catch {
76
- return false;
77
- }
78
- },
79
- });
80
- if (!signalTarget)
81
- return true;
82
- if (!matchesIdentity(signalTarget, identity))
83
- return false;
84
- if (!signaled && dependencies.isProcessAlive(identity.pid))
85
- return false;
86
- return waitForExit(identity.pid, dependencies);
87
- }
88
35
  /**
89
36
  * Stops an ephemeral Host that Desktop did not launch in this process. Unlike
90
37
  * the owned-process path above, its authority is limited to the exact root PID
@@ -129,12 +76,6 @@ function matchesObservedRegistration(current, observed) {
129
76
  current.lifecycleMode === observed.lifecycleMode &&
130
77
  current.createdAt === observed.createdAt);
131
78
  }
132
- function matchesIdentity(registration, identity) {
133
- return (registration?.rootId === identity.rootId &&
134
- registration.hostEpoch === identity.hostEpoch &&
135
- registration.pid === identity.pid &&
136
- registration.lifecycleMode === 'ephemeral');
137
- }
138
79
  async function waitForExit(pid, dependencies) {
139
80
  const deadline = Date.now() + dependencies.settleMs;
140
81
  while (dependencies.isProcessAlive(pid)) {
@@ -616,6 +616,16 @@ class PeerMeshNodeImpl {
616
616
  transitRelayPeerIds: Object.freeze(transitRelayPeerIds),
617
617
  });
618
618
  }
619
+ peerConnected(peerId) {
620
+ if (this.#lifetime.signal.aborted)
621
+ return;
622
+ const localPeerId = this.#peer.identity().peerId;
623
+ if (peerId !== localPeerId &&
624
+ this.#store
625
+ .read()
626
+ .meshes.some((state) => isActiveMembership(state, localPeerId) && state.roster.roster.members.includes(peerId)))
627
+ this.#triggerReconciliation();
628
+ }
619
629
  async prepareRoutes(peerId, signal) {
620
630
  this.#assertOpen();
621
631
  signal.throwIfAborted();
@@ -821,13 +831,22 @@ class PeerMeshNodeImpl {
821
831
  const gossipCursor = this.#gossipCursor;
822
832
  this.#gossipCursor = (this.#gossipCursor + 1) % PEER_MESH_MAX_MEMBERS;
823
833
  const now = this.#now();
834
+ const targetFor = (peerId) => {
835
+ const signed = latestReachability(stored.reachability, peerId, now, true);
836
+ if (signed)
837
+ return dialTarget(signed);
838
+ // A fresh Session Share can reconnect an existing member after all of
839
+ // its stored routes expired. Reuse that authenticated connection to
840
+ // exchange signed evidence; the connection itself grants no membership.
841
+ return this.#peer.isConnected(peerId)
842
+ ? { peerId, routeHints: [], coordinationRelays: [] }
843
+ : undefined;
844
+ };
824
845
  for (const [index, state] of memberships.entries()) {
825
846
  const meshId = state.roster.roster.meshId;
826
847
  const desiredMembership = state.role === 'replica' ? state.desiredMembership : 'active';
827
- const authority = state.role === 'replica'
828
- ? currentAuthorityTarget(state, stored.reachability, now)
829
- : undefined;
830
- if (authority && authority.lease.peerId !== excludedPeerId) {
848
+ const authority = state.role === 'replica' ? targetFor(state.roster.roster.authorityPeerId) : undefined;
849
+ if (authority && authority.peerId !== excludedPeerId) {
831
850
  pending.push({
832
851
  kind: 'membership',
833
852
  meshId,
@@ -843,7 +862,7 @@ class PeerMeshNodeImpl {
843
862
  peerId !== excludedPeerId &&
844
863
  (state.role === 'authority' || peerId !== state.roster.roster.authorityPeerId))
845
864
  .flatMap((peerId) => {
846
- const target = peerTarget(peerId, stored.reachability, now);
865
+ const target = targetFor(peerId);
847
866
  return target ? [target] : [];
848
867
  });
849
868
  if (rotatingTargets.length === 0)
@@ -902,7 +921,7 @@ class PeerMeshNodeImpl {
902
921
  }
903
922
  async #notifyLeave(meshId, target, roster, signal) {
904
923
  const stream = await this.#peer.connectMeshControl({
905
- ...dialTarget(target),
924
+ ...target,
906
925
  directDeadlineMs: CONNECT_DEADLINE_MS,
907
926
  }, signal);
908
927
  try {
@@ -918,16 +937,15 @@ class PeerMeshNodeImpl {
918
937
  }
919
938
  async #syncPeer(meshId, target, signal) {
920
939
  const localPeerId = this.#peer.identity().peerId;
921
- const targetPeerId = target.lease.peerId;
940
+ const targetPeerId = target.peerId;
922
941
  for (let page = 0; page <= PEER_MESH_MAX_MEMBERS; page += 1) {
923
942
  if (!isActiveMeshMember(this.#store.read().meshes, meshId, localPeerId, targetPeerId))
924
943
  return;
925
944
  const discovered = this.resolveRoutes(targetPeerId);
926
- const targetRoutes = dialTarget(target);
927
945
  const stream = await this.#peer.connectMeshControl({
928
946
  peerId: targetPeerId,
929
- routeHints: mergeAddresses(discovered?.routeHints ?? [], targetRoutes.routeHints),
930
- coordinationRelays: mergeAddresses(discovered?.coordinationRelays ?? [], targetRoutes.coordinationRelays),
947
+ routeHints: mergeAddresses(discovered?.routeHints ?? [], target.routeHints),
948
+ coordinationRelays: mergeAddresses(discovered?.coordinationRelays ?? [], target.coordinationRelays),
931
949
  transitRelayPeerIds: discovered?.transitRelayPeerIds,
932
950
  directDeadlineMs: CONNECT_DEADLINE_MS,
933
951
  }, signal);
@@ -1700,9 +1718,6 @@ function peerMeshStatus(state, identity, endpointKind, reachability, advertiseme
1700
1718
  })),
1701
1719
  });
1702
1720
  }
1703
- function currentAuthorityTarget(state, reachability, now) {
1704
- return latestReachability(reachability, state.roster.roster.authorityPeerId, now, true);
1705
- }
1706
1721
  function rosterAnnouncementTargets(memberPeerIds, reachability, localPeerId, now) {
1707
1722
  const members = new Set(memberPeerIds);
1708
1723
  return Object.freeze(reachability.filter((signed) => signed.lease.peerId !== localPeerId &&
@@ -1937,9 +1952,6 @@ function hasReachabilityRoutes(signed) {
1937
1952
  function usableHistoricalReachability(signed, now) {
1938
1953
  return signed.lease.expiresAt > now - PEER_REACHABILITY_RECOVERY_HORIZON_MS;
1939
1954
  }
1940
- function peerTarget(peerId, reachability, now) {
1941
- return latestReachability(reachability, peerId, now, true);
1942
- }
1943
1955
  function dialTarget(reachability) {
1944
1956
  return Object.freeze({
1945
1957
  peerId: reachability.lease.peerId,
@@ -46,6 +46,10 @@ const IMPORT_ERRORS = [
46
46
  'not_found',
47
47
  'operation_conflict',
48
48
  'commit_outcome_unknown',
49
+ // Distinct, stable reasons the import page renders as specific banners, so the
50
+ // shell classifies by code rather than by the redacted error message.
51
+ 'model_unavailable',
52
+ 'source_unreadable',
49
53
  ];
50
54
  export const EXTERNAL_SESSION_OPERATION_SPECS = {
51
55
  'external-session.source.query': defineOperation({
@@ -81,6 +81,7 @@ function decodeHostDiagnosticsResult(value) {
81
81
  'activeOperations',
82
82
  'activeResidencies',
83
83
  ...(valueRecord.peerEndpoint === undefined ? [] : ['peerEndpoint']),
84
+ 'upgradeBlockingActivity',
84
85
  'compositionModules',
85
86
  'residencies',
86
87
  'protocolVersion',
@@ -104,6 +105,7 @@ function decodeHostDiagnosticsResult(value) {
104
105
  }
105
106
  return {
106
107
  ...decodeHostStatusFields(record),
108
+ upgradeBlockingActivity: requireUpgradeBlockingActivity(record.upgradeBlockingActivity),
107
109
  compositionModules: record.compositionModules.map((moduleId) => requireString(moduleId, 'Runtime Host composition module id', 64)),
108
110
  residencies: record.residencies.map((value) => {
109
111
  const residency = requireExactRecord(value, 'Runtime Host residency', ['label', 'count']);
@@ -123,6 +125,12 @@ function decodeHostDiagnosticsResult(value) {
123
125
  logs: record.logs.map((entry) => requireUtf8String(entry, 'Runtime Host diagnostic log entry', HOST_DIAGNOSTIC_LOG_MAX_ENTRY_BYTES)),
124
126
  };
125
127
  }
128
+ function requireUpgradeBlockingActivity(value) {
129
+ if (typeof value !== 'boolean') {
130
+ throw invalidProtocolFrame('Invalid Runtime Host upgrade blocking activity');
131
+ }
132
+ return value;
133
+ }
126
134
  export function decodeHostActivitySnapshot(value) {
127
135
  const record = requireExactRecord(value, 'Runtime Host activity', [
128
136
  'connections',
@@ -62,7 +62,19 @@ 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 = 117;
65
+ export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 121;
66
+ // 121: Host diagnostics report `upgradeBlockingActivity`, the Host's
67
+ // authoritative activity answer for maintenance probes, computed by the same
68
+ // authority that gates `host.upgrade.prepare`. Older Clients reject the
69
+ // unknown key when decoding diagnostics, so the pair must refuse each other
70
+ // at the handshake.
71
+ // 120: WorkHub admits named resume proposals with an explicit resumesActionId
72
+ // and returns a transient resume outcome. Older peers cannot decode this action.
73
+ // 119: Session Guest principals expose optional display names and an owner-only
74
+ // rename command. Older peers reject named principal projections.
75
+ // 118: External-session import publishes distinct `model_unavailable` and
76
+ // `source_unreadable` error codes so the shell classifies failures by code
77
+ // instead of the redacted message. Older peers cannot decode the new codes.
66
78
  // 117: WorkHub exposes only one correction linkage per bounded candidate and
67
79
  // no longer returns the Host's complete active-link set.
68
80
  // 116: User deletion rejects workflow-owned Artifacts with operation_conflict.
@@ -114,6 +114,7 @@ export const REMOTE_OWNER_OPERATION_GRANTS = Object.freeze([
114
114
  'collaboration.grant.revoke',
115
115
  'collaboration.invitation.prepare',
116
116
  'collaboration.principal.revoke',
117
+ 'collaboration.principal.rename',
117
118
  'collaboration.turn-request.decide',
118
119
  'collaboration.turn-request.query',
119
120
  'connection.catalog.create',
@@ -54,6 +54,24 @@ export const SESSION_COLLABORATION_OPERATION_SPECS = {
54
54
  decodeInput: decodeCollaborationGrantRevokeInput,
55
55
  decodeOutput: decodeCollaborationGrantRevokeResult,
56
56
  }),
57
+ 'collaboration.principal.rename': defineOperation({
58
+ mode: 'command',
59
+ availability: 'ready',
60
+ errors: COLLABORATION_ERRORS,
61
+ decodeInput(value) {
62
+ const record = requireExactRecord(value, 'Guest alias', ['principalId', 'displayName']);
63
+ return {
64
+ principalId: decodePrincipalId(record.principalId),
65
+ displayName: decodeCollaborationDisplayName(record.displayName),
66
+ };
67
+ },
68
+ decodeOutput(value) {
69
+ const record = requireExactRecord(value, 'Guest alias result', ['renamed']);
70
+ if (typeof record.renamed !== 'boolean')
71
+ throw invalidProtocolFrame('Invalid Guest alias result');
72
+ return { renamed: record.renamed };
73
+ },
74
+ }),
57
75
  'collaboration.principal.revoke': defineOperation({
58
76
  mode: 'command',
59
77
  availability: 'ready',
@@ -179,19 +197,28 @@ function decodeCollaborationAccessQueryResult(value) {
179
197
  };
180
198
  }
181
199
  function decodeSessionGuestPrincipal(value) {
182
- const record = requireShapedRecord(value, 'Session Guest principal', ['principalId', 'status', 'createdAt'], ['expiresAt']);
200
+ const record = requireShapedRecord(value, 'Session Guest principal', ['principalId', 'status', 'createdAt'], ['expiresAt', 'displayName']);
183
201
  if (record.status !== 'pending' && record.status !== 'active') {
184
202
  throw invalidProtocolFrame('Invalid Session Guest principal status');
185
203
  }
186
204
  return {
187
205
  principalId: decodePrincipalId(record.principalId),
188
206
  status: record.status,
207
+ ...(record.displayName === undefined
208
+ ? {}
209
+ : { displayName: decodeCollaborationDisplayName(record.displayName) }),
189
210
  createdAt: decodeIsoTimestamp(record.createdAt, 'createdAt'),
190
211
  ...(record.expiresAt === undefined
191
212
  ? {}
192
213
  : { expiresAt: decodeIsoTimestamp(record.expiresAt, 'expiresAt') }),
193
214
  };
194
215
  }
216
+ export function decodeCollaborationDisplayName(value) {
217
+ const name = requireUtf8String(value, 'Guest alias', 256).trim();
218
+ if (!name)
219
+ throw invalidProtocolFrame('Guest alias must not be blank');
220
+ return name;
221
+ }
195
222
  function decodeCollaborationGrantRevokeInput(value) {
196
223
  const record = requireExactRecord(value, 'collaboration grant revoke input', ['grantId']);
197
224
  return { grantId: requireId(record.grantId, 'grantId') };