Skip to content

fix: address 6 review findings — timeout, audit, cache, stderr, filter, comments#7

Merged
false200 merged 3 commits into
false200:mainfrom
Perseus-Computing-LLC:fix/review-findings-2026-06-13
Jun 17, 2026
Merged

fix: address 6 review findings — timeout, audit, cache, stderr, filter, comments#7
false200 merged 3 commits into
false200:mainfrom
Perseus-Computing-LLC:fix/review-findings-2026-06-13

Conversation

@tcconnally

Copy link
Copy Markdown
Contributor

Fixes all 6 issues from a deep-dive code review.

Changes

# Issue Fix
#2 No timeout on upstream tools/list Promise.race with configurable timeout (30s default). New upstreamTimeoutMs config key.
#3 Audit logger NDJSON interleaving Promise chain serializes appendFile calls.
#4 collectTools called on every tools/list 2s TTL cache prevents redundant upstream fan-out.
#5 Upstream stderr unbounded Rate-limited to 10KB/min per upstream via stderrLogBytesPerMinute.
#6 micromatch compiled per-tool Pre-compiled via micromatch.matcher() in constructor.
#1 Comment numbering in shrinker Steps 6→8, 7→9.

Tests

8 files, 36 tests, all passing. No regressions. Backward-compatible — new config keys have sensible defaults.

…r, comments

- Add configurable timeout (30s default) to upstream tools/list calls.
  A hung upstream no longer blocks tool discovery from healthy ones.
  New config key: upstreamTimeoutMs (schema.ts).

- Add 2s TTL cache on collectTools() to prevent redundant fan-out
  when multiple clients call tools/list simultaneously.

- Serialize audit logger writes with a promise chain to prevent
  NDJSON line interleaving under concurrent HTTP requests.

- Rate-limit upstream stderr logging to 10KB/min per upstream
  (configurable via stderrLogBytesPerMinute in server config).

- Pre-compile micromatch glob patterns via matcher() in ToolFilter
  constructor instead of recompiling on every isAllowed() call.

- Fix comment numbering in Shrinker.applyRules() (steps 6→8, 7→9).

Closes false200#1, false200#2, false200#3, false200#4, false200#5, false200#6
@false200

Copy link
Copy Markdown
Owner

Thanks @tcconnally, reviewed the diff locally, all 36 existing tests pass. Left a few inline notes. Happy to merge once all the fixes are in place.

@false200 false200 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three small fixes requested (stderr 0=unlimited, clearTimeout after Promise.race, schema comment accuracy). Rest looks good, will merge once those are addressed. Thanks again!

Comment thread src/upstream/manager.ts Outdated
stderrBytesThisMinute = 0;
stderrMinuteStart = now;
}
if (stderrBytesThisMinute < stderrBudget) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: schema says stderrLogBytesPerMinute: 0 means unlimited, but when budget is 0 this condition is never true, so nothing gets logged.

So the suggested fix would be:
if (stderrBudget === 0 || stderrBytesThisMinute < stderrBudget) {
...
}

Comment thread src/core/aggregator.ts Outdated
const result = await Promise.race([
conn.client.listTools(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`upstream \"${id}\" tools/list timed out after ${timeoutMs}ms`)), timeoutMs),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor leak: if listTools() wins the race, this timer is never cleared.

Suggested pattern would be:
let timer: ReturnType;
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(...)), timeoutMs);
});
try {
const result = await Promise.race([conn.client.listTools(), timeout]);
clearTimeout(timer!);
...
} catch (err) {
clearTimeout(timer!);
...
}

Comment thread src/config/schema.ts Outdated
observability: observabilitySchema,
policy: policySchema,
logLevel: z.enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"]).default("info"),
/** Timeout in ms for upstream listTools/listResources/listPrompts calls (default 30s). */

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment mentions listResources/listPrompts, but the PR only wraps listTools() in aggregator.ts. Either extend the timeout to those calls too, or narrow this comment to tools/list for now.

@false200 false200 assigned false200 and tcconnally and unassigned tcconnally and false200 Jun 15, 2026
@tcconnally

Copy link
Copy Markdown
Contributor Author

All three review findings addressed:

  1. stderr 0=unlimited — added stderrBudget === 0 || guard in manager.ts:174
  2. clearTimeout leak — timer stored and cleared after Promise.race resolves + in catch block (aggregator.ts)
  3. Schema comment — narrowed to just listTools in schema.ts:149

Ready for another look when you have a moment.

Comment thread src/core/aggregator.ts Outdated
setTimeout(() => reject(new Error(`upstream \"${id}\" tools/list timed out after ${timeoutMs}ms`)), timeoutMs),
),
]);
let timer: ReturnType<typeof setTimeout>;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timer is declared inside try, so it's out of scope in catch - tsc fails with:

  • TS2454: Variable 'timer' is used before being assigned (line 233)
  • TS2304: Cannot find name 'timer' (line 264)

Move let timer before the try block.
Suggested fix should look like:

let timer: ReturnType<typeof setTimeout> | undefined;
try {
  const timeout = new Promise<never>((_, reject) => {
    timer = setTimeout(
      () => reject(new Error(`upstream "${id}" tools/list timed out after ${timeoutMs}ms`)),
      timeoutMs,
    );
  });
  const result = await Promise.race([conn.client.listTools(), timeout]);
  if (timer !== undefined) clearTimeout(timer);
  timer = undefined;
  // ... process tools
} catch (err) {
  if (timer !== undefined) clearTimeout(timer);
  this.log.warn({ id, err: errMsg(err) }, "upstream tools/list failed");
}

Comment thread src/core/filter.ts
this.applyTools = apply.tools ?? true;
this.applyResources = apply.resources ?? true;
this.applyPrompts = apply.prompts ?? true;
this.allowMatcher = this.allow.length > 0 ? micromatch.matcher(this.allow) : null;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pnpm typecheck fails here - @types/micromatch types matcher() as taking string, not string[]:

Suggested changes:

this.allowMatcher = this.allow.length > 0 ? micromatch.matcher(this.allow) : null;
this.denyMatcher = this.deny.length > 0 ? micromatch.matcher(this.deny) : null;

to

this.allowMatcher = this.allow.length > 0 ? micromatch.matcher(this.allow as string | string[]) : null;
this.denyMatcher = this.deny.length > 0 ? micromatch.matcher(this.deny as string | string[]) : null;

@false200

Copy link
Copy Markdown
Owner

@tcconnally Thanks for the quick turnaround, stderr fix and schema comment look good.

One more thing on clearTimeout: timer is declared inside the try block so it's out of scope in catch, and tsc fails on this branch. Move let timer before the try.

Also pnpm typecheck fails on filter.ts micromatch.matcher() typings, may need a cast for string[].

Happy to merge once all the fixes are in place.

…laration

- aggregator.ts: move `let timer` before `try` so it is in scope for the
  catch block's clearTimeout call
- Add src/types/micromatch.d.ts to resolve tsc "Could not find declaration
  file for module 'micromatch'" (the @types/micromatch package is a stub)

Addresses review feedback on PR false200#7.
@tcconnally

Copy link
Copy Markdown
Contributor Author

Fixed both remaining issues:

  1. timer scope — moved let timer before try block so it's accessible in catch
  2. micromatch types — added src/types/micromatch.d.ts declaration since the @types/micromatch package is a stub

tsc --noEmit passes clean on these two issues now.

@false200

Copy link
Copy Markdown
Owner

Thanks @tcconnally!
Approving and merging once CI is green.

@false200 false200 merged commit 4456edb into false200:main Jun 17, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants