@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
@@ -3,13 +3,13 @@ import { callFileTaskController } from "../controller/clientRuntime.js";
3
3
  import { resolveJobCaller } from "../task/taskAuthority.js";
4
4
  export async function runScheduleCommand(args, options) {
5
5
  const [op, ...rest] = args;
6
- if (op === undefined || !["create", "list", "show", "update", "enable", "disable"].includes(op)) {
7
- throw usageError("Schedule command must be create, list, show, update, enable, or disable.");
6
+ if (op === undefined || !["create", "list", "show", "update", "enable", "disable", "revoke"].includes(op)) {
7
+ throw usageError("Schedule command must be create, list, show, update, enable, disable, or revoke.");
8
8
  }
9
9
  const flags = new Map();
10
10
  for (let i = 0; i < rest.length; i++) {
11
11
  const flag = rest[i];
12
- if (!["--global", "--task", "--schedule", "--job", "--rule", "--revision"].includes(flag) || flags.has(flag)) {
12
+ if (!["--global", "--task", "--schedule", "--job", "--target", "--authorization", "--rule", "--revision"].includes(flag) || flags.has(flag)) {
13
13
  throw usageError(`Unknown or repeated Schedule flag: ${flag}.`);
14
14
  }
15
15
  if (flag === "--global") {
@@ -25,6 +25,7 @@ export async function runScheduleCommand(args, options) {
25
25
  throw usageError("Choose exactly one of --global or --task.");
26
26
  const taskId = flags.get("--task");
27
27
  let rule;
28
+ let target;
28
29
  if (flags.has("--rule")) {
29
30
  try {
30
31
  rule = JSON.parse(flags.get("--rule"));
@@ -33,12 +34,22 @@ export async function runScheduleCommand(args, options) {
33
34
  throw usageError("--rule must be JSON.");
34
35
  }
35
36
  }
37
+ if (flags.has("--target")) {
38
+ try {
39
+ target = JSON.parse(flags.get("--target"));
40
+ }
41
+ catch {
42
+ throw usageError("--target must be JSON.");
43
+ }
44
+ }
36
45
  const result = await callFileTaskController(options.home, "schedule.control", {
37
46
  op, scope: taskId === undefined ? { kind: "global" } : { kind: "task", taskId },
38
47
  caller: resolveJobCaller(options.environment, taskId ?? ""),
39
48
  ...(flags.has("--schedule") ? { id: flags.get("--schedule") } : {}),
40
49
  ...(flags.has("--job") ? { jobId: flags.get("--job") } : {}),
41
50
  ...(rule === undefined ? {} : { rule }),
51
+ ...(target === undefined ? {} : { target }),
52
+ ...(flags.has("--authorization") ? { authorization: flags.get("--authorization") } : {}),
42
53
  ...(flags.has("--revision") ? { expectedRevision: Number(flags.get("--revision")) } : {})
43
54
  }, { environment: options.environment });
44
55
  return result;
@@ -937,8 +937,9 @@ function parseTaskCreation(args, store) {
937
937
  defaultProjectIds
938
938
  };
939
939
  }
940
- function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []) {
941
- const task = createTask(store.nextTaskId(), title, now, metadata);
940
+ export function createTaskAggregate(store, title, metadata, now, defaultProjectIds = [], source) {
941
+ const task = { ...createTask(store.nextTaskId(), title, now, metadata),
942
+ ...(source === undefined ? {} : { source: structuredClone(source) }) };
942
943
  const leader = createTaskRole(store, task, LEADER_ROLE, undefined, now);
943
944
  store.saveTask(task);
944
945
  store.saveRole(task.id, leader);
@@ -995,6 +996,7 @@ function showTaskCommand(args, store, currentTaskCandidate) {
995
996
  `Title: ${task.title}`,
996
997
  `Status: ${task.status}`,
997
998
  `Type: ${task.type ?? "unspecified"}`,
999
+ `Source: ${task.source === undefined ? "ordinary" : `Job ${task.source.jobId} / Run ${task.source.run.runId}`}`,
998
1000
  ...(task.description === undefined ? [] : [`Description: ${task.description}`]),
999
1001
  ...(task.priority === undefined ? [] : [`Priority: ${task.priority}`]),
1000
1002
  ...(task.tags === undefined ? [] : [`Tags: ${task.tags.join(", ")}`]),
@@ -1036,6 +1038,7 @@ function showTaskCommand(args, store, currentTaskCandidate) {
1036
1038
  ].join("\n").concat("\n");
1037
1039
  return output(rendered, {
1038
1040
  task,
1041
+ delegations: store.listTaskDelegations(task.id),
1039
1042
  counts,
1040
1043
  hasBrief: brief !== null,
1041
1044
  remoteDelivery,
@@ -15,7 +15,7 @@ export function parseTaskCatalogOptions(args) {
15
15
  all = true;
16
16
  continue;
17
17
  }
18
- if (!["--limit", "--cursor", "--status", "--project", "--search", "--attention"].includes(key)
18
+ if (!["--limit", "--cursor", "--status", "--project", "--search", "--attention", "--source"].includes(key)
19
19
  || values.has(key) || args[i + 1] === undefined || args[i + 1].startsWith("--")) {
20
20
  throw usageError("Task list expects [--all] [--status <status>] [--project <id>] [--search <text>] [--attention <category>] [--limit <1..100>] [--cursor <cursor>].");
21
21
  }
@@ -26,6 +26,9 @@ export function parseTaskCatalogOptions(args) {
26
26
  throw usageError("Unknown Task catalog status.");
27
27
  }
28
28
  const attention = values.get("--attention");
29
+ const source = values.get("--source");
30
+ if (source !== undefined && !["ordinary", "job"].includes(source))
31
+ throw usageError("Task source must be ordinary or job.");
29
32
  if (attention !== undefined && !CATALOG_ATTENTION.includes(attention)) {
30
33
  throw usageError(`Task catalog attention must be one of: ${CATALOG_ATTENTION.join(", ")}.`);
31
34
  }
@@ -42,6 +45,7 @@ export function parseTaskCatalogOptions(args) {
42
45
  ...(status === undefined ? {} : { status: status }),
43
46
  ...(attention === undefined ? {} : { attention: attention }),
44
47
  ...(search ? { search } : {}),
48
+ ...(source === undefined ? {} : { source: source }),
45
49
  ...(project === undefined ? {} : { project }),
46
50
  ...(values.has("--cursor") ? { cursor: values.get("--cursor") } : {})
47
51
  };
@@ -121,6 +125,7 @@ export function readTaskCatalog(store, options, environment = {}) {
121
125
  // only when necessary; length-at-limit is conservatively marked omitted.
122
126
  const item = {
123
127
  id: row.id, title, status: row.status, createdAt: row.createdAt, updatedAt: row.updatedAt,
128
+ source: reader.getTask(row.id)?.source ?? null,
124
129
  summary, summaryStatus: row.summaryPresent ? "available" : "missing",
125
130
  omitted: { title: row.title.length >= 256 || title !== row.title,
126
131
  summary: row.summary !== null && (row.summary.length >= 512 || summary !== row.summary) },
@@ -13,6 +13,7 @@ import { listArtifactsCapability, readArtifactCapability, saveArtifactCapability
13
13
  import { renderAgentConfigurationResolutionNotice } from "./cli/agentConfigurationPicker.js";
14
14
  import { describeCommandTree, findCommandNode } from "./cli/commandCatalog.js";
15
15
  import { discoveryCommandTree } from "./cli/commandDiscovery.js";
16
+ import { callerDiscoveryAudience } from "./cli/callerDiscovery.js";
16
17
  import { contextRecordReader } from "./context/taskContext.js";
17
18
  import { renderCompletion } from "./cli/completion.js";
18
19
  import { runCompletionWizard } from "./cli/completionWizard.js";
@@ -23,7 +24,8 @@ import { routeInvocation } from "./cli/invocationRouter.js";
23
24
  import { operatorOfflineCommand, taskDiagnosticTarget } from "./cli/managedDiagnostics.js";
24
25
  import { assertConfigurationAuthority, assertTaskInvocationScope } from "./cli/invocationAuthority.js";
25
26
  import { requireManagedGlobalCaller, requireManagedGlobalReader, ManagedRuntimeDriftError } from "./runtime/managedCaller.js";
26
- import { sessionCallerEnvironment, requireManagedCaller } from "./runtime/sessionCaller.js";
27
+ import { assertJobAgentCommand } from "./job/agentRoleScope.js";
28
+ import { requireManagedCaller } from "./runtime/sessionCaller.js";
27
29
  import { maintenanceCallerEnvironment } from "./storage/maintenanceCaller.js";
28
30
  import { checkCurrentSkills, SKILL_REUSE_RULE } from "./context/currentSkills.js";
29
31
  import { roleSkillIds } from "./context/roleSessionContext.js";
@@ -165,7 +167,7 @@ function failureContext() {
165
167
  }
166
168
  export async function main() {
167
169
  const home = resolveYuiHome(process.env);
168
- const discovery = discoveryCommandTree(process.env);
170
+ const discovery = discoveryCommandTree(process.env, callerDiscoveryAudience(home, process.env));
169
171
  invocationHome = home;
170
172
  const routedForFence = args.length === 0 ? undefined : routeInvocation(args);
171
173
  const homeFreeInvocation = args.length === 0
@@ -1985,12 +1987,13 @@ export async function main() {
1985
1987
  return;
1986
1988
  }
1987
1989
  if (resolved[0] === "job") {
1988
- emit(await runDurableJobCommand(resolved.slice(1), {
1990
+ const result = await runDurableJobCommand(resolved.slice(1), {
1989
1991
  home,
1990
1992
  json: jsonOutput,
1991
1993
  environment: process.env,
1992
1994
  store
1993
- }));
1995
+ });
1996
+ emit(result, false, jsonOutput ? JSON.parse(result) : undefined);
1994
1997
  return;
1995
1998
  }
1996
1999
  if (resolved[0] === "telemetry") {
@@ -2043,13 +2046,7 @@ async function preflightManagedTaskControlPlane() {
2043
2046
  // Home only, and never creates a Session or mutates its registration.
2044
2047
  if ((process.env.CODEX_THREAD_ID !== undefined || process.env.YUI_NATIVE_SESSION_ID !== undefined)
2045
2048
  && (process.env.YUI_SESSION_SCOPE === undefined || process.env.YUI_ROLE === undefined)) {
2046
- const registry = openCurrentTaskStore(resolveYuiHome(process.env));
2047
- try {
2048
- Object.assign(process.env, sessionCallerEnvironment(registry, process.env));
2049
- }
2050
- finally {
2051
- registry.close();
2052
- }
2049
+ Object.assign(process.env, maintenanceCallerEnvironment(resolveYuiHome(process.env), process.env, false));
2053
2050
  }
2054
2051
  if (process.env.YUI_SESSION_SCOPE !== "task") {
2055
2052
  if (process.env.YUI_SESSION_SCOPE === "global") {
@@ -2148,6 +2145,9 @@ async function preflightManagedGlobalControlPlane() {
2148
2145
  requireManagedGlobalReader(verifiedStore, process.env);
2149
2146
  else
2150
2147
  requireManagedGlobalCaller(verifiedStore, process.env);
2148
+ const role = verifiedStore.getGlobalRole(process.env.YUI_ROLE);
2149
+ if (role)
2150
+ assertJobAgentCommand(role, args);
2151
2151
  }
2152
2152
  catch (error) {
2153
2153
  verifiedStore.close();
@@ -2700,7 +2700,7 @@ async function completionCommand(home, node) {
2700
2700
  const candidates = await resolveCompletionCandidates({
2701
2701
  current: prefix,
2702
2702
  words: args.slice(separator + 1),
2703
- root: discoveryCommandTree(process.env),
2703
+ root: discoveryCommandTree(process.env, callerDiscoveryAudience(home, process.env)),
2704
2704
  ports: { call: async (method, params) => {
2705
2705
  if (!selection) {
2706
2706
  assertCliHomeReleaseFence({
@@ -0,0 +1,391 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdirSync } from "node:fs";
3
+ import { isDeepStrictEqual } from "node:util";
4
+ import { join } from "node:path";
5
+ import { jobScopeKey } from "../job/jobDefinition.js";
6
+ import { jobAgentRoleName } from "../job/agentJob.js";
7
+ import { validateBusinessResult } from "../job/jobBusinessResult.js";
8
+ import { assertCallerAuthorized } from "./jobControl.js";
9
+ import { authorizeScope } from "./jobDefinitionControl.js";
10
+ import { createGlobalRole, createRoleAgentBinding } from "../role/role.js";
11
+ import { createTaskMessage, createGlobalRoleMessage } from "../message/message.js";
12
+ import { messageAssignmentBlocker } from "../message/messageContinuation.js";
13
+ import { enqueueWork } from "../coordination/workMailboxQueue.js";
14
+ import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
15
+ import { requireManagedTaskCaller } from "../runtime/managedCaller.js";
16
+ import { assertRoleRuntimeMutationAllowed } from "../commands/roleRuntimeGuard.js";
17
+ import { prepareOperatorNewSession } from "../operator/operatorSessionHistory.js";
18
+ import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
19
+ import { jobAttemptOwnsInput, recordJobAgentObservation } from "../job/agentJobObservation.js";
20
+ import { assertJobRoleRunnable } from "../job/agentRoleScope.js";
21
+ import { providerRetryPending } from "../runtime/providerRetry.js";
22
+ export function authorizeAgentDefinition(store, job, caller, write) {
23
+ const config = job.configuration;
24
+ if (config.kind !== "agent")
25
+ throw new Error("Not an Agent Job.");
26
+ // Execution readers get only their selected Job. They cannot edit/dispatch it.
27
+ if (!write && caller.scope === "global" && caller.role !== "operator") {
28
+ assertCallerAuthorized(store, caller, "");
29
+ const owner = store.getGlobalRole(caller.role)?.jobOwner;
30
+ if (owner && owner.jobId === job.id && isDeepStrictEqual(owner.scope, job.scope))
31
+ return;
32
+ throw new Error("This Job Agent may read only its owning Job.");
33
+ }
34
+ if (!write && caller.scope === "task" && config.target.kind === "task"
35
+ && caller.taskId === config.target.taskId && caller.role === config.target.roleName) {
36
+ assertCallerAuthorized(store, caller, caller.taskId);
37
+ requireTaskTarget(store, config.target, false);
38
+ return;
39
+ }
40
+ authorizeScope(store, job.scope, caller, true);
41
+ if (caller.scope === "task" && caller.role !== "leader")
42
+ throw new Error("Only the Task Leader manages Agent Jobs.");
43
+ if (write)
44
+ requireWritableTaskManager(store, caller);
45
+ if (write && job.scope.kind === "task") {
46
+ const task = store.getTask(job.scope.taskId);
47
+ if (task?.status !== "active" || task.executionGate.state !== "enabled")
48
+ throw new Error("Agent Jobs require their owning Task to be active and execution-enabled.");
49
+ }
50
+ const target = config.target;
51
+ if (target.kind === "operator" && job.scope.kind !== "global")
52
+ throw new Error("Task Jobs cannot delegate to Operator.");
53
+ if (target.kind === "task") {
54
+ if (job.scope.kind === "task" && job.scope.taskId !== target.taskId)
55
+ throw new Error("Task Agent target escapes Job Task.");
56
+ if (write)
57
+ requireTaskTarget(store, target);
58
+ }
59
+ if (write && target.kind === "job" && !store.getConfiguredAgent(target.agentId))
60
+ throw new Error("Job Agent is not configured.");
61
+ }
62
+ function requireTaskTarget(store, target, runnable = true) {
63
+ const task = store.getTask(target.taskId);
64
+ if (!task || runnable && (task.status !== "active" || task.executionGate.state !== "enabled"))
65
+ throw new Error("Task Agent requires an active, execution-enabled Task.");
66
+ if (!store.getRole(target.taskId, target.roleName))
67
+ throw new Error("Task Agent Role is unavailable.");
68
+ const recipient = taskRecipient(store, target);
69
+ if (recipient.ownerRunId) {
70
+ const owner = store.getRun(target.taskId, recipient.ownerRunId);
71
+ // Validate a proposed recipient with the shared Assignment guard. This
72
+ // temporary value is never stored or delivered; continuations of the same
73
+ // Assignment remain valid, but a redispatch/workspace/owner change does not.
74
+ const blocker = messageAssignmentBlocker(store, { taskId: target.taskId,
75
+ createdAt: owner.createdAt, recipient });
76
+ if (blocker && (runnable || !["execution-disabled", `task-${task.status}`].includes(blocker))) {
77
+ throw new Error(`Task Agent Assignment unavailable: ${blocker}.`);
78
+ }
79
+ }
80
+ }
81
+ function taskRecipient(store, target) {
82
+ if (target.roleName === "leader" && !target.ownerRunId)
83
+ return { roleName: "leader" };
84
+ const owner = target.ownerRunId ? store.getRun(target.taskId, target.ownerRunId) : null;
85
+ if (!owner || owner.roleName !== target.roleName)
86
+ throw new Error("Task Agent requires its exact existing Assignment.");
87
+ if (!owner.workItemId && !owner.reviewRoundId)
88
+ throw new Error("Task Agent target has no existing Assignment scope.");
89
+ return { roleName: target.roleName, ownerRunId: owner.id,
90
+ ...(owner.workItemId ? { workItemId: owner.workItemId } : {}),
91
+ ...(owner.reviewRoundId ? { reviewRoundId: owner.reviewRoundId } : {}) };
92
+ }
93
+ function requireWritableTaskManager(store, caller) {
94
+ if (caller.scope !== "task")
95
+ return;
96
+ const current = requireManagedTaskCaller(store, { YUI_SESSION_SCOPE: "task", YUI_TASK_ID: caller.taskId,
97
+ YUI_ROLE: caller.role, YUI_NATIVE_SESSION_ID: caller.nativeSessionId });
98
+ const set = store.getTaskRoleSessionSet(current.taskId, current.roleName);
99
+ if (current.executionAuthority !== "delivery"
100
+ || set?.sessions[current.agentId]?.effective.profileAccess !== "write") {
101
+ throw new Error("Agent Job management requires a current writable delivery Session.");
102
+ }
103
+ }
104
+ function requireManager(store, job, caller, write = true) {
105
+ authorizeScope(store, job.scope, caller, true);
106
+ if (caller.scope === "task" && caller.role !== "leader")
107
+ throw new Error("Only the Task Leader manages Agent Jobs.");
108
+ if (write)
109
+ requireWritableTaskManager(store, caller);
110
+ }
111
+ function ownedRole(store, home, job, runId, now) {
112
+ if (job.configuration.kind !== "agent" || job.configuration.target.kind !== "job")
113
+ throw new Error("Not a Job-owned Agent.");
114
+ const isolated = job.configuration.concurrency === "isolated";
115
+ const name = jobAgentRoleName(job.scope, job.id, isolated ? runId : undefined);
116
+ const workspace = join(home, "workspaces", "job-agents", name);
117
+ const jobOwner = { scope: job.scope, jobId: job.id, ...(isolated ? { runId } : {}) };
118
+ const existing = store.getGlobalRole(name);
119
+ if (existing) {
120
+ if (!isDeepStrictEqual(existing.jobOwner, jobOwner) || existing.activeAgentId !== job.configuration.target.agentId) {
121
+ throw new Error("Job Role binding changed; inspect its Session/configuration before dispatch.");
122
+ }
123
+ }
124
+ else {
125
+ const agent = store.getConfiguredAgent(job.configuration.target.agentId);
126
+ if (!agent)
127
+ throw new Error("Job Agent is not configured.");
128
+ const role = createGlobalRole(name, [createRoleAgentBinding(agent)], agent.id, workspace, now, {
129
+ description: `Execution Agent owned by Job ${jobScopeKey(job.scope)}/${job.id}.`,
130
+ responsibilities: ["Read the exact JobRun context from its original queued message. Preserve its input, results and evidence independently of this conversation."],
131
+ constraints: ["No Operator authority, Task creation/delegation authority, or implicit Project write access.",
132
+ "Delivery and native Turn completion are not Job business completion. Report only the exact claimed Run.",
133
+ "On cancellation stop this Run's work only. Never replay unknown effects; continuation preserves the same Run."],
134
+ expectedOutput: "Use job run-claim and run-context, then job run-report for an explicit business result; acknowledge the original inbox message with its concrete disposition."
135
+ }, "write");
136
+ store.createGlobalRoleIfAbsent({ ...role, jobOwner });
137
+ }
138
+ mkdirSync(workspace, { recursive: true });
139
+ return { scope: "global", roleName: name };
140
+ }
141
+ function queueAttempt(store, home, job, runId, index, now) {
142
+ if (job.configuration.kind !== "agent")
143
+ throw new Error("Not an Agent Job.");
144
+ const target = job.configuration.target;
145
+ const recipient = target.kind === "operator" ? { scope: "global", roleName: "operator" }
146
+ : target.kind === "task" ? { scope: "task", taskId: target.taskId, roleName: target.roleName,
147
+ ...(target.ownerRunId === undefined ? {} : { ownerRunId: target.ownerRunId }) }
148
+ : ownedRole(store, home, job, runId, now);
149
+ const selector = `${job.scope.kind === "global" ? "--global" : `--task ${job.scope.taskId}`} --job ${job.id} --run ${runId}`;
150
+ const body = `JobRun ${runId}, continuation ${index}. Read durable input, not this notification as requirements.\n`
151
+ + `Claim this exact call: yui job run-claim ${selector} --json\n`
152
+ + `Read full context: yui job run-context ${selector} --json\n`
153
+ + `Report explicit business outcome with yui job run-report ${selector} --result '<JobBusinessResult JSON>' --json.\n`
154
+ + "Do not replay unknown effects or treat Turn completion as success. Preserve existing Role/Task/Assignment/resource boundaries. Check cancellation before effects.";
155
+ const inputControl = { action: "queue", requestId: `job-agent:${runId}:${index}` };
156
+ let messageId;
157
+ if (recipient.scope === "global") {
158
+ messageId = store.nextGlobalRoleMessageId();
159
+ let message = createGlobalRoleMessage(messageId, recipient.roleName, body, "system", { type: "system" }, now, { inputControl });
160
+ const set = store.getGlobalRoleSessionSet(recipient.roleName);
161
+ const session = set?.sessions[set.activeAgentId];
162
+ if (session)
163
+ message = { ...message, deliveryTarget: { agentId: session.agentId, nativeSessionId: session.nativeSessionId } };
164
+ store.saveGlobalRoleMessage(message);
165
+ }
166
+ else {
167
+ requireTaskTarget(store, { ...recipient });
168
+ const resolved = taskRecipient(store, recipient);
169
+ messageId = store.nextMessageId(recipient.taskId);
170
+ store.saveMessage(recipient.taskId, createTaskMessage(messageId, recipient.taskId, body, "system", { type: "system" }, now, { ...(resolved.ownerRunId ? { recipient: resolved } : {}), inputControl }));
171
+ enqueueWork(store, resolved.ownerRunId ? { kind: "task", taskId: recipient.taskId }
172
+ : { kind: "role", taskId: recipient.taskId, roleName: "leader" }, resolved.ownerRunId ? "message-continuation" : "job-agent-input", now, [{ type: "message", taskId: recipient.taskId, id: messageId }], { dedupeKey: `message:${recipient.taskId}:${messageId}` });
173
+ }
174
+ return { messageId, recipient, createdAt: now.toISOString() };
175
+ }
176
+ function sessionsFor(store, recipient) {
177
+ return recipient.scope === "global" ? store.getGlobalRoleSessionSet(recipient.roleName)
178
+ : store.getTaskRoleSessionSet(recipient.taskId, recipient.roleName);
179
+ }
180
+ /** Only an actual delivery of the original message can claim this execution. */
181
+ function currentExecution(store, run, caller) {
182
+ const attempt = run.attempts.at(-1);
183
+ const recipient = attempt.recipient;
184
+ if (caller.scope !== recipient.scope || caller.role !== recipient.roleName
185
+ || recipient.scope === "task" && caller.taskId !== recipient.taskId)
186
+ throw new Error("Caller is not this Run's execution target.");
187
+ assertCallerAuthorized(store, caller, recipient.scope === "task" ? recipient.taskId : "");
188
+ if (recipient.scope === "task")
189
+ requireTaskTarget(store, recipient);
190
+ else {
191
+ const role = store.getGlobalRole(recipient.roleName);
192
+ if (role)
193
+ assertJobRoleRunnable(store, role);
194
+ }
195
+ const binding = sessionsFor(store, recipient)?.providerBinding;
196
+ const turn = binding?.run;
197
+ if (!binding || !turn || turn.status !== "accepted" || !turn.nativeTurnId
198
+ || currentProviderConversation(binding).conversationId !== caller.nativeSessionId) {
199
+ throw new Error("An exact accepted native Turn is required; inspect the original delivery.");
200
+ }
201
+ if (!jobAttemptOwnsInput(store, attempt, binding))
202
+ throw new Error("Current Turn does not own the original Job message; unrelated work cannot claim this Run.");
203
+ return { nativeSessionId: caller.nativeSessionId, attemptId: turn.attemptId, nativeTurnId: turn.nativeTurnId };
204
+ }
205
+ function observeAttempt(store, run, now) {
206
+ const attempt = run.attempts.at(-1);
207
+ const binding = sessionsFor(store, attempt.recipient)?.providerBinding;
208
+ recordJobAgentObservation(store, attempt.recipient, binding, now);
209
+ return store.getAgentJobRun(run.id);
210
+ }
211
+ /** No transport or second scheduler: original Role messages own delivery and retry. */
212
+ export function controlAgentJob(store, home, job, input, now) {
213
+ const { caller, op } = input;
214
+ authorizeAgentDefinition(store, job, caller, false);
215
+ const history = () => store.listAgentJobRuns(job.scope, job.id).filter(run => {
216
+ const role = caller.scope === "global" ? store.getGlobalRole(caller.role ?? "") : null;
217
+ if (role?.jobOwner?.runId && role.jobOwner.runId !== run.id)
218
+ return false;
219
+ try {
220
+ authorizeAgentDefinition(store, { ...job, configuration: run.configuration }, caller, false);
221
+ return true;
222
+ }
223
+ catch {
224
+ return false;
225
+ }
226
+ });
227
+ if (op === "show")
228
+ return { ...job, results: summarizeAgentRuns(history()) };
229
+ if (op === "runs")
230
+ return history();
231
+ if (["session-inspect", "session-create", "session-stop", "session-new"].includes(op)) {
232
+ requireManager(store, job, caller, op !== "session-inspect");
233
+ if (job.configuration.kind !== "agent" || job.configuration.target.kind !== "job")
234
+ throw new Error("This Job uses a shared Role; inspect that Role's existing Session.");
235
+ if (job.configuration.concurrency === "isolated" && !history().some(run => run.id === input.runId))
236
+ throw new Error("Isolated Session operations require an exact existing Run.");
237
+ const roleName = jobAgentRoleName(job.scope, job.id, job.configuration.concurrency === "isolated" ? input.runId : undefined);
238
+ if (op === "session-create") {
239
+ authorizeAgentDefinition(store, job, caller, true);
240
+ ownedRole(store, home, job, input.runId ?? "", now);
241
+ }
242
+ const role = store.getGlobalRole(roleName);
243
+ if (op === "session-stop") {
244
+ const set = store.getGlobalRoleSessionSet(roleName);
245
+ const current = set?.sessions[set.activeAgentId];
246
+ if (!current || !input.expectedSessionId || current.nativeSessionId !== input.expectedSessionId)
247
+ throw new Error("Inspect and select the exact Job-owned Session before stopping.");
248
+ if (set?.providerBinding?.run && !["completed", "failed", "cancelled", "rejected"].includes(set.providerBinding.run.status)) {
249
+ throw new Error("Job Session has an active or unknown native Turn; cancel/settle that exact call before stopping.");
250
+ }
251
+ const target = runtimeLifecycleTarget({ scope: "global", roleName });
252
+ enqueueWork(store, target, RUNTIME_CLEANUP_REQUIRED_REASON, now, []);
253
+ return { status: "accepted", roleName, nativeSessionId: current.nativeSessionId,
254
+ boundary: "Cleanup requested, not yet proven stopped. Inspect the Session before replacement." };
255
+ }
256
+ if (op === "session-new") {
257
+ if (!role)
258
+ throw new Error("Create the Job Role first.");
259
+ assertRoleRuntimeMutationAllowed(store, { scope: "global", roleName }, "Job Session replacement");
260
+ const set = store.getGlobalRoleSessionSet(roleName);
261
+ const old = set?.sessions[set.activeAgentId];
262
+ if (!set || !old || !input.expectedSessionId || old.nativeSessionId !== input.expectedSessionId)
263
+ throw new Error("Job Session changed; inspect and supply its exact expectedSessionId.");
264
+ if (providerRetryPending(set.providerBinding))
265
+ throw new Error("Existing bounded Provider recovery owns this input; inspect its retry evidence before replacement.");
266
+ if (old.status !== "ended" || set.providerBinding?.run
267
+ && !["completed", "failed", "cancelled", "rejected"].includes(set.providerBinding.run.status))
268
+ throw new Error("Old Job Session is not proven stopped; preserve its writer and unknown effects.");
269
+ for (const pending of history().filter(run => !run.businessResult)) {
270
+ const observed = observeAttempt(store, pending, now);
271
+ if (!observed.attempts.at(-1)?.terminal)
272
+ throw new Error("Unsettled Job input prevents replacement; inspect its original delivery.");
273
+ }
274
+ // Shared Role lifecycle history, not a second conversation registry.
275
+ store.saveGlobalRoleSessionSet(prepareOperatorNewSession(set, role.activeAgentId, now));
276
+ }
277
+ return { role, sessions: role ? sessionsFor(store, { scope: "global", roleName }) : null,
278
+ operations: { enter: `yui session enter ${roleName}`, inspect: `yui config role show ${roleName}` },
279
+ boundary: "Use existing exact Role lifecycle operations. Replacement never proves an old Run stopped or authorizes replay." };
280
+ }
281
+ if (op === "run") {
282
+ requireManager(store, job, caller);
283
+ authorizeAgentDefinition(store, job, caller, true);
284
+ const trigger = input.trigger;
285
+ if (!trigger || !["manual", "retry"].includes(trigger.source) || !trigger.key.trim())
286
+ throw new Error("Agent Run requires a manual/retry trigger identity.");
287
+ const runs = history();
288
+ const same = runs.find(run => isDeepStrictEqual(run.trigger, trigger));
289
+ if (same) {
290
+ if (same.definitionRevision !== job.revision || same.retryOf !== input.retryOf)
291
+ throw new Error("Agent Job trigger conflicts with its original snapshot.");
292
+ return { run: same, created: false };
293
+ }
294
+ if (job.configuration.kind !== "agent")
295
+ throw new Error("Not an Agent Job.");
296
+ const occupied = runs.find(run => !run.businessResult || run.status === "unknown-needs-attention");
297
+ if (occupied && job.configuration.concurrency !== "isolated")
298
+ return { run: occupied, created: false, reason: "overlap" };
299
+ if (input.retryOf && !runs.some(run => run.id === input.retryOf
300
+ && ["failed", "cancelled", "timed-out"].includes(run.status)))
301
+ throw new Error("Retry requires an exact failed/cancelled/timed-out Agent Run.");
302
+ const id = `job-${randomUUID()}`;
303
+ const run = { schemaVersion: 1, executor: "agent", id, scope: job.scope,
304
+ definitionId: job.id, definitionRevision: job.revision, configuration: structuredClone(job.configuration),
305
+ trigger: structuredClone(trigger), ...(input.retryOf ? { retryOf: input.retryOf } : {}),
306
+ status: "queued", createdAt: now.toISOString(), updatedAt: now.toISOString(),
307
+ attempts: [queueAttempt(store, home, job, id, 0, now)] };
308
+ store.saveAgentJobRun(run);
309
+ return { run, created: true };
310
+ }
311
+ let run = history().find(run => run.id === input.runId);
312
+ if (!run)
313
+ throw new Error("Exact Agent JobRun not found.");
314
+ // Read historical execution under its original target, not a revised definition.
315
+ authorizeAgentDefinition(store, { ...job, configuration: run.configuration }, caller, false);
316
+ if (op === "run-get" || op === "run-context" || op === "run-result")
317
+ return {
318
+ ...run, ...(op === "run-context" ? { history: history().slice(-16).map(r => ({ id: r.id, status: r.status,
319
+ result: r.businessResult ? { outcome: r.businessResult.outcome, summary: r.businessResult.summary.slice(0, 512) } : null })),
320
+ historyRead: `yui job runs ${job.scope.kind === "global" ? "--global" : `--task ${job.scope.taskId}`} --job ${job.id} --json`,
321
+ authority: "Only the saved input within the selected Role's existing authority. No implicit Task/Project or external-resource grants." } : {})
322
+ };
323
+ if (op === "run-evidence")
324
+ return { runId: run.id, scope: run.scope, attempts: run.attempts,
325
+ delivery: run.attempts.map(attempt => attempt.recipient.scope === "global"
326
+ ? store.queryGlobalRoleMessages(attempt.recipient.roleName, { id: attempt.messageId, limit: 1 }).messages[0] ?? null
327
+ : store.listMessages(attempt.recipient.taskId).find(m => m.id === attempt.messageId) ?? null), businessResult: run.businessResult ?? null };
328
+ if (op === "run-confirm")
329
+ return observeAttempt(store, run, now);
330
+ if (op === "run-claim" || op === "run-report") {
331
+ const execution = currentExecution(store, run, caller);
332
+ run = observeAttempt(store, run, now);
333
+ const last = run.attempts.at(-1);
334
+ if (last.execution && !isDeepStrictEqual(last.execution, execution))
335
+ throw new Error("This native attempt does not own the Run; use explicit safe continuation.");
336
+ if (op === "run-report") {
337
+ if (!last.execution)
338
+ throw new Error("Claim the exact Run before reporting its result.");
339
+ const result = validateBusinessResult(input.result, { scope: run.scope, runId: run.id });
340
+ if (run.businessResult) {
341
+ if (!isDeepStrictEqual(run.businessResult, result))
342
+ throw new Error("Job business result is immutable.");
343
+ return run;
344
+ }
345
+ run = { ...run, status: result.outcome, businessResult: structuredClone(result), terminalAt: now.toISOString(), updatedAt: now.toISOString() };
346
+ }
347
+ else if (!last.execution)
348
+ run = { ...run, status: "running", updatedAt: now.toISOString(),
349
+ attempts: [...run.attempts.slice(0, -1), { ...last, execution }] };
350
+ store.saveAgentJobRun(run);
351
+ return run;
352
+ }
353
+ requireManager(store, job, caller);
354
+ if (op === "run-cancel") {
355
+ if (run.businessResult || run.cancelRequestedAt)
356
+ return run;
357
+ const next = { ...run, cancelRequestedAt: now.toISOString(), updatedAt: now.toISOString() };
358
+ store.saveAgentJobRun(next);
359
+ return next; // Cooperative exact-call cancellation; never stop the shared Role.
360
+ }
361
+ if (op === "run-continue") {
362
+ // Reconciliation of a repeated command returns the already-created
363
+ // successor, even if it has since finished. Never create another input.
364
+ if (input.expectedAttemptId && run.attempts.slice(0, -1).some(attempt => attempt.execution?.attemptId === input.expectedAttemptId))
365
+ return run;
366
+ run = observeAttempt(store, run, now);
367
+ if (providerRetryPending(sessionsFor(store, run.attempts.at(-1).recipient)?.providerBinding)) {
368
+ throw new Error("Existing bounded Provider recovery owns this input; do not submit a competing continuation.");
369
+ }
370
+ if (run.businessResult || !run.attempts.at(-1).terminal)
371
+ throw new Error("Continuation requires a proven exact prior native terminal and an unfinished business result. Unknown effects must not be replayed.");
372
+ if (!input.expectedAttemptId || run.attempts.at(-1).execution?.attemptId !== input.expectedAttemptId) {
373
+ throw new Error("Inspect the last native terminal and supply its exact expectedAttemptId before continuation.");
374
+ }
375
+ const snapshot = { ...job, configuration: run.configuration };
376
+ authorizeAgentDefinition(store, snapshot, caller, true);
377
+ const next = { ...run, status: "queued", updatedAt: now.toISOString(),
378
+ attempts: [...run.attempts, queueAttempt(store, home, snapshot, run.id, run.attempts.length, now)] };
379
+ store.saveAgentJobRun(next);
380
+ return next;
381
+ }
382
+ throw new Error(`Unsupported Agent Job operation: ${op}.`);
383
+ }
384
+ export function summarizeAgentRuns(runs) {
385
+ const ordered = [...runs].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
386
+ const summary = (run) => run ? {
387
+ runId: run.id, sourceRun: { scope: run.scope, runId: run.id }, status: run.status,
388
+ outcome: run.businessResult?.outcome ?? null, summary: run.businessResult?.summary.slice(0, 512) ?? null
389
+ } : null;
390
+ return { latest: summary(ordered[0]), latestSuccessful: summary(ordered.find(run => run.businessResult?.outcome === "succeeded")) };
391
+ }
@@ -1651,18 +1651,39 @@ export async function startFileTaskController(home, store, delivery, dispatcher,
1651
1651
  throw controllerApplicationError("METHOD_NOT_FOUND", "Job definitions unavailable.");
1652
1652
  let result;
1653
1653
  try {
1654
- result = options.jobDefinitions(params, new Date());
1654
+ result = await options.jobDefinitions(params, new Date());
1655
1655
  }
1656
1656
  catch (error) {
1657
1657
  if (error instanceof Error && error.name === "Error")
1658
1658
  error.name = "CoreJobError";
1659
1659
  throw error;
1660
1660
  }
1661
+ if (params.op === "run-confirm"
1662
+ && result.executor !== "agent"
1663
+ && !result?.delegation) {
1664
+ if (!options.jobSupervisor?.confirmResult) {
1665
+ throw controllerApplicationError("METHOD_NOT_FOUND", "Job result collection unavailable.");
1666
+ }
1667
+ // The service above first authenticates scope and this exact Run.
1668
+ const run = result;
1669
+ options.jobSupervisor.confirmResult(run.taskId ?? "global", run.id, new Date());
1670
+ result = options.jobDefinitions({ ...params, op: "run-get" }, new Date());
1671
+ }
1661
1672
  // The service has validated this scope. Only execution mutations need
1662
1673
  // reconciliation; definition reads/edits do not start background work.
1663
1674
  const input = params;
1664
- if (["run", "run-cancel", "run-acknowledge"].includes(input.op)) {
1665
- runtime.signal(input.scope.kind === "task" ? `task:${input.scope.taskId}` : "operator");
1675
+ if (input.op === "session-stop" && typeof result.roleName === "string") {
1676
+ runtime.signal(`global-role:${encodeURIComponent(result.roleName)}`);
1677
+ }
1678
+ else if (["run", "run-cancel", "run-acknowledge", "run-continue"].includes(input.op)) {
1679
+ const agentRun = result.run
1680
+ ?? result;
1681
+ const recipient = agentRun.executor === "agent" ? agentRun.attempts.at(-1)?.recipient : undefined;
1682
+ runtime.signal(recipient?.scope === "task"
1683
+ ? recipient.ownerRunId ? `task:${recipient.taskId}`
1684
+ : `role:${encodeURIComponent(recipient.taskId)}/${encodeURIComponent(recipient.roleName)}`
1685
+ : recipient?.scope === "global" ? "operator"
1686
+ : input.scope.kind === "task" ? `task:${input.scope.taskId}` : "operator");
1666
1687
  }
1667
1688
  return jobControlJson(result);
1668
1689
  }