deadbyte-mcp 0.15.2 → 0.15.4
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/MANIFEST.SHA256 +66 -41
- package/PROVENANCE.json +3 -3
- package/RELEASE.json +3 -3
- package/package.json +1 -1
- package/scripts/autonomous-build.ps1 +1 -0
- package/scripts/autonomous-tests.ps1 +1 -0
- package/scripts/check-agent-backend-r46.mjs +77 -0
- package/scripts/check-atomic-resource-r46.mjs +84 -0
- package/scripts/check-autonomous-race-r46.mjs +160 -0
- package/scripts/check-coding-task-deadline-r46.mjs +59 -0
- package/scripts/check-deadline-cancellation-r46.mjs +49 -0
- package/scripts/check-process-deadline-r46.mjs +52 -0
- package/scripts/check-r46-background.mjs +5 -0
- package/scripts/extract-r46-failures.mjs +5 -0
- package/scripts/full-regression-r46.mjs +20 -12
- package/scripts/gate-windows-r46.ps1 +2 -0
- package/scripts/inspect-r46-active.mjs +2 -0
- package/scripts/r47-core-failures.mjs +1 -0
- package/scripts/release-manifest.mjs +1 -1
- package/scripts/run-r46-full-regression-background.mjs +7 -0
- package/scripts/run-r46-windows-gate-background.mjs +6 -0
- package/scripts/run-r46-windows-gate-background.ps1 +10 -0
- package/scripts/windows-gate-evidence-r46.mjs +4 -3
- package/scripts/workload-contract-regression-r47.mjs +14 -0
- package/scripts/workload-stream-regression.mjs +16 -0
- package/scripts/workload-substrate-r47-regression.mjs +12 -0
- package/src/agent-backend-r44.mjs +75 -10
- package/src/atomic-mutation.mjs +55 -0
- package/src/autonomous-mcp-tools.mjs +4 -3
- package/src/autonomous-output-contracts.mjs +16 -3
- package/src/autonomous-runtime.mjs +83 -30
- package/src/coding-mcp-tools.mjs +6 -6
- package/src/coding-output-contracts.mjs +20 -5
- package/src/coding-plane.mjs +3 -24
- package/src/coding-search.mjs +52 -15
- package/src/coding-task-runtime.mjs +41 -6
- package/src/compact-gateway-mcp-tools.mjs +3 -3
- package/src/deadline-cancellation.mjs +83 -0
- package/src/fuzzy-search-core.mjs +117 -0
- package/src/fuzzy-search-worker.mjs +8 -0
- package/src/fuzzy-search.mjs +34 -0
- package/src/machine-fs-mcp-tools.mjs +1 -1
- package/src/machine-fs-output-contracts.mjs +1 -1
- package/src/machine-fs-smoke-client.mjs +1 -1
- package/src/machine-fs.mjs +10 -29
- package/src/mcp-server.mjs +6 -0
- package/src/observation-smoke-client.mjs +1 -1
- package/src/process-mcp-tools.mjs +11 -3
- package/src/process-output-contracts.mjs +23 -0
- package/src/process-runtime.mjs +54 -5
- package/src/process-smoke-client.mjs +1 -1
- package/src/resource-identity.mjs +59 -0
- package/src/runtime-update.mjs +526 -162
- package/src/workload-substrate.mjs +87 -0
- package/test/coding-runtime.test.mjs +12 -0
- package/test/coding-search-r36.test.mjs +7 -0
- package/test/full-max-catalog-r46.test.mjs +2 -2
- package/test/fuzzy-search-r46.test.mjs +54 -0
- package/test/helpers/autonomous-r34-fixture.mjs +10 -1
- package/test/machine-fs-mcp-tools.test.mjs +2 -0
- package/test/machine-fs-runtime.test.mjs +6 -0
- package/test/process-mcp-tools.test.mjs +5 -2
- package/test/process-runtime.test.mjs +18 -1
- package/test/r45-finalization.test.mjs +1 -1
- package/test/release-version.test.mjs +1 -1
- package/test/runtime-self-update.test.mjs +118 -1
- package/test/workload-substrate.test.mjs +22 -0
package/src/coding-mcp-tools.mjs
CHANGED
|
@@ -70,14 +70,14 @@ export function registerCodingTools(server, { codingRuntime, taskRuntime, semant
|
|
|
70
70
|
server.registerTool('coding_search', {
|
|
71
71
|
description: 'Deterministic bounded content/path search with literal or regex mode, signed cursor paging, and observed file identities.',
|
|
72
72
|
inputSchema: z.strictObject({ root_id: rootId, path: relPath.default('.'), pattern: z.string().min(1).max(256),
|
|
73
|
-
target: z.enum(['content','path']).default('content'), mode: z.enum(['literal','regex']).default('literal'), case_sensitive: z.boolean().default(false),
|
|
73
|
+
target: z.enum(['content','path']).default('content'), mode: z.enum(['literal','regex','fuzzy']).default('literal'), case_sensitive: z.boolean().default(false),
|
|
74
74
|
max_results: z.number().int().min(1).max(1000).default(200), max_file_bytes: z.number().int().min(1).max(4194304).default(262144),
|
|
75
75
|
max_scan_bytes: z.number().int().min(1).max(67108864).default(4194304), page_size: z.number().int().min(1).max(500).default(100),
|
|
76
|
-
cursor: z.string().max(8192).nullable().default(null) }),
|
|
76
|
+
cursor: z.string().max(8192).nullable().default(null), fuzzy_timeout_ms: z.number().int().min(100).max(30000).default(30000) }),
|
|
77
77
|
outputSchema: codingSearchOutputSchema
|
|
78
|
-
}, async ({ root_id, path, pattern, target, mode, case_sensitive, max_results, max_file_bytes, max_scan_bytes, page_size, cursor }) => {
|
|
78
|
+
}, async ({ root_id, path, pattern, target, mode, case_sensitive, max_results, max_file_bytes, max_scan_bytes, page_size, cursor, fuzzy_timeout_ms }) => {
|
|
79
79
|
try { return result(codingSearchOutputSchema, await codingRuntime.search({ rootId: root_id, path, pattern, target, mode, caseSensitive: case_sensitive,
|
|
80
|
-
maxResults: max_results, maxFileBytes: max_file_bytes, maxScanBytes: max_scan_bytes, pageSize: page_size, cursor })); }
|
|
80
|
+
maxResults: max_results, maxFileBytes: max_file_bytes, maxScanBytes: max_scan_bytes, pageSize: page_size, cursor, fuzzyTimeoutMs: fuzzy_timeout_ms })); }
|
|
81
81
|
catch (error) { return fail(codingSearchOutputSchema, error); }
|
|
82
82
|
});
|
|
83
83
|
|
|
@@ -171,8 +171,8 @@ export function registerCodingTools(server, { codingRuntime, taskRuntime, semant
|
|
|
171
171
|
}, async ({ submission_id, title, steps }) => { try { return result(codingTaskSubmitOutputSchema, await taskRuntime.submit({ submissionId: submission_id, title, steps })); } catch (error) { return fail(codingTaskSubmitOutputSchema, error); } });
|
|
172
172
|
server.registerTool('coding_task_run', {
|
|
173
173
|
description: 'Advance one durable coding task by bounded steps with retry/checkpoint/crash reconciliation.',
|
|
174
|
-
inputSchema: z.strictObject({ task_id: taskId, max_steps: z.number().int().min(1).max(32).default(8) }), outputSchema: codingTaskRunOutputSchema
|
|
175
|
-
}, async ({ task_id, max_steps }) => { try { return result(codingTaskRunOutputSchema, await taskRuntime.run({ taskId: task_id, maxSteps: max_steps })); } catch (error) { return fail(codingTaskRunOutputSchema, error); } });
|
|
174
|
+
inputSchema: z.strictObject({ task_id: taskId, max_steps: z.number().int().min(1).max(32).default(8), timeout_ms: z.number().int().min(100).max(900000).nullable().default(null) }), outputSchema: codingTaskRunOutputSchema
|
|
175
|
+
}, async ({ task_id, max_steps, timeout_ms }) => { try { return result(codingTaskRunOutputSchema, await taskRuntime.run({ taskId: task_id, maxSteps: max_steps, timeoutMs: timeout_ms })); } catch (error) { return fail(codingTaskRunOutputSchema, error); } });
|
|
176
176
|
|
|
177
177
|
server.registerTool('coding_task_status', {
|
|
178
178
|
description: 'Read derived coding-task state from the authenticated event journal.',
|
|
@@ -8,9 +8,19 @@ const hex64 = z.string().regex(HEX64_RE);
|
|
|
8
8
|
const receiptId = z.string().regex(HEX24_RE);
|
|
9
9
|
const taskId = z.string().regex(TASK_ID_RE);
|
|
10
10
|
const stepId = z.string().regex(STEP_ID_RE);
|
|
11
|
+
const streamCoverage = z.strictObject({
|
|
12
|
+
logical_first_seq:z.number().int().min(1), logical_last_seq:z.number().int().min(0),
|
|
13
|
+
retained_first_seq:z.number().int().min(1), retained_last_seq:z.number().int().min(0),
|
|
14
|
+
complete:z.boolean(), truncated:z.boolean(),
|
|
15
|
+
missing_ranges:z.array(z.strictObject({from:z.number().int().min(1),to:z.number().int().min(0)})).max(100000),
|
|
16
|
+
logical_item_count:z.number().int().min(0), retained_item_count:z.number().int().min(0),
|
|
17
|
+
logical_bytes:z.number().int().min(0), retained_bytes:z.number().int().min(0),
|
|
18
|
+
dropped_items:z.number().int().min(0), dropped_bytes:z.number().int().min(0), status:z.enum(['open','closed'])
|
|
19
|
+
});
|
|
11
20
|
const jsonObject = z.record(z.string(), z.json());
|
|
12
21
|
const taskState = z.enum(['pending', 'running', 'paused', 'failed', 'cancelled', 'succeeded']);
|
|
13
22
|
const codingKind = z.enum(['checkpoint', 'write', 'replace', 'command', 'benchmark']);
|
|
23
|
+
const deadline = z.strictObject({ schema:z.literal('deadbyte.deadline.v1'), id:z.string().nullable(), created_at_ms:z.number(), timeout_ms:z.number().int().min(1).nullable(), deadline_ms:z.number().nullable() });
|
|
14
24
|
|
|
15
25
|
function requireFields(value, ctx, fields) {
|
|
16
26
|
for (const field of fields) if (value[field] === undefined) ctx.addIssue({ code: 'custom', path: [field], message: `${field} is required` });
|
|
@@ -82,12 +92,15 @@ export const codingSearchOutputSchema = statusContract({
|
|
|
82
92
|
pattern: z.string().min(1).max(256).optional(), target: z.enum(['content','path']).optional(),
|
|
83
93
|
mode: z.enum(['literal','regex']).optional(), case_sensitive: z.boolean().optional(),
|
|
84
94
|
matches: z.array(z.strictObject({ path: z.string().min(1).max(512), line: z.number().int().min(1).nullable(),
|
|
85
|
-
column: z.number().int().min(1).nullable(), preview: z.string().max(512), file_sha256: hex64.nullable()
|
|
95
|
+
column: z.number().int().min(1).nullable(), preview: z.string().max(512), file_sha256: hex64.nullable(),
|
|
96
|
+
distance: z.number().int().nonnegative().optional(), similarity: z.number().min(0).max(1).optional(), match_length: z.number().int().positive().optional() })).max(500).optional(),
|
|
86
97
|
files_scanned: z.number().int().min(0).optional(), bytes_scanned: z.number().int().min(0).optional(),
|
|
87
98
|
binary_files_skipped: z.number().int().min(0).optional(), truncated: z.boolean().optional(),
|
|
88
99
|
truncation_reason: z.enum(['tree_limit','scan_byte_limit','max_results']).nullable().optional(),
|
|
89
100
|
query_sha256: hex64.optional(), snapshot_sha256: hex64.optional(), page_sha256: hex64.optional(),
|
|
90
|
-
next_cursor: z.string().max(8192).nullable().optional()
|
|
101
|
+
next_cursor: z.string().max(8192).nullable().optional(),
|
|
102
|
+
stream_id: hex64.optional(), stream_generation: z.number().int().min(1).optional(), stream_next_seq: z.number().int().min(1).optional(),
|
|
103
|
+
stream_coverage: streamCoverage.optional() },
|
|
91
104
|
successStatuses: ['ok'], successRequired: ['root_id','path','pattern','target','mode','case_sensitive','matches','files_scanned','bytes_scanned','binary_files_skipped','truncated','truncation_reason','query_sha256','snapshot_sha256','page_sha256','next_cursor']
|
|
92
105
|
});
|
|
93
106
|
const semanticLocationSchema = z.strictObject({
|
|
@@ -222,7 +235,7 @@ export const codingTaskSubmitOutputSchema = statusContract({
|
|
|
222
235
|
successStatuses: ['accepted', 'existing'], successRequired: ['task_id', 'plan_sha256', 'state']
|
|
223
236
|
});
|
|
224
237
|
export const codingTaskRunOutputSchema = z.strictObject({
|
|
225
|
-
status: z.enum(['terminal', 'paused', 'completed', 'cancelled', 'failed', 'yielded', 'succeeded']), error: z.string().optional(), task_id: taskId.optional(),
|
|
238
|
+
status: z.enum(['terminal', 'paused', 'completed', 'cancelled', 'failed', 'yielded', 'succeeded']), error: z.string().optional(), task_id: taskId.optional(), deadline: deadline.optional(),
|
|
226
239
|
...Object.fromEntries(Object.entries(derivedShape).map(([key, schema]) => [key, schema.optional()]))
|
|
227
240
|
}).superRefine((value, ctx) => {
|
|
228
241
|
if (value.error !== undefined) { if (value.status !== 'failed') ctx.addIssue({ code: 'custom', path: ['error'], message: 'error wrapper requires status=failed' }); return; }
|
|
@@ -231,13 +244,15 @@ export const codingTaskRunOutputSchema = z.strictObject({
|
|
|
231
244
|
export const codingTaskStatusOutputSchema = statusContract({
|
|
232
245
|
statuses: ['ok', 'failed'],
|
|
233
246
|
shape: { task_id: taskId.optional(), plan_sha256: hex64.optional(),
|
|
234
|
-
...Object.fromEntries(Object.entries(derivedShape).map(([key, schema]) => [key, schema.optional()])), journal_verified: z.literal(true).optional()
|
|
247
|
+
...Object.fromEntries(Object.entries(derivedShape).map(([key, schema]) => [key, schema.optional()])), journal_verified: z.literal(true).optional(),
|
|
248
|
+
stream_id: taskId.optional(), stream_generation: z.number().int().min(1).optional(), stream_next_seq: z.number().int().min(1).optional(), stream_coverage: streamCoverage.optional() },
|
|
235
249
|
successStatuses: ['ok'], successRequired: ['task_id', 'plan_sha256', ...Object.keys(derivedShape), 'journal_verified']
|
|
236
250
|
});
|
|
237
251
|
export const codingTaskEventsOutputSchema = statusContract({
|
|
238
252
|
statuses: ['ok', 'failed'],
|
|
239
253
|
shape: { task_id: taskId.optional(), plan_sha256: hex64.optional(), journal_verified: z.literal(true).optional(),
|
|
240
|
-
journal_head_sha256: hex64.optional(), events: z.array(eventSchema).max(200).optional()
|
|
254
|
+
journal_head_sha256: hex64.optional(), events: z.array(eventSchema).max(200).optional(),
|
|
255
|
+
stream_id: taskId.optional(), stream_generation: z.number().int().min(1).optional(), stream_next_seq: z.number().int().min(1).optional(), stream_coverage: streamCoverage.optional() },
|
|
241
256
|
successStatuses: ['ok'], successRequired: ['task_id', 'plan_sha256', 'journal_verified', 'journal_head_sha256', 'events']
|
|
242
257
|
});
|
|
243
258
|
export const codingTaskCancelOutputSchema = statusContract({
|
package/src/coding-plane.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { isInside, validatePortableRelativePath } from './path-policy.mjs';
|
|
|
10
10
|
import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
|
|
11
11
|
import { createCodingSearch } from './coding-search.mjs';
|
|
12
12
|
import { createSearchCursorCodec } from './search-cursor.mjs';
|
|
13
|
+
import { atomicMutation } from './atomic-mutation.mjs';
|
|
13
14
|
|
|
14
15
|
export const CODING_POLICY_SCHEMA = 'deadbyte.coding-policy.v1';
|
|
15
16
|
export const CODING_LEASE_SCHEMA = 'deadbyte.coding-lease.v1';
|
|
@@ -261,30 +262,8 @@ function decodeContent(content, encoding) {
|
|
|
261
262
|
throw new Error('encoding must be utf8 or base64');
|
|
262
263
|
}
|
|
263
264
|
async function atomicWrite(target, bytes, expectedSha256, authorityCheck = null) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const temp = path.join(path.dirname(target), `.${path.basename(target)}.deadbyte-coding-${process.pid}-${randomBytes(8).toString('hex')}.tmp`);
|
|
267
|
-
let handle;
|
|
268
|
-
try {
|
|
269
|
-
if (authorityCheck) await authorityCheck();
|
|
270
|
-
handle = await open(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);
|
|
271
|
-
await handle.writeFile(bytes);
|
|
272
|
-
await handle.sync();
|
|
273
|
-
await handle.close();
|
|
274
|
-
handle = null;
|
|
275
|
-
const race = await targetState(target);
|
|
276
|
-
checkExpected(race, expectedSha256);
|
|
277
|
-
if (authorityCheck) await authorityCheck();
|
|
278
|
-
await rename(temp, target);
|
|
279
|
-
} catch (error) {
|
|
280
|
-
if (handle) await handle.close().catch(() => {});
|
|
281
|
-
await rm(temp, { force: true }).catch(() => {});
|
|
282
|
-
throw error;
|
|
283
|
-
}
|
|
284
|
-
const after = await targetState(target);
|
|
285
|
-
const expectedAfter = sha256Bytes(bytes);
|
|
286
|
-
if (!after.exists || after.sha256 !== expectedAfter) throw new Error('coding write post-commit hash mismatch');
|
|
287
|
-
return { before, after };
|
|
265
|
+
return atomicMutation({ target, bytes, expected: expectedSha256, readState: targetState,
|
|
266
|
+
assertExpected: checkExpected, authorityCheck, tempTag: 'deadbyte-coding' });
|
|
288
267
|
}
|
|
289
268
|
function codingEnvironment() {
|
|
290
269
|
const allowed = new Set([
|
package/src/coding-search.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { canonicalJson } from './canonical-json.mjs';
|
|
3
3
|
import { decodeUtf8Strict } from './text-policy.mjs';
|
|
4
|
+
import { createSequenceStream } from './workload-substrate.mjs';
|
|
5
|
+
import { fuzzyIndexOf } from './fuzzy-search-core.mjs';
|
|
6
|
+
import { runFuzzySearchInWorker, FUZZY_SEARCH_TIMEOUT_MS } from './fuzzy-search.mjs';
|
|
4
7
|
|
|
5
8
|
const sha256Object=value=>createHash('sha256').update(canonicalJson(value),'utf8').digest('hex');
|
|
6
9
|
const sha256Bytes=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
@@ -52,11 +55,11 @@ function preview(text,index,length){
|
|
|
52
55
|
const start=Math.max(0,index-48); const end=Math.min(text.length,index+Math.max(length,1)+112);
|
|
53
56
|
return text.slice(start,end);
|
|
54
57
|
}
|
|
55
|
-
function normalizeQuery({rootId,path,target,mode,pattern,caseSensitive,maxResults,maxFileBytes,maxScanBytes,pageSize}){
|
|
58
|
+
function normalizeQuery({rootId,path,target,mode,pattern,caseSensitive,maxResults,maxFileBytes,maxScanBytes,pageSize,fuzzyTimeoutMs}){
|
|
56
59
|
return {
|
|
57
|
-
schema:'deadbyte.coding-search-query.
|
|
60
|
+
schema:'deadbyte.coding-search-query.v3',root_id:rootId,path,target,mode,pattern,
|
|
58
61
|
case_sensitive:caseSensitive,max_results:maxResults,max_file_bytes:maxFileBytes,
|
|
59
|
-
max_scan_bytes:maxScanBytes,page_size:pageSize
|
|
62
|
+
max_scan_bytes:maxScanBytes,page_size:pageSize,fuzzy_timeout_ms:fuzzyTimeoutMs
|
|
60
63
|
};
|
|
61
64
|
}
|
|
62
65
|
|
|
@@ -66,15 +69,18 @@ export function createCodingSearch({tree,fileRead,cursorCodec}){
|
|
|
66
69
|
}
|
|
67
70
|
return async function search({
|
|
68
71
|
rootId,path='.',pattern,target='content',mode='literal',caseSensitive=false,
|
|
69
|
-
maxResults=200,maxFileBytes=262144,maxScanBytes=4*1024*1024,pageSize=100,cursor=null
|
|
72
|
+
maxResults=200,maxFileBytes=262144,maxScanBytes=4*1024*1024,pageSize=100,cursor=null,
|
|
73
|
+
fuzzyTimeoutMs=FUZZY_SEARCH_TIMEOUT_MS
|
|
70
74
|
}){
|
|
71
75
|
if(!['content','path'].includes(target)) throw new Error('coding search target must be content or path');
|
|
76
|
+
if(!['literal','regex','fuzzy'].includes(mode)) throw new Error('coding search mode must be literal, regex, or fuzzy');
|
|
72
77
|
validateInt(maxResults,'max_results',1,1000);
|
|
73
78
|
validateInt(maxFileBytes,'max_file_bytes',1,4*1024*1024);
|
|
74
79
|
validateInt(maxScanBytes,'max_scan_bytes',1,64*1024*1024);
|
|
75
80
|
validateInt(pageSize,'page_size',1,500);
|
|
76
|
-
|
|
77
|
-
const
|
|
81
|
+
if(!Number.isInteger(fuzzyTimeoutMs)||fuzzyTimeoutMs<100||fuzzyTimeoutMs>FUZZY_SEARCH_TIMEOUT_MS) throw new Error('fuzzy_timeout_ms must be in [100, 30000]');
|
|
82
|
+
const matcher=mode==='fuzzy'?null:compileMatcher({pattern,mode,caseSensitive});
|
|
83
|
+
const query=normalizeQuery({rootId,path,target,mode,pattern,caseSensitive,maxResults,maxFileBytes,maxScanBytes,pageSize,fuzzyTimeoutMs});
|
|
78
84
|
const querySha=sha256Object(query);
|
|
79
85
|
let decodedCursor=null;
|
|
80
86
|
if(cursor!==null){
|
|
@@ -92,10 +98,18 @@ export function createCodingSearch({tree,fileRead,cursorCodec}){
|
|
|
92
98
|
if(target==='path'){
|
|
93
99
|
for(const entry of entries){
|
|
94
100
|
snapshotEntries.push({path:entry.path,bytes:entry.bytes,type:'file'});
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
const found=mode==='fuzzy'
|
|
102
|
+
? [fuzzyIndexOf(caseSensitive?entry.path:entry.path.toLowerCase(),caseSensitive?pattern:pattern.toLowerCase())]
|
|
103
|
+
: (matcher.path(entry.path)?[{start:0,end:entry.path.length,value:entry.path}]:[]);
|
|
104
|
+
for(const hit of found){
|
|
105
|
+
if(mode==='fuzzy' && hit.distance>Math.max(1,Math.floor(pattern.length*0.75))) continue;
|
|
106
|
+
matches.push({
|
|
107
|
+
path:entry.path,line:null,column:null,preview:entry.path,file_sha256:null,
|
|
108
|
+
...(mode==='fuzzy'?{distance:hit.distance,similarity:hit.similarity,match_length:Math.max(1,hit.value.length)}:{})
|
|
109
|
+
});
|
|
97
110
|
if(matches.length>=maxResults){truncated=true;truncationReason='max_results';break;}
|
|
98
111
|
}
|
|
112
|
+
if(matches.length>=maxResults) break;
|
|
99
113
|
}
|
|
100
114
|
} else {
|
|
101
115
|
for(const entry of entries){
|
|
@@ -109,11 +123,26 @@ export function createCodingSearch({tree,fileRead,cursorCodec}){
|
|
|
109
123
|
snapshotEntries.push({path:entry.path,bytes:item.bytes,sha256:fileSha});
|
|
110
124
|
let text;
|
|
111
125
|
try{text=decodeUtf8Strict(raw);}catch{binaryFilesSkipped+=1;continue;}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
if(mode==='fuzzy'){
|
|
127
|
+
const haystack=caseSensitive?text:text.toLowerCase();
|
|
128
|
+
const queryText=caseSensitive?pattern:pattern.toLowerCase();
|
|
129
|
+
const hit=await runFuzzySearchInWorker(haystack,queryText,fuzzyTimeoutMs);
|
|
130
|
+
const threshold=Math.max(1,Math.floor(queryText.length*0.75));
|
|
131
|
+
if(hit.distance<=threshold){
|
|
132
|
+
const lc=lineColumn(haystack,hit.start);
|
|
133
|
+
matches.push({
|
|
134
|
+
path:entry.path,line:lc.line,column:lc.column,
|
|
135
|
+
preview:preview(text,hit.start,hit.end-hit.start),file_sha256:fileSha,
|
|
136
|
+
distance:hit.distance,similarity:hit.similarity,match_length:Math.max(1,hit.value.length)
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
const found=matcher.all(text);
|
|
141
|
+
for(const hit of found){
|
|
142
|
+
const lc=lineColumn(text,hit.index);
|
|
143
|
+
matches.push({path:entry.path,line:lc.line,column:lc.column,preview:preview(text,hit.index,hit.length),file_sha256:fileSha});
|
|
144
|
+
if(matches.length>=maxResults){truncated=true;truncationReason='max_results';break;}
|
|
145
|
+
}
|
|
117
146
|
}
|
|
118
147
|
if(matches.length>=maxResults) break;
|
|
119
148
|
}
|
|
@@ -121,18 +150,26 @@ export function createCodingSearch({tree,fileRead,cursorCodec}){
|
|
|
121
150
|
|
|
122
151
|
const snapshotSha=sha256Object({schema:'deadbyte.coding-search-snapshot.v2',query_sha256:querySha,entries:snapshotEntries});
|
|
123
152
|
if(decodedCursor&&decodedCursor.snapshot_sha256!==snapshotSha) throw new Error('coding search cursor stale snapshot mismatch');
|
|
153
|
+
const stream=createSequenceStream({streamId:querySha,maxItems:Math.max(1,maxResults),maxBytes:Math.max(1024, maxResults*4096)});
|
|
154
|
+
for(const match of matches) stream.append(match,{producer:'coding-search',channel:'match'});
|
|
124
155
|
const offset=decodedCursor?.offset??0;
|
|
125
156
|
if(offset>matches.length) throw new Error('coding search cursor offset exceeds retained results');
|
|
126
|
-
const
|
|
157
|
+
const page=stream.read(offset+1,pageSize);
|
|
158
|
+
if(page.missing) throw new Error('coding search cursor offset is outside retained stream coverage');
|
|
159
|
+
const pageMatches=page.items.map(item=>item.value);
|
|
127
160
|
const nextOffset=offset+pageMatches.length;
|
|
128
161
|
const hasMore=nextOffset<matches.length;
|
|
129
162
|
const nextCursor=hasMore?await cursorCodec.encode({query_sha256:querySha,snapshot_sha256:snapshotSha,offset:nextOffset}):null;
|
|
130
163
|
const pageSha=sha256Object({schema:'deadbyte.coding-search-page.v2',query_sha256:querySha,snapshot_sha256:snapshotSha,offset,matches:pageMatches});
|
|
164
|
+
const coverage=stream.coverage();
|
|
165
|
+
const streamSnapshot=stream.snapshot();
|
|
131
166
|
return {
|
|
132
167
|
status:'ok',root_id:rootId,path,pattern,target,mode,case_sensitive:caseSensitive,
|
|
133
168
|
matches:pageMatches,files_scanned:filesScanned,bytes_scanned:bytesScanned,binary_files_skipped:binaryFilesSkipped,
|
|
134
169
|
truncated:truncated||hasMore,truncation_reason:truncationReason,query_sha256:querySha,snapshot_sha256:snapshotSha,
|
|
135
|
-
page_sha256:pageSha,next_cursor:nextCursor
|
|
170
|
+
page_sha256:pageSha,next_cursor:nextCursor,
|
|
171
|
+
stream_id:stream.streamId,stream_generation:stream.generation,stream_next_seq:streamSnapshot.next_seq,
|
|
172
|
+
stream_coverage:coverage
|
|
136
173
|
};
|
|
137
174
|
};
|
|
138
175
|
}
|
|
@@ -4,6 +4,8 @@ import path from 'node:path';
|
|
|
4
4
|
import { canonicalJson } from './canonical-json.mjs';
|
|
5
5
|
import { validatePortableRelativePath } from './path-policy.mjs';
|
|
6
6
|
import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
|
|
7
|
+
import { createJournalStream } from './workload-substrate.mjs';
|
|
8
|
+
import { createDeadline, deadlineState } from './deadline-cancellation.mjs';
|
|
7
9
|
|
|
8
10
|
export const CODING_TASK_PLAN_SCHEMA = 'deadbyte.coding-task-plan.v1';
|
|
9
11
|
export const CODING_TASK_EVENT_SCHEMA = 'deadbyte.coding-task-event.v1';
|
|
@@ -525,7 +527,11 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
|
|
|
525
527
|
const ctx = taskContext(policy, taskId);
|
|
526
528
|
const plan = await loadPlan(ctx, policy, verifyKeyPath);
|
|
527
529
|
const events = await readEvents(ctx, verifyKeyPath);
|
|
528
|
-
|
|
530
|
+
const stream = createJournalStream({streamId:taskId,events,maxItems:MAX_EVENTS,maxBytes:8*1024*1024,producer:'coding-task',channel:'event'});
|
|
531
|
+
const snapshot = stream.snapshot();
|
|
532
|
+
return { status: 'ok', task_id: taskId, plan_sha256: plan.plan_sha256, ...deriveState(plan, events), journal_verified: true,
|
|
533
|
+
stream_id:snapshot.stream_id, stream_generation:snapshot.generation, stream_next_seq:snapshot.next_seq,
|
|
534
|
+
stream_coverage:stream.coverage() };
|
|
529
535
|
}
|
|
530
536
|
|
|
531
537
|
async function events({ taskId, fromSeq = 1, limit = 100 }) {
|
|
@@ -534,10 +540,15 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
|
|
|
534
540
|
const ctx = taskContext(policy, taskId);
|
|
535
541
|
const plan = await loadPlan(ctx, policy, verifyKeyPath);
|
|
536
542
|
const all = await readEvents(ctx, verifyKeyPath);
|
|
543
|
+
const stream = createJournalStream({streamId:taskId,events:all,maxItems:MAX_EVENTS,maxBytes:8*1024*1024,producer:'coding-task',channel:'event'});
|
|
544
|
+
const page = stream.read(fromSeq,limit);
|
|
545
|
+
const snapshot = stream.snapshot();
|
|
537
546
|
return {
|
|
538
547
|
status: 'ok', task_id: taskId, plan_sha256: plan.plan_sha256, journal_verified: true,
|
|
539
548
|
journal_head_sha256: all.at(-1)?.event_sha256 ?? ZERO_HASH,
|
|
540
|
-
|
|
549
|
+
stream_id: snapshot.stream_id, stream_generation: snapshot.generation, stream_next_seq: snapshot.next_seq,
|
|
550
|
+
stream_coverage: stream.coverage(),
|
|
551
|
+
events: page.items.map(item=>item.value)
|
|
541
552
|
};
|
|
542
553
|
}
|
|
543
554
|
|
|
@@ -590,8 +601,18 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
|
|
|
590
601
|
const journal = await readEvents(ctx, verifyKeyPath);
|
|
591
602
|
return verifyTaskReceipt(ctx, plan, journal, policy, verifyKeyPath, codingRuntime);
|
|
592
603
|
}
|
|
593
|
-
async function run({ taskId, maxSteps = 8 }) {
|
|
604
|
+
async function run({ taskId, maxSteps = 8, timeoutMs = null }) {
|
|
594
605
|
if (!Number.isInteger(maxSteps) || maxSteps < 1 || maxSteps > MAX_STEPS) throw new Error(`max_steps must be in [1, ${MAX_STEPS}]`);
|
|
606
|
+
if (timeoutMs !== null && (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 900000)) throw new Error('timeout_ms must be null or in [100,900000]');
|
|
607
|
+
const deadline = createDeadline({ timeoutMs });
|
|
608
|
+
const deadlineExpired = () => timeoutMs !== null && deadlineState(deadline).expired;
|
|
609
|
+
const failDeadline = async (ctx, journal, plan, stepId = null) => {
|
|
610
|
+
await appendEvent(ctx, journal, signingKeyPath, 'task_failed', {
|
|
611
|
+
stepId,
|
|
612
|
+
payload: { reason: 'deadline_exceeded', deadline }
|
|
613
|
+
});
|
|
614
|
+
return deriveState(plan, journal);
|
|
615
|
+
};
|
|
595
616
|
const session = await codingRuntime.sessionStatus();
|
|
596
617
|
if (!session.armed) throw new Error(`coding plane is disarmed: ${session.reason}`);
|
|
597
618
|
const ctx = taskContext(policy, taskId);
|
|
@@ -610,10 +631,14 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
|
|
|
610
631
|
if (await consumeCancel(ctx, journal, signingKeyPath, verifyKeyPath)) {
|
|
611
632
|
derived = deriveState(plan, journal); return { status: 'cancelled', task_id: taskId, ...derived };
|
|
612
633
|
}
|
|
613
|
-
await appendEvent(ctx, journal, signingKeyPath, 'run_started', { payload: { max_steps: maxSteps } });
|
|
634
|
+
await appendEvent(ctx, journal, signingKeyPath, 'run_started', { payload: { max_steps: maxSteps, deadline } });
|
|
614
635
|
derived = deriveState(plan, journal);
|
|
615
636
|
let consumed = 0;
|
|
616
637
|
while (consumed < maxSteps) {
|
|
638
|
+
if (deadlineExpired()) {
|
|
639
|
+
derived = await failDeadline(ctx, journal, plan);
|
|
640
|
+
return { status: 'failed', task_id: taskId, ...derived, deadline };
|
|
641
|
+
}
|
|
617
642
|
if (await consumeCancel(ctx, journal, signingKeyPath, verifyKeyPath)) {
|
|
618
643
|
derived = deriveState(plan, journal); return { status: 'cancelled', task_id: taskId, ...derived };
|
|
619
644
|
}
|
|
@@ -638,7 +663,9 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
|
|
|
638
663
|
derived = deriveState(plan, journal);
|
|
639
664
|
consumed += 1;
|
|
640
665
|
try {
|
|
641
|
-
const
|
|
666
|
+
const remainingMs = timeoutMs === null ? null : Math.max(100, Math.floor(deadlineState(deadline).remaining_ms));
|
|
667
|
+
const boundedStep = remainingMs === null ? next : { ...next, timeout_ms: next.timeout_ms === null || next.timeout_ms === undefined ? remainingMs : Math.min(next.timeout_ms, remainingMs) };
|
|
668
|
+
const executed = await executeStep(codingRuntime, plan, boundedStep, attempt);
|
|
642
669
|
if (!executed.outcome.valid) {
|
|
643
670
|
await appendEvent(ctx, journal, signingKeyPath, 'task_paused', {
|
|
644
671
|
stepId: next.step_id, payload: { reason: 'post_execute_receipt_mismatch', detail: executed.outcome.reason }
|
|
@@ -646,6 +673,10 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
|
|
|
646
673
|
derived = deriveState(plan, journal);
|
|
647
674
|
return { status: 'paused', task_id: taskId, ...derived };
|
|
648
675
|
}
|
|
676
|
+
if (deadlineExpired()) {
|
|
677
|
+
derived = await failDeadline(ctx, journal, plan, next.step_id);
|
|
678
|
+
return { status: 'failed', task_id: taskId, ...derived, deadline };
|
|
679
|
+
}
|
|
649
680
|
if (executed.outcome.success) {
|
|
650
681
|
await appendEvent(ctx, journal, signingKeyPath, 'step_succeeded', {
|
|
651
682
|
stepId: next.step_id, payload: { ...executed.outcome.payload, attempt, recovered: false }
|
|
@@ -664,6 +695,10 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
|
|
|
664
695
|
}
|
|
665
696
|
derived = deriveState(plan, journal);
|
|
666
697
|
} catch (error) {
|
|
698
|
+
if (deadlineExpired()) {
|
|
699
|
+
derived = await failDeadline(ctx, journal, plan, next.step_id);
|
|
700
|
+
return { status: 'failed', task_id: taskId, ...derived, deadline };
|
|
701
|
+
}
|
|
667
702
|
const checked = await codingRuntime.verifyReceiptDetailed(receiptId);
|
|
668
703
|
if (checked.ok) {
|
|
669
704
|
const outcome = receiptOutcome(next, checked, requestSha256);
|
|
@@ -717,7 +752,7 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
|
|
|
717
752
|
}
|
|
718
753
|
}
|
|
719
754
|
}
|
|
720
|
-
return { status: 'yielded', task_id: taskId, ...derived };
|
|
755
|
+
return { status: 'yielded', task_id: taskId, ...derived, deadline };
|
|
721
756
|
} finally { await releaseLock(ctx); }
|
|
722
757
|
}
|
|
723
758
|
|
|
@@ -13,7 +13,7 @@ const symbolName=z.string().regex(/^[A-Za-z_$][A-Za-z0-9_$]{0,127}$/);
|
|
|
13
13
|
|
|
14
14
|
export const codingInspectInputSchema=z.discriminatedUnion('op',[
|
|
15
15
|
z.strictObject({op:z.literal('stat'),root_id:id,path:rel512.default('.'),hash:z.boolean().default(false)}),
|
|
16
|
-
z.strictObject({op:z.literal('search'),root_id:id,path:rel512.default('.'),pattern:z.string().min(1).max(256),target:z.enum(['content','path']).default('content'),mode:z.enum(['literal','regex']).default('literal'),case_sensitive:z.boolean().default(false),max_results:z.number().int().min(1).max(1000).default(200),max_file_bytes:z.number().int().min(1).max(4*1024*1024).default(262144),max_scan_bytes:z.number().int().min(1).max(64*1024*1024).default(4*1024*1024),page_size:z.number().int().min(1).max(500).default(100),cursor}),
|
|
16
|
+
z.strictObject({op:z.literal('search'),root_id:id,path:rel512.default('.'),pattern:z.string().min(1).max(256),target:z.enum(['content','path']).default('content'),mode:z.enum(['literal','regex','fuzzy']).default('literal'),case_sensitive:z.boolean().default(false),max_results:z.number().int().min(1).max(1000).default(200),max_file_bytes:z.number().int().min(1).max(4*1024*1024).default(262144),max_scan_bytes:z.number().int().min(1).max(64*1024*1024).default(4*1024*1024),page_size:z.number().int().min(1).max(500).default(100),cursor,fuzzy_timeout_ms:z.number().int().min(100).max(30000).default(30000)}),
|
|
17
17
|
z.strictObject({op:z.literal('files_read'),root_id:id,files:z.array(z.strictObject({path:rel512,encoding:z.enum(['utf8','base64']).default('utf8')})).min(1).max(32),max_total_bytes:z.number().int().min(1).max(2097152).default(1048576)}),
|
|
18
18
|
z.strictObject({op:z.literal('symbol_outline'),root_id:id,path:rel512,max_symbols:z.number().int().min(1).max(500).default(200)}),
|
|
19
19
|
z.strictObject({op:z.literal('symbol_definition'),root_id:id,path:rel512,line:nonneg,column:nonneg,max_results:z.number().int().min(1).max(200).default(50)}),
|
|
@@ -26,7 +26,7 @@ export const codingInspectInputSchema=z.discriminatedUnion('op',[
|
|
|
26
26
|
export const machineFsGatewayInputSchema=z.discriminatedUnion('op',[
|
|
27
27
|
z.strictObject({op:z.literal('stat'),root_id:id,path:rel.default('.'),hash:z.boolean().default(false)}),
|
|
28
28
|
z.strictObject({op:z.literal('list'),root_id:id,path:rel.default('.'),cursor,limit:z.number().int().min(1).max(1000).default(100),hash_files:z.boolean().default(false)}),
|
|
29
|
-
z.strictObject({op:z.literal('read'),root_id:id,path:rel,offset:
|
|
29
|
+
z.strictObject({op:z.literal('read'),root_id:id,path:rel,offset:z.number().int().min(-4*1024*1024).max(4*1024*1024).default(0),max_bytes:z.number().int().min(1).max(4*1024*1024).default(65536),hash_file:z.boolean().default(false),timeout_ms:z.number().int().min(100).max(180000).default(30000)}),
|
|
30
30
|
z.strictObject({op:z.literal('write'),root_id:id,path:rel,content:z.string().max(12*1024*1024),encoding:z.enum(['utf8','base64']).default('utf8'),expected_sha256:z.string().regex(/^(?:absent|[0-9a-f]{64})$/)}),
|
|
31
31
|
z.strictObject({op:z.literal('edit_exact'),root_id:id,path:rel,expected_sha256:hex64,old_string:z.string().min(1).max(262144),new_string:z.string().max(524288),expected_replacements:z.number().int().min(1).max(64).default(1)}),
|
|
32
32
|
z.strictObject({op:z.literal('mkdir'),root_id:id,path:rel}),
|
|
@@ -69,7 +69,7 @@ async function invoke(op,runtime,name,call){
|
|
|
69
69
|
async function codingInspect(args,codingRuntime,semanticRuntime){
|
|
70
70
|
switch(args.op){
|
|
71
71
|
case 'stat': return invoke(args.op,codingRuntime,'coding',r=>r.stat({rootId:args.root_id,relativePath:args.path,hash:args.hash}));
|
|
72
|
-
case 'search': return invoke(args.op,codingRuntime,'coding',r=>r.search({rootId:args.root_id,path:args.path,pattern:args.pattern,target:args.target,mode:args.mode,caseSensitive:args.case_sensitive,maxResults:args.max_results,maxFileBytes:args.max_file_bytes,maxScanBytes:args.max_scan_bytes,pageSize:args.page_size,cursor:args.cursor}));
|
|
72
|
+
case 'search': return invoke(args.op,codingRuntime,'coding',r=>r.search({rootId:args.root_id,path:args.path,pattern:args.pattern,target:args.target,mode:args.mode,caseSensitive:args.case_sensitive,maxResults:args.max_results,maxFileBytes:args.max_file_bytes,maxScanBytes:args.max_scan_bytes,pageSize:args.page_size,cursor:args.cursor,fuzzyTimeoutMs:args.fuzzy_timeout_ms}));
|
|
73
73
|
case 'files_read': return invoke(args.op,codingRuntime,'coding',r=>r.filesRead({rootId:args.root_id,files:args.files,maxTotalBytes:args.max_total_bytes}));
|
|
74
74
|
case 'symbol_outline': return invoke(args.op,semanticRuntime,'semantic',r=>r.outline({rootId:args.root_id,relativePath:args.path,maxSymbols:args.max_symbols}));
|
|
75
75
|
case 'symbol_definition': return invoke(args.op,semanticRuntime,'semantic',r=>r.definition({rootId:args.root_id,relativePath:args.path,line:args.line,column:args.column,maxResults:args.max_results}));
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const ID=/^[A-Za-z0-9._:-]{1,128}$/;
|
|
2
|
+
|
|
3
|
+
export function createDeadline({timeoutMs=null, nowMs=Date.now(), id=null}={}) {
|
|
4
|
+
if(timeoutMs!==null && (!Number.isInteger(timeoutMs)||timeoutMs<1)) throw new Error('deadline timeout_ms invalid');
|
|
5
|
+
if(!Number.isFinite(nowMs)) throw new Error('deadline now_ms invalid');
|
|
6
|
+
const deadlineMs=timeoutMs===null?null:nowMs+timeoutMs;
|
|
7
|
+
return Object.freeze({
|
|
8
|
+
schema:'deadbyte.deadline.v1',
|
|
9
|
+
id:id===null?null:validateId(id),
|
|
10
|
+
created_at_ms:nowMs,
|
|
11
|
+
timeout_ms:timeoutMs,
|
|
12
|
+
deadline_ms:deadlineMs
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function deadlineState(deadline,{nowMs=Date.now()}={}) {
|
|
17
|
+
if(!deadline||deadline.schema!=='deadbyte.deadline.v1') throw new Error('deadline invalid');
|
|
18
|
+
if(!Number.isFinite(nowMs)) throw new Error('deadline now_ms invalid');
|
|
19
|
+
if(deadline.deadline_ms===null) return Object.freeze({expired:false,remaining_ms:null});
|
|
20
|
+
const remainingMs=deadline.deadline_ms-nowMs;
|
|
21
|
+
return Object.freeze({expired:remainingMs<=0,remaining_ms:Math.max(0,remainingMs)});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createCancellationToken({id=null}={}) {
|
|
25
|
+
const controller=new AbortController();
|
|
26
|
+
let requestedAtMs=null;
|
|
27
|
+
let reason=null;
|
|
28
|
+
const request=(value='cancelled')=>{
|
|
29
|
+
if(controller.signal.aborted)return false;
|
|
30
|
+
requestedAtMs=Date.now();
|
|
31
|
+
reason=String(value);
|
|
32
|
+
controller.abort(reason);
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
schema:'deadbyte.cancellation.v1',
|
|
37
|
+
id:id===null?null:validateId(id),
|
|
38
|
+
signal:controller.signal,
|
|
39
|
+
request,
|
|
40
|
+
snapshot:()=>Object.freeze({
|
|
41
|
+
requested:controller.signal.aborted,
|
|
42
|
+
requested_at_ms:requestedAtMs,
|
|
43
|
+
reason
|
|
44
|
+
})
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function composeAbortSignal({deadline=null,cancellation=null}={}) {
|
|
49
|
+
const controller=new AbortController();
|
|
50
|
+
let timeout=null;
|
|
51
|
+
const abort=(reason)=>{
|
|
52
|
+
if(!controller.signal.aborted)controller.abort(reason);
|
|
53
|
+
};
|
|
54
|
+
if(cancellation?.signal){
|
|
55
|
+
if(cancellation.signal.aborted)abort(cancellation.signal.reason??'cancelled');
|
|
56
|
+
else cancellation.signal.addEventListener('abort',()=>abort(cancellation.signal.reason??'cancelled'),{once:true});
|
|
57
|
+
}
|
|
58
|
+
if(deadline?.deadline_ms!==null&&deadline?.deadline_ms!==undefined){
|
|
59
|
+
const remaining=Math.max(0,deadline.deadline_ms-Date.now());
|
|
60
|
+
if(remaining===0)abort('deadline_exceeded');
|
|
61
|
+
else timeout=setTimeout(()=>abort('deadline_exceeded'),remaining);
|
|
62
|
+
timeout?.unref?.();
|
|
63
|
+
}
|
|
64
|
+
const close=()=>{if(timeout)clearTimeout(timeout);};
|
|
65
|
+
return Object.freeze({signal:controller.signal,close});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function classifyTermination({deadlineExceeded=false,cancellationRequested=false,physicallyTerminated=false,verified=false}={}) {
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
deadline_exceeded:deadlineExceeded===true,
|
|
71
|
+
cancellation_requested:cancellationRequested===true,
|
|
72
|
+
physical_termination:physicallyTerminated===true,
|
|
73
|
+
verified:verified===true,
|
|
74
|
+
terminal_semantics:
|
|
75
|
+
verified ? (physicallyTerminated ? 'verified_termination' : 'verified_stop')
|
|
76
|
+
: physicallyTerminated ? 'termination_unverified'
|
|
77
|
+
: cancellationRequested ? 'cancellation_requested'
|
|
78
|
+
: deadlineExceeded ? 'deadline_exceeded'
|
|
79
|
+
: 'running'
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateId(id){if(typeof id!=='string'||!ID.test(id))throw new Error('deadline/cancellation id invalid');return id;}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const clamp=(value,min,max)=>Math.min(max,Math.max(min,value));
|
|
2
|
+
|
|
3
|
+
function boundedLevenshtein(a,b,cutoff=Number.POSITIVE_INFINITY){
|
|
4
|
+
if(a===b)return 0;
|
|
5
|
+
if(a.length===0)return b.length;
|
|
6
|
+
if(b.length===0)return a.length;
|
|
7
|
+
if(a.length>b.length){const t=a;a=b;b=t;}
|
|
8
|
+
const previous=new Uint16Array(a.length+1);
|
|
9
|
+
const current=new Uint16Array(a.length+1);
|
|
10
|
+
for(let i=0;i<=a.length;i+=1)previous[i]=i;
|
|
11
|
+
const limit=Number.isFinite(cutoff)?Math.floor(cutoff):Number.MAX_SAFE_INTEGER;
|
|
12
|
+
for(let row=1;row<=b.length;row+=1){
|
|
13
|
+
current[0]=row;
|
|
14
|
+
let rowMin=current[0];
|
|
15
|
+
const ch=b.charCodeAt(row-1);
|
|
16
|
+
for(let col=1;col<=a.length;col+=1){
|
|
17
|
+
const cost=a.charCodeAt(col-1)===ch?0:1;
|
|
18
|
+
const value=Math.min(current[col-1]+1,previous[col]+1,previous[col-1]+cost);
|
|
19
|
+
current[col]=value;
|
|
20
|
+
if(value<rowMin)rowMin=value;
|
|
21
|
+
}
|
|
22
|
+
if(rowMin>limit)return limit+1;
|
|
23
|
+
previous.set(current);
|
|
24
|
+
}
|
|
25
|
+
return previous[a.length];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cheapPositionalScore(text,query,start,length){
|
|
29
|
+
const overlap=Math.min(length,query.length);
|
|
30
|
+
if(overlap<=0)return -1;
|
|
31
|
+
let same=0;
|
|
32
|
+
for(let i=0;i<overlap;i+=1)if(text.charCodeAt(start+i)===query.charCodeAt(i))same+=1;
|
|
33
|
+
const lengthPenalty=Math.abs(length-query.length)/Math.max(1,query.length);
|
|
34
|
+
return (same/overlap)-lengthPenalty*0.20;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function candidateLengths(queryLength,textLength){
|
|
38
|
+
if(textLength<=queryLength)return [textLength];
|
|
39
|
+
const delta=clamp(Math.floor(queryLength*0.25),2,32);
|
|
40
|
+
return [...new Set([
|
|
41
|
+
Math.max(1,queryLength-delta),
|
|
42
|
+
queryLength,
|
|
43
|
+
Math.min(textLength,queryLength+delta)
|
|
44
|
+
])];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function fuzzyIndexOf(text,query){
|
|
48
|
+
if(typeof text!=='string'||typeof query!=='string')throw new Error('fuzzy search requires text and query strings');
|
|
49
|
+
if(query.length===0)return {start:0,end:0,value:'',distance:0,similarity:1};
|
|
50
|
+
if(text.length===0)return {start:0,end:0,value:'',distance:query.length,similarity:0};
|
|
51
|
+
|
|
52
|
+
const lengths=candidateLengths(query.length,text.length);
|
|
53
|
+
const stride=query.length<=32?1:Math.max(1,Math.floor(query.length/4));
|
|
54
|
+
const ranked=[];
|
|
55
|
+
const seen=new Set();
|
|
56
|
+
for(let start=0;start<text.length;start+=stride){
|
|
57
|
+
for(const length of lengths){
|
|
58
|
+
if(start+length>text.length)continue;
|
|
59
|
+
const key=`${start}:${length}`;
|
|
60
|
+
if(seen.has(key))continue;
|
|
61
|
+
seen.add(key);
|
|
62
|
+
ranked.push({start,length,score:cheapPositionalScore(text,query,start,length)});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const tailStart=Math.max(0,text.length-query.length);
|
|
66
|
+
for(const start of [0,tailStart]){
|
|
67
|
+
for(const length of lengths){
|
|
68
|
+
if(start<0||start+length>text.length)continue;
|
|
69
|
+
const key=`${start}:${length}`;
|
|
70
|
+
if(seen.has(key))continue;
|
|
71
|
+
seen.add(key);
|
|
72
|
+
ranked.push({start,length,score:cheapPositionalScore(text,query,start,length)});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ranked.sort((a,b)=>b.score-a.score||a.start-b.start||a.length-b.length);
|
|
77
|
+
const shortlist=ranked.slice(0,64);
|
|
78
|
+
const refined=new Set();
|
|
79
|
+
for(const item of shortlist){
|
|
80
|
+
const radius=Math.max(1,Math.floor(stride/2));
|
|
81
|
+
for(let delta=-radius;delta<=radius;delta+=1){
|
|
82
|
+
const start=item.start+delta;
|
|
83
|
+
if(start<0||start>=text.length)continue;
|
|
84
|
+
for(const length of lengths){
|
|
85
|
+
if(start+length>text.length)continue;
|
|
86
|
+
refined.add(`${start}:${length}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if(refined.size===0)refined.add(`0:${Math.min(text.length,query.length)}`);
|
|
91
|
+
|
|
92
|
+
let best=null;
|
|
93
|
+
for(const key of refined){
|
|
94
|
+
const [startText,lengthText]=key.split(':');
|
|
95
|
+
const start=Number(startText);
|
|
96
|
+
const length=Number(lengthText);
|
|
97
|
+
const value=text.slice(start,start+length);
|
|
98
|
+
const cutoff=best?.distance??Number.POSITIVE_INFINITY;
|
|
99
|
+
const distance=boundedLevenshtein(value,query,cutoff);
|
|
100
|
+
if(best&&distance>best.distance)continue;
|
|
101
|
+
const similarity=1-(distance/Math.max(query.length,value.length));
|
|
102
|
+
const candidate={start,end:start+value.length,value,distance,similarity};
|
|
103
|
+
if(!best||
|
|
104
|
+
candidate.distance<best.distance||
|
|
105
|
+
(candidate.distance===best.distance&&candidate.similarity>best.similarity)||
|
|
106
|
+
(candidate.distance===best.distance&&candidate.similarity===best.similarity&&candidate.start<best.start)){
|
|
107
|
+
best=candidate;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return best??{start:0,end:Math.min(text.length,query.length),value:text.slice(0,query.length),
|
|
111
|
+
distance:boundedLevenshtein(text.slice(0,query.length),query),similarity:0};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getSimilarityRatio(a,b){
|
|
115
|
+
if(a.length===0&&b.length===0)return 1;
|
|
116
|
+
return 1-(boundedLevenshtein(a,b)/Math.max(a.length,b.length));
|
|
117
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
2
|
+
import { fuzzyIndexOf } from './fuzzy-search-core.mjs';
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
parentPort.postMessage({ ok:true, result:fuzzyIndexOf(workerData.text,workerData.query) });
|
|
6
|
+
} catch (error) {
|
|
7
|
+
parentPort.postMessage({ ok:false, error:String(error?.stack??error) });
|
|
8
|
+
}
|