research-copilot 0.2.12 → 0.2.15

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.
Files changed (65) hide show
  1. package/app/out/main/index.mjs +314 -53
  2. package/app/out/preload/index.js +7 -0
  3. package/app/out/renderer/assets/{MilkdownMarkdownEditor-BqfydyHs.js → MilkdownMarkdownEditor-UNNnHq-Q.js} +50 -50
  4. package/app/out/renderer/assets/{arc-B102x0uC.js → arc-CpQkTVSV.js} +1 -1
  5. package/app/out/renderer/assets/{blockDiagram-c4efeb88-hlEjcPtb.js → blockDiagram-c4efeb88-DV91zeYk.js} +8 -8
  6. package/app/out/renderer/assets/{c4Diagram-c83219d4-Dc1nfavC.js → c4Diagram-c83219d4-BYZtgyKD.js} +3 -3
  7. package/app/out/renderer/assets/{channel-ZMdB8bH1.js → channel-DKZcFGxg.js} +1 -1
  8. package/app/out/renderer/assets/{classDiagram-beda092f-Gbpax7vO.js → classDiagram-beda092f-bfcCUskQ.js} +6 -6
  9. package/app/out/renderer/assets/{classDiagram-v2-2358418a-_KwHXPjs.js → classDiagram-v2-2358418a-BR3pIwy5.js} +10 -10
  10. package/app/out/renderer/assets/{clone-DDahKPIj.js → clone-DYFbSe8n.js} +1 -1
  11. package/app/out/renderer/assets/{createText-1719965b-BNW0X7Oe.js → createText-1719965b-DzSrNkTO.js} +2 -2
  12. package/app/out/renderer/assets/{edges-96097737-G9l1oUoZ.js → edges-96097737-NtlJJRFS.js} +3 -3
  13. package/app/out/renderer/assets/{erDiagram-0228fc6a-BUs4XtQY.js → erDiagram-0228fc6a-D-IkKUzx.js} +5 -5
  14. package/app/out/renderer/assets/{flowDb-c6c81e3f-CQ-jcycx.js → flowDb-c6c81e3f-Ci22GdDY.js} +1 -1
  15. package/app/out/renderer/assets/{flowDiagram-50d868cf-uu8ab--w.js → flowDiagram-50d868cf-zRBNrGzH.js} +12 -12
  16. package/app/out/renderer/assets/{flowDiagram-v2-4f6560a1-CAw9BkoT.js → flowDiagram-v2-4f6560a1-vnbqAJeS.js} +12 -12
  17. package/app/out/renderer/assets/{flowchart-elk-definition-6af322e1-BjQ21EHx.js → flowchart-elk-definition-6af322e1-CuVXk2cv.js} +6 -6
  18. package/app/out/renderer/assets/{ganttDiagram-a2739b55-CP8oCEhK.js → ganttDiagram-a2739b55-BKt7y8Nk.js} +3 -3
  19. package/app/out/renderer/assets/{gitGraphDiagram-82fe8481-BwBuEj3Z.js → gitGraphDiagram-82fe8481-MVrgAPy8.js} +2 -2
  20. package/app/out/renderer/assets/{graph-Cm7VMO24.js → graph-47tN-JjA.js} +1 -1
  21. package/app/out/renderer/assets/{index-5325376f-rxU_OBcM.js → index-5325376f-BAwly5ON.js} +6 -6
  22. package/app/out/renderer/assets/{index-YHTq32NV.js → index-B7PIn7Sy.js} +3 -3
  23. package/app/out/renderer/assets/{index-BOSzW1Fp.js → index-BCAfDfd2.js} +3 -3
  24. package/app/out/renderer/assets/{index-D075LvBi.js → index-BtnFDu4c.js} +3 -3
  25. package/app/out/renderer/assets/{index-CmuzUeEb.js → index-C9k23NfW.js} +3 -3
  26. package/app/out/renderer/assets/{index-DwyuGyq2.js → index-CI7jql7H.js} +3 -3
  27. package/app/out/renderer/assets/{index-B86Rm5VH.js → index-CTXUhrja.js} +5 -5
  28. package/app/out/renderer/assets/{index-CXW3LNrw.js → index-CZhJgR4h.js} +6 -6
  29. package/app/out/renderer/assets/{index-C90yL_Oq.js → index-CaKrYTYv.js} +116 -72
  30. package/app/out/renderer/assets/{index-CnSaLake.js → index-Cdfq9GLa.js} +6 -6
  31. package/app/out/renderer/assets/{index-PcwO-UQr.js → index-ChRW2wyg.js} +4 -4
  32. package/app/out/renderer/assets/{index-qYYoWrK0.js → index-D38yt-gB.js} +3 -3
  33. package/app/out/renderer/assets/{index-BireTC8B.js → index-DF3gD3Ct.js} +6 -6
  34. package/app/out/renderer/assets/{index-JxH0rooB.js → index-DP9hc671.js} +6 -6
  35. package/app/out/renderer/assets/{index-DnIy5Txo.js → index-DcHrpeaZ.js} +3 -3
  36. package/app/out/renderer/assets/{index-B0bfGE4l.js → index-DnGwgCJt.js} +3 -3
  37. package/app/out/renderer/assets/{index-CZmZWnTr.js → index-DqAuRK1m.js} +3 -3
  38. package/app/out/renderer/assets/{index-UvRVROtt.js → index-DuccscxT.js} +3 -3
  39. package/app/out/renderer/assets/{index-BurA4U4Q.js → index-DyfldTxE.js} +4 -4
  40. package/app/out/renderer/assets/{index-Cnl3rlb4.js → index-IxItY9MF.js} +3 -3
  41. package/app/out/renderer/assets/{index-21ZazB27.js → index-LPLJgcfa.js} +3 -3
  42. package/app/out/renderer/assets/{index-CYcmNTWS.js → index-PvzYpUGb.js} +1 -1
  43. package/app/out/renderer/assets/{index-CdJBtB9B.js → index-Z0DiYIAm.js} +6 -6
  44. package/app/out/renderer/assets/{index-DQ1LKaqr.js → index-aAuuSY42.js} +6 -6
  45. package/app/out/renderer/assets/{infoDiagram-8eee0895-DX3y3-A9.js → infoDiagram-8eee0895-B7zSsSA5.js} +2 -2
  46. package/app/out/renderer/assets/{journeyDiagram-c64418c1-CWFt3XGT.js → journeyDiagram-c64418c1-D6rYrYE9.js} +4 -4
  47. package/app/out/renderer/assets/{layout-BlEAfCmy.js → layout-C3nD7S8q.js} +2 -2
  48. package/app/out/renderer/assets/{line-C8wQDOie.js → line-BD7gUCLm.js} +1 -1
  49. package/app/out/renderer/assets/{linear-B-vozVTM.js → linear-Dml1F21i.js} +1 -1
  50. package/app/out/renderer/assets/{mindmap-definition-8da855dc-BCWxRP64.js → mindmap-definition-8da855dc-IY0DZul4.js} +3 -3
  51. package/app/out/renderer/assets/{pieDiagram-a8764435-CjTt_TYd.js → pieDiagram-a8764435-CrWonzc_.js} +3 -3
  52. package/app/out/renderer/assets/{quadrantDiagram-1e28029f-D1Ke9L8a.js → quadrantDiagram-1e28029f-Bca0GRbN.js} +3 -3
  53. package/app/out/renderer/assets/{requirementDiagram-08caed73-qZ5fBq-x.js → requirementDiagram-08caed73-Dz2SRyGA.js} +5 -5
  54. package/app/out/renderer/assets/{sankeyDiagram-a04cb91d-B-KtJYmc.js → sankeyDiagram-a04cb91d-NfJN0Agl.js} +2 -2
  55. package/app/out/renderer/assets/{sequenceDiagram-c5b8d532-BjqWnQP_.js → sequenceDiagram-c5b8d532-D2Dx4CJP.js} +3 -3
  56. package/app/out/renderer/assets/{stateDiagram-1ecb1508-DQo_1-ne.js → stateDiagram-1ecb1508-CClex8bq.js} +6 -6
  57. package/app/out/renderer/assets/{stateDiagram-v2-c2b004d7-BNb4d-sS.js → stateDiagram-v2-c2b004d7-vkISAVpU.js} +10 -10
  58. package/app/out/renderer/assets/{styles-b4e223ce-pjL5kdCz.js → styles-b4e223ce-q4XIFTqx.js} +1 -1
  59. package/app/out/renderer/assets/{styles-ca3715f6-CtyHB9Sz.js → styles-ca3715f6-DEA1MmgW.js} +1 -1
  60. package/app/out/renderer/assets/{styles-d45a18b0-r6zLUSmM.js → styles-d45a18b0-JdnahWSX.js} +4 -4
  61. package/app/out/renderer/assets/{svgDrawCommon-b86b1483-CbHYm_eu.js → svgDrawCommon-b86b1483-nssZCQ5K.js} +1 -1
  62. package/app/out/renderer/assets/{timeline-definition-faaaa080-DvoW_Frb.js → timeline-definition-faaaa080-ChRZSyE8.js} +3 -3
  63. package/app/out/renderer/assets/{xychartDiagram-f5964ef8-C-G_0ZnR.js → xychartDiagram-f5964ef8-BrgVCZqF.js} +5 -5
  64. package/app/out/renderer/index.html +1 -1
  65. package/package.json +1 -1
@@ -3,7 +3,7 @@ import { setMaxListeners } from "node:events";
3
3
  import fs, { existsSync as existsSync$1 } from "node:fs";
4
4
  import { execFile, spawn, execSync } from "node:child_process";
5
5
  import path, { resolve, join, sep, isAbsolute, extname, dirname, basename, relative } from "path";
6
- import { existsSync, statSync, readdirSync as readdirSync$1, readFileSync, writeFileSync, mkdirSync, appendFileSync, rmSync, unlinkSync, renameSync, openSync, constants, writeSync, closeSync } from "fs";
6
+ import { existsSync, statSync, readdirSync as readdirSync$1, readFileSync, writeFileSync, mkdirSync, appendFileSync, rmSync, unlinkSync, renameSync, openSync, constants, writeSync, closeSync, watch } from "fs";
7
7
  import os$1, { homedir } from "os";
8
8
  import { createHash, randomUUID } from "crypto";
9
9
  import { Agent } from "@mariozechner/pi-agent-core";
@@ -633,6 +633,43 @@ function registerUsageHandlers(handle, getCtx, loadUsageTotals2, resetUsageTotal
633
633
  return resetUsageTotals2(baseDir);
634
634
  });
635
635
  }
636
+ const LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
637
+ const inFlightLogin = /* @__PURE__ */ new Map();
638
+ function loginErrorMessage(raw, label) {
639
+ const msg = (raw instanceof Error ? raw.message : String(raw ?? "")).trim();
640
+ return msg || `${label} OAuth login failed`;
641
+ }
642
+ async function runLoginWithTimeout(key, label, runLogin, onSuccess) {
643
+ if (inFlightLogin.has(key)) {
644
+ return { success: false, error: `${label} sign-in is already in progress. Cancel it first.` };
645
+ }
646
+ let rejectManual = () => {
647
+ };
648
+ const manualPromise = new Promise((_, reject) => {
649
+ rejectManual = reject;
650
+ });
651
+ manualPromise.catch(() => {
652
+ });
653
+ const attempt = {
654
+ abort: (reason) => rejectManual(new Error(reason))
655
+ };
656
+ inFlightLogin.set(key, attempt);
657
+ const timer = setTimeout(() => {
658
+ attempt.abort(
659
+ `${label} sign-in timed out after ${Math.round(LOGIN_TIMEOUT_MS / 6e4)} minutes. Please try again.`
660
+ );
661
+ }, LOGIN_TIMEOUT_MS);
662
+ try {
663
+ const creds = await runLogin(() => manualPromise);
664
+ onSuccess(creds);
665
+ return { success: true };
666
+ } catch (err) {
667
+ return { success: false, error: loginErrorMessage(err, label) };
668
+ } finally {
669
+ clearTimeout(timer);
670
+ if (inFlightLogin.get(key) === attempt) inFlightLogin.delete(key);
671
+ }
672
+ }
636
673
  function registerAuthHandlers(handleRaw) {
637
674
  handleRaw("auth:get-anthropic-status", () => {
638
675
  const hasApiKey = !!(process.env.ANTHROPIC_API_KEY || "").trim();
@@ -659,8 +696,10 @@ function registerAuthHandlers(handleRaw) {
659
696
  handleRaw("auth:anthropic-sub-login", async () => {
660
697
  const { loginAnthropic } = await import("@mariozechner/pi-ai/oauth");
661
698
  const { shell: shell2 } = await import("electron");
662
- try {
663
- const creds = await loginAnthropic({
699
+ return runLoginWithTimeout(
700
+ "anthropic-sub",
701
+ "Anthropic",
702
+ (onManualCodeInput) => loginAnthropic({
664
703
  onAuth: (info) => {
665
704
  shell2.openExternal(info.url);
666
705
  },
@@ -670,13 +709,17 @@ function registerAuthHandlers(handleRaw) {
670
709
  },
671
710
  onProgress: (msg) => {
672
711
  console.log("[OAuth Anthropic]", msg);
673
- }
674
- });
675
- saveAnthropicSubCredentials(creds);
676
- return { success: true };
677
- } catch (err) {
678
- return { success: false, error: err.message || "Anthropic OAuth login failed" };
679
- }
712
+ },
713
+ onManualCodeInput
714
+ }),
715
+ (creds) => saveAnthropicSubCredentials(creds)
716
+ );
717
+ });
718
+ handleRaw("auth:anthropic-sub-cancel", () => {
719
+ const attempt = inFlightLogin.get("anthropic-sub");
720
+ if (!attempt) return { success: false, error: "No Anthropic sign-in in progress" };
721
+ attempt.abort("Anthropic sign-in cancelled.");
722
+ return { success: true };
680
723
  });
681
724
  handleRaw("auth:anthropic-sub-logout", () => {
682
725
  clearAnthropicSubCredentials();
@@ -704,8 +747,10 @@ function registerAuthHandlers(handleRaw) {
704
747
  handleRaw("auth:openai-codex-login", async () => {
705
748
  const { loginOpenAICodex } = await import("@mariozechner/pi-ai/oauth");
706
749
  const { shell: shell2 } = await import("electron");
707
- try {
708
- const creds = await loginOpenAICodex({
750
+ return runLoginWithTimeout(
751
+ "openai-codex",
752
+ "ChatGPT",
753
+ (onManualCodeInput) => loginOpenAICodex({
709
754
  onAuth: (info) => {
710
755
  shell2.openExternal(info.url);
711
756
  },
@@ -715,13 +760,17 @@ function registerAuthHandlers(handleRaw) {
715
760
  },
716
761
  onProgress: (msg) => {
717
762
  console.log("[OAuth]", msg);
718
- }
719
- });
720
- saveCodexCredentials(creds);
721
- return { success: true };
722
- } catch (err) {
723
- return { success: false, error: err.message || "OAuth login failed" };
724
- }
763
+ },
764
+ onManualCodeInput
765
+ }),
766
+ (creds) => saveCodexCredentials(creds)
767
+ );
768
+ });
769
+ handleRaw("auth:openai-codex-cancel", () => {
770
+ const attempt = inFlightLogin.get("openai-codex");
771
+ if (!attempt) return { success: false, error: "No ChatGPT sign-in in progress" };
772
+ attempt.abort("ChatGPT sign-in cancelled.");
773
+ return { success: true };
725
774
  });
726
775
  handleRaw("auth:openai-codex-logout", () => {
727
776
  clearCodexCredentials();
@@ -1255,6 +1304,32 @@ function readLatestSessionSummary(projectPath, sessionId) {
1255
1304
  });
1256
1305
  return readJson(join(dir, files[0]), null);
1257
1306
  }
1307
+ function readOrphanMessages(projectPath, sessionId, cutoffMs) {
1308
+ const file = join(projectPath, PATHS.sessions, `${sessionId}.jsonl`);
1309
+ if (!existsSync(file)) return [];
1310
+ const result = [];
1311
+ let raw;
1312
+ try {
1313
+ raw = readFileSync(file, "utf-8");
1314
+ } catch {
1315
+ return [];
1316
+ }
1317
+ for (const line of raw.split("\n")) {
1318
+ if (!line) continue;
1319
+ let msg;
1320
+ try {
1321
+ msg = JSON.parse(line);
1322
+ } catch {
1323
+ continue;
1324
+ }
1325
+ if (!msg || typeof msg !== "object") continue;
1326
+ if (msg.role !== "user" && msg.role !== "assistant") continue;
1327
+ if (typeof msg.timestamp !== "number" || msg.timestamp <= cutoffMs) continue;
1328
+ if (typeof msg.content !== "string" || msg.content.length === 0) continue;
1329
+ result.push({ role: msg.role, content: msg.content, timestamp: msg.timestamp });
1330
+ }
1331
+ return result;
1332
+ }
1258
1333
  function generateCiteKey$1(authors, year, title) {
1259
1334
  const firstAuthor = authors[0] || "unknown";
1260
1335
  const lastName = firstAuthor.split(/\s+/).pop()?.toLowerCase() || "unknown";
@@ -2619,29 +2694,30 @@ If conversation history contains previous literature-search results with coverag
2619
2694
  ## Rules
2620
2695
 
2621
2696
  1. You MUST provide a \`relevanceJustification\` for EVERY paper explaining WHY it received that score
2622
- 2. After scoring all papers, perform a FORCED RANKING: cut the bottom 30% — papers in the bottom 30% get excluded from relevantPapers even if their score is above threshold
2697
+ 2. After scoring all papers, perform a FORCED RANKING: cut the bottom 30% — papers in the bottom 30% get excluded from scoredPapers even if their score is above threshold
2623
2698
  3. Auto-save threshold is **>= 7**. Papers scoring 7+ are saved to the local library. Be decisive: if a paper is meaningfully relevant (not just tangential), score it >= 7.
2624
2699
  4. Approve only if at least 3 papers score >= 7 AND coverage >= 0.5. If confidence is low or critical coverage is missing, request targeted refinement.
2625
2700
  5. If not approved, suggest at most 2-3 **targeted refinement queries** for specific missing sub-topics — NOT broad re-searches. These queries run through the FULL search pipeline again, so be selective. CRITICAL: Your refinement queries MUST be DIFFERENT from the "Queries used" listed at the bottom — the system will reject duplicate queries. Use different terminology, synonyms, or narrower/broader scope to find what the original queries missed
2626
2701
  6. Track cumulative coverage across sub-topics
2627
- 7. Output size guard: include AT MOST 25 relevantPapers. If there are any reasonably relevant papers, include at least 3 (do NOT return an empty list unless ZERO papers are even tangentially relevant).
2702
+ 7. Output size guard: include AT MOST 25 scoredPapers. If there are any reasonably relevant papers, include at least 3 (do NOT return an empty list unless ZERO papers are even tangentially relevant).
2703
+
2704
+ ## CRITICAL: Compact output contract
2628
2705
 
2629
- ## Paper metadata preservation
2706
+ The caller already holds the full paper metadata (title, authors, abstract, venue, doi, year, url, citationCount, source). You MUST NOT echo any of it back. Your ONLY job is to return scoring decisions keyed by the paper's **index number** — the 1-based number shown at the start of each paper line in the input (e.g. \`1. [semantic_scholar] "Foo..."\` → \`index: 1\`).
2630
2707
 
2631
- IMPORTANT: Preserve ALL paper metadata in relevantPapers. Every paper MUST include ALL fields copy exactly from input, using null for missing values:
2632
- - id, title, authors (full array), abstract (full text if possible; may truncate if very long), year, url
2633
- - source (e.g. "semantic_scholar", "arxiv", "openalex", "dblp", "local")
2634
- - relevanceScore (your 0-10 rating), relevanceJustification (1-2 sentence explanation)
2635
- - doi (string or null), venue (string or null), citationCount (number or null)
2708
+ - Output ONLY integers for \`index\`, integers 0-10 for \`relevanceScore\`, and a short 1-2 sentence \`relevanceJustification\`.
2709
+ - Do NOT include title, authors, abstract, venue, doi, year, url, source, or citationCount fields in scoredPapers. Those will be merged by the caller.
2710
+ - Every \`index\` MUST refer to a paper actually shown in the input. Do not invent indices.
2711
+ - Do not wrap the JSON in markdown fences unless strictly necessary; raw JSON is preferred.
2636
2712
 
2637
- If the full abstract is very long, you may truncate it to ~800 characters, but preserve the core meaning. Do NOT omit authors. Do NOT drop any field.
2713
+ Keeping the output compact is REQUIRED echoing metadata caused past responses to be truncated, making the entire review unparseable.
2638
2714
 
2639
2715
  ## Output JSON
2640
2716
 
2641
2717
  {
2642
2718
  "approved": boolean,
2643
- "relevantPapers": [
2644
- { "id": "...", "title": "...", "authors": [...], "abstract": "full text...", "year": number, "url": "...", "source": "...", "relevanceScore": number, "relevanceJustification": "why this score", "doi": "..." or null, "venue": "..." or null, "citationCount": number or null }
2719
+ "scoredPapers": [
2720
+ { "index": 1, "relevanceScore": 9, "relevanceJustification": "1-2 sentence explanation" }
2645
2721
  ],
2646
2722
  "confidence": number,
2647
2723
  "coverage": {
@@ -3427,20 +3503,46 @@ Additional context: ${extraContext}` : `Research request: ${query}`;
3427
3503
  try {
3428
3504
  const reviewText = await ctx.callLlm(REVIEWER_SYSTEM, reviewInput);
3429
3505
  const parsed = safeJsonParse(reviewText);
3430
- if (!parsed) {
3506
+ if (!parsed || !Array.isArray(parsed.scoredPapers)) {
3507
+ const fallbackCap = ctx.settings?.researchIntensity?.reviewCap ?? 25;
3431
3508
  review = {
3432
3509
  approved: true,
3433
- relevantPapers: deduplicated.slice(0, ctx.settings?.researchIntensity?.reviewCap ?? 25).map((p) => ({ ...p, relevanceScore: 5, relevanceJustification: "Review parsing failed; included by default." })),
3434
- confidence: 0.5,
3435
- coverage: { score: 0.5, coveredTopics: [], missingTopics: [], gaps: ["Review parsing failed"] },
3510
+ relevantPapers: deduplicated.slice(0, fallbackCap).map((p) => ({
3511
+ ...p,
3512
+ relevanceScore: 7,
3513
+ relevanceJustification: "Review parsing failed; included with default score 7 so the paper is still saved. Re-review manually."
3514
+ })),
3515
+ confidence: 0.3,
3516
+ coverage: { score: 0.3, coveredTopics: [], missingTopics: [], gaps: ["Review parsing failed"] },
3436
3517
  issues: ["Review parsing failed"],
3437
3518
  additionalQueries: null
3438
3519
  };
3439
3520
  pipelineWarnings.push(
3440
- "LLM review parsing failed — papers included with default relevance score of 5. Relevance scores may not be accurate. Consider re-reviewing the top papers manually."
3521
+ "LLM review parsing failed — papers saved with default relevance score of 7. Relevance scores are NOT accurate. Re-review the saved papers manually."
3441
3522
  );
3442
3523
  } else {
3443
- review = parsed;
3524
+ const seenIndices = /* @__PURE__ */ new Set();
3525
+ const merged = [];
3526
+ for (const s of parsed.scoredPapers) {
3527
+ const idx = (Number(s?.index) | 0) - 1;
3528
+ if (idx < 0 || idx >= deduplicated.length) continue;
3529
+ if (seenIndices.has(idx)) continue;
3530
+ seenIndices.add(idx);
3531
+ const score = Number(s?.relevanceScore);
3532
+ merged.push({
3533
+ ...deduplicated[idx],
3534
+ relevanceScore: Number.isFinite(score) ? score : 0,
3535
+ relevanceJustification: typeof s?.relevanceJustification === "string" ? s.relevanceJustification : ""
3536
+ });
3537
+ }
3538
+ review = {
3539
+ approved: parsed.approved ?? false,
3540
+ relevantPapers: merged,
3541
+ confidence: typeof parsed.confidence === "number" ? parsed.confidence : 0.5,
3542
+ coverage: parsed.coverage ?? { score: 0.5, coveredTopics: [], missingTopics: [], gaps: [] },
3543
+ issues: Array.isArray(parsed.issues) ? parsed.issues : [],
3544
+ additionalQueries: parsed.additionalQueries ?? null
3545
+ };
3444
3546
  }
3445
3547
  } catch (err) {
3446
3548
  return toAgentResult("literature-search", toolError("EXECUTION_FAILED", `Review step failed: ${err.message}`, {
@@ -6511,6 +6613,7 @@ function getWikiRoot() {
6511
6613
  return join(homedir(), ".research-pilot", "paper-wiki");
6512
6614
  }
6513
6615
  const GENERATOR_VERSION = 3;
6616
+ const HASH_SCHEMA_VERSION = 2;
6514
6617
  function isValidArxivId(arxivId) {
6515
6618
  const bare = arxivId.replace(/^https?:\/\/arxiv\.org\/abs\//, "").replace(/v\d+$/, "");
6516
6619
  if (/^\d{4}\.\d{4,}$/.test(bare)) return true;
@@ -6529,6 +6632,18 @@ function computeCanonicalKey(artifact) {
6529
6632
  return { canonicalKey: `title:${title}:${artifact.year ?? "nd"}`, keySource: "title+year" };
6530
6633
  }
6531
6634
  function computeSemanticHash(artifact) {
6635
+ const projection = {
6636
+ title: artifact.title,
6637
+ authors: artifact.authors,
6638
+ abstract: artifact.abstract,
6639
+ year: artifact.year,
6640
+ venue: artifact.venue,
6641
+ doi: artifact.doi,
6642
+ arxivId: artifact.arxivId
6643
+ };
6644
+ return createHash("sha256").update(JSON.stringify(projection)).digest("hex").slice(0, 16);
6645
+ }
6646
+ function computeSemanticHashV1(artifact) {
6532
6647
  const projection = {
6533
6648
  title: artifact.title,
6534
6649
  authors: artifact.authors,
@@ -6544,6 +6659,10 @@ function computeSemanticHash(artifact) {
6544
6659
  };
6545
6660
  return createHash("sha256").update(JSON.stringify(projection)).digest("hex").slice(0, 16);
6546
6661
  }
6662
+ function canSilentRestampLegacyWatermark(priorWatermark, artifact) {
6663
+ if ((priorWatermark.hashSchemaVersion ?? 1) >= HASH_SCHEMA_VERSION) return false;
6664
+ return priorWatermark.semanticHash === computeSemanticHashV1(artifact);
6665
+ }
6547
6666
  function canonicalKeyToSlug(canonicalKey) {
6548
6667
  return canonicalKey.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120);
6549
6668
  }
@@ -6629,6 +6748,25 @@ function markPaperProcessed(entry) {
6629
6748
  const content = Array.from(existing.values()).map((e) => JSON.stringify(e)).join("\n") + "\n";
6630
6749
  safeWriteFile(path2, content);
6631
6750
  }
6751
+ function restampProcessedBatch(updates) {
6752
+ if (updates.length === 0) return;
6753
+ const existing = readProcessedWatermark();
6754
+ let changed = 0;
6755
+ for (const upd of updates) {
6756
+ const entry = existing.get(upd.canonicalKey);
6757
+ if (!entry) continue;
6758
+ existing.set(upd.canonicalKey, {
6759
+ ...entry,
6760
+ semanticHash: upd.semanticHash,
6761
+ hashSchemaVersion: upd.hashSchemaVersion
6762
+ });
6763
+ changed++;
6764
+ }
6765
+ if (changed === 0) return;
6766
+ const path2 = processedPath();
6767
+ const content = Array.from(existing.values()).map((e) => JSON.stringify(e)).join("\n") + "\n";
6768
+ safeWriteFile(path2, content);
6769
+ }
6632
6770
  function markFulltextFailure(canonicalKey) {
6633
6771
  const existing = readProcessedWatermark();
6634
6772
  const entry = existing.get(canonicalKey);
@@ -7042,6 +7180,17 @@ function parsePaperPage(content, slug) {
7042
7180
  repairUsed
7043
7181
  };
7044
7182
  }
7183
+ function synthesizeMinimalSidecar(canonicalKey, slug, sourceTier, generatorVersion) {
7184
+ return {
7185
+ schemaVersion: 3,
7186
+ canonicalKey,
7187
+ slug,
7188
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
7189
+ generator_version: generatorVersion,
7190
+ source_tier: sourceTier,
7191
+ paper_type: "empirical"
7192
+ };
7193
+ }
7045
7194
  function serializeMetaBlock(sidecar) {
7046
7195
  const json = JSON.stringify(sidecar, null, 2);
7047
7196
  return `${WIKI_META_OPEN}
@@ -8731,6 +8880,14 @@ function buildSessionSummaryContext(summary) {
8731
8880
  ];
8732
8881
  return lines.join("\n");
8733
8882
  }
8883
+ function buildRecentConversationContext(messages) {
8884
+ const lines = ["## Recent Conversation (resumed from prior session)", ""];
8885
+ for (const msg of messages) {
8886
+ const speaker = msg.role === "user" ? "User" : "Assistant";
8887
+ lines.push(`**${speaker}:** ${msg.content}`, "");
8888
+ }
8889
+ return lines.join("\n").trimEnd();
8890
+ }
8734
8891
  function writeExplainSnapshot(projectPath, snapshot) {
8735
8892
  const explainDir = join(projectPath, PATHS.explainDir);
8736
8893
  mkdirSync(explainDir, { recursive: true });
@@ -8875,12 +9032,13 @@ async function createCoordinator(config) {
8875
9032
  const skillsCatalog = buildSkillsCatalogPrompt(skills);
8876
9033
  const baseSystemPrompt = SYSTEM_PROMPT + (skillsCatalog ? "\n\n" + skillsCatalog : "");
8877
9034
  let compactionSummary;
9035
+ let bootstrapDone = false;
8878
9036
  const agent = new Agent({
8879
9037
  initialState: {
8880
9038
  systemPrompt: baseSystemPrompt,
8881
9039
  model: piModel ?? void 0,
8882
9040
  tools: allTools,
8883
- thinkingLevel: reasoningEffort === "high" ? "high" : reasoningEffort === "medium" ? "medium" : "low"
9041
+ thinkingLevel: reasoningEffort === "max" ? "xhigh" : reasoningEffort === "high" ? "high" : reasoningEffort === "medium" ? "medium" : "low"
8884
9042
  },
8885
9043
  sessionId,
8886
9044
  getApiKey: resolveApiKey,
@@ -9078,6 +9236,22 @@ ${historyText}`,
9078
9236
  const mentionContext = buildMentionContext(mentions);
9079
9237
  const latestSummary = readLatestSessionSummary(projectPath, sessionId);
9080
9238
  const summaryContext = latestSummary ? buildSessionSummaryContext(latestSummary) : "";
9239
+ let bootstrapContext = "";
9240
+ if (!bootstrapDone) {
9241
+ bootstrapDone = true;
9242
+ try {
9243
+ const cutoffMs = latestSummary ? Date.parse(latestSummary.createdAt) || 0 : 0;
9244
+ const orphans = readOrphanMessages(projectPath, sessionId, cutoffMs);
9245
+ if (orphans.length > 0) {
9246
+ bootstrapContext = buildRecentConversationContext(orphans);
9247
+ if (debug) {
9248
+ console.log(`[Bootstrap] Recovered ${orphans.length} orphan message(s) from prior session`);
9249
+ }
9250
+ }
9251
+ } catch (err) {
9252
+ if (debug) console.warn("[Bootstrap] Failed to read orphan messages:", err);
9253
+ }
9254
+ }
9081
9255
  const persistence = classifyPersistenceDecision(message);
9082
9256
  const explain = {
9083
9257
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -9119,6 +9293,7 @@ ${agentMdContent}`;
9119
9293
  agent.state.systemPrompt = enrichedSystem;
9120
9294
  const contextParts = [];
9121
9295
  if (summaryContext) contextParts.push(summaryContext);
9296
+ if (bootstrapContext) contextParts.push(bootstrapContext);
9122
9297
  if (skillSummariesPrompt) contextParts.push(skillSummariesPrompt);
9123
9298
  if (mentionContext) contextParts.push(mentionContext);
9124
9299
  let userMessage = contextParts.length > 0 ? `${contextParts.join("\n\n")}
@@ -9162,6 +9337,23 @@ ${message}` : message;
9162
9337
  }
9163
9338
  }
9164
9339
  writeExplainSnapshot(projectPath, explain);
9340
+ const stopReason = lastMsg && "stopReason" in lastMsg ? lastMsg.stopReason : void 0;
9341
+ const errorMessage = lastMsg && "errorMessage" in lastMsg ? lastMsg.errorMessage : void 0;
9342
+ if (stopReason === "aborted") {
9343
+ return { success: false, error: "Generation stopped by user." };
9344
+ }
9345
+ if (stopReason === "error") {
9346
+ return {
9347
+ success: false,
9348
+ error: errorMessage || "The LLM request failed. This usually indicates an issue on the model server or with authentication. If you are using a Claude or ChatGPT subscription, try signing out and back in via Settings. Otherwise verify your API key, model selection, and network connection."
9349
+ };
9350
+ }
9351
+ if (!responseText && responseImages.length === 0) {
9352
+ return {
9353
+ success: false,
9354
+ error: `The model returned an empty response (stopReason=${stopReason ?? "unknown"}). This usually indicates a transient issue with the LLM server. Please try again. If the problem persists and you are using a Claude or ChatGPT subscription, try signing out and back in via Settings.`
9355
+ };
9356
+ }
9165
9357
  turnCount++;
9166
9358
  turnHistory.push({
9167
9359
  userMessage: message.slice(0, 300),
@@ -11131,6 +11323,7 @@ function scanForNewContent(projectPaths) {
11131
11323
  const processed = readProcessedWatermark();
11132
11324
  const toProcess = [];
11133
11325
  const provenanceOnly = [];
11326
+ const restamps = [];
11134
11327
  const existingProvenance = readProvenance();
11135
11328
  const provenanceIndex = /* @__PURE__ */ new Map();
11136
11329
  for (const entry of existingProvenance) {
@@ -11189,6 +11382,20 @@ function scanForNewContent(projectPaths) {
11189
11382
  provenanceIndex.delete(fallbackKey);
11190
11383
  }
11191
11384
  }
11385
+ const priorWatermark = processed.get(canonicalKey);
11386
+ if (priorWatermark && canSilentRestampLegacyWatermark(priorWatermark, artifact)) {
11387
+ const restamped = {
11388
+ ...priorWatermark,
11389
+ semanticHash,
11390
+ hashSchemaVersion: HASH_SCHEMA_VERSION
11391
+ };
11392
+ processed.set(canonicalKey, restamped);
11393
+ restamps.push({
11394
+ canonicalKey,
11395
+ semanticHash,
11396
+ hashSchemaVersion: HASH_SCHEMA_VERSION
11397
+ });
11398
+ }
11192
11399
  const watermark = processed.get(canonicalKey);
11193
11400
  const knownProvenance = provenanceIndex.get(canonicalKey) || /* @__PURE__ */ new Set();
11194
11401
  const siblings = entries.filter((e) => e.artifact.id !== artifact.id || e.projectPath !== projectPath).map((e) => ({ projectPath: e.projectPath, artifact: e.artifact }));
@@ -11227,6 +11434,9 @@ function scanForNewContent(projectPaths) {
11227
11434
  const bTime = new Date(b.artifact.createdAt).getTime();
11228
11435
  return bTime - aTime;
11229
11436
  });
11437
+ if (restamps.length > 0) {
11438
+ restampProcessedBatch(restamps);
11439
+ }
11230
11440
  return { toProcess, provenanceOnly };
11231
11441
  }
11232
11442
  const ARXIV_RATE_LIMIT_MS = 3e3;
@@ -11347,19 +11557,6 @@ function buildPaperUserContent(artifact, fulltext, existingConceptSlugs) {
11347
11557
  parts.push(`
11348
11558
  Abstract:
11349
11559
  ${artifact.abstract || "(no abstract)"}`);
11350
- if (artifact.keyFindings?.length) {
11351
- parts.push(`
11352
- Key Findings:
11353
- ${artifact.keyFindings.map((f) => `- ${f}`).join("\n")}`);
11354
- }
11355
- if (artifact.relevanceJustification) {
11356
- parts.push(`
11357
- Relevance: ${artifact.relevanceJustification}`);
11358
- }
11359
- if (artifact.subTopic) {
11360
- parts.push(`
11361
- Sub-topic: ${artifact.subTopic}`);
11362
- }
11363
11560
  if (fulltext) {
11364
11561
  const truncated = fulltext.length > 3e4 ? fulltext.slice(0, 3e4) + "\n\n[... truncated for length ...]" : fulltext;
11365
11562
  parts.push(`
@@ -11510,9 +11707,11 @@ function recordSidecarStatus(entry) {
11510
11707
  const content = Array.from(existing.values()).map((e) => JSON.stringify(e)).join("\n") + "\n";
11511
11708
  safeWriteFile(statusFilePath(), content);
11512
11709
  }
11710
+ const MAX_REPAIR_ATTEMPTS = 2;
11513
11711
  function listStaleOrMissing(currentGeneratorVersion) {
11514
11712
  const result = [];
11515
11713
  for (const entry of readSidecarStatus().values()) {
11714
+ if ((entry.repairAttempts ?? 0) >= MAX_REPAIR_ATTEMPTS) continue;
11516
11715
  if (entry.status === "missing" || entry.generator_version < currentGeneratorVersion) {
11517
11716
  result.push(entry);
11518
11717
  }
@@ -11642,6 +11841,7 @@ function createWikiAgent(config) {
11642
11841
  let errors = 0;
11643
11842
  if (!acquireProcessLock()) {
11644
11843
  log("process lock held by another instance, skipping");
11844
+ emitStatus(0);
11645
11845
  return { processed: 0, errors: 0, pendingRemaining: 0 };
11646
11846
  }
11647
11847
  try {
@@ -11650,6 +11850,7 @@ function createWikiAgent(config) {
11650
11850
  const projectPaths = config.projectPaths();
11651
11851
  if (projectPaths.length === 0) {
11652
11852
  log("no project paths, nothing to scan");
11853
+ emitStatus(0);
11653
11854
  return { processed: 0, errors: 0, pendingRemaining: 0 };
11654
11855
  }
11655
11856
  const { toProcess, provenanceOnly } = scanForNewContent(projectPaths);
@@ -11776,7 +11977,16 @@ function createWikiAgent(config) {
11776
11977
  if (!result || !shouldContinue()) return;
11777
11978
  const paperPath = join(getWikiRoot(), "papers", `${slug}.md`);
11778
11979
  safeWriteFile(paperPath, result.content);
11779
- const parseOutcome = parsePaperPage(result.content, slug);
11980
+ let parseOutcome = parsePaperPage(result.content, slug);
11981
+ if (parseOutcome.status === "missing") {
11982
+ const sourceTier = result.fulltextStatus === "fulltext" ? "fulltext" : "abstract-only";
11983
+ const fallback = synthesizeMinimalSidecar(canonicalKey, slug, sourceTier, GENERATOR_VERSION);
11984
+ const patched = writeMetaBlockInto(parseOutcome.body, fallback);
11985
+ safeWriteFile(paperPath, patched);
11986
+ parseOutcome = parsePaperPage(patched, slug);
11987
+ log(`synthesized fallback sidecar for ${slug}`);
11988
+ }
11989
+ const priorStatus = scanResult.reason === "repair" ? readSidecarStatus().get(slug) : void 0;
11780
11990
  recordSidecarStatus({
11781
11991
  slug,
11782
11992
  status: parseOutcome.status,
@@ -11784,7 +11994,8 @@ function createWikiAgent(config) {
11784
11994
  droppedFields: parseOutcome.droppedFields,
11785
11995
  generator_version: GENERATOR_VERSION,
11786
11996
  recorded_at: (/* @__PURE__ */ new Date()).toISOString(),
11787
- repairUsed: parseOutcome.repairUsed
11997
+ repairUsed: parseOutcome.repairUsed,
11998
+ repairAttempts: scanResult.reason === "repair" ? (priorStatus?.repairAttempts ?? 0) + 1 : void 0
11788
11999
  });
11789
12000
  mergeProjectContextIntoPage(slug, projectPath, artifact);
11790
12001
  if (!shouldContinue()) return;
@@ -11813,6 +12024,7 @@ function createWikiAgent(config) {
11813
12024
  semanticHash,
11814
12025
  fulltextStatus: result.fulltextStatus,
11815
12026
  generatorVersion: GENERATOR_VERSION,
12027
+ hashSchemaVersion: HASH_SCHEMA_VERSION,
11816
12028
  processedAt: (/* @__PURE__ */ new Date()).toISOString()
11817
12029
  };
11818
12030
  markPaperProcessed(entry);
@@ -11865,6 +12077,7 @@ function createWikiAgent(config) {
11865
12077
  start() {
11866
12078
  if (state !== "created") return;
11867
12079
  state = "idle";
12080
+ emitStatus(0);
11868
12081
  log(`starting (delay ${config.pacing.startupDelayMs / 1e3}s)`);
11869
12082
  timer = setTimeout(() => tick(), config.pacing.startupDelayMs);
11870
12083
  },
@@ -12325,6 +12538,7 @@ function resetUsageTotals(baseDir) {
12325
12538
  writeAtomically(usagePath(baseDir), JSON.stringify(cleared, null, 2));
12326
12539
  return cleared;
12327
12540
  }
12541
+ let loggedLinuxWatchWarning = false;
12328
12542
  function compareVersions(a, b) {
12329
12543
  const pa = a.split(".").map(Number);
12330
12544
  const pb = b.split(".").map(Number);
@@ -12407,7 +12621,8 @@ function createWindowRuntimeState() {
12407
12621
  projectPath: "",
12408
12622
  sessionId: crypto.randomUUID(),
12409
12623
  isClosing: false,
12410
- realtimeBuffer: createRealtimeBuffer()
12624
+ realtimeBuffer: createRealtimeBuffer(),
12625
+ fsWatcher: null
12411
12626
  };
12412
12627
  }
12413
12628
  function getOrCreateWindowState(win) {
@@ -12432,6 +12647,10 @@ function registerWindow(win) {
12432
12647
  win.on("closed", () => {
12433
12648
  const state = windowStates.get(key);
12434
12649
  if (!state) return;
12650
+ if (state.fsWatcher) {
12651
+ state.fsWatcher.close();
12652
+ state.fsWatcher = null;
12653
+ }
12435
12654
  if (state.coordinator) {
12436
12655
  state.coordinator.destroy().catch(() => {
12437
12656
  });
@@ -13484,6 +13703,43 @@ ${msg.content}
13484
13703
  writeFileSync(result.filePath, markdown, "utf-8");
13485
13704
  return { success: true, path: result.filePath };
13486
13705
  });
13706
+ const IGNORED_SEGMENTS = /* @__PURE__ */ new Set(["node_modules", ".git", ".research-pilot"]);
13707
+ function startFsWatcher(state, win) {
13708
+ if (state.fsWatcher) {
13709
+ state.fsWatcher.close();
13710
+ state.fsWatcher = null;
13711
+ }
13712
+ if (!state.projectPath) return;
13713
+ if (process.platform === "linux" && !loggedLinuxWatchWarning) {
13714
+ loggedLinuxWatchWarning = true;
13715
+ console.warn(
13716
+ "[fs-watcher] Recursive fs.watch is not supported on Linux. Only top-level changes in the workspace will auto-refresh the file tree; changes in subdirectories will require a manual refresh."
13717
+ );
13718
+ }
13719
+ let debounceTimer = null;
13720
+ try {
13721
+ state.fsWatcher = watch(state.projectPath, { recursive: true }, (_event, filename) => {
13722
+ if (filename) {
13723
+ const segments = filename.toString().split(/[/\\]/);
13724
+ if (segments.some((s) => IGNORED_SEGMENTS.has(s))) return;
13725
+ }
13726
+ if (debounceTimer) clearTimeout(debounceTimer);
13727
+ debounceTimer = setTimeout(() => {
13728
+ debounceTimer = null;
13729
+ safeSend(win, "fs:external-change");
13730
+ }, 500);
13731
+ });
13732
+ state.fsWatcher.on("error", () => {
13733
+ if (debounceTimer) {
13734
+ clearTimeout(debounceTimer);
13735
+ debounceTimer = null;
13736
+ }
13737
+ state.fsWatcher?.close();
13738
+ state.fsWatcher = null;
13739
+ });
13740
+ } catch {
13741
+ }
13742
+ }
13487
13743
  async function openProjectFolder(state, win, projectPath) {
13488
13744
  if (!projectPath || !existsSync(projectPath)) return null;
13489
13745
  if (state.coordinator) {
@@ -13496,6 +13752,7 @@ ${msg.content}
13496
13752
  state.realtimeBuffer.reset();
13497
13753
  state.projectPath = projectPath;
13498
13754
  initializeProject(state.projectPath);
13755
+ startFsWatcher(state, win);
13499
13756
  state.sessionId = loadOrCreateSessionId(PATHS.root, state.projectPath);
13500
13757
  const prefsFile = join(state.projectPath, PATHS.root, "preferences.json");
13501
13758
  if (existsSync(prefsFile)) {
@@ -13577,6 +13834,10 @@ ${msg.content}
13577
13834
  handleWindow("project:close", async ({ state }) => {
13578
13835
  state.isClosing = true;
13579
13836
  try {
13837
+ if (state.fsWatcher) {
13838
+ state.fsWatcher.close();
13839
+ state.fsWatcher = null;
13840
+ }
13580
13841
  if (state.coordinator) {
13581
13842
  try {
13582
13843
  state.coordinator.abort();
@@ -27,11 +27,13 @@ const api = {
27
27
  // OpenAI Codex (ChatGPT Subscription) OAuth
28
28
  getOpenAICodexStatus: () => electron.ipcRenderer.invoke("auth:get-openai-codex-status"),
29
29
  openaiCodexLogin: () => electron.ipcRenderer.invoke("auth:openai-codex-login"),
30
+ openaiCodexCancel: () => electron.ipcRenderer.invoke("auth:openai-codex-cancel"),
30
31
  openaiCodexLogout: () => electron.ipcRenderer.invoke("auth:openai-codex-logout"),
31
32
  // Anthropic Subscription (Claude Pro/Max) OAuth — enabled by default
32
33
  isClaudeSubEnabled: () => true,
33
34
  getAnthropicSubStatus: () => electron.ipcRenderer.invoke("auth:get-anthropic-sub-status"),
34
35
  anthropicSubLogin: () => electron.ipcRenderer.invoke("auth:anthropic-sub-login"),
36
+ anthropicSubCancel: () => electron.ipcRenderer.invoke("auth:anthropic-sub-cancel"),
35
37
  anthropicSubLogout: () => electron.ipcRenderer.invoke("auth:anthropic-sub-logout"),
36
38
  pickPreferredModel: () => electron.ipcRenderer.invoke("config:pick-preferred-model"),
37
39
  getApiKeyStatus: () => electron.ipcRenderer.invoke("config:get-api-key-status"),
@@ -105,6 +107,11 @@ const api = {
105
107
  electron.ipcRenderer.on("agent:entity-created", handler);
106
108
  return () => electron.ipcRenderer.removeListener("agent:entity-created", handler);
107
109
  },
110
+ onExternalChange: (cb) => {
111
+ const handler = () => cb();
112
+ electron.ipcRenderer.on("fs:external-change", handler);
113
+ return () => electron.ipcRenderer.removeListener("fs:external-change", handler);
114
+ },
108
115
  onFileCreated: (cb) => {
109
116
  const handler = (_, path) => cb(path);
110
117
  electron.ipcRenderer.on("agent:file-created", handler);