@zq-silk/yui 2.17.0 → 2.19.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 (73) hide show
  1. package/dist/cli/commandCatalog.js +2 -1
  2. package/dist/commands/controllerCommands.js +14 -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 +45 -22
  10. package/dist/controller/diagnosticJournal.js +214 -0
  11. package/dist/controller/fileSchedulerStoreAdapter.js +143 -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/messageHealth.js +354 -0
  18. package/dist/controller/runtime.js +75 -5
  19. package/dist/controller/taskHostTerminalRecovery.js +11 -0
  20. package/dist/controller/workerSchedulerStore.js +24 -0
  21. package/dist/core/boundedRpc.js +4 -0
  22. package/dist/core/controllerServer.js +1 -1
  23. package/dist/core/controllerTelemetry.js +92 -19
  24. package/dist/job/globalShell.js +51 -0
  25. package/dist/job/jobBusinessResult.js +4 -3
  26. package/dist/job/jobDefinition.js +6 -1
  27. package/dist/job/jobRunner.js +9 -0
  28. package/dist/job/scheduleService.js +2 -0
  29. package/dist/message/globalInboxRecovery.js +30 -19
  30. package/dist/message/globalProviderRetry.js +7 -1
  31. package/dist/runtime/builtinAgentErrorMappers.js +4 -0
  32. package/dist/runtime/globalSessionHealth.js +39 -0
  33. package/dist/scheduler/roleRunStall.js +10 -10
  34. package/dist/storage/contextRecords.js +16 -0
  35. package/dist/storage/persistenceWorker.js +41 -19
  36. package/dist/storage/sqliteSchema.js +35 -11
  37. package/dist/storage/sqliteStore.js +102 -8
  38. package/dist/storage/storageVersions.js +1 -1
  39. package/dist/storage/storeRpc.js +25 -11
  40. package/dist/storage/taskReadRevisionSchema.js +39 -0
  41. package/dist/web/assets/client/app/catalog.js +0 -29
  42. package/dist/web/assets/client/app/jobs.js +115 -0
  43. package/dist/web/assets/client/app/main.js +17 -6
  44. package/dist/web/assets/client/app/manage.js +1 -0
  45. package/dist/web/assets/client/app/settings.js +1 -5
  46. package/dist/web/assets/client/i18n/en.js +203 -18
  47. package/dist/web/assets/client/i18n/zh-CN.js +202 -17
  48. package/dist/web/assets/client/index.js +2 -0
  49. package/dist/web/assets/client/layout/manageShell.js +61 -15
  50. package/dist/web/assets/client/layout/router.js +2 -1
  51. package/dist/web/assets/client/lib/prefs.js +1 -1
  52. package/dist/web/assets/client/views/jobs.js +1042 -160
  53. package/dist/web/assets/client/views/sidebar.js +3 -0
  54. package/dist/web/assets/shared/manageNav.js +3 -1
  55. package/dist/web/assets/shell/appRail.js +2 -1
  56. package/dist/web/assets/shell/manage.js +7 -2
  57. package/dist/web/assets/shell/models.js +4 -3
  58. package/dist/web/assets/shell/settingsContent.js +0 -3
  59. package/dist/web/assets/shell/sidebar.js +0 -1
  60. package/dist/web/assets/styles/views/jobs.js +301 -12
  61. package/dist/web/assets/styles/views/sidebar.js +1 -2
  62. package/dist/web/webServer.js +28 -1
  63. package/docs/global-shell-jobs.md +78 -0
  64. package/docs/job-runs.md +6 -3
  65. package/docs/managed-turn-and-session-runtime.md +170 -0
  66. package/docs/managed-turn-and-session-runtime.zh-CN.md +116 -0
  67. package/docs/release-workflow.md +63 -0
  68. package/docs/release-workflow.zh-CN.md +44 -0
  69. package/docs/storage-baseline.md +14 -1
  70. package/docs/storage-baseline.zh-CN.md +6 -1
  71. package/docs/testing/verification-levels.md +2 -2
  72. package/docs/testing/verification-levels.zh-CN.md +2 -2
  73. 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,9 @@ 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 { reconcileMessageHealth } from "./messageHealth.js";
39
+ import { reconcileProjectSync } from "../job/projectSync.js";
40
+ import { serializeError } from "../core/boundedRpc.js";
37
41
  import { claimOperatorAttention } from "../scheduler/operatorEvent.js";
38
42
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
39
43
  import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_HOST_DETACH_REQUIRED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeCleanupReason, runtimeCleanupDisposition, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
@@ -65,6 +69,26 @@ export class FileSchedulerStoreAdapter {
65
69
  telemetry;
66
70
  drivers;
67
71
  snapshotExecutionLaneWorkspace;
72
+ #reads = new ControllerWorkObserver();
73
+ #projectionCache = { hits: 0, rebuilds: 0, invalidations: 0 };
74
+ readObservationMetrics() {
75
+ return { ...this.#reads.snapshot(), projectionCache: { ...this.#projectionCache } };
76
+ }
77
+ compactReadObservationMetrics() {
78
+ return compactWorkObservation(this.#reads);
79
+ }
80
+ #observeRead(phase, target, read) {
81
+ const finish = this.#reads.start(phase, target);
82
+ try {
83
+ const result = read();
84
+ finish();
85
+ return result;
86
+ }
87
+ catch (error) {
88
+ finish(true);
89
+ throw error;
90
+ }
91
+ }
68
92
  /** Diagnostic telemetry is optional and never participates in runtime truth. */
69
93
  constructor(store, telemetry = null, drivers = builtinAgentDriverRegistry(), snapshotExecutionLaneWorkspace = snapshotExecutionLaneWorkspaceSync) {
70
94
  this.store = store;
@@ -72,6 +96,15 @@ export class FileSchedulerStoreAdapter {
72
96
  this.drivers = drivers;
73
97
  this.snapshotExecutionLaneWorkspace = snapshotExecutionLaneWorkspace;
74
98
  }
99
+ reconcileMessageHealth(now, hosts) {
100
+ return reconcileMessageHealth(this.store, now, hosts);
101
+ }
102
+ /** Registry maintenance only; this never executes a Job or starts a process. */
103
+ reconcileProjectSync(now) {
104
+ const errors = [];
105
+ reconcileProjectSync(this.store, now, error => errors.push(serializeError(error)));
106
+ return errors;
107
+ }
75
108
  observeAgentHostObservation(event, now = new Date()) {
76
109
  // Match canonical ingress: archive and Host fact settlement share the
77
110
  // same write boundary, including the original transport envelope.
@@ -168,7 +201,7 @@ export class FileSchedulerStoreAdapter {
168
201
  && sessions?.sessions[input.fence.agentId]?.nativeSessionId === input.fence.nativeSessionId;
169
202
  if (!runFolded && !(isSteer && input.fence.runId === undefined))
170
203
  return "obsolete";
171
- if (runFolded && !hasPersistedRuntimeObservation(store.listEvents(taskId), input) && run.status === "active") {
204
+ if (runFolded && !hasPersistedRuntimeObservation(store, input) && run.status === "active") {
172
205
  const updated = appendRunInput(run, createRunInput({
173
206
  source: input.fence.receiptId.startsWith("native-input:")
174
207
  ? { type: "provider", channel: "visible-input" }
@@ -196,7 +229,7 @@ export class FileSchedulerStoreAdapter {
196
229
  if ((input.kind === "input.rejected" || input.kind === "input.delivery-unknown")
197
230
  && input.fence.receiptId?.startsWith("steer:") === true) {
198
231
  return this.store.transaction((store) => {
199
- if (hasPersistedRuntimeObservation(store.listEvents(taskId), input))
232
+ if (hasPersistedRuntimeObservation(store, input))
200
233
  return "applied";
201
234
  const recorded = this.recordSteerControlOutcome(store, input.fence.receiptId, input.kind === "input.rejected" ? "rejected" : "delivery-unknown", now);
202
235
  if (!recorded) {
@@ -217,7 +250,7 @@ export class FileSchedulerStoreAdapter {
217
250
  // together, including a retry after a previously interrupted fold.
218
251
  if (!isRunTerminalObservation(input)
219
252
  && input.kind !== "turn.accepted" && input.kind !== "input.accepted"
220
- && hasPersistedRuntimeObservation(this.store.listEvents(taskId), input))
253
+ && hasPersistedRuntimeObservation(this.store, input))
221
254
  return "applied";
222
255
  let outcome;
223
256
  switch (input.kind) {
@@ -330,7 +363,7 @@ export class FileSchedulerStoreAdapter {
330
363
  recordCanonicalObservationObsolete(store, input, "steer-receipt-fence-mismatch", now);
331
364
  return "obsolete";
332
365
  }
333
- if (hasPersistedRuntimeObservation(store.listEvents(taskId), input))
366
+ if (hasPersistedRuntimeObservation(store, input))
334
367
  return "applied";
335
368
  const target = { kind: "role", taskId, roleName: "leader" };
336
369
  const mailbox = store.getWorkMailbox(target);
@@ -408,7 +441,7 @@ export class FileSchedulerStoreAdapter {
408
441
  }
409
442
  if (input.kind === "turn.accepted") {
410
443
  if (input.authority === "transport" && matches) {
411
- const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`);
444
+ const message = globalMessageForReceipt(store, fence.roleName, receipt);
412
445
  if (message !== undefined)
413
446
  store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at), "transport"));
414
447
  return "applied";
@@ -428,9 +461,8 @@ export class FileSchedulerStoreAdapter {
428
461
  }
429
462
  else if (binding.run?.status !== "accepted")
430
463
  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);
464
+ const message = globalMessageForReceipt(store, fence.roleName, receipt, "global-input", binding?.retry?.currentAttemptId === receipt && binding.retry.input.kind === "message"
465
+ ? binding.retry.input.messageId : undefined);
434
466
  if (message !== undefined)
435
467
  store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at)));
436
468
  }
@@ -443,9 +475,8 @@ export class FileSchedulerStoreAdapter {
443
475
  if (input.kind === "turn.completed" && binding.run != null
444
476
  && ["submitting", "delivery-unknown"].includes(binding.run.status)) {
445
477
  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);
478
+ const message = globalMessageForReceipt(store, fence.roleName, receipt, "global-input", binding?.retry?.currentAttemptId === receipt && binding.retry.input.kind === "message"
479
+ ? binding.retry.input.messageId : undefined);
449
480
  if (message !== undefined)
450
481
  store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at)));
451
482
  }
@@ -466,7 +497,7 @@ export class FileSchedulerStoreAdapter {
466
497
  if (input.authority !== "provider-structured" || binding.run?.nativeTurnId !== fence.nativeTurnId
467
498
  || binding.run?.status !== "accepted")
468
499
  return "obsolete";
469
- const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `steer:${fence.roleName}/${entry.id}`);
500
+ const message = globalMessageForReceipt(store, fence.roleName, receipt, "steer");
470
501
  if (message !== undefined && message.inputControl !== undefined) {
471
502
  const outcome = input.kind === "input.accepted" ? "accepted"
472
503
  : input.kind === "input.rejected" ? "rejected" : "delivery-unknown";
@@ -548,7 +579,7 @@ export class FileSchedulerStoreAdapter {
548
579
  }
549
580
  validateCanonicalRunObservation(input, now) {
550
581
  return this.store.transaction((store) => {
551
- if (hasPersistedRuntimeObservation(store.listEvents(input.fence.taskId), input)) {
582
+ if (hasPersistedRuntimeObservation(store, input)) {
552
583
  return "applied";
553
584
  }
554
585
  const run = input.fence.runId === undefined ? null : store.getRun(input.fence.taskId, input.fence.runId);
@@ -565,7 +596,7 @@ export class FileSchedulerStoreAdapter {
565
596
  ? "applied" : "obsolete";
566
597
  }
567
598
  const knownContinuation = (input.kind.startsWith("continuation.") || input.kind === "observer.health")
568
- && projectProviderContinuations(store.listEvents(input.fence.taskId)).some((entry) => (entry.runId === input.fence.runId
599
+ && projectProviderContinuations(store.listEventsByType(input.fence.taskId, [RUNTIME_OBSERVATION_TASK_EVENT])).some((entry) => (entry.runId === input.fence.runId
569
600
  && entry.identity.providerNamespace === input.fence.driverId
570
601
  && entry.identity.accountScope === input.fence.agentId
571
602
  && entry.identity.conversationId === input.fence.conversationId
@@ -647,7 +678,7 @@ export class FileSchedulerStoreAdapter {
647
678
  const status = "ended";
648
679
  let updatedSessions = updateRoleAgentSessionStatus(sessions, input.fence.agentId, status, now, input.kind === "session.failed" ? "failed" : "stopped");
649
680
  store.saveTaskRoleSessionSet(updatedSessions);
650
- for (const continuation of projectProviderContinuations(store.listEvents(input.fence.taskId))) {
681
+ for (const continuation of projectProviderContinuations(store.listEventsByType(input.fence.taskId, [RUNTIME_OBSERVATION_TASK_EVENT]))) {
651
682
  if (continuation.runId !== input.fence.runId || continuation.identity.conversationId
652
683
  !== (input.fence.conversationId ?? input.fence.nativeSessionId) || continuation.execution === "quiescent" || continuation.attachment === "detached")
653
684
  continue;
@@ -721,9 +752,9 @@ export class FileSchedulerStoreAdapter {
721
752
  persistRuntimeObservation(input, now, notifyGoal = true) {
722
753
  this.store.transaction((store) => {
723
754
  const taskId = input.fence.taskId;
724
- const events = store.listEvents(taskId);
725
- if (hasPersistedRuntimeObservation(events, input))
755
+ if (hasPersistedRuntimeObservation(store, input))
726
756
  return;
757
+ const events = store.listEventsByType(taskId, [RUNTIME_OBSERVATION_TASK_EVENT]);
727
758
  if (usageSnapshotIsSuperseded(events, input))
728
759
  return;
729
760
  const removable = compactedRuntimeObservationIds(events, input);
@@ -851,43 +882,53 @@ export class FileSchedulerStoreAdapter {
851
882
  return "obsolete";
852
883
  }
853
884
  /**
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.
885
+ * Task-scoped cache tokens cover the exact source tables across connections.
886
+ * Unrelated Tasks/Global traffic do not invalidate these projections. Retain
887
+ * only the 16 most recently read Tasks; eviction changes cost, never truth.
857
888
  */
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() };
866
- }
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);
889
+ #readProjection = new Map();
890
+ #projected(taskId, family, read) {
891
+ const observed = this.store.getTaskReadRevision(taskId);
892
+ const previous = this.#readProjection.get(taskId);
893
+ const hit = previous?.revision === observed ? previous.value[family] : undefined;
894
+ if (hit !== undefined) {
895
+ // The token read is the read's snapshot point. A cache hit needs no new
896
+ // transaction: its value was built under that exact committed token.
897
+ this.#projectionCache.hits++;
898
+ this.#readProjection.delete(taskId);
899
+ this.#readProjection.set(taskId, previous);
900
+ return hit;
884
901
  }
885
- return task;
902
+ // Pin and recheck misses before reading any source rows: an external writer
903
+ // may have advanced the token after the optimistic lookup above.
904
+ return this.store.readTransaction(() => {
905
+ const revision = this.store.getTaskReadRevision(taskId);
906
+ let task = this.#readProjection.get(taskId);
907
+ if (task === undefined || task.revision !== revision) {
908
+ if (task !== undefined)
909
+ this.#projectionCache.invalidations++;
910
+ this.#projectionCache.rebuilds++;
911
+ task = { revision, value: {} };
912
+ }
913
+ this.#readProjection.delete(taskId);
914
+ this.#readProjection.set(taskId, task);
915
+ if (this.#readProjection.size > 16) {
916
+ this.#readProjection.delete(this.#readProjection.keys().next().value);
917
+ }
918
+ const cached = task.value[family];
919
+ if (cached !== undefined) {
920
+ this.#projectionCache.hits++;
921
+ return cached;
922
+ }
923
+ const value = this.#observeRead(`task-${family}-read`, taskId, read);
924
+ task.value[family] = value;
925
+ return value;
926
+ });
886
927
  }
887
928
  withRuntimeEventTransaction(execute) {
888
929
  return this.store.withRuntimeEventTransaction(execute);
889
930
  }
890
- listTasks() { return this.store.listTasks(); }
931
+ listTasks() { return this.#observeRead("list-tasks", "home", () => this.store.listTasks()); }
891
932
  listActiveTaskIds() {
892
933
  return [...this.store.listActiveTaskIds()].sort((left, right) => (left.localeCompare(right, undefined, { numeric: true })));
893
934
  }
@@ -980,51 +1021,48 @@ export class FileSchedulerStoreAdapter {
980
1021
  return this.store.getTaskRoleSessionSet(taskId, roleName);
981
1022
  }
982
1023
  listEvents(taskId) {
983
- return this.#taskReadProjection(taskId).events;
1024
+ return this.#projected(taskId, "events", () => this.store.listEvents(taskId));
984
1025
  }
985
1026
  listRuns(taskId) {
986
- return this.#taskReadProjection(taskId).runs;
1027
+ return this.#projected(taskId, "runs", () => this.store.listRuns(taskId));
987
1028
  }
988
1029
  listWorkItems(taskId) {
989
- return this.#taskReadProjection(taskId).workItems;
1030
+ return this.#projected(taskId, "workItems", () => this.store.listWorkItems(taskId));
990
1031
  }
991
1032
  listReviewRounds(taskId) {
992
- return this.#taskReadProjection(taskId).reviewRounds;
1033
+ return this.#projected(taskId, "reviewRounds", () => this.store.listReviewRounds(taskId));
993
1034
  }
994
1035
  listIntegrationAttempts(taskId) {
995
- return this.#taskReadProjection(taskId).integrationAttempts;
1036
+ return this.#projected(taskId, "integrationAttempts", () => this.store.listIntegrationAttempts(taskId));
996
1037
  }
997
1038
  listDurableJobs(taskId) {
998
- return this.#taskReadProjection(taskId).durableJobs;
1039
+ return this.#projected(taskId, "durableJobs", () => this.store.listDurableJobs(taskId));
999
1040
  }
1000
1041
  listInputRequests(taskId) {
1001
- return this.#taskReadProjection(taskId).inputRequests;
1042
+ return this.#projected(taskId, "inputRequests", () => this.store.listInputRequests(taskId));
1002
1043
  }
1003
1044
  listMessages(taskId) {
1004
- return this.#taskReadProjection(taskId).messages;
1045
+ return this.#projected(taskId, "messages", () => this.store.listMessages(taskId));
1005
1046
  }
1006
1047
  getRunProgressFacts(taskId, runId) {
1007
- return this.#taskReadProjection(taskId).runFacts.get(runId);
1048
+ return this.#projected(taskId, "runFacts", () => foldRunProgressFacts(this.listEvents(taskId))).get(runId);
1008
1049
  }
1009
1050
  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) ?? {});
1051
+ return this.store.readTransaction(() => {
1052
+ // Pin related reads to one snapshot while loading only required families.
1053
+ const view = {
1054
+ getRun: (id, runId) => this.store.getRun(id, runId),
1055
+ listEvents: () => this.listEvents(taskId),
1056
+ getWorkItem: (id, workItemId) => this.store.getWorkItem(id, workItemId),
1057
+ listReviewRounds: () => this.listReviewRounds(taskId),
1058
+ listChangeSets: () => this.#projected(taskId, "changeSets", () => this.store.listChangeSets(taskId)),
1059
+ listIntegrationAttempts: () => this.listIntegrationAttempts(taskId),
1060
+ listInputRequests: () => this.listInputRequests(taskId)
1061
+ };
1062
+ // A missing fold entry is authoritative empty evidence, not permission
1063
+ // to fall back to a per-candidate full-history scan.
1064
+ return latestRunDurableProgressAt(view, taskId, roleName, runId, this.getRunProgressFacts(taskId, runId) ?? {});
1065
+ });
1028
1066
  }
1029
1067
  recordRoleRunDiagnostic(input) {
1030
1068
  return this.store.transaction((store) => {
@@ -1160,12 +1198,11 @@ export class FileSchedulerStoreAdapter {
1160
1198
  if (turn != null && ["submitting", "accepted", "delivery-unknown"].includes(turn.status)) {
1161
1199
  throw new AgentHostProviderSessionBusyError("Global Conversation has an unsettled input.");
1162
1200
  }
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);
1201
+ const message = globalMessageForReceipt(store, input.roleName, input.attemptId);
1202
+ const reserved = store.queryGlobalRoleMessages(input.roleName, {
1203
+ limit: 1, reservedThen: { nativeSessionId: input.nativeSessionId,
1204
+ agentId: input.agentId, authorityEpoch: input.authorityEpoch, holderId: input.holderId }
1205
+ }).messages[0];
1169
1206
  if (reserved !== undefined && reserved.id !== message?.id) {
1170
1207
  throw new AgentHostProviderSessionBusyError("The next Global input belongs to an explicit then handoff.");
1171
1208
  }
@@ -1285,7 +1322,7 @@ export class FileSchedulerStoreAdapter {
1285
1322
  // This shares the exact writer/session admission and its transaction.
1286
1323
  if ((input.authorityOwner === "human" || input.attemptId.startsWith(`human:${input.holderId}:`)) && input.roleName === "leader"
1287
1324
  && 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);
1325
+ const prior = store.listEventsByType(input.taskId, ["message.native-user-input"]).find(event => event.payload.attemptId === input.attemptId);
1289
1326
  if (prior !== undefined) {
1290
1327
  const message = store.listMessages(input.taskId).find(entry => entry.id === prior.payload.messageId);
1291
1328
  if (message?.body !== input.boundedText || prior.payload.nativeSessionId !== input.nativeSessionId) {
@@ -1344,7 +1381,7 @@ export class FileSchedulerStoreAdapter {
1344
1381
  unknown: "External effects are not established by submission settlement."
1345
1382
  }, input.now);
1346
1383
  }
1347
- const message = store.listGlobalRoleMessages(input.roleName).find(entry => input.attemptId === `global-input:${input.roleName}/${entry.id}`);
1384
+ const message = globalMessageForReceipt(store, input.roleName, input.attemptId);
1348
1385
  if (message !== undefined && message.delivery?.via !== "provider") {
1349
1386
  const outcome = input.status === "delivery-unknown" ? "delivery-unknown" : "rejected";
1350
1387
  const updated = { ...message, control: {
@@ -1890,9 +1927,9 @@ export class FileSchedulerStoreAdapter {
1890
1927
  return null;
1891
1928
  const wakeId = processing.owner.slice("leader-notification:".length);
1892
1929
  const attemptId = processing.batchId;
1893
- const previous = store.listEvents(taskId).filter((event) => event.type === "notification.delivery" && event.payload.attemptId === attemptId).at(-1);
1930
+ const previous = store.listEventsByType(taskId, ["notification.delivery"]).filter((event) => event.payload.attemptId === attemptId).at(-1);
1894
1931
  const native = provider?.attemptId === attemptId ? provider : undefined;
1895
- const accepted = store.listEvents(taskId).some((event) => {
1932
+ const accepted = store.listEventsByType(taskId, [RUNTIME_OBSERVATION_TASK_EVENT]).some((event) => {
1896
1933
  const observation = runtimeObservationFromTaskEvent(event);
1897
1934
  return observation?.fence.roleName === "leader"
1898
1935
  && observation.fence.receiptId === attemptId
@@ -2016,8 +2053,7 @@ export class FileSchedulerStoreAdapter {
2016
2053
  if (mailbox == null || processing == null || processing.batchId !== attemptId
2017
2054
  || !processing.owner.startsWith("leader-notification:"))
2018
2055
  return;
2019
- if (!store.listEvents(taskId).some((event) => event.type === "notification.delivery"
2020
- && event.payload.attemptId === attemptId && event.payload.outcome === outcome)) {
2056
+ if (!store.listEventsByType(taskId, ["notification.delivery"]).some((event) => event.payload.attemptId === attemptId && event.payload.outcome === outcome)) {
2021
2057
  // Native ingress may already have persisted the same rejection. Reuse
2022
2058
  // that original failure rather than reporting it twice from the caller.
2023
2059
  const existing = store.listEventsByType(taskId, ["runtime.agent-error"])
@@ -2083,7 +2119,7 @@ export class FileSchedulerStoreAdapter {
2083
2119
  nativeSessionId: input.session.nativeSessionId
2084
2120
  }, "active", input.now);
2085
2121
  }
2086
- if (!defersConversationReplacement && !store.listEvents(input.task.id).some((event) => event.type === "run.session-prepared" && event.payload.runId === input.run.id
2122
+ if (!defersConversationReplacement && !store.listEventsByType(input.task.id, ["run.session-prepared"]).some((event) => event.payload.runId === input.run.id
2087
2123
  && event.payload.nativeSessionId === input.session.nativeSessionId)) {
2088
2124
  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
2125
  nativeSessionId: input.session.nativeSessionId }, input.now));
@@ -2235,7 +2271,7 @@ export class FileSchedulerStoreAdapter {
2235
2271
  && preparedRun.executionLaneId !== undefined
2236
2272
  && preparedRun.workspace !== undefined
2237
2273
  && input.outcome.status === "completed"
2238
- ? this.snapshotExecutionLaneWorkspace(this.store, preparedRun.workspace)
2274
+ ? this.#observeRead("git-workspace-snapshot", preparedRun.taskId, () => this.snapshotExecutionLaneWorkspace(this.store, preparedRun.workspace))
2239
2275
  : undefined;
2240
2276
  return this.store.transaction((store) => {
2241
2277
  const task = store.getTask(input.taskId);
@@ -2476,7 +2512,7 @@ export class FileSchedulerStoreAdapter {
2476
2512
  observeProviderGoal(input, now) {
2477
2513
  return this.store.transaction((store) => {
2478
2514
  const taskId = input.fence.taskId;
2479
- if (hasPersistedRuntimeObservation(store.listEvents(taskId), input))
2515
+ if (hasPersistedRuntimeObservation(store, input))
2480
2516
  return "applied";
2481
2517
  const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
2482
2518
  const binding = sessions?.providerBinding;
@@ -2798,7 +2834,7 @@ export class FileSchedulerStoreAdapter {
2798
2834
  ...(boundNativeSessionId === undefined ? {} : { nativeSessionId: boundNativeSessionId })
2799
2835
  };
2800
2836
  const driverId = this.drivers.requireByAdapterId(run.effective.adapterId).id;
2801
- const lifecycleEvents = store.listEvents(fence.taskId).flatMap((event) => {
2837
+ const lifecycleEvents = store.listEventsByType(fence.taskId, [RUNTIME_OBSERVATION_TASK_EVENT]).flatMap((event) => {
2802
2838
  const observation = runtimeObservationFromTaskEvent(event);
2803
2839
  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
2840
  ? [observation]
@@ -2966,7 +3002,7 @@ function runtimeReceiptBelongsToRun(store, input) {
2966
3002
  && sessions?.sessions[input.fence.agentId]?.nativeSessionId === input.fence.nativeSessionId) {
2967
3003
  return true;
2968
3004
  }
2969
- return store.listEvents(taskId).some((event) => {
3005
+ return store.listEventsByType(taskId, [RUNTIME_OBSERVATION_TASK_EVENT]).some((event) => {
2970
3006
  const accepted = runtimeObservationFromTaskEvent(event);
2971
3007
  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
3008
  });
@@ -2988,8 +3024,7 @@ function recordCanonicalObservationObsolete(store, input, reason, now) {
2988
3024
  }, reason, now);
2989
3025
  }
2990
3026
  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))))
3027
+ if (store.listEventsByType(input.taskId, ["runtime.event-obsolete"]).some((event) => ((event.payload.dedupeKey ?? event.payload.eventId) === (input.dedupeKey ?? input.eventId))))
2993
3028
  return;
2994
3029
  store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.event-obsolete", {
2995
3030
  eventId: input.eventId,
@@ -3513,7 +3548,7 @@ function resolveTerminalExecution(store, input) {
3513
3548
  const session = sessions?.sessions[input.agentId];
3514
3549
  const binding = sessions?.providerBinding;
3515
3550
  const pending = binding?.run;
3516
- const evidence = store.listEvents(input.taskId).map(runtimeObservationFromTaskEvent)
3551
+ const evidence = store.listEventsByType(input.taskId, [RUNTIME_OBSERVATION_TASK_EVENT]).map(runtimeObservationFromTaskEvent)
3517
3552
  .filter((event) => event !== null
3518
3553
  && (event.kind === "turn.accepted" || isRunTerminalObservation(event)));
3519
3554
  if (input.attemptId !== undefined && evidence.some((event) => (event.fence.roleName === input.roleName
@@ -3574,7 +3609,7 @@ function isRunTerminalObservation(input) {
3574
3609
  return ["turn.completed", "turn.failed", "turn.cancelled"].includes(input.kind);
3575
3610
  }
3576
3611
  function assertConsistentTerminal(store, input, run) {
3577
- const previous = store.listEvents(input.taskId).map(runtimeObservationFromTaskEvent)
3612
+ const previous = store.listEventsByType(input.taskId, [RUNTIME_OBSERVATION_TASK_EVENT]).map(runtimeObservationFromTaskEvent)
3578
3613
  .filter((event) => event !== null
3579
3614
  && isRunTerminalObservation(event)
3580
3615
  && event.fence.roleName === input.roleName
@@ -3700,14 +3735,21 @@ function usageSnapshotIsSuperseded(events, incoming) {
3700
3735
  && runtimeObservationFenceMatches(observation.fence, incoming.fence)
3701
3736
  && observationIsStrictlyNewer(observation, incoming));
3702
3737
  }
3703
- function hasPersistedRuntimeObservation(events, incoming) {
3738
+ function hasPersistedRuntimeObservation(store, incoming) {
3704
3739
  const usageIdentity = isRuntimeUsageEvidence(incoming)
3705
3740
  ? incoming.eventId
3706
3741
  : 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)));
3742
+ return store.hasRuntimeObservation(incoming.fence.taskId, usageIdentity === undefined
3743
+ ? { semanticKey: incoming.semanticKey } : { eventId: usageIdentity });
3744
+ }
3745
+ function globalMessageForReceipt(store, roleName, receipt, kind = "global-input", retryMessageId) {
3746
+ const prefix = `${kind}:${roleName}/`;
3747
+ const id = receipt?.startsWith(prefix) ? receipt.slice(prefix.length) : undefined;
3748
+ // A retry references the original Message, not a second input. Preserve the
3749
+ // old numeric-id ordering if both explicit identities name different rows.
3750
+ return [...new Set([id, retryMessageId].filter((value) => value !== undefined))]
3751
+ .flatMap(id => store.queryGlobalRoleMessages(roleName, { id, limit: 1 }).messages)
3752
+ .sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }))[0];
3711
3753
  }
3712
3754
  function observationIsStrictlyNewer(left, right) {
3713
3755
  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.