memorix 1.2.3 → 1.2.5
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 +33 -0
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/dist/cli/index.js +5273 -4730
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +506 -99
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.js +176 -53
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +33 -0
- package/dist/sdk.d.ts +4 -2
- package/dist/sdk.js +507 -99
- package/dist/sdk.js.map +1 -1
- package/dist/types.d.ts +8 -1
- package/dist/types.js.map +1 -1
- package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +86 -0
- package/docs/1.2.5-RETRIEVAL-PERFORMANCE.md +85 -0
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +13 -1
- package/docs/API_REFERENCE.md +13 -3
- package/docs/PERFORMANCE.md +22 -1
- package/docs/dev-log/progress.txt +73 -7
- package/package.json +2 -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/memory.ts +12 -3
- package/src/cli/commands/operator-shared.ts +74 -2
- package/src/cli/commands/resume.ts +31 -0
- package/src/cli/index.ts +5 -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/resolved-config.ts +29 -4
- 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/runtime/project-maintenance.ts +1 -14
- package/src/sdk.ts +4 -0
- package/src/server.ts +144 -25
- package/src/store/bun-sqlite-compat.ts +118 -15
- package/src/store/orama-store.ts +39 -18
- package/src/store/sqlite-db.ts +3 -3
- package/src/timeout.ts +23 -0
- package/src/types.ts +8 -0
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;
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
enqueueObservationQualification,
|
|
10
10
|
type MaintenanceQueue,
|
|
11
11
|
} from './lifecycle.js';
|
|
12
|
+
import { withTimeout } from '../timeout.js';
|
|
12
13
|
|
|
13
14
|
const DEFAULT_VECTOR_BATCH_SIZE = 12;
|
|
14
15
|
const DEFAULT_RETENTION_BATCH_SIZE = 100;
|
|
@@ -110,20 +111,6 @@ async function loadWorkspaceForMaintenance(
|
|
|
110
111
|
return versioned ?? local;
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
/**
|
|
114
|
-
* Creates handlers for one initialized project runtime. The worker itself is
|
|
115
|
-
* project-scoped so it never claims a different project's queued work.
|
|
116
|
-
*/
|
|
117
|
-
function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
|
118
|
-
return new Promise<T>((resolve, reject) => {
|
|
119
|
-
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
120
|
-
promise.then(
|
|
121
|
-
(value) => { clearTimeout(timer); resolve(value); },
|
|
122
|
-
(error) => { clearTimeout(timer); reject(error); },
|
|
123
|
-
);
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
114
|
async function runAutomaticConsolidation(
|
|
128
115
|
projectId: string,
|
|
129
116
|
projectDir: string,
|
package/src/sdk.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
ProjectInfo,
|
|
27
27
|
DetectionResult,
|
|
28
28
|
ObservationReader,
|
|
29
|
+
RetrievalQuality,
|
|
29
30
|
} from './types.js';
|
|
30
31
|
import {
|
|
31
32
|
canManageObservation,
|
|
@@ -90,6 +91,8 @@ export interface ClientSearchOptions {
|
|
|
90
91
|
status?: ObservationStatus | 'all';
|
|
91
92
|
/** Maximum results. Default: 20 */
|
|
92
93
|
limit?: number;
|
|
94
|
+
/** Retrieval profile. Default: balanced. */
|
|
95
|
+
quality?: RetrievalQuality;
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
/** Result from resolving observations */
|
|
@@ -220,6 +223,7 @@ export class MemoryClient {
|
|
|
220
223
|
type: options.type,
|
|
221
224
|
source: options.source,
|
|
222
225
|
status: options.status === 'all' ? undefined : (options.status ?? 'active'),
|
|
226
|
+
quality: options.quality,
|
|
223
227
|
reader: this._reader,
|
|
224
228
|
};
|
|
225
229
|
|
package/src/server.ts
CHANGED
|
@@ -50,23 +50,13 @@ import type { ExistingMemory } from './llm/memory-manager.js';
|
|
|
50
50
|
import { runFormation, getMetricsSummary, getBeforeAfterMetrics } from './memory/formation/index.js';
|
|
51
51
|
import type { FormationConfig, SearchHit, FormedMemory, FormationStage, FormationStageEvent } from './memory/formation/types.js';
|
|
52
52
|
import { parseFormationTimeoutMs } from './server/formation-timeout.js';
|
|
53
|
+
import { withTimeout } from './timeout.js';
|
|
53
54
|
|
|
54
55
|
// ── Timeout budgets for LLM-heavy paths ──────────────────────────
|
|
55
56
|
const FORMATION_TIMEOUT_MS = parseFormationTimeoutMs(process.env.MEMORIX_FORMATION_TIMEOUT_MS); // Formation pipeline (extract+resolve+evaluate)
|
|
56
57
|
const COMPACT_ON_WRITE_TIMEOUT_MS = 12_000; // Legacy compact-on-write fallback path
|
|
57
58
|
const COMPRESSION_TIMEOUT_MS = 5_000; // Narrative compression
|
|
58
59
|
|
|
59
|
-
/** Race a promise against a timeout. Rejects with a descriptive Error on timeout. */
|
|
60
|
-
function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
|
61
|
-
let timer: ReturnType<typeof setTimeout>;
|
|
62
|
-
return Promise.race([
|
|
63
|
-
promise,
|
|
64
|
-
new Promise<never>((_, reject) => {
|
|
65
|
-
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
66
|
-
}),
|
|
67
|
-
]).finally(() => clearTimeout(timer!));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
60
|
function formatFormationStageDurations(stageDurationsMs: Partial<Record<FormationStage, number>>): string {
|
|
71
61
|
const orderedStages: FormationStage[] = ['extract', 'resolve', 'evaluate'];
|
|
72
62
|
const parts = orderedStages
|
|
@@ -228,6 +218,9 @@ export const BOOTSTRAP_SAFE_TOOL_NAMES = new Set([
|
|
|
228
218
|
'memorix_context_pack',
|
|
229
219
|
]);
|
|
230
220
|
|
|
221
|
+
const AUTOPILOT_RETRIEVAL_BOUNDARY_TTL_MS = 2 * 60 * 1000;
|
|
222
|
+
const READ_ONLY_TASK_PATTERN = /\b(?:do not|don't|never)\s+(?:modify|edit|change|write)\b|\bread[- ]only\b|(?:不要|不准|勿|禁止).{0,6}(?:修改|编辑|写入)|只读/i;
|
|
223
|
+
|
|
231
224
|
export function shouldAwaitProjectRuntime(toolName: string): boolean {
|
|
232
225
|
return !BOOTSTRAP_SAFE_TOOL_NAMES.has(toolName);
|
|
233
226
|
}
|
|
@@ -602,6 +595,63 @@ export async function createMemorixServer(
|
|
|
602
595
|
};
|
|
603
596
|
};
|
|
604
597
|
|
|
598
|
+
// A complete Autopilot brief is the normal retrieval boundary for one coding
|
|
599
|
+
// turn. Keep accidental search/detail loops cheap, while preserving an
|
|
600
|
+
// explicit escape hatch when a caller really needs deeper history.
|
|
601
|
+
let autopilotRetrievalBoundary: {
|
|
602
|
+
projectId: string;
|
|
603
|
+
issuedAt: number;
|
|
604
|
+
coveredObservationIds: Set<number>;
|
|
605
|
+
readOnly: boolean;
|
|
606
|
+
} | null = null;
|
|
607
|
+
const getActiveAutopilotRetrievalBoundary = () => {
|
|
608
|
+
const boundary = autopilotRetrievalBoundary;
|
|
609
|
+
if (!boundary) return null;
|
|
610
|
+
if (boundary.projectId === project.id && Date.now() - boundary.issuedAt <= AUTOPILOT_RETRIEVAL_BOUNDARY_TTL_MS) {
|
|
611
|
+
return boundary;
|
|
612
|
+
}
|
|
613
|
+
autopilotRetrievalBoundary = null;
|
|
614
|
+
return null;
|
|
615
|
+
};
|
|
616
|
+
const requireExplicitAutopilotExpansion = (toolName: string, purpose?: string) => {
|
|
617
|
+
if (!getActiveAutopilotRetrievalBoundary() || purpose?.trim()) return null;
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
content: [{
|
|
621
|
+
type: 'text' as const,
|
|
622
|
+
text:
|
|
623
|
+
'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. ' +
|
|
624
|
+
'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.',
|
|
625
|
+
}],
|
|
626
|
+
};
|
|
627
|
+
};
|
|
628
|
+
const blockCoveredAutopilotEvidence = (toolName: string, observationIds: number[], force?: boolean) => {
|
|
629
|
+
const boundary = getActiveAutopilotRetrievalBoundary();
|
|
630
|
+
if (!boundary || force || observationIds.length === 0 || !observationIds.every(id => boundary.coveredObservationIds.has(id))) {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
content: [{
|
|
635
|
+
type: 'text' as const,
|
|
636
|
+
text:
|
|
637
|
+
'Memorix Autopilot retrieval boundary: every requested memory is already represented in the latest project brief, so no duplicate detail was retrieved. ' +
|
|
638
|
+
'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.',
|
|
639
|
+
}],
|
|
640
|
+
};
|
|
641
|
+
};
|
|
642
|
+
const blockReadOnlyAutopilotWrite = (overrideReadOnly?: boolean) => {
|
|
643
|
+
const boundary = getActiveAutopilotRetrievalBoundary();
|
|
644
|
+
if (!boundary?.readOnly || overrideReadOnly) return null;
|
|
645
|
+
return {
|
|
646
|
+
content: [{
|
|
647
|
+
type: 'text' as const,
|
|
648
|
+
text:
|
|
649
|
+
'Memorix write boundary: the latest task is read-only or asks not to modify files, so no memory was stored. ' +
|
|
650
|
+
'Persist a record only when the user explicitly asks to save it, then retry with `overrideReadOnly: true`.',
|
|
651
|
+
}],
|
|
652
|
+
};
|
|
653
|
+
};
|
|
654
|
+
|
|
605
655
|
// ================================================================
|
|
606
656
|
// Memorix Extended Tools (3-layer Progressive Disclosure)
|
|
607
657
|
// ================================================================
|
|
@@ -622,7 +672,8 @@ export async function createMemorixServer(
|
|
|
622
672
|
'problem-solution ([FIX] bug fix), how-it-works ([INFO] explanation), what-changed ([CHANGE] change), ' +
|
|
623
673
|
'discovery ([DISCOVERY] insight), why-it-exists ([WHY] rationale), trade-off ([TRADEOFF] compromise), ' +
|
|
624
674
|
'session-request ([SESSION] original goal). ' +
|
|
625
|
-
'Project visibility is the default. Personal or team visibility requires an explicitly joined coordination identity.'
|
|
675
|
+
'Project visibility is the default. Personal or team visibility requires an explicitly joined coordination identity. ' +
|
|
676
|
+
'For a read-only task, do not store unless the user explicitly asks to save a record.',
|
|
626
677
|
inputSchema: {
|
|
627
678
|
entityName: z.string().describe('The entity this observation belongs to (e.g., "auth-module", "port-config")'),
|
|
628
679
|
type: z.enum(OBSERVATION_TYPES).describe('Observation type for classification'),
|
|
@@ -646,11 +697,16 @@ export async function createMemorixServer(
|
|
|
646
697
|
visibility: z.enum(['personal', 'project', 'team']).optional().describe(
|
|
647
698
|
'Retrieval scope. Project is the normal shared default; personal/team require memorix_session_start with joinTeam=true.',
|
|
648
699
|
),
|
|
700
|
+
overrideReadOnly: z.boolean().optional().describe(
|
|
701
|
+
'Use only when the user explicitly asks to save memory during a read-only or no-modification task.',
|
|
702
|
+
),
|
|
649
703
|
},
|
|
650
704
|
},
|
|
651
|
-
async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility }) => {
|
|
705
|
+
async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility, overrideReadOnly }) => {
|
|
652
706
|
const unresolved = requireResolvedProject('store memory in the current project');
|
|
653
707
|
if (unresolved) return unresolved;
|
|
708
|
+
const readOnlyBoundary = blockReadOnlyAutopilotWrite(overrideReadOnly);
|
|
709
|
+
if (readOnlyBoundary) return readOnlyBoundary;
|
|
654
710
|
const requestedVisibility = (visibility ?? 'project') as 'personal' | 'project' | 'team';
|
|
655
711
|
const reader = getObservationReader();
|
|
656
712
|
if (requestedVisibility !== 'project' && !currentAgentId) {
|
|
@@ -1224,6 +1280,7 @@ export async function createMemorixServer(
|
|
|
1224
1280
|
title: 'Search Memory',
|
|
1225
1281
|
description:
|
|
1226
1282
|
'Search project memory. Returns a compact index (~50-100 tokens/result). ' +
|
|
1283
|
+
'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
1284
|
'Use memorix_detail to fetch full content for specific IDs. ' +
|
|
1228
1285
|
'Use memorix_timeline to see chronological context. ' +
|
|
1229
1286
|
'Searches across all observations stored from any IDE session — enabling cross-session and cross-agent context retrieval.',
|
|
@@ -1243,13 +1300,26 @@ export async function createMemorixServer(
|
|
|
1243
1300
|
source: z.enum(['agent', 'git', 'manual']).optional().describe(
|
|
1244
1301
|
'Filter by memory source. "git" returns only commit-derived ground truth memories. Omit for all sources.',
|
|
1245
1302
|
),
|
|
1303
|
+
quality: z.enum(['fast', 'balanced', 'thorough']).optional().default('balanced').describe(
|
|
1304
|
+
'Retrieval profile: fast stays local, balanced uses configured embeddings, thorough explicitly permits optional LLM refinement.',
|
|
1305
|
+
),
|
|
1306
|
+
purpose: z.string().optional().describe(
|
|
1307
|
+
'Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user\'s explicit request.',
|
|
1308
|
+
),
|
|
1309
|
+
force: z.boolean().optional().describe(
|
|
1310
|
+
'Use only when the user explicitly asks to read a record already represented in the latest Autopilot brief.',
|
|
1311
|
+
),
|
|
1246
1312
|
},
|
|
1247
1313
|
},
|
|
1248
|
-
async ({ query, limit, type, maxTokens, scope, since, until, status, source }) => {
|
|
1314
|
+
async ({ query, limit, type, maxTokens, scope, since, until, status, source, quality, purpose, force }) => {
|
|
1249
1315
|
if (scope !== 'global') {
|
|
1250
1316
|
const unresolved = requireResolvedProject('search the current project');
|
|
1251
1317
|
if (unresolved) return unresolved;
|
|
1252
1318
|
}
|
|
1319
|
+
if (scope !== 'global') {
|
|
1320
|
+
const boundary = requireExplicitAutopilotExpansion('memorix_search', purpose);
|
|
1321
|
+
if (boundary) return boundary;
|
|
1322
|
+
}
|
|
1253
1323
|
return withFreshIndex(async () => {
|
|
1254
1324
|
|
|
1255
1325
|
const safeLimit = limit != null ? coerceNumber(limit, 20) : undefined;
|
|
@@ -1269,18 +1339,15 @@ export async function createMemorixServer(
|
|
|
1269
1339
|
projectId: scope === 'global' ? undefined : project.id,
|
|
1270
1340
|
status: (status as 'active' | 'resolved' | 'archived' | 'all') ?? 'active',
|
|
1271
1341
|
source: source as 'agent' | 'git' | 'manual' | undefined,
|
|
1342
|
+
quality: quality as 'fast' | 'balanced' | 'thorough',
|
|
1272
1343
|
reader: getObservationReader(scope === 'global' ? 'global' : 'project'),
|
|
1273
1344
|
});
|
|
1274
1345
|
|
|
1275
|
-
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
1276
|
-
setTimeout(() => reject(new Error(`Search timeout after ${TIMEOUT_MS}ms`)), TIMEOUT_MS)
|
|
1277
|
-
);
|
|
1278
|
-
|
|
1279
1346
|
let result;
|
|
1280
1347
|
try {
|
|
1281
|
-
result = await
|
|
1348
|
+
result = await withTimeout(searchPromise, TIMEOUT_MS, 'Search');
|
|
1282
1349
|
} catch (error) {
|
|
1283
|
-
if (error instanceof Error && error.message
|
|
1350
|
+
if (error instanceof Error && /\btimeout\b|timed out/i.test(error.message)) {
|
|
1284
1351
|
// Timeout: return empty result with error message
|
|
1285
1352
|
return {
|
|
1286
1353
|
content: [
|
|
@@ -1295,6 +1362,18 @@ export async function createMemorixServer(
|
|
|
1295
1362
|
throw error;
|
|
1296
1363
|
}
|
|
1297
1364
|
|
|
1365
|
+
const activeBoundary = getActiveAutopilotRetrievalBoundary();
|
|
1366
|
+
if (
|
|
1367
|
+
scope !== 'global'
|
|
1368
|
+
&& activeBoundary
|
|
1369
|
+
&& !force
|
|
1370
|
+
&& result.entries.length > 0
|
|
1371
|
+
&& result.entries.every(entry => activeBoundary.coveredObservationIds.has(entry.id))
|
|
1372
|
+
) {
|
|
1373
|
+
const duplicate = blockCoveredAutopilotEvidence('memorix_search', result.entries.map(entry => entry.id), force);
|
|
1374
|
+
if (duplicate) return duplicate;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1298
1377
|
// Append retrieval diagnostics only; do not mix workspace-sync guidance into memory results.
|
|
1299
1378
|
let text = result.formatted;
|
|
1300
1379
|
try {
|
|
@@ -1414,6 +1493,7 @@ export async function createMemorixServer(
|
|
|
1414
1493
|
observations,
|
|
1415
1494
|
task,
|
|
1416
1495
|
refresh: refresh ?? 'auto',
|
|
1496
|
+
reader: getObservationReader(),
|
|
1417
1497
|
enqueueRefresh: () => {
|
|
1418
1498
|
enqueueCodegraphRefresh({
|
|
1419
1499
|
dataDir: projectDir,
|
|
@@ -1430,6 +1510,17 @@ export async function createMemorixServer(
|
|
|
1430
1510
|
? formatAutoProjectContextSummary(context)
|
|
1431
1511
|
: formatAutoProjectContextPrompt(context);
|
|
1432
1512
|
|
|
1513
|
+
autopilotRetrievalBoundary = {
|
|
1514
|
+
projectId: project.id,
|
|
1515
|
+
issuedAt: Date.now(),
|
|
1516
|
+
coveredObservationIds: new Set([
|
|
1517
|
+
...(context.workset.continuation?.memories.map(memory => memory.id) ?? []),
|
|
1518
|
+
...context.workset.reliableMemory.map(memory => memory.id),
|
|
1519
|
+
...context.workset.cautionMemory.map(memory => memory.id),
|
|
1520
|
+
]),
|
|
1521
|
+
readOnly: READ_ONLY_TASK_PATTERN.test(task ?? ''),
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1433
1524
|
return {
|
|
1434
1525
|
content: [{ type: 'text' as const, text }],
|
|
1435
1526
|
};
|
|
@@ -1475,18 +1566,24 @@ export async function createMemorixServer(
|
|
|
1475
1566
|
title: 'Context Pack',
|
|
1476
1567
|
description:
|
|
1477
1568
|
'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.'
|
|
1569
|
+
'Combines relevant memories, CodeGraph Memory facts, freshness warnings, suggested reads, and verification hints. ' +
|
|
1570
|
+
'After a complete memorix_project_context brief, provide purpose only when deliberately expanding beyond it.',
|
|
1479
1571
|
inputSchema: {
|
|
1480
1572
|
task: z.string().describe('Current coding task or question'),
|
|
1481
1573
|
limit: z.preprocess(
|
|
1482
1574
|
value => (typeof value === 'string' && value.trim() !== '' ? Number(value) : value),
|
|
1483
1575
|
z.number().int().positive().max(100),
|
|
1484
1576
|
).optional().describe('Max active memories to inspect before code-ref filtering (default: 20)'),
|
|
1577
|
+
purpose: z.string().optional().describe(
|
|
1578
|
+
'Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user\'s explicit request.',
|
|
1579
|
+
),
|
|
1485
1580
|
},
|
|
1486
1581
|
},
|
|
1487
|
-
async ({ task, limit }) => {
|
|
1582
|
+
async ({ task, limit, purpose }) => {
|
|
1488
1583
|
const unresolved = requireResolvedProject('build a context pack for the current project');
|
|
1489
1584
|
if (unresolved) return unresolved;
|
|
1585
|
+
const boundary = requireExplicitAutopilotExpansion('memorix_context_pack', purpose);
|
|
1586
|
+
if (boundary) return boundary;
|
|
1490
1587
|
|
|
1491
1588
|
const [
|
|
1492
1589
|
{ CodeGraphStore },
|
|
@@ -2380,6 +2477,7 @@ export async function createMemorixServer(
|
|
|
2380
2477
|
description:
|
|
2381
2478
|
'Fetch full observation or mini-skill details — includes source kind (explicit memory / hook trace / git evidence), ' +
|
|
2382
2479
|
'value category, and cross-references (~500-1000 tokens each). ' +
|
|
2480
|
+
'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
2481
|
'Always use memorix_search first to find relevant IDs, then fetch only what you need. ' +
|
|
2384
2482
|
'Accepts typed refs from search results (e.g. "obs:42", "skill:3") via the typedRefs field, ' +
|
|
2385
2483
|
'or legacy numeric ids / object refs for backward compatibility.',
|
|
@@ -2392,19 +2490,39 @@ export async function createMemorixServer(
|
|
|
2392
2490
|
}),
|
|
2393
2491
|
).optional().describe('Explicit observation refs. Prefer this for global search results.'),
|
|
2394
2492
|
typedRefs: z.array(z.string()).optional().describe('Typed memory refs from search results, e.g. "obs:42", "skill:3", "obs:42@org/proj"'),
|
|
2493
|
+
purpose: z.string().optional().describe(
|
|
2494
|
+
'Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user\'s explicit request.',
|
|
2495
|
+
),
|
|
2496
|
+
force: z.boolean().optional().describe(
|
|
2497
|
+
'Use only when the user explicitly asks to read a record already represented in the latest Autopilot brief.',
|
|
2498
|
+
),
|
|
2395
2499
|
},
|
|
2396
2500
|
},
|
|
2397
|
-
async ({ ids, refs, typedRefs }) => {
|
|
2501
|
+
async ({ ids, refs, typedRefs, purpose, force }) => {
|
|
2398
2502
|
// Defensive coercion: Claude Code CLI + GLM may send "[16]" instead of [16]
|
|
2399
2503
|
const safeIds = coerceNumberArray(ids);
|
|
2400
2504
|
const safeRefs = coerceObservationRefs(refs);
|
|
2401
2505
|
const safeTypedRefs = coerceStringArray(typedRefs);
|
|
2506
|
+
const hasCrossProjectRef = safeRefs.some((ref) => ref.projectId && ref.projectId !== project.id)
|
|
2507
|
+
|| safeTypedRefs.some((ref) => ref.includes('@') && !ref.endsWith(`@${project.id}`));
|
|
2508
|
+
if (!hasCrossProjectRef) {
|
|
2509
|
+
const boundary = requireExplicitAutopilotExpansion('memorix_detail', purpose);
|
|
2510
|
+
if (boundary) return boundary;
|
|
2511
|
+
const requestedObservationIds = [
|
|
2512
|
+
...safeIds,
|
|
2513
|
+
...safeRefs.map(ref => ref.id),
|
|
2514
|
+
...safeTypedRefs.flatMap((ref) => {
|
|
2515
|
+
const match = /^obs:(\d+)(?:@.+)?$/i.exec(ref.trim());
|
|
2516
|
+
return match ? [Number(match[1])] : [];
|
|
2517
|
+
}),
|
|
2518
|
+
];
|
|
2519
|
+
const duplicate = blockCoveredAutopilotEvidence('memorix_detail', requestedObservationIds, force);
|
|
2520
|
+
if (duplicate) return duplicate;
|
|
2521
|
+
}
|
|
2402
2522
|
|
|
2403
2523
|
// Priority: typedRefs > refs > ids (each is a complete, homogeneous input path)
|
|
2404
2524
|
let result;
|
|
2405
2525
|
try {
|
|
2406
|
-
const hasCrossProjectRef = safeRefs.some((ref) => ref.projectId && ref.projectId !== project.id)
|
|
2407
|
-
|| safeTypedRefs.some((ref) => ref.includes('@') && !ref.endsWith(`@${project.id}`));
|
|
2408
2526
|
const reader = getObservationReader(hasCrossProjectRef ? 'global' : 'project');
|
|
2409
2527
|
if (safeTypedRefs.length > 0) {
|
|
2410
2528
|
// Pass typed ref strings directly — compactDetail handles parsing
|
|
@@ -4825,6 +4943,7 @@ export async function createMemorixServer(
|
|
|
4825
4943
|
|
|
4826
4944
|
// Phase 4a: clear agent identity — old project's agent is not valid in new project
|
|
4827
4945
|
currentAgentId = undefined;
|
|
4946
|
+
autopilotRetrievalBoundary = null;
|
|
4828
4947
|
|
|
4829
4948
|
// Re-resolve data dir with canonical ID (may differ from raw detected ID)
|
|
4830
4949
|
const canonicalProjectDir = newCanonicalId !== newDetected.id
|