wave-agent-sdk 0.14.3 → 0.15.0

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 (84) hide show
  1. package/builtin/skills/settings/SKILLS.md +34 -6
  2. package/dist/agent.d.ts +0 -5
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +0 -15
  5. package/dist/constants/toolLimits.d.ts +10 -0
  6. package/dist/constants/toolLimits.d.ts.map +1 -0
  7. package/dist/constants/toolLimits.js +9 -0
  8. package/dist/managers/aiManager.d.ts +3 -5
  9. package/dist/managers/aiManager.d.ts.map +1 -1
  10. package/dist/managers/aiManager.js +107 -104
  11. package/dist/managers/forkedAgentManager.d.ts +1 -0
  12. package/dist/managers/forkedAgentManager.d.ts.map +1 -1
  13. package/dist/managers/forkedAgentManager.js +1 -0
  14. package/dist/managers/hookManager.d.ts +0 -4
  15. package/dist/managers/hookManager.d.ts.map +1 -1
  16. package/dist/managers/hookManager.js +0 -25
  17. package/dist/managers/permissionManager.d.ts +1 -1
  18. package/dist/managers/permissionManager.d.ts.map +1 -1
  19. package/dist/managers/permissionManager.js +5 -5
  20. package/dist/managers/subagentManager.d.ts +1 -0
  21. package/dist/managers/subagentManager.d.ts.map +1 -1
  22. package/dist/managers/subagentManager.js +1 -0
  23. package/dist/prompts/index.d.ts +0 -1
  24. package/dist/prompts/index.d.ts.map +1 -1
  25. package/dist/prompts/index.js +3 -4
  26. package/dist/services/aiService.d.ts.map +1 -1
  27. package/dist/services/aiService.js +10 -8
  28. package/dist/services/autoMemoryService.d.ts.map +1 -1
  29. package/dist/services/autoMemoryService.js +1 -0
  30. package/dist/services/hook.d.ts +0 -4
  31. package/dist/services/hook.d.ts.map +1 -1
  32. package/dist/services/hook.js +0 -10
  33. package/dist/services/session.d.ts.map +1 -1
  34. package/dist/services/session.js +4 -1
  35. package/dist/tools/bashTool.d.ts.map +1 -1
  36. package/dist/tools/bashTool.js +2 -45
  37. package/dist/tools/editTool.js +1 -1
  38. package/dist/tools/types.d.ts +0 -3
  39. package/dist/tools/types.d.ts.map +1 -1
  40. package/dist/types/agent.d.ts +0 -1
  41. package/dist/types/agent.d.ts.map +1 -1
  42. package/dist/types/hooks.d.ts +1 -5
  43. package/dist/types/hooks.d.ts.map +1 -1
  44. package/dist/types/hooks.js +0 -1
  45. package/dist/utils/constants.d.ts +2 -2
  46. package/dist/utils/constants.d.ts.map +1 -1
  47. package/dist/utils/constants.js +2 -2
  48. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  49. package/dist/utils/convertMessagesForAPI.js +16 -8
  50. package/dist/utils/editUtils.d.ts +5 -2
  51. package/dist/utils/editUtils.d.ts.map +1 -1
  52. package/dist/utils/editUtils.js +3 -57
  53. package/dist/utils/markdownParser.d.ts +8 -1
  54. package/dist/utils/markdownParser.d.ts.map +1 -1
  55. package/dist/utils/markdownParser.js +64 -11
  56. package/dist/utils/openaiClient.d.ts.map +1 -1
  57. package/dist/utils/openaiClient.js +0 -11
  58. package/dist/utils/stringUtils.d.ts +8 -0
  59. package/dist/utils/stringUtils.d.ts.map +1 -1
  60. package/dist/utils/stringUtils.js +45 -0
  61. package/package.json +1 -1
  62. package/src/agent.ts +0 -17
  63. package/src/constants/toolLimits.ts +12 -0
  64. package/src/managers/aiManager.ts +141 -148
  65. package/src/managers/forkedAgentManager.ts +3 -0
  66. package/src/managers/hookManager.ts +0 -32
  67. package/src/managers/permissionManager.ts +6 -6
  68. package/src/managers/subagentManager.ts +2 -0
  69. package/src/prompts/index.ts +3 -5
  70. package/src/services/aiService.ts +10 -12
  71. package/src/services/autoMemoryService.ts +1 -0
  72. package/src/services/hook.ts +0 -15
  73. package/src/services/session.ts +6 -1
  74. package/src/tools/bashTool.ts +2 -51
  75. package/src/tools/editTool.ts +1 -1
  76. package/src/tools/types.ts +0 -3
  77. package/src/types/agent.ts +0 -1
  78. package/src/types/hooks.ts +1 -7
  79. package/src/utils/constants.ts +2 -2
  80. package/src/utils/convertMessagesForAPI.ts +15 -8
  81. package/src/utils/editUtils.ts +3 -73
  82. package/src/utils/markdownParser.ts +85 -11
  83. package/src/utils/openaiClient.ts +0 -11
  84. package/src/utils/stringUtils.ts +43 -0
@@ -24,6 +24,7 @@ import type { SubagentManager } from "./subagentManager.js";
24
24
  import type { SkillManager } from "./skillManager.js";
25
25
  import { buildSystemPrompt } from "../prompts/index.js";
26
26
  import { Container } from "../utils/container.js";
27
+ import { recoverTruncatedJson } from "../utils/stringUtils.js";
27
28
  import { ConfigurationService } from "../services/configurationService.js";
28
29
  import type { NotificationQueue } from "./notificationQueue.js";
29
30
 
@@ -32,7 +33,6 @@ import { logger } from "../utils/globalLogger.js";
32
33
  export interface AIManagerCallbacks {
33
34
  onCompactionStateChange?: (isCompacting: boolean) => void;
34
35
  onUsageAdded?: (usage: Usage) => void;
35
- onCwdChange?: (newCwd: string) => void;
36
36
  }
37
37
 
38
38
  export interface AIManagerOptions {
@@ -44,6 +44,8 @@ export interface AIManagerOptions {
44
44
  stream?: boolean;
45
45
  /**Optional model override (e.g. for subagents) */
46
46
  modelOverride?: string;
47
+ /**Optional max turns limit to prevent runaway recursion (e.g. for auto-memory extraction) */
48
+ maxTurns?: number;
47
49
  }
48
50
 
49
51
  export class AIManager {
@@ -52,13 +54,12 @@ export class AIManager {
52
54
  onLoadingChange?: (loading: boolean) => void;
53
55
  private toolAbortController: AbortController | null = null;
54
56
  private workdir: string;
55
- private originalWorkdir: string;
56
57
  private systemPrompt?: string;
57
58
  private subagentType?: string; // Store subagent type for hook context
58
59
  private stream: boolean; // Streaming mode flag
59
60
  private modelOverride?: string;
60
- private _onCwdChange?: (newCwd: string) => void; // Store callback for CWD changes
61
61
  private consecutiveCompactionFailures: number = 0;
62
+ private readonly maxTurns?: number;
62
63
 
63
64
  // Service overrides
64
65
  constructor(
@@ -66,13 +67,12 @@ export class AIManager {
66
67
  options: AIManagerOptions,
67
68
  ) {
68
69
  this.workdir = options.workdir;
69
- this.originalWorkdir = options.workdir;
70
70
  this.systemPrompt = options.systemPrompt;
71
71
  this.subagentType = options.subagentType; // Store subagent type
72
72
  this.stream = options.stream ?? true; // Default to true if not specified
73
73
  this.callbacks = options.callbacks ?? {};
74
74
  this.modelOverride = options.modelOverride;
75
- this._onCwdChange = options.callbacks?.onCwdChange; // Initialize onCwdChange
75
+ this.maxTurns = options.maxTurns;
76
76
  }
77
77
 
78
78
  private get toolManager(): ToolManager {
@@ -169,14 +169,6 @@ export class AIManager {
169
169
  return this.workdir;
170
170
  }
171
171
 
172
- public getOriginalWorkdir(): string {
173
- return this.originalWorkdir;
174
- }
175
-
176
- public setOnCwdChange(callback: (newCwd: string) => void): void {
177
- this._onCwdChange = callback;
178
- }
179
-
180
172
  private isCompacting: boolean = false;
181
173
  private callbacks: AIManagerCallbacks;
182
174
 
@@ -242,7 +234,6 @@ export class AIManager {
242
234
  if (toolPlugin?.formatCompactParams) {
243
235
  const context: ToolContext = {
244
236
  workdir: this.workdir,
245
- originalWorkdir: this.originalWorkdir,
246
237
  taskManager: this.taskManager,
247
238
  };
248
239
  return toolPlugin.formatCompactParams(toolArgs, context);
@@ -652,7 +643,6 @@ export class AIManager {
652
643
  filteredToolPlugins,
653
644
  {
654
645
  workdir: this.workdir,
655
- originalWorkdir: this.originalWorkdir,
656
646
  memory: combinedMemory,
657
647
  language: this.getLanguage(),
658
648
  isSubagent: !!this.subagentType,
@@ -814,34 +804,45 @@ export class AIManager {
814
804
  const toolName = functionToolCall.function?.name || "";
815
805
  // Safely parse tool parameters, handle tools without parameters
816
806
  let toolArgs: Record<string, unknown> = {};
807
+ let jsonRecovered = false;
817
808
  const argsString = functionToolCall.function?.arguments?.trim();
818
809
 
819
810
  if (!argsString || argsString === "") {
820
811
  // Tool without parameters, use empty object
821
812
  toolArgs = {};
822
813
  } else {
814
+ let recoveredArgs = argsString;
823
815
  try {
824
816
  toolArgs = JSON.parse(argsString);
825
- } catch (parseError) {
826
- // For non-empty but malformed JSON, still throw exception
827
- let errorMessage = `Failed to parse tool arguments`;
828
- if (result.finish_reason === "length") {
829
- errorMessage +=
830
- " (output truncated, please reduce your output)";
817
+ } catch {
818
+ // Attempt to recover truncated JSON (e.g., missing closing braces)
819
+ recoveredArgs = recoverTruncatedJson(argsString);
820
+ try {
821
+ toolArgs = JSON.parse(recoveredArgs);
822
+ jsonRecovered = true;
823
+ logger.warn(
824
+ `Recovered truncated JSON for tool "${toolName}"`,
825
+ );
826
+ } catch (parseError) {
827
+ let errorMessage = `Failed to parse tool arguments`;
828
+ if (result.finish_reason === "length") {
829
+ errorMessage +=
830
+ " (output truncated, please reduce your output)";
831
+ }
832
+ logger?.error(errorMessage, parseError);
833
+ this.messageManager.updateToolBlock({
834
+ id: toolId,
835
+ parameters: argsString,
836
+ result: errorMessage,
837
+ success: false,
838
+ error: errorMessage,
839
+ stage: "end",
840
+ name: toolName,
841
+ compactParams: "",
842
+ timestamp: Date.now(),
843
+ });
844
+ return;
831
845
  }
832
- logger?.error(errorMessage, parseError);
833
- this.messageManager.updateToolBlock({
834
- id: toolId,
835
- parameters: argsString,
836
- result: errorMessage,
837
- success: false,
838
- error: errorMessage,
839
- stage: "end",
840
- name: toolName,
841
- compactParams: "",
842
- timestamp: Date.now(),
843
- });
844
- return;
845
846
  }
846
847
  }
847
848
 
@@ -892,7 +893,6 @@ export class AIManager {
892
893
  abortSignal: toolAbortController.signal,
893
894
  backgroundTaskManager: this.backgroundTaskManager,
894
895
  workdir: this.workdir,
895
- originalWorkdir: this.originalWorkdir,
896
896
  messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
897
897
  sessionId: this.messageManager.getSessionId(),
898
898
  toolCallId: toolId,
@@ -911,28 +911,6 @@ export class AIManager {
911
911
  stage: "running", // Keep it in running stage while updating result
912
912
  });
913
913
  },
914
- onCwdChange: async (newCwd: string) => {
915
- const oldCwd = this.workdir;
916
- this.workdir = newCwd;
917
- this._onCwdChange?.(newCwd);
918
- if (this.hookManager) {
919
- const sessionId = this.messageManager.getSessionId();
920
- const transcriptPath =
921
- this.messageManager.getTranscriptPath();
922
- const env = Object.fromEntries(
923
- Object.entries(process.env).filter(
924
- (e) => e[1] !== undefined,
925
- ),
926
- ) as Record<string, string>;
927
- await this.hookManager.executeCwdChangedHooks(
928
- oldCwd,
929
- newCwd,
930
- sessionId,
931
- transcriptPath,
932
- env,
933
- );
934
- }
935
- },
936
914
  };
937
915
 
938
916
  // Execute tool
@@ -942,13 +920,20 @@ export class AIManager {
942
920
  context,
943
921
  );
944
922
 
923
+ // Build result content, adding truncation warning if JSON was recovered
924
+ let toolResultContent =
925
+ toolResult.content ||
926
+ (toolResult.error ? `Error: ${toolResult.error}` : "");
927
+ if (jsonRecovered) {
928
+ toolResultContent +=
929
+ "\n\n⚠️ Tool arguments were truncated (likely exceeded max output tokens). Please reduce your output or split into multiple tool calls.";
930
+ }
931
+
945
932
  // Update message state - tool execution completed
946
933
  this.messageManager.updateToolBlock({
947
934
  id: toolId,
948
935
  parameters: argsString,
949
- result:
950
- toolResult.content ||
951
- (toolResult.error ? `Error: ${toolResult.error}` : ""),
936
+ result: toolResultContent,
952
937
  success: toolResult.success,
953
938
  error: toolResult.error,
954
939
  stage: "end",
@@ -1001,108 +986,116 @@ export class AIManager {
1001
986
 
1002
987
  // Check if there are tool operations or response was truncated, if so automatically initiate next AI service call
1003
988
  if (toolCalls.length > 0 || result.finish_reason === "length") {
1004
- // Record committed snapshots to message history
1005
- if (this.reversionManager) {
1006
- const snapshots =
1007
- this.reversionManager.getAndClearCommittedSnapshots();
1008
- if (snapshots.length > 0) {
1009
- this.messageManager.addFileHistoryBlock(snapshots);
1010
- }
1011
- }
1012
-
1013
- // Check interruption status
1014
- const isCurrentlyAborted =
1015
- abortController.signal.aborted || toolAbortController.signal.aborted;
1016
-
1017
- // Check if all tools were manually backgrounded
1018
- const lastMessage =
1019
- this.messageManager.getMessages()[
1020
- this.messageManager.getMessages().length - 1
1021
- ];
1022
- const toolBlocks =
1023
- lastMessage?.blocks.filter(
1024
- (block): block is import("../types/messaging.js").ToolBlock =>
1025
- block.type === "tool",
1026
- ) || [];
1027
- const hasBackgrounded =
1028
- toolBlocks.length > 0 &&
1029
- toolBlocks.some((block) => block.isManuallyBackgrounded);
1030
-
1031
- if (hasBackgrounded) {
1032
- logger?.info(
1033
- "Some tools were manually backgrounded, stopping recursion.",
989
+ // Check maxTurns limit before recursing
990
+ if (this.maxTurns && recursionDepth + 1 >= this.maxTurns) {
991
+ logger?.debug(
992
+ `Max turns (${this.maxTurns}) reached, stopping recursion.`,
1034
993
  );
1035
- } else if (!isCurrentlyAborted) {
1036
- // If response was truncated, add a hidden continuation message
1037
- if (result.finish_reason === "length") {
1038
- this.messageManager.addUserMessage({
1039
- content:
1040
- "Output token limit hit. Resume directly — no apology, no recap of what you were doing. Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces.",
1041
- isMeta: true,
1042
- });
994
+ } else {
995
+ // Record committed snapshots to message history
996
+ if (this.reversionManager) {
997
+ const snapshots =
998
+ this.reversionManager.getAndClearCommittedSnapshots();
999
+ if (snapshots.length > 0) {
1000
+ this.messageManager.addFileHistoryBlock(snapshots);
1001
+ }
1043
1002
  }
1044
1003
 
1045
- // Duplicate Tool Call Detection
1046
- if (toolCalls.length > 0) {
1047
- const messages = this.messageManager.getMessages();
1048
- // Find the most recent assistant message BEFORE the current one that has tool blocks
1049
- // The current assistant message is messages[messages.length - 1]
1050
- let previousAssistantWithTools: Message | undefined;
1051
- for (let i = messages.length - 2; i >= 0; i--) {
1052
- const msg = messages[i];
1053
- if (
1054
- msg.role === "assistant" &&
1055
- msg.blocks.some((b) => b.type === "tool")
1056
- ) {
1057
- previousAssistantWithTools = msg;
1058
- break;
1059
- }
1004
+ // Check interruption status
1005
+ const isCurrentlyAborted =
1006
+ abortController.signal.aborted ||
1007
+ toolAbortController.signal.aborted;
1008
+
1009
+ // Check if all tools were manually backgrounded
1010
+ const lastMessage =
1011
+ this.messageManager.getMessages()[
1012
+ this.messageManager.getMessages().length - 1
1013
+ ];
1014
+ const toolBlocks =
1015
+ lastMessage?.blocks.filter(
1016
+ (block): block is import("../types/messaging.js").ToolBlock =>
1017
+ block.type === "tool",
1018
+ ) || [];
1019
+ const hasBackgrounded =
1020
+ toolBlocks.length > 0 &&
1021
+ toolBlocks.some((block) => block.isManuallyBackgrounded);
1022
+
1023
+ if (hasBackgrounded) {
1024
+ logger?.info(
1025
+ "Some tools were manually backgrounded, stopping recursion.",
1026
+ );
1027
+ } else if (!isCurrentlyAborted) {
1028
+ // If response was truncated, add a hidden continuation message
1029
+ if (result.finish_reason === "length") {
1030
+ this.messageManager.addUserMessage({
1031
+ content:
1032
+ "Output token limit hit. Resume directly — no apology, no recap of what you were doing. Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces.",
1033
+ isMeta: true,
1034
+ });
1060
1035
  }
1061
1036
 
1062
- if (previousAssistantWithTools) {
1063
- const previousToolBlocks =
1064
- previousAssistantWithTools.blocks.filter(
1065
- (b): b is import("../types/messaging.js").ToolBlock =>
1066
- b.type === "tool",
1067
- );
1037
+ // Duplicate Tool Call Detection
1038
+ if (toolCalls.length > 0) {
1039
+ const messages = this.messageManager.getMessages();
1040
+ // Find the most recent assistant message BEFORE the current one that has tool blocks
1041
+ // The current assistant message is messages[messages.length - 1]
1042
+ let previousAssistantWithTools: Message | undefined;
1043
+ for (let i = messages.length - 2; i >= 0; i--) {
1044
+ const msg = messages[i];
1045
+ if (
1046
+ msg.role === "assistant" &&
1047
+ msg.blocks.some((b) => b.type === "tool")
1048
+ ) {
1049
+ previousAssistantWithTools = msg;
1050
+ break;
1051
+ }
1052
+ }
1068
1053
 
1069
- for (const currentToolCall of toolCalls) {
1070
- const currentName = currentToolCall.function?.name;
1071
- const currentArgs = currentToolCall.function?.arguments;
1054
+ if (previousAssistantWithTools) {
1055
+ const previousToolBlocks =
1056
+ previousAssistantWithTools.blocks.filter(
1057
+ (b): b is import("../types/messaging.js").ToolBlock =>
1058
+ b.type === "tool",
1059
+ );
1072
1060
 
1073
- const isDuplicate = previousToolBlocks.some(
1074
- (prevBlock) =>
1075
- prevBlock.name === currentName &&
1076
- prevBlock.parameters === currentArgs,
1077
- );
1061
+ for (const currentToolCall of toolCalls) {
1062
+ const currentName = currentToolCall.function?.name;
1063
+ const currentArgs = currentToolCall.function?.arguments;
1078
1064
 
1079
- if (isDuplicate && currentName) {
1080
- const toolId = currentToolCall.id;
1081
- const lastMessage = messages[messages.length - 1];
1082
- const toolBlock = lastMessage.blocks.find(
1083
- (b): b is import("../types/messaging.js").ToolBlock =>
1084
- b.type === "tool" && b.id === toolId,
1065
+ const isDuplicate = previousToolBlocks.some(
1066
+ (prevBlock) =>
1067
+ prevBlock.name === currentName &&
1068
+ prevBlock.parameters === currentArgs,
1085
1069
  );
1086
- if (toolBlock) {
1087
- const warning = `\n\nNote: You just called this tool with the same arguments in the previous turn. Please ensure you are not in a loop and consider if you need to change your approach.`;
1088
- this.messageManager.updateToolBlock({
1089
- id: toolId,
1090
- result: (toolBlock.result || "") + warning,
1091
- stage: "end",
1092
- });
1070
+
1071
+ if (isDuplicate && currentName) {
1072
+ const toolId = currentToolCall.id;
1073
+ const lastMessage = messages[messages.length - 1];
1074
+ const toolBlock = lastMessage.blocks.find(
1075
+ (b): b is import("../types/messaging.js").ToolBlock =>
1076
+ b.type === "tool" && b.id === toolId,
1077
+ );
1078
+ if (toolBlock) {
1079
+ const warning = `\n\nNote: You just called this tool with the same arguments in the previous turn. Please ensure you are not in a loop and consider if you need to change your approach.`;
1080
+ this.messageManager.updateToolBlock({
1081
+ id: toolId,
1082
+ result: (toolBlock.result || "") + warning,
1083
+ stage: "end",
1084
+ });
1085
+ }
1093
1086
  }
1094
1087
  }
1095
1088
  }
1096
1089
  }
1097
- }
1098
1090
 
1099
- // Recursively call AI service, increment recursion depth, and pass same configuration
1100
- await this.sendAIMessage({
1101
- recursionDepth: recursionDepth + 1,
1102
- model,
1103
- allowedRules,
1104
- maxTokens,
1105
- });
1091
+ // Recursively call AI service, increment recursion depth, and pass same configuration
1092
+ await this.sendAIMessage({
1093
+ recursionDepth: recursionDepth + 1,
1094
+ model,
1095
+ allowedRules,
1096
+ maxTokens,
1097
+ });
1098
+ }
1106
1099
  }
1107
1100
  }
1108
1101
  } catch (error) {
@@ -47,6 +47,7 @@ export class ForkedAgentManager {
47
47
  allowedTools?: string[];
48
48
  model?: string;
49
49
  permissionModeOverride?: PermissionMode;
50
+ maxTurns?: number;
50
51
  },
51
52
  prompt: string,
52
53
  ): Promise<string> {
@@ -84,6 +85,7 @@ export class ForkedAgentManager {
84
85
  allowedTools?: string[];
85
86
  model?: string;
86
87
  permissionModeOverride?: PermissionMode;
88
+ maxTurns?: number;
87
89
  },
88
90
  prompt: string,
89
91
  ): Promise<void> {
@@ -103,6 +105,7 @@ export class ForkedAgentManager {
103
105
  allowedTools: parameters.allowedTools,
104
106
  model: parameters.model,
105
107
  permissionModeOverride: parameters.permissionModeOverride,
108
+ maxTurns: parameters.maxTurns,
106
109
  },
107
110
  false,
108
111
  );
@@ -669,7 +669,6 @@ export class HookManager {
669
669
  event === "Stop" ||
670
670
  event === "SubagentStop" ||
671
671
  event === "WorktreeCreate" ||
672
- event === "CwdChanged" ||
673
672
  event === "SessionStart" ||
674
673
  event === "SessionEnd"
675
674
  ) {
@@ -773,7 +772,6 @@ export class HookManager {
773
772
  SubagentStop: 0,
774
773
  PermissionRequest: 0,
775
774
  WorktreeCreate: 0,
776
- CwdChanged: 0,
777
775
  SessionStart: 0,
778
776
  SessionEnd: 0,
779
777
  },
@@ -788,7 +786,6 @@ export class HookManager {
788
786
  SubagentStop: 0,
789
787
  PermissionRequest: 0,
790
788
  WorktreeCreate: 0,
791
- CwdChanged: 0,
792
789
  SessionStart: 0,
793
790
  SessionEnd: 0,
794
791
  };
@@ -815,35 +812,6 @@ export class HookManager {
815
812
  };
816
813
  }
817
814
 
818
- /**
819
- * Execute CwdChanged hooks.
820
- */
821
- async executeCwdChangedHooks(
822
- oldCwd: string,
823
- newCwd: string,
824
- sessionId: string,
825
- transcriptPath: string,
826
- env: Record<string, string>,
827
- ): Promise<HookExecutionResult[]> {
828
- const context: ExtendedHookExecutionContext = {
829
- event: "CwdChanged",
830
- projectDir: this.workdir,
831
- timestamp: new Date(),
832
- sessionId,
833
- transcriptPath,
834
- cwd: newCwd,
835
- oldCwd,
836
- newCwd,
837
- env,
838
- };
839
- const results = await this.executeHooks("CwdChanged", context);
840
- if (results.length > 0) {
841
- // For CwdChanged hooks, we don't block, just log errors
842
- this.processHookResults("CwdChanged", results);
843
- }
844
- return results;
845
- }
846
-
847
815
  /**
848
816
  * Register hooks provided by a plugin
849
817
  */
@@ -131,7 +131,7 @@ export class PermissionManager {
131
131
  private planFilePath?: string;
132
132
  private worktreeName?: string;
133
133
  private mainRepoRoot?: string;
134
- private originalWorkdir?: string;
134
+ private workdir?: string;
135
135
  private onConfiguredPermissionModeChange?: (mode: PermissionMode) => void;
136
136
  private _logger?: Logger;
137
137
 
@@ -153,7 +153,7 @@ export class PermissionManager {
153
153
 
154
154
  this.worktreeName = this.container.get<string>("WorktreeName");
155
155
  this.mainRepoRoot = this.container.get<string>("MainRepoRoot");
156
- this.originalWorkdir = this.container.get<string>("Workdir");
156
+ this.workdir = this.container.get<string>("Workdir");
157
157
  }
158
158
 
159
159
  /**
@@ -277,7 +277,7 @@ export class PermissionManager {
277
277
  * Update the additional directories (e.g., when configuration reloads)
278
278
  */
279
279
  updateAdditionalDirectories(directories: string[]): void {
280
- const workdir = this.originalWorkdir;
280
+ const workdir = this.workdir;
281
281
  this.additionalDirectories = directories.map((dir) => {
282
282
  if (workdir && !path.isAbsolute(dir)) {
283
283
  return path.resolve(workdir, dir);
@@ -290,7 +290,7 @@ export class PermissionManager {
290
290
  * Add a system-level additional directory that is persistent across configuration reloads
291
291
  */
292
292
  public addSystemAdditionalDirectory(directory: string): void {
293
- const workdir = this.originalWorkdir;
293
+ const workdir = this.workdir;
294
294
  const resolvedPath =
295
295
  workdir && !path.isAbsolute(directory)
296
296
  ? path.resolve(workdir, directory)
@@ -329,7 +329,7 @@ export class PermissionManager {
329
329
  targetPath: string,
330
330
  workdir?: string,
331
331
  ): { isInside: boolean; resolvedPath: string } {
332
- const effectiveWorkdir = this.originalWorkdir || workdir;
332
+ const effectiveWorkdir = this.workdir || workdir;
333
333
 
334
334
  // Resolve the target path relative to effectiveWorkdir if it's not absolute
335
335
  const absolutePath =
@@ -1068,7 +1068,7 @@ export class PermissionManager {
1068
1068
  * @param rule - The rule to add (e.g., "Bash(ls)")
1069
1069
  */
1070
1070
  public async addPermissionRule(rule: string): Promise<void> {
1071
- const workdir = this.originalWorkdir;
1071
+ const workdir = this.workdir;
1072
1072
  if (!workdir) {
1073
1073
  throw new Error("Working directory not set in PermissionManager");
1074
1074
  }
@@ -251,6 +251,7 @@ export class SubagentManager {
251
251
  model?: string;
252
252
  stream?: boolean;
253
253
  permissionModeOverride?: PermissionMode;
254
+ maxTurns?: number;
254
255
  },
255
256
  runInBackground?: boolean,
256
257
  onUpdate?: () => void,
@@ -356,6 +357,7 @@ export class SubagentManager {
356
357
  subagentType: parameters.subagent_type, // Pass subagent type for hook context
357
358
  modelOverride: parameters.model || configuration.model, // Pass model override
358
359
  stream: parameters.stream ?? this.stream, // Pass streaming mode flag
360
+ maxTurns: parameters.maxTurns, // Pass maxTurns limit
359
361
  callbacks: {
360
362
  onUsageAdded: this.onUsageAdded,
361
363
  },
@@ -238,7 +238,6 @@ export function buildSystemPrompt(
238
238
  tools: ToolPlugin[],
239
239
  options: {
240
240
  workdir?: string;
241
- originalWorkdir?: string;
242
241
  memory?: string;
243
242
  language?: string;
244
243
  isSubagent?: boolean;
@@ -276,9 +275,8 @@ export function buildSystemPrompt(
276
275
  prompt += `\n\n${buildPlanModePrompt(options.planMode.planFilePath, options.planMode.planExists, options.isSubagent)}`;
277
276
  }
278
277
 
279
- const workdirForPrompt = options.originalWorkdir || options.workdir;
280
- if (workdirForPrompt) {
281
- const isGitRepo = isGitRepository(workdirForPrompt);
278
+ if (options.workdir) {
279
+ const isGitRepo = isGitRepository(options.workdir);
282
280
  const platform = os.platform();
283
281
  const osVersion = `${os.type()} ${os.release()}`;
284
282
  const today = new Date().toISOString().split("T")[0];
@@ -293,7 +291,7 @@ export function buildSystemPrompt(
293
291
 
294
292
  Here is useful information about the environment you are running in:
295
293
  <env>
296
- Working directory: ${workdirForPrompt}
294
+ Working directory: ${options.workdir}
297
295
  Is directory a git repo: ${isGitRepo}
298
296
  Platform: ${platform}
299
297
  Shell: ${shellName}
@@ -377,10 +377,7 @@ export async function callAgent(
377
377
  result.content = finalContent;
378
378
  }
379
379
 
380
- if (
381
- typeof finalReasoningContent === "string" &&
382
- finalReasoningContent.length > 0
383
- ) {
380
+ if (typeof finalReasoningContent === "string") {
384
381
  result.reasoning_content = finalReasoningContent;
385
382
  }
386
383
 
@@ -544,6 +541,7 @@ async function processStreamingResponse(
544
541
  ): Promise<CallAgentResult> {
545
542
  let accumulatedContent = "";
546
543
  let accumulatedReasoningContent = "";
544
+ let hasReasoningContent = false;
547
545
  const toolCalls: {
548
546
  id: string;
549
547
  type: "function";
@@ -618,13 +616,13 @@ async function processStreamingResponse(
618
616
  }
619
617
  }
620
618
 
621
- if (
622
- typeof reasoning_content === "string" &&
623
- reasoning_content.length > 0
624
- ) {
625
- accumulatedReasoningContent += reasoning_content;
626
- if (onReasoningUpdate) {
627
- onReasoningUpdate(accumulatedReasoningContent);
619
+ if (typeof reasoning_content === "string") {
620
+ hasReasoningContent = true;
621
+ if (reasoning_content.length > 0) {
622
+ accumulatedReasoningContent += reasoning_content;
623
+ if (onReasoningUpdate) {
624
+ onReasoningUpdate(accumulatedReasoningContent);
625
+ }
628
626
  }
629
627
  }
630
628
 
@@ -716,7 +714,7 @@ async function processStreamingResponse(
716
714
  result.content = accumulatedContent.trim();
717
715
  }
718
716
 
719
- if (accumulatedReasoningContent) {
717
+ if (hasReasoningContent) {
720
718
  result.reasoning_content = accumulatedReasoningContent.trim();
721
719
  }
722
720
 
@@ -164,6 +164,7 @@ export class AutoMemoryService {
164
164
  ],
165
165
  model: "fastModel", // Use fast model for background tasks to reduce latency and cost
166
166
  permissionModeOverride: "dontAsk", // Auto-deny out-of-scope writes without prompting user
167
+ maxTurns: 5, // Limit turns to prevent verification rabbit-holes
167
168
  },
168
169
  `${prompt}\n\nThe memory directory for this project is: ${memoryDir}`,
169
170
  );