@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.
- package/dist/main.js +307 -54
- 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
|
-
*
|
|
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
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
() =>
|
|
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
|
-
* -
|
|
7865
|
-
* -
|
|
7866
|
-
* -
|
|
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
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|