@zq-silk/yui 0.15.9 → 0.15.12

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 (160) hide show
  1. package/ARCHITECTURE.md +8 -4
  2. package/ARCHITECTURE.zh-CN.md +5 -2
  3. package/README.md +13 -5
  4. package/dist/agent/launchEnvironment.js +7 -0
  5. package/dist/agentRun/agentRun.js +3 -0
  6. package/dist/cli/commandCatalog.js +64 -16
  7. package/dist/cli/interactionPolicy.js +7 -3
  8. package/dist/cli/managedDiagnostics.js +1 -1
  9. package/dist/cli/updateOrchestrator.js +24 -1
  10. package/dist/cli/updatePorts.js +7 -3
  11. package/dist/cli/upgradeCommand.js +42 -2
  12. package/dist/cli.js +381 -107
  13. package/dist/commands/executionAuditCommands.js +10 -0
  14. package/dist/commands/globalRoleCommands.js +339 -4
  15. package/dist/commands/projectCommands.js +50 -22
  16. package/dist/commands/releaseCommands.js +18 -0
  17. package/dist/commands/taskActor.js +25 -0
  18. package/dist/commands/taskCommands.js +586 -96
  19. package/dist/commands/taskIntegrationCommands.js +19 -39
  20. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  21. package/dist/commands/taskOverviewCommand.js +4 -3
  22. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  23. package/dist/commands/taskPublicationCommands.js +11 -2
  24. package/dist/commands/taskPublicationVerifyCommand.js +23 -39
  25. package/dist/commands/taskRemoteDeliveryCommand.js +22 -11
  26. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  27. package/dist/context/runContextPack.js +3 -0
  28. package/dist/context/taskCatalog.js +187 -0
  29. package/dist/context/taskContext.js +55 -6
  30. package/dist/controller/agentHostObservation.js +155 -0
  31. package/dist/controller/clientRuntime.js +17 -2
  32. package/dist/controller/controller.js +14 -2
  33. package/dist/controller/fileSchedulerStoreAdapter.js +519 -25
  34. package/dist/controller/globalInputDelivery.js +132 -0
  35. package/dist/controller/jobControl.js +6 -2
  36. package/dist/controller/providerRetryAdmission.js +100 -0
  37. package/dist/controller/providerRetryDelivery.js +218 -0
  38. package/dist/controller/resourceInventory.js +14 -4
  39. package/dist/controller/resourceInventoryLinux.js +2 -6
  40. package/dist/controller/runtime.js +117 -7
  41. package/dist/controller/runtimeEventInbox.js +32 -3
  42. package/dist/controller/runtimeEventProcessor.js +26 -6
  43. package/dist/controller/runtimeHookRunFence.js +75 -19
  44. package/dist/controller/structuredProviderObservation.js +133 -70
  45. package/dist/coordination/workMailboxQueue.js +5 -0
  46. package/dist/execution/workItemExecutionProjection.js +1 -1
  47. package/dist/executor/agentExecutor.js +64 -4
  48. package/dist/executor/executorRegistry.js +3 -0
  49. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  50. package/dist/integration/deliveryObligation.js +2 -1
  51. package/dist/integration/gitIntegrationService.js +329 -386
  52. package/dist/integration/integrationAttempt.js +30 -4
  53. package/dist/integration/integrationQueueService.js +7 -7
  54. package/dist/integration/integrationSourceApplication.js +323 -0
  55. package/dist/lifecycle/exactRunTerminalization.js +4 -1
  56. package/dist/message/globalInterrupt.js +33 -0
  57. package/dist/message/globalProviderRetry.js +15 -0
  58. package/dist/message/inputControlResolution.js +106 -0
  59. package/dist/message/message.js +367 -0
  60. package/dist/message/messageContinuation.js +126 -3
  61. package/dist/message/taskInterrupt.js +34 -0
  62. package/dist/observability/executionAudit.js +19 -0
  63. package/dist/observability/orchestrationMetrics.js +1 -1
  64. package/dist/release/releaseHandover.js +22 -0
  65. package/dist/release/releaseWorkflowPorts.js +15 -7
  66. package/dist/repository/gitWorkspace.js +430 -107
  67. package/dist/repository/projectMaintenanceLock.js +75 -18
  68. package/dist/repository/taskWorkspaceCoordinator.js +182 -101
  69. package/dist/repository/taskWorkspacePreparer.js +205 -72
  70. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  71. package/dist/repository/workspaceCleanupInspection.js +187 -0
  72. package/dist/resources/resourceDiscovery.js +3 -2
  73. package/dist/runtime/agentError.js +5 -3
  74. package/dist/runtime/agentHost.js +179 -82
  75. package/dist/runtime/agentHostCompatibility.js +127 -0
  76. package/dist/runtime/agentHostProtocol.js +53 -0
  77. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  78. package/dist/runtime/codexAppServerRuntime.js +34 -3
  79. package/dist/runtime/executionEnvironment.js +0 -19
  80. package/dist/runtime/launchBroker.js +6 -0
  81. package/dist/runtime/providerControl.js +5 -1
  82. package/dist/runtime/providerRetry.js +198 -0
  83. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  84. package/dist/runtime/sessionReconciliation.js +4 -4
  85. package/dist/runtime/sessionTokenMetrics.js +15 -5
  86. package/dist/runtime/structuredProviderHost.js +6 -2
  87. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  88. package/dist/runtime/taskUsageMetrics.js +275 -0
  89. package/dist/runtime/tmuxAdapters.js +5 -3
  90. package/dist/scheduler/activeRoleRunDelivery.js +12 -0
  91. package/dist/scheduler/leaderWakeupProcessor.js +5 -0
  92. package/dist/scheduler/operatorEvent.js +4 -0
  93. package/dist/scheduler/taskExecutionProjection.js +38 -6
  94. package/dist/scheduler/taskObservabilityProjection.js +6 -44
  95. package/dist/scheduler/wakeReason.js +7 -1
  96. package/dist/scheduler/wakeupQueue.js +2 -0
  97. package/dist/setup/setupCommand.js +26 -8
  98. package/dist/storage/homeLayout.js +130 -0
  99. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  100. package/dist/storage/migrations/integrationContinuation.js +104 -0
  101. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  102. package/dist/storage/sqliteSchema.js +167 -4
  103. package/dist/storage/sqliteStore.js +57 -1
  104. package/dist/storage/storageVersions.js +1 -1
  105. package/dist/storage/storeRpc.js +2 -0
  106. package/dist/storage/taskCatalog.js +123 -0
  107. package/dist/storage/taskStore.js +2 -0
  108. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  109. package/dist/task/archiveDiagnostics.js +129 -0
  110. package/dist/task/archivePreflight.js +124 -0
  111. package/dist/task/nextAction.js +44 -11
  112. package/dist/task/publicationAdoption.js +56 -0
  113. package/dist/task/publicationReference.js +10 -0
  114. package/dist/task/remoteDelivery.js +31 -16
  115. package/dist/web/assets/client/app.js +147 -17
  116. package/dist/web/assets/client/components.js +56 -13
  117. package/dist/web/assets/client/i18n.js +78 -4
  118. package/dist/web/assets/client/taskSurface.js +108 -1
  119. package/dist/web/assets/client/view.js +39 -8
  120. package/dist/web/assets/shell.js +29 -0
  121. package/dist/web/assets/styles/layout.js +8 -1
  122. package/dist/web/assets/styles/widgets.js +12 -0
  123. package/dist/web/webServer.js +131 -4
  124. package/dist/web/webSnapshot.js +16 -6
  125. package/dist/web/webTaskSurface.js +222 -5
  126. package/dist/workspace/cleanupInspection.js +63 -0
  127. package/dist/workspace/workItemChangeSetManager.js +111 -35
  128. package/docs/agent-result-consumption.md +4 -0
  129. package/docs/agent-result-consumption.zh-CN.md +3 -0
  130. package/docs/agent-runtime-drivers.md +7 -0
  131. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  132. package/docs/architecture/README.md +2 -0
  133. package/docs/architecture/README.zh-CN.md +3 -1
  134. package/docs/architecture/capabilities-and-resources.md +30 -5
  135. package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
  136. package/docs/managed-turn-and-session-runtime.md +47 -0
  137. package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
  138. package/docs/observability/README.md +62 -0
  139. package/docs/observability/README.zh-CN.md +47 -0
  140. package/docs/project-refresh.md +77 -0
  141. package/docs/project-refresh.zh-CN.md +59 -0
  142. package/docs/provider-retry.md +70 -0
  143. package/docs/release-workflow.md +39 -0
  144. package/docs/release-workflow.zh-CN.md +29 -0
  145. package/docs/sqlite-control-plane-design.md +223 -1
  146. package/docs/task-delivery.md +133 -13
  147. package/docs/task-delivery.zh-CN.md +99 -10
  148. package/docs/task-discovery.md +102 -0
  149. package/docs/task-discovery.zh-CN.md +86 -0
  150. package/docs/testing/verification-levels.md +40 -0
  151. package/docs/testing/verification-levels.zh-CN.md +23 -0
  152. package/i18n/README.zh-CN.md +13 -7
  153. package/package.json +1 -1
  154. package/skills/yui-leader/references/execution.md +154 -51
  155. package/skills/yui-leader/references/integration.md +52 -2
  156. package/skills/yui-operator/SKILL.md +19 -3
  157. package/skills/yui-reviewer/SKILL.md +4 -0
  158. package/skills/yui-runtime/SKILL.md +42 -0
  159. package/skills/yui-runtime/references/publication.md +42 -0
  160. package/skills/yui-runtime/references/recovery.md +24 -0
@@ -4,13 +4,11 @@ import { tmpdir, homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { createConnection, createServer } from "node:net";
6
6
  import { createInterface } from "node:readline";
7
- import { assertAgentExecutionEnvironment } from "./executionEnvironment.js";
8
7
  import { withSessionContextPointer } from "../context/sessionBootstrapManifest.js";
9
8
  import { callController, controllerCallMayHaveApplied, ControllerClientError } from "../core/controllerClient.js";
10
9
  import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
11
- import { callFileTaskController } from "../controller/clientRuntime.js";
12
10
  import { isForeignHandoverLockHeld } from "../release/runtimeRelease.js";
13
- import { publishStructuredProviderAccepted, publishStructuredProviderAttachmentExit, publishStructuredProviderActivity, publishStructuredProviderInputSettlement, publishStructuredProviderInputObserved, publishStructuredProviderGoal, publishStructuredConversationRecoverability, publishStructuredProviderOpened, publishStructuredProviderStarted, publishStructuredProviderTerminal } from "../controller/structuredProviderObservation.js";
11
+ import { publishStructuredProviderAccepted, publishStructuredProviderAttachmentExit, publishStructuredProviderActivity, publishStructuredProviderInputSettlement, publishStructuredProviderInputObserved, publishStructuredProviderGoal, publishStructuredConversationRecoverability, publishStructuredProviderOpened, publishStructuredProviderStarted, publishStructuredProviderTerminal, publishStructuredProviderConnection, structuredProviderEventDelivery } from "../controller/structuredProviderObservation.js";
14
12
  import { validateAgentHostLaunchPayload } from "./launchBroker.js";
15
13
  import { ProviderDeliveryUnknownError, ProviderConversationMissingError, ProviderTurnBusyError, ProviderTurnRejectedError } from "./structuredProviderHost.js";
16
14
  import { createAgentEndpointOwner } from "./agentEndpointOwnership.js";
@@ -20,16 +18,50 @@ import { persistRuntimeProcessExitObservation, replayRuntimeProcessExitOutbox }
20
18
  import { AGENT_HOST_CLIENT_EXIT_GRACE_MS, AGENT_HOST_CONTROL_TIMEOUT_MS, AGENT_HOST_READY_TIMEOUT_MS, ENDPOINT_DRAIN_TIMEOUT_MS, PROVIDER_ACCEPT_TIMEOUT_MS } from "./runtimeDeadlines.js";
21
19
  import { providerDeliveryFailure, providerDeliveryFailureFrom, redactAgentErrorText, serializeAgentErrorRaw } from "./agentError.js";
22
20
  import { runCodexInteractiveHost } from "./codexInteractiveHost.js";
21
+ import { CodexPreSubmissionError } from "./codexAppServerRuntime.js";
23
22
  import { readAgentRunConfigurationObservation, unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
24
- import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
25
23
  import { createSessionOwnerIdentity, readLinuxProcessIdentity } from "./sessionOwnerIdentity.js";
26
24
  import { yuiTmuxServerName, yuiTmuxSessionName } from "../tmux/tmuxManager.js";
27
25
  import { tmuxSocketDirectory } from "../tmux/tmuxSocketEndpoint.js";
28
- import { createTaskEvent } from "../event/taskEvent.js";
29
- export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v5";
26
+ import { AGENT_HOST_CONTROL_PROTOCOL, AGENT_HOST_EVENT_PROTOCOL } from "./agentHostProtocol.js";
27
+ import { FILE_TASK_CONTROLLER_PROTOCOL_VERSION } from "../core/protocol.js";
28
+ export { AGENT_HOST_CONTROL_PROTOCOL } from "./agentHostProtocol.js";
30
29
  const HOST_CONTROL_MAX_BYTES = 32 * 1024;
31
30
  const CODEX_CLIENT_STABLE_MS = 5_000;
32
31
  const MAX_CONSECUTIVE_CODEX_DISCONNECTS = 3;
32
+ export function foldSteerLiveReceipt(control) {
33
+ const detail = control.failure?.detail ?? control.snapshot.detail;
34
+ const withDetail = detail === undefined ? {} : { detail };
35
+ switch (control.outcome) {
36
+ case "accepted":
37
+ // The only ok state. A live-accepted steer keeps `not-steered` (static,
38
+ // pre-flight) and this live disposition strictly separate phases.
39
+ return { state: "steered", outcome: "accepted" };
40
+ case "pending":
41
+ return { state: "steer-unknown", outcome: "pending", ...withDetail };
42
+ case "rejected":
43
+ return { state: "steer-rejected", outcome: "rejected", ...withDetail };
44
+ default:
45
+ // A steer edge only ever answers accepted/pending/rejected; `busy`,
46
+ // `status`, or `cancel-requested` here means the Host could not run the
47
+ // steer. Report it faithfully rather than flattening to success.
48
+ return { state: "steer-unavailable", outcome: control.outcome, ...withDetail };
49
+ }
50
+ }
51
+ export function foldInterruptLiveReceipt(control) {
52
+ const cancellation = control.cancellation;
53
+ const withCancellation = cancellation === undefined ? {} : { cancellation };
54
+ if (control.outcome !== "cancel-requested") {
55
+ const detail = control.failure?.detail ?? control.snapshot.detail;
56
+ return { state: "interrupt-unavailable", outcome: control.outcome,
57
+ ...withCancellation, ...(detail === undefined ? {} : { detail }) };
58
+ }
59
+ // A transport outcome without native cancellation evidence proves no stop.
60
+ const state = cancellation?.status === "not-active" ? "interrupt-not-active"
61
+ : cancellation?.status === "requested" ? "interrupt-requested"
62
+ : "interrupt-unknown";
63
+ return { state, outcome: "cancel-requested", ...withCancellation };
64
+ }
33
65
  export function serializeAgentHostLaunchControl(control) {
34
66
  return JSON.stringify(validateControl(control));
35
67
  }
@@ -37,6 +69,11 @@ export async function runAgentHost(input) {
37
69
  const hostInstanceId = randomUUID();
38
70
  let hostSequence = 0;
39
71
  let payload = await redeem(input.home, input.ticket);
72
+ if (payload.environment.YUI_SESSION_SCOPE === "global" && payload.providerControl !== undefined) {
73
+ process.stdout.write("Yui controlled Global Session: this is the Host console, not the native Agent TUI. "
74
+ + "Type a line to submit to this Session; model, permissions and workspace remain as configured. "
75
+ + "Queued inputs and live controls use the same Endpoint.\n");
76
+ }
40
77
  if (payload.environment.YUI_SESSION_SCOPE === "global"
41
78
  && payload.environment.YUI_ADAPTER_ID === "codex"
42
79
  && payload.providerControl === undefined) {
@@ -304,7 +341,7 @@ export async function runAgentHost(input) {
304
341
  if (hostStopRequested)
305
342
  return;
306
343
  try {
307
- assertAgentExecutionEnvironment(input.home, reconnectPayload);
344
+ await assertHostExecutionEnvironment(input.home, reconnectPayload, disconnectedSession.nativeSessionId);
308
345
  // Reattaching a dropped client continues the same Session on the same
309
346
  // pinned generation; it never re-selects an implementation.
310
347
  if (endpointLease === undefined) {
@@ -549,7 +586,8 @@ export async function runAgentHost(input) {
549
586
  activeRunAttemptId = providerControl.ownedTurn.attemptId;
550
587
  activeNativeTurnId = providerControl.ownedTurn.turnId;
551
588
  }
552
- assertAgentExecutionEnvironment(input.home, next);
589
+ // The Controller revalidates the adopted execution environment while
590
+ // redeeming this launch; a persistent Host must not reopen its schema.
553
591
  // Pin before opening: the Session holds this exact generation for every
554
592
  // later Turn. A pinned reference naming code this process is not running
555
593
  // fails here, rather than silently starting on a different generation.
@@ -559,8 +597,7 @@ export async function runAgentHost(input) {
559
597
  ? endpointLease.open(next) : endpointLease.resume(next));
560
598
  session = started.session;
561
599
  sessionPayload = next;
562
- recordOwnedProviderProcess(input.home, next.environment, started.session);
563
- recordNativeConnection(input.home, next, started.session);
600
+ await recordProviderConnection(input.home, next, started.session);
564
601
  lastTerminalPayload = undefined;
565
602
  lastTerminal = undefined;
566
603
  // Recoverable means a later process can rebind this Conversation by
@@ -611,6 +648,7 @@ export async function runAgentHost(input) {
611
648
  await publishStructuredProviderOpened({
612
649
  home: input.home,
613
650
  environment: next.environment,
651
+ startupRunId: next.startupRunId,
614
652
  conversationId: session.conversationId,
615
653
  nativeSessionId: session.nativeSessionId,
616
654
  recoverability: conversationRecoverability,
@@ -730,13 +768,25 @@ export async function runAgentHost(input) {
730
768
  attemptId: request.run.attemptId,
731
769
  ...authorityFields()
732
770
  }));
733
- const durableRun = hostRunControlParams(sessionPayload, session.nativeSessionId, request.authority, request.run.attemptId, request.runId);
771
+ const durableRun = { ...hostRunControlParams(sessionPayload, session.nativeSessionId, request.authority, request.run.attemptId, request.runId), boundedText: request.run.boundedText,
772
+ retrySupport: session.adapterId === "codex" ? "codex-failed-turn-v1" : "unsupported" };
773
+ let recoveryInput;
734
774
  // Registration precedes the Provider write, so its failure modes are not
735
775
  // the Provider's. A definite failure means the Provider never saw this
736
776
  // input and the occupancy must be released; anything unconfirmed leaves
737
777
  // durable state ambiguous and keeps it held.
738
778
  try {
739
- await beginDurableProviderTurn(input.home, durableRun);
779
+ const admitted = await beginDurableProviderTurn(input.home, durableRun);
780
+ if (admitted !== null && typeof admitted === "object" && !Array.isArray(admitted)) {
781
+ const retry = admitted.retryInput;
782
+ if (retry !== null && typeof retry === "object" && !Array.isArray(retry)
783
+ && typeof retry.text === "string")
784
+ recoveryInput = {
785
+ text: retry.text,
786
+ ...(typeof retry.expectedFailedNativeTurnId === "string"
787
+ ? { expectedFailedNativeTurnId: retry.expectedFailedNativeTurnId } : {})
788
+ };
789
+ }
740
790
  }
741
791
  catch (error) {
742
792
  // Registration ends in one of three states and they are not
@@ -790,10 +840,15 @@ export async function runAgentHost(input) {
790
840
  let providerAccepted = false;
791
841
  let transportAccepted = false;
792
842
  try {
793
- assertAgentExecutionEnvironment(input.home, activeRunPayload);
843
+ // beginDurableProviderTurn validated current execution-environment,
844
+ // authority and Session identity before this native write.
794
845
  const receipt = endpointReceipt(await session.submit({
795
846
  ...request.run,
796
- boundedText: withSessionContextPointer(request.run.boundedText, activeRunPayload.environment),
847
+ boundedText: withSessionContextPointer(recoveryInput?.text ?? request.run.boundedText, activeRunPayload.environment),
848
+ ...(recoveryInput === undefined ? {} : { requireQuiescent: true }),
849
+ ...(recoveryInput?.expectedFailedNativeTurnId === undefined ? {} : {
850
+ expectedFailedNativeTurnId: recoveryInput.expectedFailedNativeTurnId
851
+ }),
797
852
  inputRef: request.runId ?? request.run.attemptId
798
853
  }), request.run.attemptId);
799
854
  transportAccepted = true;
@@ -906,7 +961,7 @@ export async function runAgentHost(input) {
906
961
  throw new Error("Agent Host rejected a stale Provider writer fence.");
907
962
  }
908
963
  try {
909
- assertAgentExecutionEnvironment(input.home, sessionPayload);
964
+ await assertHostExecutionEnvironment(input.home, sessionPayload, session.nativeSessionId);
910
965
  const receipt = endpointReceipt(await session.steer({
911
966
  ...request.run,
912
967
  boundedText: withSessionContextPointer(request.run.boundedText, sessionPayload.environment),
@@ -972,6 +1027,9 @@ export async function runAgentHost(input) {
972
1027
  }));
973
1028
  }
974
1029
  if (request.type === "cancel") {
1030
+ if (request.nativeOnly === true && session?.capabilities.cancel !== "native-interrupt") {
1031
+ throw new Error("This Endpoint cannot natively interrupt; owned-process termination is not input control.");
1032
+ }
975
1033
  if (session === undefined || request.nativeSessionId !== session.nativeSessionId
976
1034
  || request.attemptId !== activeRunAttemptId
977
1035
  || authority === undefined || !sameProviderAuthorityFence(authority, request.authority)) {
@@ -1079,7 +1137,8 @@ export async function runAgentHost(input) {
1079
1137
  ? createInterface({ input: process.stdin, output: process.stdout, terminal: true })
1080
1138
  : undefined;
1081
1139
  promptHuman = () => {
1082
- if (humanConsole !== undefined && authority?.owner === "human"
1140
+ if (humanConsole !== undefined && (authority?.owner === "human"
1141
+ || sessionPayload?.environment.YUI_SESSION_SCOPE === "global" && authority?.owner === "controller")
1083
1142
  && activeRunPayload === undefined
1084
1143
  && ["idle", "rejected", "failed"].includes(snapshot.state)) {
1085
1144
  humanConsole.setPrompt("yui(provider)> ");
@@ -1091,7 +1150,8 @@ export async function runAgentHost(input) {
1091
1150
  const currentAuthority = authority;
1092
1151
  const currentSession = session;
1093
1152
  const currentPayload = sessionPayload;
1094
- if (currentAuthority?.owner !== "human"
1153
+ if ((currentAuthority?.owner !== "human"
1154
+ && !(currentPayload?.environment.YUI_SESSION_SCOPE === "global" && currentAuthority?.owner === "controller"))
1095
1155
  || currentSession === undefined
1096
1156
  || currentPayload === undefined) {
1097
1157
  process.stderr.write("Provider input rejected: human authority is not active.\n");
@@ -1111,8 +1171,10 @@ export async function runAgentHost(input) {
1111
1171
  boundedText
1112
1172
  }
1113
1173
  };
1114
- await submitRun(runControl, {});
1115
- process.stdout.write("Provider accepted the human AgentRun; waiting for its terminal boundary.\n");
1174
+ const submitted = await submitRun(runControl, {});
1175
+ process.stdout.write(submitted.inputAcceptance === "provider"
1176
+ ? "Provider accepted this console input; waiting for its terminal boundary.\n"
1177
+ : "Input written to transport; Provider acceptance is not yet proven.\n");
1116
1178
  }).catch((error) => {
1117
1179
  process.stderr.write(`Provider input failed: ${errorText(error)}\n`);
1118
1180
  promptHuman();
@@ -1204,58 +1266,42 @@ export async function runAgentHost(input) {
1204
1266
  }
1205
1267
  }
1206
1268
  /** Host-restart-independent OS custody. No secret or launch payload is stored. */
1207
- function recordOwnedProviderProcess(home, environment, endpoint) {
1208
- if (endpoint.ownedProcessId === undefined || environment.YUI_SESSION_SCOPE !== "task")
1209
- return;
1210
- const identity = readLinuxProcessIdentity(endpoint.ownedProcessId);
1211
- if (identity === undefined)
1269
+ async function recordProviderConnection(home, payload, endpoint) {
1270
+ const environment = payload.environment;
1271
+ const identity = endpoint.ownedProcessId === undefined ? undefined : readLinuxProcessIdentity(endpoint.ownedProcessId);
1272
+ if (endpoint.ownedProcessId !== undefined && identity === undefined) {
1212
1273
  throw new Error("Dedicated Provider process identity was lost before registration.");
1274
+ }
1213
1275
  const taskId = environment.YUI_TASK_ID;
1214
1276
  const roleName = environment.YUI_ROLE;
1215
- const store = openCurrentTaskStore(home);
1216
- try {
1217
- store.saveSessionOwner(createSessionOwnerIdentity({
1218
- owner: { scope: "task", taskId, roleName },
1219
- agentId: environment.YUI_AGENT_ID, adapterId: endpoint.adapterId,
1220
- nativeSessionId: endpoint.nativeSessionId,
1221
- tmux: {
1222
- serverName: yuiTmuxServerName(home),
1223
- socketPath: join(tmuxSocketDirectory(environment), yuiTmuxServerName(home)),
1224
- sessionName: yuiTmuxSessionName(home, taskId), windowName: roleName, panePid: process.pid
1225
- },
1226
- providerRoot: { pid: identity.pid, startIdentity: identity.startIdentity,
1227
- processGroupId: identity.processGroupId, processSessionId: identity.processSessionId,
1228
- attribution: "owned-child" },
1229
- recordedAt: new Date()
1230
- }));
1231
- }
1232
- finally {
1233
- store.close();
1234
- }
1235
- }
1236
- function recordNativeConnection(home, payload, endpoint) {
1237
- if (endpoint.adapterId !== "codex" || payload.environment.YUI_SESSION_SCOPE !== "task")
1238
- return;
1239
- const taskId = payload.environment.YUI_TASK_ID;
1240
- const store = openCurrentTaskStore(home);
1241
- try {
1242
- if (store.listEvents(taskId).some(event => event.type === "runtime.native-connection-bound"
1243
- && event.payload.nativeSessionId === endpoint.nativeSessionId))
1244
- return;
1245
- const userHome = resolve(payload.cwd, payload.environment.HOME ?? homedir());
1246
- store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "runtime.native-connection-bound", {
1247
- roleName: payload.environment.YUI_ROLE, agentId: payload.environment.YUI_AGENT_ID,
1248
- nativeSessionId: endpoint.nativeSessionId,
1249
- // Account location is needed after Controller/Host loss. Never retain
1250
- // resolved credentials or duplicate the full process environment.
1251
- home: userHome,
1252
- codexHome: resolve(payload.cwd, payload.environment.CODEX_HOME ?? join(userHome, ".codex")),
1253
- ...(endpoint.nativeAccountHome === undefined ? {} : { nativeAccountHome: endpoint.nativeAccountHome })
1254
- }, new Date()));
1255
- }
1256
- finally {
1257
- store.close();
1258
- }
1277
+ const userHome = resolve(payload.cwd, environment.HOME ?? homedir());
1278
+ await publishStructuredProviderConnection({
1279
+ home, environment, nativeSessionId: endpoint.nativeSessionId, startupRunId: payload.startupRunId,
1280
+ connection: {
1281
+ ...(identity === undefined ? {} : { processOwner: createSessionOwnerIdentity({
1282
+ owner: environment.YUI_SESSION_SCOPE === "global"
1283
+ ? { scope: "global", roleName } : { scope: "task", taskId, roleName },
1284
+ agentId: environment.YUI_AGENT_ID, adapterId: endpoint.adapterId,
1285
+ nativeSessionId: endpoint.nativeSessionId,
1286
+ tmux: {
1287
+ serverName: yuiTmuxServerName(home),
1288
+ socketPath: join(tmuxSocketDirectory(environment), yuiTmuxServerName(home)),
1289
+ sessionName: yuiTmuxSessionName(home, taskId ?? "operator"), windowName: roleName, panePid: process.pid
1290
+ },
1291
+ providerRoot: { pid: identity.pid, startIdentity: identity.startIdentity,
1292
+ processGroupId: identity.processGroupId, processSessionId: identity.processSessionId,
1293
+ attribution: "owned-child" },
1294
+ recordedAt: new Date()
1295
+ }) }),
1296
+ ...(endpoint.adapterId !== "codex" ? {} : { account: {
1297
+ // Account location is needed after Controller/Host loss. Never retain
1298
+ // resolved credentials or duplicate the full process environment.
1299
+ home: userHome,
1300
+ codexHome: resolve(payload.cwd, environment.CODEX_HOME ?? join(userHome, ".codex")),
1301
+ ...(endpoint.nativeAccountHome === undefined ? {} : { nativeAccountHome: endpoint.nativeAccountHome })
1302
+ } })
1303
+ }
1304
+ });
1259
1305
  }
1260
1306
  export function agentHostControlSocketPath(input) {
1261
1307
  const owner = input.scope === "task" ? input.taskId ?? "missing-task" : "global";
@@ -1329,13 +1375,22 @@ export async function waitForAgentHostLaunchAck(input) {
1329
1375
  }
1330
1376
  async function sendAgentHostControl(input) {
1331
1377
  const path = agentHostControlSocketPath(input);
1378
+ return exchangeAgentHostControl(path, input.control, AGENT_HOST_CONTROL_TIMEOUT_MS);
1379
+ }
1380
+ /** Read-only upgrade/diagnostic probe of a Home-fenced socket. Never launches a Host. */
1381
+ export async function inspectAgentHostSocket(path, timeoutMs = 250) {
1382
+ return (await exchangeAgentHostControl(path, {
1383
+ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "status"
1384
+ }, timeoutMs)).snapshot;
1385
+ }
1386
+ async function exchangeAgentHostControl(path, control, timeoutMs) {
1332
1387
  return await new Promise((resolvePromise, reject) => {
1333
1388
  const client = createConnection(path);
1334
1389
  let response = "";
1335
1390
  const timer = setTimeout(() => {
1336
1391
  client.destroy();
1337
1392
  reject(new Error("Agent Host control request timed out."));
1338
- }, AGENT_HOST_CONTROL_TIMEOUT_MS);
1393
+ }, timeoutMs);
1339
1394
  let settled = false;
1340
1395
  const settle = (callback, value) => {
1341
1396
  if (settled)
@@ -1345,7 +1400,7 @@ async function sendAgentHostControl(input) {
1345
1400
  callback(value);
1346
1401
  };
1347
1402
  client.setEncoding("utf8");
1348
- client.once("connect", () => client.end(`${JSON.stringify(validateControl(input.control))}\n`));
1403
+ client.once("connect", () => client.end(`${JSON.stringify(validateControl(control))}\n`));
1349
1404
  client.on("data", (chunk) => {
1350
1405
  response += chunk;
1351
1406
  if (Buffer.byteLength(response, "utf8") > HOST_CONTROL_MAX_BYTES) {
@@ -1370,6 +1425,17 @@ async function redeem(home, ticket) {
1370
1425
  });
1371
1426
  return validateAgentHostLaunchPayload(result);
1372
1427
  }
1428
+ async function assertHostExecutionEnvironment(home, payload, nativeSessionId) {
1429
+ if (payload.executionEnvironment === undefined)
1430
+ return;
1431
+ await callController(home, "runtime.execution-environment-check", {
1432
+ taskId: payload.environment.YUI_TASK_ID,
1433
+ roleName: payload.environment.YUI_ROLE,
1434
+ agentId: payload.environment.YUI_AGENT_ID,
1435
+ workspace: payload.cwd,
1436
+ nativeSessionId
1437
+ });
1438
+ }
1373
1439
  async function persistAndSubmitExit(home, observation) {
1374
1440
  try {
1375
1441
  await persistRuntimeProcessExitObservation(home, observation, (value) => callController(home, "runtime.process-exit-observe", value).then(() => undefined));
@@ -1426,7 +1492,26 @@ export async function openAgentHostControl(home, payload, snapshot, dispatch) {
1426
1492
  void (async () => {
1427
1493
  try {
1428
1494
  const request = validateControl(JSON.parse(body.trim()));
1429
- socket.end(`${JSON.stringify(boundControlResponse(await dispatch(request)))}\n`);
1495
+ const result = await dispatch(request);
1496
+ socket.end(`${JSON.stringify(boundControlResponse({
1497
+ ...result,
1498
+ snapshot: {
1499
+ ...result.snapshot,
1500
+ compatibility: {
1501
+ control: AGENT_HOST_CONTROL_PROTOCOL, events: AGENT_HOST_EVENT_PROTOCOL,
1502
+ rpc: FILE_TASK_CONTROLLER_PROTOCOL_VERSION, storage: "controller-owned"
1503
+ },
1504
+ hostProcess: { pid: process.pid, startIdentity: readLinuxProcessIdentity(process.pid).startIdentity },
1505
+ owner: {
1506
+ scope: payload.environment.YUI_SESSION_SCOPE ?? "task",
1507
+ ...(payload.environment.YUI_TASK_ID === undefined ? {} : { taskId: payload.environment.YUI_TASK_ID }),
1508
+ roleName: payload.environment.YUI_ROLE ?? "unknown-role"
1509
+ },
1510
+ ...(payload.environment.YUI_SESSION_SCOPE !== "task" ? {} : {
1511
+ eventDelivery: structuredProviderEventDelivery(home, payload.environment, result.snapshot.nativeSessionId)
1512
+ })
1513
+ }
1514
+ }))}\n`);
1430
1515
  }
1431
1516
  catch (caught) {
1432
1517
  const error = caught instanceof AgentHostOperationError ? caught.cause : caught;
@@ -1565,6 +1650,9 @@ function validateControl(control) {
1565
1650
  if (control.type === "status")
1566
1651
  return Object.freeze({ ...control });
1567
1652
  if (control.type === "cancel") {
1653
+ if (control.nativeOnly !== undefined && typeof control.nativeOnly !== "boolean") {
1654
+ throw new Error("Native-only cancellation flag is invalid.");
1655
+ }
1568
1656
  validateIdentity(control.nativeSessionId, "native Session id");
1569
1657
  validateIdentity(control.attemptId, "Provider input attempt id");
1570
1658
  validateProviderAuthorityFence(control.authority);
@@ -1746,9 +1834,11 @@ async function observePendingHostSubmission(submission, snapshot) {
1746
1834
  function signalRoleMailbox(home, payload) {
1747
1835
  const taskId = payload.environment.YUI_TASK_ID;
1748
1836
  const roleName = payload.environment.YUI_ROLE;
1749
- if (taskId === undefined || roleName === undefined)
1837
+ if (roleName === undefined)
1750
1838
  return;
1751
- const key = `role:${encodeURIComponent(taskId)}/${encodeURIComponent(roleName)}`;
1839
+ const key = taskId === undefined
1840
+ ? `global-role:${encodeURIComponent(roleName)}`
1841
+ : `role:${encodeURIComponent(taskId)}/${encodeURIComponent(roleName)}`;
1752
1842
  void callController(home, "scheduler.signal", { key }).catch(() => {
1753
1843
  // This is a low-latency hint. Durable mailbox state and periodic
1754
1844
  // reconciliation remain the recovery path across a Controller handover.
@@ -1763,7 +1853,9 @@ async function delay(milliseconds) {
1763
1853
  function hostRunControlParams(payload, nativeSessionId, authority, attemptId, runId) {
1764
1854
  const environment = payload.environment;
1765
1855
  return Object.freeze({
1766
- taskId: requiredEnvironment(environment.YUI_TASK_ID, "Task id"),
1856
+ ...(environment.YUI_SESSION_SCOPE === "global" ? {} : {
1857
+ taskId: requiredEnvironment(environment.YUI_TASK_ID, "Task id")
1858
+ }),
1767
1859
  roleName: requiredEnvironment(environment.YUI_ROLE, "Role name"),
1768
1860
  ...(runId === undefined ? {} : { runId: requiredEnvironment(runId, "AgentRun id") }),
1769
1861
  agentId: requiredEnvironment(environment.YUI_AGENT_ID, "Agent id"),
@@ -1777,7 +1869,7 @@ function hostRunControlParams(payload, nativeSessionId, authority, attemptId, ru
1777
1869
  }
1778
1870
  async function beginDurableProviderTurn(home, durableRun) {
1779
1871
  try {
1780
- await callControllerIdempotently(home, "runtime.provider-turn-begin", durableRun);
1872
+ return await callControllerIdempotently(home, "runtime.provider-turn-begin", durableRun);
1781
1873
  }
1782
1874
  catch (error) {
1783
1875
  if (error instanceof ControllerClientError && error.code === "SESSION_BUSY") {
@@ -1785,7 +1877,7 @@ async function beginDurableProviderTurn(home, durableRun) {
1785
1877
  }
1786
1878
  if (!(error instanceof ControllerAcknowledgementUnknownError))
1787
1879
  throw error;
1788
- await resolveProviderTurnSubmission(home, durableRun, new Error(`Provider Turn intent acknowledgement failed before Provider write: ${errorText(error)}`, { cause: error }));
1880
+ await resolveProviderTurnSubmission(home, durableRun, new Error(`Provider Turn intent acknowledgement failed before Provider write: ${errorText(error)}`, { cause: error }), true);
1789
1881
  // The resolve is fenced on this exact attempt id, so its success proves
1790
1882
  // the registration did commit and is now settled with nothing written to
1791
1883
  // the Provider. That is a known outcome; reporting it as unknown would
@@ -1796,7 +1888,7 @@ async function beginDurableProviderTurn(home, durableRun) {
1796
1888
  }
1797
1889
  async function callControllerIdempotently(home, method, request) {
1798
1890
  try {
1799
- await callAgentController(home, method, request);
1891
+ return await callAgentController(home, method, request);
1800
1892
  }
1801
1893
  catch (error) {
1802
1894
  if (!(error instanceof ControllerClientError)
@@ -1807,7 +1899,7 @@ async function callControllerIdempotently(home, method, request) {
1807
1899
  // These methods carry exact attempt, launch, and authority fences. A
1808
1900
  // bounded replay confirms a commit whose acknowledgement may have been lost.
1809
1901
  try {
1810
- await callAgentController(home, method, request);
1902
+ return await callAgentController(home, method, request);
1811
1903
  }
1812
1904
  catch (replayError) {
1813
1905
  if (firstCallMayHaveApplied || controllerCallMayHaveApplied(replayError)) {
@@ -1828,7 +1920,7 @@ class ControllerAcknowledgementUnknownError extends Error {
1828
1920
  class ProviderTurnRegistrationSettledError extends Error {
1829
1921
  name = "ProviderTurnRegistrationSettledError";
1830
1922
  }
1831
- async function resolveProviderTurnSubmission(home, durableRun, error) {
1923
+ async function resolveProviderTurnSubmission(home, durableRun, error, noProviderWrite = false) {
1832
1924
  const attemptId = durableRun.attemptId;
1833
1925
  if (typeof attemptId !== "string") {
1834
1926
  throw new Error("Provider Turn resolution has no attempt id.");
@@ -1840,6 +1932,7 @@ async function resolveProviderTurnSubmission(home, durableRun, error) {
1840
1932
  : error instanceof ProviderTurnBusyError ? "deferred" : "rejected",
1841
1933
  reason: errorText(error),
1842
1934
  raw: serializeAgentErrorRaw(error),
1935
+ ...(noProviderWrite || error instanceof CodexPreSubmissionError ? { providerWrite: "not-issued" } : {}),
1843
1936
  observedAt: new Date().toISOString()
1844
1937
  };
1845
1938
  try {
@@ -1860,15 +1953,19 @@ async function resolveProviderTurnSubmission(home, durableRun, error) {
1860
1953
  */
1861
1954
  async function callAgentController(home, method, params) {
1862
1955
  try {
1863
- await callController(home, method, params);
1864
- return;
1956
+ return await callController(home, method, params);
1865
1957
  }
1866
1958
  catch (error) {
1867
1959
  if (!isControllerUnavailable(error) || !isForeignHandoverLockHeld(home)) {
1868
1960
  throw error;
1869
1961
  }
1870
1962
  }
1871
- await callFileTaskController(home, method, params);
1963
+ const deadline = Date.now() + AGENT_HOST_CONTROL_TIMEOUT_MS;
1964
+ while (isForeignHandoverLockHeld(home) && Date.now() < deadline)
1965
+ await delay(50);
1966
+ // The pinned Host can rediscover a replacement, but must never start a
1967
+ // Controller using its own old implementation or open the Home's database.
1968
+ return await callController(home, method, params);
1872
1969
  }
1873
1970
  function isControllerUnavailable(error) {
1874
1971
  return error instanceof ControllerClientError
@@ -0,0 +1,127 @@
1
+ import { lstatSync, readdirSync, readFileSync } from "node:fs";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { agentHostControlSocketPath, inspectAgentHostSocket } from "./agentHost.js";
4
+ import { AGENT_HOST_CONTROL_PROTOCOL, AGENT_HOST_EVENT_PROTOCOL } from "./agentHostProtocol.js";
5
+ import { FILE_TASK_CONTROLLER_PROTOCOL_VERSION } from "../core/protocol.js";
6
+ import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
7
+ import { readLinuxProcessIdentity } from "./sessionOwnerIdentity.js";
8
+ /** Inspect the concrete Home's existing control sockets, independently of the
9
+ * database layout. No discovery write, process signal, input replay or repair.
10
+ * Idle legacy Hosts are not safe: their next event still opens their old schema.
11
+ */
12
+ export async function inspectAgentHostCompatibility(home) {
13
+ const sample = agentHostControlSocketPath({ home, scope: "global", roleName: "compatibility-probe" });
14
+ const directory = dirname(sample);
15
+ const prefix = `${basename(sample).split("-")[0]}-`;
16
+ let names;
17
+ try {
18
+ names = readdirSync(directory).filter(name => name.startsWith(prefix) && /^[a-f0-9]{16}-[a-f0-9]{16}\.sock$/.test(name));
19
+ }
20
+ catch (error) {
21
+ if (error.code === "ENOENT")
22
+ names = [];
23
+ else
24
+ return [{ socket: directory, reason: `Host inventory is unreadable: ${messageOf(error)}` }];
25
+ }
26
+ // A launch can have redeemed its payload but not yet bound its socket.
27
+ // It is still a loaded Host implementation. Do not miss it just because
28
+ // preflight happened in that short startup interval.
29
+ const processes = liveHostProcesses(home);
30
+ const sockets = new Set([...names.map(name => join(directory, name)), ...processes.map(p => p.socket)]);
31
+ const results = await Promise.all([...sockets].map(async (socket) => {
32
+ const live = processes.filter(p => p.socket === socket);
33
+ try {
34
+ const stat = lstatSync(socket);
35
+ if (!stat.isSocket() || (stat.mode & 0o077) !== 0
36
+ || (process.getuid !== undefined && stat.uid !== process.getuid())) {
37
+ return { socket, reason: "Host socket ownership cannot be verified." };
38
+ }
39
+ const snapshot = await inspectAgentHostSocket(socket);
40
+ if (live.some(p => snapshot.hostProcess?.pid !== p.pid
41
+ || snapshot.hostProcess?.startIdentity !== p.startIdentity)) {
42
+ return {
43
+ socket, pid: live[0].pid,
44
+ nativeSessionId: snapshot.nativeSessionId,
45
+ reason: "The live Host process has no exact matching compatibility response (legacy or still starting)."
46
+ };
47
+ }
48
+ const compatibility = snapshot.compatibility;
49
+ if (compatibility?.control === AGENT_HOST_CONTROL_PROTOCOL
50
+ && compatibility.events === AGENT_HOST_EVENT_PROTOCOL
51
+ && compatibility.rpc === FILE_TASK_CONTROLLER_PROTOCOL_VERSION
52
+ && compatibility.storage === "controller-owned")
53
+ return undefined;
54
+ return {
55
+ socket,
56
+ ...(snapshot.owner === undefined ? {} : { owner: snapshot.owner }),
57
+ ...(snapshot.nativeSessionId === undefined ? {} : { nativeSessionId: snapshot.nativeSessionId }),
58
+ ...(snapshot.attemptId === undefined ? {} : { attemptId: snapshot.attemptId }),
59
+ state: snapshot.state,
60
+ reason: compatibility === undefined
61
+ ? "Legacy Host has no schema-independent event capability; its loaded code cannot be upgraded in place."
62
+ : "Host control/event/RPC compatibility is outside this Controller's supported range."
63
+ };
64
+ }
65
+ catch (error) {
66
+ const stillLive = live.find(p => readLinuxProcessIdentity(p.pid)?.startIdentity === p.startIdentity);
67
+ if (["ENOENT", "ECONNREFUSED"].includes(error.code ?? "")
68
+ && stillLive === undefined)
69
+ return undefined;
70
+ return { socket, ...(stillLive === undefined ? {} : { pid: stillLive.pid }),
71
+ reason: `Live Host compatibility is unconfirmed: ${messageOf(error)}` };
72
+ }
73
+ }));
74
+ return results.filter((entry) => entry !== undefined);
75
+ }
76
+ export function describeAgentHostUpgradeBlockers(blockers) {
77
+ return blockers.map(blocker => [
78
+ blocker.owner === undefined ? blocker.socket
79
+ : `${blocker.owner.taskId ?? blocker.owner.scope}/${blocker.owner.roleName}`,
80
+ blocker.nativeSessionId === undefined ? "" : `Session=${blocker.nativeSessionId}`,
81
+ blocker.attemptId === undefined ? "" : `input=${blocker.attemptId}`,
82
+ blocker.state === undefined ? "" : `Host=${blocker.state}`,
83
+ blocker.pid === undefined ? "" : `PID=${blocker.pid}`,
84
+ blocker.reason
85
+ ].filter(Boolean).join("; ")).join("\n");
86
+ }
87
+ function liveHostProcesses(home) {
88
+ if (process.platform !== "linux")
89
+ return [];
90
+ const expected = readHomeFilesystemId(home);
91
+ const hosts = [];
92
+ for (const name of readdirSync("/proc")) {
93
+ if (!/^[1-9][0-9]*$/.test(name))
94
+ continue;
95
+ const pid = Number(name);
96
+ try {
97
+ const status = readFileSync(`/proc/${pid}/status`, "utf8");
98
+ if (Number(/^Uid:\s+([0-9]+)/mu.exec(status)?.[1]) !== process.getuid?.())
99
+ continue;
100
+ const args = readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0");
101
+ if (!args.some((arg, i) => /(?:^|\/)cli\.js$/.test(arg)
102
+ && args[i + 1] === "internal" && args[i + 2] === "agent-host"))
103
+ continue;
104
+ const env = readFileSync(`/proc/${pid}/environ`, "utf8").split("\0");
105
+ const value = (key) => env.find(entry => entry.startsWith(`${key}=`))?.slice(key.length + 1);
106
+ const processHome = value("YUI_HOME");
107
+ if (processHome === undefined || readHomeFilesystemId(processHome) !== expected)
108
+ continue;
109
+ const identity = readLinuxProcessIdentity(pid);
110
+ if (identity === undefined || identity.state === "Z")
111
+ continue;
112
+ hosts.push({ pid, startIdentity: identity.startIdentity,
113
+ socket: agentHostControlSocketPath({
114
+ home, scope: value("YUI_SESSION_SCOPE") ?? "task",
115
+ taskId: value("YUI_TASK_ID"), roleName: value("YUI_ROLE") ?? "unknown-role"
116
+ }) });
117
+ }
118
+ catch {
119
+ // Other owners and processes that exited while being read are not this
120
+ // Home's proven Hosts. No PID is ever acted on by this read-only inventory.
121
+ }
122
+ }
123
+ return hosts;
124
+ }
125
+ function messageOf(error) {
126
+ return error instanceof Error ? error.message : String(error);
127
+ }