@zq-silk/yui 0.14.2 → 0.15.1
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 +27 -12
- package/README.md +85 -61
- package/dist/cli/commandCatalog.js +6 -6
- package/dist/cli/updateCommand.js +17 -9
- package/dist/cli/updateOrchestrator.js +81 -15
- package/dist/cli/updatePorts.js +72 -10
- package/dist/cli/upgradeCommand.js +104 -19
- package/dist/cli.js +2 -2
- package/dist/commands/agentCommands.js +13 -6
- package/dist/commands/controllerCommands.js +1 -1
- package/dist/commands/globalRoleCommands.js +11 -3
- package/dist/commands/roleConfiguration.js +7 -0
- package/dist/commands/roleRuntimeGuard.js +30 -0
- package/dist/commands/taskCommands.js +10 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +4 -4
- package/dist/controller/runtime.js +11 -25
- package/dist/controller/runtimeLaunchCoordinator.js +9 -30
- package/dist/controller/sessionNotify.js +5 -0
- package/dist/core/controllerServer.js +5 -5
- package/dist/doctor/doctor.js +37 -14
- package/dist/executor/agentExecutor.js +8 -11
- package/dist/executor/effectiveLaunch.js +34 -17
- package/dist/executor/fileRoleLaunchPlanner.js +11 -8
- package/dist/observability/runtimeIdentity.js +48 -50
- package/dist/release/runtimeRelease.js +9 -1
- package/dist/runtime/agentHost.js +7 -0
- package/dist/runtime/codexInteractiveHost.js +191 -0
- package/dist/runtime/exactControlPlane.js +20 -29
- package/dist/runtime/structuredProviderHost.js +35 -0
- package/dist/runtime/tmuxAdapters.js +51 -9
- package/dist/scheduler/activeRoleTurnDelivery.js +4 -4
- package/dist/scheduler/leaderWakeupProcessor.js +3 -4
- package/dist/storage/currentTaskStore.js +6 -4
- package/dist/storage/sqliteSchema.js +134 -59
- package/dist/storage/sqliteStore.js +7 -5
- package/dist/storage/storageSchema.js +92 -223
- package/dist/storage/storageVersions.js +12 -16
- package/dist/storage/upgrade/upgradeOrchestrator.js +224 -62
- package/dist/tmux/tmuxManager.js +43 -28
- package/dist/version.js +3 -3
- package/docs/task-local-identity.md +9 -9
- package/i18n/README.zh-CN.md +33 -17
- package/package.json +1 -1
- package/dist/storage/upgrade/recordVersions.js +0 -82
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
3
3
|
import { createRuntimeBinding, RuntimeGenerationMismatchError, RuntimeHostContentionError, RuntimeLaunchError } from "../runtime/index.js";
|
|
4
|
-
import {
|
|
4
|
+
import { sameEffectiveLaunch, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
|
|
5
5
|
class RuntimeBindingContractError extends Error {
|
|
6
6
|
constructor(message, options) {
|
|
7
7
|
super(message, options);
|
|
@@ -25,7 +25,6 @@ export class RuntimeLaunchCoordinator {
|
|
|
25
25
|
#createGenerationId;
|
|
26
26
|
#now;
|
|
27
27
|
#assertCurrent;
|
|
28
|
-
#launchFingerprint;
|
|
29
28
|
#onCleanupRequired;
|
|
30
29
|
#runtimeIsolation;
|
|
31
30
|
constructor(reservations, host, options = {}) {
|
|
@@ -34,8 +33,6 @@ export class RuntimeLaunchCoordinator {
|
|
|
34
33
|
this.#createGenerationId = options.createGenerationId ?? randomUUID;
|
|
35
34
|
this.#now = options.now ?? (() => new Date());
|
|
36
35
|
this.#assertCurrent = options.assertCurrent;
|
|
37
|
-
this.#launchFingerprint = options.launchFingerprint
|
|
38
|
-
?? defaultLaunchFingerprint;
|
|
39
36
|
this.#onCleanupRequired = options.onCleanupRequired;
|
|
40
37
|
this.#runtimeIsolation = options.runtimeIsolation;
|
|
41
38
|
}
|
|
@@ -79,22 +76,19 @@ export class RuntimeLaunchCoordinator {
|
|
|
79
76
|
if (request.owner.scope === "global" && request.managedWorkspace !== undefined) {
|
|
80
77
|
throw new Error("A global runtime cannot use a Task ManagedWorkspace.");
|
|
81
78
|
}
|
|
82
|
-
const expectedFingerprint = requireText(this.#launchFingerprint(request), "Launch fingerprint");
|
|
83
79
|
const assertLaunchCurrent = () => {
|
|
84
80
|
this.#assertCurrent?.(request);
|
|
85
81
|
assertCurrent?.();
|
|
86
|
-
if (this.#launchFingerprint(request) !== expectedFingerprint) {
|
|
87
|
-
throw new Error(`Role or Agent launch state changed: ${request.owner.roleName}.`);
|
|
88
|
-
}
|
|
89
82
|
};
|
|
90
|
-
const generationPrefix = `runtime-${expectedFingerprint}:generation:`;
|
|
91
83
|
const proposedGenerationId = requireText(this.#createGenerationId(), "Launch generation id");
|
|
92
|
-
|
|
84
|
+
// A Host activation id is an opaque durable identity, not a digest of the
|
|
85
|
+
// launch configuration. Whether a live activation may be reused is answered
|
|
86
|
+
// by the Role's durable Session record through `assertCurrent`, so launch
|
|
87
|
+
// configuration that only shapes the next activation never invalidates the
|
|
88
|
+
// current one.
|
|
89
|
+
let proposedRuntimeGenerationId = `runtime-${proposedGenerationId}`;
|
|
93
90
|
let reusedConfirmedRunningHost = false;
|
|
94
91
|
if (request.mode === "resume" && request.hostActivationId !== undefined) {
|
|
95
|
-
if (!request.hostActivationId.startsWith(generationPrefix)) {
|
|
96
|
-
throw new Error("Session restore targets an incompatible Host activation.");
|
|
97
|
-
}
|
|
98
92
|
const inspection = await this.host.inspectOwner(request.owner);
|
|
99
93
|
if (inspection.state === "unavailable" || inspection.state === "starting") {
|
|
100
94
|
throw new RuntimeLaunchError(true, request.hostActivationId, `Host activation is temporarily ${inspection.state}: ${request.owner.roleName}.`);
|
|
@@ -112,10 +106,6 @@ export class RuntimeLaunchCoordinator {
|
|
|
112
106
|
runtimeGenerationId: proposedRuntimeGenerationId
|
|
113
107
|
}, assertLaunchCurrent, this.#now());
|
|
114
108
|
if (reservation.status === "existing") {
|
|
115
|
-
if (!reservation.runtimeGenerationId.startsWith(generationPrefix)) {
|
|
116
|
-
this.#requireCleanup(request.owner);
|
|
117
|
-
throw new Error(`Runtime launch reservation belongs to stale Role or Agent state: ${request.owner.roleName}.`);
|
|
118
|
-
}
|
|
119
109
|
const inspection = await this.host.inspectOwner(request.owner);
|
|
120
110
|
if (inspection.state === "unavailable" || inspection.state === "starting") {
|
|
121
111
|
throw new RuntimeLaunchError(true, reservation.runtimeGenerationId, `Runtime is temporarily ${inspection.state}: ${request.owner.roleName}/${reservation.runtimeGenerationId}.`);
|
|
@@ -416,7 +406,7 @@ function validateRuntimeLaunchPreflight(preflight, request, runtimeGenerationId)
|
|
|
416
406
|
|| preflight.turnId !== request.turnId
|
|
417
407
|
|| preflight.agentId !== request.agentId
|
|
418
408
|
|| preflight.adapterId !== request.adapterId
|
|
419
|
-
|| !
|
|
409
|
+
|| !sameEffectiveLaunch(preflight.effective, request.effective)
|
|
420
410
|
|| (request.mode === "resume"
|
|
421
411
|
&& preflight.nativeSessionId !== request.nativeSessionId)) {
|
|
422
412
|
throw new Error(`Session host pre-start launch fence does not match the requested runtime: ${request.owner.roleName}.`);
|
|
@@ -445,17 +435,6 @@ function requireMatchingRuntimeBinding(raw, request, runtimeGenerationId) {
|
|
|
445
435
|
}
|
|
446
436
|
return binding;
|
|
447
437
|
}
|
|
448
|
-
function defaultLaunchFingerprint(request) {
|
|
449
|
-
return createHash("sha256").update(JSON.stringify([
|
|
450
|
-
request.owner,
|
|
451
|
-
request.agentId,
|
|
452
|
-
request.adapterId,
|
|
453
|
-
request.effective,
|
|
454
|
-
request.workspace,
|
|
455
|
-
request.managedWorkspace,
|
|
456
|
-
request.runtimePolicy
|
|
457
|
-
])).digest("hex");
|
|
458
|
-
}
|
|
459
438
|
function requireText(value, label) {
|
|
460
439
|
if (typeof value !== "string"
|
|
461
440
|
|| value.length === 0
|
|
@@ -7,6 +7,11 @@ import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
|
|
|
7
7
|
/** Hidden CLI entrypoint used by Codex's structured notify hook. */
|
|
8
8
|
export async function runSessionNotifyCommand(payloadArgument, environment = process.env, call, setThreadName = setCodexThreadName) {
|
|
9
9
|
const params = parseCodexSessionNotification(payloadArgument, environment);
|
|
10
|
+
// Old global TUIs can still emit their invocation-local hook. It is not
|
|
11
|
+
// evidence of a shared-daemon Thread's identity or process lifecycle.
|
|
12
|
+
// Global identity is committed from App Server at successful host start.
|
|
13
|
+
if (params.scope === "global")
|
|
14
|
+
return;
|
|
10
15
|
const home = requireText(environment.YUI_HOME, "YUI_HOME");
|
|
11
16
|
// A Codex process outlives its Turn, so the notify envelope cannot say which
|
|
12
17
|
// Turn or runtime generation is current. Durable Session state answers both;
|
|
@@ -400,8 +400,8 @@ async function routeRequest(socket, line, token, dispatcher, stop, status, telem
|
|
|
400
400
|
homeFilesystemId,
|
|
401
401
|
controllerInstanceId,
|
|
402
402
|
version: YUI_VERSION,
|
|
403
|
-
|
|
404
|
-
|
|
403
|
+
storageVersion: yuiVersionIdentity().storageVersion,
|
|
404
|
+
minimumStorageVersion: yuiVersionIdentity().minimumStorageVersion,
|
|
405
405
|
...(status === undefined ? {} : { runtime: status(telemetry) })
|
|
406
406
|
}
|
|
407
407
|
});
|
|
@@ -675,7 +675,7 @@ export function buildRuntimeIdentityReceipt(input) {
|
|
|
675
675
|
}
|
|
676
676
|
})();
|
|
677
677
|
return Object.freeze({
|
|
678
|
-
schemaVersion:
|
|
678
|
+
schemaVersion: 2,
|
|
679
679
|
version: YUI_VERSION,
|
|
680
680
|
executablePath: process.execPath,
|
|
681
681
|
args: process.argv.slice(1),
|
|
@@ -685,8 +685,8 @@ export function buildRuntimeIdentityReceipt(input) {
|
|
|
685
685
|
cliRealpath: controllerCliRealpath(),
|
|
686
686
|
controllerRealpath: realpathSync(fileURLToPath(import.meta.url)),
|
|
687
687
|
controllerProtocolVersion: FILE_TASK_CONTROLLER_PROTOCOL_VERSION,
|
|
688
|
-
|
|
689
|
-
|
|
688
|
+
storageVersion: yuiVersionIdentity().storageVersion,
|
|
689
|
+
minimumStorageVersion: yuiVersionIdentity().minimumStorageVersion,
|
|
690
690
|
storageBackend: input.storageBackend,
|
|
691
691
|
workerEnabled: input.workerEnabled,
|
|
692
692
|
pid: process.pid,
|
package/dist/doctor/doctor.js
CHANGED
|
@@ -34,7 +34,7 @@ export const STORAGE_DOCTOR_CHECK_NAMES = Object.freeze([
|
|
|
34
34
|
* healthy only when schema, compatibility, and state are all `ok`. Any
|
|
35
35
|
* `unsupported` (version mismatch / needs-new-version), `invalid` (corrupted /
|
|
36
36
|
* unreadable), or `missing` (uninitialized) storage check is blocking — even
|
|
37
|
-
*
|
|
37
|
+
* and the machine-readable command exits non-zero.
|
|
38
38
|
*/
|
|
39
39
|
export function summarizeStorageHealth(checks) {
|
|
40
40
|
const names = new Set(STORAGE_DOCTOR_CHECK_NAMES);
|
|
@@ -56,14 +56,16 @@ export function buildDoctorReport(env, executor) {
|
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
58
|
* Read the physical-backend facts for a Home (Issue 01 observability). Every
|
|
59
|
-
* field is best-effort read-only evidence: an unreadable database
|
|
60
|
-
*
|
|
61
|
-
*
|
|
59
|
+
* field is best-effort read-only evidence: an unreadable database yields
|
|
60
|
+
* `null` for that field, and the `storage state` check reports the structural
|
|
61
|
+
* problem separately.
|
|
62
62
|
*/
|
|
63
63
|
function inspectStorageDetails(home, env) {
|
|
64
64
|
const schema = inspectStorageSchema(home);
|
|
65
|
-
const
|
|
66
|
-
|
|
65
|
+
const storageVersion = schema.status === "current"
|
|
66
|
+
|| schema.status === "upgradeable"
|
|
67
|
+
|| schema.status === "unsupported"
|
|
68
|
+
? schema.currentVersion
|
|
67
69
|
: null;
|
|
68
70
|
const authoritativeBackend = "sqlite";
|
|
69
71
|
const dbPath = join(home, CURRENT_DATABASE_FILENAME);
|
|
@@ -88,7 +90,8 @@ function inspectStorageDetails(home, env) {
|
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
return {
|
|
91
|
-
|
|
93
|
+
storageVersion,
|
|
94
|
+
minimumSupportedVersion: schema.minimumSupportedVersion,
|
|
92
95
|
authoritativeBackend,
|
|
93
96
|
databasePath,
|
|
94
97
|
journalMode,
|
|
@@ -216,13 +219,22 @@ function checkSchema(state) {
|
|
|
216
219
|
return {
|
|
217
220
|
name: "storage schema",
|
|
218
221
|
status: "ok",
|
|
219
|
-
detail: `current=${state.currentVersion} latest=${state.latestVersion}`
|
|
222
|
+
detail: `current=${state.currentVersion} latest=${state.latestVersion} `
|
|
223
|
+
+ `minimum=${state.minimumSupportedVersion}`
|
|
224
|
+
};
|
|
225
|
+
case "upgradeable":
|
|
226
|
+
return {
|
|
227
|
+
name: "storage schema",
|
|
228
|
+
status: "unsupported",
|
|
229
|
+
detail: `current=${state.currentVersion} latest=${state.latestVersion} `
|
|
230
|
+
+ `minimum=${state.minimumSupportedVersion} migration=available`
|
|
220
231
|
};
|
|
221
232
|
case "unsupported":
|
|
222
233
|
return {
|
|
223
234
|
name: "storage schema",
|
|
224
235
|
status: "unsupported",
|
|
225
|
-
detail: `current=${state.currentVersion} latest=${state.latestVersion}
|
|
236
|
+
detail: `current=${state.currentVersion} latest=${state.latestVersion} `
|
|
237
|
+
+ `minimum=${state.minimumSupportedVersion} direction=${state.direction}`
|
|
226
238
|
};
|
|
227
239
|
case "invalid":
|
|
228
240
|
return { name: "storage schema", status: "invalid", detail: state.detail };
|
|
@@ -251,9 +263,20 @@ function inspectCompatibility(home, homeCheck, schema) {
|
|
|
251
263
|
check: {
|
|
252
264
|
name,
|
|
253
265
|
status: "unsupported",
|
|
254
|
-
detail: `
|
|
255
|
-
+ `(current=${schema.currentVersion},
|
|
256
|
-
+
|
|
266
|
+
detail: `storage version is ${schema.direction} `
|
|
267
|
+
+ `(current=${schema.currentVersion}, supported=`
|
|
268
|
+
+ `${schema.minimumSupportedVersion}..${schema.latestVersion}).`
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (schema.status === "upgradeable") {
|
|
273
|
+
return {
|
|
274
|
+
storageStatus: "upgradeable",
|
|
275
|
+
check: {
|
|
276
|
+
name,
|
|
277
|
+
status: "unsupported",
|
|
278
|
+
detail: `migration available from ${schema.currentVersion} to ${schema.latestVersion}; `
|
|
279
|
+
+ "run `yui upgrade` or `yui update`."
|
|
257
280
|
}
|
|
258
281
|
};
|
|
259
282
|
}
|
|
@@ -262,8 +285,8 @@ function inspectCompatibility(home, homeCheck, schema) {
|
|
|
262
285
|
check: {
|
|
263
286
|
name,
|
|
264
287
|
status: "ok",
|
|
265
|
-
detail: `current
|
|
266
|
-
+ `
|
|
288
|
+
detail: `current=${schema.currentVersion}/${schema.latestVersion} `
|
|
289
|
+
+ `minimum=${schema.minimumSupportedVersion}`
|
|
267
290
|
}
|
|
268
291
|
};
|
|
269
292
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { hasRecentTurnId, rememberRecentTurnId, validateRecentTurnIds } from "../runtime/recentTurnIds.js";
|
|
3
|
-
import {
|
|
3
|
+
import { roleSessionMayContinue, validateEffectiveLaunchSnapshot } from "./effectiveLaunch.js";
|
|
4
4
|
import { currentProviderActivation, endProviderActivation, settleProviderTurn, settleProviderTurnSubmission, validateProviderRuntimeBinding } from "../runtime/providerRuntimeIdentity.js";
|
|
5
5
|
import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
6
6
|
export function createRoleSessionSet(owner, activeAgentId, now) {
|
|
@@ -66,10 +66,9 @@ export function recordRoleAgentSession(set, input, now) {
|
|
|
66
66
|
throw new Error(`Role Agent session effective identity is inconsistent: ${agentId}.`);
|
|
67
67
|
}
|
|
68
68
|
if (existing !== undefined && existing.nativeSessionId === nativeSessionId
|
|
69
|
-
&& !
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
throw new Error(`Role Agent session effective launch cannot change: ${agentId}.`);
|
|
69
|
+
&& !roleSessionMayContinue(existing.effective, effective)) {
|
|
70
|
+
throw new Error(`Role Agent session cannot continue under this launch: ${agentId}. `
|
|
71
|
+
+ "Its Agent, adapter or physical workspace changed.");
|
|
73
72
|
}
|
|
74
73
|
if (existing !== undefined && existing.nativeSessionId !== nativeSessionId
|
|
75
74
|
&& existing.status === "active") {
|
|
@@ -280,13 +279,11 @@ export function roleAgentSessionResumeMode(set, agentId, desired) {
|
|
|
280
279
|
if (session.status === "ended") {
|
|
281
280
|
return "new";
|
|
282
281
|
}
|
|
283
|
-
|
|
284
|
-
? effectiveLaunchSnapshotsCompatibleForTaskSession(session.effective, desired)
|
|
285
|
-
: effectiveLaunchSnapshotsCompatible(session.effective, desired);
|
|
286
|
-
if (compatible)
|
|
282
|
+
if (roleSessionMayContinue(session.effective, desired))
|
|
287
283
|
return "resume";
|
|
288
|
-
throw new Error(`Role Agent session
|
|
289
|
-
+ "
|
|
284
|
+
throw new Error(`Role Agent session cannot continue under the next launch: ${agentId}. `
|
|
285
|
+
+ "Its Agent, adapter or physical workspace changed; stop the existing native "
|
|
286
|
+
+ "process before starting a fresh Session.");
|
|
290
287
|
}
|
|
291
288
|
export function bindTaskRoleProviderRuntime(set, binding, updatedAt) {
|
|
292
289
|
validateRoleSessionSet(set);
|
|
@@ -69,7 +69,12 @@ function claudeConfigFromSnapshot(snapshot) {
|
|
|
69
69
|
: { settingsSources: [...snapshot.settingsSources] })
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Exactness fence for one launch: the same resolved launch must be observed by
|
|
74
|
+
* every participant of that launch. Desired-revision bookkeeping is provenance
|
|
75
|
+
* and never part of the resolved launch itself.
|
|
76
|
+
*/
|
|
77
|
+
export function sameEffectiveLaunch(existing, desired) {
|
|
73
78
|
validateEffectiveLaunchSnapshot(existing);
|
|
74
79
|
validateEffectiveLaunchSnapshot(desired);
|
|
75
80
|
const withoutDesiredRevision = (snapshot) => {
|
|
@@ -79,20 +84,24 @@ export function effectiveLaunchSnapshotsCompatible(existing, desired) {
|
|
|
79
84
|
return isDeepStrictEqual(withoutDesiredRevision(existing), withoutDesiredRevision(desired));
|
|
80
85
|
}
|
|
81
86
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
87
|
+
* Whether a live native Session can still serve the next launch request.
|
|
88
|
+
*
|
|
89
|
+
* Only facts that make continuation impossible participate: the Session
|
|
90
|
+
* protocol, the provider identity that owns the conversation, and the physical
|
|
91
|
+
* workspace the Session runs in. Launch configuration such as model, effort,
|
|
92
|
+
* permission, Role context, declared write scope, and Turn-scoped facts like
|
|
93
|
+
* ReviewRound identity or candidate commits shape the next Host activation
|
|
94
|
+
* instead of ending the Session; that divergence is acknowledged where the
|
|
95
|
+
* configuration changes and stays visible as launch provenance.
|
|
96
|
+
*
|
|
97
|
+
* Session kind needs no separate check: a Role's review Turns run in their own
|
|
98
|
+
* ReviewRound workspace, so the physical workspace already separates a review
|
|
99
|
+
* Session from an execution Session.
|
|
86
100
|
*/
|
|
87
|
-
export function
|
|
88
|
-
if (effectiveLaunchSnapshotsCompatible(existing, desired))
|
|
89
|
-
return true;
|
|
101
|
+
export function roleSessionMayContinue(existing, desired) {
|
|
90
102
|
validateEffectiveLaunchSnapshot(existing);
|
|
91
103
|
validateEffectiveLaunchSnapshot(desired);
|
|
92
|
-
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
return isDeepStrictEqual(taskSessionCompatibleSnapshot(existing), taskSessionCompatibleSnapshot(desired));
|
|
104
|
+
return isDeepStrictEqual(sessionContinuitySnapshot(existing), sessionContinuitySnapshot(desired));
|
|
96
105
|
}
|
|
97
106
|
/** Preserves a fixed Session's launch configuration while freezing fresh Task-main Git facts. */
|
|
98
107
|
export function effectiveLaunchWithTaskMainWorkspace(existing, workspace) {
|
|
@@ -109,13 +118,21 @@ export function effectiveLaunchWithTaskMainWorkspace(existing, workspace) {
|
|
|
109
118
|
}
|
|
110
119
|
});
|
|
111
120
|
}
|
|
112
|
-
function
|
|
113
|
-
const { sourceDesiredRevision: _sourceDesiredRevision, reviewRoundId: _reviewRoundId, reviewBaseCommit: _reviewBaseCommit, workspace, ...launch } = snapshot;
|
|
121
|
+
function sessionContinuitySnapshot(snapshot) {
|
|
114
122
|
return {
|
|
115
|
-
|
|
123
|
+
schemaVersion: snapshot.schemaVersion,
|
|
124
|
+
contextProtocolVersion: snapshot.contextProtocolVersion,
|
|
125
|
+
agentId: snapshot.agentId,
|
|
126
|
+
adapterId: snapshot.adapterId,
|
|
116
127
|
workspace: {
|
|
117
|
-
root: workspace.root,
|
|
118
|
-
entries: workspace.entries.map((
|
|
128
|
+
root: snapshot.workspace.root,
|
|
129
|
+
entries: snapshot.workspace.entries.map((entry) => ({
|
|
130
|
+
projectId: entry.projectId,
|
|
131
|
+
directory: entry.directory,
|
|
132
|
+
access: entry.access,
|
|
133
|
+
path: entry.path,
|
|
134
|
+
branch: entry.branch
|
|
135
|
+
}))
|
|
119
136
|
}
|
|
120
137
|
};
|
|
121
138
|
}
|
|
@@ -15,7 +15,7 @@ import { resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
|
15
15
|
import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
|
|
16
16
|
import { classifyWorkspacePreflight, formatWorkspacePreflightError } from "./workspacePreflightClassification.js";
|
|
17
17
|
import { activeLiveRoleAgentSession } from "./agentExecutor.js";
|
|
18
|
-
import {
|
|
18
|
+
import { roleSessionMayContinue, effectiveRoleForLaunch, resolveEffectiveLaunch } from "./effectiveLaunch.js";
|
|
19
19
|
import { YUI_CONTROL_PLANE_DESCRIPTOR, createExactControlPlaneDescriptor, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
|
|
20
20
|
import { detectRunningRelease } from "../release/runtimeRelease.js";
|
|
21
21
|
import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
|
|
@@ -176,9 +176,7 @@ export class FileRoleLaunchPlanner {
|
|
|
176
176
|
const effective = input.effective ?? resolvedEffective;
|
|
177
177
|
const existing = sessionSet?.sessions[effective.agentId];
|
|
178
178
|
const compatibleExisting = existing !== undefined
|
|
179
|
-
&& (
|
|
180
|
-
? effectiveLaunchSnapshotsCompatibleForTaskSession(existing.effective, effective)
|
|
181
|
-
: effectiveLaunchSnapshotsCompatible(existing.effective, effective));
|
|
179
|
+
&& roleSessionMayContinue(existing.effective, effective);
|
|
182
180
|
if (input.mode === "resume" && !compatibleExisting) {
|
|
183
181
|
throw new Error(`Task Role resume effective snapshot drifted: ${task.id}/${role.name}.`);
|
|
184
182
|
}
|
|
@@ -204,7 +202,7 @@ export class FileRoleLaunchPlanner {
|
|
|
204
202
|
const effective = input.effective ?? resolvedEffective;
|
|
205
203
|
const existing = sessionSet?.sessions[effective.agentId];
|
|
206
204
|
const compatibleExisting = existing !== undefined
|
|
207
|
-
&&
|
|
205
|
+
&& roleSessionMayContinue(existing.effective, effective);
|
|
208
206
|
if (input.mode === "resume" && !compatibleExisting) {
|
|
209
207
|
throw new Error(`Global Role resume effective snapshot drifted: ${role.name}.`);
|
|
210
208
|
}
|
|
@@ -246,7 +244,9 @@ export class FileRoleLaunchPlanner {
|
|
|
246
244
|
: undefined,
|
|
247
245
|
trustWorkspace: true
|
|
248
246
|
});
|
|
249
|
-
assertCodexLaunchOverridesAvailable(codexConfig,
|
|
247
|
+
assertCodexLaunchOverridesAvailable(codexConfig, owner.scope === "global"
|
|
248
|
+
? ["developerInstructions"]
|
|
249
|
+
: ["developerInstructions", "notify"]);
|
|
250
250
|
}
|
|
251
251
|
const runtimeIsolation = input.runtimeIsolation === undefined
|
|
252
252
|
? undefined
|
|
@@ -365,10 +365,10 @@ export class FileRoleLaunchPlanner {
|
|
|
365
365
|
args.push("--plugin-dir", ensureClaudeLifecyclePlugin(this.home, this.#cliPath));
|
|
366
366
|
}
|
|
367
367
|
if (binding.adapterId === "codex") {
|
|
368
|
-
//
|
|
368
|
+
// Interactive Task sessions may use notify for presentation.
|
|
369
369
|
// Managed Turns receive lifecycle facts through their ordinary App Server
|
|
370
370
|
// subscription, avoiding a second Hook channel for the same Turn.
|
|
371
|
-
if (owner.scope
|
|
371
|
+
if (owner.scope === "task" && input.turnId === undefined) {
|
|
372
372
|
args = addCodexSessionNotify(args, launchMode, this.#cliPath);
|
|
373
373
|
}
|
|
374
374
|
// Managed Codex Turns use disposable proxy clients against the shared
|
|
@@ -472,6 +472,9 @@ export class FileRoleLaunchPlanner {
|
|
|
472
472
|
YUI_WORKSPACE: effectiveWorkspace,
|
|
473
473
|
YUI_SESSION_MANIFEST: sessionContext.sessionManifestPath,
|
|
474
474
|
YUI_SESSION_CLI: sessionContext.sessionCliPath,
|
|
475
|
+
...(owner.scope === "global" && configured.adapterId === "codex"
|
|
476
|
+
? { YUI_AGENT_BASE_ARGS: JSON.stringify(configured.baseArgs) }
|
|
477
|
+
: {}),
|
|
475
478
|
...(jobCallerKey === undefined ? {} : { YUI_JOB_CALLER_KEY: jobCallerKey }),
|
|
476
479
|
...(owner.scope !== "task"
|
|
477
480
|
? {}
|
|
@@ -13,7 +13,8 @@ import { spawnSync } from "node:child_process";
|
|
|
13
13
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import Database from "better-sqlite3";
|
|
16
|
-
import {
|
|
16
|
+
import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "../storage/storageVersions.js";
|
|
17
|
+
import { inspectStorageSchema } from "../storage/storageSchema.js";
|
|
17
18
|
import { resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
|
|
18
19
|
export const UNSUPPORTED = "unsupported";
|
|
19
20
|
/**
|
|
@@ -140,14 +141,7 @@ export function collectRuntimeBuildIdentity(ports) {
|
|
|
140
141
|
export function createProductionStorageIdentityPorts(env = process.env) {
|
|
141
142
|
return {
|
|
142
143
|
env,
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
return readFileSync(path, "utf8");
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
},
|
|
144
|
+
inspectStorage: (home) => inspectStorageSchema(home),
|
|
151
145
|
fileSize: (path) => {
|
|
152
146
|
try {
|
|
153
147
|
return statSync(path).size;
|
|
@@ -181,36 +175,12 @@ export function createProductionStorageIdentityPorts(env = process.env) {
|
|
|
181
175
|
* are never treated as an alternate authority or a repair source.
|
|
182
176
|
*/
|
|
183
177
|
export function collectStorageIdentity(home, ports = createProductionStorageIdentityPorts()) {
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
manifestStatus = "current";
|
|
191
|
-
try {
|
|
192
|
-
const manifest = JSON.parse(manifestText);
|
|
193
|
-
if (typeof manifest.storageVersion === "number"
|
|
194
|
-
&& Number.isFinite(manifest.storageVersion)) {
|
|
195
|
-
logicalLayout = manifest.storageVersion;
|
|
196
|
-
}
|
|
197
|
-
if (typeof manifest.aggregateSchemaVersion === "number"
|
|
198
|
-
&& Number.isFinite(manifest.aggregateSchemaVersion)) {
|
|
199
|
-
aggregateSchemaVersion = manifest.aggregateSchemaVersion;
|
|
200
|
-
}
|
|
201
|
-
if (typeof logicalLayout !== "number"
|
|
202
|
-
|| typeof aggregateSchemaVersion !== "number"
|
|
203
|
-
|| logicalLayout !== CURRENT_STORAGE_LAYOUT_VERSION
|
|
204
|
-
|| aggregateSchemaVersion !== CURRENT_AGGREGATE_SCHEMA_VERSION) {
|
|
205
|
-
manifestStatus = "unsupported";
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
manifestStatus = "invalid";
|
|
210
|
-
logicalLayout = UNSUPPORTED;
|
|
211
|
-
aggregateSchemaVersion = UNSUPPORTED;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
178
|
+
const storage = ports.inspectStorage(home);
|
|
179
|
+
const storageVersion = storage.status === "current"
|
|
180
|
+
|| storage.status === "upgradeable"
|
|
181
|
+
|| storage.status === "unsupported"
|
|
182
|
+
? storage.currentVersion
|
|
183
|
+
: UNSUPPORTED;
|
|
214
184
|
const statePath = join(home, "state.json");
|
|
215
185
|
const statePresent = ports.exists(statePath);
|
|
216
186
|
const stateBytes = ports.fileSize(statePath);
|
|
@@ -223,7 +193,7 @@ export function collectStorageIdentity(home, ports = createProductionStorageIden
|
|
|
223
193
|
const workerEnabled = resolveStoreWorkerEnabledForHome(home, ports.env);
|
|
224
194
|
const dbHealth = dbPresent ? ports.probeDatabaseHealth(dbPath) : null;
|
|
225
195
|
const findings = [];
|
|
226
|
-
if (
|
|
196
|
+
if (storage.status === "current" && !dbPresent) {
|
|
227
197
|
findings.push({
|
|
228
198
|
code: "current-database-missing",
|
|
229
199
|
severity: "contradiction",
|
|
@@ -231,20 +201,48 @@ export function collectStorageIdentity(home, ports = createProductionStorageIden
|
|
|
231
201
|
remediation: "Preserve this Home for diagnosis and initialize a new Home."
|
|
232
202
|
});
|
|
233
203
|
}
|
|
234
|
-
if (
|
|
204
|
+
if (storage.status === "uninitialized") {
|
|
205
|
+
findings.push({
|
|
206
|
+
code: "storage-uninitialized",
|
|
207
|
+
severity: "needs-repair",
|
|
208
|
+
message: "Yui storage has not been initialized for this Home.",
|
|
209
|
+
remediation: "Run `yui setup` for a new, empty Home."
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (storage.status === "upgradeable") {
|
|
213
|
+
findings.push({
|
|
214
|
+
code: "storage-upgrade-required",
|
|
215
|
+
severity: "needs-repair",
|
|
216
|
+
message: `Storage version ${storage.currentVersion} requires migration to `
|
|
217
|
+
+ `${CURRENT_STORAGE_VERSION}.`,
|
|
218
|
+
remediation: "Run `yui upgrade` or `yui update` before resuming writes."
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (storage.status === "unsupported") {
|
|
235
222
|
findings.push({
|
|
236
223
|
code: "unsupported-storage-contract",
|
|
237
224
|
severity: "contradiction",
|
|
238
|
-
message:
|
|
239
|
-
|
|
225
|
+
message: `Storage version ${storage.currentVersion} is outside the supported migration range `
|
|
226
|
+
+ `${MIN_SUPPORTED_STORAGE_VERSION}..${CURRENT_STORAGE_VERSION}.`,
|
|
227
|
+
remediation: storage.direction === "newer"
|
|
228
|
+
? "Use a newer Yui release."
|
|
229
|
+
: "Use a Yui release whose migration floor includes this Home."
|
|
240
230
|
});
|
|
241
231
|
}
|
|
242
|
-
if (
|
|
232
|
+
if (storage.status === "invalid") {
|
|
243
233
|
findings.push({
|
|
244
|
-
code: "invalid-storage
|
|
234
|
+
code: "invalid-storage",
|
|
245
235
|
severity: "contradiction",
|
|
246
|
-
message:
|
|
247
|
-
remediation: "Preserve this Home for diagnosis and
|
|
236
|
+
message: storage.detail,
|
|
237
|
+
remediation: "Preserve this Home for diagnosis and restore a known-good backup."
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (ports.exists(join(home, "schema.json"))) {
|
|
241
|
+
findings.push({
|
|
242
|
+
code: "ignored-legacy-storage-manifest",
|
|
243
|
+
severity: "warning",
|
|
244
|
+
message: "schema.json is legacy metadata and is not a storage authority.",
|
|
245
|
+
remediation: "A successful storage upgrade removes it automatically."
|
|
248
246
|
});
|
|
249
247
|
}
|
|
250
248
|
if (dbPresent && dbHealth !== null && dbHealth !== "ok") {
|
|
@@ -274,9 +272,9 @@ export function collectStorageIdentity(home, ports = createProductionStorageIden
|
|
|
274
272
|
}
|
|
275
273
|
return {
|
|
276
274
|
home,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
275
|
+
storageStatus: storage.status,
|
|
276
|
+
storageVersion,
|
|
277
|
+
minimumStorageVersion: MIN_SUPPORTED_STORAGE_VERSION,
|
|
280
278
|
configuredBackend,
|
|
281
279
|
workerEnabled,
|
|
282
280
|
physicalStateJson: {
|
|
@@ -484,7 +484,7 @@ function validatePointer(value) {
|
|
|
484
484
|
function validateRuntimeIdentity(value) {
|
|
485
485
|
if (value === null
|
|
486
486
|
|| typeof value !== "object"
|
|
487
|
-
|| value.schemaVersion !==
|
|
487
|
+
|| value.schemaVersion !== 2
|
|
488
488
|
|| typeof value.version !== "string"
|
|
489
489
|
|| typeof value.executablePath !== "string"
|
|
490
490
|
|| value.executablePath === ""
|
|
@@ -492,6 +492,11 @@ function validateRuntimeIdentity(value) {
|
|
|
492
492
|
|| typeof value.buildId !== "string"
|
|
493
493
|
|| typeof value.cliRealpath !== "string"
|
|
494
494
|
|| typeof value.controllerRealpath !== "string"
|
|
495
|
+
|| !isPositiveInteger(value.controllerProtocolVersion)
|
|
496
|
+
|| !isPositiveInteger(value.storageVersion)
|
|
497
|
+
|| !isPositiveInteger(value.minimumStorageVersion)
|
|
498
|
+
|| value.minimumStorageVersion
|
|
499
|
+
> value.storageVersion
|
|
495
500
|
|| typeof value.pid !== "number"
|
|
496
501
|
|| typeof value.processStartIdentity !== "string"
|
|
497
502
|
|| (value.mode !== "primary"
|
|
@@ -520,6 +525,9 @@ function isHandoverPhase(value) {
|
|
|
520
525
|
function isStringArray(value) {
|
|
521
526
|
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
522
527
|
}
|
|
528
|
+
function isPositiveInteger(value) {
|
|
529
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
530
|
+
}
|
|
523
531
|
function isEexist(error) {
|
|
524
532
|
return isNodeError(error) && error.code === "EEXIST";
|
|
525
533
|
}
|
|
@@ -17,6 +17,7 @@ import { persistRuntimeProcessExitObservation, replayRuntimeProcessExitOutbox }
|
|
|
17
17
|
import { readRuntimeStopReceipt, removeRuntimeStopReceipt } from "./runtimeStopReceipt.js";
|
|
18
18
|
import { AGENT_HOST_CONTROL_TIMEOUT_MS, AGENT_HOST_READY_TIMEOUT_MS } from "./runtimeDeadlines.js";
|
|
19
19
|
import { serializeAgentErrorRaw } from "./agentError.js";
|
|
20
|
+
import { runCodexInteractiveHost } from "./codexInteractiveHost.js";
|
|
20
21
|
export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v4";
|
|
21
22
|
const HOST_CONTROL_MAX_BYTES = 32 * 1024;
|
|
22
23
|
const CODEX_CLIENT_STABLE_MS = 5_000;
|
|
@@ -28,6 +29,11 @@ export async function runAgentHost(input) {
|
|
|
28
29
|
const hostInstanceId = randomUUID();
|
|
29
30
|
let hostSequence = 0;
|
|
30
31
|
let payload = await redeem(input.home, input.runtimeGenerationId, input.ticket);
|
|
32
|
+
if (payload.environment.YUI_SESSION_SCOPE === "global"
|
|
33
|
+
&& payload.environment.YUI_ADAPTER_ID === "codex"
|
|
34
|
+
&& payload.providerControl === undefined) {
|
|
35
|
+
return runCodexInteractiveHost(input.home, payload);
|
|
36
|
+
}
|
|
31
37
|
let session;
|
|
32
38
|
let sessionPayload;
|
|
33
39
|
let activeTurnPayload;
|
|
@@ -882,6 +888,7 @@ export async function waitForAgentHostLaunchAck(input) {
|
|
|
882
888
|
const code = error.code;
|
|
883
889
|
if (code !== "ENOENT" && code !== "ECONNREFUSED")
|
|
884
890
|
throw error;
|
|
891
|
+
await input.assertHostRunning?.();
|
|
885
892
|
}
|
|
886
893
|
await new Promise((resolvePromise) => setTimeout(resolvePromise, 50));
|
|
887
894
|
}
|