@tiflis-io/tiflis-code-workstation 0.3.12 → 0.3.14

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 +146 -49
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -110,8 +110,8 @@ var EnvSchema = z.object({
110
110
  // ─────────────────────────────────────────────────────────────
111
111
  // Headless Agents Configuration
112
112
  // ─────────────────────────────────────────────────────────────
113
- /** Timeout for agent command execution in seconds (default: 15 minutes) */
114
- AGENT_EXECUTION_TIMEOUT: z.coerce.number().default(900),
113
+ /** Timeout for agent command execution in seconds (default: 2 hours) */
114
+ AGENT_EXECUTION_TIMEOUT: z.coerce.number().default(7200),
115
115
  CLAUDE_SESSION_LOCK_WAIT_MS: z.coerce.number().default(1500),
116
116
  // ─────────────────────────────────────────────────────────────
117
117
  // Agent Visibility Configuration
@@ -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: {
@@ -339,8 +343,8 @@ var AGENT_COMMANDS = {
339
343
  }
340
344
  };
341
345
  var AGENT_EXECUTION_CONFIG = {
342
- /** Default execution timeout (seconds) - 15 minutes for complex tasks */
343
- DEFAULT_TIMEOUT_SECONDS: 900,
346
+ /** Default execution timeout (seconds) - 2 hours for complex tasks */
347
+ DEFAULT_TIMEOUT_SECONDS: 7200,
344
348
  /** Timeout for waiting on process termination during graceful shutdown (ms) */
345
349
  GRACEFUL_SHUTDOWN_TIMEOUT_MS: 2e3,
346
350
  /** Maximum buffer size for JSON line parsing (bytes) */
@@ -2588,15 +2592,12 @@ var TerminalSession = class extends Session {
2588
2592
  return this._masterDeviceId;
2589
2593
  }
2590
2594
  /**
2591
- * Sets the master device ID. Only sets if not already set (first subscriber wins).
2592
- * @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)
2593
2597
  */
2594
2598
  setMaster(deviceId) {
2595
- if (this._masterDeviceId === null) {
2596
- this._masterDeviceId = deviceId;
2597
- return true;
2598
- }
2599
- return this._masterDeviceId === deviceId;
2599
+ this._masterDeviceId = deviceId;
2600
+ return true;
2600
2601
  }
2601
2602
  /**
2602
2603
  * Checks if the given device is the master for this session.
@@ -2917,6 +2918,75 @@ var PtyManager = class {
2917
2918
  }
2918
2919
  };
2919
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
+
2920
2990
  // src/infrastructure/agents/agent-session-manager.ts
2921
2991
  import { EventEmitter as EventEmitter2 } from "events";
2922
2992
  import { randomUUID as randomUUID2 } from "crypto";
@@ -5103,7 +5173,8 @@ var MessageRepository = class {
5103
5173
  const result = db2.select({ maxSeq: max(messages.sequence) }).from(messages).where(eq3(messages.sessionId, params.sessionId)).get();
5104
5174
  const nextSequence = (result?.maxSeq ?? 0) + 1;
5105
5175
  const newMessage = {
5106
- id: nanoid2(16),
5176
+ id: params.messageId ?? nanoid2(16),
5177
+ // Use provided ID or generate new one
5107
5178
  sessionId: params.sessionId,
5108
5179
  sequence: nextSequence,
5109
5180
  role: params.role,
@@ -5776,8 +5847,9 @@ var ChatHistoryService = class _ChatHistoryService {
5776
5847
  * Saves a supervisor message to the database.
5777
5848
  * Messages are shared across all devices connected to this workstation.
5778
5849
  * @param contentBlocks - Optional structured content blocks (for assistant messages)
5850
+ * @param messageId - Optional ID to use (e.g., streaming_message_id for deduplication)
5779
5851
  */
5780
- saveSupervisorMessage(role, content, contentBlocks) {
5852
+ saveSupervisorMessage(role, content, contentBlocks, messageId) {
5781
5853
  this.ensureSupervisorSession();
5782
5854
  const sessionId = _ChatHistoryService.SUPERVISOR_SESSION_ID;
5783
5855
  const params = {
@@ -5786,7 +5858,9 @@ var ChatHistoryService = class _ChatHistoryService {
5786
5858
  contentType: "text",
5787
5859
  content,
5788
5860
  contentBlocks: contentBlocks ? JSON.stringify(contentBlocks) : void 0,
5789
- isComplete: true
5861
+ isComplete: true,
5862
+ messageId
5863
+ // Pass through to repository for consistent IDs
5790
5864
  };
5791
5865
  const saved = this.messageRepo.create(params);
5792
5866
  this.logger.debug(
@@ -5875,14 +5949,16 @@ var ChatHistoryService = class _ChatHistoryService {
5875
5949
  * @param content - Text content (summary for assistant messages)
5876
5950
  * @param contentBlocks - Structured content blocks for rich UI
5877
5951
  */
5878
- saveAgentMessage(sessionId, role, content, contentBlocks) {
5952
+ saveAgentMessage(sessionId, role, content, contentBlocks, messageId) {
5879
5953
  const params = {
5880
5954
  sessionId,
5881
5955
  role,
5882
5956
  contentType: "text",
5883
5957
  content,
5884
5958
  contentBlocks: contentBlocks ? JSON.stringify(contentBlocks) : void 0,
5885
- isComplete: true
5959
+ isComplete: true,
5960
+ messageId
5961
+ // Pass through to repository
5886
5962
  };
5887
5963
  const saved = this.messageRepo.create(params);
5888
5964
  this.logger.debug(
@@ -9461,7 +9537,7 @@ async function bootstrap() {
9461
9537
  let currentStreamingBlocks;
9462
9538
  if (supervisorIsExecuting) {
9463
9539
  const blocks = supervisorMessageAccumulator.get();
9464
- if (blocks && blocks.length > 0) {
9540
+ if (blocks.length > 0) {
9465
9541
  currentStreamingBlocks = blocks;
9466
9542
  }
9467
9543
  }
@@ -10538,9 +10614,9 @@ async function bootstrap() {
10538
10614
  const isExecuting = supervisorAgent.isProcessing();
10539
10615
  let currentStreamingBlocks;
10540
10616
  let streamingMessageId;
10541
- if (isExecuting && !beforeSequence) {
10617
+ if (isExecuting) {
10542
10618
  const blocks = supervisorMessageAccumulator.get();
10543
- if (blocks && blocks.length > 0) {
10619
+ if (blocks.length > 0) {
10544
10620
  currentStreamingBlocks = blocks;
10545
10621
  streamingMessageId = supervisorMessageAccumulator.getStreamingMessageId() ?? void 0;
10546
10622
  }
@@ -10594,7 +10670,7 @@ async function bootstrap() {
10594
10670
  const isExecuting = agentSessionManager.isExecuting(sessionId);
10595
10671
  let currentStreamingBlocks;
10596
10672
  let streamingMessageId;
10597
- if (isExecuting && !beforeSequence) {
10673
+ if (isExecuting) {
10598
10674
  const blocks = agentMessageAccumulator.get(sessionId);
10599
10675
  if (blocks && blocks.length > 0) {
10600
10676
  currentStreamingBlocks = blocks;
@@ -10850,11 +10926,11 @@ async function bootstrap() {
10850
10926
  );
10851
10927
  }
10852
10928
  }
10929
+ const accumulatedBlocks = agentMessageAccumulator.get(sessionId) ?? [];
10930
+ const streamingMessageId = getOrCreateAgentStreamingMessageId(sessionId);
10853
10931
  let fullTextContent = "";
10854
10932
  if (isComplete) {
10855
- const allBlocks = agentMessageAccumulator.get(sessionId) ?? [];
10856
- agentMessageAccumulator.delete(sessionId);
10857
- if (allBlocks.length === 0) {
10933
+ if (accumulatedBlocks.length === 0) {
10858
10934
  logger.warn(
10859
10935
  {
10860
10936
  sessionId,
@@ -10864,8 +10940,8 @@ async function bootstrap() {
10864
10940
  "Completion received but no blocks were accumulated - blocks may be lost"
10865
10941
  );
10866
10942
  }
10867
- if (allBlocks.length > 0) {
10868
- const blockTypeCounts = allBlocks.reduce(
10943
+ if (accumulatedBlocks.length > 0) {
10944
+ const blockTypeCounts = accumulatedBlocks.reduce(
10869
10945
  (acc, b) => {
10870
10946
  acc[b.block_type] = (acc[b.block_type] ?? 0) + 1;
10871
10947
  return acc;
@@ -10875,26 +10951,25 @@ async function bootstrap() {
10875
10951
  logger.info(
10876
10952
  {
10877
10953
  sessionId,
10878
- totalBlocks: allBlocks.length,
10954
+ totalBlocks: accumulatedBlocks.length,
10879
10955
  blockTypes: blockTypeCounts
10880
10956
  },
10881
10957
  "Saving agent message with accumulated blocks"
10882
10958
  );
10883
- fullTextContent = allBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
10884
- const hasError = allBlocks.some((b) => b.block_type === "error");
10959
+ fullTextContent = accumulatedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
10960
+ const hasError = accumulatedBlocks.some((b) => b.block_type === "error");
10885
10961
  const role = hasError ? "system" : "assistant";
10886
10962
  chatHistoryService.saveAgentMessage(
10887
10963
  sessionId,
10888
10964
  role,
10889
10965
  fullTextContent,
10890
- allBlocks
10966
+ accumulatedBlocks,
10967
+ streamingMessageId
10891
10968
  );
10892
10969
  }
10893
10970
  }
10894
- const accumulatedBlocks = agentMessageAccumulator.get(sessionId) ?? [];
10895
10971
  const mergedBlocks = mergeToolBlocks(accumulatedBlocks);
10896
10972
  const fullAccumulatedText = mergedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
10897
- const streamingMessageId = getOrCreateAgentStreamingMessageId(sessionId);
10898
10973
  const outputEvent = {
10899
10974
  type: "session.output",
10900
10975
  session_id: sessionId,
@@ -10915,6 +10990,7 @@ async function bootstrap() {
10915
10990
  );
10916
10991
  if (isComplete) {
10917
10992
  clearAgentStreamingMessageId(sessionId);
10993
+ agentMessageAccumulator.delete(sessionId);
10918
10994
  }
10919
10995
  if (isComplete && fullTextContent.length > 0) {
10920
10996
  const pendingVoiceCommand = pendingAgentVoiceCommands.get(sessionId);
@@ -11035,7 +11111,8 @@ async function bootstrap() {
11035
11111
  chatHistoryService.saveSupervisorMessage(
11036
11112
  "assistant",
11037
11113
  finalOutput,
11038
- mergedBlocks
11114
+ mergedBlocks,
11115
+ streamingMessageId ?? void 0
11039
11116
  );
11040
11117
  const pendingVoiceCommand = pendingSupervisorVoiceCommands.get(deviceId);
11041
11118
  if (pendingVoiceCommand && ttsService) {
@@ -11153,23 +11230,35 @@ async function bootstrap() {
11153
11230
  }
11154
11231
  };
11155
11232
  broadcaster.broadcastToAll(JSON.stringify(broadcastMessage));
11233
+ const batcher = new TerminalOutputBatcher({
11234
+ batchIntervalMs: env.TERMINAL_BATCH_INTERVAL_MS,
11235
+ maxBatchSize: env.TERMINAL_BATCH_MAX_SIZE,
11236
+ onFlush: (batchedData) => {
11237
+ const outputMessage = session.addOutputToBuffer(batchedData);
11238
+ const outputEvent = {
11239
+ type: "session.output",
11240
+ session_id: sessionId.value,
11241
+ payload: {
11242
+ content_type: "terminal",
11243
+ content: batchedData,
11244
+ timestamp: outputMessage.timestamp,
11245
+ sequence: outputMessage.sequence
11246
+ }
11247
+ };
11248
+ broadcaster.broadcastToSubscribers(
11249
+ sessionId.value,
11250
+ JSON.stringify(outputEvent)
11251
+ );
11252
+ }
11253
+ });
11156
11254
  session.onOutput((data) => {
11157
- const outputMessage = session.addOutputToBuffer(data);
11158
- const outputEvent = {
11159
- type: "session.output",
11160
- session_id: sessionId.value,
11161
- payload: {
11162
- content_type: "terminal",
11163
- content: data,
11164
- timestamp: outputMessage.timestamp,
11165
- sequence: outputMessage.sequence
11166
- }
11167
- };
11168
- broadcaster.broadcastToSubscribers(
11169
- sessionId.value,
11170
- JSON.stringify(outputEvent)
11171
- );
11255
+ batcher.append(data);
11172
11256
  });
11257
+ const originalTerminate = session.terminate.bind(session);
11258
+ session.terminate = async () => {
11259
+ batcher.dispose();
11260
+ return originalTerminate();
11261
+ };
11173
11262
  });
11174
11263
  if (env.MOCK_MODE) {
11175
11264
  const terminalSession = await sessionManager.createSession({
@@ -11401,6 +11490,14 @@ bootstrap().catch((error) => {
11401
11490
  * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
11402
11491
  * @license FSL-1.1-NC
11403
11492
  */
11493
+ /**
11494
+ * @file terminal-output-batcher.ts
11495
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
11496
+ * @license FSL-1.1-NC
11497
+ *
11498
+ * Batches terminal output chunks to reduce message frequency and improve performance.
11499
+ * Uses adaptive batching based on output rate for optimal responsiveness.
11500
+ */
11404
11501
  /**
11405
11502
  * @file headless-agent-executor.ts
11406
11503
  * @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.12",
3
+ "version": "0.3.14",
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",