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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.30.1",
3
+ "version": "0.30.3",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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(agentModel) || "cli",
1630
+ model: normalizeString(identity.model) || "cli",
1619
1631
  role: "listener",
1620
- displayName: normalizeString(displayName) || agentId,
1621
- clientKind: "cli",
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(agentModel) || "cli",
1709
+ model: normalizeString(identity.model) || "cli",
1688
1710
  role: "listener",
1689
- displayName: normalizeString(displayName) || agentId,
1690
- clientKind: "cli",
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(agentModel) || "cli",
1750
+ model: normalizeString(identity.model) || "cli",
1719
1751
  role: "listener",
1720
- displayName: normalizeString(displayName) || agentId,
1721
- clientKind: "cli",
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
- clientKind: normalizeString(clientKind) || undefined,
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(options.model) || "cli",
3333
- displayName: normalizeString(options.displayName) || undefined,
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 runLockDirectiveCommand = async (verb, sessionId, files, options, command) => {
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
- const directive = buildSessionLockDirective(verb, file, intent);
3704
- await postSessionDirectiveMessage(normalizedSessionId, directive, {
3705
- agentId: identity.agentId,
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
- processed.push(file);
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 runLockDirectiveCommand("lock", sessionId, files, options, command);
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 runLockDirectiveCommand("unlock", sessionId, files, options, command);
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 agentModel = normalizeString(options.model) || "cli";
3970
- const displayName = normalizeString(options.displayName) || agentId;
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
- async function writeAgentSnapshot(snapshotPath, snapshot) {
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
- agentId: payload.agentId,
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: normalizeString(model) || normalizeString(existing?.model) || "unknown",
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