@zq-silk/yui 2.16.0 → 2.17.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 +17 -11
- package/dist/cli/interactionCandidates.js +20 -5
- package/dist/cli/interactionPolicy.js +5 -4
- package/dist/cli/roleWizard.js +7 -0
- package/dist/commands/globalRoleCommands.js +13 -2
- package/dist/commands/operatorCommands.js +2 -0
- 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/controller/agentJobControl.js +9 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +35 -4
- package/dist/controller/globalInputDelivery.js +14 -8
- package/dist/controller/providerRetryAdmission.js +9 -0
- package/dist/controller/providerRetryDelivery.js +16 -0
- package/dist/controller/runtime.js +14 -0
- package/dist/controller/runtimeLaunchCoordinator.js +3 -1
- package/dist/executor/fileRoleLaunchPlanner.js +3 -0
- 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/codexNativeAccess.js +21 -4
- 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/storage/sqliteSchema.js +10 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/web/assets/client/app/main.js +15 -22
- package/dist/web/assets/client/app/taskView.js +7 -11
- package/dist/web/assets/client/i18n/en.js +62 -18
- package/dist/web/assets/client/i18n/zh-CN.js +62 -18
- package/dist/web/assets/client/index.js +2 -0
- 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/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/overview.js +2 -2
- package/dist/web/assets/client/views/task/page.js +2 -12
- package/dist/web/assets/shell/dock.js +9 -27
- 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/task.js +1 -2
- package/dist/web/assets/styles/views/terminal.js +12 -8
- package/dist/web/webConversation.js +1 -0
- package/dist/web/webSettings.js +10 -2
- package/docs/managed-turn-and-session-runtime.md +10 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +6 -0
- package/docs/release-workflow.md +24 -0
- package/docs/release-workflow.zh-CN.md +19 -0
- package/docs/roles-and-configuration.md +45 -1
- package/docs/roles-and-configuration.zh-CN.md +35 -0
- package/docs/storage-baseline.md +6 -1
- package/docs/storage-baseline.zh-CN.md +5 -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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isEnabled } from "../role/availability.js";
|
|
1
2
|
import { sendAgentHostRunControl, reconcileAgentHostTurn, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
|
|
2
3
|
import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
|
|
3
4
|
import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
@@ -25,14 +26,6 @@ export async function deliverGlobalInputs(home, store, ensure, onError, options
|
|
|
25
26
|
if (sessions !== null && selected?.status !== "active"
|
|
26
27
|
&& !(role.jobOwner && selected === undefined))
|
|
27
28
|
continue;
|
|
28
|
-
if (sessions?.providerBinding?.retry !== undefined && !providerRetryPending(sessions.providerBinding)) {
|
|
29
|
-
store.transaction(tx => settleGlobalRetryInput(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, new Date()));
|
|
30
|
-
pending = store.queryGlobalRoleMessages(role.name, { pending: true, unclaimed: true, limit: 64 }).messages;
|
|
31
|
-
if (pending.length === 0)
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
if (providerRetryPending(sessions?.providerBinding))
|
|
35
|
-
continue;
|
|
36
29
|
let current = sessions?.providerBinding?.run;
|
|
37
30
|
const bindingBefore = sessions?.providerBinding;
|
|
38
31
|
if (options.reconcile && current != null
|
|
@@ -95,6 +88,19 @@ export async function deliverGlobalInputs(home, store, ensure, onError, options
|
|
|
95
88
|
if (pending.length === 0)
|
|
96
89
|
continue;
|
|
97
90
|
}
|
|
91
|
+
// Availability blocks new work, not observation of the original writer's
|
|
92
|
+
// exact terminal. Re-read after the asynchronous reconciliation above.
|
|
93
|
+
const currentRole = store.getGlobalRole(role.name);
|
|
94
|
+
if (currentRole === null || !isEnabled(currentRole))
|
|
95
|
+
continue;
|
|
96
|
+
if (sessions?.providerBinding?.retry !== undefined && !providerRetryPending(sessions.providerBinding)) {
|
|
97
|
+
store.transaction(tx => settleGlobalRetryInput(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, new Date()));
|
|
98
|
+
pending = store.queryGlobalRoleMessages(role.name, { pending: true, unclaimed: true, limit: 64 }).messages;
|
|
99
|
+
if (pending.length === 0)
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (providerRetryPending(sessions?.providerBinding))
|
|
103
|
+
continue;
|
|
98
104
|
const recovery = globalInboxRecovery(store, role.name, options.now ?? new Date());
|
|
99
105
|
const recovering = recovery !== undefined && recovery.inbox === undefined && recovery.delivery === undefined
|
|
100
106
|
&& recovery.notDelivered === undefined && recovery.control === undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertRoleEnabled, isEnabled } from "../role/availability.js";
|
|
1
2
|
import { admitProviderRetry, cancelProviderRetry, providerRetryAttemptId, providerRetryPending } from "../runtime/providerRetry.js";
|
|
2
3
|
import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
3
4
|
import { validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
|
|
@@ -20,6 +21,8 @@ export function ownedProviderInput(input) {
|
|
|
20
21
|
export function retryIntentBlocker(store, input, retry) {
|
|
21
22
|
if (input.taskId === undefined) {
|
|
22
23
|
const role = store.getGlobalRole(input.roleName);
|
|
24
|
+
if (role !== null && !isEnabled(role))
|
|
25
|
+
return "role-disabled";
|
|
23
26
|
const sessions = store.getGlobalRoleSessionSet(input.roleName);
|
|
24
27
|
if (role === null || sessions?.activeAgentId !== role.activeAgentId)
|
|
25
28
|
return "role-changed";
|
|
@@ -38,6 +41,8 @@ export function retryIntentBlocker(store, input, retry) {
|
|
|
38
41
|
|| task.executionGate.state !== "enabled")
|
|
39
42
|
return "task-execution-stopped";
|
|
40
43
|
const role = store.getRole(input.taskId, input.roleName);
|
|
44
|
+
if (role !== null && !isEnabled(role))
|
|
45
|
+
return "role-disabled";
|
|
41
46
|
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
42
47
|
if (role === null || sessions?.activeAgentId !== role.activeAgentId)
|
|
43
48
|
return "role-changed";
|
|
@@ -54,6 +59,10 @@ export function retryIntentBlocker(store, input, retry) {
|
|
|
54
59
|
/** Shared by automatic recovery and every ordinary Host submit. The native
|
|
55
60
|
* writer CAS remains the only grant; a scheduled successor is not a grant. */
|
|
56
61
|
export function admitOwnedProviderInput(store, binding, input) {
|
|
62
|
+
const role = input.taskId === undefined ? store.getGlobalRole(input.roleName)
|
|
63
|
+
: store.getRole(input.taskId, input.roleName);
|
|
64
|
+
if (role !== null)
|
|
65
|
+
assertRoleEnabled(role);
|
|
57
66
|
const retry = binding.retry;
|
|
58
67
|
const sameReviewSuccessor = retry?.successorReviewRoundId !== undefined && input.taskId !== undefined && input.runId !== undefined
|
|
59
68
|
&& store.getRun(input.taskId, input.runId)?.reviewRoundId === retry.successorReviewRoundId;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { isEnabled } from "../role/availability.js";
|
|
2
3
|
import { runTaskCommand } from "../commands/taskCommands.js";
|
|
3
4
|
import { routeRoleEvent } from "../scheduler/operatorEvent.js";
|
|
4
5
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
@@ -64,12 +65,19 @@ export function createProviderRetryHooks(home, store, ports = {}) {
|
|
|
64
65
|
const key = (set) => set.owner.scope === "task"
|
|
65
66
|
? `role:${encodeURIComponent(set.owner.taskId)}/${encodeURIComponent(set.owner.roleName)}`
|
|
66
67
|
: `global-role:${encodeURIComponent(set.owner.roleName)}`;
|
|
68
|
+
const disabled = (set) => {
|
|
69
|
+
const role = set.owner.scope === "task"
|
|
70
|
+
? store.getRole(set.owner.taskId, set.owner.roleName) : store.getGlobalRole(set.owner.roleName);
|
|
71
|
+
return role !== null && !isEnabled(role);
|
|
72
|
+
};
|
|
67
73
|
return {
|
|
68
74
|
deadlines() {
|
|
69
75
|
return store.listProviderRetrySessions().flatMap(set => {
|
|
70
76
|
const retry = set.providerBinding.retry;
|
|
71
77
|
if (retry.status !== "waiting")
|
|
72
78
|
return [];
|
|
79
|
+
if (disabled(set))
|
|
80
|
+
return [];
|
|
73
81
|
// Busy or already-queued work awaits the next ordinary pass without a
|
|
74
82
|
// zero-delay timer loop. Waiting never consumes an execution attempt.
|
|
75
83
|
const wall = (ports.now ?? (() => new Date()))().getTime();
|
|
@@ -97,6 +105,10 @@ export function createProviderRetryHooks(home, store, ports = {}) {
|
|
|
97
105
|
const scoped = { ...(owner.scope === "task" ? { taskId: owner.taskId } : {}), roleName: owner.roleName };
|
|
98
106
|
const session = set.sessions[set.activeAgentId];
|
|
99
107
|
const blocker = retryIntentBlocker(store, scoped, retry);
|
|
108
|
+
// Configuration pause neither settles input nor cancels an already
|
|
109
|
+
// admitted attempt. Normal ingress still records its exact terminal.
|
|
110
|
+
if (blocker === "role-disabled")
|
|
111
|
+
continue;
|
|
100
112
|
if (blocker !== undefined && blocker !== "background-work-unsettled") {
|
|
101
113
|
stop(set, blocker, at);
|
|
102
114
|
continue;
|
|
@@ -175,6 +187,10 @@ export function createProviderRetryHooks(home, store, ports = {}) {
|
|
|
175
187
|
|| latest.status !== "waiting")
|
|
176
188
|
return;
|
|
177
189
|
const reason = retryIntentBlocker(store, scoped, latest);
|
|
190
|
+
// Snapshotting yields to configuration changes. Apply the same
|
|
191
|
+
// pause semantics as the pre-await check, without settling intent.
|
|
192
|
+
if (reason === "role-disabled")
|
|
193
|
+
return;
|
|
178
194
|
if (reason !== undefined)
|
|
179
195
|
throw new Error(reason);
|
|
180
196
|
runTaskCommand(["run", "retry", `${owner.taskId}/${previous.id}`], store, {
|
|
@@ -2,6 +2,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
2
2
|
import { prepareNativeHostSteer } from "./nativeHostInput.js";
|
|
3
3
|
import { archiveLeaderTask } from "../task/leaderArchive.js";
|
|
4
4
|
import { TaskWorkspaceCoordinator } from "../repository/taskWorkspaceCoordinator.js";
|
|
5
|
+
import { assertRoleEnabled, RoleDisabledError } from "../role/availability.js";
|
|
5
6
|
import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, NATIVE_AGENT_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
|
|
6
7
|
import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
|
|
7
8
|
import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveDeliveryTimeoutSeconds, resolveRuntimeHealth, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
|
|
@@ -740,6 +741,17 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
740
741
|
});
|
|
741
742
|
}
|
|
742
743
|
catch (error) {
|
|
744
|
+
if (error instanceof RoleDisabledError) {
|
|
745
|
+
const turn = (value.taskId === undefined
|
|
746
|
+
? store.getGlobalRoleSessionSet(value.roleName)
|
|
747
|
+
: store.getTaskRoleSessionSet(value.taskId, value.roleName))?.providerBinding?.run;
|
|
748
|
+
// The admission transaction rolled back. Only a new, unregistered
|
|
749
|
+
// input may use the existing temporary-backpressure wire contract.
|
|
750
|
+
// A matching prior registration remains fenced, never a fresh retry.
|
|
751
|
+
if (turn?.attemptId !== value.attemptId) {
|
|
752
|
+
throw applicationError("SESSION_BUSY", error.message);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
743
755
|
if (error instanceof AgentHostProviderSessionBusyError)
|
|
744
756
|
throw error;
|
|
745
757
|
if (error instanceof AgentHostProviderTurnFenceError) {
|
|
@@ -960,6 +972,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
960
972
|
? activeRun?.workspace
|
|
961
973
|
?? currentDesiredManagedWorkspace(store, request.taskId, request.roleName)
|
|
962
974
|
: undefined;
|
|
975
|
+
assertRoleEnabled(role);
|
|
963
976
|
// Only Draft planning may launch without an adopted delivery workspace.
|
|
964
977
|
const workspaceFree = request.scope === "task"
|
|
965
978
|
&& managedWorkspace === undefined
|
|
@@ -1082,6 +1095,7 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
|
|
|
1082
1095
|
if (role === null) {
|
|
1083
1096
|
throw new Error(`Role no longer exists: ${request.owner.roleName}.`);
|
|
1084
1097
|
}
|
|
1098
|
+
assertRoleEnabled(role);
|
|
1085
1099
|
const sessions = request.owner.scope === "task"
|
|
1086
1100
|
? store.getTaskRoleSessionSet(request.owner.taskId, request.owner.roleName)
|
|
1087
1101
|
: store.getGlobalRoleSessionSet(request.owner.roleName);
|
|
@@ -92,7 +92,9 @@ export class RuntimeLaunchCoordinator {
|
|
|
92
92
|
...(request.runId === undefined ? {} : { runId: request.runId }),
|
|
93
93
|
...(request.environment === undefined ? {} : { environment: request.environment })
|
|
94
94
|
};
|
|
95
|
-
|
|
95
|
+
// Even without a startup-facts consumer, recheck after asynchronous Host
|
|
96
|
+
// validation and immediately before creating the process.
|
|
97
|
+
const callback = beforeHostStart === undefined ? () => assertLaunchCurrent() : observePreflight;
|
|
96
98
|
if (request.mode === "resume" && !request.nativeSessionId) {
|
|
97
99
|
throw new Error("Restoring a Session requires its native session id.");
|
|
98
100
|
}
|
|
@@ -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.");
|
|
@@ -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
|
? {}
|
|
@@ -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
|
}
|
|
@@ -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 });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isEnabled } from "../role/availability.js";
|
|
1
2
|
import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
|
|
2
3
|
import { formatTaskRecordReference } from "../task/taskRecordReference.js";
|
|
3
4
|
import { selectedActiveSchedulerTasks, selectedSchedulerRoles } from "./ports.js";
|
|
@@ -104,6 +105,8 @@ export async function reconcileExitedRoleRuns(store, delivery, now, selection, e
|
|
|
104
105
|
observedAt: now
|
|
105
106
|
});
|
|
106
107
|
}
|
|
108
|
+
if (!isEnabled(role))
|
|
109
|
+
continue;
|
|
107
110
|
if (session?.nativeSessionId === undefined) {
|
|
108
111
|
store.queueTaskProgress(task.id, "host-missing-native-resume-unproven", now);
|
|
109
112
|
continue;
|
|
@@ -243,6 +243,16 @@ const MINOR_UPGRADES = Object.freeze([{
|
|
|
243
243
|
targetChecksum: CURRENT_SCHEMA_CHECKSUM,
|
|
244
244
|
sql: "",
|
|
245
245
|
dataMigration: "project-sync-clock"
|
|
246
|
+
}, {
|
|
247
|
+
fromVersion: "1.21",
|
|
248
|
+
toVersion: "1.22",
|
|
249
|
+
name: "role-profile-availability",
|
|
250
|
+
introducedIn: "next",
|
|
251
|
+
sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
|
|
252
|
+
targetChecksum: CURRENT_SCHEMA_CHECKSUM,
|
|
253
|
+
// Optional enabled flag; absence means enabled. Preserve all historical
|
|
254
|
+
// Role/Profile bytes, frozen contexts and Session snapshots.
|
|
255
|
+
sql: ""
|
|
246
256
|
}]);
|
|
247
257
|
export function storageMinorUpgradePlan(from) {
|
|
248
258
|
if (from === CURRENT_STORAGE_VERSION)
|