@zq-silk/yui 2.1.0 → 2.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/README.md +4 -0
- package/dist/cli/commandCatalog.js +24 -3
- package/dist/cli/commandDiscovery.js +4 -1
- package/dist/cli.js +23 -3078
- package/dist/commands/taskCommands.js +79 -21
- package/dist/commands/taskIntegrationCommands.js +3 -1
- package/dist/commands/taskUpstreamCommands.js +3 -1
- package/dist/context/runContextPack.js +132 -5
- package/dist/context/sourceRunContext.js +4 -2
- package/dist/context/taskContext.js +6 -1
- package/dist/controlPlaneCli.js +3092 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +19 -6
- package/dist/controller/jobControl.js +21 -0
- package/dist/executor/agentExecutor.js +1 -1
- package/dist/executor/effectiveLaunch.js +18 -5
- package/dist/executor/fileRoleLaunchPlanner.js +15 -19
- package/dist/integration/gitIntegrationService.js +50 -15
- package/dist/message/messageContinuation.js +7 -2
- package/dist/nativeAgent/agent.js +176 -74
- package/dist/nativeAgent/cliDemo.js +37 -0
- package/dist/nativeAgent/codingTools.js +11 -0
- package/dist/nativeAgent/commandTool.js +215 -0
- package/dist/nativeAgent/compactionDemo.js +158 -0
- package/dist/nativeAgent/composition.js +71 -0
- package/dist/nativeAgent/context/budget.js +44 -0
- package/dist/nativeAgent/context/index.js +339 -0
- package/dist/nativeAgent/context/providerCompressor.js +103 -0
- package/dist/nativeAgent/demo.js +12 -1
- package/dist/nativeAgent/evaluation/cases.js +38 -0
- package/dist/nativeAgent/evaluation/checks.js +91 -0
- package/dist/nativeAgent/evaluation/demo.js +19 -0
- package/dist/nativeAgent/evaluation/files.js +54 -0
- package/dist/nativeAgent/evaluation/fixture.js +36 -0
- package/dist/nativeAgent/evaluation/index.js +239 -0
- package/dist/nativeAgent/executionOwner.js +209 -0
- package/dist/nativeAgent/filePatterns.js +170 -0
- package/dist/nativeAgent/fileToolsSupport.js +202 -0
- package/dist/nativeAgent/index.js +13 -0
- package/dist/nativeAgent/interaction/cli.js +358 -0
- package/dist/nativeAgent/interaction/contracts.js +1 -0
- package/dist/nativeAgent/interaction/index.js +3 -0
- package/dist/nativeAgent/interaction/memoryDemo.js +115 -0
- package/dist/nativeAgent/interaction/renderer.js +34 -0
- package/dist/nativeAgent/localSafety.js +258 -0
- package/dist/nativeAgent/model/anthropicMessages.js +204 -0
- package/dist/nativeAgent/model/chatCompletions.js +210 -0
- package/dist/nativeAgent/model/errors.js +47 -0
- package/dist/nativeAgent/model/gateway.js +423 -0
- package/dist/nativeAgent/model/index.js +7 -0
- package/dist/nativeAgent/model/observationAdapter.js +21 -0
- package/dist/nativeAgent/model/protocols.js +19 -0
- package/dist/nativeAgent/model/responses.js +263 -0
- package/dist/nativeAgent/model/types.js +1 -0
- package/dist/nativeAgent/model/wire.js +73 -0
- package/dist/nativeAgent/observability/index.js +220 -0
- package/dist/nativeAgent/product/catalog.js +82 -0
- package/dist/nativeAgent/product/config.js +295 -0
- package/dist/nativeAgent/product/facts.js +30 -0
- package/dist/nativeAgent/product/index.js +62 -0
- package/dist/nativeAgent/product/location.js +44 -0
- package/dist/nativeAgent/product/runtime.js +276 -0
- package/dist/nativeAgent/product/storage.js +49 -0
- package/dist/nativeAgent/product/tools.js +47 -0
- package/dist/nativeAgent/product/transport.js +54 -0
- package/dist/nativeAgent/projectGuidance/index.js +425 -0
- package/dist/nativeAgent/searchTools.js +305 -0
- package/dist/nativeAgent/session/backends.js +293 -0
- package/dist/nativeAgent/session/catalog.js +97 -0
- package/dist/nativeAgent/session/catalogDemo.js +87 -0
- package/dist/nativeAgent/session/contracts.js +1 -0
- package/dist/nativeAgent/session/format.js +269 -0
- package/dist/nativeAgent/session/index.js +5 -0
- package/dist/nativeAgent/session/location.js +36 -0
- package/dist/nativeAgent/session/sqliteFormat.js +134 -0
- package/dist/nativeAgent/session/store.js +248 -0
- package/dist/nativeAgent/textTools.js +270 -133
- package/dist/nativeAgent/toolManager/executor.js +290 -0
- package/dist/nativeAgent/toolManager/index.js +4 -0
- package/dist/nativeAgent/validation.js +2 -2
- package/dist/task/taskAuthority.js +56 -0
- package/dist/web/assets/client/app.js +64 -3
- package/dist/web/assets/client/detail.js +6 -6
- package/dist/web/assets/client/i18n.js +4 -0
- package/dist/web/assets/client/overview.js +12 -9
- package/dist/web/assets/client/sidebar.js +31 -2
- package/dist/web/assets/shell.js +2 -1
- package/dist/web/assets/styles/components.js +2 -0
- package/dist/web/assets/styles/layout.js +11 -4
- package/dist/web/assets/styles/responsive.js +12 -4
- package/dist/web/assets/styles/views.js +16 -13
- package/docs/agent-result-consumption.md +16 -0
- package/docs/agent-result-consumption.zh-CN.md +13 -0
- package/docs/examples/agent-offline.mjs +194 -0
- package/docs/native-agent.md +283 -0
- package/docs/release-workflow.md +47 -9
- package/docs/release-workflow.zh-CN.md +36 -6
- package/docs/roles-and-configuration.md +32 -0
- package/docs/roles-and-configuration.zh-CN.md +26 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +9 -0
- package/skills/yui-reviewer/SKILL.md +5 -0
- package/skills/yui-runtime/SKILL.md +7 -0
|
@@ -1,156 +1,293 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { lstat, open, rename, unlink } from 'node:fs/promises';
|
|
1
|
+
import { open, rename, unlink } from 'node:fs/promises';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
|
|
6
|
-
code;
|
|
7
|
-
constructor(code, message) {
|
|
8
|
-
super(message);
|
|
9
|
-
this.code = code;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
4
|
+
import { FileFault, bounded, workspaceRoot, validPath, checkPath, readSnapshot, digest, fits, cursor, readCursor, replace, diff, assertText } from './fileToolsSupport.js';
|
|
12
5
|
const fail = (code, message) => ({ code, message, effect: 'none' });
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
return fail('too_large', '
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
6
|
+
const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
/** Controlled directories only. Queues are same-pack, not cross-process CAS or a multi-file transaction. */
|
|
8
|
+
export function createTextTools(options) {
|
|
9
|
+
const root = workspaceRoot(options.root);
|
|
10
|
+
const maxBytes = bounded(options.maxBytes, 8 * 1024 * 1024, 'maxBytes');
|
|
11
|
+
const maxOutputBytes = bounded(options.maxOutputBytes, 64 * 1024, 'maxOutputBytes');
|
|
12
|
+
const writers = new Map();
|
|
13
|
+
const targetsFor = (args, name) => name === 'patch'
|
|
14
|
+
? args.files : name === 'edit' ? [{
|
|
15
|
+
path: args.path, expectedSha256: args.expectedSha256,
|
|
16
|
+
edits: [{ oldText: args.oldText, newText: args.newText }],
|
|
17
|
+
}] : [args];
|
|
18
|
+
const validate = (args, name) => {
|
|
19
|
+
if (!object(args))
|
|
20
|
+
return fail('invalid_arguments', 'Expected an argument object');
|
|
21
|
+
const keys = name === 'patch' ? ['files'] : name === 'read' ? ['path', 'startLine', 'limit', 'cursor']
|
|
22
|
+
: name === 'edit' ? ['path', 'expectedSha256', 'oldText', 'newText'] : ['path', 'content', 'expectedSha256'];
|
|
23
|
+
if (Object.keys(args).some(key => !keys.includes(key)))
|
|
24
|
+
return fail('invalid_arguments', 'Unexpected tool fields');
|
|
25
|
+
if (Buffer.byteLength(JSON.stringify(args)) > 64 * 1024)
|
|
26
|
+
return fail('too_large', 'Arguments exceed 64 KiB');
|
|
27
|
+
if (name === 'read') {
|
|
28
|
+
if (!validPath(args.path))
|
|
29
|
+
return fail(typeof args.path === 'string' && (path.isAbsolute(args.path)
|
|
30
|
+
|| args.path.split(/[\\/]/).includes('..')) ? 'path_out_of_scope' : 'invalid_arguments', 'Expected a bounded relative file path');
|
|
31
|
+
if (['startLine', 'limit'].some(key => args[key] !== undefined && (typeof args[key] !== 'number'
|
|
32
|
+
|| !Number.isSafeInteger(args[key]) || args[key] < 1 || args[key] > 1_000_000))
|
|
33
|
+
|| (args.cursor !== undefined && (typeof args.cursor !== 'string' || !args.cursor || args.cursor.length > 2048))) {
|
|
34
|
+
return fail('invalid_arguments', 'Expected positive startLine/limit and an optional cursor');
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
41
37
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
if (name === 'patch' && (!Array.isArray(args.files) || !args.files.length || args.files.length > 16
|
|
39
|
+
|| !args.files.every(object)))
|
|
40
|
+
return fail('invalid_arguments', 'patch expects 1..16 existing files');
|
|
41
|
+
const targets = targetsFor(args, name);
|
|
42
|
+
for (const target of targets) {
|
|
43
|
+
if (!validPath(target.path))
|
|
44
|
+
return fail(typeof target.path === 'string' && (path.isAbsolute(target.path)
|
|
45
|
+
|| target.path.split(/[\\/]/).includes('..')) ? 'path_out_of_scope' : 'invalid_arguments', 'Expected bounded relative file paths');
|
|
46
|
+
if (name === 'patch' && Object.keys(target).some(key => !['path', 'expectedSha256', 'edits'].includes(key))) {
|
|
47
|
+
return fail('invalid_arguments', 'Unexpected patch file fields');
|
|
50
48
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
if (target.expectedSha256 !== undefined && (typeof target.expectedSha256 !== 'string'
|
|
50
|
+
|| !/^[a-f0-9]{64}$/.test(target.expectedSha256)))
|
|
51
|
+
return fail('invalid_arguments', 'Expected lowercase SHA-256');
|
|
52
|
+
if (name === 'write') {
|
|
53
|
+
if (typeof target.content !== 'string')
|
|
54
|
+
return fail('invalid_arguments', 'Expected content');
|
|
55
|
+
if (Buffer.byteLength(target.content) > maxBytes)
|
|
56
|
+
return fail('too_large', 'Text exceeds byte limit');
|
|
55
57
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
else {
|
|
59
|
+
if (typeof target.expectedSha256 !== 'string' || !Array.isArray(target.edits) || !target.edits.length
|
|
60
|
+
|| target.edits.length > 32 || target.edits.some(edit => !object(edit)
|
|
61
|
+
|| Object.keys(edit).some(key => !['oldText', 'newText'].includes(key))
|
|
62
|
+
|| typeof edit.oldText !== 'string' || !edit.oldText || typeof edit.newText !== 'string')) {
|
|
63
|
+
return fail('invalid_arguments', 'Expected 1..32 exact edits and a SHA-256 precondition');
|
|
64
|
+
}
|
|
65
|
+
if (target.edits.some(edit => [edit.oldText, edit.newText].some(text => Buffer.byteLength(text) > maxBytes))) {
|
|
66
|
+
return fail('too_large', 'Text exceeds byte limit');
|
|
67
|
+
}
|
|
60
68
|
}
|
|
61
69
|
}
|
|
62
|
-
return
|
|
70
|
+
return null;
|
|
63
71
|
};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
finally {
|
|
115
|
-
await file.close();
|
|
72
|
+
const editSchema = { type: 'object', properties: { oldText: { type: 'string' }, newText: { type: 'string' } },
|
|
73
|
+
required: ['oldText', 'newText'], additionalProperties: false };
|
|
74
|
+
return ['read', 'write', 'edit', 'patch'].map(name => ({
|
|
75
|
+
definition: {
|
|
76
|
+
name,
|
|
77
|
+
description: name === 'read' ? 'Read UTF-8 pages with full-file SHA-256, exact byte offsets and request-bound cursor'
|
|
78
|
+
: name === 'patch' ? 'Unique nonoverlapping original-text edits to existing files; preflight all, sequential commits, bounded actual diffs; partial failures stop'
|
|
79
|
+
: name === 'edit' ? 'Replace one unique literal match with expected SHA-256 and actual diff'
|
|
80
|
+
: 'Create or replace UTF-8 text with SHA-256 precondition and actual diff',
|
|
81
|
+
inputSchema: { type: 'object', properties: name === 'patch' ? { files: { type: 'array', minItems: 1, maxItems: 16,
|
|
82
|
+
items: { type: 'object', properties: { path: { type: 'string' }, expectedSha256: { type: 'string' },
|
|
83
|
+
edits: { type: 'array', minItems: 1, maxItems: 32, items: editSchema } },
|
|
84
|
+
required: ['path', 'expectedSha256', 'edits'], additionalProperties: false } } }
|
|
85
|
+
: { path: { type: 'string' }, ...(name === 'read' ? { startLine: { type: 'integer' },
|
|
86
|
+
limit: { type: 'integer' }, cursor: { type: 'string' } } : { expectedSha256: { type: 'string' },
|
|
87
|
+
...(name === 'edit' ? editSchema.properties : { content: { type: 'string' } }) }) },
|
|
88
|
+
required: name === 'patch' ? ['files'] : name === 'read' ? ['path'] : name === 'edit'
|
|
89
|
+
? ['path', 'expectedSha256', 'oldText', 'newText'] : ['path', 'content'], additionalProperties: false },
|
|
90
|
+
},
|
|
91
|
+
validate: args => validate(args, name),
|
|
92
|
+
async execute(args, scope, signal) {
|
|
93
|
+
const invalid = validate(args, name);
|
|
94
|
+
if (invalid)
|
|
95
|
+
return { ok: false, error: invalid };
|
|
96
|
+
const outcome = (value) => ({ ok: true, content: JSON.stringify(value) });
|
|
97
|
+
const fit = (result) => fits(result, maxOutputBytes, name, scope.toolCallId);
|
|
98
|
+
const readText = async (target) => {
|
|
99
|
+
const snapshot = await readSnapshot(target, maxBytes, signal);
|
|
100
|
+
assertText(snapshot.text);
|
|
101
|
+
return snapshot;
|
|
102
|
+
};
|
|
103
|
+
const held = [];
|
|
104
|
+
const receipts = [];
|
|
105
|
+
let temporary;
|
|
106
|
+
let committing = false;
|
|
107
|
+
let index = 0;
|
|
108
|
+
try {
|
|
109
|
+
signal.throwIfAborted();
|
|
110
|
+
if (name === 'read') {
|
|
111
|
+
const request = args;
|
|
112
|
+
const snapshot = await readText(await checkPath(root, request.path));
|
|
113
|
+
const binding = digest(JSON.stringify([request.path, request.startLine ?? 1, request.limit ?? 200, maxBytes, maxOutputBytes]));
|
|
114
|
+
const fingerprint = snapshot.sha256 + ':' + snapshot.identity;
|
|
115
|
+
let start = readCursor(request.cursor, binding, fingerprint, snapshot.text.length);
|
|
116
|
+
if (!request.cursor) {
|
|
117
|
+
for (let line = 1; line < (request.startLine ?? 1); line++) {
|
|
118
|
+
const offset = snapshot.text.indexOf('\n', start);
|
|
119
|
+
if (offset < 0)
|
|
120
|
+
throw new FileFault('line_out_of_range', 'startLine is beyond EOF');
|
|
121
|
+
start = offset + 1;
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
if (start > 0 && /[\uDC00-\uDFFF]/.test(snapshot.text[start] ?? ''))
|
|
125
|
+
throw new FileFault('invalid_cursor', 'Cursor splits a code point');
|
|
126
|
+
let end = start;
|
|
127
|
+
for (let count = 0; count < (request.limit ?? 200) && end < snapshot.text.length; count++) {
|
|
128
|
+
const offset = snapshot.text.indexOf('\n', end);
|
|
129
|
+
end = offset < 0 ? snapshot.text.length : offset + 1;
|
|
130
|
+
}
|
|
131
|
+
const requestedEnd = end;
|
|
132
|
+
const lineAt = (offset) => {
|
|
133
|
+
let line = 1;
|
|
134
|
+
for (let i = 0; i < offset; i++)
|
|
135
|
+
if (snapshot.text[i] === '\n')
|
|
136
|
+
line++;
|
|
137
|
+
return line;
|
|
138
|
+
};
|
|
139
|
+
const make = (stop) => outcome({ path: request.path, text: snapshot.text.slice(start, stop),
|
|
140
|
+
bytes: Buffer.byteLength(snapshot.text.slice(start, stop)), fileBytes: snapshot.bytes, sha256: snapshot.sha256,
|
|
141
|
+
byteStart: Buffer.byteLength(snapshot.text.slice(0, start)), byteEnd: Buffer.byteLength(snapshot.text.slice(0, stop)),
|
|
142
|
+
startLine: lineAt(start), endLine: lineAt(Math.max(start, stop - 1)),
|
|
143
|
+
complete: stop === snapshot.text.length, nextCursor: stop === snapshot.text.length ? null : cursor(binding, fingerprint, stop),
|
|
144
|
+
truncationReason: stop < requestedEnd ? 'output_bytes' : stop < snapshot.text.length ? 'line_limit' : null });
|
|
145
|
+
if (!fit(make(end))) {
|
|
146
|
+
let low = start;
|
|
147
|
+
let high = end;
|
|
148
|
+
while (low < high) {
|
|
149
|
+
let middle = Math.ceil((low + high) / 2);
|
|
150
|
+
if (/[\uDC00-\uDFFF]/.test(snapshot.text[middle] ?? ''))
|
|
151
|
+
middle--;
|
|
152
|
+
if (middle <= low)
|
|
153
|
+
break;
|
|
154
|
+
if (fit(make(middle)))
|
|
155
|
+
low = middle;
|
|
156
|
+
else
|
|
157
|
+
high = middle - 1;
|
|
127
158
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
temporary = undefined;
|
|
133
|
-
result = { ok: true, content: JSON.stringify({ path: relative, bytes: Buffer.byteLength(content) }) };
|
|
159
|
+
end = low;
|
|
160
|
+
}
|
|
161
|
+
if ((end === start && start < snapshot.text.length) || !fit(make(end))) {
|
|
162
|
+
throw new FileFault('output_limit', 'Budget cannot hold one code point and metadata');
|
|
134
163
|
}
|
|
164
|
+
return make(end);
|
|
135
165
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
const targets = targetsFor(args, name);
|
|
167
|
+
const keys = targets.map(target => path.resolve(root, target.path));
|
|
168
|
+
if (new Set(keys).size !== keys.length)
|
|
169
|
+
throw new FileFault('edit_conflict', 'Duplicate target paths');
|
|
170
|
+
for (const key of [...keys].sort()) {
|
|
171
|
+
const previous = writers.get(key);
|
|
172
|
+
let release;
|
|
173
|
+
const promise = new Promise(resolve => { release = resolve; });
|
|
174
|
+
writers.set(key, promise);
|
|
175
|
+
held.push({ key, promise, release });
|
|
176
|
+
await previous;
|
|
177
|
+
signal.throwIfAborted();
|
|
141
178
|
}
|
|
179
|
+
const snapshots = [];
|
|
180
|
+
const candidates = [];
|
|
181
|
+
const snapshot = async (target) => {
|
|
182
|
+
const absolute = await checkPath(root, target.path, name === 'write');
|
|
183
|
+
if (absolute === root)
|
|
184
|
+
throw new FileFault('not_regular_file', 'Expected a file');
|
|
185
|
+
try {
|
|
186
|
+
return await readText(absolute);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (name === 'write' && error.code === 'ENOENT')
|
|
190
|
+
return undefined;
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
let totalBytes = 0;
|
|
195
|
+
for (const target of targets) {
|
|
196
|
+
signal.throwIfAborted();
|
|
197
|
+
const before = await snapshot(target);
|
|
198
|
+
if (target.expectedSha256 === undefined ? before !== undefined : before?.sha256 !== target.expectedSha256) {
|
|
199
|
+
throw new FileFault('edit_conflict', 'File changed or exists without an expected SHA-256');
|
|
200
|
+
}
|
|
201
|
+
const edits = target.edits ? target.edits.map(edit => {
|
|
202
|
+
const start = before.text.indexOf(edit.oldText);
|
|
203
|
+
if (start < 0 || before.text.indexOf(edit.oldText, start + 1) >= 0) {
|
|
204
|
+
throw new FileFault('edit_conflict', 'oldText must match exactly once, including overlapping matches');
|
|
205
|
+
}
|
|
206
|
+
return { start, end: start + edit.oldText.length, text: edit.newText };
|
|
207
|
+
}).sort((a, b) => a.start - b.start) : [{ start: 0, end: before?.text.length ?? 0, text: target.content }];
|
|
208
|
+
if (edits.some((edit, i) => i > 0 && edit.start < edits[i - 1].end))
|
|
209
|
+
throw new FileFault('edit_conflict', 'Edits overlap in original text');
|
|
210
|
+
const next = replace(before?.text ?? '', edits);
|
|
211
|
+
assertText(next);
|
|
212
|
+
const bytes = Buffer.byteLength(next);
|
|
213
|
+
totalBytes += bytes + (before?.bytes ?? 0);
|
|
214
|
+
if (bytes > maxBytes || totalBytes > 32 * 1024 * 1024)
|
|
215
|
+
throw new FileFault('too_large', 'Mutation snapshot byte budget exceeded');
|
|
216
|
+
snapshots.push(before);
|
|
217
|
+
candidates.push(next);
|
|
218
|
+
receipts.push({ path: target.path, status: 'not_attempted', beforeSha256: before?.sha256 ?? null,
|
|
219
|
+
sha256: null, candidateSha256: digest(next), bytes,
|
|
220
|
+
diff: diff(target.path, before?.text ?? '', edits, next, before === undefined) });
|
|
221
|
+
}
|
|
222
|
+
const success = () => outcome(name === 'patch' ? { files: receipts, complete: true } : receipts[0]);
|
|
223
|
+
const cleanupPath = targets.reduce((longest, target) => {
|
|
224
|
+
const candidate = path.join(path.dirname(target.path), `.agent-${'0'.repeat(36)}.tmp`);
|
|
225
|
+
return Buffer.byteLength(JSON.stringify(JSON.stringify(candidate))) > Buffer.byteLength(JSON.stringify(JSON.stringify(longest)))
|
|
226
|
+
? candidate : longest;
|
|
227
|
+
}, '');
|
|
228
|
+
const reserved = receipts.map(receipt => ({ ...receipt, status: 'not_attempted', sha256: receipt.candidateSha256 }));
|
|
229
|
+
const partial = { ok: false, error: { code: 'cancelled_before_commit', effect: 'unknown',
|
|
230
|
+
message: JSON.stringify({ complete: false, files: reserved, failedIndex: targets.length, cause: 'cancelled_before_commit',
|
|
231
|
+
temporary: cleanupPath }) + ' '.repeat(256) } };
|
|
232
|
+
// Include the most expensive doubly encoded cleanup path BEFORE touching any file.
|
|
233
|
+
if (!fit(outcome(name === 'patch' ? { files: reserved, complete: true } : reserved[0])) || !fit(partial)) {
|
|
234
|
+
throw new FileFault('output_limit', 'Encoded diff/receipt exceeds budget; no writes attempted');
|
|
235
|
+
}
|
|
236
|
+
committing = true;
|
|
237
|
+
for (index = 0; index < targets.length; index++) {
|
|
238
|
+
const target = targets[index];
|
|
239
|
+
signal.throwIfAborted();
|
|
240
|
+
const absolute = await checkPath(root, target.path, name === 'write');
|
|
241
|
+
const candidate = path.join(path.dirname(absolute), `.agent-${randomUUID()}.tmp`);
|
|
242
|
+
const file = await open(candidate, 'wx', 0o600);
|
|
243
|
+
temporary = candidate;
|
|
244
|
+
try {
|
|
245
|
+
await file.writeFile(candidates[index], { encoding: 'utf8', signal });
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
await file.close();
|
|
249
|
+
}
|
|
250
|
+
const after = await snapshot(target);
|
|
251
|
+
if (snapshots[index]?.identity !== after?.identity || snapshots[index]?.sha256 !== after?.sha256) {
|
|
252
|
+
throw new FileFault('edit_conflict', 'File changed before commit; no replacement was made');
|
|
253
|
+
}
|
|
254
|
+
signal.throwIfAborted();
|
|
255
|
+
await rename(temporary, absolute);
|
|
256
|
+
temporary = undefined;
|
|
257
|
+
receipts[index].status = 'committed';
|
|
258
|
+
receipts[index].sha256 = receipts[index].candidateSha256;
|
|
259
|
+
}
|
|
260
|
+
return success();
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
const code = error instanceof FileFault ? error.code : signal.aborted ? 'cancelled_before_commit'
|
|
264
|
+
: error.code === 'ENOENT' ? 'not_found' : 'io_error';
|
|
265
|
+
let cleanupFailed = false;
|
|
142
266
|
if (temporary) {
|
|
143
267
|
try {
|
|
144
268
|
await unlink(temporary);
|
|
269
|
+
temporary = undefined;
|
|
145
270
|
}
|
|
146
271
|
catch {
|
|
147
|
-
|
|
148
|
-
message: `Owned temporary file remains: ${path.relative(workspace, temporary)}; targetCommitted=${committed}`,
|
|
149
|
-
effect: 'unknown' } };
|
|
272
|
+
cleanupFailed = true;
|
|
150
273
|
}
|
|
151
274
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
275
|
+
const committed = receipts.some(receipt => receipt.status === 'committed');
|
|
276
|
+
if (committing && index < receipts.length)
|
|
277
|
+
receipts[index].status = 'rejected';
|
|
278
|
+
if (committed || cleanupFailed)
|
|
279
|
+
return { ok: false, error: { code: cleanupFailed ? 'cleanup_failed' : code, effect: 'unknown',
|
|
280
|
+
message: JSON.stringify({ complete: false, files: receipts, failedIndex: index, cause: code,
|
|
281
|
+
temporary: temporary ? path.relative(root, temporary) : null }) } };
|
|
282
|
+
return { ok: false, error: fail(code, error instanceof FileFault ? error.message : `File operation failed (${code})`) };
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
for (const lock of held.reverse()) {
|
|
286
|
+
lock.release();
|
|
287
|
+
if (writers.get(lock.key) === lock.promise)
|
|
288
|
+
writers.delete(lock.key);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
}));
|
|
156
293
|
}
|