@lazyingart/agintiflow 0.20.238 → 0.20.239
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 +1 -1
- package/scripts/smoke-coding-tools.js +74 -0
- package/scripts/smoke-context-budget-recovery.js +5 -0
- package/scripts/smoke-dynamic-step-budget.js +21 -0
- package/scripts/smoke-progressive-tool-selection.js +26 -0
- package/src/agent-runner.js +20 -2
- package/src/permission-advice.js +47 -3
- package/src/scs-controller.js +1 -1
- package/src/tool-contract.js +7 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.239",
|
|
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",
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
runAgent,
|
|
15
15
|
sanitizeToolResult,
|
|
16
16
|
toolResultForModel,
|
|
17
|
+
shouldPauseForPermissionAdvice,
|
|
17
18
|
shouldShortCircuitToolBatch,
|
|
18
19
|
shellDiagnosticHint,
|
|
19
20
|
skippedAfterBlockedToolResult,
|
|
@@ -1311,6 +1312,79 @@ try {
|
|
|
1311
1312
|
),
|
|
1312
1313
|
"an explicitly requested deletion was incorrectly treated as optional housekeeping"
|
|
1313
1314
|
);
|
|
1315
|
+
assert(
|
|
1316
|
+
isUnrequestedCleanupCommand(
|
|
1317
|
+
"run_command",
|
|
1318
|
+
{ command: "rm -rf /tmp/run-a /tmp/run-b" },
|
|
1319
|
+
{
|
|
1320
|
+
goal:
|
|
1321
|
+
"Continue the deterministic comparison. Do not delete any project or temporary directory; use fresh paths instead.",
|
|
1322
|
+
},
|
|
1323
|
+
{}
|
|
1324
|
+
),
|
|
1325
|
+
"a negated deletion constraint was mistaken for destructive authorization"
|
|
1326
|
+
);
|
|
1327
|
+
const negatedCleanupAdvice = buildPermissionAdvice({
|
|
1328
|
+
toolName: "run_command",
|
|
1329
|
+
args: { command: "rm -rf /tmp/run-a /tmp/run-b" },
|
|
1330
|
+
guard: {
|
|
1331
|
+
category: "destructive",
|
|
1332
|
+
reason: "Destructive shell commands require Allow destructive actions.",
|
|
1333
|
+
},
|
|
1334
|
+
config: {
|
|
1335
|
+
...dockerWorkspacePolicy,
|
|
1336
|
+
goal: "Do not delete any project or temporary directory; keep working on the build.",
|
|
1337
|
+
},
|
|
1338
|
+
state: { sessionId: "coding-negated-cleanup-smoke" },
|
|
1339
|
+
});
|
|
1340
|
+
assert(
|
|
1341
|
+
negatedCleanupAdvice.autoRecover === true,
|
|
1342
|
+
"blocked cleanup under a do-not-delete goal should recover without pausing"
|
|
1343
|
+
);
|
|
1344
|
+
const dynamicEvidenceAdvice = buildPermissionAdvice({
|
|
1345
|
+
toolName: "run_command",
|
|
1346
|
+
args: {
|
|
1347
|
+
command:
|
|
1348
|
+
'STAMP=$(date -u +%Y%m%dT%H%M%SZ); bash build.sh 2>&1 | tee ".aginti/build-${STAMP}.log"',
|
|
1349
|
+
},
|
|
1350
|
+
guard: {
|
|
1351
|
+
category: "destructive",
|
|
1352
|
+
reason:
|
|
1353
|
+
'Command contains a write-capable or destructive token: tee ".aginti/build-${STAMP}.log"',
|
|
1354
|
+
},
|
|
1355
|
+
config: {
|
|
1356
|
+
...dockerWorkspacePolicy,
|
|
1357
|
+
goal: "Build twice and retain deterministic evidence without deleting anything.",
|
|
1358
|
+
},
|
|
1359
|
+
state: { sessionId: "coding-dynamic-evidence-smoke" },
|
|
1360
|
+
});
|
|
1361
|
+
assert(
|
|
1362
|
+
dynamicEvidenceAdvice.autoRecover === true &&
|
|
1363
|
+
/literal workspace-relative evidence paths/i.test(dynamicEvidenceAdvice.instruction),
|
|
1364
|
+
"dynamic evidence filename false positive should recover into literal workspace paths"
|
|
1365
|
+
);
|
|
1366
|
+
assert(
|
|
1367
|
+
!shouldPauseForPermissionAdvice({ blocked: true, permissionAdvice: dynamicEvidenceAdvice }),
|
|
1368
|
+
"dynamic evidence filename recovery still produced a permission pause"
|
|
1369
|
+
);
|
|
1370
|
+
const destructiveDynamicEvidenceAdvice = buildPermissionAdvice({
|
|
1371
|
+
toolName: "run_command",
|
|
1372
|
+
args: {
|
|
1373
|
+
command:
|
|
1374
|
+
'rm -rf output; STAMP=$(date -u +%Y%m%dT%H%M%SZ); bash build.sh 2>&1 | tee ".aginti/build-${STAMP}.log"',
|
|
1375
|
+
},
|
|
1376
|
+
guard: {
|
|
1377
|
+
category: "destructive",
|
|
1378
|
+
reason: "Destructive shell commands require Allow destructive actions.",
|
|
1379
|
+
},
|
|
1380
|
+
config: { ...dockerWorkspacePolicy, goal: "Build and verify the document." },
|
|
1381
|
+
state: { sessionId: "coding-destructive-dynamic-evidence-smoke" },
|
|
1382
|
+
});
|
|
1383
|
+
assert(
|
|
1384
|
+
destructiveDynamicEvidenceAdvice.autoRecover === true &&
|
|
1385
|
+
/Unrequested cleanup was blocked safely/i.test(destructiveDynamicEvidenceAdvice.summary),
|
|
1386
|
+
"a real cleanup token should not be mislabeled as only dynamic evidence formatting"
|
|
1387
|
+
);
|
|
1314
1388
|
const documentPageBatchGuard = checkToolUse({
|
|
1315
1389
|
toolName: "read_image",
|
|
1316
1390
|
args: { imagePaths: ["build/verification/page-1.png", "build/verification/page-2.png"] },
|
|
@@ -144,6 +144,8 @@ const runtimeMessages = buildContextBudgetCompactionMessages(
|
|
|
144
144
|
toolName: "read_file",
|
|
145
145
|
path: "/evidence/Musia/SKILL.md",
|
|
146
146
|
bytes: 2048,
|
|
147
|
+
sha256: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
|
148
|
+
contentTruncated: false,
|
|
147
149
|
content: [
|
|
148
150
|
"---",
|
|
149
151
|
"name: musia-music-production",
|
|
@@ -187,9 +189,12 @@ assert.ok(runtimeText.includes("Retained source evidence"));
|
|
|
187
189
|
assert.ok(runtimeText.includes("/evidence/Musia/SKILL.md"));
|
|
188
190
|
assert.ok(runtimeText.includes("Create and review songs through the established Musia production workflow"));
|
|
189
191
|
assert.ok(runtimeText.includes("# Musia Music Production"));
|
|
192
|
+
assert.ok(runtimeText.includes("sha256=1234567890abcdef"));
|
|
193
|
+
assert.ok(runtimeText.includes("content=complete"));
|
|
190
194
|
assert.ok(runtimeText.includes("node bin/musia.js doctor --json"));
|
|
191
195
|
assert.ok(runtimeText.includes("scripts/xyq_cdp_browser.py"));
|
|
192
196
|
assert.ok(!runtimeText.includes("OLD-COMPACTION-MUST-NOT-RECUR"));
|
|
193
197
|
assert.match(runtimeText, /Do not reread a listed source solely because compaction occurred/);
|
|
198
|
+
assert.match(runtimeText, /never restart a full-file read loop after compaction/);
|
|
194
199
|
|
|
195
200
|
console.log("context budget recovery smoke passed");
|
|
@@ -2281,6 +2281,27 @@ try {
|
|
|
2281
2281
|
shellMutationState.meta.projectVerification?.mutationRevision === 1,
|
|
2282
2282
|
"git metadata incorrectly invalidated current project-content verification"
|
|
2283
2283
|
);
|
|
2284
|
+
const gitMetadataWithObservationsResult = {
|
|
2285
|
+
toolName: "run_command",
|
|
2286
|
+
ok: true,
|
|
2287
|
+
exitCode: 0,
|
|
2288
|
+
args: {
|
|
2289
|
+
command:
|
|
2290
|
+
"git add report.md && git commit -m 'record verified report' && echo '=== status ===' && git status --porcelain && git log -1 --oneline",
|
|
2291
|
+
},
|
|
2292
|
+
stdout: "=== status ===\nabc123 record verified report",
|
|
2293
|
+
stderr: "",
|
|
2294
|
+
};
|
|
2295
|
+
recordProjectVerificationOutcome(shellMutationState, gitMetadataWithObservationsResult, {
|
|
2296
|
+
commandCwd: workspace,
|
|
2297
|
+
taskProfile: "writing",
|
|
2298
|
+
allowShellTool: true,
|
|
2299
|
+
sandboxMode: "host",
|
|
2300
|
+
});
|
|
2301
|
+
assert(
|
|
2302
|
+
shellMutationState.meta.projectVerification?.mutationRevision === 1,
|
|
2303
|
+
"metadata-only Git chain with observational output invalidated current verification"
|
|
2304
|
+
);
|
|
2284
2305
|
const gitCheckoutResult = {
|
|
2285
2306
|
toolName: "run_command",
|
|
2286
2307
|
ok: true,
|
|
@@ -2471,6 +2471,25 @@ assert(recoveredMixedBatch.ok, "valid mixed batch could not recover through boun
|
|
|
2471
2471
|
assert(recoveredMixedBatch.recoveredSequentially, "mixed batch recovery was not recorded");
|
|
2472
2472
|
assert(recoveredMixedBatch.acceptedToolCalls.length === 1, "mixed batch recovery dispatched more than one call");
|
|
2473
2473
|
assert(recoveredMixedBatch.deferredToolCalls.length === 1, "mixed batch recovery did not defer the extra call");
|
|
2474
|
+
const fiveCallMixedBatch = [
|
|
2475
|
+
...safeReadCalls,
|
|
2476
|
+
contractCall("safe-read-four", "read_file", { path: "fourth.txt" }),
|
|
2477
|
+
contractCall("deferred-write-five", "write_file", { path: "later.txt", content: "later" }),
|
|
2478
|
+
];
|
|
2479
|
+
const recoveredFiveCallMixedBatch = resolveDispatchableToolCallBatch(
|
|
2480
|
+
fiveCallMixedBatch,
|
|
2481
|
+
createToolContract([...safeReadDescriptors, strictWriteDescriptor])
|
|
2482
|
+
);
|
|
2483
|
+
assert(recoveredFiveCallMixedBatch.ok, "five-call mixed batch was rejected instead of bounded deferral");
|
|
2484
|
+
assert(
|
|
2485
|
+
recoveredFiveCallMixedBatch.acceptedToolCalls.length === 1 &&
|
|
2486
|
+
recoveredFiveCallMixedBatch.deferredToolCalls.length === 4,
|
|
2487
|
+
"five-call mixed batch did not dispatch exactly one call and preserve the suffix"
|
|
2488
|
+
);
|
|
2489
|
+
assert(
|
|
2490
|
+
recoveredFiveCallMixedBatch.deferredToolCalls.at(-1)?.function?.name === "write_file",
|
|
2491
|
+
"bounded mixed recovery lost or executed the deferred write"
|
|
2492
|
+
);
|
|
2474
2493
|
const oversizedReadCalls = Array.from({ length: 5 }, (_, index) =>
|
|
2475
2494
|
contractCall(`read-${index}`, "read_file", { path: `file-${index}.txt` })
|
|
2476
2495
|
);
|
|
@@ -2492,6 +2511,13 @@ assert(
|
|
|
2492
2511
|
!resolveDispatchableToolCallBatch(excessiveReadBatch, safeReadContract).ok,
|
|
2493
2512
|
"unbounded safe read batch escaped the reported-call cap"
|
|
2494
2513
|
);
|
|
2514
|
+
assert(
|
|
2515
|
+
!resolveDispatchableToolCallBatch(
|
|
2516
|
+
[...excessiveReadBatch.slice(0, 12), contractCall("excessive-write", "write_file", { path: "later.txt", content: "later" })],
|
|
2517
|
+
createToolContract([...safeReadDescriptors, strictWriteDescriptor])
|
|
2518
|
+
).ok,
|
|
2519
|
+
"unbounded mixed batch escaped the reported-call cap"
|
|
2520
|
+
);
|
|
2495
2521
|
|
|
2496
2522
|
for (const [label, call, expectedCode] of [
|
|
2497
2523
|
[
|
package/src/agent-runner.js
CHANGED
|
@@ -826,6 +826,12 @@ function summarizeRetainedSourceEvidence(messages = [], limit = 28) {
|
|
|
826
826
|
const parts = [`tool=${toolName}`];
|
|
827
827
|
if (sourcePath) parts.push(`path=${sourcePath}`);
|
|
828
828
|
if (Number.isFinite(Number(payload.bytes))) parts.push(`bytes=${Number(payload.bytes)}`);
|
|
829
|
+
if (payload.sha256) parts.push(`sha256=${String(payload.sha256).slice(0, 16)}`);
|
|
830
|
+
if (payload.contentTruncated === true || payload.contentTruncatedByLines === true) {
|
|
831
|
+
parts.push("content=truncated");
|
|
832
|
+
} else if (toolName === "read_file" && typeof payload.content === "string") {
|
|
833
|
+
parts.push("content=complete");
|
|
834
|
+
}
|
|
829
835
|
if (payload.summary) parts.push(`summary=${compactSingleLine(payload.summary, 220)}`);
|
|
830
836
|
if (toolName === "read_file") {
|
|
831
837
|
const semantic = summarizeReadSemanticEvidence(payload);
|
|
@@ -1249,6 +1255,7 @@ function buildCompactedRuntimeMessages(state, config, snapshot, step, options =
|
|
|
1249
1255
|
"",
|
|
1250
1256
|
"Retained source evidence summaries (already inspected; reread an exact source only when its needed content is absent here):",
|
|
1251
1257
|
"Do not reread a listed source solely because compaction occurred.",
|
|
1258
|
+
"A content=complete entry is authoritative for that recorded sha256. Use search_files or one bounded range read only when an exact edit anchor is absent; never restart a full-file read loop after compaction.",
|
|
1252
1259
|
...(retainedSourceEvidence.length
|
|
1253
1260
|
? retainedSourceEvidence.map((item) => `- ${item}`)
|
|
1254
1261
|
: ["- No structured source evidence was available before compaction."]),
|
|
@@ -5986,6 +5993,17 @@ const WORKTREE_CHANGING_GIT_ACTIONS = new Set([
|
|
|
5986
5993
|
|
|
5987
5994
|
function commandCanMutateProjectContent(command = "", commandPolicy = {}) {
|
|
5988
5995
|
if (commandPolicy.writesWorkspace !== true && commandPolicy.mayMutateProject !== true) return false;
|
|
5996
|
+
const sequence = parseTopLevelShellSequence(String(command || ""));
|
|
5997
|
+
if (
|
|
5998
|
+
sequence.commands.length > 1 &&
|
|
5999
|
+
!sequence.openQuote &&
|
|
6000
|
+
!sequence.trailingEscape &&
|
|
6001
|
+
!sequence.trailingSeparator
|
|
6002
|
+
) {
|
|
6003
|
+
return sequence.commands.some((segment) =>
|
|
6004
|
+
commandCanMutateProjectContent(segment, classifyCommand(segment))
|
|
6005
|
+
);
|
|
6006
|
+
}
|
|
5989
6007
|
const category = String(commandPolicy.category || "");
|
|
5990
6008
|
if (!["git-workflow", "git-remote"].includes(category)) return true;
|
|
5991
6009
|
if (/\bgit\s+clone\b/i.test(String(command || ""))) return true;
|
|
@@ -11787,9 +11805,9 @@ export async function runAgent(config) {
|
|
|
11787
11805
|
content: [
|
|
11788
11806
|
"Runtime batching note: the valid tool batch exceeded the bounded per-turn dispatch limit.",
|
|
11789
11807
|
`The first ${toolCalls.length} call(s) ran sequentially; do not repeat them.`,
|
|
11790
|
-
"These remaining
|
|
11808
|
+
"These remaining calls were deferred and did not run:",
|
|
11791
11809
|
...deferredSummary,
|
|
11792
|
-
"
|
|
11810
|
+
"Review the deferred list, request only the calls still needed in a bounded batch, and do not assume any deferred write or command ran.",
|
|
11793
11811
|
].join("\n"),
|
|
11794
11812
|
});
|
|
11795
11813
|
}
|
package/src/permission-advice.js
CHANGED
|
@@ -67,11 +67,40 @@ export function isOptionalGeneratedPreviewCleanup(toolName = "", args = {}) {
|
|
|
67
67
|
);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
export function isRecoverableDynamicEvidenceWrite(toolName = "", args = {}, guard = {}) {
|
|
71
|
+
if (toolName !== "run_command" || guard?.category !== "destructive") return false;
|
|
72
|
+
const command = String(args.command || args.text || "");
|
|
73
|
+
if (!/\btee\s+/i.test(command)) return false;
|
|
74
|
+
if (
|
|
75
|
+
/(?:^|[;&|\n]\s*)(?:command\s+)?(?:rm|rmdir|mv|chmod|chown)\b/i.test(command) ||
|
|
76
|
+
/\bgit\s+(?:checkout|switch|reset|clean)\b/i.test(command) ||
|
|
77
|
+
/(?:^|\s)-delete(?:\s|$)/i.test(command)
|
|
78
|
+
) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const dynamicWorkspaceEvidenceTarget = new RegExp(
|
|
82
|
+
String.raw`\btee\s+(?:--?append\s+|-a\s+)?["']?(?:\.aginti|artifacts|build/verification|output/verification)/[^\n;|]*\$\{?[A-Z_][A-Z0-9_]*\}?`,
|
|
83
|
+
"i"
|
|
84
|
+
);
|
|
85
|
+
return dynamicWorkspaceEvidenceTarget.test(command);
|
|
86
|
+
}
|
|
87
|
+
|
|
70
88
|
function goalRequestsDeletion(config = {}, state = {}) {
|
|
71
89
|
const goal = String(config.goal || state.goal || state.meta?.goalContract?.current || "");
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
const deletionIntent = /\b(?:delete|remove|clean\s+up|cleanup|purge|erase|discard|drop)\b|删除|刪除|移除|清理|清除|删掉|刪掉|削除|消去/i;
|
|
91
|
+
if (!deletionIntent.test(goal)) return false;
|
|
92
|
+
|
|
93
|
+
// A safety constraint such as "do not delete" is the opposite of
|
|
94
|
+
// authorization. Strip bounded negated phrases before looking for a genuine
|
|
95
|
+
// deletion request elsewhere in the goal.
|
|
96
|
+
const withoutNegatedDeletion = goal
|
|
97
|
+
.replace(
|
|
98
|
+
/\b(?:do\s+not|don't|dont|never|must\s+not|should\s+not|shouldn't|without)\s+(?:retry(?:ing)?\s+or\s+)?(?:delete|remove|clean\s+up|cleanup|purge|erase|discard|drop)(?:\s+any)?\b/gi,
|
|
99
|
+
" "
|
|
100
|
+
)
|
|
101
|
+
.replace(/(?:不要|不可|禁止|无需|無需|不需要)(?:再)?(?:删除|刪除|移除|清理|清除|删掉|刪掉)/g, " ")
|
|
102
|
+
.replace(/(?:削除|消去)(?:しない|するな|不要)/g, " ");
|
|
103
|
+
return deletionIntent.test(withoutNegatedDeletion);
|
|
75
104
|
}
|
|
76
105
|
|
|
77
106
|
export function isUnrequestedCleanupCommand(toolName = "", args = {}, config = {}, state = {}) {
|
|
@@ -291,6 +320,21 @@ function adviceForCategory(category = "", { toolName = "", args = {}, config = {
|
|
|
291
320
|
}
|
|
292
321
|
|
|
293
322
|
if (category === "destructive") {
|
|
323
|
+
if (isRecoverableDynamicEvidenceWrite(toolName, args, { category, reason })) {
|
|
324
|
+
return {
|
|
325
|
+
...base,
|
|
326
|
+
autoRecover: true,
|
|
327
|
+
summary:
|
|
328
|
+
"A generated-evidence command used a shell-expanded output filename that the workspace guard could not prove safe. The command stayed blocked, but no destructive permission is needed.",
|
|
329
|
+
instruction:
|
|
330
|
+
"Do not retry the same command and do not request destructive approval. Reissue the check with fresh literal workspace-relative evidence paths under `.aginti/verification/`; avoid variables, globs, and `/tmp` in tee/redirection targets, then continue the substantive validation.",
|
|
331
|
+
options: [
|
|
332
|
+
"Use a literal timestamp or nonce already written into the command text.",
|
|
333
|
+
"Keep every log, hash file, and render under `.aginti/verification/`.",
|
|
334
|
+
"Split the build and evidence checks into smaller commands if that makes each output path explicit.",
|
|
335
|
+
],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
294
338
|
if (
|
|
295
339
|
isOptionalGeneratedPreviewCleanup(toolName, args) ||
|
|
296
340
|
isUnrequestedCleanupCommand(toolName, args, config, state)
|
package/src/scs-controller.js
CHANGED
|
@@ -354,7 +354,7 @@ function fallbackHardContractPlan(goal = "", contract = {}, studentReason = "",
|
|
|
354
354
|
forbiddenTextTerms.length ? `5. Ensure the output does not contain these forbidden term(s): ${forbiddenTextTerms.join(", ")}.` : "",
|
|
355
355
|
requiredToolCalls.length ? `6. Call these explicitly required tool(s) before finish: ${requiredToolCalls.join(", ")}.` : "",
|
|
356
356
|
selectedSkillPaths.length
|
|
357
|
-
? `7. Read the selected Markdown guidance
|
|
357
|
+
? `7. Read the selected Markdown guidance once before choosing an interface: ${selectedSkillPaths.join(", ")}. If retained source evidence records the path as already inspected, use that evidence and do not reread it solely after compaction. Skill IDs are not commands.`
|
|
358
358
|
: "",
|
|
359
359
|
readOnlyRoots.length
|
|
360
360
|
? `8. Inspect only the exact active read-only roots or their children with structured read tools: ${readOnlyRoots.join(", ")}. Do not pass --read-root to an in-task command.`
|
package/src/tool-contract.js
CHANGED
|
@@ -29,8 +29,7 @@ const SAFE_SEQUENTIAL_READ_TOOLS = new Set([
|
|
|
29
29
|
const MAX_VALIDATION_ERRORS = 8;
|
|
30
30
|
const MAX_VALIDATION_NODES = 50_000;
|
|
31
31
|
const MAX_SAFE_SEQUENTIAL_READ_CALLS = 4;
|
|
32
|
-
const
|
|
33
|
-
const MAX_REPORTED_SAFE_READ_CALLS = 12;
|
|
32
|
+
const MAX_REPORTED_SEQUENTIAL_CALLS = 12;
|
|
34
33
|
|
|
35
34
|
function cloneValue(value) {
|
|
36
35
|
return structuredClone(value);
|
|
@@ -436,9 +435,12 @@ export function resolveDispatchableToolCallBatch(toolCalls, contract) {
|
|
|
436
435
|
const onlyExceededBatchLimit =
|
|
437
436
|
errors.length > 0 && errors.every((error) => error?.code === "TOO_MANY_TOOL_CALLS");
|
|
438
437
|
const safeReadBatch = isSafeSequentialReadBatch(calls);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
438
|
+
// The model may report a bounded batch even though the runtime deliberately
|
|
439
|
+
// dispatches only one mixed/mutating call at a time. Validate every reported
|
|
440
|
+
// call against the authenticated contract, then defer the untouched suffix.
|
|
441
|
+
// This keeps writes sequential without turning a harmless fifth call into a
|
|
442
|
+
// whole-turn failure.
|
|
443
|
+
const recoverableCallLimit = MAX_REPORTED_SEQUENTIAL_CALLS;
|
|
442
444
|
if (
|
|
443
445
|
!onlyExceededBatchLimit ||
|
|
444
446
|
calls.length <= 1 ||
|