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.
Files changed (90) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +14 -2
  3. package/README.zh-CN.md +14 -2
  4. package/dist/cli/index.js +15424 -13780
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/index.js +1337 -536
  7. package/dist/index.js.map +1 -1
  8. package/dist/maintenance-runner.d.ts +1 -1
  9. package/dist/maintenance-runner.js +8458 -8087
  10. package/dist/maintenance-runner.js.map +1 -1
  11. package/dist/memcode-runtime/CHANGELOG.md +23 -0
  12. package/dist/sdk.d.ts +7 -2
  13. package/dist/sdk.js +1365 -542
  14. package/dist/sdk.js.map +1 -1
  15. package/dist/types.d.ts +49 -1
  16. package/dist/types.js.map +1 -1
  17. package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
  18. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
  19. package/docs/API_REFERENCE.md +24 -4
  20. package/docs/README.md +1 -1
  21. package/docs/dev-log/progress.txt +101 -11
  22. package/package.json +1 -1
  23. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  24. package/src/cli/command-guide.ts +192 -0
  25. package/src/cli/commands/audit.ts +9 -4
  26. package/src/cli/commands/cleanup.ts +5 -1
  27. package/src/cli/commands/codegraph.ts +15 -5
  28. package/src/cli/commands/context.ts +3 -2
  29. package/src/cli/commands/doctor.ts +4 -2
  30. package/src/cli/commands/explain.ts +9 -3
  31. package/src/cli/commands/handoff.ts +21 -7
  32. package/src/cli/commands/identity.ts +116 -0
  33. package/src/cli/commands/ingest-image.ts +5 -3
  34. package/src/cli/commands/lock.ts +11 -10
  35. package/src/cli/commands/memory.ts +58 -21
  36. package/src/cli/commands/message.ts +19 -14
  37. package/src/cli/commands/operator-shared.ts +98 -3
  38. package/src/cli/commands/poll.ts +16 -6
  39. package/src/cli/commands/reasoning.ts +17 -3
  40. package/src/cli/commands/retention.ts +9 -4
  41. package/src/cli/commands/serve-http.ts +8 -2
  42. package/src/cli/commands/session.ts +44 -10
  43. package/src/cli/commands/skills.ts +10 -5
  44. package/src/cli/commands/status.ts +4 -3
  45. package/src/cli/commands/task.ts +26 -17
  46. package/src/cli/commands/team.ts +14 -10
  47. package/src/cli/commands/transfer.ts +63 -10
  48. package/src/cli/identity.ts +89 -0
  49. package/src/cli/index.ts +96 -19
  50. package/src/cli/invocation.ts +115 -0
  51. package/src/cli/tui/chat-service.ts +41 -18
  52. package/src/cli/tui/data.ts +23 -44
  53. package/src/cli/tui/operator-context.ts +60 -0
  54. package/src/cli/tui/session-service.ts +3 -2
  55. package/src/cli/tui/views/MemoryView.tsx +10 -8
  56. package/src/codegraph/auto-context.ts +31 -2
  57. package/src/codegraph/context-pack.ts +1 -0
  58. package/src/codegraph/project-context.ts +2 -0
  59. package/src/compact/engine.ts +26 -10
  60. package/src/compact/index-format.ts +25 -2
  61. package/src/dashboard/server.ts +46 -9
  62. package/src/hooks/admission.ts +117 -0
  63. package/src/hooks/handler.ts +98 -91
  64. package/src/knowledge/context-assembly.ts +97 -0
  65. package/src/knowledge/workset.ts +179 -10
  66. package/src/memory/admission.ts +57 -0
  67. package/src/memory/consolidation.ts +13 -2
  68. package/src/memory/disclosure-policy.ts +6 -1
  69. package/src/memory/export-import.ts +11 -3
  70. package/src/memory/graph-context.ts +8 -2
  71. package/src/memory/observations.ts +162 -4
  72. package/src/memory/quality-audit.ts +2 -0
  73. package/src/memory/retention.ts +22 -2
  74. package/src/memory/session.ts +29 -11
  75. package/src/memory/visibility.ts +80 -0
  76. package/src/orchestrate/memorix-bridge.ts +38 -0
  77. package/src/runtime/control-plane-maintenance.ts +1 -0
  78. package/src/runtime/isolated-maintenance.ts +1 -0
  79. package/src/runtime/lifecycle.ts +18 -0
  80. package/src/runtime/maintenance-jobs.ts +1 -0
  81. package/src/runtime/maintenance-runner.ts +2 -0
  82. package/src/runtime/project-maintenance.ts +89 -0
  83. package/src/sdk.ts +35 -5
  84. package/src/server.ts +267 -83
  85. package/src/store/orama-store.ts +61 -6
  86. package/src/store/sqlite-db.ts +23 -1
  87. package/src/store/sqlite-store.ts +12 -2
  88. package/src/team/handoff.ts +7 -0
  89. package/src/types.ts +51 -0
  90. package/src/wiki/generator.ts +2 -0
@@ -9,7 +9,7 @@
9
9
  * Layer 3 (detail) → Full observation content (~500-1000 tokens/result)
10
10
  */
11
11
 
12
- import type { SearchOptions, IndexEntry, TimelineContext, MemorixDocument, ObservationRef, MemoryRef, MiniSkill, SourceSnapshot } from '../types.js';
12
+ import type { SearchOptions, IndexEntry, TimelineContext, MemorixDocument, ObservationRef, MemoryRef, MiniSkill, SourceSnapshot, ObservationReader } from '../types.js';
13
13
  import { searchObservations, getTimeline, getObservationsByIds, makeOramaObservationId } from '../store/orama-store.js';
14
14
  import { getObservation, getAllObservations } from '../memory/observations.js';
15
15
  import { ensureFreshIndex } from '../memory/freshness.js';
@@ -21,6 +21,7 @@ import { getObservationStore } from '../store/obs-store.js';
21
21
  import { getMiniSkillStore } from '../store/mini-skill-store.js';
22
22
  import { miniSkillToDocument, resolveProvenanceStatus, type ProvenanceStatus } from '../skills/mini-skills.js';
23
23
  import { redactCredentials } from '../memory/secret-filter.js';
24
+ import { canReadObservation, filterReadableObservations } from '../memory/visibility.js';
24
25
 
25
26
  export function normalizeMemoryBrowseQuery(query: string): string {
26
27
  const trimmed = query.trim();
@@ -76,14 +77,17 @@ export async function compactSearch(options: SearchOptions): Promise<{
76
77
  let formatted = formatIndexTable(entries, searchOptions.query, !options.projectId);
77
78
 
78
79
  if (entries.length === 0 && options.projectId) {
79
- const allObservations = getAllObservations();
80
+ const allObservations = filterReadableObservations(getAllObservations(), options.reader);
80
81
  const projectAliases = new Set(await resolveAliases(options.projectId).catch(() => [options.projectId]));
81
82
  const projectHasStoredMemory = allObservations.some((obs) => obs.projectId && projectAliases.has(obs.projectId));
82
83
  if (!projectHasStoredMemory) {
83
84
  formatted =
84
- `This project does not have any Memorix memories yet.\n\n` +
85
- `It looks like a fresh project: the tool call worked, but there is nothing stored to retrieve yet.\n\n` +
86
- `Memories will start appearing after observations, session summaries, hook captures, or git-memory are written.`;
85
+ options.reader
86
+ ? `No Memorix memories are available to this session yet.\n\n` +
87
+ `The tool call worked, but this session has no project-visible memory to retrieve.`
88
+ : `This project does not have any Memorix memories yet.\n\n` +
89
+ `It looks like a fresh project: the tool call worked, but there is nothing stored to retrieve yet.\n\n` +
90
+ `Memories will start appearing after observations, session summaries, hook captures, or git-memory are written.`;
87
91
  } else {
88
92
  formatted =
89
93
  `No memories found matching "${options.query}".\n\n` +
@@ -106,12 +110,13 @@ export async function compactTimeline(
106
110
  projectId?: string,
107
111
  depthBefore = 3,
108
112
  depthAfter = 3,
113
+ reader?: ObservationReader,
109
114
  ): Promise<{
110
115
  timeline: TimelineContext;
111
116
  formatted: string;
112
117
  totalTokens: number;
113
118
  }> {
114
- const result = await getTimeline(anchorId, projectId, depthBefore, depthAfter);
119
+ const result = await getTimeline(anchorId, projectId, depthBefore, depthAfter, reader);
115
120
 
116
121
  const timeline: TimelineContext = {
117
122
  anchorId,
@@ -135,6 +140,7 @@ export async function compactTimeline(
135
140
  */
136
141
  export async function compactDetail(
137
142
  idsOrRefs: number[] | ObservationRef[] | string[],
143
+ options: { reader?: ObservationReader } = {},
138
144
  ): Promise<{
139
145
  documents: MemorixDocument[];
140
146
  formatted: string;
@@ -212,7 +218,7 @@ export async function compactDetail(
212
218
  const missingRefs: ObservationRef[] = [];
213
219
  for (const ref of refs) {
214
220
  const obs = getObservation(ref.id, ref.projectId);
215
- if (obs && (ref.projectId ? obs.projectId === ref.projectId : true)) {
221
+ if (obs && (ref.projectId ? obs.projectId === ref.projectId : true) && canReadObservation(obs, options.reader)) {
216
222
  documentMap.set(toRefKey(ref), {
217
223
  id: makeOramaObservationId(obs.projectId, obs.id),
218
224
  observationId: obs.id,
@@ -232,6 +238,11 @@ export async function compactDetail(
232
238
  source: obs.source ?? 'agent',
233
239
  sourceDetail: obs.sourceDetail ?? '',
234
240
  valueCategory: obs.valueCategory ?? '',
241
+ admissionState: obs.admissionState ?? '',
242
+ admissionReason: obs.admissionReason ?? '',
243
+ visibility: obs.visibility ?? 'project',
244
+ createdByAgentId: obs.createdByAgentId ?? '',
245
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
235
246
  });
236
247
  } else {
237
248
  missingRefs.push(ref);
@@ -242,19 +253,19 @@ export async function compactDetail(
242
253
  for (const ref of missingRefs) {
243
254
  const fallbackDocs = await getObservationsByIds([ref.id], ref.projectId);
244
255
  const doc = fallbackDocs[0];
245
- if (doc) {
256
+ if (doc && canReadObservation(doc, options.reader)) {
246
257
  documentMap.set(toRefKey(ref), doc);
247
258
  continue;
248
259
  }
249
260
  const stored = await getPersistedObservation(ref);
250
- if (stored) {
261
+ if (stored && canReadObservation(stored, options.reader)) {
251
262
  documentMap.set(toRefKey(ref), observationToDocument(stored));
252
263
  }
253
264
  }
254
265
  }
255
266
 
256
267
  // Build cross-reference map for all requested observations
257
- const allObs = getAllObservations();
268
+ const allObs = filterReadableObservations(getAllObservations(), options.reader);
258
269
  const crossRefMap = new Map<string, string[]>();
259
270
  for (const ref of refs) {
260
271
  const obs = getObservation(ref.id, ref.projectId);
@@ -401,6 +412,11 @@ function observationToDocument(obs: ReturnType<typeof getAllObservations>[number
401
412
  source: obs.source ?? 'agent',
402
413
  sourceDetail: obs.sourceDetail ?? '',
403
414
  valueCategory: obs.valueCategory ?? '',
415
+ admissionState: obs.admissionState ?? '',
416
+ admissionReason: obs.admissionReason ?? '',
417
+ visibility: obs.visibility ?? 'project',
418
+ createdByAgentId: obs.createdByAgentId ?? '',
419
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
404
420
  };
405
421
  }
406
422
 
@@ -179,6 +179,8 @@ export function formatObservationDetail(doc: {
179
179
  entityName: string;
180
180
  sourceDetail?: string;
181
181
  valueCategory?: string;
182
+ admissionState?: string;
183
+ admissionReason?: string;
182
184
  source?: string;
183
185
  commitHash?: string;
184
186
  relatedCommits?: string[];
@@ -187,7 +189,15 @@ export function formatObservationDetail(doc: {
187
189
  const lines: string[] = [];
188
190
 
189
191
  // Provenance header — shown before #ID when sourceDetail (or legacy source='git') is set
190
- const header = buildProvenanceHeader(doc.sourceDetail, doc.valueCategory, doc.source, doc.commitHash, doc.relatedCommits);
192
+ const header = buildProvenanceHeader(
193
+ doc.sourceDetail,
194
+ doc.valueCategory,
195
+ doc.admissionState,
196
+ doc.admissionReason,
197
+ doc.source,
198
+ doc.commitHash,
199
+ doc.relatedCommits,
200
+ );
191
201
  if (header) {
192
202
  lines.push(header);
193
203
  lines.push('');
@@ -236,6 +246,8 @@ export function formatObservationDetail(doc: {
236
246
  function buildProvenanceHeader(
237
247
  sourceDetail?: string,
238
248
  valueCategory?: string,
249
+ admissionState?: string,
250
+ admissionReason?: string,
239
251
  source?: string,
240
252
  commitHash?: string,
241
253
  relatedCommits?: string[],
@@ -257,12 +269,23 @@ function buildProvenanceHeader(
257
269
  lines.push(verificationLine);
258
270
  }
259
271
 
260
- if (valueCategory === 'core') {
272
+ if (valueCategory === 'core' && admissionState !== 'candidate' && admissionState !== 'ephemeral') {
261
273
  lines.push('[CORE] Core — immune to decay');
274
+ } else if (valueCategory === 'core') {
275
+ lines.push('[CORE] Core candidate — not durable until qualification completes');
262
276
  } else if (valueCategory === 'ephemeral') {
263
277
  lines.push('[WARN] Ephemeral — short-lived signal');
264
278
  }
265
279
 
280
+ if (admissionState === 'candidate') {
281
+ lines.push('[PENDING] Candidate — excluded from automatic context until current Code Memory qualifies it');
282
+ } else if (admissionState === 'ephemeral') {
283
+ lines.push('[TRACE] Ephemeral capture — excluded from automatic context');
284
+ } else if (admissionState === 'qualified') {
285
+ lines.push('[QUALIFIED] Automatic evidence passed current Code Memory qualification');
286
+ }
287
+ if (admissionReason) lines.push(`Admission: ${redactCredentials(admissionReason)}`);
288
+
266
289
  return lines.join('\n');
267
290
  }
268
291
 
@@ -23,6 +23,8 @@ import { resetDotenv } from '../config/dotenv-loader.js';
23
23
  import { initProjectRoot } from '../config/yaml-loader.js';
24
24
  import { clearProjectRoot } from '../config/yaml-loader.js';
25
25
  import { scopeKnowledgeGraphToProject } from '../memory/graph-scope.js';
26
+ import { canManageObservation, filterReadableObservations } from '../memory/visibility.js';
27
+ import type { Observation } from '../types.js';
26
28
 
27
29
  // MIME types for static file serving
28
30
  const MIME_TYPES: Record<string, string> = {
@@ -59,6 +61,15 @@ function filterByProject<T extends { projectId?: string }>(items: T[], projectId
59
61
  return items.filter(item => item.projectId === projectId);
60
62
  }
61
63
 
64
+ /**
65
+ * The dashboard has no per-agent authentication. Treat it as an unbound reader:
66
+ * project facts remain inspectable, while personal and team-scoped records stay
67
+ * in their owning MCP session.
68
+ */
69
+ function filterDashboardObservations(observations: Observation[], projectId: string): Observation[] {
70
+ return filterReadableObservations(observations, { projectId });
71
+ }
72
+
62
73
  function isActiveStatus(status?: string): boolean {
63
74
  return (status ?? 'active') === 'active';
64
75
  }
@@ -147,7 +158,10 @@ async function handleApi(
147
158
  // List all unique project IDs from observations data (flat storage)
148
159
  // Deduplicate using alias registry – aliased IDs are merged under canonical
149
160
  try {
150
- const allObs = await getObservationStore().loadAll() as Array<{ projectId?: string; status?: string }>;
161
+ const allObs = filterReadableObservations(
162
+ await getObservationStore().loadAll(),
163
+ {},
164
+ ) as Array<{ projectId?: string; status?: string }>;
151
165
  const projectSet = new Map<string, number>();
152
166
  for (const obs of allObs) {
153
167
  if (!isActiveStatus(obs.status)) continue;
@@ -204,14 +218,20 @@ async function handleApi(
204
218
  await initGraphStore(effectiveDataDir);
205
219
  const gStore = getGraphStore();
206
220
  const graph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
207
- const graphObs = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' });
221
+ const graphObs = filterDashboardObservations(
222
+ await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }),
223
+ effectiveProjectId,
224
+ );
208
225
  const scoped = scopeKnowledgeGraphToProject(graph, graphObs);
209
226
  sendJson(res, { entities: scoped.entities, relations: scoped.relations });
210
227
  break;
211
228
  }
212
229
 
213
230
  case '/observations': {
214
- const observations = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' });
231
+ const observations = filterDashboardObservations(
232
+ await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }),
233
+ effectiveProjectId,
234
+ );
215
235
  sendJson(res, observations);
216
236
  break;
217
237
  }
@@ -226,7 +246,10 @@ async function handleApi(
226
246
  case '/stats': {
227
247
  await initGraphStore(effectiveDataDir);
228
248
  const graph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
229
- const observations = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }) as Array<{ type?: string; id?: number; createdAt?: string; title?: string; entityName?: string; relatedEntities?: string[]; status?: string }>;
249
+ const observations = filterDashboardObservations(
250
+ await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }),
251
+ effectiveProjectId,
252
+ ) as Array<{ type?: string; id?: number; createdAt?: string; title?: string; entityName?: string; relatedEntities?: string[]; status?: string }>;
230
253
  const nextId = await getObservationStore().loadIdCounter();
231
254
 
232
255
  // Project-scoped graph counts (must match /api/graph and /api/export)
@@ -362,7 +385,10 @@ async function handleApi(
362
385
  }
363
386
 
364
387
  case '/retention': {
365
- const observations = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }) as Array<{
388
+ const observations = filterDashboardObservations(
389
+ await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }),
390
+ effectiveProjectId,
391
+ ) as Array<{
366
392
  id?: number;
367
393
  title?: string;
368
394
  type?: string;
@@ -423,7 +449,10 @@ async function handleApi(
423
449
 
424
450
  await initMiniSkillStore(effectiveDataDir);
425
451
 
426
- const allObs = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' });
452
+ const allObs = filterDashboardObservations(
453
+ await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }),
454
+ effectiveProjectId,
455
+ );
427
456
  const skills = await getMiniSkillStore().loadByProject(effectiveProjectId);
428
457
 
429
458
  const overview = generateKnowledgeBase({
@@ -444,7 +473,10 @@ async function handleApi(
444
473
  await initMiniSkillStore(effectiveDataDir);
445
474
  await initGraphStore(effectiveDataDir);
446
475
 
447
- const allObs = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' });
476
+ const allObs = filterDashboardObservations(
477
+ await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }),
478
+ effectiveProjectId,
479
+ );
448
480
  const skills = await getMiniSkillStore().loadByProject(effectiveProjectId);
449
481
 
450
482
  const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
@@ -597,7 +629,7 @@ async function handleApi(
597
629
 
598
630
  case '/identity': {
599
631
  // Project identity health — with classification layering (matches control-plane contract)
600
- const allObs = await getObservationStore().loadAll() as Array<{ projectId?: string }>;
632
+ const allObs = filterReadableObservations(await getObservationStore().loadAll(), {}) as Array<{ projectId?: string }>;
601
633
  const allProjectIds = [...new Set(allObs.map(o => o.projectId).filter(Boolean))] as string[];
602
634
 
603
635
  // Classify every known ID (real / temporary / placeholder) + dirty axis
@@ -698,6 +730,8 @@ async function handleApi(
698
730
  } else if (matchObs.projectId !== effectiveProjectId) {
699
731
  // Cross-project deletion guard: reject if obs belongs to a different project
700
732
  sendError(res, `Observation #${obsId} belongs to project "${matchObs.projectId}", not "${effectiveProjectId}"`, 403);
733
+ } else if (!canManageObservation(matchObs, { projectId: effectiveProjectId })) {
734
+ sendError(res, `Observation #${obsId} is not manageable from the unbound dashboard.`, 403);
701
735
  } else {
702
736
  await obsStore.remove(obsId);
703
737
 
@@ -722,7 +756,10 @@ async function handleApi(
722
756
  if (apiPath === '/export') {
723
757
  await initGraphStore(effectiveDataDir);
724
758
  const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
725
- const observations = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' });
759
+ const observations = filterDashboardObservations(
760
+ await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }),
761
+ effectiveProjectId,
762
+ );
726
763
  const nextId = await getObservationStore().loadIdCounter();
727
764
  const scoped = scopeKnowledgeGraphToProject(fullGraph, observations);
728
765
  const exportData = {
@@ -0,0 +1,117 @@
1
+ import type { ObservationAdmissionState, ObservationType } from '../types.js';
2
+ import { isSignificantKnowledge } from './significance-filter.js';
3
+ import type { NormalizedHookInput } from './types.js';
4
+
5
+ export type HookCaptureCategory =
6
+ | 'file_modify'
7
+ | 'file_read'
8
+ | 'command'
9
+ | 'search'
10
+ | 'memorix_internal'
11
+ | 'unknown';
12
+
13
+ export type HookAdmissionDecision =
14
+ | { action: 'drop'; reason: string }
15
+ | {
16
+ action: 'store';
17
+ admissionState: ObservationAdmissionState;
18
+ admissionReason: string;
19
+ valueCategory: 'core' | 'contextual' | 'ephemeral';
20
+ };
21
+
22
+ const FAILURE_PATTERN = /\b(fail(?:ed|ure)?|error|exception|assert(?:ion)?|exit code [1-9]|TypeError|ReferenceError|SyntaxError|ENOENT|ECONNREFUSED|panic)\b/i;
23
+ const DECISION_PATTERN = /\b(decision|decided|choose|chosen|because|trade-?off|invariant|root cause|workaround|must|should not)\b/i;
24
+ const TASK_PATTERN = /\b(add|implement|fix|refactor|migrate|upgrade|debug|investigate|test|release|deploy|remove)\b/i;
25
+ const VALIDATION_PATTERN = /\b(test|tests|vitest|jest|pytest|playwright|cypress|typecheck|lint|build|cargo test|go test)\b/i;
26
+ const FOCUSED_VALIDATION_PATTERN = /(?:--run|--filter|--grep|\s-t\s|\.test\.|\.spec\.|::[\w-]+)/i;
27
+ const ROUTINE_SUCCESS_PATTERN = /\b(added \d+ packages|up to date|passed|all tests pass|tests? passed|0 failures|success(?:fully)?)\b/i;
28
+
29
+ function isCandidateType(type: ObservationType): boolean {
30
+ return type === 'decision' ||
31
+ type === 'gotcha' ||
32
+ type === 'problem-solution' ||
33
+ type === 'trade-off' ||
34
+ type === 'why-it-exists';
35
+ }
36
+
37
+ function candidate(
38
+ admissionReason: string,
39
+ valueCategory: 'core' | 'contextual' = 'contextual',
40
+ ): HookAdmissionDecision {
41
+ return { action: 'store', admissionState: 'candidate', admissionReason, valueCategory };
42
+ }
43
+
44
+ function ephemeral(admissionReason: string): HookAdmissionDecision {
45
+ return { action: 'store', admissionState: 'ephemeral', admissionReason, valueCategory: 'ephemeral' };
46
+ }
47
+
48
+ /**
49
+ * Cheap, deterministic first-pass admission for cross-agent hooks. It never
50
+ * promotes an automatic record directly to durable context: background
51
+ * qualification must still prove a current Code Memory reference.
52
+ */
53
+ export function assessHookAdmission(input: {
54
+ hook: NormalizedHookInput;
55
+ category: HookCaptureCategory;
56
+ content: string;
57
+ observationType: ObservationType;
58
+ }): HookAdmissionDecision {
59
+ const { hook, category, content, observationType } = input;
60
+ const command = hook.command ?? '';
61
+ const hasFailure = FAILURE_PATTERN.test(content);
62
+ const hasDecision = DECISION_PATTERN.test(content) || isCandidateType(observationType);
63
+ const isValidation = VALIDATION_PATTERN.test(command) || VALIDATION_PATTERN.test(content);
64
+ const isFocusedValidation = FOCUSED_VALIDATION_PATTERN.test(command);
65
+ const isRoutineSuccess = ROUTINE_SUCCESS_PATTERN.test(content);
66
+
67
+ if (hasFailure) {
68
+ return candidate('automatic capture contains a concrete failure or verification signal');
69
+ }
70
+ if (hasDecision) {
71
+ return candidate('automatic capture contains a decision or durable rationale', 'core');
72
+ }
73
+
74
+ if (hook.event === 'user_prompt') {
75
+ return TASK_PATTERN.test(content)
76
+ ? candidate('user task contains a concrete technical action')
77
+ : { action: 'drop', reason: 'prompt has no durable technical task signal' };
78
+ }
79
+
80
+ if (hook.event === 'post_response') {
81
+ // Agent integrations such as OpenCode emit concise end-of-turn summaries.
82
+ // Keep technically meaningful ones as candidates; qualification still
83
+ // prevents them from becoming automatic context without code evidence.
84
+ return isSignificantKnowledge(content).isSignificant
85
+ ? candidate('assistant response records a concrete technical outcome')
86
+ : { action: 'drop', reason: 'response has no durable technical outcome' };
87
+ }
88
+
89
+ if (hook.event === 'session_end') {
90
+ // The handler already applies a minimum-content gate for session end.
91
+ // Preserve a compact handoff trace, but keep it out of automatic context
92
+ // until later qualification.
93
+ return candidate('session-end summary awaits Code Memory qualification');
94
+ }
95
+
96
+ if (category === 'file_modify') {
97
+ return candidate('file mutation awaits Code Memory qualification');
98
+ }
99
+
100
+ if (category === 'command') {
101
+ if (isValidation && isFocusedValidation && !isRoutineSuccess) {
102
+ return candidate('focused validation result awaits Code Memory qualification');
103
+ }
104
+ if (isValidation || isRoutineSuccess) {
105
+ return ephemeral('routine command result retained only as a short-lived trace');
106
+ }
107
+ return ephemeral('command activity retained only as a short-lived trace');
108
+ }
109
+
110
+ if (category === 'search' || category === 'unknown') {
111
+ return content.length >= 300
112
+ ? ephemeral('unverified automatic activity retained only as a short-lived trace')
113
+ : { action: 'drop', reason: 'automatic activity has insufficient evidence' };
114
+ }
115
+
116
+ return { action: 'drop', reason: 'capture category is not eligible for automatic storage' };
117
+ }