@cryptiklemur/lattice 1.40.8 → 1.41.1

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.
@@ -23,7 +23,7 @@ import { useBookmarks } from "../../hooks/useBookmarks";
23
23
  import { formatSessionTitle } from "../../utils/formatSessionTitle";
24
24
 
25
25
  export function ChatView({ sessionId: tabSessionId, projectSlug: tabProjectSlug }: { sessionId?: string; projectSlug?: string } = {}) {
26
- var { messages, isProcessing, sendMessage, activeSessionId, activeSessionTitle, currentStatus, contextUsage, contextBreakdown, lastResponseCost, lastResponseDuration, historyLoading, wasInterrupted, promptSuggestion, failedInput, clearFailedInput, messageQueue, enqueueMessage, removeQueuedMessage, updateQueuedMessage, isBusy, isPlanMode, pendingPrefill, activateSession, budgetStatus, budgetExceeded, sendBudgetOverride, dismissBudgetExceeded } = useSession();
26
+ var { messages, isProcessing, sendMessage, activeSessionId, activeSessionTitle, currentStatus, contextUsage, contextBreakdown, lastResponseCost, lastResponseDuration, historyLoading, wasInterrupted, promptSuggestion, failedInput, clearFailedInput, messageQueue, enqueueMessage, removeQueuedMessage, updateQueuedMessage, isBusy, busyOwner, isPlanMode, pendingPrefill, activateSession, budgetStatus, budgetExceeded, sendBudgetOverride, dismissBudgetExceeded } = useSession();
27
27
  var { activeProject } = useProjects();
28
28
  var { toggleDrawer } = useSidebar();
29
29
 
@@ -955,7 +955,11 @@ export function ChatView({ sessionId: tabSessionId, projectSlug: tabProjectSlug
955
955
  {isBusy && !isProcessing && (
956
956
  <div className="flex items-center gap-2 px-3 sm:px-5 py-2 bg-info/10 border-t border-info/20">
957
957
  <Terminal size={13} className="text-info flex-shrink-0" />
958
- <span className="text-[12px] text-info flex-1">This session is being used by another client — input is disabled</span>
958
+ <span className="text-[12px] text-info flex-1">
959
+ {busyOwner === "cli"
960
+ ? "This session is controlled by Claude Code CLI — input is disabled"
961
+ : "This session is in use by another Lattice client — input is disabled"}
962
+ </span>
959
963
  <button
960
964
  onClick={function () { setConfirmStopExternal(true); }}
961
965
  className="btn btn-ghost btn-xs text-error/70 hover:text-error gap-1 flex-shrink-0"
@@ -1063,7 +1067,9 @@ export function ChatView({ sessionId: tabSessionId, projectSlug: tabProjectSlug
1063
1067
  disabled={!activeSessionId || !online || isBusy || (budgetStatus !== null && budgetStatus.enforcement === "hard-block" && budgetStatus.dailySpend >= budgetStatus.dailyLimit)}
1064
1068
  disabledPlaceholder={
1065
1069
  isBusy
1066
- ? "Session in use by another client..."
1070
+ ? (busyOwner === "cli"
1071
+ ? "Controlled by Claude Code CLI"
1072
+ : "Session in use by another Lattice client")
1067
1073
  : (budgetStatus !== null && budgetStatus.enforcement === "hard-block" && budgetStatus.dailySpend >= budgetStatus.dailyLimit)
1068
1074
  ? "Daily budget exceeded ($" + budgetStatus.dailySpend.toFixed(2) + " / $" + budgetStatus.dailyLimit.toFixed(2) + ")"
1069
1075
  : undefined
@@ -305,6 +305,7 @@ export function useSession(): UseSessionReturn {
305
305
  historyLoading: false,
306
306
  wasInterrupted: m.interrupted || false,
307
307
  isBusy: m.busy || false,
308
+ busyOwner: m.busyOwner ?? null,
308
309
  isPlanMode: false,
309
310
  };
310
311
  });
@@ -335,13 +336,13 @@ export function useSession(): UseSessionReturn {
335
336
  }
336
337
 
337
338
  function handleSessionBusy(msg: ServerMessage) {
338
- var m = msg as { type: string; sessionId: string; busy: boolean };
339
+ var m = msg as { type: string; sessionId: string; busy: boolean; busyOwner?: "cli" | "lattice" };
339
340
  var sessionState = getSessionStore().state;
340
341
  if (m.sessionId === sessionState.activeSessionId) {
341
342
  if (m.busy && sessionState.isProcessing) {
342
343
  return;
343
344
  }
344
- setSessionBusy(m.busy);
345
+ setSessionBusy(m.busy, m.busyOwner);
345
346
  }
346
347
  }
347
348
 
@@ -445,6 +446,7 @@ export function useSession(): UseSessionReturn {
445
446
  failedInput: state.failedInput,
446
447
  messageQueue: state.messageQueue,
447
448
  isBusy: state.isBusy,
449
+ busyOwner: state.busyOwner,
448
450
  isPlanMode: state.isPlanMode,
449
451
  pendingPrefill: state.pendingPrefill,
450
452
  budgetStatus: state.budgetStatus,
@@ -47,6 +47,7 @@ export interface SessionState {
47
47
  failedInput: string | null;
48
48
  messageQueue: string[];
49
49
  isBusy: boolean;
50
+ busyOwner: "cli" | "lattice" | null;
50
51
  isPlanMode: boolean;
51
52
  pendingPrefill: string | null;
52
53
  budgetStatus: BudgetStatus | null;
@@ -72,6 +73,7 @@ var sessionStore = new Store<SessionState>({
72
73
  failedInput: null,
73
74
  messageQueue: [],
74
75
  isBusy: false,
76
+ busyOwner: null,
75
77
  isPlanMode: false,
76
78
  pendingPrefill: null,
77
79
  budgetStatus: null,
@@ -296,6 +298,7 @@ export function clearSession(): void {
296
298
  failedInput: null,
297
299
  messageQueue: [],
298
300
  isBusy: false,
301
+ busyOwner: null,
299
302
  isPlanMode: false,
300
303
  pendingPrefill: null,
301
304
  budgetStatus: null,
@@ -328,9 +331,9 @@ export function setFailedInput(text: string | null): void {
328
331
  });
329
332
  }
330
333
 
331
- export function setSessionBusy(busy: boolean): void {
334
+ export function setSessionBusy(busy: boolean, owner?: "cli" | "lattice" | null): void {
332
335
  sessionStore.setState(function (state) {
333
- return { ...state, isBusy: busy };
336
+ return { ...state, isBusy: busy, busyOwner: busy ? (owner ?? null) : null };
334
337
  });
335
338
  }
336
339
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.40.8",
3
+ "version": "1.41.1",
4
4
  "description": "Multi-machine agentic dashboard for Claude Code. Monitor sessions, manage MCP servers and skills, orchestrate across mesh-networked nodes.",
5
5
  "license": "MIT",
6
6
  "author": "Aaron Scherer <me@aaronscherer.me>",
@@ -24,7 +24,7 @@ import {
24
24
  import { getContextBreakdown } from "../project/context-breakdown";
25
25
  import { setActiveSession } from "./chat";
26
26
  import { setActiveProject } from "./fs";
27
- import { wasSessionInterrupted, clearInterruptedFlag, isSessionBusy, watchSessionLock, stopExternalSession } from "../project/sdk-bridge";
27
+ import { wasSessionInterrupted, clearInterruptedFlag, isSessionBusy, watchSessionLock, stopExternalSession, getBusyOwner } from "../project/sdk-bridge";
28
28
  import { log } from "../logger";
29
29
 
30
30
  registerHandler("session", function (clientId: string, message: ClientMessage) {
@@ -113,6 +113,7 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
113
113
  clearInterruptedFlag(activateMsg.sessionId);
114
114
  }
115
115
  var busy = isSessionBusy(activateMsg.sessionId);
116
+ var busyOwner = busy ? getBusyOwner(activateMsg.sessionId) : undefined;
116
117
  sendTo(clientId, {
117
118
  type: "session:history",
118
119
  projectSlug: activateMsg.projectSlug,
@@ -121,6 +122,7 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
121
122
  title: results[1],
122
123
  interrupted: interrupted || undefined,
123
124
  busy: busy || undefined,
125
+ busyOwner: busyOwner,
124
126
  });
125
127
  } catch (err) {
126
128
  log.session("Error sending session history: %O", err);
@@ -68,6 +68,7 @@ var interruptedSessions = new Set<string>();
68
68
  // Track external lock state so we only broadcast on changes
69
69
  var externalLockState = new Map<string, boolean>();
70
70
  var watchedSessions = new Set<string>();
71
+ var remoteSessionWatchers = new Map<string, Set<string>>();
71
72
 
72
73
  /**
73
74
  * Start polling a session's lock file for external CLI usage.
@@ -85,18 +86,47 @@ export function unwatchSessionLock(sessionId: string): void {
85
86
  externalLockState.delete(sessionId);
86
87
  }
87
88
 
89
+ export function addRemoteSessionWatcher(sessionId: string, nodeId: string): void {
90
+ var watchers = remoteSessionWatchers.get(sessionId);
91
+ if (!watchers) {
92
+ watchers = new Set();
93
+ remoteSessionWatchers.set(sessionId, watchers);
94
+ }
95
+ watchers.add(nodeId);
96
+ }
97
+
98
+ export function getBusyOwner(sessionId: string): "cli" | "lattice" | undefined {
99
+ if (activeStreams.has(sessionId)) return "lattice";
100
+ if (isSessionLockedByExternal(sessionId)) return "cli";
101
+ return undefined;
102
+ }
103
+
88
104
  // Poll every 3 seconds for external lock changes
89
105
  setInterval(function () {
90
106
  for (var sessionId of watchedSessions) {
91
- // Skip sessions with active Lattice streams — those are already tracked
92
- if (activeStreams.has(sessionId)) continue;
93
-
94
- var locked = isSessionLockedByExternal(sessionId);
107
+ var busy = isSessionBusy(sessionId);
95
108
  var prev = externalLockState.get(sessionId) ?? false;
96
109
 
97
- if (locked !== prev) {
98
- externalLockState.set(sessionId, locked);
99
- broadcast({ type: "session:busy", sessionId, busy: locked });
110
+ if (busy !== prev) {
111
+ externalLockState.set(sessionId, busy);
112
+ var owner = busy ? getBusyOwner(sessionId) : undefined;
113
+ broadcast({ type: "session:busy", sessionId, busy: busy, busyOwner: owner });
114
+
115
+ var watchers = remoteSessionWatchers.get(sessionId);
116
+ if (watchers) {
117
+ var { getPeerConnection } = require("../mesh/connector") as typeof import("../mesh/connector");
118
+ for (var nodeId of watchers) {
119
+ var peerWs = getPeerConnection(nodeId);
120
+ if (peerWs) {
121
+ peerWs.send(JSON.stringify({
122
+ type: "mesh:proxy_response",
123
+ projectSlug: "",
124
+ requestId: "busy-" + sessionId,
125
+ payload: { type: "session:busy", sessionId, busy: busy, busyOwner: owner },
126
+ }));
127
+ }
128
+ }
129
+ }
100
130
  }
101
131
  }
102
132
  }, 3000);
@@ -188,6 +218,7 @@ export function getActiveStreamCount(): number {
188
218
  * so this ONLY returns true for external CLI instances.
189
219
  */
190
220
  export function isSessionBusy(sessionId: string): boolean {
221
+ if (activeStreams.has(sessionId)) return true;
191
222
  return isSessionLockedByExternal(sessionId);
192
223
  }
193
224
 
@@ -622,12 +622,14 @@ export interface SessionHistoryMessage {
622
622
  title?: string;
623
623
  interrupted?: boolean;
624
624
  busy?: boolean;
625
+ busyOwner?: "cli" | "lattice";
625
626
  }
626
627
 
627
628
  export interface SessionBusyMessage {
628
629
  type: "session:busy";
629
630
  sessionId: string;
630
631
  busy: boolean;
632
+ busyOwner?: "cli" | "lattice";
631
633
  }
632
634
 
633
635
  export interface ChatUserMessage {