memorix 1.2.1 → 1.2.2

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 (199) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +14 -2
  3. package/README.zh-CN.md +14 -2
  4. package/TEAM.md +86 -86
  5. package/dist/cli/index.js +15407 -13779
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1321 -529
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +8458 -8087
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +16 -0
  13. package/dist/sdk.d.ts +7 -2
  14. package/dist/sdk.js +1349 -535
  15. package/dist/sdk.js.map +1 -1
  16. package/dist/types.d.ts +49 -1
  17. package/dist/types.js.map +1 -1
  18. package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
  19. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
  20. package/docs/API_REFERENCE.md +24 -4
  21. package/docs/DESIGN_DECISIONS.md +357 -357
  22. package/docs/README.md +1 -1
  23. package/docs/dev-log/progress.txt +91 -11
  24. package/package.json +1 -1
  25. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  26. package/src/audit/index.ts +156 -156
  27. package/src/cli/command-guide.ts +192 -0
  28. package/src/cli/commands/audit-list.ts +89 -89
  29. package/src/cli/commands/audit.ts +9 -4
  30. package/src/cli/commands/background.ts +659 -659
  31. package/src/cli/commands/cleanup.ts +5 -1
  32. package/src/cli/commands/codegraph.ts +15 -5
  33. package/src/cli/commands/context.ts +3 -2
  34. package/src/cli/commands/doctor.ts +4 -2
  35. package/src/cli/commands/explain.ts +9 -3
  36. package/src/cli/commands/formation.ts +48 -48
  37. package/src/cli/commands/git-hook-install.ts +111 -111
  38. package/src/cli/commands/handoff.ts +75 -61
  39. package/src/cli/commands/hooks-status.ts +63 -63
  40. package/src/cli/commands/identity.ts +116 -0
  41. package/src/cli/commands/ingest-commit.ts +153 -153
  42. package/src/cli/commands/ingest-image.ts +71 -69
  43. package/src/cli/commands/ingest-log.ts +180 -180
  44. package/src/cli/commands/ingest.ts +44 -44
  45. package/src/cli/commands/integrate-shared.ts +15 -15
  46. package/src/cli/commands/lock.ts +93 -92
  47. package/src/cli/commands/memory.ts +58 -21
  48. package/src/cli/commands/message.ts +123 -118
  49. package/src/cli/commands/operator-shared.ts +98 -3
  50. package/src/cli/commands/poll.ts +74 -64
  51. package/src/cli/commands/purge-all-memory.ts +85 -85
  52. package/src/cli/commands/purge-project-memory.ts +83 -83
  53. package/src/cli/commands/reasoning.ts +135 -121
  54. package/src/cli/commands/retention.ts +9 -4
  55. package/src/cli/commands/serve-http.ts +8 -2
  56. package/src/cli/commands/serve-shared.ts +118 -118
  57. package/src/cli/commands/session.ts +29 -3
  58. package/src/cli/commands/skills.ts +124 -119
  59. package/src/cli/commands/status.ts +4 -3
  60. package/src/cli/commands/task.ts +193 -184
  61. package/src/cli/commands/team.ts +14 -10
  62. package/src/cli/commands/transfer.ts +108 -55
  63. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  64. package/src/cli/identity.ts +89 -0
  65. package/src/cli/index.ts +96 -19
  66. package/src/cli/invocation.ts +115 -0
  67. package/src/cli/tui/ChatView.tsx +234 -234
  68. package/src/cli/tui/CommandBar.tsx +312 -312
  69. package/src/cli/tui/ContextRail.tsx +118 -118
  70. package/src/cli/tui/HeaderBar.tsx +72 -72
  71. package/src/cli/tui/LogoBanner.tsx +51 -51
  72. package/src/cli/tui/Sidebar.tsx +179 -179
  73. package/src/cli/tui/chat-service.ts +41 -18
  74. package/src/cli/tui/data.ts +23 -44
  75. package/src/cli/tui/index.ts +41 -41
  76. package/src/cli/tui/markdown-render.tsx +371 -371
  77. package/src/cli/tui/operator-context.ts +60 -0
  78. package/src/cli/tui/use-mouse.ts +157 -157
  79. package/src/cli/tui/useNavigation.ts +56 -56
  80. package/src/cli/tui/views/MemoryView.tsx +10 -8
  81. package/src/cli/update-checker.ts +211 -211
  82. package/src/cli/version.ts +7 -7
  83. package/src/cli/workbench.ts +1 -1
  84. package/src/codegraph/auto-context.ts +31 -2
  85. package/src/codegraph/context-pack.ts +1 -0
  86. package/src/codegraph/project-context.ts +2 -0
  87. package/src/compact/engine.ts +26 -10
  88. package/src/compact/index-format.ts +25 -2
  89. package/src/compact/token-budget.ts +74 -74
  90. package/src/dashboard/project-classification.ts +64 -64
  91. package/src/dashboard/server.ts +46 -9
  92. package/src/embedding/fastembed-provider.ts +142 -142
  93. package/src/embedding/transformers-provider.ts +111 -111
  94. package/src/git/extractor.ts +209 -209
  95. package/src/git/hooks-path.ts +85 -85
  96. package/src/hooks/admission.ts +117 -0
  97. package/src/hooks/handler.ts +98 -91
  98. package/src/hooks/pattern-detector.ts +173 -173
  99. package/src/hooks/significance-filter.ts +250 -250
  100. package/src/knowledge/context-assembly.ts +97 -0
  101. package/src/knowledge/workset.ts +179 -10
  102. package/src/llm/memory-manager.ts +328 -328
  103. package/src/llm/provider.ts +885 -885
  104. package/src/llm/quality.ts +248 -248
  105. package/src/memory/admission.ts +57 -0
  106. package/src/memory/attribution-guard.ts +249 -249
  107. package/src/memory/consolidation.ts +13 -2
  108. package/src/memory/disclosure-policy.ts +140 -135
  109. package/src/memory/entity-extractor.ts +197 -197
  110. package/src/memory/export-import.ts +11 -3
  111. package/src/memory/formation/evaluate.ts +217 -217
  112. package/src/memory/formation/extract.ts +361 -361
  113. package/src/memory/formation/index.ts +417 -417
  114. package/src/memory/formation/resolve.ts +344 -344
  115. package/src/memory/formation/types.ts +315 -315
  116. package/src/memory/freshness.ts +122 -122
  117. package/src/memory/graph-context.ts +8 -2
  118. package/src/memory/graph.ts +197 -197
  119. package/src/memory/observations.ts +162 -4
  120. package/src/memory/quality-audit.ts +2 -0
  121. package/src/memory/refs.ts +94 -94
  122. package/src/memory/retention.ts +22 -2
  123. package/src/memory/secret-filter.ts +79 -79
  124. package/src/memory/session.ts +5 -2
  125. package/src/memory/visibility.ts +80 -0
  126. package/src/multimodal/image-loader.ts +143 -143
  127. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  128. package/src/orchestrate/adapters/claude.ts +111 -111
  129. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  130. package/src/orchestrate/adapters/codex.ts +41 -41
  131. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  132. package/src/orchestrate/adapters/gemini.ts +42 -42
  133. package/src/orchestrate/adapters/index.ts +73 -73
  134. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  135. package/src/orchestrate/adapters/opencode.ts +47 -47
  136. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  137. package/src/orchestrate/adapters/types.ts +77 -77
  138. package/src/orchestrate/capability-router.ts +284 -284
  139. package/src/orchestrate/context-compact.ts +188 -188
  140. package/src/orchestrate/cost-tracker.ts +219 -219
  141. package/src/orchestrate/error-recovery.ts +191 -191
  142. package/src/orchestrate/evidence.ts +140 -140
  143. package/src/orchestrate/ledger.ts +110 -110
  144. package/src/orchestrate/memorix-bridge.ts +378 -340
  145. package/src/orchestrate/output-budget.ts +80 -80
  146. package/src/orchestrate/permission.ts +152 -152
  147. package/src/orchestrate/pipeline-trace.ts +131 -131
  148. package/src/orchestrate/prompt-builder.ts +155 -155
  149. package/src/orchestrate/ring-buffer.ts +37 -37
  150. package/src/orchestrate/task-graph.ts +389 -389
  151. package/src/orchestrate/worktree.ts +232 -232
  152. package/src/project/aliases.ts +374 -374
  153. package/src/project/detector.ts +268 -268
  154. package/src/rules/adapters/claude-code.ts +99 -99
  155. package/src/rules/adapters/codex.ts +97 -97
  156. package/src/rules/adapters/copilot.ts +124 -124
  157. package/src/rules/adapters/cursor.ts +114 -114
  158. package/src/rules/adapters/kiro.ts +126 -126
  159. package/src/rules/adapters/trae.ts +56 -56
  160. package/src/rules/adapters/windsurf.ts +83 -83
  161. package/src/rules/syncer.ts +235 -235
  162. package/src/runtime/control-plane-maintenance.ts +1 -0
  163. package/src/runtime/isolated-maintenance.ts +1 -0
  164. package/src/runtime/lifecycle.ts +18 -0
  165. package/src/runtime/maintenance-jobs.ts +1 -0
  166. package/src/runtime/maintenance-runner.ts +2 -0
  167. package/src/runtime/project-maintenance.ts +89 -0
  168. package/src/sdk.ts +334 -304
  169. package/src/search/intent-detector.ts +289 -289
  170. package/src/search/query-expansion.ts +52 -52
  171. package/src/server/formation-timeout.ts +27 -27
  172. package/src/server.ts +260 -81
  173. package/src/skills/mini-skills.ts +386 -386
  174. package/src/store/chat-store.ts +119 -119
  175. package/src/store/graph-store.ts +249 -249
  176. package/src/store/mini-skill-store.ts +349 -349
  177. package/src/store/orama-store.ts +61 -6
  178. package/src/store/persistence-json.ts +212 -212
  179. package/src/store/persistence.ts +291 -291
  180. package/src/store/project-affinity.ts +195 -195
  181. package/src/store/sqlite-db.ts +23 -1
  182. package/src/store/sqlite-store.ts +12 -2
  183. package/src/team/event-bus.ts +76 -76
  184. package/src/team/file-locks.ts +173 -173
  185. package/src/team/handoff.ts +168 -161
  186. package/src/team/messages.ts +203 -203
  187. package/src/team/poll.ts +132 -132
  188. package/src/team/tasks.ts +211 -211
  189. package/src/types.ts +51 -0
  190. package/src/wiki/generator.ts +2 -0
  191. package/src/workspace/mcp-adapters/codex.ts +191 -191
  192. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  193. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  194. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  195. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  196. package/src/workspace/mcp-adapters/trae.ts +134 -134
  197. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  198. package/src/workspace/sanitizer.ts +60 -60
  199. package/src/workspace/workflow-sync.ts +131 -131
@@ -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
 
@@ -1,74 +1,74 @@
1
- /**
2
- * Token Budget Manager
3
- *
4
- * Provides token counting and budget management for Progressive Disclosure.
5
- * Source: gpt-tokenizer (737 stars, JS port of OpenAI's tiktoken)
6
- *
7
- * Used by the Compact Engine to determine which layer of detail
8
- * fits within the caller's token budget.
9
- */
10
-
11
- import { countTokens, isWithinTokenLimit } from 'gpt-tokenizer';
12
-
13
- /**
14
- * Count tokens in a string.
15
- */
16
- export function countTextTokens(text: string): number {
17
- return countTokens(text);
18
- }
19
-
20
- /**
21
- * Check if text fits within a token limit.
22
- * Returns the token count if within limit, false otherwise.
23
- */
24
- export function fitsInBudget(text: string, limit: number): number | false {
25
- return isWithinTokenLimit(text, limit);
26
- }
27
-
28
- /**
29
- * Truncate text to fit within a token budget.
30
- * Truncates at sentence boundaries when possible.
31
- */
32
- export function truncateToTokenBudget(text: string, budget: number): string {
33
- if (fitsInBudget(text, budget) !== false) {
34
- return text;
35
- }
36
-
37
- // Binary search for the right length
38
- const sentences = text.split(/(?<=[.!?])\s+/);
39
- let result = '';
40
-
41
- for (const sentence of sentences) {
42
- const candidate = result ? `${result} ${sentence}` : sentence;
43
- if (fitsInBudget(candidate, budget) === false) {
44
- break;
45
- }
46
- result = candidate;
47
- }
48
-
49
- // If no complete sentence fits, truncate by characters
50
- if (!result) {
51
- // Rough estimate: 1 token ≈ 4 chars for English, ≈ 1.5 chars for Chinese
52
- const estimatedChars = budget * 2;
53
- result = text.slice(0, estimatedChars);
54
- // Refine
55
- while (fitsInBudget(result, budget) === false && result.length > 0) {
56
- result = result.slice(0, Math.floor(result.length * 0.9));
57
- }
58
- if (result.length < text.length) {
59
- result += '...';
60
- }
61
- }
62
-
63
- return result;
64
- }
65
-
66
- /**
67
- * Estimate the token cost of an IndexEntry line.
68
- * Used to predict compact index size.
69
- */
70
- export function estimateIndexEntryTokens(title: string): number {
71
- // Format: "| #ID | Time | Icon | Title | ~Tokens |"
72
- // Overhead is roughly 15 tokens for formatting
73
- return countTextTokens(title) + 15;
74
- }
1
+ /**
2
+ * Token Budget Manager
3
+ *
4
+ * Provides token counting and budget management for Progressive Disclosure.
5
+ * Source: gpt-tokenizer (737 stars, JS port of OpenAI's tiktoken)
6
+ *
7
+ * Used by the Compact Engine to determine which layer of detail
8
+ * fits within the caller's token budget.
9
+ */
10
+
11
+ import { countTokens, isWithinTokenLimit } from 'gpt-tokenizer';
12
+
13
+ /**
14
+ * Count tokens in a string.
15
+ */
16
+ export function countTextTokens(text: string): number {
17
+ return countTokens(text);
18
+ }
19
+
20
+ /**
21
+ * Check if text fits within a token limit.
22
+ * Returns the token count if within limit, false otherwise.
23
+ */
24
+ export function fitsInBudget(text: string, limit: number): number | false {
25
+ return isWithinTokenLimit(text, limit);
26
+ }
27
+
28
+ /**
29
+ * Truncate text to fit within a token budget.
30
+ * Truncates at sentence boundaries when possible.
31
+ */
32
+ export function truncateToTokenBudget(text: string, budget: number): string {
33
+ if (fitsInBudget(text, budget) !== false) {
34
+ return text;
35
+ }
36
+
37
+ // Binary search for the right length
38
+ const sentences = text.split(/(?<=[.!?])\s+/);
39
+ let result = '';
40
+
41
+ for (const sentence of sentences) {
42
+ const candidate = result ? `${result} ${sentence}` : sentence;
43
+ if (fitsInBudget(candidate, budget) === false) {
44
+ break;
45
+ }
46
+ result = candidate;
47
+ }
48
+
49
+ // If no complete sentence fits, truncate by characters
50
+ if (!result) {
51
+ // Rough estimate: 1 token ≈ 4 chars for English, ≈ 1.5 chars for Chinese
52
+ const estimatedChars = budget * 2;
53
+ result = text.slice(0, estimatedChars);
54
+ // Refine
55
+ while (fitsInBudget(result, budget) === false && result.length > 0) {
56
+ result = result.slice(0, Math.floor(result.length * 0.9));
57
+ }
58
+ if (result.length < text.length) {
59
+ result += '...';
60
+ }
61
+ }
62
+
63
+ return result;
64
+ }
65
+
66
+ /**
67
+ * Estimate the token cost of an IndexEntry line.
68
+ * Used to predict compact index size.
69
+ */
70
+ export function estimateIndexEntryTokens(title: string): number {
71
+ // Format: "| #ID | Time | Icon | Title | ~Tokens |"
72
+ // Overhead is roughly 15 tokens for formatting
73
+ return countTextTokens(title) + 15;
74
+ }
@@ -1,64 +1,64 @@
1
- /**
2
- * Project classification helpers — shared by /api/projects and /api/identity.
3
- *
4
- * Three kinds:
5
- * - 'real': genuine user projects (e.g. AVIDS2/memorix, github.com/org/repo)
6
- * - 'temporary': test/demo/smoke/e2e scratch projects (local/task-*, local/smoke-*, etc.)
7
- * - 'placeholder': unresolved / obviously broken IDs (placeholder/*, __unresolved__, System32)
8
- *
9
- * A 'dirty' project is one with a clearly broken canonical ID (System32 etc.).
10
- * 'dirty' and 'temporary' are orthogonal axes:
11
- * - local/task-abc → temporary, NOT dirty
12
- * - placeholder/xxx → placeholder, dirty
13
- * - System32\something → real-looking location but dirty (broken ID)
14
- */
15
-
16
- export type ProjectKind = 'real' | 'temporary' | 'placeholder';
17
-
18
- /** Regex list — anything matching is temporary (scratch projects) */
19
- const TEMPORARY_PATTERNS: RegExp[] = [
20
- /^local\/task-/i,
21
- /^local\/smoke-/i,
22
- /^local\/release-smoke-/i,
23
- /^local\/memorix-e2e-/i,
24
- /^local\/orchestrate-/i,
25
- /^local\/scratch-/i,
26
- /^local\/tmp-/i,
27
- ];
28
-
29
- /** Regex list — anything matching is placeholder/unresolved */
30
- const PLACEHOLDER_PATTERNS: RegExp[] = [
31
- /^__unresolved__$/,
32
- /^placeholder\//i,
33
- ];
34
-
35
- /** Regex list — IDs that indicate a broken canonical ID (dirty). */
36
- const DIRTY_PATTERNS: RegExp[] = [
37
- /^placeholder\//i,
38
- /System32/i,
39
- /Microsoft VS Code/i,
40
- /node_modules/i,
41
- /\.vscode/i,
42
- /^local\/[A-Z]:\\/i,
43
- ];
44
-
45
- export function classifyProjectId(id: string): ProjectKind {
46
- if (!id) return 'placeholder';
47
- if (PLACEHOLDER_PATTERNS.some(p => p.test(id))) return 'placeholder';
48
- if (TEMPORARY_PATTERNS.some(p => p.test(id))) return 'temporary';
49
- return 'real';
50
- }
51
-
52
- export function isDirtyProjectId(id: string): boolean {
53
- if (!id) return false;
54
- return DIRTY_PATTERNS.some(p => p.test(id));
55
- }
56
-
57
- /** Friendly label for UI badges */
58
- export function projectKindLabel(kind: ProjectKind): string {
59
- switch (kind) {
60
- case 'real': return 'real';
61
- case 'temporary': return 'temporary';
62
- case 'placeholder': return 'placeholder';
63
- }
64
- }
1
+ /**
2
+ * Project classification helpers — shared by /api/projects and /api/identity.
3
+ *
4
+ * Three kinds:
5
+ * - 'real': genuine user projects (e.g. AVIDS2/memorix, github.com/org/repo)
6
+ * - 'temporary': test/demo/smoke/e2e scratch projects (local/task-*, local/smoke-*, etc.)
7
+ * - 'placeholder': unresolved / obviously broken IDs (placeholder/*, __unresolved__, System32)
8
+ *
9
+ * A 'dirty' project is one with a clearly broken canonical ID (System32 etc.).
10
+ * 'dirty' and 'temporary' are orthogonal axes:
11
+ * - local/task-abc → temporary, NOT dirty
12
+ * - placeholder/xxx → placeholder, dirty
13
+ * - System32\something → real-looking location but dirty (broken ID)
14
+ */
15
+
16
+ export type ProjectKind = 'real' | 'temporary' | 'placeholder';
17
+
18
+ /** Regex list — anything matching is temporary (scratch projects) */
19
+ const TEMPORARY_PATTERNS: RegExp[] = [
20
+ /^local\/task-/i,
21
+ /^local\/smoke-/i,
22
+ /^local\/release-smoke-/i,
23
+ /^local\/memorix-e2e-/i,
24
+ /^local\/orchestrate-/i,
25
+ /^local\/scratch-/i,
26
+ /^local\/tmp-/i,
27
+ ];
28
+
29
+ /** Regex list — anything matching is placeholder/unresolved */
30
+ const PLACEHOLDER_PATTERNS: RegExp[] = [
31
+ /^__unresolved__$/,
32
+ /^placeholder\//i,
33
+ ];
34
+
35
+ /** Regex list — IDs that indicate a broken canonical ID (dirty). */
36
+ const DIRTY_PATTERNS: RegExp[] = [
37
+ /^placeholder\//i,
38
+ /System32/i,
39
+ /Microsoft VS Code/i,
40
+ /node_modules/i,
41
+ /\.vscode/i,
42
+ /^local\/[A-Z]:\\/i,
43
+ ];
44
+
45
+ export function classifyProjectId(id: string): ProjectKind {
46
+ if (!id) return 'placeholder';
47
+ if (PLACEHOLDER_PATTERNS.some(p => p.test(id))) return 'placeholder';
48
+ if (TEMPORARY_PATTERNS.some(p => p.test(id))) return 'temporary';
49
+ return 'real';
50
+ }
51
+
52
+ export function isDirtyProjectId(id: string): boolean {
53
+ if (!id) return false;
54
+ return DIRTY_PATTERNS.some(p => p.test(id));
55
+ }
56
+
57
+ /** Friendly label for UI badges */
58
+ export function projectKindLabel(kind: ProjectKind): string {
59
+ switch (kind) {
60
+ case 'real': return 'real';
61
+ case 'temporary': return 'temporary';
62
+ case 'placeholder': return 'placeholder';
63
+ }
64
+ }
@@ -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 = {