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
|
@@ -22,11 +22,16 @@ export default defineCommand({
|
|
|
22
22
|
},
|
|
23
23
|
args: {
|
|
24
24
|
task: { type: 'string', description: 'Current task for context shaping' },
|
|
25
|
+
input: { type: 'positional', description: 'Current task for context shaping (ergonomic positional form)' },
|
|
26
|
+
resume: { type: 'boolean', description: 'Always include the bounded prior-work projection' },
|
|
25
27
|
refresh: { type: 'string', description: 'Project scan policy: auto, always, or never' },
|
|
26
28
|
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
27
29
|
},
|
|
28
30
|
run: async ({ args }) => {
|
|
29
31
|
const asJson = !!args.json;
|
|
32
|
+
const task = (args.task as string | undefined)?.trim()
|
|
33
|
+
|| (args.input as string | undefined)?.trim()
|
|
34
|
+
|| undefined;
|
|
30
35
|
|
|
31
36
|
try {
|
|
32
37
|
const { project, dataDir, reader } = await getCliProjectContext();
|
|
@@ -34,8 +39,10 @@ export default defineCommand({
|
|
|
34
39
|
project,
|
|
35
40
|
dataDir,
|
|
36
41
|
observations: filterReadableObservations(getAllObservations(), reader),
|
|
37
|
-
task
|
|
42
|
+
task,
|
|
38
43
|
refresh: coerceRefreshMode(args.refresh as string | undefined),
|
|
44
|
+
reader,
|
|
45
|
+
...(args.resume ? { continuation: 'always' as const } : {}),
|
|
39
46
|
});
|
|
40
47
|
|
|
41
48
|
emitResult(
|
|
@@ -49,6 +56,7 @@ export default defineCommand({
|
|
|
49
56
|
providerQuality: context.providerQuality,
|
|
50
57
|
workset: context.workset,
|
|
51
58
|
...(context.task ? { task: context.task } : {}),
|
|
59
|
+
...(context.continuation ? { continuation: context.continuation } : {}),
|
|
52
60
|
},
|
|
53
61
|
formatAutoProjectContextPrompt(context),
|
|
54
62
|
asJson,
|
|
@@ -7,8 +7,10 @@ import { canManageObservation, filterReadableObservations, resolveObservationVis
|
|
|
7
7
|
import {
|
|
8
8
|
coerceObservationStatus,
|
|
9
9
|
coerceObservationType,
|
|
10
|
+
coerceRetrievalQuality,
|
|
10
11
|
emitError,
|
|
11
12
|
emitResult,
|
|
13
|
+
getCliReadContext,
|
|
12
14
|
getCliProjectContext,
|
|
13
15
|
parseCsvList,
|
|
14
16
|
parsePositiveInt,
|
|
@@ -36,6 +38,7 @@ export default defineCommand({
|
|
|
36
38
|
topicKey: { type: 'string', description: 'Stable topic key override' },
|
|
37
39
|
action: { type: 'string', description: 'Secondary action for advanced memory commands' },
|
|
38
40
|
limit: { type: 'string', description: 'Limit for search/recent output' },
|
|
41
|
+
quality: { type: 'string', description: 'Retrieval profile: fast, balanced (default), or thorough' },
|
|
39
42
|
graphLimit: { type: 'string', description: 'Limit for graph-context output' },
|
|
40
43
|
graphQuery: { type: 'string', description: 'Query for graph-context packet' },
|
|
41
44
|
format: { type: 'string', description: 'Output format for graph-context: summary or prompt' },
|
|
@@ -56,7 +59,12 @@ export default defineCommand({
|
|
|
56
59
|
const asJson = !!args.json;
|
|
57
60
|
|
|
58
61
|
try {
|
|
59
|
-
const
|
|
62
|
+
const readOnlyActions = new Set(['', 'search', 'graph-context', 'recent', 'suggest-topic-key', 'detail', 'timeline']);
|
|
63
|
+
const needsSearchIndex = action === 'search' || action === 'detail' || action === 'timeline';
|
|
64
|
+
const context = readOnlyActions.has(action)
|
|
65
|
+
? await getCliReadContext({ searchIndex: needsSearchIndex })
|
|
66
|
+
: await getCliProjectContext({ searchIndex: true });
|
|
67
|
+
const { project, dataDir, reader, identity } = context;
|
|
60
68
|
|
|
61
69
|
switch (action) {
|
|
62
70
|
case 'search': {
|
|
@@ -66,7 +74,8 @@ export default defineCommand({
|
|
|
66
74
|
return;
|
|
67
75
|
}
|
|
68
76
|
const limit = parsePositiveInt(args.limit as string | undefined, 10);
|
|
69
|
-
const
|
|
77
|
+
const quality = coerceRetrievalQuality(args.quality as string | undefined);
|
|
78
|
+
const result = await compactSearch({ query, limit, quality, projectId: project.id, reader });
|
|
70
79
|
emitResult({ project, entries: result.entries }, result.formatted, asJson);
|
|
71
80
|
return;
|
|
72
81
|
}
|
|
@@ -420,7 +429,7 @@ export default defineCommand({
|
|
|
420
429
|
console.log('Memorix Memory Commands');
|
|
421
430
|
console.log('');
|
|
422
431
|
console.log('Usage:');
|
|
423
|
-
console.log(' memorix memory search --query "timeout bug" [--limit 10]');
|
|
432
|
+
console.log(' memorix memory search --query "timeout bug" [--limit 10] [--quality fast|balanced|thorough]');
|
|
424
433
|
console.log(' memorix memory recent [--limit 10]');
|
|
425
434
|
console.log(' memorix memory store --text "..." [--title "..."] [--type discovery] [--visibility project|personal|team]');
|
|
426
435
|
console.log(' memorix memory suggest-topic-key --type decision --title "..."');
|
|
@@ -9,9 +9,10 @@ import type {
|
|
|
9
9
|
ObservationStatus,
|
|
10
10
|
ObservationType,
|
|
11
11
|
ObservationVisibility,
|
|
12
|
+
RetrievalQuality,
|
|
12
13
|
} from '../../types.js';
|
|
13
14
|
import { getCliInvocation } from '../invocation.js';
|
|
14
|
-
import { resolveCliIdentity, type CliIdentity } from '../identity.js';
|
|
15
|
+
import { loadCliIdentity, resolveCliIdentity, type CliIdentity } from '../identity.js';
|
|
15
16
|
|
|
16
17
|
export interface CliProjectContext {
|
|
17
18
|
project: ProjectInfo;
|
|
@@ -22,7 +23,24 @@ export interface CliProjectContext {
|
|
|
22
23
|
identityWarning?: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export
|
|
26
|
+
export interface CliReadContext {
|
|
27
|
+
project: ProjectInfo;
|
|
28
|
+
dataDir: string;
|
|
29
|
+
reader: ObservationReader;
|
|
30
|
+
identity: CliIdentity | null;
|
|
31
|
+
identityWarning?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface CliContextOptions {
|
|
35
|
+
searchIndex?: boolean;
|
|
36
|
+
projectRoot?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function resolveCliProjectContext(options?: CliContextOptions): Promise<{
|
|
40
|
+
project: ProjectInfo;
|
|
41
|
+
dataDir: string;
|
|
42
|
+
invocation: ReturnType<typeof getCliInvocation>;
|
|
43
|
+
}> {
|
|
26
44
|
const invocation = getCliInvocation();
|
|
27
45
|
const detection = detectProjectWithDiagnostics(
|
|
28
46
|
options?.projectRoot
|
|
@@ -37,6 +55,51 @@ export async function getCliProjectContext(options?: { searchIndex?: boolean; pr
|
|
|
37
55
|
|
|
38
56
|
const project = detection.project;
|
|
39
57
|
const dataDir = await getProjectDataDir(project.id);
|
|
58
|
+
return { project, dataDir, invocation };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Read-only memory commands do not need session bookkeeping or maintenance
|
|
63
|
+
* target registration. Identity resolution remains intact so visibility rules
|
|
64
|
+
* stay identical to the full operator context.
|
|
65
|
+
*/
|
|
66
|
+
export async function getCliReadContext(options?: CliContextOptions): Promise<CliReadContext> {
|
|
67
|
+
const { project, dataDir, invocation } = await resolveCliProjectContext(options);
|
|
68
|
+
await initObservations(dataDir);
|
|
69
|
+
|
|
70
|
+
if (options?.searchIndex) {
|
|
71
|
+
await prepareSearchIndex();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const storedIdentity = await loadCliIdentity(dataDir, project.id);
|
|
75
|
+
if (!invocation.actorId && !storedIdentity) {
|
|
76
|
+
return {
|
|
77
|
+
project,
|
|
78
|
+
dataDir,
|
|
79
|
+
reader: { projectId: project.id },
|
|
80
|
+
identity: null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const teamStore = await initTeamStore(dataDir);
|
|
85
|
+
const identity = await resolveCliIdentity({
|
|
86
|
+
project,
|
|
87
|
+
dataDir,
|
|
88
|
+
teamStore,
|
|
89
|
+
explicitActorId: invocation.actorId,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
project,
|
|
94
|
+
dataDir,
|
|
95
|
+
reader: identity.reader,
|
|
96
|
+
identity: identity.identity,
|
|
97
|
+
...(identity.warning ? { identityWarning: identity.warning } : {}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function getCliProjectContext(options?: CliContextOptions): Promise<CliProjectContext> {
|
|
102
|
+
const { project, dataDir, invocation } = await resolveCliProjectContext(options);
|
|
40
103
|
try {
|
|
41
104
|
const { MaintenanceTargetStore } = await import('../../runtime/maintenance-targets.js');
|
|
42
105
|
new MaintenanceTargetStore(dataDir).register({
|
|
@@ -129,6 +192,7 @@ const OBSERVATION_TYPES: ObservationType[] = [
|
|
|
129
192
|
];
|
|
130
193
|
|
|
131
194
|
const OBSERVATION_STATUSES: ObservationStatus[] = ['active', 'resolved', 'archived'];
|
|
195
|
+
const RETRIEVAL_QUALITIES: RetrievalQuality[] = ['fast', 'balanced', 'thorough'];
|
|
132
196
|
|
|
133
197
|
export function coerceObservationType(input?: string): ObservationType {
|
|
134
198
|
const normalized = (input ?? 'discovery') as ObservationType;
|
|
@@ -150,6 +214,14 @@ export function coerceObservationStatus(input?: string): ObservationStatus {
|
|
|
150
214
|
return normalized;
|
|
151
215
|
}
|
|
152
216
|
|
|
217
|
+
export function coerceRetrievalQuality(input?: string): RetrievalQuality {
|
|
218
|
+
const normalized = (input ?? 'balanced').trim().toLowerCase() as RetrievalQuality;
|
|
219
|
+
if (!RETRIEVAL_QUALITIES.includes(normalized)) {
|
|
220
|
+
throw new Error('quality must be fast, balanced, or thorough');
|
|
221
|
+
}
|
|
222
|
+
return normalized;
|
|
223
|
+
}
|
|
224
|
+
|
|
153
225
|
export function coerceObservationVisibility(input?: string): ObservationVisibility {
|
|
154
226
|
const normalized = (input ?? 'project').trim().toLowerCase();
|
|
155
227
|
if (normalized === 'personal' || normalized === 'project' || normalized === 'team') {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import contextCommand from './context.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A human- and agent-friendly continuation entry point. It deliberately
|
|
6
|
+
* delegates to Project Context so CLI and MCP keep one Workset contract.
|
|
7
|
+
*/
|
|
8
|
+
export default defineCommand({
|
|
9
|
+
meta: {
|
|
10
|
+
name: 'resume',
|
|
11
|
+
description: 'Resume prior project work with one bounded Memory Autopilot brief',
|
|
12
|
+
},
|
|
13
|
+
args: {
|
|
14
|
+
task: { type: 'positional', description: 'Current continuation task', required: false },
|
|
15
|
+
refresh: { type: 'string', description: 'Project scan policy: auto, always, or never' },
|
|
16
|
+
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
17
|
+
},
|
|
18
|
+
async run({ args }) {
|
|
19
|
+
await contextCommand.run?.({
|
|
20
|
+
args: {
|
|
21
|
+
_: [],
|
|
22
|
+
input: args.task,
|
|
23
|
+
refresh: args.refresh,
|
|
24
|
+
json: args.json,
|
|
25
|
+
resume: true,
|
|
26
|
+
},
|
|
27
|
+
rawArgs: [],
|
|
28
|
+
cmd: contextCommand,
|
|
29
|
+
} as any);
|
|
30
|
+
},
|
|
31
|
+
});
|
package/src/cli/index.ts
CHANGED
|
@@ -985,12 +985,14 @@ const main = defineCommand({
|
|
|
985
985
|
args: {
|
|
986
986
|
query: { type: 'positional', description: 'Search query', required: true },
|
|
987
987
|
limit: { type: 'string', description: 'Maximum results' },
|
|
988
|
+
quality: { type: 'string', description: 'Retrieval profile: fast, balanced (default), or thorough' },
|
|
988
989
|
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
989
990
|
},
|
|
990
991
|
async run({ args }) {
|
|
991
992
|
await runMemoryShortcut('search', {
|
|
992
993
|
query: args.query,
|
|
993
994
|
limit: args.limit,
|
|
995
|
+
quality: args.quality,
|
|
994
996
|
json: args.json,
|
|
995
997
|
});
|
|
996
998
|
},
|
|
@@ -1045,6 +1047,7 @@ const main = defineCommand({
|
|
|
1045
1047
|
integrate: () => import('./commands/integrate.js').then(m => m.default),
|
|
1046
1048
|
memory: () => import('./commands/memory.js').then(m => m.default),
|
|
1047
1049
|
context: () => import('./commands/context.js').then(m => m.default),
|
|
1050
|
+
resume: () => import('./commands/resume.js').then(m => m.default),
|
|
1048
1051
|
explain: () => import('./commands/explain.js').then(m => m.default),
|
|
1049
1052
|
codegraph: () => import('./commands/codegraph.js').then(m => m.default),
|
|
1050
1053
|
knowledge: () => import('./commands/knowledge.js').then(m => m.default),
|
|
@@ -1115,7 +1118,7 @@ const main = defineCommand({
|
|
|
1115
1118
|
// Detect by checking if the first CLI arg matches a registered subcommand name.
|
|
1116
1119
|
const firstArg = process.argv[2];
|
|
1117
1120
|
const knownSubs = ['ask', 'search', 'remember', 'recent', 'help', 'workbench', 'memcode', 'config',
|
|
1118
|
-
'init', 'setup', 'integrate', 'memory', 'context', 'explain', 'codegraph', 'knowledge', 'reasoning', 'retention', 'formation', 'audit', 'transfer', 'skills', 'identity',
|
|
1121
|
+
'init', 'setup', 'integrate', 'memory', 'context', 'resume', 'explain', 'codegraph', 'knowledge', 'reasoning', 'retention', 'formation', 'audit', 'transfer', 'skills', 'identity',
|
|
1119
1122
|
'session', 'team', 'task', 'message', 'lock', 'handoff', 'poll',
|
|
1120
1123
|
'receipt',
|
|
1121
1124
|
'serve', 'serve-http', 'status', 'sync',
|
|
@@ -1155,6 +1158,7 @@ const main = defineCommand({
|
|
|
1155
1158
|
console.error(' session Start/end/context for coding sessions');
|
|
1156
1159
|
console.error(' memory Search/store/detail/timeline/resolve observations');
|
|
1157
1160
|
console.error(' context Show the Memory Autopilot brief for this project');
|
|
1161
|
+
console.error(' resume Resume prior work with one bounded project brief');
|
|
1158
1162
|
console.error(' explain Explain where Memorix project context comes from');
|
|
1159
1163
|
console.error(' codegraph Refresh/status/context-pack for CodeGraph Memory');
|
|
1160
1164
|
console.error(' knowledge Review source-backed knowledge pages and project workflows');
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { existsSync, statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { truncateToTokenBudget } from '../compact/token-budget.js';
|
|
3
4
|
import { getResolvedConfig } from '../config/resolved-config.js';
|
|
4
5
|
import { buildTaskWorkset, type TaskWorkset, type WorksetCaution } from '../knowledge/workset.js';
|
|
5
6
|
import type { ContextDeliveryTarget } from '../knowledge/context-assembly.js';
|
|
6
|
-
import
|
|
7
|
+
import { sanitizeCredentials } from '../memory/secret-filter.js';
|
|
8
|
+
import type { ObservationReader, ProjectInfo } from '../types.js';
|
|
7
9
|
import { backfillMissingObservationCodeRefs, type CodeRefBackfillResult } from './binder.js';
|
|
8
10
|
import { collectCurrentProjectFacts, formatGitFact, type CurrentProjectFacts } from './current-facts.js';
|
|
9
11
|
import { refreshProjectLite } from './lite-provider.js';
|
|
@@ -21,7 +23,10 @@ import {
|
|
|
21
23
|
} from './project-context.js';
|
|
22
24
|
import { CodeGraphStore } from './store.js';
|
|
23
25
|
import { isEligibleForAutomaticDelivery } from '../memory/admission.js';
|
|
26
|
+
import { getSessionResumeBrief, type SessionResumeBrief } from '../memory/session.js';
|
|
27
|
+
import { initSessionStore } from '../store/session-store.js';
|
|
24
28
|
import {
|
|
29
|
+
isContinuationTask,
|
|
25
30
|
lensPathCandidates,
|
|
26
31
|
lensVerificationHints,
|
|
27
32
|
rankLensPaths,
|
|
@@ -50,6 +55,8 @@ export interface AutoProjectContext {
|
|
|
50
55
|
explain: ProjectContextExplain;
|
|
51
56
|
refresh: AutoContextRefreshResult;
|
|
52
57
|
providerQuality: CodeGraphProviderQuality;
|
|
58
|
+
/** Present only when the caller asked to continue prior work. */
|
|
59
|
+
continuation?: SessionResumeBrief;
|
|
53
60
|
workset: TaskWorkset;
|
|
54
61
|
}
|
|
55
62
|
|
|
@@ -123,6 +130,10 @@ export async function buildAutoProjectContext(input: {
|
|
|
123
130
|
maxFileBytes?: number;
|
|
124
131
|
/** Test-only injection point; production uses the bounded local runner. */
|
|
125
132
|
externalRunner?: ExternalCodeGraphRunner;
|
|
133
|
+
/** Reader used when continuation retrieval loads session and durable memory evidence. */
|
|
134
|
+
reader?: ObservationReader;
|
|
135
|
+
/** Auto detects continuation language; always is used by the explicit resume path. */
|
|
136
|
+
continuation?: 'auto' | 'always' | 'never';
|
|
126
137
|
/**
|
|
127
138
|
* When supplied, a needed refresh is queued instead of running in this
|
|
128
139
|
* request. MCP and hook callers use this to keep their response path fast.
|
|
@@ -135,6 +146,8 @@ export async function buildAutoProjectContext(input: {
|
|
|
135
146
|
const now = input.now ?? new Date();
|
|
136
147
|
const task = input.task?.trim();
|
|
137
148
|
const lens = resolveTaskLens(task);
|
|
149
|
+
const continuationRequested = input.continuation === 'always'
|
|
150
|
+
|| (input.continuation !== 'never' && isContinuationTask(task));
|
|
138
151
|
const codegraphConfig = getResolvedConfig({ projectRoot: input.project.rootPath }).codegraph;
|
|
139
152
|
const exclude = input.exclude ?? codegraphConfig.excludePatterns;
|
|
140
153
|
const maxFileBytes = input.maxFileBytes ?? codegraphConfig.maxFileBytes;
|
|
@@ -278,6 +291,14 @@ export async function buildAutoProjectContext(input: {
|
|
|
278
291
|
if (externalCaution) {
|
|
279
292
|
runtimeCautions.push({ kind: 'external-codegraph-fallback', message: externalCaution });
|
|
280
293
|
}
|
|
294
|
+
// Project Context is also used by lightweight callers that have not touched
|
|
295
|
+
// session APIs yet. Initialize only when continuation was requested so a
|
|
296
|
+
// normal Workset remains independent of session persistence.
|
|
297
|
+
let continuation: SessionResumeBrief | undefined;
|
|
298
|
+
if (continuationRequested) {
|
|
299
|
+
await initSessionStore(input.dataDir);
|
|
300
|
+
continuation = await getSessionResumeBrief(input.project.id, task, input.reader);
|
|
301
|
+
}
|
|
281
302
|
const workset = await buildTaskWorkset({
|
|
282
303
|
projectId: input.project.id,
|
|
283
304
|
dataDir: input.dataDir,
|
|
@@ -287,6 +308,7 @@ export async function buildAutoProjectContext(input: {
|
|
|
287
308
|
...(externalOutline ? { semanticCode: externalOutline } : {}),
|
|
288
309
|
providerQuality,
|
|
289
310
|
currentFacts: worksetFactLines(currentFacts),
|
|
311
|
+
...(continuation ? { continuation } : {}),
|
|
290
312
|
codeState: codeStateLine(overview),
|
|
291
313
|
reliableMemory: sourceSets.reliableSources
|
|
292
314
|
.slice(0, lens.sourceLimit)
|
|
@@ -340,6 +362,7 @@ export async function buildAutoProjectContext(input: {
|
|
|
340
362
|
explain,
|
|
341
363
|
refresh,
|
|
342
364
|
providerQuality,
|
|
365
|
+
...(continuationRequested && workset.continuation ? { continuation: workset.continuation } : {}),
|
|
343
366
|
workset,
|
|
344
367
|
};
|
|
345
368
|
}
|
|
@@ -350,6 +373,13 @@ function formatLanguages(overview: ProjectContextOverview): string {
|
|
|
350
373
|
: 'none indexed yet';
|
|
351
374
|
}
|
|
352
375
|
|
|
376
|
+
function compactContinuationText(text: string, budget: number): string {
|
|
377
|
+
return truncateToTokenBudget(
|
|
378
|
+
sanitizeCredentials(text).replace(/\s+/g, ' ').trim(),
|
|
379
|
+
budget,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
353
383
|
function codeStateLine(overview: ProjectContextOverview): string {
|
|
354
384
|
const snapshot = overview.code.latestSnapshot;
|
|
355
385
|
if (!snapshot) return '- Code state: no completed snapshot yet';
|
|
@@ -533,6 +563,29 @@ export function formatAutoProjectContextSummary(context: AutoProjectContext): st
|
|
|
533
563
|
: '- none yet',
|
|
534
564
|
);
|
|
535
565
|
|
|
566
|
+
const continuation = context.workset.continuation;
|
|
567
|
+
if (continuation?.previousSession || continuation?.memories.length) {
|
|
568
|
+
lines.push('', 'Resume from prior work');
|
|
569
|
+
if (continuation.previousSession) {
|
|
570
|
+
const session = continuation.previousSession;
|
|
571
|
+
const source = [session.agent, session.endedAt ? session.endedAt.slice(0, 10) : undefined]
|
|
572
|
+
.filter(Boolean)
|
|
573
|
+
.join(', ');
|
|
574
|
+
lines.push(
|
|
575
|
+
'- Previous session' + (source ? ` (${source})` : '') + ': '
|
|
576
|
+
+ compactContinuationText(session.summary, 44),
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
for (const memory of continuation.memories.slice(0, 3)) {
|
|
580
|
+
const detail = memory.detail ? ': ' + compactContinuationText(memory.detail, 20) : '';
|
|
581
|
+
lines.push(
|
|
582
|
+
'- #' + memory.id + ' ' + memory.type + ': '
|
|
583
|
+
+ compactContinuationText(memory.title, 18)
|
|
584
|
+
+ detail,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
536
589
|
return lines.join('\n');
|
|
537
590
|
}
|
|
538
591
|
|
|
@@ -121,6 +121,7 @@ const KEYWORDS: Record<Exclude<TaskLensId, 'general'>, string[]> = {
|
|
|
121
121
|
'fail',
|
|
122
122
|
'failing',
|
|
123
123
|
'fix',
|
|
124
|
+
'fixing',
|
|
124
125
|
'issue',
|
|
125
126
|
'regression',
|
|
126
127
|
'repro',
|
|
@@ -139,6 +140,22 @@ const KEYWORDS: Record<Exclude<TaskLensId, 'general'>, string[]> = {
|
|
|
139
140
|
test: ['coverage', 'fixture', 'smoke', 'spec', 'test', 'tests', 'testing', 'vitest', '测试'],
|
|
140
141
|
};
|
|
141
142
|
|
|
143
|
+
// Continuation is a delivery intent, not a task lens. A request can both resume
|
|
144
|
+
// prior work and be a bugfix, feature, or release task, so callers keep the
|
|
145
|
+
// normal lens and add a bounded prior-work projection separately.
|
|
146
|
+
const CONTINUATION_KEYWORDS = [
|
|
147
|
+
'continue',
|
|
148
|
+
'resume',
|
|
149
|
+
'pick up',
|
|
150
|
+
'carry on',
|
|
151
|
+
'previous session',
|
|
152
|
+
'继续',
|
|
153
|
+
'接手',
|
|
154
|
+
'恢复',
|
|
155
|
+
'延续',
|
|
156
|
+
'上次会话',
|
|
157
|
+
];
|
|
158
|
+
|
|
142
159
|
const LENS_PRIORITY: Exclude<TaskLensId, 'general'>[] = [
|
|
143
160
|
'bugfix',
|
|
144
161
|
'release',
|
|
@@ -229,6 +246,18 @@ export function resolveTaskLens(task?: string): TaskLens {
|
|
|
229
246
|
return best.score > 0 ? LENSES[best.id] : LENSES.general;
|
|
230
247
|
}
|
|
231
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Detect when the caller is asking to continue existing work. This stays
|
|
251
|
+
* separate from task-lens routing so "continue fixing the timeout" remains a
|
|
252
|
+
* bugfix, while still receiving a compact prior-work brief.
|
|
253
|
+
*/
|
|
254
|
+
export function isContinuationTask(task?: string): boolean {
|
|
255
|
+
const normalized = (task ?? '').trim().toLowerCase();
|
|
256
|
+
return normalized.length > 0 && CONTINUATION_KEYWORDS.some((keyword) =>
|
|
257
|
+
containsTaskKeyword(normalized, keyword),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
232
261
|
function pathKindScore(path: string, lens: TaskLens): number {
|
|
233
262
|
const normalized = normalizePath(path).toLowerCase();
|
|
234
263
|
const name = normalized.split('/').pop() ?? normalized;
|
|
@@ -56,7 +56,22 @@ export function truncateToTokenBudget(text: string, budget: number): string {
|
|
|
56
56
|
result = result.slice(0, Math.floor(result.length * 0.9));
|
|
57
57
|
}
|
|
58
58
|
if (result.length < text.length) {
|
|
59
|
-
|
|
59
|
+
// A character estimate can end halfway through a flag, path, or symbol.
|
|
60
|
+
// Drop the incomplete whitespace-delimited token instead of returning
|
|
61
|
+
// misleading fragments such as `AUTH...`.
|
|
62
|
+
const nextCharacter = text.charAt(result.length);
|
|
63
|
+
if (nextCharacter && !/[\s.,;:!?)}\]]/.test(nextCharacter)) {
|
|
64
|
+
const boundary = result.search(/\s+\S*$/);
|
|
65
|
+
result = boundary > 0 ? result.slice(0, boundary).trimEnd() : '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Keep the suffix inside the stated budget when there is room. An empty
|
|
69
|
+
// prefix is more honest than a partial identifier that appears valid.
|
|
70
|
+
while (result && fitsInBudget(result + '...', budget) === false) {
|
|
71
|
+
const boundary = result.lastIndexOf(' ');
|
|
72
|
+
result = boundary > 0 ? result.slice(0, boundary).trimEnd() : '';
|
|
73
|
+
}
|
|
74
|
+
result = result ? result + '...' : '...';
|
|
60
75
|
}
|
|
61
76
|
}
|
|
62
77
|
|
|
@@ -88,6 +88,27 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
|
|
|
88
88
|
const legacy = loadFileConfig();
|
|
89
89
|
const embeddingBaseUrl = first(process.env.MEMORIX_EMBEDDING_BASE_URL, toml.embedding?.base_url, yaml.embedding?.baseUrl, legacy.embeddingApi?.baseUrl);
|
|
90
90
|
const openRouterEmbeddingApiKey = isOpenRouterUrl(embeddingBaseUrl) ? process.env.OPENROUTER_API_KEY : undefined;
|
|
91
|
+
const memoryLlmProvider = first(
|
|
92
|
+
process.env.MEMORIX_LLM_PROVIDER,
|
|
93
|
+
toml.memory?.llm?.provider,
|
|
94
|
+
yaml.llm?.provider,
|
|
95
|
+
legacy.llm?.provider,
|
|
96
|
+
);
|
|
97
|
+
const memoryLlmModel = first(
|
|
98
|
+
process.env.MEMORIX_LLM_MODEL,
|
|
99
|
+
toml.memory?.llm?.model,
|
|
100
|
+
yaml.llm?.model,
|
|
101
|
+
legacy.llm?.model,
|
|
102
|
+
);
|
|
103
|
+
const memoryLlmBaseUrl = first(
|
|
104
|
+
process.env.MEMORIX_LLM_BASE_URL,
|
|
105
|
+
toml.memory?.llm?.base_url,
|
|
106
|
+
yaml.llm?.baseUrl,
|
|
107
|
+
legacy.llm?.baseUrl,
|
|
108
|
+
);
|
|
109
|
+
const openRouterMemoryLlmApiKey = isOpenRouterMemoryLane(memoryLlmProvider, memoryLlmBaseUrl)
|
|
110
|
+
? process.env.OPENROUTER_API_KEY
|
|
111
|
+
: undefined;
|
|
91
112
|
|
|
92
113
|
const resolved: ResolvedMemorixConfig = {
|
|
93
114
|
agent: {
|
|
@@ -126,9 +147,9 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
|
|
|
126
147
|
autoCleanup: firstBool(toml.memory?.auto_cleanup, yaml.behavior?.autoCleanup),
|
|
127
148
|
syncAdvisory: firstBool(toml.memory?.sync_advisory, yaml.behavior?.syncAdvisory),
|
|
128
149
|
llm: {
|
|
129
|
-
provider:
|
|
130
|
-
model:
|
|
131
|
-
baseUrl:
|
|
150
|
+
provider: memoryLlmProvider,
|
|
151
|
+
model: memoryLlmModel,
|
|
152
|
+
baseUrl: memoryLlmBaseUrl,
|
|
132
153
|
apiKey: first(
|
|
133
154
|
process.env.MEMORIX_LLM_API_KEY,
|
|
134
155
|
process.env.MEMORIX_API_KEY,
|
|
@@ -137,7 +158,7 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
|
|
|
137
158
|
legacy.llm?.apiKey,
|
|
138
159
|
process.env.OPENAI_API_KEY,
|
|
139
160
|
process.env.ANTHROPIC_API_KEY,
|
|
140
|
-
|
|
161
|
+
openRouterMemoryLlmApiKey,
|
|
141
162
|
),
|
|
142
163
|
},
|
|
143
164
|
},
|
|
@@ -290,6 +311,10 @@ function isOpenRouterUrl(value: string | undefined): boolean {
|
|
|
290
311
|
}
|
|
291
312
|
}
|
|
292
313
|
|
|
314
|
+
function isOpenRouterMemoryLane(provider: string | undefined, baseUrl: string | undefined): boolean {
|
|
315
|
+
return provider?.trim().toLowerCase() === 'openrouter' || isOpenRouterUrl(baseUrl);
|
|
316
|
+
}
|
|
317
|
+
|
|
293
318
|
function normalizeExternalContext(value: string | undefined): 'auto' | 'off' | undefined {
|
|
294
319
|
const normalized = value?.trim().toLowerCase();
|
|
295
320
|
if (normalized === 'auto' || normalized === 'off') return normalized;
|
|
@@ -144,6 +144,9 @@ function parseTomlValue(raw: string, filePath: string, line: number): unknown {
|
|
|
144
144
|
if (raw.startsWith('"') && raw.endsWith('"')) {
|
|
145
145
|
return raw.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
146
146
|
}
|
|
147
|
+
if (raw.startsWith("'") && raw.endsWith("'")) {
|
|
148
|
+
return raw.slice(1, -1);
|
|
149
|
+
}
|
|
147
150
|
if (raw === 'true') return true;
|
|
148
151
|
if (raw === 'false') return false;
|
|
149
152
|
if (/^-?\d+$/.test(raw)) return Number.parseInt(raw, 10);
|
|
@@ -157,7 +160,7 @@ function parseTomlValue(raw: string, filePath: string, line: number): unknown {
|
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
function stripComment(line: string): string {
|
|
160
|
-
let
|
|
163
|
+
let quote: '"' | "'" | null = null;
|
|
161
164
|
let escaped = false;
|
|
162
165
|
for (let i = 0; i < line.length; i++) {
|
|
163
166
|
const char = line[i];
|
|
@@ -165,15 +168,16 @@ function stripComment(line: string): string {
|
|
|
165
168
|
escaped = false;
|
|
166
169
|
continue;
|
|
167
170
|
}
|
|
168
|
-
if (char === '\\' &&
|
|
171
|
+
if (char === '\\' && quote === '"') {
|
|
169
172
|
escaped = true;
|
|
170
173
|
continue;
|
|
171
174
|
}
|
|
172
|
-
if (char === '"') {
|
|
173
|
-
|
|
175
|
+
if (char === '"' || char === "'") {
|
|
176
|
+
if (quote === char) quote = null;
|
|
177
|
+
else if (quote === null) quote = char;
|
|
174
178
|
continue;
|
|
175
179
|
}
|
|
176
|
-
if (char === '#' &&
|
|
180
|
+
if (char === '#' && quote === null) {
|
|
177
181
|
return line.slice(0, i);
|
|
178
182
|
}
|
|
179
183
|
}
|