@zq-silk/yui 0.2.0 → 0.4.2
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 +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -1,13 +1,53 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { callController } from "../core/controllerClient.js";
|
|
3
|
+
import { callController, readControllerDiscovery } from "../core/controllerClient.js";
|
|
4
|
+
import { FILE_TASK_CONTROLLER_PROTOCOL_VERSION } from "../core/protocol.js";
|
|
5
|
+
import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, operationalAgentEnvironment, selectEnvironment, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
|
|
6
|
+
import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
|
|
7
|
+
import { hasRuntimeLifecycleWork } from "../runtime/lifecycleReservation.js";
|
|
8
|
+
import { assertControllerStatusIdentity } from "../runtime/exactControlPlane.js";
|
|
9
|
+
import { EPHEMERAL_DOMAIN_ENVIRONMENT_NAMES } from "./domainIdentity.js";
|
|
10
|
+
import { YUI_VERSION, yuiVersionIdentity } from "../version.js";
|
|
4
11
|
const STARTUP_TIMEOUT_MS = 5_000;
|
|
12
|
+
// A lifecycle RPC may legitimately occupy the Controller for 30 seconds.
|
|
13
|
+
// Restart must allow that request to drain before deciding shutdown is stuck.
|
|
14
|
+
const SHUTDOWN_TIMEOUT_MS = 45_000;
|
|
5
15
|
const POLL_INTERVAL_MS = 50;
|
|
16
|
+
const LIFECYCLE_REQUEST_TIMEOUT_MS = 30_000;
|
|
17
|
+
const ENVIRONMENT_REFRESH_TIMEOUT_MS = 500;
|
|
18
|
+
const CONFIGURATION_REFRESH_TIMEOUT_MS = 500;
|
|
19
|
+
const CONTROLLER_OPERATIONAL_ENVIRONMENT = [
|
|
20
|
+
...AGENT_OPERATIONAL_ENVIRONMENT_NAMES,
|
|
21
|
+
"YUI_TMUX_BIN",
|
|
22
|
+
...EPHEMERAL_DOMAIN_ENVIRONMENT_NAMES
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Direct FileTaskStore writers must not extend state while an older Controller
|
|
26
|
+
* is still reading the same YUI_HOME.
|
|
27
|
+
*/
|
|
28
|
+
export async function assertFileTaskControllerStorageCompatible(home, options = {}) {
|
|
29
|
+
const call = options.call ?? callController;
|
|
30
|
+
let status;
|
|
31
|
+
try {
|
|
32
|
+
status = await call(home, "controller.status", {});
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (isDefinitelyNotRunning(error))
|
|
36
|
+
return;
|
|
37
|
+
if (isUnavailable(error)) {
|
|
38
|
+
throw new Error("Controller compatibility could not be verified. Retry or run `yui controller restart`.", { cause: error });
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
assertCompatibleControllerStatus(status);
|
|
43
|
+
}
|
|
6
44
|
/** Starts the per-home FileTask Controller on demand and waits until callable. */
|
|
7
45
|
export async function ensureFileTaskController(home, options = {}) {
|
|
8
46
|
const call = options.call ?? callController;
|
|
9
47
|
try {
|
|
10
|
-
|
|
48
|
+
const status = await call(home, "controller.status", {});
|
|
49
|
+
assertCompatibleControllerStatus(status, options.expectedVersion);
|
|
50
|
+
return status;
|
|
11
51
|
}
|
|
12
52
|
catch (error) {
|
|
13
53
|
if (!isUnavailable(error))
|
|
@@ -16,11 +56,13 @@ export async function ensureFileTaskController(home, options = {}) {
|
|
|
16
56
|
const timeoutMs = positive(options.startupTimeoutMs, STARTUP_TIMEOUT_MS, "startupTimeoutMs");
|
|
17
57
|
const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
|
|
18
58
|
const spawnController = options.spawnController ?? spawnDetachedFileTaskController;
|
|
19
|
-
spawnController(home,
|
|
59
|
+
spawnController(home, controllerSpawnEnvironment(home, options.environment ?? process.env));
|
|
20
60
|
const deadline = Date.now() + timeoutMs;
|
|
21
61
|
for (;;) {
|
|
22
62
|
try {
|
|
23
|
-
|
|
63
|
+
const status = await call(home, "controller.status", {});
|
|
64
|
+
assertCompatibleControllerStatus(status, options.expectedVersion);
|
|
65
|
+
return status;
|
|
24
66
|
}
|
|
25
67
|
catch (error) {
|
|
26
68
|
if (!isUnavailable(error))
|
|
@@ -32,6 +74,79 @@ export async function ensureFileTaskController(home, options = {}) {
|
|
|
32
74
|
}
|
|
33
75
|
}
|
|
34
76
|
}
|
|
77
|
+
/** Start and await readiness for a captured Controller identity. */
|
|
78
|
+
export async function ensureFileTaskControllerIdentity(home, identity, options = {}) {
|
|
79
|
+
assertExpectedControllerLaunchIdentity(identity);
|
|
80
|
+
const status = await ensureFileTaskController(home, {
|
|
81
|
+
...options,
|
|
82
|
+
expectedVersion: identity.version
|
|
83
|
+
});
|
|
84
|
+
// A same-version status is not an identity proof: another Controller binary
|
|
85
|
+
// may own the Home after a restart or a stale discovery race. Authenticate
|
|
86
|
+
// the socket owner and compare the complete launch vector before reporting
|
|
87
|
+
// readiness. This post-readiness check deliberately has no retry or spawn
|
|
88
|
+
// fallback; a mismatch/unavailable/malformed identity is fail-closed.
|
|
89
|
+
const call = options.call ?? callController;
|
|
90
|
+
const actual = await call(home, "controller.identity", {}, {
|
|
91
|
+
timeoutMs: options.requestTimeoutMs
|
|
92
|
+
});
|
|
93
|
+
assertControllerLaunchIdentity(actual, identity);
|
|
94
|
+
return status;
|
|
95
|
+
}
|
|
96
|
+
function assertExpectedControllerLaunchIdentity(identity) {
|
|
97
|
+
if (typeof identity.executablePath !== "string"
|
|
98
|
+
|| identity.executablePath.length === 0
|
|
99
|
+
|| !Array.isArray(identity.args)
|
|
100
|
+
|| identity.args.some((arg) => typeof arg !== "string")
|
|
101
|
+
|| typeof identity.version !== "string"
|
|
102
|
+
|| identity.version.length === 0) {
|
|
103
|
+
throw new Error("Expected Controller launch identity is malformed; refusing to start or accept a Controller.");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function assertControllerLaunchIdentity(actual, expected) {
|
|
107
|
+
if (!isJsonRecord(actual)) {
|
|
108
|
+
throw new Error("Authenticated Controller launch identity is malformed; refusing to accept readiness.");
|
|
109
|
+
}
|
|
110
|
+
const actualArgs = actual.args;
|
|
111
|
+
if (typeof actual.executablePath !== "string"
|
|
112
|
+
|| !Array.isArray(actualArgs)
|
|
113
|
+
|| actualArgs.some((arg) => typeof arg !== "string")
|
|
114
|
+
|| typeof actual.version !== "string") {
|
|
115
|
+
throw new Error("Authenticated Controller launch identity is malformed; refusing to accept readiness.");
|
|
116
|
+
}
|
|
117
|
+
const argsMatch = actualArgs.length === expected.args.length
|
|
118
|
+
&& actualArgs.every((arg, index) => arg === expected.args[index]);
|
|
119
|
+
if (actual.executablePath !== expected.executablePath
|
|
120
|
+
|| !argsMatch
|
|
121
|
+
|| actual.version !== expected.version) {
|
|
122
|
+
throw new Error("Authenticated Controller launch identity does not match the captured executable, argv, and version; refusing readiness.");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function assertCompatibleControllerStatus(status, expectedVersion) {
|
|
126
|
+
const statusRecord = isJsonRecord(status) && status.running === true ? status : null;
|
|
127
|
+
const actual = statusRecord?.protocolVersion;
|
|
128
|
+
if (statusRecord === null || actual !== FILE_TASK_CONTROLLER_PROTOCOL_VERSION) {
|
|
129
|
+
throw new Error(`Controller protocol is incompatible (expected ${FILE_TASK_CONTROLLER_PROTOCOL_VERSION}, found ${typeof actual === "number" ? actual : "unknown"}). `
|
|
130
|
+
+ "Run `yui controller restart` before writing new task records.");
|
|
131
|
+
}
|
|
132
|
+
const actualVersion = statusRecord.version;
|
|
133
|
+
const expected = expectedVersion ?? YUI_VERSION;
|
|
134
|
+
const versionMismatch = expectedVersion === undefined
|
|
135
|
+
? typeof actualVersion === "string" && actualVersion !== expected
|
|
136
|
+
: actualVersion !== expected;
|
|
137
|
+
if (versionMismatch) {
|
|
138
|
+
throw new Error(`Controller version is incompatible (expected ${expected}, found ${typeof actualVersion === "string" ? actualVersion : "unknown"}). `
|
|
139
|
+
+ "Run `yui controller restart` before writing new task records.");
|
|
140
|
+
}
|
|
141
|
+
// Ordinary callers must authenticate the complete control-plane identity.
|
|
142
|
+
// An update/upgrade restore instead targets a previously captured Controller
|
|
143
|
+
// binary, which may predate the current storage-layout status fields; that
|
|
144
|
+
// path authenticates its executable, argv, and version immediately after
|
|
145
|
+
// readiness in ensureFileTaskControllerIdentity.
|
|
146
|
+
if (expectedVersion === undefined) {
|
|
147
|
+
assertControllerStatusIdentity(status, yuiVersionIdentity());
|
|
148
|
+
}
|
|
149
|
+
}
|
|
35
150
|
function spawnDetachedFileTaskController(home, environment) {
|
|
36
151
|
const child = spawn(process.execPath, [fileURLToPath(new URL("./controllerMain.js", import.meta.url))], {
|
|
37
152
|
env: environment,
|
|
@@ -40,22 +155,80 @@ function spawnDetachedFileTaskController(home, environment) {
|
|
|
40
155
|
});
|
|
41
156
|
child.unref();
|
|
42
157
|
}
|
|
158
|
+
/** Stops the per-home Controller and waits until its owned discovery is gone. */
|
|
159
|
+
export async function stopFileTaskController(home, options = {}) {
|
|
160
|
+
const call = options.call ?? callController;
|
|
161
|
+
const shutdownTimeoutMs = positive(options.shutdownTimeoutMs, SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
|
|
162
|
+
const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
|
|
163
|
+
const expectedPid = options.expectedPid === undefined
|
|
164
|
+
? undefined
|
|
165
|
+
: positive(options.expectedPid, 0, "expectedPid");
|
|
166
|
+
const current = await readOptionalControllerStatus(home, call);
|
|
167
|
+
const pid = controllerPid(current);
|
|
168
|
+
if (expectedPid !== undefined && pid !== expectedPid) {
|
|
169
|
+
throw new Error(`Controller ownership changed before fenced stop (expected PID ${expectedPid}, `
|
|
170
|
+
+ `found ${pid === undefined ? "none" : pid}).`);
|
|
171
|
+
}
|
|
172
|
+
if (!controllerRunning(current)) {
|
|
173
|
+
return { stopped: false, alreadyStopped: true };
|
|
174
|
+
}
|
|
175
|
+
await callFileTaskController(home, "controller.stop", expectedPid === undefined ? {} : { expectedPid }, options);
|
|
176
|
+
const deadline = Date.now() + shutdownTimeoutMs;
|
|
177
|
+
for (;;) {
|
|
178
|
+
if (options.call === undefined) {
|
|
179
|
+
const discoveryOwned = await ownedControllerDiscoveryExists(home, pid);
|
|
180
|
+
if (!discoveryOwned && expectedPid !== undefined) {
|
|
181
|
+
// A replacement owner appearing during the drain is not equivalent to
|
|
182
|
+
// the fenced process disappearing; fail closed instead of reporting a
|
|
183
|
+
// successful stop against a foreign PID.
|
|
184
|
+
const observed = controllerPid(await readOptionalControllerStatus(home, call));
|
|
185
|
+
if (observed !== undefined && observed !== expectedPid) {
|
|
186
|
+
throw new Error(`Controller ownership changed during fenced stop (expected PID ${expectedPid}, `
|
|
187
|
+
+ `found ${observed}).`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!discoveryOwned)
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const observed = controllerPid(await readOptionalControllerStatus(home, call));
|
|
195
|
+
if (observed === undefined)
|
|
196
|
+
break;
|
|
197
|
+
if (expectedPid !== undefined && observed !== expectedPid) {
|
|
198
|
+
throw new Error(`Controller ownership changed during fenced stop (expected PID ${expectedPid}, `
|
|
199
|
+
+ `found ${observed}).`);
|
|
200
|
+
}
|
|
201
|
+
if (observed !== pid)
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
if (Date.now() >= deadline) {
|
|
205
|
+
throw new Error(`Controller did not stop within ${shutdownTimeoutMs} ms.`);
|
|
206
|
+
}
|
|
207
|
+
await delay(pollMs);
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
stopped: true,
|
|
211
|
+
...(pid === undefined ? {} : { pid })
|
|
212
|
+
};
|
|
213
|
+
}
|
|
43
214
|
/** Restarts only the per-home Controller process; managed tmux sessions remain untouched. */
|
|
44
215
|
export async function restartFileTaskController(home, options = {}) {
|
|
45
216
|
const call = options.call ?? callController;
|
|
46
|
-
const
|
|
217
|
+
const shutdownTimeoutMs = positive(options.shutdownTimeoutMs, SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
|
|
47
218
|
const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
|
|
48
219
|
const current = await readOptionalControllerStatus(home, call);
|
|
49
220
|
const previousPid = controllerPid(current);
|
|
50
221
|
if (controllerRunning(current)) {
|
|
51
222
|
await callFileTaskController(home, "controller.stop", {}, options);
|
|
52
|
-
const deadline = Date.now() +
|
|
223
|
+
const deadline = Date.now() + shutdownTimeoutMs;
|
|
53
224
|
for (;;) {
|
|
54
|
-
const
|
|
55
|
-
|
|
225
|
+
const stillOwned = options.call === undefined
|
|
226
|
+
? await ownedControllerDiscoveryExists(home, previousPid)
|
|
227
|
+
: controllerPid(await readOptionalControllerStatus(home, call)) === previousPid;
|
|
228
|
+
if (!stillOwned)
|
|
56
229
|
break;
|
|
57
230
|
if (Date.now() >= deadline) {
|
|
58
|
-
throw new Error(`Controller did not stop within ${
|
|
231
|
+
throw new Error(`Controller did not stop within ${shutdownTimeoutMs} ms.`);
|
|
59
232
|
}
|
|
60
233
|
await delay(pollMs);
|
|
61
234
|
}
|
|
@@ -68,6 +241,40 @@ export async function restartFileTaskController(home, options = {}) {
|
|
|
68
241
|
...(pid === undefined ? {} : { pid })
|
|
69
242
|
};
|
|
70
243
|
}
|
|
244
|
+
function controllerSpawnEnvironment(home, source) {
|
|
245
|
+
const allowed = new Set(CONTROLLER_OPERATIONAL_ENVIRONMENT);
|
|
246
|
+
try {
|
|
247
|
+
for (const agent of openCompatibleFileTaskStore(home).listConfiguredAgents()) {
|
|
248
|
+
for (const name of nativeAgentEnvironmentNames(agent.adapterId))
|
|
249
|
+
allowed.add(name);
|
|
250
|
+
for (const binding of agent.environment)
|
|
251
|
+
allowed.add(binding.sourceName);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// The Controller remains authoritative for reporting an invalid/unavailable
|
|
256
|
+
// home. Environment filtering must never fall back to forwarding all names.
|
|
257
|
+
}
|
|
258
|
+
const environment = {};
|
|
259
|
+
for (const name of allowed) {
|
|
260
|
+
const value = source[name];
|
|
261
|
+
if (value !== undefined)
|
|
262
|
+
environment[name] = value;
|
|
263
|
+
}
|
|
264
|
+
environment.YUI_HOME = home;
|
|
265
|
+
return environment;
|
|
266
|
+
}
|
|
267
|
+
async function ownedControllerDiscoveryExists(home, previousPid) {
|
|
268
|
+
try {
|
|
269
|
+
const discovery = await readControllerDiscovery(home);
|
|
270
|
+
return previousPid === undefined || discovery.pid === previousPid;
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
if (isUnavailable(error))
|
|
274
|
+
return false;
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
71
278
|
export async function callFileTaskController(home, method, params = {}, options = {}) {
|
|
72
279
|
const call = options.call ?? callController;
|
|
73
280
|
if (method === "controller.status" || method === "controller.stop") {
|
|
@@ -82,8 +289,78 @@ export async function callFileTaskController(home, method, params = {}, options
|
|
|
82
289
|
: { stopped: false, alreadyStopped: true };
|
|
83
290
|
}
|
|
84
291
|
}
|
|
292
|
+
try {
|
|
293
|
+
return await call(home, method, params, {
|
|
294
|
+
timeoutMs: options.requestTimeoutMs
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
if (!isUnavailable(error))
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
85
301
|
await ensureFileTaskController(home, options);
|
|
86
|
-
return call(home, method, params
|
|
302
|
+
return call(home, method, params, {
|
|
303
|
+
timeoutMs: options.requestTimeoutMs
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Best-effort refresh for a Controller that is already running. This path
|
|
308
|
+
* deliberately calls the authenticated socket directly: an Agent config
|
|
309
|
+
* command must not start a background Controller merely to copy secrets.
|
|
310
|
+
*/
|
|
311
|
+
export async function refreshRunningFileTaskControllerEnvironment(home, store, source = process.env, options = {}) {
|
|
312
|
+
const configuredSourceNames = new Set();
|
|
313
|
+
const configuredNativeNames = new Set();
|
|
314
|
+
for (const agent of store.listConfiguredAgents()) {
|
|
315
|
+
for (const name of nativeAgentEnvironmentNames(agent.adapterId)) {
|
|
316
|
+
configuredNativeNames.add(name);
|
|
317
|
+
}
|
|
318
|
+
for (const binding of agent.environment) {
|
|
319
|
+
if (!MANAGED_RUNTIME_ENVIRONMENT.has(binding.sourceName)) {
|
|
320
|
+
configuredSourceNames.add(binding.sourceName);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const sourceNames = options.sourceNames === undefined
|
|
325
|
+
? [...configuredSourceNames]
|
|
326
|
+
: [...new Set(options.sourceNames)];
|
|
327
|
+
const nativeNames = options.nativeNames === undefined
|
|
328
|
+
? [...configuredNativeNames]
|
|
329
|
+
: [...new Set(options.nativeNames)];
|
|
330
|
+
const sources = selectEnvironment(source, sourceNames);
|
|
331
|
+
const nativeSources = selectEnvironment(source, nativeNames);
|
|
332
|
+
try {
|
|
333
|
+
await (options.call ?? callController)(home, "runtime.replace-agent-environment", { sources, sourceNames, nativeSources, nativeNames }, {
|
|
334
|
+
timeoutMs: options.requestTimeoutMs
|
|
335
|
+
?? ENVIRONMENT_REFRESH_TIMEOUT_MS
|
|
336
|
+
});
|
|
337
|
+
return { status: "refreshed" };
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
return classifyRefreshFailure(error, options);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/** Reloads durable Controller settings without starting an absent Controller. */
|
|
344
|
+
export async function refreshRunningFileTaskControllerConfiguration(home, options = {}) {
|
|
345
|
+
try {
|
|
346
|
+
await (options.call ?? callController)(home, "scheduler.configure", {}, {
|
|
347
|
+
timeoutMs: options.requestTimeoutMs
|
|
348
|
+
?? CONFIGURATION_REFRESH_TIMEOUT_MS
|
|
349
|
+
});
|
|
350
|
+
return { status: "refreshed" };
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
return classifyRefreshFailure(error, options);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function classifyRefreshFailure(error, options) {
|
|
357
|
+
if (isDefinitelyNotRunning(error))
|
|
358
|
+
return { status: "not-running" };
|
|
359
|
+
options.onError?.(error);
|
|
360
|
+
return {
|
|
361
|
+
status: "failed",
|
|
362
|
+
message: error instanceof Error ? error.message : String(error)
|
|
363
|
+
};
|
|
87
364
|
}
|
|
88
365
|
/** Foreground command bridge. It never reads or writes Agent terminal bytes. */
|
|
89
366
|
export class FileTaskWorkflowRuntime {
|
|
@@ -104,81 +381,156 @@ export class FileTaskWorkflowRuntime {
|
|
|
104
381
|
this.clientOptions = clientOptions;
|
|
105
382
|
}
|
|
106
383
|
notifyStateChanged(taskId) {
|
|
107
|
-
|
|
384
|
+
this.notifyMailboxChanged({ kind: "task", taskId });
|
|
385
|
+
}
|
|
386
|
+
notifyMailboxChanged(target) {
|
|
387
|
+
void callFileTaskController(this.home, "scheduler.signal", { key: controllerMailboxKey(target) }, this.clientOptions).catch(this.clientOptions.onError ?? (() => { }));
|
|
108
388
|
}
|
|
109
389
|
reconcileTask(taskId) {
|
|
110
390
|
void this.#prepareAndScan(taskId).catch(this.clientOptions.onError ?? (() => { }));
|
|
111
391
|
}
|
|
112
|
-
prepareTaskRoleEnter(input) {
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
392
|
+
async prepareTaskRoleEnter(input) {
|
|
393
|
+
const task = this.store.getTask(input.taskId);
|
|
394
|
+
if (task?.status === "active" && this.workspacePreparer !== undefined) {
|
|
395
|
+
await this.workspacePreparer.prepareTaskWorkspace(task.id);
|
|
396
|
+
}
|
|
397
|
+
const environment = foregroundRoleEnvironment(this.store, { scope: "task", taskId: input.taskId, roleName: input.roleName }, this.clientOptions.environment ?? process.env);
|
|
398
|
+
await callFileTaskController(this.home, "runtime.ensure-role-session", {
|
|
399
|
+
scope: "task",
|
|
119
400
|
taskId: input.taskId,
|
|
120
401
|
roleName: input.roleName,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
402
|
+
...(environment === undefined ? {} : { environment })
|
|
403
|
+
}, {
|
|
404
|
+
...this.clientOptions,
|
|
405
|
+
requestTimeoutMs: LIFECYCLE_REQUEST_TIMEOUT_MS
|
|
125
406
|
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
407
|
+
}
|
|
408
|
+
async stopTaskRoleSessions(taskId, roleNames) {
|
|
409
|
+
const targets = [];
|
|
410
|
+
for (const roleName of [...new Set(roleNames)]) {
|
|
411
|
+
if (this.store.getActiveAgentRun(taskId, roleName) !== null) {
|
|
412
|
+
throw new Error(`Role has an active Run: ${taskId}/${roleName}.`);
|
|
413
|
+
}
|
|
414
|
+
const target = this.schedulerStore.enqueueRuntimeCleanup({
|
|
415
|
+
scope: "task",
|
|
416
|
+
taskId,
|
|
417
|
+
roleName
|
|
134
418
|
});
|
|
419
|
+
if (target === null)
|
|
420
|
+
throw new Error(`Task not found: ${taskId}.`);
|
|
421
|
+
if (target.kind !== "role-runtime") {
|
|
422
|
+
throw new Error(`Role runtime cleanup target is invalid: ${taskId}/${roleName}.`);
|
|
423
|
+
}
|
|
424
|
+
targets.push(target);
|
|
425
|
+
}
|
|
426
|
+
if (targets.length === 0)
|
|
427
|
+
return;
|
|
428
|
+
await callFileTaskController(this.home, "scheduler.scan", {}, {
|
|
429
|
+
...this.clientOptions,
|
|
430
|
+
requestTimeoutMs: LIFECYCLE_REQUEST_TIMEOUT_MS
|
|
431
|
+
});
|
|
432
|
+
for (const target of targets) {
|
|
433
|
+
if (hasRuntimeLifecycleWork(this.store.getWorkMailbox(target))) {
|
|
434
|
+
throw new Error(`Role runtime did not stop: ${target.taskId}/${target.roleName}.`);
|
|
435
|
+
}
|
|
436
|
+
const session = this.store.getRoleSession(target.taskId, target.roleName);
|
|
437
|
+
if (session !== null && session.status !== "stopped") {
|
|
438
|
+
throw new Error(`Role runtime session is still active: ${target.taskId}/${target.roleName}.`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async stopGlobalRoleSession(roleName) {
|
|
443
|
+
if (this.store.getGlobalRole(roleName) === null) {
|
|
444
|
+
throw new Error(`Global Role not found: ${roleName}.`);
|
|
445
|
+
}
|
|
446
|
+
const target = this.schedulerStore.enqueueRuntimeCleanup({
|
|
447
|
+
scope: "global",
|
|
448
|
+
roleName
|
|
449
|
+
});
|
|
450
|
+
if (target?.kind !== "global-role-runtime") {
|
|
451
|
+
throw new Error(`Global Role runtime cleanup target is invalid: ${roleName}.`);
|
|
452
|
+
}
|
|
453
|
+
await callFileTaskController(this.home, "scheduler.scan", {}, {
|
|
454
|
+
...this.clientOptions,
|
|
455
|
+
requestTimeoutMs: LIFECYCLE_REQUEST_TIMEOUT_MS
|
|
456
|
+
});
|
|
457
|
+
if (hasRuntimeLifecycleWork(this.store.getWorkMailbox(target))) {
|
|
458
|
+
throw new Error(`Global Role runtime did not stop: ${roleName}.`);
|
|
459
|
+
}
|
|
460
|
+
const sessions = this.store.getGlobalRoleSessionSet(roleName);
|
|
461
|
+
const active = sessions?.sessions[sessions.activeAgentId];
|
|
462
|
+
if (active !== undefined && active.status !== "stopped") {
|
|
463
|
+
throw new Error(`Global Role runtime session is still active: ${roleName}.`);
|
|
135
464
|
}
|
|
136
465
|
}
|
|
137
466
|
inspectTaskRolePanes(taskId) {
|
|
138
467
|
return this.tmux.inspectTaskRolePanes(taskId);
|
|
139
468
|
}
|
|
140
|
-
prepareGlobalRoleEnter(roleName
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const sessionSet = this.store.getGlobalRoleSessionSet(roleName);
|
|
145
|
-
const session = sessionSet?.sessions[role.activeAgentId];
|
|
146
|
-
const binding = role.agentBindings[role.activeAgentId];
|
|
147
|
-
const mode = session?.nativeSessionId === undefined ? "new" : "resume";
|
|
148
|
-
const planned = this.planner.planGlobalRole({
|
|
469
|
+
async prepareGlobalRoleEnter(roleName) {
|
|
470
|
+
const environment = foregroundRoleEnvironment(this.store, { scope: "global", roleName }, this.clientOptions.environment ?? process.env);
|
|
471
|
+
await callFileTaskController(this.home, "runtime.ensure-role-session", {
|
|
472
|
+
scope: "global",
|
|
149
473
|
roleName,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
474
|
+
...(environment === undefined ? {} : { environment })
|
|
475
|
+
}, {
|
|
476
|
+
...this.clientOptions,
|
|
477
|
+
requestTimeoutMs: LIFECYCLE_REQUEST_TIMEOUT_MS
|
|
154
478
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
this.schedulerStore.recordGlobalRuntimeNativeSession({
|
|
158
|
-
roleName,
|
|
159
|
-
agentId: planned.session.agentId,
|
|
160
|
-
adapterId: planned.session.adapterId,
|
|
161
|
-
nativeSessionId: planned.session.nativeSessionId
|
|
162
|
-
});
|
|
479
|
+
if (roleName === "operator") {
|
|
480
|
+
await callFileTaskController(this.home, "scheduler.signal", { key: "operator" }, this.clientOptions);
|
|
163
481
|
}
|
|
164
482
|
}
|
|
165
483
|
async #prepareAndScan(taskId) {
|
|
166
484
|
const task = this.store.getTask(taskId);
|
|
167
|
-
if (task
|
|
168
|
-
&& task.status === "active"
|
|
485
|
+
if (task !== null
|
|
486
|
+
&& (task.status === "draft" || task.status === "active")
|
|
169
487
|
&& this.workspacePreparer !== undefined) {
|
|
170
488
|
await this.workspacePreparer.prepareTaskWorkspace(taskId);
|
|
171
489
|
}
|
|
172
490
|
await callFileTaskController(this.home, "scheduler.scan", {}, this.clientOptions);
|
|
173
491
|
}
|
|
174
492
|
}
|
|
493
|
+
const MANAGED_RUNTIME_ENVIRONMENT = new Set(YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES);
|
|
494
|
+
function foregroundRoleEnvironment(store, owner, source) {
|
|
495
|
+
const role = owner.scope === "task"
|
|
496
|
+
? store.getRole?.(owner.taskId, owner.roleName)
|
|
497
|
+
: store.getGlobalRole?.(owner.roleName);
|
|
498
|
+
if (role === null || role === undefined)
|
|
499
|
+
return undefined;
|
|
500
|
+
const agent = store.getConfiguredAgent?.(role.activeAgentId);
|
|
501
|
+
if (agent === null || agent === undefined)
|
|
502
|
+
return undefined;
|
|
503
|
+
const declaredSources = new Set(agent.environment.map((binding) => binding.sourceName));
|
|
504
|
+
for (const name of MANAGED_RUNTIME_ENVIRONMENT)
|
|
505
|
+
declaredSources.delete(name);
|
|
506
|
+
return {
|
|
507
|
+
...operationalAgentEnvironment(agent.adapterId, source),
|
|
508
|
+
...selectEnvironment(source, declaredSources)
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function controllerMailboxKey(target) {
|
|
512
|
+
switch (target.kind) {
|
|
513
|
+
case "operator": return "operator";
|
|
514
|
+
case "task": return `task:${encodeURIComponent(target.taskId)}`;
|
|
515
|
+
case "role": return `role:${encodeURIComponent(target.taskId)}/${encodeURIComponent(target.roleName)}`;
|
|
516
|
+
case "role-runtime":
|
|
517
|
+
return `role:${encodeURIComponent(target.taskId)}/${encodeURIComponent(target.roleName)}`;
|
|
518
|
+
case "global-role-runtime":
|
|
519
|
+
return `global-role:${encodeURIComponent(target.roleName)}`;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
175
522
|
function isUnavailable(error) {
|
|
176
523
|
if (typeof error !== "object" || error === null || !("code" in error))
|
|
177
524
|
return false;
|
|
178
525
|
const code = error.code;
|
|
179
|
-
return code === "
|
|
180
|
-
|| code === "
|
|
181
|
-
|
|
526
|
+
return code === "CONTROLLER_NOT_RUNNING"
|
|
527
|
+
|| code === "CONTROLLER_UNAVAILABLE";
|
|
528
|
+
}
|
|
529
|
+
function isDefinitelyNotRunning(error) {
|
|
530
|
+
if (typeof error !== "object" || error === null || !("code" in error))
|
|
531
|
+
return false;
|
|
532
|
+
const code = error.code;
|
|
533
|
+
return code === "CONTROLLER_NOT_RUNNING";
|
|
182
534
|
}
|
|
183
535
|
async function readOptionalControllerStatus(home, call) {
|
|
184
536
|
try {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { callController } from "../core/controllerClient.js";
|
|
2
|
+
import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
|
|
3
|
+
import { FileRuntimeEventInbox, MAX_RUNTIME_EVENT_FILE_BYTES } from "./runtimeEventInbox.js";
|
|
4
|
+
import { resolveProviderHookRunFence } from "./providerHookRunFence.js";
|
|
5
|
+
/**
|
|
6
|
+
* Hidden CLI entrypoint used by the managed Codex lifecycle hooks. Codex 0.145
|
|
7
|
+
* fires SessionStart and UserPromptSubmit inside run_turn(input); this parses
|
|
8
|
+
* by hook_event_name and writes one immutable runtime-inbox event per fact.
|
|
9
|
+
* SessionStart maps (via the adapter) to provider-session-started only — never
|
|
10
|
+
* pre-input readiness; UserPromptSubmit is the exact provider-accepted fence.
|
|
11
|
+
* The durable write is authoritative; the socket call is only a wake-up hint.
|
|
12
|
+
*/
|
|
13
|
+
export async function runCodexLifecycleHookCommand(stdinJson, environment = process.env, call = callController) {
|
|
14
|
+
const home = requireIdentity(environment.YUI_HOME, "YUI_HOME");
|
|
15
|
+
const inbox = new FileRuntimeEventInbox(home);
|
|
16
|
+
const envelope = parseCodexHookEnvelope(stdinJson, environment);
|
|
17
|
+
if (envelope.kind === "session-start") {
|
|
18
|
+
inbox.enqueueSessionLifecycle({
|
|
19
|
+
scope: "task",
|
|
20
|
+
taskId: envelope.taskId,
|
|
21
|
+
roleName: envelope.roleName,
|
|
22
|
+
agentId: envelope.agentId,
|
|
23
|
+
adapterId: "codex",
|
|
24
|
+
launchId: envelope.launchId,
|
|
25
|
+
nativeSessionId: envelope.nativeSessionId,
|
|
26
|
+
runId: envelope.runId
|
|
27
|
+
// Codex SessionStart arrives within the first turn, not before input.
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
inbox.enqueuePromptAccepted({
|
|
32
|
+
scope: "task",
|
|
33
|
+
taskId: envelope.taskId,
|
|
34
|
+
roleName: envelope.roleName,
|
|
35
|
+
agentId: envelope.agentId,
|
|
36
|
+
adapterId: "codex",
|
|
37
|
+
launchId: envelope.launchId,
|
|
38
|
+
nativeSessionId: envelope.nativeSessionId,
|
|
39
|
+
runId: envelope.runId,
|
|
40
|
+
receiptId: envelope.receiptId
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
await call(home, "scheduler.signal", {
|
|
44
|
+
key: runtimeLifecycleSignalKey({
|
|
45
|
+
scope: "task",
|
|
46
|
+
taskId: envelope.taskId,
|
|
47
|
+
roleName: envelope.roleName
|
|
48
|
+
})
|
|
49
|
+
}, { timeoutMs: 100 }).catch(() => { });
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Parses a Codex hook payload by hook_event_name into the exact fenced envelope.
|
|
53
|
+
* Every event requires the managed launch envelope, and the payload session id
|
|
54
|
+
* must match YUI_NATIVE_SESSION_ID, so a mismatched generation fails closed
|
|
55
|
+
* before any inbox write.
|
|
56
|
+
*/
|
|
57
|
+
export function parseCodexHookEnvelope(stdinJson, environment) {
|
|
58
|
+
const payload = parseObject(stdinJson);
|
|
59
|
+
if (payload.hook_event_name !== "SessionStart"
|
|
60
|
+
&& payload.hook_event_name !== "UserPromptSubmit") {
|
|
61
|
+
throw new Error("Managed Codex lifecycle ingestion received an unsupported hook event.");
|
|
62
|
+
}
|
|
63
|
+
const base = parseCodexEnvelope(payload, environment);
|
|
64
|
+
switch (payload.hook_event_name) {
|
|
65
|
+
case "SessionStart":
|
|
66
|
+
return { ...base, kind: "session-start" };
|
|
67
|
+
case "UserPromptSubmit":
|
|
68
|
+
return {
|
|
69
|
+
...base,
|
|
70
|
+
kind: "prompt-submit",
|
|
71
|
+
receiptId: requireIdentity(base.receiptId, "Codex transport receipt id")
|
|
72
|
+
};
|
|
73
|
+
default:
|
|
74
|
+
throw new Error("Managed Codex lifecycle ingestion received an unsupported hook event.");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function parseCodexEnvelope(payload, environment) {
|
|
78
|
+
const nativeSessionId = requireIdentity(payload.session_id, "Codex session id");
|
|
79
|
+
return resolveProviderHookRunFence(environment, "codex", nativeSessionId);
|
|
80
|
+
}
|
|
81
|
+
function parseObject(value) {
|
|
82
|
+
if (value === undefined
|
|
83
|
+
|| Buffer.byteLength(value, "utf8") > MAX_RUNTIME_EVENT_FILE_BYTES) {
|
|
84
|
+
throw new Error("Codex lifecycle hook stdin JSON is invalid.");
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(value);
|
|
88
|
+
if (!isObject(parsed))
|
|
89
|
+
throw new Error("shape");
|
|
90
|
+
return parsed;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
throw new Error("Codex lifecycle hook stdin JSON is invalid.");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function requireIdentity(value, label) {
|
|
97
|
+
if (typeof value !== "string" || value.includes("\0")) {
|
|
98
|
+
throw new Error(`${label} is required.`);
|
|
99
|
+
}
|
|
100
|
+
const normalized = value.trim();
|
|
101
|
+
if (normalized.length === 0 || normalized.length > 1_024) {
|
|
102
|
+
throw new Error(`${label} is invalid.`);
|
|
103
|
+
}
|
|
104
|
+
return normalized;
|
|
105
|
+
}
|
|
106
|
+
function isObject(value) {
|
|
107
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
108
|
+
}
|