@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.
Files changed (69) hide show
  1. package/dist/cli/commandCatalog.js +17 -11
  2. package/dist/cli/interactionCandidates.js +20 -5
  3. package/dist/cli/interactionPolicy.js +5 -4
  4. package/dist/cli/roleWizard.js +7 -0
  5. package/dist/commands/globalRoleCommands.js +13 -2
  6. package/dist/commands/operatorCommands.js +2 -0
  7. package/dist/commands/profileCommands.js +19 -2
  8. package/dist/commands/roleConfiguration.js +3 -0
  9. package/dist/commands/taskCommands.js +16 -0
  10. package/dist/commands/taskRoleRuntimeStatus.js +3 -0
  11. package/dist/controller/agentJobControl.js +9 -1
  12. package/dist/controller/fileSchedulerStoreAdapter.js +35 -4
  13. package/dist/controller/globalInputDelivery.js +14 -8
  14. package/dist/controller/providerRetryAdmission.js +9 -0
  15. package/dist/controller/providerRetryDelivery.js +16 -0
  16. package/dist/controller/runtime.js +14 -0
  17. package/dist/controller/runtimeLaunchCoordinator.js +3 -1
  18. package/dist/executor/fileRoleLaunchPlanner.js +3 -0
  19. package/dist/message/messageContinuation.js +4 -0
  20. package/dist/output/rolePresentation.js +3 -0
  21. package/dist/profile/agentProfile.js +8 -0
  22. package/dist/profile/agentProfileRuntime.js +16 -1
  23. package/dist/review/reviewDecision.js +6 -0
  24. package/dist/role/availability.js +28 -0
  25. package/dist/role/role.js +6 -0
  26. package/dist/runtime/codexNativeAccess.js +21 -4
  27. package/dist/runtime/tmuxAdapters.js +5 -0
  28. package/dist/scheduler/activeRoleRunDelivery.js +9 -0
  29. package/dist/scheduler/leaderWakeupProcessor.js +8 -1
  30. package/dist/scheduler/roleRunLiveness.js +3 -0
  31. package/dist/storage/sqliteSchema.js +10 -0
  32. package/dist/storage/storageVersions.js +1 -1
  33. package/dist/web/assets/client/app/main.js +15 -22
  34. package/dist/web/assets/client/app/taskView.js +7 -11
  35. package/dist/web/assets/client/i18n/en.js +62 -18
  36. package/dist/web/assets/client/i18n/zh-CN.js +62 -18
  37. package/dist/web/assets/client/index.js +2 -0
  38. package/dist/web/assets/client/layout/shortcuts.js +2 -2
  39. package/dist/web/assets/client/layout/workspace.js +99 -112
  40. package/dist/web/assets/client/ui/primitives.js +15 -2
  41. package/dist/web/assets/client/views/dock/conversation.js +421 -123
  42. package/dist/web/assets/client/views/dock/session.js +229 -0
  43. package/dist/web/assets/client/views/dock/terminal.js +48 -39
  44. package/dist/web/assets/client/views/overview.js +2 -2
  45. package/dist/web/assets/client/views/task/page.js +2 -12
  46. package/dist/web/assets/shell/dock.js +9 -27
  47. package/dist/web/assets/styles/views/conversation.js +96 -17
  48. package/dist/web/assets/styles/views/dock.js +17 -5
  49. package/dist/web/assets/styles/views/task.js +1 -2
  50. package/dist/web/assets/styles/views/terminal.js +12 -8
  51. package/dist/web/webConversation.js +1 -0
  52. package/dist/web/webSettings.js +10 -2
  53. package/docs/managed-turn-and-session-runtime.md +10 -0
  54. package/docs/managed-turn-and-session-runtime.zh-CN.md +6 -0
  55. package/docs/release-workflow.md +24 -0
  56. package/docs/release-workflow.zh-CN.md +19 -0
  57. package/docs/roles-and-configuration.md +45 -1
  58. package/docs/roles-and-configuration.zh-CN.md +35 -0
  59. package/docs/storage-baseline.md +6 -1
  60. package/docs/storage-baseline.zh-CN.md +5 -1
  61. package/docs/testing/review-evaluation.md +35 -1
  62. package/docs/testing/review-evaluation.zh-CN.md +25 -1
  63. package/docs/testing/verification-levels.md +2 -2
  64. package/docs/testing/verification-levels.zh-CN.md +2 -2
  65. package/package.json +1 -1
  66. package/skills/yui-leader/VERSION +1 -1
  67. package/skills/yui-leader/references/execution.md +19 -8
  68. package/skills/yui-reviewer/SKILL.md +13 -2
  69. 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
- const callback = beforeHostStart === undefined ? undefined : observePreflight;
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
- const observed = () => typeof configuration?.model !== "string"
22
- ? unknownAgentRunConfiguration("No confirmed native model response. Reconnect to restore the fixed launch configuration.")
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: [{ key: "model", category: "model", current: { status: "observed", value: configuration.model }, offered: [] }]
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
- configuration = await channel.request("thread/resume", { threadId,
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: "failed", reason: "not-ready", error: detail });
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)
@@ -1,4 +1,4 @@
1
- export const CURRENT_STORAGE_VERSION = "1.21";
1
+ export const CURRENT_STORAGE_VERSION = "1.22";
2
2
  export const MIN_SUPPORTED_STORAGE_VERSION = "1.0";
3
3
  export const STORAGE_FORMAT = "yui-home";
4
4
  export function isStorageVersion(value) {