memorix 1.2.0 → 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 (212) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +18 -4
  3. package/README.zh-CN.md +18 -4
  4. package/TEAM.md +86 -86
  5. package/dist/cli/index.js +15919 -14055
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1997 -1021
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +8481 -8005
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +30 -1
  13. package/dist/sdk.d.ts +7 -2
  14. package/dist/sdk.js +2022 -1024
  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 +27 -5
  21. package/docs/DESIGN_DECISIONS.md +357 -357
  22. package/docs/DEVELOPMENT.md +4 -0
  23. package/docs/README.md +1 -1
  24. package/docs/SETUP.md +7 -1
  25. package/docs/dev-log/progress.txt +91 -11
  26. package/docs/knowledge/workflows/memorix-release.md +57 -0
  27. package/package.json +1 -1
  28. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  29. package/src/audit/index.ts +156 -156
  30. package/src/cli/command-guide.ts +192 -0
  31. package/src/cli/commands/audit-list.ts +89 -89
  32. package/src/cli/commands/audit.ts +9 -4
  33. package/src/cli/commands/background.ts +659 -659
  34. package/src/cli/commands/cleanup.ts +5 -1
  35. package/src/cli/commands/codegraph.ts +17 -8
  36. package/src/cli/commands/context.ts +3 -2
  37. package/src/cli/commands/doctor.ts +4 -2
  38. package/src/cli/commands/explain.ts +9 -3
  39. package/src/cli/commands/formation.ts +48 -48
  40. package/src/cli/commands/git-hook-install.ts +111 -111
  41. package/src/cli/commands/handoff.ts +75 -61
  42. package/src/cli/commands/hooks-status.ts +63 -63
  43. package/src/cli/commands/identity.ts +116 -0
  44. package/src/cli/commands/ingest-commit.ts +153 -153
  45. package/src/cli/commands/ingest-image.ts +71 -69
  46. package/src/cli/commands/ingest-log.ts +180 -180
  47. package/src/cli/commands/ingest.ts +44 -44
  48. package/src/cli/commands/integrate-shared.ts +15 -15
  49. package/src/cli/commands/knowledge.ts +40 -0
  50. package/src/cli/commands/lock.ts +93 -92
  51. package/src/cli/commands/memory.ts +58 -21
  52. package/src/cli/commands/message.ts +123 -118
  53. package/src/cli/commands/operator-shared.ts +98 -3
  54. package/src/cli/commands/poll.ts +74 -64
  55. package/src/cli/commands/purge-all-memory.ts +85 -85
  56. package/src/cli/commands/purge-project-memory.ts +83 -83
  57. package/src/cli/commands/reasoning.ts +135 -121
  58. package/src/cli/commands/retention.ts +9 -4
  59. package/src/cli/commands/serve-http.ts +22 -43
  60. package/src/cli/commands/serve-shared.ts +118 -118
  61. package/src/cli/commands/session.ts +29 -3
  62. package/src/cli/commands/setup.ts +9 -3
  63. package/src/cli/commands/skills.ts +124 -119
  64. package/src/cli/commands/status.ts +4 -3
  65. package/src/cli/commands/task.ts +193 -184
  66. package/src/cli/commands/team.ts +14 -10
  67. package/src/cli/commands/transfer.ts +108 -55
  68. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  69. package/src/cli/identity.ts +89 -0
  70. package/src/cli/index.ts +96 -19
  71. package/src/cli/invocation.ts +115 -0
  72. package/src/cli/tui/ChatView.tsx +234 -234
  73. package/src/cli/tui/CommandBar.tsx +312 -312
  74. package/src/cli/tui/ContextRail.tsx +118 -118
  75. package/src/cli/tui/HeaderBar.tsx +72 -72
  76. package/src/cli/tui/LogoBanner.tsx +51 -51
  77. package/src/cli/tui/Sidebar.tsx +179 -179
  78. package/src/cli/tui/chat-service.ts +41 -18
  79. package/src/cli/tui/data.ts +23 -44
  80. package/src/cli/tui/index.ts +41 -41
  81. package/src/cli/tui/markdown-render.tsx +371 -371
  82. package/src/cli/tui/operator-context.ts +60 -0
  83. package/src/cli/tui/use-mouse.ts +157 -157
  84. package/src/cli/tui/useNavigation.ts +56 -56
  85. package/src/cli/tui/views/MemoryView.tsx +10 -8
  86. package/src/cli/update-checker.ts +211 -211
  87. package/src/cli/version.ts +7 -7
  88. package/src/cli/workbench.ts +1 -1
  89. package/src/codegraph/auto-context.ts +34 -17
  90. package/src/codegraph/context-pack.ts +1 -0
  91. package/src/codegraph/current-facts.ts +19 -1
  92. package/src/codegraph/project-context.ts +2 -0
  93. package/src/codegraph/task-lens.ts +49 -5
  94. package/src/compact/engine.ts +26 -10
  95. package/src/compact/index-format.ts +25 -2
  96. package/src/compact/token-budget.ts +74 -74
  97. package/src/dashboard/project-classification.ts +64 -64
  98. package/src/dashboard/server.ts +58 -52
  99. package/src/embedding/fastembed-provider.ts +142 -142
  100. package/src/embedding/transformers-provider.ts +111 -111
  101. package/src/git/extractor.ts +209 -209
  102. package/src/git/hooks-path.ts +85 -85
  103. package/src/hooks/admission.ts +117 -0
  104. package/src/hooks/handler.ts +98 -91
  105. package/src/hooks/pattern-detector.ts +173 -173
  106. package/src/hooks/significance-filter.ts +250 -250
  107. package/src/knowledge/claims.ts +51 -1
  108. package/src/knowledge/context-assembly.ts +97 -0
  109. package/src/knowledge/types.ts +1 -0
  110. package/src/knowledge/workflows.ts +34 -3
  111. package/src/knowledge/workset.ts +179 -10
  112. package/src/llm/memory-manager.ts +328 -328
  113. package/src/llm/provider.ts +885 -885
  114. package/src/llm/quality.ts +248 -248
  115. package/src/memory/admission.ts +57 -0
  116. package/src/memory/attribution-guard.ts +249 -249
  117. package/src/memory/auto-relations.ts +21 -0
  118. package/src/memory/consolidation.ts +13 -2
  119. package/src/memory/disclosure-policy.ts +140 -135
  120. package/src/memory/entity-extractor.ts +197 -197
  121. package/src/memory/export-import.ts +11 -3
  122. package/src/memory/formation/evaluate.ts +217 -217
  123. package/src/memory/formation/extract.ts +361 -361
  124. package/src/memory/formation/index.ts +417 -417
  125. package/src/memory/formation/resolve.ts +344 -344
  126. package/src/memory/formation/types.ts +315 -315
  127. package/src/memory/freshness.ts +122 -122
  128. package/src/memory/graph-context.ts +8 -2
  129. package/src/memory/graph-scope.ts +46 -0
  130. package/src/memory/graph.ts +197 -197
  131. package/src/memory/observations.ts +162 -4
  132. package/src/memory/quality-audit.ts +2 -0
  133. package/src/memory/refs.ts +94 -94
  134. package/src/memory/retention.ts +22 -2
  135. package/src/memory/secret-filter.ts +79 -79
  136. package/src/memory/session.ts +5 -2
  137. package/src/memory/visibility.ts +80 -0
  138. package/src/multimodal/image-loader.ts +143 -143
  139. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  140. package/src/orchestrate/adapters/claude.ts +111 -111
  141. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  142. package/src/orchestrate/adapters/codex.ts +41 -41
  143. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  144. package/src/orchestrate/adapters/gemini.ts +42 -42
  145. package/src/orchestrate/adapters/index.ts +73 -73
  146. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  147. package/src/orchestrate/adapters/opencode.ts +47 -47
  148. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  149. package/src/orchestrate/adapters/types.ts +77 -77
  150. package/src/orchestrate/capability-router.ts +284 -284
  151. package/src/orchestrate/context-compact.ts +188 -188
  152. package/src/orchestrate/cost-tracker.ts +219 -219
  153. package/src/orchestrate/error-recovery.ts +191 -191
  154. package/src/orchestrate/evidence.ts +140 -140
  155. package/src/orchestrate/ledger.ts +110 -110
  156. package/src/orchestrate/memorix-bridge.ts +378 -340
  157. package/src/orchestrate/output-budget.ts +80 -80
  158. package/src/orchestrate/permission.ts +152 -152
  159. package/src/orchestrate/pipeline-trace.ts +131 -131
  160. package/src/orchestrate/prompt-builder.ts +155 -155
  161. package/src/orchestrate/ring-buffer.ts +37 -37
  162. package/src/orchestrate/task-graph.ts +389 -389
  163. package/src/orchestrate/verify-gate.ts +33 -10
  164. package/src/orchestrate/worktree.ts +232 -232
  165. package/src/project/aliases.ts +374 -374
  166. package/src/project/detector.ts +268 -268
  167. package/src/rules/adapters/claude-code.ts +99 -99
  168. package/src/rules/adapters/codex.ts +97 -97
  169. package/src/rules/adapters/copilot.ts +124 -124
  170. package/src/rules/adapters/cursor.ts +114 -114
  171. package/src/rules/adapters/kiro.ts +126 -126
  172. package/src/rules/adapters/trae.ts +56 -56
  173. package/src/rules/adapters/windsurf.ts +83 -83
  174. package/src/rules/syncer.ts +235 -235
  175. package/src/runtime/control-plane-maintenance.ts +1 -0
  176. package/src/runtime/isolated-maintenance.ts +1 -0
  177. package/src/runtime/lifecycle.ts +18 -0
  178. package/src/runtime/maintenance-jobs.ts +1 -0
  179. package/src/runtime/maintenance-runner.ts +2 -0
  180. package/src/runtime/project-maintenance.ts +89 -0
  181. package/src/sdk.ts +334 -304
  182. package/src/search/intent-detector.ts +289 -289
  183. package/src/search/query-expansion.ts +52 -52
  184. package/src/server/formation-timeout.ts +27 -27
  185. package/src/server.ts +334 -93
  186. package/src/skills/mini-skills.ts +386 -386
  187. package/src/store/chat-store.ts +119 -119
  188. package/src/store/graph-store.ts +249 -249
  189. package/src/store/mini-skill-store.ts +349 -349
  190. package/src/store/orama-store.ts +61 -6
  191. package/src/store/persistence-json.ts +212 -212
  192. package/src/store/persistence.ts +291 -291
  193. package/src/store/project-affinity.ts +195 -195
  194. package/src/store/sqlite-db.ts +23 -1
  195. package/src/store/sqlite-store.ts +12 -2
  196. package/src/team/event-bus.ts +76 -76
  197. package/src/team/file-locks.ts +173 -173
  198. package/src/team/handoff.ts +168 -161
  199. package/src/team/messages.ts +203 -203
  200. package/src/team/poll.ts +132 -132
  201. package/src/team/tasks.ts +211 -211
  202. package/src/types.ts +51 -0
  203. package/src/wiki/generator.ts +2 -0
  204. package/src/workspace/mcp-adapters/codex.ts +191 -191
  205. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  206. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  207. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  208. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  209. package/src/workspace/mcp-adapters/trae.ts +134 -134
  210. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  211. package/src/workspace/sanitizer.ts +60 -60
  212. package/src/workspace/workflow-sync.ts +131 -131
package/dist/sdk.js CHANGED
@@ -32,6 +32,58 @@ var init_esm_shims = __esm({
32
32
  }
33
33
  });
34
34
 
35
+ // src/memory/visibility.ts
36
+ function resolveObservationVisibility(record) {
37
+ switch (record.visibility) {
38
+ case "personal":
39
+ case "team":
40
+ case "project":
41
+ return record.visibility;
42
+ default:
43
+ return "project";
44
+ }
45
+ }
46
+ function sharedAgentIds(record) {
47
+ if (Array.isArray(record.sharedWithAgentIds)) return record.sharedWithAgentIds;
48
+ if (typeof record.sharedWithAgentIds !== "string" || !record.sharedWithAgentIds) return [];
49
+ try {
50
+ const parsed = JSON.parse(record.sharedWithAgentIds);
51
+ return Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string") : [];
52
+ } catch {
53
+ return [];
54
+ }
55
+ }
56
+ function canReadObservation(record, reader) {
57
+ if (!reader) return true;
58
+ const sameProject = reader.projectId === record.projectId;
59
+ const visibility = resolveObservationVisibility(record);
60
+ if (visibility === "project") return !reader.projectId || sameProject;
61
+ if (!sameProject || !reader.agentId) return false;
62
+ if (visibility === "team") return reader.isTeamMember === true;
63
+ return record.createdByAgentId === reader.agentId || sharedAgentIds(record).includes(reader.agentId);
64
+ }
65
+ function canManageObservation(record, reader) {
66
+ if (!reader) return true;
67
+ if (!reader.projectId || reader.projectId !== record.projectId) return false;
68
+ switch (resolveObservationVisibility(record)) {
69
+ case "project":
70
+ return true;
71
+ case "team":
72
+ return reader.isTeamMember === true;
73
+ case "personal":
74
+ return Boolean(reader.agentId && record.createdByAgentId === reader.agentId);
75
+ }
76
+ }
77
+ function filterReadableObservations(observations2, reader) {
78
+ return reader ? observations2.filter((observation) => canReadObservation(observation, reader)) : [...observations2];
79
+ }
80
+ var init_visibility = __esm({
81
+ "src/memory/visibility.ts"() {
82
+ "use strict";
83
+ init_esm_shims();
84
+ }
85
+ });
86
+
35
87
  // src/store/bun-sqlite-compat.ts
36
88
  import { createRequire } from "module";
37
89
  function loadSqlite() {
@@ -208,7 +260,11 @@ CREATE TABLE IF NOT EXISTS observations (
208
260
  relatedCommits TEXT,
209
261
  relatedEntities TEXT,
210
262
  sourceDetail TEXT,
211
- valueCategory TEXT
263
+ valueCategory TEXT,
264
+ admissionState TEXT,
265
+ admissionReason TEXT,
266
+ visibility TEXT,
267
+ sharedWithAgentIds TEXT
212
268
  );
213
269
  `;
214
270
  CREATE_MINI_SKILLS_TABLE = `
@@ -631,6 +687,8 @@ CREATE INDEX IF NOT EXISTS idx_observations_projectId ON observations(projectId)
631
687
  CREATE INDEX IF NOT EXISTS idx_observations_topicKey ON observations(projectId, topicKey);
632
688
  CREATE INDEX IF NOT EXISTS idx_observations_status ON observations(status);
633
689
  CREATE INDEX IF NOT EXISTS idx_observations_project_status_id ON observations(projectId, status, id);
690
+ CREATE INDEX IF NOT EXISTS idx_observations_project_admission ON observations(projectId, status, admissionState, id);
691
+ CREATE INDEX IF NOT EXISTS idx_observations_project_visibility ON observations(projectId, status, visibility, id);
634
692
  CREATE INDEX IF NOT EXISTS idx_mini_skills_projectId ON mini_skills(projectId);
635
693
  CREATE INDEX IF NOT EXISTS idx_sessions_projectId ON sessions(projectId);
636
694
  CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(projectId, status);
@@ -675,6 +733,22 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_maintenance_jobs_active_dedupe
675
733
  WHERE status IN ('pending', 'running', 'retry');
676
734
  `;
677
735
  SCHEMA_MIGRATIONS = [
736
+ {
737
+ id: "1.2.2-observation-admission",
738
+ apply: (db2) => {
739
+ addColumnIfMissing(db2, "observations", "admissionState", "admissionState TEXT");
740
+ addColumnIfMissing(db2, "observations", "admissionReason", "admissionReason TEXT");
741
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_observations_project_admission ON observations(projectId, status, admissionState, id)");
742
+ }
743
+ },
744
+ {
745
+ id: "1.2.2-observation-visibility",
746
+ apply: (db2) => {
747
+ addColumnIfMissing(db2, "observations", "visibility", "visibility TEXT");
748
+ addColumnIfMissing(db2, "observations", "sharedWithAgentIds", "sharedWithAgentIds TEXT");
749
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_observations_project_visibility ON observations(projectId, status, visibility, id)");
750
+ }
751
+ },
678
752
  {
679
753
  id: "1.2-code-state-snapshots",
680
754
  apply: (db2) => {
@@ -5349,7 +5423,8 @@ __export(orama_store_exports, {
5349
5423
  makeOramaObservationId: () => makeOramaObservationId,
5350
5424
  removeObservation: () => removeObservation,
5351
5425
  resetDb: () => resetDb,
5352
- searchObservations: () => searchObservations
5426
+ searchObservations: () => searchObservations,
5427
+ updateObservationMetadata: () => updateObservationMetadata
5353
5428
  });
5354
5429
  import { create, insert as insert2, search, remove as remove2, update, count, getByID } from "@orama/orama";
5355
5430
  function getLastSearchMode(projectId) {
@@ -5427,6 +5502,11 @@ async function initializeDb(options, generation) {
5427
5502
  source: "string",
5428
5503
  sourceDetail: "string",
5429
5504
  valueCategory: "string",
5505
+ admissionState: "string",
5506
+ admissionReason: "string",
5507
+ visibility: "string",
5508
+ createdByAgentId: "string",
5509
+ sharedWithAgentIds: "string",
5430
5510
  documentType: "string",
5431
5511
  knowledgeLayer: "string"
5432
5512
  };
@@ -5565,6 +5645,13 @@ async function hydrateIndex(observations2, options = {}) {
5565
5645
  lastAccessedAt: obs.lastAccessedAt || "",
5566
5646
  status: obs.status ?? "active",
5567
5647
  source: obs.source || "agent",
5648
+ sourceDetail: obs.sourceDetail ?? "",
5649
+ valueCategory: obs.valueCategory ?? "",
5650
+ admissionState: obs.admissionState ?? "",
5651
+ admissionReason: obs.admissionReason ?? "",
5652
+ visibility: obs.visibility ?? "project",
5653
+ createdByAgentId: obs.createdByAgentId ?? "",
5654
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
5568
5655
  documentType: "observation",
5569
5656
  knowledgeLayer: resolveKnowledgeLayer("observation", obs.sourceDetail, obs.source),
5570
5657
  ...compatibleVector ? { embedding: compatibleVector } : {}
@@ -5598,6 +5685,16 @@ async function insertObservation(doc) {
5598
5685
  await insert2(database, doc);
5599
5686
  rememberObservationDoc(doc);
5600
5687
  }
5688
+ async function updateObservationMetadata(projectId, observationId2, patch) {
5689
+ const database = await getDb();
5690
+ const id = makeOramaObservationId(projectId, observationId2);
5691
+ const existing = getByID(database, id);
5692
+ if (!existing) return false;
5693
+ const next = { ...existing, ...patch };
5694
+ await update(database, id, next);
5695
+ rememberObservationDoc(next);
5696
+ return true;
5697
+ }
5601
5698
  async function removeObservation(oramaId) {
5602
5699
  const database = await getDb();
5603
5700
  await remove2(database, oramaId);
@@ -5751,7 +5848,7 @@ async function searchObservations(options) {
5751
5848
  if (!projectIds) return true;
5752
5849
  const doc = hit.document;
5753
5850
  return projectIds.includes(doc.projectId);
5754
- }).filter((hit) => {
5851
+ }).filter((hit) => canReadObservation(hit.document, options.reader)).filter((hit) => {
5755
5852
  if (statusFilter === "all") return true;
5756
5853
  const doc = hit.document;
5757
5854
  return (doc.status || "active") === statusFilter;
@@ -5782,6 +5879,8 @@ async function searchObservations(options) {
5782
5879
  source: doc.source || "agent",
5783
5880
  sourceDetail: doc.sourceDetail || void 0,
5784
5881
  valueCategory: doc.valueCategory || void 0,
5882
+ admissionState: doc.admissionState || void 0,
5883
+ visibility: doc.visibility || void 0,
5785
5884
  entityName: doc.entityName || void 0,
5786
5885
  documentType: doc.documentType || "observation",
5787
5886
  knowledgeLayer: doc.knowledgeLayer || "project-truth",
@@ -5825,6 +5924,12 @@ async function searchObservations(options) {
5825
5924
  score: isCommandStyleEntry(entry.title) ? entry.score * 0.3 : entry.score
5826
5925
  }));
5827
5926
  }
5927
+ if (hasQuery) {
5928
+ const qualifiedEntries = intermediate.filter(
5929
+ (entry) => entry.admissionState !== "candidate" && entry.admissionState !== "ephemeral"
5930
+ );
5931
+ if (qualifiedEntries.length > 0) intermediate = qualifiedEntries;
5932
+ }
5828
5933
  if (intentResult?.preferChronological) {
5829
5934
  intermediate.sort((a, b) => new Date(b.rawTime).getTime() - new Date(a.rawTime).getTime());
5830
5935
  } else {
@@ -5968,7 +6073,8 @@ async function searchObservations(options) {
5968
6073
  }
5969
6074
  let entries = intermediate.map(({ rawTime: _, _isCommandLog: _c, ...rest }) => rest);
5970
6075
  for (const hit of results.hits) {
5971
- rememberObservationDoc(hit.document);
6076
+ const doc = hit.document;
6077
+ if (canReadObservation(doc, options.reader)) rememberObservationDoc(doc);
5972
6078
  }
5973
6079
  if (hasQuery && originalQuery) {
5974
6080
  const queryLower = originalQuery.toLowerCase();
@@ -6012,7 +6118,8 @@ async function searchObservations(options) {
6012
6118
  entries = applyTokenBudget(entries, options.maxTokens);
6013
6119
  }
6014
6120
  if (options.trackAccess !== false) {
6015
- const hitDocs = results.hits.map((h) => ({ id: h.id, doc: h.document }));
6121
+ const returnedKeys = new Set(entries.map((entry) => makeEntryKey(entry.projectId, entry.id)));
6122
+ const hitDocs = results.hits.map((hit) => ({ id: hit.id, doc: hit.document })).filter(({ doc }) => returnedKeys.has(makeEntryKey(doc.projectId, doc.observationId)));
6016
6123
  recordAccessBatch(hitDocs).catch(() => {
6017
6124
  });
6018
6125
  }
@@ -6044,11 +6151,11 @@ async function getObservationsByIds(ids, projectId) {
6044
6151
  }
6045
6152
  return results;
6046
6153
  }
6047
- async function getTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3) {
6154
+ async function getTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3, reader) {
6048
6155
  const { withFreshIndex: withFreshIndex2 } = await Promise.resolve().then(() => (init_freshness(), freshness_exports));
6049
6156
  const { getAllObservations: getAllObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
6050
6157
  const rawObs = await withFreshIndex2(() => getAllObservations2());
6051
- const allObs = projectId ? rawObs.filter((o) => o.projectId === projectId) : rawObs;
6158
+ const allObs = (projectId ? rawObs.filter((o) => o.projectId === projectId) : rawObs).filter((observation) => canReadObservation(observation, reader));
6052
6159
  const sorted = allObs.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
6053
6160
  const anchorIndex = sorted.findIndex((o) => o.id === anchorId);
6054
6161
  if (anchorIndex === -1) {
@@ -6065,7 +6172,9 @@ async function getTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3)
6065
6172
  tokens: obs.tokens,
6066
6173
  source: obs.source || void 0,
6067
6174
  sourceDetail: obs.sourceDetail || void 0,
6068
- valueCategory: obs.valueCategory || void 0
6175
+ valueCategory: obs.valueCategory || void 0,
6176
+ admissionState: obs.admissionState || void 0,
6177
+ visibility: obs.visibility || void 0
6069
6178
  };
6070
6179
  };
6071
6180
  const before = sorted.slice(Math.max(0, anchorIndex - depthBefore), anchorIndex).map(toIndexEntry);
@@ -6133,6 +6242,7 @@ var init_orama_store = __esm({
6133
6242
  init_esm_shims();
6134
6243
  init_types();
6135
6244
  init_mini_skills();
6245
+ init_visibility();
6136
6246
  init_provider();
6137
6247
  init_project_affinity();
6138
6248
  init_intent_detector();
@@ -6189,6 +6299,10 @@ function obsToRow(obs) {
6189
6299
  relatedEntities: obs.relatedEntities ? JSON.stringify(obs.relatedEntities) : null,
6190
6300
  sourceDetail: obs.sourceDetail ?? null,
6191
6301
  valueCategory: obs.valueCategory ?? null,
6302
+ admissionState: obs.admissionState ?? null,
6303
+ admissionReason: obs.admissionReason ?? null,
6304
+ visibility: obs.visibility ?? null,
6305
+ sharedWithAgentIds: obs.sharedWithAgentIds ? JSON.stringify(obs.sharedWithAgentIds) : null,
6192
6306
  createdByAgentId: obs.createdByAgentId ?? null,
6193
6307
  writeGeneration: obs.writeGeneration ?? 0
6194
6308
  };
@@ -6219,6 +6333,10 @@ function rowToObs(row) {
6219
6333
  ...row.relatedEntities ? { relatedEntities: safeJsonParse3(row.relatedEntities, []) } : {},
6220
6334
  ...row.sourceDetail ? { sourceDetail: row.sourceDetail } : {},
6221
6335
  ...row.valueCategory ? { valueCategory: row.valueCategory } : {},
6336
+ ...row.admissionState ? { admissionState: row.admissionState } : {},
6337
+ ...row.admissionReason ? { admissionReason: row.admissionReason } : {},
6338
+ ...row.visibility ? { visibility: row.visibility } : {},
6339
+ ...row.sharedWithAgentIds ? { sharedWithAgentIds: safeJsonParse3(row.sharedWithAgentIds, []) } : {},
6222
6340
  ...row.createdByAgentId ? { createdByAgentId: row.createdByAgentId } : {},
6223
6341
  ...row.writeGeneration ? { writeGeneration: row.writeGeneration } : {}
6224
6342
  };
@@ -6270,12 +6388,14 @@ var init_sqlite_store = __esm({
6270
6388
  (id, entityName, type, title, narrative, facts, filesModified, concepts, tokens,
6271
6389
  createdAt, updatedAt, projectId, hasCausalLanguage, topicKey, revisionCount,
6272
6390
  sessionId, status, progress, source, commitHash, relatedCommits, relatedEntities,
6273
- sourceDetail, valueCategory, createdByAgentId, writeGeneration)
6391
+ sourceDetail, valueCategory, admissionState, admissionReason, visibility, sharedWithAgentIds,
6392
+ createdByAgentId, writeGeneration)
6274
6393
  VALUES
6275
6394
  (@id, @entityName, @type, @title, @narrative, @facts, @filesModified, @concepts, @tokens,
6276
6395
  @createdAt, @updatedAt, @projectId, @hasCausalLanguage, @topicKey, @revisionCount,
6277
6396
  @sessionId, @status, @progress, @source, @commitHash, @relatedCommits, @relatedEntities,
6278
- @sourceDetail, @valueCategory, @createdByAgentId, @writeGeneration)
6397
+ @sourceDetail, @valueCategory, @admissionState, @admissionReason, @visibility, @sharedWithAgentIds,
6398
+ @createdByAgentId, @writeGeneration)
6279
6399
  `);
6280
6400
  this.stmtUpdate = this.stmtInsert;
6281
6401
  this.stmtSetStatus = this.db.prepare(`UPDATE observations SET status = ? WHERE id = ?`);
@@ -6827,6 +6947,7 @@ var init_maintenance_jobs = __esm({
6827
6947
  "retention-archive",
6828
6948
  "consolidation",
6829
6949
  "codegraph-refresh",
6950
+ "observation-qualify",
6830
6951
  "claim-derive",
6831
6952
  "claim-requalification",
6832
6953
  "knowledge-compile",
@@ -7185,7 +7306,8 @@ __export(lifecycle_exports, {
7185
7306
  enqueueClaimDerivation: () => enqueueClaimDerivation,
7186
7307
  enqueueClaimRequalification: () => enqueueClaimRequalification,
7187
7308
  enqueueCodegraphRefresh: () => enqueueCodegraphRefresh,
7188
- enqueueKnowledgeFollowups: () => enqueueKnowledgeFollowups
7309
+ enqueueKnowledgeFollowups: () => enqueueKnowledgeFollowups,
7310
+ enqueueObservationQualification: () => enqueueObservationQualification
7189
7311
  });
7190
7312
  function queueFor(input) {
7191
7313
  return input.queue ?? new MaintenanceJobStore(input.dataDir);
@@ -7220,6 +7342,14 @@ function enqueueClaimDerivation(input) {
7220
7342
  payload: { observationId: input.observationId }
7221
7343
  });
7222
7344
  }
7345
+ function enqueueObservationQualification(input) {
7346
+ queueFor(input).enqueue({
7347
+ projectId: input.projectId,
7348
+ kind: "observation-qualify",
7349
+ dedupeKey: "observation-qualify",
7350
+ payload: { source: input.source, limit: 100 }
7351
+ });
7352
+ }
7223
7353
  function enqueueKnowledgeFollowups(input) {
7224
7354
  const queue = queueFor(input);
7225
7355
  const payload = {
@@ -8084,6 +8214,7 @@ __export(observations_exports, {
8084
8214
  resolveObservations: () => resolveObservations,
8085
8215
  storeObservation: () => storeObservation,
8086
8216
  suggestTopicKey: () => suggestTopicKey,
8217
+ updateObservationAdmission: () => updateObservationAdmission,
8087
8218
  withFreshObservations: () => withFreshObservations
8088
8219
  });
8089
8220
  function logEmbeddingFailureOnce(key, message) {
@@ -8150,6 +8281,18 @@ function queueClaimDerivation(observation) {
8150
8281
  } catch {
8151
8282
  }
8152
8283
  }
8284
+ function queueObservationQualification(observation) {
8285
+ const dataDir = projectDir;
8286
+ if (!dataDir || observation.admissionState !== "candidate") return;
8287
+ try {
8288
+ enqueueObservationQualification({
8289
+ dataDir,
8290
+ projectId: observation.projectId,
8291
+ source: "automatic-capture:" + observation.id
8292
+ });
8293
+ } catch {
8294
+ }
8295
+ }
8153
8296
  function isVectorCompatibleWithCurrentIndex(embedding) {
8154
8297
  if (!embedding) return false;
8155
8298
  const vectorDimensions = getVectorDimensions();
@@ -8205,6 +8348,9 @@ async function storeObservation(input) {
8205
8348
  (o) => o.topicKey === input.topicKey && o.projectId === input.projectId
8206
8349
  );
8207
8350
  if (existing) {
8351
+ if (input.visibilityReader && !canManageObservation(existing, input.visibilityReader)) {
8352
+ throw new Error("Cannot update a memory outside this session's write scope.");
8353
+ }
8208
8354
  return { observation: await upsertObservation(existing, input, now3), upserted: true };
8209
8355
  }
8210
8356
  }
@@ -8241,6 +8387,9 @@ async function storeObservation(input) {
8241
8387
  if (input.topicKey) {
8242
8388
  const diskExisting = await tx.findByTopicKey(input.projectId, input.topicKey);
8243
8389
  if (diskExisting) {
8390
+ if (input.visibilityReader && !canManageObservation(diskExisting, input.visibilityReader)) {
8391
+ throw new Error("Cannot update a memory outside this session's write scope.");
8392
+ }
8244
8393
  upsertedInsideLock = true;
8245
8394
  observation = diskExisting;
8246
8395
  return;
@@ -8271,6 +8420,10 @@ async function storeObservation(input) {
8271
8420
  relatedEntities: input.relatedEntities,
8272
8421
  sourceDetail: input.sourceDetail,
8273
8422
  valueCategory: input.valueCategory,
8423
+ admissionState: input.admissionState,
8424
+ admissionReason: input.admissionReason ? sanitizeCredentials(input.admissionReason) : void 0,
8425
+ visibility: input.visibility ?? "project",
8426
+ sharedWithAgentIds: input.sharedWithAgentIds,
8274
8427
  createdByAgentId: input.createdByAgentId,
8275
8428
  // Predict the generation that atomic() will commit after this callback.
8276
8429
  // bumpGeneration() runs after fn(tx) returns, incrementing by 1.
@@ -8317,6 +8470,10 @@ async function storeObservation(input) {
8317
8470
  relatedEntities: input.relatedEntities,
8318
8471
  sourceDetail: input.sourceDetail,
8319
8472
  valueCategory: input.valueCategory,
8473
+ admissionState: input.admissionState,
8474
+ admissionReason: input.admissionReason ? sanitizeCredentials(input.admissionReason) : void 0,
8475
+ visibility: input.visibility ?? "project",
8476
+ sharedWithAgentIds: input.sharedWithAgentIds,
8320
8477
  createdByAgentId: input.createdByAgentId,
8321
8478
  writeGeneration: 0
8322
8479
  };
@@ -8340,7 +8497,12 @@ async function storeObservation(input) {
8340
8497
  status: "active",
8341
8498
  source: input.source ?? "agent",
8342
8499
  sourceDetail: input.sourceDetail ?? "",
8343
- valueCategory: input.valueCategory ?? ""
8500
+ valueCategory: input.valueCategory ?? "",
8501
+ admissionState: input.admissionState ?? "",
8502
+ admissionReason: input.admissionReason ? sanitizeCredentials(input.admissionReason) : "",
8503
+ visibility: input.visibility ?? "project",
8504
+ createdByAgentId: input.createdByAgentId ?? "",
8505
+ sharedWithAgentIds: JSON.stringify(input.sharedWithAgentIds ?? [])
8344
8506
  };
8345
8507
  await insertObservation(doc);
8346
8508
  };
@@ -8349,7 +8511,10 @@ async function storeObservation(input) {
8349
8511
  return { observation: await upsertObservation(observation, input, now3), upserted: true };
8350
8512
  }
8351
8513
  await bindObservationCodeRefsBestEffort(observation);
8352
- queueClaimDerivation(observation);
8514
+ queueObservationQualification(observation);
8515
+ if (resolveObservationVisibility(observation) === "project") {
8516
+ queueClaimDerivation(observation);
8517
+ }
8353
8518
  const obsId = observation.id;
8354
8519
  vectorMissingIds.add(obsId);
8355
8520
  const searchableText = [input.title, input.narrative, ...input.facts ?? []].join(" ");
@@ -8422,6 +8587,10 @@ async function upsertObservation(existing, input, now3) {
8422
8587
  if (input.progress) existing.progress = input.progress;
8423
8588
  if (input.sourceDetail !== void 0) existing.sourceDetail = input.sourceDetail;
8424
8589
  if (input.valueCategory !== void 0) existing.valueCategory = input.valueCategory;
8590
+ if (input.admissionState !== void 0) existing.admissionState = input.admissionState;
8591
+ if (input.admissionReason !== void 0) existing.admissionReason = sanitizeCredentials(input.admissionReason);
8592
+ if (input.visibility !== void 0) existing.visibility = input.visibility;
8593
+ if (input.sharedWithAgentIds !== void 0) existing.sharedWithAgentIds = input.sharedWithAgentIds;
8425
8594
  const doc = {
8426
8595
  id: makeOramaObservationId(existing.projectId, existing.id),
8427
8596
  observationId: existing.id,
@@ -8440,7 +8609,12 @@ async function upsertObservation(existing, input, now3) {
8440
8609
  status: "active",
8441
8610
  source: existing.source ?? "agent",
8442
8611
  sourceDetail: existing.sourceDetail ?? "",
8443
- valueCategory: existing.valueCategory ?? ""
8612
+ valueCategory: existing.valueCategory ?? "",
8613
+ admissionState: existing.admissionState ?? "",
8614
+ admissionReason: existing.admissionReason ?? "",
8615
+ visibility: existing.visibility ?? "project",
8616
+ createdByAgentId: existing.createdByAgentId ?? "",
8617
+ sharedWithAgentIds: JSON.stringify(existing.sharedWithAgentIds ?? [])
8444
8618
  };
8445
8619
  const oramaId = makeOramaObservationId(existing.projectId, existing.id);
8446
8620
  try {
@@ -8463,7 +8637,10 @@ async function upsertObservation(existing, input, now3) {
8463
8637
  await store.update(existing);
8464
8638
  }
8465
8639
  await bindObservationCodeRefsBestEffort(existing);
8466
- queueClaimDerivation(existing);
8640
+ queueObservationQualification(existing);
8641
+ if (resolveObservationVisibility(existing) === "project") {
8642
+ queueClaimDerivation(existing);
8643
+ }
8467
8644
  const searchableText = [input.title, input.narrative, ...input.facts ?? []].join(" ");
8468
8645
  const obsId = existing.id;
8469
8646
  vectorMissingIds.add(obsId);
@@ -8502,6 +8679,56 @@ async function upsertObservation(existing, input, now3) {
8502
8679
  function getObservation(id, projectId) {
8503
8680
  return observations.find((o) => o.id === id && (projectId ? o.projectId === projectId : true));
8504
8681
  }
8682
+ async function updateObservationAdmission(input) {
8683
+ await ensureFreshObservations();
8684
+ const cached = observations.find(
8685
+ (observation) => observation.id === input.observationId && (!input.projectId || observation.projectId === input.projectId)
8686
+ );
8687
+ if (!cached) return void 0;
8688
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
8689
+ const admissionReason = sanitizeCredentials(input.admissionReason).slice(0, 240);
8690
+ const apply = (current) => {
8691
+ if (input.projectId && current.projectId !== input.projectId) return void 0;
8692
+ if (input.expectedState && current.admissionState !== input.expectedState) return void 0;
8693
+ return {
8694
+ ...current,
8695
+ admissionState: input.admissionState,
8696
+ admissionReason,
8697
+ ...input.visibility !== void 0 ? { visibility: input.visibility } : {},
8698
+ updatedAt: now3
8699
+ };
8700
+ };
8701
+ let updated;
8702
+ if (projectDir) {
8703
+ const store = getObservationStore();
8704
+ await store.atomic(async (tx) => {
8705
+ const current = await tx.getById(input.observationId);
8706
+ if (!current) return;
8707
+ const next = apply(current);
8708
+ if (!next) return;
8709
+ await tx.update(next);
8710
+ updated = next;
8711
+ });
8712
+ } else {
8713
+ updated = apply(cached);
8714
+ }
8715
+ if (!updated) return void 0;
8716
+ observations = observations.map(
8717
+ (observation) => observation.id === updated.id && observation.projectId === updated.projectId ? updated : observation
8718
+ );
8719
+ try {
8720
+ await updateObservationMetadata(updated.projectId, updated.id, {
8721
+ admissionState: updated.admissionState,
8722
+ admissionReason: updated.admissionReason,
8723
+ ...updated.visibility !== void 0 ? { visibility: updated.visibility } : {}
8724
+ });
8725
+ } catch {
8726
+ }
8727
+ if (updated.admissionState === "qualified" && resolveObservationVisibility(updated) === "project") {
8728
+ queueClaimDerivation(updated);
8729
+ }
8730
+ return updated;
8731
+ }
8505
8732
  async function resolveObservations(ids, status = "resolved") {
8506
8733
  const resolved = [];
8507
8734
  const notFound = [];
@@ -8541,7 +8768,12 @@ async function resolveObservations(ids, status = "resolved") {
8541
8768
  status,
8542
8769
  source: obs.source ?? "agent",
8543
8770
  sourceDetail: obs.sourceDetail ?? "",
8544
- valueCategory: obs.valueCategory ?? ""
8771
+ valueCategory: obs.valueCategory ?? "",
8772
+ admissionState: obs.admissionState ?? "",
8773
+ admissionReason: obs.admissionReason ?? "",
8774
+ visibility: obs.visibility ?? "project",
8775
+ createdByAgentId: obs.createdByAgentId ?? "",
8776
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? [])
8545
8777
  };
8546
8778
  await insertObservation(doc);
8547
8779
  const obsId = obs.id;
@@ -8671,6 +8903,11 @@ async function reindexObservations() {
8671
8903
  source: obs.source ?? "agent",
8672
8904
  sourceDetail: obs.sourceDetail ?? "",
8673
8905
  valueCategory: obs.valueCategory ?? "",
8906
+ admissionState: obs.admissionState ?? "",
8907
+ admissionReason: obs.admissionReason ?? "",
8908
+ visibility: obs.visibility ?? "project",
8909
+ createdByAgentId: obs.createdByAgentId ?? "",
8910
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
8674
8911
  ...compatibleEmbedding ? { embedding: compatibleEmbedding } : {}
8675
8912
  };
8676
8913
  await insertObservation(doc);
@@ -8743,6 +8980,7 @@ async function probeSearchIndex(projectId) {
8743
8980
  await searchObservations({
8744
8981
  query: "semantic memory retrieval status",
8745
8982
  projectId,
8983
+ reader: { projectId },
8746
8984
  limit: 1,
8747
8985
  status: "all",
8748
8986
  trackAccess: false
@@ -8808,6 +9046,11 @@ async function backfillVectorEmbeddings(options = {}) {
8808
9046
  source: obs.source ?? "agent",
8809
9047
  sourceDetail: obs.sourceDetail ?? "",
8810
9048
  valueCategory: obs.valueCategory ?? "",
9049
+ admissionState: obs.admissionState ?? "",
9050
+ admissionReason: obs.admissionReason ?? "",
9051
+ visibility: obs.visibility ?? "project",
9052
+ createdByAgentId: obs.createdByAgentId ?? "",
9053
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
8811
9054
  embedding
8812
9055
  };
8813
9056
  await insertObservation(doc);
@@ -8849,6 +9092,7 @@ var init_observations = __esm({
8849
9092
  init_provider();
8850
9093
  init_secret_filter();
8851
9094
  init_lifecycle();
9095
+ init_visibility();
8852
9096
  observations = [];
8853
9097
  nextId = 1;
8854
9098
  projectDir = null;
@@ -9066,6 +9310,36 @@ var init_session_store = __esm({
9066
9310
  }
9067
9311
  });
9068
9312
 
9313
+ // src/memory/graph-scope.ts
9314
+ function projectGraphEntityNames(observations2) {
9315
+ const entityNames = /* @__PURE__ */ new Set();
9316
+ for (const observation of observations2) {
9317
+ if ((observation.status ?? "active") !== "active") continue;
9318
+ const entityName = observation.entityName?.trim();
9319
+ if (entityName) entityNames.add(entityName);
9320
+ for (const relatedEntity of observation.relatedEntities ?? []) {
9321
+ const name = relatedEntity.trim();
9322
+ if (name) entityNames.add(name);
9323
+ }
9324
+ }
9325
+ return entityNames;
9326
+ }
9327
+ function scopeKnowledgeGraphToProject(graph, observations2) {
9328
+ const entityNames = projectGraphEntityNames(observations2);
9329
+ const entities = graph.entities.filter((entity) => entityNames.has(entity.name));
9330
+ const visibleNames = new Set(entities.map((entity) => entity.name));
9331
+ const relations = graph.relations.filter(
9332
+ (relation) => visibleNames.has(relation.from) && visibleNames.has(relation.to)
9333
+ );
9334
+ return { entities, relations, entityNames };
9335
+ }
9336
+ var init_graph_scope = __esm({
9337
+ "src/memory/graph-scope.ts"() {
9338
+ "use strict";
9339
+ init_esm_shims();
9340
+ }
9341
+ });
9342
+
9069
9343
  // src/memory/disclosure-policy.ts
9070
9344
  function resolveEvidenceBasis(fields) {
9071
9345
  if (fields.commitHash || fields.source === "git" || fields.sourceDetail === "git-ingest") {
@@ -9098,8 +9372,9 @@ function resolveSourceDetail(sourceDetail, source) {
9098
9372
  return void 0;
9099
9373
  }
9100
9374
  function classifyLayer(fields) {
9101
- const { valueCategory } = fields;
9375
+ const { valueCategory, admissionState } = fields;
9102
9376
  const sd = resolveSourceDetail(fields.sourceDetail, fields.source);
9377
+ if (admissionState === "candidate" || admissionState === "ephemeral") return "L1";
9103
9378
  if (valueCategory === "core") return "L2";
9104
9379
  if (sd === "hook") return "L1";
9105
9380
  if (sd === "git-ingest") return "L3";
@@ -9157,6 +9432,7 @@ function getEffectiveRetentionDays(doc) {
9157
9432
  }
9158
9433
  function isImmune(doc) {
9159
9434
  if (doc.type === "probe") return false;
9435
+ if (doc.admissionState === "candidate" || doc.admissionState === "ephemeral") return false;
9160
9436
  if (doc.valueCategory === "core") return true;
9161
9437
  const importance = getImportanceLevel(doc);
9162
9438
  if (importance === "critical") return true;
@@ -9166,6 +9442,7 @@ function isImmune(doc) {
9166
9442
  }
9167
9443
  function getImmunityReason(doc) {
9168
9444
  if (doc.type === "probe") return null;
9445
+ if (doc.admissionState === "candidate" || doc.admissionState === "ephemeral") return null;
9169
9446
  if (doc.valueCategory === "core") return "core valueCategory (formation-classified)";
9170
9447
  const importance = getImportanceLevel(doc);
9171
9448
  if (importance === "critical") return "critical importance";
@@ -9298,7 +9575,9 @@ function toRetentionDocument(obs, accessMap) {
9298
9575
  status: obs.status ?? "active",
9299
9576
  source: obs.source ?? "agent",
9300
9577
  sourceDetail: obs.sourceDetail ?? "",
9301
- valueCategory: obs.valueCategory ?? ""
9578
+ valueCategory: obs.valueCategory ?? "",
9579
+ admissionState: obs.admissionState ?? "",
9580
+ admissionReason: obs.admissionReason ?? ""
9302
9581
  };
9303
9582
  }
9304
9583
  async function archiveExpiredBatch(_projectDir, options) {
@@ -9312,7 +9591,7 @@ async function archiveExpiredBatch(_projectDir, options) {
9312
9591
  });
9313
9592
  const hasMore = page.length > limit;
9314
9593
  const scanned = hasMore ? page.slice(0, limit) : page;
9315
- const candidateIds = scanned.filter((observation) => getRetentionZone(
9594
+ const candidateIds = scanned.filter((observation) => !options.reader || canManageObservation(observation, options.reader)).filter((observation) => getRetentionZone(
9316
9595
  toRetentionDocument(observation, options.accessMap),
9317
9596
  options.referenceTime
9318
9597
  ) === "archive-candidate").map((observation) => observation.id);
@@ -9325,7 +9604,7 @@ async function archiveExpiredBatch(_projectDir, options) {
9325
9604
  const nextCursor = hasMore && scanned.length > 0 ? scanned[scanned.length - 1].id : void 0;
9326
9605
  return nextCursor === void 0 ? { archived, scanned: scanned.length } : { archived, scanned: scanned.length, nextCursor };
9327
9606
  }
9328
- async function archiveExpired(projectDir2, referenceTime, accessMap, projectId) {
9607
+ async function archiveExpired(projectDir2, referenceTime, accessMap, projectId, reader) {
9329
9608
  const store = getObservationStore();
9330
9609
  if (projectId) {
9331
9610
  let afterId;
@@ -9335,12 +9614,14 @@ async function archiveExpired(projectDir2, referenceTime, accessMap, projectId)
9335
9614
  projectId,
9336
9615
  afterId,
9337
9616
  referenceTime,
9338
- accessMap
9617
+ accessMap,
9618
+ reader
9339
9619
  });
9340
9620
  archived += batch.archived;
9341
9621
  afterId = batch.nextCursor;
9342
9622
  } while (afterId !== void 0);
9343
- const remaining = await store.countByProject(projectId, { status: "active" });
9623
+ const remainingObservations = await store.loadByProject(projectId, { status: "active" });
9624
+ const remaining = reader ? remainingObservations.filter((observation) => canManageObservation(observation, reader)).length : remainingObservations.length;
9344
9625
  return { archived, remaining };
9345
9626
  }
9346
9627
  return await store.atomic(async (tx) => {
@@ -9370,6 +9651,7 @@ var init_retention = __esm({
9370
9651
  "use strict";
9371
9652
  init_esm_shims();
9372
9653
  init_obs_store();
9654
+ init_visibility();
9373
9655
  RETENTION_DAYS = {
9374
9656
  critical: 365,
9375
9657
  high: 180,
@@ -9405,6 +9687,49 @@ var init_retention = __esm({
9405
9687
  }
9406
9688
  });
9407
9689
 
9690
+ // src/memory/admission.ts
9691
+ var admission_exports = {};
9692
+ __export(admission_exports, {
9693
+ isCandidateObservation: () => isCandidateObservation,
9694
+ isEligibleForAutomaticDelivery: () => isEligibleForAutomaticDelivery,
9695
+ isEligibleForKnowledgePromotion: () => isEligibleForKnowledgePromotion,
9696
+ qualifyCandidateFromCurrentCode: () => qualifyCandidateFromCurrentCode
9697
+ });
9698
+ function isEligibleForAutomaticDelivery(observation) {
9699
+ return observation.admissionState !== "ephemeral" && observation.admissionState !== "candidate";
9700
+ }
9701
+ function isCandidateObservation(observation) {
9702
+ return observation.admissionState === "candidate";
9703
+ }
9704
+ function isEligibleForKnowledgePromotion(observation) {
9705
+ return isEligibleForAutomaticDelivery(observation) && observation.valueCategory !== "ephemeral" && resolveObservationVisibility(observation) === "project";
9706
+ }
9707
+ function qualifyCandidateFromCurrentCode(input) {
9708
+ if (!isCandidateObservation(input.observation)) return void 0;
9709
+ if (input.observation.valueCategory === "ephemeral") return void 0;
9710
+ if (input.currentCodeReferenceCount <= 0) return void 0;
9711
+ const typeLabel = DURABLE_AUTOMATIC_TYPES.has(input.observation.type) ? "high-value automatic record" : "automatic record";
9712
+ return {
9713
+ admissionState: "qualified",
9714
+ admissionReason: `${typeLabel} qualified against ${input.currentCodeReferenceCount} current Code Memory reference(s)`
9715
+ };
9716
+ }
9717
+ var DURABLE_AUTOMATIC_TYPES;
9718
+ var init_admission = __esm({
9719
+ "src/memory/admission.ts"() {
9720
+ "use strict";
9721
+ init_esm_shims();
9722
+ init_visibility();
9723
+ DURABLE_AUTOMATIC_TYPES = /* @__PURE__ */ new Set([
9724
+ "decision",
9725
+ "gotcha",
9726
+ "problem-solution",
9727
+ "trade-off",
9728
+ "why-it-exists"
9729
+ ]);
9730
+ }
9731
+ });
9732
+
9408
9733
  // src/workspace/workflow-sync.ts
9409
9734
  import matter9 from "gray-matter";
9410
9735
  var WorkflowSyncer;
@@ -10805,6 +11130,7 @@ var init_isolated_maintenance = __esm({
10805
11130
  "retention-archive",
10806
11131
  "consolidation",
10807
11132
  "codegraph-refresh",
11133
+ "observation-qualify",
10808
11134
  "claim-derive",
10809
11135
  "claim-requalification",
10810
11136
  "knowledge-compile",
@@ -11240,9 +11566,10 @@ async function loadConsolidationPage(projectId, options) {
11240
11566
  return hasMore && observations2.length > 0 ? { observations: observations2, nextCursor: observations2[observations2.length - 1].id } : { observations: observations2 };
11241
11567
  }
11242
11568
  function findClusters(observations2, threshold) {
11243
- if (observations2.length < MIN_CLUSTER_SIZE) return [];
11569
+ const eligible = observations2.filter(isEligibleForAutomaticDelivery).filter((observation) => resolveObservationVisibility(observation) === "project");
11570
+ if (eligible.length < MIN_CLUSTER_SIZE) return [];
11244
11571
  const groups = /* @__PURE__ */ new Map();
11245
- for (const obs of observations2) {
11572
+ for (const obs of eligible) {
11246
11573
  const key = `${obs.entityName}::${obs.type}`;
11247
11574
  const group = groups.get(key) ?? [];
11248
11575
  group.push(obs);
@@ -11379,6 +11706,8 @@ var init_consolidation = __esm({
11379
11706
  "use strict";
11380
11707
  init_esm_shims();
11381
11708
  init_obs_store();
11709
+ init_admission();
11710
+ init_visibility();
11382
11711
  DEFAULT_SIMILARITY_THRESHOLD = 0.45;
11383
11712
  HIGH_VALUE_SIMILARITY_THRESHOLD = 0.85;
11384
11713
  HIGH_VALUE_TYPES = /* @__PURE__ */ new Set(["gotcha", "decision", "trade-off", "reasoning", "problem-solution"]);
@@ -12178,6 +12507,7 @@ __export(claims_exports, {
12178
12507
  buildClaimIdentity: () => buildClaimIdentity,
12179
12508
  deriveLowRiskClaimsFromObservation: () => deriveLowRiskClaimsFromObservation,
12180
12509
  requalifyClaimsForCodeState: () => requalifyClaimsForCodeState,
12510
+ reviewClaim: () => reviewClaim,
12181
12511
  selectClaimsForTask: () => selectClaimsForTask,
12182
12512
  supersedeClaim: () => supersedeClaim,
12183
12513
  writeClaim: () => writeClaim
@@ -12364,6 +12694,39 @@ function writeClaim(store, input) {
12364
12694
  };
12365
12695
  });
12366
12696
  }
12697
+ function reviewClaim(store, input) {
12698
+ if (input.reviewState !== "approved" && input.reviewState !== "rejected") {
12699
+ throw new Error("A claim review must approve or reject the claim");
12700
+ }
12701
+ const detail = compactText(input.detail, "review detail");
12702
+ return store.transaction(() => {
12703
+ const claim = store.getClaim(input.claimId);
12704
+ if (!claim) throw new Error("Claim was not found");
12705
+ if (input.reviewState === "approved" && claim.status === "superseded") {
12706
+ throw new Error("A superseded claim cannot be approved");
12707
+ }
12708
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
12709
+ const nextStatus = input.reviewState === "rejected" ? "unknown" : claim.status === "unknown" && claim.reviewState === "rejected" ? "active" : claim.status;
12710
+ const updated = {
12711
+ ...claim,
12712
+ status: nextStatus,
12713
+ reviewState: input.reviewState,
12714
+ updatedAt: now3
12715
+ };
12716
+ store.updateClaim(updated);
12717
+ store.recordEvent({
12718
+ projectId: claim.projectId,
12719
+ claimId: claim.id,
12720
+ kind: "reviewed",
12721
+ fromStatus: claim.status,
12722
+ toStatus: updated.status,
12723
+ detail: "Review: " + claim.reviewState + " -> " + input.reviewState + ". " + detail,
12724
+ createdAt: now3
12725
+ });
12726
+ reconcileConflicts(store, claim.projectId, claim.conflictKey, now3);
12727
+ return store.getClaim(claim.id) ?? updated;
12728
+ });
12729
+ }
12367
12730
  function supersedeClaim(store, input) {
12368
12731
  const evidence = validateEvidence(input.evidence);
12369
12732
  return store.transaction(() => {
@@ -12452,7 +12815,7 @@ function deriveLowRiskClaimsFromObservation(store, observation, codeStore) {
12452
12815
  scope: "project",
12453
12816
  confidence,
12454
12817
  observedAt: observation.updatedAt ?? observation.createdAt,
12455
- reviewState: "approved",
12818
+ reviewState: isGit ? "approved" : "needs-review",
12456
12819
  origin: isGit ? "git" : "derived",
12457
12820
  evidence
12458
12821
  });
@@ -13328,81 +13691,425 @@ var init_workflow_store = __esm({
13328
13691
  }
13329
13692
  });
13330
13693
 
13331
- // src/knowledge/workflows.ts
13332
- var workflows_exports = {};
13333
- __export(workflows_exports, {
13334
- WORKFLOW_ADAPTER_TARGETS: () => WORKFLOW_ADAPTER_TARGETS,
13335
- applyWorkflowAdapter: () => applyWorkflowAdapter,
13336
- importWindsurfWorkflows: () => importWindsurfWorkflows,
13337
- parseWorkflowMarkdown: () => parseWorkflowMarkdown,
13338
- previewWorkflowAdapter: () => previewWorkflowAdapter,
13339
- recordWorkflowRun: () => recordWorkflowRun,
13340
- renderWorkflowMarkdown: () => renderWorkflowMarkdown,
13341
- selectWorkflows: () => selectWorkflows,
13342
- selectWorkspaceWorkflows: () => selectWorkspaceWorkflows,
13343
- syncCanonicalWorkflows: () => syncCanonicalWorkflows,
13344
- writeCanonicalWorkflow: () => writeCanonicalWorkflow
13694
+ // src/codegraph/task-lens.ts
13695
+ var task_lens_exports = {};
13696
+ __export(task_lens_exports, {
13697
+ containsTaskKeyword: () => containsTaskKeyword,
13698
+ lensPathCandidates: () => lensPathCandidates,
13699
+ lensVerificationHints: () => lensVerificationHints,
13700
+ rankLensPaths: () => rankLensPaths,
13701
+ rankLensSources: () => rankLensSources,
13702
+ resolveTaskLens: () => resolveTaskLens,
13703
+ scoreLensSource: () => scoreLensSource,
13704
+ shouldShowLensSource: () => shouldShowLensSource
13345
13705
  });
13346
- import { createHash as createHash12 } from "crypto";
13347
- import { promises as fs14 } from "fs";
13348
- import path17 from "path";
13349
- import matter11 from "gray-matter";
13350
- function hash3(value) {
13351
- return createHash12("sha256").update(value).digest("hex");
13352
- }
13353
- function now2() {
13354
- return (/* @__PURE__ */ new Date()).toISOString();
13355
- }
13356
- function slug(value) {
13357
- const normalized = value.trim().toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72);
13358
- return normalized || "workflow";
13706
+ function normalizePath2(path25) {
13707
+ return path25.replace(/\\/g, "/");
13359
13708
  }
13360
- function titleFromName(name) {
13361
- return name.replace(/[-_]+/g, " ").split(" ").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ") || "Imported Workflow";
13709
+ function tokenize2(text) {
13710
+ return (text.toLowerCase().match(/[a-z0-9_./-]+|[\u4e00-\u9fff]+/g) ?? []).map((token) => token.trim()).filter((token) => token.length > 1 && !STOP_WORDS.has(token));
13362
13711
  }
13363
- function requiredText(data, key) {
13364
- const value = data[key];
13365
- if (typeof value !== "string" || !value.trim()) {
13366
- throw new Error("workflow frontmatter requires " + key);
13712
+ function containsTaskKeyword(text, keyword) {
13713
+ const matcher = /^[a-z0-9_-]+$/i.test(keyword) ? new RegExp(`(^|[^a-z0-9_-])${keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?=[^a-z0-9_-]|$)`, "gi") : new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
13714
+ let match;
13715
+ while ((match = matcher.exec(text)) !== null) {
13716
+ const keywordIndex = match.index + (match[1]?.length ?? 0);
13717
+ if (!isNegatedTaskKeyword(text, keywordIndex)) return true;
13367
13718
  }
13368
- return value.trim();
13719
+ return false;
13369
13720
  }
13370
- function optionalText4(data, key) {
13371
- const value = data[key];
13372
- if (value === void 0 || value === null || value === "") return void 0;
13373
- if (typeof value !== "string") throw new Error("workflow frontmatter field " + key + " must be text");
13374
- return value.trim() || void 0;
13721
+ function isNegatedTaskKeyword(text, keywordIndex) {
13722
+ const boundary = Math.max(
13723
+ text.lastIndexOf(".", keywordIndex - 1),
13724
+ text.lastIndexOf("!", keywordIndex - 1),
13725
+ text.lastIndexOf("?", keywordIndex - 1),
13726
+ text.lastIndexOf(";", keywordIndex - 1),
13727
+ text.lastIndexOf("\n", keywordIndex - 1),
13728
+ text.lastIndexOf("\u3002", keywordIndex - 1),
13729
+ text.lastIndexOf("\uFF01", keywordIndex - 1),
13730
+ text.lastIndexOf("\uFF1F", keywordIndex - 1),
13731
+ text.lastIndexOf("\uFF1B", keywordIndex - 1)
13732
+ );
13733
+ let prefix = text.slice(boundary + 1, keywordIndex).toLowerCase();
13734
+ const contrast = /(?:,|\uff0c)\s*(?:but|however|instead|yet|\u4f46\u662f|\u4f46|\u800c\u662f)\s*/gi;
13735
+ let contrastMatch;
13736
+ let contrastEnd = -1;
13737
+ while ((contrastMatch = contrast.exec(prefix)) !== null) contrastEnd = contrast.lastIndex;
13738
+ if (contrastEnd >= 0) prefix = prefix.slice(contrastEnd);
13739
+ if (/\b(?:do|does|did|should|must|can|could|will|would|may|might)\s+not\b/.test(prefix)) return true;
13740
+ if (/\b(?:don't|dont|never|without|avoid|skip)\b/.test(prefix)) return true;
13741
+ if (/(?:^|[\s,])no\s+(?:[a-z0-9_-]+\s*){0,4}$/i.test(prefix)) return true;
13742
+ const chineseClauseStart = Math.max(prefix.lastIndexOf(","), prefix.lastIndexOf("\uFF0C"), prefix.lastIndexOf("\u3001"));
13743
+ const chineseClause = prefix.slice(chineseClauseStart + 1);
13744
+ return /(?:\u4e0d\u8981|\u4e0d\u5e94|\u4e0d\u53ef|\u4e0d\u80fd|\u4e0d\u4f1a|\u7981\u6b62|\u52ff|\u4e0d)\s*(?:\u7acb\u5373|\u76f4\u63a5|\u518d|\u73b0\u5728|\u64c5\u81ea|\u5148)?\s*$/.test(chineseClause);
13375
13745
  }
13376
- function optionalStringArray(data, key) {
13377
- const value = data[key];
13378
- if (value === void 0 || value === null) return [];
13379
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
13380
- throw new Error("workflow frontmatter field " + key + " must be a string array");
13746
+ function resolveTaskLens(task) {
13747
+ const normalized = (task ?? "").toLowerCase();
13748
+ if (!normalized.trim()) return LENSES.general;
13749
+ let best = {
13750
+ id: "general",
13751
+ score: 0,
13752
+ priority: Number.MAX_SAFE_INTEGER
13753
+ };
13754
+ for (const id of LENS_PRIORITY) {
13755
+ const score = KEYWORDS[id].reduce((sum, keyword) => sum + (containsTaskKeyword(normalized, keyword) ? 1 : 0), 0);
13756
+ const priority = LENS_PRIORITY.indexOf(id);
13757
+ if (score > best.score || score === best.score && score > 0 && priority < best.priority) {
13758
+ best = { id, score, priority };
13759
+ }
13381
13760
  }
13382
- return [...new Set(value.map((item) => item.trim()).filter(Boolean))];
13761
+ return best.score > 0 ? LENSES[best.id] : LENSES.general;
13383
13762
  }
13384
- function optionalNumber(data, key, fallback) {
13385
- const value = data[key];
13386
- if (value === void 0 || value === null || value === "") return fallback;
13387
- if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
13388
- throw new Error("workflow frontmatter field " + key + " must be a positive integer");
13763
+ function pathKindScore(path25, lens) {
13764
+ const normalized = normalizePath2(path25).toLowerCase();
13765
+ const name = normalized.split("/").pop() ?? normalized;
13766
+ const inDocs = normalized.startsWith("docs/") || name === "readme.md" || name.includes("readme");
13767
+ const isTest = /(^|\/)(tests?|__tests__|spec|fixtures?)\//.test(normalized) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(normalized) || /\.(test|spec)\.py$/.test(normalized);
13768
+ const isSource = normalized.startsWith("src/") || normalized.includes("/src/");
13769
+ const isPackage = name === "package.json" || name === "package-lock.json" || name === "pnpm-lock.yaml" || name === "yarn.lock";
13770
+ const isChangelog = name === "changelog.md" || name === "changes.md";
13771
+ const isWorkflow = normalized.startsWith(".github/workflows/");
13772
+ switch (lens.id) {
13773
+ case "bugfix":
13774
+ return (isTest ? 80 : 0) + (isSource ? 50 : 0) + (normalized.includes("debug") ? 20 : 0);
13775
+ case "test":
13776
+ return (isTest ? 90 : 0) + (isSource ? 45 : 0);
13777
+ case "release":
13778
+ return (isChangelog ? 100 : 0) + (isPackage ? 90 : 0) + (isWorkflow ? 70 : 0) + (inDocs ? 35 : 0);
13779
+ case "onboarding":
13780
+ return (name === "readme.md" ? 100 : 0) + (inDocs ? 80 : 0) + (isPackage ? 45 : 0) + (isSource ? 20 : 0);
13781
+ case "docs":
13782
+ return (inDocs ? 100 : 0) + (isChangelog ? 60 : 0) + (isSource ? 25 : 0);
13783
+ case "refactor":
13784
+ return (isSource ? 70 : 0) + (isTest ? 65 : 0);
13785
+ case "feature":
13786
+ return (isSource ? 80 : 0) + (isTest ? 45 : 0) + (inDocs ? 20 : 0);
13787
+ default:
13788
+ if (isSource || isTest) return 60;
13789
+ if (normalized.startsWith("packages/") && normalized.includes("/src/")) return 45;
13790
+ if (inDocs) return 20;
13791
+ return 0;
13389
13792
  }
13390
- return value;
13391
13793
  }
13392
- function optionalStatus(data) {
13393
- const value = data.status;
13394
- if (value === void 0 || value === null || value === "") return "draft";
13395
- if (typeof value !== "string" || !STATUS_VALUES.includes(value)) {
13396
- throw new Error("workflow frontmatter field status is invalid");
13397
- }
13398
- return value;
13794
+ function tokenScore(text, tokens) {
13795
+ if (tokens.length === 0) return 0;
13796
+ const normalized = text.toLowerCase();
13797
+ return tokens.reduce((sum, token) => sum + (normalized.includes(token) ? 12 : 0), 0);
13399
13798
  }
13400
- function normalizeAgents(values) {
13401
- const unknown = values.filter((value) => !KNOWN_AGENTS.includes(value));
13402
- if (unknown.length > 0) {
13403
- throw new Error("workflow frontmatter has unsupported agent: " + unknown[0]);
13404
- }
13405
- return values;
13799
+ function sourceTaskMatchScore(source, task) {
13800
+ const tokens = tokenize2(task ?? "");
13801
+ if (tokens.length === 0) return 0;
13802
+ const text = [
13803
+ source.title,
13804
+ source.path ?? "",
13805
+ source.symbol ?? ""
13806
+ ].join("\n").toLowerCase();
13807
+ const matches = tokens.filter((token) => text.includes(token)).length;
13808
+ if (matches >= 2) return 40;
13809
+ if (matches === 1) return 24;
13810
+ return 0;
13811
+ }
13812
+ function fallbackPathRank(path25) {
13813
+ const normalized = normalizePath2(path25);
13814
+ if (normalized.startsWith("src/")) return 0;
13815
+ if (normalized.startsWith("tests/") || normalized.startsWith("test/")) return 1;
13816
+ if (normalized.startsWith("packages/") && normalized.includes("/src/")) return 2;
13817
+ if (normalized.startsWith("docs/") || normalized.toLowerCase() === "readme.md") return 3;
13818
+ return 4;
13819
+ }
13820
+ function rankLensPaths(paths, lens, task) {
13821
+ const tokens = tokenize2(task ?? "");
13822
+ return [...new Set(paths.map(normalizePath2))].map((path25, index) => ({
13823
+ path: path25,
13824
+ index,
13825
+ score: pathKindScore(path25, lens) + tokenScore(path25, tokens),
13826
+ fallback: fallbackPathRank(path25)
13827
+ })).sort((a, b) => b.score - a.score || a.fallback - b.fallback || a.index - b.index || a.path.localeCompare(b.path)).map((item) => item.path);
13828
+ }
13829
+ function lensPathCandidates(lens) {
13830
+ switch (lens.id) {
13831
+ case "release":
13832
+ return ["CHANGELOG.md", "package.json", "package-lock.json", ".github/workflows/ci.yml", ".github/workflows/test.yml", "README.md"];
13833
+ case "onboarding":
13834
+ return ["README.md", "docs/README.md", "docs/API_REFERENCE.md", "package.json"];
13835
+ case "docs":
13836
+ return ["README.md", "README.zh-CN.md", "docs/README.md", "docs/API_REFERENCE.md", "CHANGELOG.md"];
13837
+ case "test":
13838
+ case "bugfix":
13839
+ return ["tests", "test"];
13840
+ default:
13841
+ return [];
13842
+ }
13843
+ }
13844
+ function scoreLensSource(source, lens, task) {
13845
+ const tokens = tokenize2(task ?? "");
13846
+ const text = [source.title, source.type, source.path ?? "", source.symbol ?? ""].join("\n");
13847
+ return pathKindScore(source.path ?? "", lens) + tokenScore(text, tokens) + sourceTaskMatchScore(source, task);
13848
+ }
13849
+ function rankLensSources(sources, lens, task) {
13850
+ return [...sources].map((source, index) => ({
13851
+ source,
13852
+ index,
13853
+ score: scoreLensSource(source, lens, task)
13854
+ })).sort((a, b) => b.score - a.score || a.source.observationId - b.source.observationId || a.index - b.index).map((item) => item.source);
13855
+ }
13856
+ function lensVerificationHints(lens) {
13857
+ switch (lens.id) {
13858
+ case "bugfix":
13859
+ return [
13860
+ "run the smallest failing test or repro first",
13861
+ "inspect the changed code path before trusting old memory"
13862
+ ];
13863
+ case "test":
13864
+ return [
13865
+ "run the exact focused test file or test name first",
13866
+ "inspect fixtures and harness setup before changing assertions"
13867
+ ];
13868
+ case "release":
13869
+ return [
13870
+ "run build, tests, package smoke, and publish dry-run where available",
13871
+ "verify package metadata, changelog, and Git state before publishing"
13872
+ ];
13873
+ case "onboarding":
13874
+ return [
13875
+ "read the docs/start files first, then inspect only the code paths needed for the task",
13876
+ "treat old implementation memories as leads until current code confirms them"
13877
+ ];
13878
+ case "refactor":
13879
+ return [
13880
+ "inspect call sites and tests before editing shared code",
13881
+ "run the narrow affected test plus one regression smoke"
13882
+ ];
13883
+ case "docs":
13884
+ return [
13885
+ "check headings, links, commands, and examples against current code",
13886
+ "run the smallest docs or package smoke available"
13887
+ ];
13888
+ case "feature":
13889
+ return [
13890
+ "inspect the closest existing implementation pattern before adding new code",
13891
+ "run focused tests plus one user-flow smoke after changes"
13892
+ ];
13893
+ default:
13894
+ return [
13895
+ "inspect the Start here files before editing",
13896
+ "run the smallest relevant test or smoke command after changes"
13897
+ ];
13898
+ }
13899
+ }
13900
+ function shouldShowLensSource(source, lens, task) {
13901
+ if (lens.id === "general") return true;
13902
+ const score = scoreLensSource(source, lens, task);
13903
+ if (lens.id === "onboarding" || lens.id === "release" || lens.id === "docs") return score >= 40;
13904
+ return score > 0;
13905
+ }
13906
+ var STOP_WORDS, LENSES, KEYWORDS, LENS_PRIORITY;
13907
+ var init_task_lens = __esm({
13908
+ "src/codegraph/task-lens.ts"() {
13909
+ "use strict";
13910
+ init_esm_shims();
13911
+ STOP_WORDS = /* @__PURE__ */ new Set([
13912
+ "a",
13913
+ "an",
13914
+ "and",
13915
+ "as",
13916
+ "for",
13917
+ "in",
13918
+ "into",
13919
+ "of",
13920
+ "on",
13921
+ "onto",
13922
+ "the",
13923
+ "this",
13924
+ "that",
13925
+ "to",
13926
+ "with",
13927
+ "work",
13928
+ "project",
13929
+ "continue",
13930
+ "\u7EE7\u7EED",
13931
+ "\u9879\u76EE"
13932
+ ]);
13933
+ LENSES = {
13934
+ bugfix: {
13935
+ id: "bugfix",
13936
+ description: "debug the failure with current code and the smallest repro first",
13937
+ sourceLimit: 6,
13938
+ cautionLimit: 4,
13939
+ hideUnrelatedCautionDetails: true,
13940
+ hideUnrelatedReliableDetails: false
13941
+ },
13942
+ feature: {
13943
+ id: "feature",
13944
+ description: "build a scoped feature from nearby source, types, and user flow",
13945
+ sourceLimit: 6,
13946
+ cautionLimit: 3,
13947
+ hideUnrelatedCautionDetails: true,
13948
+ hideUnrelatedReliableDetails: false
13949
+ },
13950
+ release: {
13951
+ id: "release",
13952
+ description: "prepare a release using current metadata, changelog, build, and package checks",
13953
+ sourceLimit: 4,
13954
+ cautionLimit: 2,
13955
+ hideUnrelatedCautionDetails: true,
13956
+ hideUnrelatedReliableDetails: true
13957
+ },
13958
+ onboarding: {
13959
+ id: "onboarding",
13960
+ description: "understand the project shape before trusting old implementation details",
13961
+ sourceLimit: 4,
13962
+ cautionLimit: 2,
13963
+ hideUnrelatedCautionDetails: true,
13964
+ hideUnrelatedReliableDetails: true
13965
+ },
13966
+ refactor: {
13967
+ id: "refactor",
13968
+ description: "change structure carefully by reading shared code, call sites, and tests",
13969
+ sourceLimit: 6,
13970
+ cautionLimit: 4,
13971
+ hideUnrelatedCautionDetails: true,
13972
+ hideUnrelatedReliableDetails: false
13973
+ },
13974
+ docs: {
13975
+ id: "docs",
13976
+ description: "update documentation against current code and public entry points",
13977
+ sourceLimit: 5,
13978
+ cautionLimit: 2,
13979
+ hideUnrelatedCautionDetails: true,
13980
+ hideUnrelatedReliableDetails: true
13981
+ },
13982
+ test: {
13983
+ id: "test",
13984
+ description: "work from tests, fixtures, harnesses, and the related source files",
13985
+ sourceLimit: 6,
13986
+ cautionLimit: 3,
13987
+ hideUnrelatedCautionDetails: true,
13988
+ hideUnrelatedReliableDetails: false
13989
+ },
13990
+ general: {
13991
+ id: "general",
13992
+ description: "balanced project handoff with current facts, code memory, and verification hints",
13993
+ sourceLimit: 8,
13994
+ cautionLimit: 5,
13995
+ hideUnrelatedCautionDetails: false,
13996
+ hideUnrelatedReliableDetails: false
13997
+ }
13998
+ };
13999
+ KEYWORDS = {
14000
+ bugfix: [
14001
+ "bug",
14002
+ "crash",
14003
+ "incident",
14004
+ "debug",
14005
+ "error",
14006
+ "fail",
14007
+ "failing",
14008
+ "fix",
14009
+ "issue",
14010
+ "regression",
14011
+ "repro",
14012
+ "\u62A5\u9519",
14013
+ "\u5D29\u6E83",
14014
+ "\u6545\u969C",
14015
+ "\u5931\u8D25",
14016
+ "\u4FEE\u590D",
14017
+ "\u95EE\u9898"
14018
+ ],
14019
+ feature: ["add", "build", "feature", "implement", "new", "support", "\u65B0\u589E", "\u5B9E\u73B0", "\u652F\u6301", "\u529F\u80FD"],
14020
+ release: ["bump", "changelog", "npm", "pack", "publish", "release", "version", "\u53D1\u7248", "\u53D1\u5E03", "\u7248\u672C"],
14021
+ onboarding: ["architecture", "handoff", "onboard", "overview", "understand", "\u63A5\u624B", "\u4E86\u89E3", "\u7406\u89E3", "\u67B6\u6784"],
14022
+ refactor: ["cleanup", "migrate", "refactor", "rename", "restructure", "\u91CD\u6784", "\u8FC1\u79FB", "\u6539\u9020"],
14023
+ docs: ["doc", "docs", "readme", "\u6587\u6863", "\u8BF4\u660E"],
14024
+ test: ["coverage", "fixture", "smoke", "spec", "test", "tests", "testing", "vitest", "\u6D4B\u8BD5"]
14025
+ };
14026
+ LENS_PRIORITY = [
14027
+ "bugfix",
14028
+ "release",
14029
+ "test",
14030
+ "refactor",
14031
+ "feature",
14032
+ "docs",
14033
+ "onboarding"
14034
+ ];
14035
+ }
14036
+ });
14037
+
14038
+ // src/knowledge/workflows.ts
14039
+ var workflows_exports = {};
14040
+ __export(workflows_exports, {
14041
+ WORKFLOW_ADAPTER_TARGETS: () => WORKFLOW_ADAPTER_TARGETS,
14042
+ applyWorkflowAdapter: () => applyWorkflowAdapter,
14043
+ importWindsurfWorkflows: () => importWindsurfWorkflows,
14044
+ parseWorkflowMarkdown: () => parseWorkflowMarkdown,
14045
+ previewWorkflowAdapter: () => previewWorkflowAdapter,
14046
+ recordWorkflowRun: () => recordWorkflowRun,
14047
+ renderWorkflowMarkdown: () => renderWorkflowMarkdown,
14048
+ selectWorkflows: () => selectWorkflows,
14049
+ selectWorkspaceWorkflows: () => selectWorkspaceWorkflows,
14050
+ syncCanonicalWorkflows: () => syncCanonicalWorkflows,
14051
+ writeCanonicalWorkflow: () => writeCanonicalWorkflow
14052
+ });
14053
+ import { createHash as createHash12 } from "crypto";
14054
+ import { promises as fs14 } from "fs";
14055
+ import path17 from "path";
14056
+ import matter11 from "gray-matter";
14057
+ function hash3(value) {
14058
+ return createHash12("sha256").update(value).digest("hex");
14059
+ }
14060
+ function now2() {
14061
+ return (/* @__PURE__ */ new Date()).toISOString();
14062
+ }
14063
+ function slug(value) {
14064
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72);
14065
+ return normalized || "workflow";
14066
+ }
14067
+ function titleFromName(name) {
14068
+ return name.replace(/[-_]+/g, " ").split(" ").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ") || "Imported Workflow";
14069
+ }
14070
+ function requiredText(data, key) {
14071
+ const value = data[key];
14072
+ if (typeof value !== "string" || !value.trim()) {
14073
+ throw new Error("workflow frontmatter requires " + key);
14074
+ }
14075
+ return value.trim();
14076
+ }
14077
+ function optionalText4(data, key) {
14078
+ const value = data[key];
14079
+ if (value === void 0 || value === null || value === "") return void 0;
14080
+ if (typeof value !== "string") throw new Error("workflow frontmatter field " + key + " must be text");
14081
+ return value.trim() || void 0;
14082
+ }
14083
+ function optionalStringArray(data, key) {
14084
+ const value = data[key];
14085
+ if (value === void 0 || value === null) return [];
14086
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
14087
+ throw new Error("workflow frontmatter field " + key + " must be a string array");
14088
+ }
14089
+ return [...new Set(value.map((item) => item.trim()).filter(Boolean))];
14090
+ }
14091
+ function optionalNumber(data, key, fallback) {
14092
+ const value = data[key];
14093
+ if (value === void 0 || value === null || value === "") return fallback;
14094
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
14095
+ throw new Error("workflow frontmatter field " + key + " must be a positive integer");
14096
+ }
14097
+ return value;
14098
+ }
14099
+ function optionalStatus(data) {
14100
+ const value = data.status;
14101
+ if (value === void 0 || value === null || value === "") return "draft";
14102
+ if (typeof value !== "string" || !STATUS_VALUES.includes(value)) {
14103
+ throw new Error("workflow frontmatter field status is invalid");
14104
+ }
14105
+ return value;
14106
+ }
14107
+ function normalizeAgents(values) {
14108
+ const unknown = values.filter((value) => !KNOWN_AGENTS.includes(value));
14109
+ if (unknown.length > 0) {
14110
+ throw new Error("workflow frontmatter has unsupported agent: " + unknown[0]);
14111
+ }
14112
+ return values;
13406
14113
  }
13407
14114
  function phaseFromValue(value, index) {
13408
14115
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -13650,20 +14357,27 @@ function taskScore(workflow, task) {
13650
14357
  const text = task.toLowerCase();
13651
14358
  let score = 0;
13652
14359
  const reasons = [];
14360
+ const matchedLenses = /* @__PURE__ */ new Set();
14361
+ const matchedTriggers = /* @__PURE__ */ new Set();
13653
14362
  for (const lens of workflow.taskLenses) {
13654
14363
  const terms = LENS_TERMS[lens.toLowerCase()] ?? [lens.toLowerCase()];
13655
- if (terms.some((term) => text.includes(term))) {
14364
+ if (terms.some((term) => containsTaskKeyword(task, term))) {
13656
14365
  score += 20;
13657
14366
  reasons.push("matches " + lens + " workflow");
14367
+ matchedLenses.add(lens.toLowerCase());
13658
14368
  }
13659
14369
  }
13660
14370
  for (const trigger of workflow.triggers) {
13661
14371
  const term = trigger.toLowerCase().trim();
13662
- if (term.length >= 2 && text.includes(term)) {
14372
+ if (term.length >= 2 && containsTaskKeyword(task, term)) {
13663
14373
  score += 8;
13664
14374
  reasons.push('matches trigger "' + trigger + '"');
14375
+ matchedTriggers.add(term);
13665
14376
  }
13666
14377
  }
14378
+ const hasSpecificLens = workflow.taskLenses.some((lens) => !GENERIC_WORKFLOW_LENSES.has(lens.toLowerCase()));
14379
+ const hasSpecificMatch = [...matchedLenses].some((lens) => !GENERIC_WORKFLOW_LENSES.has(lens)) || [...matchedTriggers].some((trigger) => !GENERIC_WORKFLOW_TERMS.has(trigger));
14380
+ if (hasSpecificLens && !hasSpecificMatch) return { score: 0, reasons: [] };
13667
14381
  return { score, reasons: [...new Set(reasons)] };
13668
14382
  }
13669
14383
  function selectWorkflows(input) {
@@ -13681,9 +14395,28 @@ function selectWorkflows(input) {
13681
14395
  return selected.slice(0, Math.max(1, Math.min(input.limit ?? 2, 2)));
13682
14396
  }
13683
14397
  function importedWorkflowSpec(input) {
14398
+ let sourceMatter;
14399
+ try {
14400
+ sourceMatter = matter11(input.raw);
14401
+ } catch (error) {
14402
+ throw new Error("Malformed Windsurf workflow Markdown: " + (error instanceof Error ? error.message : String(error)));
14403
+ }
14404
+ if (sourceMatter.data.id !== void 0) {
14405
+ const id = requiredText(sourceMatter.data, "id");
14406
+ const sourcePath2 = "workflows/" + slug(id) + ".md";
14407
+ const canonical = parsedWorkflow(sourceMatter.data, sourceMatter.content, {
14408
+ workspaceId: input.workspace.id,
14409
+ sourcePath: sourcePath2,
14410
+ contentHash: hash3(input.raw)
14411
+ });
14412
+ return materializeWorkflow({
14413
+ ...canonical,
14414
+ importedFrom: input.sourcePath.replace(/\\/g, "/")
14415
+ });
14416
+ }
13684
14417
  const entry = new WorkflowSyncer().parseWindsurfWorkflow(input.sourceName, input.raw);
13685
14418
  const content = entry.content.trim() || "## Execute\n\nFollow the imported workflow.";
13686
- const lenses = inferTaskLenses(entry.name + "\n" + entry.description + "\n" + content);
14419
+ const lenses = inferTaskLenses(entry.name + "\n" + entry.description);
13687
14420
  const createdAt = now2();
13688
14421
  const sourcePath = "workflows/" + slug(entry.name) + ".md";
13689
14422
  const spec = {
@@ -13904,7 +14637,7 @@ async function selectWorkspaceWorkflows(input) {
13904
14637
  errors: synced.errors
13905
14638
  };
13906
14639
  }
13907
- var KNOWN_AGENTS, WORKFLOW_ADAPTER_TARGETS, STATUS_VALUES, LENS_TERMS;
14640
+ var KNOWN_AGENTS, WORKFLOW_ADAPTER_TARGETS, STATUS_VALUES, LENS_TERMS, GENERIC_WORKFLOW_LENSES, GENERIC_WORKFLOW_TERMS;
13908
14641
  var init_workflows = __esm({
13909
14642
  "src/knowledge/workflows.ts"() {
13910
14643
  "use strict";
@@ -13913,6 +14646,7 @@ var init_workflows = __esm({
13913
14646
  init_workflow_sync();
13914
14647
  init_workspace();
13915
14648
  init_workflow_store();
14649
+ init_task_lens();
13916
14650
  KNOWN_AGENTS = [
13917
14651
  "windsurf",
13918
14652
  "cursor",
@@ -13944,6 +14678,8 @@ var init_workflows = __esm({
13944
14678
  refactor: ["refactor", "cleanup", "restructure", "\u91CD\u6784", "\u6574\u7406"],
13945
14679
  test: ["test", "verify", "smoke", "\u6D4B\u8BD5", "\u9A8C\u8BC1"]
13946
14680
  };
14681
+ GENERIC_WORKFLOW_LENSES = /* @__PURE__ */ new Set(["review", "test"]);
14682
+ GENERIC_WORKFLOW_TERMS = /* @__PURE__ */ new Set(["review", "audit", "test", "verify", "smoke"]);
13947
14683
  }
13948
14684
  });
13949
14685
 
@@ -13990,6 +14726,15 @@ function claimDerivationCursor(payload) {
13990
14726
  const value = payload.cursor;
13991
14727
  return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
13992
14728
  }
14729
+ function observationQualificationBatchSize(payload) {
14730
+ const value = payload.limit;
14731
+ if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE;
14732
+ return Math.min(500, Math.max(1, Math.floor(value)));
14733
+ }
14734
+ function observationQualificationCursor(payload) {
14735
+ const value = payload.cursor;
14736
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
14737
+ }
13993
14738
  function observationId(payload) {
13994
14739
  const value = payload.observationId;
13995
14740
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
@@ -14168,6 +14913,71 @@ function createProjectMaintenanceHandler(projectId, projectDir2, projectRoot, op
14168
14913
  ...snapshot?.id ? { snapshotId: snapshot.id } : {},
14169
14914
  ...options.maintenanceQueue ? { queue: options.maintenanceQueue } : {}
14170
14915
  });
14916
+ enqueueObservationQualification({
14917
+ projectId,
14918
+ dataDir: projectDir2,
14919
+ source: "codegraph-refresh",
14920
+ ...options.maintenanceQueue ? { queue: options.maintenanceQueue } : {}
14921
+ });
14922
+ return { action: "complete" };
14923
+ }
14924
+ if (job.kind === "observation-qualify") {
14925
+ const [
14926
+ { CodeGraphStore: CodeGraphStore2 },
14927
+ { bindObservationToCode: bindObservationToCode2 },
14928
+ { getObservationStore: getObservationStore2 },
14929
+ { qualifyCandidateFromCurrentCode: qualifyCandidateFromCurrentCode2 },
14930
+ { updateObservationAdmission: updateObservationAdmission2 }
14931
+ ] = await Promise.all([
14932
+ Promise.resolve().then(() => (init_store(), store_exports)),
14933
+ Promise.resolve().then(() => (init_binder(), binder_exports)),
14934
+ Promise.resolve().then(() => (init_obs_store(), obs_store_exports)),
14935
+ Promise.resolve().then(() => (init_admission(), admission_exports)),
14936
+ Promise.resolve().then(() => (init_observations(), observations_exports))
14937
+ ]);
14938
+ const codeStore = new CodeGraphStore2();
14939
+ await codeStore.init(projectDir2);
14940
+ const limit = observationQualificationBatchSize(job.payload);
14941
+ const page = await getObservationStore2().loadByProject(projectId, {
14942
+ status: "active",
14943
+ afterId: observationQualificationCursor(job.payload),
14944
+ limit: limit + 1
14945
+ });
14946
+ const hasMore = page.length > limit;
14947
+ const observations2 = hasMore ? page.slice(0, limit) : page;
14948
+ const indexedAtMs = Date.parse(codeStore.status(projectId).indexedAt ?? "");
14949
+ for (const observation of observations2) {
14950
+ if (observation.admissionState !== "candidate") continue;
14951
+ if (!Number.isFinite(indexedAtMs) || indexedAtMs < Date.parse(observation.createdAt)) continue;
14952
+ await bindObservationToCode2(codeStore, observation);
14953
+ const currentCodeReferenceCount = codeStore.listObservationRefs(projectId, observation.id).filter((reference) => reference.status === "current").length;
14954
+ const qualification = qualifyCandidateFromCurrentCode2({
14955
+ observation,
14956
+ currentCodeReferenceCount
14957
+ });
14958
+ if (!qualification) continue;
14959
+ await updateObservationAdmission2({
14960
+ observationId: observation.id,
14961
+ projectId,
14962
+ expectedState: "candidate",
14963
+ // Automatic capture earns project visibility only after a current
14964
+ // Code Memory link exists. Explicit/private records retain scope.
14965
+ ...observation.sourceDetail === "hook" ? { visibility: "project" } : {},
14966
+ ...qualification
14967
+ });
14968
+ }
14969
+ if (hasMore && observations2.length > 0) {
14970
+ return {
14971
+ action: "reschedule",
14972
+ delayMs: 0,
14973
+ resetAttempts: true,
14974
+ payload: {
14975
+ ...job.payload,
14976
+ cursor: observations2[observations2.length - 1].id,
14977
+ limit
14978
+ }
14979
+ };
14980
+ }
14171
14981
  return { action: "complete" };
14172
14982
  }
14173
14983
  if (job.kind === "claim-derive") {
@@ -14333,7 +15143,7 @@ function createProjectMaintenanceDispatcher(projectId, projectDir2, projectRoot,
14333
15143
  return isolatedRunner({ job, projectRoot, dataDir: projectDir2 });
14334
15144
  };
14335
15145
  }
14336
- var DEFAULT_VECTOR_BATCH_SIZE, DEFAULT_RETENTION_BATCH_SIZE, DEFAULT_CONSOLIDATION_BATCH_SIZE, DEFAULT_CODEGRAPH_MAX_FILES, DEFAULT_CLAIM_DERIVATION_BATCH_SIZE, VECTOR_RETRY_DELAY_MS, DEDUP_PER_PAIR_TIMEOUT_MS;
15146
+ var DEFAULT_VECTOR_BATCH_SIZE, DEFAULT_RETENTION_BATCH_SIZE, DEFAULT_CONSOLIDATION_BATCH_SIZE, DEFAULT_CODEGRAPH_MAX_FILES, DEFAULT_CLAIM_DERIVATION_BATCH_SIZE, DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE, VECTOR_RETRY_DELAY_MS, DEDUP_PER_PAIR_TIMEOUT_MS;
14337
15147
  var init_project_maintenance = __esm({
14338
15148
  "src/runtime/project-maintenance.ts"() {
14339
15149
  "use strict";
@@ -14345,6 +15155,7 @@ var init_project_maintenance = __esm({
14345
15155
  DEFAULT_CONSOLIDATION_BATCH_SIZE = 200;
14346
15156
  DEFAULT_CODEGRAPH_MAX_FILES = 5e3;
14347
15157
  DEFAULT_CLAIM_DERIVATION_BATCH_SIZE = 100;
15158
+ DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE = 100;
14348
15159
  VECTOR_RETRY_DELAY_MS = 5e3;
14349
15160
  DEDUP_PER_PAIR_TIMEOUT_MS = 5e3;
14350
15161
  }
@@ -14586,9 +15397,9 @@ async function getSessionContext(projectDir2, projectId, limit = 3) {
14586
15397
  return true;
14587
15398
  });
14588
15399
  })() : l2Scored).slice(0, 5).map(({ obs }) => obs);
14589
- const l1HookObs = projectObs.filter((obs) => classifyLayer(obs) === "L1").sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).slice(0, 3);
15400
+ const l1HookObs = projectObs.filter((obs) => isEligibleForAutomaticDelivery(obs) && classifyLayer(obs) === "L1").sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).slice(0, 3);
14590
15401
  const l3GitCount = projectObs.filter((obs) => classifyLayer(obs) === "L3").length;
14591
- const totalHookCount = projectObs.filter((obs) => classifyLayer(obs) === "L1").length;
15402
+ const totalHookCount = projectObs.filter((obs) => isEligibleForAutomaticDelivery(obs) && classifyLayer(obs) === "L1").length;
14592
15403
  const activeEntities = [
14593
15404
  ...new Set(l2Obs.map((o) => o.entityName).filter((n) => !!n && n.trim().length > 0))
14594
15405
  ].slice(0, 5);
@@ -14714,6 +15525,7 @@ var init_session = __esm({
14714
15525
  "src/memory/session.ts"() {
14715
15526
  "use strict";
14716
15527
  init_esm_shims();
15528
+ init_admission();
14717
15529
  init_disclosure_policy();
14718
15530
  init_aliases();
14719
15531
  init_obs_store();
@@ -14866,27 +15678,80 @@ function workflowOutput(selection) {
14866
15678
  cautions: selection.cautions.map((caution) => short(caution, 22))
14867
15679
  };
14868
15680
  }
14869
- function appendLine(lines, candidate, maxTokens, omitted, omittedKind) {
15681
+ function appendLine(lines, candidate, maxTokens, omitted, omittedKind, selected, receiptSelection) {
14870
15682
  const next = lines.length ? lines.join("\n") + "\n" + candidate : candidate;
14871
15683
  if (countTextTokens(next) <= maxTokens) {
14872
15684
  lines.push(candidate);
15685
+ if (receiptSelection) selected?.push(receiptSelection);
14873
15686
  return true;
14874
15687
  }
14875
- omitted.push(omittedKind);
14876
- return false;
15688
+ omitted.push(omittedKind);
15689
+ return false;
15690
+ }
15691
+ function freshnessForMemory(status) {
15692
+ if (status === "current" || status === "suspect" || status === "stale") return status;
15693
+ return "unknown";
15694
+ }
15695
+ function receiptOmissionKind(raw) {
15696
+ if (raw.includes("task")) return "task";
15697
+ if (raw.includes("fact")) return "current-fact";
15698
+ if (raw.includes("state")) return "code-state";
15699
+ if (raw.includes("semantic")) return "semantic-code";
15700
+ if (raw.includes("start")) return "start-here";
15701
+ if (raw.includes("memory")) return "memory";
15702
+ if (raw.includes("claim")) return "claim";
15703
+ if (raw.includes("knowledge-page")) return "knowledge-page";
15704
+ if (raw.includes("workflow")) return "workflow";
15705
+ if (raw.includes("verification")) return "verification";
15706
+ if (raw.includes("caution")) return "caution";
15707
+ return void 0;
15708
+ }
15709
+ function receiptOmissions(omitted, hiddenCautionMemoryCount) {
15710
+ const counts = /* @__PURE__ */ new Map();
15711
+ for (const raw of omitted) {
15712
+ const kind = receiptOmissionKind(raw);
15713
+ if (!kind || raw.endsWith("-heading")) continue;
15714
+ counts.set(kind, (counts.get(kind) ?? 0) + 1);
15715
+ }
15716
+ const receipt = [...counts.entries()].map(([kind, count2]) => ({
15717
+ kind,
15718
+ reason: "token-budget",
15719
+ count: count2
15720
+ }));
15721
+ if (hiddenCautionMemoryCount > 0) {
15722
+ receipt.push({
15723
+ kind: "caution",
15724
+ reason: "hidden-by-task-lens",
15725
+ count: hiddenCautionMemoryCount
15726
+ });
15727
+ }
15728
+ return receipt;
15729
+ }
15730
+ function scheduledActions(cautions) {
15731
+ return cautions.filter((caution) => caution.kind === "codegraph-refresh-queued").map((caution) => short(caution.message, 28));
14877
15732
  }
14878
15733
  function renderTaskWorksetPrompt(input) {
14879
15734
  const maxTokens = input.budget?.maxTokens ?? 180;
14880
15735
  const omitted = [];
15736
+ const selected = [];
14881
15737
  const lines = ["Memorix Autopilot Brief"];
14882
15738
  const task = short(input.task || "Continue the current task.", 34);
14883
- appendLine(lines, "Task: " + task, maxTokens, omitted, "task-detail");
15739
+ appendLine(lines, "Task: " + task, maxTokens, omitted, "task-detail", selected, {
15740
+ kind: "task",
15741
+ reason: "current task supplied by the caller",
15742
+ trust: "source-backed"
15743
+ });
14884
15744
  appendLine(lines, "Task lens: " + input.lens, maxTokens, omitted, "lens");
14885
15745
  if (input.cautions.length > 0 || input.cautionMemory.length > 0) {
14886
15746
  appendLine(lines, "", maxTokens, omitted, "caution-heading");
14887
15747
  appendLine(lines, "Cautions", maxTokens, omitted, "caution-heading");
14888
15748
  for (const caution of input.cautions.slice(0, 6)) {
14889
- appendLine(lines, "- " + short(caution.message, 22), maxTokens, omitted, "caution");
15749
+ appendLine(lines, "- " + short(caution.message, 22), maxTokens, omitted, "caution", selected, {
15750
+ kind: "caution",
15751
+ id: "caution:" + caution.kind,
15752
+ reason: "current project caution",
15753
+ trust: "source-backed"
15754
+ });
14890
15755
  }
14891
15756
  for (const memory of input.cautionMemory.slice(0, 3)) {
14892
15757
  const location = memory.path ? memory.path + (memory.symbol ? "#" + memory.symbol : "") : "no current code location";
@@ -14896,7 +15761,15 @@ function renderTaskWorksetPrompt(input) {
14896
15761
  "- #" + memory.id + " " + memory.status + ": " + short(memory.title, 18) + " (" + location + reason + ")",
14897
15762
  maxTokens,
14898
15763
  omitted,
14899
- "caution-memory"
15764
+ "caution-memory",
15765
+ selected,
15766
+ {
15767
+ kind: "memory",
15768
+ id: "memory:" + memory.id,
15769
+ reason: "task-relevant memory requiring source verification",
15770
+ freshness: freshnessForMemory(memory.status),
15771
+ trust: "historical"
15772
+ }
14900
15773
  );
14901
15774
  }
14902
15775
  if (input.hiddenCautionMemoryCount > 0) {
@@ -14907,13 +15780,22 @@ function renderTaskWorksetPrompt(input) {
14907
15780
  appendLine(lines, "", maxTokens, omitted, "facts-heading");
14908
15781
  appendLine(lines, "Current project facts", maxTokens, omitted, "facts-heading");
14909
15782
  for (const fact of input.currentFacts.slice(0, 4)) {
14910
- appendLine(lines, "- " + short(fact, 40), maxTokens, omitted, "current-fact");
15783
+ appendLine(lines, "- " + short(fact, 40), maxTokens, omitted, "current-fact", selected, {
15784
+ kind: "current-fact",
15785
+ reason: "current project state",
15786
+ trust: "source-backed"
15787
+ });
14911
15788
  }
14912
15789
  }
14913
15790
  if (input.codeState) {
14914
15791
  appendLine(lines, "", maxTokens, omitted, "state-heading");
14915
15792
  appendLine(lines, "Project state", maxTokens, omitted, "state-heading");
14916
- appendLine(lines, input.codeState, maxTokens, omitted, "code-state");
15793
+ appendLine(lines, input.codeState, maxTokens, omitted, "code-state", selected, {
15794
+ kind: "code-state",
15795
+ ...input.provenance.snapshotId ? { id: "snapshot:" + input.provenance.snapshotId } : {},
15796
+ reason: "latest available Code State snapshot",
15797
+ trust: "source-backed"
15798
+ });
14917
15799
  }
14918
15800
  if (input.semanticCode && (input.semanticCode.entryPoints.length > 0 || input.semanticCode.relations.length > 0)) {
14919
15801
  appendLine(lines, "", maxTokens, omitted, "semantic-code-heading");
@@ -14925,7 +15807,14 @@ function renderTaskWorksetPrompt(input) {
14925
15807
  "- " + location + ": " + short(relation.from.name, 12) + " " + short(relation.kind, 8) + " " + short(relation.to.name, 12),
14926
15808
  maxTokens,
14927
15809
  omitted,
14928
- "semantic-relation"
15810
+ "semantic-relation",
15811
+ selected,
15812
+ {
15813
+ kind: "semantic-code",
15814
+ id: "code:" + location,
15815
+ reason: "validated optional semantic code relation",
15816
+ trust: "derived"
15817
+ }
14929
15818
  );
14930
15819
  }
14931
15820
  if (input.semanticCode.relations.length === 0) {
@@ -14936,7 +15825,14 @@ function renderTaskWorksetPrompt(input) {
14936
15825
  "- " + location + ": " + short(entry.name, 16) + " (" + short(entry.kind, 8) + ")",
14937
15826
  maxTokens,
14938
15827
  omitted,
14939
- "semantic-entry"
15828
+ "semantic-entry",
15829
+ selected,
15830
+ {
15831
+ kind: "semantic-code",
15832
+ id: "code:" + location,
15833
+ reason: "validated optional semantic code entry point",
15834
+ trust: "derived"
15835
+ }
14940
15836
  );
14941
15837
  }
14942
15838
  }
@@ -14945,7 +15841,12 @@ function renderTaskWorksetPrompt(input) {
14945
15841
  appendLine(lines, "", maxTokens, omitted, "start-heading");
14946
15842
  appendLine(lines, "Start here", maxTokens, omitted, "start-heading");
14947
15843
  for (const source of input.startHere.slice(0, 5)) {
14948
- appendLine(lines, "- " + source, maxTokens, omitted, "start-here");
15844
+ appendLine(lines, "- " + source, maxTokens, omitted, "start-here", selected, {
15845
+ kind: "start-here",
15846
+ id: "path:" + source,
15847
+ reason: "task-lensed starting point",
15848
+ trust: "derived"
15849
+ });
14949
15850
  }
14950
15851
  }
14951
15852
  if (input.reliableMemory.length > 0) {
@@ -14958,7 +15859,15 @@ function renderTaskWorksetPrompt(input) {
14958
15859
  "- #" + memory.id + " " + memory.type + ": " + short(memory.title, 18) + " (" + location + ")",
14959
15860
  maxTokens,
14960
15861
  omitted,
14961
- "reliable-memory"
15862
+ "reliable-memory",
15863
+ selected,
15864
+ {
15865
+ kind: "memory",
15866
+ id: "memory:" + memory.id,
15867
+ reason: "current code-bound memory",
15868
+ freshness: freshnessForMemory(memory.status),
15869
+ trust: "historical"
15870
+ }
14962
15871
  );
14963
15872
  }
14964
15873
  }
@@ -14966,10 +15875,25 @@ function renderTaskWorksetPrompt(input) {
14966
15875
  appendLine(lines, "", maxTokens, omitted, "knowledge-heading");
14967
15876
  appendLine(lines, "Project knowledge", maxTokens, omitted, "knowledge-heading");
14968
15877
  for (const claim of input.claims.slice(0, 3)) {
14969
- appendLine(lines, "- " + claim.assertion + " [" + claim.id + "]", maxTokens, omitted, "claim");
15878
+ appendLine(lines, "- " + claim.assertion + " [" + claim.id + "]", maxTokens, omitted, "claim", selected, {
15879
+ kind: "claim",
15880
+ id: "claim:" + claim.id,
15881
+ reason: "source-qualified task match",
15882
+ trust: "source-backed"
15883
+ });
14970
15884
  }
14971
15885
  for (const page of input.pages.slice(0, 2)) {
14972
- appendLine(lines, "- page: " + page.relativePath, maxTokens, omitted, "knowledge-page");
15886
+ const supportsDeliveredClaim = page.claimIds.some((claimId) => selected.some((item) => item.kind === "claim" && item.id === "claim:" + claimId));
15887
+ if (!supportsDeliveredClaim) {
15888
+ omitted.push("knowledge-page-dependency");
15889
+ continue;
15890
+ }
15891
+ appendLine(lines, "- page: " + page.relativePath, maxTokens, omitted, "knowledge-page", selected, {
15892
+ kind: "knowledge-page",
15893
+ id: "page:" + page.id,
15894
+ reason: "approved page linked to a selected claim",
15895
+ trust: "source-backed"
15896
+ });
14973
15897
  }
14974
15898
  }
14975
15899
  if (input.workflows.length > 0) {
@@ -14981,7 +15905,14 @@ function renderTaskWorksetPrompt(input) {
14981
15905
  "- " + workflow.title + ": " + workflow.firstPhase.title + " - " + workflow.firstPhase.instructions,
14982
15906
  maxTokens,
14983
15907
  omitted,
14984
- "workflow"
15908
+ "workflow",
15909
+ selected,
15910
+ {
15911
+ kind: "workflow",
15912
+ id: "workflow:" + workflow.id,
15913
+ reason: "task-matching project workflow",
15914
+ trust: "source-backed"
15915
+ }
14985
15916
  );
14986
15917
  }
14987
15918
  }
@@ -14989,16 +15920,23 @@ function renderTaskWorksetPrompt(input) {
14989
15920
  appendLine(lines, "", maxTokens, omitted, "verification-heading");
14990
15921
  appendLine(lines, "Verify", maxTokens, omitted, "verification-heading");
14991
15922
  for (const check of input.verification.slice(0, 4)) {
14992
- appendLine(lines, "- " + short(check, 20), maxTokens, omitted, "verification");
15923
+ appendLine(lines, "- " + short(check, 20), maxTokens, omitted, "verification", selected, {
15924
+ kind: "verification",
15925
+ reason: "task-lensed verification guidance",
15926
+ trust: "derived"
15927
+ });
14993
15928
  }
14994
15929
  }
14995
15930
  return {
14996
15931
  prompt: lines.join("\n"),
14997
15932
  tokenCount: countTextTokens(lines.join("\n")),
14998
- omitted: unique(omitted)
15933
+ omitted: unique(omitted),
15934
+ omittedItems: omitted,
15935
+ selected
14999
15936
  };
15000
15937
  }
15001
15938
  async function buildTaskWorkset(input) {
15939
+ const startedAt = Date.now();
15002
15940
  const task = input.task?.trim() ?? "";
15003
15941
  const maxTokens = Math.max(96, Math.min(Math.floor(input.maxTokens ?? 180), 320));
15004
15942
  const cautions = [...input.runtimeCautions ?? [], ...snapshotCautions(input)];
@@ -15100,6 +16038,18 @@ async function buildTaskWorkset(input) {
15100
16038
  ...base,
15101
16039
  budget: { maxTokens }
15102
16040
  });
16041
+ const receipt = {
16042
+ version: "1.2.2",
16043
+ target: input.deliveryTarget ?? "project-context",
16044
+ elapsedMs: Math.max(0, Date.now() - startedAt),
16045
+ budget: {
16046
+ maxTokens,
16047
+ tokenCount: rendered.tokenCount
16048
+ },
16049
+ selected: rendered.selected,
16050
+ omitted: receiptOmissions(rendered.omittedItems, base.hiddenCautionMemoryCount),
16051
+ scheduledActions: scheduledActions(normalizedCautions)
16052
+ };
15103
16053
  return {
15104
16054
  ...base,
15105
16055
  budget: {
@@ -15107,6 +16057,7 @@ async function buildTaskWorkset(input) {
15107
16057
  tokenCount: rendered.tokenCount,
15108
16058
  omitted: rendered.omitted
15109
16059
  },
16060
+ receipt,
15110
16061
  prompt: rendered.prompt
15111
16062
  };
15112
16063
  }
@@ -15128,7 +16079,8 @@ var init_workset = __esm({
15128
16079
  // src/codegraph/current-facts.ts
15129
16080
  var current_facts_exports = {};
15130
16081
  __export(current_facts_exports, {
15131
- collectCurrentProjectFacts: () => collectCurrentProjectFacts
16082
+ collectCurrentProjectFacts: () => collectCurrentProjectFacts,
16083
+ formatGitFact: () => formatGitFact
15132
16084
  });
15133
16085
  import { execFileSync } from "child_process";
15134
16086
  import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
@@ -15173,11 +16125,16 @@ function runGit2(rootPath, args) {
15173
16125
  }
15174
16126
  }
15175
16127
  function readGitFacts(rootPath) {
15176
- const branch = runGit2(rootPath, ["branch", "--show-current"]);
16128
+ const available = runGit2(rootPath, ["rev-parse", "--is-inside-work-tree"]) === "true";
16129
+ if (!available) {
16130
+ return { available: false, dirty: false, detached: false };
16131
+ }
16132
+ const branch = runGit2(rootPath, ["symbolic-ref", "--quiet", "--short", "HEAD"]) ?? runGit2(rootPath, ["branch", "--show-current"]);
15177
16133
  const commit = runGit2(rootPath, ["rev-parse", "--short", "HEAD"]);
15178
16134
  const latestCommit = runGit2(rootPath, ["log", "-1", "--pretty=%s"]);
15179
16135
  const dirty = Boolean(runGit2(rootPath, ["status", "--porcelain"]));
15180
16136
  return {
16137
+ available: true,
15181
16138
  ...branch ? { branch } : {},
15182
16139
  ...commit ? { commit } : {},
15183
16140
  ...latestCommit ? { latestCommit } : {},
@@ -15185,6 +16142,15 @@ function readGitFacts(rootPath) {
15185
16142
  detached: !branch
15186
16143
  };
15187
16144
  }
16145
+ function formatGitFact(git) {
16146
+ if (!git.available) return "Git: unavailable";
16147
+ const parts = [];
16148
+ if (git.detached) parts.push("detached HEAD");
16149
+ else if (git.branch) parts.push("branch " + git.branch);
16150
+ if (git.commit) parts.push("commit " + git.commit);
16151
+ parts.push(git.dirty ? "dirty worktree" : "clean worktree");
16152
+ return "Git: " + parts.join(", ");
16153
+ }
15188
16154
  function parseProgressNote(content) {
15189
16155
  const lastUpdated = content.match(/Last updated\*\*:\s*(\d{4}-\d{2}-\d{2})/i) ?? content.match(/Last updated:\s*(\d{4}-\d{2}-\d{2})/i);
15190
16156
  const branchHint = content.match(/Branch\*\*:\s*([^\r\n]+)/i) ?? content.match(/Branch:\s*([^\r\n]+)/i);
@@ -15801,370 +16767,57 @@ function collectGraph(store, projectId, observations2, exclude) {
15801
16767
  }
15802
16768
  return {
15803
16769
  files,
15804
- symbols,
15805
- refs,
15806
- freshness,
15807
- sources,
15808
- suggestedReads: compactSuggestedReads(suggestedReads, 8, exclude)
15809
- };
15810
- }
15811
- function overviewFromGraph(input) {
15812
- const status = input.store.status(input.project.id);
15813
- return {
15814
- project: input.project,
15815
- code: {
15816
- provider: status.provider,
15817
- files: status.files,
15818
- symbols: status.symbols,
15819
- edges: status.edges,
15820
- refs: status.refs,
15821
- ...status.indexedAt ? { indexedAt: status.indexedAt } : {},
15822
- languages: countLanguages(input.graph.files),
15823
- ...status.latestSnapshot ? { latestSnapshot: status.latestSnapshot } : {}
15824
- },
15825
- memory: {
15826
- total: input.observations.filter((obs) => obs.projectId === input.project.id).length,
15827
- active: input.active.length
15828
- },
15829
- freshness: input.graph.freshness,
15830
- suggestedReads: input.graph.suggestedReads
15831
- };
15832
- }
15833
- function buildProjectContextExplain(input) {
15834
- const active = activeObservations(input.observations, input.project.id);
15835
- const graph = collectGraph(input.store, input.project.id, active, input.exclude);
15836
- const overview = overviewFromGraph({
15837
- project: input.project,
15838
- store: input.store,
15839
- observations: input.observations,
15840
- active,
15841
- graph
15842
- });
15843
- return {
15844
- project: input.project,
15845
- sources: graph.sources.sort((a, b) => a.observationId - b.observationId || (a.path ?? "").localeCompare(b.path ?? "")),
15846
- overview
15847
- };
15848
- }
15849
- var init_project_context = __esm({
15850
- "src/codegraph/project-context.ts"() {
15851
- "use strict";
15852
- init_esm_shims();
15853
- init_freshness2();
15854
- init_exclude();
15855
- }
15856
- });
15857
-
15858
- // src/codegraph/task-lens.ts
15859
- var task_lens_exports = {};
15860
- __export(task_lens_exports, {
15861
- lensPathCandidates: () => lensPathCandidates,
15862
- lensVerificationHints: () => lensVerificationHints,
15863
- rankLensPaths: () => rankLensPaths,
15864
- rankLensSources: () => rankLensSources,
15865
- resolveTaskLens: () => resolveTaskLens,
15866
- scoreLensSource: () => scoreLensSource,
15867
- shouldShowLensSource: () => shouldShowLensSource
15868
- });
15869
- function normalizePath2(path25) {
15870
- return path25.replace(/\\/g, "/");
15871
- }
15872
- function tokenize2(text) {
15873
- return (text.toLowerCase().match(/[a-z0-9_./-]+|[\u4e00-\u9fff]+/g) ?? []).map((token) => token.trim()).filter((token) => token.length > 1 && !STOP_WORDS.has(token));
15874
- }
15875
- function containsKeyword(text, keyword) {
15876
- if (/^[a-z0-9_-]+$/i.test(keyword)) {
15877
- return new RegExp(`(^|[^a-z0-9_-])${keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^a-z0-9_-]|$)`, "i").test(text);
15878
- }
15879
- return text.includes(keyword);
15880
- }
15881
- function resolveTaskLens(task) {
15882
- const normalized = (task ?? "").toLowerCase();
15883
- if (!normalized.trim()) return LENSES.general;
15884
- let best = {
15885
- id: "general",
15886
- score: 0,
15887
- priority: Number.MAX_SAFE_INTEGER
15888
- };
15889
- for (const id of LENS_PRIORITY) {
15890
- const score = KEYWORDS[id].reduce((sum, keyword) => sum + (containsKeyword(normalized, keyword) ? 1 : 0), 0);
15891
- const priority = LENS_PRIORITY.indexOf(id);
15892
- if (score > best.score || score === best.score && score > 0 && priority < best.priority) {
15893
- best = { id, score, priority };
15894
- }
15895
- }
15896
- return best.score > 0 ? LENSES[best.id] : LENSES.general;
15897
- }
15898
- function pathKindScore(path25, lens) {
15899
- const normalized = normalizePath2(path25).toLowerCase();
15900
- const name = normalized.split("/").pop() ?? normalized;
15901
- const inDocs = normalized.startsWith("docs/") || name === "readme.md" || name.includes("readme");
15902
- const isTest = /(^|\/)(tests?|__tests__|spec|fixtures?)\//.test(normalized) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(normalized) || /\.(test|spec)\.py$/.test(normalized);
15903
- const isSource = normalized.startsWith("src/") || normalized.includes("/src/");
15904
- const isPackage = name === "package.json" || name === "package-lock.json" || name === "pnpm-lock.yaml" || name === "yarn.lock";
15905
- const isChangelog = name === "changelog.md" || name === "changes.md";
15906
- const isWorkflow = normalized.startsWith(".github/workflows/");
15907
- switch (lens.id) {
15908
- case "bugfix":
15909
- return (isTest ? 80 : 0) + (isSource ? 50 : 0) + (normalized.includes("debug") ? 20 : 0);
15910
- case "test":
15911
- return (isTest ? 90 : 0) + (isSource ? 45 : 0);
15912
- case "release":
15913
- return (isChangelog ? 100 : 0) + (isPackage ? 90 : 0) + (isWorkflow ? 70 : 0) + (inDocs ? 35 : 0);
15914
- case "onboarding":
15915
- return (name === "readme.md" ? 100 : 0) + (inDocs ? 80 : 0) + (isPackage ? 45 : 0) + (isSource ? 20 : 0);
15916
- case "docs":
15917
- return (inDocs ? 100 : 0) + (isChangelog ? 60 : 0) + (isSource ? 25 : 0);
15918
- case "refactor":
15919
- return (isSource ? 70 : 0) + (isTest ? 65 : 0);
15920
- case "feature":
15921
- return (isSource ? 80 : 0) + (isTest ? 45 : 0) + (inDocs ? 20 : 0);
15922
- default:
15923
- if (isSource || isTest) return 60;
15924
- if (normalized.startsWith("packages/") && normalized.includes("/src/")) return 45;
15925
- if (inDocs) return 20;
15926
- return 0;
15927
- }
15928
- }
15929
- function tokenScore(text, tokens) {
15930
- if (tokens.length === 0) return 0;
15931
- const normalized = text.toLowerCase();
15932
- return tokens.reduce((sum, token) => sum + (normalized.includes(token) ? 12 : 0), 0);
15933
- }
15934
- function sourceTaskMatchScore(source, task) {
15935
- const tokens = tokenize2(task ?? "");
15936
- if (tokens.length === 0) return 0;
15937
- const text = [
15938
- source.title,
15939
- source.path ?? "",
15940
- source.symbol ?? ""
15941
- ].join("\n").toLowerCase();
15942
- const matches = tokens.filter((token) => text.includes(token)).length;
15943
- if (matches >= 2) return 40;
15944
- if (matches === 1) return 24;
15945
- return 0;
15946
- }
15947
- function fallbackPathRank(path25) {
15948
- const normalized = normalizePath2(path25);
15949
- if (normalized.startsWith("src/")) return 0;
15950
- if (normalized.startsWith("tests/") || normalized.startsWith("test/")) return 1;
15951
- if (normalized.startsWith("packages/") && normalized.includes("/src/")) return 2;
15952
- if (normalized.startsWith("docs/") || normalized.toLowerCase() === "readme.md") return 3;
15953
- return 4;
15954
- }
15955
- function rankLensPaths(paths, lens, task) {
15956
- const tokens = tokenize2(task ?? "");
15957
- return [...new Set(paths.map(normalizePath2))].map((path25, index) => ({
15958
- path: path25,
15959
- index,
15960
- score: pathKindScore(path25, lens) + tokenScore(path25, tokens),
15961
- fallback: fallbackPathRank(path25)
15962
- })).sort((a, b) => b.score - a.score || a.fallback - b.fallback || a.index - b.index || a.path.localeCompare(b.path)).map((item) => item.path);
15963
- }
15964
- function lensPathCandidates(lens) {
15965
- switch (lens.id) {
15966
- case "release":
15967
- return ["CHANGELOG.md", "package.json", "package-lock.json", ".github/workflows/ci.yml", ".github/workflows/test.yml", "README.md"];
15968
- case "onboarding":
15969
- return ["README.md", "docs/README.md", "docs/API_REFERENCE.md", "package.json"];
15970
- case "docs":
15971
- return ["README.md", "README.zh-CN.md", "docs/README.md", "docs/API_REFERENCE.md", "CHANGELOG.md"];
15972
- case "test":
15973
- case "bugfix":
15974
- return ["tests", "test"];
15975
- default:
15976
- return [];
15977
- }
15978
- }
15979
- function scoreLensSource(source, lens, task) {
15980
- const tokens = tokenize2(task ?? "");
15981
- const text = [source.title, source.type, source.path ?? "", source.symbol ?? ""].join("\n");
15982
- return pathKindScore(source.path ?? "", lens) + tokenScore(text, tokens) + sourceTaskMatchScore(source, task);
15983
- }
15984
- function rankLensSources(sources, lens, task) {
15985
- return [...sources].map((source, index) => ({
15986
- source,
15987
- index,
15988
- score: scoreLensSource(source, lens, task)
15989
- })).sort((a, b) => b.score - a.score || a.source.observationId - b.source.observationId || a.index - b.index).map((item) => item.source);
15990
- }
15991
- function lensVerificationHints(lens) {
15992
- switch (lens.id) {
15993
- case "bugfix":
15994
- return [
15995
- "run the smallest failing test or repro first",
15996
- "inspect the changed code path before trusting old memory"
15997
- ];
15998
- case "test":
15999
- return [
16000
- "run the exact focused test file or test name first",
16001
- "inspect fixtures and harness setup before changing assertions"
16002
- ];
16003
- case "release":
16004
- return [
16005
- "run build, tests, package smoke, and publish dry-run where available",
16006
- "verify package metadata, changelog, and Git state before publishing"
16007
- ];
16008
- case "onboarding":
16009
- return [
16010
- "read the docs/start files first, then inspect only the code paths needed for the task",
16011
- "treat old implementation memories as leads until current code confirms them"
16012
- ];
16013
- case "refactor":
16014
- return [
16015
- "inspect call sites and tests before editing shared code",
16016
- "run the narrow affected test plus one regression smoke"
16017
- ];
16018
- case "docs":
16019
- return [
16020
- "check headings, links, commands, and examples against current code",
16021
- "run the smallest docs or package smoke available"
16022
- ];
16023
- case "feature":
16024
- return [
16025
- "inspect the closest existing implementation pattern before adding new code",
16026
- "run focused tests plus one user-flow smoke after changes"
16027
- ];
16028
- default:
16029
- return [
16030
- "inspect the Start here files before editing",
16031
- "run the smallest relevant test or smoke command after changes"
16032
- ];
16033
- }
16770
+ symbols,
16771
+ refs,
16772
+ freshness,
16773
+ sources,
16774
+ suggestedReads: compactSuggestedReads(suggestedReads, 8, exclude)
16775
+ };
16034
16776
  }
16035
- function shouldShowLensSource(source, lens, task) {
16036
- if (lens.id === "general") return true;
16037
- const score = scoreLensSource(source, lens, task);
16038
- if (lens.id === "onboarding" || lens.id === "release" || lens.id === "docs") return score >= 40;
16039
- return score > 0;
16777
+ function overviewFromGraph(input) {
16778
+ const status = input.store.status(input.project.id);
16779
+ return {
16780
+ project: input.project,
16781
+ code: {
16782
+ provider: status.provider,
16783
+ files: status.files,
16784
+ symbols: status.symbols,
16785
+ edges: status.edges,
16786
+ refs: status.refs,
16787
+ ...status.indexedAt ? { indexedAt: status.indexedAt } : {},
16788
+ languages: countLanguages(input.graph.files),
16789
+ ...status.latestSnapshot ? { latestSnapshot: status.latestSnapshot } : {}
16790
+ },
16791
+ memory: {
16792
+ total: input.observations.filter((obs) => obs.projectId === input.project.id).length,
16793
+ active: input.active.length
16794
+ },
16795
+ freshness: input.graph.freshness,
16796
+ suggestedReads: input.graph.suggestedReads
16797
+ };
16040
16798
  }
16041
- var STOP_WORDS, LENSES, KEYWORDS, LENS_PRIORITY;
16042
- var init_task_lens = __esm({
16043
- "src/codegraph/task-lens.ts"() {
16799
+ function buildProjectContextExplain(input) {
16800
+ const active = activeObservations(input.observations, input.project.id);
16801
+ const graph = collectGraph(input.store, input.project.id, active, input.exclude);
16802
+ const overview = overviewFromGraph({
16803
+ project: input.project,
16804
+ store: input.store,
16805
+ observations: input.observations,
16806
+ active,
16807
+ graph
16808
+ });
16809
+ return {
16810
+ project: input.project,
16811
+ sources: graph.sources.sort((a, b) => a.observationId - b.observationId || (a.path ?? "").localeCompare(b.path ?? "")),
16812
+ overview
16813
+ };
16814
+ }
16815
+ var init_project_context = __esm({
16816
+ "src/codegraph/project-context.ts"() {
16044
16817
  "use strict";
16045
16818
  init_esm_shims();
16046
- STOP_WORDS = /* @__PURE__ */ new Set([
16047
- "a",
16048
- "an",
16049
- "and",
16050
- "as",
16051
- "for",
16052
- "in",
16053
- "into",
16054
- "of",
16055
- "on",
16056
- "onto",
16057
- "the",
16058
- "this",
16059
- "that",
16060
- "to",
16061
- "with",
16062
- "work",
16063
- "project",
16064
- "continue",
16065
- "\u7EE7\u7EED",
16066
- "\u9879\u76EE"
16067
- ]);
16068
- LENSES = {
16069
- bugfix: {
16070
- id: "bugfix",
16071
- description: "debug the failure with current code and the smallest repro first",
16072
- sourceLimit: 6,
16073
- cautionLimit: 4,
16074
- hideUnrelatedCautionDetails: true,
16075
- hideUnrelatedReliableDetails: false
16076
- },
16077
- feature: {
16078
- id: "feature",
16079
- description: "build a scoped feature from nearby source, types, and user flow",
16080
- sourceLimit: 6,
16081
- cautionLimit: 3,
16082
- hideUnrelatedCautionDetails: true,
16083
- hideUnrelatedReliableDetails: false
16084
- },
16085
- release: {
16086
- id: "release",
16087
- description: "prepare a release using current metadata, changelog, build, and package checks",
16088
- sourceLimit: 4,
16089
- cautionLimit: 2,
16090
- hideUnrelatedCautionDetails: true,
16091
- hideUnrelatedReliableDetails: true
16092
- },
16093
- onboarding: {
16094
- id: "onboarding",
16095
- description: "understand the project shape before trusting old implementation details",
16096
- sourceLimit: 4,
16097
- cautionLimit: 2,
16098
- hideUnrelatedCautionDetails: true,
16099
- hideUnrelatedReliableDetails: true
16100
- },
16101
- refactor: {
16102
- id: "refactor",
16103
- description: "change structure carefully by reading shared code, call sites, and tests",
16104
- sourceLimit: 6,
16105
- cautionLimit: 4,
16106
- hideUnrelatedCautionDetails: true,
16107
- hideUnrelatedReliableDetails: false
16108
- },
16109
- docs: {
16110
- id: "docs",
16111
- description: "update documentation against current code and public entry points",
16112
- sourceLimit: 5,
16113
- cautionLimit: 2,
16114
- hideUnrelatedCautionDetails: true,
16115
- hideUnrelatedReliableDetails: true
16116
- },
16117
- test: {
16118
- id: "test",
16119
- description: "work from tests, fixtures, harnesses, and the related source files",
16120
- sourceLimit: 6,
16121
- cautionLimit: 3,
16122
- hideUnrelatedCautionDetails: true,
16123
- hideUnrelatedReliableDetails: false
16124
- },
16125
- general: {
16126
- id: "general",
16127
- description: "balanced project handoff with current facts, code memory, and verification hints",
16128
- sourceLimit: 8,
16129
- cautionLimit: 5,
16130
- hideUnrelatedCautionDetails: false,
16131
- hideUnrelatedReliableDetails: false
16132
- }
16133
- };
16134
- KEYWORDS = {
16135
- bugfix: [
16136
- "bug",
16137
- "crash",
16138
- "debug",
16139
- "error",
16140
- "fail",
16141
- "failing",
16142
- "fix",
16143
- "issue",
16144
- "regression",
16145
- "repro",
16146
- "\u62A5\u9519",
16147
- "\u5D29\u6E83",
16148
- "\u5931\u8D25",
16149
- "\u4FEE\u590D",
16150
- "\u95EE\u9898"
16151
- ],
16152
- feature: ["add", "build", "feature", "implement", "new", "support", "\u65B0\u589E", "\u5B9E\u73B0", "\u652F\u6301", "\u529F\u80FD"],
16153
- release: ["bump", "changelog", "npm", "pack", "publish", "release", "version", "\u53D1\u7248", "\u53D1\u5E03", "\u7248\u672C"],
16154
- onboarding: ["architecture", "handoff", "onboard", "overview", "understand", "\u63A5\u624B", "\u4E86\u89E3", "\u7406\u89E3", "\u67B6\u6784"],
16155
- refactor: ["cleanup", "migrate", "refactor", "rename", "restructure", "\u91CD\u6784", "\u8FC1\u79FB", "\u6539\u9020"],
16156
- docs: ["doc", "docs", "readme", "\u6587\u6863", "\u8BF4\u660E"],
16157
- test: ["coverage", "fixture", "smoke", "spec", "test", "tests", "testing", "vitest", "\u6D4B\u8BD5"]
16158
- };
16159
- LENS_PRIORITY = [
16160
- "bugfix",
16161
- "release",
16162
- "test",
16163
- "refactor",
16164
- "feature",
16165
- "docs",
16166
- "onboarding"
16167
- ];
16819
+ init_freshness2();
16820
+ init_exclude();
16168
16821
  }
16169
16822
  });
16170
16823
 
@@ -16182,6 +16835,9 @@ import path19 from "path";
16182
16835
  function activeProjectObservations(observations2, projectId) {
16183
16836
  return observations2.filter((obs) => obs.projectId === projectId && (obs.status ?? "active") === "active");
16184
16837
  }
16838
+ function deliveryEligibleProjectObservations(observations2, projectId) {
16839
+ return activeProjectObservations(observations2, projectId).filter((observation) => isEligibleForAutomaticDelivery(observation));
16840
+ }
16185
16841
  function decideRefresh(input) {
16186
16842
  if (input.mode === "never") {
16187
16843
  return { performed: false, reason: "disabled", message: "Automatic project scan disabled." };
@@ -16256,13 +16912,27 @@ async function buildAutoProjectContext(input) {
16256
16912
  activeProjectObservations(input.observations, input.project.id)
16257
16913
  );
16258
16914
  try {
16259
- const { enqueueClaimRequalification: enqueueClaimRequalification2 } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
16915
+ const { MaintenanceTargetStore: MaintenanceTargetStore2 } = await Promise.resolve().then(() => (init_maintenance_targets(), maintenance_targets_exports));
16916
+ new MaintenanceTargetStore2(input.dataDir).register({
16917
+ projectId: input.project.id,
16918
+ projectRoot: input.project.rootPath,
16919
+ dataDir: input.dataDir
16920
+ });
16921
+ const {
16922
+ enqueueClaimRequalification: enqueueClaimRequalification2,
16923
+ enqueueObservationQualification: enqueueObservationQualification2
16924
+ } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
16260
16925
  enqueueClaimRequalification2({
16261
16926
  dataDir: input.dataDir,
16262
16927
  projectId: input.project.id,
16263
16928
  source: "foreground-refresh",
16264
16929
  snapshotId: store.latestSnapshot(input.project.id)?.id
16265
16930
  });
16931
+ enqueueObservationQualification2({
16932
+ dataDir: input.dataDir,
16933
+ projectId: input.project.id,
16934
+ source: "foreground-refresh"
16935
+ });
16266
16936
  } catch {
16267
16937
  }
16268
16938
  refresh = { ...refresh, backfill };
@@ -16279,7 +16949,9 @@ async function buildAutoProjectContext(input) {
16279
16949
  const explain = buildProjectContextExplain({
16280
16950
  project: input.project,
16281
16951
  store,
16282
- observations: input.observations,
16952
+ // Binding sees all active evidence, but automatic delivery sees only
16953
+ // evidence that has passed the control-plane admission gate.
16954
+ observations: deliveryEligibleProjectObservations(input.observations, input.project.id),
16283
16955
  exclude
16284
16956
  });
16285
16957
  const overview = explain.overview;
@@ -16370,7 +17042,8 @@ async function buildAutoProjectContext(input) {
16370
17042
  suspect: overview.freshness.suspect,
16371
17043
  stale: overview.freshness.stale
16372
17044
  },
16373
- runtimeCautions
17045
+ runtimeCautions,
17046
+ ...input.deliveryTarget ? { deliveryTarget: input.deliveryTarget } : {}
16374
17047
  });
16375
17048
  return {
16376
17049
  project: input.project,
@@ -16463,15 +17136,7 @@ function formatCurrentFactsLines(facts) {
16463
17136
  if (facts.latestChangelog) {
16464
17137
  lines.push(`- Latest changelog: ${facts.latestChangelog.version}${facts.latestChangelog.date ? ` (${facts.latestChangelog.date})` : ""}`);
16465
17138
  }
16466
- const gitParts = [];
16467
- if (facts.git.detached) {
16468
- gitParts.push("detached HEAD");
16469
- } else if (facts.git.branch) {
16470
- gitParts.push(`branch ${facts.git.branch}`);
16471
- }
16472
- if (facts.git.commit) gitParts.push(`commit ${facts.git.commit}`);
16473
- gitParts.push(facts.git.dirty ? "dirty worktree" : "clean worktree");
16474
- lines.push(`- Git: ${gitParts.join(", ")}`);
17139
+ lines.push("- " + formatGitFact(facts.git));
16475
17140
  if (facts.git.latestCommit) lines.push(`- Latest commit: ${facts.git.latestCommit}`);
16476
17141
  lines.push("- Current facts above outrank progress/dev-log files when they conflict.");
16477
17142
  if (facts.staleNotes.length > 0) {
@@ -16493,11 +17158,7 @@ function worksetFactLines(facts) {
16493
17158
  if (facts.latestChangelog) {
16494
17159
  lines.push("Latest changelog: " + facts.latestChangelog.version + (facts.latestChangelog.date ? " (" + facts.latestChangelog.date + ")" : ""));
16495
17160
  }
16496
- const gitParts = [];
16497
- if (facts.git.branch) gitParts.push("branch " + facts.git.branch);
16498
- if (facts.git.commit) gitParts.push("commit " + facts.git.commit);
16499
- gitParts.push(facts.git.dirty ? "dirty worktree" : "clean worktree");
16500
- lines.push("Git: " + gitParts.join(", "));
17161
+ lines.push(formatGitFact(facts.git));
16501
17162
  for (const note of facts.staleNotes.slice(0, 1)) {
16502
17163
  lines.push(
16503
17164
  "Historical note: " + note.path + (note.branchHint ? " (branch hint " + note.branchHint + "; " + note.reason + ")" : " (" + note.reason + ")")
@@ -16616,6 +17277,7 @@ var init_auto_context = __esm({
16616
17277
  init_external_provider();
16617
17278
  init_project_context();
16618
17279
  init_store();
17280
+ init_admission();
16619
17281
  init_task_lens();
16620
17282
  DEFAULT_MAX_AGE_MS = 10 * 60 * 1e3;
16621
17283
  }
@@ -16808,7 +17470,8 @@ async function attachTaskWorkset(input) {
16808
17470
  suspect: input.pack.warnings.filter((warning) => warning.status === "suspect").length,
16809
17471
  stale: input.pack.warnings.filter((warning) => warning.status === "stale").length
16810
17472
  },
16811
- runtimeCautions: input.runtimeCautions
17473
+ runtimeCautions: input.runtimeCautions,
17474
+ deliveryTarget: "context-pack"
16812
17475
  });
16813
17476
  return { ...input.pack, workset };
16814
17477
  }
@@ -18220,11 +18883,14 @@ __export(export_import_exports, {
18220
18883
  exportAsMarkdown: () => exportAsMarkdown,
18221
18884
  importFromJson: () => importFromJson
18222
18885
  });
18223
- async function exportAsJson(projectDir2, projectId) {
18886
+ async function exportAsJson(projectDir2, projectId, reader) {
18224
18887
  const store = getObservationStore();
18225
18888
  const allObs = await store.loadAll();
18226
18889
  const allSessions = await getSessionStore().loadAll();
18227
- const projectObs = allObs.filter((o) => o.projectId === projectId);
18890
+ const projectObs = filterReadableObservations(
18891
+ allObs.filter((o) => o.projectId === projectId),
18892
+ reader
18893
+ );
18228
18894
  const projectSessions = allSessions.filter((s) => s.projectId === projectId);
18229
18895
  const typeBreakdown = {};
18230
18896
  for (const obs of projectObs) {
@@ -18243,8 +18909,8 @@ async function exportAsJson(projectDir2, projectId) {
18243
18909
  }
18244
18910
  };
18245
18911
  }
18246
- async function exportAsMarkdown(projectDir2, projectId) {
18247
- const data = await exportAsJson(projectDir2, projectId);
18912
+ async function exportAsMarkdown(projectDir2, projectId, reader) {
18913
+ const data = await exportAsJson(projectDir2, projectId, reader);
18248
18914
  const lines = [];
18249
18915
  lines.push(`# Memorix Export: ${projectId}`);
18250
18916
  lines.push(`Exported: ${data.exportedAt}`);
@@ -18346,6 +19012,7 @@ var init_export_import = __esm({
18346
19012
  "src/memory/export-import.ts"() {
18347
19013
  "use strict";
18348
19014
  init_esm_shims();
19015
+ init_visibility();
18349
19016
  init_obs_store();
18350
19017
  init_session_store();
18351
19018
  OBSERVATION_ICONS2 = {
@@ -18522,6 +19189,7 @@ function isEligible(o, projectId) {
18522
19189
  if (isCommandLog2(o)) return false;
18523
19190
  if (isInactive(o)) return false;
18524
19191
  if (isOtherProject(o, projectId)) return false;
19192
+ if (!isEligibleForKnowledgePromotion(o)) return false;
18525
19193
  if (o.valueCategory === "ephemeral") return false;
18526
19194
  if (!contextualHasSubstance(o)) return false;
18527
19195
  return true;
@@ -18670,6 +19338,7 @@ var init_generator = __esm({
18670
19338
  "src/wiki/generator.ts"() {
18671
19339
  "use strict";
18672
19340
  init_esm_shims();
19341
+ init_admission();
18673
19342
  COMMAND_LOG_TITLE4 = /^(Ran:|Command:|Executed:)\s/i;
18674
19343
  SECTION_DEFS = [
18675
19344
  {
@@ -18956,6 +19625,9 @@ function sendError(res, message, status = 500) {
18956
19625
  function filterByProject(items, projectId) {
18957
19626
  return items.filter((item) => item.projectId === projectId);
18958
19627
  }
19628
+ function filterDashboardObservations(observations2, projectId) {
19629
+ return filterReadableObservations(observations2, { projectId });
19630
+ }
18959
19631
  function isActiveStatus(status) {
18960
19632
  return (status ?? "active") === "active";
18961
19633
  }
@@ -18979,13 +19651,8 @@ function prepareDashboardConfig(projectRoot) {
18979
19651
  }
18980
19652
  }
18981
19653
  function computeProjectGraphCounts(allEntities, allRelations, projectObs) {
18982
- const entityNames = new Set(
18983
- projectObs.filter((o) => (o.status ?? "active") === "active" && o.entityName).map((o) => o.entityName)
18984
- );
18985
- const entities = allEntities.filter((e) => entityNames.has(e.name));
18986
- const entityNameSet = new Set(entities.map((e) => e.name));
18987
- const relations = allRelations.filter((r) => entityNameSet.has(r.from) && entityNameSet.has(r.to));
18988
- return { entities: entities.length, relations: relations.length, entityNames };
19654
+ const scoped = scopeKnowledgeGraphToProject({ entities: allEntities, relations: allRelations }, projectObs);
19655
+ return { entities: scoped.entities.length, relations: scoped.relations.length, entityNames: scoped.entityNames };
18989
19656
  }
18990
19657
  async function handleApi(req, res, dataDir, projectId, projectName, baseDir, projectRoot, projectResolved, mode = "standalone", port = 3210) {
18991
19658
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
@@ -19008,7 +19675,10 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19008
19675
  switch (apiPath) {
19009
19676
  case "/projects": {
19010
19677
  try {
19011
- const allObs = await getObservationStore().loadAll();
19678
+ const allObs = filterReadableObservations(
19679
+ await getObservationStore().loadAll(),
19680
+ {}
19681
+ );
19012
19682
  const projectSet = /* @__PURE__ */ new Map();
19013
19683
  for (const obs of allObs) {
19014
19684
  if (!isActiveStatus(obs.status)) continue;
@@ -19057,18 +19727,19 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19057
19727
  await initGraphStore(effectiveDataDir);
19058
19728
  const gStore = getGraphStore();
19059
19729
  const graph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
19060
- const graphObs = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19061
- const projectEntityNames = new Set(
19062
- graphObs.filter((o) => o.entityName).map((o) => o.entityName)
19730
+ const graphObs = filterDashboardObservations(
19731
+ await getObservationStore().loadByProject(effectiveProjectId, { status: "active" }),
19732
+ effectiveProjectId
19063
19733
  );
19064
- const entities = graph.entities.filter((e) => projectEntityNames.has(e.name));
19065
- const entityNameSet = new Set(entities.map((e) => e.name));
19066
- const relations = graph.relations.filter((r) => entityNameSet.has(r.from) && entityNameSet.has(r.to));
19067
- sendJson(res, { entities, relations });
19734
+ const scoped = scopeKnowledgeGraphToProject(graph, graphObs);
19735
+ sendJson(res, { entities: scoped.entities, relations: scoped.relations });
19068
19736
  break;
19069
19737
  }
19070
19738
  case "/observations": {
19071
- const observations2 = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19739
+ const observations2 = filterDashboardObservations(
19740
+ await getObservationStore().loadByProject(effectiveProjectId, { status: "active" }),
19741
+ effectiveProjectId
19742
+ );
19072
19743
  sendJson(res, observations2);
19073
19744
  break;
19074
19745
  }
@@ -19081,7 +19752,10 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19081
19752
  case "/stats": {
19082
19753
  await initGraphStore(effectiveDataDir);
19083
19754
  const graph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
19084
- const observations2 = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19755
+ const observations2 = filterDashboardObservations(
19756
+ await getObservationStore().loadByProject(effectiveProjectId, { status: "active" }),
19757
+ effectiveProjectId
19758
+ );
19085
19759
  const nextId2 = await getObservationStore().loadIdCounter();
19086
19760
  const projectGraphCounts = computeProjectGraphCounts(graph.entities, graph.relations, observations2);
19087
19761
  const typeCounts = {};
@@ -19199,7 +19873,10 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19199
19873
  break;
19200
19874
  }
19201
19875
  case "/retention": {
19202
- const observations2 = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19876
+ const observations2 = filterDashboardObservations(
19877
+ await getObservationStore().loadByProject(effectiveProjectId, { status: "active" }),
19878
+ effectiveProjectId
19879
+ );
19203
19880
  const now3 = Date.now();
19204
19881
  const scored = observations2.filter((obs) => obs.type !== "probe").map((obs) => {
19205
19882
  const age = now3 - new Date(obs.createdAt || now3).getTime();
@@ -19237,7 +19914,10 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19237
19914
  const { generateKnowledgeBase: generateKnowledgeBase2 } = await Promise.resolve().then(() => (init_generator(), generator_exports));
19238
19915
  const { initMiniSkillStore: initMiniSkillStore2, getMiniSkillStore: getMiniSkillStore2 } = await Promise.resolve().then(() => (init_mini_skill_store(), mini_skill_store_exports));
19239
19916
  await initMiniSkillStore2(effectiveDataDir);
19240
- const allObs = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19917
+ const allObs = filterDashboardObservations(
19918
+ await getObservationStore().loadByProject(effectiveProjectId, { status: "active" }),
19919
+ effectiveProjectId
19920
+ );
19241
19921
  const skills = await getMiniSkillStore2().loadByProject(effectiveProjectId);
19242
19922
  const overview = generateKnowledgeBase2({
19243
19923
  projectId: effectiveProjectId,
@@ -19253,22 +19933,19 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19253
19933
  const { initGraphStore: initGraphStore2, getGraphStore: getGraphStore2 } = await Promise.resolve().then(() => (init_graph_store(), graph_store_exports));
19254
19934
  await initMiniSkillStore2(effectiveDataDir);
19255
19935
  await initGraphStore2(effectiveDataDir);
19256
- const allObs = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19936
+ const allObs = filterDashboardObservations(
19937
+ await getObservationStore().loadByProject(effectiveProjectId, { status: "active" }),
19938
+ effectiveProjectId
19939
+ );
19257
19940
  const skills = await getMiniSkillStore2().loadByProject(effectiveProjectId);
19258
19941
  const fullGraph = { entities: getGraphStore2().loadEntities(), relations: getGraphStore2().loadRelations() };
19259
- const graphObs = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19260
- const projectEntityNames = new Set(
19261
- graphObs.filter((o) => o.entityName).map((o) => o.entityName)
19262
- );
19263
- const scopedEntities = fullGraph.entities.filter((e) => projectEntityNames.has(e.name));
19264
- const scopedEntityNameSet = new Set(scopedEntities.map((e) => e.name));
19265
- const scopedRelations = fullGraph.relations.filter((r) => scopedEntityNameSet.has(r.from) && scopedEntityNameSet.has(r.to));
19942
+ const scoped = scopeKnowledgeGraphToProject(fullGraph, allObs);
19266
19943
  const graph = generateKnowledgeGraph2({
19267
19944
  projectId: effectiveProjectId,
19268
19945
  observations: allObs,
19269
19946
  miniSkills: skills,
19270
- graphEntities: scopedEntities,
19271
- graphRelations: scopedRelations
19947
+ graphEntities: scoped.entities,
19948
+ graphRelations: scoped.relations
19272
19949
  });
19273
19950
  sendJson(res, graph);
19274
19951
  break;
@@ -19382,7 +20059,7 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19382
20059
  break;
19383
20060
  }
19384
20061
  case "/identity": {
19385
- const allObs = await getObservationStore().loadAll();
20062
+ const allObs = filterReadableObservations(await getObservationStore().loadAll(), {});
19386
20063
  const allProjectIds = [...new Set(allObs.map((o) => o.projectId).filter(Boolean))];
19387
20064
  let classifyProjectId2 = () => "real";
19388
20065
  let isDirtyProjectId2 = () => false;
@@ -19467,6 +20144,8 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19467
20144
  sendError(res, "Observation not found", 404);
19468
20145
  } else if (matchObs.projectId !== effectiveProjectId) {
19469
20146
  sendError(res, `Observation #${obsId} belongs to project "${matchObs.projectId}", not "${effectiveProjectId}"`, 403);
20147
+ } else if (!canManageObservation(matchObs, { projectId: effectiveProjectId })) {
20148
+ sendError(res, `Observation #${obsId} is not manageable from the unbound dashboard.`, 403);
19470
20149
  } else {
19471
20150
  await obsStore.remove(obsId);
19472
20151
  try {
@@ -19488,18 +20167,16 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19488
20167
  if (apiPath === "/export") {
19489
20168
  await initGraphStore(effectiveDataDir);
19490
20169
  const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
19491
- const observations2 = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19492
- const nextId2 = await getObservationStore().loadIdCounter();
19493
- const exportEntityNames = new Set(
19494
- observations2.filter((o) => (o.status ?? "active") === "active" && o.entityName).map((o) => o.entityName)
20170
+ const observations2 = filterDashboardObservations(
20171
+ await getObservationStore().loadByProject(effectiveProjectId, { status: "active" }),
20172
+ effectiveProjectId
19495
20173
  );
19496
- const exportEntities = fullGraph.entities.filter((e) => exportEntityNames.has(e.name));
19497
- const exportEntitySet = new Set(exportEntities.map((e) => e.name));
19498
- const exportRelations = fullGraph.relations.filter((r) => exportEntitySet.has(r.from) && exportEntitySet.has(r.to));
20174
+ const nextId2 = await getObservationStore().loadIdCounter();
20175
+ const scoped = scopeKnowledgeGraphToProject(fullGraph, observations2);
19499
20176
  const exportData = {
19500
20177
  project: { id: effectiveProjectId, name: effectiveProjectName },
19501
20178
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
19502
- graph: { entities: exportEntities, relations: exportRelations },
20179
+ graph: { entities: scoped.entities, relations: scoped.relations },
19503
20180
  observations: observations2,
19504
20181
  nextId: nextId2
19505
20182
  };
@@ -19816,6 +20493,8 @@ var init_server = __esm({
19816
20493
  init_dotenv_loader();
19817
20494
  init_yaml_loader();
19818
20495
  init_yaml_loader();
20496
+ init_graph_scope();
20497
+ init_visibility();
19819
20498
  MIME_TYPES = {
19820
20499
  ".html": "text/html; charset=utf-8",
19821
20500
  ".css": "text/css; charset=utf-8",
@@ -20593,6 +21272,11 @@ async function createHandoffArtifact(input, storeObservation2, teamStore) {
20593
21272
  sourceDetail: "explicit",
20594
21273
  valueCategory: "core",
20595
21274
  // immune to retention archival
21275
+ // A direct handoff is not a project-wide memo. The sender retains access
21276
+ // and the explicit recipient receives a read grant. Broadcast handoffs
21277
+ // are deliberately visible only to active team members.
21278
+ visibility: input.toAgentId ? "personal" : "team",
21279
+ ...input.toAgentId ? { sharedWithAgentIds: [input.toAgentId] } : {},
20596
21280
  createdByAgentId: input.fromAgentId
20597
21281
  });
20598
21282
  try {
@@ -22298,6 +22982,7 @@ var init_hooks_path = __esm({
22298
22982
 
22299
22983
  // src/sdk.ts
22300
22984
  init_esm_shims();
22985
+ init_visibility();
22301
22986
 
22302
22987
  // src/server.ts
22303
22988
  init_esm_shims();
@@ -22443,6 +23128,24 @@ async function createAutoRelations(obs, extracted, graphManager) {
22443
23128
  const relationType = inferRelationType(obs);
22444
23129
  const relations = [];
22445
23130
  const selfName = obs.entityName.toLowerCase();
23131
+ const explicitRelated = [...new Set((obs.relatedEntities ?? []).map((name) => name.trim()).filter((name) => name && name.toLowerCase() !== selfName))];
23132
+ if (explicitRelated.length > 0) {
23133
+ await graphManager.createEntities(explicitRelated.map((name) => ({
23134
+ name,
23135
+ entityType: "related",
23136
+ observations: []
23137
+ })));
23138
+ for (const name of explicitRelated) {
23139
+ const matchedEntity = graphManager.findEntityByName(name);
23140
+ if (matchedEntity) {
23141
+ relations.push({
23142
+ from: obs.entityName,
23143
+ to: matchedEntity.name,
23144
+ relationType: "related_entity"
23145
+ });
23146
+ }
23147
+ }
23148
+ }
22446
23149
  const candidates = [
22447
23150
  ...extracted.identifiers,
22448
23151
  ...extracted.files.map((f) => f.split("/").pop()?.replace(/\.\w+$/, "") ?? ""),
@@ -22484,6 +23187,8 @@ async function createAutoRelations(obs, extracted, graphManager) {
22484
23187
 
22485
23188
  // src/server.ts
22486
23189
  init_entity_extractor();
23190
+ init_graph_scope();
23191
+ init_visibility();
22487
23192
 
22488
23193
  // src/compact/engine.ts
22489
23194
  init_esm_shims();
@@ -22609,7 +23314,15 @@ function formatTimeline(timeline) {
22609
23314
  function formatObservationDetail(doc) {
22610
23315
  const icon = getTypeIcon(doc.type);
22611
23316
  const lines = [];
22612
- const header = buildProvenanceHeader(doc.sourceDetail, doc.valueCategory, doc.source, doc.commitHash, doc.relatedCommits);
23317
+ const header = buildProvenanceHeader(
23318
+ doc.sourceDetail,
23319
+ doc.valueCategory,
23320
+ doc.admissionState,
23321
+ doc.admissionReason,
23322
+ doc.source,
23323
+ doc.commitHash,
23324
+ doc.relatedCommits
23325
+ );
22613
23326
  if (header) {
22614
23327
  lines.push(header);
22615
23328
  lines.push("");
@@ -22644,7 +23357,7 @@ function formatObservationDetail(doc) {
22644
23357
  }
22645
23358
  return lines.join("\n");
22646
23359
  }
22647
- function buildProvenanceHeader(sourceDetail, valueCategory, source, commitHash, relatedCommits) {
23360
+ function buildProvenanceHeader(sourceDetail, valueCategory, admissionState, admissionReason, source, commitHash, relatedCommits) {
22648
23361
  const sd = resolveSourceDetail(sourceDetail, source);
22649
23362
  if (!sd) return "";
22650
23363
  const label = sourceKindLabel(sd);
@@ -22655,11 +23368,21 @@ function buildProvenanceHeader(sourceDetail, valueCategory, source, commitHash,
22655
23368
  if (verificationLine) {
22656
23369
  lines.push(verificationLine);
22657
23370
  }
22658
- if (valueCategory === "core") {
23371
+ if (valueCategory === "core" && admissionState !== "candidate" && admissionState !== "ephemeral") {
22659
23372
  lines.push("[CORE] Core \u2014 immune to decay");
23373
+ } else if (valueCategory === "core") {
23374
+ lines.push("[CORE] Core candidate \u2014 not durable until qualification completes");
22660
23375
  } else if (valueCategory === "ephemeral") {
22661
23376
  lines.push("[WARN] Ephemeral \u2014 short-lived signal");
22662
23377
  }
23378
+ if (admissionState === "candidate") {
23379
+ lines.push("[PENDING] Candidate \u2014 excluded from automatic context until current Code Memory qualifies it");
23380
+ } else if (admissionState === "ephemeral") {
23381
+ lines.push("[TRACE] Ephemeral capture \u2014 excluded from automatic context");
23382
+ } else if (admissionState === "qualified") {
23383
+ lines.push("[QUALIFIED] Automatic evidence passed current Code Memory qualification");
23384
+ }
23385
+ if (admissionReason) lines.push(`Admission: ${redactCredentials(admissionReason)}`);
22663
23386
  return lines.join("\n");
22664
23387
  }
22665
23388
  function sourceKindLabel(sd) {
@@ -22740,6 +23463,7 @@ init_obs_store();
22740
23463
  init_mini_skill_store();
22741
23464
  init_mini_skills();
22742
23465
  init_secret_filter();
23466
+ init_visibility();
22743
23467
  function normalizeMemoryBrowseQuery(query) {
22744
23468
  const trimmed = query.trim();
22745
23469
  if (!trimmed) return "";
@@ -22782,11 +23506,13 @@ async function compactSearch(options) {
22782
23506
  );
22783
23507
  let formatted = formatIndexTable(entries, searchOptions.query, !options.projectId);
22784
23508
  if (entries.length === 0 && options.projectId) {
22785
- const allObservations = getAllObservations();
23509
+ const allObservations = filterReadableObservations(getAllObservations(), options.reader);
22786
23510
  const projectAliases = new Set(await resolveAliases(options.projectId).catch(() => [options.projectId]));
22787
23511
  const projectHasStoredMemory = allObservations.some((obs) => obs.projectId && projectAliases.has(obs.projectId));
22788
23512
  if (!projectHasStoredMemory) {
22789
- formatted = `This project does not have any Memorix memories yet.
23513
+ formatted = options.reader ? `No Memorix memories are available to this session yet.
23514
+
23515
+ The tool call worked, but this session has no project-visible memory to retrieve.` : `This project does not have any Memorix memories yet.
22790
23516
 
22791
23517
  It looks like a fresh project: the tool call worked, but there is nothing stored to retrieve yet.
22792
23518
 
@@ -22800,8 +23526,8 @@ This project does have stored Memorix memories, but none matched the current que
22800
23526
  const totalTokens = countTextTokens(formatted);
22801
23527
  return { entries, formatted, totalTokens };
22802
23528
  }
22803
- async function compactTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3) {
22804
- const result = await getTimeline(anchorId, projectId, depthBefore, depthAfter);
23529
+ async function compactTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3, reader) {
23530
+ const result = await getTimeline(anchorId, projectId, depthBefore, depthAfter, reader);
22805
23531
  const timeline = {
22806
23532
  anchorId,
22807
23533
  anchorEntry: result.anchor,
@@ -22812,7 +23538,7 @@ async function compactTimeline(anchorId, projectId, depthBefore = 3, depthAfter
22812
23538
  const totalTokens = countTextTokens(formatted);
22813
23539
  return { timeline, formatted, totalTokens };
22814
23540
  }
22815
- async function compactDetail(idsOrRefs) {
23541
+ async function compactDetail(idsOrRefs, options = {}) {
22816
23542
  const invalidRefs = [];
22817
23543
  const parsedRefs = [];
22818
23544
  for (let i = 0; i < idsOrRefs.length; i++) {
@@ -22869,7 +23595,7 @@ async function compactDetail(idsOrRefs) {
22869
23595
  const missingRefs = [];
22870
23596
  for (const ref of refs) {
22871
23597
  const obs = getObservation(ref.id, ref.projectId);
22872
- if (obs && (ref.projectId ? obs.projectId === ref.projectId : true)) {
23598
+ if (obs && (ref.projectId ? obs.projectId === ref.projectId : true) && canReadObservation(obs, options.reader)) {
22873
23599
  documentMap.set(toRefKey(ref), {
22874
23600
  id: makeOramaObservationId(obs.projectId, obs.id),
22875
23601
  observationId: obs.id,
@@ -22888,7 +23614,12 @@ async function compactDetail(idsOrRefs) {
22888
23614
  status: obs.status ?? "active",
22889
23615
  source: obs.source ?? "agent",
22890
23616
  sourceDetail: obs.sourceDetail ?? "",
22891
- valueCategory: obs.valueCategory ?? ""
23617
+ valueCategory: obs.valueCategory ?? "",
23618
+ admissionState: obs.admissionState ?? "",
23619
+ admissionReason: obs.admissionReason ?? "",
23620
+ visibility: obs.visibility ?? "project",
23621
+ createdByAgentId: obs.createdByAgentId ?? "",
23622
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? [])
22892
23623
  });
22893
23624
  } else {
22894
23625
  missingRefs.push(ref);
@@ -22898,17 +23629,17 @@ async function compactDetail(idsOrRefs) {
22898
23629
  for (const ref of missingRefs) {
22899
23630
  const fallbackDocs = await getObservationsByIds([ref.id], ref.projectId);
22900
23631
  const doc = fallbackDocs[0];
22901
- if (doc) {
23632
+ if (doc && canReadObservation(doc, options.reader)) {
22902
23633
  documentMap.set(toRefKey(ref), doc);
22903
23634
  continue;
22904
23635
  }
22905
23636
  const stored = await getPersistedObservation(ref);
22906
- if (stored) {
23637
+ if (stored && canReadObservation(stored, options.reader)) {
22907
23638
  documentMap.set(toRefKey(ref), observationToDocument(stored));
22908
23639
  }
22909
23640
  }
22910
23641
  }
22911
- const allObs = getAllObservations();
23642
+ const allObs = filterReadableObservations(getAllObservations(), options.reader);
22912
23643
  const crossRefMap = /* @__PURE__ */ new Map();
22913
23644
  for (const ref of refs) {
22914
23645
  const obs = getObservation(ref.id, ref.projectId);
@@ -23022,7 +23753,12 @@ function observationToDocument(obs) {
23022
23753
  status: obs.status ?? "active",
23023
23754
  source: obs.source ?? "agent",
23024
23755
  sourceDetail: obs.sourceDetail ?? "",
23025
- valueCategory: obs.valueCategory ?? ""
23756
+ valueCategory: obs.valueCategory ?? "",
23757
+ admissionState: obs.admissionState ?? "",
23758
+ admissionReason: obs.admissionReason ?? "",
23759
+ visibility: obs.visibility ?? "project",
23760
+ createdByAgentId: obs.createdByAgentId ?? "",
23761
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? [])
23026
23762
  };
23027
23763
  }
23028
23764
  function formatMiniSkillDetail(skill2, provenanceStatus) {
@@ -23141,7 +23877,9 @@ function auditMemoryQuality(observations2, options) {
23141
23877
  status: obs.status ?? "active",
23142
23878
  source: obs.source ?? "agent",
23143
23879
  sourceDetail: obs.sourceDetail ?? "",
23144
- valueCategory: obs.valueCategory ?? ""
23880
+ valueCategory: obs.valueCategory ?? "",
23881
+ admissionState: obs.admissionState ?? "",
23882
+ admissionReason: obs.admissionReason ?? ""
23145
23883
  }, options.referenceTime) !== "active").map((obs) => makeEntry(obs, "Outside active retention zone"));
23146
23884
  const summary = {
23147
23885
  total: scoped.length,
@@ -23183,6 +23921,7 @@ function auditMemoryQuality(observations2, options) {
23183
23921
 
23184
23922
  // src/memory/graph-context.ts
23185
23923
  init_retention();
23924
+ init_admission();
23186
23925
  function formatGraphContextPrompt(packet) {
23187
23926
  const lines = [
23188
23927
  "## Memory Context Packet",
@@ -23340,7 +24079,9 @@ function scoreForPacket(obs, queryTokens, referenceTime, broadMemoryQuery) {
23340
24079
  status: obs.status ?? "active",
23341
24080
  source: obs.source ?? "agent",
23342
24081
  sourceDetail: obs.sourceDetail ?? "",
23343
- valueCategory: obs.valueCategory ?? ""
24082
+ valueCategory: obs.valueCategory ?? "",
24083
+ admissionState: obs.admissionState ?? "",
24084
+ admissionReason: obs.admissionReason ?? ""
23344
24085
  }, referenceTime) !== "active") {
23345
24086
  score -= 2;
23346
24087
  }
@@ -23434,8 +24175,11 @@ function buildGraphContextPacket(observations2, options) {
23434
24175
  ...audit.issues.orphans.map((entry) => entry.id),
23435
24176
  ...audit.issues.retentionCandidates.map((entry) => entry.id)
23436
24177
  ]);
23437
- const filteredObservations = observations2.filter((obs) => obs.projectId === options.projectId && !riskIds.has(obs.id));
23438
- const baseObservations = filteredObservations.length > 0 ? filteredObservations : observations2;
24178
+ const deliveryEligible = observations2.filter(
24179
+ (obs) => obs.projectId === options.projectId && isEligibleForAutomaticDelivery(obs)
24180
+ );
24181
+ const filteredObservations = deliveryEligible.filter((obs) => !riskIds.has(obs.id));
24182
+ const baseObservations = filteredObservations.length > 0 ? filteredObservations : deliveryEligible;
23439
24183
  const memories = pickMemories(baseObservations, options.projectId, options.query, options.limit ?? 5, referenceTime);
23440
24184
  const entities = buildEntities(memories);
23441
24185
  const entityNames = new Set(entities.map((entity) => entity.name));
@@ -25922,6 +26666,8 @@ async function createMemorixServer(cwd, existingServer, sharedTeam, options = {}
25922
26666
  let projectResolutionError = null;
25923
26667
  let explicitProjectBound = false;
25924
26668
  let currentAgentId;
26669
+ let teamStore;
26670
+ let initTeamStoreForProject;
25925
26671
  if (detectedProject) {
25926
26672
  rawProject = detectedProject;
25927
26673
  } else {
@@ -26169,7 +26915,7 @@ The path should point to a directory containing a .git folder.`
26169
26915
  };
26170
26916
  const server = existingServer ?? new McpServer({
26171
26917
  name: "memorix",
26172
- version: true ? "1.2.0" : "1.0.1"
26918
+ version: true ? "1.2.2" : "1.0.1"
26173
26919
  });
26174
26920
  const originalRegisterTool = server.registerTool.bind(server);
26175
26921
  server.registerTool = ((name, ...args) => {
@@ -26189,11 +26935,26 @@ The path should point to a directory containing a .git folder.`
26189
26935
  }
26190
26936
  return originalRegisterTool(name, ...args);
26191
26937
  });
26938
+ const getObservationReader = (scope = "project") => {
26939
+ let isTeamMember = false;
26940
+ if (teamFeaturesEnabled && currentAgentId) {
26941
+ try {
26942
+ const agent = teamStore.getAgent(currentAgentId);
26943
+ isTeamMember = agent?.project_id === project.id && agent.status === "active";
26944
+ } catch {
26945
+ }
26946
+ }
26947
+ return {
26948
+ ...scope === "project" ? { projectId: project.id } : {},
26949
+ ...currentAgentId ? { agentId: currentAgentId } : {},
26950
+ isTeamMember
26951
+ };
26952
+ };
26192
26953
  server.registerTool(
26193
26954
  "memorix_store",
26194
26955
  {
26195
26956
  title: "Store Memory",
26196
- description: "Store a new observation/memory. Automatically indexed for search. Use type to classify: gotcha ([GOTCHA] critical pitfall), decision ([DECISION] architecture choice), problem-solution ([FIX] bug fix), how-it-works ([INFO] explanation), what-changed ([CHANGE] change), discovery ([DISCOVERY] insight), why-it-exists ([WHY] rationale), trade-off ([TRADEOFF] compromise), session-request ([SESSION] original goal). Stored memories persist across sessions and are shared with other IDEs and agents (Cursor, Windsurf, Claude Code, Codex, Copilot, Gemini CLI, OpenCode, OpenClaw, Hermes Agent, Oh-my-Pi, Kiro, Antigravity, Trae) via the same local data directory.",
26957
+ description: "Store a new observation/memory. Automatically indexed for search. Use type to classify: gotcha ([GOTCHA] critical pitfall), decision ([DECISION] architecture choice), problem-solution ([FIX] bug fix), how-it-works ([INFO] explanation), what-changed ([CHANGE] change), discovery ([DISCOVERY] insight), why-it-exists ([WHY] rationale), trade-off ([TRADEOFF] compromise), session-request ([SESSION] original goal). Project visibility is the default. Personal or team visibility requires an explicitly joined coordination identity.",
26197
26958
  inputSchema: {
26198
26959
  entityName: z2.string().describe('The entity this observation belongs to (e.g., "auth-module", "port-config")'),
26199
26960
  type: z2.enum(OBSERVATION_TYPES).describe("Observation type for classification"),
@@ -26211,361 +26972,214 @@ The path should point to a directory containing a .git folder.`
26211
26972
  completion: z2.number().optional().describe("Completion percentage 0-100")
26212
26973
  }).optional().describe("Progress tracking for task/feature observations"),
26213
26974
  relatedCommits: z2.array(z2.string()).optional().describe("Git commit hashes this memory relates to (links ground truth \u2194 reasoning)"),
26214
- relatedEntities: z2.array(z2.string()).optional().describe("Other entity names this memory cross-references")
26975
+ relatedEntities: z2.array(z2.string()).optional().describe("Other entity names this memory cross-references"),
26976
+ visibility: z2.enum(["personal", "project", "team"]).optional().describe(
26977
+ "Retrieval scope. Project is the normal shared default; personal/team require memorix_session_start with joinTeam=true."
26978
+ )
26215
26979
  }
26216
26980
  },
26217
- async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities }) => {
26981
+ async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility }) => {
26218
26982
  const unresolved = requireResolvedProject("store memory in the current project");
26219
26983
  if (unresolved) return unresolved;
26220
- return withFreshIndex(async () => {
26221
- let entityName = rawEntityName;
26222
- let type = rawType;
26223
- let title = rawTitle;
26224
- let safeFacts = facts ? coerceStringArray(facts) : void 0;
26225
- const safeFiles = filesModified ? coerceStringArray(filesModified) : void 0;
26226
- const safeConcepts = concepts ? coerceStringArray(concepts) : void 0;
26227
- let formationMode = "active";
26228
- if (process.env.MEMORIX_FORMATION_MODE) {
26229
- formationMode = process.env.MEMORIX_FORMATION_MODE;
26230
- } else {
26231
- try {
26232
- const { getBehaviorConfig: getBehaviorConfig2 } = await Promise.resolve().then(() => (init_behavior(), behavior_exports));
26233
- formationMode = getBehaviorConfig2({ projectRoot: project.rootPath }).formationMode;
26234
- } catch {
26235
- }
26236
- }
26237
- const useFormation = formationMode === "active";
26238
- let formationResult = null;
26239
- let formationNote = "";
26240
- if (useFormation && !topicKey && !progress) {
26241
- let currentFormationStage = "setup";
26242
- const completedFormationStages = {};
26243
- const formationStartTime = Date.now();
26244
- const onFormationStageEvent = (event) => {
26245
- if (event.status === "start") {
26246
- currentFormationStage = event.stage;
26247
- return;
26248
- }
26249
- currentFormationStage = event.stage;
26250
- if (event.stageDurationMs !== void 0) {
26251
- completedFormationStages[event.stage] = event.stageDurationMs;
26252
- }
26253
- };
26254
- try {
26255
- const formationConfig = {
26256
- mode: "active",
26257
- useLLM: isLLMEnabled(),
26258
- minValueScore: 0.3,
26259
- searchMemories: async (q, limit, pid) => {
26260
- const result = await compactSearch({ query: q, limit, projectId: pid, status: "active" });
26261
- if (result.entries.length === 0) return [];
26262
- const details = await compactDetail(result.entries.map((e) => e.id));
26263
- return details.documents.map((d, i) => ({
26264
- id: Number(d.id.replace("obs-", "")),
26265
- observationId: d.observationId,
26266
- title: d.title,
26267
- narrative: d.narrative,
26268
- facts: d.facts,
26269
- entityName: d.entityName,
26270
- type: d.type,
26271
- score: result.entries[i]?.score ?? 0
26272
- }));
26273
- },
26274
- getObservation: (id) => {
26275
- const o = getObservation(id);
26276
- if (!o) return null;
26277
- return {
26278
- id: o.id,
26279
- entityName: o.entityName,
26280
- type: o.type,
26281
- title: o.title,
26282
- narrative: o.narrative,
26283
- facts: o.facts,
26284
- topicKey: o.topicKey
26285
- };
26286
- },
26287
- getEntityNames: () => graphManager.getEntityNames(),
26288
- onStageEvent: onFormationStageEvent
26289
- };
26290
- formationResult = await withTimeout2(
26291
- runFormation({
26292
- entityName,
26293
- type,
26294
- title,
26295
- narrative,
26296
- facts: safeFacts,
26297
- projectId: project.id,
26298
- source: "explicit"
26299
- }, formationConfig),
26300
- FORMATION_TIMEOUT_MS,
26301
- "Formation pipeline"
26302
- );
26303
- const modeIcon = "[FAST]";
26304
- formationNote = `
26305
- ${modeIcon} Formation[active]: ${formationResult.evaluation.category} (${formationResult.evaluation.score.toFixed(2)}) | ${formationResult.resolution.action} | ${formationResult.pipeline.durationMs}ms`;
26306
- if (formationResult.extraction.extractedFacts.length > 0) {
26307
- formationNote += ` | +${formationResult.extraction.extractedFacts.length} facts`;
26308
- }
26309
- if (formationResult.extraction.titleImproved) formationNote += " | title\u2191";
26310
- if (formationResult.extraction.entityResolved) formationNote += ` | entity\u2192${formationResult.entityName}`;
26311
- if (formationResult.extraction.typeCorrected) formationNote += ` | type\u2192${formationResult.type}`;
26312
- } catch (formationErr) {
26313
- const isTimeout = formationErr instanceof Error && formationErr.message.includes("timed out");
26314
- const elapsedMs = Date.now() - formationStartTime;
26315
- const stageSummary = formatFormationStageDurations(completedFormationStages);
26316
- console.error(
26317
- `[memorix] Formation ${isTimeout ? "timed out" : "failed"} in memorix_store after ${elapsedMs}ms/${FORMATION_TIMEOUT_MS}ms at stage ${currentFormationStage}${stageSummary ? ` | completed: ${stageSummary}` : ""}`
26318
- );
26319
- if (!isTimeout && formationErr instanceof Error) {
26320
- console.error(`[memorix] Formation error: ${formationErr.message}`);
26984
+ const requestedVisibility = visibility ?? "project";
26985
+ const reader = getObservationReader();
26986
+ if (requestedVisibility !== "project" && !currentAgentId) {
26987
+ return {
26988
+ content: [{ type: "text", text: "Personal or team memory requires memorix_session_start with joinTeam=true so Memorix can bind an owner." }],
26989
+ isError: true
26990
+ };
26991
+ }
26992
+ if (requestedVisibility === "team" && !reader.isTeamMember) {
26993
+ return {
26994
+ content: [{ type: "text", text: "Team memory requires an active coordination membership in the current project." }],
26995
+ isError: true
26996
+ };
26997
+ }
26998
+ try {
26999
+ return await withFreshIndex(async () => {
27000
+ let entityName = rawEntityName;
27001
+ let type = rawType;
27002
+ let title = rawTitle;
27003
+ let safeFacts = facts ? coerceStringArray(facts) : void 0;
27004
+ const safeFiles = filesModified ? coerceStringArray(filesModified) : void 0;
27005
+ const safeConcepts = concepts ? coerceStringArray(concepts) : void 0;
27006
+ let formationMode = "active";
27007
+ if (process.env.MEMORIX_FORMATION_MODE) {
27008
+ formationMode = process.env.MEMORIX_FORMATION_MODE;
27009
+ } else {
27010
+ try {
27011
+ const { getBehaviorConfig: getBehaviorConfig2 } = await Promise.resolve().then(() => (init_behavior(), behavior_exports));
27012
+ formationMode = getBehaviorConfig2({ projectRoot: project.rootPath }).formationMode;
27013
+ } catch {
26321
27014
  }
26322
- formationNote = `
26323
- [WARN] Formation ${isTimeout ? "timed out" : "failed"} \u2014 storing base observation without enrichment`;
26324
27015
  }
26325
- }
26326
- if (useFormation && formationResult && formationResult.resolution.action !== "new") {
26327
- const { action: action2, targetId, reason } = formationResult.resolution;
26328
- if (action2 === "merge" && targetId) {
26329
- const targetObs = getObservation(targetId);
26330
- if (targetObs) {
26331
- await storeObservation({
26332
- entityName: targetObs.entityName,
26333
- type: targetObs.type,
26334
- title: formationResult.title,
26335
- narrative: formationResult.narrative,
26336
- facts: formationResult.facts,
26337
- filesModified: safeFiles,
26338
- concepts: safeConcepts,
26339
- projectId: project.id,
26340
- topicKey: targetObs.topicKey,
26341
- progress,
26342
- sourceDetail: "explicit",
26343
- createdByAgentId: currentAgentId
26344
- });
26345
- return {
26346
- content: [{
26347
- type: "text",
26348
- text: `[UPDATED] Formation MERGE: merged into #${targetId} (${reason})${formationNote}`
26349
- }]
26350
- };
26351
- }
26352
- } else if (action2 === "evolve" && targetId) {
26353
- const targetObs = getObservation(targetId);
26354
- if (targetObs) {
26355
- await storeObservation({
26356
- entityName: targetObs.entityName,
26357
- type: targetObs.type,
26358
- title: formationResult.title,
26359
- narrative: formationResult.narrative,
26360
- facts: formationResult.facts,
26361
- filesModified: safeFiles,
26362
- concepts: safeConcepts,
26363
- projectId: project.id,
26364
- topicKey: targetObs.topicKey,
26365
- progress,
26366
- sourceDetail: "explicit",
26367
- createdByAgentId: currentAgentId
26368
- });
26369
- return {
26370
- content: [{
26371
- type: "text",
26372
- text: `[UPDATED] Formation EVOLVE: evolved #${targetId} (${reason})${formationNote}`
26373
- }]
26374
- };
26375
- }
26376
- } else if (action2 === "discard") {
26377
- return {
26378
- content: [{
26379
- type: "text",
26380
- text: `[SKIP] Formation DISCARD: ${reason}${formationNote}`
26381
- }]
27016
+ const useFormation = formationMode === "active";
27017
+ let formationResult = null;
27018
+ let formationNote = "";
27019
+ if (useFormation && !topicKey && !progress) {
27020
+ let currentFormationStage = "setup";
27021
+ const completedFormationStages = {};
27022
+ const formationStartTime = Date.now();
27023
+ const onFormationStageEvent = (event) => {
27024
+ if (event.status === "start") {
27025
+ currentFormationStage = event.stage;
27026
+ return;
27027
+ }
27028
+ currentFormationStage = event.stage;
27029
+ if (event.stageDurationMs !== void 0) {
27030
+ completedFormationStages[event.stage] = event.stageDurationMs;
27031
+ }
26382
27032
  };
26383
- }
26384
- }
26385
- let compactAction = "";
26386
- let compactMerged = false;
26387
- if (!useFormation && !topicKey && !progress) {
26388
- try {
26389
- const searchResult = await compactSearch({
26390
- query: `${title} ${narrative.substring(0, 200)}`,
26391
- limit: 5,
26392
- projectId: project.id,
26393
- status: "active"
26394
- });
26395
- const similarEntries = searchResult.entries.map((e) => e);
26396
- if (similarEntries.length > 0) {
26397
- const similarIds = similarEntries.map((e) => e.id);
26398
- const details = await compactDetail(similarIds);
26399
- const existingMemories = details.documents.map((d, i) => ({
26400
- id: d.observationId,
26401
- title: d.title,
26402
- narrative: d.narrative,
26403
- facts: d.facts,
26404
- score: similarEntries[i]?.score ?? 0
26405
- }));
26406
- const decision = await withTimeout2(
26407
- compactOnWrite(
26408
- { title, narrative, facts: safeFacts ?? [] },
26409
- existingMemories
26410
- ),
26411
- COMPACT_ON_WRITE_TIMEOUT_MS,
26412
- "Compact-on-write"
26413
- );
26414
- if (decision.action === "UPDATE" && decision.targetId) {
26415
- const targetObs = getObservation(decision.targetId);
26416
- if (targetObs) {
26417
- await storeObservation({
26418
- entityName: targetObs.entityName,
26419
- type: targetObs.type,
26420
- title: decision.mergedNarrative ? title : targetObs.title,
26421
- narrative: decision.mergedNarrative ?? narrative,
26422
- facts: decision.mergedFacts ?? safeFacts,
26423
- filesModified: safeFiles,
26424
- concepts: safeConcepts,
26425
- projectId: project.id,
26426
- topicKey: targetObs.topicKey,
26427
- progress,
26428
- sourceDetail: "explicit",
26429
- createdByAgentId: currentAgentId
26430
- });
26431
- compactAction = `[UPDATED] Compact UPDATE: merged into #${decision.targetId} (${decision.reason})`;
26432
- compactMerged = true;
27033
+ try {
27034
+ const formationConfig = {
27035
+ mode: "active",
27036
+ useLLM: isLLMEnabled(),
27037
+ minValueScore: 0.3,
27038
+ searchMemories: async (q, limit, pid) => {
27039
+ const result = await compactSearch({ query: q, limit, projectId: pid, status: "active", reader });
27040
+ if (result.entries.length === 0) return [];
27041
+ const details = await compactDetail(result.entries.map((e) => e.id), { reader });
27042
+ return details.documents.map((d, i) => ({
27043
+ id: Number(d.id.replace("obs-", "")),
27044
+ observationId: d.observationId,
27045
+ title: d.title,
27046
+ narrative: d.narrative,
27047
+ facts: d.facts,
27048
+ entityName: d.entityName,
27049
+ type: d.type,
27050
+ score: result.entries[i]?.score ?? 0
27051
+ }));
27052
+ },
27053
+ getObservation: (id) => {
27054
+ const o = getObservation(id);
27055
+ if (!o || o.projectId !== project.id || !canReadObservation(o, reader)) return null;
26433
27056
  return {
26434
- content: [{
26435
- type: "text",
26436
- text: `${compactAction}
26437
- Mode: ${decision.usedLLM ? "LLM" : "heuristic"}`
26438
- }]
27057
+ id: o.id,
27058
+ entityName: o.entityName,
27059
+ type: o.type,
27060
+ title: o.title,
27061
+ narrative: o.narrative,
27062
+ facts: o.facts,
27063
+ topicKey: o.topicKey
26439
27064
  };
26440
- }
26441
- } else if (decision.action === "NONE") {
27065
+ },
27066
+ getEntityNames: () => graphManager.getEntityNames(),
27067
+ onStageEvent: onFormationStageEvent
27068
+ };
27069
+ formationResult = await withTimeout2(
27070
+ runFormation({
27071
+ entityName,
27072
+ type,
27073
+ title,
27074
+ narrative,
27075
+ facts: safeFacts,
27076
+ projectId: project.id,
27077
+ source: "explicit"
27078
+ }, formationConfig),
27079
+ FORMATION_TIMEOUT_MS,
27080
+ "Formation pipeline"
27081
+ );
27082
+ const modeIcon = "[FAST]";
27083
+ formationNote = `
27084
+ ${modeIcon} Formation[active]: ${formationResult.evaluation.category} (${formationResult.evaluation.score.toFixed(2)}) | ${formationResult.resolution.action} | ${formationResult.pipeline.durationMs}ms`;
27085
+ if (formationResult.extraction.extractedFacts.length > 0) {
27086
+ formationNote += ` | +${formationResult.extraction.extractedFacts.length} facts`;
27087
+ }
27088
+ if (formationResult.extraction.titleImproved) formationNote += " | title\u2191";
27089
+ if (formationResult.extraction.entityResolved) formationNote += ` | entity\u2192${formationResult.entityName}`;
27090
+ if (formationResult.extraction.typeCorrected) formationNote += ` | type\u2192${formationResult.type}`;
27091
+ } catch (formationErr) {
27092
+ const isTimeout = formationErr instanceof Error && formationErr.message.includes("timed out");
27093
+ const elapsedMs = Date.now() - formationStartTime;
27094
+ const stageSummary = formatFormationStageDurations(completedFormationStages);
27095
+ console.error(
27096
+ `[memorix] Formation ${isTimeout ? "timed out" : "failed"} in memorix_store after ${elapsedMs}ms/${FORMATION_TIMEOUT_MS}ms at stage ${currentFormationStage}${stageSummary ? ` | completed: ${stageSummary}` : ""}`
27097
+ );
27098
+ if (!isTimeout && formationErr instanceof Error) {
27099
+ console.error(`[memorix] Formation error: ${formationErr.message}`);
27100
+ }
27101
+ formationNote = `
27102
+ [WARN] Formation ${isTimeout ? "timed out" : "failed"} \u2014 storing base observation without enrichment`;
27103
+ }
27104
+ }
27105
+ if (useFormation && formationResult && formationResult.resolution.action !== "new") {
27106
+ const { action: action2, targetId, reason } = formationResult.resolution;
27107
+ if (action2 === "merge" && targetId) {
27108
+ const targetObs = getObservation(targetId);
27109
+ if (targetObs && targetObs.projectId === project.id && canReadObservation(targetObs, reader)) {
27110
+ await storeObservation({
27111
+ entityName: targetObs.entityName,
27112
+ type: targetObs.type,
27113
+ title: formationResult.title,
27114
+ narrative: formationResult.narrative,
27115
+ facts: formationResult.facts,
27116
+ filesModified: safeFiles,
27117
+ concepts: safeConcepts,
27118
+ projectId: project.id,
27119
+ topicKey: targetObs.topicKey,
27120
+ progress,
27121
+ sourceDetail: "explicit",
27122
+ createdByAgentId: currentAgentId,
27123
+ visibility,
27124
+ visibilityReader: reader
27125
+ });
26442
27126
  return {
26443
27127
  content: [{
26444
27128
  type: "text",
26445
- text: `[SKIP] Compact SKIP: ${decision.reason}
26446
- Existing memory #${decision.targetId} already covers this.
26447
- Mode: ${decision.usedLLM ? "LLM" : "heuristic"}`
27129
+ text: `[UPDATED] Formation MERGE: merged into #${targetId} (${reason})${formationNote}`
26448
27130
  }]
26449
27131
  };
26450
- } else if (decision.action === "DELETE" && decision.targetId) {
26451
- const { resolveObservations: resolveObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
26452
- await resolveObservations2([decision.targetId], "resolved");
26453
- compactAction = ` | Compact: resolved outdated #${decision.targetId}`;
26454
27132
  }
26455
- if (decision.enrichedFacts && decision.enrichedFacts.length > 0) {
26456
- const currentFacts = safeFacts ?? [];
26457
- const newFacts = decision.enrichedFacts.filter((f) => !currentFacts.includes(f));
26458
- if (newFacts.length > 0) {
26459
- compactAction += ` | +${newFacts.length} LLM-extracted facts`;
26460
- }
27133
+ } else if (action2 === "evolve" && targetId) {
27134
+ const targetObs = getObservation(targetId);
27135
+ if (targetObs && targetObs.projectId === project.id && canReadObservation(targetObs, reader)) {
27136
+ await storeObservation({
27137
+ entityName: targetObs.entityName,
27138
+ type: targetObs.type,
27139
+ title: formationResult.title,
27140
+ narrative: formationResult.narrative,
27141
+ facts: formationResult.facts,
27142
+ filesModified: safeFiles,
27143
+ concepts: safeConcepts,
27144
+ projectId: project.id,
27145
+ topicKey: targetObs.topicKey,
27146
+ progress,
27147
+ sourceDetail: "explicit",
27148
+ createdByAgentId: currentAgentId,
27149
+ visibility,
27150
+ visibilityReader: reader
27151
+ });
27152
+ return {
27153
+ content: [{
27154
+ type: "text",
27155
+ text: `[UPDATED] Formation EVOLVE: evolved #${targetId} (${reason})${formationNote}`
27156
+ }]
27157
+ };
26461
27158
  }
26462
- }
26463
- } catch {
26464
- }
26465
- }
26466
- if (formationResult && formationResult.resolution.action === "new") {
26467
- const llmFacts = formationResult.extraction.extractedFacts;
26468
- if (llmFacts.length > 0) {
26469
- const currentFacts = safeFacts ?? [];
26470
- const currentLower = new Set(currentFacts.map((f) => f.toLowerCase().trim()));
26471
- const newFacts = llmFacts.filter((f) => !currentLower.has(f.toLowerCase().trim()));
26472
- if (newFacts.length > 0) {
26473
- safeFacts = [...currentFacts, ...newFacts];
27159
+ } else if (action2 === "discard") {
27160
+ return {
27161
+ content: [{
27162
+ type: "text",
27163
+ text: `[SKIP] Formation DISCARD: ${reason}${formationNote}`
27164
+ }]
27165
+ };
26474
27166
  }
26475
27167
  }
26476
- if (formationResult.extraction.titleImproved && formationResult.title) {
26477
- title = formationResult.title;
26478
- }
26479
- if (formationResult.extraction.typeCorrected && formationResult.type) {
26480
- type = formationResult.type;
26481
- }
26482
- if (formationResult.extraction.entityResolved && formationResult.entityName) {
26483
- entityName = formationResult.entityName;
26484
- }
26485
- }
26486
- await graphManager.createEntities([
26487
- { name: entityName, entityType: "auto", observations: [] }
26488
- ]);
26489
- let sessionId;
26490
- try {
26491
- const { getActiveSession: getActiveSession2 } = await Promise.resolve().then(() => (init_session(), session_exports));
26492
- const active = await getActiveSession2(projectDir2, project.id);
26493
- if (active) sessionId = active.id;
26494
- } catch {
26495
- }
26496
- let finalNarrative = narrative;
26497
- let compressionNote = "";
26498
- try {
26499
- const { compressNarrative: compressNarrative2 } = await Promise.resolve().then(() => (init_quality(), quality_exports));
26500
- const { compressed, saved, usedLLM } = await withTimeout2(
26501
- compressNarrative2(narrative, safeFacts, type),
26502
- COMPRESSION_TIMEOUT_MS,
26503
- "Narrative compression"
26504
- );
26505
- if (usedLLM && saved > 0) {
26506
- finalNarrative = compressed;
26507
- compressionNote = ` | compressed -${saved} tokens`;
26508
- }
26509
- } catch {
26510
- }
26511
- let attributionWarning = "";
26512
- try {
26513
- const attrCheck = await checkProjectAttribution(entityName, project.id, getAllObservations());
26514
- if (attrCheck.suspicious) {
26515
- attributionWarning = `
26516
- [WARN] Attribution notice: entity "${entityName}" has 0 observations in "${project.id}" but ${attrCheck.count} in "${attrCheck.knownIn}" (confidence: ${attrCheck.confidence}). Verify the correct project is bound before storing.`;
26517
- }
26518
- } catch {
26519
- }
26520
- const { observation: obs, upserted } = await storeObservation({
26521
- entityName,
26522
- type,
26523
- title,
26524
- narrative: finalNarrative,
26525
- facts: safeFacts,
26526
- filesModified: safeFiles,
26527
- concepts: safeConcepts,
26528
- projectId: project.id,
26529
- topicKey,
26530
- sessionId,
26531
- progress,
26532
- relatedCommits,
26533
- relatedEntities,
26534
- sourceDetail: "explicit",
26535
- valueCategory: formationResult?.evaluation.category,
26536
- createdByAgentId: currentAgentId
26537
- });
26538
- await graphManager.addObservations([
26539
- { entityName, contents: [`[#${obs.id}] ${title}`] }
26540
- ]);
26541
- const extracted = extractEntities([title, narrative, ...safeFacts ?? []].join(" "));
26542
- const autoRelCount = await createAutoRelations(obs, extracted, graphManager);
26543
- const enrichmentParts = [];
26544
- const autoFiles = obs.filesModified.filter((f) => !(safeFiles ?? []).includes(f));
26545
- const autoConcepts = obs.concepts.filter((c) => !(safeConcepts ?? []).includes(c));
26546
- if (autoFiles.length > 0) enrichmentParts.push(`+${autoFiles.length} files extracted`);
26547
- if (autoConcepts.length > 0) enrichmentParts.push(`+${autoConcepts.length} concepts enriched`);
26548
- if (autoRelCount > 0) enrichmentParts.push(`+${autoRelCount} relations auto-created`);
26549
- if (obs.hasCausalLanguage) enrichmentParts.push("causal language detected");
26550
- if (upserted) enrichmentParts.push(`topic upserted (rev ${obs.revisionCount ?? 1})`);
26551
- const enrichment = enrichmentParts.length > 0 ? `
26552
- Auto-enriched: ${enrichmentParts.join(", ")}` : "";
26553
- const action = upserted ? "[UPDATED] Updated" : "[OK] Stored";
26554
- if (!useFormation && !topicKey && !progress) {
26555
- const shadowFormation = async () => {
26556
- let oldCompactDecision = null;
27168
+ let compactAction = "";
27169
+ let compactMerged = false;
27170
+ if (!useFormation && !topicKey && !progress) {
26557
27171
  try {
26558
- const compactStart = Date.now();
26559
27172
  const searchResult = await compactSearch({
26560
27173
  query: `${title} ${narrative.substring(0, 200)}`,
26561
27174
  limit: 5,
26562
27175
  projectId: project.id,
26563
- status: "active"
27176
+ status: "active",
27177
+ reader
26564
27178
  });
26565
27179
  const similarEntries = searchResult.entries.map((e) => e);
26566
27180
  if (similarEntries.length > 0) {
26567
27181
  const similarIds = similarEntries.map((e) => e.id);
26568
- const details = await compactDetail(similarIds);
27182
+ const details = await compactDetail(similarIds, { reader });
26569
27183
  const existingMemories = details.documents.map((d, i) => ({
26570
27184
  id: d.observationId,
26571
27185
  title: d.title,
@@ -26573,78 +27187,266 @@ Auto-enriched: ${enrichmentParts.join(", ")}` : "";
26573
27187
  facts: d.facts,
26574
27188
  score: similarEntries[i]?.score ?? 0
26575
27189
  }));
26576
- const decision = await compactOnWrite(
26577
- { title, narrative, facts: safeFacts ?? [] },
26578
- existingMemories
27190
+ const decision = await withTimeout2(
27191
+ compactOnWrite(
27192
+ { title, narrative, facts: safeFacts ?? [] },
27193
+ existingMemories
27194
+ ),
27195
+ COMPACT_ON_WRITE_TIMEOUT_MS,
27196
+ "Compact-on-write"
26579
27197
  );
26580
- oldCompactDecision = {
26581
- action: decision.action,
26582
- targetId: decision.targetId,
26583
- reason: decision.reason,
26584
- durationMs: Date.now() - compactStart
26585
- };
27198
+ if (decision.action === "UPDATE" && decision.targetId) {
27199
+ const targetObs = getObservation(decision.targetId);
27200
+ if (targetObs && targetObs.projectId === project.id && canReadObservation(targetObs, reader)) {
27201
+ await storeObservation({
27202
+ entityName: targetObs.entityName,
27203
+ type: targetObs.type,
27204
+ title: decision.mergedNarrative ? title : targetObs.title,
27205
+ narrative: decision.mergedNarrative ?? narrative,
27206
+ facts: decision.mergedFacts ?? safeFacts,
27207
+ filesModified: safeFiles,
27208
+ concepts: safeConcepts,
27209
+ projectId: project.id,
27210
+ topicKey: targetObs.topicKey,
27211
+ progress,
27212
+ sourceDetail: "explicit",
27213
+ createdByAgentId: currentAgentId,
27214
+ visibility,
27215
+ visibilityReader: reader
27216
+ });
27217
+ compactAction = `[UPDATED] Compact UPDATE: merged into #${decision.targetId} (${decision.reason})`;
27218
+ compactMerged = true;
27219
+ return {
27220
+ content: [{
27221
+ type: "text",
27222
+ text: `${compactAction}
27223
+ Mode: ${decision.usedLLM ? "LLM" : "heuristic"}`
27224
+ }]
27225
+ };
27226
+ }
27227
+ } else if (decision.action === "NONE") {
27228
+ return {
27229
+ content: [{
27230
+ type: "text",
27231
+ text: `[SKIP] Compact SKIP: ${decision.reason}
27232
+ Existing memory #${decision.targetId} already covers this.
27233
+ Mode: ${decision.usedLLM ? "LLM" : "heuristic"}`
27234
+ }]
27235
+ };
27236
+ } else if (decision.action === "DELETE" && decision.targetId) {
27237
+ const { resolveObservations: resolveObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
27238
+ await resolveObservations2([decision.targetId], "resolved");
27239
+ compactAction = ` | Compact: resolved outdated #${decision.targetId}`;
27240
+ }
27241
+ if (decision.enrichedFacts && decision.enrichedFacts.length > 0) {
27242
+ const currentFacts = safeFacts ?? [];
27243
+ const newFacts = decision.enrichedFacts.filter((f) => !currentFacts.includes(f));
27244
+ if (newFacts.length > 0) {
27245
+ compactAction += ` | +${newFacts.length} LLM-extracted facts`;
27246
+ }
27247
+ }
26586
27248
  }
26587
27249
  } catch {
26588
27250
  }
26589
- const formationConfig = {
26590
- mode: formationMode,
26591
- useLLM: isLLMEnabled(),
26592
- minValueScore: 0.3,
26593
- searchMemories: async (q, limit, pid) => {
26594
- const result = await compactSearch({ query: q, limit, projectId: pid, status: "active" });
26595
- if (result.entries.length === 0) return [];
26596
- const details = await compactDetail(result.entries.map((e) => e.id));
26597
- return details.documents.map((d, i) => ({
26598
- id: Number(d.id.replace("obs-", "")),
26599
- observationId: d.observationId,
26600
- title: d.title,
26601
- narrative: d.narrative,
26602
- facts: d.facts,
26603
- entityName: d.entityName,
26604
- type: d.type,
26605
- score: result.entries[i]?.score ?? 0
26606
- }));
26607
- },
26608
- getObservation: (id) => {
26609
- const o = getObservation(id);
26610
- if (!o) return null;
26611
- return { id: o.id, entityName: o.entityName, type: o.type, title: o.title, narrative: o.narrative, facts: o.facts, topicKey: o.topicKey };
26612
- },
26613
- getEntityNames: () => graphManager.getEntityNames()
26614
- };
26615
- const formed = await withTimeout2(
26616
- runFormation({ entityName, type, title, narrative, facts: safeFacts, projectId: project.id, source: "explicit", topicKey }, formationConfig),
26617
- FORMATION_TIMEOUT_MS,
26618
- "Shadow formation"
27251
+ }
27252
+ if (formationResult && formationResult.resolution.action === "new") {
27253
+ const llmFacts = formationResult.extraction.extractedFacts;
27254
+ if (llmFacts.length > 0) {
27255
+ const currentFacts = safeFacts ?? [];
27256
+ const currentLower = new Set(currentFacts.map((f) => f.toLowerCase().trim()));
27257
+ const newFacts = llmFacts.filter((f) => !currentLower.has(f.toLowerCase().trim()));
27258
+ if (newFacts.length > 0) {
27259
+ safeFacts = [...currentFacts, ...newFacts];
27260
+ }
27261
+ }
27262
+ if (formationResult.extraction.titleImproved && formationResult.title) {
27263
+ title = formationResult.title;
27264
+ }
27265
+ if (formationResult.extraction.typeCorrected && formationResult.type) {
27266
+ type = formationResult.type;
27267
+ }
27268
+ if (formationResult.extraction.entityResolved && formationResult.entityName) {
27269
+ entityName = formationResult.entityName;
27270
+ }
27271
+ }
27272
+ await graphManager.createEntities([
27273
+ { name: entityName, entityType: "auto", observations: [] }
27274
+ ]);
27275
+ let sessionId;
27276
+ try {
27277
+ const { getActiveSession: getActiveSession2 } = await Promise.resolve().then(() => (init_session(), session_exports));
27278
+ const active = await getActiveSession2(projectDir2, project.id);
27279
+ if (active) sessionId = active.id;
27280
+ } catch {
27281
+ }
27282
+ let finalNarrative = narrative;
27283
+ let compressionNote = "";
27284
+ try {
27285
+ const { compressNarrative: compressNarrative2 } = await Promise.resolve().then(() => (init_quality(), quality_exports));
27286
+ const { compressed, saved, usedLLM } = await withTimeout2(
27287
+ compressNarrative2(narrative, safeFacts, type),
27288
+ COMPRESSION_TIMEOUT_MS,
27289
+ "Narrative compression"
26619
27290
  );
26620
- const { recordBeforeAfterMetrics: recordBeforeAfterMetrics2 } = await Promise.resolve().then(() => (init_formation(), formation_exports));
26621
- if (oldCompactDecision) {
26622
- recordBeforeAfterMetrics2({
26623
- formationAction: formed.resolution.action,
26624
- formationTargetId: formed.resolution.targetId,
26625
- oldCompactAction: oldCompactDecision.action,
26626
- oldCompactTargetId: oldCompactDecision.targetId,
26627
- oldCompactReason: oldCompactDecision.reason,
26628
- formationValueScore: formed.evaluation.score,
26629
- formationValueCategory: formed.evaluation.category,
26630
- formationDurationMs: formed.pipeline.durationMs,
26631
- compactDurationMs: oldCompactDecision.durationMs
26632
- });
27291
+ if (usedLLM && saved > 0) {
27292
+ finalNarrative = compressed;
27293
+ compressionNote = ` | compressed -${saved} tokens`;
26633
27294
  }
26634
- };
26635
- shadowFormation().catch(() => {
27295
+ } catch {
27296
+ }
27297
+ let attributionWarning = "";
27298
+ try {
27299
+ const attrCheck = await checkProjectAttribution(
27300
+ entityName,
27301
+ project.id,
27302
+ filterReadableObservations(getAllObservations(), reader)
27303
+ );
27304
+ if (attrCheck.suspicious) {
27305
+ attributionWarning = `
27306
+ [WARN] Attribution notice: entity "${entityName}" has 0 observations in "${project.id}" but ${attrCheck.count} in "${attrCheck.knownIn}" (confidence: ${attrCheck.confidence}). Verify the correct project is bound before storing.`;
27307
+ }
27308
+ } catch {
27309
+ }
27310
+ const { observation: obs, upserted } = await storeObservation({
27311
+ entityName,
27312
+ type,
27313
+ title,
27314
+ narrative: finalNarrative,
27315
+ facts: safeFacts,
27316
+ filesModified: safeFiles,
27317
+ concepts: safeConcepts,
27318
+ projectId: project.id,
27319
+ topicKey,
27320
+ sessionId,
27321
+ progress,
27322
+ relatedCommits,
27323
+ relatedEntities,
27324
+ sourceDetail: "explicit",
27325
+ valueCategory: formationResult?.evaluation.category,
27326
+ createdByAgentId: currentAgentId,
27327
+ visibility,
27328
+ visibilityReader: reader
26636
27329
  });
26637
- }
26638
- return {
26639
- content: [
26640
- {
26641
- type: "text",
26642
- text: `${action} observation #${obs.id} "${title}" (~${obs.tokens} tokens)
27330
+ await graphManager.addObservations([
27331
+ { entityName, contents: [`[#${obs.id}] ${title}`] }
27332
+ ]);
27333
+ const extracted = extractEntities([title, narrative, ...safeFacts ?? []].join(" "));
27334
+ const autoRelCount = await createAutoRelations(obs, extracted, graphManager);
27335
+ const enrichmentParts = [];
27336
+ const autoFiles = obs.filesModified.filter((f) => !(safeFiles ?? []).includes(f));
27337
+ const autoConcepts = obs.concepts.filter((c) => !(safeConcepts ?? []).includes(c));
27338
+ if (autoFiles.length > 0) enrichmentParts.push(`+${autoFiles.length} files extracted`);
27339
+ if (autoConcepts.length > 0) enrichmentParts.push(`+${autoConcepts.length} concepts enriched`);
27340
+ if (autoRelCount > 0) enrichmentParts.push(`+${autoRelCount} relations auto-created`);
27341
+ if (obs.hasCausalLanguage) enrichmentParts.push("causal language detected");
27342
+ if (upserted) enrichmentParts.push(`topic upserted (rev ${obs.revisionCount ?? 1})`);
27343
+ const enrichment = enrichmentParts.length > 0 ? `
27344
+ Auto-enriched: ${enrichmentParts.join(", ")}` : "";
27345
+ const action = upserted ? "[UPDATED] Updated" : "[OK] Stored";
27346
+ if (!useFormation && !topicKey && !progress) {
27347
+ const shadowFormation = async () => {
27348
+ let oldCompactDecision = null;
27349
+ try {
27350
+ const compactStart = Date.now();
27351
+ const searchResult = await compactSearch({
27352
+ query: `${title} ${narrative.substring(0, 200)}`,
27353
+ limit: 5,
27354
+ projectId: project.id,
27355
+ status: "active",
27356
+ reader
27357
+ });
27358
+ const similarEntries = searchResult.entries.map((e) => e);
27359
+ if (similarEntries.length > 0) {
27360
+ const similarIds = similarEntries.map((e) => e.id);
27361
+ const details = await compactDetail(similarIds, { reader });
27362
+ const existingMemories = details.documents.map((d, i) => ({
27363
+ id: d.observationId,
27364
+ title: d.title,
27365
+ narrative: d.narrative,
27366
+ facts: d.facts,
27367
+ score: similarEntries[i]?.score ?? 0
27368
+ }));
27369
+ const decision = await compactOnWrite(
27370
+ { title, narrative, facts: safeFacts ?? [] },
27371
+ existingMemories
27372
+ );
27373
+ oldCompactDecision = {
27374
+ action: decision.action,
27375
+ targetId: decision.targetId,
27376
+ reason: decision.reason,
27377
+ durationMs: Date.now() - compactStart
27378
+ };
27379
+ }
27380
+ } catch {
27381
+ }
27382
+ const formationConfig = {
27383
+ mode: formationMode,
27384
+ useLLM: isLLMEnabled(),
27385
+ minValueScore: 0.3,
27386
+ searchMemories: async (q, limit, pid) => {
27387
+ const result = await compactSearch({ query: q, limit, projectId: pid, status: "active", reader });
27388
+ if (result.entries.length === 0) return [];
27389
+ const details = await compactDetail(result.entries.map((e) => e.id), { reader });
27390
+ return details.documents.map((d, i) => ({
27391
+ id: Number(d.id.replace("obs-", "")),
27392
+ observationId: d.observationId,
27393
+ title: d.title,
27394
+ narrative: d.narrative,
27395
+ facts: d.facts,
27396
+ entityName: d.entityName,
27397
+ type: d.type,
27398
+ score: result.entries[i]?.score ?? 0
27399
+ }));
27400
+ },
27401
+ getObservation: (id) => {
27402
+ const o = getObservation(id);
27403
+ if (!o || o.projectId !== project.id || !canReadObservation(o, reader)) return null;
27404
+ return { id: o.id, entityName: o.entityName, type: o.type, title: o.title, narrative: o.narrative, facts: o.facts, topicKey: o.topicKey };
27405
+ },
27406
+ getEntityNames: () => graphManager.getEntityNames()
27407
+ };
27408
+ const formed = await withTimeout2(
27409
+ runFormation({ entityName, type, title, narrative, facts: safeFacts, projectId: project.id, source: "explicit", topicKey }, formationConfig),
27410
+ FORMATION_TIMEOUT_MS,
27411
+ "Shadow formation"
27412
+ );
27413
+ const { recordBeforeAfterMetrics: recordBeforeAfterMetrics2 } = await Promise.resolve().then(() => (init_formation(), formation_exports));
27414
+ if (oldCompactDecision) {
27415
+ recordBeforeAfterMetrics2({
27416
+ formationAction: formed.resolution.action,
27417
+ formationTargetId: formed.resolution.targetId,
27418
+ oldCompactAction: oldCompactDecision.action,
27419
+ oldCompactTargetId: oldCompactDecision.targetId,
27420
+ oldCompactReason: oldCompactDecision.reason,
27421
+ formationValueScore: formed.evaluation.score,
27422
+ formationValueCategory: formed.evaluation.category,
27423
+ formationDurationMs: formed.pipeline.durationMs,
27424
+ compactDurationMs: oldCompactDecision.durationMs
27425
+ });
27426
+ }
27427
+ };
27428
+ shadowFormation().catch(() => {
27429
+ });
27430
+ }
27431
+ return {
27432
+ content: [
27433
+ {
27434
+ type: "text",
27435
+ text: `${action} observation #${obs.id} "${title}" (~${obs.tokens} tokens)
26643
27436
  Entity: ${entityName} | Type: ${type} | Project: ${project.id}${obs.topicKey ? ` | Topic: ${obs.topicKey}` : ""}${compactAction}${compressionNote}${enrichment}${formationNote}${attributionWarning}`
26644
- }
26645
- ]
27437
+ }
27438
+ ]
27439
+ };
27440
+ });
27441
+ } catch (error) {
27442
+ return {
27443
+ content: [{
27444
+ type: "text",
27445
+ text: error instanceof Error ? error.message : "Failed to store memory."
27446
+ }],
27447
+ isError: true
26646
27448
  };
26647
- });
27449
+ }
26648
27450
  }
26649
27451
  );
26650
27452
  server.registerTool(
@@ -26716,7 +27518,8 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
26716
27518
  // Use scope: 'global' to explicitly search all projects.
26717
27519
  projectId: scope === "global" ? void 0 : project.id,
26718
27520
  status: status ?? "active",
26719
- source
27521
+ source,
27522
+ reader: getObservationReader(scope === "global" ? "global" : "project")
26720
27523
  });
26721
27524
  const timeoutPromise = new Promise(
26722
27525
  (_, reject) => setTimeout(() => reject(new Error(`Search timeout after ${TIMEOUT_MS}ms`)), TIMEOUT_MS)
@@ -26774,7 +27577,10 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
26774
27577
  const unresolved = requireResolvedProject("build graph context for the current project");
26775
27578
  if (unresolved) return unresolved;
26776
27579
  const { getObservationStore: getObservationStore2 } = await Promise.resolve().then(() => (init_obs_store(), obs_store_exports));
26777
- const observations2 = await getObservationStore2().loadByProject(project.id);
27580
+ const observations2 = filterReadableObservations(
27581
+ await getObservationStore2().loadByProject(project.id),
27582
+ getObservationReader()
27583
+ );
26778
27584
  const packet = buildGraphContextPacket(observations2, {
26779
27585
  projectId: project.id,
26780
27586
  query,
@@ -26826,7 +27632,10 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
26826
27632
  Promise.resolve().then(() => (init_maintenance_jobs(), maintenance_jobs_exports)),
26827
27633
  Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports))
26828
27634
  ]);
26829
- const observations2 = await getObservationStore2().loadByProject(project.id, { status: "active" });
27635
+ const observations2 = filterReadableObservations(
27636
+ await getObservationStore2().loadByProject(project.id, { status: "active" }),
27637
+ getObservationReader()
27638
+ );
26830
27639
  const context = await buildAutoProjectContext2({
26831
27640
  project,
26832
27641
  dataDir: projectDir2,
@@ -26901,7 +27710,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
26901
27710
  { getResolvedConfig: getResolvedConfig2 },
26902
27711
  { getExternalCodeGraphContext: getExternalCodeGraphContext2 },
26903
27712
  { getObservationStore: getObservationStore2 },
26904
- { collectCurrentProjectFacts: collectCurrentProjectFacts2 },
27713
+ { collectCurrentProjectFacts: collectCurrentProjectFacts2, formatGitFact: formatGitFact2 },
26905
27714
  { resolveTaskLens: resolveTaskLens2 }
26906
27715
  ] = await Promise.all([
26907
27716
  Promise.resolve().then(() => (init_store(), store_exports)),
@@ -26916,7 +27725,10 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
26916
27725
  await store.init(projectDir2);
26917
27726
  const codegraphConfig = getResolvedConfig2({ projectRoot: project.rootPath }).codegraph;
26918
27727
  const exclude = codegraphConfig.excludePatterns;
26919
- const observations2 = await getObservationStore2().loadByProject(project.id, { status: "active" });
27728
+ const observations2 = filterReadableObservations(
27729
+ await getObservationStore2().loadByProject(project.id, { status: "active" }),
27730
+ getObservationReader()
27731
+ );
26920
27732
  observations2.reverse();
26921
27733
  const basePack = assembleContextPackForTask2({
26922
27734
  store,
@@ -26942,7 +27754,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
26942
27754
  if (currentFacts.latestChangelog) {
26943
27755
  worksetFacts.push("Latest changelog: " + currentFacts.latestChangelog.version + (currentFacts.latestChangelog.date ? " (" + currentFacts.latestChangelog.date + ")" : ""));
26944
27756
  }
26945
- worksetFacts.push("Git: " + (currentFacts.git.branch ? "branch " + currentFacts.git.branch + ", " : "") + (currentFacts.git.dirty ? "dirty worktree" : "clean worktree"));
27757
+ worksetFacts.push(formatGitFact2(currentFacts.git));
26946
27758
  const codeState = snapshot ? "- Code state: " + (snapshot.baseRevision ? snapshot.baseRevision.slice(0, 12) : "Git unavailable") + ", " + snapshot.worktreeState + " worktree, epoch " + snapshot.sourceEpoch : "- Code state: no completed snapshot yet";
26947
27759
  const pack = await attachTaskWorkset2({
26948
27760
  pack: basePack,
@@ -26974,11 +27786,13 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
26974
27786
  "memorix_knowledge",
26975
27787
  {
26976
27788
  title: "Knowledge Workspace",
26977
- description: "Manage the reviewable project Knowledge Workspace. Use it only for deliberate knowledge operations: initialize a local or versioned workspace, inspect status, compile source-backed proposals, lint, apply a reviewed proposal, or manage canonical workflows. It is intentionally absent from the default micro/lite profiles.",
27789
+ description: "Manage the reviewable project Knowledge Workspace. Use it only for deliberate knowledge operations: initialize a local or versioned workspace, review source-backed claims, compile proposals, lint, apply a reviewed proposal, or manage canonical workflows. It is intentionally absent from the default micro/lite profiles.",
26978
27790
  inputSchema: {
26979
27791
  action: z2.enum([
26980
27792
  "workspace_init",
26981
27793
  "status",
27794
+ "claim_list",
27795
+ "claim_review",
26982
27796
  "compile",
26983
27797
  "lint",
26984
27798
  "proposal_apply",
@@ -26993,6 +27807,9 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
26993
27807
  path: z2.string().optional().describe("Explicit versioned workspace path, used only by workspace_init"),
26994
27808
  proposalId: z2.string().optional().describe("Pending proposal id for proposal_apply"),
26995
27809
  allowManualOverwrite: z2.boolean().optional().default(false).describe("Explicitly allow proposal_apply to replace a manually edited page"),
27810
+ claimId: z2.string().optional().describe("Source-backed claim id for claim_review"),
27811
+ claimReviewState: z2.enum(["approved", "rejected"]).optional().describe("Deliberate review verdict for claim_review"),
27812
+ reviewDetail: z2.string().max(2e3).optional().describe("Evidence check performed before approving or rejecting a claim"),
26996
27813
  workflowId: z2.string().optional().describe("Canonical workflow id for workflow preview, apply, or run"),
26997
27814
  agent: z2.string().optional().describe("Target agent for a workflow adapter"),
26998
27815
  task: z2.string().optional().describe("Task text for workflow selection or a workflow run"),
@@ -27009,6 +27826,9 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27009
27826
  path: workspacePath,
27010
27827
  proposalId,
27011
27828
  allowManualOverwrite,
27829
+ claimId,
27830
+ claimReviewState,
27831
+ reviewDetail,
27012
27832
  workflowId,
27013
27833
  agent,
27014
27834
  task,
@@ -27031,6 +27851,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27031
27851
  };
27032
27852
  const [
27033
27853
  { ClaimStore: ClaimStore2 },
27854
+ { reviewClaim: reviewClaim2 },
27034
27855
  { CodeGraphStore: CodeGraphStore2 },
27035
27856
  { initializeKnowledgeWorkspace: initializeKnowledgeWorkspace2, loadKnowledgeWorkspace: loadKnowledgeWorkspace2 },
27036
27857
  { KnowledgeWorkspaceStore: KnowledgeWorkspaceStore2 },
@@ -27046,6 +27867,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27046
27867
  }
27047
27868
  ] = await Promise.all([
27048
27869
  Promise.resolve().then(() => (init_claim_store(), claim_store_exports)),
27870
+ Promise.resolve().then(() => (init_claims(), claims_exports)),
27049
27871
  Promise.resolve().then(() => (init_store(), store_exports)),
27050
27872
  Promise.resolve().then(() => (init_workspace(), workspace_exports)),
27051
27873
  Promise.resolve().then(() => (init_workspace_store(), workspace_store_exports)),
@@ -27095,10 +27917,52 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27095
27917
  targetPath: proposal.targetPath,
27096
27918
  reason: proposal.reason,
27097
27919
  createdAt: proposal.createdAt
27098
- }))
27920
+ })),
27921
+ reviewableClaims: claims.listClaims(project.id, { limit: 100 }).filter((claim) => claim.reviewState === "needs-review").map((claim) => ({ id: claim.id, subject: claim.subject, predicate: claim.predicate, objectValue: claim.objectValue }))
27099
27922
  }
27100
27923
  });
27101
27924
  }
27925
+ if (action === "claim_list") {
27926
+ return text({
27927
+ claims: claims.listClaims(project.id, { limit: 100 }).map((claim) => ({
27928
+ id: claim.id,
27929
+ subject: claim.subject,
27930
+ predicate: claim.predicate,
27931
+ objectValue: claim.objectValue,
27932
+ status: claim.status,
27933
+ reviewState: claim.reviewState,
27934
+ origin: claim.origin,
27935
+ confidence: claim.confidence,
27936
+ evidenceCount: claims.listEvidence(claim.id).length
27937
+ })),
27938
+ next: "Approve only after checking the linked evidence. Rejected claims stay out of retrieval and publication."
27939
+ });
27940
+ }
27941
+ if (action === "claim_review") {
27942
+ const requestedClaimId = requireText(claimId, "claimId");
27943
+ const detail = requireText(reviewDetail, "reviewDetail");
27944
+ if (!requestedClaimId || !claimReviewState || !detail) {
27945
+ return text({ error: "claimId, claimReviewState, and reviewDetail are required for claim_review." }, true);
27946
+ }
27947
+ const existing = claims.getClaim(requestedClaimId);
27948
+ if (!existing || existing.projectId !== project.id) {
27949
+ return text({ error: "Claim was not found for the current project." }, true);
27950
+ }
27951
+ const claim = reviewClaim2(claims, {
27952
+ claimId: requestedClaimId,
27953
+ reviewState: claimReviewState,
27954
+ detail
27955
+ });
27956
+ return text({
27957
+ claim: {
27958
+ id: claim.id,
27959
+ status: claim.status,
27960
+ reviewState: claim.reviewState,
27961
+ updatedAt: claim.updatedAt
27962
+ },
27963
+ next: claim.reviewState === "approved" ? "This approved source-backed claim can now be considered by knowledge compilation." : "This rejected claim is excluded from retrieval and knowledge compilation."
27964
+ });
27965
+ }
27102
27966
  if (action === "compile") {
27103
27967
  const result = await compileKnowledgeWorkspace2({ workspace, claims });
27104
27968
  return text({
@@ -27137,7 +28001,13 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27137
28001
  if (action === "workflow_import") {
27138
28002
  const result = await importWindsurfWorkflows2({ workspace, projectRoot });
27139
28003
  return text({
27140
- imported: result.imported.map((workflow2) => ({ id: workflow2.id, title: workflow2.title, sourcePath: workflow2.sourcePath })),
28004
+ imported: result.imported.map((workflow2) => ({
28005
+ id: workflow2.id,
28006
+ title: workflow2.title,
28007
+ sourcePath: workflow2.sourcePath,
28008
+ importedFrom: workflow2.importedFrom,
28009
+ verificationGates: workflow2.verificationGates
28010
+ })),
27141
28011
  skipped: result.skipped
27142
28012
  });
27143
28013
  }
@@ -27149,7 +28019,9 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27149
28019
  title: workflow2.title,
27150
28020
  status: workflow2.status,
27151
28021
  taskLenses: workflow2.taskLenses,
27152
- sourcePath: workflow2.sourcePath
28022
+ sourcePath: workflow2.sourcePath,
28023
+ importedFrom: workflow2.importedFrom,
28024
+ verificationGates: workflow2.verificationGates
27153
28025
  })),
27154
28026
  parseErrors: synced.errors
27155
28027
  });
@@ -27228,9 +28100,15 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27228
28100
  }
27229
28101
  },
27230
28102
  async ({ ids, status }) => {
27231
- const { resolveObservations: resolveObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
28103
+ const { resolveObservations: resolveObservations2, getObservation: getObservation2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
27232
28104
  const safeIds = (Array.isArray(ids) ? ids : [ids]).map((id) => coerceNumber(id, 0)).filter((id) => id > 0);
27233
- const result = await resolveObservations2(safeIds, status ?? "resolved");
28105
+ const reader = getObservationReader();
28106
+ const authorizedIds = safeIds.filter((id) => {
28107
+ const observation = getObservation2(id, project.id);
28108
+ return observation ? canManageObservation(observation, reader) : false;
28109
+ });
28110
+ const result = await resolveObservations2(authorizedIds, status ?? "resolved");
28111
+ const deniedCount = safeIds.length - authorizedIds.length;
27234
28112
  const parts = [];
27235
28113
  if (result.resolved.length > 0) {
27236
28114
  parts.push(`[OK] Resolved ${result.resolved.length} observation(s): #${result.resolved.join(", #")}`);
@@ -27238,6 +28116,9 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27238
28116
  if (result.notFound.length > 0) {
27239
28117
  parts.push(`[WARN] Not found: #${result.notFound.join(", #")}`);
27240
28118
  }
28119
+ if (deniedCount > 0) {
28120
+ parts.push(`[WARN] Skipped ${deniedCount} observation(s) outside this session's write scope.`);
28121
+ }
27241
28122
  parts.push('\nResolved memories are hidden from default search. Use status="all" to include them.');
27242
28123
  parts.push('[STATS] Run `memorix_retention` with `action: "report"` to check remaining cleanup status.');
27243
28124
  return {
@@ -27267,6 +28148,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27267
28148
  async ({ entityName, decision, alternatives, rationale, constraints, expectedOutcome, risks, concepts, filesModified, relatedCommits, relatedEntities }) => {
27268
28149
  const unresolved = requireResolvedProject("store reasoning in the current project");
27269
28150
  if (unresolved) return unresolved;
28151
+ const reader = getObservationReader();
27270
28152
  return withFreshIndex(async () => {
27271
28153
  const narrativeParts = [rationale];
27272
28154
  if (alternatives && alternatives.length > 0) {
@@ -27289,7 +28171,11 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27289
28171
  ]);
27290
28172
  let reasoningAttributionWarning = "";
27291
28173
  try {
27292
- const attrCheck = await checkProjectAttribution(entityName, project.id, getAllObservations());
28174
+ const attrCheck = await checkProjectAttribution(
28175
+ entityName,
28176
+ project.id,
28177
+ filterReadableObservations(getAllObservations(), reader)
28178
+ );
27293
28179
  if (attrCheck.suspicious) {
27294
28180
  reasoningAttributionWarning = `
27295
28181
  [WARN] Attribution notice: entity "${entityName}" has 0 observations in "${project.id}" but ${attrCheck.count} in "${attrCheck.knownIn}" (confidence: ${attrCheck.confidence}). Verify the correct project is bound before storing.`;
@@ -27309,7 +28195,8 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27309
28195
  relatedCommits,
27310
28196
  relatedEntities,
27311
28197
  sourceDetail: "explicit",
27312
- createdByAgentId: currentAgentId
28198
+ createdByAgentId: currentAgentId,
28199
+ visibility: "project"
27313
28200
  });
27314
28201
  await graphManager.addObservations([
27315
28202
  { entityName, contents: [`[#${obs.id}] [REASONING] ${decision}`] }
@@ -27341,7 +28228,11 @@ Entity: ${entityName} | ${facts.length} facts | ${obs.tokens} tokens${reasoningA
27341
28228
  const minCount = threshold ?? 2;
27342
28229
  let entries;
27343
28230
  try {
27344
- entries = await auditProjectObservations(project.id, await withFreshIndex(() => getAllObservations()), minCount);
28231
+ entries = await auditProjectObservations(
28232
+ project.id,
28233
+ await withFreshIndex(() => filterReadableObservations(getAllObservations(), getObservationReader())),
28234
+ minCount
28235
+ );
27345
28236
  } catch (err) {
27346
28237
  return {
27347
28238
  content: [{
@@ -27406,7 +28297,8 @@ Entity: ${entityName} | ${facts.length} facts | ${obs.tokens} tokens${reasoningA
27406
28297
  limit: safeLimit,
27407
28298
  type: "reasoning",
27408
28299
  projectId: scope === "global" ? void 0 : project.id,
27409
- status: "active"
28300
+ status: "active",
28301
+ reader: getObservationReader(scope === "global" ? "global" : "project")
27410
28302
  }));
27411
28303
  if (result.entries.length === 0) {
27412
28304
  return {
@@ -27431,7 +28323,11 @@ ${result.formatted}` }]
27431
28323
  },
27432
28324
  async ({ query, dryRun }) => {
27433
28325
  const { getAllObservations: getAllObservations2, resolveObservations: resolveObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
27434
- const allObs = await withFreshIndex(() => getAllObservations2().filter((o) => (o.status ?? "active") === "active" && o.projectId === project.id));
28326
+ const reader = getObservationReader();
28327
+ const allObs = await withFreshIndex(() => filterReadableObservations(
28328
+ getAllObservations2().filter((o) => (o.status ?? "active") === "active" && o.projectId === project.id),
28329
+ reader
28330
+ ).filter((observation) => canManageObservation(observation, reader)));
27435
28331
  if (allObs.length < 2) {
27436
28332
  return { content: [{ type: "text", text: "Not enough active memories to deduplicate." }] };
27437
28333
  }
@@ -27445,7 +28341,7 @@ ${result.formatted}` }]
27445
28341
  }
27446
28342
  let candidates;
27447
28343
  if (query) {
27448
- const searchResult = await compactSearch({ query, limit: 20, projectId: project.id, status: "active" });
28344
+ const searchResult = await compactSearch({ query, limit: 20, projectId: project.id, status: "active", reader });
27449
28345
  const idSet = new Set(searchResult.entries.map((e) => e.id));
27450
28346
  candidates = allObs.filter((o) => idSet.has(o.id));
27451
28347
  } else {
@@ -27535,7 +28431,8 @@ ${actions.join("\n")}`
27535
28431
  safeAnchor,
27536
28432
  project.id,
27537
28433
  safeBefore,
27538
- safeAfter
28434
+ safeAfter,
28435
+ getObservationReader()
27539
28436
  );
27540
28437
  return {
27541
28438
  content: [
@@ -27569,12 +28466,14 @@ ${actions.join("\n")}`
27569
28466
  const safeTypedRefs = coerceStringArray(typedRefs);
27570
28467
  let result;
27571
28468
  try {
28469
+ const hasCrossProjectRef = safeRefs.some((ref) => ref.projectId && ref.projectId !== project.id) || safeTypedRefs.some((ref) => ref.includes("@") && !ref.endsWith(`@${project.id}`));
28470
+ const reader = getObservationReader(hasCrossProjectRef ? "global" : "project");
27572
28471
  if (safeTypedRefs.length > 0) {
27573
- result = await compactDetail(safeTypedRefs);
28472
+ result = await compactDetail(safeTypedRefs, { reader });
27574
28473
  } else if (safeRefs.length > 0) {
27575
- result = await compactDetail(safeRefs);
28474
+ result = await compactDetail(safeRefs, { reader });
27576
28475
  } else {
27577
- result = await compactDetail(safeIds.map((id) => ({ id, projectId: project.id })));
28476
+ result = await compactDetail(safeIds.map((id) => ({ id, projectId: project.id })), { reader });
27578
28477
  }
27579
28478
  } catch (err) {
27580
28479
  return { content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }], isError: true };
@@ -27600,11 +28499,15 @@ ${actions.join("\n")}`
27600
28499
  },
27601
28500
  async (args) => {
27602
28501
  const action = args.action ?? "report";
27603
- const { getRetentionSummary: getRetentionSummary2, getArchiveCandidates: getArchiveCandidates2, rankByRelevance: rankByRelevance2, archiveExpired: archiveExpired2, getRetentionZone: getRetentionZone2, explainRetention: explainRetention2 } = await Promise.resolve().then(() => (init_retention(), retention_exports));
28502
+ const { getRetentionSummary: getRetentionSummary2, getArchiveCandidates: getArchiveCandidates2, rankByRelevance: rankByRelevance2, getRetentionZone: getRetentionZone2, explainRetention: explainRetention2 } = await Promise.resolve().then(() => (init_retention(), retention_exports));
27604
28503
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_orama_store(), orama_store_exports));
27605
28504
  const { search: search2 } = await import("@orama/orama");
27606
- const { getAllObservations: getAllObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
27607
- const allObs = await withFreshIndex(() => getAllObservations2());
28505
+ const { getAllObservations: getAllObservations2, resolveObservations: resolveObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
28506
+ const reader = getObservationReader();
28507
+ const allObs = await withFreshIndex(() => filterReadableObservations(
28508
+ getAllObservations2().filter((observation) => observation.projectId === project.id),
28509
+ reader
28510
+ ));
27608
28511
  const accessMap = /* @__PURE__ */ new Map();
27609
28512
  try {
27610
28513
  const database = await getDb2();
@@ -27621,20 +28524,6 @@ ${actions.join("\n")}`
27621
28524
  }
27622
28525
  } catch {
27623
28526
  }
27624
- if (action === "archive") {
27625
- const result = await archiveExpired2(projectDir2, void 0, accessMap, project.id);
27626
- if (result.archived === 0) {
27627
- return {
27628
- content: [{ type: "text", text: "[OK] No expired observations to archive. All memories are within their retention period." }]
27629
- };
27630
- }
27631
- return {
27632
- content: [{ type: "text", text: `[ARCHIVED] Archived ${result.archived} expired observations (status set to 'archived' in-place)
27633
- ${result.remaining} active observations remaining.
27634
-
27635
- Archived memories are hidden from default search but can be found with status: "all".` }]
27636
- };
27637
- }
27638
28527
  const docs = allObs.map((obs) => ({
27639
28528
  id: `obs-${obs.id}`,
27640
28529
  observationId: obs.id,
@@ -27653,13 +28542,34 @@ Archived memories are hidden from default search but can be found with status: "
27653
28542
  status: obs.status ?? "active",
27654
28543
  source: obs.source ?? "agent",
27655
28544
  sourceDetail: obs.sourceDetail ?? "",
27656
- valueCategory: obs.valueCategory ?? ""
28545
+ valueCategory: obs.valueCategory ?? "",
28546
+ admissionState: obs.admissionState ?? "",
28547
+ admissionReason: obs.admissionReason ?? "",
28548
+ visibility: obs.visibility ?? "project",
28549
+ createdByAgentId: obs.createdByAgentId ?? "",
28550
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? [])
27657
28551
  }));
27658
28552
  if (docs.length === 0) {
27659
28553
  return {
27660
28554
  content: [{ type: "text", text: "No observations found for this project." }]
27661
28555
  };
27662
28556
  }
28557
+ if (action === "archive") {
28558
+ const managedIds = new Set(
28559
+ allObs.filter((observation) => canManageObservation(observation, reader)).map((observation) => observation.id)
28560
+ );
28561
+ const candidates2 = getArchiveCandidates2(docs).filter((document) => managedIds.has(document.observationId));
28562
+ if (candidates2.length === 0) {
28563
+ return {
28564
+ content: [{ type: "text", text: "[OK] No expired memories in this session's write scope to archive." }]
28565
+ };
28566
+ }
28567
+ const result = await resolveObservations2(candidates2.map((document) => document.observationId), "archived");
28568
+ return {
28569
+ content: [{ type: "text", text: `[ARCHIVED] Archived ${result.resolved.length} expired observation(s) in this session's write scope.
28570
+ ${Math.max(0, managedIds.size - result.resolved.length)} visible writable observations remaining.` }]
28571
+ };
28572
+ }
27663
28573
  if (action === "stale") {
27664
28574
  const staleDocs = docs.filter((d) => getRetentionZone2(d) === "stale");
27665
28575
  if (staleDocs.length === 0) {
@@ -27978,13 +28888,14 @@ Archived memories are hidden from default search but can be found with status: "
27978
28888
  async function scopeGraphToProject(graph) {
27979
28889
  const { getAllObservations: getAllObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
27980
28890
  const allObs = await withFreshIndex(() => getAllObservations2());
27981
- const projectEntityNames = new Set(
27982
- allObs.filter((o) => o.projectId === project.id && (o.status ?? "active") === "active" && o.entityName).map((o) => o.entityName)
28891
+ const scoped = scopeKnowledgeGraphToProject(
28892
+ graph,
28893
+ filterReadableObservations(
28894
+ allObs.filter((observation) => observation.projectId === project.id),
28895
+ getObservationReader()
28896
+ )
27983
28897
  );
27984
- const entities = graph.entities.filter((e) => projectEntityNames.has(e.name));
27985
- const entityNameSet = new Set(entities.map((e) => e.name));
27986
- const relations = graph.relations.filter((r) => entityNameSet.has(r.from) && entityNameSet.has(r.to));
27987
- return { entities, relations };
28898
+ return { entities: scoped.entities, relations: scoped.relations };
27988
28899
  }
27989
28900
  server.registerTool(
27990
28901
  "read_graph",
@@ -28257,7 +29168,10 @@ ${skill2.content}` }]
28257
29168
  };
28258
29169
  }
28259
29170
  const { getObservationStore: getStore } = await Promise.resolve().then(() => (init_obs_store(), obs_store_exports));
28260
- const allObs = await getStore().loadAll();
29171
+ const allObs = filterReadableObservations(
29172
+ (await getStore().loadAll()).filter((observation) => observation.projectId === project.id),
29173
+ getObservationReader()
29174
+ );
28261
29175
  const obsData = allObs.map((o) => ({
28262
29176
  id: o.id || 0,
28263
29177
  entityName: o.entityName || "unknown",
@@ -28356,7 +29270,11 @@ ${skill2.content}` }]
28356
29270
  }
28357
29271
  const { getAllObservations: getAllObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
28358
29272
  const allObs = await withFreshIndex(() => getAllObservations2());
28359
- const matched = allObs.filter((o) => observationIds.includes(o.id));
29273
+ const reader = getObservationReader();
29274
+ const matched = filterReadableObservations(
29275
+ allObs.filter((observation) => observation.projectId === project.id && observationIds.includes(observation.id)),
29276
+ reader
29277
+ );
28360
29278
  if (matched.length === 0) {
28361
29279
  return { content: [{ type: "text", text: `No observations found for IDs: [${observationIds.join(", ")}]. Use \`memorix_search\` to find valid IDs.` }], isError: true };
28362
29280
  }
@@ -28364,6 +29282,13 @@ ${skill2.content}` }]
28364
29282
  if (nonActive.length > 0) {
28365
29283
  return { content: [{ type: "text", text: `Cannot promote: ${nonActive.length} observation(s) are not active: ${nonActive.map((o) => `#${o.id} (${o.status})`).join(", ")}. Only active observations can be promoted to permanent knowledge.` }], isError: true };
28366
29284
  }
29285
+ const nonProjectShared = matched.filter((observation) => resolveObservationVisibility(observation) !== "project");
29286
+ if (nonProjectShared.length > 0) {
29287
+ return {
29288
+ content: [{ type: "text", text: `Cannot promote private or team-scoped observations: ${nonProjectShared.map((observation) => `#${observation.id}`).join(", ")}. Promote only deliberate project-shared knowledge.` }],
29289
+ isError: true
29290
+ };
29291
+ }
28367
29292
  const skill2 = await promoteToMiniSkill2(projectDir2, project.id, matched, { trigger, instruction, tags });
28368
29293
  const lines = [
28369
29294
  `[OK] Created mini-skill #${skill2.id}`,
@@ -28522,8 +29447,11 @@ Ensure the path points to a directory containing a .git folder (or a subdirector
28522
29447
  const lastSeen = registeredAgent.last_seen_obs_generation;
28523
29448
  const store = getObservationStore();
28524
29449
  const currentGen = store.getGeneration();
28525
- const projectObs = await withFreshIndex(() => getAllObservations().filter(
28526
- (o) => o.projectId === project.id && (o.writeGeneration ?? 0) > lastSeen
29450
+ const projectObs = await withFreshIndex(() => filterReadableObservations(
29451
+ getAllObservations().filter(
29452
+ (o) => o.projectId === project.id && (o.writeGeneration ?? 0) > lastSeen
29453
+ ),
29454
+ getObservationReader()
28527
29455
  ));
28528
29456
  const wm = computeWatermark2(lastSeen, currentGen, projectObs.length);
28529
29457
  if (wm.newObservationCount > 0) {
@@ -28692,7 +29620,7 @@ ${summary ? "Summary saved for next session context injection." : "No summary pr
28692
29620
  "memorix_transfer",
28693
29621
  {
28694
29622
  title: "Transfer Memories",
28695
- description: 'Export or import project memories. Action "export": export observations and sessions (JSON or Markdown). Action "import": import from a JSON export (re-assigns IDs, skips duplicate topicKeys).',
29623
+ description: 'Export or import project memories. Action "export": export observations visible to the current agent and project sessions (JSON or Markdown). Action "import": import from a JSON export (re-assigns IDs, skips duplicate topicKeys).',
28696
29624
  inputSchema: {
28697
29625
  action: z2.enum(["export", "import"]).describe("Operation: export or import"),
28698
29626
  format: z2.enum(["json", "markdown"]).optional().describe("Export format (for export, default: json)"),
@@ -28703,10 +29631,10 @@ ${summary ? "Summary saved for next session context injection." : "No summary pr
28703
29631
  if (action === "export") {
28704
29632
  const { exportAsJson: exportAsJson2, exportAsMarkdown: exportAsMarkdown2 } = await Promise.resolve().then(() => (init_export_import(), export_import_exports));
28705
29633
  if (format === "markdown") {
28706
- const md = await exportAsMarkdown2(projectDir2, project.id);
29634
+ const md = await exportAsMarkdown2(projectDir2, project.id, getObservationReader());
28707
29635
  return { content: [{ type: "text", text: md }] };
28708
29636
  }
28709
- const data = await exportAsJson2(projectDir2, project.id);
29637
+ const data = await exportAsJson2(projectDir2, project.id, getObservationReader());
28710
29638
  const json = JSON.stringify(data, null, 2);
28711
29639
  return {
28712
29640
  content: [{
@@ -28903,8 +29831,6 @@ ${json}
28903
29831
  }
28904
29832
  }
28905
29833
  );
28906
- let teamStore;
28907
- let initTeamStoreForProject;
28908
29834
  if (teamFeaturesEnabled) {
28909
29835
  const { initTeamStore: initTeamStore2 } = await Promise.resolve().then(() => (init_team_store(), team_store_exports));
28910
29836
  initTeamStoreForProject = initTeamStore2;
@@ -29177,7 +30103,7 @@ ${lines.join("\n")}` }] };
29177
30103
  description: `Send, broadcast, or read messages between agents. Durable: messages survive restarts and reach inactive recipients. Action "send": direct message to one agent. Action "broadcast": message all agents. Action "inbox": read this agent's inbox.`,
29178
30104
  inputSchema: {
29179
30105
  action: z2.enum(["send", "broadcast", "inbox"]).describe("Operation to perform"),
29180
- from: z2.string().optional().describe("Sender agent ID (for send/broadcast)"),
30106
+ from: z2.string().optional().describe("Your sender agent ID. Omit it to use this session identity."),
29181
30107
  to: z2.string().optional().describe("Receiver agent ID (for send)"),
29182
30108
  type: z2.enum(["request", "response", "info", "announcement", "contract", "error", "handoff"]).optional().describe("Message type (for send/broadcast)"),
29183
30109
  content: z2.string().optional().describe("Message content (for send/broadcast)"),
@@ -29188,13 +30114,20 @@ ${lines.join("\n")}` }] };
29188
30114
  }
29189
30115
  },
29190
30116
  async ({ action, from, to, type: msgType, content, agentId, markRead, toRole, handoffStatus }) => {
30117
+ const requireCurrentTeamAgent = () => {
30118
+ if (!currentAgentId) return null;
30119
+ const agent = teamStore.getAgent(currentAgentId);
30120
+ return agent?.project_id === project.id && agent.status === "active" ? agent : null;
30121
+ };
29191
30122
  if (action === "send") {
29192
- if (!from || !msgType || !content) return { content: [{ type: "text", text: "[ERROR] from, type, and content required for send" }], isError: true };
30123
+ const sender = requireCurrentTeamAgent();
30124
+ if (!sender || !msgType || !content) return { content: [{ type: "text", text: "[ERROR] active session identity, type, and content required for send" }], isError: true };
30125
+ if (from && from !== currentAgentId) return { content: [{ type: "text", text: "[ERROR] from must match the current session identity" }], isError: true };
29193
30126
  if (!to && !toRole) return { content: [{ type: "text", text: "[ERROR] either to (agent ID) or toRole is required for send" }], isError: true };
29194
30127
  if (content.length > 1e4) return { content: [{ type: "text", text: "[ERROR] Message too large (max 10KB)" }], isError: true };
29195
30128
  const msg = teamStore.sendMessage({
29196
30129
  projectId: project.id,
29197
- senderAgentId: from,
30130
+ senderAgentId: currentAgentId,
29198
30131
  recipientAgentId: to ?? null,
29199
30132
  type: msgType,
29200
30133
  content,
@@ -29206,11 +30139,13 @@ ${lines.join("\n")}` }] };
29206
30139
  return { content: [{ type: "text", text: `Message sent (${msgType}) to ${target} | ID: ${msg.id.slice(0, 8)}\u2026${toRole ? ` [role: ${toRole}]` : ""}` }] };
29207
30140
  }
29208
30141
  if (action === "broadcast") {
29209
- if (!from || !msgType || !content) return { content: [{ type: "text", text: "[ERROR] from, type, and content required for broadcast" }], isError: true };
30142
+ const sender = requireCurrentTeamAgent();
30143
+ if (!sender || !msgType || !content) return { content: [{ type: "text", text: "[ERROR] active session identity, type, and content required for broadcast" }], isError: true };
30144
+ if (from && from !== currentAgentId) return { content: [{ type: "text", text: "[ERROR] from must match the current session identity" }], isError: true };
29210
30145
  if (content.length > 1e4) return { content: [{ type: "text", text: "[ERROR] Message too large (max 10KB)" }], isError: true };
29211
30146
  const msg = teamStore.sendMessage({
29212
30147
  projectId: project.id,
29213
- senderAgentId: from,
30148
+ senderAgentId: currentAgentId,
29214
30149
  recipientAgentId: null,
29215
30150
  type: msgType,
29216
30151
  content
@@ -29218,8 +30153,12 @@ ${lines.join("\n")}` }] };
29218
30153
  if ("error" in msg) return { content: [{ type: "text", text: `[ERROR] ${msg.error}` }], isError: true };
29219
30154
  return { content: [{ type: "text", text: `Broadcast (${msgType}) | ID: ${msg.id.slice(0, 8)}\u2026` }] };
29220
30155
  }
29221
- const inboxId = agentId || from || "";
29222
- if (!inboxId) return { content: [{ type: "text", text: "[ERROR] agentId required for inbox" }], isError: true };
30156
+ const inboxAgent = requireCurrentTeamAgent();
30157
+ if (!inboxAgent) return { content: [{ type: "text", text: "[ERROR] active session identity required for inbox" }], isError: true };
30158
+ if (agentId && agentId !== currentAgentId || from && from !== currentAgentId) {
30159
+ return { content: [{ type: "text", text: "[ERROR] inbox access is limited to the current session identity" }], isError: true };
30160
+ }
30161
+ const inboxId = currentAgentId;
29223
30162
  const inbox = teamStore.getInbox(project.id, inboxId);
29224
30163
  const unread = teamStore.getUnreadCount(project.id, inboxId);
29225
30164
  if (inbox.length === 0) return { content: [{ type: "text", text: "Inbox empty" }] };
@@ -29247,24 +30186,34 @@ ${lines.join("\n")}` }] };
29247
30186
  },
29248
30187
  async ({ agentId, markInboxRead }) => {
29249
30188
  const { computeWatermark: computeWatermark2, computePoll: computePoll2 } = await Promise.resolve().then(() => (init_poll(), poll_exports));
30189
+ if (agentId && agentId !== currentAgentId) {
30190
+ return {
30191
+ content: [{ type: "text", text: "[ERROR] agentId must match the current session identity." }],
30192
+ isError: true
30193
+ };
30194
+ }
30195
+ const effectiveAgentId = currentAgentId;
29250
30196
  let watermark = computeWatermark2(0, 0, 0);
29251
- if (agentId) {
29252
- const agent = teamStore.getAgent(agentId);
30197
+ if (effectiveAgentId) {
30198
+ const agent = teamStore.getAgent(effectiveAgentId);
29253
30199
  if (agent) {
29254
30200
  const lastSeen = agent.last_seen_obs_generation;
29255
30201
  const store = getObservationStore();
29256
30202
  const currentGen = store.getGeneration();
29257
- const projectObs = await withFreshIndex(() => getAllObservations().filter(
29258
- (o) => o.projectId === project.id && (o.writeGeneration ?? 0) > lastSeen
30203
+ const projectObs = await withFreshIndex(() => filterReadableObservations(
30204
+ getAllObservations().filter(
30205
+ (o) => o.projectId === project.id && (o.writeGeneration ?? 0) > lastSeen
30206
+ ),
30207
+ getObservationReader()
29259
30208
  ));
29260
30209
  watermark = computeWatermark2(lastSeen, currentGen, projectObs.length);
29261
- teamStore.updateWatermark(agentId, currentGen);
29262
- teamStore.heartbeat(agentId);
30210
+ teamStore.updateWatermark(effectiveAgentId, currentGen);
30211
+ teamStore.heartbeat(effectiveAgentId);
29263
30212
  }
29264
30213
  }
29265
- const poll = computePoll2(teamStore, project.id, agentId ?? null, watermark);
29266
- if (markInboxRead && agentId) {
29267
- teamStore.markAllRead(project.id, agentId);
30214
+ const poll = computePoll2(teamStore, project.id, effectiveAgentId ?? null, watermark);
30215
+ if (markInboxRead && effectiveAgentId) {
30216
+ teamStore.markAllRead(project.id, effectiveAgentId);
29268
30217
  }
29269
30218
  const lines = [];
29270
30219
  if (poll.agent) {
@@ -29324,7 +30273,7 @@ ${lines.join("\n")}` }] };
29324
30273
  "memorix_handoff",
29325
30274
  {
29326
30275
  title: "Team Handoff \u2014 Agent Context Transfer",
29327
- description: "Create a structured handoff artifact when passing work to another agent. The handoff is stored as a durable observation (searchable, immune to archival) and a notification message is sent to the recipient. Use this when completing a task and another agent should continue, or when you want to leave context for whoever works on this next.",
30276
+ description: "Create a structured handoff artifact when passing work to another agent. A targeted handoff is visible only to its sender and recipient; a broadcast handoff is team-visible. The handoff is stored as a durable observation (immune to archival) and a notification message is sent to the recipient. Use this when completing a task and another agent should continue, or when you want to leave context for whoever works on this next.",
29328
30277
  inputSchema: {
29329
30278
  fromAgentId: z2.string().describe("Your agent ID (from team_manage join or session_start with joinTeam=true)"),
29330
30279
  summary: z2.string().describe("Human-readable summary of what you did and what needs to happen next"),
@@ -29337,6 +30286,34 @@ ${lines.join("\n")}` }] };
29337
30286
  },
29338
30287
  async ({ fromAgentId, summary, context, toAgentId, taskId, filesModified, concepts }) => {
29339
30288
  const { createHandoffArtifact: createHandoffArtifact2 } = await Promise.resolve().then(() => (init_handoff(), handoff_exports));
30289
+ if (!currentAgentId) {
30290
+ return {
30291
+ content: [{ type: "text", text: "Create a coordination identity first: call memorix_session_start with joinTeam=true." }],
30292
+ isError: true
30293
+ };
30294
+ }
30295
+ if (fromAgentId !== currentAgentId) {
30296
+ return {
30297
+ content: [{ type: "text", text: "fromAgentId must match the identity returned for this session. Memorix will not create a handoff on behalf of another agent." }],
30298
+ isError: true
30299
+ };
30300
+ }
30301
+ const sender = teamStore.getAgent(currentAgentId);
30302
+ if (!sender || sender.project_id !== project.id || sender.status !== "active") {
30303
+ return {
30304
+ content: [{ type: "text", text: "The current coordination identity is not an active member of this project." }],
30305
+ isError: true
30306
+ };
30307
+ }
30308
+ if (toAgentId) {
30309
+ const recipient = teamStore.getAgent(toAgentId);
30310
+ if (!recipient || recipient.project_id !== project.id) {
30311
+ return {
30312
+ content: [{ type: "text", text: "The handoff recipient must be an agent registered in the current project." }],
30313
+ isError: true
30314
+ };
30315
+ }
30316
+ }
29340
30317
  const result = await createHandoffArtifact2(
29341
30318
  {
29342
30319
  projectId: project.id,
@@ -29576,6 +30553,8 @@ var MemoryClient = class {
29576
30553
  _projectId;
29577
30554
  _projectRoot;
29578
30555
  _dataDir;
30556
+ /** SDK calls are unbound by default, so they see only project-shared records. */
30557
+ _reader;
29579
30558
  _closed = false;
29580
30559
  // Internal module references — loaded lazily to avoid top-level side effects
29581
30560
  _observations;
@@ -29587,6 +30566,7 @@ var MemoryClient = class {
29587
30566
  this._projectId = projectId;
29588
30567
  this._projectRoot = projectRoot;
29589
30568
  this._dataDir = dataDir;
30569
+ this._reader = { projectId };
29590
30570
  }
29591
30571
  /** The canonical project ID (derived from Git remote or local path) */
29592
30572
  get projectId() {
@@ -29645,7 +30625,8 @@ var MemoryClient = class {
29645
30625
  this._ensureOpen();
29646
30626
  return this._observations.storeObservation({
29647
30627
  ...input,
29648
- projectId: this._projectId
30628
+ projectId: this._projectId,
30629
+ visibilityReader: this._reader
29649
30630
  });
29650
30631
  }
29651
30632
  /**
@@ -29669,7 +30650,8 @@ var MemoryClient = class {
29669
30650
  limit: options.limit ?? 20,
29670
30651
  type: options.type,
29671
30652
  source: options.source,
29672
- status: options.status === "all" ? void 0 : options.status ?? "active"
30653
+ status: options.status === "all" ? void 0 : options.status ?? "active",
30654
+ reader: this._reader
29673
30655
  };
29674
30656
  return this._oramaStore.searchObservations(searchOpts);
29675
30657
  }
@@ -29680,7 +30662,8 @@ var MemoryClient = class {
29680
30662
  this._ensureOpen();
29681
30663
  await this._freshness.withFreshIndex(() => {
29682
30664
  });
29683
- return this._observations.getObservation(id, this._projectId);
30665
+ const observation = this._observations.getObservation(id, this._projectId);
30666
+ return observation && canReadObservation(observation, this._reader) ? observation : void 0;
29684
30667
  }
29685
30668
  /**
29686
30669
  * Get all observations for this project.
@@ -29689,7 +30672,10 @@ var MemoryClient = class {
29689
30672
  this._ensureOpen();
29690
30673
  await this._freshness.withFreshIndex(() => {
29691
30674
  });
29692
- return this._observations.getProjectObservations(this._projectId);
30675
+ return filterReadableObservations(
30676
+ this._observations.getProjectObservations(this._projectId),
30677
+ this._reader
30678
+ );
29693
30679
  }
29694
30680
  /**
29695
30681
  * Get the total observation count for this project.
@@ -29698,7 +30684,10 @@ var MemoryClient = class {
29698
30684
  this._ensureOpen();
29699
30685
  await this._freshness.withFreshIndex(() => {
29700
30686
  });
29701
- return this._observations.getProjectObservations(this._projectId).length;
30687
+ return filterReadableObservations(
30688
+ this._observations.getProjectObservations(this._projectId),
30689
+ this._reader
30690
+ ).length;
29702
30691
  }
29703
30692
  /**
29704
30693
  * Mark observations as resolved or archived.
@@ -29714,7 +30703,16 @@ var MemoryClient = class {
29714
30703
  */
29715
30704
  async resolve(ids, status = "resolved") {
29716
30705
  this._ensureOpen();
29717
- return this._observations.resolveObservations(ids, status);
30706
+ const manageableIds = ids.filter((id) => {
30707
+ const observation = this._observations.getObservation(id, this._projectId);
30708
+ return observation && canManageObservation(observation, this._reader);
30709
+ });
30710
+ const deniedIds = ids.filter((id) => !manageableIds.includes(id));
30711
+ const result = await this._observations.resolveObservations(manageableIds, status);
30712
+ return {
30713
+ resolved: result.resolved,
30714
+ notFound: [.../* @__PURE__ */ new Set([...result.notFound, ...deniedIds])]
30715
+ };
29718
30716
  }
29719
30717
  /**
29720
30718
  * Release resources (close SQLite handle, reset index).