@companion-ai/feynman 0.5.17 → 0.5.18

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.
@@ -31,7 +31,7 @@ You are Feynman's evidence-gathering subagent.
31
31
  | Biomedical papers | `source: "pubmed"`, then `source: "europepmc"` for open-access full-text sections | `semanticscholar` for citation counts |
32
32
  | Citation graph | `source: "openalex"` with `openalex_citations:` / `openalex_references:` | `semanticscholar` citation counts to spot seminal work |
33
33
  | Conceptual or recent work that keyword search misses | `source: "openalex"` with a `semantic:` query prefix | `semanticscholar` with `sort: "relevance"` |
34
- | Web, docs, repos, grey literature | `web_search` (if the default provider fails or is rate-limited, retry with `provider: "parallel-mcp"`, which needs no key) | `fetch_content` on the best results |
34
+ | Web, docs, repos, grey literature | `web_search` (if the default provider fails or is rate-limited, retry with `provider` set to the plain string `parallel-mcp`, not a JSON-encoded list; it needs no key) | `fetch_content` on the best results |
35
35
  | Known paper ID | `source: "arxiv"` for arXiv IDs, `source: "crossref"` for DOIs | `fetch_content` on `arxiv.org/html/<id>` for full text |
36
36
 
37
37
  Run 2–4 reworded queries for each question (synonyms, the method's name, the problem's name) and merge the results. Do not trust one query's ranking; seminal papers often appear only under one phrasing or one sort order.
package/RELEASES.md CHANGED
@@ -6,6 +6,12 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## v0.5.18 - 2026-09-30
10
+
11
+ - **A rate-limited paper index no longer ends a research step.** When Semantic Scholar is rate-limited or down, a search is answered from OpenAlex, and an OpenAlex topic search that hits OpenAlex's anonymous rate limit or an outage is answered from Semantic Scholar. The result says first which index answered and why. Lookups by OpenAlex ID, DOI, author, venue, or citation still report the error, since the other index has no equivalent.
12
+ - **alphaXiv tools answer when alphaXiv cannot.** If your alphaXiv login expired or alphaXiv does not respond, `alpha_search` returns OpenAlex semantic search results and `alpha_get_paper` returns the arXiv metadata and abstract for an arXiv ID, each marked as a fallback. Run `feynman alpha login` to get alphaXiv's reports back.
13
+ - pi-web-access 0.35.0: OpenAI web search works when you signed in with ChatGPT (`feynman model login openai`), and Brave search keeps to its rate limits. The researcher is told to pass a search provider as a plain string, since some models sent `provider: "[\"parallel-mcp\"]"`, which `web_search` rejects; nicobailon/pi-web-access#491 makes `web_search` accept that form too.
14
+
9
15
  ## v0.5.17 - 2026-09-30
10
16
 
11
17
  - **`feynman model login openai` works again.** Since Pi 0.99, Sign in with ChatGPT needs a stable ID for the installation, which Feynman did not pass, so the login failed at once with "requires a device ID (UUID)". Feynman now shares the `deviceId` Pi's own `/login` keeps in `~/.feynman/agent/settings.json`.
@@ -13,6 +13,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
13
  import { Type } from "typebox";
14
14
 
15
15
  import { extractPaperSections } from "./alpha-sections.js";
16
+ import { searchArxiv } from "./science-database-arxiv.js";
17
+ import { searchOpenAlex } from "./science-database-openalex.js";
16
18
 
17
19
  function formatText(value: unknown): string {
18
20
  if (typeof value === "string") return value;
@@ -21,11 +23,23 @@ function formatText(value: unknown): string {
21
23
 
22
24
  // alphaXiv's API has changed without notice before; when a call fails, point
23
25
  // the model at the other paper sources instead of leaving it on a broken tool.
24
- async function withPaperFallback<T>(run: () => Promise<T>): Promise<T> {
26
+ const ARXIV_ID = /(?:^|arxiv\.org\/(?:abs|pdf)\/|alphaxiv\.org\/(?:abs|overview)\/)(\d{4}\.\d{4,5}(?:v\d+)?|[a-z-]+(?:\.[A-Z]{2})?\/\d{7}(?:v\d+)?)/i;
27
+
28
+ // When alphaXiv does not answer (expired login, outage), answer from another
29
+ // paper source and say so first, instead of failing the research step.
30
+ async function withPaperFallback<T>(
31
+ run: () => Promise<T>,
32
+ fallback?: { source: string; run: () => Promise<Record<string, unknown>> },
33
+ ): Promise<T | Record<string, unknown>> {
25
34
  try {
26
35
  return await run();
27
36
  } catch (error) {
28
37
  const message = (error instanceof Error ? error.message : String(error)).replace("`alpha login`", "`feynman alpha login`");
38
+ if (fallback) {
39
+ try {
40
+ return { fallbackNote: `alphaXiv did not answer (${message}); these results are from ${fallback.source}.`, ...(await fallback.run()) };
41
+ } catch {}
42
+ }
29
43
  throw new Error(
30
44
  `${message}\nalphaXiv did not answer this call. Use feynman_science_database_search (source "arxiv" or "semanticscholar") or fetch_content on https://arxiv.org/abs/<id> instead.`,
31
45
  );
@@ -65,7 +79,10 @@ export function registerAlphaTools(pi: ExtensionAPI, signedIn = isLoggedIn()): v
65
79
  ),
66
80
  }),
67
81
  async execute(_toolCallId, params) {
68
- const result = await withPaperFallback(() => searchPapers(params.query, params.mode?.trim() || "semantic"));
82
+ const result = await withPaperFallback(() => searchPapers(params.query, params.mode?.trim() || "semantic"), {
83
+ source: "OpenAlex semantic search",
84
+ run: () => searchOpenAlex({ source: "openalex", query: `semantic: ${params.query}`, limit: 10 }),
85
+ });
69
86
  return { content: [{ type: "text", text: formatText(result) }], details: result };
70
87
  },
71
88
  });
@@ -87,7 +104,14 @@ export function registerAlphaTools(pi: ExtensionAPI, signedIn = isLoggedIn()): v
87
104
  sections: Type.Optional(paperSectionsSchema),
88
105
  }),
89
106
  async execute(_toolCallId, params) {
90
- const result = await withPaperFallback(() => getPaper(params.paper, { fullText: params.fullText }));
107
+ const arxivId = ARXIV_ID.exec(params.paper.trim())?.[1];
108
+ const result = await withPaperFallback(() => getPaper(params.paper, { fullText: params.fullText }), arxivId ? {
109
+ source: "arXiv metadata and abstract (no full text or sections)",
110
+ run: () => searchArxiv({ query: arxivId }),
111
+ } : undefined);
112
+ if ("fallbackNote" in result) {
113
+ return { content: [{ type: "text", text: formatText(result) }], details: result };
114
+ }
91
115
  const extracted = extractPaperSections(result.content, params.section, params.sections);
92
116
  const filteredResult = extracted.requested.length
93
117
  ? {
@@ -380,19 +380,54 @@ async function searchSemanticScholar(params: ScienceDatabaseSearchParams): Promi
380
380
  };
381
381
  }
382
382
 
383
+ // OpenAlex lookups by ID, DOI, author, venue, or citation have no Semantic
384
+ // Scholar equivalent; only plain topic searches fall back.
385
+ const OPENALEX_LOOKUP_QUERY =
386
+ /^(?:openalex_\w+|rate-limit|authors?|author-search|sources?|venues?|citations?|references?|work|detail)(?::|\s|$)|^(?:W\d+|doi:\s*10\.|10\.\S+\/)/i;
387
+
388
+ function isIndexUnavailable(error: unknown): boolean {
389
+ if (error instanceof ScienceDatabaseRequestError) return error.status === 429 || error.status >= 500;
390
+ return error instanceof Error && /^OpenAlex request failed: (?:429|5\d\d)\b/.test(error.message);
391
+ }
392
+
393
+ // A rate-limited or overloaded index should not end a research step: answer a
394
+ // topic search from the other general index, and say so first in the result.
395
+ async function withIndexFallback(
396
+ run: () => Promise<Record<string, unknown>>,
397
+ fallback: (() => Promise<Record<string, unknown>>) | undefined,
398
+ describe: (reason: string) => string,
399
+ ): Promise<Record<string, unknown>> {
400
+ try {
401
+ return await run();
402
+ } catch (error) {
403
+ if (!fallback || !isIndexUnavailable(error)) throw error;
404
+ let result: Record<string, unknown>;
405
+ try {
406
+ result = await fallback();
407
+ } catch {
408
+ throw error;
409
+ }
410
+ return { fallbackNote: describe(error instanceof Error ? error.message : String(error)), ...result };
411
+ }
412
+ }
413
+
383
414
  async function scienceDatabaseSearch(params: ScienceDatabaseSearchParams): Promise<Record<string, unknown>> {
384
415
  if (params.source === "arxiv") return searchArxiv(params);
385
416
  if (params.source === "biorxiv") return searchPreprints(params, "biorxiv");
386
417
  if (params.source === "medrxiv") return searchPreprints(params, "medrxiv");
387
418
  if (params.source === "pubmed") return searchPubMed(params);
388
419
  if (params.source === "crossref") return searchCrossref(params);
389
- if (params.source === "semanticscholar") return searchSemanticScholar(params);
420
+ const openAlex = () => searchOpenAlex({ limit: params.limit, query: params.query, source: "openalex" });
421
+ const semanticScholar = () => searchSemanticScholar({ ...params, source: "semanticscholar" });
422
+ if (params.source === "semanticscholar") {
423
+ return withIndexFallback(semanticScholar, openAlex, (reason) => `Semantic Scholar was unavailable (${reason}); these results are from OpenAlex.`);
424
+ }
390
425
  if (params.source === "openalex") {
391
- return searchOpenAlex({
392
- limit: params.limit,
393
- query: params.query,
394
- source: params.source,
395
- });
426
+ return withIndexFallback(
427
+ openAlex,
428
+ OPENALEX_LOOKUP_QUERY.test(params.query.trim()) ? undefined : semanticScholar,
429
+ (reason) => `OpenAlex was unavailable (${reason}); these results are from Semantic Scholar.`,
430
+ );
396
431
  }
397
432
  return searchEuropePmc(params);
398
433
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.5.17",
3
+ "version": "0.5.18",
4
4
  "description": "Research-first CLI agent built on Pi and alphaXiv",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -83,7 +83,7 @@
83
83
  "pi-btw": "0.6.1",
84
84
  "pi-docparser": "4.0.0",
85
85
  "pi-subagents": "0.73.1",
86
- "pi-web-access": "0.34.0",
86
+ "pi-web-access": "0.35.0",
87
87
  "posthog-node": "^5.53.0",
88
88
  "undici": "^8.11.2"
89
89
  },