chapterhouse 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/server.js +14 -8
- package/dist/api/server.test.js +30 -0
- package/dist/copilot/agents.js +3 -2
- package/dist/copilot/orchestrator.js +48 -10
- package/dist/copilot/orchestrator.test.js +59 -14
- package/dist/copilot/tools.js +183 -7
- package/dist/copilot/tools.memory.test.js +125 -1
- package/dist/copilot/turn-event-log.js +35 -15
- package/dist/copilot/turn-event-log.test.js +31 -0
- 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/eot.js +28 -3
- package/dist/memory/eot.test.js +111 -3
- 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/housekeeping.test.js +26 -26
- package/dist/memory/index.js +1 -0
- 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 +430 -16
- package/dist/store/db.test.js +452 -10
- package/package.json +1 -1
- package/web/dist/assets/index-BTI_m0OE.css +10 -0
- package/web/dist/assets/{index-DmYLALt0.js → index-D4-uRAi6.js} +52 -52
- package/web/dist/assets/index-D4-uRAi6.js.map +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-DhY5yWmC.css +0 -10
- package/web/dist/assets/index-DmYLALt0.js.map +0 -1
|
@@ -45,30 +45,30 @@ test("dedupObservationsPass supersedes similar observations in scope determinist
|
|
|
45
45
|
const team = getScope("team");
|
|
46
46
|
assert.ok(chapterhouse && team);
|
|
47
47
|
const first = recordObservation({
|
|
48
|
-
scope_id:
|
|
48
|
+
scope_id: team.id,
|
|
49
49
|
content: "The worker event stream uses server sent events for live task output.",
|
|
50
50
|
source: "test",
|
|
51
51
|
confidence: 0.4,
|
|
52
52
|
});
|
|
53
53
|
const keeper = recordObservation({
|
|
54
|
-
scope_id:
|
|
54
|
+
scope_id: team.id,
|
|
55
55
|
content: "Worker event streams use server sent events for live task output.",
|
|
56
56
|
source: "test",
|
|
57
57
|
confidence: 0.9,
|
|
58
58
|
});
|
|
59
59
|
const third = recordObservation({
|
|
60
|
-
scope_id:
|
|
60
|
+
scope_id: team.id,
|
|
61
61
|
content: "The worker event stream uses server sent events for live task output today.",
|
|
62
62
|
source: "test",
|
|
63
63
|
confidence: 0.9,
|
|
64
64
|
});
|
|
65
65
|
const otherScope = recordObservation({
|
|
66
|
-
scope_id:
|
|
66
|
+
scope_id: chapterhouse.id,
|
|
67
67
|
content: "Worker event streams use server sent events for live task output.",
|
|
68
68
|
source: "test",
|
|
69
69
|
confidence: 0.1,
|
|
70
70
|
});
|
|
71
|
-
const summary = housekeepingModule.dedupObservationsPass(
|
|
71
|
+
const summary = housekeepingModule.dedupObservationsPass(team.id);
|
|
72
72
|
assert.equal(summary.pass, "dedupObservationsPass");
|
|
73
73
|
assert.equal(summary.examined, 3);
|
|
74
74
|
assert.equal(summary.modified, 2);
|
|
@@ -79,7 +79,7 @@ test("dedupObservationsPass supersedes similar observations in scope determinist
|
|
|
79
79
|
{ id: third.id, superseded_by: keeper.id },
|
|
80
80
|
]);
|
|
81
81
|
assert.equal(db.prepare(`SELECT superseded_by FROM mem_observations WHERE id = ?`).get(otherScope.id).superseded_by, null);
|
|
82
|
-
const second = housekeepingModule.dedupObservationsPass(
|
|
82
|
+
const second = housekeepingModule.dedupObservationsPass(team.id);
|
|
83
83
|
assert.equal(second.modified, 0);
|
|
84
84
|
});
|
|
85
85
|
test("dedupDecisionsPass supersedes similar active decisions within scope and keeps the latest decision", async () => {
|
|
@@ -88,39 +88,39 @@ test("dedupDecisionsPass supersedes similar active decisions within scope and ke
|
|
|
88
88
|
const getScope = getFunction(memoryModule, "getScope");
|
|
89
89
|
const upsertEntity = getFunction(memoryModule, "upsertEntity");
|
|
90
90
|
const recordDecision = getFunction(memoryModule, "recordDecision");
|
|
91
|
-
const chapterhouse = getScope("chapterhouse");
|
|
92
91
|
const team = getScope("team");
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
92
|
+
const infra = getScope("infra");
|
|
93
|
+
assert.ok(team && infra);
|
|
94
|
+
const api = upsertEntity({ scope_id: team.id, kind: "component", name: "api" });
|
|
95
|
+
const web = upsertEntity({ scope_id: team.id, kind: "component", name: "web" });
|
|
96
96
|
const oldDecision = recordDecision({
|
|
97
|
-
scope_id:
|
|
97
|
+
scope_id: team.id,
|
|
98
98
|
entity_id: api.id,
|
|
99
99
|
title: "Use SQLite FTS5 for memory recall",
|
|
100
100
|
rationale: "Initial choice.",
|
|
101
101
|
decided_at: "2026-05-11",
|
|
102
102
|
});
|
|
103
103
|
const keeper = recordDecision({
|
|
104
|
-
scope_id:
|
|
104
|
+
scope_id: team.id,
|
|
105
105
|
entity_id: api.id,
|
|
106
106
|
title: "Use SQLite FTS5 for scoped memory recall",
|
|
107
107
|
rationale: "Latest choice.",
|
|
108
108
|
decided_at: "2026-05-13",
|
|
109
109
|
});
|
|
110
110
|
const otherEntity = recordDecision({
|
|
111
|
-
scope_id:
|
|
111
|
+
scope_id: team.id,
|
|
112
112
|
entity_id: web.id,
|
|
113
113
|
title: "Use SQLite FTS5 for memory recall",
|
|
114
114
|
rationale: "Same title, different entity context, but newer scope-level decision.",
|
|
115
115
|
decided_at: "2026-05-14",
|
|
116
116
|
});
|
|
117
117
|
const otherScope = recordDecision({
|
|
118
|
-
scope_id:
|
|
118
|
+
scope_id: infra.id,
|
|
119
119
|
title: "Use SQLite FTS5 for memory recall",
|
|
120
120
|
rationale: "Same title, different scope.",
|
|
121
121
|
decided_at: "2026-05-14",
|
|
122
122
|
});
|
|
123
|
-
const summary = housekeepingModule.dedupDecisionsPass(
|
|
123
|
+
const summary = housekeepingModule.dedupDecisionsPass(team.id);
|
|
124
124
|
assert.equal(summary.pass, "dedupDecisionsPass");
|
|
125
125
|
assert.equal(summary.examined, 3);
|
|
126
126
|
assert.equal(summary.modified, 2);
|
|
@@ -129,7 +129,7 @@ test("dedupDecisionsPass supersedes similar active decisions within scope and ke
|
|
|
129
129
|
assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(keeper.id).superseded_by, otherEntity.id);
|
|
130
130
|
assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(otherEntity.id).superseded_by, null);
|
|
131
131
|
assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(otherScope.id).superseded_by, null);
|
|
132
|
-
const second = housekeepingModule.dedupDecisionsPass(
|
|
132
|
+
const second = housekeepingModule.dedupDecisionsPass(team.id);
|
|
133
133
|
assert.equal(second.modified, 0);
|
|
134
134
|
});
|
|
135
135
|
test("orphanCleanupPass clears missing observation entity references without touching valid or out-of-scope rows", async () => {
|
|
@@ -225,18 +225,18 @@ test("tieringPass promotes and demotes rows from lifecycle signals and is idempo
|
|
|
225
225
|
const upsertEntity = getFunction(memoryModule, "upsertEntity");
|
|
226
226
|
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
227
227
|
const recordDecision = getFunction(memoryModule, "recordDecision");
|
|
228
|
-
const
|
|
229
|
-
assert.ok(
|
|
230
|
-
const entity = upsertEntity({ scope_id:
|
|
228
|
+
const team = getScope("team");
|
|
229
|
+
assert.ok(team);
|
|
230
|
+
const entity = upsertEntity({ scope_id: team.id, kind: "component", name: "memory", tier: "warm" });
|
|
231
231
|
const referencedObservation = recordObservation({
|
|
232
|
-
scope_id:
|
|
232
|
+
scope_id: team.id,
|
|
233
233
|
entity_id: entity.id,
|
|
234
234
|
content: "Referenced by a recent decision through its entity.",
|
|
235
235
|
source: "test",
|
|
236
236
|
tier: "warm",
|
|
237
237
|
});
|
|
238
238
|
const recentDecision = recordDecision({
|
|
239
|
-
scope_id:
|
|
239
|
+
scope_id: team.id,
|
|
240
240
|
entity_id: entity.id,
|
|
241
241
|
title: "Restate memory tiering decision",
|
|
242
242
|
rationale: "Recent entity-linked decisions keep related observations hot.",
|
|
@@ -244,20 +244,20 @@ test("tieringPass promotes and demotes rows from lifecycle signals and is idempo
|
|
|
244
244
|
tier: "warm",
|
|
245
245
|
});
|
|
246
246
|
const oldHot = recordObservation({
|
|
247
|
-
scope_id:
|
|
247
|
+
scope_id: team.id,
|
|
248
248
|
content: "Old hot row with no recall activity should cool down.",
|
|
249
249
|
source: "test",
|
|
250
250
|
tier: "hot",
|
|
251
251
|
});
|
|
252
252
|
const staleLowConfidence = recordObservation({
|
|
253
|
-
scope_id:
|
|
253
|
+
scope_id: team.id,
|
|
254
254
|
content: "Low confidence stale row should go cold.",
|
|
255
255
|
source: "test",
|
|
256
256
|
tier: "warm",
|
|
257
257
|
confidence: 0.2,
|
|
258
258
|
});
|
|
259
259
|
const archived = recordObservation({
|
|
260
|
-
scope_id:
|
|
260
|
+
scope_id: team.id,
|
|
261
261
|
content: "Archived row should always be cold.",
|
|
262
262
|
source: "test",
|
|
263
263
|
tier: "hot",
|
|
@@ -265,7 +265,7 @@ test("tieringPass promotes and demotes rows from lifecycle signals and is idempo
|
|
|
265
265
|
db.prepare(`UPDATE mem_observations SET created_at = datetime('now', '-45 days') WHERE id = ?`).run(oldHot.id);
|
|
266
266
|
db.prepare(`UPDATE mem_observations SET created_at = datetime('now', '-61 days') WHERE id = ?`).run(staleLowConfidence.id);
|
|
267
267
|
db.prepare(`UPDATE mem_observations SET archived_at = CURRENT_TIMESTAMP WHERE id = ?`).run(archived.id);
|
|
268
|
-
const summary = housekeepingModule.tieringPass(
|
|
268
|
+
const summary = housekeepingModule.tieringPass(team.id);
|
|
269
269
|
assert.equal(summary.pass, "tieringPass");
|
|
270
270
|
assert.equal(summary.modified, 5);
|
|
271
271
|
assert.deepEqual(summary.errors, []);
|
|
@@ -274,7 +274,7 @@ test("tieringPass promotes and demotes rows from lifecycle signals and is idempo
|
|
|
274
274
|
assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(oldHot.id).tier, "warm");
|
|
275
275
|
assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(staleLowConfidence.id).tier, "cold");
|
|
276
276
|
assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(archived.id).tier, "cold");
|
|
277
|
-
const second = housekeepingModule.tieringPass(
|
|
277
|
+
const second = housekeepingModule.tieringPass(team.id);
|
|
278
278
|
assert.equal(second.modified, 0);
|
|
279
279
|
});
|
|
280
280
|
//# sourceMappingURL=housekeeping.test.js.map
|
package/dist/memory/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { completeActionItem, dropActionItem, getActionItem, listActionItems, recordActionItem, snoozeActionItem, } from "./action-items.js";
|
|
1
2
|
export { getActiveScope, inferScopeFromText, setActiveScope } from "./active-scope.js";
|
|
2
3
|
export { recordDecision, getDecision, listDecisions, supersedeDecision } from "./decisions.js";
|
|
3
4
|
export { getEntity, findEntityByName, listEntities, upsertEntity } from "./entities.js";
|
package/dist/memory/recall.js
CHANGED
|
@@ -169,6 +169,63 @@ function recallEntityHits(query, scopeId, options = {}) {
|
|
|
169
169
|
snippet: `${row.name}${row.summary ? ` — ${row.summary}` : ""}`,
|
|
170
170
|
}));
|
|
171
171
|
}
|
|
172
|
+
function recallActionItemHits(query, scopeId, options = {}) {
|
|
173
|
+
if (isFts5Available()) {
|
|
174
|
+
const ftsQuery = quoteFts5QueryTerms(query);
|
|
175
|
+
const rows = getDb().prepare(`
|
|
176
|
+
SELECT
|
|
177
|
+
a.id,
|
|
178
|
+
a.scope_id,
|
|
179
|
+
s.slug AS scope,
|
|
180
|
+
a.title,
|
|
181
|
+
a.detail,
|
|
182
|
+
a.tier,
|
|
183
|
+
-bm25(mem_action_items_fts) * CASE WHEN a.tier = 'hot' THEN ? ELSE 1 END AS score,
|
|
184
|
+
snippet(mem_action_items_fts, 0, '[', ']', '…', 8) || COALESCE(' — ' ||
|
|
185
|
+
snippet(mem_action_items_fts, 1, '[', ']', '…', 12), '') AS snippet
|
|
186
|
+
FROM mem_action_items a
|
|
187
|
+
JOIN mem_scopes s ON s.id = a.scope_id
|
|
188
|
+
JOIN mem_action_items_fts ON mem_action_items_fts.rowid = a.id
|
|
189
|
+
WHERE mem_action_items_fts MATCH ?
|
|
190
|
+
AND (? IS NULL OR a.scope_id = ?)
|
|
191
|
+
AND (? = 1 OR a.tier != 'cold')
|
|
192
|
+
AND a.status IN ('open', 'snoozed')
|
|
193
|
+
ORDER BY score DESC, a.id DESC
|
|
194
|
+
`).all(config.memoryHotRecallBoost, ftsQuery, scopeId ?? null, scopeId ?? null, options.includeCold ? 1 : 0);
|
|
195
|
+
return rows.map((row) => ({
|
|
196
|
+
kind: "action_item",
|
|
197
|
+
id: row.id,
|
|
198
|
+
scopeId: row.scope_id,
|
|
199
|
+
scope: row.scope,
|
|
200
|
+
content: `${row.title}${row.detail ? ` — ${row.detail}` : ""}`,
|
|
201
|
+
score: row.score,
|
|
202
|
+
snippet: row.snippet ?? `${row.title}${row.detail ? ` — ${row.detail}` : ""}`,
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
const pattern = `%${query}%`;
|
|
206
|
+
const rows = getDb().prepare(`
|
|
207
|
+
SELECT a.id, a.scope_id, s.slug AS scope, a.title, a.detail, a.tier
|
|
208
|
+
FROM mem_action_items a
|
|
209
|
+
JOIN mem_scopes s ON s.id = a.scope_id
|
|
210
|
+
WHERE (? IS NULL OR a.scope_id = ?)
|
|
211
|
+
AND (? = 1 OR a.tier != 'cold')
|
|
212
|
+
AND a.status IN ('open', 'snoozed')
|
|
213
|
+
AND (a.title LIKE ? OR COALESCE(a.detail, '') LIKE ?)
|
|
214
|
+
ORDER BY
|
|
215
|
+
CASE WHEN a.due_at IS NULL THEN 1 ELSE 0 END ASC,
|
|
216
|
+
datetime(a.due_at) ASC,
|
|
217
|
+
a.id DESC
|
|
218
|
+
`).all(scopeId ?? null, scopeId ?? null, options.includeCold ? 1 : 0, pattern, pattern);
|
|
219
|
+
return rows.map((row) => ({
|
|
220
|
+
kind: "action_item",
|
|
221
|
+
id: row.id,
|
|
222
|
+
scopeId: row.scope_id,
|
|
223
|
+
scope: row.scope,
|
|
224
|
+
content: `${row.title}${row.detail ? ` — ${row.detail}` : ""}`,
|
|
225
|
+
score: row.tier === "hot" ? config.memoryHotRecallBoost : 1,
|
|
226
|
+
snippet: `${row.title}${row.detail ? ` — ${row.detail}` : ""}`,
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
172
229
|
export function recall(input) {
|
|
173
230
|
const activeScope = getActiveScope();
|
|
174
231
|
const effectiveScopeId = input.scope_id ?? activeScope?.id;
|
|
@@ -180,6 +237,7 @@ export function recall(input) {
|
|
|
180
237
|
...(requestedKinds.has("observation") ? recallObservationHits(input.query, effectiveScopeId, input) : []),
|
|
181
238
|
...(requestedKinds.has("decision") ? recallDecisionHits(input.query, effectiveScopeId, input) : []),
|
|
182
239
|
...(requestedKinds.has("entity") ? recallEntityHits(input.query, effectiveScopeId, input) : []),
|
|
240
|
+
...(requestedKinds.has("action_item") ? recallActionItemHits(input.query, effectiveScopeId, input) : []),
|
|
183
241
|
]
|
|
184
242
|
.sort((a, b) => {
|
|
185
243
|
if (b.score !== a.score)
|
|
@@ -193,6 +251,7 @@ export function recall(input) {
|
|
|
193
251
|
observation: "mem_observations",
|
|
194
252
|
decision: "mem_decisions",
|
|
195
253
|
entity: "mem_entities",
|
|
254
|
+
action_item: "mem_action_items",
|
|
196
255
|
};
|
|
197
256
|
for (const kind of Object.keys(tables)) {
|
|
198
257
|
const ids = hits.filter((hit) => hit.kind === kind).map((hit) => hit.id);
|
|
@@ -96,6 +96,33 @@ test("recall returns active-scope hot-tier entries before ranked FTS hits and re
|
|
|
96
96
|
});
|
|
97
97
|
assert.equal(limited.hits.length, 1);
|
|
98
98
|
});
|
|
99
|
+
test("recall can opt into action item hits without adding them to default kind searches", async () => {
|
|
100
|
+
const { dbModule, memoryModule } = await loadModules();
|
|
101
|
+
dbModule.getDb();
|
|
102
|
+
const getScope = getFunction(memoryModule, "getScope");
|
|
103
|
+
const recordActionItem = getFunction(memoryModule, "recordActionItem");
|
|
104
|
+
const recall = getFunction(memoryModule, "recall");
|
|
105
|
+
const chapterhouse = getScope("chapterhouse");
|
|
106
|
+
assert.ok(chapterhouse);
|
|
107
|
+
const actionItem = recordActionItem({
|
|
108
|
+
scope_id: chapterhouse.id,
|
|
109
|
+
title: "Bellonda disk alert",
|
|
110
|
+
detail: "Next time disk usage exceeds 85 percent, notify infra.",
|
|
111
|
+
source: "test",
|
|
112
|
+
});
|
|
113
|
+
const defaults = recall({ query: "Bellonda disk alert", scope_id: chapterhouse.id, limit: 10 });
|
|
114
|
+
assert.equal(defaults.hits.some((hit) => hit.kind === "action_item" && hit.id === actionItem.id), false);
|
|
115
|
+
const actionOnly = recall({
|
|
116
|
+
query: "Bellonda disk alert",
|
|
117
|
+
scope_id: chapterhouse.id,
|
|
118
|
+
kinds: ["action_item"],
|
|
119
|
+
limit: 10,
|
|
120
|
+
});
|
|
121
|
+
assert.equal(actionOnly.hits.length, 1);
|
|
122
|
+
assert.equal(actionOnly.hits[0]?.kind, "action_item");
|
|
123
|
+
assert.equal(actionOnly.hits[0]?.id, actionItem.id);
|
|
124
|
+
assert.match(actionOnly.hits[0]?.content ?? "", /Bellonda disk alert/);
|
|
125
|
+
});
|
|
99
126
|
test("recall excludes superseded and archived rows by default with opt-in inclusion", async () => {
|
|
100
127
|
const { dbModule, memoryModule } = await loadModules();
|
|
101
128
|
const db = dbModule.getDb();
|
package/dist/memory/tiering.js
CHANGED
|
@@ -6,6 +6,7 @@ const TABLES = {
|
|
|
6
6
|
observation: "mem_observations",
|
|
7
7
|
decision: "mem_decisions",
|
|
8
8
|
entity: "mem_entities",
|
|
9
|
+
action_item: "mem_action_items",
|
|
9
10
|
};
|
|
10
11
|
function dbTable(table) {
|
|
11
12
|
return TABLES[table];
|
|
@@ -71,8 +72,9 @@ export function tieringPass(scopeId) {
|
|
|
71
72
|
SELECT
|
|
72
73
|
(SELECT COUNT(*) FROM mem_observations WHERE scope_id = ?) AS observations,
|
|
73
74
|
(SELECT COUNT(*) FROM mem_decisions WHERE scope_id = ?) AS decisions,
|
|
74
|
-
(SELECT COUNT(*) FROM mem_entities WHERE scope_id = ?) AS entities
|
|
75
|
-
|
|
75
|
+
(SELECT COUNT(*) FROM mem_entities WHERE scope_id = ?) AS entities,
|
|
76
|
+
(SELECT COUNT(*) FROM mem_action_items WHERE scope_id = ?) AS action_items
|
|
77
|
+
`).get(scopeId, scopeId, scopeId, scopeId);
|
|
76
78
|
let modified = 0;
|
|
77
79
|
const tx = db.transaction(() => {
|
|
78
80
|
modified += updateTier(`
|
|
@@ -163,6 +165,34 @@ export function tieringPass(scopeId) {
|
|
|
163
165
|
AND superseded_by IS NULL
|
|
164
166
|
AND last_recalled_at IS NULL
|
|
165
167
|
AND datetime(decided_at) < datetime('now', ?)
|
|
168
|
+
`, [scopeId, `-${config.memoryHotAgeDays} days`]);
|
|
169
|
+
modified += updateTier(`
|
|
170
|
+
UPDATE mem_action_items
|
|
171
|
+
SET tier = 'cold', tier_reason = 'resolved action item'
|
|
172
|
+
WHERE scope_id = ?
|
|
173
|
+
AND tier != 'cold'
|
|
174
|
+
AND tier_pinned_at IS NULL
|
|
175
|
+
AND status IN ('done', 'dropped')
|
|
176
|
+
`, [scopeId]);
|
|
177
|
+
modified += updateTier(`
|
|
178
|
+
UPDATE mem_action_items
|
|
179
|
+
SET tier = 'hot', tier_reason = 'open action item due soon'
|
|
180
|
+
WHERE scope_id = ?
|
|
181
|
+
AND tier != 'hot'
|
|
182
|
+
AND tier_pinned_at IS NULL
|
|
183
|
+
AND status = 'open'
|
|
184
|
+
AND due_at IS NOT NULL
|
|
185
|
+
AND datetime(due_at) <= datetime('now', '+7 days')
|
|
186
|
+
`, [scopeId]);
|
|
187
|
+
modified += updateTier(`
|
|
188
|
+
UPDATE mem_action_items
|
|
189
|
+
SET tier = 'warm', tier_reason = 'hot age threshold without recall'
|
|
190
|
+
WHERE scope_id = ?
|
|
191
|
+
AND tier = 'hot'
|
|
192
|
+
AND tier_pinned_at IS NULL
|
|
193
|
+
AND status = 'open'
|
|
194
|
+
AND last_recalled_at IS NULL
|
|
195
|
+
AND datetime(created_at) < datetime('now', ?)
|
|
166
196
|
`, [scopeId, `-${config.memoryHotAgeDays} days`]);
|
|
167
197
|
modified += updateTier(`
|
|
168
198
|
UPDATE mem_observations
|
|
@@ -184,7 +214,7 @@ export function tieringPass(scopeId) {
|
|
|
184
214
|
`, [scopeId]);
|
|
185
215
|
});
|
|
186
216
|
tx();
|
|
187
|
-
return passSummary("tieringPass", counts.observations + counts.decisions + counts.entities, modified);
|
|
217
|
+
return passSummary("tieringPass", counts.observations + counts.decisions + counts.entities + counts.action_items, modified);
|
|
188
218
|
}
|
|
189
219
|
catch (error) {
|
|
190
220
|
return passSummary("tieringPass", 0, 0, [error instanceof Error ? error.message : String(error)]);
|