@byok-sdk/cloud-dataplane 0.12.0 → 0.13.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/index.js +231 -29
- package/dist/index.js.map +1 -1
- package/dist/runtime.js +231 -29
- package/dist/runtime.js.map +1 -1
- package/dist/sql/0018_task_attempt_claimed_runtime.sql +23 -0
- package/dist/sql/0019_agent_ref_replay_keys.sql +36 -0
- package/dist/sql/0020_agent_ref_request_keys.sql +7 -0
- package/dist/stores/agent-egress.d.ts +2 -1
- package/dist/stores/approval-timeline.d.ts +2 -1
- package/dist/stores/dedup.d.ts +8 -5
- package/dist/stores/task-attempts.d.ts +8 -2
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import pg from 'pg';
|
|
2
|
-
import { DEDUP_RING_CAPACITY, NONCE_TTL_MS, validateActivityAppend, projectTimelineEvents, ByokCloudError, validateApprovalTimelineAppend, ApprovalObservationSchema, AllowAllRateLimiter, TruthCommitError, TruthCommitResponseSchema, truthRecordMetadata, TRUTH_REQUEST_ID_MAX_LENGTH, parseTimelineEvents, activityCursor, parseApprovalObservations, approvalTimelineCursor } from '@byok-sdk/cloud';
|
|
2
|
+
import { DEDUP_RING_CAPACITY, NONCE_TTL_MS, assertTaskAttemptListLimit, validateActivityAppend, projectTimelineEvents, activitySourceBatchState, ByokCloudError, validateApprovalTimelineAppend, ApprovalObservationSchema, validateApprovalTimelineResolve, AllowAllRateLimiter, TruthCommitError, TruthCommitResponseSchema, truthRecordMetadata, TRUTH_REQUEST_ID_MAX_LENGTH, parseTimelineEvents, activityCursor, parseApprovalObservations, approvalTimelineCursor } from '@byok-sdk/cloud';
|
|
3
3
|
import { contentHash, ByokCoreError, assertCanonicalTimestamp, isContentHash, tenantObjectKey, objectKeyPrefix, CoreConflictError, isLegalBoardTransition, checkSkillPackManifest, checkSkillPackEntry, SKILL_PACK_ENTRY_PATH, tenantId, SKILL_PACK_MANIFEST_SCHEMA_ID } from '@byok-sdk/core';
|
|
4
4
|
import { AwsClient } from 'aws4fetch';
|
|
5
5
|
import { XMLParser } from 'fast-xml-parser';
|
|
@@ -1038,23 +1038,31 @@ var PostgresInboundDedupStore = class {
|
|
|
1038
1038
|
this.#capacity = capacity;
|
|
1039
1039
|
}
|
|
1040
1040
|
async checkAndRecord(tenant, deviceId, envelopeId) {
|
|
1041
|
+
return this.#checkAndRecord(tenant, deviceId, "", "", envelopeId);
|
|
1042
|
+
}
|
|
1043
|
+
async checkAndRecordAgent(tenant, deviceId, agentRef, envelopeId) {
|
|
1044
|
+
return this.#checkAndRecord(tenant, deviceId, agentRef.agentId, agentRef.profileRevision, envelopeId);
|
|
1045
|
+
}
|
|
1046
|
+
async #checkAndRecord(tenant, deviceId, agentId, agentProfileRevision, envelopeId) {
|
|
1041
1047
|
const inserted = await this.#pool.query(
|
|
1042
|
-
`INSERT INTO inbound_dedup (tenant_id, device_id, envelope_id)
|
|
1043
|
-
VALUES ($1, $2, $3)
|
|
1044
|
-
ON CONFLICT (tenant_id, device_id, envelope_id) DO NOTHING
|
|
1048
|
+
`INSERT INTO inbound_dedup (tenant_id, device_id, agent_id, agent_profile_revision, envelope_id)
|
|
1049
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
1050
|
+
ON CONFLICT (tenant_id, device_id, agent_id, agent_profile_revision, envelope_id) DO NOTHING
|
|
1045
1051
|
RETURNING 1`,
|
|
1046
|
-
[tenant, deviceId, envelopeId]
|
|
1052
|
+
[tenant, deviceId, agentId, agentProfileRevision, envelopeId]
|
|
1047
1053
|
);
|
|
1048
1054
|
if (inserted.rowCount === 0) return true;
|
|
1049
1055
|
await this.#pool.query(
|
|
1050
1056
|
`DELETE FROM inbound_dedup
|
|
1051
1057
|
WHERE tenant_id = $1 AND device_id = $2
|
|
1058
|
+
AND agent_id = $3 AND agent_profile_revision = $4
|
|
1052
1059
|
AND recorded_seq <= (
|
|
1053
1060
|
SELECT recorded_seq FROM inbound_dedup
|
|
1054
1061
|
WHERE tenant_id = $1 AND device_id = $2
|
|
1062
|
+
AND agent_id = $3 AND agent_profile_revision = $4
|
|
1055
1063
|
ORDER BY recorded_seq DESC
|
|
1056
|
-
OFFSET $
|
|
1057
|
-
[tenant, deviceId, this.#capacity]
|
|
1064
|
+
OFFSET $5 LIMIT 1)`,
|
|
1065
|
+
[tenant, deviceId, agentId, agentProfileRevision, this.#capacity]
|
|
1058
1066
|
);
|
|
1059
1067
|
return false;
|
|
1060
1068
|
}
|
|
@@ -1348,7 +1356,7 @@ var PostgresProofRequestReceiptStore = class {
|
|
|
1348
1356
|
return row === void 0 ? void 0 : toReceipt2(row);
|
|
1349
1357
|
}
|
|
1350
1358
|
};
|
|
1351
|
-
var TASK_SELECT_COLUMNS = "tenant_id, task_id, device_id, agent_id, agent_profile_revision, owner_device_id, status, terminal_cause, cancel_requested_at, cancel_reason, cancel_message_id, updated_at";
|
|
1359
|
+
var TASK_SELECT_COLUMNS = "tenant_id, task_id, device_id, agent_id, agent_profile_revision, owner_device_id, status, terminal_cause, cancel_requested_at, cancel_reason, cancel_message_id, claimed_runtime, claimed_runtime_capabilities, updated_at";
|
|
1352
1360
|
var AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS = "message_id, payload_body, terminal_body";
|
|
1353
1361
|
function admissionRowToRecord(row) {
|
|
1354
1362
|
return {
|
|
@@ -1374,6 +1382,10 @@ function taskRowToAttempt(row) {
|
|
|
1374
1382
|
// former.
|
|
1375
1383
|
...row.owner_device_id === null ? {} : { ownerDeviceId: row.owner_device_id },
|
|
1376
1384
|
status: row.status,
|
|
1385
|
+
// `claimed_runtime` is written only by the guarded claim UPDATE below, so a
|
|
1386
|
+
// non-null value here is always the winning claim's own self-report.
|
|
1387
|
+
...row.claimed_runtime === null ? {} : { claimedRuntime: row.claimed_runtime },
|
|
1388
|
+
...row.claimed_runtime_capabilities === null ? {} : { claimedRuntimeCapabilities: row.claimed_runtime_capabilities },
|
|
1377
1389
|
...row.terminal_cause == null ? {} : { terminalCause: row.terminal_cause },
|
|
1378
1390
|
...row.cancel_requested_at === null ? {} : {
|
|
1379
1391
|
cancellation: {
|
|
@@ -1442,6 +1454,16 @@ var PostgresTaskAttemptStore = class {
|
|
|
1442
1454
|
[tenant, input.taskId]
|
|
1443
1455
|
);
|
|
1444
1456
|
const attempt = selected.rows[0];
|
|
1457
|
+
const existing = await client.query(
|
|
1458
|
+
`SELECT message_id, payload_body FROM agent_message_admission
|
|
1459
|
+
WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
|
|
1460
|
+
[tenant, input.deviceId, input.taskId]
|
|
1461
|
+
);
|
|
1462
|
+
const admission = existing.rows[0];
|
|
1463
|
+
if (admission !== void 0) {
|
|
1464
|
+
await client.query("COMMIT");
|
|
1465
|
+
return admission.message_id === input.messageId && admission.payload_body === input.payloadBody ? "pending" : "rejected";
|
|
1466
|
+
}
|
|
1445
1467
|
if (attempt === void 0 || attempt.device_id !== input.deviceId || attempt.cancel_requested_at !== null || !["offered", "claimed", "running"].includes(attempt.status)) {
|
|
1446
1468
|
await client.query("COMMIT");
|
|
1447
1469
|
return "rejected";
|
|
@@ -1458,14 +1480,14 @@ var PostgresTaskAttemptStore = class {
|
|
|
1458
1480
|
await client.query("COMMIT");
|
|
1459
1481
|
return "reserved";
|
|
1460
1482
|
}
|
|
1461
|
-
const
|
|
1483
|
+
const raced = await client.query(
|
|
1462
1484
|
`SELECT message_id, payload_body FROM agent_message_admission
|
|
1463
1485
|
WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
|
|
1464
1486
|
[tenant, input.deviceId, input.taskId]
|
|
1465
1487
|
);
|
|
1466
|
-
const
|
|
1488
|
+
const racedAdmission = raced.rows[0];
|
|
1467
1489
|
await client.query("COMMIT");
|
|
1468
|
-
return
|
|
1490
|
+
return racedAdmission?.message_id === input.messageId && racedAdmission.payload_body === input.payloadBody ? "pending" : "rejected";
|
|
1469
1491
|
} catch (cause) {
|
|
1470
1492
|
await client.query("ROLLBACK").catch(() => {
|
|
1471
1493
|
});
|
|
@@ -1512,14 +1534,44 @@ var PostgresTaskAttemptStore = class {
|
|
|
1512
1534
|
);
|
|
1513
1535
|
return result.rows.map(taskRowToAttempt);
|
|
1514
1536
|
}
|
|
1537
|
+
async list(tenant, query) {
|
|
1538
|
+
assertTaskAttemptListLimit(query.limit);
|
|
1539
|
+
const result = await this.#pool.query(
|
|
1540
|
+
`SELECT ${TASK_SELECT_COLUMNS}
|
|
1541
|
+
FROM task
|
|
1542
|
+
WHERE tenant_id = $1 AND ($2::text IS NULL OR task_id > $2)
|
|
1543
|
+
ORDER BY task_id
|
|
1544
|
+
LIMIT $3`,
|
|
1545
|
+
[tenant, query.cursor ?? null, query.limit + 1]
|
|
1546
|
+
);
|
|
1547
|
+
const rows = result.rows.slice(0, query.limit);
|
|
1548
|
+
const attempts = rows.map(taskRowToAttempt);
|
|
1549
|
+
const hasMore = result.rows.length > rows.length;
|
|
1550
|
+
const last = attempts.at(-1);
|
|
1551
|
+
return {
|
|
1552
|
+
attempts,
|
|
1553
|
+
...hasMore && last !== void 0 ? { nextCursor: last.taskId } : {}
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1515
1556
|
async claim(tenant, input) {
|
|
1516
1557
|
const claimed = await this.#pool.query(
|
|
1517
1558
|
`UPDATE task
|
|
1518
|
-
SET owner_device_id = $3,
|
|
1559
|
+
SET owner_device_id = $3,
|
|
1560
|
+
status = 'claimed',
|
|
1561
|
+
claimed_runtime = $5,
|
|
1562
|
+
claimed_runtime_capabilities = $6::jsonb,
|
|
1563
|
+
updated_at = $4
|
|
1519
1564
|
WHERE tenant_id = $1 AND task_id = $2 AND owner_device_id IS NULL
|
|
1520
1565
|
AND cancel_requested_at IS NULL AND status = 'offered'
|
|
1521
1566
|
RETURNING ${TASK_SELECT_COLUMNS}`,
|
|
1522
|
-
[
|
|
1567
|
+
[
|
|
1568
|
+
tenant,
|
|
1569
|
+
input.taskId,
|
|
1570
|
+
input.deviceId,
|
|
1571
|
+
this.#now(),
|
|
1572
|
+
input.runtime ?? null,
|
|
1573
|
+
input.capabilities === void 0 ? null : JSON.stringify(input.capabilities)
|
|
1574
|
+
]
|
|
1523
1575
|
);
|
|
1524
1576
|
const won = claimed.rows[0];
|
|
1525
1577
|
if (won !== void 0) return taskRowToAttempt(won);
|
|
@@ -1684,9 +1736,36 @@ var PostgresMailboxStore = class {
|
|
|
1684
1736
|
// Nothing above was mutated, so an identical call replays the same page.
|
|
1685
1737
|
// The returned position is a READ cursor and moves no ack.
|
|
1686
1738
|
nextSeq: page.at(-1)?.seq ?? query.afterSeq,
|
|
1687
|
-
hasMore: result.rows.length > page.length
|
|
1739
|
+
hasMore: result.rows.length > page.length,
|
|
1740
|
+
recoverableFrom: await this.#recoverableFrom(tenant, query.deviceId)
|
|
1688
1741
|
};
|
|
1689
1742
|
}
|
|
1743
|
+
/**
|
|
1744
|
+
* One past the highest row this device lost, which is exactly the highest
|
|
1745
|
+
* `expired` seq — `1` when it has lost nothing.
|
|
1746
|
+
*
|
|
1747
|
+
* No column and no migration, because `collectRetired` never deletes an
|
|
1748
|
+
* unacked row: §12.7.5 keeps the dead letter, so the loss is already durable
|
|
1749
|
+
* in `outbox` and a stored floor would be a second authority free to drift
|
|
1750
|
+
* from the sweep that moved it. Whatever eventually reaps dead letters (S4B,
|
|
1751
|
+
* O-009) is the change that has to carry this floor forward instead.
|
|
1752
|
+
*
|
|
1753
|
+
* `acked` rows are not counted: a consumed row is not a lost one.
|
|
1754
|
+
*
|
|
1755
|
+
* Read AFTER the page on purpose. A sweep landing between the two reads can
|
|
1756
|
+
* only report a floor HIGHER than the page it accompanies, which makes the
|
|
1757
|
+
* caller fail closed over a page that may already be missing rows; reading
|
|
1758
|
+
* the floor first would report one too low and serve that gap silently.
|
|
1759
|
+
*/
|
|
1760
|
+
async #recoverableFrom(tenant, deviceId) {
|
|
1761
|
+
const result = await this.#pool.query(
|
|
1762
|
+
`SELECT (COALESCE(MAX(seq), 0) + 1)::bigint AS recoverable_from
|
|
1763
|
+
FROM outbox
|
|
1764
|
+
WHERE tenant_id = $1 AND device_id = $2 AND state = 'expired'`,
|
|
1765
|
+
[tenant, deviceId]
|
|
1766
|
+
);
|
|
1767
|
+
return Number(result.rows[0].recoverable_from);
|
|
1768
|
+
}
|
|
1690
1769
|
async advanceCursor(tenant, input) {
|
|
1691
1770
|
this.#requireDeviceId(input.deviceId);
|
|
1692
1771
|
const now = this.#now();
|
|
@@ -1947,8 +2026,35 @@ var PostgresActivityStore = class {
|
|
|
1947
2026
|
const receivedAt = now.toISOString();
|
|
1948
2027
|
const incoming = projectTimelineEvents(input, receivedAt);
|
|
1949
2028
|
const expiresAt = new Date(now.getTime() + input.ttlMs).toISOString();
|
|
1950
|
-
const
|
|
1951
|
-
|
|
2029
|
+
const client = await this.pool.connect();
|
|
2030
|
+
try {
|
|
2031
|
+
await client.query("BEGIN");
|
|
2032
|
+
await client.query(
|
|
2033
|
+
`SELECT pg_advisory_xact_lock(hashtextextended($1 || E'\\x1f' || $2, 0))`,
|
|
2034
|
+
[tenant, input.taskId]
|
|
2035
|
+
);
|
|
2036
|
+
const locked = await client.query(
|
|
2037
|
+
`SELECT tenant_id, task_id, entries, dropped, capacity, expires_at
|
|
2038
|
+
FROM activity_tail
|
|
2039
|
+
WHERE tenant_id = $1 AND task_id = $2
|
|
2040
|
+
FOR UPDATE`,
|
|
2041
|
+
[tenant, input.taskId]
|
|
2042
|
+
);
|
|
2043
|
+
const stored = locked.rows[0];
|
|
2044
|
+
const live = stored !== void 0 && receivedAt < stored.expires_at ? toTail(stored) : void 0;
|
|
2045
|
+
const sourceState = activitySourceBatchState(live?.entries ?? [], input);
|
|
2046
|
+
if (sourceState === "same") {
|
|
2047
|
+
await client.query("COMMIT");
|
|
2048
|
+
return live;
|
|
2049
|
+
}
|
|
2050
|
+
if (sourceState === "conflict") {
|
|
2051
|
+
throw new ByokCloudError(
|
|
2052
|
+
"coordination_input_invalid",
|
|
2053
|
+
`Activity source envelope ${input.sourceEnvelopeId} already belongs to another canonical batch.`
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
const result = await client.query(
|
|
2057
|
+
`WITH incoming AS (
|
|
1952
2058
|
SELECT entry
|
|
1953
2059
|
FROM jsonb_array_elements($4::jsonb) AS element(entry)
|
|
1954
2060
|
), trimmed AS (
|
|
@@ -2015,16 +2121,24 @@ var PostgresActivityStore = class {
|
|
|
2015
2121
|
)
|
|
2016
2122
|
)
|
|
2017
2123
|
RETURNING tenant_id, task_id, entries, dropped, capacity, expires_at`,
|
|
2018
|
-
|
|
2019
|
-
);
|
|
2020
|
-
const row = result.rows[0];
|
|
2021
|
-
if (row === void 0) {
|
|
2022
|
-
throw new ByokCloudError(
|
|
2023
|
-
"coordination_input_invalid",
|
|
2024
|
-
`Activity batch ${input.batchSeq} conflicts with the existing typed tail authority.`
|
|
2124
|
+
[tenant, input.taskId, receivedAt, JSON.stringify(incoming), input.dropped, capacity, expiresAt]
|
|
2025
2125
|
);
|
|
2126
|
+
const row = result.rows[0];
|
|
2127
|
+
if (row === void 0) {
|
|
2128
|
+
throw new ByokCloudError(
|
|
2129
|
+
"coordination_input_invalid",
|
|
2130
|
+
`Activity batch ${input.batchSeq} conflicts with the existing typed tail authority.`
|
|
2131
|
+
);
|
|
2132
|
+
}
|
|
2133
|
+
await client.query("COMMIT");
|
|
2134
|
+
return toTail(row);
|
|
2135
|
+
} catch (caught) {
|
|
2136
|
+
await client.query("ROLLBACK").catch(() => {
|
|
2137
|
+
});
|
|
2138
|
+
throw caught;
|
|
2139
|
+
} finally {
|
|
2140
|
+
client.release();
|
|
2026
2141
|
}
|
|
2027
|
-
return toTail(row);
|
|
2028
2142
|
}
|
|
2029
2143
|
async read(tenant, taskId) {
|
|
2030
2144
|
const result = await this.pool.query(
|
|
@@ -2143,6 +2257,79 @@ var PostgresApprovalTimelineStore = class {
|
|
|
2143
2257
|
client.release();
|
|
2144
2258
|
}
|
|
2145
2259
|
}
|
|
2260
|
+
async resolvePending(tenant, input) {
|
|
2261
|
+
const { capacity, ttlMs, event } = validateApprovalTimelineResolve(input);
|
|
2262
|
+
const now = this.clock.now();
|
|
2263
|
+
const receivedAt = now.toISOString();
|
|
2264
|
+
const expiresAt = new Date(now.getTime() + ttlMs).toISOString();
|
|
2265
|
+
const client = await this.pool.connect();
|
|
2266
|
+
try {
|
|
2267
|
+
await client.query("BEGIN");
|
|
2268
|
+
await client.query(
|
|
2269
|
+
`SELECT pg_advisory_xact_lock(hashtextextended($1 || E'\\x1f' || $2, 0))`,
|
|
2270
|
+
[tenant, input.taskId]
|
|
2271
|
+
);
|
|
2272
|
+
const stored = await readLocked(client, tenant, input.taskId);
|
|
2273
|
+
const live = stored !== void 0 && receivedAt < stored.expires_at ? toTail2(stored) : void 0;
|
|
2274
|
+
if (live === void 0) {
|
|
2275
|
+
await client.query("COMMIT");
|
|
2276
|
+
return { status: "absent" };
|
|
2277
|
+
}
|
|
2278
|
+
const duplicate = live.entries.find((entry) => entry.sourceEnvelopeId === input.sourceEnvelopeId);
|
|
2279
|
+
if (duplicate !== void 0) {
|
|
2280
|
+
await client.query("COMMIT");
|
|
2281
|
+
return JSON.stringify(duplicate.event) === JSON.stringify(event) ? { status: "replayed", tail: live } : { status: "conflict", tail: live };
|
|
2282
|
+
}
|
|
2283
|
+
const pending = pendingObservation(live.entries);
|
|
2284
|
+
if (pending === void 0) {
|
|
2285
|
+
await client.query("COMMIT");
|
|
2286
|
+
return { status: "absent", tail: live };
|
|
2287
|
+
}
|
|
2288
|
+
if (pending.sourceEnvelopeId !== input.expectedSourceEnvelopeId || pending.revision !== input.expectedRevision) {
|
|
2289
|
+
await client.query("COMMIT");
|
|
2290
|
+
return { status: "superseded", tail: live };
|
|
2291
|
+
}
|
|
2292
|
+
const revision = Number(stored.next_revision);
|
|
2293
|
+
const expectedRevision = (live.cursor ?? 0) + 1;
|
|
2294
|
+
if (!Number.isSafeInteger(revision) || revision <= 0 || revision !== expectedRevision) {
|
|
2295
|
+
throw new ByokCloudError(
|
|
2296
|
+
"coordination_input_invalid",
|
|
2297
|
+
"Approval timeline revision authority is malformed."
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
const observation = ApprovalObservationSchema.parse({
|
|
2301
|
+
taskId: input.taskId,
|
|
2302
|
+
sourceEnvelopeId: input.sourceEnvelopeId,
|
|
2303
|
+
revision,
|
|
2304
|
+
receivedAt,
|
|
2305
|
+
event
|
|
2306
|
+
});
|
|
2307
|
+
const allEntries = [...live.entries, observation];
|
|
2308
|
+
const evicted = Math.max(allEntries.length - capacity, 0);
|
|
2309
|
+
const entries = allEntries.slice(evicted);
|
|
2310
|
+
const dropped = live.dropped + evicted;
|
|
2311
|
+
const result = await client.query(
|
|
2312
|
+
`INSERT INTO approval_timeline_tail
|
|
2313
|
+
(tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at)
|
|
2314
|
+
VALUES ($1, $2, $3::jsonb, $4, $5, $6, $7)
|
|
2315
|
+
ON CONFLICT (tenant_id, task_id) DO UPDATE
|
|
2316
|
+
SET entries = EXCLUDED.entries,
|
|
2317
|
+
next_revision = EXCLUDED.next_revision,
|
|
2318
|
+
dropped = EXCLUDED.dropped,
|
|
2319
|
+
capacity = EXCLUDED.capacity,
|
|
2320
|
+
expires_at = EXCLUDED.expires_at
|
|
2321
|
+
RETURNING tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at`,
|
|
2322
|
+
[tenant, input.taskId, JSON.stringify(entries), revision + 1, dropped, capacity, expiresAt]
|
|
2323
|
+
);
|
|
2324
|
+
await client.query("COMMIT");
|
|
2325
|
+
return { status: "applied", tail: toTail2(result.rows[0]) };
|
|
2326
|
+
} catch (caught) {
|
|
2327
|
+
await client.query("ROLLBACK");
|
|
2328
|
+
throw caught;
|
|
2329
|
+
} finally {
|
|
2330
|
+
client.release();
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2146
2333
|
async read(tenant, taskId) {
|
|
2147
2334
|
const result = await this.pool.query(
|
|
2148
2335
|
`SELECT tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at
|
|
@@ -2154,6 +2341,19 @@ var PostgresApprovalTimelineStore = class {
|
|
|
2154
2341
|
return row === void 0 ? void 0 : toTail2(row);
|
|
2155
2342
|
}
|
|
2156
2343
|
};
|
|
2344
|
+
function pendingObservation(entries) {
|
|
2345
|
+
let pending;
|
|
2346
|
+
for (const entry of entries) {
|
|
2347
|
+
if (entry.event.type === "approval_requested") {
|
|
2348
|
+
pending = entry;
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
if (pending !== void 0 && (pending.event.approvalId === void 0 || pending.event.approvalId === entry.event.approvalId)) {
|
|
2352
|
+
pending = void 0;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
return pending;
|
|
2356
|
+
}
|
|
2157
2357
|
var SELECT_COLUMNS4 = [
|
|
2158
2358
|
"tenant_id",
|
|
2159
2359
|
"device_id",
|
|
@@ -2200,7 +2400,7 @@ var PostgresAgentEgressStore = class {
|
|
|
2200
2400
|
const inserted = await this.pool.query(
|
|
2201
2401
|
`INSERT INTO agent_egress_event (${SELECT_COLUMNS4})
|
|
2202
2402
|
VALUES ($1, $2, $3::uuid, $4, $5, $6, $7, $8::bigint, $9::jsonb, $10, $11::integer, $12::uuid, $13)
|
|
2203
|
-
ON CONFLICT (tenant_id, device_id, event_id) DO NOTHING
|
|
2403
|
+
ON CONFLICT (tenant_id, device_id, agent_id, agent_profile_revision, event_id) DO NOTHING
|
|
2204
2404
|
RETURNING ${SELECT_COLUMNS4}`,
|
|
2205
2405
|
[
|
|
2206
2406
|
tenant,
|
|
@@ -2220,16 +2420,18 @@ var PostgresAgentEgressStore = class {
|
|
|
2220
2420
|
);
|
|
2221
2421
|
const row = inserted.rows[0];
|
|
2222
2422
|
if (row !== void 0) return { record: toRecord2(row), created: true };
|
|
2223
|
-
const existing = await this.get(tenant, input.deviceId, payload.eventId);
|
|
2423
|
+
const existing = await this.get(tenant, input.deviceId, payload.agentRef, payload.eventId);
|
|
2224
2424
|
if (existing === void 0) throw new Error(`Agent egress ${payload.eventId} vanished during first-write record.`);
|
|
2225
2425
|
return { record: existing, created: false };
|
|
2226
2426
|
}
|
|
2227
|
-
async get(tenant, deviceId, eventId) {
|
|
2427
|
+
async get(tenant, deviceId, agentRef, eventId) {
|
|
2228
2428
|
const result = await this.pool.query(
|
|
2229
2429
|
`SELECT ${SELECT_COLUMNS4}
|
|
2230
2430
|
FROM agent_egress_event
|
|
2231
|
-
WHERE tenant_id = $1 AND device_id = $2
|
|
2232
|
-
|
|
2431
|
+
WHERE tenant_id = $1 AND device_id = $2
|
|
2432
|
+
AND agent_id = $3 AND agent_profile_revision = $4
|
|
2433
|
+
AND event_id = $5::uuid`,
|
|
2434
|
+
[tenant, deviceId, agentRef.agentId, agentRef.profileRevision, eventId]
|
|
2233
2435
|
);
|
|
2234
2436
|
const row = result.rows[0];
|
|
2235
2437
|
return row === void 0 ? void 0 : toRecord2(row);
|