memorix 1.2.6 → 1.2.8
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/CHANGELOG.md +20 -0
- package/README.md +6 -3
- package/README.zh-CN.md +5 -2
- package/dist/cli/index.js +1272 -105
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/app.js +7 -2
- package/dist/index.js +809 -52
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.d.ts +6 -0
- package/dist/maintenance-runner.js +204 -25
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +20 -0
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +809 -52
- package/dist/sdk.js.map +1 -1
- package/dist/types.d.ts +38 -1
- package/dist/types.js.map +1 -1
- package/docs/1.2.7-CONTEXT-CONTINUITY.md +68 -0
- package/docs/API_REFERENCE.md +6 -2
- package/docs/dev-log/progress.txt +48 -0
- package/docs/hooks-architecture.md +2 -1
- package/package.json +3 -3
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/extensions/memorix.js +17 -0
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/extensions/memorix.js +17 -0
- package/plugins/pi/memorix/package.json +1 -1
- package/src/cli/capability-map.ts +1 -0
- package/src/cli/command-guide.ts +10 -0
- package/src/cli/commands/agent-integrations.ts +102 -5
- package/src/cli/commands/checkpoint.ts +144 -0
- package/src/cli/commands/serve-http.ts +14 -3
- package/src/cli/commands/setup.ts +44 -0
- package/src/cli/index.ts +3 -1
- package/src/codegraph/auto-context.ts +51 -5
- package/src/dashboard/server.ts +11 -0
- package/src/hooks/handler.ts +103 -11
- package/src/hooks/normalizer.ts +85 -1
- package/src/hooks/types.ts +16 -0
- package/src/knowledge/workset.ts +43 -2
- package/src/memory/compaction.ts +165 -0
- package/src/memory/observations.ts +67 -20
- package/src/runtime/maintenance-jobs.ts +84 -4
- package/src/runtime/project-maintenance.ts +63 -6
- package/src/server/tool-profile.ts +1 -0
- package/src/server.ts +94 -0
- package/src/store/compaction-checkpoint-store.ts +397 -0
- package/src/store/sqlite-db.ts +41 -0
- package/src/types.ts +46 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import type {
|
|
3
|
+
CompactionCaptureKind,
|
|
4
|
+
CompactionCheckpoint,
|
|
5
|
+
CompactionCheckpointStatus,
|
|
6
|
+
CompactionReason,
|
|
7
|
+
} from '../types.js';
|
|
8
|
+
import { sanitizeCredentials } from '../memory/secret-filter.js';
|
|
9
|
+
import { getDatabase } from './sqlite-db.js';
|
|
10
|
+
|
|
11
|
+
const MAX_SUMMARY_CHARS = 24_000;
|
|
12
|
+
const MAX_DETAILS_CHARS = 4_000;
|
|
13
|
+
|
|
14
|
+
export interface RecordCompactionPreflightInput {
|
|
15
|
+
projectId: string;
|
|
16
|
+
sessionId: string;
|
|
17
|
+
agent: string;
|
|
18
|
+
reason?: CompactionReason;
|
|
19
|
+
sourceEvent: string;
|
|
20
|
+
transcriptAvailable?: boolean;
|
|
21
|
+
capturedAt?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CompleteCompactionCheckpointInput {
|
|
25
|
+
projectId: string;
|
|
26
|
+
sessionId: string;
|
|
27
|
+
agent: string;
|
|
28
|
+
reason?: CompactionReason;
|
|
29
|
+
sourceEvent: string;
|
|
30
|
+
sourceKey?: string;
|
|
31
|
+
summary?: string;
|
|
32
|
+
tokensBefore?: number;
|
|
33
|
+
firstKeptEntryId?: string;
|
|
34
|
+
details?: Record<string, unknown>;
|
|
35
|
+
completedAt?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ListCompactionCheckpointsOptions {
|
|
39
|
+
projectId: string;
|
|
40
|
+
sessionId?: string;
|
|
41
|
+
agent?: string;
|
|
42
|
+
includeArchived?: boolean;
|
|
43
|
+
limit?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface FindLatestCompletedCheckpointOptions {
|
|
47
|
+
/**
|
|
48
|
+
* A Hook may already have delivered this exact host session's checkpoint via
|
|
49
|
+
* an official native context channel. Do not put it back into its generic
|
|
50
|
+
* Autopilot brief on a later event from the same session.
|
|
51
|
+
*/
|
|
52
|
+
excludeSession?: {
|
|
53
|
+
sessionId: string;
|
|
54
|
+
agent: string;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function optionalText(value: unknown): string | undefined {
|
|
59
|
+
return typeof value === 'string' && value ? value : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function optionalNumber(value: unknown): number | undefined {
|
|
63
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseDetails(value: unknown): Record<string, unknown> | undefined {
|
|
67
|
+
if (typeof value !== 'string' || !value) return undefined;
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(value);
|
|
70
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
71
|
+
? parsed as Record<string, unknown>
|
|
72
|
+
: undefined;
|
|
73
|
+
} catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sanitizeSummary(value: string | undefined): string | undefined {
|
|
79
|
+
if (!value?.trim()) return undefined;
|
|
80
|
+
return sanitizeCredentials(value).slice(0, MAX_SUMMARY_CHARS).trim() || undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sanitizeDetails(value: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
84
|
+
if (!value) return undefined;
|
|
85
|
+
try {
|
|
86
|
+
const sanitized = sanitizeCredentials(JSON.stringify(value));
|
|
87
|
+
if (sanitized.length > MAX_DETAILS_CHARS) return { truncated: true };
|
|
88
|
+
const parsed = JSON.parse(sanitized);
|
|
89
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
90
|
+
? parsed as Record<string, unknown>
|
|
91
|
+
: undefined;
|
|
92
|
+
} catch {
|
|
93
|
+
return { unavailable: true };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sourceKeyFor(input: CompleteCompactionCheckpointInput, summary?: string): string {
|
|
98
|
+
if (input.sourceKey?.trim()) return input.sourceKey.trim().slice(0, 300);
|
|
99
|
+
const material = [
|
|
100
|
+
input.projectId,
|
|
101
|
+
input.sessionId,
|
|
102
|
+
input.agent,
|
|
103
|
+
input.sourceEvent,
|
|
104
|
+
summary ?? '',
|
|
105
|
+
input.tokensBefore ?? '',
|
|
106
|
+
input.firstKeptEntryId ?? '',
|
|
107
|
+
].join('\u0000');
|
|
108
|
+
return `derived:${createHash('sha256').update(material).digest('hex').slice(0, 32)}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function rowToCheckpoint(row: any): CompactionCheckpoint {
|
|
112
|
+
return {
|
|
113
|
+
id: row.id,
|
|
114
|
+
projectId: row.project_id,
|
|
115
|
+
sessionId: row.session_id,
|
|
116
|
+
agent: row.agent,
|
|
117
|
+
phase: row.phase,
|
|
118
|
+
captureKind: row.capture_kind,
|
|
119
|
+
reason: row.reason,
|
|
120
|
+
sourceEvent: row.source_event,
|
|
121
|
+
sourceKey: row.source_key,
|
|
122
|
+
...(optionalText(row.summary) ? { summary: row.summary } : {}),
|
|
123
|
+
...(optionalNumber(row.tokens_before) !== undefined ? { tokensBefore: Number(row.tokens_before) } : {}),
|
|
124
|
+
...(optionalText(row.first_kept_entry_id) ? { firstKeptEntryId: row.first_kept_entry_id } : {}),
|
|
125
|
+
...(parseDetails(row.details_json) ? { details: parseDetails(row.details_json) } : {}),
|
|
126
|
+
transcriptAvailable: Boolean(row.transcript_available),
|
|
127
|
+
status: row.status as CompactionCheckpointStatus,
|
|
128
|
+
preCapturedAt: row.pre_captured_at,
|
|
129
|
+
...(optionalText(row.completed_at) ? { completedAt: row.completed_at } : {}),
|
|
130
|
+
...(optionalText(row.delivered_at) ? { deliveredAt: row.delivered_at } : {}),
|
|
131
|
+
deliveryCount: Number(row.delivery_count ?? 0),
|
|
132
|
+
createdAt: row.created_at,
|
|
133
|
+
updatedAt: row.updated_at,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function safeReason(value: CompactionReason | undefined): CompactionReason {
|
|
138
|
+
return value === 'manual' || value === 'auto' ? value : 'unknown';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function safeLimit(value: number | undefined, fallback: number): number {
|
|
142
|
+
if (!Number.isFinite(value) || value == null) return fallback;
|
|
143
|
+
return Math.max(1, Math.min(500, Math.floor(value)));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* SQLite persistence for host-native compaction lifecycle records.
|
|
148
|
+
*
|
|
149
|
+
* The store deliberately keeps evidence separate from Observations so a
|
|
150
|
+
* compactor's summary cannot silently become durable project knowledge.
|
|
151
|
+
*/
|
|
152
|
+
export class CompactionCheckpointStore {
|
|
153
|
+
private readonly db: any;
|
|
154
|
+
|
|
155
|
+
constructor(dataDir: string) {
|
|
156
|
+
this.db = getDatabase(dataDir);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
recordPreflight(input: RecordCompactionPreflightInput): CompactionCheckpoint {
|
|
160
|
+
const pending = this.db.prepare(`
|
|
161
|
+
SELECT * FROM compaction_checkpoints
|
|
162
|
+
WHERE project_id = ? AND session_id = ? AND agent = ?
|
|
163
|
+
AND phase = 'pre' AND status = 'active'
|
|
164
|
+
ORDER BY pre_captured_at DESC, created_at DESC
|
|
165
|
+
LIMIT 1
|
|
166
|
+
`).get(input.projectId, input.sessionId, input.agent);
|
|
167
|
+
if (pending) return rowToCheckpoint(pending);
|
|
168
|
+
|
|
169
|
+
const now = input.capturedAt ?? new Date().toISOString();
|
|
170
|
+
const checkpoint: CompactionCheckpoint = {
|
|
171
|
+
id: randomUUID(),
|
|
172
|
+
projectId: input.projectId,
|
|
173
|
+
sessionId: input.sessionId,
|
|
174
|
+
agent: input.agent,
|
|
175
|
+
phase: 'pre',
|
|
176
|
+
captureKind: 'preflight',
|
|
177
|
+
reason: safeReason(input.reason),
|
|
178
|
+
sourceEvent: input.sourceEvent,
|
|
179
|
+
sourceKey: `pre:${randomUUID()}`,
|
|
180
|
+
transcriptAvailable: Boolean(input.transcriptAvailable),
|
|
181
|
+
status: 'active',
|
|
182
|
+
preCapturedAt: now,
|
|
183
|
+
deliveryCount: 0,
|
|
184
|
+
createdAt: now,
|
|
185
|
+
updatedAt: now,
|
|
186
|
+
};
|
|
187
|
+
this.insert(checkpoint);
|
|
188
|
+
return checkpoint;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
complete(input: CompleteCompactionCheckpointInput): CompactionCheckpoint {
|
|
192
|
+
const completedAt = input.completedAt ?? new Date().toISOString();
|
|
193
|
+
const summary = sanitizeSummary(input.summary);
|
|
194
|
+
const pending = this.db.prepare(`
|
|
195
|
+
SELECT * FROM compaction_checkpoints
|
|
196
|
+
WHERE project_id = ? AND session_id = ? AND agent = ?
|
|
197
|
+
AND phase = 'pre' AND status = 'active'
|
|
198
|
+
ORDER BY pre_captured_at DESC, created_at DESC
|
|
199
|
+
LIMIT 1
|
|
200
|
+
`).get(input.projectId, input.sessionId, input.agent);
|
|
201
|
+
|
|
202
|
+
// Hosts such as Codex and Claude expose a post-compact lifecycle signal
|
|
203
|
+
// but no event ID. Correlate it with the newest preflight marker so two
|
|
204
|
+
// compactions in one long session remain distinct. With no fresh marker,
|
|
205
|
+
// treat another identical lifecycle signal as a duplicate rather than
|
|
206
|
+
// creating a stream of empty checkpoints.
|
|
207
|
+
if (!input.sourceKey?.trim() && !summary && input.tokensBefore === undefined && !input.firstKeptEntryId && !pending) {
|
|
208
|
+
const priorLifecycle = this.db.prepare(`
|
|
209
|
+
SELECT * FROM compaction_checkpoints
|
|
210
|
+
WHERE project_id = ? AND session_id = ? AND agent = ?
|
|
211
|
+
AND phase = 'complete' AND capture_kind = 'lifecycle'
|
|
212
|
+
AND source_event = ? AND status = 'active'
|
|
213
|
+
ORDER BY completed_at DESC, created_at DESC
|
|
214
|
+
LIMIT 1
|
|
215
|
+
`).get(input.projectId, input.sessionId, input.agent, input.sourceEvent);
|
|
216
|
+
if (priorLifecycle) return rowToCheckpoint(priorLifecycle);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const sourceKey = input.sourceKey?.trim()
|
|
220
|
+
? input.sourceKey.trim().slice(0, 300)
|
|
221
|
+
: pending
|
|
222
|
+
? `lifecycle:${pending.id}`
|
|
223
|
+
: sourceKeyFor(input, summary);
|
|
224
|
+
const existing = this.db.prepare(`
|
|
225
|
+
SELECT * FROM compaction_checkpoints
|
|
226
|
+
WHERE project_id = ? AND source_key = ?
|
|
227
|
+
LIMIT 1
|
|
228
|
+
`).get(input.projectId, sourceKey);
|
|
229
|
+
if (existing) return rowToCheckpoint(existing);
|
|
230
|
+
|
|
231
|
+
const captureKind: CompactionCaptureKind = summary ? 'native-summary' : 'lifecycle';
|
|
232
|
+
const details = sanitizeDetails(input.details);
|
|
233
|
+
|
|
234
|
+
if (pending) {
|
|
235
|
+
const checkpoint = rowToCheckpoint(pending);
|
|
236
|
+
const nextReason = safeReason(input.reason) === 'unknown'
|
|
237
|
+
? checkpoint.reason
|
|
238
|
+
: safeReason(input.reason);
|
|
239
|
+
this.db.prepare(`
|
|
240
|
+
UPDATE compaction_checkpoints
|
|
241
|
+
SET phase = 'complete', capture_kind = ?, reason = ?, source_event = ?, source_key = ?,
|
|
242
|
+
summary = ?, tokens_before = ?, first_kept_entry_id = ?, details_json = ?,
|
|
243
|
+
completed_at = ?, updated_at = ?
|
|
244
|
+
WHERE id = ?
|
|
245
|
+
`).run(
|
|
246
|
+
captureKind,
|
|
247
|
+
nextReason,
|
|
248
|
+
input.sourceEvent,
|
|
249
|
+
sourceKey,
|
|
250
|
+
summary ?? null,
|
|
251
|
+
input.tokensBefore ?? null,
|
|
252
|
+
input.firstKeptEntryId ?? null,
|
|
253
|
+
JSON.stringify(details ?? {}),
|
|
254
|
+
completedAt,
|
|
255
|
+
completedAt,
|
|
256
|
+
checkpoint.id,
|
|
257
|
+
);
|
|
258
|
+
return this.get(checkpoint.id)!;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const checkpoint: CompactionCheckpoint = {
|
|
262
|
+
id: randomUUID(),
|
|
263
|
+
projectId: input.projectId,
|
|
264
|
+
sessionId: input.sessionId,
|
|
265
|
+
agent: input.agent,
|
|
266
|
+
phase: 'complete',
|
|
267
|
+
captureKind,
|
|
268
|
+
reason: safeReason(input.reason),
|
|
269
|
+
sourceEvent: input.sourceEvent,
|
|
270
|
+
sourceKey,
|
|
271
|
+
...(summary ? { summary } : {}),
|
|
272
|
+
...(input.tokensBefore !== undefined ? { tokensBefore: input.tokensBefore } : {}),
|
|
273
|
+
...(input.firstKeptEntryId ? { firstKeptEntryId: input.firstKeptEntryId } : {}),
|
|
274
|
+
...(details ? { details } : {}),
|
|
275
|
+
transcriptAvailable: false,
|
|
276
|
+
status: 'active',
|
|
277
|
+
preCapturedAt: completedAt,
|
|
278
|
+
completedAt,
|
|
279
|
+
deliveryCount: 0,
|
|
280
|
+
createdAt: completedAt,
|
|
281
|
+
updatedAt: completedAt,
|
|
282
|
+
};
|
|
283
|
+
this.insert(checkpoint);
|
|
284
|
+
return checkpoint;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
get(id: string): CompactionCheckpoint | undefined {
|
|
288
|
+
const row = this.db.prepare('SELECT * FROM compaction_checkpoints WHERE id = ?').get(id);
|
|
289
|
+
return row ? rowToCheckpoint(row) : undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
list(options: ListCompactionCheckpointsOptions): CompactionCheckpoint[] {
|
|
293
|
+
const conditions = ['project_id = ?'];
|
|
294
|
+
const values: unknown[] = [options.projectId];
|
|
295
|
+
if (options.sessionId) {
|
|
296
|
+
conditions.push('session_id = ?');
|
|
297
|
+
values.push(options.sessionId);
|
|
298
|
+
}
|
|
299
|
+
if (options.agent) {
|
|
300
|
+
conditions.push('agent = ?');
|
|
301
|
+
values.push(options.agent);
|
|
302
|
+
}
|
|
303
|
+
if (!options.includeArchived) {
|
|
304
|
+
conditions.push("status = 'active'");
|
|
305
|
+
}
|
|
306
|
+
const rows = this.db.prepare(`
|
|
307
|
+
SELECT * FROM compaction_checkpoints
|
|
308
|
+
WHERE ${conditions.join(' AND ')}
|
|
309
|
+
ORDER BY COALESCE(completed_at, pre_captured_at) DESC, created_at DESC
|
|
310
|
+
LIMIT ?
|
|
311
|
+
`).all(...values, safeLimit(options.limit, 20));
|
|
312
|
+
return rows.map(rowToCheckpoint);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
findUndelivered(projectId: string, sessionId: string, agent: string): CompactionCheckpoint | undefined {
|
|
316
|
+
const row = this.db.prepare(`
|
|
317
|
+
SELECT * FROM compaction_checkpoints
|
|
318
|
+
WHERE project_id = ? AND session_id = ? AND agent = ?
|
|
319
|
+
AND phase = 'complete' AND status = 'active' AND delivered_at IS NULL
|
|
320
|
+
ORDER BY completed_at DESC, created_at DESC
|
|
321
|
+
LIMIT 1
|
|
322
|
+
`).get(projectId, sessionId, agent);
|
|
323
|
+
return row ? rowToCheckpoint(row) : undefined;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
findLatestCompleted(
|
|
327
|
+
projectId: string,
|
|
328
|
+
options: FindLatestCompletedCheckpointOptions = {},
|
|
329
|
+
): CompactionCheckpoint | undefined {
|
|
330
|
+
const excludeSession = options.excludeSession;
|
|
331
|
+
const exclusion = excludeSession
|
|
332
|
+
? ' AND NOT (session_id = ? AND agent = ? )'
|
|
333
|
+
: '';
|
|
334
|
+
const values = excludeSession
|
|
335
|
+
? [projectId, excludeSession.sessionId, excludeSession.agent]
|
|
336
|
+
: [projectId];
|
|
337
|
+
const row = this.db.prepare(`
|
|
338
|
+
SELECT * FROM compaction_checkpoints
|
|
339
|
+
WHERE project_id = ? AND phase = 'complete' AND status = 'active'
|
|
340
|
+
${exclusion}
|
|
341
|
+
ORDER BY completed_at DESC, created_at DESC
|
|
342
|
+
LIMIT 1
|
|
343
|
+
`).get(...values);
|
|
344
|
+
return row ? rowToCheckpoint(row) : undefined;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
archive(id: string, archivedAt = new Date().toISOString()): CompactionCheckpoint | undefined {
|
|
348
|
+
const result = this.db.prepare(`
|
|
349
|
+
UPDATE compaction_checkpoints
|
|
350
|
+
SET status = 'archived', updated_at = ?
|
|
351
|
+
WHERE id = ? AND status = 'active'
|
|
352
|
+
`).run(archivedAt, id);
|
|
353
|
+
return Number(result.changes ?? 0) > 0 ? this.get(id) : undefined;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
markDelivered(id: string, deliveredAt = new Date().toISOString()): CompactionCheckpoint | undefined {
|
|
357
|
+
const result = this.db.prepare(`
|
|
358
|
+
UPDATE compaction_checkpoints
|
|
359
|
+
SET delivered_at = ?, delivery_count = delivery_count + 1, updated_at = ?
|
|
360
|
+
WHERE id = ? AND phase = 'complete' AND status = 'active' AND delivered_at IS NULL
|
|
361
|
+
`).run(deliveredAt, deliveredAt, id);
|
|
362
|
+
return Number(result.changes ?? 0) > 0 ? this.get(id) : undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private insert(checkpoint: CompactionCheckpoint): void {
|
|
366
|
+
this.db.prepare(`
|
|
367
|
+
INSERT INTO compaction_checkpoints (
|
|
368
|
+
id, project_id, session_id, agent, phase, capture_kind, reason,
|
|
369
|
+
source_event, source_key, summary, tokens_before, first_kept_entry_id,
|
|
370
|
+
details_json, transcript_available, status, pre_captured_at, completed_at,
|
|
371
|
+
delivered_at, delivery_count, created_at, updated_at
|
|
372
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
373
|
+
`).run(
|
|
374
|
+
checkpoint.id,
|
|
375
|
+
checkpoint.projectId,
|
|
376
|
+
checkpoint.sessionId,
|
|
377
|
+
checkpoint.agent,
|
|
378
|
+
checkpoint.phase,
|
|
379
|
+
checkpoint.captureKind,
|
|
380
|
+
checkpoint.reason,
|
|
381
|
+
checkpoint.sourceEvent,
|
|
382
|
+
checkpoint.sourceKey,
|
|
383
|
+
checkpoint.summary ?? null,
|
|
384
|
+
checkpoint.tokensBefore ?? null,
|
|
385
|
+
checkpoint.firstKeptEntryId ?? null,
|
|
386
|
+
JSON.stringify(checkpoint.details ?? {}),
|
|
387
|
+
checkpoint.transcriptAvailable ? 1 : 0,
|
|
388
|
+
checkpoint.status,
|
|
389
|
+
checkpoint.preCapturedAt,
|
|
390
|
+
checkpoint.completedAt ?? null,
|
|
391
|
+
checkpoint.deliveredAt ?? null,
|
|
392
|
+
checkpoint.deliveryCount,
|
|
393
|
+
checkpoint.createdAt,
|
|
394
|
+
checkpoint.updatedAt,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
package/src/store/sqlite-db.ts
CHANGED
|
@@ -528,6 +528,34 @@ CREATE TABLE IF NOT EXISTS maintenance_targets (
|
|
|
528
528
|
);
|
|
529
529
|
`;
|
|
530
530
|
|
|
531
|
+
// ── 1.2.7 Cross-Agent Compaction Checkpoints ───────────────────────
|
|
532
|
+
|
|
533
|
+
const CREATE_COMPACTION_CHECKPOINTS_TABLE = `
|
|
534
|
+
CREATE TABLE IF NOT EXISTS compaction_checkpoints (
|
|
535
|
+
id TEXT PRIMARY KEY,
|
|
536
|
+
project_id TEXT NOT NULL,
|
|
537
|
+
session_id TEXT NOT NULL,
|
|
538
|
+
agent TEXT NOT NULL,
|
|
539
|
+
phase TEXT NOT NULL,
|
|
540
|
+
capture_kind TEXT NOT NULL,
|
|
541
|
+
reason TEXT NOT NULL DEFAULT 'unknown',
|
|
542
|
+
source_event TEXT NOT NULL,
|
|
543
|
+
source_key TEXT NOT NULL,
|
|
544
|
+
summary TEXT,
|
|
545
|
+
tokens_before INTEGER,
|
|
546
|
+
first_kept_entry_id TEXT,
|
|
547
|
+
details_json TEXT NOT NULL DEFAULT '{}',
|
|
548
|
+
transcript_available INTEGER NOT NULL DEFAULT 0,
|
|
549
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
550
|
+
pre_captured_at TEXT NOT NULL,
|
|
551
|
+
completed_at TEXT,
|
|
552
|
+
delivered_at TEXT,
|
|
553
|
+
delivery_count INTEGER NOT NULL DEFAULT 0,
|
|
554
|
+
created_at TEXT NOT NULL,
|
|
555
|
+
updated_at TEXT NOT NULL
|
|
556
|
+
);
|
|
557
|
+
`;
|
|
558
|
+
|
|
531
559
|
const CREATE_INDEXES = `
|
|
532
560
|
CREATE INDEX IF NOT EXISTS idx_observations_projectId ON observations(projectId);
|
|
533
561
|
CREATE INDEX IF NOT EXISTS idx_observations_topicKey ON observations(projectId, topicKey);
|
|
@@ -574,6 +602,9 @@ CREATE INDEX IF NOT EXISTS idx_knowledge_workflow_runs_project_workflow ON knowl
|
|
|
574
602
|
CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_ready ON maintenance_jobs(status, run_after);
|
|
575
603
|
CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_project ON maintenance_jobs(project_id, status, run_after);
|
|
576
604
|
CREATE INDEX IF NOT EXISTS idx_maintenance_targets_updated ON maintenance_targets(updated_at);
|
|
605
|
+
CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_project_recent ON compaction_checkpoints(project_id, status, completed_at DESC, pre_captured_at DESC);
|
|
606
|
+
CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_session_pending ON compaction_checkpoints(project_id, session_id, agent, phase, status, pre_captured_at DESC);
|
|
607
|
+
CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_source ON compaction_checkpoints(project_id, source_key);
|
|
577
608
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_maintenance_jobs_active_dedupe
|
|
578
609
|
ON maintenance_jobs(project_id, kind, dedupe_key)
|
|
579
610
|
WHERE status IN ('pending', 'running', 'retry');
|
|
@@ -665,6 +696,15 @@ const SCHEMA_MIGRATIONS: SchemaMigration[] = [
|
|
|
665
696
|
db.exec('CREATE INDEX IF NOT EXISTS idx_knowledge_workflow_runs_project_workflow ON knowledge_workflow_runs(projectId, workflowId, startedAt DESC)');
|
|
666
697
|
},
|
|
667
698
|
},
|
|
699
|
+
{
|
|
700
|
+
id: '1.2.7-compaction-checkpoints',
|
|
701
|
+
apply: (db) => {
|
|
702
|
+
db.exec(CREATE_COMPACTION_CHECKPOINTS_TABLE);
|
|
703
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_project_recent ON compaction_checkpoints(project_id, status, completed_at DESC, pre_captured_at DESC)');
|
|
704
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_session_pending ON compaction_checkpoints(project_id, session_id, agent, phase, status, pre_captured_at DESC)');
|
|
705
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_source ON compaction_checkpoints(project_id, source_key)');
|
|
706
|
+
},
|
|
707
|
+
},
|
|
668
708
|
];
|
|
669
709
|
|
|
670
710
|
function applySchemaMigrations(db: any): void {
|
|
@@ -732,6 +772,7 @@ export function getDatabase(dataDir: string): any {
|
|
|
732
772
|
db.exec(CREATE_OBSERVATION_CODE_REFS_TABLE);
|
|
733
773
|
db.exec(CREATE_MAINTENANCE_JOBS_TABLE);
|
|
734
774
|
db.exec(CREATE_MAINTENANCE_TARGETS_TABLE);
|
|
775
|
+
db.exec(CREATE_COMPACTION_CHECKPOINTS_TABLE);
|
|
735
776
|
|
|
736
777
|
// Phase 3a migration: add sourceSnapshot + updatedAt to mini_skills
|
|
737
778
|
// Idempotent — ALTER TABLE ADD COLUMN throws if column already exists
|
package/src/types.ts
CHANGED
|
@@ -189,6 +189,52 @@ export interface Session {
|
|
|
189
189
|
agent?: string;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// ============================================================
|
|
193
|
+
// Cross-Agent Compaction Continuity
|
|
194
|
+
// ============================================================
|
|
195
|
+
|
|
196
|
+
/** Whether a checkpoint is still waiting for host compaction or has completed. */
|
|
197
|
+
export type CompactionCheckpointPhase = 'pre' | 'complete';
|
|
198
|
+
|
|
199
|
+
/** Native compaction reason when the host exposes it. */
|
|
200
|
+
export type CompactionReason = 'manual' | 'auto' | 'unknown';
|
|
201
|
+
|
|
202
|
+
/** How much first-party compaction evidence the host actually exposed. */
|
|
203
|
+
export type CompactionCaptureKind = 'preflight' | 'lifecycle' | 'native-summary';
|
|
204
|
+
|
|
205
|
+
/** Lifecycle state for a persisted compaction checkpoint. */
|
|
206
|
+
export type CompactionCheckpointStatus = 'active' | 'archived';
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* A source-aware record of one host-native context compaction.
|
|
210
|
+
*
|
|
211
|
+
* This is intentionally not an Observation: a host summary is evidence for
|
|
212
|
+
* continuation, not automatically durable project truth.
|
|
213
|
+
*/
|
|
214
|
+
export interface CompactionCheckpoint {
|
|
215
|
+
id: string;
|
|
216
|
+
projectId: string;
|
|
217
|
+
sessionId: string;
|
|
218
|
+
agent: string;
|
|
219
|
+
phase: CompactionCheckpointPhase;
|
|
220
|
+
captureKind: CompactionCaptureKind;
|
|
221
|
+
reason: CompactionReason;
|
|
222
|
+
sourceEvent: string;
|
|
223
|
+
sourceKey: string;
|
|
224
|
+
summary?: string;
|
|
225
|
+
tokensBefore?: number;
|
|
226
|
+
firstKeptEntryId?: string;
|
|
227
|
+
details?: Record<string, unknown>;
|
|
228
|
+
transcriptAvailable: boolean;
|
|
229
|
+
status: CompactionCheckpointStatus;
|
|
230
|
+
preCapturedAt: string;
|
|
231
|
+
completedAt?: string;
|
|
232
|
+
deliveredAt?: string;
|
|
233
|
+
deliveryCount: number;
|
|
234
|
+
createdAt: string;
|
|
235
|
+
updatedAt: string;
|
|
236
|
+
}
|
|
237
|
+
|
|
192
238
|
// ============================================================
|
|
193
239
|
// Compact Engine (adopted from claude-mem 3-layer workflow)
|
|
194
240
|
// ============================================================
|