@byok-sdk/cloud-dataplane 0.12.0 → 0.14.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 +232 -30
- package/dist/index.js.map +1 -1
- package/dist/runtime.js +232 -30
- 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/runtime.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, SKILL_PACK_MANIFEST_SCHEMA_ID } from '@byok-sdk/core';
|
|
4
4
|
import { AwsClient } from 'aws4fetch';
|
|
5
5
|
import { XMLParser } from 'fast-xml-parser';
|
|
@@ -1034,23 +1034,31 @@ var PostgresInboundDedupStore = class {
|
|
|
1034
1034
|
this.#capacity = capacity;
|
|
1035
1035
|
}
|
|
1036
1036
|
async checkAndRecord(tenant, deviceId, envelopeId) {
|
|
1037
|
+
return this.#checkAndRecord(tenant, deviceId, "", "", envelopeId);
|
|
1038
|
+
}
|
|
1039
|
+
async checkAndRecordAgent(tenant, deviceId, agentRef, envelopeId) {
|
|
1040
|
+
return this.#checkAndRecord(tenant, deviceId, agentRef.agentId, agentRef.profileRevision, envelopeId);
|
|
1041
|
+
}
|
|
1042
|
+
async #checkAndRecord(tenant, deviceId, agentId, agentProfileRevision, envelopeId) {
|
|
1037
1043
|
const inserted = await this.#pool.query(
|
|
1038
|
-
`INSERT INTO inbound_dedup (tenant_id, device_id, envelope_id)
|
|
1039
|
-
VALUES ($1, $2, $3)
|
|
1040
|
-
ON CONFLICT (tenant_id, device_id, envelope_id) DO NOTHING
|
|
1044
|
+
`INSERT INTO inbound_dedup (tenant_id, device_id, agent_id, agent_profile_revision, envelope_id)
|
|
1045
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
1046
|
+
ON CONFLICT (tenant_id, device_id, agent_id, agent_profile_revision, envelope_id) DO NOTHING
|
|
1041
1047
|
RETURNING 1`,
|
|
1042
|
-
[tenant, deviceId, envelopeId]
|
|
1048
|
+
[tenant, deviceId, agentId, agentProfileRevision, envelopeId]
|
|
1043
1049
|
);
|
|
1044
1050
|
if (inserted.rowCount === 0) return true;
|
|
1045
1051
|
await this.#pool.query(
|
|
1046
1052
|
`DELETE FROM inbound_dedup
|
|
1047
1053
|
WHERE tenant_id = $1 AND device_id = $2
|
|
1054
|
+
AND agent_id = $3 AND agent_profile_revision = $4
|
|
1048
1055
|
AND recorded_seq <= (
|
|
1049
1056
|
SELECT recorded_seq FROM inbound_dedup
|
|
1050
1057
|
WHERE tenant_id = $1 AND device_id = $2
|
|
1058
|
+
AND agent_id = $3 AND agent_profile_revision = $4
|
|
1051
1059
|
ORDER BY recorded_seq DESC
|
|
1052
|
-
OFFSET $
|
|
1053
|
-
[tenant, deviceId, this.#capacity]
|
|
1060
|
+
OFFSET $5 LIMIT 1)`,
|
|
1061
|
+
[tenant, deviceId, agentId, agentProfileRevision, this.#capacity]
|
|
1054
1062
|
);
|
|
1055
1063
|
return false;
|
|
1056
1064
|
}
|
|
@@ -1344,7 +1352,7 @@ var PostgresProofRequestReceiptStore = class {
|
|
|
1344
1352
|
return row === void 0 ? void 0 : toReceipt2(row);
|
|
1345
1353
|
}
|
|
1346
1354
|
};
|
|
1347
|
-
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";
|
|
1355
|
+
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";
|
|
1348
1356
|
var AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS = "message_id, payload_body, terminal_body";
|
|
1349
1357
|
function admissionRowToRecord(row) {
|
|
1350
1358
|
return {
|
|
@@ -1370,6 +1378,10 @@ function taskRowToAttempt(row) {
|
|
|
1370
1378
|
// former.
|
|
1371
1379
|
...row.owner_device_id === null ? {} : { ownerDeviceId: row.owner_device_id },
|
|
1372
1380
|
status: row.status,
|
|
1381
|
+
// `claimed_runtime` is written only by the guarded claim UPDATE below, so a
|
|
1382
|
+
// non-null value here is always the winning claim's own self-report.
|
|
1383
|
+
...row.claimed_runtime === null ? {} : { claimedRuntime: row.claimed_runtime },
|
|
1384
|
+
...row.claimed_runtime_capabilities === null ? {} : { claimedRuntimeCapabilities: row.claimed_runtime_capabilities },
|
|
1373
1385
|
...row.terminal_cause == null ? {} : { terminalCause: row.terminal_cause },
|
|
1374
1386
|
...row.cancel_requested_at === null ? {} : {
|
|
1375
1387
|
cancellation: {
|
|
@@ -1438,6 +1450,16 @@ var PostgresTaskAttemptStore = class {
|
|
|
1438
1450
|
[tenant, input.taskId]
|
|
1439
1451
|
);
|
|
1440
1452
|
const attempt = selected.rows[0];
|
|
1453
|
+
const existing = await client.query(
|
|
1454
|
+
`SELECT message_id, payload_body FROM agent_message_admission
|
|
1455
|
+
WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
|
|
1456
|
+
[tenant, input.deviceId, input.taskId]
|
|
1457
|
+
);
|
|
1458
|
+
const admission = existing.rows[0];
|
|
1459
|
+
if (admission !== void 0) {
|
|
1460
|
+
await client.query("COMMIT");
|
|
1461
|
+
return admission.message_id === input.messageId && admission.payload_body === input.payloadBody ? "pending" : "rejected";
|
|
1462
|
+
}
|
|
1441
1463
|
if (attempt === void 0 || attempt.device_id !== input.deviceId || attempt.cancel_requested_at !== null || !["offered", "claimed", "running"].includes(attempt.status)) {
|
|
1442
1464
|
await client.query("COMMIT");
|
|
1443
1465
|
return "rejected";
|
|
@@ -1454,14 +1476,14 @@ var PostgresTaskAttemptStore = class {
|
|
|
1454
1476
|
await client.query("COMMIT");
|
|
1455
1477
|
return "reserved";
|
|
1456
1478
|
}
|
|
1457
|
-
const
|
|
1479
|
+
const raced = await client.query(
|
|
1458
1480
|
`SELECT message_id, payload_body FROM agent_message_admission
|
|
1459
1481
|
WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
|
|
1460
1482
|
[tenant, input.deviceId, input.taskId]
|
|
1461
1483
|
);
|
|
1462
|
-
const
|
|
1484
|
+
const racedAdmission = raced.rows[0];
|
|
1463
1485
|
await client.query("COMMIT");
|
|
1464
|
-
return
|
|
1486
|
+
return racedAdmission?.message_id === input.messageId && racedAdmission.payload_body === input.payloadBody ? "pending" : "rejected";
|
|
1465
1487
|
} catch (cause) {
|
|
1466
1488
|
await client.query("ROLLBACK").catch(() => {
|
|
1467
1489
|
});
|
|
@@ -1508,14 +1530,44 @@ var PostgresTaskAttemptStore = class {
|
|
|
1508
1530
|
);
|
|
1509
1531
|
return result.rows.map(taskRowToAttempt);
|
|
1510
1532
|
}
|
|
1533
|
+
async list(tenant, query) {
|
|
1534
|
+
assertTaskAttemptListLimit(query.limit);
|
|
1535
|
+
const result = await this.#pool.query(
|
|
1536
|
+
`SELECT ${TASK_SELECT_COLUMNS}
|
|
1537
|
+
FROM task
|
|
1538
|
+
WHERE tenant_id = $1 AND ($2::text IS NULL OR task_id > $2)
|
|
1539
|
+
ORDER BY task_id
|
|
1540
|
+
LIMIT $3`,
|
|
1541
|
+
[tenant, query.cursor ?? null, query.limit + 1]
|
|
1542
|
+
);
|
|
1543
|
+
const rows = result.rows.slice(0, query.limit);
|
|
1544
|
+
const attempts = rows.map(taskRowToAttempt);
|
|
1545
|
+
const hasMore = result.rows.length > rows.length;
|
|
1546
|
+
const last = attempts.at(-1);
|
|
1547
|
+
return {
|
|
1548
|
+
attempts,
|
|
1549
|
+
...hasMore && last !== void 0 ? { nextCursor: last.taskId } : {}
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1511
1552
|
async claim(tenant, input) {
|
|
1512
1553
|
const claimed = await this.#pool.query(
|
|
1513
1554
|
`UPDATE task
|
|
1514
|
-
SET owner_device_id = $3,
|
|
1515
|
-
|
|
1555
|
+
SET owner_device_id = $3,
|
|
1556
|
+
status = 'claimed',
|
|
1557
|
+
claimed_runtime = $5,
|
|
1558
|
+
claimed_runtime_capabilities = $6::jsonb,
|
|
1559
|
+
updated_at = $4
|
|
1560
|
+
WHERE tenant_id = $1 AND task_id = $2 AND device_id = $3 AND owner_device_id IS NULL
|
|
1516
1561
|
AND cancel_requested_at IS NULL AND status = 'offered'
|
|
1517
1562
|
RETURNING ${TASK_SELECT_COLUMNS}`,
|
|
1518
|
-
[
|
|
1563
|
+
[
|
|
1564
|
+
tenant,
|
|
1565
|
+
input.taskId,
|
|
1566
|
+
input.deviceId,
|
|
1567
|
+
this.#now(),
|
|
1568
|
+
input.runtime ?? null,
|
|
1569
|
+
input.capabilities === void 0 ? null : JSON.stringify(input.capabilities)
|
|
1570
|
+
]
|
|
1519
1571
|
);
|
|
1520
1572
|
const won = claimed.rows[0];
|
|
1521
1573
|
if (won !== void 0) return taskRowToAttempt(won);
|
|
@@ -1680,9 +1732,36 @@ var PostgresMailboxStore = class {
|
|
|
1680
1732
|
// Nothing above was mutated, so an identical call replays the same page.
|
|
1681
1733
|
// The returned position is a READ cursor and moves no ack.
|
|
1682
1734
|
nextSeq: page.at(-1)?.seq ?? query.afterSeq,
|
|
1683
|
-
hasMore: result.rows.length > page.length
|
|
1735
|
+
hasMore: result.rows.length > page.length,
|
|
1736
|
+
recoverableFrom: await this.#recoverableFrom(tenant, query.deviceId)
|
|
1684
1737
|
};
|
|
1685
1738
|
}
|
|
1739
|
+
/**
|
|
1740
|
+
* One past the highest row this device lost, which is exactly the highest
|
|
1741
|
+
* `expired` seq — `1` when it has lost nothing.
|
|
1742
|
+
*
|
|
1743
|
+
* No column and no migration, because `collectRetired` never deletes an
|
|
1744
|
+
* unacked row: §12.7.5 keeps the dead letter, so the loss is already durable
|
|
1745
|
+
* in `outbox` and a stored floor would be a second authority free to drift
|
|
1746
|
+
* from the sweep that moved it. Whatever eventually reaps dead letters (S4B,
|
|
1747
|
+
* O-009) is the change that has to carry this floor forward instead.
|
|
1748
|
+
*
|
|
1749
|
+
* `acked` rows are not counted: a consumed row is not a lost one.
|
|
1750
|
+
*
|
|
1751
|
+
* Read AFTER the page on purpose. A sweep landing between the two reads can
|
|
1752
|
+
* only report a floor HIGHER than the page it accompanies, which makes the
|
|
1753
|
+
* caller fail closed over a page that may already be missing rows; reading
|
|
1754
|
+
* the floor first would report one too low and serve that gap silently.
|
|
1755
|
+
*/
|
|
1756
|
+
async #recoverableFrom(tenant, deviceId) {
|
|
1757
|
+
const result = await this.#pool.query(
|
|
1758
|
+
`SELECT (COALESCE(MAX(seq), 0) + 1)::bigint AS recoverable_from
|
|
1759
|
+
FROM outbox
|
|
1760
|
+
WHERE tenant_id = $1 AND device_id = $2 AND state = 'expired'`,
|
|
1761
|
+
[tenant, deviceId]
|
|
1762
|
+
);
|
|
1763
|
+
return Number(result.rows[0].recoverable_from);
|
|
1764
|
+
}
|
|
1686
1765
|
async advanceCursor(tenant, input) {
|
|
1687
1766
|
this.#requireDeviceId(input.deviceId);
|
|
1688
1767
|
const now = this.#now();
|
|
@@ -1943,8 +2022,35 @@ var PostgresActivityStore = class {
|
|
|
1943
2022
|
const receivedAt = now.toISOString();
|
|
1944
2023
|
const incoming = projectTimelineEvents(input, receivedAt);
|
|
1945
2024
|
const expiresAt = new Date(now.getTime() + input.ttlMs).toISOString();
|
|
1946
|
-
const
|
|
1947
|
-
|
|
2025
|
+
const client = await this.pool.connect();
|
|
2026
|
+
try {
|
|
2027
|
+
await client.query("BEGIN");
|
|
2028
|
+
await client.query(
|
|
2029
|
+
`SELECT pg_advisory_xact_lock(hashtextextended($1 || E'\\x1f' || $2, 0))`,
|
|
2030
|
+
[tenant, input.taskId]
|
|
2031
|
+
);
|
|
2032
|
+
const locked = await client.query(
|
|
2033
|
+
`SELECT tenant_id, task_id, entries, dropped, capacity, expires_at
|
|
2034
|
+
FROM activity_tail
|
|
2035
|
+
WHERE tenant_id = $1 AND task_id = $2
|
|
2036
|
+
FOR UPDATE`,
|
|
2037
|
+
[tenant, input.taskId]
|
|
2038
|
+
);
|
|
2039
|
+
const stored = locked.rows[0];
|
|
2040
|
+
const live = stored !== void 0 && receivedAt < stored.expires_at ? toTail(stored) : void 0;
|
|
2041
|
+
const sourceState = activitySourceBatchState(live?.entries ?? [], input);
|
|
2042
|
+
if (sourceState === "same") {
|
|
2043
|
+
await client.query("COMMIT");
|
|
2044
|
+
return live;
|
|
2045
|
+
}
|
|
2046
|
+
if (sourceState === "conflict") {
|
|
2047
|
+
throw new ByokCloudError(
|
|
2048
|
+
"coordination_input_invalid",
|
|
2049
|
+
`Activity source envelope ${input.sourceEnvelopeId} already belongs to another canonical batch.`
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
const result = await client.query(
|
|
2053
|
+
`WITH incoming AS (
|
|
1948
2054
|
SELECT entry
|
|
1949
2055
|
FROM jsonb_array_elements($4::jsonb) AS element(entry)
|
|
1950
2056
|
), trimmed AS (
|
|
@@ -2011,16 +2117,24 @@ var PostgresActivityStore = class {
|
|
|
2011
2117
|
)
|
|
2012
2118
|
)
|
|
2013
2119
|
RETURNING tenant_id, task_id, entries, dropped, capacity, expires_at`,
|
|
2014
|
-
|
|
2015
|
-
);
|
|
2016
|
-
const row = result.rows[0];
|
|
2017
|
-
if (row === void 0) {
|
|
2018
|
-
throw new ByokCloudError(
|
|
2019
|
-
"coordination_input_invalid",
|
|
2020
|
-
`Activity batch ${input.batchSeq} conflicts with the existing typed tail authority.`
|
|
2120
|
+
[tenant, input.taskId, receivedAt, JSON.stringify(incoming), input.dropped, capacity, expiresAt]
|
|
2021
2121
|
);
|
|
2122
|
+
const row = result.rows[0];
|
|
2123
|
+
if (row === void 0) {
|
|
2124
|
+
throw new ByokCloudError(
|
|
2125
|
+
"coordination_input_invalid",
|
|
2126
|
+
`Activity batch ${input.batchSeq} conflicts with the existing typed tail authority.`
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
await client.query("COMMIT");
|
|
2130
|
+
return toTail(row);
|
|
2131
|
+
} catch (caught) {
|
|
2132
|
+
await client.query("ROLLBACK").catch(() => {
|
|
2133
|
+
});
|
|
2134
|
+
throw caught;
|
|
2135
|
+
} finally {
|
|
2136
|
+
client.release();
|
|
2022
2137
|
}
|
|
2023
|
-
return toTail(row);
|
|
2024
2138
|
}
|
|
2025
2139
|
async read(tenant, taskId) {
|
|
2026
2140
|
const result = await this.pool.query(
|
|
@@ -2139,6 +2253,79 @@ var PostgresApprovalTimelineStore = class {
|
|
|
2139
2253
|
client.release();
|
|
2140
2254
|
}
|
|
2141
2255
|
}
|
|
2256
|
+
async resolvePending(tenant, input) {
|
|
2257
|
+
const { capacity, ttlMs, event } = validateApprovalTimelineResolve(input);
|
|
2258
|
+
const now = this.clock.now();
|
|
2259
|
+
const receivedAt = now.toISOString();
|
|
2260
|
+
const expiresAt = new Date(now.getTime() + ttlMs).toISOString();
|
|
2261
|
+
const client = await this.pool.connect();
|
|
2262
|
+
try {
|
|
2263
|
+
await client.query("BEGIN");
|
|
2264
|
+
await client.query(
|
|
2265
|
+
`SELECT pg_advisory_xact_lock(hashtextextended($1 || E'\\x1f' || $2, 0))`,
|
|
2266
|
+
[tenant, input.taskId]
|
|
2267
|
+
);
|
|
2268
|
+
const stored = await readLocked(client, tenant, input.taskId);
|
|
2269
|
+
const live = stored !== void 0 && receivedAt < stored.expires_at ? toTail2(stored) : void 0;
|
|
2270
|
+
if (live === void 0) {
|
|
2271
|
+
await client.query("COMMIT");
|
|
2272
|
+
return { status: "absent" };
|
|
2273
|
+
}
|
|
2274
|
+
const duplicate = live.entries.find((entry) => entry.sourceEnvelopeId === input.sourceEnvelopeId);
|
|
2275
|
+
if (duplicate !== void 0) {
|
|
2276
|
+
await client.query("COMMIT");
|
|
2277
|
+
return JSON.stringify(duplicate.event) === JSON.stringify(event) ? { status: "replayed", tail: live } : { status: "conflict", tail: live };
|
|
2278
|
+
}
|
|
2279
|
+
const pending = pendingObservation(live.entries);
|
|
2280
|
+
if (pending === void 0) {
|
|
2281
|
+
await client.query("COMMIT");
|
|
2282
|
+
return { status: "absent", tail: live };
|
|
2283
|
+
}
|
|
2284
|
+
if (pending.sourceEnvelopeId !== input.expectedSourceEnvelopeId || pending.revision !== input.expectedRevision) {
|
|
2285
|
+
await client.query("COMMIT");
|
|
2286
|
+
return { status: "superseded", tail: live };
|
|
2287
|
+
}
|
|
2288
|
+
const revision = Number(stored.next_revision);
|
|
2289
|
+
const expectedRevision = (live.cursor ?? 0) + 1;
|
|
2290
|
+
if (!Number.isSafeInteger(revision) || revision <= 0 || revision !== expectedRevision) {
|
|
2291
|
+
throw new ByokCloudError(
|
|
2292
|
+
"coordination_input_invalid",
|
|
2293
|
+
"Approval timeline revision authority is malformed."
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
const observation = ApprovalObservationSchema.parse({
|
|
2297
|
+
taskId: input.taskId,
|
|
2298
|
+
sourceEnvelopeId: input.sourceEnvelopeId,
|
|
2299
|
+
revision,
|
|
2300
|
+
receivedAt,
|
|
2301
|
+
event
|
|
2302
|
+
});
|
|
2303
|
+
const allEntries = [...live.entries, observation];
|
|
2304
|
+
const evicted = Math.max(allEntries.length - capacity, 0);
|
|
2305
|
+
const entries = allEntries.slice(evicted);
|
|
2306
|
+
const dropped = live.dropped + evicted;
|
|
2307
|
+
const result = await client.query(
|
|
2308
|
+
`INSERT INTO approval_timeline_tail
|
|
2309
|
+
(tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at)
|
|
2310
|
+
VALUES ($1, $2, $3::jsonb, $4, $5, $6, $7)
|
|
2311
|
+
ON CONFLICT (tenant_id, task_id) DO UPDATE
|
|
2312
|
+
SET entries = EXCLUDED.entries,
|
|
2313
|
+
next_revision = EXCLUDED.next_revision,
|
|
2314
|
+
dropped = EXCLUDED.dropped,
|
|
2315
|
+
capacity = EXCLUDED.capacity,
|
|
2316
|
+
expires_at = EXCLUDED.expires_at
|
|
2317
|
+
RETURNING tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at`,
|
|
2318
|
+
[tenant, input.taskId, JSON.stringify(entries), revision + 1, dropped, capacity, expiresAt]
|
|
2319
|
+
);
|
|
2320
|
+
await client.query("COMMIT");
|
|
2321
|
+
return { status: "applied", tail: toTail2(result.rows[0]) };
|
|
2322
|
+
} catch (caught) {
|
|
2323
|
+
await client.query("ROLLBACK");
|
|
2324
|
+
throw caught;
|
|
2325
|
+
} finally {
|
|
2326
|
+
client.release();
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2142
2329
|
async read(tenant, taskId) {
|
|
2143
2330
|
const result = await this.pool.query(
|
|
2144
2331
|
`SELECT tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at
|
|
@@ -2150,6 +2337,19 @@ var PostgresApprovalTimelineStore = class {
|
|
|
2150
2337
|
return row === void 0 ? void 0 : toTail2(row);
|
|
2151
2338
|
}
|
|
2152
2339
|
};
|
|
2340
|
+
function pendingObservation(entries) {
|
|
2341
|
+
let pending;
|
|
2342
|
+
for (const entry of entries) {
|
|
2343
|
+
if (entry.event.type === "approval_requested") {
|
|
2344
|
+
pending = entry;
|
|
2345
|
+
continue;
|
|
2346
|
+
}
|
|
2347
|
+
if (pending !== void 0 && (pending.event.approvalId === void 0 || pending.event.approvalId === entry.event.approvalId)) {
|
|
2348
|
+
pending = void 0;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
return pending;
|
|
2352
|
+
}
|
|
2153
2353
|
var SELECT_COLUMNS4 = [
|
|
2154
2354
|
"tenant_id",
|
|
2155
2355
|
"device_id",
|
|
@@ -2196,7 +2396,7 @@ var PostgresAgentEgressStore = class {
|
|
|
2196
2396
|
const inserted = await this.pool.query(
|
|
2197
2397
|
`INSERT INTO agent_egress_event (${SELECT_COLUMNS4})
|
|
2198
2398
|
VALUES ($1, $2, $3::uuid, $4, $5, $6, $7, $8::bigint, $9::jsonb, $10, $11::integer, $12::uuid, $13)
|
|
2199
|
-
ON CONFLICT (tenant_id, device_id, event_id) DO NOTHING
|
|
2399
|
+
ON CONFLICT (tenant_id, device_id, agent_id, agent_profile_revision, event_id) DO NOTHING
|
|
2200
2400
|
RETURNING ${SELECT_COLUMNS4}`,
|
|
2201
2401
|
[
|
|
2202
2402
|
tenant,
|
|
@@ -2216,16 +2416,18 @@ var PostgresAgentEgressStore = class {
|
|
|
2216
2416
|
);
|
|
2217
2417
|
const row = inserted.rows[0];
|
|
2218
2418
|
if (row !== void 0) return { record: toRecord2(row), created: true };
|
|
2219
|
-
const existing = await this.get(tenant, input.deviceId, payload.eventId);
|
|
2419
|
+
const existing = await this.get(tenant, input.deviceId, payload.agentRef, payload.eventId);
|
|
2220
2420
|
if (existing === void 0) throw new Error(`Agent egress ${payload.eventId} vanished during first-write record.`);
|
|
2221
2421
|
return { record: existing, created: false };
|
|
2222
2422
|
}
|
|
2223
|
-
async get(tenant, deviceId, eventId) {
|
|
2423
|
+
async get(tenant, deviceId, agentRef, eventId) {
|
|
2224
2424
|
const result = await this.pool.query(
|
|
2225
2425
|
`SELECT ${SELECT_COLUMNS4}
|
|
2226
2426
|
FROM agent_egress_event
|
|
2227
|
-
WHERE tenant_id = $1 AND device_id = $2
|
|
2228
|
-
|
|
2427
|
+
WHERE tenant_id = $1 AND device_id = $2
|
|
2428
|
+
AND agent_id = $3 AND agent_profile_revision = $4
|
|
2429
|
+
AND event_id = $5::uuid`,
|
|
2430
|
+
[tenant, deviceId, agentRef.agentId, agentRef.profileRevision, eventId]
|
|
2229
2431
|
);
|
|
2230
2432
|
const row = result.rows[0];
|
|
2231
2433
|
return row === void 0 ? void 0 : toRecord2(row);
|