Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
bff5ebc
feat: harden agent thread lifecycle projection and ui-state boundaries
0xTomDaniel Feb 28, 2026
5f8d81d
chore: ignore runtime transition logs
0xTomDaniel Feb 28, 2026
801c748
fix(agents): stabilize onboarding state transitions and command routing
0xTomDaniel Feb 28, 2026
c98cb28
fix: harden lifecycle and fire run invariants
0xTomDaniel Mar 2, 2026
f3a78c0
fix(agent-gmx-allora): use delegator wallet for perpetual plan building
0xTomDaniel Mar 3, 2026
afdef6d
fix(web): treat fire stream aborts as transient retries
0xTomDaniel Mar 3, 2026
85b776b
chore(web-ag-ui): make start depend on build
0xTomDaniel Mar 3, 2026
6f13e33
fix(agent-pendle): use transition helper for workflow nodes
0xTomDaniel Mar 3, 2026
9edf1a7
fix(agents): reset rehire onboarding state and harden cycle projection
0xTomDaniel Mar 3, 2026
ae81d03
fix(web): disconnect agent connect streams on detail teardown
0xTomDaniel Mar 4, 2026
3b1f16a
fix(agent-clmm): prevent stale workflow regressions
0xTomDaniel Mar 4, 2026
7181853
fix: harden agent onboarding transitions and stream teardown
0xTomDaniel Mar 4, 2026
9e5c6de
fix(agent-clmm): compute inherited position apy from post-hire fee delta
0xTomDaniel Mar 4, 2026
e342101
fix(gmx-allora): stabilize position handling and session metrics
0xTomDaniel Mar 5, 2026
02cc840
fix(agent-pendle): add latency instrumentation
0xTomDaniel Mar 6, 2026
9e284f6
fix(agent-pendle): restore buy pt routing
0xTomDaniel Mar 6, 2026
73e89c7
fix(agent-pendle): match market symbols case-insensitively
0xTomDaniel Mar 6, 2026
c298710
Merge remote-tracking branch 'origin/next' into feature/455-fix-ui-fl…
0xTomDaniel Mar 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions typescript/clients/web-ag-ui/apps/agent-clmm/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ coverage

# LangGraph API
.langgraph_api
.logs/

# This package intentionally keeps its own lockfile for standalone Docker builds.
!pnpm-lock.yaml
1 change: 1 addition & 0 deletions typescript/clients/web-ag-ui/apps/agent-clmm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"typecheck": "tsc --noEmit --project tsconfig.eslint.json",
"format": "prettier \"{src,tests}/**/*.{ts,tsx,js,jsx}\" --write",
"format:check": "prettier \"{src,tests}/**/*.{ts,tsx,js,jsx}\" --check",
"transitions:analyze": "tsx scripts/analyze-state-transitions.ts",
"workflow:demo": "tsx --env-file=.env ./scripts/run-camelot-demo.ts",
"wallet:withdraw": "tsx --env-file=.env scripts/manualWithdraw.ts"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { existsSync, readFileSync } from 'node:fs';
import process from 'node:process';
import { resolve } from 'node:path';

import {
type ClmmTransitionSource,
evaluateTransitionBudget,
parseTransitionLogNdjson,
summarizeTransitionChurn,
type TransitionBudget,
} from '../src/workflow/stateTransitionAnalysis.js';

const DEFAULT_LOG_PATH = './.logs/clmm-state-transitions.ndjson';

type ParsedArgs = {
logPath: string;
budget: TransitionBudget;
json: boolean;
sources: ClmmTransitionSource[];
};

function parseNumberFlag(value: string | undefined, fallback: number): number {
if (!value) {
return fallback;
}
const parsed = Number(value);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
}

function parseSourcesFlag(rawValue: string | undefined): ClmmTransitionSource[] {
if (!rawValue || rawValue === 'reducer') {
return ['threadReducer'];
}
if (rawValue === 'emit') {
return ['applyThreadPatch'];
}
if (rawValue === 'both') {
return ['threadReducer', 'applyThreadPatch'];
}
return ['threadReducer'];
}

function parseArgs(argv: string[]): ParsedArgs {
const args: Record<string, string | undefined> = {};

for (let index = 0; index < argv.length; index += 1) {
const raw = argv[index];
if (!raw.startsWith('--')) {
continue;
}
const withoutPrefix = raw.slice(2);
const [key, inlineValue] = withoutPrefix.split('=', 2);
if (!key) {
continue;
}
if (inlineValue !== undefined) {
args[key] = inlineValue;
continue;
}
const next = argv[index + 1];
if (next && !next.startsWith('--')) {
args[key] = next;
index += 1;
} else {
args[key] = 'true';
}
}

return {
logPath: resolve(args['log'] ?? process.env['CLMM_STATE_TRANSITION_LOG_PATH'] ?? DEFAULT_LOG_PATH),
budget: {
maxInputRequiredEntries: parseNumberFlag(args['max-input-required-entries'], 3),
maxWorkingToInputRequired: parseNumberFlag(args['max-working-to-input-required'], 3),
maxInputRequiredToWorking: parseNumberFlag(args['max-input-required-to-working'], 3),
maxOnboardingRegressions: parseNumberFlag(args['max-onboarding-regressions'], 0),
},
json: args['json'] === 'true',
sources: parseSourcesFlag(args['sources']),
};
}

function printWriterCounts(title: string, counts: Record<string, number>): void {
const rows = Object.entries(counts).sort((left, right) => right[1] - left[1]);
console.log(`${title}:`);
if (rows.length === 0) {
console.log(' (none)');
return;
}
for (const [writer, count] of rows) {
console.log(` ${writer} -> ${count}`);
}
}

function run(): number {
const parsed = parseArgs(process.argv.slice(2));

if (!existsSync(parsed.logPath)) {
console.error(`[clmm-transition-analysis] Log file not found: ${parsed.logPath}`);
return 1;
}

const raw = readFileSync(parsed.logPath, 'utf8');
const entries = parseTransitionLogNdjson(raw);
const summary = summarizeTransitionChurn(entries, { sources: parsed.sources });
const evaluation = evaluateTransitionBudget(summary, parsed.budget);

if (parsed.json) {
console.log(
JSON.stringify(
{
logPath: parsed.logPath,
budget: parsed.budget,
sources: parsed.sources,
summary,
evaluation,
},
null,
2,
),
);
} else {
console.log(`[clmm-transition-analysis] logPath=${parsed.logPath}`);
console.log(`[clmm-transition-analysis] sources=${parsed.sources.join(',')}`);
console.log(`[clmm-transition-analysis] entries=${summary.totalEntries}`);
console.log(
`[clmm-transition-analysis] input-required entries=${summary.transitions.inputRequiredEntries}`,
);
console.log(
`[clmm-transition-analysis] working -> input-required=${summary.transitions.workingToInputRequired}`,
);
console.log(
`[clmm-transition-analysis] input-required -> working=${summary.transitions.inputRequiredToWorking}`,
);
console.log(
`[clmm-transition-analysis] onboarding regressions=${summary.onboardingRegressions.length}`,
);
printWriterCounts(
'[clmm-transition-analysis] writer attribution (working -> input-required)',
summary.transitions.byWriter.workingToInputRequired,
);
printWriterCounts(
'[clmm-transition-analysis] writer attribution (input-required -> working)',
summary.transitions.byWriter.inputRequiredToWorking,
);

if (!evaluation.passes) {
console.log('[clmm-transition-analysis] budget violations:');
for (const violation of evaluation.violations) {
console.log(` - ${violation}`);
}
} else {
console.log('[clmm-transition-analysis] budget check passed');
}
}

return evaluation.passes ? 0 : 1;
}

process.exitCode = run();
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ function extractManagedPools(params: {
flowLog?: FlowLogEvent[];
managedPoolAddresses?: Array<`0x${string}`>;
}): Set<string> | null {
if (params.managedPoolAddresses && params.managedPoolAddresses.length > 0) {
return new Set(params.managedPoolAddresses.map((poolAddress) => poolAddress.toLowerCase()));
}

const pools = new Set<string>();
if (params.flowLog) {
for (const event of params.flowLog) {
Expand All @@ -121,11 +125,6 @@ function extractManagedPools(params: {
}
}
}
if (pools.size === 0 && params.managedPoolAddresses) {
for (const poolAddress of params.managedPoolAddresses) {
pools.add(poolAddress.toLowerCase());
}
}
return pools.size > 0 ? pools : null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,56 @@ describe('createCamelotNavSnapshot', () => {
expect(call?.positions[0]?.poolAddress).toBe('0xpool1');
});

it('prefers explicit managed pool addresses over stale flow-log pool entries', async () => {
const { createCamelotNavSnapshot } = await import('./snapshot.js');

const positions: WalletPosition[] = [
basePosition,
{ ...basePosition, poolAddress: '0xpool2' },
];
const flowLog: FlowLogEvent[] = [
{
id: 'flow-supply-old',
type: 'supply',
timestamp: '2025-01-01T00:00:00.000Z',
contextId: 'ctx-1',
chainId: 42161,
protocolId: 'camelot-clmm',
poolAddress: '0xpool1',
},
];

const poolTwo: CamelotPool = {
...basePool,
address: '0xpool2',
};
const { client } = buildClient({ positions, pools: [basePool, poolTwo] });
resolveTokenPriceMap.mockResolvedValue(new Map());
computeCamelotPositionValues.mockReturnValue([
{
positionId: 'camelot-0xpool2-0',
poolAddress: '0xpool2',
protocolId: 'camelot-clmm',
tokens: [],
positionValueUsd: 123,
},
]);

await createCamelotNavSnapshot({
contextId: 'ctx-1',
trigger: 'cycle',
walletAddress: '0xabc',
chainId: 42161,
camelotClient: client,
flowLog,
managedPoolAddresses: ['0xpool2'],
});

const call = computeCamelotPositionValues.mock.calls[0]?.[0];
expect(call?.positions).toHaveLength(1);
expect(call?.positions[0]?.poolAddress).toBe('0xpool2');
});

it('reports mixed pricing sources and aggregates fees/rewards', async () => {
const { createCamelotNavSnapshot } = await import('./snapshot.js');

Expand Down
Loading