@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.
Files changed (2) hide show
  1. package/dist/main.js +286 -209
  2. 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
- // If omitted, returns supervisor history
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 count = this.deps.sessionManager.countByType("terminal");
4649
- if (count >= SESSION_CONFIG.MAX_TERMINAL_SESSIONS) {
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
- * Gets messages after a specific timestamp.
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
- * Clears supervisor chat history (global).
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
- * Clears agent session chat history.
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
- await session.terminate();
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: session.id.value, error },
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: sessions2.length }, "All sessions terminated");
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 parsedMessage = parseClientMessage(data);
8981
- if (!parsedMessage) {
8982
- logger.warn({ type: messageType }, "Failed to parse tunnel message");
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, isLightweight },
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
- const supervisorHistory = await Promise.all(
9356
- supervisorHistoryRaw.map(async (msg) => ({
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 currentStreamingBlocks = {};
9439
- for (const [sessionId, isExecuting] of Object.entries(executingStates)) {
9440
- if (isExecuting) {
9441
- const blocks = agentMessageAccumulator.get(sessionId);
9442
- if (blocks && blocks.length > 0) {
9443
- currentStreamingBlocks[sessionId] = blocks;
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
- executingStates,
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
- currentStreamingBlocks: Object.keys(currentStreamingBlocks).length > 0 ? currentStreamingBlocks : void 0
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
- text: "",
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
- text: commandText,
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
- text: "",
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
- "Sent agent session history on subscribe"
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
- text: transcribedText,
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
- text: "",
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
- "History request received"
10514
+ { sessionId, isSupervisor, beforeSequence, limit, requestId: historyRequest.id },
10515
+ "Paginated history request received"
10515
10516
  );
10516
10517
  try {
10517
10518
  if (isSupervisor) {
10518
- const supervisorHistoryRaw = chatHistoryService.getSupervisorHistory();
10519
+ const result = chatHistoryService.getSupervisorHistoryPaginated({
10520
+ beforeSequence,
10521
+ limit
10522
+ });
10519
10523
  const supervisorHistory = await Promise.all(
10520
- supervisorHistoryRaw.map(async (msg) => ({
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
- is_executing: isExecuting
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
- { messageCount: supervisorHistory.length, isExecuting },
10551
- "Supervisor history sent"
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 history = chatHistoryService.getAgentHistory(sessionId);
10575
+ const result = chatHistoryService.getAgentHistoryPaginated(sessionId, {
10576
+ beforeSequence,
10577
+ limit
10578
+ });
10555
10579
  const enrichedHistory = await Promise.all(
10556
- history.map(async (msg) => ({
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
- if (isExecuting) {
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
- audio: audioBase64
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
- audio: audioBase64,
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
- supervisorBlockAccumulator = [];
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
- accumulateBlocks(supervisorBlockAccumulator, persistableBlocks);
11013
+ supervisorMessageAccumulator.accumulate(persistableBlocks);
10968
11014
  }
10969
- const mergedBlocks = mergeToolBlocks(supervisorBlockAccumulator);
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
- supervisorBlockAccumulator = [];
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
- audio: audioBase64,
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
- await sessionManager.terminateAll();
11203
- logger.info("All sessions terminated");
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
- await app.close();
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.11",
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",