@tiflis-io/tiflis-code-workstation 0.3.28 → 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 +413 -86
- 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.
|
|
@@ -5113,21 +5191,97 @@ var MessageBroadcasterImpl = class {
|
|
|
5113
5191
|
}
|
|
5114
5192
|
/**
|
|
5115
5193
|
* Broadcasts a message to all clients subscribed to a session (by session ID string).
|
|
5116
|
-
* Sends targeted messages to each subscriber
|
|
5194
|
+
* Sends targeted messages to each subscriber in parallel with timeout.
|
|
5195
|
+
* Prevents slow clients from blocking others.
|
|
5196
|
+
*
|
|
5197
|
+
* FIX #4: Also buffers messages for unauthenticated subscribers during auth flow.
|
|
5117
5198
|
*/
|
|
5118
|
-
broadcastToSubscribers(sessionId, message) {
|
|
5199
|
+
async broadcastToSubscribers(sessionId, message) {
|
|
5119
5200
|
const session = new SessionId(sessionId);
|
|
5120
5201
|
const subscribers = this.deps.clientRegistry.getSubscribers(session);
|
|
5121
5202
|
const authenticatedSubscribers = subscribers.filter(
|
|
5122
5203
|
(c) => c.isAuthenticated
|
|
5123
5204
|
);
|
|
5124
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
|
+
}
|
|
5125
5229
|
return;
|
|
5126
5230
|
}
|
|
5127
|
-
|
|
5128
|
-
|
|
5231
|
+
const SEND_TIMEOUT_MS = 2e3;
|
|
5232
|
+
this.logger.debug(
|
|
5233
|
+
{ sessionId, subscriberCount: authenticatedSubscribers.length },
|
|
5234
|
+
"Broadcasting message to subscribers"
|
|
5235
|
+
);
|
|
5236
|
+
const sendPromises = authenticatedSubscribers.map(async (client) => {
|
|
5237
|
+
try {
|
|
5238
|
+
await Promise.race([
|
|
5239
|
+
new Promise((resolve2, reject) => {
|
|
5240
|
+
const sent = this.deps.tunnelClient.sendToDevice(
|
|
5241
|
+
client.deviceId.value,
|
|
5242
|
+
message
|
|
5243
|
+
);
|
|
5244
|
+
if (sent) {
|
|
5245
|
+
resolve2();
|
|
5246
|
+
} else {
|
|
5247
|
+
reject(new Error(`sendToDevice returned false for ${client.deviceId.value}`));
|
|
5248
|
+
}
|
|
5249
|
+
}),
|
|
5250
|
+
new Promise(
|
|
5251
|
+
(_, reject) => setTimeout(
|
|
5252
|
+
() => reject(new Error(`Send timeout for ${client.deviceId.value}`)),
|
|
5253
|
+
SEND_TIMEOUT_MS
|
|
5254
|
+
)
|
|
5255
|
+
)
|
|
5256
|
+
]);
|
|
5257
|
+
this.logger.debug(
|
|
5258
|
+
{ deviceId: client.deviceId.value, sessionId },
|
|
5259
|
+
"Message sent to subscriber"
|
|
5260
|
+
);
|
|
5261
|
+
} catch (error) {
|
|
5262
|
+
this.logger.warn(
|
|
5263
|
+
{
|
|
5264
|
+
deviceId: client.deviceId.value,
|
|
5265
|
+
sessionId,
|
|
5266
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5267
|
+
},
|
|
5268
|
+
"Failed to send to subscriber (timeout or error)"
|
|
5269
|
+
);
|
|
5270
|
+
}
|
|
5271
|
+
});
|
|
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
|
+
);
|
|
5129
5279
|
}
|
|
5130
5280
|
}
|
|
5281
|
+
getAuthenticatedDeviceIds() {
|
|
5282
|
+
const clients = this.deps.clientRegistry.getAll();
|
|
5283
|
+
return clients.filter((c) => c.isAuthenticated).map((c) => c.deviceId.value);
|
|
5284
|
+
}
|
|
5131
5285
|
};
|
|
5132
5286
|
|
|
5133
5287
|
// src/infrastructure/persistence/repositories/message-repository.ts
|
|
@@ -6619,6 +6773,7 @@ import { EventEmitter as EventEmitter4 } from "events";
|
|
|
6619
6773
|
import { ChatOpenAI } from "@langchain/openai";
|
|
6620
6774
|
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
|
6621
6775
|
import { HumanMessage, AIMessage, isAIMessage } from "@langchain/core/messages";
|
|
6776
|
+
import AsyncLock from "async-lock";
|
|
6622
6777
|
|
|
6623
6778
|
// src/infrastructure/agents/supervisor/tools/workspace-tools.ts
|
|
6624
6779
|
import { tool } from "@langchain/core/tools";
|
|
@@ -7524,7 +7679,7 @@ function formatSize(bytes) {
|
|
|
7524
7679
|
}
|
|
7525
7680
|
|
|
7526
7681
|
// src/infrastructure/agents/supervisor/supervisor-agent.ts
|
|
7527
|
-
var SupervisorAgent = class extends EventEmitter4 {
|
|
7682
|
+
var SupervisorAgent = class _SupervisorAgent extends EventEmitter4 {
|
|
7528
7683
|
logger;
|
|
7529
7684
|
agent;
|
|
7530
7685
|
getMessageBroadcaster;
|
|
@@ -7537,6 +7692,11 @@ var SupervisorAgent = class extends EventEmitter4 {
|
|
|
7537
7692
|
isProcessingCommand = false;
|
|
7538
7693
|
/** Timestamp when current execution started (for race condition protection) */
|
|
7539
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();
|
|
7540
7700
|
constructor(config2) {
|
|
7541
7701
|
super();
|
|
7542
7702
|
this.logger = config2.logger.child({ component: "SupervisorAgent" });
|
|
@@ -7562,7 +7722,11 @@ var SupervisorAgent = class extends EventEmitter4 {
|
|
|
7562
7722
|
config2.workspacesRoot,
|
|
7563
7723
|
config2.getMessageBroadcaster,
|
|
7564
7724
|
config2.getChatHistoryService,
|
|
7565
|
-
() =>
|
|
7725
|
+
() => {
|
|
7726
|
+
this.clearContext().catch((error) => {
|
|
7727
|
+
this.logger.error({ error }, "Failed to clear supervisor context");
|
|
7728
|
+
});
|
|
7729
|
+
},
|
|
7566
7730
|
terminateSessionCallback
|
|
7567
7731
|
),
|
|
7568
7732
|
...createFilesystemTools(config2.workspacesRoot)
|
|
@@ -7824,27 +7988,86 @@ var SupervisorAgent = class extends EventEmitter4 {
|
|
|
7824
7988
|
this.logger.info("Global conversation history cleared");
|
|
7825
7989
|
}
|
|
7826
7990
|
/**
|
|
7827
|
-
* Clears supervisor context completely:
|
|
7828
|
-
* -
|
|
7829
|
-
* -
|
|
7830
|
-
* -
|
|
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.
|
|
7831
8059
|
*/
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
type: "supervisor.context_cleared",
|
|
7843
|
-
payload: { timestamp: Date.now() }
|
|
7844
|
-
});
|
|
7845
|
-
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
|
+
}
|
|
7846
8070
|
}
|
|
7847
|
-
this.logger.info("Supervisor context cleared (in-memory, persistent, and clients notified)");
|
|
7848
8071
|
}
|
|
7849
8072
|
/**
|
|
7850
8073
|
* Resets the cancellation state.
|
|
@@ -8339,9 +8562,15 @@ var MockSupervisorAgent = class extends EventEmitter5 {
|
|
|
8339
8562
|
getConversationHistory() {
|
|
8340
8563
|
return [...this.conversationHistory];
|
|
8341
8564
|
}
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
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
|
+
}
|
|
8345
8574
|
sleep(ms) {
|
|
8346
8575
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
8347
8576
|
}
|
|
@@ -9145,7 +9374,7 @@ function handleTunnelMessage(rawMessage, tunnelClient, messageBroadcaster, creat
|
|
|
9145
9374
|
);
|
|
9146
9375
|
}
|
|
9147
9376
|
}
|
|
9148
|
-
function handleAuthMessageViaTunnel(data, tunnelClient, authenticateClient, logger, subscriptionService) {
|
|
9377
|
+
function handleAuthMessageViaTunnel(data, tunnelClient, authenticateClient, logger, subscriptionService, messageBroadcaster) {
|
|
9149
9378
|
const authResult = AuthMessageSchema.safeParse(data);
|
|
9150
9379
|
if (!authResult.success) {
|
|
9151
9380
|
logger.warn(
|
|
@@ -9175,6 +9404,32 @@ function handleAuthMessageViaTunnel(data, tunnelClient, authenticateClient, logg
|
|
|
9175
9404
|
);
|
|
9176
9405
|
}
|
|
9177
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
|
+
}
|
|
9178
9433
|
const responseJson = JSON.stringify(result);
|
|
9179
9434
|
if (tunnelClient.send(responseJson)) {
|
|
9180
9435
|
logger.info(
|
|
@@ -9413,7 +9668,7 @@ async function bootstrap() {
|
|
|
9413
9668
|
}
|
|
9414
9669
|
};
|
|
9415
9670
|
const createMessageHandlers = () => ({
|
|
9416
|
-
auth: (socket, message) => {
|
|
9671
|
+
auth: async (socket, message) => {
|
|
9417
9672
|
const authMessage = message;
|
|
9418
9673
|
const deviceId = authMessage.payload.device_id;
|
|
9419
9674
|
const result = authenticateClient.execute({
|
|
@@ -9431,6 +9686,21 @@ async function bootstrap() {
|
|
|
9431
9686
|
);
|
|
9432
9687
|
}
|
|
9433
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
|
+
}
|
|
9434
9704
|
sendToDevice(socket, deviceId, JSON.stringify(result));
|
|
9435
9705
|
return Promise.resolve();
|
|
9436
9706
|
},
|
|
@@ -9875,13 +10145,13 @@ async function bootstrap() {
|
|
|
9875
10145
|
return Promise.resolve();
|
|
9876
10146
|
},
|
|
9877
10147
|
// Clear supervisor conversation history (global)
|
|
9878
|
-
"supervisor.clear_context": (socket, message) => {
|
|
10148
|
+
"supervisor.clear_context": async (socket, message) => {
|
|
9879
10149
|
const clearMessage = message;
|
|
9880
10150
|
const directClient = clientRegistry.getBySocket(socket);
|
|
9881
10151
|
const tunnelClient2 = clearMessage.device_id ? clientRegistry.getByDeviceId(new DeviceId(clearMessage.device_id)) : void 0;
|
|
9882
10152
|
const isAuthenticated = directClient?.isAuthenticated ?? tunnelClient2?.isAuthenticated;
|
|
9883
10153
|
if (isAuthenticated) {
|
|
9884
|
-
supervisorAgent.clearContext();
|
|
10154
|
+
await supervisorAgent.clearContext();
|
|
9885
10155
|
sendToDevice(
|
|
9886
10156
|
socket,
|
|
9887
10157
|
clearMessage.device_id,
|
|
@@ -10132,7 +10402,7 @@ async function bootstrap() {
|
|
|
10132
10402
|
message_id: messageId
|
|
10133
10403
|
}
|
|
10134
10404
|
};
|
|
10135
|
-
messageBroadcaster.broadcastToSubscribers(
|
|
10405
|
+
await messageBroadcaster.broadcastToSubscribers(
|
|
10136
10406
|
sessionId,
|
|
10137
10407
|
JSON.stringify(errorEvent)
|
|
10138
10408
|
);
|
|
@@ -10168,7 +10438,7 @@ async function bootstrap() {
|
|
|
10168
10438
|
from_device_id: deviceId
|
|
10169
10439
|
}
|
|
10170
10440
|
};
|
|
10171
|
-
messageBroadcaster.broadcastToSubscribers(
|
|
10441
|
+
await messageBroadcaster.broadcastToSubscribers(
|
|
10172
10442
|
sessionId,
|
|
10173
10443
|
JSON.stringify(transcriptionEvent)
|
|
10174
10444
|
);
|
|
@@ -10259,7 +10529,7 @@ async function bootstrap() {
|
|
|
10259
10529
|
timestamp: Date.now()
|
|
10260
10530
|
}
|
|
10261
10531
|
};
|
|
10262
|
-
messageBroadcaster.broadcastToSubscribers(
|
|
10532
|
+
await messageBroadcaster.broadcastToSubscribers(
|
|
10263
10533
|
sessionId,
|
|
10264
10534
|
JSON.stringify(errorEvent)
|
|
10265
10535
|
);
|
|
@@ -10387,14 +10657,20 @@ async function bootstrap() {
|
|
|
10387
10657
|
is_complete: true
|
|
10388
10658
|
}
|
|
10389
10659
|
};
|
|
10390
|
-
messageBroadcaster.broadcastToSubscribers(
|
|
10660
|
+
void messageBroadcaster.broadcastToSubscribers(
|
|
10391
10661
|
sessionId,
|
|
10392
10662
|
JSON.stringify(cancelOutput)
|
|
10393
|
-
)
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10663
|
+
).then(() => {
|
|
10664
|
+
logger.info(
|
|
10665
|
+
{ sessionId },
|
|
10666
|
+
"Broadcasted cancel message to subscribers"
|
|
10667
|
+
);
|
|
10668
|
+
}).catch((error) => {
|
|
10669
|
+
logger.error(
|
|
10670
|
+
{ sessionId, error },
|
|
10671
|
+
"Failed to broadcast cancel message"
|
|
10672
|
+
);
|
|
10673
|
+
});
|
|
10398
10674
|
chatHistoryService.saveAgentMessage(sessionId, "assistant", "", [
|
|
10399
10675
|
cancelBlock
|
|
10400
10676
|
]);
|
|
@@ -10827,8 +11103,27 @@ async function bootstrap() {
|
|
|
10827
11103
|
tunnelClient,
|
|
10828
11104
|
authenticateClient,
|
|
10829
11105
|
logger,
|
|
10830
|
-
subscriptionService
|
|
11106
|
+
subscriptionService,
|
|
11107
|
+
broadcaster
|
|
10831
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
|
+
}
|
|
10832
11127
|
} else {
|
|
10833
11128
|
handleTunnelMessage(
|
|
10834
11129
|
message,
|
|
@@ -10880,9 +11175,13 @@ async function bootstrap() {
|
|
|
10880
11175
|
subscriptionRepository,
|
|
10881
11176
|
logger
|
|
10882
11177
|
});
|
|
11178
|
+
let supervisorMessageSequence = 0;
|
|
11179
|
+
const agentMessageSequences = /* @__PURE__ */ new Map();
|
|
10883
11180
|
const broadcaster = messageBroadcaster;
|
|
10884
11181
|
const agentMessageAccumulator = /* @__PURE__ */ new Map();
|
|
10885
11182
|
const agentStreamingMessageIds = /* @__PURE__ */ new Map();
|
|
11183
|
+
const STREAMING_STATE_GRACE_PERIOD_MS = 1e4;
|
|
11184
|
+
const agentCleanupTimeouts = /* @__PURE__ */ new Map();
|
|
10886
11185
|
const getOrCreateAgentStreamingMessageId = (sessionId) => {
|
|
10887
11186
|
let messageId = agentStreamingMessageIds.get(sessionId);
|
|
10888
11187
|
if (!messageId) {
|
|
@@ -10892,7 +11191,16 @@ async function bootstrap() {
|
|
|
10892
11191
|
return messageId;
|
|
10893
11192
|
};
|
|
10894
11193
|
const clearAgentStreamingMessageId = (sessionId) => {
|
|
10895
|
-
|
|
11194
|
+
const existingTimeout = agentCleanupTimeouts.get(sessionId);
|
|
11195
|
+
if (existingTimeout) {
|
|
11196
|
+
clearTimeout(existingTimeout);
|
|
11197
|
+
}
|
|
11198
|
+
const timeout = setTimeout(() => {
|
|
11199
|
+
agentStreamingMessageIds.delete(sessionId);
|
|
11200
|
+
agentCleanupTimeouts.delete(sessionId);
|
|
11201
|
+
logger.debug({ sessionId }, "Agent streaming state cleaned up after grace period");
|
|
11202
|
+
}, STREAMING_STATE_GRACE_PERIOD_MS);
|
|
11203
|
+
agentCleanupTimeouts.set(sessionId, timeout);
|
|
10896
11204
|
};
|
|
10897
11205
|
agentSessionManager.on(
|
|
10898
11206
|
"blocks",
|
|
@@ -10966,10 +11274,14 @@ async function bootstrap() {
|
|
|
10966
11274
|
}
|
|
10967
11275
|
const mergedBlocks = mergeToolBlocks(accumulatedBlocks);
|
|
10968
11276
|
const fullAccumulatedText = mergedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
|
|
11277
|
+
const currentSequence = agentMessageSequences.get(sessionId) ?? 0;
|
|
11278
|
+
const newSequence = currentSequence + 1;
|
|
11279
|
+
agentMessageSequences.set(sessionId, newSequence);
|
|
10969
11280
|
const outputEvent = {
|
|
10970
11281
|
type: "session.output",
|
|
10971
11282
|
session_id: sessionId,
|
|
10972
11283
|
streaming_message_id: streamingMessageId,
|
|
11284
|
+
sequence: newSequence,
|
|
10973
11285
|
payload: {
|
|
10974
11286
|
content_type: "agent",
|
|
10975
11287
|
content: fullAccumulatedText,
|
|
@@ -10980,13 +11292,19 @@ async function bootstrap() {
|
|
|
10980
11292
|
is_complete: isComplete
|
|
10981
11293
|
}
|
|
10982
11294
|
};
|
|
10983
|
-
broadcaster.broadcastToSubscribers(
|
|
11295
|
+
await broadcaster.broadcastToSubscribers(
|
|
10984
11296
|
sessionId,
|
|
10985
11297
|
JSON.stringify(outputEvent)
|
|
10986
11298
|
);
|
|
10987
11299
|
if (isComplete) {
|
|
10988
11300
|
clearAgentStreamingMessageId(sessionId);
|
|
10989
|
-
|
|
11301
|
+
setTimeout(() => {
|
|
11302
|
+
agentMessageAccumulator.delete(sessionId);
|
|
11303
|
+
logger.debug(
|
|
11304
|
+
{ sessionId, messageId: streamingMessageId },
|
|
11305
|
+
"Agent message accumulator cleaned up after grace period"
|
|
11306
|
+
);
|
|
11307
|
+
}, STREAMING_STATE_GRACE_PERIOD_MS);
|
|
10990
11308
|
}
|
|
10991
11309
|
if (isComplete && fullTextContent.length > 0) {
|
|
10992
11310
|
const pendingVoiceCommand = pendingAgentVoiceCommands.get(sessionId);
|
|
@@ -11045,7 +11363,7 @@ async function bootstrap() {
|
|
|
11045
11363
|
from_device_id: originDeviceId
|
|
11046
11364
|
}
|
|
11047
11365
|
};
|
|
11048
|
-
broadcaster.broadcastToSubscribers(
|
|
11366
|
+
await broadcaster.broadcastToSubscribers(
|
|
11049
11367
|
sessionId,
|
|
11050
11368
|
JSON.stringify(voiceOutputEvent)
|
|
11051
11369
|
);
|
|
@@ -11090,6 +11408,7 @@ async function bootstrap() {
|
|
|
11090
11408
|
const outputEvent = {
|
|
11091
11409
|
type: "supervisor.output",
|
|
11092
11410
|
streaming_message_id: streamingMessageId,
|
|
11411
|
+
sequence: ++supervisorMessageSequence,
|
|
11093
11412
|
payload: {
|
|
11094
11413
|
content_type: "supervisor",
|
|
11095
11414
|
content: textContent,
|
|
@@ -11101,7 +11420,13 @@ async function bootstrap() {
|
|
|
11101
11420
|
const message = JSON.stringify(outputEvent);
|
|
11102
11421
|
broadcaster.broadcastToAll(message);
|
|
11103
11422
|
if (isComplete) {
|
|
11104
|
-
|
|
11423
|
+
setTimeout(() => {
|
|
11424
|
+
supervisorMessageAccumulator.clear();
|
|
11425
|
+
logger.debug(
|
|
11426
|
+
{ messageId: streamingMessageId },
|
|
11427
|
+
"Supervisor message accumulator cleaned up after grace period"
|
|
11428
|
+
);
|
|
11429
|
+
}, STREAMING_STATE_GRACE_PERIOD_MS);
|
|
11105
11430
|
}
|
|
11106
11431
|
if (isComplete && finalOutput && finalOutput.length > 0) {
|
|
11107
11432
|
chatHistoryService.saveSupervisorMessage(
|
|
@@ -11230,21 +11555,23 @@ async function bootstrap() {
|
|
|
11230
11555
|
batchIntervalMs: env.TERMINAL_BATCH_INTERVAL_MS,
|
|
11231
11556
|
maxBatchSize: env.TERMINAL_BATCH_MAX_SIZE,
|
|
11232
11557
|
onFlush: (batchedData) => {
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
|
|
11558
|
+
void (async () => {
|
|
11559
|
+
const outputMessage = session.addOutputToBuffer(batchedData);
|
|
11560
|
+
const outputEvent = {
|
|
11561
|
+
type: "session.output",
|
|
11562
|
+
session_id: sessionId.value,
|
|
11563
|
+
payload: {
|
|
11564
|
+
content_type: "terminal",
|
|
11565
|
+
content: batchedData,
|
|
11566
|
+
timestamp: outputMessage.timestamp,
|
|
11567
|
+
sequence: outputMessage.sequence
|
|
11568
|
+
}
|
|
11569
|
+
};
|
|
11570
|
+
await broadcaster.broadcastToSubscribers(
|
|
11571
|
+
sessionId.value,
|
|
11572
|
+
JSON.stringify(outputEvent)
|
|
11573
|
+
);
|
|
11574
|
+
})();
|
|
11248
11575
|
}
|
|
11249
11576
|
});
|
|
11250
11577
|
session.onOutput((data) => {
|
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",
|