@zq-silk/yui 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/artifacts/artifactCapability.js +14 -8
  2. package/dist/artifacts/artifactCommitLock.js +7 -5
  3. package/dist/artifacts/jobArtifact.js +93 -0
  4. package/dist/artifacts/jobArtifactTransport.js +13 -0
  5. package/dist/artifacts/taskArtifactRepository.js +58 -10
  6. package/dist/cli/commandCatalog.js +45 -14
  7. package/dist/cli/commandDiscovery.js +1 -1
  8. package/dist/commands/durableJobCommands.js +31 -6
  9. package/dist/commands/globalRoleCommands.js +5 -0
  10. package/dist/commands/scheduleCommands.js +14 -3
  11. package/dist/commands/taskCommands.js +5 -2
  12. package/dist/context/taskCatalog.js +6 -1
  13. package/dist/controlPlaneCli.js +7 -2
  14. package/dist/controller/agentJobControl.js +391 -0
  15. package/dist/controller/controller.js +24 -3
  16. package/dist/controller/fileSchedulerStoreAdapter.js +12 -1
  17. package/dist/controller/globalInputDelivery.js +8 -1
  18. package/dist/controller/jobArtifactControl.js +111 -0
  19. package/dist/controller/jobControl.js +4 -0
  20. package/dist/controller/jobDefinitionControl.js +191 -19
  21. package/dist/controller/jobSupervisor.js +83 -3
  22. package/dist/controller/jobTaskDelegationControl.js +193 -0
  23. package/dist/controller/runtime.js +7 -1
  24. package/dist/controller/scheduleControl.js +83 -20
  25. package/dist/integration/gitIntegrationService.js +2 -0
  26. package/dist/job/agentJob.js +131 -0
  27. package/dist/job/agentJobObservation.js +61 -0
  28. package/dist/job/agentRoleScope.js +33 -0
  29. package/dist/job/durableJob.js +63 -28
  30. package/dist/job/jobAssignmentScope.js +1 -0
  31. package/dist/job/jobBusinessResult.js +72 -0
  32. package/dist/job/jobCleanupInspection.js +4 -0
  33. package/dist/job/jobDefinition.js +26 -2
  34. package/dist/job/jobRunner.js +4 -3
  35. package/dist/job/projectRefreshRunner.js +54 -18
  36. package/dist/job/projectSync.js +162 -0
  37. package/dist/job/scheduleService.js +55 -0
  38. package/dist/job/taskDelegation.js +48 -0
  39. package/dist/message/messageContinuation.js +1 -1
  40. package/dist/resources/projectResourceService.js +1 -1
  41. package/dist/resources/resourceGc.js +1 -1
  42. package/dist/role/role.js +12 -0
  43. package/dist/schedule/schedule.js +65 -15
  44. package/dist/scheduler/wakeReason.js +4 -0
  45. package/dist/storage/agentJobSchema.js +10 -0
  46. package/dist/storage/recordValidation.js +2 -0
  47. package/dist/storage/sqliteSchema.js +77 -6
  48. package/dist/storage/sqliteStore.js +53 -0
  49. package/dist/storage/storageVersions.js +1 -1
  50. package/dist/storage/storeRpc.js +3 -0
  51. package/dist/storage/taskCatalog.js +10 -2
  52. package/dist/storage/upgrade/jobBusinessResultMigration.js +20 -0
  53. package/dist/storage/upgrade/scheduleDispatchMigration.js +15 -0
  54. package/dist/task/archivePreflight.js +15 -0
  55. package/dist/task/completionReadiness.js +7 -0
  56. package/dist/task/task.js +17 -1
  57. package/dist/verification/verificationGateService.js +4 -0
  58. package/dist/web/assets/client/app/catalog.js +16 -0
  59. package/dist/web/assets/client/i18n/en.js +29 -2
  60. package/dist/web/assets/client/i18n/zh-CN.js +29 -2
  61. package/dist/web/assets/client/layout/manageShell.js +9 -0
  62. package/dist/web/assets/client/layout/router.js +1 -0
  63. package/dist/web/assets/client/views/jobs.js +166 -37
  64. package/dist/web/assets/client/views/task/files.js +5 -4
  65. package/dist/web/assets/client/views/task/page.js +5 -1
  66. package/dist/web/taskPreviews.js +2 -2
  67. package/dist/web/webJobs.js +36 -11
  68. package/dist/web/webMaterials.js +21 -9
  69. package/dist/web/webServer.js +6 -5
  70. package/dist/web/webTaskSurface.js +1 -0
  71. package/docs/job-runs.md +392 -17
  72. package/docs/release-workflow.md +42 -0
  73. package/docs/release-workflow.zh-CN.md +31 -0
  74. package/docs/schedules.md +69 -15
  75. package/docs/storage-baseline.md +38 -1
  76. package/docs/storage-baseline.zh-CN.md +27 -1
  77. package/docs/testing/verification-levels.md +2 -2
  78. package/docs/testing/verification-levels.zh-CN.md +2 -2
  79. package/package.json +1 -1
@@ -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) },
@@ -24,6 +24,7 @@ import { routeInvocation } from "./cli/invocationRouter.js";
24
24
  import { operatorOfflineCommand, taskDiagnosticTarget } from "./cli/managedDiagnostics.js";
25
25
  import { assertConfigurationAuthority, assertTaskInvocationScope } from "./cli/invocationAuthority.js";
26
26
  import { requireManagedGlobalCaller, requireManagedGlobalReader, ManagedRuntimeDriftError } from "./runtime/managedCaller.js";
27
+ import { assertJobAgentCommand } from "./job/agentRoleScope.js";
27
28
  import { requireManagedCaller } from "./runtime/sessionCaller.js";
28
29
  import { maintenanceCallerEnvironment } from "./storage/maintenanceCaller.js";
29
30
  import { checkCurrentSkills, SKILL_REUSE_RULE } from "./context/currentSkills.js";
@@ -1986,12 +1987,13 @@ export async function main() {
1986
1987
  return;
1987
1988
  }
1988
1989
  if (resolved[0] === "job") {
1989
- emit(await runDurableJobCommand(resolved.slice(1), {
1990
+ const result = await runDurableJobCommand(resolved.slice(1), {
1990
1991
  home,
1991
1992
  json: jsonOutput,
1992
1993
  environment: process.env,
1993
1994
  store
1994
- }));
1995
+ });
1996
+ emit(result, false, jsonOutput ? JSON.parse(result) : undefined);
1995
1997
  return;
1996
1998
  }
1997
1999
  if (resolved[0] === "telemetry") {
@@ -2143,6 +2145,9 @@ async function preflightManagedGlobalControlPlane() {
2143
2145
  requireManagedGlobalReader(verifiedStore, process.env);
2144
2146
  else
2145
2147
  requireManagedGlobalCaller(verifiedStore, process.env);
2148
+ const role = verifiedStore.getGlobalRole(process.env.YUI_ROLE);
2149
+ if (role)
2150
+ assertJobAgentCommand(role, args);
2146
2151
  }
2147
2152
  catch (error) {
2148
2153
  verifiedStore.close();
@@ -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
  }
@@ -1,4 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
+ import { assertJobRoleRunnable } from "../job/agentRoleScope.js";
3
+ import { recordJobAgentObservation } from "../job/agentJobObservation.js";
2
4
  import { isDeepStrictEqual } from "node:util";
3
5
  import { nonReplayableMessageWake, pinMessageRevisions, taskMessageInboxReceipt, unfinishedLeaderInbox } from "../message/taskInboxOwnership.js";
4
6
  import { contextSnapshotRef } from "../context/contextSnapshot.js";
@@ -275,6 +277,8 @@ export class FileSchedulerStoreAdapter {
275
277
  && input.kind !== "goal.updated" && input.kind !== "goal.cleared") {
276
278
  this.persistRuntimeObservation(input, now);
277
279
  }
280
+ if (outcome === "applied")
281
+ recordJobAgentObservation(this.store, { scope: "task", taskId, roleName: input.fence.roleName }, this.store.getTaskRoleSessionSet(taskId, input.fence.roleName)?.providerBinding, now);
278
282
  return outcome;
279
283
  }
280
284
  /**
@@ -490,6 +494,7 @@ export class FileSchedulerStoreAdapter {
490
494
  });
491
495
  }
492
496
  store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, binding, now));
497
+ recordJobAgentObservation(store, { scope: "global", roleName: fence.roleName }, binding, now);
493
498
  settleGlobalRetryInput(store, fence.roleName, binding, now);
494
499
  if (["turn.completed", "turn.failed", "turn.cancelled"].includes(input.kind)) {
495
500
  checkInboxTurn(store, { scope: "global", roleName: fence.roleName, agentId: fence.agentId,
@@ -1129,6 +1134,9 @@ export class FileSchedulerStoreAdapter {
1129
1134
  beginAgentHostProviderTurn(input) {
1130
1135
  if (input.taskId === undefined) {
1131
1136
  this.store.transaction((store) => {
1137
+ const role = store.getGlobalRole(input.roleName);
1138
+ if (role)
1139
+ assertJobRoleRunnable(store, role);
1132
1140
  if (input.runId !== undefined || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: input.roleName })))) {
1133
1141
  throw new AgentHostProviderTurnFenceError("Global Session admission is stopped.");
1134
1142
  }
@@ -1326,6 +1334,7 @@ export class FileSchedulerStoreAdapter {
1326
1334
  ? { retry: { ...resolved.retry, failureRef } } : {}) };
1327
1335
  }
1328
1336
  store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, resolved, input.now));
1337
+ recordJobAgentObservation(store, { scope: "global", roleName: input.roleName }, resolved, input.now);
1329
1338
  settleGlobalRetryInput(store, input.roleName, resolved, input.now);
1330
1339
  if (input.status !== "deferred" && !providerRetryPending(resolved)) {
1331
1340
  recordGlobalRuntimeAttention(store, input.roleName, `${input.attemptId}:submission`, {
@@ -1561,7 +1570,9 @@ export class FileSchedulerStoreAdapter {
1561
1570
  if ("taskId" in next)
1562
1571
  throw new Error("A global JobRun cannot change scope.");
1563
1572
  store.saveGlobalJobRun(next);
1564
- if (wakeup !== undefined)
1573
+ // Public and explicitly service-owned work have no Agent recipient.
1574
+ // Results stay queryable without implicitly starting an Operator.
1575
+ if (wakeup !== undefined && !["user", "service:schedule"].includes(current.operation.actorId))
1565
1576
  enqueueWork(store, { kind: "operator" }, "global-job-finished", now, [{ type: "global-job-run", id: jobId }], { source: "global-job", dedupeKey: `operator-global-job-run:${jobId}` });
1566
1577
  return next;
1567
1578
  }
@@ -7,6 +7,7 @@ import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
7
7
  import { globalMessagePresentation } from "../interaction/notificationPresentation.js";
8
8
  import { recordGlobalRuntimeAttention, recordGlobalRuntimeRecovery } from "./globalRuntimeAttention.js";
9
9
  import { globalInboxRecovery } from "../message/globalInboxRecovery.js";
10
+ import { assertJobRoleRunnable } from "../job/agentRoleScope.js";
10
11
  /** Messages are the only queue. Host's atomic begin arbitrates native writers. */
11
12
  export async function deliverGlobalInputs(home, store, ensure, onError, options = {}) {
12
13
  for (const role of store.listGlobalRoles()) {
@@ -17,7 +18,12 @@ export async function deliverGlobalInputs(home, store, ensure, onError, options
17
18
  let sessions = store.getGlobalRoleSessionSet(role.name);
18
19
  // A detached Host keeps an active Session and may reconnect. An explicit
19
20
  // Session stop/switch must not be undone by background notification work.
20
- if (sessions !== null && sessions.sessions[sessions.activeAgentId]?.status !== "active")
21
+ const selected = sessions?.sessions[sessions.activeAgentId];
22
+ // An explicit Job session-new archives/removes the proven-ended writer.
23
+ // Its next original input may launch normally; an ended selected Session
24
+ // still means stopped and is never implicitly restarted.
25
+ if (sessions !== null && selected?.status !== "active"
26
+ && !(role.jobOwner && selected === undefined))
21
27
  continue;
22
28
  if (sessions?.providerBinding?.retry !== undefined && !providerRetryPending(sessions.providerBinding)) {
23
29
  store.transaction(tx => settleGlobalRetryInput(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, new Date()));
@@ -110,6 +116,7 @@ export async function deliverGlobalInputs(home, store, ensure, onError, options
110
116
  pending = pending.filter(message => !obsoleteIds.has(message.id));
111
117
  if (pending.length === 0 && !recovering)
112
118
  continue;
119
+ assertJobRoleRunnable(store, role);
113
120
  if (reservedPage.count > reserved.length && obsolete.length === reserved.length)
114
121
  continue;
115
122
  if (current != null && ["submitting", "accepted", "delivery-unknown"].includes(current.status))