@zq-silk/yui 2.6.0 → 2.8.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 (126) hide show
  1. package/README.md +23 -0
  2. package/dist/cli/commandCatalog.js +34 -7
  3. package/dist/cli/updateCommand.js +3 -0
  4. package/dist/commands/configCommands.js +1 -0
  5. package/dist/commands/globalRoleCommands.js +62 -35
  6. package/dist/commands/inboxCommands.js +372 -0
  7. package/dist/commands/roleSkillValidation.js +2 -0
  8. package/dist/commands/taskCommands.js +39 -7
  9. package/dist/commands/taskRoleRuntimeStatus.js +2 -0
  10. package/dist/context/currentSkills.js +48 -0
  11. package/dist/context/roleSessionContext.js +13 -16
  12. package/dist/context/runContextPack.js +50 -3
  13. package/dist/context/runInputContract.js +1 -1
  14. package/dist/context/sessionBootstrapManifest.js +27 -4
  15. package/dist/context/skillCatalog.js +2 -3
  16. package/dist/context/taskContext.js +38 -12
  17. package/dist/controlPlaneCli.js +58 -7
  18. package/dist/controller/controller.js +8 -2
  19. package/dist/controller/fileSchedulerStoreAdapter.js +26 -6
  20. package/dist/controller/globalInputDelivery.js +86 -16
  21. package/dist/controller/globalRuntimeAttention.js +136 -6
  22. package/dist/controller/operatorNotification.js +6 -1
  23. package/dist/controller/providerRetryDelivery.js +3 -5
  24. package/dist/controller/runtime.js +6 -2
  25. package/dist/controller/structuredProviderObservation.js +51 -15
  26. package/dist/controller/taskHostTerminalRecovery.js +33 -0
  27. package/dist/doctor/doctor.js +25 -2
  28. package/dist/executor/effectiveLaunch.js +2 -2
  29. package/dist/executor/fileRoleLaunchPlanner.js +8 -15
  30. package/dist/interaction/notificationPresentation.js +2 -2
  31. package/dist/interaction/operatorPresentation.js +1 -1
  32. package/dist/message/globalInputBacklog.js +69 -0
  33. package/dist/message/globalMessageQuery.js +1 -0
  34. package/dist/message/inboxReceipt.js +62 -0
  35. package/dist/message/message.js +6 -1
  36. package/dist/message/messageContinuation.js +34 -13
  37. package/dist/message/taskInboxOwnership.js +43 -0
  38. package/dist/models/modelManagement.js +469 -0
  39. package/dist/models/modelManagementConfig.js +46 -0
  40. package/dist/models/modelManagementFiles.js +90 -0
  41. package/dist/models/modelManagementHttp.js +90 -0
  42. package/dist/models/modelManagementTypes.js +1 -0
  43. package/dist/nativeAgent/session/backends.js +1 -1
  44. package/dist/observability/runtimeIdentity.js +1 -1
  45. package/dist/output/boundedRead.js +43 -14
  46. package/dist/resources/sqliteResourceRegistry.js +1 -1
  47. package/dist/runtime/agentEndpointIdentity.js +8 -4
  48. package/dist/runtime/agentEndpointOwnership.js +5 -6
  49. package/dist/runtime/agentHost.js +165 -24
  50. package/dist/runtime/agentHostImplementation.js +18 -0
  51. package/dist/runtime/managedCaller.js +25 -1
  52. package/dist/runtime/managedIdentity.js +1 -1
  53. package/dist/runtime/providerRetry.js +19 -1
  54. package/dist/runtime/sessionCaller.js +53 -0
  55. package/dist/scheduler/operatorEvent.js +49 -0
  56. package/dist/scheduler/operatorInputNotificationProcessor.js +5 -4
  57. package/dist/scheduler/taskWake.js +5 -0
  58. package/dist/sqlite/driver.js +108 -0
  59. package/dist/storage/contextRecords.js +28 -0
  60. package/dist/storage/sqliteSchema.js +21 -1
  61. package/dist/storage/sqliteStore.js +64 -1
  62. package/dist/storage/storageSchema.js +1 -1
  63. package/dist/storage/storageVersions.js +1 -1
  64. package/dist/storage/storeRpc.js +1 -0
  65. package/dist/storage/taskStore.js +4 -0
  66. package/dist/storage/upgrade/upgradeOrchestrator.js +1 -1
  67. package/dist/task/completionReadiness.js +13 -6
  68. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  69. package/dist/web/assets/assetManifest.js +28 -2
  70. package/dist/web/assets/client/app/manage.js +6 -0
  71. package/dist/web/assets/client/app/models.js +20 -0
  72. package/dist/web/assets/client/app/settings.js +41 -9
  73. package/dist/web/assets/client/i18n/en.js +180 -2
  74. package/dist/web/assets/client/i18n/zh-CN.js +178 -2
  75. package/dist/web/assets/client/index.js +8 -0
  76. package/dist/web/assets/client/layout/manageShell.js +94 -0
  77. package/dist/web/assets/client/lib/api.js +13 -1
  78. package/dist/web/assets/client/lib/i18n.js +5 -3
  79. package/dist/web/assets/client/views/dock/conversation.js +231 -80
  80. package/dist/web/assets/client/views/models.js +777 -0
  81. package/dist/web/assets/client/views/skills.js +22 -15
  82. package/dist/web/assets/shared/icons.js +2 -0
  83. package/dist/web/assets/shared/manageNav.js +13 -0
  84. package/dist/web/assets/shell/manage.js +54 -0
  85. package/dist/web/assets/shell/models.js +7 -0
  86. package/dist/web/assets/shell/settings.js +1 -35
  87. package/dist/web/assets/shell/settingsContent.js +30 -0
  88. package/dist/web/assets/shell/sidebar.js +1 -0
  89. package/dist/web/assets/styles/index.js +6 -0
  90. package/dist/web/assets/styles/views/conversation.js +133 -0
  91. package/dist/web/assets/styles/views/manage.js +71 -0
  92. package/dist/web/assets/styles/views/models.js +129 -0
  93. package/dist/web/assets/styles/views/settings.js +28 -14
  94. package/dist/web/assets/styles/views/sidebar.js +1 -0
  95. package/dist/web/webServer.js +59 -9
  96. package/docs/cli-information-contract.md +42 -11
  97. package/docs/cli-information-contract.zh-CN.md +28 -7
  98. package/docs/managed-turn-and-session-runtime.md +127 -5
  99. package/docs/managed-turn-and-session-runtime.zh-CN.md +83 -3
  100. package/docs/release-workflow.md +105 -9
  101. package/docs/release-workflow.zh-CN.md +73 -5
  102. package/docs/storage-baseline.md +22 -7
  103. package/docs/storage-baseline.zh-CN.md +14 -5
  104. package/docs/task-delivery.md +17 -8
  105. package/docs/task-delivery.zh-CN.md +11 -5
  106. package/docs/testing/review-evaluation.md +148 -0
  107. package/docs/testing/review-evaluation.zh-CN.md +117 -0
  108. package/docs/testing/verification-levels.md +26 -4
  109. package/docs/testing/verification-levels.zh-CN.md +11 -1
  110. package/i18n/README.zh-CN.md +19 -0
  111. package/package.json +5 -2
  112. package/skills/yui-leader/SKILL.md +28 -11
  113. package/skills/yui-leader/VERSION +1 -0
  114. package/skills/yui-leader/references/execution.md +29 -0
  115. package/skills/yui-leader/references/replicated-execution.md +7 -0
  116. package/skills/yui-operator/SKILL.md +15 -3
  117. package/skills/yui-operator/VERSION +1 -0
  118. package/skills/yui-operator/references/task-delivery.md +2 -0
  119. package/skills/yui-reviewer/SKILL.md +43 -1
  120. package/skills/yui-reviewer/VERSION +1 -0
  121. package/skills/yui-runtime/SKILL.md +144 -15
  122. package/skills/yui-runtime/VERSION +1 -0
  123. package/skills/yui-runtime/references/publication.md +16 -5
  124. package/skills/yui-runtime/references/recovery.md +24 -5
  125. package/skills/yui-worker/SKILL.md +8 -1
  126. package/skills/yui-worker/VERSION +1 -0
@@ -0,0 +1,372 @@
1
+ import { requireManagedCaller } from "../runtime/sessionCaller.js";
2
+ import { boundedDocument, readDigest, readOptions, recordPage, summaryMetadata, summaryText } 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 { globalInboxDeliveryState, inboxHandlingState, isGlobalInboxMessage, pendingGlobalInboxMessages } from "../message/inboxReceipt.js";
8
+ import { deliveredMessageWake, isLeaderSteer, pinMessageRevisions, taskMessageInboxReceipt, unfinishedLeaderInbox } from "../message/taskInboxOwnership.js";
9
+ import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
10
+ import { isTaskRecordRetired, retiredTaskRecordIds } from "../task/taskRecordRetirement.js";
11
+ const target = (address) => ({ kind: "role", taskId: address.taskId, roleName: address.roleName });
12
+ const source = (address) => `inbox:${address.scope}:${address.taskId ?? ""}:${address.roleName}`;
13
+ const pendingId = (pending) => `pending-${readDigest(pending)}`;
14
+ const sameSession = (a, b) => a.scope === b.scope && a.taskId === b.taskId
15
+ && a.roleName === b.roleName && a.agentId === b.agentId && a.nativeSessionId === b.nativeSessionId;
16
+ function addressOf(store, env) {
17
+ const caller = requireManagedCaller(store, env);
18
+ if (caller.scope === "global") {
19
+ return { scope: "global", roleName: caller.roleName, agentId: caller.agentId, nativeSessionId: caller.nativeSessionId };
20
+ }
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(isGlobalInboxMessage);
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 acceptedInput(entry) {
57
+ return isWake(entry) ? entry.status === "consumed" : "roleName" in entry
58
+ ? globalInboxDeliveryState(entry) === "accepted" : entry.control?.outcome === "accepted";
59
+ }
60
+ function assertSelectedTarget(address, message) {
61
+ const selected = message.inputControl?.expectedSessionId;
62
+ const globalTarget = "roleName" in message ? message.deliveryTarget : undefined;
63
+ if (selected !== undefined && selected !== address.nativeSessionId
64
+ || globalTarget !== undefined && (globalTarget.agentId !== address.agentId
65
+ || globalTarget.nativeSessionId !== address.nativeSessionId)) {
66
+ throw new Error("The original input explicitly targets another native Session; inbox handling cannot retarget it.");
67
+ }
68
+ }
69
+ function save(store, address, entry) {
70
+ if (isWake(entry))
71
+ store.saveTaskWake(address.taskId, entry);
72
+ else if (address.scope === "global")
73
+ store.updateGlobalRoleMessage(entry);
74
+ else
75
+ store.updateMessage(address.taskId, entry);
76
+ }
77
+ /** The same durable claim is checked at automatic delivery admission. No lease
78
+ * expires, and neither a receipt nor a cursor gives a caller identity. */
79
+ function blocker(store, address, entry) {
80
+ if (!isWake(entry) && address.scope === "task"
81
+ && isTaskRecordRetired(store.listEvents(address.taskId), "message", entry.id))
82
+ return "message-retired";
83
+ if (entry.inbox?.ack !== undefined)
84
+ return "acknowledged";
85
+ if (entry.inbox !== undefined) {
86
+ return sameSession(entry.inbox.claim.actor, address) ? undefined : "claimed-by-previous-session";
87
+ }
88
+ if (isWake(entry)) {
89
+ // Native acceptance releases transport ownership, not semantic handling.
90
+ // The current Leader may explicitly claim the fixed delivered window.
91
+ if (entry.status === "consumed")
92
+ return undefined;
93
+ return "automatic-notification-owns-window";
94
+ }
95
+ if (address.scope === "task" && taskMessageInboxReceipt(store, entry) !== undefined) {
96
+ return "claimed-by-notification-window";
97
+ }
98
+ if (entry.control !== undefined && ["pending", "delivery-unknown"].includes(entry.control.outcome)) {
99
+ return `delivery-${entry.control.outcome}`;
100
+ }
101
+ const accepted = acceptedInput(entry);
102
+ if (entry.interruptThen !== undefined && !accepted
103
+ && !(address.scope === "task" && entry.continuation?.runId === address.runId
104
+ && address.runId !== undefined))
105
+ return "explicit-interrupt-handoff";
106
+ // Both steer paths reserve pending control before submission and arbitrate
107
+ // with inbox claims. No control is unsubmitted, not an unknown delivery.
108
+ if (address.scope === "global") {
109
+ const message = entry;
110
+ if (message.delivery !== undefined && !accepted)
111
+ return `already-delivered-${message.delivery.via}`;
112
+ // Explicitly settling delivered input is not a new submission or target
113
+ // selection. A current rightful successor retains the original target.
114
+ if (accepted)
115
+ return undefined;
116
+ if (message.deliveryTarget !== undefined && (message.deliveryTarget.nativeSessionId !== address.nativeSessionId
117
+ || message.deliveryTarget.agentId !== address.agentId))
118
+ return "selected-native-session-changed";
119
+ if (message.inputControl?.expectedSessionId !== undefined
120
+ && message.inputControl.expectedSessionId !== address.nativeSessionId)
121
+ return "selected-native-session-changed";
122
+ const turn = store.getGlobalRoleSessionSet(address.roleName)?.providerBinding?.run;
123
+ if (turn?.attemptId === `global-input:${address.roleName}/${entry.id}`)
124
+ return `native-attempt-${turn.status}`;
125
+ }
126
+ else {
127
+ const message = entry;
128
+ if (message.continuation?.runId !== undefined && message.continuation.runId !== address.runId) {
129
+ return "continuation-already-dispatched";
130
+ }
131
+ if (!accepted && message.inputControl?.expectedSessionId !== undefined
132
+ && message.inputControl.expectedSessionId !== address.nativeSessionId)
133
+ return "selected-native-session-changed";
134
+ return address.roleName === "leader" && isLeaderSteer(message) ? undefined : messageAssignmentBlocker(store, message);
135
+ }
136
+ return undefined;
137
+ }
138
+ function checkMutation(store, address) {
139
+ const owner = address.scope === "global" ? { scope: "global", roleName: address.roleName }
140
+ : { scope: "task", taskId: address.taskId, roleName: address.roleName };
141
+ if (hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget(owner)))) {
142
+ throw new Error("Inbox cannot change while this runtime is being released.");
143
+ }
144
+ if (address.scope === "task") {
145
+ const task = store.getTask(address.taskId);
146
+ if (!task || !["active", "draft"].includes(task.status) || task.executionGate.state !== "enabled") {
147
+ throw new Error("Inbox handling requires an open Task with execution enabled.");
148
+ }
149
+ }
150
+ }
151
+ function summary(store, address, entry) {
152
+ return { id: entry.id, kind: isWake(entry) ? "notification-window" : "message",
153
+ createdAt: entry.createdAt, summary: summaryText(isWake(entry) ? entry.reasons.join(", ") : entry.body),
154
+ blocked: blocker(store, address, entry) ?? null, inbox: summaryMetadata(entry.inbox ?? null),
155
+ handling: inboxHandlingState(entry.inbox),
156
+ delivery: isWake(entry) ? entry.status : summaryMetadata({
157
+ control: entry.control ?? null,
158
+ ...(address.scope === "global" ? { receipt: entry.delivery ?? null,
159
+ state: globalInboxDeliveryState(entry) }
160
+ : { continuation: entry.continuation ?? null })
161
+ }),
162
+ read: `yui inbox show ${entry.id}`, ...(isWake(entry)
163
+ ? { originals: `yui task wake show ${entry.taskId} ${entry.id} --json` } : {}) };
164
+ }
165
+ function receipt(entry) {
166
+ return { id: entry.id, inbox: entry.inbox,
167
+ ...(isWake(entry) ? { originals: `yui task wake show ${entry.taskId} ${entry.id} --json` } : {}) };
168
+ }
169
+ /** Own-address command: there is deliberately no --role, --task or --session.
170
+ * Reads do not claim. Exact claim precedes semantic handling; exact ack follows
171
+ * full original reads and actual handling. A batch is independent per item. */
172
+ export function runInboxCommand(args, store, env, now = new Date()) {
173
+ const [action, ...tail] = args;
174
+ const address = addressOf(store, env);
175
+ if (action === "list") {
176
+ const parsed = readOptions(tail);
177
+ if (parsed.positionals.length)
178
+ throw new Error("Inbox list accepts only --limit and --cursor.");
179
+ const retired = address.scope === "task" ? retiredTaskRecordIds(store.listEvents(address.taskId), "message") : new Set();
180
+ const selected = address.scope === "global" ? pendingGlobalInboxMessages(store.listGlobalRoleMessages(address.roleName))
181
+ : entries(store, address).filter(entry => (isWake(entry) || !retired.has(entry.id))
182
+ && inboxHandlingState(entry.inbox) !== "handled");
183
+ const items = selected.map(entry => summary(store, address, entry));
184
+ if (address.scope === "task" && address.roleName === "leader") {
185
+ const mailbox = store.getWorkMailbox(target(address));
186
+ if (mailbox?.pending != null)
187
+ items.push({ id: pendingId(mailbox.pending), kind: "notification-window",
188
+ summary: summaryText(mailbox.pending.reasons.join(", ")),
189
+ blocked: mailbox.processing === null && !unfinishedLeaderInbox(store, address.taskId) ? null : "earlier-window-owned",
190
+ read: `yui inbox show ${pendingId(mailbox.pending)}` });
191
+ }
192
+ return recordPage(items, source(address), parsed, { identity: address });
193
+ }
194
+ if (action === "show") {
195
+ const parsed = readOptions(tail, ["--cursor"]);
196
+ if (parsed.positionals.length !== 1)
197
+ throw new Error("Inbox show requires one exact entry.");
198
+ const id = parsed.positionals[0];
199
+ if (id.startsWith("pending-") && address.scope === "task" && address.roleName === "leader") {
200
+ const pending = store.getWorkMailbox(target(address))?.pending;
201
+ if (!pending || id !== pendingId(pending))
202
+ throw new Error("Pending window changed; list the inbox again.");
203
+ return boundedDocument({ id, ...pending,
204
+ note: "Claim this exact window before processing; claim returns its durable wake and original-read command." }, `${source(address)}/${id}`, parsed.cursor);
205
+ }
206
+ const entry = getEntry(store, address, id);
207
+ const value = !isWake(entry) && address.scope === "task"
208
+ ? expandTaskMessageResult(entry, (task, run) => store.getRun(task, run)) : entry;
209
+ return boundedDocument(value, `${source(address)}/${id}`, parsed.cursor);
210
+ }
211
+ if (action !== "claim" && action !== "ack")
212
+ throw new Error("Inbox commands: list, show, claim, ack.");
213
+ const parsed = readOptions(tail, ["--request-id", "--reason"]);
214
+ const ids = parsed.positionals;
215
+ const requestId = parsed.values.get("--request-id");
216
+ const reason = parsed.values.get("--reason");
217
+ if (!requestId || requestId.length > 128 || !/^[A-Za-z0-9_.:-]+$/.test(requestId)) {
218
+ throw new Error("Inbox mutation requires a stable --request-id (1..128 safe characters).");
219
+ }
220
+ if (!ids.length || ids.length > 16 || new Set(ids).size !== ids.length) {
221
+ throw new Error("Select 1..16 distinct exact inbox entries; no wildcard acknowledgement.");
222
+ }
223
+ if (action === "ack" && (!reason?.trim() || Buffer.byteLength(reason) > 2048)) {
224
+ throw new Error("Acknowledgement requires --reason describing handling or why no action remains (up to 2048 bytes).");
225
+ }
226
+ if (action === "claim" && reason !== undefined)
227
+ throw new Error("Claim does not record a disposition; use ack after handling.");
228
+ const apply = (id) => store.transaction(tx => {
229
+ const current = addressOf(tx, env);
230
+ checkMutation(tx, current);
231
+ let entry;
232
+ if (id.startsWith("pending-") && current.scope === "task" && current.roleName === "leader" && action === "claim") {
233
+ // Retry the exact logical operation even after its pending batch moved.
234
+ const existing = tx.listTaskWakes(current.taskId).find(wake => wake.inbox?.claim.requestId === requestId && sameSession(wake.inbox.claim.actor, current));
235
+ if (existing) {
236
+ if (existing.inboxPendingId !== id) {
237
+ throw new Error("Inbox request id was reused for a different window.");
238
+ }
239
+ return receipt(existing);
240
+ }
241
+ const mailbox = tx.getWorkMailbox(target(current));
242
+ if (!mailbox?.pending || pendingId(mailbox.pending) !== id)
243
+ throw new Error("Pending window changed; list the inbox again.");
244
+ if (mailbox.processing !== null || unfinishedLeaderInbox(tx, current.taskId)) {
245
+ throw new Error("An earlier automatic or active inbox claim owns the notification window.");
246
+ }
247
+ // A pending Leader input with an exact transport/interrupt owner cannot be
248
+ // hidden inside an actively consumed window.
249
+ for (const ref of mailbox.pending.refs) {
250
+ if (ref.type !== "message")
251
+ continue;
252
+ const message = tx.listMessages(current.taskId).find(message => message.id === ref.id);
253
+ if (message !== undefined && (taskMessageInboxReceipt(tx, message) !== undefined
254
+ || deliveredMessageWake(tx, message) !== undefined)
255
+ || message?.interruptThen !== undefined
256
+ || message?.control !== undefined && message.control.outcome !== "rejected"
257
+ || message?.inputControl?.expectedSessionId !== undefined
258
+ && message.inputControl.expectedSessionId !== current.nativeSessionId) {
259
+ throw new Error("Window contains an input with an existing or foreign delivery owner; inspect its original receipt.");
260
+ }
261
+ }
262
+ const latest = latestTaskWake(tx.listTaskWakes(current.taskId));
263
+ // Pin each mutable Draft original's existing update event, not a clock
264
+ // comparison. A later edit is distinct input even within the same tick.
265
+ const refs = pinMessageRevisions(tx, current.taskId, mailbox.pending.refs);
266
+ const wake = createTaskWake({ id: tx.nextTaskWakeId(current.taskId), taskId: current.taskId,
267
+ reasons: mailbox.pending.reasons, refs,
268
+ fromCursor: latest?.toCursor ?? tx.getTask(current.taskId).createdAt,
269
+ toCursor: new Date(Math.max(now.getTime(), Date.parse(mailbox.pending.lastQueuedAt))).toISOString(), now });
270
+ entry = { ...wake, inbox: { claim: { requestId, actor: current, at: now.toISOString() } },
271
+ inboxPendingId: id };
272
+ tx.saveWorkMailbox(claimPending(mailbox, { batchId: `inbox:${wake.id}`,
273
+ owner: `agent-inbox:${wake.id}`, startedAt: now.toISOString() }));
274
+ }
275
+ else {
276
+ entry = getEntry(tx, current, id);
277
+ if (action === "claim") {
278
+ if (entry.inbox !== undefined) {
279
+ if (!sameSession(entry.inbox.claim.actor, current))
280
+ throw new Error("Claim belongs to a previous Session; read the original and prior effects, then explicitly acknowledge only after finishing its handling.");
281
+ if (entry.inbox.claim.requestId !== requestId)
282
+ throw new Error("Entry already claimed; retain the original request id.");
283
+ return receipt(entry);
284
+ }
285
+ const blocked = blocker(tx, current, entry);
286
+ if (blocked !== undefined)
287
+ throw new Error(`Inbox entry cannot be claimed: ${blocked}.`);
288
+ if (isWake(entry) && !acceptedInput(entry)) {
289
+ for (const ref of entry.refs ?? []) {
290
+ if (ref.type !== "message")
291
+ continue;
292
+ const message = tx.listMessages(current.taskId).find(message => message.id === ref.id);
293
+ if (message !== undefined)
294
+ assertSelectedTarget(current, message);
295
+ }
296
+ }
297
+ else if (!isWake(entry) && !acceptedInput(entry))
298
+ assertSelectedTarget(current, entry);
299
+ for (const previous of entries(tx, current)) {
300
+ if (previous.id === entry.id)
301
+ break;
302
+ if (isWake(previous) || previous.inbox?.ack !== undefined)
303
+ continue;
304
+ if (current.scope === "task"
305
+ && isTaskRecordRetired(tx.listEvents(current.taskId), "message", previous.id))
306
+ continue;
307
+ if (current.scope === "global" && previous.notDelivered !== undefined)
308
+ continue;
309
+ if (previous.inbox !== undefined && sameSession(previous.inbox.claim.actor, current))
310
+ continue;
311
+ throw new Error(`Earlier inbox entry ${previous.id} must be handled first; input order is preserved.`);
312
+ }
313
+ entry = { ...entry, inbox: { claim: { requestId, actor: current, at: now.toISOString() } } };
314
+ }
315
+ else {
316
+ if (!entry.inbox)
317
+ throw new Error("Claim the exact entry before handling and acknowledgement.");
318
+ // Ack settles already-owned handling; it never submits or retargets
319
+ // the original. The current rightful successor may settle a claim
320
+ // acquired by the exact selected Session, retaining both identities.
321
+ const claimant = entry.inbox.claim.actor;
322
+ if (isWake(entry) && !acceptedInput(entry)) {
323
+ const messageIds = new Set(entry.refs?.filter(ref => ref.type === "message").map(ref => ref.id));
324
+ for (const message of tx.listMessages(current.taskId)) {
325
+ if (messageIds.has(message.id))
326
+ assertSelectedTarget(claimant, message);
327
+ }
328
+ }
329
+ else if (!isWake(entry) && !acceptedInput(entry))
330
+ assertSelectedTarget(claimant, entry);
331
+ if (!isWake(entry) && current.scope === "task"
332
+ && !(current.roleName === "leader" && isLeaderSteer(entry))) {
333
+ const invalid = messageAssignmentBlocker(tx, entry);
334
+ if (invalid !== undefined)
335
+ throw new Error(`Assignment no longer authorizes this inbox disposition: ${invalid}.`);
336
+ }
337
+ // Current rightful successor may settle a prior Session's unfinished
338
+ // claim explicitly; this never replays input or discards prior evidence.
339
+ if (entry.inbox.ack) {
340
+ if (entry.inbox.ack.requestId !== requestId || entry.inbox.ack.reason !== reason
341
+ || !sameSession(entry.inbox.ack.actor, current))
342
+ throw new Error("Conflicting acknowledgement; original disposition retained.");
343
+ return receipt(entry);
344
+ }
345
+ const inbox = { ...entry.inbox,
346
+ ack: { requestId, actor: current, at: now.toISOString(), reason: reason } };
347
+ if (isWake(entry)) {
348
+ const mailbox = tx.getWorkMailbox(target(current));
349
+ // stop/start removes operational mailboxes, not durable claim
350
+ // ownership. Settle that receipt without touching a later batch.
351
+ if (mailbox?.processing?.owner === `agent-inbox:${entry.id}`
352
+ && mailbox.processing.batchId === `inbox:${entry.id}`) {
353
+ tx.saveWorkMailbox(completeProcessing(mailbox, mailbox.processing.batchId));
354
+ }
355
+ }
356
+ entry = { ...entry, inbox };
357
+ }
358
+ }
359
+ save(tx, current, entry);
360
+ return receipt(entry);
361
+ });
362
+ if (ids.length === 1)
363
+ return apply(ids[0]);
364
+ return { results: ids.map(id => {
365
+ try {
366
+ return { id, ok: true, result: apply(id) };
367
+ }
368
+ catch (error) {
369
+ return { id, ok: false, reason: error instanceof Error ? error.message : String(error) };
370
+ }
371
+ }) };
372
+ }
@@ -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) {
@@ -1,12 +1,13 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
+ import { taskMessageInboxReceipt } from "../message/taskInboxOwnership.js";
3
4
  import { agentExecutionComponentLabel } from "../agent/executionComponents.js";
4
5
  import { assertLeaderArchiveAdmission } from "../task/leaderArchiveAuthority.js";
5
6
  import { createRun, runExecutionObservation, runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
6
7
  import { isGitArtifactRefString, parseGitArtifactRef } from "../artifacts/gitArtifactRef.js";
7
8
  import { contextContentDigest, contextSnapshotRef } from "../context/contextSnapshot.js";
8
9
  import { sourceRunContextValue } from "../context/sourceRunContext.js";
9
- import { buildRunContextDelta, buildRunContextPack, contextSnapshotDeltaRefIds, expandRunContextRef, freezeReviewStageContextSnapshot, freezeRunContextSnapshot, freezeWorkItemExecutionAssignmentContextSnapshot, freezeCandidateContextSnapshot, readRunContextSnapshot, synthesisSourceRunIds } from "../context/runContextPack.js";
10
+ import { buildRunContextDelta, buildRunContextPack, runContextEntry, runContextPointerPage, contextSnapshotDeltaRefIds, expandRunContextRef, freezeReviewStageContextSnapshot, freezeRunContextSnapshot, freezeWorkItemExecutionAssignmentContextSnapshot, freezeCandidateContextSnapshot, readRunContextSnapshot, synthesisSourceRunIds } from "../context/runContextPack.js";
10
11
  import { createRunInput } from "../context/runInputContract.js";
11
12
  import { parseTaskCatalogOptions, readTaskCatalog, renderTaskCatalog } from "../context/taskCatalog.js";
12
13
  import { assertContextRecordReadable, readTaskContextResource } from "../context/taskContext.js";
@@ -1631,9 +1632,25 @@ function taskMessageCommand(args, store, options) {
1631
1632
  const message = store.transaction((tx) => {
1632
1633
  assertTaskDeliveryAuthority(tx, options.environment, ref.taskId);
1633
1634
  const current = tx.listMessages(ref.taskId).find((entry) => entry.id === ref.localId);
1634
- if (current?.recipient?.ownerRunId === undefined || current.continuation?.runId !== undefined) {
1635
+ if (current?.inbox?.ack !== undefined || current?.recipient?.ownerRunId === undefined || current.continuation?.runId !== undefined) {
1635
1636
  throw usageError("Only an unassigned pending Message can be explicitly handed off.");
1636
1637
  }
1638
+ if (current.inbox !== undefined) {
1639
+ const actor = current.inbox.claim.actor;
1640
+ for (const runId of new Set([actor.runId, current.recipient.ownerRunId])) {
1641
+ const handler = runId === undefined ? null : tx.getRun(ref.taskId, runId);
1642
+ const observation = handler === null ? undefined : runExecutionObservation(handler, tx.getTaskRoleSessionSet(ref.taskId, handler.roleName)?.providerBinding, tx.listEvents(ref.taskId));
1643
+ if (handler === null || handler.status === "active"
1644
+ || handler.result?.failureReason === "delivery-unknown"
1645
+ || handler.result?.provider === undefined
1646
+ || runId === actor.runId && handler.result.provider.conversationId !== actor.nativeSessionId
1647
+ || !["completed", "failed", "cancelled"].includes(observation?.delivery ?? "")) {
1648
+ throw usageError("The original and current inbox handlers must have exact stopped native execution before explicit handoff.");
1649
+ }
1650
+ }
1651
+ // Preserve the original claim: the new authorized Assignment must
1652
+ // inspect prior effects and ack explicitly, never auto-replay the input.
1653
+ }
1637
1654
  const recipient = resolveMessageRecipient(tx, ref.taskId, requiredOption(parsed.options, "--to"), {
1638
1655
  ...(current.recipient.workItemId === undefined ? {} : { workItemId: current.recipient.workItemId }),
1639
1656
  ...(current.recipient.reviewRoundId === undefined ? {} : { reviewRoundId: current.recipient.reviewRoundId })
@@ -1853,15 +1870,23 @@ function steerTaskInput(taskId, input, store, options) {
1853
1870
  // and idempotent, so the Host's later settlement fold promotes it to a proven
1854
1871
  // terminal and never rewinds it. An idempotent replay above already returned
1855
1872
  // the saved Message, so this records the attempt exactly once.
1856
- store.transaction((tx) => {
1873
+ const admitted = store.transaction((tx) => {
1857
1874
  const saved = tx.listMessages(result.task.id).find((entry) => entry.id === result.message.id);
1858
1875
  const requestId = saved?.inputControl?.requestId;
1859
- if (saved === undefined || requestId === undefined)
1860
- return;
1876
+ // The recipient may claim between Message persistence and control
1877
+ // admission. A Leader window owns its referenced originals even after ack;
1878
+ // never re-submit them through a delayed steer.
1879
+ if (saved === undefined || requestId === undefined || taskMessageInboxReceipt(tx, saved) !== undefined)
1880
+ return false;
1861
1881
  tx.updateMessage(result.task.id, recordTaskMessageControlOutcome(saved, {
1862
1882
  requestId, receiptId, outcome: "pending", observedAt: clock(options)
1863
1883
  }));
1884
+ return true;
1864
1885
  });
1886
+ if (!admitted) {
1887
+ return output(`Steer message ${result.message.id} saved but not delivered (TARGET_CHANGED: inbox handling owns the input).\n`, { taskId: result.task.id, message: messageReceipt(result.message),
1888
+ steer: { state: "not-steered", code: "TARGET_CHANGED", detail: "Inbox handling already owns the original input." } });
1889
+ }
1865
1890
  return {
1866
1891
  kind: "input-steer",
1867
1892
  taskId: result.task.id,
@@ -2407,6 +2432,9 @@ function registerInterruptThen(store, taskId, roleName, thenMessageRef, target,
2407
2432
  }
2408
2433
  // A Message that already delivered, or was already ruled undeliverable, is not
2409
2434
  // a fresh input to reuse; reusing it would replay or resurrect a settled fact.
2435
+ if (taskMessageInboxReceipt(store, message) !== undefined) {
2436
+ return { code: "TARGET_CHANGED", detail: `Message ${thenMessageRef} is owned by active inbox handling.` };
2437
+ }
2410
2438
  if (message.continuation?.runId !== undefined) {
2411
2439
  return { code: "TARGET_CHANGED", detail: `Message ${thenMessageRef} was already delivered by ${message.continuation.runId}.` };
2412
2440
  }
@@ -4749,14 +4777,18 @@ function runContextCommand(args, store, options) {
4749
4777
  const data = boundedDocument(delta, `run:${taskId}/${runId}/delta/${parsed.values.get("--after")}`, parsed.cursor);
4750
4778
  return output(`${JSON.stringify(data)}\n`, { contextDelta: data });
4751
4779
  }
4752
- const parsed = readOptions(rest, ["--cursor"]);
4780
+ const parsed = readOptions(rest, ["--cursor", "--view"]);
4753
4781
  if (first === undefined || parsed.positionals.length !== 0) {
4754
4782
  throw usageError("Task run context usage: yui task run context <task>/<run>.");
4755
4783
  }
4756
4784
  const { taskId, runId } = parseRunContextReference(first);
4757
4785
  authorizeRunContext(store, taskId, runId, options.environment);
4758
4786
  const pack = store.transaction((tx) => buildRunContextPack(tx, taskId, runId));
4759
- const data = boundedDocument(pack, `run:${taskId}/${runId}/context`, parsed.cursor);
4787
+ const view = parsed.values.get("--view") ?? "current";
4788
+ if (!["current", "full", "refs", "input"].includes(view))
4789
+ throw usageError("Run Context view must be current, refs, input or full.");
4790
+ const data = view === "refs" ? runContextPointerPage(pack, parsed)
4791
+ : boundedDocument(view === "full" ? pack : view === "input" ? pack.input : runContextEntry(pack), `run:${taskId}/${runId}/context/${view}`, parsed.cursor);
4760
4792
  return output(`${JSON.stringify(data)}\n`, { context: data });
4761
4793
  }
4762
4794
  function parseRunContextReference(value) {
@@ -18,6 +18,7 @@ export function taskRoleHostDiagnostic(host) {
18
18
  const delivery = snapshot.eventDelivery;
19
19
  return [
20
20
  `Provider Host=${snapshot.state}`,
21
+ `terminal reconciliation=${snapshot.controlCapabilities?.includes("reconcile-turn") ? "supported" : "unreported/unsupported"}`,
21
22
  delivery === undefined ? "event delivery=unreported" :
22
23
  `pending facts=${delivery.pending}; native terminals awaiting acknowledgement=${delivery.pendingTerminals}`,
23
24
  delivery?.failure === undefined ? undefined :
@@ -35,6 +36,7 @@ export function withTaskRoleHostObservation(status, host) {
35
36
  return { ...status, host: { detail: "Host does not match the recorded native Session." } };
36
37
  }
37
38
  const reportingNeedsAttention = snapshot !== undefined && (snapshot.state === "failed" || snapshot.state === "exited"
39
+ || snapshot.eventDelivery === undefined
38
40
  || snapshot.eventDelivery?.failure !== undefined
39
41
  || (snapshot.eventDelivery?.pendingTerminals ?? 0) > 0);
40
42
  return {
@@ -0,0 +1,48 @@
1
+ import { accessSync, constants, lstatSync, readFileSync, realpathSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export const BUILTIN_YUI_SKILLS = new Set([
5
+ "yui-runtime", "yui-operator", "yui-leader", "yui-worker", "yui-reviewer"
6
+ ]);
7
+ export function currentSkillPath(home, id) {
8
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/u.test(id))
9
+ throw new Error(`Invalid configured Skill id: ${id}.`);
10
+ if (BUILTIN_YUI_SKILLS.has(id))
11
+ return fileURLToPath(new URL(`../../skills/${id}`, import.meta.url));
12
+ if (home === undefined)
13
+ throw new Error("YUI_HOME is required to load configured Role skills.");
14
+ return resolve(home, "skills", id);
15
+ }
16
+ /** Per-turn metadata only: never read the body, traverse resources, snapshot a
17
+ * package, or claim that the model still has instructions in its context. */
18
+ export function checkCurrentSkills(home, ids) {
19
+ return [...new Set(ids)].map(id => {
20
+ const path = currentSkillPath(home, id);
21
+ const versionPath = join(path, "VERSION");
22
+ try {
23
+ for (const file of [join(path, "SKILL.md"), versionPath]) {
24
+ if (realpathSync(file) !== resolve(file) || !lstatSync(file).isFile()) {
25
+ throw new Error(`Skill files must be regular files without symlinks: ${file}.`);
26
+ }
27
+ accessSync(file, constants.R_OK);
28
+ }
29
+ if (lstatSync(versionPath).size > 128)
30
+ throw new Error("VERSION exceeds 128 bytes.");
31
+ const version = readFileSync(versionPath, "utf8").trim();
32
+ if (!/^[1-9][0-9]{0,8}$/u.test(version))
33
+ throw new Error("VERSION must contain a positive internal integer.");
34
+ return { id, path, version, versionPath };
35
+ }
36
+ catch (error) {
37
+ throw new Error(`Current Skill ${id} is missing, unreadable or invalid at ${path}. `
38
+ + `${error instanceof Error ? error.message : String(error)} `
39
+ + "Restore the current installation/configured Skill (including VERSION) in this Home, then check again; "
40
+ + "do not replace the Session or replay input to repair Skill loading.", { cause: error });
41
+ }
42
+ });
43
+ }
44
+ export const SKILL_CHECK_COMMAND = "yui session skills --json";
45
+ export const SKILL_REUSE_RULE = "At the start of every turn, check current Skill versions and availability. "
46
+ + "Read full SKILL.md and necessary references on first entry, version change, recovery, "
47
+ + "or missing/compacted/uncertain context; reuse only unchanged full instructions available now. "
48
+ + "A version check is not a loading receipt or authority.";
@@ -3,29 +3,18 @@ import { readFileSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { snapshotSkillPackage, verifySkillPackage } from "./skillPackage.js";
6
+ import { BUILTIN_YUI_SKILLS, checkCurrentSkills } from "./currentSkills.js";
7
+ export { BUILTIN_YUI_SKILLS } from "./currentSkills.js";
6
8
  import { SYSTEM_LEADER_ROLE, SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
7
- export const BUILTIN_YUI_SKILLS = new Set([
8
- "yui-runtime",
9
- "yui-operator",
10
- "yui-leader",
11
- "yui-worker",
12
- "yui-reviewer"
13
- ]);
14
9
  /**
15
10
  * Compiles stable Role policy for an Agent-native instruction channel.
16
11
  * Dynamic wakeups and AgentRun assignments deliberately remain outside this value.
17
12
  */
18
13
  export function compileRoleSessionContext(yuiHome, role, owner, options = {}) {
19
14
  const kind = roleSessionKind(role, owner, options.purpose ?? "execution");
20
- const builtInSkillId = kind === "global" ? undefined : `yui-${kind}`;
21
- const skillIds = unique([
22
- "yui-runtime",
23
- ...(builtInSkillId === undefined ? [] : [builtInSkillId]),
24
- ...(role.skills ?? [])
25
- ]);
26
- const skills = options.skillPackages === undefined
27
- ? loadYuiSkillContexts(yuiHome, skillIds)
28
- : options.skillPackages.map(loadSkillPackageContext);
15
+ const skills = checkCurrentSkills(yuiHome, roleSkillIds(role, kind)).map(skill => ({
16
+ ...skill, content: readFileSync(join(skill.path, "SKILL.md"), "utf8").trim()
17
+ }));
29
18
  return {
30
19
  developerInstructions: renderDeveloperInstructions(kind, role, owner),
31
20
  skills,
@@ -34,6 +23,14 @@ export function compileRoleSessionContext(yuiHome, role, owner, options = {}) {
34
23
  : { managedContextFile: roleContextFile(yuiHome, role, owner, kind) })
35
24
  };
36
25
  }
26
+ export function roleSkillIds(role, kind) {
27
+ const builtInSkillId = kind === "global" ? undefined : `yui-${kind}`;
28
+ return unique([
29
+ "yui-runtime",
30
+ ...(builtInSkillId === undefined ? [] : [builtInSkillId]),
31
+ ...(role.skills ?? [])
32
+ ]);
33
+ }
37
34
  function renderDeveloperInstructions(kind, role, owner) {
38
35
  const core = renderRoleCore(kind, role, owner);
39
36
  const profile = [