engrm 0.4.47 → 0.4.48

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.
@@ -4275,80 +4275,6 @@ function findTranscriptPathBySessionId(sessionId) {
4275
4275
  }
4276
4276
  return null;
4277
4277
  }
4278
- function truncateTranscript(messages, maxBytes = 50000) {
4279
- const lines = [];
4280
- for (const msg of messages) {
4281
- lines.push(`[${msg.role}]: ${msg.text}`);
4282
- }
4283
- const full = lines.join(`
4284
- `);
4285
- if (Buffer.byteLength(full, "utf-8") <= maxBytes)
4286
- return full;
4287
- let result = "";
4288
- for (let i = lines.length - 1;i >= 0; i--) {
4289
- const candidate = lines[i] + `
4290
- ` + result;
4291
- if (Buffer.byteLength(candidate, "utf-8") > maxBytes)
4292
- break;
4293
- result = candidate;
4294
- }
4295
- return result.trim();
4296
- }
4297
- async function analyzeTranscript(config, transcript, sessionId) {
4298
- if (!config.candengo_url || !config.candengo_api_key)
4299
- return null;
4300
- const url = `${config.candengo_url}/v1/mem/transcript-analysis`;
4301
- const controller = new AbortController;
4302
- const timeout = setTimeout(() => controller.abort(), 30000);
4303
- try {
4304
- const response = await fetch(url, {
4305
- method: "POST",
4306
- headers: {
4307
- "Content-Type": "application/json",
4308
- Authorization: `Bearer ${config.candengo_api_key}`
4309
- },
4310
- body: JSON.stringify({
4311
- transcript,
4312
- session_id: sessionId
4313
- }),
4314
- signal: controller.signal
4315
- });
4316
- if (!response.ok)
4317
- return null;
4318
- const data = await response.json();
4319
- if (!Array.isArray(data.plans) || !Array.isArray(data.decisions) || !Array.isArray(data.insights)) {
4320
- return null;
4321
- }
4322
- return data;
4323
- } catch {
4324
- return null;
4325
- } finally {
4326
- clearTimeout(timeout);
4327
- }
4328
- }
4329
- async function saveTranscriptResults(db, config, results, sessionId, cwd) {
4330
- let saved = 0;
4331
- const items = [
4332
- ...results.plans.map((item) => ({ item, type: "decision" })),
4333
- ...results.decisions.map((item) => ({ item, type: "decision" })),
4334
- ...results.insights.map((item) => ({ item, type: "discovery" }))
4335
- ];
4336
- for (const { item, type } of items) {
4337
- if (!item.title || item.title.trim().length === 0)
4338
- continue;
4339
- const result = await saveObservation(db, config, {
4340
- type,
4341
- title: item.title.slice(0, 80),
4342
- narrative: item.narrative,
4343
- concepts: item.concepts,
4344
- session_id: sessionId,
4345
- cwd
4346
- });
4347
- if (result.success)
4348
- saved++;
4349
- }
4350
- return saved;
4351
- }
4352
4278
 
4353
4279
  // src/tools/recent-chat.ts
4354
4280
  function summarizeChatSources(messages) {
@@ -4563,80 +4563,6 @@ function findTranscriptPathBySessionId(sessionId) {
4563
4563
  }
4564
4564
  return null;
4565
4565
  }
4566
- function truncateTranscript(messages, maxBytes = 50000) {
4567
- const lines = [];
4568
- for (const msg of messages) {
4569
- lines.push(`[${msg.role}]: ${msg.text}`);
4570
- }
4571
- const full = lines.join(`
4572
- `);
4573
- if (Buffer.byteLength(full, "utf-8") <= maxBytes)
4574
- return full;
4575
- let result = "";
4576
- for (let i = lines.length - 1;i >= 0; i--) {
4577
- const candidate = lines[i] + `
4578
- ` + result;
4579
- if (Buffer.byteLength(candidate, "utf-8") > maxBytes)
4580
- break;
4581
- result = candidate;
4582
- }
4583
- return result.trim();
4584
- }
4585
- async function analyzeTranscript(config, transcript, sessionId) {
4586
- if (!config.candengo_url || !config.candengo_api_key)
4587
- return null;
4588
- const url = `${config.candengo_url}/v1/mem/transcript-analysis`;
4589
- const controller = new AbortController;
4590
- const timeout = setTimeout(() => controller.abort(), 30000);
4591
- try {
4592
- const response = await fetch(url, {
4593
- method: "POST",
4594
- headers: {
4595
- "Content-Type": "application/json",
4596
- Authorization: `Bearer ${config.candengo_api_key}`
4597
- },
4598
- body: JSON.stringify({
4599
- transcript,
4600
- session_id: sessionId
4601
- }),
4602
- signal: controller.signal
4603
- });
4604
- if (!response.ok)
4605
- return null;
4606
- const data = await response.json();
4607
- if (!Array.isArray(data.plans) || !Array.isArray(data.decisions) || !Array.isArray(data.insights)) {
4608
- return null;
4609
- }
4610
- return data;
4611
- } catch {
4612
- return null;
4613
- } finally {
4614
- clearTimeout(timeout);
4615
- }
4616
- }
4617
- async function saveTranscriptResults(db, config, results, sessionId, cwd) {
4618
- let saved = 0;
4619
- const items = [
4620
- ...results.plans.map((item) => ({ item, type: "decision" })),
4621
- ...results.decisions.map((item) => ({ item, type: "decision" })),
4622
- ...results.insights.map((item) => ({ item, type: "discovery" }))
4623
- ];
4624
- for (const { item, type } of items) {
4625
- if (!item.title || item.title.trim().length === 0)
4626
- continue;
4627
- const result = await saveObservation(db, config, {
4628
- type,
4629
- title: item.title.slice(0, 80),
4630
- narrative: item.narrative,
4631
- concepts: item.concepts,
4632
- session_id: sessionId,
4633
- cwd
4634
- });
4635
- if (result.success)
4636
- saved++;
4637
- }
4638
- return saved;
4639
- }
4640
4566
 
4641
4567
  // hooks/pre-compact.ts
4642
4568
  function formatCurrentSessionContext(observations) {
@@ -3280,7 +3280,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
3280
3280
  import { join as join3 } from "node:path";
3281
3281
  import { homedir } from "node:os";
3282
3282
  var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
3283
- var CLIENT_VERSION = "0.4.47";
3283
+ var CLIENT_VERSION = "0.4.48";
3284
3284
  function hashFile(filePath) {
3285
3285
  try {
3286
3286
  if (!existsSync3(filePath))
@@ -2,6 +2,13 @@
2
2
  import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
+ // hooks/stop.ts
6
+ import { spawn } from "node:child_process";
7
+ import { mkdtempSync, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { dirname as dirname2, join as join6 } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
5
12
  // src/intelligence/observation-priority.ts
6
13
  var RECENCY_WINDOW_SECONDS = 30 * 86400;
7
14
  function computeBlendedScore(quality, createdAtEpoch, nowEpoch) {
@@ -3507,7 +3514,7 @@ function buildBeacon(db, config, sessionId, metrics) {
3507
3514
  sentinel_used: valueSignals.security_findings_count > 0,
3508
3515
  risk_score: riskScore,
3509
3516
  stacks_detected: stacks,
3510
- client_version: "0.4.47",
3517
+ client_version: "0.4.48",
3511
3518
  context_observations_injected: metrics?.contextObsInjected ?? 0,
3512
3519
  context_total_available: metrics?.contextTotalAvailable ?? 0,
3513
3520
  recall_attempts: metrics?.recallAttempts ?? 0,
@@ -4656,533 +4663,11 @@ function findTranscriptPathBySessionId(sessionId) {
4656
4663
  }
4657
4664
  return null;
4658
4665
  }
4659
- function truncateTranscript(messages, maxBytes = 50000) {
4660
- const lines = [];
4661
- for (const msg of messages) {
4662
- lines.push(`[${msg.role}]: ${msg.text}`);
4663
- }
4664
- const full = lines.join(`
4665
- `);
4666
- if (Buffer.byteLength(full, "utf-8") <= maxBytes)
4667
- return full;
4668
- let result = "";
4669
- for (let i = lines.length - 1;i >= 0; i--) {
4670
- const candidate = lines[i] + `
4671
- ` + result;
4672
- if (Buffer.byteLength(candidate, "utf-8") > maxBytes)
4673
- break;
4674
- result = candidate;
4675
- }
4676
- return result.trim();
4677
- }
4678
- async function analyzeTranscript(config, transcript, sessionId) {
4679
- if (!config.candengo_url || !config.candengo_api_key)
4680
- return null;
4681
- const url = `${config.candengo_url}/v1/mem/transcript-analysis`;
4682
- const controller = new AbortController;
4683
- const timeout = setTimeout(() => controller.abort(), 30000);
4684
- try {
4685
- const response = await fetch(url, {
4686
- method: "POST",
4687
- headers: {
4688
- "Content-Type": "application/json",
4689
- Authorization: `Bearer ${config.candengo_api_key}`
4690
- },
4691
- body: JSON.stringify({
4692
- transcript,
4693
- session_id: sessionId
4694
- }),
4695
- signal: controller.signal
4696
- });
4697
- if (!response.ok)
4698
- return null;
4699
- const data = await response.json();
4700
- if (!Array.isArray(data.plans) || !Array.isArray(data.decisions) || !Array.isArray(data.insights)) {
4701
- return null;
4702
- }
4703
- return data;
4704
- } catch {
4705
- return null;
4706
- } finally {
4707
- clearTimeout(timeout);
4708
- }
4709
- }
4710
- async function saveTranscriptResults(db, config, results, sessionId, cwd) {
4711
- let saved = 0;
4712
- const items = [
4713
- ...results.plans.map((item) => ({ item, type: "decision" })),
4714
- ...results.decisions.map((item) => ({ item, type: "decision" })),
4715
- ...results.insights.map((item) => ({ item, type: "discovery" }))
4716
- ];
4717
- for (const { item, type } of items) {
4718
- if (!item.title || item.title.trim().length === 0)
4719
- continue;
4720
- const result = await saveObservation(db, config, {
4721
- type,
4722
- title: item.title.slice(0, 80),
4723
- narrative: item.narrative,
4724
- concepts: item.concepts,
4725
- session_id: sessionId,
4726
- cwd
4727
- });
4728
- if (result.success)
4729
- saved++;
4730
- }
4731
- return saved;
4732
- }
4733
-
4734
- // src/tools/recent-chat.ts
4735
- function summarizeChatSources(messages) {
4736
- return messages.reduce((summary, message) => {
4737
- summary[getChatCaptureOrigin(message)] += 1;
4738
- return summary;
4739
- }, { transcript: 0, history: 0, hook: 0 });
4740
- }
4741
- function getChatCoverageState(messagesOrSummary) {
4742
- const summary = Array.isArray(messagesOrSummary) ? summarizeChatSources(messagesOrSummary) : messagesOrSummary;
4743
- if (summary.transcript > 0)
4744
- return "transcript-backed";
4745
- if (summary.history > 0)
4746
- return "history-backed";
4747
- if (summary.hook > 0)
4748
- return "hook-only";
4749
- return "none";
4750
- }
4751
- function getChatCaptureOrigin(message) {
4752
- if (message.source_kind === "transcript")
4753
- return "transcript";
4754
- if (typeof message.remote_source_id === "string" && message.remote_source_id.startsWith("history:")) {
4755
- return "history";
4756
- }
4757
- return "hook";
4758
- }
4759
-
4760
- // src/tools/session-story.ts
4761
- function getSessionStory(db, input) {
4762
- const session = db.getSessionById(input.session_id);
4763
- const summary = db.getSessionSummary(input.session_id);
4764
- const prompts = db.getSessionUserPrompts(input.session_id, 50);
4765
- const chatMessages = db.getSessionChatMessages(input.session_id, 50);
4766
- const toolEvents = db.getSessionToolEvents(input.session_id, 100);
4767
- const allObservations = db.getObservationsBySession(input.session_id);
4768
- const handoffs = allObservations.filter((obs) => looksLikeHandoff(obs));
4769
- const rollingHandoffDrafts = handoffs.filter((obs) => isDraftHandoff(obs));
4770
- const savedHandoffs = handoffs.filter((obs) => !isDraftHandoff(obs));
4771
- const observations = allObservations.filter((obs) => !looksLikeHandoff(obs));
4772
- const metrics = db.getSessionMetrics(input.session_id);
4773
- const projectName = session?.project_id !== null && session?.project_id !== undefined ? db.getProjectById(session.project_id)?.name ?? null : null;
4774
- const latestRequest = prompts[prompts.length - 1]?.prompt?.trim() || summary?.request?.trim() || null;
4775
- return {
4776
- session,
4777
- project_name: projectName,
4778
- summary,
4779
- prompts,
4780
- chat_messages: chatMessages,
4781
- chat_source_summary: summarizeChatSources(chatMessages),
4782
- chat_coverage_state: getChatCoverageState(chatMessages),
4783
- tool_events: toolEvents,
4784
- observations,
4785
- handoffs,
4786
- saved_handoffs: savedHandoffs,
4787
- rolling_handoff_drafts: rollingHandoffDrafts,
4788
- metrics,
4789
- capture_state: classifyCaptureState({
4790
- hasSummary: Boolean(summary?.request || summary?.completed),
4791
- promptCount: prompts.length,
4792
- toolEventCount: toolEvents.length
4793
- }),
4794
- capture_gaps: buildCaptureGaps({
4795
- promptCount: prompts.length,
4796
- toolEventCount: toolEvents.length,
4797
- toolCallsCount: metrics?.tool_calls_count ?? 0,
4798
- observationCount: observations.length,
4799
- hasSummary: Boolean(summary?.request || summary?.completed)
4800
- }),
4801
- latest_request: latestRequest,
4802
- recent_outcomes: collectRecentOutcomes(observations),
4803
- hot_files: collectHotFiles(observations),
4804
- provenance_summary: collectProvenanceSummary(observations)
4805
- };
4806
- }
4807
- function classifyCaptureState(input) {
4808
- if (input.promptCount > 0 && input.toolEventCount > 0)
4809
- return "rich";
4810
- if (input.promptCount > 0 || input.toolEventCount > 0)
4811
- return "partial";
4812
- if (input.hasSummary)
4813
- return "summary-only";
4814
- return "legacy";
4815
- }
4816
- function buildCaptureGaps(input) {
4817
- const gaps = [];
4818
- if (input.promptCount === 0)
4819
- gaps.push("missing prompts");
4820
- if (input.toolCallsCount > 0 && input.toolEventCount === 0) {
4821
- gaps.push("missing raw tool chronology");
4822
- } else if (input.toolEventCount === 0) {
4823
- gaps.push("no tool events");
4824
- }
4825
- if (input.observationCount === 0 && input.hasSummary) {
4826
- gaps.push("summary without reusable observations");
4827
- }
4828
- return gaps;
4829
- }
4830
- function collectRecentOutcomes(observations) {
4831
- const seen = new Set;
4832
- const outcomes = [];
4833
- for (const obs of observations) {
4834
- if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
4835
- continue;
4836
- const title = obs.title.trim();
4837
- if (!title || looksLikeFileOperationTitle(title))
4838
- continue;
4839
- const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
4840
- if (seen.has(normalized))
4841
- continue;
4842
- seen.add(normalized);
4843
- outcomes.push(title);
4844
- if (outcomes.length >= 6)
4845
- break;
4846
- }
4847
- return outcomes;
4848
- }
4849
- function collectHotFiles(observations) {
4850
- const counts = new Map;
4851
- for (const obs of observations) {
4852
- for (const path of [...parseJsonArray3(obs.files_modified), ...parseJsonArray3(obs.files_read)]) {
4853
- counts.set(path, (counts.get(path) ?? 0) + 1);
4854
- }
4855
- }
4856
- return Array.from(counts.entries()).map(([path, count]) => ({ path, count })).sort((a, b) => b.count - a.count || a.path.localeCompare(b.path)).slice(0, 8);
4857
- }
4858
- function parseJsonArray3(value) {
4859
- if (!value)
4860
- return [];
4861
- try {
4862
- const parsed = JSON.parse(value);
4863
- return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
4864
- } catch {
4865
- return [];
4866
- }
4867
- }
4868
- function looksLikeFileOperationTitle(value) {
4869
- return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
4870
- }
4871
- function collectProvenanceSummary(observations) {
4872
- const counts = new Map;
4873
- for (const obs of observations) {
4874
- if (!obs.source_tool)
4875
- continue;
4876
- counts.set(obs.source_tool, (counts.get(obs.source_tool) ?? 0) + 1);
4877
- }
4878
- return Array.from(counts.entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
4879
- }
4880
-
4881
- // src/tools/handoffs.ts
4882
- async function upsertRollingHandoff(db, config, input) {
4883
- const resolved = resolveTargetSession(db, input.cwd, config.user_id, input.session_id);
4884
- if (!resolved.session) {
4885
- return {
4886
- success: false,
4887
- reason: "No recent session found to draft a handoff yet"
4888
- };
4889
- }
4890
- const story = getSessionStory(db, { session_id: resolved.session.session_id });
4891
- if (!story.session) {
4892
- return {
4893
- success: false,
4894
- reason: `Session ${resolved.session.session_id} not found`
4895
- };
4896
- }
4897
- const includeChat = input.include_chat === true || input.include_chat !== false && shouldAutoIncludeChat(story);
4898
- const chatLimit = Math.max(1, Math.min(input.chat_limit ?? 3, 6));
4899
- const title = `Handoff Draft: ${buildHandoffTitle(story.summary, story.latest_request)}`;
4900
- const narrative = buildHandoffNarrative(story.summary, story, {
4901
- includeChat,
4902
- chatLimit
4903
- });
4904
- const facts = buildHandoffFacts(story.summary, story);
4905
- const concepts = buildDraftHandoffConcepts(story.project_name, story.capture_state);
4906
- const existing = getSessionRollingHandoff(db, story.session.session_id);
4907
- const now = Math.floor(Date.now() / 1000);
4908
- if (existing) {
4909
- const nextFacts = JSON.stringify(facts);
4910
- const nextConcepts = JSON.stringify(concepts);
4911
- const shouldRefresh = existing.title !== title || (existing.narrative ?? null) !== narrative || (existing.facts ?? null) !== nextFacts || (existing.concepts ?? null) !== nextConcepts || now - existing.created_at_epoch >= 120;
4912
- if (!shouldRefresh) {
4913
- return {
4914
- success: true,
4915
- observation_id: existing.id,
4916
- session_id: story.session.session_id,
4917
- title: existing.title
4918
- };
4919
- }
4920
- const updated = db.updateObservationContent(existing.id, {
4921
- title,
4922
- narrative,
4923
- facts: nextFacts,
4924
- concepts: nextConcepts,
4925
- created_at_epoch: now
4926
- });
4927
- if (!updated) {
4928
- return {
4929
- success: false,
4930
- reason: "Failed to update rolling handoff draft"
4931
- };
4932
- }
4933
- db.addToOutbox("observation", updated.id);
4934
- return {
4935
- success: true,
4936
- observation_id: updated.id,
4937
- session_id: story.session.session_id,
4938
- title: updated.title
4939
- };
4940
- }
4941
- const result = await saveObservation(db, config, {
4942
- type: "message",
4943
- title,
4944
- narrative,
4945
- facts,
4946
- concepts,
4947
- session_id: story.session.session_id,
4948
- cwd: input.cwd,
4949
- agent: "engrm-handoff",
4950
- source_tool: "rolling_handoff"
4951
- });
4952
- return {
4953
- success: result.success,
4954
- observation_id: result.observation_id,
4955
- session_id: story.session.session_id,
4956
- title,
4957
- reason: result.reason
4958
- };
4959
- }
4960
- function getRecentHandoffs(db, input) {
4961
- const limit = Math.max(1, Math.min(input.limit ?? 10, 25));
4962
- const queryLimit = input.current_device_id ? Math.max(limit, Math.min(limit * 5, 50)) : limit;
4963
- const projectScoped = input.project_scoped !== false;
4964
- let projectId = null;
4965
- let projectName;
4966
- if (projectScoped) {
4967
- const cwd = input.cwd ?? process.cwd();
4968
- const detected = detectProject(cwd);
4969
- const project = db.getProjectByCanonicalId(detected.canonical_id);
4970
- if (project) {
4971
- projectId = project.id;
4972
- projectName = project.name;
4973
- }
4974
- }
4975
- const conditions = [
4976
- "o.type = 'message'",
4977
- "o.lifecycle IN ('active', 'aging', 'pinned')",
4978
- "o.superseded_by IS NULL",
4979
- `(o.title LIKE 'Handoff:%' OR o.concepts LIKE '%"handoff"%')`
4980
- ];
4981
- const params = [];
4982
- if (input.user_id) {
4983
- conditions.push("(o.sensitivity != 'personal' OR o.user_id = ?)");
4984
- params.push(input.user_id);
4985
- }
4986
- if (projectId !== null) {
4987
- conditions.push("o.project_id = ?");
4988
- params.push(projectId);
4989
- }
4990
- params.push(queryLimit);
4991
- const handoffs = db.db.query(`SELECT o.*, p.name AS project_name
4992
- FROM observations o
4993
- LEFT JOIN projects p ON p.id = o.project_id
4994
- WHERE ${conditions.join(" AND ")}
4995
- ORDER BY o.created_at_epoch DESC, o.id DESC
4996
- LIMIT ?`).all(...params);
4997
- handoffs.sort((a, b) => compareHandoffs(a, b, input.current_device_id));
4998
- return {
4999
- handoffs: handoffs.slice(0, limit),
5000
- project: projectName
5001
- };
5002
- }
5003
- function formatHandoffSource(handoff) {
5004
- const ageSeconds = Math.max(0, Math.floor(Date.now() / 1000) - handoff.created_at_epoch);
5005
- const ageLabel = ageSeconds < 3600 ? `${Math.max(1, Math.floor(ageSeconds / 60) || 1)}m ago` : ageSeconds < 86400 ? `${Math.floor(ageSeconds / 3600)}h ago` : `${Math.floor(ageSeconds / 86400)}d ago`;
5006
- return `from ${handoff.device_id} · ${ageLabel}`;
5007
- }
5008
- function isDraftHandoff(obs) {
5009
- if (obs.title.startsWith("Handoff Draft:"))
5010
- return true;
5011
- const concepts = parseJsonArray4(obs.concepts);
5012
- return concepts.includes("draft-handoff") || concepts.includes("auto-handoff");
5013
- }
5014
- function getSessionRollingHandoff(db, sessionId) {
5015
- return db.db.query(`SELECT o.*, p.name AS project_name
5016
- FROM observations o
5017
- LEFT JOIN projects p ON p.id = o.project_id
5018
- WHERE o.session_id = ?
5019
- AND o.type = 'message'
5020
- AND o.lifecycle IN ('active', 'aging', 'pinned')
5021
- AND o.superseded_by IS NULL
5022
- AND (o.title LIKE 'Handoff Draft:%' OR o.concepts LIKE '%"draft-handoff"%')
5023
- ORDER BY o.created_at_epoch DESC, o.id DESC
5024
- LIMIT 1`).get(sessionId) ?? null;
5025
- }
5026
- function compareHandoffs(a, b, currentDeviceId) {
5027
- const aDraft = isDraftHandoff(a) ? 1 : 0;
5028
- const bDraft = isDraftHandoff(b) ? 1 : 0;
5029
- if (aDraft !== bDraft)
5030
- return aDraft - bDraft;
5031
- if (currentDeviceId) {
5032
- const aOther = a.device_id !== currentDeviceId ? 1 : 0;
5033
- const bOther = b.device_id !== currentDeviceId ? 1 : 0;
5034
- if (aOther !== bOther)
5035
- return bOther - aOther;
5036
- }
5037
- if (b.created_at_epoch !== a.created_at_epoch) {
5038
- return b.created_at_epoch - a.created_at_epoch;
5039
- }
5040
- return b.id - a.id;
5041
- }
5042
- function resolveTargetSession(db, cwd, userId, sessionId) {
5043
- if (sessionId) {
5044
- const session = db.getSessionById(sessionId);
5045
- if (!session)
5046
- return { session: null };
5047
- const projectName = session.project_id ? db.getProjectById(session.project_id)?.name : undefined;
5048
- return {
5049
- session: {
5050
- ...session,
5051
- project_name: projectName ?? null,
5052
- request: db.getSessionSummary(sessionId)?.request ?? null,
5053
- completed: db.getSessionSummary(sessionId)?.completed ?? null,
5054
- current_thread: db.getSessionSummary(sessionId)?.current_thread ?? null,
5055
- capture_state: db.getSessionSummary(sessionId)?.capture_state ?? null,
5056
- recent_tool_names: db.getSessionSummary(sessionId)?.recent_tool_names ?? null,
5057
- hot_files: db.getSessionSummary(sessionId)?.hot_files ?? null,
5058
- recent_outcomes: db.getSessionSummary(sessionId)?.recent_outcomes ?? null,
5059
- prompt_count: db.getSessionUserPrompts(sessionId, 200).length,
5060
- tool_event_count: db.getSessionToolEvents(sessionId, 200).length
5061
- },
5062
- projectName: projectName ?? undefined
5063
- };
5064
- }
5065
- const detected = detectProject(cwd ?? process.cwd());
5066
- const project = db.getProjectByCanonicalId(detected.canonical_id);
5067
- const sessions = db.getRecentSessions(project?.id ?? null, 10, userId);
5068
- return {
5069
- session: sessions[0] ?? null,
5070
- projectName: project?.name
5071
- };
5072
- }
5073
- function buildHandoffTitle(summary, latestRequest, explicit) {
5074
- const chosen = explicit?.trim() || summary?.current_thread?.trim() || summary?.completed?.trim() || latestRequest?.trim() || "Current work";
5075
- return compactLine2(chosen) ?? "Current work";
5076
- }
5077
- function buildHandoffNarrative(summary, story, options) {
5078
- const sections = [];
5079
- if (summary?.request || story.latest_request) {
5080
- sections.push(`Request: ${summary?.request ?? story.latest_request}`);
5081
- }
5082
- if (summary?.current_thread) {
5083
- sections.push(`Current thread: ${summary.current_thread}`);
5084
- }
5085
- if (summary?.investigated) {
5086
- sections.push(`Investigated: ${summary.investigated}`);
5087
- }
5088
- if (summary?.learned) {
5089
- sections.push(`Learned: ${summary.learned}`);
5090
- }
5091
- if (summary?.completed) {
5092
- sections.push(`Completed: ${summary.completed}`);
5093
- }
5094
- if (summary?.next_steps) {
5095
- sections.push(`Next Steps: ${summary.next_steps}`);
5096
- }
5097
- if (story.recent_outcomes.length > 0) {
5098
- sections.push(`Recent outcomes:
5099
- ${story.recent_outcomes.slice(0, 5).map((item) => `- ${item}`).join(`
5100
- `)}`);
5101
- }
5102
- if (story.hot_files.length > 0) {
5103
- sections.push(`Hot files:
5104
- ${story.hot_files.slice(0, 5).map((file) => `- ${file.path}`).join(`
5105
- `)}`);
5106
- }
5107
- if (story.provenance_summary.length > 0) {
5108
- sections.push(`Tool trail:
5109
- ${story.provenance_summary.slice(0, 5).map((item) => `- ${item.tool}: ${item.count}`).join(`
5110
- `)}`);
5111
- }
5112
- if (options.includeChat && story.chat_messages.length > 0) {
5113
- const chatLines = story.chat_messages.slice(-options.chatLimit).map((msg) => `- [${msg.role}] ${compactLine2(msg.content) ?? msg.content.slice(0, 120)}`);
5114
- sections.push(`Chat snippets:
5115
- ${chatLines.join(`
5116
- `)}`);
5117
- }
5118
- return sections.filter(Boolean).join(`
5119
-
5120
- `);
5121
- }
5122
- function shouldAutoIncludeChat(story) {
5123
- if (story.chat_messages.length === 0)
5124
- return false;
5125
- const summary = story.summary;
5126
- const thinSummary = !summary?.completed && !summary?.current_thread && story.recent_outcomes.length < 2;
5127
- const thinChronology = story.capture_state !== "rich" || story.tool_events.length === 0;
5128
- return thinSummary || thinChronology;
5129
- }
5130
- function buildHandoffFacts(summary, story) {
5131
- const facts = [
5132
- `session_id=${story.session?.session_id ?? "unknown"}`,
5133
- `capture_state=${story.capture_state}`,
5134
- story.project_name ? `project=${story.project_name}` : null,
5135
- summary?.current_thread ? `current_thread=${summary.current_thread}` : null,
5136
- story.hot_files[0] ? `hot_file=${story.hot_files[0].path}` : null,
5137
- story.provenance_summary[0] ? `primary_tool=${story.provenance_summary[0].tool}` : null
5138
- ];
5139
- return facts.filter((item) => Boolean(item));
5140
- }
5141
- function buildDraftHandoffConcepts(projectName, captureState) {
5142
- return [
5143
- "handoff",
5144
- "draft-handoff",
5145
- "auto-handoff",
5146
- `capture:${captureState}`,
5147
- ...projectName ? [projectName] : []
5148
- ];
5149
- }
5150
- function looksLikeHandoff(obs) {
5151
- if (obs.title.startsWith("Handoff:") || obs.title.startsWith("Handoff Draft:"))
5152
- return true;
5153
- const concepts = parseJsonArray4(obs.concepts);
5154
- return concepts.includes("handoff") || concepts.includes("session-handoff") || concepts.includes("draft-handoff");
5155
- }
5156
- function parseJsonArray4(value) {
5157
- if (!value)
5158
- return [];
5159
- try {
5160
- const parsed = JSON.parse(value);
5161
- return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
5162
- } catch {
5163
- return [];
5164
- }
5165
- }
5166
- function compactLine2(value) {
5167
- if (value === null || value === undefined)
5168
- return null;
5169
- let text;
5170
- if (typeof value === "string") {
5171
- text = value;
5172
- } else {
5173
- try {
5174
- text = JSON.stringify(value);
5175
- } catch {
5176
- text = String(value);
5177
- }
5178
- }
5179
- const trimmed = text.replace(/\s+/g, " ").trim();
5180
- if (!trimmed)
5181
- return null;
5182
- return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
5183
- }
5184
4666
 
5185
4667
  // hooks/stop.ts
4668
+ var thisFile = fileURLToPath(import.meta.url);
4669
+ var thisDir = dirname2(thisFile);
4670
+ var isWorker = process.env.ENGRM_STOP_WORKER === "1";
5186
4671
  function printRetrospective(summary) {
5187
4672
  const lines = [];
5188
4673
  lines.push("");
@@ -5217,9 +4702,53 @@ function printRetrospective(summary) {
5217
4702
  `));
5218
4703
  }
5219
4704
  async function main() {
5220
- const event = await parseStdinJson();
5221
- if (!event)
4705
+ if (isWorker) {
4706
+ await runWorkerFromFile(process.argv[2]);
4707
+ process.exit(0);
4708
+ }
4709
+ const raw = await readStdin();
4710
+ if (!raw.trim())
5222
4711
  process.exit(0);
4712
+ spawnDetachedWorker(raw);
4713
+ process.exit(0);
4714
+ }
4715
+ function spawnDetachedWorker(raw) {
4716
+ const tempDir = mkdtempSync(join6(tmpdir(), "engrm-stop-"));
4717
+ const payloadPath = join6(tempDir, "event.json");
4718
+ writeFileSync2(payloadPath, raw, "utf-8");
4719
+ const isBun = process.execPath.endsWith("bun");
4720
+ const childArgs = isBun ? ["run", thisFile, payloadPath] : [thisFile, payloadPath];
4721
+ const child = spawn(process.execPath, childArgs, {
4722
+ detached: true,
4723
+ stdio: "ignore",
4724
+ env: {
4725
+ ...process.env,
4726
+ ENGRM_STOP_WORKER: "1"
4727
+ }
4728
+ });
4729
+ child.unref();
4730
+ }
4731
+ async function runWorkerFromFile(payloadPath) {
4732
+ if (!payloadPath)
4733
+ return;
4734
+ let raw = "";
4735
+ try {
4736
+ raw = readFileSync5(payloadPath, "utf-8");
4737
+ } catch {
4738
+ return;
4739
+ } finally {
4740
+ try {
4741
+ rmSync(dirname2(payloadPath), { recursive: true, force: true });
4742
+ } catch {}
4743
+ }
4744
+ let event = null;
4745
+ try {
4746
+ event = JSON.parse(raw);
4747
+ } catch {
4748
+ return;
4749
+ }
4750
+ if (!event)
4751
+ return;
5223
4752
  if (event.stop_hook_active)
5224
4753
  process.exit(0);
5225
4754
  const boot = bootstrapHook("stop");
@@ -5253,12 +4782,6 @@ async function main() {
5253
4782
  source_kind: "hook"
5254
4783
  });
5255
4784
  db.addToOutbox("chat_message", chatMessage.id);
5256
- if (db.vecAvailable) {
5257
- const chatEmbedding = await embedText(composeChatEmbeddingText(event.last_assistant_message));
5258
- if (chatEmbedding) {
5259
- db.vecChatInsert(chatMessage.id, chatEmbedding);
5260
- }
5261
- }
5262
4785
  }
5263
4786
  createAssistantCheckpoint(db, event.session_id, event.cwd, event.last_assistant_message);
5264
4787
  } catch {}
@@ -5273,10 +4796,6 @@ async function main() {
5273
4796
  if (summary) {
5274
4797
  const row = db.upsertSessionSummary(summary);
5275
4798
  db.addToOutbox("summary", row.id);
5276
- await upsertRollingHandoff(db, config, {
5277
- session_id: event.session_id,
5278
- cwd: event.cwd
5279
- });
5280
4799
  let securityFindings = [];
5281
4800
  try {
5282
4801
  if (session?.project_id) {
@@ -5313,36 +4832,19 @@ async function main() {
5313
4832
  createSessionDigest(db, event.session_id, event.cwd);
5314
4833
  } catch {}
5315
4834
  }
5316
- if (config.transcript_analysis?.enabled && event.session_id) {
5317
- try {
5318
- const messages = readTranscript(event.session_id, event.cwd, event.transcript_path);
5319
- if (messages.length > 10) {
5320
- const transcript = truncateTranscript(messages);
5321
- const results = await analyzeTranscript(config, transcript, event.session_id);
5322
- if (results) {
5323
- const saved = await saveTranscriptResults(db, config, results, event.session_id, event.cwd);
5324
- if (saved > 0) {
5325
- console.error(`
5326
- \uD83D\uDCA1 Engrm: Extracted ${saved} insight(s) from session transcript.`);
5327
- }
5328
- }
5329
- }
5330
- } catch {}
5331
- }
5332
- await pushOnce(db, config, { timeoutMs: 1500 });
4835
+ await withTimeout(pushOnce(db, config, { timeoutMs: 250 }), 350);
5333
4836
  try {
5334
4837
  if (event.session_id) {
5335
4838
  const metrics = readSessionMetrics(event.session_id);
5336
4839
  const beacon = buildBeacon(db, config, event.session_id, metrics);
5337
4840
  if (beacon) {
5338
- await sendBeacon(config, beacon);
4841
+ await withTimeout(sendBeacon(config, beacon), 150);
5339
4842
  }
5340
4843
  }
5341
4844
  } catch {}
5342
4845
  } finally {
5343
4846
  db.close();
5344
4847
  }
5345
- process.exit(0);
5346
4848
  }
5347
4849
  function buildFallbackSessionSummary(db, sessionId, projectId, userId, lastAssistantMessage) {
5348
4850
  const prompts = db.getSessionUserPrompts(sessionId, 10).filter((prompt) => isMeaningfulSummaryPrompt(prompt));
@@ -5554,6 +5056,12 @@ ${sections.join(`
5554
5056
  });
5555
5057
  db.addToOutbox("observation", digestObs.id);
5556
5058
  }
5059
+ async function withTimeout(promise, timeoutMs) {
5060
+ return await Promise.race([
5061
+ promise,
5062
+ new Promise((resolve2) => setTimeout(() => resolve2(null), timeoutMs))
5063
+ ]);
5064
+ }
5557
5065
  function createAssistantCheckpoint(db, sessionId, cwd, message) {
5558
5066
  const checkpoint = extractAssistantCheckpoint(message);
5559
5067
  if (!checkpoint)
@@ -5644,14 +5152,14 @@ function detectUnsavedPlans(message) {
5644
5152
  return hints;
5645
5153
  }
5646
5154
  function readSessionMetrics(sessionId) {
5647
- const { existsSync: existsSync5, readFileSync: readFileSync5, unlinkSync } = __require("node:fs");
5648
- const { join: join6 } = __require("node:path");
5155
+ const { existsSync: existsSync5, readFileSync: readFileSync6, unlinkSync } = __require("node:fs");
5156
+ const { join: join7 } = __require("node:path");
5649
5157
  const { homedir: homedir4 } = __require("node:os");
5650
5158
  const result = {};
5651
5159
  try {
5652
- const obsPath = join6(homedir4(), ".engrm", "observer-sessions", `${sessionId}.json`);
5160
+ const obsPath = join7(homedir4(), ".engrm", "observer-sessions", `${sessionId}.json`);
5653
5161
  if (existsSync5(obsPath)) {
5654
- const state = JSON.parse(readFileSync5(obsPath, "utf-8"));
5162
+ const state = JSON.parse(readFileSync6(obsPath, "utf-8"));
5655
5163
  if (typeof state.recallAttempts === "number")
5656
5164
  result.recallAttempts = state.recallAttempts;
5657
5165
  if (typeof state.recallHits === "number")
@@ -5659,9 +5167,9 @@ function readSessionMetrics(sessionId) {
5659
5167
  }
5660
5168
  } catch {}
5661
5169
  try {
5662
- const hookPath = join6(homedir4(), ".engrm", "hook-session-metrics.json");
5170
+ const hookPath = join7(homedir4(), ".engrm", "hook-session-metrics.json");
5663
5171
  if (existsSync5(hookPath)) {
5664
- const hookMetrics = JSON.parse(readFileSync5(hookPath, "utf-8"));
5172
+ const hookMetrics = JSON.parse(readFileSync6(hookPath, "utf-8"));
5665
5173
  if (typeof hookMetrics.contextObsInjected === "number")
5666
5174
  result.contextObsInjected = hookMetrics.contextObsInjected;
5667
5175
  if (typeof hookMetrics.contextTotalAvailable === "number")
@@ -5672,9 +5180,9 @@ function readSessionMetrics(sessionId) {
5672
5180
  }
5673
5181
  } catch {}
5674
5182
  try {
5675
- const mcpPath = join6(homedir4(), ".engrm", "mcp-session-metrics.json");
5183
+ const mcpPath = join7(homedir4(), ".engrm", "mcp-session-metrics.json");
5676
5184
  if (existsSync5(mcpPath)) {
5677
- const metrics = JSON.parse(readFileSync5(mcpPath, "utf-8"));
5185
+ const metrics = JSON.parse(readFileSync6(mcpPath, "utf-8"));
5678
5186
  if (typeof metrics.contextObsInjected === "number" && metrics.contextObsInjected > 0) {
5679
5187
  result.contextObsInjected = metrics.contextObsInjected;
5680
5188
  }
package/dist/server.js CHANGED
@@ -23464,7 +23464,7 @@ function installStdioLivenessGuards() {
23464
23464
  function buildServer() {
23465
23465
  const server = new McpServer({
23466
23466
  name: "engrm",
23467
- version: "0.4.47"
23467
+ version: "0.4.48"
23468
23468
  });
23469
23469
  const enabledToolNames = getEnabledToolNames(config2.tool_profile);
23470
23470
  const originalTool = server.tool.bind(server);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.47",
3
+ "version": "0.4.48",
4
4
  "description": "Shared memory across devices, sessions, and agents, with thin MCP tools for durable capture, live continuity, and Hermes-ready remote MCP support",
5
5
  "mcpName": "io.github.dr12hes/engrm",
6
6
  "type": "module",