open-agents-ai 0.187.554 → 0.187.555

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/dist/index.js CHANGED
@@ -2877,6 +2877,43 @@ Annotated files:`);
2877
2877
  }
2878
2878
  });
2879
2879
 
2880
+ // packages/execution/dist/tools/edit-metadata.js
2881
+ import { createHash } from "node:crypto";
2882
+ function contentHash(content) {
2883
+ return createHash("sha256").update(content, "utf8").digest("hex");
2884
+ }
2885
+ function normalizeExpectedHash(value2) {
2886
+ if (typeof value2 !== "string")
2887
+ return null;
2888
+ const raw = value2.trim();
2889
+ if (!raw)
2890
+ return null;
2891
+ const stripped = raw.replace(/^sha256:/i, "").toLowerCase();
2892
+ return /^[a-f0-9]{64}$/.test(stripped) ? stripped : null;
2893
+ }
2894
+ function extractExpectedHash(args) {
2895
+ return normalizeExpectedHash(args["expected_hash"] ?? args["expectedHash"] ?? args["before_hash"] ?? args["beforeHash"]);
2896
+ }
2897
+ function hashMismatchMessage(filePath, expectedHash, actualHash) {
2898
+ return [
2899
+ `Refusing to edit ${filePath}: expected_hash does not match current file content.`,
2900
+ `expected_hash: ${expectedHash}`,
2901
+ `current_hash: ${actualHash}`,
2902
+ `Re-read the file and rebuild the edit from the current content before retrying.`
2903
+ ].join("\n");
2904
+ }
2905
+ function fileContextHeader(filePath, content, range) {
2906
+ const totalLines = content.split("\n").length;
2907
+ const hash = contentHash(content);
2908
+ const shown = range && (range.offset !== void 0 || range.limit !== void 0) ? ` showing=${range.offset ?? 1}-${range.limit ? (range.offset ?? 1) + range.limit - 1 : "end"}` : "";
2909
+ return `[FILE CONTEXT path=${filePath} sha256=${hash} lines=${totalLines}${shown}]`;
2910
+ }
2911
+ var init_edit_metadata = __esm({
2912
+ "packages/execution/dist/tools/edit-metadata.js"() {
2913
+ "use strict";
2914
+ }
2915
+ });
2916
+
2880
2917
  // packages/execution/dist/tools/file-read.js
2881
2918
  import { readFile } from "node:fs/promises";
2882
2919
  import { resolve as resolve2 } from "node:path";
@@ -2964,6 +3001,7 @@ var init_file_read = __esm({
2964
3001
  "packages/execution/dist/tools/file-read.js"() {
2965
3002
  "use strict";
2966
3003
  init_semantic_map();
3004
+ init_edit_metadata();
2967
3005
  SIG_PATTERNS = [
2968
3006
  /^\s*(export\s+)?(async\s+)?function\s+\w+/,
2969
3007
  /^\s*(export\s+)?(abstract\s+)?class\s+\w+/,
@@ -3019,10 +3057,15 @@ var init_file_read = __esm({
3019
3057
  const startIdx = Math.max(0, (offset ?? 1) - 1);
3020
3058
  lines = lines.slice(startIdx, limit ? startIdx + limit : void 0);
3021
3059
  const numbered2 = lines.map((line, i2) => `${String(startIdx + i2 + 1).padStart(6)} | ${line}`).join("\n");
3060
+ const hash = contentHash(content);
3022
3061
  return {
3023
3062
  success: true,
3024
- output: numbered2,
3025
- durationMs: performance.now() - start2
3063
+ output: `${fileContextHeader(filePath, content, { offset, limit })}
3064
+ ${numbered2}`,
3065
+ durationMs: performance.now() - start2,
3066
+ mutated: false,
3067
+ mutatedFiles: [],
3068
+ beforeHash: hash
3026
3069
  };
3027
3070
  }
3028
3071
  touchFile(this.workingDir, filePath);
@@ -3031,15 +3074,23 @@ var init_file_read = __esm({
3031
3074
  const notes2 = getFileNotes(this.workingDir, filePath);
3032
3075
  return {
3033
3076
  success: true,
3034
- output: buildStructuralPreview(lines, filePath, maxLines) + (notes2 ? "\n\n" + notes2 : ""),
3035
- durationMs: performance.now() - start2
3077
+ output: `${fileContextHeader(filePath, content)}
3078
+ ${buildStructuralPreview(lines, filePath, maxLines)}${notes2 ? "\n\n" + notes2 : ""}`,
3079
+ durationMs: performance.now() - start2,
3080
+ mutated: false,
3081
+ mutatedFiles: [],
3082
+ beforeHash: contentHash(content)
3036
3083
  };
3037
3084
  }
3038
3085
  const numbered = lines.map((line, i2) => `${String(i2 + 1).padStart(6)} | ${line}`).join("\n");
3039
3086
  return {
3040
3087
  success: true,
3041
- output: numbered,
3042
- durationMs: performance.now() - start2
3088
+ output: `${fileContextHeader(filePath, content)}
3089
+ ${numbered}`,
3090
+ durationMs: performance.now() - start2,
3091
+ mutated: false,
3092
+ mutatedFiles: [],
3093
+ beforeHash: contentHash(content)
3043
3094
  };
3044
3095
  } catch (error) {
3045
3096
  const errMsg = error instanceof Error ? error.message : String(error);
@@ -3223,6 +3274,7 @@ var init_file_write = __esm({
3223
3274
  "packages/execution/dist/tools/file-write.js"() {
3224
3275
  "use strict";
3225
3276
  init_change_log();
3277
+ init_edit_metadata();
3226
3278
  FileWriteTool = class {
3227
3279
  name = "file_write";
3228
3280
  description = "Write content to a file, creating directories as needed";
@@ -3230,7 +3282,15 @@ var init_file_write = __esm({
3230
3282
  type: "object",
3231
3283
  properties: {
3232
3284
  path: { type: "string", description: "Absolute or relative file path" },
3233
- content: { type: "string", description: "File content to write" }
3285
+ content: { type: "string", description: "File content to write" },
3286
+ overwrite: {
3287
+ type: "boolean",
3288
+ description: "Required when overwriting an existing file with different content."
3289
+ },
3290
+ expected_hash: {
3291
+ type: "string",
3292
+ description: "SHA-256 hash from the most recent file_read. Required with overwrite=true for existing files."
3293
+ }
3234
3294
  },
3235
3295
  required: ["path", "content"]
3236
3296
  };
@@ -3241,6 +3301,8 @@ var init_file_write = __esm({
3241
3301
  async execute(args) {
3242
3302
  const filePath = extractWritePath(args);
3243
3303
  const content = args["content"] ?? args["text"] ?? args["data"];
3304
+ const overwrite = args["overwrite"] === true || args["overwriteExisting"] === true;
3305
+ const expectedHash = extractExpectedHash(args);
3244
3306
  const start2 = performance.now();
3245
3307
  if (!filePath) {
3246
3308
  return {
@@ -3261,14 +3323,57 @@ var init_file_write = __esm({
3261
3323
  try {
3262
3324
  const fullPath = resolve3(this.workingDir, filePath);
3263
3325
  const isNew = !existsSync6(fullPath);
3326
+ const newHash = contentHash(content);
3264
3327
  if (!isNew) {
3265
3328
  try {
3266
3329
  const existing = await readFile2(fullPath, "utf-8");
3330
+ const beforeHash = contentHash(existing);
3267
3331
  if (existing === content) {
3268
3332
  return {
3269
3333
  success: true,
3270
3334
  output: `[NO-OP — file ${filePath} already contains these exact bytes (${content.length}B). Skipped redundant write. If you intended to make a change, the content is identical to disk — you may be replaying an earlier plan. Update todo_write and proceed to the next pending task.]`,
3271
- durationMs: performance.now() - start2
3335
+ durationMs: performance.now() - start2,
3336
+ mutated: false,
3337
+ mutatedFiles: [],
3338
+ noop: true,
3339
+ beforeHash,
3340
+ afterHash: beforeHash
3341
+ };
3342
+ }
3343
+ if (!overwrite) {
3344
+ return {
3345
+ success: false,
3346
+ output: "",
3347
+ error: `Refusing to overwrite existing file ${filePath} without overwrite=true. Use file_edit/file_patch for targeted changes, or re-read the file and call file_write with overwrite=true and expected_hash=${beforeHash}.`,
3348
+ durationMs: performance.now() - start2,
3349
+ mutated: false,
3350
+ mutatedFiles: [],
3351
+ beforeHash,
3352
+ afterHash: beforeHash
3353
+ };
3354
+ }
3355
+ if (!expectedHash) {
3356
+ return {
3357
+ success: false,
3358
+ output: "",
3359
+ error: `Refusing to overwrite existing file ${filePath} without expected_hash. Re-read the file first; file_read returns a sha256 header to pass as expected_hash.`,
3360
+ durationMs: performance.now() - start2,
3361
+ mutated: false,
3362
+ mutatedFiles: [],
3363
+ beforeHash,
3364
+ afterHash: beforeHash
3365
+ };
3366
+ }
3367
+ if (expectedHash !== beforeHash) {
3368
+ return {
3369
+ success: false,
3370
+ output: "",
3371
+ error: hashMismatchMessage(filePath, expectedHash, beforeHash),
3372
+ durationMs: performance.now() - start2,
3373
+ mutated: false,
3374
+ mutatedFiles: [],
3375
+ beforeHash,
3376
+ afterHash: beforeHash
3272
3377
  };
3273
3378
  }
3274
3379
  } catch {
@@ -3284,8 +3389,13 @@ var init_file_write = __esm({
3284
3389
  });
3285
3390
  return {
3286
3391
  success: true,
3287
- output: `Written ${content.length} bytes to ${fullPath}`,
3288
- durationMs: performance.now() - start2
3392
+ output: `${isNew ? "Created" : "Overwrote"} ${content.length} bytes at ${fullPath} (sha256 ${newHash})`,
3393
+ durationMs: performance.now() - start2,
3394
+ mutated: true,
3395
+ mutatedFiles: [filePath],
3396
+ noop: false,
3397
+ beforeHash: isNew ? void 0 : expectedHash ?? void 0,
3398
+ afterHash: newHash
3289
3399
  };
3290
3400
  } catch (error) {
3291
3401
  return {
@@ -4211,6 +4321,7 @@ var init_file_edit = __esm({
4211
4321
  "use strict";
4212
4322
  init_change_log();
4213
4323
  init_edit_snippet_finder();
4324
+ init_edit_metadata();
4214
4325
  FileEditTool = class {
4215
4326
  name = "file_edit";
4216
4327
  description = "Make a precise edit to a file by replacing an exact string match. The old_string must be unique in the file unless replace_all is true. Use replace_all to rename variables or change repeated patterns throughout the file.";
@@ -4229,6 +4340,10 @@ var init_file_edit = __esm({
4229
4340
  replace_all: {
4230
4341
  type: "boolean",
4231
4342
  description: "Replace ALL occurrences instead of just the first. Use for variable renames, import path changes, etc. Default: false"
4343
+ },
4344
+ expected_hash: {
4345
+ type: "string",
4346
+ description: "Optional SHA-256 from the most recent file_read. If provided, edit is refused when the file changed."
4232
4347
  }
4233
4348
  },
4234
4349
  required: ["path", "old_string", "new_string"]
@@ -4242,6 +4357,7 @@ var init_file_edit = __esm({
4242
4357
  const oldString = args["old_string"] ?? args["oldString"] ?? args["search"] ?? args["find"];
4243
4358
  const newString = args["new_string"] ?? args["newString"] ?? args["replace"] ?? args["replacement"];
4244
4359
  const replaceAll = args["replace_all"] === true || args["replaceAll"] === true;
4360
+ const expectedHash = extractExpectedHash(args);
4245
4361
  const start2 = performance.now();
4246
4362
  if (!filePath) {
4247
4363
  return {
@@ -4270,6 +4386,19 @@ var init_file_edit = __esm({
4270
4386
  try {
4271
4387
  const fullPath = resolve6(this.workingDir, filePath);
4272
4388
  const content = await readFile3(fullPath, "utf-8");
4389
+ const beforeHash = contentHash(content);
4390
+ if (expectedHash && expectedHash !== beforeHash) {
4391
+ return {
4392
+ success: false,
4393
+ output: "",
4394
+ error: hashMismatchMessage(filePath, expectedHash, beforeHash),
4395
+ durationMs: performance.now() - start2,
4396
+ mutated: false,
4397
+ mutatedFiles: [],
4398
+ beforeHash,
4399
+ afterHash: beforeHash
4400
+ };
4401
+ }
4273
4402
  const occurrences = countOccurrences(content, oldString);
4274
4403
  if (occurrences === 0) {
4275
4404
  const snippet = findClosestSnippet(content, oldString, 5);
@@ -4289,7 +4418,11 @@ Use the EXACT current content above to construct a working old_string. Do not re
4289
4418
  success: false,
4290
4419
  output: "",
4291
4420
  error: errorMsg,
4292
- durationMs: performance.now() - start2
4421
+ durationMs: performance.now() - start2,
4422
+ mutated: false,
4423
+ mutatedFiles: [],
4424
+ beforeHash,
4425
+ afterHash: beforeHash
4293
4426
  };
4294
4427
  }
4295
4428
  if (!replaceAll && occurrences > 1) {
@@ -4308,7 +4441,11 @@ ${s2.content}`;
4308
4441
  ${snippets}
4309
4442
 
4310
4443
  Add UNIQUE surrounding context (a function name, distinctive comment, or unique line above/below) to disambiguate. Or set replace_all=true if you want all ${occurrences} occurrences replaced identically.`,
4311
- durationMs: performance.now() - start2
4444
+ durationMs: performance.now() - start2,
4445
+ mutated: false,
4446
+ mutatedFiles: [],
4447
+ beforeHash,
4448
+ afterHash: beforeHash
4312
4449
  };
4313
4450
  }
4314
4451
  let updated;
@@ -4322,19 +4459,40 @@ Add UNIQUE surrounding context (a function name, distinctive comment, or unique
4322
4459
  editedLines = [lineNumber];
4323
4460
  updated = content.slice(0, index) + newString + content.slice(index + oldString.length);
4324
4461
  }
4462
+ const afterHash = contentHash(updated);
4463
+ const diff = buildCompactDiff(oldString, newString);
4464
+ if (afterHash === beforeHash) {
4465
+ return {
4466
+ success: true,
4467
+ output: `[NO-OP — ${filePath} already matches the requested file_edit at ${editedLines.length === 1 ? `line ${editedLines[0]}` : `${editedLines.length} locations`}.]`,
4468
+ durationMs: performance.now() - start2,
4469
+ mutated: false,
4470
+ mutatedFiles: [],
4471
+ diff,
4472
+ noop: true,
4473
+ beforeHash,
4474
+ afterHash
4475
+ };
4476
+ }
4325
4477
  await writeFile2(fullPath, updated, "utf-8");
4326
4478
  recordChange(this.workingDir, {
4327
4479
  tool: "file_edit",
4328
4480
  file: filePath,
4329
4481
  lineRange: editedLines.length > 0 ? [editedLines[0], editedLines[editedLines.length - 1]] : void 0,
4330
4482
  summary: replaceAll ? `Replaced ${editedLines.length} occurrences in ${filePath}` : `Edited ${filePath} at line ${editedLines[0]}`,
4331
- diff: buildCompactDiff(oldString, newString)
4483
+ diff
4332
4484
  });
4333
4485
  const linesInfo = editedLines.length === 1 ? `line ${editedLines[0]}` : `${editedLines.length} locations (lines ${editedLines.join(", ")})`;
4334
4486
  return {
4335
4487
  success: true,
4336
- output: `Edited ${filePath} at ${linesInfo}`,
4337
- durationMs: performance.now() - start2
4488
+ output: `Edited ${filePath} at ${linesInfo} (sha256 ${beforeHash} → ${afterHash})`,
4489
+ durationMs: performance.now() - start2,
4490
+ mutated: true,
4491
+ mutatedFiles: [filePath],
4492
+ diff,
4493
+ noop: false,
4494
+ beforeHash,
4495
+ afterHash
4338
4496
  };
4339
4497
  } catch (error) {
4340
4498
  return {
@@ -4889,7 +5047,7 @@ var init_explore_tools = __esm({
4889
5047
  memory_write: "Store a fact in persistent memory",
4890
5048
  memory_search: "Search all memories by relevance",
4891
5049
  batch_edit: "Apply multiple file edits atomically",
4892
- file_patch: "Apply unified diff patches to files",
5050
+ file_patch: "Apply version-checked line-range patches to files",
4893
5051
  git_info: "Get git status, branch, recent commits",
4894
5052
  codebase_map: "Generate overview of project structure",
4895
5053
  diagnostic: "Run project diagnostics (build, test, lint)",
@@ -5829,6 +5987,7 @@ var init_batch_edit = __esm({
5829
5987
  "use strict";
5830
5988
  init_change_log();
5831
5989
  init_edit_snippet_finder();
5990
+ init_edit_metadata();
5832
5991
  BatchEditTool = class {
5833
5992
  name = "batch_edit";
5834
5993
  description = "Make multiple precise edits across one or more files in a single call. More efficient than calling file_edit repeatedly. Each edit replaces an exact string match with uniqueness validation. Edits are applied in order within each file. Set replace_all on individual edits for bulk renames.";
@@ -5856,6 +6015,10 @@ var init_batch_edit = __esm({
5856
6015
  replace_all: {
5857
6016
  type: "boolean",
5858
6017
  description: "Replace all occurrences (for renames). Default: false"
6018
+ },
6019
+ expected_hash: {
6020
+ type: "string",
6021
+ description: "SHA-256 from the most recent file_read for this file. All edits for a file must use the same hash if provided."
5859
6022
  }
5860
6023
  },
5861
6024
  required: ["path", "old_string", "new_string"]
@@ -5888,15 +6051,33 @@ var init_batch_edit = __esm({
5888
6051
  old_string: edit.old_string,
5889
6052
  new_string: edit.new_string,
5890
6053
  replace_all: edit.replace_all,
6054
+ expected_hash: edit.expected_hash,
5891
6055
  relPath: edit.path
5892
6056
  });
5893
6057
  }
5894
6058
  const results = [];
5895
6059
  let successCount = 0;
5896
6060
  let failCount = 0;
6061
+ const pendingWrites = [];
5897
6062
  for (const [fullPath, fileEdits] of byFile) {
5898
6063
  try {
5899
6064
  let content = await readFile7(fullPath, "utf-8");
6065
+ const beforeHash = contentHash(content);
6066
+ const expectedHashes = new Set(fileEdits.map((edit) => extractExpectedHash({ expected_hash: edit.expected_hash })).filter((h) => !!h));
6067
+ if (expectedHashes.size > 1) {
6068
+ results.push(`ERROR: ${fileEdits[0].relPath}: conflicting expected_hash values in one batch`);
6069
+ failCount += fileEdits.length;
6070
+ continue;
6071
+ }
6072
+ const expectedHash = expectedHashes.values().next().value;
6073
+ if (expectedHash && expectedHash !== beforeHash) {
6074
+ results.push(hashMismatchMessage(fileEdits[0].relPath, expectedHash, beforeHash));
6075
+ failCount += fileEdits.length;
6076
+ continue;
6077
+ }
6078
+ const originalContent = content;
6079
+ let fileSuccessCount = 0;
6080
+ let fileFailed = false;
5900
6081
  for (const edit of fileEdits) {
5901
6082
  const occurrences = countOccurrences2(content, edit.old_string);
5902
6083
  if (occurrences === 0) {
@@ -5911,7 +6092,8 @@ ${snippet.content}
5911
6092
  results.push(`SKIP: old_string not found in ${edit.relPath} (file empty or binary)`);
5912
6093
  }
5913
6094
  failCount++;
5914
- continue;
6095
+ fileFailed = true;
6096
+ break;
5915
6097
  }
5916
6098
  if (!edit.replace_all && occurrences > 1) {
5917
6099
  const offsets = findMatchOffsetsBatch(content, edit.old_string).slice(0, 4);
@@ -5924,7 +6106,8 @@ ${s2.content}`;
5924
6106
  results.push(`AMBIGUOUS: old_string has ${occurrences} matches in ${edit.relPath}.${snippets}
5925
6107
  Add UNIQUE surrounding context, or use replace_all=true.`);
5926
6108
  failCount++;
5927
- continue;
6109
+ fileFailed = true;
6110
+ break;
5928
6111
  }
5929
6112
  if (edit.replace_all) {
5930
6113
  content = content.split(edit.old_string).join(edit.new_string);
@@ -5935,26 +6118,55 @@ ${s2.content}`;
5935
6118
  content = content.slice(0, index) + edit.new_string + content.slice(index + edit.old_string.length);
5936
6119
  results.push(`EDIT: ${edit.relPath} line ${lineNumber}`);
5937
6120
  }
5938
- successCount++;
6121
+ fileSuccessCount++;
5939
6122
  }
5940
- await writeFile4(fullPath, content, "utf-8");
5941
- recordChange(this.workingDir, {
5942
- tool: "batch_edit",
5943
- file: fileEdits[0].relPath,
5944
- summary: `Batch: ${fileEdits.length} edit(s) in ${fileEdits[0].relPath}`
6123
+ if (fileFailed) {
6124
+ content = originalContent;
6125
+ continue;
6126
+ }
6127
+ const afterHash = contentHash(content);
6128
+ if (afterHash === beforeHash) {
6129
+ results.push(`NO-OP: ${fileEdits[0].relPath} batch produced no disk changes`);
6130
+ continue;
6131
+ }
6132
+ pendingWrites.push({
6133
+ fullPath,
6134
+ relPath: fileEdits[0].relPath,
6135
+ content,
6136
+ beforeHash,
6137
+ afterHash,
6138
+ editCount: fileSuccessCount
5945
6139
  });
6140
+ successCount += fileSuccessCount;
5946
6141
  } catch (error) {
5947
6142
  results.push(`ERROR: ${fullPath}: ${error instanceof Error ? error.message : String(error)}`);
5948
6143
  failCount++;
5949
6144
  }
5950
6145
  }
5951
- const summary = `${successCount} edit(s) applied, ${failCount} failed across ${byFile.size} file(s)`;
6146
+ if (failCount === 0) {
6147
+ for (const write2 of pendingWrites) {
6148
+ await writeFile4(write2.fullPath, write2.content, "utf-8");
6149
+ recordChange(this.workingDir, {
6150
+ tool: "batch_edit",
6151
+ file: write2.relPath,
6152
+ summary: `Batch: ${write2.editCount} edit(s) in ${write2.relPath}`
6153
+ });
6154
+ }
6155
+ }
6156
+ const appliedCount = failCount === 0 ? successCount : 0;
6157
+ const summary = `${appliedCount} edit(s) applied, ${failCount} failed across ${byFile.size} file(s)` + (failCount > 0 ? " — atomic batch aborted; no files modified" : "");
5952
6158
  return {
5953
6159
  success: failCount === 0,
5954
6160
  output: `${summary}
5955
6161
  ${results.join("\n")}`,
5956
6162
  error: failCount > 0 ? `${failCount} edit(s) failed` : void 0,
5957
- durationMs: performance.now() - start2
6163
+ durationMs: performance.now() - start2,
6164
+ mutated: failCount === 0 && pendingWrites.length > 0,
6165
+ mutatedFiles: failCount === 0 ? pendingWrites.map((w) => w.relPath) : [],
6166
+ noop: failCount === 0 && pendingWrites.length === 0,
6167
+ partial: false,
6168
+ beforeHash: pendingWrites.length === 1 ? pendingWrites[0].beforeHash : void 0,
6169
+ afterHash: pendingWrites.length === 1 ? pendingWrites[0].afterHash : void 0
5958
6170
  };
5959
6171
  }
5960
6172
  };
@@ -5969,6 +6181,7 @@ var init_file_patch = __esm({
5969
6181
  "packages/execution/dist/tools/file-patch.js"() {
5970
6182
  "use strict";
5971
6183
  init_change_log();
6184
+ init_edit_metadata();
5972
6185
  FilePatchTool = class {
5973
6186
  name = "file_patch";
5974
6187
  description = "Edit specific line ranges in a file. More precise than string matching for large files. Modes: 'replace' replaces lines start_line..end_line with new_content, 'insert_before' inserts before start_line, 'insert_after' inserts after start_line, 'delete' removes lines start_line..end_line. Use dry_run to preview changes.";
@@ -5989,7 +6202,19 @@ var init_file_patch = __esm({
5989
6202
  },
5990
6203
  new_content: {
5991
6204
  type: "string",
5992
- description: "Replacement content (for replace mode) or content to insert (for insert modes). Not needed for delete mode."
6205
+ description: "Replacement content (for replace mode) or content to insert (for insert modes). Required for replace and insert modes. Not needed for delete mode."
6206
+ },
6207
+ expected_hash: {
6208
+ type: "string",
6209
+ description: "SHA-256 from the most recent file_read. Required for insert modes and accepted for all modes."
6210
+ },
6211
+ expected_old_content: {
6212
+ type: "string",
6213
+ description: "Exact current content in start_line..end_line. Required for replace/delete unless expected_hash is provided."
6214
+ },
6215
+ allow_empty_content: {
6216
+ type: "boolean",
6217
+ description: "Set true only when an intentional blank replacement/insert is required."
5993
6218
  },
5994
6219
  mode: {
5995
6220
  type: "string",
@@ -6011,9 +6236,13 @@ var init_file_patch = __esm({
6011
6236
  const filePath = args["path"];
6012
6237
  const startLine = args["start_line"];
6013
6238
  const endLine = args["end_line"] ?? startLine;
6014
- const newContent = args["new_content"] ?? "";
6015
6239
  const mode = args["mode"] ?? "replace";
6016
6240
  const dryRun = args["dry_run"] === true;
6241
+ const hasNewContent = typeof args["new_content"] === "string";
6242
+ const newContent = hasNewContent ? args["new_content"] : "";
6243
+ const expectedHash = extractExpectedHash(args);
6244
+ const expectedOldContent = typeof args["expected_old_content"] === "string" ? args["expected_old_content"] : void 0;
6245
+ const allowEmptyContent = args["allow_empty_content"] === true;
6017
6246
  const start2 = performance.now();
6018
6247
  try {
6019
6248
  if (!Number.isInteger(startLine) || startLine < 1) {
@@ -6034,8 +6263,21 @@ var init_file_patch = __esm({
6034
6263
  }
6035
6264
  const fullPath = resolve12(this.workingDir, filePath);
6036
6265
  const content = await readFile8(fullPath, "utf-8");
6266
+ const beforeHash = contentHash(content);
6037
6267
  const lines = content.split("\n");
6038
6268
  const totalLines = lines.length;
6269
+ if (expectedHash && expectedHash !== beforeHash) {
6270
+ return {
6271
+ success: false,
6272
+ output: "",
6273
+ error: hashMismatchMessage(filePath, expectedHash, beforeHash),
6274
+ durationMs: performance.now() - start2,
6275
+ mutated: false,
6276
+ mutatedFiles: [],
6277
+ beforeHash,
6278
+ afterHash: beforeHash
6279
+ };
6280
+ }
6039
6281
  if (startLine > totalLines) {
6040
6282
  return {
6041
6283
  success: false,
@@ -6047,6 +6289,60 @@ var init_file_patch = __esm({
6047
6289
  const effectiveEnd = Math.min(endLine, totalLines);
6048
6290
  const startIdx = startLine - 1;
6049
6291
  const endIdx = effectiveEnd;
6292
+ const oldTargetContent = lines.slice(startIdx, endIdx).join("\n");
6293
+ if ((mode === "replace" || mode === "delete") && !expectedHash && expectedOldContent === void 0) {
6294
+ return {
6295
+ success: false,
6296
+ output: "",
6297
+ error: `file_patch ${mode} requires targeted context: pass expected_hash from file_read or expected_old_content copied exactly from lines ${startLine}-${effectiveEnd}.`,
6298
+ durationMs: performance.now() - start2,
6299
+ mutated: false,
6300
+ mutatedFiles: [],
6301
+ beforeHash,
6302
+ afterHash: beforeHash
6303
+ };
6304
+ }
6305
+ if ((mode === "insert_before" || mode === "insert_after") && !expectedHash) {
6306
+ return {
6307
+ success: false,
6308
+ output: "",
6309
+ error: `file_patch ${mode} requires expected_hash from the most recent file_read so line numbers are versioned.`,
6310
+ durationMs: performance.now() - start2,
6311
+ mutated: false,
6312
+ mutatedFiles: [],
6313
+ beforeHash,
6314
+ afterHash: beforeHash
6315
+ };
6316
+ }
6317
+ if (expectedOldContent !== void 0 && expectedOldContent !== oldTargetContent) {
6318
+ return {
6319
+ success: false,
6320
+ output: "",
6321
+ error: `Refusing to patch ${filePath}: expected_old_content does not match current lines ${startLine}-${effectiveEnd}.
6322
+
6323
+ Current content:
6324
+ ${oldTargetContent}
6325
+
6326
+ Re-read the target range and retry with exact current content.`,
6327
+ durationMs: performance.now() - start2,
6328
+ mutated: false,
6329
+ mutatedFiles: [],
6330
+ beforeHash,
6331
+ afterHash: beforeHash
6332
+ };
6333
+ }
6334
+ if ((mode === "replace" || mode === "insert_before" || mode === "insert_after") && (!hasNewContent || newContent.length === 0 && !allowEmptyContent)) {
6335
+ return {
6336
+ success: false,
6337
+ output: "",
6338
+ error: `file_patch mode=${mode} requires non-empty new_content. Use mode="delete" for removal, or set allow_empty_content=true for an intentional blank edit.`,
6339
+ durationMs: performance.now() - start2,
6340
+ mutated: false,
6341
+ mutatedFiles: [],
6342
+ beforeHash,
6343
+ afterHash: beforeHash
6344
+ };
6345
+ }
6050
6346
  const newLines = newContent.length > 0 ? newContent.split("\n") : [];
6051
6347
  let resultLines;
6052
6348
  let description;
@@ -6110,10 +6406,31 @@ ${newSection}`;
6110
6406
  output: `[DRY RUN] ${diff}
6111
6407
 
6112
6408
  File NOT modified. Remove dry_run to apply.`,
6113
- durationMs: performance.now() - start2
6409
+ durationMs: performance.now() - start2,
6410
+ mutated: false,
6411
+ mutatedFiles: [],
6412
+ diff,
6413
+ dryRun: true,
6414
+ beforeHash,
6415
+ afterHash: beforeHash
6416
+ };
6417
+ }
6418
+ const updatedContent = resultLines.join("\n");
6419
+ const afterHash = contentHash(updatedContent);
6420
+ if (afterHash === beforeHash) {
6421
+ return {
6422
+ success: true,
6423
+ output: `[NO-OP — ${filePath}: ${description}; resulting content is identical to disk.]`,
6424
+ durationMs: performance.now() - start2,
6425
+ mutated: false,
6426
+ mutatedFiles: [],
6427
+ diff,
6428
+ noop: true,
6429
+ beforeHash,
6430
+ afterHash
6114
6431
  };
6115
6432
  }
6116
- await writeFile5(fullPath, resultLines.join("\n"), "utf-8");
6433
+ await writeFile5(fullPath, updatedContent, "utf-8");
6117
6434
  recordChange(this.workingDir, {
6118
6435
  tool: "file_patch",
6119
6436
  file: filePath,
@@ -6123,10 +6440,16 @@ File NOT modified. Remove dry_run to apply.`,
6123
6440
  });
6124
6441
  return {
6125
6442
  success: true,
6126
- output: `${filePath}: ${description} (${totalLines} → ${resultLines.length} total lines)
6443
+ output: `${filePath}: ${description} (${totalLines} → ${resultLines.length} total lines, sha256 ${beforeHash} → ${afterHash})
6127
6444
 
6128
6445
  ${diff}`,
6129
- durationMs: performance.now() - start2
6446
+ durationMs: performance.now() - start2,
6447
+ mutated: true,
6448
+ mutatedFiles: [filePath],
6449
+ diff,
6450
+ noop: false,
6451
+ beforeHash,
6452
+ afterHash
6130
6453
  };
6131
6454
  } catch (error) {
6132
6455
  return {
@@ -6706,7 +7029,7 @@ var init_git_info = __esm({
6706
7029
  });
6707
7030
 
6708
7031
  // packages/execution/dist/tools/jibberlink.js
6709
- import { createCipheriv, createDecipheriv, createHash, randomBytes as randomBytes5 } from "node:crypto";
7032
+ import { createCipheriv, createDecipheriv, createHash as createHash2, randomBytes as randomBytes5 } from "node:crypto";
6710
7033
  function crc16(bytes) {
6711
7034
  let crc = 65535;
6712
7035
  for (let i2 = 0; i2 < bytes.length; i2++) {
@@ -6719,7 +7042,7 @@ function crc16(bytes) {
6719
7042
  }
6720
7043
  function deriveRoomKey(roomId, secret = "jibberlink-v1") {
6721
7044
  const material = `${secret}\0${roomId}`;
6722
- return createHash("sha256").update(material).digest();
7045
+ return createHash2("sha256").update(material).digest();
6723
7046
  }
6724
7047
  function aesGcmEncrypt(key, plaintext) {
6725
7048
  if (key.length !== 32)
@@ -6805,7 +7128,7 @@ __export(nexus_exports, {
6805
7128
  import { readFile as readFile9, writeFile as writeFile6, mkdir as mkdir3, chmod, unlink, readdir as readdir2, open as fsOpen, copyFile as copyFile2 } from "node:fs/promises";
6806
7129
  import { existsSync as existsSync14, readFileSync as readFileSync12, watch as fsWatchLocal } from "node:fs";
6807
7130
  import { resolve as resolve13, join as join18 } from "node:path";
6808
- import { randomBytes as randomBytes6, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, scryptSync, createHash as createHash2 } from "node:crypto";
7131
+ import { randomBytes as randomBytes6, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, scryptSync, createHash as createHash3 } from "node:crypto";
6809
7132
  import { execSync as execSync8, spawn as spawn2 } from "node:child_process";
6810
7133
  import { hostname, userInfo, homedir as homedir4 } from "node:os";
6811
7134
  function readBundledDependencySpec(packageName, fallback) {
@@ -12087,7 +12410,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12087
12410
  // =========================================================================
12088
12411
  async doConnect(args) {
12089
12412
  await this.ensureDir();
12090
- const currentScriptHash = createHash2("sha256").update(DAEMON_SCRIPT).digest("hex").slice(0, 16);
12413
+ const currentScriptHash = createHash3("sha256").update(DAEMON_SCRIPT).digest("hex").slice(0, 16);
12091
12414
  const existingPid = this.getDaemonPid();
12092
12415
  if (existingPid) {
12093
12416
  let processAlive = false;
@@ -12693,7 +13016,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12693
13016
  } catch {
12694
13017
  const privKey = randomBytes6(32);
12695
13018
  privKeyHex = privKey.toString("hex");
12696
- address = "0x" + createHash2("sha256").update(privKey).digest("hex").slice(0, 40);
13019
+ address = "0x" + createHash3("sha256").update(privKey).digest("hex").slice(0, 40);
12697
13020
  privKey.fill(0);
12698
13021
  }
12699
13022
  const salt = randomBytes6(32);
@@ -12755,7 +13078,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12755
13078
  } catch {
12756
13079
  const privKey = randomBytes6(32);
12757
13080
  privKeyHex = privKey.toString("hex");
12758
- address = "0x" + createHash2("sha256").update(privKey).digest("hex").slice(0, 40);
13081
+ address = "0x" + createHash3("sha256").update(privKey).digest("hex").slice(0, 40);
12759
13082
  privKey.fill(0);
12760
13083
  }
12761
13084
  const salt = randomBytes6(32);
@@ -13278,7 +13601,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
13278
13601
  }
13279
13602
  }
13280
13603
  const nonce = randomBytes6(16).toString("hex");
13281
- const hash = createHash2("sha256").update(`${modelName}:${nonce}:${Date.now()}`).digest("hex");
13604
+ const hash = createHash3("sha256").update(`${modelName}:${nonce}:${Date.now()}`).digest("hex");
13282
13605
  const bar = (s2) => "█".repeat(Math.round(s2 / 5)) + "░".repeat(20 - Math.round(s2 / 5));
13283
13606
  const mem = vramMb > 24e3 ? 95 : vramMb > 16e3 ? 80 : vramMb > 8e3 ? 60 : vramMb > 0 ? 40 : 20;
13284
13607
  await this.ensureDir();
@@ -89943,7 +90266,7 @@ var require_auto = __commonJS({
89943
90266
  // ../node_modules/acme-client/src/client.js
89944
90267
  var require_client = __commonJS({
89945
90268
  "../node_modules/acme-client/src/client.js"(exports, module) {
89946
- var { createHash: createHash20 } = __require("crypto");
90269
+ var { createHash: createHash21 } = __require("crypto");
89947
90270
  var { getPemBodyAsB64u } = require_crypto();
89948
90271
  var { log: log22 } = require_logger();
89949
90272
  var HttpClient = require_http();
@@ -90254,14 +90577,14 @@ var require_client = __commonJS({
90254
90577
  */
90255
90578
  async getChallengeKeyAuthorization(challenge) {
90256
90579
  const jwk = this.http.getJwk();
90257
- const keysum = createHash20("sha256").update(JSON.stringify(jwk));
90580
+ const keysum = createHash21("sha256").update(JSON.stringify(jwk));
90258
90581
  const thumbprint = keysum.digest("base64url");
90259
90582
  const result = `${challenge.token}.${thumbprint}`;
90260
90583
  if (challenge.type === "http-01") {
90261
90584
  return result;
90262
90585
  }
90263
90586
  if (challenge.type === "dns-01") {
90264
- return createHash20("sha256").update(result).digest("base64url");
90587
+ return createHash21("sha256").update(result).digest("base64url");
90265
90588
  }
90266
90589
  if (challenge.type === "tls-alpn-01") {
90267
90590
  return result;
@@ -232941,7 +233264,7 @@ var require_websocket2 = __commonJS({
232941
233264
  var http6 = __require("http");
232942
233265
  var net5 = __require("net");
232943
233266
  var tls2 = __require("tls");
232944
- var { randomBytes: randomBytes25, createHash: createHash20 } = __require("crypto");
233267
+ var { randomBytes: randomBytes25, createHash: createHash21 } = __require("crypto");
232945
233268
  var { Duplex: Duplex3, Readable } = __require("stream");
232946
233269
  var { URL: URL3 } = __require("url");
232947
233270
  var PerMessageDeflate2 = require_permessage_deflate2();
@@ -233601,7 +233924,7 @@ var require_websocket2 = __commonJS({
233601
233924
  abortHandshake(websocket, socket, "Invalid Upgrade header");
233602
233925
  return;
233603
233926
  }
233604
- const digest3 = createHash20("sha1").update(key + GUID).digest("base64");
233927
+ const digest3 = createHash21("sha1").update(key + GUID).digest("base64");
233605
233928
  if (res.headers["sec-websocket-accept"] !== digest3) {
233606
233929
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
233607
233930
  return;
@@ -233968,7 +234291,7 @@ var require_websocket_server = __commonJS({
233968
234291
  var EventEmitter13 = __require("events");
233969
234292
  var http6 = __require("http");
233970
234293
  var { Duplex: Duplex3 } = __require("stream");
233971
- var { createHash: createHash20 } = __require("crypto");
234294
+ var { createHash: createHash21 } = __require("crypto");
233972
234295
  var extension2 = require_extension2();
233973
234296
  var PerMessageDeflate2 = require_permessage_deflate2();
233974
234297
  var subprotocol2 = require_subprotocol();
@@ -234269,7 +234592,7 @@ var require_websocket_server = __commonJS({
234269
234592
  );
234270
234593
  }
234271
234594
  if (this._state > RUNNING) return abortHandshake(socket, 503);
234272
- const digest3 = createHash20("sha1").update(key + GUID).digest("base64");
234595
+ const digest3 = createHash21("sha1").update(key + GUID).digest("base64");
234273
234596
  const headers = [
234274
234597
  "HTTP/1.1 101 Switching Protocols",
234275
234598
  "Upgrade: websocket",
@@ -247076,13 +247399,13 @@ Justification: ${justification || "(none provided)"}`,
247076
247399
  }
247077
247400
  const snapshot = JSON.stringify(this.selfState, null, 2);
247078
247401
  try {
247079
- const { createHash: createHash20 } = await import("node:crypto");
247402
+ const { createHash: createHash21 } = await import("node:crypto");
247080
247403
  const snapshotDir = join27(this.cwd, ".oa", "identity", "snapshots");
247081
247404
  await mkdir6(snapshotDir, { recursive: true });
247082
247405
  const version4 = this.selfState.version;
247083
247406
  const snapshotPath = join27(snapshotDir, `v${version4}.json`);
247084
247407
  await writeFile11(snapshotPath, snapshot, "utf8");
247085
- const hash = createHash20("sha256").update(snapshot).digest("hex");
247408
+ const hash = createHash21("sha256").update(snapshot).digest("hex");
247086
247409
  await writeFile11(join27(this.cwd, ".oa", "identity", "latest-hash.txt"), hash, "utf8");
247087
247410
  let ipfsCid = "";
247088
247411
  try {
@@ -247215,8 +247538,8 @@ New: ${newNarrative.slice(0, 200)}...`,
247215
247538
  }
247216
247539
  // ── Helpers ──────────────────────────────────────────────────────────────
247217
247540
  createDefaultState() {
247218
- const { createHash: createHash20 } = __require("node:crypto");
247219
- const machineId = createHash20("sha256").update(this.cwd).digest("hex").slice(0, 12);
247541
+ const { createHash: createHash21 } = __require("node:crypto");
247542
+ const machineId = createHash21("sha256").update(this.cwd).digest("hex").slice(0, 12);
247220
247543
  return {
247221
247544
  self_id: `oa-${machineId}`,
247222
247545
  version: 1,
@@ -247298,9 +247621,9 @@ New: ${newNarrative.slice(0, 200)}...`,
247298
247621
  let cid;
247299
247622
  if (this.selfState.version > prevVersion) {
247300
247623
  try {
247301
- const { createHash: createHash20 } = await import("node:crypto");
247624
+ const { createHash: createHash21 } = await import("node:crypto");
247302
247625
  const stateJson = JSON.stringify(this.selfState);
247303
- const hash = createHash20("sha256").update(stateJson).digest("hex").slice(0, 32);
247626
+ const hash = createHash21("sha256").update(stateJson).digest("hex").slice(0, 32);
247304
247627
  const cidsPath = join27(this.cwd, ".oa", "identity", "cids.json");
247305
247628
  const cidsData = { latest: "", hash, version: this.selfState.version };
247306
247629
  try {
@@ -253007,7 +253330,7 @@ import { execSync as execSync22, exec as execCb, spawnSync as spawnSync2 } from
253007
253330
  import { readFile as readFile16, writeFile as writeFile17, mkdir as mkdir12 } from "node:fs/promises";
253008
253331
  import { resolve as resolve24, join as join41 } from "node:path";
253009
253332
  import { homedir as homedir10 } from "node:os";
253010
- import { randomBytes as randomBytes11, createHash as createHash3 } from "node:crypto";
253333
+ import { randomBytes as randomBytes11, createHash as createHash4 } from "node:crypto";
253011
253334
  function isValidCron(expr) {
253012
253335
  const parts = expr.trim().split(/\s+/);
253013
253336
  if (parts.length !== 5)
@@ -253330,7 +253653,7 @@ var init_scheduler = __esm({
253330
253653
  }
253331
253654
  const scope = String(args["scope"] ?? "local");
253332
253655
  const fingerprint = `${resolve24(this.workingDir)}|${task}|${cronExpr}|${scope}`;
253333
- const id = `sched-${createHash3("sha1").update(fingerprint).digest("hex").slice(0, 8)}`;
253656
+ const id = `sched-${createHash4("sha1").update(fingerprint).digest("hex").slice(0, 8)}`;
253334
253657
  const oneShot = Boolean(args["one_shot"]);
253335
253658
  const maxRuns = typeof args["max_runs"] === "number" ? args["max_runs"] : void 0;
253336
253659
  const newTask = {
@@ -256873,7 +257196,7 @@ var init_import_graph = __esm({
256873
257196
  import { createRequire as __createRequireGlob } from "node:module";
256874
257197
  import ignore from "ignore";
256875
257198
  import { readFile as readFile21, stat as stat4 } from "node:fs/promises";
256876
- import { createHash as createHash4 } from "node:crypto";
257199
+ import { createHash as createHash5 } from "node:crypto";
256877
257200
  import { join as join49, relative as relative4, extname as extname8, basename as basename11 } from "node:path";
256878
257201
  var __requireGlob, glob2, DEFAULT_EXCLUDE, LANGUAGE_MAP, CodebaseIndexer;
256879
257202
  var init_codebase_indexer = __esm({
@@ -256941,7 +257264,7 @@ var init_codebase_indexer = __esm({
256941
257264
  if (fileStat.size > this.config.maxFileSize)
256942
257265
  continue;
256943
257266
  const content = await readFile21(fullPath);
256944
- const hash = createHash4("sha256").update(content).digest("hex");
257267
+ const hash = createHash5("sha256").update(content).digest("hex");
256945
257268
  const ext = extname8(relativePath);
256946
257269
  indexed.push({
256947
257270
  path: fullPath,
@@ -501260,7 +501583,7 @@ var init_ts_morph_parser = __esm({
501260
501583
 
501261
501584
  // packages/indexer/dist/code-graph-db.js
501262
501585
  import { createRequire as createRequire2 } from "node:module";
501263
- import { createHash as createHash5 } from "node:crypto";
501586
+ import { createHash as createHash6 } from "node:crypto";
501264
501587
  import { mkdirSync as mkdirSync12, readFileSync as readFileSync26 } from "node:fs";
501265
501588
  import { join as join50, dirname as dirname14, extname as extname9 } from "node:path";
501266
501589
  function loadDatabaseCtor() {
@@ -501332,7 +501655,7 @@ function extractFileImports(content, filePath) {
501332
501655
  return imports.map((p2) => p2.replace(/\.(js|ts|jsx|tsx|mjs|cjs)$/, ""));
501333
501656
  }
501334
501657
  function hashContent(content) {
501335
- return createHash5("sha1").update(content).digest("hex").slice(0, 16);
501658
+ return createHash6("sha1").update(content).digest("hex").slice(0, 16);
501336
501659
  }
501337
501660
  function detectLanguage(filePath) {
501338
501661
  return EXT_TO_LANG[extname9(filePath)] ?? "unknown";
@@ -509952,7 +510275,7 @@ var init_environment_snapshot = __esm({
509952
510275
  import { execSync as execSync42 } from "node:child_process";
509953
510276
  import { existsSync as existsSync47, mkdirSync as mkdirSync24, writeFileSync as writeFileSync22, readFileSync as readFileSync39, readdirSync as readdirSync14, unlinkSync as unlinkSync11 } from "node:fs";
509954
510277
  import { join as join64, basename as basename12 } from "node:path";
509955
- import { createHash as createHash6 } from "node:crypto";
510278
+ import { createHash as createHash7 } from "node:crypto";
509956
510279
  function isYouTubeUrl2(url) {
509957
510280
  return /(?:youtube\.com\/(?:watch|shorts|live|embed|v\/)|youtu\.be\/)/i.test(url);
509958
510281
  }
@@ -509980,7 +510303,7 @@ function ensureFfmpeg() {
509980
510303
  function imageHash(imagePath) {
509981
510304
  try {
509982
510305
  const data = readFileSync39(imagePath);
509983
- return createHash6("md5").update(data).digest("hex").slice(0, 12);
510306
+ return createHash7("md5").update(data).digest("hex").slice(0, 12);
509984
510307
  } catch {
509985
510308
  return "unknown";
509986
510309
  }
@@ -514004,14 +514327,14 @@ var init_artifact_inspector = __esm({
514004
514327
  // packages/orchestrator/dist/lesson-bank.js
514005
514328
  import { existsSync as existsSync53, mkdirSync as mkdirSync27, appendFileSync as appendFileSync2, readFileSync as readFileSync44 } from "node:fs";
514006
514329
  import { join as join69, dirname as dirname19 } from "node:path";
514007
- import { createHash as createHash7 } from "node:crypto";
514330
+ import { createHash as createHash8 } from "node:crypto";
514008
514331
  function tokenize2(text) {
514009
514332
  if (!text)
514010
514333
  return [];
514011
514334
  return text.toLowerCase().split(TOKENIZE_RE).filter((t2) => t2.length >= 3).slice(0, 80);
514012
514335
  }
514013
514336
  function shortHash(s2) {
514014
- return createHash7("sha256").update(s2).digest("hex").slice(0, 16);
514337
+ return createHash8("sha256").update(s2).digest("hex").slice(0, 16);
514015
514338
  }
514016
514339
  function solicit(args) {
514017
514340
  const { taskGoal, stem, reflections, successOutputPreview } = args;
@@ -517476,7 +517799,7 @@ var init_pprRetrieval = __esm({
517476
517799
  import { join as join75 } from "node:path";
517477
517800
  import { mkdirSync as mkdirSync29, existsSync as existsSync59 } from "node:fs";
517478
517801
  import { randomUUID as randomUUID8 } from "node:crypto";
517479
- import { createHash as createHash8 } from "node:crypto";
517802
+ import { createHash as createHash9 } from "node:crypto";
517480
517803
  function readEpisodeAffect2(metadata) {
517481
517804
  if (!metadata || typeof metadata !== "object")
517482
517805
  return null;
@@ -517682,7 +518005,7 @@ var init_episodeStore = __esm({
517682
518005
  insert(ep) {
517683
518006
  const id = randomUUID8();
517684
518007
  const now = Date.now();
517685
- const contentHash = createHash8("sha256").update(ep.content).digest("hex").slice(0, 16);
518008
+ const contentHash2 = createHash9("sha256").update(ep.content).digest("hex").slice(0, 16);
517686
518009
  const modality = ep.modality ?? "text";
517687
518010
  const rawImportance = ep.importance ?? autoImportance(ep.toolName ?? null, modality, ep.content);
517688
518011
  const modulated = ep.emotionalState ? modulateImportance(sanitizeImportance(rawImportance), ep.emotionalState) : sanitizeImportance(rawImportance);
@@ -517691,13 +518014,13 @@ var init_episodeStore = __esm({
517691
518014
  return "";
517692
518015
  }
517693
518016
  const decayClass = ep.decayClass ?? autoDecayClass(ep.toolName ?? null, modality, ep.content);
517694
- const existing = this.db.prepare("SELECT id FROM episodes WHERE content_hash = ? AND session_id = ? LIMIT 1").get(contentHash, ep.sessionId ?? null);
518017
+ const existing = this.db.prepare("SELECT id FROM episodes WHERE content_hash = ? AND session_id = ? LIMIT 1").get(contentHash2, ep.sessionId ?? null);
517695
518018
  if (existing)
517696
518019
  return existing.id;
517697
518020
  this.db.prepare(`
517698
518021
  INSERT INTO episodes (id, timestamp, session_id, task_id, turn_number, modality, tool_name, content, content_hash, metadata, importance, decay_class, strength)
517699
518022
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1.0)
517700
- `).run(id, now, ep.sessionId ?? null, ep.taskId ?? null, ep.turnNumber ?? null, modality, ep.toolName ?? null, ep.content, contentHash, ep.metadata ? JSON.stringify(ep.metadata) : null, importance, decayClass);
518023
+ `).run(id, now, ep.sessionId ?? null, ep.taskId ?? null, ep.turnNumber ?? null, modality, ep.toolName ?? null, ep.content, contentHash2, ep.metadata ? JSON.stringify(ep.metadata) : null, importance, decayClass);
517701
518024
  if (this.graph) {
517702
518025
  try {
517703
518026
  const episode = this.get(id);
@@ -522251,7 +522574,7 @@ var init_memoryStageContext = __esm({
522251
522574
  });
522252
522575
 
522253
522576
  // packages/memory/dist/sessionGist.js
522254
- import { createHash as createHash9 } from "node:crypto";
522577
+ import { createHash as createHash10 } from "node:crypto";
522255
522578
  function inferDomain(input) {
522256
522579
  const blob = [
522257
522580
  input.goal,
@@ -522276,7 +522599,7 @@ function inferDomain(input) {
522276
522599
  return ranked[0][0];
522277
522600
  }
522278
522601
  function computeGoalHash(goal) {
522279
- return createHash9("sha256").update(goal.trim().toLowerCase()).digest("hex").slice(0, 16);
522602
+ return createHash10("sha256").update(goal.trim().toLowerCase()).digest("hex").slice(0, 16);
522280
522603
  }
522281
522604
  function clip(text, n2) {
522282
522605
  if (!text)
@@ -522487,12 +522810,12 @@ var init_toolOutcomes = __esm({
522487
522810
  });
522488
522811
 
522489
522812
  // packages/memory/dist/stagnationRecipes.js
522490
- import { createHash as createHash10 } from "node:crypto";
522813
+ import { createHash as createHash11 } from "node:crypto";
522491
522814
  function fingerprintSignature(fp) {
522492
522815
  const normClusters = (fp.errorClusters ?? []).map((s2) => (s2 || "").toLowerCase().replace(/[0-9]+/g, "N").replace(/\s+/g, " ").trim()).filter(Boolean).sort();
522493
522816
  const tool = (fp.stuckTool ?? "").toLowerCase().trim();
522494
522817
  const blob = `tool=${tool};clusters=${normClusters.join("|")}`;
522495
- return createHash10("sha256").update(blob).digest("hex").slice(0, 16);
522818
+ return createHash11("sha256").update(blob).digest("hex").slice(0, 16);
522496
522819
  }
522497
522820
  function crystallize(store2, input) {
522498
522821
  const sig = fingerprintSignature(input.fingerprint);
@@ -522549,7 +522872,7 @@ var init_stagnationRecipes = __esm({
522549
522872
  });
522550
522873
 
522551
522874
  // packages/memory/dist/codebaseMap.js
522552
- import { createHash as createHash11, randomUUID as randomUUID12 } from "node:crypto";
522875
+ import { createHash as createHash12, randomUUID as randomUUID12 } from "node:crypto";
522553
522876
  function freshNodeId() {
522554
522877
  return randomUUID12();
522555
522878
  }
@@ -522563,7 +522886,7 @@ var init_codebaseMap = __esm({
522563
522886
  touchCount = /* @__PURE__ */ new Map();
522564
522887
  constructor(db, repoRoot, commitSha) {
522565
522888
  this.db = db;
522566
- this.repoFp = createHash11("sha256").update(`${repoRoot}::${commitSha ?? "no-commit"}`).digest("hex").slice(0, 16);
522889
+ this.repoFp = createHash12("sha256").update(`${repoRoot}::${commitSha ?? "no-commit"}`).digest("hex").slice(0, 16);
522567
522890
  this.ensureSchema();
522568
522891
  }
522569
522892
  ensureSchema() {
@@ -522699,7 +523022,7 @@ var init_codebaseMap = __esm({
522699
523022
  }
522700
523023
  /** Stable composite id: `<kind>:<sha16(path)>` so insert ON CONFLICT works. */
522701
523024
  idFor(kind, path11) {
522702
- const h = createHash11("sha256").update(`${this.repoFp}:${kind}:${path11}`).digest("hex").slice(0, 24);
523025
+ const h = createHash12("sha256").update(`${this.repoFp}:${kind}:${path11}`).digest("hex").slice(0, 24);
522703
523026
  return `${kind}-${h}`;
522704
523027
  }
522705
523028
  };
@@ -524758,7 +525081,7 @@ import { existsSync as existsSync66, readFileSync as readFileSync54, statSync as
524758
525081
  import { execSync as execSync45 } from "node:child_process";
524759
525082
  import { homedir as homedir22, platform as platform2, arch as arch2, totalmem as totalmem2, freemem as freemem2, hostname as hostname3 } from "node:os";
524760
525083
  import { join as join82 } from "node:path";
524761
- import { createHash as createHash12 } from "node:crypto";
525084
+ import { createHash as createHash13 } from "node:crypto";
524762
525085
  function capturePreflightSnapshot(workingDir) {
524763
525086
  const warnings = [];
524764
525087
  const configFingerprints = {};
@@ -524925,7 +525248,7 @@ function expandPath(p2) {
524925
525248
  return p2;
524926
525249
  }
524927
525250
  function sha2563(s2) {
524928
- return createHash12("sha256").update(s2).digest("hex").slice(0, 16);
525251
+ return createHash13("sha256").update(s2).digest("hex").slice(0, 16);
524929
525252
  }
524930
525253
  function freeDiskBytes(path11 = "/tmp") {
524931
525254
  try {
@@ -526876,14 +527199,14 @@ Your NEXT tool call MUST be EXACTLY ONE of:
526876
527199
  • file_write — create a new file
526877
527200
  • file_edit — modify an existing file (find/replace)
526878
527201
  • batch_edit — multiple find/replace edits in one call
526879
- • file_patch — apply a unified diff
527202
+ • file_patch — apply a version-checked line-range patch
526880
527203
 
526881
527204
  These are the ONLY four tools that count as creative edits. The following do NOT count and will NOT satisfy this directive:
526882
527205
  • todo_write (only updates the todo list, not the filesystem)
526883
527206
  • memory_write (writes to memory store, not the project)
526884
527207
  • list_directory / file_read / file_explore / grep_search / shell
526885
527208
 
526886
- Pick the SMALLEST concrete deliverable from the spec — typically the project entry point or the file most other modules depend on. Write a stub or skeleton if the full implementation is too large; you can iterate later. Do NOT issue another read or todo update before producing the next file_write/file_edit/batch_edit/file_patch.`;
527209
+ Pick the SMALLEST concrete deliverable from the spec — typically the project entry point or the file most other modules depend on. Write a complete, version-checked slice that changes disk state; you can iterate later. Do NOT issue another read or todo update before producing the next file_write/file_edit/batch_edit/file_patch.`;
526887
527210
  messages2.push({ role: "system", content: reg61Msg });
526888
527211
  const _cyclePart = cycleLabel ? ` (${cycleLabel})` : "";
526889
527212
  this.emit({
@@ -526918,8 +527241,9 @@ Pick the SMALLEST concrete deliverable from the spec — typically the project e
526918
527241
  const _editTools = /* @__PURE__ */ new Set(["file_write", "file_edit", "batch_edit", "file_patch"]);
526919
527242
  if (!_editTools.has(tc.name))
526920
527243
  return null;
526921
- const _editPath = tc.arguments?.["path"] ?? "";
526922
- if (!_editPath || this._decomp2MainContextFiles.has(_editPath))
527244
+ const _editPaths = this._extractToolTargetPaths(tc.name, tc.arguments);
527245
+ const _editPath = _editPaths.find((p2) => !this._decomp2MainContextFiles.has(p2)) ?? "";
527246
+ if (!_editPath)
526923
527247
  return null;
526924
527248
  const _filesList = [...this._decomp2MainContextFiles].slice(0, 8).map((p2) => ` • ${p2}`).join("\n");
526925
527249
  const _moreFiles = this._decomp2MainContextFiles.size > 8 ? `
@@ -526979,21 +527303,18 @@ Pick the SMALLEST concrete deliverable from the spec — typically the project e
526979
527303
  _trackDecomp2(tc, result, turn) {
526980
527304
  if (process.env["OA_DISABLE_DECOMP2"] === "1")
526981
527305
  return;
526982
- if (result && result.success !== false) {
526983
- const _editTools = /* @__PURE__ */ new Set(["file_write", "file_edit", "batch_edit", "file_patch"]);
526984
- if (_editTools.has(tc.name)) {
526985
- const _editPath = tc.arguments?.["path"] ?? "";
526986
- if (_editPath && typeof _editPath === "string") {
526987
- this._decomp2MainContextFiles.add(_editPath);
526988
- const DECOMP2_FILE_SPREAD_THRESHOLD = 5;
526989
- if (!this._decomp2GateActive && this._decomp2MainContextFiles.size >= DECOMP2_FILE_SPREAD_THRESHOLD && this._decomp2SubAgentCalls === 0) {
526990
- this._decomp2GateActive = true;
526991
- this.emit({
526992
- type: "status",
526993
- content: `DECOMP-2 NEW-FILE GATE ACTIVATED — ${this._decomp2MainContextFiles.size} distinct files edited in main context, 0 sub_agent calls; further edits to NEW files will be blocked until sub_agent is invoked`,
526994
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
526995
- });
526996
- }
527306
+ if (this._isRealProjectMutation(tc.name, result)) {
527307
+ const _editPaths = this._extractToolTargetPaths(tc.name, tc.arguments, result);
527308
+ for (const _editPath of _editPaths) {
527309
+ this._decomp2MainContextFiles.add(_editPath);
527310
+ const DECOMP2_FILE_SPREAD_THRESHOLD = 5;
527311
+ if (!this._decomp2GateActive && this._decomp2MainContextFiles.size >= DECOMP2_FILE_SPREAD_THRESHOLD && this._decomp2SubAgentCalls === 0) {
527312
+ this._decomp2GateActive = true;
527313
+ this.emit({
527314
+ type: "status",
527315
+ content: `DECOMP-2 NEW-FILE GATE ACTIVATED — ${this._decomp2MainContextFiles.size} distinct files edited in main context, 0 sub_agent calls; further edits to NEW files will be blocked until sub_agent is invoked`,
527316
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
527317
+ });
526997
527318
  }
526998
527319
  }
526999
527320
  }
@@ -527101,6 +527422,31 @@ Pick the SMALLEST concrete deliverable from the spec — typically the project e
527101
527422
  return null;
527102
527423
  }
527103
527424
  }
527425
+ _isProjectEditTool(toolName) {
527426
+ return toolName === "file_write" || toolName === "file_edit" || toolName === "batch_edit" || toolName === "file_patch";
527427
+ }
527428
+ _extractToolTargetPaths(toolName, args, result) {
527429
+ const fromResult = Array.isArray(result?.mutatedFiles) ? result.mutatedFiles.filter((p3) => typeof p3 === "string" && p3.trim().length > 0) : [];
527430
+ if (fromResult.length > 0)
527431
+ return [...new Set(fromResult)];
527432
+ if (toolName === "batch_edit" && Array.isArray(args?.["edits"])) {
527433
+ const paths = args["edits"].map((edit) => edit && typeof edit === "object" ? edit["path"] : null).filter((p3) => typeof p3 === "string" && p3.trim().length > 0);
527434
+ return [...new Set(paths)];
527435
+ }
527436
+ const p2 = args?.["path"] ?? args?.["file_path"] ?? args?.["file"] ?? args?.["filename"] ?? args?.["filepath"] ?? args?.["filePath"];
527437
+ return typeof p2 === "string" && p2.trim().length > 0 ? [p2.trim()] : [];
527438
+ }
527439
+ _isRealProjectMutation(toolName, result) {
527440
+ if (!this._isProjectEditTool(toolName))
527441
+ return false;
527442
+ if (!result || result.success === false)
527443
+ return false;
527444
+ if (typeof result.mutated === "boolean")
527445
+ return result.mutated;
527446
+ if (result.dryRun || result.noop || result.partial)
527447
+ return false;
527448
+ return true;
527449
+ }
527104
527450
  /** Track the turn index of the last todo_write call so the reminder
527105
527451
  * path can compute `turnsSinceLastTodoWrite` cheaply without walking
527106
527452
  * the entire messages array. Reset on run(). */
@@ -527765,7 +528111,7 @@ ${body}`;
527765
528111
  * converging — particularly common when a build error names a specific
527766
528112
  * file the agent thinks needs more work.
527767
528113
  *
527768
- * Suggests targeted alternatives (file_edit/edit_block) and "good enough"
528114
+ * Suggests targeted alternatives (file_edit/file_patch) and "good enough"
527769
528115
  * recognition. Returns null when no churn is happening.
527770
528116
  */
527771
528117
  _renderWriteChurnBlock(turn) {
@@ -527789,7 +528135,7 @@ ${body}`;
527789
528135
  for (const c9 of top) {
527790
528136
  lines.push(` • ${c9.path} — ${c9.writes} writes (most recent ${c9.ageTurns} turn${c9.ageTurns === 1 ? "" : "s"} ago)`);
527791
528137
  }
527792
- lines.push("Refining the same file again and again rarely converges. If you have a specific change in mind, use file_edit or edit_block for targeted changes. If you're trying different approaches, read the file once with file_read to see current state, then decide. If the current version is reasonable but a downstream tool fails, the bug is probably in the OTHER file (importer, schema, dep) — not this one.");
528138
+ lines.push("Refining the same file again and again rarely converges. If you have a specific change in mind, use file_edit or file_patch for targeted changes. If you're trying different approaches, read the file once with file_read to see current state, then decide. If the current version is reasonable but a downstream tool fails, the bug is probably in the OTHER file (importer, schema, dep) — not this one.");
527793
528139
  lines.push(`(turn ${turn} — this warning auto-clears when no path has ≥3 writes within 10 turns)`);
527794
528140
  return lines.join("\n");
527795
528141
  }
@@ -527810,6 +528156,10 @@ ${body}`;
527810
528156
  return false;
527811
528157
  if (/(^|[^&\d])(>|>>)\s*\S/.test(cmd))
527812
528158
  return false;
528159
+ if (/(^|[^<])<\s*\S/.test(cmd))
528160
+ return false;
528161
+ if (/\bpatch\b/i.test(cmd))
528162
+ return false;
527813
528163
  const MUTATE_BINS = [
527814
528164
  // POSIX file/process mutators
527815
528165
  "rm",
@@ -528013,8 +528363,6 @@ ${body}`;
528013
528363
  "unexpand",
528014
528364
  "diff",
528015
528365
  "cmp",
528016
- "patch",
528017
- // patch with -R or no-args could be mutating; --dry-run only is read
528018
528366
  "echo",
528019
528367
  "printf",
528020
528368
  "pwd",
@@ -528206,10 +528554,7 @@ ${body}`;
528206
528554
  */
528207
528555
  _renderTodoStateBlock(turn) {
528208
528556
  try {
528209
- const { readTodos: readTodos2 } = __require("@open-agents/execution");
528210
- const { getTodoSessionId: getTodoSessionId2 } = __require("@open-agents/execution");
528211
- const sessionId = getTodoSessionId2();
528212
- const todos = readTodos2(sessionId);
528557
+ const todos = this.readSessionTodos();
528213
528558
  if (!todos || todos.length === 0)
528214
528559
  return null;
528215
528560
  const lines = ["[CURRENT TODO PLAN — already in your state, no need to call todo_write to see it]"];
@@ -529399,7 +529744,6 @@ If the hypothesis cannot be tested by a creative edit, ask the human via task_co
529399
529744
  content: `REG-58 NO-WRITE STAGNATION — ${gap} turns since last creative edit (turn ${this._lastFileWriteTurn})${_telSuffix58}`,
529400
529745
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
529401
529746
  });
529402
- this._lastFileWriteTurn = turn;
529403
529747
  }
529404
529748
  const REG60_WINDOW_MS = 60 * 60 * 1e3;
529405
529749
  const REG60_MIN_WRITES = 3;
@@ -529626,8 +529970,11 @@ If this matches your current shape, try it before continuing.`
529626
529970
  if (_readClass.has(c9.name)) {
529627
529971
  _readCount++;
529628
529972
  } else if (_mutationClass.has(c9.name)) {
529629
- if (!_isStale)
529973
+ if (c9.mutated === true) {
529630
529974
  _mutationCount++;
529975
+ } else if (c9.success === true && c9.mutated !== false && !_isStale) {
529976
+ _mutationCount++;
529977
+ }
529631
529978
  } else if (c9.name === "shell") {
529632
529979
  if (c9.success === true && !_isStale) {
529633
529980
  _readCount++;
@@ -529801,6 +530148,14 @@ ${_staleSamples.join("\n")}` : ``,
529801
530148
  for (const c9 of _wtWindow) {
529802
530149
  if (!_wtWriteClass.has(c9.name))
529803
530150
  continue;
530151
+ if (c9.mutated === false)
530152
+ continue;
530153
+ if (Array.isArray(c9.mutatedFiles) && c9.mutatedFiles.length > 0) {
530154
+ for (const p2 of c9.mutatedFiles) {
530155
+ _wtFileCounts.set(p2, (_wtFileCounts.get(p2) ?? 0) + 1);
530156
+ }
530157
+ continue;
530158
+ }
529804
530159
  const _ak = c9.argsKey || "";
529805
530160
  const _m = /(?:^|,)path=([^,]+)/.exec(_ak);
529806
530161
  const _pk = _m ? _m[1].slice(0, 200) : _ak.slice(0, 200);
@@ -530428,8 +530783,8 @@ If you're stuck, try a completely different approach. Do NOT repeat what failed
530428
530783
  if (process.env["OA_DISABLE_ADAPTIVE_RETRIEVAL"] !== "1") {
530429
530784
  const goalForSig = (this._taskState.goal || "").slice(0, 200);
530430
530785
  const recentTools = this._toolSequence.slice(-5).join("|");
530431
- const { createHash: createHash20 } = await import("node:crypto");
530432
- const sig = createHash20("sha256").update(`${goalForSig}::${recentTools}`).digest("hex").slice(0, 16);
530786
+ const { createHash: createHash21 } = await import("node:crypto");
530787
+ const sig = createHash21("sha256").update(`${goalForSig}::${recentTools}`).digest("hex").slice(0, 16);
530433
530788
  if (this._lastPprSig === sig && this._lastPprMemoryLines.length > 0) {
530434
530789
  compacted.push({
530435
530790
  role: "system",
@@ -531005,14 +531360,6 @@ ${memoryLines.join("\n")}`
531005
531360
  "priority_delegate",
531006
531361
  "background_run"
531007
531362
  ]);
531008
- if (REG61_EDIT_TOOLS.has(tc.name) && this._reg61PerpetualGateActive) {
531009
- this._reg61PerpetualGateActive = false;
531010
- this.emit({
531011
- type: "status",
531012
- content: `REG-61 GATE CLEARED — '${tc.name}' satisfied REG-61 directive at turn ${turn}`,
531013
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
531014
- });
531015
- }
531016
531363
  if (this._reg61PerpetualGateActive && !REG61_BYPASS_TOOLS.has(tc.name) && process.env["OA_DISABLE_REG61_COERCE"] !== "1") {
531017
531364
  this.emit({
531018
531365
  type: "tool_call",
@@ -531038,7 +531385,7 @@ ${memoryLines.join("\n")}`
531038
531385
  ``,
531039
531386
  `This is NOT random guessing — it's targeted hypothesis falsification. Reading the same files 5+ times has already proven uninformative; only a state change will move the system.`,
531040
531387
  ``,
531041
- `Issue EXACTLY ONE of: file_write / file_edit / batch_edit / file_patch on a single concrete change. The exact CHOICE of edit matters less than NOT continuing to re-read.`,
531388
+ `Issue EXACTLY ONE of: file_write / file_edit / batch_edit / file_patch on a single concrete change. The edit must actually change disk state; dry-runs and no-ops do not clear this directive.`,
531042
531389
  ``,
531043
531390
  `Allowed bypasses (will not be blocked but will not clear the directive either):`,
531044
531391
  ` • web_search — search the EXACT recurring error string`,
@@ -531055,14 +531402,14 @@ ${memoryLines.join("\n")}`
531055
531402
  ` • file_write — create a new file`,
531056
531403
  ` • file_edit — modify an existing file (find/replace)`,
531057
531404
  ` • batch_edit — multiple find/replace edits in one call`,
531058
- ` • file_patch — apply a unified diff`,
531405
+ ` • file_patch — apply a version-checked line-range patch`,
531059
531406
  ``,
531060
531407
  `These tools are also allowed while the directive is active (will not be blocked, will not clear the gate):`,
531061
531408
  ` • web_search — for genuinely-unknown APIs / error strings`,
531062
531409
  ` • task_complete — to exit if you cannot make any progress`,
531063
531410
  ` • ask_user — to escalate to human (if available)`,
531064
531411
  ``,
531065
- `Until you issue a creative edit, ALL of these will be BLOCKED again on every turn: file_read, file_explore, list_directory, grep_search, shell, todo_write, todo_read, memory_read, memory_write, etc. Pick the smallest concrete change that moves work forward — even a partial stub or a single-line edit counts.`
531412
+ `Until you issue a creative edit that actually changes disk, ALL of these will be BLOCKED again on every turn: file_read, file_explore, list_directory, grep_search, shell, todo_write, todo_read, memory_read, memory_write, etc. Pick the smallest concrete change that moves work forward.`
531066
531413
  ].join("\n");
531067
531414
  this.emit({
531068
531415
  type: "tool_result",
@@ -531514,6 +531861,16 @@ Respond with EXACTLY this structure before your next tool call:
531514
531861
  }
531515
531862
  }
531516
531863
  }
531864
+ const realFileMutation = this._isRealProjectMutation(tc.name, result);
531865
+ const realMutationPaths = realFileMutation ? this._extractToolTargetPaths(tc.name, tc.arguments, result) : [];
531866
+ if (realFileMutation && this._reg61PerpetualGateActive) {
531867
+ this._reg61PerpetualGateActive = false;
531868
+ this.emit({
531869
+ type: "status",
531870
+ content: `REG-61 GATE CLEARED — '${tc.name}' landed real file mutation at turn ${turn}`,
531871
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
531872
+ });
531873
+ }
531517
531874
  const updated = this._argCohorts.get(cohortKey) || { success: 0, failure: 0, lastOutcomeTurn: turn };
531518
531875
  if (result.success)
531519
531876
  updated.success++;
@@ -531522,9 +531879,9 @@ Respond with EXACTLY this structure before your next tool call:
531522
531879
  updated.lastOutcomeTurn = turn;
531523
531880
  this._argCohorts.set(cohortKey, updated);
531524
531881
  try {
531525
- if (tc.name === "file_write" || tc.name === "file_edit" || tc.name === "edit_block" || tc.name === "batch_edit") {
531526
- const p2 = String(tc.arguments?.["path"] ?? tc.arguments?.["file_path"] ?? tc.arguments?.["file"] ?? "");
531527
- if (p2 && result.success) {
531882
+ if (realFileMutation) {
531883
+ const paths = realMutationPaths.length > 0 ? realMutationPaths : this._extractToolTargetPaths(tc.name, tc.arguments, result);
531884
+ for (const p2 of paths) {
531528
531885
  const prev = this._worldFacts.files.get(p2);
531529
531886
  this._worldFacts.files.set(p2, {
531530
531887
  exists: true,
@@ -531534,7 +531891,9 @@ Respond with EXACTLY this structure before your next tool call:
531534
531891
  lastWriteTurn: turn,
531535
531892
  writeCount: (prev?.writeCount ?? 0) + 1
531536
531893
  });
531537
- this._writesSinceLastTodoWrite++;
531894
+ }
531895
+ if (paths.length > 0) {
531896
+ this._writesSinceLastTodoWrite += paths.length;
531538
531897
  if (this._writesSinceLastTodoWrite >= 6 && !this._progressGateActive) {
531539
531898
  this._progressGateActive = true;
531540
531899
  this.emit({
@@ -531907,12 +532266,17 @@ Respond with EXACTLY this structure before your next tool call:
531907
532266
  const lastLog = toolCallLog[toolCallLog.length - 1];
531908
532267
  if (lastLog) {
531909
532268
  lastLog.success = false;
532269
+ lastLog.mutated = false;
532270
+ lastLog.mutatedFiles = [];
531910
532271
  lastLog.outputPreview = errorText.slice(0, 100);
531911
532272
  }
531912
532273
  } else if (result.success) {
531913
532274
  const lastLog = toolCallLog[toolCallLog.length - 1];
531914
- if (lastLog)
532275
+ if (lastLog) {
531915
532276
  lastLog.success = true;
532277
+ lastLog.mutated = realFileMutation;
532278
+ lastLog.mutatedFiles = realMutationPaths;
532279
+ }
531916
532280
  if (tc.name === "shell") {
531917
532281
  const _shellCmd = String(tc.arguments?.["command"] ?? tc.arguments?.["cmd"] ?? "");
531918
532282
  const _typecheckOnly = /\b(--noEmit|--dry-run|--check\b|\bmypy\b|\bruff check\b|\bcargo check\b|\bstylelint --check\b|\bpylint\b(?!.*--exit-zero))\b/i.test(_shellCmd);
@@ -532013,16 +532377,16 @@ Respond with EXACTLY this structure before your next tool call:
532013
532377
  } catch {
532014
532378
  }
532015
532379
  }
532016
- if (["file_write", "file_edit", "file_patch", "batch_edit"].includes(tc.name) && this._patchHistoryStore) {
532380
+ if (realFileMutation && this._patchHistoryStore) {
532017
532381
  try {
532018
- const filePath2 = tc.arguments?.path || tc.arguments?.file_path;
532019
- if (filePath2) {
532382
+ const patchFiles = realMutationPaths.length > 0 ? realMutationPaths : this._extractToolTargetPaths(tc.name, tc.arguments, result);
532383
+ for (const filePath2 of patchFiles) {
532020
532384
  this._patchHistoryStore.insert({
532021
532385
  taskId: this._sessionId,
532022
532386
  sessionId: this._sessionId,
532023
532387
  repoRoot: process.cwd(),
532024
532388
  filePath: filePath2,
532025
- diff: JSON.stringify(tc.arguments).slice(0, 1e3),
532389
+ diff: (result.diff ?? JSON.stringify(tc.arguments)).slice(0, 1e3),
532026
532390
  status: "applied",
532027
532391
  appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
532028
532392
  revertedAt: null,
@@ -532043,13 +532407,13 @@ Respond with EXACTLY this structure before your next tool call:
532043
532407
  }
532044
532408
  }
532045
532409
  }
532046
- const isFileMutation = ["file_write", "file_edit", "batch_edit", "file_patch"].includes(tc.name);
532047
- if (isFileMutation && result.success && dedupHitCount.size > 0) {
532410
+ const isFileMutation = realFileMutation;
532411
+ if (isFileMutation && dedupHitCount.size > 0) {
532048
532412
  dedupHitCount.clear();
532049
532413
  }
532050
- if (isFileMutation && result.success) {
532051
- this._fileWritesSinceLastWorldState++;
532052
- this._fileWritesThisRun++;
532414
+ if (isFileMutation) {
532415
+ this._fileWritesSinceLastWorldState += Math.max(1, realMutationPaths.length);
532416
+ this._fileWritesThisRun += Math.max(1, realMutationPaths.length);
532053
532417
  }
532054
532418
  if (result.success) {
532055
532419
  this._recentFailures = this._recentFailures.filter((f2) => f2.fingerprint !== toolFingerprint);
@@ -532197,14 +532561,14 @@ Respond with EXACTLY this structure before your next tool call:
532197
532561
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
532198
532562
  });
532199
532563
  this._taskState.toolCallCount++;
532200
- if (result && result.success !== false) {
532201
- const creativeTools = ["file_write", "file_edit", "batch_edit", "file_patch"];
532202
- if (creativeTools.includes(tc.name)) {
532203
- this._lastFileWriteTurn = turn;
532564
+ if (realFileMutation) {
532565
+ this._lastFileWriteTurn = turn;
532566
+ const writeEvents = Math.max(1, realMutationPaths.length);
532567
+ for (let i2 = 0; i2 < writeEvents; i2++) {
532204
532568
  this._fileWriteTimestamps.push(Date.now());
532205
- if (this._fileWriteTimestamps.length > 200) {
532206
- this._fileWriteTimestamps.shift();
532207
- }
532569
+ }
532570
+ while (this._fileWriteTimestamps.length > 200) {
532571
+ this._fileWriteTimestamps.shift();
532208
532572
  }
532209
532573
  }
532210
532574
  this._trackDecomp2(tc, result, turn);
@@ -544333,7 +544697,7 @@ var require_websocket3 = __commonJS({
544333
544697
  var http6 = __require("http");
544334
544698
  var net5 = __require("net");
544335
544699
  var tls2 = __require("tls");
544336
- var { randomBytes: randomBytes25, createHash: createHash20 } = __require("crypto");
544700
+ var { randomBytes: randomBytes25, createHash: createHash21 } = __require("crypto");
544337
544701
  var { Duplex: Duplex3, Readable } = __require("stream");
544338
544702
  var { URL: URL3 } = __require("url");
544339
544703
  var PerMessageDeflate2 = require_permessage_deflate3();
@@ -544993,7 +545357,7 @@ var require_websocket3 = __commonJS({
544993
545357
  abortHandshake(websocket, socket, "Invalid Upgrade header");
544994
545358
  return;
544995
545359
  }
544996
- const digest3 = createHash20("sha1").update(key + GUID).digest("base64");
545360
+ const digest3 = createHash21("sha1").update(key + GUID).digest("base64");
544997
545361
  if (res.headers["sec-websocket-accept"] !== digest3) {
544998
545362
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
544999
545363
  return;
@@ -545360,7 +545724,7 @@ var require_websocket_server2 = __commonJS({
545360
545724
  var EventEmitter13 = __require("events");
545361
545725
  var http6 = __require("http");
545362
545726
  var { Duplex: Duplex3 } = __require("stream");
545363
- var { createHash: createHash20 } = __require("crypto");
545727
+ var { createHash: createHash21 } = __require("crypto");
545364
545728
  var extension2 = require_extension3();
545365
545729
  var PerMessageDeflate2 = require_permessage_deflate3();
545366
545730
  var subprotocol2 = require_subprotocol2();
@@ -545661,7 +546025,7 @@ var require_websocket_server2 = __commonJS({
545661
546025
  );
545662
546026
  }
545663
546027
  if (this._state > RUNNING) return abortHandshake(socket, 503);
545664
- const digest3 = createHash20("sha1").update(key + GUID).digest("base64");
546028
+ const digest3 = createHash21("sha1").update(key + GUID).digest("base64");
545665
546029
  const headers = [
545666
546030
  "HTTP/1.1 101 Switching Protocols",
545667
546031
  "Upgrade: websocket",
@@ -546721,7 +547085,7 @@ __export(oa_directory_exports, {
546721
547085
  import { existsSync as existsSync74, mkdirSync as mkdirSync42, readFileSync as readFileSync61, writeFileSync as writeFileSync38, readdirSync as readdirSync21, statSync as statSync25, unlinkSync as unlinkSync14, openSync as openSync2, closeSync as closeSync2, renameSync as renameSync3 } from "node:fs";
546722
547086
  import { join as join93, relative as relative8, basename as basename14, dirname as dirname27 } from "node:path";
546723
547087
  import { homedir as homedir26 } from "node:os";
546724
- import { createHash as createHash13 } from "node:crypto";
547088
+ import { createHash as createHash14 } from "node:crypto";
546725
547089
  function findGitRoot(startDir) {
546726
547090
  let dir = startDir;
546727
547091
  const visited = /* @__PURE__ */ new Set();
@@ -547086,7 +547450,7 @@ function buildHandoffPrompt(repoRoot) {
547086
547450
  return lines.join("\n");
547087
547451
  }
547088
547452
  function computeDedupeHash(task, savedAt) {
547089
- return createHash13("sha256").update(`${task}|${savedAt}`).digest("hex").slice(0, 16);
547453
+ return createHash14("sha256").update(`${task}|${savedAt}`).digest("hex").slice(0, 16);
547090
547454
  }
547091
547455
  function generateSessionId() {
547092
547456
  const timestamp = Date.now().toString(36);
@@ -575641,7 +576005,7 @@ var init_types = __esm({
575641
576005
  });
575642
576006
 
575643
576007
  // packages/cli/src/tui/p2p/secret-vault.ts
575644
- import { createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv3, randomBytes as randomBytes19, scryptSync as scryptSync2, createHash as createHash14 } from "node:crypto";
576008
+ import { createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv3, randomBytes as randomBytes19, scryptSync as scryptSync2, createHash as createHash15 } from "node:crypto";
575645
576009
  import { readFileSync as readFileSync71, writeFileSync as writeFileSync46, existsSync as existsSync86, mkdirSync as mkdirSync50 } from "node:fs";
575646
576010
  import { dirname as dirname31 } from "node:path";
575647
576011
  var PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, CIPHER_ALGO, SALT_LEN, IV_LEN, KEY_LEN, SecretVault;
@@ -575886,7 +576250,7 @@ var init_secret_vault = __esm({
575886
576250
  /** Generate a deterministic fingerprint of vault contents (for sync verification) */
575887
576251
  fingerprint() {
575888
576252
  const names = Array.from(this.secrets.keys()).sort();
575889
- const hash = createHash14("sha256");
576253
+ const hash = createHash15("sha256");
575890
576254
  for (const name10 of names) {
575891
576255
  hash.update(name10 + ":");
575892
576256
  hash.update(this.secrets.get(name10).value);
@@ -575901,7 +576265,7 @@ var init_secret_vault = __esm({
575901
576265
  // packages/cli/src/tui/p2p/peer-mesh.ts
575902
576266
  import { EventEmitter as EventEmitter7 } from "node:events";
575903
576267
  import { createServer as createServer5 } from "node:http";
575904
- import { randomBytes as randomBytes20, createHash as createHash15, generateKeyPairSync } from "node:crypto";
576268
+ import { randomBytes as randomBytes20, createHash as createHash16, generateKeyPairSync } from "node:crypto";
575905
576269
  var PING_INTERVAL_MS, PEER_TIMEOUT_MS, GOSSIP_INTERVAL_MS, MAX_PEERS, PeerMesh;
575906
576270
  var init_peer_mesh = __esm({
575907
576271
  "packages/cli/src/tui/p2p/peer-mesh.ts"() {
@@ -575918,7 +576282,7 @@ var init_peer_mesh = __esm({
575918
576282
  const { publicKey: publicKey2, privateKey } = generateKeyPairSync("ed25519");
575919
576283
  this.publicKey = publicKey2.export({ type: "spki", format: "der" });
575920
576284
  this.privateKey = privateKey.export({ type: "pkcs8", format: "der" });
575921
- this.peerId = createHash15("sha256").update(this.publicKey).digest("base64url").slice(0, 22);
576285
+ this.peerId = createHash16("sha256").update(this.publicKey).digest("base64url").slice(0, 22);
575922
576286
  this.capabilities = options2.capabilities;
575923
576287
  this.displayName = options2.displayName;
575924
576288
  this._authKey = options2.authKey ?? randomBytes20(24).toString("base64url");
@@ -585025,14 +585389,14 @@ var init_access_policy = __esm({
585025
585389
  });
585026
585390
 
585027
585391
  // packages/cli/src/api/project-preferences.ts
585028
- import { createHash as createHash16 } from "node:crypto";
585392
+ import { createHash as createHash17 } from "node:crypto";
585029
585393
  import { existsSync as existsSync97, mkdirSync as mkdirSync59, readFileSync as readFileSync81, renameSync as renameSync7, writeFileSync as writeFileSync53, unlinkSync as unlinkSync22 } from "node:fs";
585030
585394
  import { homedir as homedir36 } from "node:os";
585031
585395
  import { join as join116, resolve as resolve36 } from "node:path";
585032
585396
  import { randomUUID as randomUUID15 } from "node:crypto";
585033
585397
  function projectKey(root) {
585034
585398
  const canonical = resolve36(root);
585035
- return createHash16("sha256").update(canonical).digest("hex").slice(0, 16);
585399
+ return createHash17("sha256").update(canonical).digest("hex").slice(0, 16);
585036
585400
  }
585037
585401
  function projectDir(root) {
585038
585402
  return join116(PROJECTS_DIR, projectKey(root));
@@ -586210,7 +586574,7 @@ var init_disk_task_output = __esm({
586210
586574
  });
586211
586575
 
586212
586576
  // packages/cli/src/api/http.ts
586213
- import { createHash as createHash17 } from "node:crypto";
586577
+ import { createHash as createHash18 } from "node:crypto";
586214
586578
  function problemDetails(opts) {
586215
586579
  const p2 = {
586216
586580
  type: opts.type ?? "about:blank",
@@ -586273,7 +586637,7 @@ function paginated(items, page2, total) {
586273
586637
  }
586274
586638
  function computeEtag(payload) {
586275
586639
  const json = typeof payload === "string" ? payload : JSON.stringify(payload);
586276
- const hash = createHash17("sha1").update(json).digest("hex").slice(0, 16);
586640
+ const hash = createHash18("sha1").update(json).digest("hex").slice(0, 16);
586277
586641
  return `W/"${hash}"`;
586278
586642
  }
586279
586643
  function checkNotModified(req2, res, etag) {
@@ -600253,7 +600617,7 @@ import { homedir as homedir43 } from "node:os";
600253
600617
  import { spawn as spawn26, execSync as execSync57 } from "node:child_process";
600254
600618
  import { mkdirSync as mkdirSync67, writeFileSync as writeFileSync59, readFileSync as readFileSync89, readdirSync as readdirSync36, existsSync as existsSync108, watch as fsWatch3, renameSync as renameSync8, unlinkSync as unlinkSync24 } from "node:fs";
600255
600619
  import { randomBytes as randomBytes23, randomUUID as randomUUID16 } from "node:crypto";
600256
- import { createHash as createHash19 } from "node:crypto";
600620
+ import { createHash as createHash20 } from "node:crypto";
600257
600621
  function getVersion3() {
600258
600622
  try {
600259
600623
  const thisDir = dirname36(fileURLToPath17(import.meta.url));
@@ -605894,7 +606258,7 @@ function listScheduledTasks() {
605894
606258
  const schedule = String(t2?.schedule || t2?.cron || t2?.when || "");
605895
606259
  const enabled2 = typeof t2?.enabled === "boolean" ? t2.enabled : true;
605896
606260
  const realId = typeof t2?.id === "string" && t2.id ? t2.id : null;
605897
- const fallbackId = createHash19("sha1").update(`${file}#${i2}`).digest("hex").slice(0, 16);
606261
+ const fallbackId = createHash20("sha1").update(`${file}#${i2}`).digest("hex").slice(0, 16);
605898
606262
  const uid = realId || fallbackId;
605899
606263
  const key = `${uid}`;
605900
606264
  if (seen.has(key)) return;
@@ -606011,8 +606375,8 @@ function deleteScheduledById(id) {
606011
606375
  if (id) candidates.push(id);
606012
606376
  if (typeof entry?.id === "string" && entry.id && !candidates.includes(entry.id)) candidates.push(entry.id);
606013
606377
  try {
606014
- const { createHash: createHash20 } = require3("node:crypto");
606015
- const fallback = createHash20("sha1").update(`${target.file}#${target.index}`).digest("hex").slice(0, 16);
606378
+ const { createHash: createHash21 } = require3("node:crypto");
606379
+ const fallback = createHash21("sha1").update(`${target.file}#${target.index}`).digest("hex").slice(0, 16);
606016
606380
  if (!candidates.includes(fallback)) candidates.push(fallback);
606017
606381
  } catch {
606018
606382
  }
@@ -613679,13 +614043,13 @@ NEW TASK: ${fullInput}`;
613679
614043
  writeContent(() => renderError2(errMsg));
613680
614044
  if (failureStore) {
613681
614045
  try {
613682
- const { createHash: createHash20 } = await import("node:crypto");
614046
+ const { createHash: createHash21 } = await import("node:crypto");
613683
614047
  failureStore.insert({
613684
614048
  taskId: "",
613685
614049
  sessionId: `${Date.now()}`,
613686
614050
  repoRoot,
613687
614051
  failureType: "runtime-error",
613688
- fingerprint: createHash20("sha256").update(errMsg.slice(0, 200)).digest("hex").slice(0, 16),
614052
+ fingerprint: createHash21("sha256").update(errMsg.slice(0, 200)).digest("hex").slice(0, 16),
613689
614053
  filePath: null,
613690
614054
  errorMessage: errMsg.slice(0, 500),
613691
614055
  context: null,