scientify 1.12.0 → 1.12.2

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 (61) hide show
  1. package/README.md +3 -1
  2. package/README.zh.md +3 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2 -5
  5. package/dist/index.js.map +1 -1
  6. package/dist/src/cli/research.d.ts +1 -1
  7. package/dist/src/cli/research.d.ts.map +1 -1
  8. package/dist/src/cli/research.js +123 -227
  9. package/dist/src/cli/research.js.map +1 -1
  10. package/dist/src/commands/metabolism-status.d.ts +2 -2
  11. package/dist/src/commands/metabolism-status.d.ts.map +1 -1
  12. package/dist/src/commands/metabolism-status.js +75 -72
  13. package/dist/src/commands/metabolism-status.js.map +1 -1
  14. package/dist/src/commands.d.ts.map +1 -1
  15. package/dist/src/commands.js +55 -0
  16. package/dist/src/commands.js.map +1 -1
  17. package/dist/src/hooks/research-mode.d.ts +1 -1
  18. package/dist/src/hooks/research-mode.d.ts.map +1 -1
  19. package/dist/src/hooks/research-mode.js +67 -24
  20. package/dist/src/hooks/research-mode.js.map +1 -1
  21. package/dist/src/hooks/scientify-signature.d.ts.map +1 -1
  22. package/dist/src/hooks/scientify-signature.js +5 -2
  23. package/dist/src/hooks/scientify-signature.js.map +1 -1
  24. package/dist/src/knowledge-state/render.d.ts +9 -1
  25. package/dist/src/knowledge-state/render.d.ts.map +1 -1
  26. package/dist/src/knowledge-state/render.js +132 -33
  27. package/dist/src/knowledge-state/render.js.map +1 -1
  28. package/dist/src/knowledge-state/store.d.ts.map +1 -1
  29. package/dist/src/knowledge-state/store.js +545 -38
  30. package/dist/src/knowledge-state/store.js.map +1 -1
  31. package/dist/src/knowledge-state/types.d.ts +31 -0
  32. package/dist/src/knowledge-state/types.d.ts.map +1 -1
  33. package/dist/src/literature/subscription-state.d.ts +2 -0
  34. package/dist/src/literature/subscription-state.d.ts.map +1 -1
  35. package/dist/src/literature/subscription-state.js +586 -7
  36. package/dist/src/literature/subscription-state.js.map +1 -1
  37. package/dist/src/research-subscriptions/constants.d.ts +1 -1
  38. package/dist/src/research-subscriptions/constants.js +1 -1
  39. package/dist/src/research-subscriptions/parse.d.ts.map +1 -1
  40. package/dist/src/research-subscriptions/parse.js +10 -0
  41. package/dist/src/research-subscriptions/parse.js.map +1 -1
  42. package/dist/src/research-subscriptions/prompt.d.ts +1 -1
  43. package/dist/src/research-subscriptions/prompt.d.ts.map +1 -1
  44. package/dist/src/research-subscriptions/prompt.js +144 -195
  45. package/dist/src/research-subscriptions/prompt.js.map +1 -1
  46. package/dist/src/research-subscriptions/types.d.ts +1 -0
  47. package/dist/src/research-subscriptions/types.d.ts.map +1 -1
  48. package/dist/src/templates/bootstrap.d.ts.map +1 -1
  49. package/dist/src/templates/bootstrap.js +19 -32
  50. package/dist/src/templates/bootstrap.js.map +1 -1
  51. package/dist/src/tools/scientify-cron.d.ts +6 -2
  52. package/dist/src/tools/scientify-cron.d.ts.map +1 -1
  53. package/dist/src/tools/scientify-cron.js +396 -12
  54. package/dist/src/tools/scientify-cron.js.map +1 -1
  55. package/dist/src/tools/scientify-literature-state.d.ts +12 -0
  56. package/dist/src/tools/scientify-literature-state.d.ts.map +1 -1
  57. package/dist/src/tools/scientify-literature-state.js +159 -45
  58. package/dist/src/tools/scientify-literature-state.js.map +1 -1
  59. package/openclaw.plugin.json +2 -4
  60. package/package.json +1 -1
  61. package/skills/research-subscription/SKILL.md +23 -0
@@ -2,18 +2,36 @@ import { createHash } from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
3
  import { appendFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { dayKeyFromTimestamp, renderDailyChangesMarkdown, renderExplorationLogMarkdown, renderHypothesisMarkdown, renderIngestLogMarkdown, renderKnowledgeIndexMarkdown, renderPaperNoteHeaderMarkdown, renderPaperNoteRunMarkdown, renderTopicUpdateMarkdown, slugifyTopic, } from "./render.js";
5
+ import { dayKeyFromTimestamp, renderDailyChangesMarkdown, renderExplorationLogMarkdown, renderHypothesisMarkdown, renderIngestLogMarkdown, renderKnowledgeIndexMarkdown, renderPaperNoteHeaderMarkdown, renderPaperNoteRunMarkdown, renderReflectionLogMarkdown, renderTopicUpdateMarkdown, slugifyTopic, } from "./render.js";
6
6
  import { resolveProjectContext } from "./project.js";
7
7
  const STATE_VERSION = 1;
8
8
  const MAX_RECENT_RUN_IDS = 200;
9
9
  const MAX_RECENT_HYPOTHESES = 50;
10
10
  const MAX_RECENT_CHANGE_STATS = 30;
11
11
  const MAX_LAST_TRACE = 20;
12
+ const MAX_LAST_REFLECTION_TASKS = 20;
12
13
  const MAX_RECENT_PAPERS = 50;
13
14
  const MAX_PAPER_NOTES = 800;
15
+ const MAX_HYPOTHESIS_REJECTION_REASONS = 24;
14
16
  const MIN_CORE_FULLTEXT_COVERAGE = 0.8;
15
17
  const MIN_EVIDENCE_BINDING_RATE = 0.9;
16
18
  const MAX_CITATION_ERROR_RATE = 0.02;
19
+ const MIN_FULLTEXT_PROFILE_COMPLETENESS = 0.55;
20
+ const MIN_HYPOTHESIS_EVIDENCE = 2;
21
+ const MIN_HYPOTHESIS_DEPENDENCY_STEPS = 2;
22
+ const MIN_HYPOTHESIS_STATEMENT_CHARS = 48;
23
+ const PLACEHOLDER_TEXT_RE = /^(?:n\/a|na|none|not provided|not available|unknown|tbd|todo|null|nil|未提供|暂无|未知|无)$/iu;
24
+ function defaultRunProfile() {
25
+ return "strict";
26
+ }
27
+ function defaultTriggerState(nowMs = Date.now()) {
28
+ return {
29
+ consecutiveNewReviseDays: 0,
30
+ bridgeCount7d: 0,
31
+ unreadCoreBacklog: 0,
32
+ lastUpdatedAtMs: nowMs,
33
+ };
34
+ }
17
35
  function defaultQualityGateState() {
18
36
  return {
19
37
  passed: false,
@@ -23,9 +41,26 @@ function defaultQualityGateState() {
23
41
  reasons: ["quality gate not evaluated"],
24
42
  };
25
43
  }
44
+ function defaultHypothesisGateState() {
45
+ return {
46
+ accepted: 0,
47
+ rejected: 0,
48
+ rejectionReasons: [],
49
+ };
50
+ }
26
51
  function normalizeText(raw) {
27
52
  return raw.trim().replace(/\s+/g, " ");
28
53
  }
54
+ function cleanOptionalText(raw) {
55
+ if (typeof raw !== "string")
56
+ return undefined;
57
+ const normalized = normalizeText(raw);
58
+ if (!normalized)
59
+ return undefined;
60
+ if (PLACEHOLDER_TEXT_RE.test(normalized))
61
+ return undefined;
62
+ return normalized;
63
+ }
29
64
  function sanitizeId(raw) {
30
65
  return normalizeText(raw)
31
66
  .toLowerCase()
@@ -106,6 +141,9 @@ async function loadState(projectPath) {
106
141
  topic: normalizeText(rawStream.topic ?? "topic"),
107
142
  topicKey,
108
143
  projectId: sanitizeId(rawStream.projectId ?? "auto-topic-global-000000") || "auto-topic-global-000000",
144
+ lastRunProfile: rawStream.lastRunProfile === "strict" || rawStream.lastRunProfile === "fast"
145
+ ? rawStream.lastRunProfile
146
+ : defaultRunProfile(),
109
147
  totalRuns: typeof rawStream.totalRuns === "number" ? Math.max(0, Math.floor(rawStream.totalRuns)) : 0,
110
148
  totalHypotheses: typeof rawStream.totalHypotheses === "number" ? Math.max(0, Math.floor(rawStream.totalHypotheses)) : 0,
111
149
  knowledgeTopics: Array.isArray(rawStream.knowledgeTopics)
@@ -114,6 +152,26 @@ async function loadState(projectPath) {
114
152
  paperNotes: Array.isArray(rawStream.paperNotes)
115
153
  ? rawStream.paperNotes.filter((item) => typeof item === "string").map((item) => normalizeText(item))
116
154
  : [],
155
+ triggerState: rawStream.triggerState && typeof rawStream.triggerState === "object" && !Array.isArray(rawStream.triggerState)
156
+ ? {
157
+ consecutiveNewReviseDays: typeof rawStream.triggerState.consecutiveNewReviseDays === "number" &&
158
+ Number.isFinite(rawStream.triggerState.consecutiveNewReviseDays)
159
+ ? Math.max(0, Math.floor(rawStream.triggerState.consecutiveNewReviseDays))
160
+ : 0,
161
+ bridgeCount7d: typeof rawStream.triggerState.bridgeCount7d === "number" &&
162
+ Number.isFinite(rawStream.triggerState.bridgeCount7d)
163
+ ? Math.max(0, Math.floor(rawStream.triggerState.bridgeCount7d))
164
+ : 0,
165
+ unreadCoreBacklog: typeof rawStream.triggerState.unreadCoreBacklog === "number" &&
166
+ Number.isFinite(rawStream.triggerState.unreadCoreBacklog)
167
+ ? Math.max(0, Math.floor(rawStream.triggerState.unreadCoreBacklog))
168
+ : 0,
169
+ lastUpdatedAtMs: typeof rawStream.triggerState.lastUpdatedAtMs === "number" &&
170
+ Number.isFinite(rawStream.triggerState.lastUpdatedAtMs)
171
+ ? Math.floor(rawStream.triggerState.lastUpdatedAtMs)
172
+ : Date.now(),
173
+ }
174
+ : defaultTriggerState(),
117
175
  recentFullTextReadCount: typeof rawStream.recentFullTextReadCount === "number"
118
176
  ? Math.max(0, Math.floor(rawStream.recentFullTextReadCount))
119
177
  : 0,
@@ -178,6 +236,39 @@ async function loadState(projectPath) {
178
236
  .map(normalizeTrace)
179
237
  .filter((item) => Boolean(item))
180
238
  : [],
239
+ lastReflectionTasks: Array.isArray(rawStream.lastReflectionTasks)
240
+ ? rawStream.lastReflectionTasks
241
+ .filter((item) => !!item && typeof item === "object")
242
+ .map((item) => ({
243
+ id: sanitizeId(item.id ?? "task"),
244
+ trigger: ["BRIDGE", "TREND", "CONTRADICTION", "UNREAD_CORE"].includes(item.trigger)
245
+ ? item.trigger
246
+ : "TREND",
247
+ reason: normalizeText(item.reason ?? ""),
248
+ query: normalizeText(item.query ?? ""),
249
+ priority: ["high", "medium", "low"].includes(item.priority) ? item.priority : "medium",
250
+ status: (item.status === "executed" ? "executed" : "planned"),
251
+ }))
252
+ .filter((item) => item.reason.length > 0 && item.query.length > 0)
253
+ : [],
254
+ lastHypothesisGate: rawStream.lastHypothesisGate &&
255
+ typeof rawStream.lastHypothesisGate === "object" &&
256
+ !Array.isArray(rawStream.lastHypothesisGate)
257
+ ? {
258
+ accepted: typeof rawStream.lastHypothesisGate.accepted === "number"
259
+ ? Math.max(0, Math.floor(rawStream.lastHypothesisGate.accepted))
260
+ : 0,
261
+ rejected: typeof rawStream.lastHypothesisGate.rejected === "number"
262
+ ? Math.max(0, Math.floor(rawStream.lastHypothesisGate.rejected))
263
+ : 0,
264
+ rejectionReasons: Array.isArray(rawStream.lastHypothesisGate.rejectionReasons)
265
+ ? rawStream.lastHypothesisGate.rejectionReasons
266
+ .filter((item) => typeof item === "string")
267
+ .map((item) => normalizeText(item))
268
+ .filter((item) => item.length > 0)
269
+ : [],
270
+ }
271
+ : defaultHypothesisGateState(),
181
272
  };
182
273
  }
183
274
  return {
@@ -208,8 +299,8 @@ function normalizeStringArray(raw) {
208
299
  return undefined;
209
300
  const values = raw
210
301
  .filter((item) => typeof item === "string")
211
- .map((item) => normalizeText(item))
212
- .filter((item) => item.length > 0);
302
+ .map((item) => cleanOptionalText(item))
303
+ .filter((item) => Boolean(item));
213
304
  return values.length > 0 ? values : undefined;
214
305
  }
215
306
  function normalizeEvidenceAnchors(raw) {
@@ -218,14 +309,14 @@ function normalizeEvidenceAnchors(raw) {
218
309
  const anchors = raw
219
310
  .filter((item) => !!item && typeof item === "object")
220
311
  .map((item) => {
221
- const claim = normalizeText(item.claim ?? "");
312
+ const claim = cleanOptionalText(item.claim);
222
313
  if (!claim)
223
314
  return undefined;
224
315
  return {
225
- ...(item.section ? { section: normalizeText(item.section) } : {}),
226
- ...(item.locator ? { locator: normalizeText(item.locator) } : {}),
316
+ ...(cleanOptionalText(item.section) ? { section: cleanOptionalText(item.section) } : {}),
317
+ ...(cleanOptionalText(item.locator) ? { locator: cleanOptionalText(item.locator) } : {}),
227
318
  claim,
228
- ...(item.quote ? { quote: normalizeText(item.quote) } : {}),
319
+ ...(cleanOptionalText(item.quote) ? { quote: cleanOptionalText(item.quote) } : {}),
229
320
  };
230
321
  })
231
322
  .filter((item) => Boolean(item));
@@ -243,7 +334,7 @@ function toPaperNoteSlug(paper) {
243
334
  }
244
335
  function normalizePaper(input) {
245
336
  const evidenceIds = Array.isArray(input.evidenceIds)
246
- ? input.evidenceIds.map((id) => normalizeText(id)).filter((id) => id.length > 0)
337
+ ? input.evidenceIds.map((id) => cleanOptionalText(id)).filter((id) => Boolean(id))
247
338
  : undefined;
248
339
  const keyEvidenceSpans = normalizeStringArray(input.keyEvidenceSpans);
249
340
  const subdomains = normalizeStringArray(input.subdomains);
@@ -264,31 +355,33 @@ function normalizePaper(input) {
264
355
  : readStatus
265
356
  ? false
266
357
  : undefined;
267
- const unreadReason = input.unreadReason ? normalizeText(input.unreadReason) : undefined;
358
+ const unreadReason = cleanOptionalText(input.unreadReason);
268
359
  return {
269
- ...(input.id ? { id: normalizeText(input.id) } : {}),
270
- ...(input.title ? { title: normalizeText(input.title) } : {}),
271
- ...(input.url ? { url: normalizeText(input.url) } : {}),
272
- ...(input.source ? { source: normalizeText(input.source) } : {}),
273
- ...(input.publishedAt ? { publishedAt: normalizeText(input.publishedAt) } : {}),
360
+ ...(cleanOptionalText(input.id) ? { id: cleanOptionalText(input.id) } : {}),
361
+ ...(cleanOptionalText(input.title) ? { title: cleanOptionalText(input.title) } : {}),
362
+ ...(cleanOptionalText(input.url) ? { url: cleanOptionalText(input.url) } : {}),
363
+ ...(cleanOptionalText(input.source) ? { source: cleanOptionalText(input.source) } : {}),
364
+ ...(cleanOptionalText(input.publishedAt) ? { publishedAt: cleanOptionalText(input.publishedAt) } : {}),
274
365
  ...(typeof input.score === "number" && Number.isFinite(input.score)
275
366
  ? { score: Number(input.score.toFixed(2)) }
276
367
  : {}),
277
- ...(input.reason ? { reason: normalizeText(input.reason) } : {}),
278
- ...(input.summary ? { summary: normalizeText(input.summary) } : {}),
368
+ ...(cleanOptionalText(input.reason) ? { reason: cleanOptionalText(input.reason) } : {}),
369
+ ...(cleanOptionalText(input.summary) ? { summary: cleanOptionalText(input.summary) } : {}),
279
370
  ...(evidenceIds && evidenceIds.length > 0 ? { evidenceIds } : {}),
280
371
  ...(typeof fullTextRead === "boolean" ? { fullTextRead } : {}),
281
372
  ...(readStatus ? { readStatus } : {}),
282
- ...(input.fullTextSource ? { fullTextSource: normalizeText(input.fullTextSource) } : {}),
283
- ...(input.fullTextRef ? { fullTextRef: normalizeText(input.fullTextRef) } : {}),
373
+ ...(cleanOptionalText(input.fullTextSource) ? { fullTextSource: cleanOptionalText(input.fullTextSource) } : {}),
374
+ ...(cleanOptionalText(input.fullTextRef) ? { fullTextRef: cleanOptionalText(input.fullTextRef) } : {}),
284
375
  ...(unreadReason ? { unreadReason } : {}),
285
376
  ...(keyEvidenceSpans && keyEvidenceSpans.length > 0 ? { keyEvidenceSpans } : {}),
286
- ...(input.domain ? { domain: normalizeText(input.domain) } : {}),
377
+ ...(cleanOptionalText(input.domain) ? { domain: cleanOptionalText(input.domain) } : {}),
287
378
  ...(subdomains ? { subdomains } : {}),
288
379
  ...(crossDomainLinks ? { crossDomainLinks } : {}),
289
- ...(input.researchGoal ? { researchGoal: normalizeText(input.researchGoal) } : {}),
290
- ...(input.approach ? { approach: normalizeText(input.approach) } : {}),
291
- ...(input.methodologyDesign ? { methodologyDesign: normalizeText(input.methodologyDesign) } : {}),
380
+ ...(cleanOptionalText(input.researchGoal) ? { researchGoal: cleanOptionalText(input.researchGoal) } : {}),
381
+ ...(cleanOptionalText(input.approach) ? { approach: cleanOptionalText(input.approach) } : {}),
382
+ ...(cleanOptionalText(input.methodologyDesign)
383
+ ? { methodologyDesign: cleanOptionalText(input.methodologyDesign) }
384
+ : {}),
292
385
  ...(keyContributions ? { keyContributions } : {}),
293
386
  ...(practicalInsights ? { practicalInsights } : {}),
294
387
  ...(mustUnderstandPoints ? { mustUnderstandPoints } : {}),
@@ -401,6 +494,32 @@ function hasStructuredProfile(paper) {
401
494
  (paper.limitations && paper.limitations.length > 0) ||
402
495
  (paper.evidenceAnchors && paper.evidenceAnchors.length > 0));
403
496
  }
497
+ function countStructuredProfileFields(paper) {
498
+ let count = 0;
499
+ if (paper.domain && paper.domain.trim())
500
+ count += 1;
501
+ if (paper.subdomains && paper.subdomains.length > 0)
502
+ count += 1;
503
+ if (paper.crossDomainLinks && paper.crossDomainLinks.length > 0)
504
+ count += 1;
505
+ if (paper.researchGoal && paper.researchGoal.trim())
506
+ count += 1;
507
+ if (paper.approach && paper.approach.trim())
508
+ count += 1;
509
+ if (paper.methodologyDesign && paper.methodologyDesign.trim())
510
+ count += 1;
511
+ if (paper.keyContributions && paper.keyContributions.length > 0)
512
+ count += 1;
513
+ if (paper.practicalInsights && paper.practicalInsights.length > 0)
514
+ count += 1;
515
+ if (paper.mustUnderstandPoints && paper.mustUnderstandPoints.length > 0)
516
+ count += 1;
517
+ if (paper.limitations && paper.limitations.length > 0)
518
+ count += 1;
519
+ if (paper.evidenceAnchors && paper.evidenceAnchors.length > 0)
520
+ count += 1;
521
+ return count;
522
+ }
404
523
  function isFullTextRead(paper) {
405
524
  return paper.fullTextRead === true || paper.readStatus === "fulltext";
406
525
  }
@@ -447,6 +566,13 @@ function applyQualityGates(args) {
447
566
  const fullTextCoreCount = corePapers.filter((paper) => isFullTextRead(paper)).length;
448
567
  const fullTextCoverage = coreCount > 0 ? fullTextCoreCount / coreCount : 0;
449
568
  const fullTextCoveragePct = Number((fullTextCoverage * 100).toFixed(2));
569
+ const fullTextCorePapers = corePapers.filter((paper) => isFullTextRead(paper));
570
+ const structuredFieldTotal = 11;
571
+ const avgFullTextProfileCompleteness = fullTextCorePapers.length > 0
572
+ ? fullTextCorePapers.reduce((sum, paper) => sum + countStructuredProfileFields(paper) / structuredFieldTotal, 0) /
573
+ fullTextCorePapers.length
574
+ : 0;
575
+ const avgFullTextProfileCompletenessPct = Number((avgFullTextProfileCompleteness * 100).toFixed(2));
450
576
  const unreadCorePaperIds = dedupeText(corePapers
451
577
  .filter((paper) => !isFullTextRead(paper))
452
578
  .map((paper) => paper.id?.trim() || paper.url?.trim() || paper.title?.trim() || "unknown-paper")).slice(0, 50);
@@ -522,15 +648,43 @@ function applyQualityGates(args) {
522
648
  }
523
649
  }
524
650
  const reasons = [];
651
+ if (typeof args.requiredCorePapers === "number" &&
652
+ Number.isFinite(args.requiredCorePapers) &&
653
+ args.requiredCorePapers > 0 &&
654
+ coreCount < args.requiredCorePapers) {
655
+ reasons.push(`core_paper_count_below_required(${coreCount} < ${Math.floor(args.requiredCorePapers)})`);
656
+ }
525
657
  if (fullTextCoverage < MIN_CORE_FULLTEXT_COVERAGE) {
526
658
  reasons.push(`core_fulltext_coverage_below_threshold(${fullTextCoveragePct}% < ${Number((MIN_CORE_FULLTEXT_COVERAGE * 100).toFixed(0))}%)`);
527
659
  }
660
+ if (fullTextCorePapers.length > 0 && avgFullTextProfileCompleteness < MIN_FULLTEXT_PROFILE_COMPLETENESS) {
661
+ reasons.push(`fulltext_profile_completeness_below_threshold(${avgFullTextProfileCompletenessPct}% < ${Number((MIN_FULLTEXT_PROFILE_COMPLETENESS * 100).toFixed(0))}%)`);
662
+ }
663
+ if (typeof args.requiredFullTextCoveragePct === "number" &&
664
+ Number.isFinite(args.requiredFullTextCoveragePct) &&
665
+ args.requiredFullTextCoveragePct > 0 &&
666
+ fullTextCoveragePct < args.requiredFullTextCoveragePct) {
667
+ reasons.push(`core_fulltext_coverage_below_required(${fullTextCoveragePct}% < ${Number(args.requiredFullTextCoveragePct.toFixed(2))}%)`);
668
+ }
528
669
  if (evidenceBindingRate < MIN_EVIDENCE_BINDING_RATE) {
529
670
  reasons.push(`evidence_binding_rate_below_threshold(${evidenceBindingRatePct}% < ${Number((MIN_EVIDENCE_BINDING_RATE * 100).toFixed(0))}%)`);
530
671
  }
531
672
  if (citationErrorRate >= MAX_CITATION_ERROR_RATE) {
532
673
  reasons.push(`citation_error_rate_above_threshold(${citationErrorRatePct}% >= ${Number((MAX_CITATION_ERROR_RATE * 100).toFixed(0))}%)`);
533
674
  }
675
+ const bridgeChangeCount = args.knowledgeChanges.filter((item) => item.type === "BRIDGE").length;
676
+ const reviseCount = args.knowledgeChanges.filter((item) => item.type === "REVISE").length;
677
+ const confirmCount = args.knowledgeChanges.filter((item) => item.type === "CONFIRM").length;
678
+ const executedReflectionCount = args.reflectionTasks.filter((task) => task.status === "executed").length;
679
+ if (bridgeChangeCount > 0 && executedReflectionCount === 0) {
680
+ reasons.push(`reflection_missing_for_bridge(bridge_count=${bridgeChangeCount})`);
681
+ }
682
+ if (reviseCount > 0 && confirmCount > 0 && executedReflectionCount === 0) {
683
+ reasons.push(`reflection_missing_for_conflict(revise_count=${reviseCount},confirm_count=${confirmCount})`);
684
+ }
685
+ if (args.hypothesisGate.rejected > 0 && args.hypothesisGate.accepted === 0 && args.hypotheses.length > 0) {
686
+ reasons.push(`hypothesis_gate_rejected_all(${args.hypothesisGate.rejected})`);
687
+ }
534
688
  if (downgradedHighConfidenceCount > 0) {
535
689
  reasons.push(`high_confidence_downgraded(${downgradedHighConfidenceCount})`);
536
690
  }
@@ -642,10 +796,12 @@ function toSummary(stream) {
642
796
  return {
643
797
  projectId: stream.projectId,
644
798
  streamKey: stream.topicKey,
799
+ runProfile: stream.lastRunProfile,
645
800
  totalRuns: stream.totalRuns,
646
801
  totalHypotheses: stream.totalHypotheses,
647
802
  knowledgeTopicsCount: stream.knowledgeTopics.length,
648
803
  paperNotesCount: stream.paperNotes.length,
804
+ triggerState: stream.triggerState,
649
805
  recentFullTextReadCount: stream.recentFullTextReadCount,
650
806
  recentNotFullTextReadCount: stream.recentNotFullTextReadCount,
651
807
  qualityGate: stream.lastQualityGate,
@@ -656,6 +812,8 @@ function toSummary(stream) {
656
812
  recentHypotheses: stream.recentHypotheses,
657
813
  recentChangeStats: stream.recentChangeStats,
658
814
  lastExplorationTrace: stream.lastExplorationTrace,
815
+ lastReflectionTasks: stream.lastReflectionTasks,
816
+ hypothesisGate: stream.lastHypothesisGate,
659
817
  };
660
818
  }
661
819
  function countChangeStats(day, runId, changes) {
@@ -682,6 +840,277 @@ function countChangeStats(day, runId, changes) {
682
840
  bridgeCount,
683
841
  };
684
842
  }
843
+ function tokenizeForQuery(raw) {
844
+ return normalizeText(raw)
845
+ .toLowerCase()
846
+ .replace(/[^a-z0-9\u4e00-\u9fff\s_-]+/g, " ")
847
+ .split(/\s+/)
848
+ .map((item) => item.trim())
849
+ .filter((item) => item.length >= 3);
850
+ }
851
+ function uniqueText(values) {
852
+ return [...new Set(values.map((item) => normalizeText(item)).filter((item) => item.length > 0))];
853
+ }
854
+ function buildReflectionQuery(topic, statement, fallbackHint) {
855
+ const topicTokens = tokenizeForQuery(topic).slice(0, 4);
856
+ const stmtTokens = tokenizeForQuery(statement).slice(0, 6);
857
+ const merged = uniqueText([...topicTokens, ...stmtTokens]);
858
+ if (merged.length === 0)
859
+ return `${topic} ${fallbackHint}`.trim();
860
+ return merged.join(" ");
861
+ }
862
+ function queryMatchesTrace(query, trace) {
863
+ const tokens = tokenizeForQuery(query).slice(0, 4);
864
+ if (tokens.length === 0)
865
+ return false;
866
+ return trace.some((step) => {
867
+ const hay = normalizeText(step.query).toLowerCase();
868
+ let hit = 0;
869
+ for (const token of tokens) {
870
+ if (hay.includes(token))
871
+ hit += 1;
872
+ if (hit >= Math.min(2, tokens.length))
873
+ return true;
874
+ }
875
+ return false;
876
+ });
877
+ }
878
+ function deriveReflectionTasks(args) {
879
+ const tasks = [];
880
+ const bridge = args.changes.filter((item) => item.type === "BRIDGE");
881
+ const revise = args.changes.filter((item) => item.type === "REVISE");
882
+ const confirm = args.changes.filter((item) => item.type === "CONFIRM");
883
+ const newly = args.changes.filter((item) => item.type === "NEW");
884
+ for (const [idx, change] of bridge.slice(0, 3).entries()) {
885
+ const query = buildReflectionQuery(args.topic, change.statement, "cross-domain mechanism");
886
+ tasks.push({
887
+ id: sanitizeId(`bridge-${idx + 1}-${query}`),
888
+ trigger: "BRIDGE",
889
+ reason: `Bridge signal requires cross-domain follow-up: ${change.statement}`,
890
+ query,
891
+ priority: "high",
892
+ status: queryMatchesTrace(query, args.trace) ? "executed" : "planned",
893
+ });
894
+ }
895
+ if (newly.length >= 3) {
896
+ const query = buildReflectionQuery(args.topic, newly.map((item) => item.statement).join(" "), "trend synthesis");
897
+ tasks.push({
898
+ id: sanitizeId(`trend-${query}`),
899
+ trigger: "TREND",
900
+ reason: `New findings accumulated (${newly.length}); run trend synthesis and gap scan.`,
901
+ query,
902
+ priority: "medium",
903
+ status: queryMatchesTrace(query, args.trace) ? "executed" : "planned",
904
+ });
905
+ }
906
+ if (revise.length > 0 && confirm.length > 0) {
907
+ const query = buildReflectionQuery(args.topic, `${revise[0]?.statement ?? ""} ${confirm[0]?.statement ?? ""}`, "contradiction resolution");
908
+ tasks.push({
909
+ id: sanitizeId(`contradiction-${query}`),
910
+ trigger: "CONTRADICTION",
911
+ reason: `Revise and confirm signals co-exist; verify contradiction boundaries.`,
912
+ query,
913
+ priority: "high",
914
+ status: queryMatchesTrace(query, args.trace) ? "executed" : "planned",
915
+ });
916
+ }
917
+ const unreadCore = args.corePapers.filter((paper) => !isFullTextRead(paper));
918
+ if (unreadCore.length > 0) {
919
+ const topId = unreadCore[0]?.id ?? unreadCore[0]?.title ?? "core-paper";
920
+ const query = buildReflectionQuery(args.topic, String(topId), "full text retrieval");
921
+ tasks.push({
922
+ id: sanitizeId(`unread-core-${query}`),
923
+ trigger: "UNREAD_CORE",
924
+ reason: `${unreadCore.length} core paper(s) were not fully read; prioritize retrieval and verification.`,
925
+ query,
926
+ priority: "medium",
927
+ status: queryMatchesTrace(query, args.trace) ? "executed" : "planned",
928
+ });
929
+ }
930
+ const dedup = new Map();
931
+ for (const task of tasks) {
932
+ const key = normalizeText(task.query).toLowerCase();
933
+ if (!key)
934
+ continue;
935
+ const existing = dedup.get(key);
936
+ if (!existing) {
937
+ dedup.set(key, task);
938
+ continue;
939
+ }
940
+ // Keep higher priority / executed status when duplicates collide.
941
+ const priorityRank = { high: 3, medium: 2, low: 1 };
942
+ const pick = (existing.status !== "executed" && task.status === "executed") ||
943
+ priorityRank[task.priority] > priorityRank[existing.priority]
944
+ ? task
945
+ : existing;
946
+ dedup.set(key, pick);
947
+ }
948
+ return [...dedup.values()].slice(0, MAX_LAST_REFLECTION_TASKS);
949
+ }
950
+ function sanitizeKnowledgeChanges(args) {
951
+ if (args.changes.length === 0) {
952
+ return {
953
+ changes: [],
954
+ droppedBridgeCount: 0,
955
+ };
956
+ }
957
+ const paperLookup = buildPaperLookup(args.allRunPapers);
958
+ const next = [];
959
+ let droppedBridgeCount = 0;
960
+ for (const change of args.changes) {
961
+ if (change.type !== "BRIDGE") {
962
+ next.push(change);
963
+ continue;
964
+ }
965
+ const evidenceIds = (change.evidenceIds ?? []).map((id) => normalizedCitationToken(id)).filter((id) => id.length > 0);
966
+ if (evidenceIds.length === 0) {
967
+ droppedBridgeCount += 1;
968
+ continue;
969
+ }
970
+ let hasResolvedEvidence = false;
971
+ let hasFullTextEvidence = false;
972
+ for (const evidenceId of evidenceIds) {
973
+ const paper = paperLookup.get(evidenceId);
974
+ if (!paper)
975
+ continue;
976
+ hasResolvedEvidence = true;
977
+ if (isFullTextRead(paper))
978
+ hasFullTextEvidence = true;
979
+ }
980
+ // Guard against speculative bridge signals with no grounded full-text evidence.
981
+ if (!hasResolvedEvidence || !hasFullTextEvidence) {
982
+ droppedBridgeCount += 1;
983
+ continue;
984
+ }
985
+ next.push(change);
986
+ }
987
+ return {
988
+ changes: next,
989
+ droppedBridgeCount,
990
+ };
991
+ }
992
+ function applyHypothesisGate(args) {
993
+ const acceptedHypotheses = [];
994
+ const rejectionReasonSet = new Set();
995
+ const paperLookup = buildPaperLookup(args.allRunPapers);
996
+ const changeCounts = {
997
+ NEW: args.knowledgeChanges.filter((item) => item.type === "NEW").length,
998
+ CONFIRM: args.knowledgeChanges.filter((item) => item.type === "CONFIRM").length,
999
+ REVISE: args.knowledgeChanges.filter((item) => item.type === "REVISE").length,
1000
+ BRIDGE: args.knowledgeChanges.filter((item) => item.type === "BRIDGE").length,
1001
+ };
1002
+ for (const hypothesis of args.hypotheses) {
1003
+ const reasons = [];
1004
+ const statementLen = normalizeText(hypothesis.statement).length;
1005
+ if (statementLen < MIN_HYPOTHESIS_STATEMENT_CHARS) {
1006
+ reasons.push(`statement_too_short(${statementLen}<${MIN_HYPOTHESIS_STATEMENT_CHARS})`);
1007
+ }
1008
+ const evidenceIds = uniqueText((hypothesis.evidenceIds ?? []).map((id) => normalizedCitationToken(id)));
1009
+ if (evidenceIds.length < MIN_HYPOTHESIS_EVIDENCE) {
1010
+ reasons.push(`insufficient_evidence_ids(${evidenceIds.length}<${MIN_HYPOTHESIS_EVIDENCE})`);
1011
+ }
1012
+ let resolvedEvidence = 0;
1013
+ let fullTextSupported = 0;
1014
+ for (const evidenceId of evidenceIds) {
1015
+ const paper = paperLookup.get(evidenceId);
1016
+ if (!paper)
1017
+ continue;
1018
+ resolvedEvidence += 1;
1019
+ if (isFullTextRead(paper))
1020
+ fullTextSupported += 1;
1021
+ }
1022
+ if (resolvedEvidence < evidenceIds.length) {
1023
+ reasons.push(`unresolved_evidence_ids(${evidenceIds.length - resolvedEvidence})`);
1024
+ }
1025
+ if (fullTextSupported === 0 && args.runProfile === "strict") {
1026
+ reasons.push("no_fulltext_backed_evidence");
1027
+ }
1028
+ if (resolvedEvidence === 0) {
1029
+ reasons.push("no_resolved_evidence");
1030
+ }
1031
+ const dependencyPathLength = hypothesis.dependencyPath?.length ?? 0;
1032
+ if (dependencyPathLength < MIN_HYPOTHESIS_DEPENDENCY_STEPS) {
1033
+ reasons.push(`dependency_path_too_short(${dependencyPathLength}<${MIN_HYPOTHESIS_DEPENDENCY_STEPS})`);
1034
+ }
1035
+ const hasScore = typeof hypothesis.novelty === "number" &&
1036
+ typeof hypothesis.feasibility === "number" &&
1037
+ typeof hypothesis.impact === "number";
1038
+ if (!hasScore) {
1039
+ reasons.push("missing_self_assessment_scores");
1040
+ }
1041
+ if (hypothesis.trigger === "BRIDGE" && changeCounts.BRIDGE === 0) {
1042
+ reasons.push("trigger_bridge_without_bridge_change");
1043
+ }
1044
+ if (hypothesis.trigger === "TREND" && changeCounts.NEW < 2) {
1045
+ reasons.push("trigger_trend_without_new_accumulation");
1046
+ }
1047
+ if (hypothesis.trigger === "CONTRADICTION" && !(changeCounts.REVISE > 0 && changeCounts.CONFIRM > 0)) {
1048
+ reasons.push("trigger_contradiction_without_revise_confirm_pair");
1049
+ }
1050
+ if (hypothesis.trigger === "GAP" && changeCounts.NEW + changeCounts.REVISE < 2) {
1051
+ reasons.push("trigger_gap_without_gap_signal");
1052
+ }
1053
+ if (reasons.length > 0) {
1054
+ for (const reason of reasons)
1055
+ rejectionReasonSet.add(reason);
1056
+ continue;
1057
+ }
1058
+ acceptedHypotheses.push(hypothesis);
1059
+ }
1060
+ return {
1061
+ acceptedHypotheses,
1062
+ gate: {
1063
+ accepted: acceptedHypotheses.length,
1064
+ rejected: Math.max(0, args.hypotheses.length - acceptedHypotheses.length),
1065
+ rejectionReasons: [...rejectionReasonSet].slice(0, MAX_HYPOTHESIS_REJECTION_REASONS),
1066
+ },
1067
+ };
1068
+ }
1069
+ function toDayStartMs(day) {
1070
+ const ts = Date.parse(`${day}T00:00:00.000Z`);
1071
+ return Number.isFinite(ts) ? ts : undefined;
1072
+ }
1073
+ function deriveTriggerState(args) {
1074
+ const sorted = [...args.recentChangeStats].sort((a, b) => b.day.localeCompare(a.day));
1075
+ // consecutive days from latest day with NEW/REVISE > 0
1076
+ let consecutiveNewReviseDays = 0;
1077
+ let expectedDayMs;
1078
+ for (const item of sorted) {
1079
+ const dayMs = toDayStartMs(item.day);
1080
+ if (dayMs === undefined)
1081
+ continue;
1082
+ const hasSignal = item.newCount > 0 || item.reviseCount > 0;
1083
+ if (!hasSignal)
1084
+ break;
1085
+ if (expectedDayMs === undefined) {
1086
+ consecutiveNewReviseDays = 1;
1087
+ expectedDayMs = dayMs - 24 * 60 * 60 * 1000;
1088
+ continue;
1089
+ }
1090
+ if (dayMs === expectedDayMs) {
1091
+ consecutiveNewReviseDays += 1;
1092
+ expectedDayMs = dayMs - 24 * 60 * 60 * 1000;
1093
+ continue;
1094
+ }
1095
+ break;
1096
+ }
1097
+ const sevenDaysAgo = args.nowMs - 7 * 24 * 60 * 60 * 1000;
1098
+ let bridgeCount7d = 0;
1099
+ for (const item of sorted) {
1100
+ const dayMs = toDayStartMs(item.day);
1101
+ if (dayMs === undefined)
1102
+ continue;
1103
+ if (dayMs < sevenDaysAgo)
1104
+ continue;
1105
+ bridgeCount7d += Math.max(0, Math.floor(item.bridgeCount));
1106
+ }
1107
+ return {
1108
+ consecutiveNewReviseDays,
1109
+ bridgeCount7d,
1110
+ unreadCoreBacklog: Math.max(0, Math.floor(args.unreadCoreBacklog)),
1111
+ lastUpdatedAtMs: args.nowMs,
1112
+ };
1113
+ }
685
1114
  export async function commitKnowledgeRun(input) {
686
1115
  const project = await resolveProjectContext({
687
1116
  projectId: input.projectId,
@@ -706,7 +1135,7 @@ export async function commitKnowledgeRun(input) {
706
1135
  const explorationTrace = (knowledgeState.explorationTrace ?? [])
707
1136
  .map(normalizeTrace)
708
1137
  .filter((item) => Boolean(item));
709
- const knowledgeChanges = (knowledgeState.knowledgeChanges ?? [])
1138
+ const submittedKnowledgeChanges = (knowledgeState.knowledgeChanges ?? [])
710
1139
  .map(normalizeChange)
711
1140
  .filter((item) => Boolean(item));
712
1141
  const knowledgeUpdates = (knowledgeState.knowledgeUpdates ?? [])
@@ -733,10 +1162,12 @@ export async function commitKnowledgeRun(input) {
733
1162
  topic: normalizeText(input.topic),
734
1163
  topicKey: streamKey,
735
1164
  projectId: project.projectId,
1165
+ lastRunProfile: defaultRunProfile(),
736
1166
  totalRuns: 0,
737
1167
  totalHypotheses: 0,
738
1168
  knowledgeTopics: [],
739
1169
  paperNotes: [],
1170
+ triggerState: defaultTriggerState(),
740
1171
  recentFullTextReadCount: 0,
741
1172
  recentNotFullTextReadCount: 0,
742
1173
  lastQualityGate: defaultQualityGateState(),
@@ -747,14 +1178,16 @@ export async function commitKnowledgeRun(input) {
747
1178
  recentHypotheses: [],
748
1179
  recentChangeStats: [],
749
1180
  lastExplorationTrace: [],
1181
+ lastReflectionTasks: [],
1182
+ lastHypothesisGate: defaultHypothesisGateState(),
750
1183
  };
751
1184
  const paperIds = mergePapers(corePapers, explorationPapers)
752
1185
  .map((paper) => paper.id || paper.url || paper.title || "")
753
1186
  .map((value) => normalizeText(value))
754
1187
  .filter((value) => value.length > 0);
755
- const runId = input.runId?.trim()
756
- ? sanitizeId(input.runId)
757
- : buildRunFingerprint({
1188
+ const explicitRunId = input.runId?.trim() ? sanitizeId(input.runId) : undefined;
1189
+ let runId = explicitRunId ??
1190
+ buildRunFingerprint({
758
1191
  scope: stream.scope,
759
1192
  topic: stream.topic,
760
1193
  status: input.status,
@@ -762,15 +1195,30 @@ export async function commitKnowledgeRun(input) {
762
1195
  paperIds,
763
1196
  note: input.note,
764
1197
  });
1198
+ const inferredRunProfile = input.knowledgeState?.runLog?.runProfile === "strict" || input.knowledgeState?.runLog?.runProfile === "fast"
1199
+ ? input.knowledgeState.runLog.runProfile
1200
+ : input.knowledgeState?.runLog?.requiredCorePapers !== undefined ||
1201
+ input.knowledgeState?.runLog?.requiredFullTextCoveragePct !== undefined
1202
+ ? "strict"
1203
+ : defaultRunProfile();
765
1204
  if (stream.recentRunIds.includes(runId)) {
766
- root.streams[streamKey] = stream;
767
- return {
768
- projectId: project.projectId,
769
- streamKey,
770
- summary: toSummary(stream),
771
- runId,
772
- createdProject: project.created,
773
- };
1205
+ // Preserve idempotency for fingerprint-based runs, but avoid silently dropping
1206
+ // valid new cron cycles that accidentally reuse an explicit run_id.
1207
+ if (!explicitRunId) {
1208
+ root.streams[streamKey] = stream;
1209
+ return {
1210
+ projectId: project.projectId,
1211
+ streamKey,
1212
+ summary: toSummary(stream),
1213
+ runId,
1214
+ createdProject: project.created,
1215
+ };
1216
+ }
1217
+ const collisionTag = createHash("sha1")
1218
+ .update(`${nowMs}\n${paperIds.join("|")}\n${input.status ?? ""}\n${input.note ?? ""}`)
1219
+ .digest("hex")
1220
+ .slice(0, 8);
1221
+ runId = `${runId}-r${collisionTag}`;
774
1222
  }
775
1223
  const rootPath = getKnowledgeStateRoot(project.projectPath);
776
1224
  const logDir = path.join(rootPath, "logs");
@@ -785,15 +1233,48 @@ export async function commitKnowledgeRun(input) {
785
1233
  trace: explorationTrace,
786
1234
  papers: explorationPapers,
787
1235
  }));
788
- await appendMarkdown(path.join(dailyDir, `day-${dayKey}.md`), renderDailyChangesMarkdown({ now: nowIso, runId, topic: stream.topic, changes: knowledgeChanges }));
789
1236
  const mergedRunPapers = mergePapers(corePapers, explorationPapers);
1237
+ const changeSanitization = sanitizeKnowledgeChanges({
1238
+ changes: submittedKnowledgeChanges,
1239
+ allRunPapers: mergedRunPapers,
1240
+ });
1241
+ const knowledgeChanges = changeSanitization.changes;
1242
+ await appendMarkdown(path.join(dailyDir, `day-${dayKey}.md`), renderDailyChangesMarkdown({ now: nowIso, runId, topic: stream.topic, changes: knowledgeChanges }));
1243
+ const reflectionTasks = deriveReflectionTasks({
1244
+ topic: stream.topic,
1245
+ changes: knowledgeChanges,
1246
+ trace: explorationTrace,
1247
+ corePapers,
1248
+ });
1249
+ await appendMarkdown(path.join(logDir, `day-${dayKey}-reflection.md`), renderReflectionLogMarkdown({
1250
+ now: nowIso,
1251
+ runId,
1252
+ tasks: reflectionTasks,
1253
+ }));
1254
+ const submittedHypotheses = hypotheses;
1255
+ const hypothesisEval = applyHypothesisGate({
1256
+ hypotheses: submittedHypotheses,
1257
+ allRunPapers: mergedRunPapers,
1258
+ knowledgeChanges,
1259
+ runProfile: inferredRunProfile,
1260
+ });
1261
+ const acceptedHypotheses = hypothesisEval.acceptedHypotheses;
790
1262
  const qualityEval = applyQualityGates({
791
1263
  corePapers,
792
1264
  allRunPapers: mergedRunPapers,
1265
+ explorationTrace,
1266
+ reflectionTasks,
793
1267
  knowledgeChanges,
794
1268
  knowledgeUpdates,
795
- hypotheses,
1269
+ hypotheses: acceptedHypotheses,
1270
+ hypothesisGate: hypothesisEval.gate,
1271
+ requiredCorePapers: input.knowledgeState?.runLog?.requiredCorePapers,
1272
+ requiredFullTextCoveragePct: input.knowledgeState?.runLog?.requiredFullTextCoveragePct,
796
1273
  });
1274
+ if (changeSanitization.droppedBridgeCount > 0) {
1275
+ qualityEval.qualityGate.passed = false;
1276
+ qualityEval.qualityGate.reasons.push(`bridge_dropped_due_to_ungrounded_evidence(${changeSanitization.droppedBridgeCount})`);
1277
+ }
797
1278
  const requestedStatus = normalizeText(input.status ?? "ok");
798
1279
  const qualitySensitiveStatus = requestedStatus === "ok" || requestedStatus === "fallback_representative";
799
1280
  const effectiveStatus = qualitySensitiveStatus && !qualityEval.qualityGate.passed ? "degraded_quality" : requestedStatus;
@@ -836,7 +1317,7 @@ export async function commitKnowledgeRun(input) {
836
1317
  const recentHypothesisSummaries = [];
837
1318
  let seq = stream.totalHypotheses;
838
1319
  const dayToken = dayKey.replace(/-/g, "");
839
- for (const hypothesis of hypotheses) {
1320
+ for (const hypothesis of acceptedHypotheses) {
840
1321
  seq += 1;
841
1322
  const hypothesisId = hypothesis.id && hypothesis.id.length > 0 ? sanitizeId(hypothesis.id) : `hyp-${dayToken}-${String(seq).padStart(4, "0")}`;
842
1323
  const file = `${hypothesisId}.md`;
@@ -854,6 +1335,7 @@ export async function commitKnowledgeRun(input) {
854
1335
  await writeFile(path.join(knowledgeDir, "_index.md"), renderKnowledgeIndexMarkdown({
855
1336
  now: nowIso,
856
1337
  topic: stream.topic,
1338
+ runProfile: inferredRunProfile,
857
1339
  topicFiles: stream.knowledgeTopics,
858
1340
  paperNotesCount: stream.paperNotes.length,
859
1341
  totalHypotheses: stream.totalHypotheses + recentHypothesisSummaries.length,
@@ -862,10 +1344,13 @@ export async function commitKnowledgeRun(input) {
862
1344
  notFullTextReadCount: fullTextStats.notFullTextReadCount,
863
1345
  qualityGate: qualityEval.qualityGate,
864
1346
  unreadCorePaperIds: qualityEval.unreadCorePaperIds,
1347
+ reflectionTasks,
1348
+ hypothesisGate: hypothesisEval.gate,
865
1349
  lastStatus: effectiveStatus,
866
1350
  }), "utf-8");
867
1351
  const changeStat = countChangeStats(dayKey, runId, knowledgeChanges);
868
1352
  stream.projectId = project.projectId;
1353
+ stream.lastRunProfile = inferredRunProfile;
869
1354
  stream.totalRuns += 1;
870
1355
  stream.totalHypotheses += recentHypothesisSummaries.length;
871
1356
  stream.lastRunAtMs = nowMs;
@@ -875,6 +1360,8 @@ export async function commitKnowledgeRun(input) {
875
1360
  stream.lastQualityGate = qualityEval.qualityGate;
876
1361
  stream.lastUnreadCorePaperIds = qualityEval.unreadCorePaperIds;
877
1362
  stream.lastExplorationTrace = explorationTrace.slice(0, MAX_LAST_TRACE);
1363
+ stream.lastReflectionTasks = reflectionTasks.slice(0, MAX_LAST_REFLECTION_TASKS);
1364
+ stream.lastHypothesisGate = hypothesisEval.gate;
878
1365
  stream.recentPapers = mergePapers(mergedRunPapers, stream.recentPapers).slice(0, MAX_RECENT_PAPERS);
879
1366
  stream.recentRunIds = [runId, ...stream.recentRunIds.filter((id) => id !== runId)].slice(0, MAX_RECENT_RUN_IDS);
880
1367
  stream.recentHypothesisIds = [
@@ -883,11 +1370,18 @@ export async function commitKnowledgeRun(input) {
883
1370
  ].slice(0, MAX_RECENT_HYPOTHESES);
884
1371
  stream.recentHypotheses = [...recentHypothesisSummaries, ...stream.recentHypotheses].slice(0, MAX_RECENT_HYPOTHESES);
885
1372
  stream.recentChangeStats = [changeStat, ...stream.recentChangeStats].slice(0, MAX_RECENT_CHANGE_STATS);
1373
+ stream.triggerState = deriveTriggerState({
1374
+ recentChangeStats: stream.recentChangeStats,
1375
+ unreadCoreBacklog: qualityEval.unreadCorePaperIds.length,
1376
+ nowMs,
1377
+ });
886
1378
  root.streams[streamKey] = stream;
887
1379
  await saveStateAtomic(project.projectPath, root);
888
1380
  await appendFile(path.join(logDir, `day-${dayKey}-run-details.jsonl`), `${JSON.stringify({
889
1381
  ts: nowMs,
1382
+ run_id: runId,
890
1383
  runId,
1384
+ run_profile: inferredRunProfile,
891
1385
  scope: stream.scope,
892
1386
  topic: stream.topic,
893
1387
  streamKey,
@@ -895,9 +1389,14 @@ export async function commitKnowledgeRun(input) {
895
1389
  corePapers,
896
1390
  explorationPapers,
897
1391
  explorationTrace,
1392
+ reflectionTasks,
1393
+ submittedKnowledgeChanges,
898
1394
  knowledgeChanges,
1395
+ droppedBridgeCount: changeSanitization.droppedBridgeCount,
899
1396
  knowledgeUpdates,
900
- hypotheses,
1397
+ hypotheses: acceptedHypotheses,
1398
+ submittedHypotheses,
1399
+ hypothesisGate: hypothesisEval.gate,
901
1400
  paperNoteFiles: runPaperNoteFiles,
902
1401
  quality: {
903
1402
  fullTextReadCount: fullTextStats.fullTextReadCount,
@@ -913,7 +1412,9 @@ export async function commitKnowledgeRun(input) {
913
1412
  })}\n`, "utf-8");
914
1413
  await appendEvent(project.projectPath, {
915
1414
  ts: nowMs,
1415
+ run_id: runId,
916
1416
  runId,
1417
+ run_profile: inferredRunProfile,
917
1418
  scope: stream.scope,
918
1419
  topic: stream.topic,
919
1420
  streamKey,
@@ -929,8 +1430,14 @@ export async function commitKnowledgeRun(input) {
929
1430
  qualityGate: qualityEval.qualityGate,
930
1431
  unreadCorePaperIds: qualityEval.unreadCorePaperIds,
931
1432
  downgradedHighConfidenceCount: qualityEval.downgradedHighConfidenceCount,
1433
+ submittedChangeCount: submittedKnowledgeChanges.length,
932
1434
  changeCount: knowledgeChanges.length,
1435
+ droppedBridgeCount: changeSanitization.droppedBridgeCount,
933
1436
  hypothesisCount: recentHypothesisSummaries.length,
1437
+ submittedHypothesisCount: submittedHypotheses.length,
1438
+ hypothesisGate: hypothesisEval.gate,
1439
+ triggerState: stream.triggerState,
1440
+ reflectionTasks,
934
1441
  corePapers,
935
1442
  explorationPapers,
936
1443
  note: input.note,