@relaycast/engine 11.1.1 → 12.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/node/index.d.ts +3 -3
- package/dist/adapters/node/index.d.ts.map +1 -1
- package/dist/adapters/node/index.js +2 -3
- package/dist/adapters/node/index.js.map +1 -1
- package/dist/adapters/node/kv.d.ts +0 -1
- package/dist/adapters/node/kv.d.ts.map +1 -1
- package/dist/adapters/node/kv.js +0 -17
- package/dist/adapters/node/kv.js.map +1 -1
- package/dist/adapters/node/presence.d.ts +18 -0
- package/dist/adapters/node/presence.d.ts.map +1 -1
- package/dist/adapters/node/presence.js +82 -0
- package/dist/adapters/node/presence.js.map +1 -1
- package/dist/adapters/node/realtime.d.ts +4 -0
- package/dist/adapters/node/realtime.d.ts.map +1 -1
- package/dist/adapters/node/realtime.js +32 -5
- package/dist/adapters/node/realtime.js.map +1 -1
- package/dist/db/migrations/0077_drop_workspace_plan.sql +4 -0
- package/dist/db/schema.d.ts +2 -21
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +0 -1
- package/dist/db/schema.js.map +1 -1
- package/dist/engine/a2a.d.ts +3 -0
- package/dist/engine/a2a.d.ts.map +1 -1
- package/dist/engine/a2a.js +1 -0
- package/dist/engine/a2a.js.map +1 -1
- package/dist/engine/action.d.ts +14 -9
- package/dist/engine/action.d.ts.map +1 -1
- package/dist/engine/action.js +335 -74
- package/dist/engine/action.js.map +1 -1
- package/dist/engine/agent.d.ts +9 -3
- package/dist/engine/agent.d.ts.map +1 -1
- package/dist/engine/agent.js +21 -3
- package/dist/engine/agent.js.map +1 -1
- package/dist/engine/delivery.d.ts.map +1 -1
- package/dist/engine/delivery.js +4 -0
- package/dist/engine/delivery.js.map +1 -1
- package/dist/engine/eventDelivery.d.ts +1 -1
- package/dist/engine/inboundWebhook.d.ts +10 -0
- package/dist/engine/inboundWebhook.d.ts.map +1 -1
- package/dist/engine/inboundWebhook.js +30 -4
- package/dist/engine/inboundWebhook.js.map +1 -1
- package/dist/engine/inbox.d.ts.map +1 -1
- package/dist/engine/inbox.js +17 -0
- package/dist/engine/inbox.js.map +1 -1
- package/dist/engine/invocationClaim.d.ts +32 -0
- package/dist/engine/invocationClaim.d.ts.map +1 -0
- package/dist/engine/invocationClaim.js +42 -0
- package/dist/engine/invocationClaim.js.map +1 -0
- package/dist/engine/messageMetadata.d.ts +2 -0
- package/dist/engine/messageMetadata.d.ts.map +1 -1
- package/dist/engine/messageMetadata.js +3 -2
- package/dist/engine/messageMetadata.js.map +1 -1
- package/dist/engine/node.d.ts +12 -2
- package/dist/engine/node.d.ts.map +1 -1
- package/dist/engine/node.js +83 -4
- package/dist/engine/node.js.map +1 -1
- package/dist/engine/presence.d.ts +173 -1
- package/dist/engine/presence.d.ts.map +1 -1
- package/dist/engine/presence.js +81 -4
- package/dist/engine/presence.js.map +1 -1
- package/dist/engine/registrationAdmission.d.ts +40 -0
- package/dist/engine/registrationAdmission.d.ts.map +1 -0
- package/dist/engine/registrationAdmission.js +78 -0
- package/dist/engine/registrationAdmission.js.map +1 -0
- package/dist/engine/taskInvocation.d.ts +1 -1
- package/dist/engine/workspace.d.ts +26 -3
- package/dist/engine/workspace.d.ts.map +1 -1
- package/dist/engine/workspace.js +108 -8
- package/dist/engine/workspace.js.map +1 -1
- package/dist/engine/workspaceDeliveryPolicy.d.ts +1 -2
- package/dist/engine/workspaceDeliveryPolicy.d.ts.map +1 -1
- package/dist/engine/workspaceDeliveryPolicy.js +2 -5
- package/dist/engine/workspaceDeliveryPolicy.js.map +1 -1
- package/dist/engine/workspaceDrain.d.ts +38 -0
- package/dist/engine/workspaceDrain.d.ts.map +1 -0
- package/dist/engine/workspaceDrain.js +202 -0
- package/dist/engine/workspaceDrain.js.map +1 -0
- package/dist/engine.d.ts +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +30 -2
- package/dist/engine.js.map +1 -1
- package/dist/entrypoints/node.d.ts +2 -2
- package/dist/entrypoints/node.d.ts.map +1 -1
- package/dist/entrypoints/node.js +1 -1
- package/dist/entrypoints/node.js.map +1 -1
- package/dist/env.d.ts +5 -5
- package/dist/env.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -6
- package/dist/index.js.map +1 -1
- package/dist/lib/admissionError.d.ts +11 -0
- package/dist/lib/admissionError.d.ts.map +1 -0
- package/dist/lib/admissionError.js +14 -0
- package/dist/lib/admissionError.js.map +1 -0
- package/dist/lib/engineDiagnostics.d.ts +26 -0
- package/dist/lib/engineDiagnostics.d.ts.map +1 -0
- package/dist/lib/engineDiagnostics.js +100 -0
- package/dist/lib/engineDiagnostics.js.map +1 -0
- package/dist/lib/httpError.d.ts.map +1 -1
- package/dist/lib/httpError.js +9 -4
- package/dist/lib/httpError.js.map +1 -1
- package/dist/middleware/admission.d.ts +49 -0
- package/dist/middleware/admission.d.ts.map +1 -0
- package/dist/middleware/admission.js +100 -0
- package/dist/middleware/admission.js.map +1 -0
- package/dist/middleware/rateLimit.d.ts +2 -0
- package/dist/middleware/rateLimit.d.ts.map +1 -1
- package/dist/middleware/rateLimit.js +29 -25
- package/dist/middleware/rateLimit.js.map +1 -1
- package/dist/ports/admission.d.ts +80 -0
- package/dist/ports/admission.d.ts.map +1 -0
- package/dist/ports/admission.js +2 -0
- package/dist/ports/admission.js.map +1 -0
- package/dist/ports/diagnostics.d.ts +42 -0
- package/dist/ports/diagnostics.d.ts.map +1 -0
- package/dist/ports/diagnostics.js +13 -0
- package/dist/ports/diagnostics.js.map +1 -0
- package/dist/ports/index.d.ts +18 -4
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js.map +1 -1
- package/dist/ports/kv.d.ts +1 -16
- package/dist/ports/kv.d.ts.map +1 -1
- package/dist/ports/presence.d.ts +14 -0
- package/dist/ports/presence.d.ts.map +1 -1
- package/dist/ports/realtime.d.ts +18 -0
- package/dist/ports/realtime.d.ts.map +1 -1
- package/dist/ports/realtime.js.map +1 -1
- package/dist/ports/workspaceDelivery.d.ts +3 -4
- package/dist/ports/workspaceDelivery.d.ts.map +1 -1
- package/dist/routes/a2a.d.ts.map +1 -1
- package/dist/routes/a2a.js +2 -0
- package/dist/routes/a2a.js.map +1 -1
- package/dist/routes/action.js +3 -0
- package/dist/routes/action.js.map +1 -1
- package/dist/routes/agent.d.ts.map +1 -1
- package/dist/routes/agent.js +5 -1
- package/dist/routes/agent.js.map +1 -1
- package/dist/routes/dm.js +3 -3
- package/dist/routes/dm.js.map +1 -1
- package/dist/routes/groupDm.js +3 -3
- package/dist/routes/groupDm.js.map +1 -1
- package/dist/routes/inboundWebhook.js +8 -0
- package/dist/routes/inboundWebhook.js.map +1 -1
- package/dist/routes/message.js +3 -3
- package/dist/routes/message.js.map +1 -1
- package/dist/routes/node.d.ts.map +1 -1
- package/dist/routes/node.js +2 -1
- package/dist/routes/node.js.map +1 -1
- package/dist/routes/thread.js +3 -3
- package/dist/routes/thread.js.map +1 -1
- package/dist/routes/workspace.js +10 -2
- package/dist/routes/workspace.js.map +1 -1
- package/package.json +3 -3
- package/dist/engine/usage.d.ts +0 -35
- package/dist/engine/usage.d.ts.map +0 -1
- package/dist/engine/usage.js +0 -59
- package/dist/engine/usage.js.map +0 -1
- package/dist/lib/planLimitError.d.ts +0 -26
- package/dist/lib/planLimitError.d.ts.map +0 -1
- package/dist/lib/planLimitError.js +0 -16
- package/dist/lib/planLimitError.js.map +0 -1
- package/dist/middleware/planLimits.d.ts +0 -36
- package/dist/middleware/planLimits.d.ts.map +0 -1
- package/dist/middleware/planLimits.js +0 -190
- package/dist/middleware/planLimits.js.map +0 -1
- package/dist/middleware/usageTracker.d.ts +0 -3
- package/dist/middleware/usageTracker.d.ts.map +0 -1
- package/dist/middleware/usageTracker.js +0 -11
- package/dist/middleware/usageTracker.js.map +0 -1
- package/dist/ports/entitlements.d.ts +0 -65
- package/dist/ports/entitlements.d.ts.map +0 -1
- package/dist/ports/entitlements.js +0 -2
- package/dist/ports/entitlements.js.map +0 -1
- package/dist/providers/static-entitlements.d.ts +0 -25
- package/dist/providers/static-entitlements.d.ts.map +0 -1
- package/dist/providers/static-entitlements.js +0 -55
- package/dist/providers/static-entitlements.js.map +0 -1
package/dist/engine/action.js
CHANGED
|
@@ -9,10 +9,12 @@ import { D1_SAFE_IN_QUERY_CHUNK_SIZE } from '../lib/queryChunks.js';
|
|
|
9
9
|
import { toFleetWireJson } from './deliveryWire.js';
|
|
10
10
|
import { emitAgentExitedEffects, emitInvocationCompletionEffects, fleetInvocationId, } from './invocationCompletion.js';
|
|
11
11
|
import { runAtomic, runAtomicWrites } from '../ports/database.js';
|
|
12
|
+
import { exclusiveClaimWrite, retryAfterAtMatches } from './invocationClaim.js';
|
|
12
13
|
import { claimSpawnNode, chooseNodeForAction, isNodeLive, releaseNodeCapacity, reserveNodeCapacity } from './placement.js';
|
|
13
14
|
import { DEFAULT_PROVIDER_NAME, capacityProviderName, getProvider, isProviderLive } from './nodeProvider.js';
|
|
14
15
|
import { AGENT_TOKEN_HASH_PATTERN } from '@relaycast/types';
|
|
15
16
|
import { createTaskState, expireTaskInvocation, expireTaskInvocations, taskExecution } from './taskInvocation.js';
|
|
17
|
+
import { withEngineOperation } from '../lib/engineDiagnostics.js';
|
|
16
18
|
const OPEN_INVOCATION_STATUSES = ['pending', 'dispatched', 'invoked'];
|
|
17
19
|
const REPLAYED_INVOCATION = Symbol('replayed-action-invocation');
|
|
18
20
|
export const ACTION_DISPATCH_TIMEOUT_MS = 30_000;
|
|
@@ -40,14 +42,13 @@ export const ACTION_HANDLER_UNREACHABLE_TTL_MS = 120_000;
|
|
|
40
42
|
export const PENDING_INVOCATION_MAX_AGE_MS = 72 * 60 * 60 * 1000;
|
|
41
43
|
/**
|
|
42
44
|
* Sleep between the never-dispatched sweep's candidate SELECT and its atomic
|
|
43
|
-
* UPDATE.
|
|
44
|
-
* `
|
|
45
|
-
*
|
|
46
|
-
* while the handler already has the frame.
|
|
47
|
-
* `dispatch_attempts = 0`, so once the
|
|
48
|
-
*
|
|
49
|
-
* gap
|
|
50
|
-
* still bounding sweep latency. Set to 0 in tests to keep them fast.
|
|
45
|
+
* UPDATE. Builtin spawn and agent-hosted sends claim before the frame, so
|
|
46
|
+
* `dispatch_attempts` is already set. Release and other unclaimed sends still
|
|
47
|
+
* record the attempt after the frame. A sweep landing in that gap could mark
|
|
48
|
+
* the row `never_dispatched_expired` while the handler already has the frame.
|
|
49
|
+
* The atomic UPDATE re-checks `dispatch_attempts = 0`, so once the attempt is
|
|
50
|
+
* recorded the row is excluded. This grace window (default 5s) covers that
|
|
51
|
+
* record gap. Set to 0 in tests to keep them fast.
|
|
51
52
|
*/
|
|
52
53
|
export const NEVER_DISPATCHED_SWEEP_GRACE_MS = 5_000;
|
|
53
54
|
const ACTION_RETRY_BACKOFF_MS = 5_000;
|
|
@@ -130,11 +131,10 @@ function nextRetryAfter(attempts) {
|
|
|
130
131
|
return new Date(Date.now() + backoff);
|
|
131
132
|
}
|
|
132
133
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* `dispatchedAt` — and the reschedule path cover drained invocations too.
|
|
134
|
+
* Live `dispatched` fields (`dispatchedAt`, `retryAfterAt`). The timeout sweep
|
|
135
|
+
* keys off `dispatchedAt`. A builtin spawn or agent-hosted claim writes them
|
|
136
|
+
* before the frame; offline-queue drain writes them when a queued frame is
|
|
137
|
+
* delivered.
|
|
138
138
|
*/
|
|
139
139
|
function dispatchedStateFields(opts = {}) {
|
|
140
140
|
return {
|
|
@@ -763,7 +763,33 @@ async function dispatchNodeProviderInvocation(args, options = {}) {
|
|
|
763
763
|
}
|
|
764
764
|
return { accepted, pending: true };
|
|
765
765
|
}
|
|
766
|
-
|
|
766
|
+
// A retry passes the generation it selected. Re-reading here would adopt a
|
|
767
|
+
// claim that won while this retry was placing, and both would send. Callers
|
|
768
|
+
// without a snapshot still read the row so a dispatched spawn does not fall
|
|
769
|
+
// back to the fresh-row defaults.
|
|
770
|
+
const suppliedClaim = args.claimStatus
|
|
771
|
+
? {
|
|
772
|
+
claimStatus: args.claimStatus,
|
|
773
|
+
observedRetryAfterAt: args.observedRetryAfterAt ?? null,
|
|
774
|
+
observedDispatchAttempts: args.observedDispatchAttempts ?? 0,
|
|
775
|
+
observedDispatchedNodeId: args.observedDispatchedNodeId ?? null,
|
|
776
|
+
}
|
|
777
|
+
: null;
|
|
778
|
+
const [claimRow] = suppliedClaim
|
|
779
|
+
? []
|
|
780
|
+
: await args.db
|
|
781
|
+
.select({
|
|
782
|
+
status: actionInvocations.status,
|
|
783
|
+
retryAfterAt: actionInvocations.retryAfterAt,
|
|
784
|
+
dispatchAttempts: actionInvocations.dispatchAttempts,
|
|
785
|
+
dispatchedNodeId: actionInvocations.dispatchedNodeId,
|
|
786
|
+
})
|
|
787
|
+
.from(actionInvocations)
|
|
788
|
+
.where(and(eq(actionInvocations.workspaceId, args.workspaceId), eq(actionInvocations.id, args.invocationId)));
|
|
789
|
+
return dispatchNodeInvocation({
|
|
790
|
+
...args,
|
|
791
|
+
...(suppliedClaim ?? (claimRow ? observedInvocationClaim(claimRow) : {})),
|
|
792
|
+
});
|
|
767
793
|
}
|
|
768
794
|
async function dispatchRelease(args) {
|
|
769
795
|
const input = recordInput(args.data.input);
|
|
@@ -1268,11 +1294,12 @@ async function dispatchSpawn(args) {
|
|
|
1268
1294
|
// Placement reserved capacity for an attempt that never landed. Return it
|
|
1269
1295
|
// first, then mark the row unsent: only the mark makes it drainable, so a
|
|
1270
1296
|
// drain never sees it while this send is in flight or still holding the
|
|
1271
|
-
// reservation.
|
|
1272
|
-
|
|
1297
|
+
// reservation. A thrown send has an unknown outcome, so it is not marked
|
|
1298
|
+
// and only the never-dispatched sweep ends it.
|
|
1299
|
+
if (!dispatched.accepted && !placement.queued && !dispatched.reservationStillHeld) {
|
|
1273
1300
|
await releaseNodeCapacity(args.db, args.workspaceId, nodeId);
|
|
1274
1301
|
}
|
|
1275
|
-
if (!dispatched.sent) {
|
|
1302
|
+
if (!dispatched.sent && !dispatched.sendOutcomeUnknown) {
|
|
1276
1303
|
await markUnsentNativeSpawn(args.db, args.workspaceId, invocation.id, nodeId);
|
|
1277
1304
|
}
|
|
1278
1305
|
return spawnResult(invocation, nodeId, dispatched);
|
|
@@ -1399,7 +1426,10 @@ export async function invokeAction(db, workspaceId, actionName, data, options =
|
|
|
1399
1426
|
invocationId,
|
|
1400
1427
|
});
|
|
1401
1428
|
}
|
|
1402
|
-
|
|
1429
|
+
// Destructive roster mutation. The input shape is not the guard: a payload
|
|
1430
|
+
// that already carries `name` and `delete_agent` still does not release
|
|
1431
|
+
// unless this caller set `authorizeBuiltinRelease`.
|
|
1432
|
+
if (!action && actionName === 'release' && options.authorizeBuiltinRelease) {
|
|
1403
1433
|
return dispatchRelease({
|
|
1404
1434
|
db,
|
|
1405
1435
|
registry: options.nodeConnections,
|
|
@@ -1557,6 +1587,21 @@ export async function invokeAction(db, workspaceId, actionName, data, options =
|
|
|
1557
1587
|
});
|
|
1558
1588
|
}
|
|
1559
1589
|
}
|
|
1590
|
+
// The agent CAS recorded dispatch before the frame. A handler can complete
|
|
1591
|
+
// that row before this response is built, and the ack returns that terminal row.
|
|
1592
|
+
if (dispatched.sent && dispatched.accepted) {
|
|
1593
|
+
const [current] = await db
|
|
1594
|
+
.select()
|
|
1595
|
+
.from(actionInvocations)
|
|
1596
|
+
.where(and(eq(actionInvocations.workspaceId, workspaceId), eq(actionInvocations.id, invocation.id)));
|
|
1597
|
+
if (current && !OPEN_INVOCATION_STATUSES.some((status) => status === current.status)) {
|
|
1598
|
+
return invocationAck(current, {
|
|
1599
|
+
actionName,
|
|
1600
|
+
handlerAgentId: action.handlerAgentId,
|
|
1601
|
+
handlerNodeId: handlerAgent.locationNodeId,
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1560
1605
|
if (!dispatched.sent) {
|
|
1561
1606
|
if (invocationId !== undefined) {
|
|
1562
1607
|
// A keyed claim must stay consistent with its replays. Re-read it so a
|
|
@@ -1839,7 +1884,16 @@ async function dispatchNodeAttempt(db, workspaceId, invocationId, nodeId, opts)
|
|
|
1839
1884
|
...(opts.targetActionId ? { actionId: opts.targetActionId } : {}),
|
|
1840
1885
|
...attemptFields,
|
|
1841
1886
|
})
|
|
1842
|
-
.where(and(eq(actionInvocations.workspaceId, workspaceId), eq(actionInvocations.id, invocationId),
|
|
1887
|
+
.where(and(eq(actionInvocations.workspaceId, workspaceId), eq(actionInvocations.id, invocationId), ...(opts.claim
|
|
1888
|
+
? [
|
|
1889
|
+
eq(actionInvocations.status, opts.claim.status),
|
|
1890
|
+
retryAfterAtMatches(opts.claim.retryAfterAt),
|
|
1891
|
+
eq(actionInvocations.dispatchAttempts, opts.claim.dispatchAttempts),
|
|
1892
|
+
...(opts.claim.spawnReservedAt
|
|
1893
|
+
? [eq(actionInvocations.spawnReservedAt, opts.claim.spawnReservedAt)]
|
|
1894
|
+
: []),
|
|
1895
|
+
]
|
|
1896
|
+
: [inArray(actionInvocations.status, OPEN_INVOCATION_STATUSES)]), ...(opts.expectedNodeLivenessRevision !== undefined
|
|
1843
1897
|
? [eq(actionInvocations.nodeLivenessRevision, opts.expectedNodeLivenessRevision)]
|
|
1844
1898
|
: []), ...(opts.expectedAction ? [
|
|
1845
1899
|
eq(actionInvocations.invocationOrigin, 'registered_action'),
|
|
@@ -1863,6 +1917,25 @@ async function dispatchNodeAttempt(db, workspaceId, invocationId, nodeId, opts)
|
|
|
1863
1917
|
.returning();
|
|
1864
1918
|
return !!updated;
|
|
1865
1919
|
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Drop a spawn claim whose frame was not accepted. The compare matches the
|
|
1922
|
+
* attempt count and deadline this claim wrote, so a failed older send cannot
|
|
1923
|
+
* reset a newer winner. Restore the observed attempt count, route, deadline,
|
|
1924
|
+
* and reservation token so the pending row is due for another sender.
|
|
1925
|
+
*/
|
|
1926
|
+
async function restoreUnsentSpawnClaim(db, workspaceId, invocationId, claim) {
|
|
1927
|
+
await db
|
|
1928
|
+
.update(actionInvocations)
|
|
1929
|
+
.set({
|
|
1930
|
+
status: 'pending',
|
|
1931
|
+
dispatchedAt: null,
|
|
1932
|
+
dispatchedNodeId: claim.restoreDispatchedNodeId,
|
|
1933
|
+
spawnReservedAt: claim.restoreSpawnReservedAt,
|
|
1934
|
+
retryAfterAt: claim.restoreRetryAfterAt,
|
|
1935
|
+
dispatchAttempts: claim.restoreDispatchAttempts,
|
|
1936
|
+
})
|
|
1937
|
+
.where(and(eq(actionInvocations.workspaceId, workspaceId), eq(actionInvocations.id, invocationId), inArray(actionInvocations.status, ['pending', 'dispatched']), eq(actionInvocations.dispatchAttempts, claim.dispatchAttempts), retryAfterAtMatches(claim.retryAfterAt)));
|
|
1938
|
+
}
|
|
1866
1939
|
function registeredActionDispatchPredicate(args) {
|
|
1867
1940
|
const handlerPredicate = args.handlerAgentId
|
|
1868
1941
|
? sql `AND ${actions.handlerAgentId} = ${args.handlerAgentId} AND ${actions.handlerNodeId} IS NULL`
|
|
@@ -2068,6 +2141,17 @@ async function dispatchNodeInvocation(args) {
|
|
|
2068
2141
|
return { accepted: false, pending: false, sent: false };
|
|
2069
2142
|
const connectedBefore = args.registry.isProviderConnected(args.workspaceId, args.nodeId, args.providerName);
|
|
2070
2143
|
const pending = !!args.pending || !connectedBefore;
|
|
2144
|
+
const claimStatus = args.claimStatus ?? 'pending';
|
|
2145
|
+
const observedRetryAfterAt = args.observedRetryAfterAt ?? null;
|
|
2146
|
+
const observedDispatchAttempts = args.observedDispatchAttempts ?? 0;
|
|
2147
|
+
const claimWrite = exclusiveClaimWrite({
|
|
2148
|
+
observedStatus: claimStatus,
|
|
2149
|
+
observedRetryAfterAt,
|
|
2150
|
+
observedDispatchAttempts,
|
|
2151
|
+
nextStatus: pending ? 'pending' : 'dispatched',
|
|
2152
|
+
nextRetryAfterAt: args.retryAfterAt ?? null,
|
|
2153
|
+
incrementAttempts: !args.skipIncrementAttempts,
|
|
2154
|
+
});
|
|
2071
2155
|
const expectedActionId = args.invocationOrigin === 'registered_action'
|
|
2072
2156
|
? (args.expectedActionId ?? args.actionId)
|
|
2073
2157
|
: null;
|
|
@@ -2097,38 +2181,127 @@ async function dispatchNodeInvocation(args) {
|
|
|
2097
2181
|
if (registeredNodeClaim?.taskState) {
|
|
2098
2182
|
frame.task_execution = taskExecution({ id: args.invocationId, ...registeredNodeClaim });
|
|
2099
2183
|
}
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2184
|
+
// Native spawn has no registered handoff. Claim before the frame so a
|
|
2185
|
+
// concurrent drain and sweep cannot both send. The claim matches the
|
|
2186
|
+
// observed status, so a dispatched timeout retry can win, and it changes
|
|
2187
|
+
// the attempt count or the deadline so a second scheduler loses. Success
|
|
2188
|
+
// does not update the row again.
|
|
2189
|
+
const nativeSpawnSend = isSpawnInvocation(args.action) && !args.agent && !registeredNodeClaim;
|
|
2190
|
+
let nativeSpawnClaimed = false;
|
|
2191
|
+
if (nativeSpawnSend) {
|
|
2192
|
+
try {
|
|
2193
|
+
nativeSpawnClaimed = await dispatchNodeAttempt(args.db, args.workspaceId, args.invocationId, args.nodeId, {
|
|
2194
|
+
pending,
|
|
2195
|
+
providerName: args.providerName,
|
|
2196
|
+
retryAfterAt: claimWrite.retryAfterAt,
|
|
2197
|
+
reservationHeld: args.reservationHeld,
|
|
2198
|
+
skipIncrementAttempts: args.skipIncrementAttempts,
|
|
2199
|
+
expectedNodeLivenessRevision: args.expectedNodeLivenessRevision,
|
|
2200
|
+
claim: {
|
|
2201
|
+
status: claimStatus,
|
|
2202
|
+
retryAfterAt: observedRetryAfterAt,
|
|
2203
|
+
dispatchAttempts: observedDispatchAttempts,
|
|
2204
|
+
...(args.preserveSpawnReservation && args.observedSpawnReservedAt
|
|
2205
|
+
? { spawnReservedAt: args.observedSpawnReservedAt }
|
|
2206
|
+
: {}),
|
|
2207
|
+
},
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
catch {
|
|
2211
|
+
// The claim write did not commit: no frame left, and the row has no
|
|
2212
|
+
// reservation marker for a later sweep to release. A false result lets
|
|
2213
|
+
// the caller return the slot it already reserved and leave the
|
|
2214
|
+
// invocation pending for another sender.
|
|
2215
|
+
return { accepted: false, pending: false, sent: false };
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
if (nativeSpawnSend && !nativeSpawnClaimed) {
|
|
2219
|
+
return { accepted: false, pending: false, sent: false };
|
|
2220
|
+
}
|
|
2221
|
+
const restoreSpawnClaim = () => restoreUnsentSpawnClaim(args.db, args.workspaceId, args.invocationId, {
|
|
2222
|
+
dispatchAttempts: claimWrite.dispatchAttempts,
|
|
2223
|
+
retryAfterAt: claimWrite.retryAfterAt,
|
|
2224
|
+
restoreRetryAfterAt: observedRetryAfterAt,
|
|
2225
|
+
restoreDispatchAttempts: observedDispatchAttempts,
|
|
2226
|
+
restoreDispatchedNodeId: args.observedDispatchedNodeId ?? null,
|
|
2227
|
+
restoreSpawnReservedAt: args.preserveSpawnReservation ? args.observedSpawnReservedAt ?? null : null,
|
|
2228
|
+
});
|
|
2229
|
+
// A failed restore leaves the marker set. Returning the slot as well lets
|
|
2230
|
+
// the timeout sweep release it a second time.
|
|
2231
|
+
const restoreSpawnClaimOrHold = async () => {
|
|
2232
|
+
try {
|
|
2233
|
+
await restoreSpawnClaim();
|
|
2234
|
+
return false;
|
|
2235
|
+
}
|
|
2236
|
+
catch {
|
|
2237
|
+
return true;
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
let sent = false;
|
|
2241
|
+
try {
|
|
2242
|
+
sent = args.agent && args.actionId
|
|
2112
2243
|
? await (args.registry.sendAuthorizedActionToProvider?.(args.workspaceId, args.nodeId, args.providerName, frame, {
|
|
2113
|
-
kind: '
|
|
2244
|
+
kind: 'agent-action-v1',
|
|
2114
2245
|
invocationId: args.invocationId,
|
|
2115
|
-
actionId:
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2246
|
+
actionId: args.actionId,
|
|
2247
|
+
handlerAgentId: args.agent.id,
|
|
2248
|
+
recordAttempt: !args.skipIncrementAttempts,
|
|
2249
|
+
pending,
|
|
2250
|
+
retryAfterAt: claimWrite.retryAfterAt,
|
|
2251
|
+
claimStatus,
|
|
2252
|
+
observedRetryAfterAt,
|
|
2253
|
+
observedDispatchAttempts,
|
|
2254
|
+
reservationHeld: !!args.reservationHeld,
|
|
2255
|
+
...(args.expectedNodeLivenessRevision !== undefined
|
|
2256
|
+
? { expectedNodeLivenessRevision: args.expectedNodeLivenessRevision }
|
|
2257
|
+
: {}),
|
|
2119
2258
|
}) ?? false)
|
|
2120
|
-
:
|
|
2259
|
+
: registeredNodeClaim
|
|
2121
2260
|
? await (args.registry.sendAuthorizedActionToProvider?.(args.workspaceId, args.nodeId, args.providerName, frame, {
|
|
2122
|
-
kind: '
|
|
2261
|
+
kind: 'registered-node-action-v2',
|
|
2123
2262
|
invocationId: args.invocationId,
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
? { expectedTokenHash: guardedReleaseHash }
|
|
2129
|
-
: { expectedAgentId: guardedReleaseAgentId }),
|
|
2263
|
+
actionId: registeredNodeClaim.actionId,
|
|
2264
|
+
dispatchAttempt: registeredNodeClaim.dispatchAttempts,
|
|
2265
|
+
invocationActionName: registeredNodeClaim.actionName,
|
|
2266
|
+
actionName: args.action,
|
|
2130
2267
|
}) ?? false)
|
|
2131
|
-
:
|
|
2268
|
+
: guardedReleaseHash || guardedReleaseAgentId
|
|
2269
|
+
? await (args.registry.sendAuthorizedActionToProvider?.(args.workspaceId, args.nodeId, args.providerName, frame, {
|
|
2270
|
+
kind: 'release-generation-v1',
|
|
2271
|
+
invocationId: args.invocationId,
|
|
2272
|
+
agentName: typeof args.input.name === 'string' ? args.input.name : '',
|
|
2273
|
+
...(guardedReleaseHash && guardedReleaseAgentId
|
|
2274
|
+
? { expectedTokenHash: guardedReleaseHash, expectedAgentId: guardedReleaseAgentId }
|
|
2275
|
+
: guardedReleaseHash
|
|
2276
|
+
? { expectedTokenHash: guardedReleaseHash }
|
|
2277
|
+
: { expectedAgentId: guardedReleaseAgentId }),
|
|
2278
|
+
}) ?? false)
|
|
2279
|
+
: await args.registry.sendToProvider(args.workspaceId, args.nodeId, args.providerName, frame);
|
|
2280
|
+
}
|
|
2281
|
+
catch (error) {
|
|
2282
|
+
// A thrown send never reached the false-result path. Restore only this
|
|
2283
|
+
// claim, then report a false result so the caller releases the capacity
|
|
2284
|
+
// it reserved. The outcome is unknown, so callers must not queue another
|
|
2285
|
+
// send. Registered-action crashes still propagate: their claim has to stay
|
|
2286
|
+
// in place for the keyed replay.
|
|
2287
|
+
if (nativeSpawnClaimed) {
|
|
2288
|
+
const reservationStillHeld = await restoreSpawnClaimOrHold();
|
|
2289
|
+
return {
|
|
2290
|
+
accepted: false,
|
|
2291
|
+
pending: false,
|
|
2292
|
+
sent: false,
|
|
2293
|
+
sendOutcomeUnknown: true,
|
|
2294
|
+
...(reservationStillHeld ? { reservationStillHeld: true } : {}),
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
throw error;
|
|
2298
|
+
}
|
|
2299
|
+
if (!sent && nativeSpawnClaimed) {
|
|
2300
|
+
const reservationStillHeld = await restoreSpawnClaimOrHold();
|
|
2301
|
+
if (reservationStillHeld) {
|
|
2302
|
+
return { accepted: false, pending: false, sent: false, reservationStillHeld: true };
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2132
2305
|
if (!sent && (guardedReleaseHash || guardedReleaseAgentId)) {
|
|
2133
2306
|
// An adapter compiled against the older owner-authorization contract may
|
|
2134
2307
|
// expose the method but reject the new proof kind. Settle any still-open
|
|
@@ -2157,10 +2330,11 @@ async function dispatchNodeInvocation(args) {
|
|
|
2157
2330
|
}
|
|
2158
2331
|
return { accepted: false, pending: false, sent: false };
|
|
2159
2332
|
}
|
|
2160
|
-
// Registered node-action state was committed by the
|
|
2161
|
-
//
|
|
2333
|
+
// Registered node-action and native-spawn state was committed by the
|
|
2334
|
+
// pre-send claim. Agent-action state was committed by the socket owner's
|
|
2335
|
+
// authorization CAS, then re-read below in case the handler already finished.
|
|
2162
2336
|
// Do not write either generation again after the frame is accepted.
|
|
2163
|
-
if (registeredNodeClaim) {
|
|
2337
|
+
if (registeredNodeClaim || nativeSpawnClaimed) {
|
|
2164
2338
|
return { accepted: true, pending, sent: true };
|
|
2165
2339
|
}
|
|
2166
2340
|
if (args.agent && args.actionId) {
|
|
@@ -2313,6 +2487,7 @@ export async function drainNodeInvocations(db, registry, workspaceId, nodeId, op
|
|
|
2313
2487
|
input: actionInvocations.input,
|
|
2314
2488
|
providerAcceptedAttempt: actionInvocations.providerAcceptedAttempt,
|
|
2315
2489
|
dispatchAttempts: actionInvocations.dispatchAttempts,
|
|
2490
|
+
retryAfterAt: actionInvocations.retryAfterAt,
|
|
2316
2491
|
spawnReservedAt: actionInvocations.spawnReservedAt,
|
|
2317
2492
|
dispatchedNodeId: actionInvocations.dispatchedNodeId,
|
|
2318
2493
|
dispatchedProvider: actionInvocations.dispatchedProvider,
|
|
@@ -2434,15 +2609,23 @@ export async function drainNodeInvocations(db, registry, workspaceId, nodeId, op
|
|
|
2434
2609
|
retryAfterAt: new Date(Date.now() + ACTION_DISPATCH_TIMEOUT_MS),
|
|
2435
2610
|
reservationHeld,
|
|
2436
2611
|
skipIncrementAttempts: row.dispatchedNodeId === nodeId,
|
|
2612
|
+
claimStatus: 'pending',
|
|
2613
|
+
observedRetryAfterAt: row.retryAfterAt,
|
|
2614
|
+
observedDispatchAttempts: row.dispatchAttempts,
|
|
2615
|
+
observedDispatchedNodeId: row.dispatchedNodeId,
|
|
2616
|
+
observedSpawnReservedAt: row.spawnReservedAt,
|
|
2617
|
+
preserveSpawnReservation: reservationHeld && !reservedForDrain,
|
|
2437
2618
|
});
|
|
2438
2619
|
if (dispatched.accepted) {
|
|
2439
2620
|
drained++;
|
|
2440
2621
|
}
|
|
2441
|
-
else if (reservedForDrain) {
|
|
2622
|
+
else if (reservedForDrain && !dispatched.reservationStillHeld) {
|
|
2442
2623
|
await releaseNodeCapacity(db, workspaceId, nodeId);
|
|
2443
2624
|
reservedForDrain = false;
|
|
2444
2625
|
}
|
|
2445
|
-
|
|
2626
|
+
// A thrown send may have delivered the frame. Leave an unsent spawn
|
|
2627
|
+
// unmarked so a later drain does not send it again.
|
|
2628
|
+
if (!dispatched.sent && !dispatched.sendOutcomeUnknown)
|
|
2446
2629
|
await remarkUnsent();
|
|
2447
2630
|
}
|
|
2448
2631
|
catch {
|
|
@@ -2459,6 +2642,16 @@ export async function drainNodeInvocations(db, registry, workspaceId, nodeId, op
|
|
|
2459
2642
|
}
|
|
2460
2643
|
return drained;
|
|
2461
2644
|
}
|
|
2645
|
+
function observedInvocationClaim(invocation) {
|
|
2646
|
+
if (invocation.status !== 'pending' && invocation.status !== 'dispatched' && invocation.status !== 'invoked')
|
|
2647
|
+
return {};
|
|
2648
|
+
return {
|
|
2649
|
+
claimStatus: invocation.status,
|
|
2650
|
+
observedRetryAfterAt: invocation.retryAfterAt ?? null,
|
|
2651
|
+
observedDispatchAttempts: invocation.dispatchAttempts ?? 0,
|
|
2652
|
+
observedDispatchedNodeId: invocation.dispatchedNodeId ?? null,
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2462
2655
|
export async function rescheduleNodeInvocation(db, registry, invocation, opts = {}) {
|
|
2463
2656
|
// Once the exact provider accepted this generation, pruning only clears its
|
|
2464
2657
|
// materialized action FK. It must not make timeout, disconnect, or inventory
|
|
@@ -2473,9 +2666,36 @@ export async function rescheduleNodeInvocation(db, registry, invocation, opts =
|
|
|
2473
2666
|
}
|
|
2474
2667
|
// Only release capacity the invocation actually holds. A shadowed spawn
|
|
2475
2668
|
// dropped its native reservation at dispatch (spawnReservedAt is null), so it
|
|
2476
|
-
// must not release capacity that now belongs to another spawn.
|
|
2669
|
+
// must not release capacity that now belongs to another spawn. Clear the
|
|
2670
|
+
// marker once so overlapping retries release that one hold, not every hold
|
|
2671
|
+
// on the node.
|
|
2477
2672
|
if (invocation.dispatchedNodeId && isSpawnInvocation(invocation.actionName) && invocation.spawnReservedAt) {
|
|
2478
|
-
|
|
2673
|
+
const heldAt = invocation.spawnReservedAt;
|
|
2674
|
+
const nodeId = invocation.dispatchedNodeId;
|
|
2675
|
+
const holdWhere = and(eq(actionInvocations.workspaceId, invocation.workspaceId), eq(actionInvocations.id, invocation.id), eq(actionInvocations.status, invocation.status), eq(actionInvocations.dispatchAttempts, invocation.dispatchAttempts ?? 0), retryAfterAtMatches(invocation.retryAfterAt ?? null), ...(opts.expectedNodeLivenessRevision !== undefined
|
|
2676
|
+
? [eq(actionInvocations.nodeLivenessRevision, opts.expectedNodeLivenessRevision)]
|
|
2677
|
+
: []));
|
|
2678
|
+
try {
|
|
2679
|
+
// Clear the marker and decrement together. A transaction rolls both
|
|
2680
|
+
// back when the counter update throws. A handle without one can commit
|
|
2681
|
+
// the clear first; the catch puts that marker back.
|
|
2682
|
+
await runAtomic(db, async (tx) => {
|
|
2683
|
+
const [releasedHold] = await tx
|
|
2684
|
+
.update(actionInvocations)
|
|
2685
|
+
.set({ spawnReservedAt: null })
|
|
2686
|
+
.where(and(holdWhere, eq(actionInvocations.spawnReservedAt, heldAt)))
|
|
2687
|
+
.returning({ id: actionInvocations.id });
|
|
2688
|
+
if (releasedHold)
|
|
2689
|
+
await releaseNodeCapacity(tx, invocation.workspaceId, nodeId);
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
catch (error) {
|
|
2693
|
+
await db
|
|
2694
|
+
.update(actionInvocations)
|
|
2695
|
+
.set({ spawnReservedAt: heldAt })
|
|
2696
|
+
.where(and(holdWhere, isNull(actionInvocations.spawnReservedAt)));
|
|
2697
|
+
throw error;
|
|
2698
|
+
}
|
|
2479
2699
|
}
|
|
2480
2700
|
// A shadowed spawn stays pinned to its node: while a `spawn:<harness>` shadow
|
|
2481
2701
|
// is registered there, native capacity for that harness is unreachable — even
|
|
@@ -2523,6 +2743,7 @@ export async function rescheduleNodeInvocation(db, registry, invocation, opts =
|
|
|
2523
2743
|
invocationOrigin: invocation.invocationOrigin,
|
|
2524
2744
|
retryAfterAt: opts.retryAfterAt ?? null,
|
|
2525
2745
|
expectedNodeLivenessRevision: opts.expectedNodeLivenessRevision,
|
|
2746
|
+
...observedInvocationClaim(invocation),
|
|
2526
2747
|
});
|
|
2527
2748
|
return dispatched.accepted;
|
|
2528
2749
|
}
|
|
@@ -2572,6 +2793,7 @@ export async function rescheduleNodeInvocation(db, registry, invocation, opts =
|
|
|
2572
2793
|
queue: target.queue,
|
|
2573
2794
|
reservationHeld: false,
|
|
2574
2795
|
expectedNodeLivenessRevision: opts.expectedNodeLivenessRevision,
|
|
2796
|
+
...observedInvocationClaim(invocation),
|
|
2575
2797
|
}, { failIfUnavailable: false });
|
|
2576
2798
|
if (dispatched.providerUnavailable) {
|
|
2577
2799
|
providerRejectedNodeIds.add(placement.node.id);
|
|
@@ -2589,15 +2811,32 @@ export async function rescheduleNodeInvocation(db, registry, invocation, opts =
|
|
|
2589
2811
|
const expectedActionId = invocation.invocationOrigin === 'registered_action'
|
|
2590
2812
|
? invocation.actionId
|
|
2591
2813
|
: null;
|
|
2814
|
+
const queuedClaim = invocation.status === 'pending' || invocation.status === 'dispatched' || invocation.status === 'invoked'
|
|
2815
|
+
? exclusiveClaimWrite({
|
|
2816
|
+
observedStatus: invocation.status,
|
|
2817
|
+
observedRetryAfterAt: invocation.retryAfterAt ?? null,
|
|
2818
|
+
observedDispatchAttempts: invocation.dispatchAttempts ?? 0,
|
|
2819
|
+
nextStatus: 'pending',
|
|
2820
|
+
nextRetryAfterAt: opts.retryAfterAt ?? null,
|
|
2821
|
+
incrementAttempts: true,
|
|
2822
|
+
})
|
|
2823
|
+
: null;
|
|
2592
2824
|
const accepted = await dispatchNodeAttempt(db, invocation.workspaceId, invocation.id, placement.node.id, {
|
|
2593
2825
|
providerName,
|
|
2594
2826
|
pending: true,
|
|
2595
|
-
retryAfterAt: opts.retryAfterAt ?? null,
|
|
2827
|
+
retryAfterAt: queuedClaim?.retryAfterAt ?? opts.retryAfterAt ?? null,
|
|
2596
2828
|
reservationHeld: false,
|
|
2597
2829
|
expectedAction: expectedActionId
|
|
2598
2830
|
? { id: expectedActionId, name: actionToSend }
|
|
2599
2831
|
: undefined,
|
|
2600
2832
|
targetActionId: target?.id,
|
|
2833
|
+
...(queuedClaim ? {
|
|
2834
|
+
claim: {
|
|
2835
|
+
status: invocation.status,
|
|
2836
|
+
retryAfterAt: invocation.retryAfterAt ?? null,
|
|
2837
|
+
dispatchAttempts: invocation.dispatchAttempts ?? 0,
|
|
2838
|
+
},
|
|
2839
|
+
} : {}),
|
|
2601
2840
|
expectedNodeLivenessRevision: opts.expectedNodeLivenessRevision,
|
|
2602
2841
|
});
|
|
2603
2842
|
if (!accepted && invocation.invocationOrigin === 'registered_action') {
|
|
@@ -2605,21 +2844,41 @@ export async function rescheduleNodeInvocation(db, registry, invocation, opts =
|
|
|
2605
2844
|
}
|
|
2606
2845
|
return accepted;
|
|
2607
2846
|
}
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2847
|
+
let dispatched;
|
|
2848
|
+
try {
|
|
2849
|
+
dispatched = await dispatchNodeInvocation({
|
|
2850
|
+
db,
|
|
2851
|
+
registry,
|
|
2852
|
+
workspaceId: invocation.workspaceId,
|
|
2853
|
+
invocationId: invocation.id,
|
|
2854
|
+
nodeId: placement.node.id,
|
|
2855
|
+
providerName,
|
|
2856
|
+
action: actionToSend,
|
|
2857
|
+
actionId: target?.id ?? null,
|
|
2858
|
+
expectedActionId: invocation.actionId,
|
|
2859
|
+
invocationOrigin: invocation.invocationOrigin,
|
|
2860
|
+
input,
|
|
2861
|
+
reservationHeld,
|
|
2862
|
+
retryAfterAt: opts.retryAfterAt ?? null,
|
|
2863
|
+
expectedNodeLivenessRevision: opts.expectedNodeLivenessRevision,
|
|
2864
|
+
...observedInvocationClaim(invocation),
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
catch (error) {
|
|
2868
|
+
// A throw before the claim commits has no marker, so return the
|
|
2869
|
+
// new slot. A restore failure returns reservationStillHeld instead
|
|
2870
|
+
// of throwing, and the check below leaves that slot in place.
|
|
2871
|
+
if (reservationHeld) {
|
|
2872
|
+
await releaseNodeCapacity(db, invocation.workspaceId, placement.node.id);
|
|
2873
|
+
}
|
|
2874
|
+
throw error;
|
|
2875
|
+
}
|
|
2876
|
+
// claimSpawnNode incremented reservedAgents for this attempt. Release
|
|
2877
|
+
// it when the pending claim loses or the send never starts. Leave it
|
|
2878
|
+
// when the reservation marker is still set.
|
|
2879
|
+
if (!dispatched.accepted && !dispatched.sent && reservationHeld && !dispatched.reservationStillHeld) {
|
|
2880
|
+
await releaseNodeCapacity(db, invocation.workspaceId, placement.node.id);
|
|
2881
|
+
}
|
|
2623
2882
|
return dispatched.accepted;
|
|
2624
2883
|
}
|
|
2625
2884
|
catch {
|
|
@@ -2642,7 +2901,7 @@ export async function rescheduleNodeInvocation(db, registry, invocation, opts =
|
|
|
2642
2901
|
spawnReservedAt: null,
|
|
2643
2902
|
retryAfterAt: opts.retryAfterAt ?? nextRetryAfter(invocation.dispatchAttempts + 1),
|
|
2644
2903
|
})
|
|
2645
|
-
.where(and(eq(actionInvocations.workspaceId, invocation.workspaceId), eq(actionInvocations.id, invocation.id),
|
|
2904
|
+
.where(and(eq(actionInvocations.workspaceId, invocation.workspaceId), eq(actionInvocations.id, invocation.id), eq(actionInvocations.status, invocation.status), eq(actionInvocations.dispatchAttempts, invocation.dispatchAttempts ?? 0), retryAfterAtMatches(invocation.retryAfterAt ?? null), ...(opts.expectedNodeLivenessRevision !== undefined
|
|
2646
2905
|
? [eq(actionInvocations.nodeLivenessRevision, opts.expectedNodeLivenessRevision)]
|
|
2647
2906
|
: [])));
|
|
2648
2907
|
return false;
|
|
@@ -2750,6 +3009,7 @@ export async function completeNodeInvocation(db, registry, workspaceId, nodeId,
|
|
|
2750
3009
|
spawnReservedAt: actionInvocations.spawnReservedAt,
|
|
2751
3010
|
attemptedNodeIds: actionInvocations.attemptedNodeIds,
|
|
2752
3011
|
dispatchAttempts: actionInvocations.dispatchAttempts,
|
|
3012
|
+
retryAfterAt: actionInvocations.retryAfterAt,
|
|
2753
3013
|
createdAt: actionInvocations.createdAt,
|
|
2754
3014
|
completedAt: actionInvocations.completedAt,
|
|
2755
3015
|
})
|
|
@@ -2866,11 +3126,9 @@ async function failNeverDispatchedExpiredInvocations(db, maxAgeMs, graceMs, comp
|
|
|
2866
3126
|
.where(and(eq(actionInvocations.status, 'pending'), eq(actionInvocations.dispatchAttempts, 0), lte(actionInvocations.createdAt, cutoff)));
|
|
2867
3127
|
if (rows.length === 0)
|
|
2868
3128
|
return;
|
|
2869
|
-
//
|
|
2870
|
-
//
|
|
2871
|
-
//
|
|
2872
|
-
// in-flight dispatch reliably invisible to the age sweep instead of racing
|
|
2873
|
-
// with it. See `NEVER_DISPATCHED_SWEEP_GRACE_MS`.
|
|
3129
|
+
// Unclaimed sends still record `dispatchAttempts` after the frame. Wait out
|
|
3130
|
+
// that gap, then the UPDATE's `dispatch_attempts = 0` re-check skips any row
|
|
3131
|
+
// the dispatcher has claimed. See `NEVER_DISPATCHED_SWEEP_GRACE_MS`.
|
|
2874
3132
|
if (graceMs > 0) {
|
|
2875
3133
|
await new Promise((resolve) => setTimeout(resolve, graceMs));
|
|
2876
3134
|
}
|
|
@@ -2916,6 +3174,9 @@ async function failNeverDispatchedInvocationRows(db, workspaceId, invocationIds,
|
|
|
2916
3174
|
.returning();
|
|
2917
3175
|
}
|
|
2918
3176
|
export async function sweepTimedOutInvocations(db, registry, opts = {}) {
|
|
3177
|
+
return withEngineOperation('invocations.sweep', undefined, () => sweepTimedOutInvocationsUnlabeled(db, registry, opts));
|
|
3178
|
+
}
|
|
3179
|
+
async function sweepTimedOutInvocationsUnlabeled(db, registry, opts) {
|
|
2919
3180
|
await expireTaskInvocations(db);
|
|
2920
3181
|
const sweepOpts = typeof opts === 'number' || opts === null ? {} : opts;
|
|
2921
3182
|
const timeoutMs = typeof opts === 'number' ? opts : sweepOpts.timeoutMs ?? ACTION_DISPATCH_TIMEOUT_MS;
|