LearnNewsExamplesServices
Frontmatter
id13304
titleNeural Link redo tool — re-apply the last undone transaction
stateClosed
labels
enhancementaitesting
assigneesneo-opus-vega
createdAtJun 15, 2026, 7:04 AM
updatedAtJun 15, 2026, 10:28 AM
githubUrlhttps://github.com/neomjs/neo/issues/13304
authorneo-opus-vega
commentsCount0
parentIssue9848
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 15, 2026, 10:28 AM

Neural Link redo tool — re-apply the last undone transaction

Closed v13.1.0/archive-v13-1-0-chunk-3 enhancementaitesting
neo-opus-vega
neo-opus-vega commented on Jun 15, 2026, 7:04 AM

Context

Slice-1 of the Neural Link mutation-undo substrate (#13221) shipped the in-heap TransactionService undo stack + the undo tool: an agent can say "undo that" and the last committed mutation reverts cleanly under live enforcement. The converged Slice design names the next step explicitly — src/ai/TransactionService.mjs:71-72:

"the undo Neural Link tool, redo + named batching (Slice-2), Memory-Core persistence (Slice-3)…"

This ticket delivers the redo half of Slice-2 (single-level, symmetric to Slice-1's single-level undo). Named transaction batching (begin_transaction / commit_transaction) is the sibling Slice-2 leaf, filed separately. A conversational UI where a user can undo an agent's change naturally needs "redo that" / "actually, put it back" — redo is the immediate undo-pair the #9848 roadmap lists first among follow-ons.

The Problem

Today TransactionService.undo({id}) (src/ai/TransactionService.mjs:263-275) pops the newest committed transaction, marks it undone, and returns its reverse-ops for the caller to re-dispatch — but the popped transaction is discarded from session state. No retained branch exists to re-apply, so an agent cannot redo. The stack is one-directional.

The Architectural Reality

  • Neo.ai.TransactionService (src/ai/TransactionService.mjs) is the in-heap, per-(agentId,sessionId) undo authority. Session state is {open, committed[]} (TransactionService.mjs:108-114). It is pure over its inputs + unit-testable with no live-heap / socket dependency (the design's explicit testability seam) — so this whole slice's logic ships green with no live :8081 bridge.
  • The undo tool flows MCP → app: ai/mcp/server/neural-link/openapi.yaml (path + tier + schema) → ai/mcp/server/neural-link/toolService.mjs (serviceMapping) → the NL service method → bridge → src/ai/Client.mjs (in-app dispatch) → re-dispatched reverse-ops re-enforced as the current requester. redo mirrors this path exactly.
  • Compliance is contract-locked: test/playwright/unit/ai/mcp/validation/OpenApiValidatorCompliance.spec.mjs asserts expectedNeuralLinkToolTiers (toEqual, REQUIRED) — a new tool absent from the fixture fails the ai/mcp/validation scope.

The Fix

1. TransactionService — add a redo branch (symmetric to undo):

  • Add redo: [] to each session entry (alongside open, committed).
  • undo: instead of discarding the popped tx, push it onto redo (retain the undone branch). Status → undone.
  • New redo({id}): pop the newest from redo, set status → committed, push back onto committed, and return its forward-ops in capture order (tx.ops.map(forward)) for the caller to re-apply — re-dispatched + re-enforced as the current requester, with the enforcement subtreePath re-derived on the live tree (NOT the stored audit path), exactly as undo does. Returns {txId:null, forwardOps:null} when there is nothing to redo.
  • Divergence invalidation: committing a new transaction clears the redo stack — a fresh mutation diverges history, so the redo branch is no longer valid (standard editor semantics). sweep already drops the whole session (covers redo). abort / timeout touch only the open tx; they leave redo intact (an aborted open tx is not a divergence).
  • The undo/redo cycle is closed: undo→redo→undo re-uses the same retained tx, status cycling committed ↔ undone.

2. The redo NL tool (mirror undo, the 6-site wiring):

  • openapi.yaml: new redo path + tier (write-class, same tier as undo) + response schema ({redone, txId, reason}).
  • toolService.mjs: serviceMapping entry.
  • The NL service method + src/ai/Client.mjs dispatch: pull forwardOps from TransactionService.redo, re-dispatch each as a validated tool call under live enforcement (mirror the undo handler).
  • OpenApiValidatorCompliance.spec.mjs: add redo to expectedNeuralLinkToolTiers (+ the appropriate write-class set).
  • Unit spec sets the NL config autoConnect=false before the import (the NL-service import-spawns-a-bridge trap).

3. Tests (no live bridge): TransactionService redo unit tests (redo after undo re-applies forward; redo with empty stack → null; a new commit clears the redo stack; the undo→redo→undo cycle; sweep clears redo) + a Client redo-dispatch wiring unit test + the compliance-fixture update.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
redo NL tool (NEW) TransactionService.mjs:71-72 (Slice-2 = redo); merged undo tool (#13257) as mirror Single-level redo of the last undone tx; re-applies forward-ops under live enforcement as the current requester {redone:false, reason:'nothing-to-redo' | 'no-writer-identity'} (fail-closed, mirrors undo) ai/mcp/server/neural-link/openapi.yaml TransactionService redo unit tests + Client wiring unit test + OpenApiValidatorCompliance fixture
TransactionService.redo({id}) (NEW method) TransactionService.mjs undo (symmetric) Pop redo stack → forwardOps in capture order; status undone→committed {txId:null, forwardOps:null} class JSDoc (Anchor & Echo) TransactionService.spec.mjs
TransactionService.commit (MODIFIED) TransactionService.mjs:227-251 Also clears the session's redo stack (divergence invalidation) unchanged fail-closed class JSDoc TransactionService.spec.mjs (new divergence test)

Decision Record impact

aligned-with the converged undo Slice design (Discussion #13216#13221; the slice-naming in TransactionService.mjs:71-72). No ADR challenged or superseded. No new ADR required — a leaf slice within an already-converged design.

Acceptance Criteria

  • TransactionService.redo({id}) re-applies the last undone transaction: returns its forward-ops in capture order, restores it to the committed stack (status committed), and it is undoable again.
  • redo with an empty redo stack (or nothing undone) returns {txId:null, forwardOps:null} — fail-closed, no throw.
  • Committing a NEW transaction clears the session's redo stack (a post-undo divergence invalidates the redo branch).
  • undo retains the popped transaction on the redo stack (no longer discards it); the undo→redo→undo cycle re-uses it.
  • sweep clears the redo stack (assert the whole-session delete covers it).
  • The redo NL tool is wired through all 6 sites (openapi path+tier+schema, toolService serviceMapping, service method, Client dispatch, the OpenApiValidatorCompliance fixture, unit spec autoConnect=false) and re-dispatches forward-ops under live enforcement as the current requester.
  • Fail-closed on incomplete writer identity (no-writer-identity), mirroring undo.
  • (Post-merge / bridge-gated) a live create → undo → redo → assert-restored e2e — deferred to a follow-up like the AC10 undo e2e (#13286), since Neo CI does not run the e2e suite and a live run needs a fresh :8081 bridge.

Out of Scope

  • Named transaction batching (begin_transaction / commit_transaction tools) — the sibling Slice-2 leaf, filed separately.
  • Multi-level redo(n) — Slice-1/2 are single-level by the converged design.
  • Memory-Core persistence of the redo branch — Slice-3.
  • The live e2e — bridge-gated follow-up (see the last AC).

Avoided Traps

  • Not re-deriving the enforcement path from the stored targetSubtreePath — redo, like undo, re-derives the subtreePath on the live tree at redo time + re-enforces as the current requester (the stored path is audit metadata only; TransactionService.mjs:65-68).
  • Not leaving the redo stack live across a divergent commit (a classic redo bug — redoing onto a changed tree). Commit clears it.
  • Not adding a live-heap or socket dependency to TransactionService — the redo branch stays pure in-heap state, unit-testable via event sequences (the design's testability invariant).

Related

  • Parent: #9848 (NL Transaction/Undo Stack — the slice-tracking umbrella). Refs Epic #13012 (Pillar-2, Agent Harness).
  • Slice-1: #13221 (undo) — #13230 (TransactionService core), #13226 (WriteGuard enforcement), #13274 (remove capture), #13284 (disconnect sweep), #13257 (undo tool). #13286 (AC10 undo e2e, bridge-gated).
  • Sibling Slice-2 leaf (to file): named transaction batching.

Origin Session ID: a0bc6b23-78c5-4bd7-b944-9db5e236f42d

Retrieval Hint: "TransactionService redo single-level Slice-2"; branch agent/<this-ticket>-nl-redo off the merged Slice-1 undo stack.

Release classification: boardless (Slice-2 feature on the undo substrate — not v13-release-blocking; post-Approve-asymptote per the §4 scope judgment).