memorix 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +14 -2
  3. package/README.zh-CN.md +14 -2
  4. package/dist/cli/index.js +15424 -13780
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/index.js +1337 -536
  7. package/dist/index.js.map +1 -1
  8. package/dist/maintenance-runner.d.ts +1 -1
  9. package/dist/maintenance-runner.js +8458 -8087
  10. package/dist/maintenance-runner.js.map +1 -1
  11. package/dist/memcode-runtime/CHANGELOG.md +23 -0
  12. package/dist/sdk.d.ts +7 -2
  13. package/dist/sdk.js +1365 -542
  14. package/dist/sdk.js.map +1 -1
  15. package/dist/types.d.ts +49 -1
  16. package/dist/types.js.map +1 -1
  17. package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
  18. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
  19. package/docs/API_REFERENCE.md +24 -4
  20. package/docs/README.md +1 -1
  21. package/docs/dev-log/progress.txt +101 -11
  22. package/package.json +1 -1
  23. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  24. package/src/cli/command-guide.ts +192 -0
  25. package/src/cli/commands/audit.ts +9 -4
  26. package/src/cli/commands/cleanup.ts +5 -1
  27. package/src/cli/commands/codegraph.ts +15 -5
  28. package/src/cli/commands/context.ts +3 -2
  29. package/src/cli/commands/doctor.ts +4 -2
  30. package/src/cli/commands/explain.ts +9 -3
  31. package/src/cli/commands/handoff.ts +21 -7
  32. package/src/cli/commands/identity.ts +116 -0
  33. package/src/cli/commands/ingest-image.ts +5 -3
  34. package/src/cli/commands/lock.ts +11 -10
  35. package/src/cli/commands/memory.ts +58 -21
  36. package/src/cli/commands/message.ts +19 -14
  37. package/src/cli/commands/operator-shared.ts +98 -3
  38. package/src/cli/commands/poll.ts +16 -6
  39. package/src/cli/commands/reasoning.ts +17 -3
  40. package/src/cli/commands/retention.ts +9 -4
  41. package/src/cli/commands/serve-http.ts +8 -2
  42. package/src/cli/commands/session.ts +44 -10
  43. package/src/cli/commands/skills.ts +10 -5
  44. package/src/cli/commands/status.ts +4 -3
  45. package/src/cli/commands/task.ts +26 -17
  46. package/src/cli/commands/team.ts +14 -10
  47. package/src/cli/commands/transfer.ts +63 -10
  48. package/src/cli/identity.ts +89 -0
  49. package/src/cli/index.ts +96 -19
  50. package/src/cli/invocation.ts +115 -0
  51. package/src/cli/tui/chat-service.ts +41 -18
  52. package/src/cli/tui/data.ts +23 -44
  53. package/src/cli/tui/operator-context.ts +60 -0
  54. package/src/cli/tui/session-service.ts +3 -2
  55. package/src/cli/tui/views/MemoryView.tsx +10 -8
  56. package/src/codegraph/auto-context.ts +31 -2
  57. package/src/codegraph/context-pack.ts +1 -0
  58. package/src/codegraph/project-context.ts +2 -0
  59. package/src/compact/engine.ts +26 -10
  60. package/src/compact/index-format.ts +25 -2
  61. package/src/dashboard/server.ts +46 -9
  62. package/src/hooks/admission.ts +117 -0
  63. package/src/hooks/handler.ts +98 -91
  64. package/src/knowledge/context-assembly.ts +97 -0
  65. package/src/knowledge/workset.ts +179 -10
  66. package/src/memory/admission.ts +57 -0
  67. package/src/memory/consolidation.ts +13 -2
  68. package/src/memory/disclosure-policy.ts +6 -1
  69. package/src/memory/export-import.ts +11 -3
  70. package/src/memory/graph-context.ts +8 -2
  71. package/src/memory/observations.ts +162 -4
  72. package/src/memory/quality-audit.ts +2 -0
  73. package/src/memory/retention.ts +22 -2
  74. package/src/memory/session.ts +29 -11
  75. package/src/memory/visibility.ts +80 -0
  76. package/src/orchestrate/memorix-bridge.ts +38 -0
  77. package/src/runtime/control-plane-maintenance.ts +1 -0
  78. package/src/runtime/isolated-maintenance.ts +1 -0
  79. package/src/runtime/lifecycle.ts +18 -0
  80. package/src/runtime/maintenance-jobs.ts +1 -0
  81. package/src/runtime/maintenance-runner.ts +2 -0
  82. package/src/runtime/project-maintenance.ts +89 -0
  83. package/src/sdk.ts +35 -5
  84. package/src/server.ts +267 -83
  85. package/src/store/orama-store.ts +61 -6
  86. package/src/store/sqlite-db.ts +23 -1
  87. package/src/store/sqlite-store.ts +12 -2
  88. package/src/team/handoff.ts +7 -0
  89. package/src/types.ts +51 -0
  90. package/src/wiki/generator.ts +2 -0
package/dist/index.js CHANGED
@@ -209,7 +209,11 @@ CREATE TABLE IF NOT EXISTS observations (
209
209
  relatedCommits TEXT,
210
210
  relatedEntities TEXT,
211
211
  sourceDetail TEXT,
212
- valueCategory TEXT
212
+ valueCategory TEXT,
213
+ admissionState TEXT,
214
+ admissionReason TEXT,
215
+ visibility TEXT,
216
+ sharedWithAgentIds TEXT
213
217
  );
214
218
  `;
215
219
  CREATE_MINI_SKILLS_TABLE = `
@@ -632,6 +636,8 @@ CREATE INDEX IF NOT EXISTS idx_observations_projectId ON observations(projectId)
632
636
  CREATE INDEX IF NOT EXISTS idx_observations_topicKey ON observations(projectId, topicKey);
633
637
  CREATE INDEX IF NOT EXISTS idx_observations_status ON observations(status);
634
638
  CREATE INDEX IF NOT EXISTS idx_observations_project_status_id ON observations(projectId, status, id);
639
+ CREATE INDEX IF NOT EXISTS idx_observations_project_admission ON observations(projectId, status, admissionState, id);
640
+ CREATE INDEX IF NOT EXISTS idx_observations_project_visibility ON observations(projectId, status, visibility, id);
635
641
  CREATE INDEX IF NOT EXISTS idx_mini_skills_projectId ON mini_skills(projectId);
636
642
  CREATE INDEX IF NOT EXISTS idx_sessions_projectId ON sessions(projectId);
637
643
  CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(projectId, status);
@@ -676,6 +682,22 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_maintenance_jobs_active_dedupe
676
682
  WHERE status IN ('pending', 'running', 'retry');
677
683
  `;
678
684
  SCHEMA_MIGRATIONS = [
685
+ {
686
+ id: "1.2.2-observation-admission",
687
+ apply: (db2) => {
688
+ addColumnIfMissing(db2, "observations", "admissionState", "admissionState TEXT");
689
+ addColumnIfMissing(db2, "observations", "admissionReason", "admissionReason TEXT");
690
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_observations_project_admission ON observations(projectId, status, admissionState, id)");
691
+ }
692
+ },
693
+ {
694
+ id: "1.2.2-observation-visibility",
695
+ apply: (db2) => {
696
+ addColumnIfMissing(db2, "observations", "visibility", "visibility TEXT");
697
+ addColumnIfMissing(db2, "observations", "sharedWithAgentIds", "sharedWithAgentIds TEXT");
698
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_observations_project_visibility ON observations(projectId, status, visibility, id)");
699
+ }
700
+ },
679
701
  {
680
702
  id: "1.2-code-state-snapshots",
681
703
  apply: (db2) => {
@@ -2118,6 +2140,58 @@ var init_mini_skills = __esm({
2118
2140
  }
2119
2141
  });
2120
2142
 
2143
+ // src/memory/visibility.ts
2144
+ function resolveObservationVisibility(record) {
2145
+ switch (record.visibility) {
2146
+ case "personal":
2147
+ case "team":
2148
+ case "project":
2149
+ return record.visibility;
2150
+ default:
2151
+ return "project";
2152
+ }
2153
+ }
2154
+ function sharedAgentIds(record) {
2155
+ if (Array.isArray(record.sharedWithAgentIds)) return record.sharedWithAgentIds;
2156
+ if (typeof record.sharedWithAgentIds !== "string" || !record.sharedWithAgentIds) return [];
2157
+ try {
2158
+ const parsed = JSON.parse(record.sharedWithAgentIds);
2159
+ return Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string") : [];
2160
+ } catch {
2161
+ return [];
2162
+ }
2163
+ }
2164
+ function canReadObservation(record, reader) {
2165
+ if (!reader) return true;
2166
+ const sameProject = reader.projectId === record.projectId;
2167
+ const visibility = resolveObservationVisibility(record);
2168
+ if (visibility === "project") return !reader.projectId || sameProject;
2169
+ if (!sameProject || !reader.agentId) return false;
2170
+ if (visibility === "team") return reader.isTeamMember === true;
2171
+ return record.createdByAgentId === reader.agentId || sharedAgentIds(record).includes(reader.agentId);
2172
+ }
2173
+ function canManageObservation(record, reader) {
2174
+ if (!reader) return true;
2175
+ if (!reader.projectId || reader.projectId !== record.projectId) return false;
2176
+ switch (resolveObservationVisibility(record)) {
2177
+ case "project":
2178
+ return true;
2179
+ case "team":
2180
+ return reader.isTeamMember === true;
2181
+ case "personal":
2182
+ return Boolean(reader.agentId && record.createdByAgentId === reader.agentId);
2183
+ }
2184
+ }
2185
+ function filterReadableObservations(observations2, reader) {
2186
+ return reader ? observations2.filter((observation) => canReadObservation(observation, reader)) : [...observations2];
2187
+ }
2188
+ var init_visibility = __esm({
2189
+ "src/memory/visibility.ts"() {
2190
+ "use strict";
2191
+ init_esm_shims();
2192
+ }
2193
+ });
2194
+
2121
2195
  // src/config/config-paths.ts
2122
2196
  import { join } from "path";
2123
2197
  function getGlobalConfigTomlPath(homeDir) {
@@ -5345,7 +5419,8 @@ __export(orama_store_exports, {
5345
5419
  makeOramaObservationId: () => makeOramaObservationId,
5346
5420
  removeObservation: () => removeObservation,
5347
5421
  resetDb: () => resetDb,
5348
- searchObservations: () => searchObservations
5422
+ searchObservations: () => searchObservations,
5423
+ updateObservationMetadata: () => updateObservationMetadata
5349
5424
  });
5350
5425
  import { create, insert as insert2, search, remove as remove2, update, count, getByID } from "@orama/orama";
5351
5426
  function getLastSearchMode(projectId) {
@@ -5423,6 +5498,11 @@ async function initializeDb(options, generation) {
5423
5498
  source: "string",
5424
5499
  sourceDetail: "string",
5425
5500
  valueCategory: "string",
5501
+ admissionState: "string",
5502
+ admissionReason: "string",
5503
+ visibility: "string",
5504
+ createdByAgentId: "string",
5505
+ sharedWithAgentIds: "string",
5426
5506
  documentType: "string",
5427
5507
  knowledgeLayer: "string"
5428
5508
  };
@@ -5561,6 +5641,13 @@ async function hydrateIndex(observations2, options = {}) {
5561
5641
  lastAccessedAt: obs.lastAccessedAt || "",
5562
5642
  status: obs.status ?? "active",
5563
5643
  source: obs.source || "agent",
5644
+ sourceDetail: obs.sourceDetail ?? "",
5645
+ valueCategory: obs.valueCategory ?? "",
5646
+ admissionState: obs.admissionState ?? "",
5647
+ admissionReason: obs.admissionReason ?? "",
5648
+ visibility: obs.visibility ?? "project",
5649
+ createdByAgentId: obs.createdByAgentId ?? "",
5650
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
5564
5651
  documentType: "observation",
5565
5652
  knowledgeLayer: resolveKnowledgeLayer("observation", obs.sourceDetail, obs.source),
5566
5653
  ...compatibleVector ? { embedding: compatibleVector } : {}
@@ -5594,6 +5681,16 @@ async function insertObservation(doc) {
5594
5681
  await insert2(database, doc);
5595
5682
  rememberObservationDoc(doc);
5596
5683
  }
5684
+ async function updateObservationMetadata(projectId, observationId2, patch) {
5685
+ const database = await getDb();
5686
+ const id = makeOramaObservationId(projectId, observationId2);
5687
+ const existing = getByID(database, id);
5688
+ if (!existing) return false;
5689
+ const next = { ...existing, ...patch };
5690
+ await update(database, id, next);
5691
+ rememberObservationDoc(next);
5692
+ return true;
5693
+ }
5597
5694
  async function removeObservation(oramaId) {
5598
5695
  const database = await getDb();
5599
5696
  await remove2(database, oramaId);
@@ -5747,7 +5844,7 @@ async function searchObservations(options) {
5747
5844
  if (!projectIds) return true;
5748
5845
  const doc = hit.document;
5749
5846
  return projectIds.includes(doc.projectId);
5750
- }).filter((hit) => {
5847
+ }).filter((hit) => canReadObservation(hit.document, options.reader)).filter((hit) => {
5751
5848
  if (statusFilter === "all") return true;
5752
5849
  const doc = hit.document;
5753
5850
  return (doc.status || "active") === statusFilter;
@@ -5778,6 +5875,8 @@ async function searchObservations(options) {
5778
5875
  source: doc.source || "agent",
5779
5876
  sourceDetail: doc.sourceDetail || void 0,
5780
5877
  valueCategory: doc.valueCategory || void 0,
5878
+ admissionState: doc.admissionState || void 0,
5879
+ visibility: doc.visibility || void 0,
5781
5880
  entityName: doc.entityName || void 0,
5782
5881
  documentType: doc.documentType || "observation",
5783
5882
  knowledgeLayer: doc.knowledgeLayer || "project-truth",
@@ -5821,6 +5920,12 @@ async function searchObservations(options) {
5821
5920
  score: isCommandStyleEntry(entry.title) ? entry.score * 0.3 : entry.score
5822
5921
  }));
5823
5922
  }
5923
+ if (hasQuery) {
5924
+ const qualifiedEntries = intermediate.filter(
5925
+ (entry) => entry.admissionState !== "candidate" && entry.admissionState !== "ephemeral"
5926
+ );
5927
+ if (qualifiedEntries.length > 0) intermediate = qualifiedEntries;
5928
+ }
5824
5929
  if (intentResult?.preferChronological) {
5825
5930
  intermediate.sort((a, b) => new Date(b.rawTime).getTime() - new Date(a.rawTime).getTime());
5826
5931
  } else {
@@ -5964,7 +6069,8 @@ async function searchObservations(options) {
5964
6069
  }
5965
6070
  let entries = intermediate.map(({ rawTime: _, _isCommandLog: _c, ...rest }) => rest);
5966
6071
  for (const hit of results.hits) {
5967
- rememberObservationDoc(hit.document);
6072
+ const doc = hit.document;
6073
+ if (canReadObservation(doc, options.reader)) rememberObservationDoc(doc);
5968
6074
  }
5969
6075
  if (hasQuery && originalQuery) {
5970
6076
  const queryLower = originalQuery.toLowerCase();
@@ -6008,7 +6114,8 @@ async function searchObservations(options) {
6008
6114
  entries = applyTokenBudget(entries, options.maxTokens);
6009
6115
  }
6010
6116
  if (options.trackAccess !== false) {
6011
- const hitDocs = results.hits.map((h) => ({ id: h.id, doc: h.document }));
6117
+ const returnedKeys = new Set(entries.map((entry) => makeEntryKey(entry.projectId, entry.id)));
6118
+ const hitDocs = results.hits.map((hit) => ({ id: hit.id, doc: hit.document })).filter(({ doc }) => returnedKeys.has(makeEntryKey(doc.projectId, doc.observationId)));
6012
6119
  recordAccessBatch(hitDocs).catch(() => {
6013
6120
  });
6014
6121
  }
@@ -6040,11 +6147,11 @@ async function getObservationsByIds(ids, projectId) {
6040
6147
  }
6041
6148
  return results;
6042
6149
  }
6043
- async function getTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3) {
6150
+ async function getTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3, reader) {
6044
6151
  const { withFreshIndex: withFreshIndex2 } = await Promise.resolve().then(() => (init_freshness(), freshness_exports));
6045
6152
  const { getAllObservations: getAllObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
6046
6153
  const rawObs = await withFreshIndex2(() => getAllObservations2());
6047
- const allObs = projectId ? rawObs.filter((o) => o.projectId === projectId) : rawObs;
6154
+ const allObs = (projectId ? rawObs.filter((o) => o.projectId === projectId) : rawObs).filter((observation) => canReadObservation(observation, reader));
6048
6155
  const sorted = allObs.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
6049
6156
  const anchorIndex = sorted.findIndex((o) => o.id === anchorId);
6050
6157
  if (anchorIndex === -1) {
@@ -6061,7 +6168,9 @@ async function getTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3)
6061
6168
  tokens: obs.tokens,
6062
6169
  source: obs.source || void 0,
6063
6170
  sourceDetail: obs.sourceDetail || void 0,
6064
- valueCategory: obs.valueCategory || void 0
6171
+ valueCategory: obs.valueCategory || void 0,
6172
+ admissionState: obs.admissionState || void 0,
6173
+ visibility: obs.visibility || void 0
6065
6174
  };
6066
6175
  };
6067
6176
  const before = sorted.slice(Math.max(0, anchorIndex - depthBefore), anchorIndex).map(toIndexEntry);
@@ -6129,6 +6238,7 @@ var init_orama_store = __esm({
6129
6238
  init_esm_shims();
6130
6239
  init_types();
6131
6240
  init_mini_skills();
6241
+ init_visibility();
6132
6242
  init_provider();
6133
6243
  init_project_affinity();
6134
6244
  init_intent_detector();
@@ -6185,6 +6295,10 @@ function obsToRow(obs) {
6185
6295
  relatedEntities: obs.relatedEntities ? JSON.stringify(obs.relatedEntities) : null,
6186
6296
  sourceDetail: obs.sourceDetail ?? null,
6187
6297
  valueCategory: obs.valueCategory ?? null,
6298
+ admissionState: obs.admissionState ?? null,
6299
+ admissionReason: obs.admissionReason ?? null,
6300
+ visibility: obs.visibility ?? null,
6301
+ sharedWithAgentIds: obs.sharedWithAgentIds ? JSON.stringify(obs.sharedWithAgentIds) : null,
6188
6302
  createdByAgentId: obs.createdByAgentId ?? null,
6189
6303
  writeGeneration: obs.writeGeneration ?? 0
6190
6304
  };
@@ -6215,6 +6329,10 @@ function rowToObs(row) {
6215
6329
  ...row.relatedEntities ? { relatedEntities: safeJsonParse3(row.relatedEntities, []) } : {},
6216
6330
  ...row.sourceDetail ? { sourceDetail: row.sourceDetail } : {},
6217
6331
  ...row.valueCategory ? { valueCategory: row.valueCategory } : {},
6332
+ ...row.admissionState ? { admissionState: row.admissionState } : {},
6333
+ ...row.admissionReason ? { admissionReason: row.admissionReason } : {},
6334
+ ...row.visibility ? { visibility: row.visibility } : {},
6335
+ ...row.sharedWithAgentIds ? { sharedWithAgentIds: safeJsonParse3(row.sharedWithAgentIds, []) } : {},
6218
6336
  ...row.createdByAgentId ? { createdByAgentId: row.createdByAgentId } : {},
6219
6337
  ...row.writeGeneration ? { writeGeneration: row.writeGeneration } : {}
6220
6338
  };
@@ -6266,12 +6384,14 @@ var init_sqlite_store = __esm({
6266
6384
  (id, entityName, type, title, narrative, facts, filesModified, concepts, tokens,
6267
6385
  createdAt, updatedAt, projectId, hasCausalLanguage, topicKey, revisionCount,
6268
6386
  sessionId, status, progress, source, commitHash, relatedCommits, relatedEntities,
6269
- sourceDetail, valueCategory, createdByAgentId, writeGeneration)
6387
+ sourceDetail, valueCategory, admissionState, admissionReason, visibility, sharedWithAgentIds,
6388
+ createdByAgentId, writeGeneration)
6270
6389
  VALUES
6271
6390
  (@id, @entityName, @type, @title, @narrative, @facts, @filesModified, @concepts, @tokens,
6272
6391
  @createdAt, @updatedAt, @projectId, @hasCausalLanguage, @topicKey, @revisionCount,
6273
6392
  @sessionId, @status, @progress, @source, @commitHash, @relatedCommits, @relatedEntities,
6274
- @sourceDetail, @valueCategory, @createdByAgentId, @writeGeneration)
6393
+ @sourceDetail, @valueCategory, @admissionState, @admissionReason, @visibility, @sharedWithAgentIds,
6394
+ @createdByAgentId, @writeGeneration)
6275
6395
  `);
6276
6396
  this.stmtUpdate = this.stmtInsert;
6277
6397
  this.stmtSetStatus = this.db.prepare(`UPDATE observations SET status = ? WHERE id = ?`);
@@ -6823,6 +6943,7 @@ var init_maintenance_jobs = __esm({
6823
6943
  "retention-archive",
6824
6944
  "consolidation",
6825
6945
  "codegraph-refresh",
6946
+ "observation-qualify",
6826
6947
  "claim-derive",
6827
6948
  "claim-requalification",
6828
6949
  "knowledge-compile",
@@ -7181,7 +7302,8 @@ __export(lifecycle_exports, {
7181
7302
  enqueueClaimDerivation: () => enqueueClaimDerivation,
7182
7303
  enqueueClaimRequalification: () => enqueueClaimRequalification,
7183
7304
  enqueueCodegraphRefresh: () => enqueueCodegraphRefresh,
7184
- enqueueKnowledgeFollowups: () => enqueueKnowledgeFollowups
7305
+ enqueueKnowledgeFollowups: () => enqueueKnowledgeFollowups,
7306
+ enqueueObservationQualification: () => enqueueObservationQualification
7185
7307
  });
7186
7308
  function queueFor(input) {
7187
7309
  return input.queue ?? new MaintenanceJobStore(input.dataDir);
@@ -7216,6 +7338,14 @@ function enqueueClaimDerivation(input) {
7216
7338
  payload: { observationId: input.observationId }
7217
7339
  });
7218
7340
  }
7341
+ function enqueueObservationQualification(input) {
7342
+ queueFor(input).enqueue({
7343
+ projectId: input.projectId,
7344
+ kind: "observation-qualify",
7345
+ dedupeKey: "observation-qualify",
7346
+ payload: { source: input.source, limit: 100 }
7347
+ });
7348
+ }
7219
7349
  function enqueueKnowledgeFollowups(input) {
7220
7350
  const queue = queueFor(input);
7221
7351
  const payload = {
@@ -8080,6 +8210,7 @@ __export(observations_exports, {
8080
8210
  resolveObservations: () => resolveObservations,
8081
8211
  storeObservation: () => storeObservation,
8082
8212
  suggestTopicKey: () => suggestTopicKey,
8213
+ updateObservationAdmission: () => updateObservationAdmission,
8083
8214
  withFreshObservations: () => withFreshObservations
8084
8215
  });
8085
8216
  function logEmbeddingFailureOnce(key, message) {
@@ -8146,6 +8277,18 @@ function queueClaimDerivation(observation) {
8146
8277
  } catch {
8147
8278
  }
8148
8279
  }
8280
+ function queueObservationQualification(observation) {
8281
+ const dataDir = projectDir;
8282
+ if (!dataDir || observation.admissionState !== "candidate") return;
8283
+ try {
8284
+ enqueueObservationQualification({
8285
+ dataDir,
8286
+ projectId: observation.projectId,
8287
+ source: "automatic-capture:" + observation.id
8288
+ });
8289
+ } catch {
8290
+ }
8291
+ }
8149
8292
  function isVectorCompatibleWithCurrentIndex(embedding) {
8150
8293
  if (!embedding) return false;
8151
8294
  const vectorDimensions = getVectorDimensions();
@@ -8201,6 +8344,9 @@ async function storeObservation(input) {
8201
8344
  (o) => o.topicKey === input.topicKey && o.projectId === input.projectId
8202
8345
  );
8203
8346
  if (existing) {
8347
+ if (input.visibilityReader && !canManageObservation(existing, input.visibilityReader)) {
8348
+ throw new Error("Cannot update a memory outside this session's write scope.");
8349
+ }
8204
8350
  return { observation: await upsertObservation(existing, input, now3), upserted: true };
8205
8351
  }
8206
8352
  }
@@ -8237,6 +8383,9 @@ async function storeObservation(input) {
8237
8383
  if (input.topicKey) {
8238
8384
  const diskExisting = await tx.findByTopicKey(input.projectId, input.topicKey);
8239
8385
  if (diskExisting) {
8386
+ if (input.visibilityReader && !canManageObservation(diskExisting, input.visibilityReader)) {
8387
+ throw new Error("Cannot update a memory outside this session's write scope.");
8388
+ }
8240
8389
  upsertedInsideLock = true;
8241
8390
  observation = diskExisting;
8242
8391
  return;
@@ -8267,6 +8416,10 @@ async function storeObservation(input) {
8267
8416
  relatedEntities: input.relatedEntities,
8268
8417
  sourceDetail: input.sourceDetail,
8269
8418
  valueCategory: input.valueCategory,
8419
+ admissionState: input.admissionState,
8420
+ admissionReason: input.admissionReason ? sanitizeCredentials(input.admissionReason) : void 0,
8421
+ visibility: input.visibility ?? "project",
8422
+ sharedWithAgentIds: input.sharedWithAgentIds,
8270
8423
  createdByAgentId: input.createdByAgentId,
8271
8424
  // Predict the generation that atomic() will commit after this callback.
8272
8425
  // bumpGeneration() runs after fn(tx) returns, incrementing by 1.
@@ -8313,6 +8466,10 @@ async function storeObservation(input) {
8313
8466
  relatedEntities: input.relatedEntities,
8314
8467
  sourceDetail: input.sourceDetail,
8315
8468
  valueCategory: input.valueCategory,
8469
+ admissionState: input.admissionState,
8470
+ admissionReason: input.admissionReason ? sanitizeCredentials(input.admissionReason) : void 0,
8471
+ visibility: input.visibility ?? "project",
8472
+ sharedWithAgentIds: input.sharedWithAgentIds,
8316
8473
  createdByAgentId: input.createdByAgentId,
8317
8474
  writeGeneration: 0
8318
8475
  };
@@ -8336,7 +8493,12 @@ async function storeObservation(input) {
8336
8493
  status: "active",
8337
8494
  source: input.source ?? "agent",
8338
8495
  sourceDetail: input.sourceDetail ?? "",
8339
- valueCategory: input.valueCategory ?? ""
8496
+ valueCategory: input.valueCategory ?? "",
8497
+ admissionState: input.admissionState ?? "",
8498
+ admissionReason: input.admissionReason ? sanitizeCredentials(input.admissionReason) : "",
8499
+ visibility: input.visibility ?? "project",
8500
+ createdByAgentId: input.createdByAgentId ?? "",
8501
+ sharedWithAgentIds: JSON.stringify(input.sharedWithAgentIds ?? [])
8340
8502
  };
8341
8503
  await insertObservation(doc);
8342
8504
  };
@@ -8345,7 +8507,10 @@ async function storeObservation(input) {
8345
8507
  return { observation: await upsertObservation(observation, input, now3), upserted: true };
8346
8508
  }
8347
8509
  await bindObservationCodeRefsBestEffort(observation);
8348
- queueClaimDerivation(observation);
8510
+ queueObservationQualification(observation);
8511
+ if (resolveObservationVisibility(observation) === "project") {
8512
+ queueClaimDerivation(observation);
8513
+ }
8349
8514
  const obsId = observation.id;
8350
8515
  vectorMissingIds.add(obsId);
8351
8516
  const searchableText = [input.title, input.narrative, ...input.facts ?? []].join(" ");
@@ -8418,6 +8583,10 @@ async function upsertObservation(existing, input, now3) {
8418
8583
  if (input.progress) existing.progress = input.progress;
8419
8584
  if (input.sourceDetail !== void 0) existing.sourceDetail = input.sourceDetail;
8420
8585
  if (input.valueCategory !== void 0) existing.valueCategory = input.valueCategory;
8586
+ if (input.admissionState !== void 0) existing.admissionState = input.admissionState;
8587
+ if (input.admissionReason !== void 0) existing.admissionReason = sanitizeCredentials(input.admissionReason);
8588
+ if (input.visibility !== void 0) existing.visibility = input.visibility;
8589
+ if (input.sharedWithAgentIds !== void 0) existing.sharedWithAgentIds = input.sharedWithAgentIds;
8421
8590
  const doc = {
8422
8591
  id: makeOramaObservationId(existing.projectId, existing.id),
8423
8592
  observationId: existing.id,
@@ -8436,7 +8605,12 @@ async function upsertObservation(existing, input, now3) {
8436
8605
  status: "active",
8437
8606
  source: existing.source ?? "agent",
8438
8607
  sourceDetail: existing.sourceDetail ?? "",
8439
- valueCategory: existing.valueCategory ?? ""
8608
+ valueCategory: existing.valueCategory ?? "",
8609
+ admissionState: existing.admissionState ?? "",
8610
+ admissionReason: existing.admissionReason ?? "",
8611
+ visibility: existing.visibility ?? "project",
8612
+ createdByAgentId: existing.createdByAgentId ?? "",
8613
+ sharedWithAgentIds: JSON.stringify(existing.sharedWithAgentIds ?? [])
8440
8614
  };
8441
8615
  const oramaId = makeOramaObservationId(existing.projectId, existing.id);
8442
8616
  try {
@@ -8459,7 +8633,10 @@ async function upsertObservation(existing, input, now3) {
8459
8633
  await store.update(existing);
8460
8634
  }
8461
8635
  await bindObservationCodeRefsBestEffort(existing);
8462
- queueClaimDerivation(existing);
8636
+ queueObservationQualification(existing);
8637
+ if (resolveObservationVisibility(existing) === "project") {
8638
+ queueClaimDerivation(existing);
8639
+ }
8463
8640
  const searchableText = [input.title, input.narrative, ...input.facts ?? []].join(" ");
8464
8641
  const obsId = existing.id;
8465
8642
  vectorMissingIds.add(obsId);
@@ -8498,6 +8675,56 @@ async function upsertObservation(existing, input, now3) {
8498
8675
  function getObservation(id, projectId) {
8499
8676
  return observations.find((o) => o.id === id && (projectId ? o.projectId === projectId : true));
8500
8677
  }
8678
+ async function updateObservationAdmission(input) {
8679
+ await ensureFreshObservations();
8680
+ const cached = observations.find(
8681
+ (observation) => observation.id === input.observationId && (!input.projectId || observation.projectId === input.projectId)
8682
+ );
8683
+ if (!cached) return void 0;
8684
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
8685
+ const admissionReason = sanitizeCredentials(input.admissionReason).slice(0, 240);
8686
+ const apply = (current) => {
8687
+ if (input.projectId && current.projectId !== input.projectId) return void 0;
8688
+ if (input.expectedState && current.admissionState !== input.expectedState) return void 0;
8689
+ return {
8690
+ ...current,
8691
+ admissionState: input.admissionState,
8692
+ admissionReason,
8693
+ ...input.visibility !== void 0 ? { visibility: input.visibility } : {},
8694
+ updatedAt: now3
8695
+ };
8696
+ };
8697
+ let updated;
8698
+ if (projectDir) {
8699
+ const store = getObservationStore();
8700
+ await store.atomic(async (tx) => {
8701
+ const current = await tx.getById(input.observationId);
8702
+ if (!current) return;
8703
+ const next = apply(current);
8704
+ if (!next) return;
8705
+ await tx.update(next);
8706
+ updated = next;
8707
+ });
8708
+ } else {
8709
+ updated = apply(cached);
8710
+ }
8711
+ if (!updated) return void 0;
8712
+ observations = observations.map(
8713
+ (observation) => observation.id === updated.id && observation.projectId === updated.projectId ? updated : observation
8714
+ );
8715
+ try {
8716
+ await updateObservationMetadata(updated.projectId, updated.id, {
8717
+ admissionState: updated.admissionState,
8718
+ admissionReason: updated.admissionReason,
8719
+ ...updated.visibility !== void 0 ? { visibility: updated.visibility } : {}
8720
+ });
8721
+ } catch {
8722
+ }
8723
+ if (updated.admissionState === "qualified" && resolveObservationVisibility(updated) === "project") {
8724
+ queueClaimDerivation(updated);
8725
+ }
8726
+ return updated;
8727
+ }
8501
8728
  async function resolveObservations(ids, status = "resolved") {
8502
8729
  const resolved = [];
8503
8730
  const notFound = [];
@@ -8537,7 +8764,12 @@ async function resolveObservations(ids, status = "resolved") {
8537
8764
  status,
8538
8765
  source: obs.source ?? "agent",
8539
8766
  sourceDetail: obs.sourceDetail ?? "",
8540
- valueCategory: obs.valueCategory ?? ""
8767
+ valueCategory: obs.valueCategory ?? "",
8768
+ admissionState: obs.admissionState ?? "",
8769
+ admissionReason: obs.admissionReason ?? "",
8770
+ visibility: obs.visibility ?? "project",
8771
+ createdByAgentId: obs.createdByAgentId ?? "",
8772
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? [])
8541
8773
  };
8542
8774
  await insertObservation(doc);
8543
8775
  const obsId = obs.id;
@@ -8667,6 +8899,11 @@ async function reindexObservations() {
8667
8899
  source: obs.source ?? "agent",
8668
8900
  sourceDetail: obs.sourceDetail ?? "",
8669
8901
  valueCategory: obs.valueCategory ?? "",
8902
+ admissionState: obs.admissionState ?? "",
8903
+ admissionReason: obs.admissionReason ?? "",
8904
+ visibility: obs.visibility ?? "project",
8905
+ createdByAgentId: obs.createdByAgentId ?? "",
8906
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
8670
8907
  ...compatibleEmbedding ? { embedding: compatibleEmbedding } : {}
8671
8908
  };
8672
8909
  await insertObservation(doc);
@@ -8739,6 +8976,7 @@ async function probeSearchIndex(projectId) {
8739
8976
  await searchObservations({
8740
8977
  query: "semantic memory retrieval status",
8741
8978
  projectId,
8979
+ reader: { projectId },
8742
8980
  limit: 1,
8743
8981
  status: "all",
8744
8982
  trackAccess: false
@@ -8804,6 +9042,11 @@ async function backfillVectorEmbeddings(options = {}) {
8804
9042
  source: obs.source ?? "agent",
8805
9043
  sourceDetail: obs.sourceDetail ?? "",
8806
9044
  valueCategory: obs.valueCategory ?? "",
9045
+ admissionState: obs.admissionState ?? "",
9046
+ admissionReason: obs.admissionReason ?? "",
9047
+ visibility: obs.visibility ?? "project",
9048
+ createdByAgentId: obs.createdByAgentId ?? "",
9049
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
8807
9050
  embedding
8808
9051
  };
8809
9052
  await insertObservation(doc);
@@ -8845,6 +9088,7 @@ var init_observations = __esm({
8845
9088
  init_provider();
8846
9089
  init_secret_filter();
8847
9090
  init_lifecycle();
9091
+ init_visibility();
8848
9092
  observations = [];
8849
9093
  nextId = 1;
8850
9094
  projectDir = null;
@@ -9124,8 +9368,9 @@ function resolveSourceDetail(sourceDetail, source) {
9124
9368
  return void 0;
9125
9369
  }
9126
9370
  function classifyLayer(fields) {
9127
- const { valueCategory } = fields;
9371
+ const { valueCategory, admissionState } = fields;
9128
9372
  const sd = resolveSourceDetail(fields.sourceDetail, fields.source);
9373
+ if (admissionState === "candidate" || admissionState === "ephemeral") return "L1";
9129
9374
  if (valueCategory === "core") return "L2";
9130
9375
  if (sd === "hook") return "L1";
9131
9376
  if (sd === "git-ingest") return "L3";
@@ -9183,6 +9428,7 @@ function getEffectiveRetentionDays(doc) {
9183
9428
  }
9184
9429
  function isImmune(doc) {
9185
9430
  if (doc.type === "probe") return false;
9431
+ if (doc.admissionState === "candidate" || doc.admissionState === "ephemeral") return false;
9186
9432
  if (doc.valueCategory === "core") return true;
9187
9433
  const importance = getImportanceLevel(doc);
9188
9434
  if (importance === "critical") return true;
@@ -9192,6 +9438,7 @@ function isImmune(doc) {
9192
9438
  }
9193
9439
  function getImmunityReason(doc) {
9194
9440
  if (doc.type === "probe") return null;
9441
+ if (doc.admissionState === "candidate" || doc.admissionState === "ephemeral") return null;
9195
9442
  if (doc.valueCategory === "core") return "core valueCategory (formation-classified)";
9196
9443
  const importance = getImportanceLevel(doc);
9197
9444
  if (importance === "critical") return "critical importance";
@@ -9324,7 +9571,9 @@ function toRetentionDocument(obs, accessMap) {
9324
9571
  status: obs.status ?? "active",
9325
9572
  source: obs.source ?? "agent",
9326
9573
  sourceDetail: obs.sourceDetail ?? "",
9327
- valueCategory: obs.valueCategory ?? ""
9574
+ valueCategory: obs.valueCategory ?? "",
9575
+ admissionState: obs.admissionState ?? "",
9576
+ admissionReason: obs.admissionReason ?? ""
9328
9577
  };
9329
9578
  }
9330
9579
  async function archiveExpiredBatch(_projectDir, options) {
@@ -9338,7 +9587,7 @@ async function archiveExpiredBatch(_projectDir, options) {
9338
9587
  });
9339
9588
  const hasMore = page.length > limit;
9340
9589
  const scanned = hasMore ? page.slice(0, limit) : page;
9341
- const candidateIds = scanned.filter((observation) => getRetentionZone(
9590
+ const candidateIds = scanned.filter((observation) => !options.reader || canManageObservation(observation, options.reader)).filter((observation) => getRetentionZone(
9342
9591
  toRetentionDocument(observation, options.accessMap),
9343
9592
  options.referenceTime
9344
9593
  ) === "archive-candidate").map((observation) => observation.id);
@@ -9351,7 +9600,7 @@ async function archiveExpiredBatch(_projectDir, options) {
9351
9600
  const nextCursor = hasMore && scanned.length > 0 ? scanned[scanned.length - 1].id : void 0;
9352
9601
  return nextCursor === void 0 ? { archived, scanned: scanned.length } : { archived, scanned: scanned.length, nextCursor };
9353
9602
  }
9354
- async function archiveExpired(projectDir2, referenceTime, accessMap, projectId) {
9603
+ async function archiveExpired(projectDir2, referenceTime, accessMap, projectId, reader) {
9355
9604
  const store = getObservationStore();
9356
9605
  if (projectId) {
9357
9606
  let afterId;
@@ -9361,12 +9610,14 @@ async function archiveExpired(projectDir2, referenceTime, accessMap, projectId)
9361
9610
  projectId,
9362
9611
  afterId,
9363
9612
  referenceTime,
9364
- accessMap
9613
+ accessMap,
9614
+ reader
9365
9615
  });
9366
9616
  archived += batch.archived;
9367
9617
  afterId = batch.nextCursor;
9368
9618
  } while (afterId !== void 0);
9369
- const remaining = await store.countByProject(projectId, { status: "active" });
9619
+ const remainingObservations = await store.loadByProject(projectId, { status: "active" });
9620
+ const remaining = reader ? remainingObservations.filter((observation) => canManageObservation(observation, reader)).length : remainingObservations.length;
9370
9621
  return { archived, remaining };
9371
9622
  }
9372
9623
  return await store.atomic(async (tx) => {
@@ -9396,6 +9647,7 @@ var init_retention = __esm({
9396
9647
  "use strict";
9397
9648
  init_esm_shims();
9398
9649
  init_obs_store();
9650
+ init_visibility();
9399
9651
  RETENTION_DAYS = {
9400
9652
  critical: 365,
9401
9653
  high: 180,
@@ -9431,6 +9683,49 @@ var init_retention = __esm({
9431
9683
  }
9432
9684
  });
9433
9685
 
9686
+ // src/memory/admission.ts
9687
+ var admission_exports = {};
9688
+ __export(admission_exports, {
9689
+ isCandidateObservation: () => isCandidateObservation,
9690
+ isEligibleForAutomaticDelivery: () => isEligibleForAutomaticDelivery,
9691
+ isEligibleForKnowledgePromotion: () => isEligibleForKnowledgePromotion,
9692
+ qualifyCandidateFromCurrentCode: () => qualifyCandidateFromCurrentCode
9693
+ });
9694
+ function isEligibleForAutomaticDelivery(observation) {
9695
+ return observation.admissionState !== "ephemeral" && observation.admissionState !== "candidate";
9696
+ }
9697
+ function isCandidateObservation(observation) {
9698
+ return observation.admissionState === "candidate";
9699
+ }
9700
+ function isEligibleForKnowledgePromotion(observation) {
9701
+ return isEligibleForAutomaticDelivery(observation) && observation.valueCategory !== "ephemeral" && resolveObservationVisibility(observation) === "project";
9702
+ }
9703
+ function qualifyCandidateFromCurrentCode(input) {
9704
+ if (!isCandidateObservation(input.observation)) return void 0;
9705
+ if (input.observation.valueCategory === "ephemeral") return void 0;
9706
+ if (input.currentCodeReferenceCount <= 0) return void 0;
9707
+ const typeLabel = DURABLE_AUTOMATIC_TYPES.has(input.observation.type) ? "high-value automatic record" : "automatic record";
9708
+ return {
9709
+ admissionState: "qualified",
9710
+ admissionReason: `${typeLabel} qualified against ${input.currentCodeReferenceCount} current Code Memory reference(s)`
9711
+ };
9712
+ }
9713
+ var DURABLE_AUTOMATIC_TYPES;
9714
+ var init_admission = __esm({
9715
+ "src/memory/admission.ts"() {
9716
+ "use strict";
9717
+ init_esm_shims();
9718
+ init_visibility();
9719
+ DURABLE_AUTOMATIC_TYPES = /* @__PURE__ */ new Set([
9720
+ "decision",
9721
+ "gotcha",
9722
+ "problem-solution",
9723
+ "trade-off",
9724
+ "why-it-exists"
9725
+ ]);
9726
+ }
9727
+ });
9728
+
9434
9729
  // src/workspace/workflow-sync.ts
9435
9730
  import matter9 from "gray-matter";
9436
9731
  var WorkflowSyncer;
@@ -10831,6 +11126,7 @@ var init_isolated_maintenance = __esm({
10831
11126
  "retention-archive",
10832
11127
  "consolidation",
10833
11128
  "codegraph-refresh",
11129
+ "observation-qualify",
10834
11130
  "claim-derive",
10835
11131
  "claim-requalification",
10836
11132
  "knowledge-compile",
@@ -11266,9 +11562,10 @@ async function loadConsolidationPage(projectId, options) {
11266
11562
  return hasMore && observations2.length > 0 ? { observations: observations2, nextCursor: observations2[observations2.length - 1].id } : { observations: observations2 };
11267
11563
  }
11268
11564
  function findClusters(observations2, threshold) {
11269
- if (observations2.length < MIN_CLUSTER_SIZE) return [];
11565
+ const eligible = observations2.filter(isEligibleForAutomaticDelivery).filter((observation) => resolveObservationVisibility(observation) === "project");
11566
+ if (eligible.length < MIN_CLUSTER_SIZE) return [];
11270
11567
  const groups = /* @__PURE__ */ new Map();
11271
- for (const obs of observations2) {
11568
+ for (const obs of eligible) {
11272
11569
  const key = `${obs.entityName}::${obs.type}`;
11273
11570
  const group = groups.get(key) ?? [];
11274
11571
  group.push(obs);
@@ -11405,6 +11702,8 @@ var init_consolidation = __esm({
11405
11702
  "use strict";
11406
11703
  init_esm_shims();
11407
11704
  init_obs_store();
11705
+ init_admission();
11706
+ init_visibility();
11408
11707
  DEFAULT_SIMILARITY_THRESHOLD = 0.45;
11409
11708
  HIGH_VALUE_SIMILARITY_THRESHOLD = 0.85;
11410
11709
  HIGH_VALUE_TYPES = /* @__PURE__ */ new Set(["gotcha", "decision", "trade-off", "reasoning", "problem-solution"]);
@@ -14423,6 +14722,15 @@ function claimDerivationCursor(payload) {
14423
14722
  const value = payload.cursor;
14424
14723
  return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
14425
14724
  }
14725
+ function observationQualificationBatchSize(payload) {
14726
+ const value = payload.limit;
14727
+ if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE;
14728
+ return Math.min(500, Math.max(1, Math.floor(value)));
14729
+ }
14730
+ function observationQualificationCursor(payload) {
14731
+ const value = payload.cursor;
14732
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
14733
+ }
14426
14734
  function observationId(payload) {
14427
14735
  const value = payload.observationId;
14428
14736
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
@@ -14601,6 +14909,71 @@ function createProjectMaintenanceHandler(projectId, projectDir2, projectRoot, op
14601
14909
  ...snapshot?.id ? { snapshotId: snapshot.id } : {},
14602
14910
  ...options.maintenanceQueue ? { queue: options.maintenanceQueue } : {}
14603
14911
  });
14912
+ enqueueObservationQualification({
14913
+ projectId,
14914
+ dataDir: projectDir2,
14915
+ source: "codegraph-refresh",
14916
+ ...options.maintenanceQueue ? { queue: options.maintenanceQueue } : {}
14917
+ });
14918
+ return { action: "complete" };
14919
+ }
14920
+ if (job.kind === "observation-qualify") {
14921
+ const [
14922
+ { CodeGraphStore: CodeGraphStore2 },
14923
+ { bindObservationToCode: bindObservationToCode2 },
14924
+ { getObservationStore: getObservationStore2 },
14925
+ { qualifyCandidateFromCurrentCode: qualifyCandidateFromCurrentCode2 },
14926
+ { updateObservationAdmission: updateObservationAdmission2 }
14927
+ ] = await Promise.all([
14928
+ Promise.resolve().then(() => (init_store(), store_exports)),
14929
+ Promise.resolve().then(() => (init_binder(), binder_exports)),
14930
+ Promise.resolve().then(() => (init_obs_store(), obs_store_exports)),
14931
+ Promise.resolve().then(() => (init_admission(), admission_exports)),
14932
+ Promise.resolve().then(() => (init_observations(), observations_exports))
14933
+ ]);
14934
+ const codeStore = new CodeGraphStore2();
14935
+ await codeStore.init(projectDir2);
14936
+ const limit = observationQualificationBatchSize(job.payload);
14937
+ const page = await getObservationStore2().loadByProject(projectId, {
14938
+ status: "active",
14939
+ afterId: observationQualificationCursor(job.payload),
14940
+ limit: limit + 1
14941
+ });
14942
+ const hasMore = page.length > limit;
14943
+ const observations2 = hasMore ? page.slice(0, limit) : page;
14944
+ const indexedAtMs = Date.parse(codeStore.status(projectId).indexedAt ?? "");
14945
+ for (const observation of observations2) {
14946
+ if (observation.admissionState !== "candidate") continue;
14947
+ if (!Number.isFinite(indexedAtMs) || indexedAtMs < Date.parse(observation.createdAt)) continue;
14948
+ await bindObservationToCode2(codeStore, observation);
14949
+ const currentCodeReferenceCount = codeStore.listObservationRefs(projectId, observation.id).filter((reference) => reference.status === "current").length;
14950
+ const qualification = qualifyCandidateFromCurrentCode2({
14951
+ observation,
14952
+ currentCodeReferenceCount
14953
+ });
14954
+ if (!qualification) continue;
14955
+ await updateObservationAdmission2({
14956
+ observationId: observation.id,
14957
+ projectId,
14958
+ expectedState: "candidate",
14959
+ // Automatic capture earns project visibility only after a current
14960
+ // Code Memory link exists. Explicit/private records retain scope.
14961
+ ...observation.sourceDetail === "hook" ? { visibility: "project" } : {},
14962
+ ...qualification
14963
+ });
14964
+ }
14965
+ if (hasMore && observations2.length > 0) {
14966
+ return {
14967
+ action: "reschedule",
14968
+ delayMs: 0,
14969
+ resetAttempts: true,
14970
+ payload: {
14971
+ ...job.payload,
14972
+ cursor: observations2[observations2.length - 1].id,
14973
+ limit
14974
+ }
14975
+ };
14976
+ }
14604
14977
  return { action: "complete" };
14605
14978
  }
14606
14979
  if (job.kind === "claim-derive") {
@@ -14766,7 +15139,7 @@ function createProjectMaintenanceDispatcher(projectId, projectDir2, projectRoot,
14766
15139
  return isolatedRunner({ job, projectRoot, dataDir: projectDir2 });
14767
15140
  };
14768
15141
  }
14769
- 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;
15142
+ 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;
14770
15143
  var init_project_maintenance = __esm({
14771
15144
  "src/runtime/project-maintenance.ts"() {
14772
15145
  "use strict";
@@ -14778,6 +15151,7 @@ var init_project_maintenance = __esm({
14778
15151
  DEFAULT_CONSOLIDATION_BATCH_SIZE = 200;
14779
15152
  DEFAULT_CODEGRAPH_MAX_FILES = 5e3;
14780
15153
  DEFAULT_CLAIM_DERIVATION_BATCH_SIZE = 100;
15154
+ DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE = 100;
14781
15155
  VECTOR_RETRY_DELAY_MS = 5e3;
14782
15156
  DEDUP_PER_PAIR_TIMEOUT_MS = 5e3;
14783
15157
  }
@@ -14968,7 +15342,7 @@ async function startSession(projectDir2, projectId, opts) {
14968
15342
  status: "active",
14969
15343
  agent: opts?.agent
14970
15344
  };
14971
- const previousContext = await getSessionContext(projectDir2, projectId);
15345
+ const previousContext = await getSessionContext(projectDir2, projectId, 3, opts?.reader);
14972
15346
  const sessionStore = getSessionStore();
14973
15347
  const aliasSet = await resolveProjectIds(projectId);
14974
15348
  await sessionStore.atomicRolloverInsert(session, [...aliasSet], now3);
@@ -14986,23 +15360,27 @@ async function endSession(projectDir2, sessionId, summary) {
14986
15360
  await sessionStore.update(session);
14987
15361
  return session;
14988
15362
  }
14989
- async function getSessionContext(projectDir2, projectId, limit = 3) {
15363
+ async function getSessionContext(projectDir2, projectId, limit = 3, reader) {
14990
15364
  const aliasSet = await resolveProjectIds(projectId);
14991
15365
  const [sessions, allObs] = await Promise.all([
14992
15366
  loadAliasSessions(aliasSet),
14993
15367
  loadAliasActiveObservations(aliasSet)
14994
15368
  ]);
15369
+ const readableObs = reader ? allObs.filter((observation) => {
15370
+ const observationReader = reader.projectId && aliasSet.has(observation.projectId) ? { ...reader, projectId: observation.projectId } : reader;
15371
+ return canReadObservation(observation, observationReader);
15372
+ }) : allObs;
14995
15373
  const isNoisySummary = (summary) => {
14996
15374
  if (!summary) return false;
14997
15375
  return NOISE_PATTERNS2.some((p) => p.test(summary)) || SYSTEM_SELF_PATTERNS.some((p) => p.test(summary));
14998
15376
  };
14999
15377
  const projectSessions = sessions.filter((session) => session.status === "completed").filter((session) => !isNoisySummary(session.summary)).sort((a, b) => new Date(b.endedAt || b.startedAt).getTime() - new Date(a.endedAt || a.startedAt).getTime()).slice(0, limit);
15000
- if (projectSessions.length === 0 && allObs.length === 0) {
15378
+ if (projectSessions.length === 0 && readableObs.length === 0) {
15001
15379
  return "";
15002
15380
  }
15003
15381
  const lines = [];
15004
15382
  const projectTokens = tokenizeProjectId(projectId);
15005
- const projectObs = allObs.filter((obs) => !isNoiseObservation(obs) && !isSystemSelfObservation(obs));
15383
+ const projectObs = readableObs.filter((obs) => !isNoiseObservation(obs) && !isSystemSelfObservation(obs));
15006
15384
  const l2Scored = projectObs.filter((obs) => PRIORITY_TYPES.has(obs.type) && classifyLayer(obs) === "L2").map((obs) => ({ obs, score: scoreObservationForSessionContext(obs, projectTokens) })).sort((a, b) => {
15007
15385
  if (b.score !== a.score) return b.score - a.score;
15008
15386
  return new Date(b.obs.createdAt).getTime() - new Date(a.obs.createdAt).getTime();
@@ -15019,16 +15397,16 @@ async function getSessionContext(projectDir2, projectId, limit = 3) {
15019
15397
  return true;
15020
15398
  });
15021
15399
  })() : l2Scored).slice(0, 5).map(({ obs }) => obs);
15022
- 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);
15023
15401
  const l3GitCount = projectObs.filter((obs) => classifyLayer(obs) === "L3").length;
15024
- const totalHookCount = projectObs.filter((obs) => classifyLayer(obs) === "L1").length;
15402
+ const totalHookCount = projectObs.filter((obs) => isEligibleForAutomaticDelivery(obs) && classifyLayer(obs) === "L1").length;
15025
15403
  const activeEntities = [
15026
15404
  ...new Set(l2Obs.map((o) => o.entityName).filter((n) => !!n && n.trim().length > 0))
15027
15405
  ].slice(0, 5);
15028
15406
  const hasL1Content = l1HookObs.length > 0 || l3GitCount > 0;
15029
15407
  if (hasL1Content) {
15030
15408
  let graphNeighbors = [];
15031
- if (activeEntities.length > 0) {
15409
+ if (!reader && activeEntities.length > 0) {
15032
15410
  try {
15033
15411
  const graphMgr = new KnowledgeGraphManager(projectDir2);
15034
15412
  await graphMgr.init();
@@ -15147,12 +15525,14 @@ var init_session = __esm({
15147
15525
  "src/memory/session.ts"() {
15148
15526
  "use strict";
15149
15527
  init_esm_shims();
15528
+ init_admission();
15150
15529
  init_disclosure_policy();
15151
15530
  init_aliases();
15152
15531
  init_obs_store();
15153
15532
  init_session_store();
15154
15533
  init_graph();
15155
15534
  init_secret_filter();
15535
+ init_visibility();
15156
15536
  PRIORITY_TYPES = /* @__PURE__ */ new Set(["gotcha", "decision", "problem-solution", "trade-off", "discovery"]);
15157
15537
  TYPE_EMOJI = {
15158
15538
  "gotcha": "[DISCOVERY]",
@@ -15299,27 +15679,80 @@ function workflowOutput(selection) {
15299
15679
  cautions: selection.cautions.map((caution) => short(caution, 22))
15300
15680
  };
15301
15681
  }
15302
- function appendLine(lines, candidate, maxTokens, omitted, omittedKind) {
15682
+ function appendLine(lines, candidate, maxTokens, omitted, omittedKind, selected, receiptSelection) {
15303
15683
  const next = lines.length ? lines.join("\n") + "\n" + candidate : candidate;
15304
15684
  if (countTextTokens(next) <= maxTokens) {
15305
15685
  lines.push(candidate);
15686
+ if (receiptSelection) selected?.push(receiptSelection);
15306
15687
  return true;
15307
15688
  }
15308
15689
  omitted.push(omittedKind);
15309
15690
  return false;
15310
15691
  }
15692
+ function freshnessForMemory(status) {
15693
+ if (status === "current" || status === "suspect" || status === "stale") return status;
15694
+ return "unknown";
15695
+ }
15696
+ function receiptOmissionKind(raw) {
15697
+ if (raw.includes("task")) return "task";
15698
+ if (raw.includes("fact")) return "current-fact";
15699
+ if (raw.includes("state")) return "code-state";
15700
+ if (raw.includes("semantic")) return "semantic-code";
15701
+ if (raw.includes("start")) return "start-here";
15702
+ if (raw.includes("memory")) return "memory";
15703
+ if (raw.includes("claim")) return "claim";
15704
+ if (raw.includes("knowledge-page")) return "knowledge-page";
15705
+ if (raw.includes("workflow")) return "workflow";
15706
+ if (raw.includes("verification")) return "verification";
15707
+ if (raw.includes("caution")) return "caution";
15708
+ return void 0;
15709
+ }
15710
+ function receiptOmissions(omitted, hiddenCautionMemoryCount) {
15711
+ const counts = /* @__PURE__ */ new Map();
15712
+ for (const raw of omitted) {
15713
+ const kind = receiptOmissionKind(raw);
15714
+ if (!kind || raw.endsWith("-heading")) continue;
15715
+ counts.set(kind, (counts.get(kind) ?? 0) + 1);
15716
+ }
15717
+ const receipt = [...counts.entries()].map(([kind, count2]) => ({
15718
+ kind,
15719
+ reason: "token-budget",
15720
+ count: count2
15721
+ }));
15722
+ if (hiddenCautionMemoryCount > 0) {
15723
+ receipt.push({
15724
+ kind: "caution",
15725
+ reason: "hidden-by-task-lens",
15726
+ count: hiddenCautionMemoryCount
15727
+ });
15728
+ }
15729
+ return receipt;
15730
+ }
15731
+ function scheduledActions(cautions) {
15732
+ return cautions.filter((caution) => caution.kind === "codegraph-refresh-queued").map((caution) => short(caution.message, 28));
15733
+ }
15311
15734
  function renderTaskWorksetPrompt(input) {
15312
15735
  const maxTokens = input.budget?.maxTokens ?? 180;
15313
15736
  const omitted = [];
15737
+ const selected = [];
15314
15738
  const lines = ["Memorix Autopilot Brief"];
15315
15739
  const task = short(input.task || "Continue the current task.", 34);
15316
- appendLine(lines, "Task: " + task, maxTokens, omitted, "task-detail");
15740
+ appendLine(lines, "Task: " + task, maxTokens, omitted, "task-detail", selected, {
15741
+ kind: "task",
15742
+ reason: "current task supplied by the caller",
15743
+ trust: "source-backed"
15744
+ });
15317
15745
  appendLine(lines, "Task lens: " + input.lens, maxTokens, omitted, "lens");
15318
15746
  if (input.cautions.length > 0 || input.cautionMemory.length > 0) {
15319
15747
  appendLine(lines, "", maxTokens, omitted, "caution-heading");
15320
15748
  appendLine(lines, "Cautions", maxTokens, omitted, "caution-heading");
15321
15749
  for (const caution of input.cautions.slice(0, 6)) {
15322
- appendLine(lines, "- " + short(caution.message, 22), maxTokens, omitted, "caution");
15750
+ appendLine(lines, "- " + short(caution.message, 22), maxTokens, omitted, "caution", selected, {
15751
+ kind: "caution",
15752
+ id: "caution:" + caution.kind,
15753
+ reason: "current project caution",
15754
+ trust: "source-backed"
15755
+ });
15323
15756
  }
15324
15757
  for (const memory of input.cautionMemory.slice(0, 3)) {
15325
15758
  const location = memory.path ? memory.path + (memory.symbol ? "#" + memory.symbol : "") : "no current code location";
@@ -15329,7 +15762,15 @@ function renderTaskWorksetPrompt(input) {
15329
15762
  "- #" + memory.id + " " + memory.status + ": " + short(memory.title, 18) + " (" + location + reason + ")",
15330
15763
  maxTokens,
15331
15764
  omitted,
15332
- "caution-memory"
15765
+ "caution-memory",
15766
+ selected,
15767
+ {
15768
+ kind: "memory",
15769
+ id: "memory:" + memory.id,
15770
+ reason: "task-relevant memory requiring source verification",
15771
+ freshness: freshnessForMemory(memory.status),
15772
+ trust: "historical"
15773
+ }
15333
15774
  );
15334
15775
  }
15335
15776
  if (input.hiddenCautionMemoryCount > 0) {
@@ -15340,13 +15781,22 @@ function renderTaskWorksetPrompt(input) {
15340
15781
  appendLine(lines, "", maxTokens, omitted, "facts-heading");
15341
15782
  appendLine(lines, "Current project facts", maxTokens, omitted, "facts-heading");
15342
15783
  for (const fact of input.currentFacts.slice(0, 4)) {
15343
- appendLine(lines, "- " + short(fact, 40), maxTokens, omitted, "current-fact");
15784
+ appendLine(lines, "- " + short(fact, 40), maxTokens, omitted, "current-fact", selected, {
15785
+ kind: "current-fact",
15786
+ reason: "current project state",
15787
+ trust: "source-backed"
15788
+ });
15344
15789
  }
15345
15790
  }
15346
15791
  if (input.codeState) {
15347
15792
  appendLine(lines, "", maxTokens, omitted, "state-heading");
15348
15793
  appendLine(lines, "Project state", maxTokens, omitted, "state-heading");
15349
- appendLine(lines, input.codeState, maxTokens, omitted, "code-state");
15794
+ appendLine(lines, input.codeState, maxTokens, omitted, "code-state", selected, {
15795
+ kind: "code-state",
15796
+ ...input.provenance.snapshotId ? { id: "snapshot:" + input.provenance.snapshotId } : {},
15797
+ reason: "latest available Code State snapshot",
15798
+ trust: "source-backed"
15799
+ });
15350
15800
  }
15351
15801
  if (input.semanticCode && (input.semanticCode.entryPoints.length > 0 || input.semanticCode.relations.length > 0)) {
15352
15802
  appendLine(lines, "", maxTokens, omitted, "semantic-code-heading");
@@ -15358,7 +15808,14 @@ function renderTaskWorksetPrompt(input) {
15358
15808
  "- " + location + ": " + short(relation.from.name, 12) + " " + short(relation.kind, 8) + " " + short(relation.to.name, 12),
15359
15809
  maxTokens,
15360
15810
  omitted,
15361
- "semantic-relation"
15811
+ "semantic-relation",
15812
+ selected,
15813
+ {
15814
+ kind: "semantic-code",
15815
+ id: "code:" + location,
15816
+ reason: "validated optional semantic code relation",
15817
+ trust: "derived"
15818
+ }
15362
15819
  );
15363
15820
  }
15364
15821
  if (input.semanticCode.relations.length === 0) {
@@ -15369,7 +15826,14 @@ function renderTaskWorksetPrompt(input) {
15369
15826
  "- " + location + ": " + short(entry.name, 16) + " (" + short(entry.kind, 8) + ")",
15370
15827
  maxTokens,
15371
15828
  omitted,
15372
- "semantic-entry"
15829
+ "semantic-entry",
15830
+ selected,
15831
+ {
15832
+ kind: "semantic-code",
15833
+ id: "code:" + location,
15834
+ reason: "validated optional semantic code entry point",
15835
+ trust: "derived"
15836
+ }
15373
15837
  );
15374
15838
  }
15375
15839
  }
@@ -15378,7 +15842,12 @@ function renderTaskWorksetPrompt(input) {
15378
15842
  appendLine(lines, "", maxTokens, omitted, "start-heading");
15379
15843
  appendLine(lines, "Start here", maxTokens, omitted, "start-heading");
15380
15844
  for (const source of input.startHere.slice(0, 5)) {
15381
- appendLine(lines, "- " + source, maxTokens, omitted, "start-here");
15845
+ appendLine(lines, "- " + source, maxTokens, omitted, "start-here", selected, {
15846
+ kind: "start-here",
15847
+ id: "path:" + source,
15848
+ reason: "task-lensed starting point",
15849
+ trust: "derived"
15850
+ });
15382
15851
  }
15383
15852
  }
15384
15853
  if (input.reliableMemory.length > 0) {
@@ -15391,7 +15860,15 @@ function renderTaskWorksetPrompt(input) {
15391
15860
  "- #" + memory.id + " " + memory.type + ": " + short(memory.title, 18) + " (" + location + ")",
15392
15861
  maxTokens,
15393
15862
  omitted,
15394
- "reliable-memory"
15863
+ "reliable-memory",
15864
+ selected,
15865
+ {
15866
+ kind: "memory",
15867
+ id: "memory:" + memory.id,
15868
+ reason: "current code-bound memory",
15869
+ freshness: freshnessForMemory(memory.status),
15870
+ trust: "historical"
15871
+ }
15395
15872
  );
15396
15873
  }
15397
15874
  }
@@ -15399,10 +15876,25 @@ function renderTaskWorksetPrompt(input) {
15399
15876
  appendLine(lines, "", maxTokens, omitted, "knowledge-heading");
15400
15877
  appendLine(lines, "Project knowledge", maxTokens, omitted, "knowledge-heading");
15401
15878
  for (const claim of input.claims.slice(0, 3)) {
15402
- appendLine(lines, "- " + claim.assertion + " [" + claim.id + "]", maxTokens, omitted, "claim");
15879
+ appendLine(lines, "- " + claim.assertion + " [" + claim.id + "]", maxTokens, omitted, "claim", selected, {
15880
+ kind: "claim",
15881
+ id: "claim:" + claim.id,
15882
+ reason: "source-qualified task match",
15883
+ trust: "source-backed"
15884
+ });
15403
15885
  }
15404
15886
  for (const page of input.pages.slice(0, 2)) {
15405
- appendLine(lines, "- page: " + page.relativePath, maxTokens, omitted, "knowledge-page");
15887
+ const supportsDeliveredClaim = page.claimIds.some((claimId) => selected.some((item) => item.kind === "claim" && item.id === "claim:" + claimId));
15888
+ if (!supportsDeliveredClaim) {
15889
+ omitted.push("knowledge-page-dependency");
15890
+ continue;
15891
+ }
15892
+ appendLine(lines, "- page: " + page.relativePath, maxTokens, omitted, "knowledge-page", selected, {
15893
+ kind: "knowledge-page",
15894
+ id: "page:" + page.id,
15895
+ reason: "approved page linked to a selected claim",
15896
+ trust: "source-backed"
15897
+ });
15406
15898
  }
15407
15899
  }
15408
15900
  if (input.workflows.length > 0) {
@@ -15414,7 +15906,14 @@ function renderTaskWorksetPrompt(input) {
15414
15906
  "- " + workflow.title + ": " + workflow.firstPhase.title + " - " + workflow.firstPhase.instructions,
15415
15907
  maxTokens,
15416
15908
  omitted,
15417
- "workflow"
15909
+ "workflow",
15910
+ selected,
15911
+ {
15912
+ kind: "workflow",
15913
+ id: "workflow:" + workflow.id,
15914
+ reason: "task-matching project workflow",
15915
+ trust: "source-backed"
15916
+ }
15418
15917
  );
15419
15918
  }
15420
15919
  }
@@ -15422,16 +15921,23 @@ function renderTaskWorksetPrompt(input) {
15422
15921
  appendLine(lines, "", maxTokens, omitted, "verification-heading");
15423
15922
  appendLine(lines, "Verify", maxTokens, omitted, "verification-heading");
15424
15923
  for (const check of input.verification.slice(0, 4)) {
15425
- appendLine(lines, "- " + short(check, 20), maxTokens, omitted, "verification");
15924
+ appendLine(lines, "- " + short(check, 20), maxTokens, omitted, "verification", selected, {
15925
+ kind: "verification",
15926
+ reason: "task-lensed verification guidance",
15927
+ trust: "derived"
15928
+ });
15426
15929
  }
15427
15930
  }
15428
15931
  return {
15429
15932
  prompt: lines.join("\n"),
15430
15933
  tokenCount: countTextTokens(lines.join("\n")),
15431
- omitted: unique(omitted)
15934
+ omitted: unique(omitted),
15935
+ omittedItems: omitted,
15936
+ selected
15432
15937
  };
15433
15938
  }
15434
15939
  async function buildTaskWorkset(input) {
15940
+ const startedAt = Date.now();
15435
15941
  const task = input.task?.trim() ?? "";
15436
15942
  const maxTokens = Math.max(96, Math.min(Math.floor(input.maxTokens ?? 180), 320));
15437
15943
  const cautions = [...input.runtimeCautions ?? [], ...snapshotCautions(input)];
@@ -15533,6 +16039,18 @@ async function buildTaskWorkset(input) {
15533
16039
  ...base,
15534
16040
  budget: { maxTokens }
15535
16041
  });
16042
+ const receipt = {
16043
+ version: "1.2.2",
16044
+ target: input.deliveryTarget ?? "project-context",
16045
+ elapsedMs: Math.max(0, Date.now() - startedAt),
16046
+ budget: {
16047
+ maxTokens,
16048
+ tokenCount: rendered.tokenCount
16049
+ },
16050
+ selected: rendered.selected,
16051
+ omitted: receiptOmissions(rendered.omittedItems, base.hiddenCautionMemoryCount),
16052
+ scheduledActions: scheduledActions(normalizedCautions)
16053
+ };
15536
16054
  return {
15537
16055
  ...base,
15538
16056
  budget: {
@@ -15540,6 +16058,7 @@ async function buildTaskWorkset(input) {
15540
16058
  tokenCount: rendered.tokenCount,
15541
16059
  omitted: rendered.omitted
15542
16060
  },
16061
+ receipt,
15543
16062
  prompt: rendered.prompt
15544
16063
  };
15545
16064
  }
@@ -16317,6 +16836,9 @@ import path19 from "path";
16317
16836
  function activeProjectObservations(observations2, projectId) {
16318
16837
  return observations2.filter((obs) => obs.projectId === projectId && (obs.status ?? "active") === "active");
16319
16838
  }
16839
+ function deliveryEligibleProjectObservations(observations2, projectId) {
16840
+ return activeProjectObservations(observations2, projectId).filter((observation) => isEligibleForAutomaticDelivery(observation));
16841
+ }
16320
16842
  function decideRefresh(input) {
16321
16843
  if (input.mode === "never") {
16322
16844
  return { performed: false, reason: "disabled", message: "Automatic project scan disabled." };
@@ -16391,13 +16913,27 @@ async function buildAutoProjectContext(input) {
16391
16913
  activeProjectObservations(input.observations, input.project.id)
16392
16914
  );
16393
16915
  try {
16394
- const { enqueueClaimRequalification: enqueueClaimRequalification2 } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
16916
+ const { MaintenanceTargetStore: MaintenanceTargetStore2 } = await Promise.resolve().then(() => (init_maintenance_targets(), maintenance_targets_exports));
16917
+ new MaintenanceTargetStore2(input.dataDir).register({
16918
+ projectId: input.project.id,
16919
+ projectRoot: input.project.rootPath,
16920
+ dataDir: input.dataDir
16921
+ });
16922
+ const {
16923
+ enqueueClaimRequalification: enqueueClaimRequalification2,
16924
+ enqueueObservationQualification: enqueueObservationQualification2
16925
+ } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
16395
16926
  enqueueClaimRequalification2({
16396
16927
  dataDir: input.dataDir,
16397
16928
  projectId: input.project.id,
16398
16929
  source: "foreground-refresh",
16399
16930
  snapshotId: store.latestSnapshot(input.project.id)?.id
16400
16931
  });
16932
+ enqueueObservationQualification2({
16933
+ dataDir: input.dataDir,
16934
+ projectId: input.project.id,
16935
+ source: "foreground-refresh"
16936
+ });
16401
16937
  } catch {
16402
16938
  }
16403
16939
  refresh = { ...refresh, backfill };
@@ -16414,7 +16950,9 @@ async function buildAutoProjectContext(input) {
16414
16950
  const explain = buildProjectContextExplain({
16415
16951
  project: input.project,
16416
16952
  store,
16417
- observations: input.observations,
16953
+ // Binding sees all active evidence, but automatic delivery sees only
16954
+ // evidence that has passed the control-plane admission gate.
16955
+ observations: deliveryEligibleProjectObservations(input.observations, input.project.id),
16418
16956
  exclude
16419
16957
  });
16420
16958
  const overview = explain.overview;
@@ -16505,7 +17043,8 @@ async function buildAutoProjectContext(input) {
16505
17043
  suspect: overview.freshness.suspect,
16506
17044
  stale: overview.freshness.stale
16507
17045
  },
16508
- runtimeCautions
17046
+ runtimeCautions,
17047
+ ...input.deliveryTarget ? { deliveryTarget: input.deliveryTarget } : {}
16509
17048
  });
16510
17049
  return {
16511
17050
  project: input.project,
@@ -16739,6 +17278,7 @@ var init_auto_context = __esm({
16739
17278
  init_external_provider();
16740
17279
  init_project_context();
16741
17280
  init_store();
17281
+ init_admission();
16742
17282
  init_task_lens();
16743
17283
  DEFAULT_MAX_AGE_MS = 10 * 60 * 1e3;
16744
17284
  }
@@ -16931,7 +17471,8 @@ async function attachTaskWorkset(input) {
16931
17471
  suspect: input.pack.warnings.filter((warning) => warning.status === "suspect").length,
16932
17472
  stale: input.pack.warnings.filter((warning) => warning.status === "stale").length
16933
17473
  },
16934
- runtimeCautions: input.runtimeCautions
17474
+ runtimeCautions: input.runtimeCautions,
17475
+ deliveryTarget: "context-pack"
16935
17476
  });
16936
17477
  return { ...input.pack, workset };
16937
17478
  }
@@ -18343,11 +18884,14 @@ __export(export_import_exports, {
18343
18884
  exportAsMarkdown: () => exportAsMarkdown,
18344
18885
  importFromJson: () => importFromJson
18345
18886
  });
18346
- async function exportAsJson(projectDir2, projectId) {
18887
+ async function exportAsJson(projectDir2, projectId, reader) {
18347
18888
  const store = getObservationStore();
18348
18889
  const allObs = await store.loadAll();
18349
18890
  const allSessions = await getSessionStore().loadAll();
18350
- const projectObs = allObs.filter((o) => o.projectId === projectId);
18891
+ const projectObs = filterReadableObservations(
18892
+ allObs.filter((o) => o.projectId === projectId),
18893
+ reader
18894
+ );
18351
18895
  const projectSessions = allSessions.filter((s) => s.projectId === projectId);
18352
18896
  const typeBreakdown = {};
18353
18897
  for (const obs of projectObs) {
@@ -18366,8 +18910,8 @@ async function exportAsJson(projectDir2, projectId) {
18366
18910
  }
18367
18911
  };
18368
18912
  }
18369
- async function exportAsMarkdown(projectDir2, projectId) {
18370
- const data = await exportAsJson(projectDir2, projectId);
18913
+ async function exportAsMarkdown(projectDir2, projectId, reader) {
18914
+ const data = await exportAsJson(projectDir2, projectId, reader);
18371
18915
  const lines = [];
18372
18916
  lines.push(`# Memorix Export: ${projectId}`);
18373
18917
  lines.push(`Exported: ${data.exportedAt}`);
@@ -18469,6 +19013,7 @@ var init_export_import = __esm({
18469
19013
  "src/memory/export-import.ts"() {
18470
19014
  "use strict";
18471
19015
  init_esm_shims();
19016
+ init_visibility();
18472
19017
  init_obs_store();
18473
19018
  init_session_store();
18474
19019
  OBSERVATION_ICONS2 = {
@@ -18645,6 +19190,7 @@ function isEligible(o, projectId) {
18645
19190
  if (isCommandLog2(o)) return false;
18646
19191
  if (isInactive(o)) return false;
18647
19192
  if (isOtherProject(o, projectId)) return false;
19193
+ if (!isEligibleForKnowledgePromotion(o)) return false;
18648
19194
  if (o.valueCategory === "ephemeral") return false;
18649
19195
  if (!contextualHasSubstance(o)) return false;
18650
19196
  return true;
@@ -18793,6 +19339,7 @@ var init_generator = __esm({
18793
19339
  "src/wiki/generator.ts"() {
18794
19340
  "use strict";
18795
19341
  init_esm_shims();
19342
+ init_admission();
18796
19343
  COMMAND_LOG_TITLE4 = /^(Ran:|Command:|Executed:)\s/i;
18797
19344
  SECTION_DEFS = [
18798
19345
  {
@@ -19079,6 +19626,9 @@ function sendError(res, message, status = 500) {
19079
19626
  function filterByProject(items, projectId) {
19080
19627
  return items.filter((item) => item.projectId === projectId);
19081
19628
  }
19629
+ function filterDashboardObservations(observations2, projectId) {
19630
+ return filterReadableObservations(observations2, { projectId });
19631
+ }
19082
19632
  function isActiveStatus(status) {
19083
19633
  return (status ?? "active") === "active";
19084
19634
  }
@@ -19126,7 +19676,10 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19126
19676
  switch (apiPath) {
19127
19677
  case "/projects": {
19128
19678
  try {
19129
- const allObs = await getObservationStore().loadAll();
19679
+ const allObs = filterReadableObservations(
19680
+ await getObservationStore().loadAll(),
19681
+ {}
19682
+ );
19130
19683
  const projectSet = /* @__PURE__ */ new Map();
19131
19684
  for (const obs of allObs) {
19132
19685
  if (!isActiveStatus(obs.status)) continue;
@@ -19175,13 +19728,19 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19175
19728
  await initGraphStore(effectiveDataDir);
19176
19729
  const gStore = getGraphStore();
19177
19730
  const graph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
19178
- const graphObs = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19731
+ const graphObs = filterDashboardObservations(
19732
+ await getObservationStore().loadByProject(effectiveProjectId, { status: "active" }),
19733
+ effectiveProjectId
19734
+ );
19179
19735
  const scoped = scopeKnowledgeGraphToProject(graph, graphObs);
19180
19736
  sendJson(res, { entities: scoped.entities, relations: scoped.relations });
19181
19737
  break;
19182
19738
  }
19183
19739
  case "/observations": {
19184
- const observations2 = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19740
+ const observations2 = filterDashboardObservations(
19741
+ await getObservationStore().loadByProject(effectiveProjectId, { status: "active" }),
19742
+ effectiveProjectId
19743
+ );
19185
19744
  sendJson(res, observations2);
19186
19745
  break;
19187
19746
  }
@@ -19194,7 +19753,10 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19194
19753
  case "/stats": {
19195
19754
  await initGraphStore(effectiveDataDir);
19196
19755
  const graph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
19197
- const observations2 = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19756
+ const observations2 = filterDashboardObservations(
19757
+ await getObservationStore().loadByProject(effectiveProjectId, { status: "active" }),
19758
+ effectiveProjectId
19759
+ );
19198
19760
  const nextId2 = await getObservationStore().loadIdCounter();
19199
19761
  const projectGraphCounts = computeProjectGraphCounts(graph.entities, graph.relations, observations2);
19200
19762
  const typeCounts = {};
@@ -19312,7 +19874,10 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19312
19874
  break;
19313
19875
  }
19314
19876
  case "/retention": {
19315
- const observations2 = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19877
+ const observations2 = filterDashboardObservations(
19878
+ await getObservationStore().loadByProject(effectiveProjectId, { status: "active" }),
19879
+ effectiveProjectId
19880
+ );
19316
19881
  const now3 = Date.now();
19317
19882
  const scored = observations2.filter((obs) => obs.type !== "probe").map((obs) => {
19318
19883
  const age = now3 - new Date(obs.createdAt || now3).getTime();
@@ -19350,7 +19915,10 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19350
19915
  const { generateKnowledgeBase: generateKnowledgeBase2 } = await Promise.resolve().then(() => (init_generator(), generator_exports));
19351
19916
  const { initMiniSkillStore: initMiniSkillStore2, getMiniSkillStore: getMiniSkillStore2 } = await Promise.resolve().then(() => (init_mini_skill_store(), mini_skill_store_exports));
19352
19917
  await initMiniSkillStore2(effectiveDataDir);
19353
- const allObs = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19918
+ const allObs = filterDashboardObservations(
19919
+ await getObservationStore().loadByProject(effectiveProjectId, { status: "active" }),
19920
+ effectiveProjectId
19921
+ );
19354
19922
  const skills = await getMiniSkillStore2().loadByProject(effectiveProjectId);
19355
19923
  const overview = generateKnowledgeBase2({
19356
19924
  projectId: effectiveProjectId,
@@ -19366,7 +19934,10 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19366
19934
  const { initGraphStore: initGraphStore2, getGraphStore: getGraphStore2 } = await Promise.resolve().then(() => (init_graph_store(), graph_store_exports));
19367
19935
  await initMiniSkillStore2(effectiveDataDir);
19368
19936
  await initGraphStore2(effectiveDataDir);
19369
- const allObs = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19937
+ const allObs = filterDashboardObservations(
19938
+ await getObservationStore().loadByProject(effectiveProjectId, { status: "active" }),
19939
+ effectiveProjectId
19940
+ );
19370
19941
  const skills = await getMiniSkillStore2().loadByProject(effectiveProjectId);
19371
19942
  const fullGraph = { entities: getGraphStore2().loadEntities(), relations: getGraphStore2().loadRelations() };
19372
19943
  const scoped = scopeKnowledgeGraphToProject(fullGraph, allObs);
@@ -19489,7 +20060,7 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19489
20060
  break;
19490
20061
  }
19491
20062
  case "/identity": {
19492
- const allObs = await getObservationStore().loadAll();
20063
+ const allObs = filterReadableObservations(await getObservationStore().loadAll(), {});
19493
20064
  const allProjectIds = [...new Set(allObs.map((o) => o.projectId).filter(Boolean))];
19494
20065
  let classifyProjectId2 = () => "real";
19495
20066
  let isDirtyProjectId2 = () => false;
@@ -19574,6 +20145,8 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19574
20145
  sendError(res, "Observation not found", 404);
19575
20146
  } else if (matchObs.projectId !== effectiveProjectId) {
19576
20147
  sendError(res, `Observation #${obsId} belongs to project "${matchObs.projectId}", not "${effectiveProjectId}"`, 403);
20148
+ } else if (!canManageObservation(matchObs, { projectId: effectiveProjectId })) {
20149
+ sendError(res, `Observation #${obsId} is not manageable from the unbound dashboard.`, 403);
19577
20150
  } else {
19578
20151
  await obsStore.remove(obsId);
19579
20152
  try {
@@ -19595,7 +20168,10 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19595
20168
  if (apiPath === "/export") {
19596
20169
  await initGraphStore(effectiveDataDir);
19597
20170
  const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
19598
- const observations2 = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
20171
+ const observations2 = filterDashboardObservations(
20172
+ await getObservationStore().loadByProject(effectiveProjectId, { status: "active" }),
20173
+ effectiveProjectId
20174
+ );
19599
20175
  const nextId2 = await getObservationStore().loadIdCounter();
19600
20176
  const scoped = scopeKnowledgeGraphToProject(fullGraph, observations2);
19601
20177
  const exportData = {
@@ -19919,6 +20495,7 @@ var init_server = __esm({
19919
20495
  init_yaml_loader();
19920
20496
  init_yaml_loader();
19921
20497
  init_graph_scope();
20498
+ init_visibility();
19922
20499
  MIME_TYPES = {
19923
20500
  ".html": "text/html; charset=utf-8",
19924
20501
  ".css": "text/css; charset=utf-8",
@@ -20696,6 +21273,11 @@ async function createHandoffArtifact(input, storeObservation2, teamStore) {
20696
21273
  sourceDetail: "explicit",
20697
21274
  valueCategory: "core",
20698
21275
  // immune to retention archival
21276
+ // A direct handoff is not a project-wide memo. The sender retains access
21277
+ // and the explicit recipient receives a read grant. Broadcast handoffs
21278
+ // are deliberately visible only to active team members.
21279
+ visibility: input.toAgentId ? "personal" : "team",
21280
+ ...input.toAgentId ? { sharedWithAgentIds: [input.toAgentId] } : {},
20699
21281
  createdByAgentId: input.fromAgentId
20700
21282
  });
20701
21283
  try {
@@ -22695,6 +23277,7 @@ async function createAutoRelations(obs, extracted, graphManager) {
22695
23277
  // src/server.ts
22696
23278
  init_entity_extractor();
22697
23279
  init_graph_scope();
23280
+ init_visibility();
22698
23281
 
22699
23282
  // src/compact/engine.ts
22700
23283
  init_esm_shims();
@@ -22820,7 +23403,15 @@ function formatTimeline(timeline) {
22820
23403
  function formatObservationDetail(doc) {
22821
23404
  const icon = getTypeIcon(doc.type);
22822
23405
  const lines = [];
22823
- const header = buildProvenanceHeader(doc.sourceDetail, doc.valueCategory, doc.source, doc.commitHash, doc.relatedCommits);
23406
+ const header = buildProvenanceHeader(
23407
+ doc.sourceDetail,
23408
+ doc.valueCategory,
23409
+ doc.admissionState,
23410
+ doc.admissionReason,
23411
+ doc.source,
23412
+ doc.commitHash,
23413
+ doc.relatedCommits
23414
+ );
22824
23415
  if (header) {
22825
23416
  lines.push(header);
22826
23417
  lines.push("");
@@ -22855,7 +23446,7 @@ function formatObservationDetail(doc) {
22855
23446
  }
22856
23447
  return lines.join("\n");
22857
23448
  }
22858
- function buildProvenanceHeader(sourceDetail, valueCategory, source, commitHash, relatedCommits) {
23449
+ function buildProvenanceHeader(sourceDetail, valueCategory, admissionState, admissionReason, source, commitHash, relatedCommits) {
22859
23450
  const sd = resolveSourceDetail(sourceDetail, source);
22860
23451
  if (!sd) return "";
22861
23452
  const label = sourceKindLabel(sd);
@@ -22866,11 +23457,21 @@ function buildProvenanceHeader(sourceDetail, valueCategory, source, commitHash,
22866
23457
  if (verificationLine) {
22867
23458
  lines.push(verificationLine);
22868
23459
  }
22869
- if (valueCategory === "core") {
23460
+ if (valueCategory === "core" && admissionState !== "candidate" && admissionState !== "ephemeral") {
22870
23461
  lines.push("[CORE] Core \u2014 immune to decay");
23462
+ } else if (valueCategory === "core") {
23463
+ lines.push("[CORE] Core candidate \u2014 not durable until qualification completes");
22871
23464
  } else if (valueCategory === "ephemeral") {
22872
23465
  lines.push("[WARN] Ephemeral \u2014 short-lived signal");
22873
23466
  }
23467
+ if (admissionState === "candidate") {
23468
+ lines.push("[PENDING] Candidate \u2014 excluded from automatic context until current Code Memory qualifies it");
23469
+ } else if (admissionState === "ephemeral") {
23470
+ lines.push("[TRACE] Ephemeral capture \u2014 excluded from automatic context");
23471
+ } else if (admissionState === "qualified") {
23472
+ lines.push("[QUALIFIED] Automatic evidence passed current Code Memory qualification");
23473
+ }
23474
+ if (admissionReason) lines.push(`Admission: ${redactCredentials(admissionReason)}`);
22874
23475
  return lines.join("\n");
22875
23476
  }
22876
23477
  function sourceKindLabel(sd) {
@@ -22951,6 +23552,7 @@ init_obs_store();
22951
23552
  init_mini_skill_store();
22952
23553
  init_mini_skills();
22953
23554
  init_secret_filter();
23555
+ init_visibility();
22954
23556
  function normalizeMemoryBrowseQuery(query) {
22955
23557
  const trimmed = query.trim();
22956
23558
  if (!trimmed) return "";
@@ -22993,11 +23595,13 @@ async function compactSearch(options) {
22993
23595
  );
22994
23596
  let formatted = formatIndexTable(entries, searchOptions.query, !options.projectId);
22995
23597
  if (entries.length === 0 && options.projectId) {
22996
- const allObservations = getAllObservations();
23598
+ const allObservations = filterReadableObservations(getAllObservations(), options.reader);
22997
23599
  const projectAliases = new Set(await resolveAliases(options.projectId).catch(() => [options.projectId]));
22998
23600
  const projectHasStoredMemory = allObservations.some((obs) => obs.projectId && projectAliases.has(obs.projectId));
22999
23601
  if (!projectHasStoredMemory) {
23000
- formatted = `This project does not have any Memorix memories yet.
23602
+ formatted = options.reader ? `No Memorix memories are available to this session yet.
23603
+
23604
+ The tool call worked, but this session has no project-visible memory to retrieve.` : `This project does not have any Memorix memories yet.
23001
23605
 
23002
23606
  It looks like a fresh project: the tool call worked, but there is nothing stored to retrieve yet.
23003
23607
 
@@ -23011,8 +23615,8 @@ This project does have stored Memorix memories, but none matched the current que
23011
23615
  const totalTokens = countTextTokens(formatted);
23012
23616
  return { entries, formatted, totalTokens };
23013
23617
  }
23014
- async function compactTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3) {
23015
- const result = await getTimeline(anchorId, projectId, depthBefore, depthAfter);
23618
+ async function compactTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3, reader) {
23619
+ const result = await getTimeline(anchorId, projectId, depthBefore, depthAfter, reader);
23016
23620
  const timeline = {
23017
23621
  anchorId,
23018
23622
  anchorEntry: result.anchor,
@@ -23023,7 +23627,7 @@ async function compactTimeline(anchorId, projectId, depthBefore = 3, depthAfter
23023
23627
  const totalTokens = countTextTokens(formatted);
23024
23628
  return { timeline, formatted, totalTokens };
23025
23629
  }
23026
- async function compactDetail(idsOrRefs) {
23630
+ async function compactDetail(idsOrRefs, options = {}) {
23027
23631
  const invalidRefs = [];
23028
23632
  const parsedRefs = [];
23029
23633
  for (let i = 0; i < idsOrRefs.length; i++) {
@@ -23080,7 +23684,7 @@ async function compactDetail(idsOrRefs) {
23080
23684
  const missingRefs = [];
23081
23685
  for (const ref of refs) {
23082
23686
  const obs = getObservation(ref.id, ref.projectId);
23083
- if (obs && (ref.projectId ? obs.projectId === ref.projectId : true)) {
23687
+ if (obs && (ref.projectId ? obs.projectId === ref.projectId : true) && canReadObservation(obs, options.reader)) {
23084
23688
  documentMap.set(toRefKey(ref), {
23085
23689
  id: makeOramaObservationId(obs.projectId, obs.id),
23086
23690
  observationId: obs.id,
@@ -23099,7 +23703,12 @@ async function compactDetail(idsOrRefs) {
23099
23703
  status: obs.status ?? "active",
23100
23704
  source: obs.source ?? "agent",
23101
23705
  sourceDetail: obs.sourceDetail ?? "",
23102
- valueCategory: obs.valueCategory ?? ""
23706
+ valueCategory: obs.valueCategory ?? "",
23707
+ admissionState: obs.admissionState ?? "",
23708
+ admissionReason: obs.admissionReason ?? "",
23709
+ visibility: obs.visibility ?? "project",
23710
+ createdByAgentId: obs.createdByAgentId ?? "",
23711
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? [])
23103
23712
  });
23104
23713
  } else {
23105
23714
  missingRefs.push(ref);
@@ -23109,17 +23718,17 @@ async function compactDetail(idsOrRefs) {
23109
23718
  for (const ref of missingRefs) {
23110
23719
  const fallbackDocs = await getObservationsByIds([ref.id], ref.projectId);
23111
23720
  const doc = fallbackDocs[0];
23112
- if (doc) {
23721
+ if (doc && canReadObservation(doc, options.reader)) {
23113
23722
  documentMap.set(toRefKey(ref), doc);
23114
23723
  continue;
23115
23724
  }
23116
23725
  const stored = await getPersistedObservation(ref);
23117
- if (stored) {
23726
+ if (stored && canReadObservation(stored, options.reader)) {
23118
23727
  documentMap.set(toRefKey(ref), observationToDocument(stored));
23119
23728
  }
23120
23729
  }
23121
23730
  }
23122
- const allObs = getAllObservations();
23731
+ const allObs = filterReadableObservations(getAllObservations(), options.reader);
23123
23732
  const crossRefMap = /* @__PURE__ */ new Map();
23124
23733
  for (const ref of refs) {
23125
23734
  const obs = getObservation(ref.id, ref.projectId);
@@ -23233,7 +23842,12 @@ function observationToDocument(obs) {
23233
23842
  status: obs.status ?? "active",
23234
23843
  source: obs.source ?? "agent",
23235
23844
  sourceDetail: obs.sourceDetail ?? "",
23236
- valueCategory: obs.valueCategory ?? ""
23845
+ valueCategory: obs.valueCategory ?? "",
23846
+ admissionState: obs.admissionState ?? "",
23847
+ admissionReason: obs.admissionReason ?? "",
23848
+ visibility: obs.visibility ?? "project",
23849
+ createdByAgentId: obs.createdByAgentId ?? "",
23850
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? [])
23237
23851
  };
23238
23852
  }
23239
23853
  function formatMiniSkillDetail(skill2, provenanceStatus) {
@@ -23352,7 +23966,9 @@ function auditMemoryQuality(observations2, options) {
23352
23966
  status: obs.status ?? "active",
23353
23967
  source: obs.source ?? "agent",
23354
23968
  sourceDetail: obs.sourceDetail ?? "",
23355
- valueCategory: obs.valueCategory ?? ""
23969
+ valueCategory: obs.valueCategory ?? "",
23970
+ admissionState: obs.admissionState ?? "",
23971
+ admissionReason: obs.admissionReason ?? ""
23356
23972
  }, options.referenceTime) !== "active").map((obs) => makeEntry(obs, "Outside active retention zone"));
23357
23973
  const summary = {
23358
23974
  total: scoped.length,
@@ -23394,6 +24010,7 @@ function auditMemoryQuality(observations2, options) {
23394
24010
 
23395
24011
  // src/memory/graph-context.ts
23396
24012
  init_retention();
24013
+ init_admission();
23397
24014
  function formatGraphContextPrompt(packet) {
23398
24015
  const lines = [
23399
24016
  "## Memory Context Packet",
@@ -23551,7 +24168,9 @@ function scoreForPacket(obs, queryTokens, referenceTime, broadMemoryQuery) {
23551
24168
  status: obs.status ?? "active",
23552
24169
  source: obs.source ?? "agent",
23553
24170
  sourceDetail: obs.sourceDetail ?? "",
23554
- valueCategory: obs.valueCategory ?? ""
24171
+ valueCategory: obs.valueCategory ?? "",
24172
+ admissionState: obs.admissionState ?? "",
24173
+ admissionReason: obs.admissionReason ?? ""
23555
24174
  }, referenceTime) !== "active") {
23556
24175
  score -= 2;
23557
24176
  }
@@ -23645,8 +24264,11 @@ function buildGraphContextPacket(observations2, options) {
23645
24264
  ...audit.issues.orphans.map((entry) => entry.id),
23646
24265
  ...audit.issues.retentionCandidates.map((entry) => entry.id)
23647
24266
  ]);
23648
- const filteredObservations = observations2.filter((obs) => obs.projectId === options.projectId && !riskIds.has(obs.id));
23649
- const baseObservations = filteredObservations.length > 0 ? filteredObservations : observations2;
24267
+ const deliveryEligible = observations2.filter(
24268
+ (obs) => obs.projectId === options.projectId && isEligibleForAutomaticDelivery(obs)
24269
+ );
24270
+ const filteredObservations = deliveryEligible.filter((obs) => !riskIds.has(obs.id));
24271
+ const baseObservations = filteredObservations.length > 0 ? filteredObservations : deliveryEligible;
23650
24272
  const memories = pickMemories(baseObservations, options.projectId, options.query, options.limit ?? 5, referenceTime);
23651
24273
  const entities = buildEntities(memories);
23652
24274
  const entityNames = new Set(entities.map((entity) => entity.name));
@@ -26133,6 +26755,8 @@ async function createMemorixServer(cwd, existingServer, sharedTeam, options = {}
26133
26755
  let projectResolutionError = null;
26134
26756
  let explicitProjectBound = false;
26135
26757
  let currentAgentId;
26758
+ let teamStore;
26759
+ let initTeamStoreForProject;
26136
26760
  if (detectedProject) {
26137
26761
  rawProject = detectedProject;
26138
26762
  } else {
@@ -26380,7 +27004,7 @@ The path should point to a directory containing a .git folder.`
26380
27004
  };
26381
27005
  const server = existingServer ?? new McpServer({
26382
27006
  name: "memorix",
26383
- version: true ? "1.2.1" : "1.0.1"
27007
+ version: true ? "1.2.3" : "1.0.1"
26384
27008
  });
26385
27009
  const originalRegisterTool = server.registerTool.bind(server);
26386
27010
  server.registerTool = ((name, ...args) => {
@@ -26400,11 +27024,26 @@ The path should point to a directory containing a .git folder.`
26400
27024
  }
26401
27025
  return originalRegisterTool(name, ...args);
26402
27026
  });
27027
+ const getObservationReader = (scope = "project") => {
27028
+ let isTeamMember = false;
27029
+ if (teamFeaturesEnabled && currentAgentId) {
27030
+ try {
27031
+ const agent = teamStore.getAgent(currentAgentId);
27032
+ isTeamMember = agent?.project_id === project.id && agent.status === "active";
27033
+ } catch {
27034
+ }
27035
+ }
27036
+ return {
27037
+ ...scope === "project" ? { projectId: project.id } : {},
27038
+ ...currentAgentId ? { agentId: currentAgentId } : {},
27039
+ isTeamMember
27040
+ };
27041
+ };
26403
27042
  server.registerTool(
26404
27043
  "memorix_store",
26405
27044
  {
26406
27045
  title: "Store Memory",
26407
- 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.",
27046
+ 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.",
26408
27047
  inputSchema: {
26409
27048
  entityName: z2.string().describe('The entity this observation belongs to (e.g., "auth-module", "port-config")'),
26410
27049
  type: z2.enum(OBSERVATION_TYPES).describe("Observation type for classification"),
@@ -26422,361 +27061,214 @@ The path should point to a directory containing a .git folder.`
26422
27061
  completion: z2.number().optional().describe("Completion percentage 0-100")
26423
27062
  }).optional().describe("Progress tracking for task/feature observations"),
26424
27063
  relatedCommits: z2.array(z2.string()).optional().describe("Git commit hashes this memory relates to (links ground truth \u2194 reasoning)"),
26425
- relatedEntities: z2.array(z2.string()).optional().describe("Other entity names this memory cross-references")
27064
+ relatedEntities: z2.array(z2.string()).optional().describe("Other entity names this memory cross-references"),
27065
+ visibility: z2.enum(["personal", "project", "team"]).optional().describe(
27066
+ "Retrieval scope. Project is the normal shared default; personal/team require memorix_session_start with joinTeam=true."
27067
+ )
26426
27068
  }
26427
27069
  },
26428
- async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities }) => {
27070
+ async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility }) => {
26429
27071
  const unresolved = requireResolvedProject("store memory in the current project");
26430
27072
  if (unresolved) return unresolved;
26431
- return withFreshIndex(async () => {
26432
- let entityName = rawEntityName;
26433
- let type = rawType;
26434
- let title = rawTitle;
26435
- let safeFacts = facts ? coerceStringArray(facts) : void 0;
26436
- const safeFiles = filesModified ? coerceStringArray(filesModified) : void 0;
26437
- const safeConcepts = concepts ? coerceStringArray(concepts) : void 0;
26438
- let formationMode = "active";
26439
- if (process.env.MEMORIX_FORMATION_MODE) {
26440
- formationMode = process.env.MEMORIX_FORMATION_MODE;
26441
- } else {
26442
- try {
26443
- const { getBehaviorConfig: getBehaviorConfig2 } = await Promise.resolve().then(() => (init_behavior(), behavior_exports));
26444
- formationMode = getBehaviorConfig2({ projectRoot: project.rootPath }).formationMode;
26445
- } catch {
27073
+ const requestedVisibility = visibility ?? "project";
27074
+ const reader = getObservationReader();
27075
+ if (requestedVisibility !== "project" && !currentAgentId) {
27076
+ return {
27077
+ content: [{ type: "text", text: "Personal or team memory requires memorix_session_start with joinTeam=true so Memorix can bind an owner." }],
27078
+ isError: true
27079
+ };
27080
+ }
27081
+ if (requestedVisibility === "team" && !reader.isTeamMember) {
27082
+ return {
27083
+ content: [{ type: "text", text: "Team memory requires an active coordination membership in the current project." }],
27084
+ isError: true
27085
+ };
27086
+ }
27087
+ try {
27088
+ return await withFreshIndex(async () => {
27089
+ let entityName = rawEntityName;
27090
+ let type = rawType;
27091
+ let title = rawTitle;
27092
+ let safeFacts = facts ? coerceStringArray(facts) : void 0;
27093
+ const safeFiles = filesModified ? coerceStringArray(filesModified) : void 0;
27094
+ const safeConcepts = concepts ? coerceStringArray(concepts) : void 0;
27095
+ let formationMode = "active";
27096
+ if (process.env.MEMORIX_FORMATION_MODE) {
27097
+ formationMode = process.env.MEMORIX_FORMATION_MODE;
27098
+ } else {
27099
+ try {
27100
+ const { getBehaviorConfig: getBehaviorConfig2 } = await Promise.resolve().then(() => (init_behavior(), behavior_exports));
27101
+ formationMode = getBehaviorConfig2({ projectRoot: project.rootPath }).formationMode;
27102
+ } catch {
27103
+ }
26446
27104
  }
26447
- }
26448
- const useFormation = formationMode === "active";
26449
- let formationResult = null;
26450
- let formationNote = "";
26451
- if (useFormation && !topicKey && !progress) {
26452
- let currentFormationStage = "setup";
26453
- const completedFormationStages = {};
26454
- const formationStartTime = Date.now();
26455
- const onFormationStageEvent = (event) => {
26456
- if (event.status === "start") {
27105
+ const useFormation = formationMode === "active";
27106
+ let formationResult = null;
27107
+ let formationNote = "";
27108
+ if (useFormation && !topicKey && !progress) {
27109
+ let currentFormationStage = "setup";
27110
+ const completedFormationStages = {};
27111
+ const formationStartTime = Date.now();
27112
+ const onFormationStageEvent = (event) => {
27113
+ if (event.status === "start") {
27114
+ currentFormationStage = event.stage;
27115
+ return;
27116
+ }
26457
27117
  currentFormationStage = event.stage;
26458
- return;
26459
- }
26460
- currentFormationStage = event.stage;
26461
- if (event.stageDurationMs !== void 0) {
26462
- completedFormationStages[event.stage] = event.stageDurationMs;
26463
- }
26464
- };
26465
- try {
26466
- const formationConfig = {
26467
- mode: "active",
26468
- useLLM: isLLMEnabled(),
26469
- minValueScore: 0.3,
26470
- searchMemories: async (q, limit, pid) => {
26471
- const result = await compactSearch({ query: q, limit, projectId: pid, status: "active" });
26472
- if (result.entries.length === 0) return [];
26473
- const details = await compactDetail(result.entries.map((e) => e.id));
26474
- return details.documents.map((d, i) => ({
26475
- id: Number(d.id.replace("obs-", "")),
26476
- observationId: d.observationId,
26477
- title: d.title,
26478
- narrative: d.narrative,
26479
- facts: d.facts,
26480
- entityName: d.entityName,
26481
- type: d.type,
26482
- score: result.entries[i]?.score ?? 0
26483
- }));
26484
- },
26485
- getObservation: (id) => {
26486
- const o = getObservation(id);
26487
- if (!o) return null;
26488
- return {
26489
- id: o.id,
26490
- entityName: o.entityName,
26491
- type: o.type,
26492
- title: o.title,
26493
- narrative: o.narrative,
26494
- facts: o.facts,
26495
- topicKey: o.topicKey
26496
- };
26497
- },
26498
- getEntityNames: () => graphManager.getEntityNames(),
26499
- onStageEvent: onFormationStageEvent
27118
+ if (event.stageDurationMs !== void 0) {
27119
+ completedFormationStages[event.stage] = event.stageDurationMs;
27120
+ }
26500
27121
  };
26501
- formationResult = await withTimeout2(
26502
- runFormation({
26503
- entityName,
26504
- type,
26505
- title,
26506
- narrative,
26507
- facts: safeFacts,
26508
- projectId: project.id,
26509
- source: "explicit"
26510
- }, formationConfig),
26511
- FORMATION_TIMEOUT_MS,
26512
- "Formation pipeline"
26513
- );
26514
- const modeIcon = "[FAST]";
26515
- formationNote = `
27122
+ try {
27123
+ const formationConfig = {
27124
+ mode: "active",
27125
+ useLLM: isLLMEnabled(),
27126
+ minValueScore: 0.3,
27127
+ searchMemories: async (q, limit, pid) => {
27128
+ const result = await compactSearch({ query: q, limit, projectId: pid, status: "active", reader });
27129
+ if (result.entries.length === 0) return [];
27130
+ const details = await compactDetail(result.entries.map((e) => e.id), { reader });
27131
+ return details.documents.map((d, i) => ({
27132
+ id: Number(d.id.replace("obs-", "")),
27133
+ observationId: d.observationId,
27134
+ title: d.title,
27135
+ narrative: d.narrative,
27136
+ facts: d.facts,
27137
+ entityName: d.entityName,
27138
+ type: d.type,
27139
+ score: result.entries[i]?.score ?? 0
27140
+ }));
27141
+ },
27142
+ getObservation: (id) => {
27143
+ const o = getObservation(id);
27144
+ if (!o || o.projectId !== project.id || !canReadObservation(o, reader)) return null;
27145
+ return {
27146
+ id: o.id,
27147
+ entityName: o.entityName,
27148
+ type: o.type,
27149
+ title: o.title,
27150
+ narrative: o.narrative,
27151
+ facts: o.facts,
27152
+ topicKey: o.topicKey
27153
+ };
27154
+ },
27155
+ getEntityNames: () => graphManager.getEntityNames(),
27156
+ onStageEvent: onFormationStageEvent
27157
+ };
27158
+ formationResult = await withTimeout2(
27159
+ runFormation({
27160
+ entityName,
27161
+ type,
27162
+ title,
27163
+ narrative,
27164
+ facts: safeFacts,
27165
+ projectId: project.id,
27166
+ source: "explicit"
27167
+ }, formationConfig),
27168
+ FORMATION_TIMEOUT_MS,
27169
+ "Formation pipeline"
27170
+ );
27171
+ const modeIcon = "[FAST]";
27172
+ formationNote = `
26516
27173
  ${modeIcon} Formation[active]: ${formationResult.evaluation.category} (${formationResult.evaluation.score.toFixed(2)}) | ${formationResult.resolution.action} | ${formationResult.pipeline.durationMs}ms`;
26517
- if (formationResult.extraction.extractedFacts.length > 0) {
26518
- formationNote += ` | +${formationResult.extraction.extractedFacts.length} facts`;
26519
- }
26520
- if (formationResult.extraction.titleImproved) formationNote += " | title\u2191";
26521
- if (formationResult.extraction.entityResolved) formationNote += ` | entity\u2192${formationResult.entityName}`;
26522
- if (formationResult.extraction.typeCorrected) formationNote += ` | type\u2192${formationResult.type}`;
26523
- } catch (formationErr) {
26524
- const isTimeout = formationErr instanceof Error && formationErr.message.includes("timed out");
26525
- const elapsedMs = Date.now() - formationStartTime;
26526
- const stageSummary = formatFormationStageDurations(completedFormationStages);
26527
- console.error(
26528
- `[memorix] Formation ${isTimeout ? "timed out" : "failed"} in memorix_store after ${elapsedMs}ms/${FORMATION_TIMEOUT_MS}ms at stage ${currentFormationStage}${stageSummary ? ` | completed: ${stageSummary}` : ""}`
26529
- );
26530
- if (!isTimeout && formationErr instanceof Error) {
26531
- console.error(`[memorix] Formation error: ${formationErr.message}`);
26532
- }
26533
- formationNote = `
27174
+ if (formationResult.extraction.extractedFacts.length > 0) {
27175
+ formationNote += ` | +${formationResult.extraction.extractedFacts.length} facts`;
27176
+ }
27177
+ if (formationResult.extraction.titleImproved) formationNote += " | title\u2191";
27178
+ if (formationResult.extraction.entityResolved) formationNote += ` | entity\u2192${formationResult.entityName}`;
27179
+ if (formationResult.extraction.typeCorrected) formationNote += ` | type\u2192${formationResult.type}`;
27180
+ } catch (formationErr) {
27181
+ const isTimeout = formationErr instanceof Error && formationErr.message.includes("timed out");
27182
+ const elapsedMs = Date.now() - formationStartTime;
27183
+ const stageSummary = formatFormationStageDurations(completedFormationStages);
27184
+ console.error(
27185
+ `[memorix] Formation ${isTimeout ? "timed out" : "failed"} in memorix_store after ${elapsedMs}ms/${FORMATION_TIMEOUT_MS}ms at stage ${currentFormationStage}${stageSummary ? ` | completed: ${stageSummary}` : ""}`
27186
+ );
27187
+ if (!isTimeout && formationErr instanceof Error) {
27188
+ console.error(`[memorix] Formation error: ${formationErr.message}`);
27189
+ }
27190
+ formationNote = `
26534
27191
  [WARN] Formation ${isTimeout ? "timed out" : "failed"} \u2014 storing base observation without enrichment`;
26535
- }
26536
- }
26537
- if (useFormation && formationResult && formationResult.resolution.action !== "new") {
26538
- const { action: action2, targetId, reason } = formationResult.resolution;
26539
- if (action2 === "merge" && targetId) {
26540
- const targetObs = getObservation(targetId);
26541
- if (targetObs) {
26542
- await storeObservation({
26543
- entityName: targetObs.entityName,
26544
- type: targetObs.type,
26545
- title: formationResult.title,
26546
- narrative: formationResult.narrative,
26547
- facts: formationResult.facts,
26548
- filesModified: safeFiles,
26549
- concepts: safeConcepts,
26550
- projectId: project.id,
26551
- topicKey: targetObs.topicKey,
26552
- progress,
26553
- sourceDetail: "explicit",
26554
- createdByAgentId: currentAgentId
26555
- });
26556
- return {
26557
- content: [{
26558
- type: "text",
26559
- text: `[UPDATED] Formation MERGE: merged into #${targetId} (${reason})${formationNote}`
26560
- }]
26561
- };
26562
- }
26563
- } else if (action2 === "evolve" && targetId) {
26564
- const targetObs = getObservation(targetId);
26565
- if (targetObs) {
26566
- await storeObservation({
26567
- entityName: targetObs.entityName,
26568
- type: targetObs.type,
26569
- title: formationResult.title,
26570
- narrative: formationResult.narrative,
26571
- facts: formationResult.facts,
26572
- filesModified: safeFiles,
26573
- concepts: safeConcepts,
26574
- projectId: project.id,
26575
- topicKey: targetObs.topicKey,
26576
- progress,
26577
- sourceDetail: "explicit",
26578
- createdByAgentId: currentAgentId
26579
- });
26580
- return {
26581
- content: [{
26582
- type: "text",
26583
- text: `[UPDATED] Formation EVOLVE: evolved #${targetId} (${reason})${formationNote}`
26584
- }]
26585
- };
26586
27192
  }
26587
- } else if (action2 === "discard") {
26588
- return {
26589
- content: [{
26590
- type: "text",
26591
- text: `[SKIP] Formation DISCARD: ${reason}${formationNote}`
26592
- }]
26593
- };
26594
27193
  }
26595
- }
26596
- let compactAction = "";
26597
- let compactMerged = false;
26598
- if (!useFormation && !topicKey && !progress) {
26599
- try {
26600
- const searchResult = await compactSearch({
26601
- query: `${title} ${narrative.substring(0, 200)}`,
26602
- limit: 5,
26603
- projectId: project.id,
26604
- status: "active"
26605
- });
26606
- const similarEntries = searchResult.entries.map((e) => e);
26607
- if (similarEntries.length > 0) {
26608
- const similarIds = similarEntries.map((e) => e.id);
26609
- const details = await compactDetail(similarIds);
26610
- const existingMemories = details.documents.map((d, i) => ({
26611
- id: d.observationId,
26612
- title: d.title,
26613
- narrative: d.narrative,
26614
- facts: d.facts,
26615
- score: similarEntries[i]?.score ?? 0
26616
- }));
26617
- const decision = await withTimeout2(
26618
- compactOnWrite(
26619
- { title, narrative, facts: safeFacts ?? [] },
26620
- existingMemories
26621
- ),
26622
- COMPACT_ON_WRITE_TIMEOUT_MS,
26623
- "Compact-on-write"
26624
- );
26625
- if (decision.action === "UPDATE" && decision.targetId) {
26626
- const targetObs = getObservation(decision.targetId);
26627
- if (targetObs) {
26628
- await storeObservation({
26629
- entityName: targetObs.entityName,
26630
- type: targetObs.type,
26631
- title: decision.mergedNarrative ? title : targetObs.title,
26632
- narrative: decision.mergedNarrative ?? narrative,
26633
- facts: decision.mergedFacts ?? safeFacts,
26634
- filesModified: safeFiles,
26635
- concepts: safeConcepts,
26636
- projectId: project.id,
26637
- topicKey: targetObs.topicKey,
26638
- progress,
26639
- sourceDetail: "explicit",
26640
- createdByAgentId: currentAgentId
26641
- });
26642
- compactAction = `[UPDATED] Compact UPDATE: merged into #${decision.targetId} (${decision.reason})`;
26643
- compactMerged = true;
26644
- return {
26645
- content: [{
26646
- type: "text",
26647
- text: `${compactAction}
26648
- Mode: ${decision.usedLLM ? "LLM" : "heuristic"}`
26649
- }]
26650
- };
26651
- }
26652
- } else if (decision.action === "NONE") {
27194
+ if (useFormation && formationResult && formationResult.resolution.action !== "new") {
27195
+ const { action: action2, targetId, reason } = formationResult.resolution;
27196
+ if (action2 === "merge" && targetId) {
27197
+ const targetObs = getObservation(targetId);
27198
+ if (targetObs && targetObs.projectId === project.id && canReadObservation(targetObs, reader)) {
27199
+ await storeObservation({
27200
+ entityName: targetObs.entityName,
27201
+ type: targetObs.type,
27202
+ title: formationResult.title,
27203
+ narrative: formationResult.narrative,
27204
+ facts: formationResult.facts,
27205
+ filesModified: safeFiles,
27206
+ concepts: safeConcepts,
27207
+ projectId: project.id,
27208
+ topicKey: targetObs.topicKey,
27209
+ progress,
27210
+ sourceDetail: "explicit",
27211
+ createdByAgentId: currentAgentId,
27212
+ visibility,
27213
+ visibilityReader: reader
27214
+ });
26653
27215
  return {
26654
27216
  content: [{
26655
27217
  type: "text",
26656
- text: `[SKIP] Compact SKIP: ${decision.reason}
26657
- Existing memory #${decision.targetId} already covers this.
26658
- Mode: ${decision.usedLLM ? "LLM" : "heuristic"}`
27218
+ text: `[UPDATED] Formation MERGE: merged into #${targetId} (${reason})${formationNote}`
26659
27219
  }]
26660
27220
  };
26661
- } else if (decision.action === "DELETE" && decision.targetId) {
26662
- const { resolveObservations: resolveObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
26663
- await resolveObservations2([decision.targetId], "resolved");
26664
- compactAction = ` | Compact: resolved outdated #${decision.targetId}`;
26665
27221
  }
26666
- if (decision.enrichedFacts && decision.enrichedFacts.length > 0) {
26667
- const currentFacts = safeFacts ?? [];
26668
- const newFacts = decision.enrichedFacts.filter((f) => !currentFacts.includes(f));
26669
- if (newFacts.length > 0) {
26670
- compactAction += ` | +${newFacts.length} LLM-extracted facts`;
26671
- }
27222
+ } else if (action2 === "evolve" && targetId) {
27223
+ const targetObs = getObservation(targetId);
27224
+ if (targetObs && targetObs.projectId === project.id && canReadObservation(targetObs, reader)) {
27225
+ await storeObservation({
27226
+ entityName: targetObs.entityName,
27227
+ type: targetObs.type,
27228
+ title: formationResult.title,
27229
+ narrative: formationResult.narrative,
27230
+ facts: formationResult.facts,
27231
+ filesModified: safeFiles,
27232
+ concepts: safeConcepts,
27233
+ projectId: project.id,
27234
+ topicKey: targetObs.topicKey,
27235
+ progress,
27236
+ sourceDetail: "explicit",
27237
+ createdByAgentId: currentAgentId,
27238
+ visibility,
27239
+ visibilityReader: reader
27240
+ });
27241
+ return {
27242
+ content: [{
27243
+ type: "text",
27244
+ text: `[UPDATED] Formation EVOLVE: evolved #${targetId} (${reason})${formationNote}`
27245
+ }]
27246
+ };
26672
27247
  }
27248
+ } else if (action2 === "discard") {
27249
+ return {
27250
+ content: [{
27251
+ type: "text",
27252
+ text: `[SKIP] Formation DISCARD: ${reason}${formationNote}`
27253
+ }]
27254
+ };
26673
27255
  }
26674
- } catch {
26675
- }
26676
- }
26677
- if (formationResult && formationResult.resolution.action === "new") {
26678
- const llmFacts = formationResult.extraction.extractedFacts;
26679
- if (llmFacts.length > 0) {
26680
- const currentFacts = safeFacts ?? [];
26681
- const currentLower = new Set(currentFacts.map((f) => f.toLowerCase().trim()));
26682
- const newFacts = llmFacts.filter((f) => !currentLower.has(f.toLowerCase().trim()));
26683
- if (newFacts.length > 0) {
26684
- safeFacts = [...currentFacts, ...newFacts];
26685
- }
26686
- }
26687
- if (formationResult.extraction.titleImproved && formationResult.title) {
26688
- title = formationResult.title;
26689
- }
26690
- if (formationResult.extraction.typeCorrected && formationResult.type) {
26691
- type = formationResult.type;
26692
- }
26693
- if (formationResult.extraction.entityResolved && formationResult.entityName) {
26694
- entityName = formationResult.entityName;
26695
- }
26696
- }
26697
- await graphManager.createEntities([
26698
- { name: entityName, entityType: "auto", observations: [] }
26699
- ]);
26700
- let sessionId;
26701
- try {
26702
- const { getActiveSession: getActiveSession2 } = await Promise.resolve().then(() => (init_session(), session_exports));
26703
- const active = await getActiveSession2(projectDir2, project.id);
26704
- if (active) sessionId = active.id;
26705
- } catch {
26706
- }
26707
- let finalNarrative = narrative;
26708
- let compressionNote = "";
26709
- try {
26710
- const { compressNarrative: compressNarrative2 } = await Promise.resolve().then(() => (init_quality(), quality_exports));
26711
- const { compressed, saved, usedLLM } = await withTimeout2(
26712
- compressNarrative2(narrative, safeFacts, type),
26713
- COMPRESSION_TIMEOUT_MS,
26714
- "Narrative compression"
26715
- );
26716
- if (usedLLM && saved > 0) {
26717
- finalNarrative = compressed;
26718
- compressionNote = ` | compressed -${saved} tokens`;
26719
- }
26720
- } catch {
26721
- }
26722
- let attributionWarning = "";
26723
- try {
26724
- const attrCheck = await checkProjectAttribution(entityName, project.id, getAllObservations());
26725
- if (attrCheck.suspicious) {
26726
- attributionWarning = `
26727
- [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.`;
26728
27256
  }
26729
- } catch {
26730
- }
26731
- const { observation: obs, upserted } = await storeObservation({
26732
- entityName,
26733
- type,
26734
- title,
26735
- narrative: finalNarrative,
26736
- facts: safeFacts,
26737
- filesModified: safeFiles,
26738
- concepts: safeConcepts,
26739
- projectId: project.id,
26740
- topicKey,
26741
- sessionId,
26742
- progress,
26743
- relatedCommits,
26744
- relatedEntities,
26745
- sourceDetail: "explicit",
26746
- valueCategory: formationResult?.evaluation.category,
26747
- createdByAgentId: currentAgentId
26748
- });
26749
- await graphManager.addObservations([
26750
- { entityName, contents: [`[#${obs.id}] ${title}`] }
26751
- ]);
26752
- const extracted = extractEntities([title, narrative, ...safeFacts ?? []].join(" "));
26753
- const autoRelCount = await createAutoRelations(obs, extracted, graphManager);
26754
- const enrichmentParts = [];
26755
- const autoFiles = obs.filesModified.filter((f) => !(safeFiles ?? []).includes(f));
26756
- const autoConcepts = obs.concepts.filter((c) => !(safeConcepts ?? []).includes(c));
26757
- if (autoFiles.length > 0) enrichmentParts.push(`+${autoFiles.length} files extracted`);
26758
- if (autoConcepts.length > 0) enrichmentParts.push(`+${autoConcepts.length} concepts enriched`);
26759
- if (autoRelCount > 0) enrichmentParts.push(`+${autoRelCount} relations auto-created`);
26760
- if (obs.hasCausalLanguage) enrichmentParts.push("causal language detected");
26761
- if (upserted) enrichmentParts.push(`topic upserted (rev ${obs.revisionCount ?? 1})`);
26762
- const enrichment = enrichmentParts.length > 0 ? `
26763
- Auto-enriched: ${enrichmentParts.join(", ")}` : "";
26764
- const action = upserted ? "[UPDATED] Updated" : "[OK] Stored";
26765
- if (!useFormation && !topicKey && !progress) {
26766
- const shadowFormation = async () => {
26767
- let oldCompactDecision = null;
27257
+ let compactAction = "";
27258
+ let compactMerged = false;
27259
+ if (!useFormation && !topicKey && !progress) {
26768
27260
  try {
26769
- const compactStart = Date.now();
26770
27261
  const searchResult = await compactSearch({
26771
27262
  query: `${title} ${narrative.substring(0, 200)}`,
26772
27263
  limit: 5,
26773
27264
  projectId: project.id,
26774
- status: "active"
27265
+ status: "active",
27266
+ reader
26775
27267
  });
26776
27268
  const similarEntries = searchResult.entries.map((e) => e);
26777
27269
  if (similarEntries.length > 0) {
26778
27270
  const similarIds = similarEntries.map((e) => e.id);
26779
- const details = await compactDetail(similarIds);
27271
+ const details = await compactDetail(similarIds, { reader });
26780
27272
  const existingMemories = details.documents.map((d, i) => ({
26781
27273
  id: d.observationId,
26782
27274
  title: d.title,
@@ -26784,78 +27276,266 @@ Auto-enriched: ${enrichmentParts.join(", ")}` : "";
26784
27276
  facts: d.facts,
26785
27277
  score: similarEntries[i]?.score ?? 0
26786
27278
  }));
26787
- const decision = await compactOnWrite(
26788
- { title, narrative, facts: safeFacts ?? [] },
26789
- existingMemories
27279
+ const decision = await withTimeout2(
27280
+ compactOnWrite(
27281
+ { title, narrative, facts: safeFacts ?? [] },
27282
+ existingMemories
27283
+ ),
27284
+ COMPACT_ON_WRITE_TIMEOUT_MS,
27285
+ "Compact-on-write"
26790
27286
  );
26791
- oldCompactDecision = {
26792
- action: decision.action,
26793
- targetId: decision.targetId,
26794
- reason: decision.reason,
26795
- durationMs: Date.now() - compactStart
26796
- };
27287
+ if (decision.action === "UPDATE" && decision.targetId) {
27288
+ const targetObs = getObservation(decision.targetId);
27289
+ if (targetObs && targetObs.projectId === project.id && canReadObservation(targetObs, reader)) {
27290
+ await storeObservation({
27291
+ entityName: targetObs.entityName,
27292
+ type: targetObs.type,
27293
+ title: decision.mergedNarrative ? title : targetObs.title,
27294
+ narrative: decision.mergedNarrative ?? narrative,
27295
+ facts: decision.mergedFacts ?? safeFacts,
27296
+ filesModified: safeFiles,
27297
+ concepts: safeConcepts,
27298
+ projectId: project.id,
27299
+ topicKey: targetObs.topicKey,
27300
+ progress,
27301
+ sourceDetail: "explicit",
27302
+ createdByAgentId: currentAgentId,
27303
+ visibility,
27304
+ visibilityReader: reader
27305
+ });
27306
+ compactAction = `[UPDATED] Compact UPDATE: merged into #${decision.targetId} (${decision.reason})`;
27307
+ compactMerged = true;
27308
+ return {
27309
+ content: [{
27310
+ type: "text",
27311
+ text: `${compactAction}
27312
+ Mode: ${decision.usedLLM ? "LLM" : "heuristic"}`
27313
+ }]
27314
+ };
27315
+ }
27316
+ } else if (decision.action === "NONE") {
27317
+ return {
27318
+ content: [{
27319
+ type: "text",
27320
+ text: `[SKIP] Compact SKIP: ${decision.reason}
27321
+ Existing memory #${decision.targetId} already covers this.
27322
+ Mode: ${decision.usedLLM ? "LLM" : "heuristic"}`
27323
+ }]
27324
+ };
27325
+ } else if (decision.action === "DELETE" && decision.targetId) {
27326
+ const { resolveObservations: resolveObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
27327
+ await resolveObservations2([decision.targetId], "resolved");
27328
+ compactAction = ` | Compact: resolved outdated #${decision.targetId}`;
27329
+ }
27330
+ if (decision.enrichedFacts && decision.enrichedFacts.length > 0) {
27331
+ const currentFacts = safeFacts ?? [];
27332
+ const newFacts = decision.enrichedFacts.filter((f) => !currentFacts.includes(f));
27333
+ if (newFacts.length > 0) {
27334
+ compactAction += ` | +${newFacts.length} LLM-extracted facts`;
27335
+ }
27336
+ }
26797
27337
  }
26798
27338
  } catch {
26799
27339
  }
26800
- const formationConfig = {
26801
- mode: formationMode,
26802
- useLLM: isLLMEnabled(),
26803
- minValueScore: 0.3,
26804
- searchMemories: async (q, limit, pid) => {
26805
- const result = await compactSearch({ query: q, limit, projectId: pid, status: "active" });
26806
- if (result.entries.length === 0) return [];
26807
- const details = await compactDetail(result.entries.map((e) => e.id));
26808
- return details.documents.map((d, i) => ({
26809
- id: Number(d.id.replace("obs-", "")),
26810
- observationId: d.observationId,
26811
- title: d.title,
26812
- narrative: d.narrative,
26813
- facts: d.facts,
26814
- entityName: d.entityName,
26815
- type: d.type,
26816
- score: result.entries[i]?.score ?? 0
26817
- }));
26818
- },
26819
- getObservation: (id) => {
26820
- const o = getObservation(id);
26821
- if (!o) return null;
26822
- return { id: o.id, entityName: o.entityName, type: o.type, title: o.title, narrative: o.narrative, facts: o.facts, topicKey: o.topicKey };
26823
- },
26824
- getEntityNames: () => graphManager.getEntityNames()
26825
- };
26826
- const formed = await withTimeout2(
26827
- runFormation({ entityName, type, title, narrative, facts: safeFacts, projectId: project.id, source: "explicit", topicKey }, formationConfig),
26828
- FORMATION_TIMEOUT_MS,
26829
- "Shadow formation"
27340
+ }
27341
+ if (formationResult && formationResult.resolution.action === "new") {
27342
+ const llmFacts = formationResult.extraction.extractedFacts;
27343
+ if (llmFacts.length > 0) {
27344
+ const currentFacts = safeFacts ?? [];
27345
+ const currentLower = new Set(currentFacts.map((f) => f.toLowerCase().trim()));
27346
+ const newFacts = llmFacts.filter((f) => !currentLower.has(f.toLowerCase().trim()));
27347
+ if (newFacts.length > 0) {
27348
+ safeFacts = [...currentFacts, ...newFacts];
27349
+ }
27350
+ }
27351
+ if (formationResult.extraction.titleImproved && formationResult.title) {
27352
+ title = formationResult.title;
27353
+ }
27354
+ if (formationResult.extraction.typeCorrected && formationResult.type) {
27355
+ type = formationResult.type;
27356
+ }
27357
+ if (formationResult.extraction.entityResolved && formationResult.entityName) {
27358
+ entityName = formationResult.entityName;
27359
+ }
27360
+ }
27361
+ await graphManager.createEntities([
27362
+ { name: entityName, entityType: "auto", observations: [] }
27363
+ ]);
27364
+ let sessionId;
27365
+ try {
27366
+ const { getActiveSession: getActiveSession2 } = await Promise.resolve().then(() => (init_session(), session_exports));
27367
+ const active = await getActiveSession2(projectDir2, project.id);
27368
+ if (active) sessionId = active.id;
27369
+ } catch {
27370
+ }
27371
+ let finalNarrative = narrative;
27372
+ let compressionNote = "";
27373
+ try {
27374
+ const { compressNarrative: compressNarrative2 } = await Promise.resolve().then(() => (init_quality(), quality_exports));
27375
+ const { compressed, saved, usedLLM } = await withTimeout2(
27376
+ compressNarrative2(narrative, safeFacts, type),
27377
+ COMPRESSION_TIMEOUT_MS,
27378
+ "Narrative compression"
26830
27379
  );
26831
- const { recordBeforeAfterMetrics: recordBeforeAfterMetrics2 } = await Promise.resolve().then(() => (init_formation(), formation_exports));
26832
- if (oldCompactDecision) {
26833
- recordBeforeAfterMetrics2({
26834
- formationAction: formed.resolution.action,
26835
- formationTargetId: formed.resolution.targetId,
26836
- oldCompactAction: oldCompactDecision.action,
26837
- oldCompactTargetId: oldCompactDecision.targetId,
26838
- oldCompactReason: oldCompactDecision.reason,
26839
- formationValueScore: formed.evaluation.score,
26840
- formationValueCategory: formed.evaluation.category,
26841
- formationDurationMs: formed.pipeline.durationMs,
26842
- compactDurationMs: oldCompactDecision.durationMs
26843
- });
27380
+ if (usedLLM && saved > 0) {
27381
+ finalNarrative = compressed;
27382
+ compressionNote = ` | compressed -${saved} tokens`;
26844
27383
  }
26845
- };
26846
- shadowFormation().catch(() => {
27384
+ } catch {
27385
+ }
27386
+ let attributionWarning = "";
27387
+ try {
27388
+ const attrCheck = await checkProjectAttribution(
27389
+ entityName,
27390
+ project.id,
27391
+ filterReadableObservations(getAllObservations(), reader)
27392
+ );
27393
+ if (attrCheck.suspicious) {
27394
+ attributionWarning = `
27395
+ [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.`;
27396
+ }
27397
+ } catch {
27398
+ }
27399
+ const { observation: obs, upserted } = await storeObservation({
27400
+ entityName,
27401
+ type,
27402
+ title,
27403
+ narrative: finalNarrative,
27404
+ facts: safeFacts,
27405
+ filesModified: safeFiles,
27406
+ concepts: safeConcepts,
27407
+ projectId: project.id,
27408
+ topicKey,
27409
+ sessionId,
27410
+ progress,
27411
+ relatedCommits,
27412
+ relatedEntities,
27413
+ sourceDetail: "explicit",
27414
+ valueCategory: formationResult?.evaluation.category,
27415
+ createdByAgentId: currentAgentId,
27416
+ visibility,
27417
+ visibilityReader: reader
26847
27418
  });
26848
- }
26849
- return {
26850
- content: [
26851
- {
26852
- type: "text",
26853
- text: `${action} observation #${obs.id} "${title}" (~${obs.tokens} tokens)
27419
+ await graphManager.addObservations([
27420
+ { entityName, contents: [`[#${obs.id}] ${title}`] }
27421
+ ]);
27422
+ const extracted = extractEntities([title, narrative, ...safeFacts ?? []].join(" "));
27423
+ const autoRelCount = await createAutoRelations(obs, extracted, graphManager);
27424
+ const enrichmentParts = [];
27425
+ const autoFiles = obs.filesModified.filter((f) => !(safeFiles ?? []).includes(f));
27426
+ const autoConcepts = obs.concepts.filter((c) => !(safeConcepts ?? []).includes(c));
27427
+ if (autoFiles.length > 0) enrichmentParts.push(`+${autoFiles.length} files extracted`);
27428
+ if (autoConcepts.length > 0) enrichmentParts.push(`+${autoConcepts.length} concepts enriched`);
27429
+ if (autoRelCount > 0) enrichmentParts.push(`+${autoRelCount} relations auto-created`);
27430
+ if (obs.hasCausalLanguage) enrichmentParts.push("causal language detected");
27431
+ if (upserted) enrichmentParts.push(`topic upserted (rev ${obs.revisionCount ?? 1})`);
27432
+ const enrichment = enrichmentParts.length > 0 ? `
27433
+ Auto-enriched: ${enrichmentParts.join(", ")}` : "";
27434
+ const action = upserted ? "[UPDATED] Updated" : "[OK] Stored";
27435
+ if (!useFormation && !topicKey && !progress) {
27436
+ const shadowFormation = async () => {
27437
+ let oldCompactDecision = null;
27438
+ try {
27439
+ const compactStart = Date.now();
27440
+ const searchResult = await compactSearch({
27441
+ query: `${title} ${narrative.substring(0, 200)}`,
27442
+ limit: 5,
27443
+ projectId: project.id,
27444
+ status: "active",
27445
+ reader
27446
+ });
27447
+ const similarEntries = searchResult.entries.map((e) => e);
27448
+ if (similarEntries.length > 0) {
27449
+ const similarIds = similarEntries.map((e) => e.id);
27450
+ const details = await compactDetail(similarIds, { reader });
27451
+ const existingMemories = details.documents.map((d, i) => ({
27452
+ id: d.observationId,
27453
+ title: d.title,
27454
+ narrative: d.narrative,
27455
+ facts: d.facts,
27456
+ score: similarEntries[i]?.score ?? 0
27457
+ }));
27458
+ const decision = await compactOnWrite(
27459
+ { title, narrative, facts: safeFacts ?? [] },
27460
+ existingMemories
27461
+ );
27462
+ oldCompactDecision = {
27463
+ action: decision.action,
27464
+ targetId: decision.targetId,
27465
+ reason: decision.reason,
27466
+ durationMs: Date.now() - compactStart
27467
+ };
27468
+ }
27469
+ } catch {
27470
+ }
27471
+ const formationConfig = {
27472
+ mode: formationMode,
27473
+ useLLM: isLLMEnabled(),
27474
+ minValueScore: 0.3,
27475
+ searchMemories: async (q, limit, pid) => {
27476
+ const result = await compactSearch({ query: q, limit, projectId: pid, status: "active", reader });
27477
+ if (result.entries.length === 0) return [];
27478
+ const details = await compactDetail(result.entries.map((e) => e.id), { reader });
27479
+ return details.documents.map((d, i) => ({
27480
+ id: Number(d.id.replace("obs-", "")),
27481
+ observationId: d.observationId,
27482
+ title: d.title,
27483
+ narrative: d.narrative,
27484
+ facts: d.facts,
27485
+ entityName: d.entityName,
27486
+ type: d.type,
27487
+ score: result.entries[i]?.score ?? 0
27488
+ }));
27489
+ },
27490
+ getObservation: (id) => {
27491
+ const o = getObservation(id);
27492
+ if (!o || o.projectId !== project.id || !canReadObservation(o, reader)) return null;
27493
+ return { id: o.id, entityName: o.entityName, type: o.type, title: o.title, narrative: o.narrative, facts: o.facts, topicKey: o.topicKey };
27494
+ },
27495
+ getEntityNames: () => graphManager.getEntityNames()
27496
+ };
27497
+ const formed = await withTimeout2(
27498
+ runFormation({ entityName, type, title, narrative, facts: safeFacts, projectId: project.id, source: "explicit", topicKey }, formationConfig),
27499
+ FORMATION_TIMEOUT_MS,
27500
+ "Shadow formation"
27501
+ );
27502
+ const { recordBeforeAfterMetrics: recordBeforeAfterMetrics2 } = await Promise.resolve().then(() => (init_formation(), formation_exports));
27503
+ if (oldCompactDecision) {
27504
+ recordBeforeAfterMetrics2({
27505
+ formationAction: formed.resolution.action,
27506
+ formationTargetId: formed.resolution.targetId,
27507
+ oldCompactAction: oldCompactDecision.action,
27508
+ oldCompactTargetId: oldCompactDecision.targetId,
27509
+ oldCompactReason: oldCompactDecision.reason,
27510
+ formationValueScore: formed.evaluation.score,
27511
+ formationValueCategory: formed.evaluation.category,
27512
+ formationDurationMs: formed.pipeline.durationMs,
27513
+ compactDurationMs: oldCompactDecision.durationMs
27514
+ });
27515
+ }
27516
+ };
27517
+ shadowFormation().catch(() => {
27518
+ });
27519
+ }
27520
+ return {
27521
+ content: [
27522
+ {
27523
+ type: "text",
27524
+ text: `${action} observation #${obs.id} "${title}" (~${obs.tokens} tokens)
26854
27525
  Entity: ${entityName} | Type: ${type} | Project: ${project.id}${obs.topicKey ? ` | Topic: ${obs.topicKey}` : ""}${compactAction}${compressionNote}${enrichment}${formationNote}${attributionWarning}`
26855
- }
26856
- ]
27526
+ }
27527
+ ]
27528
+ };
27529
+ });
27530
+ } catch (error) {
27531
+ return {
27532
+ content: [{
27533
+ type: "text",
27534
+ text: error instanceof Error ? error.message : "Failed to store memory."
27535
+ }],
27536
+ isError: true
26857
27537
  };
26858
- });
27538
+ }
26859
27539
  }
26860
27540
  );
26861
27541
  server.registerTool(
@@ -26927,7 +27607,8 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
26927
27607
  // Use scope: 'global' to explicitly search all projects.
26928
27608
  projectId: scope === "global" ? void 0 : project.id,
26929
27609
  status: status ?? "active",
26930
- source
27610
+ source,
27611
+ reader: getObservationReader(scope === "global" ? "global" : "project")
26931
27612
  });
26932
27613
  const timeoutPromise = new Promise(
26933
27614
  (_, reject) => setTimeout(() => reject(new Error(`Search timeout after ${TIMEOUT_MS}ms`)), TIMEOUT_MS)
@@ -26985,7 +27666,10 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
26985
27666
  const unresolved = requireResolvedProject("build graph context for the current project");
26986
27667
  if (unresolved) return unresolved;
26987
27668
  const { getObservationStore: getObservationStore2 } = await Promise.resolve().then(() => (init_obs_store(), obs_store_exports));
26988
- const observations2 = await getObservationStore2().loadByProject(project.id);
27669
+ const observations2 = filterReadableObservations(
27670
+ await getObservationStore2().loadByProject(project.id),
27671
+ getObservationReader()
27672
+ );
26989
27673
  const packet = buildGraphContextPacket(observations2, {
26990
27674
  projectId: project.id,
26991
27675
  query,
@@ -27037,7 +27721,10 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27037
27721
  Promise.resolve().then(() => (init_maintenance_jobs(), maintenance_jobs_exports)),
27038
27722
  Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports))
27039
27723
  ]);
27040
- const observations2 = await getObservationStore2().loadByProject(project.id, { status: "active" });
27724
+ const observations2 = filterReadableObservations(
27725
+ await getObservationStore2().loadByProject(project.id, { status: "active" }),
27726
+ getObservationReader()
27727
+ );
27041
27728
  const context = await buildAutoProjectContext2({
27042
27729
  project,
27043
27730
  dataDir: projectDir2,
@@ -27127,7 +27814,10 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27127
27814
  await store.init(projectDir2);
27128
27815
  const codegraphConfig = getResolvedConfig2({ projectRoot: project.rootPath }).codegraph;
27129
27816
  const exclude = codegraphConfig.excludePatterns;
27130
- const observations2 = await getObservationStore2().loadByProject(project.id, { status: "active" });
27817
+ const observations2 = filterReadableObservations(
27818
+ await getObservationStore2().loadByProject(project.id, { status: "active" }),
27819
+ getObservationReader()
27820
+ );
27131
27821
  observations2.reverse();
27132
27822
  const basePack = assembleContextPackForTask2({
27133
27823
  store,
@@ -27499,9 +28189,15 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27499
28189
  }
27500
28190
  },
27501
28191
  async ({ ids, status }) => {
27502
- const { resolveObservations: resolveObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
28192
+ const { resolveObservations: resolveObservations2, getObservation: getObservation2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
27503
28193
  const safeIds = (Array.isArray(ids) ? ids : [ids]).map((id) => coerceNumber(id, 0)).filter((id) => id > 0);
27504
- const result = await resolveObservations2(safeIds, status ?? "resolved");
28194
+ const reader = getObservationReader();
28195
+ const authorizedIds = safeIds.filter((id) => {
28196
+ const observation = getObservation2(id, project.id);
28197
+ return observation ? canManageObservation(observation, reader) : false;
28198
+ });
28199
+ const result = await resolveObservations2(authorizedIds, status ?? "resolved");
28200
+ const deniedCount = safeIds.length - authorizedIds.length;
27505
28201
  const parts = [];
27506
28202
  if (result.resolved.length > 0) {
27507
28203
  parts.push(`[OK] Resolved ${result.resolved.length} observation(s): #${result.resolved.join(", #")}`);
@@ -27509,6 +28205,9 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27509
28205
  if (result.notFound.length > 0) {
27510
28206
  parts.push(`[WARN] Not found: #${result.notFound.join(", #")}`);
27511
28207
  }
28208
+ if (deniedCount > 0) {
28209
+ parts.push(`[WARN] Skipped ${deniedCount} observation(s) outside this session's write scope.`);
28210
+ }
27512
28211
  parts.push('\nResolved memories are hidden from default search. Use status="all" to include them.');
27513
28212
  parts.push('[STATS] Run `memorix_retention` with `action: "report"` to check remaining cleanup status.');
27514
28213
  return {
@@ -27538,6 +28237,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27538
28237
  async ({ entityName, decision, alternatives, rationale, constraints, expectedOutcome, risks, concepts, filesModified, relatedCommits, relatedEntities }) => {
27539
28238
  const unresolved = requireResolvedProject("store reasoning in the current project");
27540
28239
  if (unresolved) return unresolved;
28240
+ const reader = getObservationReader();
27541
28241
  return withFreshIndex(async () => {
27542
28242
  const narrativeParts = [rationale];
27543
28243
  if (alternatives && alternatives.length > 0) {
@@ -27560,7 +28260,11 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27560
28260
  ]);
27561
28261
  let reasoningAttributionWarning = "";
27562
28262
  try {
27563
- const attrCheck = await checkProjectAttribution(entityName, project.id, getAllObservations());
28263
+ const attrCheck = await checkProjectAttribution(
28264
+ entityName,
28265
+ project.id,
28266
+ filterReadableObservations(getAllObservations(), reader)
28267
+ );
27564
28268
  if (attrCheck.suspicious) {
27565
28269
  reasoningAttributionWarning = `
27566
28270
  [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.`;
@@ -27580,7 +28284,8 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27580
28284
  relatedCommits,
27581
28285
  relatedEntities,
27582
28286
  sourceDetail: "explicit",
27583
- createdByAgentId: currentAgentId
28287
+ createdByAgentId: currentAgentId,
28288
+ visibility: "project"
27584
28289
  });
27585
28290
  await graphManager.addObservations([
27586
28291
  { entityName, contents: [`[#${obs.id}] [REASONING] ${decision}`] }
@@ -27612,7 +28317,11 @@ Entity: ${entityName} | ${facts.length} facts | ${obs.tokens} tokens${reasoningA
27612
28317
  const minCount = threshold ?? 2;
27613
28318
  let entries;
27614
28319
  try {
27615
- entries = await auditProjectObservations(project.id, await withFreshIndex(() => getAllObservations()), minCount);
28320
+ entries = await auditProjectObservations(
28321
+ project.id,
28322
+ await withFreshIndex(() => filterReadableObservations(getAllObservations(), getObservationReader())),
28323
+ minCount
28324
+ );
27616
28325
  } catch (err) {
27617
28326
  return {
27618
28327
  content: [{
@@ -27677,7 +28386,8 @@ Entity: ${entityName} | ${facts.length} facts | ${obs.tokens} tokens${reasoningA
27677
28386
  limit: safeLimit,
27678
28387
  type: "reasoning",
27679
28388
  projectId: scope === "global" ? void 0 : project.id,
27680
- status: "active"
28389
+ status: "active",
28390
+ reader: getObservationReader(scope === "global" ? "global" : "project")
27681
28391
  }));
27682
28392
  if (result.entries.length === 0) {
27683
28393
  return {
@@ -27702,7 +28412,11 @@ ${result.formatted}` }]
27702
28412
  },
27703
28413
  async ({ query, dryRun }) => {
27704
28414
  const { getAllObservations: getAllObservations2, resolveObservations: resolveObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
27705
- const allObs = await withFreshIndex(() => getAllObservations2().filter((o) => (o.status ?? "active") === "active" && o.projectId === project.id));
28415
+ const reader = getObservationReader();
28416
+ const allObs = await withFreshIndex(() => filterReadableObservations(
28417
+ getAllObservations2().filter((o) => (o.status ?? "active") === "active" && o.projectId === project.id),
28418
+ reader
28419
+ ).filter((observation) => canManageObservation(observation, reader)));
27706
28420
  if (allObs.length < 2) {
27707
28421
  return { content: [{ type: "text", text: "Not enough active memories to deduplicate." }] };
27708
28422
  }
@@ -27716,7 +28430,7 @@ ${result.formatted}` }]
27716
28430
  }
27717
28431
  let candidates;
27718
28432
  if (query) {
27719
- const searchResult = await compactSearch({ query, limit: 20, projectId: project.id, status: "active" });
28433
+ const searchResult = await compactSearch({ query, limit: 20, projectId: project.id, status: "active", reader });
27720
28434
  const idSet = new Set(searchResult.entries.map((e) => e.id));
27721
28435
  candidates = allObs.filter((o) => idSet.has(o.id));
27722
28436
  } else {
@@ -27806,7 +28520,8 @@ ${actions.join("\n")}`
27806
28520
  safeAnchor,
27807
28521
  project.id,
27808
28522
  safeBefore,
27809
- safeAfter
28523
+ safeAfter,
28524
+ getObservationReader()
27810
28525
  );
27811
28526
  return {
27812
28527
  content: [
@@ -27840,12 +28555,14 @@ ${actions.join("\n")}`
27840
28555
  const safeTypedRefs = coerceStringArray(typedRefs);
27841
28556
  let result;
27842
28557
  try {
28558
+ const hasCrossProjectRef = safeRefs.some((ref) => ref.projectId && ref.projectId !== project.id) || safeTypedRefs.some((ref) => ref.includes("@") && !ref.endsWith(`@${project.id}`));
28559
+ const reader = getObservationReader(hasCrossProjectRef ? "global" : "project");
27843
28560
  if (safeTypedRefs.length > 0) {
27844
- result = await compactDetail(safeTypedRefs);
28561
+ result = await compactDetail(safeTypedRefs, { reader });
27845
28562
  } else if (safeRefs.length > 0) {
27846
- result = await compactDetail(safeRefs);
28563
+ result = await compactDetail(safeRefs, { reader });
27847
28564
  } else {
27848
- result = await compactDetail(safeIds.map((id) => ({ id, projectId: project.id })));
28565
+ result = await compactDetail(safeIds.map((id) => ({ id, projectId: project.id })), { reader });
27849
28566
  }
27850
28567
  } catch (err) {
27851
28568
  return { content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }], isError: true };
@@ -27871,11 +28588,15 @@ ${actions.join("\n")}`
27871
28588
  },
27872
28589
  async (args) => {
27873
28590
  const action = args.action ?? "report";
27874
- const { getRetentionSummary: getRetentionSummary2, getArchiveCandidates: getArchiveCandidates2, rankByRelevance: rankByRelevance2, archiveExpired: archiveExpired2, getRetentionZone: getRetentionZone2, explainRetention: explainRetention2 } = await Promise.resolve().then(() => (init_retention(), retention_exports));
28591
+ const { getRetentionSummary: getRetentionSummary2, getArchiveCandidates: getArchiveCandidates2, rankByRelevance: rankByRelevance2, getRetentionZone: getRetentionZone2, explainRetention: explainRetention2 } = await Promise.resolve().then(() => (init_retention(), retention_exports));
27875
28592
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_orama_store(), orama_store_exports));
27876
28593
  const { search: search2 } = await import("@orama/orama");
27877
- const { getAllObservations: getAllObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
27878
- const allObs = await withFreshIndex(() => getAllObservations2());
28594
+ const { getAllObservations: getAllObservations2, resolveObservations: resolveObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
28595
+ const reader = getObservationReader();
28596
+ const allObs = await withFreshIndex(() => filterReadableObservations(
28597
+ getAllObservations2().filter((observation) => observation.projectId === project.id),
28598
+ reader
28599
+ ));
27879
28600
  const accessMap = /* @__PURE__ */ new Map();
27880
28601
  try {
27881
28602
  const database = await getDb2();
@@ -27892,20 +28613,6 @@ ${actions.join("\n")}`
27892
28613
  }
27893
28614
  } catch {
27894
28615
  }
27895
- if (action === "archive") {
27896
- const result = await archiveExpired2(projectDir2, void 0, accessMap, project.id);
27897
- if (result.archived === 0) {
27898
- return {
27899
- content: [{ type: "text", text: "[OK] No expired observations to archive. All memories are within their retention period." }]
27900
- };
27901
- }
27902
- return {
27903
- content: [{ type: "text", text: `[ARCHIVED] Archived ${result.archived} expired observations (status set to 'archived' in-place)
27904
- ${result.remaining} active observations remaining.
27905
-
27906
- Archived memories are hidden from default search but can be found with status: "all".` }]
27907
- };
27908
- }
27909
28616
  const docs = allObs.map((obs) => ({
27910
28617
  id: `obs-${obs.id}`,
27911
28618
  observationId: obs.id,
@@ -27924,13 +28631,34 @@ Archived memories are hidden from default search but can be found with status: "
27924
28631
  status: obs.status ?? "active",
27925
28632
  source: obs.source ?? "agent",
27926
28633
  sourceDetail: obs.sourceDetail ?? "",
27927
- valueCategory: obs.valueCategory ?? ""
28634
+ valueCategory: obs.valueCategory ?? "",
28635
+ admissionState: obs.admissionState ?? "",
28636
+ admissionReason: obs.admissionReason ?? "",
28637
+ visibility: obs.visibility ?? "project",
28638
+ createdByAgentId: obs.createdByAgentId ?? "",
28639
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? [])
27928
28640
  }));
27929
28641
  if (docs.length === 0) {
27930
28642
  return {
27931
28643
  content: [{ type: "text", text: "No observations found for this project." }]
27932
28644
  };
27933
28645
  }
28646
+ if (action === "archive") {
28647
+ const managedIds = new Set(
28648
+ allObs.filter((observation) => canManageObservation(observation, reader)).map((observation) => observation.id)
28649
+ );
28650
+ const candidates2 = getArchiveCandidates2(docs).filter((document) => managedIds.has(document.observationId));
28651
+ if (candidates2.length === 0) {
28652
+ return {
28653
+ content: [{ type: "text", text: "[OK] No expired memories in this session's write scope to archive." }]
28654
+ };
28655
+ }
28656
+ const result = await resolveObservations2(candidates2.map((document) => document.observationId), "archived");
28657
+ return {
28658
+ content: [{ type: "text", text: `[ARCHIVED] Archived ${result.resolved.length} expired observation(s) in this session's write scope.
28659
+ ${Math.max(0, managedIds.size - result.resolved.length)} visible writable observations remaining.` }]
28660
+ };
28661
+ }
27934
28662
  if (action === "stale") {
27935
28663
  const staleDocs = docs.filter((d) => getRetentionZone2(d) === "stale");
27936
28664
  if (staleDocs.length === 0) {
@@ -28251,7 +28979,10 @@ Archived memories are hidden from default search but can be found with status: "
28251
28979
  const allObs = await withFreshIndex(() => getAllObservations2());
28252
28980
  const scoped = scopeKnowledgeGraphToProject(
28253
28981
  graph,
28254
- allObs.filter((observation) => observation.projectId === project.id)
28982
+ filterReadableObservations(
28983
+ allObs.filter((observation) => observation.projectId === project.id),
28984
+ getObservationReader()
28985
+ )
28255
28986
  );
28256
28987
  return { entities: scoped.entities, relations: scoped.relations };
28257
28988
  }
@@ -28526,7 +29257,10 @@ ${skill2.content}` }]
28526
29257
  };
28527
29258
  }
28528
29259
  const { getObservationStore: getStore } = await Promise.resolve().then(() => (init_obs_store(), obs_store_exports));
28529
- const allObs = await getStore().loadAll();
29260
+ const allObs = filterReadableObservations(
29261
+ (await getStore().loadAll()).filter((observation) => observation.projectId === project.id),
29262
+ getObservationReader()
29263
+ );
28530
29264
  const obsData = allObs.map((o) => ({
28531
29265
  id: o.id || 0,
28532
29266
  entityName: o.entityName || "unknown",
@@ -28625,7 +29359,11 @@ ${skill2.content}` }]
28625
29359
  }
28626
29360
  const { getAllObservations: getAllObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
28627
29361
  const allObs = await withFreshIndex(() => getAllObservations2());
28628
- const matched = allObs.filter((o) => observationIds.includes(o.id));
29362
+ const reader = getObservationReader();
29363
+ const matched = filterReadableObservations(
29364
+ allObs.filter((observation) => observation.projectId === project.id && observationIds.includes(observation.id)),
29365
+ reader
29366
+ );
28629
29367
  if (matched.length === 0) {
28630
29368
  return { content: [{ type: "text", text: `No observations found for IDs: [${observationIds.join(", ")}]. Use \`memorix_search\` to find valid IDs.` }], isError: true };
28631
29369
  }
@@ -28633,6 +29371,13 @@ ${skill2.content}` }]
28633
29371
  if (nonActive.length > 0) {
28634
29372
  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 };
28635
29373
  }
29374
+ const nonProjectShared = matched.filter((observation) => resolveObservationVisibility(observation) !== "project");
29375
+ if (nonProjectShared.length > 0) {
29376
+ return {
29377
+ content: [{ type: "text", text: `Cannot promote private or team-scoped observations: ${nonProjectShared.map((observation) => `#${observation.id}`).join(", ")}. Promote only deliberate project-shared knowledge.` }],
29378
+ isError: true
29379
+ };
29380
+ }
28636
29381
  const skill2 = await promoteToMiniSkill2(projectDir2, project.id, matched, { trigger, instruction, tags });
28637
29382
  const lines = [
28638
29383
  `[OK] Created mini-skill #${skill2.id}`,
@@ -28760,7 +29505,6 @@ Ensure the path points to a directory containing a .git folder (or a subdirector
28760
29505
  const unresolved = requireResolvedProject("start a project session");
28761
29506
  if (unresolved) return unresolved;
28762
29507
  const { startSession: startSession2 } = await Promise.resolve().then(() => (init_session(), session_exports));
28763
- const result = await startSession2(projectDir2, project.id, { sessionId, agent });
28764
29508
  const llmStatus = isLLMEnabled() ? `LLM enhanced mode: ${getLLMConfig()?.provider}/${getLLMConfig()?.model} (fact extraction + auto-dedup active)` : "LLM mode: off (set MEMORIX_LLM_API_KEY to enable enhanced memory quality)";
28765
29509
  const shouldJoinTeam = !!joinTeam;
28766
29510
  let registeredAgent = null;
@@ -28791,8 +29535,11 @@ Ensure the path points to a directory containing a .git folder (or a subdirector
28791
29535
  const lastSeen = registeredAgent.last_seen_obs_generation;
28792
29536
  const store = getObservationStore();
28793
29537
  const currentGen = store.getGeneration();
28794
- const projectObs = await withFreshIndex(() => getAllObservations().filter(
28795
- (o) => o.projectId === project.id && (o.writeGeneration ?? 0) > lastSeen
29538
+ const projectObs = await withFreshIndex(() => filterReadableObservations(
29539
+ getAllObservations().filter(
29540
+ (o) => o.projectId === project.id && (o.writeGeneration ?? 0) > lastSeen
29541
+ ),
29542
+ getObservationReader()
28796
29543
  ));
28797
29544
  const wm = computeWatermark2(lastSeen, currentGen, projectObs.length);
28798
29545
  if (wm.newObservationCount > 0) {
@@ -28814,6 +29561,11 @@ Ensure the path points to a directory containing a .git folder (or a subdirector
28814
29561
  }
28815
29562
  } catch {
28816
29563
  }
29564
+ const result = await startSession2(projectDir2, project.id, {
29565
+ sessionId,
29566
+ agent,
29567
+ reader: getObservationReader()
29568
+ });
28817
29569
  const lines = [
28818
29570
  `[OK] Session started: ${result.session.id}`,
28819
29571
  `Project: ${project.name} (${project.id})`,
@@ -28936,7 +29688,7 @@ ${summary ? "Summary saved for next session context injection." : "No summary pr
28936
29688
  async ({ limit }) => {
28937
29689
  const safeLimit = limit != null ? coerceNumber(limit, 3) : 3;
28938
29690
  const { getSessionContext: getSessionContext2, listSessions: listSessions2 } = await Promise.resolve().then(() => (init_session(), session_exports));
28939
- const context = await getSessionContext2(projectDir2, project.id, safeLimit);
29691
+ const context = await getSessionContext2(projectDir2, project.id, safeLimit, getObservationReader());
28940
29692
  const sessions = await listSessions2(projectDir2, project.id);
28941
29693
  const activeSessions = sessions.filter((s) => s.status === "active");
28942
29694
  const completedSessions = sessions.filter((s) => s.status === "completed");
@@ -28961,7 +29713,7 @@ ${summary ? "Summary saved for next session context injection." : "No summary pr
28961
29713
  "memorix_transfer",
28962
29714
  {
28963
29715
  title: "Transfer Memories",
28964
- 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).',
29716
+ 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).',
28965
29717
  inputSchema: {
28966
29718
  action: z2.enum(["export", "import"]).describe("Operation: export or import"),
28967
29719
  format: z2.enum(["json", "markdown"]).optional().describe("Export format (for export, default: json)"),
@@ -28972,10 +29724,10 @@ ${summary ? "Summary saved for next session context injection." : "No summary pr
28972
29724
  if (action === "export") {
28973
29725
  const { exportAsJson: exportAsJson2, exportAsMarkdown: exportAsMarkdown2 } = await Promise.resolve().then(() => (init_export_import(), export_import_exports));
28974
29726
  if (format === "markdown") {
28975
- const md = await exportAsMarkdown2(projectDir2, project.id);
29727
+ const md = await exportAsMarkdown2(projectDir2, project.id, getObservationReader());
28976
29728
  return { content: [{ type: "text", text: md }] };
28977
29729
  }
28978
- const data = await exportAsJson2(projectDir2, project.id);
29730
+ const data = await exportAsJson2(projectDir2, project.id, getObservationReader());
28979
29731
  const json = JSON.stringify(data, null, 2);
28980
29732
  return {
28981
29733
  content: [{
@@ -29172,8 +29924,6 @@ ${json}
29172
29924
  }
29173
29925
  }
29174
29926
  );
29175
- let teamStore;
29176
- let initTeamStoreForProject;
29177
29927
  if (teamFeaturesEnabled) {
29178
29928
  const { initTeamStore: initTeamStore2 } = await Promise.resolve().then(() => (init_team_store(), team_store_exports));
29179
29929
  initTeamStoreForProject = initTeamStore2;
@@ -29446,7 +30196,7 @@ ${lines.join("\n")}` }] };
29446
30196
  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.`,
29447
30197
  inputSchema: {
29448
30198
  action: z2.enum(["send", "broadcast", "inbox"]).describe("Operation to perform"),
29449
- from: z2.string().optional().describe("Sender agent ID (for send/broadcast)"),
30199
+ from: z2.string().optional().describe("Your sender agent ID. Omit it to use this session identity."),
29450
30200
  to: z2.string().optional().describe("Receiver agent ID (for send)"),
29451
30201
  type: z2.enum(["request", "response", "info", "announcement", "contract", "error", "handoff"]).optional().describe("Message type (for send/broadcast)"),
29452
30202
  content: z2.string().optional().describe("Message content (for send/broadcast)"),
@@ -29457,13 +30207,20 @@ ${lines.join("\n")}` }] };
29457
30207
  }
29458
30208
  },
29459
30209
  async ({ action, from, to, type: msgType, content, agentId, markRead, toRole, handoffStatus }) => {
30210
+ const requireCurrentTeamAgent = () => {
30211
+ if (!currentAgentId) return null;
30212
+ const agent = teamStore.getAgent(currentAgentId);
30213
+ return agent?.project_id === project.id && agent.status === "active" ? agent : null;
30214
+ };
29460
30215
  if (action === "send") {
29461
- if (!from || !msgType || !content) return { content: [{ type: "text", text: "[ERROR] from, type, and content required for send" }], isError: true };
30216
+ const sender = requireCurrentTeamAgent();
30217
+ if (!sender || !msgType || !content) return { content: [{ type: "text", text: "[ERROR] active session identity, type, and content required for send" }], isError: true };
30218
+ if (from && from !== currentAgentId) return { content: [{ type: "text", text: "[ERROR] from must match the current session identity" }], isError: true };
29462
30219
  if (!to && !toRole) return { content: [{ type: "text", text: "[ERROR] either to (agent ID) or toRole is required for send" }], isError: true };
29463
30220
  if (content.length > 1e4) return { content: [{ type: "text", text: "[ERROR] Message too large (max 10KB)" }], isError: true };
29464
30221
  const msg = teamStore.sendMessage({
29465
30222
  projectId: project.id,
29466
- senderAgentId: from,
30223
+ senderAgentId: currentAgentId,
29467
30224
  recipientAgentId: to ?? null,
29468
30225
  type: msgType,
29469
30226
  content,
@@ -29475,11 +30232,13 @@ ${lines.join("\n")}` }] };
29475
30232
  return { content: [{ type: "text", text: `Message sent (${msgType}) to ${target} | ID: ${msg.id.slice(0, 8)}\u2026${toRole ? ` [role: ${toRole}]` : ""}` }] };
29476
30233
  }
29477
30234
  if (action === "broadcast") {
29478
- if (!from || !msgType || !content) return { content: [{ type: "text", text: "[ERROR] from, type, and content required for broadcast" }], isError: true };
30235
+ const sender = requireCurrentTeamAgent();
30236
+ if (!sender || !msgType || !content) return { content: [{ type: "text", text: "[ERROR] active session identity, type, and content required for broadcast" }], isError: true };
30237
+ if (from && from !== currentAgentId) return { content: [{ type: "text", text: "[ERROR] from must match the current session identity" }], isError: true };
29479
30238
  if (content.length > 1e4) return { content: [{ type: "text", text: "[ERROR] Message too large (max 10KB)" }], isError: true };
29480
30239
  const msg = teamStore.sendMessage({
29481
30240
  projectId: project.id,
29482
- senderAgentId: from,
30241
+ senderAgentId: currentAgentId,
29483
30242
  recipientAgentId: null,
29484
30243
  type: msgType,
29485
30244
  content
@@ -29487,8 +30246,12 @@ ${lines.join("\n")}` }] };
29487
30246
  if ("error" in msg) return { content: [{ type: "text", text: `[ERROR] ${msg.error}` }], isError: true };
29488
30247
  return { content: [{ type: "text", text: `Broadcast (${msgType}) | ID: ${msg.id.slice(0, 8)}\u2026` }] };
29489
30248
  }
29490
- const inboxId = agentId || from || "";
29491
- if (!inboxId) return { content: [{ type: "text", text: "[ERROR] agentId required for inbox" }], isError: true };
30249
+ const inboxAgent = requireCurrentTeamAgent();
30250
+ if (!inboxAgent) return { content: [{ type: "text", text: "[ERROR] active session identity required for inbox" }], isError: true };
30251
+ if (agentId && agentId !== currentAgentId || from && from !== currentAgentId) {
30252
+ return { content: [{ type: "text", text: "[ERROR] inbox access is limited to the current session identity" }], isError: true };
30253
+ }
30254
+ const inboxId = currentAgentId;
29492
30255
  const inbox = teamStore.getInbox(project.id, inboxId);
29493
30256
  const unread = teamStore.getUnreadCount(project.id, inboxId);
29494
30257
  if (inbox.length === 0) return { content: [{ type: "text", text: "Inbox empty" }] };
@@ -29516,24 +30279,34 @@ ${lines.join("\n")}` }] };
29516
30279
  },
29517
30280
  async ({ agentId, markInboxRead }) => {
29518
30281
  const { computeWatermark: computeWatermark2, computePoll: computePoll2 } = await Promise.resolve().then(() => (init_poll(), poll_exports));
30282
+ if (agentId && agentId !== currentAgentId) {
30283
+ return {
30284
+ content: [{ type: "text", text: "[ERROR] agentId must match the current session identity." }],
30285
+ isError: true
30286
+ };
30287
+ }
30288
+ const effectiveAgentId = currentAgentId;
29519
30289
  let watermark = computeWatermark2(0, 0, 0);
29520
- if (agentId) {
29521
- const agent = teamStore.getAgent(agentId);
30290
+ if (effectiveAgentId) {
30291
+ const agent = teamStore.getAgent(effectiveAgentId);
29522
30292
  if (agent) {
29523
30293
  const lastSeen = agent.last_seen_obs_generation;
29524
30294
  const store = getObservationStore();
29525
30295
  const currentGen = store.getGeneration();
29526
- const projectObs = await withFreshIndex(() => getAllObservations().filter(
29527
- (o) => o.projectId === project.id && (o.writeGeneration ?? 0) > lastSeen
30296
+ const projectObs = await withFreshIndex(() => filterReadableObservations(
30297
+ getAllObservations().filter(
30298
+ (o) => o.projectId === project.id && (o.writeGeneration ?? 0) > lastSeen
30299
+ ),
30300
+ getObservationReader()
29528
30301
  ));
29529
30302
  watermark = computeWatermark2(lastSeen, currentGen, projectObs.length);
29530
- teamStore.updateWatermark(agentId, currentGen);
29531
- teamStore.heartbeat(agentId);
30303
+ teamStore.updateWatermark(effectiveAgentId, currentGen);
30304
+ teamStore.heartbeat(effectiveAgentId);
29532
30305
  }
29533
30306
  }
29534
- const poll = computePoll2(teamStore, project.id, agentId ?? null, watermark);
29535
- if (markInboxRead && agentId) {
29536
- teamStore.markAllRead(project.id, agentId);
30307
+ const poll = computePoll2(teamStore, project.id, effectiveAgentId ?? null, watermark);
30308
+ if (markInboxRead && effectiveAgentId) {
30309
+ teamStore.markAllRead(project.id, effectiveAgentId);
29537
30310
  }
29538
30311
  const lines = [];
29539
30312
  if (poll.agent) {
@@ -29593,7 +30366,7 @@ ${lines.join("\n")}` }] };
29593
30366
  "memorix_handoff",
29594
30367
  {
29595
30368
  title: "Team Handoff \u2014 Agent Context Transfer",
29596
- 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.",
30369
+ 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.",
29597
30370
  inputSchema: {
29598
30371
  fromAgentId: z2.string().describe("Your agent ID (from team_manage join or session_start with joinTeam=true)"),
29599
30372
  summary: z2.string().describe("Human-readable summary of what you did and what needs to happen next"),
@@ -29606,6 +30379,34 @@ ${lines.join("\n")}` }] };
29606
30379
  },
29607
30380
  async ({ fromAgentId, summary, context, toAgentId, taskId, filesModified, concepts }) => {
29608
30381
  const { createHandoffArtifact: createHandoffArtifact2 } = await Promise.resolve().then(() => (init_handoff(), handoff_exports));
30382
+ if (!currentAgentId) {
30383
+ return {
30384
+ content: [{ type: "text", text: "Create a coordination identity first: call memorix_session_start with joinTeam=true." }],
30385
+ isError: true
30386
+ };
30387
+ }
30388
+ if (fromAgentId !== currentAgentId) {
30389
+ return {
30390
+ content: [{ type: "text", text: "fromAgentId must match the identity returned for this session. Memorix will not create a handoff on behalf of another agent." }],
30391
+ isError: true
30392
+ };
30393
+ }
30394
+ const sender = teamStore.getAgent(currentAgentId);
30395
+ if (!sender || sender.project_id !== project.id || sender.status !== "active") {
30396
+ return {
30397
+ content: [{ type: "text", text: "The current coordination identity is not an active member of this project." }],
30398
+ isError: true
30399
+ };
30400
+ }
30401
+ if (toAgentId) {
30402
+ const recipient = teamStore.getAgent(toAgentId);
30403
+ if (!recipient || recipient.project_id !== project.id) {
30404
+ return {
30405
+ content: [{ type: "text", text: "The handoff recipient must be an agent registered in the current project." }],
30406
+ isError: true
30407
+ };
30408
+ }
30409
+ }
29609
30410
  const result = await createHandoffArtifact2(
29610
30411
  {
29611
30412
  projectId: project.id,