maka-agent 0.2.0-dev.12.20260901 → 0.2.0-dev.14.20260902

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 (105) hide show
  1. package/THIRD_PARTY_NOTICES.txt +2 -2
  2. package/dist/acp/maka-acp-agent.js +6 -3
  3. package/dist/acp/session-registry.js +307 -0
  4. package/dist/acp/stdio-server.js +20 -1
  5. package/dist/cli-core.js +15 -2
  6. package/dist/pi-transcript.js +1 -1
  7. package/dist/pi-tui-layout.js +1 -1
  8. package/dist/pi-tui-mcp-status.js +1 -1
  9. package/dist/pi-tui-runner.js +1 -1
  10. package/dist/runtime-host-access-command.js +37 -4
  11. package/dist/runtime-host-capability-provider-command.js +16 -2
  12. package/dist/runtime-host-cli-context.js +73 -3
  13. package/dist/runtime-host-cli.js +37 -6
  14. package/dist/runtime-host-run-command.js +3 -1
  15. package/dist/runtime-host-update-command.js +59 -31
  16. package/dist/tui-copy-catalog.js +4 -2
  17. package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
  18. package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
  19. package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
  20. package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
  21. package/node_modules/@maka/core/dist/attachments.js +12 -0
  22. package/node_modules/@maka/core/dist/connection-error-copy.js +17 -34
  23. package/node_modules/@maka/core/dist/events.js +0 -13
  24. package/node_modules/@maka/core/dist/maka-wordmark.js +23 -0
  25. package/node_modules/@maka/core/dist/model-catalog.js +22 -1
  26. package/node_modules/@maka/core/dist/runtime-event.js +1 -0
  27. package/node_modules/@maka/core/dist/runtime-policy/connection-catalog-codec.js +50 -1
  28. package/node_modules/@maka/core/dist/runtime-policy/credential-vault-codec.js +9 -1
  29. package/node_modules/@maka/core/dist/runtime-policy/model-catalog-entry-codec.js +10 -1
  30. package/node_modules/@maka/core/dist/runtime-policy/policy-codec.js +75 -0
  31. package/node_modules/@maka/core/dist/runtime-policy.js +4 -2
  32. package/node_modules/@maka/core/dist/session.js +1 -0
  33. package/node_modules/@maka/core/dist/settings.js +10 -2
  34. package/node_modules/@maka/core/dist/usage-record-schema.js +11 -1
  35. package/node_modules/@maka/core/package.json +2 -1
  36. package/node_modules/@maka/mcp/dist/index.js +1 -0
  37. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +52 -80
  38. package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +178 -140
  39. package/node_modules/@maka/runtime/dist/archive-read-tool.js +26 -5
  40. package/node_modules/@maka/runtime/dist/builtin-tools.js +27 -2
  41. package/node_modules/@maka/runtime/dist/context-budget-policy.js +0 -12
  42. package/node_modules/@maka/runtime/dist/conversation-copy.js +66 -43
  43. package/node_modules/@maka/runtime/dist/durable-tool-result-projection.js +31 -0
  44. package/node_modules/@maka/runtime/dist/history-compact-error.js +10 -2
  45. package/node_modules/@maka/runtime/dist/history-compact-input-fit.js +7 -1
  46. package/node_modules/@maka/runtime/dist/history-compaction.js +13 -22
  47. package/node_modules/@maka/runtime/dist/model-history.js +41 -16
  48. package/node_modules/@maka/runtime/dist/provider-image-overflow-recovery.js +12 -31
  49. package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +3 -0
  50. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +13 -2
  51. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +3 -6
  52. package/node_modules/@maka/runtime/dist/shell-tools.js +77 -1
  53. package/node_modules/@maka/runtime/dist/tool-availability.js +74 -5
  54. package/node_modules/@maka/runtime/dist/tool-result-archive-resource.js +333 -52
  55. package/node_modules/@maka/runtime/dist/tool-result-archive-transition.js +34 -3
  56. package/node_modules/@maka/runtime/dist/tool-runtime.js +12 -0
  57. package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +0 -3
  58. package/node_modules/@maka/runtime-host/dist/client/catalog-reader.js +40 -13
  59. package/node_modules/@maka/runtime-host/dist/client/connection.js +50 -31
  60. package/node_modules/@maka/runtime-host/dist/client/host-profile.js +48 -4
  61. package/node_modules/@maka/runtime-host/dist/client/index.js +2 -2
  62. package/node_modules/@maka/runtime-host/dist/client/owner-connection-code.js +32 -1
  63. package/node_modules/@maka/runtime-host/dist/client/peer-client.js +22 -4
  64. package/node_modules/@maka/runtime-host/dist/operator/access-management-frame.js +10 -1
  65. package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +56 -12
  66. package/node_modules/@maka/runtime-host/dist/peer-mesh/owner.js +3 -0
  67. package/node_modules/@maka/runtime-host/dist/protocol/configuration.js +55 -9
  68. package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +37 -0
  69. package/node_modules/@maka/runtime-host/dist/protocol/index.js +17 -1
  70. package/node_modules/@maka/runtime-host/dist/protocol/network-proxy.js +1 -7
  71. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +2 -0
  72. package/node_modules/@maka/runtime-host/dist/protocol/runtime-policy.js +48 -1
  73. package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +4 -2
  74. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +2 -12
  75. package/node_modules/@maka/runtime-host/dist/server/access-authority.js +2 -1
  76. package/node_modules/@maka/runtime-host/dist/server/agent-graph-execution-coordinator.js +1 -2
  77. package/node_modules/@maka/runtime-host/dist/server/canonical-turn-snapshot.js +0 -9
  78. package/node_modules/@maka/runtime-host/dist/server/configuration-coordinator.js +20 -1
  79. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +46 -24
  80. package/node_modules/@maka/runtime-host/dist/server/execution-service.js +4 -1
  81. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +15 -0
  82. package/node_modules/@maka/runtime-host/dist/server/listener-set.js +8 -4
  83. package/node_modules/@maka/runtime-host/dist/server/network-proxy-coordinator.js +0 -1
  84. package/node_modules/@maka/runtime-host/dist/server/peer-listener.js +3 -0
  85. package/node_modules/@maka/runtime-host/dist/server/runtime-policy-coordinator.js +28 -1
  86. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +3 -3
  87. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +35 -3
  88. package/node_modules/@maka/runtime-host/dist/server/session-sidecar-purge.js +16 -0
  89. package/node_modules/@maka/storage/dist/runtime-policy/coordinator.js +189 -6
  90. package/node_modules/@maka/storage/dist/runtime-policy-stores.js +4 -1
  91. package/node_modules/@maka/storage/dist/settings-store.js +16 -25
  92. package/node_modules/fast-uri/index.js +215 -47
  93. package/node_modules/fast-uri/lib/schemes.js +9 -4
  94. package/node_modules/fast-uri/lib/utils.js +396 -92
  95. package/node_modules/fast-uri/package.json +1 -1
  96. package/node_modules/qs/.github/THREAT_MODEL.md +3 -3
  97. package/node_modules/qs/CHANGELOG.md +15 -0
  98. package/node_modules/qs/README.md +19 -1
  99. package/node_modules/qs/dist/qs.js +21 -21
  100. package/node_modules/qs/eslint.config.mjs +1 -1
  101. package/node_modules/qs/lib/parse.js +3 -4
  102. package/node_modules/qs/lib/stringify.js +23 -8
  103. package/node_modules/qs/lib/utils.js +10 -3
  104. package/node_modules/qs/package.json +4 -4
  105. package/package.json +1 -1
@@ -16,10 +16,33 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
+ /**
20
+ * The one writer that turns a Tool Result prune decision into durable truth
21
+ * (#4283).
22
+ *
23
+ * Both prune paths — the current Turn's active prune before the next provider
24
+ * step, and the prior Turn's stale prune before compaction — come through here.
25
+ * They used to keep their replacement in a Turn-local map and a policy-carried
26
+ * ref table respectively, so each owned a private recovery contract and neither
27
+ * survived a restart. Now each records one `ModelProjectionTransition`, and the
28
+ * Session reducer is the only thing that decides what the model sees.
29
+ *
30
+ * Write order is the whole safety argument:
31
+ *
32
+ * 1. Archive the replaced body. A failure here leaves the projection untouched.
33
+ * 2. Append the transition. A failure here leaves an artifact nothing points
34
+ * at — unreachable by the reducer, so safe to reclaim once something does —
35
+ * and again leaves the projection untouched.
36
+ * 3. Only then may a caller show the replacement.
37
+ *
38
+ * There is no state in which the model has lost content the ledger cannot
39
+ * explain, and none in which a completed tool effect is repeated.
40
+ */
41
+ import { MATERIALIZED_IMAGE_TOKENS } from '@maka/core/attachments';
19
42
  import { DURABLE_TOOL_RESULT_PROJECTION_VERSION } from '@maka/core/durable-tool-result-projection';
20
43
  import { buildModelProjectionTransition, } from '@maka/core/model-projection-transition';
21
44
  import { estimateTokens, finitePositive, sha256, turnKey, utf8ByteLength, } from './context-budget-helpers.js';
22
- import { durableProjectionToToolResultOutput } from './durable-tool-result-projection.js';
45
+ import { durableProjectionToToolResultOutput, projectionArtifactMedia, } from './durable-tool-result-projection.js';
23
46
  import { baseToolResultProjection, nextInChain } from './model-projection-transition-ledger.js';
24
47
  import { ARCHIVED_TOOL_RESULT_REWRITE_VERSION, buildArchivedToolResultPlaceholder, isArchivedToolResultPlaceholder, serializeToolResultForArchive, } from './tool-result-archive.js';
25
48
  const DEFAULT_MAX_TOOL_RESULT_ESTIMATED_TOKENS = 2048;
@@ -168,8 +191,16 @@ export function collectStaleToolResultArchiveCandidates(events, prunePolicy, cha
168
191
  continue;
169
192
  const serializedResult = serializedToolResultProjection(sourceProjection);
170
193
  const originalBytes = utf8ByteLength(serializedResult);
171
- const originalEstimatedTokens = estimateTokens(serializedResult.length, charsPerToken);
172
- if (originalEstimatedTokens <= maxResultEstimatedTokens)
194
+ const media = projectionArtifactMedia(sourceProjection);
195
+ // An artifact serializes to a short reference and materializes to real
196
+ // image bytes, so the string alone would price a screenshot at nothing.
197
+ const originalEstimatedTokens = estimateTokens(serializedResult.length, charsPerToken) +
198
+ media.length * MATERIALIZED_IMAGE_TOKENS;
199
+ // A result that carries media is always a candidate: archiving it drops
200
+ // whole images from the request, which is worth doing whatever the
201
+ // reference text around them happens to weigh. The size gate is there to
202
+ // spare small text results, so it only decides those.
203
+ if (media.length === 0 && originalEstimatedTokens <= maxResultEstimatedTokens)
173
204
  continue;
174
205
  candidates.push({
175
206
  runtimeEventId: event.id,
@@ -1571,6 +1571,18 @@ export class ToolRuntime {
1571
1571
  });
1572
1572
  }
1573
1573
  catch (error) {
1574
+ try {
1575
+ await input.tool.compensateDurableOutcomeCommitFailure?.({
1576
+ result,
1577
+ isError,
1578
+ sessionId: this.input.sessionId,
1579
+ operationId,
1580
+ });
1581
+ }
1582
+ catch {
1583
+ // T2 remains authoritative. Compensation is deliberately best-effort
1584
+ // and must never replace the persistence failure that triggered it.
1585
+ }
1574
1586
  throw new RuntimeCommitBoundaryError('T2', error);
1575
1587
  }
1576
1588
  committedOutcome = {
@@ -410,9 +410,6 @@ export class RuntimeHostSessionProjector {
410
410
  recoverable: false,
411
411
  reason: root.failureClass,
412
412
  message: root.failureMessage ?? `Turn failed: ${root.failureClass}`,
413
- ...(root.contextBudgetExhaustedDetail
414
- ? { details: { contextBudgetExhaustedDetail: root.contextBudgetExhaustedDetail } }
415
- : {}),
416
413
  });
417
414
  }
418
415
  else {
@@ -30,6 +30,16 @@ export class RuntimeHostCatalogReadError extends Error {
30
30
  this.name = 'RuntimeHostCatalogReadError';
31
31
  }
32
32
  }
33
+ export class RuntimeHostSessionCatalogRevisionChangedError extends Error {
34
+ expectedRevision;
35
+ actualRevision;
36
+ constructor(expectedRevision, actualRevision) {
37
+ super('Runtime Host Session catalog revision changed');
38
+ this.expectedRevision = expectedRevision;
39
+ this.actualRevision = actualRevision;
40
+ this.name = 'RuntimeHostSessionCatalogRevisionChangedError';
41
+ }
42
+ }
33
43
  export async function readRuntimeHostConnectionCatalog(connection) {
34
44
  const { first, pages } = await collectStablePages('connection', async () => {
35
45
  const result = await connection.request('connection.catalog.query', { kind: 'start' });
@@ -105,21 +115,38 @@ export async function readRuntimeHostInvocableSkills(connection, target) {
105
115
  return pages.flatMap((page) => page.items);
106
116
  }
107
117
  export async function readRuntimeHostSessions(connection) {
108
- const { pages } = await collectStablePages('session', async () => {
109
- const result = await connection.request('session.catalog.query', {
110
- kind: 'list_start',
111
- });
112
- return result.kind === 'page' ? result : null;
113
- }, async (revision, cursor) => {
114
- const result = await connection.request('session.catalog.query', {
115
- kind: 'list_continue',
116
- revision,
117
- cursor,
118
- });
119
- return result.kind === 'page' ? result : null;
120
- });
118
+ const readPageOrRestart = async (cursor) => {
119
+ try {
120
+ return await readRuntimeHostSessionCatalogPage(connection, cursor);
121
+ }
122
+ catch (error) {
123
+ if (error instanceof RuntimeHostSessionCatalogRevisionChangedError)
124
+ return null;
125
+ throw error;
126
+ }
127
+ };
128
+ const { pages } = await collectStablePages('session', () => readPageOrRestart(), (_revision, cursor) => readPageOrRestart(cursor));
121
129
  return pages.flatMap((page) => page.sessions);
122
130
  }
131
+ export async function readRuntimeHostSessionCatalogPage(connection, cursor) {
132
+ const result = await connection.request('session.catalog.query', cursor
133
+ ? { kind: 'list_continue', revision: cursor.revision, cursor: cursor.cursor }
134
+ : { kind: 'list_start' });
135
+ if (result.kind === 'revision_changed') {
136
+ throw new RuntimeHostSessionCatalogRevisionChangedError(result.expectedRevision, result.actualRevision);
137
+ }
138
+ if (result.kind !== 'page' || (cursor && result.revision !== cursor.revision)) {
139
+ throw new RuntimeHostCatalogReadError('session', 'invalid_projection');
140
+ }
141
+ if (cursor && result.nextCursor === cursor.cursor) {
142
+ throw new RuntimeHostCatalogReadError('session', 'repeated_cursor');
143
+ }
144
+ return {
145
+ revision: result.revision,
146
+ sessions: result.sessions,
147
+ nextCursor: result.nextCursor === null ? null : { revision: result.revision, cursor: result.nextCursor },
148
+ };
149
+ }
123
150
  export async function readRuntimeHostProjects(connection) {
124
151
  return readRuntimeHostProjectCatalog(connection, 'summary');
125
152
  }
@@ -22,7 +22,7 @@ import { performance } from 'node:perf_hooks';
22
22
  import WebSocket from 'ws';
23
23
  import { discoverMarkedStorageRoot, prepareStorageRootControlDirectory, resolveExistingStorageRootControlDirectory, resolveStorageRoot, } from '@maka/storage/root-authority';
24
24
  import { readHostRegistration, RuntimeHostRegistrationError } from '../control/registration.js';
25
- import { decodeHostFrame, encodeProtocolMessage, isClientCapabilityHostFrameKind, HOST_OPERATION_SPECS, INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID, RUNTIME_HOST_COMPATIBILITY_EPOCH, RUNTIME_HOST_MAX_IN_FLIGHT_DOMAIN_REQUESTS, RUNTIME_HOST_MAX_MESSAGE_BYTES, requireClientInstanceId, requireHostCompositionId, requireHostGeneration, requireHostRootId, validateProtocolRange, } from '../protocol/index.js';
25
+ import { decodeHostFrame, encodeProtocolMessage, isClientCapabilityHostFrameKind, isHostActivityIdle, HOST_OPERATION_SPECS, INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID, RUNTIME_HOST_COMPATIBILITY_EPOCH, RUNTIME_HOST_MAX_IN_FLIGHT_DOMAIN_REQUESTS, RUNTIME_HOST_MAX_MESSAGE_BYTES, requireClientInstanceId, requireHostCompositionId, requireHostGeneration, requireHostRootId, validateProtocolRange, } from '../protocol/index.js';
26
26
  import { FramedTransport, RuntimeHostTransportError } from '../transport/framed-transport.js';
27
27
  import { WebSocketTransport } from '../transport/websocket-transport.js';
28
28
  import { ClientSessionSubscription, RuntimeHostSubscriptionError, } from './session-subscription.js';
@@ -94,17 +94,20 @@ class RuntimeHostConnectionImpl {
94
94
  #sessionCatalogChangeListeners = new Set();
95
95
  #scheduledTaskChangeListeners = new Set();
96
96
  #livenessTimer;
97
+ #livenessProbeDeadline;
97
98
  #livenessProbePending = false;
98
99
  #inFlightDomainRequests = 0;
99
100
  #terminalError;
100
101
  #livenessIntervalMs;
101
102
  #onLivenessProbe;
103
+ #onHostStatus;
102
104
  constructor(transport, accepted,
103
105
  // livenessIntervalMs is validated by connectResolvedRuntimeHost alongside
104
106
  // the other connect timeouts, before any transport work happens.
105
107
  options) {
106
108
  this.#livenessIntervalMs = options?.livenessIntervalMs ?? DEFAULT_LIVENESS_INTERVAL_MS;
107
109
  this.#onLivenessProbe = options?.onLivenessProbe;
110
+ this.#onHostStatus = options?.onHostStatus;
108
111
  this.#transport = transport;
109
112
  this.rootId = accepted.rootId;
110
113
  this.hostEpoch = accepted.hostEpoch;
@@ -131,6 +134,7 @@ class RuntimeHostConnectionImpl {
131
134
  onFailure: (error) => this.#fail(error),
132
135
  });
133
136
  void this.#readResponses();
137
+ this.#scheduleLivenessCheck();
134
138
  }
135
139
  request(operation, input, timeoutMs) {
136
140
  if (isHostStatusOperation(operation)) {
@@ -180,7 +184,6 @@ class RuntimeHostConnectionImpl {
180
184
  ...(isDomainRequest ? { domainState: 'queued' } : {}),
181
185
  timer,
182
186
  });
183
- this.#scheduleLivenessCheck();
184
187
  });
185
188
  const frame = {
186
189
  requestId,
@@ -221,6 +224,12 @@ class RuntimeHostConnectionImpl {
221
224
  this.#fail(error);
222
225
  throw error;
223
226
  }
227
+ try {
228
+ this.#onHostStatus?.(status);
229
+ }
230
+ catch {
231
+ // Observation cannot control the authenticated connection it watches.
232
+ }
224
233
  return status;
225
234
  }
226
235
  openSessionSubscription(input, timeoutMs) {
@@ -333,7 +342,6 @@ class RuntimeHostConnectionImpl {
333
342
  if (retired?.operation === frame.operation) {
334
343
  this.#retiredRequests.delete(frame.requestId);
335
344
  this.#releaseDomainSlot(retired);
336
- this.#scheduleLivenessCheck();
337
345
  return;
338
346
  }
339
347
  this.#fail(new Error('Runtime Host returned an unmatched operation response'));
@@ -346,7 +354,6 @@ class RuntimeHostConnectionImpl {
346
354
  this.#pendingRequests.delete(frame.requestId);
347
355
  if (pending.timer)
348
356
  clearTimeout(pending.timer);
349
- this.#scheduleLivenessCheck();
350
357
  if (frame.ok) {
351
358
  try {
352
359
  const accepted = pending.accept(frame.result);
@@ -423,7 +430,6 @@ class RuntimeHostConnectionImpl {
423
430
  if (index !== -1)
424
431
  this.#queuedDomainFrames.splice(index, 1);
425
432
  pending.reject(interruptedRequestError(pending.operation, 'not_dispatched', 'timeout', error));
426
- this.#scheduleLivenessCheck();
427
433
  return;
428
434
  }
429
435
  this.#retiredRequests.set(requestId, {
@@ -431,7 +437,6 @@ class RuntimeHostConnectionImpl {
431
437
  ...(pending.domainState === 'in_flight' ? { domainState: pending.domainState } : {}),
432
438
  });
433
439
  pending.reject(interruptedRequestError(pending.operation, 'dispatched', 'timeout', error));
434
- this.#scheduleLivenessCheck();
435
440
  }
436
441
  #releaseDomainSlot(request) {
437
442
  if (request.domainState !== 'in_flight')
@@ -441,20 +446,20 @@ class RuntimeHostConnectionImpl {
441
446
  this.#drainDomainRequests();
442
447
  }
443
448
  #resetLivenessCheck() {
449
+ // Any authenticated Host frame proves the transport is still making
450
+ // progress, even when a periodic status observation is slow.
451
+ this.#resetLivenessProbeDeadline();
452
+ // A status observer needs periodic route observations even while other
453
+ // Session traffic keeps the connection active.
454
+ if (this.#onHostStatus)
455
+ return;
444
456
  if (this.#livenessTimer)
445
457
  clearTimeout(this.#livenessTimer);
446
458
  this.#livenessTimer = undefined;
447
459
  this.#scheduleLivenessCheck();
448
460
  }
449
461
  #scheduleLivenessCheck() {
450
- if (this.#terminalError ||
451
- this.#livenessTimer ||
452
- this.#livenessProbePending ||
453
- !this.#hasOutstandingDomainRequest()) {
454
- if (!this.#hasOutstandingDomainRequest() && this.#livenessTimer) {
455
- clearTimeout(this.#livenessTimer);
456
- this.#livenessTimer = undefined;
457
- }
462
+ if (this.#terminalError || this.#livenessTimer || this.#livenessProbePending) {
458
463
  return;
459
464
  }
460
465
  this.#livenessTimer = setTimeout(() => {
@@ -462,21 +467,12 @@ class RuntimeHostConnectionImpl {
462
467
  this.#startLivenessProbe();
463
468
  }, this.#livenessIntervalMs);
464
469
  }
465
- #hasOutstandingDomainRequest() {
466
- if (this.#retiredRequests.size > 0)
467
- return true;
468
- for (const pending of this.#pendingRequests.values()) {
469
- if (pending.operation !== 'host.status')
470
- return true;
471
- }
472
- return false;
473
- }
474
470
  #startLivenessProbe() {
475
- if (this.#terminalError || this.#livenessProbePending || !this.#hasOutstandingDomainRequest()) {
471
+ if (this.#terminalError || this.#livenessProbePending)
476
472
  return;
477
- }
478
473
  this.#livenessProbePending = true;
479
- void this.#requestOperation('host.status', {}, DEFAULT_LIVENESS_TIMEOUT_MS, (status) => {
474
+ this.#resetLivenessProbeDeadline();
475
+ void this.#requestOperation('host.status', {}, undefined, (status) => {
480
476
  this.#validateHostStatusIdentity(status);
481
477
  try {
482
478
  this.#onLivenessProbe?.();
@@ -485,13 +481,26 @@ class RuntimeHostConnectionImpl {
485
481
  // Diagnostics hook: an observer exception must never fail the
486
482
  // connection it is watching.
487
483
  }
488
- }, 'connection')
484
+ }, 'request')
489
485
  .catch((error) => this.#fail(asError(error)))
490
486
  .finally(() => {
487
+ if (this.#livenessProbeDeadline)
488
+ clearTimeout(this.#livenessProbeDeadline);
489
+ this.#livenessProbeDeadline = undefined;
491
490
  this.#livenessProbePending = false;
492
491
  this.#scheduleLivenessCheck();
493
492
  });
494
493
  }
494
+ #resetLivenessProbeDeadline() {
495
+ if (!this.#livenessProbePending || this.#terminalError)
496
+ return;
497
+ if (this.#livenessProbeDeadline)
498
+ clearTimeout(this.#livenessProbeDeadline);
499
+ this.#livenessProbeDeadline = setTimeout(() => {
500
+ this.#livenessProbeDeadline = undefined;
501
+ this.#fail(requestTimeoutError('host.status'));
502
+ }, DEFAULT_LIVENESS_TIMEOUT_MS);
503
+ }
495
504
  #acceptSubscriptionFrame(frame) {
496
505
  const subscription = this.#subscriptions.get(frame.subscriptionId);
497
506
  if (!subscription) {
@@ -545,6 +554,9 @@ class RuntimeHostConnectionImpl {
545
554
  if (this.#livenessTimer)
546
555
  clearTimeout(this.#livenessTimer);
547
556
  this.#livenessTimer = undefined;
557
+ if (this.#livenessProbeDeadline)
558
+ clearTimeout(this.#livenessProbeDeadline);
559
+ this.#livenessProbeDeadline = undefined;
548
560
  this.#queuedDomainFrames.length = 0;
549
561
  this.#inFlightDomainRequests = 0;
550
562
  for (const pending of this.#pendingRequests.values()) {
@@ -628,6 +640,7 @@ export async function connectRemoteRuntimeHost(input) {
628
640
  handshakeTimeoutMs: normalized.handshakeTimeoutMs,
629
641
  livenessIntervalMs: normalized.livenessIntervalMs,
630
642
  onLivenessProbe: input.onLivenessProbe,
643
+ onHostStatus: input.onHostStatus,
631
644
  connectionResource: input.connectionResource,
632
645
  });
633
646
  }
@@ -639,11 +652,13 @@ export async function connectRemoteRuntimeHost(input) {
639
652
  export async function connectRuntimeHostMessageTransport(input) {
640
653
  let resourceTransferred = false;
641
654
  let timer;
655
+ let handshakeTimedOut = false;
642
656
  try {
643
657
  const normalized = normalizeConnectRuntimeHostInput(input);
644
658
  const compositionId = requireHostCompositionId(input.compositionId);
645
659
  const expectedRootId = requireHostRootId(input.expectedRootId);
646
660
  timer = setTimeout(() => {
661
+ handshakeTimedOut = true;
647
662
  input.transport.abort(new Error('Timed out handshaking with Runtime Host'));
648
663
  }, normalized.handshakeTimeoutMs);
649
664
  const result = await exchangeRuntimeHostHandshake({
@@ -654,6 +669,7 @@ export async function connectRuntimeHostMessageTransport(input) {
654
669
  expectedRootId,
655
670
  livenessIntervalMs: normalized.livenessIntervalMs,
656
671
  onLivenessProbe: input.onLivenessProbe,
672
+ onHostStatus: input.onHostStatus,
657
673
  connectionResource: input.connectionResource,
658
674
  ...(input.peerPath ? { peerPath: input.peerPath } : {}),
659
675
  });
@@ -672,7 +688,10 @@ export async function connectRuntimeHostMessageTransport(input) {
672
688
  if (error instanceof RuntimeHostCompositionMismatchError) {
673
689
  return { kind: 'unavailable', reason: 'composition_mismatch' };
674
690
  }
675
- return { kind: 'unavailable', reason: 'handshake_failed' };
691
+ return {
692
+ kind: 'unavailable',
693
+ reason: handshakeTimedOut ? 'handshake_timed_out' : 'handshake_failed',
694
+ };
676
695
  }
677
696
  finally {
678
697
  if (timer)
@@ -819,6 +838,7 @@ export async function connectResolvedRuntimeHost(input) {
819
838
  hostProtocol: { min: registration.protocolMin, max: registration.protocolMax },
820
839
  livenessIntervalMs,
821
840
  onLivenessProbe: input.onLivenessProbe,
841
+ onHostStatus: input.onHostStatus,
822
842
  });
823
843
  if (result.kind === 'connected') {
824
844
  if (generation !== undefined &&
@@ -838,9 +858,7 @@ export async function connectResolvedRuntimeHost(input) {
838
858
  return registration.lifecycleMode === 'ephemeral' &&
839
859
  result.handshake.state === 'ready' &&
840
860
  result.handshake.activity !== undefined &&
841
- result.handshake.activity.connections === 0 &&
842
- result.handshake.activity.activeOperations === 0 &&
843
- result.handshake.activity.residencies.length === 0
861
+ isHostActivityIdle(result.handshake.activity)
844
862
  ? {
845
863
  kind: 'upgrade_required',
846
864
  registration,
@@ -954,6 +972,7 @@ async function exchangeRuntimeHostHandshake(input) {
954
972
  connection: new RuntimeHostConnectionImpl(input.transport, handshake, {
955
973
  livenessIntervalMs: input.livenessIntervalMs,
956
974
  onLivenessProbe: input.onLivenessProbe,
975
+ onHostStatus: input.onHostStatus,
957
976
  connectionResource: input.connectionResource,
958
977
  ...(input.peerPath ? { peerPath: input.peerPath } : {}),
959
978
  }),
@@ -41,6 +41,7 @@ const PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
41
41
  const PEER_ID_MAX_BYTES = 160;
42
42
  const PEER_ADDRESS_MAX_BYTES = 2 * 1024;
43
43
  const PEER_ROUTE_MAX = 16;
44
+ const DEFAULT_PEER_HANDSHAKE_TIMEOUT_MS = 5_000;
44
45
  const PROFILE_CREDENTIAL_RECORD_PREFIX = 'maka-runtime-host-profile-credential-v1:';
45
46
  const PROFILE_INCARNATION_ID_MAX_BYTES = 128;
46
47
  export const RUNTIME_HOST_ACCESS_CREDENTIAL_MAX_BYTES = 8 * 1024;
@@ -173,6 +174,9 @@ export async function connectRemoteRuntimeHostProfile(input, overrides = {}) {
173
174
  expectedRootId: input.profile.rootId,
174
175
  clientInstanceId: input.clientInstanceId,
175
176
  peerClient: requireRuntimeHostPeerClient(input.peerClient),
177
+ ...(input.refreshPeerRoutes === undefined
178
+ ? {}
179
+ : { refreshPeerRoutes: input.refreshPeerRoutes }),
176
180
  ...(input.signal === undefined ? {} : { signal: input.signal }),
177
181
  ...(input.connectTimeoutMs === undefined ? {} : { connectTimeoutMs: input.connectTimeoutMs }),
178
182
  ...(input.handshakeTimeoutMs === undefined
@@ -181,6 +185,7 @@ export async function connectRemoteRuntimeHostProfile(input, overrides = {}) {
181
185
  ...(input.onConnectionPhase === undefined
182
186
  ? {}
183
187
  : { onConnectionPhase: input.onConnectionPhase }),
188
+ ...(input.onHostStatus === undefined ? {} : { onHostStatus: input.onHostStatus }),
184
189
  });
185
190
  }
186
191
  else {
@@ -224,6 +229,7 @@ export async function connectRemoteRuntimeHostProfile(input, overrides = {}) {
224
229
  ...(input.handshakeTimeoutMs === undefined
225
230
  ? {}
226
231
  : { handshakeTimeoutMs: input.handshakeTimeoutMs }),
232
+ ...(input.onHostStatus === undefined ? {} : { onHostStatus: input.onHostStatus }),
227
233
  });
228
234
  try {
229
235
  input.signal?.throwIfAborted();
@@ -272,11 +278,13 @@ function requireConnectOnlySshEndpoint(transport) {
272
278
  }
273
279
  export async function connectPeerRuntimeHost(input) {
274
280
  input.signal?.throwIfAborted();
281
+ const handshakeTimeoutMs = input.handshakeTimeoutMs ?? DEFAULT_PEER_HANDSHAKE_TIMEOUT_MS;
275
282
  const stream = await input.peerClient.connect({
276
283
  peerId: input.transport.peerId,
277
284
  routeHints: input.transport.routeHints,
278
285
  coordinationRelays: input.transport.coordinationRelays,
279
286
  directDeadlineMs: Math.min(input.connectTimeoutMs ?? 40_000, 120_000),
287
+ ...(input.refreshPeerRoutes === undefined ? {} : { refreshRoutes: input.refreshPeerRoutes }),
280
288
  }, input.signal, input.onConnectionPhase);
281
289
  const abort = () => stream.abort();
282
290
  input.signal?.addEventListener('abort', abort, { once: true });
@@ -287,7 +295,7 @@ export async function connectPeerRuntimeHost(input) {
287
295
  input.signal?.throwIfAborted();
288
296
  notifyConnectionPhase(input.onConnectionPhase, 'authenticating');
289
297
  await writeRuntimeHostPeerAuthentication(stream, input.credential);
290
- const authentication = await readRuntimeHostPeerAuthenticationResult(stream, input.handshakeTimeoutMs);
298
+ const authentication = await readRuntimeHostPeerAuthenticationResult(stream, handshakeTimeoutMs);
291
299
  if (!authentication.accepted) {
292
300
  throw new RuntimeHostProfileConnectionError('credential_rejected', `Runtime Host profile ${input.profileId} rejected its access credential`);
293
301
  }
@@ -301,9 +309,14 @@ export async function connectPeerRuntimeHost(input) {
301
309
  max: RUNTIME_HOST_PROTOCOL_VERSION,
302
310
  },
303
311
  clientInstanceId: input.clientInstanceId,
304
- ...(input.handshakeTimeoutMs === undefined
305
- ? {}
306
- : { handshakeTimeoutMs: input.handshakeTimeoutMs }),
312
+ handshakeTimeoutMs,
313
+ onHostStatus: (status) => {
314
+ const endpoint = status.peerEndpoint;
315
+ if (endpoint?.peerId === input.transport.peerId) {
316
+ input.peerClient.observeAuthenticatedRoutes(endpoint);
317
+ }
318
+ input.onHostStatus?.(status);
319
+ },
307
320
  ...(stream.path ? { peerPath: stream.path } : {}),
308
321
  });
309
322
  input.signal?.throwIfAborted();
@@ -357,6 +370,9 @@ export function remoteRuntimeHostUnavailableError(subject, reason) {
357
370
  case 'unreachable':
358
371
  message = `${subject} could not reach its endpoint`;
359
372
  break;
373
+ case 'handshake_timed_out':
374
+ message = `${subject} timed out while establishing its protocol session`;
375
+ break;
360
376
  default:
361
377
  message = `${subject} is unavailable (${reason})`;
362
378
  }
@@ -589,6 +605,34 @@ class FileRuntimeHostProfileCatalog {
589
605
  return { rebound: true, document: next };
590
606
  });
591
607
  }
608
+ updateRemoteProfileIfCurrent(target, update) {
609
+ const expectedProfile = decodeRemoteRuntimeHostProfile(target.profile);
610
+ const expectedIncarnationId = requireProfileIncarnationId(target.profileIncarnationId);
611
+ return this.#exclusive(async () => {
612
+ const current = await this.#readSnapshot();
613
+ const profile = current.profiles.find((candidate) => candidate.id === expectedProfile.id && candidate.kind === 'remote');
614
+ if (!profile || !sameRemoteRuntimeHostProfileTarget(profile, expectedProfile))
615
+ return false;
616
+ const credential = await this.credentials.get(profile);
617
+ if (credential?.profileIncarnationId !== expectedIncarnationId)
618
+ return false;
619
+ const value = update(profile);
620
+ if (value === profile)
621
+ return true;
622
+ const updated = decodeRemoteRuntimeHostProfile(value);
623
+ if (updated.id !== profile.id ||
624
+ !sameRemoteRuntimeHostProfileTarget(updated, profile) ||
625
+ runtimeHostProfileAccess(updated) !== runtimeHostProfileAccess(profile)) {
626
+ throw new Error('A Runtime Host profile metadata update must retain its connection');
627
+ }
628
+ const next = decodeRuntimeHostProfileDocument({
629
+ schemaVersion: PROFILE_SCHEMA_VERSION,
630
+ profiles: current.profiles.map((candidate) => candidate.id === updated.id ? updated : candidate),
631
+ });
632
+ await writeProfileDocument(this.path, next);
633
+ return true;
634
+ });
635
+ }
592
636
  mutateRemoteProfileIfCurrent(target, mutation) {
593
637
  const expectedProfile = decodeRemoteRuntimeHostProfile(target.profile);
594
638
  const expectedIncarnationId = requireProfileIncarnationId(target.profileIncarnationId);
@@ -30,7 +30,7 @@ export { RUNTIME_HOST_REMOTE_INCOMPATIBLE_CODE, RuntimeHostRemoteCompatibilityEr
30
30
  export { RuntimeHostSubscriptionError, } from './session-subscription.js';
31
31
  export { RuntimeHostStartupError, runtimeHostStartupError, } from './startup-error.js';
32
32
  export { connectRuntimeHostWslEnvironment, listRuntimeHostWslDistributions, normalizeRuntimeHostWslDistribution, normalizeRuntimeHostWslOperatorPath, resolveSystemRuntimeHostWslExecutable, } from './wsl-environment.js';
33
- export { RuntimeHostCatalogReadError, readRuntimeHostConnectionCatalog, readRuntimeHostInvocableSkills, readRuntimeHostProjectDetails, readRuntimeHostResources, readRuntimeHostProjects, readRuntimeHostSessions, readRuntimeHostSkillCatalog, } from './catalog-reader.js';
33
+ export { RuntimeHostCatalogReadError, RuntimeHostSessionCatalogRevisionChangedError, readRuntimeHostConnectionCatalog, readRuntimeHostInvocableSkills, readRuntimeHostProjectDetails, readRuntimeHostResources, readRuntimeHostProjects, readRuntimeHostSessionCatalogPage, readRuntimeHostSessions, readRuntimeHostSkillCatalog, } from './catalog-reader.js';
34
34
  export { connectOrSpawnRuntimeHost, } from './connect-or-spawn.js';
35
35
  export { abortable, waitForRuntimeHostReady } from './wait-for-ready.js';
36
36
  export { createRuntimeHostCandidateLaunchBarrier, } from './candidate-launch-barrier.js';
@@ -40,7 +40,7 @@ export { startRuntimeHostCapabilityProviderService, } from './capability-provide
40
40
  export { loadOrCreateRuntimeHostClientInstanceId } from './client-instance-identity.js';
41
41
  export { projectSessionCatalogSummary } from './session-catalog-summary.js';
42
42
  export { consumeAccessCredentialDelivery } from '../control/access-credential-delivery.js';
43
- export { decodeRuntimeHostOwnerConnectionCode, encodeRuntimeHostOwnerConnectionCode, } from './owner-connection-code.js';
43
+ export { decodeRuntimeHostOwnerConnectionCode, encodeRuntimeHostOwnerConnectionCode, issueRuntimeHostOwnerConnectionCode, REMOTE_DESKTOP_OWNER_ACCESS_POLICY, } from './owner-connection-code.js';
44
44
  export { ensureRuntimeHostPeerIdentity } from '../transport/peer-native.js';
45
45
  export { createRuntimeHostPeerClient, createRuntimeHostPeerClientFromEnvironment, } from './peer-client.js';
46
46
  export { createOAuthPresentationClientProvider, } from './oauth-presentation.js';
@@ -17,14 +17,22 @@
17
17
  * under the License.
18
18
  */
19
19
  import { z } from 'zod';
20
- import { requireHostRootId } from '../protocol/index.js';
20
+ import { consumeAccessCredentialDelivery } from '../control/access-credential-delivery.js';
21
+ import { REMOTE_OWNER_OPERATION_GRANTS, requireHostRootId } from '../protocol/index.js';
21
22
  import { decodeRuntimeHostRemoteTransport, RUNTIME_HOST_ACCESS_CREDENTIAL_MAX_BYTES, } from './host-profile.js';
22
23
  const PREFIX = 'maka-runtime-host:connect:v1:';
23
24
  const ENCODED_MAX_BYTES = 48 * 1024;
25
+ export const REMOTE_DESKTOP_OWNER_ACCESS_POLICY = Object.freeze({
26
+ principalKind: 'remote_owner',
27
+ operationGrants: REMOTE_OWNER_OPERATION_GRANTS,
28
+ canPublishClientCapabilities: true,
29
+ canUseHostPaths: false,
30
+ });
24
31
  const boundedString = (maxBytes) => z
25
32
  .string()
26
33
  .min(1)
27
34
  .refine((value) => Buffer.byteLength(value, 'utf8') <= maxBytes);
35
+ const nameSchema = boundedString(128);
28
36
  const payloadSchema = z
29
37
  .object({
30
38
  schemaVersion: z.literal(1),
@@ -42,6 +50,29 @@ const payloadSchema = z
42
50
  credential: boundedString(RUNTIME_HOST_ACCESS_CREDENTIAL_MAX_BYTES),
43
51
  })
44
52
  .strict();
53
+ /**
54
+ * Issue one pending Owner credential and bind it to the Host's current,
55
+ * authenticated Direct peer endpoint. The credential remains one-time and
56
+ * short-lived until the importing Client finalizes it.
57
+ */
58
+ export async function issueRuntimeHostOwnerConnectionCode(input) {
59
+ const name = nameSchema.parse(input.name);
60
+ const rootId = requireHostRootId(input.client.rootId);
61
+ const endpoint = (await input.client.status()).peerEndpoint;
62
+ if (!endpoint)
63
+ throw new Error('Runtime Host Direct peer is not available');
64
+ if (input.expectedPeerId && endpoint.peerId !== input.expectedPeerId) {
65
+ throw new Error('Runtime Host Direct peer identity changed');
66
+ }
67
+ const transport = requireDirectPeerTransport({ kind: 'libp2p-direct', ...endpoint });
68
+ const prepared = await input.client.request('access.credential.prepare', {
69
+ ...REMOTE_DESKTOP_OWNER_ACCESS_POLICY,
70
+ principalId: input.principalId,
71
+ bindClientInstance: true,
72
+ });
73
+ const credential = await consumeAccessCredentialDelivery(input.rootPath, prepared.deliveryId, prepared.credentialId);
74
+ return encodeRuntimeHostOwnerConnectionCode({ name, rootId, transport, credential });
75
+ }
45
76
  export function encodeRuntimeHostOwnerConnectionCode(input) {
46
77
  const transport = requireDirectPeerTransport(input.transport);
47
78
  const payload = payloadSchema.parse({ schemaVersion: 1, ...input, transport });
@@ -38,6 +38,7 @@ class RuntimeHostPeerClientImpl {
38
38
  #automaticRelayDiscovery;
39
39
  #webRtcStunUrls;
40
40
  #routeResolver;
41
+ #authenticatedRoutes = new Map();
41
42
  #endpoint;
42
43
  #draining;
43
44
  #meshDraining;
@@ -95,9 +96,19 @@ class RuntimeHostPeerClientImpl {
95
96
  throw normalizePeerError(error);
96
97
  });
97
98
  }
99
+ observeAuthenticatedRoutes(input) {
100
+ if (input.routeHints.length === 0 && input.coordinationRelays.length === 0)
101
+ return;
102
+ this.#authenticatedRoutes.set(input.peerId, Object.freeze({
103
+ routeHints: Object.freeze([...input.routeHints]),
104
+ coordinationRelays: Object.freeze([...input.coordinationRelays]),
105
+ }));
106
+ }
98
107
  async connect(input, signal, onPhase) {
99
- notifyPhase(onPhase, 'discovering');
100
- await this.#prepareRoutes(input, signal);
108
+ if (input.refreshRoutes !== false) {
109
+ notifyPhase(onPhase, 'discovering');
110
+ await this.#prepareRoutes(input, signal);
111
+ }
101
112
  notifyPhase(onPhase, 'connecting');
102
113
  return this.#connect(input, signal, 'application');
103
114
  }
@@ -193,10 +204,17 @@ class RuntimeHostPeerClientImpl {
193
204
  const endpoint = this.#requireEndpoint();
194
205
  const requestId = this.#allocateRequestId();
195
206
  const discovered = kind === 'application' ? this.#routeResolver?.resolveRoutes(input.peerId) : undefined;
207
+ const authenticated = kind === 'application' ? this.#authenticatedRoutes.get(input.peerId) : undefined;
196
208
  const connection = endpoint[kind === 'application' ? 'connect' : 'connectMeshControl']({
197
209
  ...input,
198
- routeHints: mergeAddresses(discovered?.routeHints ?? [], input.routeHints),
199
- coordinationRelays: mergeAddresses(discovered?.coordinationRelays ?? [], input.coordinationRelays),
210
+ routeHints: mergeAddresses(discovered?.routeHints ?? [], [
211
+ ...(authenticated?.routeHints ?? []),
212
+ ...input.routeHints,
213
+ ]),
214
+ coordinationRelays: mergeAddresses(discovered?.coordinationRelays ?? [], [
215
+ ...(authenticated?.coordinationRelays ?? []),
216
+ ...(input.coordinationRelays ?? []),
217
+ ]),
200
218
  transitRelayPeerIds: mergeValues(discovered?.transitRelayPeerIds ?? [], input.transitRelayPeerIds, 64),
201
219
  requestId,
202
220
  });