@zq-silk/yui 2.5.0 → 2.7.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.
Files changed (104) hide show
  1. package/README.md +23 -0
  2. package/dist/cli/commandCatalog.js +31 -4
  3. package/dist/commands/globalRoleCommands.js +22 -2
  4. package/dist/commands/inboxCommands.js +352 -0
  5. package/dist/commands/roleSkillValidation.js +2 -0
  6. package/dist/commands/taskCommands.js +40 -7
  7. package/dist/commands/taskContextCommand.js +3 -2
  8. package/dist/context/currentSkills.js +48 -0
  9. package/dist/context/roleSessionContext.js +13 -16
  10. package/dist/context/runContextPack.js +2 -0
  11. package/dist/context/runInputContract.js +1 -1
  12. package/dist/context/sessionBootstrapManifest.js +27 -4
  13. package/dist/context/skillCatalog.js +2 -3
  14. package/dist/context/taskContext.js +9 -4
  15. package/dist/controlPlaneCli.js +18 -1
  16. package/dist/controller/controller.js +3 -2
  17. package/dist/controller/fileSchedulerStoreAdapter.js +19 -4
  18. package/dist/controller/globalInputDelivery.js +73 -15
  19. package/dist/controller/globalRuntimeAttention.js +1 -1
  20. package/dist/controller/operatorNotification.js +6 -1
  21. package/dist/controller/providerRetryDelivery.js +3 -5
  22. package/dist/controller/runtime.js +2 -2
  23. package/dist/doctor/doctor.js +25 -2
  24. package/dist/executor/effectiveLaunch.js +2 -2
  25. package/dist/executor/fileRoleLaunchPlanner.js +4 -6
  26. package/dist/interaction/notificationPresentation.js +2 -2
  27. package/dist/interaction/operatorPresentation.js +1 -1
  28. package/dist/kernel/builtinCapabilities.js +2 -1
  29. package/dist/message/globalInputBacklog.js +69 -0
  30. package/dist/message/globalMessageQuery.js +1 -0
  31. package/dist/message/inboxReceipt.js +32 -0
  32. package/dist/message/message.js +6 -1
  33. package/dist/message/messageContinuation.js +34 -13
  34. package/dist/message/taskInboxOwnership.js +21 -0
  35. package/dist/nativeAgent/session/backends.js +1 -1
  36. package/dist/observability/runtimeIdentity.js +1 -1
  37. package/dist/resources/sqliteResourceRegistry.js +1 -1
  38. package/dist/runtime/agentHost.js +105 -16
  39. package/dist/runtime/builtinAgentErrorMappers.js +9 -5
  40. package/dist/runtime/providerRetry.js +38 -0
  41. package/dist/runtime/taskSessions.js +240 -0
  42. package/dist/scheduler/operatorEvent.js +49 -0
  43. package/dist/scheduler/operatorInputNotificationProcessor.js +5 -4
  44. package/dist/scheduler/taskExecutionProjection.js +78 -6
  45. package/dist/scheduler/taskWake.js +5 -0
  46. package/dist/sqlite/driver.js +108 -0
  47. package/dist/storage/sqliteSchema.js +11 -1
  48. package/dist/storage/sqliteStore.js +60 -1
  49. package/dist/storage/storageSchema.js +1 -1
  50. package/dist/storage/storageVersions.js +1 -1
  51. package/dist/storage/storeRpc.js +1 -0
  52. package/dist/storage/upgrade/upgradeOrchestrator.js +1 -1
  53. package/dist/task/completionReadiness.js +11 -4
  54. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  55. package/dist/web/assets/assetManifest.js +2 -0
  56. package/dist/web/assets/client/app/taskView.js +1 -0
  57. package/dist/web/assets/client/domain/records.js +70 -2
  58. package/dist/web/assets/client/domain/runs.js +3 -2
  59. package/dist/web/assets/client/domain/vocab.js +1 -1
  60. package/dist/web/assets/client/i18n/en.js +17 -1
  61. package/dist/web/assets/client/i18n/zh-CN.js +17 -1
  62. package/dist/web/assets/client/lib/api.js +4 -1
  63. package/dist/web/assets/client/lib/theme.js +31 -10
  64. package/dist/web/assets/client/views/task/actions.js +38 -21
  65. package/dist/web/assets/client/views/task/observation.js +12 -3
  66. package/dist/web/assets/client/views/task/overview.js +7 -40
  67. package/dist/web/assets/client/views/task/runtime.js +71 -34
  68. package/dist/web/assets/shared/icons.js +2 -0
  69. package/dist/web/assets/shell/index.js +1 -0
  70. package/dist/web/assets/shell/settings.js +1 -0
  71. package/dist/web/assets/styles/views/task.js +14 -0
  72. package/dist/web/webServer.js +12 -0
  73. package/dist/web/webSessions.js +2 -165
  74. package/dist/web/webSnapshot.js +9 -5
  75. package/dist/web/webTaskActivity.js +54 -0
  76. package/dist/web/webTaskSurface.js +1 -0
  77. package/docs/managed-turn-and-session-runtime.md +81 -5
  78. package/docs/managed-turn-and-session-runtime.zh-CN.md +51 -3
  79. package/docs/observability/README.md +25 -1
  80. package/docs/observability/README.zh-CN.md +18 -1
  81. package/docs/provider-retry.md +11 -0
  82. package/docs/release-workflow.md +74 -2
  83. package/docs/release-workflow.zh-CN.md +56 -1
  84. package/docs/storage-baseline.md +17 -7
  85. package/docs/storage-baseline.zh-CN.md +11 -5
  86. package/docs/task-delivery.md +17 -8
  87. package/docs/task-delivery.zh-CN.md +11 -5
  88. package/docs/testing/verification-levels.md +22 -4
  89. package/docs/testing/verification-levels.zh-CN.md +8 -1
  90. package/i18n/README.zh-CN.md +19 -0
  91. package/package.json +5 -2
  92. package/skills/yui-leader/SKILL.md +28 -11
  93. package/skills/yui-leader/VERSION +1 -0
  94. package/skills/yui-operator/SKILL.md +10 -2
  95. package/skills/yui-operator/VERSION +1 -0
  96. package/skills/yui-operator/references/task-delivery.md +2 -0
  97. package/skills/yui-reviewer/SKILL.md +8 -1
  98. package/skills/yui-reviewer/VERSION +1 -0
  99. package/skills/yui-runtime/SKILL.md +94 -11
  100. package/skills/yui-runtime/VERSION +1 -0
  101. package/skills/yui-runtime/references/publication.md +16 -5
  102. package/skills/yui-runtime/references/recovery.md +9 -0
  103. package/skills/yui-worker/SKILL.md +8 -1
  104. package/skills/yui-worker/VERSION +1 -0
package/README.md CHANGED
@@ -67,6 +67,29 @@ Native first-run confirmations may still require your input.
67
67
  npm install -g @zq-silk/yui
68
68
  ```
69
69
 
70
+ SQLite is selected lazily before opening your Home: Yui probes a disposable
71
+ in-memory connection with `sqlite-modern` (`better-sqlite3@13.0.3`, N-API) first,
72
+ then tries `sqlite-compat` (`better-sqlite3@10.1.0`) only for identified missing
73
+ native bindings, Node ABI or system-library incompatibility. Each process keeps
74
+ its successful selection. Home permissions, corruption, locks, SQL and migration
75
+ errors never trigger a driver switch. `yui doctor` reports the driver version,
76
+ actual SQLite engine and fallback reason separately from Home health.
77
+
78
+ The N-API driver avoids the native `ObjectWrap` garbage-collection abort seen
79
+ with the earlier driver on Node 24.19/24.20. It requires Node-API 10;
80
+ runtimes with an older API (including Node 20 and Node 22.9) skip it before
81
+ native loading and use the compatibility candidate. No global Node change
82
+ or Home migration is needed for this dependency correction.
83
+
84
+ npm attempts **both** fixed optional aliases during installation; it does not
85
+ install the older one only after the newer one fails. This increases download
86
+ size and installation/build time. Optional installation failures may leave npm
87
+ successful, but Yui cannot run without a usable candidate; do not use
88
+ `--omit=optional`. The compatibility driver uses older SQLite (3.46.0), without
89
+ later fixes, and is not guaranteed on every old glibc/toolchain or newer Node
90
+ ABI. The supported Node range above is unchanged; an OS/toolchain upgrade may
91
+ still be necessary. Selection does not downgrade or migrate Home data.
92
+
70
93
  ### 2. Set up, yourself or with an Agent
71
94
 
72
95
  Run the interactive setup:
@@ -190,6 +190,11 @@ const roleChildren = [
190
190
  { name: "unbind", discovery: { surface: "public", audiences: ["public", "operator"] }, summary: "Unbind a dormant Agent from a global Role.", usage: "yui config role unbind <role> <agent-id>" }
191
191
  ];
192
192
  const globalSessionChildren = [
193
+ {
194
+ name: "skills", discovery: { surface: "managed", audiences: ["operator", "leader", "assignment", "global", "unbound"] },
195
+ summary: "Check current Skill paths, versions and availability for this managed Session without reading bodies.",
196
+ usage: "yui session skills"
197
+ },
193
198
  { name: "enter", discovery: { surface: "public", audiences: ["public", "operator"] }, summary: "Enter a global Role's native session.", usage: "yui session enter <role>" },
194
199
  {
195
200
  name: "context", discovery: { surface: "managed", audiences: ["operator", "global", "unbound"] },
@@ -366,7 +371,7 @@ const taskChildren = [
366
371
  }
367
372
  ]
368
373
  },
369
- { name: "reopen", discovery: { surface: "public", audiences: ["public", "operator"] }, summary: "Explicitly reopen completed or cancelled intent without replaying historical inputs.", usage: "yui task reopen <id>" },
374
+ { name: "reopen", discovery: { surface: "public", audiences: ["public", "operator", "leader"] }, summary: "Reopen a completed Task without replaying historical inputs; a current Leader is limited to its own Task. Restoring cancelled intent requires user or Operator authority; an independent execution stop is preserved.", usage: "yui task reopen <id>" },
370
375
  {
371
376
  name: "cancel", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
372
377
  summary: "Stop pursuing a Task without claiming its processes stopped.",
@@ -408,8 +413,9 @@ const taskChildren = [
408
413
  {
409
414
  name: "context", discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment", "unbound"] },
410
415
  summary: "Read compact authorized facts, fixed-bound delta, or inspect a Context reference.",
411
- usage: "yui task context [read|list|delta|inspect] <task> [--after <cursor>] [--continuation <cursor>] [--cursor <cursor>] [--limit <n>] [--store <store> --ref <id>] [--digest <digest>]",
412
- options: ["--after", "--continuation", "--cursor", "--limit", "--store", "--ref", "--digest", "--status", "--work-item"]
416
+ usage: "yui task context [read|list|delta|inspect] <task> [--after <cursor>] [--continuation <cursor>] [--cursor <cursor>] [--limit <n>] [--store <store> --ref <id>] [--digest <digest>] [--view <current|all>]",
417
+ options: ["--after", "--continuation", "--cursor", "--limit", "--store", "--ref", "--digest", "--status", "--work-item", "--view"],
418
+ optionValues: { "--view": ["current", "all"] }
413
419
  },
414
420
  {
415
421
  name: "next-action", discovery: { surface: "managed", audiences: ["operator", "leader"] },
@@ -1127,7 +1133,7 @@ export const ROOT_COMMAND = buildNode({
1127
1133
  { id: "general", title: "General", entries: [
1128
1134
  "help", "version", "update", "upgrade", "setup", "start", "doctor"
1129
1135
  ] },
1130
- { id: "workflow", title: "Workflow", entries: ["agent", "operator", "role", "project", "task"] },
1136
+ { id: "workflow", title: "Workflow", entries: ["agent", "operator", "role", "inbox", "project", "task"] },
1131
1137
  { id: "configuration", title: "Configuration", entries: ["config"] },
1132
1138
  { id: "operations", title: "Operations", entries: ["web", "controller", "session", "execution", "capability", "job", "jobs", "telemetry", "release"] },
1133
1139
  { id: "resources", title: "Resources", entries: ["resources"] },
@@ -1153,6 +1159,26 @@ export const ROOT_COMMAND = buildNode({
1153
1159
  options: catalogCommandOptions(name) }))
1154
1160
  ]
1155
1161
  },
1162
+ {
1163
+ name: "inbox",
1164
+ summary: "Read and explicitly handle the current managed Agent's own inbox.",
1165
+ sections: [{ id: "commands", title: "Commands", entries: ["list", "show", "claim", "ack"] }],
1166
+ children: [
1167
+ { name: "list", discovery: { surface: "managed", audiences: ["operator", "global", "leader", "assignment"] },
1168
+ summary: "Discover bounded own-address entries without consuming them.",
1169
+ usage: "yui inbox list [--limit <1..100>] [--cursor <cursor>]", options: ["--limit", "--cursor"] },
1170
+ { name: "show", discovery: { surface: "managed", audiences: ["operator", "global", "leader", "assignment"] },
1171
+ summary: "Read an exact original or fixed notification window without acknowledgement.",
1172
+ usage: "yui inbox show <entry> [--cursor <cursor>]", options: ["--cursor"] },
1173
+ { name: "claim", discovery: { surface: "managed", audiences: ["operator", "global", "leader", "assignment"] },
1174
+ summary: "Reserve 1..16 exact entries for handling in this turn; never starts a model turn.",
1175
+ usage: "yui inbox claim <entry> [<entry> ...] --request-id <id>", options: ["--request-id"] },
1176
+ { name: "ack", discovery: { surface: "managed", audiences: ["operator", "global", "leader", "assignment"] },
1177
+ summary: "Confirm handling of claimed entries with an auditable disposition, not Provider acceptance.",
1178
+ usage: "yui inbox ack <entry> [<entry> ...] --request-id <id> --reason <disposition>",
1179
+ options: ["--request-id", "--reason"] }
1180
+ ]
1181
+ },
1156
1182
  {
1157
1183
  name: "role", discovery: { surface: "public", audiences: ["public", "operator"] },
1158
1184
  summary: "Queue input or control an exact current Global Role Turn.",
@@ -1583,6 +1609,7 @@ export const ROOT_COMMAND = buildNode({
1583
1609
  "yui session reconcile --report"
1584
1610
  ],
1585
1611
  sections: [
1612
+ { id: "instructions", title: "Managed instructions", entries: ["skills"] },
1586
1613
  { id: "global", title: "Global Role sessions", entries: ["context", "enter", "record", "replace"] },
1587
1614
  { id: "maintenance", title: "Maintenance", entries: ["stop"] },
1588
1615
  { id: "recovery", title: "Recovery", entries: ["reconcile", "retry"] }
@@ -3,6 +3,9 @@ import { hasManagedIdentity } from "../runtime/managedIdentity.js";
3
3
  import { boundedDocument, recordPage, readOptions, readDigest, messageReceipt } from "../output/boundedRead.js";
4
4
  import { controlProviderRetry, providerRetryProjection, renderProviderRetry } from "../runtime/providerRetry.js";
5
5
  import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
6
+ import { globalInputBacklog } from "../message/globalInputBacklog.js";
7
+ import { checkCurrentSkills, SKILL_REUSE_RULE } from "../context/currentSkills.js";
8
+ import { roleSkillIds } from "../context/roleSessionContext.js";
6
9
  import { createRoleSessionSet, recordRoleAgentSession } from "../executor/agentExecutor.js";
7
10
  import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
8
11
  import { managedGlobalRoleWorkspace } from "../storage/homeLayout.js";
@@ -129,13 +132,21 @@ function roleContext(args, store, options) {
129
132
  const effective = resolveEffectiveLaunch({ role, purpose: "execution" });
130
133
  const sessions = store.getGlobalRoleSessionSet(name);
131
134
  const session = sessions?.sessions[role.activeAgentId];
132
- // Only the controlled Endpoint's accepted receipt consumes a queued Message.
135
+ const currentSkills = checkCurrentSkills(options.yuiHome ?? environment.YUI_HOME, roleSkillIds(session?.effective.context ?? role, role.name === "operator" ? "operator" : "global"));
136
+ // Native delivery and explicit Agent handling settle the same queued input
137
+ // through distinct receipts; a context read does neither.
133
138
  const messages = store.listGlobalRoleMessages(name);
134
139
  const queued = messages.filter((message) => message.delivery === undefined
140
+ && message.inbox?.ack === undefined
135
141
  && message.notDelivered === undefined
136
142
  && (message.interruptThen !== undefined || message.inputControl?.action === "queue"));
137
143
  const pending = recordPage(queued.map(globalMessageSummary), `global:${name}/messages/pending`, { limit: 8 });
138
144
  const recent = recordPage([...messages].reverse().map(globalMessageSummary), `global:${name}/messages/all`, { limit: 8 });
145
+ const observedAt = new Date();
146
+ const { oldestAgeMs, ...backlog } = globalInputBacklog(queued, sessions, observedAt, {
147
+ attention: name === "operator" ? store.getWorkMailbox({ kind: "operator" }) : null,
148
+ lifecycle: store.getWorkMailbox({ kind: "global-role-runtime", roleName: name })
149
+ });
139
150
  const context = Object.freeze({
140
151
  schemaVersion: 1,
141
152
  protocol: "yui-managed-context/v1",
@@ -173,7 +184,9 @@ function roleContext(args, store, options) {
173
184
  // Reading pending input is neither delivery nor implementation.
174
185
  pending,
175
186
  recent,
187
+ backlog,
176
188
  reads: {
189
+ ownInbox: "yui inbox list --json",
177
190
  messages: `role message list ${name}`,
178
191
  pending: `role message list ${name} --pending`,
179
192
  detail: `role message show ${name} <message-id>`
@@ -183,10 +196,16 @@ function roleContext(args, store, options) {
183
196
  // Historical interrupt receipts are available through Session inspection;
184
197
  // only this Turn's exact identity is required at entry.
185
198
  sessionManifestPath: environment.YUI_SESSION_MANIFEST,
199
+ skillCheckCommand: "yui session skills --json",
200
+ currentSkills,
201
+ skillLoadingRule: SKILL_REUSE_RULE,
186
202
  skillPackages: session?.effective.skillPackages ?? null,
187
203
  cliCommand: "yui"
188
204
  });
189
- const data = boundedDocument(context, `global:${name}/context`, parsed.cursor);
205
+ // Elapsed wall-clock time is live display metadata, not a changing source
206
+ // fact: keep it outside the digest so a long Context remains pageable.
207
+ const data = { ...boundedDocument(context, `global:${name}/context`, parsed.cursor),
208
+ backlogObservation: { observedAt: observedAt.toISOString(), oldestAgeMs } };
190
209
  return { kind: "output", data, output: [
191
210
  `Role Context: ${role.name}`,
192
211
  `Scope: global (${context.identity.roleKind})`,
@@ -626,6 +645,7 @@ function globalRoleMessage(args, store, options) {
626
645
  if (parsed.positionals.length !== 0)
627
646
  throw usageError("Role message list <role> [--pending] [--limit <n>] [--cursor <cursor>].");
628
647
  const selected = pending ? messages.filter(message => message.delivery === undefined
648
+ && message.inbox?.ack === undefined
629
649
  && message.notDelivered === undefined
630
650
  && (message.interruptThen !== undefined || message.inputControl?.action === "queue")) : [...messages].reverse();
631
651
  data = recordPage(selected.map(globalMessageSummary), `global:${name}/messages/${pending ? "pending" : "all"}`, parsed);
@@ -0,0 +1,352 @@
1
+ import { requireManagedGlobalCaller, requireManagedTaskCaller } from "../runtime/managedCaller.js";
2
+ import { boundedDocument, readDigest, readOptions, recordPage } from "../output/boundedRead.js";
3
+ import { claimPending, completeProcessing } from "../coordination/workMailbox.js";
4
+ import { createTaskWake, latestTaskWake } from "../scheduler/taskWake.js";
5
+ import { expandTaskMessageResult } from "../message/message.js";
6
+ import { messageAssignmentBlocker } from "../message/messageContinuation.js";
7
+ import { isLeaderSteer, latestMessageUpdate, taskMessageInboxReceipt, unfinishedLeaderInbox } from "../message/taskInboxOwnership.js";
8
+ import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
9
+ import { isTaskRecordRetired, retiredTaskRecordIds } from "../task/taskRecordRetirement.js";
10
+ const target = (address) => ({ kind: "role", taskId: address.taskId, roleName: address.roleName });
11
+ const source = (address) => `inbox:${address.scope}:${address.taskId ?? ""}:${address.roleName}`;
12
+ const pendingId = (pending) => `pending-${readDigest(pending)}`;
13
+ const sameSession = (a, b) => a.scope === b.scope && a.taskId === b.taskId
14
+ && a.roleName === b.roleName && a.agentId === b.agentId && a.nativeSessionId === b.nativeSessionId;
15
+ function addressOf(store, env) {
16
+ if (env.YUI_SESSION_SCOPE === "global") {
17
+ const caller = requireManagedGlobalCaller(store, env);
18
+ return { scope: "global", roleName: caller.roleName, agentId: caller.agentId, nativeSessionId: caller.nativeSessionId };
19
+ }
20
+ const caller = requireManagedTaskCaller(store, env);
21
+ if (caller.roleName !== "leader" && caller.currentRunId === undefined) {
22
+ throw new Error("An Assignment inbox requires its current active AgentRun.");
23
+ }
24
+ return { scope: "task", taskId: caller.taskId, roleName: caller.roleName,
25
+ agentId: caller.agentId, nativeSessionId: caller.nativeSessionId,
26
+ ...(caller.currentRunId === undefined ? {} : { runId: caller.currentRunId }) };
27
+ }
28
+ function ownsMessage(store, address, message) {
29
+ if (address.roleName === "leader" && message.taskId === address.taskId && isLeaderSteer(message))
30
+ return true;
31
+ if (address.runId === undefined || message.taskId !== address.taskId
32
+ || message.recipient?.roleName !== address.roleName)
33
+ return false;
34
+ const current = store.getRun(address.taskId, address.runId);
35
+ const owner = message.recipient.ownerRunId === undefined ? null
36
+ : store.getRun(address.taskId, message.recipient.ownerRunId);
37
+ return current !== null && owner !== null && current.roleName === owner.roleName
38
+ && owner.workItemId === message.recipient.workItemId && owner.reviewRoundId === message.recipient.reviewRoundId
39
+ && current.workItemId === owner.workItemId && current.reviewRoundId === owner.reviewRoundId
40
+ && (owner.id === current.id || messageAssignmentBlocker(store, message) === undefined);
41
+ }
42
+ function entries(store, address) {
43
+ if (address.scope === "global")
44
+ return store.listGlobalRoleMessages(address.roleName)
45
+ .filter(message => message.inputControl !== undefined || message.interruptThen !== undefined);
46
+ const messages = store.listMessages(address.taskId).filter(message => ownsMessage(store, address, message));
47
+ return address.roleName === "leader" ? [...store.listTaskWakes(address.taskId), ...messages] : messages;
48
+ }
49
+ function getEntry(store, address, id) {
50
+ const entry = entries(store, address).find(entry => entry.id === id);
51
+ if (entry === undefined)
52
+ throw new Error("Inbox entry is not in this Agent's current authorized receiving scope.");
53
+ return entry;
54
+ }
55
+ function isWake(entry) { return "fromCursor" in entry; }
56
+ function assertSelectedTarget(address, message) {
57
+ const selected = message.inputControl?.expectedSessionId;
58
+ const globalTarget = "roleName" in message ? message.deliveryTarget : undefined;
59
+ if (selected !== undefined && selected !== address.nativeSessionId
60
+ || globalTarget !== undefined && (globalTarget.agentId !== address.agentId
61
+ || globalTarget.nativeSessionId !== address.nativeSessionId)) {
62
+ throw new Error("The original input explicitly targets another native Session; inbox handling cannot retarget it.");
63
+ }
64
+ }
65
+ function save(store, address, entry) {
66
+ if (isWake(entry))
67
+ store.saveTaskWake(address.taskId, entry);
68
+ else if (address.scope === "global")
69
+ store.updateGlobalRoleMessage(entry);
70
+ else
71
+ store.updateMessage(address.taskId, entry);
72
+ }
73
+ /** The same durable claim is checked at automatic delivery admission. No lease
74
+ * expires, and neither a receipt nor a cursor gives a caller identity. */
75
+ function blocker(store, address, entry) {
76
+ if (!isWake(entry) && address.scope === "task"
77
+ && isTaskRecordRetired(store.listEvents(address.taskId), "message", entry.id))
78
+ return "message-retired";
79
+ if (entry.inbox?.ack !== undefined)
80
+ return "acknowledged";
81
+ if (entry.inbox !== undefined) {
82
+ return sameSession(entry.inbox.claim.actor, address) ? undefined : "claimed-by-previous-session";
83
+ }
84
+ if (isWake(entry)) {
85
+ if (entry.status === "consumed")
86
+ return "already-delivered";
87
+ return "automatic-notification-owns-window";
88
+ }
89
+ if (address.scope === "task" && taskMessageInboxReceipt(store, entry) !== undefined) {
90
+ return "claimed-by-notification-window";
91
+ }
92
+ if (entry.interruptThen !== undefined)
93
+ return "explicit-interrupt-handoff";
94
+ if (entry.control !== undefined && entry.control.outcome !== "rejected")
95
+ return `delivery-${entry.control.outcome}`;
96
+ // Both steer paths reserve pending control before submission and arbitrate
97
+ // with inbox claims. No control is unsubmitted, not an unknown delivery.
98
+ if (address.scope === "global") {
99
+ const message = entry;
100
+ if (message.delivery !== undefined)
101
+ return `already-delivered-${message.delivery.via}`;
102
+ if (message.deliveryTarget !== undefined && (message.deliveryTarget.nativeSessionId !== address.nativeSessionId
103
+ || message.deliveryTarget.agentId !== address.agentId))
104
+ return "selected-native-session-changed";
105
+ if (message.inputControl?.expectedSessionId !== undefined
106
+ && message.inputControl.expectedSessionId !== address.nativeSessionId)
107
+ return "selected-native-session-changed";
108
+ const turn = store.getGlobalRoleSessionSet(address.roleName)?.providerBinding?.run;
109
+ if (turn?.attemptId === `global-input:${address.roleName}/${entry.id}`)
110
+ return `native-attempt-${turn.status}`;
111
+ }
112
+ else {
113
+ const message = entry;
114
+ if (message.continuation?.runId !== undefined)
115
+ return "continuation-already-dispatched";
116
+ if (message.inputControl?.expectedSessionId !== undefined
117
+ && message.inputControl.expectedSessionId !== address.nativeSessionId)
118
+ return "selected-native-session-changed";
119
+ return address.roleName === "leader" && isLeaderSteer(message) ? undefined : messageAssignmentBlocker(store, message);
120
+ }
121
+ return undefined;
122
+ }
123
+ function checkMutation(store, address) {
124
+ const owner = address.scope === "global" ? { scope: "global", roleName: address.roleName }
125
+ : { scope: "task", taskId: address.taskId, roleName: address.roleName };
126
+ if (hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget(owner)))) {
127
+ throw new Error("Inbox cannot change while this runtime is being released.");
128
+ }
129
+ if (address.scope === "task") {
130
+ const task = store.getTask(address.taskId);
131
+ if (!task || !["active", "draft"].includes(task.status) || task.executionGate.state !== "enabled") {
132
+ throw new Error("Inbox handling requires an open Task with execution enabled.");
133
+ }
134
+ }
135
+ }
136
+ function summary(store, address, entry) {
137
+ return { id: entry.id, kind: isWake(entry) ? "notification-window" : "message",
138
+ createdAt: entry.createdAt, summary: isWake(entry) ? entry.reasons.join(", ").slice(0, 240) : entry.body.slice(0, 240),
139
+ blocked: blocker(store, address, entry) ?? null, inbox: entry.inbox ?? null,
140
+ read: `yui inbox show ${entry.id}`, ...(isWake(entry)
141
+ ? { originals: `yui task wake show ${entry.taskId} ${entry.id} --json` } : {}) };
142
+ }
143
+ function receipt(entry) {
144
+ return { id: entry.id, inbox: entry.inbox,
145
+ ...(isWake(entry) ? { originals: `yui task wake show ${entry.taskId} ${entry.id} --json` } : {}) };
146
+ }
147
+ /** Own-address command: there is deliberately no --role, --task or --session.
148
+ * Reads do not claim. Exact claim precedes semantic handling; exact ack follows
149
+ * full original reads and actual handling. A batch is independent per item. */
150
+ export function runInboxCommand(args, store, env, now = new Date()) {
151
+ const [action, ...tail] = args;
152
+ const address = addressOf(store, env);
153
+ if (action === "list") {
154
+ const parsed = readOptions(tail);
155
+ if (parsed.positionals.length)
156
+ throw new Error("Inbox list accepts only --limit and --cursor.");
157
+ const retired = address.scope === "task" ? retiredTaskRecordIds(store.listEvents(address.taskId), "message") : new Set();
158
+ const selected = entries(store, address).filter(entry => (isWake(entry) || !retired.has(entry.id))
159
+ && entry.inbox?.ack === undefined
160
+ && (isWake(entry) ? entry.status !== "consumed"
161
+ : address.scope === "global" ? entry.delivery === undefined
162
+ : entry.continuation?.runId === undefined));
163
+ const items = selected.map(entry => summary(store, address, entry));
164
+ if (address.scope === "task" && address.roleName === "leader") {
165
+ const mailbox = store.getWorkMailbox(target(address));
166
+ if (mailbox?.pending != null)
167
+ items.push({ id: pendingId(mailbox.pending), kind: "notification-window",
168
+ summary: mailbox.pending.reasons.join(", ").slice(0, 240),
169
+ blocked: mailbox.processing === null && !unfinishedLeaderInbox(store, address.taskId) ? null : "earlier-window-owned",
170
+ read: `yui inbox show ${pendingId(mailbox.pending)}` });
171
+ }
172
+ return recordPage(items, source(address), parsed);
173
+ }
174
+ if (action === "show") {
175
+ const parsed = readOptions(tail, ["--cursor"]);
176
+ if (parsed.positionals.length !== 1)
177
+ throw new Error("Inbox show requires one exact entry.");
178
+ const id = parsed.positionals[0];
179
+ if (id.startsWith("pending-") && address.scope === "task" && address.roleName === "leader") {
180
+ const pending = store.getWorkMailbox(target(address))?.pending;
181
+ if (!pending || id !== pendingId(pending))
182
+ throw new Error("Pending window changed; list the inbox again.");
183
+ return boundedDocument({ id, ...pending,
184
+ note: "Claim this exact window before processing; claim returns its durable wake and original-read command." }, `${source(address)}/${id}`, parsed.cursor);
185
+ }
186
+ const entry = getEntry(store, address, id);
187
+ const value = !isWake(entry) && address.scope === "task"
188
+ ? expandTaskMessageResult(entry, (task, run) => store.getRun(task, run)) : entry;
189
+ return boundedDocument(value, `${source(address)}/${id}`, parsed.cursor);
190
+ }
191
+ if (action !== "claim" && action !== "ack")
192
+ throw new Error("Inbox commands: list, show, claim, ack.");
193
+ const parsed = readOptions(tail, ["--request-id", "--reason"]);
194
+ const ids = parsed.positionals;
195
+ const requestId = parsed.values.get("--request-id");
196
+ const reason = parsed.values.get("--reason");
197
+ if (!requestId || requestId.length > 128 || !/^[A-Za-z0-9_.:-]+$/.test(requestId)) {
198
+ throw new Error("Inbox mutation requires a stable --request-id (1..128 safe characters).");
199
+ }
200
+ if (!ids.length || ids.length > 16 || new Set(ids).size !== ids.length) {
201
+ throw new Error("Select 1..16 distinct exact inbox entries; no wildcard acknowledgement.");
202
+ }
203
+ if (action === "ack" && (!reason?.trim() || Buffer.byteLength(reason) > 2048)) {
204
+ throw new Error("Acknowledgement requires --reason describing handling or why no action remains (up to 2048 bytes).");
205
+ }
206
+ if (action === "claim" && reason !== undefined)
207
+ throw new Error("Claim does not record a disposition; use ack after handling.");
208
+ const apply = (id) => store.transaction(tx => {
209
+ const current = addressOf(tx, env);
210
+ checkMutation(tx, current);
211
+ let entry;
212
+ if (id.startsWith("pending-") && current.scope === "task" && current.roleName === "leader" && action === "claim") {
213
+ // Retry the exact logical operation even after its pending batch moved.
214
+ const existing = tx.listTaskWakes(current.taskId).find(wake => wake.inbox?.claim.requestId === requestId && sameSession(wake.inbox.claim.actor, current));
215
+ if (existing) {
216
+ if (existing.inboxPendingId !== id) {
217
+ throw new Error("Inbox request id was reused for a different window.");
218
+ }
219
+ return receipt(existing);
220
+ }
221
+ const mailbox = tx.getWorkMailbox(target(current));
222
+ if (!mailbox?.pending || pendingId(mailbox.pending) !== id)
223
+ throw new Error("Pending window changed; list the inbox again.");
224
+ if (mailbox.processing !== null || unfinishedLeaderInbox(tx, current.taskId)) {
225
+ throw new Error("An earlier automatic or active inbox claim owns the notification window.");
226
+ }
227
+ // A pending Leader input with an exact transport/interrupt owner cannot be
228
+ // hidden inside an actively consumed window.
229
+ for (const ref of mailbox.pending.refs) {
230
+ if (ref.type !== "message")
231
+ continue;
232
+ const message = tx.listMessages(current.taskId).find(message => message.id === ref.id);
233
+ if (message !== undefined && taskMessageInboxReceipt(tx, message) !== undefined
234
+ || message?.interruptThen !== undefined
235
+ || message?.control !== undefined && message.control.outcome !== "rejected"
236
+ || message?.inputControl?.expectedSessionId !== undefined
237
+ && message.inputControl.expectedSessionId !== current.nativeSessionId) {
238
+ throw new Error("Window contains an input with an existing or foreign delivery owner; inspect its original receipt.");
239
+ }
240
+ }
241
+ const latest = latestTaskWake(tx.listTaskWakes(current.taskId));
242
+ // Pin each mutable Draft original's existing update event, not a clock
243
+ // comparison. A later edit is distinct input even within the same tick.
244
+ const refs = [...mailbox.pending.refs];
245
+ for (const ref of mailbox.pending.refs) {
246
+ if (ref.type !== "message" || ref.taskId !== current.taskId)
247
+ continue;
248
+ const message = tx.listMessages(current.taskId).find(message => message.id === ref.id);
249
+ const revision = message === undefined ? undefined : latestMessageUpdate(tx, message);
250
+ if (revision !== undefined && !refs.some(ref => ref.type === "event" && ref.id === revision.id)) {
251
+ refs.push({ type: "event", taskId: current.taskId, id: revision.id });
252
+ }
253
+ }
254
+ const wake = createTaskWake({ id: tx.nextTaskWakeId(current.taskId), taskId: current.taskId,
255
+ reasons: mailbox.pending.reasons, refs,
256
+ fromCursor: latest?.toCursor ?? tx.getTask(current.taskId).createdAt,
257
+ toCursor: new Date(Math.max(now.getTime(), Date.parse(mailbox.pending.lastQueuedAt))).toISOString(), now });
258
+ entry = { ...wake, inbox: { claim: { requestId, actor: current, at: now.toISOString() } },
259
+ inboxPendingId: id };
260
+ tx.saveWorkMailbox(claimPending(mailbox, { batchId: `inbox:${wake.id}`,
261
+ owner: `agent-inbox:${wake.id}`, startedAt: now.toISOString() }));
262
+ }
263
+ else {
264
+ entry = getEntry(tx, current, id);
265
+ if (action === "claim") {
266
+ if (entry.inbox !== undefined) {
267
+ if (!sameSession(entry.inbox.claim.actor, current))
268
+ throw new Error("Claim belongs to a previous Session; read the original and prior effects, then explicitly acknowledge only after finishing its handling.");
269
+ if (entry.inbox.claim.requestId !== requestId)
270
+ throw new Error("Entry already claimed; retain the original request id.");
271
+ return receipt(entry);
272
+ }
273
+ const blocked = blocker(tx, current, entry);
274
+ if (blocked !== undefined)
275
+ throw new Error(`Inbox entry cannot be claimed: ${blocked}.`);
276
+ for (const previous of entries(tx, current)) {
277
+ if (previous.id === entry.id)
278
+ break;
279
+ if (isWake(previous) || previous.inbox?.ack !== undefined)
280
+ continue;
281
+ if (current.scope === "task"
282
+ && isTaskRecordRetired(tx.listEvents(current.taskId), "message", previous.id))
283
+ continue;
284
+ if (current.scope === "global" && (previous.delivery !== undefined
285
+ || previous.notDelivered !== undefined))
286
+ continue;
287
+ if (current.scope === "task" && previous.continuation?.runId !== undefined)
288
+ continue;
289
+ if (previous.inbox !== undefined && sameSession(previous.inbox.claim.actor, current))
290
+ continue;
291
+ throw new Error(`Earlier inbox entry ${previous.id} must be handled first; input order is preserved.`);
292
+ }
293
+ entry = { ...entry, inbox: { claim: { requestId, actor: current, at: now.toISOString() } } };
294
+ }
295
+ else {
296
+ if (!entry.inbox)
297
+ throw new Error("Claim the exact entry before handling and acknowledgement.");
298
+ // Ack settles already-owned handling; it never submits or retargets
299
+ // the original. The current rightful successor may settle a claim
300
+ // acquired by the exact selected Session, retaining both identities.
301
+ const claimant = entry.inbox.claim.actor;
302
+ if (isWake(entry)) {
303
+ const messageIds = new Set(entry.refs?.filter(ref => ref.type === "message").map(ref => ref.id));
304
+ for (const message of tx.listMessages(current.taskId)) {
305
+ if (messageIds.has(message.id))
306
+ assertSelectedTarget(claimant, message);
307
+ }
308
+ }
309
+ else
310
+ assertSelectedTarget(claimant, entry);
311
+ if (!isWake(entry) && current.scope === "task"
312
+ && !(current.roleName === "leader" && isLeaderSteer(entry))) {
313
+ const invalid = messageAssignmentBlocker(tx, entry);
314
+ if (invalid !== undefined)
315
+ throw new Error(`Assignment no longer authorizes this inbox disposition: ${invalid}.`);
316
+ }
317
+ // Current rightful successor may settle a prior Session's unfinished
318
+ // claim explicitly; this never replays input or discards prior evidence.
319
+ if (entry.inbox.ack) {
320
+ if (entry.inbox.ack.requestId !== requestId || entry.inbox.ack.reason !== reason
321
+ || !sameSession(entry.inbox.ack.actor, current))
322
+ throw new Error("Conflicting acknowledgement; original disposition retained.");
323
+ return receipt(entry);
324
+ }
325
+ const inbox = { ...entry.inbox,
326
+ ack: { requestId, actor: current, at: now.toISOString(), reason: reason } };
327
+ if (isWake(entry)) {
328
+ const mailbox = tx.getWorkMailbox(target(current));
329
+ // stop/start removes operational mailboxes, not durable claim
330
+ // ownership. Settle that receipt without touching a later batch.
331
+ if (mailbox?.processing?.owner === `agent-inbox:${entry.id}`
332
+ && mailbox.processing.batchId === `inbox:${entry.id}`) {
333
+ tx.saveWorkMailbox(completeProcessing(mailbox, mailbox.processing.batchId));
334
+ }
335
+ }
336
+ entry = { ...entry, inbox };
337
+ }
338
+ }
339
+ save(tx, current, entry);
340
+ return receipt(entry);
341
+ });
342
+ if (ids.length === 1)
343
+ return apply(ids[0]);
344
+ return { results: ids.map(id => {
345
+ try {
346
+ return { id, ok: true, result: apply(id) };
347
+ }
348
+ catch (error) {
349
+ return { id, ok: false, reason: error instanceof Error ? error.message : String(error) };
350
+ }
351
+ }) };
352
+ }
@@ -1,5 +1,6 @@
1
1
  import { usageError } from "../errors/cliError.js";
2
2
  import { loadYuiSkillContexts } from "../context/roleSessionContext.js";
3
+ import { checkCurrentSkills } from "../context/currentSkills.js";
3
4
  const ROLE_LAUNCH_CONTEXT_OPTIONS = new Set([
4
5
  "--workspace",
5
6
  "--description",
@@ -34,6 +35,7 @@ export function validateConfiguredRoleSkills(yuiHome, skillIds) {
34
35
  throw usageError(`Configured Skill id is invalid: ${id}.`);
35
36
  }
36
37
  try {
38
+ checkCurrentSkills(yuiHome, [id]);
37
39
  loadYuiSkillContexts(yuiHome, [id]);
38
40
  }
39
41
  catch (error) {