@zq-silk/yui 2.10.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.
- package/dist/artifacts/artifactCapability.js +14 -8
- package/dist/artifacts/artifactCommitLock.js +7 -5
- package/dist/artifacts/jobArtifact.js +93 -0
- package/dist/artifacts/jobArtifactTransport.js +13 -0
- package/dist/artifacts/taskArtifactRepository.js +58 -10
- package/dist/cli/callerDiscovery.js +18 -0
- package/dist/cli/commandCatalog.js +45 -14
- package/dist/cli/commandDiscovery.js +2 -3
- package/dist/commands/durableJobCommands.js +31 -6
- package/dist/commands/globalRoleCommands.js +5 -0
- package/dist/commands/scheduleCommands.js +14 -3
- package/dist/commands/taskCommands.js +5 -2
- package/dist/context/taskCatalog.js +6 -1
- package/dist/controlPlaneCli.js +12 -12
- package/dist/controller/agentJobControl.js +391 -0
- package/dist/controller/controller.js +24 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +12 -1
- package/dist/controller/globalInputDelivery.js +8 -1
- package/dist/controller/jobArtifactControl.js +111 -0
- package/dist/controller/jobControl.js +4 -0
- package/dist/controller/jobDefinitionControl.js +191 -19
- package/dist/controller/jobSupervisor.js +83 -3
- package/dist/controller/jobTaskDelegationControl.js +193 -0
- package/dist/controller/runtime.js +7 -1
- package/dist/controller/scheduleControl.js +83 -20
- package/dist/integration/gitIntegrationService.js +2 -0
- package/dist/job/agentJob.js +131 -0
- package/dist/job/agentJobObservation.js +61 -0
- package/dist/job/agentRoleScope.js +33 -0
- package/dist/job/durableJob.js +63 -28
- package/dist/job/jobAssignmentScope.js +1 -0
- package/dist/job/jobBusinessResult.js +72 -0
- package/dist/job/jobCleanupInspection.js +4 -0
- package/dist/job/jobDefinition.js +26 -2
- package/dist/job/jobRunner.js +4 -3
- package/dist/job/projectRefreshRunner.js +54 -18
- package/dist/job/projectSync.js +162 -0
- package/dist/job/scheduleService.js +55 -0
- package/dist/job/taskDelegation.js +48 -0
- package/dist/message/messageContinuation.js +1 -1
- package/dist/resources/projectResourceService.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/role/role.js +22 -12
- package/dist/runtime/managedIdentity.js +5 -3
- package/dist/runtime/sessionCaller.js +9 -10
- package/dist/schedule/schedule.js +65 -15
- package/dist/scheduler/wakeReason.js +4 -0
- package/dist/storage/agentJobSchema.js +10 -0
- package/dist/storage/maintenanceCaller.js +17 -25
- package/dist/storage/nativeSessionLookup.js +23 -0
- package/dist/storage/recordValidation.js +2 -0
- package/dist/storage/sessionHistorySchema.js +17 -0
- package/dist/storage/sqliteSchema.js +96 -11
- package/dist/storage/sqliteStore.js +71 -4
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +4 -0
- package/dist/storage/taskCatalog.js +10 -2
- package/dist/storage/upgrade/jobBusinessResultMigration.js +20 -0
- package/dist/storage/upgrade/scheduleDispatchMigration.js +15 -0
- package/dist/task/archivePreflight.js +15 -0
- package/dist/task/completionReadiness.js +7 -0
- package/dist/task/task.js +17 -1
- package/dist/verification/verificationGateService.js +4 -0
- package/dist/web/assets/client/app/catalog.js +16 -0
- package/dist/web/assets/client/i18n/en.js +29 -2
- package/dist/web/assets/client/i18n/zh-CN.js +29 -2
- package/dist/web/assets/client/layout/manageShell.js +9 -0
- package/dist/web/assets/client/layout/router.js +1 -0
- package/dist/web/assets/client/views/jobs.js +166 -37
- package/dist/web/assets/client/views/task/files.js +5 -4
- package/dist/web/assets/client/views/task/page.js +5 -1
- package/dist/web/taskPreviews.js +2 -2
- package/dist/web/webJobs.js +36 -11
- package/dist/web/webMaterials.js +21 -9
- package/dist/web/webServer.js +6 -5
- package/dist/web/webTaskSurface.js +1 -0
- package/docs/authorization.md +130 -0
- package/docs/job-runs.md +392 -17
- package/docs/managed-turn-and-session-runtime.md +10 -5
- package/docs/managed-turn-and-session-runtime.zh-CN.md +12 -5
- package/docs/release-workflow.md +79 -0
- package/docs/release-workflow.zh-CN.md +58 -0
- package/docs/schedules.md +69 -15
- package/docs/storage-baseline.md +45 -1
- package/docs/storage-baseline.zh-CN.md +33 -1
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-runtime/SKILL.md +12 -5
- package/skills/yui-runtime/VERSION +1 -1
|
@@ -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
|
+
`;
|
|
@@ -5,16 +5,23 @@ import { ManagedRuntimeDriftError, requireManagedGlobalCaller } from "../runtime
|
|
|
5
5
|
import { sessionCallerEnvironment } from "../runtime/sessionCaller.js";
|
|
6
6
|
import { validateStoredRecord } from "./recordValidation.js";
|
|
7
7
|
import { inspectSqliteSchema, storageMinorUpgradePlan, validateSqliteSchemaObjects } from "./sqliteSchema.js";
|
|
8
|
-
import { CURRENT_DATABASE_FILENAME } from "./storageSchema.js";
|
|
8
|
+
import { CURRENT_DATABASE_FILENAME, inspectStorageSchema } from "./storageSchema.js";
|
|
9
|
+
import { findNativeSessionSets } from "./nativeSessionLookup.js";
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* envelopes and physical tables. Read only those records in one SQLite snapshot
|
|
13
|
-
*
|
|
11
|
+
* Read-only caller resolution, also usable before maintenance/migration and by
|
|
12
|
+
* offline discovery. The declared 1.x migrations preserve these Role/Session
|
|
13
|
+
* envelopes and physical tables. Read only those records in one SQLite snapshot.
|
|
14
|
+
* operatorOnly adds the maintenance gate; ordinary execution still applies its
|
|
15
|
+
* current storage, Session and domain checks. No implicit migration or setup.
|
|
14
16
|
*/
|
|
15
17
|
export function maintenanceCallerEnvironment(home, env, operatorOnly = true) {
|
|
16
|
-
if (!hasManagedIdentity(env))
|
|
18
|
+
if (!hasManagedIdentity(env) && env.CODEX_THREAD_ID === undefined)
|
|
17
19
|
return env;
|
|
20
|
+
// A genuinely new Home has no managed registrations. Do not initialize it
|
|
21
|
+
// here: setup remains the explicit owner. Existing invalid storage fails closed.
|
|
22
|
+
if (!hasManagedIdentity(env) && inspectStorageSchema(home).status === "uninitialized") {
|
|
23
|
+
return sessionCallerEnvironment({ findNativeSessionSets: () => [] }, env);
|
|
24
|
+
}
|
|
18
25
|
const db = new Database(join(home, CURRENT_DATABASE_FILENAME), { readonly: true, fileMustExist: true });
|
|
19
26
|
try {
|
|
20
27
|
return db.transaction(() => {
|
|
@@ -34,29 +41,14 @@ export function maintenanceCallerEnvironment(home, env, operatorOnly = true) {
|
|
|
34
41
|
const registry = {
|
|
35
42
|
getGlobalRole: (name) => read("global_roles", "name=?", [name]),
|
|
36
43
|
getGlobalRoleSessionSet: (name) => read("global_role_session_sets", "name=?", [name]),
|
|
37
|
-
|
|
38
|
-
listRuntimeSessionCandidates: () => {
|
|
39
|
-
// Read canonical sets, not a derived index that could be stale. Only
|
|
40
|
-
// active bindings participate, exactly as in normal caller discovery.
|
|
41
|
-
const rows = db.prepare(`
|
|
42
|
-
SELECT 'global_role_session_sets' AS source, payload FROM global_role_session_sets
|
|
43
|
-
UNION ALL SELECT 'role_session_sets' AS source, payload FROM role_session_sets
|
|
44
|
-
`).all();
|
|
45
|
-
return rows.flatMap(row => {
|
|
46
|
-
const set = JSON.parse(row.payload);
|
|
47
|
-
validateStoredRecord(row.source, set);
|
|
48
|
-
const session = set.sessions[set.activeAgentId];
|
|
49
|
-
if (!session || session.status !== "active")
|
|
50
|
-
return [];
|
|
51
|
-
return [{ owner: set.owner, agentId: session.agentId, adapterId: session.adapterId,
|
|
52
|
-
nativeSessionId: session.nativeSessionId, sessionUpdatedAt: set.updatedAt }];
|
|
53
|
-
});
|
|
54
|
-
}
|
|
44
|
+
findNativeSessionSets: (nativeIds) => findNativeSessionSets(db, nativeIds)
|
|
55
45
|
};
|
|
56
46
|
const current = sessionCallerEnvironment(registry, env);
|
|
47
|
+
if (!hasManagedIdentity(current))
|
|
48
|
+
return current;
|
|
57
49
|
if (current.YUI_SESSION_SCOPE !== "global" || current.YUI_ROLE !== "operator") {
|
|
58
50
|
if (!operatorOnly)
|
|
59
|
-
return current; //
|
|
51
|
+
return current; // Execution retains its normal scoped checks.
|
|
60
52
|
throw new ManagedRuntimeDriftError("Home maintenance requires the user or current Operator, not a Task or custom Role.");
|
|
61
53
|
}
|
|
62
54
|
requireManagedGlobalCaller(registry, current);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { validateStoredRecord } from "./recordValidation.js";
|
|
2
|
+
/** Identity lookup includes terminal and replaced Sessions. The hot active
|
|
3
|
+
* runtime projection cannot prove that an unknown caller is unmanaged.
|
|
4
|
+
* Read existing canonical records; no second identity registry or schema. */
|
|
5
|
+
export function findNativeSessionSets(db, nativeIds) {
|
|
6
|
+
const ids = [...new Set(nativeIds)];
|
|
7
|
+
if (!ids.length)
|
|
8
|
+
return [];
|
|
9
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
10
|
+
return ["role_session_sets", "global_role_session_sets"].flatMap(table => {
|
|
11
|
+
const rows = db.prepare(`SELECT payload FROM ${table}
|
|
12
|
+
WHERE EXISTS (SELECT 1 FROM json_each(payload, '$.sessions')
|
|
13
|
+
WHERE json_extract(value, '$.nativeSessionId') IN (${placeholders}))
|
|
14
|
+
OR EXISTS (SELECT 1 FROM json_each(payload, '$.history')
|
|
15
|
+
WHERE json_extract(value, '$.nativeSessionId') IN (${placeholders}))`)
|
|
16
|
+
.all(...ids, ...ids);
|
|
17
|
+
return rows.map(row => {
|
|
18
|
+
const set = JSON.parse(row.payload);
|
|
19
|
+
validateStoredRecord(table, set);
|
|
20
|
+
return set;
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -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),
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Session history outlives Role deletion. It is identity evidence, not a
|
|
2
|
+
* grant or an independently managed lifecycle. Payload format is unchanged. */
|
|
3
|
+
export const GLOBAL_SESSION_TABLE_SQL = `CREATE TABLE global_role_session_sets (
|
|
4
|
+
name TEXT PRIMARY KEY,
|
|
5
|
+
payload TEXT NOT NULL,
|
|
6
|
+
updated_at TEXT NOT NULL
|
|
7
|
+
);`;
|
|
8
|
+
export const GLOBAL_SESSION_RETRY_INDEX_SQL = `CREATE INDEX idx_global_provider_retry ON global_role_session_sets(
|
|
9
|
+
json_extract(payload, '$.providerBinding.retry.status')
|
|
10
|
+
) WHERE json_extract(payload, '$.providerBinding.retry.status') IN ('waiting', 'in-flight');`;
|
|
11
|
+
export const SESSION_HISTORY_UPGRADE_SQL = `
|
|
12
|
+
ALTER TABLE global_role_session_sets RENAME TO global_role_session_sets_113;
|
|
13
|
+
${GLOBAL_SESSION_TABLE_SQL}
|
|
14
|
+
INSERT INTO global_role_session_sets SELECT * FROM global_role_session_sets_113;
|
|
15
|
+
DROP TABLE global_role_session_sets_113;
|
|
16
|
+
${GLOBAL_SESSION_RETRY_INDEX_SQL}
|
|
17
|
+
`;
|
|
@@ -4,16 +4,25 @@ 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";
|
|
9
|
+
import { GLOBAL_SESSION_TABLE_SQL, SESSION_HISTORY_UPGRADE_SQL } from "./sessionHistorySchema.js";
|
|
8
10
|
import { migrateJobDefinitions } from "./upgrade/jobDefinitionMigration.js";
|
|
11
|
+
import { migrateJobBusinessResults } from "./upgrade/jobBusinessResultMigration.js";
|
|
12
|
+
import { migrateScheduleDispatch } from "./upgrade/scheduleDispatchMigration.js";
|
|
9
13
|
import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION, STORAGE_FORMAT, isMinorStorageUpgrade, isStorageVersion, storageVersionParts } from "./storageVersions.js";
|
|
10
14
|
export { BASELINE_SCHEMA_SQL };
|
|
11
|
-
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"];
|
|
12
16
|
export const TELEMETRY_KEEP_PER_RUN = 200;
|
|
13
17
|
export const TELEMETRY_RUN_CAP = 50_000;
|
|
14
18
|
export const BASELINE_SCHEMA_CHECKSUM = createHash("sha256").update(BASELINE_SCHEMA_SQL).digest("hex");
|
|
15
19
|
export const JOB_SCHEMA_CHECKSUM = createHash("sha256").update(BASELINE_SCHEMA_SQL + JOB_SCHEMA_SQL).digest("hex");
|
|
16
|
-
export const
|
|
20
|
+
export const SCHEDULE_SCHEMA_SQL_COMPLETE = BASELINE_SCHEMA_SQL + JOB_SCHEMA_SQL + SCHEDULE_SCHEMA_SQL;
|
|
21
|
+
export const SCHEDULE_SCHEMA_CHECKSUM = createHash("sha256").update(SCHEDULE_SCHEMA_SQL_COMPLETE).digest("hex");
|
|
22
|
+
// Fresh Homes create the final layout directly; published baseline SQL stays immutable.
|
|
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;
|
|
17
26
|
export const CURRENT_SCHEMA_CHECKSUM = createHash("sha256").update(CURRENT_SCHEMA_SQL).digest("hex");
|
|
18
27
|
export class SqliteSchemaError extends Error {
|
|
19
28
|
code;
|
|
@@ -118,15 +127,15 @@ const MINOR_UPGRADES = Object.freeze([{
|
|
|
118
127
|
name: "persistent-schedules",
|
|
119
128
|
introducedIn: "next",
|
|
120
129
|
sourceChecksum: JOB_SCHEMA_CHECKSUM,
|
|
121
|
-
targetChecksum:
|
|
130
|
+
targetChecksum: SCHEDULE_SCHEMA_CHECKSUM,
|
|
122
131
|
sql: SCHEDULE_SCHEMA_SQL
|
|
123
132
|
}, {
|
|
124
133
|
fromVersion: "1.10",
|
|
125
134
|
toVersion: "1.11",
|
|
126
135
|
name: "activation-input-provenance",
|
|
127
136
|
introducedIn: "next",
|
|
128
|
-
sourceChecksum:
|
|
129
|
-
targetChecksum:
|
|
137
|
+
sourceChecksum: SCHEDULE_SCHEMA_CHECKSUM,
|
|
138
|
+
targetChecksum: SCHEDULE_SCHEMA_CHECKSUM,
|
|
130
139
|
// Activation event payloads may now name an exact input or contain attributed
|
|
131
140
|
// native input text. Existing events remain immutable; missing old provenance
|
|
132
141
|
// stays explicitly missing. No historical source is guessed from timestamps.
|
|
@@ -138,8 +147,8 @@ const MINOR_UPGRADES = Object.freeze([{
|
|
|
138
147
|
toVersion: "1.12",
|
|
139
148
|
name: "project-delivery-scope",
|
|
140
149
|
introducedIn: "next",
|
|
141
|
-
sourceChecksum:
|
|
142
|
-
targetChecksum:
|
|
150
|
+
sourceChecksum: SCHEDULE_SCHEMA_CHECKSUM,
|
|
151
|
+
targetChecksum: SCHEDULE_SCHEMA_CHECKSUM,
|
|
143
152
|
// New append-only Project scope decisions use Task events. Preserve all old
|
|
144
153
|
// completion, binding and publication records; absence still means required.
|
|
145
154
|
sql: ""
|
|
@@ -148,12 +157,82 @@ const MINOR_UPGRADES = Object.freeze([{
|
|
|
148
157
|
toVersion: "1.13",
|
|
149
158
|
name: "inbox-turn-handling",
|
|
150
159
|
introducedIn: "next",
|
|
151
|
-
sourceChecksum:
|
|
152
|
-
targetChecksum:
|
|
160
|
+
sourceChecksum: SCHEDULE_SCHEMA_CHECKSUM,
|
|
161
|
+
targetChecksum: SCHEDULE_SCHEMA_CHECKSUM,
|
|
153
162
|
// Claims may name their exact current attempt; explicit Global retirement
|
|
154
163
|
// and terminal-check notices preserve original records. Never backfill
|
|
155
164
|
// historical turn ownership, handling evidence or acknowledgements.
|
|
156
165
|
sql: ""
|
|
166
|
+
}, {
|
|
167
|
+
fromVersion: "1.13",
|
|
168
|
+
toVersion: "1.14",
|
|
169
|
+
name: "retained-session-identity",
|
|
170
|
+
introducedIn: "next",
|
|
171
|
+
sourceChecksum: SCHEDULE_SCHEMA_CHECKSUM,
|
|
172
|
+
targetChecksum: SESSION_SCHEMA_CHECKSUM,
|
|
173
|
+
// Preserve every payload byte. No identity or authorization is inferred.
|
|
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
|
|
157
236
|
}]);
|
|
158
237
|
export function storageMinorUpgradePlan(from) {
|
|
159
238
|
if (from === CURRENT_STORAGE_VERSION)
|
|
@@ -183,8 +262,10 @@ export function validateSqliteSchemaObjects(db, version = CURRENT_STORAGE_VERSIO
|
|
|
183
262
|
const checksum = version === CURRENT_STORAGE_VERSION
|
|
184
263
|
? CURRENT_SCHEMA_CHECKSUM : storageMinorUpgradePlan(version)?.[0]?.sourceChecksum;
|
|
185
264
|
const sql = checksum === CURRENT_SCHEMA_CHECKSUM ? CURRENT_SCHEMA_SQL
|
|
186
|
-
: checksum ===
|
|
187
|
-
: checksum ===
|
|
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;
|
|
188
269
|
if (sql === undefined || checksum === undefined)
|
|
189
270
|
throw new SqliteSchemaError(`unsupported schema definition for storage ${version}.`);
|
|
190
271
|
let expectedObjects = schemaObjectsByChecksum.get(checksum);
|
|
@@ -278,6 +359,10 @@ export function applySqliteMinorUpgrades(db, home, createdRoots = []) {
|
|
|
278
359
|
migrateProjectKnowledge(db);
|
|
279
360
|
if (step.dataMigration === "job-definitions")
|
|
280
361
|
migrateJobDefinitions(db);
|
|
362
|
+
if (step.dataMigration === "job-business-results")
|
|
363
|
+
migrateJobBusinessResults(db);
|
|
364
|
+
if (step.dataMigration === "schedule-dispatch")
|
|
365
|
+
migrateScheduleDispatch(db);
|
|
281
366
|
const { major, minor } = storageVersionParts(step.toVersion);
|
|
282
367
|
db.prepare("UPDATE storage_schema SET major=?,minor=?,checksum=? WHERE id=1")
|
|
283
368
|
.run(major, minor, step.targetChecksum);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateAgentJobRun, assertAgentJobRunTransition } from "../job/agentJob.js";
|
|
1
2
|
/**
|
|
2
3
|
* SQLite WAL Store (task-21, work-item-3).
|
|
3
4
|
*
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
* in-process contract or deferred validation phase.
|
|
34
35
|
*/
|
|
35
36
|
import Database from "../sqlite/driver.js";
|
|
37
|
+
import { findNativeSessionSets } from "./nativeSessionLookup.js";
|
|
36
38
|
import { existsSync, mkdirSync } from "node:fs";
|
|
37
39
|
import { join } from "node:path";
|
|
38
40
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -929,7 +931,9 @@ export class SqliteTaskStore {
|
|
|
929
931
|
this.#saveRuntimeSessionCandidate(sessions);
|
|
930
932
|
}
|
|
931
933
|
else {
|
|
932
|
-
this
|
|
934
|
+
if (this.getGlobalRoleSessionSet(role.name) !== null) {
|
|
935
|
+
throw new Error("Cannot discard recorded Session identity/history while saving a Role.");
|
|
936
|
+
}
|
|
933
937
|
this.#deleteRuntimeSessionCandidate({ scope: "global", roleName: role.name });
|
|
934
938
|
}
|
|
935
939
|
});
|
|
@@ -948,8 +952,13 @@ export class SqliteTaskStore {
|
|
|
948
952
|
}
|
|
949
953
|
removeGlobalRole(name) {
|
|
950
954
|
return this.#mutate(() => {
|
|
955
|
+
const sessions = this.getGlobalRoleSessionSet(name);
|
|
956
|
+
if (Object.values(sessions?.sessions ?? {}).some(session => session.status === "active")) {
|
|
957
|
+
throw new Error("Stop the Role's native Sessions before removing it.");
|
|
958
|
+
}
|
|
951
959
|
this.#deleteRuntimeSessionCandidate({ scope: "global", roleName: name });
|
|
952
|
-
|
|
960
|
+
// Removal requires stopped Sessions at the domain boundary. Retain their
|
|
961
|
+
// canonical identity evidence so native-only callers never become public.
|
|
953
962
|
return this.#db.prepare("DELETE FROM global_roles WHERE name = ?").run(name).changes > 0;
|
|
954
963
|
});
|
|
955
964
|
}
|
|
@@ -1071,6 +1080,10 @@ export class SqliteTaskStore {
|
|
|
1071
1080
|
throw new StorageRecordError("Task id is required.");
|
|
1072
1081
|
}
|
|
1073
1082
|
this.#mutate(() => {
|
|
1083
|
+
const previous = this.getTask(task.id);
|
|
1084
|
+
if (previous !== null && !isDeepStrictEqual(previous.source, task.source)) {
|
|
1085
|
+
throw new StorageRecordError("Task source is immutable after creation.");
|
|
1086
|
+
}
|
|
1074
1087
|
const schedules = this.listSchedules({ kind: "task", taskId: task.id });
|
|
1075
1088
|
if (task.status === "completed" && schedules.some(s => s.rule.kind === "once"
|
|
1076
1089
|
&& (s.state === "enabled" || s.state === "needs-attention"))) {
|
|
@@ -1109,6 +1122,18 @@ export class SqliteTaskStore {
|
|
|
1109
1122
|
const tasks = this.#listPayload("task_records", "1=1", []);
|
|
1110
1123
|
return this.#sortById(tasks, (task) => task.id);
|
|
1111
1124
|
}
|
|
1125
|
+
listTaskDelegations(taskId) {
|
|
1126
|
+
const rows = this.#db.prepare(`SELECT task_id AS scope, job_id AS id, payload FROM durable_jobs
|
|
1127
|
+
WHERE json_extract(payload,'$.delegation.taskId') = ?
|
|
1128
|
+
UNION ALL SELECT 'global' AS scope, run_id AS id, payload FROM global_job_runs
|
|
1129
|
+
WHERE json_extract(payload,'$.delegation.taskId') = ? LIMIT 101`).all(taskId, taskId);
|
|
1130
|
+
return { complete: rows.length <= 100, items: rows.slice(0, 100).map(row => {
|
|
1131
|
+
const run = JSON.parse(row.payload);
|
|
1132
|
+
return { sourceRun: { scope: row.scope === "global" ? { kind: "global" }
|
|
1133
|
+
: { kind: "task", taskId: row.scope }, runId: row.id },
|
|
1134
|
+
jobId: run.definitionId, delegation: run.delegation, status: run.status };
|
|
1135
|
+
}) };
|
|
1136
|
+
}
|
|
1112
1137
|
queryTaskCatalog(query) {
|
|
1113
1138
|
return queryTaskCatalog(this.#db, query);
|
|
1114
1139
|
}
|
|
@@ -1167,6 +1192,7 @@ export class SqliteTaskStore {
|
|
|
1167
1192
|
return {
|
|
1168
1193
|
...base,
|
|
1169
1194
|
pendingUserMessages: pendingCompletionMessages(this, taskId),
|
|
1195
|
+
agentJobs: this.listAgentJobRuns({ kind: "task", taskId }),
|
|
1170
1196
|
managedWorkspaces: this.#sortById(this.#listPayload("managed_workspaces", "task_id = ?", [taskId]), (workspace) => managedWorkspaceKey(workspace.owner)),
|
|
1171
1197
|
durableJobs: this.#sortById(this.#listPayload("durable_jobs", "task_id = ?", [taskId]), (job) => job.id),
|
|
1172
1198
|
};
|
|
@@ -1324,6 +1350,41 @@ export class SqliteTaskStore {
|
|
|
1324
1350
|
.run(run.id, this.#recordJson("global_job_runs", run), run.status);
|
|
1325
1351
|
});
|
|
1326
1352
|
}
|
|
1353
|
+
saveAgentJobRun(run) {
|
|
1354
|
+
validateAgentJobRun(run);
|
|
1355
|
+
if (run.scope.kind === "task")
|
|
1356
|
+
this.#requireTask(run.scope.taskId);
|
|
1357
|
+
this.#mutate(() => {
|
|
1358
|
+
const previous = this.getAgentJobRun(run.id);
|
|
1359
|
+
if (previous)
|
|
1360
|
+
assertAgentJobRunTransition(previous, run);
|
|
1361
|
+
this.#db.prepare(`INSERT INTO agent_job_runs VALUES(?,?,?,?)
|
|
1362
|
+
ON CONFLICT(run_id) DO UPDATE SET payload=excluded.payload`)
|
|
1363
|
+
.run(run.id, jobScopeKey(run.scope), run.definitionId, this.#recordJson("agent_job_runs", run));
|
|
1364
|
+
if (run.scope.kind === "task" && (!previous || previous.status !== run.status
|
|
1365
|
+
|| !isDeepStrictEqual(previous.businessResult, run.businessResult) || previous.cancelRequestedAt !== run.cancelRequestedAt)) {
|
|
1366
|
+
const taskId = run.scope.taskId;
|
|
1367
|
+
this.saveEvent(taskId, createTaskEvent(this.nextEventId(taskId), taskId, "job.state-changed", {
|
|
1368
|
+
jobId: run.id, status: run.status,
|
|
1369
|
+
...(previous ? { previousStatus: previous.status } : {}),
|
|
1370
|
+
...(run.cancelRequestedAt ? { cancelRequestedAt: run.cancelRequestedAt } : {})
|
|
1371
|
+
}, new Date(run.updatedAt)));
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
getAgentJobRun(id) {
|
|
1376
|
+
return this.#getPayload("agent_job_runs", "run_id=?", [id]);
|
|
1377
|
+
}
|
|
1378
|
+
listAgentJobRuns(scope, definitionId) {
|
|
1379
|
+
return this.#listPayload("agent_job_runs", definitionId === undefined ? "scope_key=?" : "scope_key=? AND definition_id=?", definitionId === undefined ? [jobScopeKey(scope)] : [jobScopeKey(scope), definitionId]);
|
|
1380
|
+
}
|
|
1381
|
+
listAgentJobRunsForRole(recipient) {
|
|
1382
|
+
return this.#listPayload("agent_job_runs", `json_extract(payload,'$.attempts[#-1].recipient.scope')=?
|
|
1383
|
+
AND json_extract(payload,'$.attempts[#-1].recipient.roleName')=?
|
|
1384
|
+
AND COALESCE(json_extract(payload,'$.attempts[#-1].recipient.taskId'),'')=?
|
|
1385
|
+
AND (json_extract(payload,'$.attempts[#-1].terminal') IS NULL
|
|
1386
|
+
OR json_extract(payload,'$.businessResult') IS NULL)`, [recipient.scope, recipient.roleName, recipient.scope === "task" ? recipient.taskId : ""]);
|
|
1387
|
+
}
|
|
1327
1388
|
getGlobalJobRun(id) {
|
|
1328
1389
|
return this.#getPayload("global_job_runs", "run_id=?", [id]);
|
|
1329
1390
|
}
|
|
@@ -1449,13 +1510,16 @@ export class SqliteTaskStore {
|
|
|
1449
1510
|
}
|
|
1450
1511
|
removeTaskRole(taskId, name) {
|
|
1451
1512
|
return this.#mutate(() => {
|
|
1513
|
+
const sessions = this.getTaskRoleSessionSet(taskId, name);
|
|
1514
|
+
if (Object.values(sessions?.sessions ?? {}).some(session => session.status === "active")) {
|
|
1515
|
+
throw new Error("Stop the Role's native Sessions before removing it.");
|
|
1516
|
+
}
|
|
1452
1517
|
this.#deleteRuntimeSessionCandidate({
|
|
1453
1518
|
scope: "task",
|
|
1454
1519
|
taskId,
|
|
1455
1520
|
roleName: name
|
|
1456
1521
|
});
|
|
1457
|
-
|
|
1458
|
-
this.#db.prepare("DELETE FROM pending_runtime_turn_completions WHERE task_id = ? AND role_name = ?").run(taskId, name);
|
|
1522
|
+
// Role deletion removes authority, not stopped Session identity/history.
|
|
1459
1523
|
const result = this.#db.prepare("DELETE FROM task_roles WHERE task_id = ? AND role_name = ?").run(taskId, name);
|
|
1460
1524
|
return result.changes > 0;
|
|
1461
1525
|
});
|
|
@@ -1517,6 +1581,9 @@ export class SqliteTaskStore {
|
|
|
1517
1581
|
...this.#listPayload("global_role_session_sets", pending, [])
|
|
1518
1582
|
];
|
|
1519
1583
|
}
|
|
1584
|
+
findNativeSessionSets(nativeIds) {
|
|
1585
|
+
return findNativeSessionSets(this.#db, nativeIds);
|
|
1586
|
+
}
|
|
1520
1587
|
listRuntimeSessionCandidates(query = {}) {
|
|
1521
1588
|
const taskIds = query.taskIds === undefined
|
|
1522
1589
|
? undefined
|
package/dist/storage/storeRpc.js
CHANGED
|
@@ -73,6 +73,9 @@ const READ_ONLY_STORE_METHODS = new Set([
|
|
|
73
73
|
"getJobDefinition",
|
|
74
74
|
"listJobDefinitions",
|
|
75
75
|
"getGlobalJobRun",
|
|
76
|
+
"getAgentJobRun",
|
|
77
|
+
"listAgentJobRuns",
|
|
78
|
+
"listAgentJobRunsForRole",
|
|
76
79
|
"listGlobalJobRuns",
|
|
77
80
|
"listActiveGlobalJobRuns",
|
|
78
81
|
"listRoles",
|
|
@@ -89,6 +92,7 @@ const READ_ONLY_STORE_METHODS = new Set([
|
|
|
89
92
|
"listRoleSessionSets",
|
|
90
93
|
"listProviderRetrySessions",
|
|
91
94
|
"listRuntimeSessionCandidates",
|
|
95
|
+
"findNativeSessionSets",
|
|
92
96
|
"getRoleSession",
|
|
93
97
|
"getWorkItem",
|
|
94
98
|
"listWorkItems",
|
|
@@ -21,6 +21,10 @@ export function queryTaskCatalog(db, query) {
|
|
|
21
21
|
match.push("status = @status");
|
|
22
22
|
params.status = query.status;
|
|
23
23
|
}
|
|
24
|
+
if (query.source !== undefined) {
|
|
25
|
+
match.push(`EXISTS (SELECT 1 FROM task_records r WHERE r.task_id = facts.id AND
|
|
26
|
+
json_extract(r.payload, '$.source.kind') ${query.source === "job" ? "= 'job-run'" : "IS NULL"})`);
|
|
27
|
+
}
|
|
24
28
|
if (query.project !== undefined) {
|
|
25
29
|
match.push(`EXISTS (SELECT 1 FROM task_records r, json_each(r.payload, '$.projectBindings') p
|
|
26
30
|
WHERE r.task_id = facts.id AND json_extract(p.value, '$.projectId') = @project)`);
|
|
@@ -57,6 +61,10 @@ export function queryTaskCatalog(db, query) {
|
|
|
57
61
|
after.push("(createdAt, id) > (@afterDate, @afterId)");
|
|
58
62
|
}
|
|
59
63
|
const count = (table, condition) => `(SELECT count(*) FROM ${table} x WHERE x.task_id = c.task_id AND (${condition}))`;
|
|
64
|
+
const incoming = (statuses) => `(SELECT count(*) FROM global_job_runs x
|
|
65
|
+
WHERE json_extract(x.payload,'$.delegation.taskId') = c.task_id AND x.status IN (${statuses}))
|
|
66
|
+
+ (SELECT count(*) FROM durable_jobs x WHERE x.task_id <> c.task_id
|
|
67
|
+
AND json_extract(x.payload,'$.delegation.taskId') = c.task_id AND x.status IN (${statuses}))`;
|
|
60
68
|
const displaySummary = `CASE
|
|
61
69
|
WHEN page.status IN ('completed','archived')
|
|
62
70
|
AND json_extract(r.payload,'$.completionSummary') IS NOT NULL
|
|
@@ -76,8 +84,8 @@ export function queryTaskCatalog(db, query) {
|
|
|
76
84
|
${count("work_items", "1=1")} AS workItems,
|
|
77
85
|
${count("turns", "status = 'active'")} AS activeRuns,
|
|
78
86
|
${count("input_requests", "status = 'open'")} AS openInputs,
|
|
79
|
-
${count("durable_jobs", "status IN ('queued','running')")} AS pendingOperations,
|
|
80
|
-
${count("durable_jobs", "status = 'unknown-needs-attention'")} AS unknownOperations,
|
|
87
|
+
${count("durable_jobs", "status IN ('queued','running')")} + ${incoming("'queued','running'")} AS pendingOperations,
|
|
88
|
+
${count("durable_jobs", "status = 'unknown-needs-attention'")} + ${incoming("'unknown-needs-attention'")} AS unknownOperations,
|
|
81
89
|
CASE WHEN c.status IN ('active','draft') THEN
|
|
82
90
|
${count("turns", "status = 'active'")} +
|
|
83
91
|
${count("work_items", "status = 'open' OR (status = 'accepted' AND json_extract(payload, '$.currentExecutionGroupId') IS NOT NULL)")} +
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { processBusinessResult } from "../../job/jobBusinessResult.js";
|
|
2
|
+
import { validateDurableJob } from "../../job/durableJob.js";
|
|
3
|
+
/** Preserve raw history and derive only what each original result actually proves. */
|
|
4
|
+
export function migrateJobBusinessResults(db) {
|
|
5
|
+
for (const table of ["durable_jobs", "global_job_runs"]) {
|
|
6
|
+
const rows = db.prepare(`SELECT rowid, payload FROM ${table}`).all();
|
|
7
|
+
for (const row of rows) {
|
|
8
|
+
const run = JSON.parse(row.payload);
|
|
9
|
+
if (run.businessResult !== undefined)
|
|
10
|
+
throw new Error("Unexpected business result in storage 1.14.");
|
|
11
|
+
validateDurableJob(run);
|
|
12
|
+
if (!run.result)
|
|
13
|
+
continue;
|
|
14
|
+
const migrated = validateDurableJob({ ...run, businessResult: processBusinessResult(run, run.result) });
|
|
15
|
+
db.prepare("INSERT INTO storage_migration_archive(migration_version,family,record_key,payload) VALUES(?,?,?,?)")
|
|
16
|
+
.run(15, table, `${"taskId" in run ? run.taskId : "global"}/${run.id}`, row.payload);
|
|
17
|
+
db.prepare(`UPDATE ${table} SET payload=? WHERE rowid=?`).run(JSON.stringify(migrated), row.rowid);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { validateSchedule } from "../../schedule/schedule.js";
|
|
2
|
+
/** Keep valid historical Session authority; never infer a service grant. */
|
|
3
|
+
export function migrateScheduleDispatch(db) {
|
|
4
|
+
const rows = db.prepare("SELECT rowid,scope_key,schedule_id,payload FROM schedules").all();
|
|
5
|
+
for (const row of rows) {
|
|
6
|
+
const old = JSON.parse(row.payload);
|
|
7
|
+
if (old.target !== undefined || old.authorization?.kind !== undefined)
|
|
8
|
+
throw new Error("Unexpected Schedule shape in storage 1.15.");
|
|
9
|
+
const next = validateSchedule({ ...old, target: { kind: "existing-job" },
|
|
10
|
+
authorization: { ...old.authorization, kind: "session" } });
|
|
11
|
+
db.prepare("INSERT INTO storage_migration_archive(migration_version,family,record_key,payload) VALUES(?,?,?,?)")
|
|
12
|
+
.run(16, "schedules", `${row.scope_key}/${row.schedule_id}`, row.payload);
|
|
13
|
+
db.prepare("UPDATE schedules SET payload=? WHERE rowid=?").run(JSON.stringify(next), row.rowid);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -23,6 +23,9 @@ export function archiveSettlementChecks(store, task) {
|
|
|
23
23
|
for (const job of store.listDurableJobs(task.id).filter(j => ["queued", "running"].includes(j.status) || (j.status === "unknown-needs-attention" && j.acknowledgedAt === undefined))) {
|
|
24
24
|
add(`job:${task.id}/${job.id}`, "active-durable-job", "Task has an active or unacknowledged DurableJob.", "settled job", job.status, `yui job get --task ${task.id} --job ${job.id}`);
|
|
25
25
|
}
|
|
26
|
+
for (const job of store.listAgentJobRuns({ kind: "task", taskId: task.id }).filter(run => !run.businessResult)) {
|
|
27
|
+
add(`job:${task.id}/${job.id}`, "active-durable-job", "Agent JobRun has no explicit business result.", "settled job", job.status, `yui job run-context --task ${task.id} --job ${job.definitionId} --run ${job.id}`);
|
|
28
|
+
}
|
|
26
29
|
return checks;
|
|
27
30
|
}
|
|
28
31
|
/** Unknown execution is never permission to remove workspaces, even after
|
|
@@ -40,6 +43,18 @@ export function archiveExecutionChecks(store, taskId, home, phase = "before-remo
|
|
|
40
43
|
if (blocker !== undefined)
|
|
41
44
|
add(`job:${taskId}/${job.id}`, "unresolved-execution", blocker, job.status, `yui job get --task ${taskId} --job ${job.id}`);
|
|
42
45
|
}
|
|
46
|
+
for (const job of store.listAgentJobRuns({ kind: "task", taskId })) {
|
|
47
|
+
const last = job.attempts.at(-1);
|
|
48
|
+
if (!last.terminal)
|
|
49
|
+
add(`job:${taskId}/${job.id}`, "unresolved-execution", "Agent Job input has no exact native terminal; business reporting alone is not quiescence.", job.status, `yui job run-evidence --task ${taskId} --job ${job.definitionId} --run ${job.id}`);
|
|
50
|
+
}
|
|
51
|
+
for (const role of store.listGlobalRoles().filter(role => role.jobOwner?.scope.kind === "task" && role.jobOwner.scope.taskId === taskId)) {
|
|
52
|
+
const binding = store.getGlobalRoleSessionSet(role.name)?.providerBinding;
|
|
53
|
+
if (binding?.run && !["completed", "failed", "cancelled", "rejected"].includes(binding.run.status)
|
|
54
|
+
|| hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name })))) {
|
|
55
|
+
add(`global-role:${role.name}`, "unresolved-provider-input", "Task-owned Job Role retains an unresolved input or lifecycle operation.", binding?.run?.status ?? "lifecycle pending", `yui job session-inspect --task ${taskId} --job ${role.jobOwner.jobId}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
43
58
|
for (const role of store.listRoles(taskId)) {
|
|
44
59
|
const resource = `role:${taskId}/${role.name}`;
|
|
45
60
|
const action = `yui task role session inspect ${taskId} ${role.name}`;
|
|
@@ -81,6 +81,13 @@ export function projectCompletionReadiness(facts) {
|
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
// Active DurableJobs (integration checks, etc.) must settle.
|
|
84
|
+
for (const job of facts.agentJobs ?? []) {
|
|
85
|
+
if (job.businessResult)
|
|
86
|
+
continue;
|
|
87
|
+
blockers.push({ code: "active-durable-job", ref: ref("durable-job", job.id),
|
|
88
|
+
reason: `Agent JobRun ${job.id} has no explicit business result.`,
|
|
89
|
+
fix: `inspect yui job run-context --task ${facts.task.id} --job ${job.definitionId} --run ${job.id}; settle its original input without replay` });
|
|
90
|
+
}
|
|
84
91
|
for (const job of facts.durableJobs) {
|
|
85
92
|
if (!ACTIVE_JOB_STATUSES.has(job.status))
|
|
86
93
|
continue;
|
package/dist/task/task.js
CHANGED
|
@@ -400,7 +400,7 @@ export function validateTask(task) {
|
|
|
400
400
|
if (task.schemaVersion !== 1)
|
|
401
401
|
throw new Error("Task must use schemaVersion 1.");
|
|
402
402
|
const fields = [
|
|
403
|
-
"schemaVersion", "id", "title", "type", "description", "priority", "tags", "dueAt",
|
|
403
|
+
"schemaVersion", "source", "id", "title", "type", "description", "priority", "tags", "dueAt",
|
|
404
404
|
"projectBindings", "cwd", "workspaceIdentity", "status", "executionGate", "activationRequest",
|
|
405
405
|
"settledActivationRequests", "completedAt", "completedBy", "completionSummary", "completionArtifactRefs",
|
|
406
406
|
"retiredAt", "retiredBy", "retirementSummary", "retirementIsolation", "replacementTaskId",
|
|
@@ -411,6 +411,22 @@ export function validateTask(task) {
|
|
|
411
411
|
throw new Error(`Task contains an unsupported field: ${field}.`);
|
|
412
412
|
}
|
|
413
413
|
requireSafeIdentity(task.id, "Task id");
|
|
414
|
+
if (task.source !== undefined) {
|
|
415
|
+
const source = task.source;
|
|
416
|
+
if (source.kind !== "job-run" || Object.keys(source).some(key => !["kind", "jobId", "run"].includes(key))) {
|
|
417
|
+
throw new Error("Invalid Task source.");
|
|
418
|
+
}
|
|
419
|
+
requireSafeIdentity(source.jobId, "Source Job id");
|
|
420
|
+
requireSafeIdentity(source.run.runId, "Source Run id");
|
|
421
|
+
if (Object.keys(source.run).some(key => !["scope", "runId"].includes(key))
|
|
422
|
+
|| Object.keys(source.run.scope).some(key => !["kind", "taskId"].includes(key))
|
|
423
|
+
|| !["global", "task"].includes(source.run.scope.kind))
|
|
424
|
+
throw new Error("Invalid Task source Run.");
|
|
425
|
+
if (source.run.scope.kind === "task")
|
|
426
|
+
requireSafeIdentity(source.run.scope.taskId, "Source Task id");
|
|
427
|
+
else if ("taskId" in source.run.scope)
|
|
428
|
+
throw new Error("Global source cannot name a Task.");
|
|
429
|
+
}
|
|
414
430
|
requireText(task.title, "Task title");
|
|
415
431
|
if (task.type !== undefined)
|
|
416
432
|
requireSafeIdentity(task.type, "Task type");
|