engrm 0.4.28 → 0.4.29

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.
@@ -3721,6 +3721,7 @@ function parseJsonArray(value) {
3721
3721
  }
3722
3722
 
3723
3723
  // src/capture/transcript.ts
3724
+ import { createHash as createHash3 } from "node:crypto";
3724
3725
  import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
3725
3726
  import { join as join4 } from "node:path";
3726
3727
  import { homedir as homedir3 } from "node:os";
@@ -3774,23 +3775,109 @@ function readTranscript(sessionId, cwd, transcriptPath) {
3774
3775
  }
3775
3776
  return messages;
3776
3777
  }
3778
+ function resolveHistoryPath(historyPath) {
3779
+ if (historyPath)
3780
+ return historyPath;
3781
+ const override = process.env["ENGRM_CLAUDE_HISTORY_PATH"];
3782
+ if (override)
3783
+ return override;
3784
+ return join4(homedir3(), ".claude", "history.jsonl");
3785
+ }
3786
+ function readHistoryFallback(sessionId, cwd, opts) {
3787
+ const path = resolveHistoryPath(opts?.historyPath);
3788
+ if (!existsSync4(path))
3789
+ return [];
3790
+ let raw;
3791
+ try {
3792
+ raw = readFileSync4(path, "utf-8");
3793
+ } catch {
3794
+ return [];
3795
+ }
3796
+ const targetCanonical = detectProject(cwd).canonical_id;
3797
+ const windowStart = Math.max(0, (opts?.startedAtEpoch ?? Math.floor(Date.now() / 1000) - 6 * 3600) - 600);
3798
+ const windowEnd = (opts?.completedAtEpoch ?? Math.floor(Date.now() / 1000)) + 600;
3799
+ const entries = [];
3800
+ for (const line of raw.split(`
3801
+ `)) {
3802
+ if (!line.trim())
3803
+ continue;
3804
+ let entry;
3805
+ try {
3806
+ entry = JSON.parse(line);
3807
+ } catch {
3808
+ continue;
3809
+ }
3810
+ if (typeof entry?.display !== "string" || typeof entry?.timestamp !== "number")
3811
+ continue;
3812
+ const createdAtEpoch = Math.floor(entry.timestamp / 1000);
3813
+ entries.push({
3814
+ display: entry.display.trim(),
3815
+ project: typeof entry.project === "string" ? entry.project : "",
3816
+ sessionId: typeof entry.sessionId === "string" ? entry.sessionId : "",
3817
+ timestamp: createdAtEpoch
3818
+ });
3819
+ }
3820
+ const bySession = entries.filter((entry) => entry.display.length > 0 && entry.sessionId === sessionId).sort((a, b) => a.timestamp - b.timestamp);
3821
+ if (bySession.length > 0) {
3822
+ return dedupeHistoryMessages(bySession.map((entry) => ({
3823
+ role: "user",
3824
+ text: entry.display,
3825
+ createdAtEpoch: entry.timestamp
3826
+ })));
3827
+ }
3828
+ const byProjectAndWindow = entries.filter((entry) => {
3829
+ if (entry.display.length === 0)
3830
+ return false;
3831
+ if (entry.timestamp < windowStart || entry.timestamp > windowEnd)
3832
+ return false;
3833
+ if (!entry.project)
3834
+ return false;
3835
+ return detectProject(entry.project).canonical_id === targetCanonical;
3836
+ }).sort((a, b) => a.timestamp - b.timestamp);
3837
+ return dedupeHistoryMessages(byProjectAndWindow.map((entry) => ({
3838
+ role: "user",
3839
+ text: entry.display,
3840
+ createdAtEpoch: entry.timestamp
3841
+ })));
3842
+ }
3777
3843
  async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
3778
- const messages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
3844
+ const session = db.getSessionById(sessionId);
3845
+ const transcriptMessages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
3779
3846
  ...message,
3780
3847
  text: message.text.trim()
3781
3848
  })).filter((message) => message.text.length > 0);
3849
+ const messages = transcriptMessages.length > 0 ? transcriptMessages.map((message, index) => ({
3850
+ ...message,
3851
+ sourceKind: "transcript",
3852
+ transcriptIndex: index + 1,
3853
+ createdAtEpoch: null,
3854
+ remoteSourceId: null
3855
+ })) : readHistoryFallback(sessionId, cwd, {
3856
+ startedAtEpoch: session?.started_at_epoch ?? null,
3857
+ completedAtEpoch: session?.completed_at_epoch ?? null
3858
+ }).map((message) => ({
3859
+ role: message.role,
3860
+ text: message.text,
3861
+ sourceKind: "hook",
3862
+ transcriptIndex: null,
3863
+ createdAtEpoch: message.createdAtEpoch,
3864
+ remoteSourceId: buildHistorySourceId(sessionId, message.createdAtEpoch, message.text)
3865
+ }));
3782
3866
  if (messages.length === 0)
3783
3867
  return { imported: 0, total: 0 };
3784
- const session = db.getSessionById(sessionId);
3785
3868
  const projectId = session?.project_id ?? null;
3786
3869
  const now = Math.floor(Date.now() / 1000);
3787
3870
  let imported = 0;
3788
3871
  for (let index = 0;index < messages.length; index++) {
3789
- const transcriptIndex = index + 1;
3790
- if (db.getTranscriptChatMessage(sessionId, transcriptIndex))
3791
- continue;
3792
3872
  const message = messages[index];
3793
- const createdAtEpoch = Math.max(0, now - (messages.length - transcriptIndex));
3873
+ const transcriptIndex = message.transcriptIndex ?? index + 1;
3874
+ if (message.sourceKind === "transcript" && db.getTranscriptChatMessage(sessionId, transcriptIndex)) {
3875
+ continue;
3876
+ }
3877
+ if (message.remoteSourceId && db.getChatMessageByRemoteSourceId(message.remoteSourceId)) {
3878
+ continue;
3879
+ }
3880
+ const createdAtEpoch = message.createdAtEpoch ?? Math.max(0, now - (messages.length - transcriptIndex));
3794
3881
  const row = db.insertChatMessage({
3795
3882
  session_id: sessionId,
3796
3883
  project_id: projectId,
@@ -3800,10 +3887,23 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
3800
3887
  device_id: config.device_id,
3801
3888
  agent: "claude-code",
3802
3889
  created_at_epoch: createdAtEpoch,
3803
- source_kind: "transcript",
3804
- transcript_index: transcriptIndex
3890
+ remote_source_id: message.remoteSourceId,
3891
+ source_kind: message.sourceKind,
3892
+ transcript_index: message.transcriptIndex
3805
3893
  });
3806
3894
  db.addToOutbox("chat_message", row.id);
3895
+ if (message.role === "user") {
3896
+ db.insertUserPrompt({
3897
+ session_id: sessionId,
3898
+ project_id: projectId,
3899
+ prompt: message.text,
3900
+ cwd,
3901
+ user_id: config.user_id,
3902
+ device_id: config.device_id,
3903
+ agent: "claude-code",
3904
+ created_at_epoch: createdAtEpoch
3905
+ });
3906
+ }
3807
3907
  if (db.vecAvailable) {
3808
3908
  const embedding = await embedText(composeChatEmbeddingText(message.text));
3809
3909
  if (embedding) {
@@ -3814,6 +3914,23 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
3814
3914
  }
3815
3915
  return { imported, total: messages.length };
3816
3916
  }
3917
+ function dedupeHistoryMessages(messages) {
3918
+ const deduped = [];
3919
+ for (const message of messages) {
3920
+ const compact = message.text.replace(/\s+/g, " ").trim();
3921
+ if (!compact)
3922
+ continue;
3923
+ const previous = deduped[deduped.length - 1];
3924
+ if (previous && previous.text.replace(/\s+/g, " ").trim() === compact)
3925
+ continue;
3926
+ deduped.push({ ...message, text: compact });
3927
+ }
3928
+ return deduped;
3929
+ }
3930
+ function buildHistorySourceId(sessionId, createdAtEpoch, text) {
3931
+ const digest = createHash3("sha1").update(text).digest("hex").slice(0, 12);
3932
+ return `history:${sessionId}:${createdAtEpoch}:${digest}`;
3933
+ }
3817
3934
  function truncateTranscript(messages, maxBytes = 50000) {
3818
3935
  const lines = [];
3819
3936
  for (const msg of messages) {
@@ -3889,6 +4006,16 @@ async function saveTranscriptResults(db, config, results, sessionId, cwd) {
3889
4006
  return saved;
3890
4007
  }
3891
4008
 
4009
+ // src/tools/recent-chat.ts
4010
+ function getChatCaptureOrigin(message) {
4011
+ if (message.source_kind === "transcript")
4012
+ return "transcript";
4013
+ if (typeof message.remote_source_id === "string" && message.remote_source_id.startsWith("history:")) {
4014
+ return "history";
4015
+ }
4016
+ return "hook";
4017
+ }
4018
+
3892
4019
  // src/tools/session-story.ts
3893
4020
  function getSessionStory(db, input) {
3894
4021
  const session = db.getSessionById(input.session_id);
@@ -4011,9 +4138,9 @@ function collectProvenanceSummary(observations) {
4011
4138
  }
4012
4139
  function summarizeChatSources(messages) {
4013
4140
  return messages.reduce((summary, message) => {
4014
- summary[message.source_kind] += 1;
4141
+ summary[getChatCaptureOrigin(message)] += 1;
4015
4142
  return summary;
4016
- }, { transcript: 0, hook: 0 });
4143
+ }, { transcript: 0, history: 0, hook: 0 });
4017
4144
  }
4018
4145
 
4019
4146
  // src/tools/handoffs.ts
@@ -2203,6 +2203,16 @@ function computeObservationPriority(obs, nowEpoch) {
2203
2203
  return computeBlendedScore(obs.quality, obs.created_at_epoch, nowEpoch) + observationTypeBoost(obs.type);
2204
2204
  }
2205
2205
 
2206
+ // src/tools/recent-chat.ts
2207
+ function getChatCaptureOrigin(message) {
2208
+ if (message.source_kind === "transcript")
2209
+ return "transcript";
2210
+ if (typeof message.remote_source_id === "string" && message.remote_source_id.startsWith("history:")) {
2211
+ return "history";
2212
+ }
2213
+ return "hook";
2214
+ }
2215
+
2206
2216
  // src/tools/session-story.ts
2207
2217
  function getSessionStory(db, input) {
2208
2218
  const session = db.getSessionById(input.session_id);
@@ -2325,9 +2335,9 @@ function collectProvenanceSummary(observations) {
2325
2335
  }
2326
2336
  function summarizeChatSources(messages) {
2327
2337
  return messages.reduce((summary, message) => {
2328
- summary[message.source_kind] += 1;
2338
+ summary[getChatCaptureOrigin(message)] += 1;
2329
2339
  return summary;
2330
- }, { transcript: 0, hook: 0 });
2340
+ }, { transcript: 0, history: 0, hook: 0 });
2331
2341
  }
2332
2342
 
2333
2343
  // src/tools/save.ts
@@ -3974,6 +3984,7 @@ function getRecentOutcomes(db, projectId, userId, recentSessions) {
3974
3984
  }
3975
3985
 
3976
3986
  // src/capture/transcript.ts
3987
+ import { createHash as createHash3 } from "node:crypto";
3977
3988
  import { readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
3978
3989
  import { join as join3 } from "node:path";
3979
3990
  import { homedir as homedir2 } from "node:os";
@@ -4027,23 +4038,109 @@ function readTranscript(sessionId, cwd, transcriptPath) {
4027
4038
  }
4028
4039
  return messages;
4029
4040
  }
4041
+ function resolveHistoryPath(historyPath) {
4042
+ if (historyPath)
4043
+ return historyPath;
4044
+ const override = process.env["ENGRM_CLAUDE_HISTORY_PATH"];
4045
+ if (override)
4046
+ return override;
4047
+ return join3(homedir2(), ".claude", "history.jsonl");
4048
+ }
4049
+ function readHistoryFallback(sessionId, cwd, opts) {
4050
+ const path = resolveHistoryPath(opts?.historyPath);
4051
+ if (!existsSync3(path))
4052
+ return [];
4053
+ let raw;
4054
+ try {
4055
+ raw = readFileSync3(path, "utf-8");
4056
+ } catch {
4057
+ return [];
4058
+ }
4059
+ const targetCanonical = detectProject(cwd).canonical_id;
4060
+ const windowStart = Math.max(0, (opts?.startedAtEpoch ?? Math.floor(Date.now() / 1000) - 6 * 3600) - 600);
4061
+ const windowEnd = (opts?.completedAtEpoch ?? Math.floor(Date.now() / 1000)) + 600;
4062
+ const entries = [];
4063
+ for (const line of raw.split(`
4064
+ `)) {
4065
+ if (!line.trim())
4066
+ continue;
4067
+ let entry;
4068
+ try {
4069
+ entry = JSON.parse(line);
4070
+ } catch {
4071
+ continue;
4072
+ }
4073
+ if (typeof entry?.display !== "string" || typeof entry?.timestamp !== "number")
4074
+ continue;
4075
+ const createdAtEpoch = Math.floor(entry.timestamp / 1000);
4076
+ entries.push({
4077
+ display: entry.display.trim(),
4078
+ project: typeof entry.project === "string" ? entry.project : "",
4079
+ sessionId: typeof entry.sessionId === "string" ? entry.sessionId : "",
4080
+ timestamp: createdAtEpoch
4081
+ });
4082
+ }
4083
+ const bySession = entries.filter((entry) => entry.display.length > 0 && entry.sessionId === sessionId).sort((a, b) => a.timestamp - b.timestamp);
4084
+ if (bySession.length > 0) {
4085
+ return dedupeHistoryMessages(bySession.map((entry) => ({
4086
+ role: "user",
4087
+ text: entry.display,
4088
+ createdAtEpoch: entry.timestamp
4089
+ })));
4090
+ }
4091
+ const byProjectAndWindow = entries.filter((entry) => {
4092
+ if (entry.display.length === 0)
4093
+ return false;
4094
+ if (entry.timestamp < windowStart || entry.timestamp > windowEnd)
4095
+ return false;
4096
+ if (!entry.project)
4097
+ return false;
4098
+ return detectProject(entry.project).canonical_id === targetCanonical;
4099
+ }).sort((a, b) => a.timestamp - b.timestamp);
4100
+ return dedupeHistoryMessages(byProjectAndWindow.map((entry) => ({
4101
+ role: "user",
4102
+ text: entry.display,
4103
+ createdAtEpoch: entry.timestamp
4104
+ })));
4105
+ }
4030
4106
  async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
4031
- const messages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
4107
+ const session = db.getSessionById(sessionId);
4108
+ const transcriptMessages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
4032
4109
  ...message,
4033
4110
  text: message.text.trim()
4034
4111
  })).filter((message) => message.text.length > 0);
4112
+ const messages = transcriptMessages.length > 0 ? transcriptMessages.map((message, index) => ({
4113
+ ...message,
4114
+ sourceKind: "transcript",
4115
+ transcriptIndex: index + 1,
4116
+ createdAtEpoch: null,
4117
+ remoteSourceId: null
4118
+ })) : readHistoryFallback(sessionId, cwd, {
4119
+ startedAtEpoch: session?.started_at_epoch ?? null,
4120
+ completedAtEpoch: session?.completed_at_epoch ?? null
4121
+ }).map((message) => ({
4122
+ role: message.role,
4123
+ text: message.text,
4124
+ sourceKind: "hook",
4125
+ transcriptIndex: null,
4126
+ createdAtEpoch: message.createdAtEpoch,
4127
+ remoteSourceId: buildHistorySourceId(sessionId, message.createdAtEpoch, message.text)
4128
+ }));
4035
4129
  if (messages.length === 0)
4036
4130
  return { imported: 0, total: 0 };
4037
- const session = db.getSessionById(sessionId);
4038
4131
  const projectId = session?.project_id ?? null;
4039
4132
  const now = Math.floor(Date.now() / 1000);
4040
4133
  let imported = 0;
4041
4134
  for (let index = 0;index < messages.length; index++) {
4042
- const transcriptIndex = index + 1;
4043
- if (db.getTranscriptChatMessage(sessionId, transcriptIndex))
4044
- continue;
4045
4135
  const message = messages[index];
4046
- const createdAtEpoch = Math.max(0, now - (messages.length - transcriptIndex));
4136
+ const transcriptIndex = message.transcriptIndex ?? index + 1;
4137
+ if (message.sourceKind === "transcript" && db.getTranscriptChatMessage(sessionId, transcriptIndex)) {
4138
+ continue;
4139
+ }
4140
+ if (message.remoteSourceId && db.getChatMessageByRemoteSourceId(message.remoteSourceId)) {
4141
+ continue;
4142
+ }
4143
+ const createdAtEpoch = message.createdAtEpoch ?? Math.max(0, now - (messages.length - transcriptIndex));
4047
4144
  const row = db.insertChatMessage({
4048
4145
  session_id: sessionId,
4049
4146
  project_id: projectId,
@@ -4053,10 +4150,23 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
4053
4150
  device_id: config.device_id,
4054
4151
  agent: "claude-code",
4055
4152
  created_at_epoch: createdAtEpoch,
4056
- source_kind: "transcript",
4057
- transcript_index: transcriptIndex
4153
+ remote_source_id: message.remoteSourceId,
4154
+ source_kind: message.sourceKind,
4155
+ transcript_index: message.transcriptIndex
4058
4156
  });
4059
4157
  db.addToOutbox("chat_message", row.id);
4158
+ if (message.role === "user") {
4159
+ db.insertUserPrompt({
4160
+ session_id: sessionId,
4161
+ project_id: projectId,
4162
+ prompt: message.text,
4163
+ cwd,
4164
+ user_id: config.user_id,
4165
+ device_id: config.device_id,
4166
+ agent: "claude-code",
4167
+ created_at_epoch: createdAtEpoch
4168
+ });
4169
+ }
4060
4170
  if (db.vecAvailable) {
4061
4171
  const embedding = await embedText(composeChatEmbeddingText(message.text));
4062
4172
  if (embedding) {
@@ -4067,6 +4177,23 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
4067
4177
  }
4068
4178
  return { imported, total: messages.length };
4069
4179
  }
4180
+ function dedupeHistoryMessages(messages) {
4181
+ const deduped = [];
4182
+ for (const message of messages) {
4183
+ const compact = message.text.replace(/\s+/g, " ").trim();
4184
+ if (!compact)
4185
+ continue;
4186
+ const previous = deduped[deduped.length - 1];
4187
+ if (previous && previous.text.replace(/\s+/g, " ").trim() === compact)
4188
+ continue;
4189
+ deduped.push({ ...message, text: compact });
4190
+ }
4191
+ return deduped;
4192
+ }
4193
+ function buildHistorySourceId(sessionId, createdAtEpoch, text) {
4194
+ const digest = createHash3("sha1").update(text).digest("hex").slice(0, 12);
4195
+ return `history:${sessionId}:${createdAtEpoch}:${digest}`;
4196
+ }
4070
4197
  function truncateTranscript(messages, maxBytes = 50000) {
4071
4198
  const lines = [];
4072
4199
  for (const msg of messages) {
@@ -473,6 +473,16 @@ function normalizeItem(value) {
473
473
  return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
474
474
  }
475
475
 
476
+ // src/tools/recent-chat.ts
477
+ function getChatCaptureOrigin(message) {
478
+ if (message.source_kind === "transcript")
479
+ return "transcript";
480
+ if (typeof message.remote_source_id === "string" && message.remote_source_id.startsWith("history:")) {
481
+ return "history";
482
+ }
483
+ return "hook";
484
+ }
485
+
476
486
  // src/tools/session-story.ts
477
487
  function getSessionStory(db, input) {
478
488
  const session = db.getSessionById(input.session_id);
@@ -595,9 +605,9 @@ function collectProvenanceSummary(observations) {
595
605
  }
596
606
  function summarizeChatSources(messages) {
597
607
  return messages.reduce((summary, message) => {
598
- summary[message.source_kind] += 1;
608
+ summary[getChatCaptureOrigin(message)] += 1;
599
609
  return summary;
600
- }, { transcript: 0, hook: 0 });
610
+ }, { transcript: 0, history: 0, hook: 0 });
601
611
  }
602
612
 
603
613
  // src/tools/save.ts
@@ -3060,7 +3070,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
3060
3070
  import { join as join3 } from "node:path";
3061
3071
  import { homedir } from "node:os";
3062
3072
  var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
3063
- var CLIENT_VERSION = "0.4.28";
3073
+ var CLIENT_VERSION = "0.4.29";
3064
3074
  function hashFile(filePath) {
3065
3075
  try {
3066
3076
  if (!existsSync3(filePath))
@@ -3082,7 +3082,7 @@ function buildBeacon(db, config, sessionId, metrics) {
3082
3082
  sentinel_used: valueSignals.security_findings_count > 0,
3083
3083
  risk_score: riskScore,
3084
3084
  stacks_detected: stacks,
3085
- client_version: "0.4.28",
3085
+ client_version: "0.4.29",
3086
3086
  context_observations_injected: metrics?.contextObsInjected ?? 0,
3087
3087
  context_total_available: metrics?.contextTotalAvailable ?? 0,
3088
3088
  recall_attempts: metrics?.recallAttempts ?? 0,
@@ -3270,6 +3270,7 @@ function detectProjectFromTouchedPaths(paths, fallbackCwd) {
3270
3270
  }
3271
3271
 
3272
3272
  // src/capture/transcript.ts
3273
+ import { createHash as createHash3 } from "node:crypto";
3273
3274
  import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
3274
3275
  import { join as join5 } from "node:path";
3275
3276
  import { homedir as homedir3 } from "node:os";
@@ -3983,23 +3984,109 @@ function readTranscript(sessionId, cwd, transcriptPath) {
3983
3984
  }
3984
3985
  return messages;
3985
3986
  }
3987
+ function resolveHistoryPath(historyPath) {
3988
+ if (historyPath)
3989
+ return historyPath;
3990
+ const override = process.env["ENGRM_CLAUDE_HISTORY_PATH"];
3991
+ if (override)
3992
+ return override;
3993
+ return join5(homedir3(), ".claude", "history.jsonl");
3994
+ }
3995
+ function readHistoryFallback(sessionId, cwd, opts) {
3996
+ const path = resolveHistoryPath(opts?.historyPath);
3997
+ if (!existsSync4(path))
3998
+ return [];
3999
+ let raw;
4000
+ try {
4001
+ raw = readFileSync4(path, "utf-8");
4002
+ } catch {
4003
+ return [];
4004
+ }
4005
+ const targetCanonical = detectProject(cwd).canonical_id;
4006
+ const windowStart = Math.max(0, (opts?.startedAtEpoch ?? Math.floor(Date.now() / 1000) - 6 * 3600) - 600);
4007
+ const windowEnd = (opts?.completedAtEpoch ?? Math.floor(Date.now() / 1000)) + 600;
4008
+ const entries = [];
4009
+ for (const line of raw.split(`
4010
+ `)) {
4011
+ if (!line.trim())
4012
+ continue;
4013
+ let entry;
4014
+ try {
4015
+ entry = JSON.parse(line);
4016
+ } catch {
4017
+ continue;
4018
+ }
4019
+ if (typeof entry?.display !== "string" || typeof entry?.timestamp !== "number")
4020
+ continue;
4021
+ const createdAtEpoch = Math.floor(entry.timestamp / 1000);
4022
+ entries.push({
4023
+ display: entry.display.trim(),
4024
+ project: typeof entry.project === "string" ? entry.project : "",
4025
+ sessionId: typeof entry.sessionId === "string" ? entry.sessionId : "",
4026
+ timestamp: createdAtEpoch
4027
+ });
4028
+ }
4029
+ const bySession = entries.filter((entry) => entry.display.length > 0 && entry.sessionId === sessionId).sort((a, b) => a.timestamp - b.timestamp);
4030
+ if (bySession.length > 0) {
4031
+ return dedupeHistoryMessages(bySession.map((entry) => ({
4032
+ role: "user",
4033
+ text: entry.display,
4034
+ createdAtEpoch: entry.timestamp
4035
+ })));
4036
+ }
4037
+ const byProjectAndWindow = entries.filter((entry) => {
4038
+ if (entry.display.length === 0)
4039
+ return false;
4040
+ if (entry.timestamp < windowStart || entry.timestamp > windowEnd)
4041
+ return false;
4042
+ if (!entry.project)
4043
+ return false;
4044
+ return detectProject(entry.project).canonical_id === targetCanonical;
4045
+ }).sort((a, b) => a.timestamp - b.timestamp);
4046
+ return dedupeHistoryMessages(byProjectAndWindow.map((entry) => ({
4047
+ role: "user",
4048
+ text: entry.display,
4049
+ createdAtEpoch: entry.timestamp
4050
+ })));
4051
+ }
3986
4052
  async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
3987
- const messages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
4053
+ const session = db.getSessionById(sessionId);
4054
+ const transcriptMessages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
3988
4055
  ...message,
3989
4056
  text: message.text.trim()
3990
4057
  })).filter((message) => message.text.length > 0);
4058
+ const messages = transcriptMessages.length > 0 ? transcriptMessages.map((message, index) => ({
4059
+ ...message,
4060
+ sourceKind: "transcript",
4061
+ transcriptIndex: index + 1,
4062
+ createdAtEpoch: null,
4063
+ remoteSourceId: null
4064
+ })) : readHistoryFallback(sessionId, cwd, {
4065
+ startedAtEpoch: session?.started_at_epoch ?? null,
4066
+ completedAtEpoch: session?.completed_at_epoch ?? null
4067
+ }).map((message) => ({
4068
+ role: message.role,
4069
+ text: message.text,
4070
+ sourceKind: "hook",
4071
+ transcriptIndex: null,
4072
+ createdAtEpoch: message.createdAtEpoch,
4073
+ remoteSourceId: buildHistorySourceId(sessionId, message.createdAtEpoch, message.text)
4074
+ }));
3991
4075
  if (messages.length === 0)
3992
4076
  return { imported: 0, total: 0 };
3993
- const session = db.getSessionById(sessionId);
3994
4077
  const projectId = session?.project_id ?? null;
3995
4078
  const now = Math.floor(Date.now() / 1000);
3996
4079
  let imported = 0;
3997
4080
  for (let index = 0;index < messages.length; index++) {
3998
- const transcriptIndex = index + 1;
3999
- if (db.getTranscriptChatMessage(sessionId, transcriptIndex))
4000
- continue;
4001
4081
  const message = messages[index];
4002
- const createdAtEpoch = Math.max(0, now - (messages.length - transcriptIndex));
4082
+ const transcriptIndex = message.transcriptIndex ?? index + 1;
4083
+ if (message.sourceKind === "transcript" && db.getTranscriptChatMessage(sessionId, transcriptIndex)) {
4084
+ continue;
4085
+ }
4086
+ if (message.remoteSourceId && db.getChatMessageByRemoteSourceId(message.remoteSourceId)) {
4087
+ continue;
4088
+ }
4089
+ const createdAtEpoch = message.createdAtEpoch ?? Math.max(0, now - (messages.length - transcriptIndex));
4003
4090
  const row = db.insertChatMessage({
4004
4091
  session_id: sessionId,
4005
4092
  project_id: projectId,
@@ -4009,10 +4096,23 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
4009
4096
  device_id: config.device_id,
4010
4097
  agent: "claude-code",
4011
4098
  created_at_epoch: createdAtEpoch,
4012
- source_kind: "transcript",
4013
- transcript_index: transcriptIndex
4099
+ remote_source_id: message.remoteSourceId,
4100
+ source_kind: message.sourceKind,
4101
+ transcript_index: message.transcriptIndex
4014
4102
  });
4015
4103
  db.addToOutbox("chat_message", row.id);
4104
+ if (message.role === "user") {
4105
+ db.insertUserPrompt({
4106
+ session_id: sessionId,
4107
+ project_id: projectId,
4108
+ prompt: message.text,
4109
+ cwd,
4110
+ user_id: config.user_id,
4111
+ device_id: config.device_id,
4112
+ agent: "claude-code",
4113
+ created_at_epoch: createdAtEpoch
4114
+ });
4115
+ }
4016
4116
  if (db.vecAvailable) {
4017
4117
  const embedding = await embedText(composeChatEmbeddingText(message.text));
4018
4118
  if (embedding) {
@@ -4023,6 +4123,23 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
4023
4123
  }
4024
4124
  return { imported, total: messages.length };
4025
4125
  }
4126
+ function dedupeHistoryMessages(messages) {
4127
+ const deduped = [];
4128
+ for (const message of messages) {
4129
+ const compact = message.text.replace(/\s+/g, " ").trim();
4130
+ if (!compact)
4131
+ continue;
4132
+ const previous = deduped[deduped.length - 1];
4133
+ if (previous && previous.text.replace(/\s+/g, " ").trim() === compact)
4134
+ continue;
4135
+ deduped.push({ ...message, text: compact });
4136
+ }
4137
+ return deduped;
4138
+ }
4139
+ function buildHistorySourceId(sessionId, createdAtEpoch, text) {
4140
+ const digest = createHash3("sha1").update(text).digest("hex").slice(0, 12);
4141
+ return `history:${sessionId}:${createdAtEpoch}:${digest}`;
4142
+ }
4026
4143
  function truncateTranscript(messages, maxBytes = 50000) {
4027
4144
  const lines = [];
4028
4145
  for (const msg of messages) {
@@ -4098,6 +4215,16 @@ async function saveTranscriptResults(db, config, results, sessionId, cwd) {
4098
4215
  return saved;
4099
4216
  }
4100
4217
 
4218
+ // src/tools/recent-chat.ts
4219
+ function getChatCaptureOrigin(message) {
4220
+ if (message.source_kind === "transcript")
4221
+ return "transcript";
4222
+ if (typeof message.remote_source_id === "string" && message.remote_source_id.startsWith("history:")) {
4223
+ return "history";
4224
+ }
4225
+ return "hook";
4226
+ }
4227
+
4101
4228
  // src/tools/session-story.ts
4102
4229
  function getSessionStory(db, input) {
4103
4230
  const session = db.getSessionById(input.session_id);
@@ -4220,9 +4347,9 @@ function collectProvenanceSummary(observations) {
4220
4347
  }
4221
4348
  function summarizeChatSources(messages) {
4222
4349
  return messages.reduce((summary, message) => {
4223
- summary[message.source_kind] += 1;
4350
+ summary[getChatCaptureOrigin(message)] += 1;
4224
4351
  return summary;
4225
- }, { transcript: 0, hook: 0 });
4352
+ }, { transcript: 0, history: 0, hook: 0 });
4226
4353
  }
4227
4354
 
4228
4355
  // src/tools/handoffs.ts