@namzu/sdk 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +223 -0
- package/dist/advisory/__tests__/consultation-context.test.d.ts +2 -0
- package/dist/advisory/__tests__/consultation-context.test.d.ts.map +1 -0
- package/dist/advisory/__tests__/consultation-context.test.js +124 -0
- package/dist/advisory/__tests__/consultation-context.test.js.map +1 -0
- package/dist/advisory/context.d.ts +25 -0
- package/dist/advisory/context.d.ts.map +1 -1
- package/dist/advisory/context.js +18 -0
- package/dist/advisory/context.js.map +1 -1
- package/dist/advisory/executor.d.ts.map +1 -1
- package/dist/advisory/executor.js +25 -3
- package/dist/advisory/executor.js.map +1 -1
- package/dist/compaction/__tests__/context-reducer.test.d.ts +2 -0
- package/dist/compaction/__tests__/context-reducer.test.d.ts.map +1 -0
- package/dist/compaction/__tests__/context-reducer.test.js +197 -0
- package/dist/compaction/__tests__/context-reducer.test.js.map +1 -0
- package/dist/compaction/factory.d.ts +7 -0
- package/dist/compaction/factory.d.ts.map +1 -1
- package/dist/compaction/factory.js +7 -0
- package/dist/compaction/factory.js.map +1 -1
- package/dist/compaction/index.d.ts +2 -0
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/index.js +1 -0
- package/dist/compaction/index.js.map +1 -1
- package/dist/compaction/interface.d.ts +13 -0
- package/dist/compaction/interface.d.ts.map +1 -1
- package/dist/compaction/managers/null.d.ts +3 -0
- package/dist/compaction/managers/null.d.ts.map +1 -1
- package/dist/compaction/managers/null.js +3 -0
- package/dist/compaction/managers/null.js.map +1 -1
- package/dist/compaction/managers/slidingWindow.d.ts +6 -0
- package/dist/compaction/managers/slidingWindow.d.ts.map +1 -1
- package/dist/compaction/managers/slidingWindow.js +6 -0
- package/dist/compaction/managers/slidingWindow.js.map +1 -1
- package/dist/compaction/managers/structured.d.ts +10 -0
- package/dist/compaction/managers/structured.d.ts.map +1 -1
- package/dist/compaction/managers/structured.js +10 -0
- package/dist/compaction/managers/structured.js.map +1 -1
- package/dist/compaction/reducer.d.ts +86 -0
- package/dist/compaction/reducer.d.ts.map +1 -0
- package/dist/compaction/reducer.js +77 -0
- package/dist/compaction/reducer.js.map +1 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts +2 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts.map +1 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.js +54 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.js.map +1 -0
- package/dist/connector/builtins/http.d.ts.map +1 -1
- package/dist/connector/builtins/http.js +24 -2
- package/dist/connector/builtins/http.js.map +1 -1
- package/dist/connector/builtins/http.test.js +18 -2
- package/dist/connector/builtins/http.test.js.map +1 -1
- package/dist/connector/index.d.ts +2 -2
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts +2 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts.map +1 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js +214 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js.map +1 -0
- package/dist/connector/mcp/client.d.ts +52 -1
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +86 -0
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/discovery.d.ts +12 -1
- package/dist/connector/mcp/discovery.d.ts.map +1 -1
- package/dist/connector/mcp/discovery.js +19 -4
- package/dist/connector/mcp/discovery.js.map +1 -1
- package/dist/connector/mcp/index.d.ts +2 -2
- package/dist/connector/mcp/index.d.ts.map +1 -1
- package/dist/connector/mcp/index.js +1 -1
- package/dist/connector/mcp/index.js.map +1 -1
- package/dist/connector/mcp/server.d.ts +42 -1
- package/dist/connector/mcp/server.d.ts.map +1 -1
- package/dist/connector/mcp/server.js +77 -4
- package/dist/connector/mcp/server.js.map +1 -1
- package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts +2 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts.map +1 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.js +58 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.js.map +1 -0
- package/dist/plugin/__tests__/discovery-scopes.test.d.ts +2 -0
- package/dist/plugin/__tests__/discovery-scopes.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/discovery-scopes.test.js +97 -0
- package/dist/plugin/__tests__/discovery-scopes.test.js.map +1 -0
- package/dist/plugin/__tests__/enable-contributions.test.js +5 -1
- package/dist/plugin/__tests__/enable-contributions.test.js.map +1 -1
- package/dist/plugin/__tests__/mcp-admission.test.d.ts +2 -0
- package/dist/plugin/__tests__/mcp-admission.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/mcp-admission.test.js +192 -0
- package/dist/plugin/__tests__/mcp-admission.test.js.map +1 -0
- package/dist/plugin/lifecycle.d.ts +41 -0
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +29 -1
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/loader.d.ts +39 -3
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +37 -4
- package/dist/plugin/loader.js.map +1 -1
- package/dist/public-runtime.d.ts +5 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +11 -3
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -2
- package/dist/public-types.d.ts.map +1 -1
- package/dist/rag/__tests__/namespace-isolation.test.d.ts +2 -0
- package/dist/rag/__tests__/namespace-isolation.test.d.ts.map +1 -0
- package/dist/rag/__tests__/namespace-isolation.test.js +80 -0
- package/dist/rag/__tests__/namespace-isolation.test.js.map +1 -0
- package/dist/rag/ingestion.d.ts.map +1 -1
- package/dist/rag/ingestion.js +1 -0
- package/dist/rag/ingestion.js.map +1 -1
- package/dist/rag/retriever.d.ts.map +1 -1
- package/dist/rag/retriever.js +2 -0
- package/dist/rag/retriever.js.map +1 -1
- package/dist/rag/vector-store.d.ts.map +1 -1
- package/dist/rag/vector-store.js +6 -0
- package/dist/rag/vector-store.js.map +1 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +113 -109
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/runtime/query/__tests__/capability-negotiation.test.js +59 -0
- package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -1
- package/dist/runtime/query/__tests__/per-step-skills.test.d.ts +10 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.js +122 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.js.map +1 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.js +153 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.js.map +1 -0
- package/dist/runtime/query/__tests__/resume-run.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/resume-run.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/resume-run.test.js +211 -0
- package/dist/runtime/query/__tests__/resume-run.test.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +1 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +10 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +10 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +26 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +118 -34
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js +96 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +95 -5
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js +180 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/resume-run.d.ts +70 -0
- package/dist/runtime/query/resume-run.d.ts.map +1 -0
- package/dist/runtime/query/resume-run.js +46 -0
- package/dist/runtime/query/resume-run.js.map +1 -0
- package/dist/telemetry/__tests__/model-call-span.test.d.ts +2 -0
- package/dist/telemetry/__tests__/model-call-span.test.d.ts.map +1 -0
- package/dist/telemetry/__tests__/model-call-span.test.js +147 -0
- package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -0
- package/dist/telemetry/__tests__/span-closure.test.d.ts +2 -0
- package/dist/telemetry/__tests__/span-closure.test.d.ts.map +1 -0
- package/dist/telemetry/__tests__/span-closure.test.js +124 -0
- package/dist/telemetry/__tests__/span-closure.test.js.map +1 -0
- package/dist/tools/advisory/index.js +1 -1
- package/dist/tools/advisory/index.js.map +1 -1
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js +78 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js.map +1 -0
- package/dist/tools/builtins/__tests__/edit.test.js +242 -1
- package/dist/tools/builtins/__tests__/edit.test.js.map +1 -1
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.js +187 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.js.map +1 -0
- package/dist/tools/builtins/atomic-write-file.d.ts +1 -1
- package/dist/tools/builtins/atomic-write-file.d.ts.map +1 -1
- package/dist/tools/builtins/atomic-write-file.js +31 -2
- package/dist/tools/builtins/atomic-write-file.js.map +1 -1
- package/dist/tools/builtins/content-fingerprint.d.ts +23 -0
- package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -0
- package/dist/tools/builtins/content-fingerprint.js +30 -0
- package/dist/tools/builtins/content-fingerprint.js.map +1 -0
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +172 -31
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +5 -2
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/write-file.d.ts.map +1 -1
- package/dist/tools/builtins/write-file.js +75 -27
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/__tests__/ask-user-question.test.js +55 -0
- package/dist/tools/coordinator/__tests__/ask-user-question.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.js +126 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +75 -5
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/coordinator/plan-dependencies.d.ts +43 -0
- package/dist/tools/coordinator/plan-dependencies.d.ts.map +1 -0
- package/dist/tools/coordinator/plan-dependencies.js +148 -0
- package/dist/tools/coordinator/plan-dependencies.js.map +1 -0
- package/dist/types/agent/supervisor.d.ts +15 -0
- package/dist/types/agent/supervisor.d.ts.map +1 -1
- package/dist/types/connector/core.d.ts +34 -0
- package/dist/types/connector/core.d.ts.map +1 -1
- package/dist/types/connector/definition.d.ts +10 -0
- package/dist/types/connector/definition.d.ts.map +1 -1
- package/dist/types/connector/mcp.d.ts +13 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/rag/retrieval.d.ts +16 -0
- package/dist/types/rag/retrieval.d.ts.map +1 -1
- package/dist/types/rag/storage.d.ts +9 -0
- package/dist/types/rag/storage.d.ts.map +1 -1
- package/dist/types/rag/vector.d.ts +11 -0
- package/dist/types/rag/vector.d.ts.map +1 -1
- package/dist/types/router/task-router.d.ts +19 -0
- package/dist/types/router/task-router.d.ts.map +1 -1
- package/dist/types/run/prepare-step.d.ts +56 -3
- package/dist/types/run/prepare-step.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +17 -1
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/toolset/index.d.ts +22 -0
- package/dist/types/toolset/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/advisory/__tests__/consultation-context.test.ts +191 -0
- package/src/advisory/context.ts +32 -0
- package/src/advisory/executor.ts +30 -3
- package/src/compaction/__tests__/context-reducer.test.ts +239 -0
- package/src/compaction/factory.ts +7 -0
- package/src/compaction/index.ts +8 -0
- package/src/compaction/interface.ts +13 -0
- package/src/compaction/managers/null.ts +3 -0
- package/src/compaction/managers/slidingWindow.ts +6 -0
- package/src/compaction/managers/structured.ts +10 -0
- package/src/compaction/reducer.ts +154 -0
- package/src/connector/builtins/__tests__/oauth2-auth.test.ts +73 -0
- package/src/connector/builtins/http.test.ts +28 -2
- package/src/connector/builtins/http.ts +26 -2
- package/src/connector/index.ts +6 -2
- package/src/connector/mcp/__tests__/prompts-and-lifecycle.test.ts +286 -0
- package/src/connector/mcp/client.ts +95 -0
- package/src/connector/mcp/discovery.ts +19 -4
- package/src/connector/mcp/index.ts +6 -2
- package/src/connector/mcp/server.ts +101 -3
- package/src/manager/agent/__tests__/depth-limit-authority.test.ts +74 -0
- package/src/plugin/__tests__/discovery-scopes.test.ts +133 -0
- package/src/plugin/__tests__/enable-contributions.test.ts +5 -1
- package/src/plugin/__tests__/mcp-admission.test.ts +242 -0
- package/src/plugin/lifecycle.ts +57 -1
- package/src/plugin/loader.ts +57 -3
- package/src/public-runtime.ts +16 -1
- package/src/public-types.ts +5 -0
- package/src/rag/__tests__/namespace-isolation.test.ts +109 -0
- package/src/rag/ingestion.ts +1 -0
- package/src/rag/retriever.ts +2 -0
- package/src/rag/vector-store.ts +5 -0
- package/src/registry/tool/execute.ts +123 -119
- package/src/runtime/query/__tests__/capability-negotiation.test.ts +69 -0
- package/src/runtime/query/__tests__/per-step-skills.test.ts +154 -0
- package/src/runtime/query/__tests__/per-step-tool-choice.test.ts +180 -0
- package/src/runtime/query/__tests__/resume-run.test.ts +262 -0
- package/src/runtime/query/executor.ts +9 -1
- package/src/runtime/query/index.ts +39 -0
- package/src/runtime/query/iteration/index.ts +130 -34
- package/src/runtime/query/iteration/phases/compaction-model-routing.test.ts +125 -0
- package/src/runtime/query/iteration/phases/compaction.ts +106 -5
- package/src/runtime/query/iteration/phases/context-reducer-dispatch.test.ts +238 -0
- package/src/runtime/query/iteration/phases/context.ts +11 -0
- package/src/runtime/query/resume-run.ts +93 -0
- package/src/telemetry/__tests__/model-call-span.test.ts +189 -0
- package/src/telemetry/__tests__/span-closure.test.ts +153 -0
- package/src/tools/advisory/index.ts +1 -1
- package/src/tools/builtins/__tests__/edit-commits-atomically.test.ts +103 -0
- package/src/tools/builtins/__tests__/edit.test.ts +324 -1
- package/src/tools/builtins/__tests__/stale-file-refusal.test.ts +258 -0
- package/src/tools/builtins/atomic-write-file.ts +30 -2
- package/src/tools/builtins/content-fingerprint.ts +33 -0
- package/src/tools/builtins/edit.ts +188 -36
- package/src/tools/builtins/read-file.ts +5 -2
- package/src/tools/builtins/write-file.ts +79 -28
- package/src/tools/coordinator/__tests__/ask-user-question.test.ts +68 -0
- package/src/tools/coordinator/__tests__/plan-dependencies.test.ts +186 -0
- package/src/tools/coordinator/index.ts +116 -42
- package/src/tools/coordinator/plan-dependencies.ts +175 -0
- package/src/types/agent/supervisor.ts +15 -0
- package/src/types/connector/core.ts +34 -0
- package/src/types/connector/definition.ts +10 -0
- package/src/types/connector/mcp.ts +14 -0
- package/src/types/rag/retrieval.ts +16 -0
- package/src/types/rag/storage.ts +9 -0
- package/src/types/rag/vector.ts +11 -0
- package/src/types/router/task-router.ts +19 -0
- package/src/types/run/prepare-step.ts +58 -3
- package/src/types/tool/index.ts +17 -1
- package/src/types/toolset/index.ts +22 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { lstatSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { atomicWriteFile } from '../atomic-write-file.js';
|
|
6
|
+
import { fingerprintContent } from '../content-fingerprint.js';
|
|
7
|
+
import { EditTool } from '../edit.js';
|
|
8
|
+
/**
|
|
9
|
+
* The mutation lock serializes THIS runtime's writers. It cannot see a
|
|
10
|
+
* person editing in an editor, another process, or a second agent run —
|
|
11
|
+
* and an edit computed against a body that has since moved is a lost
|
|
12
|
+
* update whichever of those did the moving.
|
|
13
|
+
*
|
|
14
|
+
* Worse than undetected, it was misreported: an `old_string` that no
|
|
15
|
+
* longer matches came back as "not found in file — make sure the string
|
|
16
|
+
* matches exactly", which tells the agent its input was wrong when the
|
|
17
|
+
* file changed underneath it. The agent then retries the same edit
|
|
18
|
+
* against the same moved file.
|
|
19
|
+
*/
|
|
20
|
+
function trackerOver(store) {
|
|
21
|
+
return {
|
|
22
|
+
recordRead: (key, content) => {
|
|
23
|
+
if (content !== undefined)
|
|
24
|
+
store.set(key, fingerprintContent(content));
|
|
25
|
+
},
|
|
26
|
+
hasRead: (key) => store.has(key),
|
|
27
|
+
fingerprint: (key) => store.get(key),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function contextWith(workingDirectory, tracker) {
|
|
31
|
+
return {
|
|
32
|
+
runId: 'run_stale',
|
|
33
|
+
workingDirectory,
|
|
34
|
+
abortSignal: new AbortController().signal,
|
|
35
|
+
env: {},
|
|
36
|
+
log: () => { },
|
|
37
|
+
...(tracker ? { fileReadTracker: tracker } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
describe('an edit against a file that moved is refused, not applied', () => {
|
|
41
|
+
it('refuses when the body changed since the agent read it', async () => {
|
|
42
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-stale-'));
|
|
43
|
+
const file = join(dir, 'doc.md');
|
|
44
|
+
writeFileSync(file, 'alpha\nbeta\n');
|
|
45
|
+
const store = new Map();
|
|
46
|
+
const tracker = trackerOver(store);
|
|
47
|
+
tracker.recordRead(file, 'alpha\nbeta\n');
|
|
48
|
+
// Somebody else rewrites the very line this edit anchors on.
|
|
49
|
+
writeFileSync(file, 'alpha\nBETA WAS REWRITTEN\n');
|
|
50
|
+
const result = await EditTool.execute({ path: 'doc.md', old_string: 'beta', new_string: 'gamma', replace_all: false }, contextWith(dir, tracker));
|
|
51
|
+
expect(result.success).toBe(false);
|
|
52
|
+
expect(result.error).toContain('changed on disk after you read it');
|
|
53
|
+
// The refusal has to be actionable, and "nothing was written" is the
|
|
54
|
+
// part that stops the agent re-running the same edit blind.
|
|
55
|
+
expect(result.error).toContain('Read the file again');
|
|
56
|
+
expect(result.error).toContain('Nothing was written');
|
|
57
|
+
expect(readFileSync(file, 'utf-8')).toBe('alpha\nBETA WAS REWRITTEN\n');
|
|
58
|
+
});
|
|
59
|
+
it('applies normally when the file is untouched', async () => {
|
|
60
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-stale-'));
|
|
61
|
+
const file = join(dir, 'doc.md');
|
|
62
|
+
writeFileSync(file, 'alpha\nbeta\n');
|
|
63
|
+
const tracker = trackerOver(new Map());
|
|
64
|
+
tracker.recordRead(file, 'alpha\nbeta\n');
|
|
65
|
+
const result = await EditTool.execute({ path: 'doc.md', old_string: 'beta', new_string: 'gamma', replace_all: false }, contextWith(dir, tracker));
|
|
66
|
+
expect(result.success).toBe(true);
|
|
67
|
+
expect(readFileSync(file, 'utf-8')).toBe('alpha\ngamma\n');
|
|
68
|
+
});
|
|
69
|
+
it('lets a second edit in the same turn proceed against what the first wrote', async () => {
|
|
70
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-stale-'));
|
|
71
|
+
const file = join(dir, 'doc.md');
|
|
72
|
+
writeFileSync(file, 'alpha\nbeta\n');
|
|
73
|
+
const tracker = trackerOver(new Map());
|
|
74
|
+
tracker.recordRead(file, 'alpha\nbeta\n');
|
|
75
|
+
const context = contextWith(dir, tracker);
|
|
76
|
+
const first = await EditTool.execute({ path: 'doc.md', old_string: 'beta', new_string: 'gamma', replace_all: false }, context);
|
|
77
|
+
// Without re-fingerprinting after a successful write, this second edit
|
|
78
|
+
// would see its own predecessor's output as someone else's drift.
|
|
79
|
+
const second = await EditTool.execute({ path: 'doc.md', old_string: 'alpha', new_string: 'omega', replace_all: false }, context);
|
|
80
|
+
expect(first.success).toBe(true);
|
|
81
|
+
expect(second.success).toBe(true);
|
|
82
|
+
expect(readFileSync(file, 'utf-8')).toBe('omega\ngamma\n');
|
|
83
|
+
});
|
|
84
|
+
it('proceeds when the host tracks nothing, which is the older contract', async () => {
|
|
85
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-stale-'));
|
|
86
|
+
writeFileSync(join(dir, 'doc.md'), 'alpha\nbeta\n');
|
|
87
|
+
// `fingerprint` is optional. A host that never captured one must not
|
|
88
|
+
// have every edit refused.
|
|
89
|
+
const result = await EditTool.execute({ path: 'doc.md', old_string: 'beta', new_string: 'gamma', replace_all: false }, contextWith(dir));
|
|
90
|
+
expect(result.success).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
it('proceeds when the path was never read, so there is nothing to be stale against', async () => {
|
|
93
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-stale-'));
|
|
94
|
+
writeFileSync(join(dir, 'doc.md'), 'alpha\nbeta\n');
|
|
95
|
+
const result = await EditTool.execute({ path: 'doc.md', old_string: 'beta', new_string: 'gamma', replace_all: false }, contextWith(dir, trackerOver(new Map())));
|
|
96
|
+
expect(result.success).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('the fingerprint distinguishes bodies', () => {
|
|
100
|
+
it('is stable for identical content', () => {
|
|
101
|
+
expect(fingerprintContent('alpha\n')).toBe(fingerprintContent('alpha\n'));
|
|
102
|
+
});
|
|
103
|
+
it('differs on a one-character change', () => {
|
|
104
|
+
expect(fingerprintContent('alpha\n')).not.toBe(fingerprintContent('alphb\n'));
|
|
105
|
+
});
|
|
106
|
+
it('differs on trailing whitespace, which a diff would also treat as a change', () => {
|
|
107
|
+
expect(fingerprintContent('alpha')).not.toBe(fingerprintContent('alpha '));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('an atomic commit writes through a symlink rather than over it', () => {
|
|
111
|
+
/** Creating one needs a privilege this platform does not always grant. */
|
|
112
|
+
function symlinkOrSkip(target, link) {
|
|
113
|
+
try {
|
|
114
|
+
symlinkSync(target, link);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
it('keeps the link and updates its target', async () => {
|
|
122
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-symlink-'));
|
|
123
|
+
const real = join(dir, 'real.md');
|
|
124
|
+
const link = join(dir, 'link.md');
|
|
125
|
+
writeFileSync(real, 'original\n');
|
|
126
|
+
if (!symlinkOrSkip(real, link))
|
|
127
|
+
return;
|
|
128
|
+
await atomicWriteFile(link, 'replaced\n');
|
|
129
|
+
// `rename` onto the link path would have swapped the link for a plain
|
|
130
|
+
// file, silently detaching every other path that pointed through it.
|
|
131
|
+
expect(lstatSync(link).isSymbolicLink()).toBe(true);
|
|
132
|
+
expect(readFileSync(real, 'utf-8')).toBe('replaced\n');
|
|
133
|
+
expect(readFileSync(link, 'utf-8')).toBe('replaced\n');
|
|
134
|
+
});
|
|
135
|
+
it('still creates a file that does not exist yet', async () => {
|
|
136
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-symlink-'));
|
|
137
|
+
const fresh = join(dir, 'new.md');
|
|
138
|
+
await atomicWriteFile(fresh, 'created\n');
|
|
139
|
+
expect(readFileSync(fresh, 'utf-8')).toBe('created\n');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('drift that does not touch the anchor is not a conflict', () => {
|
|
143
|
+
it('applies when someone changed an unrelated part of the file', async () => {
|
|
144
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-stale-'));
|
|
145
|
+
const file = join(dir, 'doc.md');
|
|
146
|
+
writeFileSync(file, 'alpha\nbeta\n');
|
|
147
|
+
const tracker = trackerOver(new Map());
|
|
148
|
+
tracker.recordRead(file, 'alpha\nbeta\n');
|
|
149
|
+
// Someone appends a line. The edit's anchor is untouched, so the edit
|
|
150
|
+
// is still exactly as well defined as when it was computed —
|
|
151
|
+
// refusing here would reject a safe edit every time anybody wrote
|
|
152
|
+
// anywhere in the file.
|
|
153
|
+
writeFileSync(file, 'alpha\nbeta\nan unrelated new line\n');
|
|
154
|
+
const result = await EditTool.execute({ path: 'doc.md', old_string: 'beta', new_string: 'gamma', replace_all: false }, contextWith(dir, tracker));
|
|
155
|
+
expect(result.success).toBe(true);
|
|
156
|
+
expect(readFileSync(file, 'utf-8')).toBe('alpha\ngamma\nan unrelated new line\n');
|
|
157
|
+
});
|
|
158
|
+
it('reports a genuinely wrong anchor as wrong, not as drift', async () => {
|
|
159
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-stale-'));
|
|
160
|
+
const file = join(dir, 'doc.md');
|
|
161
|
+
writeFileSync(file, 'alpha\nbeta\n');
|
|
162
|
+
const tracker = trackerOver(new Map());
|
|
163
|
+
tracker.recordRead(file, 'alpha\nbeta\n');
|
|
164
|
+
// Nothing moved. The anchor is simply not in the file, and telling
|
|
165
|
+
// the agent to re-read would send it to look for a change that never
|
|
166
|
+
// happened.
|
|
167
|
+
const result = await EditTool.execute({ path: 'doc.md', old_string: 'nowhere', new_string: 'x', replace_all: false }, contextWith(dir, tracker));
|
|
168
|
+
expect(result.success).toBe(false);
|
|
169
|
+
expect(result.error).toContain('not found in file');
|
|
170
|
+
expect(result.error).not.toContain('changed on disk');
|
|
171
|
+
});
|
|
172
|
+
it('reports drift when the anchor became ambiguous because of it', async () => {
|
|
173
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-stale-'));
|
|
174
|
+
const file = join(dir, 'doc.md');
|
|
175
|
+
writeFileSync(file, 'alpha\nbeta\n');
|
|
176
|
+
const tracker = trackerOver(new Map());
|
|
177
|
+
tracker.recordRead(file, 'alpha\nbeta\n');
|
|
178
|
+
// A second copy of the anchor appears. "not unique — add more
|
|
179
|
+
// context" would send the agent to widen an anchor that was unique
|
|
180
|
+
// when it read the file.
|
|
181
|
+
writeFileSync(file, 'alpha\nbeta\nbeta\n');
|
|
182
|
+
const result = await EditTool.execute({ path: 'doc.md', old_string: 'beta', new_string: 'gamma', replace_all: false }, contextWith(dir, tracker));
|
|
183
|
+
expect(result.success).toBe(false);
|
|
184
|
+
expect(result.error).toContain('changed on disk');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
//# sourceMappingURL=stale-file-refusal.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stale-file-refusal.test.js","sourceRoot":"","sources":["../../../../src/tools/builtins/__tests__/stale-file-refusal.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,6 +8,6 @@ interface AtomicWriteHooks {
|
|
|
8
8
|
* The temp file lives beside the destination so rename stays on one
|
|
9
9
|
* filesystem. A failed pre-commit write leaves the original untouched.
|
|
10
10
|
*/
|
|
11
|
-
export declare function atomicWriteFile(
|
|
11
|
+
export declare function atomicWriteFile(requestedPath: string, content: string, hooks?: AtomicWriteHooks): Promise<void>;
|
|
12
12
|
export {};
|
|
13
13
|
//# sourceMappingURL=atomic-write-file.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"atomic-write-file.d.ts","sourceRoot":"","sources":["../../../src/tools/builtins/atomic-write-file.ts"],"names":[],"mappings":"AAIA,UAAU,gBAAgB;IACzB,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAClC;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,
|
|
1
|
+
{"version":3,"file":"atomic-write-file.d.ts","sourceRoot":"","sources":["../../../src/tools/builtins/atomic-write-file.ts"],"names":[],"mappings":"AAIA,UAAU,gBAAgB;IACzB,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAClC;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,gBAAqB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAmCf"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir, open, rename, stat, unlink } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, open, realpath, rename, stat, unlink } from 'node:fs/promises';
|
|
3
3
|
import { basename, dirname, join } from 'node:path';
|
|
4
4
|
/**
|
|
5
5
|
* Commit a complete file body without exposing a truncated destination.
|
|
@@ -7,7 +7,14 @@ import { basename, dirname, join } from 'node:path';
|
|
|
7
7
|
* The temp file lives beside the destination so rename stays on one
|
|
8
8
|
* filesystem. A failed pre-commit write leaves the original untouched.
|
|
9
9
|
*/
|
|
10
|
-
export async function atomicWriteFile(
|
|
10
|
+
export async function atomicWriteFile(requestedPath, content, hooks = {}) {
|
|
11
|
+
// Write THROUGH a symlink, not over it. `rename` replaces whatever sits
|
|
12
|
+
// at the destination, so committing onto a link path swaps the link for
|
|
13
|
+
// a regular file: the link is gone, and every other path that pointed
|
|
14
|
+
// through it now reads stale content. Resolving first keeps the link and
|
|
15
|
+
// updates what it points at, which is what a caller editing a linked
|
|
16
|
+
// file means. A path that does not exist yet resolves to itself.
|
|
17
|
+
const filePath = await realpath(requestedPath).catch(() => requestedPath);
|
|
11
18
|
const directory = dirname(filePath);
|
|
12
19
|
await mkdir(directory, { recursive: true });
|
|
13
20
|
const existingMode = await stat(filePath)
|
|
@@ -33,12 +40,34 @@ export async function atomicWriteFile(filePath, content, hooks = {}) {
|
|
|
33
40
|
await unlink(tempPath).catch(() => undefined);
|
|
34
41
|
}
|
|
35
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Best-effort, and only after the rename has already committed.
|
|
45
|
+
*
|
|
46
|
+
* Syncing the directory is what makes the RENAME survive a power loss on a
|
|
47
|
+
* POSIX filesystem. Not every platform permits it — opening a directory and
|
|
48
|
+
* calling fsync raises `EPERM` on Windows — and there the whole builtin was
|
|
49
|
+
* unusable: every `edit` and every `write` failed with `EPERM: operation not
|
|
50
|
+
* permitted, fsync` after correctly writing the file.
|
|
51
|
+
*
|
|
52
|
+
* Swallowing is right here and nowhere else in this file. By this point the
|
|
53
|
+
* destination already holds the complete new body; the only thing still at
|
|
54
|
+
* stake is durability across an OS-level crash, and on a platform that
|
|
55
|
+
* refuses the call there is no second way to ask. Failing the write would
|
|
56
|
+
* trade a durability refinement for a guaranteed error on every write.
|
|
57
|
+
*
|
|
58
|
+
* The sibling in `utils/atomic-write.ts` — the one the disk stores use, and
|
|
59
|
+
* the one that has run on this platform all along — does not sync the
|
|
60
|
+
* directory at all.
|
|
61
|
+
*/
|
|
36
62
|
async function syncDirectory(directory) {
|
|
37
63
|
let handle;
|
|
38
64
|
try {
|
|
39
65
|
handle = await open(directory, 'r');
|
|
40
66
|
await handle.sync();
|
|
41
67
|
}
|
|
68
|
+
catch {
|
|
69
|
+
// Unsupported on this platform; the rename already landed.
|
|
70
|
+
}
|
|
42
71
|
finally {
|
|
43
72
|
await handle?.close().catch(() => undefined);
|
|
44
73
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"atomic-write-file.js","sourceRoot":"","sources":["../../../src/tools/builtins/atomic-write-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAmB,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"atomic-write-file.js","sourceRoot":"","sources":["../../../src/tools/builtins/atomic-write-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAmB,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAC/F,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAOnD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACpC,aAAqB,EACrB,OAAe,EACf,QAA0B,EAAE;IAE5B,wEAAwE;IACxE,wEAAwE;IACxE,sEAAsE;IACtE,yEAAyE;IACzE,qEAAqE;IACrE,iEAAiE;IACjE,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,aAAa,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,CAAA;IACzE,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IACnC,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAE3C,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC;SACvC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;SAC3B,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IACxB,MAAM,QAAQ,GAAG,IAAI,CACpB,SAAS,EACT,IAAI,QAAQ,CAAC,QAAQ,CAAC,UAAU,OAAO,CAAC,GAAG,IAAI,UAAU,EAAE,MAAM,CACjE,CAAA;IACD,IAAI,MAA8B,CAAA;IAClC,IAAI,SAAS,GAAG,KAAK,CAAA;IAErB,IAAI,CAAC;QACJ,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,YAAY,IAAI,KAAK,CAAC,CAAA;QAC1D,MAAM,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACxC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;QACnB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;QACpB,MAAM,GAAG,SAAS,CAAA;QAClB,MAAM,KAAK,CAAC,YAAY,EAAE,EAAE,CAAA;QAC5B,MAAM,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;QAChC,SAAS,GAAG,IAAI,CAAA;QAChB,MAAM,aAAa,CAAC,SAAS,CAAC,CAAA;IAC/B,CAAC;YAAS,CAAC;QACV,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;QAC5C,IAAI,CAAC,SAAS;YAAE,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IAC9D,CAAC;AACF,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,KAAK,UAAU,aAAa,CAAC,SAAiB;IAC7C,IAAI,MAA8B,CAAA;IAClC,IAAI,CAAC;QACJ,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;QACnC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;IACpB,CAAC;IAAC,MAAM,CAAC;QACR,2DAA2D;IAC5D,CAAC;YAAS,CAAC;QACV,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IAC7C,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A short, stable fingerprint of a file body.
|
|
3
|
+
*
|
|
4
|
+
* Used to answer one question at mutation time: is the file still what the
|
|
5
|
+
* agent read? An `edit` is computed against a remembered body, and between
|
|
6
|
+
* the read and the write that body can move — a person editing in an
|
|
7
|
+
* editor, another process, a second agent run. The in-process mutation lock
|
|
8
|
+
* serializes this runtime's own writers and is blind to every one of those.
|
|
9
|
+
*
|
|
10
|
+
* Without this the drift is not merely undetected, it is actively
|
|
11
|
+
* misreported: an `old_string` that no longer matches comes back as "not
|
|
12
|
+
* found in file — make sure the string matches exactly", which tells the
|
|
13
|
+
* agent its input was wrong when the file changed underneath it. The agent
|
|
14
|
+
* then retries the same edit against the same moved file.
|
|
15
|
+
*
|
|
16
|
+
* Truncated deliberately. This distinguishes two bodies; it is not a
|
|
17
|
+
* security boundary, and a full digest per read costs bytes in a structure
|
|
18
|
+
* that lives for the whole run.
|
|
19
|
+
*/
|
|
20
|
+
export declare function fingerprintContent(content: string): string;
|
|
21
|
+
/** What a mutation tool tells the agent when the file moved under it. */
|
|
22
|
+
export declare function staleFileError(path: string): string;
|
|
23
|
+
//# sourceMappingURL=content-fingerprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-fingerprint.d.ts","sourceRoot":"","sources":["../../../src/tools/builtins/content-fingerprint.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMnD"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* A short, stable fingerprint of a file body.
|
|
4
|
+
*
|
|
5
|
+
* Used to answer one question at mutation time: is the file still what the
|
|
6
|
+
* agent read? An `edit` is computed against a remembered body, and between
|
|
7
|
+
* the read and the write that body can move — a person editing in an
|
|
8
|
+
* editor, another process, a second agent run. The in-process mutation lock
|
|
9
|
+
* serializes this runtime's own writers and is blind to every one of those.
|
|
10
|
+
*
|
|
11
|
+
* Without this the drift is not merely undetected, it is actively
|
|
12
|
+
* misreported: an `old_string` that no longer matches comes back as "not
|
|
13
|
+
* found in file — make sure the string matches exactly", which tells the
|
|
14
|
+
* agent its input was wrong when the file changed underneath it. The agent
|
|
15
|
+
* then retries the same edit against the same moved file.
|
|
16
|
+
*
|
|
17
|
+
* Truncated deliberately. This distinguishes two bodies; it is not a
|
|
18
|
+
* security boundary, and a full digest per read costs bytes in a structure
|
|
19
|
+
* that lives for the whole run.
|
|
20
|
+
*/
|
|
21
|
+
export function fingerprintContent(content) {
|
|
22
|
+
return createHash('sha256').update(content, 'utf-8').digest('hex').slice(0, 16);
|
|
23
|
+
}
|
|
24
|
+
/** What a mutation tool tells the agent when the file moved under it. */
|
|
25
|
+
export function staleFileError(path) {
|
|
26
|
+
return (`${path} changed on disk after you read it, so this edit was computed against a stale copy ` +
|
|
27
|
+
'and was not applied. Read the file again and redo the edit against its current contents. ' +
|
|
28
|
+
'Nothing was written.');
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=content-fingerprint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-fingerprint.js","sourceRoot":"","sources":["../../../src/tools/builtins/content-fingerprint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe;IACjD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AAChF,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,cAAc,CAAC,IAAY;IAC1C,OAAO,CACN,GAAG,IAAI,qFAAqF;QAC5F,2FAA2F;QAC3F,sBAAsB,CACtB,CAAA;AACF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit.d.ts","sourceRoot":"","sources":["../../../src/tools/builtins/edit.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"edit.d.ts","sourceRoot":"","sources":["../../../src/tools/builtins/edit.ts"],"names":[],"mappings":"AA4HA,eAAO,MAAM,QAAQ;;;;;;;;EAiHnB,CAAA"}
|
|
@@ -1,16 +1,37 @@
|
|
|
1
|
-
import { readFile
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { defineTool } from '../defineTool.js';
|
|
5
|
+
import { atomicWriteFile } from './atomic-write-file.js';
|
|
6
|
+
import { fingerprintContent, staleFileError } from './content-fingerprint.js';
|
|
7
|
+
import { withFileMutationLock } from './file-mutation-lock.js';
|
|
8
|
+
/**
|
|
9
|
+
* Two schemas, on purpose.
|
|
10
|
+
*
|
|
11
|
+
* `inputSchema` is what a HOST may send, and it accepts the `oldStr`/`newStr`
|
|
12
|
+
* aliases and `insertLine` because hosts that expose replacement under those
|
|
13
|
+
* names are real. `modelInputSchema` below is what a MODEL is constrained to
|
|
14
|
+
* — one closed canonical shape — because giving a model four spellings of the
|
|
15
|
+
* same field is how it learns to guess between them.
|
|
16
|
+
*
|
|
17
|
+
* `.strict()` is what makes the accepted set closed. Without it zod's default
|
|
18
|
+
* is to STRIP an unknown key, so a hallucinated or misspelled field is
|
|
19
|
+
* silently dropped and the edit proceeds against an input nobody wrote.
|
|
20
|
+
*/
|
|
5
21
|
const inputSchema = z
|
|
6
22
|
.object({
|
|
7
|
-
path: z
|
|
23
|
+
path: z
|
|
24
|
+
.string()
|
|
25
|
+
.refine((value) => value.trim().length > 0, 'Path must not be empty.')
|
|
26
|
+
.describe('Path to the file to edit. Must not be empty.'),
|
|
8
27
|
old_string: z
|
|
9
28
|
.string()
|
|
29
|
+
.min(1)
|
|
10
30
|
.optional()
|
|
11
31
|
.describe('The exact string to find and replace. Must be unique in the file.'),
|
|
12
32
|
oldStr: z
|
|
13
33
|
.string()
|
|
34
|
+
.min(1)
|
|
14
35
|
.optional()
|
|
15
36
|
.describe('Alias for old_string. Used by hosts that expose text replacement as oldStr/newStr.'),
|
|
16
37
|
new_string: z
|
|
@@ -30,23 +51,69 @@ const inputSchema = z
|
|
|
30
51
|
.default(false)
|
|
31
52
|
.describe('Replace all occurrences instead of just the first unique match'),
|
|
32
53
|
})
|
|
54
|
+
.strict()
|
|
33
55
|
.refine((value) => typeof value.new_string === 'string' || typeof value.newStr === 'string', {
|
|
34
56
|
message: 'Either new_string or newStr is required.',
|
|
35
57
|
})
|
|
36
58
|
.refine((value) => value.insertLine !== undefined ||
|
|
37
59
|
typeof value.old_string === 'string' ||
|
|
38
60
|
typeof value.oldStr === 'string', { message: 'Either old_string/oldStr or insertLine is required.' });
|
|
61
|
+
/**
|
|
62
|
+
* What a capable provider constrains the model to emit: one shape, closed.
|
|
63
|
+
*
|
|
64
|
+
* The aliases above exist for hosts, not for models. A model offered
|
|
65
|
+
* `old_string` and `oldStr` as separate optional fields has to guess which
|
|
66
|
+
* one this deployment wants, and `additionalProperties: false` is what turns
|
|
67
|
+
* an invented field into a generation-time refusal rather than a silent drop.
|
|
68
|
+
*/
|
|
69
|
+
const modelInputSchema = {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
path: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'Path to the file to edit. Must not be empty.',
|
|
75
|
+
},
|
|
76
|
+
old_string: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'Exact unique text from the file, without read-tool line-number prefixes. Must not be empty.',
|
|
79
|
+
},
|
|
80
|
+
new_string: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
description: 'Exact replacement text. May be empty to delete old_string. Keep under 12000 characters.',
|
|
83
|
+
},
|
|
84
|
+
replace_all: {
|
|
85
|
+
type: 'boolean',
|
|
86
|
+
description: 'Replace every occurrence instead of requiring one unique match.',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
required: ['path', 'old_string', 'new_string'],
|
|
90
|
+
additionalProperties: false,
|
|
91
|
+
};
|
|
39
92
|
export const EditTool = defineTool({
|
|
40
93
|
name: 'edit',
|
|
41
94
|
description: 'Makes targeted edits to a file using exact string find-and-replace or line insertion. THIS IS THE PREFERRED WAY TO MODIFY AN EXISTING FILE — never reach for `write` to change a file that already exists, because `write` overwrites the whole body and discards earlier work on partial failure. `edit` keeps the rest of the file byte-for-byte intact and is recoverable: if a single edit fails (old_string/oldStr ambiguous, broader restructuring needed), follow up with another `edit` instead of re-emitting the entire file via `write`. The old_string/oldStr must be unique in the file unless replace_all is true. For insertions, pass insertLine plus new_string/newStr; use insertLine: "end" to extend a file at the end. Self-budget new_string/newStr under 12000 characters before emitting the tool call; use repeated bounded edits for long sections. Preserves file formatting and indentation.',
|
|
42
95
|
inputSchema,
|
|
96
|
+
modelInputSchema,
|
|
97
|
+
enforceModelInput: true,
|
|
98
|
+
validationErrorHint: 'Required shape: {"path":"file.md","old_string":"exact unique text","new_string":"replacement text"}. Optional: "replace_all": true.',
|
|
43
99
|
category: 'filesystem',
|
|
44
100
|
permissions: ['file_write'],
|
|
45
101
|
readOnly: false,
|
|
46
102
|
destructive: false,
|
|
47
103
|
concurrencySafe: false,
|
|
48
104
|
async execute(input, context) {
|
|
49
|
-
|
|
105
|
+
// Re-validated here rather than trusted from the registry: `execute` is
|
|
106
|
+
// reachable directly, and the closed contract is only closed if the
|
|
107
|
+
// check runs on the path a caller can actually take.
|
|
108
|
+
const parsed = inputSchema.safeParse(input);
|
|
109
|
+
if (!parsed.success) {
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
output: '',
|
|
113
|
+
error: `Invalid edit input: ${parsed.error.issues.map((issue) => issue.message).join('; ')}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const normalized = normalizeEditInput(parsed.data);
|
|
50
117
|
if (!normalized.success) {
|
|
51
118
|
return { success: false, output: '', error: normalized.error };
|
|
52
119
|
}
|
|
@@ -58,33 +125,70 @@ export const EditTool = defineTool({
|
|
|
58
125
|
error: 'old_string/oldStr and new_string/newStr are identical — no change needed',
|
|
59
126
|
};
|
|
60
127
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
128
|
+
const filePath = resolve(context.workingDirectory, parsed.data.path);
|
|
129
|
+
// Read-modify-write is not atomic on its own: two edits to the same
|
|
130
|
+
// path interleave their reads, and the second write lands on content
|
|
131
|
+
// the first had already replaced — so one edit vanishes and the loser
|
|
132
|
+
// reports "old_string not found", blaming the model for a race. The
|
|
133
|
+
// key spans both branches because sandbox and local are distinct
|
|
134
|
+
// files even when the path string matches.
|
|
135
|
+
const lockKey = `${context.sandbox ? 'sandbox' : 'local'}:${filePath}`;
|
|
136
|
+
return withFileMutationLock(lockKey, async () => {
|
|
137
|
+
if (context.sandbox) {
|
|
138
|
+
const buffer = await context.sandbox.readFile(parsed.data.path);
|
|
139
|
+
const result = applyEdit(buffer.toString('utf-8'), normalized.operation);
|
|
140
|
+
if (!result.success) {
|
|
141
|
+
return { success: false, output: '', error: result.error };
|
|
142
|
+
}
|
|
143
|
+
await context.sandbox.writeFile(parsed.data.path, result.content);
|
|
144
|
+
return {
|
|
145
|
+
success: true,
|
|
146
|
+
output: `Edited ${parsed.data.path}: ${result.replacements} replacement(s) [sandboxed]`,
|
|
147
|
+
data: { path: parsed.data.path, replacements: result.replacements, sandboxed: true },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const content = await readFile(filePath, 'utf-8');
|
|
65
151
|
const result = applyEdit(content, normalized.operation);
|
|
66
152
|
if (!result.success) {
|
|
153
|
+
// The anchor did not match what is on disk. Two very different
|
|
154
|
+
// situations produce that, and the difference is the whole
|
|
155
|
+
// value of the fingerprint:
|
|
156
|
+
//
|
|
157
|
+
// The file is as the agent left it, and the anchor is simply
|
|
158
|
+
// wrong — "not found, match the whitespace exactly" is the
|
|
159
|
+
// right thing to say.
|
|
160
|
+
//
|
|
161
|
+
// Or the file moved after the read — a person in an editor,
|
|
162
|
+
// another process, a second agent — and the same message is a
|
|
163
|
+
// lie that sends the agent to re-check text that was never the
|
|
164
|
+
// problem, so it retries the identical edit against the same
|
|
165
|
+
// moved file.
|
|
166
|
+
//
|
|
167
|
+
// Deliberately NOT a gate on the successful path. An anchor
|
|
168
|
+
// that still matches uniquely is well defined however much
|
|
169
|
+
// changed elsewhere in the file, and refusing there would
|
|
170
|
+
// reject safe edits every time anyone touched an unrelated
|
|
171
|
+
// line.
|
|
172
|
+
const seen = context.fileReadTracker?.fingerprint?.(filePath);
|
|
173
|
+
if (seen !== undefined && seen !== fingerprintContent(content)) {
|
|
174
|
+
return { success: false, output: '', error: staleFileError(filePath) };
|
|
175
|
+
}
|
|
67
176
|
return { success: false, output: '', error: result.error };
|
|
68
177
|
}
|
|
69
|
-
|
|
178
|
+
// Temp file, fsync, rename — a reader sees the old body or the new
|
|
179
|
+
// one, never a half-written one. A plain `writeFile` that fails
|
|
180
|
+
// partway leaves the user's source truncated.
|
|
181
|
+
await atomicWriteFile(filePath, result.content);
|
|
182
|
+
// This runtime is now the last writer, so the next edit in the same
|
|
183
|
+
// turn compares against what we just wrote rather than the read
|
|
184
|
+
// before it.
|
|
185
|
+
context.fileReadTracker?.recordRead(filePath, result.content);
|
|
70
186
|
return {
|
|
71
187
|
success: true,
|
|
72
|
-
output: `Edited ${
|
|
73
|
-
data: { path:
|
|
188
|
+
output: `Edited ${filePath}: ${result.replacements} replacement(s)`,
|
|
189
|
+
data: { path: filePath, replacements: result.replacements },
|
|
74
190
|
};
|
|
75
|
-
}
|
|
76
|
-
const filePath = resolve(context.workingDirectory, input.path);
|
|
77
|
-
const content = await readFile(filePath, 'utf-8');
|
|
78
|
-
const result = applyEdit(content, normalized.operation);
|
|
79
|
-
if (!result.success) {
|
|
80
|
-
return { success: false, output: '', error: result.error };
|
|
81
|
-
}
|
|
82
|
-
await writeFile(filePath, result.content, 'utf-8');
|
|
83
|
-
return {
|
|
84
|
-
success: true,
|
|
85
|
-
output: `Edited ${filePath}: ${result.replacements} replacement(s)`,
|
|
86
|
-
data: { path: filePath, replacements: result.replacements },
|
|
87
|
-
};
|
|
191
|
+
});
|
|
88
192
|
},
|
|
89
193
|
});
|
|
90
194
|
function normalizeEditInput(input) {
|
|
@@ -138,24 +242,25 @@ function applyEdit(content, input) {
|
|
|
138
242
|
if (input.operation === 'insert') {
|
|
139
243
|
return applyLineInsert(content, input);
|
|
140
244
|
}
|
|
141
|
-
|
|
245
|
+
const replacement = normalizeLineEndings(content, input);
|
|
246
|
+
if (!content.includes(replacement.oldString)) {
|
|
142
247
|
return {
|
|
143
248
|
success: false,
|
|
144
249
|
error: 'old_string/oldStr not found in file. Make sure the string matches exactly, including whitespace and indentation.',
|
|
145
250
|
};
|
|
146
251
|
}
|
|
147
|
-
if (
|
|
148
|
-
const parts = content.split(
|
|
252
|
+
if (replacement.replace_all) {
|
|
253
|
+
const parts = content.split(replacement.oldString);
|
|
149
254
|
const replacements = parts.length - 1;
|
|
150
255
|
return {
|
|
151
256
|
success: true,
|
|
152
|
-
content: parts.join(
|
|
257
|
+
content: parts.join(replacement.newString),
|
|
153
258
|
replacements,
|
|
154
259
|
};
|
|
155
260
|
}
|
|
156
261
|
// Uniqueness check: old_string/oldStr must appear exactly once
|
|
157
|
-
const firstIndex = content.indexOf(
|
|
158
|
-
const secondIndex = content.indexOf(
|
|
262
|
+
const firstIndex = content.indexOf(replacement.oldString);
|
|
263
|
+
const secondIndex = content.indexOf(replacement.oldString, firstIndex + 1);
|
|
159
264
|
if (secondIndex !== -1) {
|
|
160
265
|
const lineNumber = content.slice(0, firstIndex).split('\n').length;
|
|
161
266
|
const secondLine = content.slice(0, secondIndex).split('\n').length;
|
|
@@ -167,8 +272,8 @@ function applyEdit(content, input) {
|
|
|
167
272
|
return {
|
|
168
273
|
success: true,
|
|
169
274
|
content: content.slice(0, firstIndex) +
|
|
170
|
-
|
|
171
|
-
content.slice(firstIndex +
|
|
275
|
+
replacement.newString +
|
|
276
|
+
content.slice(firstIndex + replacement.oldString.length),
|
|
172
277
|
replacements: 1,
|
|
173
278
|
};
|
|
174
279
|
}
|
|
@@ -190,4 +295,40 @@ function applyLineInsert(content, input) {
|
|
|
190
295
|
replacements: 1,
|
|
191
296
|
};
|
|
192
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Reconcile the caller's line endings with the file's.
|
|
300
|
+
*
|
|
301
|
+
* A model reading a CRLF file and writing back LF (or the reverse) produces
|
|
302
|
+
* an `old_string` that is correct in every visible way and matches nothing.
|
|
303
|
+
* The failure reads as "your text is wrong" when the text is right and only
|
|
304
|
+
* the invisible half of each line break differs.
|
|
305
|
+
*
|
|
306
|
+
* Only applied when the file is CONSISTENT. A mixed-ending file has no
|
|
307
|
+
* single right answer, and rewriting boundaries there would corrupt the
|
|
308
|
+
* half that was already correct.
|
|
309
|
+
*/
|
|
310
|
+
function normalizeLineEndings(content, input) {
|
|
311
|
+
const withoutCrlf = content.replaceAll('\r\n', '');
|
|
312
|
+
const usesOnlyCrlf = content.includes('\r\n') && !withoutCrlf.includes('\n');
|
|
313
|
+
if (usesOnlyCrlf) {
|
|
314
|
+
return {
|
|
315
|
+
...input,
|
|
316
|
+
oldString: content.includes(input.oldString)
|
|
317
|
+
? input.oldString
|
|
318
|
+
: input.oldString.replaceAll('\r\n', '\n').replaceAll('\n', '\r\n'),
|
|
319
|
+
newString: input.newString.replaceAll('\r\n', '\n').replaceAll('\n', '\r\n'),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const usesOnlyLf = content.includes('\n') && !content.includes('\r\n');
|
|
323
|
+
if (usesOnlyLf) {
|
|
324
|
+
return {
|
|
325
|
+
...input,
|
|
326
|
+
oldString: content.includes(input.oldString)
|
|
327
|
+
? input.oldString
|
|
328
|
+
: input.oldString.replaceAll('\r\n', '\n'),
|
|
329
|
+
newString: input.newString.replaceAll('\r\n', '\n'),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
return input;
|
|
333
|
+
}
|
|
193
334
|
//# sourceMappingURL=edit.js.map
|