@zq-silk/yui 2.8.1 → 2.10.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/README.md +5 -3
- package/dist/cli/commandCatalog.js +56 -21
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionPolicy.js +2 -2
- package/dist/commands/durableJobCommands.js +50 -0
- package/dist/commands/globalRoleCommands.js +30 -9
- package/dist/commands/inboxCommands.js +79 -22
- package/dist/commands/projectCommands.js +4 -33
- package/dist/commands/scheduleCommands.js +45 -0
- package/dist/commands/taskActivationCommands.js +19 -5
- package/dist/commands/taskCommands.js +8 -2
- package/dist/commands/taskFactCommands.js +24 -12
- package/dist/commands/taskProjectDeliveryCommand.js +56 -0
- package/dist/commands/taskPublicationCommands.js +6 -3
- package/dist/commands/taskPublicationVerifyCommand.js +74 -28
- package/dist/commands/taskRemoteDeliveryCommand.js +5 -3
- package/dist/context/runContextPack.js +25 -4
- package/dist/context/runInputContract.js +1 -0
- package/dist/context/taskContext.js +53 -16
- package/dist/context/taskPhase.js +99 -0
- package/dist/controlPlaneCli.js +38 -17
- package/dist/controller/controller.js +35 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +75 -16
- package/dist/controller/globalInputDelivery.js +17 -7
- package/dist/controller/inboxTurnCheck.js +64 -0
- package/dist/controller/jobControl.js +42 -12
- package/dist/controller/jobDefinitionControl.js +304 -0
- package/dist/controller/jobSupervisor.js +47 -32
- package/dist/controller/providerRetryAdmission.js +2 -0
- package/dist/controller/runtime.js +11 -2
- package/dist/controller/scheduleControl.js +176 -0
- package/dist/coordination/workMailbox.js +3 -3
- package/dist/external/githubPublicationVerifier.js +14 -5
- package/dist/external/gitlabPublicationVerifier.js +12 -29
- package/dist/grant/taskAuthorization.js +7 -2
- package/dist/interaction/notificationPresentation.js +12 -3
- package/dist/interaction/operatorPresentation.js +5 -0
- package/dist/job/builtinJob.js +58 -0
- package/dist/job/durableJob.js +72 -49
- package/dist/job/jobDefinition.js +68 -0
- package/dist/job/jobRunner.js +2 -1
- package/dist/job/projectRefreshRunner.js +48 -0
- package/dist/message/globalInboxRecovery.js +44 -0
- package/dist/message/globalInterrupt.js +1 -1
- package/dist/message/inboxReceipt.js +14 -6
- package/dist/message/inboxTurn.js +86 -0
- package/dist/message/message.js +11 -0
- package/dist/message/taskInboxOwnership.js +20 -6
- package/dist/repository/projectRefresh.js +58 -0
- package/dist/repository/taskWorkspacePreparer.js +2 -1
- package/dist/repository/workspaceCleanupInspection.js +6 -1
- package/dist/schedule/schedule.js +108 -0
- package/dist/scheduler/operatorEvent.js +2 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +8 -0
- package/dist/storage/contextRecords.js +8 -0
- package/dist/storage/jobSchema.js +15 -0
- package/dist/storage/maintenanceCaller.js +1 -1
- package/dist/storage/recordValidation.js +5 -0
- package/dist/storage/scheduleSchema.js +12 -0
- package/dist/storage/sqliteSchema.js +95 -24
- package/dist/storage/sqliteStore.js +123 -10
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +5 -0
- package/dist/storage/upgrade/jobDefinitionMigration.js +22 -0
- package/dist/task/archiveDiagnostics.js +1 -1
- package/dist/task/archivePreflight.js +1 -1
- package/dist/task/projectDeliveryScope.js +20 -0
- package/dist/task/publicationToolObservation.js +126 -0
- package/dist/task/remoteDelivery.js +19 -8
- package/dist/task/remoteDeliveryService.js +5 -4
- package/dist/task/taskActivationService.js +35 -0
- package/dist/web/assets/client/app/settings.js +7 -1
- package/dist/web/assets/client/i18n/en.js +65 -2
- package/dist/web/assets/client/i18n/zh-CN.js +65 -2
- package/dist/web/assets/client/index.js +2 -0
- package/dist/web/assets/client/views/jobs.js +222 -0
- package/dist/web/assets/client/views/task/deliveryResults.js +6 -2
- package/dist/web/assets/client/views/task/page.js +11 -1
- package/dist/web/assets/shared/manageNav.js +1 -0
- package/dist/web/assets/shared/taskTabs.js +2 -2
- package/dist/web/assets/shell/settingsContent.js +3 -0
- package/dist/web/assets/styles/index.js +2 -0
- package/dist/web/assets/styles/views/jobs.js +19 -0
- package/dist/web/controllerWeb.js +73 -21
- package/dist/web/taskPreviews.js +21 -13
- package/dist/web/webAccess.js +109 -0
- package/dist/web/webJobs.js +52 -0
- package/dist/web/webServer.js +76 -44
- package/docs/architecture/capabilities-and-resources.md +6 -4
- package/docs/architecture/capabilities-and-resources.zh-CN.md +5 -4
- package/docs/cli-information-contract.md +57 -4
- package/docs/cli-information-contract.zh-CN.md +41 -2
- package/docs/job-runs.md +224 -0
- package/docs/project-delivery-scope.md +59 -0
- package/docs/release-workflow.md +86 -0
- package/docs/release-workflow.zh-CN.md +68 -0
- package/docs/schedules.md +132 -0
- package/docs/storage-baseline.md +31 -1
- package/docs/storage-baseline.zh-CN.md +25 -1
- package/docs/task-delivery.md +74 -2
- package/docs/task-delivery.zh-CN.md +39 -1
- package/docs/testing/verification-levels.md +6 -2
- package/docs/testing/verification-levels.zh-CN.md +6 -3
- package/i18n/README.zh-CN.md +6 -3
- package/package.json +4 -3
- package/skills/yui-leader/SKILL.md +44 -1
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-leader/references/planning.md +9 -1
- package/skills/yui-operator/SKILL.md +31 -0
- package/skills/yui-operator/VERSION +1 -1
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-reviewer/VERSION +1 -1
- package/skills/yui-runtime/SKILL.md +67 -8
- package/skills/yui-runtime/VERSION +1 -1
- package/skills/yui-runtime/references/context-ordering.md +103 -0
- package/skills/yui-runtime/references/publication.md +44 -4
- package/skills/yui-runtime/references/recovery.md +21 -0
- package/skills/yui-worker/SKILL.md +5 -0
- package/skills/yui-worker/VERSION +1 -1
|
@@ -689,6 +689,7 @@ export class FileTaskController {
|
|
|
689
689
|
#onMaintenanceFenceDefer;
|
|
690
690
|
#leaderWakeFence;
|
|
691
691
|
#jobSupervisor;
|
|
692
|
+
#schedules;
|
|
692
693
|
#globalInputDelivery;
|
|
693
694
|
#providerRetry;
|
|
694
695
|
#continuationReconciler;
|
|
@@ -741,6 +742,7 @@ export class FileTaskController {
|
|
|
741
742
|
this.#onMaintenanceFenceDefer = options.onMaintenanceFenceDefer;
|
|
742
743
|
this.#leaderWakeFence = options.leaderWakeFence;
|
|
743
744
|
this.#jobSupervisor = options.jobSupervisor;
|
|
745
|
+
this.#schedules = options.schedules;
|
|
744
746
|
this.#continuationReconciler = options.continuationReconciler;
|
|
745
747
|
this.#hostTerminalRecovery = options.hostTerminalRecovery;
|
|
746
748
|
this.#globalInputDelivery = options.globalInputDelivery;
|
|
@@ -1014,6 +1016,7 @@ export class FileTaskController {
|
|
|
1014
1016
|
// DurableJob reconciliation runs before the scheduler pass so a
|
|
1015
1017
|
// terminal job's Leader wakeup is enqueued in the same pass that
|
|
1016
1018
|
// processes Leader wakeups.
|
|
1019
|
+
this.#schedules?.reconcile(this.#now());
|
|
1017
1020
|
this.#jobSupervisor?.reconcile(this.#now());
|
|
1018
1021
|
if (scope.kind === "full") {
|
|
1019
1022
|
result = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, scope, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, runtimeFailedTaskIds, this.#diagnosticAfterMs, this.#leaderWakeFence, this.#onError);
|
|
@@ -1631,6 +1634,38 @@ export async function startFileTaskController(home, store, delivery, dispatcher,
|
|
|
1631
1634
|
lifecycleRequests.delete(request);
|
|
1632
1635
|
}
|
|
1633
1636
|
}
|
|
1637
|
+
if (method === "schedule.control") {
|
|
1638
|
+
if (options.schedules === undefined)
|
|
1639
|
+
throw controllerApplicationError("METHOD_NOT_FOUND", "Schedules unavailable.");
|
|
1640
|
+
try {
|
|
1641
|
+
return jobControlJson(options.schedules.control(params, options.now?.() ?? new Date()));
|
|
1642
|
+
}
|
|
1643
|
+
catch (error) {
|
|
1644
|
+
if (error instanceof Error && error.name === "Error")
|
|
1645
|
+
error.name = "CoreJobError";
|
|
1646
|
+
throw error;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
if (method === "job.definition") {
|
|
1650
|
+
if (options.jobDefinitions === undefined)
|
|
1651
|
+
throw controllerApplicationError("METHOD_NOT_FOUND", "Job definitions unavailable.");
|
|
1652
|
+
let result;
|
|
1653
|
+
try {
|
|
1654
|
+
result = options.jobDefinitions(params, new Date());
|
|
1655
|
+
}
|
|
1656
|
+
catch (error) {
|
|
1657
|
+
if (error instanceof Error && error.name === "Error")
|
|
1658
|
+
error.name = "CoreJobError";
|
|
1659
|
+
throw error;
|
|
1660
|
+
}
|
|
1661
|
+
// The service has validated this scope. Only execution mutations need
|
|
1662
|
+
// reconciliation; definition reads/edits do not start background work.
|
|
1663
|
+
const input = params;
|
|
1664
|
+
if (["run", "run-cancel", "run-acknowledge"].includes(input.op)) {
|
|
1665
|
+
runtime.signal(input.scope.kind === "task" ? `task:${input.scope.taskId}` : "operator");
|
|
1666
|
+
}
|
|
1667
|
+
return jobControlJson(result);
|
|
1668
|
+
}
|
|
1634
1669
|
if (method === "job.start" || method === "job.get" || method === "job.cancel" || method === "job.acknowledge") {
|
|
1635
1670
|
const control = options.jobControl;
|
|
1636
1671
|
if (control === undefined) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
import {
|
|
3
|
+
import { nonReplayableMessageWake, pinMessageRevisions, taskMessageInboxReceipt, unfinishedLeaderInbox } from "../message/taskInboxOwnership.js";
|
|
4
4
|
import { contextSnapshotRef } from "../context/contextSnapshot.js";
|
|
5
5
|
import { freezeRunContextSnapshot } from "../context/runContextPack.js";
|
|
6
6
|
import { taskNotificationPresentation } from "../interaction/notificationPresentation.js";
|
|
@@ -30,6 +30,7 @@ import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
|
|
|
30
30
|
import { standardAgentError } from "../runtime/agentError.js";
|
|
31
31
|
import { recordTaskAgentError } from "./taskAgentError.js";
|
|
32
32
|
import { recordGlobalRuntimeAttention, recordGlobalRuntimeFailure, recordGlobalRuntimeRecovery } from "./globalRuntimeAttention.js";
|
|
33
|
+
import { checkInboxTurn } from "./inboxTurnCheck.js";
|
|
33
34
|
import { queueOperatorNotification } from "./operatorNotification.js";
|
|
34
35
|
import { claimOperatorAttention } from "../scheduler/operatorEvent.js";
|
|
35
36
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
@@ -466,7 +467,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
466
467
|
const outcome = input.kind === "input.accepted" ? "accepted"
|
|
467
468
|
: input.kind === "input.rejected" ? "rejected" : "delivery-unknown";
|
|
468
469
|
if (message.control?.outcome !== "accepted" && message.control?.outcome !== "rejected") {
|
|
469
|
-
const updated = { ...message, control: { requestId: message.inputControl.requestId,
|
|
470
|
+
const updated = { ...message, control: { ...message.control, requestId: message.inputControl.requestId,
|
|
470
471
|
receiptId: receipt, outcome, observedAt: at } };
|
|
471
472
|
store.updateGlobalRoleMessage(outcome === "accepted"
|
|
472
473
|
? markGlobalRoleMessageDelivered(updated, new Date(at)) : updated);
|
|
@@ -490,6 +491,10 @@ export class FileSchedulerStoreAdapter {
|
|
|
490
491
|
}
|
|
491
492
|
store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, binding, now));
|
|
492
493
|
settleGlobalRetryInput(store, fence.roleName, binding, now);
|
|
494
|
+
if (["turn.completed", "turn.failed", "turn.cancelled"].includes(input.kind)) {
|
|
495
|
+
checkInboxTurn(store, { scope: "global", roleName: fence.roleName, agentId: fence.agentId,
|
|
496
|
+
nativeSessionId: fence.nativeSessionId }, binding.run, now);
|
|
497
|
+
}
|
|
493
498
|
if (input.kind === "turn.failed" && matches && input.payload.failure !== undefined
|
|
494
499
|
&& !providerRetryPending(binding)) {
|
|
495
500
|
recordGlobalRuntimeAttention(store, fence.roleName, input.eventId, {
|
|
@@ -1157,8 +1162,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1157
1162
|
throw new AgentHostProviderSessionBusyError("The next Global input belongs to an explicit then handoff.");
|
|
1158
1163
|
}
|
|
1159
1164
|
if (message !== undefined) {
|
|
1160
|
-
if (
|
|
1161
|
-
|| message.inbox !== undefined || message.delivery !== undefined || message.notDelivered !== undefined
|
|
1165
|
+
if (message.inbox !== undefined || message.delivery !== undefined || message.notDelivered !== undefined
|
|
1162
1166
|
|| message.control !== undefined && message.control.outcome !== "rejected"
|
|
1163
1167
|
|| message.deliveryTarget?.agentId !== input.agentId
|
|
1164
1168
|
|| message.deliveryTarget?.nativeSessionId !== input.nativeSessionId) {
|
|
@@ -1535,8 +1539,9 @@ export class FileSchedulerStoreAdapter {
|
|
|
1535
1539
|
});
|
|
1536
1540
|
}
|
|
1537
1541
|
listActiveDurableJobs() {
|
|
1538
|
-
return this.store.listActiveDurableJobs();
|
|
1542
|
+
return [...this.store.listActiveDurableJobs(), ...this.store.listActiveGlobalJobRuns()];
|
|
1539
1543
|
}
|
|
1544
|
+
getGlobalJobRun(id) { return this.store.getGlobalJobRun(id); }
|
|
1540
1545
|
/**
|
|
1541
1546
|
* Apply one durable-job transition and, in the SAME transaction, enqueue
|
|
1542
1547
|
* the Leader wakeup. A terminal job without its wakeup enqueued is a lost
|
|
@@ -1548,10 +1553,24 @@ export class FileSchedulerStoreAdapter {
|
|
|
1548
1553
|
*/
|
|
1549
1554
|
transitionDurableJob(taskId, jobId, transition, now, wakeup) {
|
|
1550
1555
|
return this.store.transaction((store) => {
|
|
1556
|
+
if (taskId === "global") {
|
|
1557
|
+
const current = store.getGlobalJobRun(jobId);
|
|
1558
|
+
if (current === null)
|
|
1559
|
+
return null;
|
|
1560
|
+
const next = transition(current);
|
|
1561
|
+
if ("taskId" in next)
|
|
1562
|
+
throw new Error("A global JobRun cannot change scope.");
|
|
1563
|
+
store.saveGlobalJobRun(next);
|
|
1564
|
+
if (wakeup !== undefined)
|
|
1565
|
+
enqueueWork(store, { kind: "operator" }, "global-job-finished", now, [{ type: "global-job-run", id: jobId }], { source: "global-job", dedupeKey: `operator-global-job-run:${jobId}` });
|
|
1566
|
+
return next;
|
|
1567
|
+
}
|
|
1551
1568
|
const current = store.getDurableJob(taskId, jobId);
|
|
1552
1569
|
if (current === null)
|
|
1553
1570
|
return null;
|
|
1554
1571
|
const next = transition(current);
|
|
1572
|
+
if (!("taskId" in next))
|
|
1573
|
+
throw new Error("A Task JobRun cannot change scope.");
|
|
1555
1574
|
store.saveDurableJob(taskId, next);
|
|
1556
1575
|
if (wakeup !== undefined) {
|
|
1557
1576
|
enqueueWork(store, { kind: "role", taskId, roleName: "leader" }, wakeup.reason, now, [...wakeup.refs]);
|
|
@@ -1672,12 +1691,24 @@ export class FileSchedulerStoreAdapter {
|
|
|
1672
1691
|
store.saveTaskRoleSessionSet(selectNewTaskRoleSession(set, set.activeAgentId, now));
|
|
1673
1692
|
const event = createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "runtime.session-replaced", { roleName: owner.roleName }, now);
|
|
1674
1693
|
store.saveEvent(owner.taskId, event);
|
|
1675
|
-
//
|
|
1676
|
-
//
|
|
1694
|
+
// Release only operational occupancy after exact runtime stop. Neither
|
|
1695
|
+
// an uncertain automatic submission nor an Agent's handling claim may
|
|
1696
|
+
// be replayed as input to the replacement.
|
|
1677
1697
|
const leaderTarget = { kind: "role", taskId: owner.taskId, roleName: "leader" };
|
|
1678
1698
|
const leaderMailbox = store.getWorkMailbox(leaderTarget);
|
|
1679
|
-
if (owner.roleName === "leader" && leaderMailbox?.processing
|
|
1680
|
-
|
|
1699
|
+
if (owner.roleName === "leader" && leaderMailbox?.processing != null) {
|
|
1700
|
+
const processing = leaderMailbox.processing;
|
|
1701
|
+
if (processing.owner.startsWith("leader-notification:")) {
|
|
1702
|
+
store.saveEvent(owner.taskId, createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "notification.resolved", {
|
|
1703
|
+
wakeId: processing.owner.slice("leader-notification:".length), attemptId: processing.batchId,
|
|
1704
|
+
reason: "Session replacement after verified native/resource stop; inspect original effects.",
|
|
1705
|
+
outcome: "released-without-replay"
|
|
1706
|
+
}, now));
|
|
1707
|
+
store.saveWorkMailbox(completeProcessing(leaderMailbox, processing.batchId));
|
|
1708
|
+
}
|
|
1709
|
+
else if (processing.owner.startsWith("agent-inbox:")) {
|
|
1710
|
+
store.saveWorkMailbox(completeProcessing(leaderMailbox, processing.batchId));
|
|
1711
|
+
}
|
|
1681
1712
|
}
|
|
1682
1713
|
enqueueWork(store, leaderTarget, "session-replaced", now, [{ type: "event", taskId: owner.taskId, id: event.id }]);
|
|
1683
1714
|
}
|
|
@@ -1824,6 +1855,24 @@ export class FileSchedulerStoreAdapter {
|
|
|
1824
1855
|
if (mailbox === null)
|
|
1825
1856
|
return null;
|
|
1826
1857
|
const provider = store.getTaskRoleSessionSet(taskId, "leader")?.providerBinding?.run;
|
|
1858
|
+
// Only a committed lifecycle event opens this initialization path, not a
|
|
1859
|
+
// caller-supplied reason string. Consume that event once, separately from
|
|
1860
|
+
// business input. It exists only after exact-owner cleanup completed.
|
|
1861
|
+
const recoveryRefs = mailbox.pending?.reasons.includes("session-replaced")
|
|
1862
|
+
? mailbox.pending.refs.filter(ref => {
|
|
1863
|
+
if (ref.type !== "event" || ref.taskId !== taskId)
|
|
1864
|
+
return false;
|
|
1865
|
+
const event = store.queryContextRecords(taskId, { family: "task-event", ids: [ref.id],
|
|
1866
|
+
eventTypes: ["runtime.session-replaced"], limit: 1 }).records[0]?.value;
|
|
1867
|
+
return event?.payload.roleName === "leader";
|
|
1868
|
+
}) : [];
|
|
1869
|
+
// Valid earlier storage may retain an actively claimed mailbox even
|
|
1870
|
+
// after replacement completed. Its durable wake/claim remains untouched.
|
|
1871
|
+
if (recoveryRefs.length > 0 && mailbox.processing?.owner.startsWith("agent-inbox:")
|
|
1872
|
+
&& !(provider && ["submitting", "accepted", "delivery-unknown"].includes(provider.status))) {
|
|
1873
|
+
mailbox = completeProcessing(mailbox, mailbox.processing.batchId);
|
|
1874
|
+
store.saveWorkMailbox(mailbox);
|
|
1875
|
+
}
|
|
1827
1876
|
const processing = mailbox.processing;
|
|
1828
1877
|
if (processing !== null) {
|
|
1829
1878
|
if (!processing.owner.startsWith("leader-notification:"))
|
|
@@ -1867,7 +1916,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1867
1916
|
return { wakeId, attemptId: nextAttempt, disposition: "submit",
|
|
1868
1917
|
presentation: taskNotificationPresentation(wake, store) };
|
|
1869
1918
|
}
|
|
1870
|
-
if (unfinishedLeaderInbox(store, taskId))
|
|
1919
|
+
if (unfinishedLeaderInbox(store, taskId) && recoveryRefs.length === 0)
|
|
1871
1920
|
return null;
|
|
1872
1921
|
if (mailbox.pending === null || (provider !== null && provider !== undefined
|
|
1873
1922
|
&& ["submitting", "accepted", "delivery-unknown"].includes(provider.status)))
|
|
@@ -1880,7 +1929,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1880
1929
|
const obsoleteIds = new Set();
|
|
1881
1930
|
for (const message of store.listMessages(taskId)) {
|
|
1882
1931
|
if (pendingMessageIds.has(message.id) && (taskMessageInboxReceipt(store, message) !== undefined
|
|
1883
|
-
||
|
|
1932
|
+
|| nonReplayableMessageWake(store, message) !== undefined)) {
|
|
1884
1933
|
obsoleteIds.add(message.id);
|
|
1885
1934
|
}
|
|
1886
1935
|
}
|
|
@@ -1915,21 +1964,23 @@ export class FileSchedulerStoreAdapter {
|
|
|
1915
1964
|
const handoffs = claimedMessages.filter(message => !obsoleteIds.has(message.id));
|
|
1916
1965
|
// A quiet/new Session is not evidence that the claimed old Turn stopped.
|
|
1917
1966
|
// Keep the exact pending intent until the original fence is provable.
|
|
1918
|
-
if (
|
|
1967
|
+
if (recoveryRefs.length === 0
|
|
1968
|
+
&& handoffs.some((message) => interruptThenTerminalState(store, message) !== "ready"))
|
|
1919
1969
|
return null;
|
|
1920
1970
|
const handoffIds = new Set(handoffs.map((message) => message.id));
|
|
1921
1971
|
if (availableRefs.length === 0 && obsoleteIds.size > 0) {
|
|
1922
1972
|
store.saveWorkMailbox(consumePendingBatch(mailbox, pending));
|
|
1923
1973
|
return null;
|
|
1924
1974
|
}
|
|
1925
|
-
const selectedRefs = handoffs.length === 0 ? availableRefs
|
|
1975
|
+
const selectedRefs = recoveryRefs.length > 0 ? recoveryRefs : handoffs.length === 0 ? availableRefs
|
|
1926
1976
|
: availableRefs.filter((ref) => ref.type === "message" && handoffIds.has(ref.id));
|
|
1927
|
-
const remainingRefs =
|
|
1928
|
-
:
|
|
1977
|
+
const remainingRefs = recoveryRefs.length > 0 ? availableRefs.filter(ref => !recoveryRefs.includes(ref))
|
|
1978
|
+
: handoffs.length === 0 ? []
|
|
1979
|
+
: availableRefs.filter((ref) => !(ref.type === "message" && handoffIds.has(ref.id)));
|
|
1929
1980
|
const latest = latestTaskWake(store.listTaskWakes(taskId));
|
|
1930
1981
|
const wakeId = store.nextTaskWakeId(taskId);
|
|
1931
1982
|
const wake = createTaskWake({
|
|
1932
|
-
id: wakeId, taskId, reasons: mailbox.pending.reasons,
|
|
1983
|
+
id: wakeId, taskId, reasons: recoveryRefs.length > 0 ? ["session-replaced"] : mailbox.pending.reasons,
|
|
1933
1984
|
refs: pinMessageRevisions(store, taskId, selectedRefs),
|
|
1934
1985
|
fromCursor: latest?.toCursor ?? task.createdAt,
|
|
1935
1986
|
toCursor: new Date(Math.max(now.getTime(), Date.parse(mailbox.pending.lastQueuedAt))).toISOString(), now
|
|
@@ -2216,6 +2267,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
2216
2267
|
}
|
|
2217
2268
|
sessions = settleStructuredProviderTurn(sessions, canonicalRunId, input.providerStatus, now, attemptId);
|
|
2218
2269
|
store.saveTaskRoleSessionSet(sessions);
|
|
2270
|
+
checkInboxTurn(store, { scope: "task", taskId: input.taskId, roleName: input.roleName,
|
|
2271
|
+
agentId: input.agentId, nativeSessionId: input.nativeSessionId }, sessions.providerBinding?.run, now);
|
|
2219
2272
|
}
|
|
2220
2273
|
if (input.observation !== undefined)
|
|
2221
2274
|
this.persistRuntimeObservation(input.observation, now);
|
|
@@ -2321,6 +2374,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
2321
2374
|
}
|
|
2322
2375
|
if (input.observation !== undefined)
|
|
2323
2376
|
this.persistRuntimeObservation(input.observation, now);
|
|
2377
|
+
checkInboxTurn(store, { scope: "task", taskId: input.taskId, roleName: input.roleName,
|
|
2378
|
+
agentId: input.agentId, nativeSessionId: input.nativeSessionId }, sessions.providerBinding?.run, now);
|
|
2324
2379
|
return {
|
|
2325
2380
|
session: sessions.sessions[input.agentId],
|
|
2326
2381
|
duplicate: false,
|
|
@@ -2807,6 +2862,10 @@ export class FileSchedulerStoreAdapter {
|
|
|
2807
2862
|
// durable native terminal recorded just above remains the proof of record.
|
|
2808
2863
|
current = settleGlobalRoleRuntimeTurn(current, input.nativeTurnId, input.providerStatus, now);
|
|
2809
2864
|
store.saveGlobalRoleSessionSet(current);
|
|
2865
|
+
if (current.providerBinding?.run?.nativeTurnId === input.nativeTurnId) {
|
|
2866
|
+
checkInboxTurn(store, { scope: "global", roleName: input.roleName,
|
|
2867
|
+
agentId: input.agentId, nativeSessionId }, current.providerBinding.run, now);
|
|
2868
|
+
}
|
|
2810
2869
|
if (input.roleName === SYSTEM_OPERATOR_ROLE
|
|
2811
2870
|
&& input.adapterId === "codex"
|
|
2812
2871
|
&& completedStatus === "active") {
|
|
@@ -6,11 +6,12 @@ import { providerRetryPending } from "../runtime/providerRetry.js";
|
|
|
6
6
|
import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
|
|
7
7
|
import { globalMessagePresentation } from "../interaction/notificationPresentation.js";
|
|
8
8
|
import { recordGlobalRuntimeAttention, recordGlobalRuntimeRecovery } from "./globalRuntimeAttention.js";
|
|
9
|
+
import { globalInboxRecovery } from "../message/globalInboxRecovery.js";
|
|
9
10
|
/** Messages are the only queue. Host's atomic begin arbitrates native writers. */
|
|
10
11
|
export async function deliverGlobalInputs(home, store, ensure, onError, options = {}) {
|
|
11
12
|
for (const role of store.listGlobalRoles()) {
|
|
12
13
|
try {
|
|
13
|
-
let pending = store.queryGlobalRoleMessages(role.name, { pending: true, limit: 64 }).messages;
|
|
14
|
+
let pending = store.queryGlobalRoleMessages(role.name, { pending: true, unclaimed: true, limit: 64 }).messages;
|
|
14
15
|
if (hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
|
|
15
16
|
continue;
|
|
16
17
|
let sessions = store.getGlobalRoleSessionSet(role.name);
|
|
@@ -20,7 +21,7 @@ export async function deliverGlobalInputs(home, store, ensure, onError, options
|
|
|
20
21
|
continue;
|
|
21
22
|
if (sessions?.providerBinding?.retry !== undefined && !providerRetryPending(sessions.providerBinding)) {
|
|
22
23
|
store.transaction(tx => settleGlobalRetryInput(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, new Date()));
|
|
23
|
-
pending = store.queryGlobalRoleMessages(role.name, { pending: true, limit: 64 }).messages;
|
|
24
|
+
pending = store.queryGlobalRoleMessages(role.name, { pending: true, unclaimed: true, limit: 64 }).messages;
|
|
24
25
|
if (pending.length === 0)
|
|
25
26
|
continue;
|
|
26
27
|
}
|
|
@@ -84,12 +85,13 @@ export async function deliverGlobalInputs(home, store, ensure, onError, options
|
|
|
84
85
|
|| providerRetryPending(sessions?.providerBinding)
|
|
85
86
|
|| hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
|
|
86
87
|
continue;
|
|
87
|
-
pending = store.queryGlobalRoleMessages(role.name, { pending: true, limit: 64 }).messages;
|
|
88
|
+
pending = store.queryGlobalRoleMessages(role.name, { pending: true, unclaimed: true, limit: 64 }).messages;
|
|
88
89
|
if (pending.length === 0)
|
|
89
90
|
continue;
|
|
90
91
|
}
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
const recovery = globalInboxRecovery(store, role.name, options.now ?? new Date());
|
|
93
|
+
const recovering = recovery !== undefined && recovery.inbox === undefined && recovery.delivery === undefined
|
|
94
|
+
&& recovery.notDelivered === undefined && recovery.control === undefined;
|
|
93
95
|
// Interrupt-then reserves the next legal slot even behind a large queue.
|
|
94
96
|
const reservedPage = store.queryGlobalRoleMessages(role.name, {
|
|
95
97
|
pending: true, interruptThen: true, limit: 64
|
|
@@ -106,7 +108,7 @@ export async function deliverGlobalInputs(home, store, ensure, onError, options
|
|
|
106
108
|
store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(message, "native-session-or-authority-changed", new Date()));
|
|
107
109
|
const obsoleteIds = new Set(obsolete.map(message => message.id));
|
|
108
110
|
pending = pending.filter(message => !obsoleteIds.has(message.id));
|
|
109
|
-
if (pending.length === 0)
|
|
111
|
+
if (pending.length === 0 && !recovering)
|
|
110
112
|
continue;
|
|
111
113
|
if (reservedPage.count > reserved.length && obsolete.length === reserved.length)
|
|
112
114
|
continue;
|
|
@@ -117,7 +119,15 @@ export async function deliverGlobalInputs(home, store, ensure, onError, options
|
|
|
117
119
|
&& sessions.sessions[message.deliveryTarget.agentId]?.nativeSessionId === message.deliveryTarget.nativeSessionId;
|
|
118
120
|
const claims = reserved.filter(message => !obsolete.includes(message));
|
|
119
121
|
let next;
|
|
120
|
-
if (
|
|
122
|
+
if (recovering) {
|
|
123
|
+
// The notice carries no original business input. An obsolete then
|
|
124
|
+
// target is never inherited; a current explicit reservation still
|
|
125
|
+
// owns its slot. Host admission checks this again atomically.
|
|
126
|
+
if (claims.length > 0)
|
|
127
|
+
continue;
|
|
128
|
+
next = recovery;
|
|
129
|
+
}
|
|
130
|
+
else if (claims.length > 0) {
|
|
121
131
|
next = claims.find(message => {
|
|
122
132
|
const claim = message.interruptThen;
|
|
123
133
|
const binding = sessions?.providerBinding;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createGlobalRoleMessage } from "../message/message.js";
|
|
2
|
+
import { INBOX_CHECK_PREFIX, INBOX_CHECK_REASON, missingTurnHandling } from "../message/inboxTurn.js";
|
|
3
|
+
import { readDigest } from "../output/boundedRead.js";
|
|
4
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
5
|
+
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
6
|
+
import { retiredTaskRecordIds } from "../task/taskRecordRetirement.js";
|
|
7
|
+
import { completeProcessing } from "../coordination/workMailbox.js";
|
|
8
|
+
/** Called inside exact-terminal persistence's transaction. A terminal checks
|
|
9
|
+
* receipts; it cannot infer a disposition. One existing notification per exact
|
|
10
|
+
* turn, no timer, retry queue, automatic ACK or reminder-of-reminder loop. */
|
|
11
|
+
export function checkInboxTurn(store, actor, turn, now) {
|
|
12
|
+
if (!turn || !["completed", "failed", "cancelled"].includes(turn.status))
|
|
13
|
+
return;
|
|
14
|
+
const retired = actor.scope === "task" ? retiredTaskRecordIds(store.listEvents(actor.taskId), "message") : new Set();
|
|
15
|
+
const entries = actor.scope === "global" ? store.listGlobalRoleMessages(actor.roleName)
|
|
16
|
+
: [...(actor.roleName === "leader" ? store.listTaskWakes(actor.taskId) : []),
|
|
17
|
+
...store.listMessages(actor.taskId).filter(message => !retired.has(message.id))];
|
|
18
|
+
const missing = missingTurnHandling(entries, actor, turn);
|
|
19
|
+
if (actor.scope === "task" && actor.roleName === "leader") {
|
|
20
|
+
const mailbox = store.getWorkMailbox({ kind: "role", taskId: actor.taskId, roleName: "leader" });
|
|
21
|
+
// End the exact active-consumption notification reservation only. Its
|
|
22
|
+
// original claim remains unfinished and discoverable; no writer is freed
|
|
23
|
+
// here (the caller already persisted an exact native terminal).
|
|
24
|
+
if (mailbox?.processing && entries.some(entry => entry.inbox?.claim.actor.attemptId === turn.attemptId
|
|
25
|
+
&& entry.inbox.claim.actor.nativeSessionId === actor.nativeSessionId
|
|
26
|
+
&& entry.inbox.claim.actor.agentId === actor.agentId
|
|
27
|
+
&& mailbox.processing?.owner === `agent-inbox:${entry.id}`
|
|
28
|
+
&& mailbox.processing.batchId === `inbox:${entry.id}`)) {
|
|
29
|
+
store.saveWorkMailbox(completeProcessing(mailbox, mailbox.processing.batchId));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!missing.length)
|
|
33
|
+
return;
|
|
34
|
+
const requestId = INBOX_CHECK_PREFIX + readDigest([actor.scope, actor.taskId, actor.roleName,
|
|
35
|
+
actor.agentId, actor.nativeSessionId, turn.attemptId]);
|
|
36
|
+
const facts = {
|
|
37
|
+
roleName: actor.roleName, nativeSessionId: actor.nativeSessionId, attemptId: turn.attemptId,
|
|
38
|
+
status: turn.status, count: String(missing.length), entries: missing.slice(0, 16).map(entry => entry.id).join(","),
|
|
39
|
+
read: actor.scope === "global" || actor.roleName === "leader" ? "yui inbox list --view history --json"
|
|
40
|
+
: `yui task message show ${actor.taskId}/${missing[0].id} --json`,
|
|
41
|
+
disposition: "Missing handling evidence, not unexecuted work. Read originals and prior effects; explicitly ack or retire within authority. Never replay external effects."
|
|
42
|
+
};
|
|
43
|
+
if (actor.scope === "global") {
|
|
44
|
+
if (store.queryGlobalRoleMessages(actor.roleName, { requestId, limit: 1 }).count)
|
|
45
|
+
return;
|
|
46
|
+
const notice = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), actor.roleName, `Yui-Summary: ${missing.length} input(s) lack handling receipts after an exact native terminal.\n`
|
|
47
|
+
+ JSON.stringify(facts), "system", { type: "system" }, now, { inputControl: { action: "queue", requestId } });
|
|
48
|
+
// Retain the original target: replacing a Session is not permission to
|
|
49
|
+
// replay this reminder elsewhere. Successors can inspect history explicitly.
|
|
50
|
+
store.saveGlobalRoleMessage({ ...notice,
|
|
51
|
+
deliveryTarget: { agentId: actor.agentId, nativeSessionId: actor.nativeSessionId } });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
if (store.listEvents(actor.taskId).some(event => event.type === INBOX_CHECK_REASON
|
|
55
|
+
&& event.payload.requestId === requestId))
|
|
56
|
+
return;
|
|
57
|
+
const event = createTaskEvent(store.nextEventId(actor.taskId), actor.taskId, INBOX_CHECK_REASON, { ...facts, requestId }, now);
|
|
58
|
+
store.saveEvent(actor.taskId, event);
|
|
59
|
+
const task = store.getTask(actor.taskId);
|
|
60
|
+
if (task && ["active", "draft"].includes(task.status) && task.executionGate.state === "enabled") {
|
|
61
|
+
enqueueWork(store, { kind: "role", taskId: actor.taskId, roleName: "leader" }, INBOX_CHECK_REASON, now, [{ type: "event", taskId: actor.taskId, id: event.id }], { source: "inbox-check", dedupeKey: requestId });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* preserves its admitted Job across authorized Session replacement.
|
|
10
10
|
*/
|
|
11
11
|
import { execSync } from "node:child_process";
|
|
12
|
-
import { createHash } from "node:crypto";
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import { resolve, sep } from "node:path";
|
|
14
14
|
import { activeLiveRoleAgentSession } from "../executor/agentExecutor.js";
|
|
15
15
|
import { acknowledgeUnknownDurableJob, createDurableJob, durableJobIdempotencyKey, isDurableJobTerminal, requestDurableJobCancel, retryDurableJobIdempotencyKey } from "../job/durableJob.js";
|
|
@@ -21,6 +21,7 @@ import { assertJobAssignmentScope, JobAssignmentScopeError } from "../job/jobAss
|
|
|
21
21
|
import { assertContextRecordReadable } from "../context/taskContext.js";
|
|
22
22
|
import { assertTaskProjectWriteAuthority } from "../task/taskAuthority.js";
|
|
23
23
|
import { validateHttpPreview } from "../job/httpPreview.js";
|
|
24
|
+
import { definitionFromTaskRun } from "../job/jobDefinition.js";
|
|
24
25
|
export function createDurableJobControl(store) {
|
|
25
26
|
const authority = createJobCallAuthority(store);
|
|
26
27
|
return {
|
|
@@ -34,7 +35,7 @@ export function createDurableJobControl(store) {
|
|
|
34
35
|
requireJobAssignment(tx, params, params.caller);
|
|
35
36
|
assertNonSecretJobInput(params);
|
|
36
37
|
validateHttpPreview(params.env, params.steps);
|
|
37
|
-
const
|
|
38
|
+
const specificationKey = durableJobIdempotencyKey({
|
|
38
39
|
owner: params.owner,
|
|
39
40
|
projectId: params.projectId,
|
|
40
41
|
head: params.head,
|
|
@@ -42,6 +43,12 @@ export function createDurableJobControl(store) {
|
|
|
42
43
|
workspace: params.workspace,
|
|
43
44
|
env: params.env
|
|
44
45
|
});
|
|
46
|
+
const definition = params.definitionId === undefined ? null
|
|
47
|
+
: tx.getJobDefinition({ kind: "task", taskId: params.taskId }, params.definitionId);
|
|
48
|
+
if (params.definitionId !== undefined && definition === null)
|
|
49
|
+
throw jobDomainError("Job definition not found.");
|
|
50
|
+
const baseKey = definition === null ? specificationKey : createHash("sha256")
|
|
51
|
+
.update(JSON.stringify([specificationKey, definition.id, definition.revision, params.trigger])).digest("hex");
|
|
45
52
|
const inputDigest = params.retryOf === undefined
|
|
46
53
|
? baseKey
|
|
47
54
|
: retryDurableJobIdempotencyKey(baseKey, params.retryOf);
|
|
@@ -77,9 +84,29 @@ export function createDurableJobControl(store) {
|
|
|
77
84
|
}
|
|
78
85
|
authority.authorize(context, params.taskId);
|
|
79
86
|
validateStartParams(tx, params);
|
|
87
|
+
const previous = params.retryOf === undefined ? null : tx.getDurableJob(params.taskId, params.retryOf);
|
|
88
|
+
if (previous !== null) {
|
|
89
|
+
requireJobAssignment(tx, previous, params.caller);
|
|
90
|
+
if (definition === null && durableJobIdempotencyKey(previous) !== specificationKey) {
|
|
91
|
+
throw jobDomainError("Legacy retry must preserve the original Run specification; update its Job definition for a changed configuration.");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const definitionId = definition?.id ?? previous?.definitionId;
|
|
95
|
+
if (definitionId !== undefined) {
|
|
96
|
+
const active = tx.listDurableJobs(params.taskId).find(run => run.definitionId === definitionId
|
|
97
|
+
&& (run.status === "queued" || run.status === "running" || run.status === "unknown-needs-attention"));
|
|
98
|
+
if (active !== undefined)
|
|
99
|
+
throw jobDomainError(`Job is already occupied by Run ${active.id} (${active.status}); no new Run was admitted.`);
|
|
100
|
+
}
|
|
80
101
|
const id = tx.nextDurableJobId(params.taskId);
|
|
81
102
|
const job = createDurableJob({
|
|
82
103
|
id,
|
|
104
|
+
definitionId: definitionId ?? `definition-${randomUUID()}`,
|
|
105
|
+
definitionRevision: definition?.revision ?? previous?.definitionRevision,
|
|
106
|
+
trigger: params.trigger ?? {
|
|
107
|
+
source: params.retryOf !== undefined ? "retry" : params.owner.kind === "integration-attempt" ? "integration" : "manual",
|
|
108
|
+
key: requestId
|
|
109
|
+
},
|
|
83
110
|
taskId: params.taskId,
|
|
84
111
|
owner: params.owner,
|
|
85
112
|
projectId: params.projectId,
|
|
@@ -94,6 +121,9 @@ export function createDurableJobControl(store) {
|
|
|
94
121
|
artifactsLocator: `artifacts/jobs/${params.taskId}/${id}`,
|
|
95
122
|
...(params.retryOf === undefined ? {} : { retryOf: params.retryOf })
|
|
96
123
|
}, now);
|
|
124
|
+
if (tx.getJobDefinition({ kind: "task", taskId: params.taskId }, job.definitionId) === null) {
|
|
125
|
+
tx.saveJobDefinition(definitionFromTaskRun(job));
|
|
126
|
+
}
|
|
97
127
|
tx.saveDurableJob(params.taskId, job);
|
|
98
128
|
return { job, created: true };
|
|
99
129
|
});
|
|
@@ -191,7 +221,7 @@ function assertLeaderJobWriteScope(store, target, caller) {
|
|
|
191
221
|
throw jobControlError("UNAUTHORIZED", error instanceof Error ? error.message : String(error));
|
|
192
222
|
}
|
|
193
223
|
}
|
|
194
|
-
function requireJobAssignment(store, target, caller) {
|
|
224
|
+
export function requireJobAssignment(store, target, caller) {
|
|
195
225
|
try {
|
|
196
226
|
assertJobAssignmentScope(store, target, caller);
|
|
197
227
|
}
|
|
@@ -201,7 +231,7 @@ function requireJobAssignment(store, target, caller) {
|
|
|
201
231
|
throw error;
|
|
202
232
|
}
|
|
203
233
|
}
|
|
204
|
-
function jobAuthorityBinding(store, scope, roleName, taskId) {
|
|
234
|
+
export function jobAuthorityBinding(store, scope, roleName, taskId) {
|
|
205
235
|
// A Host detach/reattach preserves the native Session and its queued work.
|
|
206
236
|
// Authenticate the live launch at ingress, but bind accepted Jobs to the
|
|
207
237
|
// caller's durable Session identity, not its disposable Host generation.
|
|
@@ -232,7 +262,7 @@ function jobAuthorityBinding(store, scope, roleName, taskId) {
|
|
|
232
262
|
role.activeAgentId, session.agentId, session.nativeSessionId
|
|
233
263
|
])).digest("hex");
|
|
234
264
|
}
|
|
235
|
-
function assertNonSecretJobInput(params) {
|
|
265
|
+
export function assertNonSecretJobInput(params) {
|
|
236
266
|
// Existing Jobs persist commands and environments. This boundary accepts
|
|
237
267
|
// non-secret executable specifications only; credential resolution is not a
|
|
238
268
|
// Job feature. Never hash a known secret then call the digest sanitized.
|
|
@@ -263,7 +293,7 @@ function assertNonSecretJobInput(params) {
|
|
|
263
293
|
* workspace was not the stable checkout. Now it binds the job to the exact
|
|
264
294
|
* managed workspace, verifies write access, and requires an active Task.
|
|
265
295
|
*/
|
|
266
|
-
function validateStartParams(store, params) {
|
|
296
|
+
export function validateStartParams(store, params) {
|
|
267
297
|
validateJobTarget(store, params);
|
|
268
298
|
assertCallerAuthorized(store, params.caller, params.taskId);
|
|
269
299
|
assertLeaderJobWriteScope(store, params, params.caller);
|
|
@@ -384,7 +414,7 @@ function validateJobTarget(store, params) {
|
|
|
384
414
|
* identity contract does not defend against another process with the same
|
|
385
415
|
* user's filesystem access. Bare user calls cannot start or cancel Jobs.
|
|
386
416
|
*/
|
|
387
|
-
function assertCallerAuthorized(store, caller, taskId) {
|
|
417
|
+
export function assertCallerAuthorized(store, caller, taskId) {
|
|
388
418
|
if (caller.scope === "user") {
|
|
389
419
|
// rr13: A user-scope caller has no per-Session channel binding. Every
|
|
390
420
|
// durable-state claim it could carry is replayable
|
|
@@ -508,7 +538,7 @@ export function parseDurableJobStartParams(value) {
|
|
|
508
538
|
const retryOf = record.retryOf === undefined
|
|
509
539
|
? undefined
|
|
510
540
|
: requiredId(record.retryOf, "job.start retryOf");
|
|
511
|
-
const caller =
|
|
541
|
+
const caller = parseDurableJobCaller(record.caller);
|
|
512
542
|
return {
|
|
513
543
|
taskId,
|
|
514
544
|
owner,
|
|
@@ -531,7 +561,7 @@ export function parseDurableJobRefParams(value) {
|
|
|
531
561
|
return {
|
|
532
562
|
taskId: requiredId(record.taskId, "DurableJob taskId"),
|
|
533
563
|
jobId: requiredId(record.jobId, "DurableJob jobId"),
|
|
534
|
-
caller:
|
|
564
|
+
caller: parseDurableJobCaller(record.caller)
|
|
535
565
|
};
|
|
536
566
|
}
|
|
537
567
|
/**
|
|
@@ -548,7 +578,7 @@ export function parseDurableJobCancelParams(value) {
|
|
|
548
578
|
return {
|
|
549
579
|
taskId: requiredId(record.taskId, "DurableJob taskId"),
|
|
550
580
|
jobId: requiredId(record.jobId, "DurableJob jobId"),
|
|
551
|
-
caller:
|
|
581
|
+
caller: parseDurableJobCaller(record.caller)
|
|
552
582
|
};
|
|
553
583
|
}
|
|
554
584
|
/**
|
|
@@ -564,7 +594,7 @@ export function parseDurableJobAcknowledgeParams(value) {
|
|
|
564
594
|
return {
|
|
565
595
|
taskId: requiredId(record.taskId, "DurableJob taskId"),
|
|
566
596
|
jobId: requiredId(record.jobId, "DurableJob jobId"),
|
|
567
|
-
caller:
|
|
597
|
+
caller: parseDurableJobCaller(record.caller)
|
|
568
598
|
};
|
|
569
599
|
}
|
|
570
600
|
function parseJobOwner(value) {
|
|
@@ -645,7 +675,7 @@ function parseSteps(value) {
|
|
|
645
675
|
* optional managed Session identity fields. The identity is verified against
|
|
646
676
|
* durable state by `assertCallerAuthorized`; parsing only validates the JSON shape.
|
|
647
677
|
*/
|
|
648
|
-
function
|
|
678
|
+
export function parseDurableJobCaller(value) {
|
|
649
679
|
if (value === undefined) {
|
|
650
680
|
throw jobControlError("INVALID_PARAMS", "job caller is required.");
|
|
651
681
|
}
|