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.
- package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/bot-events.js +18 -63
- package/node_modules/@maka/core/dist/model-call-attempt.js +0 -82
- package/node_modules/@maka/core/dist/model-call-usage-projection.js +0 -186
- package/node_modules/@maka/core/dist/record-schema.js +13 -0
- package/node_modules/@maka/core/dist/redaction.js +41 -58
- package/node_modules/@maka/core/dist/session-trace.js +5 -1
- package/node_modules/@maka/core/dist/usage-ledger-merge.js +7 -9
- package/node_modules/@maka/core/dist/workhub-creation-intent.js +34 -20
- package/node_modules/@maka/eval/harbor/egress-proxy/entrypoint.sh +2 -1
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +51 -24
- package/node_modules/@maka/runtime/dist/bots/base-adapter.js +17 -0
- package/node_modules/@maka/runtime/dist/bots/bot-test.js +38 -22
- package/node_modules/@maka/runtime/dist/bots/dingtalk-bridge.js +7 -5
- package/node_modules/@maka/runtime/dist/bots/discord-bridge.js +4 -4
- package/node_modules/@maka/runtime/dist/bots/feishu-bridge.js +4 -5
- package/node_modules/@maka/runtime/dist/bots/qq-bridge.js +6 -4
- package/node_modules/@maka/runtime/dist/bots/slack-bridge.js +3 -4
- package/node_modules/@maka/runtime/dist/bots/telegram-bridge.js +4 -5
- package/node_modules/@maka/runtime/dist/bots/wechat-bridge.js +17 -19
- package/node_modules/@maka/runtime/dist/bots/wecom-bridge.js +5 -6
- package/node_modules/@maka/runtime/dist/bots/ws-bridge-base.js +2 -3
- package/node_modules/@maka/runtime/dist/session-trace-projection.js +3 -30
- package/node_modules/@maka/runtime-host/dist/candidate-entry.js +4 -1
- package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
- package/node_modules/@maka/runtime-host/dist/client/peer-client.js +13 -0
- package/node_modules/@maka/runtime-host/dist/client/registered-host-termination.js +0 -59
- package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +28 -16
- package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +4 -0
- package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +8 -0
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +13 -1
- package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
- package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +28 -1
- package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +33 -0
- package/node_modules/@maka/runtime-host/dist/server/access-authority.js +26 -1
- package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +5 -1
- package/node_modules/@maka/runtime-host/dist/server/canonical-usage-reader.js +24 -11
- package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +7 -4
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +70 -6
- package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +6 -3
- package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +12 -6
- package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +9 -2
- package/node_modules/@maka/runtime-host/dist/server/root-admission-owner.js +7 -0
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +21 -0
- package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +151 -5
- package/node_modules/@maka/runtime-host/dist/server/usage-pricing-coordinator.js +4 -11
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +44 -15
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +6 -1
- package/node_modules/@maka/storage/dist/agent-run-store.js +44 -0
- package/node_modules/@maka/storage/dist/conversation-operational-state.js +4 -0
- package/node_modules/@maka/storage/dist/execution-stores.js +2 -0
- package/node_modules/@maka/storage/dist/model-call-ledger.js +199 -32
- package/node_modules/@maka/storage/dist/model-call-usage-sql.js +136 -0
- package/node_modules/@maka/storage/dist/operational-state-store-public.js +1 -1
- package/node_modules/@maka/storage/dist/operational-state-store.js +5 -3
- package/node_modules/@maka/storage/dist/sqlite-core-execution-schema.js +11 -1
- package/node_modules/@maka/storage/dist/sqlite-usage-schema.js +200 -20
- package/node_modules/@maka/storage/dist/usage-stores.js +6 -2
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
459
|
-
|
|
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
|
-
|
|
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(
|
|
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 = '
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
-
...
|
|
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.
|
|
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 ?? [],
|
|
930
|
-
coordinationRelays: mergeAddresses(discovered?.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 =
|
|
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') };
|