Context
Sub-A2 of #13221 (NL mutation undo — Slice-1). The TransactionService core (#13230/#13231) and the set_instance_properties reverse-capture hook (Sub-A1, #13248/#13249) have merged. This sub adds the undo NL tool — the layer that makes captured reverses actually invocable, so an agent can say "undo that" and the live app reverts. Pillar-2 / #13012, parent #9848.
Scope — the tool layer (no core change)
The TransactionService core already exposes everything needed: undo({id}) pops the last committed tx, marks it undone, and returns reverseOps (reverse capture order); stackOf({id}) is a non-consuming peek. So Sub-A2 is purely the NL-tool wiring + orchestration:
undo execution: peek the requester's last committed tx (stackOf) → if none, recoverable nothing-to-undo. Re-dispatch each captured reverse op ({tool, args}) through the normal enforced dispatch (Client.handleRequest) so it re-enters admitWrite as the current caller, with subtreePath re-derived on the live tree. On full success, call undo() to consume (mark undone).
- Capture-suppression during replay: the re-dispatch carries a
context.undoReplay marker; the capture-hook skips recording — undo must not create a new undoable tx (single-level Slice-1; redo is Slice-2).
- Preserve-on-fail: a denied / unresolvable re-dispatch → no-op (tx stays
committed, not consumed) + recoverable error. (Safe: capture is suppressed during replay + the App Worker is single-threaded, so the peeked last-committed tx is stable through the consume.)
Acceptance Criteria
Out of Scope
create_component / remove_component reverse-capture + their undo e2e → Sub-B (this tool re-dispatches whatever reverse is captured, so it works for those once Sub-B lands; only set is captured today, so only undo-of-set is e2e-proven here).
redo, named/atomic batching → Slice-2 (#13221 out-of-scope). Memory-Core persistence → Slice-3.
Related
- Parent: #13221 (Slice-1). Slice-tracking parent: #9848. Pillar-2: #13012.
- Core:
src/ai/TransactionService.mjs (undo, stackOf). Capture-hook: src/ai/client/InstanceService.mjs (#13249). Enforcement: src/ai/admitWrite.mjs, deriveSubtreePath.
Authored by @neo-opus-vega (Claude Opus 4.8, Claude Code). Origin Session ID: 4cc428e3-cf36-4324-8646-1b96cb23fa4a.
Context
Sub-A2 of #13221 (NL mutation undo — Slice-1). The TransactionService core (#13230/#13231) and the
set_instance_propertiesreverse-capture hook (Sub-A1, #13248/#13249) have merged. This sub adds theundoNL tool — the layer that makes captured reverses actually invocable, so an agent can say "undo that" and the live app reverts. Pillar-2 / #13012, parent #9848.Scope — the tool layer (no core change)
The TransactionService core already exposes everything needed:
undo({id})pops the last committed tx, marks itundone, and returnsreverseOps(reverse capture order);stackOf({id})is a non-consuming peek. So Sub-A2 is purely the NL-tool wiring + orchestration:undoexecution: peek the requester's last committed tx (stackOf) → if none, recoverablenothing-to-undo. Re-dispatch each capturedreverseop ({tool, args}) through the normal enforced dispatch (Client.handleRequest) so it re-entersadmitWriteas the current caller, withsubtreePathre-derived on the live tree. On full success, callundo()to consume (markundone).context.undoReplaymarker; the capture-hook skips recording — undo must not create a new undoable tx (single-level Slice-1; redo is Slice-2).committed, not consumed) + recoverable error. (Safe: capture is suppressed during replay + the App Worker is single-threaded, so the peeked last-committed tx is stable through the consume.)Acceptance Criteria
undoNL tool (write-locked tier) reverts the requester's last committed transaction, re-dispatching the captured reverse via the enforced dispatch path (re-entersadmitWriteas the current caller;subtreePathre-derived on the live tree — NOT the stored audit path). Generic admincall_methodis NOT undoable (only the 3 whitelisted tools).openapi.yaml(path + tier + schema),ClientserviceMap, the service method, the 2OpenApiValidatorCompliancefixtures (expectedNeuralLinkToolTiers+neuralLinkDangerousReadForbidden), unit test (autoConnect=falsebefore import).undo→ reverts in the live tree, stack consumed.Out of Scope
create_component/remove_componentreverse-capture + their undo e2e → Sub-B (this tool re-dispatches whatever reverse is captured, so it works for those once Sub-B lands; onlysetis captured today, so only undo-of-set is e2e-proven here).redo, named/atomic batching → Slice-2 (#13221 out-of-scope). Memory-Core persistence → Slice-3.Related
src/ai/TransactionService.mjs(undo,stackOf). Capture-hook:src/ai/client/InstanceService.mjs(#13249). Enforcement:src/ai/admitWrite.mjs,deriveSubtreePath.Authored by @neo-opus-vega (Claude Opus 4.8, Claude Code). Origin Session ID: 4cc428e3-cf36-4324-8646-1b96cb23fa4a.