memorix 1.2.6 → 1.2.7

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 (45) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +6 -3
  3. package/README.zh-CN.md +5 -2
  4. package/dist/cli/index.js +954 -72
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/cli/memcode.js +0 -0
  7. package/dist/index.js +632 -27
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.js +39 -1
  10. package/dist/maintenance-runner.js.map +1 -1
  11. package/dist/memcode-runtime/CHANGELOG.md +12 -0
  12. package/dist/sdk.d.ts +1 -1
  13. package/dist/sdk.js +632 -27
  14. package/dist/sdk.js.map +1 -1
  15. package/dist/types.d.ts +38 -1
  16. package/dist/types.js.map +1 -1
  17. package/docs/1.2.7-CONTEXT-CONTINUITY.md +68 -0
  18. package/docs/API_REFERENCE.md +6 -2
  19. package/docs/dev-log/progress.txt +23 -0
  20. package/docs/hooks-architecture.md +2 -1
  21. package/package.json +1 -1
  22. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  23. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  24. package/plugins/copilot/memorix/plugin.json +1 -1
  25. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  26. package/plugins/omp/memorix/extensions/memorix.js +17 -0
  27. package/plugins/omp/memorix/package.json +1 -1
  28. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  29. package/plugins/pi/memorix/extensions/memorix.js +17 -0
  30. package/plugins/pi/memorix/package.json +1 -1
  31. package/src/cli/capability-map.ts +1 -0
  32. package/src/cli/command-guide.ts +10 -0
  33. package/src/cli/commands/checkpoint.ts +144 -0
  34. package/src/cli/index.ts +3 -1
  35. package/src/codegraph/auto-context.ts +51 -5
  36. package/src/hooks/handler.ts +103 -11
  37. package/src/hooks/normalizer.ts +85 -1
  38. package/src/hooks/types.ts +16 -0
  39. package/src/knowledge/workset.ts +43 -2
  40. package/src/memory/compaction.ts +165 -0
  41. package/src/server/tool-profile.ts +1 -0
  42. package/src/server.ts +94 -0
  43. package/src/store/compaction-checkpoint-store.ts +397 -0
  44. package/src/store/sqlite-db.ts +41 -0
  45. package/src/types.ts +46 -0
package/src/cli/index.ts CHANGED
@@ -1050,6 +1050,7 @@ const main = defineCommand({
1050
1050
  resume: () => import('./commands/resume.js').then(m => m.default),
1051
1051
  explain: () => import('./commands/explain.js').then(m => m.default),
1052
1052
  codegraph: () => import('./commands/codegraph.js').then(m => m.default),
1053
+ checkpoint: () => import('./commands/checkpoint.js').then(m => m.default),
1053
1054
  knowledge: () => import('./commands/knowledge.js').then(m => m.default),
1054
1055
  reasoning: () => import('./commands/reasoning.js').then(m => m.default),
1055
1056
  retention: () => import('./commands/retention.js').then(m => m.default),
@@ -1118,7 +1119,7 @@ const main = defineCommand({
1118
1119
  // Detect by checking if the first CLI arg matches a registered subcommand name.
1119
1120
  const firstArg = process.argv[2];
1120
1121
  const knownSubs = ['ask', 'search', 'remember', 'recent', 'help', 'workbench', 'memcode', 'config',
1121
- 'init', 'setup', 'integrate', 'memory', 'context', 'resume', 'explain', 'codegraph', 'knowledge', 'reasoning', 'retention', 'formation', 'audit', 'transfer', 'skills', 'identity',
1122
+ 'init', 'setup', 'integrate', 'memory', 'context', 'resume', 'explain', 'codegraph', 'checkpoint', 'knowledge', 'reasoning', 'retention', 'formation', 'audit', 'transfer', 'skills', 'identity',
1122
1123
  'session', 'team', 'task', 'message', 'lock', 'handoff', 'poll',
1123
1124
  'receipt',
1124
1125
  'serve', 'serve-http', 'status', 'sync',
@@ -1161,6 +1162,7 @@ const main = defineCommand({
1161
1162
  console.error(' resume Resume prior work with one bounded project brief');
1162
1163
  console.error(' explain Explain where Memorix project context comes from');
1163
1164
  console.error(' codegraph Refresh/status/context-pack for CodeGraph Memory');
1165
+ console.error(' checkpoint Inspect native compact continuity checkpoints');
1164
1166
  console.error(' knowledge Review source-backed knowledge pages and project workflows');
1165
1167
  console.error(' reasoning Store/search decision rationale');
1166
1168
  console.error(' retention Inspect stale/archive status');
@@ -2,7 +2,12 @@ import { existsSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { truncateToTokenBudget } from '../compact/token-budget.js';
4
4
  import { getResolvedConfig } from '../config/resolved-config.js';
5
- import { buildTaskWorkset, type TaskWorkset, type WorksetCaution } from '../knowledge/workset.js';
5
+ import {
6
+ buildTaskWorkset,
7
+ type TaskWorkset,
8
+ type WorksetCaution,
9
+ type WorksetContinuation,
10
+ } from '../knowledge/workset.js';
6
11
  import type { ContextDeliveryTarget } from '../knowledge/context-assembly.js';
7
12
  import { sanitizeCredentials } from '../memory/secret-filter.js';
8
13
  import type { ObservationReader, ProjectInfo } from '../types.js';
@@ -23,7 +28,7 @@ import {
23
28
  } from './project-context.js';
24
29
  import { CodeGraphStore } from './store.js';
25
30
  import { isEligibleForAutomaticDelivery } from '../memory/admission.js';
26
- import { getSessionResumeBrief, type SessionResumeBrief } from '../memory/session.js';
31
+ import { getSessionResumeBrief } from '../memory/session.js';
27
32
  import { initSessionStore } from '../store/session-store.js';
28
33
  import {
29
34
  isContinuationTask,
@@ -56,7 +61,7 @@ export interface AutoProjectContext {
56
61
  refresh: AutoContextRefreshResult;
57
62
  providerQuality: CodeGraphProviderQuality;
58
63
  /** Present only when the caller asked to continue prior work. */
59
- continuation?: SessionResumeBrief;
64
+ continuation?: WorksetContinuation;
60
65
  workset: TaskWorkset;
61
66
  }
62
67
 
@@ -72,6 +77,7 @@ export interface AutoProjectBrief {
72
77
  }
73
78
 
74
79
  const DEFAULT_MAX_AGE_MS = 10 * 60 * 1000;
80
+ const COMPACT_CHECKPOINT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
75
81
 
76
82
  function activeProjectObservations(
77
83
  observations: ProjectContextObservation[],
@@ -134,6 +140,14 @@ export async function buildAutoProjectContext(input: {
134
140
  reader?: ObservationReader;
135
141
  /** Auto detects continuation language; always is used by the explicit resume path. */
136
142
  continuation?: 'auto' | 'always' | 'never';
143
+ /**
144
+ * Suppress a checkpoint already delivered through a host-native channel in
145
+ * this exact session. Other agents and later sessions remain eligible.
146
+ */
147
+ excludeCompactionCheckpointFor?: {
148
+ sessionId: string;
149
+ agent: string;
150
+ };
137
151
  /**
138
152
  * When supplied, a needed refresh is queued instead of running in this
139
153
  * request. MCP and hook callers use this to keep their response path fast.
@@ -294,10 +308,34 @@ export async function buildAutoProjectContext(input: {
294
308
  // Project Context is also used by lightweight callers that have not touched
295
309
  // session APIs yet. Initialize only when continuation was requested so a
296
310
  // normal Workset remains independent of session persistence.
297
- let continuation: SessionResumeBrief | undefined;
311
+ let continuation: WorksetContinuation | undefined;
298
312
  if (continuationRequested) {
299
313
  await initSessionStore(input.dataDir);
300
314
  continuation = await getSessionResumeBrief(input.project.id, task, input.reader);
315
+ const { CompactionCheckpointStore } = await import('../store/compaction-checkpoint-store.js');
316
+ const checkpoint = new CompactionCheckpointStore(input.dataDir).findLatestCompleted(
317
+ input.project.id,
318
+ input.excludeCompactionCheckpointFor
319
+ ? { excludeSession: input.excludeCompactionCheckpointFor }
320
+ : undefined,
321
+ );
322
+ const completedAtMs = checkpoint ? Date.parse(checkpoint.completedAt ?? checkpoint.preCapturedAt) : Number.NaN;
323
+ if (
324
+ checkpoint
325
+ && Number.isFinite(completedAtMs)
326
+ && completedAtMs <= now.getTime()
327
+ && now.getTime() - completedAtMs <= COMPACT_CHECKPOINT_MAX_AGE_MS
328
+ ) {
329
+ continuation.compactCheckpoint = {
330
+ id: checkpoint.id,
331
+ agent: checkpoint.agent,
332
+ captureKind: checkpoint.captureKind === 'native-summary' ? 'native-summary' : 'lifecycle',
333
+ reason: checkpoint.reason,
334
+ ...(checkpoint.completedAt ? { completedAt: checkpoint.completedAt } : {}),
335
+ summary: checkpoint.summary
336
+ ?? 'The host completed context compaction without exposing a native summary. Reconstruct only what the current task needs from current code and durable evidence.',
337
+ };
338
+ }
301
339
  }
302
340
  const workset = await buildTaskWorkset({
303
341
  projectId: input.project.id,
@@ -564,7 +602,7 @@ export function formatAutoProjectContextSummary(context: AutoProjectContext): st
564
602
  );
565
603
 
566
604
  const continuation = context.workset.continuation;
567
- if (continuation?.previousSession || continuation?.memories.length) {
605
+ if (continuation?.previousSession || continuation?.memories.length || continuation?.compactCheckpoint) {
568
606
  lines.push('', 'Resume from prior work');
569
607
  if (continuation.previousSession) {
570
608
  const session = continuation.previousSession;
@@ -584,6 +622,14 @@ export function formatAutoProjectContextSummary(context: AutoProjectContext): st
584
622
  + detail,
585
623
  );
586
624
  }
625
+ if (continuation.compactCheckpoint) {
626
+ const checkpoint = continuation.compactCheckpoint;
627
+ lines.push(
628
+ '- Recent host compact checkpoint ('
629
+ + [checkpoint.agent, checkpoint.captureKind, checkpoint.reason].join(', ')
630
+ + '): ' + compactContinuationText(checkpoint.summary, 36),
631
+ );
632
+ }
587
633
  }
588
634
 
589
635
  return lines.join('\n');
@@ -340,6 +340,17 @@ async function buildHookProjectContext(
340
340
  refresh: 'auto',
341
341
  reader: { projectId: canonicalId },
342
342
  continuation: 'always',
343
+ ...(input.sessionId
344
+ ? {
345
+ // A native compact recovery is delivered once per host session.
346
+ // Do not echo that same checkpoint back through the generic
347
+ // continuation brief on later events from this session.
348
+ excludeCompactionCheckpointFor: {
349
+ sessionId: input.sessionId,
350
+ agent: input.agent,
351
+ },
352
+ }
353
+ : {}),
343
354
  enqueueRefresh: () => import('../runtime/lifecycle.js').then(({ enqueueCodegraphRefresh }) => {
344
355
  enqueueCodegraphRefresh({
345
356
  dataDir,
@@ -365,6 +376,30 @@ async function buildHookProjectContext(
365
376
  }
366
377
  }
367
378
 
379
+ function isCompactSessionStart(input: NormalizedHookInput): boolean {
380
+ return input.event === 'session_start'
381
+ && input.sessionStartReason?.trim().toLowerCase() === 'compact';
382
+ }
383
+
384
+ /**
385
+ * Consume one host-scoped checkpoint at a delivery point the host actually
386
+ * supports. This is intentionally best-effort: a local checkpoint failure
387
+ * must never interfere with the host's native compactor or current turn.
388
+ */
389
+ async function consumeCompactContinuation(
390
+ input: NormalizedHookInput,
391
+ task: string,
392
+ ): Promise<string | undefined> {
393
+ try {
394
+ const { consumeCompactionWorkset } = await import('../memory/compaction.js');
395
+ const workset = await consumeCompactionWorkset(input, { task, maxTokens: 420 });
396
+ return workset?.text;
397
+ } catch (error) {
398
+ console.error('[memorix] compact continuation failed:', (error as Error)?.message ?? error);
399
+ return undefined;
400
+ }
401
+ }
402
+
368
403
  /**
369
404
  * Claude Code ignores SessionStart systemMessage as model context. Its official
370
405
  * UserPromptSubmit output accepts additionalContext, so explicit continuation
@@ -375,20 +410,46 @@ async function buildClaudeContinuationPromptContext(input: NormalizedHookInput):
375
410
  return undefined;
376
411
  }
377
412
 
378
- const { isContinuationTask } = await import('../codegraph/task-lens.js');
379
- if (!isContinuationTask(input.userPrompt)) return undefined;
380
-
381
413
  if (await getHookInjectionMode(input) === 'silent') return undefined;
382
414
 
383
- const context = await buildHookProjectContext(input, input.userPrompt, 'hook-user-prompt');
384
- if (!context?.hasContinuation) return undefined;
415
+ const compactContinuation = await consumeCompactContinuation(input, input.userPrompt);
416
+ if (compactContinuation) {
417
+ return [
418
+ 'Memorix recovered one bounded checkpoint after the host compacted this session.',
419
+ 'Treat it as background context; current code and the user request win.',
420
+ '',
421
+ compactContinuation,
422
+ ].join('\n');
423
+ }
424
+
425
+ const { isContinuationTask, resolveTaskLens } = await import('../codegraph/task-lens.js');
426
+ const continuationRequested = isContinuationTask(input.userPrompt);
427
+ if (continuationRequested) {
428
+ const context = await buildHookProjectContext(input, input.userPrompt, 'hook-user-prompt');
429
+ if (context?.hasContinuation) {
430
+ return [
431
+ 'Memorix prepared a bounded prior-work brief for this explicit continuation request.',
432
+ 'Treat it as background context; current code wins. Use it before broad Git or file-history archaeology.',
433
+ '',
434
+ context.prompt,
435
+ ].join('\n');
436
+ }
437
+ }
438
+
439
+ // Claude Code skills are user-invoked rather than automatically injected.
440
+ // For the narrow cases where prior project context is materially useful,
441
+ // provide a small routing nudge instead of silently adding a broad brief to
442
+ // every prompt. This keeps new feature work quiet while making handoffs and
443
+ // onboarding naturally discover the one-call Autopilot path.
444
+ if (continuationRequested || resolveTaskLens(input.userPrompt).id === 'onboarding') {
445
+ return [
446
+ 'Memorix is available for this handoff or continuation.',
447
+ 'Before broad file or Git exploration, call memorix_project_context with the user\'s actual task.',
448
+ 'If it is not visible yet, use Claude Code tool search for memorix_project_context; use CLI only when MCP discovery is unavailable.',
449
+ ].join(' ');
450
+ }
385
451
 
386
- return [
387
- 'Memorix prepared a bounded prior-work brief for this explicit continuation request.',
388
- 'Treat it as background context; current code wins. Use it before broad Git or file-history archaeology.',
389
- '',
390
- context.prompt,
391
- ].join('\n');
452
+ return undefined;
392
453
  }
393
454
 
394
455
  async function handleSessionStart(input: NormalizedHookInput): Promise<{
@@ -401,6 +462,29 @@ async function handleSessionStart(input: NormalizedHookInput): Promise<{
401
462
  return { observation: null, output: { continue: true } };
402
463
  }
403
464
 
465
+ // Codex officially supports SessionStart additionalContext. Deliver the
466
+ // compact checkpoint there, before the generic startup brief can add noise.
467
+ if (input.agent === 'codex' && isCompactSessionStart(input)) {
468
+ const compactContinuation = await consumeCompactContinuation(
469
+ input,
470
+ 'Continue after the host compacted the current session.',
471
+ );
472
+ if (compactContinuation) {
473
+ return {
474
+ observation: null,
475
+ output: {
476
+ continue: true,
477
+ systemMessage: [
478
+ 'Memorix recovered one bounded checkpoint after the host compacted this session.',
479
+ 'Treat it as background context; current code and the user request win.',
480
+ '',
481
+ compactContinuation,
482
+ ].join('\n'),
483
+ },
484
+ };
485
+ }
486
+ }
487
+
404
488
  let contextSummary = '';
405
489
  if (injectMode === 'full') {
406
490
  const context = await buildHookProjectContext(input, 'Continue the current task.', 'hook-session-start');
@@ -706,6 +790,14 @@ export async function runHook(agentOverride?: string, eventOverride?: string): P
706
790
  }
707
791
 
708
792
  const input = normalizeHookInput(payload);
793
+ try {
794
+ const { captureCompactionCheckpoint } = await import('../memory/compaction.js');
795
+ await captureCompactionCheckpoint(input);
796
+ } catch (checkpointError) {
797
+ // Host-native compaction is authoritative. Checkpoint persistence is an
798
+ // additive recovery layer and must never block it.
799
+ console.error('[memorix] compact checkpoint failed:', (checkpointError as Error)?.message ?? checkpointError);
800
+ }
709
801
  const { observation, output } = await handleHookEvent(input, { deferMaintenance: true });
710
802
 
711
803
  if (observation) {
@@ -6,7 +6,7 @@
6
6
  * but they all communicate via stdin/stdout JSON.
7
7
  */
8
8
 
9
- import type { AgentName, HookEvent, NormalizedHookInput } from './types.js';
9
+ import type { AgentName, HookEvent, NativeCompactionMetadata, NormalizedHookInput } from './types.js';
10
10
 
11
11
  /**
12
12
  * Map agent-specific event names → normalized event names.
@@ -143,6 +143,59 @@ function stringifyValue(value: unknown): string | undefined {
143
143
  }
144
144
  }
145
145
 
146
+ function firstNumber(...values: unknown[]): number | undefined {
147
+ for (const value of values) {
148
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
149
+ if (typeof value === 'string' && value.trim()) {
150
+ const parsed = Number(value);
151
+ if (Number.isFinite(parsed)) return parsed;
152
+ }
153
+ }
154
+ return undefined;
155
+ }
156
+
157
+ function normalizeCompactionReason(value: string | undefined): NativeCompactionMetadata['reason'] {
158
+ const normalized = value?.trim().toLowerCase();
159
+ if (normalized === 'manual' || normalized === 'auto') return normalized;
160
+ return 'unknown';
161
+ }
162
+
163
+ /**
164
+ * Read only documented/explicit compact fields. Unknown host payloads remain
165
+ * lifecycle markers; we never fabricate a native summary from a transcript.
166
+ */
167
+ function normalizeCompactionMetadata(
168
+ payload: Record<string, unknown>,
169
+ ...records: Array<Record<string, unknown> | undefined>
170
+ ): NativeCompactionMetadata | undefined {
171
+ const nestedSources = records.filter((record): record is Record<string, unknown> => Boolean(record));
172
+ const sources = [payload, ...nestedSources];
173
+ const from = (field: string): unknown[] => sources.map((source) => source[field]);
174
+ const fromNested = (field: string): unknown[] => nestedSources.map((source) => source[field]);
175
+ const summary = firstString(...from('summary'), ...from('compaction_summary'));
176
+ // A hook payload's top-level `id` is often a session or transcript ID. Only
177
+ // use an explicit compact ID at the top level, or an entry ID nested under a
178
+ // documented compaction object, for idempotency.
179
+ const sourceKey = firstString(...from('compaction_id'), ...from('entry_id'), ...fromNested('id'));
180
+ const reasonValue = firstString(...from('trigger'), ...from('reason'), ...from('compaction_reason'));
181
+ const tokensBefore = firstNumber(...from('tokensBefore'), ...from('tokens_before'));
182
+ const firstKeptEntryId = firstString(...from('firstKeptEntryId'), ...from('first_kept_entry_id'));
183
+ const details = sources
184
+ .map((source) => asRecord(source.details))
185
+ .find((value): value is Record<string, unknown> => Boolean(value));
186
+ if (!summary && !sourceKey && !tokensBefore && !firstKeptEntryId && !details && !reasonValue) {
187
+ return undefined;
188
+ }
189
+ return {
190
+ ...(reasonValue ? { reason: normalizeCompactionReason(reasonValue) } : {}),
191
+ ...(sourceKey ? { sourceKey } : {}),
192
+ ...(summary ? { summary } : {}),
193
+ ...(tokensBefore !== undefined ? { tokensBefore } : {}),
194
+ ...(firstKeptEntryId ? { firstKeptEntryId } : {}),
195
+ ...(details ? { details } : {}),
196
+ };
197
+ }
198
+
146
199
  /**
147
200
  * Detect which agent sent this hook event based on payload structure.
148
201
  */
@@ -287,6 +340,11 @@ function normalizeClaude(payload: Record<string, unknown>, event: HookEvent): Pa
287
340
  result.aiResponse = assistantMessage;
288
341
  }
289
342
 
343
+ if (event === 'session_start') {
344
+ const source = firstString(payload.source, payload.session_start_reason, payload.reason);
345
+ if (source) result.sessionStartReason = source;
346
+ }
347
+
290
348
  return result;
291
349
  }
292
350
 
@@ -463,6 +521,10 @@ function normalizeGemini(payload: Record<string, unknown>, event: HookEvent): Pa
463
521
  if (event === 'user_prompt') {
464
522
  result.userPrompt = (payload.prompt as string) ?? '';
465
523
  }
524
+ if (event === 'session_start') {
525
+ const source = firstString(payload.source, payload.session_start_reason, payload.reason);
526
+ if (source) result.sessionStartReason = source;
527
+ }
466
528
 
467
529
  return result;
468
530
  }
@@ -514,6 +576,7 @@ function normalizePi(payload: Record<string, unknown>, event: HookEvent): Partia
514
576
  const result: Partial<NormalizedHookInput> = {
515
577
  sessionId: (payload.session_id as string) ?? (payload.sessionId as string) ?? '',
516
578
  cwd: (payload.cwd as string) ?? '',
579
+ transcriptPath: (payload.transcript_path as string) ?? (payload.transcriptPath as string),
517
580
  };
518
581
 
519
582
  const toolName = (payload.tool_name as string) ?? '';
@@ -537,6 +600,10 @@ function normalizePi(payload: Record<string, unknown>, event: HookEvent): Partia
537
600
  if (event === 'post_command') {
538
601
  result.command = (payload.command as string) ?? '';
539
602
  }
603
+ if (event === 'session_start') {
604
+ const source = firstString(payload.source, payload.reason);
605
+ if (source) result.sessionStartReason = source;
606
+ }
540
607
 
541
608
  if (result.toolInput && typeof result.toolInput === 'object') {
542
609
  const filePath =
@@ -604,6 +671,10 @@ function normalizeBridgePayload(payload: Record<string, unknown>, event: HookEve
604
671
  openclawContext?.message,
605
672
  ) ?? stringifyValue(openclawEvent?.message) ?? '';
606
673
  }
674
+ if (event === 'session_start') {
675
+ const source = firstString(payload.source, bridgePayload?.source, bridgeKwargs?.source, openclawEvent?.reason);
676
+ if (source) result.sessionStartReason = source;
677
+ }
607
678
 
608
679
  return result;
609
680
  }
@@ -660,6 +731,18 @@ export function normalizeHookInput(payload: Record<string, unknown>): Normalized
660
731
  agentSpecific = { sessionId: '', cwd: '' };
661
732
  }
662
733
 
734
+ const openclawEvent = asRecord(payload.openclaw_event);
735
+ const genericCompaction = event === 'pre_compact' || event === 'post_compact'
736
+ ? normalizeCompactionMetadata(
737
+ payload,
738
+ asRecord(payload.compaction),
739
+ asRecord(payload.compaction_entry),
740
+ asRecord(payload.compactionEntry),
741
+ asRecord(openclawEvent?.compaction),
742
+ asRecord(openclawEvent?.compaction_entry),
743
+ )
744
+ : undefined;
745
+
663
746
  return {
664
747
  event,
665
748
  agent,
@@ -668,5 +751,6 @@ export function normalizeHookInput(payload: Record<string, unknown>): Normalized
668
751
  cwd: agentSpecific.cwd ?? '',
669
752
  raw: payload,
670
753
  ...agentSpecific,
754
+ ...(genericCompaction ? { compaction: genericCompaction } : {}),
671
755
  };
672
756
  }
@@ -16,6 +16,16 @@ export type HookEvent =
16
16
  | 'session_end'
17
17
  | 'post_response';
18
18
 
19
+ /** Native compact metadata exposed by a host hook or package extension. */
20
+ export interface NativeCompactionMetadata {
21
+ reason?: 'manual' | 'auto' | 'unknown';
22
+ sourceKey?: string;
23
+ summary?: string;
24
+ tokensBefore?: number;
25
+ firstKeptEntryId?: string;
26
+ details?: Record<string, unknown>;
27
+ }
28
+
19
29
  /** Supported agent identifiers */
20
30
  export type AgentName =
21
31
  | 'claude'
@@ -92,6 +102,12 @@ export interface NormalizedHookInput {
92
102
  /** Full transcript path (for pre_compact / session_end) */
93
103
  transcriptPath?: string;
94
104
 
105
+ /** Host-provided SessionStart source (for example, `compact` in Codex). */
106
+ sessionStartReason?: string;
107
+
108
+ /** Native compaction metadata when the host makes it available. */
109
+ compaction?: NativeCompactionMetadata;
110
+
95
111
  /** Raw agent-specific payload (preserved for debugging) */
96
112
  raw: Record<string, unknown>;
97
113
  }
@@ -91,6 +91,15 @@ export interface WorksetContinuation {
91
91
  type: string;
92
92
  detail?: string;
93
93
  }>;
94
+ /** Recent host-native compaction evidence, kept distinct from durable memory. */
95
+ compactCheckpoint?: {
96
+ id: string;
97
+ agent: string;
98
+ captureKind: 'native-summary' | 'lifecycle';
99
+ reason: 'manual' | 'auto' | 'unknown';
100
+ completedAt?: string;
101
+ summary: string;
102
+ };
94
103
  }
95
104
 
96
105
  export interface TaskWorkset {
@@ -353,7 +362,9 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
353
362
  appendLine(lines, 'Task lens: ' + input.lens, maxTokens, omitted, 'lens');
354
363
 
355
364
  const hasContinuation = Boolean(
356
- input.continuation?.previousSession || (input.continuation?.memories.length ?? 0) > 0,
365
+ input.continuation?.previousSession
366
+ || (input.continuation?.memories.length ?? 0) > 0
367
+ || input.continuation?.compactCheckpoint,
357
368
  );
358
369
  if (hasContinuation && input.continuation) {
359
370
  appendLine(lines, '', maxTokens, omitted, 'continuation-heading');
@@ -395,6 +406,24 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
395
406
  },
396
407
  );
397
408
  }
409
+ if (input.continuation.compactCheckpoint) {
410
+ const checkpoint = input.continuation.compactCheckpoint;
411
+ const source = `${checkpoint.agent}, ${checkpoint.captureKind}, ${checkpoint.reason}`;
412
+ appendLine(
413
+ lines,
414
+ '- Recent host compact checkpoint (' + source + '): ' + short(checkpoint.summary, 36),
415
+ maxTokens,
416
+ omitted,
417
+ 'continuation-compact-checkpoint',
418
+ selected,
419
+ {
420
+ kind: 'continuation',
421
+ id: 'compact:' + checkpoint.id,
422
+ reason: 'recent host-native compact lifecycle evidence',
423
+ trust: 'historical',
424
+ },
425
+ );
426
+ }
398
427
  }
399
428
 
400
429
  if (input.cautions.length > 0 || input.cautionMemory.length > 0) {
@@ -699,7 +728,11 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
699
728
  .map(kind => cautions.find(caution => caution.kind === kind)!)
700
729
  .slice(0, 6);
701
730
  const continuation = input.continuation
702
- && (input.continuation.previousSession || input.continuation.memories.length > 0)
731
+ && (
732
+ input.continuation.previousSession
733
+ || input.continuation.memories.length > 0
734
+ || input.continuation.compactCheckpoint
735
+ )
703
736
  ? {
704
737
  ...(input.continuation.previousSession
705
738
  ? {
@@ -714,6 +747,14 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
714
747
  title: short(memory.title, 20),
715
748
  ...(memory.detail ? { detail: short(memory.detail, CONTINUATION_DETAIL_TOKEN_BUDGET) } : {}),
716
749
  })),
750
+ ...(input.continuation.compactCheckpoint
751
+ ? {
752
+ compactCheckpoint: {
753
+ ...input.continuation.compactCheckpoint,
754
+ summary: short(input.continuation.compactCheckpoint.summary, 44),
755
+ },
756
+ }
757
+ : {}),
717
758
  }
718
759
  : undefined;
719
760
  const base = {