maka-agent 0.2.0-dev.30.20260912 → 0.2.0-dev.31.20260913
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/THIRD_PARTY_NOTICES.txt +392 -0
- package/dist/acp/maka-acp-agent.js +8 -2
- package/dist/acp/prompt-content.js +225 -0
- package/dist/acp/session-event-mapper.js +103 -0
- package/dist/acp/session-registry.js +654 -17
- package/dist/acp/stdio-server.js +10 -1
- package/dist/cli-core.js +1 -1
- package/dist/pi-transcript-tools.js +3 -2
- package/dist/pi-transcript.js +12 -2
- package/dist/runtime-host-session-channel.js +63 -9
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/artifacts.js +6 -0
- package/node_modules/@maka/core/dist/attachments.js +7 -2
- package/node_modules/@maka/core/dist/durable-tool-result-projection.js +3 -1
- package/node_modules/@maka/core/dist/events.js +21 -0
- package/node_modules/@maka/core/dist/goal.js +44 -2
- package/node_modules/@maka/core/dist/permission.js +0 -1
- package/node_modules/@maka/core/dist/runtime-boundary.js +73 -29
- package/node_modules/@maka/core/dist/runtime-event.js +6 -3
- package/node_modules/@maka/core/dist/runtime-handoff.js +10 -2
- package/node_modules/@maka/core/dist/runtime-logical-execution.js +39 -14
- package/node_modules/@maka/core/dist/tool-result-archive-evidence.js +3 -1
- package/node_modules/@maka/core/dist/tool-result-record-schema.js +2 -1
- package/node_modules/@maka/core/dist/unified-diff.js +33 -26
- package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +49 -178
- package/node_modules/@maka/runtime/dist/ai-sdk-message-projection.js +7 -2
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +15 -53
- package/node_modules/@maka/runtime/dist/bash-model-output.js +7 -1
- package/node_modules/@maka/runtime/dist/bash-tail-buffer.js +13 -52
- package/node_modules/@maka/runtime/dist/builtin-tools.js +59 -111
- package/node_modules/@maka/runtime/dist/child-fd-input.js +3 -2
- package/node_modules/@maka/runtime/dist/context-budget-policy.js +1 -11
- package/node_modules/@maka/runtime/dist/context-budget.js +12 -1
- package/node_modules/@maka/runtime/dist/conversation-copy.js +104 -9
- package/node_modules/@maka/runtime/dist/filesystem-executor.js +10 -6
- package/node_modules/@maka/runtime/dist/filesystem-worker/client.js +77 -19
- package/node_modules/@maka/runtime/dist/filesystem-worker/launch-spec.js +66 -55
- package/node_modules/@maka/runtime/dist/filesystem-worker/operations.js +34 -97
- package/node_modules/@maka/runtime/dist/filesystem-worker/protocol.js +25 -10
- package/node_modules/@maka/runtime/dist/filesystem-worker/worker-entry.js +2 -0
- package/node_modules/@maka/runtime/dist/glob-search.js +74 -0
- package/node_modules/@maka/runtime/dist/goal-continuation.js +78 -8
- package/node_modules/@maka/runtime/dist/goal-state.js +7 -0
- package/node_modules/@maka/runtime/dist/grep-search.js +155 -0
- package/node_modules/@maka/runtime/dist/ledger-tool-result-archive-reader.js +84 -94
- package/node_modules/@maka/runtime/dist/model-history.js +25 -3
- package/node_modules/@maka/runtime/dist/pipe-tail-collector.js +6 -9
- package/node_modules/@maka/runtime/dist/read-page.js +231 -0
- package/node_modules/@maka/runtime/dist/request-projection.js +2 -15
- package/node_modules/@maka/runtime/dist/ripgrep-executable.js +62 -0
- package/node_modules/@maka/runtime/dist/ripgrep-guidance.js +81 -0
- package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +187 -20
- package/node_modules/@maka/runtime/dist/runtime-read-model.js +2 -1
- package/node_modules/@maka/runtime/dist/sandbox/linux-profile-path.js +7 -1
- package/node_modules/@maka/runtime/dist/sandbox/linux-sandbox.js +20 -5
- package/node_modules/@maka/runtime/dist/session-manager.js +6 -6
- package/node_modules/@maka/runtime/dist/session-recap.js +9 -5
- package/node_modules/@maka/runtime/dist/shell-exec.js +7 -25
- package/node_modules/@maka/runtime/dist/shell-run-manager.js +1 -1
- package/node_modules/@maka/runtime/dist/shell-run-tool-result.js +1 -1
- package/node_modules/@maka/runtime/dist/shell-tools.js +6 -12
- package/node_modules/@maka/runtime/dist/tool-availability.js +0 -1
- package/node_modules/@maka/runtime/dist/tool-output.js +1 -1
- package/node_modules/@maka/runtime/dist/tool-result-archive-capability.js +24 -36
- package/node_modules/@maka/runtime/dist/tool-result-archive-resource.js +32 -527
- package/node_modules/@maka/runtime/dist/tool-result-archive-transition.js +17 -119
- package/node_modules/@maka/runtime/dist/tool-result-archive.js +7 -25
- package/node_modules/@maka/runtime/dist/tool-runtime.js +7 -5
- package/node_modules/@maka/runtime/dist/workers/filesystem-worker.js +6933 -3681
- package/node_modules/@maka/runtime/dist/workspace-executor.js +20 -24
- package/node_modules/@maka/runtime/package.json +2 -0
- package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +185 -191
- package/node_modules/@maka/runtime-host/dist/client/reconnecting-connection.js +3 -0
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +5 -1
- package/node_modules/@maka/runtime-host/dist/protocol/message.js +2 -2
- package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
- package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +2 -2
- package/node_modules/@maka/runtime-host/dist/protocol/turn.js +10 -2
- package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +38 -0
- package/node_modules/@maka/runtime-host/dist/server/execution-artifacts.js +2 -8
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +2 -0
- package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +81 -7
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +13 -13
- package/node_modules/@maka/runtime-host/dist/server/interaction-coordinator.js +65 -0
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +1 -0
- package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +5 -2
- package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +88 -72
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +21 -4
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +145 -1
- package/node_modules/@maka/storage/dist/agent-run-store.js +5 -2
- package/node_modules/@maka/storage/dist/execution-stores.js +2 -1
- package/node_modules/@maka/storage/dist/runtime-transcript-query.js +27 -14
- package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +69 -3
- package/node_modules/balanced-match/LICENSE.md +23 -0
- package/node_modules/balanced-match/README.md +57 -0
- package/node_modules/balanced-match/dist/commonjs/index.js +59 -0
- package/node_modules/balanced-match/dist/commonjs/package.json +3 -0
- package/node_modules/balanced-match/dist/esm/index.js +54 -0
- package/node_modules/balanced-match/dist/esm/package.json +3 -0
- package/node_modules/balanced-match/package.json +68 -0
- package/node_modules/brace-expansion/LICENSE +23 -0
- package/node_modules/brace-expansion/README.md +105 -0
- package/node_modules/brace-expansion/dist/commonjs/index.js +289 -0
- package/node_modules/brace-expansion/dist/commonjs/package.json +3 -0
- package/node_modules/brace-expansion/dist/esm/index.js +285 -0
- package/node_modules/brace-expansion/dist/esm/package.json +3 -0
- package/node_modules/brace-expansion/package.json +64 -0
- package/node_modules/glob/LICENSE.md +63 -0
- package/node_modules/glob/README.md +1203 -0
- package/node_modules/glob/dist/commonjs/glob.js +248 -0
- package/node_modules/glob/dist/commonjs/has-magic.js +27 -0
- package/node_modules/glob/dist/commonjs/ignore.js +119 -0
- package/node_modules/glob/dist/commonjs/index.js +68 -0
- package/node_modules/glob/dist/commonjs/index.min.js +4 -0
- package/node_modules/glob/dist/commonjs/package.json +3 -0
- package/node_modules/glob/dist/commonjs/pattern.js +223 -0
- package/node_modules/glob/dist/commonjs/processor.js +301 -0
- package/node_modules/glob/dist/commonjs/walker.js +387 -0
- package/node_modules/glob/dist/esm/glob.js +244 -0
- package/node_modules/glob/dist/esm/has-magic.js +23 -0
- package/node_modules/glob/dist/esm/ignore.js +115 -0
- package/node_modules/glob/dist/esm/index.js +55 -0
- package/node_modules/glob/dist/esm/index.min.js +4 -0
- package/node_modules/glob/dist/esm/package.json +3 -0
- package/node_modules/glob/dist/esm/pattern.js +219 -0
- package/node_modules/glob/dist/esm/processor.js +294 -0
- package/node_modules/glob/dist/esm/walker.js +381 -0
- package/node_modules/glob/package.json +98 -0
- package/node_modules/minimatch/LICENSE.md +55 -0
- package/node_modules/minimatch/README.md +528 -0
- package/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js +14 -0
- package/node_modules/minimatch/dist/commonjs/ast.js +845 -0
- package/node_modules/minimatch/dist/commonjs/brace-expressions.js +150 -0
- package/node_modules/minimatch/dist/commonjs/escape.js +30 -0
- package/node_modules/minimatch/dist/commonjs/index.js +1127 -0
- package/node_modules/minimatch/dist/commonjs/package.json +3 -0
- package/node_modules/minimatch/dist/commonjs/unescape.js +38 -0
- package/node_modules/minimatch/dist/esm/assert-valid-pattern.js +10 -0
- package/node_modules/minimatch/dist/esm/ast.js +841 -0
- package/node_modules/minimatch/dist/esm/brace-expressions.js +146 -0
- package/node_modules/minimatch/dist/esm/escape.js +26 -0
- package/node_modules/minimatch/dist/esm/index.js +1114 -0
- package/node_modules/minimatch/dist/esm/package.json +3 -0
- package/node_modules/minimatch/dist/esm/unescape.js +34 -0
- package/node_modules/minimatch/package.json +73 -0
- package/node_modules/minipass/LICENSE.md +55 -0
- package/node_modules/minipass/README.md +825 -0
- package/node_modules/minipass/dist/commonjs/index.js +1038 -0
- package/node_modules/minipass/dist/commonjs/package.json +3 -0
- package/node_modules/minipass/dist/esm/index.js +1020 -0
- package/node_modules/minipass/dist/esm/package.json +3 -0
- package/node_modules/minipass/package.json +77 -0
- package/node_modules/path-scurry/LICENSE.md +55 -0
- package/node_modules/path-scurry/README.md +636 -0
- package/node_modules/path-scurry/dist/commonjs/index.js +2018 -0
- package/node_modules/path-scurry/dist/commonjs/package.json +3 -0
- package/node_modules/path-scurry/dist/esm/index.js +1983 -0
- package/node_modules/path-scurry/dist/esm/package.json +3 -0
- package/node_modules/path-scurry/node_modules/lru-cache/LICENSE.md +55 -0
- package/node_modules/path-scurry/node_modules/lru-cache/README.md +469 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js +1726 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.d.cts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +1726 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js +9 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js +1726 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/package.json +3 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +4 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js +1722 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.d.mts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.js +19 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +1722 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +6 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js +1722 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/package.json +3 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/package.json +154 -0
- package/node_modules/path-scurry/package.json +72 -0
- package/package.json +1 -1
- package/node_modules/@maka/runtime/dist/active-tool-result-prune.js +0 -248
- package/node_modules/@maka/runtime/dist/active-tool-result-working-set.js +0 -218
- package/node_modules/@maka/runtime/dist/archive-read-tool.js +0 -115
|
@@ -16,16 +16,17 @@
|
|
|
16
16
|
* specific language governing permissions and limitations
|
|
17
17
|
* under the License.
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
19
|
+
import { searchFiles, GrepSearchError } from '../grep-search.js';
|
|
20
20
|
import { promises as fs } from 'node:fs';
|
|
21
|
-
import {
|
|
21
|
+
import { globFiles } from '../glob-search.js';
|
|
22
22
|
import { dirname, isAbsolute, parse, resolve } from 'node:path';
|
|
23
23
|
import { isPathInside } from '../path-containment.js';
|
|
24
|
+
import { ripgrepMissingMessage, ripgrepVanishedMessage, } from '../ripgrep-guidance.js';
|
|
24
25
|
import { sandboxPathApi } from './sandbox-paths.js';
|
|
25
26
|
import { sandboxBoundaryExpansionAllowsPath } from '@maka/core/sandbox-boundary';
|
|
26
27
|
import { ApplyPatchRejectedError, applyUpdateToContent, createPatchedFile, } from '../apply-patch-file.js';
|
|
27
28
|
import { computeEditedSource } from '../edit-replace.js';
|
|
28
|
-
import {
|
|
29
|
+
import { readPage } from '../read-page.js';
|
|
29
30
|
import { createEditUnifiedDiff, createUnifiedDiff } from '../unified-diff.js';
|
|
30
31
|
import { compareAndDeleteEntry, hostVisibilityAfterWrite, openStableTarget, readModifyWriteThroughHandle, StableWriteFailure, writeThroughHandle, } from '../file-stable-write.js';
|
|
31
32
|
import { isSupportedImagePath, readWorkspaceImage } from '../image-file.js';
|
|
@@ -35,9 +36,6 @@ import { isLikelySandboxDenial } from '../sandbox/detect.js';
|
|
|
35
36
|
// on POSIX, lexical + reparse-rejecting inside the Windows AppContainer where
|
|
36
37
|
// realpath is denied. See sandbox-paths.ts.
|
|
37
38
|
const { realpath, realpathAllowMissing, resolveCanonicalDirectoryEntryTarget } = sandboxPathApi();
|
|
38
|
-
const DEFAULT_GLOB_LIMIT = 200;
|
|
39
|
-
const MAX_GREP_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
40
|
-
const MAX_GREP_STDERR_BYTES = 16 * 1024;
|
|
41
39
|
export async function executeFilesystemWorkerRequest(request, dependencies = {}) {
|
|
42
40
|
try {
|
|
43
41
|
await assertTargetUnchanged(request.operation.cwd, request.operation.path, request.expectedTarget, operationUsesDirectoryEntry(request.operation), operationAccess(request.operation.kind));
|
|
@@ -76,10 +74,14 @@ export async function executeFilesystemOperation(operation, operationBoundary, d
|
|
|
76
74
|
}
|
|
77
75
|
}
|
|
78
76
|
const content = await fs.readFile(path, 'utf8');
|
|
79
|
-
|
|
80
|
-
kind: 'read',
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
try {
|
|
78
|
+
return { kind: 'read', ...readPage(content, operation, undefined, operation.continuation) };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw operationError('invalid_request', error instanceof Error
|
|
82
|
+
? error.message
|
|
83
|
+
: 'Read pagination failed. Use the original path again.');
|
|
84
|
+
}
|
|
83
85
|
}
|
|
84
86
|
case 'write': {
|
|
85
87
|
const path = await resolveWritableAllowed(operation.cwd, operation.path, 'Write', operationBoundary);
|
|
@@ -258,14 +260,7 @@ export async function executeFilesystemOperation(operation, operationBoundary, d
|
|
|
258
260
|
case 'glob': {
|
|
259
261
|
assertContainedGlobPattern(operation.pattern);
|
|
260
262
|
const path = await resolveExistingAllowed(operation.cwd, operation.path, 'Glob cwd', 'read', operationBoundary);
|
|
261
|
-
|
|
262
|
-
const limit = operation.limit ?? DEFAULT_GLOB_LIMIT;
|
|
263
|
-
for await (const file of nodeGlob(operation.pattern, { cwd: path })) {
|
|
264
|
-
files.push(typeof file === 'string' ? file : file.name);
|
|
265
|
-
if (files.length >= limit)
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
return { kind: 'glob', files };
|
|
263
|
+
return { kind: 'glob', ...(await globFiles({ ...operation, cwd: path })) };
|
|
269
264
|
}
|
|
270
265
|
case 'grep': {
|
|
271
266
|
const path = await resolveExistingAllowed(operation.cwd, operation.path, 'Grep', 'read', operationBoundary);
|
|
@@ -281,34 +276,32 @@ export async function executeFilesystemOperation(operation, operationBoundary, d
|
|
|
281
276
|
if (dependencies.windowsSandboxed) {
|
|
282
277
|
throw operationError('grep_unavailable', 'Grep is not available inside the Windows sandbox preview; use Glob and Read instead.');
|
|
283
278
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
executable: dependencies.grepExecutable,
|
|
292
|
-
args,
|
|
279
|
+
const grepExecutable = dependencies.grepExecutable;
|
|
280
|
+
if (!grepExecutable)
|
|
281
|
+
throw operationError('grep_unavailable', ripgrepMissingMessage(dependencies.ripgrepEnvironment));
|
|
282
|
+
const result = await searchFiles({
|
|
283
|
+
...operation,
|
|
284
|
+
path,
|
|
285
|
+
executable: grepExecutable,
|
|
293
286
|
// The target is canonical and absolute. Running from its filesystem root avoids
|
|
294
287
|
// requiring operation-scoped workers to read the broader session workspace.
|
|
295
288
|
cwd: parse(path).root,
|
|
296
289
|
timeoutMs: operation.timeoutMs,
|
|
290
|
+
}, dependencies.runGrep).catch((error) => {
|
|
291
|
+
// The cwd is a filesystem root, which always exists, so a spawn ENOENT
|
|
292
|
+
// means the executable this worker was launched with is gone — removed
|
|
293
|
+
// after the launch configuration checked it. Left alone it would be
|
|
294
|
+
// normalized to `not_found` and read as a missing search path.
|
|
295
|
+
if (nodeErrorCode(error) === 'ENOENT')
|
|
296
|
+
throw operationError('grep_unavailable', ripgrepVanishedMessage(grepExecutable, dependencies.ripgrepEnvironment));
|
|
297
|
+
if (error instanceof GrepSearchError) {
|
|
298
|
+
throw operationError(isLikelySandboxDenial({ stdout: '', stderr: error.message, sandboxed: true })
|
|
299
|
+
? 'sandbox_denied'
|
|
300
|
+
: 'filesystem_error', error.message);
|
|
301
|
+
}
|
|
302
|
+
throw error;
|
|
297
303
|
});
|
|
298
|
-
|
|
299
|
-
return { kind: 'grep', matches: [] };
|
|
300
|
-
if (result.exitCode !== 0) {
|
|
301
|
-
const detail = result.stderrTail.trim();
|
|
302
|
-
throw operationError(isLikelySandboxDenial({ stdout: result.stdout, stderr: detail, sandboxed: true })
|
|
303
|
-
? 'sandbox_denied'
|
|
304
|
-
: 'filesystem_error', detail
|
|
305
|
-
? `Grep failed while searching files.\n${detail}`
|
|
306
|
-
: 'Grep failed while searching files.');
|
|
307
|
-
}
|
|
308
|
-
return {
|
|
309
|
-
kind: 'grep',
|
|
310
|
-
matches: result.stdout.split('\n').filter(Boolean).slice(0, operation.limit),
|
|
311
|
-
};
|
|
304
|
+
return { kind: 'grep', ...result };
|
|
312
305
|
}
|
|
313
306
|
}
|
|
314
307
|
}
|
|
@@ -483,59 +476,3 @@ function nodeErrorCode(error) {
|
|
|
483
476
|
return undefined;
|
|
484
477
|
return typeof error.code === 'string' ? error.code : undefined;
|
|
485
478
|
}
|
|
486
|
-
async function runRipgrep(input) {
|
|
487
|
-
return await new Promise((resolvePromise, reject) => {
|
|
488
|
-
const child = spawn(input.executable, [...input.args], {
|
|
489
|
-
cwd: input.cwd,
|
|
490
|
-
shell: false,
|
|
491
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
492
|
-
});
|
|
493
|
-
const chunks = [];
|
|
494
|
-
let bytes = 0;
|
|
495
|
-
let stderrTail = Buffer.alloc(0);
|
|
496
|
-
let settled = false;
|
|
497
|
-
const timer = setTimeout(() => {
|
|
498
|
-
child.kill('SIGKILL');
|
|
499
|
-
rejectOnce(operationError('filesystem_error', 'Grep timed out.'));
|
|
500
|
-
}, input.timeoutMs);
|
|
501
|
-
child.stdout.on('data', (chunk) => {
|
|
502
|
-
bytes += chunk.length;
|
|
503
|
-
if (bytes > MAX_GREP_OUTPUT_BYTES) {
|
|
504
|
-
child.kill('SIGKILL');
|
|
505
|
-
rejectOnce(operationError('filesystem_error', 'Grep output exceeded the worker limit.'));
|
|
506
|
-
}
|
|
507
|
-
else {
|
|
508
|
-
chunks.push(chunk);
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
child.stderr.on('data', (chunk) => {
|
|
512
|
-
stderrTail = appendBoundedTail(stderrTail, chunk, MAX_GREP_STDERR_BYTES);
|
|
513
|
-
});
|
|
514
|
-
child.once('error', (error) => rejectOnce(error));
|
|
515
|
-
child.once('close', (exitCode) => {
|
|
516
|
-
if (settled)
|
|
517
|
-
return;
|
|
518
|
-
settled = true;
|
|
519
|
-
clearTimeout(timer);
|
|
520
|
-
resolvePromise({
|
|
521
|
-
exitCode: exitCode ?? 2,
|
|
522
|
-
stdout: Buffer.concat(chunks).toString('utf8'),
|
|
523
|
-
stderrTail: stderrTail.toString('utf8'),
|
|
524
|
-
});
|
|
525
|
-
});
|
|
526
|
-
function rejectOnce(error) {
|
|
527
|
-
if (settled)
|
|
528
|
-
return;
|
|
529
|
-
settled = true;
|
|
530
|
-
clearTimeout(timer);
|
|
531
|
-
reject(error);
|
|
532
|
-
}
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
function appendBoundedTail(current, chunk, limit) {
|
|
536
|
-
if (chunk.length >= limit)
|
|
537
|
-
return chunk.subarray(chunk.length - limit);
|
|
538
|
-
if (current.length + chunk.length <= limit)
|
|
539
|
-
return Buffer.concat([current, chunk]);
|
|
540
|
-
return Buffer.concat([current.subarray(current.length - (limit - chunk.length)), chunk]);
|
|
541
|
-
}
|
|
@@ -17,13 +17,12 @@
|
|
|
17
17
|
* under the License.
|
|
18
18
|
*/
|
|
19
19
|
import { z } from 'zod';
|
|
20
|
+
import { readContinuationSchema, readPageSchema } from '../read-page.js';
|
|
20
21
|
import { validateSandboxBoundaryExpansion } from '@maka/core/sandbox-boundary';
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
// JSON protocol boundary.
|
|
26
|
-
export const FILESYSTEM_WORKER_PROTOCOL_VERSION = 7;
|
|
22
|
+
import { GREP_MAX_LINES, GREP_MAX_LINES_PER_FILE, GREP_MAX_MATCH_BYTES } from '../grep-search.js';
|
|
23
|
+
// v10 adds bounded Read pages to v9's exact Grep counts. Older workers cannot
|
|
24
|
+
// satisfy the combined result contract and must be rejected at the handshake.
|
|
25
|
+
export const FILESYSTEM_WORKER_PROTOCOL_VERSION = 10;
|
|
27
26
|
/** The single authority on which operation kinds are writes. Shared by the
|
|
28
27
|
* client (permission/identity decisions) and the worker (operation guards) so
|
|
29
28
|
* the set cannot drift. */
|
|
@@ -100,6 +99,7 @@ export const FilesystemWorkerOperationSchema = z.union([
|
|
|
100
99
|
path,
|
|
101
100
|
offset: z.number().int().nonnegative().optional(),
|
|
102
101
|
limit: z.number().int().positive().optional(),
|
|
102
|
+
continuation: readContinuationSchema.optional(),
|
|
103
103
|
})
|
|
104
104
|
.strict(),
|
|
105
105
|
z.object({ kind: z.literal('write'), cwd, path, content: z.string() }).strict(),
|
|
@@ -146,8 +146,8 @@ export const FilesystemWorkerOperationSchema = z.union([
|
|
|
146
146
|
path,
|
|
147
147
|
pattern: z.string(),
|
|
148
148
|
glob: z.string().min(1).optional(),
|
|
149
|
-
maxCountPerFile: z.number().int().positive(),
|
|
150
|
-
limit: z.number().int().positive(),
|
|
149
|
+
maxCountPerFile: z.number().int().positive().max(GREP_MAX_LINES_PER_FILE),
|
|
150
|
+
limit: z.number().int().positive().max(GREP_MAX_LINES),
|
|
151
151
|
timeoutMs: z.number().int().positive(),
|
|
152
152
|
})
|
|
153
153
|
.strict(),
|
|
@@ -162,7 +162,7 @@ export const FilesystemWorkerRequestSchema = z
|
|
|
162
162
|
})
|
|
163
163
|
.strict();
|
|
164
164
|
export const FilesystemWorkerResultSchema = z.discriminatedUnion('kind', [
|
|
165
|
-
|
|
165
|
+
readPageSchema.extend({ kind: z.literal('read') }).strict(),
|
|
166
166
|
z
|
|
167
167
|
.object({
|
|
168
168
|
kind: z.literal('read_image'),
|
|
@@ -207,7 +207,22 @@ export const FilesystemWorkerResultSchema = z.discriminatedUnion('kind', [
|
|
|
207
207
|
})
|
|
208
208
|
.strict(),
|
|
209
209
|
z.object({ kind: z.literal('glob'), files: z.array(z.string()) }).strict(),
|
|
210
|
-
z
|
|
210
|
+
z
|
|
211
|
+
.object({
|
|
212
|
+
kind: z.literal('grep'),
|
|
213
|
+
matchedLines: z.number().int().nonnegative(),
|
|
214
|
+
returnedLines: z.number().int().nonnegative(),
|
|
215
|
+
omittedLines: z.number().int().nonnegative(),
|
|
216
|
+
truncated: z.boolean(),
|
|
217
|
+
matches: z
|
|
218
|
+
.array(z.string())
|
|
219
|
+
.max(GREP_MAX_LINES)
|
|
220
|
+
.refine((matches) => Buffer.byteLength(JSON.stringify(matches)) <= GREP_MAX_MATCH_BYTES),
|
|
221
|
+
})
|
|
222
|
+
.strict()
|
|
223
|
+
.refine((result) => result.returnedLines === result.matches.length &&
|
|
224
|
+
result.matchedLines === result.returnedLines + result.omittedLines &&
|
|
225
|
+
result.truncated === result.omittedLines > 0),
|
|
211
226
|
]);
|
|
212
227
|
export const FilesystemWorkerErrorCodeSchema = z.enum([
|
|
213
228
|
'invalid_request',
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { stdin, stdout } from 'node:process';
|
|
20
20
|
import { executeFilesystemWorkerRequest } from './operations.js';
|
|
21
|
+
import { parseRipgrepEnvironmentArg } from '../ripgrep-guidance.js';
|
|
21
22
|
import { FILESYSTEM_WORKER_PROTOCOL_VERSION, FilesystemWorkerRequestSchema, FilesystemWorkerResponseSchema, } from './protocol.js';
|
|
22
23
|
const MAX_REQUEST_BYTES = 16 * 1024 * 1024;
|
|
23
24
|
async function main() {
|
|
@@ -47,6 +48,7 @@ async function main() {
|
|
|
47
48
|
}
|
|
48
49
|
writeResponse(await executeFilesystemWorkerRequest(parsed.data, {
|
|
49
50
|
grepExecutable: readOption('--grep-executable'),
|
|
51
|
+
ripgrepEnvironment: parseRipgrepEnvironmentArg(readOption('--ripgrep-environment')),
|
|
50
52
|
windowsSandboxed: process.env.MAKA_WINDOWS_SANDBOX === '1',
|
|
51
53
|
}));
|
|
52
54
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
* or more contributor license agreements. See the NOTICE file
|
|
4
|
+
* distributed with this work for additional information
|
|
5
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
* to you under the Apache License, Version 2.0 (the
|
|
7
|
+
* "License"); you may not use this file except in compliance
|
|
8
|
+
* with the License. You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing,
|
|
13
|
+
* software distributed under the License is distributed on an
|
|
14
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
* KIND, either express or implied. See the License for the
|
|
16
|
+
* specific language governing permissions and limitations
|
|
17
|
+
* under the License.
|
|
18
|
+
*/
|
|
19
|
+
import { promises as fs, readdir } from 'node:fs';
|
|
20
|
+
import { resolve } from 'node:path';
|
|
21
|
+
import { globIterate } from 'glob';
|
|
22
|
+
export async function globFiles(input) {
|
|
23
|
+
let failure;
|
|
24
|
+
const directories = new Set([resolve(input.cwd)]);
|
|
25
|
+
function record(error, path) {
|
|
26
|
+
// Speculative literal components may miss. A directory already admitted by
|
|
27
|
+
// cwd, stat, or enumeration disappearing instead makes this walk incomplete.
|
|
28
|
+
if (directories.has(resolve(path)) || (error.code !== 'ENOENT' && error.code !== 'ENOTDIR')) {
|
|
29
|
+
failure ??= error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const files = [];
|
|
33
|
+
for await (const file of globIterate(input.pattern, {
|
|
34
|
+
cwd: input.cwd,
|
|
35
|
+
ignore: { childrenIgnored: (entry) => entry.isSymbolicLink() },
|
|
36
|
+
fs: {
|
|
37
|
+
readdir(path, options, callback) {
|
|
38
|
+
readdir(path, options, (error, entries) => {
|
|
39
|
+
if (error)
|
|
40
|
+
record(error, path);
|
|
41
|
+
else
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (entry.isDirectory())
|
|
44
|
+
directories.add(resolve(path, entry.name));
|
|
45
|
+
}
|
|
46
|
+
callback(error, entries);
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
promises: {
|
|
50
|
+
async lstat(path) {
|
|
51
|
+
try {
|
|
52
|
+
const stat = await fs.lstat(path);
|
|
53
|
+
if (stat.isDirectory())
|
|
54
|
+
directories.add(resolve(path));
|
|
55
|
+
return stat;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
record(error, path);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
})) {
|
|
65
|
+
if (failure)
|
|
66
|
+
throw failure;
|
|
67
|
+
files.push(file);
|
|
68
|
+
if (files.length >= (input.limit ?? 200))
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
if (failure)
|
|
72
|
+
throw failure;
|
|
73
|
+
return { files };
|
|
74
|
+
}
|
|
@@ -24,12 +24,14 @@
|
|
|
24
24
|
* an in-memory backoff instead of immediately spending another turn.
|
|
25
25
|
*/
|
|
26
26
|
import { evaluateGoal, } from './goal-evaluator.js';
|
|
27
|
-
import { goalCheckpoint, } from './goal-state.js';
|
|
27
|
+
import { goalCheckpoint, truncateGoalText, } from './goal-state.js';
|
|
28
28
|
import { GoalSessionCloseFence, } from './goal-session-close-fence.js';
|
|
29
29
|
import { GoalTaskGatePolicy } from './goal-task-gate-policy.js';
|
|
30
|
-
import { isDrivingGoal, sameGoalControlLease, } from '@maka/core/goal';
|
|
30
|
+
import { GOAL_PENDING_PROMPT_TEXT_LIMIT, isDrivingGoal, sameGoalControlLease, } from '@maka/core/goal';
|
|
31
31
|
export const volatileGoalDurability = Object.freeze({
|
|
32
32
|
flush: async () => { },
|
|
33
|
+
recordPendingContinuation: async () => { },
|
|
34
|
+
clearPendingContinuation: async () => { },
|
|
33
35
|
recordCurrentExecution: async () => { },
|
|
34
36
|
settleCurrentExecution: async () => { },
|
|
35
37
|
});
|
|
@@ -287,6 +289,39 @@ export class GoalContinuationCoordinator {
|
|
|
287
289
|
reason: goal.lastReason ?? 'Goal continuation recovered after Host restart.',
|
|
288
290
|
});
|
|
289
291
|
}
|
|
292
|
+
/** Restore the durable outbox entry before synthesizing generic recovery. */
|
|
293
|
+
recoverPendingContinuation(pending) {
|
|
294
|
+
if (this.disposed)
|
|
295
|
+
return;
|
|
296
|
+
const sessionId = this.deps.goalManager.getSessionIdByGoalId(pending.checkpoint.goalId);
|
|
297
|
+
if (!sessionId || this.sessionCloseFence.isClosed(sessionId))
|
|
298
|
+
return;
|
|
299
|
+
const goal = this.deps.goalManager.get(sessionId);
|
|
300
|
+
if (!goal ||
|
|
301
|
+
(goal.status !== 'active' && goal.status !== 'waiting') ||
|
|
302
|
+
!this.deps.goalManager.matches(sessionId, pending.checkpoint) ||
|
|
303
|
+
!this.deps.goalManager.matchesControlLease(sessionId, pending.controlLease)) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const lane = this.laneFor(sessionId);
|
|
307
|
+
if (lane.intent || lane.turns.size > 0)
|
|
308
|
+
return;
|
|
309
|
+
lane.intent = {
|
|
310
|
+
checkpoint: pending.checkpoint,
|
|
311
|
+
controlLease: pending.controlLease,
|
|
312
|
+
...(pending.triggeringTurnId ? { triggeringTurnId: pending.triggeringTurnId } : {}),
|
|
313
|
+
recoveredPrompt: pending.prompt,
|
|
314
|
+
evaluation: {
|
|
315
|
+
met: false,
|
|
316
|
+
impossible: false,
|
|
317
|
+
progress: false,
|
|
318
|
+
waiting: goal.status === 'waiting',
|
|
319
|
+
evaluatorFailed: true,
|
|
320
|
+
reason: 'Durable Goal continuation recovered after Host restart.',
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
this.scheduleDrain(lane);
|
|
324
|
+
}
|
|
290
325
|
/** Resume an exact paused Goal generation from Host control without a model-owned Turn. */
|
|
291
326
|
resumeFromControl(sessionId, checkpoint) {
|
|
292
327
|
if (this.disposed || this.sessionCloseFence.isClosed(sessionId))
|
|
@@ -353,9 +388,6 @@ export class GoalContinuationCoordinator {
|
|
|
353
388
|
}
|
|
354
389
|
enqueueTurn(outcome, controlLease, registration) {
|
|
355
390
|
const lane = registration.lane;
|
|
356
|
-
// New evidence always outranks an older, not-yet-admitted continuation.
|
|
357
|
-
lane.intent = undefined;
|
|
358
|
-
this.clearWaitingTimer(lane);
|
|
359
391
|
return new Promise((resolve) => {
|
|
360
392
|
lane.queue.push({
|
|
361
393
|
turnId: registration.turnId,
|
|
@@ -484,10 +516,21 @@ export class GoalContinuationCoordinator {
|
|
|
484
516
|
if (!this.deps.goalManager.matchesControlLease(lane.sessionId, item.controlLease)) {
|
|
485
517
|
return;
|
|
486
518
|
}
|
|
487
|
-
const goal = this.deps.goalManager.get(lane.sessionId);
|
|
488
519
|
if (item.checkpoint && !this.deps.goalManager.matchesActive(lane.sessionId, item.checkpoint)) {
|
|
489
520
|
return;
|
|
490
521
|
}
|
|
522
|
+
if (sameGoalControlLease(lane.intent?.controlLease, item.controlLease)) {
|
|
523
|
+
// New evidence always outranks an older, not-yet-admitted continuation.
|
|
524
|
+
lane.intent = undefined;
|
|
525
|
+
this.clearWaitingTimer(lane);
|
|
526
|
+
await this.deps.durability.clearPendingContinuation(lane.sessionId, item.controlLease);
|
|
527
|
+
if (!this.isCurrent(lane) ||
|
|
528
|
+
!this.deps.goalManager.matchesControlLease(lane.sessionId, item.controlLease) ||
|
|
529
|
+
(item.checkpoint && !this.deps.goalManager.matchesActive(lane.sessionId, item.checkpoint))) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const goal = this.deps.goalManager.get(lane.sessionId);
|
|
491
534
|
if (item.outcome.kind !== 'completed') {
|
|
492
535
|
if (goal.status !== 'active' && goal.status !== 'waiting')
|
|
493
536
|
return;
|
|
@@ -592,6 +635,13 @@ export class GoalContinuationCoordinator {
|
|
|
592
635
|
triggeringTurnId: turnId,
|
|
593
636
|
evaluation,
|
|
594
637
|
};
|
|
638
|
+
const pendingPrompt = truncateGoalText(buildContinuationPrompt(settled, evaluation, undefined), GOAL_PENDING_PROMPT_TEXT_LIMIT);
|
|
639
|
+
await this.deps.durability.recordPendingContinuation({
|
|
640
|
+
checkpoint: lane.intent.checkpoint,
|
|
641
|
+
controlLease,
|
|
642
|
+
prompt: pendingPrompt,
|
|
643
|
+
triggeringTurnId: turnId,
|
|
644
|
+
});
|
|
595
645
|
if (settled.status === 'waiting') {
|
|
596
646
|
lane.consecutiveWaits++;
|
|
597
647
|
return;
|
|
@@ -621,7 +671,9 @@ export class GoalContinuationCoordinator {
|
|
|
621
671
|
lane.intent = undefined;
|
|
622
672
|
return;
|
|
623
673
|
}
|
|
624
|
-
const prompt =
|
|
674
|
+
const prompt = intent.recoveredPrompt
|
|
675
|
+
? attachTaskReminder(intent.recoveredPrompt, taskPlan.reminder)
|
|
676
|
+
: buildContinuationPrompt(goal, intent.evaluation, taskPlan.reminder);
|
|
625
677
|
const admission = this.deps.admitTurn(lane.sessionId, prompt, intent.checkpoint, intent.controlLease);
|
|
626
678
|
if (admission.kind === 'busy') {
|
|
627
679
|
this.watchBusyLane(lane, admission.whenIdle);
|
|
@@ -720,7 +772,16 @@ export class GoalContinuationCoordinator {
|
|
|
720
772
|
return;
|
|
721
773
|
lane.intent = { ...lane.intent, checkpoint: goalCheckpoint(woken) };
|
|
722
774
|
void this.deps.durability
|
|
723
|
-
.
|
|
775
|
+
.recordPendingContinuation({
|
|
776
|
+
checkpoint: lane.intent.checkpoint,
|
|
777
|
+
controlLease: lane.intent.controlLease,
|
|
778
|
+
prompt: lane.intent.recoveredPrompt ??
|
|
779
|
+
buildContinuationPrompt(woken, lane.intent.evaluation, undefined),
|
|
780
|
+
...(lane.intent.triggeringTurnId
|
|
781
|
+
? { triggeringTurnId: lane.intent.triggeringTurnId }
|
|
782
|
+
: {}),
|
|
783
|
+
})
|
|
784
|
+
.then(() => this.deps.durability.flush(lane.sessionId))
|
|
724
785
|
.then(() => this.scheduleDrain(lane))
|
|
725
786
|
.catch(() => undefined);
|
|
726
787
|
}, delayMs);
|
|
@@ -778,6 +839,15 @@ function buildContinuationPrompt(goal, evaluation, taskReminder) {
|
|
|
778
839
|
`\n\nEvaluation: ${evaluation.reason}${evaluation.waiting ? ' (scheduled external-event re-check)' : ''}\n` +
|
|
779
840
|
`Goal: "${goal.condition}" (turn ${goal.iterations}/${goal.maxIterations}${noProgress})`);
|
|
780
841
|
}
|
|
842
|
+
function attachTaskReminder(prompt, taskReminder) {
|
|
843
|
+
if (!taskReminder)
|
|
844
|
+
return prompt;
|
|
845
|
+
const evaluationMarker = '\n\nEvaluation:';
|
|
846
|
+
const evaluationAt = prompt.indexOf(evaluationMarker);
|
|
847
|
+
if (evaluationAt === -1)
|
|
848
|
+
return `${prompt}\n\n${taskReminder}`;
|
|
849
|
+
return `${prompt.slice(0, evaluationAt)}\n\n${taskReminder}${prompt.slice(evaluationAt)}`;
|
|
850
|
+
}
|
|
781
851
|
function waitBackoffMs(consecutiveWaits) {
|
|
782
852
|
return Math.min(GOAL_WAIT_BACKOFF_MAX_MS, GOAL_WAIT_BACKOFF_BASE_MS * 2 ** Math.max(0, consecutiveWaits - 1));
|
|
783
853
|
}
|
|
@@ -158,6 +158,13 @@ export class GoalManager {
|
|
|
158
158
|
get(sessionId) {
|
|
159
159
|
return this.goals.get(sessionId)?.state;
|
|
160
160
|
}
|
|
161
|
+
getSessionIdByGoalId(goalId) {
|
|
162
|
+
for (const [sessionId, record] of this.goals) {
|
|
163
|
+
if (record.state.id === goalId)
|
|
164
|
+
return sessionId;
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
161
168
|
getActive(sessionId) {
|
|
162
169
|
const goal = this.goals.get(sessionId)?.state;
|
|
163
170
|
return goal?.status === 'active' ? goal : undefined;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
* or more contributor license agreements. See the NOTICE file
|
|
4
|
+
* distributed with this work for additional information
|
|
5
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
* to you under the Apache License, Version 2.0 (the
|
|
7
|
+
* "License"); you may not use this file except in compliance
|
|
8
|
+
* with the License. You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing,
|
|
13
|
+
* software distributed under the License is distributed on an
|
|
14
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
* KIND, either express or implied. See the License for the
|
|
16
|
+
* specific language governing permissions and limitations
|
|
17
|
+
* under the License.
|
|
18
|
+
*/
|
|
19
|
+
import { spawn } from 'node:child_process';
|
|
20
|
+
export const GREP_MAX_LINES_PER_FILE = 50;
|
|
21
|
+
export const GREP_MAX_LINES = 200;
|
|
22
|
+
export const GREP_MAX_MATCH_BYTES = 24 * 1024;
|
|
23
|
+
const MAX_RECORD_BYTES = 256 * 1024;
|
|
24
|
+
const MAX_STDERR_BYTES = 16 * 1024;
|
|
25
|
+
export class GrepSearchError extends Error {
|
|
26
|
+
}
|
|
27
|
+
export async function searchFiles(input, run = runRipgrep) {
|
|
28
|
+
const matches = [];
|
|
29
|
+
let matchBytes = 2;
|
|
30
|
+
let returnedInFile = 0;
|
|
31
|
+
let matchedLines;
|
|
32
|
+
let pending = Buffer.alloc(0);
|
|
33
|
+
let oversized = false;
|
|
34
|
+
const perFile = Math.min(input.maxCountPerFile, GREP_MAX_LINES_PER_FILE);
|
|
35
|
+
const limit = Math.min(input.limit, GREP_MAX_LINES);
|
|
36
|
+
// Text mode prevents binary detection from stopping a file early. Single-threaded
|
|
37
|
+
// output keeps each file's begin/match/end records together without a growing map.
|
|
38
|
+
const args = ['--no-config', '--json', '--text', '--threads=1', '--line-buffered'];
|
|
39
|
+
if (input.glob)
|
|
40
|
+
args.push('--glob', input.glob);
|
|
41
|
+
args.push('--', input.pattern, input.path);
|
|
42
|
+
const result = await run({ ...input, args, onStdout });
|
|
43
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
44
|
+
throw new GrepSearchError(`Grep failed; search totals are unknown. Check the regex and search path, then retry.\n${result.stderrTail.trim()}`);
|
|
45
|
+
}
|
|
46
|
+
if (pending.length || oversized || matchedLines === undefined || matchedLines < matches.length) {
|
|
47
|
+
throw new GrepSearchError('Grep did not produce a complete search summary; search totals are unknown. Retry with a narrower path or glob.');
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
matchedLines,
|
|
51
|
+
returnedLines: matches.length,
|
|
52
|
+
omittedLines: matchedLines - matches.length,
|
|
53
|
+
truncated: matchedLines > matches.length,
|
|
54
|
+
matches,
|
|
55
|
+
};
|
|
56
|
+
function onStdout(chunk) {
|
|
57
|
+
let start = 0;
|
|
58
|
+
while (start < chunk.length) {
|
|
59
|
+
const newline = chunk.indexOf(10, start);
|
|
60
|
+
const end = newline === -1 ? chunk.length : newline;
|
|
61
|
+
if (!oversized) {
|
|
62
|
+
const part = chunk.subarray(start, end);
|
|
63
|
+
if (pending.length + part.length > MAX_RECORD_BYTES) {
|
|
64
|
+
oversized = true;
|
|
65
|
+
pending = Buffer.alloc(0);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
pending = Buffer.concat([pending, part]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (newline === -1)
|
|
72
|
+
break;
|
|
73
|
+
// Oversized match records are omitted, not shortened. The final engine
|
|
74
|
+
// summary still counts their lines without retaining their content.
|
|
75
|
+
if (!oversized)
|
|
76
|
+
consumeRecord(pending.toString('utf8'));
|
|
77
|
+
pending = Buffer.alloc(0);
|
|
78
|
+
oversized = false;
|
|
79
|
+
start = newline + 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function consumeRecord(line) {
|
|
83
|
+
const event = JSON.parse(line);
|
|
84
|
+
if (matchedLines !== undefined)
|
|
85
|
+
throw new GrepSearchError('Grep emitted data after its summary; search totals are unknown.');
|
|
86
|
+
if (event.type === 'begin')
|
|
87
|
+
returnedInFile = 0;
|
|
88
|
+
if (event.type === 'summary') {
|
|
89
|
+
const count = event.data.stats.matched_lines;
|
|
90
|
+
if (!Number.isSafeInteger(count) || count < 0)
|
|
91
|
+
throw new GrepSearchError('Grep returned an invalid line count; search totals are unknown.');
|
|
92
|
+
matchedLines = count;
|
|
93
|
+
}
|
|
94
|
+
if (event.type !== 'match' || matches.length >= limit || returnedInFile >= perFile)
|
|
95
|
+
return;
|
|
96
|
+
const { path, lines, line_number: lineNumber } = event.data;
|
|
97
|
+
// ripgrep uses base64 for non-UTF8 paths/content; do not pass a lossy decoded
|
|
98
|
+
// filename or pretend that a replacement-character preview is the original.
|
|
99
|
+
if (typeof path.text !== 'string' || typeof lines.text !== 'string')
|
|
100
|
+
return;
|
|
101
|
+
const match = `${path.text}:${lineNumber}:${lines.text.replace(/\r?\n$/, '')}`;
|
|
102
|
+
const bytes = Buffer.byteLength(JSON.stringify(match)) + (matches.length ? 1 : 0);
|
|
103
|
+
if (matchBytes + bytes > GREP_MAX_MATCH_BYTES)
|
|
104
|
+
return;
|
|
105
|
+
matchBytes += bytes;
|
|
106
|
+
matches.push(match);
|
|
107
|
+
returnedInFile++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function runRipgrep(input) {
|
|
111
|
+
if (input.abortSignal?.aborted)
|
|
112
|
+
throw new GrepSearchError('Grep was cancelled; search totals are unknown.');
|
|
113
|
+
return await new Promise((resolve, reject) => {
|
|
114
|
+
const child = spawn(input.executable, [...input.args], {
|
|
115
|
+
cwd: input.cwd,
|
|
116
|
+
shell: false,
|
|
117
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
118
|
+
});
|
|
119
|
+
let stderrTail = Buffer.alloc(0);
|
|
120
|
+
let failure;
|
|
121
|
+
const timer = setTimeout(() => fail(new GrepSearchError('Grep timed out; search totals are unknown. Retry with a narrower path or glob.')), input.timeoutMs);
|
|
122
|
+
const abort = () => fail(new GrepSearchError('Grep was cancelled; search totals are unknown.'));
|
|
123
|
+
input.abortSignal?.addEventListener('abort', abort, { once: true });
|
|
124
|
+
if (input.abortSignal?.aborted)
|
|
125
|
+
abort();
|
|
126
|
+
child.stdout.on('data', (chunk) => {
|
|
127
|
+
if (failure)
|
|
128
|
+
return;
|
|
129
|
+
try {
|
|
130
|
+
input.onStdout(chunk);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
fail(new GrepSearchError('Grep returned invalid search output; search totals are unknown.'));
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
child.stderr.on('data', (chunk) => {
|
|
137
|
+
stderrTail = Buffer.concat([stderrTail, chunk]).subarray(-MAX_STDERR_BYTES);
|
|
138
|
+
});
|
|
139
|
+
child.once('error', (error) => {
|
|
140
|
+
failure ??= error;
|
|
141
|
+
});
|
|
142
|
+
child.once('close', (exitCode) => {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
input.abortSignal?.removeEventListener('abort', abort);
|
|
145
|
+
if (failure)
|
|
146
|
+
reject(failure);
|
|
147
|
+
else
|
|
148
|
+
resolve({ exitCode: exitCode ?? 2, stderrTail: stderrTail.toString('utf8') });
|
|
149
|
+
});
|
|
150
|
+
function fail(error) {
|
|
151
|
+
failure ??= error;
|
|
152
|
+
child.kill('SIGKILL');
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|