@pcircle/memesh 4.9.4 → 4.10.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +35 -4
- package/README.de.md +3 -3
- package/README.md +3 -3
- package/README.zh-TW.md +3 -3
- package/dashboard/dist/index.html +8 -8
- package/dist/cli/view-live.d.ts.map +1 -1
- package/dist/cli/view-live.js +0 -427
- package/dist/cli/view-live.js.map +1 -1
- package/dist/core/briefing-index.d.ts +30 -0
- package/dist/core/briefing-index.d.ts.map +1 -0
- package/dist/core/briefing-index.js +140 -0
- package/dist/core/briefing-index.js.map +1 -0
- package/dist/core/briefing.d.ts +4 -0
- package/dist/core/briefing.d.ts.map +1 -1
- package/dist/core/briefing.js +42 -4
- package/dist/core/briefing.js.map +1 -1
- package/dist/core/capture-liveness.d.ts +136 -0
- package/dist/core/capture-liveness.d.ts.map +1 -0
- package/dist/core/capture-liveness.js +447 -0
- package/dist/core/capture-liveness.js.map +1 -0
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +3 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/delegation.d.ts +47 -0
- package/dist/core/delegation.d.ts.map +1 -0
- package/dist/core/delegation.js +174 -0
- package/dist/core/delegation.js.map +1 -0
- package/dist/core/doctor-fixes.d.ts +1 -0
- package/dist/core/doctor-fixes.d.ts.map +1 -1
- package/dist/core/doctor-fixes.js +21 -0
- package/dist/core/doctor-fixes.js.map +1 -1
- package/dist/core/doctor.d.ts +8 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +148 -3
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/note-derive.d.ts +14 -0
- package/dist/core/note-derive.d.ts.map +1 -0
- package/dist/core/note-derive.js +66 -0
- package/dist/core/note-derive.js.map +1 -0
- package/dist/core/note-ingest.d.ts +34 -0
- package/dist/core/note-ingest.d.ts.map +1 -0
- package/dist/core/note-ingest.js +445 -0
- package/dist/core/note-ingest.js.map +1 -0
- package/dist/core/operations.d.ts +10 -1
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +129 -14
- package/dist/core/operations.js.map +1 -1
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js.map +1 -1
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +10 -4
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/serializer.d.ts.map +1 -1
- package/dist/core/serializer.js +3 -0
- package/dist/core/serializer.js.map +1 -1
- package/dist/core/session-insight.d.ts +44 -0
- package/dist/core/session-insight.d.ts.map +1 -0
- package/dist/core/session-insight.js +216 -0
- package/dist/core/session-insight.js.map +1 -0
- package/dist/core/task-state-store.d.ts +4 -0
- package/dist/core/task-state-store.d.ts.map +1 -1
- package/dist/core/task-state-store.js +18 -5
- package/dist/core/task-state-store.js.map +1 -1
- package/dist/core/turn-signal.d.ts +22 -0
- package/dist/core/turn-signal.d.ts.map +1 -0
- package/dist/core/turn-signal.js +89 -0
- package/dist/core/turn-signal.js.map +1 -0
- package/dist/core/types.d.ts +12 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/update-entrypoint.d.ts +19 -0
- package/dist/core/update-entrypoint.d.ts.map +1 -0
- package/dist/core/update-entrypoint.js +222 -0
- package/dist/core/update-entrypoint.js.map +1 -0
- package/dist/core/update-notice.d.ts +69 -0
- package/dist/core/update-notice.d.ts.map +1 -0
- package/dist/core/update-notice.js +188 -0
- package/dist/core/update-notice.js.map +1 -0
- package/dist/host-runtime/codex-session.d.ts +10 -0
- package/dist/host-runtime/codex-session.d.ts.map +1 -1
- package/dist/host-runtime/codex-session.js +1 -1
- package/dist/host-runtime/codex-session.js.map +1 -1
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +24 -3
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/mcp/THIRD_PARTY_NOTICES.txt +2 -2
- package/dist/mcp/server.js +1079 -90
- package/dist/mcp/server.js.map +3 -3
- package/dist/skills-manifest.json +46 -26
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +7702 -5764
- package/dist/transports/cli/cli.js.map +3 -3
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +38 -27
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +25 -5
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +43 -5
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +4 -2
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +29 -3
- package/dist/transports/schemas.js.map +1 -1
- package/docs/platforms/README.md +1 -0
- package/docs/platforms/agent-messaging.md +22 -5
- package/package.json +14 -4
- package/scripts/hooks/_generated/briefing-index.js +147 -0
- package/scripts/hooks/_generated/capture-liveness.js +454 -0
- package/scripts/hooks/_generated/update-notice.js +195 -0
- package/scripts/hooks/_shared.js +302 -14
- package/scripts/hooks/_stop-notes.js +504 -0
- package/scripts/hooks/auto-update-runner.mjs +22 -0
- package/scripts/hooks/decision-nudge.js +32 -5
- package/scripts/hooks/guard-check.js +30 -3
- package/scripts/hooks/post-commit.js +329 -133
- package/scripts/hooks/pre-compact.js +17 -2
- package/scripts/hooks/pre-edit-recall.js +32 -1
- package/scripts/hooks/session-start.js +361 -78
- package/scripts/hooks/session-summary.js +217 -41
- package/scripts/hooks/user-prompt-intent.js +89 -19
- package/skills/memesh/SKILL.md +20 -8
- package/dist/cli/assets/d3.v7.min.js +0 -2
- package/dist/core/graph.d.ts +0 -26
- package/dist/core/graph.d.ts.map +0 -1
- package/dist/core/graph.js +0 -74
- package/dist/core/graph.js.map +0 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
import { createRequire } from 'module';
|
|
8
8
|
import { basename, join } from 'path';
|
|
9
|
-
import { existsSync, readFileSync } from 'fs';
|
|
9
|
+
import { existsSync, readFileSync, writeSync } from 'fs';
|
|
10
10
|
import { pathToFileURL } from 'url';
|
|
11
11
|
import {
|
|
12
12
|
AUTO_CAPTURE_TAG,
|
|
@@ -20,13 +20,17 @@ import {
|
|
|
20
20
|
openHookDb,
|
|
21
21
|
readUpdateCheckCache,
|
|
22
22
|
redactSecrets,
|
|
23
|
+
hookErrorReason,
|
|
24
|
+
recordHookOutcome,
|
|
23
25
|
recordHookRun,
|
|
24
26
|
stampHookRunOnly,
|
|
25
27
|
resolveAutoUpdatePolicy,
|
|
26
28
|
resolvePluginRoot,
|
|
29
|
+
SKIP_REASONS,
|
|
27
30
|
spawnAutoUpdate,
|
|
28
31
|
truncateTitle,
|
|
29
32
|
} from './_shared.js';
|
|
33
|
+
import { runStopNotes } from './_stop-notes.js';
|
|
30
34
|
|
|
31
35
|
const require = createRequire(import.meta.url);
|
|
32
36
|
|
|
@@ -211,13 +215,22 @@ function parseTranscript(transcriptPath) {
|
|
|
211
215
|
let input = '';
|
|
212
216
|
process.stdin.setEncoding('utf8');
|
|
213
217
|
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
218
|
+
// See post-commit.js for why every exit path leaves a record (#327).
|
|
219
|
+
let payload = null;
|
|
220
|
+
let pendingSystemMessage = null;
|
|
221
|
+
/** Set by runStopNotes; called by exit0() with whether the nudge went out. */
|
|
222
|
+
let settleNudge = null;
|
|
223
|
+
function record(outcome, reason, entity) {
|
|
224
|
+
recordHookOutcome(process.env, { hook: 'session-summary', outcome, reason, entity, payload });
|
|
225
|
+
}
|
|
226
|
+
|
|
214
227
|
process.stdin.on('end', async () => {
|
|
215
228
|
let sessionId = 'unknown';
|
|
216
229
|
try {
|
|
217
|
-
if (!input.trim())
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
230
|
+
if (!input.trim()) {
|
|
231
|
+
record('skipped', SKIP_REASONS.emptyStdin);
|
|
232
|
+
return exit0();
|
|
233
|
+
}
|
|
221
234
|
|
|
222
235
|
let inputData;
|
|
223
236
|
try {
|
|
@@ -230,12 +243,37 @@ process.stdin.on('end', async () => {
|
|
|
230
243
|
const preview = (input || '').slice(0, 80).replace(/\n/g, ' ');
|
|
231
244
|
process.stderr.write(`[memesh session-summary] malformed stdin JSON (len=${input.length}): ${parseErr?.message || parseErr}; preview="${preview}"\n`);
|
|
232
245
|
} catch {}
|
|
246
|
+
record('error', 'malformed stdin JSON');
|
|
233
247
|
return exit0();
|
|
234
248
|
}
|
|
235
249
|
|
|
250
|
+
payload = inputData;
|
|
236
251
|
sessionId = inputData.session_id || 'unknown';
|
|
237
252
|
const transcriptPath = inputData.transcript_path;
|
|
238
253
|
|
|
254
|
+
// Opt-out check (env > config > default-on). Read before the note work
|
|
255
|
+
// below because ingestion WRITES memories and must honour it; the nudge
|
|
256
|
+
// writes nothing and runs either way (#324). It used to sit above the
|
|
257
|
+
// JSON parse, which is why the payload is parsed first now.
|
|
258
|
+
const captureEnabled = isAutoCaptureEnabled(process.env);
|
|
259
|
+
|
|
260
|
+
// #324: ingest the project's note directory and decide the
|
|
261
|
+
// "decided things, stored nothing" nudge. Records its own outcomes under
|
|
262
|
+
// `note-ingest` / `remember-nudge`, never throws, and only ever yields
|
|
263
|
+
// one line for exit0() to print.
|
|
264
|
+
const stopNotes = await runStopNotes(inputData, {
|
|
265
|
+
captureEnabled,
|
|
266
|
+
project: inputData.cwd ? getProjectName(inputData.cwd) : undefined,
|
|
267
|
+
metaUrl: import.meta.url,
|
|
268
|
+
});
|
|
269
|
+
pendingSystemMessage = stopNotes.message;
|
|
270
|
+
settleNudge = stopNotes.settle;
|
|
271
|
+
|
|
272
|
+
if (!captureEnabled) {
|
|
273
|
+
record('skipped', SKIP_REASONS.autoCaptureOff);
|
|
274
|
+
return exit0();
|
|
275
|
+
}
|
|
276
|
+
|
|
239
277
|
// `cwd` decides the project tag, and the project tag decides which
|
|
240
278
|
// sessions `session-start` injects and which memories `pre-edit-recall`
|
|
241
279
|
// surfaces. Falling back to `process.cwd()` — the hook process's launch
|
|
@@ -249,6 +287,7 @@ process.stdin.on('end', async () => {
|
|
|
249
287
|
// capture than to file it under the wrong project. Same rule here.
|
|
250
288
|
if (!inputData.cwd) {
|
|
251
289
|
try { process.stderr.write(`[memesh session-summary] cwd absent in payload (keys: ${Object.keys(inputData).join(',')}); cannot resolve project, skipping capture\n`); } catch {}
|
|
290
|
+
record('skipped', SKIP_REASONS.cwdAbsent);
|
|
252
291
|
return exit0();
|
|
253
292
|
}
|
|
254
293
|
const cwd = inputData.cwd;
|
|
@@ -279,7 +318,11 @@ process.stdin.on('end', async () => {
|
|
|
279
318
|
// (empty stdin, malformed JSON, missing cwd) are schema-flip shapes: if
|
|
280
319
|
// Claude Code's payload changed under us, capture is effectively dead,
|
|
281
320
|
// and a heartbeat would mask exactly that.
|
|
282
|
-
if (!wasAgenticLoop) {
|
|
321
|
+
if (!wasAgenticLoop) {
|
|
322
|
+
stampHookRunOnly(process.env, 'session-summary');
|
|
323
|
+
record('skipped', SKIP_REASONS.notAgenticLoop);
|
|
324
|
+
return exit0();
|
|
325
|
+
}
|
|
283
326
|
// Trace why we're skipping. Two failure modes:
|
|
284
327
|
// (a) transcript_path absent — schema flip, Claude Code stopped
|
|
285
328
|
// sending the field. Same bug shape as `was_in_agentic_loop`
|
|
@@ -291,6 +334,7 @@ process.stdin.on('end', async () => {
|
|
|
291
334
|
// breadcrumb so a schema flip doesn't ship undetected again.
|
|
292
335
|
if (!transcriptPath) {
|
|
293
336
|
try { process.stderr.write(`[memesh session-summary] transcript_path absent in payload (keys: ${Object.keys(inputData).join(',')}); skipping capture\n`); } catch {}
|
|
337
|
+
record('skipped', SKIP_REASONS.transcriptPathAbsent);
|
|
294
338
|
return exit0();
|
|
295
339
|
}
|
|
296
340
|
if (!existsSync(transcriptPath)) {
|
|
@@ -299,6 +343,7 @@ process.stdin.on('end', async () => {
|
|
|
299
343
|
// race) — the hook itself ran fine, so this stamps. A payload that
|
|
300
344
|
// never carried the field at all (schema flip) bails above, unstamped.
|
|
301
345
|
stampHookRunOnly(process.env, 'session-summary');
|
|
346
|
+
record('skipped', SKIP_REASONS.transcriptGone);
|
|
302
347
|
return exit0();
|
|
303
348
|
}
|
|
304
349
|
|
|
@@ -310,11 +355,18 @@ process.stdin.on('end', async () => {
|
|
|
310
355
|
// LOST (permissions, I/O), and a heartbeat here would keep doctor green
|
|
311
356
|
// through exactly the repeated failure it exists to expose. No stamp —
|
|
312
357
|
// parseTranscript already traced the fault to stderr.
|
|
313
|
-
if (readFailed)
|
|
358
|
+
if (readFailed) {
|
|
359
|
+
record('error', 'the transcript could not be read');
|
|
360
|
+
return exit0();
|
|
361
|
+
}
|
|
314
362
|
|
|
315
363
|
// Skip sessions with too little activity — the single most common
|
|
316
364
|
// healthy exit, so it MUST stamp (see stampHookRunOnly).
|
|
317
|
-
if (toolCallCount < 3) {
|
|
365
|
+
if (toolCallCount < 3) {
|
|
366
|
+
stampHookRunOnly(process.env, 'session-summary');
|
|
367
|
+
record('skipped', SKIP_REASONS.tooLittleActivity);
|
|
368
|
+
return exit0();
|
|
369
|
+
}
|
|
318
370
|
|
|
319
371
|
const projectName = getProjectName(cwd);
|
|
320
372
|
|
|
@@ -324,35 +376,23 @@ process.stdin.on('end', async () => {
|
|
|
324
376
|
//
|
|
325
377
|
const { db } = openHookDb(process.env, { fts: true });
|
|
326
378
|
let writeFailed = false;
|
|
379
|
+
let firstFailedEntity = null;
|
|
380
|
+
// True once any of the three rules below actually calls storeMemory.
|
|
381
|
+
// Between the toolCallCount < 3 guard above and Rule 3's >= 20 bar, a
|
|
382
|
+
// session that ran real commands but edited no file matches none of
|
|
383
|
+
// them — storeMemory never runs, writeFailed stays false, and without
|
|
384
|
+
// this flag the outcome below fell through to record('wrote') anyway:
|
|
385
|
+
// a claimed write with zero entities actually touched.
|
|
386
|
+
let anyRuleMatched = false;
|
|
387
|
+
// True only once captureEntity actually lands a write. A matched rule
|
|
388
|
+
// whose entity is `forget`-archived sets anyRuleMatched but not this —
|
|
389
|
+
// the archived branch below returns before either flag changes, so a
|
|
390
|
+
// Stop where every matched rule's target was archived falls through to
|
|
391
|
+
// the `!anyWrote` branch instead of a false 'wrote' (same bug shape as
|
|
392
|
+
// the noRuleMatched fix above, one level deeper).
|
|
393
|
+
let anyWrote = false;
|
|
394
|
+
let lastWrittenEntity = null;
|
|
327
395
|
try {
|
|
328
|
-
// Duplicate detection: if we already captured this session, bail.
|
|
329
|
-
//
|
|
330
|
-
// Use the FULL session_id rather than the first 8 chars: real
|
|
331
|
-
// Claude Code UUIDs collide on 8 chars only with cosmically small
|
|
332
|
-
// probability, but artificial test IDs (verify-fix-001 vs -002)
|
|
333
|
-
// share the prefix and silently skipped the second session
|
|
334
|
-
// entirely. The contract is one stored capture per distinct
|
|
335
|
-
// session_id, so the dedup key has to be the full id.
|
|
336
|
-
//
|
|
337
|
-
// A dedup bail is a SUCCESSFUL run — the loop executed and correctly
|
|
338
|
-
// decided there was nothing to do — so it stamps the heartbeat like
|
|
339
|
-
// the capture path below does. Only a throw leaves no stamp.
|
|
340
|
-
// Guard on ANY of this session's three entities, not only `-files`.
|
|
341
|
-
// A Bash-only session created no `-files` row, so the guard never
|
|
342
|
-
// tripped and `-summary` was re-appended on every Stop — measured: 56
|
|
343
|
-
// observations, 16 unique, three commands stored fourteen times each.
|
|
344
|
-
const alreadyCaptured = db.prepare(
|
|
345
|
-
"SELECT id FROM entities WHERE name IN (?, ?, ?) LIMIT 1",
|
|
346
|
-
).get(`session-${sessionId}-files`, `session-${sessionId}-fixes`, `session-${sessionId}-summary`);
|
|
347
|
-
if (alreadyCaptured) {
|
|
348
|
-
recordHookRun(db, 'session-summary');
|
|
349
|
-
// A duplicate capture is still a completed Stop lifecycle. Update
|
|
350
|
-
// consent is session-scoped and must not be skipped merely because
|
|
351
|
-
// the same transcript was observed twice (a common host retry).
|
|
352
|
-
await runAutoUpdateAtStop(sessionId);
|
|
353
|
-
return exit0();
|
|
354
|
-
}
|
|
355
|
-
|
|
356
396
|
// Build and store session memories
|
|
357
397
|
const baseTags = [AUTO_CAPTURE_TAG, `session:${sessionId}`, `project:${projectName}`];
|
|
358
398
|
|
|
@@ -378,12 +418,51 @@ process.stdin.on('end', async () => {
|
|
|
378
418
|
// entities_fts too. This copy used to insert entity + observations + tags
|
|
379
419
|
// only, skipping the FTS reindex the sibling hooks did — which left every
|
|
380
420
|
// session-insight memory unrecallable via the FTS keyword path.
|
|
421
|
+
//
|
|
422
|
+
// Known tradeoff, not a bug: the three Rule blocks below each call this
|
|
423
|
+
// function independently, and captureEntity() commits its own
|
|
424
|
+
// transaction per call. A failure partway through Rule 2 or 3 can leave
|
|
425
|
+
// an earlier entity (e.g. -files) replaced while a later one is not,
|
|
426
|
+
// even though the overall Stop is recorded as 'error'. Wrapping all
|
|
427
|
+
// three in one outer db.transaction() would close that gap (nested
|
|
428
|
+
// calls become SAVEPOINTs — see MemeshDatabase.transaction() in
|
|
429
|
+
// src/storage/sqlite.ts) but was deliberately not done here: it widens
|
|
430
|
+
// the write-lock hold on every Stop (this hook's busy_timeout is
|
|
431
|
+
// shorter than the harness timeout on purpose), to guard a failure mode
|
|
432
|
+
// that self-heals — the next Stop rebuilds each entity fresh from the
|
|
433
|
+
// transcript, since these are snapshots, not accumulations.
|
|
381
434
|
function storeMemory(name, type, observations, tags, title) {
|
|
435
|
+
anyRuleMatched = true;
|
|
436
|
+
// `replace`: these three entities are a SNAPSHOT of one session, and
|
|
437
|
+
// Stop fires at the end of every turn. Appending stored the same
|
|
438
|
+
// sentences on every turn; skipping after the first froze a two-day
|
|
439
|
+
// session at its first turn (#322). A snapshot is restated, not added
|
|
440
|
+
// to.
|
|
441
|
+
const result = captureEntity(db, { name, type, observations, tags, title, replace: true });
|
|
442
|
+
if (result?.archived) {
|
|
443
|
+
// The user `forget`-archived this exact entity. Not a failure —
|
|
444
|
+
// captureEntity's contract left it untouched on purpose — so it
|
|
445
|
+
// must not set writeFailed (that would misreport an honoured
|
|
446
|
+
// `forget` as a broken hook). Traced, not silent (#3d): the next
|
|
447
|
+
// Stop will try again and say the same thing until the user either
|
|
448
|
+
// reactivates the entity or the session ends.
|
|
449
|
+
try { process.stderr.write(`MeMesh: session-summary left "${name}" alone — archived by forget.\n`); } catch {}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
382
452
|
// null = the entity row could not be resolved = this write did NOT
|
|
383
453
|
// happen (captureEntity's contract). A run with a failed write must
|
|
384
454
|
// not stamp the heartbeat below — "alive" would be a lie about the
|
|
385
455
|
// exact thing the heartbeat certifies.
|
|
386
|
-
if (!
|
|
456
|
+
if (!result) {
|
|
457
|
+
writeFailed = true;
|
|
458
|
+
// First failure, not last: with three independent per-entity
|
|
459
|
+
// transactions, the first is the root cause — later calls run
|
|
460
|
+
// regardless and naming one of them would point at a symptom.
|
|
461
|
+
if (firstFailedEntity === null) firstFailedEntity = name;
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
anyWrote = true;
|
|
465
|
+
lastWrittenEntity = name;
|
|
387
466
|
}
|
|
388
467
|
|
|
389
468
|
// No free-form human text exists for these three entities the way a
|
|
@@ -393,7 +472,8 @@ process.stdin.on('end', async () => {
|
|
|
393
472
|
const titleDate = new Date().toISOString().slice(0, 10);
|
|
394
473
|
const titlePrefix = `${titleDate} ${projectName}`;
|
|
395
474
|
|
|
396
|
-
// Rule 1: File editing session summary
|
|
475
|
+
// Rule 1: File editing session summary — name uses the FULL
|
|
476
|
+
// session_id (tests/core/extractor.test.ts pins why).
|
|
397
477
|
if (filesEdited.length > 0) {
|
|
398
478
|
storeMemory(
|
|
399
479
|
`session-${sessionId}-files`,
|
|
@@ -421,7 +501,15 @@ process.stdin.on('end', async () => {
|
|
|
421
501
|
);
|
|
422
502
|
}
|
|
423
503
|
|
|
424
|
-
// Rule 3: Heavy session summary (20+ tool calls = significant work)
|
|
504
|
+
// Rule 3: Heavy session summary (20+ tool calls = significant work).
|
|
505
|
+
// This literal is the one place that actually decides the bar; two
|
|
506
|
+
// doc strings describe it in prose without importing it (this file is
|
|
507
|
+
// plain JS with no shared constant module, and capture-liveness.ts is
|
|
508
|
+
// a deliberate zero-import leaf) — src/core/capture-liveness.ts's
|
|
509
|
+
// SKIP_REASONS.noRuleMatched and src/core/session-insight.ts's own
|
|
510
|
+
// (HEAVY_SESSION_TOOL_CALLS-derived) copy. A future change to this
|
|
511
|
+
// number needs both updated by hand, or doctor's text will drift from
|
|
512
|
+
// what actually happened.
|
|
425
513
|
if (toolCallCount >= 20) {
|
|
426
514
|
storeMemory(
|
|
427
515
|
`session-${sessionId}-summary`,
|
|
@@ -548,9 +636,42 @@ process.stdin.on('end', async () => {
|
|
|
548
636
|
// session so it survives DB restores from either era) plus
|
|
549
637
|
// the compliance denominators: sessions that HAD an injection
|
|
550
638
|
// vs sessions whose transcript carried any citation marker.
|
|
639
|
+
//
|
|
640
|
+
// The counters are scoped to the generation named in the stamp.
|
|
641
|
+
// When a graph that counted under an EARLIER generation meets
|
|
642
|
+
// this one, they are cleared rather than added to: the
|
|
643
|
+
// numerator changed meaning (v1 asked "did the transcript
|
|
644
|
+
// contain any marker", v2 asks "was an id we injected cited"),
|
|
645
|
+
// so a sum across both is one ratio wearing the newer label and
|
|
646
|
+
// no key separates the eras. Readers — analytics.ts and
|
|
647
|
+
// scripts/audit/measure-signals.mjs — read the bare keys and
|
|
648
|
+
// therefore keep working unchanged; what they report is now
|
|
649
|
+
// this generation only. A graph that never counted has no
|
|
650
|
+
// stamp, so nothing resets on a new install.
|
|
651
|
+
const ACCOUNTING_MODE = 'citation-v2 since 2026-09-12';
|
|
652
|
+
const priorMode = db.prepare(
|
|
653
|
+
"SELECT value FROM memesh_metadata WHERE key = 'recall_accounting_mode'"
|
|
654
|
+
).get()?.value;
|
|
655
|
+
if (priorMode && priorMode !== ACCOUNTING_MODE) {
|
|
656
|
+
// Traced, not silently dropped: the numbers being discarded
|
|
657
|
+
// are the only record of the previous era, and a reset that
|
|
658
|
+
// leaves no result record is the silent-skip shape.
|
|
659
|
+
const prior = (key) => db.prepare('SELECT value FROM memesh_metadata WHERE key = ?').get(key)?.value ?? 'absent';
|
|
660
|
+
const priorTotal = prior('citation_sessions_total');
|
|
661
|
+
const priorCited = prior('citation_sessions_cited');
|
|
662
|
+
db.prepare(
|
|
663
|
+
"DELETE FROM memesh_metadata WHERE key IN ('citation_sessions_total', 'citation_sessions_cited')"
|
|
664
|
+
).run();
|
|
665
|
+
try {
|
|
666
|
+
process.stderr.write(
|
|
667
|
+
`[memesh session-summary] citation accounting generation changed (${priorMode} -> ${ACCOUNTING_MODE}); ` +
|
|
668
|
+
`counters reset from total=${priorTotal} cited=${priorCited} — the two eras count different things and are not comparable.\n`,
|
|
669
|
+
);
|
|
670
|
+
} catch {}
|
|
671
|
+
}
|
|
551
672
|
db.prepare(
|
|
552
673
|
'INSERT OR REPLACE INTO memesh_metadata (key, value) VALUES (?, ?)'
|
|
553
|
-
).run('recall_accounting_mode',
|
|
674
|
+
).run('recall_accounting_mode', ACCOUNTING_MODE);
|
|
554
675
|
const bump = db.prepare(
|
|
555
676
|
`INSERT INTO memesh_metadata (key, value) VALUES (?, '1')
|
|
556
677
|
ON CONFLICT(key) DO UPDATE SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT)`
|
|
@@ -584,7 +705,39 @@ process.stdin.on('end', async () => {
|
|
|
584
705
|
// write did not land must not read as alive. (The recall-effectiveness
|
|
585
706
|
// block catches its own errors — session memories were already stored
|
|
586
707
|
// by then, so the run still counts.)
|
|
587
|
-
if (
|
|
708
|
+
if (writeFailed) {
|
|
709
|
+
// Name the entity whose captureEntity call actually returned null —
|
|
710
|
+
// not a fixed guess. With three independent per-entity writes, a
|
|
711
|
+
// hardcoded name here would point at the wrong one whenever the
|
|
712
|
+
// failure was in Rule 1 or 2.
|
|
713
|
+
record('error', 'captureEntity did not land the write', firstFailedEntity ?? undefined);
|
|
714
|
+
} else if (!anyRuleMatched) {
|
|
715
|
+
// Correctly deciding there was nothing to capture is still a
|
|
716
|
+
// completed run — same stance as the tooLittleActivity skip above,
|
|
717
|
+
// which stamps too. What it must NOT do is claim 'wrote': that was
|
|
718
|
+
// this hook's shape for every real-work-but-no-file-edit session
|
|
719
|
+
// until this branch existed.
|
|
720
|
+
recordHookRun(db, 'session-summary');
|
|
721
|
+
// No entity named: by definition no rule matched, so `-files`,
|
|
722
|
+
// `-fixes` and `-summary` are all equally untouched this Stop —
|
|
723
|
+
// naming one of them would misreport which entity this record is
|
|
724
|
+
// about.
|
|
725
|
+
record('skipped', SKIP_REASONS.noRuleMatched);
|
|
726
|
+
} else if (!anyWrote) {
|
|
727
|
+
// A rule DID match, but every entity it targeted was `forget`-
|
|
728
|
+
// archived — the same false-'wrote' shape as the branch above, one
|
|
729
|
+
// level deeper (a matched rule that produced no write). Its own
|
|
730
|
+
// reason, not noRuleMatched: a rule fired, saying otherwise would
|
|
731
|
+
// hide that.
|
|
732
|
+
recordHookRun(db, 'session-summary');
|
|
733
|
+
record('skipped', SKIP_REASONS.allMatchedEntitiesArchived);
|
|
734
|
+
} else {
|
|
735
|
+
recordHookRun(db, 'session-summary');
|
|
736
|
+
// Name the entity that actually landed the write (the last one, if
|
|
737
|
+
// more than one rule wrote) — not a fixed guess at which of the
|
|
738
|
+
// three this Stop touched.
|
|
739
|
+
record('wrote', undefined, lastWrittenEntity ?? undefined);
|
|
740
|
+
}
|
|
588
741
|
} finally {
|
|
589
742
|
db.close();
|
|
590
743
|
}
|
|
@@ -596,6 +749,7 @@ process.stdin.on('end', async () => {
|
|
|
596
749
|
// failures from this hook; with that branch gone, real capture errors stay
|
|
597
750
|
// visible without crashing the host session.
|
|
598
751
|
try { process.stderr.write(`[memesh session-summary] ${err?.message || err}\n`); } catch {}
|
|
752
|
+
record('error', hookErrorReason(err));
|
|
599
753
|
}
|
|
600
754
|
|
|
601
755
|
// Update only after all session work so installed files cannot change while
|
|
@@ -619,6 +773,28 @@ process.stdin.on('end', async () => {
|
|
|
619
773
|
});
|
|
620
774
|
|
|
621
775
|
function exit0() {
|
|
776
|
+
// The one thing this hook may print (#324): the nudge. `systemMessage` is
|
|
777
|
+
// the only Stop output Claude Code shows the user (Stop has no
|
|
778
|
+
// hookSpecificOutput variant — tests/helpers/hook-output-contract.ts).
|
|
779
|
+
// Codex's acceptance of it on Stop is NOT verified against a live Codex in
|
|
780
|
+
// this repository; tests/hooks/cross-host-output-contract.test.ts pins the
|
|
781
|
+
// exact envelope so a rejection report maps to one line. `suppressOutput`,
|
|
782
|
+
// which Codex did reject, stays gone. writeSync, not console.log: stdout is a
|
|
783
|
+
// pipe, and an async pipe write can be cut off by process.exit on macOS.
|
|
784
|
+
//
|
|
785
|
+
// The nudge's outcome is decided HERE, by whether the write succeeded —
|
|
786
|
+
// not by runStopNotes, which cannot know. A host that closed stdout gets
|
|
787
|
+
// an `error` record and keeps its transcript offset, so the next Stop
|
|
788
|
+
// judges the same window again instead of losing it to a line nobody read.
|
|
789
|
+
let delivered = true;
|
|
790
|
+
if (pendingSystemMessage) {
|
|
791
|
+
try {
|
|
792
|
+
writeSync(1, `${JSON.stringify({ systemMessage: pendingSystemMessage })}\n`);
|
|
793
|
+
} catch {
|
|
794
|
+
delivered = false; // host closed stdout; the record says so.
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
try { settleNudge?.(delivered); } catch { /* diagnostics never take the hook down */ }
|
|
622
798
|
process.exit(0);
|
|
623
799
|
}
|
|
624
800
|
/**
|
|
@@ -21,11 +21,19 @@
|
|
|
21
21
|
import { pathToFileURL } from 'url';
|
|
22
22
|
import {
|
|
23
23
|
findAutoUpdateConsent,
|
|
24
|
+
importFromPluginRoot,
|
|
24
25
|
isAutoCaptureEnabled,
|
|
26
|
+
hookErrorReason,
|
|
27
|
+
SKIP_REASONS,
|
|
28
|
+
recordHookOutcome,
|
|
29
|
+
markUpdatePromptAnswered,
|
|
30
|
+
memeshDir,
|
|
25
31
|
parseAutoUpdateConsent,
|
|
26
32
|
readUpdateCheckCache,
|
|
33
|
+
readUpdatePromptClaim,
|
|
27
34
|
resolvePluginRoot,
|
|
28
35
|
writeAutoUpdateConsent,
|
|
36
|
+
writeSnooze,
|
|
29
37
|
} from './_shared.js';
|
|
30
38
|
import { join } from 'path';
|
|
31
39
|
import { existsSync, readFileSync } from 'fs';
|
|
@@ -56,24 +64,64 @@ function currentInstalledVersion() {
|
|
|
56
64
|
} catch { return null; }
|
|
57
65
|
}
|
|
58
66
|
|
|
59
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Record the owner's answer to this session's first-use update notice.
|
|
69
|
+
*
|
|
70
|
+
* Returns 'approved' | 'declined' | 'never' | null. The answer counts only
|
|
71
|
+
* when THIS session was actually shown the notice (a prompt claim exists for
|
|
72
|
+
* session/current/latest), so a stray "no" in unrelated conversation is not
|
|
73
|
+
* a decision about updates.
|
|
74
|
+
*
|
|
75
|
+
* approved — npm-global only (the one channel with a hook-owned installer);
|
|
76
|
+
* other channels were told their manual action and cannot be
|
|
77
|
+
* approved into an install.
|
|
78
|
+
* declined — "Not now": escalating snooze for this target version, on
|
|
79
|
+
* every channel. A distinct newer target is offered again.
|
|
80
|
+
* never — "Never ask again": config.updateCheck = false; also snoozed so
|
|
81
|
+
* a host that already read the old config stays quiet.
|
|
82
|
+
*/
|
|
83
|
+
async function recordUpdateConsent(sessionId, prompt) {
|
|
60
84
|
const current = currentInstalledVersion();
|
|
61
85
|
if (!current || !sessionId) return null;
|
|
62
|
-
const channel = currentInstallChannel();
|
|
63
|
-
// Only npm-global has a hook-owned installer. Other channels receive an
|
|
64
|
-
// actionable notice at SessionStart and must not turn an "Upgrade" word
|
|
65
|
-
// into a misleading approval marker for a different installation path.
|
|
66
|
-
if (channel !== 'npm-global') return null;
|
|
67
86
|
const cache = readUpdateCheckCache(current);
|
|
68
87
|
const latest = cache?.latestVersion;
|
|
69
88
|
if (typeof latest !== 'string' || !latest) return null;
|
|
70
|
-
const pending = findAutoUpdateConsent(sessionId, current, latest, channel);
|
|
71
|
-
if (!pending || !['pending'].includes(pending.decision)) return null;
|
|
72
89
|
const decision = parseAutoUpdateConsent(prompt);
|
|
73
90
|
if (!decision) return null;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
91
|
+
const channel = currentInstallChannel();
|
|
92
|
+
const pending = channel === 'npm-global'
|
|
93
|
+
? findAutoUpdateConsent(sessionId, current, latest, channel)
|
|
94
|
+
: null;
|
|
95
|
+
const claim = readUpdatePromptClaim(sessionId, current, latest);
|
|
96
|
+
// The notice must have been shown to THIS session and not answered yet.
|
|
97
|
+
// Once answered, later "no"/"later" in ordinary conversation is not a
|
|
98
|
+
// decision about updates (it used to escalate the snooze every time).
|
|
99
|
+
const open = (claim && claim.decision !== 'answered') || pending?.decision === 'pending';
|
|
100
|
+
if (!open) return null;
|
|
101
|
+
|
|
102
|
+
if (decision === 'approved') {
|
|
103
|
+
if (!pending || pending.decision !== 'pending') return null;
|
|
104
|
+
if (!writeAutoUpdateConsent(sessionId, current, latest, pending.channel ?? channel, 'approved')) return null;
|
|
105
|
+
markUpdatePromptAnswered(sessionId, current, latest, 'approved');
|
|
106
|
+
return 'approved';
|
|
107
|
+
}
|
|
108
|
+
// declined | never
|
|
109
|
+
try { writeSnooze(memeshDir(), latest); } catch { /* best-effort */ }
|
|
110
|
+
if (pending?.decision === 'pending') {
|
|
111
|
+
writeAutoUpdateConsent(sessionId, current, latest, pending.channel ?? channel, 'declined');
|
|
112
|
+
}
|
|
113
|
+
let recorded = decision;
|
|
114
|
+
if (decision === 'never') {
|
|
115
|
+
try {
|
|
116
|
+
const configMod = await importFromPluginRoot(resolvePluginRoot(import.meta.url), 'dist/core/config.js');
|
|
117
|
+
configMod.updateConfig({ updateCheck: false });
|
|
118
|
+
} catch (err) {
|
|
119
|
+
logError('user-prompt-intent', `could not persist updateCheck=false: ${err?.message || err}`);
|
|
120
|
+
recorded = 'declined';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
markUpdatePromptAnswered(sessionId, current, latest, recorded);
|
|
124
|
+
return recorded;
|
|
77
125
|
}
|
|
78
126
|
|
|
79
127
|
// Patterns compiled at module load — invalid regex MUST fail loudly. Do
|
|
@@ -171,10 +219,20 @@ function logError(scope, msg) {
|
|
|
171
219
|
// every platform, so the comparison is portable.
|
|
172
220
|
const isMainModule = import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
173
221
|
if (isMainModule) {
|
|
222
|
+
// See post-commit.js for why every exit path leaves a record (#327). This
|
|
223
|
+
// hook's only effect is the additionalContext it injects, so its outcome is
|
|
224
|
+
// `notified`, not `wrote`: doctor's `writes` answers "is memory capture
|
|
225
|
+
// still alive", and an injected hint is not a memory. Both branches that
|
|
226
|
+
// reach it — a remember intent and an update-consent decision — inject and
|
|
227
|
+
// store nothing, so there is no writing branch here to keep.
|
|
228
|
+
let payload = null;
|
|
229
|
+
const record = (outcome, reason, entity) =>
|
|
230
|
+
recordHookOutcome(process.env, { hook: 'user-prompt-intent', outcome, reason, entity, payload });
|
|
231
|
+
|
|
174
232
|
let input = '';
|
|
175
233
|
process.stdin.setEncoding('utf8');
|
|
176
234
|
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
177
|
-
process.stdin.on('end', () => {
|
|
235
|
+
process.stdin.on('end', async () => {
|
|
178
236
|
try {
|
|
179
237
|
// Distinguish empty stdin (legitimate degenerate event) from malformed
|
|
180
238
|
// input (protocol drift). Both stay non-blocking, but only malformed
|
|
@@ -186,6 +244,7 @@ if (isMainModule) {
|
|
|
186
244
|
data = JSON.parse(trimmed);
|
|
187
245
|
} catch (parseErr) {
|
|
188
246
|
logError('user-prompt-intent', `malformed stdin JSON (len=${input.length}): ${parseErr.message}`);
|
|
247
|
+
record('error', 'malformed stdin JSON');
|
|
189
248
|
return process.exit(0);
|
|
190
249
|
}
|
|
191
250
|
}
|
|
@@ -194,26 +253,37 @@ if (isMainModule) {
|
|
|
194
253
|
// Claude Code's transcript format changed once before (2026-05-07), so
|
|
195
254
|
// we accept either name to survive a similar rename. If both are absent
|
|
196
255
|
// or non-string, detectRememberIntent's type guard returns false safely.
|
|
256
|
+
payload = data;
|
|
197
257
|
const prompt = data.prompt ?? data.user_prompt ?? '';
|
|
198
|
-
const updateDecision = recordUpdateConsent(data.session_id, prompt);
|
|
258
|
+
const updateDecision = await recordUpdateConsent(data.session_id, prompt);
|
|
199
259
|
const rememberIntent = detectRememberIntent(prompt);
|
|
200
|
-
if (!rememberIntent && !updateDecision)
|
|
260
|
+
if (!rememberIntent && !updateDecision) {
|
|
261
|
+
record('skipped', SKIP_REASONS.noPromptIntent);
|
|
262
|
+
return process.exit(0);
|
|
263
|
+
}
|
|
201
264
|
// Update consent is a user-authorized control decision, not memory
|
|
202
265
|
// capture; it must still be recorded when auto-capture is disabled.
|
|
203
|
-
if (!isAutoCaptureEnabled(process.env) && !updateDecision)
|
|
266
|
+
if (!isAutoCaptureEnabled(process.env) && !updateDecision) {
|
|
267
|
+
record('skipped', SKIP_REASONS.autoCaptureOff);
|
|
268
|
+
return process.exit(0);
|
|
269
|
+
}
|
|
204
270
|
|
|
205
271
|
const contexts = [];
|
|
206
|
-
if (updateDecision) {
|
|
207
|
-
contexts.push(
|
|
208
|
-
|
|
209
|
-
|
|
272
|
+
if (updateDecision === 'approved') {
|
|
273
|
+
contexts.push('The user explicitly approved the MeMesh upgrade. The Stop hook may now update the consented installation.');
|
|
274
|
+
} else if (updateDecision === 'declined') {
|
|
275
|
+
contexts.push('The user declined the MeMesh upgrade. It is snoozed for this target version (24h, then 48h, then 7 days on repeated declines); do not install it or mention it again unless a newer version appears.');
|
|
276
|
+
} else if (updateDecision === 'never') {
|
|
277
|
+
contexts.push('The user asked never to be asked about MeMesh updates again. updateCheck is now off; do not mention updates. `memesh config set updateCheck true` turns checks back on.');
|
|
210
278
|
}
|
|
211
279
|
if (rememberIntent) contexts.push(buildHint());
|
|
212
280
|
const out = { hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: contexts.join('\n\n') } };
|
|
213
281
|
process.stdout.write(JSON.stringify(out));
|
|
282
|
+
record('notified', undefined, `hint:${updateDecision ?? 'remember-intent'}`);
|
|
214
283
|
process.exit(0);
|
|
215
284
|
} catch (err) {
|
|
216
285
|
logError('user-prompt-intent', err?.message || err);
|
|
286
|
+
record('error', hookErrorReason(err));
|
|
217
287
|
process.exit(0);
|
|
218
288
|
}
|
|
219
289
|
});
|
package/skills/memesh/SKILL.md
CHANGED
|
@@ -28,14 +28,14 @@ All examples below use CLI. MCP tools accept the same parameters as JSON objects
|
|
|
28
28
|
| Tool | Purpose |
|
|
29
29
|
|---|---|
|
|
30
30
|
| `work_package` | Prepare one bounded untrusted `digest` (calendar cluster) or `transcript` package from the newest Claude Code session under the client's single matching MCP workspace root; submit exactly one strict result or defer. Submit only stages pending human review and retains bounded redacted source turns for comparison; agents cannot apply or reject. No hidden reasoning, raw transcript, transcript path, API key, LLM, embedding, or vector data is exposed or used; hashes identify freshness and workspace scope rather than authentication. |
|
|
31
|
-
| `remember` | Store knowledge as an entity with observations, tags, and relations |
|
|
31
|
+
| `remember` | Store knowledge as an entity with observations, tags, and relations; `note` (free text) derives title/observations/name; `replace: true` rewrites a named memory, keeping history |
|
|
32
32
|
| `recall` | Search stored knowledge; empty query lists recent memories |
|
|
33
33
|
| `forget` | Archive an entity or remove one exact observation |
|
|
34
34
|
| `export` | Export memories as portable JSON |
|
|
35
35
|
| `import` | Import a JSON export with the required skip, append, or overwrite strategy |
|
|
36
36
|
| `learn` | Record a structured lesson with error, fix, root cause, and prevention |
|
|
37
37
|
| `task_state` | Read or update user-stated goal, next step, blocker, and finished work |
|
|
38
|
-
| `briefing` | Assemble the current project's work topology |
|
|
38
|
+
| `briefing` | Assemble the current project's work topology, closing with a capped index of its durable memories |
|
|
39
39
|
| `user_patterns` | Analyze work schedule, tool preferences, and focus areas |
|
|
40
40
|
| `improvement` | Propose an evidence-linked product improvement or read its status; only a human may accept or reject it |
|
|
41
41
|
| `message` | Discover live agents in one project, then contact one exact recipient with a bounded, untrusted payload. Native size and availability failures are distinct; acceptance, discovery, polling, and fetching do not acknowledge |
|
|
@@ -101,6 +101,8 @@ Call the `briefing` MCP tool or run `memesh briefing`. It returns the assembled
|
|
|
101
101
|
work topology: where the work was left off (goal / next / blocked / done),
|
|
102
102
|
decisions and direction, lessons not to repeat, what is known, recent activity.
|
|
103
103
|
One call is cheaper than re-exploring the repo to reconstruct the same picture.
|
|
104
|
+
`memesh briefing --index` returns only the index of durable memories — what is
|
|
105
|
+
known here, one line each, without the ranked sections.
|
|
104
106
|
Generic briefing and SessionStart context do not report unread durable messages:
|
|
105
107
|
they have no recipient identity. If you already know the exact logical
|
|
106
108
|
recipient, pass `recipient` with `project` (MCP) or use
|
|
@@ -154,7 +156,7 @@ With the Claude Code plugin, the first eight rows happen **without any action fr
|
|
|
154
156
|
| **UserPromptSubmit** | When you submit a prompt | Detects "remember this" intent (5 languages) and reminds Claude to use memesh |
|
|
155
157
|
| **PostToolUse (Bash)** | After `git commit` | Auto-tracks the commit with diff stats as a memory entity |
|
|
156
158
|
| **PostToolUse (ExitPlanMode/AskUserQuestion)** | A plan is approved or you answer a question | Reminds Claude to `remember` the decision if it's worth keeping — once per tool per session |
|
|
157
|
-
| **Stop** | Session ends | Auto-captures session knowledge and applies the configured update policy |
|
|
159
|
+
| **Stop** | Session ends | Auto-captures session knowledge, ingests the project's Claude Code memory directory (frontmatter notes → `source:note-file` memories), shows one line when the turn made a decision-shaped move and stored no memory, and applies the configured update policy. The two writes (session capture, note-directory ingestion) stop when auto-capture is off (`memesh config set autoCapture false` / `MEMESH_AUTO_CAPTURE=false`); the advisory line still runs |
|
|
158
160
|
| **PreCompact** | Before context compaction | Saves important knowledge before history is compressed |
|
|
159
161
|
| **PreToolUse (Bash)** | Before a command runs | Fires accepted lesson-guards — warns when a recorded mistake is about to repeat |
|
|
160
162
|
| **SessionStart/SessionEnd (Codex)** | An ordinary Codex CLI plugin session starts, resumes, or ends | Launches the detached exact-thread companion, replaces its generation on resume, and retires it after the bounded idle queue window; a matching owner-private config may override its project/principal |
|
|
@@ -174,7 +176,8 @@ and retiring outdated info.
|
|
|
174
176
|
| Situation | Action |
|
|
175
177
|
|-----------|--------|
|
|
176
178
|
| User states what they're working on / what's next / what's blocking | `memesh task --goal "…"` / `--next "…"` / `--blocked "…"` |
|
|
177
|
-
| Design decision made | `memesh remember
|
|
179
|
+
| Design decision made | `memesh remember "Use OAuth 2.0 with PKCE for the API" --type decision --tags "project:myapp"` (or `remember({ note })` over MCP) |
|
|
180
|
+
| A stored memory is wrong | `memesh remember --name "auth-choice" --obs "the corrected fact" --replace` — the memory keeps its type and the old version moves to `metadata.replaced_history` (add `--type` only to reclassify it) |
|
|
178
181
|
| Bug fixed | `memesh learn --error "what broke" --fix "what fixed it" --root-cause "why" --severity major` |
|
|
179
182
|
| Starting work on a feature | `memesh recall "feature-name" --json` |
|
|
180
183
|
| User asks "what did we decide?" | `memesh recall "topic" --tag "project:myapp"` |
|
|
@@ -211,7 +214,13 @@ memesh remember \
|
|
|
211
214
|
Use a **stable name** (`db-choice`, not `db-choice-2026-08-16`): reusing the
|
|
212
215
|
name appends to the same entity instead of scattering duplicates. `--title` is
|
|
213
216
|
the human-readable headline; the name stays the machine key. If this replaces
|
|
214
|
-
an older decision, add `--supersedes "old-db-choice"`.
|
|
217
|
+
an older decision, add `--supersedes "old-db-choice"`. To correct it instead
|
|
218
|
+
of adding to it, repeat the call with `--replace`.
|
|
219
|
+
|
|
220
|
+
Quicker when the text is all you have: `memesh remember "SQLite for local-first
|
|
221
|
+
storage"` (MCP: `remember({ note: "…" })`). The first line becomes the title,
|
|
222
|
+
each following paragraph an observation, and the name is derived from the text,
|
|
223
|
+
so repeating the same text does not create a duplicate.
|
|
215
224
|
Types: `decision` `pattern` `lesson_learned` `bug_fix` `architecture` `convention` `feature` `best_practice` `concept` `tool` `note`
|
|
216
225
|
|
|
217
226
|
### You need context on a specific topic
|
|
@@ -245,9 +254,12 @@ memesh reindex --fts # rebuild the local keyword index
|
|
|
245
254
|
|
|
246
255
|
## Memory hygiene
|
|
247
256
|
|
|
248
|
-
1. **Stable names append.** Remembering under an
|
|
249
|
-
observations and dedupes tags
|
|
250
|
-
|
|
257
|
+
1. **Stable names append — unless you ask to replace.** Remembering under an
|
|
258
|
+
existing name adds observations and dedupes tags by default. Pass
|
|
259
|
+
`replace: true` (CLI: `--replace`) to rewrite the entity's observations,
|
|
260
|
+
tags and title instead — the previous version moves to
|
|
261
|
+
`metadata.replaced_history`, not lost. Reuse the name to grow or correct
|
|
262
|
+
one memory; do not mint `-v2` / dated variants of it.
|
|
251
263
|
2. **`supersedes` retires the loser.** When a new memory replaces an old one,
|
|
252
264
|
record it with `--supersedes <old-name>` (MCP: a relation of type
|
|
253
265
|
`supersedes`). The old entity is archived — recoverable, out of recall.
|