@zq-silk/yui 2.8.0 → 2.9.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 +35 -12
- package/dist/cli/interactionPolicy.js +2 -2
- package/dist/cli/updatePorts.js +20 -0
- package/dist/commands/durableJobCommands.js +50 -0
- 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 +2 -1
- package/dist/commands/taskFactCommands.js +24 -12
- package/dist/commands/taskPublicationCommands.js +6 -3
- package/dist/commands/taskPublicationVerifyCommand.js +74 -28
- package/dist/context/runContextPack.js +24 -4
- package/dist/context/taskContext.js +52 -15
- package/dist/context/taskPhase.js +99 -0
- package/dist/controlPlaneCli.js +24 -5
- package/dist/controller/controller.js +35 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +16 -1
- package/dist/controller/jobControl.js +42 -12
- package/dist/controller/jobDefinitionControl.js +304 -0
- package/dist/controller/jobSupervisor.js +47 -32
- 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 +2 -1
- package/dist/interaction/notificationPresentation.js +3 -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/repository/projectRefresh.js +58 -0
- package/dist/repository/taskWorkspacePreparer.js +2 -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 +69 -0
- package/dist/storage/recordValidation.js +5 -0
- package/dist/storage/scheduleSchema.js +12 -0
- package/dist/storage/sqliteSchema.js +75 -25
- package/dist/storage/sqliteStore.js +110 -9
- 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/publicationToolObservation.js +128 -0
- 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 +61 -0
- package/dist/web/assets/client/i18n/zh-CN.js +61 -0
- 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/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/webJobs.js +52 -0
- package/dist/web/webServer.js +31 -0
- package/docs/cli-information-contract.md +43 -2
- package/docs/cli-information-contract.zh-CN.md +32 -1
- package/docs/job-runs.md +224 -0
- package/docs/release-workflow.md +65 -0
- package/docs/release-workflow.zh-CN.md +49 -0
- package/docs/schedules.md +132 -0
- package/docs/storage-baseline.md +18 -1
- package/docs/storage-baseline.zh-CN.md +15 -1
- package/docs/task-delivery.md +68 -2
- package/docs/task-delivery.zh-CN.md +33 -1
- package/docs/testing/verification-levels.md +6 -2
- package/docs/testing/verification-levels.zh-CN.md +6 -3
- package/package.json +4 -3
- package/skills/yui-leader/SKILL.md +18 -1
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-leader/references/planning.md +9 -1
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-reviewer/VERSION +1 -1
- package/skills/yui-runtime/SKILL.md +9 -3
- 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-worker/SKILL.md +5 -0
- package/skills/yui-worker/VERSION +1 -1
|
@@ -4,8 +4,15 @@ import { upsertTaskPublication } from "./taskPublicationCommands.js";
|
|
|
4
4
|
import { dataError, runtimeError, taskNotFound, usageError, CliError } from "../errors/cliError.js";
|
|
5
5
|
import { publicationExternalKey } from "../task/publicationReference.js";
|
|
6
6
|
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
7
|
+
import { assertPublicationToolObservation, canonicalPublicationUrl, parsePublicationToolObservation, publicationObservationNotBefore } from "../task/publicationToolObservation.js";
|
|
7
8
|
export async function runTaskPublicationVerifyCommand(args, store, options) {
|
|
8
|
-
const
|
|
9
|
+
const observationIndex = args.indexOf("--observation");
|
|
10
|
+
if (observationIndex !== -1 && observationIndex !== args.length - 2) {
|
|
11
|
+
throw usageError("--observation must be followed by one JSON object at the end of the command.");
|
|
12
|
+
}
|
|
13
|
+
const supplied = observationIndex === -1 ? undefined
|
|
14
|
+
: parsePublicationToolObservation(args[observationIndex + 1]);
|
|
15
|
+
const reference = parsePublicationVerificationReference(observationIndex === -1 ? args : args.slice(0, observationIndex));
|
|
9
16
|
const task = store.getTask(reference.taskId);
|
|
10
17
|
if (task === null)
|
|
11
18
|
throw taskNotFound(reference.taskId);
|
|
@@ -13,31 +20,48 @@ export async function runTaskPublicationVerifyCommand(args, store, options) {
|
|
|
13
20
|
throw usageError(`Archived Task Publication cannot be verified: ${task.id}.`);
|
|
14
21
|
}
|
|
15
22
|
taskActor(options.environment, task.id);
|
|
16
|
-
const request = preparePublicationVerification(store, reference.taskId, reference.localId, options.environment);
|
|
17
|
-
const
|
|
18
|
-
if (verifier === undefined) {
|
|
19
|
-
throw usageError(`Publication verification is not supported for provider `
|
|
20
|
-
+ `${request.publication.provider}.`);
|
|
21
|
-
}
|
|
23
|
+
const request = preparePublicationVerification(store, reference.taskId, reference.localId, options.environment, supplied);
|
|
24
|
+
const now = options.now?.() ?? new Date();
|
|
22
25
|
let observation;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
if (error instanceof CliError)
|
|
37
|
-
throw error;
|
|
38
|
-
throw runtimeError(`Publication verification failed for ${request.publication.provider}/`
|
|
39
|
-
+ `${request.publication.repository}/${request.publication.externalId}.`, { cause: error });
|
|
26
|
+
if (supplied !== undefined) {
|
|
27
|
+
if (request.publication.evidence === supplied.evidence
|
|
28
|
+
&& request.publication.supersedes === supplied.publicationId) {
|
|
29
|
+
return {
|
|
30
|
+
kind: "output",
|
|
31
|
+
output: `Tool observation already recorded: ${request.publication.id} (${request.publication.verification}).\n`,
|
|
32
|
+
data: { publication: request.publication, observation: supplied }
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
assertPublicationToolObservation(request.publication, supplied, now, request.observationNotBefore);
|
|
36
|
+
observation = supplied;
|
|
40
37
|
}
|
|
38
|
+
else
|
|
39
|
+
try {
|
|
40
|
+
const verifier = options.verifiers[request.publication.provider];
|
|
41
|
+
if (verifier === undefined) {
|
|
42
|
+
throw usageError(`No built-in verifier for ${request.publication.provider}; `
|
|
43
|
+
+ "use --observation with evidence from an authorized tool.");
|
|
44
|
+
}
|
|
45
|
+
observation = await verifier.inspect({
|
|
46
|
+
provider: request.publication.provider,
|
|
47
|
+
repository: request.publication.repository,
|
|
48
|
+
externalKind: request.publication.externalKind,
|
|
49
|
+
externalId: request.publication.externalId,
|
|
50
|
+
...(request.publication.externalUrl === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { externalUrl: request.publication.externalUrl }),
|
|
53
|
+
...(request.publication.targetBranch === undefined
|
|
54
|
+
? {}
|
|
55
|
+
: { targetBranch: request.publication.targetBranch }),
|
|
56
|
+
expectedLocalCommit: request.expectedLocalCommit
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof CliError)
|
|
61
|
+
throw error;
|
|
62
|
+
throw runtimeError(`Publication verification failed for ${request.publication.provider}/`
|
|
63
|
+
+ `${request.publication.repository}/${request.publication.externalId}.`, { cause: error });
|
|
64
|
+
}
|
|
41
65
|
assertVerificationObservation(request, observation);
|
|
42
66
|
const result = commitPublicationVerification(store, request, observation, options.now?.() ?? new Date());
|
|
43
67
|
return {
|
|
@@ -54,7 +78,7 @@ export async function runTaskPublicationVerifyCommand(args, store, options) {
|
|
|
54
78
|
data: { publication: result.reference, observation }
|
|
55
79
|
};
|
|
56
80
|
}
|
|
57
|
-
function preparePublicationVerification(store, taskId, publicationId, environment) {
|
|
81
|
+
function preparePublicationVerification(store, taskId, publicationId, environment, supplied) {
|
|
58
82
|
return store.transaction((reader) => {
|
|
59
83
|
const task = reader.getTask(taskId);
|
|
60
84
|
if (task === null)
|
|
@@ -63,11 +87,18 @@ function preparePublicationVerification(store, taskId, publicationId, environmen
|
|
|
63
87
|
throw usageError(`Archived Task Publication cannot be verified: ${task.id}.`);
|
|
64
88
|
}
|
|
65
89
|
const actor = taskActor(environment, task.id);
|
|
66
|
-
|
|
90
|
+
let publication = reader.getPublicationReference(task.id, publicationId);
|
|
67
91
|
if (publication === null) {
|
|
68
92
|
throw dataError(`Publication reference not found: ${task.id}/${publicationId}.`);
|
|
69
93
|
}
|
|
70
94
|
const current = reader.findPublicationReferenceByExternalKey(publicationExternalKey(publication));
|
|
95
|
+
// Only the immediate immutable successor of this exact observation is a
|
|
96
|
+
// retry. A later observation/candidate must never revive an old receipt.
|
|
97
|
+
if (current?.taskId === task.id && current.supersedes === publication.id
|
|
98
|
+
&& supplied?.taskId === task.id && supplied.publicationId === publication.id
|
|
99
|
+
&& current.evidence === supplied.evidence) {
|
|
100
|
+
publication = current;
|
|
101
|
+
}
|
|
71
102
|
if (current === null || current.taskId !== task.id || current.id !== publication.id) {
|
|
72
103
|
throw usageError(`Publication ${publication.id} is not the current unsuperseded record `
|
|
73
104
|
+ `for ${publicationExternalKey(publication)}.`);
|
|
@@ -84,7 +115,9 @@ function preparePublicationVerification(store, taskId, publicationId, environmen
|
|
|
84
115
|
task: taskVerificationInvariant(task),
|
|
85
116
|
actor,
|
|
86
117
|
publication,
|
|
87
|
-
expectedLocalCommit: publication.localCommit
|
|
118
|
+
expectedLocalCommit: publication.localCommit,
|
|
119
|
+
observationNotBefore: supplied === undefined ? undefined
|
|
120
|
+
: publicationObservationNotBefore(publication, id => reader.getPublicationReference(task.id, id))
|
|
88
121
|
};
|
|
89
122
|
});
|
|
90
123
|
}
|
|
@@ -99,6 +132,18 @@ function assertVerificationObservation(request, observation) {
|
|
|
99
132
|
if (!/^[0-9a-f]{40}$/u.test(observation.headCommit)) {
|
|
100
133
|
throw usageError("Provider observation requires an exact remote head.");
|
|
101
134
|
}
|
|
135
|
+
if (publication.externalUrl !== undefined
|
|
136
|
+
&& (observation.externalUrl === undefined
|
|
137
|
+
|| canonicalPublicationUrl(observation.externalUrl, publication.provider)
|
|
138
|
+
!== canonicalPublicationUrl(publication.externalUrl, publication.provider))) {
|
|
139
|
+
throw usageError("Provider returned mismatched Publication service or URL.");
|
|
140
|
+
}
|
|
141
|
+
if (observation.targetBranch === undefined || observation.targetBranch.trim().length === 0) {
|
|
142
|
+
throw usageError("Provider observation requires an exact target branch.");
|
|
143
|
+
}
|
|
144
|
+
if (publication.targetBranch !== undefined && observation.targetBranch !== publication.targetBranch) {
|
|
145
|
+
throw usageError("Provider returned mismatched Publication target branch.");
|
|
146
|
+
}
|
|
102
147
|
if (observation.state === "merged" && observation.remoteCommit === undefined) {
|
|
103
148
|
throw usageError(`Merged remote ${publication.externalKind} did not expose a remote commit.`);
|
|
104
149
|
}
|
|
@@ -130,6 +175,7 @@ function commitPublicationVerification(store, request, observation, now) {
|
|
|
130
175
|
...(observation.externalUrl === undefined
|
|
131
176
|
? {}
|
|
132
177
|
: { externalUrl: observation.externalUrl }),
|
|
178
|
+
targetBranch: observation.targetBranch,
|
|
133
179
|
localCommit: request.expectedLocalCommit,
|
|
134
180
|
headCommit: observation.headCommit,
|
|
135
181
|
...(observation.remoteCommit === undefined ? {} : { remoteCommit: observation.remoteCommit }),
|
|
@@ -145,7 +191,7 @@ function commitPublicationVerification(store, request, observation, now) {
|
|
|
145
191
|
}
|
|
146
192
|
function parsePublicationVerificationReference(args) {
|
|
147
193
|
const usage = "Task publication verify usage: "
|
|
148
|
-
+ "yui task publication verify (<task>/<publication-id> | <task> <publication-id>).";
|
|
194
|
+
+ "yui task publication verify (<task>/<publication-id> | <task> <publication-id>) [--observation <json>].";
|
|
149
195
|
if (args.length !== 1 && args.length !== 2)
|
|
150
196
|
throw usageError(usage);
|
|
151
197
|
return resolveTaskRecordReference(args.length === 1 ? args[0] : `${args[0]}/${args[1]}`, { kind: "publicationReference", label: "Publication reference" });
|
|
@@ -300,9 +300,9 @@ export function contextSnapshotDeltaRefIds(store, snapshot) {
|
|
|
300
300
|
throw new Error(`Context Snapshot parent is missing or drifted: ${snapshot.parentRef.id}.`);
|
|
301
301
|
}
|
|
302
302
|
validateContextSnapshot(parent);
|
|
303
|
-
const previous = new Map(parent.refs.map((ref) => [ref.refId
|
|
303
|
+
const previous = new Map(parent.refs.map((ref) => [`${ref.store}:${ref.refId}`, ref]));
|
|
304
304
|
const changed = snapshot.refs.filter((ref) => {
|
|
305
|
-
const before = previous.get(ref.refId);
|
|
305
|
+
const before = previous.get(`${ref.store}:${ref.refId}`);
|
|
306
306
|
return before === undefined
|
|
307
307
|
|| before.digest !== ref.digest
|
|
308
308
|
|| before.revision !== ref.revision
|
|
@@ -440,7 +440,7 @@ export function expandRunContextRef(store, taskId, runId, refId, refStore) {
|
|
|
440
440
|
export function buildRunContextDelta(store, taskId, runId, after) {
|
|
441
441
|
const pack = buildRunContextPack(store, taskId, runId);
|
|
442
442
|
if (after === pack.digest || after === pack.snapshot.digest) {
|
|
443
|
-
return Object.freeze({ schemaVersion: 1, after, cursor: pack.digest, refs: [] });
|
|
443
|
+
return Object.freeze({ schemaVersion: 1, after, cursor: pack.digest, refs: [], removedRefs: [], complete: true });
|
|
444
444
|
}
|
|
445
445
|
const run = requireExactRun(store, taskId, runId);
|
|
446
446
|
const snapshot = readRunContextSnapshot(store, run);
|
|
@@ -449,11 +449,31 @@ export function buildRunContextDelta(store, taskId, runId, after) {
|
|
|
449
449
|
|| (parentDigest === undefined && after === "none"))) {
|
|
450
450
|
throw new Error("AgentRun Context delta cursor is outside the frozen Snapshot lineage.");
|
|
451
451
|
}
|
|
452
|
+
const parent = snapshot.parentRef === undefined ? null
|
|
453
|
+
: store.getContextSnapshot(taskId, snapshot.parentRef.id);
|
|
454
|
+
if (snapshot.parentRef !== undefined && (parent === null || parent.digest !== parentDigest
|
|
455
|
+
|| parent.sequence !== snapshot.parentRef.sequence)) {
|
|
456
|
+
throw new Error("AgentRun Context parent changed or is unavailable; reload the exact current pack.");
|
|
457
|
+
}
|
|
458
|
+
if (parent !== null)
|
|
459
|
+
validateContextSnapshot(parent);
|
|
460
|
+
const identity = (ref) => `${ref.store}:${ref.refId}`;
|
|
461
|
+
const previous = new Map((parent?.refs ?? []).map(ref => [identity(ref), ref]));
|
|
462
|
+
const present = new Set(pack.pointers.map(identity));
|
|
452
463
|
return Object.freeze({
|
|
453
464
|
schemaVersion: 1,
|
|
454
465
|
after,
|
|
455
466
|
cursor: pack.digest,
|
|
456
|
-
|
|
467
|
+
// deltaRefs is only a bounded launch hint. The immutable inventory (<=256)
|
|
468
|
+
// is the authority for a complete patch, including removals and changes
|
|
469
|
+
// beyond that hint's limit. Transport paginates large documents as usual.
|
|
470
|
+
refs: pack.pointers.filter(ref => {
|
|
471
|
+
const before = previous.get(identity(ref));
|
|
472
|
+
return before === undefined || contextContentDigest(before) !== contextContentDigest(ref);
|
|
473
|
+
}),
|
|
474
|
+
removedRefs: (parent?.refs ?? []).filter(ref => !present.has(identity(ref)))
|
|
475
|
+
.map(({ store, refId }) => ({ store, refId })),
|
|
476
|
+
complete: true
|
|
457
477
|
});
|
|
458
478
|
}
|
|
459
479
|
function requireExactRun(store, taskId, runId) {
|
|
@@ -2,7 +2,7 @@ import { runExecutionObservation } from "../agentRun/agentRun.js";
|
|
|
2
2
|
import { isGitArtifactRefString, parseGitArtifactRef } from "../artifacts/gitArtifactRef.js";
|
|
3
3
|
import { taskNotFound, usageError } from "../errors/cliError.js";
|
|
4
4
|
import { expandTaskMessageResult } from "../message/message.js";
|
|
5
|
-
import { resolveManagedTaskReader } from "../runtime/managedCaller.js";
|
|
5
|
+
import { currentManagedRuntime, resolveManagedTaskReader } from "../runtime/managedCaller.js";
|
|
6
6
|
import { hasManagedIdentity } from "../runtime/managedIdentity.js";
|
|
7
7
|
import { providerRetrySummary } from "../runtime/providerRetry.js";
|
|
8
8
|
import { projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryService.js";
|
|
@@ -12,6 +12,7 @@ import { contextContentDigest } from "./contextSnapshot.js";
|
|
|
12
12
|
import { buildRunContextPack } from "./runContextPack.js";
|
|
13
13
|
import { sourceRunContextValue } from "./sourceRunContext.js";
|
|
14
14
|
import { boundedDocument, fitsReadBudget, summaryText, recordPage } from "../output/boundedRead.js";
|
|
15
|
+
import { taskBriefGuard, taskPhase } from "./taskPhase.js";
|
|
15
16
|
const MAX_RECORDS = 64;
|
|
16
17
|
const MAX_VALUE_BYTES = 2048;
|
|
17
18
|
const MAX_PAGE_BYTES = 24 * 1024;
|
|
@@ -26,6 +27,9 @@ export function readTaskContext(store, taskId, environment = {}) {
|
|
|
26
27
|
const sources = authorizedSources(reader, scope, true);
|
|
27
28
|
const count = sources.reduce((total, source) => total + source.count, 0);
|
|
28
29
|
const cursor = currentCursor(reader, taskId);
|
|
30
|
+
const phase = scope.allow === undefined ? taskPhase(reader, scope.task) : undefined;
|
|
31
|
+
const phaseSource = phase?.activation?.source;
|
|
32
|
+
const priorMessageId = phaseSource?.priorMessageId;
|
|
29
33
|
// Reserve a small, classified view before ordinary history consumes the
|
|
30
34
|
// page. Counts and samples come from the exact same authorized read.
|
|
31
35
|
const attention = summarizeAttention(reader, taskId, scope.allow);
|
|
@@ -44,7 +48,20 @@ export function readTaskContext(store, taskId, environment = {}) {
|
|
|
44
48
|
const content = valueBytes > MAX_VALUE_BYTES || ["role", "task-message", "run", "work-item", "project-knowledge"].includes(entry.ref.store)
|
|
45
49
|
? { ref: entry.ref, summary: summarize(entry.value), omitted: true }
|
|
46
50
|
: { ...entry, omitted: false };
|
|
47
|
-
const
|
|
51
|
+
const revisedAfterActivation = entry.ref.store === "task-message" && phaseSource?.ref !== undefined
|
|
52
|
+
&& reader.queryContextRecords(taskId, {
|
|
53
|
+
family: "task-event", eventTypes: ["message.updated"], eventMessageId: entry.ref.refId,
|
|
54
|
+
afterSequence: sequence(phaseSource.ref.refId), limit: 0
|
|
55
|
+
}).count > 0;
|
|
56
|
+
const record = { ...content,
|
|
57
|
+
...(entry.ref.store === "task-message" && phase?.activation?.disposition === "adopted"
|
|
58
|
+
&& priorMessageId !== undefined ? {
|
|
59
|
+
phaseRelation: revisedAfterActivation ? "revised-after-activation-request"
|
|
60
|
+
: entry.ref.refId === phaseSource?.messageId ? "activation-source"
|
|
61
|
+
: Number(entry.ref.refId.slice("message-".length)) <= Number(priorMessageId.slice("message-".length))
|
|
62
|
+
? "recorded-before-activation-request" : "recorded-after-activation-request"
|
|
63
|
+
} : {}),
|
|
64
|
+
...(entry.ref.store === "run" ? {
|
|
48
65
|
execution: runExecutionObservation(entry.value, reader.getTaskRoleSessionSet(taskId, entry.value.roleName)?.providerBinding, runtimeEvents)
|
|
49
66
|
} : {}), ...(entry.ref.store === "role" ? {
|
|
50
67
|
providerRetry: providerRetrySummary(reader.getTaskRoleSessionSet(taskId, entry.value.name)?.providerBinding)
|
|
@@ -58,6 +75,14 @@ export function readTaskContext(store, taskId, environment = {}) {
|
|
|
58
75
|
const result = {
|
|
59
76
|
taskId, coreCursor: encode(cursor), throughCursor: encode(cursor),
|
|
60
77
|
view: "current",
|
|
78
|
+
...(scope.allow === undefined ? {
|
|
79
|
+
phase,
|
|
80
|
+
briefWriteGuard: taskBriefGuard(reader, scope.task)
|
|
81
|
+
} : {}),
|
|
82
|
+
caller: scope.caller === undefined ? { kind: "unmanaged-or-global" } : {
|
|
83
|
+
role: scope.caller.roleName,
|
|
84
|
+
executionAuthority: currentManagedRuntime(reader, environment, taskId)?.executionAuthority ?? "historical-read-only"
|
|
85
|
+
},
|
|
61
86
|
collections: Object.entries(sources.reduce((counts, source) => {
|
|
62
87
|
counts[source.family] = (counts[source.family] ?? 0) + source.count;
|
|
63
88
|
return counts;
|
|
@@ -133,11 +158,16 @@ export function readTaskContextDelta(store, taskId, input, environment = {}) {
|
|
|
133
158
|
const start = decode(input.after, taskId);
|
|
134
159
|
const now = currentCursor(reader, taskId);
|
|
135
160
|
const continuation = input.continuation === undefined ? undefined : decodePage(input.continuation, taskId);
|
|
161
|
+
if (continuation !== undefined && continuation.baseline !== encode(start)) {
|
|
162
|
+
throw usageError("Context continuation belongs to a different baseline; restart from the original starting cursor.");
|
|
163
|
+
}
|
|
136
164
|
const bound = continuation ?? now;
|
|
137
165
|
const after = continuation?.after ?? start.sequence;
|
|
138
|
-
if (start.
|
|
166
|
+
if (start.source !== now.source || bound.source !== now.source
|
|
167
|
+
|| start.sequence > bound.sequence || after < start.sequence || after > bound.sequence
|
|
168
|
+
|| start.revision > bound.revision
|
|
139
169
|
|| bound.sequence > now.sequence || start.revision > now.revision || bound.revision > now.revision) {
|
|
140
|
-
throw usageError(
|
|
170
|
+
throw usageError(`Context cursor is outside the current Task history; reread task context ${taskId} for a new bounded baseline.`);
|
|
141
171
|
}
|
|
142
172
|
const limit = input.limit ?? MAX_EVENTS;
|
|
143
173
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_EVENTS) {
|
|
@@ -163,11 +193,14 @@ export function readTaskContextDelta(store, taskId, input, environment = {}) {
|
|
|
163
193
|
}
|
|
164
194
|
const last = page.at(-1)?.ref.refId;
|
|
165
195
|
const more = selected.count > page.length;
|
|
166
|
-
const through = { taskId, sequence: bound.sequence, revision: bound.revision };
|
|
196
|
+
const through = { taskId, source: bound.source, sequence: bound.sequence, revision: bound.revision };
|
|
167
197
|
return {
|
|
168
|
-
taskId,
|
|
198
|
+
taskId, kind: "event-history", fromCursor: encode(start),
|
|
199
|
+
events: page, count: selected.count, complete: !more, throughCursor: encode(through),
|
|
200
|
+
stateChanged: start.revision !== bound.revision,
|
|
201
|
+
currentRead: `task context ${taskId}`,
|
|
169
202
|
...(more && last !== undefined
|
|
170
|
-
? { continuation: encode({ ...through, after: sequence(last) }) } : {}),
|
|
203
|
+
? { continuation: encode({ ...through, after: sequence(last), baseline: encode(start) }) } : {}),
|
|
171
204
|
observations: []
|
|
172
205
|
};
|
|
173
206
|
});
|
|
@@ -568,7 +601,8 @@ export function materialize(store, refId, value) {
|
|
|
568
601
|
return { ref: { store, refId, revision: String(record.revision ?? record.updatedAt ?? record.createdAt ?? digest), digest }, value };
|
|
569
602
|
}
|
|
570
603
|
function currentCursor(store, taskId) {
|
|
571
|
-
return { taskId,
|
|
604
|
+
return { taskId, source: store.getHomeIdentity().homeId,
|
|
605
|
+
sequence: store.latestEventSequence(taskId), revision: store.getStateRevision() };
|
|
572
606
|
}
|
|
573
607
|
function sequence(id) {
|
|
574
608
|
const result = /^event-(\d+)$/.exec(id);
|
|
@@ -584,20 +618,23 @@ function decode(value, taskId) {
|
|
|
584
618
|
if (value.length > 1024)
|
|
585
619
|
throw new Error("size");
|
|
586
620
|
const cursor = JSON.parse(Buffer.from(value, "base64url").toString());
|
|
587
|
-
if (cursor.taskId !== taskId ||
|
|
621
|
+
if (cursor.taskId !== taskId || typeof cursor.source !== "string"
|
|
622
|
+
|| !Number.isSafeInteger(cursor.sequence) || cursor.sequence < 0
|
|
588
623
|
|| !Number.isSafeInteger(cursor.revision) || cursor.revision < 0)
|
|
589
624
|
throw new Error("shape");
|
|
590
|
-
return cursor;
|
|
625
|
+
return { taskId: cursor.taskId, source: cursor.source, sequence: cursor.sequence, revision: cursor.revision };
|
|
591
626
|
}
|
|
592
627
|
catch {
|
|
593
|
-
throw usageError("Invalid Context cursor for this Task.");
|
|
628
|
+
throw usageError("Invalid or expired Context cursor for this Task; reread current Context for a new bounded baseline.");
|
|
594
629
|
}
|
|
595
630
|
}
|
|
596
631
|
function decodePage(value, taskId) {
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
632
|
+
const base = decode(value, taskId);
|
|
633
|
+
const cursor = JSON.parse(Buffer.from(value, "base64url").toString());
|
|
634
|
+
if (!Number.isSafeInteger(cursor.after) || cursor.after < 0 || typeof cursor.baseline !== "string") {
|
|
635
|
+
throw usageError("Invalid or expired Context continuation; reread current Context and start a new bounded window.");
|
|
636
|
+
}
|
|
637
|
+
return { ...base, after: cursor.after, baseline: cursor.baseline };
|
|
601
638
|
}
|
|
602
639
|
function freeze(value) {
|
|
603
640
|
if (value !== null && typeof value === "object") {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { contextContentDigest } from "./contextSnapshot.js";
|
|
2
|
+
/** Existing immutable event payloads carry provenance, not a second authority
|
|
3
|
+
* record. No body or historical aggregate is copied into the current view. */
|
|
4
|
+
export function activationSourceEvent(store, task) {
|
|
5
|
+
const requestId = task.activationRequest?.operation.requestId;
|
|
6
|
+
if (requestId === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
return store.queryContextRecords(task.id, {
|
|
9
|
+
family: "task-event", eventTypes: ["task.activation-requested"], eventRequestId: requestId,
|
|
10
|
+
descending: true, limit: 1
|
|
11
|
+
}).records[0]?.value;
|
|
12
|
+
}
|
|
13
|
+
export function taskPhase(store, task) {
|
|
14
|
+
const request = task.activationRequest;
|
|
15
|
+
const event = activationSourceEvent(store, task);
|
|
16
|
+
const messageId = event?.payload.sourceMessageId;
|
|
17
|
+
const message = messageId === undefined ? undefined : store.queryContextRecords(task.id, {
|
|
18
|
+
family: "task-message", ids: [messageId], limit: 1
|
|
19
|
+
}).records[0];
|
|
20
|
+
const body = message?.value?.body;
|
|
21
|
+
const status = event?.payload.sourceText !== undefined ? "available"
|
|
22
|
+
: messageId === undefined ? "missing"
|
|
23
|
+
: body === undefined ? "unavailable"
|
|
24
|
+
: contextContentDigest(body) === event?.payload.sourceBodyDigest ? "available" : "changed";
|
|
25
|
+
return {
|
|
26
|
+
status: task.status,
|
|
27
|
+
executionGate: task.executionGate.state,
|
|
28
|
+
brief: taskBriefFreshness(store, task),
|
|
29
|
+
activation: request === undefined ? null : {
|
|
30
|
+
requestId: request.operation.requestId, disposition: request.disposition,
|
|
31
|
+
actor: request.operation.actorId, effect: request.operation.effect,
|
|
32
|
+
...(request.afterPlanningRun === undefined ? {} : { afterPlanningRun: request.afterPlanningRun }),
|
|
33
|
+
source: {
|
|
34
|
+
status,
|
|
35
|
+
...(event === undefined ? {} : {
|
|
36
|
+
ref: { store: "task-event", refId: event.id, digest: contextContentDigest(event) },
|
|
37
|
+
read: `task event show ${task.id} ${event.id}`,
|
|
38
|
+
attribution: event.payload.sourceKind ?? "unspecified",
|
|
39
|
+
...(event.payload.priorMessageId === undefined ? {} : { priorMessageId: event.payload.priorMessageId }),
|
|
40
|
+
...(messageId === undefined ? {} : {
|
|
41
|
+
messageId, messageRead: `task message show ${task.id}/${messageId}`
|
|
42
|
+
})
|
|
43
|
+
}),
|
|
44
|
+
...(status === "available" ? {} : {
|
|
45
|
+
recovery: "Read the exact activation event and original input; missing provenance is not a new prohibition or permission."
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function activationWakeRefs(store, task) {
|
|
52
|
+
const event = activationSourceEvent(store, task);
|
|
53
|
+
if (event === undefined)
|
|
54
|
+
return [];
|
|
55
|
+
return [
|
|
56
|
+
{ type: "event", taskId: task.id, id: event.id },
|
|
57
|
+
...(event.payload.sourceMessageId === undefined ? [] : [
|
|
58
|
+
{ type: "message", taskId: task.id, id: event.payload.sourceMessageId }
|
|
59
|
+
])
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
const BRIEF_RELEVANT_EVENTS = [
|
|
63
|
+
"message.sent", "message.updated", "message.native-user-input", "brief.updated",
|
|
64
|
+
"decision.recorded", "decision.superseded", "input.requested", "input.answered", "input.cancelled", "input.auto-answered",
|
|
65
|
+
"run.completed", "run.failed", "run.cancelled", "job.state-changed", "work.accepted",
|
|
66
|
+
"runtime.role-delivery-failed", "run.retired",
|
|
67
|
+
"work.updated", "work.edited", "work.scope-updated", "work.rejected", "work.retired",
|
|
68
|
+
"review.completed", "review.failed", "integration.updated", "integration.completed", "integration.failed",
|
|
69
|
+
"milestone.added",
|
|
70
|
+
"task.activation-requested", "task.activation-adopted", "task.activated",
|
|
71
|
+
"task.execution-started", "task.execution-stopped", "task.reopened", "task.updated"
|
|
72
|
+
];
|
|
73
|
+
function latestBriefChange(store, taskId) {
|
|
74
|
+
return store.queryContextRecords(taskId, {
|
|
75
|
+
family: "task-event", eventTypes: BRIEF_RELEVANT_EVENTS, descending: true, limit: 1
|
|
76
|
+
}).records[0]?.value;
|
|
77
|
+
}
|
|
78
|
+
function taskBriefFreshness(store, task) {
|
|
79
|
+
const brief = store.getTaskBrief(task.id);
|
|
80
|
+
const last = latestBriefChange(store, task.id);
|
|
81
|
+
return {
|
|
82
|
+
status: brief === null ? "missing" : last === undefined ? "unverified"
|
|
83
|
+
: last.type === "brief.updated" ? "current-at-last-semantic-write" : "refresh-needed",
|
|
84
|
+
...(last === undefined ? {} : { changedBy: { store: "task-event", refId: last.id, type: last.type } }),
|
|
85
|
+
read: `task brief show ${task.id}`,
|
|
86
|
+
note: "Leader-maintained summary; lifecycle, gate and Session authority are separate live facts."
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Optimistic guard for semantic edits, independent of wall clocks and noisy
|
|
90
|
+
* runtime heartbeats. New input/result/decision/progress invalidates an old
|
|
91
|
+
* writer even when its Brief text itself has not changed. */
|
|
92
|
+
export function taskBriefGuard(store, task) {
|
|
93
|
+
const latest = latestBriefChange(store, task.id)?.id;
|
|
94
|
+
return contextContentDigest({
|
|
95
|
+
taskId: task.id, status: task.status, gate: task.executionGate,
|
|
96
|
+
title: task.title, description: task.description, activation: task.activationRequest,
|
|
97
|
+
brief: store.getTaskBrief(task.id), latest
|
|
98
|
+
});
|
|
99
|
+
}
|
package/dist/controlPlaneCli.js
CHANGED
|
@@ -24,6 +24,7 @@ import { operatorOfflineCommand, taskDiagnosticTarget } from "./cli/managedDiagn
|
|
|
24
24
|
import { assertConfigurationAuthority, assertTaskInvocationScope } from "./cli/invocationAuthority.js";
|
|
25
25
|
import { requireManagedGlobalCaller, requireManagedGlobalReader, ManagedRuntimeDriftError } from "./runtime/managedCaller.js";
|
|
26
26
|
import { sessionCallerEnvironment, requireManagedCaller } from "./runtime/sessionCaller.js";
|
|
27
|
+
import { maintenanceCallerEnvironment } from "./storage/maintenanceCaller.js";
|
|
27
28
|
import { checkCurrentSkills, SKILL_REUSE_RULE } from "./context/currentSkills.js";
|
|
28
29
|
import { roleSkillIds } from "./context/roleSessionContext.js";
|
|
29
30
|
import { hasManagedIdentity } from "./runtime/managedIdentity.js";
|
|
@@ -38,6 +39,7 @@ import { runConfigCommand } from "./commands/configCommands.js";
|
|
|
38
39
|
import { runConfigOverview } from "./commands/configOverview.js";
|
|
39
40
|
import { parseControllerCleanupOptions, parseControllerRuntimeSnapshot, parseControllerStatusOptions, renderControllerResourceStatus, renderRuntimeIdentitySection, runInteractiveControllerCleanup, summarizeDurablePhysicalMismatch } from "./commands/controllerCommands.js";
|
|
40
41
|
import { runDurableJobCommand } from "./commands/durableJobCommands.js";
|
|
42
|
+
import { runScheduleCommand } from "./commands/scheduleCommands.js";
|
|
41
43
|
import { parseExecutionAuditOptions, runExecutionAuditCommand } from "./commands/executionAuditCommands.js";
|
|
42
44
|
import { runGlobalRoleCommand, previewGlobalRoleAgentConfigurationMutation } from "./commands/globalRoleCommands.js";
|
|
43
45
|
import { runJobCommand } from "./commands/jobCommands.js";
|
|
@@ -1962,6 +1964,11 @@ export async function main() {
|
|
|
1962
1964
|
emit(runJobCommand(resolved.slice(1), store, { runtime }));
|
|
1963
1965
|
return;
|
|
1964
1966
|
}
|
|
1967
|
+
if (resolved[0] === "schedule") {
|
|
1968
|
+
const result = await runScheduleCommand(resolved.slice(1), { home, environment: process.env });
|
|
1969
|
+
emit(JSON.stringify(result, null, 2), false, result);
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1965
1972
|
if (resolved[0] === "job") {
|
|
1966
1973
|
emit(await runDurableJobCommand(resolved.slice(1), {
|
|
1967
1974
|
home,
|
|
@@ -1999,6 +2006,23 @@ async function preflightManagedTaskControlPlane() {
|
|
|
1999
2006
|
if (taskFinalReviewInvocation.error !== undefined) {
|
|
2000
2007
|
throw new Error(taskFinalReviewInvocation.error);
|
|
2001
2008
|
}
|
|
2009
|
+
if (["doctor", "upgrade", "update"].includes(args[0] ?? "")) {
|
|
2010
|
+
const home = resolveYuiHome(process.env);
|
|
2011
|
+
Object.assign(process.env, maintenanceCallerEnvironment(home, process.env, args[0] !== "doctor"));
|
|
2012
|
+
if (!hasManagedIdentity(process.env)
|
|
2013
|
+
|| (process.env.YUI_SESSION_SCOPE === "global" && process.env.YUI_ROLE === "operator")) {
|
|
2014
|
+
if (process.env.YUI_SESSION_SCOPE === "global") {
|
|
2015
|
+
const manifest = assertManagedSessionManifest(home, "global");
|
|
2016
|
+
if (manifest !== undefined && manifest.roleKind !== "operator") {
|
|
2017
|
+
throw new ManagedRuntimeDriftError("Managed global invocation does not match its Session Manifest.");
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
if (taskFinalReviewInvocation.request !== undefined) {
|
|
2021
|
+
throw new Error("Task final-review contract cannot be established by Home maintenance.");
|
|
2022
|
+
}
|
|
2023
|
+
return { contract: undefined, verifiedStore: undefined };
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2002
2026
|
// Desktop clients may carry only their provider-native identity. Resolve
|
|
2003
2027
|
// command-local hints before any public/Task/Global routing. This reads this
|
|
2004
2028
|
// Home only, and never creates a Session or mutates its registration.
|
|
@@ -2094,11 +2118,6 @@ async function preflightManagedGlobalControlPlane() {
|
|
|
2094
2118
|
|| manifest.roleKind !== expectedRoleKind)) {
|
|
2095
2119
|
throw new ManagedRuntimeDriftError("Managed global invocation does not match its Session Manifest.");
|
|
2096
2120
|
}
|
|
2097
|
-
if (expectedRoleKind === "operator" && ["doctor", "upgrade", "update"].includes(args[0] ?? "")) {
|
|
2098
|
-
// These commands inspect/adopt the storage contract themselves. Requiring
|
|
2099
|
-
// current storage before reaching upgrade would make recovery impossible.
|
|
2100
|
-
return { contract: undefined, verifiedStore: undefined };
|
|
2101
|
-
}
|
|
2102
2121
|
await assertRuntimeCoherence({ actualHome: home }, {
|
|
2103
2122
|
checkController: !(expectedRoleKind === "operator" && operatorOfflineCommand(args))
|
|
2104
2123
|
});
|
|
@@ -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) {
|
|
@@ -1535,8 +1535,9 @@ export class FileSchedulerStoreAdapter {
|
|
|
1535
1535
|
});
|
|
1536
1536
|
}
|
|
1537
1537
|
listActiveDurableJobs() {
|
|
1538
|
-
return this.store.listActiveDurableJobs();
|
|
1538
|
+
return [...this.store.listActiveDurableJobs(), ...this.store.listActiveGlobalJobRuns()];
|
|
1539
1539
|
}
|
|
1540
|
+
getGlobalJobRun(id) { return this.store.getGlobalJobRun(id); }
|
|
1540
1541
|
/**
|
|
1541
1542
|
* Apply one durable-job transition and, in the SAME transaction, enqueue
|
|
1542
1543
|
* the Leader wakeup. A terminal job without its wakeup enqueued is a lost
|
|
@@ -1548,10 +1549,24 @@ export class FileSchedulerStoreAdapter {
|
|
|
1548
1549
|
*/
|
|
1549
1550
|
transitionDurableJob(taskId, jobId, transition, now, wakeup) {
|
|
1550
1551
|
return this.store.transaction((store) => {
|
|
1552
|
+
if (taskId === "global") {
|
|
1553
|
+
const current = store.getGlobalJobRun(jobId);
|
|
1554
|
+
if (current === null)
|
|
1555
|
+
return null;
|
|
1556
|
+
const next = transition(current);
|
|
1557
|
+
if ("taskId" in next)
|
|
1558
|
+
throw new Error("A global JobRun cannot change scope.");
|
|
1559
|
+
store.saveGlobalJobRun(next);
|
|
1560
|
+
if (wakeup !== undefined)
|
|
1561
|
+
enqueueWork(store, { kind: "operator" }, "global-job-finished", now, [{ type: "global-job-run", id: jobId }], { source: "global-job", dedupeKey: `operator-global-job-run:${jobId}` });
|
|
1562
|
+
return next;
|
|
1563
|
+
}
|
|
1551
1564
|
const current = store.getDurableJob(taskId, jobId);
|
|
1552
1565
|
if (current === null)
|
|
1553
1566
|
return null;
|
|
1554
1567
|
const next = transition(current);
|
|
1568
|
+
if (!("taskId" in next))
|
|
1569
|
+
throw new Error("A Task JobRun cannot change scope.");
|
|
1555
1570
|
store.saveDurableJob(taskId, next);
|
|
1556
1571
|
if (wakeup !== undefined) {
|
|
1557
1572
|
enqueueWork(store, { kind: "role", taskId, roleName: "leader" }, wakeup.reason, now, [...wakeup.refs]);
|