openclaw-telegram-manager 2.5.7 → 2.6.1

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.
package/dist/plugin.js CHANGED
@@ -8815,7 +8815,7 @@ __export(value_exports2, {
8815
8815
  });
8816
8816
 
8817
8817
  // src/lib/types.ts
8818
- var CURRENT_REGISTRY_VERSION = 4;
8818
+ var CURRENT_REGISTRY_VERSION = 5;
8819
8819
  var CAPSULE_VERSION = 3;
8820
8820
  var MAX_POST_ERROR_LENGTH = 500;
8821
8821
  var MAX_NAME_LENGTH = 100;
@@ -8832,7 +8832,7 @@ var TopicTypeSchema = Type.Union([
8832
8832
  Type.Literal("coding"),
8833
8833
  Type.Literal("research"),
8834
8834
  Type.Literal("marketing"),
8835
- Type.Literal("custom")
8835
+ Type.Literal("general")
8836
8836
  ]);
8837
8837
  var TopicStatusSchema = Type.Union([
8838
8838
  Type.Literal("active"),
@@ -8870,7 +8870,7 @@ var OVERLAY_FILES = {
8870
8870
  coding: ["ARCHITECTURE.md", "DEPLOY.md"],
8871
8871
  research: ["SOURCES.md", "FINDINGS.md"],
8872
8872
  marketing: ["CAMPAIGNS.md", "METRICS.md"],
8873
- custom: []
8873
+ general: []
8874
8874
  };
8875
8875
  var BASE_FILES = [
8876
8876
  "README.md",
@@ -8931,6 +8931,17 @@ var migrations = {
8931
8931
  }
8932
8932
  }
8933
8933
  return data;
8934
+ },
8935
+ "4_to_5": (data) => {
8936
+ const topics = data["topics"];
8937
+ if (topics && typeof topics === "object" && !Array.isArray(topics)) {
8938
+ for (const entry of Object.values(topics)) {
8939
+ if (entry["type"] === "custom") {
8940
+ entry["type"] = "general";
8941
+ }
8942
+ }
8943
+ }
8944
+ return data;
8934
8945
  }
8935
8946
  };
8936
8947
  function migrateRegistry(data) {
@@ -9392,7 +9403,7 @@ function buildInitTypeButtons(groupId, threadId, secret, userId) {
9392
9403
  ],
9393
9404
  [
9394
9405
  { text: "Marketing", callback_data: cb("im") },
9395
- { text: "Custom", callback_data: cb("ix") }
9406
+ { text: "General", callback_data: cb("ig") }
9396
9407
  ]
9397
9408
  ]);
9398
9409
  }
@@ -9401,7 +9412,7 @@ function buildInitConfirmButton(groupId, threadId, secret, userId, type) {
9401
9412
  coding: "yc",
9402
9413
  research: "yr",
9403
9414
  marketing: "ym",
9404
- custom: "yx"
9415
+ general: "yg"
9405
9416
  };
9406
9417
  const cb = buildCallbackData(actionMap[type], groupId, threadId, secret, userId);
9407
9418
  return buildInlineKeyboard([[{ text: "Use this name", callback_data: cb }]]);
@@ -9428,7 +9439,7 @@ function buildInitWelcomeHtml() {
9428
9439
  "\u2022 <b>Coding</b> \u2014 tracks architecture decisions and deployment steps",
9429
9440
  "\u2022 <b>Research</b> \u2014 tracks sources and key findings",
9430
9441
  "\u2022 <b>Marketing</b> \u2014 tracks campaigns and metrics",
9431
- "\u2022 <b>Custom</b> \u2014 general-purpose tracking",
9442
+ "\u2022 <b>General</b> \u2014 general-purpose tracking",
9432
9443
  "",
9433
9444
  "<i>The AI may take a few seconds to respond \u2014 no need to tap twice.</i>"
9434
9445
  ].join("\n");
@@ -9526,9 +9537,7 @@ function buildHelpCard() {
9526
9537
  "/tm rename new-name \u2014 rename this topic",
9527
9538
  "/tm snooze 7d \u2014 pause health checks (e.g. 7d, 30d)",
9528
9539
  "/tm archive \u2014 archive this topic",
9529
- "/tm unarchive \u2014 bring back an archived topic",
9530
- "/tm sync \u2014 fix config if out of sync",
9531
- "/tm upgrade \u2014 update topic files to latest version"
9540
+ "/tm unarchive \u2014 bring back an archived topic"
9532
9541
  ].join("\n");
9533
9542
  }
9534
9543
  function buildListMessage(topics) {
@@ -9544,8 +9553,8 @@ function buildListMessage(topics) {
9544
9553
  let rendered = 0;
9545
9554
  for (const t of sorted) {
9546
9555
  const activity = t.lastMessageAt ? `active ${relativeTime(t.lastMessageAt)}` : "no activity yet";
9547
- const statusTag = t.status !== "active" ? ` \u2014 ${t.status}` : "";
9548
- const entry = `**${t.name}** \xB7 ${t.type}${statusTag}
9556
+ const icon = t.status === "snoozed" ? "\u{1F4A4} " : t.status === "archived" ? "\u{1F4E6} " : "";
9557
+ const entry = `${icon}**${t.name}** \xB7 ${t.type}
9549
9558
  ${activity}`;
9550
9559
  const tentative = [...lines, entry, ""].join("\n");
9551
9560
  if (tentative.length > TELEGRAM_MSG_LIMIT - 40) {
@@ -9628,12 +9637,22 @@ var FILE_MODE3 = 384;
9628
9637
  function getSystemPromptTemplate(name, slug, absoluteWorkspacePath) {
9629
9638
  return `You are the assistant for the Telegram topic: ${name}.
9630
9639
 
9640
+ Context hierarchy (strictly ordered):
9641
+ 1. TOPIC FILES (primary) \u2014 your project folder at: ${absoluteWorkspacePath}/projects/${slug}/
9642
+ These define WHAT this topic is working on: current tasks, status, goals, history.
9643
+ When asked "what are we working on?", answer ONLY from these files.
9644
+ 2. WORKSPACE MEMORY (secondary) \u2014 files like memory/, MEMORY.md at the workspace root.
9645
+ These contain general learnings, user preferences, and cross-topic knowledge.
9646
+ Use them for HOW to work (patterns, mistakes to avoid, coding style) \u2014 but
9647
+ never to determine what this topic is about or what its current tasks are.
9648
+ Workspace memory may reference projects belonging to OTHER topics \u2014 ignore those.
9649
+
9631
9650
  Determinism rules:
9632
- - Source of truth is the project folder at: ${absoluteWorkspacePath}/projects/${slug}/
9633
- - After /reset, /new, or context compaction: ALWAYS re-read STATUS.md,
9634
- then TODO.md, then LEARNINGS.md (last 20 entries), then COMMANDS.md
9635
- before continuing work. Do not rely on summarized memory for paths,
9636
- commands, or task state.
9651
+ - After /reset, /new, or context compaction: ALWAYS re-read your topic files
9652
+ first \u2014 STATUS.md, then TODO.md, then LEARNINGS.md (last 20 entries), then
9653
+ COMMANDS.md \u2014 before doing anything else. These are your ground truth.
9654
+ Do not rely on summarized memory or workspace-level files for this topic's
9655
+ tasks, status, or goals.
9637
9656
  - Before context compaction or when the conversation is long: proactively
9638
9657
  flush current progress to STATUS.md (update "Last done (UTC)" and
9639
9658
  "Next actions (now)") so compaction cannot erase critical state.
@@ -9656,8 +9675,11 @@ Learning capture:
9656
9675
  - If LEARNINGS.md exceeds ~200 lines, archive older entries to LEARNINGS-archive.md.
9657
9676
 
9658
9677
  Separation:
9659
- - Your workspace is strictly projects/${slug}/. Do not read, write, or reference
9660
- files in any other topic's project directory.
9678
+ - Your project folder is strictly projects/${slug}/. This is your identity.
9679
+ - Do not read, write, or reference files in any other topic's project directory.
9680
+ - Workspace-level files (memory/, MEMORY.md, projects-index, etc.) are shared
9681
+ context \u2014 you may read them for general knowledge and learnings, but they
9682
+ do not define this topic's current work, goals, or identity.
9661
9683
  - If the user mentions another topic by name or slug, ask for explicit
9662
9684
  confirmation before mixing work: "This references topic X \u2014 switch context?"
9663
9685
  - Never copy data between topic folders without explicit user instruction.
@@ -9973,7 +9995,7 @@ async function handleStatus(ctx) {
9973
9995
  }
9974
9996
 
9975
9997
  // src/commands/init.ts
9976
- var VALID_TYPES = /* @__PURE__ */ new Set(["coding", "research", "marketing", "custom"]);
9998
+ var VALID_TYPES = /* @__PURE__ */ new Set(["coding", "research", "marketing", "general"]);
9977
9999
  function deriveTopicName(nameArg, messageContext, threadId) {
9978
10000
  const topicTitle = messageContext?.["topicTitle"] ?? "";
9979
10001
  let name;
@@ -10120,13 +10142,12 @@ Warning: config sync failed: ${msg}`;
10120
10142
  try {
10121
10143
  const htmlCard = buildTopicCardHtml(name, topicType);
10122
10144
  await ctx.postFn(groupId, threadId, htmlCard);
10123
- return { text: "", pin: true };
10145
+ return { text: "" };
10124
10146
  } catch {
10125
10147
  }
10126
10148
  }
10127
10149
  return {
10128
- text: `${topicCard}${restartMsg}`,
10129
- pin: true
10150
+ text: `${topicCard}${restartMsg}`
10130
10151
  };
10131
10152
  }
10132
10153
  async function handleInitInteractive(ctx, args) {
@@ -10740,14 +10761,14 @@ function readFileOrNull(filePath) {
10740
10761
  }
10741
10762
  }
10742
10763
  function extractDoneSection(statusContent) {
10743
- if (!statusContent) return "_No status available yet._";
10764
+ if (!statusContent) return "No status available yet.";
10744
10765
  const match = statusContent.match(/^##\s*Last done\s*\(UTC\)\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im);
10745
- if (!match) return '_No "Last done" section found._';
10766
+ if (!match) return "No recent activity found.";
10746
10767
  const text = match[1]?.trim();
10747
- return text || "_Empty._";
10768
+ return text || "Empty.";
10748
10769
  }
10749
10770
  function extractTodayLearnings(learningsContent) {
10750
- if (!learningsContent) return "_No learnings recorded yet._";
10771
+ if (!learningsContent) return "No learnings recorded yet.";
10751
10772
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10752
10773
  const lines = learningsContent.split("\n");
10753
10774
  const todayLines = [];
@@ -10764,32 +10785,32 @@ function extractTodayLearnings(learningsContent) {
10764
10785
  todayLines.push(line);
10765
10786
  }
10766
10787
  }
10767
- return todayLines.length > 0 ? todayLines.join("\n") : "_None today._";
10788
+ return todayLines.length > 0 ? todayLines.join("\n") : "None today.";
10768
10789
  }
10769
10790
  function extractBlockers(todoContent) {
10770
- if (!todoContent) return "_No tasks recorded yet._";
10791
+ if (!todoContent) return "No tasks recorded yet.";
10771
10792
  const lines = todoContent.split("\n");
10772
10793
  const blockerLines = lines.filter(
10773
10794
  (l) => /\[BLOCKED\]/i.test(l) || /\bblocked\b/i.test(l)
10774
10795
  );
10775
- return blockerLines.length > 0 ? blockerLines.join("\n") : "_None._";
10796
+ return blockerLines.length > 0 ? blockerLines.join("\n") : "None.";
10776
10797
  }
10777
10798
  function extractNextActions(statusContent) {
10778
- if (!statusContent) return "_No status available yet._";
10799
+ if (!statusContent) return "No status available yet.";
10779
10800
  const match = statusContent.match(/^##\s*Next (?:3 )?actions(?: \(now\))?\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im);
10780
- if (!match) return '_No "Next actions" section found._';
10801
+ if (!match) return "None yet.";
10781
10802
  const text = match[1]?.trim();
10782
- return text || "_Empty._";
10803
+ return text || "None yet.";
10783
10804
  }
10784
10805
  function extractUpcoming(statusContent) {
10785
- if (!statusContent) return "_No status available yet._";
10806
+ if (!statusContent) return "No status available yet.";
10786
10807
  const match = statusContent.match(/^##\s*Upcoming actions\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im);
10787
- if (!match) return '_No "Upcoming actions" section found._';
10808
+ if (!match) return "None yet.";
10788
10809
  const text = match[1]?.trim();
10789
- return text || "_Empty._";
10810
+ return text || "None yet.";
10790
10811
  }
10791
10812
  function computeHealth(lastMessageAt, statusContent, blockers) {
10792
- if (blockers && blockers !== "_None._" && blockers !== "_No tasks recorded yet._") {
10813
+ if (blockers && blockers !== "None." && blockers !== "No tasks recorded yet.") {
10793
10814
  return "blocked";
10794
10815
  }
10795
10816
  if (!lastMessageAt) return "stale";
@@ -10799,6 +10820,132 @@ function computeHealth(lastMessageAt, statusContent, blockers) {
10799
10820
  }
10800
10821
 
10801
10822
  // src/commands/doctor-all.ts
10823
+ function getEligibility(entry, now, statusTimestamp) {
10824
+ if (entry.status === "archived") return { eligible: false, skipReason: "archived" };
10825
+ if (entry.snoozeUntil && new Date(entry.snoozeUntil).getTime() > now.getTime()) {
10826
+ return { eligible: false, skipReason: "snoozed" };
10827
+ }
10828
+ const lastActive = mostRecent(entry.lastMessageAt, statusTimestamp);
10829
+ if (lastActive) {
10830
+ const lastActiveMs = new Date(lastActive).getTime();
10831
+ const inactiveMs = INACTIVE_AFTER_DAYS * 24 * 60 * 60 * 1e3;
10832
+ if (now.getTime() - lastActiveMs > inactiveMs) {
10833
+ return { eligible: false, skipReason: "inactive" };
10834
+ }
10835
+ }
10836
+ if (entry.lastDoctorReportAt) {
10837
+ const lastReport = new Date(entry.lastDoctorReportAt).getTime();
10838
+ if (now.getTime() - lastReport < DOCTOR_PER_TOPIC_CAP_MS) {
10839
+ return { eligible: false, skipReason: "recently-checked" };
10840
+ }
10841
+ }
10842
+ return { eligible: true };
10843
+ }
10844
+ var SKIP_ICONS = {
10845
+ archived: "\u{1F4E6}",
10846
+ // 📦
10847
+ snoozed: "\u{1F4A4}",
10848
+ // 💤
10849
+ inactive: "\u{1F507}",
10850
+ // 🔇
10851
+ "recently-checked": "\u23F0"
10852
+ // ⏰
10853
+ };
10854
+ var SKIP_LABELS = {
10855
+ archived: "archived",
10856
+ snoozed: "snoozed",
10857
+ inactive: "inactive",
10858
+ "recently-checked": "recently checked"
10859
+ };
10860
+ var SUMMARY_SOFT_LIMIT = 3800;
10861
+ function buildDoctorAllSummary(data) {
10862
+ const {
10863
+ checkedTopics,
10864
+ skippedTopics,
10865
+ postFailures,
10866
+ dailyReportsSent,
10867
+ dailyReportsSkipped,
10868
+ hasPostFn,
10869
+ migrationGroups,
10870
+ errors
10871
+ } = data;
10872
+ if (checkedTopics.length === 0 && skippedTopics.length === 0) {
10873
+ return "**Health Check Summary**\n\nNo topics registered yet.";
10874
+ }
10875
+ const lines = ["**Health Check Summary**", ""];
10876
+ let checkedRendered = 0;
10877
+ for (const t of checkedTopics) {
10878
+ let icon;
10879
+ let label;
10880
+ switch (t.status) {
10881
+ case "checked":
10882
+ icon = "\u2705";
10883
+ label = "checked";
10884
+ break;
10885
+ case "check-failed":
10886
+ icon = "\u274C";
10887
+ label = "check failed";
10888
+ break;
10889
+ case "post-failed":
10890
+ icon = "\u26A0\uFE0F";
10891
+ label = "failed to post";
10892
+ break;
10893
+ }
10894
+ const line = `${icon} ${t.name} \u2014 ${label}`;
10895
+ if (lines.join("\n").length + line.length > SUMMARY_SOFT_LIMIT) {
10896
+ const remaining = checkedTopics.length - checkedRendered;
10897
+ lines.push(`... and ${remaining} more`);
10898
+ break;
10899
+ }
10900
+ lines.push(line);
10901
+ checkedRendered++;
10902
+ }
10903
+ if (skippedTopics.length > 0) {
10904
+ lines.push("");
10905
+ lines.push("\u23ED\uFE0F Skipped:");
10906
+ let skippedRendered = 0;
10907
+ for (const t of skippedTopics) {
10908
+ const icon = SKIP_ICONS[t.reason];
10909
+ const label = SKIP_LABELS[t.reason];
10910
+ const line = `${icon} ${t.name} \u2014 ${label}`;
10911
+ if (lines.join("\n").length + line.length > SUMMARY_SOFT_LIMIT) {
10912
+ const remaining = skippedTopics.length - skippedRendered;
10913
+ lines.push(`... and ${remaining} more`);
10914
+ break;
10915
+ }
10916
+ lines.push(line);
10917
+ skippedRendered++;
10918
+ }
10919
+ }
10920
+ if (postFailures > 0) {
10921
+ lines.push("");
10922
+ lines.push(`\u26A0\uFE0F ${postFailures} topic(s) failed to post`);
10923
+ }
10924
+ if (hasPostFn) {
10925
+ const parts = [];
10926
+ if (dailyReportsSent > 0) parts.push(`${dailyReportsSent} sent`);
10927
+ if (dailyReportsSkipped > 0) parts.push(`${dailyReportsSkipped} skipped`);
10928
+ if (parts.length > 0) {
10929
+ lines.push("");
10930
+ lines.push(`Daily reports: ${parts.join(", ")}`);
10931
+ }
10932
+ }
10933
+ if (migrationGroups > 0) {
10934
+ lines.push("");
10935
+ lines.push(`**Warning:** ${migrationGroups} group(s) had all topics fail. The group may have been migrated or deleted.`);
10936
+ }
10937
+ if (errors.length > 0) {
10938
+ lines.push("");
10939
+ lines.push(`**Errors (${errors.length}):**`);
10940
+ for (const e of errors.slice(0, 10)) {
10941
+ lines.push(`- ${e}`);
10942
+ }
10943
+ if (errors.length > 10) {
10944
+ lines.push(`... and ${errors.length - 10} more`);
10945
+ }
10946
+ }
10947
+ return truncateMessage(lines.join("\n"));
10948
+ }
10802
10949
  async function handleDoctorAll(ctx) {
10803
10950
  const { workspaceDir, configDir, logger } = ctx;
10804
10951
  const registry = readRegistry(workspaceDir);
@@ -10834,12 +10981,16 @@ async function handleDoctorAll(ctx) {
10834
10981
  const allEntries = Object.entries(registry.topics);
10835
10982
  const reports = [];
10836
10983
  const errors = [];
10837
- let processed = 0;
10838
- let skipped = 0;
10984
+ const checkedTopics = [];
10985
+ const skippedTopics = [];
10839
10986
  const groupPostResults = /* @__PURE__ */ new Map();
10840
10987
  for (const [_key, entry] of allEntries) {
10841
- if (!isEligible(entry, now)) {
10842
- skipped++;
10988
+ const capsuleDir = path11.join(projectsBase, entry.slug);
10989
+ const statusForEligibility = readFileOrNull(path11.join(capsuleDir, "STATUS.md"));
10990
+ const statusTs = statusForEligibility ? extractStatusTimestamp(statusForEligibility) : null;
10991
+ const eligibility = getEligibility(entry, now, statusTs);
10992
+ if (!eligibility.eligible) {
10993
+ skippedTopics.push({ name: entry.name, reason: eligibility.skipReason });
10843
10994
  continue;
10844
10995
  }
10845
10996
  try {
@@ -10875,11 +11026,12 @@ async function handleDoctorAll(ctx) {
10875
11026
  }
10876
11027
  const group = groupPostResults.get(gk);
10877
11028
  group.total++;
10878
- processed++;
11029
+ checkedTopics.push({ name: entry.name, slug: entry.slug, status: "checked" });
10879
11030
  } catch (err) {
10880
11031
  const msg = err instanceof Error ? err.message : String(err);
10881
11032
  errors.push(`${entry.slug}: ${msg}`);
10882
11033
  logger.error(`[doctor-all] Error processing ${entry.slug}: ${msg}`);
11034
+ checkedTopics.push({ name: entry.name, slug: entry.slug, status: "check-failed" });
10883
11035
  const gk = entry.groupId;
10884
11036
  if (!groupPostResults.has(gk)) {
10885
11037
  groupPostResults.set(gk, { total: 0, failed: 0 });
@@ -10895,14 +11047,13 @@ async function handleDoctorAll(ctx) {
10895
11047
  migrationGroups.push(gid);
10896
11048
  }
10897
11049
  }
10898
- let postErrors = 0;
10899
- let postSuccesses = 0;
11050
+ let postFailures = 0;
11051
+ const postFailedSlugs = /* @__PURE__ */ new Set();
10900
11052
  if (ctx.postFn && reports.length > 0) {
10901
11053
  const rateLimitedPost = createRateLimitedPoster(ctx.postFn);
10902
11054
  for (const report of reports) {
10903
11055
  try {
10904
11056
  await rateLimitedPost(report.groupId, report.threadId, report.text, report.keyboard);
10905
- postSuccesses++;
10906
11057
  await withRegistry(workspaceDir, (data) => {
10907
11058
  const key = `${report.groupId}:${report.threadId}`;
10908
11059
  const entry = data.topics[key];
@@ -10912,7 +11063,8 @@ async function handleDoctorAll(ctx) {
10912
11063
  }
10913
11064
  });
10914
11065
  } catch (err) {
10915
- postErrors++;
11066
+ postFailures++;
11067
+ postFailedSlugs.add(report.slug);
10916
11068
  const msg = err instanceof Error ? err.message : String(err);
10917
11069
  logger.error(`[doctor-all] Post failed for ${report.slug}: ${msg}`);
10918
11070
  await withRegistry(workspaceDir, (data) => {
@@ -10924,6 +11076,11 @@ async function handleDoctorAll(ctx) {
10924
11076
  });
10925
11077
  }
10926
11078
  }
11079
+ for (const outcome of checkedTopics) {
11080
+ if (postFailedSlugs.has(outcome.slug) && outcome.status === "checked") {
11081
+ outcome.status = "post-failed";
11082
+ }
11083
+ }
10927
11084
  }
10928
11085
  let dailyReportSuccesses = 0;
10929
11086
  let dailyReportSkipped = 0;
@@ -10976,7 +11133,10 @@ async function handleDoctorAll(ctx) {
10976
11133
  await withRegistry(workspaceDir, (data) => {
10977
11134
  data.lastDoctorAllRunAt = now.toISOString();
10978
11135
  for (const [_key, entry] of Object.entries(data.topics)) {
10979
- if (!isEligible(entry, now)) continue;
11136
+ const dir = path11.join(projectsBase, entry.slug);
11137
+ const statusFile = readFileOrNull(path11.join(dir, "STATUS.md"));
11138
+ const ts = statusFile ? extractStatusTimestamp(statusFile) : null;
11139
+ if (!isEligible(entry, now, ts)) continue;
10980
11140
  if (entry.lastMessageAt) {
10981
11141
  const lastMsg = new Date(entry.lastMessageAt).getTime();
10982
11142
  const lastDoctor = entry.lastDoctorRunAt ? new Date(entry.lastDoctorRunAt).getTime() : 0;
@@ -11002,42 +11162,44 @@ async function handleDoctorAll(ctx) {
11002
11162
  }
11003
11163
  }
11004
11164
  });
11005
- const lines = [
11006
- `**Health Check Summary**`,
11007
- "",
11008
- `Checked: ${processed}`,
11009
- `Skipped: ${skipped}`,
11010
- `Total topics: ${allEntries.length}`
11011
- ];
11012
- if (ctx.postFn) {
11013
- lines.push(`Posted: ${postSuccesses}, Post failures: ${postErrors}`);
11014
- lines.push(`Daily reports: ${dailyReportSuccesses} sent, ${dailyReportSkipped} skipped`);
11015
- }
11016
- if (errors.length > 0) {
11017
- lines.push("");
11018
- lines.push(`**Errors (${errors.length}):**`);
11019
- for (const e of errors.slice(0, 10)) {
11020
- lines.push(`- ${e}`);
11021
- }
11022
- if (errors.length > 10) {
11023
- lines.push(`... and ${errors.length - 10} more`);
11024
- }
11025
- }
11026
- if (migrationGroups.length > 0) {
11027
- lines.push("");
11028
- lines.push(`**Warning:** ${migrationGroups.length} group(s) had all topics fail. The group may have been migrated or deleted.`);
11029
- }
11030
11165
  return {
11031
- text: lines.join("\n")
11166
+ text: buildDoctorAllSummary({
11167
+ checkedTopics,
11168
+ skippedTopics,
11169
+ postFailures,
11170
+ dailyReportsSent: dailyReportSuccesses,
11171
+ dailyReportsSkipped: dailyReportSkipped,
11172
+ hasPostFn: !!ctx.postFn,
11173
+ migrationGroups: migrationGroups.length,
11174
+ errors
11175
+ })
11032
11176
  };
11033
11177
  }
11034
- function isEligible(entry, now) {
11178
+ var STATUS_TIMESTAMP_RE = /^##\s*Last done\s*\(UTC\)/im;
11179
+ var ISO_TIMESTAMP_RE = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
11180
+ function extractStatusTimestamp(content) {
11181
+ if (!STATUS_TIMESTAMP_RE.test(content)) return null;
11182
+ const idx = content.search(STATUS_TIMESTAMP_RE);
11183
+ const sectionAfter = content.slice(idx);
11184
+ const nextSection = sectionAfter.indexOf("\n## ", 1);
11185
+ const section = nextSection > 0 ? sectionAfter.slice(0, nextSection) : sectionAfter;
11186
+ const match = section.match(ISO_TIMESTAMP_RE);
11187
+ return match ? match[0] : null;
11188
+ }
11189
+ function mostRecent(a, b) {
11190
+ if (!a && !b) return null;
11191
+ if (!a) return b;
11192
+ if (!b) return a;
11193
+ return a > b ? a : b;
11194
+ }
11195
+ function isEligible(entry, now, statusTimestamp) {
11035
11196
  if (entry.status === "archived") return false;
11036
11197
  if (entry.snoozeUntil && new Date(entry.snoozeUntil).getTime() > now.getTime()) return false;
11037
- if (entry.lastMessageAt) {
11038
- const lastActive = new Date(entry.lastMessageAt).getTime();
11198
+ const lastActive = mostRecent(entry.lastMessageAt, statusTimestamp);
11199
+ if (lastActive) {
11200
+ const lastActiveMs = new Date(lastActive).getTime();
11039
11201
  const inactiveMs = INACTIVE_AFTER_DAYS * 24 * 60 * 60 * 1e3;
11040
- if (now.getTime() - lastActive > inactiveMs) return false;
11202
+ if (now.getTime() - lastActiveMs > inactiveMs) return false;
11041
11203
  }
11042
11204
  if (entry.lastDoctorReportAt) {
11043
11205
  const lastReport = new Date(entry.lastDoctorReportAt).getTime();
@@ -11106,18 +11268,18 @@ function formatStatus(name, content) {
11106
11268
  ""
11107
11269
  ];
11108
11270
  if (timestamp) {
11109
- lines.push(`**Last activity:** ${relativeTime(timestamp)}`);
11271
+ lines.push(`\u{1F552} **Last activity:** ${relativeTime(timestamp)}`);
11110
11272
  }
11111
11273
  if (lastDoneBody && !isPlaceholder(lastDoneBody)) {
11112
11274
  lines.push(lastDoneBody);
11113
11275
  }
11114
11276
  lines.push("");
11115
- lines.push("**Next actions**");
11277
+ lines.push("\u{1F3AF} **Next actions**");
11116
11278
  lines.push(formatSection(nextRaw));
11117
11279
  const upcomingFormatted = formatSection(upcomingRaw);
11118
11280
  if (upcomingFormatted !== "_None yet._") {
11119
11281
  lines.push("");
11120
- lines.push("**Upcoming**");
11282
+ lines.push("\u{1F4C5} **Upcoming**");
11121
11283
  lines.push(upcomingFormatted);
11122
11284
  }
11123
11285
  return lines.join("\n");
@@ -11561,13 +11723,13 @@ async function handleCallback(data, ctx) {
11561
11723
  ic: "coding",
11562
11724
  ir: "research",
11563
11725
  im: "marketing",
11564
- ix: "custom"
11726
+ ig: "general"
11565
11727
  };
11566
11728
  const initConfirmMap = {
11567
11729
  yc: "coding",
11568
11730
  yr: "research",
11569
11731
  ym: "marketing",
11570
- yx: "custom"
11732
+ yg: "general"
11571
11733
  };
11572
11734
  const cbCtx = { ...ctx, groupId: cbGroupId, threadId: cbThreadId, userId: cbUserId };
11573
11735
  if (action in initTypeMap) {
package/dist/setup.js CHANGED
@@ -1156,7 +1156,7 @@ var PLUGIN_FILES = ["openclaw.plugin.json", "dist/plugin.js", "skills", "package
1156
1156
  var REQUIRED_PLUGIN_FILES = ["openclaw.plugin.json", "dist/plugin.js"];
1157
1157
  var FLUSH_TAG = "[tm]";
1158
1158
  var FLUSH_FINGERPRINTS = [FLUSH_TAG, "STATUS.md"];
1159
- var SETUP_REGISTRY_VERSION = 4;
1159
+ var SETUP_REGISTRY_VERSION = 5;
1160
1160
  var MEMORY_FLUSH_INSTRUCTION = `If you are working on a Telegram topic folder (projects/<slug>/), update its STATUS.md with current "Last done (UTC)" and "Next actions (now)" before this context is compacted. ${FLUSH_TAG}`;
1161
1161
  var SETUP_MARKER_START = "<!-- TM_AUTOPILOT_START -->";
1162
1162
  var SETUP_MARKER_END = "<!-- TM_AUTOPILOT_END -->";
@@ -1270,8 +1270,10 @@ async function runSetup() {
1270
1270
  startSpinner("Patching memory flush\u2026");
1271
1271
  patchMemoryFlush(configDir);
1272
1272
  startSpinner("Preparing workspace\u2026");
1273
- const projectsDir = path.join(configDir, "workspace", "projects");
1273
+ const workspaceDir = path.join(configDir, "workspace");
1274
+ const projectsDir = path.join(workspaceDir, "projects");
1274
1275
  ensureDir(projectsDir);
1276
+ ensureGitignore(workspaceDir);
1275
1277
  initRegistry(projectsDir);
1276
1278
  createEmptyInclude(configDir, existingGroups);
1277
1279
  ok("Workspace ready");
@@ -1498,6 +1500,29 @@ function createEmptyInclude(configDir, seedGroups) {
1498
1500
  ].join("\n");
1499
1501
  fs.writeFileSync(includePath, content, { mode: 384 });
1500
1502
  }
1503
+ var GITIGNORE_ENTRIES = [
1504
+ "projects/topics.json",
1505
+ "projects/audit.jsonl",
1506
+ "projects/*/.tm-backup/"
1507
+ ];
1508
+ function ensureGitignore(workspaceDir) {
1509
+ const gitignorePath = path.join(workspaceDir, ".gitignore");
1510
+ let content = "";
1511
+ try {
1512
+ if (fs.existsSync(gitignorePath)) {
1513
+ content = fs.readFileSync(gitignorePath, "utf-8");
1514
+ }
1515
+ } catch {
1516
+ }
1517
+ const lines = content.split("\n");
1518
+ const missing = GITIGNORE_ENTRIES.filter(
1519
+ (entry) => !lines.some((line) => line.trim() === entry)
1520
+ );
1521
+ if (missing.length === 0) return;
1522
+ const block = "\n# telegram-manager (operational files)\n" + missing.join("\n") + "\n";
1523
+ const newContent = content ? content.trimEnd() + "\n" + block : block.trimStart();
1524
+ fs.writeFileSync(gitignorePath, newContent);
1525
+ }
1501
1526
  function writeHeartbeat(configDir) {
1502
1527
  const heartbeatPath = path.join(configDir, "workspace", "HEARTBEAT.md");
1503
1528
  let content = "";