claude-mem-lite 3.87.0 β†’ 3.89.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server.mjs CHANGED
@@ -50,7 +50,7 @@ import { ensureRegistryDb, collectRegistryStats, listResourcesRanked, formatRegi
50
50
  import { IMPORT_STRING_FIELDS, importResource, removeResource, reindexResources } from './lib/registry-core.mjs';
51
51
  import { searchResources } from './registry-retriever.mjs';
52
52
  import { probeOtherSources as probeIdSources, bucketIdTokens, splitDeferredTokens } from './lib/id-routing.mjs';
53
- import { saveObservation } from './lib/save-observation.mjs';
53
+ import { saveObservation, formatSupersedeSkipped, formatSupersededNote } from './lib/save-observation.mjs';
54
54
  import { applyObsUpdate } from './lib/observation-write.mjs';
55
55
  import { EXPORT_COLUMNS_SQL } from './lib/export-columns.mjs';
56
56
  import { liveObsFilterSql } from './lib/inject-search-core.mjs';
@@ -58,7 +58,7 @@ import { recallByFile } from './lib/recall-core.mjs';
58
58
  import { fetchRecent } from './lib/recent-core.mjs';
59
59
  import { AUTO_MERGE_THRESHOLD } from './lib/dedup-constants.mjs';
60
60
  import {
61
- insertDeferred, listOpenWithOrdinal, dropDeferred,
61
+ insertDeferred, listOpenWithOrdinal, dropDeferred, formatDropReasonHint,
62
62
  resolveDeferredIds, closeDeferredItems,
63
63
  getDeferredByIds, formatDeferredDetail,
64
64
  searchDeferredWork, formatDeferredSearchTrailer,
@@ -853,7 +853,9 @@ server.registerTool(
853
853
  // Resolve INSIDE tx + after dedup check so duplicate replays don't throw on
854
854
  // already-closed items. Mirrors mem-cli.mjs cmdSave shape.
855
855
  if (args.closes_deferred && args.closes_deferred.length > 0) {
856
- closesIds = resolveDeferredIds(db, project, args.closes_deferred);
856
+ // D#195: 'dropped' is closable by the close verb (kept in sync with
857
+ // mem-cli.mjs cmdSave β€” same policy, both faces).
858
+ closesIds = resolveDeferredIds(db, project, args.closes_deferred, { allowStatuses: ['open', 'dropped'] });
857
859
  closeDeferredItems(db, closesIds, r.id);
858
860
  }
859
861
  return r;
@@ -869,22 +871,26 @@ server.registerTool(
869
871
  }
870
872
 
871
873
  if (result.kind === 'duplicate') {
872
- return { content: [{ type: 'text', text: `Skipped: similar to existing #${result.existingId} in project "${project}". Use mem_get(ids=[${result.existingId}]) to review.` }] };
874
+ // D#201: this branch returns before the note below, so it renders its own.
875
+ const dupSkip = formatSupersedeSkipped(result.supersedeSkipped);
876
+ const dupText = `Skipped: similar to existing #${result.existingId} in project "${project}". Use mem_get(ids=[${result.existingId}]) to review.`;
877
+ return { content: [{ type: 'text', text: dupSkip ? `${dupText}\n${dupSkip}` : dupText }] };
873
878
  }
874
879
 
875
880
  const lessonNote = result.lessonCaptured ? ` πŸ’‘lesson captured` : '';
876
881
  const closedNote = closesIds && closesIds.length > 0
877
882
  ? ` Closed deferred: ${closesIds.map(i => `D#${i}`).join(', ')}.`
878
883
  : '';
879
- const supersededNote = result.supersededIds && result.supersededIds.length > 0
880
- ? ` Superseded: ${result.supersededIds.map(i => `#${i}`).join(', ')}.`
881
- : '';
884
+ const supersededNote = formatSupersededNote(result);
882
885
  const nudge = buildLessonNudge({ type: result.type, id: result.id, lessonCaptured: result.lessonCaptured, surface: 'mcp' });
883
886
  // G1+G2: detached backfill worker (lesson for obligated types + aliases for
884
887
  // every save) β€” fill-only-empty, so an agent acting on the nudge still wins.
885
888
  const enrichNote = shouldQueueSaveEnrich(result) && queueSaveEnrich(result.id)
886
889
  ? ' (background enrichment queued)' : '';
887
- return { content: [{ type: 'text', text: `Saved as observation #${result.id} [${result.type}] in project "${project}".${lessonNote}${closedNote}${supersededNote}${enrichNote}${nudge}` }] };
890
+ // D#201: on its own line rather than inside the success sentence.
891
+ const skipNote = formatSupersedeSkipped(result.supersedeSkipped);
892
+ const savedText = `Saved as observation #${result.id} [${result.type}] in project "${project}".${lessonNote}${closedNote}${supersededNote}${enrichNote}${nudge}`;
893
+ return { content: [{ type: 'text', text: skipNote ? `${savedText}\n${skipNote}` : savedText }] };
888
894
  })
889
895
  );
890
896
 
@@ -960,7 +966,11 @@ server.registerTool(
960
966
  if (r.changed === 0) {
961
967
  return { content: [{ type: 'text', text: `D#${realId} was not in 'open' status β€” drop is a no-op.` }] };
962
968
  }
963
- return { content: [{ type: 'text', text: `Dropped D#${realId} in project "${project}". Reason: ${args.reason}` }] };
969
+ // D#195 (c): same advisory as the CLI's `defer drop`, so an agent reaching
970
+ // this through MCP gets the same steer toward `mem_save(closes_deferred)`.
971
+ const hint = formatDropReasonHint(args.reason);
972
+ const dropText = `Dropped D#${realId} in project "${project}". Reason: ${args.reason}`;
973
+ return { content: [{ type: 'text', text: hint ? `${dropText}\n${hint}` : dropText }] };
964
974
  })
965
975
  );
966
976
 
package/tool-schemas.mjs CHANGED
@@ -191,13 +191,20 @@ const coerceDeferredTokens = z.preprocess(
191
191
  ])).min(1).max(20)
192
192
  );
193
193
 
194
- // Coerce supersedes input β€” array of positive observation ids (accept numeric
195
- // strings from MCP bridges that JSON-stringify ints). Empty/other shapes reject.
194
+ // Coerce supersedes input β€” positive observation ids, plus `E#<n>` for an EVENT row
195
+ // (D#205), which is the prefix those rows are rendered with in the injected lessons
196
+ // block. Numeric strings are accepted because some MCP bridges JSON-stringify ints;
197
+ // `E#<n>` is kept as a STRING and split by `splitSupersedeTokens`, since coercing it to
198
+ // a number here would lose the only thing that says which table it names.
199
+ // Empty/other shapes reject.
196
200
  const coerceSupersedes = z.preprocess(
197
201
  (v) => (Array.isArray(v)
198
202
  ? v.map(x => (typeof x === 'string' && /^\d+$/.test(x.trim()) ? parseInt(x.trim(), 10) : x))
199
203
  : v),
200
- z.array(z.number().int().positive()).min(1).max(20)
204
+ z.array(z.union([
205
+ z.number().int().positive(),
206
+ z.string().regex(/^[Ee]#?\d+$/, 'expected a positive observation id or E#<n> for an event'),
207
+ ])).min(1).max(20)
201
208
  );
202
209
 
203
210
  export const memSaveSchema = {
@@ -216,7 +223,7 @@ export const memSaveSchema = {
216
223
  files: coerceStringArray.optional().describe('File paths associated with this observation. Stored in the `files_modified` column and rendered as `files` β€” passing a path here does not assert the file was edited; a file you only read belongs here too'),
217
224
  lesson_learned: z.string().max(500).optional().describe('Key lesson or takeaway, ≀500 chars (for bugfix: root cause & fix; for decision: rationale)'),
218
225
  closes_deferred: coerceDeferredTokens.optional().describe('Close one or more deferred_work items in the same project. Mixed array: bare integer = ordinal-within-project, "D#<n>" string = raw id. Transactional with the obs insert β€” a single invalid id rolls back the whole save.'),
219
- supersedes: coerceSupersedes.optional().describe('Observation ids (same project) that this save overturns. They are marked superseded β€” dropped from live search β€” and linked to the new row (superseded_by). Use ONLY when this genuinely replaces a prior conclusion; do NOT use for merely-related or updated-but-still-valid memories.'),
226
+ supersedes: coerceSupersedes.optional().describe('Ids (same project) that this save overturns: a bare number for an observation, or E#<n> for an event β€” the same prefix events are shown with in the injected lessons block, so you can retire one by typing back what you read. They are marked superseded (dropped from live search); observations are also linked to the new row via superseded_by, events are not, because that column can only reference another event. Use ONLY when this genuinely replaces a prior conclusion; do NOT use for merely-related or updated-but-still-valid memories.'),
220
227
  };
221
228
 
222
229
  export const memStatsSchema = {