@zq-silk/yui 2.19.0 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -136,6 +136,7 @@ const agentChildren = [
136
136
  { name: "remove", discovery: { surface: "public", audiences: ["public", "operator"] }, summary: "Remove a configured Agent.", usage: "yui config agent remove <id>" }
137
137
  ];
138
138
  const roleProfileOptions = [
139
+ "--instance-code",
139
140
  "--description", "--responsibility", "--constraint",
140
141
  "--expected-output", "--system-prompt", "--skill"
141
142
  ];
@@ -144,6 +145,7 @@ const roleAgentOptions = [
144
145
  "--allowed-tool", "--disallowed-tool", "--search"
145
146
  ];
146
147
  const roleProfileClearOptions = [
148
+ "--clear-instance-code",
147
149
  "--clear-description", "--clear-responsibilities", "--clear-constraints",
148
150
  "--clear-expected-output", "--clear-system-prompt", "--clear-skills"
149
151
  ];
@@ -1,4 +1,5 @@
1
1
  import { usageError } from "../errors/cliError.js";
2
+ import { validateInstanceCode } from "../role/instanceCode.js";
2
3
  import { parseEnabled } from "../role/availability.js";
3
4
  import { defaultRoleAgentConfig, resolveAgentAdapter } from "../executor/agentAdapter.js";
4
5
  const PROFILE_OPTIONS = [
@@ -66,6 +67,8 @@ export function roleOptionSpecs(input) {
66
67
  ...(input.includeWorkspace === true ? [["--workspace", "value"]] : []),
67
68
  ...(input.update ? [["--yes", "flag"]] : []),
68
69
  ...(input.update ? [["--enabled", "value"]] : []),
70
+ ["--instance-code", "value"],
71
+ ...(input.update ? [["--clear-instance-code", "flag"]] : []),
69
72
  ...PROFILE_OPTIONS,
70
73
  ...agentValueOptions,
71
74
  ...agentRepeatableOptions,
@@ -105,6 +108,7 @@ export function parseRoleOptions(args, specs, usage) {
105
108
  }
106
109
  export function roleProfileFrom(parsed) {
107
110
  return {
111
+ ...roleDisplayPatch(parsed),
108
112
  ...(parsed.has("--description") ? { description: requiredText(parsed.one("--description")) } : {}),
109
113
  responsibilities: parsed.many("--responsibility").map(requiredText),
110
114
  constraints: parsed.many("--constraint").map(requiredText),
@@ -125,6 +129,7 @@ export function roleProfilePatch(parsed) {
125
129
  ["--skill", "--clear-skills"]
126
130
  ]);
127
131
  return {
132
+ ...roleDisplayPatch(parsed),
128
133
  ...(parsed.has("--enabled") ? { enabled: parseEnabled(parsed.one("--enabled")) } : {}),
129
134
  ...(parsed.has("--description") ? { description: requiredText(parsed.one("--description")) } : {}),
130
135
  ...(parsed.has("--clear-description") ? { description: undefined } : {}),
@@ -144,6 +149,18 @@ export function roleProfilePatch(parsed) {
144
149
  ...(parsed.has("--clear-skills") ? { skills: [] } : {})
145
150
  };
146
151
  }
152
+ function roleDisplayPatch(parsed) {
153
+ assertPairs(parsed, [["--instance-code", "--clear-instance-code"]]);
154
+ try {
155
+ return {
156
+ ...(parsed.has("--instance-code") ? { instanceCode: validateInstanceCode(parsed.one("--instance-code")) } : {}),
157
+ ...(parsed.has("--clear-instance-code") ? { instanceCode: undefined } : {})
158
+ };
159
+ }
160
+ catch (error) {
161
+ throw usageError(error instanceof Error ? error.message : String(error));
162
+ }
163
+ }
147
164
  export function hasAgentConfigOptions(parsed) {
148
165
  return [...parsed.seen].some((option) => AGENT_CONFIG_OPTIONS.has(option));
149
166
  }
@@ -2114,8 +2114,14 @@ export class FileSchedulerStoreAdapter {
2114
2114
  && existing.nativeSessionId !== input.session.nativeSessionId
2115
2115
  && existing.status === "active";
2116
2116
  if (!defersConversationReplacement && (existing?.nativeSessionId !== input.session.nativeSessionId || existing.status !== "active")) {
2117
+ // Preparation reactivates the fixed native identity, not its name.
2118
+ // Preserve the stored title even for an ended Session; only the
2119
+ // naming receipt may advance it on resume.
2120
+ const { title, ...preparedSession } = input.session;
2117
2121
  saveTaskSession(store, role, {
2118
- ...input.session,
2122
+ ...preparedSession,
2123
+ ...(existing?.nativeSessionId !== input.session.nativeSessionId && title !== undefined
2124
+ ? { title } : {}),
2119
2125
  nativeSessionId: input.session.nativeSessionId
2120
2126
  }, "active", input.now);
2121
2127
  }
@@ -2180,7 +2186,7 @@ export class FileSchedulerStoreAdapter {
2180
2186
  recordLaunchedRuntimeNativeSession(input, assertCurrent, now = new Date()) {
2181
2187
  return this.store.transaction((store) => {
2182
2188
  assertCurrent();
2183
- const session = input.owner.scope === "task"
2189
+ let session = input.owner.scope === "task"
2184
2190
  ? recordTaskRuntimeNativeSession(store, {
2185
2191
  taskId: input.owner.taskId,
2186
2192
  roleName: input.owner.roleName,
@@ -2196,6 +2202,20 @@ export class FileSchedulerStoreAdapter {
2196
2202
  nativeSessionId: input.nativeSessionId,
2197
2203
  effective: input.effective
2198
2204
  }, now);
2205
+ if (input.generatedTitle !== undefined && (session.title === undefined
2206
+ || session.title === session.generatedTitle || session.title === input.generatedTitle)) {
2207
+ const set = input.owner.scope === "task"
2208
+ ? store.getTaskRoleSessionSet(input.owner.taskId, input.owner.roleName)
2209
+ : store.getGlobalRoleSessionSet(input.owner.roleName);
2210
+ const titled = recordRoleAgentSession(set, {
2211
+ ...session, title: input.generatedTitle, generatedTitle: input.generatedTitle
2212
+ }, now);
2213
+ if (titled.owner.scope === "task")
2214
+ store.saveTaskRoleSessionSet(titled);
2215
+ else
2216
+ store.saveGlobalRoleSessionSet(titled);
2217
+ session = titled.sessions[input.agentId];
2218
+ }
2199
2219
  if (input.owner.scope === "global") {
2200
2220
  let sessions = store.getGlobalRoleSessionSet(input.owner.roleName);
2201
2221
  let binding = sessions.providerBinding;
@@ -69,6 +69,7 @@ export class RuntimeLaunchCoordinator {
69
69
  }
70
70
  }
71
71
  let preflightObserved = beforeHostStart === undefined;
72
+ let generatedTitle;
72
73
  const observePreflight = (preflight) => {
73
74
  if (preflightObserved || !sameOwner(preflight.owner, request.owner)
74
75
  || preflight.runId !== request.runId
@@ -80,6 +81,7 @@ export class RuntimeLaunchCoordinator {
80
81
  }
81
82
  assertLaunchCurrent();
82
83
  preflightObserved = true;
84
+ generatedTitle = preflight.generatedTitle;
83
85
  beforeHostStart?.(preflight);
84
86
  };
85
87
  const shared = {
@@ -94,7 +96,9 @@ export class RuntimeLaunchCoordinator {
94
96
  };
95
97
  // Even without a startup-facts consumer, recheck after asynchronous Host
96
98
  // validation and immediately before creating the process.
97
- const callback = beforeHostStart === undefined ? () => assertLaunchCurrent() : observePreflight;
99
+ const callback = beforeHostStart === undefined
100
+ ? (preflight) => { assertLaunchCurrent(); generatedTitle = preflight.generatedTitle; }
101
+ : observePreflight;
98
102
  if (request.mode === "resume" && !request.nativeSessionId) {
99
103
  throw new Error("Restoring a Session requires its native session id.");
100
104
  }
@@ -109,11 +113,18 @@ export class RuntimeLaunchCoordinator {
109
113
  throw new Error("Session host returned facts inconsistent with the requested launch.");
110
114
  }
111
115
  if (binding.nativeSessionId !== undefined) {
116
+ // A live Host may reuse its Provider without applying the new plan.
117
+ // Codex reports the actual naming effect; other adapters can only name
118
+ // at creation and must not overwrite native user names on resume.
119
+ const recordedTitle = request.adapterId === "codex"
120
+ ? binding.appliedGeneratedTitle
121
+ : request.mode === "new" ? generatedTitle : undefined;
112
122
  this.sessions.recordLaunchedRuntimeNativeSession({
113
123
  owner: request.owner,
114
124
  agentId: request.agentId,
115
125
  adapterId: request.adapterId,
116
126
  nativeSessionId: binding.nativeSessionId,
127
+ ...(recordedTitle === undefined ? {} : { generatedTitle: recordedTitle }),
117
128
  effective
118
129
  }, assertLaunchCurrent, this.options.now?.() ?? new Date());
119
130
  }
@@ -111,6 +111,8 @@ export function recordRoleAgentSession(set, input, now) {
111
111
  adapterId,
112
112
  nativeSessionId,
113
113
  ...optionalSessionText("title", input.title ?? continuing?.title),
114
+ ...((input.generatedTitle ?? continuing?.generatedTitle) === undefined ? {}
115
+ : { generatedTitle: input.generatedTitle ?? continuing?.generatedTitle }),
114
116
  ...optionalSessionText("preview", input.preview ?? continuing?.preview),
115
117
  policy: input.policy,
116
118
  effective: continuing?.effective ?? effective,
@@ -567,6 +569,11 @@ export function validateRoleAgentSession(session, expectedAgentId = session.agen
567
569
  throw new Error(`Role Agent session effective identity is inconsistent: ${agentId}.`);
568
570
  }
569
571
  requireText(session.nativeSessionId, "Native session id");
572
+ if (session.generatedTitle !== undefined
573
+ && (typeof session.generatedTitle !== "string" || session.generatedTitle.length > 80
574
+ || optionalSessionText("title", session.generatedTitle).title !== session.generatedTitle)) {
575
+ throw new Error("Role Agent generated title is invalid.");
576
+ }
570
577
  if (session.title !== undefined
571
578
  && optionalSessionText("title", session.title).title !== session.title) {
572
579
  throw new Error("Role Agent session title is invalid.");
@@ -17,7 +17,7 @@ import { managedRuntimeAdmission } from "../runtime/agentDriver.js";
17
17
  import { builtinAgentEndpointImplementation, validateAgentEndpointImplementation } from "../runtime/agentEndpointIdentity.js";
18
18
  import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
19
19
  import { assertProviderConversationReplaceable } from "../runtime/providerRuntimeIdentity.js";
20
- import { operatorSessionTitle, resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
20
+ import { globalRoleSessionTitle, resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
21
21
  import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
22
22
  import { planningRuntimeCwd } from "../storage/homeLayout.js";
23
23
  import { resolveAgentAdapter } from "./agentAdapter.js";
@@ -220,7 +220,11 @@ export class FileRoleLaunchPlanner {
220
220
  // attempt was rejected, cancelled, or completed.
221
221
  assertProviderConversationReplaceable(sessionSet.providerBinding);
222
222
  }
223
- return this.#compile(role, input, { scope: "task", taskId: task.id }, resolveTaskRoleSessionTitle(input.mode === "resume" ? existing?.title : undefined, task, role.name), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, {
223
+ return this.#compile(role, input, { scope: "task", taskId: task.id }, resolveTaskRoleSessionTitle(input.mode === "resume" ? existing?.title : undefined, task, role.name, {
224
+ executionAuthority: (input.mode === "resume" ? existing?.effective : effective)?.executionAuthority,
225
+ instanceCode: role.instanceCode,
226
+ generatedTitle: existing?.generatedTitle
227
+ }), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, {
224
228
  purpose
225
229
  });
226
230
  }
@@ -249,9 +253,12 @@ export class FileRoleLaunchPlanner {
249
253
  if (input.mode === "new" && sessionSet?.providerBinding != null) {
250
254
  assertProviderConversationReplaceable(sessionSet.providerBinding);
251
255
  }
252
- return this.#compile(role, input, { scope: "global" }, input.mode === "new" && role.name === "operator"
253
- ? operatorSessionTitle(this.#now(), this.store.getConfig().timeZone)
254
- : undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, { purpose: "execution" });
256
+ return this.#compile(role, input, { scope: "global" }, input.mode === "new"
257
+ ? globalRoleSessionTitle(role.name, this.#now(), this.store.getConfig().timeZone, role.instanceCode)
258
+ : existing?.generatedTitle !== undefined && existing.title === existing.generatedTitle
259
+ ? globalRoleSessionTitle(role.name, new Date(existing.createdAt), this.store.getConfig().timeZone, role.instanceCode)
260
+ ?? `Yui ${role.name}`
261
+ : undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, { purpose: "execution" });
255
262
  }
256
263
  #compile(role, input, owner, sessionTitle, knownNativeSessionId, effective, sessionPolicy) {
257
264
  const launchRole = effectiveRoleForLaunch(role, effective);
@@ -385,7 +392,9 @@ export class FileRoleLaunchPlanner {
385
392
  agent,
386
393
  config: effectiveConfig,
387
394
  workspace: agentWorkspace,
388
- ...(sessionTitle === undefined || postCreateOperatorTitle
395
+ // CLI --name has no ownership check on resume. Only the Codex native
396
+ // metadata path below may rename an existing conversation.
397
+ ...(sessionTitle === undefined || postCreateOperatorTitle || input.mode === "resume"
389
398
  ? {}
390
399
  : { sessionTitle }),
391
400
  ...sessionContext
@@ -510,6 +519,9 @@ export class FileRoleLaunchPlanner {
510
519
  mode: "resume",
511
520
  nativeSessionId: requireText(providerNativeSessionId, "Managed Provider restore native session id"),
512
521
  ...(sessionTitle === undefined ? {} : { sessionTitle }),
522
+ ...(existingSession?.generatedTitle !== undefined
523
+ && existingSession.title === existingSession.generatedTitle
524
+ ? { previousSessionTitle: existingSession.generatedTitle } : {}),
513
525
  ...(managedCompiled.codexThread === undefined
514
526
  ? {}
515
527
  : { codexThread: managedCompiled.codexThread }),
@@ -594,6 +606,10 @@ export class FileRoleLaunchPlanner {
594
606
  },
595
607
  launch: ordinaryConversationLaunch,
596
608
  session,
609
+ ...(sessionTitle !== undefined && (input.mode === "new"
610
+ || existingSession?.title === undefined
611
+ || existingSession.title === existingSession.generatedTitle)
612
+ ? { generatedTitle: sessionTitle } : {}),
597
613
  ...(sessionTitle === undefined ? {} : { sessionTitle })
598
614
  };
599
615
  }
@@ -54,6 +54,7 @@ export function renderRoleDetails(title, role, input) {
54
54
  ...(!isEnabled(role) ? [` Availability ${roleDisabledReason(role)}`] : []),
55
55
  ` Active Agent ${role.activeAgentId}`,
56
56
  ` Workspace ${role.workspace}`,
57
+ ` Instance code ${role.instanceCode ?? "-"} (display only; next Session launch)`,
57
58
  ...("taskId" in role ? [
58
59
  ` Desired environment ${role.executionEnvironment === undefined
59
60
  ? "managed workspace"
@@ -0,0 +1,7 @@
1
+ export function validateInstanceCode(value) {
2
+ if (typeof value !== "string" || value.length === 0 || value.length > 24
3
+ || !/^[\p{L}\p{N}\p{M}_-]+$/u.test(value)) {
4
+ throw new Error("Instance code must be 1–24 UTF-16 units of Unicode letters, numbers, marks, '_' or '-'; omit '@'.");
5
+ }
6
+ return value;
7
+ }
package/dist/role/role.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
+ import { validateInstanceCode } from "./instanceCode.js";
2
3
  import { assertRoleEnabled, validateEnabled } from "./availability.js";
3
4
  import { requireKnownFields } from "../domain/validation.js";
4
5
  import { jobScopeKey } from "../job/jobDefinition.js";
@@ -49,6 +50,7 @@ export function copyGlobalRoleToTaskRole(globalRole, taskId, now, name = globalR
49
50
  const timestamp = now.toISOString();
50
51
  return validateTaskRole({
51
52
  ...cloneProfile(globalRole),
53
+ ...(globalRole.instanceCode === undefined ? {} : { instanceCode: globalRole.instanceCode }),
52
54
  schemaVersion: 1,
53
55
  enabled: true,
54
56
  launchRevision: 1,
@@ -75,6 +77,8 @@ export function updateGlobalRole(role, patch, now) {
75
77
  updatedAt: now.toISOString()
76
78
  };
77
79
  clearProfileFields(updated, patch);
80
+ if (Object.hasOwn(patch, "instanceCode") && patch.instanceCode === undefined)
81
+ delete updated.instanceCode;
78
82
  updated.launchRevision = isDeepStrictEqual(desiredBefore, desiredLaunchProjection(updated))
79
83
  ? role.launchRevision
80
84
  : role.launchRevision + 1;
@@ -94,6 +98,8 @@ export function updateRole(role, patch, now) {
94
98
  updated.executionEnvironment = cloneJson(patch.executionEnvironment);
95
99
  }
96
100
  clearProfileFields(updated, patch);
101
+ if (Object.hasOwn(patch, "instanceCode") && patch.instanceCode === undefined)
102
+ delete updated.instanceCode;
97
103
  updated.launchRevision = isDeepStrictEqual(desiredBefore, desiredLaunchProjection(updated))
98
104
  ? role.launchRevision
99
105
  : role.launchRevision + 1;
@@ -222,6 +228,7 @@ function createRoleOwner(name, bindings, activeAgentId, workspace, now, profile,
222
228
  const timestamp = now.toISOString();
223
229
  return {
224
230
  ...cloneProfile(profile),
231
+ ...(profile.instanceCode === undefined ? {} : { instanceCode: validateInstanceCode(profile.instanceCode) }),
225
232
  enabled: true,
226
233
  launchRevision: 1,
227
234
  defaultAccess,
@@ -234,6 +241,8 @@ function createRoleOwner(name, bindings, activeAgentId, workspace, now, profile,
234
241
  };
235
242
  }
236
243
  function validateRoleOwner(role) {
244
+ if (role.instanceCode !== undefined)
245
+ validateInstanceCode(role.instanceCode);
237
246
  validateEnabled(role.enabled);
238
247
  if (role.schemaVersion !== 1) {
239
248
  throw new Error("Role schema version is invalid.");
@@ -303,6 +312,7 @@ function assertSessionOwnerMatchesRole(role, sessions) {
303
312
  }
304
313
  function cloneRolePatch(patch) {
305
314
  return {
315
+ ...(patch.instanceCode === undefined ? {} : { instanceCode: validateInstanceCode(patch.instanceCode) }),
306
316
  ...(patch.enabled === undefined ? {} : { enabled: patch.enabled }),
307
317
  ...(patch.name === undefined ? {} : { name: requireSafeIdentity(patch.name, "Role name") }),
308
318
  ...(patch.activeAgentId === undefined
@@ -81,6 +81,7 @@ class BuiltinAgentEndpoint {
81
81
  driver;
82
82
  configuration;
83
83
  get nativeAccess() { return this.driver.nativeAccess; }
84
+ get appliedGeneratedTitle() { return this.driver.appliedGeneratedTitle; }
84
85
  get ownedProcessId() { return this.driver.ownedProcessId; }
85
86
  get nativeAccountHome() { return this.driver.nativeAccountHome; }
86
87
  capabilities;
@@ -120,7 +120,13 @@ export async function runAgentHost(input) {
120
120
  const recentSteerAttempts = new Map();
121
121
  await replayExitOutbox(input.home);
122
122
  const updateSnapshot = (next) => {
123
- snapshot = validateSnapshot(next);
123
+ const { appliedGeneratedTitle: _oldTitle, ...rest } = next;
124
+ snapshot = validateSnapshot({
125
+ ...rest,
126
+ ...(session?.appliedGeneratedTitle === undefined ? {} : {
127
+ appliedGeneratedTitle: session.appliedGeneratedTitle
128
+ })
129
+ });
124
130
  };
125
131
  const authorityFields = () => ({
126
132
  ...(session === undefined ? {} : { endpointImplementation: session.configuration.implementation }),
@@ -374,6 +380,8 @@ export async function runAgentHost(input) {
374
380
  && currentPayload.environment.YUI_ROLE === "operator")
375
381
  ? {}
376
382
  : { sessionTitle: previousControl.sessionTitle }),
383
+ ...(previousControl.previousSessionTitle === undefined
384
+ ? {} : { previousSessionTitle: previousControl.previousSessionTitle }),
377
385
  codexThread: disconnectedSession.configuration.threadOptions,
378
386
  endpointImplementation: disconnectedSession.configuration.implementation,
379
387
  ...(ownedTurn === undefined ? {} : { ownedTurn }),
@@ -167,6 +167,9 @@ function validateProviderControl(control) {
167
167
  throw new Error("Agent Host Provider session title is invalid.");
168
168
  }
169
169
  }
170
+ if (control.previousSessionTitle !== undefined) {
171
+ text(control.previousSessionTitle, "previousSessionTitle");
172
+ }
170
173
  validateProviderAuthorityFence(control.authority);
171
174
  }
172
175
  function validateCodexThreadOptions(options) {
@@ -15,6 +15,9 @@ export function createRuntimeBinding(input) {
15
15
  ...(input.nativeSessionId === undefined
16
16
  ? {}
17
17
  : { nativeSessionId: requireText(input.nativeSessionId, "Native session id") }),
18
+ ...(input.appliedGeneratedTitle === undefined ? {} : {
19
+ appliedGeneratedTitle: requireText(input.appliedGeneratedTitle, "Applied generated title")
20
+ }),
18
21
  ...(input.providerAuthority === undefined
19
22
  ? {}
20
23
  : { providerAuthority: validateProviderAuthorityFence(input.providerAuthority) })
@@ -1,29 +1,33 @@
1
1
  import { resolveTimeZone } from "../config/timeZone.js";
2
+ import { validateInstanceCode } from "../role/instanceCode.js";
2
3
  const TITLE_SEPARATOR = " · ";
3
4
  export const MAX_SESSION_TITLE_LENGTH = 80;
4
5
  const TASK_TITLE_MAX_LENGTH = 20;
5
- export function taskRoleSessionTitle(task, roleName) {
6
- const prefix = `Yui ${roleLabel(roleName)} ${normalizeSegment(task.id)}`;
6
+ export function taskRoleSessionTitle(task, roleName, options = {}) {
7
+ const prefix = `Yui ${roleLabel(roleName)} ${normalizeSegment(task.id)}`
8
+ + (roleName === "leader" && options.executionAuthority === "planning" ? " Draft" : "")
9
+ + instanceSegment(options.instanceCode);
7
10
  const title = displayTitle(normalizeSegment(task.title), TASK_TITLE_MAX_LENGTH);
8
11
  const full = `${prefix}${TITLE_SEPARATOR}${title}`;
9
12
  if (full.length <= MAX_SESSION_TITLE_LENGTH)
10
13
  return full;
11
14
  if (prefix.length + TITLE_SEPARATOR.length + 1 > MAX_SESSION_TITLE_LENGTH) {
12
- return truncate(prefix, MAX_SESSION_TITLE_LENGTH);
15
+ throw new Error("Session title identity exceeds the maximum length; shorten the Role name or instance code.");
13
16
  }
14
- const titleLength = MAX_SESSION_TITLE_LENGTH - prefix.length - TITLE_SEPARATOR.length - 1;
17
+ const titleLength = MAX_SESSION_TITLE_LENGTH - prefix.length - TITLE_SEPARATOR.length;
15
18
  return `${prefix}${TITLE_SEPARATOR}${displayTitle(title, Math.max(titleLength, 1))}`;
16
19
  }
17
- export function resolveTaskRoleSessionTitle(existingTitle, task, roleName) {
20
+ export function resolveTaskRoleSessionTitle(existingTitle, task, roleName, options = {}) {
18
21
  if (existingTitle !== undefined
19
22
  && existingTitle.length > 0
20
23
  && existingTitle.length <= MAX_SESSION_TITLE_LENGTH
21
- && !/[\r\n\0]/u.test(existingTitle)) {
24
+ && !/[\r\n\0]/u.test(existingTitle)
25
+ && existingTitle !== options.generatedTitle) {
22
26
  return existingTitle;
23
27
  }
24
- return taskRoleSessionTitle(task, roleName);
28
+ return taskRoleSessionTitle(task, roleName, options);
25
29
  }
26
- export function operatorSessionTitle(now, configuredTimeZone) {
30
+ export function operatorSessionTitle(now, configuredTimeZone, instanceCode) {
27
31
  if (!(now instanceof Date) || !Number.isFinite(now.getTime())) {
28
32
  throw new TypeError("Operator session title date is invalid.");
29
33
  }
@@ -35,7 +39,23 @@ export function operatorSessionTitle(now, configuredTimeZone) {
35
39
  if (parts.month === undefined || parts.day === undefined) {
36
40
  throw new TypeError("Operator session title date is unavailable.");
37
41
  }
38
- return `Yui${TITLE_SEPARATOR}Operator${TITLE_SEPARATOR}${parts.month}${parts.day}`;
42
+ const prefix = instanceCode === undefined
43
+ ? `Yui${TITLE_SEPARATOR}Operator`
44
+ : `Yui Operator${instanceSegment(instanceCode)}`;
45
+ return `${prefix}${TITLE_SEPARATOR}${parts.month}${parts.day}`;
46
+ }
47
+ export function globalRoleSessionTitle(roleName, now, configuredTimeZone, instanceCode) {
48
+ if (roleName === "operator")
49
+ return operatorSessionTitle(now, configuredTimeZone, instanceCode);
50
+ if (instanceCode === undefined)
51
+ return undefined;
52
+ const title = `Yui ${roleLabel(roleName)}${instanceSegment(instanceCode)}`;
53
+ if (title.length > MAX_SESSION_TITLE_LENGTH)
54
+ throw new Error("Session title identity exceeds the maximum length.");
55
+ return title;
56
+ }
57
+ function instanceSegment(value) {
58
+ return value === undefined ? "" : ` @${validateInstanceCode(value)}`;
39
59
  }
40
60
  function displayTitle(value, maxLength) {
41
61
  if (value.length <= maxLength)
@@ -384,6 +384,7 @@ class CodexStructuredProviderSession {
384
384
  onActivity;
385
385
  mirror;
386
386
  nativeAccountHome;
387
+ appliedGeneratedTitle;
387
388
  nativeAccess;
388
389
  #turnAttempts = new Map();
389
390
  adapterId = "codex";
@@ -445,12 +446,20 @@ class CodexStructuredProviderSession {
445
446
  throw error;
446
447
  }
447
448
  }
448
- if (control.sessionTitle !== undefined) {
449
+ const threadMetadata = nativeConfiguration.thread;
450
+ const nativeTitle = typeof threadMetadata === "object" && threadMetadata !== null
451
+ && !Array.isArray(threadMetadata) ? threadMetadata.name : undefined;
452
+ // A native rename belongs to the user. Missing metadata is not evidence
453
+ // that an old title is still ours; leave it alone on resume.
454
+ let appliedGeneratedTitle;
455
+ if (control.sessionTitle !== undefined && (control.mode === "new"
456
+ || (control.previousSessionTitle !== undefined && nativeTitle === control.previousSessionTitle))) {
449
457
  try {
450
458
  await runtime.setConversationName({
451
459
  conversationId,
452
460
  name: control.sessionTitle
453
461
  });
462
+ appliedGeneratedTitle = control.sessionTitle;
454
463
  }
455
464
  catch (error) {
456
465
  onDiagnostic?.({
@@ -465,6 +474,7 @@ class CodexStructuredProviderSession {
465
474
  }
466
475
  const session = new CodexStructuredProviderSession(child, exit, processInstanceId, conversationId, runtime, onStarted, onTerminal, onGoal, onInput, onActivity, mirror, optionalId(initialized.codexHome));
467
476
  session.nativeAccess = createCodexNativeAccess(conversationId, initialized, channel, openingMessages, nativeConfiguration);
477
+ session.appliedGeneratedTitle = appliedGeneratedTitle;
468
478
  session.#activeTurnId = resumedActiveTurnId;
469
479
  const ownedTurn = control.kind === "restore" ? control.ownedTurn : undefined;
470
480
  session.#clientOwnedTurnId = ownedTurn?.turnId;
@@ -246,6 +246,7 @@ export class TmuxSessionHost {
246
246
  agentId: request.agentId,
247
247
  adapterId: request.adapterId,
248
248
  effective: request.effective,
249
+ ...(planned.generatedTitle === undefined ? {} : { generatedTitle: planned.generatedTitle }),
249
250
  ...(planned.launch.env.YUI_SESSION_TITLE === undefined
250
251
  ? {}
251
252
  : { sessionTitle: planned.launch.env.YUI_SESSION_TITLE }),
@@ -436,6 +437,9 @@ export class TmuxSessionHost {
436
437
  roleName: request.owner.roleName
437
438
  }),
438
439
  hostCreated,
440
+ ...(providerSnapshot?.appliedGeneratedTitle === undefined ? {} : {
441
+ appliedGeneratedTitle: providerSnapshot.appliedGeneratedTitle
442
+ }),
439
443
  ...((providerSnapshot?.nativeSessionId ?? nativeSessionId) === undefined
440
444
  ? {}
441
445
  : { nativeSessionId: providerSnapshot?.nativeSessionId ?? nativeSessionId }),
@@ -276,6 +276,16 @@ const MINOR_UPGRADES = Object.freeze([{
276
276
  // No payload rewriting or historical revision invention. A newly opened
277
277
  // reader starts empty; zero is the initial token until the first mutation.
278
278
  sql: TASK_READ_REVISION_SCHEMA_SQL
279
+ }, {
280
+ fromVersion: "1.24",
281
+ toVersion: "1.25",
282
+ name: "role-instance-code-session-titles",
283
+ introducedIn: "next",
284
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
285
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
286
+ // Optional Role presentation and generated-title provenance. Never infer
287
+ // ownership of historical titles, rewrite frozen profiles, or fill defaults.
288
+ sql: ""
279
289
  }]);
280
290
  export function storageMinorUpgradePlan(from) {
281
291
  if (from === CURRENT_STORAGE_VERSION)
@@ -1,4 +1,4 @@
1
- export const CURRENT_STORAGE_VERSION = "1.24";
1
+ export const CURRENT_STORAGE_VERSION = "1.25";
2
2
  export const MIN_SUPPORTED_STORAGE_VERSION = "1.0";
3
3
  export const STORAGE_FORMAT = "yui-home";
4
4
  export function isStorageVersion(value) {
@@ -51,6 +51,35 @@ merely to avoid a version decision. Package versions remain distinct from protoc
51
51
  and persistent-storage contracts: their compatibility checks and required migrations
52
52
  are unchanged, and the storage rules below do not authorize a package release.
53
53
 
54
+ ### 2.20.0 compatibility and migration notes
55
+
56
+ This minor adds independent optional Role instance codes (`--instance-code` /
57
+ `--clear-instance-code`) and a `Draft` marker for Leader Sessions with planning
58
+ authority. Operator-only configuration does not propagate to Leader or other
59
+ Roles. Task Role template copies remain independent; codes are presentation,
60
+ not identity, authorization or launch-configuration drift. Full lowercase Task
61
+ IDs remain unchanged. See [Role configuration](roles-and-configuration.md).
62
+
63
+ New Sessions use the configured code. Normal Codex Host launches/resumes can refresh
64
+ confirmed Yui-owned titles without forcing a stop/replacement. Historical untagged
65
+ titles and recorded custom names are preserved. Codex resume naming requires
66
+ the native name to match the previous generated title; missing metadata and
67
+ user renames are left untouched. Title provenance advances only after a successful
68
+ native naming receipt, not pre-start registration or live Host reuse. Other
69
+ Providers receive generated names only on creation, never on resume. Provider
70
+ naming remains best effort.
71
+
72
+ Storage advances **1.24 → 1.25** through an additive, no-rewrite transition for
73
+ optional Role code and Session title provenance. Older supported Homes follow
74
+ the centralized contiguous chain; do not open an upgraded Home with older
75
+ software. Controller/Host protocols remain v1. Publication does not authorize
76
+ installing the release, migrating a production Home, restarting shared services,
77
+ replacing Sessions or choosing codes for existing Roles.
78
+
79
+ Release gates require exact-merge Core CI and the same runtime tarball's
80
+ fresh-install matrix before npm `latest` publication. These deterministic
81
+ checks do not establish real-model behavior.
82
+
54
83
  ### 2.19.0 compatibility and migration notes
55
84
 
56
85
  This minor adds message backlog and blocked-delivery monitoring with Operator
@@ -36,6 +36,28 @@ Agent 选择一个预先声明的计划,设施从持久状态驱动该计划
36
36
  不能将不兼容冒称为兼容,也不为回避版本选择新增历史兼容机制。包版本与协议、
37
37
  持久存储契约仍然独立:兼容性检查和必要迁移要求不变,下文存储规则不授予发版权限。
38
38
 
39
+ ### 2.20.0 兼容性及迁移说明
40
+
41
+ 本 minor 增加按 Role 独立可选的实例码(`--instance-code` /
42
+ `--clear-instance-code`),以及持有 planning 权限的 Leader Session 的 `Draft`
43
+ 标识。仅设置 Operator 不会传播到 Leader 或其他 Role;Task Role 模板副本保持
44
+ 独立。实例码仅供展示,不改变身份、权限、启动配置版本或完整小写 Task ID。
45
+ 详见 [Role 配置](roles-and-configuration.zh-CN.md)。
46
+
47
+ 新会话采用配置,正常 Codex Host 启动/恢复可刷新确认由 Yui 管理的标题,不强停或替换会话。
48
+ 旧的无来源标记标题和已记录的用户标题保留。Codex 恢复时仅在原生名字仍匹配上次
49
+ 生成标题时改名;原生信息缺失或用户已改名则跳过。标题来源仅在原生命名成功回执后
50
+ 推进,不因预启动登记或存活 Host 复用而变化。其他 Provider 仅在新建时传入生成名,
51
+ 恢复时不传入。Provider 命名仍为尽力能力。
52
+
53
+ 存储通过中央连续迁移从 **1.24 → 1.25**,只增加可选实例码和标题来源,
54
+ 不改写历史记录。较早的受支持 Home 沿既有迁移链升级,不可用旧程序打开升级后的
55
+ Home。Controller/Host 协议仍为 v1。发布不授权安装升级、迁移生产 Home、
56
+ 重启共享服务、替换会话或为现有 Role 选择实例码。
57
+
58
+ 发布门禁要求准确合入 commit 的 Core CI 和同一 runtime tarball 的全平台
59
+ fresh-install 矩阵通过后才向 npm `latest` 发布;这些确定性验证不证明真实模型行为。
60
+
39
61
  ### 2.19.0 兼容性及迁移说明
40
62
 
41
63
  本 minor 新增消息积压、投递阻塞监测及 Operator 告警(PR #492)。观测区分正常执行、
@@ -18,6 +18,55 @@ requiring a same-named global Role. A global Role is a creation template only
18
18
  when the requested Task Role does not exist. Availability, producer separation
19
19
  and the frozen review candidate still apply.
20
20
 
21
+ ## Optional instance codes and Session titles
22
+
23
+ Each Role can independently set a display-only instance code. For example,
24
+ configure only Operator, leaving Leader and all other Roles unset:
25
+
26
+ ```sh
27
+ yui config role update operator --instance-code dev-bj
28
+ yui config role show operator
29
+ yui config role update operator --clear-instance-code
30
+ yui task role update task-159 leader --instance-code dev-sg
31
+ ```
32
+
33
+ `add` also accepts `--instance-code`. Codes accept 1–24 UTF-16 units of Unicode
34
+ letters, numbers, combining marks, hyphen and underscore; omit `@`. Whitespace,
35
+ controls, escapes and punctuation are rejected, not silently normalized.
36
+ There is no Home-wide fallback. A Global Role's code is copied when that template
37
+ creates a Task Role; later template changes do not rewrite the copy. Agent
38
+ Profiles do not supply display codes. Use Task Role update/clear for an existing
39
+ copy. Codes do not alter launch revisions, permissions, IDs or message routing.
40
+ Choose recognizable values; codes are not globally unique identities. Use
41
+ Session inspection and the actual Home/native identity when resolving ambiguity.
42
+
43
+ Generated Task titles use `Yui Leader task-159 Draft @dev-sg · Task name`.
44
+ `Draft` means the Leader Session has planning authority, including a retained
45
+ planning Session after activation; delivery Sessions omit it. Other Role names
46
+ and the full lowercase Task ID remain intact. Operator uses
47
+ `Yui Operator @dev-bj · MMDD`; without a code its existing
48
+ `Yui · Operator · MMDD` format remains. The date derives from Session creation
49
+ and the configured timezone. Other Global Roles get a generated title when a
50
+ code is configured. Descriptions are truncated first without splitting surrogate
51
+ pairs; if the identity itself cannot fit the 80-unit title limit, launch fails
52
+ with a bounded diagnostic instead of truncating the identity.
53
+
54
+ Configuration does not rename, stop or recreate a live Session. New Sessions
55
+ use it; a subsequent normal Host launch/resume can refresh confirmed Yui-owned titles,
56
+ not historical records. Existing untagged titles and recorded user names stay
57
+ untouched. Codex applies a resume rename only when its returned native name still
58
+ matches the previous generated title; absent metadata or a user rename prevents
59
+ the rename. Codex records only successfully applied names; live Host reuse,
60
+ skipped naming and naming failures do not advance title provenance. Naming is
61
+ best effort; unsupported Providers or failures do not invalidate the Session.
62
+ Other Providers receive generated names only on creation, never on resume,
63
+ to protect native user renames. No restart is performed to force a naming change.
64
+ On the safe Codex resume path, clearing a custom Global Role's code restores
65
+ `Yui role`; clearing Operator's code restores its unconfigured format.
66
+
67
+ Storage 1.24 → 1.25 adds optional code/title provenance without rewriting
68
+ historical payloads or inferring who named an old Session.
69
+
21
70
  ## Enable and disable
22
71
 
23
72
  Availability is configuration, separate from active Agent selection, Session
@@ -15,6 +15,44 @@ Task Role 是 Task 局部的;global Role 提供已配置的默认值和全局
15
15
  的 global Role。只有当请求的 Task Role 不存在时,global Role 才作为创建模板。
16
16
  可用性、producer 分离和冻结的审查候选仍然适用。
17
17
 
18
+ ## 可选实例码与会话标题
19
+
20
+ 每个 Role 独立配置仅用于展示的实例码。例如只设置 Operator,其他角色留空:
21
+
22
+ ```sh
23
+ yui config role update operator --instance-code dev-bj
24
+ yui config role show operator
25
+ yui config role update operator --clear-instance-code
26
+ yui task role update task-159 leader --instance-code dev-sg
27
+ ```
28
+
29
+ `add` 也接受 `--instance-code`。允许 1–24 个 UTF-16 单位的 Unicode 字母、
30
+ 数字、组合标记、短横线和下划线,不输入 `@`。空白、控制字符、转义和其他标点
31
+ 直接拒绝,不静默清洗。没有 Home 级默认值;Global Role 作为模板创建 Task Role
32
+ 时复制其值,之后模板变化不回写。Agent Profile 不携带实例码;既有 Task Role
33
+ 通过自己的 update/clear 修改。实例码不改变启动版本、权限、内部 ID 或消息寻址,
34
+ 也不保证全局唯一;遇到歧义应查看实际 Home、Session 和原生身份。
35
+
36
+ 生成的 Task 标题为 `Yui Leader task-159 Draft @dev-sg · 任务名`。
37
+ `Draft` 根据 Leader Session 的 planning 权限显示;Task 激活后保留的 planning
38
+ 会话仍显示 Draft,delivery 会话不显示。保留完整小写 Task ID。
39
+ Operator 为 `Yui Operator @dev-bj · MMDD`;未配置保持
40
+ `Yui · Operator · MMDD`,日期使用会话创建时间和配置时区。
41
+ 其他 Global Role 配置实例码后生成标题。80 单位总长度内优先截断任务名,
42
+ 不拆开代理对;身份部分无法容纳则明确报错,不静默截断。
43
+
44
+ 配置修改不会立即重命名、停止或重建活动会话。新会话采用新配置,后续正常
45
+ Host 启动/恢复可刷新确认由 Yui 管理的标题,不改写历史记录。
46
+ 缺少来源标记的旧标题及已记录的用户标题保留。Codex 恢复时只有原生返回的
47
+ 名字仍等于上次生成标题才改名;原生信息缺失或用户已改名则跳过。
48
+ Codex 只记录已成功应用的名称;复用存活 Host、命名失败或跳过不推进标题来源。
49
+ Provider 命名为尽力能力;失败或不支持不会使会话失效。其他 Provider 仅在新建时
50
+ 传入生成名,恢复时不重新传入名称,以保护原生用户改名;不为改名主动重启。
51
+ 在可安全改名的 Codex 恢复路径,清除自定义 Global Role 实例码后恢复
52
+ `Yui role`;Operator 恢复未配置格式。
53
+
54
+ 存储 1.24 → 1.25 增加可选实例码和生成标题来源,不回填历史记录或猜测标题所有权。
55
+
18
56
  ## 启用与停用
19
57
 
20
58
  启停是配置状态,与 Active Agent、Session 是否运行和 Task executionGate 分开:
@@ -2,7 +2,10 @@
2
2
 
3
3
  # Storage baseline 1.0
4
4
 
5
- Current storage is 1.24. The 1.23 → 1.24 transition adds transaction-maintained
5
+ Current storage is 1.25. The 1.24 → 1.25 transition admits optional Role
6
+ `instanceCode` and Session `generatedTitle` presentation fields without rewriting
7
+ existing records. Untagged historical titles remain user-owned; no code or
8
+ title provenance is inferred. The 1.23 → 1.24 transition adds transaction-maintained
6
9
  Task read-cache revisions for the scheduler's nine source tables. Inserts,
7
10
  updates and deletions invalidate only the affected Task; rollback also rolls
8
11
  back the token. These tokens are derived cache identities, never Task authority
@@ -2,7 +2,7 @@
2
2
 
3
3
  # 存储基线 1.0
4
4
 
5
- 当前存储为 1.24。1.23 → 1.24 为调度器的九类来源表增加事务维护的 Task 读取缓存修订号。插入、更新和删除只使对应 Task 的缓存失效,回滚同时回滚修订号。该值仅是派生缓存标识,不是 Task 权限或 Home revision CAS 的替代。历史 payload 不变,旧 Task 在首次修改前使用隐含的零修订号。
5
+ 当前存储为 1.25。1.24 → 1.25 增加可选 Role `instanceCode` 和 Session `generatedTitle` 展示字段,不回填历史标题来源或实例码,不改写既有记录。1.23 → 1.24 为调度器的九类来源表增加事务维护的 Task 读取缓存修订号。插入、更新和删除只使对应 Task 的缓存失效,回滚同时回滚修订号。该值仅是派生缓存标识,不是 Task 权限或 Home revision CAS 的替代。历史 payload 不变,旧 Task 在首次修改前使用隐含的零修订号。
6
6
  同时增加两个局部索引,支持按精确运行观测身份查找,不再扫描或反序列化历史观测集合。
7
7
 
8
8
  1.22 → 1.23 新增全局 Shell Job、版本化脚本文件及每次运行的配置快照,不改写已有 Job、计划和历史记录。
@@ -53,12 +53,12 @@ is required.
53
53
  Both include `test/core` and `test/platform`. For an already built checkout,
54
54
  `npm run test:business` runs only `test/core` and `npm run test:platform` runs
55
55
  only `test/platform`; neither rebuilds. CI owns one explicit build per job.
56
- The stable suite owns current storage 1.24 and its declared 1.0 through 1.23 minor
56
+ The stable suite owns current storage 1.25 and its declared 1.0 through 1.24 minor
57
57
  transition. It contains no undeclared historical format compatibility path.
58
58
 
59
59
  Current coverage includes:
60
60
 
61
- 1. Fresh storage 1.24, exact schema/record validation, no initialization over
61
+ 1. Fresh storage 1.25, exact schema/record validation, no initialization over
62
62
  unknown data, and rejection of old integer formats without mutation.
63
63
  2. Exact-version staging, mismatched-target refusal, same-major contiguous
64
64
  minor preflight, explicit maintenance-owner identity across handover, and
@@ -39,7 +39,7 @@ Yui 是一个单用户本地产品。永久验证保护关键的 happy path 和
39
39
  两者均包含 `test/core` 和 `test/platform`。已构建的 checkout 可分别运行
40
40
  `npm run test:business` 与 `npm run test:platform`,它们不会再次构建。
41
41
  CI 每个作业只显式构建一次。
42
- 稳定套件维护当前存储 1.24 和明确声明的 1.0 至 1.23 连续小版本迁移,
42
+ 稳定套件维护当前存储 1.25 和明确声明的 1.0 至 1.24 连续小版本迁移,
43
43
  不包含未声明的历史格式兼容路径。
44
44
 
45
45
  当前覆盖包括:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "2.19.0",
3
+ "version": "2.20.0",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,