mixdog 0.9.10 → 0.9.12
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/package.json +1 -1
- package/scripts/agent-tag-reuse-smoke.mjs +12 -8
- package/scripts/bench/cache-probe-tasks.json +8 -0
- package/scripts/tool-smoke.mjs +109 -0
- package/src/defaults/mixdog-config.template.json +1 -0
- package/src/mixdog-session-runtime.mjs +46 -0
- package/src/runtime/agent/orchestrator/config.mjs +30 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +160 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +25 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +24 -9
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +18 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +79 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -22
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +29 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +17 -16
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +30 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +333 -14
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +129 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +58 -18
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +56 -83
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +26 -56
- package/src/runtime/channels/lib/webhook/deliveries.mjs +4 -1
- package/src/session-runtime/settings-api.mjs +13 -0
- package/src/session-runtime/tool-catalog.mjs +35 -7
- package/src/session-runtime/tool-defs.mjs +28 -0
- package/src/standalone/agent-tool/render.mjs +2 -0
- package/src/standalone/agent-tool.mjs +28 -6
- package/src/standalone/memory-runtime-proxy.mjs +85 -21
- package/src/tui/App.jsx +20 -1
- package/src/tui/app/extension-pickers.mjs +6 -3
- package/src/tui/dist/index.mjs +63 -16
- package/src/tui/engine.mjs +57 -12
|
@@ -12,10 +12,20 @@ import { withAdvisoryLocks } from '../builtin/advisory-lock.mjs';
|
|
|
12
12
|
import { wrapMutationRouteOutput } from '../mutation-planner.mjs';
|
|
13
13
|
import { getPluginData } from '../../config.mjs';
|
|
14
14
|
import { prepareInput, isV4APatchInput, hasUnifiedBareV4AHunk, canFallbackCountedUnified, parseV4APatch, isCompactedPlaceholderPatch } from './parsing.mjs';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
resolveBasePath,
|
|
17
|
+
resolveV4AEntryPath,
|
|
18
|
+
parsedEntryResolvedPath,
|
|
19
|
+
isResolvedPathOutsideBase,
|
|
20
|
+
assertNoDuplicateParsedModifyTargets,
|
|
21
|
+
mergeDuplicateParsedModifyEntries,
|
|
22
|
+
renderParsedUnifiedPatch,
|
|
23
|
+
rewriteHeaderPaths,
|
|
24
|
+
preValidateNativeBatch,
|
|
25
|
+
} from './paths.mjs';
|
|
16
26
|
import { ensureNativePatchBinaryAvailable } from './native-server.mjs';
|
|
17
27
|
import { assertPathReachable } from '../builtin/fs-reachability.mjs';
|
|
18
|
-
import { dispatchNativePatch } from './dispatch.mjs';
|
|
28
|
+
import { dispatchNativePatch, dispatchJsPatchEntries } from './dispatch.mjs';
|
|
19
29
|
import {
|
|
20
30
|
planV4ARenameSections,
|
|
21
31
|
applyV4ARenameSections,
|
|
@@ -172,6 +182,11 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
172
182
|
return 'Error: patch contained no file sections';
|
|
173
183
|
}
|
|
174
184
|
if (!v4aRenameOnly) {
|
|
185
|
+
try {
|
|
186
|
+
assertNoDuplicateParsedModifyTargets(parsed, basePath);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
return `Error: ${err?.message || String(err)}`;
|
|
189
|
+
}
|
|
175
190
|
const merged = mergeDuplicateParsedModifyEntries(parsed, basePath);
|
|
176
191
|
if (merged.changed) {
|
|
177
192
|
parsed = merged.parsed;
|
|
@@ -216,25 +231,50 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
216
231
|
if (lines.length === 0) return 'Error: patch contained no applicable file sections';
|
|
217
232
|
return wrapPatchMutationOutput(`${lines.join('\n')}\n`, mutationPlan, { backend: 'v4a-rename' });
|
|
218
233
|
}
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
basePath,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
234
|
+
const insideEntries = entries.filter((entry) => !isResolvedPathOutsideBase(entry.fullPath, basePath));
|
|
235
|
+
const outsideEntries = entries.filter((entry) => isResolvedPathOutsideBase(entry.fullPath, basePath));
|
|
236
|
+
const parsedInside = (parsed || []).filter(
|
|
237
|
+
(entry) => !isResolvedPathOutsideBase(parsedEntryResolvedPath(entry, basePath), basePath),
|
|
238
|
+
);
|
|
239
|
+
const resultParts = [];
|
|
240
|
+
if (outsideEntries.length > 0) {
|
|
241
|
+
const jsResult = await dispatchJsPatchEntries({
|
|
242
|
+
rows: outsideEntries,
|
|
243
|
+
parsed,
|
|
244
|
+
basePath,
|
|
245
|
+
dryRun,
|
|
246
|
+
fuzzy,
|
|
247
|
+
readStateScope,
|
|
248
|
+
});
|
|
249
|
+
if (isPatchErrorText(jsResult)) return wrapPatchMutationOutput(jsResult, mutationPlan, { backend: 'js-patch' });
|
|
250
|
+
resultParts.push(jsResult);
|
|
251
|
+
}
|
|
252
|
+
if (insideEntries.length > 0) {
|
|
253
|
+
const nativePatchStr = rewriteHeaderPaths(renderParsedUnifiedPatch(parsedInside), headerRewrites);
|
|
254
|
+
const nativeResult = await dispatchNativePatch({
|
|
255
|
+
entries: insideEntries,
|
|
256
|
+
basePath,
|
|
257
|
+
nativePatchStr,
|
|
258
|
+
fuzz,
|
|
259
|
+
rejectPartial,
|
|
260
|
+
dryRun,
|
|
261
|
+
readStateScope,
|
|
262
|
+
signal: abortSignal,
|
|
263
|
+
parsed: parsedInside,
|
|
264
|
+
});
|
|
265
|
+
if (isPatchErrorText(nativeResult)) {
|
|
266
|
+
if (resultParts.length > 0) return wrapPatchMutationOutput(nativeResult, mutationPlan, { backend: 'native-patch' });
|
|
267
|
+
return wrapPatchMutationOutput(nativeResult, mutationPlan, { backend: 'native-patch' });
|
|
268
|
+
}
|
|
269
|
+
resultParts.push(nativeResult);
|
|
270
|
+
}
|
|
271
|
+
let combined = resultParts.join('\n');
|
|
232
272
|
const renameLines = formatV4ARenameSuccessLines(v4aRenameResults);
|
|
233
|
-
if (renameLines.length > 0 && !isPatchErrorText(
|
|
234
|
-
combined = `${renameLines.join('\n')}\n${
|
|
273
|
+
if (renameLines.length > 0 && !isPatchErrorText(combined)) {
|
|
274
|
+
combined = `${renameLines.join('\n')}\n${combined}`;
|
|
235
275
|
}
|
|
236
276
|
if (!isPatchErrorText(combined) && options?.toolCallId) {
|
|
237
|
-
registerApplyPatchUiDiff(options.toolCallId,
|
|
277
|
+
registerApplyPatchUiDiff(options.toolCallId, rewriteHeaderPaths(normalizedPatchStr, headerRewrites));
|
|
238
278
|
}
|
|
239
279
|
if (!isPatchErrorText(combined) && rejectedV4AHunks.length > 0) {
|
|
240
280
|
const tail = [
|
|
@@ -39,11 +39,17 @@ export function resolveV4AEntryPath(basePath, rawName) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function resolveBasePath(cwd, basePath) {
|
|
42
|
-
if (
|
|
42
|
+
if (basePath == null || basePath === '') return cwd;
|
|
43
43
|
const norm = normalizeInputPath(basePath);
|
|
44
44
|
return isAbsolute(norm) ? pathResolve(norm) : resolveAgainstCwd(norm, cwd);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export function isResolvedPathOutsideBase(fullPath, basePath) {
|
|
48
|
+
const rel = pathRelative(pathResolve(basePath), pathResolve(fullPath)).replace(/\\/g, '/');
|
|
49
|
+
if (!rel || isAbsolute(rel)) return true;
|
|
50
|
+
return rel.split(/[\\/]+/).some((part) => part === '..');
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
// Categorise the per-file entry. A unified diff can describe:
|
|
48
54
|
// - modify : both files named, oldFileName exists on disk
|
|
49
55
|
// - create : oldFileName === /dev/null (or file doesn't exist + hunks start at 0)
|
|
@@ -56,6 +62,13 @@ export function classifyEntry(entry) {
|
|
|
56
62
|
return 'modify';
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
|
|
66
|
+
export function parsedEntryResolvedPath(entry, basePath) {
|
|
67
|
+
const kind = classifyEntry(entry);
|
|
68
|
+
const headerName = kind === 'create' ? entry.newFileName : entry.oldFileName;
|
|
69
|
+
return resolveEntryPath(basePath, headerName);
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
function parsedEntryTargetKey(entry, basePath) {
|
|
60
73
|
if (classifyEntry(entry) !== 'modify') return '';
|
|
61
74
|
const headerName = entry.oldFileName || entry.newFileName;
|
|
@@ -86,6 +99,28 @@ export function mergeDuplicateParsedModifyEntries(parsed, basePath) {
|
|
|
86
99
|
return { parsed: out, changed };
|
|
87
100
|
}
|
|
88
101
|
|
|
102
|
+
|
|
103
|
+
export function assertNoDuplicateParsedModifyTargets(parsed, basePath) {
|
|
104
|
+
const seenPaths = new Set();
|
|
105
|
+
for (const entry of parsed || []) {
|
|
106
|
+
if (classifyEntry(entry) !== 'modify') continue;
|
|
107
|
+
const key = parsedEntryTargetKey(entry, basePath);
|
|
108
|
+
if (!key) continue;
|
|
109
|
+
if (seenPaths.has(key)) {
|
|
110
|
+
const headerName = entry.oldFileName || entry.newFileName;
|
|
111
|
+
const display = normalizeOutputPath(stripDiffPrefix(headerName));
|
|
112
|
+
throw new Error(`apply_patch: duplicate target ${display} — patch lists the same path twice.`);
|
|
113
|
+
}
|
|
114
|
+
seenPaths.add(key);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function headerRelFromBase(basePath, absNorm) {
|
|
119
|
+
const rel = pathRelative(pathResolve(basePath), pathResolve(absNorm)).replace(/\\/g, '/');
|
|
120
|
+
if (!rel || isAbsolute(rel)) return null;
|
|
121
|
+
return rel;
|
|
122
|
+
}
|
|
123
|
+
|
|
89
124
|
function unifiedRange(start, lines) {
|
|
90
125
|
const s = Math.max(0, Number(start) || 0);
|
|
91
126
|
const n = Math.max(0, Number(lines) || 0);
|
|
@@ -120,36 +155,13 @@ export function countHunkChanges(hunks) {
|
|
|
120
155
|
return { added, removed };
|
|
121
156
|
}
|
|
122
157
|
|
|
123
|
-
// Header-shape pre-validator
|
|
124
|
-
// `..`
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
// silently degrading to a JS fallback.
|
|
128
|
-
function nativeHeaderSupported(entry, basePath) {
|
|
158
|
+
// Header-shape pre-validator: missing or /dev/null target path only.
|
|
159
|
+
// Lexical `..` and out-of-base absolutes resolve via resolveEntryPath;
|
|
160
|
+
// write permission is enforced at the hook layer, not here.
|
|
161
|
+
function nativeHeaderSupported(entry) {
|
|
129
162
|
const kind = classifyEntry(entry);
|
|
130
163
|
const headerName = kind === 'create' ? entry.newFileName : entry.oldFileName;
|
|
131
|
-
|
|
132
|
-
// Reject any `..` segment on EITHER oldFileName or newFileName (every
|
|
133
|
-
// non-/dev/null header) — a modify whose newFileName traverses out of
|
|
134
|
-
// base must still be refused even when the resolved path lands inside.
|
|
135
|
-
for (const which of ['oldFileName', 'newFileName']) {
|
|
136
|
-
const raw = entry[which];
|
|
137
|
-
if (!raw || DEV_NULL.test(raw)) continue;
|
|
138
|
-
const segs = normalizeInputPath(stripDiffPrefix(raw)).split(/[\\/]+/);
|
|
139
|
-
if (segs.some((part) => part === '..')) return false;
|
|
140
|
-
}
|
|
141
|
-
const stripped = stripDiffPrefix(headerName);
|
|
142
|
-
const norm = normalizeInputPath(stripped);
|
|
143
|
-
if (isAbsolute(norm) || /^[A-Za-z]:[\\/]/.test(norm)) {
|
|
144
|
-
if (!basePath) return false;
|
|
145
|
-
const absHeader = pathResolve(norm);
|
|
146
|
-
const absBase = pathResolve(basePath);
|
|
147
|
-
const rel = pathRelative(absBase, absHeader);
|
|
148
|
-
if (!rel || rel.startsWith('..') || isAbsolute(rel)) return false;
|
|
149
|
-
if (rel.split(/[\\/]+/).some((part) => part === '..')) return false;
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
return true;
|
|
164
|
+
return !!(headerName && !DEV_NULL.test(headerName));
|
|
153
165
|
}
|
|
154
166
|
|
|
155
167
|
// Resolve `absPath` via fs.realpathSync, walking up to the nearest existing
|
|
@@ -172,9 +184,7 @@ export function realpathNearestExistingAncestor(absPath) {
|
|
|
172
184
|
// Pre-validator (throws): walks every parsed entry and enforces
|
|
173
185
|
// - native engine is enabled
|
|
174
186
|
// - native binary exists on disk
|
|
175
|
-
// - each header
|
|
176
|
-
// - realpath of each non-/dev/null header stays inside the real basePath
|
|
177
|
-
// (catches symlink/junction escapes that lexical checks miss)
|
|
187
|
+
// - each header has a usable target path (shape only)
|
|
178
188
|
// - no duplicate target paths in the patch (case-insensitive on win32)
|
|
179
189
|
// Returns the list of normalized entry rows the dispatcher uses to format
|
|
180
190
|
// output, plus the header-rewrite map for absolute-path normalization.
|
|
@@ -199,61 +209,25 @@ export async function preValidateNativeBatch(parsed, basePath) {
|
|
|
199
209
|
}
|
|
200
210
|
}
|
|
201
211
|
await assertPathsReachable(reachabilityPaths);
|
|
202
|
-
let realBase;
|
|
203
|
-
try {
|
|
204
|
-
realBase = realpathSync(pathResolve(basePath));
|
|
205
|
-
} catch (err) {
|
|
206
|
-
throw new Error(`apply_patch: base_path unreadable (${err?.code || err?.message || String(err)}): ${basePath}`);
|
|
207
|
-
}
|
|
208
212
|
const entries = [];
|
|
209
213
|
const seenPaths = new Set();
|
|
210
214
|
const headerRewrites = [];
|
|
211
215
|
for (const entry of parsed) {
|
|
212
216
|
const kind = classifyEntry(entry);
|
|
213
217
|
const headerName = kind === 'create' ? entry.newFileName : entry.oldFileName;
|
|
214
|
-
if (!nativeHeaderSupported(entry
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
// patch that uses no `..`, cannot self-correct, and re-submits the same
|
|
223
|
-
// patch until the iteration cap. Emit a shape-specific, actionable hint
|
|
224
|
-
// for (a) so the loop breaks on the first failure.
|
|
225
|
-
const hasUsableHeader = headerName && !DEV_NULL.test(headerName);
|
|
226
|
-
if (!hasUsableHeader) {
|
|
227
|
-
throw new Error(
|
|
228
|
-
'apply_patch: a file section header could not be parsed (no target path). '
|
|
229
|
-
+ 'Each section must start with a valid header: `*** Update File: <path>` / '
|
|
230
|
-
+ '`*** Add File: <path>` / `*** Delete File: <path>` (V4A), or a '
|
|
231
|
-
+ '`--- a/<path>` + `+++ b/<path>` pair (unified). Wrap multi-hunk V4A '
|
|
232
|
-
+ 'edits in a `*** Begin Patch` / `*** End Patch` envelope and pass format:"v4a".',
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
const display = normalizeOutputPath(stripDiffPrefix(headerName));
|
|
236
|
-
throw new Error(`apply_patch: header ${display} is unsupported (path escapes base_path or contains \`..\`).`);
|
|
218
|
+
if (!nativeHeaderSupported(entry)) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
'apply_patch: a file section header could not be parsed (no target path). '
|
|
221
|
+
+ 'Each section must start with a valid header: `*** Update File: <path>` / '
|
|
222
|
+
+ '`*** Add File: <path>` / `*** Delete File: <path>` (V4A), or a '
|
|
223
|
+
+ '`--- a/<path>` + `+++ b/<path>` pair (unified). Wrap multi-hunk V4A '
|
|
224
|
+
+ 'edits in a `*** Begin Patch` / `*** End Patch` envelope and pass format:"v4a".',
|
|
225
|
+
);
|
|
237
226
|
}
|
|
238
227
|
if (kind !== 'delete' && !(entry.hunks?.length > 0)) {
|
|
239
228
|
const display = headerName ? normalizeOutputPath(stripDiffPrefix(headerName)) : '(unknown)';
|
|
240
229
|
throw new Error(`apply_patch: entry ${display} has no hunks — patch header malformed (use \`@@ -A,B +C,D @@\` per hunk).`);
|
|
241
230
|
}
|
|
242
|
-
// Realpath each non-/dev/null header; nearest-existing-ancestor handles
|
|
243
|
-
// create-mode leaves that do not yet exist. A symlink/junction whose
|
|
244
|
-
// target escapes basePath fails here even when the lexical check above
|
|
245
|
-
// looked safe.
|
|
246
|
-
for (const which of ['oldFileName', 'newFileName']) {
|
|
247
|
-
const checkName = entry[which];
|
|
248
|
-
if (!checkName || DEV_NULL.test(checkName)) continue;
|
|
249
|
-
const checkFull = resolveEntryPath(basePath, checkName);
|
|
250
|
-
const checkReal = realpathNearestExistingAncestor(checkFull);
|
|
251
|
-
const checkRel = pathRelative(realBase, checkReal);
|
|
252
|
-
if (checkRel.split(/[\\/]+/).some((part) => part === '..') || isAbsolute(checkRel)) {
|
|
253
|
-
const display = normalizeOutputPath(stripDiffPrefix(checkName));
|
|
254
|
-
throw new Error(`apply_patch: ${display} resolves outside base_path via symlink/junction; refusing to apply.`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
231
|
const fullPath = resolveEntryPath(basePath, headerName);
|
|
258
232
|
const pathKey = process.platform === 'win32' ? fullPath.toLowerCase() : fullPath;
|
|
259
233
|
if (seenPaths.has(pathKey)) {
|
|
@@ -272,19 +246,18 @@ export async function preValidateNativeBatch(parsed, basePath) {
|
|
|
272
246
|
hunks: entry.hunks?.length || 0,
|
|
273
247
|
linesChanged: added + removed,
|
|
274
248
|
});
|
|
275
|
-
// Absolute-form headers must be rewritten to
|
|
276
|
-
//
|
|
249
|
+
// Absolute-form headers must be rewritten to paths relative to basePath
|
|
250
|
+
// (including `..` segments for out-of-base targets) before the native
|
|
251
|
+
// server, which joins headers to basePath, sees them.
|
|
277
252
|
for (const which of ['oldFileName', 'newFileName']) {
|
|
278
253
|
const raw = entry[which];
|
|
279
254
|
if (!raw || DEV_NULL.test(raw)) continue;
|
|
280
255
|
const stripped = stripDiffPrefix(raw);
|
|
281
256
|
const norm = normalizeInputPath(stripped);
|
|
282
257
|
if (!isAbsolute(norm) && !/^[A-Za-z]:[\\/]/.test(norm)) continue;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
throw new Error(`apply_patch: absolute header ${display} does not resolve inside base_path.`);
|
|
287
|
-
}
|
|
258
|
+
if (isResolvedPathOutsideBase(pathResolve(norm), basePath)) continue;
|
|
259
|
+
const rel = headerRelFromBase(basePath, norm);
|
|
260
|
+
if (!rel || rel.startsWith('..')) continue;
|
|
288
261
|
headerRewrites.push({ from: raw, to: rel });
|
|
289
262
|
}
|
|
290
263
|
}
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
// conversion. Moved verbatim from patch.mjs; anchor/context matching, EOF
|
|
3
3
|
// handling, rename atomicity, and conversion output are all unchanged.
|
|
4
4
|
|
|
5
|
-
import { readFileSync,
|
|
5
|
+
import { readFileSync, lstatSync, mkdirSync } from 'node:fs';
|
|
6
6
|
import { unlink } from 'node:fs/promises';
|
|
7
|
-
import {
|
|
7
|
+
import { dirname as pathDirname } from 'node:path';
|
|
8
8
|
import {
|
|
9
|
-
normalizeInputPath,
|
|
10
9
|
normalizeOutputPath,
|
|
11
10
|
invalidateBuiltinResultCache,
|
|
12
11
|
clearReadSnapshotForPath,
|
|
@@ -18,7 +17,8 @@ import {
|
|
|
18
17
|
import { atomicWrite } from '../builtin/atomic-write.mjs';
|
|
19
18
|
import { assertPathReachable, assertPathsReachable } from '../builtin/fs-reachability.mjs';
|
|
20
19
|
import { markCodeGraphDirtyPaths } from '../code-graph-state.mjs';
|
|
21
|
-
import {
|
|
20
|
+
import { isSpecialFileStat } from '../builtin/device-paths.mjs';
|
|
21
|
+
import { resolveV4AEntryPath } from './paths.mjs';
|
|
22
22
|
import {
|
|
23
23
|
isV4AEndOfFileMarker,
|
|
24
24
|
parseUnifiedBareV4APatch,
|
|
@@ -219,7 +219,7 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
|
|
|
219
219
|
};
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
function applyV4AHunksToLines(sourceLines, hunks, options = {}) {
|
|
222
|
+
export function applyV4AHunksToLines(sourceLines, hunks, options = {}) {
|
|
223
223
|
const lines = cloneTextLinesForPatch(sourceLines);
|
|
224
224
|
const orderedHunks = orderV4AHunksByFilePosition(lines, hunks, options.fuzzy !== false);
|
|
225
225
|
let nextSearchLine = 0;
|
|
@@ -304,28 +304,21 @@ function v4aRenamePathKey(absPath) {
|
|
|
304
304
|
return process.platform === 'win32' ? String(absPath || '').toLowerCase() : String(absPath || '');
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
function
|
|
308
|
-
|
|
309
|
-
const checkRel = pathRelative(realBase, checkReal);
|
|
310
|
-
if (!checkRel || checkRel.startsWith('..') || isAbsolute(checkRel)) return false;
|
|
311
|
-
return !checkRel.split(/[\\/]+/).some((part) => part === '..');
|
|
307
|
+
function v4aSpecialFileStatMessage(displayPath) {
|
|
308
|
+
return `apply_patch: cannot patch special file (FIFO / character / block device / socket): ${normalizeOutputPath(displayPath)}`;
|
|
312
309
|
}
|
|
313
310
|
|
|
314
|
-
function
|
|
315
|
-
const
|
|
316
|
-
if (
|
|
317
|
-
|
|
318
|
-
|
|
311
|
+
function lstatV4APatchTarget(fullPath, displayPath) {
|
|
312
|
+
const st = lstatSync(fullPath);
|
|
313
|
+
if (isSpecialFileStat(st)) {
|
|
314
|
+
throw new Error(v4aSpecialFileStatMessage(displayPath));
|
|
315
|
+
}
|
|
316
|
+
return st;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function validateV4ARenameSection(section, basePath, seenDestKeys) {
|
|
319
320
|
const srcFull = resolveV4AEntryPath(basePath, section.path);
|
|
320
321
|
const destFull = resolveV4AEntryPath(basePath, section.movePath);
|
|
321
|
-
if (realBase) {
|
|
322
|
-
if (!v4aRenamePathInsideRealBase(srcFull, realBase)) {
|
|
323
|
-
return `apply_patch: ${normalizeOutputPath(section.path)} resolves outside base_path via symlink/junction; refusing V4A rename.`;
|
|
324
|
-
}
|
|
325
|
-
if (!v4aRenamePathInsideRealBase(destFull, realBase)) {
|
|
326
|
-
return `apply_patch: ${normalizeOutputPath(section.movePath)} resolves outside base_path via symlink/junction; refusing V4A rename.`;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
322
|
if (v4aRenamePathKey(srcFull) === v4aRenamePathKey(destFull)) {
|
|
330
323
|
return `apply_patch: V4A rename source and destination are the same path (${normalizeOutputPath(section.path)})`;
|
|
331
324
|
}
|
|
@@ -335,7 +328,10 @@ function validateV4ARenameSection(section, basePath, seenDestKeys, realBase) {
|
|
|
335
328
|
}
|
|
336
329
|
seenDestKeys.add(destKey);
|
|
337
330
|
try {
|
|
338
|
-
const st =
|
|
331
|
+
const st = lstatSync(srcFull);
|
|
332
|
+
if (isSpecialFileStat(st)) {
|
|
333
|
+
return v4aSpecialFileStatMessage(section.path);
|
|
334
|
+
}
|
|
339
335
|
if (!st.isFile()) {
|
|
340
336
|
return `apply_patch: V4A rename source is not a regular file: ${normalizeOutputPath(section.path)}`;
|
|
341
337
|
}
|
|
@@ -343,7 +339,10 @@ function validateV4ARenameSection(section, basePath, seenDestKeys, realBase) {
|
|
|
343
339
|
return `apply_patch: V4A rename source missing or unreadable: ${normalizeOutputPath(section.path)} (${err?.code || err?.message || String(err)})`;
|
|
344
340
|
}
|
|
345
341
|
try {
|
|
346
|
-
const destSt =
|
|
342
|
+
const destSt = lstatSync(destFull);
|
|
343
|
+
if (isSpecialFileStat(destSt)) {
|
|
344
|
+
return v4aSpecialFileStatMessage(section.movePath);
|
|
345
|
+
}
|
|
347
346
|
if (destSt.isDirectory()) {
|
|
348
347
|
return `apply_patch: V4A rename destination is a directory: ${normalizeOutputPath(section.movePath)}`;
|
|
349
348
|
}
|
|
@@ -451,15 +450,9 @@ export async function planV4ARenameSections(sections, basePath) {
|
|
|
451
450
|
resolveV4AEntryPath(basePath, section.movePath),
|
|
452
451
|
]);
|
|
453
452
|
await assertPathsReachable(renameReachPaths);
|
|
454
|
-
let realBase;
|
|
455
|
-
try {
|
|
456
|
-
realBase = realpathSync(pathResolve(basePath));
|
|
457
|
-
} catch (err) {
|
|
458
|
-
throw new Error(`apply_patch: base_path unreadable (${err?.code || err?.message || String(err)}): ${basePath}`);
|
|
459
|
-
}
|
|
460
453
|
const seenDestKeys = new Set();
|
|
461
454
|
for (const section of renameSections) {
|
|
462
|
-
const errText = validateV4ARenameSection(section, basePath, seenDestKeys
|
|
455
|
+
const errText = validateV4ARenameSection(section, basePath, seenDestKeys);
|
|
463
456
|
if (errText) throw new Error(errText);
|
|
464
457
|
}
|
|
465
458
|
return {
|
|
@@ -485,29 +478,8 @@ export function convertUnifiedCountedToUnifiedPatchViaV4A(patchStr, basePath, op
|
|
|
485
478
|
return convertV4ASectionsToUnifiedPatch(parseUnifiedCountedAsV4APatch(patchStr), basePath, options);
|
|
486
479
|
}
|
|
487
480
|
|
|
488
|
-
// Lexical path-escape guard for V4A section paths.
|
|
489
|
-
function v4aSectionPathEscapeError(section, basePath) {
|
|
490
|
-
const raw = section?.path;
|
|
491
|
-
if (!raw) return null;
|
|
492
|
-
const norm = normalizeInputPath(raw);
|
|
493
|
-
const segs = norm.split(/[\\/]+/);
|
|
494
|
-
if (segs.some((part) => part === '..')) {
|
|
495
|
-
return `apply_patch: header ${normalizeOutputPath(raw)} is unsupported (path escapes base_path or contains \`..\`).`;
|
|
496
|
-
}
|
|
497
|
-
if (isAbsolute(norm) || /^[A-Za-z]:[\\/]/.test(norm)) {
|
|
498
|
-
if (!basePath) return `apply_patch: header ${normalizeOutputPath(raw)} is unsupported (path escapes base_path or contains \`..\`).`;
|
|
499
|
-
const absHeader = pathResolve(norm);
|
|
500
|
-
const absBase = pathResolve(basePath);
|
|
501
|
-
const rel = pathRelative(absBase, absHeader);
|
|
502
|
-
if (!rel || rel.startsWith('..') || isAbsolute(rel) || rel.split(/[\\/]+/).some((part) => part === '..')) {
|
|
503
|
-
return `apply_patch: header ${normalizeOutputPath(raw)} is unsupported (path escapes base_path or contains \`..\`).`;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
return null;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
481
|
function readRawBufForV4AConversion(fullPath) {
|
|
510
|
-
const st =
|
|
482
|
+
const st = lstatV4APatchTarget(fullPath, fullPath);
|
|
511
483
|
const cached = rawContentCacheGet(fullPath, st);
|
|
512
484
|
if (cached) return cached;
|
|
513
485
|
const rawBuf = readFileSync(fullPath);
|
|
@@ -543,8 +515,6 @@ export async function convertV4ASectionsToUnifiedPatch(sections, basePath, optio
|
|
|
543
515
|
const out = [];
|
|
544
516
|
const v4aLinesCache = new Map();
|
|
545
517
|
for (const section of sections) {
|
|
546
|
-
const escapeErr = v4aSectionPathEscapeError(section, basePath);
|
|
547
|
-
if (escapeErr) throw new Error(escapeErr);
|
|
548
518
|
const displayPath = section.path.replace(/\\/g, '/');
|
|
549
519
|
if (section.kind === 'add') {
|
|
550
520
|
out.push('--- /dev/null');
|
|
@@ -114,7 +114,10 @@ function _mergeDeliveryRows(prior, entry) {
|
|
|
114
114
|
return prior ? { ...prior, ...entry } : entry;
|
|
115
115
|
}
|
|
116
116
|
function _isBlockingDeliveryStatus(status) {
|
|
117
|
-
|
|
117
|
+
// "pending" is a non-terminal claim too: between the pending row write and
|
|
118
|
+
// the terminal done/failed row (eventPipeline enqueue, delegate dispatch) a
|
|
119
|
+
// concurrent retry of the same delivery id must dedup, not double-dispatch.
|
|
120
|
+
return status === "received" || status === "pending" || status === "processing" || status === "done";
|
|
118
121
|
}
|
|
119
122
|
function _deliveryIndexFor(name) {
|
|
120
123
|
let map = _deliveryIndexByEndpoint.get(name);
|
|
@@ -13,6 +13,7 @@ export function createSettingsApi({
|
|
|
13
13
|
getSession,
|
|
14
14
|
getRemoteEnabled,
|
|
15
15
|
// config-lifecycle
|
|
16
|
+
adoptConfig,
|
|
16
17
|
saveConfigAndAdopt,
|
|
17
18
|
scheduleBackendSave,
|
|
18
19
|
// normalizers / helpers
|
|
@@ -129,6 +130,18 @@ export function createSettingsApi({
|
|
|
129
130
|
// setProfile patches title and/or language and persists. Unknown language
|
|
130
131
|
// ids normalize back to 'system'. Prompt-side injection is wired separately
|
|
131
132
|
// (composeSystemPrompt) — this only owns the stored value.
|
|
133
|
+
getDisabledSkills() {
|
|
134
|
+
const config = getConfig();
|
|
135
|
+
return cfgMod.normalizeSkillsConfig(config.skills);
|
|
136
|
+
},
|
|
137
|
+
setDisabledSkills(disabled) {
|
|
138
|
+
const names = disabled instanceof Set
|
|
139
|
+
? [...disabled]
|
|
140
|
+
: (Array.isArray(disabled) ? disabled : []);
|
|
141
|
+
const nextSkills = cfgMod.patchSkillsDisabled(names);
|
|
142
|
+
adoptConfig({ ...getConfig(), skills: nextSkills });
|
|
143
|
+
return this.getDisabledSkills();
|
|
144
|
+
},
|
|
132
145
|
setProfile(input = {}) {
|
|
133
146
|
const config = getConfig();
|
|
134
147
|
const current = cfgMod.normalizeProfileConfig(config.profile);
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// application/selection logic. Pure module (session objects passed in).
|
|
4
4
|
import { clean } from './session-text.mjs';
|
|
5
5
|
import { estimateToolSchemaTokens } from '../runtime/agent/orchestrator/session/context-utils.mjs';
|
|
6
|
+
import { applyInitialDeferredToolManifestToBp1 } from '../runtime/agent/orchestrator/context/collect.mjs';
|
|
7
|
+
import { getMcpServerInstructionsMap } from '../runtime/agent/orchestrator/mcp/client.mjs';
|
|
6
8
|
import {
|
|
7
9
|
isResponsesFreeformTool,
|
|
8
10
|
toResponsesCustomTool,
|
|
@@ -64,6 +66,7 @@ export const DEFERRED_DEFAULT_LEAD_TOOLS = Object.freeze([
|
|
|
64
66
|
'search',
|
|
65
67
|
'web_fetch',
|
|
66
68
|
'cwd',
|
|
69
|
+
'session_manage',
|
|
67
70
|
'Skill',
|
|
68
71
|
'tool_search',
|
|
69
72
|
]);
|
|
@@ -553,7 +556,21 @@ function setDeferredToolState(session, names) {
|
|
|
553
556
|
return selected;
|
|
554
557
|
}
|
|
555
558
|
|
|
556
|
-
function
|
|
559
|
+
export function deferredPoolToolNames(session) {
|
|
560
|
+
if (!session || session.deferredProviderMode === 'full') return [];
|
|
561
|
+
const catalog = Array.isArray(session.deferredToolCatalog)
|
|
562
|
+
? session.deferredToolCatalog
|
|
563
|
+
: [];
|
|
564
|
+
const active = new Set((session.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
565
|
+
const out = [];
|
|
566
|
+
for (const tool of catalog) {
|
|
567
|
+
const name = clean(tool?.name);
|
|
568
|
+
if (name && !active.has(name)) out.push(name);
|
|
569
|
+
}
|
|
570
|
+
return sortedNamesByMeasuredUsage(out);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export function isReadonlySelectable(tool) {
|
|
557
574
|
const name = clean(tool?.name);
|
|
558
575
|
if (READONLY_TOOL_NAMES.has(name)) return true;
|
|
559
576
|
const annotations = tool?.annotations || {};
|
|
@@ -602,16 +619,24 @@ export function applyDeferredToolSurface(session, mode, extraTools = [], options
|
|
|
602
619
|
} else {
|
|
603
620
|
setDeferredToolState(session, active);
|
|
604
621
|
}
|
|
622
|
+
if (!session.deferredToolBp1Applied && session.messages?.some((m) => m?.role === 'system')) {
|
|
623
|
+
if (!session.mcpServerInstructions || typeof session.mcpServerInstructions !== 'object') {
|
|
624
|
+
session.mcpServerInstructions = getMcpServerInstructionsMap();
|
|
625
|
+
}
|
|
626
|
+
applyInitialDeferredToolManifestToBp1(session, deferredPoolToolNames(session));
|
|
627
|
+
}
|
|
605
628
|
return session;
|
|
606
629
|
}
|
|
607
630
|
|
|
608
|
-
export function selectDeferredTools(session, names, mode) {
|
|
631
|
+
export function selectDeferredTools(session, names, mode, options = {}) {
|
|
632
|
+
const promoteToActive = options?.promoteToActive === true;
|
|
609
633
|
const catalog = Array.isArray(session?.deferredToolCatalog)
|
|
610
634
|
? session.deferredToolCatalog
|
|
611
635
|
: (Array.isArray(session?.tools) ? session.tools : []);
|
|
612
636
|
const active = new Set((session?.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
613
637
|
const native = session?.deferredProviderMode === 'native' || session?.deferredNativeTools === true;
|
|
614
638
|
const discovered = new Set(Array.isArray(session?.deferredDiscoveredTools) ? session.deferredDiscoveredTools : []);
|
|
639
|
+
const activateOnSurface = !native || promoteToActive;
|
|
615
640
|
const byName = new Map();
|
|
616
641
|
for (const tool of catalog) {
|
|
617
642
|
const name = clean(tool?.name);
|
|
@@ -635,20 +660,23 @@ export function selectDeferredTools(session, names, mode) {
|
|
|
635
660
|
blocked.push({ name, reason: 'readonly mode' });
|
|
636
661
|
continue;
|
|
637
662
|
}
|
|
638
|
-
if (active.has(name) || discovered.has(name)) {
|
|
663
|
+
if (active.has(name) || (!activateOnSurface && discovered.has(name))) {
|
|
639
664
|
already.push(name);
|
|
640
665
|
continue;
|
|
641
666
|
}
|
|
642
|
-
if (
|
|
643
|
-
discovered.add(name);
|
|
644
|
-
} else {
|
|
667
|
+
if (activateOnSurface) {
|
|
645
668
|
session.tools.push(tool);
|
|
646
669
|
active.add(name);
|
|
670
|
+
discovered.delete(name);
|
|
671
|
+
} else {
|
|
672
|
+
discovered.add(name);
|
|
647
673
|
}
|
|
648
674
|
added.push(name);
|
|
649
675
|
}
|
|
650
676
|
if (native) {
|
|
651
|
-
session.deferredDiscoveredTools = sortedNamesByMeasuredUsage(
|
|
677
|
+
session.deferredDiscoveredTools = sortedNamesByMeasuredUsage(
|
|
678
|
+
[...discovered].filter((toolName) => !active.has(toolName)),
|
|
679
|
+
);
|
|
652
680
|
session.deferredSelectedTools = sortedNamesByMeasuredUsage(active);
|
|
653
681
|
} else {
|
|
654
682
|
setDeferredToolState(session, active);
|
|
@@ -69,6 +69,34 @@ export const SKILL_TOOL = {
|
|
|
69
69
|
},
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
+
// Owner-directed session reset tool. `clear` mirrors /clear (full wipe);
|
|
73
|
+
// `compact_clear` mirrors the auto-clear path (summarize via the configured
|
|
74
|
+
// compactType, then reset — context carries forward in the summary). The
|
|
75
|
+
// reset is SCHEDULED: it runs when the current turn ends, never mid-turn,
|
|
76
|
+
// because the live transcript is still feeding the loop. Lead-session only;
|
|
77
|
+
// the runtime executor rejects agent-worker callers.
|
|
78
|
+
export const SESSION_MANAGE_TOOL = {
|
|
79
|
+
name: 'session_manage',
|
|
80
|
+
title: 'Session Manage',
|
|
81
|
+
annotations: {
|
|
82
|
+
title: 'Session Manage',
|
|
83
|
+
readOnlyHint: false,
|
|
84
|
+
destructiveHint: true,
|
|
85
|
+
idempotentHint: false,
|
|
86
|
+
openWorldHint: false,
|
|
87
|
+
agentHidden: true,
|
|
88
|
+
},
|
|
89
|
+
description: 'Reset this conversation on explicit user request. action=clear wipes all context (like /clear); action=compact_clear summarizes first and carries context forward (auto-clear style). Applies when the current turn ends.',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
action: { type: 'string', enum: ['clear', 'compact_clear'], description: 'clear = full wipe; compact_clear = summarize then reset.' },
|
|
94
|
+
},
|
|
95
|
+
required: ['action'],
|
|
96
|
+
additionalProperties: false,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
72
100
|
export const LEAD_DISALLOWED_TOOLS = Object.freeze([]);
|
|
73
101
|
export const AGENT_HIDDEN_WRAPPER_TOOLS = new Set([]);
|
|
74
102
|
|
|
@@ -82,6 +82,8 @@ export function renderResult(value) {
|
|
|
82
82
|
lines.push(`status: ${value.status}`);
|
|
83
83
|
if (value.type) lines.push(`type: ${value.type}`);
|
|
84
84
|
if (value.reused) lines.push('reused: true');
|
|
85
|
+
if (value.respawned) lines.push('respawned: true');
|
|
86
|
+
if (value.note) lines.push(`note: ${value.note}`);
|
|
85
87
|
if (value.tag || value.sessionId) lines.push(`target: ${value.tag || '-'} ${value.sessionId || ''}`.trim());
|
|
86
88
|
if (value.agent) lines.push(`agent: ${value.agent}`);
|
|
87
89
|
if (value.provider && value.model) lines.push(`model: ${value.provider}/${value.model}`);
|