memorix 1.5.1 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/dist/cli/index.js +558 -102
- package/dist/cli/memcode.js +12 -1
- package/dist/index.js +221 -27
- package/dist/maintenance-runner.js +59 -11
- package/dist/media-audio-runner.js +59 -11
- package/dist/media-video-runner.js +59 -11
- package/dist/memcode-runtime/CHANGELOG.md +47 -0
- package/dist/sdk.js +220 -26
- package/dist/vector-backfill-runner.js +59 -11
- package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +1 -1
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +11 -3
- package/docs/API_REFERENCE.md +2 -2
- package/docs/DEVELOPMENT.md +5 -3
- package/docs/DOCKER.md +5 -1
- package/docs/PERFORMANCE.md +2 -2
- package/docs/README.md +3 -1
- package/docs/SETUP.md +8 -2
- package/package.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/commands/agent-integrations.ts +9 -3
- package/src/cli/commands/codegraph.ts +48 -5
- package/src/cli/commands/context.ts +20 -4
- package/src/cli/commands/doctor.ts +46 -15
- package/src/cli/commands/hook.ts +13 -1
- package/src/cli/commands/resume.ts +4 -0
- package/src/cli/commands/serve-http.ts +68 -7
- package/src/cli/heap.ts +76 -0
- package/src/cli/invocation.ts +7 -4
- package/src/codegraph/auto-context.ts +4 -1
- package/src/codegraph/context-pack.ts +3 -1
- package/src/codegraph/external-provider.ts +93 -0
- package/src/embedding/provider.ts +76 -6
- package/src/hooks/handler.ts +9 -18
- package/src/hooks/installers/index.ts +1 -1
- package/src/hooks/lifecycle.ts +173 -0
- package/src/hooks/official-skills.ts +3 -3
- package/src/hooks/rules/memorix-agent-rules.md +1 -1
- package/src/knowledge/context-receipt.ts +41 -0
- package/src/knowledge/workflows.ts +6 -3
- package/src/knowledge/workset.ts +56 -2
- package/src/server.ts +15 -3
|
@@ -175,12 +175,26 @@ let initPromise: Promise<EmbeddingProvider | null> | null = null;
|
|
|
175
175
|
type EmbeddingMode = 'off' | 'fastembed' | 'transformers' | 'api' | 'auto';
|
|
176
176
|
type ProviderKind = 'api' | 'fastembed' | 'transformers' | 'unknown';
|
|
177
177
|
|
|
178
|
+
export type EmbeddingRuntimeStatus = 'disabled' | 'uninitialized' | 'ready' | 'degraded';
|
|
179
|
+
export type EmbeddingFailureKind = 'authorization' | 'billing' | 'timeout' | 'upstream';
|
|
180
|
+
|
|
181
|
+
export interface EmbeddingRuntimeHealth {
|
|
182
|
+
status: EmbeddingRuntimeStatus;
|
|
183
|
+
provider?: { name: string; dimensions: number };
|
|
184
|
+
failure?: {
|
|
185
|
+
kind: EmbeddingFailureKind;
|
|
186
|
+
occurredAt: string;
|
|
187
|
+
retryAfterMs: number;
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
178
191
|
/**
|
|
179
192
|
* Tracks whether the last init attempt resulted in a temporary failure
|
|
180
193
|
* (mode != 'off' but provider returned null). When true, the next
|
|
181
194
|
* getEmbeddingProvider() call will retry instead of returning cached null.
|
|
182
195
|
*/
|
|
183
196
|
let lastInitWasTemporaryFailure = false;
|
|
197
|
+
let lastEmbeddingFailure: { kind: EmbeddingFailureKind; occurredAt: number } | null = null;
|
|
184
198
|
let lastEmbeddingWarning = '';
|
|
185
199
|
let lastEmbeddingWarningAt = 0;
|
|
186
200
|
const WARNING_COOLDOWN_MS = 30_000;
|
|
@@ -271,13 +285,38 @@ function isTemporaryEmbeddingFailure(error: unknown): boolean {
|
|
|
271
285
|
);
|
|
272
286
|
}
|
|
273
287
|
|
|
288
|
+
function classifyEmbeddingFailure(error: unknown): EmbeddingFailureKind {
|
|
289
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
290
|
+
if (/embedding api error \(401\)|invalid_api_key|incorrect api key|unauthorized/i.test(message)) {
|
|
291
|
+
return 'authorization';
|
|
292
|
+
}
|
|
293
|
+
if (/embedding api error \(402\)|quota exceeded|account balance/i.test(message)) {
|
|
294
|
+
return 'billing';
|
|
295
|
+
}
|
|
296
|
+
if (/embedding api timeout|abort/i.test(message)) {
|
|
297
|
+
return 'timeout';
|
|
298
|
+
}
|
|
299
|
+
return 'upstream';
|
|
300
|
+
}
|
|
301
|
+
|
|
274
302
|
function markTemporaryFailure(reason: unknown): void {
|
|
275
303
|
provider = null;
|
|
276
304
|
initPromise = null;
|
|
277
305
|
lastInitWasTemporaryFailure = true;
|
|
278
306
|
lastFailureTimestamp = Date.now();
|
|
307
|
+
lastEmbeddingFailure = {
|
|
308
|
+
kind: classifyEmbeddingFailure(reason),
|
|
309
|
+
occurredAt: lastFailureTimestamp,
|
|
310
|
+
};
|
|
279
311
|
const message = reason instanceof Error ? reason.message : String(reason);
|
|
280
|
-
warnOnce(`[memorix] Embedding provider
|
|
312
|
+
warnOnce(`[memorix] Embedding provider unavailable at runtime: ${message}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function markProviderReady(candidate: EmbeddingProvider): EmbeddingProvider {
|
|
316
|
+
provider = wrapProvider(candidate);
|
|
317
|
+
lastInitWasTemporaryFailure = false;
|
|
318
|
+
lastEmbeddingFailure = null;
|
|
319
|
+
return provider;
|
|
281
320
|
}
|
|
282
321
|
|
|
283
322
|
async function createFastEmbedProvider(): Promise<EmbeddingProvider | null> {
|
|
@@ -438,7 +477,7 @@ export async function getEmbeddingProvider(
|
|
|
438
477
|
if (mode === 'fastembed') {
|
|
439
478
|
const initialized = await createFastEmbedProvider();
|
|
440
479
|
if (!initialized) return null;
|
|
441
|
-
provider =
|
|
480
|
+
provider = markProviderReady(initialized);
|
|
442
481
|
warnOnce(`[memorix] Embedding provider: ${provider.name} (${provider.dimensions}d)`);
|
|
443
482
|
return provider;
|
|
444
483
|
}
|
|
@@ -447,7 +486,7 @@ export async function getEmbeddingProvider(
|
|
|
447
486
|
if (mode === 'transformers') {
|
|
448
487
|
const initialized = await createTransformersProvider();
|
|
449
488
|
if (!initialized) return null;
|
|
450
|
-
provider =
|
|
489
|
+
provider = markProviderReady(initialized);
|
|
451
490
|
warnOnce(`[memorix] Embedding provider: ${provider.name} (${provider.dimensions}d)`);
|
|
452
491
|
return provider;
|
|
453
492
|
}
|
|
@@ -456,7 +495,7 @@ export async function getEmbeddingProvider(
|
|
|
456
495
|
if (mode === 'api') {
|
|
457
496
|
const initialized = await createAPIProvider(options);
|
|
458
497
|
if (!initialized) return null;
|
|
459
|
-
provider =
|
|
498
|
+
provider = markProviderReady(initialized);
|
|
460
499
|
warnOnce(`[memorix] Embedding provider: ${provider.name} (${provider.dimensions}d)`);
|
|
461
500
|
return provider;
|
|
462
501
|
}
|
|
@@ -465,7 +504,7 @@ export async function getEmbeddingProvider(
|
|
|
465
504
|
if (hasAPIEmbeddingConfig()) {
|
|
466
505
|
const initialized = await createAPIProvider(options);
|
|
467
506
|
if (initialized) {
|
|
468
|
-
provider =
|
|
507
|
+
provider = markProviderReady(initialized);
|
|
469
508
|
warnOnce(`[memorix] Embedding provider: ${provider.name} (${provider.dimensions}d)`);
|
|
470
509
|
return provider;
|
|
471
510
|
}
|
|
@@ -473,7 +512,7 @@ export async function getEmbeddingProvider(
|
|
|
473
512
|
|
|
474
513
|
const localFallback = await createLocalFallbackProvider();
|
|
475
514
|
if (localFallback) {
|
|
476
|
-
provider =
|
|
515
|
+
provider = markProviderReady(localFallback);
|
|
477
516
|
warnOnce(`[memorix] Embedding provider: ${provider.name} (${provider.dimensions}d)`);
|
|
478
517
|
return provider;
|
|
479
518
|
}
|
|
@@ -507,6 +546,36 @@ export async function isVectorSearchAvailable(): Promise<boolean> {
|
|
|
507
546
|
return p !== null;
|
|
508
547
|
}
|
|
509
548
|
|
|
549
|
+
/**
|
|
550
|
+
* Process-local embedding health, intentionally read-only and network-free.
|
|
551
|
+
* It lets the control plane distinguish a configured provider from one that
|
|
552
|
+
* has degraded after a live credential, billing, timeout, or upstream error.
|
|
553
|
+
*/
|
|
554
|
+
export function getEmbeddingRuntimeHealth(): EmbeddingRuntimeHealth {
|
|
555
|
+
const mode = getEmbeddingMode();
|
|
556
|
+
if (mode === 'off') return { status: 'disabled' };
|
|
557
|
+
|
|
558
|
+
if (lastInitWasTemporaryFailure && lastEmbeddingFailure) {
|
|
559
|
+
return {
|
|
560
|
+
status: 'degraded',
|
|
561
|
+
failure: {
|
|
562
|
+
kind: lastEmbeddingFailure.kind,
|
|
563
|
+
occurredAt: new Date(lastEmbeddingFailure.occurredAt).toISOString(),
|
|
564
|
+
retryAfterMs: Math.max(0, RETRY_COOLDOWN_MS - (Date.now() - lastEmbeddingFailure.occurredAt)),
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (provider) {
|
|
570
|
+
return {
|
|
571
|
+
status: 'ready',
|
|
572
|
+
provider: { name: provider.name, dimensions: provider.dimensions },
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return { status: 'uninitialized' };
|
|
577
|
+
}
|
|
578
|
+
|
|
510
579
|
/**
|
|
511
580
|
* Check if embedding is explicitly disabled by configuration (mode === 'off').
|
|
512
581
|
*
|
|
@@ -529,6 +598,7 @@ export function resetProvider(): void {
|
|
|
529
598
|
initPromise = null;
|
|
530
599
|
lastInitWasTemporaryFailure = false;
|
|
531
600
|
lastFailureTimestamp = 0;
|
|
601
|
+
lastEmbeddingFailure = null;
|
|
532
602
|
lastEmbeddingWarning = '';
|
|
533
603
|
lastEmbeddingWarningAt = 0;
|
|
534
604
|
}
|
package/src/hooks/handler.ts
CHANGED
|
@@ -21,6 +21,11 @@ import { normalizeHookInput } from './normalizer.js';
|
|
|
21
21
|
import { detectBestPattern, patternToObservationType } from './pattern-detector.js';
|
|
22
22
|
import { isSignificantKnowledge, isRetrievedResult, isTrivialCommand } from './significance-filter.js';
|
|
23
23
|
import type { AgentName, HookEvent, HookOutput, NormalizedHookInput } from './types.js';
|
|
24
|
+
import {
|
|
25
|
+
hookObservationRuntimeOptions,
|
|
26
|
+
readHookStdin,
|
|
27
|
+
resolveHookStdinTimeoutMs,
|
|
28
|
+
} from './lifecycle.js';
|
|
24
29
|
|
|
25
30
|
type HookInjectionMode = 'full' | 'minimal' | 'silent';
|
|
26
31
|
type HookContextTarget = 'hook-session-start' | 'hook-user-prompt';
|
|
@@ -762,23 +767,9 @@ export function formatHookOutput(
|
|
|
762
767
|
* Called by the CLI: `memorix hook`
|
|
763
768
|
*/
|
|
764
769
|
export async function runHook(agentOverride?: string, eventOverride?: string): Promise<void> {
|
|
765
|
-
//
|
|
766
|
-
//
|
|
767
|
-
const rawInput = await
|
|
768
|
-
const chunks: Buffer[] = [];
|
|
769
|
-
const finish = () => resolve(Buffer.concat(chunks).toString('utf-8').trim());
|
|
770
|
-
|
|
771
|
-
// Hard timeout: resolve with whatever we have after 3 s
|
|
772
|
-
const timer = setTimeout(() => {
|
|
773
|
-
process.stdin.removeAllListeners('data');
|
|
774
|
-
process.stdin.removeAllListeners('end');
|
|
775
|
-
finish();
|
|
776
|
-
}, 3_000);
|
|
777
|
-
|
|
778
|
-
process.stdin.on('data', (chunk: Buffer) => { chunks.push(chunk); });
|
|
779
|
-
process.stdin.on('end', () => { clearTimeout(timer); finish(); });
|
|
780
|
-
process.stdin.on('error', () => { clearTimeout(timer); finish(); });
|
|
781
|
-
});
|
|
770
|
+
// Some hosts (e.g. Gemini CLI) may not close stdin promptly. Bound the
|
|
771
|
+
// read and release the stream so an open pipe cannot keep Node alive.
|
|
772
|
+
const rawInput = await readHookStdin(process.stdin, resolveHookStdinTimeoutMs());
|
|
782
773
|
|
|
783
774
|
if (!rawInput) {
|
|
784
775
|
process.stdout.write(JSON.stringify({ continue: true }));
|
|
@@ -841,7 +832,7 @@ export async function runHook(agentOverride?: string, eventOverride?: string): P
|
|
|
841
832
|
await initObservationStore(dataDir);
|
|
842
833
|
await initMSStore(dataDir);
|
|
843
834
|
await initSessStore(dataDir);
|
|
844
|
-
await initObservations(dataDir);
|
|
835
|
+
await initObservations(dataDir, hookObservationRuntimeOptions(rawProject.rootPath));
|
|
845
836
|
await storeObservation({ ...observation, projectId, sourceDetail: 'hook' });
|
|
846
837
|
// Automatic capture is deliberately quiet. Candidate state and later
|
|
847
838
|
// qualification are visible through Memorix inspection, not injected as
|
|
@@ -1259,7 +1259,7 @@ function getAgentRulesContent(agent?: AgentName, scope: 'project' | 'global' = '
|
|
|
1259
1259
|
'',
|
|
1260
1260
|
`Default first step for non-trivial coding work: call \x60memorix_project_context\x60 with the user's actual task before progress files, dev-log reads, ad-hoc file reads, or git archaeology. Memorix will choose a task-lensed brief (bugfix, feature, release, onboarding, refactor, docs, test, or general). When the task is continuing prior work, the same brief also includes a bounded prior-work projection. Treat its "Start here" files as the first ${contextNoun} files to inspect.`,
|
|
1261
1261
|
'',
|
|
1262
|
-
'If the MCP tool is not visible yet but the client supports tool discovery or dynamic loading, search/select \x60memorix_project_context\x60 first. Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run exactly one CLI brief with the user\'s real task before inspecting files, Git history, progress notes, or guessing: \x60memorix resume "<task>" --json\x60. For a new task, use \x60memorix context "<task>" --json\x60 instead. The absence of \x60.memorix\x60 or visible memory files never proves project memory is empty. If that one command fails, report it and proceed normally. Do not probe help, enumerate commands, chain broad searches, wait indefinitely on MCP startup, or hand-write tool-call syntax.',
|
|
1262
|
+
'If the MCP tool is not visible yet but the client supports tool discovery or dynamic loading, search/select \x60memorix_project_context\x60 first. Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run exactly one CLI brief with the user\'s real task before inspecting files, Git history, progress notes, or guessing: \x60memorix resume "<task>" --brief-json\x60. For a new task, use \x60memorix context "<task>" --brief-json\x60 instead. The absence of \x60.memorix\x60 or visible memory files never proves project memory is empty. Use \x60--json\x60 only when a diagnostic needs the detailed legacy payload. If that one command fails, report it and proceed normally. Do not probe help, enumerate commands, chain broad searches, wait indefinitely on MCP startup, or hand-write tool-call syntax.',
|
|
1263
1263
|
'',
|
|
1264
1264
|
'After a successful \x60memorix_project_context\x60 result, the brief is the default retrieval boundary. Do not call more Memorix retrieval tools after a complete brief. Use \x60memorix_context_pack\x60, \x60memorix_search\x60, or \x60memorix_detail\x60 only when the brief lacks a specific reference, freshness field, or fact needed for the task, or when the user explicitly asks for deeper history. In MCP, name that missing fact in \x60purpose\x60 when intentionally expanding beyond the brief. Do not retrieve the same decision twice just to confirm an already-complete brief.',
|
|
1265
1265
|
'If the user asks for read-only work or says not to modify files, do not call \x60memorix_store\x60 just to record an assessment. Store only when the user explicitly asks to preserve it.',
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-level lifecycle for `memorix hook`.
|
|
3
|
+
*
|
|
4
|
+
* Hosts invoke this command on every agent event. It must read stdin, emit
|
|
5
|
+
* one JSON object, and exit. Leftover hook processes are a bug: they reserve
|
|
6
|
+
* heap, orphan onto PID 1, and can exhaust a laptop.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ObservationRuntimeOptions } from '../memory/observations.js';
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_HOOK_TIMEOUT_MS = 20_000;
|
|
12
|
+
export const MIN_HOOK_TIMEOUT_MS = 1_000;
|
|
13
|
+
export const MAX_HOOK_TIMEOUT_MS = 120_000;
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_HOOK_STDIN_TIMEOUT_MS = 3_000;
|
|
16
|
+
export const MIN_HOOK_STDIN_TIMEOUT_MS = 100;
|
|
17
|
+
export const MAX_HOOK_STDIN_TIMEOUT_MS = 30_000;
|
|
18
|
+
|
|
19
|
+
type EnvMap = NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
20
|
+
|
|
21
|
+
function parseBoundedMs(
|
|
22
|
+
raw: string | undefined,
|
|
23
|
+
fallback: number,
|
|
24
|
+
min: number,
|
|
25
|
+
max: number,
|
|
26
|
+
): number {
|
|
27
|
+
const value = raw?.trim();
|
|
28
|
+
if (!value) return fallback;
|
|
29
|
+
const parsed = Number(value);
|
|
30
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
31
|
+
return Math.max(min, Math.min(max, Math.floor(parsed)));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveHookTimeoutMs(env: EnvMap = process.env): number {
|
|
35
|
+
return parseBoundedMs(
|
|
36
|
+
env.MEMORIX_HOOK_TIMEOUT_MS,
|
|
37
|
+
DEFAULT_HOOK_TIMEOUT_MS,
|
|
38
|
+
MIN_HOOK_TIMEOUT_MS,
|
|
39
|
+
MAX_HOOK_TIMEOUT_MS,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resolveHookStdinTimeoutMs(env: EnvMap = process.env): number {
|
|
44
|
+
return parseBoundedMs(
|
|
45
|
+
env.MEMORIX_HOOK_STDIN_TIMEOUT_MS,
|
|
46
|
+
DEFAULT_HOOK_STDIN_TIMEOUT_MS,
|
|
47
|
+
MIN_HOOK_STDIN_TIMEOUT_MS,
|
|
48
|
+
MAX_HOOK_STDIN_TIMEOUT_MS,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function hookObservationRuntimeOptions(projectRoot: string): ObservationRuntimeOptions {
|
|
53
|
+
return {
|
|
54
|
+
embeddingWriteMode: 'deferred',
|
|
55
|
+
projectRoot,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type HookStdin = NodeJS.ReadableStream & {
|
|
60
|
+
pause?: () => void;
|
|
61
|
+
destroy?: (error?: Error) => void;
|
|
62
|
+
unref?: () => void;
|
|
63
|
+
isTTY?: boolean;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read hook JSON from a pipe. Resolves on EOF, stream error, or idle timeout.
|
|
68
|
+
* Always releases the stream so an open stdin cannot keep Node alive.
|
|
69
|
+
*/
|
|
70
|
+
export async function readHookStdin(stdin: HookStdin, timeoutMs: number): Promise<string> {
|
|
71
|
+
return new Promise<string>((resolve) => {
|
|
72
|
+
const chunks: Buffer[] = [];
|
|
73
|
+
let settled = false;
|
|
74
|
+
|
|
75
|
+
const finish = () => {
|
|
76
|
+
if (settled) return;
|
|
77
|
+
settled = true;
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
stdin.removeListener('data', onData);
|
|
80
|
+
stdin.removeListener('end', finish);
|
|
81
|
+
stdin.removeListener('error', finish);
|
|
82
|
+
stdin.pause?.();
|
|
83
|
+
if (!stdin.isTTY) {
|
|
84
|
+
stdin.destroy?.();
|
|
85
|
+
} else {
|
|
86
|
+
stdin.unref?.();
|
|
87
|
+
}
|
|
88
|
+
resolve(Buffer.concat(chunks).toString('utf-8').trim());
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const onData = (chunk: Buffer | string) => {
|
|
92
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
96
|
+
stdin.on('data', onData);
|
|
97
|
+
stdin.on('end', finish);
|
|
98
|
+
stdin.on('error', finish);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface HookDeadlineIo {
|
|
103
|
+
write(chunk: string): unknown;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface HookDeadlineOptions {
|
|
107
|
+
timeoutMs: number;
|
|
108
|
+
run: () => Promise<void>;
|
|
109
|
+
stdout?: HookDeadlineIo;
|
|
110
|
+
stderr?: HookDeadlineIo;
|
|
111
|
+
exit?: (code: number) => void;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function writeContinue(stdout: HookDeadlineIo): void {
|
|
115
|
+
try {
|
|
116
|
+
stdout.write(JSON.stringify({ continue: true }));
|
|
117
|
+
} catch {
|
|
118
|
+
// The host already closed the pipe. Exiting is still required.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Run a hook body under a hard deadline, then force-exit.
|
|
124
|
+
* `withTimeout` cannot abort in-flight fetches; process.exit is the backstop.
|
|
125
|
+
*/
|
|
126
|
+
export async function runHookWithDeadline(options: HookDeadlineOptions): Promise<void> {
|
|
127
|
+
const stdout = options.stdout ?? process.stdout;
|
|
128
|
+
const stderr = options.stderr ?? process.stderr;
|
|
129
|
+
const exit = options.exit ?? ((code: number) => { process.exit(code); });
|
|
130
|
+
|
|
131
|
+
await new Promise<void>((resolve) => {
|
|
132
|
+
let finished = false;
|
|
133
|
+
|
|
134
|
+
const settle = () => {
|
|
135
|
+
if (finished) return;
|
|
136
|
+
finished = true;
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
resolve();
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const timer = setTimeout(() => {
|
|
142
|
+
if (finished) return;
|
|
143
|
+
try {
|
|
144
|
+
stderr.write(`[memorix] hook timed out after ${options.timeoutMs}ms\n`);
|
|
145
|
+
} catch {
|
|
146
|
+
// stderr may already be closed
|
|
147
|
+
}
|
|
148
|
+
writeContinue(stdout);
|
|
149
|
+
exit(0);
|
|
150
|
+
settle();
|
|
151
|
+
}, options.timeoutMs);
|
|
152
|
+
|
|
153
|
+
options.run().then(
|
|
154
|
+
() => {
|
|
155
|
+
if (finished) return;
|
|
156
|
+
exit(0);
|
|
157
|
+
settle();
|
|
158
|
+
},
|
|
159
|
+
(error: unknown) => {
|
|
160
|
+
if (finished) return;
|
|
161
|
+
try {
|
|
162
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
163
|
+
stderr.write(`[memorix] hook failed: ${message}\n`);
|
|
164
|
+
} catch {
|
|
165
|
+
// stderr may already be closed
|
|
166
|
+
}
|
|
167
|
+
writeContinue(stdout);
|
|
168
|
+
exit(0);
|
|
169
|
+
settle();
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -31,8 +31,8 @@ export const OFFICIAL_MEMORIX_SKILLS: OfficialMemorixSkill[] = [
|
|
|
31
31
|
'',
|
|
32
32
|
'| Situation | Prefer | CLI fallback |',
|
|
33
33
|
'|---|---|---|',
|
|
34
|
-
'| Starting a coding task and needing the best task-lensed project map | `memorix_project_context` with the user\'s task | `memorix context "<task>" --json` |',
|
|
35
|
-
'| Continuing prior project work | `memorix_project_context` with the user\'s task | `memorix resume "<task>" --json` |',
|
|
34
|
+
'| Starting a coding task and needing the best task-lensed project map | `memorix_project_context` with the user\'s task | `memorix context "<task>" --brief-json` |',
|
|
35
|
+
'| Continuing prior project work | `memorix_project_context` with the user\'s task | `memorix resume "<task>" --brief-json` |',
|
|
36
36
|
'| Need structured refs/freshness for code-bound memories | `memorix_context_pack` | `memorix codegraph context-pack --task "<topic>"` |',
|
|
37
37
|
'| Explicit memory graph question | `memorix_graph_context` | `memorix memory graph-context --query "<topic>"` |',
|
|
38
38
|
'| Specific past decision, bug, file, or change | `memorix_search` | `memorix memory search --query "<topic>"` |',
|
|
@@ -45,7 +45,7 @@ export const OFFICIAL_MEMORIX_SKILLS: OfficialMemorixSkill[] = [
|
|
|
45
45
|
'',
|
|
46
46
|
'- Default first step for non-trivial coding work: call `memorix_project_context` before progress files, dev-log reads, ad-hoc file reads, or git archaeology. Pass the user\'s actual task so Memorix can choose a bugfix, feature, release, onboarding, refactor, docs, test, or general brief. Start by reading its suggested files.',
|
|
47
47
|
'- In Claude Code headless/print-mode, an initial MCP `pending` status is not a failure by itself. Prefer MCP first: use the client\'s tool discovery/dynamic loading to find `memorix_project_context` when it is not in the first visible tool list.',
|
|
48
|
-
'- Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run `memorix resume "<task>" --json` exactly once before inspecting files, Git history, progress notes, or guessing. The absence of `.memorix` or visible memory files never proves project memory is empty. For a new task, use `memorix context "<task>" --json` instead. If that one command fails, report it and proceed normally; do not probe help, enumerate commands, or chain broad searches.',
|
|
48
|
+
'- Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run `memorix resume "<task>" --brief-json` exactly once before inspecting files, Git history, progress notes, or guessing. The absence of `.memorix` or visible memory files never proves project memory is empty. For a new task, use `memorix context "<task>" --brief-json` instead. Use `--json` only when a diagnostic needs the detailed legacy payload. If that one command fails, report it and proceed normally; do not probe help, enumerate commands, or chain broad searches.',
|
|
49
49
|
'- After a successful `memorix_project_context`, the brief is the default retrieval boundary. Do not call more Memorix retrieval tools after a complete brief. Use Context Pack, search, or detail only when the brief lacks a specific reference, freshness field, or fact needed for the task, or when the user explicitly asks for deeper history. In MCP, name that missing fact in `purpose` when intentionally expanding beyond the brief. Do not retrieve the same decision twice just to confirm an already-complete brief.',
|
|
50
50
|
'- If the user asks for read-only work or says not to modify files, do not call `memorix_store` just to record an assessment. Store only when the user explicitly asks to preserve it.',
|
|
51
51
|
'- Use `memorix_search` for a specific decision, bug, file, or prior change that the project context did not answer. Fetch detail only for a result you still need after the brief.',
|
|
@@ -7,7 +7,7 @@ You have access to Memorix memory tools. Follow these rules to maintain persiste
|
|
|
7
7
|
At the beginning of non-trivial coding work, use Memorix Memory Autopilot before local progress notes or broad file exploration. A session bind is not required for every conversation.
|
|
8
8
|
|
|
9
9
|
1. Default first step for non-trivial coding work: call `memorix_project_context` with the user's actual task before progress files, dev-log reads, ad-hoc file reads, or git archaeology. Memorix will choose a task-lensed brief (bugfix, feature, release, onboarding, refactor, docs, test, or general). Treat its "Start here" files as the first code or docs to inspect.
|
|
10
|
-
2. If the MCP tool is not visible yet but the client supports tool discovery or dynamic loading, search/select `memorix_project_context` first. Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run exactly one CLI brief with the user's real task before inspecting files, Git history, progress notes, or guessing: use `memorix resume "<task>" --json`. For a new task, use `memorix context "<task>" --json` instead. The absence of `.memorix` or visible memory files never proves project memory is empty. If that one command fails, report it and proceed normally. Do not probe help, enumerate commands, chain broad searches, wait indefinitely on MCP startup, or hand-write tool-call syntax.
|
|
10
|
+
2. If the MCP tool is not visible yet but the client supports tool discovery or dynamic loading, search/select `memorix_project_context` first. Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run exactly one CLI brief with the user's real task before inspecting files, Git history, progress notes, or guessing: use `memorix resume "<task>" --brief-json`. For a new task, use `memorix context "<task>" --brief-json` instead. The absence of `.memorix` or visible memory files never proves project memory is empty. If that one command fails, report it and proceed normally. Do not probe help, enumerate commands, chain broad searches, wait indefinitely on MCP startup, or hand-write tool-call syntax. Use `--json` only when a diagnostic needs the detailed legacy payload.
|
|
11
11
|
3. After a successful `memorix_project_context` result, the brief is the default retrieval boundary. Do not call more Memorix retrieval tools after a complete brief. Use `memorix_context_pack`, `memorix_search`, or `memorix_detail` only when the brief lacks a specific reference, freshness field, or fact needed for the task, or when the user explicitly asks for deeper history. In MCP, name that missing fact in `purpose` when intentionally expanding beyond the brief. Do not retrieve the same decision twice just to confirm an already-complete brief.
|
|
12
12
|
If the user asks for read-only work or says not to modify files, do not call `memorix_store` just to record an assessment. Store only when the user explicitly asks to preserve it.
|
|
13
13
|
4. Use `memorix_context_pack` when you need structured refs and freshness for code-bound memories.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CodeGraphProviderQuality } from '../codegraph/types.js';
|
|
2
|
+
import type { TaskWorkset } from './workset.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A small machine-readable counterpart to the agent-facing Workset prompt.
|
|
6
|
+
* Detailed CLI JSON remains available for diagnostics; this shape is for
|
|
7
|
+
* clients that need a bounded handoff without loading raw stores.
|
|
8
|
+
*/
|
|
9
|
+
export interface BoundedContextReceipt {
|
|
10
|
+
schemaVersion: '1';
|
|
11
|
+
task: string;
|
|
12
|
+
brief: string;
|
|
13
|
+
receipt: TaskWorkset['receipt'];
|
|
14
|
+
code: {
|
|
15
|
+
selected: CodeGraphProviderQuality['selected'];
|
|
16
|
+
quality: CodeGraphProviderQuality['selectedQuality'];
|
|
17
|
+
externalState: CodeGraphProviderQuality['external']['state'];
|
|
18
|
+
snapshotId?: string;
|
|
19
|
+
};
|
|
20
|
+
loadout?: TaskWorkset['agentLoadout'];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildBoundedContextReceipt(input: {
|
|
24
|
+
workset: TaskWorkset;
|
|
25
|
+
providerQuality?: CodeGraphProviderQuality;
|
|
26
|
+
}): BoundedContextReceipt {
|
|
27
|
+
const provider = input.providerQuality ?? input.workset.provenance.codeProvider;
|
|
28
|
+
return {
|
|
29
|
+
schemaVersion: '1',
|
|
30
|
+
task: input.workset.task,
|
|
31
|
+
brief: input.workset.prompt,
|
|
32
|
+
receipt: input.workset.receipt,
|
|
33
|
+
code: {
|
|
34
|
+
selected: provider?.selected ?? 'lite',
|
|
35
|
+
quality: provider?.selectedQuality ?? 'heuristic',
|
|
36
|
+
externalState: provider?.external.state ?? 'not-detected',
|
|
37
|
+
...(input.workset.provenance.snapshotId ? { snapshotId: input.workset.provenance.snapshotId } : {}),
|
|
38
|
+
},
|
|
39
|
+
...(input.workset.agentLoadout ? { loadout: input.workset.agentLoadout } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -22,7 +22,7 @@ import type {
|
|
|
22
22
|
} from './workflow-types.js';
|
|
23
23
|
import type { KnowledgeWorkspace } from './workspace-types.js';
|
|
24
24
|
|
|
25
|
-
const
|
|
25
|
+
export const WORKFLOW_AGENT_TARGETS: AgentTarget[] = [
|
|
26
26
|
'windsurf',
|
|
27
27
|
'cursor',
|
|
28
28
|
'claude-code',
|
|
@@ -117,7 +117,7 @@ function optionalStatus(data: Record<string, unknown>): WorkflowStatus {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
function normalizeAgents(values: string[]): AgentTarget[] {
|
|
120
|
-
const unknown = values.filter(value => !
|
|
120
|
+
const unknown = values.filter(value => !WORKFLOW_AGENT_TARGETS.includes(value as AgentTarget));
|
|
121
121
|
if (unknown.length > 0) {
|
|
122
122
|
throw new Error('workflow frontmatter has unsupported agent: ' + unknown[0]);
|
|
123
123
|
}
|
|
@@ -459,12 +459,15 @@ function taskScore(workflow: WorkflowSpec, task: string): { score: number; reaso
|
|
|
459
459
|
export function selectWorkflows(input: {
|
|
460
460
|
workflows: WorkflowSpec[];
|
|
461
461
|
task: string;
|
|
462
|
+
/** Only select workflows that explicitly support the target agent. */
|
|
463
|
+
agent?: AgentTarget;
|
|
462
464
|
projectId?: string;
|
|
463
465
|
store?: WorkflowStore;
|
|
464
466
|
limit?: number;
|
|
465
467
|
}): WorkflowSelection[] {
|
|
466
468
|
const selected = input.workflows
|
|
467
|
-
.filter(workflow => workflow.status === 'active'
|
|
469
|
+
.filter(workflow => workflow.status === 'active'
|
|
470
|
+
&& (!input.agent || workflow.compatibleAgents.includes(input.agent)))
|
|
468
471
|
.map(workflow => {
|
|
469
472
|
const match = taskScore(workflow, input.task);
|
|
470
473
|
const cautions = input.store && input.projectId
|
package/src/knowledge/workset.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { WorkflowStore } from './workflow-store.js';
|
|
|
8
8
|
import { OutcomeStore } from './outcome-store.js';
|
|
9
9
|
import { qualityFromOutcome, type OutcomeQuality } from './outcome-types.js';
|
|
10
10
|
import { selectWorkflows } from './workflows.js';
|
|
11
|
+
import type { AgentTarget } from '../types.js';
|
|
11
12
|
import { renderLongTermMemorySummary, selectLongTermMemoriesForTask } from '../memory/long-term.js';
|
|
12
13
|
import { governContextCandidates, type GovernancePlan } from './governor.js';
|
|
13
14
|
import type {
|
|
@@ -73,6 +74,18 @@ export interface WorksetWorkflow {
|
|
|
73
74
|
cautions: string[];
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
/**
|
|
78
|
+
* A task-scoped plan assembled from existing workflow and wiki assets. It is
|
|
79
|
+
* descriptive rather than an authorization boundary: host agents retain their
|
|
80
|
+
* own tool and approval policies.
|
|
81
|
+
*/
|
|
82
|
+
export interface WorksetAgentLoadout {
|
|
83
|
+
agent: AgentTarget;
|
|
84
|
+
workflowIds: string[];
|
|
85
|
+
requiredContext: string[];
|
|
86
|
+
allowedTools: string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
export interface WorksetMemorySource {
|
|
77
90
|
id: number;
|
|
78
91
|
title: string;
|
|
@@ -156,6 +169,7 @@ export interface TaskWorkset {
|
|
|
156
169
|
pages: WorksetPage[];
|
|
157
170
|
durableMemory: WorksetLongTermMemory[];
|
|
158
171
|
workflows: WorksetWorkflow[];
|
|
172
|
+
agentLoadout?: WorksetAgentLoadout;
|
|
159
173
|
cautions: WorksetCaution[];
|
|
160
174
|
verification: string[];
|
|
161
175
|
evidenceIds: string[];
|
|
@@ -179,6 +193,8 @@ export interface BuildTaskWorksetInput {
|
|
|
179
193
|
projectId: string;
|
|
180
194
|
dataDir: string;
|
|
181
195
|
task?: string;
|
|
196
|
+
/** Optional target used to select workflows explicitly compatible with this agent. */
|
|
197
|
+
agent?: AgentTarget;
|
|
182
198
|
lens: string;
|
|
183
199
|
currentFacts?: string[];
|
|
184
200
|
continuation?: WorksetContinuation;
|
|
@@ -313,6 +329,18 @@ function workflowOutput(selection: WorkflowSelection): WorksetWorkflow {
|
|
|
313
329
|
};
|
|
314
330
|
}
|
|
315
331
|
|
|
332
|
+
function workflowLoadout(agent: AgentTarget | undefined, selections: WorkflowSelection[]): WorksetAgentLoadout | undefined {
|
|
333
|
+
if (!agent) return undefined;
|
|
334
|
+
return {
|
|
335
|
+
agent,
|
|
336
|
+
workflowIds: selections.map(selection => selection.workflow.id),
|
|
337
|
+
requiredContext: unique(selections.flatMap(selection => selection.workflow.requiredContext))
|
|
338
|
+
.map(item => short(item, 18)).slice(0, 4),
|
|
339
|
+
allowedTools: unique(selections.flatMap(selection => selection.workflow.allowedTools))
|
|
340
|
+
.map(item => short(item, 12)).slice(0, 6),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
316
344
|
function appendLine(
|
|
317
345
|
lines: string[],
|
|
318
346
|
candidate: string,
|
|
@@ -853,6 +881,23 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
|
|
|
853
881
|
}
|
|
854
882
|
}
|
|
855
883
|
|
|
884
|
+
if (input.agentLoadout) {
|
|
885
|
+
appendLine(lines, '', maxTokens, omitted, 'agent-loadout-heading');
|
|
886
|
+
appendLine(lines, 'Agent loadout (' + input.agentLoadout.agent + ')', maxTokens, omitted, 'agent-loadout-heading');
|
|
887
|
+
for (const required of input.agentLoadout.requiredContext.slice(0, 2)) {
|
|
888
|
+
appendLine(lines, '- Need: ' + required, maxTokens, omitted, 'agent-loadout-context');
|
|
889
|
+
}
|
|
890
|
+
if (input.agentLoadout.allowedTools.length > 0) {
|
|
891
|
+
appendLine(
|
|
892
|
+
lines,
|
|
893
|
+
'- Workflow tools: ' + input.agentLoadout.allowedTools.join(', '),
|
|
894
|
+
maxTokens,
|
|
895
|
+
omitted,
|
|
896
|
+
'agent-loadout-tools',
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
856
901
|
if (input.verification.length > 0) {
|
|
857
902
|
appendLine(lines, '', maxTokens, omitted, 'verification-heading');
|
|
858
903
|
appendLine(lines, 'Verify', maxTokens, omitted, 'verification-heading');
|
|
@@ -965,6 +1010,7 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
|
|
|
965
1010
|
let workspace: KnowledgeWorkspace | undefined;
|
|
966
1011
|
let pages: WorksetPage[] = [];
|
|
967
1012
|
let workflows: WorksetWorkflow[] = [];
|
|
1013
|
+
let workflowSelections: WorkflowSelection[] = [];
|
|
968
1014
|
try {
|
|
969
1015
|
workspace = await preferredWorkspace(input.projectId, input.dataDir);
|
|
970
1016
|
if (workspace) {
|
|
@@ -985,14 +1031,15 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
|
|
|
985
1031
|
if (task) {
|
|
986
1032
|
const workflowStore = new WorkflowStore();
|
|
987
1033
|
await workflowStore.init(input.dataDir);
|
|
988
|
-
|
|
1034
|
+
workflowSelections = selectWorkflows({
|
|
989
1035
|
workflows: workflowStore.listWorkflows(workspace.id, 'active'),
|
|
990
1036
|
task,
|
|
991
1037
|
projectId: input.projectId,
|
|
992
1038
|
store: workflowStore,
|
|
1039
|
+
...(input.agent ? { agent: input.agent } : {}),
|
|
993
1040
|
limit: 2,
|
|
994
1041
|
});
|
|
995
|
-
workflows =
|
|
1042
|
+
workflows = workflowSelections.map(workflowOutput);
|
|
996
1043
|
for (const workflow of workflows) {
|
|
997
1044
|
for (const caution of workflow.cautions) {
|
|
998
1045
|
cautions.push({ kind: 'workflow-failed-verification', message: caution });
|
|
@@ -1018,6 +1065,10 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
|
|
|
1018
1065
|
workflows,
|
|
1019
1066
|
});
|
|
1020
1067
|
const governedDurableIds = new Set(governed.durableMemory.map(memory => memory.id));
|
|
1068
|
+
const agentLoadout = workflowLoadout(
|
|
1069
|
+
input.agent,
|
|
1070
|
+
workflowSelections.filter(selection => governed.workflows.some(workflow => workflow.id === selection.workflow.id)),
|
|
1071
|
+
);
|
|
1021
1072
|
const evidenceIds = unique(governed.claims.flatMap(claim => evidenceIdsForClaim(
|
|
1022
1073
|
claim,
|
|
1023
1074
|
evidenceByClaim.get(claim.id) ?? [],
|
|
@@ -1084,6 +1135,9 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
|
|
|
1084
1135
|
pages: pages.filter(page => page.claimIds.some(claimId => governed.claims.some(claim => claim.id === claimId))),
|
|
1085
1136
|
durableMemory: governed.durableMemory,
|
|
1086
1137
|
workflows: governed.workflows,
|
|
1138
|
+
...(agentLoadout
|
|
1139
|
+
? { agentLoadout }
|
|
1140
|
+
: {}),
|
|
1087
1141
|
cautions: normalizedCautions,
|
|
1088
1142
|
verification,
|
|
1089
1143
|
evidenceIds,
|