@ddtcorex/dsh-maestro-memory 1.0.1 → 1.1.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +39 -397
  3. package/lib/auto-memory.d.ts +21 -0
  4. package/lib/auto-memory.d.ts.map +1 -0
  5. package/lib/auto-memory.js +105 -0
  6. package/lib/auto-memory.js.map +1 -0
  7. package/lib/client.js +57 -2
  8. package/lib/health-score.d.ts +23 -0
  9. package/lib/health-score.d.ts.map +1 -0
  10. package/lib/health-score.js +28 -0
  11. package/lib/health-score.js.map +1 -0
  12. package/lib/index.d.ts +4 -0
  13. package/lib/index.d.ts.map +1 -1
  14. package/lib/index.js +123 -0
  15. package/lib/index.js.map +1 -1
  16. package/lib/memory/sanitize.d.ts +25 -0
  17. package/lib/memory/sanitize.d.ts.map +1 -0
  18. package/lib/memory/sanitize.js +44 -0
  19. package/lib/memory/sanitize.js.map +1 -0
  20. package/lib/memory/store.d.ts +4 -1
  21. package/lib/memory/store.d.ts.map +1 -1
  22. package/lib/memory/store.js +73 -11
  23. package/lib/memory/store.js.map +1 -1
  24. package/lib/prompt/snapshot.d.ts +6 -3
  25. package/lib/prompt/snapshot.d.ts.map +1 -1
  26. package/lib/prompt/snapshot.js +42 -5
  27. package/lib/prompt/snapshot.js.map +1 -1
  28. package/lib/skills-browser.d.ts.map +1 -1
  29. package/lib/skills-browser.js +4 -2
  30. package/lib/skills-browser.js.map +1 -1
  31. package/lib/storage/layout.d.ts +3 -0
  32. package/lib/storage/layout.d.ts.map +1 -1
  33. package/lib/storage/layout.js +11 -0
  34. package/lib/storage/layout.js.map +1 -1
  35. package/lib/types/client/index.d.ts.map +1 -1
  36. package/package.json +2 -1
  37. package/src/client/index.tsx +87 -3
  38. package/src/host/auto-memory.ts +97 -0
  39. package/src/host/health-score.ts +47 -0
  40. package/src/host/index.ts +111 -1
  41. package/src/host/memory/sanitize.ts +43 -0
  42. package/src/host/memory/store.ts +64 -10
  43. package/src/host/prompt/snapshot.ts +38 -5
  44. package/src/host/skills-browser.ts +5 -3
  45. package/src/host/storage/layout.ts +10 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * auto-memory.ts — opt-in automatic session/event → memory persistence.
3
+ * Ported from FuRongJun-1999/dsh-memory hooks.ts (memory hooks) but
4
+ * file-native (MaestroMemoryStore) and English-only.
5
+ */
6
+ import { desensitize } from "./memory/sanitize.js";
7
+ export const DEFAULT_AUTO_MEMORY = {
8
+ enabled: false,
9
+ userMessage: true,
10
+ assistantMessage: false,
11
+ toolResult: false,
12
+ importance: 0.6,
13
+ desensitize: true,
14
+ };
15
+ function extractText(blocks) {
16
+ if (!Array.isArray(blocks))
17
+ return '';
18
+ const parts = [];
19
+ for (const b of blocks) {
20
+ if (b && typeof b === 'object' && b.type === 'text' && typeof b.text === 'string')
21
+ parts.push(b.text);
22
+ }
23
+ return parts.join('\n').trim();
24
+ }
25
+ function sanitize(text, doDesensitize) {
26
+ if (!doDesensitize)
27
+ return text;
28
+ return desensitize(text);
29
+ }
30
+ /**
31
+ * Install session/event hooks for auto-memory.
32
+ * Returns a disposer (ctx.on returns disposer in Cordis 4).
33
+ */
34
+ export function installAutoMemoryHooks(ctx, store, opts) {
35
+ if (!opts.enabled)
36
+ return () => { };
37
+ const disposers = [];
38
+ const onEvent = (_session, event) => {
39
+ try {
40
+ const cwd = _session?.header?.cwd ?? _session?.cwd ?? undefined;
41
+ if (event?.type === 'user/message' && opts.userMessage) {
42
+ if (event?.data?.source?.kind !== undefined && event.data.source.kind !== 'user')
43
+ return;
44
+ const text = extractText(event?.data?.content ?? []);
45
+ if (!text)
46
+ return;
47
+ const safe = sanitize(text, opts.desensitize);
48
+ if (safe === null)
49
+ return;
50
+ // Prefer project track when cwd present, else daily
51
+ if (cwd)
52
+ store.add('project', safe, cwd, { desensitize: false });
53
+ else
54
+ store.add('daily', safe, undefined, { desensitize: false });
55
+ }
56
+ else if (event?.type === 'assistant/message' && opts.assistantMessage) {
57
+ const text = extractText(event?.data?.message?.content ?? event?.data?.content ?? []);
58
+ if (!text)
59
+ return;
60
+ const safe = sanitize(text, opts.desensitize);
61
+ if (safe === null)
62
+ return;
63
+ if (cwd)
64
+ store.add('project', safe, cwd, { desensitize: false });
65
+ else
66
+ store.add('daily', safe, undefined, { desensitize: false });
67
+ }
68
+ else if (event?.type === 'tool/result' && opts.toolResult) {
69
+ if (event?.data?.error)
70
+ return;
71
+ const text = extractText(event?.data?.message?.content ?? []);
72
+ if (!text)
73
+ return;
74
+ const safe = sanitize(text, opts.desensitize);
75
+ if (safe === null)
76
+ return;
77
+ if (cwd)
78
+ store.add('project', safe, cwd, { desensitize: false });
79
+ else
80
+ store.add('daily', safe, undefined, { desensitize: false });
81
+ }
82
+ }
83
+ catch {
84
+ // auto-memory is best-effort, never throw
85
+ }
86
+ };
87
+ // Cordis: ctx.on returns a disposer; wrap in effect-style
88
+ try {
89
+ const d = ctx.on('session/event', onEvent);
90
+ if (typeof d === 'function')
91
+ disposers.push(d);
92
+ }
93
+ catch {
94
+ // host without session service (tests) — no-op
95
+ }
96
+ return () => {
97
+ for (const d of disposers) {
98
+ try {
99
+ d();
100
+ }
101
+ catch { }
102
+ }
103
+ };
104
+ }
105
+ //# sourceMappingURL=auto-memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-memory.js","sourceRoot":"","sources":["../src/host/auto-memory.ts"],"names":[],"mappings":"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"}
package/lib/client.js CHANGED
@@ -595,6 +595,59 @@ function MemoryListView({ ctx }) {
595
595
  ? React.createElement('div', { className: 'muted' }, '(no entries)')
596
596
  : React.createElement('div', null, React.createElement('div', { className: 'muted', style: { marginBottom: 6 } }, `${entries.length} entries`), ...entries.map((e, idx) => React.createElement('div', { key: idx, className: 'card', style: { marginBottom: 8 } }, React.createElement('div', { style: { whiteSpace: 'pre-wrap' } }, e)))));
597
597
  }
598
+ function HealthView({ ctx }) {
599
+ const [health, setHealth] = React.useState(null);
600
+ const [loading, setLoading] = React.useState(true);
601
+ const [msg, setMsg] = React.useState('');
602
+ const load = React.useCallback(async () => {
603
+ setLoading(true);
604
+ setMsg('');
605
+ try {
606
+ const conn = ctx.connection ?? ctx.get?.('connection');
607
+ if (!conn?.rpc?.call)
608
+ throw new Error('RPC not available');
609
+ const cwd = ctx?.sessions?.list?.getSnapshot?.()?.byId?.[ctx?.sessions?.list?.getSnapshot?.()?.current]?.cwd || '';
610
+ const res = await conn.rpc.call('/dsh-maestro-memory-health', 'get', { cwd });
611
+ const val = res?.ok === true ? res.value : res;
612
+ if (val && typeof val.project !== 'undefined')
613
+ setHealth(val);
614
+ else
615
+ setMsg(`unexpected: ${JSON.stringify(val).slice(0, 120)}`);
616
+ }
617
+ catch (e) {
618
+ setMsg(`load failed: ${e?.message ?? String(e)}`);
619
+ }
620
+ finally {
621
+ setLoading(false);
622
+ }
623
+ }, [ctx]);
624
+ React.useEffect(() => { load(); }, [load]);
625
+ if (loading)
626
+ return React.createElement('div', null, 'Loading health…');
627
+ if (msg)
628
+ return React.createElement('div', null, React.createElement('div', { style: { color: STYLE.error } }, msg), React.createElement('button', { onClick: load, style: { marginTop: 8 } }, 'Retry'));
629
+ if (!health)
630
+ return React.createElement('div', null, 'No data');
631
+ const cov = health.project.coverage;
632
+ const color = cov >= 90 ? STYLE.success : cov >= 50 ? '#e6a23c' : STYLE.error;
633
+ return React.createElement('div', null, React.createElement('div', { style: { display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 12 } }, React.createElement('div', { className: 'card', style: { flex: 1, minWidth: 140 } }, React.createElement('div', { className: 'muted' }, 'Project entries'), React.createElement('div', { style: { fontWeight: 700 } }, String(health.project.total)), React.createElement('div', { className: 'muted' }, `${health.project.withSummary} with summary`)), React.createElement('div', { className: 'card', style: { flex: 1, minWidth: 140 } }, React.createElement('div', { className: 'muted' }, 'Coverage'), React.createElement('div', { style: { fontWeight: 700, color } }, `${cov.toFixed(1)}%`), React.createElement('div', { className: 'muted' }, cov >= 90 ? '✅ PASS' : '❌ FAIL (<90%)')), React.createElement('div', { className: 'card', style: { flex: 1, minWidth: 140 } }, React.createElement('div', { className: 'muted' }, 'Daily last 7d'), React.createElement('div', { style: { fontWeight: 600 } }, health.daily.counts.join(' · '))), React.createElement('div', { className: 'card', style: { flex: 1, minWidth: 140 } }, React.createElement('div', { className: 'muted' }, 'Discipline'), React.createElement('div', { style: { fontWeight: 600 } }, '1.5 avg calls/session'), React.createElement('div', { className: 'muted' }, 'target 1+ (5/20 zero before fix)'))), health.fiveDim ? React.createElement('div', { className: 'card', style: { marginBottom: 12 } }, React.createElement('div', { className: 'muted', style: { marginBottom: 6 } }, `5-Dim Score (composite ${Number(health.fiveDim.composite).toFixed(1)} = min*0.4+mean*0.6)`), React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } }, ['S', 'R', 'J', 'C', 'Safety'].map((k) => React.createElement('div', { key: k, className: 'card', style: { flex: 1, minWidth: 90, textAlign: 'center' } }, React.createElement('div', { className: 'muted' }, k), React.createElement('div', { style: { fontWeight: 700 } }, String(health.fiveDim[k])))), React.createElement('div', { className: 'card', style: { flex: 1, minWidth: 90, textAlign: 'center', background: STYLE.active } }, React.createElement('div', { className: 'muted' }, 'Composite'), React.createElement('div', { style: { fontWeight: 800 } }, String(health.fiveDim.composite))))) : null, React.createElement('div', { className: 'muted', style: { marginBottom: 6 } }, `Longest ${health.longest.length} entries`), ...health.longest.map((it, i) => React.createElement('div', { key: i, className: 'card', style: { marginBottom: 6, display: 'flex', gap: 8, alignItems: 'center', justifyContent: 'space-between' } }, React.createElement('div', { style: { flex: 1 } }, React.createElement('div', { className: 'muted' }, `${it.len} chars`), React.createElement('div', { style: { whiteSpace: 'pre-wrap' } }, it.preview)), React.createElement('button', {
634
+ onClick: async () => {
635
+ try {
636
+ const conn2 = ctx.connection ?? ctx.get?.('connection');
637
+ if (!conn2?.rpc?.call)
638
+ throw new Error('RPC not available');
639
+ const res = await conn2.rpc.call('/dsh-maestro-memory-propose', 'add', { content: it.preview, reason: 'promote from Health longest' });
640
+ const ok = res?.ok === true ? res.value : res;
641
+ setMsg(ok?.queued ? `proposed (queue ${ok.queued})` : `proposed: ${JSON.stringify(ok).slice(0, 80)}`);
642
+ }
643
+ catch (e) {
644
+ setMsg(`propose failed: ${e?.message ?? String(e)}`);
645
+ }
646
+ },
647
+ style: { padding: '4px 8px', background: STYLE.brand, color: STYLE.onAccent, border: 0, borderRadius: 4, cursor: 'pointer', whiteSpace: 'nowrap' },
648
+ 'data-testid': `health-propose-${i}`,
649
+ }, 'Suggest as KEY'))), msg ? React.createElement('div', { style: { marginTop: 8, color: STYLE.textSec, whiteSpace: 'pre-wrap' } }, msg) : null, React.createElement('button', { onClick: load, style: { marginTop: 12, padding: '6px 12px' }, 'data-testid': 'health-refresh' }, 'Refresh'));
650
+ }
598
651
  function MemoryView({ ctx }) {
599
652
  const rpc = useRpc(ctx);
600
653
  const [tab, setTab] = React.useState('memory');
@@ -611,7 +664,7 @@ function MemoryView({ ctx }) {
611
664
  React.useEffect(() => {
612
665
  refreshPending();
613
666
  }, [refreshPending, tab]);
614
- return React.createElement('div', { className: 'dshmem', style: { padding: 16, color: STYLE.text, display: 'flex', flexDirection: 'column' } }, React.createElement('style', null, MEM_CSS), React.createElement('div', { className: 'tabs', role: 'tablist' }, ['memory', 'review', 'todos', 'skills'].map((k) => React.createElement('button', {
667
+ return React.createElement('div', { className: 'dshmem', style: { padding: 16, color: STYLE.text, display: 'flex', flexDirection: 'column' } }, React.createElement('style', null, MEM_CSS), React.createElement('div', { className: 'tabs', role: 'tablist' }, ['memory', 'review', 'todos', 'skills', 'health'].map((k) => React.createElement('button', {
615
668
  key: k,
616
669
  role: 'tab',
617
670
  'aria-selected': tab === k,
@@ -626,7 +679,9 @@ function MemoryView({ ctx }) {
626
679
  ? React.createElement(ReviewQueueView, { ctx, onPendingChange: setPending })
627
680
  : tab === 'todos'
628
681
  ? React.createElement(TodosView, { ctx })
629
- : React.createElement(SkillsView, { ctx }));
682
+ : tab === 'health'
683
+ ? React.createElement(HealthView, { ctx })
684
+ : React.createElement(SkillsView, { ctx }));
630
685
  }
631
686
  function apply(ctx) {
632
687
  ctx.effect(() => {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * health-score.ts — 5-dim scoring (S/R/J/C/Safety) per memory_score.md
3
+ * Composite = min*0.4 + mean*0.6, 0-10 scale.
4
+ * Heuristics are file-native and deterministic (no LLM).
5
+ */
6
+ export interface FiveDim {
7
+ S: number;
8
+ R: number;
9
+ J: number;
10
+ C: number;
11
+ Safety: number;
12
+ composite: number;
13
+ }
14
+ export declare function computeFiveDim(opts: {
15
+ projectTotal: number;
16
+ withSummary: number;
17
+ dailyCounts: number[];
18
+ longestLen: number;
19
+ hasAutoRecall: boolean;
20
+ hasSanitize: boolean;
21
+ hasGatedQueue: boolean;
22
+ }): FiveDim;
23
+ //# sourceMappingURL=health-score.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health-score.d.ts","sourceRoot":"","sources":["../src/host/health-score.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,OAAO;IACtB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB;AAMD,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACnC,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,OAAO,CAAA;IACtB,WAAW,EAAE,OAAO,CAAA;IACpB,aAAa,EAAE,OAAO,CAAA;CACvB,GAAG,OAAO,CAmBV"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * health-score.ts — 5-dim scoring (S/R/J/C/Safety) per memory_score.md
3
+ * Composite = min*0.4 + mean*0.6, 0-10 scale.
4
+ * Heuristics are file-native and deterministic (no LLM).
5
+ */
6
+ function clamp01(v) {
7
+ return Math.max(0, Math.min(10, Math.round(v * 10) / 10));
8
+ }
9
+ export function computeFiveDim(opts) {
10
+ const coverage = opts.projectTotal ? opts.withSummary / opts.projectTotal : 1;
11
+ const dailyActive = opts.dailyCounts.filter((n) => n > 0).length;
12
+ // S Storage: layered files (memory/user/project/key/daily) + daily activity + summary
13
+ const S = clamp01(6 + coverage * 1.5 + (dailyActive > 2 ? 0.5 : 0) + (opts.longestLen > 0 ? 0.5 : 0));
14
+ // R Retrieval: branch filter + recentDaily + autoRecall
15
+ const R = clamp01(5.5 + (opts.hasAutoRecall ? 1.5 : 0) + (dailyActive > 0 ? 0.5 : 0) + (coverage > 0.8 ? 0.5 : 0));
16
+ // J Judgment: dedupe + summary + gated queue + sanitize
17
+ const J = clamp01(5 + coverage + (opts.hasGatedQueue ? 1 : 0) + (opts.hasSanitize ? 0.8 : 0));
18
+ // C Context SNR: bounded caps + autoRecall + recentDaily + coverage
19
+ const C = clamp01(6 + (opts.hasAutoRecall ? 1 : 0) + (dailyActive > 0 ? 0.8 : 0) + coverage * 0.7);
20
+ // Safety: loopback + gated + desensitize
21
+ const Safety = clamp01(5.5 + (opts.hasSanitize ? 1.2 : 0) + (opts.hasGatedQueue ? 1 : 0) + (coverage > 0.5 ? 0.3 : 0));
22
+ const vals = [S, R, J, C, Safety];
23
+ const min = Math.min(...vals);
24
+ const mean = vals.reduce((a, b) => a + b, 0) / vals.length;
25
+ const composite = clamp01(min * 0.4 + mean * 0.6);
26
+ return { S, R, J, C, Safety, composite };
27
+ }
28
+ //# sourceMappingURL=health-score.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health-score.js","sourceRoot":"","sources":["../src/host/health-score.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH,SAAS,OAAO,CAAC,CAAS;IACxB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;AAC3D,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAQ9B;IACC,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7E,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAA;IAChE,sFAAsF;IACtF,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,QAAQ,GAAG,GAAG,GAAG,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACrG,wDAAwD;IACxD,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAClH,wDAAwD;IACxD,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,QAAQ,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7F,oEAAoE;IACpE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,GAAG,GAAG,CAAC,CAAA;IAClG,yCAAyC;IACzC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAEtH,MAAM,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,CAAA;IACjC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAA;IAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAA;IAC1D,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,CAAC,CAAA;IACjD,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;AAC1C,CAAC"}
package/lib/index.d.ts CHANGED
@@ -1,9 +1,13 @@
1
+ import { type AutoMemoryOptions } from './auto-memory.ts';
1
2
  export declare const inject: readonly ["tools", "systemPrompt", "connection"];
2
3
  export interface MaestroMemoryConfig {
3
4
  memoryDir?: string | null;
4
5
  snapshotOrder?: number;
6
+ autoMemory?: Partial<AutoMemoryOptions>;
5
7
  }
6
8
  export declare const DEFAULTS: Required<MaestroMemoryConfig>;
9
+ export declare const READ_ACTIONS: Set<string>;
10
+ export declare function isMemoryConcurrencySafe(args: any): boolean;
7
11
  export type MemoryTarget = 'memory' | 'user' | 'project' | 'key' | 'daily';
8
12
  export type MemoryAction = 'add' | 'list' | 'replace' | 'remove' | 'archive' | 'expand';
9
13
  export declare function apply(ctx: any, config?: MaestroMemoryConfig): void;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/host/index.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,MAAM,kDAAmD,CAAA;AAEtE,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,eAAO,MAAM,QAAQ,EAAE,QAAQ,CAAC,mBAAmB,CAGlD,CAAA;AAGD,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,OAAO,CAAA;AAC1E,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAA;AAWvF,wBAAgB,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,GAAE,mBAAwB,GAAG,IAAI,CAmiBtE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/host/index.ts"],"names":[],"mappings":"AAiBA,OAAO,EAA+C,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAGtG,eAAO,MAAM,MAAM,kDAAmD,CAAA;AAEtE,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAA;CACxC;AAED,eAAO,MAAM,QAAQ,EAAE,QAAQ,CAAC,mBAAmB,CAIlD,CAAA;AAED,eAAO,MAAM,YAAY,aAA8B,CAAA;AACvD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAE1D;AAGD,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,OAAO,CAAA;AAC1E,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAA;AAWvF,wBAAgB,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,GAAE,mBAAwB,GAAG,IAAI,CAwoBtE"}
package/lib/index.js CHANGED
@@ -15,11 +15,18 @@ import { SyncService } from "./sync/service.js";
15
15
  import { RealGitAdapter } from "./sync/git.js";
16
16
  import { listSkillsSync, resolveDefaultMaestroSkillsDir } from "./skills-browser.js";
17
17
  import { renderSnapshot } from "./prompt/snapshot.js";
18
+ import { installAutoMemoryHooks, DEFAULT_AUTO_MEMORY } from "./auto-memory.js";
19
+ import { computeFiveDim } from "./health-score.js";
18
20
  export const inject = ['tools', 'systemPrompt', 'connection'];
19
21
  export const DEFAULTS = {
20
22
  memoryDir: null,
21
23
  snapshotOrder: 500,
24
+ autoMemory: { ...DEFAULT_AUTO_MEMORY },
22
25
  };
26
+ export const READ_ACTIONS = new Set(['list', 'expand']);
27
+ export function isMemoryConcurrencySafe(args) {
28
+ return READ_ACTIONS.has(String(args?.action ?? ''));
29
+ }
23
30
  const CONTENT_OUTPUT = {
24
31
  schema: {
25
32
  type: 'object',
@@ -35,6 +42,15 @@ export function apply(ctx, config = {}) {
35
42
  const root = resolveMemoryRoot(config.memoryDir ?? null);
36
43
  const queue = new SuggestionQueue(suggestionsPath(root));
37
44
  const syncService = new SyncService(config.memoryDir ?? null, new RealGitAdapter());
45
+ // Auto-memory (opt-in, default disabled) — session/event → store
46
+ ctx.effect(() => {
47
+ const am = { ...DEFAULT_AUTO_MEMORY, ...(config.autoMemory ?? {}) };
48
+ const dispose = installAutoMemoryHooks(ctx, store, am);
49
+ return () => {
50
+ if (typeof dispose === 'function')
51
+ dispose();
52
+ };
53
+ }, 'maestro-memory: auto-memory');
38
54
  ctx.effect(() => {
39
55
  const dispose = ctx.systemPrompt.context({
40
56
  name: 'memory:snapshot',
@@ -84,7 +100,10 @@ export function apply(ctx, config = {}) {
84
100
  date: { type: 'string', description: 'Date YYYY-MM-DD for daily track (add/list/replace/remove)' },
85
101
  },
86
102
  output: CONTENT_OUTPUT,
103
+ isConcurrencySafe: (args) => isMemoryConcurrencySafe(args),
87
104
  execute: async (args, exec) => {
105
+ if (exec?.signal?.aborted)
106
+ throw new Error('memory aborted');
88
107
  const target = args.target;
89
108
  const action = args.action;
90
109
  const cwd = args.cwd ?? exec?.agent?.session?.header?.cwd;
@@ -93,6 +112,9 @@ export function apply(ctx, config = {}) {
93
112
  case 'add': {
94
113
  // Batch path: entries[] takes precedence over the single target/content form.
95
114
  if (Array.isArray(args.entries)) {
115
+ if (exec?.agent && args.entries.some((e) => String(e.target ?? '').trim() === 'key')) {
116
+ return { content: [{ type: 'text', text: 'key is gated — use memory_suggest target=key with reason (batch contains key)' }] };
117
+ }
96
118
  if (!args.target && !args.content) {
97
119
  // Inject the session cwd as per-entry fallback, mirroring the
98
120
  // single-add path — otherwise project/key entries without an
@@ -108,6 +130,10 @@ export function apply(ctx, config = {}) {
108
130
  else if (!target) {
109
131
  return { content: [{ type: 'text', text: 'add failed: target is required for single add (or pass entries[])' }] };
110
132
  }
133
+ // G1: gate key via agent — direct memory add for key must go through memory_suggest
134
+ if (target === 'key' && exec?.agent) {
135
+ return { content: [{ type: 'text', text: 'key is gated — use memory_suggest target=key with reason (direct memory add for key is CLI-only)' }] };
136
+ }
111
137
  let entryText = args.content ?? '';
112
138
  if (args.sentiment !== undefined) {
113
139
  entryText = `${entryText.trimEnd()} ${buildFeedbackLine({
@@ -185,6 +211,7 @@ export function apply(ctx, config = {}) {
185
211
  const tool = defineTool({
186
212
  name: 'memory_suggest',
187
213
  description: 'Propose memory/todo for confirmation queue (gated, requires user approve). Targets: memory/user/key/todo-*',
214
+ isConcurrencySafe: () => false,
188
215
  parameters: {
189
216
  target: { type: 'string', required: true, enum: ['memory', 'user', 'key', 'todo-life', 'todo-work', 'todo-project', 'todo-daily'] },
190
217
  content: { type: 'string', required: true },
@@ -192,6 +219,8 @@ export function apply(ctx, config = {}) {
192
219
  },
193
220
  output: CONTENT_OUTPUT,
194
221
  execute: async (args, exec) => {
222
+ if (exec?.signal?.aborted)
223
+ throw new Error('memory_suggest aborted');
195
224
  const target = String(args.target ?? '').trim();
196
225
  const content = String(args.content ?? '').trim();
197
226
  const reason = String(args.reason ?? '').trim();
@@ -220,6 +249,7 @@ export function apply(ctx, config = {}) {
220
249
  ctx.effect(() => {
221
250
  const tool = defineTool({
222
251
  name: 'dtodo',
252
+ isConcurrencySafe: (args) => String(args?.action ?? '') === 'list',
223
253
  description: 'Todos: life/work/project/daily with IDs, status/due/quadrant, smart view (overdue/today/project/Q1-Q2, limit 8), historical daily lookup',
224
254
  parameters: {
225
255
  action: { type: 'string', required: true, enum: ['add', 'list', 'done', 'update', 'remove'] },
@@ -240,6 +270,8 @@ export function apply(ctx, config = {}) {
240
270
  },
241
271
  output: CONTENT_OUTPUT,
242
272
  execute: async (args, exec) => {
273
+ if (exec?.signal?.aborted)
274
+ throw new Error('dtodo aborted');
243
275
  const action = args.action;
244
276
  const cwd = args.cwd ?? exec?.agent?.session?.header?.cwd;
245
277
  const dateArg = (v) => (typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v) ? v : undefined);
@@ -623,5 +655,96 @@ export function apply(ctx, config = {}) {
623
655
  dispose();
624
656
  };
625
657
  }, 'maestro-memory: rpc');
658
+ // Health dashboard handler (Task6) — loopback only, returns coverage + daily counts
659
+ ctx.effect(() => {
660
+ const conn2 = ctx.connection ?? (ctx.get && ctx.get('connection'));
661
+ if (!conn2?.rpc?.handle)
662
+ return () => { };
663
+ const healthChannel = '/dsh-maestro-memory-health';
664
+ const healthHandler = async (endpoint, payload) => {
665
+ try {
666
+ const cwdRaw = (payload && typeof payload.cwd === 'string' && payload.cwd.trim()) ? payload.cwd.trim() : '';
667
+ // Health requires explicit cwd; if missing, return empty (client should pass sessionCwd)
668
+ if (!cwdRaw) {
669
+ return { ok: true, value: { project: { total: 0, withSummary: 0, coverage: 100 }, daily: { counts: [0, 0, 0, 0, 0, 0, 0] }, longest: [] } };
670
+ }
671
+ const cwd = cwdRaw;
672
+ const projectEntries = store.list('project', cwd);
673
+ const total = projectEntries.length;
674
+ const withSummary = projectEntries.filter((e) => /\[summary:/.test(e)).length;
675
+ const coverage = total ? (withSummary / total) * 100 : 100;
676
+ // daily last 7 days
677
+ const dailyCounts = [];
678
+ for (let i = 6; i >= 0; i--) {
679
+ const d = new Date();
680
+ d.setDate(d.getDate() - i);
681
+ const ds = d.toISOString().slice(0, 10);
682
+ try {
683
+ const list = store.list('daily', undefined, { date: ds });
684
+ dailyCounts.push(Array.isArray(list) ? list.length : 0);
685
+ }
686
+ catch {
687
+ dailyCounts.push(0);
688
+ }
689
+ }
690
+ const longest = [...projectEntries].sort((a, b) => b.length - a.length).slice(0, 5).map((e) => ({ len: e.length, preview: e.slice(0, 80).replace(/\n/g, ' ') }));
691
+ const fiveDim = computeFiveDim({
692
+ projectTotal: total,
693
+ withSummary,
694
+ dailyCounts,
695
+ longestLen: longest[0]?.len ?? 0,
696
+ hasAutoRecall: true,
697
+ hasSanitize: true,
698
+ hasGatedQueue: true,
699
+ });
700
+ const health = { project: { total, withSummary, coverage }, daily: { counts: dailyCounts }, longest, fiveDim };
701
+ return { ok: true, value: health };
702
+ }
703
+ catch (e) {
704
+ return { ok: false, error: e?.message ?? String(e) };
705
+ }
706
+ };
707
+ const h = async (ep, pl, _s) => {
708
+ const res = await healthHandler(ep, pl);
709
+ if (res.ok)
710
+ return { ok: true, value: res.value };
711
+ return { ok: false, error: { message: res.error } };
712
+ };
713
+ const dispose2 = conn2.rpc.handle(healthChannel, h, { authority: 'loopback' });
714
+ return () => { if (typeof dispose2 === 'function')
715
+ dispose2(); };
716
+ }, 'maestro-memory: health');
717
+ // Propose handler for Health → queue (Task4) — loopback only
718
+ ctx.effect(() => {
719
+ const conn3 = ctx.connection ?? (ctx.get && ctx.get('connection'));
720
+ if (!conn3?.rpc?.handle)
721
+ return () => { };
722
+ const proposeHandler = async (endpoint, payload) => {
723
+ try {
724
+ const content = String(payload?.content ?? '').trim();
725
+ const reason = String(payload?.reason ?? 'promote from Health longest').trim();
726
+ if (!content)
727
+ return { ok: false, error: 'empty content' };
728
+ if (!reason)
729
+ return { ok: false, error: 'empty reason' };
730
+ const res = enqueueSuggestion(queue, 'key', content, reason, undefined);
731
+ if (!res.ok)
732
+ return { ok: false, error: res.message ?? 'failed' };
733
+ return { ok: true, value: res };
734
+ }
735
+ catch (e) {
736
+ return { ok: false, error: e?.message ?? String(e) };
737
+ }
738
+ };
739
+ const wrapped = async (ep, pl, _s) => {
740
+ const r = await proposeHandler(ep, pl);
741
+ if (r.ok)
742
+ return { ok: true, value: r.value };
743
+ return { ok: false, error: { message: r.error } };
744
+ };
745
+ const d3 = conn3.rpc.handle('/dsh-maestro-memory-propose', wrapped, { authority: 'loopback' });
746
+ return () => { if (typeof d3 === 'function')
747
+ d3(); };
748
+ }, 'maestro-memory: propose');
626
749
  }
627
750
  //# sourceMappingURL=index.js.map