@zq-silk/yui 0.6.8 → 0.6.10

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 (58) hide show
  1. package/README.md +10 -3
  2. package/dist/cli/commandCatalog.js +1 -1
  3. package/dist/commands/taskCommands.js +63 -28
  4. package/dist/commands/taskContextCommand.js +27 -1
  5. package/dist/commands/taskRoleRuntimeStatus.js +17 -6
  6. package/dist/controller/agentRuntimeObserver.js +6 -3
  7. package/dist/controller/controller.js +29 -47
  8. package/dist/controller/fileSchedulerStoreAdapter.js +572 -258
  9. package/dist/controller/runtime.js +8 -1
  10. package/dist/controller/runtimeEventInbox.js +16 -5
  11. package/dist/controller/runtimeHookRunFence.js +51 -5
  12. package/dist/controller/runtimeObservationHook.js +8 -2
  13. package/dist/coordination/workMailbox.js +408 -28
  14. package/dist/coordination/workMailboxQueue.js +12 -10
  15. package/dist/executor/agentExecutor.js +101 -94
  16. package/dist/executor/executorRegistry.js +47 -2
  17. package/dist/executor/fileRoleLaunchPlanner.js +4 -2
  18. package/dist/lifecycle/exactRunTerminalization.js +1 -7
  19. package/dist/repository/taskWorkspaceCoordinator.js +9 -4
  20. package/dist/runtime/agentDriver.js +83 -4
  21. package/dist/runtime/agentDriverObservation.js +25 -10
  22. package/dist/runtime/builtinAgentDrivers.js +168 -18
  23. package/dist/runtime/codexAppServerRuntime.js +355 -0
  24. package/dist/runtime/continuationManager.js +117 -0
  25. package/dist/runtime/index.js +2 -0
  26. package/dist/runtime/lifecycleReservation.js +4 -3
  27. package/dist/runtime/promptEnvelope.js +14 -3
  28. package/dist/runtime/providerContinuation.js +225 -0
  29. package/dist/runtime/providerContinuationReconciliationService.js +172 -0
  30. package/dist/runtime/providerRuntimeIdentity.js +232 -0
  31. package/dist/runtime/providerRuntimeReconciler.js +166 -0
  32. package/dist/runtime/runtimeContinuationProjection.js +34 -0
  33. package/dist/runtime/runtimeObservation.js +217 -6
  34. package/dist/runtime/runtimeProjection.js +172 -11
  35. package/dist/scheduler/activeRoleRunDelivery.js +314 -1
  36. package/dist/scheduler/leaderWakeupProcessor.js +2 -1
  37. package/dist/scheduler/operatorInputNotificationProcessor.js +3 -2
  38. package/dist/scheduler/roleRunLiveness.js +8 -7
  39. package/dist/scheduler/roleRunStall.js +4 -2
  40. package/dist/scheduler/taskExecutionProjection.js +2 -2
  41. package/dist/storage/migration/productionRegistry.js +474 -1
  42. package/dist/storage/sqliteSchema.js +102 -21
  43. package/dist/storage/sqliteStore.js +52 -110
  44. package/dist/storage/storageVersions.js +1 -1
  45. package/dist/storage/storeRpc.js +0 -1
  46. package/dist/storage/taskStore.js +40 -53
  47. package/dist/storage/upgrade/sqliteStateMigration.js +0 -21
  48. package/dist/task/nextAction.js +1 -1
  49. package/dist/web/assets/client/app.js +1 -1
  50. package/dist/web/assets/client/components.js +233 -4
  51. package/dist/web/assets/client/i18n.js +166 -2
  52. package/dist/web/assets/client/view.js +30 -13
  53. package/dist/web/assets/styles/cards.js +62 -0
  54. package/dist/web/assets/styles/widgets.js +1 -0
  55. package/dist/web/webSnapshot.js +11 -2
  56. package/package.json +1 -1
  57. package/skills/yui-leader/SKILL.md +33 -24
  58. package/skills/yui-operator/SKILL.md +7 -5
@@ -12,10 +12,9 @@
12
12
  * the write transaction; conflict ->
13
13
  * StorageConflictError (transactionWithRevisionCas).
14
14
  * - Atomic durable write ........ WAL + synchronous=FULL; COMMIT == fsync.
15
- * - Mailbox per-target ordering . mailboxes.next_sequence + mailbox_signals
16
- * (mailbox_id, sequence) primary key.
15
+ * - Mailbox per-target ordering . WorkMailbox v2 sequence/cursors in one row.
17
16
  * - Exactly-once terminal state . conditional updates + UNIQUE(request_id)
18
- * on outbox / mailbox_signals.
17
+ * on the durable outbox.
19
18
  * - Crash recovery .............. WAL rollback of uncommitted transactions;
20
19
  * outbox replay of committed-but-unacked effects.
21
20
  * - Record family versioning .... full record (incl. schemaVersion) in payload.
@@ -39,8 +38,7 @@ import { existsSync, mkdirSync } from "node:fs";
39
38
  import { join } from "node:path";
40
39
  import { isDeepStrictEqual } from "node:util";
41
40
  import Database from "better-sqlite3";
42
- import { mailboxTargetKey } from "../coordination/workMailbox.js";
43
- import { validatePendingTurnCompletion } from "../executor/turnCompletion.js";
41
+ import { consumePendingBatch, mailboxTargetKey, pendingLane, validateWorkMailbox } from "../coordination/workMailbox.js";
44
42
  import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
45
43
  import { validateReviewFinding } from "../review/reviewFinding.js";
46
44
  import { generateHomeIdentity, validateHomeIdentity } from "../repository/homeIdentity.js";
@@ -90,17 +88,18 @@ function isUniqueConstraint(error) {
90
88
  }
91
89
  /** Project a leader-role work mailbox's pending batch to a PendingWakeup (mirrors taskStore.ts). */
92
90
  function pendingWakeupProjection(mailbox) {
91
+ const pending = mailbox === null ? null : pendingLane(mailbox, "normal");
93
92
  if (mailbox === null || mailbox.target.kind !== "role" || mailbox.target.roleName !== "leader"
94
- || mailbox.pending === null) {
93
+ || pending === null) {
95
94
  return null;
96
95
  }
97
96
  return {
98
97
  schemaVersion: CURRENT_PENDING_WAKEUP_SCHEMA_VERSION,
99
98
  taskId: mailbox.target.taskId,
100
- reasons: [...mailbox.pending.reasons],
101
- requestCount: mailbox.pending.requestCount,
102
- firstRequestedAt: mailbox.pending.firstQueuedAt,
103
- lastRequestedAt: mailbox.pending.lastQueuedAt
99
+ reasons: [...pending.reasons],
100
+ requestCount: pending.requestCount,
101
+ firstRequestedAt: pending.firstQueuedAt,
102
+ lastRequestedAt: pending.lastQueuedAt
104
103
  };
105
104
  }
106
105
  export class SqliteTaskStore {
@@ -915,7 +914,6 @@ export class SqliteTaskStore {
915
914
  this.#db.prepare(`INSERT INTO role_session_sets (task_id, role_name, payload, updated_at) VALUES (?, ?, ?, ?)
916
915
  ON CONFLICT(task_id, role_name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(sessions.owner.taskId, sessions.owner.roleName, this.#json(sessions), this.#now());
917
916
  this.#saveRuntimeSessionCandidate(sessions);
918
- this.#savePendingRuntimeTurnCompletion(sessions);
919
917
  });
920
918
  }
921
919
  removeTaskRole(taskId, name) {
@@ -1104,33 +1102,6 @@ export class SqliteTaskStore {
1104
1102
  && row.session_updated_at === candidate.sessionUpdatedAt
1105
1103
  && row.cleanup_required === (candidate.cleanupRequired ? 1 : 0);
1106
1104
  }
1107
- listPendingRuntimeTurnCompletions(taskIds) {
1108
- const selectedTaskIds = taskIds === undefined
1109
- ? undefined
1110
- : [...new Set(taskIds)].sort(numericCompare);
1111
- if (selectedTaskIds?.length === 0)
1112
- return [];
1113
- const where = selectedTaskIds === undefined
1114
- ? ""
1115
- : ` WHERE task_id IN (${selectedTaskIds.map(() => "?").join(", ")})`;
1116
- const rows = this.#db.prepare(`SELECT task_id, role_name, schema_version, agent_id, native_session_id,
1117
- turn_id, run_id, summary, observed_at, due_at
1118
- FROM pending_runtime_turn_completions${where}`).all(...(selectedTaskIds ?? []));
1119
- return rows.map((row) => validatePendingTurnCompletion({
1120
- schemaVersion: row.schema_version,
1121
- taskId: row.task_id,
1122
- roleName: row.role_name,
1123
- agentId: row.agent_id,
1124
- nativeSessionId: row.native_session_id,
1125
- turnId: row.turn_id,
1126
- runId: row.run_id,
1127
- summary: row.summary,
1128
- observedAt: row.observed_at,
1129
- dueAt: row.due_at
1130
- })).sort((left, right) => (numericCompare(left.taskId, right.taskId)
1131
- || numericCompare(left.roleName, right.roleName)
1132
- || numericCompare(left.runId, right.runId)));
1133
- }
1134
1105
  saveRoleSessionSet(sessions) {
1135
1106
  const taskId = sessions.owner.taskId;
1136
1107
  this.#requireTask(taskId);
@@ -1138,7 +1109,6 @@ export class SqliteTaskStore {
1138
1109
  this.#db.prepare(`INSERT INTO role_session_sets (task_id, role_name, payload, updated_at) VALUES (?, ?, ?, ?)
1139
1110
  ON CONFLICT(task_id, role_name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, sessions.owner.roleName, this.#json(sessions), this.#now());
1140
1111
  this.#saveRuntimeSessionCandidate(sessions);
1141
- this.#savePendingRuntimeTurnCompletion(sessions);
1142
1112
  });
1143
1113
  }
1144
1114
  saveTaskRoleSessionSet(sessions) {
@@ -1175,32 +1145,6 @@ export class SqliteTaskStore {
1175
1145
  this.#db.prepare(`DELETE FROM runtime_session_candidates
1176
1146
  WHERE scope = ? AND task_id = ? AND role_name = ?`).run(owner.scope, owner.scope === "task" ? owner.taskId : "", owner.roleName);
1177
1147
  }
1178
- /** Maintain the independent pending-Turn projection in the same write txn. */
1179
- #savePendingRuntimeTurnCompletion(sessions) {
1180
- const owner = sessions.owner;
1181
- const pending = sessions.pendingTurnCompletion;
1182
- if (pending === null) {
1183
- this.#db.prepare("DELETE FROM pending_runtime_turn_completions WHERE task_id = ? AND role_name = ?").run(owner.taskId, owner.roleName);
1184
- return;
1185
- }
1186
- const normalized = validatePendingTurnCompletion(pending);
1187
- if (normalized.taskId !== owner.taskId || normalized.roleName !== owner.roleName) {
1188
- throw new StorageRecordError(`Pending Turn completion owner does not match Role session: ${owner.taskId}/${owner.roleName}.`);
1189
- }
1190
- this.#db.prepare(`INSERT INTO pending_runtime_turn_completions (
1191
- task_id, role_name, schema_version, agent_id, native_session_id,
1192
- turn_id, run_id, summary, observed_at, due_at
1193
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1194
- ON CONFLICT(task_id, role_name) DO UPDATE SET
1195
- schema_version = excluded.schema_version,
1196
- agent_id = excluded.agent_id,
1197
- native_session_id = excluded.native_session_id,
1198
- turn_id = excluded.turn_id,
1199
- run_id = excluded.run_id,
1200
- summary = excluded.summary,
1201
- observed_at = excluded.observed_at,
1202
- due_at = excluded.due_at`).run(normalized.taskId, normalized.roleName, normalized.schemaVersion, normalized.agentId, normalized.nativeSessionId, normalized.turnId, normalized.runId, normalized.summary, normalized.observedAt, normalized.dueAt);
1203
- }
1204
1148
  // -- work items -------------------------------------------------------------
1205
1149
  nextWorkItemId(taskId) { return this.#nextTaskRecordId(taskId, "workItem"); }
1206
1150
  getWorkItem(taskId, workItemId) {
@@ -1776,13 +1720,14 @@ export class SqliteTaskStore {
1776
1720
  }
1777
1721
  #rowToMailbox(row) {
1778
1722
  const target = this.#targetFromCols(row.target_kind, row.task_id, row.role_name);
1779
- return {
1780
- schemaVersion: 1,
1723
+ return validateWorkMailbox({
1724
+ schemaVersion: CURRENT_WORK_MAILBOX_SCHEMA_VERSION,
1781
1725
  target,
1782
1726
  nextSequence: row.next_sequence,
1783
1727
  processing: row.processing === null ? null : this.#parse(row.processing),
1784
- pending: row.pending === null ? null : this.#parse(row.pending)
1785
- };
1728
+ pending: this.#parse(row.pending),
1729
+ inputDelivery: row.input_delivery === null ? null : this.#parse(row.input_delivery)
1730
+ });
1786
1731
  }
1787
1732
  #targetFromCols(kind, taskId, roleName) {
1788
1733
  switch (kind) {
@@ -1796,27 +1741,31 @@ export class SqliteTaskStore {
1796
1741
  }
1797
1742
  getWorkMailbox(target) {
1798
1743
  const cols = this.#mailboxCols(target);
1799
- const row = this.#db.prepare("SELECT target_kind, task_id, role_name, next_sequence, processing, pending FROM mailboxes WHERE target_key = ?").get(cols.targetKey);
1744
+ const row = this.#db.prepare("SELECT target_kind, task_id, role_name, next_sequence, processing, pending, input_delivery FROM mailboxes WHERE target_key = ?").get(cols.targetKey);
1800
1745
  return row === undefined ? null : this.#rowToMailbox(row);
1801
1746
  }
1802
1747
  listWorkMailboxes() {
1803
- const rows = this.#db.prepare("SELECT target_kind, task_id, role_name, next_sequence, processing, pending FROM mailboxes ORDER BY target_key").all();
1748
+ const rows = this.#db.prepare("SELECT target_kind, task_id, role_name, next_sequence, processing, pending, input_delivery FROM mailboxes ORDER BY target_key").all();
1804
1749
  return rows.map((row) => this.#rowToMailbox(row));
1805
1750
  }
1806
1751
  listReadyWorkMailboxes() {
1807
- const rows = this.#db.prepare(`SELECT target_kind, task_id, role_name, next_sequence, processing, pending
1752
+ const rows = this.#db.prepare(`SELECT target_kind, task_id, role_name, next_sequence, processing, pending, input_delivery
1808
1753
  FROM mailboxes
1809
- WHERE processing IS NOT NULL OR pending IS NOT NULL
1754
+ WHERE processing IS NOT NULL
1755
+ OR input_delivery IS NOT NULL
1756
+ OR json_type(pending, '$.normal') <> 'null'
1757
+ OR json_type(pending, '$.userCorrection') <> 'null'
1810
1758
  ORDER BY target_key`).all();
1811
1759
  return rows.map((row) => this.#rowToMailbox(row));
1812
1760
  }
1813
1761
  saveWorkMailbox(mailbox) {
1814
1762
  const cols = this.#mailboxCols(mailbox.target);
1815
1763
  this.#mutate(() => {
1816
- this.#db.prepare(`INSERT INTO mailboxes (target_kind, task_id, role_name, target_key, next_sequence, processing, pending)
1817
- VALUES (?, ?, ?, ?, ?, ?, ?)
1764
+ this.#db.prepare(`INSERT INTO mailboxes (target_kind, task_id, role_name, target_key, next_sequence, processing, pending, input_delivery)
1765
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1818
1766
  ON CONFLICT(target_key) DO UPDATE SET next_sequence = excluded.next_sequence,
1819
- processing = excluded.processing, pending = excluded.pending`).run(cols.targetKind, cols.taskId, cols.roleName, cols.targetKey, mailbox.nextSequence, mailbox.processing === null ? null : this.#json(mailbox.processing), mailbox.pending === null ? null : this.#json(mailbox.pending));
1767
+ processing = excluded.processing, pending = excluded.pending,
1768
+ input_delivery = excluded.input_delivery`).run(cols.targetKind, cols.taskId, cols.roleName, cols.targetKey, mailbox.nextSequence, mailbox.processing === null ? null : this.#json(mailbox.processing), this.#json(mailbox.pending), mailbox.inputDelivery === null ? null : this.#json(mailbox.inputDelivery));
1820
1769
  });
1821
1770
  }
1822
1771
  removeWorkMailbox(target) {
@@ -1832,28 +1781,6 @@ export class SqliteTaskStore {
1832
1781
  * single writer connection serializes enqueues, so sequences stay gapless per
1833
1782
  * mailbox. `(mailbox_id, sequence)` is the exactly-once key.
1834
1783
  */
1835
- enqueueMailboxSignal(target, input) {
1836
- return this.#mutate(() => {
1837
- const cols = this.#mailboxCols(target);
1838
- let mailboxId;
1839
- let sequence;
1840
- const existing = this.#db.prepare("SELECT mailbox_id, next_sequence FROM mailboxes WHERE target_key = ?").get(cols.targetKey);
1841
- if (existing === undefined) {
1842
- const result = this.#db.prepare(`INSERT INTO mailboxes (target_kind, task_id, role_name, target_key, next_sequence, processing, pending)
1843
- VALUES (?, ?, ?, ?, 1, NULL, NULL)`).run(cols.targetKind, cols.taskId, cols.roleName, cols.targetKey);
1844
- mailboxId = Number(result.lastInsertRowid);
1845
- sequence = 1;
1846
- }
1847
- else {
1848
- mailboxId = existing.mailbox_id;
1849
- sequence = existing.next_sequence;
1850
- }
1851
- this.#db.prepare(`INSERT INTO mailbox_signals (mailbox_id, sequence, reason, ref_type, ref_task_id, ref_id, occurred_at, request_id)
1852
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(mailboxId, sequence, input.reason, input.ref?.type ?? null, input.ref && "taskId" in input.ref ? input.ref.taskId : null, input.ref?.id ?? null, this.#now(), input.requestId);
1853
- this.#db.prepare("UPDATE mailboxes SET next_sequence = ? WHERE mailbox_id = ?").run(sequence + 1, mailboxId);
1854
- return sequence;
1855
- });
1856
- }
1857
1784
  // -- scheduler projections -----------------------------------------------------
1858
1785
  #getProjection(taskId, kind) {
1859
1786
  const row = this.#db.prepare("SELECT payload FROM task_projections WHERE task_id = ? AND kind = ?").get(taskId, kind);
@@ -1908,32 +1835,47 @@ export class SqliteTaskStore {
1908
1835
  const target = { kind: "role", taskId: value.taskId, roleName: "leader" };
1909
1836
  this.transaction((store) => {
1910
1837
  const existing = store.getWorkMailbox(target);
1911
- if (existing !== null && existing.pending !== null
1912
- && value.requestCount <= existing.pending.requestCount) {
1838
+ const existingPending = existing === null ? null : pendingLane(existing, "normal");
1839
+ if (existingPending !== null
1840
+ && value.requestCount <= existingPending.requestCount) {
1913
1841
  throw new StorageRecordError(`Pending wakeup is stale: ${value.taskId}`);
1914
1842
  }
1915
- const fromSequence = existing?.pending?.fromSequence ?? existing?.nextSequence ?? 1;
1843
+ const fromSequence = existingPending?.fromSequence ?? existing?.nextSequence ?? 1;
1916
1844
  const toSequence = fromSequence + value.requestCount - 1;
1917
1845
  store.saveWorkMailbox({
1918
1846
  schemaVersion: CURRENT_WORK_MAILBOX_SCHEMA_VERSION,
1919
1847
  target,
1920
1848
  nextSequence: Math.max(existing?.nextSequence ?? 1, toSequence + 1),
1921
1849
  processing: existing?.processing ?? null,
1850
+ inputDelivery: existing?.inputDelivery ?? null,
1922
1851
  pending: {
1923
- ...existing?.pending,
1924
- fromSequence,
1925
- toSequence,
1926
- reasons: [...value.reasons],
1927
- refs: existing?.pending?.refs ?? [],
1928
- requestCount: value.requestCount,
1929
- firstQueuedAt: value.firstRequestedAt,
1930
- lastQueuedAt: value.lastRequestedAt
1852
+ normal: {
1853
+ fromSequence,
1854
+ toSequence,
1855
+ reasons: [...value.reasons],
1856
+ refs: existingPending?.refs ?? [],
1857
+ requestCount: value.requestCount,
1858
+ firstQueuedAt: value.firstRequestedAt,
1859
+ lastQueuedAt: value.lastRequestedAt,
1860
+ sources: existingPending?.sources ?? ["pending-wakeup-projection"],
1861
+ dedupeKeys: existingPending?.dedupeKeys ?? [
1862
+ `pending-wakeup:${value.taskId}:${fromSequence}-${toSequence}`
1863
+ ],
1864
+ deliveryModes: existingPending?.deliveryModes ?? ["followup"]
1865
+ },
1866
+ userCorrection: existing?.pending.userCorrection ?? null,
1867
+ cursors: existing?.pending.cursors ?? { normal: 0, userCorrection: 0 },
1868
+ recentDedupeKeys: existing?.pending.recentDedupeKeys ?? []
1931
1869
  }
1932
1870
  });
1933
1871
  });
1934
1872
  }
1935
1873
  clearPendingWakeup(taskId) {
1936
- this.removeWorkMailbox({ kind: "role", taskId, roleName: "leader" });
1874
+ const target = { kind: "role", taskId, roleName: "leader" };
1875
+ const mailbox = this.getWorkMailbox(target);
1876
+ if (mailbox === null || mailbox.pending.normal === null)
1877
+ return;
1878
+ this.saveWorkMailbox(consumePendingBatch(mailbox, "normal"));
1937
1879
  }
1938
1880
  // -- telemetry (§4.4) -----------------------------------------------------------
1939
1881
  /**
@@ -15,4 +15,4 @@
15
15
  */
16
16
  export const CURRENT_STORAGE_LAYOUT_VERSION = 7;
17
17
  /** Version of the authoritative aggregate stored in `state.json`. */
18
- export const CURRENT_AGGREGATE_SCHEMA_VERSION = 19;
18
+ export const CURRENT_AGGREGATE_SCHEMA_VERSION = 20;
@@ -69,7 +69,6 @@ const READ_ONLY_STORE_METHODS = new Set([
69
69
  "getTaskRoleSessionSet",
70
70
  "listRoleSessionSets",
71
71
  "listRuntimeSessionCandidates",
72
- "listPendingRuntimeTurnCompletions",
73
72
  "getRoleSession",
74
73
  "getWorkItem",
75
74
  "listWorkItems",
@@ -7,10 +7,9 @@ import { validateCapabilityGrant } from "../grant/capabilityGrant.js";
7
7
  import { validateReleaseWorkflow } from "../release/releaseWorkflow.js";
8
8
  import { reconciliationIntervalMilliseconds, resolveLeaderNextActionMode, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode } from "../config/yuiConfig.js";
9
9
  import { resolveTimeZone } from "../output/timePresentation.js";
10
- import { mailboxTargetKey, validateWorkMailbox } from "../coordination/workMailbox.js";
10
+ import { mailboxBatches, consumePendingBatch, mailboxHasWork, mailboxTargetKey, pendingLane, 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";
14
13
  import { validateTaskMessage } from "../message/message.js";
15
14
  import { validateAgentRun } from "../run/agentRun.js";
16
15
  import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
@@ -71,7 +70,7 @@ export const CURRENT_MILESTONE_SCHEMA_VERSION = 1;
71
70
  export const CURRENT_EVENT_SCHEMA_VERSION = 2;
72
71
  export const CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION = 1;
73
72
  export const CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION = 1;
74
- export const CURRENT_WORK_MAILBOX_SCHEMA_VERSION = 1;
73
+ export const CURRENT_WORK_MAILBOX_SCHEMA_VERSION = 2;
75
74
  export const CURRENT_ROLE_AGENT_SESSION_SCHEMA_VERSION = 3;
76
75
  export const CURRENT_PENDING_WAKEUP_SCHEMA_VERSION = 1;
77
76
  const STORAGE_LOCK_DIRECTORY = ".state.lock";
@@ -111,7 +110,7 @@ export const CURRENT_STORED_TASK_SCHEMA_VERSION = 16;
111
110
  * Keep these named at the storage boundary so the upgrade record-axis map can
112
111
  * assert it is classifying the same bytes the store reads and writes.
113
112
  */
114
- export const CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION = 4;
113
+ export const CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION = 5;
115
114
  /**
116
115
  * v7 combines optional Issue 04 retry/receipt fields and Issue 05 Leader
117
116
  * actionability fields. All are optional, so the v6→v7 migration is a
@@ -779,27 +778,6 @@ export class FileTaskStore {
779
778
  .filter((candidate) => !query.cleanupRequiredOnly || candidate.cleanupRequired)
780
779
  .sort(compareRuntimeSessionCandidates);
781
780
  }
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
- }
803
781
  saveRoleSessionSet(sessions) {
804
782
  const stored = taskSessions(sessions);
805
783
  const taskId = stored.owner.taskId;
@@ -1484,7 +1462,7 @@ export class FileTaskStore {
1484
1462
  // in-memory aggregate while preserving the indexed SQLite contract's
1485
1463
  // target-key order; production layout 7 uses the bounded SQLite query.
1486
1464
  return Object.entries(this.#state().mailboxes)
1487
- .filter(([, mailbox]) => mailbox.processing !== null || mailbox.pending !== null)
1465
+ .filter(([, mailbox]) => mailboxHasWork(mailbox))
1488
1466
  .sort(([left], [right]) => left.localeCompare(right))
1489
1467
  .map(([, mailbox]) => clone(mailbox));
1490
1468
  }
@@ -1520,32 +1498,47 @@ export class FileTaskStore {
1520
1498
  const target = { kind: "role", taskId: wakeup.taskId, roleName: "leader" };
1521
1499
  this.transaction(() => {
1522
1500
  const existing = this.getWorkMailbox(target);
1523
- if (existing !== null && existing.pending !== null
1524
- && wakeup.requestCount <= existing.pending.requestCount) {
1501
+ const existingPending = existing === null ? null : pendingLane(existing, "normal");
1502
+ if (existingPending !== null
1503
+ && wakeup.requestCount <= existingPending.requestCount) {
1525
1504
  throw new StorageRecordError(`Pending wakeup is stale: ${wakeup.taskId}`);
1526
1505
  }
1527
- const fromSequence = existing?.pending?.fromSequence ?? existing?.nextSequence ?? 1;
1506
+ const fromSequence = existingPending?.fromSequence ?? existing?.nextSequence ?? 1;
1528
1507
  const toSequence = fromSequence + wakeup.requestCount - 1;
1529
1508
  this.saveWorkMailbox({
1530
1509
  schemaVersion: CURRENT_WORK_MAILBOX_SCHEMA_VERSION,
1531
1510
  target,
1532
1511
  nextSequence: Math.max(existing?.nextSequence ?? 1, toSequence + 1),
1533
1512
  processing: existing?.processing ?? null,
1513
+ inputDelivery: existing?.inputDelivery ?? null,
1534
1514
  pending: {
1535
- ...existing?.pending,
1536
- fromSequence,
1537
- toSequence,
1538
- reasons: [...wakeup.reasons],
1539
- refs: existing?.pending?.refs ?? [],
1540
- requestCount: wakeup.requestCount,
1541
- firstQueuedAt: wakeup.firstRequestedAt,
1542
- lastQueuedAt: wakeup.lastRequestedAt
1515
+ normal: {
1516
+ fromSequence,
1517
+ toSequence,
1518
+ reasons: [...wakeup.reasons],
1519
+ refs: existingPending?.refs ?? [],
1520
+ requestCount: wakeup.requestCount,
1521
+ firstQueuedAt: wakeup.firstRequestedAt,
1522
+ lastQueuedAt: wakeup.lastRequestedAt,
1523
+ sources: existingPending?.sources ?? ["pending-wakeup-projection"],
1524
+ dedupeKeys: existingPending?.dedupeKeys ?? [
1525
+ `pending-wakeup:${wakeup.taskId}:${fromSequence}-${toSequence}`
1526
+ ],
1527
+ deliveryModes: existingPending?.deliveryModes ?? ["followup"]
1528
+ },
1529
+ userCorrection: existing?.pending.userCorrection ?? null,
1530
+ cursors: existing?.pending.cursors ?? { normal: 0, userCorrection: 0 },
1531
+ recentDedupeKeys: existing?.pending.recentDedupeKeys ?? []
1543
1532
  }
1544
1533
  });
1545
1534
  });
1546
1535
  }
1547
1536
  clearPendingWakeup(taskId) {
1548
- this.removeWorkMailbox({ kind: "role", taskId, roleName: "leader" });
1537
+ const target = { kind: "role", taskId, roleName: "leader" };
1538
+ const mailbox = this.getWorkMailbox(target);
1539
+ if (mailbox === null || mailbox.pending.normal === null)
1540
+ return;
1541
+ this.saveWorkMailbox(consumePendingBatch(mailbox, "normal"));
1549
1542
  }
1550
1543
  getLeaderFailure(taskId) { return optional(this.#state().tasks[taskId]?.leaderFailure ?? undefined); }
1551
1544
  saveLeaderFailure(value) { this.#saveSingleton(value.taskId, "leaderFailure", value, "Leader failure"); }
@@ -2912,17 +2905,18 @@ function values(records, identity) {
2912
2905
  }
2913
2906
  function numericCompare(left, right) { return left.localeCompare(right, undefined, { numeric: true }); }
2914
2907
  export function pendingWakeupProjection(mailbox) {
2908
+ const pending = mailbox === null ? null : pendingLane(mailbox, "normal");
2915
2909
  if (mailbox === null || mailbox.target.kind !== "role" || mailbox.target.roleName !== "leader"
2916
- || mailbox.pending === null) {
2910
+ || pending === null) {
2917
2911
  return null;
2918
2912
  }
2919
2913
  return {
2920
2914
  schemaVersion: CURRENT_PENDING_WAKEUP_SCHEMA_VERSION,
2921
2915
  taskId: mailbox.target.taskId,
2922
- reasons: [...mailbox.pending.reasons],
2923
- requestCount: mailbox.pending.requestCount,
2924
- firstRequestedAt: mailbox.pending.firstQueuedAt,
2925
- lastRequestedAt: mailbox.pending.lastQueuedAt
2916
+ reasons: [...pending.reasons],
2917
+ requestCount: pending.requestCount,
2918
+ firstRequestedAt: pending.firstQueuedAt,
2919
+ lastRequestedAt: pending.lastQueuedAt
2926
2920
  };
2927
2921
  }
2928
2922
  function validateMailboxReferences(state, mailbox) {
@@ -2939,12 +2933,11 @@ function validateMailboxReferences(state, mailbox) {
2939
2933
  }
2940
2934
  const refs = [];
2941
2935
  if (mailbox.processing !== null) {
2942
- refs.push(...mailbox.processing.batch.refs);
2943
2936
  if (mailbox.processing.executionRef !== undefined)
2944
2937
  refs.push(mailbox.processing.executionRef);
2945
2938
  }
2946
- if (mailbox.pending !== null)
2947
- refs.push(...mailbox.pending.refs);
2939
+ for (const batch of mailboxBatches(mailbox))
2940
+ refs.push(...batch.refs);
2948
2941
  for (const ref of refs) {
2949
2942
  if (!mailboxReferenceExists(state, ref)) {
2950
2943
  const identity = "taskId" in ref ? `${ref.taskId}/${ref.id}` : ref.id;
@@ -3012,13 +3005,6 @@ function validateCanonicalTaskReferences(state, aggregate) {
3012
3005
  throw new StorageRecordError(`Task Role in-flight Run is invalid: ${taskId}/${roleName}.`);
3013
3006
  }
3014
3007
  }
3015
- if (sessions.pendingTurnCompletion !== null) {
3016
- const completion = sessions.pendingTurnCompletion;
3017
- const run = aggregate.agentRuns[completion.runId];
3018
- if (completion.taskId !== taskId || run === undefined || run.roleName !== roleName) {
3019
- throw new StorageRecordError(`Task Role pending completion Run is invalid: ${taskId}/${roleName}.`);
3020
- }
3021
- }
3022
3008
  }
3023
3009
  for (const run of Object.values(aggregate.agentRuns)) {
3024
3010
  if (run.workItemId !== undefined && aggregate.workItems[run.workItemId] === undefined) {
@@ -3316,6 +3302,7 @@ function mailboxReferenceExists(state, ref) {
3316
3302
  case "work-item": return aggregate.workItems[ref.id] !== undefined;
3317
3303
  case "input": return aggregate.inputRequests[ref.id] !== undefined;
3318
3304
  case "message": return aggregate.messages[ref.id] !== undefined;
3305
+ case "event": return aggregate.events[ref.id] !== undefined;
3319
3306
  }
3320
3307
  }
3321
3308
  switch (ref.type) {
@@ -49,7 +49,6 @@ export function copySqlitePassthroughState(home, sourceDatabaseFilename, targetD
49
49
  target.transaction(() => {
50
50
  mergeGlobalSequences(source, target);
51
51
  copyTableRows(source, target, "outbox");
52
- copyMailboxSignals(source, target);
53
52
  copyTableRows(source, target, "work_item_candidates");
54
53
  copyTableRows(source, target, "review_findings");
55
54
  copyTableRows(source, target, "telemetry");
@@ -78,26 +77,6 @@ function mergeGlobalSequences(source, target) {
78
77
  for (const row of rows)
79
78
  merge.run(row.name, row.high_water);
80
79
  }
81
- function copyMailboxSignals(source, target) {
82
- if (!sqliteTableExists(source, "mailbox_signals"))
83
- return;
84
- const rows = source.prepare(`SELECT m.target_key, s.sequence, s.reason, s.ref_type, s.ref_task_id,
85
- s.ref_id, s.occurred_at, s.request_id
86
- FROM mailbox_signals s
87
- JOIN mailboxes m ON m.mailbox_id = s.mailbox_id
88
- ORDER BY m.target_key, s.sequence`).iterate();
89
- const findMailbox = target.prepare("SELECT mailbox_id FROM mailboxes WHERE target_key = ?");
90
- const insert = target.prepare(`INSERT INTO mailbox_signals
91
- (mailbox_id, sequence, reason, ref_type, ref_task_id, ref_id, occurred_at, request_id)
92
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
93
- for (const row of rows) {
94
- const mailbox = findMailbox.get(row.target_key);
95
- if (mailbox === undefined) {
96
- throw new Error(`SQLite migration cannot preserve signals for missing mailbox ${row.target_key}.`);
97
- }
98
- insert.run(mailbox.mailbox_id, row.sequence, row.reason, row.ref_type, row.ref_task_id, row.ref_id, row.occurred_at, row.request_id);
99
- }
100
- }
101
80
  function copyTableRows(source, target, table) {
102
81
  if (!sqliteTableExists(source, table) || !sqliteTableExists(target, table))
103
82
  return;
@@ -244,7 +244,7 @@ export function projectNextAction(facts) {
244
244
  },
245
245
  {
246
246
  kind: "native-subagent",
247
- reason: "Use one native implementer subagent when one bounded implementation pass benefits from parallel attention.",
247
+ reason: "Use native implementer subagents when bounded work benefits from specialist attention or parallel fan-out inside the Leader Session.",
248
248
  refs
249
249
  }
250
250
  ],
@@ -47,7 +47,7 @@ const state = {
47
47
  detail: null,
48
48
  detailKey: null
49
49
  };
50
- const VALID_FILTERS = ["all", "active", "draft", "completed", "archived"];
50
+ const VALID_FILTERS = ["all", "active", "draft", "completed", "retired", "archived"];
51
51
  let terminalSession = null;
52
52
  let terminalStateKey = "terminal.closed";
53
53