memorix 1.4.4 → 1.4.6
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/CHANGELOG.md +69 -0
- package/CLAUDE.md +7 -3
- package/README.md +14 -4
- package/README.zh-CN.md +9 -2
- package/dist/cli/index.js +1802 -1138
- package/dist/index.js +1298 -991
- package/dist/{maintenance-jobs-B1-W0ssz.d.ts → maintenance-jobs-DVRF-46r.d.ts} +1 -1
- package/dist/maintenance-runner.d.ts +1 -1
- package/dist/maintenance-runner.js +1755 -829
- package/dist/media-audio-runner.js +1760 -835
- package/dist/media-video-runner.js +1755 -830
- package/dist/memcode-runtime/CHANGELOG.md +69 -0
- package/dist/sdk.js +1290 -990
- package/dist/types.d.ts +7 -1
- package/dist/vector-backfill-runner.d.ts +1 -1
- package/dist/vector-backfill-runner.js +1751 -826
- package/docs/1.4.5-AGENT-AUTO-MEMORY-RESEARCH.md +95 -0
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +3 -2
- package/docs/API_REFERENCE.md +23 -4
- package/docs/CONFIGURATION.md +17 -3
- package/docs/INTEGRATIONS.md +9 -2
- package/docs/KNOWN_ISSUES_AND_ROADMAP.md +1 -0
- package/docs/MEMCODE.md +1 -1
- package/docs/SETUP.md +14 -4
- package/llms-full.txt +5 -4
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/plugins/antigravity/memorix/mcp_config.json +3 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/memorix/.mcp.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.mcp.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.mcp.json +1 -1
- package/plugins/copilot/memorix/.mcp.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +4 -2
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/openclaw/memorix/.mcp.json +3 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/commands/agent-integrations.ts +7 -1
- package/src/cli/commands/dashboard.ts +14 -3
- package/src/cli/commands/hooks-install.ts +2 -0
- package/src/cli/commands/hooks-preview.ts +5 -0
- package/src/cli/commands/ingest-commit.ts +47 -3
- package/src/cli/commands/integrate.ts +2 -0
- package/src/cli/commands/memory.ts +17 -4
- package/src/cli/commands/purge.ts +120 -0
- package/src/cli/commands/serve-http.ts +39 -5
- package/src/cli/commands/setup.ts +60 -3
- package/src/cli/commands/uninstall.ts +20 -0
- package/src/cli/index.ts +3 -804
- package/src/codegraph/auto-context.ts +82 -1
- package/src/config/yaml-loader.ts +3 -8
- package/src/config.ts +0 -5
- package/src/evaluation/session-replay.ts +98 -0
- package/src/git/extractor.ts +5 -3
- package/src/hooks/installers/index.ts +58 -2
- package/src/hooks/types.ts +2 -0
- package/src/integrations/registry.ts +9 -0
- package/src/knowledge/workset.ts +58 -0
- package/src/memory/long-term.ts +97 -2
- package/src/runtime/control-plane-maintenance.ts +1 -0
- package/src/runtime/isolated-maintenance.ts +1 -0
- package/src/runtime/lifecycle.ts +19 -0
- package/src/runtime/maintenance-jobs.ts +1 -0
- package/src/runtime/project-maintenance.ts +8 -0
- package/src/search/surfaced-registry.ts +62 -0
- package/src/server.ts +74 -14
- package/src/store/orama-store.ts +17 -1
- package/src/types.ts +7 -1
- package/src/workspace/mcp-adapters/dsh.ts +147 -0
- package/src/cli/commands/audit-list.ts +0 -89
- package/src/cli/commands/purge-all-memory.ts +0 -85
- package/src/cli/commands/purge-project-memory.ts +0 -83
- package/src/cli/commands/uninstall-project-artifacts.ts +0 -86
- package/src/cli/tui/Sidebar.tsx +0 -179
- package/src/cli/tui/interactive-commands.ts +0 -261
- package/src/cli/tui/markdown-render.tsx +0 -371
- package/src/cli/tui/use-mouse.ts +0 -157
- package/src/cli/tui/useNavigation.ts +0 -56
- package/src/orchestrate/context-compact.ts +0 -188
- package/src/team/file-locks.ts +0 -173
- package/src/team/index.ts +0 -23
- package/src/team/messages.ts +0 -203
- package/src/team/persistence.ts +0 -96
- package/src/team/registry.ts +0 -158
- package/src/team/tasks.ts +0 -211
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
import { CodeGraphStore } from './store.js';
|
|
30
30
|
import { isEligibleForAutomaticDelivery } from '../memory/admission.js';
|
|
31
31
|
import { getSessionResumeBrief } from '../memory/session.js';
|
|
32
|
+
import { listLongTermMemories } from '../memory/long-term.js';
|
|
33
|
+
import { filterReadableObservations } from '../memory/visibility.js';
|
|
32
34
|
import { initSessionStore } from '../store/session-store.js';
|
|
33
35
|
import {
|
|
34
36
|
isContinuationTask,
|
|
@@ -94,6 +96,64 @@ function deliveryEligibleProjectObservations(
|
|
|
94
96
|
.filter((observation) => isEligibleForAutomaticDelivery(observation));
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The always-on block: a small "who you are and what this workspace is
|
|
101
|
+
* doing" context every brief carries, whatever the task. It mirrors the
|
|
102
|
+
* memory-native feel of a per-session MEMORY.md and stays strictly bounded:
|
|
103
|
+
* profile 2 lines, latest session 1 line, durable facts 2 lines.
|
|
104
|
+
*/
|
|
105
|
+
async function buildAlwaysOnBlock(input: {
|
|
106
|
+
dataDir: string;
|
|
107
|
+
projectId: string;
|
|
108
|
+
reader: ObservationReader;
|
|
109
|
+
eligible: ProjectContextObservation[];
|
|
110
|
+
}): Promise<{ profile: string[]; state?: string; durable: string[] }> {
|
|
111
|
+
// Profile: personal-visibility observations the guidance teaches as
|
|
112
|
+
// entityName 'user-profile'. Only their owner's reader sees them.
|
|
113
|
+
const profile = filterReadableObservations(input.eligible, input.reader)
|
|
114
|
+
.filter(observation => (
|
|
115
|
+
observation as unknown as { visibility?: string; entityName?: string }
|
|
116
|
+
).visibility === 'personal'
|
|
117
|
+
&& (
|
|
118
|
+
observation as unknown as { entityName?: string }
|
|
119
|
+
).entityName === 'user-profile')
|
|
120
|
+
.slice(0, 2)
|
|
121
|
+
.map(observation => compactContinuationText(observation.title, 22));
|
|
122
|
+
|
|
123
|
+
// Workspace state: the latest completed session with a summary.
|
|
124
|
+
let state: string | undefined;
|
|
125
|
+
try {
|
|
126
|
+
await initSessionStore(input.dataDir);
|
|
127
|
+
const { getSessionStore } = await import('../store/session-store.js');
|
|
128
|
+
const sessions = await getSessionStore().loadByProject(input.projectId);
|
|
129
|
+
const latest = sessions
|
|
130
|
+
.filter(session => session.endedAt && session.summary?.trim())
|
|
131
|
+
.sort((a, b) => Date.parse(b.endedAt ?? '') - Date.parse(a.endedAt ?? ''))[0];
|
|
132
|
+
if (latest?.summary) state = compactContinuationText(latest.summary, 40);
|
|
133
|
+
} catch { /* session context is optional enrichment */ }
|
|
134
|
+
|
|
135
|
+
// Durable facts: the most recently updated approved long-term memories.
|
|
136
|
+
const durable: string[] = [];
|
|
137
|
+
try {
|
|
138
|
+
const items = await listLongTermMemories({
|
|
139
|
+
dataDir: input.dataDir,
|
|
140
|
+
reader: {
|
|
141
|
+
projectId: input.projectId,
|
|
142
|
+
...(input.reader.agentId ? { agentId: input.reader.agentId } : {}),
|
|
143
|
+
...(input.reader.isTeamMember ? { isTeamMember: true } : {}),
|
|
144
|
+
},
|
|
145
|
+
limit: 10,
|
|
146
|
+
});
|
|
147
|
+
durable.push(...items
|
|
148
|
+
.filter(item => item.memory.state === 'approved')
|
|
149
|
+
.sort((a, b) => (b.memory.updatedAt ?? '').localeCompare(a.memory.updatedAt ?? ''))
|
|
150
|
+
.slice(0, 2)
|
|
151
|
+
.map(item => item.memory.kind + ': ' + compactContinuationText(item.memory.title, 22)));
|
|
152
|
+
} catch { /* long-term memory is optional enrichment */ }
|
|
153
|
+
|
|
154
|
+
return { profile, state, durable };
|
|
155
|
+
}
|
|
156
|
+
|
|
97
157
|
function decideRefresh(input: {
|
|
98
158
|
mode: AutoContextRefreshMode;
|
|
99
159
|
status: ReturnType<CodeGraphStore['status']>;
|
|
@@ -356,12 +416,21 @@ export async function buildAutoProjectContext(input: {
|
|
|
356
416
|
};
|
|
357
417
|
}
|
|
358
418
|
}
|
|
419
|
+
const alwaysOn = await buildAlwaysOnBlock({
|
|
420
|
+
dataDir: input.dataDir,
|
|
421
|
+
projectId: input.project.id,
|
|
422
|
+
reader: input.reader ?? { projectId: input.project.id },
|
|
423
|
+
eligible: deliveryEligibleProjectObservations(input.observations, input.project.id),
|
|
424
|
+
});
|
|
359
425
|
const workset = await buildTaskWorkset({
|
|
360
426
|
projectId: input.project.id,
|
|
361
427
|
dataDir: input.dataDir,
|
|
362
428
|
...(task ? { task } : {}),
|
|
363
429
|
lens: lens.id,
|
|
364
430
|
startHere,
|
|
431
|
+
...(alwaysOn.profile.length > 0 || alwaysOn.state || alwaysOn.durable.length > 0
|
|
432
|
+
? { alwaysOn }
|
|
433
|
+
: {}),
|
|
365
434
|
...(externalOutline ? { semanticCode: externalOutline } : {}),
|
|
366
435
|
providerQuality,
|
|
367
436
|
currentFacts: worksetFactLines(currentFacts),
|
|
@@ -598,6 +667,18 @@ export function formatAutoProjectContextSummary(context: AutoProjectContext): st
|
|
|
598
667
|
'',
|
|
599
668
|
...formatCurrentFactsLines(context.currentFacts),
|
|
600
669
|
'',
|
|
670
|
+
].filter(Boolean);
|
|
671
|
+
|
|
672
|
+
if (context.workset.alwaysOn
|
|
673
|
+
&& (context.workset.alwaysOn.profile.length > 0 || context.workset.alwaysOn.state || context.workset.alwaysOn.durable.length > 0)) {
|
|
674
|
+
lines.push('You and this workspace');
|
|
675
|
+
for (const profile of context.workset.alwaysOn.profile.slice(0, 2)) lines.push(`- ${profile}`);
|
|
676
|
+
if (context.workset.alwaysOn.state) lines.push(`- Recently: ${context.workset.alwaysOn.state}`);
|
|
677
|
+
for (const durable of context.workset.alwaysOn.durable.slice(0, 2)) lines.push(`- ${durable}`);
|
|
678
|
+
lines.push('');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
lines.push(...[
|
|
601
682
|
`- Code memory: ${context.overview.code.files} files / ${context.overview.code.symbols} symbols / ${context.overview.code.refs} memory links`,
|
|
602
683
|
`- Code provider: ${context.providerQuality.selected} (${context.providerQuality.selectedQuality})`,
|
|
603
684
|
`- Languages: ${formatLanguages(context.overview)}`,
|
|
@@ -606,7 +687,7 @@ export function formatAutoProjectContextSummary(context: AutoProjectContext): st
|
|
|
606
687
|
`- Refresh: ${context.refresh.message}`,
|
|
607
688
|
'',
|
|
608
689
|
'Start here',
|
|
609
|
-
].filter(Boolean);
|
|
690
|
+
].filter(Boolean));
|
|
610
691
|
|
|
611
692
|
if (startHere.length > 0) {
|
|
612
693
|
startHere.forEach((path, index) => lines.push(`${index + 1}. ${path}`));
|
|
@@ -110,14 +110,9 @@ export interface MemorixYamlConfig {
|
|
|
110
110
|
workspaceCollection?: string;
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
command?: string;
|
|
117
|
-
args?: string[];
|
|
118
|
-
env?: Record<string, string>;
|
|
119
|
-
url?: string;
|
|
120
|
-
}>;
|
|
113
|
+
// NOTE: a previous `mcpServers` declaration ("Memorix as an MCP hub") was
|
|
114
|
+
// removed — no code ever consumed it. Unknown top-level keys are ignored,
|
|
115
|
+
// so existing files with the old key keep loading without error.
|
|
121
116
|
}
|
|
122
117
|
|
|
123
118
|
// ─── Loader ──────────────────────────────────────────────────────────
|
package/src/config.ts
CHANGED
|
@@ -120,10 +120,5 @@ export function getServerConfig(): NonNullable<MemorixYamlConfig['server']> {
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
/** Team config */
|
|
124
|
-
export function getTeamConfig(): NonNullable<MemorixYamlConfig['team']> {
|
|
125
|
-
return loadYamlConfig().team ?? {};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
123
|
/** Get the full resolved YAML config (for status display) */
|
|
129
124
|
export { loadYamlConfig } from './config/yaml-loader.js';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-replay evaluation — deterministic scoring for the retrieval path.
|
|
3
|
+
*
|
|
4
|
+
* Purpose: before changing how Memorix retrieves memory, measure the current
|
|
5
|
+
* behavior with a fixed exam so every later change can be judged by the same
|
|
6
|
+
* three scores:
|
|
7
|
+
*
|
|
8
|
+
* - hit rate — the right memory showed up for each question
|
|
9
|
+
* - duplicate rate — how much of a session's delivery re-shows the same
|
|
10
|
+
* memory (lower is better; the dedup change targets it)
|
|
11
|
+
* - token ceiling — no turn exceeds its budget
|
|
12
|
+
*
|
|
13
|
+
* Scoring is mechanical: ids in, ids out. No model, no network, no keys.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface SessionReplayQuestion {
|
|
17
|
+
id: string;
|
|
18
|
+
query: string;
|
|
19
|
+
/** The observation ids that MUST appear in this turn's results. */
|
|
20
|
+
expectedObsIds: number[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SessionReplayTurn {
|
|
24
|
+
questionId: string;
|
|
25
|
+
shownObsIds: number[];
|
|
26
|
+
tokens: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SessionReplayScores {
|
|
30
|
+
questions: number;
|
|
31
|
+
hits: number;
|
|
32
|
+
/** hitRate ∈ [0, 1] — share of questions whose expected ids all appeared. */
|
|
33
|
+
hitRate: number;
|
|
34
|
+
/** How many times an observation was re-shown after its first appearance. */
|
|
35
|
+
duplicateShows: number;
|
|
36
|
+
/** duplicateRate ∈ [0, 1] — duplicate shows / total shows (0 = perfect). */
|
|
37
|
+
duplicateRate: number;
|
|
38
|
+
totalShows: number;
|
|
39
|
+
/** Highest token count across turns. */
|
|
40
|
+
maxTurnTokens: number;
|
|
41
|
+
overBudgetTurns: number;
|
|
42
|
+
turns: SessionReplayTurn[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function scoreSessionReplay(
|
|
46
|
+
questions: SessionReplayQuestion[],
|
|
47
|
+
turns: SessionReplayTurn[],
|
|
48
|
+
tokenCeiling: number,
|
|
49
|
+
): SessionReplayScores {
|
|
50
|
+
const turnById = new Map(turns.map((turn) => [turn.questionId, turn]));
|
|
51
|
+
|
|
52
|
+
let hits = 0;
|
|
53
|
+
let duplicateShows = 0;
|
|
54
|
+
let totalShows = 0;
|
|
55
|
+
let maxTurnTokens = 0;
|
|
56
|
+
let overBudgetTurns = 0;
|
|
57
|
+
const seen = new Set<number>();
|
|
58
|
+
|
|
59
|
+
for (const question of questions) {
|
|
60
|
+
const turn = turnById.get(question.id);
|
|
61
|
+
if (!turn) continue;
|
|
62
|
+
maxTurnTokens = Math.max(maxTurnTokens, turn.tokens);
|
|
63
|
+
if (turn.tokens > tokenCeiling) overBudgetTurns += 1;
|
|
64
|
+
|
|
65
|
+
const expectedFound = question.expectedObsIds.every((id) => turn.shownObsIds.includes(id));
|
|
66
|
+
if (expectedFound) hits += 1;
|
|
67
|
+
|
|
68
|
+
for (const id of turn.shownObsIds) {
|
|
69
|
+
totalShows += 1;
|
|
70
|
+
if (seen.has(id)) duplicateShows += 1;
|
|
71
|
+
seen.add(id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
questions: questions.length,
|
|
77
|
+
hits,
|
|
78
|
+
hitRate: questions.length === 0 ? 1 : hits / questions.length,
|
|
79
|
+
duplicateShows,
|
|
80
|
+
duplicateRate: totalShows === 0 ? 0 : duplicateShows / totalShows,
|
|
81
|
+
totalShows,
|
|
82
|
+
maxTurnTokens,
|
|
83
|
+
overBudgetTurns,
|
|
84
|
+
turns,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function formatSessionReplayScoreboard(scores: SessionReplayScores, label: string): string {
|
|
89
|
+
const lines = [
|
|
90
|
+
`Session replay scoreboard — ${label}`,
|
|
91
|
+
` questions: ${scores.questions}`,
|
|
92
|
+
` hit rate: ${scores.hits}/${scores.questions} (${(scores.hitRate * 100).toFixed(0)}%)`,
|
|
93
|
+
` duplicate shows: ${scores.duplicateShows} of ${scores.totalShows} shows (${(scores.duplicateRate * 100).toFixed(0)}%)`,
|
|
94
|
+
` max turn tokens: ${scores.maxTurnTokens}`,
|
|
95
|
+
` over-budget: ${scores.overBudgetTurns} turn(s)`,
|
|
96
|
+
];
|
|
97
|
+
return lines.join('\n');
|
|
98
|
+
}
|
package/src/git/extractor.ts
CHANGED
|
@@ -61,8 +61,10 @@ function normalizeCommitCount(count: number): number {
|
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Get commit info by hash (or HEAD).
|
|
64
|
+
* `maxDiffChars` caps how much diff content is captured (default 500).
|
|
64
65
|
*/
|
|
65
|
-
export function getCommitInfo(cwd: string, ref = 'HEAD'): CommitInfo {
|
|
66
|
+
export function getCommitInfo(cwd: string, ref = 'HEAD', maxDiffChars = 500): CommitInfo {
|
|
67
|
+
const cap = Number.isFinite(maxDiffChars) ? Math.max(0, Math.floor(maxDiffChars)) : 500;
|
|
66
68
|
const FORMAT = '%H%n%h%n%aN%n%aI%n%s%n%b';
|
|
67
69
|
const revision = normalizeCommitRef(ref);
|
|
68
70
|
const raw = runGit(cwd, ['log', '-1', `--format=${FORMAT}`, '--end-of-options', revision]).trim();
|
|
@@ -93,11 +95,11 @@ export function getCommitInfo(cwd: string, ref = 'HEAD'): CommitInfo {
|
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
// Get short diff summary
|
|
98
|
+
// Get short diff summary, capped at the configured diff-size budget.
|
|
97
99
|
let diffSummary = '';
|
|
98
100
|
try {
|
|
99
101
|
const diff = runGit(cwd, ['diff-tree', '-p', '--no-commit-id', hash], 1024 * 100);
|
|
100
|
-
diffSummary = diff.substring(0,
|
|
102
|
+
diffSummary = diff.substring(0, cap);
|
|
101
103
|
} catch { /* diff may be too large */ }
|
|
102
104
|
|
|
103
105
|
return {
|
|
@@ -360,6 +360,9 @@ const AGENT_SKILL_DIRS: Partial<Record<AgentName, { project: string; global?: st
|
|
|
360
360
|
kiro: { project: path.join('.kiro', 'skills'), global: path.join('.kiro', 'skills') },
|
|
361
361
|
opencode: { project: path.join('.opencode', 'skills'), global: path.join('.config', 'opencode', 'skills') },
|
|
362
362
|
trae: { project: path.join('.trae', 'skills'), global: path.join('.trae', 'skills') },
|
|
363
|
+
// DSH's user skill root is <harness home>/skills; the global root below is
|
|
364
|
+
// resolved to $DSH_HOME (or ~/.dsh) for dsh by installOfficialSkillsForAgent.
|
|
365
|
+
dsh: { project: path.join('.dsh', 'skills'), global: 'skills' },
|
|
363
366
|
};
|
|
364
367
|
|
|
365
368
|
const PACKAGE_OWNED_HOOK_AGENTS = new Set<AgentName>(['openclaw', 'hermes', 'omp']);
|
|
@@ -384,7 +387,11 @@ async function installOfficialSkillsForAgent(
|
|
|
384
387
|
const relativeDir = global ? dirs?.global : dirs?.project;
|
|
385
388
|
if (!relativeDir) return [];
|
|
386
389
|
|
|
387
|
-
const root = global
|
|
390
|
+
const root = global
|
|
391
|
+
? (agent === 'dsh'
|
|
392
|
+
? (process.env.DSH_HOME?.trim() || path.join(os.homedir(), '.dsh'))
|
|
393
|
+
: os.homedir())
|
|
394
|
+
: projectRoot;
|
|
388
395
|
const skillPaths: string[] = [];
|
|
389
396
|
for (const skillEntry of OFFICIAL_MEMORIX_SKILLS) {
|
|
390
397
|
const skillPath = path.join(root, relativeDir, skillEntry.name, 'SKILL.md');
|
|
@@ -626,6 +633,9 @@ export function getProjectConfigPath(agent: AgentName, projectRoot: string): str
|
|
|
626
633
|
case 'trae':
|
|
627
634
|
// Trae has no hooks system — only rules (.trae/rules/project_rules.md)
|
|
628
635
|
return path.join(projectRoot, '.trae', 'rules', 'project_rules.md');
|
|
636
|
+
case 'dsh':
|
|
637
|
+
// DeepSeek Harness has no hooks system — only guidance (AGENTS.md)
|
|
638
|
+
return path.join(projectRoot, 'AGENTS.md');
|
|
629
639
|
case 'opencode':
|
|
630
640
|
// OpenCode uses plugin files for hooks
|
|
631
641
|
return path.join(projectRoot, '.opencode', 'plugins', 'memorix.js');
|
|
@@ -684,6 +694,9 @@ export function getGlobalConfigPath(agent: AgentName): string {
|
|
|
684
694
|
return path.join(home, '.omp', 'agent', 'packages', 'memorix', 'extensions', 'memorix.js');
|
|
685
695
|
case 'trae':
|
|
686
696
|
return path.join(home, '.trae', 'rules', 'project_rules.md');
|
|
697
|
+
case 'dsh':
|
|
698
|
+
// DSH reads the user-global AGENTS.md from its harness home.
|
|
699
|
+
return path.join(process.env.DSH_HOME?.trim() || path.join(home, '.dsh'), 'AGENTS.md');
|
|
687
700
|
default:
|
|
688
701
|
return path.join(home, '.memorix', 'hooks.json');
|
|
689
702
|
}
|
|
@@ -709,6 +722,9 @@ export function getAgentRulesPath(agent: AgentName, root: string, global = false
|
|
|
709
722
|
return path.join(root, '.gemini', 'GEMINI.md');
|
|
710
723
|
case 'trae':
|
|
711
724
|
return path.join(root, '.trae', 'rules', 'project_rules.md');
|
|
725
|
+
case 'dsh':
|
|
726
|
+
// The user-global AGENTS.md lives in the harness home DSH reads.
|
|
727
|
+
return path.join(process.env.DSH_HOME?.trim() || path.join(root, '.dsh'), 'AGENTS.md');
|
|
712
728
|
default:
|
|
713
729
|
return path.join(root, '.agent', 'rules', 'memorix.md');
|
|
714
730
|
}
|
|
@@ -734,6 +750,8 @@ export function getAgentRulesPath(agent: AgentName, root: string, global = false
|
|
|
734
750
|
return path.join(root, 'GEMINI.md');
|
|
735
751
|
case 'trae':
|
|
736
752
|
return path.join(root, '.trae', 'rules', 'project_rules.md');
|
|
753
|
+
case 'dsh':
|
|
754
|
+
return path.join(root, 'AGENTS.md');
|
|
737
755
|
default:
|
|
738
756
|
return path.join(root, '.agent', 'rules', 'memorix.md');
|
|
739
757
|
}
|
|
@@ -842,6 +860,13 @@ export async function detectInstalledAgents(): Promise<AgentName[]> {
|
|
|
842
860
|
agents.push('trae');
|
|
843
861
|
} catch { /* not installed */ }
|
|
844
862
|
|
|
863
|
+
// Check for DeepSeek Harness (its home dir or an explicit DSH_HOME)
|
|
864
|
+
const dshDir = process.env.DSH_HOME?.trim() || path.join(home, '.dsh');
|
|
865
|
+
try {
|
|
866
|
+
await fs.access(dshDir);
|
|
867
|
+
agents.push('dsh');
|
|
868
|
+
} catch { /* not installed */ }
|
|
869
|
+
|
|
845
870
|
return agents;
|
|
846
871
|
}
|
|
847
872
|
|
|
@@ -947,6 +972,22 @@ export async function installHooks(
|
|
|
947
972
|
generated: { note: 'Trae has no hooks system, only rules (.trae/rules/project_rules.md) installed' },
|
|
948
973
|
};
|
|
949
974
|
}
|
|
975
|
+
case 'dsh':
|
|
976
|
+
// DeepSeek Harness has no hook system — install AGENTS.md guidance and
|
|
977
|
+
// skills; the MCP row is installed by `memorix setup --agent dsh`.
|
|
978
|
+
{
|
|
979
|
+
const rulesPath = await installAgentRules(agent, projectRoot, global);
|
|
980
|
+
const skillPaths = await installOfficialSkillsForAgent(agent, projectRoot, global);
|
|
981
|
+
return {
|
|
982
|
+
agent,
|
|
983
|
+
configPath: rulesPath,
|
|
984
|
+
events: [],
|
|
985
|
+
generated: {
|
|
986
|
+
note: 'DeepSeek Harness has no hook system — installed AGENTS.md guidance and skills; the MCP row is installed by `memorix setup --agent dsh`.',
|
|
987
|
+
...(skillPaths.length > 0 ? { skillPaths, skillPath: skillPaths[0] } : {}),
|
|
988
|
+
},
|
|
989
|
+
};
|
|
990
|
+
}
|
|
950
991
|
case 'opencode': {
|
|
951
992
|
// OpenCode uses JS plugin files for hooks
|
|
952
993
|
const pluginContent = generateOpenCodePlugin();
|
|
@@ -1127,7 +1168,7 @@ async function installAgentRules(agent: AgentName, projectRoot: string, global =
|
|
|
1127
1168
|
try {
|
|
1128
1169
|
await fs.mkdir(path.dirname(rulesPath), { recursive: true });
|
|
1129
1170
|
|
|
1130
|
-
if (agent === 'claude' || agent === 'codex' || agent === 'opencode' || agent === 'antigravity' || agent === 'gemini-cli') {
|
|
1171
|
+
if (agent === 'claude' || agent === 'codex' || agent === 'opencode' || agent === 'antigravity' || agent === 'gemini-cli' || agent === 'dsh') {
|
|
1131
1172
|
// For shared context files (CLAUDE.md / AGENTS.md / GEMINI.md), append rather than overwrite.
|
|
1132
1173
|
try {
|
|
1133
1174
|
const existing = await fs.readFile(rulesPath, 'utf-8');
|
|
@@ -1255,13 +1296,28 @@ function getAgentRulesContent(agent?: AgentName, scope: 'project' | 'global' = '
|
|
|
1255
1296
|
'- For "why" decisions, use \x60memorix_store_reasoning\x60',
|
|
1256
1297
|
'- For a stable fact, reusable procedure, or completed episode that merits deliberate long-term review, include \x60longTerm\x60 in \x60memorix_store\x60 with the appropriate kind and normally \x60scope: "project"\x60. It creates a candidate only: do not use it for routine updates, do not make project-derived evidence portable user memory, and do not assume it enters context until an operator qualifies and approves it through \x60memorix memory long-term\x60.',
|
|
1257
1298
|
'- A \x60user\x60 + \x60portable\x60 durable memory delivered in a task brief is intentionally available across projects. When it matches the task, use it as reusable background even if its origin differs; do not treat it as a current-project fact. Expand it only when needed with \x60memorix_detail\x60 using its \x60durable:<id>\x60 reference and a specific purpose.',
|
|
1299
|
+
'- Record the user profile: the user\u2019s role, expertise, preferences, and goals. Save these with \x60entityName: "user-profile"\x60 and \x60visibility: "personal"\x60 so they stay private and appear in every brief as the "who you are" context.',
|
|
1258
1300
|
'',
|
|
1259
1301
|
"**Don't store:** greetings, simple file reads, trivial commands (ls, pwd, git status).",
|
|
1260
1302
|
'',
|
|
1303
|
+
"**Only store what a future session cannot re-derive.** Code structure, file contents, and Git history are live in the checkout — do not store facts already visible there. A memory earns its place by capturing the why, the context, or a conclusion the checkout alone cannot show.",
|
|
1304
|
+
'',
|
|
1305
|
+
"**Record what worked, not only what failed.** Store validated approaches and explicit user confirmations alongside corrections. Saving only failures drifts behavior away from what the user already accepted; a clear \"yes, that's right\" is feedback worth keeping too.",
|
|
1306
|
+
'',
|
|
1307
|
+
'**Recalled memory is a claim about the past.** A memory naming a specific file, function, or flag describes the past at write time — check the file exists or grep the symbol before recommending it. If the user says to ignore or not use memory, proceed as if memory were empty: do not apply, cite, compare, or mention stored content.',
|
|
1308
|
+
'',
|
|
1261
1309
|
'## When to resolve memory',
|
|
1262
1310
|
'',
|
|
1263
1311
|
'Use \x60memorix_resolve\x60 when a task is done or a bug is fixed. This keeps future searches focused on active work instead of surfacing completed items.',
|
|
1264
1312
|
'',
|
|
1313
|
+
'## End sessions with a summary',
|
|
1314
|
+
'',
|
|
1315
|
+
'When a session finishes, call \x60memorix_session_end\x60 with a short structured summary so the next agent can resume. Recommended sections:',
|
|
1316
|
+
'- **Goal** — what this session was working on',
|
|
1317
|
+
'- **Discoveries** — findings, gotchas, learnings',
|
|
1318
|
+
'- **Accomplished** — completed items, plus PENDING items for the next session',
|
|
1319
|
+
'- **Relevant Files** — paths and what changed',
|
|
1320
|
+
'',
|
|
1265
1321
|
'## Tools quick reference',
|
|
1266
1322
|
'',
|
|
1267
1323
|
'| Tool | Use when |',
|
package/src/hooks/types.ts
CHANGED
|
@@ -41,6 +41,7 @@ export type AgentName =
|
|
|
41
41
|
| 'hermes'
|
|
42
42
|
| 'opencode'
|
|
43
43
|
| 'trae'
|
|
44
|
+
| 'dsh'
|
|
44
45
|
| 'pi'
|
|
45
46
|
| 'omp';
|
|
46
47
|
|
|
@@ -61,6 +62,7 @@ export const AGENT_SUPPORT_TIER: Record<AgentName, SupportTier> = {
|
|
|
61
62
|
hermes: 'community',
|
|
62
63
|
opencode: 'community',
|
|
63
64
|
trae: 'community',
|
|
65
|
+
dsh: 'community',
|
|
64
66
|
pi: 'community',
|
|
65
67
|
omp: 'community',
|
|
66
68
|
antigravity: 'community',
|
|
@@ -150,6 +150,15 @@ export const MEMORIX_INTEGRATIONS: MemorixIntegration[] = [
|
|
|
150
150
|
install: 'memorix setup --agent trae',
|
|
151
151
|
surfaces: ['MCP', 'rules'],
|
|
152
152
|
},
|
|
153
|
+
{
|
|
154
|
+
agent: 'dsh',
|
|
155
|
+
name: 'DeepSeek Harness',
|
|
156
|
+
entry: 'official-config',
|
|
157
|
+
status: 'ready',
|
|
158
|
+
install: 'memorix setup --agent dsh',
|
|
159
|
+
surfaces: ['MCP', 'AGENTS.md', 'skills'],
|
|
160
|
+
note: 'DeepSeek Harness support writes a @deepseek-ai/dsh-mcp-client row into $DSH_HOME/cordis.patch.yml (default ~/.dsh/cordis.patch.yml), guidance into the harness AGENTS.md, and skills under $DSH_HOME/skills.',
|
|
161
|
+
},
|
|
153
162
|
{
|
|
154
163
|
agent: 'memcode',
|
|
155
164
|
name: 'memcode',
|
package/src/knowledge/workset.ts
CHANGED
|
@@ -136,6 +136,12 @@ export interface TaskWorkset {
|
|
|
136
136
|
version: '1.3';
|
|
137
137
|
task: string;
|
|
138
138
|
lens: string;
|
|
139
|
+
/** Always-on "who you are and what this workspace is doing" block. */
|
|
140
|
+
alwaysOn?: {
|
|
141
|
+
profile: string[];
|
|
142
|
+
state?: string;
|
|
143
|
+
durable: string[];
|
|
144
|
+
};
|
|
139
145
|
currentFacts: string[];
|
|
140
146
|
continuation?: WorksetContinuation;
|
|
141
147
|
codeState?: string;
|
|
@@ -197,6 +203,12 @@ export interface BuildTaskWorksetInput {
|
|
|
197
203
|
stale: number;
|
|
198
204
|
};
|
|
199
205
|
runtimeCautions?: WorksetCaution[];
|
|
206
|
+
/** Always-on "who you are and what this workspace is doing" lines. */
|
|
207
|
+
alwaysOn?: {
|
|
208
|
+
profile: string[];
|
|
209
|
+
state?: string;
|
|
210
|
+
durable: string[];
|
|
211
|
+
};
|
|
200
212
|
/** Reader identity used only for team-scoped durable memory filtering. */
|
|
201
213
|
reader?: ObservationReader;
|
|
202
214
|
maxTokens?: number;
|
|
@@ -478,6 +490,44 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
|
|
|
478
490
|
});
|
|
479
491
|
appendLine(lines, 'Task lens: ' + input.lens, maxTokens, omitted, 'lens');
|
|
480
492
|
|
|
493
|
+
// Always-on block: the small "who you are and what this workspace is
|
|
494
|
+
// doing" context every brief carries, mirroring the memory-native feel of
|
|
495
|
+
// a per-session MEMORY.md. Lines are pre-truncated by the caller; the
|
|
496
|
+
// enclosing budget still decides whether they fit.
|
|
497
|
+
if (input.alwaysOn) {
|
|
498
|
+
const hasAlwaysOn = input.alwaysOn.profile.length > 0
|
|
499
|
+
|| Boolean(input.alwaysOn.state)
|
|
500
|
+
|| input.alwaysOn.durable.length > 0;
|
|
501
|
+
if (hasAlwaysOn) {
|
|
502
|
+
appendLine(lines, '', maxTokens, omitted, 'always-on-heading');
|
|
503
|
+
appendLine(lines, 'You and this workspace', maxTokens, omitted, 'always-on-heading');
|
|
504
|
+
for (const profile of input.alwaysOn.profile.slice(0, 2)) {
|
|
505
|
+
appendLine(lines, '- ' + short(profile, 26), maxTokens, omitted, 'always-on-profile', selected, {
|
|
506
|
+
kind: 'memory',
|
|
507
|
+
id: 'profile',
|
|
508
|
+
reason: 'user profile memory',
|
|
509
|
+
trust: 'source-backed',
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
if (input.alwaysOn.state) {
|
|
513
|
+
appendLine(lines, '- Recently: ' + short(input.alwaysOn.state, 40), maxTokens, omitted, 'always-on-state', selected, {
|
|
514
|
+
kind: 'continuation',
|
|
515
|
+
id: 'workspace-state',
|
|
516
|
+
reason: 'latest session summary for this workspace',
|
|
517
|
+
trust: 'historical',
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
for (const durable of input.alwaysOn.durable.slice(0, 2)) {
|
|
521
|
+
appendLine(lines, '- ' + short(durable, 28), maxTokens, omitted, 'always-on-durable', selected, {
|
|
522
|
+
kind: 'durable-memory',
|
|
523
|
+
id: 'durable-always',
|
|
524
|
+
reason: 'recent durable memory for this workspace',
|
|
525
|
+
trust: 'source-backed',
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
481
531
|
const hasContinuation = Boolean(
|
|
482
532
|
input.continuation?.previousSession
|
|
483
533
|
|| (input.continuation?.memories.length ?? 0) > 0
|
|
@@ -1011,6 +1061,14 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
|
|
|
1011
1061
|
version: '1.3' as const,
|
|
1012
1062
|
task,
|
|
1013
1063
|
lens: input.lens,
|
|
1064
|
+
...(input.alwaysOn
|
|
1065
|
+
&& (
|
|
1066
|
+
input.alwaysOn.profile.length > 0
|
|
1067
|
+
|| input.alwaysOn.state
|
|
1068
|
+
|| input.alwaysOn.durable.length > 0
|
|
1069
|
+
)
|
|
1070
|
+
? { alwaysOn: input.alwaysOn }
|
|
1071
|
+
: {}),
|
|
1014
1072
|
currentFacts: input.currentFacts?.map(fact => fact.startsWith('Historical note:')
|
|
1015
1073
|
? short(fact, 48)
|
|
1016
1074
|
: short(fact, 28)).slice(0, 4) ?? [],
|
package/src/memory/long-term.ts
CHANGED
|
@@ -511,13 +511,108 @@ export async function supersedeLongTermMemory(input: {
|
|
|
511
511
|
});
|
|
512
512
|
}
|
|
513
513
|
|
|
514
|
+
/**
|
|
515
|
+
* Rule-leg auto qualification: an explicitly requested candidate carries its
|
|
516
|
+
* own source evidence, so it qualifies immediately instead of waiting for a
|
|
517
|
+
* manual CLI review. Hook-captured and git-derived candidates keep the
|
|
518
|
+
* manual review path. The LLM lane (formation) may enrich evaluation on top
|
|
519
|
+
* of this deterministic rule; no LLM is required for it to work.
|
|
520
|
+
*/
|
|
521
|
+
export async function maybeAutoQualifyLongTermMemory(input: {
|
|
522
|
+
dataDir: string;
|
|
523
|
+
id: string;
|
|
524
|
+
source?: 'agent' | 'git' | 'manual';
|
|
525
|
+
sourceDetail?: 'explicit' | 'hook' | 'git-ingest';
|
|
526
|
+
}): Promise<LongTermMemory | null> {
|
|
527
|
+
if (input.source !== 'manual' && input.sourceDetail !== 'explicit') return null;
|
|
528
|
+
try {
|
|
529
|
+
return await qualifyLongTermMemory({
|
|
530
|
+
dataDir: input.dataDir,
|
|
531
|
+
id: input.id,
|
|
532
|
+
reason: 'auto-qualified: explicit request carries its own source evidence',
|
|
533
|
+
});
|
|
534
|
+
} catch {
|
|
535
|
+
// Already beyond candidate, or the record no longer exists.
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export interface LongTermMaintenanceResult {
|
|
541
|
+
archived: number;
|
|
542
|
+
superseded: number;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Rule-leg self maintenance: stale active records archive themselves, and a
|
|
547
|
+
* newer record supersedes older ones sharing the same title within the same
|
|
548
|
+
* scope and kind. Deterministic and offline; the optional LLM lane can layer
|
|
549
|
+
* semantic consolidation on top later.
|
|
550
|
+
*/
|
|
551
|
+
export async function maintainLongTermMemories(input: {
|
|
552
|
+
dataDir: string;
|
|
553
|
+
projectId: string;
|
|
554
|
+
now?: Date;
|
|
555
|
+
decayDays?: number;
|
|
556
|
+
}): Promise<LongTermMaintenanceResult> {
|
|
557
|
+
const now = input.now ?? new Date();
|
|
558
|
+
const decayDays = input.decayDays ?? 30;
|
|
559
|
+
const cutoff = now.getTime() - decayDays * 24 * 60 * 60 * 1000;
|
|
560
|
+
const result: LongTermMaintenanceResult = { archived: 0, superseded: 0 };
|
|
561
|
+
const store = await createStore(input.dataDir);
|
|
562
|
+
const all = store.list({ originProjectId: input.projectId });
|
|
563
|
+
|
|
564
|
+
// Decay: qualified/approved records with no read or write activity since
|
|
565
|
+
// the cutoff archive themselves.
|
|
566
|
+
for (const memory of all) {
|
|
567
|
+
if (memory.state !== 'qualified' && memory.state !== 'approved') continue;
|
|
568
|
+
const lastActivity = Date.parse(memory.lastAccessedAt ?? memory.updatedAt);
|
|
569
|
+
if (!Number.isFinite(lastActivity) || lastActivity >= cutoff) continue;
|
|
570
|
+
try {
|
|
571
|
+
await archiveLongTermMemory({
|
|
572
|
+
dataDir: input.dataDir,
|
|
573
|
+
id: memory.id,
|
|
574
|
+
reason: 'auto-archive: no activity since ' + new Date(lastActivity).toISOString().slice(0, 10),
|
|
575
|
+
});
|
|
576
|
+
result.archived += 1;
|
|
577
|
+
} catch { /* already inactive */ }
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Supersede: within one scope and kind, records sharing a title keep only
|
|
581
|
+
// the newest; the rest are retired with a traceable supersededBy link.
|
|
582
|
+
const groups = new Map<string, LongTermMemory[]>();
|
|
583
|
+
for (const memory of store.list({ originProjectId: input.projectId })) {
|
|
584
|
+
if (memory.state !== 'qualified' && memory.state !== 'approved') continue;
|
|
585
|
+
const key = memory.scope + '\0' + memory.kind + '\0' + memory.title.trim().toLowerCase();
|
|
586
|
+
const group = groups.get(key) ?? [];
|
|
587
|
+
group.push(memory);
|
|
588
|
+
groups.set(key, group);
|
|
589
|
+
}
|
|
590
|
+
for (const group of groups.values()) {
|
|
591
|
+
if (group.length < 2) continue;
|
|
592
|
+
group.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
|
|
593
|
+
const newest = group[0];
|
|
594
|
+
for (const older of group.slice(1)) {
|
|
595
|
+
try {
|
|
596
|
+
await supersedeLongTermMemory({
|
|
597
|
+
dataDir: input.dataDir,
|
|
598
|
+
id: older.id,
|
|
599
|
+
supersededBy: newest.id,
|
|
600
|
+
reason: 'auto-supersede: a newer record shares the same title',
|
|
601
|
+
});
|
|
602
|
+
result.superseded += 1;
|
|
603
|
+
} catch { /* scope or owner boundary, or already inactive */ }
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return result;
|
|
608
|
+
}
|
|
609
|
+
|
|
514
610
|
export async function listLongTermMemories(input: {
|
|
515
611
|
dataDir: string;
|
|
516
612
|
reader: LongTermMemoryReader;
|
|
517
613
|
includeInactive?: boolean;
|
|
518
614
|
limit?: number;
|
|
519
|
-
}): Promise<Array<{ memory: LongTermMemory; evidence: LongTermMemoryEvidence[] }>> {
|
|
520
|
-
const store = await createStore(input.dataDir);
|
|
615
|
+
}): Promise<Array<{ memory: LongTermMemory; evidence: LongTermMemoryEvidence[] }>> { const store = await createStore(input.dataDir);
|
|
521
616
|
return store.list({ limit: input.limit ?? 100 })
|
|
522
617
|
.filter(memory => canReadLongTermMemory(memory, input.reader))
|
|
523
618
|
.filter(memory => input.includeInactive || memory.state !== 'archived' && memory.state !== 'superseded')
|
|
@@ -20,6 +20,7 @@ export const ISOLATED_MAINTENANCE_JOB_KINDS: readonly MaintenanceJobKind[] = [
|
|
|
20
20
|
'knowledge-compile',
|
|
21
21
|
'knowledge-lint',
|
|
22
22
|
'workflow-index',
|
|
23
|
+
'long-term-maintenance',
|
|
23
24
|
];
|
|
24
25
|
const DEFAULT_TIMEOUT_MS = 5 * 60_000;
|
|
25
26
|
const MAX_CHILD_OUTPUT_BYTES = 1_000_000;
|