@zq-silk/yui 0.6.4 → 0.6.6

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.
@@ -10,8 +10,10 @@ import { resolveTimeZone } from "../output/timePresentation.js";
10
10
  import { mailboxTargetKey, validateWorkMailbox } from "../coordination/workMailbox.js";
11
11
  import { validateInputRequest } from "../input/inputRequest.js";
12
12
  import { validateRoleSessionSet } from "../executor/agentExecutor.js";
13
+ import { validatePendingTurnCompletion } from "../executor/turnCompletion.js";
13
14
  import { validateTaskMessage } from "../message/message.js";
14
15
  import { validateAgentRun } from "../run/agentRun.js";
16
+ import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
15
17
  import { FileSessionOwnerRegistry } from "../runtime/sessionOwnerRegistry.js";
16
18
  import { validateReviewConfig } from "../review/reviewConfig.js";
17
19
  import { validateReviewRound } from "../review/reviewRound.js";
@@ -387,6 +389,13 @@ export class FileTaskStore {
387
389
  .map((aggregate) => clone(aggregate.task))
388
390
  .sort((left, right) => numericCompare(left.id, right.id));
389
391
  }
392
+ listActiveTaskIds() {
393
+ // The file rollback backend has no catalog index, so it filters the loaded
394
+ // aggregate. Layout-7 Controller hot paths use SQLite's bounded selector.
395
+ return this.listTasks()
396
+ .filter((task) => task.status === "active")
397
+ .map((task) => task.id);
398
+ }
390
399
  getStateRevision() { return this.#state().revision; }
391
400
  getTask(id) { return optional(this.#state().tasks[id]?.task); }
392
401
  readNextActionFacts(taskId) {
@@ -592,12 +601,21 @@ export class FileTaskStore {
592
601
  .find((job) => job.idempotencyKey === key);
593
602
  return optional(found);
594
603
  }
595
- listAllDurableJobs() {
604
+ listActiveDurableJobs() {
605
+ // The file rollback backend has no secondary indexes, so it filters its
606
+ // already-loaded aggregate here. Layout-7 Controller hot paths use the
607
+ // SQLite implementation below, whose status index avoids this history
608
+ // scan; the return contract and ordering stay identical across backends.
596
609
  const all = [];
597
610
  for (const aggregate of Object.values(this.#state().tasks)) {
598
- all.push(...Object.values(aggregate.durableJobs));
611
+ for (const job of Object.values(aggregate.durableJobs)) {
612
+ if (job.status === "queued" || job.status === "running")
613
+ all.push(job);
614
+ }
599
615
  }
600
- return all.map((job) => clone(job));
616
+ return all
617
+ .map((job) => clone(job))
618
+ .sort((left, right) => numericCompare(`${left.taskId}/${left.id}`, `${right.taskId}/${right.id}`));
601
619
  }
602
620
  hasActiveDurableJobs() {
603
621
  for (const aggregate of Object.values(this.#state().tasks)) {
@@ -732,6 +750,56 @@ export class FileTaskStore {
732
750
  listRoleSessionSets(taskId) {
733
751
  return values(this.#requireTask(taskId).roleSessionSets, (set) => set.owner.roleName);
734
752
  }
753
+ listRuntimeSessionCandidates(query = {}) {
754
+ const state = this.#state();
755
+ const selectedTaskIds = query.taskIds === undefined
756
+ ? undefined
757
+ : [...new Set(query.taskIds)].sort(numericCompare);
758
+ const taskAggregates = query.scope === "global"
759
+ ? []
760
+ : selectedTaskIds === undefined
761
+ ? Object.values(state.tasks)
762
+ : selectedTaskIds.flatMap((taskId) => {
763
+ const aggregate = state.tasks[taskId];
764
+ return aggregate === undefined ? [] : [aggregate];
765
+ });
766
+ const candidates = [
767
+ ...taskAggregates.flatMap((task) => (Object.values(task.roleSessionSets).flatMap((sessions) => {
768
+ const candidate = projectRuntimeSessionCandidate(sessions);
769
+ return candidate === null ? [] : [candidate];
770
+ }))),
771
+ ...(query.scope === "task" || selectedTaskIds !== undefined
772
+ ? []
773
+ : Object.values(state.globalRoleSessionSets).flatMap((sessions) => {
774
+ const candidate = projectRuntimeSessionCandidate(sessions);
775
+ return candidate === null ? [] : [candidate];
776
+ }))
777
+ ];
778
+ return candidates
779
+ .filter((candidate) => !query.cleanupRequiredOnly || candidate.cleanupRequired)
780
+ .sort(compareRuntimeSessionCandidates);
781
+ }
782
+ listPendingRuntimeTurnCompletions(taskIds) {
783
+ const selectedTaskIds = taskIds === undefined
784
+ ? undefined
785
+ : [...new Set(taskIds)].sort(numericCompare);
786
+ if (selectedTaskIds?.length === 0)
787
+ return [];
788
+ const taskAggregates = selectedTaskIds === undefined
789
+ ? Object.values(this.#state().tasks)
790
+ : selectedTaskIds.flatMap((taskId) => {
791
+ const aggregate = this.#state().tasks[taskId];
792
+ return aggregate === undefined ? [] : [aggregate];
793
+ });
794
+ return taskAggregates.flatMap((aggregate) => (Object.values(aggregate.roleSessionSets).flatMap((sessions) => {
795
+ const pending = sessions.pendingTurnCompletion;
796
+ return pending === null || pending === undefined
797
+ ? []
798
+ : [validatePendingTurnCompletion(pending)];
799
+ }))).sort((left, right) => (numericCompare(left.taskId, right.taskId)
800
+ || numericCompare(left.roleName, right.roleName)
801
+ || numericCompare(left.runId, right.runId)));
802
+ }
735
803
  saveRoleSessionSet(sessions) {
736
804
  const stored = taskSessions(sessions);
737
805
  const taskId = stored.owner.taskId;
@@ -836,11 +904,17 @@ export class FileTaskStore {
836
904
  }
837
905
  getAgentRun(taskId, id) { return optional(this.#state().tasks[taskId]?.agentRuns[id]); }
838
906
  listAgentRuns(taskId) { return values(this.#requireTask(taskId).agentRuns, "id"); }
839
- listPendingProviderRetries() {
907
+ listPendingProviderRetries(taskIds) {
840
908
  // The legacy File store can answer the empty case without a scan fallback.
841
909
  // If durable retry state exists, the db-only capability must fail closed
842
910
  // instead of silently losing the Controller's wake deadline.
843
- for (const task of this.listTasks()) {
911
+ const tasks = taskIds === undefined
912
+ ? this.listTasks()
913
+ : [...new Set(taskIds)].sort(numericCompare).flatMap((taskId) => {
914
+ const task = this.getTask(taskId);
915
+ return task === null ? [] : [task];
916
+ });
917
+ for (const task of tasks) {
844
918
  if (task.status !== "active")
845
919
  continue;
846
920
  for (const run of this.listAgentRuns(task.id)) {
@@ -1104,6 +1178,21 @@ export class FileTaskStore {
1104
1178
  listInputRequests(taskId) {
1105
1179
  return values(this.#requireTask(taskId).inputRequests, "id");
1106
1180
  }
1181
+ listOpenInputRequests(taskIds) {
1182
+ const state = this.#state();
1183
+ const selected = taskIds === undefined
1184
+ ? Object.values(state.tasks)
1185
+ : [...new Set(taskIds)].sort(numericCompare).flatMap((taskId) => {
1186
+ const aggregate = state.tasks[taskId];
1187
+ return aggregate === undefined ? [] : [aggregate];
1188
+ });
1189
+ return selected
1190
+ .flatMap((aggregate) => Object.values(aggregate.inputRequests))
1191
+ .filter((request) => request.status === "open")
1192
+ .map(clone)
1193
+ .sort((left, right) => (numericCompare(left.taskId, right.taskId)
1194
+ || numericCompare(left.id, right.id)));
1195
+ }
1107
1196
  listAllInputRequests() {
1108
1197
  return Object.values(this.#state().tasks)
1109
1198
  .flatMap((aggregate) => Object.values(aggregate.inputRequests).map(clone))
@@ -1390,6 +1479,15 @@ export class FileTaskStore {
1390
1479
  .sort(([left], [right]) => left.localeCompare(right))
1391
1480
  .map(([, mailbox]) => clone(mailbox));
1392
1481
  }
1482
+ listReadyWorkMailboxes() {
1483
+ // The file rollback backend has no secondary indexes. It filters the
1484
+ // in-memory aggregate while preserving the indexed SQLite contract's
1485
+ // target-key order; production layout 7 uses the bounded SQLite query.
1486
+ return Object.entries(this.#state().mailboxes)
1487
+ .filter(([, mailbox]) => mailbox.processing !== null || mailbox.pending !== null)
1488
+ .sort(([left], [right]) => left.localeCompare(right))
1489
+ .map(([, mailbox]) => clone(mailbox));
1490
+ }
1393
1491
  saveWorkMailbox(value) {
1394
1492
  let mailbox;
1395
1493
  try {
@@ -1410,7 +1508,7 @@ export class FileTaskStore {
1410
1508
  return pendingWakeupProjection(this.getWorkMailbox({ kind: "role", taskId, roleName: "leader" }));
1411
1509
  }
1412
1510
  listPendingWakeups() {
1413
- return this.listWorkMailboxes()
1511
+ return this.listReadyWorkMailboxes()
1414
1512
  .flatMap((mailbox) => {
1415
1513
  const wakeup = pendingWakeupProjection(mailbox);
1416
1514
  return wakeup === null ? [] : [wakeup];
@@ -52,7 +52,8 @@ Leader 和 Operator,再说明全局 Worker 配置会复制到新建的 Task Ro
52
52
  支持的思考强度。随后 setup 会确认位于 Yui home 外部的 Project workspace,
53
53
  并询问 shell completion。选择器同时提供原生 CLI 默认值和自定义值入口。
54
54
  再次运行不会删除已有 Task/Role,也不会改变当前安装的 Project workspace,
55
- 可用于安全地调整配置。
55
+ 可用于安全地调整配置。setup 成功返回前会确保当前 Home 的后台 Controller
56
+ 已经启动。
56
57
 
57
58
  模型与思考强度属于 Agent binding 设置,因此 Operator、Leader 和全局
58
59
  Worker 即使使用同一个 Agent CLI,也可以采用不同配置。Profile 中的
@@ -477,6 +478,11 @@ yui controller restart
477
478
 
478
479
  `controller restart` 会用当前安装的 Yui 版本替换 Controller 进程及其调度循环、socket 服务,不会停止或重启已受管的 tmux/Agent 会话。
479
480
 
481
+ 成功的 `setup`、`upgrade` 和 `update` 都会确保当前 Home 有一个运行中的
482
+ Controller;如果之前没有运行,会在完成后启动。只读命令和
483
+ `upgrade --dry-run` 不会启动 Controller。`update` 只有在新二进制健康检查通过后,
484
+ 才会替换或启动 Controller。
485
+
480
486
  恢复 reconciliation 默认每 120 秒执行一次。普通持久状态变化只会将 Task、Role 或 Operator key 放入队列并立即返回;固定 100ms 窗口内到达的 key 会合并触发一次不重叠的定向处理。Operator 呈现使用独立 lane,不会被 Task 的 Git/worktree 操作阻塞;周期 Git/worktree 处理只覆盖仍有持久 Task mailbox 工作的 Task,活动 Role 的存活检查合并为一次 tmux inventory。Codex turn-complete Hook 直接写入存储,不启动或等待 Controller,并给合法的 yield、输入请求或完成动作保留 2 秒竞争窗口;到期后才关闭被 Agent 遗忘的活动 Role Run。持久 WorkMailbox 会冻结当前 processing 批次,期间的新事件合并到下一 pending 批次;失败会释放当前批次供恢复。推荐输入与 pending Turn 共用最近 deadline 选择器,不依赖恢复扫描间隔;显式 `task reconcile` 仍会立即请求恢复扫描。保留的闭环为:
481
487
 
482
488
  1. 准备 active Project Task 的主 worktree;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,