research-copilot 0.2.11 → 0.2.14

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 +239 -49
  2. package/app/out/preload/index.js +2 -0
  3. package/app/out/renderer/assets/{MilkdownMarkdownEditor-BqfydyHs.js → MilkdownMarkdownEditor-BX4GvUB0.js} +50 -50
  4. package/app/out/renderer/assets/{arc-B102x0uC.js → arc-BV5qVZeX.js} +1 -1
  5. package/app/out/renderer/assets/{blockDiagram-c4efeb88-hlEjcPtb.js → blockDiagram-c4efeb88-B4EHe6ol.js} +8 -8
  6. package/app/out/renderer/assets/{c4Diagram-c83219d4-Dc1nfavC.js → c4Diagram-c83219d4-iDfyIJbZ.js} +3 -3
  7. package/app/out/renderer/assets/{channel-ZMdB8bH1.js → channel-Cpwy9r1z.js} +1 -1
  8. package/app/out/renderer/assets/{classDiagram-beda092f-Gbpax7vO.js → classDiagram-beda092f-D053CRJR.js} +6 -6
  9. package/app/out/renderer/assets/{classDiagram-v2-2358418a-_KwHXPjs.js → classDiagram-v2-2358418a-B7s6a7vM.js} +10 -10
  10. package/app/out/renderer/assets/{clone-DDahKPIj.js → clone-pbZD71xt.js} +1 -1
  11. package/app/out/renderer/assets/{createText-1719965b-BNW0X7Oe.js → createText-1719965b-D25dhEwh.js} +2 -2
  12. package/app/out/renderer/assets/{edges-96097737-G9l1oUoZ.js → edges-96097737-BydHfBnP.js} +3 -3
  13. package/app/out/renderer/assets/{erDiagram-0228fc6a-BUs4XtQY.js → erDiagram-0228fc6a-4tnYtIdv.js} +5 -5
  14. package/app/out/renderer/assets/{flowDb-c6c81e3f-CQ-jcycx.js → flowDb-c6c81e3f-CY6JAmVt.js} +1 -1
  15. package/app/out/renderer/assets/{flowDiagram-50d868cf-uu8ab--w.js → flowDiagram-50d868cf-DF_znPBp.js} +12 -12
  16. package/app/out/renderer/assets/{flowDiagram-v2-4f6560a1-CAw9BkoT.js → flowDiagram-v2-4f6560a1-CFOGKf9t.js} +12 -12
  17. package/app/out/renderer/assets/{flowchart-elk-definition-6af322e1-BjQ21EHx.js → flowchart-elk-definition-6af322e1-YLx5Qxt0.js} +6 -6
  18. package/app/out/renderer/assets/{ganttDiagram-a2739b55-CP8oCEhK.js → ganttDiagram-a2739b55-qZme1oAP.js} +3 -3
  19. package/app/out/renderer/assets/{gitGraphDiagram-82fe8481-BwBuEj3Z.js → gitGraphDiagram-82fe8481-DFYMRsVI.js} +2 -2
  20. package/app/out/renderer/assets/{graph-Cm7VMO24.js → graph-BXAfTdpJ.js} +1 -1
  21. package/app/out/renderer/assets/{index-5325376f-rxU_OBcM.js → index-5325376f-BHpHsMhb.js} +6 -6
  22. package/app/out/renderer/assets/{index-CXW3LNrw.js → index-BA2tAI_M.js} +6 -6
  23. package/app/out/renderer/assets/{index-qYYoWrK0.js → index-BE8bYtp7.js} +3 -3
  24. package/app/out/renderer/assets/{index-C90yL_Oq.js → index-BG7PX3k8.js} +114 -72
  25. package/app/out/renderer/assets/{index-21ZazB27.js → index-BO9tGk26.js} +3 -3
  26. package/app/out/renderer/assets/{index-PcwO-UQr.js → index-BgOxdzKV.js} +4 -4
  27. package/app/out/renderer/assets/{index-CnSaLake.js → index-Bj9LQiuY.js} +6 -6
  28. package/app/out/renderer/assets/{index-Cnl3rlb4.js → index-Bx4kt3Ep.js} +3 -3
  29. package/app/out/renderer/assets/{index-JxH0rooB.js → index-C1UejWJF.js} +6 -6
  30. package/app/out/renderer/assets/{index-B0bfGE4l.js → index-CQ3AUQjH.js} +3 -3
  31. package/app/out/renderer/assets/{index-YHTq32NV.js → index-CQ7kmaED.js} +3 -3
  32. package/app/out/renderer/assets/{index-D075LvBi.js → index-CUHnEfWQ.js} +3 -3
  33. package/app/out/renderer/assets/{index-UvRVROtt.js → index-CXKb_B6Q.js} +3 -3
  34. package/app/out/renderer/assets/{index-CdJBtB9B.js → index-CkhHGu8F.js} +6 -6
  35. package/app/out/renderer/assets/{index-B86Rm5VH.js → index-DM53DzJy.js} +5 -5
  36. package/app/out/renderer/assets/{index-DQ1LKaqr.js → index-DPsH0d7l.js} +6 -6
  37. package/app/out/renderer/assets/{index-CZmZWnTr.js → index-DQAavBDm.js} +3 -3
  38. package/app/out/renderer/assets/{index-CmuzUeEb.js → index-DcR5rCVe.js} +3 -3
  39. package/app/out/renderer/assets/{index-BOSzW1Fp.js → index-DdJol6dy.js} +3 -3
  40. package/app/out/renderer/assets/{index-CYcmNTWS.js → index-DmFXl1ev.js} +1 -1
  41. package/app/out/renderer/assets/{index-BurA4U4Q.js → index-Dr-t9DZm.js} +4 -4
  42. package/app/out/renderer/assets/{index-DwyuGyq2.js → index-ZTBCB5Su.js} +3 -3
  43. package/app/out/renderer/assets/{index-BireTC8B.js → index-rmxKZnZp.js} +6 -6
  44. package/app/out/renderer/assets/{index-DnIy5Txo.js → index-uewGaFvs.js} +3 -3
  45. package/app/out/renderer/assets/{infoDiagram-8eee0895-DX3y3-A9.js → infoDiagram-8eee0895-ByEFQmpu.js} +2 -2
  46. package/app/out/renderer/assets/{journeyDiagram-c64418c1-CWFt3XGT.js → journeyDiagram-c64418c1-3-T1DDmD.js} +4 -4
  47. package/app/out/renderer/assets/{layout-BlEAfCmy.js → layout-CQUDsAvc.js} +2 -2
  48. package/app/out/renderer/assets/{line-C8wQDOie.js → line-DxIlbhab.js} +1 -1
  49. package/app/out/renderer/assets/{linear-B-vozVTM.js → linear-Bn3sUUKK.js} +1 -1
  50. package/app/out/renderer/assets/{mindmap-definition-8da855dc-BCWxRP64.js → mindmap-definition-8da855dc-Csu-LGhp.js} +3 -3
  51. package/app/out/renderer/assets/{pieDiagram-a8764435-CjTt_TYd.js → pieDiagram-a8764435-CuLtSzv9.js} +3 -3
  52. package/app/out/renderer/assets/{quadrantDiagram-1e28029f-D1Ke9L8a.js → quadrantDiagram-1e28029f-Bl59fpd9.js} +3 -3
  53. package/app/out/renderer/assets/{requirementDiagram-08caed73-qZ5fBq-x.js → requirementDiagram-08caed73-DC3vhRtq.js} +5 -5
  54. package/app/out/renderer/assets/{sankeyDiagram-a04cb91d-B-KtJYmc.js → sankeyDiagram-a04cb91d-BMU-49qE.js} +2 -2
  55. package/app/out/renderer/assets/{sequenceDiagram-c5b8d532-BjqWnQP_.js → sequenceDiagram-c5b8d532-Bkcjg7DD.js} +3 -3
  56. package/app/out/renderer/assets/{stateDiagram-1ecb1508-DQo_1-ne.js → stateDiagram-1ecb1508-C1C8BcZQ.js} +6 -6
  57. package/app/out/renderer/assets/{stateDiagram-v2-c2b004d7-BNb4d-sS.js → stateDiagram-v2-c2b004d7-DSiaz6V7.js} +10 -10
  58. package/app/out/renderer/assets/{styles-b4e223ce-pjL5kdCz.js → styles-b4e223ce-DR414-dj.js} +1 -1
  59. package/app/out/renderer/assets/{styles-ca3715f6-CtyHB9Sz.js → styles-ca3715f6-CoJirLYO.js} +1 -1
  60. package/app/out/renderer/assets/{styles-d45a18b0-r6zLUSmM.js → styles-d45a18b0-Dbanopw_.js} +4 -4
  61. package/app/out/renderer/assets/{svgDrawCommon-b86b1483-CbHYm_eu.js → svgDrawCommon-b86b1483-RvKK2C6p.js} +1 -1
  62. package/app/out/renderer/assets/{timeline-definition-faaaa080-DvoW_Frb.js → timeline-definition-faaaa080-BG9xYjlZ.js} +3 -3
  63. package/app/out/renderer/assets/{xychartDiagram-f5964ef8-C-G_0ZnR.js → xychartDiagram-f5964ef8-kiztprx8.js} +5 -5
  64. package/app/out/renderer/index.html +1 -1
  65. package/package.json +1 -1
@@ -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).
2628
2703
 
2629
- ## Paper metadata preservation
2704
+ ## CRITICAL: Compact output contract
2630
2705
 
2631
- IMPORTANT: Preserve ALL paper metadata in relevantPapers. Every paper MUST include ALL fieldscopy 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)
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\`).
2636
2707
 
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.
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.
2712
+
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);
@@ -8731,6 +8869,14 @@ function buildSessionSummaryContext(summary) {
8731
8869
  ];
8732
8870
  return lines.join("\n");
8733
8871
  }
8872
+ function buildRecentConversationContext(messages) {
8873
+ const lines = ["## Recent Conversation (resumed from prior session)", ""];
8874
+ for (const msg of messages) {
8875
+ const speaker = msg.role === "user" ? "User" : "Assistant";
8876
+ lines.push(`**${speaker}:** ${msg.content}`, "");
8877
+ }
8878
+ return lines.join("\n").trimEnd();
8879
+ }
8734
8880
  function writeExplainSnapshot(projectPath, snapshot) {
8735
8881
  const explainDir = join(projectPath, PATHS.explainDir);
8736
8882
  mkdirSync(explainDir, { recursive: true });
@@ -8875,12 +9021,13 @@ async function createCoordinator(config) {
8875
9021
  const skillsCatalog = buildSkillsCatalogPrompt(skills);
8876
9022
  const baseSystemPrompt = SYSTEM_PROMPT + (skillsCatalog ? "\n\n" + skillsCatalog : "");
8877
9023
  let compactionSummary;
9024
+ let bootstrapDone = false;
8878
9025
  const agent = new Agent({
8879
9026
  initialState: {
8880
9027
  systemPrompt: baseSystemPrompt,
8881
9028
  model: piModel ?? void 0,
8882
9029
  tools: allTools,
8883
- thinkingLevel: reasoningEffort === "high" ? "high" : reasoningEffort === "medium" ? "medium" : "low"
9030
+ thinkingLevel: reasoningEffort === "max" ? "xhigh" : reasoningEffort === "high" ? "high" : reasoningEffort === "medium" ? "medium" : "low"
8884
9031
  },
8885
9032
  sessionId,
8886
9033
  getApiKey: resolveApiKey,
@@ -9078,6 +9225,22 @@ ${historyText}`,
9078
9225
  const mentionContext = buildMentionContext(mentions);
9079
9226
  const latestSummary = readLatestSessionSummary(projectPath, sessionId);
9080
9227
  const summaryContext = latestSummary ? buildSessionSummaryContext(latestSummary) : "";
9228
+ let bootstrapContext = "";
9229
+ if (!bootstrapDone) {
9230
+ bootstrapDone = true;
9231
+ try {
9232
+ const cutoffMs = latestSummary ? Date.parse(latestSummary.createdAt) || 0 : 0;
9233
+ const orphans = readOrphanMessages(projectPath, sessionId, cutoffMs);
9234
+ if (orphans.length > 0) {
9235
+ bootstrapContext = buildRecentConversationContext(orphans);
9236
+ if (debug) {
9237
+ console.log(`[Bootstrap] Recovered ${orphans.length} orphan message(s) from prior session`);
9238
+ }
9239
+ }
9240
+ } catch (err) {
9241
+ if (debug) console.warn("[Bootstrap] Failed to read orphan messages:", err);
9242
+ }
9243
+ }
9081
9244
  const persistence = classifyPersistenceDecision(message);
9082
9245
  const explain = {
9083
9246
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -9119,6 +9282,7 @@ ${agentMdContent}`;
9119
9282
  agent.state.systemPrompt = enrichedSystem;
9120
9283
  const contextParts = [];
9121
9284
  if (summaryContext) contextParts.push(summaryContext);
9285
+ if (bootstrapContext) contextParts.push(bootstrapContext);
9122
9286
  if (skillSummariesPrompt) contextParts.push(skillSummariesPrompt);
9123
9287
  if (mentionContext) contextParts.push(mentionContext);
9124
9288
  let userMessage = contextParts.length > 0 ? `${contextParts.join("\n\n")}
@@ -9162,6 +9326,23 @@ ${message}` : message;
9162
9326
  }
9163
9327
  }
9164
9328
  writeExplainSnapshot(projectPath, explain);
9329
+ const stopReason = lastMsg && "stopReason" in lastMsg ? lastMsg.stopReason : void 0;
9330
+ const errorMessage = lastMsg && "errorMessage" in lastMsg ? lastMsg.errorMessage : void 0;
9331
+ if (stopReason === "aborted") {
9332
+ return { success: false, error: "Generation stopped by user." };
9333
+ }
9334
+ if (stopReason === "error") {
9335
+ return {
9336
+ success: false,
9337
+ 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."
9338
+ };
9339
+ }
9340
+ if (!responseText && responseImages.length === 0) {
9341
+ return {
9342
+ success: false,
9343
+ 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.`
9344
+ };
9345
+ }
9165
9346
  turnCount++;
9166
9347
  turnHistory.push({
9167
9348
  userMessage: message.slice(0, 300),
@@ -11131,6 +11312,7 @@ function scanForNewContent(projectPaths) {
11131
11312
  const processed = readProcessedWatermark();
11132
11313
  const toProcess = [];
11133
11314
  const provenanceOnly = [];
11315
+ const restamps = [];
11134
11316
  const existingProvenance = readProvenance();
11135
11317
  const provenanceIndex = /* @__PURE__ */ new Map();
11136
11318
  for (const entry of existingProvenance) {
@@ -11189,6 +11371,20 @@ function scanForNewContent(projectPaths) {
11189
11371
  provenanceIndex.delete(fallbackKey);
11190
11372
  }
11191
11373
  }
11374
+ const priorWatermark = processed.get(canonicalKey);
11375
+ if (priorWatermark && canSilentRestampLegacyWatermark(priorWatermark, artifact)) {
11376
+ const restamped = {
11377
+ ...priorWatermark,
11378
+ semanticHash,
11379
+ hashSchemaVersion: HASH_SCHEMA_VERSION
11380
+ };
11381
+ processed.set(canonicalKey, restamped);
11382
+ restamps.push({
11383
+ canonicalKey,
11384
+ semanticHash,
11385
+ hashSchemaVersion: HASH_SCHEMA_VERSION
11386
+ });
11387
+ }
11192
11388
  const watermark = processed.get(canonicalKey);
11193
11389
  const knownProvenance = provenanceIndex.get(canonicalKey) || /* @__PURE__ */ new Set();
11194
11390
  const siblings = entries.filter((e) => e.artifact.id !== artifact.id || e.projectPath !== projectPath).map((e) => ({ projectPath: e.projectPath, artifact: e.artifact }));
@@ -11227,6 +11423,9 @@ function scanForNewContent(projectPaths) {
11227
11423
  const bTime = new Date(b.artifact.createdAt).getTime();
11228
11424
  return bTime - aTime;
11229
11425
  });
11426
+ if (restamps.length > 0) {
11427
+ restampProcessedBatch(restamps);
11428
+ }
11230
11429
  return { toProcess, provenanceOnly };
11231
11430
  }
11232
11431
  const ARXIV_RATE_LIMIT_MS = 3e3;
@@ -11347,19 +11546,6 @@ function buildPaperUserContent(artifact, fulltext, existingConceptSlugs) {
11347
11546
  parts.push(`
11348
11547
  Abstract:
11349
11548
  ${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
11549
  if (fulltext) {
11364
11550
  const truncated = fulltext.length > 3e4 ? fulltext.slice(0, 3e4) + "\n\n[... truncated for length ...]" : fulltext;
11365
11551
  parts.push(`
@@ -11642,6 +11828,7 @@ function createWikiAgent(config) {
11642
11828
  let errors = 0;
11643
11829
  if (!acquireProcessLock()) {
11644
11830
  log("process lock held by another instance, skipping");
11831
+ emitStatus(0);
11645
11832
  return { processed: 0, errors: 0, pendingRemaining: 0 };
11646
11833
  }
11647
11834
  try {
@@ -11650,6 +11837,7 @@ function createWikiAgent(config) {
11650
11837
  const projectPaths = config.projectPaths();
11651
11838
  if (projectPaths.length === 0) {
11652
11839
  log("no project paths, nothing to scan");
11840
+ emitStatus(0);
11653
11841
  return { processed: 0, errors: 0, pendingRemaining: 0 };
11654
11842
  }
11655
11843
  const { toProcess, provenanceOnly } = scanForNewContent(projectPaths);
@@ -11813,6 +12001,7 @@ function createWikiAgent(config) {
11813
12001
  semanticHash,
11814
12002
  fulltextStatus: result.fulltextStatus,
11815
12003
  generatorVersion: GENERATOR_VERSION,
12004
+ hashSchemaVersion: HASH_SCHEMA_VERSION,
11816
12005
  processedAt: (/* @__PURE__ */ new Date()).toISOString()
11817
12006
  };
11818
12007
  markPaperProcessed(entry);
@@ -11865,6 +12054,7 @@ function createWikiAgent(config) {
11865
12054
  start() {
11866
12055
  if (state !== "created") return;
11867
12056
  state = "idle";
12057
+ emitStatus(0);
11868
12058
  log(`starting (delay ${config.pacing.startupDelayMs / 1e3}s)`);
11869
12059
  timer = setTimeout(() => tick(), config.pacing.startupDelayMs);
11870
12060
  },
@@ -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"),