@zq-silk/yui 0.15.8 → 0.15.11
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 +2 -0
- package/ARCHITECTURE.zh-CN.md +151 -0
- package/README.md +211 -14
- package/dist/agent/launchEnvironment.js +7 -0
- package/dist/artifacts/artifactCapability.js +74 -0
- package/dist/artifacts/artifactCommitLock.js +249 -0
- package/dist/artifacts/artifactPaths.js +151 -0
- package/dist/artifacts/gitArtifactRef.js +146 -0
- package/dist/artifacts/managedGit.js +332 -0
- package/dist/artifacts/taskArtifactRepository.js +277 -0
- package/dist/cli/commandCatalog.js +40 -16
- package/dist/cli/interactionPolicy.js +3 -3
- 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 +403 -93
- package/dist/commands/globalRoleCommands.js +314 -4
- package/dist/commands/operatorCommands.js +33 -2
- package/dist/commands/projectCommands.js +6 -7
- package/dist/commands/releaseCommands.js +18 -0
- package/dist/commands/taskActivationCommands.js +22 -0
- package/dist/commands/taskActor.js +25 -0
- package/dist/commands/taskCommands.js +846 -155
- package/dist/commands/taskIntegrationCommands.js +16 -38
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
- package/dist/commands/taskRoleRuntimeStatus.js +35 -0
- package/dist/context/runContextPack.js +28 -16
- package/dist/context/taskContext.js +64 -5
- package/dist/controller/agentHostObservation.js +155 -0
- package/dist/controller/clientRuntime.js +17 -2
- package/dist/controller/controller.js +11 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
- package/dist/controller/globalInputDelivery.js +119 -0
- package/dist/controller/jobControl.js +6 -2
- package/dist/controller/resourceInventory.js +14 -4
- package/dist/controller/resourceInventoryLinux.js +2 -6
- package/dist/controller/runtime.js +81 -6
- 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 +312 -382
- package/dist/integration/integrationAttempt.js +30 -4
- package/dist/integration/integrationQueueService.js +7 -7
- package/dist/integration/integrationSourceApplication.js +323 -0
- package/dist/kernel/builtinCapabilities.js +32 -24
- package/dist/message/globalInterrupt.js +33 -0
- package/dist/message/inputControlResolution.js +106 -0
- package/dist/message/message.js +423 -0
- package/dist/message/messageContinuation.js +126 -3
- package/dist/message/taskInterrupt.js +34 -0
- package/dist/observability/orchestrationMetrics.js +1 -1
- package/dist/plugins/pluginService.js +11 -3
- package/dist/release/releaseHandover.js +22 -0
- package/dist/release/releaseWorkflowPorts.js +15 -7
- package/dist/repository/gitWorkspace.js +72 -15
- package/dist/repository/taskWorkspaceCoordinator.js +134 -0
- package/dist/repository/taskWorkspacePreparer.js +120 -49
- package/dist/repository/workItemCandidateSnapshot.js +34 -0
- package/dist/resources/projectResource.js +0 -48
- package/dist/resources/projectResourceService.js +3 -81
- package/dist/resources/resourceDiscovery.js +3 -2
- package/dist/runtime/agentHost.js +152 -72
- package/dist/runtime/agentHostCompatibility.js +127 -0
- package/dist/runtime/agentHostProtocol.js +53 -0
- package/dist/runtime/executionEnvironment.js +0 -19
- package/dist/runtime/launchBroker.js +6 -0
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/taskRuntimeIsolation.js +30 -6
- package/dist/runtime/tmuxAdapters.js +5 -3
- package/dist/scheduler/operatorEvent.js +4 -0
- package/dist/scheduler/taskExecutionProjection.js +12 -1
- package/dist/scheduler/wakeReason.js +7 -1
- package/dist/scheduler/wakeupQueue.js +2 -0
- package/dist/setup/setupCommand.js +29 -16
- package/dist/storage/homeLayout.js +130 -0
- package/dist/storage/migrations/artifactsToGit.js +338 -0
- package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
- package/dist/storage/migrations/integrationContinuation.js +104 -0
- package/dist/storage/migrations/submitIntent.js +126 -0
- package/dist/storage/migrations/unifyHomeLayout.js +925 -0
- package/dist/storage/sqliteSchema.js +173 -7
- package/dist/storage/sqliteStore.js +41 -22
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -1
- package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
- package/dist/task/archiveDiagnostics.js +128 -0
- package/dist/task/nextAction.js +44 -11
- package/dist/task/taskActivation.js +26 -0
- package/dist/task/taskActivationService.js +85 -69
- package/dist/task/taskSubmission.js +236 -0
- package/dist/web/assets/client/app.js +58 -2
- package/dist/web/assets/client/components.js +1 -0
- package/dist/web/assets/client/i18n.js +6 -0
- package/dist/web/assets/client/taskSurface.js +202 -7
- package/dist/web/assets/client/view.js +7 -4
- package/dist/web/assets/shell.js +23 -0
- package/dist/web/assets/styles/layout.js +1 -1
- package/dist/web/assets/styles/widgets.js +12 -0
- package/dist/web/webServer.js +135 -4
- package/dist/web/webSnapshot.js +4 -3
- package/dist/web/webTaskSurface.js +225 -8
- package/dist/workItem/workItem.js +14 -10
- package/dist/workspace/workItemChangeSetManager.js +18 -2
- package/docs/agent-result-consumption.md +2 -0
- package/docs/agent-result-consumption.zh-CN.md +81 -0
- package/docs/agent-runtime-drivers.md +2 -0
- package/docs/agent-runtime-drivers.zh-CN.md +77 -0
- package/docs/architecture/README.md +44 -32
- package/docs/architecture/README.zh-CN.md +43 -0
- package/docs/architecture/capabilities-and-resources.md +118 -79
- package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
- package/docs/managed-turn-and-session-runtime.md +2 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
- package/docs/observability/README.md +2 -0
- package/docs/observability/README.zh-CN.md +71 -0
- package/docs/plugin-sdk.md +320 -217
- package/docs/plugin-sdk.zh-CN.md +293 -0
- package/docs/provider-runtime.md +2 -0
- package/docs/provider-runtime.zh-CN.md +132 -0
- package/docs/release-workflow.md +41 -0
- package/docs/release-workflow.zh-CN.md +266 -0
- package/docs/roles-and-configuration.md +2 -0
- package/docs/roles-and-configuration.zh-CN.md +96 -0
- package/docs/sqlite-control-plane-design.md +225 -1
- package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
- package/docs/task-dag-semantics.md +80 -57
- package/docs/task-dag-semantics.zh-CN.md +59 -0
- package/docs/task-delivery.md +2 -0
- package/docs/task-delivery.zh-CN.md +82 -0
- package/docs/task-local-identity.md +2 -0
- package/docs/task-local-identity.zh-CN.md +58 -0
- package/docs/testing/verification-levels.md +26 -0
- package/docs/testing/verification-levels.zh-CN.md +80 -0
- package/i18n/README.zh-CN.md +199 -10
- package/package.json +2 -1
- package/skills/yui-leader/SKILL.md +88 -331
- package/skills/yui-leader/references/execution.md +405 -0
- package/skills/yui-leader/references/integration.md +52 -2
- package/skills/yui-leader/references/planning.md +109 -0
- package/skills/yui-leader/references/task-plugins.md +8 -4
- package/skills/yui-operator/SKILL.md +22 -4
- package/skills/yui-runtime/SKILL.md +27 -0
- package/skills/yui-runtime/references/publication.md +20 -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";
|
|
@@ -21,15 +19,48 @@ import { AGENT_HOST_CLIENT_EXIT_GRACE_MS, AGENT_HOST_CONTROL_TIMEOUT_MS, AGENT_H
|
|
|
21
19
|
import { providerDeliveryFailure, providerDeliveryFailureFrom, redactAgentErrorText, serializeAgentErrorRaw } from "./agentError.js";
|
|
22
20
|
import { runCodexInteractiveHost } from "./codexInteractiveHost.js";
|
|
23
21
|
import { readAgentRunConfigurationObservation, unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
|
|
24
|
-
import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
|
|
25
22
|
import { createSessionOwnerIdentity, readLinuxProcessIdentity } from "./sessionOwnerIdentity.js";
|
|
26
23
|
import { yuiTmuxServerName, yuiTmuxSessionName } from "../tmux/tmuxManager.js";
|
|
27
24
|
import { tmuxSocketDirectory } from "../tmux/tmuxSocketEndpoint.js";
|
|
28
|
-
import {
|
|
29
|
-
|
|
25
|
+
import { AGENT_HOST_CONTROL_PROTOCOL, AGENT_HOST_EVENT_PROTOCOL } from "./agentHostProtocol.js";
|
|
26
|
+
import { FILE_TASK_CONTROLLER_PROTOCOL_VERSION } from "../core/protocol.js";
|
|
27
|
+
export { AGENT_HOST_CONTROL_PROTOCOL } from "./agentHostProtocol.js";
|
|
30
28
|
const HOST_CONTROL_MAX_BYTES = 32 * 1024;
|
|
31
29
|
const CODEX_CLIENT_STABLE_MS = 5_000;
|
|
32
30
|
const MAX_CONSECUTIVE_CODEX_DISCONNECTS = 3;
|
|
31
|
+
export function foldSteerLiveReceipt(control) {
|
|
32
|
+
const detail = control.failure?.detail ?? control.snapshot.detail;
|
|
33
|
+
const withDetail = detail === undefined ? {} : { detail };
|
|
34
|
+
switch (control.outcome) {
|
|
35
|
+
case "accepted":
|
|
36
|
+
// The only ok state. A live-accepted steer keeps `not-steered` (static,
|
|
37
|
+
// pre-flight) and this live disposition strictly separate phases.
|
|
38
|
+
return { state: "steered", outcome: "accepted" };
|
|
39
|
+
case "pending":
|
|
40
|
+
return { state: "steer-unknown", outcome: "pending", ...withDetail };
|
|
41
|
+
case "rejected":
|
|
42
|
+
return { state: "steer-rejected", outcome: "rejected", ...withDetail };
|
|
43
|
+
default:
|
|
44
|
+
// A steer edge only ever answers accepted/pending/rejected; `busy`,
|
|
45
|
+
// `status`, or `cancel-requested` here means the Host could not run the
|
|
46
|
+
// steer. Report it faithfully rather than flattening to success.
|
|
47
|
+
return { state: "steer-unavailable", outcome: control.outcome, ...withDetail };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function foldInterruptLiveReceipt(control) {
|
|
51
|
+
const cancellation = control.cancellation;
|
|
52
|
+
const withCancellation = cancellation === undefined ? {} : { cancellation };
|
|
53
|
+
if (control.outcome !== "cancel-requested") {
|
|
54
|
+
const detail = control.failure?.detail ?? control.snapshot.detail;
|
|
55
|
+
return { state: "interrupt-unavailable", outcome: control.outcome,
|
|
56
|
+
...withCancellation, ...(detail === undefined ? {} : { detail }) };
|
|
57
|
+
}
|
|
58
|
+
// A transport outcome without native cancellation evidence proves no stop.
|
|
59
|
+
const state = cancellation?.status === "not-active" ? "interrupt-not-active"
|
|
60
|
+
: cancellation?.status === "requested" ? "interrupt-requested"
|
|
61
|
+
: "interrupt-unknown";
|
|
62
|
+
return { state, outcome: "cancel-requested", ...withCancellation };
|
|
63
|
+
}
|
|
33
64
|
export function serializeAgentHostLaunchControl(control) {
|
|
34
65
|
return JSON.stringify(validateControl(control));
|
|
35
66
|
}
|
|
@@ -37,6 +68,11 @@ export async function runAgentHost(input) {
|
|
|
37
68
|
const hostInstanceId = randomUUID();
|
|
38
69
|
let hostSequence = 0;
|
|
39
70
|
let payload = await redeem(input.home, input.ticket);
|
|
71
|
+
if (payload.environment.YUI_SESSION_SCOPE === "global" && payload.providerControl !== undefined) {
|
|
72
|
+
process.stdout.write("Yui controlled Global Session: this is the Host console, not the native Agent TUI. "
|
|
73
|
+
+ "Type a line to submit to this Session; model, permissions and workspace remain as configured. "
|
|
74
|
+
+ "Queued inputs and live controls use the same Endpoint.\n");
|
|
75
|
+
}
|
|
40
76
|
if (payload.environment.YUI_SESSION_SCOPE === "global"
|
|
41
77
|
&& payload.environment.YUI_ADAPTER_ID === "codex"
|
|
42
78
|
&& payload.providerControl === undefined) {
|
|
@@ -304,7 +340,7 @@ export async function runAgentHost(input) {
|
|
|
304
340
|
if (hostStopRequested)
|
|
305
341
|
return;
|
|
306
342
|
try {
|
|
307
|
-
|
|
343
|
+
await assertHostExecutionEnvironment(input.home, reconnectPayload, disconnectedSession.nativeSessionId);
|
|
308
344
|
// Reattaching a dropped client continues the same Session on the same
|
|
309
345
|
// pinned generation; it never re-selects an implementation.
|
|
310
346
|
if (endpointLease === undefined) {
|
|
@@ -549,7 +585,8 @@ export async function runAgentHost(input) {
|
|
|
549
585
|
activeRunAttemptId = providerControl.ownedTurn.attemptId;
|
|
550
586
|
activeNativeTurnId = providerControl.ownedTurn.turnId;
|
|
551
587
|
}
|
|
552
|
-
|
|
588
|
+
// The Controller revalidates the adopted execution environment while
|
|
589
|
+
// redeeming this launch; a persistent Host must not reopen its schema.
|
|
553
590
|
// Pin before opening: the Session holds this exact generation for every
|
|
554
591
|
// later Turn. A pinned reference naming code this process is not running
|
|
555
592
|
// fails here, rather than silently starting on a different generation.
|
|
@@ -559,8 +596,7 @@ export async function runAgentHost(input) {
|
|
|
559
596
|
? endpointLease.open(next) : endpointLease.resume(next));
|
|
560
597
|
session = started.session;
|
|
561
598
|
sessionPayload = next;
|
|
562
|
-
|
|
563
|
-
recordNativeConnection(input.home, next, started.session);
|
|
599
|
+
await recordProviderConnection(input.home, next, started.session);
|
|
564
600
|
lastTerminalPayload = undefined;
|
|
565
601
|
lastTerminal = undefined;
|
|
566
602
|
// Recoverable means a later process can rebind this Conversation by
|
|
@@ -611,6 +647,7 @@ export async function runAgentHost(input) {
|
|
|
611
647
|
await publishStructuredProviderOpened({
|
|
612
648
|
home: input.home,
|
|
613
649
|
environment: next.environment,
|
|
650
|
+
startupRunId: next.startupRunId,
|
|
614
651
|
conversationId: session.conversationId,
|
|
615
652
|
nativeSessionId: session.nativeSessionId,
|
|
616
653
|
recoverability: conversationRecoverability,
|
|
@@ -790,7 +827,8 @@ export async function runAgentHost(input) {
|
|
|
790
827
|
let providerAccepted = false;
|
|
791
828
|
let transportAccepted = false;
|
|
792
829
|
try {
|
|
793
|
-
|
|
830
|
+
// beginDurableProviderTurn validated current execution-environment,
|
|
831
|
+
// authority and Session identity before this native write.
|
|
794
832
|
const receipt = endpointReceipt(await session.submit({
|
|
795
833
|
...request.run,
|
|
796
834
|
boundedText: withSessionContextPointer(request.run.boundedText, activeRunPayload.environment),
|
|
@@ -906,7 +944,7 @@ export async function runAgentHost(input) {
|
|
|
906
944
|
throw new Error("Agent Host rejected a stale Provider writer fence.");
|
|
907
945
|
}
|
|
908
946
|
try {
|
|
909
|
-
|
|
947
|
+
await assertHostExecutionEnvironment(input.home, sessionPayload, session.nativeSessionId);
|
|
910
948
|
const receipt = endpointReceipt(await session.steer({
|
|
911
949
|
...request.run,
|
|
912
950
|
boundedText: withSessionContextPointer(request.run.boundedText, sessionPayload.environment),
|
|
@@ -972,6 +1010,9 @@ export async function runAgentHost(input) {
|
|
|
972
1010
|
}));
|
|
973
1011
|
}
|
|
974
1012
|
if (request.type === "cancel") {
|
|
1013
|
+
if (request.nativeOnly === true && session?.capabilities.cancel !== "native-interrupt") {
|
|
1014
|
+
throw new Error("This Endpoint cannot natively interrupt; owned-process termination is not input control.");
|
|
1015
|
+
}
|
|
975
1016
|
if (session === undefined || request.nativeSessionId !== session.nativeSessionId
|
|
976
1017
|
|| request.attemptId !== activeRunAttemptId
|
|
977
1018
|
|| authority === undefined || !sameProviderAuthorityFence(authority, request.authority)) {
|
|
@@ -1079,7 +1120,8 @@ export async function runAgentHost(input) {
|
|
|
1079
1120
|
? createInterface({ input: process.stdin, output: process.stdout, terminal: true })
|
|
1080
1121
|
: undefined;
|
|
1081
1122
|
promptHuman = () => {
|
|
1082
|
-
if (humanConsole !== undefined && authority?.owner === "human"
|
|
1123
|
+
if (humanConsole !== undefined && (authority?.owner === "human"
|
|
1124
|
+
|| sessionPayload?.environment.YUI_SESSION_SCOPE === "global" && authority?.owner === "controller")
|
|
1083
1125
|
&& activeRunPayload === undefined
|
|
1084
1126
|
&& ["idle", "rejected", "failed"].includes(snapshot.state)) {
|
|
1085
1127
|
humanConsole.setPrompt("yui(provider)> ");
|
|
@@ -1091,7 +1133,8 @@ export async function runAgentHost(input) {
|
|
|
1091
1133
|
const currentAuthority = authority;
|
|
1092
1134
|
const currentSession = session;
|
|
1093
1135
|
const currentPayload = sessionPayload;
|
|
1094
|
-
if (currentAuthority?.owner !== "human"
|
|
1136
|
+
if ((currentAuthority?.owner !== "human"
|
|
1137
|
+
&& !(currentPayload?.environment.YUI_SESSION_SCOPE === "global" && currentAuthority?.owner === "controller"))
|
|
1095
1138
|
|| currentSession === undefined
|
|
1096
1139
|
|| currentPayload === undefined) {
|
|
1097
1140
|
process.stderr.write("Provider input rejected: human authority is not active.\n");
|
|
@@ -1111,8 +1154,10 @@ export async function runAgentHost(input) {
|
|
|
1111
1154
|
boundedText
|
|
1112
1155
|
}
|
|
1113
1156
|
};
|
|
1114
|
-
await submitRun(runControl, {});
|
|
1115
|
-
process.stdout.write(
|
|
1157
|
+
const submitted = await submitRun(runControl, {});
|
|
1158
|
+
process.stdout.write(submitted.inputAcceptance === "provider"
|
|
1159
|
+
? "Provider accepted this console input; waiting for its terminal boundary.\n"
|
|
1160
|
+
: "Input written to transport; Provider acceptance is not yet proven.\n");
|
|
1116
1161
|
}).catch((error) => {
|
|
1117
1162
|
process.stderr.write(`Provider input failed: ${errorText(error)}\n`);
|
|
1118
1163
|
promptHuman();
|
|
@@ -1204,58 +1249,42 @@ export async function runAgentHost(input) {
|
|
|
1204
1249
|
}
|
|
1205
1250
|
}
|
|
1206
1251
|
/** Host-restart-independent OS custody. No secret or launch payload is stored. */
|
|
1207
|
-
function
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
if (identity === undefined)
|
|
1252
|
+
async function recordProviderConnection(home, payload, endpoint) {
|
|
1253
|
+
const environment = payload.environment;
|
|
1254
|
+
const identity = endpoint.ownedProcessId === undefined ? undefined : readLinuxProcessIdentity(endpoint.ownedProcessId);
|
|
1255
|
+
if (endpoint.ownedProcessId !== undefined && identity === undefined) {
|
|
1212
1256
|
throw new Error("Dedicated Provider process identity was lost before registration.");
|
|
1257
|
+
}
|
|
1213
1258
|
const taskId = environment.YUI_TASK_ID;
|
|
1214
1259
|
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
|
-
}
|
|
1260
|
+
const userHome = resolve(payload.cwd, environment.HOME ?? homedir());
|
|
1261
|
+
await publishStructuredProviderConnection({
|
|
1262
|
+
home, environment, nativeSessionId: endpoint.nativeSessionId, startupRunId: payload.startupRunId,
|
|
1263
|
+
connection: {
|
|
1264
|
+
...(identity === undefined ? {} : { processOwner: createSessionOwnerIdentity({
|
|
1265
|
+
owner: environment.YUI_SESSION_SCOPE === "global"
|
|
1266
|
+
? { scope: "global", roleName } : { scope: "task", taskId, roleName },
|
|
1267
|
+
agentId: environment.YUI_AGENT_ID, adapterId: endpoint.adapterId,
|
|
1268
|
+
nativeSessionId: endpoint.nativeSessionId,
|
|
1269
|
+
tmux: {
|
|
1270
|
+
serverName: yuiTmuxServerName(home),
|
|
1271
|
+
socketPath: join(tmuxSocketDirectory(environment), yuiTmuxServerName(home)),
|
|
1272
|
+
sessionName: yuiTmuxSessionName(home, taskId ?? "operator"), windowName: roleName, panePid: process.pid
|
|
1273
|
+
},
|
|
1274
|
+
providerRoot: { pid: identity.pid, startIdentity: identity.startIdentity,
|
|
1275
|
+
processGroupId: identity.processGroupId, processSessionId: identity.processSessionId,
|
|
1276
|
+
attribution: "owned-child" },
|
|
1277
|
+
recordedAt: new Date()
|
|
1278
|
+
}) }),
|
|
1279
|
+
...(endpoint.adapterId !== "codex" ? {} : { account: {
|
|
1280
|
+
// Account location is needed after Controller/Host loss. Never retain
|
|
1281
|
+
// resolved credentials or duplicate the full process environment.
|
|
1282
|
+
home: userHome,
|
|
1283
|
+
codexHome: resolve(payload.cwd, environment.CODEX_HOME ?? join(userHome, ".codex")),
|
|
1284
|
+
...(endpoint.nativeAccountHome === undefined ? {} : { nativeAccountHome: endpoint.nativeAccountHome })
|
|
1285
|
+
} })
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1259
1288
|
}
|
|
1260
1289
|
export function agentHostControlSocketPath(input) {
|
|
1261
1290
|
const owner = input.scope === "task" ? input.taskId ?? "missing-task" : "global";
|
|
@@ -1329,13 +1358,22 @@ export async function waitForAgentHostLaunchAck(input) {
|
|
|
1329
1358
|
}
|
|
1330
1359
|
async function sendAgentHostControl(input) {
|
|
1331
1360
|
const path = agentHostControlSocketPath(input);
|
|
1361
|
+
return exchangeAgentHostControl(path, input.control, AGENT_HOST_CONTROL_TIMEOUT_MS);
|
|
1362
|
+
}
|
|
1363
|
+
/** Read-only upgrade/diagnostic probe of a Home-fenced socket. Never launches a Host. */
|
|
1364
|
+
export async function inspectAgentHostSocket(path, timeoutMs = 250) {
|
|
1365
|
+
return (await exchangeAgentHostControl(path, {
|
|
1366
|
+
protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "status"
|
|
1367
|
+
}, timeoutMs)).snapshot;
|
|
1368
|
+
}
|
|
1369
|
+
async function exchangeAgentHostControl(path, control, timeoutMs) {
|
|
1332
1370
|
return await new Promise((resolvePromise, reject) => {
|
|
1333
1371
|
const client = createConnection(path);
|
|
1334
1372
|
let response = "";
|
|
1335
1373
|
const timer = setTimeout(() => {
|
|
1336
1374
|
client.destroy();
|
|
1337
1375
|
reject(new Error("Agent Host control request timed out."));
|
|
1338
|
-
},
|
|
1376
|
+
}, timeoutMs);
|
|
1339
1377
|
let settled = false;
|
|
1340
1378
|
const settle = (callback, value) => {
|
|
1341
1379
|
if (settled)
|
|
@@ -1345,7 +1383,7 @@ async function sendAgentHostControl(input) {
|
|
|
1345
1383
|
callback(value);
|
|
1346
1384
|
};
|
|
1347
1385
|
client.setEncoding("utf8");
|
|
1348
|
-
client.once("connect", () => client.end(`${JSON.stringify(validateControl(
|
|
1386
|
+
client.once("connect", () => client.end(`${JSON.stringify(validateControl(control))}\n`));
|
|
1349
1387
|
client.on("data", (chunk) => {
|
|
1350
1388
|
response += chunk;
|
|
1351
1389
|
if (Buffer.byteLength(response, "utf8") > HOST_CONTROL_MAX_BYTES) {
|
|
@@ -1370,6 +1408,17 @@ async function redeem(home, ticket) {
|
|
|
1370
1408
|
});
|
|
1371
1409
|
return validateAgentHostLaunchPayload(result);
|
|
1372
1410
|
}
|
|
1411
|
+
async function assertHostExecutionEnvironment(home, payload, nativeSessionId) {
|
|
1412
|
+
if (payload.executionEnvironment === undefined)
|
|
1413
|
+
return;
|
|
1414
|
+
await callController(home, "runtime.execution-environment-check", {
|
|
1415
|
+
taskId: payload.environment.YUI_TASK_ID,
|
|
1416
|
+
roleName: payload.environment.YUI_ROLE,
|
|
1417
|
+
agentId: payload.environment.YUI_AGENT_ID,
|
|
1418
|
+
workspace: payload.cwd,
|
|
1419
|
+
nativeSessionId
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1373
1422
|
async function persistAndSubmitExit(home, observation) {
|
|
1374
1423
|
try {
|
|
1375
1424
|
await persistRuntimeProcessExitObservation(home, observation, (value) => callController(home, "runtime.process-exit-observe", value).then(() => undefined));
|
|
@@ -1426,7 +1475,26 @@ export async function openAgentHostControl(home, payload, snapshot, dispatch) {
|
|
|
1426
1475
|
void (async () => {
|
|
1427
1476
|
try {
|
|
1428
1477
|
const request = validateControl(JSON.parse(body.trim()));
|
|
1429
|
-
|
|
1478
|
+
const result = await dispatch(request);
|
|
1479
|
+
socket.end(`${JSON.stringify(boundControlResponse({
|
|
1480
|
+
...result,
|
|
1481
|
+
snapshot: {
|
|
1482
|
+
...result.snapshot,
|
|
1483
|
+
compatibility: {
|
|
1484
|
+
control: AGENT_HOST_CONTROL_PROTOCOL, events: AGENT_HOST_EVENT_PROTOCOL,
|
|
1485
|
+
rpc: FILE_TASK_CONTROLLER_PROTOCOL_VERSION, storage: "controller-owned"
|
|
1486
|
+
},
|
|
1487
|
+
hostProcess: { pid: process.pid, startIdentity: readLinuxProcessIdentity(process.pid).startIdentity },
|
|
1488
|
+
owner: {
|
|
1489
|
+
scope: payload.environment.YUI_SESSION_SCOPE ?? "task",
|
|
1490
|
+
...(payload.environment.YUI_TASK_ID === undefined ? {} : { taskId: payload.environment.YUI_TASK_ID }),
|
|
1491
|
+
roleName: payload.environment.YUI_ROLE ?? "unknown-role"
|
|
1492
|
+
},
|
|
1493
|
+
...(payload.environment.YUI_SESSION_SCOPE !== "task" ? {} : {
|
|
1494
|
+
eventDelivery: structuredProviderEventDelivery(home, payload.environment, result.snapshot.nativeSessionId)
|
|
1495
|
+
})
|
|
1496
|
+
}
|
|
1497
|
+
}))}\n`);
|
|
1430
1498
|
}
|
|
1431
1499
|
catch (caught) {
|
|
1432
1500
|
const error = caught instanceof AgentHostOperationError ? caught.cause : caught;
|
|
@@ -1565,6 +1633,9 @@ function validateControl(control) {
|
|
|
1565
1633
|
if (control.type === "status")
|
|
1566
1634
|
return Object.freeze({ ...control });
|
|
1567
1635
|
if (control.type === "cancel") {
|
|
1636
|
+
if (control.nativeOnly !== undefined && typeof control.nativeOnly !== "boolean") {
|
|
1637
|
+
throw new Error("Native-only cancellation flag is invalid.");
|
|
1638
|
+
}
|
|
1568
1639
|
validateIdentity(control.nativeSessionId, "native Session id");
|
|
1569
1640
|
validateIdentity(control.attemptId, "Provider input attempt id");
|
|
1570
1641
|
validateProviderAuthorityFence(control.authority);
|
|
@@ -1746,9 +1817,11 @@ async function observePendingHostSubmission(submission, snapshot) {
|
|
|
1746
1817
|
function signalRoleMailbox(home, payload) {
|
|
1747
1818
|
const taskId = payload.environment.YUI_TASK_ID;
|
|
1748
1819
|
const roleName = payload.environment.YUI_ROLE;
|
|
1749
|
-
if (
|
|
1820
|
+
if (roleName === undefined)
|
|
1750
1821
|
return;
|
|
1751
|
-
const key =
|
|
1822
|
+
const key = taskId === undefined
|
|
1823
|
+
? `global-role:${encodeURIComponent(roleName)}`
|
|
1824
|
+
: `role:${encodeURIComponent(taskId)}/${encodeURIComponent(roleName)}`;
|
|
1752
1825
|
void callController(home, "scheduler.signal", { key }).catch(() => {
|
|
1753
1826
|
// This is a low-latency hint. Durable mailbox state and periodic
|
|
1754
1827
|
// reconciliation remain the recovery path across a Controller handover.
|
|
@@ -1763,7 +1836,9 @@ async function delay(milliseconds) {
|
|
|
1763
1836
|
function hostRunControlParams(payload, nativeSessionId, authority, attemptId, runId) {
|
|
1764
1837
|
const environment = payload.environment;
|
|
1765
1838
|
return Object.freeze({
|
|
1766
|
-
|
|
1839
|
+
...(environment.YUI_SESSION_SCOPE === "global" ? {} : {
|
|
1840
|
+
taskId: requiredEnvironment(environment.YUI_TASK_ID, "Task id")
|
|
1841
|
+
}),
|
|
1767
1842
|
roleName: requiredEnvironment(environment.YUI_ROLE, "Role name"),
|
|
1768
1843
|
...(runId === undefined ? {} : { runId: requiredEnvironment(runId, "AgentRun id") }),
|
|
1769
1844
|
agentId: requiredEnvironment(environment.YUI_AGENT_ID, "Agent id"),
|
|
@@ -1868,7 +1943,12 @@ async function callAgentController(home, method, params) {
|
|
|
1868
1943
|
throw error;
|
|
1869
1944
|
}
|
|
1870
1945
|
}
|
|
1871
|
-
|
|
1946
|
+
const deadline = Date.now() + AGENT_HOST_CONTROL_TIMEOUT_MS;
|
|
1947
|
+
while (isForeignHandoverLockHeld(home) && Date.now() < deadline)
|
|
1948
|
+
await delay(50);
|
|
1949
|
+
// The pinned Host can rediscover a replacement, but must never start a
|
|
1950
|
+
// Controller using its own old implementation or open the Home's database.
|
|
1951
|
+
await callController(home, method, params);
|
|
1872
1952
|
}
|
|
1873
1953
|
function isControllerUnavailable(error) {
|
|
1874
1954
|
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
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createSessionOwnerIdentity } from "./sessionOwnerIdentity.js";
|
|
2
|
+
/** Independent of CLI/RPC and Home schema. Keep v1 readable while v1 Hosts live. */
|
|
3
|
+
export const AGENT_HOST_EVENT_PROTOCOL = "yui-agent-host-events/v1";
|
|
4
|
+
export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v5";
|
|
5
|
+
export function readAgentHostObservationSource(value) {
|
|
6
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
7
|
+
throw new Error("Agent Host observation source is invalid.");
|
|
8
|
+
}
|
|
9
|
+
const source = value;
|
|
10
|
+
if (source.protocol !== AGENT_HOST_EVENT_PROTOCOL) {
|
|
11
|
+
throw new Error("Agent Host event protocol is unsupported.");
|
|
12
|
+
}
|
|
13
|
+
if (source.connection !== undefined && (source.connection === null
|
|
14
|
+
|| typeof source.connection !== "object" || Array.isArray(source.connection))) {
|
|
15
|
+
throw new Error("Agent Host connection evidence is invalid.");
|
|
16
|
+
}
|
|
17
|
+
if (source.connection?.processOwner !== undefined
|
|
18
|
+
&& (source.connection.processOwner.schemaVersion !== 2
|
|
19
|
+
|| source.connection.processOwner.kind !== "yui-session-owner")) {
|
|
20
|
+
throw new Error("Agent Host process custody wire contract is invalid.");
|
|
21
|
+
}
|
|
22
|
+
const text = (value) => {
|
|
23
|
+
if (typeof value !== "string" || !value.trim() || value.includes("\0") || value.length > 4096) {
|
|
24
|
+
throw new Error("Agent Host observation identity is invalid.");
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
protocol: AGENT_HOST_EVENT_PROTOCOL,
|
|
30
|
+
adapterId: text(source.adapterId),
|
|
31
|
+
workspace: text(source.workspace),
|
|
32
|
+
...(source.startupRunId === undefined ? {} : { startupRunId: text(source.startupRunId) }),
|
|
33
|
+
...(source.connection === undefined ? {} : {
|
|
34
|
+
connection: {
|
|
35
|
+
...(source.connection.processOwner === undefined ? {} : {
|
|
36
|
+
processOwner: createSessionOwnerIdentity({
|
|
37
|
+
...source.connection.processOwner,
|
|
38
|
+
recordedAt: new Date(source.connection.processOwner.recordedAt)
|
|
39
|
+
})
|
|
40
|
+
}),
|
|
41
|
+
...(source.connection.account === undefined ? {} : {
|
|
42
|
+
account: {
|
|
43
|
+
home: text(source.connection.account.home),
|
|
44
|
+
codexHome: text(source.connection.account.codexHome),
|
|
45
|
+
...(source.connection.account.nativeAccountHome === undefined ? {} : {
|
|
46
|
+
nativeAccountHome: text(source.connection.account.nativeAccountHome)
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
|
-
import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
|
|
3
2
|
import { createProjectResources } from "../resources/projectResourceService.js";
|
|
4
3
|
/** Revalidate the adopted owner; a saved path alone never authorizes execution. */
|
|
5
4
|
export function assertExecutionEnvironmentCurrent(store, taskId, snapshot) {
|
|
@@ -10,21 +9,3 @@ export function assertExecutionEnvironmentCurrent(store, taskId, snapshot) {
|
|
|
10
9
|
throw new Error("Execution environment changed; select an adopted environment and start a new Session.");
|
|
11
10
|
}
|
|
12
11
|
}
|
|
13
|
-
/** Last local check before native process creation or another Provider input. */
|
|
14
|
-
export function assertAgentExecutionEnvironment(home, payload) {
|
|
15
|
-
const snapshot = payload.executionEnvironment;
|
|
16
|
-
if (snapshot === undefined)
|
|
17
|
-
return;
|
|
18
|
-
if (payload.environment.YUI_SESSION_SCOPE !== "task"
|
|
19
|
-
|| payload.environment.YUI_TASK_ID !== snapshot.taskId
|
|
20
|
-
|| payload.cwd !== snapshot.directory.path) {
|
|
21
|
-
throw new Error("Agent launch does not match its adopted execution environment.");
|
|
22
|
-
}
|
|
23
|
-
const store = openCurrentTaskStore(home);
|
|
24
|
-
try {
|
|
25
|
-
assertExecutionEnvironmentCurrent(store, snapshot.taskId, snapshot);
|
|
26
|
-
}
|
|
27
|
-
finally {
|
|
28
|
-
store.close();
|
|
29
|
-
}
|
|
30
|
-
}
|
|
@@ -60,6 +60,12 @@ function validatePayload(payload) {
|
|
|
60
60
|
throw new Error("Agent Host launch payload version is invalid.");
|
|
61
61
|
text(payload.command, "command");
|
|
62
62
|
text(payload.cwd, "cwd");
|
|
63
|
+
if (payload.startupRunId !== undefined) {
|
|
64
|
+
text(payload.startupRunId, "startup Run id");
|
|
65
|
+
if (payload.environment?.YUI_SESSION_SCOPE !== "task") {
|
|
66
|
+
throw new Error("Only a Task launch may carry a startup Run identity.");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
63
69
|
if (!Array.isArray(payload.args))
|
|
64
70
|
throw new Error("Agent Host launch args must be an array.");
|
|
65
71
|
payload.args.forEach((value) => text(value, "argument"));
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* live Session can always be re-attributed and reported.
|
|
9
9
|
*
|
|
10
10
|
* Pure: all I/O is injected. Unknown owners are reported, never cleaned.
|
|
11
|
-
* An entry
|
|
12
|
-
*
|
|
11
|
+
* An entry blocks ordinary archive / workspace cleanup when a terminal Task
|
|
12
|
+
* still owns a live or unverified physical root, regardless of Session status.
|
|
13
13
|
*/
|
|
14
14
|
export function reconcileSessionOwners(input) {
|
|
15
15
|
const entries = input.records.map((record) => reconcileOne(record, input));
|
|
@@ -54,8 +54,8 @@ function reconcileOne(record, input) {
|
|
|
54
54
|
const terminalTask = taskStatus === "completed"
|
|
55
55
|
|| taskStatus === "cancelled"
|
|
56
56
|
|| taskStatus === "archived";
|
|
57
|
-
const archiveBlocked =
|
|
58
|
-
|
|
57
|
+
const archiveBlocked = terminalTask && (physical === undefined
|
|
58
|
+
|| physical.alive || physical.identityConflict);
|
|
59
59
|
return {
|
|
60
60
|
owner: {
|
|
61
61
|
scope: owner.scope,
|