engrm 0.4.46 → 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/cli.js +15 -0
- package/dist/hooks/elicitation-result.js +15 -0
- package/dist/hooks/post-tool-use.js +15 -74
- package/dist/hooks/pre-compact.js +15 -74
- package/dist/hooks/sentinel.js +15 -0
- package/dist/hooks/session-start.js +16 -1
- package/dist/hooks/stop.js +88 -565
- package/dist/hooks/user-prompt-submit.js +15 -0
- package/dist/server.js +16 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1005,6 +1005,20 @@ function ensureChatMessageColumns(db) {
|
|
|
1005
1005
|
db.exec("PRAGMA user_version = 17");
|
|
1006
1006
|
}
|
|
1007
1007
|
}
|
|
1008
|
+
function ensureObservationVectorTable(db) {
|
|
1009
|
+
if (!isVecExtensionLoaded(db))
|
|
1010
|
+
return;
|
|
1011
|
+
db.exec(`
|
|
1012
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
|
|
1013
|
+
observation_id INTEGER PRIMARY KEY,
|
|
1014
|
+
embedding FLOAT[384]
|
|
1015
|
+
);
|
|
1016
|
+
`);
|
|
1017
|
+
const current = getSchemaVersion(db);
|
|
1018
|
+
if (current < 4) {
|
|
1019
|
+
db.exec("PRAGMA user_version = 4");
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1008
1022
|
function ensureChatVectorTable(db) {
|
|
1009
1023
|
if (!isVecExtensionLoaded(db))
|
|
1010
1024
|
return;
|
|
@@ -1233,6 +1247,7 @@ class MemDatabase {
|
|
|
1233
1247
|
ensureObservationTypes(this.db);
|
|
1234
1248
|
ensureSessionSummaryColumns(this.db);
|
|
1235
1249
|
ensureChatMessageColumns(this.db);
|
|
1250
|
+
ensureObservationVectorTable(this.db);
|
|
1236
1251
|
ensureChatVectorTable(this.db);
|
|
1237
1252
|
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
1238
1253
|
}
|
|
@@ -1879,6 +1879,20 @@ function ensureChatMessageColumns(db) {
|
|
|
1879
1879
|
db.exec("PRAGMA user_version = 17");
|
|
1880
1880
|
}
|
|
1881
1881
|
}
|
|
1882
|
+
function ensureObservationVectorTable(db) {
|
|
1883
|
+
if (!isVecExtensionLoaded(db))
|
|
1884
|
+
return;
|
|
1885
|
+
db.exec(`
|
|
1886
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
|
|
1887
|
+
observation_id INTEGER PRIMARY KEY,
|
|
1888
|
+
embedding FLOAT[384]
|
|
1889
|
+
);
|
|
1890
|
+
`);
|
|
1891
|
+
const current = getSchemaVersion(db);
|
|
1892
|
+
if (current < 4) {
|
|
1893
|
+
db.exec("PRAGMA user_version = 4");
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1882
1896
|
function ensureChatVectorTable(db) {
|
|
1883
1897
|
if (!isVecExtensionLoaded(db))
|
|
1884
1898
|
return;
|
|
@@ -2107,6 +2121,7 @@ class MemDatabase {
|
|
|
2107
2121
|
ensureObservationTypes(this.db);
|
|
2108
2122
|
ensureSessionSummaryColumns(this.db);
|
|
2109
2123
|
ensureChatMessageColumns(this.db);
|
|
2124
|
+
ensureObservationVectorTable(this.db);
|
|
2110
2125
|
ensureChatVectorTable(this.db);
|
|
2111
2126
|
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
2112
2127
|
}
|
|
@@ -1183,6 +1183,20 @@ function ensureChatMessageColumns(db) {
|
|
|
1183
1183
|
db.exec("PRAGMA user_version = 17");
|
|
1184
1184
|
}
|
|
1185
1185
|
}
|
|
1186
|
+
function ensureObservationVectorTable(db) {
|
|
1187
|
+
if (!isVecExtensionLoaded(db))
|
|
1188
|
+
return;
|
|
1189
|
+
db.exec(`
|
|
1190
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
|
|
1191
|
+
observation_id INTEGER PRIMARY KEY,
|
|
1192
|
+
embedding FLOAT[384]
|
|
1193
|
+
);
|
|
1194
|
+
`);
|
|
1195
|
+
const current = getSchemaVersion(db);
|
|
1196
|
+
if (current < 4) {
|
|
1197
|
+
db.exec("PRAGMA user_version = 4");
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1186
1200
|
function ensureChatVectorTable(db) {
|
|
1187
1201
|
if (!isVecExtensionLoaded(db))
|
|
1188
1202
|
return;
|
|
@@ -1411,6 +1425,7 @@ class MemDatabase {
|
|
|
1411
1425
|
ensureObservationTypes(this.db);
|
|
1412
1426
|
ensureSessionSummaryColumns(this.db);
|
|
1413
1427
|
ensureChatMessageColumns(this.db);
|
|
1428
|
+
ensureObservationVectorTable(this.db);
|
|
1414
1429
|
ensureChatVectorTable(this.db);
|
|
1415
1430
|
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
1416
1431
|
}
|
|
@@ -4260,80 +4275,6 @@ function findTranscriptPathBySessionId(sessionId) {
|
|
|
4260
4275
|
}
|
|
4261
4276
|
return null;
|
|
4262
4277
|
}
|
|
4263
|
-
function truncateTranscript(messages, maxBytes = 50000) {
|
|
4264
|
-
const lines = [];
|
|
4265
|
-
for (const msg of messages) {
|
|
4266
|
-
lines.push(`[${msg.role}]: ${msg.text}`);
|
|
4267
|
-
}
|
|
4268
|
-
const full = lines.join(`
|
|
4269
|
-
`);
|
|
4270
|
-
if (Buffer.byteLength(full, "utf-8") <= maxBytes)
|
|
4271
|
-
return full;
|
|
4272
|
-
let result = "";
|
|
4273
|
-
for (let i = lines.length - 1;i >= 0; i--) {
|
|
4274
|
-
const candidate = lines[i] + `
|
|
4275
|
-
` + result;
|
|
4276
|
-
if (Buffer.byteLength(candidate, "utf-8") > maxBytes)
|
|
4277
|
-
break;
|
|
4278
|
-
result = candidate;
|
|
4279
|
-
}
|
|
4280
|
-
return result.trim();
|
|
4281
|
-
}
|
|
4282
|
-
async function analyzeTranscript(config, transcript, sessionId) {
|
|
4283
|
-
if (!config.candengo_url || !config.candengo_api_key)
|
|
4284
|
-
return null;
|
|
4285
|
-
const url = `${config.candengo_url}/v1/mem/transcript-analysis`;
|
|
4286
|
-
const controller = new AbortController;
|
|
4287
|
-
const timeout = setTimeout(() => controller.abort(), 30000);
|
|
4288
|
-
try {
|
|
4289
|
-
const response = await fetch(url, {
|
|
4290
|
-
method: "POST",
|
|
4291
|
-
headers: {
|
|
4292
|
-
"Content-Type": "application/json",
|
|
4293
|
-
Authorization: `Bearer ${config.candengo_api_key}`
|
|
4294
|
-
},
|
|
4295
|
-
body: JSON.stringify({
|
|
4296
|
-
transcript,
|
|
4297
|
-
session_id: sessionId
|
|
4298
|
-
}),
|
|
4299
|
-
signal: controller.signal
|
|
4300
|
-
});
|
|
4301
|
-
if (!response.ok)
|
|
4302
|
-
return null;
|
|
4303
|
-
const data = await response.json();
|
|
4304
|
-
if (!Array.isArray(data.plans) || !Array.isArray(data.decisions) || !Array.isArray(data.insights)) {
|
|
4305
|
-
return null;
|
|
4306
|
-
}
|
|
4307
|
-
return data;
|
|
4308
|
-
} catch {
|
|
4309
|
-
return null;
|
|
4310
|
-
} finally {
|
|
4311
|
-
clearTimeout(timeout);
|
|
4312
|
-
}
|
|
4313
|
-
}
|
|
4314
|
-
async function saveTranscriptResults(db, config, results, sessionId, cwd) {
|
|
4315
|
-
let saved = 0;
|
|
4316
|
-
const items = [
|
|
4317
|
-
...results.plans.map((item) => ({ item, type: "decision" })),
|
|
4318
|
-
...results.decisions.map((item) => ({ item, type: "decision" })),
|
|
4319
|
-
...results.insights.map((item) => ({ item, type: "discovery" }))
|
|
4320
|
-
];
|
|
4321
|
-
for (const { item, type } of items) {
|
|
4322
|
-
if (!item.title || item.title.trim().length === 0)
|
|
4323
|
-
continue;
|
|
4324
|
-
const result = await saveObservation(db, config, {
|
|
4325
|
-
type,
|
|
4326
|
-
title: item.title.slice(0, 80),
|
|
4327
|
-
narrative: item.narrative,
|
|
4328
|
-
concepts: item.concepts,
|
|
4329
|
-
session_id: sessionId,
|
|
4330
|
-
cwd
|
|
4331
|
-
});
|
|
4332
|
-
if (result.success)
|
|
4333
|
-
saved++;
|
|
4334
|
-
}
|
|
4335
|
-
return saved;
|
|
4336
|
-
}
|
|
4337
4278
|
|
|
4338
4279
|
// src/tools/recent-chat.ts
|
|
4339
4280
|
function summarizeChatSources(messages) {
|
|
@@ -977,6 +977,20 @@ function ensureChatMessageColumns(db) {
|
|
|
977
977
|
db.exec("PRAGMA user_version = 17");
|
|
978
978
|
}
|
|
979
979
|
}
|
|
980
|
+
function ensureObservationVectorTable(db) {
|
|
981
|
+
if (!isVecExtensionLoaded(db))
|
|
982
|
+
return;
|
|
983
|
+
db.exec(`
|
|
984
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
|
|
985
|
+
observation_id INTEGER PRIMARY KEY,
|
|
986
|
+
embedding FLOAT[384]
|
|
987
|
+
);
|
|
988
|
+
`);
|
|
989
|
+
const current = getSchemaVersion(db);
|
|
990
|
+
if (current < 4) {
|
|
991
|
+
db.exec("PRAGMA user_version = 4");
|
|
992
|
+
}
|
|
993
|
+
}
|
|
980
994
|
function ensureChatVectorTable(db) {
|
|
981
995
|
if (!isVecExtensionLoaded(db))
|
|
982
996
|
return;
|
|
@@ -1205,6 +1219,7 @@ class MemDatabase {
|
|
|
1205
1219
|
ensureObservationTypes(this.db);
|
|
1206
1220
|
ensureSessionSummaryColumns(this.db);
|
|
1207
1221
|
ensureChatMessageColumns(this.db);
|
|
1222
|
+
ensureObservationVectorTable(this.db);
|
|
1208
1223
|
ensureChatVectorTable(this.db);
|
|
1209
1224
|
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
1210
1225
|
}
|
|
@@ -4548,80 +4563,6 @@ function findTranscriptPathBySessionId(sessionId) {
|
|
|
4548
4563
|
}
|
|
4549
4564
|
return null;
|
|
4550
4565
|
}
|
|
4551
|
-
function truncateTranscript(messages, maxBytes = 50000) {
|
|
4552
|
-
const lines = [];
|
|
4553
|
-
for (const msg of messages) {
|
|
4554
|
-
lines.push(`[${msg.role}]: ${msg.text}`);
|
|
4555
|
-
}
|
|
4556
|
-
const full = lines.join(`
|
|
4557
|
-
`);
|
|
4558
|
-
if (Buffer.byteLength(full, "utf-8") <= maxBytes)
|
|
4559
|
-
return full;
|
|
4560
|
-
let result = "";
|
|
4561
|
-
for (let i = lines.length - 1;i >= 0; i--) {
|
|
4562
|
-
const candidate = lines[i] + `
|
|
4563
|
-
` + result;
|
|
4564
|
-
if (Buffer.byteLength(candidate, "utf-8") > maxBytes)
|
|
4565
|
-
break;
|
|
4566
|
-
result = candidate;
|
|
4567
|
-
}
|
|
4568
|
-
return result.trim();
|
|
4569
|
-
}
|
|
4570
|
-
async function analyzeTranscript(config, transcript, sessionId) {
|
|
4571
|
-
if (!config.candengo_url || !config.candengo_api_key)
|
|
4572
|
-
return null;
|
|
4573
|
-
const url = `${config.candengo_url}/v1/mem/transcript-analysis`;
|
|
4574
|
-
const controller = new AbortController;
|
|
4575
|
-
const timeout = setTimeout(() => controller.abort(), 30000);
|
|
4576
|
-
try {
|
|
4577
|
-
const response = await fetch(url, {
|
|
4578
|
-
method: "POST",
|
|
4579
|
-
headers: {
|
|
4580
|
-
"Content-Type": "application/json",
|
|
4581
|
-
Authorization: `Bearer ${config.candengo_api_key}`
|
|
4582
|
-
},
|
|
4583
|
-
body: JSON.stringify({
|
|
4584
|
-
transcript,
|
|
4585
|
-
session_id: sessionId
|
|
4586
|
-
}),
|
|
4587
|
-
signal: controller.signal
|
|
4588
|
-
});
|
|
4589
|
-
if (!response.ok)
|
|
4590
|
-
return null;
|
|
4591
|
-
const data = await response.json();
|
|
4592
|
-
if (!Array.isArray(data.plans) || !Array.isArray(data.decisions) || !Array.isArray(data.insights)) {
|
|
4593
|
-
return null;
|
|
4594
|
-
}
|
|
4595
|
-
return data;
|
|
4596
|
-
} catch {
|
|
4597
|
-
return null;
|
|
4598
|
-
} finally {
|
|
4599
|
-
clearTimeout(timeout);
|
|
4600
|
-
}
|
|
4601
|
-
}
|
|
4602
|
-
async function saveTranscriptResults(db, config, results, sessionId, cwd) {
|
|
4603
|
-
let saved = 0;
|
|
4604
|
-
const items = [
|
|
4605
|
-
...results.plans.map((item) => ({ item, type: "decision" })),
|
|
4606
|
-
...results.decisions.map((item) => ({ item, type: "decision" })),
|
|
4607
|
-
...results.insights.map((item) => ({ item, type: "discovery" }))
|
|
4608
|
-
];
|
|
4609
|
-
for (const { item, type } of items) {
|
|
4610
|
-
if (!item.title || item.title.trim().length === 0)
|
|
4611
|
-
continue;
|
|
4612
|
-
const result = await saveObservation(db, config, {
|
|
4613
|
-
type,
|
|
4614
|
-
title: item.title.slice(0, 80),
|
|
4615
|
-
narrative: item.narrative,
|
|
4616
|
-
concepts: item.concepts,
|
|
4617
|
-
session_id: sessionId,
|
|
4618
|
-
cwd
|
|
4619
|
-
});
|
|
4620
|
-
if (result.success)
|
|
4621
|
-
saved++;
|
|
4622
|
-
}
|
|
4623
|
-
return saved;
|
|
4624
|
-
}
|
|
4625
4566
|
|
|
4626
4567
|
// hooks/pre-compact.ts
|
|
4627
4568
|
function formatCurrentSessionContext(observations) {
|
package/dist/hooks/sentinel.js
CHANGED
|
@@ -1053,6 +1053,20 @@ function ensureChatMessageColumns(db) {
|
|
|
1053
1053
|
db.exec("PRAGMA user_version = 17");
|
|
1054
1054
|
}
|
|
1055
1055
|
}
|
|
1056
|
+
function ensureObservationVectorTable(db) {
|
|
1057
|
+
if (!isVecExtensionLoaded(db))
|
|
1058
|
+
return;
|
|
1059
|
+
db.exec(`
|
|
1060
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
|
|
1061
|
+
observation_id INTEGER PRIMARY KEY,
|
|
1062
|
+
embedding FLOAT[384]
|
|
1063
|
+
);
|
|
1064
|
+
`);
|
|
1065
|
+
const current = getSchemaVersion(db);
|
|
1066
|
+
if (current < 4) {
|
|
1067
|
+
db.exec("PRAGMA user_version = 4");
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1056
1070
|
function ensureChatVectorTable(db) {
|
|
1057
1071
|
if (!isVecExtensionLoaded(db))
|
|
1058
1072
|
return;
|
|
@@ -1281,6 +1295,7 @@ class MemDatabase {
|
|
|
1281
1295
|
ensureObservationTypes(this.db);
|
|
1282
1296
|
ensureSessionSummaryColumns(this.db);
|
|
1283
1297
|
ensureChatMessageColumns(this.db);
|
|
1298
|
+
ensureObservationVectorTable(this.db);
|
|
1284
1299
|
ensureChatVectorTable(this.db);
|
|
1285
1300
|
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
1286
1301
|
}
|
|
@@ -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))
|
|
@@ -4920,6 +4920,20 @@ function ensureChatMessageColumns(db) {
|
|
|
4920
4920
|
db.exec("PRAGMA user_version = 17");
|
|
4921
4921
|
}
|
|
4922
4922
|
}
|
|
4923
|
+
function ensureObservationVectorTable(db) {
|
|
4924
|
+
if (!isVecExtensionLoaded(db))
|
|
4925
|
+
return;
|
|
4926
|
+
db.exec(`
|
|
4927
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
|
|
4928
|
+
observation_id INTEGER PRIMARY KEY,
|
|
4929
|
+
embedding FLOAT[384]
|
|
4930
|
+
);
|
|
4931
|
+
`);
|
|
4932
|
+
const current = getSchemaVersion(db);
|
|
4933
|
+
if (current < 4) {
|
|
4934
|
+
db.exec("PRAGMA user_version = 4");
|
|
4935
|
+
}
|
|
4936
|
+
}
|
|
4923
4937
|
function ensureChatVectorTable(db) {
|
|
4924
4938
|
if (!isVecExtensionLoaded(db))
|
|
4925
4939
|
return;
|
|
@@ -5068,6 +5082,7 @@ class MemDatabase {
|
|
|
5068
5082
|
ensureObservationTypes(this.db);
|
|
5069
5083
|
ensureSessionSummaryColumns(this.db);
|
|
5070
5084
|
ensureChatMessageColumns(this.db);
|
|
5085
|
+
ensureObservationVectorTable(this.db);
|
|
5071
5086
|
ensureChatVectorTable(this.db);
|
|
5072
5087
|
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
5073
5088
|
}
|
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) {
|
|
@@ -1290,6 +1297,20 @@ function ensureChatMessageColumns(db) {
|
|
|
1290
1297
|
db.exec("PRAGMA user_version = 17");
|
|
1291
1298
|
}
|
|
1292
1299
|
}
|
|
1300
|
+
function ensureObservationVectorTable(db) {
|
|
1301
|
+
if (!isVecExtensionLoaded(db))
|
|
1302
|
+
return;
|
|
1303
|
+
db.exec(`
|
|
1304
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
|
|
1305
|
+
observation_id INTEGER PRIMARY KEY,
|
|
1306
|
+
embedding FLOAT[384]
|
|
1307
|
+
);
|
|
1308
|
+
`);
|
|
1309
|
+
const current = getSchemaVersion(db);
|
|
1310
|
+
if (current < 4) {
|
|
1311
|
+
db.exec("PRAGMA user_version = 4");
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1293
1314
|
function ensureChatVectorTable(db) {
|
|
1294
1315
|
if (!isVecExtensionLoaded(db))
|
|
1295
1316
|
return;
|
|
@@ -1438,6 +1459,7 @@ class MemDatabase {
|
|
|
1438
1459
|
ensureObservationTypes(this.db);
|
|
1439
1460
|
ensureSessionSummaryColumns(this.db);
|
|
1440
1461
|
ensureChatMessageColumns(this.db);
|
|
1462
|
+
ensureObservationVectorTable(this.db);
|
|
1441
1463
|
ensureChatVectorTable(this.db);
|
|
1442
1464
|
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
1443
1465
|
}
|
|
@@ -3492,7 +3514,7 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
3492
3514
|
sentinel_used: valueSignals.security_findings_count > 0,
|
|
3493
3515
|
risk_score: riskScore,
|
|
3494
3516
|
stacks_detected: stacks,
|
|
3495
|
-
client_version: "0.4.
|
|
3517
|
+
client_version: "0.4.48",
|
|
3496
3518
|
context_observations_injected: metrics?.contextObsInjected ?? 0,
|
|
3497
3519
|
context_total_available: metrics?.contextTotalAvailable ?? 0,
|
|
3498
3520
|
recall_attempts: metrics?.recallAttempts ?? 0,
|
|
@@ -4641,533 +4663,11 @@ function findTranscriptPathBySessionId(sessionId) {
|
|
|
4641
4663
|
}
|
|
4642
4664
|
return null;
|
|
4643
4665
|
}
|
|
4644
|
-
function truncateTranscript(messages, maxBytes = 50000) {
|
|
4645
|
-
const lines = [];
|
|
4646
|
-
for (const msg of messages) {
|
|
4647
|
-
lines.push(`[${msg.role}]: ${msg.text}`);
|
|
4648
|
-
}
|
|
4649
|
-
const full = lines.join(`
|
|
4650
|
-
`);
|
|
4651
|
-
if (Buffer.byteLength(full, "utf-8") <= maxBytes)
|
|
4652
|
-
return full;
|
|
4653
|
-
let result = "";
|
|
4654
|
-
for (let i = lines.length - 1;i >= 0; i--) {
|
|
4655
|
-
const candidate = lines[i] + `
|
|
4656
|
-
` + result;
|
|
4657
|
-
if (Buffer.byteLength(candidate, "utf-8") > maxBytes)
|
|
4658
|
-
break;
|
|
4659
|
-
result = candidate;
|
|
4660
|
-
}
|
|
4661
|
-
return result.trim();
|
|
4662
|
-
}
|
|
4663
|
-
async function analyzeTranscript(config, transcript, sessionId) {
|
|
4664
|
-
if (!config.candengo_url || !config.candengo_api_key)
|
|
4665
|
-
return null;
|
|
4666
|
-
const url = `${config.candengo_url}/v1/mem/transcript-analysis`;
|
|
4667
|
-
const controller = new AbortController;
|
|
4668
|
-
const timeout = setTimeout(() => controller.abort(), 30000);
|
|
4669
|
-
try {
|
|
4670
|
-
const response = await fetch(url, {
|
|
4671
|
-
method: "POST",
|
|
4672
|
-
headers: {
|
|
4673
|
-
"Content-Type": "application/json",
|
|
4674
|
-
Authorization: `Bearer ${config.candengo_api_key}`
|
|
4675
|
-
},
|
|
4676
|
-
body: JSON.stringify({
|
|
4677
|
-
transcript,
|
|
4678
|
-
session_id: sessionId
|
|
4679
|
-
}),
|
|
4680
|
-
signal: controller.signal
|
|
4681
|
-
});
|
|
4682
|
-
if (!response.ok)
|
|
4683
|
-
return null;
|
|
4684
|
-
const data = await response.json();
|
|
4685
|
-
if (!Array.isArray(data.plans) || !Array.isArray(data.decisions) || !Array.isArray(data.insights)) {
|
|
4686
|
-
return null;
|
|
4687
|
-
}
|
|
4688
|
-
return data;
|
|
4689
|
-
} catch {
|
|
4690
|
-
return null;
|
|
4691
|
-
} finally {
|
|
4692
|
-
clearTimeout(timeout);
|
|
4693
|
-
}
|
|
4694
|
-
}
|
|
4695
|
-
async function saveTranscriptResults(db, config, results, sessionId, cwd) {
|
|
4696
|
-
let saved = 0;
|
|
4697
|
-
const items = [
|
|
4698
|
-
...results.plans.map((item) => ({ item, type: "decision" })),
|
|
4699
|
-
...results.decisions.map((item) => ({ item, type: "decision" })),
|
|
4700
|
-
...results.insights.map((item) => ({ item, type: "discovery" }))
|
|
4701
|
-
];
|
|
4702
|
-
for (const { item, type } of items) {
|
|
4703
|
-
if (!item.title || item.title.trim().length === 0)
|
|
4704
|
-
continue;
|
|
4705
|
-
const result = await saveObservation(db, config, {
|
|
4706
|
-
type,
|
|
4707
|
-
title: item.title.slice(0, 80),
|
|
4708
|
-
narrative: item.narrative,
|
|
4709
|
-
concepts: item.concepts,
|
|
4710
|
-
session_id: sessionId,
|
|
4711
|
-
cwd
|
|
4712
|
-
});
|
|
4713
|
-
if (result.success)
|
|
4714
|
-
saved++;
|
|
4715
|
-
}
|
|
4716
|
-
return saved;
|
|
4717
|
-
}
|
|
4718
|
-
|
|
4719
|
-
// src/tools/recent-chat.ts
|
|
4720
|
-
function summarizeChatSources(messages) {
|
|
4721
|
-
return messages.reduce((summary, message) => {
|
|
4722
|
-
summary[getChatCaptureOrigin(message)] += 1;
|
|
4723
|
-
return summary;
|
|
4724
|
-
}, { transcript: 0, history: 0, hook: 0 });
|
|
4725
|
-
}
|
|
4726
|
-
function getChatCoverageState(messagesOrSummary) {
|
|
4727
|
-
const summary = Array.isArray(messagesOrSummary) ? summarizeChatSources(messagesOrSummary) : messagesOrSummary;
|
|
4728
|
-
if (summary.transcript > 0)
|
|
4729
|
-
return "transcript-backed";
|
|
4730
|
-
if (summary.history > 0)
|
|
4731
|
-
return "history-backed";
|
|
4732
|
-
if (summary.hook > 0)
|
|
4733
|
-
return "hook-only";
|
|
4734
|
-
return "none";
|
|
4735
|
-
}
|
|
4736
|
-
function getChatCaptureOrigin(message) {
|
|
4737
|
-
if (message.source_kind === "transcript")
|
|
4738
|
-
return "transcript";
|
|
4739
|
-
if (typeof message.remote_source_id === "string" && message.remote_source_id.startsWith("history:")) {
|
|
4740
|
-
return "history";
|
|
4741
|
-
}
|
|
4742
|
-
return "hook";
|
|
4743
|
-
}
|
|
4744
|
-
|
|
4745
|
-
// src/tools/session-story.ts
|
|
4746
|
-
function getSessionStory(db, input) {
|
|
4747
|
-
const session = db.getSessionById(input.session_id);
|
|
4748
|
-
const summary = db.getSessionSummary(input.session_id);
|
|
4749
|
-
const prompts = db.getSessionUserPrompts(input.session_id, 50);
|
|
4750
|
-
const chatMessages = db.getSessionChatMessages(input.session_id, 50);
|
|
4751
|
-
const toolEvents = db.getSessionToolEvents(input.session_id, 100);
|
|
4752
|
-
const allObservations = db.getObservationsBySession(input.session_id);
|
|
4753
|
-
const handoffs = allObservations.filter((obs) => looksLikeHandoff(obs));
|
|
4754
|
-
const rollingHandoffDrafts = handoffs.filter((obs) => isDraftHandoff(obs));
|
|
4755
|
-
const savedHandoffs = handoffs.filter((obs) => !isDraftHandoff(obs));
|
|
4756
|
-
const observations = allObservations.filter((obs) => !looksLikeHandoff(obs));
|
|
4757
|
-
const metrics = db.getSessionMetrics(input.session_id);
|
|
4758
|
-
const projectName = session?.project_id !== null && session?.project_id !== undefined ? db.getProjectById(session.project_id)?.name ?? null : null;
|
|
4759
|
-
const latestRequest = prompts[prompts.length - 1]?.prompt?.trim() || summary?.request?.trim() || null;
|
|
4760
|
-
return {
|
|
4761
|
-
session,
|
|
4762
|
-
project_name: projectName,
|
|
4763
|
-
summary,
|
|
4764
|
-
prompts,
|
|
4765
|
-
chat_messages: chatMessages,
|
|
4766
|
-
chat_source_summary: summarizeChatSources(chatMessages),
|
|
4767
|
-
chat_coverage_state: getChatCoverageState(chatMessages),
|
|
4768
|
-
tool_events: toolEvents,
|
|
4769
|
-
observations,
|
|
4770
|
-
handoffs,
|
|
4771
|
-
saved_handoffs: savedHandoffs,
|
|
4772
|
-
rolling_handoff_drafts: rollingHandoffDrafts,
|
|
4773
|
-
metrics,
|
|
4774
|
-
capture_state: classifyCaptureState({
|
|
4775
|
-
hasSummary: Boolean(summary?.request || summary?.completed),
|
|
4776
|
-
promptCount: prompts.length,
|
|
4777
|
-
toolEventCount: toolEvents.length
|
|
4778
|
-
}),
|
|
4779
|
-
capture_gaps: buildCaptureGaps({
|
|
4780
|
-
promptCount: prompts.length,
|
|
4781
|
-
toolEventCount: toolEvents.length,
|
|
4782
|
-
toolCallsCount: metrics?.tool_calls_count ?? 0,
|
|
4783
|
-
observationCount: observations.length,
|
|
4784
|
-
hasSummary: Boolean(summary?.request || summary?.completed)
|
|
4785
|
-
}),
|
|
4786
|
-
latest_request: latestRequest,
|
|
4787
|
-
recent_outcomes: collectRecentOutcomes(observations),
|
|
4788
|
-
hot_files: collectHotFiles(observations),
|
|
4789
|
-
provenance_summary: collectProvenanceSummary(observations)
|
|
4790
|
-
};
|
|
4791
|
-
}
|
|
4792
|
-
function classifyCaptureState(input) {
|
|
4793
|
-
if (input.promptCount > 0 && input.toolEventCount > 0)
|
|
4794
|
-
return "rich";
|
|
4795
|
-
if (input.promptCount > 0 || input.toolEventCount > 0)
|
|
4796
|
-
return "partial";
|
|
4797
|
-
if (input.hasSummary)
|
|
4798
|
-
return "summary-only";
|
|
4799
|
-
return "legacy";
|
|
4800
|
-
}
|
|
4801
|
-
function buildCaptureGaps(input) {
|
|
4802
|
-
const gaps = [];
|
|
4803
|
-
if (input.promptCount === 0)
|
|
4804
|
-
gaps.push("missing prompts");
|
|
4805
|
-
if (input.toolCallsCount > 0 && input.toolEventCount === 0) {
|
|
4806
|
-
gaps.push("missing raw tool chronology");
|
|
4807
|
-
} else if (input.toolEventCount === 0) {
|
|
4808
|
-
gaps.push("no tool events");
|
|
4809
|
-
}
|
|
4810
|
-
if (input.observationCount === 0 && input.hasSummary) {
|
|
4811
|
-
gaps.push("summary without reusable observations");
|
|
4812
|
-
}
|
|
4813
|
-
return gaps;
|
|
4814
|
-
}
|
|
4815
|
-
function collectRecentOutcomes(observations) {
|
|
4816
|
-
const seen = new Set;
|
|
4817
|
-
const outcomes = [];
|
|
4818
|
-
for (const obs of observations) {
|
|
4819
|
-
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
4820
|
-
continue;
|
|
4821
|
-
const title = obs.title.trim();
|
|
4822
|
-
if (!title || looksLikeFileOperationTitle(title))
|
|
4823
|
-
continue;
|
|
4824
|
-
const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
|
|
4825
|
-
if (seen.has(normalized))
|
|
4826
|
-
continue;
|
|
4827
|
-
seen.add(normalized);
|
|
4828
|
-
outcomes.push(title);
|
|
4829
|
-
if (outcomes.length >= 6)
|
|
4830
|
-
break;
|
|
4831
|
-
}
|
|
4832
|
-
return outcomes;
|
|
4833
|
-
}
|
|
4834
|
-
function collectHotFiles(observations) {
|
|
4835
|
-
const counts = new Map;
|
|
4836
|
-
for (const obs of observations) {
|
|
4837
|
-
for (const path of [...parseJsonArray3(obs.files_modified), ...parseJsonArray3(obs.files_read)]) {
|
|
4838
|
-
counts.set(path, (counts.get(path) ?? 0) + 1);
|
|
4839
|
-
}
|
|
4840
|
-
}
|
|
4841
|
-
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);
|
|
4842
|
-
}
|
|
4843
|
-
function parseJsonArray3(value) {
|
|
4844
|
-
if (!value)
|
|
4845
|
-
return [];
|
|
4846
|
-
try {
|
|
4847
|
-
const parsed = JSON.parse(value);
|
|
4848
|
-
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
4849
|
-
} catch {
|
|
4850
|
-
return [];
|
|
4851
|
-
}
|
|
4852
|
-
}
|
|
4853
|
-
function looksLikeFileOperationTitle(value) {
|
|
4854
|
-
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
4855
|
-
}
|
|
4856
|
-
function collectProvenanceSummary(observations) {
|
|
4857
|
-
const counts = new Map;
|
|
4858
|
-
for (const obs of observations) {
|
|
4859
|
-
if (!obs.source_tool)
|
|
4860
|
-
continue;
|
|
4861
|
-
counts.set(obs.source_tool, (counts.get(obs.source_tool) ?? 0) + 1);
|
|
4862
|
-
}
|
|
4863
|
-
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);
|
|
4864
|
-
}
|
|
4865
|
-
|
|
4866
|
-
// src/tools/handoffs.ts
|
|
4867
|
-
async function upsertRollingHandoff(db, config, input) {
|
|
4868
|
-
const resolved = resolveTargetSession(db, input.cwd, config.user_id, input.session_id);
|
|
4869
|
-
if (!resolved.session) {
|
|
4870
|
-
return {
|
|
4871
|
-
success: false,
|
|
4872
|
-
reason: "No recent session found to draft a handoff yet"
|
|
4873
|
-
};
|
|
4874
|
-
}
|
|
4875
|
-
const story = getSessionStory(db, { session_id: resolved.session.session_id });
|
|
4876
|
-
if (!story.session) {
|
|
4877
|
-
return {
|
|
4878
|
-
success: false,
|
|
4879
|
-
reason: `Session ${resolved.session.session_id} not found`
|
|
4880
|
-
};
|
|
4881
|
-
}
|
|
4882
|
-
const includeChat = input.include_chat === true || input.include_chat !== false && shouldAutoIncludeChat(story);
|
|
4883
|
-
const chatLimit = Math.max(1, Math.min(input.chat_limit ?? 3, 6));
|
|
4884
|
-
const title = `Handoff Draft: ${buildHandoffTitle(story.summary, story.latest_request)}`;
|
|
4885
|
-
const narrative = buildHandoffNarrative(story.summary, story, {
|
|
4886
|
-
includeChat,
|
|
4887
|
-
chatLimit
|
|
4888
|
-
});
|
|
4889
|
-
const facts = buildHandoffFacts(story.summary, story);
|
|
4890
|
-
const concepts = buildDraftHandoffConcepts(story.project_name, story.capture_state);
|
|
4891
|
-
const existing = getSessionRollingHandoff(db, story.session.session_id);
|
|
4892
|
-
const now = Math.floor(Date.now() / 1000);
|
|
4893
|
-
if (existing) {
|
|
4894
|
-
const nextFacts = JSON.stringify(facts);
|
|
4895
|
-
const nextConcepts = JSON.stringify(concepts);
|
|
4896
|
-
const shouldRefresh = existing.title !== title || (existing.narrative ?? null) !== narrative || (existing.facts ?? null) !== nextFacts || (existing.concepts ?? null) !== nextConcepts || now - existing.created_at_epoch >= 120;
|
|
4897
|
-
if (!shouldRefresh) {
|
|
4898
|
-
return {
|
|
4899
|
-
success: true,
|
|
4900
|
-
observation_id: existing.id,
|
|
4901
|
-
session_id: story.session.session_id,
|
|
4902
|
-
title: existing.title
|
|
4903
|
-
};
|
|
4904
|
-
}
|
|
4905
|
-
const updated = db.updateObservationContent(existing.id, {
|
|
4906
|
-
title,
|
|
4907
|
-
narrative,
|
|
4908
|
-
facts: nextFacts,
|
|
4909
|
-
concepts: nextConcepts,
|
|
4910
|
-
created_at_epoch: now
|
|
4911
|
-
});
|
|
4912
|
-
if (!updated) {
|
|
4913
|
-
return {
|
|
4914
|
-
success: false,
|
|
4915
|
-
reason: "Failed to update rolling handoff draft"
|
|
4916
|
-
};
|
|
4917
|
-
}
|
|
4918
|
-
db.addToOutbox("observation", updated.id);
|
|
4919
|
-
return {
|
|
4920
|
-
success: true,
|
|
4921
|
-
observation_id: updated.id,
|
|
4922
|
-
session_id: story.session.session_id,
|
|
4923
|
-
title: updated.title
|
|
4924
|
-
};
|
|
4925
|
-
}
|
|
4926
|
-
const result = await saveObservation(db, config, {
|
|
4927
|
-
type: "message",
|
|
4928
|
-
title,
|
|
4929
|
-
narrative,
|
|
4930
|
-
facts,
|
|
4931
|
-
concepts,
|
|
4932
|
-
session_id: story.session.session_id,
|
|
4933
|
-
cwd: input.cwd,
|
|
4934
|
-
agent: "engrm-handoff",
|
|
4935
|
-
source_tool: "rolling_handoff"
|
|
4936
|
-
});
|
|
4937
|
-
return {
|
|
4938
|
-
success: result.success,
|
|
4939
|
-
observation_id: result.observation_id,
|
|
4940
|
-
session_id: story.session.session_id,
|
|
4941
|
-
title,
|
|
4942
|
-
reason: result.reason
|
|
4943
|
-
};
|
|
4944
|
-
}
|
|
4945
|
-
function getRecentHandoffs(db, input) {
|
|
4946
|
-
const limit = Math.max(1, Math.min(input.limit ?? 10, 25));
|
|
4947
|
-
const queryLimit = input.current_device_id ? Math.max(limit, Math.min(limit * 5, 50)) : limit;
|
|
4948
|
-
const projectScoped = input.project_scoped !== false;
|
|
4949
|
-
let projectId = null;
|
|
4950
|
-
let projectName;
|
|
4951
|
-
if (projectScoped) {
|
|
4952
|
-
const cwd = input.cwd ?? process.cwd();
|
|
4953
|
-
const detected = detectProject(cwd);
|
|
4954
|
-
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
4955
|
-
if (project) {
|
|
4956
|
-
projectId = project.id;
|
|
4957
|
-
projectName = project.name;
|
|
4958
|
-
}
|
|
4959
|
-
}
|
|
4960
|
-
const conditions = [
|
|
4961
|
-
"o.type = 'message'",
|
|
4962
|
-
"o.lifecycle IN ('active', 'aging', 'pinned')",
|
|
4963
|
-
"o.superseded_by IS NULL",
|
|
4964
|
-
`(o.title LIKE 'Handoff:%' OR o.concepts LIKE '%"handoff"%')`
|
|
4965
|
-
];
|
|
4966
|
-
const params = [];
|
|
4967
|
-
if (input.user_id) {
|
|
4968
|
-
conditions.push("(o.sensitivity != 'personal' OR o.user_id = ?)");
|
|
4969
|
-
params.push(input.user_id);
|
|
4970
|
-
}
|
|
4971
|
-
if (projectId !== null) {
|
|
4972
|
-
conditions.push("o.project_id = ?");
|
|
4973
|
-
params.push(projectId);
|
|
4974
|
-
}
|
|
4975
|
-
params.push(queryLimit);
|
|
4976
|
-
const handoffs = db.db.query(`SELECT o.*, p.name AS project_name
|
|
4977
|
-
FROM observations o
|
|
4978
|
-
LEFT JOIN projects p ON p.id = o.project_id
|
|
4979
|
-
WHERE ${conditions.join(" AND ")}
|
|
4980
|
-
ORDER BY o.created_at_epoch DESC, o.id DESC
|
|
4981
|
-
LIMIT ?`).all(...params);
|
|
4982
|
-
handoffs.sort((a, b) => compareHandoffs(a, b, input.current_device_id));
|
|
4983
|
-
return {
|
|
4984
|
-
handoffs: handoffs.slice(0, limit),
|
|
4985
|
-
project: projectName
|
|
4986
|
-
};
|
|
4987
|
-
}
|
|
4988
|
-
function formatHandoffSource(handoff) {
|
|
4989
|
-
const ageSeconds = Math.max(0, Math.floor(Date.now() / 1000) - handoff.created_at_epoch);
|
|
4990
|
-
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`;
|
|
4991
|
-
return `from ${handoff.device_id} · ${ageLabel}`;
|
|
4992
|
-
}
|
|
4993
|
-
function isDraftHandoff(obs) {
|
|
4994
|
-
if (obs.title.startsWith("Handoff Draft:"))
|
|
4995
|
-
return true;
|
|
4996
|
-
const concepts = parseJsonArray4(obs.concepts);
|
|
4997
|
-
return concepts.includes("draft-handoff") || concepts.includes("auto-handoff");
|
|
4998
|
-
}
|
|
4999
|
-
function getSessionRollingHandoff(db, sessionId) {
|
|
5000
|
-
return db.db.query(`SELECT o.*, p.name AS project_name
|
|
5001
|
-
FROM observations o
|
|
5002
|
-
LEFT JOIN projects p ON p.id = o.project_id
|
|
5003
|
-
WHERE o.session_id = ?
|
|
5004
|
-
AND o.type = 'message'
|
|
5005
|
-
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
5006
|
-
AND o.superseded_by IS NULL
|
|
5007
|
-
AND (o.title LIKE 'Handoff Draft:%' OR o.concepts LIKE '%"draft-handoff"%')
|
|
5008
|
-
ORDER BY o.created_at_epoch DESC, o.id DESC
|
|
5009
|
-
LIMIT 1`).get(sessionId) ?? null;
|
|
5010
|
-
}
|
|
5011
|
-
function compareHandoffs(a, b, currentDeviceId) {
|
|
5012
|
-
const aDraft = isDraftHandoff(a) ? 1 : 0;
|
|
5013
|
-
const bDraft = isDraftHandoff(b) ? 1 : 0;
|
|
5014
|
-
if (aDraft !== bDraft)
|
|
5015
|
-
return aDraft - bDraft;
|
|
5016
|
-
if (currentDeviceId) {
|
|
5017
|
-
const aOther = a.device_id !== currentDeviceId ? 1 : 0;
|
|
5018
|
-
const bOther = b.device_id !== currentDeviceId ? 1 : 0;
|
|
5019
|
-
if (aOther !== bOther)
|
|
5020
|
-
return bOther - aOther;
|
|
5021
|
-
}
|
|
5022
|
-
if (b.created_at_epoch !== a.created_at_epoch) {
|
|
5023
|
-
return b.created_at_epoch - a.created_at_epoch;
|
|
5024
|
-
}
|
|
5025
|
-
return b.id - a.id;
|
|
5026
|
-
}
|
|
5027
|
-
function resolveTargetSession(db, cwd, userId, sessionId) {
|
|
5028
|
-
if (sessionId) {
|
|
5029
|
-
const session = db.getSessionById(sessionId);
|
|
5030
|
-
if (!session)
|
|
5031
|
-
return { session: null };
|
|
5032
|
-
const projectName = session.project_id ? db.getProjectById(session.project_id)?.name : undefined;
|
|
5033
|
-
return {
|
|
5034
|
-
session: {
|
|
5035
|
-
...session,
|
|
5036
|
-
project_name: projectName ?? null,
|
|
5037
|
-
request: db.getSessionSummary(sessionId)?.request ?? null,
|
|
5038
|
-
completed: db.getSessionSummary(sessionId)?.completed ?? null,
|
|
5039
|
-
current_thread: db.getSessionSummary(sessionId)?.current_thread ?? null,
|
|
5040
|
-
capture_state: db.getSessionSummary(sessionId)?.capture_state ?? null,
|
|
5041
|
-
recent_tool_names: db.getSessionSummary(sessionId)?.recent_tool_names ?? null,
|
|
5042
|
-
hot_files: db.getSessionSummary(sessionId)?.hot_files ?? null,
|
|
5043
|
-
recent_outcomes: db.getSessionSummary(sessionId)?.recent_outcomes ?? null,
|
|
5044
|
-
prompt_count: db.getSessionUserPrompts(sessionId, 200).length,
|
|
5045
|
-
tool_event_count: db.getSessionToolEvents(sessionId, 200).length
|
|
5046
|
-
},
|
|
5047
|
-
projectName: projectName ?? undefined
|
|
5048
|
-
};
|
|
5049
|
-
}
|
|
5050
|
-
const detected = detectProject(cwd ?? process.cwd());
|
|
5051
|
-
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
5052
|
-
const sessions = db.getRecentSessions(project?.id ?? null, 10, userId);
|
|
5053
|
-
return {
|
|
5054
|
-
session: sessions[0] ?? null,
|
|
5055
|
-
projectName: project?.name
|
|
5056
|
-
};
|
|
5057
|
-
}
|
|
5058
|
-
function buildHandoffTitle(summary, latestRequest, explicit) {
|
|
5059
|
-
const chosen = explicit?.trim() || summary?.current_thread?.trim() || summary?.completed?.trim() || latestRequest?.trim() || "Current work";
|
|
5060
|
-
return compactLine2(chosen) ?? "Current work";
|
|
5061
|
-
}
|
|
5062
|
-
function buildHandoffNarrative(summary, story, options) {
|
|
5063
|
-
const sections = [];
|
|
5064
|
-
if (summary?.request || story.latest_request) {
|
|
5065
|
-
sections.push(`Request: ${summary?.request ?? story.latest_request}`);
|
|
5066
|
-
}
|
|
5067
|
-
if (summary?.current_thread) {
|
|
5068
|
-
sections.push(`Current thread: ${summary.current_thread}`);
|
|
5069
|
-
}
|
|
5070
|
-
if (summary?.investigated) {
|
|
5071
|
-
sections.push(`Investigated: ${summary.investigated}`);
|
|
5072
|
-
}
|
|
5073
|
-
if (summary?.learned) {
|
|
5074
|
-
sections.push(`Learned: ${summary.learned}`);
|
|
5075
|
-
}
|
|
5076
|
-
if (summary?.completed) {
|
|
5077
|
-
sections.push(`Completed: ${summary.completed}`);
|
|
5078
|
-
}
|
|
5079
|
-
if (summary?.next_steps) {
|
|
5080
|
-
sections.push(`Next Steps: ${summary.next_steps}`);
|
|
5081
|
-
}
|
|
5082
|
-
if (story.recent_outcomes.length > 0) {
|
|
5083
|
-
sections.push(`Recent outcomes:
|
|
5084
|
-
${story.recent_outcomes.slice(0, 5).map((item) => `- ${item}`).join(`
|
|
5085
|
-
`)}`);
|
|
5086
|
-
}
|
|
5087
|
-
if (story.hot_files.length > 0) {
|
|
5088
|
-
sections.push(`Hot files:
|
|
5089
|
-
${story.hot_files.slice(0, 5).map((file) => `- ${file.path}`).join(`
|
|
5090
|
-
`)}`);
|
|
5091
|
-
}
|
|
5092
|
-
if (story.provenance_summary.length > 0) {
|
|
5093
|
-
sections.push(`Tool trail:
|
|
5094
|
-
${story.provenance_summary.slice(0, 5).map((item) => `- ${item.tool}: ${item.count}`).join(`
|
|
5095
|
-
`)}`);
|
|
5096
|
-
}
|
|
5097
|
-
if (options.includeChat && story.chat_messages.length > 0) {
|
|
5098
|
-
const chatLines = story.chat_messages.slice(-options.chatLimit).map((msg) => `- [${msg.role}] ${compactLine2(msg.content) ?? msg.content.slice(0, 120)}`);
|
|
5099
|
-
sections.push(`Chat snippets:
|
|
5100
|
-
${chatLines.join(`
|
|
5101
|
-
`)}`);
|
|
5102
|
-
}
|
|
5103
|
-
return sections.filter(Boolean).join(`
|
|
5104
|
-
|
|
5105
|
-
`);
|
|
5106
|
-
}
|
|
5107
|
-
function shouldAutoIncludeChat(story) {
|
|
5108
|
-
if (story.chat_messages.length === 0)
|
|
5109
|
-
return false;
|
|
5110
|
-
const summary = story.summary;
|
|
5111
|
-
const thinSummary = !summary?.completed && !summary?.current_thread && story.recent_outcomes.length < 2;
|
|
5112
|
-
const thinChronology = story.capture_state !== "rich" || story.tool_events.length === 0;
|
|
5113
|
-
return thinSummary || thinChronology;
|
|
5114
|
-
}
|
|
5115
|
-
function buildHandoffFacts(summary, story) {
|
|
5116
|
-
const facts = [
|
|
5117
|
-
`session_id=${story.session?.session_id ?? "unknown"}`,
|
|
5118
|
-
`capture_state=${story.capture_state}`,
|
|
5119
|
-
story.project_name ? `project=${story.project_name}` : null,
|
|
5120
|
-
summary?.current_thread ? `current_thread=${summary.current_thread}` : null,
|
|
5121
|
-
story.hot_files[0] ? `hot_file=${story.hot_files[0].path}` : null,
|
|
5122
|
-
story.provenance_summary[0] ? `primary_tool=${story.provenance_summary[0].tool}` : null
|
|
5123
|
-
];
|
|
5124
|
-
return facts.filter((item) => Boolean(item));
|
|
5125
|
-
}
|
|
5126
|
-
function buildDraftHandoffConcepts(projectName, captureState) {
|
|
5127
|
-
return [
|
|
5128
|
-
"handoff",
|
|
5129
|
-
"draft-handoff",
|
|
5130
|
-
"auto-handoff",
|
|
5131
|
-
`capture:${captureState}`,
|
|
5132
|
-
...projectName ? [projectName] : []
|
|
5133
|
-
];
|
|
5134
|
-
}
|
|
5135
|
-
function looksLikeHandoff(obs) {
|
|
5136
|
-
if (obs.title.startsWith("Handoff:") || obs.title.startsWith("Handoff Draft:"))
|
|
5137
|
-
return true;
|
|
5138
|
-
const concepts = parseJsonArray4(obs.concepts);
|
|
5139
|
-
return concepts.includes("handoff") || concepts.includes("session-handoff") || concepts.includes("draft-handoff");
|
|
5140
|
-
}
|
|
5141
|
-
function parseJsonArray4(value) {
|
|
5142
|
-
if (!value)
|
|
5143
|
-
return [];
|
|
5144
|
-
try {
|
|
5145
|
-
const parsed = JSON.parse(value);
|
|
5146
|
-
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
5147
|
-
} catch {
|
|
5148
|
-
return [];
|
|
5149
|
-
}
|
|
5150
|
-
}
|
|
5151
|
-
function compactLine2(value) {
|
|
5152
|
-
if (value === null || value === undefined)
|
|
5153
|
-
return null;
|
|
5154
|
-
let text;
|
|
5155
|
-
if (typeof value === "string") {
|
|
5156
|
-
text = value;
|
|
5157
|
-
} else {
|
|
5158
|
-
try {
|
|
5159
|
-
text = JSON.stringify(value);
|
|
5160
|
-
} catch {
|
|
5161
|
-
text = String(value);
|
|
5162
|
-
}
|
|
5163
|
-
}
|
|
5164
|
-
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
5165
|
-
if (!trimmed)
|
|
5166
|
-
return null;
|
|
5167
|
-
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
5168
|
-
}
|
|
5169
4666
|
|
|
5170
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";
|
|
5171
4671
|
function printRetrospective(summary) {
|
|
5172
4672
|
const lines = [];
|
|
5173
4673
|
lines.push("");
|
|
@@ -5202,9 +4702,53 @@ function printRetrospective(summary) {
|
|
|
5202
4702
|
`));
|
|
5203
4703
|
}
|
|
5204
4704
|
async function main() {
|
|
5205
|
-
|
|
5206
|
-
|
|
4705
|
+
if (isWorker) {
|
|
4706
|
+
await runWorkerFromFile(process.argv[2]);
|
|
5207
4707
|
process.exit(0);
|
|
4708
|
+
}
|
|
4709
|
+
const raw = await readStdin();
|
|
4710
|
+
if (!raw.trim())
|
|
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;
|
|
5208
4752
|
if (event.stop_hook_active)
|
|
5209
4753
|
process.exit(0);
|
|
5210
4754
|
const boot = bootstrapHook("stop");
|
|
@@ -5238,12 +4782,6 @@ async function main() {
|
|
|
5238
4782
|
source_kind: "hook"
|
|
5239
4783
|
});
|
|
5240
4784
|
db.addToOutbox("chat_message", chatMessage.id);
|
|
5241
|
-
if (db.vecAvailable) {
|
|
5242
|
-
const chatEmbedding = await embedText(composeChatEmbeddingText(event.last_assistant_message));
|
|
5243
|
-
if (chatEmbedding) {
|
|
5244
|
-
db.vecChatInsert(chatMessage.id, chatEmbedding);
|
|
5245
|
-
}
|
|
5246
|
-
}
|
|
5247
4785
|
}
|
|
5248
4786
|
createAssistantCheckpoint(db, event.session_id, event.cwd, event.last_assistant_message);
|
|
5249
4787
|
} catch {}
|
|
@@ -5258,10 +4796,6 @@ async function main() {
|
|
|
5258
4796
|
if (summary) {
|
|
5259
4797
|
const row = db.upsertSessionSummary(summary);
|
|
5260
4798
|
db.addToOutbox("summary", row.id);
|
|
5261
|
-
await upsertRollingHandoff(db, config, {
|
|
5262
|
-
session_id: event.session_id,
|
|
5263
|
-
cwd: event.cwd
|
|
5264
|
-
});
|
|
5265
4799
|
let securityFindings = [];
|
|
5266
4800
|
try {
|
|
5267
4801
|
if (session?.project_id) {
|
|
@@ -5298,36 +4832,19 @@ async function main() {
|
|
|
5298
4832
|
createSessionDigest(db, event.session_id, event.cwd);
|
|
5299
4833
|
} catch {}
|
|
5300
4834
|
}
|
|
5301
|
-
|
|
5302
|
-
try {
|
|
5303
|
-
const messages = readTranscript(event.session_id, event.cwd, event.transcript_path);
|
|
5304
|
-
if (messages.length > 10) {
|
|
5305
|
-
const transcript = truncateTranscript(messages);
|
|
5306
|
-
const results = await analyzeTranscript(config, transcript, event.session_id);
|
|
5307
|
-
if (results) {
|
|
5308
|
-
const saved = await saveTranscriptResults(db, config, results, event.session_id, event.cwd);
|
|
5309
|
-
if (saved > 0) {
|
|
5310
|
-
console.error(`
|
|
5311
|
-
\uD83D\uDCA1 Engrm: Extracted ${saved} insight(s) from session transcript.`);
|
|
5312
|
-
}
|
|
5313
|
-
}
|
|
5314
|
-
}
|
|
5315
|
-
} catch {}
|
|
5316
|
-
}
|
|
5317
|
-
await pushOnce(db, config, { timeoutMs: 1500 });
|
|
4835
|
+
await withTimeout(pushOnce(db, config, { timeoutMs: 250 }), 350);
|
|
5318
4836
|
try {
|
|
5319
4837
|
if (event.session_id) {
|
|
5320
4838
|
const metrics = readSessionMetrics(event.session_id);
|
|
5321
4839
|
const beacon = buildBeacon(db, config, event.session_id, metrics);
|
|
5322
4840
|
if (beacon) {
|
|
5323
|
-
await sendBeacon(config, beacon);
|
|
4841
|
+
await withTimeout(sendBeacon(config, beacon), 150);
|
|
5324
4842
|
}
|
|
5325
4843
|
}
|
|
5326
4844
|
} catch {}
|
|
5327
4845
|
} finally {
|
|
5328
4846
|
db.close();
|
|
5329
4847
|
}
|
|
5330
|
-
process.exit(0);
|
|
5331
4848
|
}
|
|
5332
4849
|
function buildFallbackSessionSummary(db, sessionId, projectId, userId, lastAssistantMessage) {
|
|
5333
4850
|
const prompts = db.getSessionUserPrompts(sessionId, 10).filter((prompt) => isMeaningfulSummaryPrompt(prompt));
|
|
@@ -5539,6 +5056,12 @@ ${sections.join(`
|
|
|
5539
5056
|
});
|
|
5540
5057
|
db.addToOutbox("observation", digestObs.id);
|
|
5541
5058
|
}
|
|
5059
|
+
async function withTimeout(promise, timeoutMs) {
|
|
5060
|
+
return await Promise.race([
|
|
5061
|
+
promise,
|
|
5062
|
+
new Promise((resolve2) => setTimeout(() => resolve2(null), timeoutMs))
|
|
5063
|
+
]);
|
|
5064
|
+
}
|
|
5542
5065
|
function createAssistantCheckpoint(db, sessionId, cwd, message) {
|
|
5543
5066
|
const checkpoint = extractAssistantCheckpoint(message);
|
|
5544
5067
|
if (!checkpoint)
|
|
@@ -5629,14 +5152,14 @@ function detectUnsavedPlans(message) {
|
|
|
5629
5152
|
return hints;
|
|
5630
5153
|
}
|
|
5631
5154
|
function readSessionMetrics(sessionId) {
|
|
5632
|
-
const { existsSync: existsSync5, readFileSync:
|
|
5633
|
-
const { join:
|
|
5155
|
+
const { existsSync: existsSync5, readFileSync: readFileSync6, unlinkSync } = __require("node:fs");
|
|
5156
|
+
const { join: join7 } = __require("node:path");
|
|
5634
5157
|
const { homedir: homedir4 } = __require("node:os");
|
|
5635
5158
|
const result = {};
|
|
5636
5159
|
try {
|
|
5637
|
-
const obsPath =
|
|
5160
|
+
const obsPath = join7(homedir4(), ".engrm", "observer-sessions", `${sessionId}.json`);
|
|
5638
5161
|
if (existsSync5(obsPath)) {
|
|
5639
|
-
const state = JSON.parse(
|
|
5162
|
+
const state = JSON.parse(readFileSync6(obsPath, "utf-8"));
|
|
5640
5163
|
if (typeof state.recallAttempts === "number")
|
|
5641
5164
|
result.recallAttempts = state.recallAttempts;
|
|
5642
5165
|
if (typeof state.recallHits === "number")
|
|
@@ -5644,9 +5167,9 @@ function readSessionMetrics(sessionId) {
|
|
|
5644
5167
|
}
|
|
5645
5168
|
} catch {}
|
|
5646
5169
|
try {
|
|
5647
|
-
const hookPath =
|
|
5170
|
+
const hookPath = join7(homedir4(), ".engrm", "hook-session-metrics.json");
|
|
5648
5171
|
if (existsSync5(hookPath)) {
|
|
5649
|
-
const hookMetrics = JSON.parse(
|
|
5172
|
+
const hookMetrics = JSON.parse(readFileSync6(hookPath, "utf-8"));
|
|
5650
5173
|
if (typeof hookMetrics.contextObsInjected === "number")
|
|
5651
5174
|
result.contextObsInjected = hookMetrics.contextObsInjected;
|
|
5652
5175
|
if (typeof hookMetrics.contextTotalAvailable === "number")
|
|
@@ -5657,9 +5180,9 @@ function readSessionMetrics(sessionId) {
|
|
|
5657
5180
|
}
|
|
5658
5181
|
} catch {}
|
|
5659
5182
|
try {
|
|
5660
|
-
const mcpPath =
|
|
5183
|
+
const mcpPath = join7(homedir4(), ".engrm", "mcp-session-metrics.json");
|
|
5661
5184
|
if (existsSync5(mcpPath)) {
|
|
5662
|
-
const metrics = JSON.parse(
|
|
5185
|
+
const metrics = JSON.parse(readFileSync6(mcpPath, "utf-8"));
|
|
5663
5186
|
if (typeof metrics.contextObsInjected === "number" && metrics.contextObsInjected > 0) {
|
|
5664
5187
|
result.contextObsInjected = metrics.contextObsInjected;
|
|
5665
5188
|
}
|
|
@@ -1123,6 +1123,20 @@ function ensureChatMessageColumns(db) {
|
|
|
1123
1123
|
db.exec("PRAGMA user_version = 17");
|
|
1124
1124
|
}
|
|
1125
1125
|
}
|
|
1126
|
+
function ensureObservationVectorTable(db) {
|
|
1127
|
+
if (!isVecExtensionLoaded(db))
|
|
1128
|
+
return;
|
|
1129
|
+
db.exec(`
|
|
1130
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
|
|
1131
|
+
observation_id INTEGER PRIMARY KEY,
|
|
1132
|
+
embedding FLOAT[384]
|
|
1133
|
+
);
|
|
1134
|
+
`);
|
|
1135
|
+
const current = getSchemaVersion(db);
|
|
1136
|
+
if (current < 4) {
|
|
1137
|
+
db.exec("PRAGMA user_version = 4");
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1126
1140
|
function ensureChatVectorTable(db) {
|
|
1127
1141
|
if (!isVecExtensionLoaded(db))
|
|
1128
1142
|
return;
|
|
@@ -1351,6 +1365,7 @@ class MemDatabase {
|
|
|
1351
1365
|
ensureObservationTypes(this.db);
|
|
1352
1366
|
ensureSessionSummaryColumns(this.db);
|
|
1353
1367
|
ensureChatMessageColumns(this.db);
|
|
1368
|
+
ensureObservationVectorTable(this.db);
|
|
1354
1369
|
ensureChatVectorTable(this.db);
|
|
1355
1370
|
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
1356
1371
|
}
|
package/dist/server.js
CHANGED
|
@@ -14534,6 +14534,20 @@ function ensureChatMessageColumns(db) {
|
|
|
14534
14534
|
db.exec("PRAGMA user_version = 17");
|
|
14535
14535
|
}
|
|
14536
14536
|
}
|
|
14537
|
+
function ensureObservationVectorTable(db) {
|
|
14538
|
+
if (!isVecExtensionLoaded(db))
|
|
14539
|
+
return;
|
|
14540
|
+
db.exec(`
|
|
14541
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
|
|
14542
|
+
observation_id INTEGER PRIMARY KEY,
|
|
14543
|
+
embedding FLOAT[384]
|
|
14544
|
+
);
|
|
14545
|
+
`);
|
|
14546
|
+
const current = getSchemaVersion(db);
|
|
14547
|
+
if (current < 4) {
|
|
14548
|
+
db.exec("PRAGMA user_version = 4");
|
|
14549
|
+
}
|
|
14550
|
+
}
|
|
14537
14551
|
function ensureChatVectorTable(db) {
|
|
14538
14552
|
if (!isVecExtensionLoaded(db))
|
|
14539
14553
|
return;
|
|
@@ -14762,6 +14776,7 @@ class MemDatabase {
|
|
|
14762
14776
|
ensureObservationTypes(this.db);
|
|
14763
14777
|
ensureSessionSummaryColumns(this.db);
|
|
14764
14778
|
ensureChatMessageColumns(this.db);
|
|
14779
|
+
ensureObservationVectorTable(this.db);
|
|
14765
14780
|
ensureChatVectorTable(this.db);
|
|
14766
14781
|
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
14767
14782
|
}
|
|
@@ -23449,7 +23464,7 @@ function installStdioLivenessGuards() {
|
|
|
23449
23464
|
function buildServer() {
|
|
23450
23465
|
const server = new McpServer({
|
|
23451
23466
|
name: "engrm",
|
|
23452
|
-
version: "0.4.
|
|
23467
|
+
version: "0.4.48"
|
|
23453
23468
|
});
|
|
23454
23469
|
const enabledToolNames = getEnabledToolNames(config2.tool_profile);
|
|
23455
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",
|