@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
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { sendAgentHostRunControl, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
|
|
1
|
+
import { sendAgentHostRunControl, reconcileAgentHostTurn, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
|
|
2
2
|
import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
|
|
3
3
|
import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
4
4
|
import { markGlobalRoleMessageNotDelivered } from "../message/message.js";
|
|
5
5
|
import { providerRetryPending } from "../runtime/providerRetry.js";
|
|
6
6
|
import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
|
|
7
7
|
import { globalMessagePresentation } from "../interaction/notificationPresentation.js";
|
|
8
|
+
import { recordGlobalRuntimeAttention } from "./globalRuntimeAttention.js";
|
|
8
9
|
/** Messages are the only queue. Host's atomic begin arbitrates native writers. */
|
|
9
|
-
export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
10
|
+
export async function deliverGlobalInputs(home, store, ensure, onError, options = {}) {
|
|
10
11
|
for (const role of store.listGlobalRoles()) {
|
|
11
12
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
if (store.queryGlobalRoleMessages(role.name, { handling: true, limit: 0 }).count > 0)
|
|
14
|
+
continue;
|
|
15
|
+
let pending = store.queryGlobalRoleMessages(role.name, { pending: true, limit: 64 }).messages;
|
|
14
16
|
if (pending.length === 0 || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
|
|
15
17
|
continue;
|
|
16
18
|
let sessions = store.getGlobalRoleSessionSet(role.name);
|
|
@@ -20,15 +22,68 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
|
20
22
|
continue;
|
|
21
23
|
if (sessions?.providerBinding?.retry !== undefined && !providerRetryPending(sessions.providerBinding)) {
|
|
22
24
|
store.transaction(tx => settleGlobalRetryInput(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, new Date()));
|
|
23
|
-
pending = store.
|
|
24
|
-
&& (message.inputControl?.action === "queue" || message.interruptThen !== undefined));
|
|
25
|
+
pending = store.queryGlobalRoleMessages(role.name, { pending: true, limit: 64 }).messages;
|
|
25
26
|
if (pending.length === 0)
|
|
26
27
|
continue;
|
|
27
28
|
}
|
|
28
29
|
if (providerRetryPending(sessions?.providerBinding))
|
|
29
30
|
continue;
|
|
30
|
-
|
|
31
|
-
const
|
|
31
|
+
let current = sessions?.providerBinding?.run;
|
|
32
|
+
const bindingBefore = sessions?.providerBinding;
|
|
33
|
+
if (options.reconcile && current != null
|
|
34
|
+
&& ["submitting", "accepted", "delivery-unknown"].includes(current.status)
|
|
35
|
+
&& bindingBefore?.authority.owner === "controller" && bindingBefore.authority.holderId !== undefined) {
|
|
36
|
+
const ownedTurn = current;
|
|
37
|
+
// Only ask the original Host to republish evidence it already holds.
|
|
38
|
+
// Busy, unknown, unavailable and older Hosts never authorize replay,
|
|
39
|
+
// Session creation, native reads through another client, or timeout settlement.
|
|
40
|
+
let observed;
|
|
41
|
+
try {
|
|
42
|
+
observed = await reconcileAgentHostTurn({
|
|
43
|
+
home, scope: "global", roleName: role.name,
|
|
44
|
+
control: { protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "reconcile-turn",
|
|
45
|
+
nativeSessionId: currentProviderConversation(bindingBefore).conversationId,
|
|
46
|
+
attemptId: current.attemptId, nativeTurnId: current.nativeTurnId,
|
|
47
|
+
authority: { epoch: bindingBefore.authority.epoch, owner: "controller",
|
|
48
|
+
holderId: bindingBefore.authority.holderId } }
|
|
49
|
+
});
|
|
50
|
+
if (observed.outcome === "rejected" || ["idle", "failed", "exited"].includes(observed.snapshot.state)
|
|
51
|
+
&& observed.reconciliation !== "terminal-republished") {
|
|
52
|
+
throw new Error(observed.failure?.detail ?? observed.snapshot.detail ?? "Host cannot reconcile this exact terminal.");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
store.transaction(tx => recordGlobalRuntimeAttention(tx, role.name, `terminal-reconcile:${currentProviderConversation(bindingBefore).conversationId}:${ownedTurn.attemptId}`, { attemptId: ownedTurn.attemptId, nativeTurnId: ownedTurn.nativeTurnId,
|
|
57
|
+
nativeSessionId: currentProviderConversation(bindingBefore).conversationId,
|
|
58
|
+
reason: "host-terminal-unobservable",
|
|
59
|
+
cause: observed === undefined ? "host-control-unavailable"
|
|
60
|
+
: observed.outcome === "rejected" ? "host-control-rejected" : "exact-terminal-not-retained",
|
|
61
|
+
hostState: observed?.snapshot.state ?? null,
|
|
62
|
+
endpointImplementation: observed?.snapshot.endpointImplementation ?? null,
|
|
63
|
+
eventDelivery: observed?.snapshot.eventDelivery ?? null,
|
|
64
|
+
// Endpoint generation is not a loaded Host/package version.
|
|
65
|
+
inputKind: ownedTurn.attemptId.startsWith("direct:") ? "direct" : "managed",
|
|
66
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
67
|
+
recovery: "Inspect the original Host, Session, input and raw Inbox. Controller restart preserves live Hosts; it does not upgrade their loaded observation code. Restore the supported observation path, or explicitly stop/replace with exact native quiescence evidence. Missing eventDelivery means unreported, not healthy. Do not resend unknown input.",
|
|
68
|
+
confirmed: ownedTurn.status, unknown: "Native terminal and remaining writer ownership." }, options.now ?? new Date()));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
sessions = store.getGlobalRoleSessionSet(role.name);
|
|
72
|
+
current = sessions?.providerBinding?.run;
|
|
73
|
+
if (sessions?.sessions[sessions.activeAgentId]?.status !== "active"
|
|
74
|
+
|| providerRetryPending(sessions?.providerBinding)
|
|
75
|
+
|| hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
|
|
76
|
+
continue;
|
|
77
|
+
pending = store.queryGlobalRoleMessages(role.name, { pending: true, limit: 64 }).messages;
|
|
78
|
+
if (pending.length === 0)
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// Interrupt-then reserves the next legal slot even behind a large queue.
|
|
82
|
+
const reservedPage = store.queryGlobalRoleMessages(role.name, {
|
|
83
|
+
pending: true, interruptThen: true, limit: 64
|
|
84
|
+
});
|
|
85
|
+
const reserved = reservedPage.messages;
|
|
86
|
+
const obsolete = reserved.filter(message => {
|
|
32
87
|
const claim = message.interruptThen;
|
|
33
88
|
return claim !== undefined && (sessions?.activeAgentId !== claim.targetAgentId
|
|
34
89
|
|| sessions.sessions[claim.targetAgentId]?.nativeSessionId !== claim.targetNativeSessionId
|
|
@@ -37,15 +92,18 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
|
37
92
|
});
|
|
38
93
|
for (const message of obsolete)
|
|
39
94
|
store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(message, "native-session-or-authority-changed", new Date()));
|
|
40
|
-
|
|
95
|
+
const obsoleteIds = new Set(obsolete.map(message => message.id));
|
|
96
|
+
pending = pending.filter(message => !obsoleteIds.has(message.id));
|
|
41
97
|
if (pending.length === 0)
|
|
42
98
|
continue;
|
|
99
|
+
if (reservedPage.count > reserved.length && obsolete.length === reserved.length)
|
|
100
|
+
continue;
|
|
43
101
|
if (current != null && ["submitting", "accepted", "delivery-unknown"].includes(current.status))
|
|
44
102
|
continue;
|
|
45
103
|
const matchesTarget = (message) => message.deliveryTarget === undefined
|
|
46
104
|
|| sessions?.activeAgentId === message.deliveryTarget.agentId
|
|
47
105
|
&& sessions.sessions[message.deliveryTarget.agentId]?.nativeSessionId === message.deliveryTarget.nativeSessionId;
|
|
48
|
-
const claims =
|
|
106
|
+
const claims = reserved.filter(message => !obsolete.includes(message));
|
|
49
107
|
let next;
|
|
50
108
|
if (claims.length > 0) {
|
|
51
109
|
next = claims.find(message => {
|
|
@@ -76,7 +134,7 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
|
76
134
|
return false;
|
|
77
135
|
});
|
|
78
136
|
}
|
|
79
|
-
if (next === undefined)
|
|
137
|
+
if (next === undefined || next.inbox !== undefined)
|
|
80
138
|
continue;
|
|
81
139
|
const attemptId = `global-input:${role.name}/${next.id}`;
|
|
82
140
|
if (current?.attemptId === attemptId)
|
|
@@ -96,8 +154,8 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
|
96
154
|
if (binding == null || binding.authority.owner !== "controller")
|
|
97
155
|
continue;
|
|
98
156
|
const nativeSessionId = currentProviderConversation(binding).conversationId;
|
|
99
|
-
const persisted = store.
|
|
100
|
-
if (persisted === undefined || JSON.stringify(persisted) !== JSON.stringify(next))
|
|
157
|
+
const persisted = store.queryGlobalRoleMessages(role.name, { id: next.id, limit: 1 }).messages[0];
|
|
158
|
+
if (persisted === undefined || persisted.inbox !== undefined || JSON.stringify(persisted) !== JSON.stringify(next))
|
|
101
159
|
continue;
|
|
102
160
|
const claim = next.interruptThen;
|
|
103
161
|
if (!matchesTarget(next) || claim !== undefined
|
|
@@ -125,8 +183,8 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
|
125
183
|
});
|
|
126
184
|
if (result.outcome === "rejected") {
|
|
127
185
|
store.transaction(tx => {
|
|
128
|
-
const latest = tx.
|
|
129
|
-
if (latest === undefined || latest.delivery !== undefined || latest.notDelivered !== undefined)
|
|
186
|
+
const latest = tx.queryGlobalRoleMessages(role.name, { id: next.id, limit: 1 }).messages[0];
|
|
187
|
+
if (latest === undefined || latest.inbox !== undefined || latest.delivery !== undefined || latest.notDelivered !== undefined)
|
|
130
188
|
return;
|
|
131
189
|
const turn = tx.getGlobalRoleSessionSet(role.name)?.providerBinding?.run;
|
|
132
190
|
if (providerRetryPending(tx.getGlobalRoleSessionSet(role.name)?.providerBinding))
|
|
@@ -19,7 +19,7 @@ export function recordGlobalRuntimeAttention(store, roleName, identity, facts, n
|
|
|
19
19
|
// diagnostic readable; never create a Role or launch a substitute Agent.
|
|
20
20
|
const recipient = store.getGlobalRole("operator") === null ? roleName : "operator";
|
|
21
21
|
const requestId = `runtime-attention:${createHash("sha256").update(`${roleName}\0${identity}`).digest("hex")}`;
|
|
22
|
-
if (store.
|
|
22
|
+
if (store.queryGlobalRoleMessages(recipient, { requestId, limit: 1 }).messages.length > 0)
|
|
23
23
|
return;
|
|
24
24
|
const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), recipient, [
|
|
25
25
|
`Global Role ${roleName} needs attention. No recovery or new authority is implied.`,
|
|
@@ -8,7 +8,7 @@ import { createGlobalRoleMessage } from "../message/message.js";
|
|
|
8
8
|
export function queueOperatorNotification(store, input, now = new Date()) {
|
|
9
9
|
return store.transaction(tx => {
|
|
10
10
|
const requestId = `operator-notice:${encodeURIComponent(input.receiptId)}`;
|
|
11
|
-
const previous = tx.
|
|
11
|
+
const previous = tx.queryGlobalRoleMessages("operator", { requestId, limit: 1 }).messages[0];
|
|
12
12
|
const mailbox = tx.getWorkMailbox({ kind: "operator" });
|
|
13
13
|
const claimed = mailbox?.processing?.batchId === input.batchId;
|
|
14
14
|
if (previous !== undefined) {
|
|
@@ -18,6 +18,11 @@ export function queueOperatorNotification(store, input, now = new Date()) {
|
|
|
18
18
|
}
|
|
19
19
|
if (!claimed)
|
|
20
20
|
throw new Error(`Operator notification claim changed: ${input.batchId}`);
|
|
21
|
+
// At most one unsent engineering notice. New facts coalesce in the existing
|
|
22
|
+
// mailbox while busy, without rewriting originals or overtaking user input.
|
|
23
|
+
if (tx.queryGlobalRoleMessages("operator", { pending: true, operatorNotice: true, limit: 1 }).count > 0) {
|
|
24
|
+
return { status: "deferred" };
|
|
25
|
+
}
|
|
21
26
|
const sessions = tx.getGlobalRoleSessionSet("operator");
|
|
22
27
|
const session = sessions?.sessions[sessions.activeAgentId];
|
|
23
28
|
if (session?.status !== "active" || sessions?.providerBinding == null)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks";
|
|
2
2
|
import { runTaskCommand } from "../commands/taskCommands.js";
|
|
3
|
-
import {
|
|
3
|
+
import { routeRoleEvent } from "../scheduler/operatorEvent.js";
|
|
4
4
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
5
5
|
import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
|
|
6
6
|
import { redactAgentErrorText } from "../runtime/agentError.js";
|
|
@@ -57,8 +57,7 @@ export function createProviderRetryHooks(home, store, ports = {}) {
|
|
|
57
57
|
roleName: owner.roleName, chainId: binding.retry.chainId, reason
|
|
58
58
|
}, new Date(at));
|
|
59
59
|
store.saveEvent(owner.taskId, event);
|
|
60
|
-
|
|
61
|
-
: { kind: "role", taskId: owner.taskId, roleName: "leader" }, "provider-retry-needs-attention", new Date(at), [{ type: "event", taskId: owner.taskId, id: event.id }]);
|
|
60
|
+
routeRoleEvent(store, event, owner.roleName, "provider-retry-needs-attention", new Date(at));
|
|
62
61
|
}
|
|
63
62
|
});
|
|
64
63
|
};
|
|
@@ -142,8 +141,7 @@ export function createProviderRetryHooks(home, store, ports = {}) {
|
|
|
142
141
|
roleName: owner.roleName, attemptId: retry.currentAttemptId, nativeSessionId: retry.nativeSessionId, detail
|
|
143
142
|
}, new Date(at));
|
|
144
143
|
store.saveEvent(owner.taskId, event);
|
|
145
|
-
|
|
146
|
-
: { kind: "role", taskId: owner.taskId, roleName: "leader" }, "provider-retry-needs-attention", new Date(at), [{ type: "event", taskId: owner.taskId, id: event.id }]);
|
|
144
|
+
routeRoleEvent(store, event, owner.roleName, "provider-retry-needs-attention", new Date(at));
|
|
147
145
|
}
|
|
148
146
|
});
|
|
149
147
|
}
|
|
@@ -560,9 +560,9 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
560
560
|
}
|
|
561
561
|
}),
|
|
562
562
|
jobSupervisor,
|
|
563
|
-
globalInputDelivery: () => deliverGlobalInputs(home, store, async (roleName) => {
|
|
563
|
+
globalInputDelivery: (deliveryOptions) => deliverGlobalInputs(home, store, async (roleName) => {
|
|
564
564
|
await lifecycleDispatcher("runtime.ensure-role-session", { scope: "global", roleName });
|
|
565
|
-
}, options.onError ?? (() => undefined)),
|
|
565
|
+
}, options.onError ?? (() => undefined), deliveryOptions),
|
|
566
566
|
jobControl,
|
|
567
567
|
capabilityDispatcher: createCapabilityDispatcher(kernel.capabilities),
|
|
568
568
|
...(continuationReconciler === undefined ? {} : { continuationReconciler }),
|
package/dist/doctor/doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { accessSync, constants, existsSync, lstatSync, realpathSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
|
-
import Database from "
|
|
3
|
+
import Database, { selectSqliteDriver } from "../sqlite/driver.js";
|
|
4
4
|
import { inspectPty } from "./ptyProbe.js";
|
|
5
5
|
import { gitVersionSupport } from "../repository/gitCompatibility.js";
|
|
6
6
|
import { resolveExecutable } from "../external/pinnedCommandRunner.js";
|
|
@@ -31,6 +31,7 @@ import { CommandExecutionError } from "../tmux/commandExecutor.js";
|
|
|
31
31
|
* and never block an update.
|
|
32
32
|
*/
|
|
33
33
|
export const STORAGE_DOCTOR_CHECK_NAMES = Object.freeze([
|
|
34
|
+
"sqlite driver",
|
|
34
35
|
"storage schema",
|
|
35
36
|
"storage compatibility",
|
|
36
37
|
"storage state"
|
|
@@ -38,7 +39,7 @@ export const STORAGE_DOCTOR_CHECK_NAMES = Object.freeze([
|
|
|
38
39
|
/**
|
|
39
40
|
* Classify the storage checks into a machine-readable health verdict. This is the
|
|
40
41
|
* canonical, parseable signal the update post-verify consumes (P1-3): storage is
|
|
41
|
-
* healthy only when schema, compatibility, and state are all `ok`. Any
|
|
42
|
+
* healthy only when driver, schema, compatibility, and state are all `ok`. Any
|
|
42
43
|
* `unsupported` (version mismatch / needs-new-version), `invalid` (corrupted /
|
|
43
44
|
* unreadable), or `missing` (uninitialized) storage check is blocking — even
|
|
44
45
|
* and the machine-readable command exits non-zero.
|
|
@@ -119,6 +120,7 @@ export async function getDoctorChecks(env, executor) {
|
|
|
119
120
|
async function inspectDoctor(env, executor) {
|
|
120
121
|
const home = resolveYuiHome(env);
|
|
121
122
|
const homeCheck = checkHome(home);
|
|
123
|
+
const driverCheck = checkSqliteDriver();
|
|
122
124
|
const schema = readSchema(home);
|
|
123
125
|
const compatibility = inspectCompatibility(home, homeCheck, schema);
|
|
124
126
|
const schemaCheck = checkSchema(schema);
|
|
@@ -135,6 +137,7 @@ async function inspectDoctor(env, executor) {
|
|
|
135
137
|
return {
|
|
136
138
|
checks: [
|
|
137
139
|
homeCheck,
|
|
140
|
+
driverCheck,
|
|
138
141
|
schemaCheck,
|
|
139
142
|
compatibility.check,
|
|
140
143
|
storage.check,
|
|
@@ -148,6 +151,26 @@ async function inspectDoctor(env, executor) {
|
|
|
148
151
|
review: review.report
|
|
149
152
|
};
|
|
150
153
|
}
|
|
154
|
+
export function checkSqliteDriver() {
|
|
155
|
+
try {
|
|
156
|
+
const selected = selectSqliteDriver();
|
|
157
|
+
return {
|
|
158
|
+
name: "sqlite driver",
|
|
159
|
+
status: "ok",
|
|
160
|
+
detail: `${selected.name}@${selected.version}; SQLite ${selected.sqliteVersion}; `
|
|
161
|
+
+ (selected.fallbackReason === undefined
|
|
162
|
+
? "no fallback"
|
|
163
|
+
: `fallback: ${selected.fallbackReason}`)
|
|
164
|
+
+ ". Isolated native connection probe; does not validate Home data."
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
return {
|
|
169
|
+
name: "sqlite driver", status: "invalid",
|
|
170
|
+
detail: `Native driver probe failed (not a Home diagnosis): ${error instanceof Error ? error.message : String(error)}`
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
151
174
|
/** Query the authenticated Controller endpoint without starting it. */
|
|
152
175
|
export async function checkController(home, probe = callController) {
|
|
153
176
|
try {
|
|
@@ -2,7 +2,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
2
2
|
import { validateExecutionEnvironmentSnapshot } from "../resources/projectResource.js";
|
|
3
3
|
import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
|
|
4
4
|
import { resolveAgentAdapter } from "./agentAdapter.js";
|
|
5
|
-
import {
|
|
5
|
+
import { loadYuiSkillContexts, roleSessionKind, roleSkillIds } from "../context/roleSessionContext.js";
|
|
6
6
|
import { validateSkillPackageRef } from "../context/skillPackage.js";
|
|
7
7
|
import { isAgentAdapterId } from "../agent/adapterCatalog.js";
|
|
8
8
|
import { adapterIdForExecutionComponent, isAgentExecutionComponentId, resolveAgentExecutionComponent } from "../agent/executionComponents.js";
|
|
@@ -24,7 +24,7 @@ export function resolveEffectiveLaunch(input) {
|
|
|
24
24
|
const continuing = existing?.status === "active";
|
|
25
25
|
const skillPackages = continuing ? existing.effective.skillPackages
|
|
26
26
|
: home === undefined ? undefined
|
|
27
|
-
:
|
|
27
|
+
: loadYuiSkillContexts(home, roleSkillIds(input.role, roleSessionKind(input.role, "taskId" in input.role ? { scope: "task", taskId: input.role.taskId } : { scope: "global" }, input.purpose))).map(skill => skill.package);
|
|
28
28
|
return snapshotFromConfig({
|
|
29
29
|
executionAuthority: input.executionAuthority ?? (input.purpose === "planning" ? "planning" : "delivery"),
|
|
30
30
|
sourceDesiredRevision: input.role.launchRevision,
|
|
@@ -337,12 +337,10 @@ export class FileRoleLaunchPlanner {
|
|
|
337
337
|
Object.assign(launchEnvironment, runtimeIsolation === undefined
|
|
338
338
|
? {}
|
|
339
339
|
: taskRuntimeIsolationEnvironment(runtimeIsolation));
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
const baseSessionContext = compileRoleSessionContext(this.home, launchRole, owner, { ...sessionPolicy, skillPackages });
|
|
340
|
+
// Frozen packages describe historical materialization, not Session identity
|
|
341
|
+
// or current instruction availability. Never require or reconstruct them
|
|
342
|
+
// when reconnecting a valid old Session.
|
|
343
|
+
const baseSessionContext = compileRoleSessionContext(this.home, launchRole, owner, sessionPolicy);
|
|
346
344
|
const bootstrap = materializeSessionBootstrap({
|
|
347
345
|
yuiHome: this.home,
|
|
348
346
|
role: launchRole,
|
|
@@ -69,7 +69,7 @@ export function taskNotificationPresentation(wake, store) {
|
|
|
69
69
|
...(messages.length > 4 ? [`${messages.length - 4} more messages in the fixed wake.`] : []),
|
|
70
70
|
`Refresh: yui task context ${wake.taskId} --json`,
|
|
71
71
|
`Read inputs/results: yui task wake show ${wake.taskId} ${wake.id} --json`,
|
|
72
|
-
"
|
|
72
|
+
"Check current Skills each turn: yui session skills --json. Apply current yui-leader; read relevant originals in full and advance authorized work."
|
|
73
73
|
].join("\n");
|
|
74
74
|
}
|
|
75
75
|
export function globalMessagePresentation(roleName, messageId, message) {
|
|
@@ -80,6 +80,6 @@ export function globalMessagePresentation(roleName, messageId, message) {
|
|
|
80
80
|
...(summary === undefined ? [] : [`Author summary (message-body): ${JSON.stringify(summary)}`]),
|
|
81
81
|
"Refresh Session Context using the current Manifest's loadCommand.",
|
|
82
82
|
`Read original: yui role message show ${roleName} ${messageId} --json`,
|
|
83
|
-
`Apply ${roleName === "operator" ? "yui-operator" : "yui-runtime"}
|
|
83
|
+
`The Global loadCommand checks current Skill versions each turn. Apply current ${roleName === "operator" ? "yui-operator" : "yui-runtime"} within existing authority.`
|
|
84
84
|
].join("\n");
|
|
85
85
|
}
|
|
@@ -10,7 +10,7 @@ export function createOperatorBatchPresentation(batchId, items) {
|
|
|
10
10
|
receiptId: `operator-batch:${batchId}`,
|
|
11
11
|
text: [
|
|
12
12
|
"[Yui updates]",
|
|
13
|
-
"
|
|
13
|
+
"Check current Skills each turn via the Global Manifest's loadCommand. Apply current yui-operator; inspect these originals and present material changes or decisions.",
|
|
14
14
|
...items.flatMap(renderItem)
|
|
15
15
|
].join("\n")
|
|
16
16
|
};
|
|
@@ -47,7 +47,7 @@ const definitions = [
|
|
|
47
47
|
name: "context.list", summary: "Discover one authorized record family with bounded summaries and exact references.",
|
|
48
48
|
effect: "query", requiredPermissions: ["task:read"], source: "listTaskContext",
|
|
49
49
|
inputSchema: object({ taskId: text, store: text, cursor: text, limit: { type: "integer" },
|
|
50
|
-
status: text, after: text, workItemId: text }, ["taskId", "store"]),
|
|
50
|
+
view: { type: "string", enum: ["current", "all"] }, status: text, after: text, workItemId: text }, ["taskId", "store"]),
|
|
51
51
|
outputSchema: { type: "object", required: ["items", "total", "complete", "nextCursor"] }
|
|
52
52
|
},
|
|
53
53
|
{
|
|
@@ -304,6 +304,7 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
|
|
|
304
304
|
}, callerEnvironment(caller));
|
|
305
305
|
if (name === "context.list")
|
|
306
306
|
return listTaskContext(store, taskId, params.store, {
|
|
307
|
+
view: params.view,
|
|
307
308
|
cursor: params.cursor, limit: params.limit,
|
|
308
309
|
status: params.status, after: params.after,
|
|
309
310
|
workItemId: params.workItemId
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { providerRetryPending } from "../runtime/providerRetry.js";
|
|
2
|
+
import { hasRuntimeCleanupObligation } from "../runtime/lifecycleReservation.js";
|
|
3
|
+
/** Bounded, derived diagnostics only; never an acknowledgement or recovery authority. */
|
|
4
|
+
export function globalInputBacklog(pending, sessions, now = new Date(), mailboxes = {}) {
|
|
5
|
+
const first = pending[0];
|
|
6
|
+
const run = sessions?.providerBinding?.run;
|
|
7
|
+
const session = sessions?.sessions[sessions.activeAgentId];
|
|
8
|
+
const authority = sessions?.providerBinding?.authority;
|
|
9
|
+
const batches = [mailboxes.attention?.pending, mailboxes.attention?.processing?.batch]
|
|
10
|
+
.filter(batch => batch != null);
|
|
11
|
+
const coalescedFactCount = batches.reduce((count, batch) => count + batch.requestCount, 0);
|
|
12
|
+
let reason = "ready";
|
|
13
|
+
let recovery = "The Controller delivers at the next legal opportunity; do not duplicate the input.";
|
|
14
|
+
if (first === undefined && coalescedFactCount === 0)
|
|
15
|
+
reason = "empty";
|
|
16
|
+
else if (hasRuntimeCleanupObligation(mailboxes.lifecycle ?? null)) {
|
|
17
|
+
reason = "lifecycle-cleanup";
|
|
18
|
+
recovery = "Complete the existing exact-owner stop/replacement; queued input cannot revive the Session.";
|
|
19
|
+
}
|
|
20
|
+
else if (session?.status !== "active") {
|
|
21
|
+
reason = session === undefined ? "session-unavailable" : "session-stopped";
|
|
22
|
+
recovery = "Inspect Session lifecycle. Explicit stop/replacement is not undone by background delivery.";
|
|
23
|
+
}
|
|
24
|
+
else if (authority?.owner !== "controller") {
|
|
25
|
+
reason = "external-or-unknown-writer";
|
|
26
|
+
recovery = "Resolve the current writer/authority explicitly; no background takeover is allowed.";
|
|
27
|
+
}
|
|
28
|
+
else if (providerRetryPending(sessions?.providerBinding)) {
|
|
29
|
+
reason = "provider-retry";
|
|
30
|
+
recovery = "The existing bounded Provider retry owns this input; inspect session retry before intervening.";
|
|
31
|
+
}
|
|
32
|
+
else if (run && ["submitting", "accepted", "delivery-unknown"].includes(run.status)) {
|
|
33
|
+
reason = run.status === "accepted" ? "in-flight-or-unobserved-terminal" : run.status;
|
|
34
|
+
recovery = "Age does not prove a stalled Turn. Periodic reconciliation asks only the original Host for exact retained terminal evidence. Inspect recent runtime-attention Messages if the Host is unobservable; never resend unknown input.";
|
|
35
|
+
}
|
|
36
|
+
else if (first?.inbox) {
|
|
37
|
+
reason = "agent-inbox-handling";
|
|
38
|
+
recovery = "Read the exact inbox original and prior handling claim; explicitly acknowledge after processing. No automatic replay or timeout release.";
|
|
39
|
+
}
|
|
40
|
+
else if (first?.control && first.control.outcome !== "rejected") {
|
|
41
|
+
reason = "message-acceptance-unresolved";
|
|
42
|
+
recovery = "Inspect this original Message's exact control receipt; do not replay or bypass uncertain input.";
|
|
43
|
+
}
|
|
44
|
+
else if (first?.interruptThen) {
|
|
45
|
+
reason = "reserved-interrupt-then";
|
|
46
|
+
recovery = "Only the original Session, authority and exact terminal release the reserved continuation.";
|
|
47
|
+
}
|
|
48
|
+
else if (first?.deliveryTarget && (first.deliveryTarget.agentId !== sessions?.activeAgentId
|
|
49
|
+
|| first.deliveryTarget.nativeSessionId !== session.nativeSessionId)) {
|
|
50
|
+
reason = "stale-target";
|
|
51
|
+
recovery = "Unsubmitted stale messages are retained with a nondelivery reason; no cross-Session input copy.";
|
|
52
|
+
}
|
|
53
|
+
const oldestCreatedAt = [...pending.map(message => message.createdAt), ...batches.map(batch => batch.firstQueuedAt)]
|
|
54
|
+
.reduce((oldest, at) => oldest === null || at < oldest ? at : oldest, null);
|
|
55
|
+
return {
|
|
56
|
+
pendingCount: pending.length, coalescedFactCount, oldestCreatedAt,
|
|
57
|
+
oldestAgeMs: oldestCreatedAt === null ? null : Math.max(0, now.getTime() - Date.parse(oldestCreatedAt)),
|
|
58
|
+
reason, recovery,
|
|
59
|
+
target: session === undefined ? null : {
|
|
60
|
+
agentId: session.agentId, nativeSessionId: session.nativeSessionId,
|
|
61
|
+
authority: authority ?? null, attemptId: run?.attemptId ?? null, nativeTurnId: run?.nativeTurnId ?? null
|
|
62
|
+
},
|
|
63
|
+
messages: pending.slice(0, 8).map(message => ({
|
|
64
|
+
id: message.id, createdAt: message.createdAt, deliveryTarget: message.deliveryTarget ?? null,
|
|
65
|
+
control: message.control ?? null, interruptThen: message.interruptThen ?? null
|
|
66
|
+
})),
|
|
67
|
+
omittedMessages: Math.max(0, pending.length - 8)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { requireKnownFields, requireText, requireTimestamp } from "../domain/validation.js";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
export function validateInboxReceipt(receipt) {
|
|
4
|
+
if (receipt === undefined)
|
|
5
|
+
return;
|
|
6
|
+
requireKnownFields(receipt, ["claim", "ack"], "Inbox receipt");
|
|
7
|
+
for (const [kind, entry] of Object.entries(receipt)) {
|
|
8
|
+
requireKnownFields(entry, kind === "claim" ? ["requestId", "actor", "at"]
|
|
9
|
+
: ["requestId", "actor", "at", "reason"], "Inbox action");
|
|
10
|
+
requireText(entry.requestId, "Inbox request id");
|
|
11
|
+
requireTimestamp(entry.at, "Inbox timestamp");
|
|
12
|
+
const actor = entry.actor;
|
|
13
|
+
requireKnownFields(actor, ["scope", "taskId", "roleName", "agentId", "nativeSessionId", "runId"], "Inbox actor");
|
|
14
|
+
if (!["task", "global"].includes(actor.scope)
|
|
15
|
+
|| (actor.scope === "task") !== (actor.taskId !== undefined))
|
|
16
|
+
throw new Error("Inbox actor scope is invalid.");
|
|
17
|
+
for (const value of Object.values(actor))
|
|
18
|
+
requireText(value, "Inbox actor identity");
|
|
19
|
+
if (kind === "ack")
|
|
20
|
+
requireText(entry.reason, "Inbox disposition");
|
|
21
|
+
}
|
|
22
|
+
if (!receipt.claim)
|
|
23
|
+
throw new Error("Inbox receipt requires its original claim.");
|
|
24
|
+
}
|
|
25
|
+
/** A stale automatic writer cannot erase an active claim between its read and
|
|
26
|
+
* Host admission. The original claim and any ack are immutable audit facts. */
|
|
27
|
+
export function assertInboxReceiptPreserved(previous, next) {
|
|
28
|
+
if (previous !== undefined && (!isDeepStrictEqual(previous.claim, next?.claim)
|
|
29
|
+
|| previous.ack !== undefined && !isDeepStrictEqual(previous.ack, next?.ack))) {
|
|
30
|
+
throw new Error("Inbox handling ownership changed; reread the original instead of overwriting its receipt.");
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/message/message.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
2
2
|
import { requireKnownFields } from "../domain/validation.js";
|
|
3
|
+
import { validateInboxReceipt } from "./inboxReceipt.js";
|
|
3
4
|
export const TASK_MESSAGE_KINDS = ["user", "operator", "role-result", "system"];
|
|
4
5
|
/**
|
|
5
6
|
* How a user/operator submission asks its Task to react (task-32 Requirement A).
|
|
@@ -207,8 +208,9 @@ export function validateTaskMessage(message) {
|
|
|
207
208
|
throw new Error("Task Message must use schemaVersion 1.");
|
|
208
209
|
requireKnownFields(message, [
|
|
209
210
|
"schemaVersion", "id", "taskId", "kind", "author", "body", "intent", "submissionKey", "submissionReceipt",
|
|
210
|
-
"inputControl", "interruptThen", "control", "runId", "resultRef", "workItemId", "recipient", "continuation", "handovers", "createdAt"
|
|
211
|
+
"inputControl", "interruptThen", "control", "runId", "resultRef", "workItemId", "recipient", "continuation", "handovers", "createdAt", "inbox"
|
|
211
212
|
], "Task Message");
|
|
213
|
+
validateInboxReceipt(message.inbox);
|
|
212
214
|
validateTaskRecordReference({ taskId: message.taskId, localId: message.id }, "message");
|
|
213
215
|
requireText(message.body, "Message body");
|
|
214
216
|
validateKindAndAuthor(message.kind, message.author);
|
|
@@ -394,6 +396,7 @@ export function createGlobalRoleMessage(id, roleName, body, kind, author, now, c
|
|
|
394
396
|
return message;
|
|
395
397
|
}
|
|
396
398
|
export function validateGlobalRoleMessage(message) {
|
|
399
|
+
validateInboxReceipt(message.inbox);
|
|
397
400
|
if (message.schemaVersion !== 1)
|
|
398
401
|
throw new Error("Global Role Message must use schemaVersion 1.");
|
|
399
402
|
validateGlobalRoleMessageId(message.id);
|
|
@@ -517,6 +520,8 @@ export function markGlobalRoleMessageDelivered(message, now, via = "provider") {
|
|
|
517
520
|
* keeps resolving to this same Message and never creates a second input.
|
|
518
521
|
*/
|
|
519
522
|
export function claimGlobalRoleMessageInterruptThen(message, claim) {
|
|
523
|
+
if (message.inbox !== undefined)
|
|
524
|
+
throw new Error("Agent inbox handling already owns this Message; it cannot be replayed through interrupt-then.");
|
|
520
525
|
const { inputControl, notDelivered: _priorNondelivery, ...rest } = message;
|
|
521
526
|
const claimed = {
|
|
522
527
|
...rest,
|
|
@@ -10,6 +10,7 @@ import { validateExactRunReviewRound } from "../lifecycle/exactRunTerminalizatio
|
|
|
10
10
|
import { sourceRunContextValue } from "../context/sourceRunContext.js";
|
|
11
11
|
import { requireManagedTaskCaller } from "../runtime/managedCaller.js";
|
|
12
12
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
13
|
+
import { isTaskRecordRetired, operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
13
14
|
export function resolveMessageRecipient(store, taskId, roleName, scope) {
|
|
14
15
|
if (scope.reviewRoundId !== undefined) {
|
|
15
16
|
const round = store.getReviewRound(taskId, scope.reviewRoundId);
|
|
@@ -28,8 +29,11 @@ export function resolveMessageRecipient(store, taskId, roleName, scope) {
|
|
|
28
29
|
throw new Error("No existing Dispatch owns this Role and work scope.");
|
|
29
30
|
return { roleName, ownerRunId: previous.id, ...scope };
|
|
30
31
|
}
|
|
31
|
-
/**
|
|
32
|
-
|
|
32
|
+
/** Current Assignment authority, independent of eligibility to start a new Run.
|
|
33
|
+
* A Reviewer still owns its frozen clarification when Task main advances. */
|
|
34
|
+
export function messageAssignmentBlocker(store, message) {
|
|
35
|
+
if (isTaskRecordRetired(store.listEvents(message.taskId), "message", message.id))
|
|
36
|
+
return "message-retired";
|
|
33
37
|
const target = message.recipient;
|
|
34
38
|
if (target?.ownerRunId === undefined)
|
|
35
39
|
return "recipient-assignment-missing";
|
|
@@ -78,22 +82,29 @@ export function messageContinuationBlocker(store, message) {
|
|
|
78
82
|
&& latest.inputs[0]?.input.source.channel !== "message-continuation")
|
|
79
83
|
return "assignment-changed";
|
|
80
84
|
if (target.reviewRoundId !== undefined) {
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
83
|
-
return "review-
|
|
85
|
+
const validation = validateExactRunReviewRound(store, latest ?? owner, { allowTerminal: true });
|
|
86
|
+
if (validation.disposition !== "applied")
|
|
87
|
+
return validation.reason ?? "review-candidate-stale";
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
/** Starting another Run additionally requires the reviewed candidate to remain current. */
|
|
92
|
+
export function messageContinuationBlocker(store, message) {
|
|
93
|
+
const invalid = messageAssignmentBlocker(store, message);
|
|
94
|
+
if (invalid !== undefined)
|
|
95
|
+
return invalid;
|
|
96
|
+
const reviewRoundId = message.recipient?.reviewRoundId;
|
|
97
|
+
if (reviewRoundId !== undefined) {
|
|
98
|
+
const round = store.getReviewRound(message.taskId, reviewRoundId);
|
|
84
99
|
if (round.scope === "task") {
|
|
100
|
+
const task = store.getTask(message.taskId);
|
|
85
101
|
if (round.taskCandidate?.projects.some((project) => task.projectBindings.find((binding) => binding.projectId === project.projectId)?.currentCommit !== project.commit)) {
|
|
86
102
|
return "review-candidate-stale";
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
|
-
else {
|
|
90
|
-
|
|
91
|
-
if (item?.candidates.at(-1)?.id !== round.candidateId)
|
|
92
|
-
return "review-candidate-stale";
|
|
105
|
+
else if (store.getWorkItem(message.taskId, round.workItemId)?.candidates.at(-1)?.id !== round.candidateId) {
|
|
106
|
+
return "review-candidate-stale";
|
|
93
107
|
}
|
|
94
|
-
const validation = validateExactRunReviewRound(store, latest ?? owner, { allowTerminal: true });
|
|
95
|
-
if (validation.disposition !== "applied")
|
|
96
|
-
return validation.reason ?? "review-candidate-stale";
|
|
97
108
|
}
|
|
98
109
|
return undefined;
|
|
99
110
|
}
|
|
@@ -189,7 +200,11 @@ export function interruptThenTerminalState(store, message) {
|
|
|
189
200
|
* remain the pending authority; Mailbox is only the existing scheduling hint.
|
|
190
201
|
* Reserving inputs and creating the next Run is one atomic effect. */
|
|
191
202
|
export function prepareMessageContinuations(store, taskId, now, roleName) {
|
|
192
|
-
const
|
|
203
|
+
const messages = operationalTaskRecords(store.listMessages(taskId), store.listEvents(taskId), "message");
|
|
204
|
+
const claims = messages.filter(message => message.recipient?.roleName === roleName
|
|
205
|
+
&& message.inbox !== undefined && message.inbox.ack === undefined);
|
|
206
|
+
const pending = messages.filter((message) => message.recipient?.roleName === roleName && message.recipient.ownerRunId !== undefined
|
|
207
|
+
&& message.inbox === undefined
|
|
193
208
|
&& message.continuation?.runId === undefined
|
|
194
209
|
// A steer targets the exact current native turn, never a queued next Run.
|
|
195
210
|
// If its live attempt did not submit, the message stays saved for the
|
|
@@ -197,6 +212,12 @@ export function prepareMessageContinuations(store, taskId, now, roleName) {
|
|
|
197
212
|
&& message.inputControl?.action !== "steer");
|
|
198
213
|
for (const message of pending) {
|
|
199
214
|
const recipient = message.recipient;
|
|
215
|
+
// Retain ordering within the claimed Assignment, not across unrelated
|
|
216
|
+
// work owned by the same Role. Only ack or explicit retirement settles a
|
|
217
|
+
// claim's scheduling obligation; neither time nor a failed Run does.
|
|
218
|
+
if (claims.some(claim => claim.recipient?.workItemId === recipient.workItemId
|
|
219
|
+
&& claim.recipient?.reviewRoundId === recipient.reviewRoundId))
|
|
220
|
+
continue;
|
|
200
221
|
const blocker = messageContinuationBlocker(store, message);
|
|
201
222
|
if (blocker !== undefined) {
|
|
202
223
|
markNotDelivered(store, message, blocker);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
2
|
+
export function latestMessageUpdate(store, message) {
|
|
3
|
+
return store.listEvents(message.taskId).filter(event => event.type === "message.updated" && event.payload.messageId === message.id).at(-1);
|
|
4
|
+
}
|
|
5
|
+
/** Leader steer has no Assignment recipient; ordinary Task records are not
|
|
6
|
+
* private inbox messages. Queued Leader inputs remain fixed wake windows. */
|
|
7
|
+
export function isLeaderSteer(message) {
|
|
8
|
+
return message.recipient === undefined && message.inputControl?.action === "steer";
|
|
9
|
+
}
|
|
10
|
+
/** Derive ownership from original receipts, never from disposable mailboxes. */
|
|
11
|
+
export function taskMessageInboxReceipt(store, message) {
|
|
12
|
+
const revision = latestMessageUpdate(store, message);
|
|
13
|
+
return message.inbox ?? store.listTaskWakes(message.taskId).find(wake => wake.inbox !== undefined
|
|
14
|
+
&& (revision === undefined || wake.refs?.some(ref => ref.type === "event" && ref.taskId === message.taskId && ref.id === revision.id))
|
|
15
|
+
&& wake.refs?.some(ref => ref.type === "message" && ref.taskId === message.taskId && ref.id === message.id))?.inbox;
|
|
16
|
+
}
|
|
17
|
+
export function unfinishedLeaderInbox(store, taskId) {
|
|
18
|
+
return store.listTaskWakes(taskId).some(wake => wake.inbox !== undefined && wake.inbox.ack === undefined)
|
|
19
|
+
|| operationalTaskRecords(store.listMessages(taskId), store.listEvents(taskId), "message").some(message => isLeaderSteer(message)
|
|
20
|
+
&& message.inbox !== undefined && message.inbox.ack === undefined);
|
|
21
|
+
}
|