Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 0 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@ pnpm build # Compile to dist/
| `memory_query` | Query across all memory backends with unified results |
| `memory_stats` | Get memory system statistics dashboard |

## Live integration mode

```bash
NEXUS_LIVE=true npx tsx src/run-live.ts
```

## License

MIT
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@
"scripts": {
"build": "tsc",
"test": "vitest run",
"typecheck": "tsc --noEmit",
"live": "NEXUS_LIVE=true tsx src/run-live.ts"
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"tsx": "^4.19.0",
"typescript": "^5.7.0",
"vitest": "^3.2.6",
"@types/node": "^22.0.0"
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 63 additions & 0 deletions src/benchmark.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
countAvailableBackends,
computeSummary,
runBenchmark,
withTimeout,
ToolCallTimeoutError,
} from './benchmark.js';
import type { BenchmarkConfig, QueryBenchmarkResult } from './types.js';
import {
Expand Down Expand Up @@ -341,3 +343,64 @@ describe('runBenchmark', () => {
expect(queryCall?.source).toBe('session');
});
});

// ============================================================================
// withTimeout
// ============================================================================

describe('withTimeout', () => {
it('rejects with ToolCallTimeoutError when the underlying call hangs', async () => {
const caller: ToolCaller = {
call: vi.fn(() => new Promise<unknown>(() => {})),
};

const bounded = withTimeout(caller, 10);

await expect(bounded.call('memory_query', {})).rejects.toBeInstanceOf(
ToolCallTimeoutError
);
});

it('passes through a fast result', async () => {
const caller: ToolCaller = {
call: vi.fn(async () => ({ ok: true })),
};

const bounded = withTimeout(caller, 1000);

await expect(bounded.call('memory_stats', {})).resolves.toEqual({
ok: true,
});
});

it('propagates an underlying thrown error', async () => {
const caller: ToolCaller = {
call: vi.fn(async () => {
throw new Error('backend exploded');
}),
};

const bounded = withTimeout(caller, 1000);

await expect(bounded.call('memory_query', {})).rejects.toThrow(
'backend exploded'
);
});

it('aborts the signal on timeout', async () => {
let captured: AbortSignal | undefined;
const caller: ToolCaller = {
call: vi.fn((_toolName: string, args: Record<string, unknown>) => {
captured = args['signal'] as AbortSignal;
return new Promise<unknown>(() => {});
}),
};

const bounded = withTimeout(caller, 10);

await expect(bounded.call('memory_query', {})).rejects.toBeInstanceOf(
ToolCallTimeoutError
);
expect(captured?.aborted).toBe(true);
});
});
52 changes: 50 additions & 2 deletions src/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,51 @@ export interface ToolCaller {
call(toolName: string, args: Record<string, unknown>): Promise<unknown>;
}

/** Default per-call timeout for remote tool calls (ms). */
export const DEFAULT_TIMEOUT_MS = 30_000;

/** Error thrown when a tool call exceeds its configured timeout. */
export class ToolCallTimeoutError extends Error {
constructor(toolName: string, timeoutMs: number) {
super(`Tool call '${toolName}' timed out after ${timeoutMs}ms`);
this.name = 'ToolCallTimeoutError';
}
}

/**
* Wrap a ToolCaller so every call is bounded by `timeoutMs`.
*
* Uses an AbortController combined with a timer, guaranteeing the returned
* promise settles even if the underlying call hangs forever.
*/
export function withTimeout(
caller: ToolCaller,
timeoutMs: number = DEFAULT_TIMEOUT_MS
): ToolCaller {
return {
call(toolName: string, args: Record<string, unknown>): Promise<unknown> {
const controller = new AbortController();
const callArgs = { ...args, signal: controller.signal };
return new Promise<unknown>((resolve, reject) => {
const timer = setTimeout(() => {
controller.abort();
reject(new ToolCallTimeoutError(toolName, timeoutMs));
}, timeoutMs);
caller.call(toolName, callArgs).then(
(value) => {
clearTimeout(timer);
resolve(value);
},
(err) => {
clearTimeout(timer);
reject(err);
}
);
});
},
};
}

// ============================================================================
// Individual steps
// ============================================================================
Expand Down Expand Up @@ -73,17 +118,20 @@ export async function runBenchmark(
caller: ToolCaller,
config: BenchmarkConfig
): Promise<BenchmarkResult> {
// Bound every remote call so a hung server can't make the benchmark hang.
const boundedCaller = withTimeout(caller, config.timeoutMs);

// Step 1: Collect stats
let stats: MemoryStatsResponse | null = null;
if (config.includeStats !== false) {
stats = await fetchStats(caller);
stats = await fetchStats(boundedCaller);
}

// Step 2: Run all queries
const queryResults: QueryBenchmarkResult[] = [];
for (const bench of config.queries) {
const { result, durationMs } = await runQuery(
caller,
boundedCaller,
bench.query,
config.source,
bench.limit
Expand Down
15 changes: 0 additions & 15 deletions src/live-caller.ts

This file was deleted.

43 changes: 0 additions & 43 deletions src/run-live.ts

This file was deleted.

2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ export interface BenchmarkConfig {
readonly includeStats?: boolean;
/** Filter memory source. */
readonly source?: MemoryQueryInput['source'];
/** Per-call timeout (ms) for remote tool calls. Omit to use the default. */
readonly timeoutMs?: number;
}

export interface QueryBenchmark {
Expand Down
Loading