chapterhouse 0.4.2 → 0.5.0
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/agents/bellonda.agent.md +11 -0
- package/agents/hwi-noree.agent.md +12 -0
- package/dist/api/server.js +39 -2
- package/dist/api/server.test.js +20 -0
- package/dist/api/turn-sse.integration.test.js +12 -0
- package/dist/copilot/agents.js +16 -4
- package/dist/copilot/agents.test.js +43 -1
- package/dist/copilot/orchestrator.js +173 -32
- package/dist/copilot/orchestrator.test.js +236 -20
- package/dist/copilot/session-manager.js +11 -2
- package/dist/copilot/session-manager.test.js +25 -0
- package/dist/copilot/tools.agent.test.js +52 -4
- package/dist/copilot/tools.js +265 -18
- package/dist/copilot/tools.memory.test.js +175 -2
- package/dist/daemon.js +6 -0
- package/dist/memory/action-items.js +100 -0
- package/dist/memory/action-items.test.js +83 -0
- package/dist/memory/active-scope.js +9 -0
- package/dist/memory/eot.js +28 -3
- package/dist/memory/eot.test.js +108 -0
- package/dist/memory/hot-tier.js +60 -1
- package/dist/memory/hot-tier.test.js +38 -0
- package/dist/memory/housekeeping-scheduler.js +152 -0
- package/dist/memory/housekeeping-scheduler.test.js +187 -0
- package/dist/memory/index.js +2 -1
- package/dist/memory/recall.js +59 -0
- package/dist/memory/recall.test.js +27 -0
- package/dist/memory/tiering.js +33 -3
- package/dist/store/db.js +130 -17
- package/dist/store/db.test.js +61 -5
- package/package.json +1 -1
- package/web/dist/assets/{index-B_cCSHan.js → index-BfHqP3-C.js} +87 -87
- package/web/dist/assets/{index-B_cCSHan.js.map → index-BfHqP3-C.js.map} +1 -1
- package/web/dist/assets/index-_O6AoWOS.css +10 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-DhY5yWmC.css +0 -10
package/dist/store/db.js
CHANGED
|
@@ -141,7 +141,7 @@ function rebuildMemoryTierTables(database) {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
function ensureMemoryTierColumns(database) {
|
|
144
|
-
for (const table of ["mem_entities", "mem_observations", "mem_decisions"]) {
|
|
144
|
+
for (const table of ["mem_entities", "mem_observations", "mem_decisions", "mem_action_items"]) {
|
|
145
145
|
if (!hasColumn(database, table, "tier")) {
|
|
146
146
|
database.exec(`ALTER TABLE ${table} ADD COLUMN tier TEXT DEFAULT 'warm'`);
|
|
147
147
|
}
|
|
@@ -184,12 +184,24 @@ function ensureMemoryTierColumns(database) {
|
|
|
184
184
|
ELSE 'warm'
|
|
185
185
|
END
|
|
186
186
|
`);
|
|
187
|
+
database.exec(`
|
|
188
|
+
UPDATE mem_action_items
|
|
189
|
+
SET tier = CASE
|
|
190
|
+
WHEN status IN ('done', 'dropped') THEN 'cold'
|
|
191
|
+
WHEN tier = 'glacier' THEN 'cold'
|
|
192
|
+
WHEN tier IN ('hot', 'warm', 'cold') THEN tier
|
|
193
|
+
WHEN status = 'open' AND due_at IS NOT NULL AND datetime(due_at) <= datetime('now', '+7 days') THEN 'hot'
|
|
194
|
+
ELSE 'warm'
|
|
195
|
+
END
|
|
196
|
+
`);
|
|
187
197
|
}
|
|
188
198
|
function ensureMemoryIndexes(database) {
|
|
189
199
|
database.exec(`CREATE INDEX IF NOT EXISTS mem_entities_scope_kind_idx ON mem_entities(scope_id, kind)`);
|
|
190
200
|
database.exec(`CREATE UNIQUE INDEX IF NOT EXISTS mem_entities_scope_kind_name_idx ON mem_entities(scope_id, kind, name)`);
|
|
191
201
|
database.exec(`CREATE INDEX IF NOT EXISTS mem_observations_scope_idx ON mem_observations(scope_id)`);
|
|
192
202
|
database.exec(`CREATE INDEX IF NOT EXISTS mem_decisions_scope_idx ON mem_decisions(scope_id)`);
|
|
203
|
+
database.exec(`CREATE INDEX IF NOT EXISTS idx_mem_action_items_scope_status ON mem_action_items(scope_id, status)`);
|
|
204
|
+
database.exec(`CREATE INDEX IF NOT EXISTS idx_mem_action_items_due ON mem_action_items(status, due_at)`);
|
|
193
205
|
}
|
|
194
206
|
const MEMORY_SCOPE_SEEDS = [
|
|
195
207
|
{
|
|
@@ -561,6 +573,8 @@ export function getDb() {
|
|
|
561
573
|
source TEXT NOT NULL DEFAULT 'unknown',
|
|
562
574
|
session_key TEXT NOT NULL DEFAULT 'default',
|
|
563
575
|
turn_id TEXT,
|
|
576
|
+
agent_slug TEXT,
|
|
577
|
+
agent_display_name TEXT,
|
|
564
578
|
run_id TEXT,
|
|
565
579
|
ts DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
566
580
|
)
|
|
@@ -587,6 +601,8 @@ export function getDb() {
|
|
|
587
601
|
const oldConvColNames = new Set(oldConvCols.map((column) => column.name));
|
|
588
602
|
const sessionKeySelect = oldConvColNames.has("session_key") ? "session_key" : "'default'";
|
|
589
603
|
const turnIdSelect = oldConvColNames.has("turn_id") ? "turn_id" : "NULL";
|
|
604
|
+
const agentSlugSelect = oldConvColNames.has("agent_slug") ? "agent_slug" : "NULL";
|
|
605
|
+
const agentDisplayNameSelect = oldConvColNames.has("agent_display_name") ? "agent_display_name" : "NULL";
|
|
590
606
|
const runIdSelect = oldConvColNames.has("run_id") ? "run_id" : "NULL";
|
|
591
607
|
db.exec(`
|
|
592
608
|
CREATE TABLE conversation_log (
|
|
@@ -596,13 +612,15 @@ export function getDb() {
|
|
|
596
612
|
source TEXT NOT NULL DEFAULT 'unknown',
|
|
597
613
|
session_key TEXT NOT NULL DEFAULT 'default',
|
|
598
614
|
turn_id TEXT,
|
|
615
|
+
agent_slug TEXT,
|
|
616
|
+
agent_display_name TEXT,
|
|
599
617
|
run_id TEXT,
|
|
600
618
|
ts DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
601
619
|
)
|
|
602
620
|
`);
|
|
603
621
|
db.exec(`
|
|
604
|
-
INSERT INTO conversation_log (role, content, source, session_key, turn_id, run_id, ts)
|
|
605
|
-
SELECT role, content, source, ${sessionKeySelect}, ${turnIdSelect}, ${runIdSelect}, ts FROM conversation_log_old
|
|
622
|
+
INSERT INTO conversation_log (role, content, source, session_key, turn_id, agent_slug, agent_display_name, run_id, ts)
|
|
623
|
+
SELECT role, content, source, ${sessionKeySelect}, ${turnIdSelect}, ${agentSlugSelect}, ${agentDisplayNameSelect}, ${runIdSelect}, ts FROM conversation_log_old
|
|
606
624
|
`);
|
|
607
625
|
db.exec(`DROP TABLE conversation_log_old`);
|
|
608
626
|
}
|
|
@@ -610,14 +628,40 @@ export function getDb() {
|
|
|
610
628
|
db.exec(`
|
|
611
629
|
CREATE TABLE IF NOT EXISTS copilot_sessions (
|
|
612
630
|
session_key TEXT PRIMARY KEY,
|
|
613
|
-
mode TEXT NOT NULL CHECK(mode IN ('default', 'project')),
|
|
631
|
+
mode TEXT NOT NULL CHECK(mode IN ('default', 'project', 'agent')),
|
|
614
632
|
project_root TEXT,
|
|
615
633
|
copilot_session_id TEXT NOT NULL,
|
|
616
634
|
model TEXT,
|
|
617
635
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
618
636
|
)
|
|
619
637
|
`);
|
|
620
|
-
|
|
638
|
+
try {
|
|
639
|
+
db.prepare(`
|
|
640
|
+
INSERT INTO copilot_sessions (session_key, mode, copilot_session_id)
|
|
641
|
+
VALUES ('__mode_probe__', 'agent', '__probe__')
|
|
642
|
+
`).run();
|
|
643
|
+
db.prepare(`DELETE FROM copilot_sessions WHERE session_key = '__mode_probe__'`).run();
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
db.exec(`ALTER TABLE copilot_sessions RENAME TO copilot_sessions_old`);
|
|
647
|
+
db.exec(`
|
|
648
|
+
CREATE TABLE copilot_sessions (
|
|
649
|
+
session_key TEXT PRIMARY KEY,
|
|
650
|
+
mode TEXT NOT NULL CHECK(mode IN ('default', 'project', 'agent')),
|
|
651
|
+
project_root TEXT,
|
|
652
|
+
copilot_session_id TEXT NOT NULL,
|
|
653
|
+
model TEXT,
|
|
654
|
+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
655
|
+
)
|
|
656
|
+
`);
|
|
657
|
+
db.exec(`
|
|
658
|
+
INSERT INTO copilot_sessions (session_key, mode, project_root, copilot_session_id, model, updated_at)
|
|
659
|
+
SELECT session_key, mode, project_root, copilot_session_id, model, updated_at
|
|
660
|
+
FROM copilot_sessions_old
|
|
661
|
+
`);
|
|
662
|
+
db.exec(`DROP TABLE copilot_sessions_old`);
|
|
663
|
+
}
|
|
664
|
+
// Migrate: add metadata columns to conversation_log if not present
|
|
621
665
|
const convCols = db.prepare(`PRAGMA table_info(conversation_log)`).all();
|
|
622
666
|
if (!convCols.some((c) => c.name === 'session_key')) {
|
|
623
667
|
db.exec(`ALTER TABLE conversation_log ADD COLUMN session_key TEXT NOT NULL DEFAULT 'default'`);
|
|
@@ -625,6 +669,12 @@ export function getDb() {
|
|
|
625
669
|
if (!convCols.some((c) => c.name === 'turn_id')) {
|
|
626
670
|
db.exec(`ALTER TABLE conversation_log ADD COLUMN turn_id TEXT`);
|
|
627
671
|
}
|
|
672
|
+
if (!convCols.some((c) => c.name === 'agent_slug')) {
|
|
673
|
+
db.exec(`ALTER TABLE conversation_log ADD COLUMN agent_slug TEXT`);
|
|
674
|
+
}
|
|
675
|
+
if (!convCols.some((c) => c.name === 'agent_display_name')) {
|
|
676
|
+
db.exec(`ALTER TABLE conversation_log ADD COLUMN agent_display_name TEXT`);
|
|
677
|
+
}
|
|
628
678
|
if (!convCols.some((c) => c.name === "run_id")) {
|
|
629
679
|
db.exec(`ALTER TABLE conversation_log ADD COLUMN run_id TEXT`);
|
|
630
680
|
}
|
|
@@ -785,6 +835,27 @@ export function getDb() {
|
|
|
785
835
|
last_recalled_at DATETIME,
|
|
786
836
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
787
837
|
)
|
|
838
|
+
`);
|
|
839
|
+
db.exec(`
|
|
840
|
+
CREATE TABLE IF NOT EXISTS mem_action_items (
|
|
841
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
842
|
+
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
843
|
+
entity_id INTEGER REFERENCES mem_entities(id),
|
|
844
|
+
title TEXT NOT NULL,
|
|
845
|
+
detail TEXT,
|
|
846
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'done', 'dropped', 'snoozed')),
|
|
847
|
+
due_at TEXT,
|
|
848
|
+
snooze_until TEXT,
|
|
849
|
+
source TEXT,
|
|
850
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
851
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
852
|
+
resolved_at TEXT,
|
|
853
|
+
resolution_reason TEXT,
|
|
854
|
+
tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
855
|
+
tier_pinned_at TEXT,
|
|
856
|
+
tier_reason TEXT,
|
|
857
|
+
last_recalled_at TEXT
|
|
858
|
+
)
|
|
788
859
|
`);
|
|
789
860
|
const decisionCols = db.prepare(`PRAGMA table_info(mem_decisions)`).all();
|
|
790
861
|
if (!decisionCols.some((column) => column.name === "superseded_by")) {
|
|
@@ -856,6 +927,13 @@ export function getDb() {
|
|
|
856
927
|
rationale,
|
|
857
928
|
content_rowid='id'
|
|
858
929
|
)
|
|
930
|
+
`);
|
|
931
|
+
db.exec(`
|
|
932
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS mem_action_items_fts USING fts5(
|
|
933
|
+
title,
|
|
934
|
+
detail,
|
|
935
|
+
content_rowid='id'
|
|
936
|
+
)
|
|
859
937
|
`);
|
|
860
938
|
// Sync triggers
|
|
861
939
|
db.exec(`DROP TRIGGER IF EXISTS memories_ai`);
|
|
@@ -867,6 +945,9 @@ export function getDb() {
|
|
|
867
945
|
db.exec(`DROP TRIGGER IF EXISTS mem_decisions_ai`);
|
|
868
946
|
db.exec(`DROP TRIGGER IF EXISTS mem_decisions_ad`);
|
|
869
947
|
db.exec(`DROP TRIGGER IF EXISTS mem_decisions_au`);
|
|
948
|
+
db.exec(`DROP TRIGGER IF EXISTS mem_action_items_ai`);
|
|
949
|
+
db.exec(`DROP TRIGGER IF EXISTS mem_action_items_ad`);
|
|
950
|
+
db.exec(`DROP TRIGGER IF EXISTS mem_action_items_au`);
|
|
870
951
|
db.exec(`
|
|
871
952
|
CREATE TRIGGER memories_ai AFTER INSERT ON memories BEGIN
|
|
872
953
|
INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
|
|
@@ -916,6 +997,24 @@ export function getDb() {
|
|
|
916
997
|
INSERT INTO mem_decisions_fts(rowid, title, rationale)
|
|
917
998
|
VALUES (new.id, new.title, new.rationale);
|
|
918
999
|
END
|
|
1000
|
+
`);
|
|
1001
|
+
db.exec(`
|
|
1002
|
+
CREATE TRIGGER mem_action_items_ai AFTER INSERT ON mem_action_items BEGIN
|
|
1003
|
+
INSERT INTO mem_action_items_fts(rowid, title, detail)
|
|
1004
|
+
VALUES (new.id, new.title, new.detail);
|
|
1005
|
+
END
|
|
1006
|
+
`);
|
|
1007
|
+
db.exec(`
|
|
1008
|
+
CREATE TRIGGER mem_action_items_ad AFTER DELETE ON mem_action_items BEGIN
|
|
1009
|
+
DELETE FROM mem_action_items_fts WHERE rowid = old.id;
|
|
1010
|
+
END
|
|
1011
|
+
`);
|
|
1012
|
+
db.exec(`
|
|
1013
|
+
CREATE TRIGGER mem_action_items_au AFTER UPDATE ON mem_action_items BEGIN
|
|
1014
|
+
DELETE FROM mem_action_items_fts WHERE rowid = old.id;
|
|
1015
|
+
INSERT INTO mem_action_items_fts(rowid, title, detail)
|
|
1016
|
+
VALUES (new.id, new.title, new.detail);
|
|
1017
|
+
END
|
|
919
1018
|
`);
|
|
920
1019
|
// Backfill: check if FTS is in sync by comparing row counts
|
|
921
1020
|
const memCount = db.prepare(`SELECT COUNT(*) as c FROM memories`).get().c;
|
|
@@ -933,6 +1032,11 @@ export function getDb() {
|
|
|
933
1032
|
if (decisionCount > 0 && decisionFtsCount < decisionCount) {
|
|
934
1033
|
db.exec(`INSERT INTO mem_decisions_fts(mem_decisions_fts) VALUES ('rebuild')`);
|
|
935
1034
|
}
|
|
1035
|
+
const actionItemCount = db.prepare(`SELECT COUNT(*) as c FROM mem_action_items`).get().c;
|
|
1036
|
+
const actionItemFtsCount = db.prepare(`SELECT COUNT(*) as c FROM mem_action_items_fts`).get().c;
|
|
1037
|
+
if (actionItemCount > 0 && actionItemFtsCount < actionItemCount) {
|
|
1038
|
+
db.exec(`INSERT INTO mem_action_items_fts(mem_action_items_fts) VALUES ('rebuild')`);
|
|
1039
|
+
}
|
|
936
1040
|
fts5Available = true;
|
|
937
1041
|
}
|
|
938
1042
|
catch {
|
|
@@ -961,10 +1065,10 @@ export function deleteState(key) {
|
|
|
961
1065
|
db.prepare(`DELETE FROM max_state WHERE key = ?`).run(key);
|
|
962
1066
|
}
|
|
963
1067
|
/** Log a conversation turn (user, assistant, or system). */
|
|
964
|
-
export function logConversation(role, content, source, sessionKey = "default",
|
|
1068
|
+
export function logConversation(role, content, source, sessionKey = "default", metadata) {
|
|
965
1069
|
const db = getDb();
|
|
966
|
-
db.prepare(`INSERT INTO conversation_log (role, content, source, session_key, turn_id,
|
|
967
|
-
|
|
1070
|
+
db.prepare(`INSERT INTO conversation_log (role, content, source, session_key, turn_id, agent_slug, agent_display_name, run_id)
|
|
1071
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(role, content, source, sessionKey, metadata?.turnId ?? null, metadata?.agentSlug ?? null, metadata?.agentDisplayName ?? null, getCurrentRunId());
|
|
968
1072
|
// Keep last 1000 entries to support context recovery after session loss
|
|
969
1073
|
logInsertCount++;
|
|
970
1074
|
if (logInsertCount % 50 === 0) {
|
|
@@ -1076,24 +1180,33 @@ export function getSessionMessages(sessionKey, limit, options = {}) {
|
|
|
1076
1180
|
const runId = options.runId ?? getCurrentRunId();
|
|
1077
1181
|
const rows = includeHistorical
|
|
1078
1182
|
? db
|
|
1079
|
-
.prepare(`SELECT id, role, content, ts, turn_id FROM conversation_log
|
|
1183
|
+
.prepare(`SELECT id, role, content, ts, turn_id, agent_slug, agent_display_name FROM conversation_log
|
|
1080
1184
|
WHERE session_key = ? AND role IN ('user', 'assistant', 'agent_completion')
|
|
1081
1185
|
ORDER BY id DESC LIMIT ?`)
|
|
1082
1186
|
.all(sessionKey, effectiveLimit)
|
|
1083
1187
|
: db
|
|
1084
|
-
.prepare(`SELECT id, role, content, ts, turn_id FROM conversation_log
|
|
1188
|
+
.prepare(`SELECT id, role, content, ts, turn_id, agent_slug, agent_display_name FROM conversation_log
|
|
1085
1189
|
WHERE session_key = ? AND run_id = ? AND role IN ('user', 'assistant', 'agent_completion')
|
|
1086
1190
|
ORDER BY id DESC LIMIT ?`)
|
|
1087
1191
|
.all(sessionKey, runId, effectiveLimit);
|
|
1088
1192
|
// Reverse so oldest is first (chronological order for the UI)
|
|
1089
1193
|
rows.reverse();
|
|
1090
|
-
return rows.map((r) =>
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1194
|
+
return rows.map((r) => {
|
|
1195
|
+
const message = {
|
|
1196
|
+
id: r.id,
|
|
1197
|
+
role: r.role === "agent_completion" ? "assistant" : r.role,
|
|
1198
|
+
content: r.content,
|
|
1199
|
+
ts: normalizeSqliteTsToIso(r.ts),
|
|
1200
|
+
turn_id: r.turn_id,
|
|
1201
|
+
};
|
|
1202
|
+
if (r.turn_id)
|
|
1203
|
+
message.turnId = r.turn_id;
|
|
1204
|
+
if (r.agent_slug)
|
|
1205
|
+
message.agentSlug = r.agent_slug;
|
|
1206
|
+
if (r.agent_display_name)
|
|
1207
|
+
message.agentDisplayName = r.agent_display_name;
|
|
1208
|
+
return message;
|
|
1209
|
+
});
|
|
1097
1210
|
}
|
|
1098
1211
|
/**
|
|
1099
1212
|
* Append one event to agent_task_events and return the new event.
|
package/dist/store/db.test.js
CHANGED
|
@@ -58,6 +58,53 @@ test("getDb initializes schema, state helpers, and conversation formatting", asy
|
|
|
58
58
|
dbModule.closeDb();
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
|
+
test("getDb initializes action-item memory schema and FTS shadow", async () => {
|
|
62
|
+
const dbModule = await loadDbModule();
|
|
63
|
+
try {
|
|
64
|
+
const db = dbModule.getDb();
|
|
65
|
+
const tables = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table'`).all();
|
|
66
|
+
const tableNames = new Set(tables.map((row) => row.name));
|
|
67
|
+
assert.equal(tableNames.has("mem_action_items"), true, "expected mem_action_items table");
|
|
68
|
+
assert.equal(tableNames.has("mem_action_items_fts"), true, "expected mem_action_items_fts virtual table");
|
|
69
|
+
const columns = db.prepare(`PRAGMA table_info(mem_action_items)`).all();
|
|
70
|
+
const columnNames = new Set(columns.map((column) => column.name));
|
|
71
|
+
for (const name of [
|
|
72
|
+
"id",
|
|
73
|
+
"scope_id",
|
|
74
|
+
"entity_id",
|
|
75
|
+
"title",
|
|
76
|
+
"detail",
|
|
77
|
+
"status",
|
|
78
|
+
"due_at",
|
|
79
|
+
"snooze_until",
|
|
80
|
+
"source",
|
|
81
|
+
"created_at",
|
|
82
|
+
"updated_at",
|
|
83
|
+
"resolved_at",
|
|
84
|
+
"resolution_reason",
|
|
85
|
+
"tier",
|
|
86
|
+
"tier_pinned_at",
|
|
87
|
+
"tier_reason",
|
|
88
|
+
"last_recalled_at",
|
|
89
|
+
]) {
|
|
90
|
+
assert.equal(columnNames.has(name), true, `expected mem_action_items.${name}`);
|
|
91
|
+
}
|
|
92
|
+
const scope = db.prepare(`SELECT id FROM mem_scopes WHERE slug = 'chapterhouse'`).get();
|
|
93
|
+
const inserted = db.prepare(`
|
|
94
|
+
INSERT INTO mem_action_items (scope_id, title, detail, source)
|
|
95
|
+
VALUES (?, 'Action FTS sentinel', 'Searchable migration reminder', 'test')
|
|
96
|
+
`).run(scope.id);
|
|
97
|
+
const ftsHits = db.prepare(`
|
|
98
|
+
SELECT rowid
|
|
99
|
+
FROM mem_action_items_fts
|
|
100
|
+
WHERE mem_action_items_fts MATCH 'migration'
|
|
101
|
+
`).all();
|
|
102
|
+
assert.equal(ftsHits.some((hit) => hit.rowid === Number(inserted.lastInsertRowid)), true);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
dbModule.closeDb();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
61
108
|
test("getDb migrates legacy conversation_log tables to allow system messages", async () => {
|
|
62
109
|
const seedDb = new Database(dbPath);
|
|
63
110
|
seedDb.exec(`
|
|
@@ -559,12 +606,15 @@ test("getSessionMessages returns empty array for unknown session", async () => {
|
|
|
559
606
|
test("getSessionMessages returns structured messages in chronological order, includes agent completions, excludes system rows, respects limit", async () => {
|
|
560
607
|
const dbModule = await loadDbModule();
|
|
561
608
|
try {
|
|
562
|
-
|
|
609
|
+
dbModule.getDb();
|
|
563
610
|
dbModule.logConversation("user", "hello", "web", "test-session");
|
|
564
611
|
dbModule.logConversation("assistant", "hi there", "web", "test-session");
|
|
565
612
|
dbModule.logConversation("system", "system noise", "worker", "test-session");
|
|
566
|
-
|
|
567
|
-
|
|
613
|
+
dbModule.logConversation("agent_completion", "Done", "background", "test-session", {
|
|
614
|
+
agentSlug: "coder",
|
|
615
|
+
agentDisplayName: "Kaylee",
|
|
616
|
+
turnId: "agent-turn-1",
|
|
617
|
+
});
|
|
568
618
|
dbModule.logConversation("user", "second message", "web", "test-session");
|
|
569
619
|
dbModule.logConversation("user", "from other session", "web", "other-session");
|
|
570
620
|
const all = dbModule.getSessionMessages("test-session");
|
|
@@ -574,14 +624,19 @@ test("getSessionMessages returns structured messages in chronological order, inc
|
|
|
574
624
|
assert.equal(all[1].role, "assistant");
|
|
575
625
|
assert.equal(all[1].content, "hi there");
|
|
576
626
|
assert.equal(all[2].role, "assistant");
|
|
577
|
-
assert.equal(all[2].content, "
|
|
627
|
+
assert.equal(all[2].content, "Done");
|
|
628
|
+
assert.equal(all[2].agentSlug, "coder");
|
|
629
|
+
assert.equal(all[2].agentDisplayName, "Kaylee");
|
|
630
|
+
assert.equal(all[2].turnId, "agent-turn-1");
|
|
578
631
|
assert.equal(all[3].role, "user");
|
|
579
632
|
assert.equal(all[3].content, "second message");
|
|
580
633
|
// Limit clamping
|
|
581
634
|
const limited = dbModule.getSessionMessages("test-session", 2);
|
|
582
635
|
assert.equal(limited.length, 2, "limit=2 returns 2 most recent rows");
|
|
583
636
|
// After reversal, these should be the 2 most-recent renderable rows.
|
|
584
|
-
assert.equal(limited[0].content, "
|
|
637
|
+
assert.equal(limited[0].content, "Done");
|
|
638
|
+
assert.equal(limited[0].agentSlug, "coder");
|
|
639
|
+
assert.equal(limited[0].turnId, "agent-turn-1");
|
|
585
640
|
assert.equal(limited[1].content, "second message");
|
|
586
641
|
// Other session not leaked
|
|
587
642
|
const other = dbModule.getSessionMessages("other-session");
|
|
@@ -605,6 +660,7 @@ test("getSessionMessages returns stable row id and turn_id for hydration reconci
|
|
|
605
660
|
const message = messages[0];
|
|
606
661
|
assert.equal(message.id, Number(result.lastInsertRowid));
|
|
607
662
|
assert.equal(message.turn_id, "turn-stable-1");
|
|
663
|
+
assert.equal(message.turnId, "turn-stable-1");
|
|
608
664
|
}
|
|
609
665
|
finally {
|
|
610
666
|
dbModule.closeDb();
|
package/package.json
CHANGED