@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.
- package/README.md +33 -9
- package/dist/cli/commandCatalog.js +5 -5
- package/dist/cli.js +5 -3
- package/dist/commands/agentCommands.js +13 -6
- 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/context/sessionBootstrapManifest.js +24 -9
- package/dist/controller/fileSchedulerStoreAdapter.js +4 -4
- package/dist/controller/jobClient.js +1 -0
- package/dist/controller/jobControl.js +137 -10
- package/dist/controller/jobSupervisor.js +89 -75
- package/dist/controller/runtime.js +19 -28
- package/dist/controller/runtimeLaunchCoordinator.js +9 -30
- package/dist/controller/sessionNotify.js +5 -0
- package/dist/core/boundedRpc.js +3 -1
- package/dist/executor/agentExecutor.js +8 -11
- package/dist/executor/effectiveLaunch.js +34 -17
- package/dist/executor/fileRoleLaunchPlanner.js +11 -8
- package/dist/job/durableJob.js +68 -6
- package/dist/kernel/callAuthority.js +24 -0
- package/dist/kernel/instanceHost.js +97 -0
- package/dist/kernel/kernelPorts.js +44 -0
- package/dist/kernel/operationFacts.js +32 -0
- package/dist/runtime/agentHost.js +7 -0
- package/dist/runtime/codexInteractiveHost.js +191 -0
- package/dist/runtime/exactControlPlane.js +8 -10
- 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/sqliteSchema.js +28 -0
- package/dist/storage/sqliteStore.js +11 -3
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -1
- package/dist/storage/taskStore.js +6 -1
- package/dist/storage/upgrade/upgradeOrchestrator.js +3 -0
- package/dist/tmux/tmuxManager.js +43 -28
- package/i18n/README.zh-CN.md +7 -0
- package/package.json +1 -1
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/i18n/README.zh-CN.md
CHANGED
|
@@ -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`;这不是存储版本链的一部分。
|