@zq-silk/yui 2.8.1 → 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.
Files changed (85) hide show
  1. package/dist/cli/commandCatalog.js +35 -12
  2. package/dist/cli/interactionPolicy.js +2 -2
  3. package/dist/commands/durableJobCommands.js +50 -0
  4. package/dist/commands/projectCommands.js +4 -33
  5. package/dist/commands/scheduleCommands.js +45 -0
  6. package/dist/commands/taskActivationCommands.js +19 -5
  7. package/dist/commands/taskCommands.js +2 -1
  8. package/dist/commands/taskFactCommands.js +24 -12
  9. package/dist/commands/taskPublicationCommands.js +6 -3
  10. package/dist/commands/taskPublicationVerifyCommand.js +74 -28
  11. package/dist/context/runContextPack.js +24 -4
  12. package/dist/context/taskContext.js +52 -15
  13. package/dist/context/taskPhase.js +99 -0
  14. package/dist/controlPlaneCli.js +6 -0
  15. package/dist/controller/controller.js +35 -0
  16. package/dist/controller/fileSchedulerStoreAdapter.js +16 -1
  17. package/dist/controller/jobControl.js +42 -12
  18. package/dist/controller/jobDefinitionControl.js +304 -0
  19. package/dist/controller/jobSupervisor.js +47 -32
  20. package/dist/controller/runtime.js +11 -2
  21. package/dist/controller/scheduleControl.js +176 -0
  22. package/dist/coordination/workMailbox.js +3 -3
  23. package/dist/external/githubPublicationVerifier.js +14 -5
  24. package/dist/external/gitlabPublicationVerifier.js +2 -1
  25. package/dist/interaction/notificationPresentation.js +3 -3
  26. package/dist/interaction/operatorPresentation.js +5 -0
  27. package/dist/job/builtinJob.js +58 -0
  28. package/dist/job/durableJob.js +72 -49
  29. package/dist/job/jobDefinition.js +68 -0
  30. package/dist/job/jobRunner.js +2 -1
  31. package/dist/job/projectRefreshRunner.js +48 -0
  32. package/dist/repository/projectRefresh.js +58 -0
  33. package/dist/repository/taskWorkspacePreparer.js +2 -1
  34. package/dist/schedule/schedule.js +108 -0
  35. package/dist/scheduler/operatorEvent.js +2 -0
  36. package/dist/scheduler/operatorInputNotificationProcessor.js +8 -0
  37. package/dist/storage/contextRecords.js +8 -0
  38. package/dist/storage/jobSchema.js +15 -0
  39. package/dist/storage/maintenanceCaller.js +1 -1
  40. package/dist/storage/recordValidation.js +5 -0
  41. package/dist/storage/scheduleSchema.js +12 -0
  42. package/dist/storage/sqliteSchema.js +74 -24
  43. package/dist/storage/sqliteStore.js +110 -9
  44. package/dist/storage/storageVersions.js +1 -1
  45. package/dist/storage/storeRpc.js +5 -0
  46. package/dist/storage/upgrade/jobDefinitionMigration.js +22 -0
  47. package/dist/task/publicationToolObservation.js +128 -0
  48. package/dist/task/taskActivationService.js +35 -0
  49. package/dist/web/assets/client/app/settings.js +7 -1
  50. package/dist/web/assets/client/i18n/en.js +61 -0
  51. package/dist/web/assets/client/i18n/zh-CN.js +61 -0
  52. package/dist/web/assets/client/index.js +2 -0
  53. package/dist/web/assets/client/views/jobs.js +222 -0
  54. package/dist/web/assets/client/views/task/page.js +11 -1
  55. package/dist/web/assets/shared/manageNav.js +1 -0
  56. package/dist/web/assets/shared/taskTabs.js +2 -2
  57. package/dist/web/assets/shell/settingsContent.js +3 -0
  58. package/dist/web/assets/styles/index.js +2 -0
  59. package/dist/web/assets/styles/views/jobs.js +19 -0
  60. package/dist/web/webJobs.js +52 -0
  61. package/dist/web/webServer.js +31 -0
  62. package/docs/cli-information-contract.md +43 -2
  63. package/docs/cli-information-contract.zh-CN.md +32 -1
  64. package/docs/job-runs.md +224 -0
  65. package/docs/release-workflow.md +43 -0
  66. package/docs/release-workflow.zh-CN.md +33 -0
  67. package/docs/schedules.md +132 -0
  68. package/docs/storage-baseline.md +18 -1
  69. package/docs/storage-baseline.zh-CN.md +15 -1
  70. package/docs/task-delivery.md +68 -2
  71. package/docs/task-delivery.zh-CN.md +33 -1
  72. package/docs/testing/verification-levels.md +6 -2
  73. package/docs/testing/verification-levels.zh-CN.md +6 -3
  74. package/package.json +4 -3
  75. package/skills/yui-leader/SKILL.md +18 -1
  76. package/skills/yui-leader/VERSION +1 -1
  77. package/skills/yui-leader/references/planning.md +9 -1
  78. package/skills/yui-reviewer/SKILL.md +4 -0
  79. package/skills/yui-reviewer/VERSION +1 -1
  80. package/skills/yui-runtime/SKILL.md +9 -3
  81. package/skills/yui-runtime/VERSION +1 -1
  82. package/skills/yui-runtime/references/context-ordering.md +103 -0
  83. package/skills/yui-runtime/references/publication.md +44 -4
  84. package/skills/yui-worker/SKILL.md +5 -0
  85. 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 reference = parsePublicationVerificationReference(args);
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 verifier = options.verifiers[request.publication.provider];
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
- try {
24
- observation = await verifier.inspect({
25
- provider: request.publication.provider,
26
- repository: request.publication.repository,
27
- externalKind: request.publication.externalKind,
28
- externalId: request.publication.externalId,
29
- ...(request.publication.externalUrl === undefined
30
- ? {}
31
- : { externalUrl: request.publication.externalUrl }),
32
- expectedLocalCommit: request.expectedLocalCommit
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
- const publication = reader.getPublicationReference(task.id, publicationId);
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, ref]));
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
- refs: pack.pointers.filter(ref => pack.deltaRefs.some(delta => delta.store === ref.store && delta.refId === ref.refId))
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 record = { ...content, ...(entry.ref.store === "run" ? {
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.sequence > bound.sequence || after < start.sequence || after > bound.sequence
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("Context cursor is outside the current Task history.");
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, events: page, count: selected.count, throughCursor: encode(through),
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, sequence: store.latestEventSequence(taskId), revision: store.getStateRevision() };
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 || !Number.isSafeInteger(cursor.sequence) || cursor.sequence < 0
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 cursor = decode(value, taskId);
598
- if (!Number.isSafeInteger(cursor.after) || cursor.after < 0)
599
- throw usageError("Invalid Context continuation.");
600
- return cursor;
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
+ }
@@ -39,6 +39,7 @@ import { runConfigCommand } from "./commands/configCommands.js";
39
39
  import { runConfigOverview } from "./commands/configOverview.js";
40
40
  import { parseControllerCleanupOptions, parseControllerRuntimeSnapshot, parseControllerStatusOptions, renderControllerResourceStatus, renderRuntimeIdentitySection, runInteractiveControllerCleanup, summarizeDurablePhysicalMismatch } from "./commands/controllerCommands.js";
41
41
  import { runDurableJobCommand } from "./commands/durableJobCommands.js";
42
+ import { runScheduleCommand } from "./commands/scheduleCommands.js";
42
43
  import { parseExecutionAuditOptions, runExecutionAuditCommand } from "./commands/executionAuditCommands.js";
43
44
  import { runGlobalRoleCommand, previewGlobalRoleAgentConfigurationMutation } from "./commands/globalRoleCommands.js";
44
45
  import { runJobCommand } from "./commands/jobCommands.js";
@@ -1963,6 +1964,11 @@ export async function main() {
1963
1964
  emit(runJobCommand(resolved.slice(1), store, { runtime }));
1964
1965
  return;
1965
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
+ }
1966
1972
  if (resolved[0] === "job") {
1967
1973
  emit(await runDurableJobCommand(resolved.slice(1), {
1968
1974
  home,
@@ -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]);