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

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 +307 -65
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -94,7 +94,7 @@ var EnvSchema = z.object({
94
94
  // ─────────────────────────────────────────────────────────────
95
95
  // Speech-to-Text (STT) Configuration
96
96
  // ─────────────────────────────────────────────────────────────
97
- STT_PROVIDER: z.enum(["openai", "elevenlabs", "deepgram"]).default("openai"),
97
+ STT_PROVIDER: z.enum(["openai", "elevenlabs", "deepgram", "local"]).default("openai"),
98
98
  STT_API_KEY: z.string().optional(),
99
99
  STT_MODEL: z.string().default("whisper-1"),
100
100
  STT_BASE_URL: z.string().url().optional(),
@@ -102,7 +102,7 @@ var EnvSchema = z.object({
102
102
  // ─────────────────────────────────────────────────────────────
103
103
  // Text-to-Speech (TTS) Configuration
104
104
  // ─────────────────────────────────────────────────────────────
105
- TTS_PROVIDER: z.enum(["openai", "elevenlabs"]).default("openai"),
105
+ TTS_PROVIDER: z.enum(["openai", "elevenlabs", "local"]).default("openai"),
106
106
  TTS_API_KEY: z.string().optional(),
107
107
  TTS_MODEL: z.string().default("tts-1"),
108
108
  TTS_VOICE: z.string().default("alloy"),
@@ -247,18 +247,18 @@ function getProtocolVersion() {
247
247
  return `${PROTOCOL_VERSION.major}.${PROTOCOL_VERSION.minor}.${PROTOCOL_VERSION.patch}`;
248
248
  }
249
249
  var CONNECTION_TIMING = {
250
- /** How often to send ping to tunnel (15 seconds - keeps connection alive through proxies) */
251
- PING_INTERVAL_MS: 15e3,
252
- /** Max time to wait for pong before considering connection stale (30 seconds) */
253
- PONG_TIMEOUT_MS: 3e4,
254
- /** Max time to wait for registration response (15 seconds) */
255
- REGISTRATION_TIMEOUT_MS: 15e3,
256
- /** Minimum reconnect delay (1 second) */
257
- RECONNECT_DELAY_MIN_MS: 1e3,
258
- /** Maximum reconnect delay (30 seconds) */
259
- RECONNECT_DELAY_MAX_MS: 3e4,
260
- /** Interval for checking timed-out client connections (10 seconds) */
261
- CLIENT_TIMEOUT_CHECK_INTERVAL_MS: 1e4
250
+ /** How often to send ping to tunnel (5 seconds - fast liveness detection) */
251
+ PING_INTERVAL_MS: 5e3,
252
+ /** Max time to wait for pong before considering connection stale (10 seconds) */
253
+ PONG_TIMEOUT_MS: 1e4,
254
+ /** Max time to wait for registration response (10 seconds) */
255
+ REGISTRATION_TIMEOUT_MS: 1e4,
256
+ /** Minimum reconnect delay (500ms - fast first retry) */
257
+ RECONNECT_DELAY_MIN_MS: 500,
258
+ /** Maximum reconnect delay (5 seconds - don't wait too long) */
259
+ RECONNECT_DELAY_MAX_MS: 5e3,
260
+ /** Interval for checking timed-out client connections (5 seconds - faster cleanup) */
261
+ CLIENT_TIMEOUT_CHECK_INTERVAL_MS: 5e3
262
262
  };
263
263
  var SESSION_CONFIG = {
264
264
  /** Maximum number of concurrent agent sessions */
@@ -1895,13 +1895,37 @@ import { join as join4 } from "path";
1895
1895
  import { execSync } from "child_process";
1896
1896
  var FileSystemWorkspaceDiscovery = class {
1897
1897
  workspacesRoot;
1898
+ cacheTtlMs;
1899
+ /** Cache for workspace list */
1900
+ workspacesCache = null;
1901
+ /** Cache for projects by workspace name */
1902
+ projectsCache = /* @__PURE__ */ new Map();
1898
1903
  constructor(config2) {
1899
1904
  this.workspacesRoot = config2.workspacesRoot;
1905
+ this.cacheTtlMs = config2.cacheTtlMs ?? 3e4;
1906
+ }
1907
+ /**
1908
+ * Checks if a cache entry is still valid.
1909
+ */
1910
+ isCacheValid(entry) {
1911
+ if (!entry) return false;
1912
+ return Date.now() - entry.timestamp < this.cacheTtlMs;
1913
+ }
1914
+ /**
1915
+ * Invalidates all caches. Call when workspace structure changes.
1916
+ */
1917
+ invalidateCache() {
1918
+ this.workspacesCache = null;
1919
+ this.projectsCache.clear();
1900
1920
  }
1901
1921
  /**
1902
1922
  * Lists all workspaces in the workspaces root.
1923
+ * Results are cached for faster subsequent calls.
1903
1924
  */
1904
1925
  async listWorkspaces() {
1926
+ if (this.isCacheValid(this.workspacesCache)) {
1927
+ return this.workspacesCache.data;
1928
+ }
1905
1929
  const entries = await readdir(this.workspacesRoot, { withFileTypes: true });
1906
1930
  const workspaces = [];
1907
1931
  for (const entry of entries) {
@@ -1915,12 +1939,22 @@ var FileSystemWorkspaceDiscovery = class {
1915
1939
  });
1916
1940
  }
1917
1941
  }
1918
- return workspaces.sort((a, b) => a.name.localeCompare(b.name));
1942
+ const result = workspaces.sort((a, b) => a.name.localeCompare(b.name));
1943
+ this.workspacesCache = {
1944
+ data: result,
1945
+ timestamp: Date.now()
1946
+ };
1947
+ return result;
1919
1948
  }
1920
1949
  /**
1921
1950
  * Lists all projects in a workspace.
1951
+ * Results are cached for faster subsequent calls.
1922
1952
  */
1923
1953
  async listProjects(workspace) {
1954
+ const cached = this.projectsCache.get(workspace);
1955
+ if (this.isCacheValid(cached)) {
1956
+ return cached.data;
1957
+ }
1924
1958
  const workspacePath = join4(this.workspacesRoot, workspace);
1925
1959
  if (!await this.pathExists(workspacePath)) {
1926
1960
  return [];
@@ -1949,7 +1983,12 @@ var FileSystemWorkspaceDiscovery = class {
1949
1983
  });
1950
1984
  }
1951
1985
  }
1952
- return projects.sort((a, b) => a.name.localeCompare(b.name));
1986
+ const result = projects.sort((a, b) => a.name.localeCompare(b.name));
1987
+ this.projectsCache.set(workspace, {
1988
+ data: result,
1989
+ timestamp: Date.now()
1990
+ });
1991
+ return result;
1953
1992
  }
1954
1993
  /**
1955
1994
  * Gets information about a specific project.
@@ -3141,6 +3180,12 @@ var HeadlessAgentExecutor = class extends EventEmitter {
3141
3180
 
3142
3181
  // src/domain/value-objects/content-block.ts
3143
3182
  import { randomUUID } from "crypto";
3183
+ function isTextBlock(block) {
3184
+ return block.block_type === "text";
3185
+ }
3186
+ function isToolBlock(block) {
3187
+ return block.block_type === "tool";
3188
+ }
3144
3189
  function createTextBlock(content) {
3145
3190
  return {
3146
3191
  id: randomUUID(),
@@ -3219,6 +3264,103 @@ function createVoiceOutputBlock(text2, options) {
3219
3264
  }
3220
3265
  };
3221
3266
  }
3267
+ function mergeToolBlocks(blocks) {
3268
+ const toolBlocksByUseId = /* @__PURE__ */ new Map();
3269
+ for (const block of blocks) {
3270
+ if (isToolBlock(block) && block.metadata.tool_use_id) {
3271
+ const toolUseId = block.metadata.tool_use_id;
3272
+ const existing = toolBlocksByUseId.get(toolUseId);
3273
+ if (existing) {
3274
+ const mergedStatus = getMergedToolStatus(
3275
+ existing.metadata.tool_status,
3276
+ block.metadata.tool_status
3277
+ );
3278
+ const mergedBlock = {
3279
+ id: existing.id,
3280
+ // Keep original ID
3281
+ block_type: "tool",
3282
+ content: block.metadata.tool_name || existing.content,
3283
+ metadata: {
3284
+ tool_name: block.metadata.tool_name || existing.metadata.tool_name,
3285
+ 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,
3288
+ tool_status: mergedStatus
3289
+ }
3290
+ };
3291
+ toolBlocksByUseId.set(toolUseId, mergedBlock);
3292
+ } else {
3293
+ toolBlocksByUseId.set(toolUseId, block);
3294
+ }
3295
+ }
3296
+ }
3297
+ const seenToolUseIds = /* @__PURE__ */ new Set();
3298
+ const result = [];
3299
+ for (const block of blocks) {
3300
+ if (isToolBlock(block) && block.metadata.tool_use_id) {
3301
+ const toolUseId = block.metadata.tool_use_id;
3302
+ if (!seenToolUseIds.has(toolUseId)) {
3303
+ seenToolUseIds.add(toolUseId);
3304
+ const mergedBlock = toolBlocksByUseId.get(toolUseId);
3305
+ if (mergedBlock) {
3306
+ result.push(mergedBlock);
3307
+ }
3308
+ }
3309
+ } else {
3310
+ result.push(block);
3311
+ }
3312
+ }
3313
+ return result;
3314
+ }
3315
+ function getMergedToolStatus(status1, status2) {
3316
+ if (status1 === "completed" || status2 === "completed") {
3317
+ return "completed";
3318
+ }
3319
+ if (status1 === "failed" || status2 === "failed") {
3320
+ return "failed";
3321
+ }
3322
+ return "running";
3323
+ }
3324
+ function accumulateBlocks(existing, newBlocks) {
3325
+ for (const block of newBlocks) {
3326
+ if (isToolBlock(block) && block.metadata.tool_use_id) {
3327
+ const toolUseId = block.metadata.tool_use_id;
3328
+ const existingIndex = existing.findIndex(
3329
+ (b) => isToolBlock(b) && b.metadata.tool_use_id === toolUseId
3330
+ );
3331
+ if (existingIndex >= 0) {
3332
+ const existingBlock = existing[existingIndex];
3333
+ const mergedStatus = getMergedToolStatus(
3334
+ existingBlock.metadata.tool_status,
3335
+ block.metadata.tool_status
3336
+ );
3337
+ existing[existingIndex] = {
3338
+ id: existingBlock.id,
3339
+ block_type: "tool",
3340
+ content: block.metadata.tool_name || existingBlock.content,
3341
+ metadata: {
3342
+ tool_name: block.metadata.tool_name || existingBlock.metadata.tool_name,
3343
+ 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,
3346
+ tool_status: mergedStatus
3347
+ }
3348
+ };
3349
+ } else {
3350
+ existing.push(block);
3351
+ }
3352
+ } else if (isTextBlock(block)) {
3353
+ const lastBlock = existing[existing.length - 1];
3354
+ if (lastBlock && isTextBlock(lastBlock)) {
3355
+ existing[existing.length - 1] = block;
3356
+ } else {
3357
+ existing.push(block);
3358
+ }
3359
+ } else {
3360
+ existing.push(block);
3361
+ }
3362
+ }
3363
+ }
3222
3364
 
3223
3365
  // src/infrastructure/agents/agent-output-parser.ts
3224
3366
  var AgentOutputParser = class _AgentOutputParser {
@@ -5598,7 +5740,7 @@ var ChatHistoryService = class _ChatHistoryService {
5598
5740
  * Gets supervisor chat history (global, shared across all devices).
5599
5741
  * Returns messages sorted by sequence (oldest first) for chronological display.
5600
5742
  */
5601
- getSupervisorHistory(limit = 50) {
5743
+ getSupervisorHistory(limit = 20) {
5602
5744
  const sessionId = _ChatHistoryService.SUPERVISOR_SESSION_ID;
5603
5745
  const rows = this.messageRepo.getBySession(sessionId, limit);
5604
5746
  return rows.reverse().map((row) => {
@@ -5663,7 +5805,7 @@ var ChatHistoryService = class _ChatHistoryService {
5663
5805
  * Gets agent session chat history.
5664
5806
  * Returns messages sorted chronologically (oldest first).
5665
5807
  */
5666
- getAgentHistory(sessionId, limit = 100) {
5808
+ getAgentHistory(sessionId, limit = 20) {
5667
5809
  const rows = this.messageRepo.getBySession(sessionId, limit);
5668
5810
  return rows.reverse().map((row) => {
5669
5811
  let contentBlocks;
@@ -5721,7 +5863,7 @@ var ChatHistoryService = class _ChatHistoryService {
5721
5863
  * @param sessionIds - List of active agent session IDs
5722
5864
  * @param limit - Max messages per session
5723
5865
  */
5724
- getAllAgentHistories(sessionIds, limit = 50) {
5866
+ getAllAgentHistories(sessionIds, limit = 20) {
5725
5867
  const histories = /* @__PURE__ */ new Map();
5726
5868
  for (const sessionId of sessionIds) {
5727
5869
  const history = this.getAgentHistory(sessionId, limit);
@@ -7346,12 +7488,7 @@ var SupervisorAgent = class extends EventEmitter4 {
7346
7488
  if (this.isExecuting && !this.isCancelled) {
7347
7489
  const textBlock = createTextBlock(content);
7348
7490
  this.emit("blocks", deviceId, [textBlock], false);
7349
- const lastTextIndex = allBlocks.findLastIndex((b) => b.block_type === "text");
7350
- if (lastTextIndex >= 0) {
7351
- allBlocks[lastTextIndex] = textBlock;
7352
- } else {
7353
- allBlocks.push(textBlock);
7354
- }
7491
+ accumulateBlocks(allBlocks, [textBlock]);
7355
7492
  }
7356
7493
  } else if (Array.isArray(content)) {
7357
7494
  for (const item of content) {
@@ -7359,7 +7496,7 @@ var SupervisorAgent = class extends EventEmitter4 {
7359
7496
  const block = this.parseContentItem(item);
7360
7497
  if (block) {
7361
7498
  this.emit("blocks", deviceId, [block], false);
7362
- allBlocks.push(block);
7499
+ accumulateBlocks(allBlocks, [block]);
7363
7500
  }
7364
7501
  }
7365
7502
  }
@@ -7368,21 +7505,24 @@ var SupervisorAgent = class extends EventEmitter4 {
7368
7505
  if (lastMessage.getType() === "tool" && this.isExecuting && !this.isCancelled) {
7369
7506
  const toolContent = lastMessage.content;
7370
7507
  const toolName = lastMessage.name ?? "tool";
7508
+ const toolCallId = lastMessage.tool_call_id;
7371
7509
  const toolBlock = createToolBlock(
7372
7510
  toolName,
7373
7511
  "completed",
7374
7512
  void 0,
7375
- typeof toolContent === "string" ? toolContent : JSON.stringify(toolContent)
7513
+ typeof toolContent === "string" ? toolContent : JSON.stringify(toolContent),
7514
+ toolCallId
7376
7515
  );
7377
7516
  this.emit("blocks", deviceId, [toolBlock], false);
7378
- allBlocks.push(toolBlock);
7517
+ accumulateBlocks(allBlocks, [toolBlock]);
7379
7518
  }
7380
7519
  }
7381
7520
  if (this.isExecuting && !this.isCancelled) {
7382
7521
  this.addToHistory("user", command);
7383
7522
  this.addToHistory("assistant", finalOutput);
7523
+ const finalBlocks = mergeToolBlocks(allBlocks);
7384
7524
  const completionBlock = createStatusBlock("Complete");
7385
- this.emit("blocks", deviceId, [completionBlock], true, finalOutput, allBlocks);
7525
+ this.emit("blocks", deviceId, [completionBlock], true, finalOutput, finalBlocks);
7386
7526
  this.logger.debug({ output: finalOutput.slice(0, 200) }, "Supervisor streaming completed");
7387
7527
  } else {
7388
7528
  this.logger.info({ deviceId, isCancelled: this.isCancelled, isExecuting: this.isExecuting }, "Supervisor streaming ended due to cancellation");
@@ -7412,7 +7552,8 @@ var SupervisorAgent = class extends EventEmitter4 {
7412
7552
  if (type === "tool_use") {
7413
7553
  const name = typeof item.name === "string" ? item.name : "tool";
7414
7554
  const input = item.input;
7415
- return createToolBlock(name, "running", input);
7555
+ const toolUseId = typeof item.id === "string" ? item.id : void 0;
7556
+ return createToolBlock(name, "running", input, void 0, toolUseId);
7416
7557
  }
7417
7558
  return null;
7418
7559
  }
@@ -7677,6 +7818,9 @@ Creating worktrees with \`create_worktree\`:
7677
7818
  - When creating sessions, confirm the workspace and project first
7678
7819
  - For ambiguous requests, ask clarifying questions
7679
7820
  - Format responses for terminal display (avoid markdown links)
7821
+ - NEVER use tables - they display poorly on mobile devices
7822
+ - ALWAYS use bullet lists or numbered lists instead of tables
7823
+ - Keep list items short and scannable for mobile reading
7680
7824
  - ALWAYS prioritize safety - check before deleting/merging`;
7681
7825
  return [new HumanMessage(`[System Instructions]
7682
7826
  ${systemPrompt}
@@ -8235,7 +8379,18 @@ var STTService = class {
8235
8379
  this.logger.debug({ audioSize: audioBuffer.length, format }, "Transcribing audio");
8236
8380
  const startTime = Date.now();
8237
8381
  try {
8238
- const result = this.config.provider === "openai" ? await this.transcribeOpenAI(audioBuffer, format, signal) : await this.transcribeElevenLabs(audioBuffer, format, signal);
8382
+ let result;
8383
+ switch (this.config.provider) {
8384
+ case "openai":
8385
+ case "local":
8386
+ result = await this.transcribeOpenAI(audioBuffer, format, signal);
8387
+ break;
8388
+ case "elevenlabs":
8389
+ result = await this.transcribeElevenLabs(audioBuffer, format, signal);
8390
+ break;
8391
+ default:
8392
+ throw new Error(`Unsupported STT provider: ${this.config.provider}`);
8393
+ }
8239
8394
  const elapsed = Date.now() - startTime;
8240
8395
  this.logger.info(
8241
8396
  {
@@ -8347,6 +8502,9 @@ var STTService = class {
8347
8502
  * Check if the service is configured and ready.
8348
8503
  */
8349
8504
  isConfigured() {
8505
+ if (this.config.provider === "local") {
8506
+ return Boolean(this.config.baseUrl && this.config.model);
8507
+ }
8350
8508
  return Boolean(this.config.apiKey && this.config.model);
8351
8509
  }
8352
8510
  /**
@@ -8363,20 +8521,28 @@ var STTService = class {
8363
8521
  function createSTTService(env, logger) {
8364
8522
  const provider = (env.STT_PROVIDER ?? "openai").toLowerCase();
8365
8523
  const apiKey = env.STT_API_KEY;
8366
- if (!apiKey) {
8524
+ const baseUrl = env.STT_BASE_URL;
8525
+ if (provider === "local") {
8526
+ if (!baseUrl) {
8527
+ logger.warn("STT_BASE_URL not configured for local provider, STT service disabled");
8528
+ return null;
8529
+ }
8530
+ } else if (!apiKey) {
8367
8531
  logger.warn("STT_API_KEY not configured, STT service disabled");
8368
8532
  return null;
8369
8533
  }
8370
8534
  const defaults = {
8371
8535
  openai: { model: "whisper-1" },
8372
- elevenlabs: { model: "scribe_v1" }
8536
+ elevenlabs: { model: "scribe_v1" },
8537
+ deepgram: { model: "nova-2" },
8538
+ local: { model: "large-v3" }
8373
8539
  };
8374
8540
  const config2 = {
8375
8541
  provider,
8376
- apiKey,
8542
+ apiKey: apiKey ?? "",
8377
8543
  model: env.STT_MODEL ?? defaults[provider].model,
8378
8544
  language: env.STT_LANGUAGE,
8379
- baseUrl: env.STT_BASE_URL
8545
+ baseUrl
8380
8546
  };
8381
8547
  return new STTService(config2, logger);
8382
8548
  }
@@ -8396,7 +8562,18 @@ var TTSService = class {
8396
8562
  this.logger.debug({ textLength: text2.length }, "Synthesizing speech");
8397
8563
  const startTime = Date.now();
8398
8564
  try {
8399
- const result = this.config.provider === "openai" ? await this.synthesizeOpenAI(text2) : await this.synthesizeElevenLabs(text2);
8565
+ let result;
8566
+ switch (this.config.provider) {
8567
+ case "openai":
8568
+ case "local":
8569
+ result = await this.synthesizeOpenAI(text2);
8570
+ break;
8571
+ case "elevenlabs":
8572
+ result = await this.synthesizeElevenLabs(text2);
8573
+ break;
8574
+ default:
8575
+ throw new Error(`Unsupported TTS provider: ${this.config.provider}`);
8576
+ }
8400
8577
  const elapsed = Date.now() - startTime;
8401
8578
  this.logger.info(
8402
8579
  { textLength: text2.length, audioSize: result.audio.length, elapsedMs: elapsed },
@@ -8473,6 +8650,9 @@ var TTSService = class {
8473
8650
  * Check if the service is configured and ready.
8474
8651
  */
8475
8652
  isConfigured() {
8653
+ if (this.config.provider === "local") {
8654
+ return Boolean(this.config.baseUrl && this.config.voice);
8655
+ }
8476
8656
  return Boolean(this.config.apiKey && this.config.model && this.config.voice);
8477
8657
  }
8478
8658
  /**
@@ -8489,20 +8669,27 @@ var TTSService = class {
8489
8669
  function createTTSService(env, logger) {
8490
8670
  const provider = (env.TTS_PROVIDER ?? "openai").toLowerCase();
8491
8671
  const apiKey = env.TTS_API_KEY;
8492
- if (!apiKey) {
8672
+ const baseUrl = env.TTS_BASE_URL;
8673
+ if (provider === "local") {
8674
+ if (!baseUrl) {
8675
+ logger.warn("TTS_BASE_URL not configured for local provider, TTS service disabled");
8676
+ return null;
8677
+ }
8678
+ } else if (!apiKey) {
8493
8679
  logger.warn("TTS_API_KEY not configured, TTS service disabled");
8494
8680
  return null;
8495
8681
  }
8496
8682
  const defaults = {
8497
8683
  openai: { model: "tts-1", voice: "alloy" },
8498
- elevenlabs: { model: "eleven_flash_v2_5", voice: "21m00Tcm4TlvDq8ikWAM" }
8684
+ elevenlabs: { model: "eleven_flash_v2_5", voice: "21m00Tcm4TlvDq8ikWAM" },
8685
+ local: { model: "kokoro", voice: "af_heart" }
8499
8686
  };
8500
8687
  const config2 = {
8501
8688
  provider,
8502
- apiKey,
8689
+ apiKey: apiKey ?? "",
8503
8690
  model: env.TTS_MODEL ?? defaults[provider].model,
8504
8691
  voice: env.TTS_VOICE ?? defaults[provider].voice,
8505
- baseUrl: env.TTS_BASE_URL
8692
+ baseUrl
8506
8693
  };
8507
8694
  return new TTSService(config2, logger);
8508
8695
  }
@@ -8585,6 +8772,8 @@ var SummarizationService = class {
8585
8772
  buildSystemPrompt() {
8586
8773
  return `You are a concise summarizer for voice output. Summarize AI assistant responses into 1-${this.maxSentences} SHORT sentences.
8587
8774
 
8775
+ CRITICAL: ALWAYS OUTPUT IN ENGLISH. Translate any non-English content to English.
8776
+
8588
8777
  STRICT RULES:
8589
8778
  - Maximum 20 words total (hard limit)
8590
8779
  - Prefer 1 sentence when possible, 2 only if essential
@@ -8592,6 +8781,7 @@ STRICT RULES:
8592
8781
  - Natural spoken language only
8593
8782
  - Never use markdown, bullets, or formatting
8594
8783
  - Never start with "I" - use passive voice or action verbs
8784
+ - ALWAYS translate to English regardless of input language
8595
8785
 
8596
8786
  ABSOLUTELY FORBIDDEN (never include these in output):
8597
8787
  - Session IDs or any alphanumeric identifiers (e.g., "session-abc123", "id: 7f3a2b", UUIDs)
@@ -8599,6 +8789,7 @@ ABSOLUTELY FORBIDDEN (never include these in output):
8599
8789
  - Any string that looks like a technical identifier, hash, or token
8600
8790
  - Code snippets, variable names, or technical jargon
8601
8791
  - Long lists or enumerations
8792
+ - Non-English output (always translate to English)
8602
8793
 
8603
8794
  If the original text contains session IDs or paths, OMIT them entirely. Just describe what happened.
8604
8795
 
@@ -8611,7 +8802,9 @@ GOOD examples:
8611
8802
  BAD examples (never do this):
8612
8803
  - "Created session abc-123-def in /Users/roman/work/project" \u274C
8613
8804
  - "Session ID is 7f3a2b1c" \u274C
8614
- - "Working in /home/user/documents/code" \u274C`;
8805
+ - "Working in /home/user/documents/code" \u274C
8806
+ - "\u0421\u043E\u0437\u0434\u0430\u043D\u0430 \u043D\u043E\u0432\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F Claude." \u274C (non-English)
8807
+ - "Sesi\xF3n creada exitosamente." \u274C (non-English)`;
8615
8808
  }
8616
8809
  /**
8617
8810
  * Builds the user prompt with the text to summarize and optional context.
@@ -9113,22 +9306,29 @@ async function bootstrap() {
9113
9306
  ).map((s) => s.session_id);
9114
9307
  const agentHistoriesMap = chatHistoryService.getAllAgentHistories(agentSessionIds);
9115
9308
  const agentHistories = {};
9116
- for (const [sessionId, history] of agentHistoriesMap) {
9117
- agentHistories[sessionId] = await Promise.all(
9118
- history.map(async (msg) => ({
9119
- sequence: msg.sequence,
9120
- role: msg.role,
9121
- content: msg.content,
9122
- content_blocks: await chatHistoryService.enrichBlocksWithAudio(
9123
- msg.contentBlocks,
9124
- msg.audioOutputPath,
9125
- msg.audioInputPath,
9126
- false
9127
- // Don't include audio in sync.state
9128
- ),
9129
- createdAt: msg.createdAt.toISOString()
9130
- }))
9131
- );
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;
9132
9332
  }
9133
9333
  const availableAgentsMap = getAvailableAgents();
9134
9334
  const availableAgents = Array.from(availableAgentsMap.values()).map(
@@ -9262,6 +9462,21 @@ async function bootstrap() {
9262
9462
  }
9263
9463
  let commandText;
9264
9464
  const messageId = commandMessage.payload.message_id;
9465
+ if (messageBroadcaster) {
9466
+ const ackMessageId = messageId || commandMessage.id;
9467
+ const ackMessage = {
9468
+ type: "message.ack",
9469
+ payload: {
9470
+ message_id: ackMessageId,
9471
+ status: "received"
9472
+ }
9473
+ };
9474
+ messageBroadcaster.sendToClient(deviceId, JSON.stringify(ackMessage));
9475
+ logger.debug(
9476
+ { messageId: ackMessageId, deviceId },
9477
+ "Sent message.ack for supervisor.command"
9478
+ );
9479
+ }
9265
9480
  supervisorAgent.resetCancellationState();
9266
9481
  let abortController;
9267
9482
  if (commandMessage.payload.audio) {
@@ -9734,6 +9949,22 @@ async function bootstrap() {
9734
9949
  const deviceId = execMessage.device_id;
9735
9950
  const sessionId = execMessage.session_id;
9736
9951
  const messageId = execMessage.payload.message_id;
9952
+ if (deviceId && messageBroadcaster) {
9953
+ const ackMessageId = messageId || execMessage.id;
9954
+ const ackMessage = {
9955
+ type: "message.ack",
9956
+ payload: {
9957
+ message_id: ackMessageId,
9958
+ session_id: sessionId,
9959
+ status: "received"
9960
+ }
9961
+ };
9962
+ messageBroadcaster.sendToClient(deviceId, JSON.stringify(ackMessage));
9963
+ logger.debug(
9964
+ { messageId: ackMessageId, sessionId, deviceId },
9965
+ "Sent message.ack for session.execute"
9966
+ );
9967
+ }
9737
9968
  cancelledDuringTranscription.delete(sessionId);
9738
9969
  if (execMessage.payload.audio) {
9739
9970
  logger.info(
@@ -10496,7 +10727,7 @@ async function bootstrap() {
10496
10727
  if (persistableBlocks.length > 0) {
10497
10728
  const accumulated = agentMessageAccumulator.get(sessionId) ?? [];
10498
10729
  const wasEmpty = accumulated.length === 0;
10499
- accumulated.push(...persistableBlocks);
10730
+ accumulateBlocks(accumulated, persistableBlocks);
10500
10731
  agentMessageAccumulator.set(sessionId, accumulated);
10501
10732
  if (wasEmpty) {
10502
10733
  logger.debug(
@@ -10551,7 +10782,8 @@ async function bootstrap() {
10551
10782
  }
10552
10783
  }
10553
10784
  const accumulatedBlocks = agentMessageAccumulator.get(sessionId) ?? [];
10554
- const fullAccumulatedText = accumulatedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
10785
+ const mergedBlocks = mergeToolBlocks(accumulatedBlocks);
10786
+ const fullAccumulatedText = mergedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
10555
10787
  const outputEvent = {
10556
10788
  type: "session.output",
10557
10789
  session_id: sessionId,
@@ -10559,8 +10791,8 @@ async function bootstrap() {
10559
10791
  content_type: "agent",
10560
10792
  content: fullAccumulatedText,
10561
10793
  // Full accumulated text for backward compat
10562
- content_blocks: accumulatedBlocks,
10563
- // Full accumulated blocks for rich UI
10794
+ content_blocks: mergedBlocks,
10795
+ // Full accumulated blocks for rich UI (merged)
10564
10796
  timestamp: Date.now(),
10565
10797
  is_complete: isComplete
10566
10798
  }
@@ -10644,10 +10876,11 @@ async function bootstrap() {
10644
10876
  }
10645
10877
  }
10646
10878
  );
10879
+ let supervisorBlockAccumulator = [];
10647
10880
  supervisorAgent.on(
10648
10881
  "blocks",
10649
10882
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
10650
- async (deviceId, blocks, isComplete, finalOutput, allBlocks) => {
10883
+ async (deviceId, blocks, isComplete, finalOutput, _allBlocks) => {
10651
10884
  const wasCancelled = supervisorAgent.wasCancelled();
10652
10885
  logger.debug(
10653
10886
  { deviceId, blockCount: blocks.length, isComplete, wasCancelled },
@@ -10658,26 +10891,35 @@ async function bootstrap() {
10658
10891
  { deviceId, blockCount: blocks.length, isComplete },
10659
10892
  "Ignoring supervisor blocks - execution was cancelled"
10660
10893
  );
10894
+ supervisorBlockAccumulator = [];
10661
10895
  return;
10662
10896
  }
10663
- const textContent = blocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
10897
+ const persistableBlocks = blocks.filter((b) => b.block_type !== "status");
10898
+ if (persistableBlocks.length > 0) {
10899
+ accumulateBlocks(supervisorBlockAccumulator, persistableBlocks);
10900
+ }
10901
+ const mergedBlocks = mergeToolBlocks(supervisorBlockAccumulator);
10902
+ const textContent = mergedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
10664
10903
  const outputEvent = {
10665
10904
  type: "supervisor.output",
10666
10905
  payload: {
10667
10906
  content_type: "supervisor",
10668
10907
  content: textContent,
10669
- content_blocks: blocks,
10908
+ content_blocks: mergedBlocks,
10670
10909
  timestamp: Date.now(),
10671
10910
  is_complete: isComplete
10672
10911
  }
10673
10912
  };
10674
10913
  const message = JSON.stringify(outputEvent);
10675
10914
  broadcaster.broadcastToAll(message);
10915
+ if (isComplete) {
10916
+ supervisorBlockAccumulator = [];
10917
+ }
10676
10918
  if (isComplete && finalOutput && finalOutput.length > 0) {
10677
10919
  chatHistoryService.saveSupervisorMessage(
10678
10920
  "assistant",
10679
10921
  finalOutput,
10680
- allBlocks
10922
+ mergedBlocks
10681
10923
  );
10682
10924
  const pendingVoiceCommand = pendingSupervisorVoiceCommands.get(deviceId);
10683
10925
  if (pendingVoiceCommand && ttsService) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiflis-io/tiflis-code-workstation",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
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",