Skip to content

Commit 9edf1a7

Browse files
committed
fix(agents): reset rehire onboarding state and harden cycle projection
1 parent 6f13e33 commit 9edf1a7

File tree

20 files changed

+1149
-116
lines changed

20 files changed

+1149
-116
lines changed

typescript/clients/web-ag-ui/apps/agent-clmm/src/accounting/snapshot.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ function extractManagedPools(params: {
110110
flowLog?: FlowLogEvent[];
111111
managedPoolAddresses?: Array<`0x${string}`>;
112112
}): Set<string> | null {
113+
if (params.managedPoolAddresses && params.managedPoolAddresses.length > 0) {
114+
return new Set(params.managedPoolAddresses.map((poolAddress) => poolAddress.toLowerCase()));
115+
}
116+
113117
const pools = new Set<string>();
114118
if (params.flowLog) {
115119
for (const event of params.flowLog) {
@@ -121,11 +125,6 @@ function extractManagedPools(params: {
121125
}
122126
}
123127
}
124-
if (pools.size === 0 && params.managedPoolAddresses) {
125-
for (const poolAddress of params.managedPoolAddresses) {
126-
pools.add(poolAddress.toLowerCase());
127-
}
128-
}
129128
return pools.size > 0 ? pools : null;
130129
}
131130

typescript/clients/web-ag-ui/apps/agent-clmm/src/accounting/snapshot.unit.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,56 @@ describe('createCamelotNavSnapshot', () => {
248248
expect(call?.positions[0]?.poolAddress).toBe('0xpool1');
249249
});
250250

251+
it('prefers explicit managed pool addresses over stale flow-log pool entries', async () => {
252+
const { createCamelotNavSnapshot } = await import('./snapshot.js');
253+
254+
const positions: WalletPosition[] = [
255+
basePosition,
256+
{ ...basePosition, poolAddress: '0xpool2' },
257+
];
258+
const flowLog: FlowLogEvent[] = [
259+
{
260+
id: 'flow-supply-old',
261+
type: 'supply',
262+
timestamp: '2025-01-01T00:00:00.000Z',
263+
contextId: 'ctx-1',
264+
chainId: 42161,
265+
protocolId: 'camelot-clmm',
266+
poolAddress: '0xpool1',
267+
},
268+
];
269+
270+
const poolTwo: CamelotPool = {
271+
...basePool,
272+
address: '0xpool2',
273+
};
274+
const { client } = buildClient({ positions, pools: [basePool, poolTwo] });
275+
resolveTokenPriceMap.mockResolvedValue(new Map());
276+
computeCamelotPositionValues.mockReturnValue([
277+
{
278+
positionId: 'camelot-0xpool2-0',
279+
poolAddress: '0xpool2',
280+
protocolId: 'camelot-clmm',
281+
tokens: [],
282+
positionValueUsd: 123,
283+
},
284+
]);
285+
286+
await createCamelotNavSnapshot({
287+
contextId: 'ctx-1',
288+
trigger: 'cycle',
289+
walletAddress: '0xabc',
290+
chainId: 42161,
291+
camelotClient: client,
292+
flowLog,
293+
managedPoolAddresses: ['0xpool2'],
294+
});
295+
296+
const call = computeCamelotPositionValues.mock.calls[0]?.[0];
297+
expect(call?.positions).toHaveLength(1);
298+
expect(call?.positions[0]?.poolAddress).toBe('0xpool2');
299+
});
300+
251301
it('reports mixed pricing sources and aggregates fees/rewards', async () => {
252302
const { createCamelotNavSnapshot } = await import('./snapshot.js');
253303

typescript/clients/web-ag-ui/apps/agent-clmm/src/workflow/accounting.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,20 @@ export async function createCamelotAccountingSnapshot(params: {
4444
return null;
4545
}
4646

47+
const managedPoolAddress =
48+
params.state.thread.selectedPool?.address ??
49+
params.state.thread.operatorInput?.poolAddress ??
50+
params.state.thread.metrics.lastSnapshot?.address ??
51+
params.state.thread.metrics.latestSnapshot?.poolAddress;
52+
4753
return createCamelotNavSnapshot({
4854
contextId,
4955
trigger: params.trigger,
5056
walletAddress,
5157
chainId: ARBITRUM_CHAIN_ID,
5258
camelotClient: params.camelotClient,
5359
flowLog: params.flowLog ?? params.state.thread.accounting.flowLog,
54-
managedPoolAddresses: params.state.thread.selectedPool
55-
? [params.state.thread.selectedPool.address]
56-
: undefined,
60+
managedPoolAddresses: managedPoolAddress ? [managedPoolAddress] : undefined,
5761
transactionHash: params.transactionHash,
5862
threadId: params.threadId,
5963
cycle: params.cycle,

typescript/clients/web-ag-ui/apps/agent-clmm/src/workflow/accounting.unit.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,54 @@ describe('createCamelotAccountingSnapshot', () => {
7878
expect(result).toBeNull();
7979
expect(createCamelotNavSnapshot).not.toHaveBeenCalled();
8080
});
81+
82+
it('uses operator input pool as managed pool fallback when selected pool is missing', async () => {
83+
const { createCamelotAccountingSnapshot } = await import('./accounting.js');
84+
85+
createCamelotNavSnapshot.mockResolvedValue({
86+
contextId: 'thread-1',
87+
trigger: 'cycle',
88+
timestamp: '2026-02-20T00:00:00.000Z',
89+
protocolId: 'camelot-clmm',
90+
walletAddress: '0x1111111111111111111111111111111111111111',
91+
chainId: 42161,
92+
totalUsd: 1,
93+
positions: [],
94+
priceSource: 'unknown',
95+
});
96+
97+
const state = {
98+
thread: {
99+
operatorInput: {
100+
poolAddress: '0x3333333333333333333333333333333333333333',
101+
walletAddress: '0x1111111111111111111111111111111111111111',
102+
baseContributionUsd: 10,
103+
},
104+
operatorConfig: {
105+
walletAddress: '0x1111111111111111111111111111111111111111',
106+
},
107+
selectedPool: undefined,
108+
metrics: {
109+
lastSnapshot: undefined,
110+
latestSnapshot: undefined,
111+
},
112+
accounting: {
113+
flowLog: [],
114+
},
115+
},
116+
} as unknown as ClmmState;
117+
118+
await createCamelotAccountingSnapshot({
119+
state,
120+
camelotClient: {} as never,
121+
trigger: 'cycle',
122+
threadId: 'thread-1',
123+
});
124+
125+
expect(createCamelotNavSnapshot).toHaveBeenCalledWith(
126+
expect.objectContaining({
127+
managedPoolAddresses: ['0x3333333333333333333333333333333333333333'],
128+
}),
129+
);
130+
});
81131
});

typescript/clients/web-ag-ui/apps/agent-clmm/src/workflow/nodes/fireCommand.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { copilotkitEmitState } from '@copilotkit/sdk-js/langgraph';
22

33
import { applyAccountingUpdate, createFlowEvent } from '../../accounting/state.js';
4+
import { fetchPoolSnapshot } from '../../clients/emberApi.js';
45
import { ARBITRUM_CHAIN_ID } from '../../config/constants.js';
56
import type { ClmmAction } from '../../domain/types.js';
67
import { resolveAccountingContextId } from '../accounting.js';
@@ -41,14 +42,28 @@ export const fireCommandNode = async (
4142
};
4243
}
4344

44-
const onboardingComplete = Boolean(state.thread.operatorConfig && state.thread.selectedPool);
45-
if (!onboardingComplete) {
45+
const operatorConfig = state.thread.operatorConfig;
46+
let selectedPool = state.thread.selectedPool ?? state.thread.metrics.lastSnapshot;
47+
const fallbackPoolAddress =
48+
state.thread.operatorInput?.poolAddress ??
49+
state.thread.metrics.lastSnapshot?.address ??
50+
state.thread.metrics.latestSnapshot?.poolAddress;
51+
let camelotClient = undefined as ReturnType<typeof getCamelotClient> | undefined;
52+
if (operatorConfig && !selectedPool && fallbackPoolAddress) {
53+
camelotClient = getCamelotClient();
54+
selectedPool =
55+
(await fetchPoolSnapshot(
56+
camelotClient,
57+
fallbackPoolAddress,
58+
ARBITRUM_CHAIN_ID,
59+
)) ?? undefined;
60+
}
61+
62+
if (!operatorConfig) {
4663
const { task, statusEvent } = buildTaskStatus(
4764
currentTask,
48-
state.thread.operatorConfig ? 'failed' : 'canceled',
49-
state.thread.operatorConfig
50-
? 'Agent fired, but workflow is missing a selected pool.'
51-
: 'Agent fired before onboarding completed.',
65+
'canceled',
66+
'Agent fired before onboarding completed.',
5267
);
5368
await copilotkitEmitState(config, {
5469
thread: { task, activity: { events: [statusEvent], telemetry: [] } },
@@ -66,13 +81,11 @@ export const fireCommandNode = async (
6681
};
6782
}
6883

69-
const operatorConfig = state.thread.operatorConfig;
70-
const selectedPool = state.thread.selectedPool;
71-
if (!operatorConfig || !selectedPool) {
84+
if (!selectedPool) {
7285
const { task, statusEvent } = buildTaskStatus(
7386
currentTask,
74-
'failed',
75-
'Agent fired, but workflow state is missing operatorConfig or selectedPool.',
87+
'completed',
88+
'Agent fired. Managed CLMM pool metadata is unavailable, so unwind was skipped.',
7689
);
7790
await copilotkitEmitState(config, {
7891
thread: { task, activity: { events: [statusEvent], telemetry: [] } },
@@ -125,7 +138,7 @@ export const fireCommandNode = async (
125138
},
126139
});
127140

128-
const camelotClient = getCamelotClient();
141+
const executionCamelotClient = camelotClient ?? getCamelotClient();
129142
let txHash: string | undefined;
130143
let executionFlowEvents: Array<ReturnType<typeof createFlowEvent>> = [];
131144
try {
@@ -136,7 +149,7 @@ export const fireCommandNode = async (
136149
};
137150
const outcome = await executeDecision({
138151
action,
139-
camelotClient,
152+
camelotClient: executionCamelotClient,
140153
pool: selectedPool,
141154
operatorConfig,
142155
fundingTokenAddress: state.thread.fundingTokenInput?.fundingTokenAddress,

0 commit comments

Comments
 (0)