@zq-silk/yui 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,10 @@ execution and continuity, but they are not competing sources of Task truth.
15
15
 
16
16
  The current implementation restores the useful Role/Agent/session and CLI framework without restoring the later data-maintenance, lease, schedule, and recovery-ledger systems.
17
17
 
18
+ The [target architecture handbook](docs/architecture/README.md) preserves the
19
+ 2026-09-06 design baseline for the upcoming refactor; it does not describe
20
+ already implemented behavior.
21
+
18
22
  ## Requirements
19
23
 
20
24
  - Node.js 20.17+, 22.9+, or 24.x
@@ -599,7 +603,11 @@ future launch. Each Turn and native Role Session stores the complete actual
599
603
  agent, adapter, model, effort, Profile access intent, exact writable Projects,
600
604
  permission strategy and native options, workspace, context, and source desired revision. Updating,
601
605
  switching, or clearing Role overrides never
602
- hot-mutates an existing process. `task context`, Role views, Turn history,
606
+ hot-mutates an existing process. When the Role has a live Session,
607
+ `task role update`, `config role update`, and `config agent update` report that
608
+ Session once and require `--yes`, so the change is recorded in the knowledge
609
+ that it applies to the next activation; stopping the Session applies it
610
+ immediately instead. `task context`, Role views, Turn history,
603
611
  Events, and Web show desired/effective revisions, Profile intent, permission, and
604
612
  pending next-launch drift.
605
613
 
@@ -909,7 +917,11 @@ Session/Driver lifecycle facts.
909
917
  Global Operator and global Role sessions remain native interactive CLIs. Codex
910
918
  connects that TUI to the default shared App Server, so the same thread can move
911
919
  between Yui and Desktop without transferring a rollout writer or losing its
912
- Global Context entry:
920
+ Global Context entry. A thin Host in the same pane transparently forwards the
921
+ native TUI's App Server connection and acknowledges its exact `thread/start`
922
+ or `thread/resume` response. Yui records that Thread ID before the first user
923
+ Turn, without depending on `notify`, scanning history, or creating a bootstrap
924
+ message. The attachment outlives Controller restarts but exits with the TUI:
913
925
 
914
926
  ```sh
915
927
  yui session enter <global-role>
@@ -934,12 +946,23 @@ running, Yui asks before stopping it and switching the conversation. On a
934
946
  cross-Agent switch, the saved model and effort are reused unless the user
935
947
  explicitly chooses to update them.
936
948
 
937
- The Role's active binding is desired state for the next compatible launch. A
949
+ Window existence does not imply a running Agent: `pane_dead=0` is live and
950
+ `pane_dead=1` is an exited, retained diagnostic pane. Status reads never delete
951
+ that evidence, and unreadable state is an error rather than proof of exit.
952
+ An explicit launch can rebuild the exact dead window; tmux refuses to replace
953
+ a live pane. An unidentified live Operator still cannot be overwritten.
954
+
955
+ The Role's active binding is desired state for the next launch. A
938
956
  running Turn and its native Session continue under their immutable
939
- effective snapshot even if the Role is edited or switched. Resume is allowed
940
- only when the complete effective snapshot and workspace remain compatible;
941
- otherwise Yui starts a new Session after the old process has stopped and keeps
942
- the terminal Session's immutable effective snapshot in history. Managed
957
+ effective snapshot even if the Role is edited or switched. Resume is refused
958
+ only when continuation is impossible: no recoverable native Session, a
959
+ different Agent or adapter, or a different physical workspace. Desired launch
960
+ configuration such as model, effort, permission, Role context, Skills, or
961
+ declared write scope shapes the next activation instead of ending the Session,
962
+ and Turn-scoped facts such as ReviewRound identity or candidate commits never
963
+ affect reuse. When continuation is impossible Yui starts a new Session after
964
+ the old process has stopped and keeps the terminal Session's immutable
965
+ effective snapshot in history. Managed
943
966
  Sessions invoke the ordinary `yui` command; their Manifest and durable
944
967
  Role/Turn fences authenticate scope while protocol and storage compatibility
945
968
  allow a CLI package or Controller upgrade in place. Exact internal callbacks
@@ -949,8 +972,9 @@ Use `yui config role unbind <global-role> <agent-id>` or `yui task role unbind <
949
972
 
950
973
  Claude session IDs are preallocated at launch. Codex discovers its native
951
974
  thread identity from App Server responses. Managed Task Turns use structured
952
- Provider observations for both CLIs. Global interactive Codex sessions may
953
- still use its `notify` callback for conversation presentation.
975
+ Provider observations for both CLIs. Global Codex uses the native TUI's exact
976
+ App Server startup response and the existing Host acknowledgement. Legacy
977
+ global `notify` callbacks cannot register a Session or change its lifecycle.
954
978
 
955
979
  Automated lifecycle and delivery decisions use structured Provider events or
956
980
  supported Hook payloads, persisted identities, usage snapshots, tmux process state, receipts, and pane
@@ -118,7 +118,7 @@ const agentChildren = [
118
118
  name: "update",
119
119
  summary: "Update a configured Agent.",
120
120
  usage: "yui config agent update <id> [--adapter <adapter>] [--command <command>] [--arg <arg> ... | --clear-args] [--env TARGET=PROCESS_NAME ... | --clear-env]",
121
- options: ["--adapter", "--command", "--arg", "--clear-args", "--env", "--clear-env"],
121
+ options: ["--adapter", "--command", "--arg", "--clear-args", "--env", "--clear-env", "--yes"],
122
122
  optionValues: { "--adapter": supportedAgentAdapterIds() },
123
123
  executableOptions: ["--command"]
124
124
  },
@@ -170,7 +170,7 @@ const roleChildren = [
170
170
  summary: "Update a global Role.",
171
171
  usage: "yui config role update <name> [profile options] [clear options]",
172
172
  options: ["--agent", "--workspace", ...roleProfileOptions, ...roleAgentOptions,
173
- ...roleProfileClearOptions, ...roleAgentClearOptions],
173
+ ...roleProfileClearOptions, ...roleAgentClearOptions, "--yes"],
174
174
  optionValues: roleAgentOptionValues,
175
175
  fileOptions: ["--workspace"]
176
176
  },
@@ -578,7 +578,7 @@ const taskChildren = [
578
578
  summary: "Update a Task Role; Agent settings target the named or active binding without switching it.",
579
579
  usage: "yui task role update <task> <role> [--profile <id>] [--agent <id>] [Role and Agent settings]",
580
580
  options: ["--profile", "--agent", ...roleProfileOptions, ...roleAgentOptions,
581
- ...roleProfileClearOptions, ...roleAgentClearOptions],
581
+ ...roleProfileClearOptions, ...roleAgentClearOptions, "--yes"],
582
582
  optionValues: roleAgentOptionValues
583
583
  },
584
584
  { name: "remove", summary: "Remove a Task Role.", usage: "yui task role remove <task> <role>" },
@@ -1132,7 +1132,7 @@ export const ROOT_COMMAND = buildNode({
1132
1132
  ...CONFIG_DOMAINS.map(durableConfigDomainNode),
1133
1133
  {
1134
1134
  name: "agent",
1135
- summary: "Manage configured native Agent CLIs; launch-setting changes require affected Sessions to be stopped.",
1135
+ summary: "Manage configured native Agent CLIs; launch-setting changes apply to the next Session activation.",
1136
1136
  examples: ["yui config agent list", "yui config agent capabilities codex"],
1137
1137
  sections: [
1138
1138
  { id: "inspect", title: "Inspect", entries: ["list", "show", "capabilities"] },
@@ -1152,7 +1152,7 @@ export const ROOT_COMMAND = buildNode({
1152
1152
  },
1153
1153
  {
1154
1154
  name: "role",
1155
- summary: "Manage reusable global Roles and desired Agent launch configuration for the next compatible Session.",
1155
+ summary: "Manage reusable global Roles and desired Agent launch configuration for the next Host activation.",
1156
1156
  examples: ["yui config role list", "yui config role show operator"],
1157
1157
  sections: [
1158
1158
  { id: "inspect", title: "Inspect", entries: ["list", "show"] },
package/dist/cli.js CHANGED
@@ -2092,10 +2092,10 @@ async function executeOperatorSessionControl(control, home, store, runtime, tmux
2092
2092
  throw usageError("Operator is not configured. Run yui setup first.");
2093
2093
  const sessionSet = store.getGlobalRoleSessionSet(role.name);
2094
2094
  const active = sessionSet?.sessions[sessionSet.activeAgentId];
2095
- const paneRunning = tmux.detectRoleStatus("operator", "operator") === "running";
2095
+ const paneRunning = tmux.probeRoleStatus("operator", "operator") === "running";
2096
2096
  if (paneRunning && active === undefined) {
2097
2097
  throw usageError("Operator is running but its native session has not been recorded yet. "
2098
- + "Wait for the first turn to settle before switching sessions.");
2098
+ + "Record the exact native session before switching sessions.");
2099
2099
  }
2100
2100
  if (control.action === "resume"
2101
2101
  && paneRunning
@@ -2,6 +2,7 @@ import { isDeepStrictEqual } from "node:util";
2
2
  import { agentNotFound, usageError } from "../errors/cliError.js";
3
3
  import { defaultTableWidth, renderTable } from "../output/table.js";
4
4
  import { createConfiguredAgent, validateConfiguredAgent } from "../agent/agent.js";
5
+ import { LIVE_SESSION_ACKNOWLEDGEMENT_OPTION } from "./roleRuntimeGuard.js";
5
6
  import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
6
7
  const SUPPORTED_ADAPTERS = Object.freeze(["codex", "claude"]);
7
8
  export function runAgentCommand(args, store) {
@@ -72,7 +73,8 @@ function updateAgent(args, store) {
72
73
  const [rawId, ...tail] = args;
73
74
  const id = agentId(rawId);
74
75
  const parsed = parseAgentOptions(tail, "update");
75
- if (parsed.seen.size === 0) {
76
+ if (parsed.seen.size === 0
77
+ || [...parsed.seen].every((option) => option === LIVE_SESSION_ACKNOWLEDGEMENT_OPTION)) {
76
78
  throw usageError("Agent update requires at least one operational option.");
77
79
  }
78
80
  if (parsed.has("--arg") && parsed.has("--clear-args")) {
@@ -113,10 +115,13 @@ function updateAgent(args, store) {
113
115
  + "Wait for lifecycle reconciliation to finish before changing Agent launch settings.");
114
116
  }
115
117
  const liveSession = findNonStoppedSessionReference(tx, id);
116
- if (liveSession !== null) {
117
- throw usageError(`Agent ${id} cannot be updated because ${describeReference(liveSession)} `
118
- + `retains a non-stopped native session (${liveSession.status}). `
119
- + "Stop that Role session before changing Agent launch settings.");
118
+ if (liveSession !== null && !parsed.has(LIVE_SESSION_ACKNOWLEDGEMENT_OPTION)) {
119
+ throw usageError(`${describeReference(liveSession)} runs a live native session (${liveSession.status}) on `
120
+ + `Agent ${id}, so this change applies to its next Host activation instead of the `
121
+ + "running one.\n"
122
+ + `Re-run with ${LIVE_SESSION_ACKNOWLEDGEMENT_OPTION} to record the change and keep that `
123
+ + "session.\n"
124
+ + "Stop the affected Role session first to apply it to a fresh session instead.");
120
125
  }
121
126
  if (changes.adapter) {
122
127
  const profile = findAgentProfileReference(tx, id);
@@ -273,7 +278,9 @@ function parseAgentOptions(args, mode) {
273
278
  ["--arg", { repeatable: true, allowOptionLikeValue: true }],
274
279
  ["--env", { repeatable: true, allowOptionLikeValue: false }]
275
280
  ]);
276
- const flags = mode === "update" ? new Set(["--clear-args", "--clear-env"]) : new Set();
281
+ const flags = mode === "update"
282
+ ? new Set(["--clear-args", "--clear-env", LIVE_SESSION_ACKNOWLEDGEMENT_OPTION])
283
+ : new Set();
277
284
  const seen = new Set();
278
285
  const values = new Map();
279
286
  for (let index = 0; index < args.length; index += 1) {
@@ -5,9 +5,9 @@ import { defaultTableWidth, renderTable } from "../output/table.js";
5
5
  import { activeRoleSummary, renderRoleDetails } from "../output/rolePresentation.js";
6
6
  import { activeRoleAgentBinding, createGlobalRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateGlobalRole } from "../role/role.js";
7
7
  import { isSystemRoleName, SYSTEM_ROLE_NAMES, systemRoleDescription } from "../role/systemRoles.js";
8
- import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfileFrom, roleProfilePatch } from "./roleConfiguration.js";
8
+ import { hasAgentConfigOptions, hasNoRoleMutation, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfileFrom, roleProfilePatch } from "./roleConfiguration.js";
9
9
  import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
10
- import { assertRoleRuntimeMutationAllowed } from "./roleRuntimeGuard.js";
10
+ import { assertLiveRoleSessionAcknowledged, assertRoleRuntimeMutationAllowed, LIVE_SESSION_ACKNOWLEDGEMENT_OPTION } from "./roleRuntimeGuard.js";
11
11
  export function runGlobalRoleCommand(args, store, options = {}) {
12
12
  const [command, ...rest] = args;
13
13
  switch (command) {
@@ -175,7 +175,7 @@ function updateRole(args, store, options) {
175
175
  if (parsed.has("--workspace") && trimmed(parsed.one("--workspace")) === undefined) {
176
176
  throw usageError("--workspace is required.");
177
177
  }
178
- if ([...parsed.seen].every((option) => option === "--agent")) {
178
+ if (hasNoRoleMutation(parsed)) {
179
179
  throw usageError("At least one role update option is required.");
180
180
  }
181
181
  const workspace = trimmed(parsed.one("--workspace"));
@@ -188,6 +188,14 @@ function updateRole(args, store, options) {
188
188
  scope: "global",
189
189
  roleName: role.name
190
190
  }, "desired launch configuration update");
191
+ assertLiveRoleSessionAcknowledged({
192
+ sessions: tx.getGlobalRoleSessionSet(role.name),
193
+ roleName: role.name,
194
+ desiredRevision: role.launchRevision,
195
+ acknowledged: parsed.has(LIVE_SESSION_ACKNOWLEDGEMENT_OPTION),
196
+ stopCommand: "yui session stop --all",
197
+ endsSession: workspace !== undefined && workspace !== role.workspace
198
+ });
191
199
  }
192
200
  let bindings = role.agentBindings;
193
201
  if (changesAgentConfig) {
@@ -42,6 +42,12 @@ const AGENT_CONFIG_OPTIONS = new Set([
42
42
  ...AGENT_REPEATABLE_OPTIONS.map(([option]) => option),
43
43
  ...AGENT_CLEAR_OPTIONS.map(([option]) => option)
44
44
  ]);
45
+ /** Options that select a target or acknowledge a fact instead of changing the Role. */
46
+ const NON_MUTATING_OPTIONS = ["--agent", "--yes"];
47
+ /** Whether the parsed options change nothing about the Role itself. */
48
+ export function hasNoRoleMutation(parsed) {
49
+ return [...parsed.seen].every((option) => NON_MUTATING_OPTIONS.includes(option));
50
+ }
45
51
  export function roleOptionSpecs(input) {
46
52
  const agentValueOptions = input.agentOptions === "execution"
47
53
  ? AGENT_VALUE_OPTIONS.filter(([option]) => option === "--model" || option === "--effort")
@@ -57,6 +63,7 @@ export function roleOptionSpecs(input) {
57
63
  return new Map([
58
64
  ...(input.includeAgent === true ? [["--agent", "value"]] : []),
59
65
  ...(input.includeWorkspace === true ? [["--workspace", "value"]] : []),
66
+ ...(input.update ? [["--yes", "flag"]] : []),
60
67
  ...PROFILE_OPTIONS,
61
68
  ...agentValueOptions,
62
69
  ...agentRepeatableOptions,
@@ -1,4 +1,5 @@
1
1
  import { usageError } from "../errors/cliError.js";
2
+ import { activeLiveRoleAgentSession } from "../executor/agentExecutor.js";
2
3
  import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
3
4
  /**
4
5
  * Runtime launch reservation and cleanup are ownership obligations for a Role
@@ -10,3 +11,32 @@ export function assertRoleRuntimeMutationAllowed(store, owner, action) {
10
11
  return;
11
12
  throw usageError(`Role ${action} is blocked while a runtime lifecycle transition is pending or processing.`);
12
13
  }
14
+ export const LIVE_SESSION_ACKNOWLEDGEMENT_OPTION = "--yes";
15
+ /**
16
+ * A live native Session keeps the launch configuration it started with. Changing
17
+ * the desired configuration is therefore a decision about the next Host
18
+ * activation, and the Agent making the change is the one that knows whether the
19
+ * running Session should keep going or be replaced. Report that fact once, here,
20
+ * instead of refusing the Turn that later resumes the Session.
21
+ */
22
+ export function assertLiveRoleSessionAcknowledged(input) {
23
+ if (input.acknowledged)
24
+ return;
25
+ const session = activeLiveRoleAgentSession(input.sessions);
26
+ if (session === null)
27
+ return;
28
+ throw usageError([
29
+ input.endsSession === true
30
+ ? `Role ${input.roleName} has a live native Session that cannot continue after this `
31
+ + "change; its next Turn will need a new Session."
32
+ : `Role ${input.roleName} has a live native Session, so this change applies to its `
33
+ + "next Host activation instead of the running one.",
34
+ ` Agent: ${session.agentId} (${session.adapterId})`,
35
+ ` Native Session: ${session.nativeSessionId}`,
36
+ ` Host activation: ${session.runtimeGenerationId ?? "not recorded"}`,
37
+ ` Session launched from desired revision r${session.effective.sourceDesiredRevision}; `
38
+ + `current desired revision r${input.desiredRevision}`,
39
+ `Re-run with ${LIVE_SESSION_ACKNOWLEDGEMENT_OPTION} to record the change and keep the Session.`,
40
+ `Run \`${input.stopCommand}\` first to apply it to a fresh Session instead.`
41
+ ].join("\n"));
42
+ }
@@ -50,9 +50,9 @@ import { projectWorkItemExecution } from "../execution/workItemExecutionProjecti
50
50
  import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
51
51
  import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractResolution.js";
52
52
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
53
- import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfilePatch } from "./roleConfiguration.js";
53
+ import { hasAgentConfigOptions, hasNoRoleMutation, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfilePatch } from "./roleConfiguration.js";
54
54
  import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
55
- import { assertRoleRuntimeMutationAllowed } from "./roleRuntimeGuard.js";
55
+ import { assertLiveRoleSessionAcknowledged, assertRoleRuntimeMutationAllowed, LIVE_SESSION_ACKNOWLEDGEMENT_OPTION } from "./roleRuntimeGuard.js";
56
56
  import { runTaskContextCommand } from "./taskContextCommand.js";
57
57
  import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
58
58
  import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
@@ -1643,7 +1643,7 @@ function updateTaskRole(args, store, options) {
1643
1643
  if (parsed.has("--agent") && (parsed.one("--agent")?.trim().length ?? 0) === 0) {
1644
1644
  throw usageError("--agent is required.", usage);
1645
1645
  }
1646
- if ([...parsed.seen].every((option) => option === "--agent")) {
1646
+ if (hasNoRoleMutation(parsed)) {
1647
1647
  throw usageError("At least one role update option is required.", usage);
1648
1648
  }
1649
1649
  const now = clock(options);
@@ -1660,6 +1660,13 @@ function updateTaskRole(args, store, options) {
1660
1660
  taskId: task.id,
1661
1661
  roleName: role.name
1662
1662
  }, "desired launch configuration update");
1663
+ assertLiveRoleSessionAcknowledged({
1664
+ sessions: tx.getTaskRoleSessionSet(task.id, role.name),
1665
+ roleName: role.name,
1666
+ desiredRevision: role.launchRevision,
1667
+ acknowledged: parsed.has(LIVE_SESSION_ACKNOWLEDGEMENT_OPTION),
1668
+ stopCommand: `yui task role session stop ${task.id} ${role.name} --reason "<decision>"`
1669
+ });
1663
1670
  }
1664
1671
  const profileId = parsed.one("--profile");
1665
1672
  const agentProfile = profileId === undefined
@@ -9,7 +9,7 @@ import { buildTaskWakeEnvelope } from "../context/wakeNotification.js";
9
9
  import { createTaskWake, fallbackWakeCursor, latestTaskWake } from "../scheduler/taskWake.js";
10
10
  import { answerInputRequest } from "../input/inputRequest.js";
11
11
  import { activeRoleAgentBinding } from "../role/role.js";
12
- import { effectiveLaunchWithTaskMainWorkspace, effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskSession, resolveEffectiveLaunch, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
12
+ import { effectiveLaunchWithTaskMainWorkspace, roleSessionMayContinue, resolveEffectiveLaunch, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
13
13
  import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
14
14
  import { appendTurnInput, createTurn } from "../turn/turn.js";
15
15
  import { transportAgentResult } from "../domain/agentResultTransport.js";
@@ -2603,7 +2603,7 @@ function recordTaskRuntimeNativeSession(store, input, now) {
2603
2603
  const effective = input.effective === undefined
2604
2604
  ? resolvedEffective
2605
2605
  : validateEffectiveLaunchSnapshot(input.effective);
2606
- if (!effectiveLaunchSnapshotsCompatibleForTaskSession(resolvedEffective, effective)) {
2606
+ if (!roleSessionMayContinue(resolvedEffective, effective)) {
2607
2607
  throw new Error("Reserved native Session effective launch changed before persistence.");
2608
2608
  }
2609
2609
  if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
@@ -2636,7 +2636,7 @@ function recordGlobalRuntimeNativeSession(store, input, now) {
2636
2636
  const effective = input.effective === undefined
2637
2637
  ? resolvedEffective
2638
2638
  : validateEffectiveLaunchSnapshot(input.effective);
2639
- if (!effectiveLaunchSnapshotsCompatible(resolvedEffective, effective)) {
2639
+ if (!roleSessionMayContinue(resolvedEffective, effective)) {
2640
2640
  throw new Error("Reserved global native Session effective launch changed before persistence.");
2641
2641
  }
2642
2642
  if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
@@ -2719,7 +2719,7 @@ function taskSessionEffective(store, taskId, roleName, agentId, existing) {
2719
2719
  throw new Error(`Native Session registration does not match the effective Turn Agent: ${taskId}/${roleName}.`);
2720
2720
  }
2721
2721
  if (existing !== undefined) {
2722
- if (!effectiveLaunchSnapshotsCompatibleForTaskSession(existing.effective, active.effective)) {
2722
+ if (!roleSessionMayContinue(existing.effective, active.effective)) {
2723
2723
  throw new Error(`Native Session effective launch does not match the active Turn: ${taskId}/${roleName}.`);
2724
2724
  }
2725
2725
  return existing.effective;
@@ -1,5 +1,4 @@
1
1
  import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveDeliveryTimeoutSeconds, resolveRuntimeHealth, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
2
- import { createHash } from "node:crypto";
3
2
  import { resolve } from "node:path";
4
3
  import { isDeepStrictEqual } from "node:util";
5
4
  import { controllerSocketPath } from "../core/controllerEndpoint.js";
@@ -7,9 +6,9 @@ import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_M
7
6
  import { hasRuntimeCleanupObligation, runtimeLifecycleSignalKey, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
8
7
  import { agentProcessReadinessProbe, ExecutorRegistry } from "../executor/executorRegistry.js";
9
8
  import { activeLiveRoleAgentSession, roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
10
- import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskSession, effectiveLaunchConfig, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
9
+ import { roleSessionMayContinue, effectiveLaunchConfig, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
11
10
  import { AgentConfigurationCatalogService, validateAgentLaunchConfiguration } from "../executor/agentConfigurationCatalog.js";
12
- import { isTaskOwnedWorkspace, managedWorkspaceIdentity, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
11
+ import { isTaskOwnedWorkspace, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
13
12
  import { FileRoleLaunchPlanner } from "../executor/fileRoleLaunchPlanner.js";
14
13
  import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
15
14
  import { SqliteTaskStore } from "../storage/sqliteStore.js";
@@ -216,7 +215,6 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
216
215
  assertCurrent: (request) => {
217
216
  assertRuntimeLaunchRequestCurrent(store, request);
218
217
  },
219
- launchFingerprint: (request) => (runtimeLaunchFingerprint(store, request)),
220
218
  onCleanupRequired: signalRuntimeCleanup,
221
219
  runtimeIsolation
222
220
  });
@@ -501,7 +499,6 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
501
499
  assertCurrent: (request) => {
502
500
  assertRuntimeLaunchRequestCurrent(store, request);
503
501
  },
504
- launchFingerprint: (request) => (runtimeLaunchFingerprint(store, request)),
505
502
  onCleanupRequired
506
503
  });
507
504
  const lifecycleTails = new Map();
@@ -862,11 +859,15 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
862
859
  || session.nativeSessionId !== request.nativeSessionId) {
863
860
  throw new Error(`Native session changed: ${request.owner.roleName}.`);
864
861
  }
865
- const sessionEffectiveCompatible = request.owner.scope === "task"
866
- ? effectiveLaunchSnapshotsCompatibleForTaskSession(session.effective, request.effective)
867
- : effectiveLaunchSnapshotsCompatible(session.effective, request.effective);
868
- if (!sessionEffectiveCompatible) {
869
- throw new Error(`Native session effective launch changed: ${request.owner.roleName}.`);
862
+ // The Role's durable Session record is the only authority for which Host
863
+ // activation may be restored. Targeting anything else would revive a
864
+ // historical activation.
865
+ if (request.hostActivationId !== undefined
866
+ && session.runtimeGenerationId !== request.hostActivationId) {
867
+ throw new Error(`Session restore does not target the Role's current Host activation: ${request.owner.roleName}.`);
868
+ }
869
+ if (!roleSessionMayContinue(session.effective, request.effective)) {
870
+ throw new Error(`Native session cannot continue under this launch: ${request.owner.roleName}.`);
870
871
  }
871
872
  }
872
873
  }
@@ -888,21 +889,6 @@ function currentDesiredEffective(store, request, role) {
888
889
  ...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
889
890
  });
890
891
  }
891
- function runtimeLaunchFingerprint(store, request) {
892
- const agent = store.getConfiguredAgent(request.effective.agentId);
893
- if (agent === null) {
894
- throw new Error(`Agent no longer exists: ${request.effective.agentId}.`);
895
- }
896
- return createHash("sha256").update(JSON.stringify([
897
- request.owner,
898
- request.effective,
899
- request.managedWorkspace === undefined
900
- ? undefined
901
- : managedWorkspaceIdentity(request.managedWorkspace),
902
- request.runtimePolicy,
903
- agent
904
- ])).digest("hex");
905
- }
906
892
  function currentDesiredManagedWorkspace(store, taskId, roleName) {
907
893
  const item = store.listWorkItems(taskId).find((candidate) => (candidate.assignee === roleName
908
894
  && !["completed", "failed", "retired"].includes(candidate.status))) ?? null;
@@ -1,7 +1,7 @@
1
- import { createHash, randomUUID } from "node:crypto";
1
+ import { randomUUID } from "node:crypto";
2
2
  import { runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
3
3
  import { createRuntimeBinding, RuntimeGenerationMismatchError, RuntimeHostContentionError, RuntimeLaunchError } from "../runtime/index.js";
4
- import { effectiveLaunchSnapshotsCompatible, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
4
+ import { sameEffectiveLaunch, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
5
5
  class RuntimeBindingContractError extends Error {
6
6
  constructor(message, options) {
7
7
  super(message, options);
@@ -25,7 +25,6 @@ export class RuntimeLaunchCoordinator {
25
25
  #createGenerationId;
26
26
  #now;
27
27
  #assertCurrent;
28
- #launchFingerprint;
29
28
  #onCleanupRequired;
30
29
  #runtimeIsolation;
31
30
  constructor(reservations, host, options = {}) {
@@ -34,8 +33,6 @@ export class RuntimeLaunchCoordinator {
34
33
  this.#createGenerationId = options.createGenerationId ?? randomUUID;
35
34
  this.#now = options.now ?? (() => new Date());
36
35
  this.#assertCurrent = options.assertCurrent;
37
- this.#launchFingerprint = options.launchFingerprint
38
- ?? defaultLaunchFingerprint;
39
36
  this.#onCleanupRequired = options.onCleanupRequired;
40
37
  this.#runtimeIsolation = options.runtimeIsolation;
41
38
  }
@@ -79,22 +76,19 @@ export class RuntimeLaunchCoordinator {
79
76
  if (request.owner.scope === "global" && request.managedWorkspace !== undefined) {
80
77
  throw new Error("A global runtime cannot use a Task ManagedWorkspace.");
81
78
  }
82
- const expectedFingerprint = requireText(this.#launchFingerprint(request), "Launch fingerprint");
83
79
  const assertLaunchCurrent = () => {
84
80
  this.#assertCurrent?.(request);
85
81
  assertCurrent?.();
86
- if (this.#launchFingerprint(request) !== expectedFingerprint) {
87
- throw new Error(`Role or Agent launch state changed: ${request.owner.roleName}.`);
88
- }
89
82
  };
90
- const generationPrefix = `runtime-${expectedFingerprint}:generation:`;
91
83
  const proposedGenerationId = requireText(this.#createGenerationId(), "Launch generation id");
92
- let proposedRuntimeGenerationId = `${generationPrefix}${proposedGenerationId}`;
84
+ // A Host activation id is an opaque durable identity, not a digest of the
85
+ // launch configuration. Whether a live activation may be reused is answered
86
+ // by the Role's durable Session record through `assertCurrent`, so launch
87
+ // configuration that only shapes the next activation never invalidates the
88
+ // current one.
89
+ let proposedRuntimeGenerationId = `runtime-${proposedGenerationId}`;
93
90
  let reusedConfirmedRunningHost = false;
94
91
  if (request.mode === "resume" && request.hostActivationId !== undefined) {
95
- if (!request.hostActivationId.startsWith(generationPrefix)) {
96
- throw new Error("Session restore targets an incompatible Host activation.");
97
- }
98
92
  const inspection = await this.host.inspectOwner(request.owner);
99
93
  if (inspection.state === "unavailable" || inspection.state === "starting") {
100
94
  throw new RuntimeLaunchError(true, request.hostActivationId, `Host activation is temporarily ${inspection.state}: ${request.owner.roleName}.`);
@@ -112,10 +106,6 @@ export class RuntimeLaunchCoordinator {
112
106
  runtimeGenerationId: proposedRuntimeGenerationId
113
107
  }, assertLaunchCurrent, this.#now());
114
108
  if (reservation.status === "existing") {
115
- if (!reservation.runtimeGenerationId.startsWith(generationPrefix)) {
116
- this.#requireCleanup(request.owner);
117
- throw new Error(`Runtime launch reservation belongs to stale Role or Agent state: ${request.owner.roleName}.`);
118
- }
119
109
  const inspection = await this.host.inspectOwner(request.owner);
120
110
  if (inspection.state === "unavailable" || inspection.state === "starting") {
121
111
  throw new RuntimeLaunchError(true, reservation.runtimeGenerationId, `Runtime is temporarily ${inspection.state}: ${request.owner.roleName}/${reservation.runtimeGenerationId}.`);
@@ -416,7 +406,7 @@ function validateRuntimeLaunchPreflight(preflight, request, runtimeGenerationId)
416
406
  || preflight.turnId !== request.turnId
417
407
  || preflight.agentId !== request.agentId
418
408
  || preflight.adapterId !== request.adapterId
419
- || !effectiveLaunchSnapshotsCompatible(preflight.effective, request.effective)
409
+ || !sameEffectiveLaunch(preflight.effective, request.effective)
420
410
  || (request.mode === "resume"
421
411
  && preflight.nativeSessionId !== request.nativeSessionId)) {
422
412
  throw new Error(`Session host pre-start launch fence does not match the requested runtime: ${request.owner.roleName}.`);
@@ -445,17 +435,6 @@ function requireMatchingRuntimeBinding(raw, request, runtimeGenerationId) {
445
435
  }
446
436
  return binding;
447
437
  }
448
- function defaultLaunchFingerprint(request) {
449
- return createHash("sha256").update(JSON.stringify([
450
- request.owner,
451
- request.agentId,
452
- request.adapterId,
453
- request.effective,
454
- request.workspace,
455
- request.managedWorkspace,
456
- request.runtimePolicy
457
- ])).digest("hex");
458
- }
459
438
  function requireText(value, label) {
460
439
  if (typeof value !== "string"
461
440
  || value.length === 0
@@ -7,6 +7,11 @@ import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
7
7
  /** Hidden CLI entrypoint used by Codex's structured notify hook. */
8
8
  export async function runSessionNotifyCommand(payloadArgument, environment = process.env, call, setThreadName = setCodexThreadName) {
9
9
  const params = parseCodexSessionNotification(payloadArgument, environment);
10
+ // Old global TUIs can still emit their invocation-local hook. It is not
11
+ // evidence of a shared-daemon Thread's identity or process lifecycle.
12
+ // Global identity is committed from App Server at successful host start.
13
+ if (params.scope === "global")
14
+ return;
10
15
  const home = requireText(environment.YUI_HOME, "YUI_HOME");
11
16
  // A Codex process outlives its Turn, so the notify envelope cannot say which
12
17
  // Turn or runtime generation is current. Durable Session state answers both;
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { hasRecentTurnId, rememberRecentTurnId, validateRecentTurnIds } from "../runtime/recentTurnIds.js";
3
- import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskSession, validateEffectiveLaunchSnapshot } from "./effectiveLaunch.js";
3
+ import { roleSessionMayContinue, validateEffectiveLaunchSnapshot } from "./effectiveLaunch.js";
4
4
  import { currentProviderActivation, endProviderActivation, settleProviderTurn, settleProviderTurnSubmission, validateProviderRuntimeBinding } from "../runtime/providerRuntimeIdentity.js";
5
5
  import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
6
6
  export function createRoleSessionSet(owner, activeAgentId, now) {
@@ -66,10 +66,9 @@ export function recordRoleAgentSession(set, input, now) {
66
66
  throw new Error(`Role Agent session effective identity is inconsistent: ${agentId}.`);
67
67
  }
68
68
  if (existing !== undefined && existing.nativeSessionId === nativeSessionId
69
- && !effectiveLaunchSnapshotsCompatible(existing.effective, effective)
70
- && !(set.owner.scope === "task"
71
- && effectiveLaunchSnapshotsCompatibleForTaskSession(existing.effective, effective))) {
72
- throw new Error(`Role Agent session effective launch cannot change: ${agentId}.`);
69
+ && !roleSessionMayContinue(existing.effective, effective)) {
70
+ throw new Error(`Role Agent session cannot continue under this launch: ${agentId}. `
71
+ + "Its Agent, adapter or physical workspace changed.");
73
72
  }
74
73
  if (existing !== undefined && existing.nativeSessionId !== nativeSessionId
75
74
  && existing.status === "active") {
@@ -280,13 +279,11 @@ export function roleAgentSessionResumeMode(set, agentId, desired) {
280
279
  if (session.status === "ended") {
281
280
  return "new";
282
281
  }
283
- const compatible = set.owner.scope === "task"
284
- ? effectiveLaunchSnapshotsCompatibleForTaskSession(session.effective, desired)
285
- : effectiveLaunchSnapshotsCompatible(session.effective, desired);
286
- if (compatible)
282
+ if (roleSessionMayContinue(session.effective, desired))
287
283
  return "resume";
288
- throw new Error(`Role Agent session is incompatible with the next effective launch: ${agentId}. `
289
- + "Stop the existing native process before starting a fresh Session.");
284
+ throw new Error(`Role Agent session cannot continue under the next launch: ${agentId}. `
285
+ + "Its Agent, adapter or physical workspace changed; stop the existing native "
286
+ + "process before starting a fresh Session.");
290
287
  }
291
288
  export function bindTaskRoleProviderRuntime(set, binding, updatedAt) {
292
289
  validateRoleSessionSet(set);