@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.
- package/dist/cli/commandCatalog.js +2 -1
- package/dist/commands/controllerCommands.js +13 -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 +35 -22
- package/dist/controller/diagnosticJournal.js +214 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +139 -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/runtime.js +66 -4
- 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/persistenceWorker.js +41 -19
- package/dist/storage/sqliteSchema.js +35 -11
- package/dist/storage/sqliteStore.js +86 -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 +74 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +51 -0
- package/docs/release-workflow.md +43 -0
- package/docs/release-workflow.zh-CN.md +29 -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,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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
432
|
-
|
|
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
|
|
447
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
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 =
|
|
859
|
-
#
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
if (
|
|
864
|
-
|
|
865
|
-
|
|
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
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const
|
|
871
|
-
task =
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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.#
|
|
1020
|
+
return this.#projected(taskId, "events", () => this.store.listEvents(taskId));
|
|
984
1021
|
}
|
|
985
1022
|
listRuns(taskId) {
|
|
986
|
-
return this.#
|
|
1023
|
+
return this.#projected(taskId, "runs", () => this.store.listRuns(taskId));
|
|
987
1024
|
}
|
|
988
1025
|
listWorkItems(taskId) {
|
|
989
|
-
return this.#
|
|
1026
|
+
return this.#projected(taskId, "workItems", () => this.store.listWorkItems(taskId));
|
|
990
1027
|
}
|
|
991
1028
|
listReviewRounds(taskId) {
|
|
992
|
-
return this.#
|
|
1029
|
+
return this.#projected(taskId, "reviewRounds", () => this.store.listReviewRounds(taskId));
|
|
993
1030
|
}
|
|
994
1031
|
listIntegrationAttempts(taskId) {
|
|
995
|
-
return this.#
|
|
1032
|
+
return this.#projected(taskId, "integrationAttempts", () => this.store.listIntegrationAttempts(taskId));
|
|
996
1033
|
}
|
|
997
1034
|
listDurableJobs(taskId) {
|
|
998
|
-
return this.#
|
|
1035
|
+
return this.#projected(taskId, "durableJobs", () => this.store.listDurableJobs(taskId));
|
|
999
1036
|
}
|
|
1000
1037
|
listInputRequests(taskId) {
|
|
1001
|
-
return this.#
|
|
1038
|
+
return this.#projected(taskId, "inputRequests", () => this.store.listInputRequests(taskId));
|
|
1002
1039
|
}
|
|
1003
1040
|
listMessages(taskId) {
|
|
1004
|
-
return this.#
|
|
1041
|
+
return this.#projected(taskId, "messages", () => this.store.listMessages(taskId));
|
|
1005
1042
|
}
|
|
1006
1043
|
getRunProgressFacts(taskId, runId) {
|
|
1007
|
-
return this.#
|
|
1044
|
+
return this.#projected(taskId, "runFacts", () => foldRunProgressFacts(this.listEvents(taskId))).get(runId);
|
|
1008
1045
|
}
|
|
1009
1046
|
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) ?? {});
|
|
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
|
|
1164
|
-
const reserved = store.
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
3734
|
+
function hasPersistedRuntimeObservation(store, incoming) {
|
|
3704
3735
|
const usageIdentity = isRuntimeUsageEvidence(incoming)
|
|
3705
3736
|
? incoming.eventId
|
|
3706
3737
|
: undefined;
|
|
3707
|
-
return
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
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
|
-
|
|
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.
|