engrm 0.4.21 → 0.4.23
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/README.md +18 -0
- package/dist/cli.js +326 -21
- package/dist/hooks/elicitation-result.js +245 -12
- package/dist/hooks/post-tool-use.js +333 -14
- package/dist/hooks/pre-compact.js +984 -14
- package/dist/hooks/sentinel.js +245 -12
- package/dist/hooks/session-start.js +1097 -90
- package/dist/hooks/stop.js +430 -75
- package/dist/hooks/user-prompt-submit.js +342 -13
- package/dist/server.js +923 -86
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -13827,7 +13827,7 @@ var MIGRATIONS = [
|
|
|
13827
13827
|
-- Sync outbox (offline-first queue)
|
|
13828
13828
|
CREATE TABLE sync_outbox (
|
|
13829
13829
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
13830
|
-
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary')),
|
|
13830
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
13831
13831
|
record_id INTEGER NOT NULL,
|
|
13832
13832
|
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
13833
13833
|
'pending', 'syncing', 'synced', 'failed'
|
|
@@ -14122,6 +14122,18 @@ var MIGRATIONS = [
|
|
|
14122
14122
|
},
|
|
14123
14123
|
{
|
|
14124
14124
|
version: 11,
|
|
14125
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
14126
|
+
sql: `
|
|
14127
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
14128
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
14129
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
14130
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
14131
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
14132
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
14133
|
+
`
|
|
14134
|
+
},
|
|
14135
|
+
{
|
|
14136
|
+
version: 12,
|
|
14125
14137
|
description: "Add synced handoff metadata to session summaries",
|
|
14126
14138
|
sql: `
|
|
14127
14139
|
ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
|
|
@@ -14131,15 +14143,79 @@ var MIGRATIONS = [
|
|
|
14131
14143
|
`
|
|
14132
14144
|
},
|
|
14133
14145
|
{
|
|
14134
|
-
version:
|
|
14135
|
-
description: "Add
|
|
14146
|
+
version: 13,
|
|
14147
|
+
description: "Add current_thread to session summaries",
|
|
14136
14148
|
sql: `
|
|
14137
|
-
ALTER TABLE
|
|
14138
|
-
|
|
14139
|
-
|
|
14140
|
-
|
|
14141
|
-
|
|
14142
|
-
|
|
14149
|
+
ALTER TABLE session_summaries ADD COLUMN current_thread TEXT;
|
|
14150
|
+
`
|
|
14151
|
+
},
|
|
14152
|
+
{
|
|
14153
|
+
version: 14,
|
|
14154
|
+
description: "Add chat_messages lane for raw conversation recall",
|
|
14155
|
+
sql: `
|
|
14156
|
+
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
14157
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
14158
|
+
session_id TEXT NOT NULL,
|
|
14159
|
+
project_id INTEGER REFERENCES projects(id),
|
|
14160
|
+
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
|
14161
|
+
content TEXT NOT NULL,
|
|
14162
|
+
user_id TEXT NOT NULL,
|
|
14163
|
+
device_id TEXT NOT NULL,
|
|
14164
|
+
agent TEXT DEFAULT 'claude-code',
|
|
14165
|
+
created_at_epoch INTEGER NOT NULL
|
|
14166
|
+
);
|
|
14167
|
+
|
|
14168
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_session
|
|
14169
|
+
ON chat_messages(session_id, created_at_epoch DESC, id DESC);
|
|
14170
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_project
|
|
14171
|
+
ON chat_messages(project_id, created_at_epoch DESC, id DESC);
|
|
14172
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_created
|
|
14173
|
+
ON chat_messages(created_at_epoch DESC, id DESC);
|
|
14174
|
+
`
|
|
14175
|
+
},
|
|
14176
|
+
{
|
|
14177
|
+
version: 15,
|
|
14178
|
+
description: "Add remote_source_id for chat message sync deduplication",
|
|
14179
|
+
sql: `
|
|
14180
|
+
ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT;
|
|
14181
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source
|
|
14182
|
+
ON chat_messages(remote_source_id)
|
|
14183
|
+
WHERE remote_source_id IS NOT NULL;
|
|
14184
|
+
`
|
|
14185
|
+
},
|
|
14186
|
+
{
|
|
14187
|
+
version: 16,
|
|
14188
|
+
description: "Allow chat_message records in sync_outbox",
|
|
14189
|
+
sql: `
|
|
14190
|
+
CREATE TABLE sync_outbox_new (
|
|
14191
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
14192
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
14193
|
+
record_id INTEGER NOT NULL,
|
|
14194
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
14195
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
14196
|
+
)),
|
|
14197
|
+
retry_count INTEGER DEFAULT 0,
|
|
14198
|
+
max_retries INTEGER DEFAULT 10,
|
|
14199
|
+
last_error TEXT,
|
|
14200
|
+
created_at_epoch INTEGER NOT NULL,
|
|
14201
|
+
synced_at_epoch INTEGER,
|
|
14202
|
+
next_retry_epoch INTEGER
|
|
14203
|
+
);
|
|
14204
|
+
|
|
14205
|
+
INSERT INTO sync_outbox_new (
|
|
14206
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
14207
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
14208
|
+
)
|
|
14209
|
+
SELECT
|
|
14210
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
14211
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
14212
|
+
FROM sync_outbox;
|
|
14213
|
+
|
|
14214
|
+
DROP TABLE sync_outbox;
|
|
14215
|
+
ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
|
|
14216
|
+
|
|
14217
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
14218
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
14143
14219
|
`
|
|
14144
14220
|
}
|
|
14145
14221
|
];
|
|
@@ -14196,6 +14272,21 @@ function inferLegacySchemaVersion(db) {
|
|
|
14196
14272
|
version2 = Math.max(version2, 10);
|
|
14197
14273
|
if (columnExists(db, "observations", "source_tool"))
|
|
14198
14274
|
version2 = Math.max(version2, 11);
|
|
14275
|
+
if (columnExists(db, "session_summaries", "capture_state") && columnExists(db, "session_summaries", "recent_tool_names") && columnExists(db, "session_summaries", "hot_files") && columnExists(db, "session_summaries", "recent_outcomes")) {
|
|
14276
|
+
version2 = Math.max(version2, 12);
|
|
14277
|
+
}
|
|
14278
|
+
if (columnExists(db, "session_summaries", "current_thread")) {
|
|
14279
|
+
version2 = Math.max(version2, 13);
|
|
14280
|
+
}
|
|
14281
|
+
if (tableExists(db, "chat_messages")) {
|
|
14282
|
+
version2 = Math.max(version2, 14);
|
|
14283
|
+
}
|
|
14284
|
+
if (columnExists(db, "chat_messages", "remote_source_id")) {
|
|
14285
|
+
version2 = Math.max(version2, 15);
|
|
14286
|
+
}
|
|
14287
|
+
if (syncOutboxSupportsChatMessages(db)) {
|
|
14288
|
+
version2 = Math.max(version2, 16);
|
|
14289
|
+
}
|
|
14199
14290
|
return version2;
|
|
14200
14291
|
}
|
|
14201
14292
|
function runMigrations(db) {
|
|
@@ -14274,6 +14365,89 @@ function ensureObservationTypes(db) {
|
|
|
14274
14365
|
}
|
|
14275
14366
|
}
|
|
14276
14367
|
}
|
|
14368
|
+
function ensureSessionSummaryColumns(db) {
|
|
14369
|
+
const required2 = [
|
|
14370
|
+
"capture_state",
|
|
14371
|
+
"recent_tool_names",
|
|
14372
|
+
"hot_files",
|
|
14373
|
+
"recent_outcomes",
|
|
14374
|
+
"current_thread"
|
|
14375
|
+
];
|
|
14376
|
+
for (const column of required2) {
|
|
14377
|
+
if (columnExists(db, "session_summaries", column))
|
|
14378
|
+
continue;
|
|
14379
|
+
db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
|
|
14380
|
+
}
|
|
14381
|
+
const current = getSchemaVersion(db);
|
|
14382
|
+
if (current < 13) {
|
|
14383
|
+
db.exec("PRAGMA user_version = 13");
|
|
14384
|
+
}
|
|
14385
|
+
}
|
|
14386
|
+
function ensureChatMessageColumns(db) {
|
|
14387
|
+
if (!tableExists(db, "chat_messages"))
|
|
14388
|
+
return;
|
|
14389
|
+
if (!columnExists(db, "chat_messages", "remote_source_id")) {
|
|
14390
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
|
|
14391
|
+
}
|
|
14392
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source ON chat_messages(remote_source_id) WHERE remote_source_id IS NOT NULL");
|
|
14393
|
+
const current = getSchemaVersion(db);
|
|
14394
|
+
if (current < 15) {
|
|
14395
|
+
db.exec("PRAGMA user_version = 15");
|
|
14396
|
+
}
|
|
14397
|
+
}
|
|
14398
|
+
function ensureSyncOutboxSupportsChatMessages(db) {
|
|
14399
|
+
if (syncOutboxSupportsChatMessages(db)) {
|
|
14400
|
+
const current = getSchemaVersion(db);
|
|
14401
|
+
if (current < 16) {
|
|
14402
|
+
db.exec("PRAGMA user_version = 16");
|
|
14403
|
+
}
|
|
14404
|
+
return;
|
|
14405
|
+
}
|
|
14406
|
+
db.exec("BEGIN TRANSACTION");
|
|
14407
|
+
try {
|
|
14408
|
+
db.exec(`
|
|
14409
|
+
CREATE TABLE sync_outbox_new (
|
|
14410
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
14411
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
14412
|
+
record_id INTEGER NOT NULL,
|
|
14413
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
14414
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
14415
|
+
)),
|
|
14416
|
+
retry_count INTEGER DEFAULT 0,
|
|
14417
|
+
max_retries INTEGER DEFAULT 10,
|
|
14418
|
+
last_error TEXT,
|
|
14419
|
+
created_at_epoch INTEGER NOT NULL,
|
|
14420
|
+
synced_at_epoch INTEGER,
|
|
14421
|
+
next_retry_epoch INTEGER
|
|
14422
|
+
);
|
|
14423
|
+
|
|
14424
|
+
INSERT INTO sync_outbox_new (
|
|
14425
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
14426
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
14427
|
+
)
|
|
14428
|
+
SELECT
|
|
14429
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
14430
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
14431
|
+
FROM sync_outbox;
|
|
14432
|
+
|
|
14433
|
+
DROP TABLE sync_outbox;
|
|
14434
|
+
ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
|
|
14435
|
+
|
|
14436
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
14437
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
14438
|
+
`);
|
|
14439
|
+
db.exec("PRAGMA user_version = 16");
|
|
14440
|
+
db.exec("COMMIT");
|
|
14441
|
+
} catch (error48) {
|
|
14442
|
+
db.exec("ROLLBACK");
|
|
14443
|
+
throw new Error(`sync_outbox repair failed: ${error48 instanceof Error ? error48.message : String(error48)}`);
|
|
14444
|
+
}
|
|
14445
|
+
}
|
|
14446
|
+
function syncOutboxSupportsChatMessages(db) {
|
|
14447
|
+
const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("sync_outbox");
|
|
14448
|
+
const sql = row?.sql ?? "";
|
|
14449
|
+
return sql.includes("'chat_message'");
|
|
14450
|
+
}
|
|
14277
14451
|
function getSchemaVersion(db) {
|
|
14278
14452
|
const result = db.query("PRAGMA user_version").get();
|
|
14279
14453
|
return result.user_version;
|
|
@@ -14432,6 +14606,9 @@ class MemDatabase {
|
|
|
14432
14606
|
this.vecAvailable = this.loadVecExtension();
|
|
14433
14607
|
runMigrations(this.db);
|
|
14434
14608
|
ensureObservationTypes(this.db);
|
|
14609
|
+
ensureSessionSummaryColumns(this.db);
|
|
14610
|
+
ensureChatMessageColumns(this.db);
|
|
14611
|
+
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
14435
14612
|
}
|
|
14436
14613
|
loadVecExtension() {
|
|
14437
14614
|
try {
|
|
@@ -14657,6 +14834,7 @@ class MemDatabase {
|
|
|
14657
14834
|
p.name AS project_name,
|
|
14658
14835
|
ss.request AS request,
|
|
14659
14836
|
ss.completed AS completed,
|
|
14837
|
+
ss.current_thread AS current_thread,
|
|
14660
14838
|
ss.capture_state AS capture_state,
|
|
14661
14839
|
ss.recent_tool_names AS recent_tool_names,
|
|
14662
14840
|
ss.hot_files AS hot_files,
|
|
@@ -14675,6 +14853,7 @@ class MemDatabase {
|
|
|
14675
14853
|
p.name AS project_name,
|
|
14676
14854
|
ss.request AS request,
|
|
14677
14855
|
ss.completed AS completed,
|
|
14856
|
+
ss.current_thread AS current_thread,
|
|
14678
14857
|
ss.capture_state AS capture_state,
|
|
14679
14858
|
ss.recent_tool_names AS recent_tool_names,
|
|
14680
14859
|
ss.hot_files AS hot_files,
|
|
@@ -14765,6 +14944,54 @@ class MemDatabase {
|
|
|
14765
14944
|
ORDER BY created_at_epoch DESC, id DESC
|
|
14766
14945
|
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
14767
14946
|
}
|
|
14947
|
+
insertChatMessage(input) {
|
|
14948
|
+
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
14949
|
+
const content = input.content.trim();
|
|
14950
|
+
const result = this.db.query(`INSERT INTO chat_messages (
|
|
14951
|
+
session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id
|
|
14952
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.session_id, input.project_id, input.role, content, input.user_id, input.device_id, input.agent ?? "claude-code", createdAt, input.remote_source_id ?? null);
|
|
14953
|
+
return this.getChatMessageById(Number(result.lastInsertRowid));
|
|
14954
|
+
}
|
|
14955
|
+
getChatMessageById(id) {
|
|
14956
|
+
return this.db.query("SELECT * FROM chat_messages WHERE id = ?").get(id) ?? null;
|
|
14957
|
+
}
|
|
14958
|
+
getChatMessageByRemoteSourceId(remoteSourceId) {
|
|
14959
|
+
return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
|
|
14960
|
+
}
|
|
14961
|
+
getSessionChatMessages(sessionId, limit = 50) {
|
|
14962
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
14963
|
+
WHERE session_id = ?
|
|
14964
|
+
ORDER BY created_at_epoch ASC, id ASC
|
|
14965
|
+
LIMIT ?`).all(sessionId, limit);
|
|
14966
|
+
}
|
|
14967
|
+
getRecentChatMessages(projectId, limit = 20, userId) {
|
|
14968
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
14969
|
+
if (projectId !== null) {
|
|
14970
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
14971
|
+
WHERE project_id = ?${visibilityClause}
|
|
14972
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
14973
|
+
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
14974
|
+
}
|
|
14975
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
14976
|
+
WHERE 1 = 1${visibilityClause}
|
|
14977
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
14978
|
+
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
14979
|
+
}
|
|
14980
|
+
searchChatMessages(query, projectId, limit = 20, userId) {
|
|
14981
|
+
const needle = `%${query.toLowerCase()}%`;
|
|
14982
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
14983
|
+
if (projectId !== null) {
|
|
14984
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
14985
|
+
WHERE project_id = ?
|
|
14986
|
+
AND lower(content) LIKE ?${visibilityClause}
|
|
14987
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
14988
|
+
LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
|
|
14989
|
+
}
|
|
14990
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
14991
|
+
WHERE lower(content) LIKE ?${visibilityClause}
|
|
14992
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
14993
|
+
LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
|
|
14994
|
+
}
|
|
14768
14995
|
addToOutbox(recordType, recordId) {
|
|
14769
14996
|
const now = Math.floor(Date.now() / 1000);
|
|
14770
14997
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|
|
@@ -14853,9 +15080,9 @@ class MemDatabase {
|
|
|
14853
15080
|
};
|
|
14854
15081
|
const result = this.db.query(`INSERT INTO session_summaries (
|
|
14855
15082
|
session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
|
|
14856
|
-
capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
15083
|
+
current_thread, capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
14857
15084
|
)
|
|
14858
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, summary.capture_state ?? null, summary.recent_tool_names ?? null, summary.hot_files ?? null, summary.recent_outcomes ?? null, now);
|
|
15085
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, summary.current_thread ?? null, summary.capture_state ?? null, summary.recent_tool_names ?? null, summary.hot_files ?? null, summary.recent_outcomes ?? null, now);
|
|
14859
15086
|
const id = Number(result.lastInsertRowid);
|
|
14860
15087
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
14861
15088
|
}
|
|
@@ -14871,6 +15098,7 @@ class MemDatabase {
|
|
|
14871
15098
|
learned: normalizeSummarySection(summary.learned ?? existing.learned),
|
|
14872
15099
|
completed: normalizeSummarySection(summary.completed ?? existing.completed),
|
|
14873
15100
|
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
|
|
15101
|
+
current_thread: summary.current_thread ?? existing.current_thread,
|
|
14874
15102
|
capture_state: summary.capture_state ?? existing.capture_state,
|
|
14875
15103
|
recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
|
|
14876
15104
|
hot_files: summary.hot_files ?? existing.hot_files,
|
|
@@ -14884,12 +15112,13 @@ class MemDatabase {
|
|
|
14884
15112
|
learned = ?,
|
|
14885
15113
|
completed = ?,
|
|
14886
15114
|
next_steps = ?,
|
|
15115
|
+
current_thread = ?,
|
|
14887
15116
|
capture_state = ?,
|
|
14888
15117
|
recent_tool_names = ?,
|
|
14889
15118
|
hot_files = ?,
|
|
14890
15119
|
recent_outcomes = ?,
|
|
14891
15120
|
created_at_epoch = ?
|
|
14892
|
-
WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, normalized.capture_state, normalized.recent_tool_names, normalized.hot_files, normalized.recent_outcomes, now, summary.session_id);
|
|
15121
|
+
WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, normalized.current_thread, normalized.capture_state, normalized.recent_tool_names, normalized.hot_files, normalized.recent_outcomes, now, summary.session_id);
|
|
14893
15122
|
return this.getSessionSummary(summary.session_id);
|
|
14894
15123
|
}
|
|
14895
15124
|
getSessionSummary(sessionId) {
|
|
@@ -16140,12 +16369,12 @@ function getRecentRequests(db, input) {
|
|
|
16140
16369
|
};
|
|
16141
16370
|
}
|
|
16142
16371
|
|
|
16143
|
-
// src/tools/recent-
|
|
16144
|
-
function
|
|
16145
|
-
const limit = Math.max(1, Math.min(input.limit ??
|
|
16372
|
+
// src/tools/recent-chat.ts
|
|
16373
|
+
function getRecentChat(db, input) {
|
|
16374
|
+
const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
|
|
16146
16375
|
if (input.session_id) {
|
|
16147
16376
|
return {
|
|
16148
|
-
|
|
16377
|
+
messages: db.getSessionChatMessages(input.session_id, limit).slice(-limit).reverse()
|
|
16149
16378
|
};
|
|
16150
16379
|
}
|
|
16151
16380
|
const projectScoped = input.project_scoped !== false;
|
|
@@ -16161,7 +16390,28 @@ function getRecentTools(db, input) {
|
|
|
16161
16390
|
}
|
|
16162
16391
|
}
|
|
16163
16392
|
return {
|
|
16164
|
-
|
|
16393
|
+
messages: db.getRecentChatMessages(projectId, limit, input.user_id),
|
|
16394
|
+
project: projectName
|
|
16395
|
+
};
|
|
16396
|
+
}
|
|
16397
|
+
|
|
16398
|
+
// src/tools/search-chat.ts
|
|
16399
|
+
function searchChat(db, input) {
|
|
16400
|
+
const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
|
|
16401
|
+
const projectScoped = input.project_scoped !== false;
|
|
16402
|
+
let projectId = null;
|
|
16403
|
+
let projectName;
|
|
16404
|
+
if (projectScoped) {
|
|
16405
|
+
const cwd = input.cwd ?? process.cwd();
|
|
16406
|
+
const detected = detectProject(cwd);
|
|
16407
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
16408
|
+
if (project) {
|
|
16409
|
+
projectId = project.id;
|
|
16410
|
+
projectName = project.name;
|
|
16411
|
+
}
|
|
16412
|
+
}
|
|
16413
|
+
return {
|
|
16414
|
+
messages: db.searchChatMessages(input.query, projectId, limit, input.user_id),
|
|
16165
16415
|
project: projectName
|
|
16166
16416
|
};
|
|
16167
16417
|
}
|
|
@@ -16171,8 +16421,11 @@ function getSessionStory(db, input) {
|
|
|
16171
16421
|
const session = db.getSessionById(input.session_id);
|
|
16172
16422
|
const summary = db.getSessionSummary(input.session_id);
|
|
16173
16423
|
const prompts = db.getSessionUserPrompts(input.session_id, 50);
|
|
16424
|
+
const chatMessages = db.getSessionChatMessages(input.session_id, 50);
|
|
16174
16425
|
const toolEvents = db.getSessionToolEvents(input.session_id, 100);
|
|
16175
|
-
const
|
|
16426
|
+
const allObservations = db.getObservationsBySession(input.session_id);
|
|
16427
|
+
const handoffs = allObservations.filter((obs) => looksLikeHandoff(obs));
|
|
16428
|
+
const observations = allObservations.filter((obs) => !looksLikeHandoff(obs));
|
|
16176
16429
|
const metrics = db.getSessionMetrics(input.session_id);
|
|
16177
16430
|
const projectName = session?.project_id !== null && session?.project_id !== undefined ? db.getProjectById(session.project_id)?.name ?? null : null;
|
|
16178
16431
|
const latestRequest = prompts[prompts.length - 1]?.prompt?.trim() || summary?.request?.trim() || null;
|
|
@@ -16181,8 +16434,10 @@ function getSessionStory(db, input) {
|
|
|
16181
16434
|
project_name: projectName,
|
|
16182
16435
|
summary,
|
|
16183
16436
|
prompts,
|
|
16437
|
+
chat_messages: chatMessages,
|
|
16184
16438
|
tool_events: toolEvents,
|
|
16185
16439
|
observations,
|
|
16440
|
+
handoffs,
|
|
16186
16441
|
metrics,
|
|
16187
16442
|
capture_state: classifyCaptureState({
|
|
16188
16443
|
hasSummary: Boolean(summary?.request || summary?.completed),
|
|
@@ -16276,6 +16531,269 @@ function collectProvenanceSummary(observations) {
|
|
|
16276
16531
|
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);
|
|
16277
16532
|
}
|
|
16278
16533
|
|
|
16534
|
+
// src/tools/handoffs.ts
|
|
16535
|
+
async function createHandoff(db, config2, input) {
|
|
16536
|
+
const resolved = resolveTargetSession(db, input.cwd, config2.user_id, input.session_id);
|
|
16537
|
+
if (!resolved.session) {
|
|
16538
|
+
return {
|
|
16539
|
+
success: false,
|
|
16540
|
+
reason: "No recent session found to hand off yet"
|
|
16541
|
+
};
|
|
16542
|
+
}
|
|
16543
|
+
const story = getSessionStory(db, { session_id: resolved.session.session_id });
|
|
16544
|
+
if (!story.session) {
|
|
16545
|
+
return {
|
|
16546
|
+
success: false,
|
|
16547
|
+
reason: `Session ${resolved.session.session_id} not found`
|
|
16548
|
+
};
|
|
16549
|
+
}
|
|
16550
|
+
const includeChat = input.include_chat === true;
|
|
16551
|
+
const chatLimit = Math.max(1, Math.min(input.chat_limit ?? 4, 8));
|
|
16552
|
+
const generatedTitle = buildHandoffTitle(story.summary, story.latest_request, input.title);
|
|
16553
|
+
const title = `Handoff: ${generatedTitle} · ${formatTimestamp(Date.now())}`;
|
|
16554
|
+
const narrative = buildHandoffNarrative(story.summary, story, {
|
|
16555
|
+
includeChat,
|
|
16556
|
+
chatLimit
|
|
16557
|
+
});
|
|
16558
|
+
const facts = buildHandoffFacts(story.summary, story);
|
|
16559
|
+
const concepts = buildHandoffConcepts(story.project_name, story.capture_state);
|
|
16560
|
+
const result = await saveObservation(db, config2, {
|
|
16561
|
+
type: "message",
|
|
16562
|
+
title,
|
|
16563
|
+
narrative,
|
|
16564
|
+
facts,
|
|
16565
|
+
concepts,
|
|
16566
|
+
session_id: story.session.session_id,
|
|
16567
|
+
cwd: input.cwd,
|
|
16568
|
+
agent: "engrm-handoff",
|
|
16569
|
+
source_tool: "create_handoff"
|
|
16570
|
+
});
|
|
16571
|
+
return {
|
|
16572
|
+
success: result.success,
|
|
16573
|
+
observation_id: result.observation_id,
|
|
16574
|
+
session_id: story.session.session_id,
|
|
16575
|
+
title,
|
|
16576
|
+
reason: result.reason
|
|
16577
|
+
};
|
|
16578
|
+
}
|
|
16579
|
+
function getRecentHandoffs(db, input) {
|
|
16580
|
+
const limit = Math.max(1, Math.min(input.limit ?? 10, 25));
|
|
16581
|
+
const projectScoped = input.project_scoped !== false;
|
|
16582
|
+
let projectId = null;
|
|
16583
|
+
let projectName;
|
|
16584
|
+
if (projectScoped) {
|
|
16585
|
+
const cwd = input.cwd ?? process.cwd();
|
|
16586
|
+
const detected = detectProject(cwd);
|
|
16587
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
16588
|
+
if (project) {
|
|
16589
|
+
projectId = project.id;
|
|
16590
|
+
projectName = project.name;
|
|
16591
|
+
}
|
|
16592
|
+
}
|
|
16593
|
+
const conditions = [
|
|
16594
|
+
"o.type = 'message'",
|
|
16595
|
+
"o.lifecycle IN ('active', 'aging', 'pinned')",
|
|
16596
|
+
"o.superseded_by IS NULL",
|
|
16597
|
+
`(o.title LIKE 'Handoff:%' OR o.concepts LIKE '%"handoff"%')`
|
|
16598
|
+
];
|
|
16599
|
+
const params = [];
|
|
16600
|
+
if (input.user_id) {
|
|
16601
|
+
conditions.push("(o.sensitivity != 'personal' OR o.user_id = ?)");
|
|
16602
|
+
params.push(input.user_id);
|
|
16603
|
+
}
|
|
16604
|
+
if (projectId !== null) {
|
|
16605
|
+
conditions.push("o.project_id = ?");
|
|
16606
|
+
params.push(projectId);
|
|
16607
|
+
}
|
|
16608
|
+
params.push(limit);
|
|
16609
|
+
const handoffs = db.db.query(`SELECT o.*, p.name AS project_name
|
|
16610
|
+
FROM observations o
|
|
16611
|
+
LEFT JOIN projects p ON p.id = o.project_id
|
|
16612
|
+
WHERE ${conditions.join(" AND ")}
|
|
16613
|
+
ORDER BY o.created_at_epoch DESC, o.id DESC
|
|
16614
|
+
LIMIT ?`).all(...params);
|
|
16615
|
+
return {
|
|
16616
|
+
handoffs,
|
|
16617
|
+
project: projectName
|
|
16618
|
+
};
|
|
16619
|
+
}
|
|
16620
|
+
function loadHandoff(db, input) {
|
|
16621
|
+
if (typeof input.id === "number") {
|
|
16622
|
+
const obs = db.getObservationById(input.id);
|
|
16623
|
+
if (!obs || obs.type !== "message" || !looksLikeHandoff(obs)) {
|
|
16624
|
+
return { handoff: null };
|
|
16625
|
+
}
|
|
16626
|
+
const projectName = obs.project_id ? db.getProjectById(obs.project_id)?.name ?? null : null;
|
|
16627
|
+
return { handoff: { ...obs, project_name: projectName } };
|
|
16628
|
+
}
|
|
16629
|
+
const recent = getRecentHandoffs(db, {
|
|
16630
|
+
limit: 1,
|
|
16631
|
+
project_scoped: input.project_scoped,
|
|
16632
|
+
cwd: input.cwd,
|
|
16633
|
+
user_id: input.user_id
|
|
16634
|
+
});
|
|
16635
|
+
return {
|
|
16636
|
+
handoff: recent.handoffs[0] ?? null,
|
|
16637
|
+
project: recent.project
|
|
16638
|
+
};
|
|
16639
|
+
}
|
|
16640
|
+
function resolveTargetSession(db, cwd, userId, sessionId) {
|
|
16641
|
+
if (sessionId) {
|
|
16642
|
+
const session = db.getSessionById(sessionId);
|
|
16643
|
+
if (!session)
|
|
16644
|
+
return { session: null };
|
|
16645
|
+
const projectName = session.project_id ? db.getProjectById(session.project_id)?.name : undefined;
|
|
16646
|
+
return {
|
|
16647
|
+
session: {
|
|
16648
|
+
...session,
|
|
16649
|
+
project_name: projectName ?? null,
|
|
16650
|
+
request: db.getSessionSummary(sessionId)?.request ?? null,
|
|
16651
|
+
completed: db.getSessionSummary(sessionId)?.completed ?? null,
|
|
16652
|
+
current_thread: db.getSessionSummary(sessionId)?.current_thread ?? null,
|
|
16653
|
+
capture_state: db.getSessionSummary(sessionId)?.capture_state ?? null,
|
|
16654
|
+
recent_tool_names: db.getSessionSummary(sessionId)?.recent_tool_names ?? null,
|
|
16655
|
+
hot_files: db.getSessionSummary(sessionId)?.hot_files ?? null,
|
|
16656
|
+
recent_outcomes: db.getSessionSummary(sessionId)?.recent_outcomes ?? null,
|
|
16657
|
+
prompt_count: db.getSessionUserPrompts(sessionId, 200).length,
|
|
16658
|
+
tool_event_count: db.getSessionToolEvents(sessionId, 200).length
|
|
16659
|
+
},
|
|
16660
|
+
projectName: projectName ?? undefined
|
|
16661
|
+
};
|
|
16662
|
+
}
|
|
16663
|
+
const detected = detectProject(cwd ?? process.cwd());
|
|
16664
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
16665
|
+
const sessions = db.getRecentSessions(project?.id ?? null, 10, userId);
|
|
16666
|
+
return {
|
|
16667
|
+
session: sessions[0] ?? null,
|
|
16668
|
+
projectName: project?.name
|
|
16669
|
+
};
|
|
16670
|
+
}
|
|
16671
|
+
function buildHandoffTitle(summary, latestRequest, explicit) {
|
|
16672
|
+
const chosen = explicit?.trim() || summary?.current_thread?.trim() || summary?.completed?.trim() || latestRequest?.trim() || "Current work";
|
|
16673
|
+
return compactLine(chosen) ?? "Current work";
|
|
16674
|
+
}
|
|
16675
|
+
function buildHandoffNarrative(summary, story, options) {
|
|
16676
|
+
const sections = [];
|
|
16677
|
+
if (summary?.request || story.latest_request) {
|
|
16678
|
+
sections.push(`Request: ${summary?.request ?? story.latest_request}`);
|
|
16679
|
+
}
|
|
16680
|
+
if (summary?.current_thread) {
|
|
16681
|
+
sections.push(`Current thread: ${summary.current_thread}`);
|
|
16682
|
+
}
|
|
16683
|
+
if (summary?.investigated) {
|
|
16684
|
+
sections.push(`Investigated: ${summary.investigated}`);
|
|
16685
|
+
}
|
|
16686
|
+
if (summary?.learned) {
|
|
16687
|
+
sections.push(`Learned: ${summary.learned}`);
|
|
16688
|
+
}
|
|
16689
|
+
if (summary?.completed) {
|
|
16690
|
+
sections.push(`Completed: ${summary.completed}`);
|
|
16691
|
+
}
|
|
16692
|
+
if (summary?.next_steps) {
|
|
16693
|
+
sections.push(`Next Steps: ${summary.next_steps}`);
|
|
16694
|
+
}
|
|
16695
|
+
if (story.recent_outcomes.length > 0) {
|
|
16696
|
+
sections.push(`Recent outcomes:
|
|
16697
|
+
${story.recent_outcomes.slice(0, 5).map((item) => `- ${item}`).join(`
|
|
16698
|
+
`)}`);
|
|
16699
|
+
}
|
|
16700
|
+
if (story.hot_files.length > 0) {
|
|
16701
|
+
sections.push(`Hot files:
|
|
16702
|
+
${story.hot_files.slice(0, 5).map((file2) => `- ${file2.path}`).join(`
|
|
16703
|
+
`)}`);
|
|
16704
|
+
}
|
|
16705
|
+
if (story.provenance_summary.length > 0) {
|
|
16706
|
+
sections.push(`Tool trail:
|
|
16707
|
+
${story.provenance_summary.slice(0, 5).map((item) => `- ${item.tool}: ${item.count}`).join(`
|
|
16708
|
+
`)}`);
|
|
16709
|
+
}
|
|
16710
|
+
if (options.includeChat && story.chat_messages.length > 0) {
|
|
16711
|
+
const chatLines = story.chat_messages.slice(-options.chatLimit).map((msg) => `- [${msg.role}] ${compactLine(msg.content) ?? msg.content.slice(0, 120)}`);
|
|
16712
|
+
sections.push(`Chat snippets:
|
|
16713
|
+
${chatLines.join(`
|
|
16714
|
+
`)}`);
|
|
16715
|
+
}
|
|
16716
|
+
return sections.filter(Boolean).join(`
|
|
16717
|
+
|
|
16718
|
+
`);
|
|
16719
|
+
}
|
|
16720
|
+
function buildHandoffFacts(summary, story) {
|
|
16721
|
+
const facts = [
|
|
16722
|
+
`session_id=${story.session?.session_id ?? "unknown"}`,
|
|
16723
|
+
`capture_state=${story.capture_state}`,
|
|
16724
|
+
story.project_name ? `project=${story.project_name}` : null,
|
|
16725
|
+
summary?.current_thread ? `current_thread=${summary.current_thread}` : null,
|
|
16726
|
+
story.hot_files[0] ? `hot_file=${story.hot_files[0].path}` : null,
|
|
16727
|
+
story.provenance_summary[0] ? `primary_tool=${story.provenance_summary[0].tool}` : null
|
|
16728
|
+
];
|
|
16729
|
+
return facts.filter((item) => Boolean(item));
|
|
16730
|
+
}
|
|
16731
|
+
function buildHandoffConcepts(projectName, captureState) {
|
|
16732
|
+
return [
|
|
16733
|
+
"handoff",
|
|
16734
|
+
"session-handoff",
|
|
16735
|
+
`capture:${captureState}`,
|
|
16736
|
+
...projectName ? [projectName] : []
|
|
16737
|
+
];
|
|
16738
|
+
}
|
|
16739
|
+
function looksLikeHandoff(obs) {
|
|
16740
|
+
if (obs.title.startsWith("Handoff:"))
|
|
16741
|
+
return true;
|
|
16742
|
+
const concepts = parseJsonArray2(obs.concepts);
|
|
16743
|
+
return concepts.includes("handoff") || concepts.includes("session-handoff");
|
|
16744
|
+
}
|
|
16745
|
+
function parseJsonArray2(value) {
|
|
16746
|
+
if (!value)
|
|
16747
|
+
return [];
|
|
16748
|
+
try {
|
|
16749
|
+
const parsed = JSON.parse(value);
|
|
16750
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
16751
|
+
} catch {
|
|
16752
|
+
return [];
|
|
16753
|
+
}
|
|
16754
|
+
}
|
|
16755
|
+
function formatTimestamp(nowMs) {
|
|
16756
|
+
const d = new Date(nowMs);
|
|
16757
|
+
const yyyy = d.getUTCFullYear();
|
|
16758
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
16759
|
+
const dd = String(d.getUTCDate()).padStart(2, "0");
|
|
16760
|
+
const hh = String(d.getUTCHours()).padStart(2, "0");
|
|
16761
|
+
const mi = String(d.getUTCMinutes()).padStart(2, "0");
|
|
16762
|
+
return `${yyyy}-${mm}-${dd} ${hh}:${mi}Z`;
|
|
16763
|
+
}
|
|
16764
|
+
function compactLine(value) {
|
|
16765
|
+
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
16766
|
+
if (!trimmed)
|
|
16767
|
+
return null;
|
|
16768
|
+
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
16769
|
+
}
|
|
16770
|
+
|
|
16771
|
+
// src/tools/recent-tools.ts
|
|
16772
|
+
function getRecentTools(db, input) {
|
|
16773
|
+
const limit = Math.max(1, Math.min(input.limit ?? 10, 50));
|
|
16774
|
+
if (input.session_id) {
|
|
16775
|
+
return {
|
|
16776
|
+
tool_events: db.getSessionToolEvents(input.session_id, limit).slice(-limit).reverse()
|
|
16777
|
+
};
|
|
16778
|
+
}
|
|
16779
|
+
const projectScoped = input.project_scoped !== false;
|
|
16780
|
+
let projectId = null;
|
|
16781
|
+
let projectName;
|
|
16782
|
+
if (projectScoped) {
|
|
16783
|
+
const cwd = input.cwd ?? process.cwd();
|
|
16784
|
+
const detected = detectProject(cwd);
|
|
16785
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
16786
|
+
if (project) {
|
|
16787
|
+
projectId = project.id;
|
|
16788
|
+
projectName = project.name;
|
|
16789
|
+
}
|
|
16790
|
+
}
|
|
16791
|
+
return {
|
|
16792
|
+
tool_events: db.getRecentToolEvents(projectId, limit, input.user_id),
|
|
16793
|
+
project: projectName
|
|
16794
|
+
};
|
|
16795
|
+
}
|
|
16796
|
+
|
|
16279
16797
|
// src/tools/recent-sessions.ts
|
|
16280
16798
|
function getRecentSessions(db, input) {
|
|
16281
16799
|
const limit = Math.max(1, Math.min(input.limit ?? 10, 50));
|
|
@@ -16631,6 +17149,12 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
16631
17149
|
const recentSessions2 = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
16632
17150
|
const projectTypeCounts2 = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
16633
17151
|
const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId, recentSessions2);
|
|
17152
|
+
const recentHandoffs2 = getRecentHandoffs(db, {
|
|
17153
|
+
cwd,
|
|
17154
|
+
project_scoped: !isNewProject,
|
|
17155
|
+
user_id: opts.userId,
|
|
17156
|
+
limit: 3
|
|
17157
|
+
}).handoffs;
|
|
16634
17158
|
return {
|
|
16635
17159
|
project_name: projectName,
|
|
16636
17160
|
canonical_id: canonicalId,
|
|
@@ -16641,7 +17165,8 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
16641
17165
|
recentToolEvents: recentToolEvents2.length > 0 ? recentToolEvents2 : undefined,
|
|
16642
17166
|
recentSessions: recentSessions2.length > 0 ? recentSessions2 : undefined,
|
|
16643
17167
|
projectTypeCounts: projectTypeCounts2,
|
|
16644
|
-
recentOutcomes: recentOutcomes2
|
|
17168
|
+
recentOutcomes: recentOutcomes2,
|
|
17169
|
+
recentHandoffs: recentHandoffs2.length > 0 ? recentHandoffs2 : undefined
|
|
16645
17170
|
};
|
|
16646
17171
|
}
|
|
16647
17172
|
let remainingBudget = tokenBudget - 30;
|
|
@@ -16669,6 +17194,12 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
16669
17194
|
const recentSessions = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
16670
17195
|
const projectTypeCounts = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
16671
17196
|
const recentOutcomes = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId, recentSessions);
|
|
17197
|
+
const recentHandoffs = getRecentHandoffs(db, {
|
|
17198
|
+
cwd,
|
|
17199
|
+
project_scoped: !isNewProject,
|
|
17200
|
+
user_id: opts.userId,
|
|
17201
|
+
limit: 3
|
|
17202
|
+
}).handoffs;
|
|
16672
17203
|
let securityFindings = [];
|
|
16673
17204
|
if (!isNewProject) {
|
|
16674
17205
|
try {
|
|
@@ -16727,7 +17258,8 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
16727
17258
|
recentToolEvents: recentToolEvents.length > 0 ? recentToolEvents : undefined,
|
|
16728
17259
|
recentSessions: recentSessions.length > 0 ? recentSessions : undefined,
|
|
16729
17260
|
projectTypeCounts,
|
|
16730
|
-
recentOutcomes
|
|
17261
|
+
recentOutcomes,
|
|
17262
|
+
recentHandoffs: recentHandoffs.length > 0 ? recentHandoffs : undefined
|
|
16731
17263
|
};
|
|
16732
17264
|
}
|
|
16733
17265
|
function estimateObservationTokens(obs, index) {
|
|
@@ -17220,6 +17752,9 @@ function buildSuggestedTools(sessions, requestCount, toolCount, observationCount
|
|
|
17220
17752
|
if (observationCount > 0) {
|
|
17221
17753
|
suggested.push("tool_memory_index", "capture_git_worktree");
|
|
17222
17754
|
}
|
|
17755
|
+
if (sessions.length > 0) {
|
|
17756
|
+
suggested.push("create_handoff", "recent_handoffs");
|
|
17757
|
+
}
|
|
17223
17758
|
return Array.from(new Set(suggested)).slice(0, 4);
|
|
17224
17759
|
}
|
|
17225
17760
|
|
|
@@ -17283,6 +17818,8 @@ function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, obse
|
|
|
17283
17818
|
suggested.push("activity_feed");
|
|
17284
17819
|
if (observationCount > 0)
|
|
17285
17820
|
suggested.push("tool_memory_index", "capture_git_worktree");
|
|
17821
|
+
if (sessionCount > 0)
|
|
17822
|
+
suggested.push("create_handoff", "recent_handoffs");
|
|
17286
17823
|
return Array.from(new Set(suggested)).slice(0, 4);
|
|
17287
17824
|
}
|
|
17288
17825
|
|
|
@@ -17499,6 +18036,17 @@ function toToolEvent(tool) {
|
|
|
17499
18036
|
};
|
|
17500
18037
|
}
|
|
17501
18038
|
function toObservationEvent(obs) {
|
|
18039
|
+
if (looksLikeHandoff(obs)) {
|
|
18040
|
+
return {
|
|
18041
|
+
kind: "handoff",
|
|
18042
|
+
created_at_epoch: obs.created_at_epoch,
|
|
18043
|
+
session_id: obs.session_id,
|
|
18044
|
+
id: obs.id,
|
|
18045
|
+
title: obs.title,
|
|
18046
|
+
detail: obs.narrative?.replace(/\s+/g, " ").trim().slice(0, 220),
|
|
18047
|
+
observation_type: obs.type
|
|
18048
|
+
};
|
|
18049
|
+
}
|
|
17502
18050
|
const detailBits = [];
|
|
17503
18051
|
if (obs.source_tool)
|
|
17504
18052
|
detailBits.push(`via ${obs.source_tool}`);
|
|
@@ -17542,9 +18090,10 @@ function compareEvents(a, b) {
|
|
|
17542
18090
|
}
|
|
17543
18091
|
const kindOrder = {
|
|
17544
18092
|
summary: 0,
|
|
17545
|
-
|
|
17546
|
-
|
|
17547
|
-
|
|
18093
|
+
handoff: 1,
|
|
18094
|
+
observation: 2,
|
|
18095
|
+
tool: 3,
|
|
18096
|
+
prompt: 4
|
|
17548
18097
|
};
|
|
17549
18098
|
if (kindOrder[a.kind] !== kindOrder[b.kind]) {
|
|
17550
18099
|
return kindOrder[a.kind] - kindOrder[b.kind];
|
|
@@ -17565,6 +18114,7 @@ function getActivityFeed(db, input) {
|
|
|
17565
18114
|
})].filter((event) => event !== null) : [],
|
|
17566
18115
|
...story.prompts.map(toPromptEvent),
|
|
17567
18116
|
...story.tool_events.map(toToolEvent),
|
|
18117
|
+
...story.handoffs.map(toObservationEvent),
|
|
17568
18118
|
...story.observations.map(toObservationEvent)
|
|
17569
18119
|
].sort(compareEvents).slice(0, limit);
|
|
17570
18120
|
return { events: events2, project };
|
|
@@ -18014,13 +18564,13 @@ function getSessionContext(db, input) {
|
|
|
18014
18564
|
function buildHotFiles(context) {
|
|
18015
18565
|
const counts = new Map;
|
|
18016
18566
|
for (const obs of context.observations) {
|
|
18017
|
-
for (const path of [...
|
|
18567
|
+
for (const path of [...parseJsonArray3(obs.files_read), ...parseJsonArray3(obs.files_modified)]) {
|
|
18018
18568
|
counts.set(path, (counts.get(path) ?? 0) + 1);
|
|
18019
18569
|
}
|
|
18020
18570
|
}
|
|
18021
18571
|
return Array.from(counts.entries()).map(([path, count]) => ({ path, count })).sort((a, b) => b.count - a.count || a.path.localeCompare(b.path)).slice(0, 6);
|
|
18022
18572
|
}
|
|
18023
|
-
function
|
|
18573
|
+
function parseJsonArray3(value) {
|
|
18024
18574
|
if (!value)
|
|
18025
18575
|
return [];
|
|
18026
18576
|
try {
|
|
@@ -18041,6 +18591,9 @@ function buildSuggestedTools2(context) {
|
|
|
18041
18591
|
if (context.observations.length > 0) {
|
|
18042
18592
|
tools.push("tool_memory_index", "capture_git_worktree");
|
|
18043
18593
|
}
|
|
18594
|
+
if ((context.recentSessions?.length ?? 0) > 0) {
|
|
18595
|
+
tools.push("create_handoff", "recent_handoffs");
|
|
18596
|
+
}
|
|
18044
18597
|
return Array.from(new Set(tools)).slice(0, 4);
|
|
18045
18598
|
}
|
|
18046
18599
|
|
|
@@ -18591,22 +19144,27 @@ function buildSourceId(config2, localId, type = "obs") {
|
|
|
18591
19144
|
return `${config2.user_id}-${config2.device_id}-${type}-${localId}`;
|
|
18592
19145
|
}
|
|
18593
19146
|
function parseSourceId(sourceId) {
|
|
18594
|
-
const
|
|
18595
|
-
|
|
18596
|
-
|
|
18597
|
-
|
|
18598
|
-
|
|
18599
|
-
|
|
18600
|
-
|
|
18601
|
-
|
|
18602
|
-
|
|
18603
|
-
|
|
18604
|
-
|
|
18605
|
-
|
|
18606
|
-
|
|
18607
|
-
|
|
18608
|
-
|
|
18609
|
-
|
|
19147
|
+
for (const type of ["obs", "summary", "chat"]) {
|
|
19148
|
+
const marker = `-${type}-`;
|
|
19149
|
+
const idx = sourceId.lastIndexOf(marker);
|
|
19150
|
+
if (idx === -1)
|
|
19151
|
+
continue;
|
|
19152
|
+
const prefix = sourceId.slice(0, idx);
|
|
19153
|
+
const localIdStr = sourceId.slice(idx + marker.length);
|
|
19154
|
+
const localId = parseInt(localIdStr, 10);
|
|
19155
|
+
if (isNaN(localId))
|
|
19156
|
+
return null;
|
|
19157
|
+
const firstDash = prefix.indexOf("-");
|
|
19158
|
+
if (firstDash === -1)
|
|
19159
|
+
return null;
|
|
19160
|
+
return {
|
|
19161
|
+
userId: prefix.slice(0, firstDash),
|
|
19162
|
+
deviceId: prefix.slice(firstDash + 1),
|
|
19163
|
+
localId,
|
|
19164
|
+
type
|
|
19165
|
+
};
|
|
19166
|
+
}
|
|
19167
|
+
return null;
|
|
18610
19168
|
}
|
|
18611
19169
|
|
|
18612
19170
|
// src/sync/client.ts
|
|
@@ -18712,7 +19270,106 @@ class VectorApiError extends Error {
|
|
|
18712
19270
|
}
|
|
18713
19271
|
}
|
|
18714
19272
|
|
|
19273
|
+
// src/capture/session-handoff.ts
|
|
19274
|
+
function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
|
|
19275
|
+
const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
|
|
19276
|
+
const recentRequestPrompts = prompts.slice(-3).map((prompt) => prompt.prompt.trim()).filter(Boolean);
|
|
19277
|
+
const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
|
|
19278
|
+
const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
|
|
19279
|
+
const hotFiles = [...new Set(observations.flatMap((obs) => [
|
|
19280
|
+
...parseJsonArray4(obs.files_modified),
|
|
19281
|
+
...parseJsonArray4(obs.files_read)
|
|
19282
|
+
]).filter(Boolean))].slice(0, 6);
|
|
19283
|
+
const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0).slice(0, 6);
|
|
19284
|
+
const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
|
|
19285
|
+
const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
|
|
19286
|
+
if (!obs.source_tool)
|
|
19287
|
+
return acc;
|
|
19288
|
+
acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
|
|
19289
|
+
return acc;
|
|
19290
|
+
}, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
19291
|
+
const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
|
|
19292
|
+
const currentThread = buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolNames);
|
|
19293
|
+
return {
|
|
19294
|
+
prompt_count: prompts.length,
|
|
19295
|
+
tool_event_count: toolEvents.length,
|
|
19296
|
+
recent_request_prompts: recentRequestPrompts,
|
|
19297
|
+
latest_request: latestRequest,
|
|
19298
|
+
current_thread: currentThread,
|
|
19299
|
+
recent_tool_names: recentToolNames,
|
|
19300
|
+
recent_tool_commands: recentToolCommands,
|
|
19301
|
+
capture_state: captureState,
|
|
19302
|
+
hot_files: hotFiles,
|
|
19303
|
+
recent_outcomes: recentOutcomes,
|
|
19304
|
+
observation_source_tools: observationSourceTools,
|
|
19305
|
+
latest_observation_prompt_number: latestObservationPromptNumber
|
|
19306
|
+
};
|
|
19307
|
+
}
|
|
19308
|
+
function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolNames) {
|
|
19309
|
+
const request = compactLine2(latestRequest);
|
|
19310
|
+
const outcome = recentOutcomes.map((item) => compactLine2(item)).find(Boolean);
|
|
19311
|
+
const file2 = hotFiles[0] ? compactFileHint(hotFiles[0]) : null;
|
|
19312
|
+
const tools = recentToolNames.slice(0, 2).join("/");
|
|
19313
|
+
if (outcome && file2) {
|
|
19314
|
+
return `${outcome} · ${file2}${tools ? ` · ${tools}` : ""}`;
|
|
19315
|
+
}
|
|
19316
|
+
if (request && file2) {
|
|
19317
|
+
return `${request} · ${file2}${tools ? ` · ${tools}` : ""}`;
|
|
19318
|
+
}
|
|
19319
|
+
if (outcome) {
|
|
19320
|
+
return `${outcome}${tools ? ` · ${tools}` : ""}`;
|
|
19321
|
+
}
|
|
19322
|
+
if (request) {
|
|
19323
|
+
return `${request}${tools ? ` · ${tools}` : ""}`;
|
|
19324
|
+
}
|
|
19325
|
+
return null;
|
|
19326
|
+
}
|
|
19327
|
+
function compactLine2(value) {
|
|
19328
|
+
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
19329
|
+
if (!trimmed)
|
|
19330
|
+
return null;
|
|
19331
|
+
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
19332
|
+
}
|
|
19333
|
+
function compactFileHint(value) {
|
|
19334
|
+
const parts = value.split("/");
|
|
19335
|
+
if (parts.length <= 2)
|
|
19336
|
+
return value;
|
|
19337
|
+
return parts.slice(-2).join("/");
|
|
19338
|
+
}
|
|
19339
|
+
function parseJsonArray4(value) {
|
|
19340
|
+
if (!value)
|
|
19341
|
+
return [];
|
|
19342
|
+
try {
|
|
19343
|
+
const parsed = JSON.parse(value);
|
|
19344
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
19345
|
+
} catch {
|
|
19346
|
+
return [];
|
|
19347
|
+
}
|
|
19348
|
+
}
|
|
19349
|
+
|
|
18715
19350
|
// src/sync/push.ts
|
|
19351
|
+
function buildChatVectorDocument(chat, config2, project) {
|
|
19352
|
+
return {
|
|
19353
|
+
site_id: config2.site_id,
|
|
19354
|
+
namespace: config2.namespace,
|
|
19355
|
+
source_type: "chat",
|
|
19356
|
+
source_id: buildSourceId(config2, chat.id, "chat"),
|
|
19357
|
+
content: chat.content,
|
|
19358
|
+
metadata: {
|
|
19359
|
+
project_canonical: project.canonical_id,
|
|
19360
|
+
project_name: project.name,
|
|
19361
|
+
user_id: chat.user_id,
|
|
19362
|
+
device_id: chat.device_id,
|
|
19363
|
+
device_name: __require("node:os").hostname(),
|
|
19364
|
+
agent: chat.agent,
|
|
19365
|
+
type: "chat",
|
|
19366
|
+
role: chat.role,
|
|
19367
|
+
session_id: chat.session_id,
|
|
19368
|
+
created_at_epoch: chat.created_at_epoch,
|
|
19369
|
+
local_id: chat.id
|
|
19370
|
+
}
|
|
19371
|
+
};
|
|
19372
|
+
}
|
|
18716
19373
|
function buildVectorDocument(obs, config2, project) {
|
|
18717
19374
|
const parts = [obs.title];
|
|
18718
19375
|
if (obs.narrative)
|
|
@@ -18793,6 +19450,7 @@ function buildSummaryVectorDocument(summary, config2, project, observations = []
|
|
|
18793
19450
|
learned: summary.learned,
|
|
18794
19451
|
completed: summary.completed,
|
|
18795
19452
|
next_steps: summary.next_steps,
|
|
19453
|
+
current_thread: summary.current_thread,
|
|
18796
19454
|
summary_sections_present: countPresentSections2(summary),
|
|
18797
19455
|
investigated_items: extractSectionItems2(summary.investigated),
|
|
18798
19456
|
learned_items: extractSectionItems2(summary.learned),
|
|
@@ -18803,6 +19461,7 @@ function buildSummaryVectorDocument(summary, config2, project, observations = []
|
|
|
18803
19461
|
capture_state: captureContext?.capture_state ?? "summary-only",
|
|
18804
19462
|
recent_request_prompts: captureContext?.recent_request_prompts ?? [],
|
|
18805
19463
|
latest_request: captureContext?.latest_request ?? null,
|
|
19464
|
+
current_thread: captureContext?.current_thread ?? null,
|
|
18806
19465
|
recent_tool_names: captureContext?.recent_tool_names ?? [],
|
|
18807
19466
|
recent_tool_commands: captureContext?.recent_tool_commands ?? [],
|
|
18808
19467
|
hot_files: captureContext?.hot_files ?? [],
|
|
@@ -18852,7 +19511,33 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
|
|
|
18852
19511
|
const doc3 = buildSummaryVectorDocument(summary, config2, {
|
|
18853
19512
|
canonical_id: project2.canonical_id,
|
|
18854
19513
|
name: project2.name
|
|
18855
|
-
}, summaryObservations,
|
|
19514
|
+
}, summaryObservations, buildSessionHandoffMetadata(sessionPrompts, sessionToolEvents, summaryObservations));
|
|
19515
|
+
batch.push({ entryId: entry.id, doc: doc3 });
|
|
19516
|
+
continue;
|
|
19517
|
+
}
|
|
19518
|
+
if (entry.record_type === "chat_message") {
|
|
19519
|
+
const chat = db.getChatMessageById(entry.record_id);
|
|
19520
|
+
if (!chat || chat.remote_source_id) {
|
|
19521
|
+
markSynced(db, entry.id);
|
|
19522
|
+
skipped++;
|
|
19523
|
+
continue;
|
|
19524
|
+
}
|
|
19525
|
+
if (!chat.project_id) {
|
|
19526
|
+
markSynced(db, entry.id);
|
|
19527
|
+
skipped++;
|
|
19528
|
+
continue;
|
|
19529
|
+
}
|
|
19530
|
+
const project2 = db.getProjectById(chat.project_id);
|
|
19531
|
+
if (!project2) {
|
|
19532
|
+
markSynced(db, entry.id);
|
|
19533
|
+
skipped++;
|
|
19534
|
+
continue;
|
|
19535
|
+
}
|
|
19536
|
+
markSyncing(db, entry.id);
|
|
19537
|
+
const doc3 = buildChatVectorDocument(chat, config2, {
|
|
19538
|
+
canonical_id: project2.canonical_id,
|
|
19539
|
+
name: project2.name
|
|
19540
|
+
});
|
|
18856
19541
|
batch.push({ entryId: entry.id, doc: doc3 });
|
|
18857
19542
|
continue;
|
|
18858
19543
|
}
|
|
@@ -18893,7 +19578,13 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
|
|
|
18893
19578
|
return { pushed, failed, skipped };
|
|
18894
19579
|
try {
|
|
18895
19580
|
await client.batchIngest(batch.map((b) => b.doc));
|
|
18896
|
-
for (const { entryId } of batch) {
|
|
19581
|
+
for (const { entryId, doc: doc2 } of batch) {
|
|
19582
|
+
if (doc2.source_type === "chat") {
|
|
19583
|
+
const localId = typeof doc2.metadata?.local_id === "number" ? doc2.metadata.local_id : null;
|
|
19584
|
+
if (localId !== null) {
|
|
19585
|
+
db.db.query("UPDATE chat_messages SET remote_source_id = ? WHERE id = ?").run(doc2.source_id, localId);
|
|
19586
|
+
}
|
|
19587
|
+
}
|
|
18897
19588
|
markSynced(db, entryId);
|
|
18898
19589
|
pushed++;
|
|
18899
19590
|
}
|
|
@@ -18901,6 +19592,12 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
|
|
|
18901
19592
|
for (const { entryId, doc: doc2 } of batch) {
|
|
18902
19593
|
try {
|
|
18903
19594
|
await client.ingest(doc2);
|
|
19595
|
+
if (doc2.source_type === "chat") {
|
|
19596
|
+
const localId = typeof doc2.metadata?.local_id === "number" ? doc2.metadata.local_id : null;
|
|
19597
|
+
if (localId !== null) {
|
|
19598
|
+
db.db.query("UPDATE chat_messages SET remote_source_id = ? WHERE id = ?").run(doc2.source_id, localId);
|
|
19599
|
+
}
|
|
19600
|
+
}
|
|
18904
19601
|
markSynced(db, entryId);
|
|
18905
19602
|
pushed++;
|
|
18906
19603
|
} catch (err) {
|
|
@@ -18923,48 +19620,6 @@ function countPresentSections2(summary) {
|
|
|
18923
19620
|
function extractSectionItems2(section) {
|
|
18924
19621
|
return extractSummaryItems(section, 4);
|
|
18925
19622
|
}
|
|
18926
|
-
function buildSummaryCaptureContext(prompts, toolEvents, observations) {
|
|
18927
|
-
const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
|
|
18928
|
-
const recentRequestPrompts = prompts.slice(-3).map((prompt) => prompt.prompt.trim()).filter(Boolean);
|
|
18929
|
-
const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
|
|
18930
|
-
const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
|
|
18931
|
-
const hotFiles = [...new Set(observations.flatMap((obs) => [
|
|
18932
|
-
...parseJsonArray3(obs.files_modified),
|
|
18933
|
-
...parseJsonArray3(obs.files_read)
|
|
18934
|
-
]).filter(Boolean))].slice(0, 6);
|
|
18935
|
-
const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0).slice(0, 6);
|
|
18936
|
-
const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
|
|
18937
|
-
const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
|
|
18938
|
-
if (!obs.source_tool)
|
|
18939
|
-
return acc;
|
|
18940
|
-
acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
|
|
18941
|
-
return acc;
|
|
18942
|
-
}, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
18943
|
-
const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
|
|
18944
|
-
return {
|
|
18945
|
-
prompt_count: prompts.length,
|
|
18946
|
-
tool_event_count: toolEvents.length,
|
|
18947
|
-
recent_request_prompts: recentRequestPrompts,
|
|
18948
|
-
latest_request: latestRequest,
|
|
18949
|
-
recent_tool_names: recentToolNames,
|
|
18950
|
-
recent_tool_commands: recentToolCommands,
|
|
18951
|
-
capture_state: captureState,
|
|
18952
|
-
hot_files: hotFiles,
|
|
18953
|
-
recent_outcomes: recentOutcomes,
|
|
18954
|
-
observation_source_tools: observationSourceTools,
|
|
18955
|
-
latest_observation_prompt_number: latestObservationPromptNumber
|
|
18956
|
-
};
|
|
18957
|
-
}
|
|
18958
|
-
function parseJsonArray3(value) {
|
|
18959
|
-
if (!value)
|
|
18960
|
-
return [];
|
|
18961
|
-
try {
|
|
18962
|
-
const parsed = JSON.parse(value);
|
|
18963
|
-
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
18964
|
-
} catch {
|
|
18965
|
-
return [];
|
|
18966
|
-
}
|
|
18967
|
-
}
|
|
18968
19623
|
|
|
18969
19624
|
// src/sync/pull.ts
|
|
18970
19625
|
var PULL_CURSOR_KEY = "pull_cursor";
|
|
@@ -18995,6 +19650,7 @@ function mergeChanges(db, config2, changes) {
|
|
|
18995
19650
|
for (const change of changes) {
|
|
18996
19651
|
const parsed = parseSourceId(change.source_id);
|
|
18997
19652
|
const remoteSummary = isRemoteSummary(change);
|
|
19653
|
+
const remoteChat = isRemoteChat(change);
|
|
18998
19654
|
if (parsed && parsed.deviceId === config2.device_id) {
|
|
18999
19655
|
skipped++;
|
|
19000
19656
|
continue;
|
|
@@ -19017,6 +19673,15 @@ function mergeChanges(db, config2, changes) {
|
|
|
19017
19673
|
merged++;
|
|
19018
19674
|
}
|
|
19019
19675
|
}
|
|
19676
|
+
if (remoteChat) {
|
|
19677
|
+
const mergedChat = mergeRemoteChat(db, config2, change, project.id);
|
|
19678
|
+
if (mergedChat) {
|
|
19679
|
+
merged++;
|
|
19680
|
+
} else {
|
|
19681
|
+
skipped++;
|
|
19682
|
+
}
|
|
19683
|
+
continue;
|
|
19684
|
+
}
|
|
19020
19685
|
const existing = db.db.query("SELECT id FROM observations WHERE remote_source_id = ?").get(change.source_id);
|
|
19021
19686
|
if (existing) {
|
|
19022
19687
|
if (!remoteSummary)
|
|
@@ -19058,6 +19723,10 @@ function isRemoteSummary(change) {
|
|
|
19058
19723
|
const rawType = typeof change.metadata?.type === "string" ? change.metadata.type.toLowerCase() : "";
|
|
19059
19724
|
return rawType === "summary" || change.source_id.includes("-summary-");
|
|
19060
19725
|
}
|
|
19726
|
+
function isRemoteChat(change) {
|
|
19727
|
+
const rawType = typeof change.metadata?.type === "string" ? change.metadata.type.toLowerCase() : "";
|
|
19728
|
+
return rawType === "chat" || change.source_id.includes("-chat-");
|
|
19729
|
+
}
|
|
19061
19730
|
function mergeRemoteSummary(db, config2, change, projectId) {
|
|
19062
19731
|
const sessionId = typeof change.metadata?.session_id === "string" ? change.metadata.session_id : null;
|
|
19063
19732
|
if (!sessionId)
|
|
@@ -19071,6 +19740,7 @@ function mergeRemoteSummary(db, config2, change, projectId) {
|
|
|
19071
19740
|
learned: typeof change.metadata?.learned === "string" ? change.metadata.learned : null,
|
|
19072
19741
|
completed: typeof change.metadata?.completed === "string" ? change.metadata.completed : null,
|
|
19073
19742
|
next_steps: typeof change.metadata?.next_steps === "string" ? change.metadata.next_steps : null,
|
|
19743
|
+
current_thread: typeof change.metadata?.current_thread === "string" ? change.metadata.current_thread : null,
|
|
19074
19744
|
capture_state: typeof change.metadata?.capture_state === "string" ? change.metadata.capture_state : null,
|
|
19075
19745
|
recent_tool_names: encodeStringArray(change.metadata?.recent_tool_names),
|
|
19076
19746
|
hot_files: encodeStringArray(change.metadata?.hot_files),
|
|
@@ -19078,6 +19748,26 @@ function mergeRemoteSummary(db, config2, change, projectId) {
|
|
|
19078
19748
|
});
|
|
19079
19749
|
return Boolean(summary);
|
|
19080
19750
|
}
|
|
19751
|
+
function mergeRemoteChat(db, config2, change, projectId) {
|
|
19752
|
+
if (db.getChatMessageByRemoteSourceId(change.source_id))
|
|
19753
|
+
return false;
|
|
19754
|
+
const sessionId = typeof change.metadata?.session_id === "string" ? change.metadata.session_id : null;
|
|
19755
|
+
const role = change.metadata?.role === "assistant" ? "assistant" : "user";
|
|
19756
|
+
if (!sessionId || typeof change.content !== "string" || !change.content.trim())
|
|
19757
|
+
return false;
|
|
19758
|
+
db.insertChatMessage({
|
|
19759
|
+
session_id: sessionId,
|
|
19760
|
+
project_id: projectId,
|
|
19761
|
+
role,
|
|
19762
|
+
content: change.content,
|
|
19763
|
+
user_id: (typeof change.metadata?.user_id === "string" ? change.metadata.user_id : null) ?? config2.user_id,
|
|
19764
|
+
device_id: (typeof change.metadata?.device_id === "string" ? change.metadata.device_id : null) ?? "remote",
|
|
19765
|
+
agent: typeof change.metadata?.agent === "string" ? change.metadata.agent : "unknown",
|
|
19766
|
+
created_at_epoch: typeof change.metadata?.created_at_epoch === "number" ? change.metadata.created_at_epoch : undefined,
|
|
19767
|
+
remote_source_id: change.source_id
|
|
19768
|
+
});
|
|
19769
|
+
return true;
|
|
19770
|
+
}
|
|
19081
19771
|
function encodeStringArray(value) {
|
|
19082
19772
|
if (!Array.isArray(value))
|
|
19083
19773
|
return null;
|
|
@@ -19886,7 +20576,7 @@ process.on("SIGTERM", () => {
|
|
|
19886
20576
|
});
|
|
19887
20577
|
var server = new McpServer({
|
|
19888
20578
|
name: "engrm",
|
|
19889
|
-
version: "0.4.
|
|
20579
|
+
version: "0.4.23"
|
|
19890
20580
|
});
|
|
19891
20581
|
server.tool("save_observation", "Save an observation to memory", {
|
|
19892
20582
|
type: exports_external.enum([
|
|
@@ -20973,6 +21663,143 @@ ${projectLines}`
|
|
|
20973
21663
|
]
|
|
20974
21664
|
};
|
|
20975
21665
|
});
|
|
21666
|
+
server.tool("create_handoff", "Capture an explicit cross-device handoff from the current or specified session into syncable memory", {
|
|
21667
|
+
session_id: exports_external.string().optional().describe("Optional session ID to hand off; defaults to the latest recent session"),
|
|
21668
|
+
cwd: exports_external.string().optional().describe("Repo path used to scope the handoff when no session ID is provided"),
|
|
21669
|
+
title: exports_external.string().optional().describe("Optional short handoff title"),
|
|
21670
|
+
include_chat: exports_external.boolean().optional().describe("Include a few recent chat snippets in the handoff"),
|
|
21671
|
+
chat_limit: exports_external.number().optional().describe("How many recent chat snippets to include when include_chat is true")
|
|
21672
|
+
}, async (params) => {
|
|
21673
|
+
const result = await createHandoff(db, config2, params);
|
|
21674
|
+
if (!result.success) {
|
|
21675
|
+
return {
|
|
21676
|
+
content: [{ type: "text", text: `Handoff not created: ${result.reason}` }]
|
|
21677
|
+
};
|
|
21678
|
+
}
|
|
21679
|
+
return {
|
|
21680
|
+
content: [
|
|
21681
|
+
{
|
|
21682
|
+
type: "text",
|
|
21683
|
+
text: `Created handoff #${result.observation_id} for session ${result.session_id}
|
|
21684
|
+
Title: ${result.title}`
|
|
21685
|
+
}
|
|
21686
|
+
]
|
|
21687
|
+
};
|
|
21688
|
+
});
|
|
21689
|
+
server.tool("recent_handoffs", "List recent explicit handoffs so you can resume work on another device or in a new session", {
|
|
21690
|
+
limit: exports_external.number().optional(),
|
|
21691
|
+
project_scoped: exports_external.boolean().optional(),
|
|
21692
|
+
cwd: exports_external.string().optional(),
|
|
21693
|
+
user_id: exports_external.string().optional()
|
|
21694
|
+
}, async (params) => {
|
|
21695
|
+
const result = getRecentHandoffs(db, {
|
|
21696
|
+
...params,
|
|
21697
|
+
user_id: params.user_id ?? config2.user_id
|
|
21698
|
+
});
|
|
21699
|
+
const projectLine = result.project ? `Project: ${result.project}
|
|
21700
|
+
` : "";
|
|
21701
|
+
const rows = result.handoffs.length > 0 ? result.handoffs.map((handoff) => {
|
|
21702
|
+
const stamp = new Date(handoff.created_at_epoch * 1000).toISOString().replace("T", " ").slice(0, 16);
|
|
21703
|
+
return `- #${handoff.id} (${stamp}) ${handoff.title}${handoff.project_name ? ` [${handoff.project_name}]` : ""}`;
|
|
21704
|
+
}).join(`
|
|
21705
|
+
`) : "- (none)";
|
|
21706
|
+
return {
|
|
21707
|
+
content: [{ type: "text", text: `${projectLine}Recent handoffs:
|
|
21708
|
+
${rows}` }]
|
|
21709
|
+
};
|
|
21710
|
+
});
|
|
21711
|
+
server.tool("load_handoff", "Open a saved handoff and turn it back into a clear resume point for a new session", {
|
|
21712
|
+
id: exports_external.number().optional().describe("Optional handoff observation ID; defaults to the latest recent handoff"),
|
|
21713
|
+
cwd: exports_external.string().optional(),
|
|
21714
|
+
project_scoped: exports_external.boolean().optional(),
|
|
21715
|
+
user_id: exports_external.string().optional()
|
|
21716
|
+
}, async (params) => {
|
|
21717
|
+
const result = loadHandoff(db, {
|
|
21718
|
+
...params,
|
|
21719
|
+
user_id: params.user_id ?? config2.user_id
|
|
21720
|
+
});
|
|
21721
|
+
if (!result.handoff) {
|
|
21722
|
+
return {
|
|
21723
|
+
content: [{ type: "text", text: "No matching handoff found" }]
|
|
21724
|
+
};
|
|
21725
|
+
}
|
|
21726
|
+
const facts = result.handoff.facts ? (() => {
|
|
21727
|
+
try {
|
|
21728
|
+
const parsed = JSON.parse(result.handoff.facts);
|
|
21729
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
21730
|
+
} catch {
|
|
21731
|
+
return [];
|
|
21732
|
+
}
|
|
21733
|
+
})() : [];
|
|
21734
|
+
const factLines = facts.length > 0 ? `
|
|
21735
|
+
|
|
21736
|
+
Facts:
|
|
21737
|
+
${facts.map((fact) => `- ${fact}`).join(`
|
|
21738
|
+
`)}` : "";
|
|
21739
|
+
const projectLine = result.handoff.project_name ? `Project: ${result.handoff.project_name}
|
|
21740
|
+
` : "";
|
|
21741
|
+
return {
|
|
21742
|
+
content: [
|
|
21743
|
+
{
|
|
21744
|
+
type: "text",
|
|
21745
|
+
text: `${projectLine}Handoff #${result.handoff.id}
|
|
21746
|
+
` + `Title: ${result.handoff.title}
|
|
21747
|
+
|
|
21748
|
+
` + `${result.handoff.narrative ?? "(no handoff narrative stored)"}${factLines}`
|
|
21749
|
+
}
|
|
21750
|
+
]
|
|
21751
|
+
};
|
|
21752
|
+
});
|
|
21753
|
+
server.tool("recent_chat", "Inspect recently captured chat messages in the separate chat lane", {
|
|
21754
|
+
limit: exports_external.number().optional(),
|
|
21755
|
+
project_scoped: exports_external.boolean().optional(),
|
|
21756
|
+
session_id: exports_external.string().optional(),
|
|
21757
|
+
cwd: exports_external.string().optional(),
|
|
21758
|
+
user_id: exports_external.string().optional()
|
|
21759
|
+
}, async (params) => {
|
|
21760
|
+
const result = getRecentChat(db, params);
|
|
21761
|
+
const projectLine = result.project ? `Project: ${result.project}
|
|
21762
|
+
` : "";
|
|
21763
|
+
const rows = result.messages.length > 0 ? result.messages.map((msg) => {
|
|
21764
|
+
const stamp = new Date(msg.created_at_epoch * 1000).toISOString().split("T")[0];
|
|
21765
|
+
return `- ${stamp} [${msg.role}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`;
|
|
21766
|
+
}).join(`
|
|
21767
|
+
`) : "- (none)";
|
|
21768
|
+
return {
|
|
21769
|
+
content: [
|
|
21770
|
+
{
|
|
21771
|
+
type: "text",
|
|
21772
|
+
text: `${projectLine}Recent chat:
|
|
21773
|
+
${rows}`
|
|
21774
|
+
}
|
|
21775
|
+
]
|
|
21776
|
+
};
|
|
21777
|
+
});
|
|
21778
|
+
server.tool("search_chat", "Search the separate chat lane without mixing it into durable memory observations", {
|
|
21779
|
+
query: exports_external.string().describe("Text to search for in captured chat"),
|
|
21780
|
+
limit: exports_external.number().optional(),
|
|
21781
|
+
project_scoped: exports_external.boolean().optional(),
|
|
21782
|
+
cwd: exports_external.string().optional(),
|
|
21783
|
+
user_id: exports_external.string().optional()
|
|
21784
|
+
}, async (params) => {
|
|
21785
|
+
const result = searchChat(db, params);
|
|
21786
|
+
const projectLine = result.project ? `Project: ${result.project}
|
|
21787
|
+
` : "";
|
|
21788
|
+
const rows = result.messages.length > 0 ? result.messages.map((msg) => {
|
|
21789
|
+
const stamp = new Date(msg.created_at_epoch * 1000).toISOString().split("T")[0];
|
|
21790
|
+
return `- ${stamp} [${msg.role}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`;
|
|
21791
|
+
}).join(`
|
|
21792
|
+
`) : "- (none)";
|
|
21793
|
+
return {
|
|
21794
|
+
content: [
|
|
21795
|
+
{
|
|
21796
|
+
type: "text",
|
|
21797
|
+
text: `${projectLine}Chat search for "${params.query}":
|
|
21798
|
+
${rows}`
|
|
21799
|
+
}
|
|
21800
|
+
]
|
|
21801
|
+
};
|
|
21802
|
+
});
|
|
20976
21803
|
server.tool("recent_requests", "Inspect recently captured raw user requests and prompt chronology", {
|
|
20977
21804
|
limit: exports_external.number().optional(),
|
|
20978
21805
|
project_scoped: exports_external.boolean().optional(),
|
|
@@ -21068,6 +21895,8 @@ server.tool("session_story", "Show the full local memory story for one session",
|
|
|
21068
21895
|
].filter(Boolean).join(`
|
|
21069
21896
|
`) : "(none)";
|
|
21070
21897
|
const promptLines = result.prompts.length > 0 ? result.prompts.map((prompt) => `- #${prompt.prompt_number} ${prompt.prompt.replace(/\s+/g, " ").trim()}`).join(`
|
|
21898
|
+
`) : "- (none)";
|
|
21899
|
+
const chatLines = result.chat_messages.length > 0 ? result.chat_messages.slice(-12).map((msg) => `- [${msg.role}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`).join(`
|
|
21071
21900
|
`) : "- (none)";
|
|
21072
21901
|
const toolLines = result.tool_events.length > 0 ? result.tool_events.slice(-15).map((tool) => {
|
|
21073
21902
|
const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
|
|
@@ -21082,6 +21911,8 @@ server.tool("session_story", "Show the full local memory story for one session",
|
|
|
21082
21911
|
provenance.push(`#${obs.source_prompt_number}`);
|
|
21083
21912
|
return `- #${obs.id} [${obs.type}] ${obs.title}${provenance.length ? ` (${provenance.join(" · ")})` : ""}`;
|
|
21084
21913
|
}).join(`
|
|
21914
|
+
`) : "- (none)";
|
|
21915
|
+
const handoffLines = result.handoffs.length > 0 ? result.handoffs.slice(-8).map((obs) => `- #${obs.id} ${obs.title}`).join(`
|
|
21085
21916
|
`) : "- (none)";
|
|
21086
21917
|
const metrics = result.metrics ? `files=${result.metrics.files_touched_count}, searches=${result.metrics.searches_performed}, tools=${result.metrics.tool_calls_count}, observations=${result.metrics.observation_count}` : "metrics unavailable";
|
|
21087
21918
|
const captureGaps = result.capture_gaps.length > 0 ? result.capture_gaps.map((gap) => `- ${gap}`).join(`
|
|
@@ -21103,9 +21934,15 @@ ${summaryLines}
|
|
|
21103
21934
|
` + `Prompts:
|
|
21104
21935
|
${promptLines}
|
|
21105
21936
|
|
|
21937
|
+
` + `Chat:
|
|
21938
|
+
${chatLines}
|
|
21939
|
+
|
|
21106
21940
|
` + `Tools:
|
|
21107
21941
|
${toolLines}
|
|
21108
21942
|
|
|
21943
|
+
` + `Handoffs:
|
|
21944
|
+
${handoffLines}
|
|
21945
|
+
|
|
21109
21946
|
` + `Provenance:
|
|
21110
21947
|
${provenanceSummary}
|
|
21111
21948
|
|