@zq-silk/yui 0.14.2 → 0.15.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.
@@ -1,5 +1,6 @@
1
- import { existsSync } from "node:fs";
2
- import { join } from "node:path";
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 { inspectStorageSchema } from "../storageSchema.js";
12
- const CURRENT_DATABASE_FILENAME = "yui.db";
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
- * Validate the one supported storage contract.
16
+ * Upgrade one valid Home through the complete linear migration chain.
15
17
  *
16
- * Aggregate 31 deliberately has no historical migration path. `upgrade`
17
- * remains as a read-only compatibility/preflight surface for current Homes;
18
- * every non-current contract is rejected without touching the Home.
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 schema = inspectStorageSchema(options.home);
22
- if (schema.status === "uninitialized") {
23
- return blocked({ ...currentClassification(options.latest), uninitialized: true }, "uninitialized", "Yui storage is not initialized for this Home.", "Run `yui setup` with a new Home.");
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 (schema.status === "invalid") {
26
- return blocked(corruptedClassification(options.latest, schema.detail), "corruption", `Storage schema is invalid: ${schema.detail}`, "Preserve this Home for diagnosis and restore it from a known-good backup.");
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 (!existsSync(join(options.home, CURRENT_DATABASE_FILENAME))) {
29
- return blocked(corruptedClassification(options.latest, "The SQLite database is missing."), "corruption", "The SQLite Home is incomplete: yui.db is missing.", "Preserve this Home for diagnosis and restore it from a known-good backup.");
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 (schema.status === "unsupported") {
32
- const classification = unsupportedClassification(schema, options.latest);
33
- return blocked(classification, "unsupported", "This Home does not exactly match the current storage contract; this release provides no migration path.", schema.direction === "newer"
34
- ? "Use a newer Yui release that supports this exact Home."
35
- : "Open it with its matching Yui version, or initialize a new Home.");
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
- return blocked(corruptedClassification(options.latest, messageOf(error)), "corruption", `Current storage validation failed: ${messageOf(error)}`, "Preserve this Home for diagnosis and restore it from a known-good backup.");
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 classification = currentClassification(options.latest);
44
- if (options.mode === "update-preflight") {
147
+ const finalState = inspectStorageSchema(options.home);
148
+ if (finalState.status !== "current") {
149
+ const restoration = tryRestoreDatabaseBackup(options.home, backupPath);
45
150
  return {
46
- outcome: "update-preflight",
47
- status: "already-current",
48
- stepCount: 0,
49
- steps: [],
50
- classification
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: "already-current",
55
- classification,
165
+ outcome: "upgraded",
166
+ classification: currentClassification(),
56
167
  report: {
57
- outcome: "already-current",
58
- mode: options.mode,
59
- source: options.latest,
60
- target: options.latest
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 currentClassification(latest) {
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
- layoutVersion: latest.layout,
108
- aggregateVersion: latest.aggregate,
109
- latestLayoutVersion: latest.layout,
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 unsupportedClassification(schema, latest) {
114
- const axis = schema.incompatibleComponent;
115
- const found = schema.currentVersion;
116
- const supported = schema.latestVersion;
117
- const future = schema.direction === "newer";
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 that supports this exact Home."
120
- : "Open it with its matching Yui version, or initialize a new Home.";
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" : "missing-step",
127
- axis,
128
- ...(schema.recordFamily === undefined ? {} : { recordKind: schema.recordFamily }),
129
- ...(future ? { found, supported } : { from: found, to: supported }),
130
- message: future
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
- layoutVersion: schema.currentLayoutVersion,
137
- aggregateVersion: schema.currentAggregateSchemaVersion,
138
- latestLayoutVersion: latest.layout,
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(latest, detail) {
298
+ function corruptedClassification(detail) {
144
299
  return {
145
300
  classification: { verdict: "CORRUPTED", status: "unsupported", detail },
146
- latestLayoutVersion: latest.layout,
147
- latestAggregateVersion: latest.aggregate
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 { outcome: "blocked", stage, message, action, classification, sceneUnchanged: true };
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/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 { CURRENT_AGGREGATE_SCHEMA_VERSION, CURRENT_STORAGE_LAYOUT_VERSION } from "./storage/storageSchema.js";
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
- storageLayoutVersion: CURRENT_STORAGE_LAYOUT_VERSION,
10
- aggregateSchemaVersion: CURRENT_AGGREGATE_SCHEMA_VERSION
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 aggregate v31 / Task v7. It does not convert,
55
- dual-read, or infer records from an older schema. If an existing `YUI_HOME`
56
- does not match the current schema, keep it untouched for external archival
57
- and initialize a fresh home for this runtime.
58
-
59
- This hard cut keeps Task-local references, Role desired configuration, and
60
- immutable Turn/RoleSession effective snapshots under one unambiguous
61
- contract. There is no compatibility lookup, conversion command, or
62
- intermediate storage format.
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.
@@ -101,7 +101,11 @@ export YUI_HOME=/absolute/path/to/yui-home
101
101
  yui setup
102
102
  ```
103
103
 
104
- home 中包含 `schema.json`、权威 SQLite 数据库 `yui.db`、Project Catalog、项目知识和 Controller 发现文件。稳定 Project checkout 与受管理 worktree 位于 home 外部的 workspace。运行时只接受当前存储契约:不会回退读取 `state.json`、转换旧 schema 或猜测旧 ID。
104
+ home 中包含权威 SQLite 数据库 `yui.db`、Project Catalog、项目知识和
105
+ Controller 发现文件。稳定 Project checkout 与受管理 worktree 位于 home
106
+ 外部的 workspace。旧 `schema.json` 与 `state.json` 只作为历史证据存在,
107
+ 不再是版本权威。运行时只接受当前存储契约;支持区间内的早期存储版本只允许
108
+ 通过显式升级入口。
105
109
 
106
110
  所有 Task-owned 记录族都在各自 Task 内分配单调递增的本地 ID。因此,不同
107
111
  Task 可以同时拥有 `work-item-1`、`turn-1` 或 `input-1`。受管 Task
@@ -111,8 +115,12 @@ Task 的命令(例如 `task work create`、`task integration start`)仍使
111
115
  Task 内的本地子记录 ID。Candidate 只在所属 WorkItem 内递增,并同时保存
112
116
  Task 与 WorkItem provenance。
113
117
 
114
- Yui 只支持当前 aggregate-v31 / Task-v7 schema。旧 home 不提供转换、
115
- 双读或历史记录推断;需要使用新版本时初始化全新的 `YUI_HOME`。当前引用契约见
118
+ Yui Home 只有一个存储版本,权威值是 SQLite 中连续且校验和有效的迁移
119
+ ledger 头。CLI 同时公布当前存储版本和最小支持迁移版本。普通运行时代码只读取
120
+ 最新结构,不提供双读;`yui upgrade --dry-run` 只显示迁移计划,
121
+ `yui upgrade` 会停住正在运行的 Controller、创建一致性备份、在一个事务中执行
122
+ 全部缺失迁移并校验最新结构。处于支持区间内的任意历史 Home 都可以直接跨多个
123
+ 版本升级,无需逐个安装中间版本。当前引用契约见
116
124
  [Task 本地 ID](../docs/task-local-identity.md)。
117
125
 
118
126
  ## 快速开始
@@ -627,13 +635,13 @@ yui task role release <task-id> <role>
627
635
 
628
636
  Codex Role thread 可在 Desktop 中直接查看和操作;Desktop 已有 active Turn 时,Yui 只保留待投递工作并等待,不会失败或重复投递。`view`、`takeover`、`release` 继续作为 Claude 等独立进程 Provider 的人工控制入口。Yui 不写入全局 Hook/config,也不启动、重启或停止共享 daemon;Codex CLI/daemon 故障由 Task 生命周期之外修复。Global Operator 与 global Role 继续使用原生交互式 CLI,不属于受管理 Task Provider 协议;Yui 在内部将 Codex 的 Global TUI 连接到同一个默认 App Server,用户不能通过 Agent 或 Role 参数覆盖该连接,Session Manifest 自带不依赖启动进程环境的 Global Context 命令,因此同一 thread 可直接切换到 Desktop 继续对话。
629
637
 
630
- 当新版本需要离线迁移 Home 时,应等待当前 Turn 完成,然后从普通 shell
631
- 执行 `yui session stop --all`,再重新执行 `yui update`。停止命令会先整体预检:
632
- 只要仍有 Session 正在运行或存在未决生命周期工作,就不会开始停止;全部空闲
633
- 时会先阻止新的 Leader 调度,停止并等待 Controller 完全退出,重新检查运行时
634
- 事实后再停止 Task Role global Role Session。成功后 Controller 保持停止,
635
- 应紧接着执行 `yui update`。如果当前安装版本还没有这条命令,应手动退出提示中
636
- 列出的全部 managed Session;新的 staged CLI 不能写入尚待迁移的旧 Home。
638
+ `yui update` 会用目标版本先做只读预检,在停住精确的旧 Controller 后自动执行
639
+ 所需的离线迁移,再校验并启动新 Controller。若升级前希望结束所有 Agent
640
+ 活动,可先执行 `yui session stop --all`;这不是存储版本链的一部分。
641
+ Yui 0.15.0 建立 storage version 1 和迁移下限。更早版本(包括 0.14.2)
642
+ 创建的 Home 不在这条兼容链上:应保留给匹配的历史 Yui 版本查看,或者初始化
643
+ 新的 Home。从 0.15.0 开始,后续版本必须保留完整迁移链,因此可以由
644
+ `yui update` 直接跨版本升级。
637
645
 
638
646
  tmux 会在 pane 创建时固定其历史容量。配置该限制之前创建的 Role 会保留原容量;Yui 会在 Terminal attach 和 Web 中提示用户退出并重新进入一次,从而创建具有 100,000 行历史的新 pane。
639
647
 
@@ -675,10 +683,10 @@ yui controller restart
675
683
 
676
684
  `controller restart` 会用当前安装的 Yui 版本替换 Controller 进程及其调度循环、socket 服务,不会停止或重启已受管的 tmux/Agent 会话;普通 Session 命令按协议与存储身份兼容,不要求 Controller 与 CLI 包版本完全相同。
677
685
 
678
- 成功的 `setup`、`upgrade` 和 `update` 都会确保当前 Home 有一个运行中的
679
- Controller;如果之前没有运行,会在完成后启动。只读命令和
680
- `upgrade --dry-run` 不会启动 Controller。`update` 只有在新二进制健康检查通过后,
681
- 才会替换或启动 Controller。
686
+ 成功的 `setup` 和 `update` 会确保当前 Home 有一个运行中的 Controller。
687
+ `upgrade` 只会在迁移前存在 Controller 时恢复它;只读命令和
688
+ `upgrade --dry-run` 不会启动 Controller。`update` 只有在迁移和新二进制健康
689
+ 检查都通过后,才会替换或启动 Controller。
682
690
 
683
691
  恢复 reconciliation 默认每 120 秒执行一次。普通持久状态变化只会将 Task、Role 或 Operator key 放入队列并立即返回;固定 100ms 窗口内到达的 key 会合并触发一次不重叠的定向处理。Operator 呈现使用独立 lane,不会被 Task 的 Git/worktree 操作阻塞;周期 Git/worktree 处理只覆盖仍有持久 Task mailbox 工作的 Task,活动 Role 的存活检查合并为一次 tmux inventory。来自 Provider 原生事件或受支持 Hook 的结构化 Agent Driver observation,会经过精确 fence 后进入持久 runtime inbox。终态 Turn observation 会原子记录精确的 Turn 结果。持久 WorkMailbox 会冻结当前 processing 批次,期间的新事件合并到下一 pending 批次;失败会释放当前批次供恢复。推荐输入与 pending Turn 共用最近 deadline 选择器,不依赖恢复扫描间隔;显式 `task reconcile` 仍会立即请求恢复扫描。保留的闭环为:
684
692
 
@@ -724,6 +732,7 @@ Web 端可以通过与 Terminal 相同的持久化 CLI 路径回答 open InputRe
724
732
 
725
733
  ```sh
726
734
  yui update
735
+ yui upgrade [--dry-run]
727
736
  yui config agent add|list|show|capabilities|update|remove
728
737
  yui config role add|list|show|update|remove|bind|unbind
729
738
  yui config profile add|list|show|update|remove|reset
@@ -749,9 +758,9 @@ npm test
749
758
  npm run lint
750
759
  ```
751
760
 
752
- `npm test` 只保留秒级核心 smoke:CLI 启动、正常 SQLite Task、受支持迁移和内置
753
- Agent Driver。针对当前修改编写的 TDD、异常数据和故障复现仅作为开发期证据,需求完成后
754
- 删除,不累积为常驻回归测试。具体约束见
761
+ `npm test` 只保留秒级核心 smoke:CLI 启动、正常 SQLite Task、存储基线、
762
+ 目标驱动更新和内置 Agent Driver。针对当前修改编写的 TDD、异常数据和故障复现
763
+ 仅作为开发期证据,需求完成后删除,不累积为常驻回归测试。具体约束见
755
764
  [验证策略](../docs/testing/verification-levels.md)。
756
765
 
757
766
  如需让用户终端使用当前 checkout,可逆地接管用户级 `yui` 命令:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.14.2",
3
+ "version": "0.15.0",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -1,82 +0,0 @@
1
- /** Current record-family versions for the single SQLite storage contract. */
2
- import { CURRENT_AGGREGATE_SCHEMA_VERSION, CURRENT_STORAGE_LAYOUT_VERSION } from "../storageVersions.js";
3
- import { CURRENT_TURN_SCHEMA_VERSION, CURRENT_AGENT_PROFILE_SCHEMA_VERSION, CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION, CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION, CURRENT_CHANGE_SET_SCHEMA_VERSION, CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION, CURRENT_DECISION_SCHEMA_VERSION, CURRENT_EVENT_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_INPUT_REQUEST_SCHEMA_VERSION, CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION, CURRENT_INTEGRATION_QUEUE_SCHEMA_VERSION, CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION, CURRENT_MESSAGE_SCHEMA_VERSION, CURRENT_MILESTONE_SCHEMA_VERSION, CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION, CURRENT_PROJECT_SCHEMA_VERSION, CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION, CURRENT_REVIEW_ROUND_SCHEMA_VERSION, CURRENT_TASK_BRIEF_SCHEMA_VERSION, CURRENT_TASK_ROLE_SCHEMA_VERSION, CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_TASK_SCHEMA_VERSION, CURRENT_WORK_ITEM_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION } from "../taskStore.js";
4
- import { CURRENT_LEADER_FAILURE_SCHEMA_VERSION } from "../../scheduler/leaderFailure.js";
5
- import { CURRENT_TASK_WAKE_SCHEMA_VERSION } from "../../scheduler/taskWake.js";
6
- import { CURRENT_DURABLE_JOB_SCHEMA_VERSION } from "../../job/durableJob.js";
7
- function descriptor(kind, version) {
8
- return Object.freeze({ version, path: `sqlite:${kind}` });
9
- }
10
- // Lazy construction avoids the taskStore -> storageSchema -> recordVersions
11
- // initialization cycle while keeping one canonical family list.
12
- let currentDescriptors = null;
13
- function getCurrentRecordDescriptors() {
14
- if (currentDescriptors === null) {
15
- const versions = {
16
- config: CURRENT_CONFIG_SCHEMA_VERSION,
17
- configuredAgent: CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION,
18
- project: CURRENT_PROJECT_SCHEMA_VERSION,
19
- agentProfile: CURRENT_AGENT_PROFILE_SCHEMA_VERSION,
20
- globalRole: CURRENT_GLOBAL_ROLE_SCHEMA_VERSION,
21
- globalRoleSessionSet: CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION,
22
- task: CURRENT_TASK_SCHEMA_VERSION,
23
- taskBrief: CURRENT_TASK_BRIEF_SCHEMA_VERSION,
24
- taskRole: CURRENT_TASK_ROLE_SCHEMA_VERSION,
25
- managedWorkspace: CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION,
26
- taskRoleSessionSet: CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION,
27
- workItem: CURRENT_WORK_ITEM_SCHEMA_VERSION,
28
- contextSnapshot: CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION,
29
- turn: CURRENT_TURN_SCHEMA_VERSION,
30
- reviewRound: CURRENT_REVIEW_ROUND_SCHEMA_VERSION,
31
- changeSet: CURRENT_CHANGE_SET_SCHEMA_VERSION,
32
- integrationAttempt: CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION,
33
- integrationQueue: CURRENT_INTEGRATION_QUEUE_SCHEMA_VERSION,
34
- durableJob: CURRENT_DURABLE_JOB_SCHEMA_VERSION,
35
- message: CURRENT_MESSAGE_SCHEMA_VERSION,
36
- inputRequest: CURRENT_INPUT_REQUEST_SCHEMA_VERSION,
37
- decision: CURRENT_DECISION_SCHEMA_VERSION,
38
- milestone: CURRENT_MILESTONE_SCHEMA_VERSION,
39
- event: CURRENT_EVENT_SCHEMA_VERSION,
40
- taskWake: CURRENT_TASK_WAKE_SCHEMA_VERSION,
41
- capabilityGrant: CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION,
42
- releaseWorkflow: CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION,
43
- publicationReference: CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION,
44
- leaderFailure: CURRENT_LEADER_FAILURE_SCHEMA_VERSION,
45
- workMailbox: CURRENT_WORK_MAILBOX_SCHEMA_VERSION
46
- };
47
- currentDescriptors = Object.freeze(Object.fromEntries(Object.entries(versions).map(([kind, version]) => [kind, descriptor(kind, version)])));
48
- }
49
- return currentDescriptors;
50
- }
51
- /** Defensive copy of the current record-family contract. */
52
- export function currentRecordVersions(candidate = getCurrentRecordDescriptors()) {
53
- assertRecordVersionDescriptors(candidate);
54
- return { ...candidate };
55
- }
56
- /** Reject missing, extra, stale, or non-SQLite record descriptors. */
57
- export function assertRecordVersionDescriptors(candidate = getCurrentRecordDescriptors()) {
58
- const expected = getCurrentRecordDescriptors();
59
- const expectedKinds = Object.keys(expected);
60
- const mappedKinds = Object.keys(candidate);
61
- const missing = expectedKinds.filter((kind) => !Object.hasOwn(candidate, kind));
62
- const unexpected = mappedKinds.filter((kind) => !Object.hasOwn(expected, kind));
63
- if (missing.length > 0 || unexpected.length > 0) {
64
- throw new Error(`Record version map completeness drift: missing=${missing.join(",") || "none"}; `
65
- + `unexpected=${unexpected.join(",") || "none"}.`);
66
- }
67
- for (const kind of expectedKinds) {
68
- const wanted = expected[kind];
69
- const found = candidate[kind];
70
- if (found?.version !== wanted.version || found.path !== wanted.path) {
71
- throw new Error(`Record version map drift for ${kind}: expected=${wanted.version}@${wanted.path}; `
72
- + `actual=${String(found?.version)}@${String(found?.path)}.`);
73
- }
74
- }
75
- }
76
- export function latestStorageVersionState(recordDescriptors = getCurrentRecordDescriptors()) {
77
- return {
78
- layout: CURRENT_STORAGE_LAYOUT_VERSION,
79
- aggregate: CURRENT_AGGREGATE_SCHEMA_VERSION,
80
- record: currentRecordVersions(recordDescriptors)
81
- };
82
- }