@zq-silk/yui 2.16.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commandCatalog.js +19 -12
- package/dist/cli/interactionCandidates.js +20 -5
- package/dist/cli/interactionPolicy.js +5 -4
- package/dist/cli/roleWizard.js +7 -0
- package/dist/commands/controllerCommands.js +13 -4
- package/dist/commands/durableJobCommands.js +50 -3
- package/dist/commands/globalRoleCommands.js +31 -14
- package/dist/commands/inboxCommands.js +1 -0
- package/dist/commands/operatorCommands.js +12 -2
- package/dist/commands/profileCommands.js +19 -2
- package/dist/commands/roleConfiguration.js +3 -0
- package/dist/commands/taskCommands.js +16 -0
- package/dist/commands/taskRoleRuntimeStatus.js +3 -0
- package/dist/controlPlaneCli.js +21 -4
- package/dist/controller/agentJobControl.js +9 -1
- package/dist/controller/clientRuntime.js +34 -13
- package/dist/controller/controller.js +35 -22
- package/dist/controller/diagnosticJournal.js +214 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +174 -105
- package/dist/controller/globalInputDelivery.js +19 -9
- package/dist/controller/globalRuntimeAttention.js +11 -3
- package/dist/controller/jobControl.js +1 -1
- package/dist/controller/jobDefinitionControl.js +24 -13
- package/dist/controller/jobSupervisor.js +4 -0
- package/dist/controller/providerRetryAdmission.js +9 -0
- package/dist/controller/providerRetryDelivery.js +16 -0
- package/dist/controller/runtime.js +80 -4
- package/dist/controller/runtimeLaunchCoordinator.js +3 -1
- package/dist/controller/workerSchedulerStore.js +24 -0
- package/dist/core/boundedRpc.js +4 -0
- package/dist/core/controllerServer.js +1 -1
- package/dist/core/controllerTelemetry.js +92 -19
- package/dist/executor/fileRoleLaunchPlanner.js +3 -0
- package/dist/job/globalShell.js +51 -0
- package/dist/job/jobBusinessResult.js +4 -3
- package/dist/job/jobDefinition.js +6 -1
- package/dist/job/jobRunner.js +9 -0
- package/dist/job/scheduleService.js +2 -0
- package/dist/message/globalInboxRecovery.js +30 -19
- package/dist/message/globalProviderRetry.js +7 -1
- package/dist/message/messageContinuation.js +4 -0
- package/dist/output/rolePresentation.js +3 -0
- package/dist/profile/agentProfile.js +8 -0
- package/dist/profile/agentProfileRuntime.js +16 -1
- package/dist/review/reviewDecision.js +6 -0
- package/dist/role/availability.js +28 -0
- package/dist/role/role.js +6 -0
- package/dist/runtime/builtinAgentErrorMappers.js +4 -0
- package/dist/runtime/codexNativeAccess.js +21 -4
- package/dist/runtime/globalSessionHealth.js +39 -0
- package/dist/runtime/tmuxAdapters.js +5 -0
- package/dist/scheduler/activeRoleRunDelivery.js +9 -0
- package/dist/scheduler/leaderWakeupProcessor.js +8 -1
- package/dist/scheduler/roleRunLiveness.js +3 -0
- package/dist/scheduler/roleRunStall.js +10 -10
- package/dist/storage/persistenceWorker.js +41 -19
- package/dist/storage/sqliteSchema.js +43 -9
- package/dist/storage/sqliteStore.js +86 -8
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +25 -11
- package/dist/storage/taskReadRevisionSchema.js +39 -0
- package/dist/web/assets/client/app/catalog.js +0 -29
- package/dist/web/assets/client/app/jobs.js +115 -0
- package/dist/web/assets/client/app/main.js +32 -28
- package/dist/web/assets/client/app/manage.js +1 -0
- package/dist/web/assets/client/app/settings.js +1 -5
- package/dist/web/assets/client/app/taskView.js +7 -11
- package/dist/web/assets/client/i18n/en.js +265 -36
- package/dist/web/assets/client/i18n/zh-CN.js +264 -35
- package/dist/web/assets/client/index.js +4 -0
- package/dist/web/assets/client/layout/manageShell.js +61 -15
- package/dist/web/assets/client/layout/router.js +2 -1
- package/dist/web/assets/client/layout/shortcuts.js +2 -2
- package/dist/web/assets/client/layout/workspace.js +99 -112
- package/dist/web/assets/client/lib/prefs.js +1 -1
- package/dist/web/assets/client/ui/primitives.js +15 -2
- package/dist/web/assets/client/views/dock/conversation.js +421 -123
- package/dist/web/assets/client/views/dock/session.js +229 -0
- package/dist/web/assets/client/views/dock/terminal.js +48 -39
- package/dist/web/assets/client/views/jobs.js +1042 -160
- package/dist/web/assets/client/views/overview.js +2 -2
- package/dist/web/assets/client/views/sidebar.js +3 -0
- package/dist/web/assets/client/views/task/page.js +2 -12
- package/dist/web/assets/shared/manageNav.js +3 -1
- package/dist/web/assets/shell/appRail.js +2 -1
- package/dist/web/assets/shell/dock.js +9 -27
- package/dist/web/assets/shell/manage.js +7 -2
- package/dist/web/assets/shell/models.js +4 -3
- package/dist/web/assets/shell/settingsContent.js +0 -3
- package/dist/web/assets/shell/sidebar.js +0 -1
- package/dist/web/assets/styles/views/conversation.js +96 -17
- package/dist/web/assets/styles/views/dock.js +17 -5
- package/dist/web/assets/styles/views/jobs.js +301 -12
- package/dist/web/assets/styles/views/sidebar.js +1 -2
- package/dist/web/assets/styles/views/task.js +1 -2
- package/dist/web/assets/styles/views/terminal.js +12 -8
- package/dist/web/webConversation.js +1 -0
- package/dist/web/webServer.js +28 -1
- package/dist/web/webSettings.js +10 -2
- package/docs/global-shell-jobs.md +78 -0
- package/docs/job-runs.md +6 -3
- package/docs/managed-turn-and-session-runtime.md +84 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +57 -0
- package/docs/release-workflow.md +67 -0
- package/docs/release-workflow.zh-CN.md +48 -0
- package/docs/roles-and-configuration.md +45 -1
- package/docs/roles-and-configuration.zh-CN.md +35 -0
- package/docs/storage-baseline.md +19 -1
- package/docs/storage-baseline.zh-CN.md +10 -1
- package/docs/testing/review-evaluation.md +35 -1
- package/docs/testing/review-evaluation.zh-CN.md +25 -1
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-leader/references/execution.md +19 -8
- package/skills/yui-reviewer/SKILL.md +13 -2
- package/skills/yui-reviewer/VERSION +1 -1
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { isDeepStrictEqual } from "node:util";
|
|
7
7
|
import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
|
|
8
8
|
import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
|
|
9
|
+
import { assertRoleEnabled } from "../role/availability.js";
|
|
9
10
|
import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/agent.js";
|
|
10
11
|
import { NATIVE_AGENT_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, operationalAgentEnvironment, selectEnvironment } from "../agent/launchEnvironment.js";
|
|
11
12
|
import { compileRoleSessionContext, roleSessionKind } from "../context/roleSessionContext.js";
|
|
@@ -106,6 +107,7 @@ export class FileRoleLaunchPlanner {
|
|
|
106
107
|
const role = this.store.getRole(input.taskId, input.roleName);
|
|
107
108
|
if (role === null)
|
|
108
109
|
throw new Error(`Role not found: ${input.taskId}/${input.roleName}.`);
|
|
110
|
+
assertRoleEnabled(role);
|
|
109
111
|
const activeRun = this.store.getActiveRun(task.id, role.name);
|
|
110
112
|
if (input.runId !== undefined && activeRun?.id !== input.runId) {
|
|
111
113
|
throw new Error(`Role AgentRun is no longer current: ${input.runId}.`);
|
|
@@ -226,6 +228,7 @@ export class FileRoleLaunchPlanner {
|
|
|
226
228
|
const role = this.store.getGlobalRole(input.roleName);
|
|
227
229
|
if (role === null)
|
|
228
230
|
throw new Error(`Global Role not found: ${input.roleName}.`);
|
|
231
|
+
assertRoleEnabled(role);
|
|
229
232
|
const sessionSet = this.store.getGlobalRoleSessionSet(role.name);
|
|
230
233
|
if (activeLiveRoleAgentSession(sessionSet) !== null && sessionSet?.providerBinding == null) {
|
|
231
234
|
throw new Error("Global Role has an unmanaged live Session. Stop that exact Session before opening controlled input; it will not be silently replaced.");
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { requireKnownFields } from "../domain/validation.js";
|
|
5
|
+
import { validateJobExecutionConfiguration } from "./durableJob.js";
|
|
6
|
+
export function validateJobScriptFiles(files) {
|
|
7
|
+
if (!Array.isArray(files) || files.length > 64)
|
|
8
|
+
throw new Error("Job supports at most 64 script files.");
|
|
9
|
+
const paths = new Set();
|
|
10
|
+
let bytes = 0;
|
|
11
|
+
for (const file of files) {
|
|
12
|
+
requireKnownFields(file, ["path", "content"], "Job script file");
|
|
13
|
+
if (typeof file.path !== "string" || file.path.length > 240
|
|
14
|
+
|| !/^[a-zA-Z0-9_-][a-zA-Z0-9._/-]*$/u.test(file.path)
|
|
15
|
+
|| file.path.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
16
|
+
throw new Error("Script paths must be safe relative paths, for example scripts/check.sh.");
|
|
17
|
+
}
|
|
18
|
+
if (paths.has(file.path) || [...paths].some(path => path.startsWith(file.path + "/") || file.path.startsWith(path + "/"))) {
|
|
19
|
+
throw new Error("Script paths must be unique and cannot overlap directories.");
|
|
20
|
+
}
|
|
21
|
+
if (typeof file.content !== "string" || file.content.includes("\0"))
|
|
22
|
+
throw new Error("Scripts must be UTF-8 text without NUL.");
|
|
23
|
+
bytes += Buffer.byteLength(file.content);
|
|
24
|
+
if (bytes > 512 * 1024)
|
|
25
|
+
throw new Error("Job scripts exceed 512 KiB.");
|
|
26
|
+
paths.add(file.path);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function prepareGlobalShell(configuration, workspace) {
|
|
30
|
+
requireKnownFields(configuration, ["kind", "env", "steps", "files"], "Global Shell Job");
|
|
31
|
+
if (configuration.kind !== "shell")
|
|
32
|
+
throw new Error("Not a Shell Job.");
|
|
33
|
+
validateJobScriptFiles(configuration.files);
|
|
34
|
+
// No repository applies. This hash identifies executable configuration,
|
|
35
|
+
// never a Project commit or an Integration candidate.
|
|
36
|
+
const head = createHash("sha1").update(JSON.stringify(configuration)).digest("hex");
|
|
37
|
+
const spec = { workspace, env: configuration.env, steps: configuration.steps, head };
|
|
38
|
+
validateJobExecutionConfiguration(spec);
|
|
39
|
+
return spec;
|
|
40
|
+
}
|
|
41
|
+
/** A fresh per-Run directory; never overwrite another run's files or follow
|
|
42
|
+
* a pre-existing workspace. The runner's exclusive ready receipt fences replay. */
|
|
43
|
+
export function materializeJobScripts(workspace, files) {
|
|
44
|
+
validateJobScriptFiles(files);
|
|
45
|
+
mkdirSync(workspace, { mode: 0o700 });
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const path = join(workspace, file.path);
|
|
48
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
49
|
+
writeFileSync(path, file.content, { flag: "wx", mode: 0o700 });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -62,9 +62,10 @@ export function jobRunSnapshot(run) {
|
|
|
62
62
|
...runReference(run), definitionId: run.definitionId, definitionRevision: run.definitionRevision,
|
|
63
63
|
owner: "owner" in run ? run.owner : null,
|
|
64
64
|
trigger: run.trigger, retryOf: run.retryOf ?? null,
|
|
65
|
-
executor: run.delegation ? { kind: "task", delegation: run.delegation } : "taskId" in run ? { kind: "shell" }
|
|
66
|
-
kind: "
|
|
67
|
-
|
|
65
|
+
executor: run.delegation ? { kind: "task", delegation: run.delegation } : "taskId" in run ? { kind: "shell" }
|
|
66
|
+
: run.action === "shell.exec" ? { kind: "shell", configuration: run.actionInput } : {
|
|
67
|
+
kind: "builtin", action: run.action, input: run.actionInput
|
|
68
|
+
},
|
|
68
69
|
target: run.delegation ? { taskId: run.delegation.taskId, workItemId: run.delegation.workItemId ?? null }
|
|
69
70
|
: { projectId: run.projectId ?? null, workspace: run.workspace, head: run.head },
|
|
70
71
|
execution: run.delegation ? null : { env: run.env, steps: run.steps, implementation: run.operation.implementation }
|
|
@@ -2,6 +2,7 @@ import { requireIdentity, requireKnownFields, requirePositiveInteger, requireTex
|
|
|
2
2
|
import { validateJobExecutionConfiguration, validateDurableJobOwner } from "./durableJob.js";
|
|
3
3
|
import { validateAgentJobConfiguration } from "./agentJob.js";
|
|
4
4
|
import { validateTaskJobConfiguration } from "./taskDelegation.js";
|
|
5
|
+
import { prepareGlobalShell } from "./globalShell.js";
|
|
5
6
|
export function jobScopeKey(scope) {
|
|
6
7
|
if (scope.kind === "global")
|
|
7
8
|
return "global";
|
|
@@ -44,6 +45,10 @@ export function validateJobDefinition(job) {
|
|
|
44
45
|
validateTaskJobConfiguration(config);
|
|
45
46
|
}
|
|
46
47
|
else if (job.scope.kind === "global") {
|
|
48
|
+
if (config.kind === "shell") {
|
|
49
|
+
prepareGlobalShell(config, "/job-validation");
|
|
50
|
+
return job;
|
|
51
|
+
}
|
|
47
52
|
if (config.kind !== "builtin")
|
|
48
53
|
throw new Error("Global Jobs require a controlled builtin action.");
|
|
49
54
|
requireKnownFields(config, ["kind", "action", "projectId", "input"], "Builtin Job");
|
|
@@ -54,7 +59,7 @@ export function validateJobDefinition(job) {
|
|
|
54
59
|
throw new Error("Builtin Job input is required.");
|
|
55
60
|
}
|
|
56
61
|
else {
|
|
57
|
-
if (config.kind !== "shell")
|
|
62
|
+
if (config.kind !== "shell" || !("owner" in config))
|
|
58
63
|
throw new Error("Task Jobs require a managed shell configuration.");
|
|
59
64
|
requireKnownFields(config, ["kind", "owner", "projectId", "head", "workspace", "env", "steps"], "Task Job");
|
|
60
65
|
requireIdentity(config.projectId, "Job Project id");
|
package/dist/job/jobRunner.js
CHANGED
|
@@ -13,6 +13,7 @@ import { join, resolve } from "node:path";
|
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { jobStepDirectory } from "./stepDirectory.js";
|
|
15
15
|
import { prepareHttpPreview, isHttpPreview } from "./httpPreview.js";
|
|
16
|
+
import { materializeJobScripts, validateJobScriptFiles } from "./globalShell.js";
|
|
16
17
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
17
18
|
import { currentProcessStartIdentity } from "../core/fileLockOwner.js";
|
|
18
19
|
const DEFAULT_STEP_TIMEOUT_MS = 30 * 60_000;
|
|
@@ -112,6 +113,8 @@ export async function runDurableJobRunner(specPath) {
|
|
|
112
113
|
let failedStep;
|
|
113
114
|
let preview;
|
|
114
115
|
try {
|
|
116
|
+
if (spec.files !== undefined)
|
|
117
|
+
materializeJobScripts(spec.workspace, spec.files);
|
|
115
118
|
for (let index = 0; index < spec.steps.length; index += 1) {
|
|
116
119
|
if (cancelled || existsSync(cancelPath)) {
|
|
117
120
|
cancelled = true;
|
|
@@ -278,6 +281,12 @@ function readSpec(specPath) {
|
|
|
278
281
|
if (typeof spec.artifactDir !== "string" || !spec.artifactDir.startsWith("/")) {
|
|
279
282
|
throw new Error("DurableJob spec artifactDir must be an absolute path.");
|
|
280
283
|
}
|
|
284
|
+
if (spec.files !== undefined) {
|
|
285
|
+
if (spec.scope !== "global" || spec.workspace !== join(spec.artifactDir, "workspace")) {
|
|
286
|
+
throw new Error("Job script snapshots require their own global Run workspace.");
|
|
287
|
+
}
|
|
288
|
+
validateJobScriptFiles(spec.files);
|
|
289
|
+
}
|
|
281
290
|
if (typeof spec.head !== "string" || spec.head.length === 0) {
|
|
282
291
|
throw new Error("DurableJob spec is missing head.");
|
|
283
292
|
}
|
|
@@ -4,6 +4,8 @@ import { requireProjectSyncTarget } from "./projectSync.js";
|
|
|
4
4
|
/** Internal compiled registration, not a caller identity accepted by RPC. */
|
|
5
5
|
const actions = new Set(["system.noop", "project.refresh"]);
|
|
6
6
|
export function requireServiceTarget(job) {
|
|
7
|
+
if (job.scope.kind === "global" && job.configuration.kind === "shell")
|
|
8
|
+
return;
|
|
7
9
|
if (job.scope.kind !== "global" || job.configuration.kind !== "builtin"
|
|
8
10
|
|| !actions.has(job.configuration.action))
|
|
9
11
|
throw new Error("Schedule service is not registered for this scope/action.");
|
|
@@ -4,28 +4,39 @@ import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/
|
|
|
4
4
|
import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
|
|
5
5
|
import { INBOX_CHECK_PREFIX } from "./inboxTurn.js";
|
|
6
6
|
const requestId = (agentId, nativeSessionId) => `${INBOX_CHECK_PREFIX}recovery:${createHash("sha256").update(JSON.stringify([agentId, nativeSessionId])).digest("hex")}`;
|
|
7
|
+
function recoveryCandidate(store, roleName) {
|
|
8
|
+
const sessions = store.getGlobalRoleSessionSet(roleName);
|
|
9
|
+
const session = sessions?.sessions[sessions.activeAgentId];
|
|
10
|
+
const binding = sessions?.providerBinding;
|
|
11
|
+
if (!session || session.status !== "active" || !session.nativeSessionId || !binding
|
|
12
|
+
|| binding.authority.owner !== "controller"
|
|
13
|
+
|| currentProviderConversation(binding).conversationId !== session.nativeSessionId
|
|
14
|
+
|| binding.run && ["submitting", "accepted", "delivery-unknown"].includes(binding.run.status)
|
|
15
|
+
|| hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName }))))
|
|
16
|
+
return;
|
|
17
|
+
if (store.queryGlobalRoleMessages(roleName, { handling: true, handlingExceptSession: {
|
|
18
|
+
agentId: session.agentId, nativeSessionId: session.nativeSessionId
|
|
19
|
+
}, limit: 0 }).count === 0)
|
|
20
|
+
return;
|
|
21
|
+
const id = requestId(session.agentId, session.nativeSessionId);
|
|
22
|
+
return { id, agentId: session.agentId, nativeSessionId: session.nativeSessionId,
|
|
23
|
+
existing: store.queryGlobalRoleMessages(roleName, { requestId: id, limit: 1 }).messages[0] };
|
|
24
|
+
}
|
|
7
25
|
/** Derive a single initialization notice from existing retained claims. New
|
|
8
26
|
* Sessions are created only by the existing authorized lifecycle path. A
|
|
9
27
|
* restart can finish notice creation, but cannot select or replace a Session. */
|
|
10
28
|
export function globalInboxRecovery(store, roleName, now) {
|
|
29
|
+
// The common idle/already-notified path must not reserve SQLite's writer.
|
|
30
|
+
// This snapshot is only a preflight; creation rechecks all authority, cleanup
|
|
31
|
+
// and retained-claim facts in the original atomic transaction.
|
|
32
|
+
const candidate = store.readTransaction(tx => recoveryCandidate(tx, roleName));
|
|
33
|
+
if (!candidate || candidate.existing)
|
|
34
|
+
return candidate?.existing;
|
|
11
35
|
return store.transaction(tx => {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|| binding.authority.owner !== "controller"
|
|
17
|
-
|| currentProviderConversation(binding).conversationId !== session.nativeSessionId
|
|
18
|
-
|| binding.run && ["submitting", "accepted", "delivery-unknown"].includes(binding.run.status)
|
|
19
|
-
|| hasRuntimeCleanupObligation(tx.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName }))))
|
|
20
|
-
return;
|
|
21
|
-
if (tx.queryGlobalRoleMessages(roleName, { handling: true, handlingExceptSession: {
|
|
22
|
-
agentId: session.agentId, nativeSessionId: session.nativeSessionId
|
|
23
|
-
}, limit: 0 }).count === 0)
|
|
24
|
-
return;
|
|
25
|
-
const id = requestId(session.agentId, session.nativeSessionId);
|
|
26
|
-
const existing = tx.queryGlobalRoleMessages(roleName, { requestId: id, limit: 1 }).messages[0];
|
|
27
|
-
if (existing)
|
|
28
|
-
return existing;
|
|
36
|
+
const latest = recoveryCandidate(tx, roleName);
|
|
37
|
+
if (!latest || latest.existing)
|
|
38
|
+
return latest?.existing;
|
|
39
|
+
const { id, agentId, nativeSessionId } = latest;
|
|
29
40
|
const notice = {
|
|
30
41
|
...createGlobalRoleMessage(tx.nextGlobalRoleMessageId(), roleName, [
|
|
31
42
|
"Yui-Summary: This successor has retained inbox handling obligations; inspect original evidence before settlement.",
|
|
@@ -34,9 +45,9 @@ export function globalInboxRecovery(store, roleName, now) {
|
|
|
34
45
|
"After handling under current authority, explicitly acknowledge the original entry without replacing its claim.",
|
|
35
46
|
"Uncertain external effects require verification or a real user decision. New queued input remains separate."
|
|
36
47
|
].join("\n"), "system", { type: "system" }, now, {
|
|
37
|
-
inputControl: { action: "queue", requestId: id, expectedSessionId:
|
|
48
|
+
inputControl: { action: "queue", requestId: id, expectedSessionId: nativeSessionId }
|
|
38
49
|
}),
|
|
39
|
-
deliveryTarget: { agentId
|
|
50
|
+
deliveryTarget: { agentId, nativeSessionId }
|
|
40
51
|
};
|
|
41
52
|
tx.saveGlobalRoleMessage(notice);
|
|
42
53
|
return notice;
|
|
@@ -2,6 +2,12 @@ import { markGlobalRoleMessageNotDelivered } from "./message.js";
|
|
|
2
2
|
/** Settle the original queue entry when recovery ends with exact nondelivery.
|
|
3
3
|
* Cancellation of a submitted/unknown request is not negative Provider proof. */
|
|
4
4
|
export function settleGlobalRetryInput(store, roleName, binding, now) {
|
|
5
|
+
const settlement = globalRetryInputSettlement(store, roleName, binding, now);
|
|
6
|
+
if (settlement !== undefined)
|
|
7
|
+
store.updateGlobalRoleMessage(settlement);
|
|
8
|
+
}
|
|
9
|
+
/** Read-only proposal; callers applying it must recheck under their transaction. */
|
|
10
|
+
export function globalRetryInputSettlement(store, roleName, binding, now) {
|
|
5
11
|
const retry = binding?.retry;
|
|
6
12
|
if (retry === undefined || retry.input.kind !== "message" || retry.input.roleName !== roleName
|
|
7
13
|
|| !["cancelled", "exhausted", "needs-attention"].includes(retry.status)
|
|
@@ -11,5 +17,5 @@ export function settleGlobalRetryInput(store, roleName, binding, now) {
|
|
|
11
17
|
const message = store.listGlobalRoleMessages(roleName).find(m => m.id === messageId);
|
|
12
18
|
if (message === undefined || message.delivery !== undefined || message.notDelivered !== undefined)
|
|
13
19
|
return;
|
|
14
|
-
|
|
20
|
+
return markGlobalRoleMessageNotDelivered(message, `provider-retry-${retry.status}: ${retry.reason ?? retry.failureRef}`, now);
|
|
15
21
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { isEnabled } from "../role/availability.js";
|
|
2
3
|
import { createRun } from "../agentRun/agentRun.js";
|
|
3
4
|
import { createRunInput } from "../context/runInputContract.js";
|
|
4
5
|
import { contextContentDigest, contextSnapshotRef, createContextSnapshot } from "../context/contextSnapshot.js";
|
|
@@ -90,6 +91,9 @@ export function messageAssignmentBlocker(store, message) {
|
|
|
90
91
|
}
|
|
91
92
|
/** Starting another Run additionally requires the reviewed candidate to remain current. */
|
|
92
93
|
export function messageContinuationBlocker(store, message) {
|
|
94
|
+
const role = message.recipient === undefined ? null : store.getRole(message.taskId, message.recipient.roleName);
|
|
95
|
+
if (role !== null && !isEnabled(role))
|
|
96
|
+
return "role-disabled";
|
|
93
97
|
const invalid = messageAssignmentBlocker(store, message);
|
|
94
98
|
if (invalid !== undefined)
|
|
95
99
|
return invalid;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isEnabled, roleDisabledReason } from "../role/availability.js";
|
|
1
2
|
import { activeRoleAgentSession } from "../executor/agentExecutor.js";
|
|
2
3
|
import { effectiveRoleForLaunch } from "../executor/effectiveLaunch.js";
|
|
3
4
|
import { defaultTableWidth, renderTable } from "./table.js";
|
|
@@ -49,6 +50,8 @@ export function renderRoleDetails(title, role, input) {
|
|
|
49
50
|
];
|
|
50
51
|
const overview = [
|
|
51
52
|
` Kind ${input.kind}`,
|
|
53
|
+
` Enabled ${isEnabled(role)}`,
|
|
54
|
+
...(!isEnabled(role) ? [` Availability ${roleDisabledReason(role)}`] : []),
|
|
52
55
|
` Active Agent ${role.activeAgentId}`,
|
|
53
56
|
` Workspace ${role.workspace}`,
|
|
54
57
|
...("taskId" in role ? [
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateEnabled } from "../role/availability.js";
|
|
1
2
|
import { cloneJson, normalizedUniqueText, optionalText, requireIdentity, requirePositiveInteger, requireTimestamp } from "../domain/validation.js";
|
|
2
3
|
export const BUILTIN_PROFILE_IDS = Object.freeze([
|
|
3
4
|
"worker",
|
|
@@ -31,6 +32,7 @@ export function updateAgentProfile(profile, patch, now) {
|
|
|
31
32
|
return validateAgentProfile(candidate);
|
|
32
33
|
}
|
|
33
34
|
export function validateAgentProfile(profile) {
|
|
35
|
+
validateEnabled(profile.enabled);
|
|
34
36
|
if (profile.schemaVersion !== 1) {
|
|
35
37
|
throw new Error("AgentProfile must use schemaVersion 1.");
|
|
36
38
|
}
|
|
@@ -82,7 +84,9 @@ export function builtinAgentProfileInputs() {
|
|
|
82
84
|
];
|
|
83
85
|
}
|
|
84
86
|
function normalizeInput(input) {
|
|
87
|
+
validateEnabled(input.enabled);
|
|
85
88
|
return {
|
|
89
|
+
enabled: input.enabled ?? true,
|
|
86
90
|
defaultAccess: validatedAccess(input.defaultAccess ?? "read"),
|
|
87
91
|
...(input.description === undefined
|
|
88
92
|
? {}
|
|
@@ -98,6 +102,10 @@ function normalizeInput(input) {
|
|
|
98
102
|
}
|
|
99
103
|
function normalizePatch(patch) {
|
|
100
104
|
const result = {};
|
|
105
|
+
if (patch.enabled !== undefined) {
|
|
106
|
+
validateEnabled(patch.enabled);
|
|
107
|
+
result.enabled = patch.enabled;
|
|
108
|
+
}
|
|
101
109
|
if (Object.hasOwn(patch, "defaultAccess")) {
|
|
102
110
|
result.defaultAccess = validatedAccess(patch.defaultAccess);
|
|
103
111
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isEnabled, roleDisabledReason } from "../role/availability.js";
|
|
1
2
|
import { createRoleAgentBinding } from "../role/role.js";
|
|
2
3
|
/** The single read model for persisted Profile intent and current runtime. */
|
|
3
4
|
export function resolveAgentProfileView(profile, store) {
|
|
@@ -13,7 +14,12 @@ export function resolveAgentProfileView(profile, store) {
|
|
|
13
14
|
* dormant, and overlays its own model/effort; another Agent starts from its
|
|
14
15
|
* provider defaults.
|
|
15
16
|
*/
|
|
16
|
-
export function resolveAgentProfileRuntime(profile, store) {
|
|
17
|
+
export function resolveAgentProfileRuntime(profile, store, options = {}) {
|
|
18
|
+
if (!options.configurationOnly && !isEnabled(profile))
|
|
19
|
+
return {
|
|
20
|
+
status: "unavailable", source: profile.runtime.source,
|
|
21
|
+
reason: `Agent Profile ${profile.id} is disabled. Use yui config profile enable ${profile.id}. Existing Task bindings are unchanged.`
|
|
22
|
+
};
|
|
17
23
|
if (profile.runtime.source === "global-worker") {
|
|
18
24
|
const worker = store.getGlobalRole("worker");
|
|
19
25
|
if (worker === null) {
|
|
@@ -23,6 +29,11 @@ export function resolveAgentProfileRuntime(profile, store) {
|
|
|
23
29
|
reason: "Global Role worker is not configured."
|
|
24
30
|
};
|
|
25
31
|
}
|
|
32
|
+
if (!options.configurationOnly && !isEnabled(worker))
|
|
33
|
+
return {
|
|
34
|
+
status: "unavailable", source: "global-worker", workerRevision: worker.launchRevision,
|
|
35
|
+
reason: roleDisabledReason(worker)
|
|
36
|
+
};
|
|
26
37
|
const binding = worker.agentBindings[worker.activeAgentId];
|
|
27
38
|
if (binding === undefined) {
|
|
28
39
|
return {
|
|
@@ -66,6 +77,10 @@ export function resolveAgentProfileRuntime(profile, store) {
|
|
|
66
77
|
}
|
|
67
78
|
const worker = store.getGlobalRole("worker");
|
|
68
79
|
const workerBinding = worker?.agentBindings[agent.id];
|
|
80
|
+
if (!options.configurationOnly && worker !== null && workerBinding !== undefined && !isEnabled(worker)) {
|
|
81
|
+
return { status: "unavailable", source: "explicit",
|
|
82
|
+
reason: `Profile copies the matching Worker binding. ${roleDisabledReason(worker)}` };
|
|
83
|
+
}
|
|
69
84
|
if (workerBinding !== undefined && workerBinding.adapterId !== agent.adapterId) {
|
|
70
85
|
return {
|
|
71
86
|
status: "unavailable",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { isEnabled, roleDisabledReason } from "../role/availability.js";
|
|
2
3
|
import { isCompletedTaskReviewEvidence } from "./reviewAcceptance.js";
|
|
3
4
|
import { projectReviewerAvailability } from "./reviewerAvailability.js";
|
|
4
5
|
/** Read-only facts for Leader judgment; this projection never blocks an action. */
|
|
@@ -43,6 +44,11 @@ export function projectReviewDecision(input) {
|
|
|
43
44
|
])].sort();
|
|
44
45
|
const reviewers = reviewerNames.map((reviewerRoleName) => {
|
|
45
46
|
const role = roles.find(({ name }) => name === reviewerRoleName);
|
|
47
|
+
const applicable = role ?? store.getGlobalRole(reviewerRoleName);
|
|
48
|
+
if (applicable !== null && !isEnabled(applicable))
|
|
49
|
+
return {
|
|
50
|
+
reviewerRoleName, status: "unavailable", retryable: false, reason: roleDisabledReason(applicable)
|
|
51
|
+
};
|
|
46
52
|
if (role === undefined && store.getGlobalRole(reviewerRoleName) === null) {
|
|
47
53
|
return {
|
|
48
54
|
reviewerRoleName,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Additive configuration: valid older records remain enabled. This is not
|
|
2
|
+
* Session state, launch configuration drift, or permission to stop a writer. */
|
|
3
|
+
export function isEnabled(value) {
|
|
4
|
+
return value.enabled !== false;
|
|
5
|
+
}
|
|
6
|
+
export function validateEnabled(value) {
|
|
7
|
+
if (value !== undefined && typeof value !== "boolean") {
|
|
8
|
+
throw new Error("Enabled must be a boolean.");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function parseEnabled(value) {
|
|
12
|
+
if (value !== "true" && value !== "false")
|
|
13
|
+
throw new Error("--enabled must be true or false.");
|
|
14
|
+
return value === "true";
|
|
15
|
+
}
|
|
16
|
+
export function roleDisabledReason(role) {
|
|
17
|
+
const target = role.taskId === undefined ? `config role enable ${role.name}`
|
|
18
|
+
: `task role enable ${role.taskId} ${role.name}`;
|
|
19
|
+
return `Role ${role.taskId === undefined ? "" : `${role.taskId}/`}${role.name} is disabled. `
|
|
20
|
+
+ `Use yui ${target} to allow new execution; existing execution is not stopped.`;
|
|
21
|
+
}
|
|
22
|
+
export class RoleDisabledError extends Error {
|
|
23
|
+
name = "RoleDisabledError";
|
|
24
|
+
}
|
|
25
|
+
export function assertRoleEnabled(role) {
|
|
26
|
+
if (!isEnabled(role))
|
|
27
|
+
throw new RoleDisabledError(roleDisabledReason(role));
|
|
28
|
+
}
|
package/dist/role/role.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { assertRoleEnabled, validateEnabled } from "./availability.js";
|
|
2
3
|
import { requireKnownFields } from "../domain/validation.js";
|
|
3
4
|
import { jobScopeKey } from "../job/jobDefinition.js";
|
|
4
5
|
import { validateExecutionEnvironmentSnapshot } from "../resources/projectResource.js";
|
|
@@ -44,10 +45,12 @@ export function createGlobalRole(name, bindings, activeAgentId, workspace, now,
|
|
|
44
45
|
}
|
|
45
46
|
export function copyGlobalRoleToTaskRole(globalRole, taskId, now, name = globalRole.name) {
|
|
46
47
|
validateGlobalRole(globalRole);
|
|
48
|
+
assertRoleEnabled(globalRole);
|
|
47
49
|
const timestamp = now.toISOString();
|
|
48
50
|
return validateTaskRole({
|
|
49
51
|
...cloneProfile(globalRole),
|
|
50
52
|
schemaVersion: 1,
|
|
53
|
+
enabled: true,
|
|
51
54
|
launchRevision: 1,
|
|
52
55
|
defaultAccess: globalRole.defaultAccess,
|
|
53
56
|
taskId: requireSafeIdentity(taskId, "Task id"),
|
|
@@ -219,6 +222,7 @@ function createRoleOwner(name, bindings, activeAgentId, workspace, now, profile,
|
|
|
219
222
|
const timestamp = now.toISOString();
|
|
220
223
|
return {
|
|
221
224
|
...cloneProfile(profile),
|
|
225
|
+
enabled: true,
|
|
222
226
|
launchRevision: 1,
|
|
223
227
|
defaultAccess,
|
|
224
228
|
name: requireSafeIdentity(name, "Role name"),
|
|
@@ -230,6 +234,7 @@ function createRoleOwner(name, bindings, activeAgentId, workspace, now, profile,
|
|
|
230
234
|
};
|
|
231
235
|
}
|
|
232
236
|
function validateRoleOwner(role) {
|
|
237
|
+
validateEnabled(role.enabled);
|
|
233
238
|
if (role.schemaVersion !== 1) {
|
|
234
239
|
throw new Error("Role schema version is invalid.");
|
|
235
240
|
}
|
|
@@ -298,6 +303,7 @@ function assertSessionOwnerMatchesRole(role, sessions) {
|
|
|
298
303
|
}
|
|
299
304
|
function cloneRolePatch(patch) {
|
|
300
305
|
return {
|
|
306
|
+
...(patch.enabled === undefined ? {} : { enabled: patch.enabled }),
|
|
301
307
|
...(patch.name === undefined ? {} : { name: requireSafeIdentity(patch.name, "Role name") }),
|
|
302
308
|
...(patch.activeAgentId === undefined
|
|
303
309
|
? {}
|
|
@@ -30,6 +30,10 @@ export function mapCodexAgentError(input) {
|
|
|
30
30
|
}
|
|
31
31
|
function mapCodexDisplayError(input) {
|
|
32
32
|
const text = `${input.message}\n${input.raw}`;
|
|
33
|
+
if (/Codex Session availability is systemError; no input was submitted\./u.test(text)) {
|
|
34
|
+
return { category: "session", code: "provider.session-system-error",
|
|
35
|
+
inputDisposition: "not-accepted", sessionDisposition: "unknown" };
|
|
36
|
+
}
|
|
33
37
|
if (/^server_error$/iu.test(input.message)) {
|
|
34
38
|
return recoverable("availability", "provider.server-error");
|
|
35
39
|
}
|
|
@@ -18,13 +18,23 @@ export function createCodexNativeAccess(threadId, initialized, channel, openingM
|
|
|
18
18
|
const listeners = new Set();
|
|
19
19
|
let configuration = initialConfiguration;
|
|
20
20
|
let observedAt = new Date().toISOString();
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// A start/resume response confirms configuration at that instant, not the
|
|
22
|
+
// current Turn or later changes made by another client. Keep its timestamp;
|
|
23
|
+
// consumers must not describe repeated reads as fresh native observations.
|
|
24
|
+
const observed = () => typeof configuration?.model !== "string" || !configuration.model.trim()
|
|
25
|
+
? unknownAgentRunConfiguration("No confirmed native configuration response. Current model and reasoning effort are unknown.")
|
|
23
26
|
: {
|
|
24
27
|
status: "observed", observedAt,
|
|
25
28
|
handshake: { status: "unsupported", reason: "Model reported by Codex thread/start or thread/resume; not a capability handshake." },
|
|
26
29
|
requested: [],
|
|
27
|
-
axes: [
|
|
30
|
+
axes: [
|
|
31
|
+
{ key: "model", category: "model", current: { status: "observed", value: configuration.model }, offered: [] },
|
|
32
|
+
{ key: "reasoningEffort", category: "effort",
|
|
33
|
+
current: typeof configuration.reasoningEffort === "string" && configuration.reasoningEffort.trim()
|
|
34
|
+
? { status: "observed", value: configuration.reasoningEffort }
|
|
35
|
+
: { status: "unobserved", reason: "The native response did not confirm reasoning effort." },
|
|
36
|
+
offered: [] }
|
|
37
|
+
]
|
|
28
38
|
};
|
|
29
39
|
const observe = (message) => {
|
|
30
40
|
const params = message.params;
|
|
@@ -86,9 +96,16 @@ export function createCodexNativeAccess(threadId, initialized, channel, openingM
|
|
|
86
96
|
if (params.threadId !== threadId)
|
|
87
97
|
throw new Error("Select the existing Session; new/forked Threads are not supported here.");
|
|
88
98
|
const page = params.initialTurnsPage;
|
|
89
|
-
|
|
99
|
+
// A failed/uncertain resume cannot leave the earlier confirmation
|
|
100
|
+
// looking valid. Do not retry this potentially applied operation.
|
|
101
|
+
configuration = undefined;
|
|
102
|
+
const result = await channel.request("thread/resume", { threadId,
|
|
90
103
|
...(typeof params.excludeTurns === "boolean" ? { excludeTurns: params.excludeTurns } : {}),
|
|
91
104
|
...(page ? { initialTurnsPage: { ...page, limit: Math.min(40, typeof page.limit === "number" && page.limit > 0 ? page.limit : 40) } } : {}) });
|
|
105
|
+
if (result.thread?.id !== threadId) {
|
|
106
|
+
throw new Error("Native resume did not confirm the selected Thread.");
|
|
107
|
+
}
|
|
108
|
+
configuration = result;
|
|
92
109
|
observedAt = new Date().toISOString();
|
|
93
110
|
return configuration;
|
|
94
111
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { currentProviderConversation } from "./providerRuntimeIdentity.js";
|
|
2
|
+
import { providerRetrySummary } from "./providerRetry.js";
|
|
3
|
+
/** Read-only provenance. Writer selection is not live Host/transport health. */
|
|
4
|
+
export function globalSessionHealth(role, sessions) {
|
|
5
|
+
const session = sessions?.sessions[role.activeAgentId];
|
|
6
|
+
const binding = sessions?.providerBinding;
|
|
7
|
+
const matches = session !== undefined && binding != null
|
|
8
|
+
&& sessions?.activeAgentId === role.activeAgentId
|
|
9
|
+
&& currentProviderConversation(binding).conversationId === session.nativeSessionId;
|
|
10
|
+
// Conversation replacement retains the terminal Turn as audit evidence while
|
|
11
|
+
// advancing writer authority. Matching the new conversation alone is insufficient.
|
|
12
|
+
const turnMatches = matches && binding.run?.authorityEpoch === binding.authority.epoch;
|
|
13
|
+
const turn = turnMatches ? binding.run : null;
|
|
14
|
+
const retry = matches && binding.retry?.nativeSessionId === session?.nativeSessionId
|
|
15
|
+
&& binding.retry?.authorityEpoch === binding.authority.epoch
|
|
16
|
+
? providerRetrySummary(binding) : null;
|
|
17
|
+
const failure = turn?.status === "failed" || turn?.status === "rejected" ? turn.failure : undefined;
|
|
18
|
+
return {
|
|
19
|
+
desiredRevision: role.launchRevision,
|
|
20
|
+
effectiveRevision: session?.effective.sourceDesiredRevision ?? null,
|
|
21
|
+
configurationDrift: session === undefined ? null
|
|
22
|
+
: session.effective.sourceDesiredRevision !== role.launchRevision,
|
|
23
|
+
nativeSessionId: session?.nativeSessionId ?? null,
|
|
24
|
+
nativeTurn: turn == null ? null : {
|
|
25
|
+
attemptId: turn.attemptId, nativeTurnId: turn.nativeTurnId, status: turn.status
|
|
26
|
+
},
|
|
27
|
+
currentFailure: failure === undefined ? null : {
|
|
28
|
+
ref: failure.ref, source: failure.error.source, phase: failure.error.phase,
|
|
29
|
+
category: failure.error.category, code: failure.error.code,
|
|
30
|
+
inputDisposition: failure.error.inputDisposition,
|
|
31
|
+
sessionDisposition: failure.error.sessionDisposition
|
|
32
|
+
},
|
|
33
|
+
retry,
|
|
34
|
+
historicalBinding: binding != null && (!matches || (binding.run !== null && !turnMatches)),
|
|
35
|
+
hostObservation: "unobserved",
|
|
36
|
+
note: "Selected/active writer state is not delivery or terminal-observation health. "
|
|
37
|
+
+ "Inspect exact Host capabilities and runtime diagnostics; Controller restart does not refresh a live Host."
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { RoleDisabledError } from "../role/availability.js";
|
|
2
3
|
import { resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { providerDeliveryFailureFrom } from "./agentError.js";
|
|
@@ -189,6 +190,8 @@ export class TmuxSessionHost {
|
|
|
189
190
|
: this.planner.planGlobalRole(input);
|
|
190
191
|
}
|
|
191
192
|
catch (error) {
|
|
193
|
+
if (error instanceof RoleDisabledError)
|
|
194
|
+
throw error;
|
|
192
195
|
throw toRuntimeLaunchFailure(error, "validation", {
|
|
193
196
|
cwd: request.workspace,
|
|
194
197
|
agentId: request.agentId
|
|
@@ -206,6 +209,8 @@ export class TmuxSessionHost {
|
|
|
206
209
|
await this.#validateLaunch(request);
|
|
207
210
|
}
|
|
208
211
|
catch (error) {
|
|
212
|
+
if (error instanceof RoleDisabledError)
|
|
213
|
+
throw error;
|
|
209
214
|
throw toRuntimeLaunchFailure(error, "validation", launchContext);
|
|
210
215
|
}
|
|
211
216
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { isEnabled, roleDisabledReason, RoleDisabledError } from "../role/availability.js";
|
|
2
3
|
import { serializeRunInputEnvelope } from "../context/runInputContract.js";
|
|
3
4
|
import { readRunContextSnapshot } from "../context/runContextPack.js";
|
|
4
5
|
import { roleSessionMayContinue, sameEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
@@ -37,6 +38,8 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
|
|
|
37
38
|
}
|
|
38
39
|
async function deliverActiveRun(store, delivery, task, role, run, now) {
|
|
39
40
|
const base = { taskId: task.id, roleName: role.name, runId: run.id };
|
|
41
|
+
if (!isEnabled(role))
|
|
42
|
+
return { ...base, status: "skipped", reason: "not-ready", error: roleDisabledReason(role) };
|
|
40
43
|
if (!isSchedulerTaskWorkspaceReady(task, store.getTaskWorkspace(task.id), run.purpose)) {
|
|
41
44
|
return { ...base, status: "skipped", reason: "workspace-not-ready" };
|
|
42
45
|
}
|
|
@@ -231,6 +234,12 @@ async function deliverActiveRun(store, delivery, task, role, run, now) {
|
|
|
231
234
|
}
|
|
232
235
|
catch (error) {
|
|
233
236
|
const message = error instanceof Error ? error.message : String(error);
|
|
237
|
+
// A configuration pause before submission preserves the original intent.
|
|
238
|
+
// After submission, the existing unknown-effect fence still owns recovery.
|
|
239
|
+
if (!submitted && error instanceof RoleDisabledError) {
|
|
240
|
+
forget(delivery, task.id, role.name, run.id);
|
|
241
|
+
return { ...base, status: "skipped", reason: "not-ready", error: message };
|
|
242
|
+
}
|
|
234
243
|
// Preserve the structured cause alongside the readable diagnosis.
|
|
235
244
|
const causeName = innermostCauseName(error);
|
|
236
245
|
store.recordAgentError({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { roleSessionMayContinue } from "../executor/effectiveLaunch.js";
|
|
2
|
+
import { isEnabled, roleDisabledReason, RoleDisabledError } from "../role/availability.js";
|
|
2
3
|
import { roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
|
|
3
4
|
import { isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
4
5
|
import { hasImmediateWakeReason } from "./wakeReason.js";
|
|
@@ -22,6 +23,10 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
22
23
|
results.push({ ...base, status: "skipped", reason: "unavailable" });
|
|
23
24
|
continue;
|
|
24
25
|
}
|
|
26
|
+
if (!isEnabled(role)) {
|
|
27
|
+
results.push({ ...base, status: "skipped", reason: "unavailable", error: roleDisabledReason(role) });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
25
30
|
if (!isSchedulerTaskWorkspaceReady(task, store.getTaskWorkspace(task.id), task.status === "draft" ? "planning" : "execution")) {
|
|
26
31
|
results.push({ ...base, status: "skipped", reason: "workspace-not-ready" });
|
|
27
32
|
continue;
|
|
@@ -104,9 +109,11 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
104
109
|
// permits another automatic launch. A config/auth/executable failure
|
|
105
110
|
// needs correction; an exception after submit cannot authorize replay.
|
|
106
111
|
const retryable = error instanceof RuntimeLifecycleBusyError
|
|
112
|
+
|| error instanceof RoleDisabledError
|
|
107
113
|
|| (error instanceof RuntimeLaunchError && error.retryable);
|
|
108
114
|
store.settleLeaderNotification(task.id, notification.attemptId, attempted ? "unknown" : retryable ? "deferred" : "rejected", now, detail, { effective, raw: detail, phase: attempted ? "turn-submit" : "session-start" });
|
|
109
|
-
results.push({ ...base, status: "
|
|
115
|
+
results.push({ ...base, status: !attempted && error instanceof RoleDisabledError ? "skipped" : "failed",
|
|
116
|
+
reason: "not-ready", error: detail });
|
|
110
117
|
}
|
|
111
118
|
finally {
|
|
112
119
|
delivery.forgetPrepared?.({ taskId: task.id, roleName: role.name });
|