@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.
- package/dist/cli/commandCatalog.js +2 -1
- package/dist/commands/controllerCommands.js +14 -4
- package/dist/commands/durableJobCommands.js +50 -3
- package/dist/commands/globalRoleCommands.js +18 -12
- package/dist/commands/inboxCommands.js +1 -0
- package/dist/commands/operatorCommands.js +10 -2
- package/dist/controlPlaneCli.js +21 -4
- package/dist/controller/clientRuntime.js +34 -13
- package/dist/controller/controller.js +45 -22
- package/dist/controller/diagnosticJournal.js +214 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +143 -101
- package/dist/controller/globalInputDelivery.js +6 -2
- package/dist/controller/globalRuntimeAttention.js +11 -3
- package/dist/controller/jobControl.js +1 -1
- package/dist/controller/jobDefinitionControl.js +24 -13
- package/dist/controller/jobSupervisor.js +4 -0
- package/dist/controller/messageHealth.js +354 -0
- package/dist/controller/runtime.js +75 -5
- package/dist/controller/taskHostTerminalRecovery.js +11 -0
- package/dist/controller/workerSchedulerStore.js +24 -0
- package/dist/core/boundedRpc.js +4 -0
- package/dist/core/controllerServer.js +1 -1
- package/dist/core/controllerTelemetry.js +92 -19
- package/dist/job/globalShell.js +51 -0
- package/dist/job/jobBusinessResult.js +4 -3
- package/dist/job/jobDefinition.js +6 -1
- package/dist/job/jobRunner.js +9 -0
- package/dist/job/scheduleService.js +2 -0
- package/dist/message/globalInboxRecovery.js +30 -19
- package/dist/message/globalProviderRetry.js +7 -1
- package/dist/runtime/builtinAgentErrorMappers.js +4 -0
- package/dist/runtime/globalSessionHealth.js +39 -0
- package/dist/scheduler/roleRunStall.js +10 -10
- package/dist/storage/contextRecords.js +16 -0
- package/dist/storage/persistenceWorker.js +41 -19
- package/dist/storage/sqliteSchema.js +35 -11
- package/dist/storage/sqliteStore.js +102 -8
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +25 -11
- package/dist/storage/taskReadRevisionSchema.js +39 -0
- package/dist/web/assets/client/app/catalog.js +0 -29
- package/dist/web/assets/client/app/jobs.js +115 -0
- package/dist/web/assets/client/app/main.js +17 -6
- package/dist/web/assets/client/app/manage.js +1 -0
- package/dist/web/assets/client/app/settings.js +1 -5
- package/dist/web/assets/client/i18n/en.js +203 -18
- package/dist/web/assets/client/i18n/zh-CN.js +202 -17
- package/dist/web/assets/client/index.js +2 -0
- package/dist/web/assets/client/layout/manageShell.js +61 -15
- package/dist/web/assets/client/layout/router.js +2 -1
- package/dist/web/assets/client/lib/prefs.js +1 -1
- package/dist/web/assets/client/views/jobs.js +1042 -160
- package/dist/web/assets/client/views/sidebar.js +3 -0
- package/dist/web/assets/shared/manageNav.js +3 -1
- package/dist/web/assets/shell/appRail.js +2 -1
- package/dist/web/assets/shell/manage.js +7 -2
- package/dist/web/assets/shell/models.js +4 -3
- package/dist/web/assets/shell/settingsContent.js +0 -3
- package/dist/web/assets/shell/sidebar.js +0 -1
- package/dist/web/assets/styles/views/jobs.js +301 -12
- package/dist/web/assets/styles/views/sidebar.js +1 -2
- package/dist/web/webServer.js +28 -1
- package/docs/global-shell-jobs.md +78 -0
- package/docs/job-runs.md +6 -3
- package/docs/managed-turn-and-session-runtime.md +170 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +116 -0
- package/docs/release-workflow.md +63 -0
- package/docs/release-workflow.zh-CN.md +44 -0
- package/docs/storage-baseline.md +14 -1
- package/docs/storage-baseline.zh-CN.md +6 -1
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +2 -2
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
432
|
-
|
|
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
|
|
447
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
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 =
|
|
859
|
-
#
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
if (
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
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.#
|
|
1024
|
+
return this.#projected(taskId, "events", () => this.store.listEvents(taskId));
|
|
984
1025
|
}
|
|
985
1026
|
listRuns(taskId) {
|
|
986
|
-
return this.#
|
|
1027
|
+
return this.#projected(taskId, "runs", () => this.store.listRuns(taskId));
|
|
987
1028
|
}
|
|
988
1029
|
listWorkItems(taskId) {
|
|
989
|
-
return this.#
|
|
1030
|
+
return this.#projected(taskId, "workItems", () => this.store.listWorkItems(taskId));
|
|
990
1031
|
}
|
|
991
1032
|
listReviewRounds(taskId) {
|
|
992
|
-
return this.#
|
|
1033
|
+
return this.#projected(taskId, "reviewRounds", () => this.store.listReviewRounds(taskId));
|
|
993
1034
|
}
|
|
994
1035
|
listIntegrationAttempts(taskId) {
|
|
995
|
-
return this.#
|
|
1036
|
+
return this.#projected(taskId, "integrationAttempts", () => this.store.listIntegrationAttempts(taskId));
|
|
996
1037
|
}
|
|
997
1038
|
listDurableJobs(taskId) {
|
|
998
|
-
return this.#
|
|
1039
|
+
return this.#projected(taskId, "durableJobs", () => this.store.listDurableJobs(taskId));
|
|
999
1040
|
}
|
|
1000
1041
|
listInputRequests(taskId) {
|
|
1001
|
-
return this.#
|
|
1042
|
+
return this.#projected(taskId, "inputRequests", () => this.store.listInputRequests(taskId));
|
|
1002
1043
|
}
|
|
1003
1044
|
listMessages(taskId) {
|
|
1004
|
-
return this.#
|
|
1045
|
+
return this.#projected(taskId, "messages", () => this.store.listMessages(taskId));
|
|
1005
1046
|
}
|
|
1006
1047
|
getRunProgressFacts(taskId, runId) {
|
|
1007
|
-
return this.#
|
|
1048
|
+
return this.#projected(taskId, "runFacts", () => foldRunProgressFacts(this.listEvents(taskId))).get(runId);
|
|
1008
1049
|
}
|
|
1009
1050
|
getRunDurableProgress(taskId, roleName, runId) {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
|
1164
|
-
const reserved = store.
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
3738
|
+
function hasPersistedRuntimeObservation(store, incoming) {
|
|
3704
3739
|
const usageIdentity = isRuntimeUsageEvidence(incoming)
|
|
3705
3740
|
? incoming.eventId
|
|
3706
3741
|
: undefined;
|
|
3707
|
-
return
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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.
|