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.
Files changed (53) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +6 -3
  3. package/README.zh-CN.md +5 -2
  4. package/dist/cli/index.js +1272 -105
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/dashboard/static/app.js +7 -2
  7. package/dist/index.js +809 -52
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +6 -0
  10. package/dist/maintenance-runner.js +204 -25
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +20 -0
  13. package/dist/sdk.d.ts +1 -1
  14. package/dist/sdk.js +809 -52
  15. package/dist/sdk.js.map +1 -1
  16. package/dist/types.d.ts +38 -1
  17. package/dist/types.js.map +1 -1
  18. package/docs/1.2.7-CONTEXT-CONTINUITY.md +68 -0
  19. package/docs/API_REFERENCE.md +6 -2
  20. package/docs/dev-log/progress.txt +48 -0
  21. package/docs/hooks-architecture.md +2 -1
  22. package/package.json +3 -3
  23. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  24. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  25. package/plugins/copilot/memorix/plugin.json +1 -1
  26. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  27. package/plugins/omp/memorix/extensions/memorix.js +17 -0
  28. package/plugins/omp/memorix/package.json +1 -1
  29. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  30. package/plugins/pi/memorix/extensions/memorix.js +17 -0
  31. package/plugins/pi/memorix/package.json +1 -1
  32. package/src/cli/capability-map.ts +1 -0
  33. package/src/cli/command-guide.ts +10 -0
  34. package/src/cli/commands/agent-integrations.ts +102 -5
  35. package/src/cli/commands/checkpoint.ts +144 -0
  36. package/src/cli/commands/serve-http.ts +14 -3
  37. package/src/cli/commands/setup.ts +44 -0
  38. package/src/cli/index.ts +3 -1
  39. package/src/codegraph/auto-context.ts +51 -5
  40. package/src/dashboard/server.ts +11 -0
  41. package/src/hooks/handler.ts +103 -11
  42. package/src/hooks/normalizer.ts +85 -1
  43. package/src/hooks/types.ts +16 -0
  44. package/src/knowledge/workset.ts +43 -2
  45. package/src/memory/compaction.ts +165 -0
  46. package/src/memory/observations.ts +67 -20
  47. package/src/runtime/maintenance-jobs.ts +84 -4
  48. package/src/runtime/project-maintenance.ts +63 -6
  49. package/src/server/tool-profile.ts +1 -0
  50. package/src/server.ts +94 -0
  51. package/src/store/compaction-checkpoint-store.ts +397 -0
  52. package/src/store/sqlite-db.ts +41 -0
  53. 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
+ }
@@ -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
  // ============================================================