memorix 1.2.3 → 1.2.4
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 +20 -0
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/dist/cli/index.js +5187 -4730
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +416 -46
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.js +97 -18
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +20 -0
- package/dist/sdk.js +416 -46
- package/dist/sdk.js.map +1 -1
- package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +86 -0
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +13 -1
- package/docs/API_REFERENCE.md +13 -3
- package/docs/dev-log/progress.txt +48 -7
- package/package.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/src/cli/capability-map.ts +1 -1
- package/src/cli/command-guide.ts +4 -1
- package/src/cli/commands/agent-integrations.ts +5 -1
- package/src/cli/commands/codegraph.ts +1 -1
- package/src/cli/commands/context.ts +9 -1
- package/src/cli/commands/resume.ts +31 -0
- package/src/cli/index.ts +3 -1
- package/src/codegraph/auto-context.ts +54 -1
- package/src/codegraph/task-lens.ts +29 -0
- package/src/compact/token-budget.ts +16 -1
- package/src/config/toml-loader.ts +9 -5
- package/src/hooks/handler.ts +127 -66
- package/src/hooks/installers/index.ts +5 -4
- package/src/hooks/official-skills.ts +6 -4
- package/src/hooks/rules/memorix-agent-rules.md +9 -7
- package/src/knowledge/context-assembly.ts +4 -1
- package/src/knowledge/workset.ts +89 -1
- package/src/memory/session.ts +144 -10
- package/src/server.ts +137 -8
- package/src/store/bun-sqlite-compat.ts +118 -15
- package/src/store/sqlite-db.ts +3 -3
package/src/memory/session.ts
CHANGED
|
@@ -22,6 +22,17 @@ import { redactCredentials, sanitizeCredentials } from './secret-filter.js';
|
|
|
22
22
|
import { canReadObservation } from './visibility.js';
|
|
23
23
|
|
|
24
24
|
const PRIORITY_TYPES = new Set(['gotcha', 'decision', 'problem-solution', 'trade-off', 'discovery']);
|
|
25
|
+
const RESUME_TYPES = new Set([
|
|
26
|
+
'gotcha',
|
|
27
|
+
'decision',
|
|
28
|
+
'problem-solution',
|
|
29
|
+
'trade-off',
|
|
30
|
+
'discovery',
|
|
31
|
+
'how-it-works',
|
|
32
|
+
'what-changed',
|
|
33
|
+
'reasoning',
|
|
34
|
+
'session-request',
|
|
35
|
+
]);
|
|
25
36
|
const TYPE_EMOJI: Record<string, string> = {
|
|
26
37
|
'gotcha': '[DISCOVERY]',
|
|
27
38
|
'decision': '[WHY]',
|
|
@@ -178,6 +189,138 @@ function isSystemSelfObservation(obs: Observation): boolean {
|
|
|
178
189
|
return SYSTEM_SELF_PATTERNS.some((pattern) => pattern.test(text));
|
|
179
190
|
}
|
|
180
191
|
|
|
192
|
+
export interface SessionResumeMemory {
|
|
193
|
+
id: number;
|
|
194
|
+
title: string;
|
|
195
|
+
type: string;
|
|
196
|
+
detail?: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** A compact, source-aware view of prior work for a task continuation. */
|
|
200
|
+
export interface SessionResumeBrief {
|
|
201
|
+
previousSession?: {
|
|
202
|
+
id: string;
|
|
203
|
+
agent?: string;
|
|
204
|
+
endedAt?: string;
|
|
205
|
+
summary: string;
|
|
206
|
+
};
|
|
207
|
+
memories: SessionResumeMemory[];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function readerForAlias(
|
|
211
|
+
reader: ObservationReader | undefined,
|
|
212
|
+
aliases: Set<string>,
|
|
213
|
+
observation: Observation,
|
|
214
|
+
): ObservationReader | undefined {
|
|
215
|
+
if (!reader) return undefined;
|
|
216
|
+
return reader.projectId && aliases.has(observation.projectId)
|
|
217
|
+
? { ...reader, projectId: observation.projectId }
|
|
218
|
+
: reader;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function readableAliasObservations(
|
|
222
|
+
observations: Observation[],
|
|
223
|
+
aliases: Set<string>,
|
|
224
|
+
reader?: ObservationReader,
|
|
225
|
+
): Observation[] {
|
|
226
|
+
return reader
|
|
227
|
+
? observations.filter((observation) => canReadObservation(
|
|
228
|
+
observation,
|
|
229
|
+
readerForAlias(reader, aliases, observation),
|
|
230
|
+
))
|
|
231
|
+
: observations;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isUsefulSessionSummary(summary: string | undefined): summary is string {
|
|
235
|
+
return Boolean(summary)
|
|
236
|
+
&& !NOISE_PATTERNS.some((pattern) => pattern.test(summary!))
|
|
237
|
+
&& !SYSTEM_SELF_PATTERNS.some((pattern) => pattern.test(summary!));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function continuationTaskTokens(task?: string): string[] {
|
|
241
|
+
return [...new Set(
|
|
242
|
+
(task?.toLowerCase().match(/[a-z0-9_./-]+|[\u4e00-\u9fff]+/g) ?? [])
|
|
243
|
+
.map((token) => token.trim())
|
|
244
|
+
.filter((token) => token.length > 1 && !['continue', 'resume', '继续', '接手', '恢复'].includes(token)),
|
|
245
|
+
)].slice(0, 8);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function continuationScore(observation: Observation, projectTokens: string[], task?: string): number {
|
|
249
|
+
let score = scoreObservationForSessionContext(observation, projectTokens);
|
|
250
|
+
if (observation.type === 'session-request' || observation.type === 'what-changed') score += 1;
|
|
251
|
+
const text = stringifyObservation(observation);
|
|
252
|
+
const matches = continuationTaskTokens(task).filter((token) => text.includes(token)).length;
|
|
253
|
+
score += Math.min(matches, 2) * 2;
|
|
254
|
+
return score;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Return only the latest meaningful session summary and a few durable memory
|
|
259
|
+
* anchors. It is deliberately separate from the verbose session-context view:
|
|
260
|
+
* this is a delivery projection for one bounded Task Workset, not another
|
|
261
|
+
* storage model or an automatic transcript dump.
|
|
262
|
+
*/
|
|
263
|
+
export async function getSessionResumeBrief(
|
|
264
|
+
projectId: string,
|
|
265
|
+
task?: string,
|
|
266
|
+
reader?: ObservationReader,
|
|
267
|
+
): Promise<SessionResumeBrief> {
|
|
268
|
+
const aliasSet = await resolveProjectIds(projectId);
|
|
269
|
+
const [sessions, allObs] = await Promise.all([
|
|
270
|
+
loadAliasSessions(aliasSet),
|
|
271
|
+
loadAliasActiveObservations(aliasSet),
|
|
272
|
+
]);
|
|
273
|
+
const readableObs = readableAliasObservations(allObs, aliasSet, reader);
|
|
274
|
+
const previous = sessions
|
|
275
|
+
.filter((session) => session.status === 'completed')
|
|
276
|
+
.sort((a, b) => new Date(b.endedAt || b.startedAt).getTime() - new Date(a.endedAt || a.startedAt).getTime())
|
|
277
|
+
.find((session) => (
|
|
278
|
+
session.summary
|
|
279
|
+
&& session.summary !== '(session ended implicitly by new session start)'
|
|
280
|
+
&& isUsefulSessionSummary(session.summary)
|
|
281
|
+
));
|
|
282
|
+
|
|
283
|
+
const projectTokens = tokenizeProjectId(projectId);
|
|
284
|
+
const memories = readableObs
|
|
285
|
+
.filter((observation) => RESUME_TYPES.has(observation.type))
|
|
286
|
+
.filter((observation) => classifyLayer(observation) === 'L2')
|
|
287
|
+
.filter((observation) => !isNoiseObservation(observation) && !isSystemSelfObservation(observation))
|
|
288
|
+
.map((observation) => ({
|
|
289
|
+
observation,
|
|
290
|
+
score: continuationScore(observation, projectTokens, task),
|
|
291
|
+
}))
|
|
292
|
+
.sort((a, b) => (
|
|
293
|
+
b.score - a.score
|
|
294
|
+
|| new Date(b.observation.createdAt).getTime() - new Date(a.observation.createdAt).getTime()
|
|
295
|
+
|| a.observation.id - b.observation.id
|
|
296
|
+
))
|
|
297
|
+
.slice(0, 3)
|
|
298
|
+
.map(({ observation }) => ({
|
|
299
|
+
id: observation.id,
|
|
300
|
+
title: sanitizeCredentials(observation.title),
|
|
301
|
+
type: observation.type,
|
|
302
|
+
...(observation.facts?.[0]
|
|
303
|
+
? { detail: sanitizeCredentials(observation.facts[0]) }
|
|
304
|
+
: observation.narrative
|
|
305
|
+
? { detail: sanitizeCredentials(observation.narrative) }
|
|
306
|
+
: {}),
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
...(previous?.summary
|
|
311
|
+
? {
|
|
312
|
+
previousSession: {
|
|
313
|
+
id: previous.id,
|
|
314
|
+
...(previous.agent ? { agent: previous.agent } : {}),
|
|
315
|
+
...(previous.endedAt ? { endedAt: previous.endedAt } : {}),
|
|
316
|
+
summary: sanitizeCredentials(previous.summary),
|
|
317
|
+
},
|
|
318
|
+
}
|
|
319
|
+
: {}),
|
|
320
|
+
memories,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
181
324
|
export function scoreObservationForSessionContext(obs: Observation, projectTokens: string[], now = Date.now()): number {
|
|
182
325
|
let score = TYPE_WEIGHTS[obs.type] ?? 1;
|
|
183
326
|
const text = stringifyObservation(obs);
|
|
@@ -322,16 +465,7 @@ export async function getSessionContext(
|
|
|
322
465
|
loadAliasSessions(aliasSet),
|
|
323
466
|
loadAliasActiveObservations(aliasSet),
|
|
324
467
|
]);
|
|
325
|
-
const readableObs = reader
|
|
326
|
-
? allObs.filter((observation) => {
|
|
327
|
-
// Aliases represent one project across moved/renamed worktrees. Preserve
|
|
328
|
-
// that project equivalence while still enforcing the caller's identity.
|
|
329
|
-
const observationReader = reader.projectId && aliasSet.has(observation.projectId)
|
|
330
|
-
? { ...reader, projectId: observation.projectId }
|
|
331
|
-
: reader;
|
|
332
|
-
return canReadObservation(observation, observationReader);
|
|
333
|
-
})
|
|
334
|
-
: allObs;
|
|
468
|
+
const readableObs = readableAliasObservations(allObs, aliasSet, reader);
|
|
335
469
|
/** Check if a session summary contains noise/system-self content */
|
|
336
470
|
const isNoisySummary = (summary: string | undefined): boolean => {
|
|
337
471
|
if (!summary) return false;
|
package/src/server.ts
CHANGED
|
@@ -228,6 +228,9 @@ export const BOOTSTRAP_SAFE_TOOL_NAMES = new Set([
|
|
|
228
228
|
'memorix_context_pack',
|
|
229
229
|
]);
|
|
230
230
|
|
|
231
|
+
const AUTOPILOT_RETRIEVAL_BOUNDARY_TTL_MS = 2 * 60 * 1000;
|
|
232
|
+
const READ_ONLY_TASK_PATTERN = /\b(?:do not|don't|never)\s+(?:modify|edit|change|write)\b|\bread[- ]only\b|(?:不要|不准|勿|禁止).{0,6}(?:修改|编辑|写入)|只读/i;
|
|
233
|
+
|
|
231
234
|
export function shouldAwaitProjectRuntime(toolName: string): boolean {
|
|
232
235
|
return !BOOTSTRAP_SAFE_TOOL_NAMES.has(toolName);
|
|
233
236
|
}
|
|
@@ -602,6 +605,63 @@ export async function createMemorixServer(
|
|
|
602
605
|
};
|
|
603
606
|
};
|
|
604
607
|
|
|
608
|
+
// A complete Autopilot brief is the normal retrieval boundary for one coding
|
|
609
|
+
// turn. Keep accidental search/detail loops cheap, while preserving an
|
|
610
|
+
// explicit escape hatch when a caller really needs deeper history.
|
|
611
|
+
let autopilotRetrievalBoundary: {
|
|
612
|
+
projectId: string;
|
|
613
|
+
issuedAt: number;
|
|
614
|
+
coveredObservationIds: Set<number>;
|
|
615
|
+
readOnly: boolean;
|
|
616
|
+
} | null = null;
|
|
617
|
+
const getActiveAutopilotRetrievalBoundary = () => {
|
|
618
|
+
const boundary = autopilotRetrievalBoundary;
|
|
619
|
+
if (!boundary) return null;
|
|
620
|
+
if (boundary.projectId === project.id && Date.now() - boundary.issuedAt <= AUTOPILOT_RETRIEVAL_BOUNDARY_TTL_MS) {
|
|
621
|
+
return boundary;
|
|
622
|
+
}
|
|
623
|
+
autopilotRetrievalBoundary = null;
|
|
624
|
+
return null;
|
|
625
|
+
};
|
|
626
|
+
const requireExplicitAutopilotExpansion = (toolName: string, purpose?: string) => {
|
|
627
|
+
if (!getActiveAutopilotRetrievalBoundary() || purpose?.trim()) return null;
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
content: [{
|
|
631
|
+
type: 'text' as const,
|
|
632
|
+
text:
|
|
633
|
+
'Memorix Autopilot retrieval boundary: the latest `memorix_project_context` already supplied the default bounded brief for this coding turn, so no additional memory was retrieved. ' +
|
|
634
|
+
'Verify the current project first. To intentionally expand beyond that brief, call `' + toolName + '` again with `purpose` naming the specific missing fact or the user\'s explicit request.',
|
|
635
|
+
}],
|
|
636
|
+
};
|
|
637
|
+
};
|
|
638
|
+
const blockCoveredAutopilotEvidence = (toolName: string, observationIds: number[], force?: boolean) => {
|
|
639
|
+
const boundary = getActiveAutopilotRetrievalBoundary();
|
|
640
|
+
if (!boundary || force || observationIds.length === 0 || !observationIds.every(id => boundary.coveredObservationIds.has(id))) {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
content: [{
|
|
645
|
+
type: 'text' as const,
|
|
646
|
+
text:
|
|
647
|
+
'Memorix Autopilot retrieval boundary: every requested memory is already represented in the latest project brief, so no duplicate detail was retrieved. ' +
|
|
648
|
+
'Inspect the current project or seek a new source. Retry with `force: true` only when the user explicitly asks to read the underlying record in full.',
|
|
649
|
+
}],
|
|
650
|
+
};
|
|
651
|
+
};
|
|
652
|
+
const blockReadOnlyAutopilotWrite = (overrideReadOnly?: boolean) => {
|
|
653
|
+
const boundary = getActiveAutopilotRetrievalBoundary();
|
|
654
|
+
if (!boundary?.readOnly || overrideReadOnly) return null;
|
|
655
|
+
return {
|
|
656
|
+
content: [{
|
|
657
|
+
type: 'text' as const,
|
|
658
|
+
text:
|
|
659
|
+
'Memorix write boundary: the latest task is read-only or asks not to modify files, so no memory was stored. ' +
|
|
660
|
+
'Persist a record only when the user explicitly asks to save it, then retry with `overrideReadOnly: true`.',
|
|
661
|
+
}],
|
|
662
|
+
};
|
|
663
|
+
};
|
|
664
|
+
|
|
605
665
|
// ================================================================
|
|
606
666
|
// Memorix Extended Tools (3-layer Progressive Disclosure)
|
|
607
667
|
// ================================================================
|
|
@@ -622,7 +682,8 @@ export async function createMemorixServer(
|
|
|
622
682
|
'problem-solution ([FIX] bug fix), how-it-works ([INFO] explanation), what-changed ([CHANGE] change), ' +
|
|
623
683
|
'discovery ([DISCOVERY] insight), why-it-exists ([WHY] rationale), trade-off ([TRADEOFF] compromise), ' +
|
|
624
684
|
'session-request ([SESSION] original goal). ' +
|
|
625
|
-
'Project visibility is the default. Personal or team visibility requires an explicitly joined coordination identity.'
|
|
685
|
+
'Project visibility is the default. Personal or team visibility requires an explicitly joined coordination identity. ' +
|
|
686
|
+
'For a read-only task, do not store unless the user explicitly asks to save a record.',
|
|
626
687
|
inputSchema: {
|
|
627
688
|
entityName: z.string().describe('The entity this observation belongs to (e.g., "auth-module", "port-config")'),
|
|
628
689
|
type: z.enum(OBSERVATION_TYPES).describe('Observation type for classification'),
|
|
@@ -646,11 +707,16 @@ export async function createMemorixServer(
|
|
|
646
707
|
visibility: z.enum(['personal', 'project', 'team']).optional().describe(
|
|
647
708
|
'Retrieval scope. Project is the normal shared default; personal/team require memorix_session_start with joinTeam=true.',
|
|
648
709
|
),
|
|
710
|
+
overrideReadOnly: z.boolean().optional().describe(
|
|
711
|
+
'Use only when the user explicitly asks to save memory during a read-only or no-modification task.',
|
|
712
|
+
),
|
|
649
713
|
},
|
|
650
714
|
},
|
|
651
|
-
async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility }) => {
|
|
715
|
+
async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility, overrideReadOnly }) => {
|
|
652
716
|
const unresolved = requireResolvedProject('store memory in the current project');
|
|
653
717
|
if (unresolved) return unresolved;
|
|
718
|
+
const readOnlyBoundary = blockReadOnlyAutopilotWrite(overrideReadOnly);
|
|
719
|
+
if (readOnlyBoundary) return readOnlyBoundary;
|
|
654
720
|
const requestedVisibility = (visibility ?? 'project') as 'personal' | 'project' | 'team';
|
|
655
721
|
const reader = getObservationReader();
|
|
656
722
|
if (requestedVisibility !== 'project' && !currentAgentId) {
|
|
@@ -1224,6 +1290,7 @@ export async function createMemorixServer(
|
|
|
1224
1290
|
title: 'Search Memory',
|
|
1225
1291
|
description:
|
|
1226
1292
|
'Search project memory. Returns a compact index (~50-100 tokens/result). ' +
|
|
1293
|
+
'Do not use as a follow-up to a complete memorix_project_context brief unless a specific fact is still missing or the user asks for deeper history; provide purpose when intentionally expanding. ' +
|
|
1227
1294
|
'Use memorix_detail to fetch full content for specific IDs. ' +
|
|
1228
1295
|
'Use memorix_timeline to see chronological context. ' +
|
|
1229
1296
|
'Searches across all observations stored from any IDE session — enabling cross-session and cross-agent context retrieval.',
|
|
@@ -1243,13 +1310,23 @@ export async function createMemorixServer(
|
|
|
1243
1310
|
source: z.enum(['agent', 'git', 'manual']).optional().describe(
|
|
1244
1311
|
'Filter by memory source. "git" returns only commit-derived ground truth memories. Omit for all sources.',
|
|
1245
1312
|
),
|
|
1313
|
+
purpose: z.string().optional().describe(
|
|
1314
|
+
'Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user\'s explicit request.',
|
|
1315
|
+
),
|
|
1316
|
+
force: z.boolean().optional().describe(
|
|
1317
|
+
'Use only when the user explicitly asks to read a record already represented in the latest Autopilot brief.',
|
|
1318
|
+
),
|
|
1246
1319
|
},
|
|
1247
1320
|
},
|
|
1248
|
-
async ({ query, limit, type, maxTokens, scope, since, until, status, source }) => {
|
|
1321
|
+
async ({ query, limit, type, maxTokens, scope, since, until, status, source, purpose, force }) => {
|
|
1249
1322
|
if (scope !== 'global') {
|
|
1250
1323
|
const unresolved = requireResolvedProject('search the current project');
|
|
1251
1324
|
if (unresolved) return unresolved;
|
|
1252
1325
|
}
|
|
1326
|
+
if (scope !== 'global') {
|
|
1327
|
+
const boundary = requireExplicitAutopilotExpansion('memorix_search', purpose);
|
|
1328
|
+
if (boundary) return boundary;
|
|
1329
|
+
}
|
|
1253
1330
|
return withFreshIndex(async () => {
|
|
1254
1331
|
|
|
1255
1332
|
const safeLimit = limit != null ? coerceNumber(limit, 20) : undefined;
|
|
@@ -1295,6 +1372,18 @@ export async function createMemorixServer(
|
|
|
1295
1372
|
throw error;
|
|
1296
1373
|
}
|
|
1297
1374
|
|
|
1375
|
+
const activeBoundary = getActiveAutopilotRetrievalBoundary();
|
|
1376
|
+
if (
|
|
1377
|
+
scope !== 'global'
|
|
1378
|
+
&& activeBoundary
|
|
1379
|
+
&& !force
|
|
1380
|
+
&& result.entries.length > 0
|
|
1381
|
+
&& result.entries.every(entry => activeBoundary.coveredObservationIds.has(entry.id))
|
|
1382
|
+
) {
|
|
1383
|
+
const duplicate = blockCoveredAutopilotEvidence('memorix_search', result.entries.map(entry => entry.id), force);
|
|
1384
|
+
if (duplicate) return duplicate;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1298
1387
|
// Append retrieval diagnostics only; do not mix workspace-sync guidance into memory results.
|
|
1299
1388
|
let text = result.formatted;
|
|
1300
1389
|
try {
|
|
@@ -1414,6 +1503,7 @@ export async function createMemorixServer(
|
|
|
1414
1503
|
observations,
|
|
1415
1504
|
task,
|
|
1416
1505
|
refresh: refresh ?? 'auto',
|
|
1506
|
+
reader: getObservationReader(),
|
|
1417
1507
|
enqueueRefresh: () => {
|
|
1418
1508
|
enqueueCodegraphRefresh({
|
|
1419
1509
|
dataDir: projectDir,
|
|
@@ -1430,6 +1520,17 @@ export async function createMemorixServer(
|
|
|
1430
1520
|
? formatAutoProjectContextSummary(context)
|
|
1431
1521
|
: formatAutoProjectContextPrompt(context);
|
|
1432
1522
|
|
|
1523
|
+
autopilotRetrievalBoundary = {
|
|
1524
|
+
projectId: project.id,
|
|
1525
|
+
issuedAt: Date.now(),
|
|
1526
|
+
coveredObservationIds: new Set([
|
|
1527
|
+
...(context.workset.continuation?.memories.map(memory => memory.id) ?? []),
|
|
1528
|
+
...context.workset.reliableMemory.map(memory => memory.id),
|
|
1529
|
+
...context.workset.cautionMemory.map(memory => memory.id),
|
|
1530
|
+
]),
|
|
1531
|
+
readOnly: READ_ONLY_TASK_PATTERN.test(task ?? ''),
|
|
1532
|
+
};
|
|
1533
|
+
|
|
1433
1534
|
return {
|
|
1434
1535
|
content: [{ type: 'text' as const, text }],
|
|
1435
1536
|
};
|
|
@@ -1475,18 +1576,24 @@ export async function createMemorixServer(
|
|
|
1475
1576
|
title: 'Context Pack',
|
|
1476
1577
|
description:
|
|
1477
1578
|
'Build a prompt-ready working context pack for a coding task. ' +
|
|
1478
|
-
'Combines relevant memories, CodeGraph Memory facts, freshness warnings, suggested reads, and verification hints.'
|
|
1579
|
+
'Combines relevant memories, CodeGraph Memory facts, freshness warnings, suggested reads, and verification hints. ' +
|
|
1580
|
+
'After a complete memorix_project_context brief, provide purpose only when deliberately expanding beyond it.',
|
|
1479
1581
|
inputSchema: {
|
|
1480
1582
|
task: z.string().describe('Current coding task or question'),
|
|
1481
1583
|
limit: z.preprocess(
|
|
1482
1584
|
value => (typeof value === 'string' && value.trim() !== '' ? Number(value) : value),
|
|
1483
1585
|
z.number().int().positive().max(100),
|
|
1484
1586
|
).optional().describe('Max active memories to inspect before code-ref filtering (default: 20)'),
|
|
1587
|
+
purpose: z.string().optional().describe(
|
|
1588
|
+
'Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user\'s explicit request.',
|
|
1589
|
+
),
|
|
1485
1590
|
},
|
|
1486
1591
|
},
|
|
1487
|
-
async ({ task, limit }) => {
|
|
1592
|
+
async ({ task, limit, purpose }) => {
|
|
1488
1593
|
const unresolved = requireResolvedProject('build a context pack for the current project');
|
|
1489
1594
|
if (unresolved) return unresolved;
|
|
1595
|
+
const boundary = requireExplicitAutopilotExpansion('memorix_context_pack', purpose);
|
|
1596
|
+
if (boundary) return boundary;
|
|
1490
1597
|
|
|
1491
1598
|
const [
|
|
1492
1599
|
{ CodeGraphStore },
|
|
@@ -2380,6 +2487,7 @@ export async function createMemorixServer(
|
|
|
2380
2487
|
description:
|
|
2381
2488
|
'Fetch full observation or mini-skill details — includes source kind (explicit memory / hook trace / git evidence), ' +
|
|
2382
2489
|
'value category, and cross-references (~500-1000 tokens each). ' +
|
|
2490
|
+
'Do not re-fetch content already covered by a complete memorix_project_context brief unless a specific fact is still missing or the user asks for deeper history; provide purpose when intentionally expanding. ' +
|
|
2383
2491
|
'Always use memorix_search first to find relevant IDs, then fetch only what you need. ' +
|
|
2384
2492
|
'Accepts typed refs from search results (e.g. "obs:42", "skill:3") via the typedRefs field, ' +
|
|
2385
2493
|
'or legacy numeric ids / object refs for backward compatibility.',
|
|
@@ -2392,19 +2500,39 @@ export async function createMemorixServer(
|
|
|
2392
2500
|
}),
|
|
2393
2501
|
).optional().describe('Explicit observation refs. Prefer this for global search results.'),
|
|
2394
2502
|
typedRefs: z.array(z.string()).optional().describe('Typed memory refs from search results, e.g. "obs:42", "skill:3", "obs:42@org/proj"'),
|
|
2503
|
+
purpose: z.string().optional().describe(
|
|
2504
|
+
'Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user\'s explicit request.',
|
|
2505
|
+
),
|
|
2506
|
+
force: z.boolean().optional().describe(
|
|
2507
|
+
'Use only when the user explicitly asks to read a record already represented in the latest Autopilot brief.',
|
|
2508
|
+
),
|
|
2395
2509
|
},
|
|
2396
2510
|
},
|
|
2397
|
-
async ({ ids, refs, typedRefs }) => {
|
|
2511
|
+
async ({ ids, refs, typedRefs, purpose, force }) => {
|
|
2398
2512
|
// Defensive coercion: Claude Code CLI + GLM may send "[16]" instead of [16]
|
|
2399
2513
|
const safeIds = coerceNumberArray(ids);
|
|
2400
2514
|
const safeRefs = coerceObservationRefs(refs);
|
|
2401
2515
|
const safeTypedRefs = coerceStringArray(typedRefs);
|
|
2516
|
+
const hasCrossProjectRef = safeRefs.some((ref) => ref.projectId && ref.projectId !== project.id)
|
|
2517
|
+
|| safeTypedRefs.some((ref) => ref.includes('@') && !ref.endsWith(`@${project.id}`));
|
|
2518
|
+
if (!hasCrossProjectRef) {
|
|
2519
|
+
const boundary = requireExplicitAutopilotExpansion('memorix_detail', purpose);
|
|
2520
|
+
if (boundary) return boundary;
|
|
2521
|
+
const requestedObservationIds = [
|
|
2522
|
+
...safeIds,
|
|
2523
|
+
...safeRefs.map(ref => ref.id),
|
|
2524
|
+
...safeTypedRefs.flatMap((ref) => {
|
|
2525
|
+
const match = /^obs:(\d+)(?:@.+)?$/i.exec(ref.trim());
|
|
2526
|
+
return match ? [Number(match[1])] : [];
|
|
2527
|
+
}),
|
|
2528
|
+
];
|
|
2529
|
+
const duplicate = blockCoveredAutopilotEvidence('memorix_detail', requestedObservationIds, force);
|
|
2530
|
+
if (duplicate) return duplicate;
|
|
2531
|
+
}
|
|
2402
2532
|
|
|
2403
2533
|
// Priority: typedRefs > refs > ids (each is a complete, homogeneous input path)
|
|
2404
2534
|
let result;
|
|
2405
2535
|
try {
|
|
2406
|
-
const hasCrossProjectRef = safeRefs.some((ref) => ref.projectId && ref.projectId !== project.id)
|
|
2407
|
-
|| safeTypedRefs.some((ref) => ref.includes('@') && !ref.endsWith(`@${project.id}`));
|
|
2408
2536
|
const reader = getObservationReader(hasCrossProjectRef ? 'global' : 'project');
|
|
2409
2537
|
if (safeTypedRefs.length > 0) {
|
|
2410
2538
|
// Pass typed ref strings directly — compactDetail handles parsing
|
|
@@ -4825,6 +4953,7 @@ export async function createMemorixServer(
|
|
|
4825
4953
|
|
|
4826
4954
|
// Phase 4a: clear agent identity — old project's agent is not valid in new project
|
|
4827
4955
|
currentAgentId = undefined;
|
|
4956
|
+
autopilotRetrievalBoundary = null;
|
|
4828
4957
|
|
|
4829
4958
|
// Re-resolve data dir with canonical ID (may differ from raw detected ID)
|
|
4830
4959
|
const canonicalProjectDir = newCanonicalId !== newDetected.id
|
|
@@ -1,21 +1,120 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bun SQLite Compatibility Layer
|
|
3
3
|
*
|
|
4
|
-
* Provides a better-sqlite3
|
|
5
|
-
*
|
|
4
|
+
* Provides a small better-sqlite3 compatibility layer for the available local
|
|
5
|
+
* SQLite implementation. Node 22 ships node:sqlite, which keeps Memorix
|
|
6
|
+
* usable when an optional better-sqlite3 native binary is unavailable.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { createRequire } from 'node:module';
|
|
9
10
|
|
|
10
11
|
let Database: any;
|
|
12
|
+
let driver: 'better-sqlite3' | 'node:sqlite' | 'bun:sqlite' | undefined;
|
|
11
13
|
const requireFromHere = createRequire(import.meta.url);
|
|
12
14
|
|
|
15
|
+
function loadNodeSqlite(): any {
|
|
16
|
+
const nodeSqlite = requireFromHere('node:sqlite');
|
|
17
|
+
return nodeSqlite.DatabaseSync;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isNativeBindingFailure(error: unknown): boolean {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
return /could not locate the bindings file|better_sqlite3\.node|module did not self-register/i.test(message);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function addCompatibility(db: any): any {
|
|
26
|
+
// Bun and node:sqlite do not expose better-sqlite3's pragma helper.
|
|
27
|
+
if (!db.pragma) {
|
|
28
|
+
db.pragma = function (pragma: string, options?: { simple?: boolean }) {
|
|
29
|
+
const statement = db.prepare(`PRAGMA ${pragma}`);
|
|
30
|
+
if (options?.simple) {
|
|
31
|
+
const result = statement.get();
|
|
32
|
+
return result ? Object.values(result)[0] : undefined;
|
|
33
|
+
}
|
|
34
|
+
return statement.all();
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Stores use better-sqlite3's synchronous transaction factory. Keep nested
|
|
39
|
+
// calls correct with savepoints instead of silently committing inner work.
|
|
40
|
+
if (!db.transaction) {
|
|
41
|
+
let depth = 0;
|
|
42
|
+
let sequence = 0;
|
|
43
|
+
db.transaction = function <T>(fn: (...args: any[]) => T) {
|
|
44
|
+
return (...args: any[]): T => {
|
|
45
|
+
const outermost = depth === 0;
|
|
46
|
+
const savepoint = `memorix_tx_${++sequence}`;
|
|
47
|
+
if (outermost) {
|
|
48
|
+
db.exec('BEGIN IMMEDIATE');
|
|
49
|
+
} else {
|
|
50
|
+
db.exec(`SAVEPOINT ${savepoint}`);
|
|
51
|
+
}
|
|
52
|
+
depth += 1;
|
|
53
|
+
try {
|
|
54
|
+
const result = fn(...args);
|
|
55
|
+
if (result && typeof (result as any).then === 'function') {
|
|
56
|
+
throw new Error('[memorix] SQLite transactions must be synchronous');
|
|
57
|
+
}
|
|
58
|
+
depth -= 1;
|
|
59
|
+
if (outermost) {
|
|
60
|
+
db.exec('COMMIT');
|
|
61
|
+
} else {
|
|
62
|
+
db.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
depth -= 1;
|
|
67
|
+
try {
|
|
68
|
+
if (outermost) {
|
|
69
|
+
db.exec('ROLLBACK');
|
|
70
|
+
} else {
|
|
71
|
+
db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
72
|
+
db.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Preserve the original application failure when rollback itself fails.
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return db;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function instantiateDatabase(Sqlite: any, filePath: string, options?: any): any {
|
|
87
|
+
// DatabaseSync validates that an explicitly supplied second argument is an
|
|
88
|
+
// object, unlike better-sqlite3 which accepts undefined.
|
|
89
|
+
return driver === 'node:sqlite' && options === undefined
|
|
90
|
+
? new Sqlite(filePath)
|
|
91
|
+
: new Sqlite(filePath, options);
|
|
92
|
+
}
|
|
93
|
+
|
|
13
94
|
export function loadSqlite(): any {
|
|
14
95
|
if (Database) return Database;
|
|
15
96
|
|
|
97
|
+
// Kept as an operational escape hatch and a regression-test seam. Node 22
|
|
98
|
+
// is the supported minimum runtime, so this does not expand the platform
|
|
99
|
+
// contract beyond package.json's existing engine requirement.
|
|
100
|
+
if (process.env.MEMORIX_SQLITE_DRIVER === 'node') {
|
|
101
|
+
Database = loadNodeSqlite();
|
|
102
|
+
driver = 'node:sqlite';
|
|
103
|
+
return Database;
|
|
104
|
+
}
|
|
105
|
+
|
|
16
106
|
// Try better-sqlite3 first (Node.js)
|
|
17
107
|
try {
|
|
18
108
|
Database = requireFromHere('better-sqlite3');
|
|
109
|
+
driver = 'better-sqlite3';
|
|
110
|
+
return Database;
|
|
111
|
+
} catch {
|
|
112
|
+
// Fall through to node:sqlite, then Bun's runtime implementation.
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
Database = loadNodeSqlite();
|
|
117
|
+
driver = 'node:sqlite';
|
|
19
118
|
return Database;
|
|
20
119
|
} catch {
|
|
21
120
|
// Fall through to bun:sqlite
|
|
@@ -26,9 +125,10 @@ export function loadSqlite(): any {
|
|
|
26
125
|
// bun:sqlite is a Bun built-in
|
|
27
126
|
const bunSqlite = requireFromHere('bun:sqlite');
|
|
28
127
|
Database = bunSqlite.Database;
|
|
128
|
+
driver = 'bun:sqlite';
|
|
29
129
|
return Database;
|
|
30
130
|
} catch {
|
|
31
|
-
throw new Error('[memorix]
|
|
131
|
+
throw new Error('[memorix] SQLite is unavailable (better-sqlite3, node:sqlite, and bun:sqlite failed)');
|
|
32
132
|
}
|
|
33
133
|
}
|
|
34
134
|
|
|
@@ -38,18 +138,21 @@ export function loadSqlite(): any {
|
|
|
38
138
|
*/
|
|
39
139
|
export function createDatabase(path: string, options?: any): any {
|
|
40
140
|
const Sqlite = loadSqlite();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
};
|
|
141
|
+
try {
|
|
142
|
+
return addCompatibility(instantiateDatabase(Sqlite, path, options));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
// require('better-sqlite3') can succeed even when npm had no prebuild and
|
|
145
|
+
// no local compiler. Fall back to Node's built-in SQLite for that one
|
|
146
|
+
// recoverable case; real open/corruption errors still surface unchanged.
|
|
147
|
+
if (driver !== 'better-sqlite3' || !isNativeBindingFailure(error)) throw error;
|
|
148
|
+
Database = loadNodeSqlite();
|
|
149
|
+
driver = 'node:sqlite';
|
|
150
|
+
return addCompatibility(instantiateDatabase(Database, path, options));
|
|
52
151
|
}
|
|
152
|
+
}
|
|
53
153
|
|
|
54
|
-
|
|
154
|
+
/** Test-only reset for the driver cache used by the forced node:sqlite path. */
|
|
155
|
+
export function resetSqliteDriverForTests(): void {
|
|
156
|
+
Database = undefined;
|
|
157
|
+
driver = undefined;
|
|
55
158
|
}
|
package/src/store/sqlite-db.ts
CHANGED
|
@@ -17,7 +17,7 @@ import path from 'node:path';
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import { createDatabase, loadSqlite } from './bun-sqlite-compat.js';
|
|
19
19
|
|
|
20
|
-
// Dynamic require for SQLite (better-sqlite3 or bun:sqlite)
|
|
20
|
+
// Dynamic require for SQLite (better-sqlite3, node:sqlite, or bun:sqlite)
|
|
21
21
|
let BetterSqlite3: any;
|
|
22
22
|
|
|
23
23
|
export function loadBetterSqlite3(): any {
|
|
@@ -26,7 +26,7 @@ export function loadBetterSqlite3(): any {
|
|
|
26
26
|
BetterSqlite3 = loadSqlite();
|
|
27
27
|
return BetterSqlite3;
|
|
28
28
|
} catch {
|
|
29
|
-
throw new Error('[memorix] SQLite is not available (
|
|
29
|
+
throw new Error('[memorix] SQLite is not available (better-sqlite3, node:sqlite, and bun:sqlite all failed)');
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -688,7 +688,7 @@ function applySchemaMigrations(db: any): void {
|
|
|
688
688
|
const _dbCache = new Map<string, any>();
|
|
689
689
|
|
|
690
690
|
/**
|
|
691
|
-
* Get or create a shared
|
|
691
|
+
* Get or create a shared SQLite database handle for the given data directory.
|
|
692
692
|
*
|
|
693
693
|
* The handle is cached per normalized dataDir path. All stores (observations,
|
|
694
694
|
* mini-skills, sessions) share the same connection and the same DB file.
|