@companion-ai/feynman 0.5.11 → 0.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/RELEASES.md CHANGED
@@ -6,6 +6,10 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## v0.5.12 - 2026-09-28
10
+
11
+ - Crossref pacing leaves a margin over the pool's limit and retries a rate-limited request once after the server's `Retry-After`. A stress test of three bursts of 10 parallel searches on the public pool now succeeds on all 30; 0.5.11 still lost about one in six to network jitter.
12
+
9
13
  ## v0.5.11 - 2026-09-27
10
14
 
11
15
  - **Crossref lookups no longer fail in bursts.** Crossref's public pool allows one request at a time, one per second, and parallel DOI checks (the verifier cross-checks every citation) got `429 Too Many Requests` on most calls. Crossref requests now run one after another at the pool's pace. Set `CROSSREF_MAILTO` to your email for Crossref's faster polite pool; a 429 now says so instead of a generic "Science database request failed".
@@ -145,7 +145,9 @@ let crossrefQueue: Promise<unknown> = Promise.resolve();
145
145
  let crossrefLastStartedAt = Number.NEGATIVE_INFINITY;
146
146
 
147
147
  export function withCrossrefPacing<T>(polite: boolean, run: () => Promise<T>): Promise<T> {
148
- const minGapMs = polite ? 350 : 1000;
148
+ // A little over the pool's limit: network jitter can land two requests paced
149
+ // exactly one second apart inside the same window.
150
+ const minGapMs = polite ? 400 : 1200;
149
151
  const next = crossrefQueue.then(async () => {
150
152
  const wait = crossrefLastStartedAt + minGapMs - Date.now();
151
153
  if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
@@ -167,9 +169,16 @@ async function searchCrossref(params: ScienceDatabaseSearchParams): Promise<Reco
167
169
  select: "DOI,title,published-print,published-online,issued,container-title,author,is-referenced-by-count,URL,type",
168
170
  ...(mailto ? { mailto } : {}),
169
171
  }).toString();
172
+ const request = () => withCrossrefPacing(Boolean(mailto), () => fetchJson(url));
170
173
  let payload: Record<string, unknown>;
171
174
  try {
172
- payload = recordValue(await withCrossrefPacing(Boolean(mailto), () => fetchJson(url)));
175
+ try {
176
+ payload = recordValue(await request());
177
+ } catch (error) {
178
+ if (!(error instanceof ScienceDatabaseRequestError) || error.status !== 429) throw error;
179
+ await new Promise((resolve) => setTimeout(resolve, Math.min(error.retryAfterMs ?? 1500, 5000)));
180
+ payload = recordValue(await request());
181
+ }
173
182
  } catch (error) {
174
183
  if (!(error instanceof ScienceDatabaseRequestError) || error.status !== 429) throw error;
175
184
  throw new Error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.5.11",
3
+ "version": "0.5.12",
4
4
  "description": "Research-first CLI agent built on Pi and alphaXiv",
5
5
  "license": "MIT",
6
6
  "type": "module",