@tiflis-io/tiflis-code-workstation 0.3.11 → 0.3.13

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 +400 -233
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -127,7 +127,11 @@ var EnvSchema = z.object({
127
127
  // Terminal Configuration
128
128
  // ─────────────────────────────────────────────────────────────
129
129
  /** Terminal output buffer size (number of messages, in-memory only, does not survive restarts) */
130
- TERMINAL_OUTPUT_BUFFER_SIZE: z.coerce.number().default(100),
130
+ TERMINAL_OUTPUT_BUFFER_SIZE: z.coerce.number().default(1e4),
131
+ /** Terminal output batch interval in milliseconds (how long to wait before flushing) */
132
+ TERMINAL_BATCH_INTERVAL_MS: z.coerce.number().default(64),
133
+ /** Terminal output batch max size in bytes (flush immediately when exceeded) */
134
+ TERMINAL_BATCH_MAX_SIZE: z.coerce.number().default(4096),
131
135
  // Legacy (fallback for STT/TTS if specific keys not set)
132
136
  OPENAI_API_KEY: z.string().optional(),
133
137
  // ─────────────────────────────────────────────────────────────
@@ -283,7 +287,7 @@ var SESSION_CONFIG = {
283
287
  /** Data retention period for terminated sessions (30 days in ms) */
284
288
  DATA_RETENTION_MS: 30 * 24 * 60 * 60 * 1e3,
285
289
  /** Default terminal output buffer size (number of messages) */
286
- DEFAULT_TERMINAL_OUTPUT_BUFFER_SIZE: 100
290
+ DEFAULT_TERMINAL_OUTPUT_BUFFER_SIZE: 1e4
287
291
  };
288
292
  var AGENT_COMMANDS = {
289
293
  cursor: {
@@ -1149,8 +1153,9 @@ var SyncMessageSchema = z2.object({
1149
1153
  // If true, excludes message histories (for watchOS)
1150
1154
  });
1151
1155
  var HistoryRequestPayloadSchema = z2.object({
1152
- session_id: z2.string().optional()
1153
- // If omitted, returns supervisor history
1156
+ session_id: z2.string().nullable().optional(),
1157
+ before_sequence: z2.number().int().optional(),
1158
+ limit: z2.number().int().min(1).max(50).optional()
1154
1159
  });
1155
1160
  var HistoryRequestSchema = z2.object({
1156
1161
  type: z2.literal("history.request"),
@@ -1193,7 +1198,7 @@ var TerminateSessionSchema = z2.object({
1193
1198
  var SupervisorCommandPayloadSchema = z2.object({
1194
1199
  command: z2.string().optional(),
1195
1200
  audio: z2.string().optional(),
1196
- audio_format: z2.enum(["m4a", "wav", "mp3"]).optional(),
1201
+ audio_format: z2.enum(["m4a", "wav", "mp3", "webm", "opus"]).optional(),
1197
1202
  message_id: z2.string().optional(),
1198
1203
  language: z2.string().optional()
1199
1204
  }).refine(
@@ -1237,7 +1242,7 @@ var SessionExecutePayloadSchema = z2.object({
1237
1242
  text: z2.string().optional(),
1238
1243
  // Alias for content (backward compat)
1239
1244
  audio: z2.string().optional(),
1240
- audio_format: z2.enum(["m4a", "wav", "mp3"]).optional(),
1245
+ audio_format: z2.enum(["m4a", "wav", "mp3", "webm", "opus"]).optional(),
1241
1246
  message_id: z2.string().optional(),
1242
1247
  // For linking transcription back to voice message
1243
1248
  language: z2.string().optional(),
@@ -1370,6 +1375,13 @@ function parseClientMessage(data) {
1370
1375
  }
1371
1376
  return void 0;
1372
1377
  }
1378
+ function parseClientMessageWithErrors(data) {
1379
+ const result = IncomingClientMessageSchema.safeParse(data);
1380
+ if (result.success) {
1381
+ return { success: true, data: result.data };
1382
+ }
1383
+ return { success: false, errors: result.error.issues };
1384
+ }
1373
1385
  function parseTunnelMessage(data) {
1374
1386
  const result = IncomingTunnelMessageSchema.safeParse(data);
1375
1387
  if (result.success) {
@@ -2580,15 +2592,12 @@ var TerminalSession = class extends Session {
2580
2592
  return this._masterDeviceId;
2581
2593
  }
2582
2594
  /**
2583
- * Sets the master device ID. Only sets if not already set (first subscriber wins).
2584
- * @returns true if this device became master, false if master was already set
2595
+ * Sets the master device ID. Always overrides the current master (new subscriber wins).
2596
+ * @returns true (new subscriber always becomes master)
2585
2597
  */
2586
2598
  setMaster(deviceId) {
2587
- if (this._masterDeviceId === null) {
2588
- this._masterDeviceId = deviceId;
2589
- return true;
2590
- }
2591
- return this._masterDeviceId === deviceId;
2599
+ this._masterDeviceId = deviceId;
2600
+ return true;
2592
2601
  }
2593
2602
  /**
2594
2603
  * Checks if the given device is the master for this session.
@@ -2909,6 +2918,75 @@ var PtyManager = class {
2909
2918
  }
2910
2919
  };
2911
2920
 
2921
+ // src/infrastructure/terminal/terminal-output-batcher.ts
2922
+ var TerminalOutputBatcher = class {
2923
+ buffer = "";
2924
+ timeout = null;
2925
+ lastActivityTime = Date.now();
2926
+ outputRate = 0;
2927
+ // Bytes/second estimate (exponential moving average)
2928
+ batchIntervalMs;
2929
+ maxBatchSize;
2930
+ onFlush;
2931
+ constructor(config2) {
2932
+ this.batchIntervalMs = config2.batchIntervalMs;
2933
+ this.maxBatchSize = config2.maxBatchSize;
2934
+ this.onFlush = config2.onFlush;
2935
+ }
2936
+ /**
2937
+ * Appends data to the batch buffer.
2938
+ * Triggers flush based on size threshold or adaptive timeout.
2939
+ */
2940
+ append(chunk) {
2941
+ this.buffer += chunk;
2942
+ const now = Date.now();
2943
+ const elapsed = Math.max(now - this.lastActivityTime, 1);
2944
+ const instantRate = chunk.length / elapsed * 1e3;
2945
+ this.outputRate = this.outputRate * 0.7 + instantRate * 0.3;
2946
+ this.lastActivityTime = now;
2947
+ if (this.buffer.length >= this.maxBatchSize) {
2948
+ this.flush();
2949
+ return;
2950
+ }
2951
+ if (this.timeout === null) {
2952
+ const adaptiveInterval = this.outputRate > 1e3 ? this.batchIntervalMs : Math.min(8, this.batchIntervalMs);
2953
+ this.timeout = setTimeout(() => this.flush(), adaptiveInterval);
2954
+ }
2955
+ }
2956
+ /**
2957
+ * Immediately flushes the buffer, invoking the onFlush callback.
2958
+ */
2959
+ flush() {
2960
+ if (this.timeout !== null) {
2961
+ clearTimeout(this.timeout);
2962
+ this.timeout = null;
2963
+ }
2964
+ if (this.buffer.length > 0) {
2965
+ const data = this.buffer;
2966
+ this.buffer = "";
2967
+ this.onFlush(data);
2968
+ }
2969
+ }
2970
+ /**
2971
+ * Disposes the batcher, flushing any pending data.
2972
+ */
2973
+ dispose() {
2974
+ this.flush();
2975
+ }
2976
+ /**
2977
+ * Returns current buffer size (for debugging/monitoring).
2978
+ */
2979
+ get pendingSize() {
2980
+ return this.buffer.length;
2981
+ }
2982
+ /**
2983
+ * Returns estimated output rate in bytes/second.
2984
+ */
2985
+ get currentOutputRate() {
2986
+ return this.outputRate;
2987
+ }
2988
+ };
2989
+
2912
2990
  // src/infrastructure/agents/agent-session-manager.ts
2913
2991
  import { EventEmitter as EventEmitter2 } from "events";
2914
2992
  import { randomUUID as randomUUID2 } from "crypto";
@@ -4645,8 +4723,8 @@ var CreateSessionUseCase = class {
4645
4723
  */
4646
4724
  checkSessionLimits(sessionType) {
4647
4725
  if (sessionType === "terminal") {
4648
- const count = this.deps.sessionManager.countByType("terminal");
4649
- if (count >= SESSION_CONFIG.MAX_TERMINAL_SESSIONS) {
4726
+ const count2 = this.deps.sessionManager.countByType("terminal");
4727
+ if (count2 >= SESSION_CONFIG.MAX_TERMINAL_SESSIONS) {
4650
4728
  throw new SessionLimitReachedError("terminal", SESSION_CONFIG.MAX_TERMINAL_SESSIONS);
4651
4729
  }
4652
4730
  } else if (sessionType !== "supervisor") {
@@ -5084,7 +5162,7 @@ var MessageBroadcasterImpl = class {
5084
5162
  };
5085
5163
 
5086
5164
  // src/infrastructure/persistence/repositories/message-repository.ts
5087
- import { eq as eq3, desc, gt, and as and2, max } from "drizzle-orm";
5165
+ import { eq as eq3, desc, gt, lt, and as and2, max, count } from "drizzle-orm";
5088
5166
  import { nanoid as nanoid2 } from "nanoid";
5089
5167
  var MessageRepository = class {
5090
5168
  /**
@@ -5110,17 +5188,24 @@ var MessageRepository = class {
5110
5188
  db2.insert(messages).values(newMessage).run();
5111
5189
  return { ...newMessage, createdAt: newMessage.createdAt };
5112
5190
  }
5113
- /**
5114
- * Gets messages for a session with pagination.
5115
- * Returns messages ordered by sequence descending (newest first).
5116
- */
5117
5191
  getBySession(sessionId, limit = 100) {
5118
5192
  const db2 = getDatabase();
5119
5193
  return db2.select().from(messages).where(eq3(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all();
5120
5194
  }
5121
- /**
5122
- * Gets messages after a specific timestamp.
5123
- */
5195
+ getBySessionPaginated(sessionId, options = {}) {
5196
+ const db2 = getDatabase();
5197
+ const limit = Math.min(options.limit ?? 20, 50);
5198
+ const totalResult = db2.select({ count: count() }).from(messages).where(eq3(messages.sessionId, sessionId)).get();
5199
+ const totalCount = totalResult?.count ?? 0;
5200
+ const whereClause = options.beforeSequence ? and2(
5201
+ eq3(messages.sessionId, sessionId),
5202
+ lt(messages.sequence, options.beforeSequence)
5203
+ ) : eq3(messages.sessionId, sessionId);
5204
+ const rows = db2.select().from(messages).where(whereClause).orderBy(desc(messages.sequence)).limit(limit + 1).all();
5205
+ const hasMore = rows.length > limit;
5206
+ const resultMessages = hasMore ? rows.slice(0, limit) : rows;
5207
+ return { messages: resultMessages, hasMore, totalCount };
5208
+ }
5124
5209
  getAfterTimestamp(sessionId, timestamp, limit = 100) {
5125
5210
  const db2 = getDatabase();
5126
5211
  return db2.select().from(messages).where(and2(eq3(messages.sessionId, sessionId), gt(messages.createdAt, timestamp))).orderBy(messages.createdAt).limit(limit).all();
@@ -5810,9 +5895,40 @@ var ChatHistoryService = class _ChatHistoryService {
5810
5895
  };
5811
5896
  });
5812
5897
  }
5813
- /**
5814
- * Clears supervisor chat history (global).
5815
- */
5898
+ getSupervisorHistoryPaginated(options = {}) {
5899
+ const sessionId = _ChatHistoryService.SUPERVISOR_SESSION_ID;
5900
+ const result = this.messageRepo.getBySessionPaginated(sessionId, options);
5901
+ const messages2 = result.messages.reverse().map((row) => {
5902
+ let contentBlocks;
5903
+ if (row.contentBlocks) {
5904
+ try {
5905
+ contentBlocks = JSON.parse(row.contentBlocks);
5906
+ } catch {
5907
+ }
5908
+ }
5909
+ return {
5910
+ id: row.id,
5911
+ sessionId: row.sessionId,
5912
+ sequence: row.sequence,
5913
+ role: row.role,
5914
+ contentType: row.contentType,
5915
+ content: row.content,
5916
+ contentBlocks,
5917
+ audioInputPath: row.audioInputPath,
5918
+ audioOutputPath: row.audioOutputPath,
5919
+ isComplete: row.isComplete ?? false,
5920
+ createdAt: row.createdAt
5921
+ };
5922
+ });
5923
+ const firstMsg = messages2[0];
5924
+ const lastMsg = messages2[messages2.length - 1];
5925
+ return {
5926
+ messages: messages2,
5927
+ hasMore: result.hasMore,
5928
+ oldestSequence: firstMsg?.sequence,
5929
+ newestSequence: lastMsg?.sequence
5930
+ };
5931
+ }
5816
5932
  clearSupervisorHistory() {
5817
5933
  const sessionId = _ChatHistoryService.SUPERVISOR_SESSION_ID;
5818
5934
  this.messageRepo.deleteBySession(sessionId);
@@ -5895,9 +6011,43 @@ var ChatHistoryService = class _ChatHistoryService {
5895
6011
  };
5896
6012
  });
5897
6013
  }
5898
- /**
5899
- * Clears agent session chat history.
5900
- */
6014
+ getAgentHistoryPaginated(sessionId, options = {}) {
6015
+ const result = this.messageRepo.getBySessionPaginated(sessionId, options);
6016
+ const storedMessages = result.messages.reverse().map((row) => {
6017
+ let contentBlocks;
6018
+ if (row.contentBlocks) {
6019
+ try {
6020
+ contentBlocks = JSON.parse(row.contentBlocks);
6021
+ } catch (error) {
6022
+ this.logger.error(
6023
+ { sessionId, messageId: row.id, error },
6024
+ "Failed to parse contentBlocks JSON"
6025
+ );
6026
+ }
6027
+ }
6028
+ return {
6029
+ id: row.id,
6030
+ sessionId: row.sessionId,
6031
+ sequence: row.sequence,
6032
+ role: row.role,
6033
+ contentType: row.contentType,
6034
+ content: row.content,
6035
+ contentBlocks,
6036
+ audioInputPath: row.audioInputPath,
6037
+ audioOutputPath: row.audioOutputPath,
6038
+ isComplete: row.isComplete ?? false,
6039
+ createdAt: row.createdAt
6040
+ };
6041
+ });
6042
+ const firstMsg = storedMessages[0];
6043
+ const lastMsg = storedMessages[storedMessages.length - 1];
6044
+ return {
6045
+ messages: storedMessages,
6046
+ hasMore: result.hasMore,
6047
+ oldestSequence: firstMsg?.sequence,
6048
+ newestSequence: lastMsg?.sequence
6049
+ };
6050
+ }
5901
6051
  clearAgentHistory(sessionId) {
5902
6052
  this.messageRepo.deleteBySession(sessionId);
5903
6053
  this.logger.info({ sessionId }, "Agent session history cleared");
@@ -6431,18 +6581,35 @@ var InMemorySessionManager = class extends EventEmitter3 {
6431
6581
  this.logger.info({ sessionId: sessionId.value }, "Session terminated");
6432
6582
  }
6433
6583
  /**
6434
- * Terminates all sessions.
6584
+ * Terminates all sessions with individual timeouts.
6585
+ * Each session has a 3-second timeout to prevent hanging.
6435
6586
  */
6436
6587
  async terminateAll() {
6437
6588
  this.agentSessionManager.cleanup();
6438
6589
  const sessions2 = Array.from(this.sessions.values());
6590
+ const sessionCount = sessions2.length;
6591
+ if (sessionCount === 0) {
6592
+ this.logger.info("No sessions to terminate");
6593
+ return;
6594
+ }
6595
+ this.logger.info({ count: sessionCount }, "Terminating all sessions...");
6596
+ const INDIVIDUAL_TIMEOUT_MS = 3e3;
6439
6597
  await Promise.all(
6440
6598
  sessions2.map(async (session) => {
6599
+ const sessionId = session.id.value;
6441
6600
  try {
6442
- await session.terminate();
6601
+ const terminatePromise = session.terminate();
6602
+ const timeoutPromise = new Promise((resolve2) => {
6603
+ setTimeout(() => {
6604
+ this.logger.warn({ sessionId }, "Session termination timed out, skipping");
6605
+ resolve2();
6606
+ }, INDIVIDUAL_TIMEOUT_MS);
6607
+ });
6608
+ await Promise.race([terminatePromise, timeoutPromise]);
6609
+ this.logger.debug({ sessionId }, "Session terminated");
6443
6610
  } catch (error) {
6444
6611
  this.logger.error(
6445
- { sessionId: session.id.value, error },
6612
+ { sessionId, error },
6446
6613
  "Error terminating session"
6447
6614
  );
6448
6615
  }
@@ -6450,7 +6617,7 @@ var InMemorySessionManager = class extends EventEmitter3 {
6450
6617
  );
6451
6618
  this.sessions.clear();
6452
6619
  this.supervisorSession = null;
6453
- this.logger.info({ count: sessions2.length }, "All sessions terminated");
6620
+ this.logger.info({ count: sessionCount }, "All sessions terminated");
6454
6621
  }
6455
6622
  /**
6456
6623
  * Gets the count of active sessions by type.
@@ -8977,11 +9144,19 @@ function handleTunnelMessage(rawMessage, tunnelClient, messageBroadcaster, creat
8977
9144
  return;
8978
9145
  }
8979
9146
  const handler = handlers[messageType];
8980
- const parsedMessage = parseClientMessage(data);
8981
- if (!parsedMessage) {
8982
- logger.warn({ type: messageType }, "Failed to parse tunnel message");
9147
+ const parseResult = parseClientMessageWithErrors(data);
9148
+ if (!parseResult.success) {
9149
+ logger.warn(
9150
+ {
9151
+ type: messageType,
9152
+ errors: parseResult.errors,
9153
+ rawMessage: JSON.stringify(data).slice(0, 500)
9154
+ },
9155
+ "Failed to parse tunnel message - Zod validation failed"
9156
+ );
8983
9157
  return;
8984
9158
  }
9159
+ const parsedMessage = parseResult.data;
8985
9160
  handler(virtualSocket, parsedMessage).catch((error) => {
8986
9161
  logger.error(
8987
9162
  { error, type: messageType },
@@ -9169,6 +9344,29 @@ async function bootstrap() {
9169
9344
  const pendingSupervisorVoiceCommands = /* @__PURE__ */ new Map();
9170
9345
  const pendingAgentVoiceCommands = /* @__PURE__ */ new Map();
9171
9346
  const cancelledDuringTranscription = /* @__PURE__ */ new Set();
9347
+ const supervisorMessageAccumulator = {
9348
+ blocks: [],
9349
+ streamingMessageId: null,
9350
+ get() {
9351
+ return this.blocks;
9352
+ },
9353
+ getStreamingMessageId() {
9354
+ return this.streamingMessageId;
9355
+ },
9356
+ set(blocks) {
9357
+ this.blocks = blocks;
9358
+ },
9359
+ clear() {
9360
+ this.blocks = [];
9361
+ this.streamingMessageId = null;
9362
+ },
9363
+ accumulate(newBlocks) {
9364
+ if (this.blocks.length === 0 && newBlocks.length > 0) {
9365
+ this.streamingMessageId = randomUUID5();
9366
+ }
9367
+ accumulateBlocks(this.blocks, newBlocks);
9368
+ }
9369
+ };
9172
9370
  const expectedAuthKey = new AuthKey(env.WORKSTATION_AUTH_KEY);
9173
9371
  let messageBroadcaster = null;
9174
9372
  const supervisorAgent = env.MOCK_MODE ? new MockSupervisorAgent({
@@ -9261,11 +9459,10 @@ async function bootstrap() {
9261
9459
  const syncMessage = message;
9262
9460
  const client = clientRegistry.getBySocket(socket) ?? (syncMessage.device_id ? clientRegistry.getByDeviceId(new DeviceId(syncMessage.device_id)) : void 0);
9263
9461
  const subscriptions2 = client ? client.getSubscriptions() : [];
9264
- const isLightweight = syncMessage.lightweight === true;
9265
9462
  const inMemorySessions = sessionManager.getSessionInfos();
9266
9463
  const persistedAgentSessions = chatHistoryService.getActiveAgentSessions();
9267
9464
  logger.debug(
9268
- { persistedAgentSessions, inMemoryCount: inMemorySessions.length, isLightweight },
9465
+ { persistedAgentSessions, inMemoryCount: inMemorySessions.length },
9269
9466
  "Sync: fetched sessions"
9270
9467
  );
9271
9468
  const inMemorySessionIds = new Set(
@@ -9290,119 +9487,14 @@ async function bootstrap() {
9290
9487
  };
9291
9488
  });
9292
9489
  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
9490
  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) => ({
9491
+ if (supervisorHistoryRaw.length > 0) {
9492
+ const historyForAgent = supervisorHistoryRaw.filter((msg) => msg.role === "user" || msg.role === "assistant").map((msg) => ({
9372
9493
  role: msg.role,
9373
9494
  content: msg.content
9374
9495
  }));
9375
9496
  supervisorAgent.restoreHistory(historyForAgent);
9376
9497
  }
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
9498
  const availableAgentsMap = getAvailableAgents();
9407
9499
  const availableAgents = Array.from(availableAgentsMap.values()).map(
9408
9500
  (agent) => ({
@@ -9435,31 +9527,23 @@ async function bootstrap() {
9435
9527
  );
9436
9528
  }
9437
9529
  }
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
- }
9530
+ const supervisorIsExecuting = supervisorAgent.isProcessing();
9531
+ let currentStreamingBlocks;
9532
+ if (supervisorIsExecuting) {
9533
+ const blocks = supervisorMessageAccumulator.get();
9534
+ if (blocks.length > 0) {
9535
+ currentStreamingBlocks = blocks;
9445
9536
  }
9446
9537
  }
9447
- const supervisorIsExecuting = supervisorAgent.isProcessing();
9448
9538
  logger.info(
9449
9539
  {
9450
9540
  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
9541
  availableAgentsCount: availableAgents.length,
9457
9542
  workspacesCount: workspaces.length,
9458
9543
  supervisorIsExecuting,
9459
- executingStates,
9460
- streamingBlocksCount: Object.keys(currentStreamingBlocks).length
9544
+ hasStreamingBlocks: !!currentStreamingBlocks
9461
9545
  },
9462
- "Sync: sending state to client"
9546
+ "Sync: sending state to client (v1.13 - no histories)"
9463
9547
  );
9464
9548
  const syncStateMessage = JSON.stringify({
9465
9549
  type: "sync.state",
@@ -9467,14 +9551,15 @@ async function bootstrap() {
9467
9551
  payload: {
9468
9552
  sessions: sessions2,
9469
9553
  subscriptions: subscriptions2,
9470
- supervisorHistory,
9471
- agentHistories,
9472
9554
  availableAgents,
9473
9555
  hiddenBaseTypes,
9474
9556
  workspaces,
9475
9557
  supervisorIsExecuting,
9476
9558
  executingStates,
9477
- currentStreamingBlocks: Object.keys(currentStreamingBlocks).length > 0 ? currentStreamingBlocks : void 0
9559
+ // Only include supervisor streaming blocks for mid-stream join
9560
+ currentStreamingBlocks
9561
+ // Protocol v1.13: No supervisorHistory, agentHistories
9562
+ // Clients use history.request for on-demand loading
9478
9563
  }
9479
9564
  });
9480
9565
  sendToDevice(socket, syncMessage.device_id, syncStateMessage);
@@ -9569,7 +9654,7 @@ async function bootstrap() {
9569
9654
  const errorEvent = {
9570
9655
  type: "supervisor.transcription",
9571
9656
  payload: {
9572
- text: "",
9657
+ transcription: "",
9573
9658
  error: "Voice transcription not available - STT service not configured",
9574
9659
  message_id: messageId,
9575
9660
  timestamp: Date.now()
@@ -9619,7 +9704,7 @@ async function bootstrap() {
9619
9704
  const transcriptionEvent = {
9620
9705
  type: "supervisor.transcription",
9621
9706
  payload: {
9622
- text: commandText,
9707
+ transcription: commandText,
9623
9708
  language: transcriptionResult.language,
9624
9709
  duration: transcriptionResult.duration,
9625
9710
  message_id: messageId,
@@ -9648,7 +9733,7 @@ async function bootstrap() {
9648
9733
  const errorEvent = {
9649
9734
  type: "supervisor.transcription",
9650
9735
  payload: {
9651
- text: "",
9736
+ transcription: "",
9652
9737
  error: error instanceof Error ? error.message : "Transcription failed",
9653
9738
  message_id: messageId,
9654
9739
  timestamp: Date.now(),
@@ -9922,45 +10007,29 @@ async function bootstrap() {
9922
10007
  },
9923
10008
  "Client subscribed to agent session"
9924
10009
  );
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
10010
  const isExecuting = agentSessionManager.isExecuting(sessionId);
9943
10011
  const currentStreamingBlocks = agentMessageAccumulator.get(sessionId) ?? [];
10012
+ const streamingMessageId = currentStreamingBlocks.length > 0 ? agentStreamingMessageIds.get(sessionId) : void 0;
9944
10013
  sendToDevice(
9945
10014
  socket,
9946
10015
  subscribeMessage.device_id,
9947
10016
  JSON.stringify({
9948
10017
  type: "session.subscribed",
9949
10018
  session_id: sessionId,
9950
- history: enrichedHistory,
9951
10019
  is_executing: isExecuting,
9952
- current_streaming_blocks: currentStreamingBlocks.length > 0 ? currentStreamingBlocks : void 0
10020
+ current_streaming_blocks: currentStreamingBlocks.length > 0 ? currentStreamingBlocks : void 0,
10021
+ streaming_message_id: streamingMessageId
9953
10022
  })
9954
10023
  );
9955
10024
  logger.debug(
9956
10025
  {
9957
10026
  deviceId: client.deviceId.value,
9958
10027
  sessionId,
9959
- historyCount: enrichedHistory.length,
9960
10028
  isExecuting,
9961
- streamingBlocksCount: currentStreamingBlocks.length
10029
+ streamingBlocksCount: currentStreamingBlocks.length,
10030
+ streamingMessageId
9962
10031
  },
9963
- "Sent agent session history on subscribe"
10032
+ "Agent session subscribed (v1.13 - use history.request for messages)"
9964
10033
  );
9965
10034
  } else {
9966
10035
  const result = subscriptionService.subscribe(
@@ -10089,7 +10158,7 @@ async function bootstrap() {
10089
10158
  type: "session.transcription",
10090
10159
  session_id: sessionId,
10091
10160
  payload: {
10092
- text: transcribedText,
10161
+ transcription: transcribedText,
10093
10162
  language: transcriptionResult.language,
10094
10163
  duration: transcriptionResult.duration,
10095
10164
  message_id: messageId,
@@ -10182,7 +10251,7 @@ async function bootstrap() {
10182
10251
  type: "session.transcription",
10183
10252
  session_id: sessionId,
10184
10253
  payload: {
10185
- text: "",
10254
+ transcription: "",
10186
10255
  error: error instanceof Error ? error.message : "Transcription failed",
10187
10256
  message_id: messageId,
10188
10257
  timestamp: Date.now()
@@ -10508,18 +10577,22 @@ async function bootstrap() {
10508
10577
  "history.request": async (socket, message) => {
10509
10578
  const historyRequest = message;
10510
10579
  const sessionId = historyRequest.payload?.session_id;
10580
+ const beforeSequence = historyRequest.payload?.before_sequence;
10581
+ const limit = historyRequest.payload?.limit;
10511
10582
  const isSupervisor = !sessionId;
10512
10583
  logger.debug(
10513
- { sessionId, isSupervisor, requestId: historyRequest.id },
10514
- "History request received"
10584
+ { sessionId, isSupervisor, beforeSequence, limit, requestId: historyRequest.id },
10585
+ "Paginated history request received"
10515
10586
  );
10516
10587
  try {
10517
10588
  if (isSupervisor) {
10518
- const supervisorHistoryRaw = chatHistoryService.getSupervisorHistory();
10589
+ const result = chatHistoryService.getSupervisorHistoryPaginated({
10590
+ beforeSequence,
10591
+ limit
10592
+ });
10519
10593
  const supervisorHistory = await Promise.all(
10520
- supervisorHistoryRaw.map(async (msg) => ({
10594
+ result.messages.map(async (msg) => ({
10521
10595
  message_id: msg.id,
10522
- // Include message ID for audio.request
10523
10596
  sequence: msg.sequence,
10524
10597
  role: msg.role,
10525
10598
  content: msg.content,
@@ -10528,32 +10601,54 @@ async function bootstrap() {
10528
10601
  msg.audioOutputPath,
10529
10602
  msg.audioInputPath,
10530
10603
  false
10531
- // Don't include audio in history response
10532
10604
  ),
10533
10605
  createdAt: msg.createdAt.toISOString()
10534
10606
  }))
10535
10607
  );
10536
10608
  const isExecuting = supervisorAgent.isProcessing();
10609
+ let currentStreamingBlocks;
10610
+ let streamingMessageId;
10611
+ if (isExecuting && !beforeSequence) {
10612
+ const blocks = supervisorMessageAccumulator.get();
10613
+ if (blocks.length > 0) {
10614
+ currentStreamingBlocks = blocks;
10615
+ streamingMessageId = supervisorMessageAccumulator.getStreamingMessageId() ?? void 0;
10616
+ }
10617
+ }
10537
10618
  socket.send(
10538
10619
  JSON.stringify({
10539
10620
  type: "history.response",
10540
10621
  id: historyRequest.id,
10541
10622
  payload: {
10542
10623
  session_id: null,
10543
- // Indicates supervisor
10544
10624
  history: supervisorHistory,
10545
- is_executing: isExecuting
10625
+ has_more: result.hasMore,
10626
+ oldest_sequence: result.oldestSequence,
10627
+ newest_sequence: result.newestSequence,
10628
+ is_executing: isExecuting,
10629
+ current_streaming_blocks: currentStreamingBlocks,
10630
+ streaming_message_id: streamingMessageId
10546
10631
  }
10547
10632
  })
10548
10633
  );
10549
10634
  logger.debug(
10550
- { messageCount: supervisorHistory.length, isExecuting },
10551
- "Supervisor history sent"
10635
+ {
10636
+ messageCount: supervisorHistory.length,
10637
+ hasMore: result.hasMore,
10638
+ oldestSeq: result.oldestSequence,
10639
+ newestSeq: result.newestSequence,
10640
+ isExecuting
10641
+ },
10642
+ "Paginated supervisor history sent"
10552
10643
  );
10553
10644
  } else {
10554
- const history = chatHistoryService.getAgentHistory(sessionId);
10645
+ const result = chatHistoryService.getAgentHistoryPaginated(sessionId, {
10646
+ beforeSequence,
10647
+ limit
10648
+ });
10555
10649
  const enrichedHistory = await Promise.all(
10556
- history.map(async (msg) => ({
10650
+ result.messages.map(async (msg) => ({
10651
+ message_id: msg.id,
10557
10652
  sequence: msg.sequence,
10558
10653
  role: msg.role,
10559
10654
  content: msg.content,
@@ -10562,17 +10657,18 @@ async function bootstrap() {
10562
10657
  msg.audioOutputPath,
10563
10658
  msg.audioInputPath,
10564
10659
  false
10565
- // Don't include audio in history response
10566
10660
  ),
10567
10661
  createdAt: msg.createdAt.toISOString()
10568
10662
  }))
10569
10663
  );
10570
10664
  const isExecuting = agentSessionManager.isExecuting(sessionId);
10571
10665
  let currentStreamingBlocks;
10572
- if (isExecuting) {
10666
+ let streamingMessageId;
10667
+ if (isExecuting && !beforeSequence) {
10573
10668
  const blocks = agentMessageAccumulator.get(sessionId);
10574
10669
  if (blocks && blocks.length > 0) {
10575
10670
  currentStreamingBlocks = blocks;
10671
+ streamingMessageId = agentStreamingMessageIds.get(sessionId);
10576
10672
  }
10577
10673
  }
10578
10674
  socket.send(
@@ -10582,13 +10678,17 @@ async function bootstrap() {
10582
10678
  payload: {
10583
10679
  session_id: sessionId,
10584
10680
  history: enrichedHistory,
10681
+ has_more: result.hasMore,
10682
+ oldest_sequence: result.oldestSequence,
10683
+ newest_sequence: result.newestSequence,
10585
10684
  is_executing: isExecuting,
10586
- current_streaming_blocks: currentStreamingBlocks
10685
+ current_streaming_blocks: currentStreamingBlocks,
10686
+ streaming_message_id: streamingMessageId
10587
10687
  }
10588
10688
  })
10589
10689
  );
10590
10690
  logger.debug(
10591
- { sessionId, messageCount: enrichedHistory.length, isExecuting },
10691
+ { sessionId, messageCount: enrichedHistory.length, isExecuting, streamingMessageId },
10592
10692
  "Agent session history sent"
10593
10693
  );
10594
10694
  }
@@ -10622,7 +10722,7 @@ async function bootstrap() {
10622
10722
  id: audioRequest.id,
10623
10723
  payload: {
10624
10724
  message_id,
10625
- audio: audioBase64
10725
+ audio_base64: audioBase64
10626
10726
  }
10627
10727
  })
10628
10728
  );
@@ -10780,6 +10880,18 @@ async function bootstrap() {
10780
10880
  });
10781
10881
  const broadcaster = messageBroadcaster;
10782
10882
  const agentMessageAccumulator = /* @__PURE__ */ new Map();
10883
+ const agentStreamingMessageIds = /* @__PURE__ */ new Map();
10884
+ const getOrCreateAgentStreamingMessageId = (sessionId) => {
10885
+ let messageId = agentStreamingMessageIds.get(sessionId);
10886
+ if (!messageId) {
10887
+ messageId = randomUUID5();
10888
+ agentStreamingMessageIds.set(sessionId, messageId);
10889
+ }
10890
+ return messageId;
10891
+ };
10892
+ const clearAgentStreamingMessageId = (sessionId) => {
10893
+ agentStreamingMessageIds.delete(sessionId);
10894
+ };
10783
10895
  agentSessionManager.on(
10784
10896
  "blocks",
10785
10897
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -10852,9 +10964,11 @@ async function bootstrap() {
10852
10964
  const accumulatedBlocks = agentMessageAccumulator.get(sessionId) ?? [];
10853
10965
  const mergedBlocks = mergeToolBlocks(accumulatedBlocks);
10854
10966
  const fullAccumulatedText = mergedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
10967
+ const streamingMessageId = getOrCreateAgentStreamingMessageId(sessionId);
10855
10968
  const outputEvent = {
10856
10969
  type: "session.output",
10857
10970
  session_id: sessionId,
10971
+ streaming_message_id: streamingMessageId,
10858
10972
  payload: {
10859
10973
  content_type: "agent",
10860
10974
  content: fullAccumulatedText,
@@ -10869,6 +10983,9 @@ async function bootstrap() {
10869
10983
  sessionId,
10870
10984
  JSON.stringify(outputEvent)
10871
10985
  );
10986
+ if (isComplete) {
10987
+ clearAgentStreamingMessageId(sessionId);
10988
+ }
10872
10989
  if (isComplete && fullTextContent.length > 0) {
10873
10990
  const pendingVoiceCommand = pendingAgentVoiceCommands.get(sessionId);
10874
10991
  if (pendingVoiceCommand && ttsService) {
@@ -10918,7 +11035,7 @@ async function bootstrap() {
10918
11035
  session_id: sessionId,
10919
11036
  payload: {
10920
11037
  text: textForTTS,
10921
- audio: audioBase64,
11038
+ audio_base64: audioBase64,
10922
11039
  audio_format: "mp3",
10923
11040
  duration: ttsResult.duration,
10924
11041
  message_id: pendingMessageId,
@@ -10944,7 +11061,6 @@ async function bootstrap() {
10944
11061
  }
10945
11062
  }
10946
11063
  );
10947
- let supervisorBlockAccumulator = [];
10948
11064
  supervisorAgent.on(
10949
11065
  "blocks",
10950
11066
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -10959,17 +11075,19 @@ async function bootstrap() {
10959
11075
  { deviceId, blockCount: blocks.length, isComplete },
10960
11076
  "Ignoring supervisor blocks - execution was cancelled"
10961
11077
  );
10962
- supervisorBlockAccumulator = [];
11078
+ supervisorMessageAccumulator.clear();
10963
11079
  return;
10964
11080
  }
10965
11081
  const persistableBlocks = blocks.filter((b) => b.block_type !== "status");
10966
11082
  if (persistableBlocks.length > 0) {
10967
- accumulateBlocks(supervisorBlockAccumulator, persistableBlocks);
11083
+ supervisorMessageAccumulator.accumulate(persistableBlocks);
10968
11084
  }
10969
- const mergedBlocks = mergeToolBlocks(supervisorBlockAccumulator);
11085
+ const mergedBlocks = mergeToolBlocks(supervisorMessageAccumulator.get());
10970
11086
  const textContent = mergedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
11087
+ const streamingMessageId = supervisorMessageAccumulator.getStreamingMessageId();
10971
11088
  const outputEvent = {
10972
11089
  type: "supervisor.output",
11090
+ streaming_message_id: streamingMessageId,
10973
11091
  payload: {
10974
11092
  content_type: "supervisor",
10975
11093
  content: textContent,
@@ -10981,7 +11099,7 @@ async function bootstrap() {
10981
11099
  const message = JSON.stringify(outputEvent);
10982
11100
  broadcaster.broadcastToAll(message);
10983
11101
  if (isComplete) {
10984
- supervisorBlockAccumulator = [];
11102
+ supervisorMessageAccumulator.clear();
10985
11103
  }
10986
11104
  if (isComplete && finalOutput && finalOutput.length > 0) {
10987
11105
  chatHistoryService.saveSupervisorMessage(
@@ -11031,7 +11149,7 @@ async function bootstrap() {
11031
11149
  type: "supervisor.voice_output",
11032
11150
  payload: {
11033
11151
  text: textForTTS,
11034
- audio: audioBase64,
11152
+ audio_base64: audioBase64,
11035
11153
  audio_format: "mp3",
11036
11154
  duration: ttsResult.duration,
11037
11155
  message_id: pendingMessageId,
@@ -11105,23 +11223,35 @@ async function bootstrap() {
11105
11223
  }
11106
11224
  };
11107
11225
  broadcaster.broadcastToAll(JSON.stringify(broadcastMessage));
11226
+ const batcher = new TerminalOutputBatcher({
11227
+ batchIntervalMs: env.TERMINAL_BATCH_INTERVAL_MS,
11228
+ maxBatchSize: env.TERMINAL_BATCH_MAX_SIZE,
11229
+ onFlush: (batchedData) => {
11230
+ const outputMessage = session.addOutputToBuffer(batchedData);
11231
+ const outputEvent = {
11232
+ type: "session.output",
11233
+ session_id: sessionId.value,
11234
+ payload: {
11235
+ content_type: "terminal",
11236
+ content: batchedData,
11237
+ timestamp: outputMessage.timestamp,
11238
+ sequence: outputMessage.sequence
11239
+ }
11240
+ };
11241
+ broadcaster.broadcastToSubscribers(
11242
+ sessionId.value,
11243
+ JSON.stringify(outputEvent)
11244
+ );
11245
+ }
11246
+ });
11108
11247
  session.onOutput((data) => {
11109
- const outputMessage = session.addOutputToBuffer(data);
11110
- const outputEvent = {
11111
- type: "session.output",
11112
- session_id: sessionId.value,
11113
- payload: {
11114
- content_type: "terminal",
11115
- content: data,
11116
- timestamp: outputMessage.timestamp,
11117
- sequence: outputMessage.sequence
11118
- }
11119
- };
11120
- broadcaster.broadcastToSubscribers(
11121
- sessionId.value,
11122
- JSON.stringify(outputEvent)
11123
- );
11248
+ batcher.append(data);
11124
11249
  });
11250
+ const originalTerminate = session.terminate.bind(session);
11251
+ session.terminate = async () => {
11252
+ batcher.dispose();
11253
+ return originalTerminate();
11254
+ };
11125
11255
  });
11126
11256
  if (env.MOCK_MODE) {
11127
11257
  const terminalSession = await sessionManager.createSession({
@@ -11191,23 +11321,52 @@ async function bootstrap() {
11191
11321
  } catch (error) {
11192
11322
  logger.error({ error }, "Failed to connect to tunnel (will retry)");
11193
11323
  }
11324
+ const SHUTDOWN_TIMEOUT_MS = 1e4;
11325
+ let isShuttingDown = false;
11194
11326
  const shutdown = async (signal) => {
11327
+ if (isShuttingDown) {
11328
+ logger.warn({ signal }, "Shutdown already in progress, forcing exit");
11329
+ process.exit(1);
11330
+ }
11331
+ isShuttingDown = true;
11195
11332
  logger.info({ signal }, "Shutdown signal received");
11333
+ const forceExitTimeout = setTimeout(() => {
11334
+ logger.error("Graceful shutdown timeout exceeded, forcing exit");
11335
+ process.exit(1);
11336
+ }, SHUTDOWN_TIMEOUT_MS);
11196
11337
  try {
11197
11338
  logger.info("Disconnecting from tunnel...");
11198
11339
  tunnelClient.disconnect();
11199
11340
  logger.info("Cleaning up agent sessions...");
11200
11341
  agentSessionManager.cleanup();
11201
11342
  logger.info("Terminating all sessions...");
11202
- await sessionManager.terminateAll();
11203
- logger.info("All sessions terminated");
11343
+ const terminatePromise = sessionManager.terminateAll();
11344
+ const sessionTimeoutPromise = new Promise((_, reject) => {
11345
+ setTimeout(() => reject(new Error("Session termination timeout")), 5e3);
11346
+ });
11347
+ try {
11348
+ await Promise.race([terminatePromise, sessionTimeoutPromise]);
11349
+ logger.info("All sessions terminated");
11350
+ } catch (error) {
11351
+ logger.warn({ error }, "Session termination timed out, continuing shutdown");
11352
+ }
11204
11353
  logger.info("Closing HTTP server...");
11205
- await app.close();
11354
+ const closePromise = app.close();
11355
+ const closeTimeoutPromise = new Promise((_, reject) => {
11356
+ setTimeout(() => reject(new Error("HTTP server close timeout")), 2e3);
11357
+ });
11358
+ try {
11359
+ await Promise.race([closePromise, closeTimeoutPromise]);
11360
+ } catch (error) {
11361
+ logger.warn({ error }, "HTTP server close timed out, continuing shutdown");
11362
+ }
11206
11363
  logger.info("Closing database...");
11207
11364
  closeDatabase();
11365
+ clearTimeout(forceExitTimeout);
11208
11366
  logger.info("Shutdown complete");
11209
11367
  process.exit(0);
11210
11368
  } catch (error) {
11369
+ clearTimeout(forceExitTimeout);
11211
11370
  logger.error({ error }, "Error during shutdown");
11212
11371
  process.exit(1);
11213
11372
  }
@@ -11324,6 +11483,14 @@ bootstrap().catch((error) => {
11324
11483
  * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
11325
11484
  * @license FSL-1.1-NC
11326
11485
  */
11486
+ /**
11487
+ * @file terminal-output-batcher.ts
11488
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
11489
+ * @license FSL-1.1-NC
11490
+ *
11491
+ * Batches terminal output chunks to reduce message frequency and improve performance.
11492
+ * Uses adaptive batching based on output rate for optimal responsiveness.
11493
+ */
11327
11494
  /**
11328
11495
  * @file headless-agent-executor.ts
11329
11496
  * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
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.13",
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",