memorix 1.2.1 → 1.2.3
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 +23 -0
- package/README.md +14 -2
- package/README.zh-CN.md +14 -2
- package/dist/cli/index.js +15424 -13780
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +1337 -536
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.d.ts +1 -1
- package/dist/maintenance-runner.js +8458 -8087
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +23 -0
- package/dist/sdk.d.ts +7 -2
- package/dist/sdk.js +1365 -542
- package/dist/sdk.js.map +1 -1
- package/dist/types.d.ts +49 -1
- package/dist/types.js.map +1 -1
- package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
- package/docs/API_REFERENCE.md +24 -4
- package/docs/README.md +1 -1
- package/docs/dev-log/progress.txt +101 -11
- package/package.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/src/cli/command-guide.ts +192 -0
- package/src/cli/commands/audit.ts +9 -4
- package/src/cli/commands/cleanup.ts +5 -1
- package/src/cli/commands/codegraph.ts +15 -5
- package/src/cli/commands/context.ts +3 -2
- package/src/cli/commands/doctor.ts +4 -2
- package/src/cli/commands/explain.ts +9 -3
- package/src/cli/commands/handoff.ts +21 -7
- package/src/cli/commands/identity.ts +116 -0
- package/src/cli/commands/ingest-image.ts +5 -3
- package/src/cli/commands/lock.ts +11 -10
- package/src/cli/commands/memory.ts +58 -21
- package/src/cli/commands/message.ts +19 -14
- package/src/cli/commands/operator-shared.ts +98 -3
- package/src/cli/commands/poll.ts +16 -6
- package/src/cli/commands/reasoning.ts +17 -3
- package/src/cli/commands/retention.ts +9 -4
- package/src/cli/commands/serve-http.ts +8 -2
- package/src/cli/commands/session.ts +44 -10
- package/src/cli/commands/skills.ts +10 -5
- package/src/cli/commands/status.ts +4 -3
- package/src/cli/commands/task.ts +26 -17
- package/src/cli/commands/team.ts +14 -10
- package/src/cli/commands/transfer.ts +63 -10
- package/src/cli/identity.ts +89 -0
- package/src/cli/index.ts +96 -19
- package/src/cli/invocation.ts +115 -0
- package/src/cli/tui/chat-service.ts +41 -18
- package/src/cli/tui/data.ts +23 -44
- package/src/cli/tui/operator-context.ts +60 -0
- package/src/cli/tui/session-service.ts +3 -2
- package/src/cli/tui/views/MemoryView.tsx +10 -8
- package/src/codegraph/auto-context.ts +31 -2
- package/src/codegraph/context-pack.ts +1 -0
- package/src/codegraph/project-context.ts +2 -0
- package/src/compact/engine.ts +26 -10
- package/src/compact/index-format.ts +25 -2
- package/src/dashboard/server.ts +46 -9
- package/src/hooks/admission.ts +117 -0
- package/src/hooks/handler.ts +98 -91
- package/src/knowledge/context-assembly.ts +97 -0
- package/src/knowledge/workset.ts +179 -10
- package/src/memory/admission.ts +57 -0
- package/src/memory/consolidation.ts +13 -2
- package/src/memory/disclosure-policy.ts +6 -1
- package/src/memory/export-import.ts +11 -3
- package/src/memory/graph-context.ts +8 -2
- package/src/memory/observations.ts +162 -4
- package/src/memory/quality-audit.ts +2 -0
- package/src/memory/retention.ts +22 -2
- package/src/memory/session.ts +29 -11
- package/src/memory/visibility.ts +80 -0
- package/src/orchestrate/memorix-bridge.ts +38 -0
- package/src/runtime/control-plane-maintenance.ts +1 -0
- package/src/runtime/isolated-maintenance.ts +1 -0
- package/src/runtime/lifecycle.ts +18 -0
- package/src/runtime/maintenance-jobs.ts +1 -0
- package/src/runtime/maintenance-runner.ts +2 -0
- package/src/runtime/project-maintenance.ts +89 -0
- package/src/sdk.ts +35 -5
- package/src/server.ts +267 -83
- package/src/store/orama-store.ts +61 -6
- package/src/store/sqlite-db.ts +23 -1
- package/src/store/sqlite-store.ts +12 -2
- package/src/team/handoff.ts +7 -0
- package/src/types.ts +51 -0
- package/src/wiki/generator.ts +2 -0
package/src/hooks/handler.ts
CHANGED
|
@@ -2,15 +2,21 @@
|
|
|
2
2
|
* Hook Handler
|
|
3
3
|
*
|
|
4
4
|
* Unified entry point for all agent hooks.
|
|
5
|
-
* Architecture: Normalize → Classify →
|
|
5
|
+
* Architecture: Normalize → Classify → Admit → Store → Respond
|
|
6
6
|
*
|
|
7
7
|
* Design principles (inspired by claude-mem + mcp-memory-service):
|
|
8
|
-
* -
|
|
8
|
+
* - Candidate-first: automatic capture is never durable context by default
|
|
9
9
|
* - Tool Taxonomy: declarative policies per tool category
|
|
10
10
|
* - Pattern = classification only: determines observation type, not storage
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
13
14
|
import type { ObservationType } from '../types.js';
|
|
15
|
+
import {
|
|
16
|
+
assessHookAdmission,
|
|
17
|
+
type HookAdmissionDecision,
|
|
18
|
+
type HookCaptureCategory,
|
|
19
|
+
} from './admission.js';
|
|
14
20
|
import { normalizeHookInput } from './normalizer.js';
|
|
15
21
|
import { detectBestPattern, patternToObservationType } from './pattern-detector.js';
|
|
16
22
|
import { isSignificantKnowledge, isRetrievedResult, isTrivialCommand } from './significance-filter.js';
|
|
@@ -35,6 +41,11 @@ const MIN_PROMPT_LENGTH = 20;
|
|
|
35
41
|
/** Max content length (truncate beyond this) */
|
|
36
42
|
const MAX_CONTENT_LENGTH = 4000;
|
|
37
43
|
|
|
44
|
+
function deriveHookActorId(input: NormalizedHookInput): string {
|
|
45
|
+
const material = `${input.agent ?? 'unknown'}\u0000${input.sessionId ?? 'unknown'}`;
|
|
46
|
+
return `hook:${createHash('sha256').update(material).digest('hex').slice(0, 24)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
/** Truly trivial commands — standalone navigation/inspection only */
|
|
39
50
|
const NOISE_COMMANDS = [
|
|
40
51
|
/^(ls|dir|cd|pwd|echo|cat|type|head|tail|wc|which|where|whoami)(\s|$)/i,
|
|
@@ -49,7 +60,7 @@ const NOISE_COMMANDS = [
|
|
|
49
60
|
// ─── Tool Taxonomy ───
|
|
50
61
|
|
|
51
62
|
/** Tool categories for storage policy */
|
|
52
|
-
type ToolCategory =
|
|
63
|
+
type ToolCategory = HookCaptureCategory;
|
|
53
64
|
|
|
54
65
|
/** Storage policy per tool category */
|
|
55
66
|
interface StoragePolicy {
|
|
@@ -233,7 +244,12 @@ function generateTitle(input: NormalizedHookInput, patternType: string): string
|
|
|
233
244
|
return `Activity (${patternType})`;
|
|
234
245
|
}
|
|
235
246
|
|
|
236
|
-
function buildObservation(
|
|
247
|
+
function buildObservation(
|
|
248
|
+
input: NormalizedHookInput,
|
|
249
|
+
content: string,
|
|
250
|
+
category: ToolCategory,
|
|
251
|
+
admission?: Extract<HookAdmissionDecision, { action: 'store' }>,
|
|
252
|
+
) {
|
|
237
253
|
const pattern = detectBestPattern(content);
|
|
238
254
|
const policy = STORAGE_POLICY[category] ?? STORAGE_POLICY.unknown;
|
|
239
255
|
const fallbackType = input.filePath ? 'what-changed' : policy.defaultType;
|
|
@@ -252,6 +268,15 @@ function buildObservation(input: NormalizedHookInput, content: string, category:
|
|
|
252
268
|
],
|
|
253
269
|
concepts: pattern?.matchedKeywords ?? [],
|
|
254
270
|
filesModified: input.filePath ? [input.filePath] : [],
|
|
271
|
+
...(admission ? {
|
|
272
|
+
valueCategory: admission.valueCategory,
|
|
273
|
+
admissionState: admission.admissionState,
|
|
274
|
+
admissionReason: admission.admissionReason,
|
|
275
|
+
// Automatic capture is private until current code can qualify it for
|
|
276
|
+
// shared project delivery. The opaque actor is stable within a hook session.
|
|
277
|
+
visibility: 'personal' as const,
|
|
278
|
+
createdByAgentId: deriveHookActorId(input),
|
|
279
|
+
} : {}),
|
|
255
280
|
};
|
|
256
281
|
}
|
|
257
282
|
|
|
@@ -281,7 +306,9 @@ async function handleSessionStart(input: NormalizedHookInput): Promise<{
|
|
|
281
306
|
const { initMiniSkillStore } = await import('../store/mini-skill-store.js');
|
|
282
307
|
const { initSessionStore } = await import('../store/session-store.js');
|
|
283
308
|
const { initAliasRegistry, registerAlias } = await import('../project/aliases.js');
|
|
309
|
+
const { MaintenanceTargetStore } = await import('../runtime/maintenance-targets.js');
|
|
284
310
|
const { buildAutoProjectContext, formatAutoProjectContextPrompt } = await import('../codegraph/auto-context.js');
|
|
311
|
+
const { filterReadableObservations } = await import('../memory/visibility.js');
|
|
285
312
|
|
|
286
313
|
const rawProject = detectProject(input.cwd || process.cwd());
|
|
287
314
|
if (!rawProject) throw new Error('No .git found');
|
|
@@ -289,10 +316,18 @@ async function handleSessionStart(input: NormalizedHookInput): Promise<{
|
|
|
289
316
|
|
|
290
317
|
initAliasRegistry(dataDir);
|
|
291
318
|
const canonicalId = await registerAlias(rawProject);
|
|
319
|
+
new MaintenanceTargetStore(dataDir).register({
|
|
320
|
+
projectId: canonicalId,
|
|
321
|
+
projectRoot: rawProject.rootPath,
|
|
322
|
+
dataDir,
|
|
323
|
+
});
|
|
292
324
|
await initObservationStore(dataDir);
|
|
293
325
|
await initMiniSkillStore(dataDir);
|
|
294
326
|
await initSessionStore(dataDir);
|
|
295
|
-
const activeObservations =
|
|
327
|
+
const activeObservations = filterReadableObservations(
|
|
328
|
+
await getStore().loadByProject(canonicalId, { status: 'active' }),
|
|
329
|
+
{ projectId: canonicalId },
|
|
330
|
+
);
|
|
296
331
|
const context = await buildAutoProjectContext({
|
|
297
332
|
project: { ...rawProject, id: canonicalId },
|
|
298
333
|
dataDir,
|
|
@@ -306,6 +341,7 @@ async function handleSessionStart(input: NormalizedHookInput): Promise<{
|
|
|
306
341
|
maxFiles: 5_000,
|
|
307
342
|
});
|
|
308
343
|
}),
|
|
344
|
+
deliveryTarget: 'hook-session-start',
|
|
309
345
|
});
|
|
310
346
|
contextSummary = `\n\n${formatAutoProjectContextPrompt(context)}`;
|
|
311
347
|
} catch (sessErr) {
|
|
@@ -352,8 +388,17 @@ async function handleHookEventCore(input: NormalizedHookInput): Promise<{
|
|
|
352
388
|
if (endContent.length < 50) {
|
|
353
389
|
return { observation: null, output: defaultOutput };
|
|
354
390
|
}
|
|
391
|
+
const draft = buildObservation(input, endContent, 'unknown');
|
|
392
|
+
const admission = assessHookAdmission({
|
|
393
|
+
hook: input,
|
|
394
|
+
category: 'unknown',
|
|
395
|
+
content: endContent,
|
|
396
|
+
observationType: draft.type,
|
|
397
|
+
});
|
|
355
398
|
return {
|
|
356
|
-
observation:
|
|
399
|
+
observation: admission.action === 'store'
|
|
400
|
+
? buildObservation(input, endContent, 'unknown', admission)
|
|
401
|
+
: null,
|
|
357
402
|
output: defaultOutput,
|
|
358
403
|
};
|
|
359
404
|
}
|
|
@@ -425,8 +470,17 @@ async function handleHookEventCore(input: NormalizedHookInput): Promise<{
|
|
|
425
470
|
}
|
|
426
471
|
markTriggered(cooldownKey);
|
|
427
472
|
|
|
473
|
+
const draft = buildObservation(input, content, category);
|
|
474
|
+
const admission = assessHookAdmission({
|
|
475
|
+
hook: input,
|
|
476
|
+
category,
|
|
477
|
+
content,
|
|
478
|
+
observationType: draft.type,
|
|
479
|
+
});
|
|
428
480
|
return {
|
|
429
|
-
observation:
|
|
481
|
+
observation: admission.action === 'store'
|
|
482
|
+
? buildObservation(input, content, category, admission)
|
|
483
|
+
: null,
|
|
430
484
|
output: defaultOutput,
|
|
431
485
|
};
|
|
432
486
|
}
|
|
@@ -440,17 +494,24 @@ async function queueCodegraphRefreshForMutation(input: NormalizedHookInput): Pro
|
|
|
440
494
|
{ getProjectDataDir },
|
|
441
495
|
{ initAliasRegistry, registerAlias },
|
|
442
496
|
{ enqueueCodegraphRefresh },
|
|
497
|
+
{ MaintenanceTargetStore },
|
|
443
498
|
] = await Promise.all([
|
|
444
499
|
import('../project/detector.js'),
|
|
445
500
|
import('../store/persistence.js'),
|
|
446
501
|
import('../project/aliases.js'),
|
|
447
502
|
import('../runtime/lifecycle.js'),
|
|
503
|
+
import('../runtime/maintenance-targets.js'),
|
|
448
504
|
]);
|
|
449
505
|
const project = detectProject(input.cwd || process.cwd());
|
|
450
506
|
if (!project) return;
|
|
451
507
|
const dataDir = await getProjectDataDir(project.id);
|
|
452
508
|
initAliasRegistry(dataDir);
|
|
453
509
|
const projectId = await registerAlias(project);
|
|
510
|
+
new MaintenanceTargetStore(dataDir).register({
|
|
511
|
+
projectId,
|
|
512
|
+
projectRoot: project.rootPath,
|
|
513
|
+
dataDir,
|
|
514
|
+
});
|
|
454
515
|
enqueueCodegraphRefresh({
|
|
455
516
|
dataDir,
|
|
456
517
|
projectId,
|
|
@@ -465,11 +526,25 @@ async function queueCodegraphRefreshForMutation(input: NormalizedHookInput): Pro
|
|
|
465
526
|
export async function handleHookEvent(input: NormalizedHookInput): Promise<{
|
|
466
527
|
observation: ReturnType<typeof buildObservation> | null;
|
|
467
528
|
output: HookOutput;
|
|
529
|
+
}>;
|
|
530
|
+
export async function handleHookEvent(input: NormalizedHookInput, options: {
|
|
531
|
+
deferMaintenance?: boolean;
|
|
532
|
+
}): Promise<{
|
|
533
|
+
observation: ReturnType<typeof buildObservation> | null;
|
|
534
|
+
output: HookOutput;
|
|
535
|
+
}>;
|
|
536
|
+
export async function handleHookEvent(input: NormalizedHookInput, options: {
|
|
537
|
+
deferMaintenance?: boolean;
|
|
538
|
+
} = {}): Promise<{
|
|
539
|
+
observation: ReturnType<typeof buildObservation> | null;
|
|
540
|
+
output: HookOutput;
|
|
468
541
|
}> {
|
|
469
542
|
try {
|
|
470
543
|
return await handleHookEventCore(input);
|
|
471
544
|
} finally {
|
|
472
|
-
|
|
545
|
+
// The CLI persists an automatic observation after this function returns.
|
|
546
|
+
// It defers scheduling so a fast worker never scans before that write.
|
|
547
|
+
if (!options.deferMaintenance) await queueCodegraphRefreshForMutation(input);
|
|
473
548
|
}
|
|
474
549
|
}
|
|
475
550
|
|
|
@@ -570,7 +645,7 @@ export async function runHook(agentOverride?: string, eventOverride?: string): P
|
|
|
570
645
|
}
|
|
571
646
|
|
|
572
647
|
const input = normalizeHookInput(payload);
|
|
573
|
-
const { observation, output } = await handleHookEvent(input);
|
|
648
|
+
const { observation, output } = await handleHookEvent(input, { deferMaintenance: true });
|
|
574
649
|
|
|
575
650
|
if (observation) {
|
|
576
651
|
try {
|
|
@@ -581,6 +656,7 @@ export async function runHook(agentOverride?: string, eventOverride?: string): P
|
|
|
581
656
|
const { detectProject } = await import('../project/detector.js');
|
|
582
657
|
const { getProjectDataDir } = await import('../store/persistence.js');
|
|
583
658
|
const { initAliasRegistry, registerAlias } = await import('../project/aliases.js');
|
|
659
|
+
const { MaintenanceTargetStore } = await import('../runtime/maintenance-targets.js');
|
|
584
660
|
|
|
585
661
|
const rawProject = detectProject(input.cwd || process.cwd());
|
|
586
662
|
if (!rawProject) throw new Error('No .git found');
|
|
@@ -590,94 +666,20 @@ export async function runHook(agentOverride?: string, eventOverride?: string): P
|
|
|
590
666
|
initAliasRegistry(dataDir);
|
|
591
667
|
const canonicalId = await registerAlias(rawProject);
|
|
592
668
|
const projectId = canonicalId;
|
|
669
|
+
new MaintenanceTargetStore(dataDir).register({
|
|
670
|
+
projectId,
|
|
671
|
+
projectRoot: rawProject.rootPath,
|
|
672
|
+
dataDir,
|
|
673
|
+
});
|
|
593
674
|
|
|
594
675
|
await initObservationStore(dataDir);
|
|
595
676
|
await initMSStore(dataDir);
|
|
596
677
|
await initSessStore(dataDir);
|
|
597
678
|
await initObservations(dataDir);
|
|
598
679
|
await storeObservation({ ...observation, projectId, sourceDetail: 'hook' });
|
|
599
|
-
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
const { runFormation } = await import('../memory/formation/index.js');
|
|
603
|
-
const formationMode = (process.env.MEMORIX_FORMATION_MODE as 'shadow' | 'active' | 'fallback') || 'shadow';
|
|
604
|
-
const samplingRate = parseFloat(process.env.MEMORIX_FORMATION_HOOKS_SAMPLING_RATE || '0.1');
|
|
605
|
-
const shouldSample = Math.random() < samplingRate;
|
|
606
|
-
|
|
607
|
-
if (shouldSample) {
|
|
608
|
-
const { withFreshIndex } = await import('../memory/freshness.js');
|
|
609
|
-
const { getAllObservations } = await import('../memory/observations.js');
|
|
610
|
-
await withFreshIndex(() => getAllObservations());
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// In hooks, shadow mode by default for performance
|
|
614
|
-
// Sampling rate controls how often we run full resolve (expensive)
|
|
615
|
-
const searchFn = shouldSample
|
|
616
|
-
? async (q: string, limit: number, pid: string) => {
|
|
617
|
-
const { compactSearch, compactDetail } = await import('../compact/engine.js');
|
|
618
|
-
const result = await compactSearch({ query: q, limit, projectId: pid, status: 'active' });
|
|
619
|
-
if (result.entries.length === 0) return [];
|
|
620
|
-
const details = await compactDetail(result.entries.map(e => e.id));
|
|
621
|
-
return details.documents.map((d, i) => ({
|
|
622
|
-
id: Number(d.id.replace('obs-', '')),
|
|
623
|
-
observationId: d.observationId,
|
|
624
|
-
title: d.title,
|
|
625
|
-
narrative: d.narrative,
|
|
626
|
-
facts: d.facts,
|
|
627
|
-
entityName: d.entityName,
|
|
628
|
-
type: d.type,
|
|
629
|
-
score: result.entries[i]?.score ?? 0,
|
|
630
|
-
}));
|
|
631
|
-
}
|
|
632
|
-
: async () => []; // Skip search for speed (shadow mode)
|
|
633
|
-
|
|
634
|
-
const getObsFn = shouldSample
|
|
635
|
-
? (id: number) => {
|
|
636
|
-
const { getObservation } = require('../memory/observations.js');
|
|
637
|
-
const o = getObservation(id);
|
|
638
|
-
if (!o) return null;
|
|
639
|
-
return {
|
|
640
|
-
id: o.id,
|
|
641
|
-
entityName: o.entityName,
|
|
642
|
-
type: o.type,
|
|
643
|
-
title: o.title,
|
|
644
|
-
narrative: o.narrative,
|
|
645
|
-
facts: o.facts,
|
|
646
|
-
topicKey: o.topicKey,
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
: () => null;
|
|
650
|
-
|
|
651
|
-
const getEntityNamesFn = shouldSample
|
|
652
|
-
? () => {
|
|
653
|
-
const { graphManager } = require('../memory/graph.js');
|
|
654
|
-
return graphManager.getEntityNames();
|
|
655
|
-
}
|
|
656
|
-
: () => [];
|
|
657
|
-
|
|
658
|
-
runFormation({
|
|
659
|
-
entityName: observation.entityName,
|
|
660
|
-
type: observation.type,
|
|
661
|
-
title: observation.title,
|
|
662
|
-
narrative: observation.narrative,
|
|
663
|
-
facts: observation.facts,
|
|
664
|
-
projectId,
|
|
665
|
-
source: 'hook' as const,
|
|
666
|
-
}, {
|
|
667
|
-
mode: formationMode,
|
|
668
|
-
useLLM: false,
|
|
669
|
-
minValueScore: 0.3,
|
|
670
|
-
hooksSamplingRate: samplingRate,
|
|
671
|
-
searchMemories: searchFn,
|
|
672
|
-
getObservation: getObsFn,
|
|
673
|
-
getEntityNames: getEntityNamesFn,
|
|
674
|
-
}).catch(() => {});
|
|
675
|
-
} catch { /* Formation is optional — never break hooks */ }
|
|
676
|
-
|
|
677
|
-
// Feedback: tell the agent what was saved
|
|
678
|
-
const emoji = TYPE_EMOJI[observation.type] ?? '[PLAN]';
|
|
679
|
-
output.systemMessage = (output.systemMessage ?? '') +
|
|
680
|
-
`\n${emoji} Memorix saved: ${observation.title} [${observation.type}]`;
|
|
680
|
+
// Automatic capture is deliberately quiet. Candidate state and later
|
|
681
|
+
// qualification are visible through Memorix inspection, not injected as
|
|
682
|
+
// a stream of status messages into the host agent's context.
|
|
681
683
|
} catch (storeErr) {
|
|
682
684
|
// Diagnostic log — hooks must never break the agent, but silent
|
|
683
685
|
// swallow makes end-to-end debugging impossible.
|
|
@@ -685,6 +687,11 @@ export async function runHook(agentOverride?: string, eventOverride?: string): P
|
|
|
685
687
|
}
|
|
686
688
|
}
|
|
687
689
|
|
|
690
|
+
// A candidate must be durable before a Code Memory refresh can qualify it.
|
|
691
|
+
// Keep direct handleHookEvent() backward-compatible, but make the real CLI
|
|
692
|
+
// hook path explicitly capture first and schedule second.
|
|
693
|
+
await queueCodegraphRefreshForMutation(input);
|
|
694
|
+
|
|
688
695
|
// Build hookSpecificOutput — Claude Code only supports it for 3 event types:
|
|
689
696
|
// PreToolUse, UserPromptSubmit, PostToolUse
|
|
690
697
|
// Other events (SessionStart, Stop, PreCompact) must NOT include hookSpecificOutput.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared metadata contract for every bounded context delivery surface.
|
|
3
|
+
* The rendered Workset stays agent-facing; this receipt is for JSON, explain,
|
|
4
|
+
* diagnostics, and future handoff adapters.
|
|
5
|
+
*/
|
|
6
|
+
export type ContextDeliveryTarget =
|
|
7
|
+
| 'project-context'
|
|
8
|
+
| 'context-pack'
|
|
9
|
+
| 'hook-session-start'
|
|
10
|
+
| 'session-handoff';
|
|
11
|
+
|
|
12
|
+
export type ContextCandidateKind =
|
|
13
|
+
| 'task'
|
|
14
|
+
| 'current-fact'
|
|
15
|
+
| 'code-state'
|
|
16
|
+
| 'semantic-code'
|
|
17
|
+
| 'start-here'
|
|
18
|
+
| 'memory'
|
|
19
|
+
| 'claim'
|
|
20
|
+
| 'knowledge-page'
|
|
21
|
+
| 'workflow'
|
|
22
|
+
| 'verification'
|
|
23
|
+
| 'caution';
|
|
24
|
+
|
|
25
|
+
export type ContextCandidateFreshness = 'current' | 'suspect' | 'stale' | 'unknown';
|
|
26
|
+
export type ContextCandidateTrust = 'source-backed' | 'derived' | 'historical';
|
|
27
|
+
|
|
28
|
+
export interface ContextReceiptSelection {
|
|
29
|
+
kind: ContextCandidateKind;
|
|
30
|
+
/** Stable source id when the candidate has one; never raw content. */
|
|
31
|
+
id?: string;
|
|
32
|
+
reason: string;
|
|
33
|
+
freshness?: ContextCandidateFreshness;
|
|
34
|
+
trust?: ContextCandidateTrust;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ContextReceiptOmission {
|
|
38
|
+
kind: ContextCandidateKind;
|
|
39
|
+
reason: 'token-budget' | 'hidden-by-task-lens' | 'unavailable';
|
|
40
|
+
count: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ContextReceipt {
|
|
44
|
+
version: '1.2.2';
|
|
45
|
+
target: ContextDeliveryTarget;
|
|
46
|
+
elapsedMs: number;
|
|
47
|
+
budget: {
|
|
48
|
+
maxTokens: number;
|
|
49
|
+
tokenCount: number;
|
|
50
|
+
};
|
|
51
|
+
selected: ContextReceiptSelection[];
|
|
52
|
+
omitted: ContextReceiptOmission[];
|
|
53
|
+
scheduledActions: string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function displayTarget(target: ContextDeliveryTarget): string {
|
|
57
|
+
switch (target) {
|
|
58
|
+
case 'project-context': return 'Project Context';
|
|
59
|
+
case 'context-pack': return 'Context Pack';
|
|
60
|
+
case 'hook-session-start': return 'SessionStart hook';
|
|
61
|
+
case 'session-handoff': return 'session handoff';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A human-facing diagnostic formatter. Do not append this to agent context. */
|
|
66
|
+
export function formatContextReceipt(receipt: ContextReceipt): string {
|
|
67
|
+
const lines = [
|
|
68
|
+
'Context delivery receipt',
|
|
69
|
+
`- Target: ${displayTarget(receipt.target)}`,
|
|
70
|
+
`- Budget: ${receipt.budget.tokenCount}/${receipt.budget.maxTokens} tokens`,
|
|
71
|
+
`- Assembly: ${receipt.elapsedMs} ms`,
|
|
72
|
+
`- Selected: ${receipt.selected.length} item(s)`,
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
if (receipt.selected.length > 0) {
|
|
76
|
+
lines.push('', 'Selected evidence');
|
|
77
|
+
for (const item of receipt.selected.slice(0, 20)) {
|
|
78
|
+
const id = item.id ? ` ${item.id}` : '';
|
|
79
|
+
const qualifiers = [item.trust, item.freshness].filter(Boolean).join(', ');
|
|
80
|
+
lines.push(`- ${item.kind}${id}: ${item.reason}${qualifiers ? ` (${qualifiers})` : ''}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (receipt.omitted.length > 0) {
|
|
85
|
+
lines.push('', 'Withheld by budget');
|
|
86
|
+
for (const item of receipt.omitted) {
|
|
87
|
+
lines.push(`- ${item.kind}: ${item.count} item(s) (${item.reason})`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (receipt.scheduledActions.length > 0) {
|
|
92
|
+
lines.push('', 'Scheduled follow-up');
|
|
93
|
+
for (const action of receipt.scheduledActions) lines.push(`- ${action}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return lines.join('\n');
|
|
97
|
+
}
|