@exreve/exk 1.0.13 → 1.0.15

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.
@@ -51,12 +51,35 @@ function extractToolName(toolResult) {
51
51
  if (toolResult.type === 'text' && toolResult.file)
52
52
  return 'Read';
53
53
  if (toolResult.file_path || toolResult.filePath) {
54
+ // Has old_string/new_string → Edit; has content/create → Write; else Read
55
+ if (toolResult.old_string !== undefined && toolResult.new_string !== undefined)
56
+ return 'Edit';
54
57
  return (toolResult.content !== undefined || toolResult.type === 'create') ? 'Write' : 'Read';
55
58
  }
56
59
  if (toolResult.stdout !== undefined || toolResult.stderr !== undefined)
57
60
  return 'Bash';
61
+ // SDK 0.2.x: content-only results from nested tool calls (no stdout/stderr wrapper)
62
+ if (toolResult.content && typeof toolResult.content === 'string' && toolResult.type === 'text')
63
+ return 'Bash';
58
64
  return 'unknown';
59
65
  }
66
+ // Look up tool name from the most recent assistant message's tool_use blocks by tool_use_id
67
+ function lookupToolNameFromHistory(messages, toolUseId) {
68
+ if (!toolUseId)
69
+ return null;
70
+ for (let i = messages.length - 1; i >= 0; i--) {
71
+ const msg = messages[i];
72
+ if (msg.role !== 'assistant')
73
+ continue;
74
+ const content = typeof msg.content === 'string' ? null : msg.content;
75
+ if (!Array.isArray(content))
76
+ continue;
77
+ const toolUse = content.find((c) => c.type === 'tool_use' && c.id === toolUseId);
78
+ if (toolUse?.name)
79
+ return toolUse.name;
80
+ }
81
+ return null;
82
+ }
60
83
  // AI config - loaded from server after registration, stored in ~/.talk-to-code/ai-config.json
61
84
  // (Do not read ANTHROPIC_* / CLAUDE_MODEL from the host environment — only this file + code default model.)
62
85
  const AI_CONFIG_PATH = path.join(os.homedir(), '.talk-to-code', 'ai-config.json');
@@ -251,6 +274,13 @@ export class AgentSessionManager {
251
274
  if (!session.isProcessingQueue) {
252
275
  this.processPromptQueue(sessionId);
253
276
  }
277
+ else if (session.isProcessingQueue && !session.activeQueryStream && !this.emergencyStopInProgress.has(sessionId)) {
278
+ // Safety: isProcessingQueue is true but there's no active stream and no emergency stop
279
+ // This means the queue got stuck (e.g. from a previous abort return that bypassed cleanup)
280
+ console.warn(`[agentSession] Queue stuck detected for session ${sessionId}, resetting isProcessingQueue`);
281
+ session.isProcessingQueue = false;
282
+ this.processPromptQueue(sessionId);
283
+ }
254
284
  }
255
285
  async processPromptQueue(sessionId) {
256
286
  const session = this.sessions.get(sessionId);
@@ -647,19 +677,62 @@ export class AgentSessionManager {
647
677
  }
648
678
  else if (message.type === 'user') {
649
679
  const msg = message;
680
+ // SDK 0.2.x: tool results can appear in two places:
681
+ // 1. msg.tool_use_result (top-level field, present in 0.1.x and some 0.2.x messages)
682
+ // 2. msg.message.content array with type='tool_result' blocks (common in 0.2.x subagent calls)
683
+ let toolResult = null;
684
+ let toolUseId = msg.parent_tool_use_id;
685
+ // Check top-level tool_use_result first
650
686
  if (msg.tool_use_result) {
651
- const toolResult = msg.tool_use_result;
652
- let toolUseId = msg.parent_tool_use_id;
653
- if (!toolUseId && Array.isArray(msg.message.content)) {
654
- const toolResultContent = msg.message.content.find((c) => c.type === 'tool_result' && c.tool_use_id);
655
- toolUseId = toolResultContent?.tool_use_id || null;
687
+ toolResult = msg.tool_use_result;
688
+ }
689
+ // Check message.content for tool_result blocks (SDK 0.2.x nested calls)
690
+ if (!toolResult && Array.isArray(msg.message?.content)) {
691
+ const contentBlocks = msg.message.content;
692
+ const toolResultBlock = contentBlocks.find((c) => c.type === 'tool_result');
693
+ if (toolResultBlock) {
694
+ // Extract tool use ID from the content block
695
+ if (toolResultBlock.tool_use_id) {
696
+ toolUseId = toolResultBlock.tool_use_id;
697
+ }
698
+ // The result content can be a string or array of content blocks
699
+ if (typeof toolResultBlock.content === 'string') {
700
+ try {
701
+ toolResult = JSON.parse(toolResultBlock.content);
702
+ }
703
+ catch {
704
+ toolResult = { content: toolResultBlock.content, type: 'text' };
705
+ }
706
+ }
707
+ else if (Array.isArray(toolResultBlock.content)) {
708
+ // Extract text from content blocks
709
+ const textParts = toolResultBlock.content
710
+ .filter((c) => c.type === 'text')
711
+ .map((c) => c.text);
712
+ const rawContent = textParts.join('\n');
713
+ try {
714
+ toolResult = JSON.parse(rawContent);
715
+ }
716
+ catch {
717
+ toolResult = { content: rawContent, type: 'text' };
718
+ }
719
+ }
720
+ else {
721
+ toolResult = toolResultBlock;
722
+ }
656
723
  }
724
+ }
725
+ if (toolResult) {
726
+ const detectedName = extractToolName(toolResult);
727
+ const resolvedName = detectedName !== 'unknown'
728
+ ? detectedName
729
+ : (lookupToolNameFromHistory(session.messages, toolUseId) || detectedName);
657
730
  onOutput({
658
731
  type: 'tool_result',
659
732
  data: toolResult,
660
733
  timestamp: Date.now(),
661
734
  metadata: {
662
- toolName: extractToolName(toolResult),
735
+ toolName: resolvedName,
663
736
  toolResult: toolResult,
664
737
  toolUseId: toolUseId || undefined,
665
738
  parentToolUseId: msg.parent_tool_use_id,
@@ -785,7 +858,9 @@ export class AgentSessionManager {
785
858
  onComplete(null);
786
859
  session.activeQueryStream = undefined;
787
860
  session.currentPromptId = undefined;
788
- return;
861
+ // Use break instead of return to ensure isProcessingQueue gets reset
862
+ // after the while loop at the end of processPromptQueue
863
+ break;
789
864
  }
790
865
  // Re-throw non-abort errors
791
866
  throw streamError;
@@ -972,24 +1047,46 @@ export class AgentSessionManager {
972
1047
  if (!session) {
973
1048
  return { success: false, message: 'Session not found' };
974
1049
  }
975
- // 1. Abort all controllers (session level + all prompt-level)
1050
+ // 1. Abort session-level controller only (not all sessions' controllers)
976
1051
  session.abortController.abort();
977
- for (const controller of this.promptAbortControllers.values()) {
978
- controller.abort();
1052
+ // Abort only controllers belonging to THIS session
1053
+ // Find and abort controllers for prompts in this session's queue and current prompt
1054
+ if (session.currentPromptId) {
1055
+ const ctrl = this.promptAbortControllers.get(session.currentPromptId);
1056
+ if (ctrl)
1057
+ ctrl.abort();
1058
+ }
1059
+ for (const queued of session.promptQueue) {
1060
+ if (queued.abortController)
1061
+ queued.abortController.abort();
1062
+ if (queued.promptId) {
1063
+ const ctrl = this.promptAbortControllers.get(queued.promptId);
1064
+ if (ctrl)
1065
+ ctrl.abort();
1066
+ }
979
1067
  }
980
1068
  // 2. Kill the entire process tree
981
1069
  await this.killProcessTree(sessionId);
982
- // 3. Clear the prompt queue
1070
+ // 3. Collect prompt IDs from queue BEFORE clearing it
983
1071
  const queueSize = session.promptQueue.length;
1072
+ const queuedPromptIds = session.promptQueue
1073
+ .map(p => p.promptId)
1074
+ .filter((id) => !!id);
1075
+ const currentPromptId = session.currentPromptId;
1076
+ // 4. Clear the prompt queue
984
1077
  session.promptQueue = [];
985
- // 4. Clear active stream
1078
+ // 5. Clear active stream
986
1079
  session.activeQueryStream = undefined;
987
- // 5. Reset processing state
1080
+ // 6. Reset processing state
988
1081
  session.isProcessingQueue = false;
989
- // 6. Clear current prompt tracking
990
- const currentPromptId = session.currentPromptId;
991
- // 7. Clean up abort controllers map
992
- this.promptAbortControllers.clear();
1082
+ // 7. Clean up abort controllers map (only for this session's prompts, not ALL sessions)
1083
+ if (currentPromptId) {
1084
+ this.promptAbortControllers.delete(currentPromptId);
1085
+ }
1086
+ for (const pid of queuedPromptIds) {
1087
+ this.promptAbortControllers.delete(pid);
1088
+ }
1089
+ session.currentPromptId = undefined;
993
1090
  // 8. Remove from emergency stop tracking
994
1091
  this.emergencyStopInProgress.delete(sessionId);
995
1092
  // 9. Resolve any pending choice request with null (cancelled)
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exreve/exk",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "exk - Control Claude CLI with voice and programmable interfaces",
5
5
  "type": "module",
6
6
  "bin": {