@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.
- package/README.md +23 -0
- package/dist/cli/commandCatalog.js +31 -4
- package/dist/commands/globalRoleCommands.js +22 -2
- package/dist/commands/inboxCommands.js +352 -0
- package/dist/commands/roleSkillValidation.js +2 -0
- package/dist/commands/taskCommands.js +40 -7
- package/dist/commands/taskContextCommand.js +3 -2
- package/dist/context/currentSkills.js +48 -0
- package/dist/context/roleSessionContext.js +13 -16
- package/dist/context/runContextPack.js +2 -0
- package/dist/context/runInputContract.js +1 -1
- package/dist/context/sessionBootstrapManifest.js +27 -4
- package/dist/context/skillCatalog.js +2 -3
- package/dist/context/taskContext.js +9 -4
- package/dist/controlPlaneCli.js +18 -1
- package/dist/controller/controller.js +3 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +19 -4
- package/dist/controller/globalInputDelivery.js +73 -15
- package/dist/controller/globalRuntimeAttention.js +1 -1
- package/dist/controller/operatorNotification.js +6 -1
- package/dist/controller/providerRetryDelivery.js +3 -5
- package/dist/controller/runtime.js +2 -2
- package/dist/doctor/doctor.js +25 -2
- package/dist/executor/effectiveLaunch.js +2 -2
- package/dist/executor/fileRoleLaunchPlanner.js +4 -6
- package/dist/interaction/notificationPresentation.js +2 -2
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/kernel/builtinCapabilities.js +2 -1
- package/dist/message/globalInputBacklog.js +69 -0
- package/dist/message/globalMessageQuery.js +1 -0
- package/dist/message/inboxReceipt.js +32 -0
- package/dist/message/message.js +6 -1
- package/dist/message/messageContinuation.js +34 -13
- package/dist/message/taskInboxOwnership.js +21 -0
- package/dist/nativeAgent/session/backends.js +1 -1
- package/dist/observability/runtimeIdentity.js +1 -1
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/runtime/agentHost.js +105 -16
- package/dist/runtime/builtinAgentErrorMappers.js +9 -5
- package/dist/runtime/providerRetry.js +38 -0
- package/dist/runtime/taskSessions.js +240 -0
- package/dist/scheduler/operatorEvent.js +49 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +5 -4
- package/dist/scheduler/taskExecutionProjection.js +78 -6
- package/dist/scheduler/taskWake.js +5 -0
- package/dist/sqlite/driver.js +108 -0
- package/dist/storage/sqliteSchema.js +11 -1
- package/dist/storage/sqliteStore.js +60 -1
- package/dist/storage/storageSchema.js +1 -1
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +1 -1
- package/dist/task/completionReadiness.js +11 -4
- package/dist/telemetry/sqliteTelemetryStore.js +1 -1
- package/dist/web/assets/assetManifest.js +2 -0
- package/dist/web/assets/client/app/taskView.js +1 -0
- package/dist/web/assets/client/domain/records.js +70 -2
- package/dist/web/assets/client/domain/runs.js +3 -2
- package/dist/web/assets/client/domain/vocab.js +1 -1
- package/dist/web/assets/client/i18n/en.js +17 -1
- package/dist/web/assets/client/i18n/zh-CN.js +17 -1
- package/dist/web/assets/client/lib/api.js +4 -1
- package/dist/web/assets/client/lib/theme.js +31 -10
- package/dist/web/assets/client/views/task/actions.js +38 -21
- package/dist/web/assets/client/views/task/observation.js +12 -3
- package/dist/web/assets/client/views/task/overview.js +7 -40
- package/dist/web/assets/client/views/task/runtime.js +71 -34
- package/dist/web/assets/shared/icons.js +2 -0
- package/dist/web/assets/shell/index.js +1 -0
- package/dist/web/assets/shell/settings.js +1 -0
- package/dist/web/assets/styles/views/task.js +14 -0
- package/dist/web/webServer.js +12 -0
- package/dist/web/webSessions.js +2 -165
- package/dist/web/webSnapshot.js +9 -5
- package/dist/web/webTaskActivity.js +54 -0
- package/dist/web/webTaskSurface.js +1 -0
- package/docs/managed-turn-and-session-runtime.md +81 -5
- package/docs/managed-turn-and-session-runtime.zh-CN.md +51 -3
- package/docs/observability/README.md +25 -1
- package/docs/observability/README.zh-CN.md +18 -1
- package/docs/provider-retry.md +11 -0
- package/docs/release-workflow.md +74 -2
- package/docs/release-workflow.zh-CN.md +56 -1
- package/docs/storage-baseline.md +17 -7
- package/docs/storage-baseline.zh-CN.md +11 -5
- package/docs/task-delivery.md +17 -8
- package/docs/task-delivery.zh-CN.md +11 -5
- package/docs/testing/verification-levels.md +22 -4
- package/docs/testing/verification-levels.zh-CN.md +8 -1
- package/i18n/README.zh-CN.md +19 -0
- package/package.json +5 -2
- package/skills/yui-leader/SKILL.md +28 -11
- package/skills/yui-leader/VERSION +1 -0
- package/skills/yui-operator/SKILL.md +10 -2
- package/skills/yui-operator/VERSION +1 -0
- package/skills/yui-operator/references/task-delivery.md +2 -0
- package/skills/yui-reviewer/SKILL.md +8 -1
- package/skills/yui-reviewer/VERSION +1 -0
- package/skills/yui-runtime/SKILL.md +94 -11
- package/skills/yui-runtime/VERSION +1 -0
- package/skills/yui-runtime/references/publication.md +16 -5
- package/skills/yui-runtime/references/recovery.md +9 -0
- package/skills/yui-worker/SKILL.md +8 -1
- 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: "
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|