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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.js +437 -292
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -118,11 +118,11 @@ var EnvSchema = z.object({
118
118
  // Hide base agent options in mobile apps (only show aliases)
119
119
  // ─────────────────────────────────────────────────────────────
120
120
  /** Hide base Cursor agent option (only show aliases) */
121
- HIDE_BASE_CURSOR: z.string().transform((val) => val?.toLowerCase() === "true").default("false"),
121
+ HIDE_BASE_CURSOR: z.string().transform((val) => val.toLowerCase() === "true").default("false"),
122
122
  /** Hide base Claude agent option (only show aliases) */
123
- HIDE_BASE_CLAUDE: z.string().transform((val) => val?.toLowerCase() === "true").default("false"),
123
+ HIDE_BASE_CLAUDE: z.string().transform((val) => val.toLowerCase() === "true").default("false"),
124
124
  /** Hide base OpenCode agent option (only show aliases) */
125
- HIDE_BASE_OPENCODE: z.string().transform((val) => val?.toLowerCase() === "true").default("false"),
125
+ HIDE_BASE_OPENCODE: z.string().transform((val) => val.toLowerCase() === "true").default("false"),
126
126
  // ─────────────────────────────────────────────────────────────
127
127
  // Terminal Configuration
128
128
  // ─────────────────────────────────────────────────────────────
@@ -134,7 +134,7 @@ var EnvSchema = z.object({
134
134
  // Mock Mode Configuration (for screenshot automation)
135
135
  // ─────────────────────────────────────────────────────────────
136
136
  /** Enable mock mode for screenshot automation tests */
137
- MOCK_MODE: z.string().transform((val) => val?.toLowerCase() === "true").default("false"),
137
+ MOCK_MODE: z.string().transform((val) => val.toLowerCase() === "true").default("false"),
138
138
  /** Path to mock fixtures directory (defaults to built-in fixtures) */
139
139
  MOCK_FIXTURES_PATH: z.string().optional()
140
140
  });
@@ -155,6 +155,7 @@ function parseAgentAliases() {
155
155
  let commandStartIndex = 0;
156
156
  for (let i = 0; i < parts.length; i++) {
157
157
  const part = parts[i];
158
+ if (!part) break;
158
159
  const eqIndex = part.indexOf("=");
159
160
  if (eqIndex > 0 && !part.startsWith("-")) {
160
161
  const varName = part.slice(0, eqIndex);
@@ -173,6 +174,10 @@ function parseAgentAliases() {
173
174
  continue;
174
175
  }
175
176
  const baseCommand = commandParts[0];
177
+ if (!baseCommand) {
178
+ console.warn(`Invalid agent alias ${key}: empty base command`);
179
+ continue;
180
+ }
176
181
  const additionalArgs = commandParts.slice(1);
177
182
  aliases.set(aliasName, {
178
183
  name: aliasName,
@@ -1144,8 +1149,9 @@ var SyncMessageSchema = z2.object({
1144
1149
  // If true, excludes message histories (for watchOS)
1145
1150
  });
1146
1151
  var HistoryRequestPayloadSchema = z2.object({
1147
- session_id: z2.string().optional()
1148
- // If omitted, returns supervisor history
1152
+ session_id: z2.string().nullable().optional(),
1153
+ before_sequence: z2.number().int().optional(),
1154
+ limit: z2.number().int().min(1).max(50).optional()
1149
1155
  });
1150
1156
  var HistoryRequestSchema = z2.object({
1151
1157
  type: z2.literal("history.request"),
@@ -1188,7 +1194,7 @@ var TerminateSessionSchema = z2.object({
1188
1194
  var SupervisorCommandPayloadSchema = z2.object({
1189
1195
  command: z2.string().optional(),
1190
1196
  audio: z2.string().optional(),
1191
- audio_format: z2.enum(["m4a", "wav", "mp3"]).optional(),
1197
+ audio_format: z2.enum(["m4a", "wav", "mp3", "webm", "opus"]).optional(),
1192
1198
  message_id: z2.string().optional(),
1193
1199
  language: z2.string().optional()
1194
1200
  }).refine(
@@ -1232,7 +1238,7 @@ var SessionExecutePayloadSchema = z2.object({
1232
1238
  text: z2.string().optional(),
1233
1239
  // Alias for content (backward compat)
1234
1240
  audio: z2.string().optional(),
1235
- audio_format: z2.enum(["m4a", "wav", "mp3"]).optional(),
1241
+ audio_format: z2.enum(["m4a", "wav", "mp3", "webm", "opus"]).optional(),
1236
1242
  message_id: z2.string().optional(),
1237
1243
  // For linking transcription back to voice message
1238
1244
  language: z2.string().optional(),
@@ -1365,6 +1371,13 @@ function parseClientMessage(data) {
1365
1371
  }
1366
1372
  return void 0;
1367
1373
  }
1374
+ function parseClientMessageWithErrors(data) {
1375
+ const result = IncomingClientMessageSchema.safeParse(data);
1376
+ if (result.success) {
1377
+ return { success: true, data: result.data };
1378
+ }
1379
+ return { success: false, errors: result.error.issues };
1380
+ }
1368
1381
  function parseTunnelMessage(data) {
1369
1382
  const result = IncomingTunnelMessageSchema.safeParse(data);
1370
1383
  if (result.success) {
@@ -2315,7 +2328,7 @@ var FileSystemWorkspaceDiscovery = class {
2315
2328
  }
2316
2329
  try {
2317
2330
  execSync(`git merge "${sourceBranch}"`, { cwd: projectPath });
2318
- } catch (error) {
2331
+ } catch {
2319
2332
  const conflicts = execSync("git diff --name-only --diff-filter=U", {
2320
2333
  cwd: projectPath,
2321
2334
  encoding: "utf-8"
@@ -3283,8 +3296,8 @@ function mergeToolBlocks(blocks) {
3283
3296
  metadata: {
3284
3297
  tool_name: block.metadata.tool_name || existing.metadata.tool_name,
3285
3298
  tool_use_id: toolUseId,
3286
- tool_input: block.metadata.tool_input || existing.metadata.tool_input,
3287
- tool_output: block.metadata.tool_output || existing.metadata.tool_output,
3299
+ tool_input: block.metadata.tool_input ?? existing.metadata.tool_input,
3300
+ tool_output: block.metadata.tool_output ?? existing.metadata.tool_output,
3288
3301
  tool_status: mergedStatus
3289
3302
  }
3290
3303
  };
@@ -3341,8 +3354,8 @@ function accumulateBlocks(existing, newBlocks) {
3341
3354
  metadata: {
3342
3355
  tool_name: block.metadata.tool_name || existingBlock.metadata.tool_name,
3343
3356
  tool_use_id: toolUseId,
3344
- tool_input: block.metadata.tool_input || existingBlock.metadata.tool_input,
3345
- tool_output: block.metadata.tool_output || existingBlock.metadata.tool_output,
3357
+ tool_input: block.metadata.tool_input ?? existingBlock.metadata.tool_input,
3358
+ tool_output: block.metadata.tool_output ?? existingBlock.metadata.tool_output,
3346
3359
  tool_status: mergedStatus
3347
3360
  }
3348
3361
  };
@@ -4022,7 +4035,7 @@ var AgentSessionManager = class extends EventEmitter2 {
4022
4035
  const worktreePath = `/${workspace}/${project}--${branch}`;
4023
4036
  const terminatedSessions = [];
4024
4037
  for (const [sessionId, state] of this.sessions) {
4025
- const isInWorktree = state.workingDir.includes(worktreePath) || state.cliSessionId?.includes(`${project}--${branch}`) || state.workingDir.endsWith(`${project}--${branch}`);
4038
+ const isInWorktree = state.workingDir.includes(worktreePath) || (state.cliSessionId?.includes(`${project}--${branch}`) ?? false) || state.workingDir.endsWith(`${project}--${branch}`);
4026
4039
  if (isInWorktree) {
4027
4040
  try {
4028
4041
  if (state.isExecuting) {
@@ -4044,7 +4057,7 @@ var AgentSessionManager = class extends EventEmitter2 {
4044
4057
  getWorktreeSessionSummary(workspace, project, branch) {
4045
4058
  const worktreePath = `/${workspace}/${project}--${branch}`;
4046
4059
  const activeSessions = Array.from(this.sessions.values()).filter(
4047
- (session) => session.workingDir.includes(worktreePath) || session.cliSessionId?.includes(`${project}--${branch}`) || session.workingDir.endsWith(`${project}--${branch}`)
4060
+ (session) => session.workingDir.includes(worktreePath) || (session.cliSessionId?.includes(`${project}--${branch}`) ?? false) || session.workingDir.endsWith(`${project}--${branch}`)
4048
4061
  );
4049
4062
  const sessionTypes = [...new Set(activeSessions.map((s) => s.agentType))];
4050
4063
  const executingCount = activeSessions.filter((s) => s.isExecuting).length;
@@ -4640,8 +4653,8 @@ var CreateSessionUseCase = class {
4640
4653
  */
4641
4654
  checkSessionLimits(sessionType) {
4642
4655
  if (sessionType === "terminal") {
4643
- const count = this.deps.sessionManager.countByType("terminal");
4644
- if (count >= SESSION_CONFIG.MAX_TERMINAL_SESSIONS) {
4656
+ const count2 = this.deps.sessionManager.countByType("terminal");
4657
+ if (count2 >= SESSION_CONFIG.MAX_TERMINAL_SESSIONS) {
4645
4658
  throw new SessionLimitReachedError("terminal", SESSION_CONFIG.MAX_TERMINAL_SESSIONS);
4646
4659
  }
4647
4660
  } else if (sessionType !== "supervisor") {
@@ -4691,6 +4704,9 @@ var TerminateSessionUseCase = class {
4691
4704
  let terminatedInMemory = false;
4692
4705
  let terminatedInDb = false;
4693
4706
  if (session) {
4707
+ if (isAgentType(session.type)) {
4708
+ this.deps.agentSessionManager.terminateSession(sessionId);
4709
+ }
4694
4710
  await this.deps.sessionManager.terminateSession(id);
4695
4711
  terminatedInMemory = true;
4696
4712
  this.logger.info(
@@ -4729,6 +4745,42 @@ var TerminateSessionUseCase = class {
4729
4745
  };
4730
4746
  return { response, broadcast };
4731
4747
  }
4748
+ /**
4749
+ * Terminates a session and broadcasts the termination to all clients.
4750
+ * Use this for internal calls (e.g., from supervisor tools) where no request/response is needed.
4751
+ * Returns true if session was found and terminated, false otherwise.
4752
+ */
4753
+ async terminateAndBroadcast(sessionId) {
4754
+ const id = new SessionId(sessionId);
4755
+ this.logger.info({ sessionId }, "Attempting to terminate session (internal)");
4756
+ const session = this.deps.sessionManager.getSession(id);
4757
+ if (session?.type === "supervisor") {
4758
+ throw new Error("Cannot terminate supervisor session");
4759
+ }
4760
+ let terminatedInMemory = false;
4761
+ let terminatedInDb = false;
4762
+ if (session) {
4763
+ if (isAgentType(session.type)) {
4764
+ this.deps.agentSessionManager.terminateSession(sessionId);
4765
+ }
4766
+ await this.deps.sessionManager.terminateSession(id);
4767
+ terminatedInMemory = true;
4768
+ }
4769
+ terminatedInDb = this.deps.chatHistoryService.terminateSession(sessionId);
4770
+ if (!terminatedInMemory && !terminatedInDb) {
4771
+ return false;
4772
+ }
4773
+ const broadcast = {
4774
+ type: "session.terminated",
4775
+ session_id: sessionId
4776
+ };
4777
+ this.deps.messageBroadcaster.broadcastToAll(JSON.stringify(broadcast));
4778
+ this.logger.info(
4779
+ { sessionId, terminatedInMemory, terminatedInDb },
4780
+ "Session terminated and broadcast sent"
4781
+ );
4782
+ return true;
4783
+ }
4732
4784
  };
4733
4785
 
4734
4786
  // src/application/queries/list-sessions.ts
@@ -5040,7 +5092,7 @@ var MessageBroadcasterImpl = class {
5040
5092
  };
5041
5093
 
5042
5094
  // src/infrastructure/persistence/repositories/message-repository.ts
5043
- import { eq as eq3, desc, gt, and as and2, max } from "drizzle-orm";
5095
+ import { eq as eq3, desc, gt, lt, and as and2, max, count } from "drizzle-orm";
5044
5096
  import { nanoid as nanoid2 } from "nanoid";
5045
5097
  var MessageRepository = class {
5046
5098
  /**
@@ -5066,17 +5118,24 @@ var MessageRepository = class {
5066
5118
  db2.insert(messages).values(newMessage).run();
5067
5119
  return { ...newMessage, createdAt: newMessage.createdAt };
5068
5120
  }
5069
- /**
5070
- * Gets messages for a session with pagination.
5071
- * Returns messages ordered by sequence descending (newest first).
5072
- */
5073
5121
  getBySession(sessionId, limit = 100) {
5074
5122
  const db2 = getDatabase();
5075
5123
  return db2.select().from(messages).where(eq3(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all();
5076
5124
  }
5077
- /**
5078
- * Gets messages after a specific timestamp.
5079
- */
5125
+ getBySessionPaginated(sessionId, options = {}) {
5126
+ const db2 = getDatabase();
5127
+ const limit = Math.min(options.limit ?? 20, 50);
5128
+ const totalResult = db2.select({ count: count() }).from(messages).where(eq3(messages.sessionId, sessionId)).get();
5129
+ const totalCount = totalResult?.count ?? 0;
5130
+ const whereClause = options.beforeSequence ? and2(
5131
+ eq3(messages.sessionId, sessionId),
5132
+ lt(messages.sequence, options.beforeSequence)
5133
+ ) : eq3(messages.sessionId, sessionId);
5134
+ const rows = db2.select().from(messages).where(whereClause).orderBy(desc(messages.sequence)).limit(limit + 1).all();
5135
+ const hasMore = rows.length > limit;
5136
+ const resultMessages = hasMore ? rows.slice(0, limit) : rows;
5137
+ return { messages: resultMessages, hasMore, totalCount };
5138
+ }
5080
5139
  getAfterTimestamp(sessionId, timestamp, limit = 100) {
5081
5140
  const db2 = getDatabase();
5082
5141
  return db2.select().from(messages).where(and2(eq3(messages.sessionId, sessionId), gt(messages.createdAt, timestamp))).orderBy(messages.createdAt).limit(limit).all();
@@ -5766,9 +5825,40 @@ var ChatHistoryService = class _ChatHistoryService {
5766
5825
  };
5767
5826
  });
5768
5827
  }
5769
- /**
5770
- * Clears supervisor chat history (global).
5771
- */
5828
+ getSupervisorHistoryPaginated(options = {}) {
5829
+ const sessionId = _ChatHistoryService.SUPERVISOR_SESSION_ID;
5830
+ const result = this.messageRepo.getBySessionPaginated(sessionId, options);
5831
+ const messages2 = result.messages.reverse().map((row) => {
5832
+ let contentBlocks;
5833
+ if (row.contentBlocks) {
5834
+ try {
5835
+ contentBlocks = JSON.parse(row.contentBlocks);
5836
+ } catch {
5837
+ }
5838
+ }
5839
+ return {
5840
+ id: row.id,
5841
+ sessionId: row.sessionId,
5842
+ sequence: row.sequence,
5843
+ role: row.role,
5844
+ contentType: row.contentType,
5845
+ content: row.content,
5846
+ contentBlocks,
5847
+ audioInputPath: row.audioInputPath,
5848
+ audioOutputPath: row.audioOutputPath,
5849
+ isComplete: row.isComplete ?? false,
5850
+ createdAt: row.createdAt
5851
+ };
5852
+ });
5853
+ const firstMsg = messages2[0];
5854
+ const lastMsg = messages2[messages2.length - 1];
5855
+ return {
5856
+ messages: messages2,
5857
+ hasMore: result.hasMore,
5858
+ oldestSequence: firstMsg?.sequence,
5859
+ newestSequence: lastMsg?.sequence
5860
+ };
5861
+ }
5772
5862
  clearSupervisorHistory() {
5773
5863
  const sessionId = _ChatHistoryService.SUPERVISOR_SESSION_ID;
5774
5864
  this.messageRepo.deleteBySession(sessionId);
@@ -5851,9 +5941,43 @@ var ChatHistoryService = class _ChatHistoryService {
5851
5941
  };
5852
5942
  });
5853
5943
  }
5854
- /**
5855
- * Clears agent session chat history.
5856
- */
5944
+ getAgentHistoryPaginated(sessionId, options = {}) {
5945
+ const result = this.messageRepo.getBySessionPaginated(sessionId, options);
5946
+ const storedMessages = result.messages.reverse().map((row) => {
5947
+ let contentBlocks;
5948
+ if (row.contentBlocks) {
5949
+ try {
5950
+ contentBlocks = JSON.parse(row.contentBlocks);
5951
+ } catch (error) {
5952
+ this.logger.error(
5953
+ { sessionId, messageId: row.id, error },
5954
+ "Failed to parse contentBlocks JSON"
5955
+ );
5956
+ }
5957
+ }
5958
+ return {
5959
+ id: row.id,
5960
+ sessionId: row.sessionId,
5961
+ sequence: row.sequence,
5962
+ role: row.role,
5963
+ contentType: row.contentType,
5964
+ content: row.content,
5965
+ contentBlocks,
5966
+ audioInputPath: row.audioInputPath,
5967
+ audioOutputPath: row.audioOutputPath,
5968
+ isComplete: row.isComplete ?? false,
5969
+ createdAt: row.createdAt
5970
+ };
5971
+ });
5972
+ const firstMsg = storedMessages[0];
5973
+ const lastMsg = storedMessages[storedMessages.length - 1];
5974
+ return {
5975
+ messages: storedMessages,
5976
+ hasMore: result.hasMore,
5977
+ oldestSequence: firstMsg?.sequence,
5978
+ newestSequence: lastMsg?.sequence
5979
+ };
5980
+ }
5857
5981
  clearAgentHistory(sessionId) {
5858
5982
  this.messageRepo.deleteBySession(sessionId);
5859
5983
  this.logger.info({ sessionId }, "Agent session history cleared");
@@ -6387,18 +6511,35 @@ var InMemorySessionManager = class extends EventEmitter3 {
6387
6511
  this.logger.info({ sessionId: sessionId.value }, "Session terminated");
6388
6512
  }
6389
6513
  /**
6390
- * Terminates all sessions.
6514
+ * Terminates all sessions with individual timeouts.
6515
+ * Each session has a 3-second timeout to prevent hanging.
6391
6516
  */
6392
6517
  async terminateAll() {
6393
6518
  this.agentSessionManager.cleanup();
6394
6519
  const sessions2 = Array.from(this.sessions.values());
6520
+ const sessionCount = sessions2.length;
6521
+ if (sessionCount === 0) {
6522
+ this.logger.info("No sessions to terminate");
6523
+ return;
6524
+ }
6525
+ this.logger.info({ count: sessionCount }, "Terminating all sessions...");
6526
+ const INDIVIDUAL_TIMEOUT_MS = 3e3;
6395
6527
  await Promise.all(
6396
6528
  sessions2.map(async (session) => {
6529
+ const sessionId = session.id.value;
6397
6530
  try {
6398
- await session.terminate();
6531
+ const terminatePromise = session.terminate();
6532
+ const timeoutPromise = new Promise((resolve2) => {
6533
+ setTimeout(() => {
6534
+ this.logger.warn({ sessionId }, "Session termination timed out, skipping");
6535
+ resolve2();
6536
+ }, INDIVIDUAL_TIMEOUT_MS);
6537
+ });
6538
+ await Promise.race([terminatePromise, timeoutPromise]);
6539
+ this.logger.debug({ sessionId }, "Session terminated");
6399
6540
  } catch (error) {
6400
6541
  this.logger.error(
6401
- { sessionId: session.id.value, error },
6542
+ { sessionId, error },
6402
6543
  "Error terminating session"
6403
6544
  );
6404
6545
  }
@@ -6406,7 +6547,7 @@ var InMemorySessionManager = class extends EventEmitter3 {
6406
6547
  );
6407
6548
  this.sessions.clear();
6408
6549
  this.supervisorSession = null;
6409
- this.logger.info({ count: sessions2.length }, "All sessions terminated");
6550
+ this.logger.info({ count: sessionCount }, "All sessions terminated");
6410
6551
  }
6411
6552
  /**
6412
6553
  * Gets the count of active sessions by type.
@@ -6857,16 +6998,7 @@ Steps executed:
6857
6998
  // src/infrastructure/agents/supervisor/tools/session-tools.ts
6858
6999
  import { tool as tool3 } from "@langchain/core/tools";
6859
7000
  import { z as z5 } from "zod";
6860
- function createSessionTools(sessionManager, agentSessionManager, workspaceDiscovery, workspacesRoot, getMessageBroadcaster, getChatHistoryService) {
6861
- const broadcastTermination = (sessionId) => {
6862
- const broadcaster = getMessageBroadcaster?.();
6863
- if (!broadcaster) return;
6864
- const message = {
6865
- type: "session.terminated",
6866
- session_id: sessionId
6867
- };
6868
- broadcaster.broadcastToAll(JSON.stringify(message));
6869
- };
7001
+ function createSessionTools(sessionManager, agentSessionManager, workspaceDiscovery, workspacesRoot, _getMessageBroadcaster, getChatHistoryService, clearSupervisorContext, terminateSessionCallback) {
6870
7002
  const listSessions = tool3(
6871
7003
  () => {
6872
7004
  const chatHistoryService = getChatHistoryService?.();
@@ -7008,26 +7140,13 @@ Use this tool when user asks to "open terminal", "create terminal", or similar r
7008
7140
  const terminateSession = tool3(
7009
7141
  async ({ sessionId }) => {
7010
7142
  try {
7011
- const { SessionId: SessionId2 } = await import("./session-id-VKOYWZAK.js");
7012
- const id = new SessionId2(sessionId);
7013
- const session = sessionManager.getSession(id);
7014
- let terminatedInMemory = false;
7015
- let terminatedInDb = false;
7016
- if (session) {
7017
- if (isAgentType(session.type)) {
7018
- agentSessionManager.terminateSession(sessionId);
7019
- }
7020
- await sessionManager.terminateSession(id);
7021
- terminatedInMemory = true;
7022
- }
7023
- const chatHistoryService = getChatHistoryService?.();
7024
- if (chatHistoryService) {
7025
- terminatedInDb = chatHistoryService.terminateSession(sessionId);
7143
+ if (!terminateSessionCallback) {
7144
+ return "Error: Terminate session callback not configured.";
7026
7145
  }
7027
- if (!terminatedInMemory && !terminatedInDb) {
7146
+ const terminated = await terminateSessionCallback(sessionId);
7147
+ if (!terminated) {
7028
7148
  return `Session "${sessionId}" not found.`;
7029
7149
  }
7030
- broadcastTermination(sessionId);
7031
7150
  return `Session "${sessionId}" terminated.`;
7032
7151
  } catch (error) {
7033
7152
  return `Error terminating session: ${error instanceof Error ? error.message : String(error)}`;
@@ -7044,6 +7163,9 @@ Use this tool when user asks to "open terminal", "create terminal", or similar r
7044
7163
  const terminateAllSessions = tool3(
7045
7164
  async ({ sessionType }) => {
7046
7165
  try {
7166
+ if (!terminateSessionCallback) {
7167
+ return "Error: Terminate session callback not configured.";
7168
+ }
7047
7169
  const typeFilter = sessionType === "all" || !sessionType ? null : sessionType;
7048
7170
  const chatHistoryService = getChatHistoryService?.();
7049
7171
  const inMemorySessions = sessionManager.getAllSessions();
@@ -7051,31 +7173,25 @@ Use this tool when user asks to "open terminal", "create terminal", or similar r
7051
7173
  const persistedSessions = chatHistoryService?.getActiveAgentSessions() ?? [];
7052
7174
  const inMemoryIds = new Set(inMemorySessions.map((s) => s.id.value));
7053
7175
  const persistedToTerminate = persistedSessions.filter((s) => !inMemoryIds.has(s.sessionId)).filter((s) => !typeFilter || s.sessionType === typeFilter);
7054
- if (inMemoryToTerminate.length === 0 && persistedToTerminate.length === 0) {
7176
+ const sessionsToTerminate = [
7177
+ ...inMemoryToTerminate.map((s) => ({ id: s.id.value, type: s.type })),
7178
+ ...persistedToTerminate.map((s) => ({ id: s.sessionId, type: s.sessionType }))
7179
+ ];
7180
+ if (sessionsToTerminate.length === 0) {
7055
7181
  return typeFilter ? `No active ${typeFilter} sessions to terminate.` : "No active sessions to terminate.";
7056
7182
  }
7057
7183
  const terminated = [];
7058
7184
  const errors = [];
7059
- for (const session of inMemoryToTerminate) {
7185
+ for (const session of sessionsToTerminate) {
7060
7186
  try {
7061
- if (isAgentType(session.type)) {
7062
- agentSessionManager.terminateSession(session.id.value);
7187
+ const success = await terminateSessionCallback(session.id);
7188
+ if (success) {
7189
+ terminated.push(`${session.type}:${session.id}`);
7190
+ } else {
7191
+ errors.push(`${session.id}: Session not found`);
7063
7192
  }
7064
- await sessionManager.terminateSession(session.id);
7065
- chatHistoryService?.terminateSession(session.id.value);
7066
- broadcastTermination(session.id.value);
7067
- terminated.push(`${session.type}:${session.id.value}`);
7068
- } catch (error) {
7069
- errors.push(`${session.id.value}: ${error instanceof Error ? error.message : String(error)}`);
7070
- }
7071
- }
7072
- for (const session of persistedToTerminate) {
7073
- try {
7074
- chatHistoryService?.terminateSession(session.sessionId);
7075
- broadcastTermination(session.sessionId);
7076
- terminated.push(`${session.sessionType}:${session.sessionId}`);
7077
7193
  } catch (error) {
7078
- errors.push(`${session.sessionId}: ${error instanceof Error ? error.message : String(error)}`);
7194
+ errors.push(`${session.id}: ${error instanceof Error ? error.message : String(error)}`);
7079
7195
  }
7080
7196
  }
7081
7197
  let result = `Terminated ${terminated.length} session(s)`;
@@ -7220,6 +7336,23 @@ ${terminatedSessions.map((id) => ` - ${id}`).join("\n")}`;
7220
7336
  })
7221
7337
  }
7222
7338
  );
7339
+ const clearContext = tool3(
7340
+ () => {
7341
+ try {
7342
+ if (clearSupervisorContext) {
7343
+ clearSupervisorContext();
7344
+ }
7345
+ return "Supervisor context cleared successfully. Conversation history has been reset.";
7346
+ } catch (error) {
7347
+ return `Error clearing context: ${error instanceof Error ? error.message : String(error)}`;
7348
+ }
7349
+ },
7350
+ {
7351
+ name: "clear_supervisor_context",
7352
+ description: 'Clears the supervisor agent conversation context and history. Use when user asks to "clear context", "reset conversation", "start fresh", or similar requests.',
7353
+ schema: z5.object({})
7354
+ }
7355
+ );
7223
7356
  return [
7224
7357
  listSessions,
7225
7358
  listAvailableAgents,
@@ -7230,7 +7363,8 @@ ${terminatedSessions.map((id) => ` - ${id}`).join("\n")}`;
7230
7363
  getSessionInfo,
7231
7364
  listSessionsWithWorktrees,
7232
7365
  getWorktreeSessionSummary,
7233
- terminateWorktreeSessions
7366
+ terminateWorktreeSessions,
7367
+ clearContext
7234
7368
  ];
7235
7369
  }
7236
7370
 
@@ -7348,6 +7482,8 @@ function formatSize(bytes) {
7348
7482
  var SupervisorAgent = class extends EventEmitter4 {
7349
7483
  logger;
7350
7484
  agent;
7485
+ getMessageBroadcaster;
7486
+ getChatHistoryService;
7351
7487
  conversationHistory = [];
7352
7488
  abortController = null;
7353
7489
  isExecuting = false;
@@ -7359,8 +7495,18 @@ var SupervisorAgent = class extends EventEmitter4 {
7359
7495
  constructor(config2) {
7360
7496
  super();
7361
7497
  this.logger = config2.logger.child({ component: "SupervisorAgent" });
7498
+ this.getMessageBroadcaster = config2.getMessageBroadcaster;
7499
+ this.getChatHistoryService = config2.getChatHistoryService;
7362
7500
  const env = getEnv();
7363
7501
  const llm = this.createLLM(env);
7502
+ const terminateSessionCallback = async (sessionId) => {
7503
+ const terminate = config2.getTerminateSession?.();
7504
+ if (!terminate) {
7505
+ this.logger.warn("Terminate session callback not available");
7506
+ return false;
7507
+ }
7508
+ return terminate(sessionId);
7509
+ };
7364
7510
  const tools = [
7365
7511
  ...createWorkspaceTools(config2.workspaceDiscovery),
7366
7512
  ...createWorktreeTools(config2.workspaceDiscovery, config2.agentSessionManager),
@@ -7370,7 +7516,9 @@ var SupervisorAgent = class extends EventEmitter4 {
7370
7516
  config2.workspaceDiscovery,
7371
7517
  config2.workspacesRoot,
7372
7518
  config2.getMessageBroadcaster,
7373
- config2.getChatHistoryService
7519
+ config2.getChatHistoryService,
7520
+ () => this.clearContext(),
7521
+ terminateSessionCallback
7374
7522
  ),
7375
7523
  ...createFilesystemTools(config2.workspacesRoot)
7376
7524
  ];
@@ -7621,14 +7769,38 @@ var SupervisorAgent = class extends EventEmitter4 {
7621
7769
  this.logger.debug("Ended command processing");
7622
7770
  }
7623
7771
  /**
7624
- * Clears global conversation history.
7772
+ * Clears global conversation history (in-memory only).
7625
7773
  * Also resets cancellation state to allow new commands.
7774
+ * @deprecated Use clearContext() for full context clearing with persistence and broadcast.
7626
7775
  */
7627
7776
  clearHistory() {
7628
7777
  this.conversationHistory = [];
7629
7778
  this.isCancelled = false;
7630
7779
  this.logger.info("Global conversation history cleared");
7631
7780
  }
7781
+ /**
7782
+ * Clears supervisor context completely:
7783
+ * - In-memory conversation history
7784
+ * - Persistent history in database
7785
+ * - Notifies all connected clients
7786
+ */
7787
+ clearContext() {
7788
+ this.conversationHistory = [];
7789
+ this.isCancelled = false;
7790
+ const chatHistoryService = this.getChatHistoryService?.();
7791
+ if (chatHistoryService) {
7792
+ chatHistoryService.clearSupervisorHistory();
7793
+ }
7794
+ const broadcaster = this.getMessageBroadcaster?.();
7795
+ if (broadcaster) {
7796
+ const clearNotification = JSON.stringify({
7797
+ type: "supervisor.context_cleared",
7798
+ payload: { timestamp: Date.now() }
7799
+ });
7800
+ broadcaster.broadcastToAll(clearNotification);
7801
+ }
7802
+ this.logger.info("Supervisor context cleared (in-memory, persistent, and clients notified)");
7803
+ }
7632
7804
  /**
7633
7805
  * Resets the cancellation state.
7634
7806
  * Call this before starting a new command to ensure previous cancellation doesn't affect it.
@@ -7864,8 +8036,9 @@ var DEFAULT_FIXTURES_PATH = join7(__dirname, "fixtures");
7864
8036
  var fixtureCache = /* @__PURE__ */ new Map();
7865
8037
  function loadFixture(name, customPath) {
7866
8038
  const cacheKey = `${customPath ?? "default"}:${name}`;
7867
- if (fixtureCache.has(cacheKey)) {
7868
- return fixtureCache.get(cacheKey);
8039
+ const cached = fixtureCache.get(cacheKey);
8040
+ if (cached) {
8041
+ return cached;
7869
8042
  }
7870
8043
  const fixturesDir = customPath ?? DEFAULT_FIXTURES_PATH;
7871
8044
  const filePath = join7(fixturesDir, `${name}.json`);
@@ -7901,20 +8074,14 @@ async function simulateStreaming(text2, delayMs = DEFAULT_TOKEN_DELAY_MS, onBloc
7901
8074
  const tokens = tokenize(text2);
7902
8075
  let accumulated = "";
7903
8076
  for (let i = 0; i < tokens.length; i++) {
7904
- accumulated += tokens[i];
7905
- const block = {
7906
- type: "text",
7907
- text: accumulated
7908
- };
8077
+ accumulated += tokens[i] ?? "";
8078
+ const block = createTextBlock(accumulated);
7909
8079
  onBlock([block], false);
7910
8080
  if (i < tokens.length - 1) {
7911
8081
  await sleep(delayMs);
7912
8082
  }
7913
8083
  }
7914
- const finalBlock = {
7915
- type: "text",
7916
- text: accumulated
7917
- };
8084
+ const finalBlock = createTextBlock(accumulated);
7918
8085
  onBlock([finalBlock], true);
7919
8086
  onComplete();
7920
8087
  }
@@ -7976,7 +8143,7 @@ var MockSupervisorAgent = class extends EventEmitter5 {
7976
8143
  /**
7977
8144
  * Executes a command (non-streaming).
7978
8145
  */
7979
- async execute(command, deviceId, _currentSessionId) {
8146
+ execute(command, deviceId, _currentSessionId) {
7980
8147
  this.logger.info({ command, deviceId }, "Mock supervisor execute");
7981
8148
  const response = this.getResponse(command);
7982
8149
  this.conversationHistory.push({ role: "user", content: command });
@@ -8571,8 +8738,10 @@ var TTSService = class {
8571
8738
  case "elevenlabs":
8572
8739
  result = await this.synthesizeElevenLabs(text2);
8573
8740
  break;
8574
- default:
8575
- throw new Error(`Unsupported TTS provider: ${this.config.provider}`);
8741
+ default: {
8742
+ const exhaustiveCheck = this.config.provider;
8743
+ throw new Error(`Unsupported TTS provider: ${exhaustiveCheck}`);
8744
+ }
8576
8745
  }
8577
8746
  const elapsed = Date.now() - startTime;
8578
8747
  this.logger.info(
@@ -8905,11 +9074,19 @@ function handleTunnelMessage(rawMessage, tunnelClient, messageBroadcaster, creat
8905
9074
  return;
8906
9075
  }
8907
9076
  const handler = handlers[messageType];
8908
- const parsedMessage = parseClientMessage(data);
8909
- if (!parsedMessage) {
8910
- logger.warn({ type: messageType }, "Failed to parse tunnel message");
9077
+ const parseResult = parseClientMessageWithErrors(data);
9078
+ if (!parseResult.success) {
9079
+ logger.warn(
9080
+ {
9081
+ type: messageType,
9082
+ errors: parseResult.errors,
9083
+ rawMessage: JSON.stringify(data).slice(0, 500)
9084
+ },
9085
+ "Failed to parse tunnel message - Zod validation failed"
9086
+ );
8911
9087
  return;
8912
9088
  }
9089
+ const parsedMessage = parseResult.data;
8913
9090
  handler(virtualSocket, parsedMessage).catch((error) => {
8914
9091
  logger.error(
8915
9092
  { error, type: messageType },
@@ -9097,6 +9274,29 @@ async function bootstrap() {
9097
9274
  const pendingSupervisorVoiceCommands = /* @__PURE__ */ new Map();
9098
9275
  const pendingAgentVoiceCommands = /* @__PURE__ */ new Map();
9099
9276
  const cancelledDuringTranscription = /* @__PURE__ */ new Set();
9277
+ const supervisorMessageAccumulator = {
9278
+ blocks: [],
9279
+ streamingMessageId: null,
9280
+ get() {
9281
+ return this.blocks;
9282
+ },
9283
+ getStreamingMessageId() {
9284
+ return this.streamingMessageId;
9285
+ },
9286
+ set(blocks) {
9287
+ this.blocks = blocks;
9288
+ },
9289
+ clear() {
9290
+ this.blocks = [];
9291
+ this.streamingMessageId = null;
9292
+ },
9293
+ accumulate(newBlocks) {
9294
+ if (this.blocks.length === 0 && newBlocks.length > 0) {
9295
+ this.streamingMessageId = randomUUID5();
9296
+ }
9297
+ accumulateBlocks(this.blocks, newBlocks);
9298
+ }
9299
+ };
9100
9300
  const expectedAuthKey = new AuthKey(env.WORKSTATION_AUTH_KEY);
9101
9301
  let messageBroadcaster = null;
9102
9302
  const supervisorAgent = env.MOCK_MODE ? new MockSupervisorAgent({
@@ -9109,7 +9309,8 @@ async function bootstrap() {
9109
9309
  workspacesRoot: env.WORKSPACES_ROOT,
9110
9310
  logger,
9111
9311
  getMessageBroadcaster: () => messageBroadcaster,
9112
- getChatHistoryService: () => chatHistoryService
9312
+ getChatHistoryService: () => chatHistoryService,
9313
+ getTerminateSession: () => terminateSession?.terminateAndBroadcast.bind(terminateSession) ?? null
9113
9314
  });
9114
9315
  if (env.MOCK_MODE) {
9115
9316
  logger.info("Mock Supervisor Agent initialized for screenshot automation");
@@ -9188,11 +9389,10 @@ async function bootstrap() {
9188
9389
  const syncMessage = message;
9189
9390
  const client = clientRegistry.getBySocket(socket) ?? (syncMessage.device_id ? clientRegistry.getByDeviceId(new DeviceId(syncMessage.device_id)) : void 0);
9190
9391
  const subscriptions2 = client ? client.getSubscriptions() : [];
9191
- const isLightweight = syncMessage.lightweight === true;
9192
9392
  const inMemorySessions = sessionManager.getSessionInfos();
9193
9393
  const persistedAgentSessions = chatHistoryService.getActiveAgentSessions();
9194
9394
  logger.debug(
9195
- { persistedAgentSessions, inMemoryCount: inMemorySessions.length, isLightweight },
9395
+ { persistedAgentSessions, inMemoryCount: inMemorySessions.length },
9196
9396
  "Sync: fetched sessions"
9197
9397
  );
9198
9398
  const inMemorySessionIds = new Set(
@@ -9217,119 +9417,14 @@ async function bootstrap() {
9217
9417
  };
9218
9418
  });
9219
9419
  const sessions2 = [...inMemorySessions, ...restoredAgentSessions];
9220
- if (isLightweight) {
9221
- const availableAgentsMap2 = getAvailableAgents();
9222
- const availableAgents2 = Array.from(availableAgentsMap2.values()).map(
9223
- (agent) => ({
9224
- name: agent.name,
9225
- base_type: agent.baseType,
9226
- description: agent.description,
9227
- is_alias: agent.isAlias
9228
- })
9229
- );
9230
- const hiddenBaseTypes2 = getDisabledBaseAgents();
9231
- const workspacesList2 = await workspaceDiscovery.listWorkspaces();
9232
- const workspaces2 = await Promise.all(
9233
- workspacesList2.map(async (ws) => {
9234
- const projects = await workspaceDiscovery.listProjects(ws.name);
9235
- return {
9236
- name: ws.name,
9237
- projects: projects.map((p) => ({
9238
- name: p.name,
9239
- is_git_repo: p.isGitRepo,
9240
- default_branch: p.defaultBranch
9241
- }))
9242
- };
9243
- })
9244
- );
9245
- const executingStates2 = {};
9246
- for (const session of sessions2) {
9247
- if (session.session_type === "cursor" || session.session_type === "claude" || session.session_type === "opencode") {
9248
- executingStates2[session.session_id] = agentSessionManager.isExecuting(
9249
- session.session_id
9250
- );
9251
- }
9252
- }
9253
- const supervisorIsExecuting2 = supervisorAgent.isProcessing();
9254
- logger.info(
9255
- {
9256
- totalSessions: sessions2.length,
9257
- isLightweight: true,
9258
- availableAgentsCount: availableAgents2.length,
9259
- workspacesCount: workspaces2.length,
9260
- supervisorIsExecuting: supervisorIsExecuting2
9261
- },
9262
- "Sync: sending lightweight state to client (no histories)"
9263
- );
9264
- const syncStateMessage2 = JSON.stringify({
9265
- type: "sync.state",
9266
- id: syncMessage.id,
9267
- payload: {
9268
- sessions: sessions2,
9269
- subscriptions: subscriptions2,
9270
- availableAgents: availableAgents2,
9271
- hiddenBaseTypes: hiddenBaseTypes2,
9272
- workspaces: workspaces2,
9273
- supervisorIsExecuting: supervisorIsExecuting2,
9274
- executingStates: executingStates2
9275
- // Omit: supervisorHistory, agentHistories, currentStreamingBlocks
9276
- }
9277
- });
9278
- sendToDevice(socket, syncMessage.device_id, syncStateMessage2);
9279
- return Promise.resolve();
9280
- }
9281
9420
  const supervisorHistoryRaw = chatHistoryService.getSupervisorHistory();
9282
- const supervisorHistory = await Promise.all(
9283
- supervisorHistoryRaw.map(async (msg) => ({
9284
- sequence: msg.sequence,
9285
- role: msg.role,
9286
- content: msg.content,
9287
- content_blocks: await chatHistoryService.enrichBlocksWithAudio(
9288
- msg.contentBlocks,
9289
- msg.audioOutputPath,
9290
- msg.audioInputPath,
9291
- false
9292
- // Don't include audio in sync.state
9293
- ),
9294
- createdAt: msg.createdAt.toISOString()
9295
- }))
9296
- );
9297
- if (supervisorHistory.length > 0) {
9298
- const historyForAgent = supervisorHistory.filter((msg) => msg.role === "user" || msg.role === "assistant").map((msg) => ({
9421
+ if (supervisorHistoryRaw.length > 0) {
9422
+ const historyForAgent = supervisorHistoryRaw.filter((msg) => msg.role === "user" || msg.role === "assistant").map((msg) => ({
9299
9423
  role: msg.role,
9300
9424
  content: msg.content
9301
9425
  }));
9302
9426
  supervisorAgent.restoreHistory(historyForAgent);
9303
9427
  }
9304
- const agentSessionIds = sessions2.filter(
9305
- (s) => s.session_type === "cursor" || s.session_type === "claude" || s.session_type === "opencode"
9306
- ).map((s) => s.session_id);
9307
- const agentHistoriesMap = chatHistoryService.getAllAgentHistories(agentSessionIds);
9308
- const agentHistories = {};
9309
- const historyEntries = Array.from(agentHistoriesMap.entries());
9310
- const processedHistories = await Promise.all(
9311
- historyEntries.map(async ([sessionId, history]) => {
9312
- const enrichedHistory = await Promise.all(
9313
- history.map(async (msg) => ({
9314
- sequence: msg.sequence,
9315
- role: msg.role,
9316
- content: msg.content,
9317
- content_blocks: await chatHistoryService.enrichBlocksWithAudio(
9318
- msg.contentBlocks,
9319
- msg.audioOutputPath,
9320
- msg.audioInputPath,
9321
- false
9322
- // Don't include audio in sync.state
9323
- ),
9324
- createdAt: msg.createdAt.toISOString()
9325
- }))
9326
- );
9327
- return { sessionId, history: enrichedHistory };
9328
- })
9329
- );
9330
- for (const { sessionId, history } of processedHistories) {
9331
- agentHistories[sessionId] = history;
9332
- }
9333
9428
  const availableAgentsMap = getAvailableAgents();
9334
9429
  const availableAgents = Array.from(availableAgentsMap.values()).map(
9335
9430
  (agent) => ({
@@ -9362,31 +9457,23 @@ async function bootstrap() {
9362
9457
  );
9363
9458
  }
9364
9459
  }
9365
- const currentStreamingBlocks = {};
9366
- for (const [sessionId, isExecuting] of Object.entries(executingStates)) {
9367
- if (isExecuting) {
9368
- const blocks = agentMessageAccumulator.get(sessionId);
9369
- if (blocks && blocks.length > 0) {
9370
- currentStreamingBlocks[sessionId] = blocks;
9371
- }
9460
+ const supervisorIsExecuting = supervisorAgent.isProcessing();
9461
+ let currentStreamingBlocks;
9462
+ if (supervisorIsExecuting) {
9463
+ const blocks = supervisorMessageAccumulator.get();
9464
+ if (blocks && blocks.length > 0) {
9465
+ currentStreamingBlocks = blocks;
9372
9466
  }
9373
9467
  }
9374
- const supervisorIsExecuting = supervisorAgent.isProcessing();
9375
9468
  logger.info(
9376
9469
  {
9377
9470
  totalSessions: sessions2.length,
9378
- sessionTypes: sessions2.map((s) => ({
9379
- id: s.session_id,
9380
- type: s.session_type
9381
- })),
9382
- agentHistoriesCount: Object.keys(agentHistories).length,
9383
9471
  availableAgentsCount: availableAgents.length,
9384
9472
  workspacesCount: workspaces.length,
9385
9473
  supervisorIsExecuting,
9386
- executingStates,
9387
- streamingBlocksCount: Object.keys(currentStreamingBlocks).length
9474
+ hasStreamingBlocks: !!currentStreamingBlocks
9388
9475
  },
9389
- "Sync: sending state to client"
9476
+ "Sync: sending state to client (v1.13 - no histories)"
9390
9477
  );
9391
9478
  const syncStateMessage = JSON.stringify({
9392
9479
  type: "sync.state",
@@ -9394,14 +9481,15 @@ async function bootstrap() {
9394
9481
  payload: {
9395
9482
  sessions: sessions2,
9396
9483
  subscriptions: subscriptions2,
9397
- supervisorHistory,
9398
- agentHistories,
9399
9484
  availableAgents,
9400
9485
  hiddenBaseTypes,
9401
9486
  workspaces,
9402
9487
  supervisorIsExecuting,
9403
9488
  executingStates,
9404
- currentStreamingBlocks: Object.keys(currentStreamingBlocks).length > 0 ? currentStreamingBlocks : void 0
9489
+ // Only include supervisor streaming blocks for mid-stream join
9490
+ currentStreamingBlocks
9491
+ // Protocol v1.13: No supervisorHistory, agentHistories
9492
+ // Clients use history.request for on-demand loading
9405
9493
  }
9406
9494
  });
9407
9495
  sendToDevice(socket, syncMessage.device_id, syncStateMessage);
@@ -9463,7 +9551,7 @@ async function bootstrap() {
9463
9551
  let commandText;
9464
9552
  const messageId = commandMessage.payload.message_id;
9465
9553
  if (messageBroadcaster) {
9466
- const ackMessageId = messageId || commandMessage.id;
9554
+ const ackMessageId = messageId ?? commandMessage.id;
9467
9555
  const ackMessage = {
9468
9556
  type: "message.ack",
9469
9557
  payload: {
@@ -9496,7 +9584,7 @@ async function bootstrap() {
9496
9584
  const errorEvent = {
9497
9585
  type: "supervisor.transcription",
9498
9586
  payload: {
9499
- text: "",
9587
+ transcription: "",
9500
9588
  error: "Voice transcription not available - STT service not configured",
9501
9589
  message_id: messageId,
9502
9590
  timestamp: Date.now()
@@ -9546,7 +9634,7 @@ async function bootstrap() {
9546
9634
  const transcriptionEvent = {
9547
9635
  type: "supervisor.transcription",
9548
9636
  payload: {
9549
- text: commandText,
9637
+ transcription: commandText,
9550
9638
  language: transcriptionResult.language,
9551
9639
  duration: transcriptionResult.duration,
9552
9640
  message_id: messageId,
@@ -9575,7 +9663,7 @@ async function bootstrap() {
9575
9663
  const errorEvent = {
9576
9664
  type: "supervisor.transcription",
9577
9665
  payload: {
9578
- text: "",
9666
+ transcription: "",
9579
9667
  error: error instanceof Error ? error.message : "Transcription failed",
9580
9668
  message_id: messageId,
9581
9669
  timestamp: Date.now(),
@@ -9721,13 +9809,7 @@ async function bootstrap() {
9721
9809
  const tunnelClient2 = clearMessage.device_id ? clientRegistry.getByDeviceId(new DeviceId(clearMessage.device_id)) : void 0;
9722
9810
  const isAuthenticated = directClient?.isAuthenticated ?? tunnelClient2?.isAuthenticated;
9723
9811
  if (isAuthenticated) {
9724
- supervisorAgent.clearHistory();
9725
- chatHistoryService.clearSupervisorHistory();
9726
- const clearNotification = JSON.stringify({
9727
- type: "supervisor.context_cleared",
9728
- payload: { timestamp: Date.now() }
9729
- });
9730
- broadcaster.broadcastToAll(clearNotification);
9812
+ supervisorAgent.clearContext();
9731
9813
  sendToDevice(
9732
9814
  socket,
9733
9815
  clearMessage.device_id,
@@ -9855,45 +9937,29 @@ async function bootstrap() {
9855
9937
  },
9856
9938
  "Client subscribed to agent session"
9857
9939
  );
9858
- const history = chatHistoryService.getAgentHistory(sessionId, 50);
9859
- const enrichedHistory = await Promise.all(
9860
- history.map(async (msg) => ({
9861
- id: msg.id,
9862
- sequence: msg.sequence,
9863
- role: msg.role,
9864
- content: msg.content,
9865
- content_blocks: await chatHistoryService.enrichBlocksWithAudio(
9866
- msg.contentBlocks,
9867
- msg.audioOutputPath,
9868
- msg.audioInputPath,
9869
- false
9870
- // Don't include audio in subscription response
9871
- ),
9872
- createdAt: msg.createdAt.toISOString()
9873
- }))
9874
- );
9875
9940
  const isExecuting = agentSessionManager.isExecuting(sessionId);
9876
9941
  const currentStreamingBlocks = agentMessageAccumulator.get(sessionId) ?? [];
9942
+ const streamingMessageId = currentStreamingBlocks.length > 0 ? agentStreamingMessageIds.get(sessionId) : void 0;
9877
9943
  sendToDevice(
9878
9944
  socket,
9879
9945
  subscribeMessage.device_id,
9880
9946
  JSON.stringify({
9881
9947
  type: "session.subscribed",
9882
9948
  session_id: sessionId,
9883
- history: enrichedHistory,
9884
9949
  is_executing: isExecuting,
9885
- current_streaming_blocks: currentStreamingBlocks.length > 0 ? currentStreamingBlocks : void 0
9950
+ current_streaming_blocks: currentStreamingBlocks.length > 0 ? currentStreamingBlocks : void 0,
9951
+ streaming_message_id: streamingMessageId
9886
9952
  })
9887
9953
  );
9888
9954
  logger.debug(
9889
9955
  {
9890
9956
  deviceId: client.deviceId.value,
9891
9957
  sessionId,
9892
- historyCount: enrichedHistory.length,
9893
9958
  isExecuting,
9894
- streamingBlocksCount: currentStreamingBlocks.length
9959
+ streamingBlocksCount: currentStreamingBlocks.length,
9960
+ streamingMessageId
9895
9961
  },
9896
- "Sent agent session history on subscribe"
9962
+ "Agent session subscribed (v1.13 - use history.request for messages)"
9897
9963
  );
9898
9964
  } else {
9899
9965
  const result = subscriptionService.subscribe(
@@ -9950,7 +10016,7 @@ async function bootstrap() {
9950
10016
  const sessionId = execMessage.session_id;
9951
10017
  const messageId = execMessage.payload.message_id;
9952
10018
  if (deviceId && messageBroadcaster) {
9953
- const ackMessageId = messageId || execMessage.id;
10019
+ const ackMessageId = messageId ?? execMessage.id;
9954
10020
  const ackMessage = {
9955
10021
  type: "message.ack",
9956
10022
  payload: {
@@ -10022,7 +10088,7 @@ async function bootstrap() {
10022
10088
  type: "session.transcription",
10023
10089
  session_id: sessionId,
10024
10090
  payload: {
10025
- text: transcribedText,
10091
+ transcription: transcribedText,
10026
10092
  language: transcriptionResult.language,
10027
10093
  duration: transcriptionResult.duration,
10028
10094
  message_id: messageId,
@@ -10115,7 +10181,7 @@ async function bootstrap() {
10115
10181
  type: "session.transcription",
10116
10182
  session_id: sessionId,
10117
10183
  payload: {
10118
- text: "",
10184
+ transcription: "",
10119
10185
  error: error instanceof Error ? error.message : "Transcription failed",
10120
10186
  message_id: messageId,
10121
10187
  timestamp: Date.now()
@@ -10441,18 +10507,22 @@ async function bootstrap() {
10441
10507
  "history.request": async (socket, message) => {
10442
10508
  const historyRequest = message;
10443
10509
  const sessionId = historyRequest.payload?.session_id;
10510
+ const beforeSequence = historyRequest.payload?.before_sequence;
10511
+ const limit = historyRequest.payload?.limit;
10444
10512
  const isSupervisor = !sessionId;
10445
10513
  logger.debug(
10446
- { sessionId, isSupervisor, requestId: historyRequest.id },
10447
- "History request received"
10514
+ { sessionId, isSupervisor, beforeSequence, limit, requestId: historyRequest.id },
10515
+ "Paginated history request received"
10448
10516
  );
10449
10517
  try {
10450
10518
  if (isSupervisor) {
10451
- const supervisorHistoryRaw = chatHistoryService.getSupervisorHistory();
10519
+ const result = chatHistoryService.getSupervisorHistoryPaginated({
10520
+ beforeSequence,
10521
+ limit
10522
+ });
10452
10523
  const supervisorHistory = await Promise.all(
10453
- supervisorHistoryRaw.map(async (msg) => ({
10524
+ result.messages.map(async (msg) => ({
10454
10525
  message_id: msg.id,
10455
- // Include message ID for audio.request
10456
10526
  sequence: msg.sequence,
10457
10527
  role: msg.role,
10458
10528
  content: msg.content,
@@ -10461,32 +10531,54 @@ async function bootstrap() {
10461
10531
  msg.audioOutputPath,
10462
10532
  msg.audioInputPath,
10463
10533
  false
10464
- // Don't include audio in history response
10465
10534
  ),
10466
10535
  createdAt: msg.createdAt.toISOString()
10467
10536
  }))
10468
10537
  );
10469
10538
  const isExecuting = supervisorAgent.isProcessing();
10539
+ let currentStreamingBlocks;
10540
+ let streamingMessageId;
10541
+ if (isExecuting && !beforeSequence) {
10542
+ const blocks = supervisorMessageAccumulator.get();
10543
+ if (blocks && blocks.length > 0) {
10544
+ currentStreamingBlocks = blocks;
10545
+ streamingMessageId = supervisorMessageAccumulator.getStreamingMessageId() ?? void 0;
10546
+ }
10547
+ }
10470
10548
  socket.send(
10471
10549
  JSON.stringify({
10472
10550
  type: "history.response",
10473
10551
  id: historyRequest.id,
10474
10552
  payload: {
10475
10553
  session_id: null,
10476
- // Indicates supervisor
10477
10554
  history: supervisorHistory,
10478
- is_executing: isExecuting
10555
+ has_more: result.hasMore,
10556
+ oldest_sequence: result.oldestSequence,
10557
+ newest_sequence: result.newestSequence,
10558
+ is_executing: isExecuting,
10559
+ current_streaming_blocks: currentStreamingBlocks,
10560
+ streaming_message_id: streamingMessageId
10479
10561
  }
10480
10562
  })
10481
10563
  );
10482
10564
  logger.debug(
10483
- { messageCount: supervisorHistory.length, isExecuting },
10484
- "Supervisor history sent"
10565
+ {
10566
+ messageCount: supervisorHistory.length,
10567
+ hasMore: result.hasMore,
10568
+ oldestSeq: result.oldestSequence,
10569
+ newestSeq: result.newestSequence,
10570
+ isExecuting
10571
+ },
10572
+ "Paginated supervisor history sent"
10485
10573
  );
10486
10574
  } else {
10487
- const history = chatHistoryService.getAgentHistory(sessionId);
10575
+ const result = chatHistoryService.getAgentHistoryPaginated(sessionId, {
10576
+ beforeSequence,
10577
+ limit
10578
+ });
10488
10579
  const enrichedHistory = await Promise.all(
10489
- history.map(async (msg) => ({
10580
+ result.messages.map(async (msg) => ({
10581
+ message_id: msg.id,
10490
10582
  sequence: msg.sequence,
10491
10583
  role: msg.role,
10492
10584
  content: msg.content,
@@ -10495,17 +10587,18 @@ async function bootstrap() {
10495
10587
  msg.audioOutputPath,
10496
10588
  msg.audioInputPath,
10497
10589
  false
10498
- // Don't include audio in history response
10499
10590
  ),
10500
10591
  createdAt: msg.createdAt.toISOString()
10501
10592
  }))
10502
10593
  );
10503
10594
  const isExecuting = agentSessionManager.isExecuting(sessionId);
10504
10595
  let currentStreamingBlocks;
10505
- if (isExecuting) {
10596
+ let streamingMessageId;
10597
+ if (isExecuting && !beforeSequence) {
10506
10598
  const blocks = agentMessageAccumulator.get(sessionId);
10507
10599
  if (blocks && blocks.length > 0) {
10508
10600
  currentStreamingBlocks = blocks;
10601
+ streamingMessageId = agentStreamingMessageIds.get(sessionId);
10509
10602
  }
10510
10603
  }
10511
10604
  socket.send(
@@ -10515,13 +10608,17 @@ async function bootstrap() {
10515
10608
  payload: {
10516
10609
  session_id: sessionId,
10517
10610
  history: enrichedHistory,
10611
+ has_more: result.hasMore,
10612
+ oldest_sequence: result.oldestSequence,
10613
+ newest_sequence: result.newestSequence,
10518
10614
  is_executing: isExecuting,
10519
- current_streaming_blocks: currentStreamingBlocks
10615
+ current_streaming_blocks: currentStreamingBlocks,
10616
+ streaming_message_id: streamingMessageId
10520
10617
  }
10521
10618
  })
10522
10619
  );
10523
10620
  logger.debug(
10524
- { sessionId, messageCount: enrichedHistory.length, isExecuting },
10621
+ { sessionId, messageCount: enrichedHistory.length, isExecuting, streamingMessageId },
10525
10622
  "Agent session history sent"
10526
10623
  );
10527
10624
  }
@@ -10555,7 +10652,7 @@ async function bootstrap() {
10555
10652
  id: audioRequest.id,
10556
10653
  payload: {
10557
10654
  message_id,
10558
- audio: audioBase64
10655
+ audio_base64: audioBase64
10559
10656
  }
10560
10657
  })
10561
10658
  );
@@ -10700,6 +10797,7 @@ async function bootstrap() {
10700
10797
  });
10701
10798
  terminateSession = new TerminateSessionUseCase({
10702
10799
  sessionManager,
10800
+ agentSessionManager,
10703
10801
  messageBroadcaster,
10704
10802
  chatHistoryService,
10705
10803
  logger
@@ -10712,6 +10810,18 @@ async function bootstrap() {
10712
10810
  });
10713
10811
  const broadcaster = messageBroadcaster;
10714
10812
  const agentMessageAccumulator = /* @__PURE__ */ new Map();
10813
+ const agentStreamingMessageIds = /* @__PURE__ */ new Map();
10814
+ const getOrCreateAgentStreamingMessageId = (sessionId) => {
10815
+ let messageId = agentStreamingMessageIds.get(sessionId);
10816
+ if (!messageId) {
10817
+ messageId = randomUUID5();
10818
+ agentStreamingMessageIds.set(sessionId, messageId);
10819
+ }
10820
+ return messageId;
10821
+ };
10822
+ const clearAgentStreamingMessageId = (sessionId) => {
10823
+ agentStreamingMessageIds.delete(sessionId);
10824
+ };
10715
10825
  agentSessionManager.on(
10716
10826
  "blocks",
10717
10827
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -10784,9 +10894,11 @@ async function bootstrap() {
10784
10894
  const accumulatedBlocks = agentMessageAccumulator.get(sessionId) ?? [];
10785
10895
  const mergedBlocks = mergeToolBlocks(accumulatedBlocks);
10786
10896
  const fullAccumulatedText = mergedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
10897
+ const streamingMessageId = getOrCreateAgentStreamingMessageId(sessionId);
10787
10898
  const outputEvent = {
10788
10899
  type: "session.output",
10789
10900
  session_id: sessionId,
10901
+ streaming_message_id: streamingMessageId,
10790
10902
  payload: {
10791
10903
  content_type: "agent",
10792
10904
  content: fullAccumulatedText,
@@ -10801,6 +10913,9 @@ async function bootstrap() {
10801
10913
  sessionId,
10802
10914
  JSON.stringify(outputEvent)
10803
10915
  );
10916
+ if (isComplete) {
10917
+ clearAgentStreamingMessageId(sessionId);
10918
+ }
10804
10919
  if (isComplete && fullTextContent.length > 0) {
10805
10920
  const pendingVoiceCommand = pendingAgentVoiceCommands.get(sessionId);
10806
10921
  if (pendingVoiceCommand && ttsService) {
@@ -10850,7 +10965,7 @@ async function bootstrap() {
10850
10965
  session_id: sessionId,
10851
10966
  payload: {
10852
10967
  text: textForTTS,
10853
- audio: audioBase64,
10968
+ audio_base64: audioBase64,
10854
10969
  audio_format: "mp3",
10855
10970
  duration: ttsResult.duration,
10856
10971
  message_id: pendingMessageId,
@@ -10876,7 +10991,6 @@ async function bootstrap() {
10876
10991
  }
10877
10992
  }
10878
10993
  );
10879
- let supervisorBlockAccumulator = [];
10880
10994
  supervisorAgent.on(
10881
10995
  "blocks",
10882
10996
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -10891,17 +11005,19 @@ async function bootstrap() {
10891
11005
  { deviceId, blockCount: blocks.length, isComplete },
10892
11006
  "Ignoring supervisor blocks - execution was cancelled"
10893
11007
  );
10894
- supervisorBlockAccumulator = [];
11008
+ supervisorMessageAccumulator.clear();
10895
11009
  return;
10896
11010
  }
10897
11011
  const persistableBlocks = blocks.filter((b) => b.block_type !== "status");
10898
11012
  if (persistableBlocks.length > 0) {
10899
- accumulateBlocks(supervisorBlockAccumulator, persistableBlocks);
11013
+ supervisorMessageAccumulator.accumulate(persistableBlocks);
10900
11014
  }
10901
- const mergedBlocks = mergeToolBlocks(supervisorBlockAccumulator);
11015
+ const mergedBlocks = mergeToolBlocks(supervisorMessageAccumulator.get());
10902
11016
  const textContent = mergedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
11017
+ const streamingMessageId = supervisorMessageAccumulator.getStreamingMessageId();
10903
11018
  const outputEvent = {
10904
11019
  type: "supervisor.output",
11020
+ streaming_message_id: streamingMessageId,
10905
11021
  payload: {
10906
11022
  content_type: "supervisor",
10907
11023
  content: textContent,
@@ -10913,7 +11029,7 @@ async function bootstrap() {
10913
11029
  const message = JSON.stringify(outputEvent);
10914
11030
  broadcaster.broadcastToAll(message);
10915
11031
  if (isComplete) {
10916
- supervisorBlockAccumulator = [];
11032
+ supervisorMessageAccumulator.clear();
10917
11033
  }
10918
11034
  if (isComplete && finalOutput && finalOutput.length > 0) {
10919
11035
  chatHistoryService.saveSupervisorMessage(
@@ -10963,7 +11079,7 @@ async function bootstrap() {
10963
11079
  type: "supervisor.voice_output",
10964
11080
  payload: {
10965
11081
  text: textForTTS,
10966
- audio: audioBase64,
11082
+ audio_base64: audioBase64,
10967
11083
  audio_format: "mp3",
10968
11084
  duration: ttsResult.duration,
10969
11085
  message_id: pendingMessageId,
@@ -11123,23 +11239,52 @@ async function bootstrap() {
11123
11239
  } catch (error) {
11124
11240
  logger.error({ error }, "Failed to connect to tunnel (will retry)");
11125
11241
  }
11242
+ const SHUTDOWN_TIMEOUT_MS = 1e4;
11243
+ let isShuttingDown = false;
11126
11244
  const shutdown = async (signal) => {
11245
+ if (isShuttingDown) {
11246
+ logger.warn({ signal }, "Shutdown already in progress, forcing exit");
11247
+ process.exit(1);
11248
+ }
11249
+ isShuttingDown = true;
11127
11250
  logger.info({ signal }, "Shutdown signal received");
11251
+ const forceExitTimeout = setTimeout(() => {
11252
+ logger.error("Graceful shutdown timeout exceeded, forcing exit");
11253
+ process.exit(1);
11254
+ }, SHUTDOWN_TIMEOUT_MS);
11128
11255
  try {
11129
11256
  logger.info("Disconnecting from tunnel...");
11130
11257
  tunnelClient.disconnect();
11131
11258
  logger.info("Cleaning up agent sessions...");
11132
11259
  agentSessionManager.cleanup();
11133
11260
  logger.info("Terminating all sessions...");
11134
- await sessionManager.terminateAll();
11135
- logger.info("All sessions terminated");
11261
+ const terminatePromise = sessionManager.terminateAll();
11262
+ const sessionTimeoutPromise = new Promise((_, reject) => {
11263
+ setTimeout(() => reject(new Error("Session termination timeout")), 5e3);
11264
+ });
11265
+ try {
11266
+ await Promise.race([terminatePromise, sessionTimeoutPromise]);
11267
+ logger.info("All sessions terminated");
11268
+ } catch (error) {
11269
+ logger.warn({ error }, "Session termination timed out, continuing shutdown");
11270
+ }
11136
11271
  logger.info("Closing HTTP server...");
11137
- await app.close();
11272
+ const closePromise = app.close();
11273
+ const closeTimeoutPromise = new Promise((_, reject) => {
11274
+ setTimeout(() => reject(new Error("HTTP server close timeout")), 2e3);
11275
+ });
11276
+ try {
11277
+ await Promise.race([closePromise, closeTimeoutPromise]);
11278
+ } catch (error) {
11279
+ logger.warn({ error }, "HTTP server close timed out, continuing shutdown");
11280
+ }
11138
11281
  logger.info("Closing database...");
11139
11282
  closeDatabase();
11283
+ clearTimeout(forceExitTimeout);
11140
11284
  logger.info("Shutdown complete");
11141
11285
  process.exit(0);
11142
11286
  } catch (error) {
11287
+ clearTimeout(forceExitTimeout);
11143
11288
  logger.error({ error }, "Error during shutdown");
11144
11289
  process.exit(1);
11145
11290
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiflis-io/tiflis-code-workstation",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Workstation server for tiflis-code - manages agent sessions and terminal access",
5
5
  "author": "Roman Barinov <rbarinov@gmail.com>",
6
6
  "license": "FSL-1.1-NC",