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.
- package/dist/hooks/post-tool-use.js +0 -74
- package/dist/hooks/pre-compact.js +0 -74
- package/dist/hooks/session-start.js +1 -1
- package/dist/hooks/stop.js +73 -565
- package/dist/server.js +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
3283
|
+
var CLIENT_VERSION = "0.4.48";
|
|
3284
3284
|
function hashFile(filePath) {
|
|
3285
3285
|
try {
|
|
3286
3286
|
if (!existsSync3(filePath))
|
package/dist/hooks/stop.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
5221
|
-
|
|
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
|
-
|
|
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:
|
|
5648
|
-
const { join:
|
|
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 =
|
|
5160
|
+
const obsPath = join7(homedir4(), ".engrm", "observer-sessions", `${sessionId}.json`);
|
|
5653
5161
|
if (existsSync5(obsPath)) {
|
|
5654
|
-
const state = JSON.parse(
|
|
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 =
|
|
5170
|
+
const hookPath = join7(homedir4(), ".engrm", "hook-session-metrics.json");
|
|
5663
5171
|
if (existsSync5(hookPath)) {
|
|
5664
|
-
const hookMetrics = JSON.parse(
|
|
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 =
|
|
5183
|
+
const mcpPath = join7(homedir4(), ".engrm", "mcp-session-metrics.json");
|
|
5676
5184
|
if (existsSync5(mcpPath)) {
|
|
5677
|
-
const metrics = JSON.parse(
|
|
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.
|
|
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.
|
|
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",
|