wave-agent-sdk 0.17.9 → 0.17.11
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.
- package/builtin/skills/settings/ENV.md +1 -1
- package/dist/managers/aiManager.d.ts +11 -4
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +232 -200
- package/dist/managers/backgroundTaskManager.js +1 -1
- package/dist/managers/bangManager.js +1 -1
- package/dist/managers/hookManager.js +2 -4
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +1 -3
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +1 -8
- package/dist/managers/toolManager.d.ts +6 -0
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +17 -12
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +20 -12
- package/dist/services/authService.d.ts +0 -1
- package/dist/services/authService.d.ts.map +1 -1
- package/dist/services/authService.js +1 -29
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +13 -0
- package/dist/services/fileWatcher.d.ts +2 -0
- package/dist/services/fileWatcher.d.ts.map +1 -1
- package/dist/services/fileWatcher.js +12 -1
- package/dist/services/jsonlHandler.d.ts +1 -1
- package/dist/services/jsonlHandler.d.ts.map +1 -1
- package/dist/services/jsonlHandler.js +7 -22
- package/dist/services/session.d.ts +2 -9
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +16 -46
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +3 -1
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +28 -1
- package/dist/tools/enterPlanMode.d.ts.map +1 -1
- package/dist/tools/enterPlanMode.js +8 -0
- package/dist/tools/exitPlanMode.d.ts.map +1 -1
- package/dist/tools/exitPlanMode.js +8 -0
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +18 -10
- package/dist/tools/types.d.ts +8 -2
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/writeTool.d.ts.map +1 -1
- package/dist/tools/writeTool.js +14 -1
- package/dist/types/agent.d.ts +0 -2
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/utils/cacheControlUtils.d.ts +20 -15
- package/dist/utils/cacheControlUtils.d.ts.map +1 -1
- package/dist/utils/cacheControlUtils.js +69 -66
- package/dist/utils/constants.d.ts +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +1 -6
- package/dist/utils/taskReminder.d.ts +17 -0
- package/dist/utils/taskReminder.d.ts.map +1 -0
- package/dist/utils/taskReminder.js +76 -0
- package/package.json +1 -1
- package/src/managers/aiManager.ts +326 -262
- package/src/managers/backgroundTaskManager.ts +1 -1
- package/src/managers/bangManager.ts +1 -1
- package/src/managers/hookManager.ts +6 -6
- package/src/managers/mcpManager.ts +2 -4
- package/src/managers/messageManager.ts +1 -9
- package/src/managers/toolManager.ts +18 -12
- package/src/services/aiService.ts +23 -12
- package/src/services/authService.ts +1 -31
- package/src/services/configurationService.ts +21 -0
- package/src/services/fileWatcher.ts +15 -1
- package/src/services/jsonlHandler.ts +7 -27
- package/src/services/session.ts +16 -57
- package/src/tools/bashTool.ts +3 -1
- package/src/tools/editTool.ts +31 -1
- package/src/tools/enterPlanMode.ts +9 -0
- package/src/tools/exitPlanMode.ts +10 -0
- package/src/tools/readTool.ts +20 -10
- package/src/tools/types.ts +15 -3
- package/src/tools/writeTool.ts +15 -1
- package/src/types/agent.ts +0 -2
- package/src/types/config.ts +1 -0
- package/src/utils/cacheControlUtils.ts +77 -95
- package/src/utils/constants.ts +1 -1
- package/src/utils/containerSetup.ts +3 -7
- package/src/utils/taskReminder.ts +108 -0
|
@@ -988,9 +988,9 @@ export class HookManager {
|
|
|
988
988
|
transcriptPath,
|
|
989
989
|
cwd: this.workdir,
|
|
990
990
|
endSource: source,
|
|
991
|
-
env:
|
|
992
|
-
|
|
993
|
-
|
|
991
|
+
env: Object.fromEntries(
|
|
992
|
+
Object.entries(process.env).filter((e) => e[1] !== undefined),
|
|
993
|
+
) as Record<string, string>,
|
|
994
994
|
};
|
|
995
995
|
|
|
996
996
|
const results = await this.executeHooks("SessionEnd", context);
|
|
@@ -1060,9 +1060,9 @@ export class HookManager {
|
|
|
1060
1060
|
transcriptPath,
|
|
1061
1061
|
cwd: this.workdir,
|
|
1062
1062
|
compactSummary,
|
|
1063
|
-
env:
|
|
1064
|
-
|
|
1065
|
-
|
|
1063
|
+
env: Object.fromEntries(
|
|
1064
|
+
Object.entries(process.env).filter((e) => e[1] !== undefined),
|
|
1065
|
+
) as Record<string, string>,
|
|
1066
1066
|
};
|
|
1067
1067
|
|
|
1068
1068
|
const results = await this.executeHooks("PostCompact", context);
|
|
@@ -432,11 +432,9 @@ export class McpManager {
|
|
|
432
432
|
`MCP server ${name} with type "stdio" requires a 'command'`,
|
|
433
433
|
);
|
|
434
434
|
}
|
|
435
|
-
|
|
436
|
-
this.container.get<Record<string, string>>("MergedEnv") ||
|
|
437
|
-
(process.env as Record<string, string>);
|
|
435
|
+
|
|
438
436
|
const env: Record<string, string> = {
|
|
439
|
-
...
|
|
437
|
+
...(process.env as Record<string, string>),
|
|
440
438
|
...(server.config.env || {}),
|
|
441
439
|
};
|
|
442
440
|
|
|
@@ -308,14 +308,6 @@ export class MessageManager {
|
|
|
308
308
|
return;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
// Filter out meta messages
|
|
312
|
-
const meaningfulMessages = unsavedMessages.filter((m) => !m.isMeta);
|
|
313
|
-
|
|
314
|
-
if (meaningfulMessages.length === 0) {
|
|
315
|
-
// No meaningful messages to save
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
311
|
// Create session if needed (only when we have messages to save)
|
|
320
312
|
if (this.savedMessageCount === 0) {
|
|
321
313
|
// This is the first time saving messages, so create the session
|
|
@@ -325,7 +317,7 @@ export class MessageManager {
|
|
|
325
317
|
// Use JSONL format for new sessions
|
|
326
318
|
await appendMessages(
|
|
327
319
|
this.sessionId,
|
|
328
|
-
|
|
320
|
+
unsavedMessages,
|
|
329
321
|
this.workdir,
|
|
330
322
|
this.sessionType,
|
|
331
323
|
this.rootSessionId,
|
|
@@ -356,24 +356,12 @@ class ToolManager {
|
|
|
356
356
|
}): ChatCompletionFunctionTool[] {
|
|
357
357
|
const permissionManager =
|
|
358
358
|
this.container.get<PermissionManager>("PermissionManager");
|
|
359
|
-
const effectivePermissionMode = this.getPermissionMode();
|
|
360
359
|
const builtInToolsConfig = Array.from(this.toolsRegistry.values())
|
|
361
360
|
.filter((tool) => {
|
|
362
361
|
// If tool is explicitly denied by name in permission rules, filter it out
|
|
363
362
|
if (permissionManager?.isToolDenied(tool.name)) {
|
|
364
363
|
return false;
|
|
365
364
|
}
|
|
366
|
-
if (effectivePermissionMode === "bypassPermissions") {
|
|
367
|
-
if (tool.name === "ExitPlanMode") {
|
|
368
|
-
return false;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
if (tool.name === "ExitPlanMode") {
|
|
372
|
-
return effectivePermissionMode === "plan";
|
|
373
|
-
}
|
|
374
|
-
if (tool.name === "EnterPlanMode") {
|
|
375
|
-
return effectivePermissionMode !== "plan";
|
|
376
|
-
}
|
|
377
365
|
return true;
|
|
378
366
|
})
|
|
379
367
|
.map((tool) => {
|
|
@@ -408,6 +396,24 @@ class ToolManager {
|
|
|
408
396
|
return Array.from(this.toolsRegistry.values());
|
|
409
397
|
}
|
|
410
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Check whether a tool is safe to execute in parallel with other tools.
|
|
401
|
+
* Built-in tools default to safe (parallel); Edit and Write opt out.
|
|
402
|
+
* MCP tools are conservatively non-safe (can't inspect side effects).
|
|
403
|
+
*/
|
|
404
|
+
public isConcurrencySafe(name: string): boolean {
|
|
405
|
+
// MCP tools are conservatively non-safe
|
|
406
|
+
if (this.mcpManager.isMcpTool(name)) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
const plugin = this.toolsRegistry.get(name);
|
|
410
|
+
if (plugin) {
|
|
411
|
+
return plugin.isConcurrencySafe ?? true;
|
|
412
|
+
}
|
|
413
|
+
// Unknown tools are conservatively non-safe
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
411
417
|
/**
|
|
412
418
|
* Get the current permission mode
|
|
413
419
|
*/
|
|
@@ -12,8 +12,7 @@ import { addOnceAbortListener } from "../utils/abortUtils.js";
|
|
|
12
12
|
import type { GatewayConfig, ModelConfig } from "../types/index.js";
|
|
13
13
|
import { ConfigurationError, CONFIG_ERRORS } from "../types/index.js";
|
|
14
14
|
import {
|
|
15
|
-
|
|
16
|
-
addCacheControlToLastTool,
|
|
15
|
+
transformMessagesForExplicitCache,
|
|
17
16
|
supportsPromptCaching,
|
|
18
17
|
extendUsageWithCacheMetrics,
|
|
19
18
|
type ClaudeUsage,
|
|
@@ -282,15 +281,10 @@ export async function callAgent(
|
|
|
282
281
|
processedTools = tools;
|
|
283
282
|
|
|
284
283
|
if (supportsPromptCaching(currentModel)) {
|
|
285
|
-
openaiMessages =
|
|
284
|
+
openaiMessages = transformMessagesForExplicitCache(
|
|
286
285
|
openaiMessages,
|
|
287
286
|
currentModel,
|
|
288
287
|
);
|
|
289
|
-
|
|
290
|
-
// Apply cache control to tools separately
|
|
291
|
-
if (tools && tools.length > 0) {
|
|
292
|
-
processedTools = addCacheControlToLastTool(tools);
|
|
293
|
-
}
|
|
294
288
|
}
|
|
295
289
|
|
|
296
290
|
// Get model configuration - use injected modelConfig with optional override
|
|
@@ -845,6 +839,7 @@ export async function compactMessages(
|
|
|
845
839
|
fastModel: _fastModel,
|
|
846
840
|
maxTokens: _maxTokens,
|
|
847
841
|
permissionMode: _permissionMode,
|
|
842
|
+
fastModelConfig: _fastModelConfig,
|
|
848
843
|
...extraParams
|
|
849
844
|
} = modelConfig;
|
|
850
845
|
void _model;
|
|
@@ -852,10 +847,16 @@ export async function compactMessages(
|
|
|
852
847
|
void _maxTokens;
|
|
853
848
|
void _permissionMode;
|
|
854
849
|
|
|
850
|
+
// When a fast model override is provided, use the fast model's hyperparams
|
|
851
|
+
// (if configured); otherwise fall back to the agent model's extraParams.
|
|
852
|
+
const activeExtraParams = options.model
|
|
853
|
+
? _fastModelConfig || {}
|
|
854
|
+
: extraParams;
|
|
855
|
+
|
|
855
856
|
const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
|
|
856
857
|
temperature: 0.1,
|
|
857
858
|
max_tokens: 8192,
|
|
858
|
-
...
|
|
859
|
+
...activeExtraParams,
|
|
859
860
|
});
|
|
860
861
|
|
|
861
862
|
try {
|
|
@@ -961,6 +962,7 @@ export async function processWebContent(
|
|
|
961
962
|
fastModel: _fastModel,
|
|
962
963
|
maxTokens: _maxTokens,
|
|
963
964
|
permissionMode: _permissionMode,
|
|
965
|
+
fastModelConfig: _fastModelConfig,
|
|
964
966
|
...extraParams
|
|
965
967
|
} = modelConfig;
|
|
966
968
|
void _model;
|
|
@@ -968,10 +970,16 @@ export async function processWebContent(
|
|
|
968
970
|
void _maxTokens;
|
|
969
971
|
void _permissionMode;
|
|
970
972
|
|
|
973
|
+
// When a fast model override is provided, use the fast model's hyperparams
|
|
974
|
+
// (if configured); otherwise fall back to the agent model's extraParams.
|
|
975
|
+
const activeExtraParams = options.model
|
|
976
|
+
? _fastModelConfig || {}
|
|
977
|
+
: extraParams;
|
|
978
|
+
|
|
971
979
|
const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
|
|
972
980
|
temperature: 0.1,
|
|
973
981
|
max_tokens: 4096,
|
|
974
|
-
...
|
|
982
|
+
...activeExtraParams,
|
|
975
983
|
});
|
|
976
984
|
|
|
977
985
|
try {
|
|
@@ -1177,17 +1185,20 @@ export async function evaluateGoal(
|
|
|
1177
1185
|
fastModel: _fastModel,
|
|
1178
1186
|
maxTokens: _maxTokens,
|
|
1179
1187
|
permissionMode: _permissionMode,
|
|
1180
|
-
|
|
1188
|
+
fastModelConfig: _fastModelConfig,
|
|
1181
1189
|
} = modelConfig;
|
|
1182
1190
|
void _model;
|
|
1183
1191
|
void _fastModel;
|
|
1184
1192
|
void _maxTokens;
|
|
1185
1193
|
void _permissionMode;
|
|
1186
1194
|
|
|
1195
|
+
// evaluateGoal always uses the fast model; use its hyperparams if configured.
|
|
1196
|
+
const activeExtraParams = _fastModelConfig || {};
|
|
1197
|
+
|
|
1187
1198
|
const openaiModelConfig = getModelConfig(model, {
|
|
1188
1199
|
temperature: 0,
|
|
1189
1200
|
max_tokens: 200,
|
|
1190
|
-
...
|
|
1201
|
+
...activeExtraParams,
|
|
1191
1202
|
});
|
|
1192
1203
|
|
|
1193
1204
|
// Strip images from messages to reduce token usage (same as compact)
|
|
@@ -19,16 +19,12 @@ import * as os from "os";
|
|
|
19
19
|
import { randomBytes } from "crypto";
|
|
20
20
|
import { createServer, Server } from "http";
|
|
21
21
|
import { URL } from "url";
|
|
22
|
-
import { execFile } from "child_process";
|
|
23
|
-
import { promisify } from "util";
|
|
24
22
|
import type { AuthConfig, AuthUser, TokenResponse } from "../types/auth.js";
|
|
25
23
|
import { logger } from "../utils/globalLogger.js";
|
|
26
24
|
|
|
27
25
|
/** Persistent anonymous ID for telemetry fallback when SSO is not authenticated. */
|
|
28
26
|
let _anonymousId: string | undefined;
|
|
29
27
|
|
|
30
|
-
const execFileAsync = promisify(execFile);
|
|
31
|
-
|
|
32
28
|
export class AuthService {
|
|
33
29
|
private static instance: AuthService;
|
|
34
30
|
private _serverUrl: string | undefined;
|
|
@@ -274,15 +270,8 @@ export class AuthService {
|
|
|
274
270
|
const callbackUrl = `http://127.0.0.1:${port}`;
|
|
275
271
|
const authUrl = `${serverUrl}/login?callback_url=${encodeURIComponent(callbackUrl)}`;
|
|
276
272
|
|
|
277
|
-
// Notify caller of the auth URL
|
|
273
|
+
// Notify caller of the auth URL; caller is responsible for opening browser
|
|
278
274
|
options?.onAuthUrl?.(authUrl);
|
|
279
|
-
|
|
280
|
-
// Try to open browser; if it fails, keep server alive for manual visit
|
|
281
|
-
try {
|
|
282
|
-
await this.openBrowser(authUrl);
|
|
283
|
-
} catch {
|
|
284
|
-
// Browser not available — server stays alive
|
|
285
|
-
}
|
|
286
275
|
});
|
|
287
276
|
|
|
288
277
|
// If manual code reading is provided, race between server callback and user input
|
|
@@ -315,25 +304,6 @@ export class AuthService {
|
|
|
315
304
|
});
|
|
316
305
|
}
|
|
317
306
|
|
|
318
|
-
private async openBrowser(url: string): Promise<void> {
|
|
319
|
-
const platform = process.platform;
|
|
320
|
-
let command: string;
|
|
321
|
-
let args: string[];
|
|
322
|
-
|
|
323
|
-
if (platform === "darwin") {
|
|
324
|
-
command = "open";
|
|
325
|
-
args = [url];
|
|
326
|
-
} else if (platform === "win32") {
|
|
327
|
-
command = "cmd";
|
|
328
|
-
args = ["/c", "start", "", url];
|
|
329
|
-
} else {
|
|
330
|
-
command = "xdg-open";
|
|
331
|
-
args = [url];
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
await execFileAsync(command, args);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
307
|
isSSOAuthenticated(): boolean {
|
|
338
308
|
const config = this.loadAuth();
|
|
339
309
|
if (!config.SSO_TOKEN) return false;
|
|
@@ -532,6 +532,27 @@ export class ConfigurationService {
|
|
|
532
532
|
permissionMode: permissionMode ?? this.options.permissionMode,
|
|
533
533
|
};
|
|
534
534
|
|
|
535
|
+
// Resolve fast model hyperparams from models[fastModel], stripping
|
|
536
|
+
// structural fields so only hyperparameters remain. Set on baseConfig
|
|
537
|
+
// before the modelSpecificConfig spread so it isn't overwritten.
|
|
538
|
+
const fastModelConfigSource =
|
|
539
|
+
resolvedFastModel &&
|
|
540
|
+
this.currentConfiguration?.models?.[resolvedFastModel];
|
|
541
|
+
if (fastModelConfigSource) {
|
|
542
|
+
const {
|
|
543
|
+
model: _fm,
|
|
544
|
+
fastModel: _ffm,
|
|
545
|
+
maxTokens: _fmt,
|
|
546
|
+
permissionMode: _fpm,
|
|
547
|
+
...fastHyperparams
|
|
548
|
+
} = fastModelConfigSource;
|
|
549
|
+
void _fm;
|
|
550
|
+
void _ffm;
|
|
551
|
+
void _fmt;
|
|
552
|
+
void _fpm;
|
|
553
|
+
baseConfig.fastModelConfig = fastHyperparams;
|
|
554
|
+
}
|
|
555
|
+
|
|
535
556
|
// Merge model-specific settings from configuration
|
|
536
557
|
const modelSpecificConfig =
|
|
537
558
|
resolvedAgentModel &&
|
|
@@ -22,6 +22,8 @@ export interface FileWatcherConfig {
|
|
|
22
22
|
maxRetries: number; // Default: 3
|
|
23
23
|
fallbackPolling: boolean; // Default: false
|
|
24
24
|
ignoreTempFiles: boolean; // Default: true
|
|
25
|
+
depth: number; // default: 5
|
|
26
|
+
ignored: string[]; // default: ['**/node_modules/**', '**/.git/**', '**/*.swp', '**/*~']
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export interface FileWatcherStatus {
|
|
@@ -59,6 +61,8 @@ export class FileWatcherService extends EventEmitter {
|
|
|
59
61
|
maxRetries: 3,
|
|
60
62
|
fallbackPolling: false,
|
|
61
63
|
ignoreTempFiles: true,
|
|
64
|
+
depth: 5,
|
|
65
|
+
ignored: ["**/node_modules/**", "**/.git/**", "**/*.swp", "**/*~"],
|
|
62
66
|
...config,
|
|
63
67
|
};
|
|
64
68
|
}
|
|
@@ -185,6 +189,8 @@ export class FileWatcherService extends EventEmitter {
|
|
|
185
189
|
this.globalWatcher = chokidar.watch([], {
|
|
186
190
|
persistent: true,
|
|
187
191
|
ignoreInitial: true,
|
|
192
|
+
depth: entry.config.depth,
|
|
193
|
+
ignored: entry.config.ignored,
|
|
188
194
|
awaitWriteFinish: {
|
|
189
195
|
stabilityThreshold: entry.config.stabilityThreshold,
|
|
190
196
|
pollInterval: entry.config.pollInterval,
|
|
@@ -262,7 +268,15 @@ export class FileWatcherService extends EventEmitter {
|
|
|
262
268
|
|
|
263
269
|
this.globalWatcher.on("error", (err: unknown) => {
|
|
264
270
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
265
|
-
|
|
271
|
+
const isEMFILE = (error as NodeJS.ErrnoException).code === "EMFILE";
|
|
272
|
+
if (isEMFILE) {
|
|
273
|
+
this.logger?.warn(
|
|
274
|
+
"FileWatcher: Too many open files (EMFILE). " +
|
|
275
|
+
"Consider increasing the limit: `ulimit -n 10240` (macOS) or `ulimit -n 65536` (Linux).",
|
|
276
|
+
);
|
|
277
|
+
} else {
|
|
278
|
+
this.logger?.error(`FileWatcher: File watcher error: ${error.message}`);
|
|
279
|
+
}
|
|
266
280
|
this.emit("watcherError", error);
|
|
267
281
|
});
|
|
268
282
|
}
|
|
@@ -229,19 +229,7 @@ export class JsonlHandler {
|
|
|
229
229
|
// Extract filename from path
|
|
230
230
|
const filename = filePath.split("/").pop() || "";
|
|
231
231
|
|
|
232
|
-
//
|
|
233
|
-
const newSubagentMatch = filename.match(
|
|
234
|
-
/^subagent-(\d{14}-[0-9a-f]{8})\.jsonl$/,
|
|
235
|
-
);
|
|
236
|
-
if (newSubagentMatch) {
|
|
237
|
-
return { sessionId: newSubagentMatch[1], sessionType: "subagent" };
|
|
238
|
-
}
|
|
239
|
-
const newMainMatch = filename.match(/^(\d{14}-[0-9a-f]{8})\.jsonl$/);
|
|
240
|
-
if (newMainMatch) {
|
|
241
|
-
return { sessionId: newMainMatch[1], sessionType: "main" };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Old UUID format (backward compat)
|
|
232
|
+
// Check if it's a subagent session
|
|
245
233
|
const subagentMatch = filename.match(
|
|
246
234
|
/^subagent-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
247
235
|
);
|
|
@@ -252,6 +240,7 @@ export class JsonlHandler {
|
|
|
252
240
|
};
|
|
253
241
|
}
|
|
254
242
|
|
|
243
|
+
// Check if it's a main session
|
|
255
244
|
const mainMatch = filename.match(
|
|
256
245
|
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
257
246
|
);
|
|
@@ -271,26 +260,18 @@ export class JsonlHandler {
|
|
|
271
260
|
* @returns True if valid, false otherwise
|
|
272
261
|
*/
|
|
273
262
|
isValidSessionFilename(filename: string): boolean {
|
|
274
|
-
//
|
|
275
|
-
const newFormatPattern = /^(\d{14}-[0-9a-f]{8})\.jsonl$/;
|
|
276
|
-
const newSubagentPattern = /^subagent-(\d{14}-[0-9a-f]{8})\.jsonl$/;
|
|
277
|
-
// Old UUID format patterns (backward compat)
|
|
263
|
+
// UUID validation patterns
|
|
278
264
|
const uuidPattern =
|
|
279
265
|
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/;
|
|
280
266
|
const subagentPattern =
|
|
281
267
|
/^subagent-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/;
|
|
282
268
|
|
|
283
|
-
return (
|
|
284
|
-
newFormatPattern.test(filename) ||
|
|
285
|
-
newSubagentPattern.test(filename) ||
|
|
286
|
-
uuidPattern.test(filename) ||
|
|
287
|
-
subagentPattern.test(filename)
|
|
288
|
-
);
|
|
269
|
+
return uuidPattern.test(filename) || subagentPattern.test(filename);
|
|
289
270
|
}
|
|
290
271
|
|
|
291
272
|
/**
|
|
292
273
|
* Generate simple filename for sessions
|
|
293
|
-
* @param sessionId -
|
|
274
|
+
* @param sessionId - UUID session identifier
|
|
294
275
|
* @param sessionType - Type of session ("main" or "subagent")
|
|
295
276
|
* @returns Generated filename
|
|
296
277
|
*/
|
|
@@ -298,11 +279,10 @@ export class JsonlHandler {
|
|
|
298
279
|
sessionId: string,
|
|
299
280
|
sessionType: "main" | "subagent",
|
|
300
281
|
): string {
|
|
301
|
-
// Validate sessionId is
|
|
302
|
-
const newFormatPattern = /^\d{14}-[0-9a-f]{8}$/;
|
|
282
|
+
// Validate sessionId is a valid UUID
|
|
303
283
|
const uuidPattern =
|
|
304
284
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
305
|
-
if (!
|
|
285
|
+
if (!uuidPattern.test(sessionId)) {
|
|
306
286
|
throw new Error(`Invalid session ID format: ${sessionId}`);
|
|
307
287
|
}
|
|
308
288
|
|
package/src/services/session.ts
CHANGED
|
@@ -63,43 +63,11 @@ export interface SessionIndex {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* Generate a new session ID
|
|
67
|
-
*
|
|
68
|
-
* @returns Timestamp-prefixed session ID string
|
|
66
|
+
* Generate a new session ID using Node.js native crypto.randomUUID()
|
|
67
|
+
* @returns UUID string for session identification
|
|
69
68
|
*/
|
|
70
69
|
export function generateSessionId(): string {
|
|
71
|
-
|
|
72
|
-
const ts = [
|
|
73
|
-
now.getFullYear(),
|
|
74
|
-
String(now.getMonth() + 1).padStart(2, "0"),
|
|
75
|
-
String(now.getDate()).padStart(2, "0"),
|
|
76
|
-
String(now.getHours()).padStart(2, "0"),
|
|
77
|
-
String(now.getMinutes()).padStart(2, "0"),
|
|
78
|
-
String(now.getSeconds()).padStart(2, "0"),
|
|
79
|
-
].join("");
|
|
80
|
-
const shortId = randomUUID().slice(0, 8);
|
|
81
|
-
return `${ts}-${shortId}`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Parse the timestamp prefix from a session ID back into a Date
|
|
86
|
-
* Format: {YYYYMMDDHHmmss}-{8hex} (e.g. 20260527143025-a1b2c3d4)
|
|
87
|
-
* @returns Date from the session ID timestamp prefix
|
|
88
|
-
*/
|
|
89
|
-
export function parseSessionIdTimestamp(sessionId: string): Date {
|
|
90
|
-
const match = sessionId.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);
|
|
91
|
-
if (!match) {
|
|
92
|
-
return new Date(); // Fallback to now for legacy UUID-format session IDs
|
|
93
|
-
}
|
|
94
|
-
const [, year, month, day, hour, minute, second] = match;
|
|
95
|
-
return new Date(
|
|
96
|
-
Number(year),
|
|
97
|
-
Number(month) - 1,
|
|
98
|
-
Number(day),
|
|
99
|
-
Number(hour),
|
|
100
|
-
Number(minute),
|
|
101
|
-
Number(second),
|
|
102
|
-
);
|
|
70
|
+
return randomUUID();
|
|
103
71
|
}
|
|
104
72
|
|
|
105
73
|
/**
|
|
@@ -293,9 +261,9 @@ export async function appendMessages(
|
|
|
293
261
|
(await getFirstMessageContent(sessionId, workdir)) || undefined;
|
|
294
262
|
}
|
|
295
263
|
|
|
296
|
-
// Derive createdAt from session ID
|
|
264
|
+
// Derive createdAt from session ID if not in index
|
|
297
265
|
if (!createdAt) {
|
|
298
|
-
createdAt = new Date(
|
|
266
|
+
createdAt = new Date();
|
|
299
267
|
}
|
|
300
268
|
|
|
301
269
|
await updateSessionIndex(projectDir.encodedPath, {
|
|
@@ -520,17 +488,15 @@ export async function listSessionsFromJsonl(
|
|
|
520
488
|
try {
|
|
521
489
|
const filePath = join(projectDir.encodedPath, file);
|
|
522
490
|
|
|
523
|
-
// Validate main session filename format (
|
|
524
|
-
const newFormatMatch = file.match(/^(\d{14}-[0-9a-f]{8})\.jsonl$/);
|
|
491
|
+
// Validate main session filename format (UUID.jsonl)
|
|
525
492
|
const uuidMatch = file.match(
|
|
526
493
|
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
527
494
|
);
|
|
528
|
-
|
|
529
|
-
if (!match) {
|
|
495
|
+
if (!uuidMatch) {
|
|
530
496
|
continue; // Skip invalid filenames
|
|
531
497
|
}
|
|
532
498
|
|
|
533
|
-
const sessionId =
|
|
499
|
+
const sessionId = uuidMatch[1];
|
|
534
500
|
|
|
535
501
|
// PERFORMANCE OPTIMIZATION: Only read the last message for timestamps and tokens
|
|
536
502
|
const jsonlHandler = new JsonlHandler();
|
|
@@ -557,7 +523,7 @@ export async function listSessionsFromJsonl(
|
|
|
557
523
|
sessionType: "main",
|
|
558
524
|
subagentType: undefined, // No longer stored in metadata
|
|
559
525
|
workdir: projectDir.originalPath,
|
|
560
|
-
createdAt: new Date(
|
|
526
|
+
createdAt: new Date(),
|
|
561
527
|
lastActiveAt,
|
|
562
528
|
latestTotalTokens: lastMessage?.usage
|
|
563
529
|
? extractLatestTotalTokens([lastMessage])
|
|
@@ -720,24 +686,17 @@ export async function cleanupExpiredSessionsFromJsonl(
|
|
|
720
686
|
);
|
|
721
687
|
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
722
688
|
const index = JSON.parse(indexContent) as SessionIndex;
|
|
723
|
-
// New timestamp-prefixed format patterns
|
|
724
|
-
const newMainMatch = file.match(/^(\d{14}-[0-9a-f]{8})\.jsonl$/);
|
|
725
|
-
const newSubagentMatch = file.match(
|
|
726
|
-
/^subagent-(\d{14}-[0-9a-f]{8})\.jsonl$/,
|
|
727
|
-
);
|
|
728
|
-
// Old UUID format patterns (backward compat)
|
|
729
689
|
const uuidMatch = file.match(
|
|
730
|
-
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{
|
|
690
|
+
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
731
691
|
);
|
|
732
692
|
const subagentMatch = file.match(
|
|
733
|
-
/^subagent-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{
|
|
693
|
+
/^subagent-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
734
694
|
);
|
|
735
|
-
const sessionId =
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
null;
|
|
695
|
+
const sessionId = uuidMatch
|
|
696
|
+
? uuidMatch[1]
|
|
697
|
+
: subagentMatch
|
|
698
|
+
? subagentMatch[1]
|
|
699
|
+
: null;
|
|
741
700
|
|
|
742
701
|
if (sessionId && index.sessions[sessionId]) {
|
|
743
702
|
delete index.sessions[sessionId];
|
package/src/tools/bashTool.ts
CHANGED
|
@@ -240,7 +240,9 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
240
240
|
stdio: "pipe",
|
|
241
241
|
detached: true,
|
|
242
242
|
cwd: context.workdir,
|
|
243
|
-
env:
|
|
243
|
+
env: {
|
|
244
|
+
...process.env,
|
|
245
|
+
},
|
|
244
246
|
});
|
|
245
247
|
|
|
246
248
|
let outputBuffer = "";
|
package/src/tools/editTool.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { readFile, writeFile } from "fs/promises";
|
|
1
|
+
import { readFile, writeFile, stat } from "fs/promises";
|
|
2
|
+
import { createHash } from "crypto";
|
|
2
3
|
import { logger } from "../utils/globalLogger.js";
|
|
3
4
|
import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
4
5
|
import { resolvePath, getDisplayPath } from "../utils/path.js";
|
|
@@ -21,6 +22,7 @@ function formatCompactParams(
|
|
|
21
22
|
*/
|
|
22
23
|
export const editTool: ToolPlugin = {
|
|
23
24
|
name: EDIT_TOOL_NAME,
|
|
25
|
+
isConcurrencySafe: false,
|
|
24
26
|
formatCompactParams,
|
|
25
27
|
prompt: () =>
|
|
26
28
|
`Performs exact string replacements in files.
|
|
@@ -137,6 +139,22 @@ Usage:
|
|
|
137
139
|
};
|
|
138
140
|
}
|
|
139
141
|
|
|
142
|
+
// Staleness check: file must not have been modified since last Read
|
|
143
|
+
if (context.readFileState) {
|
|
144
|
+
const state = context.readFileState.get(resolvedPath);
|
|
145
|
+
if (state) {
|
|
146
|
+
const currentStats = await stat(resolvedPath);
|
|
147
|
+
if (currentStats.mtime.getTime() !== state.mtime) {
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
content: "",
|
|
151
|
+
error:
|
|
152
|
+
"File has been unexpectedly modified since last read. Read it again before editing it.",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
140
158
|
// Normalize line endings for matching
|
|
141
159
|
const normalizedContent = originalContent.replace(/\r\n/g, "\n");
|
|
142
160
|
const normalizedOldString = oldString.replace(/\r\n/g, "\n");
|
|
@@ -240,6 +258,18 @@ Usage:
|
|
|
240
258
|
};
|
|
241
259
|
}
|
|
242
260
|
|
|
261
|
+
// Update readFileState so subsequent serialized edits see fresh state
|
|
262
|
+
if (context.readFileState) {
|
|
263
|
+
const newStats = await stat(resolvedPath);
|
|
264
|
+
const hash = createHash("sha256").update(newContent).digest("hex");
|
|
265
|
+
context.readFileState.set(resolvedPath, {
|
|
266
|
+
mtime: newStats.mtime.getTime(),
|
|
267
|
+
hash,
|
|
268
|
+
offset: undefined,
|
|
269
|
+
limit: undefined,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
243
273
|
const shortResult = replaceAll
|
|
244
274
|
? `Replaced ${replacementCount} instances`
|
|
245
275
|
: "Text replaced successfully";
|
|
@@ -49,6 +49,15 @@ export const enterPlanModeTool: ToolPlugin = {
|
|
|
49
49
|
context: ToolContext,
|
|
50
50
|
): Promise<ToolResult> => {
|
|
51
51
|
try {
|
|
52
|
+
// Runtime guard: tool is always visible but only works outside plan mode
|
|
53
|
+
if (context.permissionMode === "plan") {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
content: "Already in plan mode. Use ExitPlanMode to exit.",
|
|
57
|
+
error: "Already in plan mode",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
if (!context.permissionManager) {
|
|
53
62
|
return {
|
|
54
63
|
success: false,
|
|
@@ -52,6 +52,16 @@ Ensure your plan is complete and unambiguous:
|
|
|
52
52
|
context: ToolContext,
|
|
53
53
|
): Promise<ToolResult> => {
|
|
54
54
|
try {
|
|
55
|
+
// Runtime guard: tool is always visible but only works in plan mode
|
|
56
|
+
if (context.permissionMode !== "plan") {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
content:
|
|
60
|
+
"Not in plan mode. ExitPlanMode can only be used when actively in plan mode.",
|
|
61
|
+
error: "Not in plan mode",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
if (!context.permissionManager) {
|
|
56
66
|
return {
|
|
57
67
|
success: false,
|