@lazyingart/agintiflow 0.20.205 → 0.20.207

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.
@@ -38,11 +38,6 @@ Create, start, and finish a concrete run with `test`, `start`, `event`, and
38
38
  idempotent or append-only SQLite operation suitable for a persistent tmux
39
39
  campaign.
40
40
 
41
- Test registration validates that any named capability and scenario belong to
42
- the same campaign. Finishing a test updates the test, capability, and scenario
43
- status in one SQLite transaction, so a typo cannot silently leave the campaign
44
- matrix stale or split across contradictory states.
45
-
46
41
  Inspect current coverage:
47
42
 
48
43
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.205",
3
+ "version": "0.20.207",
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",
@@ -152,6 +152,7 @@
152
152
  "smoke:runtime-core": "node scripts/smoke-runtime-core.js",
153
153
  "smoke:localllm-auto-max": "node scripts/smoke-localllm-auto-max.js",
154
154
  "smoke:localllm-code-routing": "node scripts/smoke-localllm-code-routing.js",
155
+ "smoke:local-failure-recovery": "node scripts/smoke-local-failure-recovery.js",
155
156
  "smoke:local-resource-policy": "node scripts/smoke-local-resource-policy.js",
156
157
  "smoke:context-budget-recovery": "node scripts/smoke-context-budget-recovery.js",
157
158
  "smoke:localllm-model-tiers": "node scripts/smoke-localllm-model-tiers.js",
@@ -188,7 +189,7 @@
188
189
  "storage:migrate": "node bin/aginti-cli.js storage migrate",
189
190
  "publish:env": "node scripts/npm-publish-from-env.js publish --access public",
190
191
  "publish:env:whoami": "node scripts/npm-publish-from-env.js whoami",
191
- "test": "npm run check && npm run smoke:integration-production-mount && npm run smoke:localllm-provider && npm run smoke:localllm-model-tiers && npm run smoke:localllm-code-routing && npm run smoke:localllm-auto-max && npm run smoke:local-resource-policy && npm run smoke:context-budget-recovery && npm run smoke:session-runtime && npm run smoke:runtime-core && npm run smoke:progressive-tools && npm run smoke:truthful-completion && npm run smoke:writing-specialist-routing && npm run eval:local-first-agent && npm run eval:provider-attribution && npm run smoke:runtime-compat && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:math-rendering && npm run smoke:web-ui && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:docker-command && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:execution-policy && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:deep-research && npm run smoke:public-research && npm run smoke:safe-chat && npm run smoke:auth && npm run smoke:agentlink && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:mcp && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:supervision-ledger && npm run smoke:tmux-tools && npm run smoke:long-jobs && npm run smoke:run-stdin && npm run smoke:cli-chat && npm run smoke:inbox",
192
+ "test": "npm run check && npm run smoke:integration-production-mount && npm run smoke:localllm-provider && npm run smoke:localllm-model-tiers && npm run smoke:localllm-code-routing && npm run smoke:local-failure-recovery && npm run smoke:localllm-auto-max && npm run smoke:local-resource-policy && npm run smoke:context-budget-recovery && npm run smoke:session-runtime && npm run smoke:runtime-core && npm run smoke:progressive-tools && npm run smoke:truthful-completion && npm run smoke:writing-specialist-routing && npm run eval:local-first-agent && npm run eval:provider-attribution && npm run smoke:runtime-compat && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:math-rendering && npm run smoke:web-ui && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:docker-command && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:execution-policy && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:deep-research && npm run smoke:public-research && npm run smoke:safe-chat && npm run smoke:auth && npm run smoke:agentlink && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:mcp && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:supervision-ledger && npm run smoke:tmux-tools && npm run smoke:long-jobs && npm run smoke:run-stdin && npm run smoke:cli-chat && npm run smoke:inbox",
192
193
  "pack:dry-run": "npm pack --dry-run",
193
194
  "smoke:capabilities": "node scripts/smoke-capabilities.js"
194
195
  },
@@ -204,6 +204,136 @@ sameNames(
204
204
  assert(!names(artifactValidationTools).includes("search_files"), "artifact validation phase reopened broad search");
205
205
  assert(!names(artifactValidationTools).includes("open_url"), "artifact validation phase reopened browser discovery");
206
206
 
207
+ const localFailureRecoveryTools = selectProgressiveTools(allTools, {
208
+ config: { provider: "localllm", localFailureRecoveryActive: true },
209
+ goal: "Recover from repeated failed edits without restarting the task.",
210
+ profile: "code",
211
+ messages: [
212
+ { role: "assistant", tool_calls: [{ id: "failed-edit", function: { name: "apply_patch", arguments: "{}" } }] },
213
+ { role: "tool", tool_call_id: "failed-edit", content: '{"ok":false}' },
214
+ ],
215
+ });
216
+ sameNames(
217
+ localFailureRecoveryTools,
218
+ ["read_file", "apply_patch", "write_file", "run_command", "search_files", "inspect_project", "finish"],
219
+ "local failure recovery did not restore the bounded repair surface"
220
+ );
221
+
222
+ const dataDiscoveryStarterTools = selectProgressiveTools(allTools, {
223
+ config: { provider: "localllm" },
224
+ goal: "Clean these experiment exports and leave a reproducible analysis.",
225
+ profile: "data",
226
+ });
227
+ sameNames(
228
+ dataDiscoveryStarterTools,
229
+ ["inspect_project", "finish"],
230
+ "local data task exposed mutation tools before project inspection"
231
+ );
232
+
233
+ const dataInspectionMessages = [
234
+ { role: "user", content: "Goal: Clean these experiment exports and leave a reproducible analysis." },
235
+ {
236
+ role: "assistant",
237
+ tool_calls: [{ id: "data-inspect", function: { name: "inspect_project", arguments: '{"path":"."}' } }],
238
+ },
239
+ {
240
+ role: "tool",
241
+ tool_call_id: "data-inspect",
242
+ content: JSON.stringify({
243
+ ok: true,
244
+ recommendedReads: ["README.md", "tests/test_analysis.py"],
245
+ manifestFiles: [{ path: "README.md" }],
246
+ testFiles: [{ path: "tests/test_analysis.py" }],
247
+ topLevel: [
248
+ { path: "analysis.py", type: "file" },
249
+ { path: "TASK.md", type: "file" },
250
+ ],
251
+ }),
252
+ },
253
+ ];
254
+ const dataContextTools = selectProgressiveTools(allTools, {
255
+ config: { provider: "localllm" },
256
+ goal: "Clean these experiment exports and leave a reproducible analysis.",
257
+ profile: "data",
258
+ messages: dataInspectionMessages,
259
+ });
260
+ sameNames(
261
+ dataContextTools,
262
+ ["read_file", "finish"],
263
+ "local data task exposed mutation tools before reading project contracts"
264
+ );
265
+ assertStrict.deepEqual(
266
+ enumFor(dataContextTools, "read_file", "path"),
267
+ ["README.md"],
268
+ "local data instruction phase did not constrain read_file to discovered instruction paths"
269
+ );
270
+
271
+ const dataImplementationMessages = [
272
+ ...dataInspectionMessages,
273
+ {
274
+ role: "assistant",
275
+ tool_calls: [{ id: "data-readme", function: { name: "read_file", arguments: '{"path":"README.md"}' } }],
276
+ },
277
+ { role: "tool", tool_call_id: "data-readme", content: '{"ok":true,"path":"README.md"}' },
278
+ ];
279
+ const dataImplementationTools = selectProgressiveTools(allTools, {
280
+ config: { provider: "localllm" },
281
+ goal: "Clean these experiment exports and leave a reproducible analysis.",
282
+ profile: "data",
283
+ messages: dataImplementationMessages,
284
+ });
285
+ sameNames(
286
+ dataImplementationTools,
287
+ ["read_file", "finish"],
288
+ "local data task exposed mutation tools before reading an analyzer/config/test surface"
289
+ );
290
+ assertStrict.deepEqual(
291
+ enumFor(dataImplementationTools, "read_file", "path"),
292
+ ["tests/test_analysis.py", "analysis.py"],
293
+ "local data context phase did not constrain read_file to discovered analysis paths"
294
+ );
295
+
296
+ const dataReadyMessages = [
297
+ ...dataImplementationMessages,
298
+ {
299
+ role: "assistant",
300
+ tool_calls: [
301
+ { id: "data-test", function: { name: "read_file", arguments: '{"path":"tests/test_analysis.py"}' } },
302
+ ],
303
+ },
304
+ { role: "tool", tool_call_id: "data-test", content: '{"ok":true,"path":"tests/test_analysis.py"}' },
305
+ ];
306
+ const dataReadyTools = selectProgressiveTools(allTools, {
307
+ config: { provider: "localllm" },
308
+ goal: "Clean these experiment exports and leave a reproducible analysis.",
309
+ profile: "data",
310
+ messages: dataReadyMessages,
311
+ });
312
+ sameNames(dataReadyTools, LOCAL_COMPACT_CODE_TOOL_NAMES, "local data task did not unlock after bounded discovery");
313
+
314
+ const dataRecoveryBeforeDiscoveryTools = selectProgressiveTools(allTools, {
315
+ config: { provider: "localllm", localFailureRecoveryActive: true },
316
+ goal: "Recover this data task after a failed mutation.",
317
+ profile: "data",
318
+ });
319
+ sameNames(
320
+ dataRecoveryBeforeDiscoveryTools,
321
+ ["inspect_project", "finish"],
322
+ "local recovery bypassed the incomplete data discovery gate"
323
+ );
324
+
325
+ const dataRecoveryAfterDiscoveryTools = selectProgressiveTools(allTools, {
326
+ config: { provider: "localllm", localFailureRecoveryActive: true },
327
+ goal: "Recover this data task after a failed mutation.",
328
+ profile: "data",
329
+ messages: dataReadyMessages,
330
+ });
331
+ sameNames(
332
+ dataRecoveryAfterDiscoveryTools,
333
+ ["read_file", "apply_patch", "write_file", "run_command", "search_files", "inspect_project", "finish"],
334
+ "local recovery remained trapped after data discovery completed"
335
+ );
336
+
207
337
  const artifactRepairTools = selectProgressiveTools(allTools, {
208
338
  config: { provider: "localllm", artifactValidationPhase: true, artifactValidationNeedsRepair: true },
209
339
  goal: "Repair the exact report from deterministic preflight evidence.",
@@ -90,6 +90,13 @@ assert(selectedIds("debug a C# dotnet web API").includes("dotnet-csharp"), ".NET
90
90
  assert(selectedIds("fix a PHP Laravel composer project").includes("php"), "PHP prompt did not select php");
91
91
  assert(selectedIds("repair a Ruby Rails app with RSpec").includes("ruby"), "Ruby prompt did not select ruby");
92
92
  assert(selectedIds("clean a CSV dataset and make plots").includes("data-analysis"), "data prompt did not select data-analysis");
93
+ const focusedDataSkills = selectedIds(
94
+ "these experiment exports are a mess; clean them, make a useful plot and report",
95
+ "data"
96
+ );
97
+ assert(focusedDataSkills.includes("data-analysis"), "explicit data profile omitted its core analysis skill");
98
+ assert(!focusedDataSkills.includes("novel-writing"), "explicit data profile selected unrelated novel guidance");
99
+ assert(!focusedDataSkills.includes("media-transcription-report"), "explicit data profile selected unrelated media guidance");
93
100
  assert(selectedIds("convert scanned PDF EPUB and image sources to markdown with OCR").includes("source-ingestion"), "source ingestion prompt did not select source-ingestion");
94
101
  assert(selectedIds("use json schema to fetch valid structured json for each chunk in parallel").includes("structured-json"), "structured JSON prompt did not select structured-json");
95
102
  assert(selectedIds("write README API docs and a tutorial").includes("docs-knowledge"), "docs prompt did not select docs-knowledge");
@@ -19,17 +19,6 @@ function run(command, args = []) {
19
19
  }));
20
20
  }
21
21
 
22
- function runFails(command, args, expected) {
23
- try {
24
- run(command, args);
25
- } catch (error) {
26
- const output = `${error?.stdout || ""}\n${error?.stderr || ""}\n${error?.message || ""}`;
27
- assert(expected.test(output), `unexpected ${command} failure: ${output}`);
28
- return;
29
- }
30
- throw new Error(`${command} unexpectedly succeeded`);
31
- }
32
-
33
22
  function assert(condition, message) {
34
23
  if (!condition) throw new Error(message);
35
24
  }
@@ -44,14 +33,6 @@ run("scenario", [
44
33
  "--prompt-quality", "normal", "--prompt", "Prepare the media chain without publishing.",
45
34
  "--expected-outputs", "[\"readiness.md\"]", "--validation", "Verify routine paths and no external write.",
46
35
  ]);
47
- runFails("test", [
48
- "--id", "missing-capability", "--capability", "not-registered", "--scenario", "media-dry-run",
49
- "--title", "Invalid capability reference", "--validation", "Must fail before insertion.",
50
- ], /Unknown capability/);
51
- runFails("test", [
52
- "--id", "missing-scenario", "--capability", "media-chain", "--scenario", "not-registered",
53
- "--title", "Invalid scenario reference", "--validation", "Must fail before insertion.",
54
- ], /Unknown scenario/);
55
36
  run("test", [
56
37
  "--id", "media-dry-run-001", "--capability", "media-chain", "--scenario", "media-dry-run",
57
38
  "--title", "Read-only established media routine probe", "--prompt-path", "TASK.md",
@@ -79,7 +60,7 @@ assert(status.ok, "ledger status did not succeed");
79
60
  assert(fs.statSync(db).size > 0, "ledger database is empty");
80
61
  assert(status.campaign?.id === "smoke", "campaign row was not preserved");
81
62
  assert(status.capability_counts.some((row) => row.status === "passed_after_fix" && row.count === 1), "capability status was not updated");
82
- assert(status.scenario_counts.some((row) => row.status === "passed_after_fix" && row.count === 1), "scenario result was not propagated");
63
+ assert(status.scenario_counts.some((row) => row.status === "backlog" && row.count === 1), "scenario was not recorded");
83
64
  assert(status.test_counts.some((row) => row.status === "passed_after_fix" && row.count === 1), "test result was not recorded");
84
65
  assert(status.recent_tests[0]?.session_id === "session-smoke", "session evidence was not retained");
85
66
 
@@ -32,24 +32,6 @@ function jsonValue(value, fallback = []) {
32
32
  return JSON.stringify(JSON.parse(String(value)));
33
33
  }
34
34
 
35
- function assertCampaignReference(db, table, id, campaignId, label) {
36
- if (!id) return;
37
- const row = db.prepare(`SELECT id FROM ${table} WHERE id=? AND campaign_id=?`).get(id, campaignId);
38
- if (!row) throw new Error(`Unknown ${label} for campaign ${campaignId}: ${id}`);
39
- }
40
-
41
- function runTransaction(db, operation) {
42
- db.exec("BEGIN IMMEDIATE");
43
- try {
44
- const result = operation();
45
- db.exec("COMMIT");
46
- return result;
47
- } catch (error) {
48
- db.exec("ROLLBACK");
49
- throw error;
50
- }
51
- }
52
-
53
35
  function openLedger(dbPath) {
54
36
  const resolved = path.resolve(dbPath);
55
37
  fs.mkdirSync(path.dirname(resolved), { recursive: true });
@@ -149,10 +131,6 @@ function main() {
149
131
  result = { scenario: id };
150
132
  } else if (command === "test") {
151
133
  const id = required(options, "id");
152
- const capabilityId = String(options.capability || "");
153
- const scenarioId = String(options.scenario || "");
154
- assertCampaignReference(db, "capabilities", capabilityId, campaignId, "capability");
155
- assertCampaignReference(db, "scenarios", scenarioId, campaignId, "scenario");
156
134
  db.prepare(`INSERT INTO test_items
157
135
  (id, campaign_id, capability_id, scenario_id, title, profile, prompt_quality,
158
136
  prompt_path, expected_outputs_json, validation_plan, status, updated_at)
@@ -161,7 +139,7 @@ function main() {
161
139
  title=excluded.title, profile=excluded.profile, prompt_quality=excluded.prompt_quality,
162
140
  prompt_path=excluded.prompt_path, expected_outputs_json=excluded.expected_outputs_json,
163
141
  validation_plan=excluded.validation_plan, updated_at=excluded.updated_at`).run(
164
- id, campaignId, capabilityId, scenarioId,
142
+ id, campaignId, String(options.capability || ""), String(options.scenario || ""),
165
143
  required(options, "title"), String(options.profile || "auto"),
166
144
  String(options.prompt_quality || "normal"), String(options.prompt_path || ""),
167
145
  jsonValue(options.expected_outputs), String(options.validation || ""),
@@ -188,28 +166,16 @@ function main() {
188
166
  } else if (command === "finish") {
189
167
  const id = required(options, "id");
190
168
  const status = required(options, "status");
191
- const summary = required(options, "summary");
192
- const evidence = jsonValue(options.evidence);
193
- const item = db.prepare(
194
- "SELECT capability_id, scenario_id FROM test_items WHERE id=? AND campaign_id=?"
195
- ).get(id, campaignId);
196
- if (!item) throw new Error(`Unknown test item: ${id}`);
197
- assertCampaignReference(db, "capabilities", item.capability_id, campaignId, "capability");
198
- assertCampaignReference(db, "scenarios", item.scenario_id, campaignId, "scenario");
199
- runTransaction(db, () => {
200
- db.prepare(`UPDATE test_items SET status=?, result_summary=?, evidence_json=?,
201
- finished_at=?, updated_at=? WHERE id=? AND campaign_id=?`).run(
202
- status, summary, evidence, timestamp, timestamp, id, campaignId
203
- );
204
- if (item.capability_id) {
205
- db.prepare(`UPDATE capabilities SET status=?, last_test_id=?, updated_at=?
206
- WHERE id=? AND campaign_id=?`).run(status, id, timestamp, item.capability_id, campaignId);
207
- }
208
- if (item.scenario_id) {
209
- db.prepare("UPDATE scenarios SET status=?, updated_at=? WHERE id=? AND campaign_id=?")
210
- .run(status, timestamp, item.scenario_id, campaignId);
211
- }
212
- });
169
+ const changed = db.prepare(`UPDATE test_items SET status=?, result_summary=?, evidence_json=?,
170
+ finished_at=?, updated_at=? WHERE id=? AND campaign_id=?`).run(
171
+ status, required(options, "summary"), jsonValue(options.evidence), timestamp, timestamp, id, campaignId
172
+ );
173
+ if (Number(changed.changes || 0) !== 1) throw new Error(`Unknown test item: ${id}`);
174
+ const item = db.prepare("SELECT capability_id FROM test_items WHERE id=?").get(id);
175
+ if (item?.capability_id) {
176
+ db.prepare("UPDATE capabilities SET status=?, last_test_id=?, updated_at=? WHERE id=?")
177
+ .run(status, id, timestamp, item.capability_id);
178
+ }
213
179
  result = { test: id, status };
214
180
  } else if (command === "fix") {
215
181
  const id = required(options, "id");
@@ -23,7 +23,8 @@ tools:
23
23
  ---
24
24
  # Data Analysis And ETL
25
25
 
26
- Preserve raw inputs. Inspect schemas, row counts, missing values, duplicate rows, units, time zones, and suspicious outliers before writing conclusions.
26
+ Preserve raw/source/input exports as immutable evidence unless the user explicitly authorizes an in-place source change. Start with `inspect_project`, then read project instructions plus the relevant existing analyzer, configuration, and tests before editing or running ad hoc calculations. Inspect schemas, row counts, missing values, duplicate rows, units, aliases, time zones, and suspicious outliers before writing conclusions.
27
27
 
28
- Prefer a reproducible script over one-off chat math. Save cleaned data, plots, and reports with descriptive filenames. Rerun the script after edits and verify the output actually changed as intended.
28
+ Prefer the project's existing reproducible analyzer over one-off chat math; repair or extend it when needed. Save cleaned data, plots, and reports at the exact declared output paths. Keep machine outputs separate from raw inputs and make the report explain methods, evidence, limitations, and the practical result.
29
29
 
30
+ After an edit or patch conflict, reread the exact target and error before choosing a different bounded repair. Never repeat an unchanged failing patch. Rerun the smallest relevant check, then the full analysis and project tests. Verify cleaned schemas and row counts, inspect plot metadata or pixels, remove only generated caches, and do not claim success until every requested artifact exists and can be regenerated.
@@ -61,6 +61,12 @@ import {
61
61
  resolveLocalCodeRoute,
62
62
  restoreLocalCodePolicy,
63
63
  } from "./local-code-routing.js";
64
+ import {
65
+ activateLocalFailureRecovery,
66
+ applyLocalFailureRecovery,
67
+ decideLocalFailureRecovery,
68
+ localFailureRecoveryInstruction,
69
+ } from "./local-failure-recovery.js";
64
70
  import { resolveRuntimeConfig } from "./config.js";
65
71
  import {
66
72
  captureSessionRuntime,
@@ -2526,9 +2532,10 @@ export function recordExactOutputProgress(state = {}, toolResult = {}, config =
2526
2532
  }
2527
2533
 
2528
2534
  export function nextStepRuntimeConfig(config = {}, state = {}) {
2535
+ const runtimeConfig = applyLocalFailureRecovery(config, state);
2529
2536
  if (state.meta?.artifactProgress?.complete) {
2530
2537
  return {
2531
- ...config,
2538
+ ...runtimeConfig,
2532
2539
  artifactValidationPhase: true,
2533
2540
  convergenceOutputPhase: false,
2534
2541
  artifactValidationNeedsRepair: state.meta.artifactProgress.needsRepair === true,
@@ -2542,10 +2549,10 @@ export function nextStepRuntimeConfig(config = {}, state = {}) {
2542
2549
  };
2543
2550
  }
2544
2551
  const staticTotal = Number(state.meta?.toolLoop?.staticTotal || 0);
2545
- if (staticTotal < STATIC_DISCOVERY_CONVERGENCE_LIMIT) return config;
2552
+ if (staticTotal < STATIC_DISCOVERY_CONVERGENCE_LIMIT) return runtimeConfig;
2546
2553
  const requiresPerSourceChecks = state.meta?.scs?.taskContract?.requiresPerSourceChecks === true;
2547
2554
  return {
2548
- ...config,
2555
+ ...runtimeConfig,
2549
2556
  convergenceOutputPhase: true,
2550
2557
  convergenceAllowRunCommand: requiresPerSourceChecks,
2551
2558
  };
@@ -4168,11 +4175,15 @@ async function recordToolContractViolation({ config, state, store, observers, va
4168
4175
  total: totalViolationCount,
4169
4176
  lastCode: validation.code || "TOOL_CALL_INVALID",
4170
4177
  };
4178
+ const localRecovery = decideLocalFailureRecovery(config, state);
4179
+ const deferStopToLocalRecovery =
4180
+ violationCount >= 2 && localRecovery.active === true && localRecovery.activated === true;
4171
4181
  const result = {
4172
4182
  ok: false,
4173
4183
  blocked: true,
4174
- recoverable: violationCount < 2,
4175
- stopRun: violationCount >= 2,
4184
+ recoverable: violationCount < 2 || deferStopToLocalRecovery,
4185
+ stopRun: violationCount >= 2 && !deferStopToLocalRecovery,
4186
+ localFailureRecoveryPending: deferStopToLocalRecovery,
4176
4187
  violationCount,
4177
4188
  reason: validation.reason || "The model returned an invalid tool call and it was not dispatched.",
4178
4189
  category: "tool-contract-violation",
@@ -5030,6 +5041,37 @@ export async function runAgent(config) {
5030
5041
  const step = state.stepsCompleted + 1;
5031
5042
  throwIfAborted(config);
5032
5043
  await injectQueuedUserMessages(store, state, observers);
5044
+ const localFailureRecovery = activateLocalFailureRecovery(config, state);
5045
+ if (localFailureRecovery.active) {
5046
+ config = applyLocalFailureRecovery(config, state);
5047
+ }
5048
+ if (localFailureRecovery.activated) {
5049
+ state.provider = config.provider;
5050
+ state.model = config.model;
5051
+ state.meta.runtimeConfig = captureSessionRuntime(config, {
5052
+ revision: state.meta.runtimeConfig?.revision || 1,
5053
+ });
5054
+ const recoveryInstruction = localFailureRecoveryInstruction(localFailureRecovery);
5055
+ state.messages.push({ role: "user", content: recoveryInstruction });
5056
+ const detail = {
5057
+ step,
5058
+ fromModel: localFailureRecovery.fromModel,
5059
+ model: localFailureRecovery.model,
5060
+ failureCount: localFailureRecovery.failureCount,
5061
+ repeatedSignatureCount: localFailureRecovery.repeatedSignatureCount,
5062
+ contractViolationCount: localFailureRecovery.contractViolationCount || 0,
5063
+ failedTools: localFailureRecovery.failedTools || [],
5064
+ reason: localFailureRecovery.reason,
5065
+ };
5066
+ await store.appendEvent("provider.local_failure_recovery", detail);
5067
+ observers.event("provider.local_failure_recovery", detail);
5068
+ emitConsole(
5069
+ config,
5070
+ `Local route recovery: ${localFailureRecovery.fromModel} -> ${localFailureRecovery.model} after repeated tool failures.`,
5071
+ { kind: "meta" }
5072
+ );
5073
+ await store.saveState(state);
5074
+ }
5033
5075
  const snapshot = await buildSnapshot(browserState, store, step, config);
5034
5076
  state.meta.lastUrl = snapshot.url || state.meta.lastUrl;
5035
5077
  await saveBrowserState(browserState, store).catch(() => {});
@@ -4,9 +4,15 @@ import {
4
4
  validateIntegrationThreadId,
5
5
  } from "./integration-policy.js";
6
6
  import { authorityFail } from "./integration-durable-common.js";
7
+ import { types as utilTypes } from "node:util";
7
8
 
8
9
  export const INTEGRATION_RUN_REGISTRY_ATTESTATION_VERSION = "aginti-live-run-registry-v1";
9
10
 
11
+ const PromisePrototype = Promise.prototype;
12
+ const PromisePrototypeThen = Promise.prototype.then;
13
+ const ReflectApply = Reflect.apply;
14
+ const ReflectOwnKeys = Reflect.ownKeys;
15
+
10
16
  function assertPrincipalId(value) {
11
17
  if (typeof value !== "string" || !/^[A-Za-z0-9._~-]{16,128}$/u.test(value)) {
12
18
  authorityFail("INVALID_PRINCIPAL", "Integration principal scope is invalid.", { status: 401 });
@@ -53,6 +59,27 @@ export function createIntegrationRunRegistry() {
53
59
  const runs = new Map();
54
60
  const threadActiveRun = new Map();
55
61
 
62
+ function releaseExactRecord(runId, record) {
63
+ const current = runs.get(runId);
64
+ if (current !== record) return Object.freeze({ released: false });
65
+ runs.delete(runId);
66
+ if (threadActiveRun.get(record.threadId) === runId) threadActiveRun.delete(record.threadId);
67
+ return Object.freeze({ released: true, runId });
68
+ }
69
+
70
+ function assertNativePromise(promise) {
71
+ if (promise && (typeof promise === "object" || typeof promise === "function") && utilTypes.isProxy(promise)) {
72
+ authorityFail("AGENT_UNAVAILABLE", "Live run registry requires a native run promise.");
73
+ }
74
+ if (!utilTypes.isPromise(promise) || Object.getPrototypeOf(promise) !== PromisePrototype) {
75
+ authorityFail("AGENT_UNAVAILABLE", "Live run registry requires a native run promise.");
76
+ }
77
+ if (ReflectOwnKeys(promise).length !== 0) {
78
+ authorityFail("AGENT_UNAVAILABLE", "Live run registry promise must not expose own fields.");
79
+ }
80
+ return promise;
81
+ }
82
+
56
83
  function claimRun(claimInput = {}) {
57
84
  const claim = normalizeClaim(claimInput);
58
85
  const active = threadActiveRun.get(claim.threadId);
@@ -83,18 +110,23 @@ export function createIntegrationRunRegistry() {
83
110
  const runId = validateIntegrationRunId(runIdInput);
84
111
  const record = runs.get(runId);
85
112
  if (!record) notFound("Run");
86
- if (!(promise instanceof Promise)) {
87
- authorityFail("AGENT_UNAVAILABLE", "Live run registry requires a native run promise.");
113
+ if (record.promise) {
114
+ authorityFail("RUN_CONFLICT", "Integration run already has a native run promise.", { status: 409 });
115
+ }
116
+ try {
117
+ const nativePromise = assertNativePromise(promise);
118
+ const release = () => {
119
+ releaseExactRecord(runId, record);
120
+ };
121
+ const observer = ReflectApply(PromisePrototypeThen, nativePromise, [release, release]);
122
+ ReflectApply(PromisePrototypeThen, observer, [undefined, () => {}]);
123
+ record.promise = nativePromise;
124
+ return nativePromise;
125
+ } catch (error) {
126
+ releaseExactRecord(runId, record);
127
+ if (error?.code || error?.publicCode) throw error;
128
+ authorityFail("AGENT_UNAVAILABLE", "Live run registry could not attach the native run promise.");
88
129
  }
89
- record.promise = promise.finally(() => {
90
- const current = runs.get(runId);
91
- if (current === record) {
92
- runs.delete(runId);
93
- if (threadActiveRun.get(record.threadId) === runId) threadActiveRun.delete(record.threadId);
94
- }
95
- });
96
- record.promise.catch(() => {});
97
- return record.promise;
98
130
  }
99
131
 
100
132
  function getRun(runIdInput, scope = {}) {
@@ -135,9 +167,7 @@ export function createIntegrationRunRegistry() {
135
167
  const runId = validateIntegrationRunId(runIdInput);
136
168
  const record = runs.get(runId);
137
169
  if (!record) return Object.freeze({ released: false });
138
- runs.delete(runId);
139
- if (threadActiveRun.get(record.threadId) === runId) threadActiveRun.delete(record.threadId);
140
- return Object.freeze({ released: true, runId });
170
+ return releaseExactRecord(runId, record);
141
171
  }
142
172
 
143
173
  function hasActiveThreadRun(threadIdInput) {