chat-recall 0.7.8 → 0.7.10

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/watch.js CHANGED
@@ -134,6 +134,72 @@ var init_banners = __esm({
134
134
  }
135
135
  });
136
136
 
137
+ // ../engine/dist/core/claude-prompt-origin.js
138
+ function originFromText(text) {
139
+ const head = text.trimStart();
140
+ for (const [prefix, origin] of INJECTED_PREFIXES) {
141
+ if (head.startsWith(prefix))
142
+ return origin;
143
+ }
144
+ return "human";
145
+ }
146
+ function declaredOrigin(value) {
147
+ if (!value || typeof value !== "object")
148
+ return void 0;
149
+ const kind = value.kind;
150
+ return typeof kind === "string" && kind ? kind : void 0;
151
+ }
152
+ function userRecordOrigin(obj, text) {
153
+ if (obj.isSidechain === true)
154
+ return "subagent";
155
+ const declared = declaredOrigin(obj.origin);
156
+ if (declared && declared !== "human")
157
+ return declared;
158
+ if (obj.isMeta === true)
159
+ return "meta";
160
+ if (declared === "human")
161
+ return "human";
162
+ return originFromText(text);
163
+ }
164
+ function queuedCommandPrompt(obj) {
165
+ if (obj.type !== "attachment")
166
+ return null;
167
+ const att = obj.attachment;
168
+ if (!att || att.type !== "queued_command")
169
+ return null;
170
+ const text = promptText(att.prompt);
171
+ if (!text)
172
+ return null;
173
+ if (obj.isSidechain === true)
174
+ return { text, origin: "subagent" };
175
+ const declared = declaredOrigin(att.origin);
176
+ if (declared)
177
+ return { text, origin: declared };
178
+ return { text, origin: att.humanTurn === true ? "human" : originFromText(text) };
179
+ }
180
+ function promptText(prompt) {
181
+ if (typeof prompt === "string")
182
+ return prompt;
183
+ if (!Array.isArray(prompt))
184
+ return "";
185
+ return prompt.filter((b) => !!b && typeof b === "object" && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
186
+ }
187
+ function isPersonMessage(m) {
188
+ return m.role === "user" && !m.origin;
189
+ }
190
+ var INJECTED_PREFIXES;
191
+ var init_claude_prompt_origin = __esm({
192
+ "../engine/dist/core/claude-prompt-origin.js"() {
193
+ "use strict";
194
+ INJECTED_PREFIXES = [
195
+ ["<task-notification", "task-notification"],
196
+ ["<agent-message", "peer"],
197
+ ["Another Claude session sent a message:", "peer"],
198
+ ["Stop hook feedback:", "hook"]
199
+ ];
200
+ }
201
+ });
202
+
137
203
  // ../engine/dist/core/project-path-match.js
138
204
  function canonicalProjectPath(p) {
139
205
  return (p || "").trim().replace(/\\/g, "/").replace(/[-_]/g, "/").replace(/\/{2,}/g, "/").replace(/\/+$/, "").toLowerCase();
@@ -5716,7 +5782,8 @@ async function parseSessionFile(sessionPath, maxMessages = 50) {
5716
5782
  const filesToParse = hasSubagentsDir(sessionPath) ? [sessionPath, ...getSubagentPaths(sessionPath)] : [sessionPath];
5717
5783
  let userCount = 0;
5718
5784
  const seenPrompts = /* @__PURE__ */ new Set();
5719
- const recordPrompt = (raw, line, dedupe = false) => {
5785
+ const pendingQueued = /* @__PURE__ */ new Set();
5786
+ const recordPrompt = (raw, line, via = "record") => {
5720
5787
  if (userCount >= maxMessages)
5721
5788
  return;
5722
5789
  const withoutReminders = raw.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "");
@@ -5725,8 +5792,14 @@ async function parseSessionFile(sessionPath, maxMessages = 50) {
5725
5792
  const cleanedText = stripInjectedBanners(withoutReminders).trim();
5726
5793
  if (cleanedText.length < 10)
5727
5794
  return;
5728
- if (dedupe && seenPrompts.has(cleanedText))
5795
+ if (via === "enqueue") {
5796
+ if (seenPrompts.has(cleanedText))
5797
+ return;
5798
+ pendingQueued.add(cleanedText);
5799
+ } else if (pendingQueued.has(cleanedText)) {
5800
+ pendingQueued.delete(cleanedText);
5729
5801
  return;
5802
+ }
5730
5803
  seenPrompts.add(cleanedText);
5731
5804
  content.userMessages.push({ text: cleanedText, lineNumber: line, contentType: "user" });
5732
5805
  userCount++;
@@ -5811,12 +5884,16 @@ async function parseSessionFile(sessionPath, maxMessages = 50) {
5811
5884
  text = textParts.join("\n");
5812
5885
  }
5813
5886
  }
5814
- if (text)
5887
+ if (text && userRecordOrigin(obj, text) === "human")
5815
5888
  recordPrompt(text, lineNum);
5816
5889
  } else if (msgType === "queue-operation") {
5817
- if (obj.operation === "enqueue" && typeof obj.content === "string") {
5818
- recordPrompt(obj.content, lineNum, true);
5890
+ if (obj.operation === "enqueue" && typeof obj.content === "string" && originFromText(obj.content) === "human") {
5891
+ recordPrompt(obj.content, lineNum, "enqueue");
5819
5892
  }
5893
+ } else if (msgType === "attachment") {
5894
+ const queued = queuedCommandPrompt(obj);
5895
+ if (queued?.origin === "human")
5896
+ recordPrompt(queued.text, lineNum, "delivery");
5820
5897
  } else if (msgType === "assistant") {
5821
5898
  const msg = obj.message;
5822
5899
  if (!msg || typeof msg !== "object")
@@ -5920,6 +5997,7 @@ var init_session = __esm({
5920
5997
  "../engine/dist/parsers/session.js"() {
5921
5998
  "use strict";
5922
5999
  init_banners();
6000
+ init_claude_prompt_origin();
5923
6001
  init_claude2();
5924
6002
  init_tool_paths();
5925
6003
  init_project_dir_name();
@@ -6008,7 +6086,16 @@ function messagesFromRawLines(raw) {
6008
6086
  const cleaned = stripBanners(text);
6009
6087
  if (!cleaned)
6010
6088
  continue;
6011
- messages.push({ line, role: "user", content: cleaned, timestamp });
6089
+ const origin = userRecordOrigin(obj, cleaned);
6090
+ messages.push({ line, role: "user", content: cleaned, timestamp, ...origin === "human" ? {} : { origin } });
6091
+ } else if (obj.type === "attachment") {
6092
+ const queued = queuedCommandPrompt(obj);
6093
+ if (!queued)
6094
+ continue;
6095
+ const cleaned = stripBanners(queued.text);
6096
+ if (!cleaned)
6097
+ continue;
6098
+ messages.push({ line, role: "user", content: cleaned, timestamp, ...queued.origin === "human" ? {} : { origin: queued.origin } });
6012
6099
  } else if (obj.type === "assistant") {
6013
6100
  const blocks = Array.isArray(obj.message?.content) ? obj.message.content : [];
6014
6101
  const thinkingBlock = blocks.find((c) => c?.type === "thinking");
@@ -6104,6 +6191,7 @@ var init_claude = __esm({
6104
6191
  "../engine/dist/transcript/claude.js"() {
6105
6192
  "use strict";
6106
6193
  init_logger();
6194
+ init_claude_prompt_origin();
6107
6195
  log2 = createLogger("transcript-claude");
6108
6196
  INJECTED_BANNERS2 = [
6109
6197
  /MCP issues detected\. ?Run \/mcp list for status\.?/g,
@@ -7224,10 +7312,15 @@ function summarizeMarkers(marked) {
7224
7312
  }
7225
7313
  return counts;
7226
7314
  }
7227
- var PROFANITY, PROFANITY_LOOSE, APPROVAL_HEAD, NEG_HEAD, QUESTION_HEAD, DIRECTIVE_HEAD, CLARIFY_PHRASES, FRUSTRATED_PHRASES;
7315
+ function markersFromTurns(sessionId, turns) {
7316
+ const prompts = turns.filter((t) => t.kind === "user" && t.text).map((t) => ({ line: t.line, ts: t.ts, tsIso: t.tsIso, ...markPrompt(t.text) }));
7317
+ return { v: MARKERS_VERSION, sessionId, prompts, summary: summarizeMarkers(prompts) };
7318
+ }
7319
+ var PROFANITY, PROFANITY_LOOSE, APPROVAL_HEAD, NEG_HEAD, QUESTION_HEAD, DIRECTIVE_HEAD, CLARIFY_PHRASES, FRUSTRATED_PHRASES, MARKERS_VERSION;
7228
7320
  var init_session_sentiment = __esm({
7229
7321
  "../engine/dist/core/session-sentiment.js"() {
7230
7322
  "use strict";
7323
+ init_claude_prompt_origin();
7231
7324
  PROFANITY = /\b(fuck(?:ing|er|ed)?|shit|ffs|wtf|bullshit|crap|damn(?:it)?|hell)\b/i;
7232
7325
  PROFANITY_LOOSE = /\b(fu[ckj]+(?:ing|er|ed|c?k)?|fukc|fucj|sht|fk|wt[ft])\b/i;
7233
7326
  APPROVAL_HEAD = /^(?:yes|y|ok(?:ay)?|sure|alright|fine|i\s*approve|approved|go\s*(?:on|ahead)|do\s*it|continue|please\s*do|sounds?\s*good|let'?s\s*go|ship\s*it|👍|✓+)\b/i;
@@ -7236,6 +7329,7 @@ var init_session_sentiment = __esm({
7236
7329
  DIRECTIVE_HEAD = /^(?:please\s+)?(?:use|implement|build|make|add|create|fix|remove|delete|update|refactor|run|deploy|push|merge|test|check|investigate|find|search|show|list|generate|set\s+up|ensure)\b/i;
7237
7330
  CLARIFY_PHRASES = /\b(?:wdym|what do you mean|explain|clarify|what is|what's|huh\??|i don'?t understand)\b/i;
7238
7331
  FRUSTRATED_PHRASES = /\b(?:why (?:the )?(?:fuck|hell)|wtf|are you (?:kidding|serious)|stop slacking|stop making|why did you|why didn'?t you|i (?:fucking )?(?:asked|told you)|are you (?:listening|even)|come\s*on|just\s+do)\b/i;
7332
+ MARKERS_VERSION = 2;
7239
7333
  }
7240
7334
  });
7241
7335
 
@@ -8102,6 +8196,7 @@ var init_claude2 = __esm({
8102
8196
  init_first_prompt();
8103
8197
  init_tail_read();
8104
8198
  init_project_dir_name();
8199
+ init_claude_prompt_origin();
8105
8200
  init_generic_engine();
8106
8201
  UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8107
8202
  ClaudeBackend = class {
@@ -8409,6 +8504,15 @@ var init_claude2 = __esm({
8409
8504
  }
8410
8505
  appendEventsFromText(events, raw, mtime, startLine) {
8411
8506
  let lineNum = startLine;
8507
+ const queued = /* @__PURE__ */ new Map();
8508
+ const superseded = /* @__PURE__ */ new Set();
8509
+ const pushPrompt = (e) => {
8510
+ const pending = queued.get(e.text);
8511
+ const earlier = pending?.shift();
8512
+ if (earlier)
8513
+ superseded.add(earlier);
8514
+ events.push(e);
8515
+ };
8412
8516
  {
8413
8517
  for (const line of raw.split("\n")) {
8414
8518
  lineNum++;
@@ -8424,13 +8528,26 @@ var init_claude2 = __esm({
8424
8528
  const ts = tsIso ? Date.parse(tsIso) || mtime : mtime;
8425
8529
  if (obj.type === "queue-operation") {
8426
8530
  if (obj.operation === "enqueue" && typeof obj.content === "string") {
8427
- const queued = stripReminders(obj.content);
8428
- if (queued && !queued.startsWith("<task-notification")) {
8429
- events.push({ kind: "user", ts, tsIso, line: lineNum, text: queued });
8531
+ const text = stripReminders(obj.content);
8532
+ if (text && originFromText(text) === "human") {
8533
+ const e = { kind: "user", ts, tsIso, line: lineNum, text };
8534
+ events.push(e);
8535
+ const list = queued.get(text);
8536
+ if (list)
8537
+ list.push(e);
8538
+ else
8539
+ queued.set(text, [e]);
8430
8540
  }
8431
8541
  }
8432
8542
  continue;
8433
8543
  }
8544
+ const delivered = queuedCommandPrompt(obj);
8545
+ if (delivered) {
8546
+ const text = stripReminders(delivered.text);
8547
+ if (text && delivered.origin === "human")
8548
+ pushPrompt({ kind: "user", ts, tsIso, line: lineNum, text });
8549
+ continue;
8550
+ }
8434
8551
  if (obj.type === "user") {
8435
8552
  const msg = obj.message;
8436
8553
  const content = msg?.content;
@@ -8465,8 +8582,9 @@ var init_claude2 = __esm({
8465
8582
  }
8466
8583
  }
8467
8584
  const spoken = stripReminders(text);
8468
- if (spoken)
8469
- events.push({ kind: "user", ts, tsIso, line: lineNum, text: spoken });
8585
+ if (spoken && userRecordOrigin(obj, spoken) === "human") {
8586
+ pushPrompt({ kind: "user", ts, tsIso, line: lineNum, text: spoken });
8587
+ }
8470
8588
  for (const tr of toolResults) {
8471
8589
  events.push({
8472
8590
  kind: "tool_result",
@@ -8515,6 +8633,13 @@ var init_claude2 = __esm({
8515
8633
  }
8516
8634
  }
8517
8635
  }
8636
+ if (superseded.size > 0) {
8637
+ let w = 0;
8638
+ for (const e of events)
8639
+ if (!superseded.has(e))
8640
+ events[w++] = e;
8641
+ events.length = w;
8642
+ }
8518
8643
  return lineNum;
8519
8644
  }
8520
8645
  // ── Per-session operations — all delegate to the generic engine ─
@@ -11845,7 +11970,21 @@ function reportWalkProgress(p) {
11845
11970
  lastProgressWrite = now;
11846
11971
  updateCollectorHealth({ progress: p });
11847
11972
  }
11848
- var healthPath, PROGRESS_WRITE_INTERVAL_MS, lastProgressWrite, STALE_AFTER_MS;
11973
+ function crashesAtBoot(prior, procStart) {
11974
+ const history = (prior?.crashes ?? []).filter((t) => typeof t === "number");
11975
+ if (!prior)
11976
+ return history;
11977
+ const leftOnPurpose = typeof prior.cleanExitAt === "number" && prior.cleanExitAt >= prior.startedAt;
11978
+ return leftOnPurpose ? history : [...history, procStart];
11979
+ }
11980
+ function recentCrashes(crashes, now = Date.now()) {
11981
+ const cutoff = now - 36e5;
11982
+ return [...new Set(crashes.filter((t) => t > cutoff))].sort((a, b) => a - b).slice(-20);
11983
+ }
11984
+ function markCleanExit(now = Date.now()) {
11985
+ updateCollectorHealth({ cleanExitAt: now });
11986
+ }
11987
+ var healthPath, PROGRESS_WRITE_INTERVAL_MS, lastProgressWrite, STALE_AFTER_MS, CRASHLOOP_RESTARTS;
11849
11988
  var init_collector_health = __esm({
11850
11989
  "../engine/dist/core/collector-health.js"() {
11851
11990
  "use strict";
@@ -11854,6 +11993,7 @@ var init_collector_health = __esm({
11854
11993
  PROGRESS_WRITE_INTERVAL_MS = 2e3;
11855
11994
  lastProgressWrite = 0;
11856
11995
  STALE_AFTER_MS = 30 * 6e4;
11996
+ CRASHLOOP_RESTARTS = 3;
11857
11997
  }
11858
11998
  });
11859
11999
 
@@ -18172,7 +18312,7 @@ var init_http = __esm({
18172
18312
  CLI_VERSION = JSON.parse(readFileSync32(new URL("../package.json", import.meta.url), "utf-8")).version || CLI_VERSION;
18173
18313
  } catch {
18174
18314
  try {
18175
- if (true) CLI_VERSION = "0.7.8";
18315
+ if (true) CLI_VERSION = "0.7.10";
18176
18316
  } catch {
18177
18317
  }
18178
18318
  }
@@ -18217,8 +18357,7 @@ function collectDerivedRows(ref, mtime, count, maxRowBytes) {
18217
18357
  if (fullOutcome.found) data = fullOutcome.commits;
18218
18358
  } else if (kind === "markers") {
18219
18359
  const turns = extractTurnsAny(id, { maxTurns: 5e4 });
18220
- const prompts = turns.turns.filter((t) => t.kind === "user" && t.text).map((t) => ({ line: t.line, ts: t.ts, tsIso: t.tsIso, ...markPrompt(t.text) }));
18221
- data = { sessionId: id, prompts, summary: summarizeMarkers(prompts) };
18360
+ data = markersFromTurns(id, turns.turns);
18222
18361
  }
18223
18362
  } catch {
18224
18363
  data = null;
@@ -18667,7 +18806,7 @@ var init_telemetry = __esm({
18667
18806
  init_sync_client();
18668
18807
  cliVersion = () => {
18669
18808
  try {
18670
- return true ? "0.7.8" : "dev";
18809
+ return true ? "0.7.10" : "dev";
18671
18810
  } catch {
18672
18811
  return "dev";
18673
18812
  }
@@ -20011,7 +20150,8 @@ async function syncToTargetInScope(cred, opts = {}) {
20011
20150
  }
20012
20151
  trace?.("conversation walk start");
20013
20152
  const walkStartedAt = Date.now();
20014
- reportWalkProgress({ done: 0, total: slice.length, startedAt: walkStartedAt, complete: false });
20153
+ const firstWalk = (ledger ?? getSyncedRows(base2)).size === 0;
20154
+ reportWalkProgress({ done: 0, total: slice.length, startedAt: walkStartedAt, complete: false, first: firstWalk });
20015
20155
  walkProgressForUpload = { done: 0, total: slice.length, complete: false };
20016
20156
  let traceN = 0;
20017
20157
  let considered = 0;
@@ -20019,7 +20159,7 @@ async function syncToTargetInScope(cred, opts = {}) {
20019
20159
  await new Promise((resolve3) => setImmediate(resolve3));
20020
20160
  considered++;
20021
20161
  const walkDone = considered >= slice.length;
20022
- reportWalkProgress({ done: considered, total: slice.length, startedAt: walkStartedAt, complete: walkDone });
20162
+ reportWalkProgress({ done: considered, total: slice.length, startedAt: walkStartedAt, complete: walkDone, first: firstWalk });
20023
20163
  walkProgressForUpload = { done: considered, total: slice.length, complete: walkDone };
20024
20164
  if (trace && ++traceN % 500 === 0) trace(`walk ${traceN}/${slice.length} \xB7 at ${ref.prefixedId}`);
20025
20165
  localSessionIds.add(ref.prefixedId);
@@ -20291,7 +20431,7 @@ async function syncToTargetInScope(cred, opts = {}) {
20291
20431
  }
20292
20432
  }
20293
20433
  if (opts.prune) await post({ prune_empty_sessions: true });
20294
- reportWalkProgress({ done: slice.length, total: slice.length, startedAt: walkStartedAt, complete: true });
20434
+ reportWalkProgress({ done: slice.length, total: slice.length, startedAt: walkStartedAt, complete: true, first: firstWalk });
20295
20435
  walkProgressForUpload = { done: slice.length, total: slice.length, complete: true };
20296
20436
  return {
20297
20437
  uploaded: convBatch.sent + appendResults.uploaded,
@@ -20422,7 +20562,7 @@ async function buildConversationSync(ref, mtime, opts = {}) {
20422
20562
  if (!transcript) return null;
20423
20563
  const textMessages = transcript.messages.filter((m) => m.role !== "summary" && m.content?.trim());
20424
20564
  if (textMessages.length === 0 && !transcript.messages.some((m) => m.toolCalls?.length)) return null;
20425
- const firstUser = textMessages.find((m) => m.role === "user");
20565
+ const firstUser = textMessages.find(isPersonMessage);
20426
20566
  if (isInternalToolPrompt(firstUser?.content)) return null;
20427
20567
  const count = { redactions: 0 };
20428
20568
  const envelope = redactDeep(
@@ -20431,7 +20571,7 @@ async function buildConversationSync(ref, mtime, opts = {}) {
20431
20571
  );
20432
20572
  let projectPath = ref.projectPath;
20433
20573
  const meta = { messageCount: textMessages.length };
20434
- if (textMessages.filter((m) => m.role === "user").length <= 1) meta.oneShot = true;
20574
+ if (textMessages.filter(isPersonMessage).length <= 1) meta.oneShot = true;
20435
20575
  try {
20436
20576
  const cwd = ref.fullPath ? readCwdFromJsonl(ref.fullPath) : "";
20437
20577
  if (cwd) projectPath = cwd;
@@ -20583,7 +20723,7 @@ async function buildConversationSync(ref, mtime, opts = {}) {
20583
20723
  envelope,
20584
20724
  raw_b64,
20585
20725
  raw_size,
20586
- first_prompt: envTexts.find((m) => m.role === "user")?.content?.slice(0, 200),
20726
+ first_prompt: envTexts.find(isPersonMessage)?.content?.slice(0, 200),
20587
20727
  // Byte offset this FULL sync is synced THROUGH (file size at build time,
20588
20728
  // or the end of the bounded tail for an oversized session). The server
20589
20729
  // stores it as the envelope's `o`; the ledger records the SAME value
@@ -20639,7 +20779,7 @@ async function buildConversationTail(ref, fromOffset, opts = {}) {
20639
20779
  bytes: 0
20640
20780
  };
20641
20781
  }
20642
- const firstUser = textMessages.find((m) => m.role === "user");
20782
+ const firstUser = textMessages.find(isPersonMessage);
20643
20783
  if (isInternalToolPrompt(firstUser?.content)) return null;
20644
20784
  const count = { redactions: 0 };
20645
20785
  const envelope = redactDeep(
@@ -20716,6 +20856,7 @@ var ownVersion, ERROR_CLASS, STANDING_FAILURE, walkProgressForUpload, lastServer
20716
20856
  var init_sync_client = __esm({
20717
20857
  "src/sync-client.ts"() {
20718
20858
  "use strict";
20859
+ init_claude_prompt_origin();
20719
20860
  init_credentials_env();
20720
20861
  init_secret_redactor();
20721
20862
  init_source_discovery();
@@ -20757,7 +20898,7 @@ var init_sync_client = __esm({
20757
20898
  ownVersion = JSON.parse(readFileSync35(join42(dir, "../package.json"), "utf-8")).version || ownVersion;
20758
20899
  } catch {
20759
20900
  try {
20760
- if (true) ownVersion = "0.7.8";
20901
+ if (true) ownVersion = "0.7.10";
20761
20902
  } catch {
20762
20903
  }
20763
20904
  }
@@ -23894,7 +24035,7 @@ try {
23894
24035
  ownVersion2 = JSON.parse(readFileSync39(join48(dir, "../package.json"), "utf-8")).version || ownVersion2;
23895
24036
  } catch {
23896
24037
  try {
23897
- if (true) ownVersion2 = "0.7.8";
24038
+ if (true) ownVersion2 = "0.7.10";
23898
24039
  } catch {
23899
24040
  }
23900
24041
  }
@@ -24279,7 +24420,7 @@ async function runCollectorMigration(opts) {
24279
24420
  init_sync_ledger();
24280
24421
  init_collector_health();
24281
24422
  import { existsSync as existsSync50, readFileSync as readFileSync45, statSync as statSync45 } from "fs";
24282
- var CLI_VERSION3 = true ? "0.7.8" : "0.0.0";
24423
+ var CLI_VERSION3 = true ? "0.7.10" : "0.0.0";
24283
24424
  var DEBOUNCE_MS = 5e3;
24284
24425
  var PLANS_DIR = claudeBackend.plansDir();
24285
24426
  var TASKS_DIR = claudeBackend.tasksDir();
@@ -24533,20 +24674,16 @@ if (BOOT_CODE) {
24533
24674
  }
24534
24675
  var PROC_START = Date.now();
24535
24676
  var targetHealth = {};
24536
- function recentStarts() {
24537
- const prior = readCollectorHealth();
24538
- const cutoff = Date.now() - 36e5;
24539
- return [...prior?.starts ?? [], PROC_START].filter((t) => typeof t === "number" && t > cutoff).filter((t, i, a) => a.indexOf(t) === i).sort((a, b) => a - b).slice(-20);
24540
- }
24677
+ var BOOT_CRASHES = crashesAtBoot(readCollectorHealth(), PROC_START);
24541
24678
  function publishHealth() {
24542
- const starts = recentStarts();
24679
+ const crashes = recentCrashes(BOOT_CRASHES);
24543
24680
  updateCollectorHealth(Object.assign({
24544
24681
  v: 1,
24545
24682
  updatedAt: Date.now(),
24546
24683
  startedAt: PROC_START,
24547
- restartsLastHour: starts.length,
24684
+ restartsLastHour: crashes.length,
24548
24685
  targets: targetHealth
24549
- }, { starts }));
24686
+ }, { crashes, cleanExitAt: void 0, starts: void 0 }));
24550
24687
  }
24551
24688
  var SHADOW_PRUNE_MS = Number(process.env.CHAT_RECALL_SHADOW_PRUNE_MS) || 6 * 60 * 60 * 1e3;
24552
24689
  async function shadowPruneTick() {
@@ -24600,9 +24737,9 @@ function noteSyncOutcome(server, ok, err, accepted = 0) {
24600
24737
  }
24601
24738
  publishHealth();
24602
24739
  {
24603
- const n = recentStarts().length;
24604
- if (n >= 3) {
24605
- daemonLog.error(`WARNING: this collector has restarted ${n} times in the last hour. Sync is probably falling behind. Health: ${collectorHealthPath()}`);
24740
+ const n = recentCrashes(BOOT_CRASHES).length;
24741
+ if (n >= CRASHLOOP_RESTARTS) {
24742
+ daemonLog.error(`WARNING: this collector crashed ${n} times in the last hour. Sync is probably falling behind. Health: ${collectorHealthPath()}`);
24606
24743
  }
24607
24744
  }
24608
24745
  setInterval(() => {
@@ -24856,11 +24993,12 @@ if (process.platform === "win32") {
24856
24993
  process.on("SIGBREAK", () => shutdown("SIGBREAK"));
24857
24994
  process.on("SIGHUP", () => shutdown("SIGHUP"));
24858
24995
  }
24859
- process.on("exit", () => {
24996
+ process.on("exit", (code) => {
24860
24997
  try {
24861
24998
  flushLedger();
24862
24999
  } catch {
24863
25000
  }
25001
+ if (code === 0 || code === EXIT_CODE_CODE_CHANGED) markCleanExit();
24864
25002
  });
24865
25003
  export {
24866
25004
  _targetHealth,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chat-recall",
3
- "version": "0.7.8",
3
+ "version": "0.7.10",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/munhq/chat-recall.git"
@@ -74,7 +74,7 @@ directly.
74
74
  Every `recall_*` tool. `packages/cli/src/skills-catalog.test.ts` fails when a
75
75
  tool is registered and never named here, so adding a tool means placing it.
76
76
 
77
- **Resume / cold start** — `recall_smart_resume` (structured resume bundle; needs a session id), `recall_recent` (list recent sessions), `recall_wake_up` (identity + high-signal facts), `recall_context` (structured dump of one session), `recall_show` (raw slice — returns 10 messages unless you raise `max_messages`), `recall_summary` (AI summary + outcome).
77
+ **Resume / cold start** — `recall_smart_resume` (structured resume bundle; needs a session id), `recall_recent` (list recent sessions), `recall_wake_up` (identity + high-signal facts), `recall_context` (structured dump of one session), `recall_show` (raw slice — `query` finds a message and its line, `around_line` reads around it; returns 10 messages unless you raise `max_messages`), `recall_summary` (AI summary + outcome).
78
78
  **Search** — `recall_search` (sessions; `include_outcome`, `like_session`), `recall_memory_search` (every memory type), `recall_memory_item` (read ONE item found by search, or browse a source type), `recall_user_prompts` (what the user actually typed), `recall_subagent_search` (inside subagent transcripts, whose work never reaches the main conversation), `recall_redundant_files` (before writing a new file, check you have not written one like it already).
79
79
  **Project** — `recall_project_context` (rich dump), `recall_weekly_digest`, `recall_analytics_summary`, `recall_outcome_summary` (how many recent sessions actually shipped), `recall_code_findings` / `recall_code_actions` / `recall_code_projects` / `recall_code_index` / `recall_recommendations`.
80
80
 
@@ -39,8 +39,10 @@ description: >-
39
39
 
40
40
  Every tool here takes a NAMED `session_id` (not a positional argument).
41
41
 
42
- - `mcp__chat-recall__recall_show` — read the session slice (`around_line`,
43
- `from_end`). Each message comes back in full, with its tool calls and
42
+ - `mcp__chat-recall__recall_show` — read the session slice. To find one
43
+ message, pass `query` with words from it: the reply gives the line of each
44
+ match. Then pass that line as `around_line`, or read the newest messages with
45
+ `from_end`. Do not guess line numbers. Each message comes back in full, with its tool calls and
44
46
  results. A body over 2000 characters shows its ends and the `expand_line`
45
47
  that returns it whole. It returns **10 messages
46
48
  by default**: raise `max_messages` before telling the user what a session
@@ -47,7 +47,9 @@ Once you have a session id:
47
47
  - `mcp__chat-recall__recall_context` — structured context (requests, decisions,
48
48
  files, tools).
49
49
  - `mcp__chat-recall__recall_show` — raw slice; `from_end: N` for the last N
50
- messages. Each message comes back in full, code and commands included, with
50
+ messages, `query` to find a message by its words and get its line,
51
+ `around_line` to read around that line. If the reply says that sync is
52
+ behind, run `recall_index` before you read the transcript file. Each message comes back in full, code and commands included, with
51
53
  its tool results. A body over 2000 characters shows its ends; pass
52
54
  `expand_line` to get it whole. It
53
55
  returns 10 messages unless you raise `max_messages`, so don't read a partial