@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,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { basename, dirname, join } from "node:path";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
3
4
|
import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation } from "../../runtime/runtimeObservation.js";
|
|
4
5
|
import { RUNTIME_PROCESS_EXIT_TASK_EVENT, validateRuntimeProcessExitObservation } from "../../runtime/processExitObservation.js";
|
|
5
6
|
import { validateAgentProfile } from "../../profile/agentProfile.js";
|
|
@@ -8,56 +9,168 @@ import { validateReviewRound } from "../../review/reviewRound.js";
|
|
|
8
9
|
import { validateTurn } from "../../turn/turn.js";
|
|
9
10
|
import { validateWorkItem } from "../../workItem/workItem.js";
|
|
10
11
|
import { SqliteTaskStore } from "../sqliteStore.js";
|
|
11
|
-
import {
|
|
12
|
-
|
|
12
|
+
import { migrateSqliteSchema, storageMigrationPlan } from "../sqliteSchema.js";
|
|
13
|
+
import { CURRENT_DATABASE_FILENAME, inspectStorageSchema } from "../storageSchema.js";
|
|
14
|
+
import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "../storageVersions.js";
|
|
13
15
|
/**
|
|
14
|
-
*
|
|
16
|
+
* Upgrade one valid Home through the complete linear migration chain.
|
|
15
17
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
18
|
+
* Supported earlier versions are readable only here. Ordinary stores still
|
|
19
|
+
* admit exactly {@link CURRENT_STORAGE_VERSION}; no old-shape normalizer or
|
|
20
|
+
* dual read path enters runtime code. Execute mode is an offline primitive:
|
|
21
|
+
* its caller must own the maintenance fence and keep the Controller quiesced
|
|
22
|
+
* for the whole backup, migration, and validation interval.
|
|
19
23
|
*/
|
|
20
24
|
export async function runStorageUpgrade(options) {
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
return blocked({ ...
|
|
25
|
+
const state = inspectStorageSchema(options.home);
|
|
26
|
+
if (state.status === "uninitialized") {
|
|
27
|
+
return blocked({ ...corruptedClassification("Storage is not initialized."), uninitialized: true }, "uninitialized", "Yui storage is not initialized for this Home.", "Run `yui setup` with a new Home.");
|
|
24
28
|
}
|
|
25
|
-
if (
|
|
26
|
-
return blocked(corruptedClassification(
|
|
29
|
+
if (state.status === "invalid") {
|
|
30
|
+
return blocked(corruptedClassification(state.detail), "corruption", `Storage is invalid: ${state.detail}`, "Preserve this Home for diagnosis and restore it from a known-good backup.");
|
|
27
31
|
}
|
|
28
|
-
if (
|
|
29
|
-
|
|
32
|
+
if (state.status === "unsupported") {
|
|
33
|
+
const classification = unsupportedClassification(state);
|
|
34
|
+
return blocked(classification, "unsupported", classification.classification.verdict === "NEEDS_NEW_VERSION"
|
|
35
|
+
? classification.classification.blocker.message
|
|
36
|
+
: "Storage is unsupported.", classification.classification.verdict === "NEEDS_NEW_VERSION"
|
|
37
|
+
? classification.classification.blocker.action
|
|
38
|
+
: "Use a compatible Yui release.");
|
|
30
39
|
}
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
if (state.status === "current") {
|
|
41
|
+
try {
|
|
42
|
+
validateCurrentStore(options.home);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return blocked(corruptedClassification(messageOf(error)), "corruption", `Current storage validation failed: ${messageOf(error)}`, "Preserve this Home for diagnosis and restore it from a known-good backup.");
|
|
46
|
+
}
|
|
47
|
+
const classification = currentClassification();
|
|
48
|
+
if (options.mode === "update-preflight") {
|
|
49
|
+
return {
|
|
50
|
+
outcome: "update-preflight",
|
|
51
|
+
status: "already-current",
|
|
52
|
+
stepCount: 0,
|
|
53
|
+
steps: [],
|
|
54
|
+
classification
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
outcome: "already-current",
|
|
59
|
+
classification,
|
|
60
|
+
report: {
|
|
61
|
+
outcome: "already-current",
|
|
62
|
+
mode: options.mode,
|
|
63
|
+
sourceVersion: CURRENT_STORAGE_VERSION,
|
|
64
|
+
targetVersion: CURRENT_STORAGE_VERSION,
|
|
65
|
+
steps: []
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const plan = storageMigrationPlan(state.currentVersion);
|
|
70
|
+
if (plan === null) {
|
|
71
|
+
return blocked(corruptedClassification(`No complete migration path exists from ${state.currentVersion} `
|
|
72
|
+
+ `to ${CURRENT_STORAGE_VERSION}.`), "corruption", "The storage migration registry is incomplete.", "Install a Yui release that carries the complete migration chain.");
|
|
73
|
+
}
|
|
74
|
+
const classification = migratableClassification(state.currentVersion);
|
|
75
|
+
if (options.mode === "update-preflight") {
|
|
76
|
+
return {
|
|
77
|
+
outcome: "update-preflight",
|
|
78
|
+
status: "migration-ready",
|
|
79
|
+
stepCount: plan.length,
|
|
80
|
+
steps: plan,
|
|
81
|
+
classification
|
|
82
|
+
};
|
|
36
83
|
}
|
|
84
|
+
if (options.mode === "dry-run") {
|
|
85
|
+
return {
|
|
86
|
+
outcome: "upgrade-plan",
|
|
87
|
+
classification,
|
|
88
|
+
report: {
|
|
89
|
+
outcome: "upgrade-plan",
|
|
90
|
+
mode: "dry-run",
|
|
91
|
+
sourceVersion: state.currentVersion,
|
|
92
|
+
targetVersion: CURRENT_STORAGE_VERSION,
|
|
93
|
+
steps: plan
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
let backupPath;
|
|
37
98
|
try {
|
|
99
|
+
backupPath = await createDatabaseBackup(options.home, state.currentVersion, options.now ?? new Date());
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return {
|
|
103
|
+
outcome: "failed",
|
|
104
|
+
stage: "backup",
|
|
105
|
+
message: `Storage backup failed: ${messageOf(error)}`,
|
|
106
|
+
action: "Storage was not modified. Resolve the backup path, permissions, or free-space problem "
|
|
107
|
+
+ "and rerun `yui upgrade`.",
|
|
108
|
+
classification,
|
|
109
|
+
sceneUnchanged: true
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
let migrationCommitted = false;
|
|
113
|
+
try {
|
|
114
|
+
const database = new Database(join(options.home, CURRENT_DATABASE_FILENAME));
|
|
115
|
+
try {
|
|
116
|
+
database.pragma("journal_mode = WAL");
|
|
117
|
+
database.pragma("synchronous = FULL");
|
|
118
|
+
database.pragma("foreign_keys = ON");
|
|
119
|
+
database.pragma("busy_timeout = 5000");
|
|
120
|
+
migrateSqliteSchema(database, { mode: "apply" });
|
|
121
|
+
migrationCommitted = true;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
database.close();
|
|
125
|
+
}
|
|
38
126
|
validateCurrentStore(options.home);
|
|
127
|
+
rmSync(join(options.home, "schema.json"), { force: true });
|
|
39
128
|
}
|
|
40
129
|
catch (error) {
|
|
41
|
-
|
|
130
|
+
const restoration = migrationCommitted
|
|
131
|
+
? tryRestoreDatabaseBackup(options.home, backupPath)
|
|
132
|
+
: { restored: true };
|
|
133
|
+
return {
|
|
134
|
+
outcome: "failed",
|
|
135
|
+
stage: "migration",
|
|
136
|
+
message: `Storage migration failed: ${messageOf(error)}`,
|
|
137
|
+
action: restoration.restored
|
|
138
|
+
? "The original database was restored from the timestamped backup. "
|
|
139
|
+
+ "Resolve the reported problem and rerun `yui upgrade`."
|
|
140
|
+
: "Automatic restore also failed. Keep the Home quiesced and restore "
|
|
141
|
+
+ `${backupPath} manually before retrying. Restore error: ${restoration.error}`,
|
|
142
|
+
backupPath,
|
|
143
|
+
classification,
|
|
144
|
+
sceneUnchanged: restoration.restored
|
|
145
|
+
};
|
|
42
146
|
}
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
147
|
+
const finalState = inspectStorageSchema(options.home);
|
|
148
|
+
if (finalState.status !== "current") {
|
|
149
|
+
const restoration = tryRestoreDatabaseBackup(options.home, backupPath);
|
|
45
150
|
return {
|
|
46
|
-
outcome: "
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
151
|
+
outcome: "failed",
|
|
152
|
+
stage: "migration",
|
|
153
|
+
message: `Storage migration did not reach version ${CURRENT_STORAGE_VERSION}.`,
|
|
154
|
+
action: restoration.restored
|
|
155
|
+
? "The original database was restored from the timestamped backup. "
|
|
156
|
+
+ "Inspect the migration registry before retrying."
|
|
157
|
+
: "Automatic restore also failed. Keep the Home quiesced and restore "
|
|
158
|
+
+ `${backupPath} manually before retrying. Restore error: ${restoration.error}`,
|
|
159
|
+
backupPath,
|
|
160
|
+
classification,
|
|
161
|
+
sceneUnchanged: restoration.restored
|
|
51
162
|
};
|
|
52
163
|
}
|
|
53
164
|
return {
|
|
54
|
-
outcome: "
|
|
55
|
-
classification,
|
|
165
|
+
outcome: "upgraded",
|
|
166
|
+
classification: currentClassification(),
|
|
56
167
|
report: {
|
|
57
|
-
outcome: "
|
|
58
|
-
mode:
|
|
59
|
-
|
|
60
|
-
|
|
168
|
+
outcome: "upgraded",
|
|
169
|
+
mode: "execute",
|
|
170
|
+
sourceVersion: state.currentVersion,
|
|
171
|
+
targetVersion: CURRENT_STORAGE_VERSION,
|
|
172
|
+
steps: plan,
|
|
173
|
+
backupPath
|
|
61
174
|
}
|
|
62
175
|
};
|
|
63
176
|
}
|
|
@@ -96,59 +209,108 @@ function validateCurrentStore(home) {
|
|
|
96
209
|
}
|
|
97
210
|
}
|
|
98
211
|
}
|
|
212
|
+
const quickCheck = store.databaseHandle().pragma("quick_check", { simple: true });
|
|
213
|
+
if (quickCheck !== "ok") {
|
|
214
|
+
throw new Error(`SQLite quick_check failed: ${String(quickCheck)}.`);
|
|
215
|
+
}
|
|
99
216
|
}
|
|
100
217
|
finally {
|
|
101
218
|
store.close();
|
|
102
219
|
}
|
|
103
220
|
}
|
|
104
|
-
function
|
|
221
|
+
async function createDatabaseBackup(home, sourceVersion, now) {
|
|
222
|
+
const source = join(home, CURRENT_DATABASE_FILENAME);
|
|
223
|
+
if (!existsSync(source))
|
|
224
|
+
throw new Error("The authoritative yui.db is missing.");
|
|
225
|
+
const backupDirectory = join(dirname(home), `${basename(home)}-backups`);
|
|
226
|
+
mkdirSync(backupDirectory, { recursive: true, mode: 0o700 });
|
|
227
|
+
const timestamp = now.toISOString().replaceAll(":", "-");
|
|
228
|
+
const backupPath = join(backupDirectory, `pre-storage-v${sourceVersion}-${timestamp}.db`);
|
|
229
|
+
const database = new Database(source, { readonly: true, fileMustExist: true });
|
|
230
|
+
try {
|
|
231
|
+
await database.backup(backupPath);
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
database.close();
|
|
235
|
+
}
|
|
236
|
+
return backupPath;
|
|
237
|
+
}
|
|
238
|
+
function restoreDatabaseBackup(home, backupPath) {
|
|
239
|
+
const databasePath = join(home, CURRENT_DATABASE_FILENAME);
|
|
240
|
+
rmSync(`${databasePath}-wal`, { force: true });
|
|
241
|
+
rmSync(`${databasePath}-shm`, { force: true });
|
|
242
|
+
copyFileSync(backupPath, databasePath);
|
|
243
|
+
}
|
|
244
|
+
function tryRestoreDatabaseBackup(home, backupPath) {
|
|
245
|
+
try {
|
|
246
|
+
restoreDatabaseBackup(home, backupPath);
|
|
247
|
+
return { restored: true };
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
return { restored: false, error: messageOf(error) };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function currentClassification() {
|
|
105
254
|
return {
|
|
106
255
|
classification: { verdict: "USABLE", status: "current" },
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
latestAggregateVersion: latest.aggregate
|
|
256
|
+
storageVersion: CURRENT_STORAGE_VERSION,
|
|
257
|
+
currentStorageVersion: CURRENT_STORAGE_VERSION,
|
|
258
|
+
minimumSupportedStorageVersion: MIN_SUPPORTED_STORAGE_VERSION
|
|
111
259
|
};
|
|
112
260
|
}
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
261
|
+
function migratableClassification(storageVersion) {
|
|
262
|
+
return {
|
|
263
|
+
classification: { verdict: "MIGRATABLE", status: "migration-ready" },
|
|
264
|
+
storageVersion,
|
|
265
|
+
currentStorageVersion: CURRENT_STORAGE_VERSION,
|
|
266
|
+
minimumSupportedStorageVersion: MIN_SUPPORTED_STORAGE_VERSION
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function unsupportedClassification(state) {
|
|
270
|
+
const future = state.direction === "newer";
|
|
271
|
+
const message = future
|
|
272
|
+
? `Storage version ${state.currentVersion} is newer than this CLI supports `
|
|
273
|
+
+ `(${CURRENT_STORAGE_VERSION}).`
|
|
274
|
+
: `Storage version ${state.currentVersion} is older than the minimum supported `
|
|
275
|
+
+ `migration version ${MIN_SUPPORTED_STORAGE_VERSION}.`;
|
|
118
276
|
const action = future
|
|
119
|
-
? "Use a newer Yui release
|
|
120
|
-
: "
|
|
277
|
+
? "Use a newer Yui release."
|
|
278
|
+
: "Preserve this Home for use with its matching historical Yui release, "
|
|
279
|
+
+ "or initialize a new Home with Yui 0.15.0 or later.";
|
|
121
280
|
return {
|
|
122
281
|
classification: {
|
|
123
282
|
verdict: "NEEDS_NEW_VERSION",
|
|
124
283
|
status: "unsupported",
|
|
125
284
|
blocker: {
|
|
126
|
-
reason: future ? "future-version" : "
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
message
|
|
131
|
-
? "The Home is newer than this Yui release."
|
|
132
|
-
: "This release provides no migration path for the older Home.",
|
|
285
|
+
reason: future ? "future-version" : "below-minimum",
|
|
286
|
+
found: state.currentVersion,
|
|
287
|
+
current: CURRENT_STORAGE_VERSION,
|
|
288
|
+
minimum: MIN_SUPPORTED_STORAGE_VERSION,
|
|
289
|
+
message,
|
|
133
290
|
action
|
|
134
291
|
}
|
|
135
292
|
},
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
latestAggregateVersion: latest.aggregate,
|
|
140
|
-
incompatibleComponent: axis
|
|
293
|
+
storageVersion: state.currentVersion,
|
|
294
|
+
currentStorageVersion: CURRENT_STORAGE_VERSION,
|
|
295
|
+
minimumSupportedStorageVersion: MIN_SUPPORTED_STORAGE_VERSION
|
|
141
296
|
};
|
|
142
297
|
}
|
|
143
|
-
function corruptedClassification(
|
|
298
|
+
function corruptedClassification(detail) {
|
|
144
299
|
return {
|
|
145
300
|
classification: { verdict: "CORRUPTED", status: "unsupported", detail },
|
|
146
|
-
|
|
147
|
-
|
|
301
|
+
currentStorageVersion: CURRENT_STORAGE_VERSION,
|
|
302
|
+
minimumSupportedStorageVersion: MIN_SUPPORTED_STORAGE_VERSION
|
|
148
303
|
};
|
|
149
304
|
}
|
|
150
305
|
function blocked(classification, stage, message, action) {
|
|
151
|
-
return {
|
|
306
|
+
return {
|
|
307
|
+
outcome: "blocked",
|
|
308
|
+
stage,
|
|
309
|
+
message,
|
|
310
|
+
action,
|
|
311
|
+
classification,
|
|
312
|
+
sceneUnchanged: true
|
|
313
|
+
};
|
|
152
314
|
}
|
|
153
315
|
function messageOf(error) {
|
|
154
316
|
return error instanceof Error ? error.message : String(error);
|
package/dist/tmux/tmuxManager.js
CHANGED
|
@@ -79,10 +79,23 @@ export class TmuxManager {
|
|
|
79
79
|
}
|
|
80
80
|
ensureRoleWindow(taskId, role, launch) {
|
|
81
81
|
if (this.windowNames(taskId).includes(role.name)) {
|
|
82
|
+
const pane = this.inspectPane(taskId, role.name);
|
|
83
|
+
if (pane.dead && launch === undefined) {
|
|
84
|
+
throw runtimeError(`Role ${role.name} has an exited-retained pane; a launch plan is required to restart it.`);
|
|
85
|
+
}
|
|
82
86
|
this.recordRoleTarget(taskId, role.name);
|
|
83
87
|
this.configureServerHistory();
|
|
88
|
+
if (pane.dead) {
|
|
89
|
+
// Without -k tmux refuses to replace a live process, even if the pane
|
|
90
|
+
// changed after inspection. Reads alone never discard the exit scene.
|
|
91
|
+
this.run([
|
|
92
|
+
"respawn-window", "-t", this.exactTarget(taskId, role.name),
|
|
93
|
+
"-c", safeValue(role.cwd ?? role.workspace, "Role cwd"),
|
|
94
|
+
"--", ...launchCommand(launch)
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
84
97
|
this.configureRoleWindowSizing(taskId, role.name);
|
|
85
|
-
return
|
|
98
|
+
return pane.dead;
|
|
86
99
|
}
|
|
87
100
|
if (launch === undefined) {
|
|
88
101
|
throw runtimeError(`Agent launch plan is required to create Role window: ${role.name}.`);
|
|
@@ -127,10 +140,21 @@ export class TmuxManager {
|
|
|
127
140
|
async ensureRoleWindowAsync(taskId, role, launch) {
|
|
128
141
|
const snapshot = await this.sessionWindowNamesAsync(taskId);
|
|
129
142
|
if (snapshot.names.includes(role.name)) {
|
|
143
|
+
const pane = await this.inspectPaneAsync(taskId, role.name);
|
|
144
|
+
if (pane.dead && launch === undefined) {
|
|
145
|
+
throw runtimeError(`Role ${role.name} has an exited-retained pane; a launch plan is required to restart it.`);
|
|
146
|
+
}
|
|
130
147
|
this.recordRoleTarget(taskId, role.name);
|
|
131
148
|
await this.configureServerHistoryAsync();
|
|
149
|
+
if (pane.dead) {
|
|
150
|
+
await this.runAsync([
|
|
151
|
+
"respawn-window", "-t", this.exactTarget(taskId, role.name),
|
|
152
|
+
"-c", safeValue(role.cwd ?? role.workspace, "Role cwd"),
|
|
153
|
+
"--", ...launchCommand(launch)
|
|
154
|
+
]);
|
|
155
|
+
}
|
|
132
156
|
await this.configureRoleWindowSizingAsync(taskId, role.name);
|
|
133
|
-
return
|
|
157
|
+
return pane.dead;
|
|
134
158
|
}
|
|
135
159
|
if (launch === undefined) {
|
|
136
160
|
throw runtimeError(`Agent launch plan is required to create Role window: ${role.name}.`);
|
|
@@ -364,7 +388,7 @@ export class TmuxManager {
|
|
|
364
388
|
inspectPane(taskId, roleName) {
|
|
365
389
|
const target = this.target(taskId, roleName);
|
|
366
390
|
const output = this.run([
|
|
367
|
-
"display-message", "-p", "-t",
|
|
391
|
+
"display-message", "-p", "-t", this.exactTarget(taskId, roleName),
|
|
368
392
|
[
|
|
369
393
|
"#{pane_dead}",
|
|
370
394
|
"#{pane_pid}",
|
|
@@ -379,6 +403,9 @@ export class TmuxManager {
|
|
|
379
403
|
throw runtimeError(`Tmux returned an invalid pane state for ${roleName}.`);
|
|
380
404
|
}
|
|
381
405
|
const dead = output.slice(0, separator);
|
|
406
|
+
if (dead !== "0" && dead !== "1") {
|
|
407
|
+
throw runtimeError(`Tmux returned an invalid pane state for ${roleName}.`);
|
|
408
|
+
}
|
|
382
409
|
const pidText = output.slice(separator + 1, secondSeparator);
|
|
383
410
|
const currentCommand = output.slice(secondSeparator + 1, lastSeparator);
|
|
384
411
|
const exitStatusText = output.slice(lastSeparator + 1);
|
|
@@ -405,7 +432,7 @@ export class TmuxManager {
|
|
|
405
432
|
: deliveryReceiptOption(receiptId);
|
|
406
433
|
const receiptFormat = receiptOption === undefined ? "" : `#{${receiptOption}}`;
|
|
407
434
|
const output = await this.runAsync([
|
|
408
|
-
"display-message", "-p", "-t",
|
|
435
|
+
"display-message", "-p", "-t", this.exactTarget(taskId, roleName),
|
|
409
436
|
[
|
|
410
437
|
PANE_STATE_MARKER,
|
|
411
438
|
"#{pane_dead}",
|
|
@@ -798,22 +825,16 @@ export class TmuxManager {
|
|
|
798
825
|
}
|
|
799
826
|
this.enterRole(taskId, role, launch);
|
|
800
827
|
}
|
|
801
|
-
detectRoleStatus(taskId, roleName, fallback = "exited") {
|
|
802
|
-
try {
|
|
803
|
-
return this.probeRoleStatus(taskId, roleName);
|
|
804
|
-
}
|
|
805
|
-
catch {
|
|
806
|
-
return fallback;
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
828
|
probeRoleStatus(taskId, roleName) {
|
|
810
|
-
if (!this.
|
|
829
|
+
if (!this.windowNames(taskId).includes(roleName))
|
|
811
830
|
return "exited";
|
|
812
|
-
return this.
|
|
831
|
+
return this.inspectPane(taskId, roleName).dead ? "exited" : "running";
|
|
813
832
|
}
|
|
814
833
|
async probeRoleStatusAsync(taskId, roleName) {
|
|
815
834
|
const snapshot = await this.sessionWindowNamesAsync(taskId);
|
|
816
|
-
|
|
835
|
+
if (!snapshot.names.includes(roleName))
|
|
836
|
+
return "exited";
|
|
837
|
+
return (await this.inspectPaneAsync(taskId, roleName)).dead ? "exited" : "running";
|
|
817
838
|
}
|
|
818
839
|
/** Exact pane process state for one Role, used by owner-identity recording. */
|
|
819
840
|
inspectRolePane(taskId, roleName) {
|
|
@@ -901,10 +922,6 @@ export class TmuxManager {
|
|
|
901
922
|
catch (error) {
|
|
902
923
|
if (isExplicitlyAbsentTmuxSession(error))
|
|
903
924
|
return false;
|
|
904
|
-
// Older/fake executors do not expose stderr. A failed has-session is the
|
|
905
|
-
// tmux absence contract; other operations remain strict.
|
|
906
|
-
if (!(error instanceof CommandExecutionError))
|
|
907
|
-
return false;
|
|
908
925
|
throw error;
|
|
909
926
|
}
|
|
910
927
|
}
|
|
@@ -916,8 +933,6 @@ export class TmuxManager {
|
|
|
916
933
|
catch (error) {
|
|
917
934
|
if (isExplicitlyAbsentTmuxSession(error))
|
|
918
935
|
return false;
|
|
919
|
-
if (!(error instanceof CommandExecutionError))
|
|
920
|
-
return false;
|
|
921
936
|
throw error;
|
|
922
937
|
}
|
|
923
938
|
}
|
|
@@ -930,7 +945,7 @@ export class TmuxManager {
|
|
|
930
945
|
]).split("\n").map((name) => name.trim()).filter(Boolean);
|
|
931
946
|
}
|
|
932
947
|
catch (error) {
|
|
933
|
-
if (
|
|
948
|
+
if (isExplicitlyAbsentTmuxSession(error))
|
|
934
949
|
return [];
|
|
935
950
|
throw error;
|
|
936
951
|
}
|
|
@@ -943,7 +958,7 @@ export class TmuxManager {
|
|
|
943
958
|
return { exists: true, names };
|
|
944
959
|
}
|
|
945
960
|
catch (error) {
|
|
946
|
-
if (
|
|
961
|
+
if (isExplicitlyAbsentTmuxSession(error))
|
|
947
962
|
return { exists: false, names: [] };
|
|
948
963
|
throw error;
|
|
949
964
|
}
|
|
@@ -990,6 +1005,11 @@ export class TmuxManager {
|
|
|
990
1005
|
target(taskId, roleName) {
|
|
991
1006
|
return yuiTmuxTarget(this.#yuiHome, taskId, roleName);
|
|
992
1007
|
}
|
|
1008
|
+
exactTarget(taskId, roleName) {
|
|
1009
|
+
// tmux otherwise accepts prefix/glob matches after the selected Role
|
|
1010
|
+
// disappears. Keep identity inspection and dead-pane replacement exact.
|
|
1011
|
+
return `=${this.sessionName(taskId)}:=${safeValue(roleName, "Role name")}`;
|
|
1012
|
+
}
|
|
993
1013
|
recordRoleTarget(taskId, roleName) {
|
|
994
1014
|
this.#onRoleTargetRecorded?.(this.target(taskId, roleName));
|
|
995
1015
|
}
|
|
@@ -1225,11 +1245,6 @@ function isExplicitlyAbsentTmuxSession(error) {
|
|
|
1225
1245
|
return /can't find (?:session|window|pane)|no server running|no current target|session not found|error connecting to .+ \(No such file or directory\)/i
|
|
1226
1246
|
.test(error.stderr);
|
|
1227
1247
|
}
|
|
1228
|
-
function isUnavailableTmuxStatus(error) {
|
|
1229
|
-
return isExplicitlyAbsentTmuxSession(error)
|
|
1230
|
-
|| (error instanceof CommandExecutionError
|
|
1231
|
-
&& /server exited unexpectedly/i.test(error.stderr));
|
|
1232
|
-
}
|
|
1233
1248
|
function tmuxWord(value) {
|
|
1234
1249
|
return JSON.stringify(value);
|
|
1235
1250
|
}
|
package/dist/version.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { FILE_TASK_CONTROLLER_PROTOCOL_VERSION } from "./core/protocol.js";
|
|
3
|
-
import {
|
|
3
|
+
import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "./storage/storageVersions.js";
|
|
4
4
|
export const YUI_VERSION = readPackageVersion();
|
|
5
5
|
export function yuiVersionIdentity() {
|
|
6
6
|
return {
|
|
7
7
|
version: YUI_VERSION,
|
|
8
8
|
controllerProtocolVersion: FILE_TASK_CONTROLLER_PROTOCOL_VERSION,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
storageVersion: CURRENT_STORAGE_VERSION,
|
|
10
|
+
minimumStorageVersion: MIN_SUPPORTED_STORAGE_VERSION
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
function readPackageVersion() {
|
|
@@ -51,12 +51,12 @@ There is no compatibility lookup, cross-Task guess, or bare-ID fallback.
|
|
|
51
51
|
|
|
52
52
|
## Current-schema boundary
|
|
53
53
|
|
|
54
|
-
Runtime opens only
|
|
55
|
-
dual-read
|
|
56
|
-
|
|
57
|
-
and
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
Runtime opens only the current Home storage version and current record shapes.
|
|
55
|
+
It does not dual-read or infer historical records during ordinary work.
|
|
56
|
+
Historical decoding and rewriting are confined to the explicit `yui upgrade`
|
|
57
|
+
boundary and the migration phase of `yui update`; every valid Home at or above
|
|
58
|
+
the CLI's minimum supported storage version can advance directly to current.
|
|
59
|
+
|
|
60
|
+
This boundary keeps Task-local references, Role desired configuration, and
|
|
61
|
+
immutable Turn/RoleSession effective snapshots under one unambiguous runtime
|
|
62
|
+
contract while the append-only migration chain preserves supported history.
|