Skip to content

Commit 1839f76

Browse files
feat(ai): Neural Link list_transactions — undo-stack audit/history tool (#13326) (#13329)
Read-only list_transactions NL tool: a non-consuming projection of TransactionService.stackOf to a {committed, redo} audit summary ({txId, status, opCount, labels} per tx). Wired 6 sites mirroring undo/redo (#13257/#13304): openapi /instance/transactions (read tier) + ListTransactionsRequest, toolService serviceMapping, server-side forward, Client dispatch, the in-app method, the OpenApiValidatorCompliance read-tier fixture. Plus the NeuralLink.md doc-row — the #13318 GuideToolParity guard now keeps doc==openapi parity (42=42). 47 unit/compliance specs green incl the new projection test + undo/redo regression. Child of #9848, Pillar-2 of #13012.
1 parent 17c2fa9 commit 1839f76

8 files changed

Lines changed: 162 additions & 0 deletions

File tree

ai/mcp/server/neural-link/openapi.yaml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,35 @@ paths:
12981298
schema:
12991299
$ref: '#/components/schemas/ErrorResponse'
13001300

1301+
/instance/transactions:
1302+
post:
1303+
summary: List Transactions
1304+
operationId: list_transactions
1305+
x-neo-tool-tier: read
1306+
x-pass-as-object: true
1307+
description: Returns a read-only audit summary of the requester's undo-stack history — the committed (undoable) transactions plus the redo branch, each as {txId, status, opCount, labels}. No mutation.
1308+
tags: [Instance]
1309+
requestBody:
1310+
content:
1311+
application/json:
1312+
schema:
1313+
$ref: '#/components/schemas/ListTransactionsRequest'
1314+
responses:
1315+
'200':
1316+
description: Transaction history
1317+
'400':
1318+
description: Invalid request body
1319+
content:
1320+
application/json:
1321+
schema:
1322+
$ref: '#/components/schemas/ErrorResponse'
1323+
'500':
1324+
description: Internal server error
1325+
content:
1326+
application/json:
1327+
schema:
1328+
$ref: '#/components/schemas/ErrorResponse'
1329+
13011330
/instance/method/call:
13021331
post:
13031332
summary: Call Instance Method
@@ -1818,6 +1847,13 @@ components:
18181847
type: string
18191848
description: The target App Worker Session ID
18201849

1850+
ListTransactionsRequest:
1851+
type: object
1852+
properties:
1853+
sessionId:
1854+
type: string
1855+
description: The target App Worker Session ID
1856+
18211857
GetRouteHistoryRequest:
18221858
type: object
18231859
properties:

ai/mcp/server/neural-link/toolService.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const serviceMapping = {
4343
inspect_state_provider : DataService .inspectStateProvider .bind(DataService),
4444
inspect_store : DataService .inspectStore .bind(DataService),
4545
list_stores : DataService .listStores .bind(DataService),
46+
list_transactions : InstanceService .listTransactions .bind(InstanceService),
4647
manage_connection : ConnectionService .manageConnection .bind(ConnectionService),
4748
manage_neo_config : RuntimeService .manageNeoConfig .bind(RuntimeService),
4849
modify_state_provider : DataService .modifyStateProvider .bind(DataService),

ai/services/neural-link/InstanceService.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,18 @@ class InstanceService extends Base {
102102
return await ConnectionService.call(sessionId, 'redo', {})
103103
}
104104

105+
/**
106+
* Lists the requester's Neural Link transaction history — forwards the `list_transactions` tool to the
107+
* connected App Worker, which returns a read-only audit summary of the writer's undo stack + redo branch.
108+
* See {@link Neo.ai.client.InstanceService#listTransactions}.
109+
* @param {Object} opts
110+
* @param {String} opts.sessionId
111+
* @returns {Promise<Object>}
112+
*/
113+
async listTransactions({sessionId}) {
114+
return await ConnectionService.call(sessionId, 'list_transactions', {})
115+
}
116+
105117
/**
106118
* Calls a method on a specific instance.
107119
* @param {Object} opts

learn/agentos/NeuralLink.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ Generic tools for working with *any* Neo.mjs instance (Components, Stores, Manag
231231
| `call_method` | Calls a method on a specific instance (e.g. `store.load()`, `grid.scrollToRow()`) — the generic instance-action tool. |
232232
| `undo` | Reverts the requester's most-recent committed mutation (single-level), re-dispatching the captured reverse under live enforcement. |
233233
| `redo` | Re-applies the requester's most-recently undone mutation (single-level) — the symmetric counterpart of `undo`. |
234+
| `list_transactions` | Read-only audit view of the requester's undo-stack history — the committed (undoable) transactions + the redo branch. |
234235

235236
### 4. Runtime & System
236237

src/ai/Client.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ class Client extends Base {
128128
set_instance_properties: instance,
129129
undo : instance,
130130
redo : instance,
131+
list_transactions : instance,
131132

132133
get_record : data,
133134
inspect_state_provider: data,

src/ai/client/InstanceService.mjs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,32 @@ class InstanceService extends Service {
436436
return {redone: true, txId, reapplied: forwardOps.length}
437437
}
438438

439+
/**
440+
* Lists the requester's transaction history — the `list_transactions` Neural Link tool (read-only audit view).
441+
*
442+
* A non-consuming projection of the writer's undo state ({@link Neo.ai.TransactionService#stackOf}): the
443+
* `committed` stack (undoable, newest last) + the `redo` branch (redoable). Each entry is summarized to
444+
* `{txId, status, opCount, labels}` — the user-facing op labels, NOT the raw forward/reverse descriptors (those
445+
* are internal). Read-only: no enforcement, no replay, never mutates the stack. Returns empty lists (never
446+
* throws) for a legacy / no-writer-identity caller or an absent stack authority.
447+
* @param {Object} [params] No parameters — lists the requester's own stack.
448+
* @param {Object|null} [context] The Bridge-stamped `{agentId, sessionId}` writer pair (2nd dispatch arg).
449+
* @returns {Promise<Object>} `{committed: Object[], redo: Object[]}` — each entry `{txId, status, opCount, labels}`.
450+
*/
451+
async listTransactions(params, context) {
452+
const transactionService = this.client?.transactionService;
453+
454+
if (!context?.agentId || !context?.sessionId || !transactionService) {
455+
return {committed: [], redo: []}
456+
}
457+
458+
const
459+
{committed, redo} = transactionService.stackOf({id: {agentId: context.agentId, sessionId: context.sessionId}}),
460+
summarize = tx => ({txId: tx.txId, status: tx.status, opCount: tx.ops.length, labels: tx.ops.map(op => op.label)});
461+
462+
return {committed: committed.map(summarize), redo: redo.map(summarize)}
463+
}
464+
439465
/**
440466
* Calls a method on a specific instance.
441467
* @param {Object} params
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {setup} from '../../setup.mjs';
2+
3+
const appName = 'InstanceServiceListTransactionsTest';
4+
5+
setup({
6+
neoConfig: {
7+
unitTestMode: true
8+
},
9+
appConfig: {
10+
name : appName,
11+
isMounted : () => true,
12+
vnodeInitialising: false
13+
}
14+
});
15+
16+
import {test, expect} from '@playwright/test';
17+
import Neo from '../../../../src/Neo.mjs';
18+
import * as core from '../../../../src/core/_export.mjs';
19+
import InstanceService from '../../../../src/ai/client/InstanceService.mjs';
20+
import TransactionService from '../../../../src/ai/TransactionService.mjs';
21+
22+
// Neo.ai.client.InstanceService.listTransactions is the read-only `list_transactions` tool: a non-consuming
23+
// projection of the writer's undo state (TransactionService.stackOf) into a {committed, redo} audit summary
24+
// ({txId, status, opCount, labels} per tx). Pure read — no live tree, no handleRequest, no enforcement — so
25+
// these tests drive it against a real TransactionService with a minimal client stub.
26+
27+
const ID = {agentId: 'agent-a', sessionId: 'sess-a'};
28+
29+
// a minimal valid reverse-op carrying a user-facing label
30+
const op = (label, seq) => ({
31+
sequenceId : seq,
32+
originWriter : {agentId: 'agent-a', sessionId: 'sess-a'},
33+
targetSubtreePath: ['root', 'leaf'],
34+
forward : {tool: 'set_instance_properties', args: {id: 'leaf', properties: {x: 1}}},
35+
reverse : {tool: 'set_instance_properties', args: {id: 'leaf', properties: {x: 0}}},
36+
label
37+
});
38+
39+
test.describe('Neo.ai.client.InstanceService — the `list_transactions` tool', () => {
40+
let service, transactionService;
41+
42+
test.beforeEach(() => {
43+
transactionService = Neo.create(TransactionService);
44+
service = Neo.create(InstanceService, {client: {transactionService}})
45+
});
46+
47+
const commit = (txId, label) => {
48+
transactionService.begin ({id: ID, txId});
49+
transactionService.record({id: ID, txId, op: op(label, `${txId}:1`)});
50+
return transactionService.commit({id: ID, txId})
51+
};
52+
53+
test('projects the committed stack + the redo branch to an audit summary', async () => {
54+
commit('tx-1', 'set x on leaf');
55+
commit('tx-2', 'set y on panel');
56+
transactionService.undo({id: ID}); // tx-2 → undone, onto the redo branch
57+
58+
const result = await service.listTransactions({}, ID);
59+
60+
expect(result.committed.map(t => t.txId)).toEqual(['tx-1']); // tx-1 still undoable
61+
expect(result.committed[0]).toEqual({txId: 'tx-1', status: 'committed', opCount: 1, labels: ['set x on leaf']});
62+
expect(result.redo.map(t => t.txId)).toEqual(['tx-2']); // tx-2 redoable
63+
expect(result.redo[0].status).toBe('undone');
64+
expect(result.redo[0].labels).toEqual(['set y on panel'])
65+
});
66+
67+
test('read-only — multiple reads never mutate the stack', async () => {
68+
commit('tx-1', 'a');
69+
70+
await service.listTransactions({}, ID);
71+
await service.listTransactions({}, ID);
72+
73+
expect(transactionService.stackOf({id: ID}).committed.map(t => t.txId)).toEqual(['tx-1']) // unchanged + still undoable
74+
});
75+
76+
test('fail-closed → empty lists for a no-writer-identity caller or an absent stack service', async () => {
77+
commit('tx-1', 'a');
78+
79+
expect(await service.listTransactions({}, null)).toEqual({committed: [], redo: []}); // no writer identity
80+
81+
const bare = Neo.create(InstanceService, {client: {}});
82+
expect(await bare.listTransactions({}, ID)).toEqual({committed: [], redo: []}) // no transactionService
83+
});
84+
});

test/playwright/unit/ai/mcp/validation/OpenApiValidatorCompliance.spec.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ const expectedNeuralLinkToolTiers = {
115115
inspect_state_provider : 'read',
116116
inspect_store : 'read',
117117
list_stores : 'read',
118+
list_transactions : 'read',
118119
manage_connection : 'admin',
119120
manage_neo_config : 'admin',
120121
modify_state_provider : 'write-locked',

0 commit comments

Comments
 (0)