@lazyingart/agintiflow 0.20.206 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.206",
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");
@@ -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.
@@ -7,6 +7,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
7
7
  import { chromium } from "playwright";
8
8
  import { createClient, createPlan, requestNextStep } from "./model-client.js";
9
9
  import { SessionStore } from "./session-store.js";
10
+ import { assertIntegrationRunAgentInvocation } from "./integration-session-persistence.js";
10
11
  import { captureSnapshot } from "./snapshot.js";
11
12
  import { checkToolUse } from "./guardrails.js";
12
13
  import { ensureDockerSandboxReady, runDockerSandboxCommand } from "./docker-sandbox.js";
@@ -60,6 +61,12 @@ import {
60
61
  resolveLocalCodeRoute,
61
62
  restoreLocalCodePolicy,
62
63
  } from "./local-code-routing.js";
64
+ import {
65
+ activateLocalFailureRecovery,
66
+ applyLocalFailureRecovery,
67
+ decideLocalFailureRecovery,
68
+ localFailureRecoveryInstruction,
69
+ } from "./local-failure-recovery.js";
63
70
  import { resolveRuntimeConfig } from "./config.js";
64
71
  import {
65
72
  captureSessionRuntime,
@@ -236,8 +243,9 @@ function withSelectedSkillReadOnlyRoots(config = {}, state = {}) {
236
243
  function throwIfAborted(config) {
237
244
  if (config.abortSignal?.aborted) {
238
245
  const reason = config.abortSignal.reason;
239
- const error = reason instanceof Error ? reason : new Error("Run interrupted by user.");
240
- error.name = error.name || "AbortError";
246
+ if (reason instanceof Error) throw reason;
247
+ const error = new Error("Run interrupted by user.");
248
+ error.name = "AbortError";
241
249
  throw error;
242
250
  }
243
251
  }
@@ -2524,9 +2532,10 @@ export function recordExactOutputProgress(state = {}, toolResult = {}, config =
2524
2532
  }
2525
2533
 
2526
2534
  export function nextStepRuntimeConfig(config = {}, state = {}) {
2535
+ const runtimeConfig = applyLocalFailureRecovery(config, state);
2527
2536
  if (state.meta?.artifactProgress?.complete) {
2528
2537
  return {
2529
- ...config,
2538
+ ...runtimeConfig,
2530
2539
  artifactValidationPhase: true,
2531
2540
  convergenceOutputPhase: false,
2532
2541
  artifactValidationNeedsRepair: state.meta.artifactProgress.needsRepair === true,
@@ -2540,10 +2549,10 @@ export function nextStepRuntimeConfig(config = {}, state = {}) {
2540
2549
  };
2541
2550
  }
2542
2551
  const staticTotal = Number(state.meta?.toolLoop?.staticTotal || 0);
2543
- if (staticTotal < STATIC_DISCOVERY_CONVERGENCE_LIMIT) return config;
2552
+ if (staticTotal < STATIC_DISCOVERY_CONVERGENCE_LIMIT) return runtimeConfig;
2544
2553
  const requiresPerSourceChecks = state.meta?.scs?.taskContract?.requiresPerSourceChecks === true;
2545
2554
  return {
2546
- ...config,
2555
+ ...runtimeConfig,
2547
2556
  convergenceOutputPhase: true,
2548
2557
  convergenceAllowRunCommand: requiresPerSourceChecks,
2549
2558
  };
@@ -4166,11 +4175,15 @@ async function recordToolContractViolation({ config, state, store, observers, va
4166
4175
  total: totalViolationCount,
4167
4176
  lastCode: validation.code || "TOOL_CALL_INVALID",
4168
4177
  };
4178
+ const localRecovery = decideLocalFailureRecovery(config, state);
4179
+ const deferStopToLocalRecovery =
4180
+ violationCount >= 2 && localRecovery.active === true && localRecovery.activated === true;
4169
4181
  const result = {
4170
4182
  ok: false,
4171
4183
  blocked: true,
4172
- recoverable: violationCount < 2,
4173
- stopRun: violationCount >= 2,
4184
+ recoverable: violationCount < 2 || deferStopToLocalRecovery,
4185
+ stopRun: violationCount >= 2 && !deferStopToLocalRecovery,
4186
+ localFailureRecoveryPending: deferStopToLocalRecovery,
4174
4187
  violationCount,
4175
4188
  reason: validation.reason || "The model returned an invalid tool call and it was not dispatched.",
4176
4189
  category: "tool-contract-violation",
@@ -4524,6 +4537,7 @@ async function recordPreInferenceFailure({ error, config, state, store, observer
4524
4537
  }
4525
4538
 
4526
4539
  export async function runAgent(config) {
4540
+ assertIntegrationRunAgentInvocation(config);
4527
4541
  const incomingConfig = config;
4528
4542
  const sessionId = config.resume || config.sessionId || `web-agent-${crypto.randomUUID()}`;
4529
4543
  const store = new SessionStore(config.sessionsDir, sessionId, {
@@ -5027,6 +5041,37 @@ export async function runAgent(config) {
5027
5041
  const step = state.stepsCompleted + 1;
5028
5042
  throwIfAborted(config);
5029
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
+ }
5030
5075
  const snapshot = await buildSnapshot(browserState, store, step, config);
5031
5076
  state.meta.lastUrl = snapshot.url || state.meta.lastUrl;
5032
5077
  await saveBrowserState(browserState, store).catch(() => {});
@@ -59,7 +59,7 @@ const FORBIDDEN_KEYS = new Set([
59
59
  "url",
60
60
  ]);
61
61
  const ABSOLUTE_PATH_OR_SECRET_PATTERN =
62
- /(?:^|[\s("'`])(?:\/(?:workspace|home|users|root|etc|usr|var|opt|srv|run|tmp|proc|sys|dev|mnt|media|aginti-(?:home|cache|env))(?:\/|\b)|[A-Za-z]:\\|(?:api[_-]?key|token|secret|password)\s*[:=])/iu;
62
+ /(?:^|[\s("'`<>\[{=])(?:file:\/\/\/[^\s"'`<>)\]}]+|\/(?!\/)[^\s"'`<>)\]}]+|[A-Za-z]:[\\/][^\s"'`<>)\]}]+|\\\\[^\\/\s"'`<>)\]}]+\\[^\s"'`<>)\]}]+|(?:api[_-]?key|token|secret|password)\s*[:=])/iu;
63
63
  const EVENT_LEDGER_STORE_REQUIRED_KEYS = Object.freeze([
64
64
  "owner",
65
65
  "authority",
@@ -383,29 +383,103 @@ export function createIntegrationCoreEventProjector(options = {}) {
383
383
  }
384
384
  }
385
385
 
386
+ function toolStateKey(scope) {
387
+ return `${scope.principalId}\n${scope.browserSessionId}\n${scope.threadId}\n${scope.runId}`;
388
+ }
389
+
390
+ function validateLedgerHeadCursor(headInput, label) {
391
+ const seq = Number(headInput?.seq ?? 0);
392
+ const hash = String(headInput?.hash ?? "0".repeat(64));
393
+ if (
394
+ !Number.isSafeInteger(seq) ||
395
+ seq < 0 ||
396
+ !/^[a-f0-9]{64}$/u.test(hash) ||
397
+ ((seq === 0) !== (hash === "0".repeat(64)))
398
+ ) {
399
+ authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", `${label} cursor is invalid.`);
400
+ }
401
+ return Object.freeze({ seq, hash });
402
+ }
403
+
404
+ function assertPublicEventEnvelopeHash(event, label) {
405
+ const checked = createPublicIntegrationEvent({
406
+ threadId: event.threadId,
407
+ runId: event.runId,
408
+ seq: event.seq,
409
+ type: event.type,
410
+ payload: event.payload,
411
+ createdAt: event.createdAt,
412
+ previousHash: event.previousHash,
413
+ });
414
+ if (
415
+ event.schemaVersion !== checked.schemaVersion ||
416
+ event.id !== checked.id ||
417
+ event.hash !== checked.hash ||
418
+ contractDigest(event) !== contractDigest(checked)
419
+ ) {
420
+ authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", `${label} hash is invalid.`);
421
+ }
422
+ }
423
+
424
+ async function loadLedgerHeadAndLastEvent(ledger, scope) {
425
+ if (
426
+ !ledger ||
427
+ typeof ledger.loadHead !== "function" ||
428
+ typeof ledger.loadCursor !== "function" ||
429
+ typeof ledger.loadEventsAfter !== "function"
430
+ ) {
431
+ authorityFail("PUBLIC_EVENT_LEDGER_UNAVAILABLE", "Public event ledger head is unavailable.");
432
+ }
433
+ const head = validateLedgerHeadCursor(await ledger.loadHead(), "Public event ledger head");
434
+ if (head.seq === 0) return Object.freeze({ head, lastEvent: null });
435
+ const cursor = validateLedgerHeadCursor(await ledger.loadCursor(head.seq), "Public event ledger head event");
436
+ if (cursor.seq !== head.seq || cursor.hash !== head.hash) {
437
+ authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Public event ledger head cursor does not match the last event.");
438
+ }
439
+ const events = await ledger.loadEventsAfter(head.seq - 1);
440
+ if (!Array.isArray(events) || events.length !== 1) {
441
+ authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Public event ledger did not return the exact last event.");
442
+ }
443
+ const [lastEvent] = events;
444
+ assertExactPublicEventView(lastEvent, "Public event ledger last event");
445
+ if (
446
+ lastEvent.threadId !== scope.threadId ||
447
+ lastEvent.runId !== scope.runId ||
448
+ lastEvent.seq !== head.seq ||
449
+ lastEvent.hash !== head.hash
450
+ ) {
451
+ authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Public event ledger last event does not match the requested head.");
452
+ }
453
+ if (
454
+ (lastEvent.principalId !== undefined && lastEvent.principalId !== scope.principalId) ||
455
+ (lastEvent.browserSessionId !== undefined && lastEvent.browserSessionId !== scope.browserSessionId)
456
+ ) {
457
+ authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Public event ledger last event substituted scope.");
458
+ }
459
+ assertPublicEventEnvelopeHash(lastEvent, "Public event ledger last event");
460
+ return Object.freeze({ head, lastEvent });
461
+ }
462
+
386
463
  async function appendProjectedEvent(projectedInput, scopeInput) {
387
464
  const scope = assertScope(scopeInput);
388
465
  const projected = projectedInput;
389
466
  if (!projected) return null;
467
+ const projectedIsTerminal = TERMINAL_TYPES.has(projected.type);
468
+ if ((projected.terminal === true) !== projectedIsTerminal) {
469
+ authorityFail("UNSUPPORTED_CORE_EVENT", "Projected event terminal flag does not match its public type.", { status: 400 });
470
+ }
471
+ const stateKey = toolStateKey(scope);
390
472
  const ledger =
391
473
  typeof eventLedgerStore.ledgerForRun === "function"
392
474
  ? eventLedgerStore.ledgerForRun(scope)
393
475
  : null;
394
476
  assertLedgerScope(ledger || {}, scope);
395
- const head =
396
- ledger && typeof ledger.loadHead === "function"
397
- ? await ledger.loadHead()
398
- : { seq: 0, hash: "0".repeat(64) };
399
- const previousSeq = Number(head?.seq || 0);
400
- const previousHash = String(head?.hash || "0".repeat(64));
401
- if (
402
- !Number.isSafeInteger(previousSeq) ||
403
- previousSeq < 0 ||
404
- !/^[a-f0-9]{64}$/u.test(previousHash) ||
405
- ((previousSeq === 0) !== (previousHash === "0".repeat(64)))
406
- ) {
407
- authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Public event ledger head cursor is invalid.");
477
+ const { head, lastEvent } = await loadLedgerHeadAndLastEvent(ledger, scope);
478
+ if (lastEvent && TERMINAL_TYPES.has(lastEvent.type)) {
479
+ authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Public event ledger terminal finality blocks later run events.");
408
480
  }
481
+ const previousSeq = head.seq;
482
+ const previousHash = head.hash;
409
483
  if (
410
484
  projected.expectedPreviousSeq !== undefined &&
411
485
  (projected.expectedPreviousSeq !== previousSeq || projected.expectedPreviousHash !== previousHash)
@@ -418,11 +492,23 @@ export function createIntegrationCoreEventProjector(options = {}) {
418
492
  createdAt: projected.createdAt,
419
493
  });
420
494
  assertExactPublicEventView(event, "Public event ledger append");
495
+ const checked = createPublicIntegrationEvent({
496
+ threadId: event.threadId,
497
+ runId: event.runId,
498
+ seq: event.seq,
499
+ type: event.type,
500
+ payload: event.payload,
501
+ createdAt: event.createdAt,
502
+ previousHash: event.previousHash,
503
+ });
421
504
  if (
422
505
  event.threadId !== scope.threadId ||
423
506
  event.runId !== scope.runId ||
507
+ event.schemaVersion !== checked.schemaVersion ||
508
+ event.id !== checked.id ||
424
509
  event.seq !== previousSeq + 1 ||
425
510
  event.previousHash !== previousHash ||
511
+ event.hash !== checked.hash ||
426
512
  event.type !== projected.type ||
427
513
  event.createdAt !== projected.createdAt ||
428
514
  contractDigest(event.payload) !== contractDigest(projected.payload)
@@ -435,16 +521,7 @@ export function createIntegrationCoreEventProjector(options = {}) {
435
521
  ) {
436
522
  authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Public event ledger append substituted the requested scope.");
437
523
  }
438
- const checked = createPublicIntegrationEvent({
439
- threadId: event.threadId,
440
- runId: event.runId,
441
- seq: event.seq,
442
- type: event.type,
443
- payload: event.payload,
444
- createdAt: event.createdAt,
445
- previousHash: event.previousHash,
446
- });
447
- if (checked.hash !== event.hash) {
524
+ if (checked.hash !== event.hash || contractDigest(checked) !== contractDigest(event)) {
448
525
  authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Appended public event hash is invalid.");
449
526
  }
450
527
  if (ledger && typeof ledger.loadHead === "function") {
@@ -453,36 +530,39 @@ export function createIntegrationCoreEventProjector(options = {}) {
453
530
  authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Public event ledger head did not advance to the appended event.");
454
531
  }
455
532
  }
456
- if (projected.terminal) toolStateByRun.delete(scope.runId);
533
+ if (projected.terminal) {
534
+ toolStateByRun.delete(stateKey);
535
+ }
457
536
  return Object.freeze({ ...checked, terminal: projected.terminal });
458
537
  }
459
538
 
460
539
  async function appendCoreEvent(type, data, scopeInput) {
461
540
  const scope = assertScope(scopeInput);
462
541
  const rawType = String(type || "");
542
+ const stateKey = toolStateKey(scope);
463
543
  const needsToolState = TOOL_EVENT_TYPES.has(rawType);
464
- const hadState = toolStateByRun.has(scope.runId);
465
- let toolState = needsToolState ? toolStateByRun.get(scope.runId) : undefined;
544
+ const hadState = toolStateByRun.has(stateKey);
545
+ let toolState = needsToolState ? toolStateByRun.get(stateKey) : undefined;
466
546
  if (needsToolState && !toolState) {
467
547
  toolState = { nextOrdinal: 0, activeByTool: new Map() };
468
- toolStateByRun.set(scope.runId, toolState);
548
+ toolStateByRun.set(stateKey, toolState);
469
549
  }
470
550
  const snapshot = needsToolState ? snapshotToolState(toolState) : null;
471
551
  let projected = null;
472
552
  try {
473
553
  projected = projectCoreEvent(type, data, { now, runId: scope.runId, ...(needsToolState ? { toolState } : {}) });
474
554
  } catch (error) {
475
- if (needsToolState) restoreToolState(scope.runId, hadState, snapshot);
555
+ if (needsToolState) restoreToolState(stateKey, hadState, snapshot);
476
556
  throw error;
477
557
  }
478
558
  if (!projected) {
479
- if (needsToolState) restoreToolState(scope.runId, hadState, snapshot);
559
+ if (needsToolState) restoreToolState(stateKey, hadState, snapshot);
480
560
  return null;
481
561
  }
482
562
  try {
483
563
  return await appendProjectedEvent(projected, scope);
484
564
  } catch (error) {
485
- if (needsToolState) restoreToolState(scope.runId, hadState, snapshot);
565
+ if (needsToolState) restoreToolState(stateKey, hadState, snapshot);
486
566
  throw error;
487
567
  }
488
568
  }
@@ -496,8 +576,12 @@ export function createIntegrationCoreEventProjector(options = {}) {
496
576
  owner: "aginti",
497
577
  authority: "aginti",
498
578
  appendCoreEvent,
499
- appendProjectedEvent,
500
579
  appendAuthorityTerminalEvent,
580
+ clearRun(scopeInput, options = {}) {
581
+ const scope = assertScope(scopeInput);
582
+ const stateKey = toolStateKey(scope);
583
+ toolStateByRun.delete(stateKey);
584
+ },
501
585
  projectCoreEvent(type, data, scope = {}) {
502
586
  return projectCoreEvent(type, data, { now, runId: scope.runId || "" });
503
587
  },