@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.
Files changed (119) hide show
  1. package/README.md +5 -3
  2. package/dist/cli/commandCatalog.js +56 -21
  3. package/dist/cli/helpRenderer.js +3 -1
  4. package/dist/cli/interactionPolicy.js +2 -2
  5. package/dist/commands/durableJobCommands.js +50 -0
  6. package/dist/commands/globalRoleCommands.js +30 -9
  7. package/dist/commands/inboxCommands.js +79 -22
  8. package/dist/commands/projectCommands.js +4 -33
  9. package/dist/commands/scheduleCommands.js +45 -0
  10. package/dist/commands/taskActivationCommands.js +19 -5
  11. package/dist/commands/taskCommands.js +8 -2
  12. package/dist/commands/taskFactCommands.js +24 -12
  13. package/dist/commands/taskProjectDeliveryCommand.js +56 -0
  14. package/dist/commands/taskPublicationCommands.js +6 -3
  15. package/dist/commands/taskPublicationVerifyCommand.js +74 -28
  16. package/dist/commands/taskRemoteDeliveryCommand.js +5 -3
  17. package/dist/context/runContextPack.js +25 -4
  18. package/dist/context/runInputContract.js +1 -0
  19. package/dist/context/taskContext.js +53 -16
  20. package/dist/context/taskPhase.js +99 -0
  21. package/dist/controlPlaneCli.js +38 -17
  22. package/dist/controller/controller.js +35 -0
  23. package/dist/controller/fileSchedulerStoreAdapter.js +75 -16
  24. package/dist/controller/globalInputDelivery.js +17 -7
  25. package/dist/controller/inboxTurnCheck.js +64 -0
  26. package/dist/controller/jobControl.js +42 -12
  27. package/dist/controller/jobDefinitionControl.js +304 -0
  28. package/dist/controller/jobSupervisor.js +47 -32
  29. package/dist/controller/providerRetryAdmission.js +2 -0
  30. package/dist/controller/runtime.js +11 -2
  31. package/dist/controller/scheduleControl.js +176 -0
  32. package/dist/coordination/workMailbox.js +3 -3
  33. package/dist/external/githubPublicationVerifier.js +14 -5
  34. package/dist/external/gitlabPublicationVerifier.js +12 -29
  35. package/dist/grant/taskAuthorization.js +7 -2
  36. package/dist/interaction/notificationPresentation.js +12 -3
  37. package/dist/interaction/operatorPresentation.js +5 -0
  38. package/dist/job/builtinJob.js +58 -0
  39. package/dist/job/durableJob.js +72 -49
  40. package/dist/job/jobDefinition.js +68 -0
  41. package/dist/job/jobRunner.js +2 -1
  42. package/dist/job/projectRefreshRunner.js +48 -0
  43. package/dist/message/globalInboxRecovery.js +44 -0
  44. package/dist/message/globalInterrupt.js +1 -1
  45. package/dist/message/inboxReceipt.js +14 -6
  46. package/dist/message/inboxTurn.js +86 -0
  47. package/dist/message/message.js +11 -0
  48. package/dist/message/taskInboxOwnership.js +20 -6
  49. package/dist/repository/projectRefresh.js +58 -0
  50. package/dist/repository/taskWorkspacePreparer.js +2 -1
  51. package/dist/repository/workspaceCleanupInspection.js +6 -1
  52. package/dist/schedule/schedule.js +108 -0
  53. package/dist/scheduler/operatorEvent.js +2 -0
  54. package/dist/scheduler/operatorInputNotificationProcessor.js +8 -0
  55. package/dist/storage/contextRecords.js +8 -0
  56. package/dist/storage/jobSchema.js +15 -0
  57. package/dist/storage/maintenanceCaller.js +1 -1
  58. package/dist/storage/recordValidation.js +5 -0
  59. package/dist/storage/scheduleSchema.js +12 -0
  60. package/dist/storage/sqliteSchema.js +95 -24
  61. package/dist/storage/sqliteStore.js +123 -10
  62. package/dist/storage/storageVersions.js +1 -1
  63. package/dist/storage/storeRpc.js +5 -0
  64. package/dist/storage/upgrade/jobDefinitionMigration.js +22 -0
  65. package/dist/task/archiveDiagnostics.js +1 -1
  66. package/dist/task/archivePreflight.js +1 -1
  67. package/dist/task/projectDeliveryScope.js +20 -0
  68. package/dist/task/publicationToolObservation.js +126 -0
  69. package/dist/task/remoteDelivery.js +19 -8
  70. package/dist/task/remoteDeliveryService.js +5 -4
  71. package/dist/task/taskActivationService.js +35 -0
  72. package/dist/web/assets/client/app/settings.js +7 -1
  73. package/dist/web/assets/client/i18n/en.js +65 -2
  74. package/dist/web/assets/client/i18n/zh-CN.js +65 -2
  75. package/dist/web/assets/client/index.js +2 -0
  76. package/dist/web/assets/client/views/jobs.js +222 -0
  77. package/dist/web/assets/client/views/task/deliveryResults.js +6 -2
  78. package/dist/web/assets/client/views/task/page.js +11 -1
  79. package/dist/web/assets/shared/manageNav.js +1 -0
  80. package/dist/web/assets/shared/taskTabs.js +2 -2
  81. package/dist/web/assets/shell/settingsContent.js +3 -0
  82. package/dist/web/assets/styles/index.js +2 -0
  83. package/dist/web/assets/styles/views/jobs.js +19 -0
  84. package/dist/web/controllerWeb.js +73 -21
  85. package/dist/web/taskPreviews.js +21 -13
  86. package/dist/web/webAccess.js +109 -0
  87. package/dist/web/webJobs.js +52 -0
  88. package/dist/web/webServer.js +76 -44
  89. package/docs/architecture/capabilities-and-resources.md +6 -4
  90. package/docs/architecture/capabilities-and-resources.zh-CN.md +5 -4
  91. package/docs/cli-information-contract.md +57 -4
  92. package/docs/cli-information-contract.zh-CN.md +41 -2
  93. package/docs/job-runs.md +224 -0
  94. package/docs/project-delivery-scope.md +59 -0
  95. package/docs/release-workflow.md +86 -0
  96. package/docs/release-workflow.zh-CN.md +68 -0
  97. package/docs/schedules.md +132 -0
  98. package/docs/storage-baseline.md +31 -1
  99. package/docs/storage-baseline.zh-CN.md +25 -1
  100. package/docs/task-delivery.md +74 -2
  101. package/docs/task-delivery.zh-CN.md +39 -1
  102. package/docs/testing/verification-levels.md +6 -2
  103. package/docs/testing/verification-levels.zh-CN.md +6 -3
  104. package/i18n/README.zh-CN.md +6 -3
  105. package/package.json +4 -3
  106. package/skills/yui-leader/SKILL.md +44 -1
  107. package/skills/yui-leader/VERSION +1 -1
  108. package/skills/yui-leader/references/planning.md +9 -1
  109. package/skills/yui-operator/SKILL.md +31 -0
  110. package/skills/yui-operator/VERSION +1 -1
  111. package/skills/yui-reviewer/SKILL.md +4 -0
  112. package/skills/yui-reviewer/VERSION +1 -1
  113. package/skills/yui-runtime/SKILL.md +67 -8
  114. package/skills/yui-runtime/VERSION +1 -1
  115. package/skills/yui-runtime/references/context-ordering.md +103 -0
  116. package/skills/yui-runtime/references/publication.md +44 -4
  117. package/skills/yui-runtime/references/recovery.md +21 -0
  118. package/skills/yui-worker/SKILL.md +5 -0
  119. package/skills/yui-worker/VERSION +1 -1
@@ -0,0 +1,58 @@
1
+ import { createHash } from "node:crypto";
2
+ import { CliError, usageError } from "../errors/cliError.js";
3
+ import { assertProjectActive } from "./project.js";
4
+ import { GitWorkspaceRefreshError, NodeGitWorkspace } from "./gitWorkspace.js";
5
+ import { acquireProjectMaintenanceLock } from "./projectMaintenanceLock.js";
6
+ /** Bind maintenance intent to catalog identity, not a caller-supplied path. */
7
+ export function projectRefreshTargetDigest(project) {
8
+ return createHash("sha256").update(JSON.stringify([
9
+ project.id, project.path, project.remoteUrl, project.stableBranch,
10
+ project.developmentBranch, project.ownership, project.status
11
+ ])).digest("hex");
12
+ }
13
+ export function requireRefreshProject(store, id, automatic = false) {
14
+ const project = store.listProjects().find(project => project.id === id);
15
+ if (!project)
16
+ throw usageError(`Project not found: ${id}.`);
17
+ assertProjectActive(project, "refresh");
18
+ if (automatic && project.ownership !== "managed") {
19
+ throw usageError(`Automatic Project refresh requires a managed Project: ${id}.`);
20
+ }
21
+ if (project.remoteUrl === undefined)
22
+ throw usageError(`Project refresh requires a remote URL: ${id}.`);
23
+ if (project.stableBranch !== project.developmentBranch) {
24
+ throw usageError(`Project refresh requires matching stable and development branches: ${id}.`);
25
+ }
26
+ return project;
27
+ }
28
+ /** Shared manual/background operation. Waiting never authorizes a changed target. */
29
+ export async function refreshRegisteredProject(store, id, options = {}) {
30
+ const project = requireRefreshProject(store, id, options.automatic);
31
+ const expected = options.expectedDigest ?? projectRefreshTargetDigest(project);
32
+ const check = () => {
33
+ const current = requireRefreshProject(store, id, options.automatic);
34
+ if (projectRefreshTargetDigest(current) !== expected) {
35
+ throw new Error(`Project changed while waiting to refresh: ${id}; read current state before retrying.`);
36
+ }
37
+ return current;
38
+ };
39
+ check();
40
+ const release = await acquireProjectMaintenanceLock(store.rootDirectory(), id, { signal: options.signal });
41
+ try {
42
+ const current = check();
43
+ options.beforeRefresh?.();
44
+ const refreshed = await (options.git ?? new NodeGitWorkspace()).refresh({
45
+ repositoryPath: current.path, remoteUrl: current.remoteUrl, stableRef: current.stableBranch
46
+ });
47
+ return { project: current, ...refreshed };
48
+ }
49
+ catch (error) {
50
+ if (error instanceof GitWorkspaceRefreshError) {
51
+ throw new CliError("RUNTIME_ERROR", error.message, undefined, { projectId: id, refresh: error.result });
52
+ }
53
+ throw error;
54
+ }
55
+ finally {
56
+ release();
57
+ }
58
+ }
@@ -2,6 +2,7 @@ import { lstat, mkdir, readdir, readlink, rmdir, symlink, unlink } from "node:fs
2
2
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { isDeepStrictEqual } from "node:util";
4
4
  import { recordArchiveCleanup } from "../task/archiveDiagnostics.js";
5
+ import { activationWakeRefs } from "../context/taskPhase.js";
5
6
  import { enqueueWork } from "../coordination/workMailboxQueue.js";
6
7
  import { createTaskEvent } from "../event/taskEvent.js";
7
8
  import { retireTaskRoleSessionsForWorkspace, updateRoleAgentSessionStatus } from "../executor/agentExecutor.js";
@@ -2282,7 +2283,7 @@ export class FileTaskWorkspacePreparer {
2282
2283
  function recordTaskActivation(store, previous, active, now, actor) {
2283
2284
  // Activation is the explicit request to enter delivery, not an ordinary
2284
2285
  // Leader-local edit. Preserve that transition for the replacement Session.
2285
- enqueueWork(store, { kind: "role", taskId: active.id, roleName: LEADER_ROLE }, "execution-started", now, [{ type: "task", id: active.id }]);
2286
+ enqueueWork(store, { kind: "role", taskId: active.id, roleName: LEADER_ROLE }, "execution-started", now, [{ type: "task", id: active.id }, ...activationWakeRefs(store, active)]);
2286
2287
  enqueueWork(store, { kind: "task", taskId: active.id }, "task-activated", now, [{ type: "task", id: active.id }]);
2287
2288
  store.saveEvent(active.id, createTaskEvent(store.nextEventId(active.id), active.id, "task.activated", { fromStatus: previous.status, status: active.status, by: actor }, now));
2288
2289
  }
@@ -157,7 +157,12 @@ export async function inspectWorkspaceCleanup(store, git, workspace, disposition
157
157
  const observed = (await git.inspect(entry.path, "HEAD")).baseCommit;
158
158
  const baseline = task.projectBindings.find(binding => binding.projectId === entry.projectId)?.baseCommit;
159
159
  const unchangedAbandonment = disposition === "abandoned" && baseline !== undefined && observed === baseline;
160
- if (!unchangedAbandonment && disposition === "abandoned" && (!covered?.merged || !covered.verified)) {
160
+ if (covered?.deliveryRequirement === "excluded"
161
+ && (baseline === undefined || observed !== baseline)
162
+ && (!covered.merged || !covered.verified)) {
163
+ check("excluded-project-retained", "Excluding delivery does not authorize discarding unique or unverified local commits.", { baseline: baseline ?? null, verifiedDelivery: true }, observed);
164
+ }
165
+ else if (!unchangedAbandonment && disposition === "abandoned" && (!covered?.merged || !covered.verified)) {
161
166
  check("delivery-coverage", "Abandonment does not discard unverified local commits; only the exact unchanged baseline is disposable.", { baseline: baseline ?? null, verifiedDelivery: true }, observed);
162
167
  }
163
168
  else if (!unchangedAbandonment && expected != null && expected !== observed && covered?.deliveryLocalCommit !== observed) {
@@ -0,0 +1,108 @@
1
+ import { CronExpressionParser } from "cron-parser";
2
+ import { requireKnownFields, requirePositiveInteger, requireTimestamp, requireText } from "../domain/validation.js";
3
+ import { jobScopeKey } from "../job/jobDefinition.js";
4
+ export function scheduleId(id) {
5
+ if (typeof id !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/u.test(id)) {
6
+ throw new Error("An explicit safe Schedule/Job id is required.");
7
+ }
8
+ }
9
+ function instant(value) {
10
+ requireTimestamp(value, "Schedule instant");
11
+ if (!/(?:Z|[+-]\d{2}:\d{2})$/u.test(value))
12
+ throw new Error("Schedule instants require an explicit UTC offset.");
13
+ }
14
+ export function validateScheduleRule(rule) {
15
+ if (rule?.kind === "once") {
16
+ requireKnownFields(rule, ["kind", "at"], "Once rule");
17
+ instant(rule.at);
18
+ }
19
+ else if (rule?.kind === "interval") {
20
+ requireKnownFields(rule, ["kind", "anchor", "everyMs"], "Interval rule");
21
+ instant(rule.anchor);
22
+ if (!Number.isSafeInteger(rule.everyMs) || rule.everyMs < 1000)
23
+ throw new Error("Interval everyMs must be an integer of at least 1000.");
24
+ }
25
+ else if (rule?.kind === "cron") {
26
+ requireKnownFields(rule, ["kind", "expression", "timezone"], "Cron rule");
27
+ if (typeof rule.timezone !== "string" || !rule.timezone.trim())
28
+ throw new Error("An explicit IANA timezone is required.");
29
+ new Intl.DateTimeFormat("en", { timeZone: rule.timezone });
30
+ if (typeof rule.expression !== "string" || rule.expression.trim().split(/\s+/u).length !== 5
31
+ || /\bH\b/iu.test(rule.expression))
32
+ throw new Error("Cron requires five fields and does not support random H.");
33
+ CronExpressionParser.parse(rule.expression, { tz: rule.timezone, currentDate: "2026-01-01T00:00:00Z" });
34
+ }
35
+ else
36
+ throw new Error("Unknown Schedule rule.");
37
+ return rule;
38
+ }
39
+ /** Strictly after the fence; once returns null after its single occurrence. */
40
+ export function nextScheduleDue(rule, after) {
41
+ validateScheduleRule(rule);
42
+ const at = after.getTime();
43
+ if (rule.kind === "once")
44
+ return Date.parse(rule.at) > at ? new Date(rule.at).toISOString() : null;
45
+ if (rule.kind === "interval") {
46
+ const anchor = Date.parse(rule.anchor);
47
+ return new Date(anchor + Math.max(0, Math.floor((at - anchor) / rule.everyMs) + 1) * rule.everyMs).toISOString();
48
+ }
49
+ return CronExpressionParser.parse(rule.expression, { currentDate: after, tz: rule.timezone }).next().toISOString();
50
+ }
51
+ export function validateSchedule(value) {
52
+ requireKnownFields(value, ["schemaVersion", "id", "scope", "jobId", "rule", "revision", "generation",
53
+ "state", "nextDue", "processedThrough", "authorization", "lastAttempt", "reason", "createdAt", "updatedAt"], "Schedule");
54
+ if (value.schemaVersion !== 1)
55
+ throw new Error("Invalid Schedule schema.");
56
+ scheduleId(value.id);
57
+ scheduleId(value.jobId);
58
+ if (!["task", "global"].includes(value.scope.kind))
59
+ throw new Error("Invalid Schedule scope.");
60
+ requireKnownFields(value.scope, value.scope.kind === "task" ? ["kind", "taskId"] : ["kind"], "Schedule scope");
61
+ jobScopeKey(value.scope);
62
+ validateScheduleRule(value.rule);
63
+ requirePositiveInteger(value.revision, "Schedule revision");
64
+ requirePositiveInteger(value.generation, "Schedule generation");
65
+ if (!["enabled", "disabled", "completed", "needs-attention", "retired"].includes(value.state))
66
+ throw new Error("Invalid Schedule state.");
67
+ if (value.state === "enabled" && value.nextDue === null)
68
+ throw new Error("Enabled Schedule requires nextDue.");
69
+ if (value.state === "completed" && (value.rule.kind !== "once" || value.nextDue !== null))
70
+ throw new Error("Only an admitted once Schedule is completed.");
71
+ if (value.nextDue !== null)
72
+ instant(value.nextDue);
73
+ if (value.processedThrough !== undefined)
74
+ instant(value.processedThrough);
75
+ instant(value.createdAt);
76
+ instant(value.updatedAt);
77
+ requireKnownFields(value.authorization, ["caller", "binding", "jobRevision"], "Schedule authorization");
78
+ requirePositiveInteger(value.authorization.jobRevision, "Schedule Job revision");
79
+ if (typeof value.authorization.binding !== "string" || !/^[a-f0-9]{64}$/u.test(value.authorization.binding))
80
+ throw new Error("Invalid Schedule authority binding.");
81
+ const caller = value.authorization.caller;
82
+ requireKnownFields(caller, ["scope", "taskId", "role", "agentId", "adapterId", "nativeSessionId", "runId"], "Schedule caller");
83
+ if (typeof caller.role !== "string" || typeof caller.nativeSessionId !== "string")
84
+ throw new Error("Schedule authority requires a Role and Session.");
85
+ requireText(caller.role, "Schedule caller role");
86
+ requireText(caller.nativeSessionId, "Schedule caller Session");
87
+ if (caller.scope === "global") {
88
+ if (caller.role !== "operator")
89
+ throw new Error("Global Schedule authority requires Operator.");
90
+ }
91
+ else if (caller.scope !== "task" || value.scope.kind !== "task" || caller.taskId !== value.scope.taskId) {
92
+ throw new Error("Schedule caller must belong to its scope.");
93
+ }
94
+ if (value.reason !== undefined)
95
+ requireText(value.reason, "Schedule reason");
96
+ if (value.lastAttempt !== undefined) {
97
+ requireKnownFields(value.lastAttempt, ["at", "dueAt", "outcome", "runId", "reason"], "Schedule attempt");
98
+ instant(value.lastAttempt.at);
99
+ instant(value.lastAttempt.dueAt);
100
+ if (!["admitted", "overlap", "blocked"].includes(value.lastAttempt.outcome))
101
+ throw new Error("Invalid Schedule attempt.");
102
+ if (value.lastAttempt.runId !== undefined)
103
+ requireText(value.lastAttempt.runId, "Schedule Run id");
104
+ if (value.lastAttempt.reason !== undefined)
105
+ requireText(value.lastAttempt.reason, "Schedule attempt reason");
106
+ }
107
+ return value;
108
+ }
@@ -16,6 +16,8 @@ export function claimOperatorAttention(mailbox, options) {
16
16
  || pending.toSequence - pending.fromSequence + 1 !== pending.requestCount
17
17
  || pending.dedupeKeys.some((key, index) => {
18
18
  const ref = pending.refs[index];
19
+ if (ref.type === "global-job-run")
20
+ return key !== `operator-global-job-run:${ref.id}`;
19
21
  return (ref.type !== "event" && ref.type !== "input")
20
22
  || key !== `operator-${ref.type}:${ref.taskId}:${ref.id}`
21
23
  && key !== `sequence:${pending.fromSequence + index}`;
@@ -20,6 +20,10 @@ export async function processOperatorInputNotifications(store, selection, now =
20
20
  return [];
21
21
  const processing = claim.processing;
22
22
  const attentions = deduplicateAttention(processing.batch.refs.flatMap((ref) => {
23
+ if (ref.type === "global-job-run") {
24
+ const run = store.getGlobalJobRun?.(ref.id);
25
+ return run == null ? [] : [{ kind: "global-job-run", run }];
26
+ }
23
27
  if (ref.type === "input") {
24
28
  const request = store.getInputRequest(ref.taskId, ref.id);
25
29
  return request !== null && request.status === "open"
@@ -70,6 +74,8 @@ function skipped(attention, reason) {
70
74
  };
71
75
  }
72
76
  function attentionIdentity(attention) {
77
+ if (attention.kind === "global-job-run")
78
+ return { sourceKind: "global-job-run", sourceId: attention.run.id };
73
79
  return attention.kind === "input"
74
80
  ? {
75
81
  sourceKind: "input",
@@ -83,6 +89,8 @@ function attentionIdentity(attention) {
83
89
  };
84
90
  }
85
91
  function toPresentationItem(attention) {
92
+ if (attention.kind === "global-job-run")
93
+ return attention;
86
94
  return attention.kind === "input"
87
95
  ? { kind: "input-request", request: attention.request }
88
96
  : { kind: "task-event", event: attention.event };
@@ -98,6 +98,14 @@ export function queryContextRecords(db, taskId, query) {
98
98
  const sequence = `CAST(substr(r.${key}, length(r.${key}) - length(ltrim(r.${key}, 'abcdefghijklmnopqrstuvwxyz-')) + 1) AS INTEGER)`;
99
99
  // Task-local numeric IDs are the stable historical order, not lexical 1,10,2.
100
100
  if (query.family === "task-event") {
101
+ if (query.eventRequestId !== undefined) {
102
+ where.push("json_extract(r.payload, '$.payload.requestId') = @requestId");
103
+ params.requestId = query.eventRequestId;
104
+ }
105
+ if (query.eventMessageId !== undefined) {
106
+ where.push("json_extract(r.payload, '$.payload.messageId') = @messageId");
107
+ params.messageId = query.eventMessageId;
108
+ }
101
109
  if (query.eventTypes !== undefined) {
102
110
  where.push("r.type IN (SELECT value FROM json_each(@types))");
103
111
  params.types = JSON.stringify(query.eventTypes);
@@ -0,0 +1,15 @@
1
+ /** Additive DDL for storage 1.9; the published baseline is unchanged. */
2
+ export const JOB_SCHEMA_SQL = `
3
+ CREATE TABLE job_definitions (
4
+ scope_key TEXT NOT NULL,
5
+ definition_id TEXT NOT NULL,
6
+ payload TEXT NOT NULL,
7
+ PRIMARY KEY (scope_key, definition_id)
8
+ );
9
+ CREATE TABLE global_job_runs (
10
+ run_id TEXT PRIMARY KEY,
11
+ payload TEXT NOT NULL,
12
+ status TEXT NOT NULL
13
+ );
14
+ CREATE INDEX idx_global_job_runs_status ON global_job_runs(status);
15
+ `;
@@ -22,7 +22,7 @@ export function maintenanceCallerEnvironment(home, env, operatorOnly = true) {
22
22
  if (storageMinorUpgradePlan(schema.currentVersion) === null) {
23
23
  throw new ManagedRuntimeDriftError("Maintenance cannot authenticate an unsupported Home storage version.");
24
24
  }
25
- validateSqliteSchemaObjects(db);
25
+ validateSqliteSchemaObjects(db, schema.currentVersion);
26
26
  const read = (table, where, params) => {
27
27
  const row = db.prepare(`SELECT payload FROM ${table} WHERE ${where}`).get(...params);
28
28
  if (!row)
@@ -8,6 +8,8 @@ import { validateInputRequest } from "../input/inputRequest.js";
8
8
  import { validateChangeSet } from "../integration/changeSet.js";
9
9
  import { validateIntegrationAttempt } from "../integration/integrationAttempt.js";
10
10
  import { validateDurableJob } from "../job/durableJob.js";
11
+ import { validateJobDefinition } from "../job/jobDefinition.js";
12
+ import { validateSchedule } from "../schedule/schedule.js";
11
13
  import { validateGlobalRoleMessage, validateTaskMessage } from "../message/message.js";
12
14
  import { validateMilestone } from "../milestone/milestone.js";
13
15
  import { validatePluginIntent } from "../plugins/pluginIntent.js";
@@ -54,6 +56,9 @@ const validators = {
54
56
  change_sets: check(validateChangeSet),
55
57
  integration_attempts: check(validateIntegrationAttempt),
56
58
  durable_jobs: check(validateDurableJob),
59
+ job_definitions: check(validateJobDefinition),
60
+ schedules: check(validateSchedule),
61
+ global_job_runs: check(validateDurableJob),
57
62
  context_snapshots: check(validateContextSnapshot),
58
63
  managed_workspaces: check(validateManagedWorkspace),
59
64
  input_requests: check(validateInputRequest),
@@ -0,0 +1,12 @@
1
+ /** Additive storage 1.9 -> 1.10. No old records are rewritten. */
2
+ export const SCHEDULE_SCHEMA_SQL = `
3
+ CREATE TABLE schedules (
4
+ scope_key TEXT NOT NULL,
5
+ schedule_id TEXT NOT NULL,
6
+ payload TEXT NOT NULL,
7
+ state TEXT NOT NULL,
8
+ next_due TEXT,
9
+ PRIMARY KEY(scope_key, schedule_id)
10
+ );
11
+ CREATE INDEX idx_schedules_due ON schedules(state, next_due);
12
+ `;
@@ -1,13 +1,20 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import Database from "../sqlite/driver.js";
3
- import { BASELINE_SCHEMA_SQL, SQLITE_SCHEMA_TABLES } from "./baselineSchema.js";
3
+ import { BASELINE_SCHEMA_SQL, SQLITE_SCHEMA_TABLES as BASELINE_SCHEMA_TABLES } from "./baselineSchema.js";
4
4
  import { migrateTaskMainWorkspaces } from "./upgrade/taskMainWorkspaceMigration.js";
5
5
  import { migrateProjectKnowledge } from "./upgrade/projectKnowledgeMigration.js";
6
+ import { JOB_SCHEMA_SQL } from "./jobSchema.js";
7
+ import { SCHEDULE_SCHEMA_SQL } from "./scheduleSchema.js";
8
+ import { migrateJobDefinitions } from "./upgrade/jobDefinitionMigration.js";
6
9
  import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION, STORAGE_FORMAT, isMinorStorageUpgrade, isStorageVersion, storageVersionParts } from "./storageVersions.js";
7
- export { BASELINE_SCHEMA_SQL, SQLITE_SCHEMA_TABLES };
10
+ export { BASELINE_SCHEMA_SQL };
11
+ export const SQLITE_SCHEMA_TABLES = [...BASELINE_SCHEMA_TABLES, "job_definitions", "global_job_runs", "schedules"];
8
12
  export const TELEMETRY_KEEP_PER_RUN = 200;
9
13
  export const TELEMETRY_RUN_CAP = 50_000;
10
- export const CURRENT_SCHEMA_CHECKSUM = createHash("sha256").update(BASELINE_SCHEMA_SQL).digest("hex");
14
+ export const BASELINE_SCHEMA_CHECKSUM = createHash("sha256").update(BASELINE_SCHEMA_SQL).digest("hex");
15
+ export const JOB_SCHEMA_CHECKSUM = createHash("sha256").update(BASELINE_SCHEMA_SQL + JOB_SCHEMA_SQL).digest("hex");
16
+ export const CURRENT_SCHEMA_SQL = BASELINE_SCHEMA_SQL + JOB_SCHEMA_SQL + SCHEDULE_SCHEMA_SQL;
17
+ export const CURRENT_SCHEMA_CHECKSUM = createHash("sha256").update(CURRENT_SCHEMA_SQL).digest("hex");
11
18
  export class SqliteSchemaError extends Error {
12
19
  code;
13
20
  constructor(detail, code = "STORAGE_SCHEMA_INVALID") {
@@ -21,8 +28,8 @@ const MINOR_UPGRADES = Object.freeze([{
21
28
  toVersion: "1.1",
22
29
  name: "task-main-workspace",
23
30
  introducedIn: "1.1.0",
24
- sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
25
- targetChecksum: CURRENT_SCHEMA_CHECKSUM,
31
+ sourceChecksum: BASELINE_SCHEMA_CHECKSUM,
32
+ targetChecksum: BASELINE_SCHEMA_CHECKSUM,
26
33
  sql: "",
27
34
  dataMigration: "task-main-workspace"
28
35
  }, {
@@ -30,8 +37,8 @@ const MINOR_UPGRADES = Object.freeze([{
30
37
  toVersion: "1.2",
31
38
  name: "task-authorization-source",
32
39
  introducedIn: "next",
33
- sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
34
- targetChecksum: CURRENT_SCHEMA_CHECKSUM,
40
+ sourceChecksum: BASELINE_SCHEMA_CHECKSUM,
41
+ targetChecksum: BASELINE_SCHEMA_CHECKSUM,
35
42
  // New grants can carry source evidence. Valid historical Operator grants
36
43
  // remain unchanged; migration must not invent authorization provenance.
37
44
  sql: ""
@@ -40,8 +47,8 @@ const MINOR_UPGRADES = Object.freeze([{
40
47
  toVersion: "1.3",
41
48
  name: "selected-session-input",
42
49
  introducedIn: "next",
43
- sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
44
- targetChecksum: CURRENT_SCHEMA_CHECKSUM,
50
+ sourceChecksum: BASELINE_SCHEMA_CHECKSUM,
51
+ targetChecksum: BASELINE_SCHEMA_CHECKSUM,
45
52
  // Optional explicit Session fences apply only to new inputs. Preserve old
46
53
  // Role-addressed messages; never infer a historical user's Session choice.
47
54
  sql: ""
@@ -50,8 +57,8 @@ const MINOR_UPGRADES = Object.freeze([{
50
57
  toVersion: "1.4",
51
58
  name: "project-knowledge-applicability",
52
59
  introducedIn: "next",
53
- sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
54
- targetChecksum: CURRENT_SCHEMA_CHECKSUM,
60
+ sourceChecksum: BASELINE_SCHEMA_CHECKSUM,
61
+ targetChecksum: BASELINE_SCHEMA_CHECKSUM,
55
62
  sql: "",
56
63
  dataMigration: "project-knowledge"
57
64
  }, {
@@ -59,8 +66,8 @@ const MINOR_UPGRADES = Object.freeze([{
59
66
  toVersion: "1.5",
60
67
  name: "frozen-skill-packages",
61
68
  introducedIn: "next",
62
- sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
63
- targetChecksum: CURRENT_SCHEMA_CHECKSUM,
69
+ sourceChecksum: BASELINE_SCHEMA_CHECKSUM,
70
+ targetChecksum: BASELINE_SCHEMA_CHECKSUM,
64
71
  // Additive effective-launch/manifest package evidence. Historical envelopes
65
72
  // and their digests remain unchanged: absence explicitly means entrypoint-only
66
73
  // provenance, not an invitation to invent packages from today's source files.
@@ -72,8 +79,8 @@ const MINOR_UPGRADES = Object.freeze([{
72
79
  toVersion: "1.6",
73
80
  name: "runtime-cost-evidence",
74
81
  introducedIn: "next",
75
- sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
76
- targetChecksum: CURRENT_SCHEMA_CHECKSUM,
82
+ sourceChecksum: BASELINE_SCHEMA_CHECKSUM,
83
+ targetChecksum: BASELINE_SCHEMA_CHECKSUM,
77
84
  // Optional sourced monetary observations; historical missing costs stay unknown.
78
85
  sql: ""
79
86
  }, {
@@ -81,8 +88,8 @@ const MINOR_UPGRADES = Object.freeze([{
81
88
  toVersion: "1.7",
82
89
  name: "agent-inbox-consumption",
83
90
  introducedIn: "next",
84
- sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
85
- targetChecksum: CURRENT_SCHEMA_CHECKSUM,
91
+ sourceChecksum: BASELINE_SCHEMA_CHECKSUM,
92
+ targetChecksum: BASELINE_SCHEMA_CHECKSUM,
86
93
  // Additive handling receipts on canonical Messages and wakes. Never invent
87
94
  // historical processing or Provider acceptance; existing records stay intact.
88
95
  sql: ""
@@ -91,11 +98,62 @@ const MINOR_UPGRADES = Object.freeze([{
91
98
  toVersion: "1.8",
92
99
  name: "model-management-connection",
93
100
  introducedIn: "next",
94
- sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
95
- targetChecksum: CURRENT_SCHEMA_CHECKSUM,
101
+ sourceChecksum: BASELINE_SCHEMA_CHECKSUM,
102
+ targetChecksum: BASELINE_SCHEMA_CHECKSUM,
96
103
  // Optional non-secret connection references. Existing Homes remain unconfigured;
97
104
  // never infer local credentials or adopt an existing external service.
98
105
  sql: ""
106
+ }, {
107
+ fromVersion: "1.8",
108
+ toVersion: "1.9",
109
+ name: "job-definitions-and-runs",
110
+ introducedIn: "next",
111
+ sourceChecksum: BASELINE_SCHEMA_CHECKSUM,
112
+ targetChecksum: JOB_SCHEMA_CHECKSUM,
113
+ sql: JOB_SCHEMA_SQL,
114
+ dataMigration: "job-definitions"
115
+ }, {
116
+ fromVersion: "1.9",
117
+ toVersion: "1.10",
118
+ name: "persistent-schedules",
119
+ introducedIn: "next",
120
+ sourceChecksum: JOB_SCHEMA_CHECKSUM,
121
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
122
+ sql: SCHEDULE_SCHEMA_SQL
123
+ }, {
124
+ fromVersion: "1.10",
125
+ toVersion: "1.11",
126
+ name: "activation-input-provenance",
127
+ introducedIn: "next",
128
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
129
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
130
+ // Activation event payloads may now name an exact input or contain attributed
131
+ // native input text. Existing events remain immutable; missing old provenance
132
+ // stays explicitly missing. No historical source is guessed from timestamps.
133
+ // New Task Job semantic writes also append job.state-changed events; no old
134
+ // Job history is backfilled and the SQL layout remains unchanged.
135
+ sql: ""
136
+ }, {
137
+ fromVersion: "1.11",
138
+ toVersion: "1.12",
139
+ name: "project-delivery-scope",
140
+ introducedIn: "next",
141
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
142
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
143
+ // New append-only Project scope decisions use Task events. Preserve all old
144
+ // completion, binding and publication records; absence still means required.
145
+ sql: ""
146
+ }, {
147
+ fromVersion: "1.12",
148
+ toVersion: "1.13",
149
+ name: "inbox-turn-handling",
150
+ introducedIn: "next",
151
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
152
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
153
+ // Claims may name their exact current attempt; explicit Global retirement
154
+ // and terminal-check notices preserve original records. Never backfill
155
+ // historical turn ownership, handling evidence or acknowledgements.
156
+ sql: ""
99
157
  }]);
100
158
  export function storageMinorUpgradePlan(from) {
101
159
  if (from === CURRENT_STORAGE_VERSION)
@@ -118,13 +176,24 @@ function objects(db) {
118
176
  return db.prepare(`SELECT type,name,sql FROM sqlite_master
119
177
  WHERE name NOT GLOB 'sqlite_*' ORDER BY type,name`).all();
120
178
  }
121
- let expectedObjects;
122
- export function validateSqliteSchemaObjects(db) {
179
+ const schemaObjectsByChecksum = new Map();
180
+ export function validateSqliteSchemaObjects(db, version = CURRENT_STORAGE_VERSION) {
181
+ // Maintenance authenticates before migration. Validate the declared source
182
+ // layout, not current-only tables that the migration has yet to create.
183
+ const checksum = version === CURRENT_STORAGE_VERSION
184
+ ? CURRENT_SCHEMA_CHECKSUM : storageMinorUpgradePlan(version)?.[0]?.sourceChecksum;
185
+ const sql = checksum === CURRENT_SCHEMA_CHECKSUM ? CURRENT_SCHEMA_SQL
186
+ : checksum === JOB_SCHEMA_CHECKSUM ? BASELINE_SCHEMA_SQL + JOB_SCHEMA_SQL
187
+ : checksum === BASELINE_SCHEMA_CHECKSUM ? BASELINE_SCHEMA_SQL : undefined;
188
+ if (sql === undefined || checksum === undefined)
189
+ throw new SqliteSchemaError(`unsupported schema definition for storage ${version}.`);
190
+ let expectedObjects = schemaObjectsByChecksum.get(checksum);
123
191
  if (expectedObjects === undefined) {
124
192
  const reference = new Database(":memory:");
125
193
  try {
126
- reference.exec(BASELINE_SCHEMA_SQL);
194
+ reference.exec(sql);
127
195
  expectedObjects = objects(reference);
196
+ schemaObjectsByChecksum.set(checksum, expectedObjects);
128
197
  }
129
198
  finally {
130
199
  reference.close();
@@ -134,7 +203,7 @@ export function validateSqliteSchemaObjects(db) {
134
203
  for (const expected of expectedObjects) {
135
204
  const found = actual.find(object => object.type === expected.type && object.name === expected.name);
136
205
  if (found?.sql !== expected.sql)
137
- throw new SqliteSchemaError(`required ${expected.type} '${expected.name}' differs from the current definition.`);
206
+ throw new SqliteSchemaError(`required ${expected.type} '${expected.name}' differs from the storage ${version} definition.`);
138
207
  }
139
208
  if (actual.length !== expectedObjects.length)
140
209
  throw new SqliteSchemaError("unexpected schema objects; preserve the database for diagnosis.");
@@ -183,7 +252,7 @@ export function initializeSqliteSchema(db) {
183
252
  }
184
253
  const { major, minor } = storageVersionParts(CURRENT_STORAGE_VERSION);
185
254
  return db.transaction(() => {
186
- db.exec(BASELINE_SCHEMA_SQL);
255
+ db.exec(CURRENT_SCHEMA_SQL);
187
256
  db.prepare("INSERT INTO storage_schema VALUES (1,?,?,?,?,?)")
188
257
  .run(STORAGE_FORMAT, major, minor, CURRENT_SCHEMA_CHECKSUM, new Date().toISOString());
189
258
  return validateSqliteSchema(db);
@@ -207,6 +276,8 @@ export function applySqliteMinorUpgrades(db, home, createdRoots = []) {
207
276
  }
208
277
  if (step.dataMigration === "project-knowledge")
209
278
  migrateProjectKnowledge(db);
279
+ if (step.dataMigration === "job-definitions")
280
+ migrateJobDefinitions(db);
210
281
  const { major, minor } = storageVersionParts(step.toVersion);
211
282
  db.prepare("UPDATE storage_schema SET major=?,minor=?,checksum=? WHERE id=1")
212
283
  .run(major, minor, step.targetChecksum);