@zq-silk/yui 2.17.0 → 2.18.0

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 (70) hide show
  1. package/dist/cli/commandCatalog.js +2 -1
  2. package/dist/commands/controllerCommands.js +13 -4
  3. package/dist/commands/durableJobCommands.js +50 -3
  4. package/dist/commands/globalRoleCommands.js +18 -12
  5. package/dist/commands/inboxCommands.js +1 -0
  6. package/dist/commands/operatorCommands.js +10 -2
  7. package/dist/controlPlaneCli.js +21 -4
  8. package/dist/controller/clientRuntime.js +34 -13
  9. package/dist/controller/controller.js +35 -22
  10. package/dist/controller/diagnosticJournal.js +214 -0
  11. package/dist/controller/fileSchedulerStoreAdapter.js +139 -101
  12. package/dist/controller/globalInputDelivery.js +6 -2
  13. package/dist/controller/globalRuntimeAttention.js +11 -3
  14. package/dist/controller/jobControl.js +1 -1
  15. package/dist/controller/jobDefinitionControl.js +24 -13
  16. package/dist/controller/jobSupervisor.js +4 -0
  17. package/dist/controller/runtime.js +66 -4
  18. package/dist/controller/workerSchedulerStore.js +24 -0
  19. package/dist/core/boundedRpc.js +4 -0
  20. package/dist/core/controllerServer.js +1 -1
  21. package/dist/core/controllerTelemetry.js +92 -19
  22. package/dist/job/globalShell.js +51 -0
  23. package/dist/job/jobBusinessResult.js +4 -3
  24. package/dist/job/jobDefinition.js +6 -1
  25. package/dist/job/jobRunner.js +9 -0
  26. package/dist/job/scheduleService.js +2 -0
  27. package/dist/message/globalInboxRecovery.js +30 -19
  28. package/dist/message/globalProviderRetry.js +7 -1
  29. package/dist/runtime/builtinAgentErrorMappers.js +4 -0
  30. package/dist/runtime/globalSessionHealth.js +39 -0
  31. package/dist/scheduler/roleRunStall.js +10 -10
  32. package/dist/storage/persistenceWorker.js +41 -19
  33. package/dist/storage/sqliteSchema.js +35 -11
  34. package/dist/storage/sqliteStore.js +86 -8
  35. package/dist/storage/storageVersions.js +1 -1
  36. package/dist/storage/storeRpc.js +25 -11
  37. package/dist/storage/taskReadRevisionSchema.js +39 -0
  38. package/dist/web/assets/client/app/catalog.js +0 -29
  39. package/dist/web/assets/client/app/jobs.js +115 -0
  40. package/dist/web/assets/client/app/main.js +17 -6
  41. package/dist/web/assets/client/app/manage.js +1 -0
  42. package/dist/web/assets/client/app/settings.js +1 -5
  43. package/dist/web/assets/client/i18n/en.js +203 -18
  44. package/dist/web/assets/client/i18n/zh-CN.js +202 -17
  45. package/dist/web/assets/client/index.js +2 -0
  46. package/dist/web/assets/client/layout/manageShell.js +61 -15
  47. package/dist/web/assets/client/layout/router.js +2 -1
  48. package/dist/web/assets/client/lib/prefs.js +1 -1
  49. package/dist/web/assets/client/views/jobs.js +1042 -160
  50. package/dist/web/assets/client/views/sidebar.js +3 -0
  51. package/dist/web/assets/shared/manageNav.js +3 -1
  52. package/dist/web/assets/shell/appRail.js +2 -1
  53. package/dist/web/assets/shell/manage.js +7 -2
  54. package/dist/web/assets/shell/models.js +4 -3
  55. package/dist/web/assets/shell/settingsContent.js +0 -3
  56. package/dist/web/assets/shell/sidebar.js +0 -1
  57. package/dist/web/assets/styles/views/jobs.js +301 -12
  58. package/dist/web/assets/styles/views/sidebar.js +1 -2
  59. package/dist/web/webServer.js +28 -1
  60. package/docs/global-shell-jobs.md +78 -0
  61. package/docs/job-runs.md +6 -3
  62. package/docs/managed-turn-and-session-runtime.md +74 -0
  63. package/docs/managed-turn-and-session-runtime.zh-CN.md +51 -0
  64. package/docs/release-workflow.md +43 -0
  65. package/docs/release-workflow.zh-CN.md +29 -0
  66. package/docs/storage-baseline.md +14 -1
  67. package/docs/storage-baseline.zh-CN.md +6 -1
  68. package/docs/testing/verification-levels.md +2 -2
  69. package/docs/testing/verification-levels.zh-CN.md +2 -2
  70. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { assertJobRoleRunnable } from "../job/agentRoleScope.js";
3
3
  import { recordJobAgentObservation } from "../job/agentJobObservation.js";
4
4
  import { isDeepStrictEqual } from "node:util";
5
5
  import { nonReplayableMessageWake, pinMessageRevisions, taskMessageInboxReceipt, unfinishedLeaderInbox } from "../message/taskInboxOwnership.js";
6
+ import { ControllerWorkObserver, compactWorkObservation } from "../core/controllerTelemetry.js";
6
7
  import { contextSnapshotRef } from "../context/contextSnapshot.js";
7
8
  import { freezeRunContextSnapshot } from "../context/runContextPack.js";
8
9
  import { taskNotificationPresentation } from "../interaction/notificationPresentation.js";
@@ -34,6 +35,8 @@ import { recordTaskAgentError } from "./taskAgentError.js";
34
35
  import { recordGlobalRuntimeAttention, recordGlobalRuntimeFailure, recordGlobalRuntimeRecovery } from "./globalRuntimeAttention.js";
35
36
  import { checkInboxTurn } from "./inboxTurnCheck.js";
36
37
  import { queueOperatorNotification } from "./operatorNotification.js";
38
+ import { reconcileProjectSync } from "../job/projectSync.js";
39
+ import { serializeError } from "../core/boundedRpc.js";
37
40
  import { claimOperatorAttention } from "../scheduler/operatorEvent.js";
38
41
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
39
42
  import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_HOST_DETACH_REQUIRED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeCleanupReason, runtimeCleanupDisposition, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
@@ -65,6 +68,26 @@ export class FileSchedulerStoreAdapter {
65
68
  telemetry;
66
69
  drivers;
67
70
  snapshotExecutionLaneWorkspace;
71
+ #reads = new ControllerWorkObserver();
72
+ #projectionCache = { hits: 0, rebuilds: 0, invalidations: 0 };
73
+ readObservationMetrics() {
74
+ return { ...this.#reads.snapshot(), projectionCache: { ...this.#projectionCache } };
75
+ }
76
+ compactReadObservationMetrics() {
77
+ return compactWorkObservation(this.#reads);
78
+ }
79
+ #observeRead(phase, target, read) {
80
+ const finish = this.#reads.start(phase, target);
81
+ try {
82
+ const result = read();
83
+ finish();
84
+ return result;
85
+ }
86
+ catch (error) {
87
+ finish(true);
88
+ throw error;
89
+ }
90
+ }
68
91
  /** Diagnostic telemetry is optional and never participates in runtime truth. */
69
92
  constructor(store, telemetry = null, drivers = builtinAgentDriverRegistry(), snapshotExecutionLaneWorkspace = snapshotExecutionLaneWorkspaceSync) {
70
93
  this.store = store;
@@ -72,6 +95,12 @@ export class FileSchedulerStoreAdapter {
72
95
  this.drivers = drivers;
73
96
  this.snapshotExecutionLaneWorkspace = snapshotExecutionLaneWorkspace;
74
97
  }
98
+ /** Registry maintenance only; this never executes a Job or starts a process. */
99
+ reconcileProjectSync(now) {
100
+ const errors = [];
101
+ reconcileProjectSync(this.store, now, error => errors.push(serializeError(error)));
102
+ return errors;
103
+ }
75
104
  observeAgentHostObservation(event, now = new Date()) {
76
105
  // Match canonical ingress: archive and Host fact settlement share the
77
106
  // same write boundary, including the original transport envelope.
@@ -168,7 +197,7 @@ export class FileSchedulerStoreAdapter {
168
197
  && sessions?.sessions[input.fence.agentId]?.nativeSessionId === input.fence.nativeSessionId;
169
198
  if (!runFolded && !(isSteer && input.fence.runId === undefined))
170
199
  return "obsolete";
171
- if (runFolded && !hasPersistedRuntimeObservation(store.listEvents(taskId), input) && run.status === "active") {
200
+ if (runFolded && !hasPersistedRuntimeObservation(store, input) && run.status === "active") {
172
201
  const updated = appendRunInput(run, createRunInput({
173
202
  source: input.fence.receiptId.startsWith("native-input:")
174
203
  ? { type: "provider", channel: "visible-input" }
@@ -196,7 +225,7 @@ export class FileSchedulerStoreAdapter {
196
225
  if ((input.kind === "input.rejected" || input.kind === "input.delivery-unknown")
197
226
  && input.fence.receiptId?.startsWith("steer:") === true) {
198
227
  return this.store.transaction((store) => {
199
- if (hasPersistedRuntimeObservation(store.listEvents(taskId), input))
228
+ if (hasPersistedRuntimeObservation(store, input))
200
229
  return "applied";
201
230
  const recorded = this.recordSteerControlOutcome(store, input.fence.receiptId, input.kind === "input.rejected" ? "rejected" : "delivery-unknown", now);
202
231
  if (!recorded) {
@@ -217,7 +246,7 @@ export class FileSchedulerStoreAdapter {
217
246
  // together, including a retry after a previously interrupted fold.
218
247
  if (!isRunTerminalObservation(input)
219
248
  && input.kind !== "turn.accepted" && input.kind !== "input.accepted"
220
- && hasPersistedRuntimeObservation(this.store.listEvents(taskId), input))
249
+ && hasPersistedRuntimeObservation(this.store, input))
221
250
  return "applied";
222
251
  let outcome;
223
252
  switch (input.kind) {
@@ -330,7 +359,7 @@ export class FileSchedulerStoreAdapter {
330
359
  recordCanonicalObservationObsolete(store, input, "steer-receipt-fence-mismatch", now);
331
360
  return "obsolete";
332
361
  }
333
- if (hasPersistedRuntimeObservation(store.listEvents(taskId), input))
362
+ if (hasPersistedRuntimeObservation(store, input))
334
363
  return "applied";
335
364
  const target = { kind: "role", taskId, roleName: "leader" };
336
365
  const mailbox = store.getWorkMailbox(target);
@@ -408,7 +437,7 @@ export class FileSchedulerStoreAdapter {
408
437
  }
409
438
  if (input.kind === "turn.accepted") {
410
439
  if (input.authority === "transport" && matches) {
411
- const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`);
440
+ const message = globalMessageForReceipt(store, fence.roleName, receipt);
412
441
  if (message !== undefined)
413
442
  store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at), "transport"));
414
443
  return "applied";
@@ -428,9 +457,8 @@ export class FileSchedulerStoreAdapter {
428
457
  }
429
458
  else if (binding.run?.status !== "accepted")
430
459
  return "applied";
431
- const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`
432
- || binding?.retry?.currentAttemptId === receipt && binding.retry.input.kind === "message"
433
- && binding.retry.input.messageId === entry.id);
460
+ const message = globalMessageForReceipt(store, fence.roleName, receipt, "global-input", binding?.retry?.currentAttemptId === receipt && binding.retry.input.kind === "message"
461
+ ? binding.retry.input.messageId : undefined);
434
462
  if (message !== undefined)
435
463
  store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at)));
436
464
  }
@@ -443,9 +471,8 @@ export class FileSchedulerStoreAdapter {
443
471
  if (input.kind === "turn.completed" && binding.run != null
444
472
  && ["submitting", "delivery-unknown"].includes(binding.run.status)) {
445
473
  binding = acceptProviderTurn(binding, { attemptId: receipt, nativeTurnId: fence.nativeTurnId, acceptedAt: at });
446
- const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`
447
- || binding?.retry?.currentAttemptId === receipt && binding.retry.input.kind === "message"
448
- && binding.retry.input.messageId === entry.id);
474
+ const message = globalMessageForReceipt(store, fence.roleName, receipt, "global-input", binding?.retry?.currentAttemptId === receipt && binding.retry.input.kind === "message"
475
+ ? binding.retry.input.messageId : undefined);
449
476
  if (message !== undefined)
450
477
  store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at)));
451
478
  }
@@ -466,7 +493,7 @@ export class FileSchedulerStoreAdapter {
466
493
  if (input.authority !== "provider-structured" || binding.run?.nativeTurnId !== fence.nativeTurnId
467
494
  || binding.run?.status !== "accepted")
468
495
  return "obsolete";
469
- const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `steer:${fence.roleName}/${entry.id}`);
496
+ const message = globalMessageForReceipt(store, fence.roleName, receipt, "steer");
470
497
  if (message !== undefined && message.inputControl !== undefined) {
471
498
  const outcome = input.kind === "input.accepted" ? "accepted"
472
499
  : input.kind === "input.rejected" ? "rejected" : "delivery-unknown";
@@ -548,7 +575,7 @@ export class FileSchedulerStoreAdapter {
548
575
  }
549
576
  validateCanonicalRunObservation(input, now) {
550
577
  return this.store.transaction((store) => {
551
- if (hasPersistedRuntimeObservation(store.listEvents(input.fence.taskId), input)) {
578
+ if (hasPersistedRuntimeObservation(store, input)) {
552
579
  return "applied";
553
580
  }
554
581
  const run = input.fence.runId === undefined ? null : store.getRun(input.fence.taskId, input.fence.runId);
@@ -565,7 +592,7 @@ export class FileSchedulerStoreAdapter {
565
592
  ? "applied" : "obsolete";
566
593
  }
567
594
  const knownContinuation = (input.kind.startsWith("continuation.") || input.kind === "observer.health")
568
- && projectProviderContinuations(store.listEvents(input.fence.taskId)).some((entry) => (entry.runId === input.fence.runId
595
+ && projectProviderContinuations(store.listEventsByType(input.fence.taskId, [RUNTIME_OBSERVATION_TASK_EVENT])).some((entry) => (entry.runId === input.fence.runId
569
596
  && entry.identity.providerNamespace === input.fence.driverId
570
597
  && entry.identity.accountScope === input.fence.agentId
571
598
  && entry.identity.conversationId === input.fence.conversationId
@@ -647,7 +674,7 @@ export class FileSchedulerStoreAdapter {
647
674
  const status = "ended";
648
675
  let updatedSessions = updateRoleAgentSessionStatus(sessions, input.fence.agentId, status, now, input.kind === "session.failed" ? "failed" : "stopped");
649
676
  store.saveTaskRoleSessionSet(updatedSessions);
650
- for (const continuation of projectProviderContinuations(store.listEvents(input.fence.taskId))) {
677
+ for (const continuation of projectProviderContinuations(store.listEventsByType(input.fence.taskId, [RUNTIME_OBSERVATION_TASK_EVENT]))) {
651
678
  if (continuation.runId !== input.fence.runId || continuation.identity.conversationId
652
679
  !== (input.fence.conversationId ?? input.fence.nativeSessionId) || continuation.execution === "quiescent" || continuation.attachment === "detached")
653
680
  continue;
@@ -721,9 +748,9 @@ export class FileSchedulerStoreAdapter {
721
748
  persistRuntimeObservation(input, now, notifyGoal = true) {
722
749
  this.store.transaction((store) => {
723
750
  const taskId = input.fence.taskId;
724
- const events = store.listEvents(taskId);
725
- if (hasPersistedRuntimeObservation(events, input))
751
+ if (hasPersistedRuntimeObservation(store, input))
726
752
  return;
753
+ const events = store.listEventsByType(taskId, [RUNTIME_OBSERVATION_TASK_EVENT]);
727
754
  if (usageSnapshotIsSuperseded(events, input))
728
755
  return;
729
756
  const removable = compactedRuntimeObservationIds(events, input);
@@ -851,43 +878,53 @@ export class FileSchedulerStoreAdapter {
851
878
  return "obsolete";
852
879
  }
853
880
  /**
854
- * Revision-scoped read projection. Keyed by the store's durable revision;
855
- * any committed mutation (ours or an external writer's) advances it and the
856
- * next read rebuilds. A store without getStateRevision disables caching.
881
+ * Task-scoped cache tokens cover the exact source tables across connections.
882
+ * Unrelated Tasks/Global traffic do not invalidate these projections. Retain
883
+ * only the 16 most recently read Tasks; eviction changes cost, never truth.
857
884
  */
858
- #readProjection = null;
859
- #taskReadProjection(taskId) {
860
- const revision = typeof this.store.getStateRevision === "function"
861
- ? this.store.getStateRevision()
862
- : Number.NaN;
863
- if (this.#readProjection === null
864
- || this.#readProjection.revision !== revision) {
865
- this.#readProjection = { revision, tasks: new Map() };
885
+ #readProjection = new Map();
886
+ #projected(taskId, family, read) {
887
+ const observed = this.store.getTaskReadRevision(taskId);
888
+ const previous = this.#readProjection.get(taskId);
889
+ const hit = previous?.revision === observed ? previous.value[family] : undefined;
890
+ if (hit !== undefined) {
891
+ // The token read is the read's snapshot point. A cache hit needs no new
892
+ // transaction: its value was built under that exact committed token.
893
+ this.#projectionCache.hits++;
894
+ this.#readProjection.delete(taskId);
895
+ this.#readProjection.set(taskId, previous);
896
+ return hit;
866
897
  }
867
- const tasks = this.#readProjection.tasks;
868
- let task = tasks.get(taskId);
869
- if (task === undefined) {
870
- const events = this.store.listEvents(taskId);
871
- task = {
872
- events,
873
- runFacts: foldRunProgressFacts(events),
874
- runs: this.store.listRuns(taskId),
875
- workItems: this.store.listWorkItems(taskId),
876
- reviewRounds: this.store.listReviewRounds(taskId),
877
- changeSets: this.store.listChangeSets(taskId),
878
- integrationAttempts: this.store.listIntegrationAttempts(taskId),
879
- inputRequests: this.store.listInputRequests(taskId),
880
- durableJobs: this.store.listDurableJobs(taskId),
881
- messages: this.store.listMessages(taskId)
882
- };
883
- tasks.set(taskId, task);
884
- }
885
- return task;
898
+ // Pin and recheck misses before reading any source rows: an external writer
899
+ // may have advanced the token after the optimistic lookup above.
900
+ return this.store.readTransaction(() => {
901
+ const revision = this.store.getTaskReadRevision(taskId);
902
+ let task = this.#readProjection.get(taskId);
903
+ if (task === undefined || task.revision !== revision) {
904
+ if (task !== undefined)
905
+ this.#projectionCache.invalidations++;
906
+ this.#projectionCache.rebuilds++;
907
+ task = { revision, value: {} };
908
+ }
909
+ this.#readProjection.delete(taskId);
910
+ this.#readProjection.set(taskId, task);
911
+ if (this.#readProjection.size > 16) {
912
+ this.#readProjection.delete(this.#readProjection.keys().next().value);
913
+ }
914
+ const cached = task.value[family];
915
+ if (cached !== undefined) {
916
+ this.#projectionCache.hits++;
917
+ return cached;
918
+ }
919
+ const value = this.#observeRead(`task-${family}-read`, taskId, read);
920
+ task.value[family] = value;
921
+ return value;
922
+ });
886
923
  }
887
924
  withRuntimeEventTransaction(execute) {
888
925
  return this.store.withRuntimeEventTransaction(execute);
889
926
  }
890
- listTasks() { return this.store.listTasks(); }
927
+ listTasks() { return this.#observeRead("list-tasks", "home", () => this.store.listTasks()); }
891
928
  listActiveTaskIds() {
892
929
  return [...this.store.listActiveTaskIds()].sort((left, right) => (left.localeCompare(right, undefined, { numeric: true })));
893
930
  }
@@ -980,51 +1017,48 @@ export class FileSchedulerStoreAdapter {
980
1017
  return this.store.getTaskRoleSessionSet(taskId, roleName);
981
1018
  }
982
1019
  listEvents(taskId) {
983
- return this.#taskReadProjection(taskId).events;
1020
+ return this.#projected(taskId, "events", () => this.store.listEvents(taskId));
984
1021
  }
985
1022
  listRuns(taskId) {
986
- return this.#taskReadProjection(taskId).runs;
1023
+ return this.#projected(taskId, "runs", () => this.store.listRuns(taskId));
987
1024
  }
988
1025
  listWorkItems(taskId) {
989
- return this.#taskReadProjection(taskId).workItems;
1026
+ return this.#projected(taskId, "workItems", () => this.store.listWorkItems(taskId));
990
1027
  }
991
1028
  listReviewRounds(taskId) {
992
- return this.#taskReadProjection(taskId).reviewRounds;
1029
+ return this.#projected(taskId, "reviewRounds", () => this.store.listReviewRounds(taskId));
993
1030
  }
994
1031
  listIntegrationAttempts(taskId) {
995
- return this.#taskReadProjection(taskId).integrationAttempts;
1032
+ return this.#projected(taskId, "integrationAttempts", () => this.store.listIntegrationAttempts(taskId));
996
1033
  }
997
1034
  listDurableJobs(taskId) {
998
- return this.#taskReadProjection(taskId).durableJobs;
1035
+ return this.#projected(taskId, "durableJobs", () => this.store.listDurableJobs(taskId));
999
1036
  }
1000
1037
  listInputRequests(taskId) {
1001
- return this.#taskReadProjection(taskId).inputRequests;
1038
+ return this.#projected(taskId, "inputRequests", () => this.store.listInputRequests(taskId));
1002
1039
  }
1003
1040
  listMessages(taskId) {
1004
- return this.#taskReadProjection(taskId).messages;
1041
+ return this.#projected(taskId, "messages", () => this.store.listMessages(taskId));
1005
1042
  }
1006
1043
  getRunProgressFacts(taskId, runId) {
1007
- return this.#taskReadProjection(taskId).runFacts.get(runId);
1044
+ return this.#projected(taskId, "runFacts", () => foldRunProgressFacts(this.listEvents(taskId))).get(runId);
1008
1045
  }
1009
1046
  getRunDurableProgress(taskId, roleName, runId) {
1010
- const projected = this.#taskReadProjection(taskId);
1011
- // Serve the related-record fold from the same revision's projection: the
1012
- // event history and the WorkItem/Review/ChangeSet/Integration/Input lists
1013
- // are read once per Task per revision, and the per-AgentRun checkpoint/activity
1014
- // facts come from the one-pass fold instead of per-candidate scans.
1015
- const view = {
1016
- getRun: (id, runId) => this.store.getRun(id, runId),
1017
- listEvents: () => projected.events,
1018
- getWorkItem: (id, workItemId) => this.store.getWorkItem(id, workItemId),
1019
- listReviewRounds: () => projected.reviewRounds,
1020
- listChangeSets: () => projected.changeSets,
1021
- listIntegrationAttempts: () => projected.integrationAttempts,
1022
- listInputRequests: () => projected.inputRequests
1023
- };
1024
- // A missing fold entry is an authoritative empty fold, not a signal to
1025
- // re-scan the whole history. Pass {} so latestTurnDurableProgressAt treats
1026
- // the fold as present and skips the per-candidate fallback scans.
1027
- return latestRunDurableProgressAt(view, taskId, roleName, runId, projected.runFacts.get(runId) ?? {});
1047
+ return this.store.readTransaction(() => {
1048
+ // Pin related reads to one snapshot while loading only required families.
1049
+ const view = {
1050
+ getRun: (id, runId) => this.store.getRun(id, runId),
1051
+ listEvents: () => this.listEvents(taskId),
1052
+ getWorkItem: (id, workItemId) => this.store.getWorkItem(id, workItemId),
1053
+ listReviewRounds: () => this.listReviewRounds(taskId),
1054
+ listChangeSets: () => this.#projected(taskId, "changeSets", () => this.store.listChangeSets(taskId)),
1055
+ listIntegrationAttempts: () => this.listIntegrationAttempts(taskId),
1056
+ listInputRequests: () => this.listInputRequests(taskId)
1057
+ };
1058
+ // A missing fold entry is authoritative empty evidence, not permission
1059
+ // to fall back to a per-candidate full-history scan.
1060
+ return latestRunDurableProgressAt(view, taskId, roleName, runId, this.getRunProgressFacts(taskId, runId) ?? {});
1061
+ });
1028
1062
  }
1029
1063
  recordRoleRunDiagnostic(input) {
1030
1064
  return this.store.transaction((store) => {
@@ -1160,12 +1194,11 @@ export class FileSchedulerStoreAdapter {
1160
1194
  if (turn != null && ["submitting", "accepted", "delivery-unknown"].includes(turn.status)) {
1161
1195
  throw new AgentHostProviderSessionBusyError("Global Conversation has an unsettled input.");
1162
1196
  }
1163
- const message = store.listGlobalRoleMessages(input.roleName).find(entry => input.attemptId === `global-input:${input.roleName}/${entry.id}`);
1164
- const reserved = store.listGlobalRoleMessages(input.roleName).find(entry => entry.delivery === undefined && entry.notDelivered === undefined
1165
- && entry.interruptThen?.targetNativeSessionId === input.nativeSessionId
1166
- && entry.interruptThen.targetAgentId === input.agentId
1167
- && entry.interruptThen.targetAuthorityEpoch === input.authorityEpoch
1168
- && entry.interruptThen.targetAuthorityHolderId === input.holderId);
1197
+ const message = globalMessageForReceipt(store, input.roleName, input.attemptId);
1198
+ const reserved = store.queryGlobalRoleMessages(input.roleName, {
1199
+ limit: 1, reservedThen: { nativeSessionId: input.nativeSessionId,
1200
+ agentId: input.agentId, authorityEpoch: input.authorityEpoch, holderId: input.holderId }
1201
+ }).messages[0];
1169
1202
  if (reserved !== undefined && reserved.id !== message?.id) {
1170
1203
  throw new AgentHostProviderSessionBusyError("The next Global input belongs to an explicit then handoff.");
1171
1204
  }
@@ -1285,7 +1318,7 @@ export class FileSchedulerStoreAdapter {
1285
1318
  // This shares the exact writer/session admission and its transaction.
1286
1319
  if ((input.authorityOwner === "human" || input.attemptId.startsWith(`human:${input.holderId}:`)) && input.roleName === "leader"
1287
1320
  && input.runId === undefined && input.boundedText !== undefined) {
1288
- const prior = store.listEvents(input.taskId).find(event => event.type === "message.native-user-input" && event.payload.attemptId === input.attemptId);
1321
+ const prior = store.listEventsByType(input.taskId, ["message.native-user-input"]).find(event => event.payload.attemptId === input.attemptId);
1289
1322
  if (prior !== undefined) {
1290
1323
  const message = store.listMessages(input.taskId).find(entry => entry.id === prior.payload.messageId);
1291
1324
  if (message?.body !== input.boundedText || prior.payload.nativeSessionId !== input.nativeSessionId) {
@@ -1344,7 +1377,7 @@ export class FileSchedulerStoreAdapter {
1344
1377
  unknown: "External effects are not established by submission settlement."
1345
1378
  }, input.now);
1346
1379
  }
1347
- const message = store.listGlobalRoleMessages(input.roleName).find(entry => input.attemptId === `global-input:${input.roleName}/${entry.id}`);
1380
+ const message = globalMessageForReceipt(store, input.roleName, input.attemptId);
1348
1381
  if (message !== undefined && message.delivery?.via !== "provider") {
1349
1382
  const outcome = input.status === "delivery-unknown" ? "delivery-unknown" : "rejected";
1350
1383
  const updated = { ...message, control: {
@@ -1890,9 +1923,9 @@ export class FileSchedulerStoreAdapter {
1890
1923
  return null;
1891
1924
  const wakeId = processing.owner.slice("leader-notification:".length);
1892
1925
  const attemptId = processing.batchId;
1893
- const previous = store.listEvents(taskId).filter((event) => event.type === "notification.delivery" && event.payload.attemptId === attemptId).at(-1);
1926
+ const previous = store.listEventsByType(taskId, ["notification.delivery"]).filter((event) => event.payload.attemptId === attemptId).at(-1);
1894
1927
  const native = provider?.attemptId === attemptId ? provider : undefined;
1895
- const accepted = store.listEvents(taskId).some((event) => {
1928
+ const accepted = store.listEventsByType(taskId, [RUNTIME_OBSERVATION_TASK_EVENT]).some((event) => {
1896
1929
  const observation = runtimeObservationFromTaskEvent(event);
1897
1930
  return observation?.fence.roleName === "leader"
1898
1931
  && observation.fence.receiptId === attemptId
@@ -2016,8 +2049,7 @@ export class FileSchedulerStoreAdapter {
2016
2049
  if (mailbox == null || processing == null || processing.batchId !== attemptId
2017
2050
  || !processing.owner.startsWith("leader-notification:"))
2018
2051
  return;
2019
- if (!store.listEvents(taskId).some((event) => event.type === "notification.delivery"
2020
- && event.payload.attemptId === attemptId && event.payload.outcome === outcome)) {
2052
+ if (!store.listEventsByType(taskId, ["notification.delivery"]).some((event) => event.payload.attemptId === attemptId && event.payload.outcome === outcome)) {
2021
2053
  // Native ingress may already have persisted the same rejection. Reuse
2022
2054
  // that original failure rather than reporting it twice from the caller.
2023
2055
  const existing = store.listEventsByType(taskId, ["runtime.agent-error"])
@@ -2083,7 +2115,7 @@ export class FileSchedulerStoreAdapter {
2083
2115
  nativeSessionId: input.session.nativeSessionId
2084
2116
  }, "active", input.now);
2085
2117
  }
2086
- if (!defersConversationReplacement && !store.listEvents(input.task.id).some((event) => event.type === "run.session-prepared" && event.payload.runId === input.run.id
2118
+ if (!defersConversationReplacement && !store.listEventsByType(input.task.id, ["run.session-prepared"]).some((event) => event.payload.runId === input.run.id
2087
2119
  && event.payload.nativeSessionId === input.session.nativeSessionId)) {
2088
2120
  store.saveEvent(input.task.id, createTaskEvent(store.nextEventId(input.task.id), input.task.id, "run.session-prepared", { runId: input.run.id, roleName: role.name,
2089
2121
  nativeSessionId: input.session.nativeSessionId }, input.now));
@@ -2235,7 +2267,7 @@ export class FileSchedulerStoreAdapter {
2235
2267
  && preparedRun.executionLaneId !== undefined
2236
2268
  && preparedRun.workspace !== undefined
2237
2269
  && input.outcome.status === "completed"
2238
- ? this.snapshotExecutionLaneWorkspace(this.store, preparedRun.workspace)
2270
+ ? this.#observeRead("git-workspace-snapshot", preparedRun.taskId, () => this.snapshotExecutionLaneWorkspace(this.store, preparedRun.workspace))
2239
2271
  : undefined;
2240
2272
  return this.store.transaction((store) => {
2241
2273
  const task = store.getTask(input.taskId);
@@ -2476,7 +2508,7 @@ export class FileSchedulerStoreAdapter {
2476
2508
  observeProviderGoal(input, now) {
2477
2509
  return this.store.transaction((store) => {
2478
2510
  const taskId = input.fence.taskId;
2479
- if (hasPersistedRuntimeObservation(store.listEvents(taskId), input))
2511
+ if (hasPersistedRuntimeObservation(store, input))
2480
2512
  return "applied";
2481
2513
  const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
2482
2514
  const binding = sessions?.providerBinding;
@@ -2798,7 +2830,7 @@ export class FileSchedulerStoreAdapter {
2798
2830
  ...(boundNativeSessionId === undefined ? {} : { nativeSessionId: boundNativeSessionId })
2799
2831
  };
2800
2832
  const driverId = this.drivers.requireByAdapterId(run.effective.adapterId).id;
2801
- const lifecycleEvents = store.listEvents(fence.taskId).flatMap((event) => {
2833
+ const lifecycleEvents = store.listEventsByType(fence.taskId, [RUNTIME_OBSERVATION_TASK_EVENT]).flatMap((event) => {
2802
2834
  const observation = runtimeObservationFromTaskEvent(event);
2803
2835
  return observation !== null && (observation.kind === "session.started" || observation.kind === "session.ready") && observation.fence.roleName === fence.roleName && observation.fence.agentId === run.effective.agentId && observation.fence.driverId === driverId && observation.fence.nativeSessionId === (boundNativeSessionId ?? fence.nativeSessionId)
2804
2836
  ? [observation]
@@ -2966,7 +2998,7 @@ function runtimeReceiptBelongsToRun(store, input) {
2966
2998
  && sessions?.sessions[input.fence.agentId]?.nativeSessionId === input.fence.nativeSessionId) {
2967
2999
  return true;
2968
3000
  }
2969
- return store.listEvents(taskId).some((event) => {
3001
+ return store.listEventsByType(taskId, [RUNTIME_OBSERVATION_TASK_EVENT]).some((event) => {
2970
3002
  const accepted = runtimeObservationFromTaskEvent(event);
2971
3003
  return accepted?.kind === "turn.accepted" && accepted.fence.runId === runId && accepted.fence.roleName === input.fence.roleName && accepted.fence.agentId === input.fence.agentId && accepted.fence.nativeSessionId === input.fence.nativeSessionId && accepted.fence.receiptId === receiptId;
2972
3004
  });
@@ -2988,8 +3020,7 @@ function recordCanonicalObservationObsolete(store, input, reason, now) {
2988
3020
  }, reason, now);
2989
3021
  }
2990
3022
  function recordObsoleteRuntimeEvent(store, input, reason, now) {
2991
- if (store.listEvents(input.taskId).some((event) => (event.type === "runtime.event-obsolete"
2992
- && (event.payload.dedupeKey ?? event.payload.eventId) === (input.dedupeKey ?? input.eventId))))
3023
+ if (store.listEventsByType(input.taskId, ["runtime.event-obsolete"]).some((event) => ((event.payload.dedupeKey ?? event.payload.eventId) === (input.dedupeKey ?? input.eventId))))
2993
3024
  return;
2994
3025
  store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.event-obsolete", {
2995
3026
  eventId: input.eventId,
@@ -3513,7 +3544,7 @@ function resolveTerminalExecution(store, input) {
3513
3544
  const session = sessions?.sessions[input.agentId];
3514
3545
  const binding = sessions?.providerBinding;
3515
3546
  const pending = binding?.run;
3516
- const evidence = store.listEvents(input.taskId).map(runtimeObservationFromTaskEvent)
3547
+ const evidence = store.listEventsByType(input.taskId, [RUNTIME_OBSERVATION_TASK_EVENT]).map(runtimeObservationFromTaskEvent)
3517
3548
  .filter((event) => event !== null
3518
3549
  && (event.kind === "turn.accepted" || isRunTerminalObservation(event)));
3519
3550
  if (input.attemptId !== undefined && evidence.some((event) => (event.fence.roleName === input.roleName
@@ -3574,7 +3605,7 @@ function isRunTerminalObservation(input) {
3574
3605
  return ["turn.completed", "turn.failed", "turn.cancelled"].includes(input.kind);
3575
3606
  }
3576
3607
  function assertConsistentTerminal(store, input, run) {
3577
- const previous = store.listEvents(input.taskId).map(runtimeObservationFromTaskEvent)
3608
+ const previous = store.listEventsByType(input.taskId, [RUNTIME_OBSERVATION_TASK_EVENT]).map(runtimeObservationFromTaskEvent)
3578
3609
  .filter((event) => event !== null
3579
3610
  && isRunTerminalObservation(event)
3580
3611
  && event.fence.roleName === input.roleName
@@ -3700,14 +3731,21 @@ function usageSnapshotIsSuperseded(events, incoming) {
3700
3731
  && runtimeObservationFenceMatches(observation.fence, incoming.fence)
3701
3732
  && observationIsStrictlyNewer(observation, incoming));
3702
3733
  }
3703
- function hasPersistedRuntimeObservation(events, incoming) {
3734
+ function hasPersistedRuntimeObservation(store, incoming) {
3704
3735
  const usageIdentity = isRuntimeUsageEvidence(incoming)
3705
3736
  ? incoming.eventId
3706
3737
  : undefined;
3707
- return events.some((event) => (event.type === RUNTIME_OBSERVATION_TASK_EVENT
3708
- && (usageIdentity === undefined
3709
- ? event.payload.semanticKey === incoming.semanticKey
3710
- : event.payload.eventId === usageIdentity)));
3738
+ return store.hasRuntimeObservation(incoming.fence.taskId, usageIdentity === undefined
3739
+ ? { semanticKey: incoming.semanticKey } : { eventId: usageIdentity });
3740
+ }
3741
+ function globalMessageForReceipt(store, roleName, receipt, kind = "global-input", retryMessageId) {
3742
+ const prefix = `${kind}:${roleName}/`;
3743
+ const id = receipt?.startsWith(prefix) ? receipt.slice(prefix.length) : undefined;
3744
+ // A retry references the original Message, not a second input. Preserve the
3745
+ // old numeric-id ordering if both explicit identities name different rows.
3746
+ return [...new Set([id, retryMessageId].filter((value) => value !== undefined))]
3747
+ .flatMap(id => store.queryGlobalRoleMessages(roleName, { id, limit: 1 }).messages)
3748
+ .sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }))[0];
3711
3749
  }
3712
3750
  function observationIsStrictlyNewer(left, right) {
3713
3751
  return compareCanonicalObservationOrder(left, right) > 0;
@@ -4,7 +4,7 @@ import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.
4
4
  import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
5
5
  import { markGlobalRoleMessageNotDelivered } from "../message/message.js";
6
6
  import { providerRetryPending } from "../runtime/providerRetry.js";
7
- import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
7
+ import { globalRetryInputSettlement, settleGlobalRetryInput } from "../message/globalProviderRetry.js";
8
8
  import { globalMessagePresentation } from "../interaction/notificationPresentation.js";
9
9
  import { recordGlobalRuntimeAttention, recordGlobalRuntimeRecovery } from "./globalRuntimeAttention.js";
10
10
  import { globalInboxRecovery } from "../message/globalInboxRecovery.js";
@@ -94,7 +94,11 @@ export async function deliverGlobalInputs(home, store, ensure, onError, options
94
94
  if (currentRole === null || !isEnabled(currentRole))
95
95
  continue;
96
96
  if (sessions?.providerBinding?.retry !== undefined && !providerRetryPending(sessions.providerBinding)) {
97
- store.transaction(tx => settleGlobalRetryInput(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, new Date()));
97
+ const now = options.now ?? new Date();
98
+ // Retained terminal retry history is not new write work each pass.
99
+ const settlement = store.readTransaction(tx => globalRetryInputSettlement(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, now));
100
+ if (settlement !== undefined)
101
+ store.transaction(tx => settleGlobalRetryInput(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, now));
98
102
  pending = store.queryGlobalRoleMessages(role.name, { pending: true, unclaimed: true, limit: 64 }).messages;
99
103
  if (pending.length === 0)
100
104
  continue;
@@ -102,15 +102,19 @@ export function runtimeDiagnosticEvidence(messages, key) {
102
102
  /** Global observations have no Task event history. Keep original failed
103
103
  * attempts as ordinary record-only Messages before replacing the live binding. */
104
104
  export function recordGlobalRuntimeFailure(store, roleName, identity, facts, now) {
105
+ return recordFailure(store, roleName, identity, facts, now, [...store.listGlobalRoleMessages(roleName)]);
106
+ }
107
+ function recordFailure(store, roleName, identity, facts, now, messages) {
105
108
  const diagnostic = diagnosticIdentity(store, roleName, identity, facts);
106
109
  const header = `${FAILURE_HEADER}${hash([identity, diagnostic.key])}\n`;
107
- const previous = store.listGlobalRoleMessages(roleName).find(message => message.kind === "system" && message.body.startsWith(header));
110
+ const previous = messages.find(message => message.kind === "system" && message.body.startsWith(header));
108
111
  if (previous !== undefined)
109
112
  return `${roleName}/${previous.id}`;
110
113
  const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), roleName, header + redactAgentErrorText(JSON.stringify({
111
114
  ...object(facts), diagnostic
112
115
  })), "system", { type: "system" }, now);
113
116
  store.saveGlobalRoleMessage(message);
117
+ messages.push(message);
114
118
  return `${roleName}/${message.id}`;
115
119
  }
116
120
  /** Reuse the Global Message inbox for an exact failure, not a recovery queue.
@@ -119,9 +123,13 @@ export function recordGlobalRuntimeAttention(store, roleName, identity, facts, n
119
123
  // A Home may have a custom Global Role without an Operator. Keep its own
120
124
  // diagnostic readable; never create a Role or launch a substitute Agent.
121
125
  const recipient = store.getGlobalRole("operator") === null ? roleName : "operator";
122
- const failureRef = recordGlobalRuntimeFailure(store, roleName, identity, facts, now);
126
+ // These observations run in the caller's transaction. Reuse that exact
127
+ // history plus our appended record, rather than loading/parsing the whole
128
+ // Role history twice on every unchanged Host failure.
129
+ const messages = [...store.listGlobalRoleMessages(roleName)];
130
+ const failureRef = recordFailure(store, roleName, identity, facts, now, messages);
123
131
  const diagnostic = diagnosticIdentity(store, roleName, identity, facts);
124
- const group = runtimeDiagnosticGroups(store.listGlobalRoleMessages(roleName))
132
+ const group = runtimeDiagnosticGroups(messages)
125
133
  .reverse().find(group => group.key === diagnostic.key && group.recoveredAt === undefined);
126
134
  const requestId = `runtime-attention:${hash([roleName, diagnostic.key, group?.episode ?? identity])}`;
127
135
  if (store.queryGlobalRoleMessages(recipient, { requestId, limit: 1 }).messages.length > 0)
@@ -277,7 +277,7 @@ export function assertNonSecretJobInput(params) {
277
277
  }
278
278
  }
279
279
  const input = JSON.stringify({
280
- env: params.env, steps: params.steps, requestId: params.requestId
280
+ env: params.env, steps: params.steps, requestId: params.requestId, files: params.files
281
281
  });
282
282
  // Reject recognizable key material regardless of the parameter name, and
283
283
  // URL userinfo before either hashing or persisting the specification.