@zq-silk/yui 0.15.0 → 0.15.2

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 (41) hide show
  1. package/README.md +33 -9
  2. package/dist/cli/commandCatalog.js +5 -5
  3. package/dist/cli.js +5 -3
  4. package/dist/commands/agentCommands.js +13 -6
  5. package/dist/commands/globalRoleCommands.js +11 -3
  6. package/dist/commands/roleConfiguration.js +7 -0
  7. package/dist/commands/roleRuntimeGuard.js +30 -0
  8. package/dist/commands/taskCommands.js +10 -3
  9. package/dist/context/sessionBootstrapManifest.js +24 -9
  10. package/dist/controller/fileSchedulerStoreAdapter.js +4 -4
  11. package/dist/controller/jobClient.js +1 -0
  12. package/dist/controller/jobControl.js +137 -10
  13. package/dist/controller/jobSupervisor.js +89 -75
  14. package/dist/controller/runtime.js +19 -28
  15. package/dist/controller/runtimeLaunchCoordinator.js +9 -30
  16. package/dist/controller/sessionNotify.js +5 -0
  17. package/dist/core/boundedRpc.js +3 -1
  18. package/dist/executor/agentExecutor.js +8 -11
  19. package/dist/executor/effectiveLaunch.js +34 -17
  20. package/dist/executor/fileRoleLaunchPlanner.js +11 -8
  21. package/dist/job/durableJob.js +68 -6
  22. package/dist/kernel/callAuthority.js +24 -0
  23. package/dist/kernel/instanceHost.js +97 -0
  24. package/dist/kernel/kernelPorts.js +44 -0
  25. package/dist/kernel/operationFacts.js +32 -0
  26. package/dist/runtime/agentHost.js +7 -0
  27. package/dist/runtime/codexInteractiveHost.js +191 -0
  28. package/dist/runtime/exactControlPlane.js +8 -10
  29. package/dist/runtime/structuredProviderHost.js +35 -0
  30. package/dist/runtime/tmuxAdapters.js +51 -9
  31. package/dist/scheduler/activeRoleTurnDelivery.js +4 -4
  32. package/dist/scheduler/leaderWakeupProcessor.js +3 -4
  33. package/dist/storage/sqliteSchema.js +28 -0
  34. package/dist/storage/sqliteStore.js +11 -3
  35. package/dist/storage/storageVersions.js +1 -1
  36. package/dist/storage/storeRpc.js +1 -1
  37. package/dist/storage/taskStore.js +6 -1
  38. package/dist/storage/upgrade/upgradeOrchestrator.js +3 -0
  39. package/dist/tmux/tmuxManager.js +43 -28
  40. package/i18n/README.zh-CN.md +7 -0
  41. package/package.json +1 -1
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
@@ -1614,7 +1614,9 @@ async function preflightManagedTaskControlPlane() {
1614
1614
  }
1615
1615
  async function preflightManagedGlobalControlPlane(digest) {
1616
1616
  if (process.env.YUI_SESSION_SCOPE !== "global") {
1617
- throw new Error("Exact Task control-plane invocation requires its frozen runtime descriptors.");
1617
+ throw new Error("A control-plane digest was supplied outside a managed Session runtime. "
1618
+ + "Run the command through the Role's own Session entry point, or use the "
1619
+ + "ordinary yui command.");
1618
1620
  }
1619
1621
  if (taskFinalReviewInvocation.request !== undefined) {
1620
1622
  throw new Error("Task final-review contract establishment requires a verified exact Task control-plane invocation.");
@@ -2092,10 +2094,10 @@ async function executeOperatorSessionControl(control, home, store, runtime, tmux
2092
2094
  throw usageError("Operator is not configured. Run yui setup first.");
2093
2095
  const sessionSet = store.getGlobalRoleSessionSet(role.name);
2094
2096
  const active = sessionSet?.sessions[sessionSet.activeAgentId];
2095
- const paneRunning = tmux.detectRoleStatus("operator", "operator") === "running";
2097
+ const paneRunning = tmux.probeRoleStatus("operator", "operator") === "running";
2096
2098
  if (paneRunning && active === undefined) {
2097
2099
  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.");
2100
+ + "Record the exact native session before switching sessions.");
2099
2101
  }
2100
2102
  if (control.action === "resume"
2101
2103
  && 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
@@ -1,15 +1,23 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { chmodSync, existsSync, readFileSync, readdirSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
- import { exactControlPlaneCommandPrefix, exactControlPlaneDigest, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
4
+ import { exactControlPlaneDigest, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
5
5
  import { writeTextFileAtomically } from "../storage/durableFile.js";
6
6
  import { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
7
7
  export { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
8
8
  const ordinarySessionCli = ["#!/bin/sh", "exec yui \"$@\"", ""].join("\n");
9
+ /**
10
+ * A managed Session wrapper answers exactly one question: which installation
11
+ * runs this command. It therefore carries only the resolved entry point, never
12
+ * a package or build identity. Version identity changes on every release while
13
+ * a Session legitimately outlives it, so embedding it here would turn an
14
+ * ordinary update into a broken Session.
15
+ */
9
16
  function renderSessionCli(controlPlane) {
10
17
  return [
11
18
  "#!/bin/sh",
12
- `exec ${exactControlPlaneCommandPrefix(controlPlane)} \"$@\"`,
19
+ `exec ${quoteShellWord(controlPlane.executable)} `
20
+ + `${quoteShellWord(controlPlane.cliEntry)} \"$@\"`,
13
21
  ""
14
22
  ].join("\n");
15
23
  }
@@ -88,9 +96,10 @@ export function materializeSessionBootstrap(input) {
88
96
  const descriptorPath = resolve(join(home, "runtime", "control-plane", `${controlDigest}.json`));
89
97
  writeImmutableText(descriptorPath, `${serializeExactDescriptor(input.controlPlane)}\n`);
90
98
  // Provider command runners may rebuild PATH independently of the managed
91
- // process environment. Keep the Session entry point deterministic; the
92
- // existing continuity preflight accepts compatible package replacement at
93
- // this path without treating package version as Session identity.
99
+ // process environment, so a bare `yui` could resolve to another install or
100
+ // Home. The wrapper pins the resolved entry point instead. Package identity
101
+ // stays out of it: the compatible continuity preflight and the Session's
102
+ // caller key already authorize the command.
94
103
  const sessionCliContent = renderSessionCli(input.controlPlane);
95
104
  const sessionCliDigest = digest(sessionCliContent);
96
105
  const sessionCliPath = resolve(join(home, "runtime", "session-cli", `yui-${sessionCliDigest}.sh`));
@@ -168,7 +177,9 @@ function quoteShellWord(value) {
168
177
  /**
169
178
  * Retargets known managed wrappers to the current resolved control plane after
170
179
  * a compatible update. Only a valid Session Manifest may nominate a wrapper,
171
- * and only Yui's ordinary or exact two-line wrapper shapes are changed.
180
+ * and only Yui's own two-line wrapper shapes are changed. A wrapper written by
181
+ * an earlier release may still pin a control-plane digest; retargeting it here
182
+ * is what removes that stale pin from a live Home.
172
183
  */
173
184
  export function refreshManagedSessionCliWrappers(homeInput, controlPlane) {
174
185
  const home = resolve(homeInput);
@@ -213,7 +224,7 @@ export function refreshManagedSessionCliWrappers(homeInput, controlPlane) {
213
224
  current += 1;
214
225
  continue;
215
226
  }
216
- if (content !== ordinarySessionCli && !isExactSessionCli(content)) {
227
+ if (content !== ordinarySessionCli && !isManagedSessionCli(content)) {
217
228
  skipped += 1;
218
229
  continue;
219
230
  }
@@ -223,8 +234,12 @@ export function refreshManagedSessionCliWrappers(homeInput, controlPlane) {
223
234
  }
224
235
  return Object.freeze({ refreshed, current, skipped });
225
236
  }
226
- function isExactSessionCli(content) {
227
- return /^#!\/bin\/sh\nexec [^\n]+ '--yui-control' '[a-f0-9]{64}' "\$@"\n$/u.test(content);
237
+ /**
238
+ * Both wrapper shapes Yui has written: the current entry-point-only form and
239
+ * the earlier form that also pinned a control-plane digest.
240
+ */
241
+ function isManagedSessionCli(content) {
242
+ return /^#!\/bin\/sh\nexec [^\n]+(?: '--yui-control' '[a-f0-9]{64}')? "\$@"\n$/u.test(content);
228
243
  }
229
244
  function writeImmutableText(path, content) {
230
245
  writeTextFileAtomically(path, content);
@@ -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;
@@ -77,6 +77,7 @@ export function createControllerIntegrationJobPort(home, clientOptions = {}) {
77
77
  const caller = resolveJobCaller(clientOptions.environment, input.taskId);
78
78
  const { job } = await startDurableJob(home, {
79
79
  taskId: input.taskId,
80
+ requestId: `integration:${input.integrationId}`,
80
81
  owner,
81
82
  projectId: input.projectId,
82
83
  head: input.head,