@zq-silk/yui 2.11.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/artifacts/artifactCapability.js +14 -8
- package/dist/artifacts/artifactCommitLock.js +7 -5
- package/dist/artifacts/jobArtifact.js +93 -0
- package/dist/artifacts/jobArtifactTransport.js +13 -0
- package/dist/artifacts/taskArtifactRepository.js +58 -10
- package/dist/cli/commandCatalog.js +45 -14
- package/dist/cli/commandDiscovery.js +1 -1
- package/dist/commands/durableJobCommands.js +31 -6
- package/dist/commands/globalRoleCommands.js +5 -0
- package/dist/commands/scheduleCommands.js +14 -3
- package/dist/commands/taskCommands.js +5 -2
- package/dist/context/taskCatalog.js +6 -1
- package/dist/controlPlaneCli.js +7 -2
- package/dist/controller/agentJobControl.js +391 -0
- package/dist/controller/controller.js +24 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +12 -1
- package/dist/controller/globalInputDelivery.js +8 -1
- package/dist/controller/jobArtifactControl.js +111 -0
- package/dist/controller/jobControl.js +4 -0
- package/dist/controller/jobDefinitionControl.js +191 -19
- package/dist/controller/jobSupervisor.js +83 -3
- package/dist/controller/jobTaskDelegationControl.js +193 -0
- package/dist/controller/runtime.js +9 -1
- package/dist/controller/scheduleControl.js +83 -20
- package/dist/integration/gitIntegrationService.js +2 -0
- package/dist/job/agentJob.js +131 -0
- package/dist/job/agentJobObservation.js +61 -0
- package/dist/job/agentRoleScope.js +33 -0
- package/dist/job/durableJob.js +63 -28
- package/dist/job/jobAssignmentScope.js +1 -0
- package/dist/job/jobBusinessResult.js +72 -0
- package/dist/job/jobCleanupInspection.js +4 -0
- package/dist/job/jobDefinition.js +26 -2
- package/dist/job/jobRunner.js +4 -3
- package/dist/job/projectRefreshRunner.js +54 -18
- package/dist/job/projectSync.js +162 -0
- package/dist/job/scheduleService.js +55 -0
- package/dist/job/taskDelegation.js +48 -0
- package/dist/message/messageContinuation.js +1 -1
- package/dist/resources/projectResourceService.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/role/role.js +12 -0
- package/dist/schedule/schedule.js +65 -15
- package/dist/scheduler/wakeReason.js +4 -0
- package/dist/storage/agentJobSchema.js +10 -0
- package/dist/storage/recordValidation.js +2 -0
- package/dist/storage/sqliteSchema.js +77 -6
- package/dist/storage/sqliteStore.js +53 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +3 -0
- package/dist/storage/taskCatalog.js +10 -2
- package/dist/storage/upgrade/jobBusinessResultMigration.js +20 -0
- package/dist/storage/upgrade/scheduleDispatchMigration.js +15 -0
- package/dist/task/archivePreflight.js +15 -0
- package/dist/task/completionReadiness.js +7 -0
- package/dist/task/task.js +17 -1
- package/dist/verification/verificationGateService.js +4 -0
- package/dist/web/assets/client/app/catalog.js +16 -0
- package/dist/web/assets/client/app/settings.js +4 -0
- package/dist/web/assets/client/i18n/en.js +75 -2
- package/dist/web/assets/client/i18n/zh-CN.js +75 -2
- package/dist/web/assets/client/index.js +2 -0
- package/dist/web/assets/client/layout/manageShell.js +9 -0
- package/dist/web/assets/client/layout/router.js +1 -0
- package/dist/web/assets/client/views/jobs.js +166 -37
- package/dist/web/assets/client/views/permissions.js +129 -0
- package/dist/web/assets/client/views/task/files.js +5 -4
- package/dist/web/assets/client/views/task/page.js +5 -1
- package/dist/web/assets/shared/manageNav.js +1 -0
- package/dist/web/assets/shell/settingsContent.js +4 -0
- package/dist/web/assets/styles/views/settings.js +13 -0
- package/dist/web/taskPreviews.js +2 -2
- package/dist/web/webJobs.js +36 -11
- package/dist/web/webMaterials.js +21 -9
- package/dist/web/webPermissions.js +97 -0
- package/dist/web/webServer.js +44 -5
- package/dist/web/webTaskSurface.js +1 -0
- package/docs/authorization.md +35 -0
- package/docs/job-runs.md +392 -17
- package/docs/release-workflow.md +77 -0
- package/docs/release-workflow.zh-CN.md +57 -0
- package/docs/schedules.md +69 -15
- package/docs/storage-baseline.md +38 -1
- package/docs/storage-baseline.zh-CN.md +27 -1
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +2 -2
- package/i18n/README.zh-CN.md +4 -0
- package/package.json +1 -1
|
@@ -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) },
|
package/dist/controlPlaneCli.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
1665
|
-
runtime.signal(
|
|
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
|
-
|
|
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
|
-
|
|
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))
|