@lazyingart/agintiflow 0.20.252 → 0.20.254

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.254",
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,157 @@ 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
+ taskHash: digest("Repair the sensor gateway lifecycle."),
208
+ toolName: "apply_patch",
209
+ paths: ["service_ctl.py"],
210
+ patch: {
211
+ path: "./service_ctl.py",
212
+ searchHash: digest(durablePatchArgs.search),
213
+ replaceHash: digest(durablePatchArgs.replace),
214
+ },
215
+ };
216
+ const durablePatchState = {
217
+ meta: {
218
+ goalContract: {
219
+ revision: 7,
220
+ taskGoal: "Repair the sensor gateway lifecycle.",
221
+ history: [
222
+ {
223
+ revision: 7,
224
+ kind: "initial",
225
+ hash: digest("Repair the sensor gateway lifecycle."),
226
+ },
227
+ ],
228
+ },
229
+ toolLoop: {
230
+ recent: Array.from({ length: 20 }, (_, index) =>
231
+ failed("apply_patch", `later-failure-${index}`)
232
+ ),
233
+ stagnationEpoch: 23,
234
+ },
235
+ projectVerification: {
236
+ mutationRevision: 4,
237
+ mutationHistory: [durableMutation],
238
+ },
239
+ },
240
+ };
241
+ assert.equal(
242
+ repeatedSuccessfulMutationBlock(durablePatchState, "apply_patch", durablePatchArgs, {
243
+ commandCwd: process.cwd(),
244
+ })?.category,
245
+ "repeated-successful-mutation",
246
+ "a successful exact patch must remain blocked after it falls out of the short tool-loop window"
247
+ );
248
+ assert.equal(
249
+ repeatedSuccessfulMutationBlock(
250
+ {
251
+ ...durablePatchState,
252
+ meta: {
253
+ ...durablePatchState.meta,
254
+ goalContract: {
255
+ revision: 8,
256
+ taskGoal: "Repair the sensor gateway lifecycle.",
257
+ history: [
258
+ ...durablePatchState.meta.goalContract.history,
259
+ {
260
+ revision: 8,
261
+ kind: "same-task-continuation",
262
+ relation: "same-task",
263
+ taskHash: digest("Repair the sensor gateway lifecycle."),
264
+ },
265
+ ],
266
+ },
267
+ },
268
+ },
269
+ "apply_patch",
270
+ durablePatchArgs,
271
+ { commandCwd: process.cwd() }
272
+ )?.category,
273
+ "repeated-successful-mutation",
274
+ "a same-task continuation must not forget an earlier successful mutation"
275
+ );
276
+ assert.equal(
277
+ repeatedSuccessfulMutationBlock(
278
+ {
279
+ ...durablePatchState,
280
+ meta: {
281
+ ...durablePatchState.meta,
282
+ goalContract: {
283
+ revision: 8,
284
+ taskGoal: "Implement a separate service migration.",
285
+ history: [
286
+ ...durablePatchState.meta.goalContract.history,
287
+ {
288
+ revision: 8,
289
+ kind: "continuation",
290
+ relation: "new-request",
291
+ taskHash: digest("Implement a separate service migration."),
292
+ },
293
+ ],
294
+ },
295
+ },
296
+ },
297
+ "apply_patch",
298
+ durablePatchArgs,
299
+ { commandCwd: process.cwd() }
300
+ ),
301
+ null,
302
+ "a genuine new task boundary must not inherit an unrelated mutation block"
303
+ );
304
+ const sanitizedDurablePatchArgs = {
305
+ ...durablePatchArgs,
306
+ search: durablePatchArgs.search.slice(0, 20),
307
+ replace: durablePatchArgs.replace.slice(0, 20),
308
+ searchHash: digest(durablePatchArgs.search),
309
+ replaceHash: digest(durablePatchArgs.replace),
310
+ };
311
+ assert.equal(
312
+ repeatedSuccessfulMutationBlock(
313
+ durablePatchState,
314
+ "apply_patch",
315
+ sanitizedDurablePatchArgs,
316
+ { commandCwd: process.cwd() }
317
+ )?.category,
318
+ "repeated-successful-mutation",
319
+ "redacted patch previews must use their retained full-content hashes for idempotency"
320
+ );
321
+ assert.equal(
322
+ repeatedSuccessfulMutationBlock(
323
+ {
324
+ ...durablePatchState,
325
+ meta: {
326
+ ...durablePatchState.meta,
327
+ projectVerification: {
328
+ mutationRevision: 5,
329
+ mutationHistory: [
330
+ durableMutation,
331
+ {
332
+ revision: 5,
333
+ goalRevision: 7,
334
+ toolName: "write_file",
335
+ paths: ["service_ctl.py"],
336
+ },
337
+ ],
338
+ },
339
+ },
340
+ },
341
+ "apply_patch",
342
+ durablePatchArgs,
343
+ { commandCwd: process.cwd() }
344
+ ),
345
+ null,
346
+ "an intervening successful mutation must permit the same exact patch when source state changed"
347
+ );
348
+
193
349
  function assistant(content, toolCalls = []) {
194
350
  return {
195
351
  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
  });
@@ -4369,9 +4397,13 @@ function clearRequiredCommandBatch(verification = {}) {
4369
4397
  }
4370
4398
 
4371
4399
  function appendProjectMutation(state = {}, verification = {}, mutation = {}, config = {}) {
4400
+ const taskGoal = String(
4401
+ state.meta?.goalContract?.taskGoal || state.goal || ""
4402
+ ).trim();
4372
4403
  const record = {
4373
4404
  ...mutation,
4374
4405
  goalRevision: Math.max(0, Number(state.meta?.goalContract?.revision || 0)),
4406
+ taskHash: taskGoal ? hashForLog(taskGoal) : "",
4375
4407
  revision: Math.max(0, Number(mutation.revision || verification.mutationRevision || 0)),
4376
4408
  paths: Array.isArray(mutation.paths)
4377
4409
  ? [...new Set(mutation.paths.map((item) => String(item || "")).filter(Boolean))].slice(0, 24)
@@ -6200,7 +6232,7 @@ export function repeatedSuccessfulMutationBlock(state, toolName, args = {}, conf
6200
6232
  commandCwd: config.commandCwd,
6201
6233
  });
6202
6234
  const stagnationEpoch = Math.max(0, Number(toolLoop.stagnationEpoch || 0));
6203
- const alreadyApplied = (Array.isArray(toolLoop.recent) ? toolLoop.recent : []).some(
6235
+ const recentlyApplied = (Array.isArray(toolLoop.recent) ? toolLoop.recent : []).some(
6204
6236
  (entry) =>
6205
6237
  entry?.signature === signature &&
6206
6238
  entry?.toolName === "apply_patch" &&
@@ -6209,10 +6241,79 @@ export function repeatedSuccessfulMutationBlock(state, toolName, args = {}, conf
6209
6241
  entry?.successfulMutation === true &&
6210
6242
  Number(entry?.stagnationEpoch || 0) === stagnationEpoch
6211
6243
  );
6244
+ const verification = state.meta?.projectVerification || {};
6245
+ const history = Array.isArray(verification.mutationHistory)
6246
+ ? verification.mutationHistory
6247
+ : verification.lastMutation
6248
+ ? [verification.lastMutation]
6249
+ : [];
6250
+ const targetPath = typeof args.path === "string" ? safeRecoveryEvidencePath(args.path) : "";
6251
+ const searchHash = String(
6252
+ args.searchHash || (typeof args.search === "string" ? hashForLog(args.search) : "")
6253
+ );
6254
+ const replaceHash = String(
6255
+ args.replaceHash || (typeof args.replace === "string" ? hashForLog(args.replace) : "")
6256
+ );
6257
+ const goalContract = state.meta?.goalContract || {};
6258
+ const currentGoalRevision = Math.max(0, Number(goalContract.revision || 0));
6259
+ const currentTaskGoal = String(goalContract.taskGoal || state.goal || "").trim();
6260
+ const currentTaskHash = currentTaskGoal ? hashForLog(currentTaskGoal) : "";
6261
+ const goalHistory = Array.isArray(goalContract.history) ? goalContract.history : [];
6262
+ const mutationBelongsToCurrentTask = (mutation = {}) => {
6263
+ const mutationGoalRevision = Math.max(0, Number(mutation.goalRevision || 0));
6264
+ const goalEntry = goalHistory.find(
6265
+ (entry) => Number(entry?.revision || 0) === mutationGoalRevision
6266
+ );
6267
+ const mutationTaskHash = String(
6268
+ mutation.taskHash ||
6269
+ goalEntry?.taskHash ||
6270
+ (goalEntry?.kind === "initial" ? goalEntry?.hash : "") ||
6271
+ ""
6272
+ );
6273
+ if (currentTaskHash && mutationTaskHash) return currentTaskHash === mutationTaskHash;
6274
+ if (mutationGoalRevision === currentGoalRevision) return true;
6275
+ const interveningGoalEntries = goalHistory.filter((entry) => {
6276
+ const revision = Number(entry?.revision || 0);
6277
+ return revision > mutationGoalRevision && revision <= currentGoalRevision;
6278
+ });
6279
+ return (
6280
+ interveningGoalEntries.length > 0 &&
6281
+ interveningGoalEntries.every(
6282
+ (entry) =>
6283
+ String(entry?.relation || "") === "same-task" ||
6284
+ String(entry?.kind || "") === "same-task-continuation"
6285
+ )
6286
+ );
6287
+ };
6288
+ let matchingHistoryIndex = -1;
6289
+ if (targetPath && searchHash && replaceHash) {
6290
+ for (let index = history.length - 1; index >= 0; index -= 1) {
6291
+ const mutation = history[index];
6292
+ if (
6293
+ mutation?.toolName === "apply_patch" &&
6294
+ mutationBelongsToCurrentTask(mutation) &&
6295
+ safeRecoveryEvidencePath(mutation?.patch?.path) === targetPath &&
6296
+ String(mutation?.patch?.searchHash || "") === searchHash &&
6297
+ String(mutation?.patch?.replaceHash || "") === replaceHash
6298
+ ) {
6299
+ matchingHistoryIndex = index;
6300
+ break;
6301
+ }
6302
+ }
6303
+ }
6304
+ const persistentlyApplied =
6305
+ matchingHistoryIndex >= 0 &&
6306
+ !history.slice(matchingHistoryIndex + 1).some(
6307
+ (mutation) =>
6308
+ mutationBelongsToCurrentTask(mutation) &&
6309
+ Number(mutation?.revision || 0) >
6310
+ Number(history[matchingHistoryIndex]?.revision || 0)
6311
+ );
6312
+ const alreadyApplied = recentlyApplied || persistentlyApplied;
6212
6313
  if (!alreadyApplied) return null;
6213
6314
  return {
6214
6315
  reason:
6215
- "This exact patch already succeeded without an intervening successful mutation or user continuation.",
6316
+ "This exact patch already succeeded in the current task lineage without a later successful mutation that changed source state.",
6216
6317
  category: "repeated-successful-mutation",
6217
6318
  permissionAdvice: {
6218
6319
  category: "repeated-successful-mutation",