agent-companion 0.1.5 → 0.1.6
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 +0 -13
- package/bridge/defaultState.mjs +11 -109
- package/package.json +1 -1
- package/relay/server.mjs +158 -14
package/README.md
CHANGED
|
@@ -22,19 +22,6 @@ npm i -g agent-companion
|
|
|
22
22
|
agent-companion
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
## Options
|
|
26
|
-
Use your hosted relay:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
agent-companion --relay https://agent-companion-relay.onrender.com
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Run with a local relay for development:
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
agent-companion --with-local-relay
|
|
36
|
-
```
|
|
37
|
-
|
|
38
25
|
## Pairing flow
|
|
39
26
|
1. Run `agent-companion` on your computer
|
|
40
27
|
2. Copy the pairing code shown in the terminal
|
package/bridge/defaultState.mjs
CHANGED
|
@@ -6,6 +6,7 @@ const SESSION_STATES = new Set(["RUNNING", "WAITING_INPUT", "COMPLETED", "FAILED
|
|
|
6
6
|
const EVENT_CATEGORIES = new Set(["INFO", "ACTION", "INPUT", "ERROR"]);
|
|
7
7
|
const TURN_ROLES = new Set(["USER", "ASSISTANT"]);
|
|
8
8
|
const TURN_KINDS = new Set(["MESSAGE", "FINAL_OUTPUT", "APPROVAL_ACTION"]);
|
|
9
|
+
const SEEDED_SAMPLE_SESSION_IDS = new Set(["s_codex_live_01", "s_claude_live_01", "s_codex_live_02"]);
|
|
9
10
|
|
|
10
11
|
function safeNumber(value, fallback = 0) {
|
|
11
12
|
const parsed = Number(value);
|
|
@@ -290,93 +291,6 @@ function buildDefaultThreads(sessions) {
|
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
export function buildDefaultState() {
|
|
293
|
-
const now = Date.now();
|
|
294
|
-
|
|
295
|
-
const sessions = [
|
|
296
|
-
createSession(
|
|
297
|
-
"s_codex_live_01",
|
|
298
|
-
"CODEX",
|
|
299
|
-
"Refactor notification pipeline",
|
|
300
|
-
"agent-control-plane",
|
|
301
|
-
"feature/queue-replay",
|
|
302
|
-
"RUNNING",
|
|
303
|
-
16_000,
|
|
304
|
-
64,
|
|
305
|
-
{
|
|
306
|
-
promptTokens: 27120,
|
|
307
|
-
completionTokens: 18340,
|
|
308
|
-
totalTokens: 45460,
|
|
309
|
-
costUsd: 0.62
|
|
310
|
-
}
|
|
311
|
-
),
|
|
312
|
-
createSession(
|
|
313
|
-
"s_claude_live_01",
|
|
314
|
-
"CLAUDE",
|
|
315
|
-
"Fix websocket reconnect",
|
|
316
|
-
"agent-bridge",
|
|
317
|
-
"bugfix/retry-loop",
|
|
318
|
-
"WAITING_INPUT",
|
|
319
|
-
75_000,
|
|
320
|
-
79,
|
|
321
|
-
{
|
|
322
|
-
promptTokens: 18110,
|
|
323
|
-
completionTokens: 11040,
|
|
324
|
-
totalTokens: 29150,
|
|
325
|
-
costUsd: 0.43
|
|
326
|
-
}
|
|
327
|
-
),
|
|
328
|
-
createSession(
|
|
329
|
-
"s_codex_live_02",
|
|
330
|
-
"CODEX",
|
|
331
|
-
"Token analytics API",
|
|
332
|
-
"agent-api",
|
|
333
|
-
"feat/token-metrics",
|
|
334
|
-
"COMPLETED",
|
|
335
|
-
4_300_000,
|
|
336
|
-
100,
|
|
337
|
-
{
|
|
338
|
-
promptTokens: 32002,
|
|
339
|
-
completionTokens: 22994,
|
|
340
|
-
totalTokens: 54996,
|
|
341
|
-
costUsd: 0.78
|
|
342
|
-
}
|
|
343
|
-
)
|
|
344
|
-
];
|
|
345
|
-
|
|
346
|
-
const pendingInputs = [
|
|
347
|
-
sanitizePendingInput({
|
|
348
|
-
id: "p_live_01",
|
|
349
|
-
sessionId: "s_claude_live_01",
|
|
350
|
-
prompt: "Can I cap backoff at 45s and ship this patch?",
|
|
351
|
-
requestedAt: now - 75_000,
|
|
352
|
-
priority: "HIGH"
|
|
353
|
-
})
|
|
354
|
-
].filter(Boolean);
|
|
355
|
-
|
|
356
|
-
const events = [
|
|
357
|
-
sanitizeEvent({
|
|
358
|
-
id: "e_live_101",
|
|
359
|
-
sessionId: "s_codex_live_01",
|
|
360
|
-
summary: "Running integration tests on queue replay logic.",
|
|
361
|
-
timestamp: now - 16_000,
|
|
362
|
-
category: "INFO"
|
|
363
|
-
}),
|
|
364
|
-
sanitizeEvent({
|
|
365
|
-
id: "e_live_201",
|
|
366
|
-
sessionId: "s_claude_live_01",
|
|
367
|
-
summary: "Input requested: confirm retry cap before patch.",
|
|
368
|
-
timestamp: now - 75_000,
|
|
369
|
-
category: "INPUT"
|
|
370
|
-
}),
|
|
371
|
-
sanitizeEvent({
|
|
372
|
-
id: "e_live_301",
|
|
373
|
-
sessionId: "s_codex_live_02",
|
|
374
|
-
summary: "Session completed: endpoint + tests merged.",
|
|
375
|
-
timestamp: now - 4_300_000,
|
|
376
|
-
category: "ACTION"
|
|
377
|
-
})
|
|
378
|
-
].filter(Boolean);
|
|
379
|
-
|
|
380
294
|
const settings = {
|
|
381
295
|
criticalRealtime: true,
|
|
382
296
|
digest: true,
|
|
@@ -387,26 +301,13 @@ export function buildDefaultState() {
|
|
|
387
301
|
workspaceRoot: resolveDefaultWorkspaceRoot()
|
|
388
302
|
};
|
|
389
303
|
|
|
390
|
-
const sessionThreads = buildDefaultThreads(sessions);
|
|
391
|
-
const chatTurns = [
|
|
392
|
-
sanitizeChatTurn({
|
|
393
|
-
id: "turn_live_01",
|
|
394
|
-
sessionId: "s_claude_live_01",
|
|
395
|
-
role: "ASSISTANT",
|
|
396
|
-
kind: "FINAL_OUTPUT",
|
|
397
|
-
text: "Can I cap backoff at 45s and ship this patch?",
|
|
398
|
-
createdAt: now - 75_000,
|
|
399
|
-
source: "LEGACY"
|
|
400
|
-
})
|
|
401
|
-
].filter(Boolean);
|
|
402
|
-
|
|
403
304
|
return {
|
|
404
305
|
source: "bridge",
|
|
405
|
-
sessions,
|
|
406
|
-
sessionThreads,
|
|
407
|
-
chatTurns,
|
|
408
|
-
pendingInputs,
|
|
409
|
-
events,
|
|
306
|
+
sessions: [],
|
|
307
|
+
sessionThreads: [],
|
|
308
|
+
chatTurns: [],
|
|
309
|
+
pendingInputs: [],
|
|
310
|
+
events: [],
|
|
410
311
|
pendingHandledAt: {},
|
|
411
312
|
settings
|
|
412
313
|
};
|
|
@@ -438,23 +339,24 @@ export function sanitizeState(raw) {
|
|
|
438
339
|
Array.isArray(candidate.sessions) ? candidate.sessions : fallback.sessions
|
|
439
340
|
)
|
|
440
341
|
.map((session) => sanitizeSession(session))
|
|
441
|
-
.filter(Boolean);
|
|
342
|
+
.filter((session) => Boolean(session && !SEEDED_SAMPLE_SESSION_IDS.has(session.id)));
|
|
442
343
|
|
|
443
344
|
const pendingInputs = (
|
|
444
345
|
Array.isArray(candidate.pendingInputs) ? candidate.pendingInputs : fallback.pendingInputs
|
|
445
346
|
)
|
|
446
347
|
.map((item) => sanitizePendingInput(item))
|
|
447
|
-
.filter(Boolean);
|
|
348
|
+
.filter((item) => Boolean(item && !SEEDED_SAMPLE_SESSION_IDS.has(item.sessionId)));
|
|
448
349
|
|
|
449
350
|
const events = (Array.isArray(candidate.events) ? candidate.events : fallback.events)
|
|
450
351
|
.map((event) => sanitizeEvent(event))
|
|
451
|
-
.filter(Boolean);
|
|
352
|
+
.filter((event) => Boolean(event && !SEEDED_SAMPLE_SESSION_IDS.has(event.sessionId)));
|
|
452
353
|
|
|
453
354
|
const sessionThreadMap = new Map();
|
|
454
355
|
const rawThreads = Array.isArray(candidate.sessionThreads) ? candidate.sessionThreads : [];
|
|
455
356
|
for (const item of rawThreads) {
|
|
456
357
|
const sanitized = sanitizeSessionThread(item);
|
|
457
358
|
if (!sanitized) continue;
|
|
359
|
+
if (SEEDED_SAMPLE_SESSION_IDS.has(sanitized.id)) continue;
|
|
458
360
|
sessionThreadMap.set(sanitized.id, sanitized);
|
|
459
361
|
}
|
|
460
362
|
|
|
@@ -481,7 +383,7 @@ export function sanitizeState(raw) {
|
|
|
481
383
|
|
|
482
384
|
const chatTurns = (Array.isArray(candidate.chatTurns) ? candidate.chatTurns : fallback.chatTurns)
|
|
483
385
|
.map((turn) => sanitizeChatTurn(turn))
|
|
484
|
-
.filter((turn) => Boolean(turn && knownSessionIds.has(turn.sessionId)))
|
|
386
|
+
.filter((turn) => Boolean(turn && knownSessionIds.has(turn.sessionId) && !SEEDED_SAMPLE_SESSION_IDS.has(turn.sessionId)))
|
|
485
387
|
.sort((a, b) => a.createdAt - b.createdAt);
|
|
486
388
|
|
|
487
389
|
return {
|
package/package.json
CHANGED
package/relay/server.mjs
CHANGED
|
@@ -39,6 +39,7 @@ const MAX_PHONE_TOKENS = 2_000;
|
|
|
39
39
|
const MAX_PREVIEW_RECORDS = 4_000;
|
|
40
40
|
const PHONE_TOKEN_HEADER = "x-agent-companion-phone-token";
|
|
41
41
|
const LAPTOP_TOKEN_HEADER = "x-agent-companion-laptop-token";
|
|
42
|
+
const SEEDED_SAMPLE_SESSION_IDS = new Set(["s_codex_live_01", "s_claude_live_01", "s_codex_live_02"]);
|
|
42
43
|
|
|
43
44
|
const STATE_FILE = path.resolve(PROJECT_ROOT, "relay", "state.json");
|
|
44
45
|
const app = express();
|
|
@@ -86,6 +87,7 @@ app.get("/health", (_req, res) => {
|
|
|
86
87
|
app.get("/api/admin/analytics", requireAdminToken, (_req, res) => {
|
|
87
88
|
cleanupExpiredPairings();
|
|
88
89
|
cleanupExpiredPreviews();
|
|
90
|
+
res.setHeader("Cache-Control", "no-store");
|
|
89
91
|
res.json(buildAdminAnalytics());
|
|
90
92
|
});
|
|
91
93
|
|
|
@@ -288,6 +290,16 @@ app.post("/api/pairings/claim", (req, res) => {
|
|
|
288
290
|
mutateState(() => {
|
|
289
291
|
pairing.claimedAt = now;
|
|
290
292
|
pairing.phoneToken = phoneToken;
|
|
293
|
+
if (!isObject(state.revokedPhoneDevices)) {
|
|
294
|
+
state.revokedPhoneDevices = {};
|
|
295
|
+
}
|
|
296
|
+
delete state.revokedPhoneDevices[pairing.deviceId];
|
|
297
|
+
if (!isObject(state.analytics)) {
|
|
298
|
+
state.analytics = {
|
|
299
|
+
totalPairClaims: 0
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
state.analytics.totalPairClaims = toInt(state.analytics.totalPairClaims, 0) + 1;
|
|
291
303
|
|
|
292
304
|
state.phones.push({
|
|
293
305
|
phoneToken,
|
|
@@ -311,6 +323,37 @@ app.post("/api/pairings/claim", (req, res) => {
|
|
|
311
323
|
});
|
|
312
324
|
});
|
|
313
325
|
|
|
326
|
+
app.delete("/api/devices/:id/pairing", requirePhoneToken, (req, res) => {
|
|
327
|
+
const deviceId = safeText(req.params.id, 200);
|
|
328
|
+
const phone = req.phoneSession;
|
|
329
|
+
if (!deviceId) {
|
|
330
|
+
return res.status(400).json({ ok: false, error: "device id is required" });
|
|
331
|
+
}
|
|
332
|
+
if (!phone || safeText(phone.deviceId, 200) !== deviceId) {
|
|
333
|
+
return res.status(403).json({ ok: false, error: "token cannot access this device" });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
mutateState(() => {
|
|
337
|
+
if (!isObject(state.revokedPhoneDevices)) {
|
|
338
|
+
state.revokedPhoneDevices = {};
|
|
339
|
+
}
|
|
340
|
+
state.revokedPhoneDevices[deviceId] = Date.now();
|
|
341
|
+
state.phones = state.phones.filter((item) => safeText(item.deviceId, 200) !== deviceId);
|
|
342
|
+
state.pairings = state.pairings.filter((item) => safeText(item.deviceId, 200) !== deviceId);
|
|
343
|
+
for (const laptop of state.laptops) {
|
|
344
|
+
if (safeText(laptop.deviceId, 200) !== deviceId) continue;
|
|
345
|
+
laptop.pairedAt = null;
|
|
346
|
+
laptop.pairCode = null;
|
|
347
|
+
laptop.pairingExpiresAt = null;
|
|
348
|
+
laptop.pairingUrl = null;
|
|
349
|
+
laptop.pairingPayload = null;
|
|
350
|
+
laptop.updatedAt = Date.now();
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
return res.json({ ok: true, deviceId });
|
|
355
|
+
});
|
|
356
|
+
|
|
314
357
|
app.use("/api/devices/:id", requirePhoneToken, requireDeviceAccess);
|
|
315
358
|
|
|
316
359
|
app.get("/api/devices/:id/status", (req, res) => {
|
|
@@ -1493,6 +1536,9 @@ function findPhoneByToken(token) {
|
|
|
1493
1536
|
function resolvePhoneSession(token) {
|
|
1494
1537
|
const exact = findPhoneByToken(token);
|
|
1495
1538
|
if (exact) {
|
|
1539
|
+
if (isPhoneDeviceRevoked(exact.deviceId)) {
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1496
1542
|
const refreshedToken = issuePhoneToken(exact.deviceId);
|
|
1497
1543
|
return {
|
|
1498
1544
|
phone: exact,
|
|
@@ -1505,6 +1551,7 @@ function resolvePhoneSession(token) {
|
|
|
1505
1551
|
|
|
1506
1552
|
const deviceId = safeText(claims.deviceId, 200);
|
|
1507
1553
|
if (!deviceId) return null;
|
|
1554
|
+
if (isPhoneDeviceRevoked(deviceId)) return null;
|
|
1508
1555
|
|
|
1509
1556
|
const restored = {
|
|
1510
1557
|
phoneToken: token,
|
|
@@ -1862,6 +1909,10 @@ function loadState() {
|
|
|
1862
1909
|
pairings: [],
|
|
1863
1910
|
phones: [],
|
|
1864
1911
|
previews: [],
|
|
1912
|
+
revokedPhoneDevices: {},
|
|
1913
|
+
analytics: {
|
|
1914
|
+
totalPairClaims: 0
|
|
1915
|
+
},
|
|
1865
1916
|
updatedAt: Date.now()
|
|
1866
1917
|
};
|
|
1867
1918
|
|
|
@@ -1924,20 +1975,29 @@ function buildAdminAnalytics() {
|
|
|
1924
1975
|
);
|
|
1925
1976
|
const claimedPairings = state.pairings.filter((item) => item.claimedAt);
|
|
1926
1977
|
const activePreviews = state.previews.filter((item) => item.expiresAt > now);
|
|
1927
|
-
const
|
|
1928
|
-
const
|
|
1929
|
-
|
|
1978
|
+
const uniqueCurrentPairedDeviceIds = new Set(state.phones.map((item) => safeText(item.deviceId, 200)).filter(Boolean));
|
|
1979
|
+
const uniqueKnownDeviceIds = new Set([
|
|
1980
|
+
...uniqueCurrentPairedDeviceIds,
|
|
1981
|
+
...claimedPairings.map((item) => safeText(item.deviceId, 200)).filter(Boolean)
|
|
1982
|
+
]);
|
|
1983
|
+
const totalPairs = Math.max(
|
|
1984
|
+
toInt(state.analytics?.totalPairClaims, 0),
|
|
1985
|
+
uniqueCurrentPairedDeviceIds.size,
|
|
1986
|
+
claimedPairings.length
|
|
1930
1987
|
);
|
|
1988
|
+
const analyticsLaptops = listCurrentPairedLaptops(uniqueCurrentPairedDeviceIds);
|
|
1989
|
+
const snapshotRollup = collectSnapshotAnalytics(analyticsLaptops, onlineLaptopIds, now);
|
|
1931
1990
|
|
|
1932
1991
|
return {
|
|
1933
1992
|
ok: true,
|
|
1934
1993
|
generatedAt: now,
|
|
1935
1994
|
summary: {
|
|
1936
|
-
totalUsers:
|
|
1937
|
-
|
|
1995
|
+
totalUsers: uniqueKnownDeviceIds.size,
|
|
1996
|
+
currentPairs: uniqueCurrentPairedDeviceIds.size,
|
|
1997
|
+
totalPairs,
|
|
1938
1998
|
newUsers24h: countUniqueRecentPairings(claimedPairings, now, 24 * 60 * 60 * 1000),
|
|
1939
1999
|
newUsers7d: countUniqueRecentPairings(claimedPairings, now, 7 * 24 * 60 * 60 * 1000),
|
|
1940
|
-
onlineDevices: onlineLaptopIds.
|
|
2000
|
+
onlineDevices: analyticsLaptops.filter((item) => onlineLaptopIds.has(item.laptopId)).length,
|
|
1941
2001
|
activeSessions: snapshotRollup.summary.activeSessions,
|
|
1942
2002
|
appSessionsTotal: snapshotRollup.summary.appSessionsTotal,
|
|
1943
2003
|
appRunsTotal: snapshotRollup.summary.appRunsTotal,
|
|
@@ -1955,6 +2015,40 @@ function buildAdminAnalytics() {
|
|
|
1955
2015
|
};
|
|
1956
2016
|
}
|
|
1957
2017
|
|
|
2018
|
+
function listCurrentPairedLaptops(currentPairDeviceIds) {
|
|
2019
|
+
const byDeviceId = new Map();
|
|
2020
|
+
|
|
2021
|
+
for (const laptop of state.laptops) {
|
|
2022
|
+
const deviceId = safeText(laptop.deviceId, 200);
|
|
2023
|
+
if (!deviceId || !currentPairDeviceIds.has(deviceId)) continue;
|
|
2024
|
+
|
|
2025
|
+
const previous = byDeviceId.get(deviceId) || null;
|
|
2026
|
+
if (!previous) {
|
|
2027
|
+
byDeviceId.set(deviceId, laptop);
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
const previousScore = Math.max(
|
|
2032
|
+
toInt(previous.lastConnectedAt, 0),
|
|
2033
|
+
toInt(previous.lastSnapshotAt, 0),
|
|
2034
|
+
toInt(previous.updatedAt, 0),
|
|
2035
|
+
toInt(previous.createdAt, 0)
|
|
2036
|
+
);
|
|
2037
|
+
const nextScore = Math.max(
|
|
2038
|
+
toInt(laptop.lastConnectedAt, 0),
|
|
2039
|
+
toInt(laptop.lastSnapshotAt, 0),
|
|
2040
|
+
toInt(laptop.updatedAt, 0),
|
|
2041
|
+
toInt(laptop.createdAt, 0)
|
|
2042
|
+
);
|
|
2043
|
+
|
|
2044
|
+
if (nextScore >= previousScore) {
|
|
2045
|
+
byDeviceId.set(deviceId, laptop);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
return [...byDeviceId.values()];
|
|
2050
|
+
}
|
|
2051
|
+
|
|
1958
2052
|
function collectSnapshotAnalytics(laptops, onlineLaptopIds, now) {
|
|
1959
2053
|
const sessionStates = {
|
|
1960
2054
|
RUNNING: 0,
|
|
@@ -1983,10 +2077,26 @@ function collectSnapshotAnalytics(laptops, onlineLaptopIds, now) {
|
|
|
1983
2077
|
const snapshot = isObject(laptop.latestSnapshot) ? laptop.latestSnapshot : {};
|
|
1984
2078
|
const runs = Array.isArray(snapshot.runs) ? snapshot.runs.filter((item) => isObject(item)) : [];
|
|
1985
2079
|
const sessionSummaries = Array.isArray(snapshot.sessionSummaries)
|
|
1986
|
-
? snapshot.sessionSummaries.filter((item) =>
|
|
2080
|
+
? snapshot.sessionSummaries.filter((item) => {
|
|
2081
|
+
if (!isObject(item)) return false;
|
|
2082
|
+
const sessionId = safeText(item.id || item?.session?.id, 200);
|
|
2083
|
+
return !SEEDED_SAMPLE_SESSION_IDS.has(sessionId);
|
|
2084
|
+
})
|
|
2085
|
+
: [];
|
|
2086
|
+
const sessions = Array.isArray(snapshot.sessions)
|
|
2087
|
+
? snapshot.sessions.filter((item) => {
|
|
2088
|
+
if (!isObject(item)) return false;
|
|
2089
|
+
const sessionId = safeText(item.id || item?.session?.id, 200);
|
|
2090
|
+
return !SEEDED_SAMPLE_SESSION_IDS.has(sessionId);
|
|
2091
|
+
})
|
|
2092
|
+
: [];
|
|
2093
|
+
const chatTurns = Array.isArray(snapshot.chatTurns)
|
|
2094
|
+
? snapshot.chatTurns.filter((item) => {
|
|
2095
|
+
if (!isObject(item)) return false;
|
|
2096
|
+
const sessionId = safeText(item.sessionId, 200);
|
|
2097
|
+
return sessionId && !SEEDED_SAMPLE_SESSION_IDS.has(sessionId);
|
|
2098
|
+
})
|
|
1987
2099
|
: [];
|
|
1988
|
-
const sessions = Array.isArray(snapshot.sessions) ? snapshot.sessions.filter((item) => isObject(item)) : [];
|
|
1989
|
-
const chatTurns = Array.isArray(snapshot.chatTurns) ? snapshot.chatTurns.filter((item) => isObject(item)) : [];
|
|
1990
2100
|
const sessionList = sessionSummaries.length > 0 ? sessionSummaries : sessions;
|
|
1991
2101
|
const sessionLookup = new Map();
|
|
1992
2102
|
for (const session of sessionList) {
|
|
@@ -2008,7 +2118,7 @@ function collectSnapshotAnalytics(laptops, onlineLaptopIds, now) {
|
|
|
2008
2118
|
if (agentType === "CLAUDE") agentUsage.claudeRuns += 1;
|
|
2009
2119
|
|
|
2010
2120
|
const sessionId = safeText(run.sessionId, 200);
|
|
2011
|
-
if (sessionId) appSessionIds.add(sessionId);
|
|
2121
|
+
if (sessionId && !SEEDED_SAMPLE_SESSION_IDS.has(sessionId)) appSessionIds.add(sessionId);
|
|
2012
2122
|
|
|
2013
2123
|
const createdAt = toInt(run.createdAt, 0);
|
|
2014
2124
|
if (createdAt) {
|
|
@@ -2020,13 +2130,19 @@ function collectSnapshotAnalytics(laptops, onlineLaptopIds, now) {
|
|
|
2020
2130
|
const sessionId = safeText(turn.sessionId, 200);
|
|
2021
2131
|
const role = safeText(turn.role, 20).toUpperCase();
|
|
2022
2132
|
const source = safeText(turn.source, 40).toUpperCase();
|
|
2133
|
+
const text = safeText(turn.text || turn.content, 20_000);
|
|
2023
2134
|
if (!sessionId || role !== "USER") continue;
|
|
2024
|
-
if (source === "MESSAGE_API"
|
|
2135
|
+
if (source === "MESSAGE_API" && text && !SEEDED_SAMPLE_SESSION_IDS.has(sessionId)) {
|
|
2025
2136
|
appSessionIds.add(sessionId);
|
|
2026
2137
|
}
|
|
2027
2138
|
}
|
|
2028
2139
|
|
|
2029
|
-
|
|
2140
|
+
const activeAppSessionIds = [...appSessionIds].filter((sessionId) => {
|
|
2141
|
+
if (sessionLookup.has(sessionId)) return true;
|
|
2142
|
+
return runs.some((run) => safeText(run.sessionId, 200) === sessionId);
|
|
2143
|
+
});
|
|
2144
|
+
|
|
2145
|
+
for (const sessionId of activeAppSessionIds) {
|
|
2030
2146
|
const session = sessionLookup.get(sessionId);
|
|
2031
2147
|
if (!session) continue;
|
|
2032
2148
|
|
|
@@ -2061,8 +2177,8 @@ function collectSnapshotAnalytics(laptops, onlineLaptopIds, now) {
|
|
|
2061
2177
|
createdAt: laptop.createdAt,
|
|
2062
2178
|
pairedAt: laptop.pairedAt || null,
|
|
2063
2179
|
lastSeenAt,
|
|
2064
|
-
sessionCount:
|
|
2065
|
-
activeSessionCount:
|
|
2180
|
+
sessionCount: activeAppSessionIds.length,
|
|
2181
|
+
activeSessionCount: activeAppSessionIds
|
|
2066
2182
|
.map((sessionId) => sessionLookup.get(sessionId))
|
|
2067
2183
|
.filter(Boolean)
|
|
2068
2184
|
.filter((session) => {
|
|
@@ -2178,6 +2294,10 @@ function sanitizeState(raw) {
|
|
|
2178
2294
|
pairings: [],
|
|
2179
2295
|
phones: [],
|
|
2180
2296
|
previews: [],
|
|
2297
|
+
revokedPhoneDevices: {},
|
|
2298
|
+
analytics: {
|
|
2299
|
+
totalPairClaims: 0
|
|
2300
|
+
},
|
|
2181
2301
|
updatedAt: Date.now()
|
|
2182
2302
|
};
|
|
2183
2303
|
|
|
@@ -2259,11 +2379,30 @@ function sanitizeState(raw) {
|
|
|
2259
2379
|
.filter((item) => item.previewId && item.accessToken && item.laptopId && item.deviceId && item.target)
|
|
2260
2380
|
: [];
|
|
2261
2381
|
|
|
2382
|
+
const revokedPhoneDevices = isObject(raw.revokedPhoneDevices)
|
|
2383
|
+
? Object.fromEntries(
|
|
2384
|
+
Object.entries(raw.revokedPhoneDevices)
|
|
2385
|
+
.map(([deviceId, revokedAt]) => [safeText(deviceId, 200), toInt(revokedAt, 0)])
|
|
2386
|
+
.filter(([deviceId, revokedAt]) => deviceId && revokedAt > 0)
|
|
2387
|
+
)
|
|
2388
|
+
: {};
|
|
2389
|
+
|
|
2390
|
+
const totalPairClaims = Math.max(
|
|
2391
|
+
toInt(raw?.analytics?.totalPairClaims, 0),
|
|
2392
|
+
phones.length,
|
|
2393
|
+
new Set(laptops.filter((item) => item.pairedAt).map((item) => item.deviceId).filter(Boolean)).size,
|
|
2394
|
+
pairings.filter((item) => item.claimedAt).length
|
|
2395
|
+
);
|
|
2396
|
+
|
|
2262
2397
|
return {
|
|
2263
2398
|
laptops,
|
|
2264
2399
|
pairings,
|
|
2265
2400
|
phones,
|
|
2266
2401
|
previews,
|
|
2402
|
+
revokedPhoneDevices,
|
|
2403
|
+
analytics: {
|
|
2404
|
+
totalPairClaims
|
|
2405
|
+
},
|
|
2267
2406
|
updatedAt: toInt(raw.updatedAt, Date.now())
|
|
2268
2407
|
};
|
|
2269
2408
|
}
|
|
@@ -2291,6 +2430,11 @@ function trimStateCollections() {
|
|
|
2291
2430
|
}
|
|
2292
2431
|
}
|
|
2293
2432
|
|
|
2433
|
+
function isPhoneDeviceRevoked(deviceId) {
|
|
2434
|
+
if (!deviceId || !isObject(state.revokedPhoneDevices)) return false;
|
|
2435
|
+
return toInt(state.revokedPhoneDevices[deviceId], 0) > 0;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2294
2438
|
function gracefulShutdown(signal) {
|
|
2295
2439
|
if (shuttingDown) return;
|
|
2296
2440
|
shuttingDown = true;
|