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.
Files changed (67) hide show
  1. package/MANIFEST.SHA256 +66 -41
  2. package/PROVENANCE.json +3 -3
  3. package/RELEASE.json +3 -3
  4. package/package.json +1 -1
  5. package/scripts/autonomous-build.ps1 +1 -0
  6. package/scripts/autonomous-tests.ps1 +1 -0
  7. package/scripts/check-agent-backend-r46.mjs +77 -0
  8. package/scripts/check-atomic-resource-r46.mjs +84 -0
  9. package/scripts/check-autonomous-race-r46.mjs +160 -0
  10. package/scripts/check-coding-task-deadline-r46.mjs +59 -0
  11. package/scripts/check-deadline-cancellation-r46.mjs +49 -0
  12. package/scripts/check-process-deadline-r46.mjs +52 -0
  13. package/scripts/check-r46-background.mjs +5 -0
  14. package/scripts/extract-r46-failures.mjs +5 -0
  15. package/scripts/full-regression-r46.mjs +20 -12
  16. package/scripts/gate-windows-r46.ps1 +2 -0
  17. package/scripts/inspect-r46-active.mjs +2 -0
  18. package/scripts/r47-core-failures.mjs +1 -0
  19. package/scripts/release-manifest.mjs +1 -1
  20. package/scripts/run-r46-full-regression-background.mjs +7 -0
  21. package/scripts/run-r46-windows-gate-background.mjs +6 -0
  22. package/scripts/run-r46-windows-gate-background.ps1 +10 -0
  23. package/scripts/windows-gate-evidence-r46.mjs +4 -3
  24. package/scripts/workload-contract-regression-r47.mjs +14 -0
  25. package/scripts/workload-stream-regression.mjs +16 -0
  26. package/scripts/workload-substrate-r47-regression.mjs +12 -0
  27. package/src/agent-backend-r44.mjs +75 -10
  28. package/src/atomic-mutation.mjs +55 -0
  29. package/src/autonomous-mcp-tools.mjs +4 -3
  30. package/src/autonomous-output-contracts.mjs +16 -3
  31. package/src/autonomous-runtime.mjs +83 -30
  32. package/src/coding-mcp-tools.mjs +6 -6
  33. package/src/coding-output-contracts.mjs +20 -5
  34. package/src/coding-plane.mjs +3 -24
  35. package/src/coding-search.mjs +52 -15
  36. package/src/coding-task-runtime.mjs +41 -6
  37. package/src/compact-gateway-mcp-tools.mjs +3 -3
  38. package/src/deadline-cancellation.mjs +83 -0
  39. package/src/fuzzy-search-core.mjs +117 -0
  40. package/src/fuzzy-search-worker.mjs +8 -0
  41. package/src/fuzzy-search.mjs +34 -0
  42. package/src/machine-fs-mcp-tools.mjs +1 -1
  43. package/src/machine-fs-output-contracts.mjs +1 -1
  44. package/src/machine-fs-smoke-client.mjs +1 -1
  45. package/src/machine-fs.mjs +10 -29
  46. package/src/mcp-server.mjs +6 -0
  47. package/src/observation-smoke-client.mjs +1 -1
  48. package/src/process-mcp-tools.mjs +11 -3
  49. package/src/process-output-contracts.mjs +23 -0
  50. package/src/process-runtime.mjs +54 -5
  51. package/src/process-smoke-client.mjs +1 -1
  52. package/src/resource-identity.mjs +59 -0
  53. package/src/runtime-update.mjs +526 -162
  54. package/src/workload-substrate.mjs +87 -0
  55. package/test/coding-runtime.test.mjs +12 -0
  56. package/test/coding-search-r36.test.mjs +7 -0
  57. package/test/full-max-catalog-r46.test.mjs +2 -2
  58. package/test/fuzzy-search-r46.test.mjs +54 -0
  59. package/test/helpers/autonomous-r34-fixture.mjs +10 -1
  60. package/test/machine-fs-mcp-tools.test.mjs +2 -0
  61. package/test/machine-fs-runtime.test.mjs +6 -0
  62. package/test/process-mcp-tools.test.mjs +5 -2
  63. package/test/process-runtime.test.mjs +18 -1
  64. package/test/r45-finalization.test.mjs +1 -1
  65. package/test/release-version.test.mjs +1 -1
  66. package/test/runtime-self-update.test.mjs +118 -1
  67. package/test/workload-substrate.test.mjs +22 -0
@@ -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() })).max(500).optional(),
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({
@@ -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
- const before = await targetState(target);
265
- checkExpected(before, expectedSha256);
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([
@@ -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.v2',root_id:rootId,path,target,mode,pattern,
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
- const matcher=compileMatcher({pattern,mode,caseSensitive});
77
- const query=normalizeQuery({rootId,path,target,mode,pattern,caseSensitive,maxResults,maxFileBytes,maxScanBytes,pageSize});
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
- if(matcher.path(entry.path)){
96
- matches.push({path:entry.path,line:null,column:null,preview:entry.path,file_sha256:null});
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
- const found=matcher.all(text);
113
- for(const hit of found){
114
- const lc=lineColumn(text,hit.index);
115
- matches.push({path:entry.path,line:lc.line,column:lc.column,preview:preview(text,hit.index,hit.length),file_sha256:fileSha});
116
- if(matches.length>=maxResults){truncated=true;truncationReason='max_results';break;}
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 pageMatches=matches.slice(offset,offset+pageSize);
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
- return { status: 'ok', task_id: taskId, plan_sha256: plan.plan_sha256, ...deriveState(plan, events), journal_verified: true };
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
- events: all.slice(fromSeq - 1, fromSeq - 1 + limit)
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 executed = await executeStep(codingRuntime, plan, next, attempt);
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:nonneg.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)}),
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
+ }