@tiflis-io/tiflis-code-workstation 0.3.29 → 0.3.30

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.
Files changed (2) hide show
  1. package/dist/main.js +307 -54
  2. package/package.json +3 -1
package/dist/main.js CHANGED
@@ -1398,7 +1398,7 @@ function getMessageType(data) {
1398
1398
  }
1399
1399
 
1400
1400
  // src/infrastructure/websocket/tunnel-client.ts
1401
- var TunnelClient = class {
1401
+ var TunnelClient = class _TunnelClient {
1402
1402
  config;
1403
1403
  callbacks;
1404
1404
  logger;
@@ -1412,6 +1412,8 @@ var TunnelClient = class {
1412
1412
  reconnectTimeout = null;
1413
1413
  registrationTimeout = null;
1414
1414
  messageBuffer = [];
1415
+ static MAX_SEND_RETRIES = 3;
1416
+ static SEND_RETRY_DELAY_MS = 100;
1415
1417
  constructor(config2, callbacks) {
1416
1418
  this.config = config2;
1417
1419
  this.callbacks = callbacks;
@@ -1514,8 +1516,9 @@ var TunnelClient = class {
1514
1516
  this.messageBuffer = [];
1515
1517
  }
1516
1518
  /**
1517
- * Sends a message to the tunnel (for forwarding to clients).
1518
- * Detects send failures and triggers reconnection.
1519
+ * Sends a message to the tunnel (for forwarding to clients) with retry logic (FIX #3).
1520
+ * Buffers during reconnection, retries on transient failures with exponential backoff.
1521
+ * Returns true if send was successful or queued for retry.
1519
1522
  */
1520
1523
  send(message) {
1521
1524
  if (this.state !== "registered" || !this.ws) {
@@ -1528,18 +1531,55 @@ var TunnelClient = class {
1528
1531
  if (this.ws.readyState !== WebSocket.OPEN) {
1529
1532
  this.logger.warn(
1530
1533
  { readyState: this.ws.readyState },
1531
- "Socket not open, triggering reconnection"
1534
+ "Socket not open, buffering message and triggering reconnection"
1532
1535
  );
1536
+ this.messageBuffer.push(message);
1533
1537
  this.handleSendFailure();
1538
+ return true;
1539
+ }
1540
+ return this.sendWithRetry(message, 0);
1541
+ }
1542
+ /**
1543
+ * FIX #3: Send with exponential backoff retry.
1544
+ * On failure, schedules retry with exponential delay (100ms, 200ms, 400ms).
1545
+ */
1546
+ sendWithRetry(message, attempt) {
1547
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1548
+ if (attempt === 0) {
1549
+ this.messageBuffer.push(message);
1550
+ }
1534
1551
  return false;
1535
1552
  }
1536
- this.ws.send(message, (error) => {
1537
- if (error) {
1538
- this.logger.error({ error }, "Send failed, triggering reconnection");
1539
- this.handleSendFailure();
1553
+ try {
1554
+ this.ws.send(message, (error) => {
1555
+ if (error) {
1556
+ if (attempt < _TunnelClient.MAX_SEND_RETRIES) {
1557
+ const delay = _TunnelClient.SEND_RETRY_DELAY_MS * Math.pow(2, attempt);
1558
+ this.logger.debug(
1559
+ { attempt: attempt + 1, delay, errorType: error.name },
1560
+ "Send failed, scheduling retry with exponential backoff"
1561
+ );
1562
+ setTimeout(() => {
1563
+ this.sendWithRetry(message, attempt + 1);
1564
+ }, delay);
1565
+ } else {
1566
+ this.logger.error(
1567
+ { error: error.message, attempts: attempt + 1 },
1568
+ "Send failed after all retries, buffering message"
1569
+ );
1570
+ this.messageBuffer.push(message);
1571
+ this.handleSendFailure();
1572
+ }
1573
+ }
1574
+ });
1575
+ return true;
1576
+ } catch (error) {
1577
+ this.logger.error({ error: error.message }, "Unexpected error in send");
1578
+ if (attempt === 0) {
1579
+ this.messageBuffer.push(message);
1540
1580
  }
1541
- });
1542
- return true;
1581
+ return false;
1582
+ }
1543
1583
  }
1544
1584
  /**
1545
1585
  * Handles send failure by disconnecting and scheduling reconnection.
@@ -3055,39 +3095,32 @@ var HeadlessAgentExecutor = class extends EventEmitter {
3055
3095
  cwd: this.workingDir,
3056
3096
  env: {
3057
3097
  ...shellEnv,
3058
- // Apply alias env vars (e.g., CLAUDE_CONFIG_DIR)
3059
3098
  ...aliasEnvVars,
3060
- // Ensure proper terminal environment
3061
3099
  TERM: "xterm-256color",
3062
- // Disable interactive prompts
3063
3100
  CI: "true"
3064
3101
  },
3065
3102
  stdio: ["ignore", "pipe", "pipe"],
3066
- // stdin ignored, stdout/stderr piped
3067
- detached: true,
3068
- // Create new process group for clean termination
3069
- // Ensure child doesn't interfere with parent's signal handling
3070
- // @ts-expect-error - Node.js 16+ option
3071
- ignoreParentSignals: true
3103
+ detached: true
3072
3104
  });
3073
- this.subprocess.stdout?.on("data", (data) => {
3105
+ const proc = this.subprocess;
3106
+ proc.stdout?.on("data", (data) => {
3074
3107
  if (this.isKilled) return;
3075
3108
  const text2 = data.toString();
3076
3109
  this.emit("stdout", text2);
3077
3110
  });
3078
- this.subprocess.stderr?.on("data", (data) => {
3111
+ proc.stderr?.on("data", (data) => {
3079
3112
  if (this.isKilled) return;
3080
3113
  const text2 = data.toString();
3081
3114
  this.emit("stderr", text2);
3082
3115
  });
3083
- this.subprocess.on("exit", (code) => {
3116
+ proc.on("exit", (code) => {
3084
3117
  this.clearExecutionTimeout();
3085
3118
  if (!this.isKilled) {
3086
3119
  this.emit("exit", code);
3087
3120
  }
3088
3121
  this.subprocess = null;
3089
3122
  });
3090
- this.subprocess.on("error", (error) => {
3123
+ proc.on("error", (error) => {
3091
3124
  this.clearExecutionTimeout();
3092
3125
  if (!this.isKilled) {
3093
3126
  this.emit("error", error);
@@ -5067,13 +5100,58 @@ var SubscriptionService = class {
5067
5100
  };
5068
5101
 
5069
5102
  // src/application/services/message-broadcaster-impl.ts
5070
- var MessageBroadcasterImpl = class {
5103
+ var MessageBroadcasterImpl = class _MessageBroadcasterImpl {
5071
5104
  deps;
5072
5105
  logger;
5106
+ /** FIX #4: Buffer messages for devices during auth flow */
5107
+ authBuffers = /* @__PURE__ */ new Map();
5108
+ static AUTH_BUFFER_TTL_MS = 5e3;
5109
+ // 5 second buffer TTL
5073
5110
  constructor(deps) {
5074
5111
  this.deps = deps;
5075
5112
  this.logger = deps.logger.child({ service: "broadcaster" });
5076
5113
  }
5114
+ /**
5115
+ * FIX #4: Buffers a message for a device during auth flow.
5116
+ * Called when subscribing clients are not yet authenticated.
5117
+ * Messages are buffered with TTL and flushed when auth completes.
5118
+ */
5119
+ bufferMessageForAuth(deviceId, sessionId, message) {
5120
+ if (!this.authBuffers.has(deviceId)) {
5121
+ this.authBuffers.set(deviceId, []);
5122
+ }
5123
+ const buffer = this.authBuffers.get(deviceId) ?? [];
5124
+ buffer.push({ sessionId, message, timestamp: Date.now() });
5125
+ const now = Date.now();
5126
+ const filtered = buffer.filter(
5127
+ (item) => now - item.timestamp < _MessageBroadcasterImpl.AUTH_BUFFER_TTL_MS
5128
+ );
5129
+ this.authBuffers.set(deviceId, filtered);
5130
+ this.logger.debug(
5131
+ { deviceId, bufferSize: filtered.length },
5132
+ "Buffered message for authenticating device"
5133
+ );
5134
+ }
5135
+ /**
5136
+ * FIX #4: Flushes buffered messages for a device after authentication.
5137
+ * Called from main.ts after subscription restore completes.
5138
+ * Returns buffered messages so they can be sent to subscribed sessions.
5139
+ */
5140
+ flushAuthBuffer(deviceId) {
5141
+ const buffer = this.authBuffers.get(deviceId) ?? [];
5142
+ this.authBuffers.delete(deviceId);
5143
+ const now = Date.now();
5144
+ const validMessages = buffer.filter(
5145
+ (item) => now - item.timestamp < _MessageBroadcasterImpl.AUTH_BUFFER_TTL_MS
5146
+ );
5147
+ if (validMessages.length > 0) {
5148
+ this.logger.info(
5149
+ { deviceId, messageCount: validMessages.length },
5150
+ "Flushing auth buffer - delivering buffered messages"
5151
+ );
5152
+ }
5153
+ return validMessages;
5154
+ }
5077
5155
  /**
5078
5156
  * Broadcasts a message to all connected clients via tunnel.
5079
5157
  * The tunnel handles client filtering and delivery.
@@ -5115,6 +5193,8 @@ var MessageBroadcasterImpl = class {
5115
5193
  * Broadcasts a message to all clients subscribed to a session (by session ID string).
5116
5194
  * Sends targeted messages to each subscriber in parallel with timeout.
5117
5195
  * Prevents slow clients from blocking others.
5196
+ *
5197
+ * FIX #4: Also buffers messages for unauthenticated subscribers during auth flow.
5118
5198
  */
5119
5199
  async broadcastToSubscribers(sessionId, message) {
5120
5200
  const session = new SessionId(sessionId);
@@ -5123,9 +5203,36 @@ var MessageBroadcasterImpl = class {
5123
5203
  (c) => c.isAuthenticated
5124
5204
  );
5125
5205
  if (authenticatedSubscribers.length === 0) {
5206
+ const unauthenticatedCount = subscribers.length - authenticatedSubscribers.length;
5207
+ if (unauthenticatedCount > 0) {
5208
+ this.logger.debug(
5209
+ { sessionId, unauthenticatedCount },
5210
+ "Subscribers are authenticating - buffering message for delivery after auth"
5211
+ );
5212
+ for (const client of subscribers) {
5213
+ if (!client.isAuthenticated) {
5214
+ this.bufferMessageForAuth(client.deviceId.value, sessionId, message);
5215
+ }
5216
+ }
5217
+ } else {
5218
+ let messageType = "unknown";
5219
+ try {
5220
+ const parsed = JSON.parse(message);
5221
+ messageType = parsed.type ?? "unknown";
5222
+ } catch {
5223
+ }
5224
+ this.logger.debug(
5225
+ { sessionId, messageType },
5226
+ "No subscribers found for session"
5227
+ );
5228
+ }
5126
5229
  return;
5127
5230
  }
5128
5231
  const SEND_TIMEOUT_MS = 2e3;
5232
+ this.logger.debug(
5233
+ { sessionId, subscriberCount: authenticatedSubscribers.length },
5234
+ "Broadcasting message to subscribers"
5235
+ );
5129
5236
  const sendPromises = authenticatedSubscribers.map(async (client) => {
5130
5237
  try {
5131
5238
  await Promise.race([
@@ -5162,7 +5269,18 @@ var MessageBroadcasterImpl = class {
5162
5269
  );
5163
5270
  }
5164
5271
  });
5165
- await Promise.allSettled(sendPromises);
5272
+ const results = await Promise.allSettled(sendPromises);
5273
+ const failedCount = results.filter((r) => r.status === "rejected").length;
5274
+ if (failedCount > 0) {
5275
+ this.logger.warn(
5276
+ { sessionId, totalSubscribers: authenticatedSubscribers.length, failedCount },
5277
+ "Some subscribers failed to receive message"
5278
+ );
5279
+ }
5280
+ }
5281
+ getAuthenticatedDeviceIds() {
5282
+ const clients = this.deps.clientRegistry.getAll();
5283
+ return clients.filter((c) => c.isAuthenticated).map((c) => c.deviceId.value);
5166
5284
  }
5167
5285
  };
5168
5286
 
@@ -6655,6 +6773,7 @@ import { EventEmitter as EventEmitter4 } from "events";
6655
6773
  import { ChatOpenAI } from "@langchain/openai";
6656
6774
  import { createReactAgent } from "@langchain/langgraph/prebuilt";
6657
6775
  import { HumanMessage, AIMessage, isAIMessage } from "@langchain/core/messages";
6776
+ import AsyncLock from "async-lock";
6658
6777
 
6659
6778
  // src/infrastructure/agents/supervisor/tools/workspace-tools.ts
6660
6779
  import { tool } from "@langchain/core/tools";
@@ -7560,7 +7679,7 @@ function formatSize(bytes) {
7560
7679
  }
7561
7680
 
7562
7681
  // src/infrastructure/agents/supervisor/supervisor-agent.ts
7563
- var SupervisorAgent = class extends EventEmitter4 {
7682
+ var SupervisorAgent = class _SupervisorAgent extends EventEmitter4 {
7564
7683
  logger;
7565
7684
  agent;
7566
7685
  getMessageBroadcaster;
@@ -7573,6 +7692,11 @@ var SupervisorAgent = class extends EventEmitter4 {
7573
7692
  isProcessingCommand = false;
7574
7693
  /** Timestamp when current execution started (for race condition protection) */
7575
7694
  executionStartedAt = 0;
7695
+ clearContextLock = new AsyncLock();
7696
+ static CLEAR_CONTEXT_TIMEOUT_MS = 1e4;
7697
+ // 10 seconds
7698
+ /** FIX #6: Broadcast acknowledgment tracking */
7699
+ broadcastAcknowledgments = /* @__PURE__ */ new Map();
7576
7700
  constructor(config2) {
7577
7701
  super();
7578
7702
  this.logger = config2.logger.child({ component: "SupervisorAgent" });
@@ -7598,7 +7722,11 @@ var SupervisorAgent = class extends EventEmitter4 {
7598
7722
  config2.workspacesRoot,
7599
7723
  config2.getMessageBroadcaster,
7600
7724
  config2.getChatHistoryService,
7601
- () => this.clearContext(),
7725
+ () => {
7726
+ this.clearContext().catch((error) => {
7727
+ this.logger.error({ error }, "Failed to clear supervisor context");
7728
+ });
7729
+ },
7602
7730
  terminateSessionCallback
7603
7731
  ),
7604
7732
  ...createFilesystemTools(config2.workspacesRoot)
@@ -7860,27 +7988,86 @@ var SupervisorAgent = class extends EventEmitter4 {
7860
7988
  this.logger.info("Global conversation history cleared");
7861
7989
  }
7862
7990
  /**
7863
- * Clears supervisor context completely:
7864
- * - In-memory conversation history
7865
- * - Persistent history in database
7866
- * - Notifies all connected clients
7991
+ * Clears supervisor context completely with multi-device synchronization:
7992
+ * - Acquires lock to prevent concurrent execution during clear
7993
+ * - Cancels active execution streams
7994
+ * - Clears in-memory conversation history
7995
+ * - Clears persistent history in database
7996
+ * - Broadcasts notification to all clients with ack tracking (FIX #6, #9)
7997
+ */
7998
+ async clearContext() {
7999
+ await this.clearContextLock.acquire("clear", async () => {
8000
+ this.logger.info({ isExecuting: this.isExecuting }, "Acquiring lock for context clear");
8001
+ if (this.isExecuting || this.isProcessingCommand) {
8002
+ this.logger.info("Active execution detected, cancelling before clear");
8003
+ this.cancel();
8004
+ await new Promise((resolve2) => setTimeout(resolve2, 50));
8005
+ }
8006
+ this.conversationHistory = [];
8007
+ this.isCancelled = false;
8008
+ const chatHistoryService = this.getChatHistoryService?.();
8009
+ if (chatHistoryService) {
8010
+ try {
8011
+ chatHistoryService.clearSupervisorHistory();
8012
+ this.logger.debug("Persistent supervisor history cleared");
8013
+ } catch (error) {
8014
+ this.logger.error({ error }, "Failed to clear persistent supervisor history");
8015
+ }
8016
+ }
8017
+ const broadcaster = this.getMessageBroadcaster?.();
8018
+ if (broadcaster) {
8019
+ const broadcastId = `clear_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
8020
+ const clearNotification = JSON.stringify({
8021
+ type: "supervisor.context_cleared",
8022
+ payload: { timestamp: Date.now(), broadcast_id: broadcastId }
8023
+ });
8024
+ try {
8025
+ const deviceIds = new Set(broadcaster.getAuthenticatedDeviceIds());
8026
+ if (deviceIds.size > 0) {
8027
+ this.broadcastAcknowledgments.set(broadcastId, {
8028
+ deviceIds,
8029
+ timestamp: Date.now(),
8030
+ timeout: setTimeout(() => {
8031
+ const ack = this.broadcastAcknowledgments.get(broadcastId);
8032
+ if (ack && ack.deviceIds.size > 0) {
8033
+ this.logger.warn({
8034
+ broadcastId,
8035
+ missingDevices: Array.from(ack.deviceIds)
8036
+ }, "Context clear broadcast - missing acks from devices");
8037
+ }
8038
+ this.broadcastAcknowledgments.delete(broadcastId);
8039
+ }, 5e3)
8040
+ // 5 second timeout
8041
+ });
8042
+ }
8043
+ broadcaster.broadcastToAll(clearNotification);
8044
+ this.logger.info({
8045
+ broadcastId,
8046
+ deviceCount: deviceIds.size
8047
+ }, "Broadcasted context cleared notification to all clients");
8048
+ } catch (error) {
8049
+ this.logger.error({ error }, "Failed to broadcast context cleared");
8050
+ }
8051
+ } else {
8052
+ this.logger.warn("MessageBroadcaster not available");
8053
+ }
8054
+ this.logger.info("Supervisor context cleared (lock released)");
8055
+ }, { timeout: _SupervisorAgent.CLEAR_CONTEXT_TIMEOUT_MS });
8056
+ }
8057
+ /**
8058
+ * FIX #6: Records acknowledgment from a device for context clear broadcast.
7867
8059
  */
7868
- clearContext() {
7869
- this.conversationHistory = [];
7870
- this.isCancelled = false;
7871
- const chatHistoryService = this.getChatHistoryService?.();
7872
- if (chatHistoryService) {
7873
- chatHistoryService.clearSupervisorHistory();
7874
- }
7875
- const broadcaster = this.getMessageBroadcaster?.();
7876
- if (broadcaster) {
7877
- const clearNotification = JSON.stringify({
7878
- type: "supervisor.context_cleared",
7879
- payload: { timestamp: Date.now() }
7880
- });
7881
- broadcaster.broadcastToAll(clearNotification);
8060
+ recordClearAck(broadcastId, deviceId) {
8061
+ const ack = this.broadcastAcknowledgments.get(broadcastId);
8062
+ if (ack) {
8063
+ ack.deviceIds.delete(deviceId);
8064
+ this.logger.debug({ broadcastId, deviceId, remaining: ack.deviceIds.size }, "Received clear ack");
8065
+ if (ack.deviceIds.size === 0) {
8066
+ clearTimeout(ack.timeout);
8067
+ this.broadcastAcknowledgments.delete(broadcastId);
8068
+ this.logger.info({ broadcastId }, "All devices acknowledged context clear");
8069
+ }
7882
8070
  }
7883
- this.logger.info("Supervisor context cleared (in-memory, persistent, and clients notified)");
7884
8071
  }
7885
8072
  /**
7886
8073
  * Resets the cancellation state.
@@ -8375,9 +8562,15 @@ var MockSupervisorAgent = class extends EventEmitter5 {
8375
8562
  getConversationHistory() {
8376
8563
  return [...this.conversationHistory];
8377
8564
  }
8378
- /**
8379
- * Sleep utility.
8380
- */
8565
+ clearContext() {
8566
+ this.conversationHistory = [];
8567
+ this.isCancelled = false;
8568
+ this.logger.info("Mock context cleared");
8569
+ return Promise.resolve();
8570
+ }
8571
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
8572
+ recordClearAck(_broadcastId, _deviceId) {
8573
+ }
8381
8574
  sleep(ms) {
8382
8575
  return new Promise((resolve2) => setTimeout(resolve2, ms));
8383
8576
  }
@@ -9181,7 +9374,7 @@ function handleTunnelMessage(rawMessage, tunnelClient, messageBroadcaster, creat
9181
9374
  );
9182
9375
  }
9183
9376
  }
9184
- function handleAuthMessageViaTunnel(data, tunnelClient, authenticateClient, logger, subscriptionService) {
9377
+ function handleAuthMessageViaTunnel(data, tunnelClient, authenticateClient, logger, subscriptionService, messageBroadcaster) {
9185
9378
  const authResult = AuthMessageSchema.safeParse(data);
9186
9379
  if (!authResult.success) {
9187
9380
  logger.warn(
@@ -9211,6 +9404,32 @@ function handleAuthMessageViaTunnel(data, tunnelClient, authenticateClient, logg
9211
9404
  );
9212
9405
  }
9213
9406
  }
9407
+ if (messageBroadcaster) {
9408
+ const bufferedMessages = messageBroadcaster.flushAuthBuffer(deviceId);
9409
+ if (bufferedMessages.length > 0) {
9410
+ logger.info(
9411
+ { deviceId, messageCount: bufferedMessages.length },
9412
+ "Flushing buffered messages after subscription restore"
9413
+ );
9414
+ (async () => {
9415
+ for (const bufferedMsg of bufferedMessages) {
9416
+ try {
9417
+ await messageBroadcaster.broadcastToSubscribers(
9418
+ bufferedMsg.sessionId,
9419
+ bufferedMsg.message
9420
+ );
9421
+ } catch (error) {
9422
+ logger.error(
9423
+ { error, deviceId, sessionId: bufferedMsg.sessionId },
9424
+ "Failed to send buffered message"
9425
+ );
9426
+ }
9427
+ }
9428
+ })().catch((error) => {
9429
+ logger.error({ error, deviceId }, "Error flushing buffered messages");
9430
+ });
9431
+ }
9432
+ }
9214
9433
  const responseJson = JSON.stringify(result);
9215
9434
  if (tunnelClient.send(responseJson)) {
9216
9435
  logger.info(
@@ -9449,7 +9668,7 @@ async function bootstrap() {
9449
9668
  }
9450
9669
  };
9451
9670
  const createMessageHandlers = () => ({
9452
- auth: (socket, message) => {
9671
+ auth: async (socket, message) => {
9453
9672
  const authMessage = message;
9454
9673
  const deviceId = authMessage.payload.device_id;
9455
9674
  const result = authenticateClient.execute({
@@ -9467,6 +9686,21 @@ async function bootstrap() {
9467
9686
  );
9468
9687
  }
9469
9688
  }
9689
+ if (broadcaster) {
9690
+ const bufferedMessages = broadcaster.flushAuthBuffer(deviceId);
9691
+ if (bufferedMessages.length > 0) {
9692
+ logger.info(
9693
+ { deviceId, messageCount: bufferedMessages.length },
9694
+ "Flushing buffered messages after subscription restore"
9695
+ );
9696
+ for (const bufferedMsg of bufferedMessages) {
9697
+ await broadcaster.broadcastToSubscribers(
9698
+ bufferedMsg.sessionId,
9699
+ bufferedMsg.message
9700
+ );
9701
+ }
9702
+ }
9703
+ }
9470
9704
  sendToDevice(socket, deviceId, JSON.stringify(result));
9471
9705
  return Promise.resolve();
9472
9706
  },
@@ -9911,13 +10145,13 @@ async function bootstrap() {
9911
10145
  return Promise.resolve();
9912
10146
  },
9913
10147
  // Clear supervisor conversation history (global)
9914
- "supervisor.clear_context": (socket, message) => {
10148
+ "supervisor.clear_context": async (socket, message) => {
9915
10149
  const clearMessage = message;
9916
10150
  const directClient = clientRegistry.getBySocket(socket);
9917
10151
  const tunnelClient2 = clearMessage.device_id ? clientRegistry.getByDeviceId(new DeviceId(clearMessage.device_id)) : void 0;
9918
10152
  const isAuthenticated = directClient?.isAuthenticated ?? tunnelClient2?.isAuthenticated;
9919
10153
  if (isAuthenticated) {
9920
- supervisorAgent.clearContext();
10154
+ await supervisorAgent.clearContext();
9921
10155
  sendToDevice(
9922
10156
  socket,
9923
10157
  clearMessage.device_id,
@@ -10869,8 +11103,27 @@ async function bootstrap() {
10869
11103
  tunnelClient,
10870
11104
  authenticateClient,
10871
11105
  logger,
10872
- subscriptionService
11106
+ subscriptionService,
11107
+ broadcaster
10873
11108
  );
11109
+ } else if (messageType === "supervisor.context_cleared.ack") {
11110
+ try {
11111
+ const ackData = data;
11112
+ const broadcastId = ackData.payload?.broadcast_id;
11113
+ const deviceId = ackData.payload?.device_id;
11114
+ if (broadcastId && deviceId && supervisorAgent) {
11115
+ supervisorAgent.recordClearAck(broadcastId, deviceId);
11116
+ logger.debug(
11117
+ { broadcastId, deviceId },
11118
+ "Recorded context clear acknowledgment"
11119
+ );
11120
+ }
11121
+ } catch (ackError) {
11122
+ logger.warn(
11123
+ { error: ackError, message: message.slice(0, 100) },
11124
+ "Failed to process context clear acknowledgment"
11125
+ );
11126
+ }
10874
11127
  } else {
10875
11128
  handleTunnelMessage(
10876
11129
  message,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiflis-io/tiflis-code-workstation",
3
- "version": "0.3.29",
3
+ "version": "0.3.30",
4
4
  "description": "Workstation server for tiflis-code - manages agent sessions and terminal access",
5
5
  "author": "Roman Barinov <rbarinov@gmail.com>",
6
6
  "license": "FSL-1.1-NC",
@@ -36,6 +36,7 @@
36
36
  "@langchain/core": "^0.3.26",
37
37
  "@langchain/langgraph": "^0.2.42",
38
38
  "@langchain/openai": "^0.3.17",
39
+ "async-lock": "^1.4.0",
39
40
  "better-sqlite3": "^11.7.0",
40
41
  "dotenv": "^16.4.7",
41
42
  "drizzle-orm": "^0.38.3",
@@ -51,6 +52,7 @@
51
52
  },
52
53
  "devDependencies": {
53
54
  "@eslint/js": "^9.17.0",
55
+ "@types/async-lock": "^1.4.2",
54
56
  "@types/better-sqlite3": "^7.6.12",
55
57
  "@types/node": "^22.10.2",
56
58
  "@types/ws": "^8.5.13",