@tiflis-io/tiflis-code-workstation 0.3.11 → 0.3.12
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 +286 -209
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -1149,8 +1149,9 @@ var SyncMessageSchema = z2.object({
|
|
|
1149
1149
|
// If true, excludes message histories (for watchOS)
|
|
1150
1150
|
});
|
|
1151
1151
|
var HistoryRequestPayloadSchema = z2.object({
|
|
1152
|
-
session_id: z2.string().optional()
|
|
1153
|
-
|
|
1152
|
+
session_id: z2.string().nullable().optional(),
|
|
1153
|
+
before_sequence: z2.number().int().optional(),
|
|
1154
|
+
limit: z2.number().int().min(1).max(50).optional()
|
|
1154
1155
|
});
|
|
1155
1156
|
var HistoryRequestSchema = z2.object({
|
|
1156
1157
|
type: z2.literal("history.request"),
|
|
@@ -1193,7 +1194,7 @@ var TerminateSessionSchema = z2.object({
|
|
|
1193
1194
|
var SupervisorCommandPayloadSchema = z2.object({
|
|
1194
1195
|
command: z2.string().optional(),
|
|
1195
1196
|
audio: z2.string().optional(),
|
|
1196
|
-
audio_format: z2.enum(["m4a", "wav", "mp3"]).optional(),
|
|
1197
|
+
audio_format: z2.enum(["m4a", "wav", "mp3", "webm", "opus"]).optional(),
|
|
1197
1198
|
message_id: z2.string().optional(),
|
|
1198
1199
|
language: z2.string().optional()
|
|
1199
1200
|
}).refine(
|
|
@@ -1237,7 +1238,7 @@ var SessionExecutePayloadSchema = z2.object({
|
|
|
1237
1238
|
text: z2.string().optional(),
|
|
1238
1239
|
// Alias for content (backward compat)
|
|
1239
1240
|
audio: z2.string().optional(),
|
|
1240
|
-
audio_format: z2.enum(["m4a", "wav", "mp3"]).optional(),
|
|
1241
|
+
audio_format: z2.enum(["m4a", "wav", "mp3", "webm", "opus"]).optional(),
|
|
1241
1242
|
message_id: z2.string().optional(),
|
|
1242
1243
|
// For linking transcription back to voice message
|
|
1243
1244
|
language: z2.string().optional(),
|
|
@@ -1370,6 +1371,13 @@ function parseClientMessage(data) {
|
|
|
1370
1371
|
}
|
|
1371
1372
|
return void 0;
|
|
1372
1373
|
}
|
|
1374
|
+
function parseClientMessageWithErrors(data) {
|
|
1375
|
+
const result = IncomingClientMessageSchema.safeParse(data);
|
|
1376
|
+
if (result.success) {
|
|
1377
|
+
return { success: true, data: result.data };
|
|
1378
|
+
}
|
|
1379
|
+
return { success: false, errors: result.error.issues };
|
|
1380
|
+
}
|
|
1373
1381
|
function parseTunnelMessage(data) {
|
|
1374
1382
|
const result = IncomingTunnelMessageSchema.safeParse(data);
|
|
1375
1383
|
if (result.success) {
|
|
@@ -4645,8 +4653,8 @@ var CreateSessionUseCase = class {
|
|
|
4645
4653
|
*/
|
|
4646
4654
|
checkSessionLimits(sessionType) {
|
|
4647
4655
|
if (sessionType === "terminal") {
|
|
4648
|
-
const
|
|
4649
|
-
if (
|
|
4656
|
+
const count2 = this.deps.sessionManager.countByType("terminal");
|
|
4657
|
+
if (count2 >= SESSION_CONFIG.MAX_TERMINAL_SESSIONS) {
|
|
4650
4658
|
throw new SessionLimitReachedError("terminal", SESSION_CONFIG.MAX_TERMINAL_SESSIONS);
|
|
4651
4659
|
}
|
|
4652
4660
|
} else if (sessionType !== "supervisor") {
|
|
@@ -5084,7 +5092,7 @@ var MessageBroadcasterImpl = class {
|
|
|
5084
5092
|
};
|
|
5085
5093
|
|
|
5086
5094
|
// src/infrastructure/persistence/repositories/message-repository.ts
|
|
5087
|
-
import { eq as eq3, desc, gt, and as and2, max } from "drizzle-orm";
|
|
5095
|
+
import { eq as eq3, desc, gt, lt, and as and2, max, count } from "drizzle-orm";
|
|
5088
5096
|
import { nanoid as nanoid2 } from "nanoid";
|
|
5089
5097
|
var MessageRepository = class {
|
|
5090
5098
|
/**
|
|
@@ -5110,17 +5118,24 @@ var MessageRepository = class {
|
|
|
5110
5118
|
db2.insert(messages).values(newMessage).run();
|
|
5111
5119
|
return { ...newMessage, createdAt: newMessage.createdAt };
|
|
5112
5120
|
}
|
|
5113
|
-
/**
|
|
5114
|
-
* Gets messages for a session with pagination.
|
|
5115
|
-
* Returns messages ordered by sequence descending (newest first).
|
|
5116
|
-
*/
|
|
5117
5121
|
getBySession(sessionId, limit = 100) {
|
|
5118
5122
|
const db2 = getDatabase();
|
|
5119
5123
|
return db2.select().from(messages).where(eq3(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all();
|
|
5120
5124
|
}
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5125
|
+
getBySessionPaginated(sessionId, options = {}) {
|
|
5126
|
+
const db2 = getDatabase();
|
|
5127
|
+
const limit = Math.min(options.limit ?? 20, 50);
|
|
5128
|
+
const totalResult = db2.select({ count: count() }).from(messages).where(eq3(messages.sessionId, sessionId)).get();
|
|
5129
|
+
const totalCount = totalResult?.count ?? 0;
|
|
5130
|
+
const whereClause = options.beforeSequence ? and2(
|
|
5131
|
+
eq3(messages.sessionId, sessionId),
|
|
5132
|
+
lt(messages.sequence, options.beforeSequence)
|
|
5133
|
+
) : eq3(messages.sessionId, sessionId);
|
|
5134
|
+
const rows = db2.select().from(messages).where(whereClause).orderBy(desc(messages.sequence)).limit(limit + 1).all();
|
|
5135
|
+
const hasMore = rows.length > limit;
|
|
5136
|
+
const resultMessages = hasMore ? rows.slice(0, limit) : rows;
|
|
5137
|
+
return { messages: resultMessages, hasMore, totalCount };
|
|
5138
|
+
}
|
|
5124
5139
|
getAfterTimestamp(sessionId, timestamp, limit = 100) {
|
|
5125
5140
|
const db2 = getDatabase();
|
|
5126
5141
|
return db2.select().from(messages).where(and2(eq3(messages.sessionId, sessionId), gt(messages.createdAt, timestamp))).orderBy(messages.createdAt).limit(limit).all();
|
|
@@ -5810,9 +5825,40 @@ var ChatHistoryService = class _ChatHistoryService {
|
|
|
5810
5825
|
};
|
|
5811
5826
|
});
|
|
5812
5827
|
}
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5828
|
+
getSupervisorHistoryPaginated(options = {}) {
|
|
5829
|
+
const sessionId = _ChatHistoryService.SUPERVISOR_SESSION_ID;
|
|
5830
|
+
const result = this.messageRepo.getBySessionPaginated(sessionId, options);
|
|
5831
|
+
const messages2 = result.messages.reverse().map((row) => {
|
|
5832
|
+
let contentBlocks;
|
|
5833
|
+
if (row.contentBlocks) {
|
|
5834
|
+
try {
|
|
5835
|
+
contentBlocks = JSON.parse(row.contentBlocks);
|
|
5836
|
+
} catch {
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5839
|
+
return {
|
|
5840
|
+
id: row.id,
|
|
5841
|
+
sessionId: row.sessionId,
|
|
5842
|
+
sequence: row.sequence,
|
|
5843
|
+
role: row.role,
|
|
5844
|
+
contentType: row.contentType,
|
|
5845
|
+
content: row.content,
|
|
5846
|
+
contentBlocks,
|
|
5847
|
+
audioInputPath: row.audioInputPath,
|
|
5848
|
+
audioOutputPath: row.audioOutputPath,
|
|
5849
|
+
isComplete: row.isComplete ?? false,
|
|
5850
|
+
createdAt: row.createdAt
|
|
5851
|
+
};
|
|
5852
|
+
});
|
|
5853
|
+
const firstMsg = messages2[0];
|
|
5854
|
+
const lastMsg = messages2[messages2.length - 1];
|
|
5855
|
+
return {
|
|
5856
|
+
messages: messages2,
|
|
5857
|
+
hasMore: result.hasMore,
|
|
5858
|
+
oldestSequence: firstMsg?.sequence,
|
|
5859
|
+
newestSequence: lastMsg?.sequence
|
|
5860
|
+
};
|
|
5861
|
+
}
|
|
5816
5862
|
clearSupervisorHistory() {
|
|
5817
5863
|
const sessionId = _ChatHistoryService.SUPERVISOR_SESSION_ID;
|
|
5818
5864
|
this.messageRepo.deleteBySession(sessionId);
|
|
@@ -5895,9 +5941,43 @@ var ChatHistoryService = class _ChatHistoryService {
|
|
|
5895
5941
|
};
|
|
5896
5942
|
});
|
|
5897
5943
|
}
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5944
|
+
getAgentHistoryPaginated(sessionId, options = {}) {
|
|
5945
|
+
const result = this.messageRepo.getBySessionPaginated(sessionId, options);
|
|
5946
|
+
const storedMessages = result.messages.reverse().map((row) => {
|
|
5947
|
+
let contentBlocks;
|
|
5948
|
+
if (row.contentBlocks) {
|
|
5949
|
+
try {
|
|
5950
|
+
contentBlocks = JSON.parse(row.contentBlocks);
|
|
5951
|
+
} catch (error) {
|
|
5952
|
+
this.logger.error(
|
|
5953
|
+
{ sessionId, messageId: row.id, error },
|
|
5954
|
+
"Failed to parse contentBlocks JSON"
|
|
5955
|
+
);
|
|
5956
|
+
}
|
|
5957
|
+
}
|
|
5958
|
+
return {
|
|
5959
|
+
id: row.id,
|
|
5960
|
+
sessionId: row.sessionId,
|
|
5961
|
+
sequence: row.sequence,
|
|
5962
|
+
role: row.role,
|
|
5963
|
+
contentType: row.contentType,
|
|
5964
|
+
content: row.content,
|
|
5965
|
+
contentBlocks,
|
|
5966
|
+
audioInputPath: row.audioInputPath,
|
|
5967
|
+
audioOutputPath: row.audioOutputPath,
|
|
5968
|
+
isComplete: row.isComplete ?? false,
|
|
5969
|
+
createdAt: row.createdAt
|
|
5970
|
+
};
|
|
5971
|
+
});
|
|
5972
|
+
const firstMsg = storedMessages[0];
|
|
5973
|
+
const lastMsg = storedMessages[storedMessages.length - 1];
|
|
5974
|
+
return {
|
|
5975
|
+
messages: storedMessages,
|
|
5976
|
+
hasMore: result.hasMore,
|
|
5977
|
+
oldestSequence: firstMsg?.sequence,
|
|
5978
|
+
newestSequence: lastMsg?.sequence
|
|
5979
|
+
};
|
|
5980
|
+
}
|
|
5901
5981
|
clearAgentHistory(sessionId) {
|
|
5902
5982
|
this.messageRepo.deleteBySession(sessionId);
|
|
5903
5983
|
this.logger.info({ sessionId }, "Agent session history cleared");
|
|
@@ -6431,18 +6511,35 @@ var InMemorySessionManager = class extends EventEmitter3 {
|
|
|
6431
6511
|
this.logger.info({ sessionId: sessionId.value }, "Session terminated");
|
|
6432
6512
|
}
|
|
6433
6513
|
/**
|
|
6434
|
-
* Terminates all sessions.
|
|
6514
|
+
* Terminates all sessions with individual timeouts.
|
|
6515
|
+
* Each session has a 3-second timeout to prevent hanging.
|
|
6435
6516
|
*/
|
|
6436
6517
|
async terminateAll() {
|
|
6437
6518
|
this.agentSessionManager.cleanup();
|
|
6438
6519
|
const sessions2 = Array.from(this.sessions.values());
|
|
6520
|
+
const sessionCount = sessions2.length;
|
|
6521
|
+
if (sessionCount === 0) {
|
|
6522
|
+
this.logger.info("No sessions to terminate");
|
|
6523
|
+
return;
|
|
6524
|
+
}
|
|
6525
|
+
this.logger.info({ count: sessionCount }, "Terminating all sessions...");
|
|
6526
|
+
const INDIVIDUAL_TIMEOUT_MS = 3e3;
|
|
6439
6527
|
await Promise.all(
|
|
6440
6528
|
sessions2.map(async (session) => {
|
|
6529
|
+
const sessionId = session.id.value;
|
|
6441
6530
|
try {
|
|
6442
|
-
|
|
6531
|
+
const terminatePromise = session.terminate();
|
|
6532
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
6533
|
+
setTimeout(() => {
|
|
6534
|
+
this.logger.warn({ sessionId }, "Session termination timed out, skipping");
|
|
6535
|
+
resolve2();
|
|
6536
|
+
}, INDIVIDUAL_TIMEOUT_MS);
|
|
6537
|
+
});
|
|
6538
|
+
await Promise.race([terminatePromise, timeoutPromise]);
|
|
6539
|
+
this.logger.debug({ sessionId }, "Session terminated");
|
|
6443
6540
|
} catch (error) {
|
|
6444
6541
|
this.logger.error(
|
|
6445
|
-
{ sessionId
|
|
6542
|
+
{ sessionId, error },
|
|
6446
6543
|
"Error terminating session"
|
|
6447
6544
|
);
|
|
6448
6545
|
}
|
|
@@ -6450,7 +6547,7 @@ var InMemorySessionManager = class extends EventEmitter3 {
|
|
|
6450
6547
|
);
|
|
6451
6548
|
this.sessions.clear();
|
|
6452
6549
|
this.supervisorSession = null;
|
|
6453
|
-
this.logger.info({ count:
|
|
6550
|
+
this.logger.info({ count: sessionCount }, "All sessions terminated");
|
|
6454
6551
|
}
|
|
6455
6552
|
/**
|
|
6456
6553
|
* Gets the count of active sessions by type.
|
|
@@ -8977,11 +9074,19 @@ function handleTunnelMessage(rawMessage, tunnelClient, messageBroadcaster, creat
|
|
|
8977
9074
|
return;
|
|
8978
9075
|
}
|
|
8979
9076
|
const handler = handlers[messageType];
|
|
8980
|
-
const
|
|
8981
|
-
if (!
|
|
8982
|
-
logger.warn(
|
|
9077
|
+
const parseResult = parseClientMessageWithErrors(data);
|
|
9078
|
+
if (!parseResult.success) {
|
|
9079
|
+
logger.warn(
|
|
9080
|
+
{
|
|
9081
|
+
type: messageType,
|
|
9082
|
+
errors: parseResult.errors,
|
|
9083
|
+
rawMessage: JSON.stringify(data).slice(0, 500)
|
|
9084
|
+
},
|
|
9085
|
+
"Failed to parse tunnel message - Zod validation failed"
|
|
9086
|
+
);
|
|
8983
9087
|
return;
|
|
8984
9088
|
}
|
|
9089
|
+
const parsedMessage = parseResult.data;
|
|
8985
9090
|
handler(virtualSocket, parsedMessage).catch((error) => {
|
|
8986
9091
|
logger.error(
|
|
8987
9092
|
{ error, type: messageType },
|
|
@@ -9169,6 +9274,29 @@ async function bootstrap() {
|
|
|
9169
9274
|
const pendingSupervisorVoiceCommands = /* @__PURE__ */ new Map();
|
|
9170
9275
|
const pendingAgentVoiceCommands = /* @__PURE__ */ new Map();
|
|
9171
9276
|
const cancelledDuringTranscription = /* @__PURE__ */ new Set();
|
|
9277
|
+
const supervisorMessageAccumulator = {
|
|
9278
|
+
blocks: [],
|
|
9279
|
+
streamingMessageId: null,
|
|
9280
|
+
get() {
|
|
9281
|
+
return this.blocks;
|
|
9282
|
+
},
|
|
9283
|
+
getStreamingMessageId() {
|
|
9284
|
+
return this.streamingMessageId;
|
|
9285
|
+
},
|
|
9286
|
+
set(blocks) {
|
|
9287
|
+
this.blocks = blocks;
|
|
9288
|
+
},
|
|
9289
|
+
clear() {
|
|
9290
|
+
this.blocks = [];
|
|
9291
|
+
this.streamingMessageId = null;
|
|
9292
|
+
},
|
|
9293
|
+
accumulate(newBlocks) {
|
|
9294
|
+
if (this.blocks.length === 0 && newBlocks.length > 0) {
|
|
9295
|
+
this.streamingMessageId = randomUUID5();
|
|
9296
|
+
}
|
|
9297
|
+
accumulateBlocks(this.blocks, newBlocks);
|
|
9298
|
+
}
|
|
9299
|
+
};
|
|
9172
9300
|
const expectedAuthKey = new AuthKey(env.WORKSTATION_AUTH_KEY);
|
|
9173
9301
|
let messageBroadcaster = null;
|
|
9174
9302
|
const supervisorAgent = env.MOCK_MODE ? new MockSupervisorAgent({
|
|
@@ -9261,11 +9389,10 @@ async function bootstrap() {
|
|
|
9261
9389
|
const syncMessage = message;
|
|
9262
9390
|
const client = clientRegistry.getBySocket(socket) ?? (syncMessage.device_id ? clientRegistry.getByDeviceId(new DeviceId(syncMessage.device_id)) : void 0);
|
|
9263
9391
|
const subscriptions2 = client ? client.getSubscriptions() : [];
|
|
9264
|
-
const isLightweight = syncMessage.lightweight === true;
|
|
9265
9392
|
const inMemorySessions = sessionManager.getSessionInfos();
|
|
9266
9393
|
const persistedAgentSessions = chatHistoryService.getActiveAgentSessions();
|
|
9267
9394
|
logger.debug(
|
|
9268
|
-
{ persistedAgentSessions, inMemoryCount: inMemorySessions.length
|
|
9395
|
+
{ persistedAgentSessions, inMemoryCount: inMemorySessions.length },
|
|
9269
9396
|
"Sync: fetched sessions"
|
|
9270
9397
|
);
|
|
9271
9398
|
const inMemorySessionIds = new Set(
|
|
@@ -9290,119 +9417,14 @@ async function bootstrap() {
|
|
|
9290
9417
|
};
|
|
9291
9418
|
});
|
|
9292
9419
|
const sessions2 = [...inMemorySessions, ...restoredAgentSessions];
|
|
9293
|
-
if (isLightweight) {
|
|
9294
|
-
const availableAgentsMap2 = getAvailableAgents();
|
|
9295
|
-
const availableAgents2 = Array.from(availableAgentsMap2.values()).map(
|
|
9296
|
-
(agent) => ({
|
|
9297
|
-
name: agent.name,
|
|
9298
|
-
base_type: agent.baseType,
|
|
9299
|
-
description: agent.description,
|
|
9300
|
-
is_alias: agent.isAlias
|
|
9301
|
-
})
|
|
9302
|
-
);
|
|
9303
|
-
const hiddenBaseTypes2 = getDisabledBaseAgents();
|
|
9304
|
-
const workspacesList2 = await workspaceDiscovery.listWorkspaces();
|
|
9305
|
-
const workspaces2 = await Promise.all(
|
|
9306
|
-
workspacesList2.map(async (ws) => {
|
|
9307
|
-
const projects = await workspaceDiscovery.listProjects(ws.name);
|
|
9308
|
-
return {
|
|
9309
|
-
name: ws.name,
|
|
9310
|
-
projects: projects.map((p) => ({
|
|
9311
|
-
name: p.name,
|
|
9312
|
-
is_git_repo: p.isGitRepo,
|
|
9313
|
-
default_branch: p.defaultBranch
|
|
9314
|
-
}))
|
|
9315
|
-
};
|
|
9316
|
-
})
|
|
9317
|
-
);
|
|
9318
|
-
const executingStates2 = {};
|
|
9319
|
-
for (const session of sessions2) {
|
|
9320
|
-
if (session.session_type === "cursor" || session.session_type === "claude" || session.session_type === "opencode") {
|
|
9321
|
-
executingStates2[session.session_id] = agentSessionManager.isExecuting(
|
|
9322
|
-
session.session_id
|
|
9323
|
-
);
|
|
9324
|
-
}
|
|
9325
|
-
}
|
|
9326
|
-
const supervisorIsExecuting2 = supervisorAgent.isProcessing();
|
|
9327
|
-
logger.info(
|
|
9328
|
-
{
|
|
9329
|
-
totalSessions: sessions2.length,
|
|
9330
|
-
isLightweight: true,
|
|
9331
|
-
availableAgentsCount: availableAgents2.length,
|
|
9332
|
-
workspacesCount: workspaces2.length,
|
|
9333
|
-
supervisorIsExecuting: supervisorIsExecuting2
|
|
9334
|
-
},
|
|
9335
|
-
"Sync: sending lightweight state to client (no histories)"
|
|
9336
|
-
);
|
|
9337
|
-
const syncStateMessage2 = JSON.stringify({
|
|
9338
|
-
type: "sync.state",
|
|
9339
|
-
id: syncMessage.id,
|
|
9340
|
-
payload: {
|
|
9341
|
-
sessions: sessions2,
|
|
9342
|
-
subscriptions: subscriptions2,
|
|
9343
|
-
availableAgents: availableAgents2,
|
|
9344
|
-
hiddenBaseTypes: hiddenBaseTypes2,
|
|
9345
|
-
workspaces: workspaces2,
|
|
9346
|
-
supervisorIsExecuting: supervisorIsExecuting2,
|
|
9347
|
-
executingStates: executingStates2
|
|
9348
|
-
// Omit: supervisorHistory, agentHistories, currentStreamingBlocks
|
|
9349
|
-
}
|
|
9350
|
-
});
|
|
9351
|
-
sendToDevice(socket, syncMessage.device_id, syncStateMessage2);
|
|
9352
|
-
return Promise.resolve();
|
|
9353
|
-
}
|
|
9354
9420
|
const supervisorHistoryRaw = chatHistoryService.getSupervisorHistory();
|
|
9355
|
-
|
|
9356
|
-
supervisorHistoryRaw.
|
|
9357
|
-
sequence: msg.sequence,
|
|
9358
|
-
role: msg.role,
|
|
9359
|
-
content: msg.content,
|
|
9360
|
-
content_blocks: await chatHistoryService.enrichBlocksWithAudio(
|
|
9361
|
-
msg.contentBlocks,
|
|
9362
|
-
msg.audioOutputPath,
|
|
9363
|
-
msg.audioInputPath,
|
|
9364
|
-
false
|
|
9365
|
-
// Don't include audio in sync.state
|
|
9366
|
-
),
|
|
9367
|
-
createdAt: msg.createdAt.toISOString()
|
|
9368
|
-
}))
|
|
9369
|
-
);
|
|
9370
|
-
if (supervisorHistory.length > 0) {
|
|
9371
|
-
const historyForAgent = supervisorHistory.filter((msg) => msg.role === "user" || msg.role === "assistant").map((msg) => ({
|
|
9421
|
+
if (supervisorHistoryRaw.length > 0) {
|
|
9422
|
+
const historyForAgent = supervisorHistoryRaw.filter((msg) => msg.role === "user" || msg.role === "assistant").map((msg) => ({
|
|
9372
9423
|
role: msg.role,
|
|
9373
9424
|
content: msg.content
|
|
9374
9425
|
}));
|
|
9375
9426
|
supervisorAgent.restoreHistory(historyForAgent);
|
|
9376
9427
|
}
|
|
9377
|
-
const agentSessionIds = sessions2.filter(
|
|
9378
|
-
(s) => s.session_type === "cursor" || s.session_type === "claude" || s.session_type === "opencode"
|
|
9379
|
-
).map((s) => s.session_id);
|
|
9380
|
-
const agentHistoriesMap = chatHistoryService.getAllAgentHistories(agentSessionIds);
|
|
9381
|
-
const agentHistories = {};
|
|
9382
|
-
const historyEntries = Array.from(agentHistoriesMap.entries());
|
|
9383
|
-
const processedHistories = await Promise.all(
|
|
9384
|
-
historyEntries.map(async ([sessionId, history]) => {
|
|
9385
|
-
const enrichedHistory = await Promise.all(
|
|
9386
|
-
history.map(async (msg) => ({
|
|
9387
|
-
sequence: msg.sequence,
|
|
9388
|
-
role: msg.role,
|
|
9389
|
-
content: msg.content,
|
|
9390
|
-
content_blocks: await chatHistoryService.enrichBlocksWithAudio(
|
|
9391
|
-
msg.contentBlocks,
|
|
9392
|
-
msg.audioOutputPath,
|
|
9393
|
-
msg.audioInputPath,
|
|
9394
|
-
false
|
|
9395
|
-
// Don't include audio in sync.state
|
|
9396
|
-
),
|
|
9397
|
-
createdAt: msg.createdAt.toISOString()
|
|
9398
|
-
}))
|
|
9399
|
-
);
|
|
9400
|
-
return { sessionId, history: enrichedHistory };
|
|
9401
|
-
})
|
|
9402
|
-
);
|
|
9403
|
-
for (const { sessionId, history } of processedHistories) {
|
|
9404
|
-
agentHistories[sessionId] = history;
|
|
9405
|
-
}
|
|
9406
9428
|
const availableAgentsMap = getAvailableAgents();
|
|
9407
9429
|
const availableAgents = Array.from(availableAgentsMap.values()).map(
|
|
9408
9430
|
(agent) => ({
|
|
@@ -9435,31 +9457,23 @@ async function bootstrap() {
|
|
|
9435
9457
|
);
|
|
9436
9458
|
}
|
|
9437
9459
|
}
|
|
9438
|
-
const
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
}
|
|
9460
|
+
const supervisorIsExecuting = supervisorAgent.isProcessing();
|
|
9461
|
+
let currentStreamingBlocks;
|
|
9462
|
+
if (supervisorIsExecuting) {
|
|
9463
|
+
const blocks = supervisorMessageAccumulator.get();
|
|
9464
|
+
if (blocks && blocks.length > 0) {
|
|
9465
|
+
currentStreamingBlocks = blocks;
|
|
9445
9466
|
}
|
|
9446
9467
|
}
|
|
9447
|
-
const supervisorIsExecuting = supervisorAgent.isProcessing();
|
|
9448
9468
|
logger.info(
|
|
9449
9469
|
{
|
|
9450
9470
|
totalSessions: sessions2.length,
|
|
9451
|
-
sessionTypes: sessions2.map((s) => ({
|
|
9452
|
-
id: s.session_id,
|
|
9453
|
-
type: s.session_type
|
|
9454
|
-
})),
|
|
9455
|
-
agentHistoriesCount: Object.keys(agentHistories).length,
|
|
9456
9471
|
availableAgentsCount: availableAgents.length,
|
|
9457
9472
|
workspacesCount: workspaces.length,
|
|
9458
9473
|
supervisorIsExecuting,
|
|
9459
|
-
|
|
9460
|
-
streamingBlocksCount: Object.keys(currentStreamingBlocks).length
|
|
9474
|
+
hasStreamingBlocks: !!currentStreamingBlocks
|
|
9461
9475
|
},
|
|
9462
|
-
"Sync: sending state to client"
|
|
9476
|
+
"Sync: sending state to client (v1.13 - no histories)"
|
|
9463
9477
|
);
|
|
9464
9478
|
const syncStateMessage = JSON.stringify({
|
|
9465
9479
|
type: "sync.state",
|
|
@@ -9467,14 +9481,15 @@ async function bootstrap() {
|
|
|
9467
9481
|
payload: {
|
|
9468
9482
|
sessions: sessions2,
|
|
9469
9483
|
subscriptions: subscriptions2,
|
|
9470
|
-
supervisorHistory,
|
|
9471
|
-
agentHistories,
|
|
9472
9484
|
availableAgents,
|
|
9473
9485
|
hiddenBaseTypes,
|
|
9474
9486
|
workspaces,
|
|
9475
9487
|
supervisorIsExecuting,
|
|
9476
9488
|
executingStates,
|
|
9477
|
-
|
|
9489
|
+
// Only include supervisor streaming blocks for mid-stream join
|
|
9490
|
+
currentStreamingBlocks
|
|
9491
|
+
// Protocol v1.13: No supervisorHistory, agentHistories
|
|
9492
|
+
// Clients use history.request for on-demand loading
|
|
9478
9493
|
}
|
|
9479
9494
|
});
|
|
9480
9495
|
sendToDevice(socket, syncMessage.device_id, syncStateMessage);
|
|
@@ -9569,7 +9584,7 @@ async function bootstrap() {
|
|
|
9569
9584
|
const errorEvent = {
|
|
9570
9585
|
type: "supervisor.transcription",
|
|
9571
9586
|
payload: {
|
|
9572
|
-
|
|
9587
|
+
transcription: "",
|
|
9573
9588
|
error: "Voice transcription not available - STT service not configured",
|
|
9574
9589
|
message_id: messageId,
|
|
9575
9590
|
timestamp: Date.now()
|
|
@@ -9619,7 +9634,7 @@ async function bootstrap() {
|
|
|
9619
9634
|
const transcriptionEvent = {
|
|
9620
9635
|
type: "supervisor.transcription",
|
|
9621
9636
|
payload: {
|
|
9622
|
-
|
|
9637
|
+
transcription: commandText,
|
|
9623
9638
|
language: transcriptionResult.language,
|
|
9624
9639
|
duration: transcriptionResult.duration,
|
|
9625
9640
|
message_id: messageId,
|
|
@@ -9648,7 +9663,7 @@ async function bootstrap() {
|
|
|
9648
9663
|
const errorEvent = {
|
|
9649
9664
|
type: "supervisor.transcription",
|
|
9650
9665
|
payload: {
|
|
9651
|
-
|
|
9666
|
+
transcription: "",
|
|
9652
9667
|
error: error instanceof Error ? error.message : "Transcription failed",
|
|
9653
9668
|
message_id: messageId,
|
|
9654
9669
|
timestamp: Date.now(),
|
|
@@ -9922,45 +9937,29 @@ async function bootstrap() {
|
|
|
9922
9937
|
},
|
|
9923
9938
|
"Client subscribed to agent session"
|
|
9924
9939
|
);
|
|
9925
|
-
const history = chatHistoryService.getAgentHistory(sessionId, 50);
|
|
9926
|
-
const enrichedHistory = await Promise.all(
|
|
9927
|
-
history.map(async (msg) => ({
|
|
9928
|
-
id: msg.id,
|
|
9929
|
-
sequence: msg.sequence,
|
|
9930
|
-
role: msg.role,
|
|
9931
|
-
content: msg.content,
|
|
9932
|
-
content_blocks: await chatHistoryService.enrichBlocksWithAudio(
|
|
9933
|
-
msg.contentBlocks,
|
|
9934
|
-
msg.audioOutputPath,
|
|
9935
|
-
msg.audioInputPath,
|
|
9936
|
-
false
|
|
9937
|
-
// Don't include audio in subscription response
|
|
9938
|
-
),
|
|
9939
|
-
createdAt: msg.createdAt.toISOString()
|
|
9940
|
-
}))
|
|
9941
|
-
);
|
|
9942
9940
|
const isExecuting = agentSessionManager.isExecuting(sessionId);
|
|
9943
9941
|
const currentStreamingBlocks = agentMessageAccumulator.get(sessionId) ?? [];
|
|
9942
|
+
const streamingMessageId = currentStreamingBlocks.length > 0 ? agentStreamingMessageIds.get(sessionId) : void 0;
|
|
9944
9943
|
sendToDevice(
|
|
9945
9944
|
socket,
|
|
9946
9945
|
subscribeMessage.device_id,
|
|
9947
9946
|
JSON.stringify({
|
|
9948
9947
|
type: "session.subscribed",
|
|
9949
9948
|
session_id: sessionId,
|
|
9950
|
-
history: enrichedHistory,
|
|
9951
9949
|
is_executing: isExecuting,
|
|
9952
|
-
current_streaming_blocks: currentStreamingBlocks.length > 0 ? currentStreamingBlocks : void 0
|
|
9950
|
+
current_streaming_blocks: currentStreamingBlocks.length > 0 ? currentStreamingBlocks : void 0,
|
|
9951
|
+
streaming_message_id: streamingMessageId
|
|
9953
9952
|
})
|
|
9954
9953
|
);
|
|
9955
9954
|
logger.debug(
|
|
9956
9955
|
{
|
|
9957
9956
|
deviceId: client.deviceId.value,
|
|
9958
9957
|
sessionId,
|
|
9959
|
-
historyCount: enrichedHistory.length,
|
|
9960
9958
|
isExecuting,
|
|
9961
|
-
streamingBlocksCount: currentStreamingBlocks.length
|
|
9959
|
+
streamingBlocksCount: currentStreamingBlocks.length,
|
|
9960
|
+
streamingMessageId
|
|
9962
9961
|
},
|
|
9963
|
-
"
|
|
9962
|
+
"Agent session subscribed (v1.13 - use history.request for messages)"
|
|
9964
9963
|
);
|
|
9965
9964
|
} else {
|
|
9966
9965
|
const result = subscriptionService.subscribe(
|
|
@@ -10089,7 +10088,7 @@ async function bootstrap() {
|
|
|
10089
10088
|
type: "session.transcription",
|
|
10090
10089
|
session_id: sessionId,
|
|
10091
10090
|
payload: {
|
|
10092
|
-
|
|
10091
|
+
transcription: transcribedText,
|
|
10093
10092
|
language: transcriptionResult.language,
|
|
10094
10093
|
duration: transcriptionResult.duration,
|
|
10095
10094
|
message_id: messageId,
|
|
@@ -10182,7 +10181,7 @@ async function bootstrap() {
|
|
|
10182
10181
|
type: "session.transcription",
|
|
10183
10182
|
session_id: sessionId,
|
|
10184
10183
|
payload: {
|
|
10185
|
-
|
|
10184
|
+
transcription: "",
|
|
10186
10185
|
error: error instanceof Error ? error.message : "Transcription failed",
|
|
10187
10186
|
message_id: messageId,
|
|
10188
10187
|
timestamp: Date.now()
|
|
@@ -10508,18 +10507,22 @@ async function bootstrap() {
|
|
|
10508
10507
|
"history.request": async (socket, message) => {
|
|
10509
10508
|
const historyRequest = message;
|
|
10510
10509
|
const sessionId = historyRequest.payload?.session_id;
|
|
10510
|
+
const beforeSequence = historyRequest.payload?.before_sequence;
|
|
10511
|
+
const limit = historyRequest.payload?.limit;
|
|
10511
10512
|
const isSupervisor = !sessionId;
|
|
10512
10513
|
logger.debug(
|
|
10513
|
-
{ sessionId, isSupervisor, requestId: historyRequest.id },
|
|
10514
|
-
"
|
|
10514
|
+
{ sessionId, isSupervisor, beforeSequence, limit, requestId: historyRequest.id },
|
|
10515
|
+
"Paginated history request received"
|
|
10515
10516
|
);
|
|
10516
10517
|
try {
|
|
10517
10518
|
if (isSupervisor) {
|
|
10518
|
-
const
|
|
10519
|
+
const result = chatHistoryService.getSupervisorHistoryPaginated({
|
|
10520
|
+
beforeSequence,
|
|
10521
|
+
limit
|
|
10522
|
+
});
|
|
10519
10523
|
const supervisorHistory = await Promise.all(
|
|
10520
|
-
|
|
10524
|
+
result.messages.map(async (msg) => ({
|
|
10521
10525
|
message_id: msg.id,
|
|
10522
|
-
// Include message ID for audio.request
|
|
10523
10526
|
sequence: msg.sequence,
|
|
10524
10527
|
role: msg.role,
|
|
10525
10528
|
content: msg.content,
|
|
@@ -10528,32 +10531,54 @@ async function bootstrap() {
|
|
|
10528
10531
|
msg.audioOutputPath,
|
|
10529
10532
|
msg.audioInputPath,
|
|
10530
10533
|
false
|
|
10531
|
-
// Don't include audio in history response
|
|
10532
10534
|
),
|
|
10533
10535
|
createdAt: msg.createdAt.toISOString()
|
|
10534
10536
|
}))
|
|
10535
10537
|
);
|
|
10536
10538
|
const isExecuting = supervisorAgent.isProcessing();
|
|
10539
|
+
let currentStreamingBlocks;
|
|
10540
|
+
let streamingMessageId;
|
|
10541
|
+
if (isExecuting && !beforeSequence) {
|
|
10542
|
+
const blocks = supervisorMessageAccumulator.get();
|
|
10543
|
+
if (blocks && blocks.length > 0) {
|
|
10544
|
+
currentStreamingBlocks = blocks;
|
|
10545
|
+
streamingMessageId = supervisorMessageAccumulator.getStreamingMessageId() ?? void 0;
|
|
10546
|
+
}
|
|
10547
|
+
}
|
|
10537
10548
|
socket.send(
|
|
10538
10549
|
JSON.stringify({
|
|
10539
10550
|
type: "history.response",
|
|
10540
10551
|
id: historyRequest.id,
|
|
10541
10552
|
payload: {
|
|
10542
10553
|
session_id: null,
|
|
10543
|
-
// Indicates supervisor
|
|
10544
10554
|
history: supervisorHistory,
|
|
10545
|
-
|
|
10555
|
+
has_more: result.hasMore,
|
|
10556
|
+
oldest_sequence: result.oldestSequence,
|
|
10557
|
+
newest_sequence: result.newestSequence,
|
|
10558
|
+
is_executing: isExecuting,
|
|
10559
|
+
current_streaming_blocks: currentStreamingBlocks,
|
|
10560
|
+
streaming_message_id: streamingMessageId
|
|
10546
10561
|
}
|
|
10547
10562
|
})
|
|
10548
10563
|
);
|
|
10549
10564
|
logger.debug(
|
|
10550
|
-
{
|
|
10551
|
-
|
|
10565
|
+
{
|
|
10566
|
+
messageCount: supervisorHistory.length,
|
|
10567
|
+
hasMore: result.hasMore,
|
|
10568
|
+
oldestSeq: result.oldestSequence,
|
|
10569
|
+
newestSeq: result.newestSequence,
|
|
10570
|
+
isExecuting
|
|
10571
|
+
},
|
|
10572
|
+
"Paginated supervisor history sent"
|
|
10552
10573
|
);
|
|
10553
10574
|
} else {
|
|
10554
|
-
const
|
|
10575
|
+
const result = chatHistoryService.getAgentHistoryPaginated(sessionId, {
|
|
10576
|
+
beforeSequence,
|
|
10577
|
+
limit
|
|
10578
|
+
});
|
|
10555
10579
|
const enrichedHistory = await Promise.all(
|
|
10556
|
-
|
|
10580
|
+
result.messages.map(async (msg) => ({
|
|
10581
|
+
message_id: msg.id,
|
|
10557
10582
|
sequence: msg.sequence,
|
|
10558
10583
|
role: msg.role,
|
|
10559
10584
|
content: msg.content,
|
|
@@ -10562,17 +10587,18 @@ async function bootstrap() {
|
|
|
10562
10587
|
msg.audioOutputPath,
|
|
10563
10588
|
msg.audioInputPath,
|
|
10564
10589
|
false
|
|
10565
|
-
// Don't include audio in history response
|
|
10566
10590
|
),
|
|
10567
10591
|
createdAt: msg.createdAt.toISOString()
|
|
10568
10592
|
}))
|
|
10569
10593
|
);
|
|
10570
10594
|
const isExecuting = agentSessionManager.isExecuting(sessionId);
|
|
10571
10595
|
let currentStreamingBlocks;
|
|
10572
|
-
|
|
10596
|
+
let streamingMessageId;
|
|
10597
|
+
if (isExecuting && !beforeSequence) {
|
|
10573
10598
|
const blocks = agentMessageAccumulator.get(sessionId);
|
|
10574
10599
|
if (blocks && blocks.length > 0) {
|
|
10575
10600
|
currentStreamingBlocks = blocks;
|
|
10601
|
+
streamingMessageId = agentStreamingMessageIds.get(sessionId);
|
|
10576
10602
|
}
|
|
10577
10603
|
}
|
|
10578
10604
|
socket.send(
|
|
@@ -10582,13 +10608,17 @@ async function bootstrap() {
|
|
|
10582
10608
|
payload: {
|
|
10583
10609
|
session_id: sessionId,
|
|
10584
10610
|
history: enrichedHistory,
|
|
10611
|
+
has_more: result.hasMore,
|
|
10612
|
+
oldest_sequence: result.oldestSequence,
|
|
10613
|
+
newest_sequence: result.newestSequence,
|
|
10585
10614
|
is_executing: isExecuting,
|
|
10586
|
-
current_streaming_blocks: currentStreamingBlocks
|
|
10615
|
+
current_streaming_blocks: currentStreamingBlocks,
|
|
10616
|
+
streaming_message_id: streamingMessageId
|
|
10587
10617
|
}
|
|
10588
10618
|
})
|
|
10589
10619
|
);
|
|
10590
10620
|
logger.debug(
|
|
10591
|
-
{ sessionId, messageCount: enrichedHistory.length, isExecuting },
|
|
10621
|
+
{ sessionId, messageCount: enrichedHistory.length, isExecuting, streamingMessageId },
|
|
10592
10622
|
"Agent session history sent"
|
|
10593
10623
|
);
|
|
10594
10624
|
}
|
|
@@ -10622,7 +10652,7 @@ async function bootstrap() {
|
|
|
10622
10652
|
id: audioRequest.id,
|
|
10623
10653
|
payload: {
|
|
10624
10654
|
message_id,
|
|
10625
|
-
|
|
10655
|
+
audio_base64: audioBase64
|
|
10626
10656
|
}
|
|
10627
10657
|
})
|
|
10628
10658
|
);
|
|
@@ -10780,6 +10810,18 @@ async function bootstrap() {
|
|
|
10780
10810
|
});
|
|
10781
10811
|
const broadcaster = messageBroadcaster;
|
|
10782
10812
|
const agentMessageAccumulator = /* @__PURE__ */ new Map();
|
|
10813
|
+
const agentStreamingMessageIds = /* @__PURE__ */ new Map();
|
|
10814
|
+
const getOrCreateAgentStreamingMessageId = (sessionId) => {
|
|
10815
|
+
let messageId = agentStreamingMessageIds.get(sessionId);
|
|
10816
|
+
if (!messageId) {
|
|
10817
|
+
messageId = randomUUID5();
|
|
10818
|
+
agentStreamingMessageIds.set(sessionId, messageId);
|
|
10819
|
+
}
|
|
10820
|
+
return messageId;
|
|
10821
|
+
};
|
|
10822
|
+
const clearAgentStreamingMessageId = (sessionId) => {
|
|
10823
|
+
agentStreamingMessageIds.delete(sessionId);
|
|
10824
|
+
};
|
|
10783
10825
|
agentSessionManager.on(
|
|
10784
10826
|
"blocks",
|
|
10785
10827
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
@@ -10852,9 +10894,11 @@ async function bootstrap() {
|
|
|
10852
10894
|
const accumulatedBlocks = agentMessageAccumulator.get(sessionId) ?? [];
|
|
10853
10895
|
const mergedBlocks = mergeToolBlocks(accumulatedBlocks);
|
|
10854
10896
|
const fullAccumulatedText = mergedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
|
|
10897
|
+
const streamingMessageId = getOrCreateAgentStreamingMessageId(sessionId);
|
|
10855
10898
|
const outputEvent = {
|
|
10856
10899
|
type: "session.output",
|
|
10857
10900
|
session_id: sessionId,
|
|
10901
|
+
streaming_message_id: streamingMessageId,
|
|
10858
10902
|
payload: {
|
|
10859
10903
|
content_type: "agent",
|
|
10860
10904
|
content: fullAccumulatedText,
|
|
@@ -10869,6 +10913,9 @@ async function bootstrap() {
|
|
|
10869
10913
|
sessionId,
|
|
10870
10914
|
JSON.stringify(outputEvent)
|
|
10871
10915
|
);
|
|
10916
|
+
if (isComplete) {
|
|
10917
|
+
clearAgentStreamingMessageId(sessionId);
|
|
10918
|
+
}
|
|
10872
10919
|
if (isComplete && fullTextContent.length > 0) {
|
|
10873
10920
|
const pendingVoiceCommand = pendingAgentVoiceCommands.get(sessionId);
|
|
10874
10921
|
if (pendingVoiceCommand && ttsService) {
|
|
@@ -10918,7 +10965,7 @@ async function bootstrap() {
|
|
|
10918
10965
|
session_id: sessionId,
|
|
10919
10966
|
payload: {
|
|
10920
10967
|
text: textForTTS,
|
|
10921
|
-
|
|
10968
|
+
audio_base64: audioBase64,
|
|
10922
10969
|
audio_format: "mp3",
|
|
10923
10970
|
duration: ttsResult.duration,
|
|
10924
10971
|
message_id: pendingMessageId,
|
|
@@ -10944,7 +10991,6 @@ async function bootstrap() {
|
|
|
10944
10991
|
}
|
|
10945
10992
|
}
|
|
10946
10993
|
);
|
|
10947
|
-
let supervisorBlockAccumulator = [];
|
|
10948
10994
|
supervisorAgent.on(
|
|
10949
10995
|
"blocks",
|
|
10950
10996
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
@@ -10959,17 +11005,19 @@ async function bootstrap() {
|
|
|
10959
11005
|
{ deviceId, blockCount: blocks.length, isComplete },
|
|
10960
11006
|
"Ignoring supervisor blocks - execution was cancelled"
|
|
10961
11007
|
);
|
|
10962
|
-
|
|
11008
|
+
supervisorMessageAccumulator.clear();
|
|
10963
11009
|
return;
|
|
10964
11010
|
}
|
|
10965
11011
|
const persistableBlocks = blocks.filter((b) => b.block_type !== "status");
|
|
10966
11012
|
if (persistableBlocks.length > 0) {
|
|
10967
|
-
|
|
11013
|
+
supervisorMessageAccumulator.accumulate(persistableBlocks);
|
|
10968
11014
|
}
|
|
10969
|
-
const mergedBlocks = mergeToolBlocks(
|
|
11015
|
+
const mergedBlocks = mergeToolBlocks(supervisorMessageAccumulator.get());
|
|
10970
11016
|
const textContent = mergedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
|
|
11017
|
+
const streamingMessageId = supervisorMessageAccumulator.getStreamingMessageId();
|
|
10971
11018
|
const outputEvent = {
|
|
10972
11019
|
type: "supervisor.output",
|
|
11020
|
+
streaming_message_id: streamingMessageId,
|
|
10973
11021
|
payload: {
|
|
10974
11022
|
content_type: "supervisor",
|
|
10975
11023
|
content: textContent,
|
|
@@ -10981,7 +11029,7 @@ async function bootstrap() {
|
|
|
10981
11029
|
const message = JSON.stringify(outputEvent);
|
|
10982
11030
|
broadcaster.broadcastToAll(message);
|
|
10983
11031
|
if (isComplete) {
|
|
10984
|
-
|
|
11032
|
+
supervisorMessageAccumulator.clear();
|
|
10985
11033
|
}
|
|
10986
11034
|
if (isComplete && finalOutput && finalOutput.length > 0) {
|
|
10987
11035
|
chatHistoryService.saveSupervisorMessage(
|
|
@@ -11031,7 +11079,7 @@ async function bootstrap() {
|
|
|
11031
11079
|
type: "supervisor.voice_output",
|
|
11032
11080
|
payload: {
|
|
11033
11081
|
text: textForTTS,
|
|
11034
|
-
|
|
11082
|
+
audio_base64: audioBase64,
|
|
11035
11083
|
audio_format: "mp3",
|
|
11036
11084
|
duration: ttsResult.duration,
|
|
11037
11085
|
message_id: pendingMessageId,
|
|
@@ -11191,23 +11239,52 @@ async function bootstrap() {
|
|
|
11191
11239
|
} catch (error) {
|
|
11192
11240
|
logger.error({ error }, "Failed to connect to tunnel (will retry)");
|
|
11193
11241
|
}
|
|
11242
|
+
const SHUTDOWN_TIMEOUT_MS = 1e4;
|
|
11243
|
+
let isShuttingDown = false;
|
|
11194
11244
|
const shutdown = async (signal) => {
|
|
11245
|
+
if (isShuttingDown) {
|
|
11246
|
+
logger.warn({ signal }, "Shutdown already in progress, forcing exit");
|
|
11247
|
+
process.exit(1);
|
|
11248
|
+
}
|
|
11249
|
+
isShuttingDown = true;
|
|
11195
11250
|
logger.info({ signal }, "Shutdown signal received");
|
|
11251
|
+
const forceExitTimeout = setTimeout(() => {
|
|
11252
|
+
logger.error("Graceful shutdown timeout exceeded, forcing exit");
|
|
11253
|
+
process.exit(1);
|
|
11254
|
+
}, SHUTDOWN_TIMEOUT_MS);
|
|
11196
11255
|
try {
|
|
11197
11256
|
logger.info("Disconnecting from tunnel...");
|
|
11198
11257
|
tunnelClient.disconnect();
|
|
11199
11258
|
logger.info("Cleaning up agent sessions...");
|
|
11200
11259
|
agentSessionManager.cleanup();
|
|
11201
11260
|
logger.info("Terminating all sessions...");
|
|
11202
|
-
|
|
11203
|
-
|
|
11261
|
+
const terminatePromise = sessionManager.terminateAll();
|
|
11262
|
+
const sessionTimeoutPromise = new Promise((_, reject) => {
|
|
11263
|
+
setTimeout(() => reject(new Error("Session termination timeout")), 5e3);
|
|
11264
|
+
});
|
|
11265
|
+
try {
|
|
11266
|
+
await Promise.race([terminatePromise, sessionTimeoutPromise]);
|
|
11267
|
+
logger.info("All sessions terminated");
|
|
11268
|
+
} catch (error) {
|
|
11269
|
+
logger.warn({ error }, "Session termination timed out, continuing shutdown");
|
|
11270
|
+
}
|
|
11204
11271
|
logger.info("Closing HTTP server...");
|
|
11205
|
-
|
|
11272
|
+
const closePromise = app.close();
|
|
11273
|
+
const closeTimeoutPromise = new Promise((_, reject) => {
|
|
11274
|
+
setTimeout(() => reject(new Error("HTTP server close timeout")), 2e3);
|
|
11275
|
+
});
|
|
11276
|
+
try {
|
|
11277
|
+
await Promise.race([closePromise, closeTimeoutPromise]);
|
|
11278
|
+
} catch (error) {
|
|
11279
|
+
logger.warn({ error }, "HTTP server close timed out, continuing shutdown");
|
|
11280
|
+
}
|
|
11206
11281
|
logger.info("Closing database...");
|
|
11207
11282
|
closeDatabase();
|
|
11283
|
+
clearTimeout(forceExitTimeout);
|
|
11208
11284
|
logger.info("Shutdown complete");
|
|
11209
11285
|
process.exit(0);
|
|
11210
11286
|
} catch (error) {
|
|
11287
|
+
clearTimeout(forceExitTimeout);
|
|
11211
11288
|
logger.error({ error }, "Error during shutdown");
|
|
11212
11289
|
process.exit(1);
|
|
11213
11290
|
}
|
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.12",
|
|
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",
|