@zq-silk/yui 2.11.0 → 2.12.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 (79) hide show
  1. package/dist/artifacts/artifactCapability.js +14 -8
  2. package/dist/artifacts/artifactCommitLock.js +7 -5
  3. package/dist/artifacts/jobArtifact.js +93 -0
  4. package/dist/artifacts/jobArtifactTransport.js +13 -0
  5. package/dist/artifacts/taskArtifactRepository.js +58 -10
  6. package/dist/cli/commandCatalog.js +45 -14
  7. package/dist/cli/commandDiscovery.js +1 -1
  8. package/dist/commands/durableJobCommands.js +31 -6
  9. package/dist/commands/globalRoleCommands.js +5 -0
  10. package/dist/commands/scheduleCommands.js +14 -3
  11. package/dist/commands/taskCommands.js +5 -2
  12. package/dist/context/taskCatalog.js +6 -1
  13. package/dist/controlPlaneCli.js +7 -2
  14. package/dist/controller/agentJobControl.js +391 -0
  15. package/dist/controller/controller.js +24 -3
  16. package/dist/controller/fileSchedulerStoreAdapter.js +12 -1
  17. package/dist/controller/globalInputDelivery.js +8 -1
  18. package/dist/controller/jobArtifactControl.js +111 -0
  19. package/dist/controller/jobControl.js +4 -0
  20. package/dist/controller/jobDefinitionControl.js +191 -19
  21. package/dist/controller/jobSupervisor.js +83 -3
  22. package/dist/controller/jobTaskDelegationControl.js +193 -0
  23. package/dist/controller/runtime.js +7 -1
  24. package/dist/controller/scheduleControl.js +83 -20
  25. package/dist/integration/gitIntegrationService.js +2 -0
  26. package/dist/job/agentJob.js +131 -0
  27. package/dist/job/agentJobObservation.js +61 -0
  28. package/dist/job/agentRoleScope.js +33 -0
  29. package/dist/job/durableJob.js +63 -28
  30. package/dist/job/jobAssignmentScope.js +1 -0
  31. package/dist/job/jobBusinessResult.js +72 -0
  32. package/dist/job/jobCleanupInspection.js +4 -0
  33. package/dist/job/jobDefinition.js +26 -2
  34. package/dist/job/jobRunner.js +4 -3
  35. package/dist/job/projectRefreshRunner.js +54 -18
  36. package/dist/job/projectSync.js +162 -0
  37. package/dist/job/scheduleService.js +55 -0
  38. package/dist/job/taskDelegation.js +48 -0
  39. package/dist/message/messageContinuation.js +1 -1
  40. package/dist/resources/projectResourceService.js +1 -1
  41. package/dist/resources/resourceGc.js +1 -1
  42. package/dist/role/role.js +12 -0
  43. package/dist/schedule/schedule.js +65 -15
  44. package/dist/scheduler/wakeReason.js +4 -0
  45. package/dist/storage/agentJobSchema.js +10 -0
  46. package/dist/storage/recordValidation.js +2 -0
  47. package/dist/storage/sqliteSchema.js +77 -6
  48. package/dist/storage/sqliteStore.js +53 -0
  49. package/dist/storage/storageVersions.js +1 -1
  50. package/dist/storage/storeRpc.js +3 -0
  51. package/dist/storage/taskCatalog.js +10 -2
  52. package/dist/storage/upgrade/jobBusinessResultMigration.js +20 -0
  53. package/dist/storage/upgrade/scheduleDispatchMigration.js +15 -0
  54. package/dist/task/archivePreflight.js +15 -0
  55. package/dist/task/completionReadiness.js +7 -0
  56. package/dist/task/task.js +17 -1
  57. package/dist/verification/verificationGateService.js +4 -0
  58. package/dist/web/assets/client/app/catalog.js +16 -0
  59. package/dist/web/assets/client/i18n/en.js +29 -2
  60. package/dist/web/assets/client/i18n/zh-CN.js +29 -2
  61. package/dist/web/assets/client/layout/manageShell.js +9 -0
  62. package/dist/web/assets/client/layout/router.js +1 -0
  63. package/dist/web/assets/client/views/jobs.js +166 -37
  64. package/dist/web/assets/client/views/task/files.js +5 -4
  65. package/dist/web/assets/client/views/task/page.js +5 -1
  66. package/dist/web/taskPreviews.js +2 -2
  67. package/dist/web/webJobs.js +36 -11
  68. package/dist/web/webMaterials.js +21 -9
  69. package/dist/web/webServer.js +6 -5
  70. package/dist/web/webTaskSurface.js +1 -0
  71. package/docs/job-runs.md +392 -17
  72. package/docs/release-workflow.md +42 -0
  73. package/docs/release-workflow.zh-CN.md +31 -0
  74. package/docs/schedules.md +69 -15
  75. package/docs/storage-baseline.md +38 -1
  76. package/docs/storage-baseline.zh-CN.md +27 -1
  77. package/docs/testing/verification-levels.md +2 -2
  78. package/docs/testing/verification-levels.zh-CN.md +2 -2
  79. package/package.json +1 -1
@@ -0,0 +1,162 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstatSync, realpathSync } from "node:fs";
3
+ import { isDeepStrictEqual } from "node:util";
4
+ import { join, resolve } from "node:path";
5
+ import { managedProjectPath } from "../repository/project.js";
6
+ import { requireRefreshProject, projectRefreshTargetDigest } from "../repository/projectRefresh.js";
7
+ import { createJobDefinition, reviseJobDefinition, validateJobDefinition } from "./jobDefinition.js";
8
+ import { nextScheduleDue, validateScheduleRule } from "../schedule/schedule.js";
9
+ import { scheduleServiceBinding } from "./scheduleService.js";
10
+ const scope = { kind: "global" };
11
+ const unavailable = "Project sync unavailable: ";
12
+ export const PROJECT_SYNC_INTERVAL_MS = 6 * 60 * 60_000;
13
+ /** Called under the maintenance lock immediately before Git, not a timer probe. */
14
+ export function requireProjectSyncCheckout(store, job) {
15
+ requireProjectSyncTarget(store, job);
16
+ const path = managedProjectPath(store.rootDirectory(), job.configuration.kind === "builtin" ? job.configuration.projectId : "");
17
+ if (realpathSync(path) !== path || !lstatSync(join(path, ".git")).isDirectory()
18
+ || lstatSync(join(path, ".git")).isSymbolicLink()) {
19
+ throw new Error("Builtin sync requires its own canonical checkout, not a linked or shared worktree.");
20
+ }
21
+ }
22
+ /** Current registry and maintenance source, not the builtin label, bound authority. */
23
+ export function requireProjectSyncTarget(store, job) {
24
+ if (job.builtin !== "project-sync" || job.configuration.kind !== "builtin")
25
+ throw new Error("Not a builtin Project sync.");
26
+ validateJobDefinition(job);
27
+ const project = requireRefreshProject(store, job.configuration.projectId, true);
28
+ if (resolve(project.path) !== managedProjectPath(store.rootDirectory(), project.id)) {
29
+ throw new Error("Project sync requires its canonical managed checkout.");
30
+ }
31
+ if (!isDeepStrictEqual(job.configuration.input, { targetDigest: projectRefreshTargetDigest(project) })) {
32
+ throw new Error("Project sync target changed; await registry reconciliation.");
33
+ }
34
+ const custom = store.listJobDefinitions(scope).find(other => !other.builtin && !other.instantiation
35
+ && other.configuration.kind === "builtin" && other.configuration.action === "project.refresh"
36
+ && other.configuration.projectId === project.id);
37
+ if (custom)
38
+ throw new Error(`Existing custom maintenance Job ${custom.id}; builtin sync does not take it over. Explicitly retarget that Job before enabling builtin sync.`);
39
+ }
40
+ function grant(store, plan, job, now) {
41
+ requireProjectSyncTarget(store, job);
42
+ const grantId = randomUUID();
43
+ return { kind: "service", service: "schedule", grantId, revoked: false, jobRevision: job.revision,
44
+ grantedBy: { scope: "service", service: "project-sync" }, grantedAt: now.toISOString(),
45
+ binding: scheduleServiceBinding(store, plan, job, grantId) };
46
+ }
47
+ /** Called by the existing Controller Schedule pass, with no timer or Git effects. */
48
+ export function reconcileProjectSync(store, now, onError = () => { }) {
49
+ const ids = new Set([...store.listProjects().map(p => p.id),
50
+ ...store.listSchedules(scope).flatMap(s => s.builtin ? [s.builtin.projectId] : [])]);
51
+ for (const projectId of ids) {
52
+ try {
53
+ store.transaction(tx => {
54
+ const id = `project-sync-${projectId}`;
55
+ const project = tx.listProjects().find(p => p.id === projectId);
56
+ let job = tx.getJobDefinition(scope, id);
57
+ let plan = tx.getSchedule(scope, id);
58
+ if ((job && job.builtin !== "project-sync") || (plan && !plan.builtin)) {
59
+ throw new Error(`Reserved builtin identity ${id} is occupied; existing records were not adopted.`);
60
+ }
61
+ if (!job && !project)
62
+ return;
63
+ let changed = false;
64
+ if (project) {
65
+ const configuration = { kind: "builtin", action: "project.refresh", projectId,
66
+ input: { targetDigest: projectRefreshTargetDigest(project) } };
67
+ if (!job) {
68
+ job = validateJobDefinition({ ...createJobDefinition({ id, scope, title: `Project sync · ${project.name}`, configuration }, now),
69
+ builtin: "project-sync" });
70
+ tx.saveJobDefinition(job);
71
+ changed = true;
72
+ }
73
+ else if (!isDeepStrictEqual(job.configuration, configuration)) {
74
+ job = reviseJobDefinition(job, { title: job.title, configuration }, now);
75
+ tx.saveJobDefinition(job);
76
+ changed = true;
77
+ }
78
+ }
79
+ if (!job)
80
+ throw new Error(`Builtin Project sync definition missing: ${id}.`);
81
+ let reason;
82
+ try {
83
+ requireProjectSyncTarget(tx, job);
84
+ }
85
+ catch (error) {
86
+ reason = unavailable + (error instanceof Error ? error.message : String(error));
87
+ }
88
+ if (!plan) {
89
+ const rule = { kind: "interval", anchor: new Date(now.getTime() + PROJECT_SYNC_INTERVAL_MS).toISOString(),
90
+ everyMs: PROJECT_SYNC_INTERVAL_MS };
91
+ const grantId = randomUUID();
92
+ plan = { schemaVersion: 1, id, scope, jobId: id, target: { kind: "existing-job" }, rule,
93
+ builtin: { projectId, enabled: true }, revision: 1, generation: 1,
94
+ state: reason ? "needs-attention" : "enabled", nextDue: nextScheduleDue(rule, now),
95
+ authorization: { kind: "service", service: "schedule", grantId, revoked: true, jobRevision: job.revision,
96
+ grantedBy: { scope: "service", service: "project-sync" }, grantedAt: now.toISOString(),
97
+ binding: scheduleServiceBinding(tx, { id, scope, jobId: id, target: { kind: "existing-job" } }, job, grantId) },
98
+ ...(reason ? { reason } : {}), createdAt: now.toISOString(), updatedAt: now.toISOString() };
99
+ if (!reason)
100
+ plan = { ...plan, authorization: grant(tx, plan, job, now) };
101
+ tx.saveSchedule(plan);
102
+ return;
103
+ }
104
+ const original = plan;
105
+ if (plan.authorization.kind !== "service")
106
+ throw new Error("Builtin Schedule requires service authority.");
107
+ if (reason || !plan.builtin.enabled) {
108
+ plan = { ...plan, state: plan.builtin.enabled ? "needs-attention" : "disabled",
109
+ reason: reason ?? "Project sync disabled by user.",
110
+ authorization: { ...plan.authorization, revoked: true } };
111
+ }
112
+ else if (changed || plan.reason?.startsWith(unavailable)) {
113
+ const { reason: _reason, ...rest } = plan;
114
+ const after = new Date(Math.max(now.getTime(), Date.parse(plan.processedThrough ?? "") || 0));
115
+ plan = { ...rest, state: "enabled", nextDue: nextScheduleDue(plan.rule, after),
116
+ processedThrough: after.toISOString(), generation: plan.generation + 1,
117
+ authorization: grant(tx, plan, job, now) };
118
+ }
119
+ // Unchanged dispatch failures are visible, not automatically retried/re-authorized.
120
+ if (!isDeepStrictEqual(original, plan))
121
+ tx.saveSchedule({
122
+ ...plan, revision: original.revision + 1, updatedAt: now.toISOString()
123
+ });
124
+ });
125
+ }
126
+ catch (error) {
127
+ onError(error instanceof Error ? error : new Error(String(error)));
128
+ }
129
+ }
130
+ }
131
+ /** Narrow user override. No alternative writable target or authorization surface. */
132
+ export function updateProjectSync(store, previous, op, rule, now) {
133
+ if (!previous.builtin || previous.authorization.kind !== "service")
134
+ throw new Error("Not a builtin Schedule.");
135
+ if (!["enable", "disable", "revoke", "update"].includes(op))
136
+ throw new Error("Unsupported builtin Schedule operation.");
137
+ const nextRule = rule ?? previous.rule;
138
+ validateScheduleRule(nextRule);
139
+ if (nextRule.kind === "once" || (nextRule.kind === "interval" && nextRule.everyMs < 60_000)) {
140
+ throw new Error("Builtin sync requires a recurring rule (interval at least 60000ms, or five-field cron).");
141
+ }
142
+ const enabled = op === "enable" ? true : ["disable", "revoke"].includes(op) ? false : previous.builtin.enabled;
143
+ const after = new Date(Math.max(now.getTime(), Date.parse(previous.processedThrough ?? "") || 0));
144
+ const { reason: _reason, ...rest } = previous;
145
+ let next = { ...rest, builtin: { ...previous.builtin, enabled }, rule: structuredClone(nextRule),
146
+ state: enabled ? "enabled" : "disabled", revision: previous.revision + 1,
147
+ generation: previous.generation + (op === "update" ? 1 : 0), updatedAt: now.toISOString(),
148
+ nextDue: nextScheduleDue(nextRule, after), processedThrough: after.toISOString() };
149
+ if (!enabled)
150
+ next = { ...next, reason: "Project sync disabled by user.",
151
+ authorization: { ...previous.authorization, revoked: true } };
152
+ else {
153
+ const job = store.getJobDefinition(scope, previous.jobId);
154
+ if (!job)
155
+ throw new Error("Builtin definition missing.");
156
+ requireProjectSyncTarget(store, job);
157
+ next = { ...next, authorization: previous.authorization.revoked || op === "enable"
158
+ ? grant(store, next, job, now) : previous.authorization };
159
+ }
160
+ store.saveSchedule(next);
161
+ return next;
162
+ }
@@ -0,0 +1,55 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isDeepStrictEqual } from "node:util";
3
+ import { requireProjectSyncTarget } from "./projectSync.js";
4
+ /** Internal compiled registration, not a caller identity accepted by RPC. */
5
+ const actions = new Set(["system.noop", "project.refresh"]);
6
+ export function requireServiceTarget(job) {
7
+ if (job.scope.kind !== "global" || job.configuration.kind !== "builtin"
8
+ || !actions.has(job.configuration.action))
9
+ throw new Error("Schedule service is not registered for this scope/action.");
10
+ }
11
+ export function scheduleServiceBinding(store, schedule, job, grantId) {
12
+ requireServiceTarget(job);
13
+ return createHash("sha256").update(JSON.stringify([
14
+ "schedule-service/v1", store.getHomeIdentity().homeId, grantId,
15
+ schedule.id, schedule.scope, schedule.jobId, schedule.target, job
16
+ ])).digest("hex");
17
+ }
18
+ export function requireScheduleService(store, ref) {
19
+ const schedule = store.getSchedule({ kind: "global" }, ref.scheduleId);
20
+ const auth = schedule?.authorization;
21
+ if (!schedule || auth?.kind !== "service" || auth.revoked || auth.grantId !== ref.grantId) {
22
+ throw new Error("Schedule service authority was revoked; no execution was started.");
23
+ }
24
+ const source = store.getJobDefinition(schedule.scope, schedule.jobId);
25
+ if (!source || source.revision !== auth.jobRevision
26
+ || scheduleServiceBinding(store, schedule, source, auth.grantId) !== auth.binding) {
27
+ throw new Error("Schedule service target or authority changed; explicitly reauthorize.");
28
+ }
29
+ if (schedule.builtin) {
30
+ if (!schedule.builtin.enabled)
31
+ throw new Error("Builtin Project sync was disabled.");
32
+ requireProjectSyncTarget(store, source);
33
+ }
34
+ return schedule;
35
+ }
36
+ /** Bind each immutable Run to its exact stored grant, definition and input. */
37
+ export function serviceRunBinding(store, run, schedule) {
38
+ return createHash("sha256").update(JSON.stringify([
39
+ "schedule-service-run/v1", store.getHomeIdentity().homeId, schedule.authorization.binding,
40
+ run.serviceAuthorization, run.id, run.definitionId, run.definitionRevision,
41
+ run.action, run.projectId, run.actionInput, run.trigger, run.operation.requestId
42
+ ])).digest("hex");
43
+ }
44
+ export function authorizeServiceJob(store, job, ref, key) {
45
+ const schedule = requireScheduleService(store, ref);
46
+ const source = store.getJobDefinition(schedule.scope, schedule.jobId);
47
+ const matches = schedule.target.kind === "existing-job" ? job.id === source.id
48
+ : job.instantiation?.sourceId === source.id && job.instantiation.sourceRevision === source.revision
49
+ && job.instantiation.key === key;
50
+ if (!matches || !key.startsWith(`${schedule.id}:`)
51
+ || !isDeepStrictEqual(job.configuration, source.configuration)) {
52
+ throw new Error("Job is outside the Schedule service authority target.");
53
+ }
54
+ return schedule;
55
+ }
@@ -0,0 +1,48 @@
1
+ import { requireIdentity, requireKnownFields, requireText } from "../domain/validation.js";
2
+ import { createTask, validateTask } from "../task/task.js";
3
+ import { createHash } from "node:crypto";
4
+ /** WorkItem revision also advances on execution/candidate submission. Freeze
5
+ * requirements, not that mutable progress counter, as the delegated scope. */
6
+ export function workItemDefinitionDigest(work) {
7
+ return createHash("sha256").update(JSON.stringify([work.title, work.objective, work.acceptance,
8
+ work.dependsOn, work.writeProjectIds, work.baseRefs ?? null])).digest("hex");
9
+ }
10
+ export function validateTaskJobConfiguration(config) {
11
+ requireKnownFields(config, ["kind", "goal", "target"], "Task Job");
12
+ if (config.target.kind === "new") {
13
+ requireKnownFields(config.target, ["kind", "title", "description", "type", "projectBindings"], "New Task target");
14
+ if (!["delivery", "task-created"].includes(config.goal))
15
+ throw new Error("New Task requires a delivery or explicit task-created goal.");
16
+ requireText(config.target.description, "Task requirements");
17
+ if (!Array.isArray(config.target.projectBindings))
18
+ throw new Error("Explicit Task Project bindings required.");
19
+ for (const binding of config.target.projectBindings) {
20
+ requireKnownFields(binding, ["projectId", "directory", "baseRef"], "Draft Project binding");
21
+ }
22
+ validateTask(createTask("task-1", config.target.title, new Date(0), config.target));
23
+ }
24
+ else if (config.target.kind === "work-item" && config.goal === "work-item") {
25
+ requireKnownFields(config.target, ["kind", "taskId", "workItemId", "revision"], "WorkItem target");
26
+ requireIdentity(config.target.taskId, "Target Task");
27
+ requireIdentity(config.target.workItemId, "Target WorkItem");
28
+ if (!/^task-\d+$/u.test(config.target.taskId) || !/^work-item-\d+$/u.test(config.target.workItemId)
29
+ || !Number.isSafeInteger(config.target.revision) || config.target.revision < 1)
30
+ throw new Error("Exact WorkItem scope and revision required.");
31
+ }
32
+ else
33
+ throw new Error("Invalid Task delegation target/goal.");
34
+ }
35
+ export function validateTaskDelegation(value) {
36
+ requireKnownFields(value, ["taskId", "goal", "workItemId", "workItemRevision", "workItemDefinitionDigest"], "Task delegation");
37
+ if (!/^task-\d+$/u.test(value.taskId))
38
+ throw new Error("Invalid delegated Task.");
39
+ if (value.goal === "work-item") {
40
+ if (!/^work-item-\d+$/u.test(value.workItemId ?? "")
41
+ || !Number.isSafeInteger(value.workItemRevision) || value.workItemRevision < 1
42
+ || !/^[a-f0-9]{64}$/u.test(value.workItemDefinitionDigest ?? ""))
43
+ throw new Error("Exact delegated WorkItem required.");
44
+ }
45
+ else if (!["delivery", "task-created"].includes(value.goal) || value.workItemId !== undefined
46
+ || value.workItemRevision !== undefined || value.workItemDefinitionDigest !== undefined)
47
+ throw new Error("Invalid delegated goal.");
48
+ }
@@ -32,7 +32,7 @@ export function resolveMessageRecipient(store, taskId, roleName, scope) {
32
32
  /** Current Assignment authority, independent of eligibility to start a new Run.
33
33
  * A Reviewer still owns its frozen clarification when Task main advances. */
34
34
  export function messageAssignmentBlocker(store, message) {
35
- if (isTaskRecordRetired(store.listEvents(message.taskId), "message", message.id))
35
+ if (message.id !== undefined && isTaskRecordRetired(store.listEvents(message.taskId), "message", message.id))
36
36
  return "message-retired";
37
37
  const target = message.recipient;
38
38
  if (target?.ownerRunId === undefined)
@@ -308,7 +308,7 @@ export function createProjectResources(store, now = () => new Date()) {
308
308
  || (run.workspace && overlaps(run.workspace.root, value.directory.path))))
309
309
  || sessionSets.some((set) => [...Object.values(set.sessions), ...(set.history ?? [])].some((session) => session.status === "active" && references(session.effective.executionEnvironment)))
310
310
  || tx.listDurableJobs(taskId).some((job) => ["queued", "running", "unknown-needs-attention"].includes(job.status)
311
- && overlaps(job.workspace, value.directory.path))))
311
+ && !job.delegation && overlaps(job.workspace, value.directory.path))))
312
312
  throw new Error("Environment still has a live or unknown execution reference.");
313
313
  }
314
314
  if (value.directory?.ownership === "preparation") {
@@ -43,7 +43,7 @@ export function readResourceGcState(store) {
43
43
  }),
44
44
  ...reader.listDurableJobs(task.id).filter(job => job.status === "queued" || job.status === "running"
45
45
  || (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined))
46
- .map(job => job.workspace)
46
+ .flatMap(job => job.delegation ? [] : [job.workspace])
47
47
  ])
48
48
  };
49
49
  });
package/dist/role/role.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
+ import { requireKnownFields } from "../domain/validation.js";
3
+ import { jobScopeKey } from "../job/jobDefinition.js";
2
4
  import { validateExecutionEnvironmentSnapshot } from "../resources/projectResource.js";
3
5
  import { isAgentAdapterId } from "../agent/adapterCatalog.js";
4
6
  import { adapterIdForExecutionComponent, resolveAgentExecutionComponent } from "../agent/executionComponents.js";
@@ -189,6 +191,16 @@ export function unbindRoleAgent(role, sessions, agentId, now) {
189
191
  export function validateGlobalRole(role) {
190
192
  if (Object.hasOwn(role, "taskId"))
191
193
  throw new Error("Global Role cannot carry a Task identity.");
194
+ if (role.jobOwner !== undefined) {
195
+ requireKnownFields(role.jobOwner, ["scope", "jobId", "runId"], "Job Role owner");
196
+ requireKnownFields(role.jobOwner.scope, role.jobOwner.scope.kind === "task" ? ["kind", "taskId"] : ["kind"], "Job Role scope");
197
+ if (!["task", "global"].includes(role.jobOwner.scope.kind) || role.name === "operator")
198
+ throw new Error("Invalid Job Role owner.");
199
+ jobScopeKey(role.jobOwner.scope);
200
+ requireSafeIdentity(role.jobOwner.jobId, "Job Role owner id");
201
+ if (role.jobOwner.runId !== undefined)
202
+ requireSafeIdentity(role.jobOwner.runId, "Job Role Run id");
203
+ }
192
204
  return validateRoleOwner(role);
193
205
  }
194
206
  export function validateTaskRole(role) {
@@ -1,6 +1,7 @@
1
1
  import { CronExpressionParser } from "cron-parser";
2
2
  import { requireKnownFields, requirePositiveInteger, requireTimestamp, requireText } from "../domain/validation.js";
3
3
  import { jobScopeKey } from "../job/jobDefinition.js";
4
+ import { parseDurableJobCaller } from "../controller/jobControl.js";
4
5
  export function scheduleId(id) {
5
6
  if (typeof id !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/u.test(id)) {
6
7
  throw new Error("An explicit safe Schedule/Job id is required.");
@@ -49,16 +50,25 @@ export function nextScheduleDue(rule, after) {
49
50
  return CronExpressionParser.parse(rule.expression, { currentDate: after, tz: rule.timezone }).next().toISOString();
50
51
  }
51
52
  export function validateSchedule(value) {
52
- requireKnownFields(value, ["schemaVersion", "id", "scope", "jobId", "rule", "revision", "generation",
53
- "state", "nextDue", "processedThrough", "authorization", "lastAttempt", "reason", "createdAt", "updatedAt"], "Schedule");
53
+ requireKnownFields(value, ["schemaVersion", "id", "scope", "jobId", "target", "rule", "revision", "generation",
54
+ "state", "nextDue", "processedThrough", "authorization", "builtin", "lastAttempt", "reason", "createdAt", "updatedAt"], "Schedule");
54
55
  if (value.schemaVersion !== 1)
55
56
  throw new Error("Invalid Schedule schema.");
56
57
  scheduleId(value.id);
57
58
  scheduleId(value.jobId);
59
+ validateScheduleTarget(value.target);
58
60
  if (!["task", "global"].includes(value.scope.kind))
59
61
  throw new Error("Invalid Schedule scope.");
60
62
  requireKnownFields(value.scope, value.scope.kind === "task" ? ["kind", "taskId"] : ["kind"], "Schedule scope");
61
63
  jobScopeKey(value.scope);
64
+ if (value.builtin !== undefined) {
65
+ requireKnownFields(value.builtin, ["projectId", "enabled"], "Builtin Schedule");
66
+ if (!/^project-\d+$/u.test(value.builtin.projectId) || typeof value.builtin.enabled !== "boolean"
67
+ || value.scope.kind !== "global" || value.id !== `project-sync-${value.builtin.projectId}`
68
+ || value.jobId !== value.id || value.target.kind !== "existing-job"
69
+ || value.authorization.kind !== "service")
70
+ throw new Error("Invalid builtin Schedule.");
71
+ }
62
72
  validateScheduleRule(value.rule);
63
73
  requirePositiveInteger(value.revision, "Schedule revision");
64
74
  requirePositiveInteger(value.generation, "Schedule generation");
@@ -74,35 +84,75 @@ export function validateSchedule(value) {
74
84
  instant(value.processedThrough);
75
85
  instant(value.createdAt);
76
86
  instant(value.updatedAt);
77
- requireKnownFields(value.authorization, ["caller", "binding", "jobRevision"], "Schedule authorization");
78
87
  requirePositiveInteger(value.authorization.jobRevision, "Schedule Job revision");
79
88
  if (typeof value.authorization.binding !== "string" || !/^[a-f0-9]{64}$/u.test(value.authorization.binding))
80
89
  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
+ if (value.authorization.kind === "service") {
91
+ requireKnownFields(value.authorization, ["kind", "service", "grantId", "revoked", "binding", "jobRevision", "grantedBy", "grantedAt"], "Schedule service authorization");
92
+ if (value.scope.kind !== "global" || value.authorization.service !== "schedule"
93
+ || typeof value.authorization.revoked !== "boolean")
94
+ throw new Error("Invalid Schedule service scope or registration.");
95
+ scheduleId(value.authorization.grantId);
96
+ instant(value.authorization.grantedAt);
97
+ if (value.authorization.grantedBy.scope === "service") {
98
+ requireKnownFields(value.authorization.grantedBy, ["scope", "service"], "Builtin service issuer");
99
+ if (value.authorization.grantedBy.service !== "project-sync" || value.builtin === undefined) {
100
+ throw new Error("Invalid builtin service issuer.");
101
+ }
102
+ }
103
+ else {
104
+ const manager = parseDurableJobCaller(value.authorization.grantedBy);
105
+ if ((manager.scope === "user" && Object.keys(manager).length !== 1)
106
+ || (manager.scope !== "user" && (manager.scope !== "global" || manager.role !== "operator" || !manager.nativeSessionId))) {
107
+ throw new Error("Invalid service grant issuer.");
108
+ }
109
+ }
90
110
  }
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.");
111
+ else {
112
+ requireKnownFields(value.authorization, ["kind", "caller", "binding", "jobRevision"], "Schedule authorization");
113
+ if (value.authorization.kind !== "session")
114
+ throw new Error("Explicit Schedule authorization kind is required.");
115
+ const caller = value.authorization.caller;
116
+ requireKnownFields(caller, ["scope", "taskId", "role", "agentId", "adapterId", "nativeSessionId", "runId"], "Schedule caller");
117
+ if (typeof caller.role !== "string" || typeof caller.nativeSessionId !== "string")
118
+ throw new Error("Schedule authority requires a Role and Session.");
119
+ requireText(caller.role, "Schedule caller role");
120
+ requireText(caller.nativeSessionId, "Schedule caller Session");
121
+ if (caller.scope === "global") {
122
+ if (caller.role !== "operator")
123
+ throw new Error("Global Schedule authority requires Operator.");
124
+ }
125
+ else if (caller.scope !== "task" || value.scope.kind !== "task" || caller.taskId !== value.scope.taskId) {
126
+ throw new Error("Schedule caller must belong to its scope.");
127
+ }
93
128
  }
94
129
  if (value.reason !== undefined)
95
130
  requireText(value.reason, "Schedule reason");
96
131
  if (value.lastAttempt !== undefined) {
97
- requireKnownFields(value.lastAttempt, ["at", "dueAt", "outcome", "runId", "reason"], "Schedule attempt");
132
+ requireKnownFields(value.lastAttempt, ["at", "dueAt", "outcome", "runId", "jobId", "generation", "reason"], "Schedule attempt");
98
133
  instant(value.lastAttempt.at);
99
134
  instant(value.lastAttempt.dueAt);
100
135
  if (!["admitted", "overlap", "blocked"].includes(value.lastAttempt.outcome))
101
136
  throw new Error("Invalid Schedule attempt.");
102
137
  if (value.lastAttempt.runId !== undefined)
103
138
  requireText(value.lastAttempt.runId, "Schedule Run id");
139
+ if (value.lastAttempt.jobId !== undefined)
140
+ scheduleId(value.lastAttempt.jobId);
141
+ if (value.lastAttempt.generation !== undefined)
142
+ requirePositiveInteger(value.lastAttempt.generation, "Schedule attempt generation");
104
143
  if (value.lastAttempt.reason !== undefined)
105
144
  requireText(value.lastAttempt.reason, "Schedule attempt reason");
106
145
  }
107
146
  return value;
108
147
  }
148
+ export function validateScheduleTarget(target) {
149
+ if (target?.kind === "existing-job")
150
+ requireKnownFields(target, ["kind"], "Schedule target");
151
+ else if (target?.kind === "new-job") {
152
+ requireKnownFields(target, ["kind", "definitionRevision"], "Schedule target");
153
+ requirePositiveInteger(target.definitionRevision, "Schedule source definition revision");
154
+ }
155
+ else
156
+ throw new Error("Invalid Schedule target.");
157
+ return target;
158
+ }
@@ -24,6 +24,7 @@ export const WAKE_REASON_KINDS = Object.freeze([
24
24
  "leader-turn-failed",
25
25
  "role-turn-failed",
26
26
  "job-finished",
27
+ "job-agent-input",
27
28
  "published-tree-authorized",
28
29
  "notification-retry",
29
30
  "force-wake"
@@ -31,6 +32,9 @@ export const WAKE_REASON_KINDS = Object.freeze([
31
32
  const IMMEDIATE_KINDS = new Set([
32
33
  "operator-input",
33
34
  "user-message",
35
+ // An explicitly admitted Agent Job is new input, not a background result
36
+ // to debounce. All existing busy/ownership/delivery checks still apply.
37
+ "job-agent-input",
34
38
  "task-created",
35
39
  "execution-started",
36
40
  "session-replaced",
@@ -0,0 +1,10 @@
1
+ /** Storage 1.18: Agent Runs are not shell runs with fabricated Task/workspace fields. */
2
+ export const AGENT_JOB_SCHEMA_SQL = `
3
+ CREATE TABLE agent_job_runs (
4
+ run_id TEXT PRIMARY KEY,
5
+ scope_key TEXT NOT NULL,
6
+ definition_id TEXT NOT NULL,
7
+ payload TEXT NOT NULL
8
+ );
9
+ CREATE INDEX idx_agent_job_runs_owner ON agent_job_runs(scope_key, definition_id);
10
+ `;
@@ -9,6 +9,7 @@ import { validateChangeSet } from "../integration/changeSet.js";
9
9
  import { validateIntegrationAttempt } from "../integration/integrationAttempt.js";
10
10
  import { validateDurableJob } from "../job/durableJob.js";
11
11
  import { validateJobDefinition } from "../job/jobDefinition.js";
12
+ import { validateAgentJobRun } from "../job/agentJob.js";
12
13
  import { validateSchedule } from "../schedule/schedule.js";
13
14
  import { validateGlobalRoleMessage, validateTaskMessage } from "../message/message.js";
14
15
  import { validateMilestone } from "../milestone/milestone.js";
@@ -57,6 +58,7 @@ const validators = {
57
58
  integration_attempts: check(validateIntegrationAttempt),
58
59
  durable_jobs: check(validateDurableJob),
59
60
  job_definitions: check(validateJobDefinition),
61
+ agent_job_runs: check(validateAgentJobRun),
60
62
  schedules: check(validateSchedule),
61
63
  global_job_runs: check(validateDurableJob),
62
64
  context_snapshots: check(validateContextSnapshot),
@@ -4,12 +4,15 @@ import { BASELINE_SCHEMA_SQL, SQLITE_SCHEMA_TABLES as BASELINE_SCHEMA_TABLES } f
4
4
  import { migrateTaskMainWorkspaces } from "./upgrade/taskMainWorkspaceMigration.js";
5
5
  import { migrateProjectKnowledge } from "./upgrade/projectKnowledgeMigration.js";
6
6
  import { JOB_SCHEMA_SQL } from "./jobSchema.js";
7
+ import { AGENT_JOB_SCHEMA_SQL } from "./agentJobSchema.js";
7
8
  import { SCHEDULE_SCHEMA_SQL } from "./scheduleSchema.js";
8
9
  import { GLOBAL_SESSION_TABLE_SQL, SESSION_HISTORY_UPGRADE_SQL } from "./sessionHistorySchema.js";
9
10
  import { migrateJobDefinitions } from "./upgrade/jobDefinitionMigration.js";
11
+ import { migrateJobBusinessResults } from "./upgrade/jobBusinessResultMigration.js";
12
+ import { migrateScheduleDispatch } from "./upgrade/scheduleDispatchMigration.js";
10
13
  import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION, STORAGE_FORMAT, isMinorStorageUpgrade, isStorageVersion, storageVersionParts } from "./storageVersions.js";
11
14
  export { BASELINE_SCHEMA_SQL };
12
- export const SQLITE_SCHEMA_TABLES = [...BASELINE_SCHEMA_TABLES, "job_definitions", "global_job_runs", "schedules"];
15
+ export const SQLITE_SCHEMA_TABLES = [...BASELINE_SCHEMA_TABLES, "job_definitions", "global_job_runs", "schedules", "agent_job_runs"];
13
16
  export const TELEMETRY_KEEP_PER_RUN = 200;
14
17
  export const TELEMETRY_RUN_CAP = 50_000;
15
18
  export const BASELINE_SCHEMA_CHECKSUM = createHash("sha256").update(BASELINE_SCHEMA_SQL).digest("hex");
@@ -17,7 +20,9 @@ export const JOB_SCHEMA_CHECKSUM = createHash("sha256").update(BASELINE_SCHEMA_S
17
20
  export const SCHEDULE_SCHEMA_SQL_COMPLETE = BASELINE_SCHEMA_SQL + JOB_SCHEMA_SQL + SCHEDULE_SCHEMA_SQL;
18
21
  export const SCHEDULE_SCHEMA_CHECKSUM = createHash("sha256").update(SCHEDULE_SCHEMA_SQL_COMPLETE).digest("hex");
19
22
  // Fresh Homes create the final layout directly; published baseline SQL stays immutable.
20
- export const CURRENT_SCHEMA_SQL = SCHEDULE_SCHEMA_SQL_COMPLETE.replace(/CREATE TABLE global_role_session_sets \([\s\S]*?\n\);/, GLOBAL_SESSION_TABLE_SQL);
23
+ const SESSION_SCHEMA_SQL = SCHEDULE_SCHEMA_SQL_COMPLETE.replace(/CREATE TABLE global_role_session_sets \([\s\S]*?\n\);/, GLOBAL_SESSION_TABLE_SQL);
24
+ const SESSION_SCHEMA_CHECKSUM = createHash("sha256").update(SESSION_SCHEMA_SQL).digest("hex");
25
+ export const CURRENT_SCHEMA_SQL = SESSION_SCHEMA_SQL + AGENT_JOB_SCHEMA_SQL;
21
26
  export const CURRENT_SCHEMA_CHECKSUM = createHash("sha256").update(CURRENT_SCHEMA_SQL).digest("hex");
22
27
  export class SqliteSchemaError extends Error {
23
28
  code;
@@ -164,9 +169,70 @@ const MINOR_UPGRADES = Object.freeze([{
164
169
  name: "retained-session-identity",
165
170
  introducedIn: "next",
166
171
  sourceChecksum: SCHEDULE_SCHEMA_CHECKSUM,
167
- targetChecksum: CURRENT_SCHEMA_CHECKSUM,
172
+ targetChecksum: SESSION_SCHEMA_CHECKSUM,
168
173
  // Preserve every payload byte. No identity or authorization is inferred.
169
174
  sql: SESSION_HISTORY_UPGRADE_SQL
175
+ }, {
176
+ fromVersion: "1.14",
177
+ toVersion: "1.15",
178
+ name: "job-business-results",
179
+ introducedIn: "next",
180
+ sourceChecksum: SESSION_SCHEMA_CHECKSUM,
181
+ targetChecksum: SESSION_SCHEMA_CHECKSUM,
182
+ sql: "",
183
+ dataMigration: "job-business-results"
184
+ }, {
185
+ fromVersion: "1.15",
186
+ toVersion: "1.16",
187
+ name: "schedule-dispatch",
188
+ introducedIn: "next",
189
+ sourceChecksum: SESSION_SCHEMA_CHECKSUM,
190
+ targetChecksum: SESSION_SCHEMA_CHECKSUM,
191
+ sql: "",
192
+ dataMigration: "schedule-dispatch"
193
+ }, {
194
+ fromVersion: "1.16",
195
+ toVersion: "1.17",
196
+ name: "job-owned-artifacts",
197
+ introducedIn: "next",
198
+ sourceChecksum: SESSION_SCHEMA_CHECKSUM,
199
+ targetChecksum: SESSION_SCHEMA_CHECKSUM,
200
+ // New optional append-only Run artifact references and Home-owned per-Run
201
+ // repositories. Existing payloads, Task repositories and frozen Git refs are
202
+ // unchanged. No files are moved and no historical provenance is invented.
203
+ sql: ""
204
+ }, {
205
+ fromVersion: "1.17",
206
+ toVersion: "1.18",
207
+ name: "builtin-project-sync",
208
+ introducedIn: "next",
209
+ sourceChecksum: SESSION_SCHEMA_CHECKSUM,
210
+ targetChecksum: SESSION_SCHEMA_CHECKSUM,
211
+ // Additive installer provenance, explicit compiled service issuer and user
212
+ // intent on new definitions/plans. Do not adopt or rewrite historical Jobs,
213
+ // grants, time overrides or Runs. Installation is a separate runtime action.
214
+ sql: ""
215
+ }, {
216
+ fromVersion: "1.18",
217
+ toVersion: "1.19",
218
+ name: "job-task-delegation",
219
+ introducedIn: "next",
220
+ sourceChecksum: SESSION_SCHEMA_CHECKSUM,
221
+ targetChecksum: SESSION_SCHEMA_CHECKSUM,
222
+ // Optional immutable Task creation source and Run delegation. Existing
223
+ // ordinary Tasks retain absence of source; all historical payload bytes,
224
+ // references and unknown effects remain unchanged.
225
+ sql: ""
226
+ }, {
227
+ fromVersion: "1.19",
228
+ toVersion: "1.20",
229
+ name: "job-agent-execution",
230
+ introducedIn: "next",
231
+ sourceChecksum: SESSION_SCHEMA_CHECKSUM,
232
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
233
+ // Existing records and immutable artifacts remain byte-for-byte unchanged.
234
+ // Declares Agent definitions/Runs and optional Job ownership of a non-Task Role.
235
+ sql: AGENT_JOB_SCHEMA_SQL
170
236
  }]);
171
237
  export function storageMinorUpgradePlan(from) {
172
238
  if (from === CURRENT_STORAGE_VERSION)
@@ -196,9 +262,10 @@ export function validateSqliteSchemaObjects(db, version = CURRENT_STORAGE_VERSIO
196
262
  const checksum = version === CURRENT_STORAGE_VERSION
197
263
  ? CURRENT_SCHEMA_CHECKSUM : storageMinorUpgradePlan(version)?.[0]?.sourceChecksum;
198
264
  const sql = checksum === CURRENT_SCHEMA_CHECKSUM ? CURRENT_SCHEMA_SQL
199
- : checksum === SCHEDULE_SCHEMA_CHECKSUM ? SCHEDULE_SCHEMA_SQL_COMPLETE
200
- : checksum === JOB_SCHEMA_CHECKSUM ? BASELINE_SCHEMA_SQL + JOB_SCHEMA_SQL
201
- : checksum === BASELINE_SCHEMA_CHECKSUM ? BASELINE_SCHEMA_SQL : undefined;
265
+ : checksum === SESSION_SCHEMA_CHECKSUM ? SESSION_SCHEMA_SQL
266
+ : checksum === SCHEDULE_SCHEMA_CHECKSUM ? SCHEDULE_SCHEMA_SQL_COMPLETE
267
+ : checksum === JOB_SCHEMA_CHECKSUM ? BASELINE_SCHEMA_SQL + JOB_SCHEMA_SQL
268
+ : checksum === BASELINE_SCHEMA_CHECKSUM ? BASELINE_SCHEMA_SQL : undefined;
202
269
  if (sql === undefined || checksum === undefined)
203
270
  throw new SqliteSchemaError(`unsupported schema definition for storage ${version}.`);
204
271
  let expectedObjects = schemaObjectsByChecksum.get(checksum);
@@ -292,6 +359,10 @@ export function applySqliteMinorUpgrades(db, home, createdRoots = []) {
292
359
  migrateProjectKnowledge(db);
293
360
  if (step.dataMigration === "job-definitions")
294
361
  migrateJobDefinitions(db);
362
+ if (step.dataMigration === "job-business-results")
363
+ migrateJobBusinessResults(db);
364
+ if (step.dataMigration === "schedule-dispatch")
365
+ migrateScheduleDispatch(db);
295
366
  const { major, minor } = storageVersionParts(step.toVersion);
296
367
  db.prepare("UPDATE storage_schema SET major=?,minor=?,checksum=? WHERE id=1")
297
368
  .run(major, minor, step.targetChecksum);