@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.
Files changed (90) 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/callerDiscovery.js +18 -0
  7. package/dist/cli/commandCatalog.js +45 -14
  8. package/dist/cli/commandDiscovery.js +2 -3
  9. package/dist/commands/durableJobCommands.js +31 -6
  10. package/dist/commands/globalRoleCommands.js +5 -0
  11. package/dist/commands/scheduleCommands.js +14 -3
  12. package/dist/commands/taskCommands.js +5 -2
  13. package/dist/context/taskCatalog.js +6 -1
  14. package/dist/controlPlaneCli.js +12 -12
  15. package/dist/controller/agentJobControl.js +391 -0
  16. package/dist/controller/controller.js +24 -3
  17. package/dist/controller/fileSchedulerStoreAdapter.js +12 -1
  18. package/dist/controller/globalInputDelivery.js +8 -1
  19. package/dist/controller/jobArtifactControl.js +111 -0
  20. package/dist/controller/jobControl.js +4 -0
  21. package/dist/controller/jobDefinitionControl.js +191 -19
  22. package/dist/controller/jobSupervisor.js +83 -3
  23. package/dist/controller/jobTaskDelegationControl.js +193 -0
  24. package/dist/controller/runtime.js +7 -1
  25. package/dist/controller/scheduleControl.js +83 -20
  26. package/dist/integration/gitIntegrationService.js +2 -0
  27. package/dist/job/agentJob.js +131 -0
  28. package/dist/job/agentJobObservation.js +61 -0
  29. package/dist/job/agentRoleScope.js +33 -0
  30. package/dist/job/durableJob.js +63 -28
  31. package/dist/job/jobAssignmentScope.js +1 -0
  32. package/dist/job/jobBusinessResult.js +72 -0
  33. package/dist/job/jobCleanupInspection.js +4 -0
  34. package/dist/job/jobDefinition.js +26 -2
  35. package/dist/job/jobRunner.js +4 -3
  36. package/dist/job/projectRefreshRunner.js +54 -18
  37. package/dist/job/projectSync.js +162 -0
  38. package/dist/job/scheduleService.js +55 -0
  39. package/dist/job/taskDelegation.js +48 -0
  40. package/dist/message/messageContinuation.js +1 -1
  41. package/dist/resources/projectResourceService.js +1 -1
  42. package/dist/resources/resourceGc.js +1 -1
  43. package/dist/role/role.js +22 -12
  44. package/dist/runtime/managedIdentity.js +5 -3
  45. package/dist/runtime/sessionCaller.js +9 -10
  46. package/dist/schedule/schedule.js +65 -15
  47. package/dist/scheduler/wakeReason.js +4 -0
  48. package/dist/storage/agentJobSchema.js +10 -0
  49. package/dist/storage/maintenanceCaller.js +17 -25
  50. package/dist/storage/nativeSessionLookup.js +23 -0
  51. package/dist/storage/recordValidation.js +2 -0
  52. package/dist/storage/sessionHistorySchema.js +17 -0
  53. package/dist/storage/sqliteSchema.js +96 -11
  54. package/dist/storage/sqliteStore.js +71 -4
  55. package/dist/storage/storageVersions.js +1 -1
  56. package/dist/storage/storeRpc.js +4 -0
  57. package/dist/storage/taskCatalog.js +10 -2
  58. package/dist/storage/upgrade/jobBusinessResultMigration.js +20 -0
  59. package/dist/storage/upgrade/scheduleDispatchMigration.js +15 -0
  60. package/dist/task/archivePreflight.js +15 -0
  61. package/dist/task/completionReadiness.js +7 -0
  62. package/dist/task/task.js +17 -1
  63. package/dist/verification/verificationGateService.js +4 -0
  64. package/dist/web/assets/client/app/catalog.js +16 -0
  65. package/dist/web/assets/client/i18n/en.js +29 -2
  66. package/dist/web/assets/client/i18n/zh-CN.js +29 -2
  67. package/dist/web/assets/client/layout/manageShell.js +9 -0
  68. package/dist/web/assets/client/layout/router.js +1 -0
  69. package/dist/web/assets/client/views/jobs.js +166 -37
  70. package/dist/web/assets/client/views/task/files.js +5 -4
  71. package/dist/web/assets/client/views/task/page.js +5 -1
  72. package/dist/web/taskPreviews.js +2 -2
  73. package/dist/web/webJobs.js +36 -11
  74. package/dist/web/webMaterials.js +21 -9
  75. package/dist/web/webServer.js +6 -5
  76. package/dist/web/webTaskSurface.js +1 -0
  77. package/docs/authorization.md +130 -0
  78. package/docs/job-runs.md +392 -17
  79. package/docs/managed-turn-and-session-runtime.md +10 -5
  80. package/docs/managed-turn-and-session-runtime.zh-CN.md +12 -5
  81. package/docs/release-workflow.md +79 -0
  82. package/docs/release-workflow.zh-CN.md +58 -0
  83. package/docs/schedules.md +69 -15
  84. package/docs/storage-baseline.md +45 -1
  85. package/docs/storage-baseline.zh-CN.md +33 -1
  86. package/docs/testing/verification-levels.md +2 -2
  87. package/docs/testing/verification-levels.zh-CN.md +2 -2
  88. package/package.json +1 -1
  89. package/skills/yui-runtime/SKILL.md +12 -5
  90. 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
- * Maintenance must authenticate before migration, without opening a current-only
11
- * business Store. The declared 1.x migrations preserve these Role/Session
12
- * envelopes and physical tables. Read only those records in one SQLite snapshot;
13
- * this grants no old-format business reads, writes or implicit migration.
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
- getTaskRoleSessionSet: (task, role) => read("role_session_sets", "task_id=? AND role_name=?", [task, role]),
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; // Doctor keeps normal scoped authorization below.
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 CURRENT_SCHEMA_SQL = BASELINE_SCHEMA_SQL + JOB_SCHEMA_SQL + SCHEDULE_SCHEMA_SQL;
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: CURRENT_SCHEMA_CHECKSUM,
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: CURRENT_SCHEMA_CHECKSUM,
129
- targetChecksum: CURRENT_SCHEMA_CHECKSUM,
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: CURRENT_SCHEMA_CHECKSUM,
142
- targetChecksum: CURRENT_SCHEMA_CHECKSUM,
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: CURRENT_SCHEMA_CHECKSUM,
152
- targetChecksum: CURRENT_SCHEMA_CHECKSUM,
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 === JOB_SCHEMA_CHECKSUM ? BASELINE_SCHEMA_SQL + JOB_SCHEMA_SQL
187
- : 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;
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.#db.prepare("DELETE FROM global_role_session_sets WHERE name = ?").run(role.name);
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
- this.#db.prepare("DELETE FROM global_role_session_sets WHERE name = ?").run(name);
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
- this.#db.prepare("DELETE FROM role_session_sets WHERE task_id = ? AND role_name = ?").run(taskId, name);
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
@@ -1,4 +1,4 @@
1
- export const CURRENT_STORAGE_VERSION = "1.13";
1
+ export const CURRENT_STORAGE_VERSION = "1.20";
2
2
  export const MIN_SUPPORTED_STORAGE_VERSION = "1.0";
3
3
  export const STORAGE_FORMAT = "yui-home";
4
4
  export function isStorageVersion(value) {
@@ -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");