@agentdevjs/core 0.1.0 → 0.1.1
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/dist/BasicAgent-UBGLABDV.js +12 -0
- package/dist/ExplorerAgent-C62LOUTX.js +12 -0
- package/dist/{chunk-BVF7RUXV.js → chunk-KCCLSQC4.js} +1 -1
- package/dist/chunk-KCCLSQC4.js.map +1 -0
- package/dist/{chunk-KO4DJKM6.js → chunk-OFBE43IS.js} +1 -1
- package/dist/{chunk-KO4DJKM6.js.map → chunk-OFBE43IS.js.map} +1 -1
- package/dist/{chunk-YFSM6IYU.js → chunk-TKVOWEJ2.js} +154 -49
- package/dist/chunk-TKVOWEJ2.js.map +1 -0
- package/dist/{chunk-574NG4UB.js → chunk-U3LWOJ3Q.js} +4 -4
- package/dist/chunk-U3LWOJ3Q.js.map +1 -0
- package/dist/{chunk-UCEIJEM5.js → chunk-V3WM6EII.js} +2 -2
- package/dist/chunk-V3WM6EII.js.map +1 -0
- package/dist/{chunk-TIJV6GJW.js → chunk-YYNP7MZC.js} +4 -3
- package/dist/chunk-YYNP7MZC.js.map +1 -0
- package/dist/features/example-feature/templates/example-tool.render.js.map +1 -1
- package/dist/features/opencode-basic/templates/edit.render.js +1 -1
- package/dist/features/opencode-basic/templates/edit.render.js.map +1 -1
- package/dist/features/opencode-basic/templates/glob.render.js.map +1 -1
- package/dist/features/opencode-basic/templates/grep.render.js.map +1 -1
- package/dist/features/opencode-basic/templates/ls.render.js.map +1 -1
- package/dist/features/opencode-basic/templates/read.render.js +1 -1
- package/dist/features/opencode-basic/templates/read.render.js.map +1 -1
- package/dist/features/opencode-basic/templates/write.render.js +1 -1
- package/dist/features/opencode-basic/templates/write.render.js.map +1 -1
- package/dist/features/skill/templates/skill.render.js.map +1 -1
- package/dist/features/subagent/templates/agent-close.render.js.map +1 -1
- package/dist/features/subagent/templates/agent-list.render.js.map +1 -1
- package/dist/features/subagent/templates/agent-send.render.js.map +1 -1
- package/dist/features/subagent/templates/agent-spawn.render.js.map +1 -1
- package/dist/features/subagent/templates/wait.render.js.map +1 -1
- package/dist/features/todo/templates/task-clear.render.js.map +1 -1
- package/dist/features/todo/templates/task-create.render.js.map +1 -1
- package/dist/features/todo/templates/task-list.render.js.map +1 -1
- package/dist/features/todo/templates/task-update.render.js.map +1 -1
- package/dist/index.d.ts +2276 -2024
- package/dist/index.js +519 -205
- package/dist/index.js.map +1 -1
- package/dist/{notification-5DFBYKKN.js → notification-CASV7QYT.js} +2 -2
- package/dist/{resolver-F5SM2RHQ.js → resolver-4JA7VPYP.js} +2 -2
- package/dist/{session-events-TETMQI45.js → session-events-S7LMJ6MM.js} +2 -2
- package/package.json +55 -55
- package/dist/BasicAgent-RPYJHYCM.js +0 -12
- package/dist/ExplorerAgent-ITTSJRJI.js +0 -12
- package/dist/chunk-574NG4UB.js.map +0 -1
- package/dist/chunk-BVF7RUXV.js.map +0 -1
- package/dist/chunk-TIJV6GJW.js.map +0 -1
- package/dist/chunk-UCEIJEM5.js.map +0 -1
- package/dist/chunk-YFSM6IYU.js.map +0 -1
- /package/dist/{BasicAgent-RPYJHYCM.js.map → BasicAgent-UBGLABDV.js.map} +0 -0
- /package/dist/{ExplorerAgent-ITTSJRJI.js.map → ExplorerAgent-C62LOUTX.js.map} +0 -0
- /package/dist/{notification-5DFBYKKN.js.map → notification-CASV7QYT.js.map} +0 -0
- /package/dist/{resolver-F5SM2RHQ.js.map → resolver-4JA7VPYP.js.map} +0 -0
- /package/dist/{session-events-TETMQI45.js.map → session-events-S7LMJ6MM.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ExplorerAgent
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-V3WM6EII.js";
|
|
4
4
|
import {
|
|
5
5
|
BasicAgent
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-YYNP7MZC.js";
|
|
7
7
|
import {
|
|
8
8
|
AgentBase,
|
|
9
9
|
CapabilityRegistry,
|
|
@@ -54,11 +54,11 @@ import {
|
|
|
54
54
|
withDeadline,
|
|
55
55
|
withDisplay,
|
|
56
56
|
withImages
|
|
57
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-TKVOWEJ2.js";
|
|
58
58
|
import {
|
|
59
59
|
emitSessionEvent,
|
|
60
60
|
subscribeSessionEvents
|
|
61
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-OFBE43IS.js";
|
|
62
62
|
import {
|
|
63
63
|
DebugHub,
|
|
64
64
|
createLLMCharCount,
|
|
@@ -70,10 +70,10 @@ import {
|
|
|
70
70
|
getDebugCapabilities,
|
|
71
71
|
getDefaultUDSPath,
|
|
72
72
|
resolveDebugTransportMode
|
|
73
|
-
} from "./chunk-
|
|
73
|
+
} from "./chunk-U3LWOJ3Q.js";
|
|
74
74
|
import {
|
|
75
75
|
PlaceholderResolver
|
|
76
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-KCCLSQC4.js";
|
|
77
77
|
import "./chunk-DGUM43GV.js";
|
|
78
78
|
|
|
79
79
|
// src/features/skill/index.ts
|
|
@@ -1225,7 +1225,7 @@ var TodoToolFactory = class {
|
|
|
1225
1225
|
},
|
|
1226
1226
|
render: { call: "task-create", result: "task-create" },
|
|
1227
1227
|
execute: ({ subject, description = "" }) => {
|
|
1228
|
-
const task = self.createTaskFn(subject, description);
|
|
1228
|
+
const task = self.createTaskFn(String(subject), String(description));
|
|
1229
1229
|
return Promise.resolve(withDisplay(
|
|
1230
1230
|
JSON.stringify({ ok: true, taskId: task.id, status: task.status }),
|
|
1231
1231
|
{
|
|
@@ -1321,8 +1321,9 @@ var TodoToolFactory = class {
|
|
|
1321
1321
|
required: ["taskId"]
|
|
1322
1322
|
},
|
|
1323
1323
|
render: { call: "task-update", result: "task-update" },
|
|
1324
|
-
execute: (
|
|
1325
|
-
const
|
|
1324
|
+
execute: (rawArgs) => {
|
|
1325
|
+
const { taskId, ...rest } = rawArgs;
|
|
1326
|
+
const task = self.updateTaskFn(taskId, rest);
|
|
1326
1327
|
if (!task) {
|
|
1327
1328
|
return Promise.resolve({ ok: false, error: `\u4EFB\u52A1 ${taskId} \u4E0D\u5B58\u5728` });
|
|
1328
1329
|
}
|
|
@@ -1449,7 +1450,7 @@ var TodoFeature = class {
|
|
|
1449
1450
|
try {
|
|
1450
1451
|
this.reminderContent = await readFile3(templatePath, "utf-8");
|
|
1451
1452
|
console.log("[TodoFeature] Loaded reminder template from: " + templatePath);
|
|
1452
|
-
} catch
|
|
1453
|
+
} catch {
|
|
1453
1454
|
console.log("[TodoFeature] Failed to load template, using default reminder");
|
|
1454
1455
|
}
|
|
1455
1456
|
}
|
|
@@ -2026,18 +2027,18 @@ import {
|
|
|
2026
2027
|
stat,
|
|
2027
2028
|
unlink
|
|
2028
2029
|
} from "fs/promises";
|
|
2029
|
-
import { dirname as dirname6, isAbsolute, join as
|
|
2030
|
+
import { dirname as dirname6, isAbsolute, join as join2, relative } from "path";
|
|
2030
2031
|
import { homedir } from "os";
|
|
2031
2032
|
var MAX_SNAPSHOTS = 100;
|
|
2032
2033
|
function getBackupDir(sessionId) {
|
|
2033
|
-
return
|
|
2034
|
+
return join2(homedir(), ".agentdev", "file-history", sessionId);
|
|
2034
2035
|
}
|
|
2035
2036
|
function getBackupFileName(filePath, version) {
|
|
2036
2037
|
const hash = createHash("sha256").update(filePath).digest("hex").slice(0, 16);
|
|
2037
2038
|
return `${hash}@v${version}`;
|
|
2038
2039
|
}
|
|
2039
2040
|
function resolveBackupPath(backupFileName, sessionId) {
|
|
2040
|
-
return
|
|
2041
|
+
return join2(getBackupDir(sessionId), backupFileName);
|
|
2041
2042
|
}
|
|
2042
2043
|
function toTrackingPath(filePath, workspaceDir) {
|
|
2043
2044
|
if (!isAbsolute(filePath)) return filePath;
|
|
@@ -2048,7 +2049,7 @@ function toTrackingPath(filePath, workspaceDir) {
|
|
|
2048
2049
|
}
|
|
2049
2050
|
function toAbsolutePath(trackingPath, workspaceDir) {
|
|
2050
2051
|
if (isAbsolute(trackingPath)) return trackingPath;
|
|
2051
|
-
return
|
|
2052
|
+
return join2(workspaceDir, trackingPath);
|
|
2052
2053
|
}
|
|
2053
2054
|
async function createBackup(filePath, version, sessionId) {
|
|
2054
2055
|
const backupFileName = getBackupFileName(filePath, version);
|
|
@@ -2158,9 +2159,8 @@ async function makeSnapshot(state) {
|
|
|
2158
2159
|
const absolutePath = toAbsolutePath(trackingPath, state.workspaceDir);
|
|
2159
2160
|
const lastBackup = latestSnapshot.trackedFileBackups[trackingPath];
|
|
2160
2161
|
const nextVersion = lastBackup ? lastBackup.version + 1 : 1;
|
|
2161
|
-
let fileStats;
|
|
2162
2162
|
try {
|
|
2163
|
-
|
|
2163
|
+
await stat(absolutePath);
|
|
2164
2164
|
} catch (e) {
|
|
2165
2165
|
if (e.code === "ENOENT") {
|
|
2166
2166
|
newBackups[trackingPath] = { backupFileName: null, version: nextVersion };
|
|
@@ -2475,7 +2475,7 @@ var FileHistoryFeature = class {
|
|
|
2475
2475
|
// src/features/handoff-seed/index.ts
|
|
2476
2476
|
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
2477
2477
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
2478
|
-
import { join as
|
|
2478
|
+
import { join as join3, resolve, normalize as normalize3 } from "path";
|
|
2479
2479
|
var __filename4 = fileURLToPath7(import.meta.url);
|
|
2480
2480
|
var MAX_FILE_CHARS = 8e3;
|
|
2481
2481
|
var MAX_TOTAL_FILE_CHARS = 3e4;
|
|
@@ -2692,8 +2692,8 @@ var HandoffSeedFeature = class {
|
|
|
2692
2692
|
const nameOnlyList = [];
|
|
2693
2693
|
let totalChars = 0;
|
|
2694
2694
|
for (const skillName of skills) {
|
|
2695
|
-
const skillDir =
|
|
2696
|
-
const skillMdPath =
|
|
2695
|
+
const skillDir = join3(projectRoot, ".agentdev", "skills", skillName);
|
|
2696
|
+
const skillMdPath = join3(skillDir, "SKILL.md");
|
|
2697
2697
|
const content = this.tryReadFile(skillMdPath);
|
|
2698
2698
|
const basePath = normalize3(skillDir);
|
|
2699
2699
|
if (content === null) {
|
|
@@ -2762,8 +2762,8 @@ import { fileURLToPath as fileURLToPath8 } from "url";
|
|
|
2762
2762
|
import { promises as fs } from "fs";
|
|
2763
2763
|
|
|
2764
2764
|
// src/features/opencode-basic/tools.ts
|
|
2765
|
-
import { readFile as readFile5, writeFile,
|
|
2766
|
-
import {
|
|
2765
|
+
import { open, readFile as readFile5, writeFile, opendir, stat as stat2, mkdir as mkdir2 } from "fs/promises";
|
|
2766
|
+
import { globIterate } from "glob";
|
|
2767
2767
|
import { spawn } from "child_process";
|
|
2768
2768
|
import { createTwoFilesPatch, diffLines } from "diff";
|
|
2769
2769
|
import path from "path";
|
|
@@ -2771,7 +2771,6 @@ import { homedir as homedir2 } from "os";
|
|
|
2771
2771
|
var DEFAULT_READ_LIMIT = 2e3;
|
|
2772
2772
|
var MAX_LINE_LENGTH = 2e3;
|
|
2773
2773
|
var MAX_BYTES = 50 * 1024;
|
|
2774
|
-
var LS_LIMIT = 100;
|
|
2775
2774
|
var SEARCH_LIMIT = 100;
|
|
2776
2775
|
var DEFAULT_WORKSPACE_DIR = process.cwd();
|
|
2777
2776
|
var IGNORE_PATTERNS = [
|
|
@@ -2911,20 +2910,51 @@ function writeTextContent(filePath, content, encoding = "utf8", lineEndings = "L
|
|
|
2911
2910
|
}
|
|
2912
2911
|
return writeFile(filePath, toWrite, encoding);
|
|
2913
2912
|
}
|
|
2914
|
-
var
|
|
2913
|
+
var EQUIVALENT_CHARS = /* @__PURE__ */ new Map([
|
|
2915
2914
|
["\u2018", "'"],
|
|
2916
|
-
// LEFT SINGLE
|
|
2915
|
+
// LEFT SINGLE QUOTATION MARK
|
|
2917
2916
|
["\u2019", "'"],
|
|
2918
|
-
// RIGHT SINGLE
|
|
2917
|
+
// RIGHT SINGLE QUOTATION MARK
|
|
2919
2918
|
["\u201C", '"'],
|
|
2920
|
-
// LEFT DOUBLE
|
|
2921
|
-
["\u201D", '"']
|
|
2922
|
-
// RIGHT DOUBLE
|
|
2923
|
-
]
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2919
|
+
// LEFT DOUBLE QUOTATION MARK
|
|
2920
|
+
["\u201D", '"'],
|
|
2921
|
+
// RIGHT DOUBLE QUOTATION MARK
|
|
2922
|
+
["\xA0", " "],
|
|
2923
|
+
// NO-BREAK SPACE
|
|
2924
|
+
["\u1680", " "],
|
|
2925
|
+
// OGHAM SPACE MARK
|
|
2926
|
+
["\u2002", " "],
|
|
2927
|
+
// EN QUAD
|
|
2928
|
+
["\u2003", " "],
|
|
2929
|
+
// EM QUAD
|
|
2930
|
+
["\u2004", " "],
|
|
2931
|
+
// THREE-PER-EM SPACE
|
|
2932
|
+
["\u2005", " "],
|
|
2933
|
+
// FOUR-PER-EM SPACE
|
|
2934
|
+
["\u2006", " "],
|
|
2935
|
+
// SIX-PER-EM SPACE
|
|
2936
|
+
["\u2007", " "],
|
|
2937
|
+
// FIGURE SPACE
|
|
2938
|
+
["\u2008", " "],
|
|
2939
|
+
// PUNCTUATION SPACE
|
|
2940
|
+
["\u2009", " "],
|
|
2941
|
+
// THIN SPACE
|
|
2942
|
+
["\u200A", " "],
|
|
2943
|
+
// HAIR SPACE
|
|
2944
|
+
["\u202F", " "],
|
|
2945
|
+
// NARROW NO-BREAK SPACE
|
|
2946
|
+
["\u205F", " "],
|
|
2947
|
+
// MEDIUM MATHEMATICAL SPACE
|
|
2948
|
+
["\u3000", " "]
|
|
2949
|
+
// IDEOGRAPHIC SPACE(全角空格)
|
|
2950
|
+
]);
|
|
2951
|
+
var INVISIBLE_CHARS = /* @__PURE__ */ new Set(["\u200B", "\u200C", "\u200D", "\uFEFF"]);
|
|
2952
|
+
var SPECIAL_CHARS_RE = /[\u00A0\u1680\u2002-\u200A\u200B-\u200D\u2018\u2019\u201C\u201D\u202F\u205F\u3000\uFEFF]/;
|
|
2953
|
+
function normalizeForMatch(str) {
|
|
2954
|
+
let result = "";
|
|
2955
|
+
for (const ch of str) {
|
|
2956
|
+
if (INVISIBLE_CHARS.has(ch)) continue;
|
|
2957
|
+
result += EQUIVALENT_CHARS.get(ch) ?? ch;
|
|
2928
2958
|
}
|
|
2929
2959
|
return result;
|
|
2930
2960
|
}
|
|
@@ -2932,13 +2962,28 @@ function findActualString(fileContent, searchString) {
|
|
|
2932
2962
|
if (fileContent.includes(searchString)) {
|
|
2933
2963
|
return searchString;
|
|
2934
2964
|
}
|
|
2935
|
-
|
|
2936
|
-
|
|
2965
|
+
if (!SPECIAL_CHARS_RE.test(fileContent) && !SPECIAL_CHARS_RE.test(searchString)) {
|
|
2966
|
+
return null;
|
|
2967
|
+
}
|
|
2968
|
+
let normalizedFile = "";
|
|
2969
|
+
const sourceIndex = [];
|
|
2970
|
+
for (let i = 0; i < fileContent.length; i++) {
|
|
2971
|
+
const ch = fileContent[i];
|
|
2972
|
+
if (INVISIBLE_CHARS.has(ch)) continue;
|
|
2973
|
+
normalizedFile += EQUIVALENT_CHARS.get(ch) ?? ch;
|
|
2974
|
+
sourceIndex.push(i);
|
|
2975
|
+
}
|
|
2976
|
+
const normalizedSearch = normalizeForMatch(searchString);
|
|
2977
|
+
if (!normalizedSearch) {
|
|
2978
|
+
return null;
|
|
2979
|
+
}
|
|
2937
2980
|
const index = normalizedFile.indexOf(normalizedSearch);
|
|
2938
|
-
if (index
|
|
2939
|
-
return
|
|
2981
|
+
if (index === -1) {
|
|
2982
|
+
return null;
|
|
2940
2983
|
}
|
|
2941
|
-
|
|
2984
|
+
const start = sourceIndex[index];
|
|
2985
|
+
const end = sourceIndex[index + normalizedSearch.length - 1] + 1;
|
|
2986
|
+
return fileContent.substring(start, end);
|
|
2942
2987
|
}
|
|
2943
2988
|
async function isBinaryFile(filepath) {
|
|
2944
2989
|
const ext = path.extname(filepath).toLowerCase();
|
|
@@ -2975,21 +3020,25 @@ async function isBinaryFile(filepath) {
|
|
|
2975
3020
|
if (binaryExts.includes(ext)) {
|
|
2976
3021
|
return true;
|
|
2977
3022
|
}
|
|
3023
|
+
let handle;
|
|
2978
3024
|
try {
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
const
|
|
2982
|
-
|
|
3025
|
+
handle = await open(filepath, "r");
|
|
3026
|
+
const buffer = Buffer.allocUnsafe(4096);
|
|
3027
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
3028
|
+
if (bytesRead === 0) return false;
|
|
2983
3029
|
let nonPrintableCount = 0;
|
|
2984
|
-
for (let i = 0; i <
|
|
2985
|
-
if (
|
|
2986
|
-
if (
|
|
2987
|
-
nonPrintableCount
|
|
3030
|
+
for (let i = 0; i < bytesRead; i += 1) {
|
|
3031
|
+
if (buffer[i] === 0) return true;
|
|
3032
|
+
if (buffer[i] < 9 || buffer[i] > 13 && buffer[i] < 32) {
|
|
3033
|
+
nonPrintableCount += 1;
|
|
2988
3034
|
}
|
|
2989
3035
|
}
|
|
2990
|
-
return nonPrintableCount /
|
|
3036
|
+
return nonPrintableCount / bytesRead > 0.3;
|
|
2991
3037
|
} catch {
|
|
2992
3038
|
return false;
|
|
3039
|
+
} finally {
|
|
3040
|
+
await handle?.close().catch(() => {
|
|
3041
|
+
});
|
|
2993
3042
|
}
|
|
2994
3043
|
}
|
|
2995
3044
|
function createReadTool(workspaceDir = DEFAULT_WORKSPACE_DIR) {
|
|
@@ -3022,7 +3071,6 @@ function createReadTool(workspaceDir = DEFAULT_WORKSPACE_DIR) {
|
|
|
3022
3071
|
const offsetParam = typeof args.offset === "number" ? args.offset : void 0;
|
|
3023
3072
|
const limitParam = typeof args.limit === "number" ? args.limit : void 0;
|
|
3024
3073
|
const resolvedFilePath = resolveWorkspacePath(filePath, workspaceDir);
|
|
3025
|
-
console.log(`[read] ${resolvedFilePath}`);
|
|
3026
3074
|
if (offsetParam !== void 0 && offsetParam < 1) {
|
|
3027
3075
|
throw new Error("offset must be greater than or equal to 1");
|
|
3028
3076
|
}
|
|
@@ -3034,36 +3082,53 @@ function createReadTool(workspaceDir = DEFAULT_WORKSPACE_DIR) {
|
|
|
3034
3082
|
throw new Error(`File not found: ${resolvedFilePath}`);
|
|
3035
3083
|
}
|
|
3036
3084
|
if (stats.isDirectory()) {
|
|
3037
|
-
const
|
|
3085
|
+
const limit2 = limitParam ?? DEFAULT_READ_LIMIT;
|
|
3086
|
+
const offset2 = offsetParam ?? 1;
|
|
3087
|
+
const start2 = offset2 - 1;
|
|
3088
|
+
const end = start2 + limit2;
|
|
3089
|
+
const candidates = [];
|
|
3090
|
+
let totalEntries = 0;
|
|
3091
|
+
const directory = await opendir(resolvedFilePath);
|
|
3092
|
+
try {
|
|
3093
|
+
for await (const dirent of directory) {
|
|
3094
|
+
totalEntries += 1;
|
|
3095
|
+
candidates.push({
|
|
3096
|
+
name: dirent.name,
|
|
3097
|
+
isDirectory: dirent.isDirectory(),
|
|
3098
|
+
isSymbolicLink: dirent.isSymbolicLink()
|
|
3099
|
+
});
|
|
3100
|
+
candidates.sort((left, right) => left.name.localeCompare(right.name));
|
|
3101
|
+
if (candidates.length > end) candidates.pop();
|
|
3102
|
+
}
|
|
3103
|
+
} finally {
|
|
3104
|
+
await directory.close().catch(() => {
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
const visibleCandidates = candidates.slice(start2, end);
|
|
3038
3108
|
const entries = [];
|
|
3039
|
-
for (const
|
|
3040
|
-
if (
|
|
3041
|
-
entries.push(
|
|
3042
|
-
} else if (
|
|
3109
|
+
for (const candidate of visibleCandidates) {
|
|
3110
|
+
if (candidate.isDirectory) {
|
|
3111
|
+
entries.push(candidate.name + path.sep);
|
|
3112
|
+
} else if (candidate.isSymbolicLink) {
|
|
3043
3113
|
try {
|
|
3044
|
-
const targetStats = await stat2(path.join(resolvedFilePath,
|
|
3045
|
-
entries.push(targetStats.isDirectory() ?
|
|
3114
|
+
const targetStats = await stat2(path.join(resolvedFilePath, candidate.name));
|
|
3115
|
+
entries.push(targetStats.isDirectory() ? candidate.name + path.sep : candidate.name);
|
|
3046
3116
|
} catch {
|
|
3047
|
-
entries.push(
|
|
3117
|
+
entries.push(candidate.name);
|
|
3048
3118
|
}
|
|
3049
3119
|
} else {
|
|
3050
|
-
entries.push(
|
|
3120
|
+
entries.push(candidate.name);
|
|
3051
3121
|
}
|
|
3052
3122
|
}
|
|
3053
|
-
|
|
3054
|
-
const limit2 = limitParam ?? DEFAULT_READ_LIMIT;
|
|
3055
|
-
const offset2 = offsetParam ?? 1;
|
|
3056
|
-
const start2 = offset2 - 1;
|
|
3057
|
-
const sliced = entries.slice(start2, start2 + limit2);
|
|
3058
|
-
const truncated2 = start2 + sliced.length < entries.length;
|
|
3123
|
+
const truncated2 = totalEntries > end;
|
|
3059
3124
|
return {
|
|
3060
3125
|
type: "directory",
|
|
3061
3126
|
path: resolvedFilePath,
|
|
3062
|
-
totalEntries
|
|
3127
|
+
totalEntries,
|
|
3063
3128
|
offset: offset2,
|
|
3064
3129
|
limit: limit2,
|
|
3065
3130
|
truncated: truncated2,
|
|
3066
|
-
entries
|
|
3131
|
+
entries
|
|
3067
3132
|
};
|
|
3068
3133
|
}
|
|
3069
3134
|
const isBinary = await isBinaryFile(resolvedFilePath);
|
|
@@ -3142,7 +3207,6 @@ function createWriteTool(workspaceDir = DEFAULT_WORKSPACE_DIR) {
|
|
|
3142
3207
|
const filePath = normalizeNamedPathArg(args, "filePath", "filepath", "path");
|
|
3143
3208
|
const content = args.content;
|
|
3144
3209
|
const resolvedFilePath = resolveWorkspacePath(filePath, workspaceDir);
|
|
3145
|
-
console.log(`[write] ${resolvedFilePath}`);
|
|
3146
3210
|
let encoding = "utf8";
|
|
3147
3211
|
const exists = await stat2(resolvedFilePath).then(() => true).catch(() => false);
|
|
3148
3212
|
let contentOld = "";
|
|
@@ -3556,7 +3620,6 @@ function createEditTool(workspaceDir = DEFAULT_WORKSPACE_DIR) {
|
|
|
3556
3620
|
const newString = args.newString;
|
|
3557
3621
|
const replaceAll = args.replaceAll;
|
|
3558
3622
|
const resolvedFilePath = resolveWorkspacePath(filePath, workspaceDir);
|
|
3559
|
-
console.log(`[edit] ${resolvedFilePath}`);
|
|
3560
3623
|
if (oldString === newString) {
|
|
3561
3624
|
throw new Error("No changes to apply: oldString and newString are identical.");
|
|
3562
3625
|
}
|
|
@@ -3629,10 +3692,122 @@ function createEditTool(workspaceDir = DEFAULT_WORKSPACE_DIR) {
|
|
|
3629
3692
|
});
|
|
3630
3693
|
}
|
|
3631
3694
|
var editTool = createEditTool();
|
|
3695
|
+
var LS_MAX_DEPTH = 2;
|
|
3696
|
+
var LS_ROOT_LIMIT = 100;
|
|
3697
|
+
var LS_CHILD_LIMIT = 12;
|
|
3698
|
+
function compileIgnorePattern(pattern) {
|
|
3699
|
+
let normalized = pattern.trim().replaceAll("\\", "/").replace(/^\.\//, "");
|
|
3700
|
+
if (!normalized) return null;
|
|
3701
|
+
if (normalized.endsWith("/**")) {
|
|
3702
|
+
normalized = normalized.slice(0, -3).replace(/\/$/, "");
|
|
3703
|
+
if (!normalized) return null;
|
|
3704
|
+
return new RegExp(`^(?:${globPatternToRegexSource(normalized)})(?:/.*)?$`);
|
|
3705
|
+
}
|
|
3706
|
+
if (!/[?*[]/.test(normalized)) {
|
|
3707
|
+
return new RegExp(`^(?:${globPatternToRegexSource(normalized)})(?:/.*)?$`);
|
|
3708
|
+
}
|
|
3709
|
+
return new RegExp(`^${globPatternToRegexSource(normalized)}$`);
|
|
3710
|
+
}
|
|
3711
|
+
function globPatternToRegexSource(pattern) {
|
|
3712
|
+
let source = "";
|
|
3713
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
3714
|
+
const char = pattern[index];
|
|
3715
|
+
if (char === "*") {
|
|
3716
|
+
if (pattern[index + 1] === "*") {
|
|
3717
|
+
source += ".*";
|
|
3718
|
+
index += 1;
|
|
3719
|
+
} else {
|
|
3720
|
+
source += "[^/]*";
|
|
3721
|
+
}
|
|
3722
|
+
} else if (char === "?") {
|
|
3723
|
+
source += "[^/]";
|
|
3724
|
+
} else {
|
|
3725
|
+
source += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
return source;
|
|
3729
|
+
}
|
|
3730
|
+
function shouldIgnoreDirectoryEntry(relativePath, patterns) {
|
|
3731
|
+
return patterns.some((pattern) => pattern.test(relativePath));
|
|
3732
|
+
}
|
|
3733
|
+
async function buildLimitedDirectoryTree(rootPath, ignorePatterns, maxDepth = LS_MAX_DEPTH, rootLimit = LS_ROOT_LIMIT, childLimit = LS_CHILD_LIMIT) {
|
|
3734
|
+
const compiledPatterns = ignorePatterns.map(compileIgnorePattern).filter((pattern) => pattern !== null);
|
|
3735
|
+
let fileCount = 0;
|
|
3736
|
+
let truncated = false;
|
|
3737
|
+
const visit = async (absolutePath, relativePath, depth) => {
|
|
3738
|
+
const limit = depth === 0 ? rootLimit : childLimit;
|
|
3739
|
+
const entries = [];
|
|
3740
|
+
let droppedCount = 0;
|
|
3741
|
+
const directory = await opendir(absolutePath).catch(() => null);
|
|
3742
|
+
if (directory === null) return entries;
|
|
3743
|
+
const compareEntries = (left, right) => {
|
|
3744
|
+
if (left.isDirectory !== right.isDirectory) return left.isDirectory ? -1 : 1;
|
|
3745
|
+
return left.name.localeCompare(right.name);
|
|
3746
|
+
};
|
|
3747
|
+
try {
|
|
3748
|
+
for await (const dirent of directory) {
|
|
3749
|
+
const childRelativePath = relativePath ? `${relativePath}/${dirent.name}` : dirent.name;
|
|
3750
|
+
if (shouldIgnoreDirectoryEntry(childRelativePath, compiledPatterns)) continue;
|
|
3751
|
+
const entry = {
|
|
3752
|
+
name: dirent.name,
|
|
3753
|
+
relativePath: childRelativePath,
|
|
3754
|
+
isDirectory: dirent.isDirectory(),
|
|
3755
|
+
children: [],
|
|
3756
|
+
droppedCount: 0
|
|
3757
|
+
};
|
|
3758
|
+
const insertAt = entries.findIndex((existing) => compareEntries(entry, existing) < 0);
|
|
3759
|
+
if (insertAt === -1) entries.push(entry);
|
|
3760
|
+
else entries.splice(insertAt, 0, entry);
|
|
3761
|
+
if (entries.length > limit) {
|
|
3762
|
+
entries.pop();
|
|
3763
|
+
droppedCount += 1;
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
} finally {
|
|
3767
|
+
await directory.close().catch(() => {
|
|
3768
|
+
});
|
|
3769
|
+
}
|
|
3770
|
+
if (droppedCount > 0) {
|
|
3771
|
+
truncated = true;
|
|
3772
|
+
if (entries.length > 0) entries[entries.length - 1].droppedCount = droppedCount;
|
|
3773
|
+
}
|
|
3774
|
+
for (const entry of entries) {
|
|
3775
|
+
if (!entry.isDirectory) {
|
|
3776
|
+
fileCount += 1;
|
|
3777
|
+
continue;
|
|
3778
|
+
}
|
|
3779
|
+
if (depth >= maxDepth) continue;
|
|
3780
|
+
entry.children = await visit(path.join(rootPath, entry.relativePath), entry.relativePath, depth + 1);
|
|
3781
|
+
}
|
|
3782
|
+
return entries;
|
|
3783
|
+
};
|
|
3784
|
+
const root = {
|
|
3785
|
+
name: ".",
|
|
3786
|
+
relativePath: "",
|
|
3787
|
+
isDirectory: true,
|
|
3788
|
+
children: await visit(rootPath, "", 0),
|
|
3789
|
+
droppedCount: 0
|
|
3790
|
+
};
|
|
3791
|
+
return { root, fileCount, truncated };
|
|
3792
|
+
}
|
|
3793
|
+
function renderLimitedDirectoryTree(root, rootPath) {
|
|
3794
|
+
const lines = [`${rootPath}${path.sep}`];
|
|
3795
|
+
const render = (entry, depth) => {
|
|
3796
|
+
const indent = " ".repeat(depth);
|
|
3797
|
+
lines.push(`${indent}${entry.name}${entry.isDirectory ? "/" : ""}`);
|
|
3798
|
+
if (entry.droppedCount > 0) {
|
|
3799
|
+
lines.push(`${" ".repeat(depth + 1)}\u2026 ${entry.droppedCount} more`);
|
|
3800
|
+
}
|
|
3801
|
+
for (const child of entry.children) render(child, depth + 1);
|
|
3802
|
+
};
|
|
3803
|
+
for (const entry of root.children) render(entry, 0);
|
|
3804
|
+
return `${lines.join("\n")}
|
|
3805
|
+
`;
|
|
3806
|
+
}
|
|
3632
3807
|
function createLsTool(workspaceDir = DEFAULT_WORKSPACE_DIR) {
|
|
3633
3808
|
return createTool({
|
|
3634
3809
|
name: "ls",
|
|
3635
|
-
description: "List
|
|
3810
|
+
description: "List a bounded directory tree. It shows up to two levels, ignores common build/cache directories, and truncates large directories per level.",
|
|
3636
3811
|
render: "ls",
|
|
3637
3812
|
parallelizable: true,
|
|
3638
3813
|
parameters: {
|
|
@@ -3653,61 +3828,21 @@ function createLsTool(workspaceDir = DEFAULT_WORKSPACE_DIR) {
|
|
|
3653
3828
|
},
|
|
3654
3829
|
execute: async (args = {}) => {
|
|
3655
3830
|
const dirPath = normalizeNamedPathArg(args, "dirPath", "dirpath", "path", "filePath", "filepath");
|
|
3656
|
-
const
|
|
3831
|
+
const rawIgnore = args.ignore;
|
|
3832
|
+
const ignore = Array.isArray(rawIgnore) ? rawIgnore.filter((item) => typeof item === "string") : [];
|
|
3657
3833
|
const resolvedDirPath = resolveWorkspacePath(dirPath, workspaceDir);
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
nodir: true
|
|
3666
|
-
});
|
|
3667
|
-
const files = [];
|
|
3668
|
-
for (const file of matches) {
|
|
3669
|
-
files.push(file);
|
|
3670
|
-
if (files.length >= LS_LIMIT) break;
|
|
3671
|
-
}
|
|
3672
|
-
const dirs = /* @__PURE__ */ new Set();
|
|
3673
|
-
const filesByDir = /* @__PURE__ */ new Map();
|
|
3674
|
-
for (const file of files) {
|
|
3675
|
-
const dir = path.dirname(file);
|
|
3676
|
-
const parts = dir === "." ? [] : dir.split(path.sep);
|
|
3677
|
-
for (let i = 0; i <= parts.length; i++) {
|
|
3678
|
-
const dirPath2 = i === 0 ? "." : parts.slice(0, i).join(path.sep);
|
|
3679
|
-
dirs.add(dirPath2);
|
|
3680
|
-
}
|
|
3681
|
-
if (!filesByDir.has(dir)) {
|
|
3682
|
-
filesByDir.set(dir, []);
|
|
3683
|
-
}
|
|
3684
|
-
filesByDir.get(dir).push(path.basename(file));
|
|
3685
|
-
}
|
|
3686
|
-
function renderDir(dirPath2, depth) {
|
|
3687
|
-
const indent = " ".repeat(depth);
|
|
3688
|
-
let output = "";
|
|
3689
|
-
if (depth > 0) {
|
|
3690
|
-
output += `${indent}${path.basename(dirPath2)}/
|
|
3691
|
-
`;
|
|
3692
|
-
}
|
|
3693
|
-
const children = Array.from(dirs).filter((d) => path.dirname(d) === dirPath2 && d !== dirPath2).sort();
|
|
3694
|
-
for (const child of children) {
|
|
3695
|
-
output += renderDir(child, depth + 1);
|
|
3696
|
-
}
|
|
3697
|
-
const dirFiles = filesByDir.get(dirPath2) || [];
|
|
3698
|
-
for (const file of dirFiles.sort()) {
|
|
3699
|
-
output += `${" ".repeat(depth + 1)}${file}
|
|
3700
|
-
`;
|
|
3701
|
-
}
|
|
3702
|
-
return output;
|
|
3703
|
-
}
|
|
3704
|
-
const treeOutput = `${resolvedDirPath}${path.sep}
|
|
3705
|
-
${renderDir(".", 0)}`;
|
|
3834
|
+
const stats = await stat2(resolvedDirPath).catch(() => null);
|
|
3835
|
+
if (!stats) throw new Error(`Directory not found: ${resolvedDirPath}`);
|
|
3836
|
+
if (!stats.isDirectory()) throw new Error(`Path is not a directory: ${resolvedDirPath}`);
|
|
3837
|
+
const result = await buildLimitedDirectoryTree(
|
|
3838
|
+
resolvedDirPath,
|
|
3839
|
+
[...IGNORE_PATTERNS, ...ignore.map((pattern) => pattern.endsWith("/**") ? pattern : `${pattern}/**`)]
|
|
3840
|
+
);
|
|
3706
3841
|
return {
|
|
3707
3842
|
path: resolvedDirPath,
|
|
3708
|
-
count:
|
|
3709
|
-
truncated:
|
|
3710
|
-
tree:
|
|
3843
|
+
count: result.fileCount,
|
|
3844
|
+
truncated: result.truncated,
|
|
3845
|
+
tree: renderLimitedDirectoryTree(result.root, resolvedDirPath)
|
|
3711
3846
|
};
|
|
3712
3847
|
}
|
|
3713
3848
|
});
|
|
@@ -3738,30 +3873,30 @@ function createGlobTool(workspaceDir = DEFAULT_WORKSPACE_DIR) {
|
|
|
3738
3873
|
const pattern = args.pattern;
|
|
3739
3874
|
const searchPath = tryNamedPathArg(args, "searchPath", "searchpath", "path", "filePath", "filepath");
|
|
3740
3875
|
const resolvedSearchPath = resolveWorkspaceSearchPath(searchPath, workspaceDir);
|
|
3741
|
-
console.log(`[glob] ${pattern} in ${resolvedSearchPath}`);
|
|
3742
3876
|
const files = [];
|
|
3743
|
-
|
|
3877
|
+
let truncated = false;
|
|
3878
|
+
for await (const file of globIterate(pattern, {
|
|
3744
3879
|
cwd: resolvedSearchPath,
|
|
3745
3880
|
absolute: true,
|
|
3746
3881
|
windowsPathsNoEscape: true,
|
|
3747
|
-
nodir: true
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
if (files.length >= SEARCH_LIMIT)
|
|
3882
|
+
nodir: true,
|
|
3883
|
+
ignore: IGNORE_PATTERNS
|
|
3884
|
+
})) {
|
|
3885
|
+
if (files.length >= SEARCH_LIMIT) {
|
|
3886
|
+
truncated = true;
|
|
3887
|
+
break;
|
|
3888
|
+
}
|
|
3751
3889
|
try {
|
|
3752
3890
|
const stats = await stat2(file);
|
|
3753
|
-
files.push({
|
|
3754
|
-
path: file,
|
|
3755
|
-
mtime: stats.mtimeMs
|
|
3756
|
-
});
|
|
3891
|
+
files.push({ path: file, mtime: stats.mtimeMs });
|
|
3757
3892
|
} catch {
|
|
3758
3893
|
}
|
|
3759
3894
|
}
|
|
3760
|
-
files.sort((a, b) => b.mtime - a.mtime);
|
|
3895
|
+
files.sort((a, b) => b.mtime - a.mtime || a.path.localeCompare(b.path));
|
|
3761
3896
|
return {
|
|
3762
3897
|
count: files.length,
|
|
3763
|
-
truncated
|
|
3764
|
-
files: files.map((
|
|
3898
|
+
truncated,
|
|
3899
|
+
files: files.map((file) => file.path)
|
|
3765
3900
|
};
|
|
3766
3901
|
}
|
|
3767
3902
|
});
|
|
@@ -3819,7 +3954,6 @@ function createGrepTool(workspaceDir = DEFAULT_WORKSPACE_DIR) {
|
|
|
3819
3954
|
const searchPath = tryNamedPathArg(args, "searchPath", "searchpath", "path", "filePath", "filepath");
|
|
3820
3955
|
const include = args.include;
|
|
3821
3956
|
const resolvedSearchPath = resolveWorkspaceSearchPath(searchPath, workspaceDir);
|
|
3822
|
-
console.log(`[grep] ${pattern} in ${resolvedSearchPath}`);
|
|
3823
3957
|
const rgPath = await getRipgrepPath();
|
|
3824
3958
|
const rgArgs = ["-nH", "--hidden", "--no-messages", "--field-match-separator=|", "--regexp", pattern];
|
|
3825
3959
|
if (include) {
|
|
@@ -3827,18 +3961,54 @@ function createGrepTool(workspaceDir = DEFAULT_WORKSPACE_DIR) {
|
|
|
3827
3961
|
}
|
|
3828
3962
|
rgArgs.push(resolvedSearchPath);
|
|
3829
3963
|
try {
|
|
3830
|
-
const
|
|
3831
|
-
const
|
|
3832
|
-
const
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3964
|
+
const { matches, truncated } = await new Promise((resolve3, reject) => {
|
|
3965
|
+
const child = spawn(rgPath, rgArgs, { windowsHide: true });
|
|
3966
|
+
const collected = [];
|
|
3967
|
+
let pending = "";
|
|
3968
|
+
let truncated2 = false;
|
|
3969
|
+
let killRequested = false;
|
|
3970
|
+
const stopSearch = () => {
|
|
3971
|
+
if (killRequested) return;
|
|
3972
|
+
killRequested = true;
|
|
3973
|
+
child.kill();
|
|
3974
|
+
};
|
|
3975
|
+
const processLine = (line) => {
|
|
3976
|
+
if (!line || truncated2) return;
|
|
3977
|
+
const parts = line.split("|");
|
|
3978
|
+
if (parts.length < 3) return;
|
|
3979
|
+
const [filePath, lineNumStr, ...lineTextParts] = parts;
|
|
3980
|
+
const lineNum = parseInt(lineNumStr, 10);
|
|
3981
|
+
if (!filePath || !Number.isFinite(lineNum)) return;
|
|
3982
|
+
const lineText = lineTextParts.join("|");
|
|
3983
|
+
if (collected.length >= SEARCH_LIMIT) {
|
|
3984
|
+
truncated2 = true;
|
|
3985
|
+
stopSearch();
|
|
3986
|
+
return;
|
|
3987
|
+
}
|
|
3988
|
+
collected.push({
|
|
3989
|
+
path: filePath,
|
|
3990
|
+
lineNum,
|
|
3991
|
+
lineText: lineText.length > MAX_LINE_LENGTH ? `${lineText.substring(0, MAX_LINE_LENGTH)}...` : lineText
|
|
3992
|
+
});
|
|
3993
|
+
};
|
|
3994
|
+
const onSearchAbort = () => {
|
|
3995
|
+
stopSearch();
|
|
3996
|
+
};
|
|
3839
3997
|
if (context?.signal) {
|
|
3840
3998
|
context.signal.addEventListener("abort", onSearchAbort, { once: true });
|
|
3841
3999
|
}
|
|
4000
|
+
child.stdout.setEncoding("utf8");
|
|
4001
|
+
child.stdout.on("data", (chunk) => {
|
|
4002
|
+
pending += chunk;
|
|
4003
|
+
let newlineIndex = pending.indexOf(String.fromCharCode(10));
|
|
4004
|
+
while (newlineIndex !== -1) {
|
|
4005
|
+
processLine(pending.slice(0, newlineIndex).replace(new RegExp(String.fromCharCode(13) + "$"), ""));
|
|
4006
|
+
pending = pending.slice(newlineIndex + 1);
|
|
4007
|
+
if (killRequested) return;
|
|
4008
|
+
newlineIndex = pending.indexOf(String.fromCharCode(10));
|
|
4009
|
+
}
|
|
4010
|
+
});
|
|
4011
|
+
child.stderr.resume();
|
|
3842
4012
|
child.on("error", (err) => {
|
|
3843
4013
|
context?.signal?.removeEventListener("abort", onSearchAbort);
|
|
3844
4014
|
reject(err);
|
|
@@ -3846,41 +4016,29 @@ function createGrepTool(workspaceDir = DEFAULT_WORKSPACE_DIR) {
|
|
|
3846
4016
|
child.on("close", (code) => {
|
|
3847
4017
|
context?.signal?.removeEventListener("abort", onSearchAbort);
|
|
3848
4018
|
if (context?.signal?.aborted) {
|
|
3849
|
-
|
|
4019
|
+
reject(new Error("Search was aborted"));
|
|
4020
|
+
return;
|
|
4021
|
+
}
|
|
4022
|
+
if (!killRequested) {
|
|
4023
|
+
processLine(pending);
|
|
3850
4024
|
}
|
|
3851
|
-
if (code !== 0 && code !== 1) {
|
|
3852
|
-
|
|
4025
|
+
if (!killRequested && code !== 0 && code !== 1) {
|
|
4026
|
+
reject(new Error(`rg exited with code ${code}`));
|
|
4027
|
+
return;
|
|
3853
4028
|
}
|
|
3854
|
-
resolve3(
|
|
4029
|
+
resolve3({ matches: collected, truncated: truncated2 });
|
|
3855
4030
|
});
|
|
3856
4031
|
});
|
|
3857
|
-
const lines = stdout.trim().split(/\r?\n/);
|
|
3858
|
-
const matches = [];
|
|
3859
|
-
for (const line of lines) {
|
|
3860
|
-
if (!line) continue;
|
|
3861
|
-
const parts = line.split("|");
|
|
3862
|
-
if (parts.length < 3) continue;
|
|
3863
|
-
const [filePath, lineNumStr, ...lineTextParts] = parts;
|
|
3864
|
-
const lineNum = parseInt(lineNumStr, 10);
|
|
3865
|
-
const lineText = lineTextParts.join("|");
|
|
3866
|
-
matches.push({
|
|
3867
|
-
path: filePath,
|
|
3868
|
-
lineNum,
|
|
3869
|
-
lineText: lineText.length > MAX_LINE_LENGTH ? lineText.substring(0, MAX_LINE_LENGTH) + "..." : lineText
|
|
3870
|
-
});
|
|
3871
|
-
}
|
|
3872
4032
|
matches.sort((a, b) => a.path === b.path ? a.lineNum - b.lineNum : a.path < b.path ? -1 : 1);
|
|
3873
|
-
const truncated = matches.length > SEARCH_LIMIT;
|
|
3874
|
-
const finalMatches = truncated ? matches.slice(0, SEARCH_LIMIT) : matches;
|
|
3875
4033
|
return {
|
|
3876
4034
|
pattern,
|
|
3877
4035
|
matches: matches.length,
|
|
3878
4036
|
truncated,
|
|
3879
|
-
results:
|
|
4037
|
+
results: matches
|
|
3880
4038
|
};
|
|
3881
4039
|
} catch (error) {
|
|
3882
4040
|
if (error.message?.includes("Search was aborted") || context?.signal?.aborted) {
|
|
3883
|
-
throw new Error("Search was aborted");
|
|
4041
|
+
throw new Error("Search was aborted", { cause: error });
|
|
3884
4042
|
}
|
|
3885
4043
|
throw error;
|
|
3886
4044
|
}
|
|
@@ -4214,7 +4372,7 @@ var ExampleFeature = class {
|
|
|
4214
4372
|
}
|
|
4215
4373
|
return "continue" /* Continue */;
|
|
4216
4374
|
}
|
|
4217
|
-
async handleStepFinish(
|
|
4375
|
+
async handleStepFinish(_ctx) {
|
|
4218
4376
|
return "continue" /* Continue */;
|
|
4219
4377
|
}
|
|
4220
4378
|
};
|
|
@@ -5008,6 +5166,7 @@ var LspFeature = class {
|
|
|
5008
5166
|
broken = /* @__PURE__ */ new Set();
|
|
5009
5167
|
constructor(config = {}) {
|
|
5010
5168
|
this.config = {
|
|
5169
|
+
// eslint-disable-next-line no-restricted-syntax -- 防御性默认值,onInitiate 会用 ctx.config.workspaceDir 覆盖
|
|
5011
5170
|
workdir: config.workdir || process.cwd(),
|
|
5012
5171
|
binDir: config.binDir || path4.join(os.homedir(), ".agentdev", "lsp-bin"),
|
|
5013
5172
|
disableDownload: config.disableDownload ?? false,
|
|
@@ -5309,7 +5468,7 @@ var LspFeature = class {
|
|
|
5309
5468
|
|
|
5310
5469
|
// src/features/output-guard/index.ts
|
|
5311
5470
|
import { fileURLToPath as fileURLToPath12 } from "url";
|
|
5312
|
-
import { dirname as dirname8, join as
|
|
5471
|
+
import { dirname as dirname8, join as join4 } from "path";
|
|
5313
5472
|
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
5314
5473
|
|
|
5315
5474
|
// src/features/output-guard/truncate.ts
|
|
@@ -5525,6 +5684,7 @@ var OutputGuardFeature = class {
|
|
|
5525
5684
|
this.config = {
|
|
5526
5685
|
hardLimit: config.hardLimit ?? DEFAULT_HARD_LIMIT,
|
|
5527
5686
|
fieldLimit: config.fieldLimit ?? DEFAULT_FIELD_LIMIT,
|
|
5687
|
+
// eslint-disable-next-line no-restricted-syntax -- 防御性默认值,仅用于临时文件落盘目录;宿主应显式传 workdir
|
|
5528
5688
|
workdir: config.workdir ?? process.cwd(),
|
|
5529
5689
|
toolOverrides: config.toolOverrides ?? {}
|
|
5530
5690
|
};
|
|
@@ -5616,13 +5776,13 @@ var OutputGuardFeature = class {
|
|
|
5616
5776
|
let filePath;
|
|
5617
5777
|
let diskContent = fullOutput;
|
|
5618
5778
|
try {
|
|
5619
|
-
const tempDir =
|
|
5779
|
+
const tempDir = join4(this.config.workdir, ".agentdev", "temp");
|
|
5620
5780
|
const now = /* @__PURE__ */ new Date();
|
|
5621
5781
|
const ts = now.getFullYear().toString() + String(now.getMonth() + 1).padStart(2, "0") + String(now.getDate()).padStart(2, "0") + "-" + String(now.getHours()).padStart(2, "0") + String(now.getMinutes()).padStart(2, "0") + String(now.getSeconds()).padStart(2, "0");
|
|
5622
5782
|
const suffix = Math.random().toString(36).slice(2, 8);
|
|
5623
5783
|
const safeToolName = toolName.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 30);
|
|
5624
5784
|
const fileName = `tool-output-${safeToolName}-${ts}-${suffix}.log`;
|
|
5625
|
-
filePath =
|
|
5785
|
+
filePath = join4(tempDir, fileName);
|
|
5626
5786
|
try {
|
|
5627
5787
|
const parsed = JSON.parse(fullOutput);
|
|
5628
5788
|
diskContent = JSON.stringify(parsed, null, 2);
|
|
@@ -5661,9 +5821,9 @@ Use the read tool with offset/limit to access specific portions.
|
|
|
5661
5821
|
};
|
|
5662
5822
|
|
|
5663
5823
|
// src/core/config.ts
|
|
5664
|
-
import { readFile as readFile7, readdir
|
|
5824
|
+
import { readFile as readFile7, readdir } from "fs/promises";
|
|
5665
5825
|
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
5666
|
-
import { resolve as resolve2, join as
|
|
5826
|
+
import { resolve as resolve2, join as join5 } from "path";
|
|
5667
5827
|
import { cwd } from "process";
|
|
5668
5828
|
var OPENAI_THINKING_EFFORTS = [
|
|
5669
5829
|
"none",
|
|
@@ -5686,7 +5846,7 @@ function getProjectRoot() {
|
|
|
5686
5846
|
}
|
|
5687
5847
|
async function loadConfig(name = "default") {
|
|
5688
5848
|
const projectRoot = getProjectRoot();
|
|
5689
|
-
const configDir =
|
|
5849
|
+
const configDir = join5(projectRoot, "config");
|
|
5690
5850
|
const configPath = resolve2(configDir, `${name}.json`);
|
|
5691
5851
|
if (!existsSync3(configPath)) {
|
|
5692
5852
|
throw new Error(
|
|
@@ -5701,7 +5861,7 @@ async function loadConfig(name = "default") {
|
|
|
5701
5861
|
}
|
|
5702
5862
|
function loadConfigSync(name = "default") {
|
|
5703
5863
|
const projectRoot = getProjectRoot();
|
|
5704
|
-
const configDir =
|
|
5864
|
+
const configDir = join5(projectRoot, "config");
|
|
5705
5865
|
const configPath = resolve2(configDir, `${name}.json`);
|
|
5706
5866
|
if (!existsSync3(configPath)) {
|
|
5707
5867
|
throw new Error(
|
|
@@ -5716,9 +5876,9 @@ function loadConfigSync(name = "default") {
|
|
|
5716
5876
|
}
|
|
5717
5877
|
async function listConfigs() {
|
|
5718
5878
|
const projectRoot = getProjectRoot();
|
|
5719
|
-
const configDir =
|
|
5879
|
+
const configDir = join5(projectRoot, "config");
|
|
5720
5880
|
try {
|
|
5721
|
-
const files = await
|
|
5881
|
+
const files = await readdir(configDir);
|
|
5722
5882
|
return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(".json", ""));
|
|
5723
5883
|
} catch {
|
|
5724
5884
|
return [];
|
|
@@ -5820,7 +5980,7 @@ var RENDER_TEMPLATES = {
|
|
|
5820
5980
|
},
|
|
5821
5981
|
"file-write": {
|
|
5822
5982
|
call: '<div class="bash-command">Write <span class="file-path">{{path}}</span></div>',
|
|
5823
|
-
result: (
|
|
5983
|
+
result: (_data) => `<div style="color:var(--success-color)">\u2713 File written successfully</div>`
|
|
5824
5984
|
},
|
|
5825
5985
|
"file-list": {
|
|
5826
5986
|
call: '<div class="bash-command">List <span class="file-path">{{path}}</span></div>',
|
|
@@ -5926,7 +6086,6 @@ var RENDER_TEMPLATES = {
|
|
|
5926
6086
|
call: '<div class="bash-command">List trash</div>',
|
|
5927
6087
|
result: (data) => {
|
|
5928
6088
|
const total = data.total || 0;
|
|
5929
|
-
const files = data.files || [];
|
|
5930
6089
|
if (total === 0) {
|
|
5931
6090
|
return `<div style="color:var(--text-secondary)">Trash is empty</div>`;
|
|
5932
6091
|
}
|
|
@@ -6974,7 +7133,7 @@ function declareContinuity(Base, descriptor) {
|
|
|
6974
7133
|
|
|
6975
7134
|
// src/core/workthread/store.ts
|
|
6976
7135
|
import { copyFile as copyFile2, mkdir as mkdir4, readFile as readFile8, rename, unlink as unlink2, writeFile as writeFile3 } from "fs/promises";
|
|
6977
|
-
import { join as
|
|
7136
|
+
import { join as join6 } from "path";
|
|
6978
7137
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6979
7138
|
var WorkThreadNotFoundError = class extends Error {
|
|
6980
7139
|
code = "workthread_not_found";
|
|
@@ -7030,12 +7189,12 @@ var WorkThreadStore = class {
|
|
|
7030
7189
|
throw new Error("WorkThreadStore requires a rootDir");
|
|
7031
7190
|
}
|
|
7032
7191
|
this.rootDir = rootDir;
|
|
7033
|
-
this.threadsDir =
|
|
7034
|
-
this.indexPath =
|
|
7192
|
+
this.threadsDir = join6(rootDir, "threads");
|
|
7193
|
+
this.indexPath = join6(rootDir, "index.json");
|
|
7035
7194
|
}
|
|
7036
7195
|
// ── 路径 ──────────────────────────────────────────────────────────
|
|
7037
7196
|
threadFilePath(threadId) {
|
|
7038
|
-
return
|
|
7197
|
+
return join6(this.threadsDir, `${sanitizeWorkThreadFragment(threadId)}.json`);
|
|
7039
7198
|
}
|
|
7040
7199
|
// ── 原子写(对齐 FileSessionStore 模式,兼容 Windows EPERM/EXDEV)─
|
|
7041
7200
|
async atomicWriteJson(filePath, data) {
|
|
@@ -7203,6 +7362,54 @@ var WorkThreadStore = class {
|
|
|
7203
7362
|
if (this._threadLocks.get(threadId) === next) this._threadLocks.delete(threadId);
|
|
7204
7363
|
}
|
|
7205
7364
|
}
|
|
7365
|
+
/**
|
|
7366
|
+
* 删除线程记录文件与 index 条目(破坏性生命周期操作的存储原语)。
|
|
7367
|
+
*
|
|
7368
|
+
* 幂等:记录不存在时返回 { removed: false, alreadyAbsent: true }——
|
|
7369
|
+
* 已删除对象视为删除成功,供宿主的级联删除重试收敛。
|
|
7370
|
+
* 走与 create / update 相同的 per-thread 串行锁;index 条目移除走
|
|
7371
|
+
* _indexLock 串行链(与 updateIndexEntry 同链,并发创建 / 更新其它
|
|
7372
|
+
* 线程时双方的 index 快照不会互相覆盖丢失对方的条目)。
|
|
7373
|
+
*/
|
|
7374
|
+
async remove(threadId) {
|
|
7375
|
+
if (!threadId || typeof threadId !== "string") {
|
|
7376
|
+
throw new WorkThreadNotFoundError(String(threadId || ""));
|
|
7377
|
+
}
|
|
7378
|
+
const prev = this._threadLocks.get(threadId) || Promise.resolve();
|
|
7379
|
+
let release;
|
|
7380
|
+
const next = new Promise((r) => release = r);
|
|
7381
|
+
this._threadLocks.set(threadId, next);
|
|
7382
|
+
await prev.catch(() => {
|
|
7383
|
+
});
|
|
7384
|
+
try {
|
|
7385
|
+
const record = await this.get(threadId);
|
|
7386
|
+
if (!record) return { removed: false, alreadyAbsent: true };
|
|
7387
|
+
await unlink2(this.threadFilePath(threadId)).catch(() => {
|
|
7388
|
+
});
|
|
7389
|
+
const indexPrev = this._indexLock;
|
|
7390
|
+
let indexRelease;
|
|
7391
|
+
const indexNext = new Promise((r) => indexRelease = r);
|
|
7392
|
+
this._indexLock = indexNext;
|
|
7393
|
+
try {
|
|
7394
|
+
await indexPrev.catch(() => {
|
|
7395
|
+
});
|
|
7396
|
+
const index = await this.readIndex();
|
|
7397
|
+
if (index.threads.some((t) => t?.threadId === threadId)) {
|
|
7398
|
+
index.threads = index.threads.filter(
|
|
7399
|
+
(t) => t?.threadId !== threadId
|
|
7400
|
+
);
|
|
7401
|
+
index.revision = (Number(index.revision) || 0) + 1;
|
|
7402
|
+
await this.writeIndex(index);
|
|
7403
|
+
}
|
|
7404
|
+
} finally {
|
|
7405
|
+
indexRelease();
|
|
7406
|
+
}
|
|
7407
|
+
return { removed: true, alreadyAbsent: false };
|
|
7408
|
+
} finally {
|
|
7409
|
+
release();
|
|
7410
|
+
if (this._threadLocks.get(threadId) === next) this._threadLocks.delete(threadId);
|
|
7411
|
+
}
|
|
7412
|
+
}
|
|
7206
7413
|
};
|
|
7207
7414
|
function buildChainEdges(record) {
|
|
7208
7415
|
const chain = Array.isArray(record.sessionChain) ? record.sessionChain : [];
|
|
@@ -7226,7 +7433,6 @@ function buildChainEdges(record) {
|
|
|
7226
7433
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
7227
7434
|
var WorkThreadCommandStatus = Object.freeze({
|
|
7228
7435
|
PENDING: "pending",
|
|
7229
|
-
IN_FLIGHT: "in_flight",
|
|
7230
7436
|
DELIVERED: "delivered",
|
|
7231
7437
|
FAILED: "failed",
|
|
7232
7438
|
CANCELLED: "cancelled"
|
|
@@ -7261,7 +7467,8 @@ function createCommandRecord(opts) {
|
|
|
7261
7467
|
createdAt: now,
|
|
7262
7468
|
updatedAt: now,
|
|
7263
7469
|
deliveredAt: null,
|
|
7264
|
-
...Array.isArray(opts.capabilityActivations) && opts.capabilityActivations.length > 0 ? { capabilityActivations: opts.capabilityActivations.filter((a) => typeof a === "string") } : {}
|
|
7470
|
+
...Array.isArray(opts.capabilityActivations) && opts.capabilityActivations.length > 0 ? { capabilityActivations: opts.capabilityActivations.filter((a) => typeof a === "string") } : {},
|
|
7471
|
+
...Array.isArray(opts.images) && opts.images.filter((i) => typeof i === "string" && i.length > 0).length > 0 ? { images: opts.images.filter((i) => typeof i === "string" && i.length > 0) } : {}
|
|
7265
7472
|
};
|
|
7266
7473
|
}
|
|
7267
7474
|
function appendCommand(record, command) {
|
|
@@ -7269,7 +7476,7 @@ function appendCommand(record, command) {
|
|
|
7269
7476
|
record.commands = commands;
|
|
7270
7477
|
if (command.idempotencyKey) {
|
|
7271
7478
|
const existing = commands.find(
|
|
7272
|
-
(c) => c.idempotencyKey === command.idempotencyKey && (c.status === WorkThreadCommandStatus.PENDING || c.status === WorkThreadCommandStatus.
|
|
7479
|
+
(c) => c.idempotencyKey === command.idempotencyKey && (c.status === WorkThreadCommandStatus.PENDING || c.status === WorkThreadCommandStatus.DELIVERED)
|
|
7273
7480
|
);
|
|
7274
7481
|
if (existing) {
|
|
7275
7482
|
return { command: existing, duplicate: true };
|
|
@@ -7334,7 +7541,8 @@ var WorkThreadRuntimeBridge = class {
|
|
|
7334
7541
|
text: command.text || "",
|
|
7335
7542
|
source: "thread",
|
|
7336
7543
|
sourceRef: command.commandId || "",
|
|
7337
|
-
...Array.isArray(command.capabilityActivations) && command.capabilityActivations.length > 0 ? { capabilityActivations: command.capabilityActivations } : {}
|
|
7544
|
+
...Array.isArray(command.capabilityActivations) && command.capabilityActivations.length > 0 ? { capabilityActivations: command.capabilityActivations } : {},
|
|
7545
|
+
...Array.isArray(command.images) && command.images.length > 0 ? { images: command.images } : {}
|
|
7338
7546
|
});
|
|
7339
7547
|
} catch (error) {
|
|
7340
7548
|
if (isUserTurnDeliveryError(error)) {
|
|
@@ -7385,6 +7593,11 @@ function pushLifecycleEvent(record, event, maxEvents = 200) {
|
|
|
7385
7593
|
}
|
|
7386
7594
|
record.lastLifecycleEvent = event;
|
|
7387
7595
|
}
|
|
7596
|
+
var DEFAULT_SUCCESSION_INSTRUCTION = [
|
|
7597
|
+
"\u4E0A\u4E0B\u6587\u5DF2\u7CBE\u7B80\u63A5\u529B\u3002\u5148\u68C0\u67E5\u5F53\u524D\u5DE5\u4F5C\u6811\u3001\u5DF2\u6709\u53D8\u66F4\u3001\u6D4B\u8BD5\u7ED3\u679C\u548C\u4E0A\u4E00\u68D2\u6458\u8981\uFF0C",
|
|
7598
|
+
"\u786E\u8BA4\u54EA\u4E9B\u6B65\u9AA4\u5DF2\u7ECF\u5B8C\u6210\uFF1B\u4E0D\u8981\u91CD\u590D\u53EF\u80FD\u5DF2\u6709\u526F\u4F5C\u7528\u7684\u64CD\u4F5C\uFF0C\u7136\u540E\u7EE7\u7EED\u5F53\u524D\u4EFB\u52A1\u3002",
|
|
7599
|
+
"\u9700\u8981\u4EBA\u5DE5\u51B3\u7B56\u6216\u65E0\u6CD5\u5B89\u5168\u5224\u65AD\u65F6\uFF0C\u660E\u786E\u8BF4\u660E\u539F\u56E0\u3002"
|
|
7600
|
+
].join("");
|
|
7388
7601
|
function threadIdentityError(message, code, status) {
|
|
7389
7602
|
return Object.assign(new Error(message), { code, status });
|
|
7390
7603
|
}
|
|
@@ -7392,10 +7605,12 @@ var WorkThread = class {
|
|
|
7392
7605
|
store;
|
|
7393
7606
|
_bridge;
|
|
7394
7607
|
_identitySource;
|
|
7395
|
-
|
|
7608
|
+
_continuationPolicy;
|
|
7609
|
+
constructor({ store, bridge, identitySource, continuationPolicy }) {
|
|
7396
7610
|
this.store = store;
|
|
7397
7611
|
this._bridge = bridge || new WorkThreadRuntimeBridge();
|
|
7398
7612
|
this._identitySource = identitySource && typeof identitySource === "function" ? identitySource : void 0;
|
|
7613
|
+
this._continuationPolicy = continuationPolicy && typeof continuationPolicy.composeSuccessionInstruction === "function" ? continuationPolicy : { composeSuccessionInstruction: () => DEFAULT_SUCCESSION_INSTRUCTION };
|
|
7399
7614
|
}
|
|
7400
7615
|
// ── 查询 ─────────────────────────────────────────────────────────
|
|
7401
7616
|
async listThreads(opts = {}) {
|
|
@@ -7509,8 +7724,16 @@ var WorkThread = class {
|
|
|
7509
7724
|
return Date.now() - pending.startedAt < HANDOFF_STALE_MS;
|
|
7510
7725
|
}
|
|
7511
7726
|
/**
|
|
7512
|
-
*
|
|
7513
|
-
*
|
|
7727
|
+
* 标记线程进入交接:原子写入 pendingSuccession + status=rotating,并在同
|
|
7728
|
+
* 一笔事务内播种接续恢复指令(R3:createdAt 与挡板同拍,早于一切交接期
|
|
7729
|
+
* 积压;幂等键绑移交易 (threadId, fromSessionId),失败恢复重走不会重复)。
|
|
7730
|
+
*
|
|
7731
|
+
* 门禁(按序):
|
|
7732
|
+
* - closed → thread_closed(硬终态)
|
|
7733
|
+
* - hold → thread_held(归档/行政冻结期不开新交接,K9)
|
|
7734
|
+
* - 已有 fresh 交接在办 → handoff_in_progress(并发竞争显式拒绝而非
|
|
7735
|
+
* 幂等刷新续命,R8 跨入口 single-flight 锚点;stale 残卷可覆写重开)
|
|
7736
|
+
* - fromSessionId ≠ 当前 head → head_mismatch
|
|
7514
7737
|
*/
|
|
7515
7738
|
async beginSessionHandoff(opts) {
|
|
7516
7739
|
const threadId = validateId(opts.threadId, "threadId");
|
|
@@ -7523,6 +7746,18 @@ var WorkThread = class {
|
|
|
7523
7746
|
status: 409
|
|
7524
7747
|
});
|
|
7525
7748
|
}
|
|
7749
|
+
if (draft.hold === true) {
|
|
7750
|
+
throw Object.assign(new Error(`WorkThread "${threadId}" is held (administrative freeze)`), {
|
|
7751
|
+
code: "thread_held",
|
|
7752
|
+
status: 409
|
|
7753
|
+
});
|
|
7754
|
+
}
|
|
7755
|
+
if (draft.status === "rotating" && draft.pendingSuccession?.startedAt && Date.now() - draft.pendingSuccession.startedAt < HANDOFF_STALE_MS) {
|
|
7756
|
+
throw Object.assign(
|
|
7757
|
+
new Error(`WorkThread "${threadId}" already has a handoff in progress; concurrent succession requests are rejected`),
|
|
7758
|
+
{ code: "handoff_in_progress", status: 409 }
|
|
7759
|
+
);
|
|
7760
|
+
}
|
|
7526
7761
|
if (draft.headSessionId !== normalizedFrom) {
|
|
7527
7762
|
throw Object.assign(
|
|
7528
7763
|
new Error(`Handoff source is not the current head of workthread "${threadId}"`),
|
|
@@ -7537,6 +7772,23 @@ var WorkThread = class {
|
|
|
7537
7772
|
stage: "started",
|
|
7538
7773
|
startedAt: now
|
|
7539
7774
|
};
|
|
7775
|
+
const instructionText = this._continuationPolicy.composeSuccessionInstruction({
|
|
7776
|
+
threadId,
|
|
7777
|
+
fromSessionId: normalizedFrom,
|
|
7778
|
+
reason: normalizedReason
|
|
7779
|
+
});
|
|
7780
|
+
if (typeof instructionText === "string" && instructionText.trim()) {
|
|
7781
|
+
const instruction = createCommandRecord({
|
|
7782
|
+
threadId,
|
|
7783
|
+
kind: WorkThreadCommandKind.SYSTEM_CONTINUATION,
|
|
7784
|
+
text: instructionText,
|
|
7785
|
+
source: "thread-succession",
|
|
7786
|
+
idempotencyKey: `succession:${threadId}:${normalizedFrom}`
|
|
7787
|
+
});
|
|
7788
|
+
instruction.createdAt = now;
|
|
7789
|
+
appendCommand(draft, instruction);
|
|
7790
|
+
pruneCommands(draft);
|
|
7791
|
+
}
|
|
7540
7792
|
pushLifecycleEvent(draft, {
|
|
7541
7793
|
type: "handoff_started",
|
|
7542
7794
|
status: "rotating",
|
|
@@ -7563,15 +7815,21 @@ var WorkThread = class {
|
|
|
7563
7815
|
/**
|
|
7564
7816
|
* 标记交接失败为接续编排状态 rotation_failed,不折叠成通用执行失败。
|
|
7565
7817
|
* pendingSuccession 保留在盘上,供后续 resume 检查被打断的确切 stage。
|
|
7818
|
+
*
|
|
7819
|
+
* K3 守卫(事务内):失败记录的资格来自「在办移交」本身——
|
|
7820
|
+
* - closed 线程:硬终态,任何迟到失败不得覆写(返回原记录,零写入);
|
|
7821
|
+
* - 无 pendingSuccession(移交已成功推进 / 从未开始):迟到的失败者
|
|
7822
|
+
* 没有立卷资格,no-op 返回。这两条共同保证:只有「挡板仍在」的在办
|
|
7823
|
+
* 移交失败才会落 rotation_failed。
|
|
7566
7824
|
*/
|
|
7567
7825
|
async failSessionHandoff(threadId, opts = {}) {
|
|
7568
|
-
const
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
if (draft.pendingSuccession) {
|
|
7573
|
-
draft.pendingSuccession.stage = cleanText(opts.stage) || draft.pendingSuccession.stage || "unknown";
|
|
7826
|
+
const tid = validateId(threadId, "threadId");
|
|
7827
|
+
const { record } = await this.store.update(tid, (draft) => {
|
|
7828
|
+
if (draft.status === WORKTHREAD_TERMINAL_STATUS || !draft.pendingSuccession) {
|
|
7829
|
+
return draft;
|
|
7574
7830
|
}
|
|
7831
|
+
draft.status = "rotation_failed";
|
|
7832
|
+
draft.pendingSuccession.stage = cleanText(opts.stage) || draft.pendingSuccession.stage || "unknown";
|
|
7575
7833
|
pushLifecycleEvent(draft, {
|
|
7576
7834
|
type: "handoff_failed",
|
|
7577
7835
|
status: "rotation_failed",
|
|
@@ -7591,9 +7849,18 @@ var WorkThread = class {
|
|
|
7591
7849
|
*/
|
|
7592
7850
|
async appendCommand(opts) {
|
|
7593
7851
|
const threadId = validateId(opts.threadId, "threadId");
|
|
7594
|
-
const normalizedKind = opts.kind
|
|
7852
|
+
const normalizedKind = opts.kind === void 0 || opts.kind === "" ? WorkThreadCommandKind.USER_MESSAGE : opts.kind;
|
|
7853
|
+
if (!Object.values(WorkThreadCommandKind).includes(normalizedKind)) {
|
|
7854
|
+
throw Object.assign(
|
|
7855
|
+
new Error(
|
|
7856
|
+
`Unknown command kind "${normalizedKind}" (expected one of: ${Object.values(WorkThreadCommandKind).join(", ")})`
|
|
7857
|
+
),
|
|
7858
|
+
{ code: "invalid_request", status: 400 }
|
|
7859
|
+
);
|
|
7860
|
+
}
|
|
7595
7861
|
const normalizedText = String(opts.text || "");
|
|
7596
|
-
|
|
7862
|
+
const normalizedImages = Array.isArray(opts.images) ? opts.images.filter((entry) => typeof entry === "string" && entry.trim()) : [];
|
|
7863
|
+
if (!normalizedText.trim() && normalizedImages.length === 0) {
|
|
7597
7864
|
throw Object.assign(new Error("Command text must be non-empty"), {
|
|
7598
7865
|
code: "invalid_request",
|
|
7599
7866
|
status: 400
|
|
@@ -7611,10 +7878,17 @@ var WorkThread = class {
|
|
|
7611
7878
|
text: normalizedText,
|
|
7612
7879
|
source: cleanText(opts.source) || "ui",
|
|
7613
7880
|
idempotencyKey: cleanText(opts.idempotencyKey),
|
|
7614
|
-
...Array.isArray(opts.capabilityActivations) ? { capabilityActivations: opts.capabilityActivations } : {}
|
|
7881
|
+
...Array.isArray(opts.capabilityActivations) ? { capabilityActivations: opts.capabilityActivations } : {},
|
|
7882
|
+
...normalizedImages.length > 0 ? { images: normalizedImages } : {}
|
|
7615
7883
|
});
|
|
7616
7884
|
let appendOutcome = { command, duplicate: false };
|
|
7617
7885
|
const { record } = await this.store.update(threadId, (draft) => {
|
|
7886
|
+
if (draft.status === WORKTHREAD_TERMINAL_STATUS) {
|
|
7887
|
+
throw Object.assign(new Error(`WorkThread "${threadId}" is closed; terminal threads hold no new commands`), {
|
|
7888
|
+
code: "thread_closed",
|
|
7889
|
+
status: 409
|
|
7890
|
+
});
|
|
7891
|
+
}
|
|
7618
7892
|
appendOutcome = appendCommand(draft, command);
|
|
7619
7893
|
pruneCommands(draft);
|
|
7620
7894
|
return draft;
|
|
@@ -7678,7 +7952,29 @@ var WorkThread = class {
|
|
|
7678
7952
|
let deliveredCount = 0;
|
|
7679
7953
|
let stopReason = null;
|
|
7680
7954
|
for (const command of pending) {
|
|
7681
|
-
const
|
|
7955
|
+
const fresh = await this.store.get(tid);
|
|
7956
|
+
if (!fresh) throw new WorkThreadNotFoundError(tid);
|
|
7957
|
+
if (this.isTerminal(fresh)) {
|
|
7958
|
+
stopReason = "thread_closed";
|
|
7959
|
+
break;
|
|
7960
|
+
}
|
|
7961
|
+
if (fresh.hold === true) {
|
|
7962
|
+
stopReason = "thread_held";
|
|
7963
|
+
break;
|
|
7964
|
+
}
|
|
7965
|
+
if (fresh.pendingSuccession && this.isHandoffActive(fresh)) {
|
|
7966
|
+
stopReason = "handoff_in_progress";
|
|
7967
|
+
break;
|
|
7968
|
+
}
|
|
7969
|
+
if (fresh.headSessionId !== thread.headSessionId) {
|
|
7970
|
+
stopReason = "head_changed";
|
|
7971
|
+
break;
|
|
7972
|
+
}
|
|
7973
|
+
const freshCommand = (fresh.commands || []).find((c) => c?.commandId === command.commandId) || null;
|
|
7974
|
+
if (!freshCommand || freshCommand.status !== WorkThreadCommandStatus.PENDING) {
|
|
7975
|
+
continue;
|
|
7976
|
+
}
|
|
7977
|
+
const outcome = await this._bridge.deliver({ thread: fresh, command: freshCommand });
|
|
7682
7978
|
results.push({ commandId: command.commandId, ...outcome });
|
|
7683
7979
|
if (outcome.accepted) {
|
|
7684
7980
|
deliveredCount += 1;
|
|
@@ -7696,6 +7992,7 @@ var WorkThread = class {
|
|
|
7696
7992
|
if (deliveredIds.size > 0 || failedResults.size > 0) {
|
|
7697
7993
|
const { record } = await this.store.update(tid, (draft) => {
|
|
7698
7994
|
for (const c of draft.commands || []) {
|
|
7995
|
+
if (c.status !== WorkThreadCommandStatus.PENDING) continue;
|
|
7699
7996
|
if (deliveredIds.has(c.commandId)) {
|
|
7700
7997
|
c.status = WorkThreadCommandStatus.DELIVERED;
|
|
7701
7998
|
c.deliveryRef = results.find((r) => r.commandId === c.commandId)?.deliveryRef || null;
|
|
@@ -7739,6 +8036,10 @@ var WorkThread = class {
|
|
|
7739
8036
|
/**
|
|
7740
8037
|
* 推进线程 head:headSessionId: fromSessionId → toSessionId。
|
|
7741
8038
|
*
|
|
8039
|
+
* fromSessionId 必填(K23):它是调用方声明的 CAS 期望值——不传即
|
|
8040
|
+
* 「接受任意当前 head」,幽灵任务防串台将被合法绕过。调用方若不知道
|
|
8041
|
+
* 当前 head,应先读线程再用读到的值调用(CLI 的自动填充即此语义)。
|
|
8042
|
+
*
|
|
7742
8043
|
* 关键不变量(与 store 原子写共同保证):任一时刻线程要么明确指向旧 head,
|
|
7743
8044
|
* 要么明确指向新 head;推进与指令状态 / 清挡板在同一次落盘中变更。
|
|
7744
8045
|
*
|
|
@@ -7756,7 +8057,7 @@ var WorkThread = class {
|
|
|
7756
8057
|
async advanceHead(opts) {
|
|
7757
8058
|
const threadId = validateId(opts.threadId, "threadId");
|
|
7758
8059
|
const normalizedTo = validateId(opts.toSessionId, "toSessionId");
|
|
7759
|
-
const normalizedFrom =
|
|
8060
|
+
const normalizedFrom = validateId(opts.fromSessionId, "fromSessionId");
|
|
7760
8061
|
const { record } = await this.store.update(
|
|
7761
8062
|
threadId,
|
|
7762
8063
|
async (draft) => {
|
|
@@ -7766,7 +8067,13 @@ var WorkThread = class {
|
|
|
7766
8067
|
status: 409
|
|
7767
8068
|
});
|
|
7768
8069
|
}
|
|
7769
|
-
if (
|
|
8070
|
+
if (draft.hold === true) {
|
|
8071
|
+
throw Object.assign(
|
|
8072
|
+
new Error(`WorkThread "${threadId}" is held (administrative freeze); head cannot advance`),
|
|
8073
|
+
{ code: "thread_held", status: 409 }
|
|
8074
|
+
);
|
|
8075
|
+
}
|
|
8076
|
+
if (draft.headSessionId !== normalizedFrom) {
|
|
7770
8077
|
throw Object.assign(
|
|
7771
8078
|
new Error(
|
|
7772
8079
|
`Head mismatch on workthread "${threadId}": expected ${normalizedFrom}, current ${draft.headSessionId}`
|
|
@@ -7854,7 +8161,7 @@ var WorkThread = class {
|
|
|
7854
8161
|
type: "handoff_completed",
|
|
7855
8162
|
status: "open",
|
|
7856
8163
|
at: now,
|
|
7857
|
-
fromSessionId: normalizedFrom
|
|
8164
|
+
fromSessionId: normalizedFrom,
|
|
7858
8165
|
toSessionId: normalizedTo,
|
|
7859
8166
|
reason: cleanText(opts.endKind) || "manual"
|
|
7860
8167
|
});
|
|
@@ -7902,7 +8209,7 @@ var WorkThread = class {
|
|
|
7902
8209
|
|
|
7903
8210
|
// src/core/workthread/board.ts
|
|
7904
8211
|
import { copyFile as copyFile3, mkdir as mkdir5, readFile as readFile9, rename as rename2, unlink as unlink3, writeFile as writeFile4 } from "fs/promises";
|
|
7905
|
-
import { join as
|
|
8212
|
+
import { join as join7 } from "path";
|
|
7906
8213
|
var BOARD_TRANSITIONS = {
|
|
7907
8214
|
idle: /* @__PURE__ */ new Set(["running", "waiting_input", "failed", "closed"]),
|
|
7908
8215
|
running: /* @__PURE__ */ new Set(["idle", "waiting_input", "failed", "closed"]),
|
|
@@ -7929,11 +8236,11 @@ var WorkThreadBoard = class {
|
|
|
7929
8236
|
throw new Error("WorkThreadBoard requires a rootDir");
|
|
7930
8237
|
}
|
|
7931
8238
|
this.core = core;
|
|
7932
|
-
this.boardsDir =
|
|
8239
|
+
this.boardsDir = join7(rootDir, "boards");
|
|
7933
8240
|
}
|
|
7934
8241
|
// ── 内部持久化(独立看板域,与锚点 store 分离)──────────────
|
|
7935
8242
|
boardFilePath(workThreadId) {
|
|
7936
|
-
return
|
|
8243
|
+
return join7(this.boardsDir, `${sanitizeWorkThreadFragment(workThreadId)}.board.json`);
|
|
7937
8244
|
}
|
|
7938
8245
|
async getBoardId(workThreadId) {
|
|
7939
8246
|
return workThreadId;
|
|
@@ -8273,6 +8580,7 @@ export {
|
|
|
8273
8580
|
HandoffSeedFeature,
|
|
8274
8581
|
HooksRegistry,
|
|
8275
8582
|
LspFeature,
|
|
8583
|
+
MAX_RETAINED_TERMINAL_COMMANDS,
|
|
8276
8584
|
OPENAI_THINKING_EFFORTS,
|
|
8277
8585
|
OPENCODE_BASIC_CONTINUITY_PROTOCOL,
|
|
8278
8586
|
OpencodeBasicFeature,
|
|
@@ -8300,6 +8608,7 @@ export {
|
|
|
8300
8608
|
WorkThreadRevisionConflictError,
|
|
8301
8609
|
WorkThreadRuntimeBridge,
|
|
8302
8610
|
WorkThreadStore,
|
|
8611
|
+
appendCommand,
|
|
8303
8612
|
applyTemplate,
|
|
8304
8613
|
assistant,
|
|
8305
8614
|
buildSummaryPrompt,
|
|
@@ -8307,6 +8616,7 @@ export {
|
|
|
8307
8616
|
buildTrimmedSeedMessages,
|
|
8308
8617
|
classifyAPIError,
|
|
8309
8618
|
classifyAndWrapError,
|
|
8619
|
+
createCommandRecord,
|
|
8310
8620
|
createLLMCharCount,
|
|
8311
8621
|
createLLMComplete,
|
|
8312
8622
|
createLLMRetry,
|
|
@@ -8322,6 +8632,8 @@ export {
|
|
|
8322
8632
|
emitSessionEvent,
|
|
8323
8633
|
extractConnectionErrorDetails,
|
|
8324
8634
|
extractErrorCode,
|
|
8635
|
+
findCommand,
|
|
8636
|
+
generateCommandId,
|
|
8325
8637
|
generateSummaryText,
|
|
8326
8638
|
generateWorkThreadId,
|
|
8327
8639
|
getClawRuntimeUrl,
|
|
@@ -8345,7 +8657,9 @@ export {
|
|
|
8345
8657
|
normalizeExportPolicy,
|
|
8346
8658
|
normalizeSummaryPolicy,
|
|
8347
8659
|
parseRetryAfter,
|
|
8660
|
+
pendingCommands,
|
|
8348
8661
|
preflightAssembly,
|
|
8662
|
+
pruneCommands,
|
|
8349
8663
|
readContinuityDescriptor,
|
|
8350
8664
|
readHookDeclarations,
|
|
8351
8665
|
resolveDebugTransportMode,
|