@zq-silk/yui 2.8.1 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/cli/commandCatalog.js +56 -21
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionPolicy.js +2 -2
- package/dist/commands/durableJobCommands.js +50 -0
- package/dist/commands/globalRoleCommands.js +30 -9
- package/dist/commands/inboxCommands.js +79 -22
- package/dist/commands/projectCommands.js +4 -33
- package/dist/commands/scheduleCommands.js +45 -0
- package/dist/commands/taskActivationCommands.js +19 -5
- package/dist/commands/taskCommands.js +8 -2
- package/dist/commands/taskFactCommands.js +24 -12
- package/dist/commands/taskProjectDeliveryCommand.js +56 -0
- package/dist/commands/taskPublicationCommands.js +6 -3
- package/dist/commands/taskPublicationVerifyCommand.js +74 -28
- package/dist/commands/taskRemoteDeliveryCommand.js +5 -3
- package/dist/context/runContextPack.js +25 -4
- package/dist/context/runInputContract.js +1 -0
- package/dist/context/taskContext.js +53 -16
- package/dist/context/taskPhase.js +99 -0
- package/dist/controlPlaneCli.js +38 -17
- package/dist/controller/controller.js +35 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +75 -16
- package/dist/controller/globalInputDelivery.js +17 -7
- package/dist/controller/inboxTurnCheck.js +64 -0
- package/dist/controller/jobControl.js +42 -12
- package/dist/controller/jobDefinitionControl.js +304 -0
- package/dist/controller/jobSupervisor.js +47 -32
- package/dist/controller/providerRetryAdmission.js +2 -0
- package/dist/controller/runtime.js +11 -2
- package/dist/controller/scheduleControl.js +176 -0
- package/dist/coordination/workMailbox.js +3 -3
- package/dist/external/githubPublicationVerifier.js +14 -5
- package/dist/external/gitlabPublicationVerifier.js +12 -29
- package/dist/grant/taskAuthorization.js +7 -2
- package/dist/interaction/notificationPresentation.js +12 -3
- package/dist/interaction/operatorPresentation.js +5 -0
- package/dist/job/builtinJob.js +58 -0
- package/dist/job/durableJob.js +72 -49
- package/dist/job/jobDefinition.js +68 -0
- package/dist/job/jobRunner.js +2 -1
- package/dist/job/projectRefreshRunner.js +48 -0
- package/dist/message/globalInboxRecovery.js +44 -0
- package/dist/message/globalInterrupt.js +1 -1
- package/dist/message/inboxReceipt.js +14 -6
- package/dist/message/inboxTurn.js +86 -0
- package/dist/message/message.js +11 -0
- package/dist/message/taskInboxOwnership.js +20 -6
- package/dist/repository/projectRefresh.js +58 -0
- package/dist/repository/taskWorkspacePreparer.js +2 -1
- package/dist/repository/workspaceCleanupInspection.js +6 -1
- package/dist/schedule/schedule.js +108 -0
- package/dist/scheduler/operatorEvent.js +2 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +8 -0
- package/dist/storage/contextRecords.js +8 -0
- package/dist/storage/jobSchema.js +15 -0
- package/dist/storage/maintenanceCaller.js +1 -1
- package/dist/storage/recordValidation.js +5 -0
- package/dist/storage/scheduleSchema.js +12 -0
- package/dist/storage/sqliteSchema.js +95 -24
- package/dist/storage/sqliteStore.js +123 -10
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +5 -0
- package/dist/storage/upgrade/jobDefinitionMigration.js +22 -0
- package/dist/task/archiveDiagnostics.js +1 -1
- package/dist/task/archivePreflight.js +1 -1
- package/dist/task/projectDeliveryScope.js +20 -0
- package/dist/task/publicationToolObservation.js +126 -0
- package/dist/task/remoteDelivery.js +19 -8
- package/dist/task/remoteDeliveryService.js +5 -4
- package/dist/task/taskActivationService.js +35 -0
- package/dist/web/assets/client/app/settings.js +7 -1
- package/dist/web/assets/client/i18n/en.js +65 -2
- package/dist/web/assets/client/i18n/zh-CN.js +65 -2
- package/dist/web/assets/client/index.js +2 -0
- package/dist/web/assets/client/views/jobs.js +222 -0
- package/dist/web/assets/client/views/task/deliveryResults.js +6 -2
- package/dist/web/assets/client/views/task/page.js +11 -1
- package/dist/web/assets/shared/manageNav.js +1 -0
- package/dist/web/assets/shared/taskTabs.js +2 -2
- package/dist/web/assets/shell/settingsContent.js +3 -0
- package/dist/web/assets/styles/index.js +2 -0
- package/dist/web/assets/styles/views/jobs.js +19 -0
- package/dist/web/controllerWeb.js +73 -21
- package/dist/web/taskPreviews.js +21 -13
- package/dist/web/webAccess.js +109 -0
- package/dist/web/webJobs.js +52 -0
- package/dist/web/webServer.js +76 -44
- package/docs/architecture/capabilities-and-resources.md +6 -4
- package/docs/architecture/capabilities-and-resources.zh-CN.md +5 -4
- package/docs/cli-information-contract.md +57 -4
- package/docs/cli-information-contract.zh-CN.md +41 -2
- package/docs/job-runs.md +224 -0
- package/docs/project-delivery-scope.md +59 -0
- package/docs/release-workflow.md +86 -0
- package/docs/release-workflow.zh-CN.md +68 -0
- package/docs/schedules.md +132 -0
- package/docs/storage-baseline.md +31 -1
- package/docs/storage-baseline.zh-CN.md +25 -1
- package/docs/task-delivery.md +74 -2
- package/docs/task-delivery.zh-CN.md +39 -1
- package/docs/testing/verification-levels.md +6 -2
- package/docs/testing/verification-levels.zh-CN.md +6 -3
- package/i18n/README.zh-CN.md +6 -3
- package/package.json +4 -3
- package/skills/yui-leader/SKILL.md +44 -1
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-leader/references/planning.md +9 -1
- package/skills/yui-operator/SKILL.md +31 -0
- package/skills/yui-operator/VERSION +1 -1
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-reviewer/VERSION +1 -1
- package/skills/yui-runtime/SKILL.md +67 -8
- package/skills/yui-runtime/VERSION +1 -1
- package/skills/yui-runtime/references/context-ordering.md +103 -0
- package/skills/yui-runtime/references/publication.md +44 -4
- package/skills/yui-runtime/references/recovery.md +21 -0
- package/skills/yui-worker/SKILL.md +5 -0
- package/skills/yui-worker/VERSION +1 -1
|
@@ -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 (
|
|
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
|
|
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
|
|
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:
|
|
25
|
-
targetChecksum:
|
|
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:
|
|
34
|
-
targetChecksum:
|
|
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:
|
|
44
|
-
targetChecksum:
|
|
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:
|
|
54
|
-
targetChecksum:
|
|
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:
|
|
63
|
-
targetChecksum:
|
|
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:
|
|
76
|
-
targetChecksum:
|
|
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:
|
|
85
|
-
targetChecksum:
|
|
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:
|
|
95
|
-
targetChecksum:
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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);
|