@relaycast/engine 8.8.0 → 8.9.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/realtime.d.ts.map +1 -1
- package/dist/adapters/node/realtime.js +13 -4
- package/dist/adapters/node/realtime.js.map +1 -1
- package/dist/db/migrations/0053_agent_retention_indexes.sql +8 -0
- package/dist/db/migrations/0054_node_status_heartbeat_index.sql +8 -0
- package/dist/db/migrations/0055_session_event_idempotency.sql +8 -0
- package/dist/db/migrations/0056_session_event_status_completion.sql +38 -0
- package/dist/db/schema.d.ts +89 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +27 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/engine/action.d.ts +4 -0
- package/dist/engine/action.d.ts.map +1 -1
- package/dist/engine/action.js +104 -35
- package/dist/engine/action.js.map +1 -1
- package/dist/engine/agentRetention.d.ts +39 -0
- package/dist/engine/agentRetention.d.ts.map +1 -0
- package/dist/engine/agentRetention.js +117 -0
- package/dist/engine/agentRetention.js.map +1 -0
- package/dist/engine/delivery.d.ts.map +1 -1
- package/dist/engine/delivery.js +6 -1
- package/dist/engine/delivery.js.map +1 -1
- package/dist/engine/node.d.ts +51 -28
- package/dist/engine/node.d.ts.map +1 -1
- package/dist/engine/node.js +140 -10
- package/dist/engine/node.js.map +1 -1
- package/dist/engine/retention.d.ts +12 -0
- package/dist/engine/retention.d.ts.map +1 -1
- package/dist/engine/retention.js +3 -0
- package/dist/engine/retention.js.map +1 -1
- package/dist/engine/rowidRetention.d.ts +33 -0
- package/dist/engine/rowidRetention.d.ts.map +1 -0
- package/dist/engine/rowidRetention.js +190 -0
- package/dist/engine/rowidRetention.js.map +1 -0
- package/dist/engine/sessionEvent.d.ts +54 -1
- package/dist/engine/sessionEvent.d.ts.map +1 -1
- package/dist/engine/sessionEvent.js +195 -2
- package/dist/engine/sessionEvent.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/ports/realtime.d.ts +7 -2
- package/dist/ports/realtime.d.ts.map +1 -1
- package/dist/ports/realtime.js.map +1 -1
- package/dist/routes/agent.d.ts.map +1 -1
- package/dist/routes/agent.js +113 -10
- package/dist/routes/agent.js.map +1 -1
- package/dist/routes/deliveryRouting.d.ts +1 -0
- package/dist/routes/deliveryRouting.d.ts.map +1 -1
- package/dist/routes/deliveryRouting.js +8 -4
- package/dist/routes/deliveryRouting.js.map +1 -1
- package/dist/routes/node.js +39 -15
- package/dist/routes/node.js.map +1 -1
- package/package.json +3 -3
package/dist/engine/action.js
CHANGED
|
@@ -83,6 +83,7 @@ function isAcceptedDeletedRegisteredActionInvocation(invocation) {
|
|
|
83
83
|
&& invocation.providerAcceptedAttempt === invocation.dispatchAttempts;
|
|
84
84
|
}
|
|
85
85
|
const RELEASE_GENERATION_CONFLICT_CODE = 'agent_release_generation_conflict';
|
|
86
|
+
const RELEASE_IDENTITY_MISMATCH_CODE = 'agent_identity_mismatch';
|
|
86
87
|
function releaseExpectedTokenHash(input) {
|
|
87
88
|
const value = input.expected_token_hash;
|
|
88
89
|
if (value === undefined)
|
|
@@ -92,11 +93,21 @@ function releaseExpectedTokenHash(input) {
|
|
|
92
93
|
}
|
|
93
94
|
return value;
|
|
94
95
|
}
|
|
95
|
-
function
|
|
96
|
+
function releaseExpectedAgentId(input) {
|
|
97
|
+
const value = input.expected_agent_id;
|
|
98
|
+
if (value === undefined)
|
|
99
|
+
return null;
|
|
100
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
101
|
+
throw codedError('release action input.expected_agent_id must be a non-empty immutable agent id', 'invalid_release_request', 400);
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
function releaseGenerationStillCurrent(workspaceId, agentId, expectedTokenHash, expectedAgentId = null) {
|
|
96
106
|
return sql `EXISTS (
|
|
97
107
|
SELECT 1 FROM ${agents}
|
|
98
108
|
WHERE ${agents.workspaceId} = ${workspaceId}
|
|
99
109
|
AND ${agents.id} = ${agentId}
|
|
110
|
+
${expectedAgentId ? sql `AND ${agents.id} = ${expectedAgentId}` : sql ``}
|
|
100
111
|
${expectedTokenHash ? sql `AND ${agents.tokenHash} = ${expectedTokenHash}` : sql ``}
|
|
101
112
|
)`;
|
|
102
113
|
}
|
|
@@ -490,8 +501,10 @@ async function waitForInvocationReplayOutcome(db, workspaceId, invocation, optio
|
|
|
490
501
|
// replay shortcut, otherwise the same idempotency key changes 409 into 201.
|
|
491
502
|
if (isBuiltinReleaseInvocation(current)
|
|
492
503
|
&& current.status === 'failed'
|
|
493
|
-
&& current.error === RELEASE_GENERATION_CONFLICT_CODE) {
|
|
494
|
-
throw codedError(
|
|
504
|
+
&& (current.error === RELEASE_GENERATION_CONFLICT_CODE || current.error === RELEASE_IDENTITY_MISMATCH_CODE)) {
|
|
505
|
+
throw codedError(current.error === RELEASE_IDENTITY_MISMATCH_CODE
|
|
506
|
+
? 'Action invocation failed because the immutable agent identity changed'
|
|
507
|
+
: 'Action invocation failed because the release generation changed', current.error, 409);
|
|
495
508
|
}
|
|
496
509
|
// Registered node actions only enter action_deleted before the provider
|
|
497
510
|
// accepts the frame. Surface that durable result before the route snapshot
|
|
@@ -747,11 +760,43 @@ async function dispatchRelease(args) {
|
|
|
747
760
|
throw codedError('release action input.name is required', 'invalid_release_request', 400);
|
|
748
761
|
}
|
|
749
762
|
const expectedTokenHash = releaseExpectedTokenHash(input);
|
|
763
|
+
const expectedAgentId = releaseExpectedAgentId(input);
|
|
764
|
+
// Claim the operation before resolving the mutable name. A completed exact
|
|
765
|
+
// release tombstones/renames the row, so a lost-response replay must find
|
|
766
|
+
// this immutable invocation before it looks up the name again.
|
|
767
|
+
const exactClaim = expectedAgentId ? await createInvocation(args.db, args.workspaceId, null, {
|
|
768
|
+
input,
|
|
769
|
+
caller_id: args.data.caller_id,
|
|
770
|
+
caller_name: args.data.caller_name,
|
|
771
|
+
action_name: 'release',
|
|
772
|
+
invocation_id: args.invocationId ?? (args.idempotencyKey && (args.idempotencyActorId ?? args.data.caller_id)
|
|
773
|
+
? await idempotentInvocationId(args.workspaceId, args.idempotencyActorId ?? args.data.caller_id, 'release', args.idempotencyKey)
|
|
774
|
+
: undefined),
|
|
775
|
+
}) : null;
|
|
776
|
+
if (exactClaim?.replayed) {
|
|
777
|
+
const { invocation } = exactClaim;
|
|
778
|
+
const settled = await waitForInvocationReplayOutcome(args.db, args.workspaceId, invocation);
|
|
779
|
+
return markInvocationReplay(invocationAck(settled, { actionName: 'release' }));
|
|
780
|
+
}
|
|
750
781
|
const [agent] = await args.db
|
|
751
782
|
.select()
|
|
752
783
|
.from(agents)
|
|
753
|
-
.where(and(eq(agents.workspaceId, args.workspaceId), eq(agents.name, name), ...(expectedTokenHash ? [eq(agents.tokenHash, expectedTokenHash)] : [])));
|
|
784
|
+
.where(and(eq(agents.workspaceId, args.workspaceId), eq(agents.name, name), ...(expectedAgentId ? [eq(agents.id, expectedAgentId)] : []), ...(expectedTokenHash ? [eq(agents.tokenHash, expectedTokenHash)] : [])));
|
|
754
785
|
if (!agent) {
|
|
786
|
+
if (expectedAgentId) {
|
|
787
|
+
await args.db
|
|
788
|
+
.update(actionInvocations)
|
|
789
|
+
.set({ status: 'failed', error: RELEASE_IDENTITY_MISMATCH_CODE, completedAt: new Date() })
|
|
790
|
+
.where(and(eq(actionInvocations.workspaceId, args.workspaceId), eq(actionInvocations.id, exactClaim.invocation.id), inArray(actionInvocations.status, OPEN_INVOCATION_STATUSES)));
|
|
791
|
+
const [sameName] = await args.db
|
|
792
|
+
.select({ id: agents.id })
|
|
793
|
+
.from(agents)
|
|
794
|
+
.where(and(eq(agents.workspaceId, args.workspaceId), eq(agents.name, name)));
|
|
795
|
+
if (sameName) {
|
|
796
|
+
throw codedError(`Agent "${name}" no longer matches the expected immutable identity`, RELEASE_IDENTITY_MISMATCH_CODE, 409);
|
|
797
|
+
}
|
|
798
|
+
throw codedError(`Agent "${name}" with expected identity is absent`, RELEASE_IDENTITY_MISMATCH_CODE, 409);
|
|
799
|
+
}
|
|
755
800
|
if (expectedTokenHash) {
|
|
756
801
|
const [sameName] = await args.db
|
|
757
802
|
.select({ id: agents.id })
|
|
@@ -763,12 +808,17 @@ async function dispatchRelease(args) {
|
|
|
763
808
|
}
|
|
764
809
|
throw codedError(`Agent "${name}" not found`, 'agent_not_found', 404);
|
|
765
810
|
}
|
|
766
|
-
|
|
811
|
+
// Legacy requests retain their prior behavior: invalid names/generation
|
|
812
|
+
// guards fail before creating an invocation. Exact requests have already
|
|
813
|
+
// claimed so their post-release replay survives the tombstone rename.
|
|
814
|
+
const { invocation, replayed } = exactClaim ?? await createInvocation(args.db, args.workspaceId, null, {
|
|
767
815
|
input,
|
|
768
816
|
caller_id: args.data.caller_id,
|
|
769
817
|
caller_name: args.data.caller_name,
|
|
770
818
|
action_name: 'release',
|
|
771
|
-
invocation_id: args.invocationId
|
|
819
|
+
invocation_id: args.invocationId ?? (args.idempotencyKey && (args.idempotencyActorId ?? args.data.caller_id)
|
|
820
|
+
? await idempotentInvocationId(args.workspaceId, args.idempotencyActorId ?? args.data.caller_id, 'release', args.idempotencyKey)
|
|
821
|
+
: undefined),
|
|
772
822
|
});
|
|
773
823
|
if (replayed) {
|
|
774
824
|
const settled = await waitForInvocationReplayOutcome(args.db, args.workspaceId, invocation);
|
|
@@ -790,7 +840,7 @@ async function dispatchRelease(args) {
|
|
|
790
840
|
AND ${actionInvocations.id} = ${invocation.id}
|
|
791
841
|
AND ${actionInvocations.status} = 'completed'
|
|
792
842
|
)`;
|
|
793
|
-
const generationStillCurrent = releaseGenerationStillCurrent(args.workspaceId, agent.id, expectedTokenHash);
|
|
843
|
+
const generationStillCurrent = releaseGenerationStillCurrent(args.workspaceId, agent.id, expectedTokenHash, expectedAgentId);
|
|
794
844
|
const atomicExitNodeId = sql `COALESCE(
|
|
795
845
|
(
|
|
796
846
|
SELECT ${agentNodeBindings.nodeId}
|
|
@@ -830,10 +880,10 @@ async function dispatchRelease(args) {
|
|
|
830
880
|
.update(actionInvocations)
|
|
831
881
|
.set({
|
|
832
882
|
status: 'failed',
|
|
833
|
-
error: RELEASE_GENERATION_CONFLICT_CODE,
|
|
883
|
+
error: expectedAgentId ? RELEASE_IDENTITY_MISMATCH_CODE : RELEASE_GENERATION_CONFLICT_CODE,
|
|
834
884
|
completedAt,
|
|
835
885
|
})
|
|
836
|
-
.where(and(eq(actionInvocations.workspaceId, args.workspaceId), eq(actionInvocations.id, invocation.id), inArray(actionInvocations.status, OPEN_INVOCATION_STATUSES), expectedTokenHash ? sql `NOT (${generationStillCurrent})` : sql `0`))
|
|
886
|
+
.where(and(eq(actionInvocations.workspaceId, args.workspaceId), eq(actionInvocations.id, invocation.id), inArray(actionInvocations.status, OPEN_INVOCATION_STATUSES), expectedTokenHash || expectedAgentId ? sql `NOT (${generationStillCurrent})` : sql `0`))
|
|
837
887
|
.returning({ id: actionInvocations.id }));
|
|
838
888
|
// Win completion and snapshot the current binding before deactivating
|
|
839
889
|
// it. The returned node drives the response and agent.exited event, so a
|
|
@@ -910,12 +960,14 @@ async function dispatchRelease(args) {
|
|
|
910
960
|
})
|
|
911
961
|
.where(and(eq(agents.workspaceId, args.workspaceId), eq(agents.id, agent.id), ...(expectedTokenHash ? [eq(agents.tokenHash, expectedTokenHash)] : []), invocationCompleted)));
|
|
912
962
|
return writes;
|
|
913
|
-
}, expectedTokenHash ? { requireAtomic: true } : undefined);
|
|
963
|
+
}, expectedTokenHash || expectedAgentId ? { requireAtomic: true } : undefined);
|
|
914
964
|
const generationConflict = results[0];
|
|
915
965
|
const completed = results[1];
|
|
916
966
|
const completedExitNodeId = completed[0]?.handlerNodeId ?? null;
|
|
917
|
-
if (expectedTokenHash && (generationConflict.length > 0 || completed.length === 0)) {
|
|
918
|
-
throw codedError(
|
|
967
|
+
if ((expectedTokenHash || expectedAgentId) && (generationConflict.length > 0 || completed.length === 0)) {
|
|
968
|
+
throw codedError(expectedAgentId
|
|
969
|
+
? `Agent "${name}" no longer matches the expected immutable identity`
|
|
970
|
+
: `Agent "${name}" no longer matches the expected token generation`, expectedAgentId ? RELEASE_IDENTITY_MISMATCH_CODE : RELEASE_GENERATION_CONFLICT_CODE, 409);
|
|
919
971
|
}
|
|
920
972
|
// External completion effects belong after the durable atomic unit: an
|
|
921
973
|
// aborted local reap must never publish agent.exited.
|
|
@@ -941,14 +993,14 @@ async function dispatchRelease(args) {
|
|
|
941
993
|
};
|
|
942
994
|
const failClosed = async () => {
|
|
943
995
|
const completedAt = new Date();
|
|
944
|
-
if (expectedTokenHash) {
|
|
945
|
-
const generationStillCurrent = releaseGenerationStillCurrent(args.workspaceId, agent.id, expectedTokenHash);
|
|
996
|
+
if (expectedTokenHash || expectedAgentId) {
|
|
997
|
+
const generationStillCurrent = releaseGenerationStillCurrent(args.workspaceId, agent.id, expectedTokenHash, expectedAgentId);
|
|
946
998
|
const results = await runAtomicWrites(args.db, (writeDb) => [
|
|
947
999
|
writeDb
|
|
948
1000
|
.update(actionInvocations)
|
|
949
1001
|
.set({
|
|
950
1002
|
status: 'failed',
|
|
951
|
-
error: RELEASE_GENERATION_CONFLICT_CODE,
|
|
1003
|
+
error: expectedAgentId ? RELEASE_IDENTITY_MISMATCH_CODE : RELEASE_GENERATION_CONFLICT_CODE,
|
|
952
1004
|
completedAt,
|
|
953
1005
|
})
|
|
954
1006
|
.where(and(eq(actionInvocations.workspaceId, args.workspaceId), eq(actionInvocations.id, invocation.id), inArray(actionInvocations.status, OPEN_INVOCATION_STATUSES), sql `NOT (${generationStillCurrent})`))
|
|
@@ -961,7 +1013,9 @@ async function dispatchRelease(args) {
|
|
|
961
1013
|
], { requireAtomic: true });
|
|
962
1014
|
const generationConflict = results[0];
|
|
963
1015
|
if (generationConflict.length > 0) {
|
|
964
|
-
throw codedError(
|
|
1016
|
+
throw codedError(expectedAgentId
|
|
1017
|
+
? `Agent "${name}" no longer matches the expected immutable identity`
|
|
1018
|
+
: `Agent "${name}" no longer matches the expected token generation`, expectedAgentId ? RELEASE_IDENTITY_MISMATCH_CODE : RELEASE_GENERATION_CONFLICT_CODE, 409);
|
|
965
1019
|
}
|
|
966
1020
|
}
|
|
967
1021
|
else {
|
|
@@ -1008,20 +1062,22 @@ async function dispatchRelease(args) {
|
|
|
1008
1062
|
if (dispatched.settled) {
|
|
1009
1063
|
if (dispatched.settled.status === 'failed') {
|
|
1010
1064
|
const errorCode = dispatched.settled.error ?? 'handler_unavailable';
|
|
1011
|
-
throw codedError('Release invocation failed after provider dispatch', errorCode, errorCode === RELEASE_GENERATION_CONFLICT_CODE ? 409 : 503);
|
|
1065
|
+
throw codedError('Release invocation failed after provider dispatch', errorCode, errorCode === RELEASE_GENERATION_CONFLICT_CODE || errorCode === RELEASE_IDENTITY_MISMATCH_CODE ? 409 : 503);
|
|
1012
1066
|
}
|
|
1013
1067
|
return invocationAck(dispatched.settled, { actionName: 'release' });
|
|
1014
1068
|
}
|
|
1015
1069
|
// The provider can disconnect between the liveness check and send. Complete
|
|
1016
1070
|
// the DB lifecycle locally instead of creating an ownerless pending request.
|
|
1017
1071
|
if (!dispatched.accepted) {
|
|
1018
|
-
if (expectedTokenHash) {
|
|
1072
|
+
if (expectedTokenHash || expectedAgentId) {
|
|
1019
1073
|
const [settled] = await args.db
|
|
1020
1074
|
.select({ error: actionInvocations.error })
|
|
1021
1075
|
.from(actionInvocations)
|
|
1022
1076
|
.where(and(eq(actionInvocations.workspaceId, args.workspaceId), eq(actionInvocations.id, invocation.id)));
|
|
1023
|
-
if (settled?.error === RELEASE_GENERATION_CONFLICT_CODE) {
|
|
1024
|
-
throw codedError(
|
|
1077
|
+
if (settled?.error === RELEASE_GENERATION_CONFLICT_CODE || settled?.error === RELEASE_IDENTITY_MISMATCH_CODE) {
|
|
1078
|
+
throw codedError(expectedAgentId
|
|
1079
|
+
? `Agent "${name}" no longer matches the expected immutable identity`
|
|
1080
|
+
: `Agent "${name}" no longer matches the expected token generation`, expectedAgentId ? RELEASE_IDENTITY_MISMATCH_CODE : RELEASE_GENERATION_CONFLICT_CODE, 409);
|
|
1025
1081
|
}
|
|
1026
1082
|
if (settled?.error === 'node_dispatch_unavailable') {
|
|
1027
1083
|
throw codedError('Node adapter does not support generation-authorized release dispatch', 'node_dispatch_unavailable', 503);
|
|
@@ -1053,6 +1109,8 @@ export async function dispatchAgentRelease(db, workspaceId, data, options = {})
|
|
|
1053
1109
|
completionDeps: options.completionDeps,
|
|
1054
1110
|
workspaceId,
|
|
1055
1111
|
data,
|
|
1112
|
+
idempotencyKey: options.idempotencyKey,
|
|
1113
|
+
idempotencyActorId: options.idempotencyActorId,
|
|
1056
1114
|
});
|
|
1057
1115
|
}
|
|
1058
1116
|
function spawnResult(invocation, nodeId, dispatched) {
|
|
@@ -1532,12 +1590,13 @@ async function completeGuardedReleaseNodeInvocation(db, workspaceId, nodeId, pro
|
|
|
1532
1590
|
&& AGENT_TOKEN_HASH_PATTERN.test(persistedTokenHash)
|
|
1533
1591
|
? persistedTokenHash
|
|
1534
1592
|
: null;
|
|
1535
|
-
|
|
1593
|
+
const expectedAgentId = releaseExpectedAgentId(input);
|
|
1594
|
+
if (!name || (!expectedTokenHash && !expectedAgentId)) {
|
|
1536
1595
|
const [failed] = await db
|
|
1537
1596
|
.update(actionInvocations)
|
|
1538
1597
|
.set({
|
|
1539
1598
|
status: 'failed',
|
|
1540
|
-
error: RELEASE_GENERATION_CONFLICT_CODE,
|
|
1599
|
+
error: expectedAgentId ? RELEASE_IDENTITY_MISMATCH_CODE : RELEASE_GENERATION_CONFLICT_CODE,
|
|
1541
1600
|
completedAt: new Date(),
|
|
1542
1601
|
spawnReservedAt: null,
|
|
1543
1602
|
})
|
|
@@ -1548,13 +1607,13 @@ async function completeGuardedReleaseNodeInvocation(db, workspaceId, nodeId, pro
|
|
|
1548
1607
|
const [agent] = await db
|
|
1549
1608
|
.select()
|
|
1550
1609
|
.from(agents)
|
|
1551
|
-
.where(and(eq(agents.workspaceId, workspaceId), eq(agents.name, name)));
|
|
1610
|
+
.where(and(eq(agents.workspaceId, workspaceId), eq(agents.name, name), ...(expectedAgentId ? [eq(agents.id, expectedAgentId)] : [])));
|
|
1552
1611
|
if (!agent) {
|
|
1553
1612
|
const [failed] = await db
|
|
1554
1613
|
.update(actionInvocations)
|
|
1555
1614
|
.set({
|
|
1556
1615
|
status: 'failed',
|
|
1557
|
-
error: RELEASE_GENERATION_CONFLICT_CODE,
|
|
1616
|
+
error: expectedAgentId ? RELEASE_IDENTITY_MISMATCH_CODE : RELEASE_GENERATION_CONFLICT_CODE,
|
|
1558
1617
|
completedAt: new Date(),
|
|
1559
1618
|
spawnReservedAt: null,
|
|
1560
1619
|
})
|
|
@@ -1563,7 +1622,7 @@ async function completeGuardedReleaseNodeInvocation(db, workspaceId, nodeId, pro
|
|
|
1563
1622
|
return failed ? publicInvocation(failed) : null;
|
|
1564
1623
|
}
|
|
1565
1624
|
const completedAt = new Date();
|
|
1566
|
-
const generationStillCurrent = releaseGenerationStillCurrent(workspaceId, agent.id, expectedTokenHash);
|
|
1625
|
+
const generationStillCurrent = releaseGenerationStillCurrent(workspaceId, agent.id, expectedTokenHash, expectedAgentId);
|
|
1567
1626
|
const activeBindingStillCurrent = sql `EXISTS (
|
|
1568
1627
|
SELECT 1 FROM ${agentNodeBindings}
|
|
1569
1628
|
WHERE ${agentNodeBindings.workspaceId} = ${workspaceId}
|
|
@@ -1601,7 +1660,7 @@ async function completeGuardedReleaseNodeInvocation(db, workspaceId, nodeId, pro
|
|
|
1601
1660
|
.update(actionInvocations)
|
|
1602
1661
|
.set({
|
|
1603
1662
|
status: 'failed',
|
|
1604
|
-
error: RELEASE_GENERATION_CONFLICT_CODE,
|
|
1663
|
+
error: expectedAgentId ? RELEASE_IDENTITY_MISMATCH_CODE : RELEASE_GENERATION_CONFLICT_CODE,
|
|
1605
1664
|
completedAt,
|
|
1606
1665
|
spawnReservedAt: null,
|
|
1607
1666
|
})
|
|
@@ -1637,7 +1696,7 @@ async function completeGuardedReleaseNodeInvocation(db, workspaceId, nodeId, pro
|
|
|
1637
1696
|
})
|
|
1638
1697
|
.where(and(eq(actionInvocations.workspaceId, workspaceId), eq(actionInvocations.id, invocation.id), eq(actionInvocations.dispatchedNodeId, nodeId), eq(actionInvocations.dispatchedProvider, providerName), inArray(actionInvocations.status, OPEN_INVOCATION_STATUSES), generationStillCurrent))
|
|
1639
1698
|
.returning());
|
|
1640
|
-
const currentAgent = and(eq(agents.workspaceId, workspaceId), eq(agents.id, agent.id), eq(agents.name, name), eq(agents.tokenHash, expectedTokenHash), invocationCompleted);
|
|
1699
|
+
const currentAgent = and(eq(agents.workspaceId, workspaceId), eq(agents.id, agent.id), eq(agents.name, name), ...(expectedTokenHash ? [eq(agents.tokenHash, expectedTokenHash)] : []), invocationCompleted);
|
|
1641
1700
|
if (input.delete_agent === true) {
|
|
1642
1701
|
writes.push(writeDb
|
|
1643
1702
|
.update(agents)
|
|
@@ -2004,12 +2063,18 @@ async function dispatchNodeInvocation(args) {
|
|
|
2004
2063
|
const guardedReleaseHash = args.invocationOrigin === 'builtin' && isReleaseInvocation(args.action)
|
|
2005
2064
|
? releaseExpectedTokenHash(args.input)
|
|
2006
2065
|
: null;
|
|
2007
|
-
|
|
2066
|
+
const guardedReleaseAgentId = args.invocationOrigin === 'builtin' && isReleaseInvocation(args.action)
|
|
2067
|
+
? releaseExpectedAgentId(args.input)
|
|
2068
|
+
: null;
|
|
2069
|
+
const releaseConflictCode = guardedReleaseAgentId
|
|
2070
|
+
? RELEASE_IDENTITY_MISMATCH_CODE
|
|
2071
|
+
: RELEASE_GENERATION_CONFLICT_CODE;
|
|
2072
|
+
if (guardedReleaseHash || guardedReleaseAgentId) {
|
|
2008
2073
|
const name = typeof args.input.name === 'string' ? args.input.name : '';
|
|
2009
2074
|
const [current] = await args.db
|
|
2010
2075
|
.select({ id: agents.id })
|
|
2011
2076
|
.from(agents)
|
|
2012
|
-
.where(and(eq(agents.workspaceId, args.workspaceId), eq(agents.name, name), eq(agents.tokenHash, guardedReleaseHash), sql `EXISTS (
|
|
2077
|
+
.where(and(eq(agents.workspaceId, args.workspaceId), eq(agents.name, name), ...(guardedReleaseHash ? [eq(agents.tokenHash, guardedReleaseHash)] : []), ...(guardedReleaseAgentId ? [eq(agents.id, guardedReleaseAgentId)] : []), sql `EXISTS (
|
|
2013
2078
|
SELECT 1 FROM ${agentNodeBindings}
|
|
2014
2079
|
WHERE ${agentNodeBindings.workspaceId} = ${args.workspaceId}
|
|
2015
2080
|
AND ${agentNodeBindings.agentId} = ${agents.id}
|
|
@@ -2021,7 +2086,7 @@ async function dispatchNodeInvocation(args) {
|
|
|
2021
2086
|
.update(actionInvocations)
|
|
2022
2087
|
.set({
|
|
2023
2088
|
status: 'failed',
|
|
2024
|
-
error:
|
|
2089
|
+
error: releaseConflictCode,
|
|
2025
2090
|
completedAt: new Date(),
|
|
2026
2091
|
})
|
|
2027
2092
|
.where(and(eq(actionInvocations.workspaceId, args.workspaceId), eq(actionInvocations.id, args.invocationId), inArray(actionInvocations.status, OPEN_INVOCATION_STATUSES)));
|
|
@@ -2099,15 +2164,19 @@ async function dispatchNodeInvocation(args) {
|
|
|
2099
2164
|
invocationActionName: registeredNodeClaim.actionName,
|
|
2100
2165
|
actionName: args.action,
|
|
2101
2166
|
}) ?? false)
|
|
2102
|
-
: guardedReleaseHash
|
|
2167
|
+
: guardedReleaseHash || guardedReleaseAgentId
|
|
2103
2168
|
? await (args.registry.sendAuthorizedActionToProvider?.(args.workspaceId, args.nodeId, args.providerName, frame, {
|
|
2104
2169
|
kind: 'release-generation-v1',
|
|
2105
2170
|
invocationId: args.invocationId,
|
|
2106
2171
|
agentName: typeof args.input.name === 'string' ? args.input.name : '',
|
|
2107
|
-
|
|
2172
|
+
...(guardedReleaseHash && guardedReleaseAgentId
|
|
2173
|
+
? { expectedTokenHash: guardedReleaseHash, expectedAgentId: guardedReleaseAgentId }
|
|
2174
|
+
: guardedReleaseHash
|
|
2175
|
+
? { expectedTokenHash: guardedReleaseHash }
|
|
2176
|
+
: { expectedAgentId: guardedReleaseAgentId }),
|
|
2108
2177
|
}) ?? false)
|
|
2109
2178
|
: await args.registry.sendToProvider(args.workspaceId, args.nodeId, args.providerName, frame);
|
|
2110
|
-
if (!sent && guardedReleaseHash) {
|
|
2179
|
+
if (!sent && (guardedReleaseHash || guardedReleaseAgentId)) {
|
|
2111
2180
|
// An adapter compiled against the older owner-authorization contract may
|
|
2112
2181
|
// expose the method but reject the new proof kind. Settle any still-open
|
|
2113
2182
|
// row before the caller considers local fallback, so a guarded delete can
|
|
@@ -2163,7 +2232,7 @@ async function dispatchNodeInvocation(args) {
|
|
|
2163
2232
|
const settled = await settleDeletedRegisteredAction(args.db, args.workspaceId, args.invocationId);
|
|
2164
2233
|
return { accepted: false, pending: false, sent: true, settled };
|
|
2165
2234
|
}
|
|
2166
|
-
if (!accepted && guardedReleaseHash) {
|
|
2235
|
+
if (!accepted && (guardedReleaseHash || guardedReleaseAgentId)) {
|
|
2167
2236
|
const [settled] = await args.db
|
|
2168
2237
|
.select()
|
|
2169
2238
|
.from(actionInvocations)
|
|
@@ -2637,7 +2706,7 @@ export async function completeNodeInvocation(db, registry, workspaceId, nodeId,
|
|
|
2637
2706
|
const existingInput = recordInput(existing.input);
|
|
2638
2707
|
if (!data.error
|
|
2639
2708
|
&& isBuiltinReleaseInvocation(existing)
|
|
2640
|
-
&& existingInput.expected_token_hash !== undefined) {
|
|
2709
|
+
&& (existingInput.expected_token_hash !== undefined || existingInput.expected_agent_id !== undefined)) {
|
|
2641
2710
|
return completeGuardedReleaseNodeInvocation(db, workspaceId, nodeId, providerName, existing, data, deps);
|
|
2642
2711
|
}
|
|
2643
2712
|
const [updated] = await db
|