memorix 1.6.0 → 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 +24 -0
- package/TEAM.md +86 -86
- package/dist/cli/index.js +317 -62
- package/dist/cli/memcode.js +12 -1
- package/dist/index.js +56 -9
- package/dist/maintenance-runner.js +54 -7
- package/dist/media-audio-runner.js +54 -7
- package/dist/media-video-runner.js +54 -7
- package/dist/memcode-runtime/CHANGELOG.md +24 -0
- package/dist/sdk.js +55 -8
- package/dist/vector-backfill-runner.js +54 -7
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +11 -3
- package/docs/API_REFERENCE.md +1 -1
- package/docs/DESIGN_DECISIONS.md +357 -357
- 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/audit/index.ts +156 -156
- package/src/cli/commands/agent-integrations.ts +9 -3
- package/src/cli/commands/doctor.ts +46 -15
- package/src/cli/commands/formation.ts +48 -48
- package/src/cli/commands/git-hook-install.ts +111 -111
- package/src/cli/commands/handoff.ts +54 -54
- package/src/cli/commands/hook.ts +13 -1
- package/src/cli/commands/hooks-status.ts +63 -63
- package/src/cli/commands/ingest.ts +44 -44
- package/src/cli/commands/integrate-shared.ts +15 -15
- package/src/cli/commands/lock.ts +82 -82
- package/src/cli/commands/message.ts +104 -104
- package/src/cli/commands/poll.ts +58 -58
- package/src/cli/commands/reasoning.ts +118 -118
- package/src/cli/commands/serve-http.ts +68 -7
- package/src/cli/commands/skills.ts +114 -114
- package/src/cli/commands/task.ts +167 -167
- package/src/cli/commands/transfer.ts +47 -47
- package/src/cli/heap.ts +76 -0
- package/src/cli/index.ts +406 -406
- package/src/cli/invocation.ts +7 -4
- package/src/cli/tui/ChatView.tsx +234 -234
- package/src/cli/tui/CommandBar.tsx +312 -312
- package/src/cli/tui/ContextRail.tsx +118 -118
- package/src/cli/tui/HeaderBar.tsx +72 -72
- package/src/cli/tui/LogoBanner.tsx +51 -51
- package/src/cli/tui/index.ts +41 -41
- package/src/cli/version.ts +7 -7
- package/src/cli/workbench.ts +1 -1
- package/src/dashboard/project-classification.ts +64 -64
- package/src/embedding/provider.ts +76 -6
- package/src/git/hooks-path.ts +85 -85
- package/src/hooks/handler.ts +9 -18
- package/src/hooks/lifecycle.ts +173 -0
- package/src/hooks/pattern-detector.ts +173 -173
- package/src/hooks/significance-filter.ts +250 -250
- package/src/llm/memory-manager.ts +328 -328
- package/src/llm/quality.ts +248 -248
- package/src/memory/attribution-guard.ts +249 -249
- package/src/memory/disclosure-policy.ts +135 -135
- package/src/memory/entity-extractor.ts +197 -197
- package/src/memory/formation/evaluate.ts +217 -217
- package/src/memory/freshness.ts +122 -122
- package/src/memory/graph.ts +197 -197
- package/src/memory/refs.ts +94 -94
- package/src/memory/secret-filter.ts +79 -79
- package/src/orchestrate/adapters/claude-stream.ts +192 -192
- package/src/orchestrate/adapters/claude.ts +111 -111
- package/src/orchestrate/adapters/codex-stream.ts +134 -134
- package/src/orchestrate/adapters/codex.ts +41 -41
- package/src/orchestrate/adapters/gemini-stream.ts +166 -166
- package/src/orchestrate/adapters/gemini.ts +42 -42
- package/src/orchestrate/adapters/index.ts +73 -73
- package/src/orchestrate/adapters/opencode-stream.ts +143 -143
- package/src/orchestrate/adapters/opencode.ts +47 -47
- package/src/orchestrate/adapters/types.ts +77 -77
- package/src/orchestrate/capability-router.ts +284 -284
- package/src/orchestrate/cost-tracker.ts +219 -219
- package/src/orchestrate/error-recovery.ts +191 -191
- package/src/orchestrate/evidence.ts +140 -140
- package/src/orchestrate/ledger.ts +110 -110
- package/src/orchestrate/memorix-bridge.ts +343 -343
- package/src/orchestrate/output-budget.ts +80 -80
- package/src/orchestrate/permission.ts +152 -152
- package/src/orchestrate/pipeline-trace.ts +131 -131
- package/src/orchestrate/prompt-builder.ts +155 -155
- package/src/orchestrate/ring-buffer.ts +37 -37
- package/src/orchestrate/task-graph.ts +389 -389
- package/src/project/aliases.ts +374 -374
- package/src/rules/adapters/trae.ts +56 -56
- package/src/rules/syncer.ts +235 -235
- package/src/search/query-expansion.ts +52 -52
- package/src/server/formation-timeout.ts +27 -27
- package/src/skills/mini-skills.ts +386 -386
- package/src/store/chat-store.ts +119 -119
- package/src/store/graph-store.ts +249 -249
- package/src/store/mini-skill-store.ts +349 -349
- package/src/store/persistence-json.ts +212 -212
- package/src/store/persistence.ts +291 -291
- package/src/store/project-affinity.ts +195 -195
- package/src/team/event-bus.ts +76 -76
- package/src/team/handoff.ts +161 -161
- package/src/team/poll.ts +132 -132
- package/src/workspace/mcp-adapters/codex.ts +191 -191
- package/src/workspace/mcp-adapters/copilot.ts +105 -105
- package/src/workspace/mcp-adapters/cursor.ts +53 -53
- package/src/workspace/mcp-adapters/kiro.ts +64 -64
- package/src/workspace/mcp-adapters/opencode.ts +123 -123
- package/src/workspace/mcp-adapters/trae.ts +134 -134
- package/src/workspace/mcp-adapters/windsurf.ts +91 -91
- package/src/workspace/sanitizer.ts +60 -60
package/src/cli/commands/poll.ts
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import { defineCommand } from 'citty';
|
|
2
|
-
import { computePoll, computeWatermark } from '../../team/poll.js';
|
|
3
|
-
import { getObservationStore } from '../../store/obs-store.js';
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { computePoll, computeWatermark } from '../../team/poll.js';
|
|
3
|
+
import { getObservationStore } from '../../store/obs-store.js';
|
|
4
4
|
import { getAllObservations } from '../../memory/observations.js';
|
|
5
5
|
import { withFreshIndex } from '../../memory/freshness.js';
|
|
6
6
|
import { filterReadableObservations } from '../../memory/visibility.js';
|
|
7
7
|
import { emitError, emitResult, getCliProjectContext, resolveCliActorId, shortId } from './operator-shared.js';
|
|
8
|
-
|
|
9
|
-
export default defineCommand({
|
|
10
|
-
meta: {
|
|
11
|
-
name: 'poll',
|
|
12
|
-
description: 'Get a full project coordination snapshot for an agent',
|
|
13
|
-
},
|
|
14
|
-
args: {
|
|
15
|
-
agentId: { type: 'string', description: 'Agent ID for personalized situational awareness' },
|
|
16
|
-
markInboxRead: { type: 'boolean', description: 'Mark all inbox messages as read after returning them' },
|
|
17
|
-
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
18
|
-
},
|
|
19
|
-
run: async ({ args }) => {
|
|
20
|
-
const asJson = !!args.json;
|
|
21
|
-
|
|
22
|
-
try {
|
|
8
|
+
|
|
9
|
+
export default defineCommand({
|
|
10
|
+
meta: {
|
|
11
|
+
name: 'poll',
|
|
12
|
+
description: 'Get a full project coordination snapshot for an agent',
|
|
13
|
+
},
|
|
14
|
+
args: {
|
|
15
|
+
agentId: { type: 'string', description: 'Agent ID for personalized situational awareness' },
|
|
16
|
+
markInboxRead: { type: 'boolean', description: 'Mark all inbox messages as read after returning them' },
|
|
17
|
+
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
18
|
+
},
|
|
19
|
+
run: async ({ args }) => {
|
|
20
|
+
const asJson = !!args.json;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
23
|
const { project, teamStore, identity, reader: activeReader } = await getCliProjectContext();
|
|
24
24
|
const agentId = resolveCliActorId(args.agentId, identity);
|
|
25
25
|
let reader = activeReader;
|
|
26
|
-
|
|
27
|
-
let watermark = computeWatermark(0, 0, 0);
|
|
28
|
-
if (agentId) {
|
|
29
|
-
const agent = teamStore.getAgent(agentId);
|
|
26
|
+
|
|
27
|
+
let watermark = computeWatermark(0, 0, 0);
|
|
28
|
+
if (agentId) {
|
|
29
|
+
const agent = teamStore.getAgent(agentId);
|
|
30
30
|
if (!agent || agent.project_id !== project.id) {
|
|
31
31
|
emitError(`Unknown agent "${agentId}"`, asJson);
|
|
32
32
|
return;
|
|
@@ -36,45 +36,45 @@ export default defineCommand({
|
|
|
36
36
|
agentId,
|
|
37
37
|
isTeamMember: agent.status === 'active',
|
|
38
38
|
};
|
|
39
|
-
|
|
40
|
-
const lastSeen = agent.last_seen_obs_generation;
|
|
41
|
-
const currentGen = getObservationStore().getGeneration();
|
|
42
|
-
const projectObs = await withFreshIndex(() =>
|
|
39
|
+
|
|
40
|
+
const lastSeen = agent.last_seen_obs_generation;
|
|
41
|
+
const currentGen = getObservationStore().getGeneration();
|
|
42
|
+
const projectObs = await withFreshIndex(() =>
|
|
43
43
|
filterReadableObservations(
|
|
44
44
|
getAllObservations().filter(
|
|
45
45
|
(obs) => obs.projectId === project.id && (obs.writeGeneration ?? 0) > lastSeen,
|
|
46
46
|
),
|
|
47
47
|
reader,
|
|
48
48
|
),
|
|
49
|
-
);
|
|
50
|
-
watermark = computeWatermark(lastSeen, currentGen, projectObs.length);
|
|
51
|
-
teamStore.updateWatermark(agentId, currentGen);
|
|
52
|
-
teamStore.heartbeat(agentId);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const poll = computePoll(teamStore, project.id, agentId ?? null, watermark);
|
|
56
|
-
if (args.markInboxRead && agentId) {
|
|
57
|
-
teamStore.markAllRead(project.id, agentId);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const text = [
|
|
61
|
-
poll.agent ? `You: ${shortId(poll.agent.agentId)} (${poll.agent.status})` : 'Project-level overview',
|
|
62
|
-
poll.watermark.newObservationCount > 0
|
|
63
|
-
? `${poll.watermark.newObservationCount} new observation(s)`
|
|
64
|
-
: 'No unseen observations',
|
|
65
|
-
poll.inbox.unreadCount > 0 ? `${poll.inbox.unreadCount} unread message(s)` : 'Inbox clear',
|
|
66
|
-
poll.tasks.myInProgress.length > 0
|
|
67
|
-
? `${poll.tasks.myInProgress.length} in-progress task(s)`
|
|
68
|
-
: 'No in-progress tasks',
|
|
69
|
-
poll.tasks.availableToClaim.length > 0
|
|
70
|
-
? `${poll.tasks.availableToClaim.length} task(s) available to claim`
|
|
71
|
-
: 'No claimable tasks',
|
|
72
|
-
`Team: ${poll.team.activeAgents.length} active / ${poll.team.totalAgents} total`,
|
|
73
|
-
].join('\n');
|
|
74
|
-
|
|
75
|
-
emitResult({ project, poll }, text, asJson);
|
|
76
|
-
} catch (error) {
|
|
77
|
-
emitError(error instanceof Error ? error.message : String(error), asJson);
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
});
|
|
49
|
+
);
|
|
50
|
+
watermark = computeWatermark(lastSeen, currentGen, projectObs.length);
|
|
51
|
+
teamStore.updateWatermark(agentId, currentGen);
|
|
52
|
+
teamStore.heartbeat(agentId);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const poll = computePoll(teamStore, project.id, agentId ?? null, watermark);
|
|
56
|
+
if (args.markInboxRead && agentId) {
|
|
57
|
+
teamStore.markAllRead(project.id, agentId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const text = [
|
|
61
|
+
poll.agent ? `You: ${shortId(poll.agent.agentId)} (${poll.agent.status})` : 'Project-level overview',
|
|
62
|
+
poll.watermark.newObservationCount > 0
|
|
63
|
+
? `${poll.watermark.newObservationCount} new observation(s)`
|
|
64
|
+
: 'No unseen observations',
|
|
65
|
+
poll.inbox.unreadCount > 0 ? `${poll.inbox.unreadCount} unread message(s)` : 'Inbox clear',
|
|
66
|
+
poll.tasks.myInProgress.length > 0
|
|
67
|
+
? `${poll.tasks.myInProgress.length} in-progress task(s)`
|
|
68
|
+
: 'No in-progress tasks',
|
|
69
|
+
poll.tasks.availableToClaim.length > 0
|
|
70
|
+
? `${poll.tasks.availableToClaim.length} task(s) available to claim`
|
|
71
|
+
: 'No claimable tasks',
|
|
72
|
+
`Team: ${poll.team.activeAgents.length} active / ${poll.team.totalAgents} total`,
|
|
73
|
+
].join('\n');
|
|
74
|
+
|
|
75
|
+
emitResult({ project, poll }, text, asJson);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
emitError(error instanceof Error ? error.message : String(error), asJson);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { defineCommand } from 'citty';
|
|
2
|
-
import { compactSearch } from '../../compact/engine.js';
|
|
3
|
-
import { storeObservation } from '../../memory/observations.js';
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { compactSearch } from '../../compact/engine.js';
|
|
3
|
+
import { storeObservation } from '../../memory/observations.js';
|
|
4
4
|
import {
|
|
5
5
|
emitError,
|
|
6
6
|
emitResult,
|
|
@@ -9,65 +9,65 @@ import {
|
|
|
9
9
|
parsePositiveInt,
|
|
10
10
|
resolveCliWriteScope,
|
|
11
11
|
} from './operator-shared.js';
|
|
12
|
-
|
|
13
|
-
export default defineCommand({
|
|
14
|
-
meta: {
|
|
15
|
-
name: 'reasoning',
|
|
16
|
-
description: 'Store and search decision rationale from the operator CLI',
|
|
17
|
-
},
|
|
18
|
-
args: {
|
|
19
|
-
entity: { type: 'string', description: 'Entity this reasoning applies to' },
|
|
20
|
-
decision: { type: 'string', description: 'Decision that was made' },
|
|
21
|
-
rationale: { type: 'string', description: 'Why this approach was chosen' },
|
|
22
|
-
alternatives: { type: 'string', description: 'Comma-separated alternatives that were considered' },
|
|
23
|
-
constraints: { type: 'string', description: 'Comma-separated constraints' },
|
|
24
|
-
expectedOutcome: { type: 'string', description: 'Expected outcome from this decision' },
|
|
25
|
-
risks: { type: 'string', description: 'Comma-separated risks or downsides' },
|
|
26
|
-
concepts: { type: 'string', description: 'Comma-separated related concepts' },
|
|
27
|
-
files: { type: 'string', description: 'Comma-separated file list' },
|
|
28
|
-
relatedCommits: { type: 'string', description: 'Comma-separated related commit hashes' },
|
|
12
|
+
|
|
13
|
+
export default defineCommand({
|
|
14
|
+
meta: {
|
|
15
|
+
name: 'reasoning',
|
|
16
|
+
description: 'Store and search decision rationale from the operator CLI',
|
|
17
|
+
},
|
|
18
|
+
args: {
|
|
19
|
+
entity: { type: 'string', description: 'Entity this reasoning applies to' },
|
|
20
|
+
decision: { type: 'string', description: 'Decision that was made' },
|
|
21
|
+
rationale: { type: 'string', description: 'Why this approach was chosen' },
|
|
22
|
+
alternatives: { type: 'string', description: 'Comma-separated alternatives that were considered' },
|
|
23
|
+
constraints: { type: 'string', description: 'Comma-separated constraints' },
|
|
24
|
+
expectedOutcome: { type: 'string', description: 'Expected outcome from this decision' },
|
|
25
|
+
risks: { type: 'string', description: 'Comma-separated risks or downsides' },
|
|
26
|
+
concepts: { type: 'string', description: 'Comma-separated related concepts' },
|
|
27
|
+
files: { type: 'string', description: 'Comma-separated file list' },
|
|
28
|
+
relatedCommits: { type: 'string', description: 'Comma-separated related commit hashes' },
|
|
29
29
|
relatedEntities: { type: 'string', description: 'Comma-separated related entity names' },
|
|
30
30
|
visibility: { type: 'string', description: 'Reasoning visibility: project (default), personal, or team' },
|
|
31
|
-
query: { type: 'string', description: 'Search query for reasoning traces' },
|
|
32
|
-
limit: { type: 'string', description: 'Search result limit' },
|
|
33
|
-
scope: { type: 'string', description: 'project or global scope for search' },
|
|
34
|
-
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
35
|
-
},
|
|
36
|
-
run: async ({ args }) => {
|
|
37
|
-
const action = (args._ as string[])?.[0] || '';
|
|
38
|
-
const asJson = !!args.json;
|
|
39
|
-
|
|
40
|
-
try {
|
|
31
|
+
query: { type: 'string', description: 'Search query for reasoning traces' },
|
|
32
|
+
limit: { type: 'string', description: 'Search result limit' },
|
|
33
|
+
scope: { type: 'string', description: 'project or global scope for search' },
|
|
34
|
+
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
35
|
+
},
|
|
36
|
+
run: async ({ args }) => {
|
|
37
|
+
const action = (args._ as string[])?.[0] || '';
|
|
38
|
+
const asJson = !!args.json;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
41
|
const { project, reader, identity } = await getCliProjectContext({ searchIndex: action === 'search' });
|
|
42
|
-
|
|
43
|
-
switch (action) {
|
|
44
|
-
case 'store': {
|
|
45
|
-
const entityName = (args.entity as string | undefined)?.trim();
|
|
46
|
-
const decision = (args.decision as string | undefined)?.trim();
|
|
47
|
-
const rationale = (args.rationale as string | undefined)?.trim();
|
|
48
|
-
if (!entityName || !decision || !rationale) {
|
|
49
|
-
emitError('entity, decision, and rationale are required for "memorix reasoning store"', asJson);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const alternatives = parseCsvList(args.alternatives as string | undefined);
|
|
54
|
-
const constraints = parseCsvList(args.constraints as string | undefined);
|
|
55
|
-
const risks = parseCsvList(args.risks as string | undefined);
|
|
56
|
-
const concepts = parseCsvList(args.concepts as string | undefined);
|
|
57
|
-
const filesModified = parseCsvList(args.files as string | undefined);
|
|
58
|
-
const relatedCommits = parseCsvList(args.relatedCommits as string | undefined);
|
|
59
|
-
const relatedEntities = parseCsvList(args.relatedEntities as string | undefined);
|
|
60
|
-
|
|
61
|
-
const narrativeParts: string[] = [rationale];
|
|
62
|
-
if (alternatives.length > 0) {
|
|
63
|
-
narrativeParts.push(`Alternatives considered: ${alternatives.join('; ')}`);
|
|
64
|
-
}
|
|
65
|
-
if (constraints.length > 0) {
|
|
66
|
-
narrativeParts.push(`Constraints: ${constraints.join('; ')}`);
|
|
67
|
-
}
|
|
68
|
-
if (args.expectedOutcome) {
|
|
69
|
-
narrativeParts.push(`Expected outcome: ${String(args.expectedOutcome).trim()}`);
|
|
70
|
-
}
|
|
42
|
+
|
|
43
|
+
switch (action) {
|
|
44
|
+
case 'store': {
|
|
45
|
+
const entityName = (args.entity as string | undefined)?.trim();
|
|
46
|
+
const decision = (args.decision as string | undefined)?.trim();
|
|
47
|
+
const rationale = (args.rationale as string | undefined)?.trim();
|
|
48
|
+
if (!entityName || !decision || !rationale) {
|
|
49
|
+
emitError('entity, decision, and rationale are required for "memorix reasoning store"', asJson);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const alternatives = parseCsvList(args.alternatives as string | undefined);
|
|
54
|
+
const constraints = parseCsvList(args.constraints as string | undefined);
|
|
55
|
+
const risks = parseCsvList(args.risks as string | undefined);
|
|
56
|
+
const concepts = parseCsvList(args.concepts as string | undefined);
|
|
57
|
+
const filesModified = parseCsvList(args.files as string | undefined);
|
|
58
|
+
const relatedCommits = parseCsvList(args.relatedCommits as string | undefined);
|
|
59
|
+
const relatedEntities = parseCsvList(args.relatedEntities as string | undefined);
|
|
60
|
+
|
|
61
|
+
const narrativeParts: string[] = [rationale];
|
|
62
|
+
if (alternatives.length > 0) {
|
|
63
|
+
narrativeParts.push(`Alternatives considered: ${alternatives.join('; ')}`);
|
|
64
|
+
}
|
|
65
|
+
if (constraints.length > 0) {
|
|
66
|
+
narrativeParts.push(`Constraints: ${constraints.join('; ')}`);
|
|
67
|
+
}
|
|
68
|
+
if (args.expectedOutcome) {
|
|
69
|
+
narrativeParts.push(`Expected outcome: ${String(args.expectedOutcome).trim()}`);
|
|
70
|
+
}
|
|
71
71
|
if (risks.length > 0) {
|
|
72
72
|
narrativeParts.push(`Risks: ${risks.join('; ')}`);
|
|
73
73
|
}
|
|
@@ -77,70 +77,70 @@ export default defineCommand({
|
|
|
77
77
|
);
|
|
78
78
|
|
|
79
79
|
const result = await storeObservation({
|
|
80
|
-
entityName,
|
|
81
|
-
type: 'reasoning',
|
|
82
|
-
title: decision,
|
|
83
|
-
narrative: narrativeParts.join('. '),
|
|
84
|
-
facts: [
|
|
85
|
-
`Decision: ${decision}`,
|
|
86
|
-
...alternatives.map((item) => `Alternative: ${item}`),
|
|
87
|
-
...constraints.map((item) => `Constraint: ${item}`),
|
|
88
|
-
...(args.expectedOutcome ? [`Expected outcome: ${String(args.expectedOutcome).trim()}`] : []),
|
|
89
|
-
...risks.map((item) => `Risk: ${item}`),
|
|
90
|
-
...relatedCommits.map((item) => `Commit: ${item}`),
|
|
91
|
-
...relatedEntities.map((item) => `Related entity: ${item}`),
|
|
92
|
-
],
|
|
93
|
-
concepts,
|
|
94
|
-
filesModified,
|
|
95
|
-
relatedCommits,
|
|
96
|
-
relatedEntities,
|
|
80
|
+
entityName,
|
|
81
|
+
type: 'reasoning',
|
|
82
|
+
title: decision,
|
|
83
|
+
narrative: narrativeParts.join('. '),
|
|
84
|
+
facts: [
|
|
85
|
+
`Decision: ${decision}`,
|
|
86
|
+
...alternatives.map((item) => `Alternative: ${item}`),
|
|
87
|
+
...constraints.map((item) => `Constraint: ${item}`),
|
|
88
|
+
...(args.expectedOutcome ? [`Expected outcome: ${String(args.expectedOutcome).trim()}`] : []),
|
|
89
|
+
...risks.map((item) => `Risk: ${item}`),
|
|
90
|
+
...relatedCommits.map((item) => `Commit: ${item}`),
|
|
91
|
+
...relatedEntities.map((item) => `Related entity: ${item}`),
|
|
92
|
+
],
|
|
93
|
+
concepts,
|
|
94
|
+
filesModified,
|
|
95
|
+
relatedCommits,
|
|
96
|
+
relatedEntities,
|
|
97
97
|
projectId: project.id,
|
|
98
98
|
source: 'manual',
|
|
99
99
|
...writeScope,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
emitResult(
|
|
103
|
-
{ project, observation: result.observation, upserted: result.upserted },
|
|
104
|
-
`${result.upserted ? 'Updated' : 'Stored'} reasoning trace #${result.observation.id}: ${decision}`,
|
|
105
|
-
asJson,
|
|
106
|
-
);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
case 'search': {
|
|
111
|
-
const query = (args.query as string | undefined)?.trim();
|
|
112
|
-
if (!query) {
|
|
113
|
-
emitError('query is required for "memorix reasoning search"', asJson);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const scope = (args.scope as string | undefined) === 'global' ? 'global' : 'project';
|
|
117
|
-
const limit = parsePositiveInt(args.limit as string | undefined, 10);
|
|
118
|
-
const result = await compactSearch({
|
|
119
|
-
query,
|
|
120
|
-
limit,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
emitResult(
|
|
103
|
+
{ project, observation: result.observation, upserted: result.upserted },
|
|
104
|
+
`${result.upserted ? 'Updated' : 'Stored'} reasoning trace #${result.observation.id}: ${decision}`,
|
|
105
|
+
asJson,
|
|
106
|
+
);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case 'search': {
|
|
111
|
+
const query = (args.query as string | undefined)?.trim();
|
|
112
|
+
if (!query) {
|
|
113
|
+
emitError('query is required for "memorix reasoning search"', asJson);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const scope = (args.scope as string | undefined) === 'global' ? 'global' : 'project';
|
|
117
|
+
const limit = parsePositiveInt(args.limit as string | undefined, 10);
|
|
118
|
+
const result = await compactSearch({
|
|
119
|
+
query,
|
|
120
|
+
limit,
|
|
121
121
|
type: 'reasoning',
|
|
122
122
|
projectId: scope === 'global' ? undefined : project.id,
|
|
123
123
|
status: 'active',
|
|
124
124
|
reader: scope === 'global' ? {} : reader,
|
|
125
|
-
});
|
|
126
|
-
emitResult(
|
|
127
|
-
{ project, scope, entries: result.entries },
|
|
128
|
-
result.entries.length === 0 ? 'No reasoning traces found.' : `[REASONING] Reasoning traces\n${result.formatted}`,
|
|
129
|
-
asJson,
|
|
130
|
-
);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
default:
|
|
135
|
-
console.log('Memorix Reasoning Commands');
|
|
136
|
-
console.log('');
|
|
137
|
-
console.log('Usage:');
|
|
125
|
+
});
|
|
126
|
+
emitResult(
|
|
127
|
+
{ project, scope, entries: result.entries },
|
|
128
|
+
result.entries.length === 0 ? 'No reasoning traces found.' : `[REASONING] Reasoning traces\n${result.formatted}`,
|
|
129
|
+
asJson,
|
|
130
|
+
);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
default:
|
|
135
|
+
console.log('Memorix Reasoning Commands');
|
|
136
|
+
console.log('');
|
|
137
|
+
console.log('Usage:');
|
|
138
138
|
console.log(' memorix reasoning store --entity auth --decision "Use SQLite" --rationale "..." [--visibility project|personal|team]');
|
|
139
|
-
console.log(' memorix reasoning search --query "why sqlite" [--scope project|global --limit 10]');
|
|
140
|
-
}
|
|
141
|
-
} catch (error) {
|
|
142
|
-
emitError(error instanceof Error ? error.message : String(error), asJson);
|
|
143
|
-
}
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
|
|
139
|
+
console.log(' memorix reasoning search --query "why sqlite" [--scope project|global --limit 10]');
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
emitError(error instanceof Error ? error.message : String(error), asJson);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
@@ -29,13 +29,19 @@ import { canManageObservation, filterReadableObservations } from '../../memory/v
|
|
|
29
29
|
import { parseTcpPortOrReport } from '../port.js';
|
|
30
30
|
import { mcpFileUriToPath } from '../mcp-root-path.js';
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* HTTP MCP is a shared control plane. A normal thinking break should not
|
|
34
|
+
* silently invalidate a client session. Operators can choose a shorter GC
|
|
35
|
+
* window, or set 0 to disable idle GC entirely.
|
|
36
|
+
*/
|
|
37
|
+
export const DEFAULT_SESSION_TIMEOUT_MS = 12 * 60 * 60 * 1000;
|
|
38
|
+
export const EXPIRED_SESSION_TTL_MS = 10 * 60 * 1000;
|
|
33
39
|
|
|
34
40
|
export function parseSessionTimeoutMs(raw: string | undefined): number {
|
|
35
41
|
const value = raw?.trim();
|
|
36
42
|
if (!value) return DEFAULT_SESSION_TIMEOUT_MS;
|
|
37
43
|
const parsed = Number(value);
|
|
38
|
-
if (!Number.isFinite(parsed) || parsed
|
|
44
|
+
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_SESSION_TIMEOUT_MS;
|
|
39
45
|
return Math.floor(parsed);
|
|
40
46
|
}
|
|
41
47
|
|
|
@@ -132,7 +138,11 @@ export default defineCommand({
|
|
|
132
138
|
console.error(`[memorix] HTTP transport starting on ${host}:${port}`);
|
|
133
139
|
console.error(`[memorix] Project root: ${projectRoot}`);
|
|
134
140
|
const sessionTimeoutMs = parseSessionTimeoutMs(process.env.MEMORIX_SESSION_TIMEOUT_MS);
|
|
135
|
-
console.error(
|
|
141
|
+
console.error(
|
|
142
|
+
sessionTimeoutMs === 0
|
|
143
|
+
? '[memorix] HTTP session idle timeout: disabled'
|
|
144
|
+
: `[memorix] HTTP session idle timeout: ${Math.round(sessionTimeoutMs / 60000)}min (${sessionTimeoutMs}ms)`,
|
|
145
|
+
);
|
|
136
146
|
|
|
137
147
|
// Per-project TeamStore cache (Option B) — each dataDir gets its own TeamStore instance.
|
|
138
148
|
// This ensures true isolation between projects in HTTP control-plane mode.
|
|
@@ -166,6 +176,9 @@ export default defineCommand({
|
|
|
166
176
|
|
|
167
177
|
// Session activity tracking (for GC timeout)
|
|
168
178
|
const sessionLastActivity = new Map<string, number>();
|
|
179
|
+
// Keep a short-lived tombstone so an expired client gets an actionable
|
|
180
|
+
// reinitialize response instead of a generic bad request.
|
|
181
|
+
const expiredSessions = new Map<string, number>();
|
|
169
182
|
|
|
170
183
|
// Probe session noise reduction: defer init logs, suppress short-lived sessions
|
|
171
184
|
let suppressedProbeCount = 0;
|
|
@@ -182,6 +195,30 @@ export default defineCommand({
|
|
|
182
195
|
}
|
|
183
196
|
}
|
|
184
197
|
|
|
198
|
+
function forgetExpiredSessions(now = Date.now()): void {
|
|
199
|
+
for (const [sessionId, expiresAt] of expiredSessions) {
|
|
200
|
+
if (expiresAt <= now) expiredSessions.delete(sessionId);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function sendUnknownSession(res: ServerResponse, sessionId: string | undefined, jsonRpc = false): void {
|
|
205
|
+
const expired = Boolean(sessionId && expiredSessions.has(sessionId));
|
|
206
|
+
const message = expired
|
|
207
|
+
? 'MCP session expired after inactivity. Initialize a new session and retry the request.'
|
|
208
|
+
: 'MCP session not found. Initialize a new session and retry the request.';
|
|
209
|
+
if (jsonRpc) {
|
|
210
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
211
|
+
res.end(JSON.stringify({
|
|
212
|
+
jsonrpc: '2.0',
|
|
213
|
+
error: { code: -32001, message },
|
|
214
|
+
id: null,
|
|
215
|
+
}));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
219
|
+
res.end(message);
|
|
220
|
+
}
|
|
221
|
+
|
|
185
222
|
/**
|
|
186
223
|
* Parse JSON body from IncomingMessage
|
|
187
224
|
*/
|
|
@@ -360,6 +397,11 @@ export default defineCommand({
|
|
|
360
397
|
return;
|
|
361
398
|
}
|
|
362
399
|
|
|
400
|
+
if (sessionId) {
|
|
401
|
+
sendUnknownSession(res, sessionId, true);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
363
405
|
if (!sessionId && isInitializeRequest(body)) {
|
|
364
406
|
// New session — create transport + server
|
|
365
407
|
let createdState: SessionState | null = null;
|
|
@@ -488,8 +530,12 @@ export default defineCommand({
|
|
|
488
530
|
async function handleGet(req: IncomingMessage, res: ServerResponse) {
|
|
489
531
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
490
532
|
if (!sessionId || !sessions.has(sessionId)) {
|
|
491
|
-
|
|
492
|
-
|
|
533
|
+
if (!sessionId) {
|
|
534
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
535
|
+
res.end('Missing MCP session ID');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
sendUnknownSession(res, sessionId);
|
|
493
539
|
return;
|
|
494
540
|
}
|
|
495
541
|
|
|
@@ -502,8 +548,12 @@ export default defineCommand({
|
|
|
502
548
|
async function handleDelete(req: IncomingMessage, res: ServerResponse) {
|
|
503
549
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
504
550
|
if (!sessionId || !sessions.has(sessionId)) {
|
|
505
|
-
|
|
506
|
-
|
|
551
|
+
if (!sessionId) {
|
|
552
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
553
|
+
res.end('Missing MCP session ID');
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
sendUnknownSession(res, sessionId);
|
|
507
557
|
return;
|
|
508
558
|
}
|
|
509
559
|
|
|
@@ -1414,6 +1464,7 @@ export default defineCommand({
|
|
|
1414
1464
|
// Lightweight health check — responds immediately, no heavy init required.
|
|
1415
1465
|
// This is the readiness signal for background start / status.
|
|
1416
1466
|
if (url.pathname === '/health') {
|
|
1467
|
+
const { getEmbeddingRuntimeHealth } = await import('../../embedding/provider.js');
|
|
1417
1468
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1418
1469
|
res.end(JSON.stringify({
|
|
1419
1470
|
status: 'ok',
|
|
@@ -1421,6 +1472,7 @@ export default defineCommand({
|
|
|
1421
1472
|
port,
|
|
1422
1473
|
uptime: Math.round(process.uptime()),
|
|
1423
1474
|
pid: process.pid,
|
|
1475
|
+
embedding: getEmbeddingRuntimeHealth(),
|
|
1424
1476
|
}));
|
|
1425
1477
|
return;
|
|
1426
1478
|
}
|
|
@@ -1491,6 +1543,12 @@ export default defineCommand({
|
|
|
1491
1543
|
await serveDashStatic(req, res);
|
|
1492
1544
|
});
|
|
1493
1545
|
|
|
1546
|
+
// MCP session continuity is independent from one TCP socket. Keep Node's
|
|
1547
|
+
// socket timeout above its 5s default while the session map remains the
|
|
1548
|
+
// actual continuity boundary.
|
|
1549
|
+
httpServer.keepAliveTimeout = 60_000;
|
|
1550
|
+
httpServer.headersTimeout = 65_000;
|
|
1551
|
+
|
|
1494
1552
|
httpServer.listen(port, host, () => {
|
|
1495
1553
|
// Write readiness file — background start polls this for out-of-band readiness detection
|
|
1496
1554
|
try {
|
|
@@ -1521,10 +1579,13 @@ export default defineCommand({
|
|
|
1521
1579
|
const gcInterval = setInterval(() => {
|
|
1522
1580
|
maybeProbeSummary(); // Flush suppressed probe count periodically
|
|
1523
1581
|
const now = Date.now();
|
|
1582
|
+
forgetExpiredSessions(now);
|
|
1583
|
+
if (SESSION_TIMEOUT_MS === 0) return;
|
|
1524
1584
|
for (const [sid, state] of sessions) {
|
|
1525
1585
|
const lastActive = sessionLastActivity.get(sid) ?? 0;
|
|
1526
1586
|
if (now - lastActive > SESSION_TIMEOUT_MS) {
|
|
1527
1587
|
console.error(`[memorix] Session ${sid.slice(0, 8)}… timed out (idle ${Math.round((now - lastActive) / 60000)}min), closing`);
|
|
1588
|
+
expiredSessions.set(sid, now + EXPIRED_SESSION_TTL_MS);
|
|
1528
1589
|
state.transport.close().catch(() => {});
|
|
1529
1590
|
sessions.delete(sid);
|
|
1530
1591
|
sessionLastActivity.delete(sid);
|