@zq-silk/yui 0.15.0 → 0.15.2

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.
Files changed (41) hide show
  1. package/README.md +33 -9
  2. package/dist/cli/commandCatalog.js +5 -5
  3. package/dist/cli.js +5 -3
  4. package/dist/commands/agentCommands.js +13 -6
  5. package/dist/commands/globalRoleCommands.js +11 -3
  6. package/dist/commands/roleConfiguration.js +7 -0
  7. package/dist/commands/roleRuntimeGuard.js +30 -0
  8. package/dist/commands/taskCommands.js +10 -3
  9. package/dist/context/sessionBootstrapManifest.js +24 -9
  10. package/dist/controller/fileSchedulerStoreAdapter.js +4 -4
  11. package/dist/controller/jobClient.js +1 -0
  12. package/dist/controller/jobControl.js +137 -10
  13. package/dist/controller/jobSupervisor.js +89 -75
  14. package/dist/controller/runtime.js +19 -28
  15. package/dist/controller/runtimeLaunchCoordinator.js +9 -30
  16. package/dist/controller/sessionNotify.js +5 -0
  17. package/dist/core/boundedRpc.js +3 -1
  18. package/dist/executor/agentExecutor.js +8 -11
  19. package/dist/executor/effectiveLaunch.js +34 -17
  20. package/dist/executor/fileRoleLaunchPlanner.js +11 -8
  21. package/dist/job/durableJob.js +68 -6
  22. package/dist/kernel/callAuthority.js +24 -0
  23. package/dist/kernel/instanceHost.js +97 -0
  24. package/dist/kernel/kernelPorts.js +44 -0
  25. package/dist/kernel/operationFacts.js +32 -0
  26. package/dist/runtime/agentHost.js +7 -0
  27. package/dist/runtime/codexInteractiveHost.js +191 -0
  28. package/dist/runtime/exactControlPlane.js +8 -10
  29. package/dist/runtime/structuredProviderHost.js +35 -0
  30. package/dist/runtime/tmuxAdapters.js +51 -9
  31. package/dist/scheduler/activeRoleTurnDelivery.js +4 -4
  32. package/dist/scheduler/leaderWakeupProcessor.js +3 -4
  33. package/dist/storage/sqliteSchema.js +28 -0
  34. package/dist/storage/sqliteStore.js +11 -3
  35. package/dist/storage/storageVersions.js +1 -1
  36. package/dist/storage/storeRpc.js +1 -1
  37. package/dist/storage/taskStore.js +6 -1
  38. package/dist/storage/upgrade/upgradeOrchestrator.js +3 -0
  39. package/dist/tmux/tmuxManager.js +43 -28
  40. package/i18n/README.zh-CN.md +7 -0
  41. package/package.json +1 -1
@@ -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 false;
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 false;
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", target,
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", target,
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.hasSession(taskId))
829
+ if (!this.windowNames(taskId).includes(roleName))
811
830
  return "exited";
812
- return this.windowNames(taskId).includes(roleName) ? "running" : "exited";
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
- return snapshot.names.includes(roleName) ? "running" : "exited";
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 (isUnavailableTmuxStatus(error))
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 (isUnavailableTmuxStatus(error))
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
  }
@@ -14,6 +14,9 @@ Yui 不把 Agent 的判断固化成确定性的工作流引擎。核心只负责
14
14
 
15
15
  当前实现保留实用的 Role/Agent/session 与 CLI 框架,不恢复后期膨胀的数据维护、租约、定时调度和恢复账本体系。
16
16
 
17
+ [目标架构手册](../docs/architecture/README.md) 保存了供后续重构使用的
18
+ 2026-09-06 版设计基线,不代表当前已实现行为。
19
+
17
20
  ## 核心模型
18
21
 
19
22
  - `WorkItem`:唯一的有界工作单元,保存目标、验收条件、依赖、状态和精简结果。
@@ -619,6 +622,8 @@ Task 生命周期的交互选择只展示有效来源状态:activate 只展示
619
622
 
620
623
  Session、Activation 与 Turn 是独立身份。Session 可以跨多个 Turn 和客户端连接;Activation 只代表 Yui 当前的连接,而不是对 Provider thread 的独占所有权。每次 Provider 执行对应一个持久 Turn;写入超时或结果不明确会进入 `delivery-unknown`,不会自动重发。Codex 已存在的 active Turn 只会让 Yui 暂时等待,不会导致待投递 Turn 失败;Claude 等独立进程 Provider 继续通过 Yui 的 view/takeover 边界进行人工控制。
621
624
 
625
+ 恢复只在真的续不下去时被拦住:provider 侧没有可恢复的 Session、换了 Agent 或适配器、换了物理工作区。模型、推理强度、权限策略、Role 说明与 Skill、声明的写范围只决定下一次 activation 用什么,审查轮次、候选 commit、工作区基线这类每轮事实不影响复用。因此当 Role 存在活跃 Session 时,`task role update`、`config role update`、`config agent update` 会先报告该 Session 并要求 `--yes` 确认;需要立刻生效则先停止该 Session。
626
+
622
627
  Turn 是 Role 是否有工作正在执行的唯一持久调度状态,记录可见输入、来源/渠道与最终回复,不复制思考过程或工具调用。所有经 Yui 中转或生成的输入统一使用 `source: yui`;Provider UI 中直接输入的消息使用 `source: user`;显式 Goal continuation 使用 `source: provider`。Provider Turn 终态后 Yui 完成该 Turn,再把下一个 Turn 投递到同一 Session。TaskRole 本身只保存身份和期望启动配置,不再保存可写的运行状态;CLI/Web 展示的 Role 状态由活动 Turn 派生,并叠加 Session/Driver 生命周期事实用于诊断。
623
628
 
624
629
  Goal 是 Session 级显式 Provider 事实,可以跨越多个 Turn。Codex 通过 Goal API/事件提供,Claude 通过 `active_goal` 提供;Yui 不用静默等待来猜测 Goal 是否完成。Turn 结束不等于 Goal、WorkItem 或 Task 完成,只有 Leader 更新 WorkItem 与 Task 的持久语义。
@@ -635,6 +640,8 @@ yui task role release <task-id> <role>
635
640
 
636
641
  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 继续对话。
637
642
 
643
+ Global Codex 的薄 Host 与原生 TUI 位于同一个 pane,透明转发 App Server 连接,并从该 TUI 自己的 `thread/start` 或 `thread/resume` 成功响应取得 Thread ID。Yui 在首条用户消息之前通过既有启动回执登记身份,不依赖 `notify`、历史目录扫描或 bootstrap 消息;旧的 global `notify` 不能登记或修改 Session 生命周期。连接随 TUI 退出,不依赖 Controller 的持续运行。tmux 窗口存在不等于 Agent 存活:`pane_dead=0` 才是运行中,`pane_dead=1` 是保留的退出现场,读取失败则报错。状态查询不删除现场;显式启动可重建精确的死亡窗口,但不能覆盖身份未知的活 Operator。
644
+
638
645
  `yui update` 会用目标版本先做只读预检,在停住精确的旧 Controller 后自动执行
639
646
  所需的离线迁移,再校验并启动新 Controller。若升级前希望结束所有 Agent
640
647
  活动,可先执行 `yui session stop --all`;这不是存储版本链的一部分。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,