@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.
- package/dist/main.js +146 -49
- 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:
|
|
114
|
-
AGENT_EXECUTION_TIMEOUT: z.coerce.number().default(
|
|
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(
|
|
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:
|
|
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) -
|
|
343
|
-
DEFAULT_TIMEOUT_SECONDS:
|
|
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.
|
|
2592
|
-
* @returns true
|
|
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
|
-
|
|
2596
|
-
|
|
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
|
|
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
|
|
10617
|
+
if (isExecuting) {
|
|
10542
10618
|
const blocks = supervisorMessageAccumulator.get();
|
|
10543
|
-
if (blocks
|
|
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
|
|
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
|
-
|
|
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 (
|
|
10868
|
-
const blockTypeCounts =
|
|
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:
|
|
10954
|
+
totalBlocks: accumulatedBlocks.length,
|
|
10879
10955
|
blockTypes: blockTypeCounts
|
|
10880
10956
|
},
|
|
10881
10957
|
"Saving agent message with accumulated blocks"
|
|
10882
10958
|
);
|
|
10883
|
-
fullTextContent =
|
|
10884
|
-
const hasError =
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|