@lazyingart/agintiflow 0.20.252 → 0.20.253

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.252",
3
+ "version": "0.20.253",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
6
6
  "license": "Apache-2.0",
@@ -341,6 +341,129 @@ assert.ok(
341
341
  "DeepSeek runtime compaction exceeded the bounded retry target"
342
342
  );
343
343
 
344
+ const staleReadAfterMutationState = {
345
+ goal: "Repair service_ctl.py from current source and verify the service lifecycle tests.",
346
+ plan: "Use current source state and latest test evidence.",
347
+ messages: [
348
+ { role: "system", content: "Preserve current source truth across compaction." },
349
+ { role: "user", content: "Continue the current service recovery task." },
350
+ {
351
+ role: "assistant",
352
+ content: "",
353
+ tool_calls: [
354
+ {
355
+ id: "stale-source-read",
356
+ type: "function",
357
+ function: {
358
+ name: "read_file",
359
+ arguments: JSON.stringify({ path: "service_ctl.py", startLine: 1, lineLimit: 40 }),
360
+ },
361
+ },
362
+ ],
363
+ },
364
+ {
365
+ role: "tool",
366
+ tool_call_id: "stale-source-read",
367
+ content: JSON.stringify({
368
+ ok: true,
369
+ toolName: "read_file",
370
+ path: "service_ctl.py",
371
+ startLine: 1,
372
+ lineLimit: 40,
373
+ lineCount: 80,
374
+ bytes: 3200,
375
+ sha256: "1111111111111111111111111111111111111111111111111111111111111111",
376
+ contentTruncated: false,
377
+ content: "STALE-SERVICE-CONTENT command = f'python gateway_service.py'",
378
+ }),
379
+ },
380
+ {
381
+ role: "assistant",
382
+ content: "",
383
+ tool_calls: [
384
+ {
385
+ id: "current-source-mutation",
386
+ type: "function",
387
+ function: {
388
+ name: "apply_patch",
389
+ arguments: JSON.stringify({
390
+ path: "service_ctl.py",
391
+ search: "command = f'python gateway_service.py'",
392
+ replace: "command = [sys.executable, 'gateway_service.py']",
393
+ }),
394
+ },
395
+ },
396
+ ],
397
+ },
398
+ {
399
+ role: "tool",
400
+ tool_call_id: "current-source-mutation",
401
+ content: JSON.stringify({
402
+ ok: true,
403
+ toolName: "apply_patch",
404
+ path: "service_ctl.py",
405
+ summary: "1 file change applied",
406
+ }),
407
+ },
408
+ {
409
+ role: "assistant",
410
+ content: "",
411
+ tool_calls: [
412
+ {
413
+ id: "fresh-source-read",
414
+ type: "function",
415
+ function: {
416
+ name: "read_file",
417
+ arguments: JSON.stringify({ path: "service_ctl.py", startLine: 41, lineLimit: 40 }),
418
+ },
419
+ },
420
+ ],
421
+ },
422
+ {
423
+ role: "tool",
424
+ tool_call_id: "fresh-source-read",
425
+ content: JSON.stringify({
426
+ ok: true,
427
+ toolName: "read_file",
428
+ path: "service_ctl.py",
429
+ startLine: 41,
430
+ lineLimit: 40,
431
+ lineCount: 80,
432
+ bytes: 3300,
433
+ sha256: "2222222222222222222222222222222222222222222222222222222222222222",
434
+ contentTruncated: false,
435
+ content: "FRESH-SERVICE-CONTENT shell=False and lifecycle helpers are current",
436
+ }),
437
+ },
438
+ ...Array.from({ length: 8 }, (_, index) => readOnlyDiagnosticPair(index + 30)).flat(),
439
+ ],
440
+ };
441
+ const staleReadAfterMutationMessages = buildContextBudgetCompactionMessages(
442
+ staleReadAfterMutationState,
443
+ config,
444
+ { title: "", url: "" },
445
+ 12,
446
+ { reason: "discard stale pre-mutation source reads" }
447
+ );
448
+ const staleReadAfterMutationText = staleReadAfterMutationMessages
449
+ .map((message) => message.content || "")
450
+ .join("\n");
451
+ assert.ok(
452
+ !staleReadAfterMutationText.includes("STALE-SERVICE-CONTENT"),
453
+ "compaction retained a source read that predates a successful mutation of the same file"
454
+ );
455
+ assert.ok(
456
+ staleReadAfterMutationText.includes("FRESH-SERVICE-CONTENT"),
457
+ "compaction discarded the bounded source read made after the successful mutation"
458
+ );
459
+ assert.ok(
460
+ staleReadAfterMutationMessages.some((message) =>
461
+ Array.isArray(message.tool_calls) &&
462
+ message.tool_calls.some((call) => call?.function?.name === "apply_patch")
463
+ ),
464
+ "compaction discarded the successful mutation while invalidating its stale predecessor read"
465
+ );
466
+
344
467
  function noisyFullReadPair(index, generation) {
345
468
  const id = `validator-${generation}-${index}`;
346
469
  const file = `tmp/validator-${generation}-${index}.py`;
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import assert from "node:assert/strict";
3
+ import crypto from "node:crypto";
3
4
  import fs from "node:fs/promises";
4
5
  import os from "node:os";
5
6
  import path from "node:path";
@@ -11,7 +12,11 @@ import {
11
12
  decideLocalFailureRecovery,
12
13
  localFailureRecoveryInstruction,
13
14
  } from "../src/local-failure-recovery.js";
14
- import { nextStepRuntimeConfig, runAgent } from "../src/agent-runner.js";
15
+ import {
16
+ nextStepRuntimeConfig,
17
+ repeatedSuccessfulMutationBlock,
18
+ runAgent,
19
+ } from "../src/agent-runner.js";
15
20
  import { resolveRuntimeConfig } from "../src/config.js";
16
21
  import { SessionStore } from "../src/session-store.js";
17
22
 
@@ -190,6 +195,89 @@ assert.match(instruction, /preserve successful work/i);
190
195
  assert.match(instruction, /Do not repeat the failing call/i);
191
196
  assert.match(instruction, /rerun the smallest relevant verification/i);
192
197
 
198
+ const durablePatchArgs = {
199
+ path: "service_ctl.py",
200
+ search: "command = f'python gateway_service.py'",
201
+ replace: "command = [sys.executable, 'gateway_service.py']",
202
+ };
203
+ const digest = (value) => crypto.createHash("sha256").update(value).digest("hex");
204
+ const durableMutation = {
205
+ revision: 4,
206
+ goalRevision: 7,
207
+ toolName: "apply_patch",
208
+ paths: ["service_ctl.py"],
209
+ patch: {
210
+ path: "./service_ctl.py",
211
+ searchHash: digest(durablePatchArgs.search),
212
+ replaceHash: digest(durablePatchArgs.replace),
213
+ },
214
+ };
215
+ const durablePatchState = {
216
+ meta: {
217
+ goalContract: { revision: 7 },
218
+ toolLoop: {
219
+ recent: Array.from({ length: 20 }, (_, index) =>
220
+ failed("apply_patch", `later-failure-${index}`)
221
+ ),
222
+ stagnationEpoch: 23,
223
+ },
224
+ projectVerification: {
225
+ mutationRevision: 4,
226
+ mutationHistory: [durableMutation],
227
+ },
228
+ },
229
+ };
230
+ assert.equal(
231
+ repeatedSuccessfulMutationBlock(durablePatchState, "apply_patch", durablePatchArgs, {
232
+ commandCwd: process.cwd(),
233
+ })?.category,
234
+ "repeated-successful-mutation",
235
+ "a successful exact patch must remain blocked after it falls out of the short tool-loop window"
236
+ );
237
+ assert.equal(
238
+ repeatedSuccessfulMutationBlock(
239
+ {
240
+ ...durablePatchState,
241
+ meta: {
242
+ ...durablePatchState.meta,
243
+ goalContract: { revision: 8 },
244
+ },
245
+ },
246
+ "apply_patch",
247
+ durablePatchArgs,
248
+ { commandCwd: process.cwd() }
249
+ ),
250
+ null,
251
+ "a genuine user continuation must permit deliberate reconsideration of an earlier patch"
252
+ );
253
+ assert.equal(
254
+ repeatedSuccessfulMutationBlock(
255
+ {
256
+ ...durablePatchState,
257
+ meta: {
258
+ ...durablePatchState.meta,
259
+ projectVerification: {
260
+ mutationRevision: 5,
261
+ mutationHistory: [
262
+ durableMutation,
263
+ {
264
+ revision: 5,
265
+ goalRevision: 7,
266
+ toolName: "write_file",
267
+ paths: ["service_ctl.py"],
268
+ },
269
+ ],
270
+ },
271
+ },
272
+ },
273
+ "apply_patch",
274
+ durablePatchArgs,
275
+ { commandCwd: process.cwd() }
276
+ ),
277
+ null,
278
+ "an intervening successful mutation must permit the same exact patch when source state changed"
279
+ );
280
+
193
281
  function assistant(content, toolCalls = []) {
194
282
  return {
195
283
  choices: [{ message: { role: "assistant", content, ...(toolCalls.length ? { tool_calls: toolCalls } : {}) } }],
@@ -877,11 +877,22 @@ function summarizeRetainedSourceEvidence(messages = [], limit = 28) {
877
877
  const payload = retained?.payload || safeParseToolContent(message.content);
878
878
  if (!payload || payload.ok === false || payload.blocked || payload.skipped) continue;
879
879
  const toolName = String(retained?.name || payload.toolName || payload.name || "");
880
+ const args = retained?.args || payload.args || {};
881
+ const sourcePath = String(payload.path || args.path || "").trim();
882
+ if (["apply_patch", "write_file"].includes(toolName) && sourcePath) {
883
+ for (const [key, record] of bySource.entries()) {
884
+ if (
885
+ record.toolName === "read_file" &&
886
+ retainedPathMatchesAny(record.sourcePath, [sourcePath])
887
+ ) {
888
+ bySource.delete(key);
889
+ }
890
+ }
891
+ continue;
892
+ }
880
893
  if (!["read_file", "list_files", "search_files", "inspect_project", "run_command"].includes(toolName)) {
881
894
  continue;
882
895
  }
883
- const args = retained?.args || payload.args || {};
884
- const sourcePath = String(payload.path || args.path || "").trim();
885
896
  const command = String(args.command || "").trim();
886
897
  const readRange = toolName === "read_file" ? retainedReadRange(payload, args) : null;
887
898
  const key = `${toolName}:${sourcePath || command}${readRange ? `:${readRange.key}` : ""}`;
@@ -937,9 +948,15 @@ function summarizeRetainedSourceEvidence(messages = [], limit = 28) {
937
948
  if (payload.stdout) parts.push(`stdout=${compactSingleLine(payload.stdout, 260)}`);
938
949
  if (payload.stderr) parts.push(`stderr=${compactSingleLine(payload.stderr, 220)}`);
939
950
  }
940
- bySource.set(key, redactSensitiveText(parts.join(" | ")));
951
+ bySource.set(key, {
952
+ toolName,
953
+ sourcePath,
954
+ text: redactSensitiveText(parts.join(" | ")),
955
+ });
941
956
  }
942
- return [...bySource.values()].slice(-Math.max(1, Number(limit) || 28));
957
+ return [...bySource.values()]
958
+ .slice(-Math.max(1, Number(limit) || 28))
959
+ .map((record) => record.text);
943
960
  }
944
961
 
945
962
  const COMPACTION_STATE_TOOL_NAMES = new Set([
@@ -1076,6 +1093,16 @@ function retainedToolStateMessages(messages = [], limit = 12, outputPaths = [],
1076
1093
  if (!COMPACTION_STATE_TOOL_NAMES.has(name)) return;
1077
1094
  if (!payload || payload.ok === false || payload.blocked || payload.skipped) return;
1078
1095
  const sourcePath = String(payload.path || args?.path || "").trim();
1096
+ if (["apply_patch", "write_file"].includes(name) && sourcePath) {
1097
+ for (const [key, record] of recordsByKey.entries()) {
1098
+ if (
1099
+ record.name === "read_file" &&
1100
+ retainedPathMatchesAny(record.sourcePath, [sourcePath])
1101
+ ) {
1102
+ recordsByKey.delete(key);
1103
+ }
1104
+ }
1105
+ }
1079
1106
  const command = String(args?.command || payload.args?.command || "").trim();
1080
1107
  const durableIdentity = String(
1081
1108
  sourcePath || command || payload.researchId || args?.researchId || args?.query || name
@@ -1085,6 +1112,7 @@ function retainedToolStateMessages(messages = [], limit = 12, outputPaths = [],
1085
1112
  recordsByKey.set(key, {
1086
1113
  ordinal: ordinal += 1,
1087
1114
  name,
1115
+ sourcePath,
1088
1116
  args: redactValue(args),
1089
1117
  payload: compactRetainedToolPayload(name, payload, args),
1090
1118
  });
@@ -6200,7 +6228,7 @@ export function repeatedSuccessfulMutationBlock(state, toolName, args = {}, conf
6200
6228
  commandCwd: config.commandCwd,
6201
6229
  });
6202
6230
  const stagnationEpoch = Math.max(0, Number(toolLoop.stagnationEpoch || 0));
6203
- const alreadyApplied = (Array.isArray(toolLoop.recent) ? toolLoop.recent : []).some(
6231
+ const recentlyApplied = (Array.isArray(toolLoop.recent) ? toolLoop.recent : []).some(
6204
6232
  (entry) =>
6205
6233
  entry?.signature === signature &&
6206
6234
  entry?.toolName === "apply_patch" &&
@@ -6209,6 +6237,41 @@ export function repeatedSuccessfulMutationBlock(state, toolName, args = {}, conf
6209
6237
  entry?.successfulMutation === true &&
6210
6238
  Number(entry?.stagnationEpoch || 0) === stagnationEpoch
6211
6239
  );
6240
+ const verification = state.meta?.projectVerification || {};
6241
+ const history = Array.isArray(verification.mutationHistory)
6242
+ ? verification.mutationHistory
6243
+ : verification.lastMutation
6244
+ ? [verification.lastMutation]
6245
+ : [];
6246
+ const targetPath = typeof args.path === "string" ? safeRecoveryEvidencePath(args.path) : "";
6247
+ const searchHash = typeof args.search === "string" ? hashForLog(args.search) : "";
6248
+ const replaceHash = typeof args.replace === "string" ? hashForLog(args.replace) : "";
6249
+ const currentGoalRevision = Math.max(0, Number(state.meta?.goalContract?.revision || 0));
6250
+ let matchingHistoryIndex = -1;
6251
+ if (targetPath && searchHash && replaceHash) {
6252
+ for (let index = history.length - 1; index >= 0; index -= 1) {
6253
+ const mutation = history[index];
6254
+ if (
6255
+ mutation?.toolName === "apply_patch" &&
6256
+ Number(mutation?.goalRevision || 0) === currentGoalRevision &&
6257
+ safeRecoveryEvidencePath(mutation?.patch?.path) === targetPath &&
6258
+ String(mutation?.patch?.searchHash || "") === searchHash &&
6259
+ String(mutation?.patch?.replaceHash || "") === replaceHash
6260
+ ) {
6261
+ matchingHistoryIndex = index;
6262
+ break;
6263
+ }
6264
+ }
6265
+ }
6266
+ const persistentlyApplied =
6267
+ matchingHistoryIndex >= 0 &&
6268
+ !history.slice(matchingHistoryIndex + 1).some(
6269
+ (mutation) =>
6270
+ Number(mutation?.goalRevision || 0) === currentGoalRevision &&
6271
+ Number(mutation?.revision || 0) >
6272
+ Number(history[matchingHistoryIndex]?.revision || 0)
6273
+ );
6274
+ const alreadyApplied = recentlyApplied || persistentlyApplied;
6212
6275
  if (!alreadyApplied) return null;
6213
6276
  return {
6214
6277
  reason: