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
package/src/hooks/handler.ts
CHANGED
|
@@ -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
|
|
384
|
-
if (
|
|
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) {
|
package/src/hooks/normalizer.ts
CHANGED
|
@@ -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
|
}
|
package/src/hooks/types.ts
CHANGED
|
@@ -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
|
}
|
package/src/knowledge/workset.ts
CHANGED
|
@@ -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
|
|
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
|
-
&& (
|
|
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 = {
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { CompactionCheckpoint } from '../types.js';
|
|
2
|
+
import type { NormalizedHookInput } from '../hooks/types.js';
|
|
3
|
+
import { countTextTokens, truncateToTokenBudget } from '../compact/token-budget.js';
|
|
4
|
+
import { sanitizeCredentials } from './secret-filter.js';
|
|
5
|
+
import { CompactionCheckpointStore } from '../store/compaction-checkpoint-store.js';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_WORKSET_BUDGET = 420;
|
|
8
|
+
const MIN_WORKSET_BUDGET = 48;
|
|
9
|
+
|
|
10
|
+
export interface CompactionWorkset {
|
|
11
|
+
checkpointId: string;
|
|
12
|
+
text: string;
|
|
13
|
+
tokens: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface BuildCompactionWorksetOptions {
|
|
17
|
+
task?: string;
|
|
18
|
+
maxTokens?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeBudget(value: number | undefined): number {
|
|
22
|
+
if (!Number.isFinite(value) || value == null) return DEFAULT_WORKSET_BUDGET;
|
|
23
|
+
return Math.max(MIN_WORKSET_BUDGET, Math.min(2_000, Math.floor(value)));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sourceLabel(checkpoint: CompactionCheckpoint): string {
|
|
27
|
+
if (checkpoint.captureKind === 'native-summary') return 'native host summary';
|
|
28
|
+
if (checkpoint.captureKind === 'preflight') return 'pre-compact lifecycle marker';
|
|
29
|
+
return 'host lifecycle marker';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Render a small, source-aware continuation packet. It never replays a full
|
|
34
|
+
* transcript and never promotes a native summary into durable knowledge.
|
|
35
|
+
*/
|
|
36
|
+
export function buildCompactionWorkset(
|
|
37
|
+
checkpoint: CompactionCheckpoint,
|
|
38
|
+
options: BuildCompactionWorksetOptions = {},
|
|
39
|
+
): CompactionWorkset {
|
|
40
|
+
const budget = normalizeBudget(options.maxTokens);
|
|
41
|
+
const task = options.task?.trim()
|
|
42
|
+
? truncateToTokenBudget(sanitizeCredentials(options.task.trim()), Math.max(8, Math.floor(budget * 0.25)))
|
|
43
|
+
: '';
|
|
44
|
+
const headerLines = [
|
|
45
|
+
'## Compact Continuation',
|
|
46
|
+
`- Host: ${checkpoint.agent} (${sourceLabel(checkpoint)})`,
|
|
47
|
+
`- Trigger: ${checkpoint.reason}`,
|
|
48
|
+
task ? `- Task now: ${task}` : '',
|
|
49
|
+
'- Current code remains authoritative.',
|
|
50
|
+
].filter(Boolean);
|
|
51
|
+
const header = `${headerLines.join('\n')}\n\n`;
|
|
52
|
+
const fallback = checkpoint.captureKind === 'native-summary'
|
|
53
|
+
? ''
|
|
54
|
+
: 'The host did not expose a native compact summary. Reconstruct only what the current task needs from current code and Memorix evidence.';
|
|
55
|
+
const source = sanitizeCredentials(checkpoint.summary?.trim() || fallback);
|
|
56
|
+
const available = Math.max(0, budget - countTextTokens(header) - 4);
|
|
57
|
+
let excerpt = source ? truncateToTokenBudget(source, available) : '';
|
|
58
|
+
let text = `${header}${excerpt ? `### Host checkpoint\n${excerpt}` : ''}`.trim();
|
|
59
|
+
while (excerpt && countTextTokens(text) > budget) {
|
|
60
|
+
const nextExcerptBudget = Math.max(1, Math.floor(countTextTokens(excerpt) * 0.8));
|
|
61
|
+
excerpt = truncateToTokenBudget(excerpt, nextExcerptBudget);
|
|
62
|
+
text = `${header}${excerpt ? `### Host checkpoint\n${excerpt}` : ''}`.trim();
|
|
63
|
+
}
|
|
64
|
+
if (countTextTokens(text) > budget) text = truncateToTokenBudget(text, budget);
|
|
65
|
+
return {
|
|
66
|
+
checkpointId: checkpoint.id,
|
|
67
|
+
text,
|
|
68
|
+
tokens: countTextTokens(text),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sourceEvent(input: NormalizedHookInput): string {
|
|
73
|
+
const raw = input.raw;
|
|
74
|
+
const event = raw.hook_event_name ?? raw.event ?? raw.type;
|
|
75
|
+
return typeof event === 'string' && event ? event : input.event;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Persist only the host lifecycle evidence available in a normalized hook.
|
|
80
|
+
* The caller owns error handling so an unavailable local DB never blocks a
|
|
81
|
+
* host-native compaction.
|
|
82
|
+
*/
|
|
83
|
+
export async function captureCompactionCheckpoint(
|
|
84
|
+
input: NormalizedHookInput,
|
|
85
|
+
): Promise<CompactionCheckpoint | null> {
|
|
86
|
+
const isCompactResume = input.event === 'session_start'
|
|
87
|
+
&& input.sessionStartReason?.trim().toLowerCase() === 'compact';
|
|
88
|
+
if (input.event !== 'pre_compact' && input.event !== 'post_compact' && !isCompactResume) return null;
|
|
89
|
+
if (!input.sessionId || !input.cwd) return null;
|
|
90
|
+
|
|
91
|
+
const [
|
|
92
|
+
{ detectProject },
|
|
93
|
+
{ getProjectDataDir },
|
|
94
|
+
{ initAliasRegistry, registerAlias },
|
|
95
|
+
] = await Promise.all([
|
|
96
|
+
import('../project/detector.js'),
|
|
97
|
+
import('../store/persistence.js'),
|
|
98
|
+
import('../project/aliases.js'),
|
|
99
|
+
]);
|
|
100
|
+
const project = detectProject(input.cwd);
|
|
101
|
+
if (!project) return null;
|
|
102
|
+
const dataDir = await getProjectDataDir(project.id);
|
|
103
|
+
initAliasRegistry(dataDir);
|
|
104
|
+
const projectId = await registerAlias(project);
|
|
105
|
+
const store = new CompactionCheckpointStore(dataDir);
|
|
106
|
+
|
|
107
|
+
if (input.event === 'pre_compact') {
|
|
108
|
+
return store.recordPreflight({
|
|
109
|
+
projectId,
|
|
110
|
+
sessionId: input.sessionId,
|
|
111
|
+
agent: input.agent,
|
|
112
|
+
reason: input.compaction?.reason,
|
|
113
|
+
sourceEvent: sourceEvent(input),
|
|
114
|
+
transcriptAvailable: Boolean(input.transcriptPath),
|
|
115
|
+
capturedAt: input.timestamp,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return store.complete({
|
|
120
|
+
projectId,
|
|
121
|
+
sessionId: input.sessionId,
|
|
122
|
+
agent: input.agent,
|
|
123
|
+
reason: input.compaction?.reason,
|
|
124
|
+
sourceEvent: sourceEvent(input),
|
|
125
|
+
sourceKey: input.compaction?.sourceKey,
|
|
126
|
+
summary: input.compaction?.summary,
|
|
127
|
+
tokensBefore: input.compaction?.tokensBefore,
|
|
128
|
+
firstKeptEntryId: input.compaction?.firstKeptEntryId,
|
|
129
|
+
details: input.compaction?.details,
|
|
130
|
+
completedAt: input.timestamp,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Take the latest undelivered compact checkpoint for an exact host session.
|
|
136
|
+
* Delivery is opt-in at adapter boundaries; this helper never injects content
|
|
137
|
+
* into a host by itself.
|
|
138
|
+
*/
|
|
139
|
+
export async function consumeCompactionWorkset(
|
|
140
|
+
input: NormalizedHookInput,
|
|
141
|
+
options: BuildCompactionWorksetOptions = {},
|
|
142
|
+
): Promise<CompactionWorkset | null> {
|
|
143
|
+
if (!input.sessionId || !input.cwd) return null;
|
|
144
|
+
const [
|
|
145
|
+
{ detectProject },
|
|
146
|
+
{ getProjectDataDir },
|
|
147
|
+
{ initAliasRegistry, registerAlias },
|
|
148
|
+
] = await Promise.all([
|
|
149
|
+
import('../project/detector.js'),
|
|
150
|
+
import('../store/persistence.js'),
|
|
151
|
+
import('../project/aliases.js'),
|
|
152
|
+
]);
|
|
153
|
+
const project = detectProject(input.cwd);
|
|
154
|
+
if (!project) return null;
|
|
155
|
+
const dataDir = await getProjectDataDir(project.id);
|
|
156
|
+
initAliasRegistry(dataDir);
|
|
157
|
+
const projectId = await registerAlias(project);
|
|
158
|
+
const store = new CompactionCheckpointStore(dataDir);
|
|
159
|
+
const checkpoint = store.findUndelivered(projectId, input.sessionId, input.agent);
|
|
160
|
+
if (!checkpoint) return null;
|
|
161
|
+
const workset = buildCompactionWorkset(checkpoint, options);
|
|
162
|
+
if (!workset.text) return null;
|
|
163
|
+
store.markDelivered(checkpoint.id);
|
|
164
|
+
return workset;
|
|
165
|
+
}
|