@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.
- package/dist/main.js +400 -233
- 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(
|
|
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: {
|
|
@@ -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
|
-
|
|
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.
|
|
2584
|
-
* @returns true
|
|
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
|
-
|
|
2588
|
-
|
|
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
|
|
4649
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
8981
|
-
if (!
|
|
8982
|
-
logger.warn(
|
|
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
|
|
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
|
-
|
|
9356
|
-
supervisorHistoryRaw.
|
|
9357
|
-
sequence: msg.sequence,
|
|
9358
|
-
role: msg.role,
|
|
9359
|
-
content: msg.content,
|
|
9360
|
-
content_blocks: await chatHistoryService.enrichBlocksWithAudio(
|
|
9361
|
-
msg.contentBlocks,
|
|
9362
|
-
msg.audioOutputPath,
|
|
9363
|
-
msg.audioInputPath,
|
|
9364
|
-
false
|
|
9365
|
-
// Don't include audio in sync.state
|
|
9366
|
-
),
|
|
9367
|
-
createdAt: msg.createdAt.toISOString()
|
|
9368
|
-
}))
|
|
9369
|
-
);
|
|
9370
|
-
if (supervisorHistory.length > 0) {
|
|
9371
|
-
const historyForAgent = supervisorHistory.filter((msg) => msg.role === "user" || msg.role === "assistant").map((msg) => ({
|
|
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
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
10584
|
+
{ sessionId, isSupervisor, beforeSequence, limit, requestId: historyRequest.id },
|
|
10585
|
+
"Paginated history request received"
|
|
10515
10586
|
);
|
|
10516
10587
|
try {
|
|
10517
10588
|
if (isSupervisor) {
|
|
10518
|
-
const
|
|
10589
|
+
const result = chatHistoryService.getSupervisorHistoryPaginated({
|
|
10590
|
+
beforeSequence,
|
|
10591
|
+
limit
|
|
10592
|
+
});
|
|
10519
10593
|
const supervisorHistory = await Promise.all(
|
|
10520
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
10551
|
-
|
|
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
|
|
10645
|
+
const result = chatHistoryService.getAgentHistoryPaginated(sessionId, {
|
|
10646
|
+
beforeSequence,
|
|
10647
|
+
limit
|
|
10648
|
+
});
|
|
10555
10649
|
const enrichedHistory = await Promise.all(
|
|
10556
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11083
|
+
supervisorMessageAccumulator.accumulate(persistableBlocks);
|
|
10968
11084
|
}
|
|
10969
|
-
const mergedBlocks = mergeToolBlocks(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11203
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|