@zq-silk/yui 0.6.5 → 0.6.7

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];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,