@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 +33 -9
- package/dist/cli/commandCatalog.js +5 -5
- package/dist/cli.js +2 -2
- package/dist/commands/agentCommands.js +13 -6
- package/dist/commands/globalRoleCommands.js +11 -3
- package/dist/commands/roleConfiguration.js +7 -0
- package/dist/commands/roleRuntimeGuard.js +30 -0
- package/dist/commands/taskCommands.js +10 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +4 -4
- package/dist/controller/runtime.js +11 -25
- package/dist/controller/runtimeLaunchCoordinator.js +9 -30
- package/dist/controller/sessionNotify.js +5 -0
- package/dist/executor/agentExecutor.js +8 -11
- package/dist/executor/effectiveLaunch.js +34 -17
- package/dist/executor/fileRoleLaunchPlanner.js +11 -8
- package/dist/runtime/agentHost.js +7 -0
- package/dist/runtime/codexInteractiveHost.js +191 -0
- package/dist/runtime/structuredProviderHost.js +35 -0
- package/dist/runtime/tmuxAdapters.js +51 -9
- package/dist/scheduler/activeRoleTurnDelivery.js +4 -4
- package/dist/scheduler/leaderWakeupProcessor.js +3 -4
- package/dist/tmux/tmuxManager.js +43 -28
- package/i18n/README.zh-CN.md +7 -0
- 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.
|
|
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
|
-
|
|
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
|
|
940
|
-
only when
|
|
941
|
-
|
|
942
|
-
|
|
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
|
|
953
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
+ "
|
|
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(
|
|
118
|
-
+ `
|
|
119
|
-
+ "
|
|
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"
|
|
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 (
|
|
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 (
|
|
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,
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 {
|
|
9
|
+
import { roleSessionMayContinue, effectiveLaunchConfig, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
11
10
|
import { AgentConfigurationCatalogService, validateAgentLaunchConfiguration } from "../executor/agentConfigurationCatalog.js";
|
|
12
|
-
import { isTaskOwnedWorkspace,
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
if (
|
|
869
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|| !
|
|
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 {
|
|
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
|
-
&& !
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
289
|
-
+ "
|
|
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);
|