sentinelayer-cli 0.30.2 → 0.30.4
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/package.json +1 -1
- package/src/commands/session.js +173 -22
- package/src/session/agent-identity.js +119 -0
- package/src/session/agent-registry.js +87 -8
- package/src/session/event-identity.js +22 -0
- package/src/session/remote-hydrate.js +70 -14
package/package.json
CHANGED
package/src/commands/session.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
registerAgent,
|
|
33
33
|
unregisterAgent,
|
|
34
34
|
} from "../session/agent-registry.js";
|
|
35
|
+
import { inferSessionAgentIdentity } from "../session/agent-identity.js";
|
|
35
36
|
import { startSenti, stopSenti } from "../session/daemon.js";
|
|
36
37
|
import {
|
|
37
38
|
getDaemonStatus,
|
|
@@ -75,6 +76,8 @@ import { appendToStream, readStream, tailStream } from "../session/stream.js";
|
|
|
75
76
|
import {
|
|
76
77
|
addSessionEventIdentityKeys,
|
|
77
78
|
dedupeSessionEvents,
|
|
79
|
+
sessionEventIdentityKeys,
|
|
80
|
+
sessionEventUpgradesExisting,
|
|
78
81
|
sessionEventHasKnownIdentity,
|
|
79
82
|
} from "../session/event-identity.js";
|
|
80
83
|
import { readSessionPreview } from "../session/preview.js";
|
|
@@ -1607,20 +1610,30 @@ async function publishListenerPresenceEvent({
|
|
|
1607
1610
|
agentId,
|
|
1608
1611
|
agentModel = "cli",
|
|
1609
1612
|
displayName = "",
|
|
1613
|
+
provider = "",
|
|
1614
|
+
clientKind = "cli",
|
|
1610
1615
|
listenerId,
|
|
1611
1616
|
lifecycle = {},
|
|
1612
1617
|
} = {}) {
|
|
1613
1618
|
const normalizedType = normalizeString(lifecycle.type) || "heartbeat";
|
|
1614
1619
|
const eventName = listenerLifecycleEventName(normalizedType);
|
|
1620
|
+
const identity = inferSessionAgentIdentity({
|
|
1621
|
+
agentId,
|
|
1622
|
+
model: agentModel,
|
|
1623
|
+
displayName,
|
|
1624
|
+
provider,
|
|
1625
|
+
clientKind,
|
|
1626
|
+
});
|
|
1615
1627
|
const event = createAgentEvent({
|
|
1616
1628
|
event: eventName,
|
|
1617
1629
|
sessionId,
|
|
1618
1630
|
agent: {
|
|
1619
1631
|
id: agentId,
|
|
1620
|
-
model: normalizeString(
|
|
1632
|
+
model: normalizeString(identity.model) || "cli",
|
|
1621
1633
|
role: "listener",
|
|
1622
|
-
displayName: normalizeString(displayName) || agentId,
|
|
1623
|
-
|
|
1634
|
+
displayName: normalizeString(identity.displayName) || agentId,
|
|
1635
|
+
provider: normalizeString(identity.provider) || undefined,
|
|
1636
|
+
clientKind: normalizeString(identity.clientKind) || "cli",
|
|
1624
1637
|
},
|
|
1625
1638
|
eventId: `session-listener-${listenerId}-${normalizedType}-${lifecycle.pollCount ?? 0}`,
|
|
1626
1639
|
idempotencyToken: `session-listener:${listenerId}:${normalizedType}:${lifecycle.pollCount ?? 0}`,
|
|
@@ -1676,20 +1689,30 @@ export function buildSessionCoachingEvent({
|
|
|
1676
1689
|
agentId,
|
|
1677
1690
|
agentModel = "cli",
|
|
1678
1691
|
displayName = "",
|
|
1692
|
+
provider = "",
|
|
1693
|
+
clientKind = "cli",
|
|
1679
1694
|
listenerId = "",
|
|
1680
1695
|
tick = 0,
|
|
1681
1696
|
tips = SESSION_LIVE_SUCCESS_TIPS,
|
|
1682
1697
|
} = {}) {
|
|
1683
1698
|
const tipList = Array.isArray(tips) && tips.length ? tips : SESSION_LIVE_SUCCESS_TIPS;
|
|
1699
|
+
const identity = inferSessionAgentIdentity({
|
|
1700
|
+
agentId,
|
|
1701
|
+
model: agentModel,
|
|
1702
|
+
displayName,
|
|
1703
|
+
provider,
|
|
1704
|
+
clientKind,
|
|
1705
|
+
});
|
|
1684
1706
|
return createAgentEvent({
|
|
1685
1707
|
event: "session_coaching",
|
|
1686
1708
|
sessionId,
|
|
1687
1709
|
agent: {
|
|
1688
1710
|
id: agentId,
|
|
1689
|
-
model: normalizeString(
|
|
1711
|
+
model: normalizeString(identity.model) || "cli",
|
|
1690
1712
|
role: "listener",
|
|
1691
|
-
displayName: normalizeString(displayName) || agentId,
|
|
1692
|
-
|
|
1713
|
+
displayName: normalizeString(identity.displayName) || agentId,
|
|
1714
|
+
provider: normalizeString(identity.provider) || undefined,
|
|
1715
|
+
clientKind: normalizeString(identity.clientKind) || "cli",
|
|
1693
1716
|
},
|
|
1694
1717
|
eventId: `session-coaching-${listenerId || agentId}-${tick}`,
|
|
1695
1718
|
idempotencyToken: `session-coaching:${listenerId || agentId}:${tick}`,
|
|
@@ -1707,20 +1730,30 @@ function buildListenerCatchupEvent({
|
|
|
1707
1730
|
agentId,
|
|
1708
1731
|
agentModel = "cli",
|
|
1709
1732
|
displayName = "",
|
|
1733
|
+
provider = "",
|
|
1734
|
+
clientKind = "cli",
|
|
1710
1735
|
listenerId,
|
|
1711
1736
|
catchup = {},
|
|
1712
1737
|
} = {}) {
|
|
1713
1738
|
const message = formatListenerCatchupNotice(catchup);
|
|
1714
1739
|
const pollCount = Number(catchup.pollCount || 0);
|
|
1740
|
+
const identity = inferSessionAgentIdentity({
|
|
1741
|
+
agentId,
|
|
1742
|
+
model: agentModel,
|
|
1743
|
+
displayName,
|
|
1744
|
+
provider,
|
|
1745
|
+
clientKind,
|
|
1746
|
+
});
|
|
1715
1747
|
return createAgentEvent({
|
|
1716
1748
|
event: "session_listen_catchup",
|
|
1717
1749
|
sessionId,
|
|
1718
1750
|
agent: {
|
|
1719
1751
|
id: agentId,
|
|
1720
|
-
model: normalizeString(
|
|
1752
|
+
model: normalizeString(identity.model) || "cli",
|
|
1721
1753
|
role: "listener",
|
|
1722
|
-
displayName: normalizeString(displayName) || agentId,
|
|
1723
|
-
|
|
1754
|
+
displayName: normalizeString(identity.displayName) || agentId,
|
|
1755
|
+
provider: normalizeString(identity.provider) || undefined,
|
|
1756
|
+
clientKind: normalizeString(identity.clientKind) || "cli",
|
|
1724
1757
|
},
|
|
1725
1758
|
eventId: `session-listener-${listenerId}-catchup-${pollCount}`,
|
|
1726
1759
|
idempotencyToken: `session-listener:${listenerId}:catchup:${pollCount}`,
|
|
@@ -1959,6 +1992,9 @@ async function ensureSessionSayAgentRegistered(
|
|
|
1959
1992
|
const registered = await rememberAgentIdentity(sessionId, {
|
|
1960
1993
|
agentId,
|
|
1961
1994
|
model: normalizeString(agent.model) || "cli",
|
|
1995
|
+
displayName: normalizeString(agent.displayName),
|
|
1996
|
+
provider: normalizeString(agent.provider),
|
|
1997
|
+
clientKind: normalizeString(agent.clientKind) || "cli",
|
|
1962
1998
|
role: sessionSayRegistryRole(agent.role),
|
|
1963
1999
|
targetPath,
|
|
1964
2000
|
});
|
|
@@ -1995,12 +2031,24 @@ async function resolveSessionAgentEnvelope(
|
|
|
1995
2031
|
const resolvedRole = normalizeString(role) || normalizeString(registeredAgent?.role);
|
|
1996
2032
|
const resolvedDisplayName =
|
|
1997
2033
|
normalizeString(displayName) || normalizeString(registeredAgent?.displayName);
|
|
2034
|
+
const identity = inferSessionAgentIdentity({
|
|
2035
|
+
agentId: normalizedAgentId,
|
|
2036
|
+
model: resolvedModel,
|
|
2037
|
+
displayName: resolvedDisplayName,
|
|
2038
|
+
provider: registeredAgent?.provider,
|
|
2039
|
+
clientKind: normalizeString(clientKind) || normalizeString(registeredAgent?.clientKind) || "cli",
|
|
2040
|
+
});
|
|
1998
2041
|
const envelope = {
|
|
1999
2042
|
id: normalizedAgentId,
|
|
2000
|
-
model: resolvedModel || undefined,
|
|
2043
|
+
model: normalizeString(identity.model) || resolvedModel || undefined,
|
|
2001
2044
|
role: resolvedRole || undefined,
|
|
2002
|
-
displayName: resolvedDisplayName || undefined,
|
|
2003
|
-
|
|
2045
|
+
displayName: normalizeString(identity.displayName) || resolvedDisplayName || undefined,
|
|
2046
|
+
provider: normalizeString(identity.provider) || undefined,
|
|
2047
|
+
clientKind:
|
|
2048
|
+
normalizeString(identity.clientKind) ||
|
|
2049
|
+
normalizeString(clientKind) ||
|
|
2050
|
+
normalizeString(registeredAgent?.clientKind) ||
|
|
2051
|
+
undefined,
|
|
2004
2052
|
};
|
|
2005
2053
|
return Object.fromEntries(Object.entries(envelope).filter(([, value]) => value !== undefined));
|
|
2006
2054
|
}
|
|
@@ -2242,6 +2290,7 @@ async function hydrateJoinBriefingContext(sessionId, { targetPath, limit = 100 }
|
|
|
2242
2290
|
reason: "",
|
|
2243
2291
|
remoteEvents: Array.isArray(remoteTail.events) ? remoteTail.events.length : 0,
|
|
2244
2292
|
appended: appended.appended,
|
|
2293
|
+
upgraded: appended.upgraded,
|
|
2245
2294
|
skipped: appended.skipped,
|
|
2246
2295
|
failed: appended.failed,
|
|
2247
2296
|
cursor: remoteTail.cursor || null,
|
|
@@ -2259,6 +2308,34 @@ async function hydrateJoinBriefingContext(sessionId, { targetPath, limit = 100 }
|
|
|
2259
2308
|
}
|
|
2260
2309
|
}
|
|
2261
2310
|
|
|
2311
|
+
function remoteSessionEventUpgradesLocal(localEvent = {}, remoteEvent = {}) {
|
|
2312
|
+
return sessionEventUpgradesExisting(localEvent, remoteEvent);
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
function indexSessionEventIdentityKeys(indexByKey, eventList = [], event = {}, index = -1) {
|
|
2316
|
+
if (!indexByKey || !Array.isArray(eventList) || index < 0) return;
|
|
2317
|
+
for (const key of sessionEventIdentityKeys(event)) {
|
|
2318
|
+
const existingIndex = indexByKey.get(key);
|
|
2319
|
+
const existingEvent = Number.isInteger(existingIndex) && existingIndex >= 0
|
|
2320
|
+
? eventList[existingIndex]
|
|
2321
|
+
: null;
|
|
2322
|
+
if (!existingEvent || remoteSessionEventUpgradesLocal(existingEvent, event)) {
|
|
2323
|
+
indexByKey.set(key, index);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
function findSessionEventIdentityIndex(indexByKey, event = {}) {
|
|
2329
|
+
if (!indexByKey) return -1;
|
|
2330
|
+
for (const key of sessionEventIdentityKeys(event)) {
|
|
2331
|
+
const index = indexByKey.get(key);
|
|
2332
|
+
if (Number.isInteger(index) && index >= 0) {
|
|
2333
|
+
return index;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
return -1;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2262
2339
|
async function appendMissingRemoteEvents(sessionId, remoteEvents = [], { targetPath } = {}) {
|
|
2263
2340
|
const events = Array.isArray(remoteEvents) ? remoteEvents : [];
|
|
2264
2341
|
if (events.length === 0) {
|
|
@@ -2269,18 +2346,47 @@ async function appendMissingRemoteEvents(sessionId, remoteEvents = [], { targetP
|
|
|
2269
2346
|
};
|
|
2270
2347
|
}
|
|
2271
2348
|
const knownKeys = new Set();
|
|
2349
|
+
const localIndexByKey = new Map();
|
|
2272
2350
|
const localEvents = await readStream(sessionId, {
|
|
2273
2351
|
targetPath,
|
|
2274
2352
|
tail: 0,
|
|
2275
2353
|
});
|
|
2276
|
-
for (const event of localEvents) {
|
|
2354
|
+
for (const [index, event] of localEvents.entries()) {
|
|
2277
2355
|
addSessionEventIdentityKeys(knownKeys, event);
|
|
2356
|
+
indexSessionEventIdentityKeys(localIndexByKey, localEvents, event, index);
|
|
2278
2357
|
}
|
|
2279
2358
|
|
|
2280
2359
|
let appended = 0;
|
|
2360
|
+
let upgraded = 0;
|
|
2281
2361
|
let skipped = 0;
|
|
2282
2362
|
let failed = 0;
|
|
2283
2363
|
for (const event of events) {
|
|
2364
|
+
const existingIndex = findSessionEventIdentityIndex(localIndexByKey, event);
|
|
2365
|
+
if (existingIndex >= 0) {
|
|
2366
|
+
const existing = localEvents[existingIndex];
|
|
2367
|
+
if (!remoteSessionEventUpgradesLocal(existing, event)) {
|
|
2368
|
+
skipped += 1;
|
|
2369
|
+
continue;
|
|
2370
|
+
}
|
|
2371
|
+
// Keep the richer canonical API row in local history when it
|
|
2372
|
+
// upgrades an optimistic/local post that had no durable cursor yet.
|
|
2373
|
+
// Future reads can then page by sequence instead of rediscovering the
|
|
2374
|
+
// upgrade every time.
|
|
2375
|
+
try {
|
|
2376
|
+
const persisted = await appendToStream(sessionId, event, {
|
|
2377
|
+
targetPath,
|
|
2378
|
+
syncRemote: false,
|
|
2379
|
+
});
|
|
2380
|
+
localEvents.push(persisted);
|
|
2381
|
+
addSessionEventIdentityKeys(knownKeys, persisted);
|
|
2382
|
+
indexSessionEventIdentityKeys(localIndexByKey, localEvents, persisted, localEvents.length - 1);
|
|
2383
|
+
upgraded += 1;
|
|
2384
|
+
} catch {
|
|
2385
|
+
addSessionEventIdentityKeys(knownKeys, event);
|
|
2386
|
+
failed += 1;
|
|
2387
|
+
}
|
|
2388
|
+
continue;
|
|
2389
|
+
}
|
|
2284
2390
|
if (sessionEventHasKnownIdentity(event, knownKeys)) {
|
|
2285
2391
|
skipped += 1;
|
|
2286
2392
|
continue;
|
|
@@ -2299,6 +2405,7 @@ async function appendMissingRemoteEvents(sessionId, remoteEvents = [], { targetP
|
|
|
2299
2405
|
}
|
|
2300
2406
|
return {
|
|
2301
2407
|
appended,
|
|
2408
|
+
upgraded,
|
|
2302
2409
|
skipped,
|
|
2303
2410
|
failed,
|
|
2304
2411
|
};
|
|
@@ -3021,6 +3128,8 @@ export function registerSessionCommand(program) {
|
|
|
3021
3128
|
targetPath,
|
|
3022
3129
|
agentId: resolvedAgentId,
|
|
3023
3130
|
model,
|
|
3131
|
+
displayName: normalizeString(acceptedOnboarding?.displayName),
|
|
3132
|
+
clientKind: "cli",
|
|
3024
3133
|
role,
|
|
3025
3134
|
trackProcessExit: false,
|
|
3026
3135
|
awaitRemoteSync: hasConcreteAgentIdentity,
|
|
@@ -3059,6 +3168,9 @@ export function registerSessionCommand(program) {
|
|
|
3059
3168
|
agentId: joined.agentId,
|
|
3060
3169
|
role: joined.role,
|
|
3061
3170
|
model: joined.model,
|
|
3171
|
+
displayName: joined.displayName || null,
|
|
3172
|
+
provider: joined.provider || null,
|
|
3173
|
+
clientKind: joined.clientKind || null,
|
|
3062
3174
|
status: joined.status,
|
|
3063
3175
|
joinedAt: joined.joinedAt,
|
|
3064
3176
|
materializedLocalSession: localSession.materialized,
|
|
@@ -3305,12 +3417,19 @@ export function registerSessionCommand(program) {
|
|
|
3305
3417
|
if (to) {
|
|
3306
3418
|
eventPayload.to = to;
|
|
3307
3419
|
}
|
|
3420
|
+
const agentIdentity = inferSessionAgentIdentity({
|
|
3421
|
+
agentId,
|
|
3422
|
+
model: options.model,
|
|
3423
|
+
displayName: options.displayName,
|
|
3424
|
+
clientKind: "cli",
|
|
3425
|
+
});
|
|
3308
3426
|
const agent = {
|
|
3309
3427
|
id: agentId,
|
|
3310
|
-
model: normalizeString(
|
|
3311
|
-
displayName: normalizeString(
|
|
3428
|
+
model: normalizeString(agentIdentity.model) || "cli",
|
|
3429
|
+
displayName: normalizeString(agentIdentity.displayName) || undefined,
|
|
3430
|
+
provider: normalizeString(agentIdentity.provider) || undefined,
|
|
3312
3431
|
role: normalizeString(options.role) || "coder",
|
|
3313
|
-
clientKind: "cli",
|
|
3432
|
+
clientKind: normalizeString(agentIdentity.clientKind) || "cli",
|
|
3314
3433
|
};
|
|
3315
3434
|
const event = createAgentEvent({
|
|
3316
3435
|
event: "session_message",
|
|
@@ -3992,8 +4111,16 @@ export function registerSessionCommand(program) {
|
|
|
3992
4111
|
"presence-interval",
|
|
3993
4112
|
60,
|
|
3994
4113
|
);
|
|
3995
|
-
const
|
|
3996
|
-
|
|
4114
|
+
const listenerIdentity = inferSessionAgentIdentity({
|
|
4115
|
+
agentId,
|
|
4116
|
+
model: options.model,
|
|
4117
|
+
displayName: options.displayName,
|
|
4118
|
+
clientKind: "cli",
|
|
4119
|
+
});
|
|
4120
|
+
const agentModel = normalizeString(listenerIdentity.model) || "cli";
|
|
4121
|
+
const displayName = normalizeString(listenerIdentity.displayName) || agentId;
|
|
4122
|
+
const provider = normalizeString(listenerIdentity.provider);
|
|
4123
|
+
const clientKind = normalizeString(listenerIdentity.clientKind) || "cli";
|
|
3997
4124
|
const limit = parsePositiveInteger(options.limit, "limit", 200);
|
|
3998
4125
|
const emitFormat = normalizeString(options.emit).toLowerCase() || "ndjson";
|
|
3999
4126
|
if (!["ndjson", "text"].includes(emitFormat)) {
|
|
@@ -4085,6 +4212,8 @@ export function registerSessionCommand(program) {
|
|
|
4085
4212
|
agentId,
|
|
4086
4213
|
agentModel,
|
|
4087
4214
|
displayName,
|
|
4215
|
+
provider,
|
|
4216
|
+
clientKind,
|
|
4088
4217
|
listenerId,
|
|
4089
4218
|
tick: coachingTick++,
|
|
4090
4219
|
}),
|
|
@@ -4155,6 +4284,8 @@ export function registerSessionCommand(program) {
|
|
|
4155
4284
|
agentId,
|
|
4156
4285
|
agentModel,
|
|
4157
4286
|
displayName,
|
|
4287
|
+
provider,
|
|
4288
|
+
clientKind,
|
|
4158
4289
|
listenerId,
|
|
4159
4290
|
catchup,
|
|
4160
4291
|
}),
|
|
@@ -4257,6 +4388,8 @@ export function registerSessionCommand(program) {
|
|
|
4257
4388
|
agentId,
|
|
4258
4389
|
agentModel,
|
|
4259
4390
|
displayName,
|
|
4391
|
+
provider,
|
|
4392
|
+
clientKind,
|
|
4260
4393
|
listenerId,
|
|
4261
4394
|
lifecycle,
|
|
4262
4395
|
});
|
|
@@ -4882,12 +5015,26 @@ export function registerSessionCommand(program) {
|
|
|
4882
5015
|
const displayEvents = [...allEvents];
|
|
4883
5016
|
let remoteTailAppended = 0;
|
|
4884
5017
|
let remoteTailDisplayedOnly = 0;
|
|
5018
|
+
let remoteTailUpgraded = 0;
|
|
4885
5019
|
if (remoteTail?.ok && Array.isArray(remoteTail.events) && remoteTail.events.length > 0) {
|
|
4886
5020
|
const knownKeys = new Set();
|
|
4887
|
-
|
|
5021
|
+
const displayIndexByKey = new Map();
|
|
5022
|
+
for (const [index, event] of displayEvents.entries()) {
|
|
4888
5023
|
addSessionEventIdentityKeys(knownKeys, event);
|
|
5024
|
+
indexSessionEventIdentityKeys(displayIndexByKey, displayEvents, event, index);
|
|
4889
5025
|
}
|
|
4890
5026
|
for (const event of remoteTail.events) {
|
|
5027
|
+
const existingIndex = findSessionEventIdentityIndex(displayIndexByKey, event);
|
|
5028
|
+
if (existingIndex >= 0) {
|
|
5029
|
+
const existing = displayEvents[existingIndex];
|
|
5030
|
+
if (remoteSessionEventUpgradesLocal(existing, event)) {
|
|
5031
|
+
displayEvents[existingIndex] = event;
|
|
5032
|
+
addSessionEventIdentityKeys(knownKeys, event);
|
|
5033
|
+
indexSessionEventIdentityKeys(displayIndexByKey, displayEvents, event, existingIndex);
|
|
5034
|
+
remoteTailUpgraded += 1;
|
|
5035
|
+
}
|
|
5036
|
+
continue;
|
|
5037
|
+
}
|
|
4891
5038
|
if (sessionEventHasKnownIdentity(event, knownKeys)) {
|
|
4892
5039
|
continue;
|
|
4893
5040
|
}
|
|
@@ -4898,10 +5045,12 @@ export function registerSessionCommand(program) {
|
|
|
4898
5045
|
});
|
|
4899
5046
|
displayEvents.push(appended);
|
|
4900
5047
|
addSessionEventIdentityKeys(knownKeys, appended);
|
|
5048
|
+
indexSessionEventIdentityKeys(displayIndexByKey, displayEvents, appended, displayEvents.length - 1);
|
|
4901
5049
|
remoteTailAppended += 1;
|
|
4902
5050
|
} catch {
|
|
4903
5051
|
displayEvents.push(event);
|
|
4904
5052
|
addSessionEventIdentityKeys(knownKeys, event);
|
|
5053
|
+
indexSessionEventIdentityKeys(displayIndexByKey, displayEvents, event, displayEvents.length - 1);
|
|
4905
5054
|
remoteTailDisplayedOnly += 1;
|
|
4906
5055
|
}
|
|
4907
5056
|
}
|
|
@@ -4915,10 +5064,11 @@ export function registerSessionCommand(program) {
|
|
|
4915
5064
|
const recentHumanActivity = remoteActions?.ok
|
|
4916
5065
|
? readProjectionRecentHumanActivity(remoteActions)
|
|
4917
5066
|
: [];
|
|
5067
|
+
const dedupedDisplayEvents = dedupeSessionEvents(displayEvents);
|
|
4918
5068
|
const transcriptEvents = includeControlEvents
|
|
4919
|
-
?
|
|
4920
|
-
:
|
|
4921
|
-
const hiddenControlEventCount =
|
|
5069
|
+
? dedupedDisplayEvents
|
|
5070
|
+
: dedupedDisplayEvents.filter((event) => !isSessionControlEvent(event));
|
|
5071
|
+
const hiddenControlEventCount = dedupedDisplayEvents.length - transcriptEvents.length;
|
|
4922
5072
|
const events = mergeSessionActionEvents(transcriptEvents, actionEvents).slice(-tail);
|
|
4923
5073
|
const autoView = recordSessionReadViews(normalizedSessionId, events, {
|
|
4924
5074
|
targetPath,
|
|
@@ -4968,6 +5118,7 @@ export function registerSessionCommand(program) {
|
|
|
4968
5118
|
verified: Boolean(remoteTail.ok),
|
|
4969
5119
|
appended: remoteTailAppended,
|
|
4970
5120
|
displayedOnly: remoteTailDisplayedOnly,
|
|
5121
|
+
upgraded: remoteTailUpgraded,
|
|
4971
5122
|
controlEventCount: remoteTailStats?.controlEventCount || 0,
|
|
4972
5123
|
materialEventCount: remoteTailStats?.materialEventCount || 0,
|
|
4973
5124
|
latestEvent: remoteTailStats?.latestEvent || null,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
function normalizeString(value) {
|
|
2
|
+
return String(value || "").trim();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function normalizeLookup(value) {
|
|
6
|
+
return normalizeString(value).toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function titleFromIdentifier(value) {
|
|
10
|
+
const parts = normalizeString(value)
|
|
11
|
+
.replace(/[_-]+/g, " ")
|
|
12
|
+
.split(/\s+/)
|
|
13
|
+
.map((part) => part.trim())
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
if (!parts.length) return "";
|
|
16
|
+
return parts
|
|
17
|
+
.map((part) => {
|
|
18
|
+
if (/^[0-9]+$/.test(part)) return part;
|
|
19
|
+
return `${part.charAt(0).toUpperCase()}${part.slice(1)}`;
|
|
20
|
+
})
|
|
21
|
+
.join(" ");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isWeakModel(value) {
|
|
25
|
+
const normalized = normalizeLookup(value);
|
|
26
|
+
return !normalized || ["anonymous", "cli", "unknown", "unreported"].includes(normalized);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function inferFamily(agentId, model) {
|
|
30
|
+
const haystack = `${agentId} ${model}`.toLowerCase();
|
|
31
|
+
if (haystack.includes("claude") || haystack.includes("sonnet") || haystack.includes("opus")) {
|
|
32
|
+
return "claude";
|
|
33
|
+
}
|
|
34
|
+
if (haystack.includes("codex") || haystack.includes("gpt-") || haystack.includes("openai")) {
|
|
35
|
+
return "codex";
|
|
36
|
+
}
|
|
37
|
+
if (haystack.includes("gemini") || haystack.includes("google")) return "gemini";
|
|
38
|
+
if (haystack.includes("grok") || haystack.includes("xai")) return "grok";
|
|
39
|
+
if (haystack.includes("senti") || haystack.includes("sentinelayer")) return "senti";
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function inferModel(family) {
|
|
44
|
+
if (family === "codex") return "gpt-5-codex";
|
|
45
|
+
if (family === "claude") return "claude";
|
|
46
|
+
if (family === "gemini") return "gemini";
|
|
47
|
+
if (family === "grok") return "grok";
|
|
48
|
+
if (family === "senti") return "gpt-5.4-mini";
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function inferProvider(family, model) {
|
|
53
|
+
const normalizedModel = normalizeLookup(model);
|
|
54
|
+
if (family === "claude" || normalizedModel.includes("claude")) return "anthropic";
|
|
55
|
+
if (family === "codex" || normalizedModel.includes("gpt") || normalizedModel.includes("openai")) {
|
|
56
|
+
return "openai";
|
|
57
|
+
}
|
|
58
|
+
if (family === "gemini" || normalizedModel.includes("gemini")) return "google";
|
|
59
|
+
if (family === "grok" || normalizedModel.includes("grok")) return "xai";
|
|
60
|
+
if (family === "senti") return "sentinelayer";
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function inferClientKind(family) {
|
|
65
|
+
if (family === "codex") return "codex";
|
|
66
|
+
if (family === "claude") return "claude";
|
|
67
|
+
if (family === "gemini") return "gemini";
|
|
68
|
+
if (family === "grok") return "grok";
|
|
69
|
+
if (family === "senti") return "senti";
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function inferDisplayName(agentId, family) {
|
|
74
|
+
const normalizedId = normalizeLookup(agentId);
|
|
75
|
+
if (normalizedId === "senti") return "Senti";
|
|
76
|
+
if (normalizedId.includes("claude-verifier")) return "Claude Verifier";
|
|
77
|
+
if (normalizedId.startsWith("claude")) return "Claude";
|
|
78
|
+
if (normalizedId.startsWith("codex")) return "Codex";
|
|
79
|
+
if (normalizedId.startsWith("gemini")) return "Gemini";
|
|
80
|
+
if (normalizedId.startsWith("grok")) return "Grok";
|
|
81
|
+
if (family) return titleFromIdentifier(family);
|
|
82
|
+
return titleFromIdentifier(agentId);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function inferSessionAgentIdentity(input = {}) {
|
|
86
|
+
const source =
|
|
87
|
+
input && typeof input === "object" && !Array.isArray(input)
|
|
88
|
+
? input
|
|
89
|
+
: {};
|
|
90
|
+
const {
|
|
91
|
+
agentId = "",
|
|
92
|
+
model = "",
|
|
93
|
+
displayName = "",
|
|
94
|
+
provider = "",
|
|
95
|
+
clientKind = "",
|
|
96
|
+
} = source;
|
|
97
|
+
const normalizedAgentId = normalizeString(agentId);
|
|
98
|
+
const explicitModel = normalizeString(model);
|
|
99
|
+
const family = inferFamily(normalizedAgentId, explicitModel);
|
|
100
|
+
const resolvedModel = isWeakModel(explicitModel)
|
|
101
|
+
? inferModel(family) || explicitModel || "unknown"
|
|
102
|
+
: explicitModel;
|
|
103
|
+
const resolvedProvider = normalizeString(provider) || inferProvider(family, resolvedModel);
|
|
104
|
+
const resolvedClientKind = normalizeString(clientKind) || inferClientKind(family) || "cli";
|
|
105
|
+
const resolvedDisplayName =
|
|
106
|
+
normalizeString(displayName) || inferDisplayName(normalizedAgentId, family);
|
|
107
|
+
|
|
108
|
+
return Object.fromEntries(
|
|
109
|
+
Object.entries({
|
|
110
|
+
agentId: normalizedAgentId,
|
|
111
|
+
model: resolvedModel,
|
|
112
|
+
displayName: resolvedDisplayName,
|
|
113
|
+
provider: resolvedProvider,
|
|
114
|
+
clientKind: resolvedClientKind,
|
|
115
|
+
}).filter(([, value]) => normalizeString(value)),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { titleFromIdentifier };
|
|
@@ -5,6 +5,7 @@ import process from "node:process";
|
|
|
5
5
|
|
|
6
6
|
import { STUCK_THRESHOLDS } from "../agents/jules/pulse.js";
|
|
7
7
|
import { createAgentEvent } from "../events/schema.js";
|
|
8
|
+
import { inferSessionAgentIdentity } from "./agent-identity.js";
|
|
8
9
|
import { resolveSessionPaths } from "./paths.js";
|
|
9
10
|
import { emitContextBriefing } from "./recap.js";
|
|
10
11
|
import {
|
|
@@ -120,6 +121,9 @@ function normalizeAgentSnapshot(snapshot = {}, nowIso = new Date().toISOString()
|
|
|
120
121
|
sessionId: normalizeString(snapshot.sessionId),
|
|
121
122
|
agentId: normalizeString(snapshot.agentId),
|
|
122
123
|
model: normalizeString(snapshot.model) || "unknown",
|
|
124
|
+
displayName: normalizeString(snapshot.displayName) || "",
|
|
125
|
+
provider: normalizeString(snapshot.provider) || "",
|
|
126
|
+
clientKind: normalizeString(snapshot.clientKind) || "",
|
|
123
127
|
role: normalizeRole(snapshot.role || "observer"),
|
|
124
128
|
status: normalizeStatus(snapshot.status, "idle"),
|
|
125
129
|
detail: normalizeString(snapshot.detail) || "",
|
|
@@ -149,13 +153,30 @@ async function readAgentSnapshot(snapshotPath) {
|
|
|
149
153
|
}
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
|
|
156
|
+
const _snapshotWriteQueues = new Map();
|
|
157
|
+
|
|
158
|
+
async function writeAgentSnapshotFile(snapshotPath, snapshot) {
|
|
153
159
|
await fsp.mkdir(path.dirname(snapshotPath), { recursive: true });
|
|
154
|
-
const tmpPath = `${snapshotPath}.${process.pid}.${Date.now()}.tmp`;
|
|
160
|
+
const tmpPath = `${snapshotPath}.${process.pid}.${Date.now()}.${randomBytes(4).toString("hex")}.tmp`;
|
|
155
161
|
await fsp.writeFile(tmpPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf-8");
|
|
156
162
|
await fsp.rename(tmpPath, snapshotPath);
|
|
157
163
|
}
|
|
158
164
|
|
|
165
|
+
async function writeAgentSnapshot(snapshotPath, snapshot) {
|
|
166
|
+
const previous = _snapshotWriteQueues.get(snapshotPath) || Promise.resolve();
|
|
167
|
+
const write = previous
|
|
168
|
+
.catch(() => {})
|
|
169
|
+
.then(() => writeAgentSnapshotFile(snapshotPath, snapshot));
|
|
170
|
+
_snapshotWriteQueues.set(snapshotPath, write);
|
|
171
|
+
try {
|
|
172
|
+
await write;
|
|
173
|
+
} finally {
|
|
174
|
+
if (_snapshotWriteQueues.get(snapshotPath) === write) {
|
|
175
|
+
_snapshotWriteQueues.delete(snapshotPath);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
159
180
|
async function emitAgentEvent(
|
|
160
181
|
sessionId,
|
|
161
182
|
event,
|
|
@@ -164,7 +185,14 @@ async function emitAgentEvent(
|
|
|
164
185
|
) {
|
|
165
186
|
const envelope = createAgentEvent({
|
|
166
187
|
event,
|
|
167
|
-
|
|
188
|
+
agent: {
|
|
189
|
+
id: payload.agentId,
|
|
190
|
+
model: payload.model,
|
|
191
|
+
displayName: payload.displayName,
|
|
192
|
+
provider: payload.provider,
|
|
193
|
+
role: payload.role,
|
|
194
|
+
clientKind: payload.clientKind,
|
|
195
|
+
},
|
|
168
196
|
sessionId,
|
|
169
197
|
payload,
|
|
170
198
|
});
|
|
@@ -198,13 +226,15 @@ function isActiveAgentSnapshot(snapshot) {
|
|
|
198
226
|
return true;
|
|
199
227
|
}
|
|
200
228
|
|
|
201
|
-
function chooseAgentRefreshModel(value, existingModel) {
|
|
229
|
+
function chooseAgentRefreshModel(value, existingModel, sourceValue = value) {
|
|
202
230
|
const candidate = normalizeString(value);
|
|
203
231
|
const existing = normalizeString(existingModel);
|
|
232
|
+
const source = normalizeString(sourceValue).toLowerCase();
|
|
233
|
+
const sourceIsWeak = !source || ["anonymous", "cli", "unknown", "unreported"].includes(source);
|
|
204
234
|
if (!candidate) {
|
|
205
235
|
return existing || "unknown";
|
|
206
236
|
}
|
|
207
|
-
if (existing && ["cli", "unknown"].includes(candidate.toLowerCase())) {
|
|
237
|
+
if (existing && (sourceIsWeak || ["cli", "unknown"].includes(candidate.toLowerCase()))) {
|
|
208
238
|
return existing;
|
|
209
239
|
}
|
|
210
240
|
return candidate;
|
|
@@ -222,6 +252,19 @@ function chooseAgentRefreshRole(value, existingRole) {
|
|
|
222
252
|
return candidate;
|
|
223
253
|
}
|
|
224
254
|
|
|
255
|
+
function chooseAgentRefreshMetadata(value, existingValue, sourceValue = value) {
|
|
256
|
+
const candidate = normalizeString(value);
|
|
257
|
+
const existing = normalizeString(existingValue);
|
|
258
|
+
const source = normalizeString(sourceValue);
|
|
259
|
+
if (!candidate) {
|
|
260
|
+
return existing;
|
|
261
|
+
}
|
|
262
|
+
if (existing && !source) {
|
|
263
|
+
return existing;
|
|
264
|
+
}
|
|
265
|
+
return candidate;
|
|
266
|
+
}
|
|
267
|
+
|
|
225
268
|
// In-process registry of agents registered by *this* CLI process. The
|
|
226
269
|
// dashboard treats any participant without a terminal agent_leave /
|
|
227
270
|
// agent_killed / session_killed event as "active". When a CLI exits via
|
|
@@ -283,6 +326,9 @@ export async function registerAgent(
|
|
|
283
326
|
{
|
|
284
327
|
agentId = "",
|
|
285
328
|
model = "",
|
|
329
|
+
displayName = "",
|
|
330
|
+
provider = "",
|
|
331
|
+
clientKind = "",
|
|
286
332
|
role = "observer",
|
|
287
333
|
targetPath = process.cwd(),
|
|
288
334
|
trackProcessExit = true,
|
|
@@ -311,6 +357,14 @@ export async function registerAgent(
|
|
|
311
357
|
}
|
|
312
358
|
}
|
|
313
359
|
|
|
360
|
+
const identity = inferSessionAgentIdentity({
|
|
361
|
+
agentId: resolvedAgentId,
|
|
362
|
+
model,
|
|
363
|
+
displayName,
|
|
364
|
+
provider,
|
|
365
|
+
clientKind,
|
|
366
|
+
});
|
|
367
|
+
|
|
314
368
|
const snapshotPath = buildAgentSnapshotPath(paths, resolvedAgentId);
|
|
315
369
|
const existing = await readAgentSnapshot(snapshotPath);
|
|
316
370
|
|
|
@@ -320,7 +374,10 @@ export async function registerAgent(
|
|
|
320
374
|
...existing,
|
|
321
375
|
sessionId: paths.sessionId,
|
|
322
376
|
agentId: resolvedAgentId,
|
|
323
|
-
model: chooseAgentRefreshModel(model, existing.model),
|
|
377
|
+
model: chooseAgentRefreshModel(identity.model, existing.model, model),
|
|
378
|
+
displayName: chooseAgentRefreshMetadata(identity.displayName, existing.displayName, displayName),
|
|
379
|
+
provider: chooseAgentRefreshMetadata(identity.provider, existing.provider, provider),
|
|
380
|
+
clientKind: chooseAgentRefreshMetadata(identity.clientKind, existing.clientKind, clientKind),
|
|
324
381
|
role: chooseAgentRefreshRole(role, existing.role),
|
|
325
382
|
status: normalizeString(existing.status) || "idle",
|
|
326
383
|
detail: normalizeString(existing.detail) || "",
|
|
@@ -350,7 +407,10 @@ export async function registerAgent(
|
|
|
350
407
|
{
|
|
351
408
|
sessionId: paths.sessionId,
|
|
352
409
|
agentId: resolvedAgentId,
|
|
353
|
-
model: normalizeString(model) || "unknown",
|
|
410
|
+
model: normalizeString(identity.model) || "unknown",
|
|
411
|
+
displayName: normalizeString(identity.displayName),
|
|
412
|
+
provider: normalizeString(identity.provider),
|
|
413
|
+
clientKind: normalizeString(identity.clientKind),
|
|
354
414
|
role,
|
|
355
415
|
status: "idle",
|
|
356
416
|
detail: "",
|
|
@@ -369,6 +429,9 @@ export async function registerAgent(
|
|
|
369
429
|
await emitAgentEvent(paths.sessionId, "agent_join", {
|
|
370
430
|
agentId: snapshot.agentId,
|
|
371
431
|
model: snapshot.model,
|
|
432
|
+
displayName: snapshot.displayName,
|
|
433
|
+
provider: snapshot.provider,
|
|
434
|
+
clientKind: snapshot.clientKind,
|
|
372
435
|
role: snapshot.role,
|
|
373
436
|
status: snapshot.status,
|
|
374
437
|
}, { targetPath, awaitRemoteSync });
|
|
@@ -412,6 +475,9 @@ export async function rememberAgentIdentity(
|
|
|
412
475
|
{
|
|
413
476
|
agentId = "",
|
|
414
477
|
model = "",
|
|
478
|
+
displayName = "",
|
|
479
|
+
provider = "",
|
|
480
|
+
clientKind = "",
|
|
415
481
|
role = "observer",
|
|
416
482
|
targetPath = process.cwd(),
|
|
417
483
|
} = {}
|
|
@@ -422,6 +488,13 @@ export async function rememberAgentIdentity(
|
|
|
422
488
|
if (!resolvedAgentId) {
|
|
423
489
|
throw new Error("agentId is required.");
|
|
424
490
|
}
|
|
491
|
+
const identity = inferSessionAgentIdentity({
|
|
492
|
+
agentId: resolvedAgentId,
|
|
493
|
+
model,
|
|
494
|
+
displayName,
|
|
495
|
+
provider,
|
|
496
|
+
clientKind,
|
|
497
|
+
});
|
|
425
498
|
|
|
426
499
|
const snapshotPath = buildAgentSnapshotPath(paths, resolvedAgentId);
|
|
427
500
|
const existing = await readAgentSnapshot(snapshotPath);
|
|
@@ -430,7 +503,10 @@ export async function rememberAgentIdentity(
|
|
|
430
503
|
...(existing || {}),
|
|
431
504
|
sessionId: paths.sessionId,
|
|
432
505
|
agentId: resolvedAgentId,
|
|
433
|
-
model:
|
|
506
|
+
model: chooseAgentRefreshModel(identity.model, existing?.model, model),
|
|
507
|
+
displayName: chooseAgentRefreshMetadata(identity.displayName, existing?.displayName, displayName),
|
|
508
|
+
provider: chooseAgentRefreshMetadata(identity.provider, existing?.provider, provider),
|
|
509
|
+
clientKind: chooseAgentRefreshMetadata(identity.clientKind, existing?.clientKind, clientKind),
|
|
434
510
|
role: normalizeString(role) || normalizeString(existing?.role) || "observer",
|
|
435
511
|
status: normalizeString(existing?.status) || "idle",
|
|
436
512
|
detail: normalizeString(existing?.detail) || "",
|
|
@@ -536,6 +612,9 @@ export async function unregisterAgent(
|
|
|
536
612
|
reason: normalizedReason,
|
|
537
613
|
role: snapshot.role,
|
|
538
614
|
model: snapshot.model,
|
|
615
|
+
displayName: snapshot.displayName,
|
|
616
|
+
provider: snapshot.provider,
|
|
617
|
+
clientKind: snapshot.clientKind,
|
|
539
618
|
}, { targetPath });
|
|
540
619
|
// Already left explicitly — don't double-emit on process exit.
|
|
541
620
|
_untrackLocalAgent(paths.sessionId, snapshot.agentId);
|
|
@@ -15,6 +15,24 @@ function timestampKey(...values) {
|
|
|
15
15
|
return "";
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export function readSessionEventSequence(event = {}) {
|
|
19
|
+
const value = Number(event?.sequenceId ?? event?.sequence_id ?? event?.sequence);
|
|
20
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function sessionEventHasDurableCursor(event = {}) {
|
|
24
|
+
return Boolean(keyString(event?.cursor));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sessionEventUpgradesExisting(existingEvent = {}, candidateEvent = {}) {
|
|
28
|
+
const existingSequence = readSessionEventSequence(existingEvent);
|
|
29
|
+
const candidateSequence = readSessionEventSequence(candidateEvent);
|
|
30
|
+
if (candidateSequence > 0 && existingSequence <= 0) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return sessionEventHasDurableCursor(candidateEvent) && !sessionEventHasDurableCursor(existingEvent);
|
|
34
|
+
}
|
|
35
|
+
|
|
18
36
|
function stableJsonValue(value) {
|
|
19
37
|
if (Array.isArray(value)) {
|
|
20
38
|
return value.map((item) => stableJsonValue(item));
|
|
@@ -51,6 +69,10 @@ export function sessionEventIdentityKeys(event = {}) {
|
|
|
51
69
|
keys.push(`idempotency:${event.idempotencyToken.trim()}`);
|
|
52
70
|
}
|
|
53
71
|
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
72
|
+
const clientMessageId = typeof payload.clientMessageId === "string" ? payload.clientMessageId.trim() : "";
|
|
73
|
+
if (clientMessageId) {
|
|
74
|
+
keys.push(`client-message:${clientMessageId}`);
|
|
75
|
+
}
|
|
54
76
|
const messageId = typeof payload.messageId === "string" ? payload.messageId.trim() : "";
|
|
55
77
|
if (messageId) {
|
|
56
78
|
keys.push(`message:${messageId}`);
|
|
@@ -29,20 +29,48 @@ import {
|
|
|
29
29
|
import { cursorAdvances, readSyncCursor, writeSyncCursor } from "./sync-cursor.js";
|
|
30
30
|
import {
|
|
31
31
|
addSessionEventIdentityKeys,
|
|
32
|
+
sessionEventIdentityKeys,
|
|
32
33
|
sessionEventHasKnownIdentity,
|
|
34
|
+
sessionEventUpgradesExisting,
|
|
33
35
|
} from "./event-identity.js";
|
|
34
36
|
|
|
35
37
|
const EVENTS_CURSOR_SUFFIX = "events";
|
|
36
38
|
const DEFAULT_EVENT_PAGE_LIMIT = 200;
|
|
37
39
|
const DEFAULT_MAX_EVENT_PAGES = 25;
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
function indexRelayEventIdentityKeys(indexByKey, eventList = [], event = {}, index = -1) {
|
|
42
|
+
if (!indexByKey || !Array.isArray(eventList) || index < 0) return;
|
|
43
|
+
for (const key of sessionEventIdentityKeys(event)) {
|
|
44
|
+
const existingIndex = indexByKey.get(key);
|
|
45
|
+
const existingEvent = Number.isInteger(existingIndex) && existingIndex >= 0
|
|
46
|
+
? eventList[existingIndex]
|
|
47
|
+
: null;
|
|
48
|
+
if (!existingEvent || sessionEventUpgradesExisting(existingEvent, event)) {
|
|
49
|
+
indexByKey.set(key, index);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findRelayEventIdentityIndex(indexByKey, event = {}) {
|
|
55
|
+
if (!indexByKey) return -1;
|
|
56
|
+
for (const key of sessionEventIdentityKeys(event)) {
|
|
57
|
+
const index = indexByKey.get(key);
|
|
58
|
+
if (Number.isInteger(index) && index >= 0) {
|
|
59
|
+
return index;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return -1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function readExistingRelayState(sessionId, { targetPath = process.cwd() } = {}) {
|
|
40
66
|
const knownKeys = new Set();
|
|
41
67
|
const events = await readStream(sessionId, { targetPath, tail: 0 }).catch(() => []);
|
|
42
|
-
|
|
68
|
+
const indexByKey = new Map();
|
|
69
|
+
for (const [index, event] of events.entries()) {
|
|
43
70
|
addSessionEventIdentityKeys(knownKeys, event);
|
|
71
|
+
indexRelayEventIdentityKeys(indexByKey, events, event, index);
|
|
44
72
|
}
|
|
45
|
-
return knownKeys;
|
|
73
|
+
return { knownKeys, events, indexByKey };
|
|
46
74
|
}
|
|
47
75
|
|
|
48
76
|
async function ensureLocalSessionShell(sessionId, { targetPath = process.cwd() } = {}) {
|
|
@@ -92,10 +120,13 @@ async function ensureLocalSessionShell(sessionId, { targetPath = process.cwd() }
|
|
|
92
120
|
return { materialized: true, refreshed: false, session: created, remoteStatus };
|
|
93
121
|
}
|
|
94
122
|
|
|
95
|
-
function sourceFullyRelayed(events = [], successfulKeys = new Set()) {
|
|
123
|
+
function sourceFullyRelayed(events = [], successfulKeys = new Set(), failedKeys = new Set()) {
|
|
96
124
|
const relayedEvents = Array.isArray(events) ? events : [];
|
|
97
125
|
if (relayedEvents.length === 0) return true;
|
|
98
|
-
return relayedEvents.every((event) =>
|
|
126
|
+
return relayedEvents.every((event) =>
|
|
127
|
+
!sessionEventHasKnownIdentity(event, failedKeys) &&
|
|
128
|
+
sessionEventHasKnownIdentity(event, successfulKeys)
|
|
129
|
+
);
|
|
99
130
|
}
|
|
100
131
|
|
|
101
132
|
function markPostKillEvent(event = {}) {
|
|
@@ -341,11 +372,26 @@ export async function hydrateSessionFromRemote({
|
|
|
341
372
|
let relayed = 0;
|
|
342
373
|
let materializedLocalSession = false;
|
|
343
374
|
let remoteStatus = "";
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
375
|
+
const relayState = merged.length > 0
|
|
376
|
+
? await readExistingRelayState(sessionId, { targetPath })
|
|
377
|
+
: { knownKeys: new Set(), events: [], indexByKey: new Map() };
|
|
378
|
+
const successfulRelayKeys = relayState.knownKeys;
|
|
379
|
+
const existingRelayEvents = relayState.events;
|
|
380
|
+
const existingRelayIndexByKey = relayState.indexByKey;
|
|
381
|
+
const newEvents = [];
|
|
382
|
+
for (const event of merged) {
|
|
383
|
+
const existingIndex = findRelayEventIdentityIndex(existingRelayIndexByKey, event);
|
|
384
|
+
if (existingIndex >= 0) {
|
|
385
|
+
const existing = existingRelayEvents[existingIndex];
|
|
386
|
+
if (sessionEventUpgradesExisting(existing, event)) {
|
|
387
|
+
newEvents.push(event);
|
|
388
|
+
}
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (!sessionEventHasKnownIdentity(event, successfulRelayKeys)) {
|
|
392
|
+
newEvents.push(event);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
349
395
|
if (newEvents.length > 0) {
|
|
350
396
|
try {
|
|
351
397
|
const localSession = await _ensureLocalSession(sessionId, { targetPath });
|
|
@@ -361,20 +407,30 @@ export async function hydrateSessionFromRemote({
|
|
|
361
407
|
remoteStatus && !["active", "pending"].includes(remoteStatus)
|
|
362
408
|
? newEvents.map((event) => markPostKillEvent(event))
|
|
363
409
|
: newEvents;
|
|
410
|
+
const failedRelayKeys = new Set();
|
|
364
411
|
for (const event of appendEvents) {
|
|
365
412
|
try {
|
|
366
|
-
await _append(sessionId, event, { targetPath, syncRemote: false });
|
|
413
|
+
const persisted = await _append(sessionId, event, { targetPath, syncRemote: false });
|
|
367
414
|
relayed += 1;
|
|
368
|
-
|
|
415
|
+
const persistedEvent = persisted && typeof persisted === "object" ? persisted : event;
|
|
416
|
+
existingRelayEvents.push(persistedEvent);
|
|
417
|
+
addSessionEventIdentityKeys(successfulRelayKeys, persistedEvent);
|
|
418
|
+
indexRelayEventIdentityKeys(
|
|
419
|
+
existingRelayIndexByKey,
|
|
420
|
+
existingRelayEvents,
|
|
421
|
+
persistedEvent,
|
|
422
|
+
existingRelayEvents.length - 1,
|
|
423
|
+
);
|
|
369
424
|
} catch {
|
|
425
|
+
addSessionEventIdentityKeys(failedRelayKeys, event);
|
|
370
426
|
// Append errors are observable via the stream but should not
|
|
371
427
|
// abort the rest of the batch — partial relay is still progress.
|
|
372
428
|
}
|
|
373
429
|
}
|
|
374
430
|
|
|
375
431
|
let persistedCursor = false;
|
|
376
|
-
const humanCursorSafe = sourceFullyRelayed(humanResult?.events || [], successfulRelayKeys);
|
|
377
|
-
const eventsCursorSafe = sourceFullyRelayed(eventsResult?.events || [], successfulRelayKeys);
|
|
432
|
+
const humanCursorSafe = sourceFullyRelayed(humanResult?.events || [], successfulRelayKeys, failedRelayKeys);
|
|
433
|
+
const eventsCursorSafe = sourceFullyRelayed(eventsResult?.events || [], successfulRelayKeys, failedRelayKeys);
|
|
378
434
|
if (humanCursorSafe && typeof humanResult?.cursor === "string" && humanResult.cursor.trim()) {
|
|
379
435
|
const result = await writeSyncCursor(sessionId, humanResult.cursor, { targetPath }).catch(() => null);
|
|
380
436
|
persistedCursor = Boolean(result && result.written);
|