scientify 1.13.6 → 2.0.0

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 (100) hide show
  1. package/README.en.md +350 -0
  2. package/README.md +148 -358
  3. package/dist/index.d.ts +8 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +131 -122
  6. package/dist/index.js.map +1 -1
  7. package/dist/src/cli/research.d.ts +1 -6
  8. package/dist/src/cli/research.d.ts.map +1 -1
  9. package/dist/src/cli/research.js +227 -123
  10. package/dist/src/cli/research.js.map +1 -1
  11. package/dist/src/commands/metabolism-status.d.ts +3 -3
  12. package/dist/src/commands/metabolism-status.d.ts.map +1 -1
  13. package/dist/src/commands/metabolism-status.js +72 -75
  14. package/dist/src/commands/metabolism-status.js.map +1 -1
  15. package/dist/src/commands.d.ts +1 -1
  16. package/dist/src/commands.d.ts.map +1 -1
  17. package/dist/src/commands.js +0 -55
  18. package/dist/src/commands.js.map +1 -1
  19. package/dist/src/hooks/cron-skill-inject.d.ts +6 -7
  20. package/dist/src/hooks/cron-skill-inject.d.ts.map +1 -1
  21. package/dist/src/hooks/cron-skill-inject.js +6 -15
  22. package/dist/src/hooks/cron-skill-inject.js.map +1 -1
  23. package/dist/src/hooks/research-mode.d.ts +1 -1
  24. package/dist/src/hooks/research-mode.d.ts.map +1 -1
  25. package/dist/src/hooks/research-mode.js +24 -101
  26. package/dist/src/hooks/research-mode.js.map +1 -1
  27. package/dist/src/hooks/scientify-signature.d.ts +1 -1
  28. package/dist/src/hooks/scientify-signature.d.ts.map +1 -1
  29. package/dist/src/hooks/scientify-signature.js +2 -5
  30. package/dist/src/hooks/scientify-signature.js.map +1 -1
  31. package/dist/src/knowledge-state/render.d.ts +1 -9
  32. package/dist/src/knowledge-state/render.d.ts.map +1 -1
  33. package/dist/src/knowledge-state/render.js +33 -187
  34. package/dist/src/knowledge-state/render.js.map +1 -1
  35. package/dist/src/knowledge-state/store.d.ts.map +1 -1
  36. package/dist/src/knowledge-state/store.js +65 -1100
  37. package/dist/src/knowledge-state/store.js.map +1 -1
  38. package/dist/src/knowledge-state/types.d.ts +0 -76
  39. package/dist/src/knowledge-state/types.d.ts.map +1 -1
  40. package/dist/src/literature/subscription-state.d.ts +0 -2
  41. package/dist/src/literature/subscription-state.d.ts.map +1 -1
  42. package/dist/src/literature/subscription-state.js +7 -1375
  43. package/dist/src/literature/subscription-state.js.map +1 -1
  44. package/dist/src/research-subscriptions/constants.d.ts +1 -1
  45. package/dist/src/research-subscriptions/constants.js +1 -1
  46. package/dist/src/research-subscriptions/cron-client.d.ts +1 -1
  47. package/dist/src/research-subscriptions/cron-client.d.ts.map +1 -1
  48. package/dist/src/research-subscriptions/delivery.d.ts +1 -1
  49. package/dist/src/research-subscriptions/delivery.d.ts.map +1 -1
  50. package/dist/src/research-subscriptions/handlers.d.ts +1 -1
  51. package/dist/src/research-subscriptions/handlers.d.ts.map +1 -1
  52. package/dist/src/research-subscriptions/handlers.js +10 -20
  53. package/dist/src/research-subscriptions/handlers.js.map +1 -1
  54. package/dist/src/research-subscriptions/parse.d.ts.map +1 -1
  55. package/dist/src/research-subscriptions/parse.js +0 -25
  56. package/dist/src/research-subscriptions/parse.js.map +1 -1
  57. package/dist/src/research-subscriptions/prompt.d.ts +1 -1
  58. package/dist/src/research-subscriptions/prompt.d.ts.map +1 -1
  59. package/dist/src/research-subscriptions/prompt.js +195 -244
  60. package/dist/src/research-subscriptions/prompt.js.map +1 -1
  61. package/dist/src/research-subscriptions/types.d.ts +1 -3
  62. package/dist/src/research-subscriptions/types.d.ts.map +1 -1
  63. package/dist/src/templates/bootstrap.d.ts.map +1 -1
  64. package/dist/src/templates/bootstrap.js +32 -19
  65. package/dist/src/templates/bootstrap.js.map +1 -1
  66. package/dist/src/tools/arxiv-download.d.ts +1 -2
  67. package/dist/src/tools/arxiv-download.d.ts.map +1 -1
  68. package/dist/src/tools/arxiv-search.d.ts +1 -2
  69. package/dist/src/tools/arxiv-search.d.ts.map +1 -1
  70. package/dist/src/tools/github-search-tool.d.ts +1 -2
  71. package/dist/src/tools/github-search-tool.d.ts.map +1 -1
  72. package/dist/src/tools/openalex-search.d.ts +1 -2
  73. package/dist/src/tools/openalex-search.d.ts.map +1 -1
  74. package/dist/src/tools/openreview-lookup.d.ts +1 -2
  75. package/dist/src/tools/openreview-lookup.d.ts.map +1 -1
  76. package/dist/src/tools/paper-browser.d.ts +1 -2
  77. package/dist/src/tools/paper-browser.d.ts.map +1 -1
  78. package/dist/src/tools/result.d.ts +3 -5
  79. package/dist/src/tools/result.d.ts.map +1 -1
  80. package/dist/src/tools/result.js +5 -7
  81. package/dist/src/tools/result.js.map +1 -1
  82. package/dist/src/tools/scientify-cron.d.ts +4 -11
  83. package/dist/src/tools/scientify-cron.d.ts.map +1 -1
  84. package/dist/src/tools/scientify-cron.js +19 -524
  85. package/dist/src/tools/scientify-cron.js.map +1 -1
  86. package/dist/src/tools/scientify-literature-state.d.ts +1 -76
  87. package/dist/src/tools/scientify-literature-state.d.ts.map +1 -1
  88. package/dist/src/tools/scientify-literature-state.js +46 -363
  89. package/dist/src/tools/scientify-literature-state.js.map +1 -1
  90. package/dist/src/tools/unpaywall-download.d.ts +1 -2
  91. package/dist/src/tools/unpaywall-download.d.ts.map +1 -1
  92. package/dist/src/types.d.ts +16 -0
  93. package/dist/src/types.d.ts.map +1 -0
  94. package/dist/src/types.js +2 -0
  95. package/dist/src/types.js.map +1 -0
  96. package/openclaw.plugin.json +4 -2
  97. package/package.json +1 -1
  98. package/skills/metabolism/SKILL.md +2 -0
  99. package/skills/research-subscription/SKILL.md +1 -29
  100. package/README.zh.md +0 -494
@@ -1,13 +1,11 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import { buildStateScopeKey, normalizeDeliveryChannelOverride, resolveDeliveryTarget, } from "../research-subscriptions/delivery.js";
3
- import { parseSubscribeOptions } from "../research-subscriptions/parse.js";
2
+ import { normalizeDeliveryChannelOverride } from "../research-subscriptions/delivery.js";
4
3
  import { createResearchSubscribeHandler, createResearchSubscriptionsHandler, createResearchUnsubscribeHandler, } from "../research-subscriptions.js";
5
- import { getIncrementalStateStatus, recordIncrementalPush } from "../literature/subscription-state.js";
6
4
  import { Result } from "./result.js";
7
5
  export const ScientifyCronToolSchema = Type.Object({
8
- action: Type.Optional(Type.String({
9
- description: 'Action: "upsert" | "list" | "remove". When omitted, tool infers action from parameters.',
10
- })),
6
+ action: Type.String({
7
+ description: 'Action: "upsert" | "list" | "remove".',
8
+ }),
11
9
  scope: Type.Optional(Type.String({
12
10
  description: "Scope key for grouping jobs (e.g. user ID or thread ID). Default: global.",
13
11
  })),
@@ -50,15 +48,6 @@ export const ScientifyCronToolSchema = Type.Object({
50
48
  no_deliver: Type.Optional(Type.Boolean({
51
49
  description: "If true, run in background without push delivery.",
52
50
  })),
53
- metadata_only: Type.Optional(Type.Boolean({
54
- description: "If true, allow metadata-only reading (skip full-text-first strict default). Use only when user explicitly requests it.",
55
- })),
56
- language: Type.Optional(Type.String({
57
- description: 'Optional output language hint: "zh", "en", or "auto".',
58
- })),
59
- run_now: Type.Optional(Type.Boolean({
60
- description: "If true (upsert only), trigger one immediate run after job creation/update; for research tasks, also return a status_json snapshot.",
61
- })),
62
51
  job_id: Type.Optional(Type.String({
63
52
  description: "Specific job id to remove (only used when action=remove).",
64
53
  })),
@@ -72,78 +61,6 @@ function readStringParam(params, key) {
72
61
  const str = String(value).trim();
73
62
  return str.length > 0 ? str : undefined;
74
63
  }
75
- function sanitizeProjectId(raw) {
76
- const trimmed = raw.trim();
77
- if (/^[A-Za-z0-9_-]+$/.test(trimmed))
78
- return trimmed;
79
- const slug = trimmed
80
- .toLowerCase()
81
- .replace(/[^a-z0-9_-]+/g, "-")
82
- .replace(/-+/g, "-")
83
- .replace(/^-|-$/g, "");
84
- return slug || "project";
85
- }
86
- function normalizeScheduleInput(raw, runNow) {
87
- if (!raw)
88
- return undefined;
89
- const trimmed = raw.trim();
90
- if (!trimmed)
91
- return undefined;
92
- const lower = trimmed.toLowerCase();
93
- if (["now", "immediate", "immediately", "right now", "asap", "立即", "马上", "立刻"].includes(lower)) {
94
- // run_now already triggers immediate execution; keep a valid persistent schedule.
95
- return runNow ? "daily 09:00 Asia/Shanghai" : "at 2m";
96
- }
97
- if (/^\d+[smhdw]$/i.test(trimmed)) {
98
- return `at ${trimmed}`;
99
- }
100
- if (/^every\s*hour$/i.test(trimmed) || /^每小时$/u.test(trimmed)) {
101
- return "every 1h";
102
- }
103
- // Guard against `at <past-time>` generated by model/tool callers.
104
- if (lower.startsWith("at ")) {
105
- const when = trimmed.slice(3).trim();
106
- if (when) {
107
- const atMs = Date.parse(when);
108
- if (!Number.isNaN(atMs) && atMs <= Date.now()) {
109
- return runNow ? "daily 09:00 Asia/Shanghai" : "at 2m";
110
- }
111
- }
112
- }
113
- return trimmed;
114
- }
115
- function inferAction(params) {
116
- const raw = readStringParam(params, "action")?.toLowerCase();
117
- if (raw) {
118
- if (["upsert", "create", "add", "set", "update", "start", "schedule", "new", "insert"].includes(raw)) {
119
- return "upsert";
120
- }
121
- if (["list", "show", "ls", "status"].includes(raw)) {
122
- return "list";
123
- }
124
- if (["remove", "delete", "cancel", "rm", "unsubscribe"].includes(raw)) {
125
- return "remove";
126
- }
127
- }
128
- const hasJobId = Boolean(readStringParam(params, "job_id"));
129
- const hasUpsertSignals = Boolean(readStringParam(params, "schedule")) ||
130
- Boolean(readStringParam(params, "topic")) ||
131
- Boolean(readStringParam(params, "message")) ||
132
- Boolean(readStringParam(params, "project")) ||
133
- readBooleanParam(params, "run_now") ||
134
- readBooleanParam(params, "no_deliver") ||
135
- readBooleanParam(params, "metadata_only") ||
136
- readNumberParam(params, "max_papers") !== undefined ||
137
- readNumberParam(params, "recency_days") !== undefined ||
138
- readNumberParam(params, "candidate_pool") !== undefined ||
139
- Boolean(readStringParam(params, "channel")) ||
140
- Boolean(readStringParam(params, "to"));
141
- if (hasUpsertSignals)
142
- return "upsert";
143
- if (hasJobId)
144
- return "remove";
145
- return "list";
146
- }
147
64
  function readBooleanParam(params, key) {
148
65
  return params[key] === true;
149
66
  }
@@ -186,15 +103,6 @@ function normalizeScope(raw) {
186
103
  .replace(/^-|-$/g, "");
187
104
  return base || "global";
188
105
  }
189
- function parseJobIdFromResultText(text) {
190
- const fenced = text.match(/Job ID:\s*`([^`]+)`/i);
191
- if (fenced?.[1])
192
- return fenced[1].trim();
193
- const plain = text.match(/Job ID:\s*([a-z0-9-]{8,})/i);
194
- if (plain?.[1])
195
- return plain[1].trim();
196
- return undefined;
197
- }
198
106
  function buildToolContext(scope, args, commandBody) {
199
107
  return {
200
108
  senderId: `tool_${scope}`,
@@ -203,13 +111,18 @@ function buildToolContext(scope, args, commandBody) {
203
111
  args,
204
112
  commandBody,
205
113
  config: {},
114
+ requestConversationBinding: async () => ({ status: "error", message: "not available in tool context" }),
115
+ detachConversationBinding: async () => ({ removed: false }),
116
+ getCurrentConversationBinding: async () => null,
206
117
  };
207
118
  }
208
119
  function getResultText(result) {
209
- return result.text ?? result.error ?? "";
120
+ return result.text ?? "";
210
121
  }
211
122
  function getResultError(result) {
212
- const maybe = result.error?.trim();
123
+ if (!result.isError)
124
+ return undefined;
125
+ const maybe = result.text?.trim();
213
126
  return maybe && maybe.length > 0 ? maybe : undefined;
214
127
  }
215
128
  function shouldPromoteMessageToTopic(message) {
@@ -235,272 +148,26 @@ function deriveTopicFromResearchMessage(message) {
235
148
  const normalized = text.trim();
236
149
  return normalized.length > 0 ? normalized : message.trim();
237
150
  }
238
- function resolveTopicAndMessage(params) {
151
+ function buildSubscribeArgs(params) {
152
+ const parts = [];
153
+ const schedule = readStringParam(params, "schedule") ?? "daily 09:00 Asia/Shanghai";
154
+ parts.push(schedule);
239
155
  let topic = readStringParam(params, "topic");
240
156
  let message = readStringParam(params, "message");
241
157
  if (!topic && message && shouldPromoteMessageToTopic(message)) {
242
158
  topic = deriveTopicFromResearchMessage(message);
243
159
  message = undefined;
244
160
  }
245
- return { topic, message };
246
- }
247
- function parseIncrementalScopeFromResultText(text) {
248
- const fenced = text.match(/Incremental Scope:\s*`([^`]+)`/i);
249
- if (fenced?.[1])
250
- return fenced[1].trim();
251
- const plain = text.match(/Incremental Scope:\s*([^\n]+)/i);
252
- if (plain?.[1])
253
- return plain[1].trim();
254
- return undefined;
255
- }
256
- function latestRunId(status) {
257
- return status?.recentChangeStats[0]?.runId;
258
- }
259
- function lastRunAtMs(status) {
260
- return status?.knowledgeStateSummary?.lastRunAtMs ?? 0;
261
- }
262
- function lastPushedAtMs(status) {
263
- return status?.lastPushedAtMs ?? 0;
264
- }
265
- function hasFreshRun(before, after, runStartedAtMs) {
266
- const beforeRunId = latestRunId(before);
267
- const afterRunId = latestRunId(after);
268
- const hasFreshTimestamp = lastRunAtMs(after) >= runStartedAtMs || lastPushedAtMs(after) >= runStartedAtMs;
269
- if (!before) {
270
- return hasFreshTimestamp;
271
- }
272
- if (after.totalRuns > before.totalRuns)
273
- return true;
274
- if (afterRunId && beforeRunId && afterRunId !== beforeRunId)
275
- return true;
276
- if (!beforeRunId && afterRunId)
277
- return true;
278
- if (lastRunAtMs(after) > lastRunAtMs(before))
279
- return true;
280
- if (lastPushedAtMs(after) > lastPushedAtMs(before))
281
- return true;
282
- if (hasFreshTimestamp &&
283
- lastRunAtMs(before) < runStartedAtMs &&
284
- lastPushedAtMs(before) < runStartedAtMs) {
285
- return true;
286
- }
287
- return false;
288
- }
289
- function uniqueScopeCandidates(candidates) {
290
- const seen = new Set();
291
- const result = [];
292
- for (const candidate of candidates) {
293
- if (!candidate)
294
- continue;
295
- const normalized = normalizeScope(candidate);
296
- if (!normalized || seen.has(normalized))
297
- continue;
298
- seen.add(normalized);
299
- result.push(normalized);
300
- }
301
- return result;
302
- }
303
- function buildFallbackRunId(jobId) {
304
- const ts = new Date().toISOString().replace(/[-:.]/g, "").replace("T", "t").replace("Z", "z");
305
- return `cron-${jobId}-${ts}-autofallback`;
306
- }
307
- function sleep(ms) {
308
- return new Promise((resolve) => setTimeout(resolve, ms));
309
- }
310
- function parseCronRunMarker(raw) {
311
- const text = (raw ?? "").trim();
312
- if (!text)
313
- return undefined;
314
- try {
315
- const parsed = JSON.parse(text);
316
- return {
317
- ok: typeof parsed.ok === "boolean" ? parsed.ok : undefined,
318
- ran: typeof parsed.ran === "boolean" ? parsed.ran : undefined,
319
- reason: typeof parsed.reason === "string" ? parsed.reason : undefined,
320
- };
321
- }
322
- catch {
323
- return undefined;
324
- }
325
- }
326
- function serializeRunStatusSnapshot(status) {
327
- const projectRecentPapers = status.knowledgeStateSummary?.recentPapers ?? [];
328
- const globalById = new Map(status.recentPapers.map((paper) => [paper.id, paper]));
329
- return {
330
- scope: status.scope,
331
- topic: status.topic,
332
- topic_key: status.topicKey,
333
- known_paper_count: status.knownPaperCount,
334
- total_runs: status.totalRuns,
335
- last_status: status.lastStatus ?? null,
336
- last_pushed_at_ms: status.lastPushedAtMs ?? null,
337
- latest_run_id: status.recentChangeStats[0]?.runId ?? null,
338
- knowledge_state_summary: status.knowledgeStateSummary
339
- ? {
340
- project_id: status.knowledgeStateSummary.projectId,
341
- stream_key: status.knowledgeStateSummary.streamKey,
342
- run_profile: status.knowledgeStateSummary.runProfile,
343
- total_runs: status.knowledgeStateSummary.totalRuns,
344
- total_hypotheses: status.knowledgeStateSummary.totalHypotheses,
345
- knowledge_topics_count: status.knowledgeStateSummary.knowledgeTopicsCount,
346
- paper_notes_count: status.knowledgeStateSummary.paperNotesCount,
347
- trigger_state: {
348
- consecutive_new_revise_days: status.knowledgeStateSummary.triggerState.consecutiveNewReviseDays,
349
- bridge_count_7d: status.knowledgeStateSummary.triggerState.bridgeCount7d,
350
- unread_core_backlog: status.knowledgeStateSummary.triggerState.unreadCoreBacklog,
351
- last_updated_at_ms: status.knowledgeStateSummary.triggerState.lastUpdatedAtMs,
352
- },
353
- quality_gate: {
354
- mode: status.knowledgeStateSummary.qualityGate.mode,
355
- severity: status.knowledgeStateSummary.qualityGate.severity,
356
- warnings: status.knowledgeStateSummary.qualityGate.warnings,
357
- fatal_reasons: status.knowledgeStateSummary.qualityGate.fatalReasons,
358
- blocking: status.knowledgeStateSummary.qualityGate.blocking,
359
- passed: status.knowledgeStateSummary.qualityGate.passed,
360
- full_text_coverage_pct: status.knowledgeStateSummary.qualityGate.fullTextCoveragePct,
361
- evidence_binding_rate_pct: status.knowledgeStateSummary.qualityGate.evidenceBindingRatePct,
362
- citation_error_rate_pct: status.knowledgeStateSummary.qualityGate.citationErrorRatePct,
363
- reasons: status.knowledgeStateSummary.qualityGate.reasons,
364
- },
365
- hypothesis_gate: {
366
- accepted: status.knowledgeStateSummary.hypothesisGate.accepted,
367
- rejected: status.knowledgeStateSummary.hypothesisGate.rejected,
368
- rejection_reasons: status.knowledgeStateSummary.hypothesisGate.rejectionReasons,
369
- },
370
- last_run_at_ms: status.knowledgeStateSummary.lastRunAtMs ?? null,
371
- last_status: status.knowledgeStateSummary.lastStatus ?? null,
372
- recent_hypotheses: status.knowledgeStateSummary.recentHypotheses.map((item) => ({
373
- id: item.id,
374
- statement: item.statement,
375
- trigger: item.trigger,
376
- created_at_ms: item.createdAtMs,
377
- file: item.file,
378
- strict_overall_score: typeof item.strictOverallScore === "number" ? item.strictOverallScore : null,
379
- strict_decision: item.strictDecision ?? null,
380
- })),
381
- last_reflection_tasks: status.knowledgeStateSummary.lastReflectionTasks,
382
- }
383
- : null,
384
- recent_hypotheses: status.recentHypotheses.map((item) => ({
385
- id: item.id,
386
- statement: item.statement,
387
- trigger: item.trigger,
388
- created_at_ms: item.createdAtMs,
389
- file: item.file,
390
- strict_overall_score: typeof item.strictOverallScore === "number" ? item.strictOverallScore : null,
391
- strict_decision: item.strictDecision ?? null,
392
- })),
393
- recent_change_stats: status.recentChangeStats.map((item) => ({
394
- day: item.day,
395
- run_id: item.runId,
396
- new_count: item.newCount,
397
- confirm_count: item.confirmCount,
398
- revise_count: item.reviseCount,
399
- bridge_count: item.bridgeCount,
400
- })),
401
- recent_papers: (projectRecentPapers.length > 0 ? projectRecentPapers : status.recentPapers).map((paper) => {
402
- const paperId = typeof paper.id === "string" ? paper.id : "";
403
- const fromGlobal = paperId ? globalById.get(paperId) : undefined;
404
- return {
405
- id: paperId || null,
406
- title: paper.title ?? null,
407
- url: paper.url ?? null,
408
- last_score: "lastScore" in paper && typeof paper.lastScore === "number"
409
- ? paper.lastScore
410
- : "score" in paper && typeof paper.score === "number"
411
- ? paper.score
412
- : fromGlobal?.lastScore ?? null,
413
- last_reason: "lastReason" in paper && typeof paper.lastReason === "string"
414
- ? paper.lastReason
415
- : "reason" in paper && typeof paper.reason === "string"
416
- ? paper.reason
417
- : fromGlobal?.lastReason ?? null,
418
- first_pushed_at_ms: "firstPushedAtMs" in paper && typeof paper.firstPushedAtMs === "number"
419
- ? paper.firstPushedAtMs
420
- : fromGlobal?.firstPushedAtMs ?? null,
421
- last_pushed_at_ms: "lastPushedAtMs" in paper && typeof paper.lastPushedAtMs === "number"
422
- ? paper.lastPushedAtMs
423
- : fromGlobal?.lastPushedAtMs ?? null,
424
- push_count: "pushCount" in paper && typeof paper.pushCount === "number"
425
- ? paper.pushCount
426
- : fromGlobal?.pushCount ?? null,
427
- full_text_read: "fullTextRead" in paper && typeof paper.fullTextRead === "boolean"
428
- ? paper.fullTextRead
429
- : null,
430
- read_status: "readStatus" in paper && typeof paper.readStatus === "string"
431
- ? paper.readStatus
432
- : null,
433
- unread_reason: "unreadReason" in paper && typeof paper.unreadReason === "string"
434
- ? paper.unreadReason
435
- : null,
436
- evidence_anchors: "evidenceAnchors" in paper && Array.isArray(paper.evidenceAnchors)
437
- ? paper.evidenceAnchors.map((anchor) => ({
438
- section: anchor && typeof anchor === "object" && "section" in anchor && typeof anchor.section === "string"
439
- ? anchor.section
440
- : null,
441
- locator: anchor && typeof anchor === "object" && "locator" in anchor && typeof anchor.locator === "string"
442
- ? anchor.locator
443
- : null,
444
- claim: anchor && typeof anchor === "object" && "claim" in anchor && typeof anchor.claim === "string"
445
- ? anchor.claim
446
- : null,
447
- quote: anchor && typeof anchor === "object" && "quote" in anchor && typeof anchor.quote === "string"
448
- ? anchor.quote
449
- : null,
450
- }))
451
- : [],
452
- };
453
- }),
454
- global_recent_papers: status.recentPapers.map((paper) => ({
455
- id: paper.id,
456
- title: paper.title ?? null,
457
- url: paper.url ?? null,
458
- last_score: paper.lastScore ?? null,
459
- last_reason: paper.lastReason ?? null,
460
- first_pushed_at_ms: paper.firstPushedAtMs,
461
- last_pushed_at_ms: paper.lastPushedAtMs,
462
- push_count: paper.pushCount,
463
- })),
464
- knowledge_state_missing_reason: status.knowledgeStateMissingReason ?? null,
465
- };
466
- }
467
- function buildSubscribeArgs(params) {
468
- const parts = [];
469
- const schedule = normalizeScheduleInput(readStringParam(params, "schedule"), readBooleanParam(params, "run_now")) ??
470
- "daily 09:00 Asia/Shanghai";
471
- parts.push(schedule);
472
- const resolved = resolveTopicAndMessage(params);
473
- const topic = resolved.topic;
474
- const message = resolved.message;
475
- const scope = readStringParam(params, "scope");
476
161
  if (topic) {
477
162
  parts.push("--topic", quoteArg(topic));
478
163
  }
479
164
  const project = readStringParam(params, "project");
480
165
  if (project) {
481
- parts.push("--project", quoteArg(sanitizeProjectId(project)));
166
+ parts.push("--project", quoteArg(project));
482
167
  }
483
168
  if (message) {
484
169
  parts.push("--message", quoteArg(message));
485
170
  }
486
- const explicitLanguage = readStringParam(params, "language");
487
- const languageCandidate = (() => {
488
- if (explicitLanguage && ["zh", "en", "auto"].includes(explicitLanguage.toLowerCase())) {
489
- return explicitLanguage.toLowerCase();
490
- }
491
- const channel = readStringParam(params, "channel");
492
- if (channel && channel.toLowerCase() === "feishu")
493
- return "zh";
494
- if (scope && scope.toLowerCase().includes("feishu"))
495
- return "zh";
496
- const text = `${topic ?? ""} ${message ?? ""}`.trim();
497
- if (/[\p{Script=Han}]/u.test(text))
498
- return "zh";
499
- return undefined;
500
- })();
501
- if (languageCandidate) {
502
- parts.push("--language", quoteArg(languageCandidate));
503
- }
504
171
  const maxPapers = readNumberParam(params, "max_papers");
505
172
  if (maxPapers !== undefined) {
506
173
  parts.push("--max-papers", String(Math.floor(maxPapers)));
@@ -538,13 +205,6 @@ function buildSubscribeArgs(params) {
538
205
  if (readBooleanParam(params, "no_deliver")) {
539
206
  parts.push("--no-deliver");
540
207
  }
541
- if (readBooleanParam(params, "metadata_only")) {
542
- parts.push("--metadata-only");
543
- }
544
- const language = readStringParam(params, "language");
545
- if (language && ["zh", "en", "auto"].includes(language.toLowerCase())) {
546
- parts.push("--language", quoteArg(language.toLowerCase()));
547
- }
548
208
  return parts.join(" ");
549
209
  }
550
210
  export function createScientifyCronTool(deps) {
@@ -558,184 +218,19 @@ export function createScientifyCronTool(deps) {
558
218
  parameters: ScientifyCronToolSchema,
559
219
  execute: async (_toolCallId, rawArgs) => {
560
220
  const params = rawArgs;
561
- const action = inferAction(params);
562
- if (!action) {
563
- return Result.err("invalid_params", 'Unable to infer action. Use one of: action="upsert" | "list" | "remove".');
564
- }
221
+ const action = (readStringParam(params, "action") ?? "").toLowerCase();
565
222
  const scope = normalizeScope(readStringParam(params, "scope"));
566
223
  try {
567
224
  if (action === "upsert") {
568
- // In tool context, delivery target may be unavailable unless caller explicitly sets channel/to.
569
- // Default to no-deliver when delivery is unspecified to avoid hard failure on creation.
570
- const hasDeliveryHints = Boolean(readStringParam(params, "channel")) || Boolean(readStringParam(params, "to"));
571
- const upsertParams = readBooleanParam(params, "no_deliver") || hasDeliveryHints ? params : { ...params, no_deliver: true };
572
- const args = buildSubscribeArgs(upsertParams);
225
+ const args = buildSubscribeArgs(params);
573
226
  const ctx = buildToolContext(scope, args, `/research-subscribe ${args}`);
574
- let expectedStateScopeKey;
575
- try {
576
- const parsed = parseSubscribeOptions(args);
577
- if (!("error" in parsed)) {
578
- const delivery = resolveDeliveryTarget(ctx, parsed);
579
- if (!("error" in delivery)) {
580
- expectedStateScopeKey = buildStateScopeKey(ctx, delivery);
581
- }
582
- }
583
- }
584
- catch {
585
- // keep best-effort behavior; fallback to parsed text scope or caller scope
586
- }
587
227
  const res = await subscribe(ctx);
588
228
  const err = getResultError(res);
589
229
  if (err) {
590
230
  return Result.err("operation_failed", err);
591
231
  }
592
232
  const text = getResultText(res);
593
- const jobId = parseJobIdFromResultText(text);
594
- const resolved = resolveTopicAndMessage(upsertParams);
595
- const incrementalScope = parseIncrementalScopeFromResultText(text);
596
- const project = readStringParam(upsertParams, "project");
597
- const runNow = readBooleanParam(upsertParams, "run_now");
598
- if (runNow && jobId) {
599
- const statusScopeCandidates = uniqueScopeCandidates([expectedStateScopeKey, incrementalScope, scope]);
600
- const beforeStatusByScope = new Map();
601
- if (resolved.topic) {
602
- for (const statusScope of statusScopeCandidates) {
603
- const before = await getIncrementalStateStatus({
604
- scope: statusScope,
605
- topic: resolved.topic,
606
- ...(project ? { projectId: project } : {}),
607
- }).catch(() => undefined);
608
- beforeStatusByScope.set(statusScope, before);
609
- }
610
- }
611
- const runArgsPrimary = [
612
- "openclaw",
613
- "cron",
614
- "run",
615
- jobId,
616
- "--expect-final",
617
- "--timeout",
618
- "900000",
619
- ];
620
- const runStartedAtMs = Date.now();
621
- let runRes = await deps.runtime.system.runCommandWithTimeout(runArgsPrimary, {
622
- timeoutMs: 920_000,
623
- });
624
- if (runRes.code !== 0 &&
625
- /unknown option '--expect-final'|unknown option \"--expect-final\"|unknown option\s+--expect-final/i.test(runRes.stderr || "")) {
626
- // Backward compatibility for older OpenClaw versions.
627
- runRes = await deps.runtime.system.runCommandWithTimeout(["openclaw", "cron", "run", jobId], { timeoutMs: 600_000 });
628
- }
629
- let runAlreadyInProgress = false;
630
- if (runRes.code !== 0) {
631
- const marker = parseCronRunMarker(runRes.stdout) ?? parseCronRunMarker(runRes.stderr);
632
- if (marker?.ok === true && marker?.ran === false && marker?.reason === "already-running") {
633
- runAlreadyInProgress = true;
634
- }
635
- else {
636
- return Result.err("operation_failed", runRes.stderr || runRes.stdout || `cron run failed for job ${jobId}`);
637
- }
638
- }
639
- let statusSnapshot;
640
- if (resolved.topic) {
641
- try {
642
- let status;
643
- let statusScopeUsed;
644
- const deadline = Date.now() + (runAlreadyInProgress ? 300_000 : 120_000);
645
- while (Date.now() <= deadline) {
646
- for (const statusScope of statusScopeCandidates) {
647
- const fetched = await getIncrementalStateStatus({
648
- scope: statusScope,
649
- topic: resolved.topic,
650
- ...(project ? { projectId: project } : {}),
651
- }).catch(() => undefined);
652
- const before = beforeStatusByScope.get(statusScope);
653
- if (fetched && hasFreshRun(before, fetched, runStartedAtMs)) {
654
- status = fetched;
655
- statusScopeUsed = statusScope;
656
- break;
657
- }
658
- }
659
- if (status) {
660
- break;
661
- }
662
- await sleep(1_000);
663
- }
664
- if (!status) {
665
- const fallbackError = "run_now completed but no new persisted research run was detected. Auto-persisted fallback error run.";
666
- try {
667
- const fallbackPersistErrors = [];
668
- for (const [idx, statusScope] of statusScopeCandidates.entries()) {
669
- const persisted = await recordIncrementalPush({
670
- scope: statusScope,
671
- topic: resolved.topic,
672
- ...(project ? { projectId: project } : {}),
673
- status: "error",
674
- runId: `${buildFallbackRunId(jobId)}-${idx + 1}`,
675
- note: fallbackError,
676
- papers: [],
677
- knowledgeState: {
678
- corePapers: [],
679
- explorationPapers: [],
680
- explorationTrace: [],
681
- knowledgeChanges: [],
682
- knowledgeUpdates: [],
683
- hypotheses: [],
684
- runLog: {
685
- runProfile: readBooleanParam(upsertParams, "metadata_only") ? "fast" : "strict",
686
- error: "run_now completed but the agent turn did not persist via scientify_literature_state.record",
687
- notes: "Fallback persisted by scientify_cron_job guard to avoid stale status response.",
688
- tempCleanupStatus: "not_needed",
689
- },
690
- },
691
- }).catch((persistError) => {
692
- fallbackPersistErrors.push(`${statusScope}:${persistError instanceof Error ? persistError.message : String(persistError)}`);
693
- return undefined;
694
- });
695
- if (!persisted)
696
- continue;
697
- const fetched = await getIncrementalStateStatus({
698
- scope: statusScope,
699
- topic: resolved.topic,
700
- ...(project ? { projectId: project } : {}),
701
- }).catch(() => undefined);
702
- const before = beforeStatusByScope.get(statusScope);
703
- if (fetched && hasFreshRun(before, fetched, runStartedAtMs)) {
704
- status = fetched;
705
- statusScopeUsed = statusScope;
706
- break;
707
- }
708
- }
709
- if (!status) {
710
- return Result.err("operation_failed", `${fallbackError} fresh_status_unavailable_after_fallback_persist. scopes=${statusScopeCandidates.join(",")} errors=${fallbackPersistErrors.join(" | ") || "none"}`);
711
- }
712
- }
713
- catch (persistError) {
714
- return Result.err("operation_failed", `run_now completed but no new persisted research run was detected. Refusing stale status response. fallback_persist_error=${persistError instanceof Error ? persistError.message : String(persistError)}`);
715
- }
716
- }
717
- statusSnapshot = {
718
- ...serializeRunStatusSnapshot(status),
719
- status_scope_used: statusScopeUsed ?? null,
720
- };
721
- }
722
- catch (statusError) {
723
- statusSnapshot = {
724
- error: statusError instanceof Error ? statusError.message : String(statusError),
725
- };
726
- }
727
- }
728
- return Result.ok({
729
- action,
730
- scope,
731
- job_id: jobId,
732
- run_now: true,
733
- run_status: runRes.code === 0 ? "ok" : "already-running",
734
- ...(statusSnapshot ? { status_json: statusSnapshot } : {}),
735
- result: text,
736
- });
737
- }
738
- return Result.ok({ action, scope, ...(jobId ? { job_id: jobId } : {}), result: text });
233
+ return Result.ok({ action, scope, result: text });
739
234
  }
740
235
  if (action === "list") {
741
236
  const ctx = buildToolContext(scope, "", "/research-subscriptions");