@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
|
@@ -12,7 +12,7 @@ import { createHash } from "node:crypto";
|
|
|
12
12
|
import { spawnSync } from "node:child_process";
|
|
13
13
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
-
import Database from "
|
|
15
|
+
import Database from "../sqlite/driver.js";
|
|
16
16
|
import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "../storage/storageVersions.js";
|
|
17
17
|
import { inspectStorageSchema } from "../storage/storageSchema.js";
|
|
18
18
|
import { resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { existsSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { isDeepStrictEqual } from "node:util";
|
|
16
|
-
import Database from "
|
|
16
|
+
import Database from "../sqlite/driver.js";
|
|
17
17
|
import { validateSqliteSchema } from "../storage/sqliteSchema.js";
|
|
18
18
|
import { emptyResourceRegistry, parseResourceRegistryState } from "./resourceRegistry.js";
|
|
19
19
|
import { RESOURCE_REGISTRY_SCHEMA_VERSION } from "./resourceTypes.js";
|
|
@@ -93,6 +93,9 @@ export async function runAgentHost(input) {
|
|
|
93
93
|
let activeRunPayload;
|
|
94
94
|
let lastTerminalPayload;
|
|
95
95
|
let lastTerminal;
|
|
96
|
+
// One exact direct Turn witnessed by this Endpoint under its original fence.
|
|
97
|
+
// This is recovery evidence, never ownership of the Desktop client's input.
|
|
98
|
+
let directObservation;
|
|
96
99
|
let activeRunAttemptId;
|
|
97
100
|
let activeNativeTurnId;
|
|
98
101
|
let codexClientAttachedAt;
|
|
@@ -166,6 +169,7 @@ export async function runAgentHost(input) {
|
|
|
166
169
|
const handleStarted = (started, observedPayload = sessionPayload) => {
|
|
167
170
|
if (started.clientOwned || observedPayload === undefined)
|
|
168
171
|
return;
|
|
172
|
+
const observedAuthority = authority === undefined ? undefined : { ...authority };
|
|
169
173
|
updateSnapshot(hostSnapshot("busy", {
|
|
170
174
|
adapterId: observedPayload.providerControl?.adapterId,
|
|
171
175
|
processInstanceId: session?.processInstanceId,
|
|
@@ -175,6 +179,22 @@ export async function runAgentHost(input) {
|
|
|
175
179
|
...authorityFields()
|
|
176
180
|
}));
|
|
177
181
|
void enqueueSerialized(async () => {
|
|
182
|
+
// Desktop can start another Turn before the Controller's periodic pass.
|
|
183
|
+
// Persist the previous exact terminal before replacing its bounded cache
|
|
184
|
+
// or publishing the new start. Never clear the newer native writer.
|
|
185
|
+
const previous = directObservation;
|
|
186
|
+
if (previous?.terminal !== undefined && authority !== undefined
|
|
187
|
+
&& sameProviderAuthorityFence(previous.authority, authority)
|
|
188
|
+
&& previous.started.nativeSessionId === session?.nativeSessionId) {
|
|
189
|
+
await publishStructuredProviderTerminal({
|
|
190
|
+
home: input.home, environment: previous.payload.environment, terminal: previous.terminal
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (session !== undefined && started.nativeSessionId === session.nativeSessionId
|
|
194
|
+
&& started.conversationId === session.conversationId && observedAuthority !== undefined
|
|
195
|
+
&& authority !== undefined && sameProviderAuthorityFence(observedAuthority, authority)) {
|
|
196
|
+
directObservation = { started, payload: observedPayload, authority: observedAuthority };
|
|
197
|
+
}
|
|
178
198
|
await publishStructuredProviderStarted({
|
|
179
199
|
home: input.home,
|
|
180
200
|
environment: observedPayload.environment,
|
|
@@ -194,6 +214,14 @@ export async function runAgentHost(input) {
|
|
|
194
214
|
// AgentRun audit record. It has no WorkItem ownership and no managed input
|
|
195
215
|
// fence; the runtime observer records only visible input/output.
|
|
196
216
|
void enqueueSerialized(async () => {
|
|
217
|
+
const direct = directObservation;
|
|
218
|
+
if (direct !== undefined && authority !== undefined
|
|
219
|
+
&& sameProviderAuthorityFence(direct.authority, authority)
|
|
220
|
+
&& direct.started.nativeSessionId === terminal.nativeSessionId
|
|
221
|
+
&& direct.started.conversationId === terminal.conversationId
|
|
222
|
+
&& direct.started.nativeTurnId === terminal.nativeTurnId) {
|
|
223
|
+
direct.terminal = terminal;
|
|
224
|
+
}
|
|
197
225
|
await publishStructuredProviderTerminal({
|
|
198
226
|
home: input.home,
|
|
199
227
|
environment: busyPayload.environment,
|
|
@@ -274,6 +302,7 @@ export async function runAgentHost(input) {
|
|
|
274
302
|
conversationId: terminal.conversationId,
|
|
275
303
|
...authorityFields()
|
|
276
304
|
}));
|
|
305
|
+
signalRoleMailbox(input.home, currentPayload);
|
|
277
306
|
promptHuman();
|
|
278
307
|
}
|
|
279
308
|
catch (error) {
|
|
@@ -621,6 +650,7 @@ export async function runAgentHost(input) {
|
|
|
621
650
|
await recordProviderConnection(input.home, next, started.session);
|
|
622
651
|
lastTerminalPayload = undefined;
|
|
623
652
|
lastTerminal = undefined;
|
|
653
|
+
directObservation = undefined;
|
|
624
654
|
// Recoverable means a later process can rebind this Conversation by
|
|
625
655
|
// its native id. The Endpoint answers that from the Driver capability
|
|
626
656
|
// or, where the protocol settles it per connection, from what this
|
|
@@ -1157,6 +1187,51 @@ export async function runAgentHost(input) {
|
|
|
1157
1187
|
}
|
|
1158
1188
|
return controlResult("accepted", { ...snapshot, nativeTui: !!nativeTui });
|
|
1159
1189
|
}
|
|
1190
|
+
if (request.type === "reconcile-turn") {
|
|
1191
|
+
if (session === undefined || session.nativeSessionId !== request.nativeSessionId
|
|
1192
|
+
|| authority === undefined || !sameProviderAuthorityFence(authority, request.authority)) {
|
|
1193
|
+
throw new Error("Terminal reconciliation targets a different Session or authority.");
|
|
1194
|
+
}
|
|
1195
|
+
const direct = directObservation;
|
|
1196
|
+
const directMatch = direct?.terminal !== undefined
|
|
1197
|
+
&& request.nativeTurnId !== undefined
|
|
1198
|
+
&& request.attemptId === `direct:${request.nativeTurnId}`
|
|
1199
|
+
&& direct.started.nativeTurnId === request.nativeTurnId
|
|
1200
|
+
&& direct.started.nativeSessionId === request.nativeSessionId
|
|
1201
|
+
&& sameProviderAuthorityFence(direct.authority, request.authority);
|
|
1202
|
+
const terminal = directMatch ? direct.terminal : lastTerminal;
|
|
1203
|
+
const terminalPayload = directMatch ? direct.payload : lastTerminalPayload;
|
|
1204
|
+
if (terminal === undefined || terminalPayload === undefined
|
|
1205
|
+
|| terminal.nativeSessionId !== request.nativeSessionId
|
|
1206
|
+
|| !directMatch && (terminal.clientOwned !== true || terminal.attemptId !== request.attemptId)
|
|
1207
|
+
|| request.nativeTurnId !== undefined && terminal.nativeTurnId !== request.nativeTurnId) {
|
|
1208
|
+
return { ...controlResult("status", snapshot), reconciliation: "unobserved" };
|
|
1209
|
+
}
|
|
1210
|
+
// Serialize with submission/terminal persistence. The original Host and
|
|
1211
|
+
// original launch envelope remain the observer, including the full result.
|
|
1212
|
+
await enqueueSerialized(async () => {
|
|
1213
|
+
if (session?.nativeSessionId !== request.nativeSessionId || authority === undefined
|
|
1214
|
+
|| !sameProviderAuthorityFence(authority, request.authority)
|
|
1215
|
+
|| directMatch && directObservation !== direct) {
|
|
1216
|
+
throw new Error("Session changed before terminal reconciliation.");
|
|
1217
|
+
}
|
|
1218
|
+
await publishStructuredProviderTerminal({
|
|
1219
|
+
home: input.home, environment: terminalPayload.environment, terminal
|
|
1220
|
+
});
|
|
1221
|
+
if (!directMatch && activeRunAttemptId === terminal.attemptId) {
|
|
1222
|
+
activeRunPayload = undefined;
|
|
1223
|
+
activeRunAttemptId = undefined;
|
|
1224
|
+
activeNativeTurnId = undefined;
|
|
1225
|
+
updateSnapshot(hostSnapshot(session.inspect().activeNativeTurnId === undefined ? "idle" : "busy", {
|
|
1226
|
+
adapterId: session.adapterId, processInstanceId: session.processInstanceId,
|
|
1227
|
+
nativeSessionId: session.nativeSessionId, conversationId: session.conversationId,
|
|
1228
|
+
...authorityFields()
|
|
1229
|
+
}));
|
|
1230
|
+
}
|
|
1231
|
+
signalRoleMailbox(input.home, terminalPayload);
|
|
1232
|
+
});
|
|
1233
|
+
return { ...controlResult("status", snapshot), reconciliation: "terminal-republished" };
|
|
1234
|
+
}
|
|
1160
1235
|
if (request.type === "status") {
|
|
1161
1236
|
// Read the Agent's configuration at answer time, not from the stored
|
|
1162
1237
|
// snapshot. The Agent may have changed it since the last state
|
|
@@ -1461,6 +1536,11 @@ export async function inspectAgentHost(input) {
|
|
|
1461
1536
|
});
|
|
1462
1537
|
return result.snapshot;
|
|
1463
1538
|
}
|
|
1539
|
+
export async function reconcileAgentHostTurn(input) {
|
|
1540
|
+
// Periodic observation must not hold the scheduler behind an unavailable Host.
|
|
1541
|
+
// A timeout is only an observation failure, never permission to submit again.
|
|
1542
|
+
return exchangeAgentHostControl(agentHostControlSocketPath(input), input.control, 1_000);
|
|
1543
|
+
}
|
|
1464
1544
|
export async function waitForAgentHostLaunchAck(input) {
|
|
1465
1545
|
const deadline = Date.now() + (input.timeoutMs ?? AGENT_HOST_READY_TIMEOUT_MS);
|
|
1466
1546
|
let lastError;
|
|
@@ -1616,7 +1696,7 @@ export async function openAgentHostControl(home, payload, snapshot, dispatch) {
|
|
|
1616
1696
|
...result,
|
|
1617
1697
|
snapshot: {
|
|
1618
1698
|
...result.snapshot,
|
|
1619
|
-
...(payload.environment.YUI_SESSION_SCOPE
|
|
1699
|
+
...(!["task", "global"].includes(payload.environment.YUI_SESSION_SCOPE ?? "") ? {} : {
|
|
1620
1700
|
eventDelivery: structuredProviderEventDelivery(home, payload.environment, result.snapshot.nativeSessionId)
|
|
1621
1701
|
})
|
|
1622
1702
|
}
|
|
@@ -1691,7 +1771,8 @@ function boundControlResponse(result) {
|
|
|
1691
1771
|
// visible through an otherwise successful status request.
|
|
1692
1772
|
result = {
|
|
1693
1773
|
...controlResult(result.outcome, validateSnapshot(result.snapshot), result.failure === undefined ? undefined : providerDeliveryFailure(result.failure)),
|
|
1694
|
-
...(result.cancellation === undefined ? {} : { cancellation: result.cancellation })
|
|
1774
|
+
...(result.cancellation === undefined ? {} : { cancellation: result.cancellation }),
|
|
1775
|
+
...(result.reconciliation === undefined ? {} : { reconciliation: result.reconciliation })
|
|
1695
1776
|
};
|
|
1696
1777
|
if (withinControlBound(result))
|
|
1697
1778
|
return result;
|
|
@@ -1709,29 +1790,29 @@ function boundControlResponse(result) {
|
|
|
1709
1790
|
};
|
|
1710
1791
|
// Never make an omitted option list look like a complete empty enumeration.
|
|
1711
1792
|
if (result.snapshot.runConfiguration?.status === "observed") {
|
|
1712
|
-
result = controlResult(result.outcome, validateSnapshot({
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1793
|
+
result = { ...result, ...controlResult(result.outcome, validateSnapshot({
|
|
1794
|
+
...result.snapshot,
|
|
1795
|
+
runConfiguration: unknownAgentRunConfiguration("The Agent's reported configuration is too large for the Agent Host "
|
|
1796
|
+
+ "control response, so it could not be carried in full and none of it "
|
|
1797
|
+
+ "is shown rather than part of it.")
|
|
1798
|
+
}), result.failure) };
|
|
1718
1799
|
if (withinControlBound(result))
|
|
1719
1800
|
return result;
|
|
1720
1801
|
}
|
|
1721
|
-
let bounded = controlResult(result.outcome, {
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1802
|
+
let bounded = { ...result, ...controlResult(result.outcome, {
|
|
1803
|
+
...result.snapshot,
|
|
1804
|
+
...(result.snapshot.detail === undefined ? {} : { detail: clip(result.snapshot.detail, 1200) })
|
|
1805
|
+
}, result.failure === undefined ? undefined : { ...result.failure, detail: clip(result.failure.detail, 1200) }) };
|
|
1725
1806
|
const raw = bounded.failure?.raw;
|
|
1726
1807
|
if (withinControlBound(bounded) || raw === undefined)
|
|
1727
1808
|
return bounded;
|
|
1728
1809
|
let chars = raw.length;
|
|
1729
1810
|
while (!withinControlBound(bounded) && chars > 0) {
|
|
1730
1811
|
chars = Math.floor(chars / 2);
|
|
1731
|
-
bounded = controlResult(bounded.outcome, bounded.snapshot, {
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1812
|
+
bounded = { ...bounded, ...controlResult(bounded.outcome, bounded.snapshot, {
|
|
1813
|
+
...bounded.failure,
|
|
1814
|
+
raw: clip(raw, chars)
|
|
1815
|
+
}) };
|
|
1735
1816
|
}
|
|
1736
1817
|
return bounded;
|
|
1737
1818
|
}
|
|
@@ -1764,6 +1845,14 @@ function validateControl(control) {
|
|
|
1764
1845
|
}
|
|
1765
1846
|
if (control.type === "status")
|
|
1766
1847
|
return Object.freeze({ ...control });
|
|
1848
|
+
if (control.type === "reconcile-turn") {
|
|
1849
|
+
validateIdentity(control.nativeSessionId, "native Session id");
|
|
1850
|
+
validateIdentity(control.attemptId, "Provider input attempt id");
|
|
1851
|
+
if (control.nativeTurnId !== undefined)
|
|
1852
|
+
validateIdentity(control.nativeTurnId, "native Turn id");
|
|
1853
|
+
validateProviderAuthorityFence(control.authority);
|
|
1854
|
+
return Object.freeze({ ...control });
|
|
1855
|
+
}
|
|
1767
1856
|
if (control.type === "native-open" || control.type === "native-respond" || control.type === "native-model") {
|
|
1768
1857
|
validateIdentity(control.nativeSessionId, "native Session id");
|
|
1769
1858
|
if (control.type === "native-model" && (typeof control.model !== "string"
|
|
@@ -26,7 +26,7 @@ function mapClaudeDisplayError(input) {
|
|
|
26
26
|
}
|
|
27
27
|
/** Provider-specific Codex failure recognition. */
|
|
28
28
|
export function mapCodexAgentError(input) {
|
|
29
|
-
return withTransientEvidence(mapCodexDisplayError(input), input);
|
|
29
|
+
return withTransientEvidence(mapCodexDisplayError(input), input, true);
|
|
30
30
|
}
|
|
31
31
|
function mapCodexDisplayError(input) {
|
|
32
32
|
const text = `${input.message}\n${input.raw}`;
|
|
@@ -208,7 +208,7 @@ function sessionUnavailable(code) {
|
|
|
208
208
|
}
|
|
209
209
|
/** Replay qualification is stricter than the readable, best-effort category.
|
|
210
210
|
* Never promote a number occurring in provider prose to submission safety. */
|
|
211
|
-
function structuredTransientError(input) {
|
|
211
|
+
function structuredTransientError(input, codex) {
|
|
212
212
|
if (/insufficient[_ -]quota|quota.{0,25}(exhaust|exceed)|billing[_ -]hard[_ -]limit|usageLimitExceeded|credits?.{0,20}(exhaust|insufficient)/iu.test(`${input.message}\n${input.raw}`))
|
|
213
213
|
return { category: "access", code: "provider.quota-exhausted", retryable: false };
|
|
214
214
|
const nodes = [];
|
|
@@ -228,7 +228,8 @@ function structuredTransientError(input) {
|
|
|
228
228
|
catch { /* Display classification may still use prose. */ }
|
|
229
229
|
const codes = nodes.flatMap(n => [n.code, n.type, n.error, n.codexErrorInfo]).filter(v => typeof v === "string");
|
|
230
230
|
if (codes.some(c => ["authentication_error", "permission_error", "invalid_api_key", "invalid_model",
|
|
231
|
-
"model_not_found", "usage_limit_exceeded", "insufficient_quota"].includes(c))
|
|
231
|
+
"model_not_found", "usage_limit_exceeded", "insufficient_quota"].includes(c))
|
|
232
|
+
|| codex && nodes.some(n => ["usageLimitExceeded", "unauthorized", "badRequest"].includes(String(n.codexErrorInfo)))) {
|
|
232
233
|
return { category: "access", code: "provider.access-denied", retryable: false };
|
|
233
234
|
}
|
|
234
235
|
const statuses = nodes.flatMap(n => [n.httpStatusCode, n.statusCode, n.status]).filter(v => typeof v === "number");
|
|
@@ -240,6 +241,9 @@ function structuredTransientError(input) {
|
|
|
240
241
|
if (statuses.includes(429) || codes.includes("rate_limit_error") || nativeCode === "rate_limit_error") {
|
|
241
242
|
result = recoverable("rate-limit", "provider.rate-limit");
|
|
242
243
|
}
|
|
244
|
+
else if (codex && nodes.some(n => n.codexErrorInfo === "serverOverloaded")) {
|
|
245
|
+
result = recoverable("availability", "provider.model-capacity");
|
|
246
|
+
}
|
|
243
247
|
else if (statuses.some(s => [500, 502, 503, 504].includes(s))
|
|
244
248
|
|| codes.some(c => ["server_error", "overloaded_error"].includes(c))
|
|
245
249
|
|| ["server_error", "overloaded_error"].includes(nativeCode)) {
|
|
@@ -277,8 +281,8 @@ function structuredTransientError(input) {
|
|
|
277
281
|
}
|
|
278
282
|
/** Add positive evidence without renaming established diagnostics or letting
|
|
279
283
|
* a transient nested cause override a known fatal Session/request failure. */
|
|
280
|
-
function withTransientEvidence(base, input) {
|
|
281
|
-
const evidence = structuredTransientError(input);
|
|
284
|
+
function withTransientEvidence(base, input, codex = false) {
|
|
285
|
+
const evidence = structuredTransientError(input, codex);
|
|
282
286
|
if (evidence === undefined)
|
|
283
287
|
return base;
|
|
284
288
|
const possiblyTransient = ["rate-limit", "availability", "transport", "unknown"].includes(base.category);
|
|
@@ -3,6 +3,44 @@ export const PROVIDER_RETRY_BUDGET_MS = 600_000;
|
|
|
3
3
|
export function providerRetryPending(binding) {
|
|
4
4
|
return binding?.retry?.status === "waiting" || binding?.retry?.status === "in-flight";
|
|
5
5
|
}
|
|
6
|
+
/** An explicit Run retry can carry exact failed-input proof even when the
|
|
7
|
+
* error did not qualify for automation. This does not grant automatic replay:
|
|
8
|
+
* the shared admission and native latest-Turn/quiescence checks still apply. */
|
|
9
|
+
export function providerRunRetry(binding, runId, at) {
|
|
10
|
+
const turn = binding.run;
|
|
11
|
+
const nativeSessionId = binding.conversations.find(c => c.epoch === binding.currentConversationEpoch)?.conversationId;
|
|
12
|
+
if (turn?.runId !== runId || !["failed", "rejected", "deferred"].includes(turn.status)
|
|
13
|
+
|| binding.authority.owner !== "controller" || turn.authorityEpoch !== binding.authority.epoch
|
|
14
|
+
|| nativeSessionId === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
const previous = binding.retry;
|
|
17
|
+
if (previous?.previousRunId === runId && previous.status !== "recovered"
|
|
18
|
+
&& previous.failedAttemptId === turn.attemptId
|
|
19
|
+
&& previous.authorityEpoch === binding.authority.epoch
|
|
20
|
+
&& previous.nativeSessionId === nativeSessionId)
|
|
21
|
+
return previous;
|
|
22
|
+
const failure = turn.failure;
|
|
23
|
+
if (turn.status !== "failed" || turn.nativeTurnId === undefined || turn.input === undefined
|
|
24
|
+
|| turn.retrySupported !== true || failure?.error.inputDisposition !== "accepted"
|
|
25
|
+
|| failure.error.sessionDisposition !== "recoverable")
|
|
26
|
+
return undefined;
|
|
27
|
+
// A manual attempt can cancel automation when its error changes character.
|
|
28
|
+
// Refresh its failed-Turn proof without granting the same input a new budget.
|
|
29
|
+
const continuing = previous?.status !== "recovered" && previous?.currentAttemptId === turn.attemptId
|
|
30
|
+
&& previous.authorityEpoch === binding.authority.epoch && previous.nativeSessionId === nativeSessionId;
|
|
31
|
+
return {
|
|
32
|
+
policyVersion: 1, chainId: continuing ? previous.chainId : turn.attemptId, status: "waiting",
|
|
33
|
+
input: continuing ? previous.input : turn.input, nativeSessionId, authorityEpoch: binding.authority.epoch,
|
|
34
|
+
attempts: continuing ? previous.attempts : 0,
|
|
35
|
+
transportSequence: continuing ? previous.transportSequence : 0,
|
|
36
|
+
startedAt: continuing ? previous.startedAt : iso(at), intentAt: iso(at),
|
|
37
|
+
deadline: continuing ? previous.deadline : iso(at + PROVIDER_RETRY_BUDGET_MS),
|
|
38
|
+
nextEligibleAt: iso(at), updatedAt: iso(at),
|
|
39
|
+
failedAttemptId: turn.attemptId, failureAttemptId: turn.attemptId,
|
|
40
|
+
failedNativeTurnId: turn.nativeTurnId, previousRunId: runId,
|
|
41
|
+
failureRef: failure.ref, error: failure.error, successorAutomatic: false
|
|
42
|
+
};
|
|
43
|
+
}
|
|
6
44
|
/** Called only in the transaction recording the exact failed/rejected input. */
|
|
7
45
|
export function recordProviderFailure(binding, input) {
|
|
8
46
|
let turn = binding.run;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
2
|
+
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
3
|
+
import { projectRuntimeTaskEvents, classifyRuntimeHealth } from "../runtime/runtimeProjection.js";
|
|
4
|
+
import { DEFAULT_RUNTIME_HEALTH_POLICY } from "../runtime/runtimeHealthPolicy.js";
|
|
5
|
+
import { providerRetryProjection } from "../runtime/providerRetry.js";
|
|
6
|
+
/** Read-only presentation of the selected native connections, never an AgentRun
|
|
7
|
+
* count. Foreground facts are fenced to the exact input; retained continuations
|
|
8
|
+
* stay attached to their original input and are not counted as extra Sessions.
|
|
9
|
+
* No live probe, timers, persistence or recovery actions are introduced here. */
|
|
10
|
+
export function projectTaskSessions(input) {
|
|
11
|
+
const policy = input.policy ?? DEFAULT_RUNTIME_HEALTH_POLICY;
|
|
12
|
+
const sessions = [];
|
|
13
|
+
const seen = new Map();
|
|
14
|
+
const selectedIdentities = new Set();
|
|
15
|
+
const observationIdentity = (fence) => JSON.stringify([fence.roleName, fence.agentId, fence.driverId, fence.nativeSessionId]);
|
|
16
|
+
const observations = input.events.map(event => ({ event, observation: runtimeObservationFromTaskEvent(event) }))
|
|
17
|
+
.filter(entry => entry.event.taskId === input.taskId && entry.observation?.fence.taskId === input.taskId);
|
|
18
|
+
for (const set of input.sessionSets) {
|
|
19
|
+
if (set.owner.scope !== "task" || set.owner.taskId !== input.taskId)
|
|
20
|
+
continue;
|
|
21
|
+
const session = set.sessions[set.activeAgentId];
|
|
22
|
+
const binding = set.providerBinding;
|
|
23
|
+
const conversation = binding?.conversations.find(c => c.epoch === binding.currentConversationEpoch && c.status === "current");
|
|
24
|
+
// Replacement can leave an empty set. That is absence, not uncertain
|
|
25
|
+
// execution; retained child facts are still discovered below.
|
|
26
|
+
if (!session && !conversation && !binding?.run)
|
|
27
|
+
continue;
|
|
28
|
+
const nativeSessionId = session?.nativeSessionId ?? conversation?.conversationId ?? null;
|
|
29
|
+
const identity = JSON.stringify([binding?.providerNamespace ?? session?.adapterId,
|
|
30
|
+
binding?.accountScope ?? set.activeAgentId, nativeSessionId ?? set.owner.roleName]);
|
|
31
|
+
const prior = seen.get(identity);
|
|
32
|
+
if (prior !== undefined) {
|
|
33
|
+
if (sessions[prior].roleName !== set.owner.roleName)
|
|
34
|
+
sessions[prior] = {
|
|
35
|
+
...sessions[prior], group: "unknown", reason: "Multiple Roles name this native Session; inspect ownership."
|
|
36
|
+
};
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
seen.set(identity, sessions.length);
|
|
40
|
+
const turn = binding?.run;
|
|
41
|
+
let driverId = null;
|
|
42
|
+
if (session) {
|
|
43
|
+
try {
|
|
44
|
+
driverId = builtinDriverIdForAdapter(session.adapterId);
|
|
45
|
+
}
|
|
46
|
+
catch { /* Unsupported native observation remains explicitly unknown. */ }
|
|
47
|
+
}
|
|
48
|
+
const row = {
|
|
49
|
+
taskId: input.taskId, roleName: set.owner.roleName, agentId: set.activeAgentId,
|
|
50
|
+
driverId,
|
|
51
|
+
nativeSessionId, attemptId: turn?.attemptId ?? null, nativeTurnId: turn?.nativeTurnId ?? null,
|
|
52
|
+
runId: turn?.runId ?? null, group: "unknown", reason: "Current native input is not observable.",
|
|
53
|
+
layer: null, lastActivityAt: null, semanticProgressAt: input.semanticProgressAt ?? null,
|
|
54
|
+
sourceUpdatedAt: turn?.updatedAt ?? session?.updatedAt ?? set.updatedAt,
|
|
55
|
+
waitingReason: null, operations: [], background: []
|
|
56
|
+
};
|
|
57
|
+
if (!session || driverId === null) {
|
|
58
|
+
sessions.push(row);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const fence = { taskId: input.taskId, roleName: set.owner.roleName,
|
|
62
|
+
agentId: session.agentId, driverId, nativeSessionId: session.nativeSessionId };
|
|
63
|
+
selectedIdentities.add(observationIdentity(fence));
|
|
64
|
+
const sessionFacts = observations.filter(({ observation }) => Object.entries(fence).every(([key, value]) => observation.fence[key] === value));
|
|
65
|
+
// Each Run's continuation fold has its own authority. Foreground operations
|
|
66
|
+
// from older inputs must not make the current input look active.
|
|
67
|
+
const groups = new Map();
|
|
68
|
+
for (const { event, observation } of sessionFacts) {
|
|
69
|
+
const runId = observation.fence.runId;
|
|
70
|
+
const group = groups.get(runId) ?? [];
|
|
71
|
+
group.push(event);
|
|
72
|
+
groups.set(runId, group);
|
|
73
|
+
}
|
|
74
|
+
const background = backgroundForRuns(fence, session.createdAt, groups);
|
|
75
|
+
if (!binding || !conversation || conversation.conversationId !== session.nativeSessionId) {
|
|
76
|
+
sessions.push({ ...row, background });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!turn) {
|
|
80
|
+
const sessionEvents = sessionFacts.filter(({ observation }) => {
|
|
81
|
+
const f = observation.fence;
|
|
82
|
+
return f.runId === undefined && f.nativeTurnId === undefined
|
|
83
|
+
&& f.receiptId === undefined && f.continuationId === undefined;
|
|
84
|
+
}).map(entry => entry.event);
|
|
85
|
+
const projection = projectRuntimeTaskEvents(fence, session.createdAt, sessionEvents);
|
|
86
|
+
const ready = session.status !== "ended" && projection.session === "ready"
|
|
87
|
+
&& projection.host !== "exited" && projection.run === "none"
|
|
88
|
+
&& projection.observer.status !== "unavailable" && projection.observer.status !== "degraded"
|
|
89
|
+
&& projection.goal !== "active" && binding.goal?.status !== "active"
|
|
90
|
+
&& Object.keys(projection.operations).length === 0;
|
|
91
|
+
sessions.push({ ...row, background, ...(background.length ? {
|
|
92
|
+
group: "background", reason: "Retained background execution is not settled."
|
|
93
|
+
} : session.status === "ended" || projection.session === "ended" ? {
|
|
94
|
+
group: "stopped", reason: "The selected Session ended with no submitted native input."
|
|
95
|
+
} : ready ? {
|
|
96
|
+
group: "idle", reason: "The selected Session is ready with no submitted native input."
|
|
97
|
+
} : {}) });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const exactEvents = sessionFacts.filter(({ observation }) => {
|
|
101
|
+
const observed = observation;
|
|
102
|
+
const f = observed.fence;
|
|
103
|
+
if (f.continuationId !== undefined || f.runId !== turn.runId)
|
|
104
|
+
return false;
|
|
105
|
+
if (f.receiptId === undefined && f.nativeTurnId === undefined
|
|
106
|
+
&& ["host.observed", "session.ended", "session.failed", "observer.health"].includes(observed.kind)) {
|
|
107
|
+
return observed.receivedAt >= turn.submittedAt;
|
|
108
|
+
}
|
|
109
|
+
if (f.receiptId !== undefined && f.receiptId !== turn.attemptId)
|
|
110
|
+
return false;
|
|
111
|
+
if (f.nativeTurnId !== undefined && f.nativeTurnId !== turn.nativeTurnId)
|
|
112
|
+
return false;
|
|
113
|
+
return f.receiptId === turn.attemptId
|
|
114
|
+
|| (turn.nativeTurnId !== undefined && f.nativeTurnId === turn.nativeTurnId);
|
|
115
|
+
}).map(entry => entry.event);
|
|
116
|
+
const projection = projectRuntimeTaskEvents({
|
|
117
|
+
...fence, ...(turn.runId ? { runId: turn.runId } : {}),
|
|
118
|
+
receiptId: turn.attemptId, ...(turn.nativeTurnId ? { nativeTurnId: turn.nativeTurnId } : {})
|
|
119
|
+
}, turn.submittedAt, exactEvents);
|
|
120
|
+
const health = classifyRuntimeHealth({ projection, now: input.now, policy,
|
|
121
|
+
semanticProgressAt: input.semanticProgressAt ?? turn.submittedAt });
|
|
122
|
+
const retry = providerRetryProjection(binding);
|
|
123
|
+
let group;
|
|
124
|
+
let reason = health.reason;
|
|
125
|
+
if (turn.status === "delivery-unknown") {
|
|
126
|
+
group = "unknown";
|
|
127
|
+
reason = "Original input delivery is unknown; do not replay it.";
|
|
128
|
+
}
|
|
129
|
+
else if (session.status === "ended") {
|
|
130
|
+
group = background.length ? "background" : "stopped";
|
|
131
|
+
reason = "The selected Session ended; Task acceptance and retained background execution are separate.";
|
|
132
|
+
}
|
|
133
|
+
else if (retry?.status === "waiting") {
|
|
134
|
+
group = "waiting";
|
|
135
|
+
reason = `Controller-owned Provider retry waiting (${retry.attempts}/${retry.limit}); next eligible ${retry.nextEligibleAt}. Existing work is preserved.`;
|
|
136
|
+
}
|
|
137
|
+
else if (["failed", "cancelled", "rejected"].includes(turn.status)) {
|
|
138
|
+
group = background.length ? "background" : "stopped";
|
|
139
|
+
reason = turn.terminalReason ?? "The selected native execution stopped; Task acceptance is separate.";
|
|
140
|
+
}
|
|
141
|
+
else if (turn.status === "completed") {
|
|
142
|
+
group = background.length ? "background" : "idle";
|
|
143
|
+
reason = background.length ? "Main Turn ended; background execution remains unsettled."
|
|
144
|
+
: "The native Turn ended; this is not Task completion.";
|
|
145
|
+
}
|
|
146
|
+
else if (turn.status !== "accepted") {
|
|
147
|
+
group = "waiting";
|
|
148
|
+
reason = "Waiting for native input acceptance.";
|
|
149
|
+
}
|
|
150
|
+
else if (projection.run === "waiting") {
|
|
151
|
+
group = "waiting";
|
|
152
|
+
}
|
|
153
|
+
else if (["broken", "stopped"].includes(health.layer)) {
|
|
154
|
+
group = "stopped";
|
|
155
|
+
}
|
|
156
|
+
else if (["completed", "failed", "cancelled"].includes(projection.run)) {
|
|
157
|
+
group = "unknown";
|
|
158
|
+
reason = "Native terminal and current input binding disagree; inspect the exact original input.";
|
|
159
|
+
}
|
|
160
|
+
else if (projection.observer.status === "unavailable") {
|
|
161
|
+
group = "unknown";
|
|
162
|
+
}
|
|
163
|
+
else if (health.layer === "diagnostic-needed") {
|
|
164
|
+
group = "diagnostic";
|
|
165
|
+
}
|
|
166
|
+
else if (health.runtimeIdleMs < policy.quietAfterMs && projection.activity.kind !== "provider") {
|
|
167
|
+
group = "active";
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
group = "quiet";
|
|
171
|
+
reason = "Accepted input has no recent structured activity; silence is not a proven deadlock.";
|
|
172
|
+
}
|
|
173
|
+
sessions.push({ ...row, group, reason, layer: health.layer, background,
|
|
174
|
+
lastActivityAt: projection.lastRuntimeActivityAt ?? null,
|
|
175
|
+
waitingReason: projection.waitingReason ?? null, operations: health.activeOperations });
|
|
176
|
+
}
|
|
177
|
+
// Retained, exactly fenced child observations can outlive a selected Session.
|
|
178
|
+
// Do not revive old foreground Turns or enumerate historical Sessions: only
|
|
179
|
+
// identities with still-unsettled child facts enter this current projection.
|
|
180
|
+
const retained = new Map();
|
|
181
|
+
for (const entry of observations) {
|
|
182
|
+
const fence = entry.observation.fence;
|
|
183
|
+
const key = observationIdentity(fence);
|
|
184
|
+
if (!fence.nativeSessionId || selectedIdentities.has(key))
|
|
185
|
+
continue;
|
|
186
|
+
const group = retained.get(key) ?? [];
|
|
187
|
+
group.push(entry);
|
|
188
|
+
retained.set(key, group);
|
|
189
|
+
}
|
|
190
|
+
for (const group of retained.values()) {
|
|
191
|
+
const first = group[0].observation;
|
|
192
|
+
const fence = first.fence;
|
|
193
|
+
const runs = new Map();
|
|
194
|
+
for (const entry of group) {
|
|
195
|
+
const runId = entry.observation.fence.runId;
|
|
196
|
+
const events = runs.get(runId) ?? [];
|
|
197
|
+
events.push(entry.event);
|
|
198
|
+
runs.set(runId, events);
|
|
199
|
+
}
|
|
200
|
+
const background = backgroundForRuns({
|
|
201
|
+
taskId: input.taskId, roleName: fence.roleName, agentId: fence.agentId,
|
|
202
|
+
driverId: fence.driverId, nativeSessionId: fence.nativeSessionId
|
|
203
|
+
}, first.receivedAt, runs);
|
|
204
|
+
if (!background.length)
|
|
205
|
+
continue;
|
|
206
|
+
sessions.push({
|
|
207
|
+
taskId: input.taskId, roleName: fence.roleName, agentId: fence.agentId,
|
|
208
|
+
driverId: fence.driverId,
|
|
209
|
+
nativeSessionId: fence.nativeSessionId, attemptId: null, nativeTurnId: null, runId: null,
|
|
210
|
+
group: "background", reason: "Retained child observations from a non-selected native Session; foreground execution is not inferred.",
|
|
211
|
+
layer: null, lastActivityAt: group.at(-1).observation.receivedAt,
|
|
212
|
+
semanticProgressAt: null, sourceUpdatedAt: group.at(-1).observation.receivedAt,
|
|
213
|
+
waitingReason: null, operations: [], background
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const counts = {
|
|
217
|
+
active: 0, waiting: 0, quiet: 0, diagnostic: 0, unknown: 0, stopped: 0, idle: 0, background: 0
|
|
218
|
+
};
|
|
219
|
+
for (const session of sessions)
|
|
220
|
+
counts[session.group]++;
|
|
221
|
+
return { scope: "task-recorded-native-activity", taskId: input.taskId,
|
|
222
|
+
readAt: input.now.toISOString(), counts, sessions,
|
|
223
|
+
coverage: "Selected Sessions and retained, exactly fenced child observations only; not a live process or complete historical Session inventory." };
|
|
224
|
+
}
|
|
225
|
+
function backgroundForRuns(fence, since, groups) {
|
|
226
|
+
return [...groups].flatMap(([runId, events]) => {
|
|
227
|
+
const projection = projectRuntimeTaskEvents({ ...fence, ...(runId === undefined ? {} : { runId }) }, since, events);
|
|
228
|
+
return [
|
|
229
|
+
...Object.entries(projection.continuations)
|
|
230
|
+
.filter(([, child]) => child.execution !== "quiescent" || child.identityConflict)
|
|
231
|
+
.map(([id, child]) => ({ id: `${runId ?? "native"}/${id}`, runId: runId ?? null,
|
|
232
|
+
execution: child.identityConflict ? "unknown" : child.execution })),
|
|
233
|
+
// Drivers may emit both forms for one child. This fold is already
|
|
234
|
+
// fenced to Task/Role/Agent/driver/Session/Run; continuation is richer.
|
|
235
|
+
...Object.entries(projection.operations).filter(([id, operation]) => operation.kind === "subagent" && !Object.hasOwn(projection.continuations, id))
|
|
236
|
+
.map(([id]) => ({ id: `${runId ?? "native"}/operation:${id}`, runId: runId ?? null,
|
|
237
|
+
execution: "unsettled" }))
|
|
238
|
+
];
|
|
239
|
+
});
|
|
240
|
+
}
|
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
2
2
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
3
|
+
import { claimPending } from "../coordination/workMailbox.js";
|
|
3
4
|
export const LEADER_ATTENTION_REQUIRED_EVENT = "leader.attention-required";
|
|
5
|
+
/** Bound each Operator pass, retaining the exact unclaimed sequence suffix. */
|
|
6
|
+
export function claimOperatorAttention(mailbox, options) {
|
|
7
|
+
const pending = mailbox.pending;
|
|
8
|
+
if (pending === null || pending.requestCount <= 64)
|
|
9
|
+
return claimPending(mailbox, options);
|
|
10
|
+
// Operator producers append one unique source ref per signal. Mailbox merge
|
|
11
|
+
// preserves insertion order for refs and keys, including the supported
|
|
12
|
+
// default sequence keys used by Provider retry attention. Preserve those
|
|
13
|
+
// keys verbatim; they are valid current data, not a migration or repair.
|
|
14
|
+
if (pending.requestCount !== pending.dedupeKeys.length
|
|
15
|
+
|| pending.refs.length !== pending.requestCount
|
|
16
|
+
|| pending.toSequence - pending.fromSequence + 1 !== pending.requestCount
|
|
17
|
+
|| pending.dedupeKeys.some((key, index) => {
|
|
18
|
+
const ref = pending.refs[index];
|
|
19
|
+
return (ref.type !== "event" && ref.type !== "input")
|
|
20
|
+
|| key !== `operator-${ref.type}:${ref.taskId}:${ref.id}`
|
|
21
|
+
&& key !== `sequence:${pending.fromSequence + index}`;
|
|
22
|
+
})) {
|
|
23
|
+
throw new Error("Operator attention batch cannot be safely partitioned; inspect its source refs and sequence before retrying.");
|
|
24
|
+
}
|
|
25
|
+
const prefixKeys = pending.dedupeKeys.slice(0, 64);
|
|
26
|
+
const suffixKeys = pending.dedupeKeys.slice(64);
|
|
27
|
+
const through = pending.fromSequence + 63;
|
|
28
|
+
const prefix = { ...pending, toSequence: through, requestCount: 64,
|
|
29
|
+
dedupeKeys: prefixKeys, refs: pending.refs.slice(0, 64) };
|
|
30
|
+
const claimed = claimPending({ ...mailbox, pending: prefix }, {
|
|
31
|
+
...options, batchId: `operator:${prefix.fromSequence}-${prefix.toSequence}`
|
|
32
|
+
});
|
|
33
|
+
return { ...claimed, pending: { ...pending, fromSequence: through + 1,
|
|
34
|
+
requestCount: pending.requestCount - 64, dedupeKeys: suffixKeys,
|
|
35
|
+
refs: pending.refs.slice(64) } };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Fixed source admission at the durable event write, in the same transaction.
|
|
39
|
+
* The existing mailbox is the incremental restart cursor: periodic scheduling
|
|
40
|
+
* reads only outstanding refs, never re-scans or replays historical events.
|
|
41
|
+
* Context, idle observations and ordinary progress are deliberately absent.
|
|
42
|
+
*/
|
|
43
|
+
export function enqueueDurableOperatorFact(store, event) {
|
|
44
|
+
const now = new Date(event.createdAt);
|
|
45
|
+
if (event.type === "input.requested" && typeof event.payload.requestId === "string") {
|
|
46
|
+
enqueueWork(store, { kind: "operator" }, "input-requested", now, [{ type: "input", taskId: event.taskId, id: event.payload.requestId }], { source: "input-request", dedupeKey: `operator-input:${event.taskId}:${event.payload.requestId}` });
|
|
47
|
+
}
|
|
48
|
+
else if (["task.completed", "task.cancelled", "task.retired", "publication.recorded",
|
|
49
|
+
"publication.candidate-adopted", LEADER_ATTENTION_REQUIRED_EVENT].includes(event.type)) {
|
|
50
|
+
enqueueOperatorEvent(store, event, event.type.startsWith("task.") ? "task-terminal" : event.type, now);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
4
53
|
/** One event, one supervisor signal; the Leader wake is this mailbox's projection. */
|
|
5
54
|
export function enqueueSupervisorEvent(store, event, recipient, reason, now) {
|
|
6
55
|
if (store.getTask(event.taskId)?.status === "archived")
|