@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.
- package/ARCHITECTURE.md +8 -4
- package/ARCHITECTURE.zh-CN.md +5 -2
- package/README.md +13 -5
- package/dist/agent/launchEnvironment.js +7 -0
- package/dist/agentRun/agentRun.js +3 -0
- package/dist/cli/commandCatalog.js +64 -16
- package/dist/cli/interactionPolicy.js +7 -3
- package/dist/cli/managedDiagnostics.js +1 -1
- package/dist/cli/updateOrchestrator.js +24 -1
- package/dist/cli/updatePorts.js +7 -3
- package/dist/cli/upgradeCommand.js +42 -2
- package/dist/cli.js +381 -107
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +339 -4
- package/dist/commands/projectCommands.js +50 -22
- package/dist/commands/releaseCommands.js +18 -0
- package/dist/commands/taskActor.js +25 -0
- package/dist/commands/taskCommands.js +586 -96
- package/dist/commands/taskIntegrationCommands.js +19 -39
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskOverviewCommand.js +4 -3
- package/dist/commands/taskPublicationAdoptCommand.js +127 -0
- package/dist/commands/taskPublicationCommands.js +11 -2
- package/dist/commands/taskPublicationVerifyCommand.js +23 -39
- package/dist/commands/taskRemoteDeliveryCommand.js +22 -11
- package/dist/commands/taskRoleRuntimeStatus.js +35 -0
- package/dist/context/runContextPack.js +3 -0
- package/dist/context/taskCatalog.js +187 -0
- package/dist/context/taskContext.js +55 -6
- package/dist/controller/agentHostObservation.js +155 -0
- package/dist/controller/clientRuntime.js +17 -2
- package/dist/controller/controller.js +14 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +519 -25
- package/dist/controller/globalInputDelivery.js +132 -0
- package/dist/controller/jobControl.js +6 -2
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/resourceInventory.js +14 -4
- package/dist/controller/resourceInventoryLinux.js +2 -6
- package/dist/controller/runtime.js +117 -7
- package/dist/controller/runtimeEventInbox.js +32 -3
- package/dist/controller/runtimeEventProcessor.js +26 -6
- package/dist/controller/runtimeHookRunFence.js +75 -19
- package/dist/controller/structuredProviderObservation.js +133 -70
- package/dist/coordination/workMailboxQueue.js +5 -0
- package/dist/execution/workItemExecutionProjection.js +1 -1
- package/dist/executor/agentExecutor.js +64 -4
- package/dist/executor/executorRegistry.js +3 -0
- package/dist/executor/fileRoleLaunchPlanner.js +78 -118
- package/dist/integration/deliveryObligation.js +2 -1
- package/dist/integration/gitIntegrationService.js +329 -386
- package/dist/integration/integrationAttempt.js +30 -4
- package/dist/integration/integrationQueueService.js +7 -7
- package/dist/integration/integrationSourceApplication.js +323 -0
- package/dist/lifecycle/exactRunTerminalization.js +4 -1
- package/dist/message/globalInterrupt.js +33 -0
- package/dist/message/globalProviderRetry.js +15 -0
- package/dist/message/inputControlResolution.js +106 -0
- package/dist/message/message.js +367 -0
- package/dist/message/messageContinuation.js +126 -3
- package/dist/message/taskInterrupt.js +34 -0
- package/dist/observability/executionAudit.js +19 -0
- package/dist/observability/orchestrationMetrics.js +1 -1
- package/dist/release/releaseHandover.js +22 -0
- package/dist/release/releaseWorkflowPorts.js +15 -7
- package/dist/repository/gitWorkspace.js +430 -107
- package/dist/repository/projectMaintenanceLock.js +75 -18
- package/dist/repository/taskWorkspaceCoordinator.js +182 -101
- package/dist/repository/taskWorkspacePreparer.js +205 -72
- package/dist/repository/workItemCandidateSnapshot.js +34 -0
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/resources/resourceDiscovery.js +3 -2
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentHost.js +179 -82
- package/dist/runtime/agentHostCompatibility.js +127 -0
- package/dist/runtime/agentHostProtocol.js +53 -0
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/executionEnvironment.js +0 -19
- package/dist/runtime/launchBroker.js +6 -0
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +6 -2
- package/dist/runtime/taskRuntimeIsolation.js +30 -6
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/runtime/tmuxAdapters.js +5 -3
- package/dist/scheduler/activeRoleRunDelivery.js +12 -0
- package/dist/scheduler/leaderWakeupProcessor.js +5 -0
- package/dist/scheduler/operatorEvent.js +4 -0
- package/dist/scheduler/taskExecutionProjection.js +38 -6
- package/dist/scheduler/taskObservabilityProjection.js +6 -44
- package/dist/scheduler/wakeReason.js +7 -1
- package/dist/scheduler/wakeupQueue.js +2 -0
- package/dist/setup/setupCommand.js +26 -8
- package/dist/storage/homeLayout.js +130 -0
- package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
- package/dist/storage/migrations/integrationContinuation.js +104 -0
- package/dist/storage/migrations/unifyHomeLayout.js +925 -0
- package/dist/storage/sqliteSchema.js +167 -4
- package/dist/storage/sqliteStore.js +57 -1
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -0
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +2 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
- package/dist/task/archiveDiagnostics.js +129 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/nextAction.js +44 -11
- package/dist/task/publicationAdoption.js +56 -0
- package/dist/task/publicationReference.js +10 -0
- package/dist/task/remoteDelivery.js +31 -16
- package/dist/web/assets/client/app.js +147 -17
- package/dist/web/assets/client/components.js +56 -13
- package/dist/web/assets/client/i18n.js +78 -4
- package/dist/web/assets/client/taskSurface.js +108 -1
- package/dist/web/assets/client/view.js +39 -8
- package/dist/web/assets/shell.js +29 -0
- package/dist/web/assets/styles/layout.js +8 -1
- package/dist/web/assets/styles/widgets.js +12 -0
- package/dist/web/webServer.js +131 -4
- package/dist/web/webSnapshot.js +16 -6
- package/dist/web/webTaskSurface.js +222 -5
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +111 -35
- package/docs/agent-result-consumption.md +4 -0
- package/docs/agent-result-consumption.zh-CN.md +3 -0
- package/docs/agent-runtime-drivers.md +7 -0
- package/docs/agent-runtime-drivers.zh-CN.md +5 -0
- package/docs/architecture/README.md +2 -0
- package/docs/architecture/README.zh-CN.md +3 -1
- package/docs/architecture/capabilities-and-resources.md +30 -5
- package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
- package/docs/managed-turn-and-session-runtime.md +47 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
- package/docs/observability/README.md +62 -0
- package/docs/observability/README.zh-CN.md +47 -0
- package/docs/project-refresh.md +77 -0
- package/docs/project-refresh.zh-CN.md +59 -0
- package/docs/provider-retry.md +70 -0
- package/docs/release-workflow.md +39 -0
- package/docs/release-workflow.zh-CN.md +29 -0
- package/docs/sqlite-control-plane-design.md +223 -1
- package/docs/task-delivery.md +133 -13
- package/docs/task-delivery.zh-CN.md +99 -10
- package/docs/task-discovery.md +102 -0
- package/docs/task-discovery.zh-CN.md +86 -0
- package/docs/testing/verification-levels.md +40 -0
- package/docs/testing/verification-levels.zh-CN.md +23 -0
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +154 -51
- package/skills/yui-leader/references/integration.md +52 -2
- package/skills/yui-operator/SKILL.md +19 -3
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +42 -0
- package/skills/yui-runtime/references/publication.md +42 -0
- 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 {
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
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
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
-
},
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
1837
|
+
if (roleName === undefined)
|
|
1750
1838
|
return;
|
|
1751
|
-
const key =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|