wave-agent-sdk 0.18.3 → 0.18.5

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 (76) hide show
  1. package/builtin/skills/settings/ENV.md +1 -1
  2. package/builtin/skills/settings/MEMORY.md +76 -0
  3. package/builtin/skills/settings/SKILL.md +4 -4
  4. package/dist/agent.d.ts.map +1 -1
  5. package/dist/agent.js +0 -2
  6. package/dist/managers/MemoryRuleManager.d.ts +9 -0
  7. package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
  8. package/dist/managers/MemoryRuleManager.js +18 -0
  9. package/dist/managers/aiManager.d.ts +1 -1
  10. package/dist/managers/aiManager.d.ts.map +1 -1
  11. package/dist/managers/aiManager.js +370 -345
  12. package/dist/managers/mcpManager.d.ts.map +1 -1
  13. package/dist/managers/mcpManager.js +22 -15
  14. package/dist/managers/messageManager.d.ts +32 -13
  15. package/dist/managers/messageManager.d.ts.map +1 -1
  16. package/dist/managers/messageManager.js +82 -76
  17. package/dist/prompts/index.d.ts +10 -4
  18. package/dist/prompts/index.d.ts.map +1 -1
  19. package/dist/prompts/index.js +21 -20
  20. package/dist/services/aiService.d.ts +2 -1
  21. package/dist/services/aiService.d.ts.map +1 -1
  22. package/dist/services/aiService.js +37 -10
  23. package/dist/services/memory.d.ts +1 -0
  24. package/dist/services/memory.d.ts.map +1 -1
  25. package/dist/services/memory.js +35 -17
  26. package/dist/tools/agentTool.d.ts.map +1 -1
  27. package/dist/tools/agentTool.js +24 -2
  28. package/dist/tools/bashTool.d.ts.map +1 -1
  29. package/dist/tools/bashTool.js +17 -10
  30. package/dist/tools/editTool.d.ts.map +1 -1
  31. package/dist/tools/editTool.js +7 -6
  32. package/dist/tools/readTool.js +2 -2
  33. package/dist/tools/writeTool.js +2 -2
  34. package/dist/utils/constants.d.ts +1 -1
  35. package/dist/utils/constants.js +1 -1
  36. package/dist/utils/containerSetup.d.ts.map +1 -1
  37. package/dist/utils/containerSetup.js +1 -0
  38. package/dist/utils/gitUtils.js +6 -6
  39. package/dist/utils/openaiClient.d.ts.map +1 -1
  40. package/dist/utils/openaiClient.js +33 -32
  41. package/dist/utils/path.d.ts +7 -0
  42. package/dist/utils/path.d.ts.map +1 -1
  43. package/dist/utils/path.js +9 -0
  44. package/dist/utils/taskReminder.d.ts +2 -3
  45. package/dist/utils/taskReminder.d.ts.map +1 -1
  46. package/dist/utils/taskReminder.js +7 -18
  47. package/dist/utils/worktreeUtils.d.ts.map +1 -1
  48. package/dist/utils/worktreeUtils.js +19 -13
  49. package/package.json +1 -1
  50. package/scripts/install_ripgrep.js +21 -1
  51. package/src/agent.ts +0 -5
  52. package/src/managers/MemoryRuleManager.ts +23 -0
  53. package/src/managers/aiManager.ts +473 -434
  54. package/src/managers/mcpManager.ts +26 -15
  55. package/src/managers/messageManager.ts +99 -82
  56. package/src/prompts/index.ts +33 -24
  57. package/src/services/aiService.ts +39 -12
  58. package/src/services/memory.ts +34 -16
  59. package/src/tools/agentTool.ts +24 -2
  60. package/src/tools/bashTool.ts +18 -9
  61. package/src/tools/editTool.ts +7 -9
  62. package/src/tools/readTool.ts +2 -2
  63. package/src/tools/writeTool.ts +2 -2
  64. package/src/utils/constants.ts +1 -1
  65. package/src/utils/containerSetup.ts +1 -0
  66. package/src/utils/gitUtils.ts +6 -6
  67. package/src/utils/openaiClient.ts +40 -31
  68. package/src/utils/path.ts +10 -0
  69. package/src/utils/taskReminder.ts +7 -19
  70. package/src/utils/worktreeUtils.ts +46 -28
  71. package/builtin/skills/settings/MEMORY_RULES.md +0 -60
  72. package/vendor/ripgrep/linux-aarch64/rg +0 -0
  73. package/vendor/ripgrep/macos-aarch64/rg +0 -0
  74. package/vendor/ripgrep/macos-x86_64/rg +0 -0
  75. package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
  76. package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
@@ -8,24 +8,26 @@ import { getGitCommonDir } from "../utils/gitUtils.js";
8
8
  import { pathEncoder } from "../utils/pathEncoder.js";
9
9
 
10
10
  export class MemoryService {
11
- private _cachedProjectMemory: string = "";
12
- private _cachedUserMemory: string = "";
11
+ private _cachedProjectMemory: string | null = null;
12
+ private _cachedUserMemory: string | null = null;
13
13
  private _cachedCombinedMemory: string | null = null;
14
+ private _cachedAutoMemoryContent: string | null = null;
14
15
 
15
16
  constructor(private container: Container) {}
16
17
 
17
18
  public get cachedProjectMemory(): string {
18
- return this._cachedProjectMemory;
19
+ return this._cachedProjectMemory ?? "";
19
20
  }
20
21
 
21
22
  public get cachedUserMemory(): string {
22
- return this._cachedUserMemory;
23
+ return this._cachedUserMemory ?? "";
23
24
  }
24
25
 
25
26
  public clearCache(): void {
26
- this._cachedProjectMemory = "";
27
- this._cachedUserMemory = "";
27
+ this._cachedProjectMemory = null;
28
+ this._cachedUserMemory = null;
28
29
  this._cachedCombinedMemory = null;
30
+ this._cachedAutoMemoryContent = null;
29
31
  }
30
32
 
31
33
  /**
@@ -76,15 +78,20 @@ export class MemoryService {
76
78
  * Get the first 200 lines of MEMORY.md.
77
79
  */
78
80
  async getAutoMemoryContent(workdir: string): Promise<string> {
81
+ if (this._cachedAutoMemoryContent !== null) {
82
+ return this._cachedAutoMemoryContent;
83
+ }
79
84
  const memoryDir = this.getAutoMemoryDirectory(workdir);
80
85
  const memoryFile = path.join(memoryDir, "MEMORY.md");
81
86
 
82
87
  try {
83
88
  const content = await fs.readFile(memoryFile, "utf-8");
84
89
  const lines = content.split("\n").slice(0, 200);
85
- return lines.join("\n");
90
+ this._cachedAutoMemoryContent = lines.join("\n");
91
+ return this._cachedAutoMemoryContent;
86
92
  } catch (error) {
87
93
  if ((error as NodeJS.ErrnoException).code === "ENOENT") {
94
+ this._cachedAutoMemoryContent = "";
88
95
  return "";
89
96
  }
90
97
  logger.error("Failed to read auto-memory content:", error);
@@ -123,6 +130,9 @@ export class MemoryService {
123
130
  }
124
131
 
125
132
  async getUserMemoryContent(): Promise<string> {
133
+ if (this._cachedUserMemory !== null) {
134
+ return this._cachedUserMemory;
135
+ }
126
136
  try {
127
137
  await this.ensureUserMemoryFile();
128
138
  const content = await fs.readFile(USER_MEMORY_FILE, "utf-8");
@@ -145,13 +155,15 @@ export class MemoryService {
145
155
  contentLength: claudeContent.length,
146
156
  },
147
157
  );
148
- return claudeContent;
158
+ this._cachedUserMemory = claudeContent;
159
+ return this._cachedUserMemory;
149
160
  } catch {
150
161
  // CLAUDE.md doesn't exist or can't be read, return the default template
151
162
  }
152
163
  }
153
164
 
154
- return content;
165
+ this._cachedUserMemory = content;
166
+ return this._cachedUserMemory;
155
167
  } catch (error) {
156
168
  logger.error("Failed to read user memory:", error);
157
169
  return "";
@@ -159,6 +171,9 @@ export class MemoryService {
159
171
  }
160
172
 
161
173
  async readMemoryFile(workdir: string): Promise<string> {
174
+ if (this._cachedProjectMemory !== null) {
175
+ return this._cachedProjectMemory;
176
+ }
162
177
  const memoryFilePath = path.join(workdir, "AGENTS.md");
163
178
  try {
164
179
  const content = await fs.readFile(memoryFilePath, "utf-8");
@@ -166,7 +181,8 @@ export class MemoryService {
166
181
  memoryFilePath,
167
182
  contentLength: content.length,
168
183
  });
169
- return content;
184
+ this._cachedProjectMemory = content;
185
+ return this._cachedProjectMemory;
170
186
  } catch (error) {
171
187
  if ((error as NodeJS.ErrnoException).code === "ENOENT") {
172
188
  // Fallback to CLAUDE.md
@@ -177,12 +193,14 @@ export class MemoryService {
177
193
  memoryFilePath: claudeMemoryPath,
178
194
  contentLength: content.length,
179
195
  });
180
- return content;
196
+ this._cachedProjectMemory = content;
197
+ return this._cachedProjectMemory;
181
198
  } catch (claudeError) {
182
199
  if ((claudeError as NodeJS.ErrnoException).code === "ENOENT") {
183
200
  logger.debug("Neither AGENTS.md nor CLAUDE.md found", {
184
201
  memoryFilePath,
185
202
  });
203
+ this._cachedProjectMemory = "";
186
204
  return "";
187
205
  }
188
206
  logger.error("Failed to read CLAUDE.md fallback", {
@@ -201,14 +219,14 @@ export class MemoryService {
201
219
  if (this._cachedCombinedMemory !== null) {
202
220
  return this._cachedCombinedMemory;
203
221
  }
204
- this._cachedProjectMemory = await this.readMemoryFile(workdir);
205
- this._cachedUserMemory = await this.getUserMemoryContent();
222
+ const projectMemory = await this.readMemoryFile(workdir);
223
+ const userMemory = await this.getUserMemoryContent();
206
224
 
207
225
  let combined = "";
208
- if (this._cachedProjectMemory.trim()) combined += this._cachedProjectMemory;
209
- if (this._cachedUserMemory.trim()) {
226
+ if (projectMemory.trim()) combined += projectMemory;
227
+ if (userMemory.trim()) {
210
228
  if (combined) combined += "\n\n";
211
- combined += this._cachedUserMemory;
229
+ combined += userMemory;
212
230
  }
213
231
  this._cachedCombinedMemory = combined;
214
232
  return combined;
@@ -266,10 +266,32 @@ When using the Agent tool, you must specify a subagent_type parameter to select
266
266
  });
267
267
  } catch (error) {
268
268
  if (!isBackgrounded) {
269
+ // Extract content from the subagent's last assistant message.
270
+ // aiManager.sendAIMessage() catch block already added an error
271
+ // block via addErrorBlock(), so the error info is in the message
272
+ // history — just return it directly.
273
+ let content = "";
274
+ try {
275
+ const messages = instance.messageManager.getMessages();
276
+ const lastAssistant = messages
277
+ .filter((m) => m.role === "assistant")
278
+ .pop();
279
+ if (lastAssistant) {
280
+ content = lastAssistant.blocks
281
+ .filter((b) => b.type === "text" || b.type === "error")
282
+ .map((b) => (b as { content: string }).content)
283
+ .join("\n")
284
+ .trim();
285
+ }
286
+ } catch {
287
+ // Ignore errors when extracting messages
288
+ }
289
+
290
+ const errorMsg = `Agent delegation failed: ${error instanceof Error ? error.message : String(error)}`;
269
291
  resolve({
270
292
  success: false,
271
- content: "",
272
- error: `Agent delegation failed: ${error instanceof Error ? error.message : String(error)}`,
293
+ content: content || errorMsg,
294
+ error: errorMsg,
273
295
  shortResult: "Delegation error",
274
296
  });
275
297
  }
@@ -4,6 +4,7 @@ import * as path from "path";
4
4
  import * as os from "os";
5
5
  import { logger } from "../utils/globalLogger.js";
6
6
  import { resolveShellPath } from "../utils/shellResolver.js";
7
+ import { toPosixPath } from "../utils/path.js";
7
8
  import { stripAnsiColors } from "../utils/stringUtils.js";
8
9
  import { processToolResult } from "../utils/toolResultStorage.js";
9
10
  import { BASH_MAX_OUTPUT_CHARS } from "../constants/toolLimits.js";
@@ -246,7 +247,8 @@ The working directory persists between commands. Try to maintain your current wo
246
247
  os.tmpdir(),
247
248
  `wave_cwd_${Date.now()}_${Math.random().toString(36).substring(2, 11)}.tmp`,
248
249
  );
249
- const wrappedCommand = `${command} && pwd -P >| ${tempCwdFile}`;
250
+ const tempCwdFileForBash = toPosixPath(tempCwdFile);
251
+ const wrappedCommand = `${command} && pwd -P >| ${tempCwdFileForBash}`;
250
252
 
251
253
  const child: ChildProcess = spawn(wrappedCommand, {
252
254
  shell: shellPath || true,
@@ -264,6 +266,17 @@ The working directory persists between commands. Try to maintain your current wo
264
266
  let isBackgrounded = false;
265
267
  let isFinished = false;
266
268
 
269
+ // Best-effort cleanup of the temp CWD file — used by abort/error/exit paths
270
+ const cleanupTempFile = () => {
271
+ try {
272
+ if (fs.existsSync(tempCwdFile)) {
273
+ fs.unlinkSync(tempCwdFile);
274
+ }
275
+ } catch {
276
+ // ignore — best-effort cleanup
277
+ }
278
+ };
279
+
267
280
  const updateRealtimeResults = () => {
268
281
  if (isAborted || isBackgrounded || isFinished) return;
269
282
 
@@ -420,6 +433,8 @@ The working directory persists between commands. Try to maintain your current wo
420
433
  }
421
434
  }
422
435
 
436
+ cleanupTempFile();
437
+
423
438
  const processedOutput = processToolResult(
424
439
  outputBuffer + (errorBuffer ? "\n" + errorBuffer : ""),
425
440
  BASH_MAX_OUTPUT_CHARS,
@@ -491,14 +506,7 @@ The working directory persists between commands. Try to maintain your current wo
491
506
  );
492
507
  newCwd = undefined;
493
508
  } finally {
494
- // Ensure temp file is cleaned up even if reading fails
495
- try {
496
- if (fs.existsSync(tempCwdFile)) {
497
- fs.unlinkSync(tempCwdFile);
498
- }
499
- } catch (fileError) {
500
- logger.error("Failed to clean up temp CWD file:", fileError);
501
- }
509
+ cleanupTempFile();
502
510
  }
503
511
 
504
512
  // If CWD changed, call the onCwdChange callback and add notification
@@ -560,6 +568,7 @@ The working directory persists between commands. Try to maintain your current wo
560
568
  if (timeoutHandle) {
561
569
  clearTimeout(timeoutHandle);
562
570
  }
571
+ cleanupTempFile();
563
572
  resolve({
564
573
  success: false,
565
574
  content: "",
@@ -109,14 +109,14 @@ Usage:
109
109
  };
110
110
  }
111
111
 
112
- // Touch file to track it in context
113
- context.messageManager?.touchFile(filePath);
112
+ // Trigger conditional rule loading for this file
113
+ context.messageManager?.triggerFileRead(filePath);
114
114
 
115
- // Enforce read-before-edit: the file must have been read first
116
- if (
117
- context.messageManager &&
118
- !context.messageManager.hasFileInContext(filePath)
119
- ) {
115
+ // Enforce read-before-edit: the file must have been read or written first.
116
+ // readFileState is populated by Read, Write, and Edit tools — single source
117
+ // of truth, aligned with Claude Code's readFileState approach.
118
+ const resolvedPath = resolvePath(filePath, context.workdir);
119
+ if (!context.readFileState?.has(resolvedPath)) {
120
120
  return {
121
121
  success: false,
122
122
  content: "",
@@ -125,8 +125,6 @@ Usage:
125
125
  }
126
126
 
127
127
  try {
128
- const resolvedPath = resolvePath(filePath, context.workdir);
129
-
130
128
  // Read file content
131
129
  let originalContent: string;
132
130
  try {
@@ -195,8 +195,8 @@ Usage:
195
195
  };
196
196
  }
197
197
 
198
- // Touch file to track it in context
199
- context.messageManager?.touchFile(filePath);
198
+ // Trigger conditional rule loading for this file
199
+ context.messageManager?.triggerFileRead(filePath);
200
200
 
201
201
  // Check for binary document formats
202
202
  if (isBinaryDocument(filePath)) {
@@ -68,8 +68,8 @@ Usage:
68
68
  };
69
69
  }
70
70
 
71
- // Touch file to track it in context
72
- context.messageManager?.touchFile(filePath);
71
+ // Trigger conditional rule loading for this file
72
+ context.messageManager?.triggerFileRead(filePath);
73
73
 
74
74
  try {
75
75
  const resolvedPath = resolvePath(filePath, context.workdir);
@@ -30,4 +30,4 @@ export const USER_MEMORY_FILE = path.join(DATA_DIRECTORY, "AGENTS.md");
30
30
  * AI related constants
31
31
  */
32
32
  export const DEFAULT_WAVE_MAX_INPUT_TOKENS = 200000; // Default token limit
33
- export const DEFAULT_WAVE_MAX_OUTPUT_TOKENS = 16384; // Default output token limit
33
+ export const DEFAULT_WAVE_MAX_OUTPUT_TOKENS = 32000; // Default output token limit (aligned with Claude Code)
@@ -117,6 +117,7 @@ export function setupAgentContainer(
117
117
  logger.error("Failed to sync task list with session:", error);
118
118
  });
119
119
  }
120
+ memoryService.clearCache();
120
121
  callbacks.onSessionIdChange?.(sessionId);
121
122
  },
122
123
  },
@@ -1,6 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import * as fsSync from "node:fs";
3
- import { execSync } from "node:child_process";
3
+ import { execFileSync } from "node:child_process";
4
4
 
5
5
  /**
6
6
  * Check if a directory is a git repository
@@ -31,7 +31,7 @@ export function isGitRepository(dirPath: string): string {
31
31
  */
32
32
  export function getGitRepoRoot(cwd: string): string {
33
33
  try {
34
- return execSync("git rev-parse --show-toplevel", {
34
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
35
35
  cwd,
36
36
  encoding: "utf8",
37
37
  stdio: ["ignore", "pipe", "ignore"],
@@ -48,7 +48,7 @@ export function getGitRepoRoot(cwd: string): string {
48
48
  */
49
49
  export function getGitCommonDir(cwd: string): string {
50
50
  try {
51
- const commonDir = execSync("git rev-parse --git-common-dir", {
51
+ const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
52
52
  cwd,
53
53
  encoding: "utf8",
54
54
  stdio: ["ignore", "pipe", "ignore"],
@@ -66,7 +66,7 @@ export function getGitCommonDir(cwd: string): string {
66
66
  */
67
67
  export function getGitMainRepoRoot(cwd: string): string {
68
68
  try {
69
- const output = execSync("git worktree list --porcelain", {
69
+ const output = execFileSync("git", ["worktree", "list", "--porcelain"], {
70
70
  cwd,
71
71
  encoding: "utf8",
72
72
  stdio: ["ignore", "pipe", "ignore"],
@@ -229,7 +229,7 @@ export function getDefaultRemoteBranch(cwd: string): string {
229
229
  */
230
230
  export function hasUncommittedChanges(cwd: string): boolean {
231
231
  try {
232
- const status = execSync("git status --porcelain", {
232
+ const status = execFileSync("git", ["status", "--porcelain"], {
233
233
  cwd,
234
234
  encoding: "utf8",
235
235
  stdio: ["ignore", "pipe", "ignore"],
@@ -249,7 +249,7 @@ export function hasUncommittedChanges(cwd: string): boolean {
249
249
  export function hasNewCommits(cwd: string, baseBranch?: string): boolean {
250
250
  try {
251
251
  const range = baseBranch ? `${baseBranch}..HEAD` : "@{u}..HEAD";
252
- const log = execSync(`git log ${range} --oneline`, {
252
+ const log = execFileSync("git", ["log", range, "--oneline"], {
253
253
  cwd,
254
254
  encoding: "utf8",
255
255
  stdio: ["ignore", "pipe", "ignore"],
@@ -7,6 +7,28 @@ import {
7
7
  import { GatewayConfig } from "../types/config.js";
8
8
  import { logger } from "./globalLogger.js";
9
9
 
10
+ const MAX_RETRIES = 10;
11
+ const BASE_DELAY_MS = 500;
12
+ const MAX_DELAY_MS = 32000;
13
+
14
+ function getRetryDelay(
15
+ attempt: number,
16
+ retryAfterHeader: string | null,
17
+ ): number {
18
+ if (retryAfterHeader) {
19
+ const seconds = parseInt(retryAfterHeader, 10);
20
+ if (!isNaN(seconds)) {
21
+ return seconds * 1000;
22
+ }
23
+ }
24
+ const baseDelay = Math.min(
25
+ BASE_DELAY_MS * Math.pow(2, attempt - 1),
26
+ MAX_DELAY_MS,
27
+ );
28
+ const jitter = Math.random() * 0.25 * baseDelay;
29
+ return baseDelay + jitter;
30
+ }
31
+
10
32
  type CreateParams =
11
33
  | ChatCompletionCreateParamsNonStreaming
12
34
  | ChatCompletionCreateParamsStreaming;
@@ -88,12 +110,11 @@ export class OpenAIClient {
88
110
 
89
111
  const fetchFn = (customFetch as typeof fetch) || fetch;
90
112
  let lastError: (Error & { status?: number; body?: unknown }) | undefined;
91
- const maxRetries = 5;
92
- const initialDelay = 2000;
113
+ let lastRetryAfter: string | null = null;
93
114
 
94
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
115
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
95
116
  if (attempt > 0) {
96
- const delay = initialDelay * Math.pow(2, attempt - 1);
117
+ const delay = getRetryDelay(attempt, lastRetryAfter);
97
118
  await new Promise((resolve) => setTimeout(resolve, delay));
98
119
  }
99
120
 
@@ -110,12 +131,13 @@ export class OpenAIClient {
110
131
  if (e instanceof Error && e.name === "AbortError") {
111
132
  throw e;
112
133
  }
113
- if (attempt < maxRetries) {
134
+ if (attempt < MAX_RETRIES) {
114
135
  logger.warn("OpenAI API network error, retrying...", {
115
136
  attempt: attempt + 1,
116
137
  error: e,
117
138
  });
118
139
  lastError = e as Error;
140
+ lastRetryAfter = null;
119
141
  continue;
120
142
  }
121
143
  throw e;
@@ -144,45 +166,28 @@ export class OpenAIClient {
144
166
  }
145
167
  }
146
168
 
147
- let errorBody: unknown;
169
+ let responseText = "";
148
170
  try {
149
- const text = await response.text();
150
- try {
151
- errorBody = JSON.parse(text);
152
- } catch (e) {
153
- logger.error("Failed to parse error response body as JSON", {
154
- error: e,
155
- text,
156
- });
157
- errorBody = text;
158
- }
171
+ responseText = await response.text();
159
172
  } catch (e) {
160
173
  logger.error("Failed to read error response text", { error: e });
161
- errorBody = {};
162
174
  }
163
175
 
164
176
  const error = new Error(
165
- typeof errorBody === "object" &&
166
- errorBody !== null &&
167
- "error" in errorBody &&
168
- typeof (errorBody as { error: unknown }).error === "object" &&
169
- (errorBody as { error: object }).error !== null &&
170
- "message" in (errorBody as { error: { message: unknown } }).error
171
- ? String((errorBody as { error: { message: string } }).error.message)
172
- : typeof errorBody === "string"
173
- ? errorBody
174
- : response.statusText,
177
+ `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}: ${responseText}`,
175
178
  ) as Error & { status?: number; body?: unknown };
176
179
  error.status = response.status;
177
- error.body = errorBody;
180
+ error.body = responseText;
178
181
 
179
182
  const retryableStatus =
180
183
  response.status === 429 ||
181
184
  (response.status >= 500 && response.status !== 501);
182
- if (retryableStatus && attempt < maxRetries) {
185
+ if (retryableStatus && attempt < MAX_RETRIES) {
186
+ lastRetryAfter = response.headers.get("retry-after");
183
187
  logger.warn("OpenAI API error, retrying...", {
184
188
  attempt: attempt + 1,
185
189
  status: response.status,
190
+ retryAfter: lastRetryAfter,
186
191
  });
187
192
  lastError = error;
188
193
  continue;
@@ -191,7 +196,7 @@ export class OpenAIClient {
191
196
  logger.error("OpenAI API Error:", {
192
197
  status: response.status,
193
198
  statusText: response.statusText,
194
- errorBody,
199
+ body: responseText,
195
200
  });
196
201
  throw error;
197
202
  }
@@ -222,12 +227,16 @@ export class OpenAIClient {
222
227
 
223
228
  const data = trimmedLine.slice(5).trim();
224
229
  if (data === "[DONE]") return;
230
+ if (!data) continue; // Skip empty data lines (keepalive heartbeats)
225
231
 
226
232
  try {
227
233
  const json = JSON.parse(data);
228
234
  yield json as ChatCompletionChunk;
229
235
  } catch (e) {
230
- logger.error("Failed to parse stream chunk", { error: e, data });
236
+ logger.error("Failed to parse stream chunk", {
237
+ error: e instanceof Error ? e.message : String(e),
238
+ data,
239
+ });
231
240
  }
232
241
  }
233
242
  }
package/src/utils/path.ts CHANGED
@@ -54,3 +54,13 @@ export function getDisplayPath(filePath: string, workdir: string): string {
54
54
  }
55
55
  return filePath;
56
56
  }
57
+
58
+ /**
59
+ * Convert backslashes to forward slashes for shell compatibility on Windows.
60
+ * On Windows, os.tmpdir() returns paths with backslashes (e.g., C:\Users\foo\Temp),
61
+ * which are treated as escape characters when interpolated into shell command strings.
62
+ * Returns the path as-is on non-Windows platforms.
63
+ */
64
+ export function toPosixPath(p: string): string {
65
+ return process.platform === "win32" ? p.replace(/\\/g, "/") : p;
66
+ }
@@ -1,18 +1,12 @@
1
1
  import type { Message } from "../types/messaging.js";
2
2
  import type { Task } from "../types/tasks.js";
3
- import type { ChatCompletionMessageParam } from "openai/resources.js";
4
3
 
5
4
  export const TASK_REMINDER_CONFIG = {
6
5
  TURNS_SINCE_WRITE: 10,
7
6
  TURNS_BETWEEN_REMINDERS: 10,
8
7
  };
9
8
 
10
- const TASK_MANAGEMENT_TOOLS = new Set([
11
- "TaskCreate",
12
- "TaskUpdate",
13
- "TaskList",
14
- "TaskGet",
15
- ]);
9
+ const TASK_MANAGEMENT_TOOLS = new Set(["TaskCreate", "TaskUpdate"]);
16
10
  const TASK_REMINDER_MARKER = "<!-- task-reminder -->";
17
11
 
18
12
  function isQualifyingAssistantMessage(message: Message): boolean {
@@ -66,43 +60,37 @@ export function getTaskReminderTurnCounts(messages: Message[]): {
66
60
  export function buildTaskReminderText(tasks: Task[]): string {
67
61
  let text = `${TASK_REMINDER_MARKER}\n`;
68
62
  text +=
69
- "Here is a gentle reminder about the task list. The user may not have explicitly asked for the following to be done, but you should check the task list to see if any tasks need attention.\n";
63
+ "The task tools haven't been used recently. If you're working on tasks that would benefit from tracking progress, consider using TaskCreate to add new tasks and TaskUpdate to update task status (set to in_progress when starting, completed when done). Also consider cleaning up the task list if it has become stale. Only use these if relevant to the current work. This is just a gentle reminder - ignore if not applicable. Make sure that you never mention this reminder to the user";
70
64
 
71
65
  if (tasks.length > 0) {
72
- text += "\n";
66
+ text += "\n\nHere are the existing tasks:\n";
73
67
  for (const task of tasks) {
74
68
  text += `#${task.id} [${task.status}] ${task.subject}\n`;
75
69
  }
76
- } else {
77
- text += "The task list is currently empty.\n";
78
70
  }
79
71
 
80
72
  return text;
81
73
  }
82
74
 
83
75
  export function maybeInjectTaskReminder(
84
- messages: ChatCompletionMessageParam[],
85
76
  turnCounts: {
86
77
  turnsSinceLastTaskManagement: number;
87
78
  turnsSinceLastReminder: number;
88
79
  },
89
80
  tasks: Task[],
90
- ): void {
81
+ ): string | null {
91
82
  if (
92
83
  turnCounts.turnsSinceLastTaskManagement <
93
84
  TASK_REMINDER_CONFIG.TURNS_SINCE_WRITE
94
85
  ) {
95
- return;
86
+ return null;
96
87
  }
97
88
  if (
98
89
  turnCounts.turnsSinceLastReminder <
99
90
  TASK_REMINDER_CONFIG.TURNS_BETWEEN_REMINDERS
100
91
  ) {
101
- return;
92
+ return null;
102
93
  }
103
94
 
104
- messages.push({
105
- role: "user",
106
- content: buildTaskReminderText(tasks),
107
- } as ChatCompletionMessageParam);
95
+ return buildTaskReminderText(tasks);
108
96
  }