@zq-silk/yui 1.0.1 → 1.1.0
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/README.md +27 -9
- package/dist/cli/commandCatalog.js +7 -5
- package/dist/cli/invocationAuthority.js +4 -0
- package/dist/cli.js +130 -5
- package/dist/commands/releaseCommands.js +33 -24
- package/dist/commands/taskCommands.js +16 -25
- package/dist/commands/taskFactCommands.js +6 -2
- package/dist/controller/clientRuntime.js +7 -2
- package/dist/controller/domainIdentity.js +7 -7
- package/dist/controller/handoverCandidate.js +2 -5
- package/dist/controller/jobSupervisor.js +4 -4
- package/dist/controller/resourceCleanupLinux.js +35 -19
- package/dist/controller/resourceInventoryLinux.js +104 -7
- package/dist/controller/runtime.js +4 -13
- package/dist/controller/runtimeLaunchCoordinator.js +2 -4
- package/dist/controller/sessionOwnerReconciliation.js +40 -16
- package/dist/core/controllerClient.js +44 -16
- package/dist/core/controllerEndpoint.js +4 -3
- package/dist/core/controllerProcessIdentity.js +54 -0
- package/dist/core/controllerServer.js +16 -11
- package/dist/core/fileLockOwner.js +53 -12
- package/dist/doctor/doctor.js +230 -12
- package/dist/execution/workItemMainRun.js +1 -1
- package/dist/executor/agentAdapter.js +0 -8
- package/dist/executor/agentExecutor.js +4 -1
- package/dist/executor/fileRoleLaunchPlanner.js +3 -22
- package/dist/message/messageContinuation.js +1 -1
- package/dist/release/releaseHandover.js +77 -25
- package/dist/release/runtimeRelease.js +7 -7
- package/dist/repository/gitWorkspace.js +9 -8
- package/dist/repository/taskWorkspacePreparer.js +15 -57
- package/dist/resources/liveReferences.js +64 -7
- package/dist/runtime/agentEndpointIdentity.js +4 -1
- package/dist/runtime/agentHost.js +8 -9
- package/dist/runtime/codexInteractiveHost.js +1 -1
- package/dist/runtime/codexSharedDaemon.js +211 -0
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/jsonLineChannel.js +6 -1
- package/dist/runtime/localStartup.js +65 -0
- package/dist/runtime/native/darwin-arm64/claude-process-owner +0 -0
- package/dist/runtime/native/darwin-arm64/process-identity +0 -0
- package/dist/runtime/native/darwin-x64/claude-process-owner +0 -0
- package/dist/runtime/native/darwin-x64/process-identity +0 -0
- package/dist/runtime/{claude-process-owner → native/linux-x64/claude-process-owner} +0 -0
- package/dist/runtime/nativeExecutable.js +10 -0
- package/dist/runtime/runtimeCoherence.js +10 -3
- package/dist/runtime/sessionOwnerIdentity.js +19 -0
- package/dist/runtime/sessionTerminationGuard.js +10 -3
- package/dist/runtime/structuredProviderHost.js +51 -26
- package/dist/scheduler/activeRoleRunDelivery.js +2 -11
- package/dist/scheduler/leaderWakeupProcessor.js +1 -2
- package/dist/scheduler/ports.js +1 -8
- package/dist/scheduler/taskExecutionProjection.js +1 -6
- package/dist/storage/sqliteSchema.js +15 -3
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/taskCatalog.js +10 -2
- package/dist/storage/upgrade/taskMainWorkspaceMigration.js +83 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +25 -6
- package/dist/task/task.js +0 -12
- package/dist/tmux/tmuxManager.js +8 -2
- package/dist/tmux/tmuxSocketEndpoint.js +23 -1
- package/dist/web/assets/client/app.js +131 -11
- package/dist/web/assets/client/i18n.js +18 -0
- package/dist/web/assets/client/taskSummary.js +7 -2
- package/dist/web/assets/client/taskSurface.js +54 -5
- package/dist/web/assets/client/view.js +7 -2
- package/dist/web/assets/shell.js +21 -11
- package/dist/web/assets/styles/layout.js +33 -6
- package/dist/web/assets/styles/responsive.js +18 -5
- package/dist/web/assets/styles/tokens.js +1 -1
- package/dist/web/assets/styles/widgets.js +5 -0
- package/docs/provider-runtime.md +7 -0
- package/docs/provider-runtime.zh-CN.md +5 -0
- package/docs/release-workflow.md +7 -1
- package/docs/task-delivery.md +5 -4
- package/docs/testing/verification-levels.md +9 -7
- package/i18n/README.zh-CN.md +19 -6
- package/package.json +5 -6
- package/skills/yui-leader/SKILL.md +8 -0
- package/skills/yui-leader/references/execution.md +4 -1
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { createConnection } from "node:net";
|
|
5
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const DAEMON_START_TIMEOUT_MS = 15_000;
|
|
9
|
+
const CODEX_APP_BUNDLE_ID = "com.openai.codex";
|
|
10
|
+
/** A denied probe says nothing about whether the shared daemon is running. */
|
|
11
|
+
export function isCodexInspectionPermissionError(error) {
|
|
12
|
+
if (typeof error === "string") {
|
|
13
|
+
return /\b(?:EPERM|EACCES)\b|operation not permitted|permission denied/iu.test(error);
|
|
14
|
+
}
|
|
15
|
+
let current = error;
|
|
16
|
+
for (let depth = 0; depth < 4 && current !== null && typeof current === "object"; depth += 1) {
|
|
17
|
+
const candidate = current;
|
|
18
|
+
if (candidate.code === "EPERM" || candidate.code === "EACCES"
|
|
19
|
+
|| [candidate.message, candidate.stderr].some((value) => typeof value === "string"
|
|
20
|
+
&& /\b(?:EPERM|EACCES)\b|operation not permitted|permission denied/iu.test(value)))
|
|
21
|
+
return true;
|
|
22
|
+
current = candidate.cause;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
/** Locate the installed App by bundle identity, including non-default installs. */
|
|
27
|
+
export function findMacCodexAppExecutable(environment = process.env) {
|
|
28
|
+
if (process.platform !== "darwin")
|
|
29
|
+
return undefined;
|
|
30
|
+
const home = environment.HOME || homedir();
|
|
31
|
+
const candidates = [
|
|
32
|
+
"/Applications/ChatGPT.app", "/Applications/Codex.app",
|
|
33
|
+
join(home, "Applications/ChatGPT.app"), join(home, "Applications/Codex.app")
|
|
34
|
+
];
|
|
35
|
+
for (const candidate of candidates) {
|
|
36
|
+
const executable = executableForCodexApp(candidate);
|
|
37
|
+
if (executable !== undefined)
|
|
38
|
+
return executable;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const registered = execFileSync("/usr/bin/osascript", [
|
|
42
|
+
"-e", `POSIX path of (path to application id "${CODEX_APP_BUNDLE_ID}")`
|
|
43
|
+
], { encoding: "utf8", timeout: 10_000 }).trim();
|
|
44
|
+
return executableForCodexApp(registered);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function executableForCodexApp(bundlePath) {
|
|
51
|
+
const plist = join(bundlePath, "Contents/Info.plist");
|
|
52
|
+
if (!existsSync(plist))
|
|
53
|
+
return undefined;
|
|
54
|
+
try {
|
|
55
|
+
const identifier = execFileSync("/usr/libexec/PlistBuddy", [
|
|
56
|
+
"-c", "Print :CFBundleIdentifier", plist
|
|
57
|
+
], { encoding: "utf8", timeout: 2_000 }).trim();
|
|
58
|
+
if (identifier !== CODEX_APP_BUNDLE_ID)
|
|
59
|
+
return undefined;
|
|
60
|
+
const name = execFileSync("/usr/libexec/PlistBuddy", [
|
|
61
|
+
"-c", "Print :CFBundleExecutable", plist
|
|
62
|
+
], { encoding: "utf8", timeout: 2_000 }).trim();
|
|
63
|
+
const executable = join(bundlePath, "Contents/MacOS", name);
|
|
64
|
+
return existsSync(executable) ? executable : undefined;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Prepare future App launches when an interactive Yui Codex Session starts. */
|
|
71
|
+
export async function prepareMacCodexAppConnection(launch, appExecutable = findMacCodexAppExecutable(launch.environment), daemonStartedEarlier = false) {
|
|
72
|
+
if (process.platform !== "darwin")
|
|
73
|
+
return { state: "not-macos" };
|
|
74
|
+
if (launch.args.at(-2) !== "app-server" || launch.args.at(-1) !== "proxy") {
|
|
75
|
+
throw new Error("Codex App sharing requires the configured app-server proxy connection.");
|
|
76
|
+
}
|
|
77
|
+
const socketPath = codexSharedSocketPath(launch.environment, launch.cwd);
|
|
78
|
+
const started = await ensureCodexSharedDaemon(launch) || daemonStartedEarlier;
|
|
79
|
+
if (appExecutable === undefined)
|
|
80
|
+
return { state: "ready", socketPath };
|
|
81
|
+
const wsUrl = codexAppWebSocketUrl(launch.environment, launch.cwd);
|
|
82
|
+
try {
|
|
83
|
+
await execFileAsync("/bin/launchctl", ["setenv", "CODEX_APP_SERVER_WS_URL", wsUrl], {
|
|
84
|
+
cwd: launch.cwd, env: launch.environment, timeout: 5_000
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
throw new Error(`Could not configure future Codex App launches: ${String(error)}`, { cause: error });
|
|
89
|
+
}
|
|
90
|
+
let stdout;
|
|
91
|
+
try {
|
|
92
|
+
({ stdout } = await execFileAsync("/bin/ps", ["-axo", "pid=,args="], {
|
|
93
|
+
cwd: launch.cwd, env: launch.environment, timeout: 5_000, maxBuffer: 4 * 1024 * 1024
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
throw new Error(`Could not inspect running Codex App processes: ${String(error)}`, { cause: error });
|
|
98
|
+
}
|
|
99
|
+
const pids = codexAppProcessIds(stdout, appExecutable);
|
|
100
|
+
for (const pid of pids) {
|
|
101
|
+
let appCommand;
|
|
102
|
+
try {
|
|
103
|
+
const app = await execFileAsync("/bin/ps", ["eww", "-p", pid, "-o", "command="], {
|
|
104
|
+
cwd: launch.cwd, env: launch.environment, timeout: 5_000, maxBuffer: 4 * 1024 * 1024
|
|
105
|
+
});
|
|
106
|
+
appCommand = String(app.stdout);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
// The App can exit between listing processes and reading its environment.
|
|
110
|
+
try {
|
|
111
|
+
process.kill(Number(pid), 0);
|
|
112
|
+
}
|
|
113
|
+
catch (probeError) {
|
|
114
|
+
if (probeError.code === "ESRCH")
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
throw new Error(`Could not inspect Codex App pid=${pid}: ${String(error)}`, { cause: error });
|
|
118
|
+
}
|
|
119
|
+
if (codexAppNeedsRestart(appCommand, wsUrl, started)) {
|
|
120
|
+
return { state: "restart-required", socketPath };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { state: "ready", socketPath };
|
|
124
|
+
}
|
|
125
|
+
export function codexAppProcessIds(processTable, appExecutable = "/Applications/ChatGPT.app/Contents/MacOS/ChatGPT") {
|
|
126
|
+
return processTable.split("\n").flatMap((line) => {
|
|
127
|
+
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
128
|
+
return match !== null
|
|
129
|
+
&& (match[2] === appExecutable || match[2].startsWith(`${appExecutable} `))
|
|
130
|
+
? [match[1]] : [];
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
export function codexAppWebSocketUrl(environment, cwd) {
|
|
134
|
+
return `ws+unix://localhost${codexSharedSocketPath(environment, cwd)}:/rpc`;
|
|
135
|
+
}
|
|
136
|
+
export function codexAppNeedsRestart(runningAppCommand, wsUrl, daemonStarted) {
|
|
137
|
+
return daemonStarted || !runningAppCommand.split(/\s+/).includes(`CODEX_APP_SERVER_WS_URL=${wsUrl}`);
|
|
138
|
+
}
|
|
139
|
+
/** Only a failed connection to the default shared socket can start the local daemon. */
|
|
140
|
+
export async function startMissingCodexSharedDaemon(launch) {
|
|
141
|
+
if (launch.args.at(-2) !== "app-server" || launch.args.at(-1) !== "proxy")
|
|
142
|
+
return false;
|
|
143
|
+
const socketPath = codexSharedSocketPath(launch.environment, launch.cwd);
|
|
144
|
+
if (!await socketIsMissing(socketPath))
|
|
145
|
+
return false;
|
|
146
|
+
try {
|
|
147
|
+
await execFileAsync(launch.command, [
|
|
148
|
+
...daemonBaseArgs(launch), "app-server", "daemon", "start"
|
|
149
|
+
], {
|
|
150
|
+
cwd: launch.cwd,
|
|
151
|
+
env: launch.environment,
|
|
152
|
+
timeout: DAEMON_START_TIMEOUT_MS,
|
|
153
|
+
maxBuffer: 32 * 1024
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
158
|
+
throw new Error(`Codex shared app-server is unavailable at ${socketPath}; daemon start failed: ${detail}`, {
|
|
159
|
+
cause: error
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
export async function ensureCodexSharedDaemon(launch) {
|
|
165
|
+
const started = await startMissingCodexSharedDaemon(launch);
|
|
166
|
+
const socketPath = codexSharedSocketPath(launch.environment, launch.cwd);
|
|
167
|
+
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
168
|
+
if (!await socketIsMissing(socketPath))
|
|
169
|
+
return started;
|
|
170
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 250));
|
|
171
|
+
}
|
|
172
|
+
throw new Error(`Codex shared app-server did not open ${socketPath}.`);
|
|
173
|
+
}
|
|
174
|
+
export function codexSharedSocketPath(environment, cwd) {
|
|
175
|
+
const accountHome = environment.CODEX_HOME?.trim()
|
|
176
|
+
|| join(environment.HOME || homedir(), ".codex");
|
|
177
|
+
const codexHome = isAbsolute(accountHome) ? accountHome : resolve(cwd, accountHome);
|
|
178
|
+
return join(codexHome, "app-server-control", "app-server-control.sock");
|
|
179
|
+
}
|
|
180
|
+
function daemonBaseArgs(launch) {
|
|
181
|
+
let parsed;
|
|
182
|
+
try {
|
|
183
|
+
parsed = JSON.parse(launch.environment.YUI_AGENT_BASE_ARGS ?? "");
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
throw new Error("Codex daemon launch is missing the configured Agent base arguments.");
|
|
187
|
+
}
|
|
188
|
+
if (!Array.isArray(parsed) || !parsed.every((arg) => typeof arg === "string")) {
|
|
189
|
+
throw new Error("Codex daemon launch has invalid configured Agent base arguments.");
|
|
190
|
+
}
|
|
191
|
+
return parsed;
|
|
192
|
+
}
|
|
193
|
+
function socketIsMissing(path) {
|
|
194
|
+
return new Promise((resolvePromise, reject) => {
|
|
195
|
+
const socket = createConnection(path);
|
|
196
|
+
socket.setTimeout(1_000, () => socket.destroy(new Error("Codex shared app-server socket probe timed out.")));
|
|
197
|
+
socket.once("connect", () => {
|
|
198
|
+
socket.destroy();
|
|
199
|
+
resolvePromise(false);
|
|
200
|
+
});
|
|
201
|
+
socket.once("error", (error) => {
|
|
202
|
+
if (error.code === "ENOENT" || error.code === "ECONNREFUSED")
|
|
203
|
+
resolvePromise(true);
|
|
204
|
+
else if (isCodexInspectionPermissionError(error)) {
|
|
205
|
+
reject(new Error(`Cannot verify Codex shared app-server socket at ${path}: access denied; daemon state is unverified. ${error.message}`, { cause: error }));
|
|
206
|
+
}
|
|
207
|
+
else
|
|
208
|
+
reject(error);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
}
|
package/dist/runtime/index.js
CHANGED
|
@@ -8,7 +8,7 @@ export { DEFAULT_RECENT_TURN_ID_LIMIT, hasRecentTurnId, rememberRecentTurnId, va
|
|
|
8
8
|
export { promptPushOutcome, RuntimeHostContentionError, RuntimeHostUnavailableError, RuntimeLaunchError } from "./ports.js";
|
|
9
9
|
export { AgentHostPromptPushAdapter, TmuxSessionHost } from "./tmuxAdapters.js";
|
|
10
10
|
export { FileTaskRuntimeIsolation, YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR, YUI_TASK_RUNTIME_SERVICE_NAMESPACE, assertTaskRuntimeIsolationPreflight, createTaskRuntimeIsolationDescriptor, parseTaskRuntimeIsolationDescriptor, planTaskRuntimeCleanup, taskRuntimeIsolationEnvironment, taskRuntimeIsolationFingerprint } from "./taskRuntimeIsolation.js";
|
|
11
|
-
export { createSessionOwnerIdentity, isLinuxProcessLive, listOwnedProcessTree, readLinuxProcessIdentity } from "./sessionOwnerIdentity.js";
|
|
11
|
+
export { createSessionOwnerIdentity, isLinuxProcessLive, listOwnedProcessTree, readLinuxProcessIdentity, readProcessIdentity } from "./sessionOwnerIdentity.js";
|
|
12
12
|
export { formatRuntimeLaunchDiagnostic, redactLaunchArgument, redactLaunchText, RuntimeLaunchFailure, toRuntimeLaunchFailure } from "./launchDiagnostics.js";
|
|
13
13
|
export { DEFAULT_FORCED_GRACE_MS, DEFAULT_GRACEFUL_GRACE_MS, terminateSessionOwners } from "./sessionTerminationGuard.js";
|
|
14
14
|
export { ProviderContinuationReconciliationService } from "./providerContinuationReconciliationService.js";
|
|
@@ -130,8 +130,13 @@ export function terminateProcessGroup(child, signal) {
|
|
|
130
130
|
process.kill(-child.pid, signal);
|
|
131
131
|
}
|
|
132
132
|
catch (error) {
|
|
133
|
-
|
|
133
|
+
const code = error.code;
|
|
134
|
+
if (code !== "ESRCH" && code !== "EPERM")
|
|
134
135
|
throw error;
|
|
136
|
+
// ESRCH: the group is already gone. EPERM: the group ID was reused by or
|
|
137
|
+
// inherited a process owned by another user (observed on macOS after the
|
|
138
|
+
// fixture child exits); the direct child is still ours to terminate.
|
|
139
|
+
// Node's ChildProcess.kill treats an already-exited child as a no-op.
|
|
135
140
|
child.kill(signal);
|
|
136
141
|
}
|
|
137
142
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/agent.js";
|
|
2
|
+
import { codexSharedSocketPath, ensureCodexSharedDaemon, findMacCodexAppExecutable, prepareMacCodexAppConnection } from "./codexSharedDaemon.js";
|
|
3
|
+
/** Current Role demand, not every CLI installed on the machine or stale Agent record. */
|
|
4
|
+
export function codexAgentsInUse(store, targetAgentId) {
|
|
5
|
+
const ids = new Set();
|
|
6
|
+
if (targetAgentId !== undefined) {
|
|
7
|
+
ids.add(targetAgentId);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
for (const role of store.listGlobalRoles())
|
|
11
|
+
ids.add(role.activeAgentId);
|
|
12
|
+
for (const taskId of store.listActiveTaskIds()) {
|
|
13
|
+
for (const role of store.listRoles(taskId))
|
|
14
|
+
ids.add(role.activeAgentId);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return [...ids].flatMap((id) => {
|
|
18
|
+
const agent = store.getConfiguredAgent(id);
|
|
19
|
+
return agent?.adapterId === "codex" ? [agent] : [];
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function prepareLocalCodexStartup(store, home, environment, targetAgentId, platform = process.platform) {
|
|
23
|
+
const agents = codexAgentsInUse(store, targetAgentId);
|
|
24
|
+
if (agents.length === 0)
|
|
25
|
+
return { daemons: [], app: "not-applicable" };
|
|
26
|
+
const groups = new Map();
|
|
27
|
+
for (const configured of agents) {
|
|
28
|
+
const agent = configuredAgentToDefinition(configured);
|
|
29
|
+
const launch = {
|
|
30
|
+
command: agent.command,
|
|
31
|
+
args: [...agent.baseArgs, "app-server", "proxy"],
|
|
32
|
+
cwd: home,
|
|
33
|
+
environment: {
|
|
34
|
+
...environment,
|
|
35
|
+
...resolveAgentEnvironment(agent, environment),
|
|
36
|
+
YUI_AGENT_BASE_ARGS: JSON.stringify(agent.baseArgs)
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const socketPath = codexSharedSocketPath(launch.environment, home);
|
|
40
|
+
const existing = groups.get(socketPath);
|
|
41
|
+
if (existing === undefined)
|
|
42
|
+
groups.set(socketPath, { launch, agentIds: [agent.id] });
|
|
43
|
+
else
|
|
44
|
+
existing.agentIds.push(agent.id);
|
|
45
|
+
}
|
|
46
|
+
const daemons = [];
|
|
47
|
+
for (const [socketPath, group] of groups) {
|
|
48
|
+
const started = await ensureCodexSharedDaemon(group.launch);
|
|
49
|
+
daemons.push({ agentIds: group.agentIds, socketPath, started });
|
|
50
|
+
}
|
|
51
|
+
if (platform !== "darwin")
|
|
52
|
+
return { daemons, app: "not-applicable" };
|
|
53
|
+
const appExecutable = findMacCodexAppExecutable(environment);
|
|
54
|
+
if (appExecutable === undefined)
|
|
55
|
+
return { daemons, app: "not-detected" };
|
|
56
|
+
const operatorAgentId = store.getGlobalRole("operator")?.activeAgentId;
|
|
57
|
+
const selected = [...groups.values()].find((group) => group.agentIds.includes(operatorAgentId ?? ""))
|
|
58
|
+
?? (groups.size === 1 ? [...groups.values()][0] : undefined);
|
|
59
|
+
if (selected === undefined)
|
|
60
|
+
return { daemons, app: "multiple-homes" };
|
|
61
|
+
const selectedSocketPath = codexSharedSocketPath(selected.launch.environment, home);
|
|
62
|
+
const daemonStartedEarlier = daemons.find((daemon) => daemon.socketPath === selectedSocketPath)?.started ?? false;
|
|
63
|
+
const prepared = await prepareMacCodexAppConnection(selected.launch, appExecutable, daemonStartedEarlier);
|
|
64
|
+
return { daemons, app: prepared.state };
|
|
65
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
/** Native binaries are shipped together; never execute another platform's helper. */
|
|
3
|
+
export function nativeExecutable(name, platform = process.platform, architecture = process.arch) {
|
|
4
|
+
const target = `${platform}-${architecture}`;
|
|
5
|
+
if (!["linux-x64", "darwin-x64", "darwin-arm64"].includes(target)
|
|
6
|
+
|| (name === "process-identity" && platform !== "darwin")) {
|
|
7
|
+
throw new Error(`Unsupported native runtime: ${target}/${name}.`);
|
|
8
|
+
}
|
|
9
|
+
return fileURLToPath(new URL(`./native/${target}/${name}`, import.meta.url));
|
|
10
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { isStorageVersion, compareStorageVersions } from "../storage/storageVersions.js";
|
|
3
|
-
import { callController as defaultCallController } from "../core/controllerClient.js";
|
|
3
|
+
import { callController as defaultCallController, ControllerClientError } from "../core/controllerClient.js";
|
|
4
4
|
import { inspectStorageSchema } from "../storage/storageSchema.js";
|
|
5
5
|
import { yuiVersionIdentity } from "../version.js";
|
|
6
6
|
export async function assertRuntimeCoherence(input, options = {}) {
|
|
@@ -22,8 +22,15 @@ export async function assertRuntimeCoherence(input, options = {}) {
|
|
|
22
22
|
assertControllerContinuityIdentity(status, identity);
|
|
23
23
|
}
|
|
24
24
|
catch (error) {
|
|
25
|
-
if (
|
|
26
|
-
|
|
25
|
+
if (isDefinitelyNotRunning(error))
|
|
26
|
+
return identity;
|
|
27
|
+
if (error instanceof ControllerClientError
|
|
28
|
+
&& ["CONTROLLER_UNAVAILABLE", "CONTROLLER_TIMEOUT", "CONTROLLER_DISCOVERY_INVALID", "INVALID_RESPONSE"].includes(error.code)) {
|
|
29
|
+
throw new Error(`Managed Controller could not be verified before this command: ${error.message} `
|
|
30
|
+
+ "Run `yui doctor` with this Session's YUI_HOME and CLI; ask the Operator or user "
|
|
31
|
+
+ "to restore it with `yui start` or `yui controller restart` as diagnosed.", { cause: error });
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
27
34
|
}
|
|
28
35
|
}
|
|
29
36
|
return identity;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { readProcessStartIdentity } from "../core/fileLockOwner.js";
|
|
2
3
|
import { requireSafeIdentity, requireText, requireTimestamp } from "./validation.js";
|
|
3
4
|
/** A concrete OS process identity, not an Agent launch identity. */
|
|
4
5
|
export function sessionOwnerProcessKey(record) {
|
|
@@ -109,6 +110,24 @@ export function readLinuxProcessIdentity(pid) {
|
|
|
109
110
|
return undefined;
|
|
110
111
|
}
|
|
111
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Identity of a process for durable ownership records. Linux returns the
|
|
115
|
+
* exact /proc start-time identity; macOS uses libproc's process start time.
|
|
116
|
+
*/
|
|
117
|
+
export function readProcessIdentity(pid = process.pid) {
|
|
118
|
+
if (process.platform === "linux") {
|
|
119
|
+
const identity = readLinuxProcessIdentity(pid);
|
|
120
|
+
if (identity === undefined) {
|
|
121
|
+
throw new Error(`Cannot read Linux process identity for PID ${pid}.`);
|
|
122
|
+
}
|
|
123
|
+
return identity;
|
|
124
|
+
}
|
|
125
|
+
const startIdentity = readProcessStartIdentity(pid);
|
|
126
|
+
if (startIdentity === undefined) {
|
|
127
|
+
throw new Error(`Dedicated Provider process identity could not be verified for PID ${pid}.`);
|
|
128
|
+
}
|
|
129
|
+
return { pid, startIdentity, rssBytes: 0 };
|
|
130
|
+
}
|
|
112
131
|
function readLinuxProcessRssBytes(pid) {
|
|
113
132
|
try {
|
|
114
133
|
const status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
@@ -30,11 +30,16 @@ export async function terminateSessionOwners(owner, records, ports, options = {}
|
|
|
30
30
|
if (records.length === 0) {
|
|
31
31
|
// Without a recorded root only the exact Role pane can be stopped.
|
|
32
32
|
const stopped = await ports.gracefulStop(owner);
|
|
33
|
-
const outcome = stopped
|
|
33
|
+
const outcome = stopped && options.requireProcessRecord !== true
|
|
34
|
+
? "stop-confirmed" : "stop-blocked";
|
|
34
35
|
emit(outcome);
|
|
35
36
|
return {
|
|
36
37
|
outcome, owner, confirmed: [], remaining: [],
|
|
37
|
-
...(
|
|
38
|
+
...(outcome === "stop-confirmed" ? {} : {
|
|
39
|
+
verificationGap: stopped
|
|
40
|
+
? "No recorded Host process identity; pane exit cannot prove physical exit."
|
|
41
|
+
: "Role pane stop is unconfirmed."
|
|
42
|
+
})
|
|
38
43
|
};
|
|
39
44
|
}
|
|
40
45
|
const pollMs = duration(options.pollMs, DEFAULT_TERMINATION_POLL_MS);
|
|
@@ -60,7 +65,9 @@ export async function terminateSessionOwners(owner, records, ports, options = {}
|
|
|
60
65
|
}
|
|
61
66
|
emit(signal === "SIGTERM" ? "graceful-stop" : "forced-stop");
|
|
62
67
|
const deadline = Date.now() + grace;
|
|
63
|
-
|
|
68
|
+
// A process can be briefly unreadable while exiting. Wait for absence,
|
|
69
|
+
// not merely for the first unverified observation.
|
|
70
|
+
while (records.some((record) => state(record) !== "absent") && Date.now() < deadline) {
|
|
64
71
|
await ports.sleep(Math.min(pollMs, Math.max(1, deadline - Date.now())));
|
|
65
72
|
}
|
|
66
73
|
if (records.every((record) => state(record) === "absent"))
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { Duplex } from "node:stream";
|
|
4
|
-
import {
|
|
4
|
+
import { nativeExecutable } from "./nativeExecutable.js";
|
|
5
|
+
import { startMissingCodexSharedDaemon } from "./codexSharedDaemon.js";
|
|
5
6
|
import { ProviderConversationMissingError, ProviderDeliveryUnknownError, ProviderTurnBusyError, ProviderTurnRejectedError } from "./providerErrors.js";
|
|
6
7
|
import WebSocket from "ws";
|
|
7
8
|
import { YUI_VERSION } from "../version.js";
|
|
@@ -24,15 +25,32 @@ export async function startStructuredProviderSession(payload, input = {}) {
|
|
|
24
25
|
if (control === undefined) {
|
|
25
26
|
throw new Error("Managed Agent Host launch requires Provider control metadata.");
|
|
26
27
|
}
|
|
28
|
+
const mirror = input.mirrorOutput ?? defaultMirrorOutput;
|
|
29
|
+
if (control.adapterId === "codex" && control.transport === "codex-app-server-proxy") {
|
|
30
|
+
const proxy = await connectCodexProxy(payload, mirror, true);
|
|
31
|
+
try {
|
|
32
|
+
const opened = await CodexStructuredProviderSession.open(proxy.child, proxy.exit, proxy.processInstanceId, payload, control, proxy.channel, input.onStarted, input.onTerminal, input.onGoal, input.onInput, input.onActivity, input.onDiagnostic, mirror);
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
session: opened.session,
|
|
35
|
+
...(opened.recoveredTerminal === undefined
|
|
36
|
+
? {}
|
|
37
|
+
: { recoveredTerminal: opened.recoveredTerminal }),
|
|
38
|
+
goal: opened.goal
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
terminateProcessGroup(proxy.child, "SIGTERM");
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
27
46
|
const ownedClaude = control.adapterId === "claude" && control.transport !== "acp-stdio";
|
|
28
|
-
const child = spawn(ownedClaude ?
|
|
47
|
+
const child = spawn(ownedClaude ? nativeExecutable("claude-process-owner") : payload.command, ownedClaude ? [payload.command, ...payload.args] : [...payload.args], {
|
|
29
48
|
cwd: payload.cwd,
|
|
30
49
|
env: { ...payload.environment },
|
|
31
50
|
stdio: ["pipe", "pipe", "pipe"],
|
|
32
51
|
detached: true
|
|
33
52
|
});
|
|
34
53
|
const processInstanceId = randomUUID();
|
|
35
|
-
const mirror = input.mirrorOutput ?? defaultMirrorOutput;
|
|
36
54
|
child.stderr.setEncoding("utf8");
|
|
37
55
|
child.stderr.on("data", (chunk) => mirror("stderr", chunk));
|
|
38
56
|
const exit = childExit(child, processInstanceId);
|
|
@@ -81,16 +99,6 @@ export async function startStructuredProviderSession(payload, input = {}) {
|
|
|
81
99
|
}
|
|
82
100
|
return Object.freeze({ session });
|
|
83
101
|
}
|
|
84
|
-
if (control.adapterId === "codex") {
|
|
85
|
-
const opened = await CodexStructuredProviderSession.open(child, exit, processInstanceId, payload, control, input.onStarted, input.onTerminal, input.onGoal, input.onInput, input.onActivity, input.onDiagnostic, mirror);
|
|
86
|
-
return Object.freeze({
|
|
87
|
-
session: opened.session,
|
|
88
|
-
...(opened.recoveredTerminal === undefined
|
|
89
|
-
? {}
|
|
90
|
-
: { recoveredTerminal: opened.recoveredTerminal }),
|
|
91
|
-
goal: opened.goal
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
102
|
const session = await ClaudeStructuredProviderSession.open(child, exit, processInstanceId, control, input.onAccepted, input.onTerminal, input.onGoal, input.onActivity, input.onDiagnostic, mirror);
|
|
95
103
|
if (isPostCreateOperatorTitle(payload)) {
|
|
96
104
|
input.onDiagnostic?.({
|
|
@@ -120,20 +128,38 @@ function isPostCreateOperatorTitle(payload) {
|
|
|
120
128
|
* Open an uninitialized, transparent connection for a native TUI. The TUI
|
|
121
129
|
* owns its requests; Yui observes only its exact startup response.
|
|
122
130
|
*/
|
|
123
|
-
export async function openCodexInteractiveConnection(launch) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
export async function openCodexInteractiveConnection(launch, startMissingDaemon = false) {
|
|
132
|
+
return (await connectCodexProxy(launch, () => { }, startMissingDaemon)).channel;
|
|
133
|
+
}
|
|
134
|
+
async function connectCodexProxy(launch, mirror, startMissingDaemon) {
|
|
135
|
+
const attempt = async () => {
|
|
136
|
+
const child = spawn(launch.command, [...launch.args], {
|
|
137
|
+
cwd: launch.cwd,
|
|
138
|
+
env: { ...launch.environment },
|
|
139
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
140
|
+
detached: true
|
|
141
|
+
});
|
|
142
|
+
const processInstanceId = randomUUID();
|
|
143
|
+
const exit = childExit(child, processInstanceId);
|
|
144
|
+
void exit.catch(() => { });
|
|
145
|
+
child.stderr.setEncoding("utf8");
|
|
146
|
+
child.stderr.on("data", (chunk) => mirror("stderr", chunk));
|
|
147
|
+
try {
|
|
148
|
+
const channel = await CodexProxyWebSocketChannel.connect(child, mirror);
|
|
149
|
+
return { child, channel, exit, processInstanceId };
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
terminateProcessGroup(child, "SIGTERM");
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
131
156
|
try {
|
|
132
|
-
return await
|
|
157
|
+
return await attempt();
|
|
133
158
|
}
|
|
134
159
|
catch (error) {
|
|
135
|
-
|
|
136
|
-
|
|
160
|
+
if (!startMissingDaemon || !await startMissingCodexSharedDaemon(launch))
|
|
161
|
+
throw error;
|
|
162
|
+
return await attempt();
|
|
137
163
|
}
|
|
138
164
|
}
|
|
139
165
|
class CodexProxyWebSocketChannel {
|
|
@@ -379,8 +405,7 @@ class CodexStructuredProviderSession {
|
|
|
379
405
|
this.mirror = mirror;
|
|
380
406
|
this.nativeAccountHome = nativeAccountHome;
|
|
381
407
|
}
|
|
382
|
-
static async open(child, exit, processInstanceId, payload, control, onStarted, onTerminal, onGoal, onInput, onActivity, onDiagnostic, mirror) {
|
|
383
|
-
const channel = await CodexProxyWebSocketChannel.connect(child, mirror);
|
|
408
|
+
static async open(child, exit, processInstanceId, payload, control, channel, onStarted, onTerminal, onGoal, onInput, onActivity, onDiagnostic, mirror) {
|
|
384
409
|
const openingMessages = [];
|
|
385
410
|
const stopOpeningBuffer = channel.onMessage((message) => openingMessages.push(message));
|
|
386
411
|
const initialized = await channel.request("initialize", codexClientInitialization());
|
|
@@ -8,7 +8,6 @@ import { formatProviderDeliveryFailure, providerDeliveryFailureFacts, innermostC
|
|
|
8
8
|
import { RuntimeLaunchFailure } from "../runtime/launchDiagnostics.js";
|
|
9
9
|
import { RuntimeLaunchError } from "../runtime/ports.js";
|
|
10
10
|
import { formatRunReceiptId } from "../task/taskRecordReference.js";
|
|
11
|
-
import { taskOwnsManagedWorkspace } from "../task/task.js";
|
|
12
11
|
import { runInputEnvelope } from "../agentRun/agentRun.js";
|
|
13
12
|
import { captureRoleRunDispatch } from "../coordination/workMailboxQueue.js";
|
|
14
13
|
import { isSchedulerPlanningDraft, isSchedulerTaskWorkspaceReady, selectedActiveSchedulerTasks, selectedSchedulerRoles } from "./ports.js";
|
|
@@ -136,16 +135,8 @@ async function deliverActiveRun(store, delivery, task, role, run, now) {
|
|
|
136
135
|
effective: run.effective,
|
|
137
136
|
workspace: run.effective.workspace.root,
|
|
138
137
|
...(run.workspace === undefined ? {} : { managedWorkspace: run.workspace }),
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
// closed (S27). Two distinct cases qualify: a Task activated with an
|
|
142
|
-
// empty plan and binding no Project, and a Draft planning conversation,
|
|
143
|
-
// which may already bind a Project but has not activated, so no worktree
|
|
144
|
-
// exists or is owed yet. Declaring it free is what keeps the isolation
|
|
145
|
-
// fence honest instead of letting it fail on a workspace nobody promised.
|
|
146
|
-
...(run.workspace === undefined
|
|
147
|
-
&& (isSchedulerPlanningDraft(task, run.purpose)
|
|
148
|
-
|| !taskOwnsManagedWorkspace(task))
|
|
138
|
+
// Draft planning has no delivery workspace or worktree to isolate.
|
|
139
|
+
...(run.workspace === undefined && isSchedulerPlanningDraft(task, run.purpose)
|
|
149
140
|
? { workspaceFree: true }
|
|
150
141
|
: {}),
|
|
151
142
|
mode,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { roleSessionMayContinue } from "../executor/effectiveLaunch.js";
|
|
2
2
|
import { roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
|
|
3
3
|
import { isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
4
|
-
import { taskOwnsManagedWorkspace } from "../task/task.js";
|
|
5
4
|
import { hasImmediateWakeReason } from "./wakeReason.js";
|
|
6
5
|
import { providerRetryPending } from "../runtime/providerRetry.js";
|
|
7
6
|
import { RuntimeLaunchError } from "../runtime/ports.js";
|
|
@@ -78,7 +77,7 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
78
77
|
taskId: task.id, roleName: role.name, agentId: effective.agentId,
|
|
79
78
|
adapterId: effective.adapterId, effective, workspace: effective.workspace.root,
|
|
80
79
|
...(role.managedWorkspace === undefined ? {} : { managedWorkspace: role.managedWorkspace }),
|
|
81
|
-
...(task.status === "draft"
|
|
80
|
+
...(task.status === "draft"
|
|
82
81
|
? { workspaceFree: true } : {}),
|
|
83
82
|
mode, ...(mode === "resume" && session?.nativeSessionId !== undefined
|
|
84
83
|
? { nativeSessionId: session.nativeSessionId } : {})
|
package/dist/scheduler/ports.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import { taskOwnsManagedWorkspace } from "../task/task.js";
|
|
2
1
|
import { isTaskOwnedWorkspace } from "../worktree/managedWorkspace.js";
|
|
3
2
|
/**
|
|
4
3
|
* Whether the Task's workspace state admits a launch.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
* plan, binding no Project — is admitted with no workspace record at all. The
|
|
8
|
-
* scheduler must not wait for a worktree that was deliberately never created.
|
|
9
|
-
* Every Task that does own one still needs the full ownership proof.
|
|
5
|
+
* Active Tasks always need the full Task-owned workspace proof.
|
|
10
6
|
*
|
|
11
7
|
* A Draft planning conversation is a third case: the Task may already bind a
|
|
12
8
|
* Project, yet activation has not run, so no worktree exists or is owed. It is
|
|
@@ -18,9 +14,6 @@ export function isSchedulerTaskWorkspaceReady(task, workspace, purpose) {
|
|
|
18
14
|
if (isSchedulerPlanningDraft(task, purpose)) {
|
|
19
15
|
return workspace === null || workspace === undefined;
|
|
20
16
|
}
|
|
21
|
-
if (!taskOwnsManagedWorkspace(task)) {
|
|
22
|
-
return workspace === null || workspace === undefined;
|
|
23
|
-
}
|
|
24
17
|
return isTaskOwnedWorkspace(workspace, task.id, task.cwd, task.projectBindings.map(({ projectId, directory }) => ({ projectId, directory })));
|
|
25
18
|
}
|
|
26
19
|
/**
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
2
2
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
3
3
|
import { providerRetryProjection } from "../runtime/providerRetry.js";
|
|
4
|
-
import { taskOwnsManagedWorkspace } from "../task/task.js";
|
|
5
4
|
import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
|
|
6
5
|
import { mailboxBatches } from "../coordination/workMailbox.js";
|
|
7
6
|
import { actionableExecutionLaneRecoveries, summarizeExecutionGroupHealth } from "../execution/executionHealth.js";
|
|
@@ -651,11 +650,7 @@ function collectBlockers(workItems, reviewRounds, integrations, openInputs, task
|
|
|
651
650
|
summary: request.question
|
|
652
651
|
});
|
|
653
652
|
}
|
|
654
|
-
|
|
655
|
-
// cwd by design; only a Task that should own one is blocked by its absence.
|
|
656
|
-
if (task.status === "active"
|
|
657
|
-
&& taskOwnsManagedWorkspace(task)
|
|
658
|
-
&& task.cwd === undefined) {
|
|
653
|
+
if (task.status === "active" && task.cwd === undefined) {
|
|
659
654
|
blockers.push({
|
|
660
655
|
kind: "identity",
|
|
661
656
|
id: `workspace:${task.id}`,
|