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.
- package/CHANGELOG.md +12 -0
- package/README.md +6 -3
- package/README.zh-CN.md +5 -2
- package/dist/cli/index.js +954 -72
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/memcode.js +0 -0
- package/dist/index.js +632 -27
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.js +39 -1
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +12 -0
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +632 -27
- 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 +23 -0
- package/docs/hooks-architecture.md +2 -1
- package/package.json +1 -1
- 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/checkpoint.ts +144 -0
- package/src/cli/index.ts +3 -1
- package/src/codegraph/auto-context.ts +51 -5
- 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/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/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 {
|
|
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
|
|
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?:
|
|
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:
|
|
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');
|
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 = {
|