@zq-silk/yui 2.19.0 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/cli/commandCatalog.js +5 -3
  2. package/dist/cli/invocationAuthority.js +2 -6
  3. package/dist/cli/managedDiagnostics.js +24 -16
  4. package/dist/commands/durableJobCommands.js +2 -3
  5. package/dist/commands/roleConfiguration.js +17 -0
  6. package/dist/commands/taskCommands.js +9 -36
  7. package/dist/commands/taskFactCommands.js +5 -5
  8. package/dist/commands/taskUsageCommand.js +2 -6
  9. package/dist/context/taskCatalog.js +4 -13
  10. package/dist/context/taskContext.js +35 -87
  11. package/dist/context/taskSearch.js +3 -7
  12. package/dist/controlPlaneCli.js +6 -18
  13. package/dist/controller/fileSchedulerStoreAdapter.js +22 -2
  14. package/dist/controller/jobClient.js +2 -2
  15. package/dist/controller/jobControl.js +2 -2
  16. package/dist/controller/jobDefinitionControl.js +5 -1
  17. package/dist/controller/runtimeLaunchCoordinator.js +12 -1
  18. package/dist/executor/agentExecutor.js +7 -0
  19. package/dist/executor/fileRoleLaunchPlanner.js +22 -6
  20. package/dist/executor/taskAgentCapabilities.js +2 -2
  21. package/dist/output/rolePresentation.js +1 -0
  22. package/dist/role/instanceCode.js +7 -0
  23. package/dist/role/role.js +10 -0
  24. package/dist/runtime/agentEndpoint.js +1 -0
  25. package/dist/runtime/agentHost.js +9 -1
  26. package/dist/runtime/launchBroker.js +3 -0
  27. package/dist/runtime/runtimeBinding.js +3 -0
  28. package/dist/runtime/sessionTitle.js +29 -9
  29. package/dist/runtime/structuredProviderHost.js +11 -1
  30. package/dist/runtime/tmuxAdapters.js +4 -0
  31. package/dist/storage/sqliteSchema.js +10 -0
  32. package/dist/storage/storageVersions.js +1 -1
  33. package/dist/task/taskAuthority.js +7 -3
  34. package/docs/cli-information-contract.md +17 -0
  35. package/docs/cli-information-contract.zh-CN.md +12 -0
  36. package/docs/release-workflow.md +54 -0
  37. package/docs/release-workflow.zh-CN.md +40 -0
  38. package/docs/roles-and-configuration.md +49 -0
  39. package/docs/roles-and-configuration.zh-CN.md +38 -0
  40. package/docs/storage-baseline.md +4 -1
  41. package/docs/storage-baseline.zh-CN.md +1 -1
  42. package/docs/task-discovery.md +5 -5
  43. package/docs/task-discovery.zh-CN.md +4 -4
  44. package/docs/testing/verification-levels.md +2 -2
  45. package/docs/testing/verification-levels.zh-CN.md +1 -1
  46. package/package.json +1 -1
  47. package/skills/yui-leader/SKILL.md +10 -6
  48. package/skills/yui-leader/VERSION +1 -1
  49. package/skills/yui-operator/SKILL.md +5 -3
  50. package/skills/yui-operator/VERSION +1 -1
  51. package/skills/yui-operator/references/task-delivery.md +4 -1
  52. package/skills/yui-reviewer/SKILL.md +4 -2
  53. package/skills/yui-reviewer/VERSION +1 -1
  54. package/skills/yui-runtime/SKILL.md +34 -8
  55. package/skills/yui-runtime/VERSION +1 -1
  56. package/skills/yui-runtime/references/publication.md +16 -9
  57. package/skills/yui-worker/SKILL.md +7 -4
  58. package/skills/yui-worker/VERSION +1 -1
@@ -9,7 +9,6 @@ import { projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryServic
9
9
  import { retiredTaskRecordIds, TASK_RECORD_RETIRED_EVENT } from "../task/taskRecordRetirement.js";
10
10
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
11
11
  import { contextContentDigest } from "./contextSnapshot.js";
12
- import { buildRunContextPack } from "./runContextPack.js";
13
12
  import { sourceRunContextValue } from "./sourceRunContext.js";
14
13
  import { boundedDocument, fitsReadBudget, summaryText, recordPage } from "../output/boundedRead.js";
15
14
  import { taskBriefGuard, taskPhase } from "./taskPhase.js";
@@ -27,12 +26,12 @@ export function readTaskContext(store, taskId, environment = {}) {
27
26
  const sources = authorizedSources(reader, scope, true);
28
27
  const count = sources.reduce((total, source) => total + source.count, 0);
29
28
  const cursor = currentCursor(reader, taskId);
30
- const phase = scope.allow === undefined ? taskPhase(reader, scope.task) : undefined;
29
+ const phase = taskPhase(reader, scope.task);
31
30
  const phaseSource = phase?.activation?.source;
32
31
  const priorMessageId = phaseSource?.priorMessageId;
33
32
  // Reserve a small, classified view before ordinary history consumes the
34
33
  // page. Counts and samples come from the exact same authorized read.
35
- const attention = summarizeAttention(reader, taskId, scope.allow);
34
+ const attention = summarizeAttention(reader, taskId);
36
35
  let bytes = Buffer.byteLength(JSON.stringify(attention));
37
36
  const records = [];
38
37
  let runtimeEvents;
@@ -75,13 +74,13 @@ export function readTaskContext(store, taskId, environment = {}) {
75
74
  const result = {
76
75
  taskId, coreCursor: encode(cursor), throughCursor: encode(cursor),
77
76
  view: "current",
78
- ...(scope.allow === undefined ? {
79
- phase,
80
- briefWriteGuard: taskBriefGuard(reader, scope.task)
81
- } : {}),
77
+ phase,
78
+ briefWriteGuard: taskBriefGuard(reader, scope.task),
82
79
  caller: scope.caller === undefined ? { kind: "unmanaged-or-global" } : {
80
+ taskId: scope.caller.taskId,
83
81
  role: scope.caller.roleName,
84
- executionAuthority: currentManagedRuntime(reader, environment, taskId)?.executionAuthority ?? "historical-read-only"
82
+ executionAuthority: scope.caller.taskId !== taskId ? "reference-only"
83
+ : currentManagedRuntime(reader, environment, taskId)?.executionAuthority ?? "historical-read-only"
85
84
  },
86
85
  collections: Object.entries(sources.reduce((counts, source) => {
87
86
  counts[source.family] = (counts[source.family] ?? 0) + source.count;
@@ -118,7 +117,7 @@ export function readTaskContext(store, taskId, environment = {}) {
118
117
  return result;
119
118
  });
120
119
  }
121
- function summarizeAttention(store, taskId, allow) {
120
+ function summarizeAttention(store, taskId) {
122
121
  const group = () => ({ count: 0, refs: [], omittedRefs: 0 });
123
122
  const attention = {
124
123
  openInputs: group(), pendingOperations: group(), unknownOperations: group(), messages: group()
@@ -131,7 +130,7 @@ function summarizeAttention(store, taskId, allow) {
131
130
  ]) {
132
131
  const page = store.queryContextRecords(taskId, {
133
132
  family, statuses, descending: category === "messages",
134
- unhandledMessages: category === "messages", ids: allowedIds(allow, family), limit: MAX_ATTENTION_REFS
133
+ unhandledMessages: category === "messages", limit: MAX_ATTENTION_REFS
135
134
  });
136
135
  const summary = attention[category];
137
136
  summary.count = page.count;
@@ -154,7 +153,7 @@ function summarizeAttention(store, taskId, allow) {
154
153
  */
155
154
  export function readTaskContextDelta(store, taskId, input, environment = {}) {
156
155
  return store.readTransaction((reader) => {
157
- const { allow } = authorizeContext(reader, taskId, environment);
156
+ authorizeContext(reader, taskId, environment);
158
157
  const start = decode(input.after, taskId);
159
158
  const now = currentCursor(reader, taskId);
160
159
  const continuation = input.continuation === undefined ? undefined : decodePage(input.continuation, taskId);
@@ -174,7 +173,7 @@ export function readTaskContextDelta(store, taskId, input, environment = {}) {
174
173
  throw usageError(`Context delta limit must be between 1 and ${MAX_EVENTS}.`);
175
174
  }
176
175
  const selected = reader.queryContextRecords(taskId, {
177
- family: "task-event", ids: allowedIds(allow, "task-event"),
176
+ family: "task-event",
178
177
  afterSequence: after, throughSequence: bound.sequence, limit
179
178
  });
180
179
  const page = [];
@@ -213,8 +212,7 @@ export function inspectTaskContext(store, taskId, selector, environment = {}) {
213
212
  export function readTaskContextResource(store, taskId, selector, environment = {}) {
214
213
  return store.readTransaction((reader) => {
215
214
  const scope = authorizeContext(reader, taskId, environment);
216
- const value = isAllowed(scope.allow, selector.store, selector.refId)
217
- ? inspectValue(reader, scope, selector) : null;
215
+ const value = inspectValue(reader, scope, selector);
218
216
  if (value === null)
219
217
  throw usageError("Context reference is unavailable in the caller's current scope.");
220
218
  const entry = materialize(selector.store, selector.refId, value);
@@ -280,9 +278,8 @@ export const CONTEXT_FAMILIES = [
280
278
  ];
281
279
  export function listContextMessages(store, taskId, environment = {}) {
282
280
  return store.readTransaction((reader) => {
283
- const { allow } = authorizeContext(reader, taskId, environment);
281
+ authorizeContext(reader, taskId, environment);
284
282
  return reader.listMessages(taskId)
285
- .filter((message) => isAllowed(allow, "task-message", message.id))
286
283
  .sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
287
284
  });
288
285
  }
@@ -334,7 +331,11 @@ export async function withContextObservations(core, providers, timeoutMs = 250)
334
331
  }
335
332
  return { ...result, omittedObservations: observations.length - result.observations.length };
336
333
  }
337
- /** Shared current/historical Session read identity; discovery may only narrow it. */
334
+ /** Authenticate ordinary reads of the local user's resource. Task/Assignment
335
+ * ownership is execution scope, not a confidentiality boundary. This does not
336
+ * switch Home, impersonate a target Role, or relax explicit global Job limits.
337
+ * Filesystem access and the authenticated Web/Controller transport remain the
338
+ * resource boundary of this single-user local product. */
338
339
  export function resolveContextReader(store, environment) {
339
340
  const caller = resolveManagedTaskReader(store, environment);
340
341
  if (caller === undefined && environment.YUI_SESSION_SCOPE === "global" && environment.YUI_ROLE !== "operator") {
@@ -351,57 +352,14 @@ export function resolveContextReader(store, environment) {
351
352
  }
352
353
  function authorizeContext(store, taskId, environment) {
353
354
  const caller = resolveContextReader(store, environment);
354
- if (caller !== undefined && caller.taskId !== taskId)
355
- throw usageError("Context is outside the caller's Task.");
356
355
  const task = store.getTask(taskId);
357
356
  if (task === null)
358
357
  throw taskNotFound(taskId);
359
- let allow;
360
- if (caller !== undefined && caller.roleName !== "leader") {
361
- if (caller.currentRunId === undefined)
362
- throw usageError("A managed Role needs a current AgentRun to read its scoped Context.");
363
- const pack = buildRunContextPack(store, taskId, caller.currentRunId);
364
- allow = new Set(pack.pointers.map((ref) => `${ref.store}:${ref.refId}`));
365
- allow.add(`role:${caller.roleName}`);
366
- allow.add(`turn:${caller.currentRunId}`);
367
- // Shared current user intent and this exact Assignment's live steers augment
368
- // the frozen refs. Query identities only: another Role's results, ordinary
369
- // addressed messages and Leader steers remain outside this scope.
370
- const run = store.getRun(taskId, caller.currentRunId);
371
- const current = store.contextInputReferences(taskId, {
372
- roleName: caller.roleName, workItemId: run?.workItemId, reviewRoundId: run?.reviewRoundId
373
- });
374
- for (const id of current.messages)
375
- allow.add(`task-message:${id}`);
376
- for (const id of current.events)
377
- allow.add(`task-event:${id}`);
378
- if (run?.workItemId !== undefined) {
379
- for (const job of store.listDurableJobs(taskId)) {
380
- if (job.owner.kind === "work-item" && job.owner.workItemId === run.workItemId)
381
- allow.add(`job:${job.id}`);
382
- }
383
- }
384
- }
385
- return { task, allow, caller };
386
- }
387
- /** Independent record commands share Context's exact read boundary, not a
388
- * second role-based approximation of Assignment visibility. */
389
- export function contextRecordReader(store, taskId, environment = {}) {
390
- const { allow } = authorizeContext(store, taskId, environment);
391
- return (family, id) => isAllowed(allow, family, id);
392
- }
393
- export function assertContextRecordReadable(store, taskId, family, id, environment = {}) {
394
- if (!contextRecordReader(store, taskId, environment)(family, id)) {
395
- throw usageError("Context reference is outside the caller's Assignment.");
396
- }
358
+ return { task, caller };
397
359
  }
398
- function isAllowed(allow, family, id) {
399
- return allow === undefined || allow.has(`${family}:${id}`);
400
- }
401
- function allowedIds(allow, family) {
402
- const prefix = `${family}:`;
403
- return allow === undefined ? undefined : [...allow]
404
- .filter(ref => ref.startsWith(prefix)).map(ref => ref.slice(prefix.length));
360
+ /** Independent record commands authenticate against the same resource boundary. */
361
+ export function assertTaskReadable(store, taskId, environment = {}) {
362
+ authorizeContext(store, taskId, environment);
405
363
  }
406
364
  function roleProfile(role) {
407
365
  return {
@@ -413,12 +371,12 @@ function roleProfile(role) {
413
371
  /** Resolve just the requested family. Families without a singular Store read
414
372
  * use their existing list primitive, without constructing unrelated Context.
415
373
  */
416
- function inspectValue(store, { task, allow, caller }, { store: family, refId }) {
374
+ function inspectValue(store, { task, caller }, { store: family, refId }) {
417
375
  const taskId = task.id;
418
376
  switch (family) {
419
377
  case "task": return refId === taskId ? task : null;
420
378
  case "task-brief": return refId === taskId ? store.getTaskBrief(taskId) : null;
421
- case "remote-delivery": return refId === taskId && allow === undefined
379
+ case "remote-delivery": return refId === taskId
422
380
  ? projectTaskRemoteDeliveryFromStore(store, task) : null;
423
381
  case "role": return store.getRole(taskId, refId);
424
382
  case "role-profile": {
@@ -442,8 +400,10 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
442
400
  }
443
401
  case "input-request": return store.getInputRequest(taskId, refId);
444
402
  case "work-item": return store.getWorkItem(taskId, refId);
445
- case "accepted-work-item":
446
- return allow?.has(`accepted-work-item:${refId}`) ? store.getWorkItem(taskId, refId) : null;
403
+ case "accepted-work-item": {
404
+ const item = store.getWorkItem(taskId, refId);
405
+ return item?.status === "accepted" ? item : null;
406
+ }
447
407
  case "candidate": {
448
408
  const parts = refId.split("/");
449
409
  if (parts.length !== 2)
@@ -457,8 +417,6 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
457
417
  return store.queryContextRecords(taskId, { family, ids: [refId], limit: 1 }).records[0]?.value ?? null;
458
418
  case "run": return store.getRun(taskId, refId);
459
419
  case "source-run": {
460
- if (!allow?.has(`source-turn:${refId}`))
461
- return null;
462
420
  const run = store.getRun(taskId, refId);
463
421
  return run === null ? null : sourceRunContextValue(run);
464
422
  }
@@ -488,15 +446,13 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
488
446
  case "managed-workspace": {
489
447
  // Existing frozen AgentRun overlays use the Task/Role alias; owner keys
490
448
  // identify the durable workspace in current Task reads.
491
- if (caller?.currentRunId !== undefined && refId === `${taskId}/${caller.roleName}`) {
449
+ if (caller?.taskId === taskId && caller.currentRunId !== undefined && refId === `${taskId}/${caller.roleName}`) {
492
450
  return store.getRun(taskId, caller.currentRunId)?.workspace ?? null;
493
451
  }
494
452
  return store.listManagedWorkspaces(taskId)
495
453
  .find((workspace) => managedWorkspaceKey(workspace.owner) === refId) ?? null;
496
454
  }
497
455
  case "mailbox": {
498
- if (allow !== undefined)
499
- return null;
500
456
  if (refId === "task")
501
457
  return store.getWorkMailbox({ kind: "task", taskId });
502
458
  const roleName = refId.startsWith("role:") ? refId.slice("role:".length) : undefined;
@@ -509,18 +465,18 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
509
465
  default: return null;
510
466
  }
511
467
  }
512
- function authorizedSources(store, { task, allow, caller }, current = false, requestedFamily, discovery = false) {
468
+ function authorizedSources(store, { task, caller }, current = false, requestedFamily, discovery = false) {
513
469
  const taskId = task.id;
514
470
  const sources = [];
515
471
  const add = (family, id, value) => {
516
- if ((requestedFamily === undefined || requestedFamily === family) && value !== null && isAllowed(allow, family, id)) {
472
+ if ((requestedFamily === undefined || requestedFamily === family) && value !== null) {
517
473
  sources.push({ family, count: 1, read: () => [materialize(family, id, value)] });
518
474
  }
519
475
  };
520
476
  const query = (family, options = {}) => {
521
477
  if (requestedFamily !== undefined && requestedFamily !== family)
522
478
  return;
523
- const selection = { ...options, family, ids: allowedIds(allow, family) };
479
+ const selection = { ...options, family };
524
480
  const { count } = store.queryContextRecords(taskId, { ...selection, limit: 0 });
525
481
  sources.push({ family, count, *read(offset = 0) {
526
482
  for (const { id, value } of store.queryContextRecords(taskId, {
@@ -531,7 +487,7 @@ function authorizedSources(store, { task, allow, caller }, current = false, requ
531
487
  };
532
488
  add("task", taskId, task);
533
489
  add("task-brief", taskId, store.getTaskBrief(taskId));
534
- if (allow === undefined && (requestedFamily === undefined || requestedFamily === "remote-delivery")) {
490
+ if (requestedFamily === undefined || requestedFamily === "remote-delivery") {
535
491
  add("remote-delivery", taskId, projectTaskRemoteDeliveryFromStore(store, task));
536
492
  }
537
493
  for (const role of store.listRoles(taskId)) {
@@ -552,9 +508,6 @@ function authorizedSources(store, { task, allow, caller }, current = false, requ
552
508
  // Current responsibilities and unanswered inputs precede historical bulk.
553
509
  query("input-request", { statuses: ["open"] });
554
510
  query("work-item", current ? { excludeStatuses: ["accepted", "retired"] } : {});
555
- for (const id of allowedIds(allow, "accepted-work-item") ?? []) {
556
- add("accepted-work-item", id, store.getWorkItem(taskId, id));
557
- }
558
511
  if (!current)
559
512
  query("candidate");
560
513
  // File artifacts are not enumerated into the synchronous Context listing: the
@@ -565,22 +518,17 @@ function authorizedSources(store, { task, allow, caller }, current = false, requ
565
518
  if (!current)
566
519
  query("environment-preparation");
567
520
  query("managed-workspace");
568
- if (caller?.currentRunId !== undefined) {
521
+ if (caller?.taskId === taskId && caller.currentRunId !== undefined) {
569
522
  add("managed-workspace", `${taskId}/${caller.roleName}`, store.getRun(taskId, caller.currentRunId)?.workspace ?? null);
570
523
  }
571
524
  if (!current)
572
525
  query("change-set");
573
526
  query("task-message", { descending: true, unhandledMessages: current });
574
527
  query("run", { descending: true, ...(current ? { statuses: ["active"] } : {}) });
575
- for (const id of allowedIds(allow, "source-turn") ?? []) {
576
- const run = store.getRun(taskId, id);
577
- if (run !== null)
578
- add("source-run", id, sourceRunContextValue(run));
579
- }
580
528
  query("review-round", { descending: true, ...(current ? { statuses: ["pending", "running"] } : {}) });
581
529
  if (!current)
582
530
  query("input-request", { excludeStatuses: ["open"] });
583
- if (!current && allow === undefined) {
531
+ if (!current) {
584
532
  add("mailbox", "task", store.getWorkMailbox({ kind: "task", taskId }));
585
533
  for (const role of store.listRoles(taskId)) {
586
534
  add("mailbox", `role:${role.name}`, store.getWorkMailbox({ kind: "role", taskId, roleName: role.name }));
@@ -1,8 +1,7 @@
1
1
  import { usageError } from "../errors/cliError.js";
2
2
  import { TASK_SEARCH_KINDS } from "../storage/taskSearch.js";
3
3
  import { contextContentDigest } from "./contextSnapshot.js";
4
- import { taskCatalogScope } from "./taskCatalog.js";
5
- import { readTaskContextResource } from "./taskContext.js";
4
+ import { readTaskContextResource, resolveContextReader } from "./taskContext.js";
6
5
  const PAGE_BYTES = 32 * 1024;
7
6
  /** Human-readable field pages, using the same source authorization and version
8
7
  * checks as Context. Offsets are Unicode code points, like search snippets. */
@@ -67,11 +66,8 @@ export function searchTaskBodies(store, options, environment = {}) {
67
66
  ...(options.kind === undefined ? [] : ["--kind", options.kind]),
68
67
  ...(options.cursor === undefined ? [] : ["--cursor", options.cursor])]);
69
68
  return store.readTransaction(reader => {
70
- const callerTask = taskCatalogScope(reader, environment);
71
- if (callerTask !== undefined && valid.taskId !== undefined && callerTask !== valid.taskId) {
72
- throw usageError("Search target is outside the caller's Task.");
73
- }
74
- const taskId = callerTask ?? valid.taskId;
69
+ resolveContextReader(reader, environment);
70
+ const taskId = valid.taskId;
75
71
  const scope = contextContentDigest({ taskId: taskId ?? null, query: valid.query, kind: valid.kind ?? null });
76
72
  const after = valid.cursor === undefined ? undefined : decodeCursor(valid.cursor, scope);
77
73
  const rows = reader.queryTaskSearch({ ...valid, taskId, after, limit: valid.limit + 1 });
@@ -14,14 +14,14 @@ import { renderAgentConfigurationResolutionNotice } from "./cli/agentConfigurati
14
14
  import { describeCommandTree, findCommandNode } from "./cli/commandCatalog.js";
15
15
  import { discoveryCommandTree } from "./cli/commandDiscovery.js";
16
16
  import { callerDiscoveryAudience } from "./cli/callerDiscovery.js";
17
- import { contextRecordReader } from "./context/taskContext.js";
17
+ import { assertTaskReadable } from "./context/taskContext.js";
18
18
  import { renderCompletion } from "./cli/completion.js";
19
19
  import { runCompletionWizard } from "./cli/completionWizard.js";
20
20
  import { resolveCompletionCandidates } from "./cli/dynamicCompletion.js";
21
21
  import { renderCommandHelp } from "./cli/helpRenderer.js";
22
22
  import { allowsInteractiveSelection, resolveInteractiveArguments } from "./cli/interactiveSelection.js";
23
23
  import { routeInvocation } from "./cli/invocationRouter.js";
24
- import { operatorOfflineCommand, taskDiagnosticTarget } from "./cli/managedDiagnostics.js";
24
+ import { operatorOfflineCommand, isTaskReadCommand } from "./cli/managedDiagnostics.js";
25
25
  import { assertConfigurationAuthority, assertTaskInvocationScope } from "./cli/invocationAuthority.js";
26
26
  import { requireManagedGlobalCaller, requireManagedGlobalReader, ManagedRuntimeDriftError } from "./runtime/managedCaller.js";
27
27
  import { assertJobAgentCommand } from "./job/agentRoleScope.js";
@@ -1094,7 +1094,7 @@ export async function main() {
1094
1094
  if (taskId === undefined || action === undefined || !["list", "read", "save"].includes(action)) {
1095
1095
  throw usageError(usage);
1096
1096
  }
1097
- if (process.env.YUI_SESSION_SCOPE === "task" && process.env.YUI_TASK_ID !== taskId) {
1097
+ if (action === "save" && process.env.YUI_SESSION_SCOPE === "task" && process.env.YUI_TASK_ID !== taskId) {
1098
1098
  throw usageError("Artifact is outside the managed Task scope.");
1099
1099
  }
1100
1100
  if (store.getTask(taskId) === null)
@@ -2065,12 +2065,7 @@ async function preflightManagedTaskControlPlane() {
2065
2065
  && ["agent-host", "session-notify", "runtime-hook"].includes(args[1] ?? "");
2066
2066
  const home = resolveYuiHome(process.env);
2067
2067
  assertManagedSessionManifest(home, "task");
2068
- const diagnosticTarget = taskDiagnosticTarget(args);
2069
- const diagnostic = diagnosticTarget !== undefined
2070
- && diagnosticTarget === process.env.YUI_TASK_ID;
2071
- if (diagnosticTarget !== undefined && !diagnostic) {
2072
- throw usageError("Diagnostics must remain within this Session's Task.");
2073
- }
2068
+ const diagnostic = isTaskReadCommand(args);
2074
2069
  // One gate for every managed command: the current CLI, Home, and Controller
2075
2070
  // must agree. Internal callbacks must still be able to append their immutable
2076
2071
  // fact while the Controller is offline.
@@ -2087,13 +2082,6 @@ async function preflightManagedTaskControlPlane() {
2087
2082
  : diagnostic
2088
2083
  ? resolveManagedTaskReader(verifiedStore, process.env)
2089
2084
  : requireManagedTaskCaller(verifiedStore, process.env);
2090
- if (diagnostic && runtime?.roleName !== "leader"
2091
- && !(args[1] === "show"
2092
- || (args[1] === "role" && args[2] === "session" && args[3] === "inspect"
2093
- && args[5] === runtime?.roleName))) {
2094
- // Non-Leaders keep their Assignment-scoped read authorization.
2095
- requireManagedTaskCaller(verifiedStore, process.env);
2096
- }
2097
2085
  const request = taskFinalReviewInvocation.request;
2098
2086
  if (request !== undefined && diagnostic)
2099
2087
  requireManagedTaskCaller(verifiedStore, process.env);
@@ -3079,7 +3067,7 @@ function completionSelectionPorts(home) {
3079
3067
  if (process.env.YUI_SESSION_SCOPE !== "task")
3080
3068
  return ports;
3081
3069
  const taskId = process.env.YUI_TASK_ID;
3082
- const readable = contextRecordReader(store, taskId, process.env);
3070
+ assertTaskReadable(store, taskId, process.env);
3083
3071
  const families = {
3084
3072
  "task.list": "task", "project.list": "project-policy", "task.role.list": "role",
3085
3073
  "task.work.list": "work-item", "task.message.list": "task-message", "task.input.list": "input-request",
@@ -3103,7 +3091,7 @@ function completionSelectionPorts(home) {
3103
3091
  return false;
3104
3092
  if (method === "project.list" && !store.getTask(taskId)?.projectBindings.some(binding => binding.projectId === id))
3105
3093
  return false;
3106
- return readable(family, id);
3094
+ return true;
3107
3095
  });
3108
3096
  } };
3109
3097
  }
@@ -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;
@@ -1,4 +1,4 @@
1
- import { resolveJobCaller } from "../task/taskAuthority.js";
1
+ import { resolveJobCaller, resolveJobReadCaller } from "../task/taskAuthority.js";
2
2
  import { callFileTaskController } from "./clientRuntime.js";
3
3
  export async function startDurableJob(home, params, clientOptions = {}) {
4
4
  // rr6/f2: Route through callFileTaskController so an Integration-issued
@@ -89,7 +89,7 @@ export function createControllerIntegrationJobPort(home, clientOptions = {}) {
89
89
  return job;
90
90
  },
91
91
  async getJob(taskId, jobId) {
92
- return getDurableJob(home, taskId, jobId, resolveJobCaller(clientOptions.environment, taskId), clientOptions);
92
+ return getDurableJob(home, taskId, jobId, resolveJobReadCaller(clientOptions.environment, taskId), clientOptions);
93
93
  },
94
94
  async cancelJob(taskId, jobId) {
95
95
  // rr8/rr12: Bind the cancel request to the caller's managed identity.
@@ -18,7 +18,7 @@ import { redactLaunchText } from "../runtime/launchDiagnostics.js";
18
18
  import { requireManagedTaskCaller, ManagedRuntimeDriftError } from "../runtime/managedCaller.js";
19
19
  import { CliError } from "../errors/cliError.js";
20
20
  import { assertJobAssignmentScope, JobAssignmentScopeError } from "../job/jobAssignmentScope.js";
21
- import { assertContextRecordReadable } from "../context/taskContext.js";
21
+ import { assertTaskReadable } from "../context/taskContext.js";
22
22
  import { assertTaskProjectWriteAuthority } from "../task/taskAuthority.js";
23
23
  import { validateHttpPreview } from "../job/httpPreview.js";
24
24
  import { definitionFromTaskRun } from "../job/jobDefinition.js";
@@ -134,7 +134,7 @@ export function createDurableJobControl(store) {
134
134
  if (caller === undefined)
135
135
  throw jobControlError("UNAUTHORIZED", "Job reads require an explicit caller.");
136
136
  try {
137
- assertContextRecordReadable(store, taskId, "job", jobId, caller.scope === "user" ? {} : {
137
+ assertTaskReadable(store, taskId, caller.scope === "user" ? {} : {
138
138
  YUI_SESSION_SCOPE: caller.scope, YUI_TASK_ID: caller.taskId, YUI_ROLE: caller.role,
139
139
  YUI_AGENT_ID: caller.agentId, YUI_ADAPTER_ID: caller.adapterId, YUI_NATIVE_SESSION_ID: caller.nativeSessionId
140
140
  });
@@ -137,7 +137,7 @@ export function authorizeRunRead(store, run, caller) {
137
137
  throw new Error("Task delegation is not an Assignment grant.");
138
138
  return;
139
139
  }
140
- // A revised definition's Assignment does not grant access to earlier Runs.
140
+ // Ordinary process-Run evidence does not transfer its execution Assignment.
141
141
  if ("taskId" in run)
142
142
  createDurableJobControl(store).getJob(run.taskId, run.id, caller);
143
143
  }
@@ -168,6 +168,8 @@ export function admitJobRun(store, home, scope, definitionId, caller, trigger, n
168
168
  const same = runs.find(run => run.trigger.key === trigger.key && run.trigger.source === trigger.source);
169
169
  if (same !== undefined) {
170
170
  authorizeRunRead(tx, same, caller);
171
+ if ("taskId" in same && !same.delegation)
172
+ requireJobAssignment(tx, same, caller);
171
173
  if (same.definitionRevision !== job.revision || same.retryOf !== retryOf) {
172
174
  throw new Error(`JobRun trigger conflicts with its original snapshot: ${same.id}.`);
173
175
  }
@@ -178,6 +180,8 @@ export function admitJobRun(store, home, scope, definitionId, caller, trigger, n
178
180
  || run.status === "unknown-needs-attention");
179
181
  if (occupied !== undefined) {
180
182
  authorizeRunRead(tx, occupied, caller);
183
+ if ("taskId" in occupied && !occupied.delegation)
184
+ requireJobAssignment(tx, occupied, caller);
181
185
  return { run: occupied, created: false, reason: "overlap" };
182
186
  }
183
187
  if (retryOf !== undefined && !runs.some(run => run.id === retryOf
@@ -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
  }