@zq-silk/yui 2.22.0 → 2.25.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 +9 -4
- package/dist/artifacts/artifactCapability.js +6 -6
- package/dist/artifacts/gitArtifactRef.js +10 -10
- package/dist/artifacts/jobArtifact.js +5 -5
- package/dist/artifacts/managedGit.js +15 -12
- package/dist/artifacts/taskArtifactRepository.js +35 -33
- package/dist/cli/commandCatalog.js +2 -2
- package/dist/cli/updateCommand.js +20 -3
- package/dist/cli/updateOrchestrator.js +13 -0
- package/dist/cli/updatePorts.js +27 -0
- package/dist/cli.js +9 -2
- package/dist/commands/configCommands.js +64 -1
- package/dist/commands/projectCommands.js +9 -9
- package/dist/commands/taskCompletionGate.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +1 -1
- package/dist/commands/taskPublicationAdoptCommand.js +1 -1
- package/dist/commands/taskRoleRuntimeStatus.js +4 -0
- package/dist/commands/taskUpstreamCommands.js +1 -1
- package/dist/config/configCatalog.js +3 -0
- package/dist/config/gitConfig.js +20 -0
- package/dist/config/yuiConfig.js +1 -0
- package/dist/context/currentSkills.js +32 -1
- package/dist/context/sessionBootstrapManifest.js +17 -4
- package/dist/context/sessionSkillNotice.js +33 -12
- package/dist/controlPlaneCli.js +4 -4
- package/dist/controller/clientRuntime.js +37 -18
- package/dist/controller/controller.js +16 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +2 -0
- package/dist/controller/hostLifecycle.js +153 -0
- package/dist/controller/hostLifecycleChecks.js +27 -0
- package/dist/controller/hostUpgrade.js +344 -0
- package/dist/controller/jobArtifactControl.js +3 -3
- package/dist/controller/jobDefinitionControl.js +1 -1
- package/dist/controller/runtime.js +67 -16
- package/dist/controller/sessionNotify.js +5 -46
- package/dist/execution/codexThreadNaming.js +54 -43
- package/dist/executor/fileRoleLaunchPlanner.js +18 -3
- package/dist/integration/gitIntegrationService.js +1 -1
- package/dist/kernel/builtinCapabilities.js +3 -3
- package/dist/repository/gitCommand.js +153 -0
- package/dist/repository/gitCompatibility.js +3 -2
- package/dist/repository/gitWorkspace.js +579 -563
- package/dist/repository/gitWorktreeInventory.js +15 -19
- package/dist/repository/projectRefresh.js +1 -1
- package/dist/repository/taskBaseFreshness.js +1 -4
- package/dist/repository/taskWorkspacePreparer.js +1 -1
- package/dist/runtime/agentEndpoint.js +11 -1
- package/dist/runtime/agentEndpointIdentity.js +2 -0
- package/dist/runtime/agentHost.js +167 -42
- package/dist/runtime/agentHostImplementation.js +3 -1
- package/dist/runtime/codexAppServerRuntime.js +3 -0
- package/dist/runtime/codexInteractiveHost.js +62 -17
- package/dist/runtime/codexNativeAccess.js +1 -1
- package/dist/runtime/contextRecovery.js +15 -0
- package/dist/runtime/hostSessionLifecycle.js +83 -0
- package/dist/runtime/hostUpgradeGate.js +43 -0
- package/dist/runtime/lifecycleChecks.js +47 -0
- package/dist/runtime/lifecycleReservation.js +9 -1
- package/dist/runtime/nativeExecutable.js +5 -2
- package/dist/runtime/nativeTitleReceipt.js +31 -0
- package/dist/runtime/nativeTitleSync.js +192 -0
- package/dist/runtime/platform.js +11 -0
- package/dist/runtime/providerRetry.js +4 -4
- package/dist/runtime/structuredProviderHost.js +42 -10
- package/dist/storage/sqliteSchema.js +30 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/taskStore.js +7 -1
- package/dist/web/webConversation.js +2 -2
- package/dist/web/webMaterials.js +19 -19
- package/dist/web/webTaskSurface.js +3 -3
- package/dist/workspace/workItemChangeSetManager.js +3 -3
- package/docs/managed-turn-and-session-runtime.md +96 -1
- package/docs/managed-turn-and-session-runtime.zh-CN.md +70 -1
- package/docs/project-refresh.md +3 -2
- package/docs/project-refresh.zh-CN.md +2 -1
- package/docs/release-workflow.md +86 -2
- package/docs/release-workflow.zh-CN.md +65 -1
- package/docs/roles-and-configuration.md +34 -0
- package/docs/roles-and-configuration.zh-CN.md +27 -0
- package/docs/storage-baseline.md +14 -1
- package/docs/storage-baseline.zh-CN.md +7 -1
- package/docs/testing/verification-levels.md +7 -7
- package/docs/testing/verification-levels.zh-CN.md +6 -6
- package/i18n/README.zh-CN.md +8 -4
- package/package.json +1 -1
- package/skills/yui-runtime/SKILL.md +7 -2
- package/skills/yui-runtime/VERSION +1 -1
- package/dist/runtime/native/darwin-x64/claude-process-owner +0 -0
- package/dist/runtime/native/darwin-x64/process-identity +0 -0
|
@@ -27,7 +27,7 @@ import { launchBrokerForHome } from "../runtime/launchBroker.js";
|
|
|
27
27
|
import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
28
28
|
import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
|
|
29
29
|
import { replayRuntimeProcessExitOutbox } from "../runtime/processExitOutbox.js";
|
|
30
|
-
import {
|
|
30
|
+
import { providerRetryContext } from "../runtime/providerRetry.js";
|
|
31
31
|
import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
32
32
|
import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
|
|
33
33
|
import { managedRuntimeRoot } from "../storage/homeLayout.js";
|
|
@@ -60,6 +60,13 @@ import { ephemeralDomainFromEnvironment, recordEphemeralTmuxTarget } from "./dom
|
|
|
60
60
|
import { createEphemeralResourceReaper } from "./ephemeralResourceReaper.js";
|
|
61
61
|
import { AgentHostProviderSessionBusyError, AgentHostProviderTurnFenceError, FileSchedulerStoreAdapter } from "./fileSchedulerStoreAdapter.js";
|
|
62
62
|
import { deliverGlobalInputs } from "./globalInputDelivery.js";
|
|
63
|
+
import { HostUpgradeCoordinator } from "./hostUpgrade.js";
|
|
64
|
+
import { createHostLifecycleChecks } from "./hostLifecycleChecks.js";
|
|
65
|
+
import { createControllerHostLifecycle } from "./hostLifecycle.js";
|
|
66
|
+
import { createNonBlockingChecks } from "../runtime/lifecycleChecks.js";
|
|
67
|
+
import { AGENT_HOST_IMPLEMENTATION } from "../runtime/agentHostImplementation.js";
|
|
68
|
+
import { builtinAgentEndpointImplementation } from "../runtime/agentEndpointIdentity.js";
|
|
69
|
+
import { sendAgentHostUpgradeControl } from "../runtime/agentHost.js";
|
|
63
70
|
import { authorizeJobStart } from "./jobControl.js";
|
|
64
71
|
import { authorizeGlobalJobStart, createJobDefinitionControl } from "./jobDefinitionControl.js";
|
|
65
72
|
import { reconcileTaskDelegation } from "./jobTaskDelegationControl.js";
|
|
@@ -198,7 +205,7 @@ async function createFileTaskControllerRuntime(home, options) {
|
|
|
198
205
|
?? new AgentConfigurationCatalogService(home, {
|
|
199
206
|
environment: options.environment ?? process.env
|
|
200
207
|
});
|
|
201
|
-
const
|
|
208
|
+
const rawSessionHost = options.sessionHost ?? new TmuxSessionHost(planner, tmux, {
|
|
202
209
|
validateLaunch: async (request) => {
|
|
203
210
|
const agent = store.getConfiguredAgent(request.agentId);
|
|
204
211
|
if (agent === null)
|
|
@@ -267,13 +274,13 @@ async function createFileTaskControllerRuntime(home, options) {
|
|
|
267
274
|
globalInstallPaths: [process.execPath]
|
|
268
275
|
}
|
|
269
276
|
});
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
+
const hostStateChecks = createNonBlockingChecks([{
|
|
278
|
+
id: "host-version-hint",
|
|
279
|
+
run: (_transition, signal) => { if (!signal.aborted)
|
|
280
|
+
hostLifecycleChecks.notify("host-observed"); }
|
|
281
|
+
}], options.onError);
|
|
282
|
+
const controllerHosts = createControllerHostLifecycle(rawSessionHost, {
|
|
283
|
+
after: hostStateChecks,
|
|
277
284
|
stopOwner: (owner) => {
|
|
278
285
|
// Issue 03: the durable `stopped` transition is gated on physical
|
|
279
286
|
// exit proof. A blocked result keeps the Session non-terminal and
|
|
@@ -287,7 +294,9 @@ async function createFileTaskControllerRuntime(home, options) {
|
|
|
287
294
|
return result.outcome === "stop-confirmed";
|
|
288
295
|
});
|
|
289
296
|
},
|
|
290
|
-
};
|
|
297
|
+
});
|
|
298
|
+
const sessionHost = controllerHosts.port;
|
|
299
|
+
const lifecycleHost = sessionHost;
|
|
291
300
|
const resourceActivity = createRuntimeResourceActivityTracker();
|
|
292
301
|
let runningRuntime;
|
|
293
302
|
let runningController;
|
|
@@ -298,6 +307,32 @@ async function createFileTaskControllerRuntime(home, options) {
|
|
|
298
307
|
},
|
|
299
308
|
runtimeIsolation
|
|
300
309
|
});
|
|
310
|
+
const hostUpgrades = new HostUpgradeCoordinator(store, AGENT_HOST_IMPLEMENTATION, {
|
|
311
|
+
inspect: owner => inspectAgentHost({ home, ...owner }),
|
|
312
|
+
control: async (owner, type, request) => {
|
|
313
|
+
const send = async () => {
|
|
314
|
+
const result = await sendAgentHostUpgradeControl({
|
|
315
|
+
home, ...owner, control: { protocol: AGENT_HOST_CONTROL_PROTOCOL, type, ...request }
|
|
316
|
+
});
|
|
317
|
+
if (result.outcome === "rejected")
|
|
318
|
+
throw new Error(result.failure?.detail ?? "Host upgrade control rejected.");
|
|
319
|
+
return result.snapshot;
|
|
320
|
+
};
|
|
321
|
+
return type === "finish-upgrade" ? controllerHosts.requestExit(owner, send) : send();
|
|
322
|
+
},
|
|
323
|
+
hostState: async (owner) => (await sessionHost.inspectOwner(owner)).state,
|
|
324
|
+
endpoint: builtinAgentEndpointImplementation,
|
|
325
|
+
restore: async (owner, request, effective, assertCurrent) => {
|
|
326
|
+
const workspace = owner.scope === "task" ? currentDesiredManagedWorkspace(store, owner.taskId, owner.roleName) : undefined;
|
|
327
|
+
await launchCoordinator.prepare({
|
|
328
|
+
owner, agentId: request.agentId, adapterId: request.adapterId, effective,
|
|
329
|
+
workspace: effective.workspace.root, mode: "resume", nativeSessionId: request.nativeSessionId,
|
|
330
|
+
...(workspace === undefined ? {} : { managedWorkspace: workspace }),
|
|
331
|
+
...(effective.executionAuthority === "planning" ? { workspaceFree: true } : {})
|
|
332
|
+
}, assertCurrent);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
const hostLifecycleChecks = createHostLifecycleChecks(hostUpgrades, options.onError);
|
|
301
336
|
// One inventory scan per scheduler pass. When the inventory worker is active
|
|
302
337
|
// the blocking /proc scan runs there; otherwise it runs on the main thread.
|
|
303
338
|
const scanInventory = (panes) => inventoryClient !== undefined
|
|
@@ -588,10 +623,22 @@ async function createFileTaskControllerRuntime(home, options) {
|
|
|
588
623
|
let messageHealthHosts = [];
|
|
589
624
|
const running = await startFileTaskController(home, workerMaintenance
|
|
590
625
|
? workerSchedulerStore(schedulerStore, asyncStoreClient)
|
|
591
|
-
: schedulerStore, delivery, (method, params) => method === "
|
|
592
|
-
?
|
|
593
|
-
: method === "
|
|
594
|
-
?
|
|
626
|
+
: schedulerStore, delivery, (method, params) => method === "runtime.host-upgrade-status"
|
|
627
|
+
? hostUpgrades.status().then(hosts => ({ hosts }))
|
|
628
|
+
: method === "runtime.upgrade-hosts"
|
|
629
|
+
? (params.generation === AGENT_HOST_IMPLEMENTATION.generation
|
|
630
|
+
? hostUpgrades.request().then(hosts => ({ hosts }))
|
|
631
|
+
: Promise.reject(new Error("Host upgrade target is not this Controller's verified loaded generation.")))
|
|
632
|
+
: method === "task.role-capabilities"
|
|
633
|
+
? readTaskAgentCapabilities(store, catalogs, params)
|
|
634
|
+
: method === "web.start" || method === "web.stop" || method === "web.status"
|
|
635
|
+
? web.dispatch(method, params) : Promise.resolve(lifecycleDispatcher(method, params)).then(result => {
|
|
636
|
+
if (["runtime.ensure-role-session", "runtime.process-exit-observe",
|
|
637
|
+
"runtime.host-observation-apply", "runtime.observation-apply"].includes(method)) {
|
|
638
|
+
hostLifecycleChecks.notify("host-observed");
|
|
639
|
+
}
|
|
640
|
+
return result;
|
|
641
|
+
}), {
|
|
595
642
|
intervalMs: options.intervalMs
|
|
596
643
|
?? reconciliationIntervalMilliseconds(store.getConfig().reconciliationIntervalSeconds),
|
|
597
644
|
signalWindowMs: options.signalWindowMs,
|
|
@@ -610,6 +657,7 @@ async function createFileTaskControllerRuntime(home, options) {
|
|
|
610
657
|
diagnosticMetrics: options.diagnosticMetrics,
|
|
611
658
|
onRequestDiagnostic: options.onRequestDiagnostic,
|
|
612
659
|
lifecycleHost,
|
|
660
|
+
lifecycleChecks: hostLifecycleChecks,
|
|
613
661
|
providerRetry: createProviderRetryHooks(home, store, {
|
|
614
662
|
now: options.now,
|
|
615
663
|
onError: options.onError,
|
|
@@ -704,7 +752,9 @@ async function createFileTaskControllerRuntime(home, options) {
|
|
|
704
752
|
}
|
|
705
753
|
let resourceClose;
|
|
706
754
|
const closeResources = () => {
|
|
707
|
-
|
|
755
|
+
hostStateChecks.close();
|
|
756
|
+
hostLifecycleChecks.close();
|
|
757
|
+
resourceClose ??= hostUpgrades.close().then(() => closeTelemetry()).finally(() => Promise.all([
|
|
708
758
|
web.close(),
|
|
709
759
|
kernel.close(),
|
|
710
760
|
asyncStoreClient?.close() ?? Promise.resolve(),
|
|
@@ -845,7 +895,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
845
895
|
const retry = binding?.retry;
|
|
846
896
|
return { recorded: true, ...(retry?.currentAttemptId === value.attemptId && value.boundedText !== undefined
|
|
847
897
|
? { retryInput: {
|
|
848
|
-
text:
|
|
898
|
+
text: value.boundedText,
|
|
899
|
+
...(retry.failedNativeTurnId === undefined ? {} : { context: providerRetryContext(retry) }),
|
|
849
900
|
...(retry.failedNativeTurnId === undefined ? {} : { expectedFailedNativeTurnId: retry.failedNativeTurnId })
|
|
850
901
|
} } : {}) };
|
|
851
902
|
}
|
|
@@ -2,10 +2,9 @@ import { callController } from "../core/controllerClient.js";
|
|
|
2
2
|
import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
|
|
3
3
|
import { transportAgentResult } from "../domain/agentResultTransport.js";
|
|
4
4
|
import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
|
|
5
|
-
import { setCodexThreadName } from "../execution/codexThreadNaming.js";
|
|
6
5
|
import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
|
|
7
6
|
/** Hidden CLI entrypoint used by Codex's structured notify hook. */
|
|
8
|
-
export async function runSessionNotifyCommand(payloadArgument, environment = process.env, call
|
|
7
|
+
export async function runSessionNotifyCommand(payloadArgument, environment = process.env, call) {
|
|
9
8
|
const params = parseCodexSessionNotification(payloadArgument, environment);
|
|
10
9
|
// Old global TUIs can still emit their invocation-local hook. It is not
|
|
11
10
|
// evidence of a shared-daemon Thread's identity or process lifecycle.
|
|
@@ -15,7 +14,7 @@ export async function runSessionNotifyCommand(payloadArgument, environment = pro
|
|
|
15
14
|
const home = requireText(environment.YUI_HOME, "YUI_HOME");
|
|
16
15
|
// The caller reports native identity; durable state supplies the current AgentRun.
|
|
17
16
|
const current = currentNotifyRun(home, params);
|
|
18
|
-
|
|
17
|
+
new FileRuntimeEventInbox(home).enqueueRunTerminal({
|
|
19
18
|
scope: params.scope,
|
|
20
19
|
...(params.scope === "task" ? { taskId: params.taskId } : {}),
|
|
21
20
|
roleName: params.roleName,
|
|
@@ -42,11 +41,9 @@ export async function runSessionNotifyCommand(payloadArgument, environment = pro
|
|
|
42
41
|
roleName: params.roleName
|
|
43
42
|
})
|
|
44
43
|
}, { timeoutMs: 100 }).catch(() => { });
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
await setThreadName(request).catch(() => { });
|
|
49
|
-
}
|
|
44
|
+
// Title synchronization belongs to the Host's separately fenced metadata
|
|
45
|
+
// lane. A terminal notification is neither a rename receipt nor permission
|
|
46
|
+
// to replay an unknown write or overwrite a native user's name.
|
|
50
47
|
}
|
|
51
48
|
/**
|
|
52
49
|
* Resolve the active AgentRun only when the notifying native Session matches.
|
|
@@ -130,44 +127,6 @@ function requireText(value, label) {
|
|
|
130
127
|
throw new Error(`${label} is invalid.`);
|
|
131
128
|
return text;
|
|
132
129
|
}
|
|
133
|
-
function shouldSetThreadName(home, params, runId) {
|
|
134
|
-
if (params.scope !== "task"
|
|
135
|
-
|| params.title === undefined
|
|
136
|
-
|| runId === undefined)
|
|
137
|
-
return false;
|
|
138
|
-
try {
|
|
139
|
-
const store = openCurrentTaskStore(home);
|
|
140
|
-
return store.getRun(params.taskId, runId)?.mode === "new";
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function threadNameRequest(params, environment) {
|
|
147
|
-
if (params.title === undefined)
|
|
148
|
-
return null;
|
|
149
|
-
const command = environment.YUI_AGENT_COMMAND;
|
|
150
|
-
const encodedBaseArgs = environment.YUI_AGENT_BASE_ARGS;
|
|
151
|
-
if (command === undefined || encodedBaseArgs === undefined)
|
|
152
|
-
return null;
|
|
153
|
-
try {
|
|
154
|
-
const parsed = JSON.parse(encodedBaseArgs);
|
|
155
|
-
if (!Array.isArray(parsed)
|
|
156
|
-
|| parsed.some((value) => typeof value !== "string" || value.includes("\0"))) {
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
return {
|
|
160
|
-
command: requireText(command, "YUI_AGENT_COMMAND"),
|
|
161
|
-
baseArgs: parsed,
|
|
162
|
-
environment,
|
|
163
|
-
threadId: params.nativeSessionId,
|
|
164
|
-
name: params.title
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
130
|
function isObject(value) {
|
|
172
131
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
173
132
|
}
|
|
@@ -2,52 +2,64 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
import { codexClientInitialization } from "../runtime/codexAppServerRuntime.js";
|
|
4
4
|
const DEFAULT_TIMEOUT_MS = 1_000;
|
|
5
|
-
export
|
|
6
|
-
const timeoutMs = threadNameTimeout(request.timeoutMs);
|
|
7
|
-
const client = await CodexNamingClient.start(request.command, request.environment, request.baseArgs ?? [], timeoutMs);
|
|
8
|
-
try {
|
|
9
|
-
await withTimeout(client.request("thread/name/set", {
|
|
10
|
-
threadId: requiredText(request.threadId, "Codex thread id"),
|
|
11
|
-
name: requiredThreadName(request.name)
|
|
12
|
-
}), timeoutMs, "Timed out setting Codex thread name.");
|
|
13
|
-
}
|
|
14
|
-
finally {
|
|
15
|
-
client.close();
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
class CodexNamingClient {
|
|
5
|
+
export class CodexNamingClient {
|
|
19
6
|
child;
|
|
7
|
+
accountHome;
|
|
8
|
+
exited;
|
|
20
9
|
#pending = new Map();
|
|
21
10
|
#stderr = [];
|
|
22
11
|
#nextId = 1;
|
|
23
12
|
#closed = false;
|
|
24
|
-
|
|
13
|
+
#deadline;
|
|
14
|
+
constructor(child, timeoutMs) {
|
|
25
15
|
this.child = child;
|
|
16
|
+
this.exited = new Promise(resolve => child.once("close", () => resolve()));
|
|
17
|
+
this.#deadline = setTimeout(() => {
|
|
18
|
+
this.#failAll(new Error("Native title metadata process deadline exceeded; effect may be unknown."));
|
|
19
|
+
this.close("SIGKILL");
|
|
20
|
+
}, timeoutMs);
|
|
26
21
|
const output = createInterface({ input: child.stdout });
|
|
27
|
-
output.on("line", (line) => {
|
|
22
|
+
output.on("line", (line) => {
|
|
23
|
+
try {
|
|
24
|
+
if (Buffer.byteLength(line) > 1_048_576)
|
|
25
|
+
throw new Error("Native title reply exceeds its bound.");
|
|
26
|
+
this.#receive(line);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
this.#failAll(error instanceof Error ? error : new Error(String(error)));
|
|
30
|
+
this.close();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
child.stdout.on("error", error => { this.#failAll(error); this.close(); });
|
|
28
34
|
child.stderr.setEncoding("utf8");
|
|
29
35
|
child.stderr.on("data", (chunk) => {
|
|
30
36
|
this.#stderr.push(chunk);
|
|
31
37
|
if (this.#stderr.join("").length > 16_384)
|
|
32
38
|
this.#stderr.shift();
|
|
33
39
|
});
|
|
34
|
-
child.on("error", (error) => { this.#failAll(error); });
|
|
40
|
+
child.on("error", (error) => { this.#failAll(error); this.close(); });
|
|
41
|
+
child.stdin.on("error", (error) => { this.#failAll(error); this.close(); });
|
|
35
42
|
child.on("exit", (code, signal) => {
|
|
43
|
+
clearTimeout(this.#deadline);
|
|
36
44
|
if (this.#closed)
|
|
37
45
|
return;
|
|
46
|
+
this.#closed = true;
|
|
38
47
|
const detail = this.#stderr.join("").trim();
|
|
39
48
|
this.#failAll(new Error(`Codex App Server exited (${code ?? signal ?? "unknown"})${detail.length === 0 ? "" : `: ${detail}`}`));
|
|
40
49
|
});
|
|
41
50
|
}
|
|
42
51
|
static async start(command, environment, baseArgs, timeoutMs) {
|
|
43
|
-
const
|
|
52
|
+
const deadlineMs = threadNameTimeout(timeoutMs);
|
|
53
|
+
const client = new CodexNamingClient(spawn(command, [...baseArgs, "app-server", "--listen", "stdio://"], { env: environment, stdio: ["pipe", "pipe", "pipe"] }), deadlineMs);
|
|
44
54
|
try {
|
|
45
|
-
await withTimeout(client.request("initialize", codexClientInitialization()), timeoutMs, "Timed out initializing Codex App Server for thread naming.");
|
|
55
|
+
const initialized = await withTimeout(client.request("initialize", codexClientInitialization()), timeoutMs, "Timed out initializing Codex App Server for thread naming.");
|
|
56
|
+
client.accountHome = typeof initialized.codexHome === "string" ? initialized.codexHome : undefined;
|
|
46
57
|
client.notify("initialized", {});
|
|
47
58
|
return client;
|
|
48
59
|
}
|
|
49
60
|
catch (error) {
|
|
50
61
|
client.close();
|
|
62
|
+
await client.exited;
|
|
51
63
|
throw error;
|
|
52
64
|
}
|
|
53
65
|
}
|
|
@@ -63,11 +75,19 @@ class CodexNamingClient {
|
|
|
63
75
|
notify(method, params) {
|
|
64
76
|
this.#write({ method, params });
|
|
65
77
|
}
|
|
66
|
-
close() {
|
|
78
|
+
close(signal = "SIGTERM") {
|
|
67
79
|
if (this.#closed)
|
|
68
80
|
return;
|
|
69
81
|
this.#closed = true;
|
|
70
|
-
this
|
|
82
|
+
clearTimeout(this.#deadline);
|
|
83
|
+
this.child.kill(signal);
|
|
84
|
+
// An owned metadata process must not survive a lost reply or ignored TERM.
|
|
85
|
+
const reap = setTimeout(() => {
|
|
86
|
+
if (this.child.exitCode === null && this.child.signalCode === null)
|
|
87
|
+
this.child.kill("SIGKILL");
|
|
88
|
+
}, 250);
|
|
89
|
+
reap.unref();
|
|
90
|
+
this.child.once("exit", () => clearTimeout(reap));
|
|
71
91
|
this.#failAll(new Error("Codex App Server closed."));
|
|
72
92
|
}
|
|
73
93
|
#write(value) {
|
|
@@ -94,13 +114,20 @@ class CodexNamingClient {
|
|
|
94
114
|
}
|
|
95
115
|
return;
|
|
96
116
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
117
|
+
try {
|
|
118
|
+
if (message.error !== undefined) {
|
|
119
|
+
const error = object(message.error, "Codex App Server error");
|
|
120
|
+
pending.reject(new Error(typeof error.message === "string" ? error.message : "Codex App Server request failed."));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
pending.resolve(object(message.result, "Codex App Server result"));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
pending.reject(error instanceof Error ? error : new Error(String(error)));
|
|
101
128
|
}
|
|
102
|
-
|
|
103
|
-
pending.
|
|
129
|
+
finally {
|
|
130
|
+
this.#pending.delete(message.id);
|
|
104
131
|
}
|
|
105
132
|
}
|
|
106
133
|
#failAll(error) {
|
|
@@ -115,22 +142,6 @@ function object(value, label) {
|
|
|
115
142
|
}
|
|
116
143
|
return value;
|
|
117
144
|
}
|
|
118
|
-
function requiredText(value, label) {
|
|
119
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
120
|
-
throw new Error(`${label} is invalid.`);
|
|
121
|
-
}
|
|
122
|
-
return value;
|
|
123
|
-
}
|
|
124
|
-
function requiredThreadName(value) {
|
|
125
|
-
if (typeof value !== "string" || value.includes("\0")) {
|
|
126
|
-
throw new Error("Codex thread name is invalid.");
|
|
127
|
-
}
|
|
128
|
-
const normalized = value.trim();
|
|
129
|
-
if (normalized.length === 0 || normalized.length > 1_024) {
|
|
130
|
-
throw new Error("Codex thread name is invalid.");
|
|
131
|
-
}
|
|
132
|
-
return normalized;
|
|
133
|
-
}
|
|
134
145
|
function threadNameTimeout(value) {
|
|
135
146
|
if (value === undefined)
|
|
136
147
|
return DEFAULT_TIMEOUT_MS;
|
|
@@ -17,7 +17,7 @@ import { managedRuntimeAdmission } from "../runtime/agentDriver.js";
|
|
|
17
17
|
import { builtinAgentEndpointImplementation, validateAgentEndpointImplementation } from "../runtime/agentEndpointIdentity.js";
|
|
18
18
|
import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
19
19
|
import { assertProviderConversationReplaceable } from "../runtime/providerRuntimeIdentity.js";
|
|
20
|
-
import { globalRoleSessionTitle, resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
20
|
+
import { globalRoleSessionTitle, taskRoleSessionTitle, resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
21
21
|
import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
|
|
22
22
|
import { planningRuntimeCwd } from "../storage/homeLayout.js";
|
|
23
23
|
import { resolveAgentAdapter } from "./agentAdapter.js";
|
|
@@ -225,7 +225,11 @@ export class FileRoleLaunchPlanner {
|
|
|
225
225
|
instanceCode: role.instanceCode,
|
|
226
226
|
generatedTitle: existing?.generatedTitle
|
|
227
227
|
}), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, {
|
|
228
|
-
purpose
|
|
228
|
+
purpose,
|
|
229
|
+
nativeGeneratedTitle: taskRoleSessionTitle(task, role.name, {
|
|
230
|
+
executionAuthority: (input.mode === "resume" ? existing?.effective : effective)?.executionAuthority,
|
|
231
|
+
instanceCode: role.instanceCode
|
|
232
|
+
})
|
|
229
233
|
});
|
|
230
234
|
}
|
|
231
235
|
planGlobalRole(input) {
|
|
@@ -258,7 +262,10 @@ export class FileRoleLaunchPlanner {
|
|
|
258
262
|
: existing?.generatedTitle !== undefined && existing.title === existing.generatedTitle
|
|
259
263
|
? globalRoleSessionTitle(role.name, new Date(existing.createdAt), this.store.getConfig().timeZone, role.instanceCode)
|
|
260
264
|
?? `Yui ${role.name}`
|
|
261
|
-
: undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, { purpose: "execution"
|
|
265
|
+
: undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, { purpose: "execution",
|
|
266
|
+
nativeGeneratedTitle: globalRoleSessionTitle(role.name, input.mode === "resume" && existing !== undefined ? new Date(existing.createdAt) : this.#now(), this.store.getConfig().timeZone, role.instanceCode)
|
|
267
|
+
?? (input.mode === "resume" ? `Yui ${role.name}` : undefined)
|
|
268
|
+
});
|
|
262
269
|
}
|
|
263
270
|
#compile(role, input, owner, sessionTitle, knownNativeSessionId, effective, sessionPolicy) {
|
|
264
271
|
const launchRole = effectiveRoleForLaunch(role, effective);
|
|
@@ -570,6 +577,14 @@ export class FileRoleLaunchPlanner {
|
|
|
570
577
|
YUI_DRIVER_ID: driver.id,
|
|
571
578
|
YUI_WORKSPACE: effectiveWorkspace,
|
|
572
579
|
YUI_SESSION_MANIFEST: sessionContext.sessionManifestPath,
|
|
580
|
+
// A proposal only. The independent metadata lane must prove native
|
|
581
|
+
// ownership; planning never reads/waits on title effects.
|
|
582
|
+
...(configured.adapterId !== "codex" || sessionPolicy.nativeGeneratedTitle === undefined ? {} : {
|
|
583
|
+
YUI_NATIVE_TITLE_PROPOSAL: sessionPolicy.nativeGeneratedTitle
|
|
584
|
+
}),
|
|
585
|
+
...(configured.adapterId !== "codex" || input.mode !== "resume" || existingSession?.title === undefined ? {} : {
|
|
586
|
+
YUI_NATIVE_TITLE_LOCAL: existingSession.title
|
|
587
|
+
}),
|
|
573
588
|
...(configured.adapterId === "codex"
|
|
574
589
|
? { YUI_AGENT_BASE_ARGS: JSON.stringify(configured.baseArgs) }
|
|
575
590
|
: {}),
|
|
@@ -64,7 +64,7 @@ export class GitIntegrationService {
|
|
|
64
64
|
environment;
|
|
65
65
|
runtimeIsolation;
|
|
66
66
|
#resourceRegistrarValue;
|
|
67
|
-
constructor(home, store, git = new NodeGitWorkspace(), now = () => new Date(), environment = process.env, runtimeIsolation = defaultIntegrationRuntimeIsolation(home, store.getHomeIdentity().homeId), jobPort) {
|
|
67
|
+
constructor(home, store, git = new NodeGitWorkspace(() => store.getConfig()), now = () => new Date(), environment = process.env, runtimeIsolation = defaultIntegrationRuntimeIsolation(home, store.getHomeIdentity().homeId), jobPort) {
|
|
68
68
|
this.store = store;
|
|
69
69
|
this.git = git;
|
|
70
70
|
this.now = now;
|
|
@@ -324,14 +324,14 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
|
|
|
324
324
|
...(params.encoding === undefined ? {} : { encoding: params.encoding }),
|
|
325
325
|
...(params.message === undefined ? {} : { message: params.message }),
|
|
326
326
|
...(params.expectedHead === undefined ? {} : { expectedHead: params.expectedHead })
|
|
327
|
-
});
|
|
327
|
+
}, () => store.getConfig());
|
|
328
328
|
if (name === "artifact.read")
|
|
329
329
|
return readArtifactCapability(store.rootDirectory(), taskId, {
|
|
330
330
|
relativePath: params.relativePath,
|
|
331
331
|
...(params.commit === undefined ? {} : { commit: params.commit })
|
|
332
|
-
});
|
|
332
|
+
}, () => store.getConfig());
|
|
333
333
|
if (name === "artifact.list")
|
|
334
|
-
return listArtifactsCapability(store.rootDirectory(), taskId);
|
|
334
|
+
return listArtifactsCapability(store.rootDirectory(), taskId, () => store.getConfig());
|
|
335
335
|
if (name === "environment.prepare")
|
|
336
336
|
return resources.prepare(taskId, params.plan);
|
|
337
337
|
if (name === "environment.adopt")
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS } from "../config/gitConfig.js";
|
|
4
|
+
export const LOCAL_GIT_TIMEOUT_MS = DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS * 1_000;
|
|
5
|
+
/** Retain process facts independently of Git's last progress/error output. */
|
|
6
|
+
export class GitCommandError extends Error {
|
|
7
|
+
timeoutKind;
|
|
8
|
+
budget;
|
|
9
|
+
elapsedMs;
|
|
10
|
+
code;
|
|
11
|
+
exitCode;
|
|
12
|
+
signal;
|
|
13
|
+
stdout;
|
|
14
|
+
stderr;
|
|
15
|
+
stderrTruncated;
|
|
16
|
+
constructor(cause, timeoutKind, budget, elapsedMs, output) {
|
|
17
|
+
const failure = cause;
|
|
18
|
+
const code = failure?.code ?? output.exitCode;
|
|
19
|
+
const exitCode = output.exitCode;
|
|
20
|
+
const stderr = output.stderr.toString("utf8").trim();
|
|
21
|
+
const reasons = [
|
|
22
|
+
...(timeoutKind === undefined ? [] : [
|
|
23
|
+
`${timeoutKind === "inactivity" ? "no-progress timeout" : "total timeout"} after `
|
|
24
|
+
+ `${timeoutKind === "inactivity" ? budget.inactivityMs : budget.totalMs} ms`
|
|
25
|
+
]),
|
|
26
|
+
...(exitCode === null ? [] : [`exit code ${exitCode}`]),
|
|
27
|
+
...(output.signal === null ? [] : [`signal ${output.signal}`]),
|
|
28
|
+
...(typeof code === "string" ? [`${code}: ${failure?.message ?? "process error"}`] : [])
|
|
29
|
+
];
|
|
30
|
+
if (reasons.length === 0)
|
|
31
|
+
reasons.push(failure?.message ?? "unknown process failure");
|
|
32
|
+
super(`Git command failed (${reasons.join("; ")}; elapsed ${elapsedMs} ms)`
|
|
33
|
+
+ (stderr.length === 0 ? "." : `: ${output.stderrTruncated ? "[stderr tail; earlier output omitted] " : ""}${stderr}`), { cause });
|
|
34
|
+
this.timeoutKind = timeoutKind;
|
|
35
|
+
this.budget = budget;
|
|
36
|
+
this.elapsedMs = elapsedMs;
|
|
37
|
+
this.name = "GitCommandError";
|
|
38
|
+
this.code = code;
|
|
39
|
+
this.exitCode = exitCode;
|
|
40
|
+
this.signal = output.signal;
|
|
41
|
+
this.stdout = output.stdout;
|
|
42
|
+
this.stderr = stderr;
|
|
43
|
+
this.stderrTruncated = output.stderrTruncated;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One buffered invocation, never retried. Output refreshes only the inactivity
|
|
48
|
+
* clock; the total deadline never moves. At the hard deadline, kill the owned
|
|
49
|
+
* process group (including transport helpers) before reporting completion.
|
|
50
|
+
*/
|
|
51
|
+
export async function executeGitCommand(program, args, options = {}) {
|
|
52
|
+
const budget = options.budget ?? { totalMs: LOCAL_GIT_TIMEOUT_MS };
|
|
53
|
+
for (const value of [budget.totalMs, ...(budget.inactivityMs === undefined ? [] : [budget.inactivityMs])]) {
|
|
54
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 2_147_483_647) {
|
|
55
|
+
throw new TypeError("Git timeout budgets must be positive integer milliseconds within the timer range.");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const started = performance.now();
|
|
59
|
+
const child = spawn(program, args, {
|
|
60
|
+
...(options.environment === undefined ? {} : { env: options.environment }),
|
|
61
|
+
detached: process.platform !== "win32",
|
|
62
|
+
windowsHide: true
|
|
63
|
+
});
|
|
64
|
+
const maxBuffer = options.maxBuffer ?? 1024 * 1024;
|
|
65
|
+
const chunks = { stdout: [], stderr: [] };
|
|
66
|
+
const sizes = { stdout: 0, stderr: 0 };
|
|
67
|
+
let stderrTruncated = false;
|
|
68
|
+
let failure;
|
|
69
|
+
let timeoutKind;
|
|
70
|
+
let terminationError;
|
|
71
|
+
const terminate = () => {
|
|
72
|
+
try {
|
|
73
|
+
if (child.pid !== undefined && process.platform !== "win32") {
|
|
74
|
+
try {
|
|
75
|
+
process.kill(-child.pid, "SIGKILL");
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code !== "ESRCH")
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
child.kill("SIGKILL");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
terminationError = error;
|
|
88
|
+
child.kill("SIGKILL");
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const expire = (kind) => {
|
|
92
|
+
if (timeoutKind !== undefined || failure !== undefined)
|
|
93
|
+
return;
|
|
94
|
+
timeoutKind = kind;
|
|
95
|
+
terminate();
|
|
96
|
+
};
|
|
97
|
+
child.once("error", error => { failure ??= error; });
|
|
98
|
+
const completion = new Promise(settle => {
|
|
99
|
+
child.once("close", (exitCode, signal) => settle({
|
|
100
|
+
// A negative close code is an OS spawn error, not a process exit.
|
|
101
|
+
exitCode: exitCode !== null && exitCode >= 0 ? exitCode : null, signal
|
|
102
|
+
}));
|
|
103
|
+
});
|
|
104
|
+
const totalTimer = setTimeout(() => expire("total"), budget.totalMs);
|
|
105
|
+
const inactivityTimer = budget.inactivityMs === undefined
|
|
106
|
+
? undefined : setTimeout(() => expire("inactivity"), budget.inactivityMs);
|
|
107
|
+
const collect = (stream, chunk) => {
|
|
108
|
+
if (chunk.length === 0)
|
|
109
|
+
return;
|
|
110
|
+
if (timeoutKind === undefined && failure === undefined)
|
|
111
|
+
inactivityTimer?.refresh();
|
|
112
|
+
if (stream === "stderr" && options.stderrTail) {
|
|
113
|
+
const limit = Math.min(maxBuffer, 64 * 1024);
|
|
114
|
+
stderrTruncated ||= sizes.stderr + chunk.length > limit;
|
|
115
|
+
const tail = Buffer.concat([...chunks.stderr, chunk]);
|
|
116
|
+
chunks.stderr = [tail.subarray(Math.max(0, tail.length - limit))];
|
|
117
|
+
sizes.stderr = Math.min(limit, tail.length);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const remaining = maxBuffer - sizes[stream];
|
|
121
|
+
if (remaining > 0) {
|
|
122
|
+
chunks[stream].push(chunk.subarray(0, remaining));
|
|
123
|
+
sizes[stream] += Math.min(chunk.length, remaining);
|
|
124
|
+
}
|
|
125
|
+
if (chunk.length > remaining && failure === undefined) {
|
|
126
|
+
failure = Object.assign(new Error(`${stream} maxBuffer length exceeded (${maxBuffer} bytes).`), { code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" });
|
|
127
|
+
terminate();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
child.stdout.on("data", chunk => collect("stdout", chunk));
|
|
131
|
+
child.stderr.on("data", chunk => collect("stderr", chunk));
|
|
132
|
+
child.stdin.on("error", error => {
|
|
133
|
+
failure ??= error;
|
|
134
|
+
terminate();
|
|
135
|
+
});
|
|
136
|
+
try {
|
|
137
|
+
child.stdin.end(options.input);
|
|
138
|
+
const status = await completion;
|
|
139
|
+
const result = { stdout: Buffer.concat(chunks.stdout), stderr: Buffer.concat(chunks.stderr), stderrTruncated };
|
|
140
|
+
if (failure !== undefined || timeoutKind !== undefined || status.exitCode !== 0 || status.signal !== null) {
|
|
141
|
+
const error = new GitCommandError(failure, timeoutKind, budget, Math.round(performance.now() - started), { ...result, ...status });
|
|
142
|
+
if (terminationError !== undefined) {
|
|
143
|
+
throw new AggregateError([error, terminationError], `${error.message} Process-group termination failed; helper effects are unconfirmed.`, { cause: error });
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
clearTimeout(totalTimer);
|
|
151
|
+
clearTimeout(inactivityTimer);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { realpathSync, statSync } from "node:fs";
|
|
3
3
|
import { resolveExecutable } from "../external/pinnedCommandRunner.js";
|
|
4
|
+
import { DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS } from "../config/gitConfig.js";
|
|
4
5
|
// The fetch isolation boundary needs --no-write-fetch-head. Older Git cannot
|
|
5
6
|
// implement it by saving/restoring FETCH_HEAD without racing another writer.
|
|
6
7
|
export const MINIMUM_GIT_VERSION = "2.29.0";
|
|
@@ -17,7 +18,7 @@ export function gitVersionSupport(output) {
|
|
|
17
18
|
}
|
|
18
19
|
let cached;
|
|
19
20
|
/** Pin the executable checked, reusing only an unchanged on-disk identity. */
|
|
20
|
-
export function supportedGit() {
|
|
21
|
+
export function supportedGit(timeoutMs = DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS * 1_000) {
|
|
21
22
|
const selected = resolveExecutable("git", process.env.PATH);
|
|
22
23
|
if (selected === undefined)
|
|
23
24
|
throw new Error("Git executable not found in PATH; install a supported Git before retrying. No Git operation was started.");
|
|
@@ -27,7 +28,7 @@ export function supportedGit() {
|
|
|
27
28
|
if (cached?.identity === identity)
|
|
28
29
|
return cached;
|
|
29
30
|
const output = execFileSync(path, ["--version"], {
|
|
30
|
-
encoding: "utf8", timeout:
|
|
31
|
+
encoding: "utf8", timeout: timeoutMs, stdio: ["ignore", "pipe", "pipe"]
|
|
31
32
|
});
|
|
32
33
|
const support = gitVersionSupport(output);
|
|
33
34
|
if (!support.supported) {
|