sentinelayer-cli 0.39.2 → 0.41.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/README.md +4 -4
- package/package.json +2 -3
- package/src/agents/ai-governance/tools/base.js +19 -166
- package/src/agents/backend/tools/base.js +33 -186
- package/src/agents/code-quality/tools/base.js +27 -154
- package/src/agents/data-layer/tools/base.js +35 -180
- package/src/agents/documentation/tools/base.js +18 -146
- package/src/agents/infrastructure/tools/base.js +17 -164
- package/src/agents/observability/tools/base.js +35 -180
- package/src/agents/performance/tools/base.js +35 -205
- package/src/agents/release/tools/base.js +35 -180
- package/src/agents/reliability/tools/base.js +35 -180
- package/src/agents/security/tools/base.js +33 -187
- package/src/agents/shared-tools/domain-base.js +246 -0
- package/src/agents/supply-chain/tools/base.js +15 -144
- package/src/agents/testing/tools/base.js +33 -184
- package/src/cli.js +5 -0
- package/src/commands/mcp.js +1 -1
- package/src/commands/ring-owner.js +58 -0
- package/src/commands/session.js +703 -263
- package/src/engram/governance.js +67 -0
- package/src/engram/index.js +64 -0
- package/src/engram/namespace.js +89 -0
- package/src/engram/observations.js +34 -0
- package/src/engram/sla.js +104 -0
- package/src/engram/store.js +115 -0
- package/src/engram/tools.js +146 -0
- package/src/engram/trust.js +105 -0
- package/src/legacy-cli.js +6 -3
- package/src/mcp/registry.js +27 -10
- package/src/mcp/session-stdio-server.js +209 -18
- package/src/mcp/smoke.js +9 -8
- package/src/pocket/ring-owner.js +97 -0
- package/src/review/local-review.js +9 -5
- package/src/review/omargate-orchestrator.js +111 -14
- package/src/review/spec-binding.js +16 -1
- package/src/scan/generator.js +1 -1
- package/src/session/agent-registry.js +22 -63
- package/src/session/checkpoints.js +194 -0
- package/src/session/coordination-guidance.js +6 -6
- package/src/session/daemon.js +19 -124
- package/src/session/file-lease-integrations.js +1300 -0
- package/src/session/file-locks.js +967 -464
- package/src/session/listener.js +243 -20
- package/src/session/listeners.js +71 -80
- package/src/session/paths.js +2 -0
- package/src/session/recall/embedder.js +181 -0
- package/src/session/recall/entities.js +0 -0
- package/src/session/recall/index-build.js +38 -0
- package/src/session/recall/index-core.js +108 -0
- package/src/session/recall/index.js +131 -0
- package/src/session/recall/needle-eval.js +279 -0
- package/src/session/recall/observation-core.js +183 -0
- package/src/session/recall/observations.js +21 -0
- package/src/session/recall/provenance.js +153 -0
- package/src/session/recall/retrieve.js +374 -0
- package/src/session/recall/text.js +0 -0
- package/src/session/recall/token-stats.js +81 -0
- package/src/session/recap.js +29 -23
- package/src/session/remote-hydrate.js +5 -1
- package/src/session/sync.js +661 -11
- package/src/session/wake/pump.js +8 -3
package/src/session/sync.js
CHANGED
|
@@ -2,9 +2,11 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
5
6
|
|
|
6
7
|
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
7
8
|
import { createAgentEvent } from "../events/schema.js";
|
|
9
|
+
import { isSessionControlEvent } from "./control-events.js";
|
|
8
10
|
|
|
9
11
|
const DEFAULT_API_BASE_URL = "https://api.sentinelayer.com";
|
|
10
12
|
const DEFAULT_SYNC_TIMEOUT_MS = 5_000;
|
|
@@ -17,6 +19,26 @@ const HUMAN_MESSAGE_FETCH_LIMIT = 50;
|
|
|
17
19
|
const SESSION_EVENT_FETCH_LIMIT = 200;
|
|
18
20
|
const SESSION_ACTION_FETCH_LIMIT = 500;
|
|
19
21
|
const SESSION_SEARCH_FETCH_LIMIT = 50;
|
|
22
|
+
const NON_SEMANTIC_TRANSCRIPT_EVENT_TYPES = new Set([
|
|
23
|
+
"agent_heartbeat",
|
|
24
|
+
"agent_identified",
|
|
25
|
+
"agent_identity",
|
|
26
|
+
"agent_join",
|
|
27
|
+
"agent_leave",
|
|
28
|
+
"agent_left",
|
|
29
|
+
"agent_status",
|
|
30
|
+
"context_briefing",
|
|
31
|
+
"session_recap",
|
|
32
|
+
"session_view",
|
|
33
|
+
"view",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function isNonSemanticTranscriptEvent(event, eventType) {
|
|
37
|
+
return (
|
|
38
|
+
NON_SEMANTIC_TRANSCRIPT_EVENT_TYPES.has(eventType) ||
|
|
39
|
+
isSessionControlEvent(event)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
20
42
|
|
|
21
43
|
// Audit §2.9: crash-recovery contract for in-memory circuit state.
|
|
22
44
|
// Persist outbound/inbound circuit state to disk so a process restart
|
|
@@ -713,19 +735,29 @@ async function processSseBlock(block, handlers) {
|
|
|
713
735
|
if (!normalizedBlock) return;
|
|
714
736
|
|
|
715
737
|
const dataLines = [];
|
|
716
|
-
let
|
|
738
|
+
let eventId = "";
|
|
739
|
+
let hasComment = false;
|
|
717
740
|
for (const rawLine of String(block).split("\n")) {
|
|
718
741
|
const line = rawLine.trimEnd();
|
|
719
742
|
if (!line) continue;
|
|
720
|
-
if (line.startsWith(":"))
|
|
721
|
-
|
|
743
|
+
if (line.startsWith(":")) {
|
|
744
|
+
hasComment = true;
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (line.startsWith("id:")) {
|
|
748
|
+
eventId = line.slice(3).trimStart();
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
722
751
|
if (line.startsWith("data:")) {
|
|
723
752
|
dataLines.push(line.slice(5).trimStart());
|
|
724
753
|
}
|
|
725
754
|
}
|
|
726
755
|
|
|
727
756
|
if (dataLines.length === 0) {
|
|
728
|
-
if (
|
|
757
|
+
if (eventId && typeof handlers.onCursor === "function") {
|
|
758
|
+
await handlers.onCursor(eventId);
|
|
759
|
+
}
|
|
760
|
+
if (hasComment && typeof handlers.onHeartbeat === "function") {
|
|
729
761
|
await handlers.onHeartbeat();
|
|
730
762
|
}
|
|
731
763
|
return;
|
|
@@ -875,14 +907,22 @@ export async function syncSessionEventToApi(
|
|
|
875
907
|
if (!normalizedSessionId || !event || typeof event !== "object" || Array.isArray(event)) {
|
|
876
908
|
return { synced: false, reason: "invalid_input" };
|
|
877
909
|
}
|
|
910
|
+
const eventType = normalizeString(event.event || event.type).toLowerCase();
|
|
911
|
+
if (isNonSemanticTranscriptEvent(event, eventType)) {
|
|
912
|
+
return {
|
|
913
|
+
synced: false,
|
|
914
|
+
reason: "non_semantic_transcript_event_rejected",
|
|
915
|
+
eventType,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
878
918
|
|
|
879
919
|
// Test-fixture leak guard. Tests in this repo (and downstream consumers)
|
|
880
920
|
// create + tear down sessions using a temp workspace; on a developer
|
|
881
921
|
// machine those calls inherit the user's stored auth and silently posted
|
|
882
922
|
// hundreds of orphan rooms to prod (Carter saw ~200 "<null>" sessions).
|
|
883
923
|
// Honoring SENTINELAYER_SKIP_REMOTE_SYNC=1 keeps everything local while
|
|
884
|
-
// still exercising the appendToStream
|
|
885
|
-
//
|
|
924
|
+
// still exercising the appendToStream paths the tests care about. Local
|
|
925
|
+
// NDJSON durability is unaffected.
|
|
886
926
|
if (String(process.env.SENTINELAYER_SKIP_REMOTE_SYNC || "").trim() === "1") {
|
|
887
927
|
return { synced: false, reason: "remote_sync_disabled_env" };
|
|
888
928
|
}
|
|
@@ -1325,18 +1365,21 @@ export async function pollHumanMessages(
|
|
|
1325
1365
|
* / `agent_response` events. The result was codex and claude talking
|
|
1326
1366
|
* past each other ("Apologies — I missed your 5 updates").
|
|
1327
1367
|
*
|
|
1328
|
-
* Endpoint contract:
|
|
1368
|
+
* Endpoint contract:
|
|
1369
|
+
* `GET /api/v1/sessions/{id}/events?after=<cursor>&limit=N&listenerAgentId=<id>`.
|
|
1329
1370
|
* The API returns events in chronological order with cursor-based
|
|
1330
|
-
* pagination.
|
|
1331
|
-
* `
|
|
1371
|
+
* pagination. When `listenerAgentId` is present, Redis-backed listener
|
|
1372
|
+
* controls are returned separately in `listenerControls`; they never join
|
|
1373
|
+
* the durable event list or participate in its cursor.
|
|
1332
1374
|
*
|
|
1333
1375
|
* @param {string} sessionId
|
|
1334
1376
|
* @param {object} [options]
|
|
1335
1377
|
* @param {string} [options.targetPath]
|
|
1336
1378
|
* @param {string|null} [options.since] - cursor to start after; null = full history
|
|
1337
1379
|
* @param {number} [options.limit] - default 200 (max from API)
|
|
1380
|
+
* @param {string} [options.listenerAgentId] - optional listener control recipient
|
|
1338
1381
|
* @param {number} [options.timeoutMs] - per-request deadline
|
|
1339
|
-
* @returns {Promise<{ok: boolean, reason: string, events: Array<object>, cursor: string|null}>}
|
|
1382
|
+
* @returns {Promise<{ok: boolean, reason: string, events: Array<object>, listenerControls: Array<object>, cursor: string|null, scannedThroughSequence: number|null, scanExhausted: boolean|null}>}
|
|
1340
1383
|
*/
|
|
1341
1384
|
export async function pollSessionEvents(
|
|
1342
1385
|
sessionId,
|
|
@@ -1344,6 +1387,7 @@ export async function pollSessionEvents(
|
|
|
1344
1387
|
targetPath = process.cwd(),
|
|
1345
1388
|
since = null,
|
|
1346
1389
|
limit = 200,
|
|
1390
|
+
listenerAgentId = "",
|
|
1347
1391
|
timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
|
|
1348
1392
|
forceCircuitProbe = false,
|
|
1349
1393
|
resolveAuthSession = resolveActiveAuthSession,
|
|
@@ -1357,6 +1401,7 @@ export async function pollSessionEvents(
|
|
|
1357
1401
|
ok: false,
|
|
1358
1402
|
reason: "invalid_session_id",
|
|
1359
1403
|
events: [],
|
|
1404
|
+
listenerControls: [],
|
|
1360
1405
|
cursor: normalizeString(since) || null,
|
|
1361
1406
|
};
|
|
1362
1407
|
}
|
|
@@ -1367,6 +1412,7 @@ export async function pollSessionEvents(
|
|
|
1367
1412
|
ok: false,
|
|
1368
1413
|
reason: "circuit_breaker_open",
|
|
1369
1414
|
events: [],
|
|
1415
|
+
listenerControls: [],
|
|
1370
1416
|
cursor: normalizeString(since) || null,
|
|
1371
1417
|
};
|
|
1372
1418
|
}
|
|
@@ -1383,6 +1429,7 @@ export async function pollSessionEvents(
|
|
|
1383
1429
|
ok: false,
|
|
1384
1430
|
reason: "no_session",
|
|
1385
1431
|
events: [],
|
|
1432
|
+
listenerControls: [],
|
|
1386
1433
|
cursor: normalizeString(since) || null,
|
|
1387
1434
|
};
|
|
1388
1435
|
}
|
|
@@ -1391,6 +1438,7 @@ export async function pollSessionEvents(
|
|
|
1391
1438
|
ok: false,
|
|
1392
1439
|
reason: "not_authenticated",
|
|
1393
1440
|
events: [],
|
|
1441
|
+
listenerControls: [],
|
|
1394
1442
|
cursor: normalizeString(since) || null,
|
|
1395
1443
|
};
|
|
1396
1444
|
}
|
|
@@ -1401,6 +1449,10 @@ export async function pollSessionEvents(
|
|
|
1401
1449
|
if (normalizedSince) {
|
|
1402
1450
|
query.set("after", normalizedSince);
|
|
1403
1451
|
}
|
|
1452
|
+
const normalizedListenerAgentId = normalizeString(listenerAgentId);
|
|
1453
|
+
if (normalizedListenerAgentId) {
|
|
1454
|
+
query.set("listenerAgentId", normalizedListenerAgentId);
|
|
1455
|
+
}
|
|
1404
1456
|
query.set(
|
|
1405
1457
|
"limit",
|
|
1406
1458
|
String(Math.max(1, Math.min(SESSION_EVENT_FETCH_LIMIT, normalizePositiveInteger(limit, SESSION_EVENT_FETCH_LIMIT))))
|
|
@@ -1426,6 +1478,7 @@ export async function pollSessionEvents(
|
|
|
1426
1478
|
status: response?.status || 429,
|
|
1427
1479
|
retryAfterMs: retryAfterMsFromResponse(response, payload, normalizedNowMs),
|
|
1428
1480
|
events: [],
|
|
1481
|
+
listenerControls: [],
|
|
1429
1482
|
cursor: normalizedSince || null,
|
|
1430
1483
|
};
|
|
1431
1484
|
}
|
|
@@ -1434,12 +1487,23 @@ export async function pollSessionEvents(
|
|
|
1434
1487
|
ok: false,
|
|
1435
1488
|
reason: `api_${response ? response.status : "no_response"}`,
|
|
1436
1489
|
events: [],
|
|
1490
|
+
listenerControls: [],
|
|
1437
1491
|
cursor: normalizedSince || null,
|
|
1438
1492
|
};
|
|
1439
1493
|
}
|
|
1440
1494
|
recordCircuitSuccess(inboundCircuit);
|
|
1441
1495
|
|
|
1442
1496
|
const items = Array.isArray(payload?.events) ? payload.events : [];
|
|
1497
|
+
const listenerControlProjection =
|
|
1498
|
+
payload?.listenerControls ?? payload?.listener_controls;
|
|
1499
|
+
const listenerControlItems = Array.isArray(listenerControlProjection)
|
|
1500
|
+
? listenerControlProjection
|
|
1501
|
+
: Array.isArray(listenerControlProjection?.items)
|
|
1502
|
+
? listenerControlProjection.items
|
|
1503
|
+
: [];
|
|
1504
|
+
const listenerControls = listenerControlItems.filter(
|
|
1505
|
+
(control) => control && typeof control === "object" && !Array.isArray(control),
|
|
1506
|
+
);
|
|
1443
1507
|
const acceptedEvents = [];
|
|
1444
1508
|
let lastCursor = normalizedSince || null;
|
|
1445
1509
|
for (const item of items) {
|
|
@@ -1450,12 +1514,23 @@ export async function pollSessionEvents(
|
|
|
1450
1514
|
// envelope shape that appendToStream expects.
|
|
1451
1515
|
acceptedEvents.push(item);
|
|
1452
1516
|
}
|
|
1517
|
+
const responseCursor =
|
|
1518
|
+
normalizeString(payload?.nextCursor || payload?.next_cursor || payload?.cursor) ||
|
|
1519
|
+
lastCursor;
|
|
1520
|
+
const scannedThroughSequence = normalizePositiveInteger(
|
|
1521
|
+
payload?.scannedThroughSequence ?? payload?.scanned_through_sequence,
|
|
1522
|
+
null,
|
|
1523
|
+
);
|
|
1524
|
+
const rawScanExhausted = payload?.scanExhausted ?? payload?.scan_exhausted;
|
|
1453
1525
|
|
|
1454
1526
|
return {
|
|
1455
1527
|
ok: true,
|
|
1456
1528
|
reason: "",
|
|
1457
1529
|
events: acceptedEvents,
|
|
1458
|
-
|
|
1530
|
+
listenerControls,
|
|
1531
|
+
cursor: responseCursor,
|
|
1532
|
+
scannedThroughSequence,
|
|
1533
|
+
scanExhausted: typeof rawScanExhausted === "boolean" ? rawScanExhausted : null,
|
|
1459
1534
|
};
|
|
1460
1535
|
} catch (error) {
|
|
1461
1536
|
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
@@ -1463,11 +1538,546 @@ export async function pollSessionEvents(
|
|
|
1463
1538
|
ok: false,
|
|
1464
1539
|
reason: normalizeString(error?.message) || "poll_failed",
|
|
1465
1540
|
events: [],
|
|
1541
|
+
listenerControls: [],
|
|
1466
1542
|
cursor: normalizedSince || null,
|
|
1467
1543
|
};
|
|
1468
1544
|
}
|
|
1469
1545
|
}
|
|
1470
1546
|
|
|
1547
|
+
/**
|
|
1548
|
+
* Read the membership-gated, three-state ephemeral listener roster.
|
|
1549
|
+
*
|
|
1550
|
+
* Compatibility is deliberately fail-closed: an older server (404), a server
|
|
1551
|
+
* with presence disabled, or a degraded Redis dependency is reported as
|
|
1552
|
+
* unknown/unsupported. Callers must never reconstruct liveness by scanning
|
|
1553
|
+
* durable listener heartbeat events.
|
|
1554
|
+
*/
|
|
1555
|
+
export async function fetchSessionPresence(
|
|
1556
|
+
sessionId,
|
|
1557
|
+
{
|
|
1558
|
+
targetPath = process.cwd(),
|
|
1559
|
+
timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
|
|
1560
|
+
forceCircuitProbe = false,
|
|
1561
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
1562
|
+
fetchImpl = fetchWithTimeout,
|
|
1563
|
+
nowMs = Date.now,
|
|
1564
|
+
} = {},
|
|
1565
|
+
) {
|
|
1566
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1567
|
+
if (!normalizedSessionId) {
|
|
1568
|
+
return {
|
|
1569
|
+
ok: false,
|
|
1570
|
+
reason: "invalid_session_id",
|
|
1571
|
+
status: "unsupported",
|
|
1572
|
+
enabled: false,
|
|
1573
|
+
present: [],
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const normalizedNowMs = Number(nowMs()) || Date.now();
|
|
1578
|
+
if (!forceCircuitProbe && isCircuitOpen(inboundCircuit, normalizedNowMs)) {
|
|
1579
|
+
return {
|
|
1580
|
+
ok: false,
|
|
1581
|
+
reason: "circuit_breaker_open",
|
|
1582
|
+
status: "degraded",
|
|
1583
|
+
enabled: null,
|
|
1584
|
+
present: [],
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
let session = null;
|
|
1589
|
+
try {
|
|
1590
|
+
session = await resolveAuthSession({
|
|
1591
|
+
cwd: targetPath,
|
|
1592
|
+
env: process.env,
|
|
1593
|
+
autoRotate: false,
|
|
1594
|
+
});
|
|
1595
|
+
} catch {
|
|
1596
|
+
return {
|
|
1597
|
+
ok: false,
|
|
1598
|
+
reason: "no_session",
|
|
1599
|
+
status: "unsupported",
|
|
1600
|
+
enabled: null,
|
|
1601
|
+
present: [],
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
if (!session?.token) {
|
|
1605
|
+
return {
|
|
1606
|
+
ok: false,
|
|
1607
|
+
reason: "not_authenticated",
|
|
1608
|
+
status: "unsupported",
|
|
1609
|
+
enabled: null,
|
|
1610
|
+
present: [],
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
const apiBaseUrl = resolveApiBaseUrl(session);
|
|
1615
|
+
const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/presence`;
|
|
1616
|
+
try {
|
|
1617
|
+
const { response, payload } = await fetchJsonWithFullTimeout(
|
|
1618
|
+
endpoint,
|
|
1619
|
+
{
|
|
1620
|
+
method: "GET",
|
|
1621
|
+
headers: { Authorization: `Bearer ${session.token}` },
|
|
1622
|
+
},
|
|
1623
|
+
normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
|
|
1624
|
+
fetchImpl,
|
|
1625
|
+
);
|
|
1626
|
+
|
|
1627
|
+
if (!response?.ok) {
|
|
1628
|
+
if (response?.status === 404) {
|
|
1629
|
+
return {
|
|
1630
|
+
ok: false,
|
|
1631
|
+
reason: "presence_unsupported",
|
|
1632
|
+
status: "unsupported",
|
|
1633
|
+
enabled: false,
|
|
1634
|
+
present: [],
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
if (isRateLimitResponse(response, payload)) {
|
|
1638
|
+
return {
|
|
1639
|
+
ok: false,
|
|
1640
|
+
reason: "rate_limited",
|
|
1641
|
+
status: "degraded",
|
|
1642
|
+
enabled: null,
|
|
1643
|
+
retryAfterMs: retryAfterMsFromResponse(response, payload, normalizedNowMs),
|
|
1644
|
+
present: [],
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
1648
|
+
return {
|
|
1649
|
+
ok: false,
|
|
1650
|
+
reason: `api_${response ? response.status : "no_response"}`,
|
|
1651
|
+
status: "degraded",
|
|
1652
|
+
enabled: null,
|
|
1653
|
+
retryAfterMs: retryAfterMsFromResponse(response, payload, normalizedNowMs),
|
|
1654
|
+
present: [],
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
recordCircuitSuccess(inboundCircuit);
|
|
1659
|
+
const enabled = payload?.enabled !== false;
|
|
1660
|
+
const status = enabled
|
|
1661
|
+
? normalizeString(payload?.status).toLowerCase() || "ok"
|
|
1662
|
+
: "unsupported";
|
|
1663
|
+
const present = enabled && Array.isArray(payload?.present)
|
|
1664
|
+
? payload.present.filter((entry) => entry && typeof entry === "object")
|
|
1665
|
+
: [];
|
|
1666
|
+
return {
|
|
1667
|
+
ok: enabled && status === "ok",
|
|
1668
|
+
reason: enabled
|
|
1669
|
+
? status === "ok"
|
|
1670
|
+
? ""
|
|
1671
|
+
: "presence_degraded"
|
|
1672
|
+
: "presence_disabled",
|
|
1673
|
+
status: status === "ok" ? "ok" : status === "unsupported" ? "unsupported" : "degraded",
|
|
1674
|
+
enabled,
|
|
1675
|
+
present,
|
|
1676
|
+
};
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
1679
|
+
return {
|
|
1680
|
+
ok: false,
|
|
1681
|
+
reason: normalizeString(error?.message) || "presence_read_failed",
|
|
1682
|
+
status: "degraded",
|
|
1683
|
+
enabled: null,
|
|
1684
|
+
present: [],
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* Renew one listener's ephemeral TTL. This endpoint is a hard protocol
|
|
1691
|
+
* boundary: it never falls back to POST /events on older or degraded servers.
|
|
1692
|
+
*/
|
|
1693
|
+
export async function renewSessionPresence(
|
|
1694
|
+
sessionId,
|
|
1695
|
+
{
|
|
1696
|
+
targetPath = process.cwd(),
|
|
1697
|
+
agentId,
|
|
1698
|
+
state = "idle",
|
|
1699
|
+
listenerId = "",
|
|
1700
|
+
model = "",
|
|
1701
|
+
displayName = "",
|
|
1702
|
+
provider = "",
|
|
1703
|
+
clientKind = "cli",
|
|
1704
|
+
observedAt = new Date().toISOString(),
|
|
1705
|
+
timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
|
|
1706
|
+
signal = undefined,
|
|
1707
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
1708
|
+
fetchImpl = fetchWithTimeout,
|
|
1709
|
+
nowMs = Date.now,
|
|
1710
|
+
} = {},
|
|
1711
|
+
) {
|
|
1712
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1713
|
+
const normalizedAgentId = normalizeString(agentId);
|
|
1714
|
+
if (!normalizedSessionId || !normalizedAgentId) {
|
|
1715
|
+
return {
|
|
1716
|
+
ok: false,
|
|
1717
|
+
reason: "invalid_input",
|
|
1718
|
+
status: "unsupported",
|
|
1719
|
+
recorded: false,
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
if (String(process.env.SENTINELAYER_SKIP_REMOTE_SYNC || "").trim() === "1") {
|
|
1723
|
+
return {
|
|
1724
|
+
ok: false,
|
|
1725
|
+
reason: "remote_sync_disabled_env",
|
|
1726
|
+
status: "unsupported",
|
|
1727
|
+
recorded: false,
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
const normalizedNowMs = Number(nowMs()) || Date.now();
|
|
1732
|
+
if (isCircuitOpen(outboundCircuit, normalizedNowMs)) {
|
|
1733
|
+
return {
|
|
1734
|
+
ok: false,
|
|
1735
|
+
reason: "circuit_breaker_open",
|
|
1736
|
+
status: "degraded",
|
|
1737
|
+
recorded: false,
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
let session = null;
|
|
1742
|
+
try {
|
|
1743
|
+
session = await resolveAuthSession({
|
|
1744
|
+
cwd: targetPath,
|
|
1745
|
+
env: process.env,
|
|
1746
|
+
autoRotate: false,
|
|
1747
|
+
});
|
|
1748
|
+
} catch {
|
|
1749
|
+
return {
|
|
1750
|
+
ok: false,
|
|
1751
|
+
reason: "no_session",
|
|
1752
|
+
status: "unsupported",
|
|
1753
|
+
recorded: false,
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
if (!session?.token) {
|
|
1757
|
+
return {
|
|
1758
|
+
ok: false,
|
|
1759
|
+
reason: "not_authenticated",
|
|
1760
|
+
status: "unsupported",
|
|
1761
|
+
recorded: false,
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
const apiBaseUrl = resolveApiBaseUrl(session);
|
|
1766
|
+
const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/presence`;
|
|
1767
|
+
const body = {
|
|
1768
|
+
agentId: normalizedAgentId,
|
|
1769
|
+
state: normalizeString(state).toLowerCase() || "idle",
|
|
1770
|
+
listenerId: normalizeString(listenerId) || undefined,
|
|
1771
|
+
model: normalizeString(model) || undefined,
|
|
1772
|
+
displayName: normalizeString(displayName) || undefined,
|
|
1773
|
+
provider: normalizeString(provider) || undefined,
|
|
1774
|
+
clientKind: normalizeString(clientKind) || "cli",
|
|
1775
|
+
observedAt: normalizeIsoTimestamp(observedAt),
|
|
1776
|
+
};
|
|
1777
|
+
|
|
1778
|
+
try {
|
|
1779
|
+
const { response, payload } = await fetchJsonWithFullTimeout(
|
|
1780
|
+
endpoint,
|
|
1781
|
+
{
|
|
1782
|
+
method: "PUT",
|
|
1783
|
+
headers: {
|
|
1784
|
+
"Content-Type": "application/json",
|
|
1785
|
+
Authorization: `Bearer ${session.token}`,
|
|
1786
|
+
},
|
|
1787
|
+
body: JSON.stringify(body),
|
|
1788
|
+
signal,
|
|
1789
|
+
},
|
|
1790
|
+
normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
|
|
1791
|
+
fetchImpl,
|
|
1792
|
+
);
|
|
1793
|
+
if (!response?.ok) {
|
|
1794
|
+
if (response?.status === 404) {
|
|
1795
|
+
return {
|
|
1796
|
+
ok: false,
|
|
1797
|
+
reason: "presence_unsupported",
|
|
1798
|
+
status: "unsupported",
|
|
1799
|
+
recorded: false,
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
if (isRateLimitResponse(response, payload)) {
|
|
1803
|
+
return {
|
|
1804
|
+
ok: false,
|
|
1805
|
+
reason: "rate_limited",
|
|
1806
|
+
status: "degraded",
|
|
1807
|
+
recorded: false,
|
|
1808
|
+
retryAfterMs: retryAfterMsFromResponse(response, payload, normalizedNowMs),
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
1812
|
+
return {
|
|
1813
|
+
ok: false,
|
|
1814
|
+
reason: `api_${response ? response.status : "no_response"}`,
|
|
1815
|
+
status: "degraded",
|
|
1816
|
+
recorded: false,
|
|
1817
|
+
retryAfterMs: retryAfterMsFromResponse(response, payload, normalizedNowMs),
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
recordCircuitSuccess(outboundCircuit);
|
|
1821
|
+
const status = normalizeString(payload?.status).toLowerCase() || "ok";
|
|
1822
|
+
return {
|
|
1823
|
+
ok: status === "ok" && Boolean(payload?.recorded ?? true),
|
|
1824
|
+
reason: status === "ok" ? "" : "presence_degraded",
|
|
1825
|
+
status: status === "ok" ? "ok" : "degraded",
|
|
1826
|
+
recorded: Boolean(payload?.recorded ?? status === "ok"),
|
|
1827
|
+
ttlSeconds: normalizePositiveInteger(payload?.ttlSeconds || payload?.ttl_seconds, null),
|
|
1828
|
+
};
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
1831
|
+
return {
|
|
1832
|
+
ok: false,
|
|
1833
|
+
reason: normalizeString(error?.message) || "presence_write_failed",
|
|
1834
|
+
status: "degraded",
|
|
1835
|
+
recorded: false,
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
/**
|
|
1841
|
+
* Enqueue an ephemeral Redis-backed listener stop control.
|
|
1842
|
+
*
|
|
1843
|
+
* This endpoint is the only remote delivery path for stop requests. It never
|
|
1844
|
+
* falls back to the durable session event stream.
|
|
1845
|
+
*/
|
|
1846
|
+
export async function requestSessionListenerStop(
|
|
1847
|
+
sessionId,
|
|
1848
|
+
{
|
|
1849
|
+
targetPath = process.cwd(),
|
|
1850
|
+
targetAgentId = "",
|
|
1851
|
+
idempotencyKey = "",
|
|
1852
|
+
timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
|
|
1853
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
1854
|
+
fetchImpl = fetchWithTimeout,
|
|
1855
|
+
nowMs = Date.now,
|
|
1856
|
+
} = {},
|
|
1857
|
+
) {
|
|
1858
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1859
|
+
if (!normalizedSessionId) {
|
|
1860
|
+
return { ok: false, reason: "invalid_session_id", recorded: false };
|
|
1861
|
+
}
|
|
1862
|
+
if (String(process.env.SENTINELAYER_SKIP_REMOTE_SYNC || "").trim() === "1") {
|
|
1863
|
+
return { ok: false, reason: "remote_sync_disabled_env", recorded: false };
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
const normalizedNowMs = Number(nowMs()) || Date.now();
|
|
1867
|
+
if (isCircuitOpen(outboundCircuit, normalizedNowMs)) {
|
|
1868
|
+
return { ok: false, reason: "circuit_breaker_open", recorded: false };
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
let session = null;
|
|
1872
|
+
try {
|
|
1873
|
+
session = await resolveAuthSession({
|
|
1874
|
+
cwd: targetPath,
|
|
1875
|
+
env: process.env,
|
|
1876
|
+
autoRotate: false,
|
|
1877
|
+
});
|
|
1878
|
+
} catch {
|
|
1879
|
+
return { ok: false, reason: "no_session", recorded: false };
|
|
1880
|
+
}
|
|
1881
|
+
if (!session?.token) {
|
|
1882
|
+
return { ok: false, reason: "not_authenticated", recorded: false };
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
const normalizedTargetAgentId = normalizeString(targetAgentId);
|
|
1886
|
+
const normalizedIdempotencyKey =
|
|
1887
|
+
normalizeString(idempotencyKey) || `sl-listener-stop-${randomUUID()}`;
|
|
1888
|
+
const apiBaseUrl = resolveApiBaseUrl(session);
|
|
1889
|
+
const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/listener-controls/stop`;
|
|
1890
|
+
const body = normalizedTargetAgentId
|
|
1891
|
+
? { targetAgentId: normalizedTargetAgentId }
|
|
1892
|
+
: { broadcast: true };
|
|
1893
|
+
|
|
1894
|
+
try {
|
|
1895
|
+
const { response, payload } = await fetchJsonWithFullTimeout(
|
|
1896
|
+
endpoint,
|
|
1897
|
+
{
|
|
1898
|
+
method: "POST",
|
|
1899
|
+
headers: {
|
|
1900
|
+
"Content-Type": "application/json",
|
|
1901
|
+
Authorization: `Bearer ${session.token}`,
|
|
1902
|
+
"Idempotency-Key": normalizedIdempotencyKey,
|
|
1903
|
+
},
|
|
1904
|
+
body: JSON.stringify(body),
|
|
1905
|
+
},
|
|
1906
|
+
normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
|
|
1907
|
+
fetchImpl,
|
|
1908
|
+
{ readErrorBody: true },
|
|
1909
|
+
);
|
|
1910
|
+
if (!response?.ok) {
|
|
1911
|
+
if (response?.status === 404) {
|
|
1912
|
+
return {
|
|
1913
|
+
ok: false,
|
|
1914
|
+
reason: "listener_control_unsupported",
|
|
1915
|
+
recorded: false,
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
if (isRateLimitResponse(response, payload)) {
|
|
1919
|
+
return {
|
|
1920
|
+
ok: false,
|
|
1921
|
+
reason: "rate_limited",
|
|
1922
|
+
recorded: false,
|
|
1923
|
+
retryAfterMs: retryAfterMsFromResponse(response, payload, normalizedNowMs),
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
1927
|
+
return {
|
|
1928
|
+
ok: false,
|
|
1929
|
+
reason: `api_${response ? response.status : "no_response"}`,
|
|
1930
|
+
recorded: false,
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
recordCircuitSuccess(outboundCircuit);
|
|
1935
|
+
const control =
|
|
1936
|
+
payload?.control && typeof payload.control === "object" && !Array.isArray(payload.control)
|
|
1937
|
+
? payload.control
|
|
1938
|
+
: payload?.listenerControl &&
|
|
1939
|
+
typeof payload.listenerControl === "object" &&
|
|
1940
|
+
!Array.isArray(payload.listenerControl)
|
|
1941
|
+
? payload.listenerControl
|
|
1942
|
+
: null;
|
|
1943
|
+
return {
|
|
1944
|
+
ok: true,
|
|
1945
|
+
reason: "",
|
|
1946
|
+
recorded: Boolean(payload?.recorded ?? true),
|
|
1947
|
+
control,
|
|
1948
|
+
};
|
|
1949
|
+
} catch (error) {
|
|
1950
|
+
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
1951
|
+
return {
|
|
1952
|
+
ok: false,
|
|
1953
|
+
reason: normalizeString(error?.message) || "listener_control_write_failed",
|
|
1954
|
+
recorded: false,
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
/**
|
|
1960
|
+
* Monotonically advance one authenticated actor's read cursor in place.
|
|
1961
|
+
*
|
|
1962
|
+
* No compatibility fallback is allowed: calling the legacy `view` action
|
|
1963
|
+
* could append one durable row per displayed event when the server flag is
|
|
1964
|
+
* disabled or races a rollout.
|
|
1965
|
+
*/
|
|
1966
|
+
export async function updateSessionReadCursor(
|
|
1967
|
+
sessionId,
|
|
1968
|
+
{
|
|
1969
|
+
targetPath = process.cwd(),
|
|
1970
|
+
targetSequenceId = null,
|
|
1971
|
+
targetCursor = "",
|
|
1972
|
+
agentId = "",
|
|
1973
|
+
timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
|
|
1974
|
+
signal = undefined,
|
|
1975
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
1976
|
+
fetchImpl = fetchWithTimeout,
|
|
1977
|
+
nowMs = Date.now,
|
|
1978
|
+
} = {},
|
|
1979
|
+
) {
|
|
1980
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1981
|
+
const normalizedTargetSequence = Number(targetSequenceId);
|
|
1982
|
+
const normalizedTargetCursor = normalizeString(targetCursor);
|
|
1983
|
+
if (
|
|
1984
|
+
!normalizedSessionId ||
|
|
1985
|
+
((!Number.isFinite(normalizedTargetSequence) || normalizedTargetSequence <= 0) &&
|
|
1986
|
+
!normalizedTargetCursor)
|
|
1987
|
+
) {
|
|
1988
|
+
return { ok: false, reason: "invalid_input", updated: false };
|
|
1989
|
+
}
|
|
1990
|
+
if (String(process.env.SENTINELAYER_SKIP_REMOTE_SYNC || "").trim() === "1") {
|
|
1991
|
+
return { ok: false, reason: "remote_sync_disabled_env", updated: false };
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
const normalizedNowMs = Number(nowMs()) || Date.now();
|
|
1995
|
+
if (isCircuitOpen(outboundCircuit, normalizedNowMs)) {
|
|
1996
|
+
return { ok: false, reason: "circuit_breaker_open", updated: false };
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
let session = null;
|
|
2000
|
+
try {
|
|
2001
|
+
session = await resolveAuthSession({
|
|
2002
|
+
cwd: targetPath,
|
|
2003
|
+
env: process.env,
|
|
2004
|
+
autoRotate: false,
|
|
2005
|
+
});
|
|
2006
|
+
} catch {
|
|
2007
|
+
return { ok: false, reason: "no_session", updated: false };
|
|
2008
|
+
}
|
|
2009
|
+
if (!session?.token) {
|
|
2010
|
+
return { ok: false, reason: "not_authenticated", updated: false };
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
const apiBaseUrl = resolveApiBaseUrl(session);
|
|
2014
|
+
const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/read-cursor`;
|
|
2015
|
+
const body = {
|
|
2016
|
+
targetSequenceId:
|
|
2017
|
+
Number.isFinite(normalizedTargetSequence) && normalizedTargetSequence > 0
|
|
2018
|
+
? Math.floor(normalizedTargetSequence)
|
|
2019
|
+
: undefined,
|
|
2020
|
+
targetCursor: normalizedTargetCursor || undefined,
|
|
2021
|
+
agentId: normalizeString(agentId) || undefined,
|
|
2022
|
+
};
|
|
2023
|
+
try {
|
|
2024
|
+
const { response, payload } = await fetchJsonWithFullTimeout(
|
|
2025
|
+
endpoint,
|
|
2026
|
+
{
|
|
2027
|
+
method: "PUT",
|
|
2028
|
+
headers: {
|
|
2029
|
+
"Content-Type": "application/json",
|
|
2030
|
+
Authorization: `Bearer ${session.token}`,
|
|
2031
|
+
},
|
|
2032
|
+
body: JSON.stringify(body),
|
|
2033
|
+
signal,
|
|
2034
|
+
},
|
|
2035
|
+
normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
|
|
2036
|
+
fetchImpl,
|
|
2037
|
+
);
|
|
2038
|
+
if (!response?.ok) {
|
|
2039
|
+
if (response?.status === 404) {
|
|
2040
|
+
return { ok: false, reason: "read_cursor_unsupported", updated: false };
|
|
2041
|
+
}
|
|
2042
|
+
if (isRateLimitResponse(response, payload)) {
|
|
2043
|
+
return {
|
|
2044
|
+
ok: false,
|
|
2045
|
+
reason: "rate_limited",
|
|
2046
|
+
status: response?.status || 429,
|
|
2047
|
+
retryAfterMs: retryAfterMsFromResponse(response, payload, normalizedNowMs),
|
|
2048
|
+
updated: false,
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
2052
|
+
return {
|
|
2053
|
+
ok: false,
|
|
2054
|
+
reason: `api_${response ? response.status : "no_response"}`,
|
|
2055
|
+
retryAfterMs: retryAfterMsFromResponse(response, payload, normalizedNowMs),
|
|
2056
|
+
updated: false,
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
recordCircuitSuccess(outboundCircuit);
|
|
2060
|
+
return {
|
|
2061
|
+
ok: Boolean(payload?.ok ?? true),
|
|
2062
|
+
reason: "",
|
|
2063
|
+
updated: Boolean(payload?.updated ?? true),
|
|
2064
|
+
lastReadSequenceId: normalizePositiveInteger(
|
|
2065
|
+
payload?.lastReadSequenceId || payload?.last_read_sequence_id,
|
|
2066
|
+
Number.isFinite(normalizedTargetSequence) && normalizedTargetSequence > 0
|
|
2067
|
+
? Math.floor(normalizedTargetSequence)
|
|
2068
|
+
: null,
|
|
2069
|
+
),
|
|
2070
|
+
};
|
|
2071
|
+
} catch (error) {
|
|
2072
|
+
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
2073
|
+
return {
|
|
2074
|
+
ok: false,
|
|
2075
|
+
reason: normalizeString(error?.message) || "read_cursor_write_failed",
|
|
2076
|
+
updated: false,
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
1471
2081
|
/**
|
|
1472
2082
|
* Consume the API's durable session SSE stream.
|
|
1473
2083
|
*
|
|
@@ -1483,6 +2093,7 @@ export async function pollSessionEvents(
|
|
|
1483
2093
|
* @param {(event: object) => Promise<void>|void} [options.onEvent]
|
|
1484
2094
|
* @param {(payload: object) => Promise<void>|void} [options.onError]
|
|
1485
2095
|
* @param {() => Promise<void>|void} [options.onHeartbeat]
|
|
2096
|
+
* @param {(cursor: string) => Promise<void>|void} [options.onCursor]
|
|
1486
2097
|
* @returns {Promise<{ok: boolean, reason: string, cursor: string|null, eventCount: number, errorCount: number, status?: number, aborted?: boolean}>}
|
|
1487
2098
|
*/
|
|
1488
2099
|
export async function streamSessionEvents(
|
|
@@ -1498,6 +2109,7 @@ export async function streamSessionEvents(
|
|
|
1498
2109
|
onEvent = async () => {},
|
|
1499
2110
|
onError = async () => {},
|
|
1500
2111
|
onHeartbeat = async () => {},
|
|
2112
|
+
onCursor = async () => {},
|
|
1501
2113
|
} = {}
|
|
1502
2114
|
) {
|
|
1503
2115
|
const normalizedSessionId = normalizeString(sessionId);
|
|
@@ -1585,6 +2197,7 @@ export async function streamSessionEvents(
|
|
|
1585
2197
|
headers: {
|
|
1586
2198
|
Accept: "text/event-stream",
|
|
1587
2199
|
Authorization: `Bearer ${session.token}`,
|
|
2200
|
+
...(normalizedSince ? { "Last-Event-ID": normalizedSince } : {}),
|
|
1588
2201
|
},
|
|
1589
2202
|
signal: controller.signal,
|
|
1590
2203
|
},
|
|
@@ -1616,6 +2229,12 @@ export async function streamSessionEvents(
|
|
|
1616
2229
|
|
|
1617
2230
|
const handlers = {
|
|
1618
2231
|
cursor: () => cursor,
|
|
2232
|
+
onCursor: async (candidate) => {
|
|
2233
|
+
const normalizedCandidate = normalizeString(candidate);
|
|
2234
|
+
if (!normalizedCandidate) return;
|
|
2235
|
+
cursor = normalizedCandidate;
|
|
2236
|
+
await onCursor(normalizedCandidate);
|
|
2237
|
+
},
|
|
1619
2238
|
onHeartbeat,
|
|
1620
2239
|
onError: async (payload) => {
|
|
1621
2240
|
errorCount += 1;
|
|
@@ -2159,6 +2778,37 @@ export async function createSessionMessageAction(
|
|
|
2159
2778
|
if (!normalizedSessionId || !normalizedActionType) {
|
|
2160
2779
|
return { ok: false, reason: "invalid_input", action: null };
|
|
2161
2780
|
}
|
|
2781
|
+
if (normalizedActionType === "view") {
|
|
2782
|
+
const actorAgentId = normalizeString(
|
|
2783
|
+
metadata?.agentId ||
|
|
2784
|
+
metadata?.agent_id ||
|
|
2785
|
+
metadata?.actorAgentId ||
|
|
2786
|
+
metadata?.actor_agent_id,
|
|
2787
|
+
);
|
|
2788
|
+
const readCursor = await updateSessionReadCursor(normalizedSessionId, {
|
|
2789
|
+
targetPath,
|
|
2790
|
+
targetSequenceId,
|
|
2791
|
+
targetCursor,
|
|
2792
|
+
agentId: actorAgentId,
|
|
2793
|
+
timeoutMs,
|
|
2794
|
+
signal,
|
|
2795
|
+
resolveAuthSession,
|
|
2796
|
+
fetchImpl,
|
|
2797
|
+
nowMs,
|
|
2798
|
+
});
|
|
2799
|
+
return {
|
|
2800
|
+
...readCursor,
|
|
2801
|
+
duplicate: false,
|
|
2802
|
+
readCursor: readCursor.ok,
|
|
2803
|
+
action: readCursor.ok
|
|
2804
|
+
? {
|
|
2805
|
+
actionType: "view",
|
|
2806
|
+
targetSequenceId: readCursor.lastReadSequenceId || targetSequenceId || null,
|
|
2807
|
+
actorId: actorAgentId || null,
|
|
2808
|
+
}
|
|
2809
|
+
: null,
|
|
2810
|
+
};
|
|
2811
|
+
}
|
|
2162
2812
|
if (String(process.env.SENTINELAYER_SKIP_REMOTE_SYNC || "").trim() === "1") {
|
|
2163
2813
|
return { ok: false, reason: "remote_sync_disabled_env", action: null };
|
|
2164
2814
|
}
|