sentinelayer-cli 0.30.1 → 0.30.3
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 +146 -48
- package/src/session/agent-identity.js +119 -0
- package/src/session/agent-registry.js +87 -8
- package/src/session/file-locks.js +12 -0
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,
|
|
@@ -42,7 +43,9 @@ import {
|
|
|
42
43
|
import { listRuntimeRuns } from "../session/runtime-bridge.js";
|
|
43
44
|
import {
|
|
44
45
|
listFileLocks,
|
|
46
|
+
lockFile,
|
|
45
47
|
releaseFileLocksForAgent,
|
|
48
|
+
unlockFile,
|
|
46
49
|
} from "../session/file-locks.js";
|
|
47
50
|
import {
|
|
48
51
|
injectSessionGuides,
|
|
@@ -1605,20 +1608,30 @@ async function publishListenerPresenceEvent({
|
|
|
1605
1608
|
agentId,
|
|
1606
1609
|
agentModel = "cli",
|
|
1607
1610
|
displayName = "",
|
|
1611
|
+
provider = "",
|
|
1612
|
+
clientKind = "cli",
|
|
1608
1613
|
listenerId,
|
|
1609
1614
|
lifecycle = {},
|
|
1610
1615
|
} = {}) {
|
|
1611
1616
|
const normalizedType = normalizeString(lifecycle.type) || "heartbeat";
|
|
1612
1617
|
const eventName = listenerLifecycleEventName(normalizedType);
|
|
1618
|
+
const identity = inferSessionAgentIdentity({
|
|
1619
|
+
agentId,
|
|
1620
|
+
model: agentModel,
|
|
1621
|
+
displayName,
|
|
1622
|
+
provider,
|
|
1623
|
+
clientKind,
|
|
1624
|
+
});
|
|
1613
1625
|
const event = createAgentEvent({
|
|
1614
1626
|
event: eventName,
|
|
1615
1627
|
sessionId,
|
|
1616
1628
|
agent: {
|
|
1617
1629
|
id: agentId,
|
|
1618
|
-
model: normalizeString(
|
|
1630
|
+
model: normalizeString(identity.model) || "cli",
|
|
1619
1631
|
role: "listener",
|
|
1620
|
-
displayName: normalizeString(displayName) || agentId,
|
|
1621
|
-
|
|
1632
|
+
displayName: normalizeString(identity.displayName) || agentId,
|
|
1633
|
+
provider: normalizeString(identity.provider) || undefined,
|
|
1634
|
+
clientKind: normalizeString(identity.clientKind) || "cli",
|
|
1622
1635
|
},
|
|
1623
1636
|
eventId: `session-listener-${listenerId}-${normalizedType}-${lifecycle.pollCount ?? 0}`,
|
|
1624
1637
|
idempotencyToken: `session-listener:${listenerId}:${normalizedType}:${lifecycle.pollCount ?? 0}`,
|
|
@@ -1674,20 +1687,30 @@ export function buildSessionCoachingEvent({
|
|
|
1674
1687
|
agentId,
|
|
1675
1688
|
agentModel = "cli",
|
|
1676
1689
|
displayName = "",
|
|
1690
|
+
provider = "",
|
|
1691
|
+
clientKind = "cli",
|
|
1677
1692
|
listenerId = "",
|
|
1678
1693
|
tick = 0,
|
|
1679
1694
|
tips = SESSION_LIVE_SUCCESS_TIPS,
|
|
1680
1695
|
} = {}) {
|
|
1681
1696
|
const tipList = Array.isArray(tips) && tips.length ? tips : SESSION_LIVE_SUCCESS_TIPS;
|
|
1697
|
+
const identity = inferSessionAgentIdentity({
|
|
1698
|
+
agentId,
|
|
1699
|
+
model: agentModel,
|
|
1700
|
+
displayName,
|
|
1701
|
+
provider,
|
|
1702
|
+
clientKind,
|
|
1703
|
+
});
|
|
1682
1704
|
return createAgentEvent({
|
|
1683
1705
|
event: "session_coaching",
|
|
1684
1706
|
sessionId,
|
|
1685
1707
|
agent: {
|
|
1686
1708
|
id: agentId,
|
|
1687
|
-
model: normalizeString(
|
|
1709
|
+
model: normalizeString(identity.model) || "cli",
|
|
1688
1710
|
role: "listener",
|
|
1689
|
-
displayName: normalizeString(displayName) || agentId,
|
|
1690
|
-
|
|
1711
|
+
displayName: normalizeString(identity.displayName) || agentId,
|
|
1712
|
+
provider: normalizeString(identity.provider) || undefined,
|
|
1713
|
+
clientKind: normalizeString(identity.clientKind) || "cli",
|
|
1691
1714
|
},
|
|
1692
1715
|
eventId: `session-coaching-${listenerId || agentId}-${tick}`,
|
|
1693
1716
|
idempotencyToken: `session-coaching:${listenerId || agentId}:${tick}`,
|
|
@@ -1705,20 +1728,30 @@ function buildListenerCatchupEvent({
|
|
|
1705
1728
|
agentId,
|
|
1706
1729
|
agentModel = "cli",
|
|
1707
1730
|
displayName = "",
|
|
1731
|
+
provider = "",
|
|
1732
|
+
clientKind = "cli",
|
|
1708
1733
|
listenerId,
|
|
1709
1734
|
catchup = {},
|
|
1710
1735
|
} = {}) {
|
|
1711
1736
|
const message = formatListenerCatchupNotice(catchup);
|
|
1712
1737
|
const pollCount = Number(catchup.pollCount || 0);
|
|
1738
|
+
const identity = inferSessionAgentIdentity({
|
|
1739
|
+
agentId,
|
|
1740
|
+
model: agentModel,
|
|
1741
|
+
displayName,
|
|
1742
|
+
provider,
|
|
1743
|
+
clientKind,
|
|
1744
|
+
});
|
|
1713
1745
|
return createAgentEvent({
|
|
1714
1746
|
event: "session_listen_catchup",
|
|
1715
1747
|
sessionId,
|
|
1716
1748
|
agent: {
|
|
1717
1749
|
id: agentId,
|
|
1718
|
-
model: normalizeString(
|
|
1750
|
+
model: normalizeString(identity.model) || "cli",
|
|
1719
1751
|
role: "listener",
|
|
1720
|
-
displayName: normalizeString(displayName) || agentId,
|
|
1721
|
-
|
|
1752
|
+
displayName: normalizeString(identity.displayName) || agentId,
|
|
1753
|
+
provider: normalizeString(identity.provider) || undefined,
|
|
1754
|
+
clientKind: normalizeString(identity.clientKind) || "cli",
|
|
1722
1755
|
},
|
|
1723
1756
|
eventId: `session-listener-${listenerId}-catchup-${pollCount}`,
|
|
1724
1757
|
idempotencyToken: `session-listener:${listenerId}:catchup:${pollCount}`,
|
|
@@ -1957,6 +1990,9 @@ async function ensureSessionSayAgentRegistered(
|
|
|
1957
1990
|
const registered = await rememberAgentIdentity(sessionId, {
|
|
1958
1991
|
agentId,
|
|
1959
1992
|
model: normalizeString(agent.model) || "cli",
|
|
1993
|
+
displayName: normalizeString(agent.displayName),
|
|
1994
|
+
provider: normalizeString(agent.provider),
|
|
1995
|
+
clientKind: normalizeString(agent.clientKind) || "cli",
|
|
1960
1996
|
role: sessionSayRegistryRole(agent.role),
|
|
1961
1997
|
targetPath,
|
|
1962
1998
|
});
|
|
@@ -1993,12 +2029,24 @@ async function resolveSessionAgentEnvelope(
|
|
|
1993
2029
|
const resolvedRole = normalizeString(role) || normalizeString(registeredAgent?.role);
|
|
1994
2030
|
const resolvedDisplayName =
|
|
1995
2031
|
normalizeString(displayName) || normalizeString(registeredAgent?.displayName);
|
|
2032
|
+
const identity = inferSessionAgentIdentity({
|
|
2033
|
+
agentId: normalizedAgentId,
|
|
2034
|
+
model: resolvedModel,
|
|
2035
|
+
displayName: resolvedDisplayName,
|
|
2036
|
+
provider: registeredAgent?.provider,
|
|
2037
|
+
clientKind: normalizeString(clientKind) || normalizeString(registeredAgent?.clientKind) || "cli",
|
|
2038
|
+
});
|
|
1996
2039
|
const envelope = {
|
|
1997
2040
|
id: normalizedAgentId,
|
|
1998
|
-
model: resolvedModel || undefined,
|
|
2041
|
+
model: normalizeString(identity.model) || resolvedModel || undefined,
|
|
1999
2042
|
role: resolvedRole || undefined,
|
|
2000
|
-
displayName: resolvedDisplayName || undefined,
|
|
2001
|
-
|
|
2043
|
+
displayName: normalizeString(identity.displayName) || resolvedDisplayName || undefined,
|
|
2044
|
+
provider: normalizeString(identity.provider) || undefined,
|
|
2045
|
+
clientKind:
|
|
2046
|
+
normalizeString(identity.clientKind) ||
|
|
2047
|
+
normalizeString(clientKind) ||
|
|
2048
|
+
normalizeString(registeredAgent?.clientKind) ||
|
|
2049
|
+
undefined,
|
|
2002
2050
|
};
|
|
2003
2051
|
return Object.fromEntries(Object.entries(envelope).filter(([, value]) => value !== undefined));
|
|
2004
2052
|
}
|
|
@@ -2014,30 +2062,6 @@ export function buildSessionLockDirective(verb, file, intent = "") {
|
|
|
2014
2062
|
return normalizedIntent ? `lock: ${normalizedFile} - ${normalizedIntent}` : `lock: ${normalizedFile}`;
|
|
2015
2063
|
}
|
|
2016
2064
|
|
|
2017
|
-
// Posts a coordination directive (e.g. "lock: <file> - <intent>") as a session
|
|
2018
|
-
// message so the session daemon processes it into the authoritative file-lock
|
|
2019
|
-
// registry. Used by `session lock`/`unlock` as ergonomic sugar over the
|
|
2020
|
-
// say-convention; locks are advisory + daemon-enforced with TTL auto-release,
|
|
2021
|
-
// so this is a best-effort post.
|
|
2022
|
-
async function postSessionDirectiveMessage(sessionId, message, {
|
|
2023
|
-
agentId,
|
|
2024
|
-
targetPath = process.cwd(),
|
|
2025
|
-
} = {}) {
|
|
2026
|
-
const clientMessageId = `cli-${randomUUID()}`;
|
|
2027
|
-
const agent = await resolveSessionAgentEnvelope(sessionId, agentId, { targetPath });
|
|
2028
|
-
await ensureSessionSayAgentRegistered(sessionId, agent, { targetPath });
|
|
2029
|
-
const event = createAgentEvent({
|
|
2030
|
-
event: "session_message",
|
|
2031
|
-
agent,
|
|
2032
|
-
sessionId,
|
|
2033
|
-
payload: { message, channel: "session", clientMessageId },
|
|
2034
|
-
});
|
|
2035
|
-
event.eventId = clientMessageId;
|
|
2036
|
-
event.idempotencyToken = clientMessageId;
|
|
2037
|
-
const result = await syncSessionEventToApi(sessionId, event, { targetPath });
|
|
2038
|
-
return { event, result };
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
2065
|
async function runWithConcurrency(items = [], concurrency = 1, worker = async () => null) {
|
|
2042
2066
|
const normalizedItems = Array.isArray(items) ? items : [];
|
|
2043
2067
|
const normalizedConcurrency = Math.max(
|
|
@@ -3043,6 +3067,8 @@ export function registerSessionCommand(program) {
|
|
|
3043
3067
|
targetPath,
|
|
3044
3068
|
agentId: resolvedAgentId,
|
|
3045
3069
|
model,
|
|
3070
|
+
displayName: normalizeString(acceptedOnboarding?.displayName),
|
|
3071
|
+
clientKind: "cli",
|
|
3046
3072
|
role,
|
|
3047
3073
|
trackProcessExit: false,
|
|
3048
3074
|
awaitRemoteSync: hasConcreteAgentIdentity,
|
|
@@ -3081,6 +3107,9 @@ export function registerSessionCommand(program) {
|
|
|
3081
3107
|
agentId: joined.agentId,
|
|
3082
3108
|
role: joined.role,
|
|
3083
3109
|
model: joined.model,
|
|
3110
|
+
displayName: joined.displayName || null,
|
|
3111
|
+
provider: joined.provider || null,
|
|
3112
|
+
clientKind: joined.clientKind || null,
|
|
3084
3113
|
status: joined.status,
|
|
3085
3114
|
joinedAt: joined.joinedAt,
|
|
3086
3115
|
materializedLocalSession: localSession.materialized,
|
|
@@ -3327,12 +3356,19 @@ export function registerSessionCommand(program) {
|
|
|
3327
3356
|
if (to) {
|
|
3328
3357
|
eventPayload.to = to;
|
|
3329
3358
|
}
|
|
3359
|
+
const agentIdentity = inferSessionAgentIdentity({
|
|
3360
|
+
agentId,
|
|
3361
|
+
model: options.model,
|
|
3362
|
+
displayName: options.displayName,
|
|
3363
|
+
clientKind: "cli",
|
|
3364
|
+
});
|
|
3330
3365
|
const agent = {
|
|
3331
3366
|
id: agentId,
|
|
3332
|
-
model: normalizeString(
|
|
3333
|
-
displayName: normalizeString(
|
|
3367
|
+
model: normalizeString(agentIdentity.model) || "cli",
|
|
3368
|
+
displayName: normalizeString(agentIdentity.displayName) || undefined,
|
|
3369
|
+
provider: normalizeString(agentIdentity.provider) || undefined,
|
|
3334
3370
|
role: normalizeString(options.role) || "coder",
|
|
3335
|
-
clientKind: "cli",
|
|
3371
|
+
clientKind: normalizeString(agentIdentity.clientKind) || "cli",
|
|
3336
3372
|
};
|
|
3337
3373
|
const event = createAgentEvent({
|
|
3338
3374
|
event: "session_message",
|
|
@@ -3672,7 +3708,7 @@ export function registerSessionCommand(program) {
|
|
|
3672
3708
|
return payload;
|
|
3673
3709
|
});
|
|
3674
3710
|
|
|
3675
|
-
const
|
|
3711
|
+
const runFileLockCommand = async (verb, sessionId, files, options, command) => {
|
|
3676
3712
|
const normalizedSessionId = normalizeString(sessionId);
|
|
3677
3713
|
if (!normalizedSessionId) {
|
|
3678
3714
|
throw new Error("session id is required.");
|
|
@@ -3698,20 +3734,68 @@ export function registerSessionCommand(program) {
|
|
|
3698
3734
|
);
|
|
3699
3735
|
}
|
|
3700
3736
|
const intent = normalizeString(options.intent);
|
|
3737
|
+
const results = [];
|
|
3701
3738
|
const processed = [];
|
|
3739
|
+
const skipped = [];
|
|
3740
|
+
const failed = [];
|
|
3702
3741
|
for (const file of fileList) {
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3742
|
+
if (verb === "lock") {
|
|
3743
|
+
const result = await lockFile(normalizedSessionId, identity.agentId, file, {
|
|
3744
|
+
intent,
|
|
3745
|
+
targetPath,
|
|
3746
|
+
awaitRemoteSync: true,
|
|
3747
|
+
});
|
|
3748
|
+
const record = {
|
|
3749
|
+
file: result.file || file,
|
|
3750
|
+
locked: Boolean(result.locked),
|
|
3751
|
+
reason: result.reason || (result.locked ? "locked" : "held_by_other_agent"),
|
|
3752
|
+
heldBy: result.heldBy || null,
|
|
3753
|
+
since: result.since || null,
|
|
3754
|
+
};
|
|
3755
|
+
results.push(record);
|
|
3756
|
+
if (result.locked) {
|
|
3757
|
+
processed.push(record.file);
|
|
3758
|
+
} else {
|
|
3759
|
+
failed.push(record);
|
|
3760
|
+
}
|
|
3761
|
+
continue;
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
const result = await unlockFile(normalizedSessionId, identity.agentId, file, {
|
|
3765
|
+
reason: intent || "manual_release",
|
|
3706
3766
|
targetPath,
|
|
3767
|
+
awaitRemoteSync: true,
|
|
3707
3768
|
});
|
|
3708
|
-
|
|
3769
|
+
const record = {
|
|
3770
|
+
file: result.file || file,
|
|
3771
|
+
unlocked: Boolean(result.unlocked),
|
|
3772
|
+
reason: result.reason || (result.unlocked ? "unlocked" : "not_locked"),
|
|
3773
|
+
heldBy: result.heldBy || null,
|
|
3774
|
+
since: result.since || null,
|
|
3775
|
+
};
|
|
3776
|
+
results.push(record);
|
|
3777
|
+
if (result.unlocked) {
|
|
3778
|
+
processed.push(record.file);
|
|
3779
|
+
} else if (record.reason === "not_locked") {
|
|
3780
|
+
skipped.push(record);
|
|
3781
|
+
} else {
|
|
3782
|
+
failed.push(record);
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
if (failed.length > 0) {
|
|
3787
|
+
const summary = failed
|
|
3788
|
+
.map((item) => `${item.file}${item.heldBy ? ` held by ${item.heldBy}` : ""}`)
|
|
3789
|
+
.join(", ");
|
|
3790
|
+
throw new Error(`session ${verb} failed for ${summary}`);
|
|
3709
3791
|
}
|
|
3710
3792
|
const payload = {
|
|
3711
3793
|
command: `session ${verb}`,
|
|
3712
3794
|
sessionId: normalizedSessionId,
|
|
3713
3795
|
agentId: identity.agentId,
|
|
3714
3796
|
files: processed,
|
|
3797
|
+
results,
|
|
3798
|
+
skipped,
|
|
3715
3799
|
};
|
|
3716
3800
|
if (shouldEmitJson(options, command)) {
|
|
3717
3801
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -3735,7 +3819,7 @@ export function registerSessionCommand(program) {
|
|
|
3735
3819
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
3736
3820
|
.option("--json", "Emit machine-readable output")
|
|
3737
3821
|
.action(async (sessionId, files, options, command) => {
|
|
3738
|
-
await
|
|
3822
|
+
await runFileLockCommand("lock", sessionId, files, options, command);
|
|
3739
3823
|
});
|
|
3740
3824
|
|
|
3741
3825
|
session
|
|
@@ -3746,7 +3830,7 @@ export function registerSessionCommand(program) {
|
|
|
3746
3830
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
3747
3831
|
.option("--json", "Emit machine-readable output")
|
|
3748
3832
|
.action(async (sessionId, files, options, command) => {
|
|
3749
|
-
await
|
|
3833
|
+
await runFileLockCommand("unlock", sessionId, files, options, command);
|
|
3750
3834
|
});
|
|
3751
3835
|
|
|
3752
3836
|
session
|
|
@@ -3966,8 +4050,16 @@ export function registerSessionCommand(program) {
|
|
|
3966
4050
|
"presence-interval",
|
|
3967
4051
|
60,
|
|
3968
4052
|
);
|
|
3969
|
-
const
|
|
3970
|
-
|
|
4053
|
+
const listenerIdentity = inferSessionAgentIdentity({
|
|
4054
|
+
agentId,
|
|
4055
|
+
model: options.model,
|
|
4056
|
+
displayName: options.displayName,
|
|
4057
|
+
clientKind: "cli",
|
|
4058
|
+
});
|
|
4059
|
+
const agentModel = normalizeString(listenerIdentity.model) || "cli";
|
|
4060
|
+
const displayName = normalizeString(listenerIdentity.displayName) || agentId;
|
|
4061
|
+
const provider = normalizeString(listenerIdentity.provider);
|
|
4062
|
+
const clientKind = normalizeString(listenerIdentity.clientKind) || "cli";
|
|
3971
4063
|
const limit = parsePositiveInteger(options.limit, "limit", 200);
|
|
3972
4064
|
const emitFormat = normalizeString(options.emit).toLowerCase() || "ndjson";
|
|
3973
4065
|
if (!["ndjson", "text"].includes(emitFormat)) {
|
|
@@ -4059,6 +4151,8 @@ export function registerSessionCommand(program) {
|
|
|
4059
4151
|
agentId,
|
|
4060
4152
|
agentModel,
|
|
4061
4153
|
displayName,
|
|
4154
|
+
provider,
|
|
4155
|
+
clientKind,
|
|
4062
4156
|
listenerId,
|
|
4063
4157
|
tick: coachingTick++,
|
|
4064
4158
|
}),
|
|
@@ -4129,6 +4223,8 @@ export function registerSessionCommand(program) {
|
|
|
4129
4223
|
agentId,
|
|
4130
4224
|
agentModel,
|
|
4131
4225
|
displayName,
|
|
4226
|
+
provider,
|
|
4227
|
+
clientKind,
|
|
4132
4228
|
listenerId,
|
|
4133
4229
|
catchup,
|
|
4134
4230
|
}),
|
|
@@ -4231,6 +4327,8 @@ export function registerSessionCommand(program) {
|
|
|
4231
4327
|
agentId,
|
|
4232
4328
|
agentModel,
|
|
4233
4329
|
displayName,
|
|
4330
|
+
provider,
|
|
4331
|
+
clientKind,
|
|
4234
4332
|
listenerId,
|
|
4235
4333
|
lifecycle,
|
|
4236
4334
|
});
|
|
@@ -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);
|
|
@@ -260,6 +260,8 @@ async function appendLockEvent(
|
|
|
260
260
|
{
|
|
261
261
|
targetPath = process.cwd(),
|
|
262
262
|
nowIso = new Date().toISOString(),
|
|
263
|
+
syncRemote = true,
|
|
264
|
+
awaitRemoteSync = false,
|
|
263
265
|
} = {}
|
|
264
266
|
) {
|
|
265
267
|
return appendToStream(
|
|
@@ -273,6 +275,8 @@ async function appendLockEvent(
|
|
|
273
275
|
}),
|
|
274
276
|
{
|
|
275
277
|
targetPath,
|
|
278
|
+
syncRemote,
|
|
279
|
+
awaitRemoteSync,
|
|
276
280
|
}
|
|
277
281
|
);
|
|
278
282
|
}
|
|
@@ -371,6 +375,8 @@ export async function lockFile(
|
|
|
371
375
|
ttlSeconds = DEFAULT_FILE_LOCK_TTL_SECONDS,
|
|
372
376
|
targetPath = process.cwd(),
|
|
373
377
|
nowIso = new Date().toISOString(),
|
|
378
|
+
syncRemote = true,
|
|
379
|
+
awaitRemoteSync = false,
|
|
374
380
|
} = {}
|
|
375
381
|
) {
|
|
376
382
|
const normalizedAgentId = normalizeAgentId(agentId);
|
|
@@ -437,6 +443,8 @@ export async function lockFile(
|
|
|
437
443
|
{
|
|
438
444
|
targetPath,
|
|
439
445
|
nowIso: normalizedNow,
|
|
446
|
+
syncRemote,
|
|
447
|
+
awaitRemoteSync,
|
|
440
448
|
}
|
|
441
449
|
);
|
|
442
450
|
return {
|
|
@@ -462,6 +470,8 @@ export async function unlockFile(
|
|
|
462
470
|
targetPath = process.cwd(),
|
|
463
471
|
nowIso = new Date().toISOString(),
|
|
464
472
|
actorAgentId = null,
|
|
473
|
+
syncRemote = true,
|
|
474
|
+
awaitRemoteSync = false,
|
|
465
475
|
} = {}
|
|
466
476
|
) {
|
|
467
477
|
const normalizedAgentId = normalizeAgentId(agentId);
|
|
@@ -527,6 +537,8 @@ export async function unlockFile(
|
|
|
527
537
|
{
|
|
528
538
|
targetPath,
|
|
529
539
|
nowIso: normalizedNow,
|
|
540
|
+
syncRemote,
|
|
541
|
+
awaitRemoteSync,
|
|
530
542
|
}
|
|
531
543
|
);
|
|
532
544
|
|