memorix 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/dist/cli/index.js +452 -109
- package/dist/index.js +376 -87
- package/dist/maintenance-runner.js +174 -41
- package/dist/media-audio-runner.js +174 -41
- package/dist/media-video-runner.js +174 -41
- package/dist/memcode-runtime/CHANGELOG.md +37 -0
- package/dist/sdk.js +375 -86
- package/dist/types.d.ts +2 -0
- package/dist/vector-backfill-runner.js +174 -41
- package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +1 -1
- package/docs/API_REFERENCE.md +1 -1
- package/docs/DEVELOPMENT.md +5 -3
- 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/codegraph.ts +48 -5
- package/src/cli/commands/context.ts +20 -4
- package/src/cli/commands/resume.ts +4 -0
- 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/api-provider.ts +105 -22
- package/src/embedding/provider.ts +2 -0
- package/src/hooks/installers/index.ts +1 -1
- 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/llm/provider.ts +17 -8
- package/src/memory/formation/extract.ts +6 -3
- package/src/memory/formation/index.ts +2 -1
- package/src/memory/formation/resolve.ts +10 -6
- package/src/memory/formation/types.ts +3 -1
- package/src/memory/long-term.ts +7 -1
- package/src/server.ts +29 -13
- package/src/store/orama-store.ts +6 -4
- package/src/timeout.ts +67 -0
- package/src/types.ts +2 -0
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
} from '../../codegraph/auto-context.js';
|
|
8
8
|
import { getAllObservations } from '../../memory/observations.js';
|
|
9
9
|
import { filterReadableObservations } from '../../memory/visibility.js';
|
|
10
|
+
import { buildBoundedContextReceipt } from '../../knowledge/context-receipt.js';
|
|
11
|
+
import { WORKFLOW_AGENT_TARGETS } from '../../knowledge/workflows.js';
|
|
12
|
+
import type { AgentTarget } from '../../types.js';
|
|
10
13
|
import { emitError, emitResult, getCliProjectContext } from './operator-shared.js';
|
|
11
14
|
|
|
12
15
|
function coerceRefreshMode(input?: string): AutoContextRefreshMode {
|
|
@@ -15,6 +18,13 @@ function coerceRefreshMode(input?: string): AutoContextRefreshMode {
|
|
|
15
18
|
throw new Error('refresh must be one of: auto, always, never');
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
function coerceAgentTarget(input?: string): AgentTarget | undefined {
|
|
22
|
+
const value = input?.trim().toLowerCase();
|
|
23
|
+
if (!value) return undefined;
|
|
24
|
+
if (WORKFLOW_AGENT_TARGETS.includes(value as AgentTarget)) return value as AgentTarget;
|
|
25
|
+
throw new Error('agent must be one of: ' + WORKFLOW_AGENT_TARGETS.join(', '));
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
export default defineCommand({
|
|
19
29
|
meta: {
|
|
20
30
|
name: 'context',
|
|
@@ -29,10 +39,12 @@ export default defineCommand({
|
|
|
29
39
|
},
|
|
30
40
|
resume: { type: 'boolean', description: 'Always include the bounded prior-work projection' },
|
|
31
41
|
refresh: { type: 'string', description: 'Project scan policy: auto, always, or never' },
|
|
42
|
+
agent: { type: 'string', description: 'Optional target agent for compatible workflow selection' },
|
|
32
43
|
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
44
|
+
briefJson: { type: 'boolean', description: 'Emit only the bounded agent brief and receipt JSON' },
|
|
33
45
|
},
|
|
34
46
|
run: async ({ args }) => {
|
|
35
|
-
const asJson = !!args.json;
|
|
47
|
+
const asJson = !!args.json || !!args.briefJson;
|
|
36
48
|
const task = (args.task as string | undefined)?.trim()
|
|
37
49
|
|| (args.input as string | undefined)?.trim()
|
|
38
50
|
|| undefined;
|
|
@@ -44,13 +56,13 @@ export default defineCommand({
|
|
|
44
56
|
dataDir,
|
|
45
57
|
observations: filterReadableObservations(getAllObservations(), reader),
|
|
46
58
|
task,
|
|
59
|
+
agent: coerceAgentTarget(args.agent as string | undefined),
|
|
47
60
|
refresh: coerceRefreshMode(args.refresh as string | undefined),
|
|
48
61
|
reader,
|
|
49
62
|
...(args.resume ? { continuation: 'always' as const } : {}),
|
|
50
63
|
});
|
|
51
64
|
|
|
52
|
-
|
|
53
|
-
{
|
|
65
|
+
const detailed = {
|
|
54
66
|
project,
|
|
55
67
|
lens: context.lens,
|
|
56
68
|
brief: buildAutoProjectBrief(context),
|
|
@@ -61,7 +73,11 @@ export default defineCommand({
|
|
|
61
73
|
workset: context.workset,
|
|
62
74
|
...(context.task ? { task: context.task } : {}),
|
|
63
75
|
...(context.continuation ? { continuation: context.continuation } : {}),
|
|
64
|
-
|
|
76
|
+
};
|
|
77
|
+
emitResult(
|
|
78
|
+
args.briefJson
|
|
79
|
+
? buildBoundedContextReceipt({ workset: context.workset, providerQuality: context.providerQuality })
|
|
80
|
+
: detailed,
|
|
65
81
|
formatAutoProjectContextPrompt(context),
|
|
66
82
|
asJson,
|
|
67
83
|
);
|
|
@@ -13,7 +13,9 @@ export default defineCommand({
|
|
|
13
13
|
args: {
|
|
14
14
|
task: { type: 'positional', description: 'Current continuation task', required: false },
|
|
15
15
|
refresh: { type: 'string', description: 'Project scan policy: auto, always, or never' },
|
|
16
|
+
agent: { type: 'string', description: 'Optional target agent for compatible workflow selection' },
|
|
16
17
|
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
18
|
+
briefJson: { type: 'boolean', description: 'Emit only the bounded agent brief and receipt JSON' },
|
|
17
19
|
},
|
|
18
20
|
async run({ args }) {
|
|
19
21
|
await contextCommand.run?.({
|
|
@@ -21,7 +23,9 @@ export default defineCommand({
|
|
|
21
23
|
_: [],
|
|
22
24
|
input: args.task,
|
|
23
25
|
refresh: args.refresh,
|
|
26
|
+
agent: args.agent,
|
|
24
27
|
json: args.json,
|
|
28
|
+
briefJson: args.briefJson,
|
|
25
29
|
resume: true,
|
|
26
30
|
},
|
|
27
31
|
rawArgs: [],
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from '../knowledge/workset.js';
|
|
11
11
|
import type { ContextDeliveryTarget } from '../knowledge/context-assembly.js';
|
|
12
12
|
import { sanitizeCredentials } from '../memory/secret-filter.js';
|
|
13
|
-
import type { ObservationReader, ProjectInfo } from '../types.js';
|
|
13
|
+
import type { AgentTarget, ObservationReader, ProjectInfo } from '../types.js';
|
|
14
14
|
import { backfillMissingObservationCodeRefs, type CodeRefBackfillResult } from './binder.js';
|
|
15
15
|
import { collectCurrentProjectFacts, formatGitFact, type CurrentProjectFacts } from './current-facts.js';
|
|
16
16
|
import { refreshProjectLite } from './lite-provider.js';
|
|
@@ -189,6 +189,8 @@ export async function buildAutoProjectContext(input: {
|
|
|
189
189
|
dataDir: string;
|
|
190
190
|
observations: ProjectContextObservation[];
|
|
191
191
|
task?: string;
|
|
192
|
+
/** Explicit host target for task-compatible workflow selection. */
|
|
193
|
+
agent?: AgentTarget;
|
|
192
194
|
refresh?: AutoContextRefreshMode;
|
|
193
195
|
maxAgeMs?: number;
|
|
194
196
|
now?: Date;
|
|
@@ -426,6 +428,7 @@ export async function buildAutoProjectContext(input: {
|
|
|
426
428
|
projectId: input.project.id,
|
|
427
429
|
dataDir: input.dataDir,
|
|
428
430
|
...(task ? { task } : {}),
|
|
431
|
+
...(input.agent ? { agent: input.agent } : {}),
|
|
429
432
|
lens: lens.id,
|
|
430
433
|
startHere,
|
|
431
434
|
...(alwaysOn.profile.length > 0 || alwaysOn.state || alwaysOn.durable.length > 0
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
} from './types.js';
|
|
11
11
|
import type { CodeGraphStore } from './store.js';
|
|
12
12
|
import { isCodeGraphExcludedPath } from './exclude.js';
|
|
13
|
-
import type { ObservationReader } from '../types.js';
|
|
13
|
+
import type { AgentTarget, ObservationReader } from '../types.js';
|
|
14
14
|
|
|
15
15
|
export interface ContextPackMemory {
|
|
16
16
|
id: number;
|
|
@@ -278,6 +278,7 @@ export async function attachTaskWorkset(input: {
|
|
|
278
278
|
providerQuality?: CodeGraphProviderQuality;
|
|
279
279
|
runtimeCautions?: WorksetCaution[];
|
|
280
280
|
reader?: ObservationReader;
|
|
281
|
+
agent?: AgentTarget;
|
|
281
282
|
}): Promise<ContextPack> {
|
|
282
283
|
const semanticStartHere = input.semanticCode
|
|
283
284
|
? [...input.semanticCode.relatedFiles, ...input.semanticCode.entryPoints.map(entry => entry.path)]
|
|
@@ -286,6 +287,7 @@ export async function attachTaskWorkset(input: {
|
|
|
286
287
|
projectId: input.projectId,
|
|
287
288
|
dataDir: input.dataDir,
|
|
288
289
|
task: input.pack.task,
|
|
290
|
+
...(input.agent ? { agent: input.agent } : {}),
|
|
289
291
|
lens: input.lens,
|
|
290
292
|
currentFacts: input.currentFacts ?? [],
|
|
291
293
|
...(input.codeState ? { codeState: input.codeState } : {}),
|
|
@@ -17,6 +17,8 @@ import type {
|
|
|
17
17
|
// Keep the default useful in real sessions while still bounding a local call.
|
|
18
18
|
export const DEFAULT_EXTERNAL_CODEGRAPH_TIMEOUT_MS = 1_200;
|
|
19
19
|
export const MAX_EXTERNAL_CODEGRAPH_TIMEOUT_MS = 5_000;
|
|
20
|
+
/** Explicit index maintenance is opt-in and may legitimately take longer. */
|
|
21
|
+
export const EXTERNAL_CODEGRAPH_LIFECYCLE_TIMEOUT_MS = 30_000;
|
|
20
22
|
export const MAX_EXTERNAL_CODEGRAPH_OUTPUT_BYTES = 256 * 1024;
|
|
21
23
|
const MAX_EXTERNAL_NODE_COUNT = 16;
|
|
22
24
|
const MAX_EXTERNAL_EDGE_COUNT = 24;
|
|
@@ -85,6 +87,19 @@ export interface ExternalCodeGraphContextResult extends InspectExternalCodeGraph
|
|
|
85
87
|
caution?: string;
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
/** Explicit lifecycle operations. Memorix never calls CodeGraph's `install`. */
|
|
91
|
+
export type ExternalCodeGraphLifecycleAction = 'init' | 'sync';
|
|
92
|
+
|
|
93
|
+
export interface ExternalCodeGraphLifecycleInput extends InspectExternalCodeGraphInput {
|
|
94
|
+
action: ExternalCodeGraphLifecycleAction;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ExternalCodeGraphLifecycleResult extends InspectExternalCodeGraphResult {
|
|
98
|
+
action: ExternalCodeGraphLifecycleAction;
|
|
99
|
+
performed: boolean;
|
|
100
|
+
message: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
88
103
|
interface RawExternalStatus {
|
|
89
104
|
initialized: boolean;
|
|
90
105
|
projectPath: string;
|
|
@@ -437,6 +452,84 @@ export async function inspectExternalCodeGraph(input: InspectExternalCodeGraphIn
|
|
|
437
452
|
return { health, quality: providerQuality({ mode, health }) };
|
|
438
453
|
}
|
|
439
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Run only a user-requested local lifecycle operation and re-inspect the
|
|
457
|
+
* result. This deliberately does not alter agent MCP files or invoke the
|
|
458
|
+
* upstream `codegraph install` command.
|
|
459
|
+
*/
|
|
460
|
+
export async function runExternalCodeGraphLifecycle(
|
|
461
|
+
input: ExternalCodeGraphLifecycleInput,
|
|
462
|
+
): Promise<ExternalCodeGraphLifecycleResult> {
|
|
463
|
+
const mode = input.mode ?? 'auto';
|
|
464
|
+
if (mode === 'off') {
|
|
465
|
+
const health: ExternalCodeGraphHealth = { state: 'disabled', reason: 'External CodeGraph is disabled by configuration.' };
|
|
466
|
+
return {
|
|
467
|
+
action: input.action,
|
|
468
|
+
performed: false,
|
|
469
|
+
message: 'External CodeGraph is disabled by configuration.',
|
|
470
|
+
health,
|
|
471
|
+
quality: providerQuality({ mode, health }),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const indexPath = join(input.projectRoot, '.codegraph');
|
|
476
|
+
if (input.action === 'sync' && !existsSync(indexPath)) {
|
|
477
|
+
const health: ExternalCodeGraphHealth = { state: 'not-detected', reason: 'No local .codegraph index is present for this project.' };
|
|
478
|
+
return {
|
|
479
|
+
action: input.action,
|
|
480
|
+
performed: false,
|
|
481
|
+
message: 'Initialize CodeGraph first with "memorix codegraph init".',
|
|
482
|
+
health,
|
|
483
|
+
quality: providerQuality({ mode, health }),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (input.action === 'init' && existsSync(indexPath)) {
|
|
488
|
+
const inspected = await inspectExternalCodeGraph(input);
|
|
489
|
+
return {
|
|
490
|
+
action: input.action,
|
|
491
|
+
performed: false,
|
|
492
|
+
message: 'A local CodeGraph directory already exists; no initialization was performed.',
|
|
493
|
+
...inspected,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const result = await runSafely(input.runner ?? defaultRunner, {
|
|
498
|
+
command: input.command,
|
|
499
|
+
args: input.action === 'init'
|
|
500
|
+
? ['init', input.projectRoot]
|
|
501
|
+
: ['sync', input.projectRoot, '--quiet'],
|
|
502
|
+
cwd: input.projectRoot,
|
|
503
|
+
// A context read must stay fast; an explicit user-requested index sync is
|
|
504
|
+
// a different operation and gets its own bounded maintenance window.
|
|
505
|
+
timeoutMs: EXTERNAL_CODEGRAPH_LIFECYCLE_TIMEOUT_MS,
|
|
506
|
+
maxOutputBytes: MAX_EXTERNAL_CODEGRAPH_OUTPUT_BYTES,
|
|
507
|
+
});
|
|
508
|
+
if (!result.ok) {
|
|
509
|
+
const health = unavailableHealth(result);
|
|
510
|
+
return {
|
|
511
|
+
action: input.action,
|
|
512
|
+
performed: false,
|
|
513
|
+
message: 'CodeGraph ' + input.action + ' did not complete: ' + (health.reason ?? 'unknown local error'),
|
|
514
|
+
health,
|
|
515
|
+
quality: providerQuality({ mode, health }),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const inspected = await inspectExternalCodeGraph(input);
|
|
520
|
+
const remainsStale = inspected.health.state === 'stale';
|
|
521
|
+
return {
|
|
522
|
+
action: input.action,
|
|
523
|
+
performed: true,
|
|
524
|
+
message: input.action === 'init'
|
|
525
|
+
? 'Local CodeGraph initialization completed. Run "memorix codegraph sync" when source changes.'
|
|
526
|
+
: remainsStale
|
|
527
|
+
? 'Local CodeGraph synchronization completed, but the graph remains stale against the current worktree; this task will use Lite evidence.'
|
|
528
|
+
: 'Local CodeGraph synchronization completed and is ready for task-scoped semantic context.',
|
|
529
|
+
...inspected,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
440
533
|
function safeRelativePath(projectRoot: string, value: unknown, exclude?: string[]): string | undefined {
|
|
441
534
|
const path = safeText(value, 1_024);
|
|
442
535
|
if (!path || isAbsolute(path)) return undefined;
|
|
@@ -406,6 +406,16 @@ interface APIEmbeddingConfig {
|
|
|
406
406
|
requestedDimensions: number | null;
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
+
class EmbeddingAPIError extends Error {
|
|
410
|
+
readonly status: number;
|
|
411
|
+
|
|
412
|
+
constructor(status: number, detail: string) {
|
|
413
|
+
super(`Embedding API error (${status}): ${detail}`);
|
|
414
|
+
this.name = 'EmbeddingAPIError';
|
|
415
|
+
this.status = status;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
409
419
|
export interface APIEmbeddingProviderCreateOptions {
|
|
410
420
|
/**
|
|
411
421
|
* When false, only previously persisted dimension metadata may initialize
|
|
@@ -432,6 +442,14 @@ function getPreferredBatchSize(config: APIEmbeddingConfig): number {
|
|
|
432
442
|
function parseBatchLimit(error: unknown): number | null {
|
|
433
443
|
if (!(error instanceof Error)) return null;
|
|
434
444
|
|
|
445
|
+
// Only a provider-side request-shape error may trigger recursive batch
|
|
446
|
+
// splitting. Billing, authentication, quota, and upstream errors must fail
|
|
447
|
+
// once; retrying them multiplies latency without changing the request.
|
|
448
|
+
const status = error instanceof EmbeddingAPIError
|
|
449
|
+
? error.status
|
|
450
|
+
: Number(error.message.match(/Embedding API error \((\d{3})\)/i)?.[1] ?? 0);
|
|
451
|
+
if (status > 0 && status !== 400) return null;
|
|
452
|
+
|
|
435
453
|
const explicit = error.message.match(/should not be larger than\s+(\d+)/i);
|
|
436
454
|
if (explicit) return parseInt(explicit[1], 10);
|
|
437
455
|
|
|
@@ -712,16 +730,14 @@ export class APIEmbeddingProvider implements EmbeddingProvider {
|
|
|
712
730
|
}
|
|
713
731
|
} catch (error) {
|
|
714
732
|
const providerLimit = parseBatchLimit(error);
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
if (chunkTexts.length > 1 && fallbackSize < chunkTexts.length) {
|
|
733
|
+
if (providerLimit !== null && chunkTexts.length > 1 && providerLimit < chunkTexts.length) {
|
|
718
734
|
console.error(
|
|
719
|
-
`[memorix] Embedding batch too large for provider, retrying in chunks of ${
|
|
735
|
+
`[memorix] Embedding batch too large for provider, retrying in chunks of ${providerLimit}`,
|
|
720
736
|
);
|
|
721
|
-
for (let start = 0; start < chunkTexts.length; start +=
|
|
737
|
+
for (let start = 0; start < chunkTexts.length; start += providerLimit) {
|
|
722
738
|
await processChunk(
|
|
723
|
-
chunkTexts.slice(start, start +
|
|
724
|
-
chunkIndices.slice(start, start +
|
|
739
|
+
chunkTexts.slice(start, start + providerLimit),
|
|
740
|
+
chunkIndices.slice(start, start + providerLimit),
|
|
725
741
|
);
|
|
726
742
|
}
|
|
727
743
|
return;
|
|
@@ -842,6 +858,9 @@ async function fetchWithRetry(
|
|
|
842
858
|
? Math.max(100, Math.min(Math.floor(options.timeoutMs), 60_000))
|
|
843
859
|
: 10_000;
|
|
844
860
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
861
|
+
const signal = options.signal
|
|
862
|
+
? AbortSignal.any([options.signal, controller.signal])
|
|
863
|
+
: controller.signal;
|
|
845
864
|
let response: Response;
|
|
846
865
|
try {
|
|
847
866
|
response = await fetch(url, {
|
|
@@ -850,30 +869,94 @@ async function fetchWithRetry(
|
|
|
850
869
|
? { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey }
|
|
851
870
|
: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
|
|
852
871
|
body: JSON.stringify(body),
|
|
853
|
-
signal
|
|
872
|
+
signal,
|
|
854
873
|
});
|
|
855
874
|
} catch (err: unknown) {
|
|
856
875
|
clearTimeout(timeout);
|
|
857
|
-
if (err instanceof Error && err.name === 'AbortError') {
|
|
876
|
+
if (err instanceof Error && (err.name === 'AbortError' || controller.signal.aborted || options.signal?.aborted)) {
|
|
858
877
|
throw new Error(`Embedding API timeout after ${timeoutMs}ms: ${url}`);
|
|
859
878
|
}
|
|
860
879
|
throw err;
|
|
861
880
|
}
|
|
862
|
-
clearTimeout(timeout);
|
|
863
881
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
882
|
+
try {
|
|
883
|
+
if (response.ok) {
|
|
884
|
+
return await raceWithAbort(
|
|
885
|
+
response.json() as Promise<EmbeddingAPIResponse>,
|
|
886
|
+
signal,
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (options.retry !== false && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
|
|
891
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
892
|
+
const retryAfter = response.headers.get('retry-after');
|
|
893
|
+
const parsedRetryAfter = retryAfter ? parseInt(retryAfter, 10) * 1000 : NaN;
|
|
894
|
+
const waitMs = Number.isFinite(parsedRetryAfter) ? parsedRetryAfter : delay;
|
|
895
|
+
console.error(`[memorix] Embedding API ${response.status}, retry ${attempt + 1}/${MAX_RETRIES} in ${waitMs}ms`);
|
|
896
|
+
await waitForRetry(waitMs, options.signal);
|
|
897
|
+
return fetchWithRetry(url, apiKey, body, options, attempt + 1, nativeGemini);
|
|
898
|
+
}
|
|
867
899
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
900
|
+
const errorText = await raceWithAbort(
|
|
901
|
+
response.text().catch(() => 'unknown error'),
|
|
902
|
+
signal,
|
|
903
|
+
);
|
|
904
|
+
throw new EmbeddingAPIError(response.status, errorText);
|
|
905
|
+
} catch (err: unknown) {
|
|
906
|
+
if (err instanceof Error && (err.name === 'AbortError' || controller.signal.aborted || options.signal?.aborted)) {
|
|
907
|
+
throw new Error(`Embedding API timeout after ${timeoutMs}ms: ${url}`);
|
|
908
|
+
}
|
|
909
|
+
throw err;
|
|
910
|
+
} finally {
|
|
911
|
+
clearTimeout(timeout);
|
|
875
912
|
}
|
|
913
|
+
}
|
|
876
914
|
|
|
877
|
-
|
|
878
|
-
|
|
915
|
+
function abortError(): Error {
|
|
916
|
+
const error = new Error('Embedding API request aborted');
|
|
917
|
+
error.name = 'AbortError';
|
|
918
|
+
return error;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function raceWithAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
922
|
+
if (signal.aborted) return Promise.reject(abortError());
|
|
923
|
+
return new Promise<T>((resolve, reject) => {
|
|
924
|
+
const onAbort = () => {
|
|
925
|
+
signal.removeEventListener('abort', onAbort);
|
|
926
|
+
reject(abortError());
|
|
927
|
+
};
|
|
928
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
929
|
+
promise.then(
|
|
930
|
+
(value) => {
|
|
931
|
+
signal.removeEventListener('abort', onAbort);
|
|
932
|
+
resolve(value);
|
|
933
|
+
},
|
|
934
|
+
(error) => {
|
|
935
|
+
signal.removeEventListener('abort', onAbort);
|
|
936
|
+
reject(error);
|
|
937
|
+
},
|
|
938
|
+
);
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async function waitForRetry(waitMs: number, signal?: AbortSignal): Promise<void> {
|
|
943
|
+
if (!signal) {
|
|
944
|
+
await new Promise<void>((resolve) => setTimeout(resolve, waitMs));
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const abortSignal = signal;
|
|
948
|
+
if (abortSignal.aborted) throw new Error('Embedding API retry aborted');
|
|
949
|
+
await new Promise<void>((resolve, reject) => {
|
|
950
|
+
const timer = setTimeout(done, waitMs);
|
|
951
|
+
const onAbort = () => {
|
|
952
|
+
clearTimeout(timer);
|
|
953
|
+
abortSignal.removeEventListener('abort', onAbort);
|
|
954
|
+
reject(new Error('Embedding API retry aborted'));
|
|
955
|
+
};
|
|
956
|
+
function done() {
|
|
957
|
+
abortSignal.removeEventListener('abort', onAbort);
|
|
958
|
+
resolve();
|
|
959
|
+
}
|
|
960
|
+
abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
961
|
+
});
|
|
879
962
|
}
|
|
@@ -34,6 +34,8 @@ export interface EmbeddingRequestOptions {
|
|
|
34
34
|
timeoutMs?: number;
|
|
35
35
|
/** Disable provider retries for latency-sensitive best-effort work. */
|
|
36
36
|
retry?: boolean;
|
|
37
|
+
/** Abort the request and any retry wait when the caller's budget expires. */
|
|
38
|
+
signal?: AbortSignal;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export type EmbeddingModality = 'text' | 'image' | 'audio' | 'video' | 'document';
|
|
@@ -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.',
|
|
@@ -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
|