engrm 0.4.22 → 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 +308 -24
- package/dist/hooks/elicitation-result.js +223 -15
- package/dist/hooks/post-tool-use.js +257 -15
- package/dist/hooks/pre-compact.js +962 -17
- package/dist/hooks/sentinel.js +223 -15
- package/dist/hooks/session-start.js +1066 -92
- package/dist/hooks/stop.js +361 -33
- package/dist/hooks/user-prompt-submit.js +267 -15
- package/dist/server.js +863 -49
- 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'
|
|
@@ -14120,6 +14120,18 @@ var MIGRATIONS = [
|
|
|
14120
14120
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
14121
14121
|
`
|
|
14122
14122
|
},
|
|
14123
|
+
{
|
|
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
|
+
},
|
|
14123
14135
|
{
|
|
14124
14136
|
version: 12,
|
|
14125
14137
|
description: "Add synced handoff metadata to session summaries",
|
|
@@ -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
|
];
|
|
@@ -14199,6 +14275,18 @@ function inferLegacySchemaVersion(db) {
|
|
|
14199
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")) {
|
|
14200
14276
|
version2 = Math.max(version2, 12);
|
|
14201
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
|
+
}
|
|
14202
14290
|
return version2;
|
|
14203
14291
|
}
|
|
14204
14292
|
function runMigrations(db) {
|
|
@@ -14282,7 +14370,8 @@ function ensureSessionSummaryColumns(db) {
|
|
|
14282
14370
|
"capture_state",
|
|
14283
14371
|
"recent_tool_names",
|
|
14284
14372
|
"hot_files",
|
|
14285
|
-
"recent_outcomes"
|
|
14373
|
+
"recent_outcomes",
|
|
14374
|
+
"current_thread"
|
|
14286
14375
|
];
|
|
14287
14376
|
for (const column of required2) {
|
|
14288
14377
|
if (columnExists(db, "session_summaries", column))
|
|
@@ -14290,10 +14379,75 @@ function ensureSessionSummaryColumns(db) {
|
|
|
14290
14379
|
db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
|
|
14291
14380
|
}
|
|
14292
14381
|
const current = getSchemaVersion(db);
|
|
14293
|
-
if (current <
|
|
14294
|
-
db.exec("PRAGMA user_version =
|
|
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)}`);
|
|
14295
14444
|
}
|
|
14296
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
|
+
}
|
|
14297
14451
|
function getSchemaVersion(db) {
|
|
14298
14452
|
const result = db.query("PRAGMA user_version").get();
|
|
14299
14453
|
return result.user_version;
|
|
@@ -14453,6 +14607,8 @@ class MemDatabase {
|
|
|
14453
14607
|
runMigrations(this.db);
|
|
14454
14608
|
ensureObservationTypes(this.db);
|
|
14455
14609
|
ensureSessionSummaryColumns(this.db);
|
|
14610
|
+
ensureChatMessageColumns(this.db);
|
|
14611
|
+
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
14456
14612
|
}
|
|
14457
14613
|
loadVecExtension() {
|
|
14458
14614
|
try {
|
|
@@ -14678,6 +14834,7 @@ class MemDatabase {
|
|
|
14678
14834
|
p.name AS project_name,
|
|
14679
14835
|
ss.request AS request,
|
|
14680
14836
|
ss.completed AS completed,
|
|
14837
|
+
ss.current_thread AS current_thread,
|
|
14681
14838
|
ss.capture_state AS capture_state,
|
|
14682
14839
|
ss.recent_tool_names AS recent_tool_names,
|
|
14683
14840
|
ss.hot_files AS hot_files,
|
|
@@ -14696,6 +14853,7 @@ class MemDatabase {
|
|
|
14696
14853
|
p.name AS project_name,
|
|
14697
14854
|
ss.request AS request,
|
|
14698
14855
|
ss.completed AS completed,
|
|
14856
|
+
ss.current_thread AS current_thread,
|
|
14699
14857
|
ss.capture_state AS capture_state,
|
|
14700
14858
|
ss.recent_tool_names AS recent_tool_names,
|
|
14701
14859
|
ss.hot_files AS hot_files,
|
|
@@ -14786,6 +14944,54 @@ class MemDatabase {
|
|
|
14786
14944
|
ORDER BY created_at_epoch DESC, id DESC
|
|
14787
14945
|
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
14788
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
|
+
}
|
|
14789
14995
|
addToOutbox(recordType, recordId) {
|
|
14790
14996
|
const now = Math.floor(Date.now() / 1000);
|
|
14791
14997
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|
|
@@ -14874,9 +15080,9 @@ class MemDatabase {
|
|
|
14874
15080
|
};
|
|
14875
15081
|
const result = this.db.query(`INSERT INTO session_summaries (
|
|
14876
15082
|
session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
|
|
14877
|
-
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
|
|
14878
15084
|
)
|
|
14879
|
-
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);
|
|
14880
15086
|
const id = Number(result.lastInsertRowid);
|
|
14881
15087
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
14882
15088
|
}
|
|
@@ -14892,6 +15098,7 @@ class MemDatabase {
|
|
|
14892
15098
|
learned: normalizeSummarySection(summary.learned ?? existing.learned),
|
|
14893
15099
|
completed: normalizeSummarySection(summary.completed ?? existing.completed),
|
|
14894
15100
|
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
|
|
15101
|
+
current_thread: summary.current_thread ?? existing.current_thread,
|
|
14895
15102
|
capture_state: summary.capture_state ?? existing.capture_state,
|
|
14896
15103
|
recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
|
|
14897
15104
|
hot_files: summary.hot_files ?? existing.hot_files,
|
|
@@ -14905,12 +15112,13 @@ class MemDatabase {
|
|
|
14905
15112
|
learned = ?,
|
|
14906
15113
|
completed = ?,
|
|
14907
15114
|
next_steps = ?,
|
|
15115
|
+
current_thread = ?,
|
|
14908
15116
|
capture_state = ?,
|
|
14909
15117
|
recent_tool_names = ?,
|
|
14910
15118
|
hot_files = ?,
|
|
14911
15119
|
recent_outcomes = ?,
|
|
14912
15120
|
created_at_epoch = ?
|
|
14913
|
-
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);
|
|
14914
15122
|
return this.getSessionSummary(summary.session_id);
|
|
14915
15123
|
}
|
|
14916
15124
|
getSessionSummary(sessionId) {
|
|
@@ -16161,12 +16369,12 @@ function getRecentRequests(db, input) {
|
|
|
16161
16369
|
};
|
|
16162
16370
|
}
|
|
16163
16371
|
|
|
16164
|
-
// src/tools/recent-
|
|
16165
|
-
function
|
|
16166
|
-
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));
|
|
16167
16375
|
if (input.session_id) {
|
|
16168
16376
|
return {
|
|
16169
|
-
|
|
16377
|
+
messages: db.getSessionChatMessages(input.session_id, limit).slice(-limit).reverse()
|
|
16170
16378
|
};
|
|
16171
16379
|
}
|
|
16172
16380
|
const projectScoped = input.project_scoped !== false;
|
|
@@ -16182,7 +16390,28 @@ function getRecentTools(db, input) {
|
|
|
16182
16390
|
}
|
|
16183
16391
|
}
|
|
16184
16392
|
return {
|
|
16185
|
-
|
|
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),
|
|
16186
16415
|
project: projectName
|
|
16187
16416
|
};
|
|
16188
16417
|
}
|
|
@@ -16192,8 +16421,11 @@ function getSessionStory(db, input) {
|
|
|
16192
16421
|
const session = db.getSessionById(input.session_id);
|
|
16193
16422
|
const summary = db.getSessionSummary(input.session_id);
|
|
16194
16423
|
const prompts = db.getSessionUserPrompts(input.session_id, 50);
|
|
16424
|
+
const chatMessages = db.getSessionChatMessages(input.session_id, 50);
|
|
16195
16425
|
const toolEvents = db.getSessionToolEvents(input.session_id, 100);
|
|
16196
|
-
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));
|
|
16197
16429
|
const metrics = db.getSessionMetrics(input.session_id);
|
|
16198
16430
|
const projectName = session?.project_id !== null && session?.project_id !== undefined ? db.getProjectById(session.project_id)?.name ?? null : null;
|
|
16199
16431
|
const latestRequest = prompts[prompts.length - 1]?.prompt?.trim() || summary?.request?.trim() || null;
|
|
@@ -16202,8 +16434,10 @@ function getSessionStory(db, input) {
|
|
|
16202
16434
|
project_name: projectName,
|
|
16203
16435
|
summary,
|
|
16204
16436
|
prompts,
|
|
16437
|
+
chat_messages: chatMessages,
|
|
16205
16438
|
tool_events: toolEvents,
|
|
16206
16439
|
observations,
|
|
16440
|
+
handoffs,
|
|
16207
16441
|
metrics,
|
|
16208
16442
|
capture_state: classifyCaptureState({
|
|
16209
16443
|
hasSummary: Boolean(summary?.request || summary?.completed),
|
|
@@ -16297,6 +16531,269 @@ function collectProvenanceSummary(observations) {
|
|
|
16297
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);
|
|
16298
16532
|
}
|
|
16299
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
|
+
|
|
16300
16797
|
// src/tools/recent-sessions.ts
|
|
16301
16798
|
function getRecentSessions(db, input) {
|
|
16302
16799
|
const limit = Math.max(1, Math.min(input.limit ?? 10, 50));
|
|
@@ -16652,6 +17149,12 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
16652
17149
|
const recentSessions2 = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
16653
17150
|
const projectTypeCounts2 = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
16654
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;
|
|
16655
17158
|
return {
|
|
16656
17159
|
project_name: projectName,
|
|
16657
17160
|
canonical_id: canonicalId,
|
|
@@ -16662,7 +17165,8 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
16662
17165
|
recentToolEvents: recentToolEvents2.length > 0 ? recentToolEvents2 : undefined,
|
|
16663
17166
|
recentSessions: recentSessions2.length > 0 ? recentSessions2 : undefined,
|
|
16664
17167
|
projectTypeCounts: projectTypeCounts2,
|
|
16665
|
-
recentOutcomes: recentOutcomes2
|
|
17168
|
+
recentOutcomes: recentOutcomes2,
|
|
17169
|
+
recentHandoffs: recentHandoffs2.length > 0 ? recentHandoffs2 : undefined
|
|
16666
17170
|
};
|
|
16667
17171
|
}
|
|
16668
17172
|
let remainingBudget = tokenBudget - 30;
|
|
@@ -16690,6 +17194,12 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
16690
17194
|
const recentSessions = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
16691
17195
|
const projectTypeCounts = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
16692
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;
|
|
16693
17203
|
let securityFindings = [];
|
|
16694
17204
|
if (!isNewProject) {
|
|
16695
17205
|
try {
|
|
@@ -16748,7 +17258,8 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
16748
17258
|
recentToolEvents: recentToolEvents.length > 0 ? recentToolEvents : undefined,
|
|
16749
17259
|
recentSessions: recentSessions.length > 0 ? recentSessions : undefined,
|
|
16750
17260
|
projectTypeCounts,
|
|
16751
|
-
recentOutcomes
|
|
17261
|
+
recentOutcomes,
|
|
17262
|
+
recentHandoffs: recentHandoffs.length > 0 ? recentHandoffs : undefined
|
|
16752
17263
|
};
|
|
16753
17264
|
}
|
|
16754
17265
|
function estimateObservationTokens(obs, index) {
|
|
@@ -17241,6 +17752,9 @@ function buildSuggestedTools(sessions, requestCount, toolCount, observationCount
|
|
|
17241
17752
|
if (observationCount > 0) {
|
|
17242
17753
|
suggested.push("tool_memory_index", "capture_git_worktree");
|
|
17243
17754
|
}
|
|
17755
|
+
if (sessions.length > 0) {
|
|
17756
|
+
suggested.push("create_handoff", "recent_handoffs");
|
|
17757
|
+
}
|
|
17244
17758
|
return Array.from(new Set(suggested)).slice(0, 4);
|
|
17245
17759
|
}
|
|
17246
17760
|
|
|
@@ -17304,6 +17818,8 @@ function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, obse
|
|
|
17304
17818
|
suggested.push("activity_feed");
|
|
17305
17819
|
if (observationCount > 0)
|
|
17306
17820
|
suggested.push("tool_memory_index", "capture_git_worktree");
|
|
17821
|
+
if (sessionCount > 0)
|
|
17822
|
+
suggested.push("create_handoff", "recent_handoffs");
|
|
17307
17823
|
return Array.from(new Set(suggested)).slice(0, 4);
|
|
17308
17824
|
}
|
|
17309
17825
|
|
|
@@ -17520,6 +18036,17 @@ function toToolEvent(tool) {
|
|
|
17520
18036
|
};
|
|
17521
18037
|
}
|
|
17522
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
|
+
}
|
|
17523
18050
|
const detailBits = [];
|
|
17524
18051
|
if (obs.source_tool)
|
|
17525
18052
|
detailBits.push(`via ${obs.source_tool}`);
|
|
@@ -17563,9 +18090,10 @@ function compareEvents(a, b) {
|
|
|
17563
18090
|
}
|
|
17564
18091
|
const kindOrder = {
|
|
17565
18092
|
summary: 0,
|
|
17566
|
-
|
|
17567
|
-
|
|
17568
|
-
|
|
18093
|
+
handoff: 1,
|
|
18094
|
+
observation: 2,
|
|
18095
|
+
tool: 3,
|
|
18096
|
+
prompt: 4
|
|
17569
18097
|
};
|
|
17570
18098
|
if (kindOrder[a.kind] !== kindOrder[b.kind]) {
|
|
17571
18099
|
return kindOrder[a.kind] - kindOrder[b.kind];
|
|
@@ -17586,6 +18114,7 @@ function getActivityFeed(db, input) {
|
|
|
17586
18114
|
})].filter((event) => event !== null) : [],
|
|
17587
18115
|
...story.prompts.map(toPromptEvent),
|
|
17588
18116
|
...story.tool_events.map(toToolEvent),
|
|
18117
|
+
...story.handoffs.map(toObservationEvent),
|
|
17589
18118
|
...story.observations.map(toObservationEvent)
|
|
17590
18119
|
].sort(compareEvents).slice(0, limit);
|
|
17591
18120
|
return { events: events2, project };
|
|
@@ -18035,13 +18564,13 @@ function getSessionContext(db, input) {
|
|
|
18035
18564
|
function buildHotFiles(context) {
|
|
18036
18565
|
const counts = new Map;
|
|
18037
18566
|
for (const obs of context.observations) {
|
|
18038
|
-
for (const path of [...
|
|
18567
|
+
for (const path of [...parseJsonArray3(obs.files_read), ...parseJsonArray3(obs.files_modified)]) {
|
|
18039
18568
|
counts.set(path, (counts.get(path) ?? 0) + 1);
|
|
18040
18569
|
}
|
|
18041
18570
|
}
|
|
18042
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);
|
|
18043
18572
|
}
|
|
18044
|
-
function
|
|
18573
|
+
function parseJsonArray3(value) {
|
|
18045
18574
|
if (!value)
|
|
18046
18575
|
return [];
|
|
18047
18576
|
try {
|
|
@@ -18062,6 +18591,9 @@ function buildSuggestedTools2(context) {
|
|
|
18062
18591
|
if (context.observations.length > 0) {
|
|
18063
18592
|
tools.push("tool_memory_index", "capture_git_worktree");
|
|
18064
18593
|
}
|
|
18594
|
+
if ((context.recentSessions?.length ?? 0) > 0) {
|
|
18595
|
+
tools.push("create_handoff", "recent_handoffs");
|
|
18596
|
+
}
|
|
18065
18597
|
return Array.from(new Set(tools)).slice(0, 4);
|
|
18066
18598
|
}
|
|
18067
18599
|
|
|
@@ -18612,22 +19144,27 @@ function buildSourceId(config2, localId, type = "obs") {
|
|
|
18612
19144
|
return `${config2.user_id}-${config2.device_id}-${type}-${localId}`;
|
|
18613
19145
|
}
|
|
18614
19146
|
function parseSourceId(sourceId) {
|
|
18615
|
-
const
|
|
18616
|
-
|
|
18617
|
-
|
|
18618
|
-
|
|
18619
|
-
|
|
18620
|
-
|
|
18621
|
-
|
|
18622
|
-
|
|
18623
|
-
|
|
18624
|
-
|
|
18625
|
-
|
|
18626
|
-
|
|
18627
|
-
|
|
18628
|
-
|
|
18629
|
-
|
|
18630
|
-
|
|
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;
|
|
18631
19168
|
}
|
|
18632
19169
|
|
|
18633
19170
|
// src/sync/client.ts
|
|
@@ -18740,8 +19277,8 @@ function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
|
|
|
18740
19277
|
const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
|
|
18741
19278
|
const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
|
|
18742
19279
|
const hotFiles = [...new Set(observations.flatMap((obs) => [
|
|
18743
|
-
...
|
|
18744
|
-
...
|
|
19280
|
+
...parseJsonArray4(obs.files_modified),
|
|
19281
|
+
...parseJsonArray4(obs.files_read)
|
|
18745
19282
|
]).filter(Boolean))].slice(0, 6);
|
|
18746
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);
|
|
18747
19284
|
const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
|
|
@@ -18752,11 +19289,13 @@ function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
|
|
|
18752
19289
|
return acc;
|
|
18753
19290
|
}, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
18754
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);
|
|
18755
19293
|
return {
|
|
18756
19294
|
prompt_count: prompts.length,
|
|
18757
19295
|
tool_event_count: toolEvents.length,
|
|
18758
19296
|
recent_request_prompts: recentRequestPrompts,
|
|
18759
19297
|
latest_request: latestRequest,
|
|
19298
|
+
current_thread: currentThread,
|
|
18760
19299
|
recent_tool_names: recentToolNames,
|
|
18761
19300
|
recent_tool_commands: recentToolCommands,
|
|
18762
19301
|
capture_state: captureState,
|
|
@@ -18766,7 +19305,38 @@ function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
|
|
|
18766
19305
|
latest_observation_prompt_number: latestObservationPromptNumber
|
|
18767
19306
|
};
|
|
18768
19307
|
}
|
|
18769
|
-
function
|
|
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) {
|
|
18770
19340
|
if (!value)
|
|
18771
19341
|
return [];
|
|
18772
19342
|
try {
|
|
@@ -18778,6 +19348,28 @@ function parseJsonArray3(value) {
|
|
|
18778
19348
|
}
|
|
18779
19349
|
|
|
18780
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
|
+
}
|
|
18781
19373
|
function buildVectorDocument(obs, config2, project) {
|
|
18782
19374
|
const parts = [obs.title];
|
|
18783
19375
|
if (obs.narrative)
|
|
@@ -18858,6 +19450,7 @@ function buildSummaryVectorDocument(summary, config2, project, observations = []
|
|
|
18858
19450
|
learned: summary.learned,
|
|
18859
19451
|
completed: summary.completed,
|
|
18860
19452
|
next_steps: summary.next_steps,
|
|
19453
|
+
current_thread: summary.current_thread,
|
|
18861
19454
|
summary_sections_present: countPresentSections2(summary),
|
|
18862
19455
|
investigated_items: extractSectionItems2(summary.investigated),
|
|
18863
19456
|
learned_items: extractSectionItems2(summary.learned),
|
|
@@ -18868,6 +19461,7 @@ function buildSummaryVectorDocument(summary, config2, project, observations = []
|
|
|
18868
19461
|
capture_state: captureContext?.capture_state ?? "summary-only",
|
|
18869
19462
|
recent_request_prompts: captureContext?.recent_request_prompts ?? [],
|
|
18870
19463
|
latest_request: captureContext?.latest_request ?? null,
|
|
19464
|
+
current_thread: captureContext?.current_thread ?? null,
|
|
18871
19465
|
recent_tool_names: captureContext?.recent_tool_names ?? [],
|
|
18872
19466
|
recent_tool_commands: captureContext?.recent_tool_commands ?? [],
|
|
18873
19467
|
hot_files: captureContext?.hot_files ?? [],
|
|
@@ -18921,6 +19515,32 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
|
|
|
18921
19515
|
batch.push({ entryId: entry.id, doc: doc3 });
|
|
18922
19516
|
continue;
|
|
18923
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
|
+
});
|
|
19541
|
+
batch.push({ entryId: entry.id, doc: doc3 });
|
|
19542
|
+
continue;
|
|
19543
|
+
}
|
|
18924
19544
|
if (entry.record_type !== "observation") {
|
|
18925
19545
|
skipped++;
|
|
18926
19546
|
continue;
|
|
@@ -18958,7 +19578,13 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
|
|
|
18958
19578
|
return { pushed, failed, skipped };
|
|
18959
19579
|
try {
|
|
18960
19580
|
await client.batchIngest(batch.map((b) => b.doc));
|
|
18961
|
-
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
|
+
}
|
|
18962
19588
|
markSynced(db, entryId);
|
|
18963
19589
|
pushed++;
|
|
18964
19590
|
}
|
|
@@ -18966,6 +19592,12 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
|
|
|
18966
19592
|
for (const { entryId, doc: doc2 } of batch) {
|
|
18967
19593
|
try {
|
|
18968
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
|
+
}
|
|
18969
19601
|
markSynced(db, entryId);
|
|
18970
19602
|
pushed++;
|
|
18971
19603
|
} catch (err) {
|
|
@@ -19018,6 +19650,7 @@ function mergeChanges(db, config2, changes) {
|
|
|
19018
19650
|
for (const change of changes) {
|
|
19019
19651
|
const parsed = parseSourceId(change.source_id);
|
|
19020
19652
|
const remoteSummary = isRemoteSummary(change);
|
|
19653
|
+
const remoteChat = isRemoteChat(change);
|
|
19021
19654
|
if (parsed && parsed.deviceId === config2.device_id) {
|
|
19022
19655
|
skipped++;
|
|
19023
19656
|
continue;
|
|
@@ -19040,6 +19673,15 @@ function mergeChanges(db, config2, changes) {
|
|
|
19040
19673
|
merged++;
|
|
19041
19674
|
}
|
|
19042
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
|
+
}
|
|
19043
19685
|
const existing = db.db.query("SELECT id FROM observations WHERE remote_source_id = ?").get(change.source_id);
|
|
19044
19686
|
if (existing) {
|
|
19045
19687
|
if (!remoteSummary)
|
|
@@ -19081,6 +19723,10 @@ function isRemoteSummary(change) {
|
|
|
19081
19723
|
const rawType = typeof change.metadata?.type === "string" ? change.metadata.type.toLowerCase() : "";
|
|
19082
19724
|
return rawType === "summary" || change.source_id.includes("-summary-");
|
|
19083
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
|
+
}
|
|
19084
19730
|
function mergeRemoteSummary(db, config2, change, projectId) {
|
|
19085
19731
|
const sessionId = typeof change.metadata?.session_id === "string" ? change.metadata.session_id : null;
|
|
19086
19732
|
if (!sessionId)
|
|
@@ -19094,6 +19740,7 @@ function mergeRemoteSummary(db, config2, change, projectId) {
|
|
|
19094
19740
|
learned: typeof change.metadata?.learned === "string" ? change.metadata.learned : null,
|
|
19095
19741
|
completed: typeof change.metadata?.completed === "string" ? change.metadata.completed : null,
|
|
19096
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,
|
|
19097
19744
|
capture_state: typeof change.metadata?.capture_state === "string" ? change.metadata.capture_state : null,
|
|
19098
19745
|
recent_tool_names: encodeStringArray(change.metadata?.recent_tool_names),
|
|
19099
19746
|
hot_files: encodeStringArray(change.metadata?.hot_files),
|
|
@@ -19101,6 +19748,26 @@ function mergeRemoteSummary(db, config2, change, projectId) {
|
|
|
19101
19748
|
});
|
|
19102
19749
|
return Boolean(summary);
|
|
19103
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
|
+
}
|
|
19104
19771
|
function encodeStringArray(value) {
|
|
19105
19772
|
if (!Array.isArray(value))
|
|
19106
19773
|
return null;
|
|
@@ -19909,7 +20576,7 @@ process.on("SIGTERM", () => {
|
|
|
19909
20576
|
});
|
|
19910
20577
|
var server = new McpServer({
|
|
19911
20578
|
name: "engrm",
|
|
19912
|
-
version: "0.4.
|
|
20579
|
+
version: "0.4.23"
|
|
19913
20580
|
});
|
|
19914
20581
|
server.tool("save_observation", "Save an observation to memory", {
|
|
19915
20582
|
type: exports_external.enum([
|
|
@@ -20996,6 +21663,143 @@ ${projectLines}`
|
|
|
20996
21663
|
]
|
|
20997
21664
|
};
|
|
20998
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
|
+
});
|
|
20999
21803
|
server.tool("recent_requests", "Inspect recently captured raw user requests and prompt chronology", {
|
|
21000
21804
|
limit: exports_external.number().optional(),
|
|
21001
21805
|
project_scoped: exports_external.boolean().optional(),
|
|
@@ -21091,6 +21895,8 @@ server.tool("session_story", "Show the full local memory story for one session",
|
|
|
21091
21895
|
].filter(Boolean).join(`
|
|
21092
21896
|
`) : "(none)";
|
|
21093
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(`
|
|
21094
21900
|
`) : "- (none)";
|
|
21095
21901
|
const toolLines = result.tool_events.length > 0 ? result.tool_events.slice(-15).map((tool) => {
|
|
21096
21902
|
const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
|
|
@@ -21105,6 +21911,8 @@ server.tool("session_story", "Show the full local memory story for one session",
|
|
|
21105
21911
|
provenance.push(`#${obs.source_prompt_number}`);
|
|
21106
21912
|
return `- #${obs.id} [${obs.type}] ${obs.title}${provenance.length ? ` (${provenance.join(" · ")})` : ""}`;
|
|
21107
21913
|
}).join(`
|
|
21914
|
+
`) : "- (none)";
|
|
21915
|
+
const handoffLines = result.handoffs.length > 0 ? result.handoffs.slice(-8).map((obs) => `- #${obs.id} ${obs.title}`).join(`
|
|
21108
21916
|
`) : "- (none)";
|
|
21109
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";
|
|
21110
21918
|
const captureGaps = result.capture_gaps.length > 0 ? result.capture_gaps.map((gap) => `- ${gap}`).join(`
|
|
@@ -21126,9 +21934,15 @@ ${summaryLines}
|
|
|
21126
21934
|
` + `Prompts:
|
|
21127
21935
|
${promptLines}
|
|
21128
21936
|
|
|
21937
|
+
` + `Chat:
|
|
21938
|
+
${chatLines}
|
|
21939
|
+
|
|
21129
21940
|
` + `Tools:
|
|
21130
21941
|
${toolLines}
|
|
21131
21942
|
|
|
21943
|
+
` + `Handoffs:
|
|
21944
|
+
${handoffLines}
|
|
21945
|
+
|
|
21132
21946
|
` + `Provenance:
|
|
21133
21947
|
${provenanceSummary}
|
|
21134
21948
|
|